Quellcode durchsuchen

Touch ID for cli install; server restarts

jmorganca vor 1 Jahr
Ursprung
Commit
2d8125042a
6 geänderte Dateien mit 225 neuen und 203 gelöschten Zeilen
  1. 0 170
      app/AppDelegate.m
  2. 13 2
      app/app_darwin.go
  3. 9 2
      app/app_darwin.h
  4. 172 1
      app/app_darwin.m
  5. 2 2
      app/darwin/Ollama.app/Contents/Info.plist
  6. 29 26
      app/server.go

+ 0 - 170
app/AppDelegate.m

@@ -1,170 +0,0 @@
-#import <CoreServices/CoreServices.h>
-#import <AppKit/AppKit.h>
-#import <Security/Security.h>
-#import "AppDelegate.h"
-#import "app_darwin.h"
-
-@interface AppDelegate () <NSToolbarDelegate>
-
-@property (strong, nonatomic) NSStatusItem *statusItem;
-@property (strong) NSWindow *settingsWindow;
-
-@end
-
-@implementation AppDelegate
-
-- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
-    // Ask to move to applications directory
-    askToMoveToApplications();
-
-    // Once in the desired directory, offer to create a symlink
-    // TODO (jmorganca): find a way to provide more context to the
-    // user about what this is doing, and ideally use Touch ID.
-    // or add an alias in the current shell environment,
-    // which wouldn't require any special privileges
-    // dispatch_async(dispatch_get_main_queue(), ^{
-    //     createSymlinkWithAuthorization();
-    // });
-
-    // show status menu
-    NSMenu *menu = [[NSMenu alloc] init];
-    [menu addItemWithTitle:@"Quit Ollama" action:@selector(quit) keyEquivalent:@"q"];
-    self.statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength];
-    [self.statusItem addObserver:self forKeyPath:@"button.effectiveAppearance" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionInitial context:nil];
-
-    self.statusItem.menu = menu;
-    [self showIcon];
-}
-
--(void) showIcon {
-    NSAppearance* appearance = self.statusItem.button.effectiveAppearance;
-    NSString* appearanceName = (NSString*)(appearance.name);
-    NSString* iconName = [[appearanceName lowercaseString] containsString:@"dark"] ? @"iconDark" : @"icon";
-    NSImage* statusImage = [NSImage imageNamed:iconName];
-    [statusImage setTemplate:YES];
-    self.statusItem.button.image = statusImage;
-}
-
--(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
-    [self showIcon];
-}
-
-- (void)openSettingsWindow {
-    if (!self.settingsWindow) {
-        // Create the settings window centered on the screen
-        self.settingsWindow = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 420, 460)
-                                                         styleMask:(NSWindowStyleMaskTitled | NSClosableWindowMask | NSWindowStyleMaskFullSizeContentView)
-                                                           backing:NSBackingStoreBuffered
-                                                             defer:NO];
-        [self.settingsWindow setTitle:@"Settings"];
-        [self.settingsWindow makeKeyAndOrderFront:nil];
-        [self.settingsWindow center];
-
-        // Create and configure the toolbar
-        NSToolbar *toolbar = [[NSToolbar alloc] initWithIdentifier:@"SettingsToolbar"];
-        toolbar.delegate = self;
-        // toolbar.showsBaselineSeparator
-        toolbar.displayMode = NSToolbarDisplayModeIconAndLabel;
-        self.settingsWindow.toolbar = toolbar;
-        self.settingsWindow.toolbarStyle = NSWindowToolbarStylePreference;
-
-        // Necessary to make the toolbar display immediately
-        [self.settingsWindow makeKeyAndOrderFront:nil];
-    } else {
-        [self.settingsWindow makeKeyAndOrderFront:nil];
-    }
-}
-
-- (void)quit {
-    [NSApp stop:nil];
-}
-
-@end
-
-int askToMoveToApplications() {
-    NSString *bundlePath = [[NSBundle mainBundle] bundlePath];
-    if ([bundlePath hasPrefix:@"/Applications"]) {
-        return 0;
-    }
-
-    NSAlert *alert = [[NSAlert alloc] init];
-    [alert setMessageText:@"Move to Applications?"];
-    [alert setInformativeText:@"Ollama works best when run from the Applications directory."];
-    [alert addButtonWithTitle:@"Move to Applications"];
-    [alert addButtonWithTitle:@"Don't move"];
-
-    [NSApp activateIgnoringOtherApps:YES];
-
-    if ([alert runModal] != NSAlertFirstButtonReturn) {
-        return 0;
-    }
-
-    // move to applications
-    NSString *applicationsPath = @"/Applications";
-    NSString *newPath = [applicationsPath stringByAppendingPathComponent:@"Ollama.app"];
-    NSFileManager *fileManager = [NSFileManager defaultManager];
-
-    // Check if the newPath already exists
-    if ([fileManager fileExistsAtPath:newPath]) {
-        NSError *removeError = nil;
-        [fileManager removeItemAtPath:newPath error:&removeError];
-        if (removeError) {
-            NSLog(@"Error removing file at %@: %@", newPath, removeError);
-            return -1; // or handle the error
-        }
-    }
-
-    NSError *moveError = nil;
-    [fileManager moveItemAtPath:bundlePath toPath:newPath error:&moveError];
-    if (moveError) {
-        NSLog(@"Error moving file from %@ to %@: %@", bundlePath, newPath, moveError);
-        return -1; // or handle the error
-    }
-
-    NSLog(@"Opening %@", newPath);
-    NSError *error = nil;
-    NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
-    [workspace launchApplicationAtURL:[NSURL fileURLWithPath:newPath]
-                                options:NSWorkspaceLaunchNewInstance | NSWorkspaceLaunchDefault
-                        configuration:@{}
-                                error:&error];
-    return 0;
-}
-
-int createSymlinkWithAuthorization() {
-    NSString *linkPath = @"/usr/local/bin/ollama";
-    NSError *error = nil;
-
-    NSFileManager *fileManager = [NSFileManager defaultManager];
-    NSString *symlinkPath = [fileManager destinationOfSymbolicLinkAtPath:linkPath error:&error];
-    NSString *bundlePath = [[NSBundle mainBundle] bundlePath];
-    NSString *execPath = [[NSBundle mainBundle] executablePath];
-    NSString *resPath = [[NSBundle mainBundle] pathForResource:@"ollama" ofType:nil];
-
-    // if the symlink already exists and points to the right place, don't prompt
-    if ([symlinkPath isEqualToString:resPath]) {
-        return 0;
-    }
-
-    OSStatus status;
-    AuthorizationRef authorizationRef;
-    status = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, kAuthorizationFlagDefaults, &authorizationRef);
-    if (status != errAuthorizationSuccess) {
-        NSLog(@"Failed to create authorization");
-        return -1;
-    }
-
-    const char *toolPath = "/bin/ln";
-    const char *args[] = {"-s", "-F", [resPath UTF8String], "/usr/local/bin/ollama", NULL};
-    FILE *pipe = NULL;
-
-    status = AuthorizationExecuteWithPrivileges(authorizationRef, toolPath, kAuthorizationFlagDefaults, (char *const *)args, &pipe);
-    if (status != errAuthorizationSuccess) {
-        NSLog(@"Failed to create symlink");
-        return -1;
-    }
-
-    AuthorizationFree(authorizationRef, kAuthorizationFlagDestroyRights);
-
-    return 0;
-}

