diff --git a/cmd/gc.js b/cmd/gc.js new file mode 100644 index 000000000..c95f75ce8 --- /dev/null +++ b/cmd/gc.js @@ -0,0 +1,33 @@ +'use strict' +const { print, outputter, InputError } = require('./iface') +const parse = require('../lib/parse') + +const output = outputter('gc', { + kill: ({ pid }) => `Killed sidecar with pid: ${pid}`, + complete: ({ killed }) => { return killed.length > 0 ? `Total killed sidecars: ${killed.length}` : 'No running sidecars' }, + error: ({ code, message, stack }) => `GC Error (code: ${code || 'none'}) ${message} ${stack}` +}) + +module.exports = (ipc) => async function gc (args) { + try { + const flags = parse.args(args, { + boolean: ['json'] + }) + const { _, json } = flags + const [resource] = _ + if (!resource) throw new InputError('A must be specified.') + if (resource !== 'sidecar') throw new InputError(`Resource '${resource}' is not valid`) + const stream = ipc.gc({ pid: Bare.pid, resource }, ipc) + await output(json, stream) + } catch (err) { + if (err instanceof InputError || err.code === 'ERR_INVALID_FLAG') { + print(err.message, false) + ipc.userData.usage.output('gc') + } else { + print('An error occured', false) + } + Bare.exit(1) + } finally { + await ipc.close() + } +} diff --git a/cmd/index.js b/cmd/index.js index 3284ff7af..85de0d8c6 100644 --- a/cmd/index.js +++ b/cmd/index.js @@ -8,6 +8,7 @@ const dump = require('./dump') const shift = require('./shift') const sidecar = require('./sidecar') const run = require('./run') +const gc = require('./gc') const parse = require('../lib/parse') const { CHECKOUT } = require('../lib/constants') @@ -74,6 +75,7 @@ module.exports = async (ipc) => { cmd.add('build', build) cmd.add('shift', shift(ipc)) cmd.add('sidecar', (args) => sidecar(ipc)(args)) + cmd.add('gc', (args) => gc(ipc)(args)) await cmd.run(argv) diff --git a/cmd/usage.js b/cmd/usage.js index 1c722f5c0..f32f1cea9 100644 --- a/cmd/usage.js +++ b/cmd/usage.js @@ -178,6 +178,16 @@ module.exports = ({ fork, length, key }) => { --json Single JSON object ` + const gc = ansi.bold(cmd + ' gc') + const gcArgs = ansi.bold('') + const gcBrief = 'Garbage Collection. Remove unused resources.' + const gcExplain = `${gc} ${gcArgs} + + ${gcBrief} + + --json Newline delimited JSON output + ` + const help = ansi.bold(cmd + ' help') const helpArgs = ansi.bold('[cmd]') const helpBrief = `${ansi.bold('Legend:')} [arg] = optional, = required, | = or \n Run ${ansi.bold('pear help')} to output full help for all commands` @@ -220,6 +230,7 @@ module.exports = ({ fork, length, key }) => { seed: seedExplain, shift: shiftExplain, sidecar: sidecarExplain, + gc: gcExplain, help: helpExplain, output, outputVersions, @@ -235,6 +246,7 @@ module.exports = ({ fork, length, key }) => { ${shift} ${ansi.sep} ${dedot(shiftBrief)} ${sidecar} ${ansi.sep} ${dedot(sidecarBrief)} ${versions} ${ansi.sep} ${dedot(versionsBrief)} + ${gc} ${ansi.sep} ${dedot(gcBrief)} ${helpExplain} ${footer}`, @@ -249,6 +261,7 @@ ${footer}`, ${shiftExplain} ${sidecarExplain} ${versionsExplain} + ${gcExplain} ${helpExplain} ${footer}` } diff --git a/lib/errors.js b/lib/errors.js index f4b27144f..4136fafab 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -79,6 +79,10 @@ function ERR_UNABLE_TO_FETCH_MANIFEST (msg) { return new PearError(msg, 'ERR_CONNECTION', ERR_UNABLE_TO_FETCH_MANIFEST) } +function ERR_UNKNOWN_GC_RESOURCE (msg) { + return new PearError(msg, 'ERR_UNKNOWN_GC_RESOURCE', ERR_UNKNOWN_GC_RESOURCE) +} + function ERR_ASSERTION (msg) { return new PearError(msg, 'ERR_ASSERTION', ERR_ASSERTION) } @@ -99,5 +103,6 @@ module.exports = { ERR_INVALID_APPLICATION_STORAGE, ERR_PACKAGE_JSON_NOT_FOUND, ERR_UNABLE_TO_FETCH_MANIFEST, + ERR_UNKNOWN_GC_RESOURCE, ERR_ASSERTION } diff --git a/lib/gc.js b/lib/gc.js new file mode 100644 index 000000000..d960349eb --- /dev/null +++ b/lib/gc.js @@ -0,0 +1,68 @@ +'use strict' +const { isBare, isWindows } = require('which-runtime') +const os = isBare ? require('bare-os') : require('os') +const { spawn } = isBare ? require('bare-subprocess') : require('child_process') +const { Readable } = require('streamx') + +module.exports = class GarbageCollector extends Readable { + constructor (client, engine) { + super() + this.client = client + this.engine = engine + } + + _destroy (cb) { + cb(null) + } + + sidecar ({ pid }) { + const name = 'pear-runtime' + const flag = '--sidecar' + + const [sh, args] = isWindows + ? ['cmd.exe', ['/c', `wmic process where (name like '%${name}%') get name,executablepath,processid,commandline /format:csv`]] + : ['/bin/sh', ['-c', `ps ax | grep -i -- '${name}' | grep -i -- '${flag}'`]] + + const sp = spawn(sh, args) + let output = '' + let pidIndex = isWindows ? -1 : 0 + let isHeader = !!isWindows + const killed = [] + + sp.stdout.on('data', (data) => { + output += data.toString() + const lines = output.split(isWindows ? '\r\r\n' : '\n') + output = lines.pop() + for (const line of lines) { + if (!line.trim()) continue + const columns = line.split(isWindows ? ',' : ' ').filter(col => col) + if (isHeader && isWindows) { + const index = columns.findIndex(col => /processid/i.test(col.trim())) + pidIndex = index !== -1 ? index : 4 + isHeader = false + } else { + const id = parseInt(columns[pidIndex]) + if (!isNaN(id) && ![Bare.pid, sp.pid, pid].includes(id)) { + os.kill(id) + this.push({ tag: 'kill', data: { pid: id } }) + killed.push(id) + } + } + } + }) + + sp.on('exit', (code, signal) => { + if (code !== 0 || signal) { + this.#error(new Error(`Process exited with code: ${code}, signal: ${signal}`)) + } + this.push({ tag: 'complete', data: { killed } }) + this.push({ tag: 'final', data: { success: true } }) + this.push(null) + }) + } + + #error (err) { + const { stack, code, message } = err + this.push({ tag: 'error', data: { stack, code, message, success: false } }) + } +} diff --git a/lib/sidecar.js b/lib/sidecar.js index 5f171fb67..e04d98b78 100644 --- a/lib/sidecar.js +++ b/lib/sidecar.js @@ -31,6 +31,7 @@ const Replicator = require('./replicator') const parse = require('./parse') const Context = require('../ctx/sidecar') const registerUrlHandler = require('./url-handler') +const GarbageCollector = require('./gc') const { PLATFORM_DIR, PLATFORM_LOCK, GC, SOCKET_PATH, CHECKOUT, APPLINGS_PATH, BOOT, @@ -44,7 +45,8 @@ const { ERR_PLATFORM_ERROR, ERR_TRACER_FAILED, ERR_SHIFT_STORAGE_ERROR, - ERR_PERMISSION_REQUIRED + ERR_PERMISSION_REQUIRED, + ERR_UNKNOWN_GC_RESOURCE } = require('./errors') // ensure that we are registered as a link handler @@ -1149,6 +1151,13 @@ class Engine extends ReadyResource { closeClients () { return this.sidecar.closeClients() } + gc ({ pid, resource }, client) { + if (resource !== 'sidecar') throw ERR_UNKNOWN_GC_RESOURCE() + const gc = new GarbageCollector(client, this) + gc.sidecar({ pid }) + return gc + } + async * shift (params, client) { const session = new Session(client) try {