-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: fuels dev
cleanup not killing node
#3038
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
--- | ||
"@fuel-ts/account": patch | ||
"fuels": patch | ||
--- | ||
|
||
fix: `fuels dev` cleanup not killing node |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -225,7 +225,6 @@ export const launchNode = async ({ | |
} | ||
childState.isDead = true; | ||
|
||
removeSideffects(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removing this line caused the issue to go away. I suspect that the root cause of the problem is somewhere in the The cleanup still happens because of the |
||
if (child.pid !== undefined) { | ||
try { | ||
process.kill(-child.pid); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import { execSync, execFileSync, spawn } from 'child_process'; | ||
import { mkdirSync, rmSync } from 'fs'; | ||
import { tmpdir } from 'os'; | ||
import path from 'path'; | ||
|
||
import { deferPromise, randomUUID } from '../../src'; | ||
import { findChildProcessPid, waitProcessEnd } from '../utils/processUtils'; | ||
|
||
function runInit() { | ||
const fuelsPath = path.join(process.cwd(), 'packages/fuels'); | ||
|
||
const init = path.join(tmpdir(), '.fuels', 'tests', randomUUID()); | ||
|
||
mkdirSync(init, { recursive: true }); | ||
|
||
execFileSync('pnpm', ['init'], { cwd: init }); | ||
execFileSync('pnpm', ['link', fuelsPath], { cwd: init }); | ||
|
||
const contractDir = path.join(init, 'contract'); | ||
const outputDir = path.join(init, 'output'); | ||
mkdirSync(contractDir); | ||
mkdirSync(outputDir); | ||
|
||
execSync(`${process.env.FORC_PATH} init`, { cwd: contractDir }); | ||
execSync(`pnpm fuels init -o ${outputDir} -c ${contractDir} --fuel-core-port 0`, { cwd: init }); | ||
|
||
return { | ||
init, | ||
[Symbol.dispose]: () => { | ||
rmSync(init, { recursive: true }); | ||
}, | ||
}; | ||
} | ||
|
||
/** | ||
* @group node | ||
*/ | ||
describe('dev', () => { | ||
it( | ||
'cleans up resources on graceful shutdown', | ||
async () => { | ||
using paths = runInit(); | ||
|
||
const devProcess = spawn('pnpm fuels dev', { | ||
shell: 'bash', | ||
detached: true, | ||
cwd: paths.init, | ||
}); | ||
|
||
const devCompleted = deferPromise(); | ||
|
||
devProcess.stdout.on('data', (chunk) => { | ||
const text = chunk.toString(); | ||
if (text.indexOf('Dev completed successfully!') !== -1) { | ||
devCompleted.resolve(undefined); | ||
} | ||
}); | ||
|
||
await devCompleted.promise; | ||
|
||
const devExited = deferPromise(); | ||
devProcess.on('exit', () => { | ||
devExited.resolve(undefined); | ||
}); | ||
|
||
const devPid = devProcess.pid as number; | ||
|
||
const fuelCorePid = findChildProcessPid(devPid, 'fuel-core') as number; | ||
|
||
// we kill the pnpm fuels dev process group | ||
// and we want to verify that the fuel-core process is also killed | ||
process.kill(-devPid, 'SIGINT'); | ||
|
||
await devExited.promise; | ||
|
||
// if it finishes before timeout, it means the process was killed successfully | ||
await waitProcessEnd(fuelCorePid); | ||
}, | ||
{ timeout: 15000 } | ||
); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import { sleep } from '@fuel-ts/utils'; | ||
import { execSync } from 'child_process'; | ||
|
||
export function findChildProcessPid( | ||
parentPid: number, | ||
childProcessName: string | ||
): number | undefined { | ||
const childProcesses = execSync(`ps --ppid ${parentPid} -o pid,cmd --no-headers || true`) | ||
.toString() | ||
.split('\n') | ||
.map((s) => s.trim()) | ||
.filter((s) => s !== ''); | ||
|
||
for (const cp of childProcesses) { | ||
const [pid, name] = cp.split(' '); | ||
if (name.indexOf(childProcessName) !== -1) { | ||
return +pid; | ||
} | ||
const childPid = findChildProcessPid(+pid, childProcessName); | ||
if (childPid) { | ||
return childPid; | ||
} | ||
} | ||
|
||
return undefined; | ||
} | ||
|
||
function isProcessRunning(pid: number) { | ||
try { | ||
// Check if the process exists | ||
process.kill(pid, 0); | ||
return true; // If no error, the process is running | ||
} catch (e) { | ||
const error = e as Error & { code: string }; | ||
// Error codes: | ||
// ESRCH: No such process | ||
// EPERM: Permission denied (you don't have permissions to check) | ||
if (error.code === 'ESRCH') { | ||
return false; // No such process | ||
} | ||
if (error.code === 'EPERM') { | ||
return true; // Process exists, but we don't have permission to send a signal | ||
} | ||
throw error; // Some other unexpected error | ||
} | ||
} | ||
|
||
export async function waitProcessEnd(pid: number) { | ||
while (isProcessRunning(pid)) { | ||
await sleep(100); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
export default function setup() { | ||
process.env.FUEL_CORE_PATH = 'fuels-core'; | ||
process.env.FORC_PATH = 'fuels-forc'; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added these to the path so that the spawn can run from the
cwd
of the temporary test project instead of runningpnpm fuels init --path ${paths.init}
from our repo's root. Both of them are using thefuels-forc
under the hood, but it feels more correct to have all the commands be running from inside the temporary test folder,pnpm fuels init
included.As an alternative, I could initiate the temporary project and pass in the
--fuel-core-path
and--forc-path
arguments to point to these binaries. I opted for addingforc
andfuel-core
to the path like this because this wasn't the first time I had to debug this problem only to come to the conclusion thatforc
andfuel-core
aren't in the path.