Skip to content
Open
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
10 changes: 6 additions & 4 deletions packages/playground/cli/src/blueprints-v2/worker-thread-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,13 @@ async function mountResources(php: PHP, mounts: Mount[]) {
mount.vfsPath,
createNodeFsMountHandler(mount.hostPath)
);
} catch {
output.stderr(
`\x1b[31m\x1b[1mError mounting path ${mount.hostPath} at ${mount.vfsPath}\x1b[0m\n`
} catch (error) {
const errorSummary =
error instanceof Error ? error.message : String(error);
throw new Error(
`Error mounting path ${mount.hostPath} at ${mount.vfsPath}: ${errorSummary}`,
{ cause: error }
);
process.exit(1);
}
}
}
Expand Down
28 changes: 27 additions & 1 deletion packages/playground/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,33 @@ function runCLI() {
// Dynamic import avoids loading run-cli when we're about to respawn.
// Do not await — top-level await is not supported in all environments.
import('./run-cli').then(({ parseOptionsAndRunCLI }) => {
parseOptionsAndRunCLI(args);
parseOptionsAndRunCLI(args)
.then((result) => {
if ('exitCode' in result) {
process.exit(result.exitCode);
return;
}

// A server is running. Clean up and exit on
// SIGINT / SIGTERM.
const cleanUpAndExit = (() => {
let cleaning: PromiseLike<void>;
return async () => {
if (!cleaning) {
cleaning = result[Symbol.asyncDispose]();
}
await cleaning;
process.exit(0);
};
})();
process.on('SIGINT', cleanUpAndExit);
process.on('SIGTERM', cleanUpAndExit);
})
.catch(() => {
// Unexpected error — already logged by
// parseOptionsAndRunCLI.
process.exit(1);
});
});
Comment thread
brandonpayton marked this conversation as resolved.
}

Expand Down
138 changes: 87 additions & 51 deletions packages/playground/cli/src/run-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,49 @@ type LogVerbosity = (typeof LogVerbosity)[keyof typeof LogVerbosity]['name'];

export type WorkerType = 'v1' | 'v2';

/**
* Returned by parseOptionsAndRunCLI when the CLI should exit
* without starting a long-running server.
*/
export interface CLIExitResult {
exitCode: number;
}

/**
* Returned by parseOptionsAndRunCLI when a server was started
* and is still running.
*/
export interface CLIServerResult extends AsyncDisposable {
[internalsKeyForTesting]: { cliServer: RunCLIServer };
}

export type ParseCLIResult = CLIExitResult | CLIServerResult;

/**
* Internal sentinel thrown inside yargs callbacks (which can only
* signal failure by throwing) when validation has already been
* reported to the user. Caught within parseOptionsAndRunCLI and
* converted to a CLIExitResult — never exposed to callers.
*/
class CLIArgsValidationError extends Error {
exitCode: number;
constructor(exitCode: number) {
super();
this.exitCode = exitCode;
}
}

