按建议,继续完成后续工作
结论:已新增 INI WASM JS SDK 封装,把 UI 和 Node smoke test 改为通过 SDK 调用 vendored LinuxCNC inifile.cc 的导出 C ABI,并同步更新兼容性、复用和漂移文档。
This commit is contained in:
@@ -49,8 +49,8 @@ The native validation script runs these checks in order:
|
|||||||
|
|
||||||
The WASM INI smoke script builds `runtime/ui/ini-panel/linuxcnc_ini.js` and
|
The WASM INI smoke script builds `runtime/ui/ini-panel/linuxcnc_ini.js` and
|
||||||
`linuxcnc_ini.wasm` from vendored LinuxCNC `inifile.cc`, then loads that
|
`linuxcnc_ini.wasm` from vendored LinuxCNC `inifile.cc`, then loads that
|
||||||
module in Node and verifies `lcini_get_string()` against an in-memory INI file
|
module through `runtime/sdk/src/linuxcnc-ini.js` in Node and verifies INI
|
||||||
written to the Emscripten filesystem.
|
queries against a file written to the Emscripten filesystem.
|
||||||
|
|
||||||
The OPFS host-boundary script validates the JavaScript file-service adapter
|
The OPFS host-boundary script validates the JavaScript file-service adapter
|
||||||
with a Node mock of the browser File System Access handles. It covers nested
|
with a Node mock of the browser File System Access handles. It covers nested
|
||||||
@@ -103,7 +103,7 @@ The validation fails if:
|
|||||||
|
|
||||||
| Harness | Purpose |
|
| Harness | Purpose |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `tests/wasm/node/verify_ini_wasm.sh` | Validates the browser-facing INI WASM module can be built from vendored LinuxCNC `inifile.cc`, loaded in Node, and queried through the exported C ABI. |
|
| `tests/wasm/node/verify_ini_wasm.sh` | Validates the browser-facing INI WASM module can be built from vendored LinuxCNC `inifile.cc`, loaded through the JS SDK in Node, and queried through the exported C ABI. |
|
||||||
| `tests/opfs/node/verify_file_service.sh` | Validates the host-owned OPFS text-file adapter used by the browser INI panel without moving file persistence into the WASM core. |
|
| `tests/opfs/node/verify_file_service.sh` | Validates the host-owned OPFS text-file adapter used by the browser INI panel without moving file persistence into the WASM core. |
|
||||||
|
|
||||||
## Fixture Coverage
|
## Fixture Coverage
|
||||||
@@ -147,10 +147,10 @@ Negative fixtures currently cover:
|
|||||||
|
|
||||||
## Validation Boundaries
|
## Validation Boundaries
|
||||||
|
|
||||||
Current full-core validation is native-only. WASM validation is limited to the
|
Current full-core validation is native-only. WASM/SDK validation is limited to
|
||||||
INI parser smoke harness. OPFS validation is limited to the JavaScript
|
the INI parser smoke harness. OPFS validation is limited to the JavaScript
|
||||||
host-boundary adapter. Browser, SDK, and full machine-session validation
|
host-boundary adapter. Browser-level validation, full SDK coverage, and full
|
||||||
remain future work.
|
machine-session validation remain future work.
|
||||||
|
|
||||||
The current fixture expectations validate standalone behavior against both the
|
The current fixture expectations validate standalone behavior against both the
|
||||||
vendored LinuxCNC source path and an upstream `rs274` side-by-side baseline for
|
vendored LinuxCNC source path and an upstream `rs274` side-by-side baseline for
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ semantic rewrites:
|
|||||||
|
|
||||||
- No browser/full-core WASM parity tests yet. The INI parser now has a Node
|
- No browser/full-core WASM parity tests yet. The INI parser now has a Node
|
||||||
WASM smoke harness against vendored LinuxCNC `inifile.cc`.
|
WASM smoke harness against vendored LinuxCNC `inifile.cc`.
|
||||||
- No JS SDK validation yet.
|
- JS SDK validation is currently limited to the INI WASM wrapper around
|
||||||
|
vendored LinuxCNC `inifile.cc`.
|
||||||
- OPFS validation is currently limited to a Node mock of the browser
|
- OPFS validation is currently limited to a Node mock of the browser
|
||||||
file-service adapter; browser-level OPFS validation is not yet established.
|
file-service adapter; browser-level OPFS validation is not yet established.
|
||||||
- Identity/trivial, `5axiskins`, TRT `xyzac`/`xyzbc`, delta, SCARA, PUMA,
|
- Identity/trivial, `5axiskins`, TRT `xyzac`/`xyzbc`, delta, SCARA, PUMA,
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ Current validation is intentionally mechanical:
|
|||||||
|
|
||||||
| Capability | LinuxCNC source files | Port classification | Standalone boundary | Current validation |
|
| Capability | LinuxCNC source files | Port classification | Standalone boundary | Current validation |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| INI parsing | `src/emc/ini/inifile.cc`, `inifile.h`, `inifile.hh` | Copy unchanged | Native file IO remains LinuxCNC-style in the vendored parser; browser OPFS integration remains outside this layer | Vendor byte sync, `linuxcnc_ini_probe`, `linuxcnc_inifile_source_probe`, `tests/wasm/node/verify_ini_wasm.sh` |
|
| INI parsing | `src/emc/ini/inifile.cc`, `inifile.h`, `inifile.hh` | Copy unchanged | Native file IO remains LinuxCNC-style in the vendored parser; browser OPFS integration remains outside this layer; `runtime/sdk/src/linuxcnc-ini.js` only wraps the exported WASM C ABI | Vendor byte sync, `linuxcnc_ini_probe`, `linuxcnc_inifile_source_probe`, `tests/wasm/node/verify_ini_wasm.sh` |
|
||||||
| RTAPI compatibility headers | `src/rtapi/rtapi_*.h` in the manifest | Copy unchanged plus standalone shim include path | `runtime/core/shims/rtapi.h` supplies the minimal standalone RTAPI surface needed by vendored code | Vendor byte sync, compile coverage through dependent source probes |
|
| RTAPI compatibility headers | `src/rtapi/rtapi_*.h` in the manifest | Copy unchanged plus standalone shim include path | `runtime/core/shims/rtapi.h` supplies the minimal standalone RTAPI surface needed by vendored code | Vendor byte sync, compile coverage through dependent source probes |
|
||||||
| Canon/NML-facing interpreter types | `src/emc/nml_intf/canon*.hh`, `emctool.h`, `interp_return.hh`, `motion_types.h`, `emcpose.*`, `emcpos.h`, `debugflags.h`, `src/emc/linuxcnc.h` | Copy unchanged plus narrow standalone status shim | NML transport is not ported; `runtime/core/shims/nml_intf/emc.hh` exposes only the `emcStatus` machine-units status edge currently needed by vendored interpreter conversion and initialization code | Vendor byte sync, dependent source probes, `linuxcnc_emc_status_probe`, `linuxcnc_tp_api_probe`, interpreter harnesses |
|
| Canon/NML-facing interpreter types | `src/emc/nml_intf/canon*.hh`, `emctool.h`, `interp_return.hh`, `motion_types.h`, `emcpose.*`, `emcpos.h`, `debugflags.h`, `src/emc/linuxcnc.h` | Copy unchanged plus narrow standalone status shim | NML transport is not ported; `runtime/core/shims/nml_intf/emc.hh` exposes only the `emcStatus` machine-units status edge currently needed by vendored interpreter conversion and initialization code | Vendor byte sync, dependent source probes, `linuxcnc_emc_status_probe`, `linuxcnc_tp_api_probe`, interpreter harnesses |
|
||||||
| Motion state headers | `src/emc/motion/state_tag.h`, `emcmotcfg.h`, `simple_tp.h`, `motion.h`, `mot_priv.h`, `axis.h` | Copy unchanged | Realtime motion process is not ported; standalone probes seed the small motion status/config state required by TP calls | Vendor byte sync, `linuxcnc_tp_api_probe` |
|
| Motion state headers | `src/emc/motion/state_tag.h`, `emcmotcfg.h`, `simple_tp.h`, `motion.h`, `mot_priv.h`, `axis.h` | Copy unchanged | Realtime motion process is not ported; standalone probes seed the small motion status/config state required by TP calls | Vendor byte sync, `linuxcnc_tp_api_probe` |
|
||||||
@@ -69,7 +69,8 @@ Current validation is intentionally mechanical:
|
|||||||
- Cutter compensation positive motion and negative interpreter paths are
|
- Cutter compensation positive motion and negative interpreter paths are
|
||||||
fixture-covered through vendored `interp_convert.cc` and `interp_queue.cc`.
|
fixture-covered through vendored `interp_convert.cc` and `interp_queue.cc`.
|
||||||
- Browser/WASM C ABI and JS SDK layers are not yet built for the full
|
- Browser/WASM C ABI and JS SDK layers are not yet built for the full
|
||||||
interpreter/planner core. The INI parser has a Node WASM smoke harness.
|
interpreter/planner core. The INI parser has a minimal JS SDK wrapper and
|
||||||
|
Node WASM smoke harness.
|
||||||
- OPFS persistence is connected to the INI panel through the host-side
|
- OPFS persistence is connected to the INI panel through the host-side
|
||||||
`runtime/opfs/file-service.js` adapter; tool table, parameter file, G-code
|
`runtime/opfs/file-service.js` adapter; tool table, parameter file, G-code
|
||||||
program loading, and browser-level OPFS validation remain future work.
|
program loading, and browser-level OPFS validation remain future work.
|
||||||
|
|||||||
4
wasm-port/runtime/sdk/package.json
Normal file
4
wasm-port/runtime/sdk/package.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
67
wasm-port/runtime/sdk/src/linuxcnc-ini.js
Normal file
67
wasm-port/runtime/sdk/src/linuxcnc-ini.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import createLinuxCncIniModule from "../../ui/ini-panel/linuxcnc_ini.js";
|
||||||
|
|
||||||
|
function allocCString(mod, value) {
|
||||||
|
const bytes = mod.lengthBytesUTF8(value) + 1;
|
||||||
|
const ptr = mod._malloc(bytes);
|
||||||
|
mod.stringToUTF8(value, ptr, bytes);
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureParentPath(mod, path) {
|
||||||
|
const parts = path.split("/").filter(Boolean);
|
||||||
|
let current = "";
|
||||||
|
for (const part of parts.slice(0, -1)) {
|
||||||
|
current += `/${part}`;
|
||||||
|
try {
|
||||||
|
mod.FS.mkdir(current);
|
||||||
|
} catch {
|
||||||
|
// Directory already exists.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createLinuxCncIniSdk(moduleOptions = {}) {
|
||||||
|
const mod = await createLinuxCncIniModule(moduleOptions);
|
||||||
|
|
||||||
|
return {
|
||||||
|
module: mod,
|
||||||
|
|
||||||
|
writeTextFile(path, text) {
|
||||||
|
ensureParentPath(mod, path);
|
||||||
|
mod.FS.writeFile(path, text, { encoding: "utf8" });
|
||||||
|
},
|
||||||
|
|
||||||
|
getString(path, section, tag) {
|
||||||
|
const pathPtr = allocCString(mod, path);
|
||||||
|
const sectionPtr = allocCString(mod, section);
|
||||||
|
const tagPtr = allocCString(mod, tag);
|
||||||
|
const outSize = 2048;
|
||||||
|
const outPtr = mod._malloc(outSize);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rc = mod._lcini_get_string(
|
||||||
|
pathPtr,
|
||||||
|
sectionPtr,
|
||||||
|
tagPtr,
|
||||||
|
outPtr,
|
||||||
|
outSize,
|
||||||
|
);
|
||||||
|
return rc === 0 ? mod.UTF8ToString(outPtr) : null;
|
||||||
|
} finally {
|
||||||
|
mod._free(pathPtr);
|
||||||
|
mod._free(sectionPtr);
|
||||||
|
mod._free(tagPtr);
|
||||||
|
mod._free(outPtr);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getFields(path, fields) {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(fields).map(([name, query]) => [
|
||||||
|
name,
|
||||||
|
this.getString(path, query.section, query.tag),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import createLinuxCncIniModule from "./linuxcnc_ini.js";
|
import { createLinuxCncIniSdk } from "../../sdk/src/linuxcnc-ini.js";
|
||||||
import {
|
import {
|
||||||
getOpfsRoot,
|
getOpfsRoot,
|
||||||
loadTextFile,
|
loadTextFile,
|
||||||
@@ -24,7 +24,7 @@ const fields = {
|
|||||||
parameterFile: document.getElementById("field-parameter-file"),
|
parameterFile: document.getElementById("field-parameter-file"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let moduleInstance = null;
|
let iniSdk = null;
|
||||||
|
|
||||||
function setBadge(node, text, className = "badge") {
|
function setBadge(node, text, className = "badge") {
|
||||||
node.className = className;
|
node.className = className;
|
||||||
@@ -40,49 +40,15 @@ function setField(name, value) {
|
|||||||
fields[name].textContent = value ?? "-";
|
fields[name].textContent = value ?? "-";
|
||||||
}
|
}
|
||||||
|
|
||||||
function requireModule() {
|
function requireIniSdk() {
|
||||||
if (!moduleInstance) {
|
if (!iniSdk) {
|
||||||
throw new Error("WASM module is not ready yet.");
|
throw new Error("WASM module is not ready yet.");
|
||||||
}
|
}
|
||||||
return moduleInstance;
|
return iniSdk;
|
||||||
}
|
|
||||||
|
|
||||||
function allocCString(mod, value) {
|
|
||||||
const bytes = mod.lengthBytesUTF8(value) + 1;
|
|
||||||
const ptr = mod._malloc(bytes);
|
|
||||||
mod.stringToUTF8(value, ptr, bytes);
|
|
||||||
return ptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
function iniQuery(mod, wasmPath, section, tag) {
|
|
||||||
const pathPtr = allocCString(mod, wasmPath);
|
|
||||||
const sectionPtr = allocCString(mod, section);
|
|
||||||
const tagPtr = allocCString(mod, tag);
|
|
||||||
const outSize = 2048;
|
|
||||||
const outPtr = mod._malloc(outSize);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const rc = mod._lcini_get_string(pathPtr, sectionPtr, tagPtr, outPtr, outSize);
|
|
||||||
if (rc !== 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return mod.UTF8ToString(outPtr);
|
|
||||||
} finally {
|
|
||||||
mod._free(pathPtr);
|
|
||||||
mod._free(sectionPtr);
|
|
||||||
mod._free(tagPtr);
|
|
||||||
mod._free(outPtr);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncEditorToWasmFs() {
|
function syncEditorToWasmFs() {
|
||||||
const mod = requireModule();
|
requireIniSdk().writeTextFile(WASM_FILE, editor.value);
|
||||||
try {
|
|
||||||
mod.FS.mkdir("/work");
|
|
||||||
} catch {
|
|
||||||
// already exists
|
|
||||||
}
|
|
||||||
mod.FS.writeFile(WASM_FILE, editor.value, { encoding: "utf8" });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadSample() {
|
async function loadSample() {
|
||||||
@@ -97,7 +63,7 @@ async function loadSample() {
|
|||||||
|
|
||||||
async function boot() {
|
async function boot() {
|
||||||
try {
|
try {
|
||||||
moduleInstance = await createLinuxCncIniModule();
|
iniSdk = await createLinuxCncIniSdk();
|
||||||
setBadge(wasmBadge, "WASM: ready");
|
setBadge(wasmBadge, "WASM: ready");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setBadge(wasmBadge, "WASM: failed", "badge danger");
|
setBadge(wasmBadge, "WASM: failed", "badge danger");
|
||||||
@@ -152,17 +118,18 @@ document.getElementById("sync-wasm").addEventListener("click", () => {
|
|||||||
document.getElementById("query").addEventListener("click", async () => {
|
document.getElementById("query").addEventListener("click", async () => {
|
||||||
try {
|
try {
|
||||||
syncEditorToWasmFs();
|
syncEditorToWasmFs();
|
||||||
const mod = requireModule();
|
const values = requireIniSdk().getFields(WASM_FILE, {
|
||||||
|
machine: { section: "EMC", tag: "MACHINE" },
|
||||||
|
display: { section: "DISPLAY", tag: "DISPLAY" },
|
||||||
|
kinematics: { section: "KINS", tag: "KINEMATICS" },
|
||||||
|
joints: { section: "KINS", tag: "JOINTS" },
|
||||||
|
coordinates: { section: "TRAJ", tag: "COORDINATES" },
|
||||||
|
parameterFile: { section: "RS274NGC", tag: "PARAMETER_FILE" },
|
||||||
|
});
|
||||||
|
|
||||||
setField("machine", iniQuery(mod, WASM_FILE, "EMC", "MACHINE"));
|
for (const [name, value] of Object.entries(values)) {
|
||||||
setField("display", iniQuery(mod, WASM_FILE, "DISPLAY", "DISPLAY"));
|
setField(name, value);
|
||||||
setField("kinematics", iniQuery(mod, WASM_FILE, "KINS", "KINEMATICS"));
|
}
|
||||||
setField("joints", iniQuery(mod, WASM_FILE, "KINS", "JOINTS"));
|
|
||||||
setField("coordinates", iniQuery(mod, WASM_FILE, "TRAJ", "COORDINATES"));
|
|
||||||
setField(
|
|
||||||
"parameterFile",
|
|
||||||
iniQuery(mod, WASM_FILE, "RS274NGC", "PARAMETER_FILE"),
|
|
||||||
);
|
|
||||||
|
|
||||||
setLog("Queried LinuxCNC INI fields through the standalone WASM parser.");
|
setLog("Queried LinuxCNC INI fields through the standalone WASM parser.");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ import { fileURLToPath } from "node:url";
|
|||||||
import { dirname, resolve } from "node:path";
|
import { dirname, resolve } from "node:path";
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
import createLinuxCncIniModule from "../../../runtime/ui/ini-panel/linuxcnc_ini.js";
|
import { createLinuxCncIniSdk } from "../../../runtime/sdk/src/linuxcnc-ini.js";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
const rootDir = resolve(__dirname, "../../..");
|
const rootDir = resolve(__dirname, "../../..");
|
||||||
const wasmPath = resolve(rootDir, "runtime/ui/ini-panel/linuxcnc_ini.wasm");
|
const wasmPath = resolve(rootDir, "runtime/ui/ini-panel/linuxcnc_ini.wasm");
|
||||||
|
|
||||||
const mod = await createLinuxCncIniModule({
|
const ini = await createLinuxCncIniSdk({
|
||||||
wasmBinary: readFileSync(wasmPath),
|
wasmBinary: readFileSync(wasmPath),
|
||||||
print() {},
|
print() {},
|
||||||
printErr(message) {
|
printErr(message) {
|
||||||
@@ -18,33 +18,7 @@ const mod = await createLinuxCncIniModule({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function allocCString(value) {
|
ini.writeTextFile(
|
||||||
const bytes = mod.lengthBytesUTF8(value) + 1;
|
|
||||||
const ptr = mod._malloc(bytes);
|
|
||||||
mod.stringToUTF8(value, ptr, bytes);
|
|
||||||
return ptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
function queryIni(path, section, tag) {
|
|
||||||
const pathPtr = allocCString(path);
|
|
||||||
const sectionPtr = allocCString(section);
|
|
||||||
const tagPtr = allocCString(tag);
|
|
||||||
const outSize = 256;
|
|
||||||
const outPtr = mod._malloc(outSize);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const rc = mod._lcini_get_string(pathPtr, sectionPtr, tagPtr, outPtr, outSize);
|
|
||||||
return { rc, value: rc === 0 ? mod.UTF8ToString(outPtr) : null };
|
|
||||||
} finally {
|
|
||||||
mod._free(pathPtr);
|
|
||||||
mod._free(sectionPtr);
|
|
||||||
mod._free(tagPtr);
|
|
||||||
mod._free(outPtr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod.FS.mkdir("/work");
|
|
||||||
mod.FS.writeFile(
|
|
||||||
"/work/node-smoke.ini",
|
"/work/node-smoke.ini",
|
||||||
`[EMC]
|
`[EMC]
|
||||||
MACHINE = wasm-node-smoke
|
MACHINE = wasm-node-smoke
|
||||||
@@ -57,21 +31,25 @@ COORDINATES = X Y Z A B
|
|||||||
KINEMATICS = xyzbc-trt-kins
|
KINEMATICS = xyzbc-trt-kins
|
||||||
JOINTS = 5
|
JOINTS = 5
|
||||||
`,
|
`,
|
||||||
{ encoding: "utf8" },
|
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.deepEqual(queryIni("/work/node-smoke.ini", "EMC", "MACHINE"), {
|
assert.equal(
|
||||||
rc: 0,
|
ini.getString("/work/node-smoke.ini", "EMC", "MACHINE"),
|
||||||
value: "wasm-node-smoke",
|
"wasm-node-smoke",
|
||||||
|
);
|
||||||
|
assert.equal(ini.getString("/work/node-smoke.ini", "TRAJ", "LINEAR_UNITS"), "mm");
|
||||||
|
assert.equal(
|
||||||
|
ini.getString("/work/node-smoke.ini", "KINS", "KINEMATICS"),
|
||||||
|
"xyzbc-trt-kins",
|
||||||
|
);
|
||||||
|
assert.equal(ini.getString("/work/node-smoke.ini", "TRAJ", "MISSING"), null);
|
||||||
|
|
||||||
|
assert.deepEqual(ini.getFields("/work/node-smoke.ini", {
|
||||||
|
machine: { section: "EMC", tag: "MACHINE" },
|
||||||
|
joints: { section: "KINS", tag: "JOINTS" },
|
||||||
|
}), {
|
||||||
|
machine: "wasm-node-smoke",
|
||||||
|
joints: "5",
|
||||||
});
|
});
|
||||||
assert.deepEqual(queryIni("/work/node-smoke.ini", "TRAJ", "LINEAR_UNITS"), {
|
|
||||||
rc: 0,
|
|
||||||
value: "mm",
|
|
||||||
});
|
|
||||||
assert.deepEqual(queryIni("/work/node-smoke.ini", "KINS", "KINEMATICS"), {
|
|
||||||
rc: 0,
|
|
||||||
value: "xyzbc-trt-kins",
|
|
||||||
});
|
|
||||||
assert.notEqual(queryIni("/work/node-smoke.ini", "TRAJ", "MISSING").rc, 0);
|
|
||||||
|
|
||||||
console.log("ini_wasm_node_smoke=ok");
|
console.log("ini_wasm_node_smoke=ok");
|
||||||
|
|||||||
Reference in New Issue
Block a user