Files
peardock/electron/ota.cjs
T
Raven Scott bd460913bf
Release rolling / release (push) Successful in 8m19s
OTA
2026-07-14 20:05:40 -04:00

202 lines
6.0 KiB
JavaScript

/**
* Pear OTA for peardock-client (Electron).
* Uses pear-runtime-updater + Corestore + Hyperswarm directly — NOT pear-runtime —
* so we avoid packaging bare-sidecar (~370MB) and stay fast to asar.
*/
'use strict'
const path = require('path')
const fs = require('fs')
const { app, BrowserWindow } = require('electron')
/**
* Load shared OTA config. Prefer lib/ota-config.cjs (packaged in asar);
* fall back to inline helpers so a missing file never bricks the client.
*/
function loadOtaConfig() {
const candidates = [
path.join(__dirname, '..', 'lib', 'ota-config.cjs'),
path.join(__dirname, 'ota-config.cjs'),
]
for (const file of candidates) {
try {
if (fs.existsSync(file)) return require(file)
} catch {
// try next
}
}
// Inline fallback (mirrors lib/ota-config.cjs client path)
return {
artifactName(kind) {
if (kind === 'server') {
return process.platform === 'win32' ? 'peardock-server.exe' : 'peardock-server'
}
if (process.platform === 'darwin') return 'peardock.app'
if (process.platform === 'win32') return 'peardock-client.exe'
return 'peardock-client'
},
buildUpdaterOptions(kind, opts = {}) {
const pkg = opts.pkg || {}
const upgrade = String(
process.env.PEARDOCK_OTA_UPGRADE ||
process.env.PEARDOCK_OTA_CLIENT ||
pkg.upgradeClient ||
pkg.upgrade ||
''
).trim()
const valid = Boolean(upgrade && /^pear:\/\//i.test(upgrade))
const disabled =
process.env.PEARDOCK_OTA_DISABLE === '1' ||
process.env.PEARDOCK_OTA_DISABLE === 'true'
return {
enabled: !disabled && valid,
upgrade: upgrade || null,
dir: opts.dir,
version: String(opts.version || pkg.version || '0.0.0'),
name: opts.name || this.artifactName(kind),
app: opts.app != null ? opts.app : null,
kind,
}
},
}
}
const otaConfig = loadOtaConfig()
/**
* @returns {Promise<{ enabled: boolean, upgrade: string|null, close: () => Promise<void> }>}
*/
async function startClientOta(opts = {}) {
const pkg = require('../package.json')
const cfg = otaConfig.buildUpdaterOptions('client', {
pkg,
dir: path.join(app.getPath('userData'), 'pear-runtime-data'),
app: getClientAppPath(),
name: otaConfig.artifactName('client'),
})
const broadcast =
opts.broadcast ||
((channel, data) => {
for (const win of BrowserWindow.getAllWindows()) {
if (!win.isDestroyed()) win.webContents.send(channel, data)
}
})
if (!cfg.enabled) {
console.log('[peardock-ota] disabled or no upgrade link')
return { enabled: false, upgrade: null, close: async () => {} }
}
if (!app.isPackaged && !process.env.PEARDOCK_OTA_FORCE) {
console.log('[peardock-ota] skipped in unpackaged dev (PEARDOCK_OTA_FORCE=1 to test)')
return { enabled: false, upgrade: cfg.upgrade, close: async () => {} }
}
fs.mkdirSync(cfg.dir, { recursive: true })
let PearRuntimeUpdater
let Corestore
let Hyperswarm
try {
PearRuntimeUpdater = require('pear-runtime-updater')
Corestore = require('corestore')
Hyperswarm = require('hyperswarm')
} catch (err) {
console.warn('[peardock-ota] deps missing:', err.message)
return { enabled: false, upgrade: cfg.upgrade, close: async () => {} }
}
const store = new Corestore(path.join(cfg.dir, 'corestore'))
await store.ready()
const updater = new PearRuntimeUpdater({
dir: cfg.dir,
version: cfg.version,
upgrade: cfg.upgrade,
name: cfg.name,
app: cfg.app,
updates: true,
store,
})
await updater.ready()
const keyPair = await store.createKeyPair('peardock-client-ota')
const swarm = new Hyperswarm({ keyPair })
swarm.on('connection', (c) => store.replicate(c))
swarm.join(updater.drive.core.discoveryKey, { client: true, server: false })
console.log(
`[peardock-ota] listening version=${cfg.version} name=${cfg.name} app=${cfg.app}`
)
updater.on('error', (err) => {
console.warn('[peardock-ota] error', err?.message || err)
broadcast('peardock:ota', { type: 'error', message: String(err?.message || err) })
})
updater.on('update-scheduled', (delay) => {
broadcast('peardock:ota', { type: 'scheduled', delayMs: delay })
})
updater.on('updating', () => {
console.log('[peardock-ota] downloading update…')
broadcast('peardock:ota', { type: 'updating' })
})
updater.on('updated', async () => {
console.log('[peardock-ota] update ready, applying…', updater.nextVersion)
broadcast('peardock:ota', { type: 'updated', nextVersion: updater.nextVersion })
try {
await updater.applyUpdate()
broadcast('peardock:ota', { type: 'applied' })
if (process.platform === 'linux' && process.env.APPIMAGE) {
app.relaunch({
execPath: process.env.APPIMAGE,
args: [
'--appimage-extract-and-run',
...process.argv.slice(1).filter((a) => a !== '--appimage-extract-and-run'),
],
})
} else {
app.relaunch()
}
app.exit(0)
} catch (err) {
console.error('[peardock-ota] apply failed', err)
broadcast('peardock:ota', { type: 'error', message: String(err.message || err) })
}
})
return {
enabled: true,
upgrade: cfg.upgrade,
close: async () => {
try {
await swarm.destroy()
} catch {
// ignore
}
try {
await updater.close()
} catch {
// ignore
}
try {
await store.close()
} catch {
// ignore
}
},
}
}
function getClientAppPath() {
if (process.env.PEARDOCK_OTA_APP) return process.env.PEARDOCK_OTA_APP
if (process.platform === 'linux' && process.env.APPIMAGE) return process.env.APPIMAGE
if (process.platform === 'win32') return process.execPath
if (process.platform === 'darwin' && app.isPackaged) {
return path.join(process.resourcesPath, '..', '..')
}
return process.execPath
}
module.exports = { startClientOta, getClientAppPath }