mirror of
https://github.com/ollama/ollama.git
synced 2025-11-12 18:57:27 +01:00
* app: add code for macOS and Windows apps under 'app' * app: add readme * app: windows and linux only for now * ci: fix ui CI validation --------- Co-authored-by: jmorganca <jmorganca@gmail.com>
239 lines
7.1 KiB
Go
239 lines
7.1 KiB
Go
package updater
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
var runningInstaller string
|
|
|
|
type OSVERSIONINFOEXW struct {
|
|
dwOSVersionInfoSize uint32
|
|
dwMajorVersion uint32
|
|
dwMinorVersion uint32
|
|
dwBuildNumber uint32
|
|
dwPlatformId uint32
|
|
szCSDVersion [128]uint16
|
|
wServicePackMajor uint16
|
|
wServicePackMinor uint16
|
|
wSuiteMask uint16
|
|
wProductType uint8
|
|
wReserved uint8
|
|
}
|
|
|
|
func init() {
|
|
VerifyDownload = verifyDownload
|
|
Installer = "Ollama-darwin.zip"
|
|
localAppData := os.Getenv("LOCALAPPDATA")
|
|
appDataDir := filepath.Join(localAppData, "Ollama")
|
|
|
|
// Use a distinct update staging directory from the old desktop app
|
|
// to avoid double upgrades on the transition
|
|
UpdateStageDir = filepath.Join(appDataDir, "updates_v2")
|
|
|
|
UpgradeLogFile = filepath.Join(appDataDir, "upgrade.log")
|
|
Installer = "OllamaSetup.exe"
|
|
runningInstaller = filepath.Join(appDataDir, Installer)
|
|
UpgradeMarkerFile = filepath.Join(appDataDir, "upgraded")
|
|
|
|
loadOSVersion()
|
|
}
|
|
|
|
func loadOSVersion() {
|
|
UserAgentOS = "Windows"
|
|
verInfo := OSVERSIONINFOEXW{}
|
|
verInfo.dwOSVersionInfoSize = (uint32)(unsafe.Sizeof(verInfo))
|
|
ntdll, err := windows.LoadDLL("ntdll.dll")
|
|
if err != nil {
|
|
slog.Warn("unable to find ntdll", "error", err)
|
|
return
|
|
}
|
|
defer ntdll.Release()
|
|
pRtlGetVersion, err := ntdll.FindProc("RtlGetVersion")
|
|
if err != nil {
|
|
slog.Warn("unable to locate RtlGetVersion", "error", err)
|
|
return
|
|
}
|
|
status, _, err := pRtlGetVersion.Call(uintptr(unsafe.Pointer(&verInfo)))
|
|
if status < 0x80000000 { // Success or Informational
|
|
// Note: Windows 11 reports 10.0.22000 or newer
|
|
UserAgentOS = fmt.Sprintf("Windows/%d.%d.%d", verInfo.dwMajorVersion, verInfo.dwMinorVersion, verInfo.dwBuildNumber)
|
|
} else {
|
|
slog.Warn("unable to get OS version", "error", err)
|
|
}
|
|
}
|
|
|
|
func getStagedUpdate() string {
|
|
// When transitioning from old to new app, cleanup the update from the old staging dir
|
|
// This can eventually be removed once enough time has passed since the transition
|
|
cleanupOldDownloads(filepath.Join(os.Getenv("LOCALAPPDATA"), "Ollama", "updates"))
|
|
|
|
files, err := filepath.Glob(filepath.Join(UpdateStageDir, "*", "*.exe"))
|
|
if err != nil {
|
|
slog.Debug("failed to lookup downloads", "error", err)
|
|
return ""
|
|
}
|
|
if len(files) == 0 {
|
|
return ""
|
|
} else if len(files) > 1 {
|
|
// Shouldn't happen
|
|
slog.Warn("multiple update downloads found, using first one", "bundles", files)
|
|
}
|
|
return files[0]
|
|
}
|
|
|
|
func DoUpgrade(interactive bool) error {
|
|
bundle := getStagedUpdate()
|
|
if bundle == "" {
|
|
return fmt.Errorf("failed to lookup downloads")
|
|
}
|
|
|
|
// We move the installer to ensure we don't race with multiple apps starting in quick succession
|
|
if err := os.Rename(bundle, runningInstaller); err != nil {
|
|
return fmt.Errorf("unable to rename %s -> %s : %w", bundle, runningInstaller, err)
|
|
}
|
|
|
|
slog.Info("upgrade log file " + UpgradeLogFile)
|
|
|
|
// make the upgrade show progress, but non interactive
|
|
installArgs := []string{
|
|
"/CLOSEAPPLICATIONS", // Quit the tray app if it's still running
|
|
"/LOG=" + filepath.Base(UpgradeLogFile), // Only relative seems reliable, so set pwd
|
|
"/FORCECLOSEAPPLICATIONS", // Force close the tray app - might be needed
|
|
"/SP", // Skip the "This will install... Do you wish to continue" prompt
|
|
"/NOCANCEL", // Disable the ability to cancel upgrade mid-flight to avoid partially installed upgrades
|
|
"/SILENT",
|
|
}
|
|
|
|
if !interactive {
|
|
// Add flags to make it totally silent without GUI
|
|
installArgs = append(installArgs, "/VERYSILENT", "/SUPPRESSMSGBOXES")
|
|
}
|
|
|
|
slog.Info("starting upgrade", "installer", runningInstaller, "args", installArgs)
|
|
os.Chdir(filepath.Dir(UpgradeLogFile)) //nolint:errcheck
|
|
cmd := exec.Command(runningInstaller, installArgs...)
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
return fmt.Errorf("unable to start ollama app %w", err)
|
|
}
|
|
|
|
if cmd.Process != nil {
|
|
err := cmd.Process.Release()
|
|
if err != nil {
|
|
slog.Error(fmt.Sprintf("failed to release server process: %s", err))
|
|
}
|
|
} else {
|
|
// TODO - some details about why it didn't start, or is this a pedantic error case?
|
|
return errors.New("installer process did not start")
|
|
}
|
|
|
|
// If the install fails to upgrade the system, and leaves a functional
|
|
// app, this marker file will cause us to remove the staged upgrade
|
|
// bundle, which will prevent trying again until we download again.
|
|
// If this becomes looping a problem, we may need to look for failures
|
|
// in the upgrade log in DoPostUpgradeCleanup and then not download
|
|
// the same version again.
|
|
f, err := os.OpenFile(UpgradeMarkerFile, os.O_RDONLY|os.O_CREATE, 0o666)
|
|
if err != nil {
|
|
slog.Warn("unable to create marker file", "file", UpgradeMarkerFile, "error", err)
|
|
}
|
|
f.Close()
|
|
|
|
// TODO should we linger for a moment and check to make sure it's actually running by checking the pid?
|
|
|
|
slog.Info("Installer started in background, exiting")
|
|
|
|
os.Exit(0)
|
|
// Not reached
|
|
return nil
|
|
}
|
|
|
|
func DoPostUpgradeCleanup() error {
|
|
cleanupOldDownloads(UpdateStageDir)
|
|
err := os.Remove(UpgradeMarkerFile)
|
|
if err != nil {
|
|
slog.Warn("unable to clean up marker file", "marker", UpgradeMarkerFile, "error", err)
|
|
}
|
|
err = os.Remove(runningInstaller)
|
|
if err != nil {
|
|
slog.Debug("failed to remove running installer on first attempt, backgrounding...", "installer", runningInstaller, "error", err)
|
|
go func() {
|
|
for range 10 {
|
|
time.Sleep(5 * time.Second)
|
|
if err := os.Remove(runningInstaller); err == nil {
|
|
slog.Debug("installer cleaned up")
|
|
return
|
|
}
|
|
slog.Debug("failed to remove running installer on background attempt", "installer", runningInstaller, "error", err)
|
|
}
|
|
}()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func verifyDownload() error {
|
|
return nil
|
|
}
|
|
|
|
func IsUpdatePending() bool {
|
|
return getStagedUpdate() != ""
|
|
}
|
|
|
|
func DoUpgradeAtStartup() error {
|
|
return DoUpgrade(false)
|
|
}
|
|
|
|
func isInstallerRunning() bool {
|
|
return len(IsProcRunning(Installer)) > 0
|
|
}
|
|
|
|
func IsProcRunning(procName string) []uint32 {
|
|
pids := make([]uint32, 2048)
|
|
var ret uint32
|
|
if err := windows.EnumProcesses(pids, &ret); err != nil || ret == 0 {
|
|
slog.Debug("failed to check for running installers", "error", err)
|
|
return nil
|
|
}
|
|
pids = pids[:ret]
|
|
matches := []uint32{}
|
|
for _, pid := range pids {
|
|
if pid == 0 {
|
|
continue
|
|
}
|
|
hProcess, err := windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION|windows.PROCESS_VM_READ, false, pid)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
defer windows.CloseHandle(hProcess)
|
|
var module windows.Handle
|
|
var cbNeeded uint32
|
|
cb := (uint32)(unsafe.Sizeof(module))
|
|
if err := windows.EnumProcessModules(hProcess, &module, cb, &cbNeeded); err != nil {
|
|
continue
|
|
}
|
|
var sz uint32 = 1024 * 8
|
|
moduleName := make([]uint16, sz)
|
|
cb = uint32(len(moduleName)) * (uint32)(unsafe.Sizeof(uint16(0)))
|
|
if err := windows.GetModuleBaseName(hProcess, module, &moduleName[0], cb); err != nil && err != syscall.ERROR_INSUFFICIENT_BUFFER {
|
|
continue
|
|
}
|
|
exeFile := path.Base(strings.ToLower(syscall.UTF16ToString(moduleName)))
|
|
if strings.EqualFold(exeFile, procName) {
|
|
matches = append(matches, pid)
|
|
}
|
|
}
|
|
return matches
|
|
}
|