index.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import { spawn } from 'child_process'
  2. import { app, autoUpdater, dialog, Tray, Menu, BrowserWindow, 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 { analytics, id } from './telemetry'
  8. import { installed } from './install'
  9. require('@electron/remote/main').initialize()
  10. const store = new Store()
  11. let tray: Tray | null = null
  12. let welcomeWindow: BrowserWindow | null = null
  13. declare const MAIN_WINDOW_WEBPACK_ENTRY: string
  14. const logger = winston.createLogger({
  15. transports: [
  16. new winston.transports.Console(),
  17. new winston.transports.File({
  18. filename: path.join(app.getPath('home'), '.ollama', 'logs', 'server.log'),
  19. maxsize: 1024 * 1024 * 20,
  20. maxFiles: 5,
  21. }),
  22. ],
  23. format: winston.format.printf(info => info.message),
  24. })
  25. const SingleInstanceLock = app.requestSingleInstanceLock()
  26. if (!SingleInstanceLock) {
  27. app.quit()
  28. }
  29. function firstRunWindow() {
  30. // Create the browser window.
  31. welcomeWindow = new BrowserWindow({
  32. width: 400,
  33. height: 500,
  34. frame: false,
  35. fullscreenable: false,
  36. resizable: false,
  37. movable: true,
  38. show: false,
  39. webPreferences: {
  40. nodeIntegration: true,
  41. contextIsolation: false,
  42. },
  43. alwaysOnTop: true,
  44. })
  45. require('@electron/remote/main').enable(welcomeWindow.webContents)
  46. // and load the index.html of the app.
  47. welcomeWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY)
  48. welcomeWindow.on('ready-to-show', () => welcomeWindow.show())
  49. // for debugging
  50. // welcomeWindow.webContents.openDevTools()
  51. if (process.platform === 'darwin') {
  52. app.dock.hide()
  53. }
  54. }
  55. function createSystemtray() {
  56. let iconPath = nativeTheme.shouldUseDarkColors
  57. ? path.join(__dirname, '..', '..', 'assets', 'ollama_icon_16x16Template.png')
  58. : path.join(__dirname, '..', '..', 'assets', 'ollama_outline_icon_16x16Template.png')
  59. if (app.isPackaged) {
  60. iconPath = nativeTheme.shouldUseDarkColors
  61. ? path.join(process.resourcesPath, 'ollama_icon_16x16Template.png')
  62. : path.join(process.resourcesPath, 'ollama_outline_icon_16x16Template.png')
  63. }
  64. tray = new Tray(iconPath)
  65. nativeTheme.on('updated', function theThemeHasChanged () {
  66. if (nativeTheme.shouldUseDarkColors) {
  67. app.isPackaged
  68. ? tray.setImage(path.join(process.resourcesPath, 'ollama_icon_16x16Template.png'))
  69. : tray.setImage(path.join(__dirname, '..', '..', 'assets', 'ollama_icon_16x16Template.png'))
  70. } else {
  71. app.isPackaged
  72. ? tray.setImage(path.join(process.resourcesPath, 'ollama_outline_icon_16x16Template.png'))
  73. : tray.setImage(path.join(__dirname, '..', '..', 'assets', 'ollama_outline_icon_16x16Template.png'))
  74. }
  75. })
  76. const contextMenu = Menu.buildFromTemplate([{ role: 'quit', label: 'Quit Ollama', accelerator: 'Command+Q' }])
  77. tray.setContextMenu(contextMenu)
  78. tray.setToolTip('Ollama')
  79. }
  80. if (require('electron-squirrel-startup')) {
  81. app.quit()
  82. }
  83. function server() {
  84. const binary = app.isPackaged
  85. ? path.join(process.resourcesPath, 'ollama')
  86. : path.resolve(process.cwd(), '..', 'ollama')
  87. const proc = spawn(binary, ['serve'])
  88. proc.stdout.on('data', data => {
  89. logger.info(data.toString().trim())
  90. })
  91. proc.stderr.on('data', data => {
  92. logger.error(data.toString().trim())
  93. })
  94. function restart() {
  95. logger.info('Restarting the server...')
  96. server()
  97. }
  98. proc.on('exit', restart)
  99. app.on('before-quit', () => {
  100. proc.off('exit', restart)
  101. proc.kill()
  102. })
  103. }
  104. if (process.platform === 'darwin') {
  105. app.dock.hide()
  106. }
  107. app.on('ready', () => {
  108. if (process.platform === 'darwin') {
  109. if (app.isPackaged) {
  110. if (!app.isInApplicationsFolder()) {
  111. const chosen = dialog.showMessageBoxSync({
  112. type: 'question',
  113. buttons: ['Move to Applications', 'Do Not Move'],
  114. message: 'Ollama works best when run from the Applications directory.',
  115. defaultId: 0,
  116. cancelId: 1,
  117. })
  118. if (chosen === 0) {
  119. try {
  120. app.moveToApplicationsFolder({
  121. conflictHandler: conflictType => {
  122. if (conflictType === 'existsAndRunning') {
  123. dialog.showMessageBoxSync({
  124. type: 'info',
  125. message: 'Cannot move to Applications directory',
  126. detail:
  127. 'Another version of Ollama is currently running from your Applications directory. Close it first and try again.',
  128. })
  129. }
  130. return true
  131. },
  132. })
  133. return
  134. } catch (e) {
  135. logger.error(`[Move to Applications] Failed to move to applications folder - ${e.message}}`)
  136. }
  137. }
  138. }
  139. }
  140. }
  141. createSystemtray()
  142. server()
  143. if (store.get('first-time-run') && installed()) {
  144. app.setLoginItemSettings({ openAtLogin: app.getLoginItemSettings().openAtLogin })
  145. return
  146. }
  147. // This is the first run or the CLI is no longer installed
  148. app.setLoginItemSettings({ openAtLogin: true })
  149. firstRunWindow()
  150. })
  151. // Quit when all windows are closed, except on macOS. There, it's common
  152. // for applications and their menu bar to stay active until the user quits
  153. // explicitly with Cmd + Q.
  154. app.on('window-all-closed', () => {
  155. if (process.platform !== 'darwin') {
  156. app.quit()
  157. }
  158. })
  159. // In this file you can include the rest of your app's specific main process
  160. // code. You can also put them in separate files and import them here.
  161. autoUpdater.setFeedURL({
  162. url: `https://ollama.ai/api/update?os=${process.platform}&arch=${process.arch}&version=${app.getVersion()}`,
  163. })
  164. async function heartbeat() {
  165. analytics.track({
  166. anonymousId: id(),
  167. event: 'heartbeat',
  168. properties: {
  169. version: app.getVersion(),
  170. },
  171. })
  172. }
  173. if (app.isPackaged) {
  174. heartbeat()
  175. autoUpdater.checkForUpdates()
  176. setInterval(() => {
  177. heartbeat()
  178. autoUpdater.checkForUpdates()
  179. }, 60 * 60 * 1000)
  180. }
  181. autoUpdater.on('error', e => {
  182. logger.error(`update check failed - ${e.message}`)
  183. })
  184. autoUpdater.on('update-downloaded', (event, releaseNotes, releaseName) => {
  185. dialog
  186. .showMessageBox({
  187. type: 'info',
  188. buttons: ['Restart Now', 'Later'],
  189. title: 'New update available',
  190. message: process.platform === 'win32' ? releaseNotes : releaseName,
  191. detail: 'A new version of Ollama is available. Restart to apply the update.',
  192. })
  193. .then(returnValue => {
  194. if (returnValue.response === 0) autoUpdater.quitAndInstall()
  195. })
  196. })