/**
* Parse the CLI args and run the appropriate command.
*
* Returns a structured result so the caller can decide how to
* exit. Only throws for truly unexpected errors.
*
* @param argsToParse string[] The CLI args to parse.
*/
export async function parseOptionsAndRunCLI(argsToParse: string[]) {
export async function parseOptionsAndRunCLI(
argsToParse: string[]
): Promise<ParseCLIResult> {
try {
/**
* @TODO This looks similar to Query API args https://wordpress.github.io/wordpress-playground/developers/apis/query-api/
Expand Down Expand Up @@ -515,10 +552,10 @@ export async function parseOptionsAndRunCLI(argsToParse: string[]) {
if (msg && msg.includes('Please specify a command')) {
yargsInstance.showHelp();
console.error('\n' + msg);
process.exit(1);
} else {
console.error(msg);
}
console.error(msg);
process.exit(1);
throw new CLIArgsValidationError(1);
})
.strictOptions()
.check(async (args) => {
Expand Down Expand Up @@ -647,7 +684,7 @@ export async function parseOptionsAndRunCLI(argsToParse: string[]) {
].includes(command)
) {
yargsObject.showHelp();
process.exit(1);
throw new CLIArgsValidationError(1);
}

const define = (args['define'] || {}) as Record<string, string>;
Expand Down Expand Up @@ -686,43 +723,30 @@ export async function parseOptionsAndRunCLI(argsToParse: string[]) {
],
} as RunCLIArgs;

const cliServer = await runCLI(cliArgs);
if (cliServer === undefined) {
const cliResult = await runCLI(cliArgs);
if (typeof cliResult === 'number') {
// A one-shot command (e.g. `php`) finished with an
// explicit exit code.
return { exitCode: cliResult };
}
if (cliResult === undefined) {
// No server was started, so we are done with our work.
process.exit(0);
return { exitCode: 0 };
}

const cleanUpCliAndExit = (() => {
// Remember we are already cleaning up to preclude the possibility
// of multiple, conflicting cleanup attempts.
let promiseToCleanup: Promise<void>;

return async () => {
if (promiseToCleanup === undefined) {
promiseToCleanup = cliServer[Symbol.asyncDispose]();
}
await promiseToCleanup;
process.exit(0);
};
})();

// Playground CLI server must be killed to exit. From the terminal,
// this may occur via Ctrl+C which sends SIGINT. Let's handle both
// SIGINT and SIGTERM (the default kill signal) to make sure we
// clean up after ourselves even if this process is being killed.
// NOTE: Windows does not support SIGTERM, but Node.js provides some emulation.
process.on('SIGINT', cleanUpCliAndExit);
process.on('SIGTERM', cleanUpCliAndExit);

return {
[Symbol.asyncDispose]: async () => {
process.off('SIGINT', cleanUpCliAndExit);
process.off('SIGTERM', cleanUpCliAndExit);
await cliServer[Symbol.asyncDispose]();
},
[internalsKeyForTesting]: { cliServer },
[Symbol.asyncDispose]: () => cliResult[Symbol.asyncDispose](),
[internalsKeyForTesting]: { cliServer: cliResult },
};
} catch (e) {
// Validation errors have already been reported to the
// user (e.g. by the yargs .fail() handler). Convert them
// to a structured result instead of re-throwing.
if (e instanceof CLIArgsValidationError) {
return { exitCode: e.exitCode };
}
// Unexpected error — log details and re-throw so the
// caller (cli.ts) can exit with a non-zero code.
console.error(e);
const debug = process.argv.includes('--debug');
if (e instanceof Error) {
Expand All @@ -736,15 +760,13 @@ export async function parseOptionsAndRunCLI(argsToParse: string[]) {
currentError = currentError.cause as Error;
} while (currentError instanceof Error);
console.error(
'\x1b[1m' +
messageChain.join(' caused by: ') +
'\x1b[0m'
'\x1b[1m' + messageChain.join(' caused by: ') + '\x1b[0m'
);
}
} else {
console.error('\x1b[1m' + describeError(e) + '\x1b[0m');
}
process.exit(1);
throw e;
Comment thread
gcsecsey marked this conversation as resolved.
}
}

Expand Down Expand Up @@ -915,16 +937,23 @@ const highlight = (text: string) =>
export { mergeDefinedConstants } from './defines';

export async function runCLI(
args: RunCLIArgs & { command: 'build-snapshot' | 'run-blueprint' | 'php' }
args: RunCLIArgs & { command: 'build-snapshot' | 'run-blueprint' }
): Promise<void>;
export async function runCLI(
args: RunCLIArgs & { command: 'php' }
): Promise<number>;
export async function runCLI(
args: RunCLIArgs & { command: 'start' }
): Promise<RunCLIServer>;
export async function runCLI(
args: RunCLIArgs & { command: 'server' }
): Promise<RunCLIServer>;
export async function runCLI(args: RunCLIArgs): Promise<RunCLIServer | void>;
export async function runCLI(args: RunCLIArgs): Promise<RunCLIServer | void> {
export async function runCLI(
args: RunCLIArgs
): Promise<RunCLIServer | number | void>;
export async function runCLI(
args: RunCLIArgs
): Promise<RunCLIServer | number | void> {
let playgroundPool: Pooled<PlaygroundCliWorker>;
const cookieStore = args.internalCookieStore
? new HttpCookieStore()
Expand Down Expand Up @@ -1562,10 +1591,11 @@ export async function runCLI(args: RunCLIArgs): Promise<RunCLIServer | void> {
),
]);
await disposeCLI();
// stdout and stderr streams are drained above,
// but we use process.exit as a hard cut-off to ensure
// Node doesn't hang on open handles.
process.exit(exitCode);
// Return the exit code so the entry-point
// can call process.exit() as a hard cut-off
// to ensure Node doesn't hang on open
// handles.
return exitCode;
}
}

Expand Down Expand Up @@ -1674,11 +1704,15 @@ export async function runCLI(args: RunCLIArgs): Promise<RunCLIServer | void> {
return response;
},
}).catch((error) => {
cliOutput.printError(describeError(error));
process.exit(1);
throw error;
});

if (server && args.command === 'start' && !args.skipBrowser) {
if (
server &&
typeof server === 'object' &&
args.command === 'start' &&
!args.skipBrowser
) {
openInBrowser(server.serverUrl);
}
return server;
Expand Down Expand Up @@ -1777,7 +1811,9 @@ function expandStartCommandArgs(
console.log(
`You may still remove the site's directory manually if you wish.`
);
process.exit(1);
throw new Error(
'This site is not managed by Playground CLI and cannot be reset.'
);
}
}

Expand Down
7 changes: 5 additions & 2 deletions packages/playground/cli/src/start-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ const exec = promisify(execCb);

export interface ServerOptions {
port: number;
onBind: (server: Server, port: number) => Promise<RunCLIServer | void>;
onBind: (
server: Server,
port: number
) => Promise<RunCLIServer | number | void>;
/**
* Handler for requests. Always returns StreamedPHPResponse.
*/
Expand All @@ -36,7 +39,7 @@ export function isPortInUse(port: number): Promise<boolean> {

export async function startServer(
options: ServerOptions
): Promise<RunCLIServer | void> {
): Promise<RunCLIServer | number | void> {
const app = express();

const server = await new Promise<
Expand Down
Loading
Loading