mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
BIN
src/native/pty/darwin.win64/pty.node
Normal file
BIN
src/native/pty/darwin.win64/pty.node
Normal file
Binary file not shown.
BIN
src/native/pty/darwin.win64/spawn-helper
Normal file
BIN
src/native/pty/darwin.win64/spawn-helper
Normal file
Binary file not shown.
BIN
src/native/pty/darwin.x64/pty.node
Normal file
BIN
src/native/pty/darwin.x64/pty.node
Normal file
Binary file not shown.
BIN
src/native/pty/darwin.x64/spawn-helper
Normal file
BIN
src/native/pty/darwin.x64/spawn-helper
Normal file
Binary file not shown.
BIN
src/native/pty/linux.arm64/pty.node
Normal file
BIN
src/native/pty/linux.arm64/pty.node
Normal file
Binary file not shown.
BIN
src/native/pty/linux.x64/pty.node
Normal file
BIN
src/native/pty/linux.x64/pty.node
Normal file
Binary file not shown.
BIN
src/native/pty/win32.x64/conpty.node
Normal file
BIN
src/native/pty/win32.x64/conpty.node
Normal file
Binary file not shown.
BIN
src/native/pty/win32.x64/conpty_console_list.node
Normal file
BIN
src/native/pty/win32.x64/conpty_console_list.node
Normal file
Binary file not shown.
BIN
src/native/pty/win32.x64/pty.node
Normal file
BIN
src/native/pty/win32.x64/pty.node
Normal file
Binary file not shown.
BIN
src/native/pty/win32.x64/winpty-agent.exe
Normal file
BIN
src/native/pty/win32.x64/winpty-agent.exe
Normal file
Binary file not shown.
BIN
src/native/pty/win32.x64/winpty.dll
Normal file
BIN
src/native/pty/win32.x64/winpty.dll
Normal file
Binary file not shown.
33
src/pty/index.ts
Normal file
33
src/pty/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { ITerminal, IPtyOpenOptions, IPtyForkOptions, IWindowsPtyForkOptions } from '@homebridge/node-pty-prebuilt-multiarch/src/interfaces';
|
||||
import type { ArgvOrCommandLine } from '@homebridge/node-pty-prebuilt-multiarch/src/types';
|
||||
import { WindowsTerminal } from './windowsTerminal';
|
||||
import { UnixTerminal } from './unixTerminal';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import path, { dirname } from 'node:path';
|
||||
|
||||
let terminalCtor: typeof WindowsTerminal | typeof UnixTerminal;
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
terminalCtor = WindowsTerminal;
|
||||
} else {
|
||||
terminalCtor = UnixTerminal;
|
||||
}
|
||||
|
||||
export function spawn(file?: string, args?: ArgvOrCommandLine, opt?: IPtyForkOptions | IWindowsPtyForkOptions): ITerminal {
|
||||
return new terminalCtor(file, args, opt);
|
||||
}
|
||||
|
||||
export function open(options: IPtyOpenOptions): ITerminal {
|
||||
return terminalCtor.open(options) as ITerminal;
|
||||
}
|
||||
export function require_dlopen(modulename: string) {
|
||||
const module = { exports: {} };
|
||||
const import__dirname = dirname(fileURLToPath(import.meta.url));
|
||||
process.dlopen(module, path.join(import__dirname, modulename));
|
||||
return module.exports as any;
|
||||
}
|
||||
/**
|
||||
* Expose the native API when not Windows, note that this is not public API and
|
||||
* could be removed at any time.
|
||||
*/
|
||||
export const native = (process.platform !== 'win32' ? require_dlopen('./pty/' + process.platform + '.' + process.arch + '/pty.node') : null);
|
54
src/pty/native.d.ts
vendored
Normal file
54
src/pty/native.d.ts
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright (c) 2018, Microsoft Corporation (MIT License).
|
||||
*/
|
||||
|
||||
interface IConptyNative {
|
||||
startProcess(file: string, cols: number, rows: number, debug: boolean, pipeName: string, conptyInheritCursor: boolean, useConptyDll: boolean): IConptyProcess;
|
||||
connect(ptyId: number, commandLine: string, cwd: string, env: string[], onExitCallback: (exitCode: number) => void): { pid: number };
|
||||
resize(ptyId: number, cols: number, rows: number, useConptyDll: boolean): void;
|
||||
clear(ptyId: number, useConptyDll: boolean): void;
|
||||
kill(ptyId: number, useConptyDll: boolean): void;
|
||||
}
|
||||
|
||||
interface IWinptyNative {
|
||||
startProcess(file: string, commandLine: string, env: string[], cwd: string, cols: number, rows: number, debug: boolean): IWinptyProcess;
|
||||
resize(pid: number, cols: number, rows: number): void;
|
||||
kill(pid: number, innerPid: number): void;
|
||||
getProcessList(pid: number): number[];
|
||||
getExitCode(innerPid: number): number;
|
||||
}
|
||||
|
||||
interface IUnixNative {
|
||||
fork(file: string, args: string[], parsedEnv: string[], cwd: string, cols: number, rows: number, uid: number, gid: number, useUtf8: boolean, helperPath: string, onExitCallback: (code: number, signal: number) => void): IUnixProcess;
|
||||
open(cols: number, rows: number): IUnixOpenProcess;
|
||||
process(fd: number, pty?: string): string;
|
||||
resize(fd: number, cols: number, rows: number): void;
|
||||
}
|
||||
|
||||
interface IConptyProcess {
|
||||
pty: number;
|
||||
fd: number;
|
||||
conin: string;
|
||||
conout: string;
|
||||
}
|
||||
|
||||
interface IWinptyProcess {
|
||||
pty: number;
|
||||
fd: number;
|
||||
conin: string;
|
||||
conout: string;
|
||||
pid: number;
|
||||
innerPid: number;
|
||||
}
|
||||
|
||||
interface IUnixProcess {
|
||||
fd: number;
|
||||
pid: number;
|
||||
pty: string;
|
||||
}
|
||||
|
||||
interface IUnixOpenProcess {
|
||||
master: number;
|
||||
slave: number;
|
||||
pty: string;
|
||||
}
|
231
src/pty/node-pty.d.ts
vendored
Normal file
231
src/pty/node-pty.d.ts
vendored
Normal file
@@ -0,0 +1,231 @@
|
||||
/**
|
||||
* Copyright (c) 2017, Daniel Imms (MIT License).
|
||||
* Copyright (c) 2018, Microsoft Corporation (MIT License).
|
||||
*/
|
||||
|
||||
declare module '@/pty' {
|
||||
/**
|
||||
* Forks a process as a pseudoterminal.
|
||||
* @param file The file to launch.
|
||||
* @param args The file's arguments as argv (string[]) or in a pre-escaped CommandLine format
|
||||
* (string). Note that the CommandLine option is only available on Windows and is expected to be
|
||||
* escaped properly.
|
||||
* @param options The options of the terminal.
|
||||
* @see CommandLineToArgvW https://msdn.microsoft.com/en-us/library/windows/desktop/bb776391(v=vs.85).aspx
|
||||
* @see Parsing C++ Comamnd-Line Arguments https://msdn.microsoft.com/en-us/library/17w5ykft.aspx
|
||||
* @see GetCommandLine https://msdn.microsoft.com/en-us/library/windows/desktop/ms683156.aspx
|
||||
*/
|
||||
export function spawn(file: string, args: string[] | string, options: IPtyForkOptions | IWindowsPtyForkOptions): IPty;
|
||||
|
||||
export interface IBasePtyForkOptions {
|
||||
|
||||
/**
|
||||
* Name of the terminal to be set in environment ($TERM variable).
|
||||
*/
|
||||
name?: string;
|
||||
|
||||
/**
|
||||
* Number of intial cols of the pty.
|
||||
*/
|
||||
cols?: number;
|
||||
|
||||
/**
|
||||
* Number of initial rows of the pty.
|
||||
*/
|
||||
rows?: number;
|
||||
|
||||
/**
|
||||
* Working directory to be set for the child program.
|
||||
*/
|
||||
cwd?: string;
|
||||
|
||||
/**
|
||||
* Environment to be set for the child program.
|
||||
*/
|
||||
env?: { [key: string]: string | undefined };
|
||||
|
||||
/**
|
||||
* String encoding of the underlying pty.
|
||||
* If set, incoming data will be decoded to strings and outgoing strings to bytes applying this encoding.
|
||||
* If unset, incoming data will be delivered as raw bytes (Buffer type).
|
||||
* By default 'utf8' is assumed, to unset it explicitly set it to `null`.
|
||||
*/
|
||||
encoding?: string | null;
|
||||
|
||||
/**
|
||||
* (EXPERIMENTAL)
|
||||
* Whether to enable flow control handling (false by default). If enabled a message of `flowControlPause`
|
||||
* will pause the socket and thus blocking the child program execution due to buffer back pressure.
|
||||
* A message of `flowControlResume` will resume the socket into flow mode.
|
||||
* For performance reasons only a single message as a whole will match (no message part matching).
|
||||
* If flow control is enabled the `flowControlPause` and `flowControlResume` messages are not forwarded to
|
||||
* the underlying pseudoterminal.
|
||||
*/
|
||||
handleFlowControl?: boolean;
|
||||
|
||||
/**
|
||||
* (EXPERIMENTAL)
|
||||
* The string that should pause the pty when `handleFlowControl` is true. Default is XOFF ('\x13').
|
||||
*/
|
||||
flowControlPause?: string;
|
||||
|
||||
/**
|
||||
* (EXPERIMENTAL)
|
||||
* The string that should resume the pty when `handleFlowControl` is true. Default is XON ('\x11').
|
||||
*/
|
||||
flowControlResume?: string;
|
||||
}
|
||||
|
||||
export interface IPtyForkOptions extends IBasePtyForkOptions {
|
||||
/**
|
||||
* Security warning: use this option with great caution,
|
||||
* as opened file descriptors with higher privileges might leak to the child program.
|
||||
*/
|
||||
uid?: number;
|
||||
gid?: number;
|
||||
}
|
||||
|
||||
export interface IWindowsPtyForkOptions extends IBasePtyForkOptions {
|
||||
/**
|
||||
* Whether to use the ConPTY system on Windows. When this is not set, ConPTY will be used when
|
||||
* the Windows build number is >= 18309 (instead of winpty). Note that ConPTY is available from
|
||||
* build 17134 but is too unstable to enable by default.
|
||||
*
|
||||
* This setting does nothing on non-Windows.
|
||||
*/
|
||||
useConpty?: boolean;
|
||||
|
||||
/**
|
||||
* (EXPERIMENTAL)
|
||||
*
|
||||
* Whether to use the conpty.dll shipped with the node-pty package instead of the one built into
|
||||
* Windows. Defaults to false.
|
||||
*/
|
||||
useConptyDll?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to use PSEUDOCONSOLE_INHERIT_CURSOR in conpty.
|
||||
* @see https://docs.microsoft.com/en-us/windows/console/createpseudoconsole
|
||||
*/
|
||||
conptyInheritCursor?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* An interface representing a pseudoterminal, on Windows this is emulated via the winpty library.
|
||||
*/
|
||||
export interface IPty {
|
||||
/**
|
||||
* The process ID of the outer process.
|
||||
*/
|
||||
readonly pid: number;
|
||||
|
||||
/**
|
||||
* The column size in characters.
|
||||
*/
|
||||
readonly cols: number;
|
||||
|
||||
/**
|
||||
* The row size in characters.
|
||||
*/
|
||||
readonly rows: number;
|
||||
|
||||
/**
|
||||
* The title of the active process.
|
||||
*/
|
||||
readonly process: string;
|
||||
|
||||
/**
|
||||
* (EXPERIMENTAL)
|
||||
* Whether to handle flow control. Useful to disable/re-enable flow control during runtime.
|
||||
* Use this for binary data that is likely to contain the `flowControlPause` string by accident.
|
||||
*/
|
||||
handleFlowControl: boolean;
|
||||
|
||||
/**
|
||||
* Adds an event listener for when a data event fires. This happens when data is returned from
|
||||
* the pty.
|
||||
* @returns an `IDisposable` to stop listening.
|
||||
*/
|
||||
readonly onData: IEvent<string>;
|
||||
|
||||
/**
|
||||
* Adds an event listener for when an exit event fires. This happens when the pty exits.
|
||||
* @returns an `IDisposable` to stop listening.
|
||||
*/
|
||||
readonly onExit: IEvent<{ exitCode: number, signal?: number }>;
|
||||
|
||||
/**
|
||||
* Resizes the dimensions of the pty.
|
||||
* @param columns The number of columns to use.
|
||||
* @param rows The number of rows to use.
|
||||
*/
|
||||
resize(columns: number, rows: number): void;
|
||||
|
||||
// Re-added this interface as homebridge-config-ui-x leverages it https://github.com/microsoft/node-pty/issues/282
|
||||
|
||||
/**
|
||||
* Adds a listener to the data event, fired when data is returned from the pty.
|
||||
* @param event The name of the event.
|
||||
* @param listener The callback function.
|
||||
* @deprecated Use IPty.onData
|
||||
*/
|
||||
on(event: 'data', listener: (data: string) => void): void;
|
||||
|
||||
/**
|
||||
* Adds a listener to the exit event, fired when the pty exits.
|
||||
* @param event The name of the event.
|
||||
* @param listener The callback function, exitCode is the exit code of the process and signal is
|
||||
* the signal that triggered the exit. signal is not supported on Windows.
|
||||
* @deprecated Use IPty.onExit
|
||||
*/
|
||||
on(event: 'exit', listener: (exitCode: number, signal?: number) => void): void;
|
||||
|
||||
/**
|
||||
* Clears the pty's internal representation of its buffer. This is a no-op
|
||||
* unless on Windows/ConPTY. This is useful if the buffer is cleared on the
|
||||
* frontend in order to synchronize state with the backend to avoid ConPTY
|
||||
* possibly reprinting the screen.
|
||||
*/
|
||||
clear(): void;
|
||||
|
||||
/**
|
||||
* Writes data to the pty.
|
||||
* @param data The data to write.
|
||||
*/
|
||||
write(data: string): void;
|
||||
|
||||
/**
|
||||
* Kills the pty.
|
||||
* @param signal The signal to use, defaults to SIGHUP. This parameter is not supported on
|
||||
* Windows.
|
||||
* @throws Will throw when signal is used on Windows.
|
||||
*/
|
||||
kill(signal?: string): void;
|
||||
|
||||
/**
|
||||
* Pauses the pty for customizable flow control.
|
||||
*/
|
||||
pause(): void;
|
||||
|
||||
/**
|
||||
* Resumes the pty for customizable flow control.
|
||||
*/
|
||||
resume(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* An object that can be disposed via a dispose function.
|
||||
*/
|
||||
export interface IDisposable {
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* An event that can be listened to.
|
||||
* @returns an `IDisposable` to stop listening.
|
||||
*/
|
||||
export interface IEvent<T> {
|
||||
(listener: (e: T) => any): IDisposable;
|
||||
}
|
||||
}
|
||||
|
10
src/pty/prebuild-loader.ts
Normal file
10
src/pty/prebuild-loader.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { require_dlopen } from '.';
|
||||
export function pty_loader() {
|
||||
let pty: any;
|
||||
try {
|
||||
pty = require_dlopen('./pty/' + process.platform + '.' + process.arch + '/pty.node');
|
||||
} catch (outerError) {
|
||||
pty = undefined;
|
||||
}
|
||||
return pty;
|
||||
};
|
297
src/pty/unixTerminal.ts
Normal file
297
src/pty/unixTerminal.ts
Normal file
@@ -0,0 +1,297 @@
|
||||
/* eslint-disable prefer-rest-params */
|
||||
/**
|
||||
* Copyright (c) 2012-2015, Christopher Jeffrey (MIT License)
|
||||
* Copyright (c) 2016, Daniel Imms (MIT License).
|
||||
* Copyright (c) 2018, Microsoft Corporation (MIT License).
|
||||
*/
|
||||
import * as net from 'net';
|
||||
import * as path from 'path';
|
||||
import * as tty from 'tty';
|
||||
import { Terminal, DEFAULT_COLS, DEFAULT_ROWS } from '@homebridge/node-pty-prebuilt-multiarch/src/terminal';
|
||||
import { IProcessEnv, IPtyForkOptions, IPtyOpenOptions } from '@homebridge/node-pty-prebuilt-multiarch/src/interfaces';
|
||||
import { ArgvOrCommandLine } from '@homebridge/node-pty-prebuilt-multiarch/src/types';
|
||||
import { assign } from '@homebridge/node-pty-prebuilt-multiarch/src/utils';
|
||||
import { pty_loader } from './prebuild-loader';
|
||||
export const pty = pty_loader();
|
||||
|
||||
let helperPath: string;
|
||||
helperPath = '../build/Release/spawn-helper';
|
||||
|
||||
helperPath = path.resolve(__dirname, helperPath);
|
||||
helperPath = helperPath.replace('app.asar', 'app.asar.unpacked');
|
||||
helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked');
|
||||
|
||||
const DEFAULT_FILE = 'sh';
|
||||
const DEFAULT_NAME = 'xterm';
|
||||
const DESTROY_SOCKET_TIMEOUT_MS = 200;
|
||||
|
||||
export class UnixTerminal extends Terminal {
|
||||
protected _fd: number;
|
||||
protected _pty: string;
|
||||
|
||||
protected _file: string;
|
||||
protected _name: string;
|
||||
|
||||
protected _readable: boolean;
|
||||
protected _writable: boolean;
|
||||
|
||||
private _boundClose: boolean = false;
|
||||
private _emittedClose: boolean = false;
|
||||
private _master: net.Socket | undefined;
|
||||
private _slave: net.Socket | undefined;
|
||||
|
||||
public get master(): net.Socket | undefined { return this._master; }
|
||||
public get slave(): net.Socket | undefined { return this._slave; }
|
||||
|
||||
constructor(file?: string, args?: ArgvOrCommandLine, opt?: IPtyForkOptions) {
|
||||
super(opt);
|
||||
|
||||
if (typeof args === 'string') {
|
||||
throw new Error('args as a string is not supported on unix.');
|
||||
}
|
||||
|
||||
// Initialize arguments
|
||||
args = args || [];
|
||||
file = file || DEFAULT_FILE;
|
||||
opt = opt || {};
|
||||
opt.env = opt.env || process.env;
|
||||
|
||||
this._cols = opt.cols || DEFAULT_COLS;
|
||||
this._rows = opt.rows || DEFAULT_ROWS;
|
||||
const uid = opt.uid ?? -1;
|
||||
const gid = opt.gid ?? -1;
|
||||
const env: IProcessEnv = assign({}, opt.env);
|
||||
|
||||
if (opt.env === process.env) {
|
||||
this._sanitizeEnv(env);
|
||||
}
|
||||
|
||||
const cwd = opt.cwd || process.cwd();
|
||||
env.PWD = cwd;
|
||||
const name = opt.name || env.TERM || DEFAULT_NAME;
|
||||
env.TERM = name;
|
||||
const parsedEnv = this._parseEnv(env);
|
||||
|
||||
const encoding = (opt.encoding === undefined ? 'utf8' : opt.encoding);
|
||||
|
||||
const onexit = (code: number, signal: number): void => {
|
||||
// XXX Sometimes a data event is emitted after exit. Wait til socket is
|
||||
// destroyed.
|
||||
if (!this._emittedClose) {
|
||||
if (this._boundClose) {
|
||||
return;
|
||||
}
|
||||
this._boundClose = true;
|
||||
// From macOS High Sierra 10.13.2 sometimes the socket never gets
|
||||
// closed. A timeout is applied here to avoid the terminal never being
|
||||
// destroyed when this occurs.
|
||||
let timeout: NodeJS.Timeout | null = setTimeout(() => {
|
||||
timeout = null;
|
||||
// Destroying the socket now will cause the close event to fire
|
||||
this._socket.destroy();
|
||||
}, DESTROY_SOCKET_TIMEOUT_MS);
|
||||
this.once('close', () => {
|
||||
if (timeout !== null) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
this.emit('exit', code, signal);
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.emit('exit', code, signal);
|
||||
};
|
||||
|
||||
// fork
|
||||
const term = pty.fork(file, args, parsedEnv, cwd, this._cols, this._rows, uid, gid, (encoding === 'utf8'), helperPath, onexit);
|
||||
|
||||
this._socket = new tty.ReadStream(term.fd);
|
||||
if (encoding !== null) {
|
||||
this._socket.setEncoding(encoding as BufferEncoding);
|
||||
}
|
||||
|
||||
// setup
|
||||
this._socket.on('error', (err: any) => {
|
||||
// NOTE: fs.ReadStream gets EAGAIN twice at first:
|
||||
if (err.code) {
|
||||
if (~err.code.indexOf('EAGAIN')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// close
|
||||
this._close();
|
||||
// EIO on exit from fs.ReadStream:
|
||||
if (!this._emittedClose) {
|
||||
this._emittedClose = true;
|
||||
this.emit('close');
|
||||
}
|
||||
|
||||
// EIO, happens when someone closes our child process: the only process in
|
||||
// the terminal.
|
||||
// node < 0.6.14: errno 5
|
||||
// node >= 0.6.14: read EIO
|
||||
if (err.code) {
|
||||
if (~err.code.indexOf('errno 5') || ~err.code.indexOf('EIO')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// throw anything else
|
||||
if (this.listeners('error').length < 2) {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
this._pid = term.pid;
|
||||
this._fd = term.fd;
|
||||
this._pty = term.pty;
|
||||
|
||||
this._file = file;
|
||||
this._name = name;
|
||||
|
||||
this._readable = true;
|
||||
this._writable = true;
|
||||
|
||||
this._socket.on('close', () => {
|
||||
if (this._emittedClose) {
|
||||
return;
|
||||
}
|
||||
this._emittedClose = true;
|
||||
this._close();
|
||||
this.emit('close');
|
||||
});
|
||||
|
||||
this._forwardEvents();
|
||||
}
|
||||
|
||||
protected _write(data: string): void {
|
||||
this._socket.write(data);
|
||||
}
|
||||
|
||||
/* Accessors */
|
||||
get fd(): number { return this._fd; }
|
||||
get ptsName(): string { return this._pty; }
|
||||
|
||||
/**
|
||||
* openpty
|
||||
*/
|
||||
|
||||
public static open(opt: IPtyOpenOptions): UnixTerminal {
|
||||
const self: UnixTerminal = Object.create(UnixTerminal.prototype);
|
||||
opt = opt || {};
|
||||
|
||||
if (arguments.length > 1) {
|
||||
opt = {
|
||||
cols: arguments[1],
|
||||
rows: arguments[2]
|
||||
};
|
||||
}
|
||||
|
||||
const cols = opt.cols || DEFAULT_COLS;
|
||||
const rows = opt.rows || DEFAULT_ROWS;
|
||||
const encoding = (opt.encoding === undefined ? 'utf8' : opt.encoding);
|
||||
|
||||
// open
|
||||
const term: IUnixOpenProcess = pty.open(cols, rows);
|
||||
|
||||
self._master = new tty.ReadStream(term.master);
|
||||
if (encoding !== null) {
|
||||
self._master.setEncoding(encoding as BufferEncoding);
|
||||
}
|
||||
self._master.resume();
|
||||
|
||||
self._slave = new tty.ReadStream(term.slave);
|
||||
if (encoding !== null) {
|
||||
self._slave.setEncoding(encoding as BufferEncoding);
|
||||
}
|
||||
self._slave.resume();
|
||||
|
||||
self._socket = self._master;
|
||||
self._pid = -1;
|
||||
self._fd = term.master;
|
||||
self._pty = term.pty;
|
||||
|
||||
self._file = process.argv[0] || 'node';
|
||||
self._name = process.env.TERM || '';
|
||||
|
||||
self._readable = true;
|
||||
self._writable = true;
|
||||
|
||||
self._socket.on('error', err => {
|
||||
self._close();
|
||||
if (self.listeners('error').length < 2) {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
self._socket.on('close', () => {
|
||||
self._close();
|
||||
});
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this._close();
|
||||
|
||||
// Need to close the read stream so node stops reading a dead file
|
||||
// descriptor. Then we can safely SIGHUP the shell.
|
||||
this._socket.once('close', () => {
|
||||
this.kill('SIGHUP');
|
||||
});
|
||||
|
||||
this._socket.destroy();
|
||||
}
|
||||
|
||||
public kill(signal?: string): void {
|
||||
try {
|
||||
process.kill(this.pid, signal || 'SIGHUP');
|
||||
} catch (e) { /* swallow */ }
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name of the process.
|
||||
*/
|
||||
public get process(): string {
|
||||
if (process.platform === 'darwin') {
|
||||
const title = pty.process(this._fd);
|
||||
return (title !== 'kernel_task') ? title : this._file;
|
||||
}
|
||||
|
||||
return pty.process(this._fd, this._pty) || this._file;
|
||||
}
|
||||
|
||||
/**
|
||||
* TTY
|
||||
*/
|
||||
|
||||
public resize(cols: number, rows: number): void {
|
||||
if (cols <= 0 || rows <= 0 || isNaN(cols) || isNaN(rows) || cols === Infinity || rows === Infinity) {
|
||||
throw new Error('resizing must be done using positive cols and rows');
|
||||
}
|
||||
pty.resize(this._fd, cols, rows);
|
||||
this._cols = cols;
|
||||
this._rows = rows;
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
|
||||
}
|
||||
|
||||
private _sanitizeEnv(env: IProcessEnv): void {
|
||||
// Make sure we didn't start our server from inside tmux.
|
||||
delete env['TMUX'];
|
||||
delete env['TMUX_PANE'];
|
||||
|
||||
// Make sure we didn't start our server from inside screen.
|
||||
// http://web.mit.edu/gnu/doc/html/screen_20.html
|
||||
delete env['STY'];
|
||||
delete env['WINDOW'];
|
||||
|
||||
// Delete some variables that might confuse our terminal.
|
||||
delete env['WINDOWID'];
|
||||
delete env['TERMCAP'];
|
||||
delete env['COLUMNS'];
|
||||
delete env['LINES'];
|
||||
}
|
||||
}
|
80
src/pty/windowsConoutConnection.ts
Normal file
80
src/pty/windowsConoutConnection.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Copyright (c) 2020, Microsoft Corporation (MIT License).
|
||||
*/
|
||||
|
||||
import { Worker } from 'worker_threads';
|
||||
import { Socket } from 'net';
|
||||
import { IDisposable } from '@homebridge/node-pty-prebuilt-multiarch/src/types';
|
||||
import { IWorkerData, ConoutWorkerMessage, getWorkerPipeName } from '@homebridge/node-pty-prebuilt-multiarch/src/shared/conout';
|
||||
import { dirname, join } from 'path';
|
||||
import { IEvent, EventEmitter2 } from '@homebridge/node-pty-prebuilt-multiarch/src/eventEmitter2';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
/**
|
||||
* The amount of time to wait for additional data after the conpty shell process has exited before
|
||||
* shutting down the worker and sockets. The timer will be reset if a new data event comes in after
|
||||
* the timer has started.
|
||||
*/
|
||||
const FLUSH_DATA_INTERVAL = 1000;
|
||||
|
||||
/**
|
||||
* Connects to and manages the lifecycle of the conout socket. This socket must be drained on
|
||||
* another thread in order to avoid deadlocks where Conpty waits for the out socket to drain
|
||||
* when `ClosePseudoConsole` is called. This happens when data is being written to the terminal when
|
||||
* the pty is closed.
|
||||
*
|
||||
* See also:
|
||||
* - https://github.com/microsoft/node-pty/issues/375
|
||||
* - https://github.com/microsoft/vscode/issues/76548
|
||||
* - https://github.com/microsoft/terminal/issues/1810
|
||||
* - https://docs.microsoft.com/en-us/windows/console/closepseudoconsole
|
||||
*/
|
||||
export class ConoutConnection implements IDisposable {
|
||||
private _worker: Worker;
|
||||
private _drainTimeout: NodeJS.Timeout | undefined;
|
||||
private _isDisposed: boolean = false;
|
||||
|
||||
private _onReady = new EventEmitter2<void>();
|
||||
public get onReady(): IEvent<void> { return this._onReady.event; }
|
||||
|
||||
constructor(
|
||||
private _conoutPipeName: string
|
||||
) {
|
||||
const workerData: IWorkerData = { conoutPipeName: _conoutPipeName };
|
||||
const scriptPath = dirname(fileURLToPath(import.meta.url));
|
||||
this._worker = new Worker(join(scriptPath, 'worker/conoutSocketWorker.mjs'), { workerData });
|
||||
this._worker.on('message', (message: ConoutWorkerMessage) => {
|
||||
switch (message) {
|
||||
case ConoutWorkerMessage.READY:
|
||||
this._onReady.fire();
|
||||
return;
|
||||
default:
|
||||
console.warn('Unexpected ConoutWorkerMessage', message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this._isDisposed) {
|
||||
return;
|
||||
}
|
||||
this._isDisposed = true;
|
||||
// Drain all data from the socket before closing
|
||||
this._drainDataAndClose();
|
||||
}
|
||||
|
||||
connectSocket(socket: Socket): void {
|
||||
socket.connect(getWorkerPipeName(this._conoutPipeName));
|
||||
}
|
||||
|
||||
private _drainDataAndClose(): void {
|
||||
if (this._drainTimeout) {
|
||||
clearTimeout(this._drainTimeout);
|
||||
}
|
||||
this._drainTimeout = setTimeout(() => this._destroySocket(), FLUSH_DATA_INTERVAL);
|
||||
}
|
||||
|
||||
private async _destroySocket(): Promise<void> {
|
||||
await this._worker.terminate();
|
||||
}
|
||||
}
|
306
src/pty/windowsPtyAgent.ts
Normal file
306
src/pty/windowsPtyAgent.ts
Normal file
@@ -0,0 +1,306 @@
|
||||
/**
|
||||
* Copyright (c) 2012-2015, Christopher Jeffrey, Peter Sunde (MIT License)
|
||||
* Copyright (c) 2016, Daniel Imms (MIT License).
|
||||
* Copyright (c) 2018, Microsoft Corporation (MIT License).
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { Socket } from 'net';
|
||||
import { ArgvOrCommandLine } from '@homebridge/node-pty-prebuilt-multiarch/src/types';
|
||||
import { fork } from 'child_process';
|
||||
import { ConoutConnection } from './windowsConoutConnection';
|
||||
import { require_dlopen } from '.';
|
||||
|
||||
let conptyNative: IConptyNative;
|
||||
let winptyNative: IWinptyNative;
|
||||
|
||||
/**
|
||||
* The amount of time to wait for additional data after the conpty shell process has exited before
|
||||
* shutting down the socket. The timer will be reset if a new data event comes in after the timer
|
||||
* has started.
|
||||
*/
|
||||
const FLUSH_DATA_INTERVAL = 1000;
|
||||
|
||||
/**
|
||||
* This agent sits between the WindowsTerminal class and provides a common interface for both conpty
|
||||
* and winpty.
|
||||
*/
|
||||
export class WindowsPtyAgent {
|
||||
private _inSocket: Socket;
|
||||
private _outSocket: Socket;
|
||||
private _pid: number = 0;
|
||||
private _innerPid: number = 0;
|
||||
private _closeTimeout: NodeJS.Timer | undefined;
|
||||
private _exitCode: number | undefined;
|
||||
private _conoutSocketWorker: ConoutConnection;
|
||||
|
||||
private _fd: any;
|
||||
private _pty: number;
|
||||
private _ptyNative: IConptyNative | IWinptyNative;
|
||||
|
||||
public get inSocket(): Socket { return this._inSocket; }
|
||||
public get outSocket(): Socket { return this._outSocket; }
|
||||
public get fd(): any { return this._fd; }
|
||||
public get innerPid(): number { return this._innerPid; }
|
||||
public get pty(): number { return this._pty; }
|
||||
|
||||
constructor(
|
||||
file: string,
|
||||
args: ArgvOrCommandLine,
|
||||
env: string[],
|
||||
cwd: string,
|
||||
cols: number,
|
||||
rows: number,
|
||||
debug: boolean,
|
||||
private _useConpty: boolean | undefined,
|
||||
private _useConptyDll: boolean = false,
|
||||
conptyInheritCursor: boolean = false
|
||||
) {
|
||||
if (this._useConpty === undefined || this._useConpty === true) {
|
||||
this._useConpty = this._getWindowsBuildNumber() >= 18309;
|
||||
}
|
||||
if (this._useConpty) {
|
||||
if (!conptyNative) {
|
||||
conptyNative = require_dlopen('./pty/' + process.platform + '.' + process.arch + '/conpty.node');
|
||||
}
|
||||
} else {
|
||||
if (!winptyNative) {
|
||||
winptyNative = require_dlopen('./pty/' + process.platform + '.' + process.arch + '/pty.node');
|
||||
}
|
||||
}
|
||||
this._ptyNative = this._useConpty ? conptyNative : winptyNative;
|
||||
|
||||
// Sanitize input variable.
|
||||
cwd = path.resolve(cwd);
|
||||
|
||||
// Compose command line
|
||||
const commandLine = argsToCommandLine(file, args);
|
||||
|
||||
// Open pty session.
|
||||
let term: IConptyProcess | IWinptyProcess;
|
||||
if (this._useConpty) {
|
||||
term = (this._ptyNative as IConptyNative).startProcess(file, cols, rows, debug, this._generatePipeName(), conptyInheritCursor, this._useConptyDll);
|
||||
} else {
|
||||
term = (this._ptyNative as IWinptyNative).startProcess(file, commandLine, env, cwd, cols, rows, debug);
|
||||
this._pid = (term as IWinptyProcess).pid;
|
||||
this._innerPid = (term as IWinptyProcess).innerPid;
|
||||
}
|
||||
|
||||
// Not available on windows.
|
||||
this._fd = term.fd;
|
||||
|
||||
// Generated incremental number that has no real purpose besides using it
|
||||
// as a terminal id.
|
||||
this._pty = term.pty;
|
||||
|
||||
// Create terminal pipe IPC channel and forward to a local unix socket.
|
||||
this._outSocket = new Socket();
|
||||
this._outSocket.setEncoding('utf8');
|
||||
// The conout socket must be ready out on another thread to avoid deadlocks
|
||||
this._conoutSocketWorker = new ConoutConnection(term.conout);
|
||||
this._conoutSocketWorker.onReady(() => {
|
||||
this._conoutSocketWorker.connectSocket(this._outSocket);
|
||||
});
|
||||
this._outSocket.on('connect', () => {
|
||||
this._outSocket.emit('ready_datapipe');
|
||||
});
|
||||
|
||||
const inSocketFD = fs.openSync(term.conin, 'w');
|
||||
this._inSocket = new Socket({
|
||||
fd: inSocketFD,
|
||||
readable: false,
|
||||
writable: true
|
||||
});
|
||||
this._inSocket.setEncoding('utf8');
|
||||
|
||||
if (this._useConpty) {
|
||||
const connect = (this._ptyNative as IConptyNative).connect(this._pty, commandLine, cwd, env, c => this._$onProcessExit(c));
|
||||
this._innerPid = connect.pid;
|
||||
}
|
||||
}
|
||||
|
||||
public resize(cols: number, rows: number): void {
|
||||
if (this._useConpty) {
|
||||
if (this._exitCode !== undefined) {
|
||||
throw new Error('Cannot resize a pty that has already exited');
|
||||
}
|
||||
(this._ptyNative as IConptyNative).resize(this._pty, cols, rows, this._useConptyDll);
|
||||
return;
|
||||
}
|
||||
(this._ptyNative as IWinptyNative).resize(this._pid, cols, rows);
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
if (this._useConpty) {
|
||||
(this._ptyNative as IConptyNative).clear(this._pty, this._useConptyDll);
|
||||
}
|
||||
}
|
||||
|
||||
public kill(): void {
|
||||
this._inSocket.readable = false;
|
||||
this._outSocket.readable = false;
|
||||
// Tell the agent to kill the pty, this releases handles to the process
|
||||
if (this._useConpty) {
|
||||
this._getConsoleProcessList().then(consoleProcessList => {
|
||||
consoleProcessList.forEach((pid: number) => {
|
||||
try {
|
||||
process.kill(pid);
|
||||
} catch (e) {
|
||||
// Ignore if process cannot be found (kill ESRCH error)
|
||||
}
|
||||
});
|
||||
(this._ptyNative as IConptyNative).kill(this._pty, this._useConptyDll);
|
||||
});
|
||||
} else {
|
||||
// Because pty.kill closes the handle, it will kill most processes by itself.
|
||||
// Process IDs can be reused as soon as all handles to them are
|
||||
// dropped, so we want to immediately kill the entire console process list.
|
||||
// If we do not force kill all processes here, node servers in particular
|
||||
// seem to become detached and remain running (see
|
||||
// Microsoft/vscode#26807).
|
||||
const processList: number[] = (this._ptyNative as IWinptyNative).getProcessList(this._pid);
|
||||
(this._ptyNative as IWinptyNative).kill(this._pid, this._innerPid);
|
||||
processList.forEach(pid => {
|
||||
try {
|
||||
process.kill(pid);
|
||||
} catch (e) {
|
||||
// Ignore if process cannot be found (kill ESRCH error)
|
||||
}
|
||||
});
|
||||
}
|
||||
this._conoutSocketWorker.dispose();
|
||||
}
|
||||
|
||||
private _getConsoleProcessList(): Promise<number[]> {
|
||||
return new Promise<number[]>(resolve => {
|
||||
const agent = fork(path.join(__dirname, 'conpty_console_list_agent'), [this._innerPid.toString()]);
|
||||
agent.on('message', message => {
|
||||
clearTimeout(timeout);
|
||||
// @ts-expect-error no need to check if it is null
|
||||
resolve(message.consoleProcessList);
|
||||
});
|
||||
const timeout = setTimeout(() => {
|
||||
// Something went wrong, just send back the shell PID
|
||||
agent.kill();
|
||||
resolve([this._innerPid]);
|
||||
}, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
public get exitCode(): number | undefined {
|
||||
if (this._useConpty) {
|
||||
return this._exitCode;
|
||||
}
|
||||
const winptyExitCode = (this._ptyNative as IWinptyNative).getExitCode(this._innerPid);
|
||||
return winptyExitCode === -1 ? undefined : winptyExitCode;
|
||||
}
|
||||
|
||||
private _getWindowsBuildNumber(): number {
|
||||
const osVersion = (/(\d+)\.(\d+)\.(\d+)/g).exec(os.release());
|
||||
let buildNumber: number = 0;
|
||||
if (osVersion && osVersion.length === 4) {
|
||||
buildNumber = parseInt(osVersion[3]);
|
||||
}
|
||||
return buildNumber;
|
||||
}
|
||||
|
||||
private _generatePipeName(): string {
|
||||
return `conpty-${Math.random() * 10000000}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered from the native side when a contpy process exits.
|
||||
*/
|
||||
private _$onProcessExit(exitCode: number): void {
|
||||
this._exitCode = exitCode;
|
||||
this._flushDataAndCleanUp();
|
||||
this._outSocket.on('data', () => this._flushDataAndCleanUp());
|
||||
}
|
||||
|
||||
private _flushDataAndCleanUp(): void {
|
||||
if (this._closeTimeout) {
|
||||
// @ts-expect-error no need to check if it is null
|
||||
clearTimeout(this._closeTimeout);
|
||||
}
|
||||
this._closeTimeout = setTimeout(() => this._cleanUpProcess(), FLUSH_DATA_INTERVAL);
|
||||
}
|
||||
|
||||
private _cleanUpProcess(): void {
|
||||
this._inSocket.readable = false;
|
||||
this._outSocket.readable = false;
|
||||
this._outSocket.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
// Convert argc/argv into a Win32 command-line following the escaping convention
|
||||
// documented on MSDN (e.g. see CommandLineToArgvW documentation). Copied from
|
||||
// winpty project.
|
||||
export function argsToCommandLine(file: string, args: ArgvOrCommandLine): string {
|
||||
if (isCommandLine(args)) {
|
||||
if (args.length === 0) {
|
||||
return file;
|
||||
}
|
||||
return `${argsToCommandLine(file, [])} ${args}`;
|
||||
}
|
||||
const argv = [file];
|
||||
Array.prototype.push.apply(argv, args);
|
||||
let result = '';
|
||||
for (let argIndex = 0; argIndex < argv.length; argIndex++) {
|
||||
if (argIndex > 0) {
|
||||
result += ' ';
|
||||
}
|
||||
const arg = argv[argIndex];
|
||||
// if it is empty or it contains whitespace and is not already quoted
|
||||
const hasLopsidedEnclosingQuote = xOr((arg[0] !== '"'), (arg[arg.length - 1] !== '"'));
|
||||
const hasNoEnclosingQuotes = ((arg[0] !== '"') && (arg[arg.length - 1] !== '"'));
|
||||
const quote =
|
||||
arg === '' ||
|
||||
(arg.indexOf(' ') !== -1 ||
|
||||
arg.indexOf('\t') !== -1) &&
|
||||
((arg.length > 1) &&
|
||||
(hasLopsidedEnclosingQuote || hasNoEnclosingQuotes));
|
||||
if (quote) {
|
||||
result += '"';
|
||||
}
|
||||
let bsCount = 0;
|
||||
for (let i = 0; i < arg.length; i++) {
|
||||
const p = arg[i];
|
||||
if (p === '\\') {
|
||||
bsCount++;
|
||||
} else if (p === '"') {
|
||||
result += repeatText('\\', bsCount * 2 + 1);
|
||||
result += '"';
|
||||
bsCount = 0;
|
||||
} else {
|
||||
result += repeatText('\\', bsCount);
|
||||
bsCount = 0;
|
||||
result += p;
|
||||
}
|
||||
}
|
||||
if (quote) {
|
||||
result += repeatText('\\', bsCount * 2);
|
||||
result += '"';
|
||||
} else {
|
||||
result += repeatText('\\', bsCount);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function isCommandLine(args: ArgvOrCommandLine): args is string {
|
||||
return typeof args === 'string';
|
||||
}
|
||||
|
||||
function repeatText(text: string, count: number): string {
|
||||
let result = '';
|
||||
for (let i = 0; i < count; i++) {
|
||||
result += text;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function xOr(arg1: boolean, arg2: boolean): boolean {
|
||||
return ((arg1 && !arg2) || (!arg1 && arg2));
|
||||
}
|
208
src/pty/windowsTerminal.ts
Normal file
208
src/pty/windowsTerminal.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* Copyright (c) 2012-2015, Christopher Jeffrey, Peter Sunde (MIT License)
|
||||
* Copyright (c) 2016, Daniel Imms (MIT License).
|
||||
* Copyright (c) 2018, Microsoft Corporation (MIT License).
|
||||
*/
|
||||
|
||||
import { Socket } from 'net';
|
||||
import { Terminal, DEFAULT_COLS, DEFAULT_ROWS } from '@homebridge/node-pty-prebuilt-multiarch/src/terminal';
|
||||
import { WindowsPtyAgent } from './windowsPtyAgent';
|
||||
import { IPtyOpenOptions, IWindowsPtyForkOptions } from '@homebridge/node-pty-prebuilt-multiarch/src/interfaces';
|
||||
import { ArgvOrCommandLine } from '@homebridge/node-pty-prebuilt-multiarch/src/types';
|
||||
import { assign } from '@homebridge/node-pty-prebuilt-multiarch/src/utils';
|
||||
|
||||
const DEFAULT_FILE = 'cmd.exe';
|
||||
const DEFAULT_NAME = 'Windows Shell';
|
||||
|
||||
export class WindowsTerminal extends Terminal {
|
||||
private _isReady: boolean;
|
||||
private _deferreds: any[];
|
||||
private _agent: WindowsPtyAgent;
|
||||
|
||||
constructor(file?: string, args?: ArgvOrCommandLine, opt?: IWindowsPtyForkOptions) {
|
||||
super(opt);
|
||||
|
||||
this._checkType('args', args, 'string', true);
|
||||
|
||||
// Initialize arguments
|
||||
args = args || [];
|
||||
file = file || DEFAULT_FILE;
|
||||
opt = opt || {};
|
||||
opt.env = opt.env || process.env;
|
||||
|
||||
if (opt.encoding) {
|
||||
console.warn('Setting encoding on Windows is not supported');
|
||||
}
|
||||
|
||||
const env = assign({}, opt.env);
|
||||
this._cols = opt.cols || DEFAULT_COLS;
|
||||
this._rows = opt.rows || DEFAULT_ROWS;
|
||||
const cwd = opt.cwd || process.cwd();
|
||||
const name = opt.name || env.TERM || DEFAULT_NAME;
|
||||
const parsedEnv = this._parseEnv(env);
|
||||
|
||||
// If the terminal is ready
|
||||
this._isReady = false;
|
||||
|
||||
// Functions that need to run after `ready` event is emitted.
|
||||
this._deferreds = [];
|
||||
|
||||
// Create new termal.
|
||||
this._agent = new WindowsPtyAgent(file, args, parsedEnv, cwd, this._cols, this._rows, false, opt.useConpty, opt.useConptyDll, opt.conptyInheritCursor);
|
||||
this._socket = this._agent.outSocket;
|
||||
|
||||
// Not available until `ready` event emitted.
|
||||
this._pid = this._agent.innerPid;
|
||||
this._fd = this._agent.fd;
|
||||
this._pty = this._agent.pty;
|
||||
|
||||
// The forked windows terminal is not available until `ready` event is
|
||||
// emitted.
|
||||
this._socket.on('ready_datapipe', () => {
|
||||
|
||||
// These events needs to be forwarded.
|
||||
['connect', 'data', 'end', 'timeout', 'drain'].forEach(event => {
|
||||
this._socket.on(event, () => {
|
||||
|
||||
// Wait until the first data event is fired then we can run deferreds.
|
||||
if (!this._isReady && event === 'data') {
|
||||
|
||||
// Terminal is now ready and we can avoid having to defer method
|
||||
// calls.
|
||||
this._isReady = true;
|
||||
|
||||
// Execute all deferred methods
|
||||
this._deferreds.forEach(fn => {
|
||||
// NB! In order to ensure that `this` has all its references
|
||||
// updated any variable that need to be available in `this` before
|
||||
// the deferred is run has to be declared above this forEach
|
||||
// statement.
|
||||
fn.run();
|
||||
});
|
||||
|
||||
// Reset
|
||||
this._deferreds = [];
|
||||
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Shutdown if `error` event is emitted.
|
||||
this._socket.on('error', err => {
|
||||
// Close terminal session.
|
||||
this._close();
|
||||
|
||||
// EIO, happens when someone closes our child process: the only process
|
||||
// in the terminal.
|
||||
// node < 0.6.14: errno 5
|
||||
// node >= 0.6.14: read EIO
|
||||
if ((<any>err).code) {
|
||||
if (~(<any>err).code.indexOf('errno 5') || ~(<any>err).code.indexOf('EIO')) return;
|
||||
}
|
||||
|
||||
// Throw anything else.
|
||||
if (this.listeners('error').length < 2) {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
// Cleanup after the socket is closed.
|
||||
this._socket.on('close', () => {
|
||||
this.emit('exit', this._agent.exitCode);
|
||||
this._close();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
this._file = file;
|
||||
this._name = name;
|
||||
|
||||
this._readable = true;
|
||||
this._writable = true;
|
||||
|
||||
this._forwardEvents();
|
||||
}
|
||||
|
||||
protected _write(data: string): void {
|
||||
this._defer(this._doWrite, data);
|
||||
}
|
||||
|
||||
private _doWrite(data: string): void {
|
||||
this._agent.inSocket.write(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* openpty
|
||||
*/
|
||||
|
||||
public static open(options?: IPtyOpenOptions): void {
|
||||
throw new Error('open() not supported on windows, use Fork() instead.');
|
||||
}
|
||||
|
||||
/**
|
||||
* TTY
|
||||
*/
|
||||
|
||||
public resize(cols: number, rows: number): void {
|
||||
if (cols <= 0 || rows <= 0 || isNaN(cols) || isNaN(rows) || cols === Infinity || rows === Infinity) {
|
||||
throw new Error('resizing must be done using positive cols and rows');
|
||||
}
|
||||
this._deferNoArgs(() => {
|
||||
this._agent.resize(cols, rows);
|
||||
this._cols = cols;
|
||||
this._rows = rows;
|
||||
});
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._deferNoArgs(() => {
|
||||
this._agent.clear();
|
||||
});
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this._deferNoArgs(() => {
|
||||
this.kill();
|
||||
});
|
||||
}
|
||||
|
||||
public kill(signal?: string): void {
|
||||
this._deferNoArgs(() => {
|
||||
if (signal) {
|
||||
throw new Error('Signals not supported on windows.');
|
||||
}
|
||||
this._close();
|
||||
this._agent.kill();
|
||||
});
|
||||
}
|
||||
|
||||
private _deferNoArgs<A>(deferredFn: () => void): void {
|
||||
// If the terminal is ready, execute.
|
||||
if (this._isReady) {
|
||||
deferredFn.call(this);
|
||||
return;
|
||||
}
|
||||
|
||||
// Queue until terminal is ready.
|
||||
this._deferreds.push({
|
||||
run: () => deferredFn.call(this)
|
||||
});
|
||||
}
|
||||
|
||||
private _defer<A>(deferredFn: (arg: A) => void, arg: A): void {
|
||||
// If the terminal is ready, execute.
|
||||
if (this._isReady) {
|
||||
deferredFn.call(this, arg);
|
||||
return;
|
||||
}
|
||||
|
||||
// Queue until terminal is ready.
|
||||
this._deferreds.push({
|
||||
run: () => deferredFn.call(this, arg)
|
||||
});
|
||||
}
|
||||
|
||||
public get process(): string { return this._name; }
|
||||
public get master(): Socket { throw new Error('master is not supported on Windows'); }
|
||||
public get slave(): Socket { throw new Error('slave is not supported on Windows'); }
|
||||
}
|
22
src/pty/worker/conoutSocketWorker.ts
Normal file
22
src/pty/worker/conoutSocketWorker.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Copyright (c) 2020, Microsoft Corporation (MIT License).
|
||||
*/
|
||||
|
||||
import { parentPort, workerData } from 'worker_threads';
|
||||
import { Socket, createServer } from 'net';
|
||||
import { ConoutWorkerMessage, IWorkerData, getWorkerPipeName } from '@homebridge/node-pty-prebuilt-multiarch/src/shared/conout';
|
||||
|
||||
const conoutPipeName = (workerData as IWorkerData).conoutPipeName;
|
||||
const conoutSocket = new Socket();
|
||||
conoutSocket.setEncoding('utf8');
|
||||
conoutSocket.connect(conoutPipeName, () => {
|
||||
const server = createServer(workerSocket => {
|
||||
conoutSocket.pipe(workerSocket);
|
||||
});
|
||||
server.listen(getWorkerPipeName(conoutPipeName));
|
||||
|
||||
if (!parentPort) {
|
||||
throw new Error('worker_threads parentPort is null');
|
||||
}
|
||||
parentPort.postMessage(ConoutWorkerMessage.READY);
|
||||
});
|
@@ -1,3 +1,2 @@
|
||||
import { NCoreInitShell } from "./base";
|
||||
|
||||
NCoreInitShell();
|
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
|
||||
import express from 'express';
|
||||
import { createServer } from 'http';
|
||||
import { LogWrapper } from '@/common/log';
|
||||
import { NapCatPathWrapper } from '@/common/path';
|
||||
import { WebUiConfigWrapper } from '@webapi/helper/config';
|
||||
@@ -11,10 +12,11 @@ import { cors } from '@webapi/middleware/cors';
|
||||
import { createUrl } from '@webapi/utils/url';
|
||||
import { sendSuccess } from '@webapi/utils/response';
|
||||
import { join } from 'node:path';
|
||||
import { terminalManager } from '@webapi/terminal/terminal_manager';
|
||||
|
||||
// 实例化Express
|
||||
const app = express();
|
||||
|
||||
const server = createServer(app);
|
||||
/**
|
||||
* 初始化并启动WebUI服务。
|
||||
* 该函数配置了Express服务器以支持JSON解析和静态文件服务,并监听6099端口。
|
||||
@@ -45,6 +47,10 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp
|
||||
// ------------挂载路由------------
|
||||
// 挂载静态路由(前端),路径为 [/前缀]/webui
|
||||
app.use('/webui', express.static(pathWrapper.staticPath));
|
||||
// 初始化WebSocket服务器
|
||||
server.on('upgrade', (request, socket, head) => {
|
||||
terminalManager.initialize(request, socket, head, logger);
|
||||
});
|
||||
// 挂载API接口
|
||||
app.use('/api', ALLRouter);
|
||||
// 所有剩下的请求都转到静态页面
|
||||
@@ -61,7 +67,7 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp
|
||||
// ------------路由挂载结束------------
|
||||
|
||||
// ------------启动服务------------
|
||||
app.listen(config.port, config.host, async () => {
|
||||
server.listen(config.port, config.host, async () => {
|
||||
// 启动后打印出相关地址
|
||||
const port = config.port.toString(),
|
||||
searchParams = { token: config.token };
|
||||
|
261
src/webui/src/api/File.ts
Normal file
261
src/webui/src/api/File.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
import type { RequestHandler } from 'express';
|
||||
import { sendError, sendSuccess } from '../utils/response';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
|
||||
const isWindows = os.platform() === 'win32';
|
||||
|
||||
// 获取系统根目录列表(Windows返回盘符列表,其他系统返回['/'])
|
||||
const getRootDirs = async (): Promise<string[]> => {
|
||||
if (!isWindows) return ['/'];
|
||||
|
||||
// Windows 驱动器字母 (A-Z)
|
||||
const drives: string[] = [];
|
||||
for (let i = 65; i <= 90; i++) {
|
||||
const driveLetter = String.fromCharCode(i);
|
||||
try {
|
||||
await fs.access(`${driveLetter}:\\`);
|
||||
drives.push(`${driveLetter}:`);
|
||||
} catch {
|
||||
// 如果驱动器不存在或无法访问,跳过
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return drives.length > 0 ? drives : ['C:'];
|
||||
};
|
||||
|
||||
// 规范化路径
|
||||
const normalizePath = (inputPath: string): string => {
|
||||
if (!inputPath) return isWindows ? 'C:\\' : '/';
|
||||
// 如果是Windows且输入为纯盘符(可能带或不带斜杠),统一返回 "X:\"
|
||||
if (isWindows && /^[A-Z]:[\\/]*$/i.test(inputPath)) {
|
||||
return inputPath.slice(0, 2) + '\\';
|
||||
}
|
||||
return path.normalize(inputPath);
|
||||
};
|
||||
|
||||
interface FileInfo {
|
||||
name: string;
|
||||
isDirectory: boolean;
|
||||
size: number;
|
||||
mtime: Date;
|
||||
}
|
||||
|
||||
// 添加系统文件黑名单
|
||||
const SYSTEM_FILES = new Set(['pagefile.sys', 'swapfile.sys', 'hiberfil.sys', 'System Volume Information']);
|
||||
|
||||
// 检查同类型的文件或目录是否存在
|
||||
const checkSameTypeExists = async (pathToCheck: string, isDirectory: boolean): Promise<boolean> => {
|
||||
try {
|
||||
const stat = await fs.stat(pathToCheck);
|
||||
// 只有当类型相同时才认为是冲突
|
||||
return stat.isDirectory() === isDirectory;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取目录内容
|
||||
export const ListFilesHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const requestPath = (req.query.path as string) || (isWindows ? 'C:\\' : '/');
|
||||
const normalizedPath = normalizePath(requestPath);
|
||||
const onlyDirectory = req.query.onlyDirectory === 'true';
|
||||
|
||||
// 如果是根路径且在Windows系统上,返回盘符列表
|
||||
if (isWindows && (!requestPath || requestPath === '/' || requestPath === '\\')) {
|
||||
const drives = await getRootDirs();
|
||||
const driveInfos: FileInfo[] = await Promise.all(
|
||||
drives.map(async (drive) => {
|
||||
try {
|
||||
const stat = await fs.stat(`${drive}\\`);
|
||||
return {
|
||||
name: drive,
|
||||
isDirectory: true,
|
||||
size: 0,
|
||||
mtime: stat.mtime,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
name: drive,
|
||||
isDirectory: true,
|
||||
size: 0,
|
||||
mtime: new Date(),
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
return sendSuccess(res, driveInfos);
|
||||
}
|
||||
|
||||
const files = await fs.readdir(normalizedPath);
|
||||
let fileInfos: FileInfo[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
// 跳过系统文件
|
||||
if (SYSTEM_FILES.has(file)) continue;
|
||||
|
||||
try {
|
||||
const fullPath = path.join(normalizedPath, file);
|
||||
const stat = await fs.stat(fullPath);
|
||||
fileInfos.push({
|
||||
name: file,
|
||||
isDirectory: stat.isDirectory(),
|
||||
size: stat.size,
|
||||
mtime: stat.mtime,
|
||||
});
|
||||
} catch (error) {
|
||||
// 忽略无法访问的文件
|
||||
// console.warn(`无法访问文件 ${file}:`, error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果请求参数 onlyDirectory 为 true,则只返回目录信息
|
||||
if (onlyDirectory) {
|
||||
fileInfos = fileInfos.filter((info) => info.isDirectory);
|
||||
}
|
||||
|
||||
return sendSuccess(res, fileInfos);
|
||||
} catch (error) {
|
||||
console.error('读取目录失败:', error);
|
||||
return sendError(res, '读取目录失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 创建目录
|
||||
export const CreateDirHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const { path: dirPath } = req.body;
|
||||
const normalizedPath = normalizePath(dirPath);
|
||||
|
||||
// 检查是否已存在同类型(目录)
|
||||
if (await checkSameTypeExists(normalizedPath, true)) {
|
||||
return sendError(res, '同名目录已存在');
|
||||
}
|
||||
|
||||
await fs.mkdir(normalizedPath, { recursive: true });
|
||||
return sendSuccess(res, true);
|
||||
} catch (error) {
|
||||
return sendError(res, '创建目录失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 删除文件/目录
|
||||
export const DeleteHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const { path: targetPath } = req.body;
|
||||
const normalizedPath = normalizePath(targetPath);
|
||||
const stat = await fs.stat(normalizedPath);
|
||||
if (stat.isDirectory()) {
|
||||
await fs.rm(normalizedPath, { recursive: true });
|
||||
} else {
|
||||
await fs.unlink(normalizedPath);
|
||||
}
|
||||
return sendSuccess(res, true);
|
||||
} catch (error) {
|
||||
return sendError(res, '删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 批量删除文件/目录
|
||||
export const BatchDeleteHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const { paths } = req.body;
|
||||
for (const targetPath of paths) {
|
||||
const normalizedPath = normalizePath(targetPath);
|
||||
const stat = await fs.stat(normalizedPath);
|
||||
if (stat.isDirectory()) {
|
||||
await fs.rm(normalizedPath, { recursive: true });
|
||||
} else {
|
||||
await fs.unlink(normalizedPath);
|
||||
}
|
||||
}
|
||||
return sendSuccess(res, true);
|
||||
} catch (error) {
|
||||
return sendError(res, '批量删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 读取文件内容
|
||||
export const ReadFileHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const filePath = normalizePath(req.query.path as string);
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
return sendSuccess(res, content);
|
||||
} catch (error) {
|
||||
return sendError(res, '读取文件失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 写入文件内容
|
||||
export const WriteFileHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const { path: filePath, content } = req.body;
|
||||
const normalizedPath = normalizePath(filePath);
|
||||
await fs.writeFile(normalizedPath, content, 'utf-8');
|
||||
return sendSuccess(res, true);
|
||||
} catch (error) {
|
||||
return sendError(res, '写入文件失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 创建新文件
|
||||
export const CreateFileHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const { path: filePath } = req.body;
|
||||
const normalizedPath = normalizePath(filePath);
|
||||
|
||||
// 检查是否已存在同类型(文件)
|
||||
if (await checkSameTypeExists(normalizedPath, false)) {
|
||||
return sendError(res, '同名文件已存在');
|
||||
}
|
||||
|
||||
await fs.writeFile(normalizedPath, '', 'utf-8');
|
||||
return sendSuccess(res, true);
|
||||
} catch (error) {
|
||||
return sendError(res, '创建文件失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 重命名文件/目录
|
||||
export const RenameHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const { oldPath, newPath } = req.body;
|
||||
const normalizedOldPath = normalizePath(oldPath);
|
||||
const normalizedNewPath = normalizePath(newPath);
|
||||
await fs.rename(normalizedOldPath, normalizedNewPath);
|
||||
return sendSuccess(res, true);
|
||||
} catch (error) {
|
||||
return sendError(res, '重命名失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 移动文件/目录
|
||||
export const MoveHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const { sourcePath, targetPath } = req.body;
|
||||
const normalizedSourcePath = normalizePath(sourcePath);
|
||||
const normalizedTargetPath = normalizePath(targetPath);
|
||||
await fs.rename(normalizedSourcePath, normalizedTargetPath);
|
||||
return sendSuccess(res, true);
|
||||
} catch (error) {
|
||||
return sendError(res, '移动失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 批量移动
|
||||
export const BatchMoveHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const { items } = req.body;
|
||||
for (const { sourcePath, targetPath } of items) {
|
||||
const normalizedSourcePath = normalizePath(sourcePath);
|
||||
const normalizedTargetPath = normalizePath(targetPath);
|
||||
await fs.rename(normalizedSourcePath, normalizedTargetPath);
|
||||
}
|
||||
return sendSuccess(res, true);
|
||||
} catch (error) {
|
||||
return sendError(res, '批量移动失败');
|
||||
}
|
||||
};
|
@@ -2,6 +2,7 @@ import type { RequestHandler } from 'express';
|
||||
import { sendError, sendSuccess } from '../utils/response';
|
||||
import { WebUiConfigWrapper } from '../helper/config';
|
||||
import { logSubscription } from '@/common/log';
|
||||
import { terminalManager } from '../terminal/terminal_manager';
|
||||
|
||||
// 日志记录
|
||||
export const LogHandler: RequestHandler = async (req, res) => {
|
||||
@@ -35,3 +36,25 @@ export const LogRealTimeHandler: RequestHandler = async (req, res) => {
|
||||
logSubscription.unsubscribe(listener);
|
||||
});
|
||||
};
|
||||
|
||||
// 终端相关处理器
|
||||
export const CreateTerminalHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const { id } = terminalManager.createTerminal();
|
||||
return sendSuccess(res, { id });
|
||||
} catch (error) {
|
||||
console.error('Failed to create terminal:', error);
|
||||
return sendError(res, '创建终端失败');
|
||||
}
|
||||
};
|
||||
|
||||
export const GetTerminalListHandler: RequestHandler = (_, res) => {
|
||||
const list = terminalManager.getTerminalList();
|
||||
return sendSuccess(res, list);
|
||||
};
|
||||
|
||||
export const CloseTerminalHandler: RequestHandler = (req, res) => {
|
||||
const id = req.params.id;
|
||||
terminalManager.closeTerminal(id);
|
||||
return sendSuccess(res, {});
|
||||
};
|
||||
|
36
src/webui/src/router/File.ts
Normal file
36
src/webui/src/router/File.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Router } from 'express';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import {
|
||||
ListFilesHandler,
|
||||
CreateDirHandler,
|
||||
DeleteHandler,
|
||||
ReadFileHandler,
|
||||
WriteFileHandler,
|
||||
CreateFileHandler,
|
||||
BatchDeleteHandler, // 添加这一行
|
||||
RenameHandler,
|
||||
MoveHandler,
|
||||
BatchMoveHandler,
|
||||
} from '../api/File';
|
||||
|
||||
const router = Router();
|
||||
|
||||
const apiLimiter = rateLimit({
|
||||
windowMs: 1 * 60 * 1000, // 1分钟内
|
||||
max: 60, // 最大60个请求
|
||||
});
|
||||
|
||||
router.use(apiLimiter);
|
||||
|
||||
router.get('/list', ListFilesHandler);
|
||||
router.post('/mkdir', CreateDirHandler);
|
||||
router.post('/delete', DeleteHandler);
|
||||
router.get('/read', ReadFileHandler);
|
||||
router.post('/write', WriteFileHandler);
|
||||
router.post('/create', CreateFileHandler);
|
||||
router.post('/batchDelete', BatchDeleteHandler);
|
||||
router.post('/rename', RenameHandler);
|
||||
router.post('/move', MoveHandler);
|
||||
router.post('/batchMove', BatchMoveHandler);
|
||||
|
||||
export { router as FileRouter };
|
@@ -1,13 +1,23 @@
|
||||
import { Router } from 'express';
|
||||
import { LogHandler, LogListHandler, LogRealTimeHandler } from '../api/Log';
|
||||
import {
|
||||
LogHandler,
|
||||
LogListHandler,
|
||||
LogRealTimeHandler,
|
||||
CreateTerminalHandler,
|
||||
GetTerminalListHandler,
|
||||
CloseTerminalHandler,
|
||||
} from '../api/Log';
|
||||
|
||||
const router = Router();
|
||||
// router:读取日志内容
|
||||
router.get('/GetLog', LogHandler);
|
||||
// router:读取日志列表
|
||||
router.get('/GetLogList', LogListHandler);
|
||||
|
||||
// router:实时日志
|
||||
// 日志相关路由
|
||||
router.get('/GetLog', LogHandler);
|
||||
router.get('/GetLogList', LogListHandler);
|
||||
router.get('/GetLogRealTime', LogRealTimeHandler);
|
||||
|
||||
// 终端相关路由
|
||||
router.get('/terminal/list', GetTerminalListHandler);
|
||||
router.post('/terminal/create', CreateTerminalHandler);
|
||||
router.post('/terminal/:id/close', CloseTerminalHandler);
|
||||
|
||||
export { router as LogRouter };
|
||||
|
@@ -12,6 +12,7 @@ import { QQLoginRouter } from '@webapi/router/QQLogin';
|
||||
import { AuthRouter } from '@webapi/router/auth';
|
||||
import { LogRouter } from '@webapi/router/Log';
|
||||
import { BaseRouter } from '@webapi/router/Base';
|
||||
import { FileRouter } from './File';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -32,5 +33,7 @@ router.use('/QQLogin', QQLoginRouter);
|
||||
router.use('/OB11Config', OB11ConfigRouter);
|
||||
// router:日志相关路由
|
||||
router.use('/Log', LogRouter);
|
||||
// file:文件相关路由
|
||||
router.use('/File', FileRouter);
|
||||
|
||||
export { router as ALLRouter };
|
||||
|
21
src/webui/src/terminal/init-dynamic-dirname.ts
Normal file
21
src/webui/src/terminal/init-dynamic-dirname.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import path from 'path';
|
||||
|
||||
Object.defineProperty(global, '__dirname', {
|
||||
get() {
|
||||
const err = new Error();
|
||||
const stack = err.stack?.split('\n') || [];
|
||||
let callerFile = '';
|
||||
// 遍历错误堆栈,跳过当前文件所在行
|
||||
// 注意:堆栈格式可能不同,请根据实际环境调整索引及正则表达式
|
||||
for (const line of stack) {
|
||||
const match = line.match(/\((.*):\d+:\d+\)/);
|
||||
if (match) {
|
||||
callerFile = match[1];
|
||||
if (!callerFile.includes('init-dynamic-dirname.ts')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return callerFile ? path.dirname(callerFile) : '';
|
||||
},
|
||||
});
|
175
src/webui/src/terminal/terminal_manager.ts
Normal file
175
src/webui/src/terminal/terminal_manager.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import './init-dynamic-dirname';
|
||||
import { WebUiConfig } from '@/webui';
|
||||
import { AuthHelper } from '../helper/SignToken';
|
||||
import { LogWrapper } from '@/common/log';
|
||||
import { WebSocket, WebSocketServer } from 'ws';
|
||||
import os from 'os';
|
||||
import { IPty, spawn as ptySpawn } from '@/pty';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
interface TerminalInstance {
|
||||
pty: IPty; // 改用 PTY 实例
|
||||
lastAccess: number;
|
||||
sockets: Set<WebSocket>;
|
||||
// 新增标识,用于防止重复关闭
|
||||
isClosing: boolean;
|
||||
}
|
||||
|
||||
class TerminalManager {
|
||||
private terminals: Map<string, TerminalInstance> = new Map();
|
||||
private wss: WebSocketServer | null = null;
|
||||
|
||||
initialize(req: any, socket: any, head: any, logger?: LogWrapper) {
|
||||
logger?.log('[NapCat] [WebUi] terminal websocket initialized');
|
||||
this.wss = new WebSocketServer({
|
||||
noServer: true,
|
||||
verifyClient: async (info, cb) => {
|
||||
// 验证 token
|
||||
const url = new URL(info.req.url || '', 'ws://localhost');
|
||||
const token = url.searchParams.get('token');
|
||||
const terminalId = url.searchParams.get('id');
|
||||
|
||||
if (!token || !terminalId) {
|
||||
cb(false, 401, 'Unauthorized');
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析token
|
||||
let Credential: WebUiCredentialJson;
|
||||
try {
|
||||
Credential = JSON.parse(Buffer.from(token, 'base64').toString('utf-8'));
|
||||
} catch (e) {
|
||||
cb(false, 401, 'Unauthorized');
|
||||
return;
|
||||
}
|
||||
const config = await WebUiConfig.GetWebUIConfig();
|
||||
const validate = AuthHelper.validateCredentialWithinOneHour(config.token, Credential);
|
||||
if (!validate) {
|
||||
cb(false, 401, 'Unauthorized');
|
||||
return;
|
||||
}
|
||||
cb(true);
|
||||
},
|
||||
});
|
||||
this.wss.handleUpgrade(req, socket, head, (ws) => {
|
||||
this.wss?.emit('connection', ws, req);
|
||||
});
|
||||
this.wss.on('connection', async (ws, req) => {
|
||||
logger?.log('建立终端连接');
|
||||
try {
|
||||
const url = new URL(req.url || '', 'ws://localhost');
|
||||
const terminalId = url.searchParams.get('id')!;
|
||||
|
||||
const instance = this.terminals.get(terminalId);
|
||||
|
||||
if (!instance) {
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const dataHandler = (data: string) => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: 'output', data }));
|
||||
}
|
||||
};
|
||||
|
||||
instance.sockets.add(ws);
|
||||
instance.lastAccess = Date.now();
|
||||
|
||||
ws.on('message', (data) => {
|
||||
if (instance) {
|
||||
const result = JSON.parse(data.toString());
|
||||
if (result.type === 'input') {
|
||||
instance.pty.write(result.data);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
instance.sockets.delete(ws);
|
||||
if (instance.sockets.size === 0 && !instance.isClosing) {
|
||||
instance.isClosing = true;
|
||||
if (os.platform() === 'win32') {
|
||||
process.kill(instance.pty.pid);
|
||||
} else {
|
||||
instance.pty.kill();
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('WebSocket authentication failed:', err);
|
||||
ws.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 修改:移除参数 id,使用 crypto.randomUUID 生成终端 id
|
||||
createTerminal() {
|
||||
const id = randomUUID();
|
||||
const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
|
||||
const pty = ptySpawn(shell, [], {
|
||||
name: 'xterm-256color',
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
// 统一编码设置
|
||||
LANG: os.platform() === 'win32' ? 'chcp 65001' : 'zh_CN.UTF-8',
|
||||
TERM: 'xterm-256color',
|
||||
},
|
||||
});
|
||||
|
||||
const instance: TerminalInstance = {
|
||||
pty,
|
||||
lastAccess: Date.now(),
|
||||
sockets: new Set(),
|
||||
isClosing: false,
|
||||
};
|
||||
|
||||
pty.onData((data: any) => {
|
||||
instance.sockets.forEach((ws) => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: 'output', data }));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
pty.onExit(() => {
|
||||
this.closeTerminal(id);
|
||||
});
|
||||
|
||||
this.terminals.set(id, instance);
|
||||
// 返回生成的 id 及对应实例,方便后续通知客户端使用该 id
|
||||
return { id, instance };
|
||||
}
|
||||
|
||||
closeTerminal(id: string) {
|
||||
const instance = this.terminals.get(id);
|
||||
if (instance) {
|
||||
if (!instance.isClosing) {
|
||||
instance.isClosing = true;
|
||||
if (os.platform() === 'win32') {
|
||||
process.kill(instance.pty.pid);
|
||||
} else {
|
||||
instance.pty.kill();
|
||||
}
|
||||
}
|
||||
instance.sockets.forEach((ws) => ws.close());
|
||||
this.terminals.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
getTerminal(id: string) {
|
||||
return this.terminals.get(id);
|
||||
}
|
||||
|
||||
getTerminalList() {
|
||||
return Array.from(this.terminals.keys()).map((id) => ({
|
||||
id,
|
||||
lastAccess: this.terminals.get(id)!.lastAccess,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
export const terminalManager = new TerminalManager();
|
Reference in New Issue
Block a user