index.ts 6.86 KB
Newer Older
1
import { spawn, ChildProcess } from 'child_process'
2
import { app, autoUpdater, dialog, Tray, Menu, BrowserWindow, MenuItemConstructorOptions, nativeTheme } from 'electron'
Eva Ho's avatar
Eva Ho committed
3
import Store from 'electron-store'
Eva Ho's avatar
Eva Ho committed
4
5
import winston from 'winston'
import 'winston-daily-rotate-file'
Bruce MacDonald's avatar
Bruce MacDonald committed
6
import * as path from 'path'
Jeffrey Morgan's avatar
Jeffrey Morgan committed
7

8
import { v4 as uuidv4 } from 'uuid'
Jeffrey Morgan's avatar
Jeffrey Morgan committed
9
import { installed } from './install'
Jeffrey Morgan's avatar
Jeffrey Morgan committed
10

Jeffrey Morgan's avatar
Jeffrey Morgan committed
11
12
require('@electron/remote/main').initialize()

13
14
15
16
if (require('electron-squirrel-startup')) {
  app.quit()
}

Jeffrey Morgan's avatar
Jeffrey Morgan committed
17
const store = new Store()
18

19
20
21
let welcomeWindow: BrowserWindow | null = null

declare const MAIN_WINDOW_WEBPACK_ENTRY: string
22

23
24
25
26
27
28
29
30
31
const logger = winston.createLogger({
  transports: [
    new winston.transports.Console(),
    new winston.transports.File({
      filename: path.join(app.getPath('home'), '.ollama', 'logs', 'server.log'),
      maxsize: 1024 * 1024 * 20,
      maxFiles: 5,
    }),
  ],
Jeffrey Morgan's avatar
Jeffrey Morgan committed
32
  format: winston.format.printf(info => info.message),
Eva Ho's avatar
Eva Ho committed
33
34
})

35
36
37
38
app.on('ready', () => {
  const gotTheLock = app.requestSingleInstanceLock()
  if (!gotTheLock) {
    app.exit(0)
39
    return
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
  }

  app.on('second-instance', () => {
    if (app.hasSingleInstanceLock()) {
      app.releaseSingleInstanceLock()
    }

    if (proc) {
      proc.off('exit', restart)
      proc.kill()
    }

    app.exit(0)
  })

  app.focus({ steal: true })

  init()
})

60
61
62
63
64
65
66
67
function firstRunWindow() {
  // Create the browser window.
  welcomeWindow = new BrowserWindow({
    width: 400,
    height: 500,
    frame: false,
    fullscreenable: false,
    resizable: false,
68
69
    movable: true,
    show: false,
70
71
72
73
74
75
76
77
78
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: false,
    },
  })

  require('@electron/remote/main').enable(welcomeWindow.webContents)

  welcomeWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY)
79
  welcomeWindow.on('ready-to-show', () => welcomeWindow.show())
80
81
82
83
84
  welcomeWindow.on('closed', () => {
    if (process.platform === 'darwin') {
      app.dock.hide()
    }
  })
85
86
}

87
let tray: Tray | null = null
88
89
let updateAvailable = false
const assetPath = app.isPackaged ? process.resourcesPath : path.join(__dirname, '..', '..', 'assets')
Eva Ho's avatar
Eva Ho committed
90

91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
function trayIconPath() {
  return nativeTheme.shouldUseDarkColors
    ? updateAvailable
      ? path.join(assetPath, 'iconDarkUpdateTemplate.png')
      : path.join(assetPath, 'iconDarkTemplate.png')
    : updateAvailable
    ? path.join(assetPath, 'iconUpdateTemplate.png')
    : path.join(assetPath, 'iconTemplate.png')
}

function updateTrayIcon() {
  if (tray) {
    tray.setImage(trayIconPath())
  }
}

function updateTray() {
Jeffrey Morgan's avatar
Jeffrey Morgan committed
108
109
110
111
112
113
114
115
  const updateItems: MenuItemConstructorOptions[] = [
    { label: 'An update is available', enabled: false },
    {
      label: 'Restart to update',
      click: () => autoUpdater.quitAndInstall(),
    },
    { type: 'separator' },
  ]
116

117
  const menu = Menu.buildFromTemplate([
Jeffrey Morgan's avatar
Jeffrey Morgan committed
118
    ...(updateAvailable ? updateItems : []),
119
120
121
122
    { role: 'quit', label: 'Quit Ollama', accelerator: 'Command+Q' },
  ])

  if (!tray) {
123
    tray = new Tray(trayIconPath())
124
  }
Jeffrey Morgan's avatar
Jeffrey Morgan committed
125

126
127
  tray.setToolTip(updateAvailable ? 'An update is available' : 'Ollama')
  tray.setContextMenu(menu)
128
129
130
131
  tray.setImage(trayIconPath())

  nativeTheme.off('updated', updateTrayIcon)
  nativeTheme.on('updated', updateTrayIcon)
132
133
}

134
let proc: ChildProcess = null
Jeffrey Morgan's avatar
Jeffrey Morgan committed
135

