adb84d97e3
Describe what your pull request does. If you can, add GIFs or images showing the before and after of your change. ### Change type - [x] `bugfix` - [ ] `improvement` - [ ] `feature` - [ ] `api` - [ ] `other` ### Test plan 1. Create a shape... 2. - [ ] Unit tests - [ ] End to end tests ### Release notes - Fixed a bug with... --------- Co-authored-by: alex <alex@dytry.ch>
167 lines
4 KiB
TypeScript
167 lines
4 KiB
TypeScript
import { ChildProcessWithoutNullStreams, spawn } from 'child_process'
|
|
import kleur from 'kleur'
|
|
import { lock } from 'proper-lockfile'
|
|
import stripAnsi from 'strip-ansi'
|
|
|
|
const lockfileName = __dirname
|
|
|
|
/**
|
|
* a long time ago, workerd would regularly crash with a segfault but the error is not caught by the
|
|
* process, so it will just hang. this script wraps the process, tailing the logs and restarting the
|
|
* process if we encounter the string 'Segmentation fault'. we think this error is probably fixed
|
|
* now.
|
|
*
|
|
* there's a separate issue where spawning multiple workerd instances at once would cause them to
|
|
* fight and crash. we use a lockfile to start our wrokerd instances one at a time, waiting for the
|
|
* previous one to be ready before we start the next.
|
|
*/
|
|
class MiniflareMonitor {
|
|
private process: ChildProcessWithoutNullStreams | null = null
|
|
|
|
constructor(
|
|
private command: string,
|
|
private args: string[] = []
|
|
) {}
|
|
|
|
public async start(): Promise<void> {
|
|
await this.stop() // Ensure any existing process is stopped
|
|
await this.lock()
|
|
await console.log(`Starting wrangler...`)
|
|
this.process = spawn(this.command, this.args, {
|
|
env: {
|
|
NODE_ENV: 'development',
|
|
...process.env,
|
|
},
|
|
})
|
|
|
|
this.process.stdout.on('data', (data: Buffer) => {
|
|
this.handleOutput(stripAnsi(data.toString().replace('\r', '').trim()))
|
|
})
|
|
|
|
this.process.stderr.on('data', (data: Buffer) => {
|
|
this.handleOutput(stripAnsi(data.toString().replace('\r', '').trim()), true)
|
|
})
|
|
}
|
|
|
|
private handleOutput(output: string, err = false): void {
|
|
if (!output) return
|
|
|
|
if (output.includes('Ready on') && this.isLocked()) {
|
|
this.release()
|
|
}
|
|
|
|
if (output.includes('Segmentation fault')) {
|
|
console.error('Segmentation fault detected. Restarting Miniflare...')
|
|
this.restart()
|
|
} else if (!err) {
|
|
console.log(output.replace('[mf:inf]', '')) // or handle the output differently
|
|
}
|
|
}
|
|
|
|
private async restart(): Promise<void> {
|
|
console.log('Restarting wrangler...')
|
|
await this.stop()
|
|
setTimeout(() => this.start(), 3000) // Restart after a short delay
|
|
}
|
|
|
|
private async stop(): Promise<void> {
|
|
if (this.isLocked()) await this.release()
|
|
if (this.process) {
|
|
this.process.kill()
|
|
this.process = null
|
|
}
|
|
}
|
|
|
|
private _lockPromise?: Promise<() => Promise<void>>
|
|
private isLocked() {
|
|
return !!this._lockPromise
|
|
}
|
|
private async lock() {
|
|
if (this.isLocked()) throw new Error('Already locked')
|
|
console.log('Locking...')
|
|
this._lockPromise = lock(lockfileName, {
|
|
retries: {
|
|
minTimeout: 500,
|
|
retries: 10,
|
|
},
|
|
})
|
|
await this._lockPromise
|
|
console.log('Locked')
|
|
}
|
|
private async release() {
|
|
if (!this.isLocked()) throw new Error('Not locked')
|
|
console.log('Releasing...')
|
|
const lockPromise = this._lockPromise!
|
|
this._lockPromise = undefined
|
|
const release = await lockPromise
|
|
await release()
|
|
console.log('Released')
|
|
}
|
|
}
|
|
|
|
class SizeReporter {
|
|
lastLineTime = Date.now()
|
|
nextTick?: NodeJS.Timeout
|
|
|
|
size = 0
|
|
|
|
start() {
|
|
console.log('Spawning size reporter...')
|
|
const proc = spawn('yarn', [
|
|
'run',
|
|
'-T',
|
|
'esbuild',
|
|
'src/worker.ts',
|
|
'--bundle',
|
|
'--minify',
|
|
'--watch',
|
|
'--external:cloudflare:*',
|
|
'--target=esnext',
|
|
'--format=esm',
|
|
])
|
|
// listen for lines on stdin
|
|
proc.stdout.on('data', (data) => {
|
|
this.size += data.length
|
|
this.lastLineTime = Date.now()
|
|
clearTimeout(this.nextTick)
|
|
this.nextTick = setTimeout(() => {
|
|
console.log(
|
|
kleur.bold(kleur.yellow('worker')),
|
|
'is roughly',
|
|
kleur.bold(kleur.cyan(Math.floor(this.size / 1024) + 'kb')),
|
|
'(minified)\n'
|
|
)
|
|
this.size = 0
|
|
}, 10)
|
|
})
|
|
proc.stderr.on('data', (data) => {
|
|
console.log(data.toString())
|
|
})
|
|
process.on('SIGINT', () => {
|
|
console.log('Int')
|
|
proc.kill()
|
|
})
|
|
process.on('SIGTERM', () => {
|
|
console.log('Term')
|
|
proc.kill()
|
|
})
|
|
process.on('exit', () => {
|
|
console.log('Exiting')
|
|
proc.kill()
|
|
})
|
|
}
|
|
}
|
|
|
|
new MiniflareMonitor('wrangler', [
|
|
'dev',
|
|
'--env',
|
|
'dev',
|
|
'--test-scheduled',
|
|
'--log-level',
|
|
'info',
|
|
'--var',
|
|
'IS_LOCAL:true',
|
|
...process.argv.slice(2),
|
|
]).start()
|
|
|
|
new SizeReporter().start()
|