From 9da9e8fb7254df1148f9619bec781e52dc954678 Mon Sep 17 00:00:00 2001 From: Daniel Hiltgen Date: Tue, 13 Feb 2024 14:15:51 -0800 Subject: [PATCH 01/20] Move Mac App to a new dir --- {app => macapp}/.eslintrc.json | 0 {app => macapp}/.gitignore | 0 {app => macapp}/README.md | 0 {app => macapp}/assets/icon.icns | Bin {app => macapp}/assets/iconDarkTemplate.png | Bin {app => macapp}/assets/iconDarkTemplate@2x.png | Bin {app => macapp}/assets/iconDarkUpdateTemplate.png | Bin .../assets/iconDarkUpdateTemplate@2x.png | Bin {app => macapp}/assets/iconTemplate.png | Bin {app => macapp}/assets/iconTemplate@2x.png | Bin {app => macapp}/assets/iconUpdateTemplate.png | Bin {app => macapp}/assets/iconUpdateTemplate@2x.png | Bin {app => macapp}/forge.config.ts | 0 {app => macapp}/package-lock.json | 0 {app => macapp}/package.json | 0 {app => macapp}/postcss.config.js | 0 {app => macapp}/src/app.css | 0 {app => macapp}/src/app.tsx | 0 {app => macapp}/src/declarations.d.ts | 0 {app => macapp}/src/index.html | 0 {app => macapp}/src/index.ts | 0 {app => macapp}/src/install.ts | 0 {app => macapp}/src/ollama.svg | 0 {app => macapp}/src/preload.ts | 0 {app => macapp}/src/renderer.tsx | 0 {app => macapp}/tailwind.config.js | 0 {app => macapp}/tsconfig.json | 0 {app => macapp}/webpack.main.config.ts | 0 {app => macapp}/webpack.plugins.ts | 0 {app => macapp}/webpack.renderer.config.ts | 0 {app => macapp}/webpack.rules.ts | 0 scripts/build_darwin.sh | 8 ++++---- 32 files changed, 4 insertions(+), 4 deletions(-) rename {app => macapp}/.eslintrc.json (100%) rename {app => macapp}/.gitignore (100%) rename {app => macapp}/README.md (100%) rename {app => macapp}/assets/icon.icns (100%) rename {app => macapp}/assets/iconDarkTemplate.png (100%) rename {app => macapp}/assets/iconDarkTemplate@2x.png (100%) rename {app => macapp}/assets/iconDarkUpdateTemplate.png (100%) rename {app => macapp}/assets/iconDarkUpdateTemplate@2x.png (100%) rename {app => macapp}/assets/iconTemplate.png (100%) rename {app => macapp}/assets/iconTemplate@2x.png (100%) rename {app => macapp}/assets/iconUpdateTemplate.png (100%) rename {app => macapp}/assets/iconUpdateTemplate@2x.png (100%) rename {app => macapp}/forge.config.ts (100%) rename {app => macapp}/package-lock.json (100%) rename {app => macapp}/package.json (100%) rename {app => macapp}/postcss.config.js (100%) rename {app => macapp}/src/app.css (100%) rename {app => macapp}/src/app.tsx (100%) rename {app => macapp}/src/declarations.d.ts (100%) rename {app => macapp}/src/index.html (100%) rename {app => macapp}/src/index.ts (100%) rename {app => macapp}/src/install.ts (100%) rename {app => macapp}/src/ollama.svg (100%) rename {app => macapp}/src/preload.ts (100%) rename {app => macapp}/src/renderer.tsx (100%) rename {app => macapp}/tailwind.config.js (100%) rename {app => macapp}/tsconfig.json (100%) rename {app => macapp}/webpack.main.config.ts (100%) rename {app => macapp}/webpack.plugins.ts (100%) rename {app => macapp}/webpack.renderer.config.ts (100%) rename {app => macapp}/webpack.rules.ts (100%) diff --git a/app/.eslintrc.json b/macapp/.eslintrc.json similarity index 100% rename from app/.eslintrc.json rename to macapp/.eslintrc.json diff --git a/app/.gitignore b/macapp/.gitignore similarity index 100% rename from app/.gitignore rename to macapp/.gitignore diff --git a/app/README.md b/macapp/README.md similarity index 100% rename from app/README.md rename to macapp/README.md diff --git a/app/assets/icon.icns b/macapp/assets/icon.icns similarity index 100% rename from app/assets/icon.icns rename to macapp/assets/icon.icns diff --git a/app/assets/iconDarkTemplate.png b/macapp/assets/iconDarkTemplate.png similarity index 100% rename from app/assets/iconDarkTemplate.png rename to macapp/assets/iconDarkTemplate.png diff --git a/app/assets/iconDarkTemplate@2x.png b/macapp/assets/iconDarkTemplate@2x.png similarity index 100% rename from app/assets/iconDarkTemplate@2x.png rename to macapp/assets/iconDarkTemplate@2x.png diff --git a/app/assets/iconDarkUpdateTemplate.png b/macapp/assets/iconDarkUpdateTemplate.png similarity index 100% rename from app/assets/iconDarkUpdateTemplate.png rename to macapp/assets/iconDarkUpdateTemplate.png diff --git a/app/assets/iconDarkUpdateTemplate@2x.png b/macapp/assets/iconDarkUpdateTemplate@2x.png similarity index 100% rename from app/assets/iconDarkUpdateTemplate@2x.png rename to macapp/assets/iconDarkUpdateTemplate@2x.png diff --git a/app/assets/iconTemplate.png b/macapp/assets/iconTemplate.png similarity index 100% rename from app/assets/iconTemplate.png rename to macapp/assets/iconTemplate.png diff --git a/app/assets/iconTemplate@2x.png b/macapp/assets/iconTemplate@2x.png similarity index 100% rename from app/assets/iconTemplate@2x.png rename to macapp/assets/iconTemplate@2x.png diff --git a/app/assets/iconUpdateTemplate.png b/macapp/assets/iconUpdateTemplate.png similarity index 100% rename from app/assets/iconUpdateTemplate.png rename to macapp/assets/iconUpdateTemplate.png diff --git a/app/assets/iconUpdateTemplate@2x.png b/macapp/assets/iconUpdateTemplate@2x.png similarity index 100% rename from app/assets/iconUpdateTemplate@2x.png rename to macapp/assets/iconUpdateTemplate@2x.png diff --git a/app/forge.config.ts b/macapp/forge.config.ts similarity index 100% rename from app/forge.config.ts rename to macapp/forge.config.ts diff --git a/app/package-lock.json b/macapp/package-lock.json similarity index 100% rename from app/package-lock.json rename to macapp/package-lock.json diff --git a/app/package.json b/macapp/package.json similarity index 100% rename from app/package.json rename to macapp/package.json diff --git a/app/postcss.config.js b/macapp/postcss.config.js similarity index 100% rename from app/postcss.config.js rename to macapp/postcss.config.js diff --git a/app/src/app.css b/macapp/src/app.css similarity index 100% rename from app/src/app.css rename to macapp/src/app.css diff --git a/app/src/app.tsx b/macapp/src/app.tsx similarity index 100% rename from app/src/app.tsx rename to macapp/src/app.tsx diff --git a/app/src/declarations.d.ts b/macapp/src/declarations.d.ts similarity index 100% rename from app/src/declarations.d.ts rename to macapp/src/declarations.d.ts diff --git a/app/src/index.html b/macapp/src/index.html similarity index 100% rename from app/src/index.html rename to macapp/src/index.html diff --git a/app/src/index.ts b/macapp/src/index.ts similarity index 100% rename from app/src/index.ts rename to macapp/src/index.ts diff --git a/app/src/install.ts b/macapp/src/install.ts similarity index 100% rename from app/src/install.ts rename to macapp/src/install.ts diff --git a/app/src/ollama.svg b/macapp/src/ollama.svg similarity index 100% rename from app/src/ollama.svg rename to macapp/src/ollama.svg diff --git a/app/src/preload.ts b/macapp/src/preload.ts similarity index 100% rename from app/src/preload.ts rename to macapp/src/preload.ts diff --git a/app/src/renderer.tsx b/macapp/src/renderer.tsx similarity index 100% rename from app/src/renderer.tsx rename to macapp/src/renderer.tsx diff --git a/app/tailwind.config.js b/macapp/tailwind.config.js similarity index 100% rename from app/tailwind.config.js rename to macapp/tailwind.config.js diff --git a/app/tsconfig.json b/macapp/tsconfig.json similarity index 100% rename from app/tsconfig.json rename to macapp/tsconfig.json diff --git a/app/webpack.main.config.ts b/macapp/webpack.main.config.ts similarity index 100% rename from app/webpack.main.config.ts rename to macapp/webpack.main.config.ts diff --git a/app/webpack.plugins.ts b/macapp/webpack.plugins.ts similarity index 100% rename from app/webpack.plugins.ts rename to macapp/webpack.plugins.ts diff --git a/app/webpack.renderer.config.ts b/macapp/webpack.renderer.config.ts similarity index 100% rename from app/webpack.renderer.config.ts rename to macapp/webpack.renderer.config.ts diff --git a/app/webpack.rules.ts b/macapp/webpack.rules.ts similarity index 100% rename from app/webpack.rules.ts rename to macapp/webpack.rules.ts diff --git a/scripts/build_darwin.sh b/scripts/build_darwin.sh index 381bcba5..2daf4c8d 100755 --- a/scripts/build_darwin.sh +++ b/scripts/build_darwin.sh @@ -24,13 +24,13 @@ fi chmod +x dist/ollama # build and optionally sign the mac app -npm install --prefix app +npm install --prefix macapp if [ -n "$APPLE_IDENTITY" ]; then - npm run --prefix app make:sign + npm run --prefix macapp make:sign else - npm run --prefix app make + npm run --prefix macapp make 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 if [ -n "$APPLE_IDENTITY" ]; then From f397e0e988272ffd14bdfb6c4070bb3ab5328df2 Mon Sep 17 00:00:00 2001 From: Daniel Hiltgen Date: Mon, 5 Feb 2024 12:59:52 -0800 Subject: [PATCH 02/20] Move hub auth out to new package --- {server => auth}/auth.go | 68 ++++++++++++++++++-------------- auth/request.go | 72 ++++++++++++++++++++++++++++++++++ server/download.go | 11 +++--- server/images.go | 84 ++++++---------------------------------- server/routes.go | 5 ++- server/upload.go | 17 ++++---- 6 files changed, 142 insertions(+), 115 deletions(-) rename {server => auth}/auth.go (87%) create mode 100644 auth/request.go diff --git a/server/auth.go b/auth/auth.go similarity index 87% rename from server/auth.go rename to auth/auth.go index 0d09668d..c0ce0a52 100644 --- a/server/auth.go +++ b/auth/auth.go @@ -1,4 +1,4 @@ -package server +package auth import ( "bytes" @@ -24,6 +24,10 @@ import ( "github.com/jmorganca/ollama/api" ) +const ( + KeyType = "id_ed25519" +) + type AuthRedirect struct { Realm string Service string @@ -71,39 +75,47 @@ func (r AuthRedirect) URL() (*url.URL, error) { return redirectURL, nil } -func getAuthToken(ctx context.Context, redirData AuthRedirect) (string, error) { +func SignRequest(method, url string, data []byte, headers http.Header) error { + home, err := os.UserHomeDir() + if err != nil { + return err + } + + keyPath := filepath.Join(home, ".ollama", KeyType) + + rawKey, err := os.ReadFile(keyPath) + if err != nil { + slog.Info(fmt.Sprintf("Failed to load private key: %v", err)) + return err + } + + s := SignatureData{ + Method: method, + Path: url, + Data: data, + } + + sig, err := s.Sign(rawKey) + if err != nil { + return err + } + + headers.Set("Authorization", sig) + return nil +} + +func GetAuthToken(ctx context.Context, redirData AuthRedirect) (string, error) { redirectURL, err := redirData.URL() if err != nil { return "", err } - home, err := os.UserHomeDir() - if err != nil { - 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.Set("Authorization", sig) - resp, err := makeRequest(ctx, http.MethodGet, redirectURL, headers, nil, nil) + err = SignRequest(http.MethodGet, redirectURL.String(), nil, headers) + if err != nil { + return "", err + } + 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 diff --git a/auth/request.go b/auth/request.go new file mode 100644 index 00000000..ab863fe3 --- /dev/null +++ b/auth/request.go @@ -0,0 +1,72 @@ +package auth + +import ( + "context" + "fmt" + "io" + "net/http" + "net/url" + "runtime" + "strconv" + + "github.com/jmorganca/ollama/version" +) + +type RegistryOptions struct { + Insecure bool + Username string + Password string + Token string +} + +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 { + requestURL.Scheme = "http" + } + + req, err := http.NewRequestWithContext(ctx, method, requestURL.String(), body) + if err != nil { + return nil, err + } + + if headers != nil { + req.Header = headers + } + + if regOpts != nil { + if regOpts.Token != "" { + req.Header.Set("Authorization", "Bearer "+regOpts.Token) + } else if regOpts.Username != "" && regOpts.Password != "" { + req.SetBasicAuth(regOpts.Username, regOpts.Password) + } + } + + req.Header.Set("User-Agent", fmt.Sprintf("ollama/%s (%s %s) Go/%s", version.Version, runtime.GOARCH, runtime.GOOS, runtime.Version())) + + if s := req.Header.Get("Content-Length"); s != "" { + contentLength, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return nil, err + } + + req.ContentLength = contentLength + } + + proxyURL, err := http.ProxyFromEnvironment(req) + if err != nil { + return nil, err + } + + client := http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyURL(proxyURL), + }, + } + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + + return resp, nil +} diff --git a/server/download.go b/server/download.go index f089bd41..dbfba2dd 100644 --- a/server/download.go +++ b/server/download.go @@ -22,6 +22,7 @@ import ( "golang.org/x/sync/errgroup" "github.com/jmorganca/ollama/api" + "github.com/jmorganca/ollama/auth" "github.com/jmorganca/ollama/format" ) @@ -85,7 +86,7 @@ func (p *blobDownloadPart) Write(b []byte) (n int, err error) { 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 *auth.RegistryOptions) error { partFilePaths, err := filepath.Glob(b.Name + "-partial-*") if err != nil { return err @@ -137,11 +138,11 @@ func (b *blobDownload) Prepare(ctx context.Context, requestURL *url.URL, opts *R 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 *auth.RegistryOptions) { 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 *auth.RegistryOptions) error { defer blobDownloadManager.Delete(b.Digest) ctx, b.CancelFunc = context.WithCancel(ctx) @@ -210,7 +211,7 @@ func (b *blobDownload) run(ctx context.Context, requestURL *url.URL, opts *Regis 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 *auth.RegistryOptions) error { g, ctx := errgroup.WithContext(ctx) g.Go(func() error { headers := make(http.Header) @@ -334,7 +335,7 @@ func (b *blobDownload) Wait(ctx context.Context, fn func(api.ProgressResponse)) type downloadOpts struct { mp ModelPath digest string - regOpts *RegistryOptions + regOpts *auth.RegistryOptions fn func(api.ProgressResponse) } diff --git a/server/images.go b/server/images.go index 55b68456..8a70cdd5 100644 --- a/server/images.go +++ b/server/images.go @@ -16,25 +16,17 @@ import ( "os" "path/filepath" "runtime" - "strconv" "strings" "text/template" "golang.org/x/exp/slices" "github.com/jmorganca/ollama/api" + "github.com/jmorganca/ollama/auth" "github.com/jmorganca/ollama/llm" "github.com/jmorganca/ollama/parser" - "github.com/jmorganca/ollama/version" ) -type RegistryOptions struct { - Insecure bool - Username string - Password string - Token string -} - type Model struct { Name string `json:"name"` Config ConfigV2 @@ -320,7 +312,7 @@ func CreateModel(ctx context.Context, name, modelFileDir string, commands []pars switch { case errors.Is(err, os.ErrNotExist): fn(api.ProgressResponse{Status: "pulling model"}) - if err := PullModel(ctx, c.Args, &RegistryOptions{}, fn); err != nil { + if err := PullModel(ctx, c.Args, &auth.RegistryOptions{}, fn); err != nil { return err } @@ -840,7 +832,7 @@ PARAMETER {{ $k }} {{ printf "%#v" $parameter }} 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 *auth.RegistryOptions, fn func(api.ProgressResponse)) error { mp := ParseModelPath(name) fn(api.ProgressResponse{Status: "retrieving manifest"}) @@ -890,7 +882,7 @@ func PushModel(ctx context.Context, name string, regOpts *RegistryOptions, fn fu return nil } -func PullModel(ctx context.Context, name string, regOpts *RegistryOptions, fn func(api.ProgressResponse)) error { +func PullModel(ctx context.Context, name string, regOpts *auth.RegistryOptions, fn func(api.ProgressResponse)) error { mp := ParseModelPath(name) var manifest *ManifestV2 @@ -996,7 +988,7 @@ func PullModel(ctx context.Context, name string, regOpts *RegistryOptions, fn fu return nil } -func pullModelManifest(ctx context.Context, mp ModelPath, regOpts *RegistryOptions) (*ManifestV2, error) { +func pullModelManifest(ctx context.Context, mp ModelPath, regOpts *auth.RegistryOptions) (*ManifestV2, error) { requestURL := mp.BaseURL().JoinPath("v2", mp.GetNamespaceRepository(), "manifests", mp.Tag) headers := make(http.Header) @@ -1028,9 +1020,9 @@ func GetSHA256Digest(r io.Reader) (string, int64) { 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 *auth.RegistryOptions) (*http.Response, error) { for i := 0; i < 2; i++ { - resp, err := makeRequest(ctx, method, requestURL, headers, body, regOpts) + resp, err := auth.MakeRequest(ctx, method, requestURL, headers, body, regOpts) if err != nil { if !errors.Is(err, context.Canceled) { slog.Info(fmt.Sprintf("request failed: %v", err)) @@ -1042,9 +1034,9 @@ func makeRequestWithRetry(ctx context.Context, method string, requestURL *url.UR switch { case resp.StatusCode == http.StatusUnauthorized: // Handle authentication error with one retry - auth := resp.Header.Get("www-authenticate") - authRedir := ParseAuthRedirectString(auth) - token, err := getAuthToken(ctx, authRedir) + authenticate := resp.Header.Get("www-authenticate") + authRedir := ParseAuthRedirectString(authenticate) + token, err := auth.GetAuthToken(ctx, authRedir) if err != nil { return nil, err } @@ -1071,58 +1063,6 @@ func makeRequestWithRetry(ctx context.Context, method string, requestURL *url.UR return nil, errUnauthorized } -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 { - requestURL.Scheme = "http" - } - - req, err := http.NewRequestWithContext(ctx, method, requestURL.String(), body) - if err != nil { - return nil, err - } - - if headers != nil { - req.Header = headers - } - - if regOpts != nil { - if regOpts.Token != "" { - req.Header.Set("Authorization", "Bearer "+regOpts.Token) - } else if regOpts.Username != "" && regOpts.Password != "" { - req.SetBasicAuth(regOpts.Username, regOpts.Password) - } - } - - req.Header.Set("User-Agent", fmt.Sprintf("ollama/%s (%s %s) Go/%s", version.Version, runtime.GOARCH, runtime.GOOS, runtime.Version())) - - if s := req.Header.Get("Content-Length"); s != "" { - contentLength, err := strconv.ParseInt(s, 10, 64) - if err != nil { - return nil, err - } - - req.ContentLength = contentLength - } - - proxyURL, err := http.ProxyFromEnvironment(req) - if err != nil { - return nil, err - } - - client := http.Client{ - Transport: &http.Transport{ - Proxy: http.ProxyURL(proxyURL), - }, - } - - resp, err := client.Do(req) - if err != nil { - return nil, err - } - - return resp, nil -} - func getValue(header, key string) string { startIdx := strings.Index(header, key+"=") if startIdx == -1 { @@ -1146,10 +1086,10 @@ func getValue(header, key string) string { return header[startIdx:endIdx] } -func ParseAuthRedirectString(authStr string) AuthRedirect { +func ParseAuthRedirectString(authStr string) auth.AuthRedirect { authStr = strings.TrimPrefix(authStr, "Bearer ") - return AuthRedirect{ + return auth.AuthRedirect{ Realm: getValue(authStr, "realm"), Service: getValue(authStr, "service"), Scope: getValue(authStr, "scope"), diff --git a/server/routes.go b/server/routes.go index bd943ee1..ddf22e78 100644 --- a/server/routes.go +++ b/server/routes.go @@ -25,6 +25,7 @@ import ( "golang.org/x/exp/slices" "github.com/jmorganca/ollama/api" + "github.com/jmorganca/ollama/auth" "github.com/jmorganca/ollama/gpu" "github.com/jmorganca/ollama/llm" "github.com/jmorganca/ollama/openai" @@ -479,7 +480,7 @@ func PullModelHandler(c *gin.Context) { ch <- r } - regOpts := &RegistryOptions{ + regOpts := &auth.RegistryOptions{ Insecure: req.Insecure, } @@ -528,7 +529,7 @@ func PushModelHandler(c *gin.Context) { ch <- r } - regOpts := &RegistryOptions{ + regOpts := &auth.RegistryOptions{ Insecure: req.Insecure, } diff --git a/server/upload.go b/server/upload.go index 3609b308..525b27b8 100644 --- a/server/upload.go +++ b/server/upload.go @@ -18,6 +18,7 @@ import ( "time" "github.com/jmorganca/ollama/api" + "github.com/jmorganca/ollama/auth" "github.com/jmorganca/ollama/format" "golang.org/x/sync/errgroup" ) @@ -49,7 +50,7 @@ const ( 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 *auth.RegistryOptions) error { p, err := GetBlobsPath(b.Digest) if err != nil { return err @@ -121,7 +122,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 // 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 *auth.RegistryOptions) { defer blobUploadManager.Delete(b.Digest) ctx, b.CancelFunc = context.WithCancel(ctx) @@ -212,7 +213,7 @@ func (b *blobUpload) Run(ctx context.Context, opts *RegistryOptions) { 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 *auth.RegistryOptions) error { headers := make(http.Header) headers.Set("Content-Type", "application/octet-stream") headers.Set("Content-Length", fmt.Sprintf("%d", part.Size)) @@ -227,7 +228,7 @@ func (b *blobUpload) uploadPart(ctx context.Context, method string, requestURL * md5sum := md5.New() w := &progressWriter{blobUpload: b} - resp, err := makeRequest(ctx, method, requestURL, headers, io.TeeReader(sr, io.MultiWriter(w, md5sum)), opts) + resp, err := auth.MakeRequest(ctx, method, requestURL, headers, io.TeeReader(sr, io.MultiWriter(w, md5sum)), opts) if err != nil { w.Rollback() return err @@ -277,9 +278,9 @@ func (b *blobUpload) uploadPart(ctx context.Context, method string, requestURL * case resp.StatusCode == http.StatusUnauthorized: w.Rollback() - auth := resp.Header.Get("www-authenticate") - authRedir := ParseAuthRedirectString(auth) - token, err := getAuthToken(ctx, authRedir) + authenticate := resp.Header.Get("www-authenticate") + authRedir := ParseAuthRedirectString(authenticate) + token, err := auth.GetAuthToken(ctx, authRedir) if err != nil { return err } @@ -364,7 +365,7 @@ func (p *progressWriter) Rollback() { 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 *auth.RegistryOptions, fn func(api.ProgressResponse)) error { requestURL := mp.BaseURL() requestURL = requestURL.JoinPath("v2", mp.GetNamespaceRepository(), "blobs", layer.Digest) From 29e90cc13b94d5306db42240042ad6f2bb8add3a Mon Sep 17 00:00:00 2001 From: Daniel Hiltgen Date: Tue, 26 Dec 2023 16:03:45 -0800 Subject: [PATCH 03/20] Implement new Go based Desktop app This focuses on Windows first, but coudl be used for Mac and possibly linux in the future. --- .gitignore | 3 +- app/README.md | 22 ++ app/assets/app.ico | Bin 0 -> 6227 bytes app/assets/assets.go | 17 + app/assets/setup.bmp | Bin 0 -> 77418 bytes app/assets/tray.ico | Bin 0 -> 22846 bytes app/assets/tray_upgrade.ico | Bin 0 -> 23698 bytes app/lifecycle/getstarted_nonwindows.go | 9 + app/lifecycle/getstarted_windows.go | 44 +++ app/lifecycle/lifecycle.go | 83 +++++ app/lifecycle/logging.go | 46 +++ app/lifecycle/logging_nonwindows.go | 9 + app/lifecycle/logging_windows.go | 19 + app/lifecycle/paths.go | 79 ++++ app/lifecycle/server.go | 135 +++++++ app/lifecycle/server_unix.go | 12 + app/lifecycle/server_windows.go | 13 + app/lifecycle/updater.go | 216 +++++++++++ app/lifecycle/updater_nonwindows.go | 12 + app/lifecycle/updater_windows.go | 79 ++++ app/main.go | 17 + app/ollama.iss | 150 ++++++++ app/ollama_welcome.ps1 | 8 + app/store/store.go | 98 +++++ app/store/store_darwin.go | 13 + app/store/store_linux.go | 16 + app/store/store_windows.go | 11 + app/tray/commontray/types.go | 24 ++ app/tray/tray.go | 33 ++ app/tray/tray_nonwindows.go | 13 + app/tray/tray_windows.go | 10 + app/tray/wintray/eventloop.go | 189 ++++++++++ app/tray/wintray/menus.go | 75 ++++ app/tray/wintray/messages.go | 15 + app/tray/wintray/notifyicon.go | 66 ++++ app/tray/wintray/tray.go | 485 +++++++++++++++++++++++++ app/tray/wintray/w32api.go | 89 +++++ app/tray/wintray/winclass.go | 45 +++ cmd/cmd.go | 31 +- cmd/start_darwin.go | 30 ++ cmd/start_default.go | 14 + cmd/start_windows.go | 81 +++++ docs/troubleshooting.md | 130 ++++--- docs/windows.md | 46 +++ go.mod | 12 + go.sum | 30 ++ llm/generate/gen_windows.ps1 | 51 ++- scripts/build_remote.py | 12 +- scripts/build_windows.ps1 | 130 +++++++ 49 files changed, 2621 insertions(+), 101 deletions(-) create mode 100644 app/README.md create mode 100644 app/assets/app.ico create mode 100644 app/assets/assets.go create mode 100644 app/assets/setup.bmp create mode 100644 app/assets/tray.ico create mode 100644 app/assets/tray_upgrade.ico create mode 100644 app/lifecycle/getstarted_nonwindows.go create mode 100644 app/lifecycle/getstarted_windows.go create mode 100644 app/lifecycle/lifecycle.go create mode 100644 app/lifecycle/logging.go create mode 100644 app/lifecycle/logging_nonwindows.go create mode 100644 app/lifecycle/logging_windows.go create mode 100644 app/lifecycle/paths.go create mode 100644 app/lifecycle/server.go create mode 100644 app/lifecycle/server_unix.go create mode 100644 app/lifecycle/server_windows.go create mode 100644 app/lifecycle/updater.go create mode 100644 app/lifecycle/updater_nonwindows.go create mode 100644 app/lifecycle/updater_windows.go create mode 100644 app/main.go create mode 100644 app/ollama.iss create mode 100644 app/ollama_welcome.ps1 create mode 100644 app/store/store.go create mode 100644 app/store/store_darwin.go create mode 100644 app/store/store_linux.go create mode 100644 app/store/store_windows.go create mode 100644 app/tray/commontray/types.go create mode 100644 app/tray/tray.go create mode 100644 app/tray/tray_nonwindows.go create mode 100644 app/tray/tray_windows.go create mode 100644 app/tray/wintray/eventloop.go create mode 100644 app/tray/wintray/menus.go create mode 100644 app/tray/wintray/messages.go create mode 100644 app/tray/wintray/notifyicon.go create mode 100644 app/tray/wintray/tray.go create mode 100644 app/tray/wintray/w32api.go create mode 100644 app/tray/wintray/winclass.go create mode 100644 cmd/start_darwin.go create mode 100644 cmd/start_default.go create mode 100644 cmd/start_windows.go create mode 100644 docs/windows.md create mode 100644 scripts/build_windows.ps1 diff --git a/.gitignore b/.gitignore index 97f73481..388175f7 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ ggml-metal.metal .cache *.exe .idea -test_data \ No newline at end of file +test_data +*.crt \ No newline at end of file diff --git a/app/README.md b/app/README.md new file mode 100644 index 00000000..883d7ab7 --- /dev/null +++ b/app/README.md @@ -0,0 +1,22 @@ +# Ollama App + +## Linux + +TODO + +## 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. + +``` +powershell -ExecutionPolicy Bypass -File .\scripts\build_windows.ps1 +``` diff --git a/app/assets/app.ico b/app/assets/app.ico new file mode 100644 index 0000000000000000000000000000000000000000..0c38e3d047e9e98441f961bc1c72075fb187a9e1 GIT binary patch literal 6227 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 I*cO4G0Lng3rT_o{ literal 0 HcmV?d00001 diff --git a/app/assets/tray.ico b/app/assets/tray.ico new file mode 100644 index 0000000000000000000000000000000000000000..c8f3e9f656c2902049ecc976ee44fd95d9a90638 GIT binary patch literal 22846 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 JE{gw({|Ct~wcY># literal 0 HcmV?d00001 diff --git a/app/assets/tray_upgrade.ico b/app/assets/tray_upgrade.ico new file mode 100644 index 0000000000000000000000000000000000000000..ade77fd14f3f879ea7c3a9de696381136c02ff27 GIT binary patch literal 23698 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 MWtsdd&s41VFHRJSrvLx| literal 0 HcmV?d00001 diff --git a/app/lifecycle/getstarted_nonwindows.go b/app/lifecycle/getstarted_nonwindows.go new file mode 100644 index 00000000..c36d14c0 --- /dev/null +++ b/app/lifecycle/getstarted_nonwindows.go @@ -0,0 +1,9 @@ +//go:build !windows + +package lifecycle + +import "fmt" + +func GetStarted() error { + return fmt.Errorf("GetStarted not implemented") +} diff --git a/app/lifecycle/getstarted_windows.go b/app/lifecycle/getstarted_windows.go new file mode 100644 index 00000000..092c3c17 --- /dev/null +++ b/app/lifecycle/getstarted_windows.go @@ -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() +} diff --git a/app/lifecycle/lifecycle.go b/app/lifecycle/lifecycle.go new file mode 100644 index 00000000..1fa9c7a8 --- /dev/null +++ b/app/lifecycle/lifecycle.go @@ -0,0 +1,83 @@ +package lifecycle + +import ( + "context" + "fmt" + "log" + "log/slog" + + "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.Debug("XXX detected server already running") + // TODO - should we fail fast, try to kill it, or just ignore? + } 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") +} diff --git a/app/lifecycle/logging.go b/app/lifecycle/logging.go new file mode 100644 index 00000000..98df9b41 --- /dev/null +++ b/app/lifecycle/logging.go @@ -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") +} diff --git a/app/lifecycle/logging_nonwindows.go b/app/lifecycle/logging_nonwindows.go new file mode 100644 index 00000000..50b3a638 --- /dev/null +++ b/app/lifecycle/logging_nonwindows.go @@ -0,0 +1,9 @@ +//go:build !windows + +package lifecycle + +import "log/slog" + +func ShowLogs() { + slog.Warn("ShowLogs not yet implemented") +} diff --git a/app/lifecycle/logging_windows.go b/app/lifecycle/logging_windows.go new file mode 100644 index 00000000..367a5274 --- /dev/null +++ b/app/lifecycle/logging_windows.go @@ -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)) + } +} diff --git a/app/lifecycle/paths.go b/app/lifecycle/paths.go new file mode 100644 index 00000000..e4f2dbd9 --- /dev/null +++ b/app/lifecycle/paths.go @@ -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 + } +} diff --git a/app/lifecycle/server.go b/app/lifecycle/server.go new file mode 100644 index 00000000..bc558d24 --- /dev/null +++ b/app/lifecycle/server.go @@ -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 +} diff --git a/app/lifecycle/server_unix.go b/app/lifecycle/server_unix.go new file mode 100644 index 00000000..c35f8b5b --- /dev/null +++ b/app/lifecycle/server_unix.go @@ -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") +} diff --git a/app/lifecycle/server_windows.go b/app/lifecycle/server_windows.go new file mode 100644 index 00000000..3044e526 --- /dev/null +++ b/app/lifecycle/server_windows.go @@ -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 +} diff --git a/app/lifecycle/updater.go b/app/lifecycle/updater.go new file mode 100644 index 00000000..c1430e28 --- /dev/null +++ b/app/lifecycle/updater.go @@ -0,0 +1,216 @@ +package lifecycle + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "log/slog" + "mime" + "net/http" + "os" + "path" + "path/filepath" + "runtime" + "strings" + "time" + + "github.com/jmorganca/ollama/auth" + "github.com/jmorganca/ollama/version" +) + +var ( + UpdateCheckURLBase = "https://ollama.ai/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 + updateCheckURL := UpdateCheckURLBase + "?os=" + runtime.GOOS + "&arch=" + runtime.GOARCH + "&version=" + version.Version + headers := make(http.Header) + err := auth.SignRequest(http.MethodGet, updateCheckURL, nil, headers) + if err != nil { + slog.Info(fmt.Sprintf("failed to sign update request %s", err)) + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, updateCheckURL, nil) + if err != nil { + slog.Warn(fmt.Sprintf("failed to check for update: %s", err)) + return false, updateResp + } + req.Header = headers + 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(fmt.Sprintf("checking for available update at %s with headers %v", updateCheckURL, headers)) + 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) + } + } + }() +} diff --git a/app/lifecycle/updater_nonwindows.go b/app/lifecycle/updater_nonwindows.go new file mode 100644 index 00000000..0f213b34 --- /dev/null +++ b/app/lifecycle/updater_nonwindows.go @@ -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") +} diff --git a/app/lifecycle/updater_windows.go b/app/lifecycle/updater_windows.go new file mode 100644 index 00000000..cc97f686 --- /dev/null +++ b/app/lifecycle/updater_windows.go @@ -0,0 +1,79 @@ +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 { + slog.Warn("XXX 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 +} diff --git a/app/main.go b/app/main.go new file mode 100644 index 00000000..57cfd72e --- /dev/null +++ b/app/main.go @@ -0,0 +1,17 @@ +package main + +// Compile with the following to get rid of the cmd pop up on windows +// go build -ldflags="-H windowsgui" . + +import ( + "os" + + "github.com/jmorganca/ollama/app/lifecycle" +) + +func main() { + // TODO - remove as we end the early access phase + os.Setenv("OLLAMA_DEBUG", "1") // nolint:errcheck + + lifecycle.Run() +} diff --git a/app/ollama.iss b/app/ollama.iss new file mode 100644 index 00000000..5d90176a --- /dev/null +++ b/app/ollama.iss @@ -0,0 +1,150 @@ +; 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, Inc." +#define MyAppURL "https://ollama.ai/" +#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 + +[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=Welcome to Ollama Windows Preview +ReadyLabel1=%nLet's get you up and running with your own large language models. +;ReadyLabel2b=We'll be installing Ollama in your user account without requiring Admin permissions + +;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; diff --git a/app/ollama_welcome.ps1 b/app/ollama_welcome.ps1 new file mode 100644 index 00000000..e7056952 --- /dev/null +++ b/app/ollama_welcome.ps1 @@ -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 "" \ No newline at end of file diff --git a/app/store/store.go b/app/store/store.go new file mode 100644 index 00000000..13a75a60 --- /dev/null +++ b/app/store/store.go @@ -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)) +} diff --git a/app/store/store_darwin.go b/app/store/store_darwin.go new file mode 100644 index 00000000..e53d8525 --- /dev/null +++ b/app/store/store_darwin.go @@ -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") +} diff --git a/app/store/store_linux.go b/app/store/store_linux.go new file mode 100644 index 00000000..3aac9b01 --- /dev/null +++ b/app/store/store_linux.go @@ -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") +} diff --git a/app/store/store_windows.go b/app/store/store_windows.go new file mode 100644 index 00000000..ba06b82c --- /dev/null +++ b/app/store/store_windows.go @@ -0,0 +1,11 @@ +package store + +import ( + "os" + "path/filepath" +) + +func getStorePath() string { + localAppData := os.Getenv("LOCALAPPDATA") + return filepath.Join(localAppData, "Ollama", "config.json") +} diff --git a/app/tray/commontray/types.go b/app/tray/commontray/types.go new file mode 100644 index 00000000..ed633dc9 --- /dev/null +++ b/app/tray/commontray/types.go @@ -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() +} diff --git a/app/tray/tray.go b/app/tray/tray.go new file mode 100644 index 00000000..47b204d6 --- /dev/null +++ b/app/tray/tray.go @@ -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 +} diff --git a/app/tray/tray_nonwindows.go b/app/tray/tray_nonwindows.go new file mode 100644 index 00000000..6c30c3c2 --- /dev/null +++ b/app/tray/tray_nonwindows.go @@ -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") +} diff --git a/app/tray/tray_windows.go b/app/tray/tray_windows.go new file mode 100644 index 00000000..8ac4e478 --- /dev/null +++ b/app/tray/tray_windows.go @@ -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) +} diff --git a/app/tray/wintray/eventloop.go b/app/tray/wintray/eventloop.go new file mode 100644 index 00000000..958b7871 --- /dev/null +++ b/app/tray/wintray/eventloop.go @@ -0,0 +1,189 @@ +//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: + // slog.Debug(fmt.Sprintf("XXX dispatching message from run loop 0x%x", m.Message)) + 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 + ) + // slog.Debug(fmt.Sprintf("XXX in wndProc: 0x%x", message)) + switch message { + case WM_COMMAND: + menuItemId := int32(wParam) + // slog.Debug(fmt.Sprintf("XXX Menu Click: %d", menuItemId)) + // 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 + slog.Debug("XXX got taskbar created event") + 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 + // slog.Debug(fmt.Sprintf("XXX default wndProc handler 0x%x", message)) + 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)) + } +} diff --git a/app/tray/wintray/menus.go b/app/tray/wintray/menus.go new file mode 100644 index 00000000..efbb8e89 --- /dev/null +++ b/app/tray/wintray/menus.go @@ -0,0 +1,75 @@ +//go:build windows + +package wintray + +import ( + "fmt" + "log/slog" + "os" + "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 debug := os.Getenv("OLLAMA_DEBUG"); debug != "" { + 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 +} diff --git a/app/tray/wintray/messages.go b/app/tray/wintray/messages.go new file mode 100644 index 00000000..d364c716 --- /dev/null +++ b/app/tray/wintray/messages.go @@ -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" +) diff --git a/app/tray/wintray/notifyicon.go b/app/tray/wintray/notifyicon.go new file mode 100644 index 00000000..47071669 --- /dev/null +++ b/app/tray/wintray/notifyicon.go @@ -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 +} diff --git a/app/tray/wintray/tray.go b/app/tray/wintray/tray.go new file mode 100644 index 00000000..365cfb82 --- /dev/null +++ b/app/tray/wintray/tray.go @@ -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() +} diff --git a/app/tray/wintray/w32api.go b/app/tray/wintray/w32api.go new file mode 100644 index 00000000..a1e0381d --- /dev/null +++ b/app/tray/wintray/w32api.go @@ -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 +} diff --git a/app/tray/wintray/winclass.go b/app/tray/wintray/winclass.go new file mode 100644 index 00000000..9ce71d00 --- /dev/null +++ b/app/tray/wintray/winclass.go @@ -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 +} diff --git a/cmd/cmd.go b/cmd/cmd.go index 66909c2c..1c7eba43 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -14,10 +14,8 @@ import ( "net" "net/http" "os" - "os/exec" "os/signal" "path/filepath" - "runtime" "strings" "syscall" "time" @@ -754,22 +752,8 @@ func initializeKeypair() error { return nil } -func startMacApp(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 - } +//nolint:unused +func waitForServer(ctx context.Context, client *api.Client) error { // wait for the server to start timeout := time.After(5 * time.Second) tick := time.Tick(500 * time.Millisecond) @@ -783,6 +767,7 @@ func startMacApp(ctx context.Context, client *api.Client) error { } } } + } func checkServerHeartbeat(cmd *cobra.Command, _ []string) error { @@ -791,15 +776,11 @@ func checkServerHeartbeat(cmd *cobra.Command, _ []string) error { return err } if err := client.Heartbeat(cmd.Context()); err != nil { - if !strings.Contains(err.Error(), "connection refused") { + if !strings.Contains(err.Error(), " refused") { return err } - if runtime.GOOS == "darwin" { - if err := startMacApp(cmd.Context(), client); err != nil { - 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") + if err := startApp(cmd.Context(), client); err != nil { + return fmt.Errorf("could not connect to ollama app, is it running?") } } return nil diff --git a/cmd/start_darwin.go b/cmd/start_darwin.go new file mode 100644 index 00000000..7e3000f0 --- /dev/null +++ b/cmd/start_darwin.go @@ -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) +} diff --git a/cmd/start_default.go b/cmd/start_default.go new file mode 100644 index 00000000..664c2d1f --- /dev/null +++ b/cmd/start_default.go @@ -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") +} diff --git a/cmd/start_windows.go b/cmd/start_windows.go new file mode 100644 index 00000000..a24f1c19 --- /dev/null +++ b/cmd/start_windows.go @@ -0,0 +1,81 @@ +package cmd + +import ( + "context" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "syscall" + + "golang.org/x/sys/windows" + + "github.com/jmorganca/ollama/api" +) + +func init() { + var inMode uint32 + var outMode uint32 + var errMode uint32 + + in := windows.Handle(os.Stdin.Fd()) + if err := windows.GetConsoleMode(in, &inMode); err == nil { + windows.SetConsoleMode(in, inMode|windows.ENABLE_VIRTUAL_TERMINAL_INPUT) //nolint:errcheck + } + + out := windows.Handle(os.Stdout.Fd()) + if err := windows.GetConsoleMode(out, &outMode); err == nil { + windows.SetConsoleMode(out, outMode|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING) //nolint:errcheck + } + + errf := windows.Handle(os.Stderr.Fd()) + if err := windows.GetConsoleMode(errf, &errMode); err == nil { + windows.SetConsoleMode(errf, errMode|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING) //nolint:errcheck + } +} + +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) +} diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 1367194e..a5fb301f 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,60 +1,72 @@ -# 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: - -```shell -cat ~/.ollama/logs/server.log -``` - -On Linux systems with systemd, the logs can be found with this command: - -```shell -journalctl -u ollama -``` - -When you run Ollama in a container, the logs go to stdout/stderr in the container: - -```shell -docker logs -``` -(Use `docker ps` to find the container name) - -If manually running `ollama serve` in a terminal, the logs will be on that terminal. - -Join the [Discord](https://discord.gg/ollama) for help interpreting the logs. - -## LLM libraries - -Ollama includes multiple LLM libraries compiled for different GPUs and CPU -vector features. Ollama tries to pick the best one based on the capabilities of -your system. If this autodetection has problems, or you run into other problems -(e.g. crashes in your GPU) you can workaround this by forcing a specific LLM -library. `cpu_avx2` will perform the best, followed by `cpu_avx` an the slowest -but most compatible is `cpu`. Rosetta emulation under MacOS will work with the -`cpu` library. - -In the server log, you will see a message that looks something like this (varies -from release to release): - -``` -Dynamic LLM libraries [rocm_v6 cpu cpu_avx cpu_avx2 cuda_v11 rocm_v5] -``` - -**Experimental LLM Library Override** - -You can set OLLAMA_LLM_LIBRARY to any of the available LLM libraries to bypass -autodetection, so for example, if you have a CUDA card, but want to force the -CPU LLM library with AVX2 vector support, use: - -``` -OLLAMA_LLM_LIBRARY="cpu_avx2" ollama serve -``` - -You can see what features your CPU has with the following. -``` -cat /proc/cpuinfo| grep flags | head -1 -``` - -## Known 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: + +```shell +cat ~/.ollama/logs/server.log +``` + +On **Linux** systems with systemd, the logs can be found with this command: + +```shell +journalctl -u ollama +``` + +When you run Ollama in a **container**, the logs go to stdout/stderr in the container: + +```shell +docker logs +``` +(Use `docker ps` to find the container name) + +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 `+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. + +## LLM libraries + +Ollama includes multiple LLM libraries compiled for different GPUs and CPU +vector features. Ollama tries to pick the best one based on the capabilities of +your system. If this autodetection has problems, or you run into other problems +(e.g. crashes in your GPU) you can workaround this by forcing a specific LLM +library. `cpu_avx2` will perform the best, followed by `cpu_avx` an the slowest +but most compatible is `cpu`. Rosetta emulation under MacOS will work with the +`cpu` library. + +In the server log, you will see a message that looks something like this (varies +from release to release): + +``` +Dynamic LLM libraries [rocm_v6 cpu cpu_avx cpu_avx2 cuda_v11 rocm_v5] +``` + +**Experimental LLM Library Override** + +You can set OLLAMA_LLM_LIBRARY to any of the available LLM libraries to bypass +autodetection, so for example, if you have a CUDA card, but want to force the +CPU LLM library with AVX2 vector support, use: + +``` +OLLAMA_LLM_LIBRARY="cpu_avx2" ollama serve +``` + +You can see what features your CPU has with the following. +``` +cat /proc/cpuinfo| grep flags | head -1 +``` + +## Known issues + * N/A \ No newline at end of file diff --git a/docs/windows.md b/docs/windows.md new file mode 100644 index 00000000..b43470c8 --- /dev/null +++ b/docs/windows.md @@ -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 be 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 `+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 diff --git a/go.mod b/go.mod index 57ec2495..1118de66 100644 --- a/go.mod +++ b/go.mod @@ -12,8 +12,20 @@ require ( ) require ( + github.com/cratonica/2goarray v0.0.0-20190331194516-514510793eaa // 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/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/rivo/uniseg v0.2.0 // indirect ) diff --git a/go.sum b/go.sum index ff6bcbd9..a256b0be 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,8 @@ github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F 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/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/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= @@ -13,6 +15,20 @@ github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc 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/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/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= @@ -31,6 +47,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.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= 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.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= @@ -39,6 +57,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/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -57,6 +77,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.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= 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.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= @@ -70,8 +92,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/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 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/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.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= @@ -84,6 +110,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/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 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/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -120,11 +147,13 @@ golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= 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/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-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-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.1.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/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -141,6 +170,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.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= 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 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/llm/generate/gen_windows.ps1 b/llm/generate/gen_windows.ps1 index f7a241cc..ba0f954c 100644 --- a/llm/generate/gen_windows.ps1 +++ b/llm/generate/gen_windows.ps1 @@ -4,7 +4,7 @@ $ErrorActionPreference = "Stop" function init_vars { $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:ARCH = "amd64" # arm not yet supported. if ($env:CGO_CFLAGS -contains "-g") { @@ -19,6 +19,7 @@ function init_vars { $d=(get-command -ea 'silentlycontinue' nvcc).path if ($d -ne $null) { $script:CUDA_LIB_DIR=($d| split-path -parent) + $script:CUDA_INCLUDE_DIR=($script:CUDA_LIB_DIR|split-path -parent)+"\include" } } else { $script:CUDA_LIB_DIR=$env:CUDA_LIB_DIR @@ -30,6 +31,8 @@ function init_vars { } else { $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 { @@ -56,8 +59,8 @@ function apply_patches { } # Checkout each file + Set-Location -Path ${script:llamacppDir} foreach ($file in $filePaths) { - Set-Location -Path ${script:llamacppDir} git checkout $file } } @@ -89,13 +92,23 @@ function install { 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}/llama.dll" "${script:buildDir}/lib" - # Display the dll dependencies in the build log if ($script:DUMPBIN -ne $null) { & "$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 { if ($script:GZIP -eq $null) { write-host "gzip not installed, not compressing files" @@ -109,8 +122,23 @@ function compress_libs { } 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" git checkout CMakeLists.txt server.cpp + } init_vars @@ -129,6 +157,7 @@ $script:buildDir="${script:llamacppDir}/build/windows/${script:ARCH}/cpu" write-host "Building LCD CPU" build install +sign compress_libs $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" build install +sign compress_libs $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" build install +sign compress_libs if ($null -ne $script:CUDA_LIB_DIR) { # Then build cuda as a dynamically loaded library - $nvcc = (get-command -ea 'silentlycontinue' nvcc) - if ($null -ne $nvcc) { - $script:CUDA_VERSION=(get-item ($nvcc | split-path | split-path)).Basename - } + $nvcc = "$script:CUDA_LIB_DIR\nvcc.exe" + $script:CUDA_VERSION=(get-item ($nvcc | split-path | split-path)).Basename if ($null -ne $script:CUDA_VERSION) { $script:CUDA_VARIANT="_"+$script:CUDA_VERSION } init_vars $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 install - cp "${script:CUDA_LIB_DIR}/cudart64_*.dll" "${script:buildDir}/lib" - cp "${script:CUDA_LIB_DIR}/cublas64_*.dll" "${script:buildDir}/lib" - cp "${script:CUDA_LIB_DIR}/cublasLt64_*.dll" "${script:buildDir}/lib" + sign compress_libs } # TODO - actually implement ROCm support on windows @@ -172,4 +199,4 @@ md "${script:buildDir}/lib" -ea 0 > $null echo $null >> "${script:buildDir}/lib/.generated" cleanup -write-host "`ngo generate completed" \ No newline at end of file +write-host "`ngo generate completed" diff --git a/scripts/build_remote.py b/scripts/build_remote.py index 314232ac..2ab58ad7 100755 --- a/scripts/build_remote.py +++ b/scripts/build_remote.py @@ -60,13 +60,17 @@ subprocess.check_call(['ssh', netloc, 'cd', path, ';', 'git', 'checkout', branch # subprocess.check_call(['ssh', netloc, 'cd', path, ';', 'env']) # TODO - or consider paramiko maybe -print("Performing generate") -subprocess.check_call(['ssh', netloc, 'cd', path, ';', GoCmd, 'generate', './...']) +print("Running Windows Build Script") +subprocess.check_call(['ssh', netloc, 'cd', path, ';', "powershell", "-ExecutionPolicy", "Bypass", "-File", "./scripts/build_windows.ps1"]) -print("Building") -subprocess.check_call(['ssh', netloc, 'cd', path, ';', GoCmd, 'build', '.']) +# print("Building") +# subprocess.check_call(['ssh', netloc, 'cd', path, ';', GoCmd, 'build', '.']) print("Copying built result") subprocess.check_call(['scp', netloc +":"+ path + "/ollama.exe", './dist/']) +print("Copying installer") +subprocess.check_call(['scp', netloc +":"+ path + "/dist/Ollama Setup.exe", './dist/']) + + diff --git a/scripts/build_windows.ps1 b/scripts/build_windows.ps1 new file mode 100644 index 00000000..5da898ac --- /dev/null +++ b/scripts/build_windows.ps1 @@ -0,0 +1,130 @@ +#!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)} + } +} + +function buildApp() { + write-host "Building Ollama App" + cd "${script:SRC_DIR}\app" + & 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="" +} \ No newline at end of file From 66ef308abdccf3e0098715f66253898e9ff12702 Mon Sep 17 00:00:00 2001 From: vinjn Date: Thu, 23 Nov 2023 22:21:32 -0800 Subject: [PATCH 04/20] Import "containerd/console" lib to support colorful output in Windows terminal --- cmd/cmd.go | 8 ++++++++ cmd/start_windows.go | 23 ----------------------- go.mod | 2 ++ go.sum | 3 +++ 4 files changed, 13 insertions(+), 23 deletions(-) diff --git a/cmd/cmd.go b/cmd/cmd.go index 1c7eba43..55535f7a 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -16,10 +16,13 @@ import ( "os" "os/signal" "path/filepath" + "runtime" "strings" "syscall" "time" + "github.com/containerd/console" + "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" "golang.org/x/crypto/ssh" @@ -810,6 +813,11 @@ func NewCLI() *cobra.Command { log.SetFlags(log.LstdFlags | log.Lshortfile) cobra.EnableCommandSorting = false + if runtime.GOOS == "windows" { + // Enable colorful ANSI escape code in Windows terminal (disabled by default) + console.ConsoleFromFile(os.Stdout) + } + rootCmd := &cobra.Command{ Use: "ollama", Short: "Large language model runner", diff --git a/cmd/start_windows.go b/cmd/start_windows.go index a24f1c19..b9a423cf 100644 --- a/cmd/start_windows.go +++ b/cmd/start_windows.go @@ -10,32 +10,9 @@ import ( "strings" "syscall" - "golang.org/x/sys/windows" - "github.com/jmorganca/ollama/api" ) -func init() { - var inMode uint32 - var outMode uint32 - var errMode uint32 - - in := windows.Handle(os.Stdin.Fd()) - if err := windows.GetConsoleMode(in, &inMode); err == nil { - windows.SetConsoleMode(in, inMode|windows.ENABLE_VIRTUAL_TERMINAL_INPUT) //nolint:errcheck - } - - out := windows.Handle(os.Stdout.Fd()) - if err := windows.GetConsoleMode(out, &outMode); err == nil { - windows.SetConsoleMode(out, outMode|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING) //nolint:errcheck - } - - errf := windows.Handle(os.Stderr.Fd()) - if err := windows.GetConsoleMode(errf, &errMode); err == nil { - windows.SetConsoleMode(errf, errMode|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING) //nolint:errcheck - } -} - func startApp(ctx context.Context, client *api.Client) error { // log.Printf("XXX Attempting to find and start ollama app") AppName := "ollama app.exe" diff --git a/go.mod b/go.mod index 1118de66..f85ce26a 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ 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/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect @@ -30,6 +31,7 @@ require ( github.com/rivo/uniseg v0.2.0 // indirect ) + require ( github.com/bytedance/sonic v1.9.1 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect diff --git a/go.sum b/go.sum index a256b0be..414a65e7 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ 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-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= 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/cratonica/2goarray v0.0.0-20190331194516-514510793eaa h1:Wg+722vs7a2zQH5lR9QWYsVbplKeffaQFIs5FTdfNNo= github.com/cratonica/2goarray v0.0.0-20190331194516-514510793eaa/go.mod h1:6Arca19mRx58CA7OWEd7Wu1NpC1rd3uDnNs6s1pj/DI= @@ -149,6 +151,7 @@ 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/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-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-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= From 823a520266ab51442d8f2d8631a0c2676f79dd3d Mon Sep 17 00:00:00 2001 From: Daniel Hiltgen Date: Tue, 13 Feb 2024 19:38:52 -0800 Subject: [PATCH 05/20] Fix lint error on ignored error for win console --- cmd/cmd.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/cmd.go b/cmd/cmd.go index 55535f7a..a56576ea 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -815,7 +815,7 @@ func NewCLI() *cobra.Command { if runtime.GOOS == "windows" { // Enable colorful ANSI escape code in Windows terminal (disabled by default) - console.ConsoleFromFile(os.Stdout) + console.ConsoleFromFile(os.Stdout) //nolint:errcheck } rootCmd := &cobra.Command{ From e43648afe5a4ff6588790f4c462f2944037ad42c Mon Sep 17 00:00:00 2001 From: Michael Yang Date: Wed, 14 Feb 2024 11:29:49 -0800 Subject: [PATCH 06/20] rerefactor --- app/lifecycle/updater.go | 38 ++++++++-- app/ollama.iss | 2 +- auth/auth.go | 153 ++++----------------------------------- auth/request.go | 72 ------------------ server/auth.go | 95 ++++++++++++++++++++++++ server/download.go | 11 ++- server/images.go | 83 ++++++++++++++++++--- server/routes.go | 5 +- server/upload.go | 16 ++-- 9 files changed, 224 insertions(+), 251 deletions(-) delete mode 100644 auth/request.go create mode 100644 server/auth.go diff --git a/app/lifecycle/updater.go b/app/lifecycle/updater.go index c1430e28..47db53c5 100644 --- a/app/lifecycle/updater.go +++ b/app/lifecycle/updater.go @@ -2,6 +2,7 @@ package lifecycle import ( "context" + "crypto/rand" "encoding/json" "errors" "fmt" @@ -9,6 +10,7 @@ import ( "log/slog" "mime" "net/http" + "net/url" "os" "path" "path/filepath" @@ -21,7 +23,7 @@ import ( ) var ( - UpdateCheckURLBase = "https://ollama.ai/api/update" + UpdateCheckURLBase = "https://ollama.com/api/update" UpdateDownloaded = false ) @@ -47,22 +49,42 @@ func getClient(req *http.Request) http.Client { func IsNewReleaseAvailable(ctx context.Context) (bool, UpdateResponse) { var updateResp UpdateResponse - updateCheckURL := UpdateCheckURLBase + "?os=" + runtime.GOOS + "&arch=" + runtime.GOARCH + "&version=" + version.Version - headers := make(http.Header) - err := auth.SignRequest(http.MethodGet, updateCheckURL, nil, headers) + + requestURL, err := url.Parse(UpdateCheckURLBase) if err != nil { - slog.Info(fmt.Sprintf("failed to sign update request %s", err)) + return false, updateResp } - req, err := http.NewRequestWithContext(ctx, http.MethodGet, updateCheckURL, nil) + + 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 = headers + 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(fmt.Sprintf("checking for available update at %s with headers %v", updateCheckURL, headers)) + slog.Debug(fmt.Sprintf("checking for available update at %s with headers %v", requestURL, req.Header)) resp, err := client.Do(req) if err != nil { slog.Warn(fmt.Sprintf("failed to check for update: %s", err)) diff --git a/app/ollama.iss b/app/ollama.iss index 5d90176a..1f9c4a62 100644 --- a/app/ollama.iss +++ b/app/ollama.iss @@ -12,7 +12,7 @@ #define MyAppVersion "0.0.0" #endif #define MyAppPublisher "Ollama, Inc." -#define MyAppURL "https://ollama.ai/" +#define MyAppURL "https://ollama.com/" #define MyAppExeName "ollama app.exe" #define MyIcon ".\assets\app.ico" diff --git a/auth/auth.go b/auth/auth.go index c0ce0a52..ca64670d 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -4,185 +4,58 @@ import ( "bytes" "context" "crypto/rand" - "crypto/sha256" "encoding/base64" - "encoding/hex" - "encoding/json" "fmt" "io" "log/slog" - "net/http" - "net/url" "os" "path/filepath" - "strconv" - "strings" - "time" "golang.org/x/crypto/ssh" - - "github.com/jmorganca/ollama/api" ) -const ( - KeyType = "id_ed25519" -) +const defaultPrivateKey = "id_ed25519" -type AuthRedirect struct { - Realm string - Service string - Scope string -} - -type SignatureData struct { - Method string - Path string - Data []byte -} - -func generateNonce(length int) (string, error) { +func NewNonce(r io.Reader, length int) (string, error) { nonce := make([]byte, length) - _, err := rand.Read(nonce) - if err != nil { + if _, err := io.ReadFull(r, nonce); err != nil { return "", err } + return base64.RawURLEncoding.EncodeToString(nonce), nil } -func (r AuthRedirect) URL() (*url.URL, error) { - redirectURL, err := url.Parse(r.Realm) - if err != nil { - return nil, err - } - - values := redirectURL.Query() - - values.Add("service", r.Service) - - for _, s := range strings.Split(r.Scope, " ") { - values.Add("scope", s) - } - - values.Add("ts", strconv.FormatInt(time.Now().Unix(), 10)) - - nonce, err := generateNonce(16) - if err != nil { - return nil, err - } - values.Add("nonce", nonce) - - redirectURL.RawQuery = values.Encode() - return redirectURL, nil -} - -func SignRequest(method, url string, data []byte, headers http.Header) error { +func Sign(ctx context.Context, bts []byte) (string, error) { home, err := os.UserHomeDir() if err != nil { - return err + return "", err } - keyPath := filepath.Join(home, ".ollama", KeyType) + keyPath := filepath.Join(home, ".ollama", defaultPrivateKey) - rawKey, err := os.ReadFile(keyPath) + privateKeyFile, err := os.ReadFile(keyPath) if err != nil { slog.Info(fmt.Sprintf("Failed to load private key: %v", err)) - return err - } - - s := SignatureData{ - Method: method, - Path: url, - Data: data, - } - - sig, err := s.Sign(rawKey) - if err != nil { - return err - } - - headers.Set("Authorization", sig) - return nil -} - -func GetAuthToken(ctx context.Context, redirData AuthRedirect) (string, error) { - redirectURL, err := redirData.URL() - if err != nil { return "", err } - headers := make(http.Header) - err = SignRequest(http.MethodGet, redirectURL.String(), nil, headers) - if err != nil { - return "", err - } - 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 { - return "", err - } - - var tok api.TokenResponse - if err := json.Unmarshal(respBody, &tok); err != nil { - return "", err - } - - return tok.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: - // ",," - // 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) + privateKey, err := ssh.ParsePrivateKey(privateKeyFile) if err != nil { return "", err } // get the pubkey, but remove the type - pubKey := ssh.MarshalAuthorizedKey(signer.PublicKey()) - parts := bytes.Split(pubKey, []byte(" ")) + publicKey := ssh.MarshalAuthorizedKey(privateKey.PublicKey()) + parts := bytes.Split(publicKey, []byte(" ")) if len(parts) < 2 { return "", fmt.Errorf("malformed public key") } - signedData, err := signer.Sign(nil, s.Bytes()) + signedData, err := privateKey.Sign(rand.Reader, bts) if err != nil { return "", err } // signature is : - sig := fmt.Sprintf("%s:%s", bytes.TrimSpace(parts[1]), base64.StdEncoding.EncodeToString(signedData.Blob)) - return sig, nil + return fmt.Sprintf("%s:%s", bytes.TrimSpace(parts[1]), base64.StdEncoding.EncodeToString(signedData.Blob)), nil } diff --git a/auth/request.go b/auth/request.go deleted file mode 100644 index ab863fe3..00000000 --- a/auth/request.go +++ /dev/null @@ -1,72 +0,0 @@ -package auth - -import ( - "context" - "fmt" - "io" - "net/http" - "net/url" - "runtime" - "strconv" - - "github.com/jmorganca/ollama/version" -) - -type RegistryOptions struct { - Insecure bool - Username string - Password string - Token string -} - -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 { - requestURL.Scheme = "http" - } - - req, err := http.NewRequestWithContext(ctx, method, requestURL.String(), body) - if err != nil { - return nil, err - } - - if headers != nil { - req.Header = headers - } - - if regOpts != nil { - if regOpts.Token != "" { - req.Header.Set("Authorization", "Bearer "+regOpts.Token) - } else if regOpts.Username != "" && regOpts.Password != "" { - req.SetBasicAuth(regOpts.Username, regOpts.Password) - } - } - - req.Header.Set("User-Agent", fmt.Sprintf("ollama/%s (%s %s) Go/%s", version.Version, runtime.GOARCH, runtime.GOOS, runtime.Version())) - - if s := req.Header.Get("Content-Length"); s != "" { - contentLength, err := strconv.ParseInt(s, 10, 64) - if err != nil { - return nil, err - } - - req.ContentLength = contentLength - } - - proxyURL, err := http.ProxyFromEnvironment(req) - if err != nil { - return nil, err - } - - client := http.Client{ - Transport: &http.Transport{ - Proxy: http.ProxyURL(proxyURL), - }, - } - - resp, err := client.Do(req) - if err != nil { - return nil, err - } - - return resp, nil -} diff --git a/server/auth.go b/server/auth.go new file mode 100644 index 00000000..5af85ff6 --- /dev/null +++ b/server/auth.go @@ -0,0 +1,95 @@ +package server + +import ( + "context" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/jmorganca/ollama/api" + "github.com/jmorganca/ollama/auth" +) + +type registryChallenge struct { + Realm string + Service string + Scope string +} + +func (r registryChallenge) URL() (*url.URL, error) { + redirectURL, err := url.Parse(r.Realm) + if err != nil { + return nil, err + } + + values := redirectURL.Query() + values.Add("service", r.Service) + for _, s := range strings.Split(r.Scope, " ") { + values.Add("scope", s) + } + + values.Add("ts", strconv.FormatInt(time.Now().Unix(), 10)) + + nonce, err := auth.NewNonce(rand.Reader, 16) + if err != nil { + return nil, err + } + + values.Add("nonce", nonce) + + redirectURL.RawQuery = values.Encode() + return redirectURL, nil +} + +func getAuthorizationToken(ctx context.Context, challenge registryChallenge) (string, error) { + redirectURL, err := challenge.URL() + if err != nil { + return "", err + } + + sha256sum := sha256.Sum256(nil) + data := []byte(fmt.Sprintf("%s,%s,%s", http.MethodGet, redirectURL.String(), base64.StdEncoding.EncodeToString([]byte(hex.EncodeToString(sha256sum[:]))))) + + headers := make(http.Header) + signature, err := auth.Sign(ctx, data) + if err != nil { + return "", err + } + + headers.Add("Authorization", signature) + + 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 token.Token, nil +} diff --git a/server/download.go b/server/download.go index dbfba2dd..f6d199b9 100644 --- a/server/download.go +++ b/server/download.go @@ -22,7 +22,6 @@ import ( "golang.org/x/sync/errgroup" "github.com/jmorganca/ollama/api" - "github.com/jmorganca/ollama/auth" "github.com/jmorganca/ollama/format" ) @@ -86,7 +85,7 @@ func (p *blobDownloadPart) Write(b []byte) (n int, err error) { return n, nil } -func (b *blobDownload) Prepare(ctx context.Context, requestURL *url.URL, opts *auth.RegistryOptions) error { +func (b *blobDownload) Prepare(ctx context.Context, requestURL *url.URL, opts *registryOptions) error { partFilePaths, err := filepath.Glob(b.Name + "-partial-*") if err != nil { return err @@ -138,11 +137,11 @@ func (b *blobDownload) Prepare(ctx context.Context, requestURL *url.URL, opts *a return nil } -func (b *blobDownload) Run(ctx context.Context, requestURL *url.URL, opts *auth.RegistryOptions) { +func (b *blobDownload) Run(ctx context.Context, requestURL *url.URL, opts *registryOptions) { b.err = b.run(ctx, requestURL, opts) } -func (b *blobDownload) run(ctx context.Context, requestURL *url.URL, opts *auth.RegistryOptions) error { +func (b *blobDownload) run(ctx context.Context, requestURL *url.URL, opts *registryOptions) error { defer blobDownloadManager.Delete(b.Digest) ctx, b.CancelFunc = context.WithCancel(ctx) @@ -211,7 +210,7 @@ func (b *blobDownload) run(ctx context.Context, requestURL *url.URL, opts *auth. return nil } -func (b *blobDownload) downloadChunk(ctx context.Context, requestURL *url.URL, w io.Writer, part *blobDownloadPart, opts *auth.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.Go(func() error { headers := make(http.Header) @@ -335,7 +334,7 @@ func (b *blobDownload) Wait(ctx context.Context, fn func(api.ProgressResponse)) type downloadOpts struct { mp ModelPath digest string - regOpts *auth.RegistryOptions + regOpts *registryOptions fn func(api.ProgressResponse) } diff --git a/server/images.go b/server/images.go index 8a70cdd5..391809cb 100644 --- a/server/images.go +++ b/server/images.go @@ -16,17 +16,25 @@ import ( "os" "path/filepath" "runtime" + "strconv" "strings" "text/template" "golang.org/x/exp/slices" "github.com/jmorganca/ollama/api" - "github.com/jmorganca/ollama/auth" "github.com/jmorganca/ollama/llm" "github.com/jmorganca/ollama/parser" + "github.com/jmorganca/ollama/version" ) +type registryOptions struct { + Insecure bool + Username string + Password string + Token string +} + type Model struct { Name string `json:"name"` Config ConfigV2 @@ -312,7 +320,7 @@ func CreateModel(ctx context.Context, name, modelFileDir string, commands []pars switch { case errors.Is(err, os.ErrNotExist): fn(api.ProgressResponse{Status: "pulling model"}) - if err := PullModel(ctx, c.Args, &auth.RegistryOptions{}, fn); err != nil { + if err := PullModel(ctx, c.Args, ®istryOptions{}, fn); err != nil { return err } @@ -832,7 +840,7 @@ PARAMETER {{ $k }} {{ printf "%#v" $parameter }} return buf.String(), nil } -func PushModel(ctx context.Context, name string, regOpts *auth.RegistryOptions, fn func(api.ProgressResponse)) error { +func PushModel(ctx context.Context, name string, regOpts *registryOptions, fn func(api.ProgressResponse)) error { mp := ParseModelPath(name) fn(api.ProgressResponse{Status: "retrieving manifest"}) @@ -882,7 +890,7 @@ func PushModel(ctx context.Context, name string, regOpts *auth.RegistryOptions, return nil } -func PullModel(ctx context.Context, name string, regOpts *auth.RegistryOptions, fn func(api.ProgressResponse)) error { +func PullModel(ctx context.Context, name string, regOpts *registryOptions, fn func(api.ProgressResponse)) error { mp := ParseModelPath(name) var manifest *ManifestV2 @@ -988,7 +996,7 @@ func PullModel(ctx context.Context, name string, regOpts *auth.RegistryOptions, return nil } -func pullModelManifest(ctx context.Context, mp ModelPath, regOpts *auth.RegistryOptions) (*ManifestV2, error) { +func pullModelManifest(ctx context.Context, mp ModelPath, regOpts *registryOptions) (*ManifestV2, error) { requestURL := mp.BaseURL().JoinPath("v2", mp.GetNamespaceRepository(), "manifests", mp.Tag) headers := make(http.Header) @@ -1020,9 +1028,9 @@ func GetSHA256Digest(r io.Reader) (string, int64) { var errUnauthorized = fmt.Errorf("unauthorized") -func makeRequestWithRetry(ctx context.Context, method string, requestURL *url.URL, headers http.Header, body io.ReadSeeker, regOpts *auth.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++ { - resp, err := auth.MakeRequest(ctx, method, requestURL, headers, body, regOpts) + resp, err := makeRequest(ctx, method, requestURL, headers, body, regOpts) if err != nil { if !errors.Is(err, context.Canceled) { slog.Info(fmt.Sprintf("request failed: %v", err)) @@ -1034,9 +1042,8 @@ func makeRequestWithRetry(ctx context.Context, method string, requestURL *url.UR switch { case resp.StatusCode == http.StatusUnauthorized: // Handle authentication error with one retry - authenticate := resp.Header.Get("www-authenticate") - authRedir := ParseAuthRedirectString(authenticate) - token, err := auth.GetAuthToken(ctx, authRedir) + challenge := parseRegistryChallenge(resp.Header.Get("www-authenticate")) + token, err := getAuthorizationToken(ctx, challenge) if err != nil { return nil, err } @@ -1063,6 +1070,58 @@ func makeRequestWithRetry(ctx context.Context, method string, requestURL *url.UR return nil, errUnauthorized } +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 { + requestURL.Scheme = "http" + } + + req, err := http.NewRequestWithContext(ctx, method, requestURL.String(), body) + if err != nil { + return nil, err + } + + if headers != nil { + req.Header = headers + } + + if regOpts != nil { + if regOpts.Token != "" { + req.Header.Set("Authorization", "Bearer "+regOpts.Token) + } else if regOpts.Username != "" && regOpts.Password != "" { + req.SetBasicAuth(regOpts.Username, regOpts.Password) + } + } + + req.Header.Set("User-Agent", fmt.Sprintf("ollama/%s (%s %s) Go/%s", version.Version, runtime.GOARCH, runtime.GOOS, runtime.Version())) + + if s := req.Header.Get("Content-Length"); s != "" { + contentLength, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return nil, err + } + + req.ContentLength = contentLength + } + + proxyURL, err := http.ProxyFromEnvironment(req) + if err != nil { + return nil, err + } + + client := http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyURL(proxyURL), + }, + } + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + + return resp, nil +} + func getValue(header, key string) string { startIdx := strings.Index(header, key+"=") if startIdx == -1 { @@ -1086,10 +1145,10 @@ func getValue(header, key string) string { return header[startIdx:endIdx] } -func ParseAuthRedirectString(authStr string) auth.AuthRedirect { +func parseRegistryChallenge(authStr string) registryChallenge { authStr = strings.TrimPrefix(authStr, "Bearer ") - return auth.AuthRedirect{ + return registryChallenge{ Realm: getValue(authStr, "realm"), Service: getValue(authStr, "service"), Scope: getValue(authStr, "scope"), diff --git a/server/routes.go b/server/routes.go index ddf22e78..49ea33ac 100644 --- a/server/routes.go +++ b/server/routes.go @@ -25,7 +25,6 @@ import ( "golang.org/x/exp/slices" "github.com/jmorganca/ollama/api" - "github.com/jmorganca/ollama/auth" "github.com/jmorganca/ollama/gpu" "github.com/jmorganca/ollama/llm" "github.com/jmorganca/ollama/openai" @@ -480,7 +479,7 @@ func PullModelHandler(c *gin.Context) { ch <- r } - regOpts := &auth.RegistryOptions{ + regOpts := ®istryOptions{ Insecure: req.Insecure, } @@ -529,7 +528,7 @@ func PushModelHandler(c *gin.Context) { ch <- r } - regOpts := &auth.RegistryOptions{ + regOpts := ®istryOptions{ Insecure: req.Insecure, } diff --git a/server/upload.go b/server/upload.go index 525b27b8..eb3c325f 100644 --- a/server/upload.go +++ b/server/upload.go @@ -18,7 +18,6 @@ import ( "time" "github.com/jmorganca/ollama/api" - "github.com/jmorganca/ollama/auth" "github.com/jmorganca/ollama/format" "golang.org/x/sync/errgroup" ) @@ -50,7 +49,7 @@ const ( maxUploadPartSize int64 = 1000 * format.MegaByte ) -func (b *blobUpload) Prepare(ctx context.Context, requestURL *url.URL, opts *auth.RegistryOptions) error { +func (b *blobUpload) Prepare(ctx context.Context, requestURL *url.URL, opts *registryOptions) error { p, err := GetBlobsPath(b.Digest) if err != nil { return err @@ -122,7 +121,7 @@ func (b *blobUpload) Prepare(ctx context.Context, requestURL *url.URL, opts *aut // 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. -func (b *blobUpload) Run(ctx context.Context, opts *auth.RegistryOptions) { +func (b *blobUpload) Run(ctx context.Context, opts *registryOptions) { defer blobUploadManager.Delete(b.Digest) ctx, b.CancelFunc = context.WithCancel(ctx) @@ -213,7 +212,7 @@ func (b *blobUpload) Run(ctx context.Context, opts *auth.RegistryOptions) { b.done = true } -func (b *blobUpload) uploadPart(ctx context.Context, method string, requestURL *url.URL, part *blobUploadPart, opts *auth.RegistryOptions) error { +func (b *blobUpload) uploadPart(ctx context.Context, method string, requestURL *url.URL, part *blobUploadPart, opts *registryOptions) error { headers := make(http.Header) headers.Set("Content-Type", "application/octet-stream") headers.Set("Content-Length", fmt.Sprintf("%d", part.Size)) @@ -228,7 +227,7 @@ func (b *blobUpload) uploadPart(ctx context.Context, method string, requestURL * md5sum := md5.New() w := &progressWriter{blobUpload: b} - resp, err := auth.MakeRequest(ctx, method, requestURL, headers, io.TeeReader(sr, io.MultiWriter(w, md5sum)), opts) + resp, err := makeRequest(ctx, method, requestURL, headers, io.TeeReader(sr, io.MultiWriter(w, md5sum)), opts) if err != nil { w.Rollback() return err @@ -278,9 +277,8 @@ func (b *blobUpload) uploadPart(ctx context.Context, method string, requestURL * case resp.StatusCode == http.StatusUnauthorized: w.Rollback() - authenticate := resp.Header.Get("www-authenticate") - authRedir := ParseAuthRedirectString(authenticate) - token, err := auth.GetAuthToken(ctx, authRedir) + challenge := parseRegistryChallenge(resp.Header.Get("www-authenticate")) + token, err := getAuthorizationToken(ctx, challenge) if err != nil { return err } @@ -365,7 +363,7 @@ func (p *progressWriter) Rollback() { p.written = 0 } -func uploadBlob(ctx context.Context, mp ModelPath, layer *Layer, opts *auth.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 = requestURL.JoinPath("v2", mp.GetNamespaceRepository(), "blobs", layer.Digest) From 7ad9844ac0cf80cff9768f69fa306aa99b9357aa Mon Sep 17 00:00:00 2001 From: jmorganca Date: Thu, 15 Feb 2024 00:10:26 +0000 Subject: [PATCH 07/20] set exe metadata using resource files --- app/.gitignore | 1 + app/ollama.rc | 30 ++++++++++++++++++++++++++++++ scripts/build_windows.ps1 | 1 + 3 files changed, 32 insertions(+) create mode 100644 app/.gitignore create mode 100644 app/ollama.rc diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 00000000..0aa24794 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +ollama.syso diff --git a/app/ollama.rc b/app/ollama.rc new file mode 100644 index 00000000..18652428 --- /dev/null +++ b/app/ollama.rc @@ -0,0 +1,30 @@ +#include + +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 "CompanyName", "Ollama Inc." + 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 diff --git a/scripts/build_windows.ps1 b/scripts/build_windows.ps1 index 5da898ac..fae821e2 100644 --- a/scripts/build_windows.ps1 +++ b/scripts/build_windows.ps1 @@ -65,6 +65,7 @@ function buildOllama() { 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}") { From 622b1f3e67be1e1ef3a874b96063cd950738530c Mon Sep 17 00:00:00 2001 From: jmorganca Date: Thu, 15 Feb 2024 00:27:10 +0000 Subject: [PATCH 08/20] update installer and app.exe metadata --- app/ollama.iss | 2 +- app/ollama.rc | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/ollama.iss b/app/ollama.iss index 1f9c4a62..c06c2a41 100644 --- a/app/ollama.iss +++ b/app/ollama.iss @@ -11,7 +11,7 @@ #else #define MyAppVersion "0.0.0" #endif -#define MyAppPublisher "Ollama, Inc." +#define MyAppPublisher "Ollama" #define MyAppURL "https://ollama.com/" #define MyAppExeName "ollama app.exe" #define MyIcon ".\assets\app.ico" diff --git a/app/ollama.rc b/app/ollama.rc index 18652428..acd84493 100644 --- a/app/ollama.rc +++ b/app/ollama.rc @@ -15,7 +15,6 @@ BEGIN BEGIN BLOCK "040904b0" BEGIN - VALUE "CompanyName", "Ollama Inc." VALUE "FileDescription", "Ollama" VALUE "InternalName", "Ollama" VALUE "OriginalFilename", "ollama app.exe" From 57e60c836ff91fcaa4355b817cf0151018e4fcb9 Mon Sep 17 00:00:00 2001 From: Jeffrey Morgan Date: Wed, 14 Feb 2024 16:44:16 -0800 Subject: [PATCH 09/20] better windows app and tray icons --- app/assets/app.ico | Bin 6227 -> 7502 bytes app/assets/tray_upgrade.ico | Bin 23698 -> 23698 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/app/assets/app.ico b/app/assets/app.ico index 0c38e3d047e9e98441f961bc1c72075fb187a9e1..875924f28e3f0e63d3f1a9b42e7fc0864d7f7ee2 100644 GIT binary patch literal 7502 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 Xe!DrQxH|;C5d}2Vv{WmVQ4#+K%p*A% literal 6227 zcmcgxi9b}||Gr}w(;!n(W62WAzC;KymWYtama#z;GZ`*}alxc~qGkADdQ@Buea0B{~G#~B&w zupl@QV39@lme$>W)_*4)2Hvc^-Z=pPie6Vs-6Y`W@@pBt5v9SdlRc++pKx^5hBABpqKAQUlKa|9hErcszpzSd9P;z+COPDi<(<)G>ZIUT@V6Vk4l) z41~$96IjjiJs<%1&|Z^x>beqw%#1#jD%7X&AQ@ybQel?0*&t%GcJYZ+TGX9a-Q0s$+@_no<2?@r+9xr~2WxbVa;G4FG}K@pLQ@anWk6yk62v%v2@O4l%h z^^36I;rD)?ZcKPMV=YpEy3gO7W2cK*Y#>r9^DEuE3zi$G@BE}ozxWEIOr7h<%Nz8y zrp|9xFhZdV2dz#fU`2&2mLf?0g>0}N#F${<(nT3qckI7ljha=YagTaGl)cD{zfcwFZxV#WObsX)JFrh2K?!( z4q7YuY#jr@TmvR)xDanIdT|CQ8ZXReq=P>bhSOC*-Yx!|4MtBk*eN0P#!Yef{z7%+ ztr3$(_ZPWqcW89Xa0$`Un;)~`9mv|++Eh^p;cM@~wd8gR=0iJmBmps0-z|(}KdtpK z?0tq~rimois{ck0aHw?~!9|}9phW8H=xSaox64nmPdgW77}?^ec@Y`!`|JE z#fzp=oLzv>JfY}`)-aDPfnIVhT9RdwbL2EH)`fL@$-^M*aD;QD@wB;%GqaWyu16jq z_^*!Zmbfr2YpU3vYE$v58b*CHXNfS|6T2$rwu8uTZ*`r~^$f^v*WOT|RLVUxj`W7a zY2FZk-t3Rle@-Ie(pc16-|{uSsHR>y;eRsD@4VP5Fa~UKM-DviCsjV3;^!=D=;41@ zCjBa@m5P*e(GBS)ez8V<)*~mPsTe%dOClhLqAz2sS@BC+?rs>Y0_(GC@KLPqy4-M_fv1UTYGPwYs`J{)wYKoKja-ddPrKCadGhlH|K|3qa z6f0Bqmavl~aY~9KM|_@Qaj?(S7#jMaz)OOFe|8rh0hpVc$IjdvZHvz%2R45?I=W$2 z@_doTpYMHv7_R-R^wy3FDRBgK^1KO=L0TP`C};4r@cp_c_LL{L0M%Pi*Xv8;swj;v zW$Nk7EoaE_@qvd}(4KQurm6Wv)Uca8oE5zrq{4)@`4EndYw&4OOj7neUq)K@oK^k( zh<(3L_O)SR*u-S)_NRXzq9O_%m)|YMl`6QE{p#K1~y#As5Lkj<9b4lb{-{Z&&_e#%u9 zRaI4do~wNC&`+Z@!Bf+Ueq-8`Y(Lo#_Ts)2XGUrHn9RJ#`U=vTN0Y;9Op~ zZ~-1M>FN=w&K2f8X6O*miA=bJVH@aM=yy08raV3I2d8m$`D~2cjn{^Y!twdOMa1Wc z%*pT@Vx`^Q@gR^5BGg&${KgJ{Uh8>O{z9rL!FV3UliGH!&P@KQzFwXAcg`Wn?M$K^ zb6ll((3pnX+B4$w1m+BuQ85>m9K+O3-wBd-wlgpDn|@O2(Go0h~=LyWL(aXP(w716dhr60iXd85dU7^{P z(>+KY`ahc-y*yzf%;M)$BJ25dl0OlYPDIN0Y8`EZpxPev$5U9~KNmNF_J&&Co_$v_ z+@^`?cAgcIes`z!Br)u0@jh`R-~VM(RC02%@<`tB#z7Zv7+NH{)?;Et-1$`WGVDW8 zf}9SSi$BVhMi}b0fDahjX&l-#{ z5FE9p0>5q`C*E*mAWG!t`LUfj=jG}JBlLwoeQWoA6CCSks zn4+}3s<0K%;SX>a;*wMPdhk~G0b!M1gi?u1Nm&Q(+_HT~pVr+;;z>*L(Kq?>o*Z|M z@H%YEM~*HRCv^E2s{WWpqY-`6@BP}}e_28&AP{3mPWBEx5vEk0jv)8LJBiQhrGm#1 zuQcnbO6r|GN-e!Zii8otyK8L@T%M3gsEUfzBu*m*1shRvuHsN`?Q$uru#!wHZnu{r z>vVdD@%BA>I);KVc+>R87*!1+58&mYd5db2#l`J=d-dHWlL7=1ZVx|guS#HuS~+=8 z0s1O&?y}DG@Lw zp(i2;PpjJrl>?~)X@Yr~4?6luqw?$DphM_K%ja~$9!xTnRwOb6gx>*sSo^M!blf@l zWgE$J@$5!75H*cpK1_Vm?NF)UKO@#joU4hx1Dn*ls~CSt^5Bec0g^tv`e!t*9zT@^ zfeS{L6Se+E1bp4zCROXrVx25^&(br0U;%0h#c!XZB?sIxHC0qrPBT;fkI}U=WF4r^ ze}sxzI-HIBWyr`sjnM4807b?kDD%Oc-poE4jp@U%78HOn`n9pqOKNhkwX%wx30$?N zxIynT6H?inwz3bC+St(lt49yH*{$}c!B3)GHa-t+YAR*eeofo+>!gp<@$c?x^#t(kVn%~&)Vp}dG$zSL8>D`@%RP*#k@{yec zurh2IQ}&gWl@B)M7`Bgn4>h^Fn*H;^<_8BsrDkG_@uehfR7_n<%Ze}K+~(h)l>+X! zN2Y4C#Dl{<@HCGAju(_MdoX#n@WdT@JrxoluUoKpFQI&Xc}hEvF}xw)AX zYMJ`vZ%|U(ScC(gq5`nF*F<6lFPp2^E>5)H2zbq7Su4ofc;u%Jebk_M4LCc8eE!SS*8k5l-PG8y&0oJh?fd6L6s=!z5LBp%%8Ae94DeQg!zKLE>oEOsT@l9+ zga+8d*3Wetfh7Bx(*l{)@zGJm)k!>WoPzDueFdtI5@}z8Wm?#Q?-??ITYll{rP>8y zM~jyU`sFRHgAIdNnjLBo%<9N~EUu(4zNS_*OFw&mKfQfN%37?MC3u+8D0j}C>~t<6 zu>)<9zTMz%#m1l-dgyY<^)2vtU|^u{<#Fx~u^3^I{tvKKRGRK3;V2JD8tIPBg_a%; zB+nXq+OKy|w&o@_nFpuZK35F8l_sD{28tETuT#D4S1&PlC>a>z{JAAFZqK~`o?C~$ zzzVdx_T%zhMUR&INxdWFj4EJ4D4Lgtr}WUp!f%rfj1aAx69(F$1#C`L9A7H6UKW}q ztLR{&1a(v9uY=*oMayH{apJeqGy7o>?xsbFj+fOkHEf&l*3dyA0)y@9XJHm?gUe}e z$6a2{*AEE{)$ZR-cPx^(lFTq0?O=A?eHz?eR#%vF6et3qFzDGLT%e{rs+3kt%HEBoXI$Md}p4 z&y3s36*W0tv;X1fJNHff%jceqk$8#a<;olrA0Mngw2ZQI@n!m8$EV;^wp6pWtm1vD zZL~DG5kgzM#>2w{GL+)th(S&;ipI_atftbImnBp~_I->7zd6aiaIB)acoRcavsas6 zmgzcqg@v8;$E;4Y>Y!oU!OW?oCQLBR<*!?ZH#^uB`YcV)?>3^q{?5C-?&_zInEul zPa1a9S60(GId>_hO=m@bqHpe4{X<>hS>7YWU`^{WSO^|5(! zw_*)NqCY+e-wSqMzPR6{iEgKr-)|K3*!ztQeej8exrnZ7GKJB0@dL%%#?tq9$3-kKI+bQL22c%$;&Q?1?(T)F(d?sUcKd8+~wsrQXBy-6`%Y(sxiYIofM zQJ|I>EqybCCd}fTckqt1 zr%e+TW)Nmso{Y7_(biQSGBa$aP6y+|HdQ^y$htE+HfC}3jb=5qI&t$oJ8Pv)`Te7l zgO`3w(?)<#cI=wUb_NxO?s&B9Hz5Mf&a>V?64v6Vna2FR(SNn{c#wa_QQ)<|CFr6H zx?fJI?9c@}!Xn?Tw#XiY{BW`?{&s7xsz0= zDdYVp!#|PzJ+phsdGC_)-n}s_8l0c*Qk-_v$m%dxe$xw9<&a_a0~+$a4cwQ!^Ig}~ zn{8-7J*Po_gp-BOtE@l&#%;LT2TIifzN+^$|Ko1{pTFd*o2U8Oq?sQ5!M@j3mW$%0 z&AIgV{G~JM{?^!4xGZCtF>V$)6Jv9S3+_3vcFre z9Q*x+Yl8NcG;M`*8Q{`vGKA+^Dnwznt{3Wx4pl1B)-kQgSi?XzS5W2y*V%S$b?k2CT%}?D_qHYG(TMdH3))y61c(A}YyvN0y zFbq=Bw4>NxCz{NK*L<$gX+Dk_-CO+WVsA8!?d}ehM+Sf8*E+dB)1Qb&(nqcw`r@lO zQyM|%0lLU2PB0*_Ja@5T>K|1`C@TQ{ei1JC0VQ57s2b>?f z?8(jg1sP=*`#Cvb>iYT_i}YOKctegPE>;0J zolBYLlPj z0Y!g~$nvktgNmYzzAReM?nD-BxktP{g%4)OvMKztZs?JnhPs1nGJ3w zVjMi0ZM-?9H9&<65~k{x{up~+7BUtaZbROcKKvy{^BVb71d8@<{w6VL>gJEjG2C1? zF_VXfR#q&8y564kA5egx>>90h0#K(_`-OCf8lG`g+MlMl@jyR5neC&xRj0h_-PFI=!m%?RGxUMiYBtwGd$LZW*{_zhYw#hy2C21-cvrnU?=W*K|O)grJ8}9(@#RSBFawMyi~R*cC`c``?2#qH|YTtfMwAI zL1221PE@&;DLjh^Zi4tEm8_)`y`z$Up$Y zR-c}^1$NuX1h3epZ|Zbr<%6Z%5|tK-JuxwjuBwuCB~hgnAA}-ymmsl&o}dt$=Y{Br zq=i7nS5B=B15v5%=H}y}_8VNVtR}huC6+Z`1X81JuSop1aDf%YAcsi4E;rVNG1)pt@tkS!`Y((Rd zWe5Pd5N}t&_NCHCLe`T!Je(;RNvKx__# z&whu_6@YWdyp;!{5-x(yul@BM3!%+Grc?G)C{wiuEaxnHA%xEh+=3Z$=!e-Z4Q8Vj z&>-k#?bfl^Me&6T)Y2tqDR^jE=g{$e7g2Qx-UI-<_84)B73DU!-P-a;_L!t(*d*re z?$lHpsj2e}3^v(_%f!#Tg?!eEUVtQ|gQCXu`PYAA`T>CZDkTH)B@lu9V$o6>&h?sS zbS_x)-fe`rX*7taw5w?X_Aa}!|NOAa6d+Gx*SI zZVeHs%5#16{N8zyKN@%e?Q(3Ju)HR(ZTfzK}Jv%I_vpXQzWxZv#qmz1jv4NUR!cwyz8@AR! zGMPM-_5mJbgMO@*yD)BV2ySg{iJoV)N6OFhdN!Y#1)Lz z`0=3}4yMy-F%wfv>7%4$Th(z-ju`qF2zi-D?W~XaQjSQF6G?WP;u~+0A-$xd$Qhx~ zHzpE^hcI?fEn|>V@QPiBOF4<8O$)HfHp=KheCB!iyrrxV?<;=dX{~3x7(9`%7;mzR zT$gI_@;)7PFt(5?ZorfhuVFg=EG#UDk&zKGI5=qRZnqmdt|mOCcH+ILiaT8jZiJh@ zoK~HI)QbQ;A`j z40f`EpYbn%miNRnTq^r=IJqL#V70H%ndk>)CfGH6VYwh6Cn? z>rwtpw#ZoO^&(A=gqtnc?j>09&9*u zWAd=fq)kn5!{TwsVKUPj8DZQCn1|Lfjl~Wlq|WBcs8-ZQejw2_-xJC+mS-O`@?s@? z#vN^^y`SgJ&h6nldYuY>!13aj1dUK^ zCp6p$t>AX6OpaAz^TiV6UQJMQR_97v?gs4kB-^}*yRQck9I(~h|IddO%Oif{@<0$* U25j~=CJ`2VHc&e^Gw`eQFXt2o-2eap delta 3182 zcmb^zTTGi(xIYy71N}!^%IJ-@K(S>GS{S!&FiHj^%RoSuVHk;>h;DdEdGLXlkrG|v zZGrB4U>Y4UGfVaWgn)^dAd8W`Er<>h9vnJeyktWL$fm;ad_Np=XiC}CCa2%|&v!lF z^;{YiE{zI3Hw_C53#9Xv7ILjjM?SCAllKY|=M)Ns2kMnc!9`=rmG|Ew^QVkt*6!&>M{)kA?`Ou1ZPB}*^4$l8y$3I0|;^dk*)?a@PrV6-3}c~&kokiTXo zFI$qy=C~_s28FnML=<5Yr#Y1K=kzVG+dAAfE<&URGOk99XS%|J*XvZ zS!z;lR6%KMt8;E{jvU#aL9TgqTUMfBY@v6wswjGM2i%K2=yX4oPQI^C*}|h2{`~s- z);ID|JT=P!vq=(ks$6brrmUOEMEK~7k7Un1?EhdJFSQYZb_YCC<+^K+mCI3JeCR0t zhu&p4)Yl`DvzOu-(k+t9Mk|lc1cr*7Xp*_j~KBW@Y#DW7gPVRm$ zyJT8sFGA#9qUVf1U5bXF#sXb&1@5xKysdMF$2;OL^se)#44~E7Att`S-QC?S$C&XT z+==g<{6xsEw}X_B34vHmz8u4+GstZ|E}M4qb|Rj}5;I)`XGAjL1qU86H3j4gM*~DvRqCgRz3(%XT-ypXFnvXD-*}Z$1yiaAP~R>W4ZZPKT*fW z2?475cZEWLD|=Ih$M$vzmXVQ>zM7hvK9kARN0Dsx(7g}t6jgyor-y#S1zTTVAF4Pv2C9vIr^1;409Q7ESji)}aFaQM58Mzm3u=TnL>W@)d*Bte^Z zlev1eb6bLklV6=mWqK;8Z(oBuRbnmrEF2v>V}*pp!_LA%v>W}fSmK3?H4=Ph^)+H1 zV7&Oo5z5QAQ!ExNv``xUo8}x`w0mJKTY?!|Gv1-G%@x_#tcX!Q`?2=U%jWkO1H8+W zy%oMH5N#M|9?$Rp=spu1O3uK3!}k}S1ot#GVNvg*scnGNd5%8|nVZlan`VpT?QCrfZLKA8 zG=I%_&W2S;t39ktDu>F;GVkP7^rlu<0zWxgTO!F>dAot|i$x0|-krxSzj-XIEgQOu zWxMZZHn;5N3*LvJ`gq(<`wc%8gOn@5mxksRuh&a*bg^M=3zhM2^kLU)r(+g76@*^Nr2m5|fT{PoqrIo-v*Ez7(i;egcVX}6DPpYHMl>kFo#k87_!?%g9(-=`f1-BZC;rww4*7v=L6_&u7|aGKRZF&WQCL_Yf*Fn$?#91 ch`g6c6siD)L;)w8+(H0)no@^Hn`VW7029%fIRF3v From 76113742cfc13a55f371e3d1c999eb74246376ad Mon Sep 17 00:00:00 2001 From: jmorganca Date: Thu, 15 Feb 2024 05:53:48 +0000 Subject: [PATCH 10/20] update installer title --- app/ollama.iss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/ollama.iss b/app/ollama.iss index c06c2a41..97e647cc 100644 --- a/app/ollama.iss +++ b/app/ollama.iss @@ -118,9 +118,8 @@ Type: filesandordirs; Name: "{%USERPROFILE}\.ollama" ; NOTE: if the user has a custom OLLAMA_MODELS it will be preserved [Messages] -WizardReady=Welcome to Ollama Windows Preview +WizardReady=Ollama Windows Preview ReadyLabel1=%nLet's get you up and running with your own large language models. -;ReadyLabel2b=We'll be installing Ollama in your user account without requiring Admin permissions ;FinishedHeadingLabel=Run your first model ;FinishedLabel=%nRun this command in a PowerShell or cmd terminal.%n%n%n ollama run llama2 From ed5489a96ebb876f8eed93fc07b3710e48f6fe0f Mon Sep 17 00:00:00 2001 From: Jeffrey Morgan Date: Wed, 14 Feb 2024 22:55:03 -0800 Subject: [PATCH 11/20] higher resolution tray icons --- app/assets/tray.ico | Bin 22846 -> 91014 bytes app/assets/tray_upgrade.ico | Bin 23698 -> 92898 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/app/assets/tray.ico b/app/assets/tray.ico index c8f3e9f656c2902049ecc976ee44fd95d9a90638..e63616c5738a8803c6c9f04ccfd4ab00e22cc88d 100644 GIT binary patch literal 91014 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 delta 75 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 delta 63 zcmaEKm37iiMiT}GMh0dEK|v6$!0>^Efx(P{fk9Cb!Z#6OU{GUaU~qJV@GV>!7^1{C Iy2Ql*057x%!T Date: Thu, 15 Feb 2024 11:36:35 -0800 Subject: [PATCH 12/20] do not print update request headers --- app/lifecycle/updater.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lifecycle/updater.go b/app/lifecycle/updater.go index 47db53c5..456e6f9c 100644 --- a/app/lifecycle/updater.go +++ b/app/lifecycle/updater.go @@ -84,7 +84,7 @@ func IsNewReleaseAvailable(ctx context.Context) (bool, UpdateResponse) { 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(fmt.Sprintf("checking for available update at %s with headers %v", requestURL, req.Header)) + 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)) From e547378893b8b40c2cc7ad63131cbe34cc25fb89 Mon Sep 17 00:00:00 2001 From: Michael Yang Date: Thu, 15 Feb 2024 12:05:13 -0800 Subject: [PATCH 13/20] disable default debug --- app/main.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/main.go b/app/main.go index 57cfd72e..57d8b1c1 100644 --- a/app/main.go +++ b/app/main.go @@ -4,14 +4,9 @@ package main // go build -ldflags="-H windowsgui" . import ( - "os" - "github.com/jmorganca/ollama/app/lifecycle" ) func main() { - // TODO - remove as we end the early access phase - os.Setenv("OLLAMA_DEBUG", "1") // nolint:errcheck - lifecycle.Run() } From 4240b045e6cd354ecc69a047f6f3e95ddca581c2 Mon Sep 17 00:00:00 2001 From: Michael Yang Date: Thu, 15 Feb 2024 12:08:27 -0800 Subject: [PATCH 14/20] always enable view logs --- app/tray/wintray/menus.go | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/app/tray/wintray/menus.go b/app/tray/wintray/menus.go index efbb8e89..757d3409 100644 --- a/app/tray/wintray/menus.go +++ b/app/tray/wintray/menus.go @@ -21,14 +21,11 @@ const ( ) func (t *winTray) initMenus() error { - if debug := os.Getenv("OLLAMA_DEBUG"); debug != "" { - 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(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) From 86808f80a8435dc0ef2558273ba135d70fd546ee Mon Sep 17 00:00:00 2001 From: Michael Yang Date: Thu, 15 Feb 2024 12:09:11 -0800 Subject: [PATCH 15/20] remove unused import --- app/tray/wintray/menus.go | 1 - 1 file changed, 1 deletion(-) diff --git a/app/tray/wintray/menus.go b/app/tray/wintray/menus.go index 757d3409..40235d47 100644 --- a/app/tray/wintray/menus.go +++ b/app/tray/wintray/menus.go @@ -5,7 +5,6 @@ package wintray import ( "fmt" "log/slog" - "os" "unsafe" "golang.org/x/sys/windows" From 272e53a1f56dcefb1b69b94af3e575d2f5e84485 Mon Sep 17 00:00:00 2001 From: Daniel Hiltgen Date: Thu, 15 Feb 2024 08:25:40 -0800 Subject: [PATCH 16/20] Prepare to distribute standalone windows executable This will be useful for our automated test riggig, and may be useful for advanced users who want to "roll their own" system service --- scripts/build_windows.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/build_windows.ps1 b/scripts/build_windows.ps1 index fae821e2..10052590 100644 --- a/scripts/build_windows.ps1 +++ b/scripts/build_windows.ps1 @@ -60,6 +60,7 @@ function buildOllama() { /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() { From bb9de6037c721aa13c5fcc93f4f110172357248f Mon Sep 17 00:00:00 2001 From: Daniel Hiltgen Date: Thu, 15 Feb 2024 08:36:41 -0800 Subject: [PATCH 17/20] Prevent multiple installers running concurrently --- app/ollama.iss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/ollama.iss b/app/ollama.iss index 97e647cc..604def04 100644 --- a/app/ollama.iss +++ b/app/ollama.iss @@ -80,6 +80,8 @@ SignTool=MySignTool SignedUninstaller=yes #endif +SetupMutex=OllamaSetupMutex + [Languages] Name: "english"; MessagesFile: "compiler:Default.isl" @@ -120,6 +122,8 @@ Type: filesandordirs; Name: "{%USERPROFILE}\.ollama" [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 From 5208cf09b13a5630ce967e0bccd1821ac8d5309a Mon Sep 17 00:00:00 2001 From: Daniel Hiltgen Date: Thu, 15 Feb 2024 09:56:49 -0800 Subject: [PATCH 18/20] clean up some logging --- app/lifecycle/updater_windows.go | 3 ++- app/tray/wintray/eventloop.go | 5 ----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/app/lifecycle/updater_windows.go b/app/lifecycle/updater_windows.go index cc97f686..f26c43c9 100644 --- a/app/lifecycle/updater_windows.go +++ b/app/lifecycle/updater_windows.go @@ -48,7 +48,8 @@ func DoUpgrade(cancel context.CancelFunc, done chan int) error { if done != nil { <-done } else { - slog.Warn("XXX done chan was nil, not actually waiting") + // Shouldn't happen + slog.Warn("done chan was nil, not actually waiting") } slog.Debug(fmt.Sprintf("starting installer: %s %v", installerExe, installArgs)) diff --git a/app/tray/wintray/eventloop.go b/app/tray/wintray/eventloop.go index 958b7871..a0af9787 100644 --- a/app/tray/wintray/eventloop.go +++ b/app/tray/wintray/eventloop.go @@ -45,7 +45,6 @@ func nativeLoop() { case 0: return default: - // slog.Debug(fmt.Sprintf("XXX dispatching message from run loop 0x%x", m.Message)) pTranslateMessage.Call(uintptr(unsafe.Pointer(m))) //nolint:errcheck pDispatchMessage.Call(uintptr(unsafe.Pointer(m))) //nolint:errcheck @@ -66,11 +65,9 @@ func (t *winTray) wndProc(hWnd windows.Handle, message uint32, wParam, lParam ui WM_MOUSEMOVE = 0x0200 WM_LBUTTONDOWN = 0x0201 ) - // slog.Debug(fmt.Sprintf("XXX in wndProc: 0x%x", message)) switch message { case WM_COMMAND: menuItemId := int32(wParam) - // slog.Debug(fmt.Sprintf("XXX Menu Click: %d", menuItemId)) // https://docs.microsoft.com/en-us/windows/win32/menurc/wm-command#menus switch menuItemId { case quitMenuID: @@ -151,7 +148,6 @@ func (t *winTray) wndProc(hWnd windows.Handle, message uint32, wParam, lParam ui slog.Debug(fmt.Sprintf("unmanaged app message, lParm: 0x%x", lParam)) } case t.wmTaskbarCreated: // on explorer.exe restarts - slog.Debug("XXX got taskbar created event") t.muNID.Lock() err := t.nid.add() if err != nil { @@ -161,7 +157,6 @@ func (t *winTray) wndProc(hWnd windows.Handle, message uint32, wParam, lParam ui 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 - // slog.Debug(fmt.Sprintf("XXX default wndProc handler 0x%x", message)) lResult, _, _ = pDefWindowProc.Call( uintptr(hWnd), uintptr(message), From 1ba734de677331a398c77662376ddab4b5bfff8c Mon Sep 17 00:00:00 2001 From: Daniel Hiltgen Date: Thu, 15 Feb 2024 10:51:56 -0800 Subject: [PATCH 19/20] typo --- docs/windows.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/windows.md b/docs/windows.md index b43470c8..875a3dc0 100644 --- a/docs/windows.md +++ b/docs/windows.md @@ -14,7 +14,7 @@ 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 be often be helpful in dianosing the problem (see +Logs will often be helpful in dianosing the problem (see [Troubleshooting](#troubleshooting) below) ## System Requirements From 117369aa732cdb49eea7d13bc2f581c6cab9388f Mon Sep 17 00:00:00 2001 From: Daniel Hiltgen Date: Thu, 15 Feb 2024 14:58:29 -0800 Subject: [PATCH 20/20] Exit if we detect another copy of Ollama running --- app/lifecycle/lifecycle.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/lifecycle/lifecycle.go b/app/lifecycle/lifecycle.go index 1fa9c7a8..521ed74a 100644 --- a/app/lifecycle/lifecycle.go +++ b/app/lifecycle/lifecycle.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "log/slog" + "os" "github.com/jmorganca/ollama/app/store" "github.com/jmorganca/ollama/app/tray" @@ -58,8 +59,8 @@ func Run() { } if IsServerRunning(ctx) { - slog.Debug("XXX detected server already running") - // TODO - should we fail fast, try to kill it, or just ignore? + slog.Info("Detected another instance of ollama running, exiting") + os.Exit(1) } else { done, err = SpawnServer(ctx, CLIName) if err != nil {