Jeffrey Morgan's avatar
Jeffrey Morgan committed
136
137
function server() {
  const binary = app.isPackaged
Eva Ho's avatar
Eva Ho committed
138
139
    ? path.join(process.resourcesPath, 'ollama')
    : path.resolve(process.cwd(), '..', 'ollama')
Jeffrey Morgan's avatar
Jeffrey Morgan committed
140

141
  proc = spawn(binary, ['serve'])
Jeffrey Morgan's avatar
Jeffrey Morgan committed
142

Jeffrey Morgan's avatar
Jeffrey Morgan committed
143
  proc.stdout.on('data', data => {
Eva Ho's avatar
Eva Ho committed
144
145
    logger.info(data.toString().trim())
  })
Jeffrey Morgan's avatar
Jeffrey Morgan committed
146

Jeffrey Morgan's avatar
Jeffrey Morgan committed
147
  proc.stderr.on('data', data => {
Eva Ho's avatar
Eva Ho committed
148
149
    logger.error(data.toString().trim())
  })
150

Eva Ho's avatar
Eva Ho committed
151
  proc.on('exit', restart)
Jeffrey Morgan's avatar
Jeffrey Morgan committed
152
153
}

154
155
function restart() {
  setTimeout(server, 1000)
156
157
}

158
159
160
app.on('before-quit', () => {
  if (proc) {
    proc.off('exit', restart)
161
    proc.kill('SIGINT') // send SIGINT signal to the server, which also stops any loaded llms
162
163
164
  }
})

165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
let currentReleaseURL = ''

async function isNewReleaseAvailable() {
  try {
    const response = await fetch('https://ollama.ai/api/update')

    if (response.status === 204) {
      return false
    }

    const data = await response.json()

    if (currentReleaseURL === data.url) {
      return false
    }

    currentReleaseURL = data.url
    return true
  } catch (error) {
    logger.error(`update check failed - ${error}`)
    return false
  }
}

async function checkUpdate() {
  const available = await isNewReleaseAvailable()
  if (available) {
    logger.info('checking for update')
    autoUpdater.checkForUpdates()
  }
}

197
function init() {
198
  if (app.isPackaged) {
199
    checkUpdate()
200
    setInterval(() => {
201
      checkUpdate()
202
203
204
    }, 60 * 60 * 1000)
  }

205
  updateTray()
Jeffrey Morgan's avatar
Jeffrey Morgan committed
206

207
  if (process.platform === 'darwin') {
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
    if (app.isPackaged) {
      if (!app.isInApplicationsFolder()) {
        const chosen = dialog.showMessageBoxSync({
          type: 'question',
          buttons: ['Move to Applications', 'Do Not Move'],
          message: 'Ollama works best when run from the Applications directory.',
          defaultId: 0,
          cancelId: 1,
        })

        if (chosen === 0) {
          try {
            app.moveToApplicationsFolder({
              conflictHandler: conflictType => {
                if (conflictType === 'existsAndRunning') {
                  dialog.showMessageBoxSync({
                    type: 'info',
                    message: 'Cannot move to Applications directory',
                    detail:
                      'Another version of Ollama is currently running from your Applications directory. Close it first and try again.',
                  })
                }
                return true
              },
            })
            return
          } catch (e) {
Eva Ho's avatar
Eva Ho committed
235
            logger.error(`[Move to Applications] Failed to move to applications folder - ${e.message}}`)
236
          }
237
238
239
        }
      }
    }
240
  }
Jeffrey Morgan's avatar
Jeffrey Morgan committed
241

242
  server()
243

244
  if (store.get('first-time-run') && installed()) {
245
246
247
248
    if (process.platform === 'darwin') {
      app.dock.hide()
    }

249
    app.setLoginItemSettings({ openAtLogin: app.getLoginItemSettings().openAtLogin })
250
    return
251
  }
252
253
254
255

  // This is the first run or the CLI is no longer installed
  app.setLoginItemSettings({ openAtLogin: true })
  firstRunWindow()
256
}
Jeffrey Morgan's avatar
Jeffrey Morgan committed
257

Jeffrey Morgan's avatar
Jeffrey Morgan committed
258
259
260
261
262
263
264
265
266
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

267
268
function id(): string {
  const id = store.get('id') as string
Jeffrey Morgan's avatar
Jeffrey Morgan committed
269

270
271
272
  if (id) {
    return id
  }
273

274
275
276
  const uuid = uuidv4()
  store.set('id', uuid)
  return uuid
Jeffrey Morgan's avatar
Jeffrey Morgan committed
277
278
}

279
280
281
282
283
284
autoUpdater.setFeedURL({
  url: `https://ollama.ai/api/update?os=${process.platform}&arch=${
    process.arch
  }&version=${app.getVersion()}&id=${id()}`,
})

285
autoUpdater.on('error', e => {
286
  logger.error(`update check failed - ${e.message}`)
287
  console.error(`update check failed - ${e.message}`)
288
289
})

290
autoUpdater.on('update-downloaded', () => {
291
292
  updateAvailable = true
  updateTray()
Jeffrey Morgan's avatar
Jeffrey Morgan committed
293
})