Skip to content

Commit e3d0166

Browse files
author
moc
committed
fix(win): strip \\?\ namespace from resolved entries; run launcher suite in Windows CI
Real-Windows evidence (fork preview, windows-latest, Node 22): running checks/win-launcher.check.mjs on a real runner surfaced a resolver bug the POSIX+win32-injected suite could never catch — fs.promises.mkdir(recursive) returns a \\?\-namespaced path on Windows, every join below keeps the prefix, and node's main resolution (realpathSync on argv[1]) dies with EISDIR lstat 'C:' (nodejs/node#62446, fixed by #65378 first in v24.21.0 / v26.8.0 — this plugin supports Node >=22, so we self-defend). loadableEntry() strips the namespace marker (drive + UNC forms) at all three node-entry return points; POSIX strings pass through unchanged. dist regenerated in lockstep. The Windows job now runs the launcher suite next to process-tree.check.mjs, closing the second review blocker.
1 parent 4c9915e commit e3d0166

4 files changed

Lines changed: 31 additions & 7 deletions

File tree

‎.github/workflows/dynamic-workflow.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,4 @@ jobs:
4646
node-version: 22
4747
- run: npm ci --ignore-scripts --registry=https://registry.npmjs.org
4848
- run: node --test checks/process-tree.check.mjs
49+
- run: node --test checks/win-launcher.check.mjs

‎plugins/hetaoBackend/mcode-dynamic-workflows/checks/win-launcher.check.mjs‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import test from 'node:test';import assert from 'node:assert/strict';
22
import {mkdir,writeFile,rm,readFile,chmod} from 'node:fs/promises';
33
import {spawn} from 'node:child_process';
44
import {tmpdir} from 'node:os';import {join} from 'node:path';
5-
import {resolveMcode} from '../src/mcode-location.mjs';
5+
import {resolveMcode,loadableEntry} from '../src/mcode-location.mjs';
66
// Windows launcher resolution is exercised from POSIX by faking the on-disk
77
// layout and passing platform:'win32' + a controlled PATH/PATHEXT: resolution
88
// is pure filesystem probing, so the real bug (mixed-install layouts) is
@@ -199,3 +199,8 @@ test('last-resort PS hop prefers pwsh (PS7) over powershell (PS5.1 -File is brok
199199
assert.match(r.command,/pwsh\.exe$/i);
200200
}finally{await f.cleanup();}
201201
});
202+
test('namespaced win32 entries are de-namespaced: node >=22 realpathSync dies on a \\\\?\\ main entry with EISDIR lstat C: (real-Windows fork runs 35491670398/35492809510)',()=>{
203+
assert.equal(loadableEntry('\\\\?\\C:\\Users\\a\\.minimax-code\\releases\\0.4.12\\node_modules\\@minimax-ai\\code\\cli.js'),'C:\\Users\\a\\.minimax-code\\releases\\0.4.12\\node_modules\\@minimax-ai\\code\\cli.js','drive form loses the namespace marker');
204+
assert.equal(loadableEntry('\\\\?\\UNC\\srv\\share\\code\\cli.js'),'\\\\srv\\share\\code\\cli.js','UNC form maps back to \\\\server\\share');
205+
assert.equal(loadableEntry('/tmp/wf-winloc/cli.js'),'/tmp/wf-winloc/cli.js','posix passes through untouched');
206+
});

