diff --git a/app/cmd/app/app.go b/app/cmd/app/app.go index 7e183b8df..904807fd7 100644 --- a/app/cmd/app/app.go +++ b/app/cmd/app/app.go @@ -253,6 +253,8 @@ func main() { done <- osrv.Run(octx) }() + upd := &updater.Updater{Store: st} + uiServer := ui.Server{ Token: token, Restart: func() { @@ -267,6 +269,10 @@ func main() { ToolRegistry: toolRegistry, Dev: devMode, Logger: slog.Default(), + Updater: upd, + UpdateAvailableFunc: func() { + UpdateAvailable("") + }, } srv := &http.Server{ @@ -284,8 +290,13 @@ func main() { slog.Debug("background desktop server done") }() - updater := &updater.Updater{Store: st} - updater.StartBackgroundUpdaterChecker(ctx, UpdateAvailable) + upd.StartBackgroundUpdaterChecker(ctx, UpdateAvailable) + + // Check for pending updates on startup (show tray notification if update is ready) + if updater.IsUpdatePending() { + slog.Debug("update pending on startup, showing tray notification") + UpdateAvailable("") + } hasCompletedFirstRun, err := st.HasCompletedFirstRun() if err != nil { @@ -348,6 +359,18 @@ func startHiddenTasks() { // CLI triggered app startup use-case slog.Info("deferring pending update for fast startup") } else { + // Check if auto-update is enabled before automatically upgrading + st := &store.Store{} + settings, err := st.Settings() + if err != nil { + slog.Warn("failed to load settings for upgrade check", "error", err) + } else if !settings.AutoUpdateEnabled { + slog.Info("auto-update disabled, skipping automatic upgrade at startup") + // Still show tray notification so user knows update is ready + UpdateAvailable("") + return + } + if err := updater.DoUpgradeAtStartup(); err != nil { slog.Info("unable to perform upgrade at startup", "error", err) // Make sure the restart to upgrade menu shows so we can attempt an interactive upgrade to get authorization diff --git a/app/cmd/app/app_windows.go b/app/cmd/app/app_windows.go index 9caeb178a..76af39f19 100644 --- a/app/cmd/app/app_windows.go +++ b/app/cmd/app/app_windows.go @@ -157,6 +157,10 @@ func UpdateAvailable(ver string) error { return app.t.UpdateAvailable(ver) } +func ClearUpdateAvailable() error { + return app.t.ClearUpdateAvailable() +} + func osRun(shutdown func(), hasCompletedFirstRun, startHidden bool) { var err error app.shutdown = shutdown diff --git a/app/store/database.go b/app/store/database.go index 0f268c6fa..b0b1155c2 100644 --- a/app/store/database.go +++ b/app/store/database.go @@ -9,12 +9,12 @@ import ( "strings" "time" - sqlite3 "github.com/mattn/go-sqlite3" + _ "github.com/mattn/go-sqlite3" ) // currentSchemaVersion defines the current database schema version. // Increment this when making schema changes that require migrations. -const currentSchemaVersion = 12 +const currentSchemaVersion = 13 // database wraps the SQLite connection. // SQLite handles its own locking for concurrent access: @@ -85,6 +85,7 @@ func (db *database) init() error { think_enabled BOOLEAN NOT NULL DEFAULT 0, think_level TEXT NOT NULL DEFAULT '', remote TEXT NOT NULL DEFAULT '', -- deprecated + auto_update_enabled BOOLEAN NOT NULL DEFAULT 1, schema_version INTEGER NOT NULL DEFAULT %d ); @@ -244,6 +245,12 @@ func (db *database) migrate() error { return fmt.Errorf("migrate v11 to v12: %w", err) } version = 12 + case 12: + // add auto_update_enabled column to settings table + if err := db.migrateV12ToV13(); err != nil { + return fmt.Errorf("migrate v12 to v13: %w", err) + } + version = 13 default: // If we have a version we don't recognize, just set it to current // This might happen during development @@ -452,6 +459,21 @@ func (db *database) migrateV11ToV12() error { return nil } +// migrateV12ToV13 adds the auto_update_enabled column to the settings table +func (db *database) migrateV12ToV13() error { + _, err := db.conn.Exec(`ALTER TABLE settings ADD COLUMN auto_update_enabled BOOLEAN NOT NULL DEFAULT 1`) + if err != nil && !duplicateColumnError(err) { + return fmt.Errorf("add auto_update_enabled column: %w", err) + } + + _, err = db.conn.Exec(`UPDATE settings SET schema_version = 13`) + if err != nil { + return fmt.Errorf("update schema version: %w", err) + } + + return nil +} + // cleanupOrphanedData removes orphaned records that may exist due to the foreign key bug func (db *database) cleanupOrphanedData() error { _, err := db.conn.Exec(` @@ -482,19 +504,11 @@ func (db *database) cleanupOrphanedData() error { } func duplicateColumnError(err error) bool { - if sqlite3Err, ok := err.(sqlite3.Error); ok { - return sqlite3Err.Code == sqlite3.ErrError && - strings.Contains(sqlite3Err.Error(), "duplicate column name") - } - return false + return err != nil && strings.Contains(err.Error(), "duplicate column name") } func columnNotExists(err error) bool { - if sqlite3Err, ok := err.(sqlite3.Error); ok { - return sqlite3Err.Code == sqlite3.ErrError && - strings.Contains(sqlite3Err.Error(), "no such column") - } - return false + return err != nil && strings.Contains(err.Error(), "no such column") } func (db *database) getAllChats() ([]Chat, error) { @@ -1108,9 +1122,9 @@ func (db *database) getSettings() (Settings, error) { var s Settings err := db.conn.QueryRow(` - SELECT expose, survey, browser, models, agent, tools, working_dir, context_length, airplane_mode, turbo_enabled, websearch_enabled, selected_model, sidebar_open, think_enabled, think_level + SELECT expose, survey, browser, models, agent, tools, working_dir, context_length, airplane_mode, turbo_enabled, websearch_enabled, selected_model, sidebar_open, think_enabled, think_level, auto_update_enabled FROM settings - `).Scan(&s.Expose, &s.Survey, &s.Browser, &s.Models, &s.Agent, &s.Tools, &s.WorkingDir, &s.ContextLength, &s.AirplaneMode, &s.TurboEnabled, &s.WebSearchEnabled, &s.SelectedModel, &s.SidebarOpen, &s.ThinkEnabled, &s.ThinkLevel) + `).Scan(&s.Expose, &s.Survey, &s.Browser, &s.Models, &s.Agent, &s.Tools, &s.WorkingDir, &s.ContextLength, &s.AirplaneMode, &s.TurboEnabled, &s.WebSearchEnabled, &s.SelectedModel, &s.SidebarOpen, &s.ThinkEnabled, &s.ThinkLevel, &s.AutoUpdateEnabled) if err != nil { return Settings{}, fmt.Errorf("get settings: %w", err) } @@ -1121,8 +1135,8 @@ func (db *database) getSettings() (Settings, error) { func (db *database) setSettings(s Settings) error { _, err := db.conn.Exec(` UPDATE settings - SET expose = ?, survey = ?, browser = ?, models = ?, agent = ?, tools = ?, working_dir = ?, context_length = ?, airplane_mode = ?, turbo_enabled = ?, websearch_enabled = ?, selected_model = ?, sidebar_open = ?, think_enabled = ?, think_level = ? - `, s.Expose, s.Survey, s.Browser, s.Models, s.Agent, s.Tools, s.WorkingDir, s.ContextLength, s.AirplaneMode, s.TurboEnabled, s.WebSearchEnabled, s.SelectedModel, s.SidebarOpen, s.ThinkEnabled, s.ThinkLevel) + SET expose = ?, survey = ?, browser = ?, models = ?, agent = ?, tools = ?, working_dir = ?, context_length = ?, airplane_mode = ?, turbo_enabled = ?, websearch_enabled = ?, selected_model = ?, sidebar_open = ?, think_enabled = ?, think_level = ?, auto_update_enabled = ? + `, s.Expose, s.Survey, s.Browser, s.Models, s.Agent, s.Tools, s.WorkingDir, s.ContextLength, s.AirplaneMode, s.TurboEnabled, s.WebSearchEnabled, s.SelectedModel, s.SidebarOpen, s.ThinkEnabled, s.ThinkLevel, s.AutoUpdateEnabled) if err != nil { return fmt.Errorf("set settings: %w", err) } diff --git a/app/store/store.go b/app/store/store.go index 052fcd617..ac3111dd5 100644 --- a/app/store/store.go +++ b/app/store/store.go @@ -169,6 +169,9 @@ type Settings struct { // SidebarOpen indicates if the chat sidebar is open SidebarOpen bool + + // AutoUpdateEnabled indicates if automatic updates should be downloaded + AutoUpdateEnabled bool } type Store struct { diff --git a/app/ui/app/codegen/gotypes.gen.ts b/app/ui/app/codegen/gotypes.gen.ts index 0bf86f2b4..347e2c157 100644 --- a/app/ui/app/codegen/gotypes.gen.ts +++ b/app/ui/app/codegen/gotypes.gen.ts @@ -413,6 +413,7 @@ export class Settings { ThinkLevel: string; SelectedModel: string; SidebarOpen: boolean; + AutoUpdateEnabled: boolean; constructor(source: any = {}) { if ('string' === typeof source) source = JSON.parse(source); @@ -431,6 +432,7 @@ export class Settings { this.ThinkLevel = source["ThinkLevel"]; this.SelectedModel = source["SelectedModel"]; this.SidebarOpen = source["SidebarOpen"]; + this.AutoUpdateEnabled = source["AutoUpdateEnabled"]; } } export class SettingsResponse { @@ -467,6 +469,46 @@ export class HealthResponse { this.healthy = source["healthy"]; } } +export class UpdateInfo { + currentVersion: string; + availableVersion: string; + updateAvailable: boolean; + updateDownloaded: boolean; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.currentVersion = source["currentVersion"]; + this.availableVersion = source["availableVersion"]; + this.updateAvailable = source["updateAvailable"]; + this.updateDownloaded = source["updateDownloaded"]; + } +} +export class UpdateCheckResponse { + updateInfo: UpdateInfo; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.updateInfo = this.convertValues(source["updateInfo"], UpdateInfo); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (Array.isArray(a)) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } +} export class User { id: string; email: string; diff --git a/app/ui/app/src/api.ts b/app/ui/app/src/api.ts index 273850d6b..689051f9b 100644 --- a/app/ui/app/src/api.ts +++ b/app/ui/app/src/api.ts @@ -414,3 +414,54 @@ export async function fetchHealth(): Promise { return false; } } + +export async function getCurrentVersion(): Promise { + try { + const response = await fetch(`${API_BASE}/api/version`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + if (response.ok) { + const data = await response.json(); + return data.version || "Unknown"; + } + return "Unknown"; + } catch (error) { + console.error("Error fetching version:", error); + return "Unknown"; + } +} + +export async function checkForUpdate(): Promise<{ + currentVersion: string; + availableVersion: string; + updateAvailable: boolean; + updateDownloaded: boolean; +}> { + const response = await fetch(`${API_BASE}/api/v1/update/check`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + if (!response.ok) { + throw new Error("Failed to check for update"); + } + const data = await response.json(); + return data.updateInfo; +} + +export async function installUpdate(): Promise { + const response = await fetch(`${API_BASE}/api/v1/update/install`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }); + if (!response.ok) { + const error = await response.text(); + throw new Error(error || "Failed to install update"); + } +} diff --git a/app/ui/app/src/components/Settings.tsx b/app/ui/app/src/components/Settings.tsx index 057f7477d..c3ae9b82d 100644 --- a/app/ui/app/src/components/Settings.tsx +++ b/app/ui/app/src/components/Settings.tsx @@ -14,12 +14,13 @@ import { XMarkIcon, CogIcon, ArrowLeftIcon, + ArrowDownTrayIcon, } from "@heroicons/react/20/solid"; import { Settings as SettingsType } from "@/gotypes"; import { useNavigate } from "@tanstack/react-router"; import { useUser } from "@/hooks/useUser"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { getSettings, updateSettings } from "@/api"; +import { getSettings, updateSettings, checkForUpdate } from "@/api"; function AnimatedDots() { return ( @@ -39,6 +40,12 @@ export default function Settings() { const queryClient = useQueryClient(); const [showSaved, setShowSaved] = useState(false); const [restartMessage, setRestartMessage] = useState(false); + const [updateInfo, setUpdateInfo] = useState<{ + currentVersion: string; + availableVersion: string; + updateAvailable: boolean; + updateDownloaded: boolean; + } | null>(null); const { user, isAuthenticated, @@ -76,8 +83,22 @@ export default function Settings() { useEffect(() => { refetchUser(); + // Check for updates + checkForUpdate() + .then(setUpdateInfo) + .catch((err) => console.error("Error checking for update:", err)); }, []); // eslint-disable-line react-hooks/exhaustive-deps + // Refresh update info when auto-update toggle changes + useEffect(() => { + if (settings?.AutoUpdateEnabled !== undefined) { + checkForUpdate() + .then(setUpdateInfo) + .catch((err) => console.error("Error checking for update:", err)); + } + }, [settings?.AutoUpdateEnabled]); + + useEffect(() => { const handleFocus = () => { if (isAwaitingConnection && pollingInterval) { @@ -344,6 +365,58 @@ export default function Settings() { {/* Local Configuration */}
+ {/* Auto Update */} + +
+
+ +
+ + + {settings.AutoUpdateEnabled ? ( + <> + Automatically downloads updates when available. +
+ Current version: {updateInfo?.currentVersion || "Loading..."} +
+ + ) : ( + <> + Manually download updates. +
+
+
+ Current version: {updateInfo?.currentVersion || "Loading..."} +
+ {updateInfo?.availableVersion && ( +
+ Available version: {updateInfo?.availableVersion} +
+ )} +
+ + Download new version → + +
+ + )} +
+
+
+
+ handleChange("AutoUpdateEnabled", checked)} + /> +
+
+
+ {/* Expose Ollama */}
diff --git a/app/ui/responses/types.go b/app/ui/responses/types.go index 2da6623fd..edbf08049 100644 --- a/app/ui/responses/types.go +++ b/app/ui/responses/types.go @@ -100,6 +100,17 @@ type HealthResponse struct { Healthy bool `json:"healthy"` } +type UpdateInfo struct { + CurrentVersion string `json:"currentVersion"` + AvailableVersion string `json:"availableVersion"` + UpdateAvailable bool `json:"updateAvailable"` + UpdateDownloaded bool `json:"updateDownloaded"` +} + +type UpdateCheckResponse struct { + UpdateInfo UpdateInfo `json:"updateInfo"` +} + type User struct { ID string `json:"id"` Email string `json:"email"` diff --git a/app/ui/ui.go b/app/ui/ui.go index 0b32f917e..88b2d5608 100644 --- a/app/ui/ui.go +++ b/app/ui/ui.go @@ -28,6 +28,7 @@ import ( "github.com/ollama/ollama/app/tools" "github.com/ollama/ollama/app/types/not" "github.com/ollama/ollama/app/ui/responses" + "github.com/ollama/ollama/app/updater" "github.com/ollama/ollama/app/version" ollamaAuth "github.com/ollama/ollama/auth" "github.com/ollama/ollama/envconfig" @@ -106,6 +107,18 @@ type Server struct { // Dev is true if the server is running in development mode Dev bool + + // Updater for checking and downloading updates + Updater UpdaterInterface + UpdateAvailableFunc func() +} + +// UpdaterInterface defines the methods we need from the updater +type UpdaterInterface interface { + CheckForUpdate(ctx context.Context) (bool, string, error) + InstallAndRestart() error + CancelOngoingDownload() + TriggerImmediateCheck() } func (s *Server) log() *slog.Logger { @@ -284,6 +297,8 @@ func (s *Server) Handler() http.Handler { mux.Handle("POST /api/v1/model/upstream", handle(s.modelUpstream)) mux.Handle("GET /api/v1/settings", handle(s.getSettings)) mux.Handle("POST /api/v1/settings", handle(s.settings)) + mux.Handle("GET /api/v1/update/check", handle(s.checkForUpdate)) + mux.Handle("POST /api/v1/update/install", handle(s.installUpdate)) // Ollama proxy endpoints ollamaProxy := s.ollamaProxy() @@ -1448,6 +1463,24 @@ func (s *Server) settings(w http.ResponseWriter, r *http.Request) error { return fmt.Errorf("failed to save settings: %w", err) } + // Handle auto-update toggle changes + if old.AutoUpdateEnabled != settings.AutoUpdateEnabled { + if !settings.AutoUpdateEnabled { + // Auto-update disabled: cancel any ongoing download + if s.Updater != nil { + s.Updater.CancelOngoingDownload() + } + } else { + // Auto-update re-enabled: show notification if update is already staged, or trigger immediate check + if (updater.IsUpdatePending() || updater.UpdateDownloaded) && s.UpdateAvailableFunc != nil { + s.UpdateAvailableFunc() + } else if s.Updater != nil { + // Trigger the background checker to run immediately + s.Updater.TriggerImmediateCheck() + } + } + } + if old.ContextLength != settings.ContextLength || old.Models != settings.Models || old.Expose != settings.Expose { @@ -1524,6 +1557,73 @@ func (s *Server) modelUpstream(w http.ResponseWriter, r *http.Request) error { return json.NewEncoder(w).Encode(response) } +func (s *Server) checkForUpdate(w http.ResponseWriter, r *http.Request) error { + currentVersion := version.Version + + if s.Updater == nil { + return fmt.Errorf("updater not available") + } + + updateAvailable, updateVersion, err := s.Updater.CheckForUpdate(r.Context()) + if err != nil { + s.log().Warn("failed to check for update", "error", err) + // Don't return error, just log it and continue with no update available + } + + response := responses.UpdateCheckResponse{ + UpdateInfo: responses.UpdateInfo{ + CurrentVersion: currentVersion, + AvailableVersion: updateVersion, + UpdateAvailable: updateAvailable, + UpdateDownloaded: updater.UpdateDownloaded, + }, + } + + w.Header().Set("Content-Type", "application/json") + return json.NewEncoder(w).Encode(response) +} + +func (s *Server) installUpdate(w http.ResponseWriter, r *http.Request) error { + if r.Method != "POST" { + return fmt.Errorf("method not allowed") + } + + if s.Updater == nil { + s.log().Error("install failed: updater not available") + return fmt.Errorf("updater not available") + } + + // Check if update is downloaded + if !updater.UpdateDownloaded { + s.log().Error("install failed: no update downloaded") + return fmt.Errorf("no update downloaded") + } + + // Send response before restarting + response := map[string]any{ + "success": true, + "message": "Installing update and restarting...", + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(response); err != nil { + return err + } + + // Give the response time to be sent + time.Sleep(500 * time.Millisecond) + + // Trigger the upgrade and restart + go func() { + time.Sleep(500 * time.Millisecond) + if err := s.Updater.InstallAndRestart(); err != nil { + s.log().Error("failed to install update", "error", err) + } + }() + + return nil +} + func userAgent() string { buildinfo, _ := debug.ReadBuildInfo() diff --git a/app/updater/updater.go b/app/updater/updater.go index 473ecf466..c5a2f2efa 100644 --- a/app/updater/updater.go +++ b/app/updater/updater.go @@ -19,6 +19,7 @@ import ( "runtime" "strconv" "strings" + "sync" "time" "github.com/ollama/ollama/app/store" @@ -58,7 +59,8 @@ func (u *Updater) checkForUpdate(ctx context.Context) (bool, UpdateResponse) { query := requestURL.Query() query.Add("os", runtime.GOOS) query.Add("arch", runtime.GOARCH) - query.Add("version", version.Version) + currentVersion := version.Version + query.Add("version", currentVersion) query.Add("ts", strconv.FormatInt(time.Now().Unix(), 10)) // The original macOS app used to use the device ID @@ -131,15 +133,27 @@ func (u *Updater) checkForUpdate(ctx context.Context) (bool, UpdateResponse) { } func (u *Updater) DownloadNewRelease(ctx context.Context, updateResp UpdateResponse) error { + // Create a cancellable context for this download + downloadCtx, cancel := context.WithCancel(ctx) + u.cancelDownloadLock.Lock() + u.cancelDownload = cancel + u.cancelDownloadLock.Unlock() + defer func() { + u.cancelDownloadLock.Lock() + u.cancelDownload = nil + u.cancelDownloadLock.Unlock() + cancel() + }() + // Do a head first to check etag info - req, err := http.NewRequestWithContext(ctx, http.MethodHead, updateResp.UpdateURL, nil) + req, err := http.NewRequestWithContext(downloadCtx, http.MethodHead, updateResp.UpdateURL, nil) if err != nil { return err } // In case of slow downloads, continue the update check in the background - bgctx, cancel := context.WithCancel(ctx) - defer cancel() + bgctx, bgcancel := context.WithCancel(downloadCtx) + defer bgcancel() go func() { for { select { @@ -176,6 +190,7 @@ func (u *Updater) DownloadNewRelease(ctx context.Context, updateResp UpdateRespo _, err = os.Stat(stageFilename) if err == nil { slog.Info("update already downloaded", "bundle", stageFilename) + UpdateDownloaded = true return nil } @@ -244,34 +259,95 @@ func cleanupOldDownloads(stageDir string) { } type Updater struct { - Store *store.Store + Store *store.Store + cancelDownload context.CancelFunc + cancelDownloadLock sync.Mutex + checkNow chan struct{} +} + +// CancelOngoingDownload cancels any currently running download +func (u *Updater) CancelOngoingDownload() { + u.cancelDownloadLock.Lock() + defer u.cancelDownloadLock.Unlock() + if u.cancelDownload != nil { + slog.Info("cancelling ongoing update download") + u.cancelDownload() + u.cancelDownload = nil + } +} + +// TriggerImmediateCheck signals the background checker to check for updates immediately +func (u *Updater) TriggerImmediateCheck() { + if u.checkNow != nil { + u.checkNow <- struct{}{} + } } func (u *Updater) StartBackgroundUpdaterChecker(ctx context.Context, cb func(string) error) { + u.checkNow = make(chan struct{}, 1) go func() { // Don't blast an update message immediately after startup time.Sleep(UpdateCheckInitialDelay) slog.Info("beginning update checker", "interval", UpdateCheckInterval) + ticker := time.NewTicker(UpdateCheckInterval) + defer ticker.Stop() + for { - available, resp := u.checkForUpdate(ctx) - if available { - err := u.DownloadNewRelease(ctx, resp) - if err != nil { - slog.Error(fmt.Sprintf("failed to download new release: %s", err)) - } else { - err = cb(resp.UpdateVersion) - if err != nil { - slog.Warn(fmt.Sprintf("failed to register update available with tray: %s", err)) - } - } - } select { case <-ctx.Done(): slog.Debug("stopping background update checker") return - default: - time.Sleep(UpdateCheckInterval) + case <-u.checkNow: + // Immediate check triggered + case <-ticker.C: + // Regular interval check + } + + // Always check for updates + available, resp := u.checkForUpdate(ctx) + if !available { + continue + } + + // Update is available - check if auto-update is enabled for downloading + settings, err := u.Store.Settings() + if err != nil { + slog.Error("failed to load settings", "error", err) + continue + } + + if !settings.AutoUpdateEnabled { + // Auto-update disabled - don't download, just log + slog.Debug("update available but auto-update disabled", "version", resp.UpdateVersion) + continue + } + + // Auto-update is enabled - download + err = u.DownloadNewRelease(ctx, resp) + if err != nil { + slog.Error("failed to download new release", "error", err) + continue + } + + // Download successful - show tray notification (regardless of toggle state) + err = cb(resp.UpdateVersion) + if err != nil { + slog.Warn("failed to register update available with tray", "error", err) } } }() } + +func (u *Updater) CheckForUpdate(ctx context.Context) (bool, string, error) { + available, resp := u.checkForUpdate(ctx) + return available, resp.UpdateVersion, nil +} + +func (u *Updater) InstallAndRestart() error { + if !UpdateDownloaded { + return fmt.Errorf("no update downloaded") + } + + slog.Info("installing update and restarting") + return DoUpgrade(true) +} diff --git a/app/updater/updater_test.go b/app/updater/updater_test.go index dea820c28..60fab9ad1 100644 --- a/app/updater/updater_test.go +++ b/app/updater/updater_test.go @@ -85,7 +85,17 @@ func TestBackgoundChecker(t *testing.T) { UpdateCheckURLBase = server.URL + "/update.json" updater := &Updater{Store: &store.Store{}} - defer updater.Store.Close() // Ensure database is closed + defer updater.Store.Close() + + settings, err := updater.Store.Settings() + if err != nil { + t.Fatal(err) + } + settings.AutoUpdateEnabled = true + if err := updater.Store.SetSettings(settings); err != nil { + t.Fatal(err) + } + updater.StartBackgroundUpdaterChecker(ctx, cb) select { case <-stallTimer.C: diff --git a/app/wintray/tray.go b/app/wintray/tray.go index 71d4bc767..0c1a2ed0f 100644 --- a/app/wintray/tray.go +++ b/app/wintray/tray.go @@ -369,24 +369,24 @@ func (t *winTray) addSeparatorMenuItem(menuItemId, parentId uint32) error { return nil } -// func (t *winTray) hideMenuItem(menuItemId, parentId uint32) error { -// const ERROR_SUCCESS syscall.Errno = 0 +func (t *winTray) removeMenuItem(menuItemId, parentId uint32) error { + const ERROR_SUCCESS syscall.Errno = 0 -// t.muMenus.RLock() -// menu := uintptr(t.menus[parentId]) -// t.muMenus.RUnlock() -// res, _, err := pRemoveMenu.Call( -// menu, -// uintptr(menuItemId), -// MF_BYCOMMAND, -// ) -// if res == 0 && err.(syscall.Errno) != ERROR_SUCCESS { -// return err -// } -// t.delFromVisibleItems(parentId, menuItemId) + t.muMenus.RLock() + menu := uintptr(t.menus[parentId]) + t.muMenus.RUnlock() + res, _, err := pRemoveMenu.Call( + menu, + uintptr(menuItemId), + MF_BYCOMMAND, + ) + if res == 0 && err.(syscall.Errno) != ERROR_SUCCESS { + return err + } + t.delFromVisibleItems(parentId, menuItemId) -// return nil -// } + return nil +} func (t *winTray) showMenu() error { p := point{} diff --git a/app/wintray/w32api.go b/app/wintray/w32api.go index 10e255816..792813a34 100644 --- a/app/wintray/w32api.go +++ b/app/wintray/w32api.go @@ -30,6 +30,7 @@ var ( pPostQuitMessage = u32.NewProc("PostQuitMessage") pRegisterClass = u32.NewProc("RegisterClassExW") pRegisterWindowMessage = u32.NewProc("RegisterWindowMessageW") + pRemoveMenu = u32.NewProc("RemoveMenu") pSendMessage = u32.NewProc("SendMessageW") pSetForegroundWindow = u32.NewProc("SetForegroundWindow") pSetMenuInfo = u32.NewProc("SetMenuInfo")