Skip to content
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

test: ns/fix/fuels dev node cleanup #3530

Closed
wants to merge 10 commits into from
6 changes: 6 additions & 0 deletions .changeset/kind-students-kneel.md
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
6 changes: 6 additions & 0 deletions .github/actions/test-setup/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ runs:
version: ${{ inputs.pnpm-version }}
run_install: true

- name: Setup forc and fuel-core paths
shell: bash
run: |
echo "$PWD/internal/forc/forc-binaries" >> $GITHUB_PATH
echo "$PWD/internal/fuel-core/fuel-core-binaries" >> $GITHUB_PATH

- name: Setup Bun
if: ${{ inputs.should-install-bun == 'true' }}
uses: oven-sh/setup-bun@v1
Expand Down
4 changes: 4 additions & 0 deletions packages/fuels/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ export const configureCli = () => {
.option('--forc-path <path>', 'Path to the `forc` binary')
.option('--fuel-core-path <path>', 'Path to the `fuel-core` binary')
.option('--auto-start-fuel-core', 'Auto-starts a `fuel-core` node during `dev` command')
.option(
'--fuel-core-port <port>',
'Port to use when starting a local `fuel-core` node for dev mode'
)
.action(withProgram(command, Commands.init, init));

(command = program.command(Commands.dev))
Expand Down
6 changes: 2 additions & 4 deletions packages/fuels/src/cli/commands/dev/autoStartFuelCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,7 @@ export const autoStartFuelCore = async (config: FuelsConfig) => {

const port = config.fuelCorePort ?? (await getPortPromise({ port: 4000 }));

const providerUrl = `http://${accessIp}:${port}/v1/graphql`;

const { cleanup, snapshotDir } = await launchNode({
const { cleanup, snapshotDir, url } = await launchNode({
args: [
['--snapshot', config.snapshotDir],
['--db-type', 'in-memory'],
Expand All @@ -44,7 +42,7 @@ export const autoStartFuelCore = async (config: FuelsConfig) => {
bindIp,
accessIp,
port,
providerUrl,
providerUrl: url,
snapshotDir,
killChildProcess: cleanup,
};
Expand Down
3 changes: 2 additions & 1 deletion packages/fuels/src/cli/commands/init/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { log } from '../../utils/logger';
export function init(program: Command) {
const options = program.opts();

const { path, autoStartFuelCore, forcPath, fuelCorePath } = options;
const { path, autoStartFuelCore, forcPath, fuelCorePath, fuelCorePort } = options;

let workspace: string | undefined;
let absoluteWorkspace: string | undefined;
Expand Down Expand Up @@ -61,6 +61,7 @@ export function init(program: Command) {
forcPath,
fuelCorePath,
autoStartFuelCore,
fuelCorePort,
});

writeFileSync(fuelsConfigPath, renderedConfig);
Expand Down
3 changes: 3 additions & 0 deletions packages/fuels/src/cli/templates/fuels.config.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ export default createConfig({
{{#if (isDefined autoStartFuelCore)}}
autoStartFuelCore: {{autoStartFuelCore}},
{{/if}}
{{#if (isDefined fuelCorePort)}}
fuelCorePort: {{fuelCorePort}},
{{/if}}
});

/**
Expand Down
1 change: 1 addition & 0 deletions packages/fuels/src/cli/templates/fuels.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export function renderFuelsConfigTemplate(props: {
forcPath?: string;
fuelCorePath?: string;
autoStartFuelCore?: boolean;
fuelCorePort?: string;
}) {
const renderTemplate = Handlebars.compile(fuelsConfigTemplate, {
strict: true,
Expand Down
81 changes: 81 additions & 0 deletions packages/fuels/test/features/dev-2.test.ts
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: 250_000 }
);
});
52 changes: 52 additions & 0 deletions packages/fuels/test/utils/processUtils.ts
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);
}
}
1 change: 1 addition & 0 deletions vitest.global-setup.ts
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';
}
Loading