diff --git a/hostd/electron-src/daemon.ts b/hostd/electron-src/daemon.ts index a4cb3e3..a6ae1ed 100644 --- a/hostd/electron-src/daemon.ts +++ b/hostd/electron-src/daemon.ts @@ -2,6 +2,7 @@ import { spawn } from 'child_process' import { state } from './state' import { getBinaryFilePath, getConfig, getConfigFilePath } from './config' import axios from 'axios' +import { Octokit } from '@octokit/rest' export function startDaemon(): Promise { return new Promise(async (resolve, reject) => { @@ -10,29 +11,29 @@ export function startDaemon(): Promise { try { const config = getConfig() const binaryFilePath = getBinaryFilePath() - state.process = spawn(binaryFilePath, ['-env'], { + state.daemon = spawn(binaryFilePath, ['-env'], { env: { ...process.env, HOSTD_CONFIG_FILE: getConfigFilePath() }, cwd: config.directory, }) - state.process.stdout?.on('data', (data) => { + state.daemon.stdout?.on('data', (data) => { console.log(`stdout: ${data}`) // Emit events or log data as needed }) - state.process.stderr?.on('data', (data) => { + state.daemon.stderr?.on('data', (data) => { console.error(`stderr: ${data}`) // Emit events or log data as needed }) - state.process.on('close', (code) => { + state.daemon.on('close', (code) => { console.log(`child process exited with code ${code}`) - state.process = null + state.daemon = null }) resolve() } catch (err) { - state.process = null + state.daemon = null reject(err) } }) @@ -40,22 +41,22 @@ export function startDaemon(): Promise { export function stopDaemon(): Promise { return new Promise((resolve) => { - if (!state.process) { + if (!state.daemon) { resolve() return } - state.process.on('close', () => { - state.process = null + state.daemon.on('close', () => { + state.daemon = null resolve() }) - state.process.kill('SIGINT') + state.daemon.kill('SIGINT') }) } export function getIsDaemonRunning(): boolean { - return !!state.process && !state.process.killed + return !!state.daemon && !state.daemon.killed } export async function getInstalledVersion(): Promise { @@ -87,3 +88,17 @@ export async function getInstalledVersion(): Promise { return '' } } + +export async function getLatestVersion(): Promise { + try { + const octokit = new Octokit() + const response = await octokit.repos.getLatestRelease({ + owner: 'SiaFoundation', + repo: 'hostd', + }) + return response.data.tag_name + } catch (err) { + console.error(err) + return '' + } +} diff --git a/hostd/electron-src/download.ts b/hostd/electron-src/download.ts index 9f9d74b..ebbebc0 100644 --- a/hostd/electron-src/download.ts +++ b/hostd/electron-src/download.ts @@ -6,20 +6,7 @@ import { getBinaryFilePath, getConfigAndBinaryDirectoryPath } from './config' import { promisify } from 'util' import stream from 'stream' import axios from 'axios' - -export async function getLatestVersion(): Promise { - try { - const octokit = new Octokit() - const response = await octokit.repos.getLatestRelease({ - owner: 'SiaFoundation', - repo: 'hostd', - }) - return response.data.tag_name - } catch (err) { - console.error(err) - return '' - } -} +import { system } from './state' export async function downloadRelease(): Promise { try { @@ -95,11 +82,11 @@ function getTempDownloadsPath(): string { } function getBinaryZipStagingPath(): string { - const binaryName = process.platform === 'win32' ? `hostd.exe` : `hostd` + const binaryName = system.isWindows ? `hostd.exe` : `hostd` return path.join(getTempDownloadsPath(), binaryName + '.zip') } -function releaseAsset(): string { +function releaseAsset() { let goos switch (process.platform) { case 'win32': @@ -111,28 +98,26 @@ function releaseAsset(): string { case 'linux': goos = 'linux' break - // Add additional mappings as needed default: throw new Error(`Unsupported platform: ${process.platform}`) } let goarch - switch (process.arch) { - case 'x64': - goarch = 'amd64' - break - case 'ia32': - goarch = '386' - break - case 'arm': - goarch = 'arm' - break - case 'arm64': - goarch = 'arm64' - break - // Add additional mappings as needed - default: - throw new Error(`Unsupported architecture: ${process.arch}`) + if (process.platform === 'win32') { + // Windows only supports amd64 + goarch = 'amd64' + } else { + // For Darwin and Linux, consider both amd64 and arm64 + switch (process.arch) { + case 'x64': + goarch = 'amd64' + break + case 'arm64': + goarch = 'arm64' + break + default: + throw new Error(`Unsupported architecture: ${process.arch}`) + } } return `hostd_${goos}_${goarch}.zip` diff --git a/hostd/electron-src/index.ts b/hostd/electron-src/index.ts index 3b7373b..2efa8d4 100644 --- a/hostd/electron-src/index.ts +++ b/hostd/electron-src/index.ts @@ -1,152 +1,36 @@ -import path, { join } from 'path' -import fs from 'fs' -import { - BrowserWindow, - app, - ipcMain, - shell, - Tray, - Menu, - globalShortcut, -} from 'electron' -import isDev from 'electron-is-dev' +import { app, globalShortcut } from 'electron' import prepareNext from 'electron-next' -import { - getInstalledVersion, - getIsDaemonRunning, - startDaemon, - stopDaemon, -} from './daemon' -import { downloadRelease, getLatestVersion } from './download' -import { - Config, - doesBinaryExist, - getConfig, - getDefaultDataPath, - getIsConfigured, - saveConfig, -} from './config' -import { format } from 'url' - -let mainWindow: BrowserWindow | null = null -let appIcon = null -const isDarwin = process.platform === 'darwin' -const isLinux = process.platform === 'linux' -let isQuitting = false -// const isWindows = process.platform === 'win32' +import { startDaemon, stopDaemon } from './daemon' +import { downloadRelease } from './download' +import { doesBinaryExist, getIsConfigured } from './config' +import { initTray } from './tray' +import { state } from './state' +import { initWindow } from './window' +import { initShortcuts } from './shortcuts' +import { initIpc } from './ipc' app.on('ready', async () => { await prepareNext('./renderer') + initWindow() + initTray() + initShortcuts() + initIpc() - mainWindow = new BrowserWindow({ - width: 500, - height: 700, - minWidth: 500, - minHeight: 600, - maxWidth: 500, - maxHeight: 800, - webPreferences: { - nodeIntegration: false, - contextIsolation: true, - preload: join(__dirname, 'preload.js'), - }, - }) - - // Hide the main window instead of closing it, to keep the app running in the tray - mainWindow.on('close', (event) => { - if (!isQuitting) { - event.preventDefault() - if (isDarwin) { - app.dock.hide() - } - mainWindow?.hide() - } - }) - - // Setup close to tray settings for both minimize and close events - mainWindow.on('minimize', () => { - if (isDarwin) { - app.dock.hide() - } - // TODO: Does Linux support tray? - // https://electronjs.org/docs/api/tray - // minimize instead of attempting to go to system tray. - if (isLinux) { - return true - } - mainWindow?.hide() - return false - }) - - const iconName = isDarwin ? 'tray.png' : 'tray-win.png' - const iconPath = isDev - ? path.join(process.cwd(), 'assets', iconName) - : path.join(__dirname, '../assets', iconName) - - appIcon = new Tray(iconPath) - const trayContextMenu = Menu.buildFromTemplate([ - { - label: 'Configure', - click: function () { - if (isDarwin) { - app.dock.show() - } - mainWindow?.show() - }, - }, - { - label: 'Quit', - click: function () { - app.quit() - }, - }, - ]) - appIcon.setToolTip('hostd') - appIcon.setContextMenu(trayContextMenu) - const url = isDev - ? 'http://localhost:8000/' - : format({ - pathname: path.join(__dirname, '../renderer/out/index.html'), - protocol: 'file:', - slashes: true, - }) - mainWindow.loadURL(url) - - if (isDev) { - mainWindow.setMaximumSize(2000, 2000) - mainWindow.setSize(1000, 800) - mainWindow.webContents.openDevTools() - } - - const needsDownload = !doesBinaryExist() - if (needsDownload) { + const needsInitialDownload = !doesBinaryExist() + if (needsInitialDownload) { await downloadRelease() } + // If the app is already configured, start the daemon and open browser + // and do not show the configuration window. if (getIsConfigured()) { startDaemon() + state.mainWindow?.close() } - // Register a global shortcut listener for the developer tools - const devToolsShortcut = - process.platform === 'darwin' ? 'Cmd+Alt+I' : 'Ctrl+Shift+I' - globalShortcut.register(devToolsShortcut, () => { - // Open the DevTools - if (mainWindow && mainWindow.webContents) { - mainWindow.webContents.openDevTools() - mainWindow.setSize(1000, 800) - mainWindow.setMaximumSize(2000, 2000) - } - }) }) -async function quitDaemonAndApp() { - isQuitting = true - await stopDaemon() - mainWindow = null -} - app.on('before-quit', async () => { - if (!isQuitting) { + if (!state.isQuitting) { await quitDaemonAndApp() } }) @@ -160,41 +44,8 @@ app.on('will-quit', async () => { // even though closing windows is not really possible app.on('window-all-closed', app.quit) -ipcMain.handle('open-browser', (_, url: string) => { - shell.openExternal(url) -}) -ipcMain.handle('daemon-start', async (_) => { - await startDaemon() -}) -ipcMain.handle('daemon-stop', async (_) => { +async function quitDaemonAndApp() { + state.isQuitting = true await stopDaemon() -}) -ipcMain.handle('daemon-is-running', (_) => { - const isDaemonRunning = getIsDaemonRunning() - return isDaemonRunning -}) -ipcMain.handle('config-get', (_) => { - const config = getConfig() - return config -}) -ipcMain.handle('get-is-configured', (_) => { - return getIsConfigured() -}) -ipcMain.handle('open-data-directory', (_) => { - shell.openPath(getDefaultDataPath()) - return true -}) -ipcMain.handle('get-default-data-directory', async (_) => { - await fs.promises.mkdir(getDefaultDataPath(), { recursive: true }) - return getDefaultDataPath() -}) -ipcMain.handle('get-installed-version', (_) => { - return getInstalledVersion() -}) -ipcMain.handle('get-latest-version', (_) => { - return getLatestVersion() -}) -ipcMain.handle('config-save', async (_, config: Config) => { - await saveConfig(config) - return true -}) + state.mainWindow = null +} diff --git a/hostd/electron-src/ipc.ts b/hostd/electron-src/ipc.ts new file mode 100644 index 0000000..f346296 --- /dev/null +++ b/hostd/electron-src/ipc.ts @@ -0,0 +1,57 @@ +import fs from 'fs' +import { ipcMain, shell } from 'electron' +import { + getInstalledVersion, + getIsDaemonRunning, + getLatestVersion, + startDaemon, + stopDaemon, +} from './daemon' +import { + Config, + getConfig, + getDefaultDataPath, + getIsConfigured, + saveConfig, +} from './config' + +export function initIpc() { + ipcMain.handle('open-browser', (_, url: string) => { + shell.openExternal(url) + }) + ipcMain.handle('daemon-start', async (_) => { + await startDaemon() + }) + ipcMain.handle('daemon-stop', async (_) => { + await stopDaemon() + }) + ipcMain.handle('daemon-is-running', (_) => { + const isDaemonRunning = getIsDaemonRunning() + return isDaemonRunning + }) + ipcMain.handle('config-get', (_) => { + const config = getConfig() + return config + }) + ipcMain.handle('get-is-configured', (_) => { + return getIsConfigured() + }) + ipcMain.handle('open-data-directory', (_) => { + shell.openPath(getDefaultDataPath()) + return true + }) + ipcMain.handle('get-default-data-directory', async (_) => { + await fs.promises.mkdir(getDefaultDataPath(), { recursive: true }) + return getDefaultDataPath() + }) + ipcMain.handle('get-installed-version', (_) => { + return getInstalledVersion() + }) + ipcMain.handle('get-latest-version', (_) => { + return getLatestVersion() + }) + ipcMain.handle('config-save', async (_, config: Config) => { + await saveConfig(config) + return true + }) +} diff --git a/hostd/electron-src/shortcuts.ts b/hostd/electron-src/shortcuts.ts new file mode 100644 index 0000000..d84900a --- /dev/null +++ b/hostd/electron-src/shortcuts.ts @@ -0,0 +1,15 @@ +import { globalShortcut } from 'electron' +import { state, system } from './state' + +export function initShortcuts() { + // Register a global shortcut listener for the developer tools + const devToolsShortcut = system.isDarwin ? 'Cmd+Alt+I' : 'Ctrl+Shift+I' + globalShortcut.register(devToolsShortcut, () => { + // Open the DevTools + if (state.mainWindow && state.mainWindow.webContents) { + state.mainWindow.webContents.openDevTools() + state.mainWindow.setSize(1000, 800) + state.mainWindow.setMaximumSize(2000, 2000) + } + }) +} diff --git a/hostd/electron-src/state.ts b/hostd/electron-src/state.ts index 72cce9e..9f6088a 100644 --- a/hostd/electron-src/state.ts +++ b/hostd/electron-src/state.ts @@ -1,7 +1,24 @@ import { ChildProcess } from 'child_process' +import { BrowserWindow, Tray } from 'electron' export let state: { - process: ChildProcess | null + mainWindow: BrowserWindow | null + tray: Tray | null + daemon: ChildProcess | null + isQuitting: boolean } = { - process: null, + mainWindow: null, + tray: null, + isQuitting: false, + daemon: null, +} + +export const system: { + isDarwin: boolean + isLinux: boolean + isWindows: boolean +} = { + isDarwin: process.platform === 'darwin', + isLinux: process.platform === 'linux', + isWindows: process.platform === 'win32', } diff --git a/hostd/electron-src/tray.ts b/hostd/electron-src/tray.ts new file mode 100644 index 0000000..fbce2de --- /dev/null +++ b/hostd/electron-src/tray.ts @@ -0,0 +1,32 @@ +import path from 'path' +import { app, Tray, Menu } from 'electron' +import isDev from 'electron-is-dev' +import { state, system } from './state' + +export function initTray() { + const iconName = system.isDarwin ? 'tray.png' : 'tray-win.png' + const iconPath = isDev + ? path.join(process.cwd(), 'assets', iconName) + : path.join(__dirname, '../assets', iconName) + + state.tray = new Tray(iconPath) + const trayContextMenu = Menu.buildFromTemplate([ + { + label: 'Configure', + click: function () { + if (system.isDarwin) { + app.dock.show() + } + state.mainWindow?.show() + }, + }, + { + label: 'Quit', + click: function () { + app.quit() + }, + }, + ]) + state.tray.setToolTip('hostd') + state.tray.setContextMenu(trayContextMenu) +} diff --git a/hostd/electron-src/utils.ts b/hostd/electron-src/utils.ts new file mode 100644 index 0000000..e69de29 diff --git a/hostd/electron-src/window.ts b/hostd/electron-src/window.ts new file mode 100644 index 0000000..5d222c5 --- /dev/null +++ b/hostd/electron-src/window.ts @@ -0,0 +1,62 @@ +import path, { join } from 'path' +import { BrowserWindow, app } from 'electron' +import isDev from 'electron-is-dev' +import { format } from 'url' +import { state, system } from './state' + +export function initWindow() { + state.mainWindow = new BrowserWindow({ + width: 500, + height: 700, + minWidth: 500, + minHeight: 600, + maxWidth: 500, + maxHeight: 800, + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + preload: join(__dirname, 'preload.js'), + }, + }) + + // Hide the main window instead of closing it, to keep the app running in the tray + state.mainWindow.on('close', (event) => { + if (!state.isQuitting) { + event.preventDefault() + if (system.isDarwin) { + app.dock.hide() + } + state.mainWindow?.hide() + } + }) + + // Setup close to tray settings for both minimize and close events + state.mainWindow.on('minimize', () => { + if (system.isDarwin) { + app.dock.hide() + } + // TODO: Does Linux support tray? + // https://electronjs.org/docs/api/tray + // minimize instead of attempting to go to system tray + if (system.isLinux) { + return true + } + state.mainWindow?.hide() + return false + }) + + const url = isDev + ? 'http://localhost:8000/' + : format({ + pathname: path.join(__dirname, '../renderer/out/index.html'), + protocol: 'file:', + slashes: true, + }) + state.mainWindow.loadURL(url) + + if (isDev) { + state.mainWindow.setMaximumSize(2000, 2000) + state.mainWindow.setSize(1000, 800) + state.mainWindow.webContents.openDevTools() + } +}