Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions packages/nx/src/command-line/graph/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,6 @@ export async function generateGraph(
)
);
await output.drain();
await new Promise((res) => setImmediate(res));
process.exit(0);
}

Expand Down Expand Up @@ -480,7 +479,7 @@ export async function generateGraph(
});
process.exit(1);
}
await new Promise((res) => setImmediate(res));
await output.drain();
process.exit(0);
} else {
const environmentJs = buildEnvironmentJs(
Expand Down
7 changes: 7 additions & 0 deletions packages/nx/src/command-line/release/command-object.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { type Argv, type CommandModule, showHelp } from 'yargs';
import { handleImport } from '../../utils/handle-import';
import { logger } from '../../utils/logger';
import { output } from '../../utils/output';
import {
type OutputStyle,
type RunManyOptions,
Expand Down Expand Up @@ -263,6 +264,7 @@ const releaseCommand: CommandModule<NxReleaseArgs, ReleaseOptions> = {
logger.warn(`\nNOTE: The "dryRun" flag means no changes were made.`);
}

await output.drain();
process.exit(result);
},
};
Expand Down Expand Up @@ -303,6 +305,7 @@ const versionCommand: CommandModule<NxReleaseArgs, VersionOptions> = {
logger.warn(`\nNOTE: The "dryRun" flag means no changes were made.`);
}

await output.drain();
process.exit(result);
},
};
Expand Down Expand Up @@ -371,6 +374,7 @@ const changelogCommand: CommandModule<NxReleaseArgs, ChangelogOptions> = {
logger.warn(`\nNOTE: The "dryRun" flag means no changes were made.`);
}

await output.drain();
process.exit(result);
},
};
Expand Down Expand Up @@ -411,6 +415,7 @@ const publishCommand: CommandModule<NxReleaseArgs, PublishOptions> = {
logger.warn(`\nNOTE: The "dryRun" flag means no changes were made.`);
}

await output.drain();
process.exit(status);
},
};
Expand Down Expand Up @@ -453,6 +458,7 @@ const planCommand: CommandModule<NxReleaseArgs, PlanOptions> = {
logger.warn(`\nNOTE: The "dryRun" flag means no changes were made.`);
}

await output.drain();
process.exit(result);
},
};
Expand All @@ -465,6 +471,7 @@ const planCheckCommand: CommandModule<NxReleaseArgs, PlanCheckOptions> = {
handler: async (args) => {
const release = await handleImport('./plan-check.js', __dirname);
const result = await release.releasePlanCheckCLIHandler(args);
await output.drain();
process.exit(result);
},
};
Expand Down
1 change: 1 addition & 0 deletions packages/nx/src/command-line/run-many/run-many.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export async function runMany(
extraTargetDependencies,
extraOptions
);
await output.drain();
process.exit(status);
}
}
Expand Down
2 changes: 2 additions & 0 deletions packages/nx/src/command-line/run/run-one.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export async function runOne(
},
workspaceRoot
);
await output.drain();
process.exit(0);
}

Expand Down Expand Up @@ -100,6 +101,7 @@ export async function runOne(
extraTargetDependencies,
extraOptions
);
await output.drain();
process.exit(status);
}
}
Expand Down
2 changes: 0 additions & 2 deletions packages/nx/src/command-line/show/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,5 @@ export async function showProjectHandler(
}
}

// TODO: Find a better fix for this
await new Promise((res) => setImmediate(res));
await output.drain();
}
2 changes: 0 additions & 2 deletions packages/nx/src/command-line/show/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,6 @@ export async function showProjectsHandler(
}
}

// TODO: Find a better fix for this
await new Promise((res) => setImmediate(res));
await output.drain();
}

