Skip to content

Commit

Permalink
change the way of interacting with cli-server, write to the client tt…
Browse files Browse the repository at this point in the history
…y directly on mac/linux (#9104)

Linux OS family treat the tty (terminal) as any other file, so it has an
actual file path we can write into.
When a client runs a bit command (using the env variable of enabling the
cli-server) it sends its tty path to the server. The server (in the
cli-raw route) monkey patches (no choice here) the
`process.stdout.write` and adds additional write - to the tty path. This
way, the client gets all the data stream in between the request and the
response.
  • Loading branch information
davidfirst authored Aug 8, 2024
1 parent 32f9def commit eba5fab
Show file tree
Hide file tree
Showing 6 changed files with 64 additions and 19 deletions.
2 changes: 1 addition & 1 deletion scopes/dependencies/pnpm/pnpm.package-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export class PnpmPackageManager implements PackageManager {
hidePackageManagerOutput: installOptions.hidePackageManagerOutput,
reportOptions: {
appendOnly: installOptions.optimizeReportForNonTerminal,
process: process.env.BIT_SERVER_RUNNING ? { ...process, stdout: new ServerSendOutStream() } : undefined,
process: process.env.BIT_CLI_SERVER_NO_TTY ? { ...process, stdout: new ServerSendOutStream() } : undefined,
throttleProgress: installOptions.throttleProgress,
hideProgressPrefix: installOptions.hideProgressPrefix,
hideLifecycleOutput: installOptions.hideLifecycleOutput,
Expand Down
1 change: 0 additions & 1 deletion scopes/harmony/api-server/api-server.main.runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,6 @@ export class ApiServerMain {
server.on('listening', () => {
this.logger.consoleSuccess(`Bit Server is listening on port ${port}`);
this.writeUsedPort(port);
process.env.BIT_SERVER_RUNNING = 'true';
resolve(port);
});
});
Expand Down
50 changes: 43 additions & 7 deletions scopes/harmony/api-server/cli-raw.route.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { CLIMain, CLIParser, YargsExitWorkaround } from '@teambit/cli';
import fs from 'fs-extra';
import chalk from 'chalk';
import { Route, Request, Response } from '@teambit/express';
import { Logger } from '@teambit/logger';
import legacyLogger, { getLevelFromArgv } from '@teambit/legacy/dist/logger/logger';
import { reloadFeatureToggle } from '@teambit/harmony.modules.feature-toggle';
import loader from '@teambit/legacy/dist/cli/loader';
import { APIForIDE } from './api-for-ide';

/**
Expand All @@ -23,11 +25,44 @@ export class CLIRawRoute implements Route {

middlewares = [
async (req: Request, res: Response) => {
const { command, pwd, envBitFeatures } = req.body;
const { command, pwd, envBitFeatures, ttyPath } = req.body;
this.logger.debug(`cli-raw server: got request for ${command}`);
if (pwd && !process.cwd().startsWith(pwd)) {
throw new Error(`bit-server is running on a different directory. bit-server: ${process.cwd()}, pwd: ${pwd}`);
}

// save the original process.stdout.write method
const originalStdoutWrite = process.stdout.write;
const originalStderrWrite = process.stderr.write;

if (ttyPath) {
const fileHandle = await fs.open(ttyPath, 'w');
// @ts-ignore monkey patch the process stdout write method
process.stdout.write = (chunk, encoding, callback) => {
fs.writeSync(fileHandle, chunk.toString());
return originalStdoutWrite.call(process.stdout, chunk, encoding, callback);
};
// @ts-ignore monkey patch the process stderr write method
process.stderr.write = (chunk, encoding, callback) => {
fs.writeSync(fileHandle, chunk.toString());
return originalStderrWrite.call(process.stdout, chunk, encoding, callback);
};
} else {
process.env.BIT_CLI_SERVER_NO_TTY = 'true';
loader.shouldSendServerEvents = true;
}

let currentLogger;
const levelFromArgv = getLevelFromArgv(command);
if (levelFromArgv) {
currentLogger = legacyLogger.logger;
if (ttyPath) {
legacyLogger.switchToConsoleLogger(levelFromArgv);
} else {
legacyLogger.switchToSSELogger(levelFromArgv);
}
}

const currentBitFeatures = process.env.BIT_FEATURES;
const shouldReloadFeatureToggle = currentBitFeatures !== envBitFeatures;
if (shouldReloadFeatureToggle) {
Expand All @@ -40,12 +75,6 @@ export class CLIRawRoute implements Route {
const cmdStrLog = `${randomNumber} ${commandStr}`;
await this.apiForIDE.logStartCmdHistory(cmdStrLog);
legacyLogger.isDaemon = true;
let currentLogger;
const levelFromArgv = getLevelFromArgv(command);
if (levelFromArgv) {
currentLogger = legacyLogger.logger;
legacyLogger.switchToSSELogger(levelFromArgv);
}
enableChalk();
const cliParser = new CLIParser(this.cli.commands, this.cli.groups, this.cli.onCommandStartSlot);
try {
Expand All @@ -66,6 +95,13 @@ export class CLIRawRoute implements Route {
});
}
} finally {
if (ttyPath) {
process.stdout.write = originalStdoutWrite;
process.stderr.write = originalStderrWrite;
} else {
delete process.env.BIT_CLI_SERVER_NO_TTY;
loader.shouldSendServerEvents = false;
}
this.logger.clearStatusLine();
// change chalk back to false, otherwise, the IDE will have colors. (this is a global setting)
chalk.enabled = false;
Expand Down
12 changes: 10 additions & 2 deletions scopes/harmony/bit/server-commander.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import fetch from 'node-fetch';
import fs from 'fs-extra';
import { execSync } from 'child_process';
import { join } from 'path';
import EventSource from 'eventsource';
import { findScopePath } from '@teambit/scope.modules.find-scope-path';
Expand Down Expand Up @@ -52,17 +53,24 @@ export class ServerCommander {

async runCommandWithHttpServer(): Promise<CommandResult | undefined> {
printBitVersionIfAsked();
const isWindows = process.platform === 'win32';
const port = await this.getExistingUsedPort();
const url = `http://localhost:${port}/api`;
this.initSSE(url);
const ttyPath = isWindows
? undefined
: execSync('tty', {
encoding: 'utf8',
stdio: ['inherit', 'pipe', 'pipe'],
}).trim();
if (!ttyPath) this.initSSE(url);
// parse the args and options from the command
const args = process.argv.slice(2);
if (!args.includes('--json') && !args.includes('-j')) {
loader.on();
}
const endpoint = `cli-raw`;
const pwd = process.cwd();
const body = { command: args, pwd, envBitFeatures: process.env.BIT_FEATURES };
const body = { command: args, pwd, envBitFeatures: process.env.BIT_FEATURES, ttyPath };
let res;
try {
res = await fetch(`${url}/${endpoint}`, {
Expand Down
15 changes: 8 additions & 7 deletions src/cli/loader/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { sendEventsToClients } from '@teambit/harmony.modules.send-server-sent-e
const SPINNER_TYPE = platform() === 'win32' ? cliSpinners.dots : cliSpinners.dots12;

export class Loader {
shouldSendServerEvents = false;
private spinner: Ora | null;

get isStarted() {
Expand All @@ -25,7 +26,7 @@ export class Loader {
}

off(): Loader {
sendEventsToClients('onLoader', { method: 'off' });
if (this.shouldSendServerEvents) sendEventsToClients('onLoader', { method: 'off' });
this.stop();
this.spinner = null;
return this;
Expand All @@ -44,7 +45,7 @@ export class Loader {
}

setTextAndRestart(text: string): Loader {
sendEventsToClients('onLoader', { method: 'setTextAndRestart', args: [text] });
if (this.shouldSendServerEvents) sendEventsToClients('onLoader', { method: 'setTextAndRestart', args: [text] });
if (this.spinner) {
this.spinner.stop();
this.spinner.text = text;
Expand All @@ -58,13 +59,13 @@ export class Loader {
}

stop(): Loader {
sendEventsToClients('onLoader', { method: 'stop' });
if (this.shouldSendServerEvents) sendEventsToClients('onLoader', { method: 'stop' });
if (this.spinner) this.spinner.stop();
return this;
}

succeed(text?: string, startTime?: [number, number]): Loader {
sendEventsToClients('onLoader', { method: 'succeed', args: [text, startTime] });
if (this.shouldSendServerEvents) sendEventsToClients('onLoader', { method: 'succeed', args: [text, startTime] });
if (text && startTime) {
const duration = process.hrtime(startTime);
text = `${text} (completed in ${prettyTime(duration)})`;
Expand All @@ -74,13 +75,13 @@ export class Loader {
}

fail(text?: string): Loader {
sendEventsToClients('onLoader', { method: 'fail', args: [text] });
if (this.shouldSendServerEvents) sendEventsToClients('onLoader', { method: 'fail', args: [text] });
if (this.spinner) this.spinner.fail(text);
return this;
}

warn(text?: string): Loader {
sendEventsToClients('onLoader', { method: 'warn', args: [text] });
if (this.shouldSendServerEvents) sendEventsToClients('onLoader', { method: 'warn', args: [text] });
if (this.spinner) this.spinner.warn(text);
return this;
}
Expand All @@ -91,7 +92,7 @@ export class Loader {
}

stopAndPersist(options?: PersistOptions): Loader {
sendEventsToClients('onLoader', { method: 'stopAndPersist', args: [options] });
if (this.shouldSendServerEvents) sendEventsToClients('onLoader', { method: 'stopAndPersist', args: [options] });
if (this.spinner) this.spinner.stopAndPersist(options);
return this;
}
Expand Down
3 changes: 2 additions & 1 deletion src/logger/pino-logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@ export function getPinoLoggerWithoutWorkers(

const prettyConsoleStream = prettifier({
...prettyOptionsConsole,
destination: 1,
// it's important to use process.stdout here (and not "1"), otherwise, for cli-server when monkey patching the stdout it won't work
destination: process.stdout,
sync: true,
});

Expand Down

0 comments on commit eba5fab

Please sign in to comment.