Compare commits

...

3 Commits

Author SHA1 Message Date
TheThing 15d1ba43f4 fix kill and make it work better on more platforms
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2024-03-01 09:40:14 +00:00
TheThing 8fad1b45b1 Fix bug in kill and add basic test. Improve error handling on import errors.
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2023-10-31 13:25:08 +00:00
TheThing a70d64e624 Implement smarter process kill when in npm mode with watch changes
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2023-10-31 13:05:45 +00:00
11 changed files with 169 additions and 17 deletions

11
cli.mjs
View File

@ -27,15 +27,20 @@ function PrintHelp() {
process.exit(1)
}
function showErrorAndExit(message = '', err = null, code = 1) {
console.log('')
function showErrorAndExit(message = '', err = null, code = 1, clean = false) {
if (!clean) {
console.log('')
}
if (message) {
console.error(`\x1b[31m${message}\x1b[0m`)
}
if (err) {
printError(err)
printError(err, '', clean)
if (err.inner) {
return showErrorAndExit(null, err.inner, code, true)
}
} else {
PrintHelp()
}

View File

@ -3,6 +3,7 @@ import fs from 'fs'
import fsPromise from 'fs/promises'
import cluster from 'cluster'
import child_process from 'child_process'
import kill from './kill.mjs'
import Watcher, { EVENT_REMOVE, EVENT_UPDATE } from './watch/index.mjs'
export const MESSAGE_FILES_REQUEST = 'message_files_request'
@ -41,6 +42,7 @@ export function CLI(e, overrides = {}) {
this.logger = overrides.logger || console
this.child_process = overrides.child_process || child_process
this.process = overrides.process || process
this.kill = overrides.kill || kill
this.importer = overrides.importer
this.loadDefaults()
}
@ -287,7 +289,7 @@ CLI.prototype.runProgram = function() {
if (runningTest) {
return
} else {
this.worker.kill()
this.kill(this.worker.pid)
}
}
@ -438,9 +440,9 @@ export function getFilesFromTarget(files, match, insidePath, grabAll, insideStar
})
}
export function printError(err, msg) {
export function printError(err, msg, clean = false) {
let before = msg || ''
console.error('')
if (!clean) console.error('')
console.error('\x1b[31m '
+ before + err.toString()
+ '\x1b[0m\n \x1b[90m'

85
lib/kill.mjs Normal file
View File

@ -0,0 +1,85 @@
import { promisify } from 'util'
import { spawn, exec } from 'child_process'
const execPromise = promisify(exec)
export default function kill(pid, signal) {
let pids = new Set([pid])
let getSpawn = null
let getPids = null
switch (process.platform) {
case 'win32':
return execPromise('taskkill /pid ' + pid + ' /T /F').then(() => pids)
case 'darwin':
getSpawn = function(parentPid) {
return spawn('pgrep', ['-P', parentPid])
}
getPids = function(data) {
return data.match(/\d+/g).map(Number)
}
break
default:
getSpawn = function (parentPid) {
return exec('ps -opid="" -oppid="" | grep ' + parentPid)
}
getPids = function(data, parentPid) {
let output = data.trim().split('\n')
return output.map(line => {
let [child, parent] = line.trim().split(/ +/)
if (Number(parent) === parentPid) {
return Number(child)
}
return 0
}).filter(x => x)
}
break
}
return buildTree(pids, getSpawn, getPids, pid)
.then(function() {
for (let pid of pids) {
try {
process.kill(pid, signal)
} catch (err) {
if (err.code !== 'ESRCH') throw err;
}
}
return pids
})
}
function buildTree(allPids, spawnGetChildren, spawnGetPids, parentPid) {
allPids.add(parentPid)
let ps = spawnGetChildren(parentPid)
let data = ''
let err = ''
ps.stdout.on('data', function(buf) {
data += buf.toString('ascii')
})
ps.stderr.on('data', function(buf) {
err += buf.toString('ascii')
})
return new Promise(function(res, rej) {
ps.on('close', function(code) {
// Check if ps errored out
if (code !== 0 && err.trim()) {
return rej(new Error('Error running ps to kill processes:\n\t' + err))
}
// Check if we otherwise got an error code (usually means empty results)
if (code !== 0 || !data.trim()) return res()
let pids = spawnGetPids(data, parentPid)
res(Promise.all(
pids.filter(pid => pid && !allPids.has(pid))
.map(buildTree.bind(this, allPids, spawnGetChildren, spawnGetPids))
))
})
})
}

View File

@ -1,6 +1,6 @@
{
"name": "eltro",
"version": "1.4.2",
"version": "1.4.5",
"description": "Eltro is a tiny no-dependancy test framework for node",
"main": "index.mjs",
"scripts": {

View File

@ -26,8 +26,9 @@ t.describe('CLI', function() {
const cluster = { c: 3 }
const process = { d: 4 }
const importer = { e: 5 }
const kill = { f: 6 }
let cliTest = createSafeCli(eltro, { logger, cluster, process, importer })
let cliTest = createSafeCli(eltro, { logger, cluster, process, importer, kill })
assert.strictEqual(cliTest.reporter, 'list')
assert.strictEqual(cliTest.ignoreOnly, false)
assert.strictEqual(cliTest.timeout, 2000)
@ -43,6 +44,7 @@ t.describe('CLI', function() {
assert.strictEqual(cliTest.cluster, cluster)
assert.strictEqual(cliTest.process, process)
assert.strictEqual(cliTest.importer, importer)
assert.strictEqual(cliTest.kill, kill)
})
t.test('should detect isSlave from cluster', function() {
@ -913,14 +915,16 @@ t.describe('CLI', function() {
let testCluster
let testChildProcess
let testWorker
let testKill
let cli
t.beforeEach(function() {
testKill = stub()
testProcess = { stdout: { write: stub() }, stderr: { write: stub() } }
testWorker = { on: stub(), once: stub(), kill: stub(), send: stub(), stderr: { on: stub() }, stdout: { on: stub() } }
testWorker = { on: stub(), once: stub(), send: stub(), stderr: { on: stub() }, stdout: { on: stub() } }
testCluster = { fork: stub().returns(testWorker) }
testChildProcess = { spawn: stub().returns(testWorker) }
cli = createSafeCli(null, { cluster: testCluster, child_process: testChildProcess, process: testProcess })
cli = createSafeCli(null, { cluster: testCluster, child_process: testChildProcess, process: testProcess, kill: testKill })
})
t.describe('in test mode', function() {
@ -984,14 +988,14 @@ t.describe('CLI', function() {
t.test('multiple calls should cancel', function() {
cli.runProgram()
assert.notOk(testWorker.kill.called)
assert.notOk(testKill.called)
assert.ok(testCluster.fork.called)
testCluster.fork.reset()
cli.runProgram()
assert.notOk(testCluster.fork.called)
assert.notOk(testWorker.kill.called)
assert.notOk(testKill.called)
})
})
@ -1063,16 +1067,19 @@ t.describe('CLI', function() {
})
t.test('multiple calls should kill', function() {
const assertPid = 12345235
testWorker.pid = assertPid
cli.runProgram()
assert.notOk(testWorker.kill.called)
assert.notOk(testKill.called)
assert.ok(testChildProcess.spawn.called)
testChildProcess.spawn.reset().returns(testWorker)
cli.runProgram()
assert.ok(testChildProcess.spawn.called)
assert.ok(testWorker.kill.called)
assert.ok(testKill.called)
assert.strictEqual(testKill.firstCall[0], assertPid)
})
})
})

View File

@ -234,7 +234,6 @@ e.test('Eltro should support capturing unknown errors outside scope', async func
t.describe('', function() {
t.test('', function(cb) {
process.nextTick(function() {
console.log('throw')
throw assertError
})
})

41
test/kill/kill.test.mjs Normal file
View File

@ -0,0 +1,41 @@
import { spawn } from 'child_process'
import t from '../../lib/eltro.mjs'
import assert from '../../lib/assert.mjs'
import kill from '../../lib/kill.mjs'
t.describe('kill', function() {
let worker
t.afterEach(function() {
if (worker?.pid && !worker.killed) {
worker.kill()
}
})
t.test('should kill process correctly', function(done) {
worker = spawn('node', ['./test/kill/runner.mjs'])
assert.ok(worker.pid)
worker.on('exit', done.finish(function(code, signal) {
assert.ok(code || signal)
}))
kill(worker.pid)
})
t.test('should succeed in killing tree', async function() {
worker = spawn('node', ['./test/kill/runner.mjs'])
assert.ok(worker.pid)
// Give it some time to start
await new Promise(res => {
worker.stdout.on('data', function(data) {
if (data.toString().indexOf('secondary') >= 0) res()
})
})
return kill(worker.pid).then(function(pids) {
assert.strictEqual(pids.size, 2)
})
})
})

11
test/kill/runner.mjs Normal file
View File

@ -0,0 +1,11 @@
import { spawn } from 'child_process'
console.log('primary', process.pid)
let secondary = spawn('node', ['./test/kill/second_runner.mjs'])
secondary.stdout.on('data', function(data) {
process.stdout.write(data)
})
setInterval(function() { console.log('primary', process.pid) }, 100)

View File

@ -0,0 +1,2 @@
console.log('secondary', process.pid)
setInterval(function() { console.log('secondary', process.pid) }, 100)

View File

@ -28,7 +28,7 @@ t.afterEach(function(done) {
t.after(function() {
if (builder) {
builder.cleanup()
return builder.cleanup()
}
})

View File

@ -173,7 +173,7 @@ Builder.prototype.newRandomFiles = function(fpath, count) {
}
Builder.prototype.cleanup = function() {
return fs.rmdir(this.root)
return fs.rm(this.root, { recursive: true, force: true })
}
Builder.prototype.getAllDirectories = function() {