Skip to content

Commit f49662f

Browse files
committed
feat(cli): simplify CLI script execution
WARNING: Drops support for running remote scripts from the Default Servient Thing. This feature was rarely used and controversial due to security implications. Users wanting to run remote scripts can easily implement this feature in their own Servient by extending the CLI Servient.
1 parent 7bb51f3 commit f49662f

11 files changed

Lines changed: 81 additions & 141 deletions

File tree

packages/binding-mqtt/src/mqtt-broker-server.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -445,8 +445,6 @@ export default class MqttBrokerServer implements ProtocolServer {
445445

446446
private async startBroker() {
447447
return new Promise<void>((resolve, reject) => {
448-
console.log("here mf");
449-
450448
if (this.brokerURI == null) {
451449
throw new Error("Unexpected configuration state broker was started even if brokerURI is null");
452450
}

packages/cli/import-json.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
const { readFileSync, writeFileSync } = require("fs");
2+
const { existsSync, mkdirSync } = require("fs");
23

34
const schema = readFileSync("./src/wot-servient-schema.conf.json", "utf8");
45
const package = readFileSync("./package.json", "utf8");
56
const { version } = JSON.parse(package);
67

8+
const generatedDir = "./src/generated";
9+
if (!existsSync(generatedDir)) {
10+
mkdirSync(generatedDir, { recursive: true });
11+
}
12+
713
writeFileSync(
814
"./src/generated/wot-servient-schema.conf.ts",
915
`const schema = ${schema.trimEnd()} as const \nexport default schema;`

packages/cli/src/cli-default-servient.ts

Lines changed: 2 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,13 @@ import { HttpServer, HttpClientFactory, HttpsClientFactory } from "@node-wot/bin
2424
import { CoapServer, CoapClientFactory, CoapsClientFactory } from "@node-wot/binding-coap";
2525
import { MqttBrokerServer, MqttClientFactory } from "@node-wot/binding-mqtt";
2626
import { FileClientFactory } from "@node-wot/binding-file";
27-
import { ThingModelHelpers } from "@thingweb/thing-model";
28-
import { createContext, Script } from "vm";
29-
import { CompilerFunction } from "./compiler-function";
30-
import { LogLevel, setLogLevel } from "./utils/set-log-level";
27+
import { LogLevel, setLogLevel } from "./utils";
3128
import { ConfigurationAfterDefaults } from "./configuration";
3229

33-
const { debug, error, info } = createLoggers("cli", "cli-default-servient");
30+
const { debug, info } = createLoggers("cli", "cli-default-servient");
3431

35-
export interface ScriptOptions {
36-
argv?: Array<string>;
37-
compiler?: CompilerFunction;
38-
env?: Record<string, string>;
39-
}
4032
export default class DefaultServient extends Servient {
4133
private uncaughtListeners: Array<NodeJS.UncaughtExceptionListener> = [];
42-
private runtime: typeof WoT | undefined;
4334
public readonly config: ConfigurationAfterDefaults;
4435
// current log level
4536
public logLevel = "info";
@@ -99,78 +90,11 @@ export default class DefaultServient extends Servient {
9990
this.addClientFactory(new MqttClientFactory());
10091
}
10192

102-
/**
103-
* Runs the script in privileged context (dangerous). In practice, this means that the script can
104-
* require system modules.
105-
* @param {string} code - the script to run
106-
* @param {string} filename - the filename of the script
107-
* @param {object} options - pass cli variables or envs to the script
108-
*/
109-
public runScript(code: string, filename = "script", options: ScriptOptions = {}): unknown {
110-
if (!this.runtime) {
111-
throw new Error("WoT runtime not loaded; have you called start()?");
112-
}
113-
const helpers = new Helpers(this);
114-
115-
options.compiler ??= (code) => code;
116-
117-
const compiledCode = options.compiler(code, filename);
118-
const script = new Script(compiledCode, filename);
119-
process.argv = options.argv ?? [];
120-
process.env = options.env ?? process.env;
121-
const context = createContext({
122-
...globalThis,
123-
process,
124-
require,
125-
console,
126-
exports: {},
127-
WoT: this.runtime,
128-
WoTHelpers: helpers,
129-
ModelHelpers: new ThingModelHelpers(helpers),
130-
});
131-
132-
const listener = (err: Error) => {
133-
this.logScriptError(`Asynchronous script error '${filename}'`, err);
134-
// TODO: clean up script resources
135-
process.exit(1);
136-
};
137-
138-
process.prependListener("uncaughtException", listener);
139-
this.uncaughtListeners.push(listener);
140-
141-
try {
142-
return script.runInContext(context, { displayErrors: true });
143-
} catch (err) {
144-
if (err instanceof Error) {
145-
this.logScriptError(`Servient found error in privileged script '${filename}'`, err);
146-
} else {
147-
error(`Servient found error in privileged script '${filename}' ${err}`);
148-
}
149-
return undefined;
150-
}
151-
}
152-
153-
private logScriptError(description: string, err: Error): void {
154-
let message: string;
155-
if (typeof err === "object" && err.stack != null) {
156-
const match = err.stack.match(/evalmachine\.<anonymous>:([0-9]+:[0-9]+)/);
157-
if (Array.isArray(match)) {
158-
message = `and halted at line ${match[1]}\n ${err}`;
159-
} else {
160-
message = `and halted with ${err.stack}`;
161-
}
162-
} else {
163-
message = `that threw ${typeof err} instead of Error\n ${err}`;
164-
}
165-
error(`Servient caught ${description} ${message}`);
166-
}
167-
16893
/**
16994
* start
17095
*/
17196
public async start(): Promise<typeof WoT> {
17297
const superWoT = await super.start();
173-
this.runtime = superWoT;
17498

17599
info("DefaultServient started");
176100

@@ -198,15 +122,6 @@ export default class DefaultServient extends Servient {
198122
description: "Stop servient",
199123
output: { type: "string" },
200124
},
201-
...(this.config.servient.scriptAction === true
202-
? {
203-
runScript: {
204-
description: "Run script",
205-
input: { type: "string" },
206-
output: { type: "string" },
207-
},
208-
}
209-
: {}),
210125
},
211126
});
212127

@@ -221,14 +136,6 @@ export default class DefaultServient extends Servient {
221136
await this.shutdown();
222137
return undefined;
223138
});
224-
if (this.config.servient.scriptAction === true) {
225-
servientProducedThing.setActionHandler("runScript", async (script) => {
226-
const scriptv = await Helpers.parseInteractionOutput(script);
227-
debug("running script", scriptv);
228-
this.runScript(scriptv as string);
229-
return undefined;
230-
});
231-
}
232139
servientProducedThing.setPropertyReadHandler("things", async () => {
233140
debug("returning things");
234141
return this.getThings();

packages/cli/src/cli.ts

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,16 @@
1414
********************************************************************************/
1515

1616
// default implementation of W3C WoT Servient (http(s) and file bindings)
17-
import DefaultServient, { ScriptOptions } from "./cli-default-servient";
17+
import DefaultServient from "./cli-default-servient";
1818

1919
// tools
2020
import * as path from "path";
2121
import { Command, Argument, Option } from "commander";
2222
import Ajv, { ValidateFunction } from "ajv";
2323
import ConfigSchema from "./generated/wot-servient-schema.conf";
2424
import version from "./generated/version";
25-
import { createLoggers } from "@node-wot/core";
26-
import { loadCompiler, loadEnvVariables } from "./utils";
25+
import { createLoggers, Helpers } from "@node-wot/core";
26+
import { loadEnvVariables } from "./utils";
2727
import { runScripts } from "./script-runner";
2828
import { readdir } from "fs/promises";
2929
import { parseConfigFile, parseConfigParams, parseIp } from "./parsers";
@@ -180,24 +180,19 @@ program.action(async function (_, options, cmd) {
180180
servient = new DefaultServient(config);
181181
}
182182

183-
await servient.start();
184-
185-
const scriptOptions: ScriptOptions = {
186-
env,
187-
argv: args,
188-
compiler: loadCompiler(options.compiler),
189-
};
183+
const runtime = await servient.start();
184+
const helpers = new Helpers(servient);
190185

191186
if (args.length > 0) {
192-
return runScripts(servient, args, scriptOptions, options.inspect ?? options.inspectBrk);
187+
return runScripts({ runtime, helpers }, args, options.inspect ?? options.inspectBrk);
193188
}
194189

195190
const files = await readdir(baseDir);
196191
const scripts = files.filter((file) => !file.startsWith(".") && file.slice(-3) === ".js");
197192

198193
info(`WoT-Servient using current directory with %d script${scripts.length > 1 ? "s" : ""}`, scripts.length);
199194

200-
return runScripts(servient, args, scriptOptions, options.inspect ?? options.inspectBrk);
195+
return runScripts({ runtime, helpers }, scripts, options.inspect ?? options.inspectBrk);
201196
});
202197

203198
program.parse(process.argv);

packages/cli/src/executor.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/********************************************************************************
2+
* Copyright (c) 2025 Contributors to the Eclipse Foundation
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information regarding copyright ownership.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License v. 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and
10+
* Document License (2015-05-13) which is available at
11+
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document.
12+
*
13+
* SPDX-License-Identifier: EPL-2.0 OR W3C-20150513
14+
********************************************************************************/
15+
16+
import { createLoggers, Helpers } from "@node-wot/core";
17+
const { debug } = createLoggers("cli", "executor");
18+
19+
export interface WoTContext {
20+
runtime: typeof WoT;
21+
helpers: Helpers;
22+
}
23+
24+
export class Executor {
25+
public async exec(file: string, wotContext: WoTContext): Promise<unknown> {
26+
debug(`Executing WoT script from file: ${file}`);
27+
const userScriptPathArg = file;
28+
const isTypeScriptScript =
29+
userScriptPathArg && (userScriptPathArg.endsWith(".ts") || userScriptPathArg.endsWith(".tsx"));
30+
global.WoT = wotContext.runtime;
31+
32+
if (isTypeScriptScript === true) {
33+
require("ts-node/register");
34+
}
35+
36+
try {
37+
// Execute the user's script
38+
// Node.js will now handle .ts files automatically if ts-node is registered
39+
// TODO: For ESM modules a more complex check might be needed.
40+
if (file.endsWith(".mjs")) {
41+
return await import(`file:///${file}`);
42+
} else {
43+
return require(file);
44+
}
45+
} catch (error) {
46+
console.error("Error running WoT script:", error);
47+
process.exit(1);
48+
}
49+
}
50+
}

packages/cli/src/script-runner.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313
* SPDX-License-Identifier: EPL-2.0 OR W3C-20150513
1414
********************************************************************************/
1515
import { createLoggers } from "@node-wot/core";
16-
import DefaultServient, { ScriptOptions } from "./cli-default-servient";
1716
import inspector from "inspector";
1817
import path from "path";
1918
import { readFile } from "fs/promises";
19+
import { Executor, WoTContext } from "./executor";
2020

2121
const { error, info, warn } = createLoggers("cli", "cli", "script-runner");
2222

@@ -25,12 +25,8 @@ export interface DebugParams {
2525
host: string;
2626
port: number;
2727
}
28-
export async function runScripts(
29-
servient: DefaultServient,
30-
scripts: string[],
31-
options: ScriptOptions,
32-
debug?: DebugParams
33-
) {
28+
export async function runScripts(context: WoTContext, scripts: string[], debug?: DebugParams) {
29+
const executor = new Executor();
3430
const launchScripts = (scripts: Array<string>) => {
3531
scripts.forEach(async (fname: string) => {
3632
info(`WoT-Servient reading script ${fname}`);
@@ -44,9 +40,9 @@ export async function runScripts(
4440
);
4541

4642
fname = path.resolve(fname);
47-
servient.runScript(data, fname, options);
43+
await executor.exec(fname, context);
4844
} catch (err) {
49-
error(`WoT-Servient experienced error while reading script. ${err}`);
45+
error(`WoT-Servient experienced error while reading script. %O`, err);
5046
}
5147
});
5248
};

packages/cli/src/utils/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@
1313
* SPDX-License-Identifier: EPL-2.0 OR W3C-20150513
1414
********************************************************************************/
1515
export * from "./load-env-variables";
16-
export * from "./load-compiler";
16+
export * from "./set-log-level";

packages/examples/src/scripts/counter.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
// * multi-language
2424
// * image contentTypes for properties (Note: the contentType applies to all forms of the property)
2525
// * links with entry containing rel and sizes
26-
2726
let count: number;
2827
let lastChange: string;
2928

packages/examples/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@
88
"sourceMap": false,
99
"removeComments": false
1010
},
11-
"include": ["src/**/*"],
11+
"include": ["src/**/*", "../../node_modules/wot-typescript-definitions/**/*.d.ts"],
1212
"references": []
1313
}
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,7 @@
1212
*
1313
* SPDX-License-Identifier: EPL-2.0 OR W3C-20150513
1414
********************************************************************************/
15-
export type CompilerFunction = (code: string, filename: string) => string;
15+
16+
export function hello() {
17+
throw new Error("This is an error");
18+
}

0 commit comments

Comments
 (0)