app_darwin.m 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. #import <AppKit/AppKit.h>
  2. #import <Cocoa/Cocoa.h>
  3. #import <CoreServices/CoreServices.h>
  4. #import <Security/Security.h>
  5. #import <ServiceManagement/ServiceManagement.h>
  6. #import "app_darwin.h"
  7. @interface AppDelegate ()
  8. @property (strong, nonatomic) NSStatusItem *statusItem;
  9. @end
  10. @implementation AppDelegate
  11. - (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
  12. // show status menu
  13. NSMenu *menu = [[NSMenu alloc] init];
  14. NSMenuItem *aboutMenuItem = [[NSMenuItem alloc] initWithTitle:@"About Ollama" action:@selector(aboutOllama) keyEquivalent:@""];
  15. [aboutMenuItem setTarget:self];
  16. [menu addItem:aboutMenuItem];
  17. // Settings submenu
  18. NSMenu *settingsMenu = [[NSMenu alloc] initWithTitle:@"Settings"];
  19. // Submenu items
  20. NSMenuItem *chooseModelDirectoryItem = [[NSMenuItem alloc] initWithTitle:@"Choose model directory..." action:@selector(chooseModelDirectory) keyEquivalent:@""];
  21. [chooseModelDirectoryItem setTarget:self];
  22. [chooseModelDirectoryItem setEnabled:YES];
  23. [settingsMenu addItem:chooseModelDirectoryItem];
  24. NSMenuItem *exposeExternallyItem = [[NSMenuItem alloc] initWithTitle:@"Allow external connections" action:@selector(toggleExposeExternally:) keyEquivalent:@""];
  25. [exposeExternallyItem setTarget:self];
  26. [exposeExternallyItem setState:NSOffState]; // Set initial state to off
  27. [exposeExternallyItem setEnabled:YES];
  28. [settingsMenu addItem:exposeExternallyItem];
  29. NSMenuItem *allowCrossOriginItem = [[NSMenuItem alloc] initWithTitle:@"Allow browser requests" action:@selector(toggleCrossOrigin:) keyEquivalent:@""];
  30. [allowCrossOriginItem setTarget:self];
  31. [allowCrossOriginItem setState:NSOffState]; // Set initial state to off
  32. [allowCrossOriginItem setEnabled:YES];
  33. [settingsMenu addItem:allowCrossOriginItem];
  34. NSMenuItem *settingsMenuItem = [[NSMenuItem alloc] initWithTitle:@"Settings" action:nil keyEquivalent:@""];
  35. [settingsMenuItem setSubmenu:settingsMenu];
  36. [menu addItem:settingsMenuItem];
  37. [menu addItemWithTitle:@"Quit Ollama" action:@selector(quit) keyEquivalent:@"q"];
  38. self.statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength];
  39. [self.statusItem addObserver:self forKeyPath:@"button.effectiveAppearance" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionInitial context:nil];
  40. self.statusItem.menu = menu;
  41. [self showIcon];
  42. }
  43. - (void)aboutOllama {
  44. [[NSApplication sharedApplication] orderFrontStandardAboutPanel:nil];
  45. }
  46. - (void)toggleCrossOrigin:(id)sender {
  47. NSMenuItem *item = (NSMenuItem *)sender;
  48. if ([item state] == NSOffState) {
  49. // Do something when cross-origin requests are allowed
  50. [item setState:NSOnState];
  51. } else {
  52. // Do something when cross-origin requests are disallowed
  53. [item setState:NSOffState];
  54. }
  55. }
  56. - (void)toggleExposeExternally:(id)sender {
  57. NSMenuItem *item = (NSMenuItem *)sender;
  58. if ([item state] == NSOffState) {
  59. // Do something when Ollama is exposed externally
  60. [item setState:NSOnState];
  61. } else {
  62. // Do something when Ollama is not exposed externally
  63. [item setState:NSOffState];
  64. }
  65. }
  66. - (void)chooseModelDirectory {
  67. NSOpenPanel *openPanel = [NSOpenPanel openPanel];
  68. [openPanel setCanChooseFiles:NO];
  69. [openPanel setCanChooseDirectories:YES];
  70. [openPanel setAllowsMultipleSelection:NO];
  71. NSInteger result = [openPanel runModal];
  72. if (result == NSModalResponseOK) {
  73. NSURL *selectedDirectoryURL = [openPanel URLs].firstObject;
  74. // Do something with the selected directory URL
  75. }
  76. }
  77. -(void) showIcon {
  78. NSAppearance* appearance = self.statusItem.button.effectiveAppearance;
  79. NSString* appearanceName = (NSString*)(appearance.name);
  80. NSString* iconName = [[appearanceName lowercaseString] containsString:@"dark"] ? @"iconDark" : @"icon";
  81. NSImage* statusImage = [NSImage imageNamed:iconName];
  82. [statusImage setTemplate:YES];
  83. self.statusItem.button.image = statusImage;
  84. }
  85. -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
  86. [self showIcon];
  87. }
  88. - (void)quit {
  89. [NSApp stop:nil];
  90. }
  91. @end
  92. void run() {
  93. @autoreleasepool {
  94. [NSApplication sharedApplication];
  95. AppDelegate *appDelegate = [[AppDelegate alloc] init];
  96. [NSApp setDelegate:appDelegate];
  97. [NSApp run];
  98. }
  99. }
  100. // killOtherInstances kills all other instances of the app currently
  101. // running. This way we can ensure that only the most recently started
  102. // instance of Ollama is running
  103. void killOtherInstances() {
  104. pid_t pid = getpid();
  105. NSArray *all = [[NSWorkspace sharedWorkspace] runningApplications];
  106. NSMutableArray *apps = [NSMutableArray array];
  107. for (NSRunningApplication *app in all) {
  108. if ([app.bundleIdentifier isEqualToString:[[NSBundle mainBundle] bundleIdentifier]] ||
  109. [app.bundleIdentifier isEqualToString:@"ai.ollama.ollama"] ||
  110. [app.bundleIdentifier isEqualToString:@"com.electron.ollama"]) {
  111. if (app.processIdentifier != pid) {
  112. [apps addObject:app];
  113. }
  114. }
  115. }
  116. for (NSRunningApplication *app in apps) {
  117. kill(app.processIdentifier, SIGTERM);
  118. }
  119. NSDate *startTime = [NSDate date];
  120. for (NSRunningApplication *app in apps) {
  121. while (!app.terminated) {
  122. if (-[startTime timeIntervalSinceNow] >= 5) {
  123. kill(app.processIdentifier, SIGKILL);
  124. break;
  125. }
  126. [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
  127. }
  128. }
  129. }
  130. bool askToMoveToApplications() {
  131. NSString *bundlePath = [[NSBundle mainBundle] bundlePath];
  132. if ([bundlePath hasPrefix:@"/Applications"]) {
  133. return false;
  134. }
  135. NSAlert *alert = [[NSAlert alloc] init];
  136. [alert setMessageText:@"Move to Applications?"];
  137. [alert setInformativeText:@"Ollama works best when run from the Applications directory."];
  138. [alert addButtonWithTitle:@"Move to Applications"];
  139. [alert addButtonWithTitle:@"Don't move"];
  140. [NSApp activateIgnoringOtherApps:YES];
  141. if ([alert runModal] != NSAlertFirstButtonReturn) {
  142. return false;
  143. }
  144. // move to applications
  145. NSString *applicationsPath = @"/Applications";
  146. NSString *newPath = [applicationsPath stringByAppendingPathComponent:@"Ollama.app"];
  147. NSFileManager *fileManager = [NSFileManager defaultManager];
  148. // Check if the newPath already exists
  149. if ([fileManager fileExistsAtPath:newPath]) {
  150. NSError *removeError = nil;
  151. [fileManager removeItemAtPath:newPath error:&removeError];
  152. if (removeError) {
  153. NSLog(@"Error removing file at %@: %@", newPath, removeError);
  154. return false; // or handle the error
  155. }
  156. }
  157. NSError *moveError = nil;
  158. [fileManager moveItemAtPath:bundlePath toPath:newPath error:&moveError];
  159. if (moveError) {
  160. NSLog(@"Error moving file from %@ to %@: %@", bundlePath, newPath, moveError);
  161. return false;
  162. }
  163. NSLog(@"Opening %@", newPath);
  164. NSError *error = nil;
  165. NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
  166. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  167. [workspace launchApplicationAtURL:[NSURL fileURLWithPath:newPath]
  168. options:NSWorkspaceLaunchNewInstance | NSWorkspaceLaunchDefault
  169. configuration:@{}
  170. error:&error];
  171. return true;
  172. }
  173. int installSymlink() {
  174. NSString *linkPath = @"/usr/local/bin/ollama";
  175. NSError *error = nil;
  176. NSFileManager *fileManager = [NSFileManager defaultManager];
  177. NSString *symlinkPath = [fileManager destinationOfSymbolicLinkAtPath:linkPath error:&error];
  178. NSString *bundlePath = [[NSBundle mainBundle] bundlePath];
  179. NSString *execPath = [[NSBundle mainBundle] executablePath];
  180. NSString *resPath = [[NSBundle mainBundle] pathForResource:@"ollama" ofType:nil];
  181. // if the symlink already exists and points to the right place, don't prompt
  182. if ([symlinkPath isEqualToString:resPath]) {
  183. NSLog(@"symbolic link already exists and points to the right place");
  184. return 0;
  185. }
  186. NSString *authorizationPrompt = @"Ollama is trying to install its command line interface (CLI) tool.";
  187. AuthorizationRef auth = NULL;
  188. OSStatus createStatus = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, kAuthorizationFlagDefaults, &auth);
  189. if (createStatus != errAuthorizationSuccess) {
  190. NSLog(@"Error creating authorization");
  191. return -1;
  192. }
  193. NSString * bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
  194. NSString *rightNameString = [NSString stringWithFormat:@"%@.%@", bundleIdentifier, @"auth3"];
  195. const char *rightName = rightNameString.UTF8String;
  196. OSStatus getRightResult = AuthorizationRightGet(rightName, NULL);
  197. if (getRightResult == errAuthorizationDenied) {
  198. if (AuthorizationRightSet(auth, rightName, (__bridge CFTypeRef _Nonnull)(@(kAuthorizationRuleAuthenticateAsAdmin)), (__bridge CFStringRef _Nullable)(authorizationPrompt), NULL, NULL) != errAuthorizationSuccess) {
  199. NSLog(@"Failed to set right");
  200. return -1;
  201. }
  202. }
  203. AuthorizationItem right = { .name = rightName, .valueLength = 0, .value = NULL, .flags = 0 };
  204. AuthorizationRights rights = { .count = 1, .items = &right };
  205. AuthorizationFlags flags = (AuthorizationFlags)(kAuthorizationFlagExtendRights | kAuthorizationFlagInteractionAllowed);
  206. AuthorizationItem iconAuthorizationItem = {.name = kAuthorizationEnvironmentIcon, .valueLength = 0, .value = NULL, .flags = 0};
  207. AuthorizationEnvironment authorizationEnvironment = {.count = 0, .items = NULL};
  208. BOOL failedToUseSystemDomain = NO;
  209. OSStatus copyStatus = AuthorizationCopyRights(auth, &rights, &authorizationEnvironment, flags, NULL);
  210. if (copyStatus != errAuthorizationSuccess) {
  211. failedToUseSystemDomain = YES;
  212. if (copyStatus == errAuthorizationCanceled) {
  213. NSLog(@"User cancelled authorization");
  214. return -1;
  215. } else {
  216. NSLog(@"Failed copying system domain rights: %d", copyStatus);
  217. return -1;
  218. }
  219. }
  220. const char *toolPath = "/bin/ln";
  221. const char *args[] = {"-s", "-F", [resPath UTF8String], "/usr/local/bin/ollama", NULL};
  222. FILE *pipe = NULL;
  223. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  224. OSStatus status = AuthorizationExecuteWithPrivileges(auth, toolPath, kAuthorizationFlagDefaults, (char *const *)args, &pipe);
  225. if (status != errAuthorizationSuccess) {
  226. NSLog(@"Failed to create symlink");
  227. return -1;
  228. }
  229. AuthorizationFree(auth, kAuthorizationFlagDestroyRights);
  230. return 0;
  231. }