Expand Down
80 changes: 49 additions & 31 deletions packages/nx/src/tasks-runner/run-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,16 +165,27 @@ async function getTerminalOutputLifeCycle(
console.log = createPatchedConsoleMethod(originalConsoleLog);
console.error = createPatchedConsoleMethod(originalConsoleError);

const patchedWrite = (_chunk, _encoding, callback) => {
// Handle both overload signatures: dropping write(chunk, cb) would strand the
// callback and hang anything awaiting it, such as output.drain().
// Typed rather than cast so the compiler keeps checking that.
const patchedWrite = (
_chunk: string | Uint8Array,
encodingOrCallback?: BufferEncoding | ((err?: Error) => void),
callback?: (err?: Error) => void
): boolean => {
const cb =
typeof encodingOrCallback === 'function'
? encodingOrCallback
: callback;
// Preserve original behavior around callback and return value, just in case
if (callback) {
callback();
if (cb) {
cb(null);
}
return true;
};

process.stdout.write = patchedWrite as any;
process.stderr.write = patchedWrite as any;
process.stdout.write = patchedWrite;
process.stderr.write = patchedWrite;

const { AppLifeCycle, restoreTerminal } = await handleImport(
'../native/index.js',
Expand Down Expand Up @@ -266,29 +277,36 @@ async function getTerminalOutputLifeCycle(
/**
* Patch stdout.write and stderr.write methods to pass Nx Cloud client logs to the TUI via the lifecycle
*/
const createPatchedLogWrite = (
originalWrite:
| typeof process.stdout.write
| typeof process.stderr.write,
isError: boolean
): typeof process.stdout.write | typeof process.stderr.write => {
// @ts-ignore
return (chunk, encoding, callback) => {
if (isError) {
logDebug(
Buffer.isBuffer(chunk)
? chunk.toString(encoding)
: chunk.toString()
);
} else {
logDebug(
Buffer.isBuffer(chunk)
? chunk.toString(encoding)
: chunk.toString()
);
}
const createPatchedLogWrite = (): typeof process.stdout.write => {
// Handle both overload signatures: dropping write(chunk, cb) would strand the
// callback, and the callback would reach toString() below as the encoding.
// isEncoding also rejects the values toString() cannot take ('buffer', '', null).
return (
chunk: string | Uint8Array,
encodingOrCallback?: BufferEncoding | ((err?: Error) => void),
callback?: (err?: Error) => void
): boolean => {
const cb =
typeof encodingOrCallback === 'function'
? encodingOrCallback
: callback;
const enc =
typeof encodingOrCallback === 'string' &&
Buffer.isEncoding(encodingOrCallback)
? encodingOrCallback
: undefined;
logDebug(
ArrayBuffer.isView(chunk)
? Buffer.from(
chunk.buffer,
chunk.byteOffset,
chunk.byteLength
).toString(enc)
: chunk.toString()
);

// Check if the log came from the Nx Cloud client, otherwise invoke the original write method
// Nx Cloud logs are held back for the TUI; everything else is swallowed,
// since the TUI owns the terminal while it is running.
const stackTrace = new Error().stack;
const isNxCloudLog = stackTrace.includes(nxCloudClientDir);
if (isNxCloudLog) {
Expand All @@ -304,8 +322,8 @@ async function getTerminalOutputLifeCycle(
}
}
// Preserve original behavior around callback and return value, just in case
if (callback) {
callback();
if (cb) {
cb(null);
}
return true;
};
Expand All @@ -325,8 +343,8 @@ async function getTerminalOutputLifeCycle(
};
};

process.stdout.write = createPatchedLogWrite(originalStdoutWrite, false);
process.stderr.write = createPatchedLogWrite(originalStderrWrite, true);
process.stdout.write = createPatchedLogWrite();
process.stderr.write = createPatchedLogWrite();

// The cloud client calls console.log when NX_VERBOSE_LOGGING is set to true
console.log = createPatchedConsoleMethod(originalConsoleLog);
Expand Down
41 changes: 41 additions & 0 deletions packages/nx/src/tasks-runner/task-orchestrator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,4 +390,45 @@ describe('TaskOrchestrator', () => {
expect(orchestrator.cache.getBatch).toHaveBeenCalledTimes(1);
});
});

describe('SIGINT output silencing', () => {
// The handler replaces process.stdout.write permanently and never restores it,
// so anything awaiting a write callback after Ctrl-C depends on this shape.
it('invokes the callback from either argument position', async () => {
const realStdoutWrite = process.stdout.write;
const realStderrWrite = process.stderr.write;
const realOn = process.on;
try {
const orchestrator: any = Object.create(TaskOrchestrator.prototype);
orchestrator.tuiEnabled = false;
orchestrator.runningContinuousTasks = new Map();
orchestrator.cleanup = jest.fn(async () => {});
orchestrator.resolveStopPromise = jest.fn();

const handlers: Record<string, (...args: any[]) => void> = {};
jest.spyOn(process, 'on').mockImplementation(((
signal: string,
fn: any
) => {
handlers[signal] = fn;
return process;
}) as any);

orchestrator.setupSignalHandlers();
handlers['SIGINT']('SIGINT');

const twoArg = jest.fn();
expect((process.stdout.write as any)('x', twoArg)).toBe(true);
expect(twoArg).toHaveBeenCalled();

const threeArg = jest.fn();
expect((process.stderr.write as any)('x', 'utf8', threeArg)).toBe(true);
expect(threeArg).toHaveBeenCalled();
} finally {
process.stdout.write = realStdoutWrite;
process.stderr.write = realStderrWrite;
process.on = realOn;
}
});
});
});
19 changes: 15 additions & 4 deletions packages/nx/src/tasks-runner/task-orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1895,12 +1895,23 @@ export class TaskOrchestrator {
// Silence output — pnpm (and similar wrappers) may exit before nx
// finishes cleanup, returning the shell prompt. Any output after
// that point would appear after the prompt.
const noop = (_chunk, _encoding, callback) => {
if (callback) callback();
// Handle both overload signatures: dropping write(chunk, cb) would strand
// the callback and hang anything awaiting it, such as output.drain().
// Typed rather than cast so the compiler keeps checking that.
const noop = (
_chunk: string | Uint8Array,
encodingOrCallback?: BufferEncoding | ((err?: Error) => void),
callback?: (err?: Error) => void
): boolean => {
const cb =
typeof encodingOrCallback === 'function'
? encodingOrCallback
: callback;
if (cb) cb(null);
return true;
};
process.stdout.write = noop as any;
process.stderr.write = noop as any;
process.stdout.write = noop;
process.stderr.write = noop;
}
this.cleanup().finally(() => {
if (this.resolveStopPromise) {
Expand Down
Loading
Loading