app: add upgrade configuration to settings page
This commit is contained in:
parent
d087e46bd1
commit
e76abac24e
|
|
@ -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,13 @@ func main() {
|
|||
ToolRegistry: toolRegistry,
|
||||
Dev: devMode,
|
||||
Logger: slog.Default(),
|
||||
Updater: upd,
|
||||
UpdateAvailableFunc: func() {
|
||||
UpdateAvailable("")
|
||||
},
|
||||
ClearUpdateAvailableFunc: func() {
|
||||
ClearUpdateAvailable()
|
||||
},
|
||||
}
|
||||
|
||||
srv := &http.Server{
|
||||
|
|
@ -284,8 +293,7 @@ func main() {
|
|||
slog.Debug("background desktop server done")
|
||||
}()
|
||||
|
||||
updater := &updater.Updater{Store: st}
|
||||
updater.StartBackgroundUpdaterChecker(ctx, UpdateAvailable)
|
||||
upd.StartBackgroundUpdaterChecker(ctx, UpdateAvailable)
|
||||
|
||||
hasCompletedFirstRun, err := st.HasCompletedFirstRun()
|
||||
if err != nil {
|
||||
|
|
@ -348,6 +356,16 @@ func startHiddenTasks() {
|
|||
// CLI triggered app startup use-case
|
||||
slog.Info("deferring pending update for fast startup")
|
||||
} else {
|
||||
// Check if auto-update is enabled before 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")
|
||||
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
|
||||
|
|
|
|||
|
|
@ -172,6 +172,14 @@ func UpdateAvailable(ver string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func ClearUpdateAvailable() error {
|
||||
slog.Debug("clearing update notification")
|
||||
if updater.BundlePath != "" {
|
||||
C.clearUpdateAvailable()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func osRun(_ func(), hasCompletedFirstRun, startHidden bool) {
|
||||
registerLaunchAgent(hasCompletedFirstRun)
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ void StartUpdate();
|
|||
void darwinStartHiddenTasks();
|
||||
void launchApp(const char *appPath);
|
||||
void updateAvailable();
|
||||
void clearUpdateAvailable();
|
||||
void quit();
|
||||
void uiRequest(char *path);
|
||||
void registerSelfAsLoginItem(bool firstTimeRun);
|
||||
|
|
|
|||
|
|
@ -241,6 +241,12 @@ bool firstTimeRun,startHidden; // Set in run before initialization
|
|||
[self showIcon];
|
||||
}
|
||||
|
||||
- (void)clearUpdateAvailable {
|
||||
self.updateAvailable = NO;
|
||||
[self.statusItem.menu.itemArray[3] setHidden:YES];
|
||||
[self.statusItem.menu.itemArray[4] setHidden:YES];
|
||||
}
|
||||
|
||||
- (void)aboutOllama {
|
||||
[[NSApplication sharedApplication] orderFrontStandardAboutPanel:nil];
|
||||
[NSApp activateIgnoringOtherApps:YES];
|
||||
|
|
@ -973,6 +979,12 @@ void updateAvailable() {
|
|||
});
|
||||
}
|
||||
|
||||
void clearUpdateAvailable() {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[appDelegate clearUpdateAvailable];
|
||||
});
|
||||
}
|
||||
|
||||
void quit() {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[appDelegate quit];
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import (
|
|||
|
||||
// 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(`
|
||||
|
|
@ -1108,9 +1130,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 +1143,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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -414,3 +414,68 @@ export async function fetchHealth(): Promise<boolean> {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCurrentVersion(): Promise<string> {
|
||||
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 downloadUpdate(version: string): Promise<void> {
|
||||
const response = await fetch(`${API_BASE}/api/v1/update/download`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ version }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(error || "Failed to download update");
|
||||
}
|
||||
}
|
||||
|
||||
export async function installUpdate(): Promise<void> {
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
<div className="relative overflow-hidden rounded-xl bg-white dark:bg-neutral-800">
|
||||
<div className="space-y-4 p-4">
|
||||
{/* Auto Update */}
|
||||
<Field>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex items-start space-x-3 flex-1">
|
||||
<ArrowDownTrayIcon className="mt-1 h-5 w-5 flex-shrink-0 text-black dark:text-neutral-100" />
|
||||
<div className="flex-1">
|
||||
<Label>Auto-download updates</Label>
|
||||
<Description>
|
||||
{settings.AutoUpdateEnabled ? (
|
||||
<>
|
||||
Automatically downloads updates and restarts the app.
|
||||
<div className="mt-2 text-xs text-zinc-600 dark:text-zinc-400">
|
||||
Current version: {updateInfo?.currentVersion || "Loading..."}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
You must manually check for updates.
|
||||
<div className="mt-3 p-3 bg-zinc-50 dark:bg-zinc-900 rounded-lg border border-zinc-200 dark:border-zinc-800">
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-zinc-600 dark:text-zinc-400">Current version: {updateInfo?.currentVersion || "Loading..."}</span>
|
||||
</div>
|
||||
{updateInfo?.availableVersion && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-zinc-600 dark:text-zinc-400">Available version: {updateInfo?.availableVersion}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<a
|
||||
href="https://ollama.com/download"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mt-3 inline-block text-sm text-neutral-600 dark:text-neutral-400 underline"
|
||||
>
|
||||
Download new version →
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Description>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<Switch
|
||||
checked={settings.AutoUpdateEnabled}
|
||||
onChange={(checked) => handleChange("AutoUpdateEnabled", checked)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Field>
|
||||
|
||||
{/* Expose Ollama */}
|
||||
<Field>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
|
|
|
|||
139
app/ui/ui.go
139
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()
|
||||
ClearUpdateAvailableFunc func()
|
||||
}
|
||||
|
||||
// UpdaterInterface defines the methods we need from the updater
|
||||
type UpdaterInterface interface {
|
||||
CheckForUpdate(ctx context.Context) (bool, string, error)
|
||||
DownloadUpdate(ctx context.Context, updateVersion string) error
|
||||
InstallAndRestart() error
|
||||
}
|
||||
|
||||
func (s *Server) log() *slog.Logger {
|
||||
|
|
@ -250,7 +263,7 @@ func (s *Server) Handler() http.Handler {
|
|||
}()
|
||||
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("X-Version", version.Version)
|
||||
w.Header().Set("X-Version", version.GetVersion())
|
||||
w.Header().Set("X-Request-ID", requestID)
|
||||
|
||||
ctx := r.Context()
|
||||
|
|
@ -284,6 +297,9 @@ 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/download", handle(s.downloadUpdate))
|
||||
mux.Handle("POST /api/v1/update/install", handle(s.installUpdate))
|
||||
|
||||
// Ollama proxy endpoints
|
||||
ollamaProxy := s.ollamaProxy()
|
||||
|
|
@ -1448,6 +1464,19 @@ func (s *Server) settings(w http.ResponseWriter, r *http.Request) error {
|
|||
return fmt.Errorf("failed to save settings: %w", err)
|
||||
}
|
||||
|
||||
// Update tray notification based on auto-update toggle
|
||||
if old.AutoUpdateEnabled && !settings.AutoUpdateEnabled {
|
||||
// Auto-update disabled: clear tray notification
|
||||
if s.ClearUpdateAvailableFunc != nil {
|
||||
s.ClearUpdateAvailableFunc()
|
||||
}
|
||||
} else if !old.AutoUpdateEnabled && settings.AutoUpdateEnabled {
|
||||
// Auto-update enabled: show tray notification if update is pending
|
||||
if (updater.IsUpdatePending() || updater.UpdateDownloaded) && s.UpdateAvailableFunc != nil {
|
||||
s.UpdateAvailableFunc()
|
||||
}
|
||||
}
|
||||
|
||||
if old.ContextLength != settings.ContextLength ||
|
||||
old.Models != settings.Models ||
|
||||
old.Expose != settings.Expose {
|
||||
|
|
@ -1524,6 +1553,114 @@ 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 {
|
||||
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
|
||||
}
|
||||
|
||||
// Get current version with fallbacks for development
|
||||
currentVersion := version.GetVersion()
|
||||
|
||||
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) downloadUpdate(w http.ResponseWriter, r *http.Request) error {
|
||||
if r.Method != "POST" {
|
||||
return fmt.Errorf("method not allowed")
|
||||
}
|
||||
|
||||
if s.Updater == nil {
|
||||
return fmt.Errorf("updater not available")
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return fmt.Errorf("invalid request body: %w", err)
|
||||
}
|
||||
|
||||
if req.Version == "" {
|
||||
return fmt.Errorf("version is required")
|
||||
}
|
||||
|
||||
err := s.Updater.DownloadUpdate(r.Context(), req.Version)
|
||||
if err != nil {
|
||||
s.log().Error("failed to download update", "error", err, "version", req.Version)
|
||||
return fmt.Errorf("failed to download update: %w", err)
|
||||
}
|
||||
|
||||
response := map[string]any{
|
||||
"success": true,
|
||||
"message": "Update downloaded successfully",
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
// Check if we can actually upgrade (not in dev mode)
|
||||
if updater.BundlePath == "" {
|
||||
return fmt.Errorf("cannot install updates in development mode")
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
|
|
|
|||
|
|
@ -58,9 +58,12 @@ 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.GetVersion()
|
||||
query.Add("version", currentVersion)
|
||||
query.Add("ts", strconv.FormatInt(time.Now().Unix(), 10))
|
||||
|
||||
slog.Debug("checking for update", "current_version", currentVersion, "os", runtime.GOOS, "arch", runtime.GOARCH)
|
||||
|
||||
// The original macOS app used to use the device ID
|
||||
// to check for updates so include it if present
|
||||
if runtime.GOOS == "darwin" {
|
||||
|
|
@ -94,7 +97,7 @@ func (u *Updater) checkForUpdate(ctx context.Context) (bool, UpdateResponse) {
|
|||
if signature != "" {
|
||||
req.Header.Set("Authorization", signature)
|
||||
}
|
||||
ua := fmt.Sprintf("ollama/%s %s Go/%s %s", version.Version, runtime.GOARCH, runtime.Version(), UserAgentOS)
|
||||
ua := fmt.Sprintf("ollama/%s %s Go/%s %s", version.GetVersion(), runtime.GOARCH, runtime.Version(), UserAgentOS)
|
||||
req.Header.Set("User-Agent", ua)
|
||||
|
||||
slog.Debug("checking for available update", "requestURL", requestURL, "User-Agent", ua)
|
||||
|
|
@ -176,6 +179,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
|
||||
}
|
||||
|
||||
|
|
@ -253,6 +257,22 @@ func (u *Updater) StartBackgroundUpdaterChecker(ctx context.Context, cb func(str
|
|||
time.Sleep(UpdateCheckInitialDelay)
|
||||
slog.Info("beginning update checker", "interval", UpdateCheckInterval)
|
||||
for {
|
||||
// Check if auto-update is enabled
|
||||
settings, err := u.Store.Settings()
|
||||
if err != nil {
|
||||
slog.Error("failed to load settings", "error", err)
|
||||
time.Sleep(UpdateCheckInterval)
|
||||
continue
|
||||
}
|
||||
|
||||
if !settings.AutoUpdateEnabled {
|
||||
// When auto-update is disabled, don't check or download anything
|
||||
slog.Debug("auto-update disabled, skipping check")
|
||||
time.Sleep(UpdateCheckInterval)
|
||||
continue
|
||||
}
|
||||
|
||||
// Auto-update is enabled - proceed with normal flow
|
||||
available, resp := u.checkForUpdate(ctx)
|
||||
if available {
|
||||
err := u.DownloadNewRelease(ctx, resp)
|
||||
|
|
@ -275,3 +295,39 @@ func (u *Updater) StartBackgroundUpdaterChecker(ctx context.Context, cb func(str
|
|||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (u *Updater) CheckForUpdate(ctx context.Context) (bool, string, error) {
|
||||
available, resp := u.checkForUpdate(ctx)
|
||||
return available, resp.UpdateVersion, nil
|
||||
}
|
||||
|
||||
func (u *Updater) DownloadUpdate(ctx context.Context, updateVersion string) error {
|
||||
// First check for update to get the actual download URL
|
||||
available, updateResp := u.checkForUpdate(ctx)
|
||||
|
||||
if !available {
|
||||
return fmt.Errorf("no update available")
|
||||
}
|
||||
if updateResp.UpdateVersion != updateVersion {
|
||||
slog.Error("version mismatch", "requested", updateVersion, "available", updateResp.UpdateVersion)
|
||||
return fmt.Errorf("version mismatch: requested %s, available %s", updateVersion, updateResp.UpdateVersion)
|
||||
}
|
||||
|
||||
slog.Info("downloading update", "version", updateVersion)
|
||||
err := u.DownloadNewRelease(ctx, updateResp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
slog.Info("update downloaded successfully", "version", updateVersion)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Updater) InstallAndRestart() error {
|
||||
if !UpdateDownloaded {
|
||||
return fmt.Errorf("no update downloaded")
|
||||
}
|
||||
|
||||
slog.Info("installing update and restarting")
|
||||
return DoUpgrade(true)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,4 +2,39 @@
|
|||
|
||||
package version
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var Version string = "0.0.0"
|
||||
|
||||
// GetVersion returns the version, with fallback to git or build info
|
||||
func GetVersion() string {
|
||||
// If version is set via ldflags, use it
|
||||
if Version != "" && Version != "0.0.0" {
|
||||
return Version
|
||||
}
|
||||
|
||||
// Try to get from build info
|
||||
if buildinfo, ok := debug.ReadBuildInfo(); ok {
|
||||
if buildinfo.Main.Version != "" && buildinfo.Main.Version != "(devel)" {
|
||||
return buildinfo.Main.Version
|
||||
}
|
||||
}
|
||||
|
||||
// In development, try to get from git
|
||||
if cmd := exec.Command("git", "describe", "--tags", "--first-parent", "--abbrev=7", "--long", "--dirty", "--always"); cmd != nil {
|
||||
if output, err := cmd.Output(); err == nil {
|
||||
version := strings.TrimSpace(string(output))
|
||||
version = strings.TrimPrefix(version, "v")
|
||||
if version != "" {
|
||||
return version
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback
|
||||
return "dev"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,34 @@ func (t *winTray) initMenus() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (t *winTray) ClearUpdateAvailable() error {
|
||||
if t.updateNotified {
|
||||
slog.Debug("clearing update notification and menu items")
|
||||
if err := t.removeMenuItem(updateSeparatorMenuID, 0); err != nil {
|
||||
return fmt.Errorf("unable to remove menu entries %w", err)
|
||||
}
|
||||
if err := t.removeMenuItem(updateAvailableMenuID, 0); err != nil {
|
||||
return fmt.Errorf("unable to remove menu entries %w", err)
|
||||
}
|
||||
if err := t.removeMenuItem(updateMenuID, 0); err != nil {
|
||||
return fmt.Errorf("unable to remove menu entries %w", err)
|
||||
}
|
||||
if err := t.removeMenuItem(separatorMenuID, 0); err != nil {
|
||||
return fmt.Errorf("unable to remove menu entries %w", err)
|
||||
}
|
||||
iconFilePath, err := iconBytesToFilePath(wt.normalIcon)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to write icon data to temp file: %w", err)
|
||||
}
|
||||
if err := t.setIcon(iconFilePath); err != nil {
|
||||
return fmt.Errorf("unable to set icon: %w", err)
|
||||
}
|
||||
t.updateNotified = false
|
||||
t.pendingUpdate = false
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *winTray) UpdateAvailable(ver string) error {
|
||||
if !t.updateNotified {
|
||||
slog.Debug("updating menu and sending notification for new update")
|
||||
|
|
|
|||
|
|
@ -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{}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Reference in New Issue