Files
sanspie d436311f69 Stage 11i4: signed self-updates for Aego and exported games (D362)
update core: signed release.json (Ed25519) per channel, ETag check, resumable download with sha256, Auto/Ask/Off policy, appliers for the install.sh layout, AppImage, NSIS installer (keeps the replaced setup.exe for Restore) and .app, trial run with rollback.
Editor and launcher: banner, status bar, Help commands, Task Center download, install on restart.
CLI: aego update, aego release keygen|manifest, aego export --release-json.
Games: modules/update with update { url publicKey }, settings and the aego/update script API.
release.sh refuses to publish without AEGO_RELEASE_KEY and AEGO_UPDATE_KEY; install.sh lays out versions; make test-update runs a real v1 -> v2 update.
2026-10-26 19:36:15 +03:00

199 lines
6.2 KiB
Go

package shell
import (
"time"
"aego/editor/core/command"
"aego/editor/core/context"
"aego/editor/core/sysopen"
"aego/editor/core/tasks"
"aego/editor/core/updateui"
"aego/engine/i18n"
"aego/update"
)
const (
CommandCheckUpdates = "help.updates.check"
CommandRestoreVersion = "help.updates.restore"
CommandUpdatesNightly = "help.updates.nightly"
commandUpdatesMode = "help.updates.mode"
)
// tickUpdates lets the update service check on time, shows its download in the Task Center and says when an update was rolled back.
func (a *App) tickUpdates() {
u := a.opt.Updates
if u == nil {
return
}
u.Tick(time.Now())
st := u.State()
if st.RolledBack != "" && !a.rollbackSaid {
a.rollbackSaid = true
a.ctx.Notify(context.LevelWarn, i18n.Note("editor.updates.rolled_back", "Aego {version} stopped right after it started, so the previous version was restored", i18n.Str("version", st.RolledBack)))
}
if st.Phase == updateui.Downloading && a.updateTask == nil && a.tasks != nil {
a.updateTask = a.tasks.Work(tasks.Spec{Title: i18n.Tr("editor.updates.task", "Downloading Aego {version}", i18n.Str("version", st.Version)), Kind: "update"}, func(t *tasks.Task) error {
for {
s := u.State()
if s.Total > 0 {
t.SetProgress(float32(s.Done) / float32(s.Total))
}
if s.Phase != updateui.Downloading {
return nil
}
time.Sleep(200 * time.Millisecond)
}
})
}
if st.Phase != updateui.Downloading {
a.updateTask = nil
}
}
// updateStatus is the status bar's word on an update: empty when there is nothing to say.
func (a *App) updateStatus() string {
u := a.opt.Updates
if u == nil {
return ""
}
st := u.State()
if st.Hidden {
return ""
}
switch st.Phase {
case updateui.Available:
return i18n.Tr("editor.updates.available", "Aego {version} is available", i18n.Str("version", st.Version))
case updateui.Ready:
return i18n.Tr("editor.updates.ready", "Aego {version} downloaded, restart to update", i18n.Str("version", st.Version))
}
return ""
}
// askUpdate offers the downloaded version: Restart to Update, Later, Skip This Version, Release Notes.
func (a *App) askUpdate() {
u := a.opt.Updates
st := u.State()
restart := i18n.Tr("editor.updates.restart", "Restart to Update")
if st.Phase == updateui.Available {
restart = i18n.Tr("editor.updates.download", "Download")
if !st.Installable {
restart = i18n.Tr("editor.updates.page", "Get It")
}
}
buttons := []string{restart, i18n.Tr("editor.updates.later", "Later"), i18n.Tr("editor.updates.skip", "Skip This Version")}
msg := st.Notes
if msg == "" {
msg = i18n.Tr("editor.updates.no_notes", "The release has no notes.")
}
a.ctx.Ask(context.Question{
Title: i18n.Tr("editor.updates.title", "Aego {version}", i18n.Str("version", st.Version)),
Message: msg,
Buttons: buttons,
Then: func(choice int) {
switch choice {
case 0:
switch {
case st.Phase == updateui.Ready:
a.restartForUpdate(true)
case st.Installable:
u.Download()
default:
if err := sysopen.Open(u.Page()); err != nil {
a.ctx.Notify(context.LevelError, err.Error())
}
}
case 1:
u.Later()
case 2:
u.Skip()
}
},
})
}
// restartForUpdate installs the downloaded version, or goes back to the previous one, and opens this project again on it; unsaved work is asked about first.
func (a *App) restartForUpdate(install bool) {
u := a.opt.Updates
do := func() {
relaunch := true
var err error
if install {
relaunch, err = u.Install()
} else {
relaunch, err = u.Restore()
}
if err != nil {
a.ctx.Notify(context.LevelError, i18n.Note("editor.updates.failed", "Cannot update Aego: {error}", i18n.Str("error", err.Error())))
return
}
if !relaunch {
a.quitting = true
a.eng.Quit()
return
}
root := ""
if a.ctx.Project != nil {
root = a.ctx.Project.Root
}
a.leave(root, "")
}
if !a.ctx.Docs.Dirty() {
do()
return
}
a.ctx.Ask(context.Question{
Title: i18n.Tr("editor.shell.unsaved_changes", "Unsaved changes"),
Message: i18n.Tr("editor.updates.save_first", "Save the changed scenes and scripts before Aego restarts?"),
Buttons: []string{i18n.Tr("editor.shell.save_and_continue", "Save and Continue"), i18n.Tr("editor.shell.discard", "Discard"), i18n.Tr("editor.shell.cancel", "Cancel")},
Then: func(choice int) {
switch choice {
case 0:
if err := a.saveAll(); err != nil {
a.ctx.Notify(context.LevelError, err.Error())
return
}
do()
case 1:
do()
}
},
})
}
// updateCommands are Help's update choices: check now, the mode, the nightly channel and going back to the previous version.
func (a *App) updateCommands() []command.Desc {
u := a.opt.Updates
if u == nil {
return nil
}
mode := func(m update.Mode) (func() bool, func() error) {
return func() bool { return u.Policy().Mode == m }, func() error { u.SetMode(m); return nil }
}
autoOn, autoRun := mode(update.Auto)
askOn, askRun := mode(update.Ask)
offOn, offRun := mode(update.Off)
return []command.Desc{
{ID: CommandCheckUpdates, Name: "Check for Updates", Category: "Help", Order: 880, Run: func() error {
u.Check()
a.ctx.Notify(context.LevelInfo, i18n.Note("editor.updates.checking", "Checking for updates"))
return nil
}},
{ID: CommandRestoreVersion, Name: "Restore Previous Version", Category: "Help", Order: 885, Enabled: u.CanRestore,
Tip: "Go back to the version this one updated from", Run: func() error { a.restartForUpdate(false); return nil }},
{ID: CommandUpdatesNightly, Name: "Nightly Updates", Category: "Help", Order: 889,
Tip: "Update to a build of every master commit instead of stable versions",
Checked: func() bool { return u.Policy().Channel == update.Nightly },
Run: func() error {
c := update.Nightly
if u.Policy().Channel == update.Nightly {
c = update.Stable
}
u.SetChannel(c)
return nil
}},
{ID: commandUpdatesMode + ".auto", Name: "Install Updates on Restart", Category: "Help", Order: 886, Checked: autoOn, Run: autoRun},
{ID: commandUpdatesMode + ".ask", Name: "Ask Before Downloading Updates", Category: "Help", Order: 887, Checked: askOn, Run: askRun},
{ID: commandUpdatesMode + ".off", Name: "Check for Updates Only When Asked", Category: "Help", Order: 888, Checked: offOn, Run: offRun},
}
}