index.ts 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. import { spawn, ChildProcess } from 'child_process'
  2. import { app, autoUpdater, dialog, Tray, Menu, BrowserWindow, MenuItemConstructorOptions, nativeTheme } from 'electron'
  3. import Store from 'electron-store'
  4. import winston from 'winston'
  5. import 'winston-daily-rotate-file'
  6. import * as path from 'path'
  7. import { v4 as uuidv4 } from 'uuid'
  8. import { installed } from './install'
  9. require('@electron/remote/main').initialize()
  10. if (require('electron-squirrel-startup')) {
  11. app.quit()
  12. }
  13. const store = new Store()
  14. let welcomeWindow: BrowserWindow | null = null
  15. declare const MAIN_WINDOW_WEBPACK_ENTRY: string
  16. const logger = winston.createLogger({
  17. transports: [
  18. new winston.transports.Console(),
  19. new winston.transports.File({
  20. filename: path.join(app.getPath('home'), '.ollama', 'logs', 'server.log'),
  21. maxsize: 1024 * 1024 * 20,
  22. maxFiles: 5,
  23. }),
  24. ],
  25. format: winston.format.printf(info => info.message),
  26. })
  27. app.on('ready', () => {
  28. const gotTheLock = app.requestSingleInstanceLock()
  29. if (!gotTheLock) {
  30. app.exit(0)
  31. return
  32. }
  33. app.on('second-instance', () => {
  34. if (app.hasSingleInstanceLock()) {
  35. app.releaseSingleInstanceLock()
  36. }
  37. if (proc) {
  38. proc.off('exit', restart)
  39. proc.kill()
  40. }
  41. app.exit(0)
  42. })
  43. app.focus({ steal: true })
  44. init()
  45. })
  46. function firstRunWindow() {
  47. // Create the browser window.
  48. welcomeWindow = new BrowserWindow({
  49. width: 400,
  50. height: 500,
  51. frame: false,
  52. fullscreenable: false,
  53. resizable: false,
  54. movable: true,
  55. show: false,
  56. webPreferences: {
  57. nodeIntegration: true,
  58. contextIsolation: false,
  59. },
  60. })
  61. require('@electron/remote/main').enable(welcomeWindow.webContents)
  62. welcomeWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY)
  63. welcomeWindow.on('ready-to-show', () => welcomeWindow.show())
  64. welcomeWindow.on('closed', () => {
  65. if (process.platform === 'darwin') {
  66. app.dock.hide()
  67. }
  68. })
  69. }
  70. let tray: Tray | null = null
  71. let updateAvailable = false
  72. const assetPath = app.isPackaged ? process.resourcesPath : path.join(__dirname, '..', '..', 'assets')
  73. function trayIconPath() {
  74. return nativeTheme.shouldUseDarkColors
  75. ? updateAvailable
  76. ? path.join(assetPath, 'iconDarkUpdateTemplate.png')
  77. : path.join(assetPath, 'iconDarkTemplate.png')
  78. : updateAvailable
  79. ? path.join(assetPath, 'iconUpdateTemplate.png')
  80. : path.join(assetPath, 'iconTemplate.png')
  81. }
  82. function updateTrayIcon() {
  83. if (tray) {
  84. tray.setImage(trayIconPath())
  85. }
  86. }
  87. function updateTray() {
  88. const updateItems: MenuItemConstructorOptions[] = [
  89. { label: 'An update is available', enabled: false },
  90. {
  91. label: 'Restart to update',
  92. click: () => autoUpdater.quitAndInstall(),
  93. },
  94. { type: 'separator' },
  95. ]
  96. const menu = Menu.buildFromTemplate([
  97. ...(updateAvailable ? updateItems : []),
  98. { role: 'quit', label: 'Quit Ollama', accelerator: 'Command+Q' },
  99. ])
  100. if (!tray) {
  101. tray = new Tray(trayIconPath())
  102. }
  103. tray.setToolTip(updateAvailable ? 'An update is available' : 'Ollama')
  104. tray.setContextMenu(menu)
  105. tray.setImage(trayIconPath())
  106. nativeTheme.off('updated', updateTrayIcon)
  107. nativeTheme.on('updated', updateTrayIcon)
  108. }
  109. let proc: ChildProcess = null
  110. function server() {
  111. const binary = app.isPackaged
  112. ? path.join(process.resourcesPath, 'ollama')
  113. : path.resolve(process.cwd(), '..', 'ollama')
  114. proc = spawn(binary, ['serve'])
  115. proc.stdout.on('data', data => {
  116. logger.info(data.toString().trim())
  117. })
  118. proc.stderr.on('data', data => {
  119. logger.error(data.toString().trim())
  120. })
  121. proc.on('exit', restart)
  122. }
  123. function restart() {
  124. setTimeout(server, 1000)
  125. }
  126. app.on('before-quit', () => {
  127. if (proc) {
  128. proc.off('exit', restart)
  129. proc.kill('SIGINT') // send SIGINT signal to the server, which also stops any loaded llms
  130. }
  131. })
  132. const updateURL = `https://ollama.com/api/update?os=${process.platform}&arch=${
  133. process.arch
  134. }&version=${app.getVersion()}&id=${id()}`
  135. let latest = ''
  136. async function isNewReleaseAvailable() {
  137. try {
  138. const response = await fetch(updateURL)
  139. if (!response.ok) {
  140. return false
  141. }
  142. if (response.status === 204) {
  143. return false
  144. }
  145. const data = await response.json()
  146. const url = data?.url
  147. if (!url) {
  148. return false
  149. }
  150. if (latest === url) {
  151. return false
  152. }
  153. latest = url
  154. return true
  155. } catch (error) {
  156. logger.error(`update check failed - ${error}`)
  157. return false
  158. }
  159. }
  160. async function checkUpdate() {
  161. const available = await isNewReleaseAvailable()
  162. if (available) {
  163. logger.info('checking for update')
  164. autoUpdater.checkForUpdates()
  165. }
  166. }
  167. function init() {
  168. if (app.isPackaged) {
  169. checkUpdate()
  170. setInterval(() => {
  171. checkUpdate()
  172. }, 60 * 60 * 1000)
  173. }
  174. updateTray()
  175. if (process.platform === 'darwin') {
  176. if (app.isPackaged) {
  177. if (!app.isInApplicationsFolder()) {
  178. const chosen = dialog.showMessageBoxSync({
  179. type: 'question',
  180. buttons: ['Move to Applications', 'Do Not Move'],
  181. message: 'Ollama works best when run from the Applications directory.',
  182. defaultId: 0,
  183. cancelId: 1,
  184. })
  185. if (chosen === 0) {
  186. try {
  187. app.moveToApplicationsFolder({
  188. conflictHandler: conflictType => {
  189. if (conflictType === 'existsAndRunning') {
  190. dialog.showMessageBoxSync({
  191. type: 'info',
  192. message: 'Cannot move to Applications directory',
  193. detail:
  194. 'Another version of Ollama is currently running from your Applications directory. Close it first and try again.',
  195. })
  196. }
  197. return true
  198. },
  199. })
  200. return
  201. } catch (e) {
  202. logger.error(`[Move to Applications] Failed to move to applications folder - ${e.message}}`)
  203. }
  204. }
  205. }
  206. }
  207. }
  208. server()
  209. if (store.get('first-time-run') && installed()) {
  210. if (process.platform === 'darwin') {
  211. app.dock.hide()
  212. }
  213. app.setLoginItemSettings({ openAtLogin: app.getLoginItemSettings().openAtLogin })
  214. return
  215. }
  216. // This is the first run or the CLI is no longer installed
  217. app.setLoginItemSettings({ openAtLogin: true })
  218. firstRunWindow()
  219. }
  220. // Quit when all windows are closed, except on macOS. There, it's common
  221. // for applications and their menu bar to stay active until the user quits
  222. // explicitly with Cmd + Q.
  223. app.on('window-all-closed', () => {
  224. if (process.platform !== 'darwin') {
  225. app.quit()
  226. }
  227. })
  228. function id(): string {
  229. const id = store.get('id') as string
  230. if (id) {
  231. return id
  232. }
  233. const uuid = uuidv4()
  234. store.set('id', uuid)
  235. return uuid
  236. }
  237. autoUpdater.setFeedURL({ url: updateURL })
  238. autoUpdater.on('error', e => {
  239. logger.error(`update check failed - ${e.message}`)
  240. console.error(`update check failed - ${e.message}`)
  241. })
  242. autoUpdater.on('update-downloaded', () => {
  243. updateAvailable = true
  244. updateTray()
  245. })