dsh-ros2-common 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/runner.js +95 -4
- package/lib/toolkit.js +2 -1
- package/lib/types/runner.d.ts +24 -0
- package/lib/types/toolkit.d.ts +2 -2
- package/package.json +1 -1
package/lib/runner.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { execFile, spawn } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { accessSync, existsSync, readdirSync } from 'node:fs';
|
|
2
4
|
const DEFAULT_TIMEOUT_MS = 15000;
|
|
3
5
|
const MAX_BUFFER = 16 * 1024 * 1024;
|
|
4
6
|
function execFileP(bin, args, options) {
|
|
@@ -20,6 +22,76 @@ function execFileP(bin, args, options) {
|
|
|
20
22
|
function shq(value) {
|
|
21
23
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
22
24
|
}
|
|
25
|
+
// ── session-scoped workspace override + ros setup fallback chain ──────────
|
|
26
|
+
// "装好即用、用错自纠、切环境不重启": a mutable per-session override (set by the
|
|
27
|
+
// ros2_workspace tool) beats the configured rosSetup; an empty/missing setup
|
|
28
|
+
// falls back through workspaceRoot/install/setup.bash -> /opt/ros/<distro>/setup.bash
|
|
29
|
+
// -> no source; failures carry actionable diagnostics (sourceOk / envNote).
|
|
30
|
+
let sessionRosSetup = null;
|
|
31
|
+
/** Set/clear the session-level ros setup prefix (ros2_workspace use/reset). */
|
|
32
|
+
export function setSessionRosSetup(prefix) {
|
|
33
|
+
sessionRosSetup = prefix;
|
|
34
|
+
}
|
|
35
|
+
/** Current session-level ros setup prefix (null = not overridden). */
|
|
36
|
+
export function getSessionRosSetup() {
|
|
37
|
+
return sessionRosSetup;
|
|
38
|
+
}
|
|
39
|
+
/** Extract the first `source <path>` from a shell prefix, if any. */
|
|
40
|
+
function extractSourcePath(prefix) {
|
|
41
|
+
const m = /\bsource\s+([^\s&;|]+)/.exec(prefix);
|
|
42
|
+
return m ? m[1] : undefined;
|
|
43
|
+
}
|
|
44
|
+
/** First existing candidate for `/opt/ros/<distro>/setup.bash`. */
|
|
45
|
+
function globFirstRosSetup() {
|
|
46
|
+
try {
|
|
47
|
+
const entries = readdirSync('/opt/ros').sort();
|
|
48
|
+
for (const e of entries) {
|
|
49
|
+
const cand = path.join('/opt/ros', e, 'setup.bash');
|
|
50
|
+
if (existsSync(cand))
|
|
51
|
+
return cand;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
/* /opt/ros missing */
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
/** Auto-detect: workspaceRoot/install/setup.bash, then /opt/ros/<distro>/setup.bash. */
|
|
60
|
+
function autoDetectSetup(opts) {
|
|
61
|
+
if (opts.workspaceRoot) {
|
|
62
|
+
const cand = path.join(opts.workspaceRoot, 'install', 'setup.bash');
|
|
63
|
+
if (existsSync(cand))
|
|
64
|
+
return cand;
|
|
65
|
+
}
|
|
66
|
+
return globFirstRosSetup();
|
|
67
|
+
}
|
|
68
|
+
/** Resolve the effective setup prefix (session override -> config -> auto). */
|
|
69
|
+
export function resolveSetup(opts) {
|
|
70
|
+
const explicit = sessionRosSetup ?? opts.rosSetup ?? '';
|
|
71
|
+
if (explicit) {
|
|
72
|
+
const src = extractSourcePath(explicit);
|
|
73
|
+
if (src && !existsSync(src)) {
|
|
74
|
+
// explicit source path is wrong: report + auto-correct via the chain
|
|
75
|
+
const auto = autoDetectSetup(opts);
|
|
76
|
+
return {
|
|
77
|
+
prefix: auto ? `source ${auto} && ` : '',
|
|
78
|
+
sourcePath: auto ?? null,
|
|
79
|
+
explicit: true,
|
|
80
|
+
autoCandidate: auto ?? null,
|
|
81
|
+
note: `配置的 rosSetup source 路径不存在:${src};已自动回退${auto ? `到 ${auto}` : '(无可用 setup,直接调用 ros2,依赖宿主 PATH)'}。建议修正配置。`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return { prefix: explicit, sourcePath: src ?? null, explicit: true, autoCandidate: null, note: '' };
|
|
85
|
+
}
|
|
86
|
+
const auto = autoDetectSetup(opts);
|
|
87
|
+
return {
|
|
88
|
+
prefix: auto ? `source ${auto} && ` : '',
|
|
89
|
+
sourcePath: auto ?? null,
|
|
90
|
+
explicit: false,
|
|
91
|
+
autoCandidate: auto ?? null,
|
|
92
|
+
note: auto ? '' : '未检测到 ros setup(直接调用 ros2,依赖宿主 PATH;可用 ros2_env_check 诊断)',
|
|
93
|
+
};
|
|
94
|
+
}
|
|
23
95
|
/**
|
|
24
96
|
* Run a CLI command (default binary `ros2`) and normalize the outcome.
|
|
25
97
|
* Non-zero exits and timeouts are reported as `ok: false` — the caller decides
|
|
@@ -43,16 +115,33 @@ export async function runCommand(bin, args, opts = {}) {
|
|
|
43
115
|
cwd,
|
|
44
116
|
env,
|
|
45
117
|
};
|
|
118
|
+
const setup = resolveSetup(opts);
|
|
119
|
+
const cmd = setup.prefix ? `${setup.prefix}${command}` : command;
|
|
120
|
+
const envNote = setup.note;
|
|
46
121
|
try {
|
|
47
|
-
const { stdout, stderr } =
|
|
48
|
-
? await execFileP('bash', ['-lc',
|
|
122
|
+
const { stdout, stderr } = setup.prefix
|
|
123
|
+
? await execFileP('bash', ['-lc', cmd], baseOptions)
|
|
49
124
|
: await execFileP(bin, args, baseOptions);
|
|
50
|
-
return {
|
|
125
|
+
return {
|
|
126
|
+
ok: true, command, stdout, stderr, exitCode: 0, timedOut: false, durationMs: Date.now() - startedAt,
|
|
127
|
+
sourceOk: true,
|
|
128
|
+
...(envNote ? { envNote: `[env] ${envNote}` } : {}),
|
|
129
|
+
};
|
|
51
130
|
}
|
|
52
131
|
catch (error) {
|
|
53
132
|
const e = error;
|
|
54
133
|
const timedOut = e.killed === true || e.signal === 'SIGKILL';
|
|
55
134
|
const exitCode = typeof e.code === 'number' ? e.code : null;
|
|
135
|
+
const hostEnv = `AMENT_PREFIX_PATH=${process.env.AMENT_PREFIX_PATH ?? ''} COLCON_PREFIX_PATH=${process.env.COLCON_PREFIX_PATH ?? ''}`;
|
|
136
|
+
const diag = [
|
|
137
|
+
envNote ? `[env] ${envNote}` : '',
|
|
138
|
+
setup.explicit && !setup.sourcePath ? '[env] 未检测到 source 路径(rosSetup 不含 source 或非标准格式)' : '',
|
|
139
|
+
timedOut ? '' : `[env] 宿主环境:${hostEnv}`,
|
|
140
|
+
].filter(Boolean).join('\n');
|
|
141
|
+
const message = [
|
|
142
|
+
timedOut ? `timed out after ${timeoutMs}ms` : e.message,
|
|
143
|
+
diag ? `\n${diag}` : '',
|
|
144
|
+
].join('');
|
|
56
145
|
return {
|
|
57
146
|
ok: false,
|
|
58
147
|
command,
|
|
@@ -61,7 +150,9 @@ export async function runCommand(bin, args, opts = {}) {
|
|
|
61
150
|
exitCode,
|
|
62
151
|
timedOut,
|
|
63
152
|
durationMs: Date.now() - startedAt,
|
|
64
|
-
error:
|
|
153
|
+
error: message,
|
|
154
|
+
sourceOk: setup.explicit && !setup.sourcePath ? false : setup.prefix ? true : undefined,
|
|
155
|
+
...(diag ? { envNote: diag } : {}),
|
|
65
156
|
};
|
|
66
157
|
}
|
|
67
158
|
}
|
package/lib/toolkit.js
CHANGED
|
@@ -30,7 +30,7 @@ export const resultSchema = {
|
|
|
30
30
|
};
|
|
31
31
|
export const renderResult = (_args, value) => [{ type: 'text', text: JSON.stringify(value) }];
|
|
32
32
|
export { parseJsonOrRaw };
|
|
33
|
-
export { runCommand, spawnJob } from './runner.js';
|
|
33
|
+
export { runCommand, spawnJob, setSessionRosSetup, getSessionRosSetup, resolveSetup } from './runner.js';
|
|
34
34
|
export { foldGraph, parseLines, parseNodeInfo, parseTopicList, parseTransforms } from './parse.js';
|
|
35
35
|
/** Build the injected run seam from a package's config (mirrors legacy index.ts). */
|
|
36
36
|
export function makeRun(config) {
|
|
@@ -39,6 +39,7 @@ export function makeRun(config) {
|
|
|
39
39
|
rosLogDir: opts.rosLogDir ?? config.rosLogDir,
|
|
40
40
|
cwd: opts.cwd ?? (config.workspaceRoot.length > 0 ? config.workspaceRoot : undefined),
|
|
41
41
|
rosSetup: opts.rosSetup ?? config.rosSetup,
|
|
42
|
+
workspaceRoot: config.workspaceRoot,
|
|
42
43
|
env: opts.env,
|
|
43
44
|
});
|
|
44
45
|
}
|
package/lib/types/runner.d.ts
CHANGED
|
@@ -9,14 +9,38 @@ export interface RosResult {
|
|
|
9
9
|
timedOut: boolean;
|
|
10
10
|
durationMs: number;
|
|
11
11
|
error?: string;
|
|
12
|
+
/** Whether the environment (ros setup) resolved cleanly (P1 error contract). */
|
|
13
|
+
sourceOk?: boolean;
|
|
14
|
+
/** Human-readable environment diagnostics (missing paths, fallback used). */
|
|
15
|
+
envNote?: string;
|
|
12
16
|
}
|
|
13
17
|
export interface RunOptions {
|
|
14
18
|
timeoutMs?: number;
|
|
15
19
|
cwd?: string;
|
|
16
20
|
rosLogDir?: string;
|
|
17
21
|
rosSetup?: string;
|
|
22
|
+
/** Workspace root used for the `workspaceRoot/install/setup.bash` fallback. */
|
|
23
|
+
workspaceRoot?: string;
|
|
18
24
|
env?: Record<string, string>;
|
|
19
25
|
}
|
|
26
|
+
/** Set/clear the session-level ros setup prefix (ros2_workspace use/reset). */
|
|
27
|
+
export declare function setSessionRosSetup(prefix: string | null): void;
|
|
28
|
+
/** Current session-level ros setup prefix (null = not overridden). */
|
|
29
|
+
export declare function getSessionRosSetup(): string | null;
|
|
30
|
+
export interface SetupResolution {
|
|
31
|
+
/** Final shell prefix ('' = no source). */
|
|
32
|
+
prefix: string;
|
|
33
|
+
/** The source path used, if any. */
|
|
34
|
+
sourcePath: string | null;
|
|
35
|
+
/** Whether an explicit rosSetup/session override was configured. */
|
|
36
|
+
explicit: boolean;
|
|
37
|
+
/** The auto-detected path that would work (when the explicit one is wrong). */
|
|
38
|
+
autoCandidate: string | null;
|
|
39
|
+
/** Human note (fallback used / misconfiguration) — becomes envNote on errors. */
|
|
40
|
+
note: string;
|
|
41
|
+
}
|
|
42
|
+
/** Resolve the effective setup prefix (session override -> config -> auto). */
|
|
43
|
+
export declare function resolveSetup(opts: RunOptions): SetupResolution;
|
|
20
44
|
/**
|
|
21
45
|
* Run a CLI command (default binary `ros2`) and normalize the outcome.
|
|
22
46
|
* Non-zero exits and timeouts are reported as `ok: false` — the caller decides
|
package/lib/types/toolkit.d.ts
CHANGED
|
@@ -127,8 +127,8 @@ export declare const renderResult: (_args: unknown, value: JsonValue) => {
|
|
|
127
127
|
text: string;
|
|
128
128
|
}[];
|
|
129
129
|
export { type JsonValue, parseJsonOrRaw };
|
|
130
|
-
export { runCommand, spawnJob } from './runner.js';
|
|
131
|
-
export type { RosResult, RunOptions, JobHooks } from './runner.js';
|
|
130
|
+
export { runCommand, spawnJob, setSessionRosSetup, getSessionRosSetup, resolveSetup } from './runner.js';
|
|
131
|
+
export type { RosResult, RunOptions, JobHooks, SetupResolution } from './runner.js';
|
|
132
132
|
export { foldGraph, parseLines, parseNodeInfo, parseTopicList, parseTransforms } from './parse.js';
|
|
133
133
|
/** Run-seam config (each domain package carries its own copy via Config). */
|
|
134
134
|
export interface RunConfig {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-ros2-common",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Shared runtime for the dsh-ros2 plugin family: command runner, parsers, ToolDeps toolkit, and the robot-profile script (zero-copy across packages). Not a cordis bundle.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|