1
.gitignore
vendored
|
@ -10,3 +10,4 @@ ggml-metal.metal
|
||||||
*.exe
|
*.exe
|
||||||
.idea
|
.idea
|
||||||
test_data
|
test_data
|
||||||
|
*.crt
|
93
app/.gitignore
vendored
|
@ -1,92 +1 @@
|
||||||
# Logs
|
ollama.syso
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
|
||||||
|
|
||||||
# Runtime data
|
|
||||||
pids
|
|
||||||
*.pid
|
|
||||||
*.seed
|
|
||||||
*.pid.lock
|
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
|
||||||
lib-cov
|
|
||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
|
||||||
coverage
|
|
||||||
*.lcov
|
|
||||||
|
|
||||||
# nyc test coverage
|
|
||||||
.nyc_output
|
|
||||||
|
|
||||||
# node-waf configuration
|
|
||||||
.lock-wscript
|
|
||||||
|
|
||||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
|
||||||
build/Release
|
|
||||||
|
|
||||||
# Dependency directories
|
|
||||||
node_modules/
|
|
||||||
jspm_packages/
|
|
||||||
|
|
||||||
# TypeScript v1 declaration files
|
|
||||||
typings/
|
|
||||||
|
|
||||||
# TypeScript cache
|
|
||||||
*.tsbuildinfo
|
|
||||||
|
|
||||||
# Optional npm cache directory
|
|
||||||
.npm
|
|
||||||
|
|
||||||
# Optional eslint cache
|
|
||||||
.eslintcache
|
|
||||||
|
|
||||||
# Optional REPL history
|
|
||||||
.node_repl_history
|
|
||||||
|
|
||||||
# Output of 'npm pack'
|
|
||||||
*.tgz
|
|
||||||
|
|
||||||
# Yarn Integrity file
|
|
||||||
.yarn-integrity
|
|
||||||
|
|
||||||
# dotenv environment variables file
|
|
||||||
.env
|
|
||||||
.env.test
|
|
||||||
|
|
||||||
# parcel-bundler cache (https://parceljs.org/)
|
|
||||||
.cache
|
|
||||||
|
|
||||||
# next.js build output
|
|
||||||
.next
|
|
||||||
|
|
||||||
# nuxt.js build output
|
|
||||||
.nuxt
|
|
||||||
|
|
||||||
# vuepress build output
|
|
||||||
.vuepress/dist
|
|
||||||
|
|
||||||
# Serverless directories
|
|
||||||
.serverless/
|
|
||||||
|
|
||||||
# FuseBox cache
|
|
||||||
.fusebox/
|
|
||||||
|
|
||||||
# DynamoDB Local files
|
|
||||||
.dynamodb/
|
|
||||||
|
|
||||||
# Webpack
|
|
||||||
.webpack/
|
|
||||||
|
|
||||||
# Vite
|
|
||||||
.vite/
|
|
||||||
|
|
||||||
# Electron-Forge
|
|
||||||
out/
|
|
||||||
|
|
|
@ -1,21 +1,22 @@
|
||||||
# Desktop
|
# Ollama App
|
||||||
|
|
||||||
This app builds upon Ollama to provide a desktop experience for running models.
|
## Linux
|
||||||
|
|
||||||
## Developing
|
TODO
|
||||||
|
|
||||||
First, build the `ollama` binary:
|
## MacOS
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
## Windows
|
||||||
|
|
||||||
|
If you want to build the installer, youll need to install
|
||||||
|
- https://jrsoftware.org/isinfo.php
|
||||||
|
|
||||||
|
|
||||||
|
In the top directory of this repo, run the following powershell script
|
||||||
|
to build the ollama CLI, ollama app, and ollama installer.
|
||||||
|
|
||||||
```
|
```
|
||||||
cd ..
|
powershell -ExecutionPolicy Bypass -File .\scripts\build_windows.ps1
|
||||||
go build .
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Then run the desktop app with `npm start`:
|
|
||||||
|
|
||||||
```
|
|
||||||
cd app
|
|
||||||
npm install
|
|
||||||
npm start
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
BIN
app/assets/app.ico
Normal file
After Width: | Height: | Size: 7.3 KiB |
17
app/assets/assets.go
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
package assets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"io/fs"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed *.ico
|
||||||
|
var icons embed.FS
|
||||||
|
|
||||||
|
func ListIcons() ([]string, error) {
|
||||||
|
return fs.Glob(icons, "*")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetIcon(filename string) ([]byte, error) {
|
||||||
|
return icons.ReadFile(filename)
|
||||||
|
}
|
BIN
app/assets/setup.bmp
Normal file
After Width: | Height: | Size: 76 KiB |
BIN
app/assets/tray.ico
Normal file
After Width: | Height: | Size: 89 KiB |
BIN
app/assets/tray_upgrade.ico
Normal file
After Width: | Height: | Size: 91 KiB |
9
app/lifecycle/getstarted_nonwindows.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package lifecycle
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func GetStarted() error {
|
||||||
|
return fmt.Errorf("GetStarted not implemented")
|
||||||
|
}
|
44
app/lifecycle/getstarted_windows.go
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
package lifecycle
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetStarted() error {
|
||||||
|
const CREATE_NEW_CONSOLE = 0x00000010
|
||||||
|
var err error
|
||||||
|
bannerScript := filepath.Join(AppDir, "ollama_welcome.ps1")
|
||||||
|
args := []string{
|
||||||
|
// TODO once we're signed, the execution policy bypass should be removed
|
||||||
|
"powershell", "-noexit", "-ExecutionPolicy", "Bypass", "-nologo", "-file", bannerScript,
|
||||||
|
}
|
||||||
|
args[0], err = exec.LookPath(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the script actually exists
|
||||||
|
_, err = os.Stat(bannerScript)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting started banner script error %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info(fmt.Sprintf("opening getting started terminal with %v", args))
|
||||||
|
attrs := &os.ProcAttr{
|
||||||
|
Files: []*os.File{os.Stdin, os.Stdout, os.Stderr},
|
||||||
|
Sys: &syscall.SysProcAttr{CreationFlags: CREATE_NEW_CONSOLE, HideWindow: false},
|
||||||
|
}
|
||||||
|
proc, err := os.StartProcess(args[0], args, attrs)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to start getting started shell %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug(fmt.Sprintf("getting started terminal PID: %d", proc.Pid))
|
||||||
|
return proc.Release()
|
||||||
|
}
|
84
app/lifecycle/lifecycle.go
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
package lifecycle
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/jmorganca/ollama/app/store"
|
||||||
|
"github.com/jmorganca/ollama/app/tray"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Run() {
|
||||||
|
InitLogging()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
var done chan int
|
||||||
|
|
||||||
|
t, err := tray.NewTray()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to start: %s", err)
|
||||||
|
}
|
||||||
|
callbacks := t.GetCallbacks()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
slog.Debug("starting callback loop")
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-callbacks.Quit:
|
||||||
|
slog.Debug("QUIT called")
|
||||||
|
t.Quit()
|
||||||
|
case <-callbacks.Update:
|
||||||
|
err := DoUpgrade(cancel, done)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintf("upgrade attempt failed: %s", err))
|
||||||
|
}
|
||||||
|
case <-callbacks.ShowLogs:
|
||||||
|
ShowLogs()
|
||||||
|
case <-callbacks.DoFirstUse:
|
||||||
|
err := GetStarted()
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintf("Failed to launch getting started shell: %s", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Are we first use?
|
||||||
|
if !store.GetFirstTimeRun() {
|
||||||
|
slog.Debug("First time run")
|
||||||
|
err = t.DisplayFirstUseNotification()
|
||||||
|
if err != nil {
|
||||||
|
slog.Debug(fmt.Sprintf("XXX failed to display first use notification %v", err))
|
||||||
|
}
|
||||||
|
store.SetFirstTimeRun(true)
|
||||||
|
} else {
|
||||||
|
slog.Debug("Not first time, skipping first run notification")
|
||||||
|
}
|
||||||
|
|
||||||
|
if IsServerRunning(ctx) {
|
||||||
|
slog.Info("Detected another instance of ollama running, exiting")
|
||||||
|
os.Exit(1)
|
||||||
|
} else {
|
||||||
|
done, err = SpawnServer(ctx, CLIName)
|
||||||
|
if err != nil {
|
||||||
|
// TODO - should we retry in a backoff loop?
|
||||||
|
// TODO - should we pop up a warning and maybe add a menu item to view application logs?
|
||||||
|
slog.Error(fmt.Sprintf("Failed to spawn ollama server %s", err))
|
||||||
|
done = make(chan int, 1)
|
||||||
|
done <- 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StartBackgroundUpdaterChecker(ctx, t.UpdateAvailable)
|
||||||
|
|
||||||
|
t.Run()
|
||||||
|
cancel()
|
||||||
|
slog.Info("Waiting for ollama server to shutdown...")
|
||||||
|
if done != nil {
|
||||||
|
<-done
|
||||||
|
}
|
||||||
|
slog.Info("Ollama app exiting")
|
||||||
|
}
|
46
app/lifecycle/logging.go
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
package lifecycle
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func InitLogging() {
|
||||||
|
level := slog.LevelInfo
|
||||||
|
|
||||||
|
if debug := os.Getenv("OLLAMA_DEBUG"); debug != "" {
|
||||||
|
level = slog.LevelDebug
|
||||||
|
}
|
||||||
|
|
||||||
|
var logFile *os.File
|
||||||
|
var err error
|
||||||
|
// Detect if we're a GUI app on windows, and if not, send logs to console
|
||||||
|
if os.Stderr.Fd() != 0 {
|
||||||
|
// Console app detected
|
||||||
|
logFile = os.Stderr
|
||||||
|
// TODO - write one-line to the app.log file saying we're running in console mode to help avoid confusion
|
||||||
|
} else {
|
||||||
|
logFile, err = os.OpenFile(AppLogFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0755)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("failed to create server log %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handler := slog.NewTextHandler(logFile, &slog.HandlerOptions{
|
||||||
|
Level: level,
|
||||||
|
AddSource: true,
|
||||||
|
ReplaceAttr: func(_ []string, attr slog.Attr) slog.Attr {
|
||||||
|
if attr.Key == slog.SourceKey {
|
||||||
|
source := attr.Value.Any().(*slog.Source)
|
||||||
|
source.File = filepath.Base(source.File)
|
||||||
|
}
|
||||||
|
return attr
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
slog.SetDefault(slog.New(handler))
|
||||||
|
|
||||||
|
slog.Info("ollama app started")
|
||||||
|
}
|
9
app/lifecycle/logging_nonwindows.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package lifecycle
|
||||||
|
|
||||||
|
import "log/slog"
|
||||||
|
|
||||||
|
func ShowLogs() {
|
||||||
|
slog.Warn("ShowLogs not yet implemented")
|
||||||
|
}
|
19
app/lifecycle/logging_windows.go
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
package lifecycle
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os/exec"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ShowLogs() {
|
||||||
|
cmd_path := "c:\\Windows\\system32\\cmd.exe"
|
||||||
|
slog.Debug(fmt.Sprintf("viewing logs with start %s", AppDataDir))
|
||||||
|
cmd := exec.Command(cmd_path, "/c", "start", AppDataDir)
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true, CreationFlags: 0x08000000}
|
||||||
|
err := cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("Failed to open log dir: %s", err))
|
||||||
|
}
|
||||||
|
}
|
79
app/lifecycle/paths.go
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
package lifecycle
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
AppName = "ollama app"
|
||||||
|
CLIName = "ollama"
|
||||||
|
AppDir = "/opt/Ollama"
|
||||||
|
AppDataDir = "/opt/Ollama"
|
||||||
|
// TODO - should there be a distinct log dir?
|
||||||
|
UpdateStageDir = "/tmp"
|
||||||
|
AppLogFile = "/tmp/ollama_app.log"
|
||||||
|
ServerLogFile = "/tmp/ollama.log"
|
||||||
|
UpgradeLogFile = "/tmp/ollama_update.log"
|
||||||
|
Installer = "OllamaSetup.exe"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
AppName += ".exe"
|
||||||
|
CLIName += ".exe"
|
||||||
|
// Logs, configs, downloads go to LOCALAPPDATA
|
||||||
|
localAppData := os.Getenv("LOCALAPPDATA")
|
||||||
|
AppDataDir = filepath.Join(localAppData, "Ollama")
|
||||||
|
UpdateStageDir = filepath.Join(AppDataDir, "updates")
|
||||||
|
AppLogFile = filepath.Join(AppDataDir, "app.log")
|
||||||
|
ServerLogFile = filepath.Join(AppDataDir, "server.log")
|
||||||
|
UpgradeLogFile = filepath.Join(AppDataDir, "upgrade.log")
|
||||||
|
|
||||||
|
// Executables are stored in APPDATA
|
||||||
|
AppDir = filepath.Join(localAppData, "Programs", "Ollama")
|
||||||
|
|
||||||
|
// Make sure we have PATH set correctly for any spawned children
|
||||||
|
paths := strings.Split(os.Getenv("PATH"), ";")
|
||||||
|
// Start with whatever we find in the PATH/LD_LIBRARY_PATH
|
||||||
|
found := false
|
||||||
|
for _, path := range paths {
|
||||||
|
d, err := filepath.Abs(path)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.EqualFold(AppDir, d) {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
paths = append(paths, AppDir)
|
||||||
|
|
||||||
|
pathVal := strings.Join(paths, ";")
|
||||||
|
slog.Debug("setting PATH=" + pathVal)
|
||||||
|
err := os.Setenv("PATH", pathVal)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("failed to update PATH: %s", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure our logging dir exists
|
||||||
|
_, err := os.Stat(AppDataDir)
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
if err := os.MkdirAll(AppDataDir, 0o755); err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("create ollama dir %s: %v", AppDataDir, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if runtime.GOOS == "darwin" {
|
||||||
|
// TODO
|
||||||
|
AppName += ".app"
|
||||||
|
// } else if runtime.GOOS == "linux" {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
}
|
135
app/lifecycle/server.go
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
package lifecycle
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jmorganca/ollama/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getCLIFullPath(command string) string {
|
||||||
|
cmdPath := ""
|
||||||
|
appExe, err := os.Executable()
|
||||||
|
if err == nil {
|
||||||
|
cmdPath = filepath.Join(filepath.Dir(appExe), command)
|
||||||
|
_, err := os.Stat(cmdPath)
|
||||||
|
if err == nil {
|
||||||
|
return cmdPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmdPath, err = exec.LookPath(command)
|
||||||
|
if err == nil {
|
||||||
|
_, err := os.Stat(cmdPath)
|
||||||
|
if err == nil {
|
||||||
|
return cmdPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmdPath = filepath.Join(".", command)
|
||||||
|
_, err = os.Stat(cmdPath)
|
||||||
|
if err == nil {
|
||||||
|
return cmdPath
|
||||||
|
}
|
||||||
|
return command
|
||||||
|
}
|
||||||
|
|
||||||
|
func SpawnServer(ctx context.Context, command string) (chan int, error) {
|
||||||
|
done := make(chan int)
|
||||||
|
|
||||||
|
logDir := filepath.Dir(ServerLogFile)
|
||||||
|
_, err := os.Stat(logDir)
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
if err := os.MkdirAll(logDir, 0o755); err != nil {
|
||||||
|
return done, fmt.Errorf("create ollama server log dir %s: %v", logDir, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := getCmd(ctx, getCLIFullPath(command))
|
||||||
|
// send stdout and stderr to a file
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return done, fmt.Errorf("failed to spawn server stdout pipe %s", err)
|
||||||
|
}
|
||||||
|
stderr, err := cmd.StderrPipe()
|
||||||
|
if err != nil {
|
||||||
|
return done, fmt.Errorf("failed to spawn server stderr pipe %s", err)
|
||||||
|
}
|
||||||
|
stdin, err := cmd.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
return done, fmt.Errorf("failed to spawn server stdin pipe %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO - rotation
|
||||||
|
logFile, err := os.OpenFile(ServerLogFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return done, fmt.Errorf("failed to create server log %w", err)
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
defer logFile.Close()
|
||||||
|
io.Copy(logFile, stdout) //nolint:errcheck
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
defer logFile.Close()
|
||||||
|
io.Copy(logFile, stderr) //nolint:errcheck
|
||||||
|
}()
|
||||||
|
|
||||||
|
// run the command and wait for it to finish
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return done, fmt.Errorf("failed to start server %w", err)
|
||||||
|
}
|
||||||
|
if cmd.Process != nil {
|
||||||
|
slog.Info(fmt.Sprintf("started ollama server with pid %d", cmd.Process.Pid))
|
||||||
|
}
|
||||||
|
slog.Info(fmt.Sprintf("ollama server logs %s", ServerLogFile))
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// Keep the server running unless we're shuttind down the app
|
||||||
|
crashCount := 0
|
||||||
|
for {
|
||||||
|
cmd.Wait() //nolint:errcheck
|
||||||
|
stdin.Close()
|
||||||
|
var code int
|
||||||
|
if cmd.ProcessState != nil {
|
||||||
|
code = cmd.ProcessState.ExitCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
slog.Debug(fmt.Sprintf("server shutdown with exit code %d", code))
|
||||||
|
done <- code
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
crashCount++
|
||||||
|
slog.Warn(fmt.Sprintf("server crash %d - exit code %d - respawning", crashCount, code))
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("failed to restart server %s", err))
|
||||||
|
// Keep trying, but back off if we keep failing
|
||||||
|
time.Sleep(time.Duration(crashCount) * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return done, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsServerRunning(ctx context.Context) bool {
|
||||||
|
client, err := api.ClientFromEnvironment()
|
||||||
|
if err != nil {
|
||||||
|
slog.Info("unable to connect to server")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
err = client.Heartbeat(ctx)
|
||||||
|
if err != nil {
|
||||||
|
slog.Debug(fmt.Sprintf("heartbeat from server: %s", err))
|
||||||
|
slog.Info("unable to connect to server")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
12
app/lifecycle/server_unix.go
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package lifecycle
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getCmd(ctx context.Context, cmd string) *exec.Cmd {
|
||||||
|
return exec.CommandContext(ctx, cmd, "serve")
|
||||||
|
}
|
13
app/lifecycle/server_windows.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package lifecycle
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os/exec"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getCmd(ctx context.Context, exePath string) *exec.Cmd {
|
||||||
|
cmd := exec.CommandContext(ctx, exePath, "serve")
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true, CreationFlags: 0x08000000}
|
||||||
|
return cmd
|
||||||
|
}
|
238
app/lifecycle/updater.go
Normal file
|
@ -0,0 +1,238 @@
|
||||||
|
package lifecycle
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jmorganca/ollama/auth"
|
||||||
|
"github.com/jmorganca/ollama/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
UpdateCheckURLBase = "https://ollama.com/api/update"
|
||||||
|
UpdateDownloaded = false
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO - maybe move up to the API package?
|
||||||
|
type UpdateResponse struct {
|
||||||
|
UpdateURL string `json:"url"`
|
||||||
|
UpdateVersion string `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getClient(req *http.Request) http.Client {
|
||||||
|
proxyURL, err := http.ProxyFromEnvironment(req)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintf("failed to handle proxy: %s", err))
|
||||||
|
return http.Client{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
Proxy: http.ProxyURL(proxyURL),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsNewReleaseAvailable(ctx context.Context) (bool, UpdateResponse) {
|
||||||
|
var updateResp UpdateResponse
|
||||||
|
|
||||||
|
requestURL, err := url.Parse(UpdateCheckURLBase)
|
||||||
|
if err != nil {
|
||||||
|
return false, updateResp
|
||||||
|
}
|
||||||
|
|
||||||
|
query := requestURL.Query()
|
||||||
|
query.Add("os", runtime.GOOS)
|
||||||
|
query.Add("arch", runtime.GOARCH)
|
||||||
|
query.Add("version", version.Version)
|
||||||
|
query.Add("ts", fmt.Sprintf("%d", time.Now().Unix()))
|
||||||
|
|
||||||
|
nonce, err := auth.NewNonce(rand.Reader, 16)
|
||||||
|
if err != nil {
|
||||||
|
return false, updateResp
|
||||||
|
}
|
||||||
|
|
||||||
|
query.Add("nonce", nonce)
|
||||||
|
requestURL.RawQuery = query.Encode()
|
||||||
|
|
||||||
|
data := []byte(fmt.Sprintf("%s,%s", http.MethodGet, requestURL.RequestURI()))
|
||||||
|
signature, err := auth.Sign(ctx, data)
|
||||||
|
if err != nil {
|
||||||
|
return false, updateResp
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintf("failed to check for update: %s", err))
|
||||||
|
return false, updateResp
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", signature)
|
||||||
|
req.Header.Set("User-Agent", fmt.Sprintf("ollama/%s (%s %s) Go/%s", version.Version, runtime.GOARCH, runtime.GOOS, runtime.Version()))
|
||||||
|
client := getClient(req)
|
||||||
|
|
||||||
|
slog.Debug("checking for available update", "requestURL", requestURL)
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintf("failed to check for update: %s", err))
|
||||||
|
return false, updateResp
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == 204 {
|
||||||
|
slog.Debug("check update response 204 (current version is up to date)")
|
||||||
|
return false, updateResp
|
||||||
|
}
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintf("failed to read body response: %s", err))
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(body, &updateResp)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintf("malformed response checking for update: %s", err))
|
||||||
|
return false, updateResp
|
||||||
|
}
|
||||||
|
// Extract the version string from the URL in the github release artifact path
|
||||||
|
updateResp.UpdateVersion = path.Base(path.Dir(updateResp.UpdateURL))
|
||||||
|
|
||||||
|
slog.Info("New update available at " + updateResp.UpdateURL)
|
||||||
|
return true, updateResp
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true if we downloaded a new update, false if we already had it
|
||||||
|
func DownloadNewRelease(ctx context.Context, updateResp UpdateResponse) error {
|
||||||
|
// Do a head first to check etag info
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodHead, updateResp.UpdateURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client := getClient(req)
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error checking update: %w", err)
|
||||||
|
}
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
return fmt.Errorf("unexpected status attempting to download update %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
etag := strings.Trim(resp.Header.Get("etag"), "\"")
|
||||||
|
if etag == "" {
|
||||||
|
slog.Debug("no etag detected, falling back to filename based dedup")
|
||||||
|
etag = "_"
|
||||||
|
}
|
||||||
|
filename := Installer
|
||||||
|
_, params, err := mime.ParseMediaType(resp.Header.Get("content-disposition"))
|
||||||
|
if err == nil {
|
||||||
|
filename = params["filename"]
|
||||||
|
}
|
||||||
|
|
||||||
|
stageFilename := filepath.Join(UpdateStageDir, etag, filename)
|
||||||
|
|
||||||
|
// Check to see if we already have it downloaded
|
||||||
|
_, err = os.Stat(stageFilename)
|
||||||
|
if err == nil {
|
||||||
|
slog.Debug("update already downloaded")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanupOldDownloads()
|
||||||
|
|
||||||
|
req.Method = http.MethodGet
|
||||||
|
resp, err = client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error checking update: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
etag = strings.Trim(resp.Header.Get("etag"), "\"")
|
||||||
|
if etag == "" {
|
||||||
|
slog.Debug("no etag detected, falling back to filename based dedup") // TODO probably can get rid of this redundant log
|
||||||
|
etag = "_"
|
||||||
|
}
|
||||||
|
|
||||||
|
stageFilename = filepath.Join(UpdateStageDir, etag, filename)
|
||||||
|
|
||||||
|
_, err = os.Stat(filepath.Dir(stageFilename))
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
if err := os.MkdirAll(filepath.Dir(stageFilename), 0o755); err != nil {
|
||||||
|
return fmt.Errorf("create ollama dir %s: %v", filepath.Dir(stageFilename), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read body response: %w", err)
|
||||||
|
}
|
||||||
|
fp, err := os.OpenFile(stageFilename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("write payload %s: %w", stageFilename, err)
|
||||||
|
}
|
||||||
|
defer fp.Close()
|
||||||
|
if n, err := fp.Write(payload); err != nil || n != len(payload) {
|
||||||
|
return fmt.Errorf("write payload %s: %d vs %d -- %w", stageFilename, n, len(payload), err)
|
||||||
|
}
|
||||||
|
slog.Info("new update downloaded " + stageFilename)
|
||||||
|
|
||||||
|
UpdateDownloaded = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanupOldDownloads() {
|
||||||
|
files, err := os.ReadDir(UpdateStageDir)
|
||||||
|
if err != nil && errors.Is(err, os.ErrNotExist) {
|
||||||
|
// Expected behavior on first run
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintf("failed to list stage dir: %s", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, file := range files {
|
||||||
|
fullname := filepath.Join(UpdateStageDir, file.Name())
|
||||||
|
slog.Debug("cleaning up old download: " + fullname)
|
||||||
|
err = os.RemoveAll(fullname)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintf("failed to cleanup stale update download %s", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartBackgroundUpdaterChecker(ctx context.Context, cb func(string) error) {
|
||||||
|
go func() {
|
||||||
|
// Don't blast an update message immediately after startup
|
||||||
|
// time.Sleep(30 * time.Second)
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
|
||||||
|
for {
|
||||||
|
available, resp := IsNewReleaseAvailable(ctx)
|
||||||
|
if available {
|
||||||
|
err := DownloadNewRelease(ctx, resp)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("failed to download new release: %s", err))
|
||||||
|
}
|
||||||
|
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(60 * 60 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
12
app/lifecycle/updater_nonwindows.go
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package lifecycle
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DoUpgrade(cancel context.CancelFunc, done chan int) error {
|
||||||
|
return fmt.Errorf("DoUpgrade not yet implemented")
|
||||||
|
}
|
80
app/lifecycle/updater_windows.go
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
package lifecycle
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DoUpgrade(cancel context.CancelFunc, done chan int) error {
|
||||||
|
files, err := filepath.Glob(filepath.Join(UpdateStageDir, "*", "*.exe")) // TODO generalize for multiplatform
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to lookup downloads: %s", err)
|
||||||
|
}
|
||||||
|
if len(files) == 0 {
|
||||||
|
return fmt.Errorf("no update downloads found")
|
||||||
|
} else if len(files) > 1 {
|
||||||
|
// Shouldn't happen
|
||||||
|
slog.Warn(fmt.Sprintf("multiple downloads found, using first one %v", files))
|
||||||
|
}
|
||||||
|
installerExe := files[0]
|
||||||
|
|
||||||
|
slog.Info("starting upgrade with " + installerExe)
|
||||||
|
slog.Info("upgrade log file " + UpgradeLogFile)
|
||||||
|
|
||||||
|
// When running in debug mode, we'll be "verbose" and let the installer pop up and prompt
|
||||||
|
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
|
||||||
|
}
|
||||||
|
// When we're not in debug mode, make the upgrade as quiet as possible (no GUI, no prompts)
|
||||||
|
// TODO - temporarily disable since we're pinning in debug mode for the preview
|
||||||
|
// if debug := os.Getenv("OLLAMA_DEBUG"); debug == "" {
|
||||||
|
installArgs = append(installArgs,
|
||||||
|
"/SP", // Skip the "This will install... Do you wish to continue" prompt
|
||||||
|
"/SUPPRESSMSGBOXES",
|
||||||
|
"/SILENT",
|
||||||
|
"/VERYSILENT",
|
||||||
|
)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Safeguard in case we have requests in flight that need to drain...
|
||||||
|
slog.Info("Waiting for server to shutdown")
|
||||||
|
cancel()
|
||||||
|
if done != nil {
|
||||||
|
<-done
|
||||||
|
} else {
|
||||||
|
// Shouldn't happen
|
||||||
|
slog.Warn("done chan was nil, not actually waiting")
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug(fmt.Sprintf("starting installer: %s %v", installerExe, installArgs))
|
||||||
|
os.Chdir(filepath.Dir(UpgradeLogFile)) //nolint:errcheck
|
||||||
|
cmd := exec.Command(installerExe, 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 fmt.Errorf("installer process did not start")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
12
app/main.go
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
// Compile with the following to get rid of the cmd pop up on windows
|
||||||
|
// go build -ldflags="-H windowsgui" .
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/jmorganca/ollama/app/lifecycle"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
lifecycle.Run()
|
||||||
|
}
|
153
app/ollama.iss
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
; Inno Setup Installer for Ollama
|
||||||
|
;
|
||||||
|
; To build the installer use the build script invoked from the top of the source tree
|
||||||
|
;
|
||||||
|
; powershell -ExecutionPolicy Bypass -File .\scripts\build_windows.ps
|
||||||
|
|
||||||
|
|
||||||
|
#define MyAppName "Ollama"
|
||||||
|
#if GetEnv("PKG_VERSION") != ""
|
||||||
|
#define MyAppVersion GetEnv("PKG_VERSION")
|
||||||
|
#else
|
||||||
|
#define MyAppVersion "0.0.0"
|
||||||
|
#endif
|
||||||
|
#define MyAppPublisher "Ollama"
|
||||||
|
#define MyAppURL "https://ollama.com/"
|
||||||
|
#define MyAppExeName "ollama app.exe"
|
||||||
|
#define MyIcon ".\assets\app.ico"
|
||||||
|
|
||||||
|
[Setup]
|
||||||
|
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
|
||||||
|
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
|
||||||
|
AppId={{44E83376-CE68-45EB-8FC1-393500EB558C}
|
||||||
|
AppName={#MyAppName}
|
||||||
|
AppVersion={#MyAppVersion}
|
||||||
|
VersionInfoVersion={#MyAppVersion}
|
||||||
|
;AppVerName={#MyAppName} {#MyAppVersion}
|
||||||
|
AppPublisher={#MyAppPublisher}
|
||||||
|
AppPublisherURL={#MyAppURL}
|
||||||
|
AppSupportURL={#MyAppURL}
|
||||||
|
AppUpdatesURL={#MyAppURL}
|
||||||
|
ArchitecturesAllowed=x64
|
||||||
|
ArchitecturesInstallIn64BitMode=x64
|
||||||
|
DefaultDirName={localappdata}\Programs\{#MyAppName}
|
||||||
|
DefaultGroupName={#MyAppName}
|
||||||
|
DisableProgramGroupPage=yes
|
||||||
|
PrivilegesRequired=lowest
|
||||||
|
OutputBaseFilename="OllamaSetup"
|
||||||
|
SetupIconFile={#MyIcon}
|
||||||
|
UninstallDisplayIcon={uninstallexe}
|
||||||
|
Compression=lzma2
|
||||||
|
SolidCompression=no
|
||||||
|
WizardStyle=modern
|
||||||
|
ChangesEnvironment=yes
|
||||||
|
OutputDir=..\dist\
|
||||||
|
|
||||||
|
; Disable logging once everything's battle tested
|
||||||
|
; Filename will be %TEMP%\Setup Log*.txt
|
||||||
|
SetupLogging=yes
|
||||||
|
CloseApplications=yes
|
||||||
|
RestartApplications=no
|
||||||
|
|
||||||
|
; Make sure they can at least download llama2 as a minimum
|
||||||
|
ExtraDiskSpaceRequired=3826806784
|
||||||
|
|
||||||
|
; https://jrsoftware.org/ishelp/index.php?topic=setup_wizardimagefile
|
||||||
|
WizardSmallImageFile=.\assets\setup.bmp
|
||||||
|
|
||||||
|
; TODO verifty actual min windows version...
|
||||||
|
; OG Win 10
|
||||||
|
MinVersion=10.0.10240
|
||||||
|
|
||||||
|
; First release that supports WinRT UI Composition for win32 apps
|
||||||
|
; MinVersion=10.0.17134
|
||||||
|
; First release with XAML Islands - possible UI path forward
|
||||||
|
; MinVersion=10.0.18362
|
||||||
|
|
||||||
|
; quiet...
|
||||||
|
DisableDirPage=yes
|
||||||
|
DisableFinishedPage=yes
|
||||||
|
DisableReadyMemo=yes
|
||||||
|
DisableReadyPage=yes
|
||||||
|
DisableStartupPrompt=yes
|
||||||
|
DisableWelcomePage=yes
|
||||||
|
|
||||||
|
; TODO - percentage can't be set less than 100, so how to make it shorter?
|
||||||
|
; WizardSizePercent=100,80
|
||||||
|
|
||||||
|
#if GetEnv("KEY_CONTAINER")
|
||||||
|
SignTool=MySignTool
|
||||||
|
SignedUninstaller=yes
|
||||||
|
#endif
|
||||||
|
|
||||||
|
SetupMutex=OllamaSetupMutex
|
||||||
|
|
||||||
|
[Languages]
|
||||||
|
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||||
|
|
||||||
|
[LangOptions]
|
||||||
|
DialogFontSize=12
|
||||||
|
|
||||||
|
[Files]
|
||||||
|
Source: ".\app.exe"; DestDir: "{app}"; DestName: "{#MyAppExeName}" ; Flags: ignoreversion 64bit
|
||||||
|
Source: "..\ollama.exe"; DestDir: "{app}"; Flags: ignoreversion 64bit
|
||||||
|
Source: "..\dist\windeps\*.dll"; DestDir: "{app}"; Flags: ignoreversion 64bit
|
||||||
|
Source: "..\dist\ollama_welcome.ps1"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
Source: ".\assets\app.ico"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
|
||||||
|
[Icons]
|
||||||
|
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\app.ico"
|
||||||
|
Name: "{userstartup}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\app.ico"
|
||||||
|
Name: "{userprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\app.ico"
|
||||||
|
|
||||||
|
[Run]
|
||||||
|
Filename: "{cmd}"; Parameters: "/C set PATH={app};%PATH% & ""{app}\{#MyAppExeName}"""; Flags: postinstall nowait runhidden
|
||||||
|
|
||||||
|
[UninstallRun]
|
||||||
|
; Filename: "{cmd}"; Parameters: "/C ""taskkill /im ''{#MyAppExeName}'' /f /t"; Flags: runhidden
|
||||||
|
; Filename: "{cmd}"; Parameters: "/C ""taskkill /im ollama.exe /f /t"; Flags: runhidden
|
||||||
|
Filename: "taskkill"; Parameters: "/im ""{#MyAppExeName}"" /f /t"; Flags: runhidden
|
||||||
|
Filename: "taskkill"; Parameters: "/im ""ollama.exe"" /f /t"; Flags: runhidden
|
||||||
|
; HACK! need to give the server and app enough time to exit
|
||||||
|
; TODO - convert this to a Pascal code script so it waits until they're no longer running, then completes
|
||||||
|
Filename: "{cmd}"; Parameters: "/c timeout 5"; Flags: runhidden
|
||||||
|
|
||||||
|
[UninstallDelete]
|
||||||
|
Type: filesandordirs; Name: "{%TEMP}\ollama*"
|
||||||
|
Type: filesandordirs; Name: "{%LOCALAPPDATA}\Ollama"
|
||||||
|
Type: filesandordirs; Name: "{%LOCALAPPDATA}\Programs\Ollama"
|
||||||
|
Type: filesandordirs; Name: "{%USERPROFILE}\.ollama"
|
||||||
|
; NOTE: if the user has a custom OLLAMA_MODELS it will be preserved
|
||||||
|
|
||||||
|
[Messages]
|
||||||
|
WizardReady=Ollama Windows Preview
|
||||||
|
ReadyLabel1=%nLet's get you up and running with your own large language models.
|
||||||
|
SetupAppRunningError=Another Ollama installer is running.%n%nPlease cancel or finish the other installer, then click OK to continue with this install, or Cancel to exit.
|
||||||
|
|
||||||
|
|
||||||
|
;FinishedHeadingLabel=Run your first model
|
||||||
|
;FinishedLabel=%nRun this command in a PowerShell or cmd terminal.%n%n%n ollama run llama2
|
||||||
|
;ClickFinish=%n
|
||||||
|
|
||||||
|
[Registry]
|
||||||
|
Root: HKCU; Subkey: "Environment"; \
|
||||||
|
ValueType: expandsz; ValueName: "Path"; ValueData: "{olddata};{app}"; \
|
||||||
|
Check: NeedsAddPath('{app}')
|
||||||
|
|
||||||
|
[Code]
|
||||||
|
|
||||||
|
function NeedsAddPath(Param: string): boolean;
|
||||||
|
var
|
||||||
|
OrigPath: string;
|
||||||
|
begin
|
||||||
|
if not RegQueryStringValue(HKEY_CURRENT_USER,
|
||||||
|
'Environment',
|
||||||
|
'Path', OrigPath)
|
||||||
|
then begin
|
||||||
|
Result := True;
|
||||||
|
exit;
|
||||||
|
end;
|
||||||
|
{ look for the path with leading and trailing semicolon }
|
||||||
|
{ Pos() returns 0 if not found }
|
||||||
|
Result := Pos(';' + ExpandConstant(Param) + ';', ';' + OrigPath + ';') = 0;
|
||||||
|
end;
|
29
app/ollama.rc
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
#include <winver.h>
|
||||||
|
|
||||||
|
VS_VERSION_INFO VERSIONINFO
|
||||||
|
FILEFLAGSMASK 0x3fL
|
||||||
|
#ifdef _DEBUG
|
||||||
|
FILEFLAGS 0x1L
|
||||||
|
#else
|
||||||
|
FILEFLAGS 0x0L
|
||||||
|
#endif
|
||||||
|
FILEOS 0x40004L
|
||||||
|
FILETYPE 0x1L
|
||||||
|
FILESUBTYPE 0x0L
|
||||||
|
BEGIN
|
||||||
|
BLOCK "StringFileInfo"
|
||||||
|
BEGIN
|
||||||
|
BLOCK "040904b0"
|
||||||
|
BEGIN
|
||||||
|
VALUE "FileDescription", "Ollama"
|
||||||
|
VALUE "InternalName", "Ollama"
|
||||||
|
VALUE "OriginalFilename", "ollama app.exe"
|
||||||
|
VALUE "ProductName", "Ollama"
|
||||||
|
END
|
||||||
|
END
|
||||||
|
|
||||||
|
BLOCK "VarFileInfo"
|
||||||
|
BEGIN
|
||||||
|
VALUE "Translation", 0x409, 1200
|
||||||
|
END
|
||||||
|
END
|
8
app/ollama_welcome.ps1
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# TODO - consider ANSI colors and maybe ASCII art...
|
||||||
|
write-host ""
|
||||||
|
write-host "Welcome to Ollama!"
|
||||||
|
write-host ""
|
||||||
|
write-host "Run your first model:"
|
||||||
|
write-host ""
|
||||||
|
write-host "`tollama run llama2"
|
||||||
|
write-host ""
|
98
app/store/store.go
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Store struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
FirstTimeRun bool `json:"first-time-run"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
lock sync.Mutex
|
||||||
|
store Store
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetID() string {
|
||||||
|
lock.Lock()
|
||||||
|
defer lock.Unlock()
|
||||||
|
if store.ID == "" {
|
||||||
|
initStore()
|
||||||
|
}
|
||||||
|
return store.ID
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFirstTimeRun() bool {
|
||||||
|
lock.Lock()
|
||||||
|
defer lock.Unlock()
|
||||||
|
if store.ID == "" {
|
||||||
|
initStore()
|
||||||
|
}
|
||||||
|
return store.FirstTimeRun
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetFirstTimeRun(val bool) {
|
||||||
|
lock.Lock()
|
||||||
|
defer lock.Unlock()
|
||||||
|
if store.FirstTimeRun == val {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
store.FirstTimeRun = val
|
||||||
|
writeStore(getStorePath())
|
||||||
|
}
|
||||||
|
|
||||||
|
// lock must be held
|
||||||
|
func initStore() {
|
||||||
|
storeFile, err := os.Open(getStorePath())
|
||||||
|
if err == nil {
|
||||||
|
defer storeFile.Close()
|
||||||
|
err = json.NewDecoder(storeFile).Decode(&store)
|
||||||
|
if err == nil {
|
||||||
|
slog.Debug(fmt.Sprintf("loaded existing store %s - ID: %s", getStorePath(), store.ID))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if !errors.Is(err, os.ErrNotExist) {
|
||||||
|
slog.Debug(fmt.Sprintf("unexpected error searching for store: %s", err))
|
||||||
|
}
|
||||||
|
slog.Debug("initializing new store")
|
||||||
|
store.ID = uuid.New().String()
|
||||||
|
writeStore(getStorePath())
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeStore(storeFilename string) {
|
||||||
|
ollamaDir := filepath.Dir(storeFilename)
|
||||||
|
_, err := os.Stat(ollamaDir)
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
if err := os.MkdirAll(ollamaDir, 0o755); err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("create ollama dir %s: %v", ollamaDir, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
payload, err := json.Marshal(store)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("failed to marshal store: %s", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fp, err := os.OpenFile(storeFilename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("write store payload %s: %v", storeFilename, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer fp.Close()
|
||||||
|
if n, err := fp.Write(payload); err != nil || n != len(payload) {
|
||||||
|
slog.Error(fmt.Sprintf("write store payload %s: %d vs %d -- %v", storeFilename, n, len(payload), err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
slog.Debug("Store contents: " + string(payload))
|
||||||
|
slog.Info(fmt.Sprintf("wrote store: %s", storeFilename))
|
||||||
|
}
|
13
app/store/store_darwin.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getStorePath() string {
|
||||||
|
// TODO - system wide location?
|
||||||
|
|
||||||
|
home := os.Getenv("HOME")
|
||||||
|
return filepath.Join(home, "Library", "Application Support", "Ollama", "config.json")
|
||||||
|
}
|
16
app/store/store_linux.go
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getStorePath() string {
|
||||||
|
if os.Geteuid() == 0 {
|
||||||
|
// TODO where should we store this on linux for system-wide operation?
|
||||||
|
return "/etc/ollama/config.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
home := os.Getenv("HOME")
|
||||||
|
return filepath.Join(home, ".ollama", "config.json")
|
||||||
|
}
|
11
app/store/store_windows.go
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getStorePath() string {
|
||||||
|
localAppData := os.Getenv("LOCALAPPDATA")
|
||||||
|
return filepath.Join(localAppData, "Ollama", "config.json")
|
||||||
|
}
|
24
app/tray/commontray/types.go
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
package commontray
|
||||||
|
|
||||||
|
var (
|
||||||
|
Title = "Ollama"
|
||||||
|
ToolTip = "Ollama"
|
||||||
|
|
||||||
|
UpdateIconName = "tray_upgrade"
|
||||||
|
IconName = "tray"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Callbacks struct {
|
||||||
|
Quit chan struct{}
|
||||||
|
Update chan struct{}
|
||||||
|
DoFirstUse chan struct{}
|
||||||
|
ShowLogs chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type OllamaTray interface {
|
||||||
|
GetCallbacks() Callbacks
|
||||||
|
Run()
|
||||||
|
UpdateAvailable(ver string) error
|
||||||
|
DisplayFirstUseNotification() error
|
||||||
|
Quit()
|
||||||
|
}
|
33
app/tray/tray.go
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
package tray
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/jmorganca/ollama/app/assets"
|
||||||
|
"github.com/jmorganca/ollama/app/tray/commontray"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewTray() (commontray.OllamaTray, error) {
|
||||||
|
extension := ".png"
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
extension = ".ico"
|
||||||
|
}
|
||||||
|
iconName := commontray.UpdateIconName + extension
|
||||||
|
updateIcon, err := assets.GetIcon(iconName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load icon %s: %w", iconName, err)
|
||||||
|
}
|
||||||
|
iconName = commontray.IconName + extension
|
||||||
|
icon, err := assets.GetIcon(iconName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load icon %s: %w", iconName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tray, err := InitPlatformTray(icon, updateIcon)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tray, nil
|
||||||
|
}
|
13
app/tray/tray_nonwindows.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package tray
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/jmorganca/ollama/app/tray/commontray"
|
||||||
|
)
|
||||||
|
|
||||||
|
func InitPlatformTray(icon, updateIcon []byte) (commontray.OllamaTray, error) {
|
||||||
|
return nil, fmt.Errorf("NOT IMPLEMENTED YET")
|
||||||
|
}
|
10
app/tray/tray_windows.go
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
package tray
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/jmorganca/ollama/app/tray/commontray"
|
||||||
|
"github.com/jmorganca/ollama/app/tray/wintray"
|
||||||
|
)
|
||||||
|
|
||||||
|
func InitPlatformTray(icon, updateIcon []byte) (commontray.OllamaTray, error) {
|
||||||
|
return wintray.InitTray(icon, updateIcon)
|
||||||
|
}
|
184
app/tray/wintray/eventloop.go
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package wintray
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"sync"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
quitOnce sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t *winTray) Run() {
|
||||||
|
nativeLoop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func nativeLoop() {
|
||||||
|
// Main message pump.
|
||||||
|
slog.Debug("starting event handling loop")
|
||||||
|
m := &struct {
|
||||||
|
WindowHandle windows.Handle
|
||||||
|
Message uint32
|
||||||
|
Wparam uintptr
|
||||||
|
Lparam uintptr
|
||||||
|
Time uint32
|
||||||
|
Pt point
|
||||||
|
LPrivate uint32
|
||||||
|
}{}
|
||||||
|
for {
|
||||||
|
ret, _, err := pGetMessage.Call(uintptr(unsafe.Pointer(m)), 0, 0, 0)
|
||||||
|
|
||||||
|
// If the function retrieves a message other than WM_QUIT, the return value is nonzero.
|
||||||
|
// If the function retrieves the WM_QUIT message, the return value is zero.
|
||||||
|
// If there is an error, the return value is -1
|
||||||
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms644936(v=vs.85).aspx
|
||||||
|
switch int32(ret) {
|
||||||
|
case -1:
|
||||||
|
slog.Error(fmt.Sprintf("get message failure: %v", err))
|
||||||
|
return
|
||||||
|
case 0:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
pTranslateMessage.Call(uintptr(unsafe.Pointer(m))) //nolint:errcheck
|
||||||
|
pDispatchMessage.Call(uintptr(unsafe.Pointer(m))) //nolint:errcheck
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WindowProc callback function that processes messages sent to a window.
|
||||||
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms633573(v=vs.85).aspx
|
||||||
|
func (t *winTray) wndProc(hWnd windows.Handle, message uint32, wParam, lParam uintptr) (lResult uintptr) {
|
||||||
|
const (
|
||||||
|
WM_RBUTTONUP = 0x0205
|
||||||
|
WM_LBUTTONUP = 0x0202
|
||||||
|
WM_COMMAND = 0x0111
|
||||||
|
WM_ENDSESSION = 0x0016
|
||||||
|
WM_CLOSE = 0x0010
|
||||||
|
WM_DESTROY = 0x0002
|
||||||
|
WM_MOUSEMOVE = 0x0200
|
||||||
|
WM_LBUTTONDOWN = 0x0201
|
||||||
|
)
|
||||||
|
switch message {
|
||||||
|
case WM_COMMAND:
|
||||||
|
menuItemId := int32(wParam)
|
||||||
|
// https://docs.microsoft.com/en-us/windows/win32/menurc/wm-command#menus
|
||||||
|
switch menuItemId {
|
||||||
|
case quitMenuID:
|
||||||
|
select {
|
||||||
|
case t.callbacks.Quit <- struct{}{}:
|
||||||
|
// should not happen but in case not listening
|
||||||
|
default:
|
||||||
|
slog.Error("no listener on Quit")
|
||||||
|
}
|
||||||
|
case updateMenuID:
|
||||||
|
select {
|
||||||
|
case t.callbacks.Update <- struct{}{}:
|
||||||
|
// should not happen but in case not listening
|
||||||
|
default:
|
||||||
|
slog.Error("no listener on Update")
|
||||||
|
}
|
||||||
|
case diagLogsMenuID:
|
||||||
|
select {
|
||||||
|
case t.callbacks.ShowLogs <- struct{}{}:
|
||||||
|
// should not happen but in case not listening
|
||||||
|
default:
|
||||||
|
slog.Error("no listener on ShowLogs")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
slog.Debug(fmt.Sprintf("Unexpected menu item id: %d", menuItemId))
|
||||||
|
}
|
||||||
|
case WM_CLOSE:
|
||||||
|
boolRet, _, err := pDestroyWindow.Call(uintptr(t.window))
|
||||||
|
if boolRet == 0 {
|
||||||
|
slog.Error(fmt.Sprintf("failed to destroy window: %s", err))
|
||||||
|
}
|
||||||
|
err = t.wcex.unregister()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("failed to uregister windo %s", err))
|
||||||
|
}
|
||||||
|
case WM_DESTROY:
|
||||||
|
// same as WM_ENDSESSION, but throws 0 exit code after all
|
||||||
|
defer pPostQuitMessage.Call(uintptr(int32(0))) //nolint:errcheck
|
||||||
|
fallthrough
|
||||||
|
case WM_ENDSESSION:
|
||||||
|
t.muNID.Lock()
|
||||||
|
if t.nid != nil {
|
||||||
|
err := t.nid.delete()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("failed to delete nid: %s", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.muNID.Unlock()
|
||||||
|
case t.wmSystrayMessage:
|
||||||
|
switch lParam {
|
||||||
|
case WM_MOUSEMOVE, WM_LBUTTONDOWN:
|
||||||
|
// Ignore these...
|
||||||
|
case WM_RBUTTONUP, WM_LBUTTONUP:
|
||||||
|
err := t.showMenu()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("failed to show menu: %s", err))
|
||||||
|
}
|
||||||
|
case 0x405: // TODO - how is this magic value derived for the notification left click
|
||||||
|
if t.pendingUpdate {
|
||||||
|
select {
|
||||||
|
case t.callbacks.Update <- struct{}{}:
|
||||||
|
// should not happen but in case not listening
|
||||||
|
default:
|
||||||
|
slog.Error("no listener on Update")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
select {
|
||||||
|
case t.callbacks.DoFirstUse <- struct{}{}:
|
||||||
|
// should not happen but in case not listening
|
||||||
|
default:
|
||||||
|
slog.Error("no listener on DoFirstUse")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 0x404: // Middle click or close notification
|
||||||
|
// slog.Debug("doing nothing on close of first time notification")
|
||||||
|
default:
|
||||||
|
// 0x402 also seems common - what is it?
|
||||||
|
slog.Debug(fmt.Sprintf("unmanaged app message, lParm: 0x%x", lParam))
|
||||||
|
}
|
||||||
|
case t.wmTaskbarCreated: // on explorer.exe restarts
|
||||||
|
t.muNID.Lock()
|
||||||
|
err := t.nid.add()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("failed to refresh the taskbar on explorer restart: %s", err))
|
||||||
|
}
|
||||||
|
t.muNID.Unlock()
|
||||||
|
default:
|
||||||
|
// Calls the default window procedure to provide default processing for any window messages that an application does not process.
|
||||||
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms633572(v=vs.85).aspx
|
||||||
|
lResult, _, _ = pDefWindowProc.Call(
|
||||||
|
uintptr(hWnd),
|
||||||
|
uintptr(message),
|
||||||
|
uintptr(wParam),
|
||||||
|
uintptr(lParam),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *winTray) Quit() {
|
||||||
|
quitOnce.Do(quit)
|
||||||
|
}
|
||||||
|
|
||||||
|
func quit() {
|
||||||
|
boolRet, _, err := pPostMessage.Call(
|
||||||
|
uintptr(wt.window),
|
||||||
|
WM_CLOSE,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
if boolRet == 0 {
|
||||||
|
slog.Error(fmt.Sprintf("failed to post close message on shutdown %s", err))
|
||||||
|
}
|
||||||
|
}
|
71
app/tray/wintray/menus.go
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package wintray
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
updatAvailableMenuID = 1
|
||||||
|
updateMenuID = updatAvailableMenuID + 1
|
||||||
|
separatorMenuID = updateMenuID + 1
|
||||||
|
diagLogsMenuID = separatorMenuID + 1
|
||||||
|
diagSeparatorMenuID = diagLogsMenuID + 1
|
||||||
|
quitMenuID = diagSeparatorMenuID + 1
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t *winTray) initMenus() error {
|
||||||
|
if err := t.addOrUpdateMenuItem(diagLogsMenuID, 0, diagLogsMenuTitle, false); err != nil {
|
||||||
|
return fmt.Errorf("unable to create menu entries %w\n", err)
|
||||||
|
}
|
||||||
|
if err := t.addSeparatorMenuItem(diagSeparatorMenuID, 0); err != nil {
|
||||||
|
return fmt.Errorf("unable to create menu entries %w", err)
|
||||||
|
}
|
||||||
|
if err := t.addOrUpdateMenuItem(quitMenuID, 0, quitMenuTitle, false); err != nil {
|
||||||
|
return fmt.Errorf("unable to create menu entries %w\n", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *winTray) UpdateAvailable(ver string) error {
|
||||||
|
slog.Debug("updating menu and sending notification for new update")
|
||||||
|
if err := t.addOrUpdateMenuItem(updatAvailableMenuID, 0, updateAvailableMenuTitle, true); err != nil {
|
||||||
|
return fmt.Errorf("unable to create menu entries %w", err)
|
||||||
|
}
|
||||||
|
if err := t.addOrUpdateMenuItem(updateMenuID, 0, updateMenutTitle, false); err != nil {
|
||||||
|
return fmt.Errorf("unable to create menu entries %w", err)
|
||||||
|
}
|
||||||
|
if err := t.addSeparatorMenuItem(separatorMenuID, 0); err != nil {
|
||||||
|
return fmt.Errorf("unable to create menu entries %w", err)
|
||||||
|
}
|
||||||
|
iconFilePath, err := iconBytesToFilePath(wt.updateIcon)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to write icon data to temp file: %w", err)
|
||||||
|
}
|
||||||
|
if err := wt.setIcon(iconFilePath); err != nil {
|
||||||
|
return fmt.Errorf("unable to set icon: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.pendingUpdate = true
|
||||||
|
// Now pop up the notification
|
||||||
|
if !t.updateNotified {
|
||||||
|
t.muNID.Lock()
|
||||||
|
defer t.muNID.Unlock()
|
||||||
|
copy(t.nid.InfoTitle[:], windows.StringToUTF16(updateTitle))
|
||||||
|
copy(t.nid.Info[:], windows.StringToUTF16(fmt.Sprintf(updateMessage, ver)))
|
||||||
|
t.nid.Flags |= NIF_INFO
|
||||||
|
t.nid.Timeout = 10
|
||||||
|
t.nid.Size = uint32(unsafe.Sizeof(*wt.nid))
|
||||||
|
err = t.nid.modify()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.updateNotified = true
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
15
app/tray/wintray/messages.go
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package wintray
|
||||||
|
|
||||||
|
const (
|
||||||
|
firstTimeTitle = "Ollama is running"
|
||||||
|
firstTimeMessage = "Click here to get started"
|
||||||
|
updateTitle = "Update available"
|
||||||
|
updateMessage = "Ollama version %s is ready to install"
|
||||||
|
|
||||||
|
quitMenuTitle = "Quit Ollama"
|
||||||
|
updateAvailableMenuTitle = "An update is available"
|
||||||
|
updateMenutTitle = "Restart to update"
|
||||||
|
diagLogsMenuTitle = "View logs"
|
||||||
|
)
|
66
app/tray/wintray/notifyicon.go
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package wintray
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Contains information that the system needs to display notifications in the notification area.
|
||||||
|
// Used by Shell_NotifyIcon.
|
||||||
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/bb773352(v=vs.85).aspx
|
||||||
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/bb762159
|
||||||
|
type notifyIconData struct {
|
||||||
|
Size uint32
|
||||||
|
Wnd windows.Handle
|
||||||
|
ID, Flags, CallbackMessage uint32
|
||||||
|
Icon windows.Handle
|
||||||
|
Tip [128]uint16
|
||||||
|
State, StateMask uint32
|
||||||
|
Info [256]uint16
|
||||||
|
// Timeout, Version uint32
|
||||||
|
Timeout uint32
|
||||||
|
|
||||||
|
InfoTitle [64]uint16
|
||||||
|
InfoFlags uint32
|
||||||
|
GuidItem windows.GUID
|
||||||
|
BalloonIcon windows.Handle
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nid *notifyIconData) add() error {
|
||||||
|
const NIM_ADD = 0x00000000
|
||||||
|
res, _, err := pShellNotifyIcon.Call(
|
||||||
|
uintptr(NIM_ADD),
|
||||||
|
uintptr(unsafe.Pointer(nid)),
|
||||||
|
)
|
||||||
|
if res == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nid *notifyIconData) modify() error {
|
||||||
|
const NIM_MODIFY = 0x00000001
|
||||||
|
res, _, err := pShellNotifyIcon.Call(
|
||||||
|
uintptr(NIM_MODIFY),
|
||||||
|
uintptr(unsafe.Pointer(nid)),
|
||||||
|
)
|
||||||
|
if res == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nid *notifyIconData) delete() error {
|
||||||
|
const NIM_DELETE = 0x00000002
|
||||||
|
res, _, err := pShellNotifyIcon.Call(
|
||||||
|
uintptr(NIM_DELETE),
|
||||||
|
uintptr(unsafe.Pointer(nid)),
|
||||||
|
)
|
||||||
|
if res == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
485
app/tray/wintray/tray.go
Normal file
|
@ -0,0 +1,485 @@
|
||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package wintray
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/jmorganca/ollama/app/tray/commontray"
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Helpful sources: https://github.com/golang/exp/blob/master/shiny/driver/internal/win32
|
||||||
|
|
||||||
|
// Contains information about loaded resources
|
||||||
|
type winTray struct {
|
||||||
|
instance,
|
||||||
|
icon,
|
||||||
|
cursor,
|
||||||
|
window windows.Handle
|
||||||
|
|
||||||
|
loadedImages map[string]windows.Handle
|
||||||
|
muLoadedImages sync.RWMutex
|
||||||
|
|
||||||
|
// menus keeps track of the submenus keyed by the menu item ID, plus 0
|
||||||
|
// which corresponds to the main popup menu.
|
||||||
|
menus map[uint32]windows.Handle
|
||||||
|
muMenus sync.RWMutex
|
||||||
|
menuOf map[uint32]windows.Handle
|
||||||
|
muMenuOf sync.RWMutex
|
||||||
|
// menuItemIcons maintains the bitmap of each menu item (if applies). It's
|
||||||
|
// needed to show the icon correctly when showing a previously hidden menu
|
||||||
|
// item again.
|
||||||
|
// menuItemIcons map[uint32]windows.Handle
|
||||||
|
// muMenuItemIcons sync.RWMutex
|
||||||
|
visibleItems map[uint32][]uint32
|
||||||
|
muVisibleItems sync.RWMutex
|
||||||
|
|
||||||
|
nid *notifyIconData
|
||||||
|
muNID sync.RWMutex
|
||||||
|
wcex *wndClassEx
|
||||||
|
|
||||||
|
wmSystrayMessage,
|
||||||
|
wmTaskbarCreated uint32
|
||||||
|
|
||||||
|
pendingUpdate bool
|
||||||
|
updateNotified bool // Only pop up the notification once - TODO consider daily nag?
|
||||||
|
// Callbacks
|
||||||
|
callbacks commontray.Callbacks
|
||||||
|
normalIcon []byte
|
||||||
|
updateIcon []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
var wt winTray
|
||||||
|
|
||||||
|
func (t *winTray) GetCallbacks() commontray.Callbacks {
|
||||||
|
return t.callbacks
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitTray(icon, updateIcon []byte) (*winTray, error) {
|
||||||
|
wt.callbacks.Quit = make(chan struct{})
|
||||||
|
wt.callbacks.Update = make(chan struct{})
|
||||||
|
wt.callbacks.ShowLogs = make(chan struct{})
|
||||||
|
wt.callbacks.DoFirstUse = make(chan struct{})
|
||||||
|
wt.normalIcon = icon
|
||||||
|
wt.updateIcon = updateIcon
|
||||||
|
if err := wt.initInstance(); err != nil {
|
||||||
|
return nil, fmt.Errorf("Unable to init instance: %w\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := wt.createMenu(); err != nil {
|
||||||
|
return nil, fmt.Errorf("Unable to create menu: %w\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
iconFilePath, err := iconBytesToFilePath(wt.normalIcon)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Unable to write icon data to temp file: %w", err)
|
||||||
|
}
|
||||||
|
if err := wt.setIcon(iconFilePath); err != nil {
|
||||||
|
return nil, fmt.Errorf("Unable to set icon: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &wt, wt.initMenus()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *winTray) initInstance() error {
|
||||||
|
const (
|
||||||
|
className = "OllamaClass"
|
||||||
|
windowName = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
t.wmSystrayMessage = WM_USER + 1
|
||||||
|
t.visibleItems = make(map[uint32][]uint32)
|
||||||
|
t.menus = make(map[uint32]windows.Handle)
|
||||||
|
t.menuOf = make(map[uint32]windows.Handle)
|
||||||
|
|
||||||
|
t.loadedImages = make(map[string]windows.Handle)
|
||||||
|
|
||||||
|
taskbarEventNamePtr, _ := windows.UTF16PtrFromString("TaskbarCreated")
|
||||||
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms644947
|
||||||
|
res, _, err := pRegisterWindowMessage.Call(
|
||||||
|
uintptr(unsafe.Pointer(taskbarEventNamePtr)),
|
||||||
|
)
|
||||||
|
if res == 0 { // success 0xc000-0xfff
|
||||||
|
return fmt.Errorf("failed to register window: %w", err)
|
||||||
|
}
|
||||||
|
t.wmTaskbarCreated = uint32(res)
|
||||||
|
|
||||||
|
instanceHandle, _, err := pGetModuleHandle.Call(0)
|
||||||
|
if instanceHandle == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.instance = windows.Handle(instanceHandle)
|
||||||
|
|
||||||
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms648072(v=vs.85).aspx
|
||||||
|
iconHandle, _, err := pLoadIcon.Call(0, uintptr(IDI_APPLICATION))
|
||||||
|
if iconHandle == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.icon = windows.Handle(iconHandle)
|
||||||
|
|
||||||
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms648391(v=vs.85).aspx
|
||||||
|
cursorHandle, _, err := pLoadCursor.Call(0, uintptr(IDC_ARROW))
|
||||||
|
if cursorHandle == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.cursor = windows.Handle(cursorHandle)
|
||||||
|
|
||||||
|
classNamePtr, err := windows.UTF16PtrFromString(className)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
windowNamePtr, err := windows.UTF16PtrFromString(windowName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
t.wcex = &wndClassEx{
|
||||||
|
Style: CS_HREDRAW | CS_VREDRAW,
|
||||||
|
WndProc: windows.NewCallback(t.wndProc),
|
||||||
|
Instance: t.instance,
|
||||||
|
Icon: t.icon,
|
||||||
|
Cursor: t.cursor,
|
||||||
|
Background: windows.Handle(6), // (COLOR_WINDOW + 1)
|
||||||
|
ClassName: classNamePtr,
|
||||||
|
IconSm: t.icon,
|
||||||
|
}
|
||||||
|
if err := t.wcex.register(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
windowHandle, _, err := pCreateWindowEx.Call(
|
||||||
|
uintptr(0),
|
||||||
|
uintptr(unsafe.Pointer(classNamePtr)),
|
||||||
|
uintptr(unsafe.Pointer(windowNamePtr)),
|
||||||
|
uintptr(WS_OVERLAPPEDWINDOW),
|
||||||
|
uintptr(CW_USEDEFAULT),
|
||||||
|
uintptr(CW_USEDEFAULT),
|
||||||
|
uintptr(CW_USEDEFAULT),
|
||||||
|
uintptr(CW_USEDEFAULT),
|
||||||
|
uintptr(0),
|
||||||
|
uintptr(0),
|
||||||
|
uintptr(t.instance),
|
||||||
|
uintptr(0),
|
||||||
|
)
|
||||||
|
if windowHandle == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.window = windows.Handle(windowHandle)
|
||||||
|
|
||||||
|
pShowWindow.Call(uintptr(t.window), uintptr(SW_HIDE)) //nolint:errcheck
|
||||||
|
|
||||||
|
boolRet, _, err := pUpdateWindow.Call(uintptr(t.window))
|
||||||
|
if boolRet == 0 {
|
||||||
|
slog.Error(fmt.Sprintf("failed to update window: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
t.muNID.Lock()
|
||||||
|
defer t.muNID.Unlock()
|
||||||
|
t.nid = ¬ifyIconData{
|
||||||
|
Wnd: windows.Handle(t.window),
|
||||||
|
ID: 100,
|
||||||
|
Flags: NIF_MESSAGE,
|
||||||
|
CallbackMessage: t.wmSystrayMessage,
|
||||||
|
}
|
||||||
|
t.nid.Size = uint32(unsafe.Sizeof(*t.nid))
|
||||||
|
|
||||||
|
return t.nid.add()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *winTray) createMenu() error {
|
||||||
|
|
||||||
|
menuHandle, _, err := pCreatePopupMenu.Call()
|
||||||
|
if menuHandle == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.menus[0] = windows.Handle(menuHandle)
|
||||||
|
|
||||||
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms647575(v=vs.85).aspx
|
||||||
|
mi := struct {
|
||||||
|
Size, Mask, Style, Max uint32
|
||||||
|
Background windows.Handle
|
||||||
|
ContextHelpID uint32
|
||||||
|
MenuData uintptr
|
||||||
|
}{
|
||||||
|
Mask: MIM_APPLYTOSUBMENUS,
|
||||||
|
}
|
||||||
|
mi.Size = uint32(unsafe.Sizeof(mi))
|
||||||
|
|
||||||
|
res, _, err := pSetMenuInfo.Call(
|
||||||
|
uintptr(t.menus[0]),
|
||||||
|
uintptr(unsafe.Pointer(&mi)),
|
||||||
|
)
|
||||||
|
if res == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contains information about a menu item.
|
||||||
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms647578(v=vs.85).aspx
|
||||||
|
type menuItemInfo struct {
|
||||||
|
Size, Mask, Type, State uint32
|
||||||
|
ID uint32
|
||||||
|
SubMenu, Checked, Unchecked windows.Handle
|
||||||
|
ItemData uintptr
|
||||||
|
TypeData *uint16
|
||||||
|
Cch uint32
|
||||||
|
BMPItem windows.Handle
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *winTray) addOrUpdateMenuItem(menuItemId uint32, parentId uint32, title string, disabled bool) error {
|
||||||
|
titlePtr, err := windows.UTF16PtrFromString(title)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
mi := menuItemInfo{
|
||||||
|
Mask: MIIM_FTYPE | MIIM_STRING | MIIM_ID | MIIM_STATE,
|
||||||
|
Type: MFT_STRING,
|
||||||
|
ID: uint32(menuItemId),
|
||||||
|
TypeData: titlePtr,
|
||||||
|
Cch: uint32(len(title)),
|
||||||
|
}
|
||||||
|
mi.Size = uint32(unsafe.Sizeof(mi))
|
||||||
|
if disabled {
|
||||||
|
mi.State |= MFS_DISABLED
|
||||||
|
}
|
||||||
|
|
||||||
|
var res uintptr
|
||||||
|
t.muMenus.RLock()
|
||||||
|
menu := t.menus[parentId]
|
||||||
|
t.muMenus.RUnlock()
|
||||||
|
if t.getVisibleItemIndex(parentId, menuItemId) != -1 {
|
||||||
|
// We set the menu item info based on the menuID
|
||||||
|
boolRet, _, err := pSetMenuItemInfo.Call(
|
||||||
|
uintptr(menu),
|
||||||
|
uintptr(menuItemId),
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(&mi)),
|
||||||
|
)
|
||||||
|
if boolRet == 0 {
|
||||||
|
return fmt.Errorf("failed to set menu item: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if res == 0 {
|
||||||
|
// Menu item does not already exist, create it
|
||||||
|
t.muMenus.RLock()
|
||||||
|
submenu, exists := t.menus[menuItemId]
|
||||||
|
t.muMenus.RUnlock()
|
||||||
|
if exists {
|
||||||
|
mi.Mask |= MIIM_SUBMENU
|
||||||
|
mi.SubMenu = submenu
|
||||||
|
}
|
||||||
|
t.addToVisibleItems(parentId, menuItemId)
|
||||||
|
position := t.getVisibleItemIndex(parentId, menuItemId)
|
||||||
|
res, _, err = pInsertMenuItem.Call(
|
||||||
|
uintptr(menu),
|
||||||
|
uintptr(position),
|
||||||
|
1,
|
||||||
|
uintptr(unsafe.Pointer(&mi)),
|
||||||
|
)
|
||||||
|
if res == 0 {
|
||||||
|
t.delFromVisibleItems(parentId, menuItemId)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.muMenuOf.Lock()
|
||||||
|
t.menuOf[menuItemId] = menu
|
||||||
|
t.muMenuOf.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *winTray) addSeparatorMenuItem(menuItemId, parentId uint32) error {
|
||||||
|
|
||||||
|
mi := menuItemInfo{
|
||||||
|
Mask: MIIM_FTYPE | MIIM_ID | MIIM_STATE,
|
||||||
|
Type: MFT_SEPARATOR,
|
||||||
|
ID: uint32(menuItemId),
|
||||||
|
}
|
||||||
|
|
||||||
|
mi.Size = uint32(unsafe.Sizeof(mi))
|
||||||
|
|
||||||
|
t.addToVisibleItems(parentId, menuItemId)
|
||||||
|
position := t.getVisibleItemIndex(parentId, menuItemId)
|
||||||
|
t.muMenus.RLock()
|
||||||
|
menu := uintptr(t.menus[parentId])
|
||||||
|
t.muMenus.RUnlock()
|
||||||
|
res, _, err := pInsertMenuItem.Call(
|
||||||
|
menu,
|
||||||
|
uintptr(position),
|
||||||
|
1,
|
||||||
|
uintptr(unsafe.Pointer(&mi)),
|
||||||
|
)
|
||||||
|
if res == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// func (t *winTray) hideMenuItem(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)
|
||||||
|
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
func (t *winTray) showMenu() error {
|
||||||
|
p := point{}
|
||||||
|
boolRet, _, err := pGetCursorPos.Call(uintptr(unsafe.Pointer(&p)))
|
||||||
|
if boolRet == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
boolRet, _, err = pSetForegroundWindow.Call(uintptr(t.window))
|
||||||
|
if boolRet == 0 {
|
||||||
|
slog.Warn(fmt.Sprintf("failed to bring menu to foreground: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
boolRet, _, err = pTrackPopupMenu.Call(
|
||||||
|
uintptr(t.menus[0]),
|
||||||
|
TPM_BOTTOMALIGN|TPM_LEFTALIGN,
|
||||||
|
uintptr(p.X),
|
||||||
|
uintptr(p.Y),
|
||||||
|
0,
|
||||||
|
uintptr(t.window),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
if boolRet == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *winTray) delFromVisibleItems(parent, val uint32) {
|
||||||
|
t.muVisibleItems.Lock()
|
||||||
|
defer t.muVisibleItems.Unlock()
|
||||||
|
visibleItems := t.visibleItems[parent]
|
||||||
|
for i, itemval := range visibleItems {
|
||||||
|
if val == itemval {
|
||||||
|
t.visibleItems[parent] = append(visibleItems[:i], visibleItems[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *winTray) addToVisibleItems(parent, val uint32) {
|
||||||
|
t.muVisibleItems.Lock()
|
||||||
|
defer t.muVisibleItems.Unlock()
|
||||||
|
if visibleItems, exists := t.visibleItems[parent]; !exists {
|
||||||
|
t.visibleItems[parent] = []uint32{val}
|
||||||
|
} else {
|
||||||
|
newvisible := append(visibleItems, val)
|
||||||
|
sort.Slice(newvisible, func(i, j int) bool { return newvisible[i] < newvisible[j] })
|
||||||
|
t.visibleItems[parent] = newvisible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *winTray) getVisibleItemIndex(parent, val uint32) int {
|
||||||
|
t.muVisibleItems.RLock()
|
||||||
|
defer t.muVisibleItems.RUnlock()
|
||||||
|
for i, itemval := range t.visibleItems[parent] {
|
||||||
|
if val == itemval {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func iconBytesToFilePath(iconBytes []byte) (string, error) {
|
||||||
|
bh := md5.Sum(iconBytes)
|
||||||
|
dataHash := hex.EncodeToString(bh[:])
|
||||||
|
iconFilePath := filepath.Join(os.TempDir(), "ollama_temp_icon_"+dataHash)
|
||||||
|
|
||||||
|
if _, err := os.Stat(iconFilePath); os.IsNotExist(err) {
|
||||||
|
if err := os.WriteFile(iconFilePath, iconBytes, 0644); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return iconFilePath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loads an image from file and shows it in tray.
|
||||||
|
// Shell_NotifyIcon: https://msdn.microsoft.com/en-us/library/windows/desktop/bb762159(v=vs.85).aspx
|
||||||
|
func (t *winTray) setIcon(src string) error {
|
||||||
|
|
||||||
|
h, err := t.loadIconFrom(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
t.muNID.Lock()
|
||||||
|
defer t.muNID.Unlock()
|
||||||
|
t.nid.Icon = h
|
||||||
|
t.nid.Flags |= NIF_ICON
|
||||||
|
t.nid.Size = uint32(unsafe.Sizeof(*t.nid))
|
||||||
|
|
||||||
|
return t.nid.modify()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loads an image from file to be shown in tray or menu item.
|
||||||
|
// LoadImage: https://msdn.microsoft.com/en-us/library/windows/desktop/ms648045(v=vs.85).aspx
|
||||||
|
func (t *winTray) loadIconFrom(src string) (windows.Handle, error) {
|
||||||
|
|
||||||
|
// Save and reuse handles of loaded images
|
||||||
|
t.muLoadedImages.RLock()
|
||||||
|
h, ok := t.loadedImages[src]
|
||||||
|
t.muLoadedImages.RUnlock()
|
||||||
|
if !ok {
|
||||||
|
srcPtr, err := windows.UTF16PtrFromString(src)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
res, _, err := pLoadImage.Call(
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(srcPtr)),
|
||||||
|
IMAGE_ICON,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
LR_LOADFROMFILE|LR_DEFAULTSIZE,
|
||||||
|
)
|
||||||
|
if res == 0 {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
h = windows.Handle(res)
|
||||||
|
t.muLoadedImages.Lock()
|
||||||
|
t.loadedImages[src] = h
|
||||||
|
t.muLoadedImages.Unlock()
|
||||||
|
}
|
||||||
|
return h, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *winTray) DisplayFirstUseNotification() error {
|
||||||
|
t.muNID.Lock()
|
||||||
|
defer t.muNID.Unlock()
|
||||||
|
copy(t.nid.InfoTitle[:], windows.StringToUTF16(firstTimeTitle))
|
||||||
|
copy(t.nid.Info[:], windows.StringToUTF16(firstTimeMessage))
|
||||||
|
t.nid.Flags |= NIF_INFO
|
||||||
|
t.nid.Size = uint32(unsafe.Sizeof(*wt.nid))
|
||||||
|
|
||||||
|
return t.nid.modify()
|
||||||
|
}
|
89
app/tray/wintray/w32api.go
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package wintray
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
k32 = windows.NewLazySystemDLL("Kernel32.dll")
|
||||||
|
u32 = windows.NewLazySystemDLL("User32.dll")
|
||||||
|
s32 = windows.NewLazySystemDLL("Shell32.dll")
|
||||||
|
|
||||||
|
pCreatePopupMenu = u32.NewProc("CreatePopupMenu")
|
||||||
|
pCreateWindowEx = u32.NewProc("CreateWindowExW")
|
||||||
|
pDefWindowProc = u32.NewProc("DefWindowProcW")
|
||||||
|
pDestroyWindow = u32.NewProc("DestroyWindow")
|
||||||
|
pDispatchMessage = u32.NewProc("DispatchMessageW")
|
||||||
|
pGetCursorPos = u32.NewProc("GetCursorPos")
|
||||||
|
pGetMessage = u32.NewProc("GetMessageW")
|
||||||
|
pGetModuleHandle = k32.NewProc("GetModuleHandleW")
|
||||||
|
pInsertMenuItem = u32.NewProc("InsertMenuItemW")
|
||||||
|
pLoadCursor = u32.NewProc("LoadCursorW")
|
||||||
|
pLoadIcon = u32.NewProc("LoadIconW")
|
||||||
|
pLoadImage = u32.NewProc("LoadImageW")
|
||||||
|
pPostMessage = u32.NewProc("PostMessageW")
|
||||||
|
pPostQuitMessage = u32.NewProc("PostQuitMessage")
|
||||||
|
pRegisterClass = u32.NewProc("RegisterClassExW")
|
||||||
|
pRegisterWindowMessage = u32.NewProc("RegisterWindowMessageW")
|
||||||
|
pSetForegroundWindow = u32.NewProc("SetForegroundWindow")
|
||||||
|
pSetMenuInfo = u32.NewProc("SetMenuInfo")
|
||||||
|
pSetMenuItemInfo = u32.NewProc("SetMenuItemInfoW")
|
||||||
|
pShellNotifyIcon = s32.NewProc("Shell_NotifyIconW")
|
||||||
|
pShowWindow = u32.NewProc("ShowWindow")
|
||||||
|
pTrackPopupMenu = u32.NewProc("TrackPopupMenu")
|
||||||
|
pTranslateMessage = u32.NewProc("TranslateMessage")
|
||||||
|
pUnregisterClass = u32.NewProc("UnregisterClassW")
|
||||||
|
pUpdateWindow = u32.NewProc("UpdateWindow")
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
CS_HREDRAW = 0x0002
|
||||||
|
CS_VREDRAW = 0x0001
|
||||||
|
CW_USEDEFAULT = 0x80000000
|
||||||
|
IDC_ARROW = 32512 // Standard arrow
|
||||||
|
IDI_APPLICATION = 32512
|
||||||
|
IMAGE_ICON = 1 // Loads an icon
|
||||||
|
LR_DEFAULTSIZE = 0x00000040 // Loads default-size icon for windows(SM_CXICON x SM_CYICON) if cx, cy are set to zero
|
||||||
|
LR_LOADFROMFILE = 0x00000010 // Loads the stand-alone image from the file
|
||||||
|
MF_BYCOMMAND = 0x00000000
|
||||||
|
MFS_DISABLED = 0x00000003
|
||||||
|
MFT_SEPARATOR = 0x00000800
|
||||||
|
MFT_STRING = 0x00000000
|
||||||
|
MIIM_BITMAP = 0x00000080
|
||||||
|
MIIM_FTYPE = 0x00000100
|
||||||
|
MIIM_ID = 0x00000002
|
||||||
|
MIIM_STATE = 0x00000001
|
||||||
|
MIIM_STRING = 0x00000040
|
||||||
|
MIIM_SUBMENU = 0x00000004
|
||||||
|
MIM_APPLYTOSUBMENUS = 0x80000000
|
||||||
|
NIF_ICON = 0x00000002
|
||||||
|
NIF_INFO = 0x00000010
|
||||||
|
NIF_MESSAGE = 0x00000001
|
||||||
|
SW_HIDE = 0
|
||||||
|
TPM_BOTTOMALIGN = 0x0020
|
||||||
|
TPM_LEFTALIGN = 0x0000
|
||||||
|
WM_CLOSE = 0x0010
|
||||||
|
WM_USER = 0x0400
|
||||||
|
WS_CAPTION = 0x00C00000
|
||||||
|
WS_MAXIMIZEBOX = 0x00010000
|
||||||
|
WS_MINIMIZEBOX = 0x00020000
|
||||||
|
WS_OVERLAPPED = 0x00000000
|
||||||
|
WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX
|
||||||
|
WS_SYSMENU = 0x00080000
|
||||||
|
WS_THICKFRAME = 0x00040000
|
||||||
|
)
|
||||||
|
|
||||||
|
// Not sure if this is actually needed on windows
|
||||||
|
func init() {
|
||||||
|
runtime.LockOSThread()
|
||||||
|
}
|
||||||
|
|
||||||
|
// The POINT structure defines the x- and y- coordinates of a point.
|
||||||
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd162805(v=vs.85).aspx
|
||||||
|
type point struct {
|
||||||
|
X, Y int32
|
||||||
|
}
|
45
app/tray/wintray/winclass.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package wintray
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Contains window class information.
|
||||||
|
// It is used with the RegisterClassEx and GetClassInfoEx functions.
|
||||||
|
// https://msdn.microsoft.com/en-us/library/ms633577.aspx
|
||||||
|
type wndClassEx struct {
|
||||||
|
Size, Style uint32
|
||||||
|
WndProc uintptr
|
||||||
|
ClsExtra, WndExtra int32
|
||||||
|
Instance, Icon, Cursor, Background windows.Handle
|
||||||
|
MenuName, ClassName *uint16
|
||||||
|
IconSm windows.Handle
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registers a window class for subsequent use in calls to the CreateWindow or CreateWindowEx function.
|
||||||
|
// https://msdn.microsoft.com/en-us/library/ms633587.aspx
|
||||||
|
func (w *wndClassEx) register() error {
|
||||||
|
w.Size = uint32(unsafe.Sizeof(*w))
|
||||||
|
res, _, err := pRegisterClass.Call(uintptr(unsafe.Pointer(w)))
|
||||||
|
if res == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unregisters a window class, freeing the memory required for the class.
|
||||||
|
// https://msdn.microsoft.com/en-us/library/ms644899.aspx
|
||||||
|
func (w *wndClassEx) unregister() error {
|
||||||
|
res, _, err := pUnregisterClass.Call(
|
||||||
|
uintptr(unsafe.Pointer(w.ClassName)),
|
||||||
|
uintptr(w.Instance),
|
||||||
|
)
|
||||||
|
if res == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
61
auth/auth.go
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultPrivateKey = "id_ed25519"
|
||||||
|
|
||||||
|
func NewNonce(r io.Reader, length int) (string, error) {
|
||||||
|
nonce := make([]byte, length)
|
||||||
|
if _, err := io.ReadFull(r, nonce); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return base64.RawURLEncoding.EncodeToString(nonce), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Sign(ctx context.Context, bts []byte) (string, error) {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
keyPath := filepath.Join(home, ".ollama", defaultPrivateKey)
|
||||||
|
|
||||||
|
privateKeyFile, err := os.ReadFile(keyPath)
|
||||||
|
if err != nil {
|
||||||
|
slog.Info(fmt.Sprintf("Failed to load private key: %v", err))
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
privateKey, err := ssh.ParsePrivateKey(privateKeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the pubkey, but remove the type
|
||||||
|
publicKey := ssh.MarshalAuthorizedKey(privateKey.PublicKey())
|
||||||
|
parts := bytes.Split(publicKey, []byte(" "))
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return "", fmt.Errorf("malformed public key")
|
||||||
|
}
|
||||||
|
|
||||||
|
signedData, err := privateKey.Sign(rand.Reader, bts)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// signature is <pubkey>:<signature>
|
||||||
|
return fmt.Sprintf("%s:%s", bytes.TrimSpace(parts[1]), base64.StdEncoding.EncodeToString(signedData.Blob)), nil
|
||||||
|
}
|
35
cmd/cmd.go
|
@ -14,7 +14,6 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
@ -22,6 +21,8 @@ import (
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/containerd/console"
|
||||||
|
|
||||||
"github.com/olekukonko/tablewriter"
|
"github.com/olekukonko/tablewriter"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
|
@ -754,22 +755,8 @@ func initializeKeypair() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func startMacApp(ctx context.Context, client *api.Client) error {
|
//nolint:unused
|
||||||
exe, err := os.Executable()
|
func waitForServer(ctx context.Context, client *api.Client) error {
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
link, err := os.Readlink(exe)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !strings.Contains(link, "Ollama.app") {
|
|
||||||
return fmt.Errorf("could not find ollama app")
|
|
||||||
}
|
|
||||||
path := strings.Split(link, "Ollama.app")
|
|
||||||
if err := exec.Command("/usr/bin/open", "-a", path[0]+"Ollama.app").Run(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// wait for the server to start
|
// wait for the server to start
|
||||||
timeout := time.After(5 * time.Second)
|
timeout := time.After(5 * time.Second)
|
||||||
tick := time.Tick(500 * time.Millisecond)
|
tick := time.Tick(500 * time.Millisecond)
|
||||||
|
@ -783,6 +770,7 @@ func startMacApp(ctx context.Context, client *api.Client) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkServerHeartbeat(cmd *cobra.Command, _ []string) error {
|
func checkServerHeartbeat(cmd *cobra.Command, _ []string) error {
|
||||||
|
@ -791,16 +779,12 @@ func checkServerHeartbeat(cmd *cobra.Command, _ []string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := client.Heartbeat(cmd.Context()); err != nil {
|
if err := client.Heartbeat(cmd.Context()); err != nil {
|
||||||
if !strings.Contains(err.Error(), "connection refused") {
|
if !strings.Contains(err.Error(), " refused") {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if runtime.GOOS == "darwin" {
|
if err := startApp(cmd.Context(), client); err != nil {
|
||||||
if err := startMacApp(cmd.Context(), client); err != nil {
|
|
||||||
return fmt.Errorf("could not connect to ollama app, is it running?")
|
return fmt.Errorf("could not connect to ollama app, is it running?")
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return fmt.Errorf("could not connect to ollama server, run 'ollama serve' to start it")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -829,6 +813,11 @@ func NewCLI() *cobra.Command {
|
||||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||||
cobra.EnableCommandSorting = false
|
cobra.EnableCommandSorting = false
|
||||||
|
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
// Enable colorful ANSI escape code in Windows terminal (disabled by default)
|
||||||
|
console.ConsoleFromFile(os.Stdout) //nolint:errcheck
|
||||||
|
}
|
||||||
|
|
||||||
rootCmd := &cobra.Command{
|
rootCmd := &cobra.Command{
|
||||||
Use: "ollama",
|
Use: "ollama",
|
||||||
Short: "Large language model runner",
|
Short: "Large language model runner",
|
||||||
|
|
30
cmd/start_darwin.go
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/jmorganca/ollama/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func startApp(ctx context.Context, client *api.Client) error {
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
link, err := os.Readlink(exe)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !strings.Contains(link, "Ollama.app") {
|
||||||
|
return fmt.Errorf("could not find ollama app")
|
||||||
|
}
|
||||||
|
path := strings.Split(link, "Ollama.app")
|
||||||
|
if err := exec.Command("/usr/bin/open", "-a", path[0]+"Ollama.app").Run(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return waitForServer(ctx, client)
|
||||||
|
}
|
14
cmd/start_default.go
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
//go:build !windows && !darwin
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/jmorganca/ollama/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func startApp(ctx context.Context, client *api.Client) error {
|
||||||
|
return fmt.Errorf("could not connect to ollama server, run 'ollama serve' to start it")
|
||||||
|
}
|
58
cmd/start_windows.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/jmorganca/ollama/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func startApp(ctx context.Context, client *api.Client) error {
|
||||||
|
// log.Printf("XXX Attempting to find and start ollama app")
|
||||||
|
AppName := "ollama app.exe"
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
appExe := filepath.Join(filepath.Dir(exe), AppName)
|
||||||
|
_, err = os.Stat(appExe)
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
// Try the standard install location
|
||||||
|
localAppData := os.Getenv("LOCALAPPDATA")
|
||||||
|
appExe = filepath.Join(localAppData, "Ollama", AppName)
|
||||||
|
_, err := os.Stat(appExe)
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
// Finally look in the path
|
||||||
|
appExe, err = exec.LookPath(AppName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not locate ollama app")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// log.Printf("XXX attempting to start app %s", appExe)
|
||||||
|
|
||||||
|
cmd_path := "c:\\Windows\\system32\\cmd.exe"
|
||||||
|
cmd := exec.Command(cmd_path, "/c", appExe)
|
||||||
|
// TODO - these hide flags aren't working - still pops up a command window for some reason
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: 0x08000000, HideWindow: true}
|
||||||
|
|
||||||
|
// TODO this didn't help either...
|
||||||
|
cmd.Stdin = strings.NewReader("")
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return fmt.Errorf("unable to start ollama app %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Process != nil {
|
||||||
|
defer cmd.Process.Release() //nolint:errcheck
|
||||||
|
}
|
||||||
|
return waitForServer(ctx, client)
|
||||||
|
}
|
|
@ -1,18 +1,18 @@
|
||||||
# How to troubleshoot issues
|
# How to troubleshoot issues
|
||||||
|
|
||||||
Sometimes Ollama may not perform as expected. One of the best ways to figure out what happened is to take a look at the logs. Find the logs on Mac by running the command:
|
Sometimes Ollama may not perform as expected. One of the best ways to figure out what happened is to take a look at the logs. Find the logs on **Mac** by running the command:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
cat ~/.ollama/logs/server.log
|
cat ~/.ollama/logs/server.log
|
||||||
```
|
```
|
||||||
|
|
||||||
On Linux systems with systemd, the logs can be found with this command:
|
On **Linux** systems with systemd, the logs can be found with this command:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
journalctl -u ollama
|
journalctl -u ollama
|
||||||
```
|
```
|
||||||
|
|
||||||
When you run Ollama in a container, the logs go to stdout/stderr in the container:
|
When you run Ollama in a **container**, the logs go to stdout/stderr in the container:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker logs <container-name>
|
docker logs <container-name>
|
||||||
|
@ -21,6 +21,18 @@ docker logs <container-name>
|
||||||
|
|
||||||
If manually running `ollama serve` in a terminal, the logs will be on that terminal.
|
If manually running `ollama serve` in a terminal, the logs will be on that terminal.
|
||||||
|
|
||||||
|
When you run Ollama on **Windows**, there are a few different locations. You can view them in the explorer window by hitting `<cmd>+R` and type in:
|
||||||
|
- `explorer %LOCALAPPDATA%\Ollama` to view logs
|
||||||
|
- `explorer %LOCALAPPDATA%\Programs\Ollama` to browse the binaries (The installer adds this to your user PATH)
|
||||||
|
- `explorer %HOMEPATH%\.ollama` to browse where models and configuration is stored
|
||||||
|
- `explorer %TEMP%` where temporary executable files are stored in one or more `ollama*` directories
|
||||||
|
|
||||||
|
To enable additional debug logging to help troubleshoot problems, first **Quit the running app from the tray menu** then in a powershell terminal
|
||||||
|
```powershell
|
||||||
|
$env:OLLAMA_DEBUG="1"
|
||||||
|
& "ollama app.exe"
|
||||||
|
```
|
||||||
|
|
||||||
Join the [Discord](https://discord.gg/ollama) for help interpreting the logs.
|
Join the [Discord](https://discord.gg/ollama) for help interpreting the logs.
|
||||||
|
|
||||||
## LLM libraries
|
## LLM libraries
|
||||||
|
|
46
docs/windows.md
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
# Ollama Windows Preview
|
||||||
|
|
||||||
|
Welcome to the Ollama Windows preview.
|
||||||
|
|
||||||
|
No more WSL required!
|
||||||
|
|
||||||
|
Ollama now runs as a native Windows application, including NVIDIA GPU support.
|
||||||
|
After installing Ollama Windows Preview, Ollama will run in the background and
|
||||||
|
the `ollama` command line is available in `cmd`, `powershell` or your favorite
|
||||||
|
terminal application. As usual the Ollama [api](./api.md) will be served on
|
||||||
|
`http://localhost:11434`.
|
||||||
|
|
||||||
|
As this is a preview release, you should expect a few bugs here and there. If
|
||||||
|
you run into a problem you can reach out on
|
||||||
|
[Discord](https://discord.gg/ollama), or file an
|
||||||
|
[issue](https://github.com/ollama/ollama/issues).
|
||||||
|
Logs will often be helpful in dianosing the problem (see
|
||||||
|
[Troubleshooting](#troubleshooting) below)
|
||||||
|
|
||||||
|
## System Requirements
|
||||||
|
|
||||||
|
* Windows 10 or newer, Home or Pro
|
||||||
|
* NVIDIA 452.39 or newer Drivers if you have an NVIDIA card
|
||||||
|
|
||||||
|
## API Access
|
||||||
|
|
||||||
|
Here's a quick example showing API access from `powershell`
|
||||||
|
```powershell
|
||||||
|
(Invoke-WebRequest -method POST -Body '{"model":"llama2", "prompt":"Why is the sky blue?", "stream": false}' -uri http://localhost:11434/api/generate ).Content | ConvertFrom-json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
While we're in preview, `OLLAMA_DEBUG` is always enabled, which adds
|
||||||
|
a "view logs" menu item to the app, and increses logging for the GUI app and
|
||||||
|
server.
|
||||||
|
|
||||||
|
Ollama on Windows stores files in a few different locations. You can view them in
|
||||||
|
the explorer window by hitting `<cmd>+R` and type in:
|
||||||
|
- `explorer %LOCALAPPDATA%\Ollama` contains logs, and downloaded updates
|
||||||
|
- *app.log* contains logs from the GUI application
|
||||||
|
- *server.log* contains the server logs
|
||||||
|
- *upgrade.log* contains log output for upgrades
|
||||||
|
- `explorer %LOCALAPPDATA%\Programs\Ollama` contains the binaries (The installer adds this to your user PATH)
|
||||||
|
- `explorer %HOMEPATH%\.ollama` contains models and configuration
|
||||||
|
- `explorer %TEMP%` contains temporary executable files in one or more `ollama*` directories
|
14
go.mod
|
@ -12,12 +12,26 @@ require (
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/containerd/console v1.0.3 // indirect
|
||||||
|
github.com/cratonica/2goarray v0.0.0-20190331194516-514510793eaa // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect
|
||||||
|
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect
|
||||||
|
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 // indirect
|
||||||
|
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect
|
||||||
|
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect
|
||||||
|
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect
|
||||||
|
github.com/getlantern/systray v1.2.2 // indirect
|
||||||
|
github.com/go-stack/stack v1.8.0 // indirect
|
||||||
|
github.com/google/uuid v1.0.0 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||||
|
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
|
||||||
|
github.com/pborman/uuid v1.2.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/rivo/uniseg v0.2.0 // indirect
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bytedance/sonic v1.9.1 // indirect
|
github.com/bytedance/sonic v1.9.1 // indirect
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||||
|
|
33
go.sum
|
@ -4,7 +4,11 @@ github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZX
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||||
|
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
|
||||||
|
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
github.com/cratonica/2goarray v0.0.0-20190331194516-514510793eaa h1:Wg+722vs7a2zQH5lR9QWYsVbplKeffaQFIs5FTdfNNo=
|
||||||
|
github.com/cratonica/2goarray v0.0.0-20190331194516-514510793eaa/go.mod h1:6Arca19mRx58CA7OWEd7Wu1NpC1rd3uDnNs6s1pj/DI=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
@ -13,6 +17,20 @@ github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc
|
||||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||||
|
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
|
||||||
|
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
|
||||||
|
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So=
|
||||||
|
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
|
||||||
|
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk=
|
||||||
|
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
|
||||||
|
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0=
|
||||||
|
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
|
||||||
|
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc=
|
||||||
|
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
|
||||||
|
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA=
|
||||||
|
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
|
||||||
|
github.com/getlantern/systray v1.2.2 h1:dCEHtfmvkJG7HZ8lS/sLklTH4RKUcIsKrAD9sThoEBE=
|
||||||
|
github.com/getlantern/systray v1.2.2/go.mod h1:pXFOI1wwqwYXEhLPm9ZGjS2u/vVELeIgNMY5HvhHhcE=
|
||||||
github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g=
|
github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g=
|
||||||
github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs=
|
github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs=
|
||||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
|
@ -31,6 +49,8 @@ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91
|
||||||
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
||||||
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
||||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||||
|
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
||||||
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
|
@ -39,6 +59,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA=
|
||||||
|
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
@ -57,6 +79,8 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||||
|
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
||||||
|
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
@ -70,8 +94,12 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||||
|
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
|
||||||
|
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
|
||||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
|
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
|
||||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
|
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
|
||||||
|
github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw=
|
||||||
|
github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||||
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
|
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
|
||||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||||
|
@ -84,6 +112,7 @@ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE
|
||||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||||
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
|
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
|
||||||
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
|
@ -120,11 +149,14 @@ golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
|
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
@ -141,6 +173,7 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
|
||||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
|
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
|
|
@ -4,7 +4,7 @@ $ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
function init_vars {
|
function init_vars {
|
||||||
$script:llamacppDir = "../llama.cpp"
|
$script:llamacppDir = "../llama.cpp"
|
||||||
$script:cmakeDefs = @("-DBUILD_SHARED_LIBS=on", "-DLLAMA_NATIVE=off", "-A","x64")
|
$script:cmakeDefs = @("-DBUILD_SHARED_LIBS=on", "-DLLAMA_NATIVE=off", "-A", "x64")
|
||||||
$script:cmakeTargets = @("ext_server")
|
$script:cmakeTargets = @("ext_server")
|
||||||
$script:ARCH = "amd64" # arm not yet supported.
|
$script:ARCH = "amd64" # arm not yet supported.
|
||||||
if ($env:CGO_CFLAGS -contains "-g") {
|
if ($env:CGO_CFLAGS -contains "-g") {
|
||||||
|
@ -19,6 +19,7 @@ function init_vars {
|
||||||
$d=(get-command -ea 'silentlycontinue' nvcc).path
|
$d=(get-command -ea 'silentlycontinue' nvcc).path
|
||||||
if ($d -ne $null) {
|
if ($d -ne $null) {
|
||||||
$script:CUDA_LIB_DIR=($d| split-path -parent)
|
$script:CUDA_LIB_DIR=($d| split-path -parent)
|
||||||
|
$script:CUDA_INCLUDE_DIR=($script:CUDA_LIB_DIR|split-path -parent)+"\include"
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$script:CUDA_LIB_DIR=$env:CUDA_LIB_DIR
|
$script:CUDA_LIB_DIR=$env:CUDA_LIB_DIR
|
||||||
|
@ -30,6 +31,8 @@ function init_vars {
|
||||||
} else {
|
} else {
|
||||||
$script:CMAKE_CUDA_ARCHITECTURES=$env:CMAKE_CUDA_ARCHITECTURES
|
$script:CMAKE_CUDA_ARCHITECTURES=$env:CMAKE_CUDA_ARCHITECTURES
|
||||||
}
|
}
|
||||||
|
# Note: 10 Windows Kit signtool crashes with GCP's plugin
|
||||||
|
${script:SignTool}="C:\Program Files (x86)\Windows Kits\8.1\bin\x64\signtool.exe"
|
||||||
}
|
}
|
||||||
|
|
||||||
function git_module_setup {
|
function git_module_setup {
|
||||||
|
@ -56,8 +59,8 @@ function apply_patches {
|
||||||
}
|
}
|
||||||
|
|
||||||
# Checkout each file
|
# Checkout each file
|
||||||
foreach ($file in $filePaths) {
|
|
||||||
Set-Location -Path ${script:llamacppDir}
|
Set-Location -Path ${script:llamacppDir}
|
||||||
|
foreach ($file in $filePaths) {
|
||||||
git checkout $file
|
git checkout $file
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -89,13 +92,23 @@ function install {
|
||||||
md "${script:buildDir}/lib" -ea 0 > $null
|
md "${script:buildDir}/lib" -ea 0 > $null
|
||||||
cp "${script:buildDir}/bin/${script:config}/ext_server.dll" "${script:buildDir}/lib"
|
cp "${script:buildDir}/bin/${script:config}/ext_server.dll" "${script:buildDir}/lib"
|
||||||
cp "${script:buildDir}/bin/${script:config}/llama.dll" "${script:buildDir}/lib"
|
cp "${script:buildDir}/bin/${script:config}/llama.dll" "${script:buildDir}/lib"
|
||||||
|
|
||||||
# Display the dll dependencies in the build log
|
# Display the dll dependencies in the build log
|
||||||
if ($script:DUMPBIN -ne $null) {
|
if ($script:DUMPBIN -ne $null) {
|
||||||
& "$script:DUMPBIN" /dependents "${script:buildDir}/bin/${script:config}/ext_server.dll" | select-string ".dll"
|
& "$script:DUMPBIN" /dependents "${script:buildDir}/bin/${script:config}/ext_server.dll" | select-string ".dll"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sign {
|
||||||
|
if ("${env:KEY_CONTAINER}") {
|
||||||
|
write-host "Signing ${script:buildDir}/lib/*.dll"
|
||||||
|
foreach ($file in (get-childitem "${script:buildDir}/lib/*.dll")){
|
||||||
|
& "${script:SignTool}" sign /v /fd sha256 /t http://timestamp.digicert.com /f "${env:OLLAMA_CERT}" `
|
||||||
|
/csp "Google Cloud KMS Provider" /kc "${env:KEY_CONTAINER}" $file
|
||||||
|
if ($LASTEXITCODE -ne 0) { exit($LASTEXITCODE)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function compress_libs {
|
function compress_libs {
|
||||||
if ($script:GZIP -eq $null) {
|
if ($script:GZIP -eq $null) {
|
||||||
write-host "gzip not installed, not compressing files"
|
write-host "gzip not installed, not compressing files"
|
||||||
|
@ -109,8 +122,23 @@ function compress_libs {
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanup {
|
function cleanup {
|
||||||
|
$patches = Get-ChildItem "../patches/*.diff"
|
||||||
|
foreach ($patch in $patches) {
|
||||||
|
# Extract file paths from the patch file
|
||||||
|
$filePaths = Get-Content $patch.FullName | Where-Object { $_ -match '^\+\+\+ ' } | ForEach-Object {
|
||||||
|
$parts = $_ -split ' '
|
||||||
|
($parts[1] -split '/', 2)[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Checkout each file
|
||||||
|
Set-Location -Path ${script:llamacppDir}
|
||||||
|
foreach ($file in $filePaths) {
|
||||||
|
git checkout $file
|
||||||
|
}
|
||||||
|
}
|
||||||
Set-Location "${script:llamacppDir}/examples/server"
|
Set-Location "${script:llamacppDir}/examples/server"
|
||||||
git checkout CMakeLists.txt server.cpp
|
git checkout CMakeLists.txt server.cpp
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init_vars
|
init_vars
|
||||||
|
@ -129,6 +157,7 @@ $script:buildDir="${script:llamacppDir}/build/windows/${script:ARCH}/cpu"
|
||||||
write-host "Building LCD CPU"
|
write-host "Building LCD CPU"
|
||||||
build
|
build
|
||||||
install
|
install
|
||||||
|
sign
|
||||||
compress_libs
|
compress_libs
|
||||||
|
|
||||||
$script:cmakeDefs = $script:commonCpuDefs + @("-DLLAMA_AVX=on", "-DLLAMA_AVX2=off", "-DLLAMA_AVX512=off", "-DLLAMA_FMA=off", "-DLLAMA_F16C=off") + $script:cmakeDefs
|
$script:cmakeDefs = $script:commonCpuDefs + @("-DLLAMA_AVX=on", "-DLLAMA_AVX2=off", "-DLLAMA_AVX512=off", "-DLLAMA_FMA=off", "-DLLAMA_F16C=off") + $script:cmakeDefs
|
||||||
|
@ -136,6 +165,7 @@ $script:buildDir="${script:llamacppDir}/build/windows/${script:ARCH}/cpu_avx"
|
||||||
write-host "Building AVX CPU"
|
write-host "Building AVX CPU"
|
||||||
build
|
build
|
||||||
install
|
install
|
||||||
|
sign
|
||||||
compress_libs
|
compress_libs
|
||||||
|
|
||||||
$script:cmakeDefs = $script:commonCpuDefs + @("-DLLAMA_AVX=on", "-DLLAMA_AVX2=on", "-DLLAMA_AVX512=off", "-DLLAMA_FMA=on", "-DLLAMA_F16C=on") + $script:cmakeDefs
|
$script:cmakeDefs = $script:commonCpuDefs + @("-DLLAMA_AVX=on", "-DLLAMA_AVX2=on", "-DLLAMA_AVX512=off", "-DLLAMA_FMA=on", "-DLLAMA_F16C=on") + $script:cmakeDefs
|
||||||
|
@ -143,25 +173,22 @@ $script:buildDir="${script:llamacppDir}/build/windows/${script:ARCH}/cpu_avx2"
|
||||||
write-host "Building AVX2 CPU"
|
write-host "Building AVX2 CPU"
|
||||||
build
|
build
|
||||||
install
|
install
|
||||||
|
sign
|
||||||
compress_libs
|
compress_libs
|
||||||
|
|
||||||
if ($null -ne $script:CUDA_LIB_DIR) {
|
if ($null -ne $script:CUDA_LIB_DIR) {
|
||||||
# Then build cuda as a dynamically loaded library
|
# Then build cuda as a dynamically loaded library
|
||||||
$nvcc = (get-command -ea 'silentlycontinue' nvcc)
|
$nvcc = "$script:CUDA_LIB_DIR\nvcc.exe"
|
||||||
if ($null -ne $nvcc) {
|
|
||||||
$script:CUDA_VERSION=(get-item ($nvcc | split-path | split-path)).Basename
|
$script:CUDA_VERSION=(get-item ($nvcc | split-path | split-path)).Basename
|
||||||
}
|
|
||||||
if ($null -ne $script:CUDA_VERSION) {
|
if ($null -ne $script:CUDA_VERSION) {
|
||||||
$script:CUDA_VARIANT="_"+$script:CUDA_VERSION
|
$script:CUDA_VARIANT="_"+$script:CUDA_VERSION
|
||||||
}
|
}
|
||||||
init_vars
|
init_vars
|
||||||
$script:buildDir="${script:llamacppDir}/build/windows/${script:ARCH}/cuda$script:CUDA_VARIANT"
|
$script:buildDir="${script:llamacppDir}/build/windows/${script:ARCH}/cuda$script:CUDA_VARIANT"
|
||||||
$script:cmakeDefs += @("-DLLAMA_CUBLAS=ON", "-DLLAMA_AVX=on", "-DCMAKE_CUDA_ARCHITECTURES=${script:CMAKE_CUDA_ARCHITECTURES}")
|
$script:cmakeDefs += @("-DLLAMA_CUBLAS=ON", "-DLLAMA_AVX=on", "-DCUDAToolkit_INCLUDE_DIR=$script:CUDA_INCLUDE_DIR", "-DCMAKE_CUDA_ARCHITECTURES=${script:CMAKE_CUDA_ARCHITECTURES}")
|
||||||
build
|
build
|
||||||
install
|
install
|
||||||
cp "${script:CUDA_LIB_DIR}/cudart64_*.dll" "${script:buildDir}/lib"
|
sign
|
||||||
cp "${script:CUDA_LIB_DIR}/cublas64_*.dll" "${script:buildDir}/lib"
|
|
||||||
cp "${script:CUDA_LIB_DIR}/cublasLt64_*.dll" "${script:buildDir}/lib"
|
|
||||||
compress_libs
|
compress_libs
|
||||||
}
|
}
|
||||||
# TODO - actually implement ROCm support on windows
|
# TODO - actually implement ROCm support on windows
|
||||||
|
|
92
macapp/.gitignore
vendored
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# TypeScript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
.env.test
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# next.js build output
|
||||||
|
.next
|
||||||
|
|
||||||
|
# nuxt.js build output
|
||||||
|
.nuxt
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# Webpack
|
||||||
|
.webpack/
|
||||||
|
|
||||||
|
# Vite
|
||||||
|
.vite/
|
||||||
|
|
||||||
|
# Electron-Forge
|
||||||
|
out/
|
21
macapp/README.md
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# Desktop
|
||||||
|
|
||||||
|
This app builds upon Ollama to provide a desktop experience for running models.
|
||||||
|
|
||||||
|
## Developing
|
||||||
|
|
||||||
|
First, build the `ollama` binary:
|
||||||
|
|
||||||
|
```
|
||||||
|
cd ..
|
||||||
|
go build .
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run the desktop app with `npm start`:
|
||||||
|
|
||||||
|
```
|
||||||
|
cd app
|
||||||
|
npm install
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
Before Width: | Height: | Size: 402 B After Width: | Height: | Size: 402 B |
Before Width: | Height: | Size: 741 B After Width: | Height: | Size: 741 B |
Before Width: | Height: | Size: 440 B After Width: | Height: | Size: 440 B |
Before Width: | Height: | Size: 763 B After Width: | Height: | Size: 763 B |
Before Width: | Height: | Size: 447 B After Width: | Height: | Size: 447 B |
Before Width: | Height: | Size: 891 B After Width: | Height: | Size: 891 B |
Before Width: | Height: | Size: 443 B After Width: | Height: | Size: 443 B |
Before Width: | Height: | Size: 844 B After Width: | Height: | Size: 844 B |
0
app/package-lock.json → macapp/package-lock.json
generated
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
@ -24,13 +24,13 @@ fi
|
||||||
chmod +x dist/ollama
|
chmod +x dist/ollama
|
||||||
|
|
||||||
# build and optionally sign the mac app
|
# build and optionally sign the mac app
|
||||||
npm install --prefix app
|
npm install --prefix macapp
|
||||||
if [ -n "$APPLE_IDENTITY" ]; then
|
if [ -n "$APPLE_IDENTITY" ]; then
|
||||||
npm run --prefix app make:sign
|
npm run --prefix macapp make:sign
|
||||||
else
|
else
|
||||||
npm run --prefix app make
|
npm run --prefix macapp make
|
||||||
fi
|
fi
|
||||||
cp app/out/make/zip/darwin/universal/Ollama-darwin-universal-$VERSION.zip dist/Ollama-darwin.zip
|
cp macapp/out/make/zip/darwin/universal/Ollama-darwin-universal-$VERSION.zip dist/Ollama-darwin.zip
|
||||||
|
|
||||||
# sign the binary and rename it
|
# sign the binary and rename it
|
||||||
if [ -n "$APPLE_IDENTITY" ]; then
|
if [ -n "$APPLE_IDENTITY" ]; then
|
||||||
|
|
|
@ -60,13 +60,17 @@ subprocess.check_call(['ssh', netloc, 'cd', path, ';', 'git', 'checkout', branch
|
||||||
# subprocess.check_call(['ssh', netloc, 'cd', path, ';', 'env'])
|
# subprocess.check_call(['ssh', netloc, 'cd', path, ';', 'env'])
|
||||||
# TODO - or consider paramiko maybe
|
# TODO - or consider paramiko maybe
|
||||||
|
|
||||||
print("Performing generate")
|
print("Running Windows Build Script")
|
||||||
subprocess.check_call(['ssh', netloc, 'cd', path, ';', GoCmd, 'generate', './...'])
|
subprocess.check_call(['ssh', netloc, 'cd', path, ';', "powershell", "-ExecutionPolicy", "Bypass", "-File", "./scripts/build_windows.ps1"])
|
||||||
|
|
||||||
print("Building")
|
# print("Building")
|
||||||
subprocess.check_call(['ssh', netloc, 'cd', path, ';', GoCmd, 'build', '.'])
|
# subprocess.check_call(['ssh', netloc, 'cd', path, ';', GoCmd, 'build', '.'])
|
||||||
|
|
||||||
print("Copying built result")
|
print("Copying built result")
|
||||||
subprocess.check_call(['scp', netloc +":"+ path + "/ollama.exe", './dist/'])
|
subprocess.check_call(['scp', netloc +":"+ path + "/ollama.exe", './dist/'])
|
||||||
|
|
||||||
|
print("Copying installer")
|
||||||
|
subprocess.check_call(['scp', netloc +":"+ path + "/dist/Ollama Setup.exe", './dist/'])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
132
scripts/build_windows.ps1
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
#!powershell
|
||||||
|
#
|
||||||
|
# powershell -ExecutionPolicy Bypass -File .\scripts\build_windows.ps1
|
||||||
|
#
|
||||||
|
# gcloud auth application-default login
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
function checkEnv() {
|
||||||
|
write-host "Locating required tools and paths"
|
||||||
|
$script:SRC_DIR=$PWD
|
||||||
|
if (!$env:VCToolsRedistDir) {
|
||||||
|
$MSVC_INSTALL=(Get-CimInstance MSFT_VSInstance -Namespace root/cimv2/vs)[0].InstallLocation
|
||||||
|
$env:VCToolsRedistDir=(get-item "${MSVC_INSTALL}\VC\Redist\MSVC\*")[0]
|
||||||
|
}
|
||||||
|
$script:NVIDIA_DIR=(get-item "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v*\bin\")[0]
|
||||||
|
$script:INNO_SETUP_DIR=(get-item "C:\Program Files*\Inno Setup*\")[0]
|
||||||
|
|
||||||
|
$script:DEPS_DIR="${script:SRC_DIR}\dist\windeps"
|
||||||
|
$env:CGO_ENABLED="1"
|
||||||
|
echo "Checking version"
|
||||||
|
if (!$env:VERSION) {
|
||||||
|
$data=(git describe --tags --first-parent --abbrev=7 --long --dirty --always)
|
||||||
|
$pattern="v(.+)"
|
||||||
|
if ($data -match $pattern) {
|
||||||
|
$script:VERSION=$matches[1]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$script:VERSION=$env:VERSION
|
||||||
|
}
|
||||||
|
$pattern = "(\d+[.]\d+[.]\d+)-(\d+)-"
|
||||||
|
if ($script:VERSION -match $pattern) {
|
||||||
|
$script:PKG_VERSION=$matches[1] + "." + $matches[2]
|
||||||
|
} else {
|
||||||
|
$script:PKG_VERSION=$script:VERSION
|
||||||
|
}
|
||||||
|
write-host "Building Ollama $script:VERSION with package version $script:PKG_VERSION"
|
||||||
|
|
||||||
|
# Check for signing key
|
||||||
|
if ("${env:KEY_CONTAINER}") {
|
||||||
|
${env:OLLAMA_CERT}=$(resolve-path "${script:SRC_DIR}\ollama_inc.crt")
|
||||||
|
Write-host "Code signing enabled"
|
||||||
|
# Note: 10 Windows Kit signtool crashes with GCP's plugin
|
||||||
|
${script:SignTool}="C:\Program Files (x86)\Windows Kits\8.1\bin\x64\signtool.exe"
|
||||||
|
} else {
|
||||||
|
write-host "Code signing disabled - please set KEY_CONTAINERS to sign and copy ollama_inc.crt to the top of the source tree"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function buildOllama() {
|
||||||
|
write-host "Building ollama CLI"
|
||||||
|
& go generate ./...
|
||||||
|
if ($LASTEXITCODE -ne 0) { exit($LASTEXITCODE)}
|
||||||
|
& go build "-ldflags=-w -s ""-X=github.com/jmorganca/ollama/version.Version=$script:VERSION"" ""-X=github.com/jmorganca/ollama/server.mode=release""" .
|
||||||
|
if ($LASTEXITCODE -ne 0) { exit($LASTEXITCODE)}
|
||||||
|
if ("${env:KEY_CONTAINER}") {
|
||||||
|
& "${script:SignTool}" sign /v /fd sha256 /t http://timestamp.digicert.com /f "${env:OLLAMA_CERT}" `
|
||||||
|
/csp "Google Cloud KMS Provider" /kc ${env:KEY_CONTAINER} ollama.exe
|
||||||
|
if ($LASTEXITCODE -ne 0) { exit($LASTEXITCODE)}
|
||||||
|
}
|
||||||
|
cp .\ollama.exe .\dist\ollama-windows-amd64.exe
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildApp() {
|
||||||
|
write-host "Building Ollama App"
|
||||||
|
cd "${script:SRC_DIR}\app"
|
||||||
|
& windres -l 0 -o ollama.syso ollama.rc
|
||||||
|
& go build "-ldflags=-H windowsgui -w -s ""-X=github.com/jmorganca/ollama/version.Version=$script:VERSION"" ""-X=github.com/jmorganca/ollama/server.mode=release""" .
|
||||||
|
if ($LASTEXITCODE -ne 0) { exit($LASTEXITCODE)}
|
||||||
|
if ("${env:KEY_CONTAINER}") {
|
||||||
|
& "${script:SignTool}" sign /v /fd sha256 /t http://timestamp.digicert.com /f "${env:OLLAMA_CERT}" `
|
||||||
|
/csp "Google Cloud KMS Provider" /kc ${env:KEY_CONTAINER} app.exe
|
||||||
|
if ($LASTEXITCODE -ne 0) { exit($LASTEXITCODE)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function gatherDependencies() {
|
||||||
|
write-host "Gathering runtime dependencies"
|
||||||
|
cd "${script:SRC_DIR}"
|
||||||
|
rm -ea 0 -recurse -force -path "${script:DEPS_DIR}"
|
||||||
|
md "${script:DEPS_DIR}" -ea 0 > $null
|
||||||
|
|
||||||
|
# TODO - this varies based on host build system and MSVC version - drive from dumpbin output
|
||||||
|
# currently works for Win11 + MSVC 2019 + Cuda V11
|
||||||
|
cp "${env:VCToolsRedistDir}\x64\Microsoft.VC*.CRT\msvcp140.dll" "${script:DEPS_DIR}\"
|
||||||
|
cp "${env:VCToolsRedistDir}\x64\Microsoft.VC*.CRT\vcruntime140.dll" "${script:DEPS_DIR}\"
|
||||||
|
cp "${env:VCToolsRedistDir}\x64\Microsoft.VC*.CRT\vcruntime140_1.dll" "${script:DEPS_DIR}\"
|
||||||
|
|
||||||
|
cp "${script:NVIDIA_DIR}\cudart64_*.dll" "${script:DEPS_DIR}\"
|
||||||
|
cp "${script:NVIDIA_DIR}\cublas64_*.dll" "${script:DEPS_DIR}\"
|
||||||
|
cp "${script:NVIDIA_DIR}\cublasLt64_*.dll" "${script:DEPS_DIR}\"
|
||||||
|
|
||||||
|
cp "${script:SRC_DIR}\app\ollama_welcome.ps1" "${script:SRC_DIR}\dist\"
|
||||||
|
if ("${env:KEY_CONTAINER}") {
|
||||||
|
write-host "about to sign"
|
||||||
|
foreach ($file in (get-childitem "${script:DEPS_DIR}/cu*.dll") + @("${script:SRC_DIR}\dist\ollama_welcome.ps1")){
|
||||||
|
write-host "signing $file"
|
||||||
|
& "${script:SignTool}" sign /v /fd sha256 /t http://timestamp.digicert.com /f "${env:OLLAMA_CERT}" `
|
||||||
|
/csp "Google Cloud KMS Provider" /kc ${env:KEY_CONTAINER} $file
|
||||||
|
if ($LASTEXITCODE -ne 0) { exit($LASTEXITCODE)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildInstaller() {
|
||||||
|
write-host "Building Ollama Installer"
|
||||||
|
cd "${script:SRC_DIR}\app"
|
||||||
|
$env:PKG_VERSION=$script:PKG_VERSION
|
||||||
|
if ("${env:KEY_CONTAINER}") {
|
||||||
|
& "${script:INNO_SETUP_DIR}\ISCC.exe" /SMySignTool="${script:SignTool} sign /fd sha256 /t http://timestamp.digicert.com /f ${env:OLLAMA_CERT} /csp `$qGoogle Cloud KMS Provider`$q /kc ${env:KEY_CONTAINER} `$f" .\ollama.iss
|
||||||
|
} else {
|
||||||
|
& "${script:INNO_SETUP_DIR}\ISCC.exe" .\ollama.iss
|
||||||
|
}
|
||||||
|
if ($LASTEXITCODE -ne 0) { exit($LASTEXITCODE)}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
checkEnv
|
||||||
|
buildOllama
|
||||||
|
buildApp
|
||||||
|
gatherDependencies
|
||||||
|
buildInstaller
|
||||||
|
} catch {
|
||||||
|
write-host "Build Failed"
|
||||||
|
write-host $_
|
||||||
|
} finally {
|
||||||
|
set-location $script:SRC_DIR
|
||||||
|
$env:PKG_VERSION=""
|
||||||
|
}
|
149
server/auth.go
|
@ -1,7 +1,6 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
@ -10,167 +9,87 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/crypto/ssh"
|
|
||||||
|
|
||||||
"github.com/jmorganca/ollama/api"
|
"github.com/jmorganca/ollama/api"
|
||||||
|
"github.com/jmorganca/ollama/auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AuthRedirect struct {
|
type registryChallenge struct {
|
||||||
Realm string
|
Realm string
|
||||||
Service string
|
Service string
|
||||||
Scope string
|
Scope string
|
||||||
}
|
}
|
||||||
|
|
||||||
type SignatureData struct {
|
func (r registryChallenge) URL() (*url.URL, error) {
|
||||||
Method string
|
|
||||||
Path string
|
|
||||||
Data []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateNonce(length int) (string, error) {
|
|
||||||
nonce := make([]byte, length)
|
|
||||||
_, err := rand.Read(nonce)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return base64.RawURLEncoding.EncodeToString(nonce), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r AuthRedirect) URL() (*url.URL, error) {
|
|
||||||
redirectURL, err := url.Parse(r.Realm)
|
redirectURL, err := url.Parse(r.Realm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
values := redirectURL.Query()
|
values := redirectURL.Query()
|
||||||
|
|
||||||
values.Add("service", r.Service)
|
values.Add("service", r.Service)
|
||||||
|
|
||||||
for _, s := range strings.Split(r.Scope, " ") {
|
for _, s := range strings.Split(r.Scope, " ") {
|
||||||
values.Add("scope", s)
|
values.Add("scope", s)
|
||||||
}
|
}
|
||||||
|
|
||||||
values.Add("ts", strconv.FormatInt(time.Now().Unix(), 10))
|
values.Add("ts", strconv.FormatInt(time.Now().Unix(), 10))
|
||||||
|
|
||||||
nonce, err := generateNonce(16)
|
nonce, err := auth.NewNonce(rand.Reader, 16)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
values.Add("nonce", nonce)
|
values.Add("nonce", nonce)
|
||||||
|
|
||||||
redirectURL.RawQuery = values.Encode()
|
redirectURL.RawQuery = values.Encode()
|
||||||
return redirectURL, nil
|
return redirectURL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAuthToken(ctx context.Context, redirData AuthRedirect) (string, error) {
|
func getAuthorizationToken(ctx context.Context, challenge registryChallenge) (string, error) {
|
||||||
redirectURL, err := redirData.URL()
|
redirectURL, err := challenge.URL()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
home, err := os.UserHomeDir()
|
sha256sum := sha256.Sum256(nil)
|
||||||
if err != nil {
|
data := []byte(fmt.Sprintf("%s,%s,%s", http.MethodGet, redirectURL.String(), base64.StdEncoding.EncodeToString([]byte(hex.EncodeToString(sha256sum[:])))))
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
keyPath := filepath.Join(home, ".ollama", "id_ed25519")
|
|
||||||
|
|
||||||
rawKey, err := os.ReadFile(keyPath)
|
|
||||||
if err != nil {
|
|
||||||
slog.Info(fmt.Sprintf("Failed to load private key: %v", err))
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
s := SignatureData{
|
|
||||||
Method: http.MethodGet,
|
|
||||||
Path: redirectURL.String(),
|
|
||||||
Data: nil,
|
|
||||||
}
|
|
||||||
|
|
||||||
sig, err := s.Sign(rawKey)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
headers := make(http.Header)
|
headers := make(http.Header)
|
||||||
headers.Set("Authorization", sig)
|
signature, err := auth.Sign(ctx, data)
|
||||||
resp, err := makeRequest(ctx, http.MethodGet, redirectURL, headers, nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
slog.Info(fmt.Sprintf("couldn't get token: %q", err))
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode >= http.StatusBadRequest {
|
|
||||||
responseBody, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("%d: %v", resp.StatusCode, err)
|
|
||||||
} else if len(responseBody) > 0 {
|
|
||||||
return "", fmt.Errorf("%d: %s", resp.StatusCode, responseBody)
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", fmt.Errorf("%s", resp.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
respBody, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
var tok api.TokenResponse
|
headers.Add("Authorization", signature)
|
||||||
if err := json.Unmarshal(respBody, &tok); err != nil {
|
|
||||||
|
response, err := makeRequest(ctx, http.MethodGet, redirectURL, headers, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("%d: %v", response.StatusCode, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.StatusCode >= http.StatusBadRequest {
|
||||||
|
if len(body) > 0 {
|
||||||
|
return "", fmt.Errorf("%d: %s", response.StatusCode, body)
|
||||||
|
} else {
|
||||||
|
return "", fmt.Errorf("%d", response.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var token api.TokenResponse
|
||||||
|
if err := json.Unmarshal(body, &token); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return tok.Token, nil
|
return token.Token, nil
|
||||||
}
|
|
||||||
|
|
||||||
// Bytes returns a byte slice of the data to sign for the request
|
|
||||||
func (s SignatureData) Bytes() []byte {
|
|
||||||
// We first derive the content hash of the request body using:
|
|
||||||
// base64(hex(sha256(request body)))
|
|
||||||
|
|
||||||
hash := sha256.Sum256(s.Data)
|
|
||||||
hashHex := make([]byte, hex.EncodedLen(len(hash)))
|
|
||||||
hex.Encode(hashHex, hash[:])
|
|
||||||
contentHash := base64.StdEncoding.EncodeToString(hashHex)
|
|
||||||
|
|
||||||
// We then put the entire request together in a serialize string using:
|
|
||||||
// "<method>,<uri>,<content hash>"
|
|
||||||
// e.g. "GET,http://localhost,OTdkZjM1O..."
|
|
||||||
|
|
||||||
return []byte(strings.Join([]string{s.Method, s.Path, contentHash}, ","))
|
|
||||||
}
|
|
||||||
|
|
||||||
// SignData takes a SignatureData object and signs it with a raw private key
|
|
||||||
func (s SignatureData) Sign(rawKey []byte) (string, error) {
|
|
||||||
signer, err := ssh.ParsePrivateKey(rawKey)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the pubkey, but remove the type
|
|
||||||
pubKey := ssh.MarshalAuthorizedKey(signer.PublicKey())
|
|
||||||
parts := bytes.Split(pubKey, []byte(" "))
|
|
||||||
if len(parts) < 2 {
|
|
||||||
return "", fmt.Errorf("malformed public key")
|
|
||||||
}
|
|
||||||
|
|
||||||
signedData, err := signer.Sign(nil, s.Bytes())
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// signature is <pubkey>:<signature>
|
|
||||||
sig := fmt.Sprintf("%s:%s", bytes.TrimSpace(parts[1]), base64.StdEncoding.EncodeToString(signedData.Blob))
|
|
||||||
return sig, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,7 +85,7 @@ func (p *blobDownloadPart) Write(b []byte) (n int, err error) {
|
||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *blobDownload) Prepare(ctx context.Context, requestURL *url.URL, opts *RegistryOptions) error {
|
func (b *blobDownload) Prepare(ctx context.Context, requestURL *url.URL, opts *registryOptions) error {
|
||||||
partFilePaths, err := filepath.Glob(b.Name + "-partial-*")
|
partFilePaths, err := filepath.Glob(b.Name + "-partial-*")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -137,11 +137,11 @@ func (b *blobDownload) Prepare(ctx context.Context, requestURL *url.URL, opts *R
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *blobDownload) Run(ctx context.Context, requestURL *url.URL, opts *RegistryOptions) {
|
func (b *blobDownload) Run(ctx context.Context, requestURL *url.URL, opts *registryOptions) {
|
||||||
b.err = b.run(ctx, requestURL, opts)
|
b.err = b.run(ctx, requestURL, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *blobDownload) run(ctx context.Context, requestURL *url.URL, opts *RegistryOptions) error {
|
func (b *blobDownload) run(ctx context.Context, requestURL *url.URL, opts *registryOptions) error {
|
||||||
defer blobDownloadManager.Delete(b.Digest)
|
defer blobDownloadManager.Delete(b.Digest)
|
||||||
ctx, b.CancelFunc = context.WithCancel(ctx)
|
ctx, b.CancelFunc = context.WithCancel(ctx)
|
||||||
|
|
||||||
|
@ -210,7 +210,7 @@ func (b *blobDownload) run(ctx context.Context, requestURL *url.URL, opts *Regis
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *blobDownload) downloadChunk(ctx context.Context, requestURL *url.URL, w io.Writer, part *blobDownloadPart, opts *RegistryOptions) error {
|
func (b *blobDownload) downloadChunk(ctx context.Context, requestURL *url.URL, w io.Writer, part *blobDownloadPart, opts *registryOptions) error {
|
||||||
g, ctx := errgroup.WithContext(ctx)
|
g, ctx := errgroup.WithContext(ctx)
|
||||||
g.Go(func() error {
|
g.Go(func() error {
|
||||||
headers := make(http.Header)
|
headers := make(http.Header)
|
||||||
|
@ -334,7 +334,7 @@ func (b *blobDownload) Wait(ctx context.Context, fn func(api.ProgressResponse))
|
||||||
type downloadOpts struct {
|
type downloadOpts struct {
|
||||||
mp ModelPath
|
mp ModelPath
|
||||||
digest string
|
digest string
|
||||||
regOpts *RegistryOptions
|
regOpts *registryOptions
|
||||||
fn func(api.ProgressResponse)
|
fn func(api.ProgressResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ import (
|
||||||
"github.com/jmorganca/ollama/version"
|
"github.com/jmorganca/ollama/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RegistryOptions struct {
|
type registryOptions struct {
|
||||||
Insecure bool
|
Insecure bool
|
||||||
Username string
|
Username string
|
||||||
Password string
|
Password string
|
||||||
|
@ -320,7 +320,7 @@ func CreateModel(ctx context.Context, name, modelFileDir string, commands []pars
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, os.ErrNotExist):
|
case errors.Is(err, os.ErrNotExist):
|
||||||
fn(api.ProgressResponse{Status: "pulling model"})
|
fn(api.ProgressResponse{Status: "pulling model"})
|
||||||
if err := PullModel(ctx, c.Args, &RegistryOptions{}, fn); err != nil {
|
if err := PullModel(ctx, c.Args, ®istryOptions{}, fn); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -840,7 +840,7 @@ PARAMETER {{ $k }} {{ printf "%#v" $parameter }}
|
||||||
return buf.String(), nil
|
return buf.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func PushModel(ctx context.Context, name string, regOpts *RegistryOptions, fn func(api.ProgressResponse)) error {
|
func PushModel(ctx context.Context, name string, regOpts *registryOptions, fn func(api.ProgressResponse)) error {
|
||||||
mp := ParseModelPath(name)
|
mp := ParseModelPath(name)
|
||||||
fn(api.ProgressResponse{Status: "retrieving manifest"})
|
fn(api.ProgressResponse{Status: "retrieving manifest"})
|
||||||
|
|
||||||
|
@ -890,7 +890,7 @@ func PushModel(ctx context.Context, name string, regOpts *RegistryOptions, fn fu
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func PullModel(ctx context.Context, name string, regOpts *RegistryOptions, fn func(api.ProgressResponse)) error {
|
func PullModel(ctx context.Context, name string, regOpts *registryOptions, fn func(api.ProgressResponse)) error {
|
||||||
mp := ParseModelPath(name)
|
mp := ParseModelPath(name)
|
||||||
|
|
||||||
var manifest *ManifestV2
|
var manifest *ManifestV2
|
||||||
|
@ -996,7 +996,7 @@ func PullModel(ctx context.Context, name string, regOpts *RegistryOptions, fn fu
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func pullModelManifest(ctx context.Context, mp ModelPath, regOpts *RegistryOptions) (*ManifestV2, error) {
|
func pullModelManifest(ctx context.Context, mp ModelPath, regOpts *registryOptions) (*ManifestV2, error) {
|
||||||
requestURL := mp.BaseURL().JoinPath("v2", mp.GetNamespaceRepository(), "manifests", mp.Tag)
|
requestURL := mp.BaseURL().JoinPath("v2", mp.GetNamespaceRepository(), "manifests", mp.Tag)
|
||||||
|
|
||||||
headers := make(http.Header)
|
headers := make(http.Header)
|
||||||
|
@ -1028,7 +1028,7 @@ func GetSHA256Digest(r io.Reader) (string, int64) {
|
||||||
|
|
||||||
var errUnauthorized = fmt.Errorf("unauthorized")
|
var errUnauthorized = fmt.Errorf("unauthorized")
|
||||||
|
|
||||||
func makeRequestWithRetry(ctx context.Context, method string, requestURL *url.URL, headers http.Header, body io.ReadSeeker, regOpts *RegistryOptions) (*http.Response, error) {
|
func makeRequestWithRetry(ctx context.Context, method string, requestURL *url.URL, headers http.Header, body io.ReadSeeker, regOpts *registryOptions) (*http.Response, error) {
|
||||||
for i := 0; i < 2; i++ {
|
for i := 0; i < 2; i++ {
|
||||||
resp, err := makeRequest(ctx, method, requestURL, headers, body, regOpts)
|
resp, err := makeRequest(ctx, method, requestURL, headers, body, regOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1042,9 +1042,8 @@ func makeRequestWithRetry(ctx context.Context, method string, requestURL *url.UR
|
||||||
switch {
|
switch {
|
||||||
case resp.StatusCode == http.StatusUnauthorized:
|
case resp.StatusCode == http.StatusUnauthorized:
|
||||||
// Handle authentication error with one retry
|
// Handle authentication error with one retry
|
||||||
auth := resp.Header.Get("www-authenticate")
|
challenge := parseRegistryChallenge(resp.Header.Get("www-authenticate"))
|
||||||
authRedir := ParseAuthRedirectString(auth)
|
token, err := getAuthorizationToken(ctx, challenge)
|
||||||
token, err := getAuthToken(ctx, authRedir)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -1071,7 +1070,7 @@ func makeRequestWithRetry(ctx context.Context, method string, requestURL *url.UR
|
||||||
return nil, errUnauthorized
|
return nil, errUnauthorized
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeRequest(ctx context.Context, method string, requestURL *url.URL, headers http.Header, body io.Reader, regOpts *RegistryOptions) (*http.Response, error) {
|
func makeRequest(ctx context.Context, method string, requestURL *url.URL, headers http.Header, body io.Reader, regOpts *registryOptions) (*http.Response, error) {
|
||||||
if requestURL.Scheme != "http" && regOpts != nil && regOpts.Insecure {
|
if requestURL.Scheme != "http" && regOpts != nil && regOpts.Insecure {
|
||||||
requestURL.Scheme = "http"
|
requestURL.Scheme = "http"
|
||||||
}
|
}
|
||||||
|
@ -1146,10 +1145,10 @@ func getValue(header, key string) string {
|
||||||
return header[startIdx:endIdx]
|
return header[startIdx:endIdx]
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseAuthRedirectString(authStr string) AuthRedirect {
|
func parseRegistryChallenge(authStr string) registryChallenge {
|
||||||
authStr = strings.TrimPrefix(authStr, "Bearer ")
|
authStr = strings.TrimPrefix(authStr, "Bearer ")
|
||||||
|
|
||||||
return AuthRedirect{
|
return registryChallenge{
|
||||||
Realm: getValue(authStr, "realm"),
|
Realm: getValue(authStr, "realm"),
|
||||||
Service: getValue(authStr, "service"),
|
Service: getValue(authStr, "service"),
|
||||||
Scope: getValue(authStr, "scope"),
|
Scope: getValue(authStr, "scope"),
|
||||||
|
|
|
@ -479,7 +479,7 @@ func PullModelHandler(c *gin.Context) {
|
||||||
ch <- r
|
ch <- r
|
||||||
}
|
}
|
||||||
|
|
||||||
regOpts := &RegistryOptions{
|
regOpts := ®istryOptions{
|
||||||
Insecure: req.Insecure,
|
Insecure: req.Insecure,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -528,7 +528,7 @@ func PushModelHandler(c *gin.Context) {
|
||||||
ch <- r
|
ch <- r
|
||||||
}
|
}
|
||||||
|
|
||||||
regOpts := &RegistryOptions{
|
regOpts := ®istryOptions{
|
||||||
Insecure: req.Insecure,
|
Insecure: req.Insecure,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,7 @@ const (
|
||||||
maxUploadPartSize int64 = 1000 * format.MegaByte
|
maxUploadPartSize int64 = 1000 * format.MegaByte
|
||||||
)
|
)
|
||||||
|
|
||||||
func (b *blobUpload) Prepare(ctx context.Context, requestURL *url.URL, opts *RegistryOptions) error {
|
func (b *blobUpload) Prepare(ctx context.Context, requestURL *url.URL, opts *registryOptions) error {
|
||||||
p, err := GetBlobsPath(b.Digest)
|
p, err := GetBlobsPath(b.Digest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -121,7 +121,7 @@ func (b *blobUpload) Prepare(ctx context.Context, requestURL *url.URL, opts *Reg
|
||||||
|
|
||||||
// Run uploads blob parts to the upstream. If the upstream supports redirection, parts will be uploaded
|
// Run uploads blob parts to the upstream. If the upstream supports redirection, parts will be uploaded
|
||||||
// in parallel as defined by Prepare. Otherwise, parts will be uploaded serially. Run sets b.err on error.
|
// in parallel as defined by Prepare. Otherwise, parts will be uploaded serially. Run sets b.err on error.
|
||||||
func (b *blobUpload) Run(ctx context.Context, opts *RegistryOptions) {
|
func (b *blobUpload) Run(ctx context.Context, opts *registryOptions) {
|
||||||
defer blobUploadManager.Delete(b.Digest)
|
defer blobUploadManager.Delete(b.Digest)
|
||||||
ctx, b.CancelFunc = context.WithCancel(ctx)
|
ctx, b.CancelFunc = context.WithCancel(ctx)
|
||||||
|
|
||||||
|
@ -212,7 +212,7 @@ func (b *blobUpload) Run(ctx context.Context, opts *RegistryOptions) {
|
||||||
b.done = true
|
b.done = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *blobUpload) uploadPart(ctx context.Context, method string, requestURL *url.URL, part *blobUploadPart, opts *RegistryOptions) error {
|
func (b *blobUpload) uploadPart(ctx context.Context, method string, requestURL *url.URL, part *blobUploadPart, opts *registryOptions) error {
|
||||||
headers := make(http.Header)
|
headers := make(http.Header)
|
||||||
headers.Set("Content-Type", "application/octet-stream")
|
headers.Set("Content-Type", "application/octet-stream")
|
||||||
headers.Set("Content-Length", fmt.Sprintf("%d", part.Size))
|
headers.Set("Content-Length", fmt.Sprintf("%d", part.Size))
|
||||||
|
@ -277,9 +277,8 @@ func (b *blobUpload) uploadPart(ctx context.Context, method string, requestURL *
|
||||||
|
|
||||||
case resp.StatusCode == http.StatusUnauthorized:
|
case resp.StatusCode == http.StatusUnauthorized:
|
||||||
w.Rollback()
|
w.Rollback()
|
||||||
auth := resp.Header.Get("www-authenticate")
|
challenge := parseRegistryChallenge(resp.Header.Get("www-authenticate"))
|
||||||
authRedir := ParseAuthRedirectString(auth)
|
token, err := getAuthorizationToken(ctx, challenge)
|
||||||
token, err := getAuthToken(ctx, authRedir)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -364,7 +363,7 @@ func (p *progressWriter) Rollback() {
|
||||||
p.written = 0
|
p.written = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func uploadBlob(ctx context.Context, mp ModelPath, layer *Layer, opts *RegistryOptions, fn func(api.ProgressResponse)) error {
|
func uploadBlob(ctx context.Context, mp ModelPath, layer *Layer, opts *registryOptions, fn func(api.ProgressResponse)) error {
|
||||||
requestURL := mp.BaseURL()
|
requestURL := mp.BaseURL()
|
||||||
requestURL = requestURL.JoinPath("v2", mp.GetNamespaceRepository(), "blobs", layer.Digest)
|
requestURL = requestURL.JoinPath("v2", mp.GetNamespaceRepository(), "blobs", layer.Digest)
|
||||||
|
|
||||||
|
|