‎plugins/hetaoBackend/mcode-dynamic-workflows/dist/main.mjs‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13964,6 +13964,9 @@ async function fileExists(file, executable = false, platform = process.platform)
1396413964
return false;
1396513965
}
1396613966
}
13967+
function loadableEntry(entry) {
13968+
return String(entry).replace(/^\\\\\?\\UNC\\/, "\\\\").replace(/^\\\\\?\\([a-zA-Z]:)/, "$1");
13969+
}
1396713970
async function executablePath(command, env = process.env, platform = process.platform) {
1396813971
if (typeof command !== "string" || !command) return null;
1396913972
const direct = /[\\/]/.test(command);
@@ -13996,7 +13999,7 @@ async function resolveMcode(command = "mcode", { env = process.env, home = homed
1399613999
if (path) {
1399714000
if (platform === "win32" && /\.(cmd|bat)$/i.test(path)) {
1399814001
const active = await launcherEntry(join2(dirname(path), ".mcode-launcher.cmd"), dirname(path));
13999-
if (active) return { command: process.execPath, args: [active], source };
14002+
if (active) return { command: process.execPath, args: [loadableEntry(active)], source };
1400014003
const root = officialRoot(home, env);
1400114004
const candidates = [
1400214005
{ entry: join2(dirname(path), "node_modules", "@minimax-ai", "code", "cli.js"), tie: "" },
@@ -14045,7 +14048,7 @@ async function resolveMcode(command = "mcode", { env = process.env, home = homed
1404514048
const version3 = await versionOf(candidate.entry);
1404614049
if (!best || cmpSemVer(version3, best.version) > 0 || cmpSemVer(version3, best.version) === 0 && candidate.tie > best.tie) best = { ...candidate, version: version3 };
1404714050
}
14048-
if (best) return { command: process.execPath, args: [best.entry], source };
14051+
if (best) return { command: process.execPath, args: [loadableEntry(best.entry)], source };
1404914052
const launcher = join2(dirname(path), "mcode.ps1");
1405014053
if (await fileExists(launcher)) {
1405114054
const powershell = await executablePath("pwsh.exe", env, platform) ?? await executablePath("powershell.exe", env, platform);
@@ -14058,7 +14061,7 @@ async function resolveMcode(command = "mcode", { env = process.env, home = homed
1405814061
}
1405914062
if (command === "mcode") {
1406014063
const entry = managedEntry(home, platform);
14061-
if (await fileExists(entry)) return { command: process.execPath, args: [entry], source: "workflow-managed" };
14064+
if (await fileExists(entry)) return { command: process.execPath, args: [loadableEntry(entry)], source: "workflow-managed" };
1406214065
}
1406314066
return null;
1406414067
}

‎plugins/hetaoBackend/mcode-dynamic-workflows/src/mcode-location.mjs‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,21 @@ export const managedEntry = (home = homedir(), platform = process.platform) => j
99
async function fileExists(file, executable = false, platform = process.platform) {
1010
try { await access(file, executable && platform !== 'win32' ? constants.X_OK : constants.F_OK); return (await stat(file)).isFile(); } catch { return false; }
1111
}
12+
// A \\?\-namespaced win32 path is fine for fs probing but fatal as node's
13+
// main entry on every Node line this plugin supports (>=22): node runs
14+
// fs.realpathSync on argv[1], whose JS implementation probes the namespaced
15+
// drive root `\\?\C:\`; the fs binding drops the trailing separator, lstat
16+
// receives the bare drive `C:` and dies with EISDIR (nodejs/node#62446,
17+
// fixed by nodejs/node#65378 — first shipped in v24.21.0/v26.8.0). Real-
18+
// Windows evidence: fork preview runs 35491670398 / 35492809510
19+
// (windows-latest, Node 22) — fs.promises.mkdir(recursive) hands back a
20+
// namespaced created path, every join below it keeps the prefix, and the
21+
// spawned entry crashed exactly there. Strip the namespace marker (drive
22+
// and UNC forms) so a returned entry is always a plain path node can load
23+
// verbatim; POSIX strings never match and pass through unchanged.
24+
export function loadableEntry(entry) {
25+
return String(entry).replace(/^\\\\\?\\UNC\\/, '\\\\').replace(/^\\\\\?\\([a-zA-Z]:)/, '$1');
26+
}
1227
export async function executablePath(command, env = process.env, platform = process.platform) {
1328
if (typeof command !== 'string' || !command) return null;
1429
const direct = /[\\/]/.test(command);
@@ -66,7 +81,7 @@ export async function resolveMcode(command = 'mcode', { env = process.env, home
6681
// any version arithmetic — the launcher runs what it runs, so we run what
6782
// it runs, and rollbacks and channel switches are followed for free.
6883
const active = await launcherEntry(join(dirname(path), '.mcode-launcher.cmd'), dirname(path));
69-
if (active) return { command: process.execPath, args: [active], source };
84+
if (active) return { command: process.execPath, args: [loadableEntry(active)], source };
7085
// No launcher pointer: fall back to ranking every coexisting layout by
7186
// version. When several installs coexist (PATH shim with an old sibling,
7287
// newer official root) a stale 0.2.x entry lacks current exec flags.
@@ -126,7 +141,7 @@ export async function resolveMcode(command = 'mcode', { env = process.env, home
126141
const version = await versionOf(candidate.entry);
127142
if (!best || cmpSemVer(version, best.version) > 0 || (cmpSemVer(version, best.version) === 0 && candidate.tie > best.tie)) best = { ...candidate, version };
128143
}
129-
if (best) return { command: process.execPath, args: [best.entry], source };
144+
if (best) return { command: process.execPath, args: [loadableEntry(best.entry)], source };
130145
const launcher = join(dirname(path), 'mcode.ps1');
131146
if (await fileExists(launcher)) {
132147
// pwsh (PS7) first: PS 5.1 binds flag-shaped argv as its own named parameters
@@ -141,7 +156,7 @@ export async function resolveMcode(command = 'mcode', { env = process.env, home
141156
}
142157
if (command === 'mcode') {
143158
const entry = managedEntry(home, platform);
144-
if (await fileExists(entry)) return { command: process.execPath, args: [entry], source: 'workflow-managed' };
159+
if (await fileExists(entry)) return { command: process.execPath, args: [loadableEntry(entry)], source: 'workflow-managed' };
145160
}
146161
return null;
147162
}

0 commit comments

Comments
 (0)