index.ts 5.61 KB
Newer Older
1
2
import { spawn } from 'child_process'
import { app, autoUpdater, dialog, Tray, Menu, BrowserWindow } 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

Jeffrey Morgan's avatar
Jeffrey Morgan committed
8
9
import { analytics, id } from './telemetry'

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

12

Jeffrey Morgan's avatar
Jeffrey Morgan committed
13
const store = new Store()
14
let tray: Tray | null = null
15
16
17
let welcomeWindow: BrowserWindow | null = null

declare const MAIN_WINDOW_WEBPACK_ENTRY: string
18

19
20
21
22
23
24
25
26
27
28
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,
    }),
  ],
  format: winston.format.printf(info => `${info.message}`),
Eva Ho's avatar
Eva Ho committed
29
30
})

Eva Ho's avatar
Eva Ho committed
31
const SingleInstanceLock = app.requestSingleInstanceLock()
32
33
34
if (!SingleInstanceLock) {
  app.quit()
}
Jeffrey Morgan's avatar
Jeffrey Morgan committed
35

36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

function firstRunWindow() {
  // Create the browser window.
  welcomeWindow = new BrowserWindow({
    width: 400,
    height: 500,
    frame: false,
    fullscreenable: false,
    resizable: false,
    movable: false,
    transparent: true,
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: false,
    },
  })

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

  // and load the index.html of the app.
  welcomeWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY)

  // for debugging
  // welcomeWindow.webContents.openDevTools()

  if (process.platform === 'darwin') {
    app.dock.hide()
  }  
}

function createSystemtray() {
67
  let iconPath = path.join(__dirname, '..', '..', 'assets', 'ollama_icon_16x16Template.png')
Eva Ho's avatar
Eva Ho committed
68
69

  if (app.isPackaged) {
70
    iconPath = path.join(process.resourcesPath, 'ollama_icon_16x16Template.png')
Eva Ho's avatar
Eva Ho committed
71
  }
Jeffrey Morgan's avatar
Jeffrey Morgan committed
72

73
  tray = new Tray(iconPath)
Jeffrey Morgan's avatar
Jeffrey Morgan committed
74

Jeffrey Morgan's avatar
Jeffrey Morgan committed
75
  const contextMenu = Menu.buildFromTemplate([{ role: 'quit', label: 'Quit Ollama', accelerator: 'Command+Q' }])
Jeffrey Morgan's avatar
Jeffrey Morgan committed
76

77
78
79
80
81
82
  tray.setContextMenu(contextMenu)
  tray.setToolTip('Ollama')
}

if (require('electron-squirrel-startup')) {
  app.quit()
Jeffrey Morgan's avatar
Jeffrey Morgan committed
83
84
}

Jeffrey Morgan's avatar
Jeffrey Morgan committed
85
86
function server() {
  const binary = app.isPackaged
87
88
    ? path.join(process.resourcesPath, 'ollama')
    : path.resolve(process.cwd(), '..', 'ollama')
Jeffrey Morgan's avatar
Jeffrey Morgan committed
89

Jeffrey Morgan's avatar
Jeffrey Morgan committed
90
91
  const proc = spawn(binary, ['serve'])

Jeffrey Morgan's avatar
Jeffrey Morgan committed
92
  proc.stdout.on('data', data => {
93
    logger.info(data.toString().trim())
Jeffrey Morgan's avatar
Jeffrey Morgan committed
94
  })
Jeffrey Morgan's avatar
Jeffrey Morgan committed
95

Jeffrey Morgan's avatar
Jeffrey Morgan committed
96
  proc.stderr.on('data', data => {
97
    logger.error(data.toString().trim())
Jeffrey Morgan's avatar
Jeffrey Morgan committed
98
99
  })

100
  proc.on('exit', () => {
101
102
    logger.info('Restarting the server...')
    server()
103
104
105
  })

  proc.on('disconnect', () => {
106
107
    logger.info('Server disconnected. Reconnecting...')
    server()
108
109
  })

Jeffrey Morgan's avatar
Jeffrey Morgan committed
110
111
112
113
114
  process.on('exit', () => {
    proc.kill()
  })
}

115
116
if (process.platform === 'darwin') {
  app.dock.hide()
117
118
}

119
120
app.on('ready', () => {
  if (process.platform === 'darwin') {
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
    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
148
            logger.error(`[Move to Applications] Failed to move to applications folder - ${e.message}}`)
149
          }
150
151
152
        }
      }
    }
153
  }
Jeffrey Morgan's avatar
Jeffrey Morgan committed
154

155
  createSystemtray()
156
  server()
157
158
159
160
161
162
163
164
165
166
  
  if (!store.has('first-time-run')) {
    // This is the first run
    app.setLoginItemSettings({ openAtLogin: true })
    firstRunWindow()
    store.set('first-time-run', false)
  } else {
    // The app has been run before
    app.setLoginItemSettings({ openAtLogin: app.getLoginItemSettings().openAtLogin })
  }
167
})
Jeffrey Morgan's avatar
Jeffrey Morgan committed
168

Jeffrey Morgan's avatar
Jeffrey Morgan committed
169
170
171
172
173
174
175
176
177
178
179
// 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()
  }
})

// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and import them here.
Jeffrey Morgan's avatar
Jeffrey Morgan committed
180
181
182
autoUpdater.setFeedURL({
  url: `https://ollama.ai/api/update?os=${process.platform}&arch=${process.arch}&version=${app.getVersion()}`,
})
183

Jeffrey Morgan's avatar
Jeffrey Morgan committed
184
185
186
187
async function heartbeat() {
  analytics.track({
    anonymousId: id(),
    event: 'heartbeat',
Jeffrey Morgan's avatar
Jeffrey Morgan committed
188
189
190
    properties: {
      version: app.getVersion(),
    },
Jeffrey Morgan's avatar
Jeffrey Morgan committed
191
192
193
  })
}

194
if (app.isPackaged) {
Jeffrey Morgan's avatar
Jeffrey Morgan committed
195
  heartbeat()
196
  autoUpdater.checkForUpdates()
197
  setInterval(() => {
Jeffrey Morgan's avatar
Jeffrey Morgan committed
198
    heartbeat()
199
    autoUpdater.checkForUpdates()
Jeffrey Morgan's avatar
Jeffrey Morgan committed
200
  }, 60 * 60 * 1000)
201
}
Jeffrey Morgan's avatar
Jeffrey Morgan committed
202

203
autoUpdater.on('error', e => {
Jeffrey Morgan's avatar
Jeffrey Morgan committed
204
  logger.error(`update check failed - ${e.message}`)
205
206
})

Jeffrey Morgan's avatar
Jeffrey Morgan committed
207
208
209
210
211
212
213
214
215
216
217
218
219
autoUpdater.on('update-downloaded', (event, releaseNotes, releaseName) => {
  dialog
    .showMessageBox({
      type: 'info',
      buttons: ['Restart Now', 'Later'],
      title: 'New update available',
      message: process.platform === 'win32' ? releaseNotes : releaseName,
      detail: 'A new version of Ollama is available. Restart to apply the update.',
    })
    .then(returnValue => {
      if (returnValue.response === 0) autoUpdater.quitAndInstall()
    })
})