Skip to content

Commit

Permalink
Pear gc sidecar (#137)
Browse files Browse the repository at this point in the history
* gc sidecar as stream, add usage

* fix win

* remove unused code

* use stream, remove GC class

* remove unused prop

* GC as stream, use os.kill

* use text-decoder

* fix win

* gc to lib/gc

* rebase and update lib errors

* remove text-decoder

* cmd to resource, errors

* add await

* inline close
  • Loading branch information
ruy-dan authored and davidmarkclements committed May 1, 2024
1 parent 8e93b41 commit f2cd5b0
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 1 deletion.
33 changes: 33 additions & 0 deletions cmd/gc.js
Original file line number Diff line number Diff line change
@@ -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 <cmd> 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()
}
}
2 changes: 2 additions & 0 deletions cmd/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down Expand Up @@ -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)

Expand Down
13 changes: 13 additions & 0 deletions cmd/usage.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,16 @@ module.exports = ({ fork, length, key }) => {
--json Single JSON object
`

const gc = ansi.bold(cmd + ' gc')
const gcArgs = ansi.bold('<resource>')
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, <arg> = required, | = or \n Run ${ansi.bold('pear help')} to output full help for all commands`
Expand Down Expand Up @@ -220,6 +230,7 @@ module.exports = ({ fork, length, key }) => {
seed: seedExplain,
shift: shiftExplain,
sidecar: sidecarExplain,
gc: gcExplain,
help: helpExplain,
output,
outputVersions,
Expand All @@ -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}`,
Expand All @@ -249,6 +261,7 @@ ${footer}`,
${shiftExplain}
${sidecarExplain}
${versionsExplain}
${gcExplain}
${helpExplain}
${footer}`
}
Expand Down
5 changes: 5 additions & 0 deletions lib/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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
}
68 changes: 68 additions & 0 deletions lib/gc.js
Original file line number Diff line number Diff line change
@@ -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 } })
}
}
11 changes: 10 additions & 1 deletion lib/sidecar.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit f2cd5b0

Please sign in to comment.