Touch ID for cli install; server restarts
This commit is contained in:
parent
776e7bb5e4
commit
2d8125042a
@ -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;
|
|
||||||
}
|
|
@ -1,7 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
// #cgo CFLAGS: -x objective-c -Wno-deprecated-declarations
|
// #cgo CFLAGS: -x objective-c
|
||||||
// #cgo LDFLAGS: -framework Cocoa -framework LocalAuthentication
|
// #cgo LDFLAGS: -framework Cocoa -framework LocalAuthentication -framework ServiceManagement
|
||||||
// #include "app_darwin.h"
|
// #include "app_darwin.h"
|
||||||
import "C"
|
import "C"
|
||||||
import (
|
import (
|
||||||
@ -26,8 +26,19 @@ func run() {
|
|||||||
initLogging()
|
initLogging()
|
||||||
slog.Info("ollama macOS app started")
|
slog.Info("ollama macOS app started")
|
||||||
|
|
||||||
|
// Ask to move to applications directory
|
||||||
|
moving := C.askToMoveToApplications()
|
||||||
|
if moving {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
C.killOtherInstances()
|
C.killOtherInstances()
|
||||||
|
|
||||||
|
code := C.installSymlink()
|
||||||
|
if code != 0 {
|
||||||
|
slog.Error("Failed to install symlink")
|
||||||
|
}
|
||||||
|
|
||||||
exe, err := os.Executable()
|
exe, err := os.Executable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
|
#import <Cocoa/Cocoa.h>
|
||||||
|
|
||||||
|
@interface AppDelegate : NSObject <NSApplicationDelegate>
|
||||||
|
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification;
|
||||||
|
@end
|
||||||
|
|
||||||
void run();
|
void run();
|
||||||
void killOtherInstances();
|
void killOtherInstances();
|
||||||
int askToMoveToApplications();
|
bool askToMoveToApplications();
|
||||||
int createSymlinkWithAuthorization();
|
int createSymlinkWithAuthorization();
|
||||||
|
int installSymlink();
|
||||||
extern void Restart();
|
extern void Restart();
|
||||||
extern void Quit();
|
extern void Quit();
|
173
app/app_darwin.m
173
app/app_darwin.m
@ -1,7 +1,48 @@
|
|||||||
|
#import <AppKit/AppKit.h>
|
||||||
#import <Cocoa/Cocoa.h>
|
#import <Cocoa/Cocoa.h>
|
||||||
#import "AppDelegate.h"
|
#import <CoreServices/CoreServices.h>
|
||||||
|
#import <Security/Security.h>
|
||||||
|
#import <ServiceManagement/ServiceManagement.h>
|
||||||
#import "app_darwin.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() {
|
void run() {
|
||||||
@autoreleasepool {
|
@autoreleasepool {
|
||||||
[NSApplication sharedApplication];
|
[NSApplication sharedApplication];
|
||||||
@ -33,9 +74,139 @@ void killOtherInstances() {
|
|||||||
kill(app.processIdentifier, SIGTERM);
|
kill(app.processIdentifier, SIGTERM);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NSDate *startTime = [NSDate date];
|
||||||
for (NSRunningApplication *app in apps) {
|
for (NSRunningApplication *app in apps) {
|
||||||
while (!app.terminated) {
|
while (!app.terminated) {
|
||||||
|
if (-[startTime timeIntervalSinceNow] >= 5) {
|
||||||
|
kill(app.processIdentifier, SIGKILL);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
|
[[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;
|
||||||
|
}
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<key>CFBundleIconFile</key>
|
<key>CFBundleIconFile</key>
|
||||||
<string>icon.icns</string>
|
<string>icon.icns</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>ai.ollama.ollama</string>
|
<string>com.ollama.ollama</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
|
@ -7,41 +7,28 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ollama/ollama/api"
|
"github.com/ollama/ollama/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SpawnServer(ctx context.Context, command string) (chan int, error) {
|
func start(ctx context.Context, command string) (*exec.Cmd, 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, command)
|
cmd := getCmd(ctx, command)
|
||||||
stdout, err := cmd.StdoutPipe()
|
stdout, err := cmd.StdoutPipe()
|
||||||
if err != nil {
|
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()
|
stderr, err := cmd.StderrPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return done, fmt.Errorf("failed to spawn server stderr pipe: %w", err)
|
return nil, 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO - rotation
|
// TODO - rotation
|
||||||
logFile, err := os.OpenFile(ServerLogFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0755)
|
logFile, err := os.OpenFile(ServerLogFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0755)
|
||||||
if err != nil {
|
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() {
|
go func() {
|
||||||
defer logFile.Close()
|
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
|
// run the command and wait for it to finish
|
||||||
if err := cmd.Start(); err != nil {
|
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 {
|
if cmd.Process != nil {
|
||||||
slog.Info(fmt.Sprintf("started ollama server with pid %d", cmd.Process.Pid))
|
slog.Info(fmt.Sprintf("started ollama server with pid %d", cmd.Process.Pid))
|
||||||
}
|
}
|
||||||
slog.Info(fmt.Sprintf("ollama server logs %s", ServerLogFile))
|
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() {
|
go func() {
|
||||||
// Keep the server running unless we're shuttind down the app
|
// Keep the server running unless we're shuttind down the app
|
||||||
crashCount := 0
|
crashCount := 0
|
||||||
for {
|
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
|
cmd.Wait() //nolint:errcheck
|
||||||
stdin.Close()
|
|
||||||
var code int
|
var code int
|
||||||
if cmd.ProcessState != nil {
|
if cmd.ProcessState != nil {
|
||||||
code = cmd.ProcessState.ExitCode()
|
code = cmd.ProcessState.ExitCode()
|
||||||
@ -112,15 +118,12 @@ func SpawnServer(ctx context.Context, command string) (chan int, error) {
|
|||||||
default:
|
default:
|
||||||
crashCount++
|
crashCount++
|
||||||
slog.Warn(fmt.Sprintf("server crash %d - exit code %d - respawning", crashCount, code))
|
slog.Warn(fmt.Sprintf("server crash %d - exit code %d - respawning", crashCount, code))
|
||||||
time.Sleep(500 * time.Millisecond)
|
time.Sleep(500 * time.Millisecond * time.Duration(crashCount))
|
||||||
if err := cmd.Start(); err != nil {
|
break
|
||||||
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
|
return done, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user