diff --git a/constants.js b/constants.js index b4a381084..4b2ec9ede 100644 --- a/constants.js +++ b/constants.js @@ -70,7 +70,7 @@ exports.WAKEUP = toPath(new URL(BIN + WAKEUP_EXEC, swapURL)) exports.RUNTIME = toPath(new URL(BIN + RUNTIME_EXEC, swapURL)) exports.DESKTOP_RUNTIME = toPath(new URL(BIN + DESKTOP_EXEC, swapURL)) -exports.BARE_RESTART_EXIT_CODE = 75 +exports.RESTART_EXIT_CODE = 75 function electronModuleURL () { const u = pathToFileURL(process.execPath) diff --git a/decal.html b/decal.html index 998c392ba..28c91324a 100644 --- a/decal.html +++ b/decal.html @@ -546,8 +546,7 @@

Unknown app

} if (name === 'action') { const cta = this.shadowRoot.querySelector('#cta') - const code = (value === 'reload') ? 64 : 0 - const handler = (value === 'quit' || value === 'reload') ? + const handler = (value === 'quit') ? () => Pear.exit(code) : () => {} cta.addEventListener('click', handler) diff --git a/electron-main.js b/electron-main.js index 4455715ad..bbd7200b9 100644 --- a/electron-main.js +++ b/electron-main.js @@ -49,10 +49,8 @@ async function electronMain (cmd) { electron.ipcMain.on('send-to', (e, id, channel, message) => { electron.webContents.fromId(id)?.send(channel, message) }) - const app = await gui.app() - app.unloading().then(async () => { - await app.close() - }) // note: would be unhandled rejection on failure, but should never fail + await gui.app() + } function configureElectron () { diff --git a/gui/gui.js b/gui/gui.js index a0ffd5d00..261d1d782 100644 --- a/gui/gui.js +++ b/gui/gui.js @@ -374,16 +374,35 @@ class App { state = null ipc = null id = null + session = null handle = null closing = null closed = false appReady = false static root = unixPathResolve(resolve(__dirname, '..')) - constructor (state, ipc) { + constructor (state, ipc, gui) { this.state = state this.ipc = ipc + this.gui = gui this.contextMenu = null + + this.ipc.messages({ type: 'pear/reload' }).once('data', async () => { + const lockWait = ipc.waitForLock() + await this.ipc.close() + console.log('ipc closed') + console.log('waiting for lock') + await lockWait + console.log('sidecar closed') + const id = 'todo' + this.state.id = id + this.session.setUserAgent(`Pear ${id}`) + console.log('now trigger app start and update electron + useragent with new startid') + console.log('then tell views to location.reload') + //location.reload() + }) + + electron.app.on('browser-window-focus', () => { this.menu.devtoolsReloaderUnlisten() }) electron.app.on('child-process-gone', (e, details) => { @@ -436,6 +455,7 @@ class App { }) }) this.menu = new Menu(this) + } async report ({ err }) { @@ -577,7 +597,7 @@ class App { } } }, - afterNativeWindowClose: () => this.close(), + afterClose: () => this.close(), afterNativeViewCreated: devtools && ((app) => { if (trace) return app.view.webContents.openDevTools({ mode: 'detach' }) @@ -612,7 +632,9 @@ class App { } }) this.id = ctrl.id + this.session = ctrl.session await this.starting + this.unloading() // note: would be unhandled rejection on failure, but should never fail } catch (err) { await this.report({ err }) this.close() @@ -626,7 +648,12 @@ class App { return { fork, length, key: key ? key.toString('hex') : null } } - unloading () { return this.ipc.unloading() } + async unloading () { + const action = await this.ipc.unloading() + for (const ctrl of PearGUI.ctrls()) ctrl.unload(action) + if (action.type === 'teardown') await this.ipc.close() + return action + } close (maxWait = 5500) { if (this.closing) return this.closing @@ -862,16 +889,17 @@ class GuiCtrl { async close () { if (this.closed) return true + console.log('this.unload', this.unload) if (this.unload) { this.unload({ type: 'close' }) await this.unloader } - let closer = null - if (this.win) { - closer = once(this.win, 'closed') - this.win.close() - } - await closer + // let closer = null + // if (this.win) { + // closer = once(this.win, 'closed') + // this.win.close() + // } + // await closer this.constructor[kMap].delete(this.id) this.id = null this.win = null @@ -917,8 +945,11 @@ class GuiCtrl { async unloading () { if (!this.#unloading) this.#unloading = this._unloading() try { - return await this.#unloading + const xxx = await this.#unloading + console.log('???', xxx) + return xxx } finally { + console.log('UNLDD') this.#unloading = null } } @@ -926,12 +957,13 @@ class GuiCtrl { async _unloading () { const { webContents } = (this.view || this.win) const until = new Promise((resolve) => { this.unload = resolve }) - webContents.once('will-navigate', (e, url) => { + const willNavListener = (e, url) => { if (!url.startsWith(this.sidecar)) return // handled by the other will-navigate handler e.preventDefault() const type = (!e.frame || e.frame.url === url) ? 'reload' : 'nav' this.unload({ type, url }) - }) + } + webContents.once('will-navigate', willNavListener) const closeListener = (e) => { e.preventDefault() @@ -940,16 +972,20 @@ class GuiCtrl { } } if (this.win) this.win.once('close', closeListener) + webContents.removeListener('will-navigate', willNavListener) this.unloader = new Promise((resolve) => { this.unloaded = resolve }) const action = await until + console.log('ACTION', action) if (this.win) this.win.removeListener('close', closeListener) this.unload = null return action } - completeUnload (action) { + async completeUnload (action) { this.unloaded() - if (action.type === 'close') this.close() + console.log('complete unload', action) + // await this.close() + // if (action.type === 'close') this.close() } setWindowButtonPosition (point) { @@ -1025,11 +1061,8 @@ class Window extends GuiCtrl { this.win.on('close', () => { this.closing = true }) - this.win.on('closed', () => { - if (typeof options.afterNativeWindowClose === 'function') { - options.afterNativeWindowClose(this) - } + if (typeof options.afterClose === 'function') options.afterClose(this) if (this.win) { if (this.opening) this.opening = false this.win.closed = true @@ -1408,7 +1441,10 @@ class PearGUI extends ReadyResource { }) this.worker = new Worker() this.pipes = new Freelist() - this.ipc.once('close', () => this.close()) + this.ipc.once('close', () => { + console.log('IPC CLOSED') + // this.close() + }) electron.ipcMain.on('exit', (e, code) => { process.exit(code) }) @@ -1439,6 +1475,9 @@ class PearGUI extends ReadyResource { messages.on('end', () => event.reply('messages', null)) }) + electron.ipcMain.handle('waitForLock', () => this.ipc.waitForLock()) + electron.ipcMain.handle('close', () => this._close()) + electron.ipcMain.handle('getMediaAccessStatus', (evt, ...args) => this.getMediaAccessStatus(...args)) electron.ipcMain.handle('askForMediaAccess', (evt, ...args) => this.askForMediaAccess(...args)) electron.ipcMain.handle('desktopSources', (evt, ...args) => this.desktopSources(...args)) @@ -1446,7 +1485,7 @@ class PearGUI extends ReadyResource { electron.ipcMain.handle('ctrl', (evt, ...args) => this.ctrl(...args)) electron.ipcMain.handle('parent', (evt, ...args) => this.parent(...args)) electron.ipcMain.handle('open', (evt, ...args) => this.open(...args)) - electron.ipcMain.handle('close', (evt, ...args) => this.guiClose(...args)) + electron.ipcMain.handle('guiClose', (evt, ...args) => this.guiClose(...args)) electron.ipcMain.handle('show', (evt, ...args) => this.show(...args)) electron.ipcMain.handle('hide ', (evt, ...args) => this.hide(...args)) electron.ipcMain.handle('minimize', (evt, ...args) => this.minimize(...args)) @@ -1529,8 +1568,8 @@ class PearGUI extends ReadyResource { } async app () { - const app = new App(this.state, this.ipc) - this.once('close', async () => { app.quit() }) + const app = new App(this.state, this.ipc, this) + // this.once('close', async () => { app.quit() }) await app.start() return app } @@ -1540,7 +1579,9 @@ class PearGUI extends ReadyResource { } async _close () { + console.log('calling ipc close') await this.ipc.close() + console.log('ipc close done') } static async ctrl (type, entry, { state, parentId = 0, ua, sessname = null, appkin }, options = {}, openOptions = {}) { @@ -1617,7 +1658,7 @@ class PearGUI extends ReadyResource { } } - async askForMediaAccess ({ id, media }) { + async askForMediaAccess ({ media }) { if (isLinux || isWindows) return false if (media === 'screen') { return electron.systemPreferences.getMediaAccessStatus(media) diff --git a/gui/preload.js b/gui/preload.js index c717ebcd9..df77c14b7 100644 --- a/gui/preload.js +++ b/gui/preload.js @@ -18,14 +18,19 @@ module.exports = class PearGUI extends ReadyResource { }) const onteardown = async (fn) => { - if (state.isDecal) return await this.ready() const action = await this.ipc.unloading({ id }) // only resolves when unloading occurs await fn() await this.ipc.completeUnload({ id, action }) + if (action.type === 'teardown') { + console.log('onteardown ipc close') + await this.ipc.closeIPC() + console.log('onteardown ipc closed') + } if (action.type === 'reload') location.reload() else if (action.type === 'nav') location.href = action.url } + const gui = this API = class extends API { constructor (ipc, state, onteardown) { super(ipc, state, onteardown) @@ -45,6 +50,18 @@ module.exports = class PearGUI extends ReadyResource { desktopSources: (options = {}) => ipc.desktopSources(options) } + // if (state.isDecal === false) ipc.messages({ type: 'pear/reload' }).once('data', async () => { + // const lockWait = ipc.waitForLock() + // await gui.close() + // console.log('ipc closed') + // console.log('waiting for lock') + // await lockWait + // console.log('sidecar closed') + // console.log('now wait trigger app start and update electron with new startid') + // console.log('ok so call location.reload') + // //location.reload() + // }) + const kGuiCtrl = Symbol('gui:ctrl') class Parent extends EventEmitter { @@ -213,6 +230,11 @@ module.exports = class PearGUI extends ReadyResource { this.View = View } + reload = async function (opts) { + if (opts?.platform) return this._reload(opts) + location.reload() + } + exit = (code) => { process.exitCode = code electron.ipcRenderer.sendSync('exit', code) @@ -220,6 +242,14 @@ module.exports = class PearGUI extends ReadyResource { } this.api = new API(this.ipc, state, onteardown) } + + _close () { + + // NEED TO TELL PARENT PROCESS TO CLOSE ITS IPC AS WELL SOMEHOW + + console.trace('PRELOAD _close') + return this.ipc.closeIPC() + } } class IPC { @@ -230,7 +260,7 @@ class IPC { ctrl (...args) { return electron.ipcRenderer.invoke('ctrl', ...args) } parent (...args) { return electron.ipcRenderer.invoke('parent', ...args) } open (...args) { return electron.ipcRenderer.invoke('open', ...args) } - close (...args) { return electron.ipcRenderer.invoke('close', ...args) } + close (...args) { return electron.ipcRenderer.invoke('guiClose', ...args) } show (...args) { return electron.ipcRenderer.invoke('show', ...args) } hide (...args) { return electron.ipcRenderer.invoke('hide', ...args) } minimize (...args) { return electron.ipcRenderer.invoke('minimize', ...args) } @@ -307,14 +337,14 @@ class IPC { stream.emit('error', new Error('Worker PipeError (from electron-main): ' + stack)) }) electron.ipcRenderer.on('workerClose', () => { stream.destroy() }) - stream.once('close', () => { - electron.ipcRenderer.send('workerPipeClose', id) - }) + stream.once('close', () => { electron.ipcRenderer.send('workerPipeClose', id) }) electron.ipcRenderer.on('workerPipeData', (e, data) => { stream.push(data) }) return stream } + waitForLock () { return electron.ipcRenderer.invoke('waitForLock') } + closeIPC () { return electron.ipcRenderer.invoke('close') } ref () {} unref () {} @@ -340,4 +370,5 @@ class IPC { electron.ipcRenderer.on('iteratePreferences', (e, data) => { stream.push(data) }) return stream } + } diff --git a/lib/api.js b/lib/api.js index 772b5c55b..7b7654210 100644 --- a/lib/api.js +++ b/lib/api.js @@ -1,6 +1,6 @@ 'use strict' const Worker = require('./worker') -const { BARE_RESTART_EXIT_CODE } = require('../constants') +const { RESTART_EXIT_CODE } = require('../constants') const noop = () => {} const teardown = global.Bare ? require('./teardown') : noop const program = global.Bare || global.process @@ -97,22 +97,17 @@ class API { versions = () => this.#reftrack(this.#ipc.versions()) restart = async (opts = {}) => { - const restart = this.#reftrack(this.#ipc.restart({ ...opts, hard: true })) + const restart = this.#reftrack(this.#ipc.restart({ ...opts })) return restart } - reload = async (opts = {}) => { - if (!opts.platform) { - // TODO: use Pear.shutdown when it lands instead - if (this.#state.type === 'terminal') Bare.exit(BARE_RESTART_EXIT_CODE) - else global.location.reload() - - return - } - - return this.#reftrack(this.#ipc.restart({ ...opts, hard: false })) + async _reload (opts = {}) { + if (opts.platform) return this.#reftrack(this.#ipc.restart({ ...opts, reload: true })) + this.exit(RESTART_EXIT_CODE) } + reload = (opts = {}) => this._reload(opts) + updates = (listener) => this.messages({ type: 'pear/updates' }, listener) wakeups = (listener) => this.messages({ type: 'pear/wakeup' }, listener) diff --git a/package.json b/package.json index 033e55774..910610d75 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "paparam": "^1.4.0", "pear-changelog": "^1.0.1", "pear-interface": "^1.0.0", - "pear-ipc": "^1.0.17", + "pear-ipc": "^1.1.0", "pear-link": "^2.0.1", "pear-updater": "^3.1.0", "protomux": "^3.6.0", diff --git a/run/index.js b/run/index.js index 55807ce3e..3e6f566cc 100644 --- a/run/index.js +++ b/run/index.js @@ -19,8 +19,7 @@ const { } = require('../errors') const parseLink = require('../lib/parse-link') const teardown = require('../lib/teardown') -const { PLATFORM_LOCK } = require('../constants') -const fsext = require('fs-native-extensions') +const { isWindows } = require('which-runtime') module.exports = async function run ({ ipc, args, cmdArgs, link, storage, detached, flags, appArgs, indices }) { const { drive, pathname } = parseLink(link) @@ -116,15 +115,10 @@ module.exports = async function run ({ ipc, args, cmdArgs, link, storage, detach global.Pear = pear - const reloadSubscriber = ipc.messages({ type: 'pear/reload' }) - reloadSubscriber.on('data', async () => { - ipc.stream.destroy() - - const fd = await new Promise((resolve, reject) => fs.open(PLATFORM_LOCK, 'r+', (err, fd) => err ? reject(err) : resolve(fd))) - await fsext.waitForLock(fd) - await new Promise((resolve, reject) => fs.close(fd, (err) => err ? reject(err) : resolve(fd))) - - await global.Pear.restart() + ipc.messages({ type: 'pear/reload' }).once('data', async () => { + await ipc.close() + await ipc.waitForLock() + await global.Pear.reload() }) const protocol = new Module.Protocol({ @@ -143,6 +137,14 @@ module.exports = async function run ({ ipc, args, cmdArgs, link, storage, detach return stream } + + + ipc.messages({ type: 'pear/reload' }).once('data', async () => { + await ipc.close() + await ipc.waitForLock() + console.log('create a new ipc client? what for we do not need it') + }) + args.unshift('--start-id=' + startId) const detach = args.includes('--detach') diff --git a/subsystems/sidecar/index.js b/subsystems/sidecar/index.js index d0c0e39df..8a4b25b40 100644 --- a/subsystems/sidecar/index.js +++ b/subsystems/sidecar/index.js @@ -72,7 +72,6 @@ class Sidecar extends ReadyResource { super() this.bus = new Iambus() this.version = CHECKOUT - this.updater = updater if (this.updater) this.updater.on('update', (checkout) => this.updateNotify(checkout)) @@ -234,7 +233,8 @@ class Sidecar extends ReadyResource { teardown () { if (this.unload) { - this.unload() + console.log('UNLOAD TYPE teardown') + this.unload({ type: 'teardown' }) return true } return false @@ -286,7 +286,9 @@ class Sidecar extends ReadyResource { if (this.decomissioned) return if (this.hasClients) return this.spindownt = setTimeout(async () => { + console.log('timeout complete', this.hasClients) if (this.hasClients) return + console.log('spindownt calling close') this.close().catch(console.error) }, this.spindownms) } @@ -513,9 +515,9 @@ class Sidecar extends ReadyResource { return metadata } - async restart ({ platform = false, hard = true } = {}, client) { - if (this.verbose) console.log(`${hard ? 'Hard' : 'Soft'} restarting ${platform ? 'platform' : 'client'}`) - if (platform === false) { + async restart ({ platform = false, reload = false } = {}, client) { + if (this.verbose) console.log(`${reload ? 'Reloading' : 'Restarting'} ${platform ? 'platform' : 'client'}`) + if (reload === false && platform === false) { const { dir, cwd, cmdArgs, env } = client.userData.state const appling = client.userData.state.appling const opts = { cwd, env, detached: true, stdio: 'ignore' } @@ -553,24 +555,19 @@ class Sidecar extends ReadyResource { return } - if (!hard && this.hasClients) { - const seen = new Set() - for (const { userData: app } of this.clients) { - if (!app.state || seen.has(app.state.id)) continue - seen.add(app.state.id) - app.message({ type: 'pear/reload' }) - } - } + if (reload) for (const { userData: app } of this.clients) app.message({ type: 'pear/reload' }) const sidecarClosed = new Promise((resolve) => this.corestore.once('close', resolve)) let restarts = await this.#shutdown(client) + // ample time for any OS cleanup operations: await new Promise((resolve) => setTimeout(resolve, 1500)) + + if (reload) return + // shutdown successful, reset death clock this.deathClock() - if (!hard) return - restarts = restarts.filter(({ run }) => run) if (restarts.length === 0) return if (this.verbose) console.log('Restarting', restarts.length, 'apps') @@ -871,7 +868,6 @@ class Sidecar extends ReadyResource { this.spindownms = 0 const restarts = this.closeClients() - this.spindownms = 0 this.#spindownCountdown() await this.closing @@ -879,26 +875,44 @@ class Sidecar extends ReadyResource { } async #close () { + console.log('#close') + console.log('pre appling close') await this.applings.close() + console.log('post appling close') clearTimeout(this.lazySwarmTimeout) + console.log('pre replicator leave') if (this.replicator) await this.replicator.leave(this.swarm) + console.log('post replicator leave') + console.log('pre http close') if (this.http) await this.http.close() + console.log('post http close') + console.log('pre http destroy') if (this.swarm) await this.swarm.destroy() + console.log('posthttp destroy') + console.log('pre corestore close') if (this.corestore) await this.corestore.close() + console.log('post corestore close') if (this.verbose) console.log((isWindows ? '^' : '✔') + ' Sidecar closed') } async _close () { + console.log('_close') if (this.decomissioned) return this.decomissioned = true // point of no return, death-march ensues + this.deathClock() const closing = this.#close() this.closeClients() + console.log('awaiting closing') await closing + console.log('closing resolved') + console.log('awaiting ipc close') await this.ipc.close() + console.log('resolved ipc close') if (this.updater) { + console.log('awaiting update') if (await this.updater.applyUpdate() !== null) { if (this.verbose) console.log((isWindows ? '^' : '✔') + ' Applied update') }