@lvce-editor/pty-host
Version:
Terminal process used in Lvce Editor.
531 lines (499 loc) • 12.1 kB
JavaScript
import { register, execute } from '@lvce-editor/command';
import { IpcChildWithWebSocket, IpcChildWithElectronMessagePort, IpcChildWithElectronUtilityProcess, IpcChildWithNodeForkedProcess } from '@lvce-editor/ipc';
import * as JsonRpc from '@lvce-editor/json-rpc';
import { handleJsonRpcMessage, resolve } from '@lvce-editor/json-rpc';
import debugModule from 'debug';
import { spawn } from 'node-pty';
import { VError } from '@lvce-editor/verror';
import { Console } from 'node:console';
import { createWriteStream } from 'node:fs';
import { tmpdir } from 'node:os';
import { prepare } from '@lvce-editor/pretty-error';
const createRpc = ipc => {
const rpc = {
// @ts-ignore
ipc,
/**
* @deprecated
*/
send(method, ...params) {
JsonRpc.send(ipc, method, ...params);
},
invoke(method, ...params) {
return JsonRpc.invoke(ipc, method, ...params);
},
invokeAndTransfer(method, ...params) {
return JsonRpc.invokeAndTransfer(ipc, method, ...params);
},
async dispose() {
await ipc?.dispose();
}
};
return rpc;
};
const requiresSocket$1 = () => {
return false;
};
const preparePrettyError = error => {
return error;
};
const logError = () => {
// handled by renderer worker
};
const handleMessage = event => {
const actualRequiresSocket = event?.target?.requiresSocket || requiresSocket$1;
const actualExecute = event?.target?.execute || execute;
return handleJsonRpcMessage(event.target, event.data, actualExecute, resolve, preparePrettyError, logError, actualRequiresSocket);
};
const handleIpc = ipc => {
if ('addEventListener' in ipc) {
ipc.addEventListener('message', handleMessage);
} else if ('on' in ipc) {
// deprecated
ipc.on('message', handleMessage);
}
};
const listen$2 = async (module, options) => {
const rawIpc = await module.listen(options);
if (module.signal) {
module.signal(rawIpc);
}
const ipc = module.wrap(rawIpc);
return ipc;
};
const create$e = async ({
commandMap,
messagePort,
requiresSocket
}) => {
// TODO create a commandMap per rpc instance
register(commandMap);
const ipc = await listen$2(IpcChildWithElectronMessagePort, {
messagePort
});
if (requiresSocket) {
ipc.requiresSocket = requiresSocket;
}
handleIpc(ipc);
const rpc = createRpc(ipc);
return rpc;
};
const ElectronMessagePortRpcClient = {
__proto__: null,
create: create$e
};
const create$d = async ({
commandMap
}) => {
// TODO create a commandMap per rpc instance
register(commandMap);
const ipc = await listen$2(IpcChildWithElectronUtilityProcess);
handleIpc(ipc);
const rpc = createRpc(ipc);
return rpc;
};
const ElectronUtilityProcessRpcClient = {
__proto__: null,
create: create$d
};
const create$7 = async ({
commandMap
}) => {
// TODO create a commandMap per rpc instance
register(commandMap);
const ipc = await listen$2(IpcChildWithNodeForkedProcess);
handleIpc(ipc);
const rpc = createRpc(ipc);
return rpc;
};
const NodeForkedProcessRpcClient = {
__proto__: null,
create: create$7
};
const create$5 = async ({
commandMap,
request,
handle,
requiresSocket
}) => {
// TODO create a commandMap per rpc instance
register(commandMap);
const ipc = await listen$2(IpcChildWithWebSocket, {
request,
handle
});
if (requiresSocket) {
ipc.requiresSocket = requiresSocket;
}
handleIpc(ipc);
const rpc = createRpc(ipc);
return rpc;
};
const NodeWebSocketRpcClient = {
__proto__: null,
create: create$5
};
class AssertionError extends Error {
constructor(message) {
super(message);
this.name = 'AssertionError';
}
}
const getType = value => {
switch (typeof value) {
case 'number':
return 'number';
case 'function':
return 'function';
case 'string':
return 'string';
case 'object':
if (value === null) {
return 'null';
}
if (Array.isArray(value)) {
return 'array';
}
return 'object';
case 'boolean':
return 'boolean';
default:
return 'unknown';
}
};
const object = value => {
const type = getType(value);
if (type !== 'object') {
throw new AssertionError('expected value to be of type object');
}
};
const number = value => {
const type = getType(value);
if (type !== 'number') {
throw new AssertionError('expected value to be of type number');
}
};
const array = value => {
const type = getType(value);
if (type !== 'array') {
throw new AssertionError('expected value to be of type array');
}
};
const string = value => {
const type = getType(value);
if (type !== 'string') {
throw new AssertionError('expected value to be of type string');
}
};
const METHODS_THAT_REQUIRE_SOCKET = new Set(['Terminal.create']);
const requiresSocket = method => {
return METHODS_THAT_REQUIRE_SOCKET.has(method);
};
const handleElectronMessagePort = async messagePort => {
object(messagePort);
await ElectronMessagePortRpcClient.create({
commandMap: {},
messagePort,
requiresSocket: requiresSocket
});
};
const NodeWorker = 1;
const NodeForkedProcess = 2;
const WebSocket = 3;
const ElectronUtilityProcess = 4;
const ElectronMessagePort = 5;
const NodeMessagePort = 6;
const Auto = () => {
const {
argv
} = process;
if (argv.includes('--ipc-type=node-worker')) {
return NodeWorker;
}
if (argv.includes('--ipc-type=node-forked-process')) {
return NodeForkedProcess;
}
if (argv.includes('--ipc-type=electron-utility-process')) {
return ElectronUtilityProcess;
}
throw new Error(`[pty-host] unknown ipc type`);
};
const getModule = method => {
switch (method) {
case NodeForkedProcess:
return NodeForkedProcessRpcClient;
case WebSocket:
return NodeWebSocketRpcClient;
case ElectronUtilityProcess:
return ElectronUtilityProcessRpcClient;
case ElectronMessagePort:
return ElectronMessagePortRpcClient;
default:
throw new Error('unexpected ipc type');
}
};
const listen$1 = async ({
method,
...params
}) => {
const module = getModule(method);
// @ts-ignore
const rpc = await module.create(params);
return rpc;
};
const handleNodeMessagePort = async messagePort => {
await listen$1({
method: NodeMessagePort,
messagePort
});
};
const handleWebSocket = async (handle, request) => {
object(handle);
object(request);
await NodeWebSocketRpcClient.create({
commandMap: {},
handle,
request,
requiresSocket: requiresSocket
});
};
const debug = debugModule('pty-host');
class DataEvent extends Event {
constructor(data) {
super('data');
this.data = data;
}
}
class ExitEvent extends Event {
constructor(data) {
super('exit');
this.data = data;
}
}
class Pty extends EventTarget {
/**
*
* @param {import('node-pty').IPty} pty
*/
constructor(pty) {
super();
this.pty = pty;
const handleData = data => {
this.dispatchEvent(new DataEvent(data));
};
const handleExit = data => {
this.dispatchEvent(new ExitEvent(data));
};
this.pty.onData(handleData);
this.pty.onExit(handleExit);
}
resize(columns, rows) {
this.pty.resize(columns, rows);
}
dispose() {
this.pty.kill();
}
write(data) {
this.pty.write(data);
}
}
/**
*
* @param {*} param0
* @returns {any}
*/
const create$1 = ({
cwd,
command,
args
} = {}) => {
try {
string(cwd);
string(command);
array(args);
const pty = spawn(command, args, {
encoding: null,
cwd
// cols: 10,
// rows: 10,
});
const wrapped = new Pty(pty);
return wrapped;
} catch (error) {
throw new VError(error, `Failed to create terminal`);
}
};
const state$1 = {
ptyMap: Object.create(null)
};
const set = (id, pty) => {
state$1.ptyMap[id] = pty;
};
const get = id => {
return state$1.ptyMap[id];
};
const remove = id => {
delete state$1.ptyMap[id];
};
// TODO maybe merge pty and pty controller
const create = (ipc, id, cwd, command, args) => {
number(id);
string(cwd);
string(command);
array(args);
object(ipc);
debug(`create ${id} ${cwd}`);
// @ts-ignore
const pty = create$1({
cwd,
command,
args
});
const handleData = event => {
ipc.send({
jsonrpc: '2.0',
method: 'Viewlet.send',
params: [id, 'handleData', event.data]
});
};
pty.addEventListener('data', handleData);
set(id, pty);
};
const write = (id, data) => {
const pty = get(id);
if (!pty) {
throw new Error(`pty ${id} not found`);
}
pty.write(data);
};
const resize = (id, columns, rows) => {
const pty = get(id);
if (!pty) {
throw new Error(`pty ${id} not found`);
}
pty.resize(columns, rows);
};
const dispose = id => {
const pty = get(id);
if (!pty) {
throw new Error(`pty ${id} not found`);
}
pty.dispose();
remove(id);
};
const HandleElectronMessagePort = 'HandleElectronMessagePort.handleElectronMessagePort';
const HandleNodeMessagePort = 'HandleNodeMessagePort.handleNodeMessagePort';
const HandleWebSocket = 'HandleWebSocket.handleWebSocket';
const TerminalCreate = 'Terminal.create';
const TerminalResize = 'Terminal.resize';
const TerminalWrite = 'Terminal.write';
const TerminalDispose = 'Terminal.dispose';
const commandMap = {
[HandleElectronMessagePort]: handleElectronMessagePort,
[HandleNodeMessagePort]: handleNodeMessagePort,
[HandleWebSocket]: handleWebSocket,
[TerminalCreate]: create,
[TerminalResize]: resize,
[TerminalWrite]: write,
[TerminalDispose]: dispose
};
const listen = async () => {
await listen$1({
method: Auto()
});
};
const NewLine = '\n';
const Error$1 = 1;
const Success = 0;
/**
*
* @param {string} string
* @param {number|undefined} startIndex
* @returns
*/
const getNewLineIndex = (string, startIndex = undefined) => {
return string.indexOf(NewLine, startIndex);
};
const EPIPE = 'EPIPE';
const ERR_IPC_CHANNEL_CLOSED = 'ERR_IPC_CHANNEL_CLOSED';
const exit = code => {
process.exit(code);
};
const setExitCode = exitCode => {
process.exitCode = exitCode;
};
const isConnected = () => {
return process.connected;
};
const isIgnoredError = error => {
if (error && error.code === EPIPE && !process.connected) {
// parent process is disposed, ignore
return true;
}
if (error && error.code === ERR_IPC_CHANNEL_CLOSED && !isConnected()) {
// parent process is disposed, ignore
return true;
}
return false;
};
// TODO mock this module when used in unit tests
const state = {
console: undefined
};
const createConsole = () => {
const logFile = `${tmpdir()}/log-terminal-process.txt`;
const writeStream = createWriteStream(logFile);
const logger = new Console(writeStream);
return logger;
};
const getOrCreateLogger = () => {
if (!state.console) {
state.console = createConsole();
}
return state.console;
};
const info = (...args) => {
const logger = getOrCreateLogger();
logger.info(...args);
console.info(...args);
};
const error = (...args) => {
const logger = getOrCreateLogger();
logger.error(...args);
console.error(...args);
};
const firstErrorLine = error => {
if (error.stack) {
return error.stack.slice(0, getNewLineIndex(error.stack));
}
if (error.message) {
return error.message;
}
return `${error}`;
};
const handleUncaughtExceptionMonitor$1 = error$1 => {
info(`[pty host] uncaught exception: ${firstErrorLine(error$1)}`);
if (isIgnoredError(error$1)) {
return;
}
const prettyError = prepare(error$1);
error(
// @ts-ignore
prettyError.codeFrame + NewLine + prettyError.stack + NewLine);
setExitCode(Error$1);
};
const handleDisconnect = () => {
console.info('[pty host] disconnected');
exit(Success);
};
const handleUncaughtExceptionMonitor = error => {
// @ts-ignore
handleUncaughtExceptionMonitor$1(error);
};
const main = async () => {
process.on('uncaughtExceptionMonitor', handleUncaughtExceptionMonitor);
process.on('disconnect', handleDisconnect);
register(commandMap);
await listen();
};
main();