Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cli, core: support invoking remote commands #1185

Merged
merged 3 commits into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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