Skip to content

Commit

Permalink
cli, core: support invoking remote commands (#1185)
Browse files Browse the repository at this point in the history
* cli, core: support launching remote commands

* fixes

* preserve buffer format for child process
  • Loading branch information
bjia56 authored Nov 15, 2023
1 parent cf9a065 commit 772bfec
Show file tree
Hide file tree
Showing 3 changed files with 40 additions and 19 deletions.
6 changes: 4 additions & 2 deletions packages/cli/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,9 @@ async function main() {
}
});

await connectShell(sdk);
const separator = process.argv.indexOf("--");
const cmd = separator != -1 ? process.argv.slice(separator + 1): [];
await connectShell(sdk, ...cmd);
}
else {
console.log('usage:');
Expand All @@ -249,7 +251,7 @@ async function main() {
console.log(' npx scrypted command name-or-id[@127.0.0.1[:10443]] method-name [...method-arguments]');
console.log(' npx scrypted ffplay name-or-id[@127.0.0.1[:10443]] method-name [...method-arguments]');
console.log(' npx scrypted create-cert-json /path/to/key.pem /path/to/cert.pem');
console.log(' npx scrypted shell [127.0.0.1[:10443]]');
console.log(' npx scrypted shell [127.0.0.1[:10443]] [-- cmd [...cmd-args]]');
console.log();
console.log('examples:');
console.log(' npx scrypted install @scrypted/rtsp');
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/shell.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DeviceProvider, ScryptedStatic, StreamService } from "@scrypted/types";
import { createAsyncQueue } from '../../../common/src/async-queue';

export async function connectShell(sdk: ScryptedStatic) {
export async function connectShell(sdk: ScryptedStatic, ...cmd: string[]) {
const termSvc = await sdk.systemManager.getDeviceByName<DeviceProvider>("@scrypted/core").getDevice("terminalservice");
if (!termSvc) {
throw Error("@scrypted/core does not provide a Terminal Service");
Expand All @@ -19,7 +19,7 @@ export async function connectShell(sdk: ScryptedStatic) {
dataQueue.enqueue(Buffer.alloc(0));
});
}
ctrlQueue.enqueue({ interactive: Boolean(process.stdin.isTTY) });
ctrlQueue.enqueue({ interactive: Boolean(process.stdin.isTTY), cmd: cmd });

const dim = { cols: process.stdout.columns, rows: process.stdout.rows };
ctrlQueue.enqueue({ dim });
Expand Down
49 changes: 34 additions & 15 deletions plugins/core/src/terminal-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@ export const TerminalServiceNativeId = 'terminalservice';
class InteractiveTerminal {
cp: IPty

constructor() {
constructor(cmd: string[]) {
const spawn = require('node-pty-prebuilt-multiarch').spawn as typeof ptySpawn;
this.cp = spawn(process.env.SHELL as string, [], {});
if (cmd?.length) {
this.cp = spawn(cmd[0], cmd.slice(1), {});
} else {
this.cp = spawn(process.env.SHELL as string, [], {});
}
}

onExit(fn: (e: { exitCode: number; signal?: number; }) => any) {
Expand All @@ -30,8 +34,8 @@ class InteractiveTerminal {
this.cp.resume();
}

write(data: string) {
this.cp.write(data);
write(data: Buffer) {
this.cp.write(data.toString());
}

sendEOF() {
Expand All @@ -43,15 +47,20 @@ class InteractiveTerminal {
}

resize(columns: number, rows: number) {
this.cp.resize(columns, rows);
if (columns > 0 && rows > 0)
this.cp.resize(columns, rows);
}
}

class NoninteractiveTerminal {
cp: ChildProcess

constructor() {
this.cp = childSpawn(process.env.SHELL as string);
constructor(cmd: string[]) {
if (cmd?.length) {
this.cp = childSpawn(cmd[0], cmd.slice(1));
} else {
this.cp = childSpawn(process.env.SHELL as string);
}
}

onExit(fn: (code: number, signal: NodeJS.Signals) => void) {
Expand All @@ -69,11 +78,11 @@ class NoninteractiveTerminal {
}

resume() {
this.cp.stdout.pause();
this.cp.stderr.pause();
this.cp.stdout.resume();
this.cp.stderr.resume();
}

write(data: any) {
write(data: Buffer) {
this.cp.stdin.write(data);
}

Expand All @@ -92,7 +101,17 @@ class NoninteractiveTerminal {


export class TerminalService extends ScryptedDeviceBase implements StreamService {
async connectStream(input: AsyncGenerator<any, void>): Promise<AsyncGenerator<any, void>> {
/*
* The input to this stream can send buffers for normal terminal data and strings
* for control messages. Control messages are JSON-formatted.
*
* The current implemented control messages:
*
* Start: { "interactive": boolean, "cmd": string[] }
* Resize: { "dim": { "cols": number, "rows": number } }
* EOF: { "eof": true }
*/
async connectStream(input: AsyncGenerator<Buffer | string, void>): Promise<AsyncGenerator<Buffer, void>> {
let cp: InteractiveTerminal | NoninteractiveTerminal = null;
const queue = createAsyncQueue<Buffer>();

Expand Down Expand Up @@ -140,7 +159,7 @@ export class TerminalService extends ScryptedDeviceBase implements StreamService

if (Buffer.isBuffer(message)) {
if (cp)
cp.write(message.toString());
cp.write(message);
continue;
}

Expand All @@ -154,15 +173,15 @@ export class TerminalService extends ScryptedDeviceBase implements StreamService
cp.sendEOF();
} else if ("interactive" in parsed && !cp) {
if (parsed.interactive) {
cp = new InteractiveTerminal();
cp = new InteractiveTerminal(parsed.cmd);
} else {
cp = new NoninteractiveTerminal();
cp = new NoninteractiveTerminal(parsed.cmd);
}
registerChildListeners();
}
} catch {
if (cp)
cp.write(message.toString());
cp.write(Buffer.from(message));
}
}
}
Expand Down

0 comments on commit 772bfec

Please sign in to comment.