+ 13 - 2
app/app_darwin.go

@@ -1,7 +1,7 @@
 package main
 
-// #cgo CFLAGS: -x objective-c -Wno-deprecated-declarations
-// #cgo LDFLAGS: -framework Cocoa -framework LocalAuthentication
+// #cgo CFLAGS: -x objective-c
+// #cgo LDFLAGS: -framework Cocoa -framework LocalAuthentication -framework ServiceManagement
 // #include "app_darwin.h"
 import "C"
 import (
@@ -26,8 +26,19 @@ func run() {
 	initLogging()
 	slog.Info("ollama macOS app started")
 
+	// Ask to move to applications directory
+	moving := C.askToMoveToApplications()
+	if moving {
+		return
+	}
+
 	C.killOtherInstances()
 
+	code := C.installSymlink()
+	if code != 0 {
+		slog.Error("Failed to install symlink")
+	}
+
 	exe, err := os.Executable()
 	if err != nil {
 		panic(err)

+ 9 - 2
app/app_darwin.h

@@ -1,6 +1,13 @@
+#import <Cocoa/Cocoa.h>
+
+@interface AppDelegate : NSObject <NSApplicationDelegate>
+- (void)applicationDidFinishLaunching:(NSNotification *)aNotification;
+@end
+
 void run();
 void killOtherInstances();
-int askToMoveToApplications();
+bool askToMoveToApplications();
 int createSymlinkWithAuthorization();
+int installSymlink();
 extern void Restart();
-extern void Quit();
+extern void Quit();

+ 172 - 1
app/app_darwin.m

@@ -1,7 +1,48 @@
+#import <AppKit/AppKit.h>
 #import <Cocoa/Cocoa.h>
-#import "AppDelegate.h"
+#import <CoreServices/CoreServices.h>
+#import <Security/Security.h>
+#import <ServiceManagement/ServiceManagement.h>
 #import "app_darwin.h"
 
+@interface AppDelegate ()
+
+@property (strong, nonatomic) NSStatusItem *statusItem;
+
+@end
+
+@implementation AppDelegate
+
+- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
+    // show status menu
+    NSMenu *menu = [[NSMenu alloc] init];
+    [menu addItemWithTitle:@"Quit Ollama" action:@selector(quit) keyEquivalent:@"q"];
+    self.statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength];
+    [self.statusItem addObserver:self forKeyPath:@"button.effectiveAppearance" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionInitial context:nil];
+
+    self.statusItem.menu = menu;
+    [self showIcon];
+}
+
+-(void) showIcon {
+    NSAppearance* appearance = self.statusItem.button.effectiveAppearance;
+    NSString* appearanceName = (NSString*)(appearance.name);
+    NSString* iconName = [[appearanceName lowercaseString] containsString:@"dark"] ? @"iconDark" : @"icon";
+    NSImage* statusImage = [NSImage imageNamed:iconName];
+    [statusImage setTemplate:YES];
+    self.statusItem.button.image = statusImage;
+}
+
+-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
+    [self showIcon];
+}
+
+- (void)quit {
+    [NSApp stop:nil];
+}
+
+@end
+
 void run() {
     @autoreleasepool {
         [NSApplication sharedApplication];
@@ -33,9 +74,139 @@ void killOtherInstances() {
         kill(app.processIdentifier, SIGTERM);
     }
 
+    NSDate *startTime = [NSDate date];
     for (NSRunningApplication *app in apps) {
         while (!app.terminated) {
+            if (-[startTime timeIntervalSinceNow] >= 5) {
+                kill(app.processIdentifier, SIGKILL);
+                break;
+            }
+
             [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
         }
     }
 }
+
+bool askToMoveToApplications() {
+    NSString *bundlePath = [[NSBundle mainBundle] bundlePath];
+    if ([bundlePath hasPrefix:@"/Applications"]) {
+        return false;
+    }
+
+    NSAlert *alert = [[NSAlert alloc] init];
+    [alert setMessageText:@"Move to Applications?"];
+    [alert setInformativeText:@"Ollama works best when run from the Applications directory."];
+    [alert addButtonWithTitle:@"Move to Applications"];
+    [alert addButtonWithTitle:@"Don't move"];
+
+    [NSApp activateIgnoringOtherApps:YES];
+
+    if ([alert runModal] != NSAlertFirstButtonReturn) {
+        return false;
+    }
+
+    // move to applications
+    NSString *applicationsPath = @"/Applications";
+    NSString *newPath = [applicationsPath stringByAppendingPathComponent:@"Ollama.app"];
+    NSFileManager *fileManager = [NSFileManager defaultManager];
+
+    // Check if the newPath already exists
+    if ([fileManager fileExistsAtPath:newPath]) {
+        NSError *removeError = nil;
+        [fileManager removeItemAtPath:newPath error:&removeError];
+        if (removeError) {
+            NSLog(@"Error removing file at %@: %@", newPath, removeError);
+            return false; // or handle the error
+        }
+    }
+
+    NSError *moveError = nil;
+    [fileManager moveItemAtPath:bundlePath toPath:newPath error:&moveError];
+    if (moveError) {
+        NSLog(@"Error moving file from %@ to %@: %@", bundlePath, newPath, moveError);
+        return false;
+    }
+
+    NSLog(@"Opening %@", newPath);
+    NSError *error = nil;
+    NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+    [workspace launchApplicationAtURL:[NSURL fileURLWithPath:newPath]
+               options:NSWorkspaceLaunchNewInstance | NSWorkspaceLaunchDefault
+               configuration:@{}
+               error:&error];
+
+    return true;
+}
+
+int installSymlink() {
+    NSString *linkPath = @"/usr/local/bin/ollama";
+    NSError *error = nil;
+
+    NSFileManager *fileManager = [NSFileManager defaultManager];
+    NSString *symlinkPath = [fileManager destinationOfSymbolicLinkAtPath:linkPath error:&error];
+    NSString *bundlePath = [[NSBundle mainBundle] bundlePath];
+    NSString *execPath = [[NSBundle mainBundle] executablePath];
+    NSString *resPath = [[NSBundle mainBundle] pathForResource:@"ollama" ofType:nil];
+
+    // if the symlink already exists and points to the right place, don't prompt
+    if ([symlinkPath isEqualToString:resPath]) {
+        NSLog(@"symbolic link already exists and points to the right place");
+        return 0;
+    }
+
+    NSString *authorizationPrompt = @"Ollama is trying to install its command line interface (CLI) tool.";
+
+    AuthorizationRef auth = NULL;
+    OSStatus createStatus = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, kAuthorizationFlagDefaults, &auth);
+    if (createStatus != errAuthorizationSuccess) {
+        NSLog(@"Error creating authorization");
+        return -1;
+    }
+
+    NSString * bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
+    NSString *rightNameString = [NSString stringWithFormat:@"%@.%@", bundleIdentifier, @"auth3"];
+    const char *rightName = rightNameString.UTF8String;
+
+    OSStatus getRightResult = AuthorizationRightGet(rightName, NULL);
+    if (getRightResult == errAuthorizationDenied) {
+        if (AuthorizationRightSet(auth, rightName, (__bridge CFTypeRef _Nonnull)(@(kAuthorizationRuleAuthenticateAsAdmin)), (__bridge CFStringRef _Nullable)(authorizationPrompt), NULL, NULL) != errAuthorizationSuccess) {
+            NSLog(@"Failed to set right");
+            return -1;
+        }
+    }
+
+    AuthorizationItem right = { .name = rightName, .valueLength = 0, .value = NULL, .flags = 0 };
+    AuthorizationRights rights = { .count = 1, .items = &right };
+    AuthorizationFlags flags = (AuthorizationFlags)(kAuthorizationFlagExtendRights | kAuthorizationFlagInteractionAllowed);
+    AuthorizationItem iconAuthorizationItem = {.name = kAuthorizationEnvironmentIcon, .valueLength = 0, .value = NULL, .flags = 0};
+    AuthorizationEnvironment authorizationEnvironment = {.count = 0, .items = NULL};
+
+    BOOL failedToUseSystemDomain = NO;
+    OSStatus copyStatus = AuthorizationCopyRights(auth, &rights, &authorizationEnvironment, flags, NULL);
+    if (copyStatus != errAuthorizationSuccess) {
+        failedToUseSystemDomain = YES;
+
+        if (copyStatus == errAuthorizationCanceled) {
+            NSLog(@"User cancelled authorization");
+            return -1;
+        } else {
+            NSLog(@"Failed copying system domain rights: %d", copyStatus);
+            return -1;
+        }
+    }
+
+    const char *toolPath = "/bin/ln";
+    const char *args[] = {"-s", "-F", [resPath UTF8String], "/usr/local/bin/ollama", NULL};
+    FILE *pipe = NULL;
+
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+    OSStatus status = AuthorizationExecuteWithPrivileges(auth, toolPath, kAuthorizationFlagDefaults, (char *const *)args, &pipe);
+    if (status != errAuthorizationSuccess) {
+        NSLog(@"Failed to create symlink");
+        return -1;
+    }
+
+    AuthorizationFree(auth, kAuthorizationFlagDestroyRights);
+    return 0;
+}

+ 2 - 2
app/darwin/Ollama.app/Contents/Info.plist

@@ -9,7 +9,7 @@
     <key>CFBundleIconFile</key>
     <string>icon.icns</string>
     <key>CFBundleIdentifier</key>
-    <string>ai.ollama.ollama</string>
+    <string>com.ollama.ollama</string>
     <key>CFBundleInfoDictionaryVersion</key>
     <string>6.0</string>
     <key>CFBundleName</key>
@@ -37,4 +37,4 @@
     <key>LSUIElement</key>
     <true/>
   </dict>
-</plist>
+</plist>

+ 29 - 26
app/server.go

@@ -7,41 +7,28 @@ import (
 	"io"
 	"log/slog"
 	"os"
+	"os/exec"
 	"path/filepath"
 	"time"
 
 	"github.com/ollama/ollama/api"
 )
 
-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)
-		}
-	}
-
+func start(ctx context.Context, command string) (*exec.Cmd, error) {
 	cmd := getCmd(ctx, command)
 	stdout, err := cmd.StdoutPipe()
 	if err != nil {
-		return done, fmt.Errorf("failed to spawn server stdout pipe: %w", err)
+		return nil, fmt.Errorf("failed to spawn server stdout pipe: %w", err)
 	}
 	stderr, err := cmd.StderrPipe()
 	if err != nil {
-		return done, fmt.Errorf("failed to spawn server stderr pipe: %w", err)
-	}
-	stdin, err := cmd.StdinPipe()
-	if err != nil {
-		return done, fmt.Errorf("failed to spawn server stdin pipe: %w", err)
+		return nil, fmt.Errorf("failed to spawn server stderr pipe: %w", 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)
+		return nil, fmt.Errorf("failed to create server log: %w", err)
 	}
 	go func() {
 		defer logFile.Close()
@@ -86,19 +73,38 @@ func SpawnServer(ctx context.Context, command string) (chan int, error) {
 
 	// 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)
+		return nil, 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))
 
+	return cmd, nil
+}
+
+func SpawnServer(ctx context.Context, command string) (chan int, error) {
+	logDir := filepath.Dir(ServerLogFile)
+	_, err := os.Stat(logDir)
+	if errors.Is(err, os.ErrNotExist) {
+		if err := os.MkdirAll(logDir, 0o755); err != nil {
+			return nil, fmt.Errorf("create ollama server log dir %s: %v", logDir, err)
+		}
+	}
+
+	done := make(chan int)
+
 	go func() {
 		// Keep the server running unless we're shuttind down the app
 		crashCount := 0
 		for {
+			slog.Info(fmt.Sprintf("starting server..."))
+			cmd, err := start(ctx, command)
+			if err != nil {
+				slog.Error(fmt.Sprintf("failed to start server %s", err))
+			}
+
 			cmd.Wait() //nolint:errcheck
-			stdin.Close()
 			var code int
 			if cmd.ProcessState != nil {
 				code = cmd.ProcessState.ExitCode()
@@ -112,15 +118,12 @@ func SpawnServer(ctx context.Context, command string) (chan int, error) {
 			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)
-				}
+				time.Sleep(500 * time.Millisecond * time.Duration(crashCount))
+				break
 			}
 		}
 	}()
+
 	return done, nil
 }