diff --git a/src/bootstraps/initiate_proc_manager.ts b/src/bootstraps/initiate_proc_manager.ts index 5e6eb8b..80c847f 100644 --- a/src/bootstraps/initiate_proc_manager.ts +++ b/src/bootstraps/initiate_proc_manager.ts @@ -1,3 +1,4 @@ +import tty from 'tty'; import { NotiosConfig } from '../../libs/notios-config/src/interfaces/notios-config'; import type { UiOptions } from '../interfaces/ui_options'; import detectNpmClient from '../utils/detect_npm_client'; @@ -9,8 +10,14 @@ export interface InitiateProcManagerParams { notiosConfig: NotiosConfig; } const initiateProcManager = ({ uiOptions, notiosConfig }: InitiateProcManagerParams): ProcManager => { + const isColorSupported = + !('NO_COLOR' in process.env || uiOptions.forceNoColor) && + ('FORCE_COLOR' in process.env || + process.platform === 'win32' || + (tty.isatty(1) && process.env.TERM !== 'dumb') || + 'CI' in process.env); const procManager = createProcManager({ - forceNoColor: uiOptions.forceNoColor, + isColorSupported, enableUnreadMarker: notiosConfig.v1.enableUnreadMarker, historyAlwaysKeepHeadSize: notiosConfig.v1.historyAlwaysKeepHeadSize, historyCacheSize: notiosConfig.v1.historyCacheSize, diff --git a/src/utils/proc_manager.ts b/src/utils/proc_manager.ts index c8f4b53..18cd8b7 100644 --- a/src/utils/proc_manager.ts +++ b/src/utils/proc_manager.ts @@ -1,12 +1,29 @@ import { AnsiParser } from 'ansi-parser/interfaces/ansi-parser'; import { decodeAnsiBytes, defaultAnsiParser } from 'ansi-parser/src'; -import type childProcess from 'child_process'; import cp from 'cross-spawn'; -import tty from 'tty'; +import ttyGlobal from 'tty'; import { envVarNames } from '../constants/ipc'; -import crossKill from './cross_kill'; +import crossKillGlobal from './cross_kill'; import { applyActionToSty, defaultStyContext, restoreSty, StyContext } from './sty'; +interface CrossSpawnParams { + command: string; + args: string[]; + cwd?: string; + env?: NodeJS.ProcessEnv | undefined; +} +type CrossProcessReadable = { + readonly on: (name: 'data', handler: (buf: Buffer) => void) => void; + readonly off: (name: 'data', handler: (buf: Buffer) => void) => void; +}; +type CrossProcess = { + readonly pid?: number | undefined; + readonly once: (name: 'exit', handler: (exitCode: number | null) => void) => void; + readonly stdout: CrossProcessReadable; + readonly stderr: CrossProcessReadable; +}; +export type CrossSpawn = (params: CrossSpawnParams) => CrossProcess; + export type ProcStatus = 'waiting' | 'running' | 'killed' | 'finished'; export interface ProcOwn { @@ -19,7 +36,8 @@ export interface ProcOwnInternal { command: string; cwd: string; npmPath: string; - $raw?: childProcess.ChildProcessWithoutNullStreams; + // $raw?: childProcess.ChildProcessWithoutNullStreams; + $raw?: CrossProcess; } export interface LogOwn { @@ -168,24 +186,35 @@ export const createEmptyLogAccumulated = (): LogAccumulated => { return $createEmptyLogAccumulated(); }; +const crossSpawnGlobal: CrossSpawn = ({ env, cwd, args, command }) => { + return cp.spawn(command, args, { + stdio: ['pipe', 'pipe', 'pipe'], + env, + cwd, + }); +}; + export interface CreateProcManagerParams { - forceNoColor: boolean; + isColorSupported: boolean; enableUnreadMarker: boolean; historyAlwaysKeepHeadSize: number; historyCacheSize: number; + + // for test + tty?: typeof ttyGlobal; + crossSpawn?: CrossSpawn; + crossKill?: typeof crossKillGlobal; } export const createProcManager = ({ - forceNoColor, + isColorSupported, enableUnreadMarker, historyAlwaysKeepHeadSize, historyCacheSize, + crossKill: crossKill0, + crossSpawn: crossSpawn0, }: CreateProcManagerParams): ProcManager => { - const isColorSupported = - !('NO_COLOR' in process.env || forceNoColor) && - ('FORCE_COLOR' in process.env || - process.platform === 'win32' || - (tty.isatty(1) && process.env.TERM !== 'dumb') || - 'CI' in process.env); + const crossSpawn: CrossSpawn = crossSpawn0 ?? crossSpawnGlobal; + const crossKill = crossKill0 ?? crossKillGlobal; const RESET = isColorSupported ? '\x1b[0m' : ''; const YELLOW = isColorSupported ? '\x1b[33m' : ''; @@ -496,8 +525,10 @@ export const createProcManager = ({ nodeStderr.parent = node; $notifyUpdate(); - const p = cp.spawn(node.procOwn.npmPath, ['run', node.name], { - stdio: ['pipe', 'pipe', 'pipe'], + const p = crossSpawn({ + command: node.procOwn.npmPath, + args: ['run', node.name], + // stdio: ['pipe', 'pipe', 'pipe'], cwd: node.procOwn.cwd, env: { ...process.env, diff --git a/test/proc_manager/test.ts b/test/proc_manager/test.ts new file mode 100644 index 0000000..e1b292b --- /dev/null +++ b/test/proc_manager/test.ts @@ -0,0 +1,86 @@ +import { describe, expect, test } from 'vitest'; +import { createProcManager, LogLinesReadonly } from '../../src/utils/proc_manager'; + +const SCRIPT_NAME_1 = 'test1'; +const SCRIPT_COMMAND_1 = 'cmd1'; +const SCRIPT_NAME_2 = 'test2'; +const SCRIPT_COMMAND_2 = 'cmd2'; +const SCRIPT_NAME_3 = 'test3'; +const SCRIPT_COMMAND_3 = 'cmd3'; + +const fakeCwd = '/fakeCwd'; +const fakeNpmClient = 'fake-npm'; + +const logLinesToStrLines = (logLines: LogLinesReadonly): string[] => { + // TODO +}; + +describe('todo', () => { + test('todo1', () => { + const c = createXXX(); + c.addFakeProc(SCRIPT_NAME_1, (h) => [h.strLn('1abc1'), h.strLn('1def1'), h.end()]); + c.addFakeProc(SCRIPT_NAME_2, (h) => [h.strLn('2abc2'), h.strLn('2def2'), h.end()]); + c.addFakeProc(SCRIPT_NAME_3, (h) => [h.bufLn(Buffer.from([0x03, 0x13, 0x23, 0x33, 0x43])), h.end()]); + const pm = createProcManager({ + historyCacheSize: 3, + historyAlwaysKeepHeadSize: 3, + enableUnreadMarker: true, + isColorSupported: true, + + crossKill: c.fakeCrossKill, + crossSpawn: c.fakeCrossSpawn, + }); + pm.createNode({ + name: SCRIPT_NAME_1, + type: 'none', + procOwn: { + command: SCRIPT_COMMAND_1, + cwd: fakeCwd, + npmPath: fakeNpmClient, + }, + status: 'running', + }); + c.tick(); + expect(pm.rootNode.logAccumulated.lineCount).toBe(2); + expect(logLinesToStrLines(pm.rootNode.logAccumulated.lines)).toBe(['1abc1', '1def1']); + expect(c.calls).toBe([ + { + name: SCRIPT_NAME_1, + command: SCRIPT_COMMAND_1, + cwd: fakeCwd, + npmPath: fakeNpmClient, + }, + ]); + }); + + test('todo2', () => { + const fakeCwd = '/fakeCwd'; + const fakeNpmClient = 'fake-npm'; + const c = createXXX(); + c.addFakeProc(SCRIPT_NAME_1, (h) => [h.strLn('1abc1'), h.strLn('1def1'), h.end()]); + c.addFakeProc(SCRIPT_NAME_2, (h) => [h.strLn('2abc2'), h.strLn('2def2'), h.end()]); + c.addFakeProc(SCRIPT_NAME_3, (h) => [h.bufLn(Buffer.from([0x03, 0x13, 0x23, 0x33, 0x43])), h.end()]); + const pm = createProcManager({ + historyCacheSize: 3, + historyAlwaysKeepHeadSize: 3, + enableUnreadMarker: true, + isColorSupported: true, + + crossKill: c.fakeCrossKill, + crossSpawn: c.fakeCrossSpawn, + }); + pm.createNode({ + name: SCRIPT_NAME_3, + type: 'none', + procOwn: { + command: SCRIPT_COMMAND_3, + cwd: fakeCwd, + npmPath: fakeNpmClient, + }, + status: 'running', + }); + c.tick(); + expect(pm.rootNode.logAccumulated.lineCount).toBe(2); + expect(logLinesToStrLines(pm.rootNode.logAccumulated.lines)).toBe(['\u0023\u0033\u0043']); + }); +});