Skip to content

Commit a86dedf

Browse files
committed
preview: de-namespace win32 entry (loadableEntry) + entry instrumentation
1 parent 4c9915e commit a86dedf

4 files changed

Lines changed: 58 additions & 7 deletions

File tree

‎.github/workflows/preview.yml‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: preview
2+
3+
# Throwaway windows-latest verification loop for the community/win-launcher
4+
# work: pushed only to preview/** branches on the fork, never landed on a PR
5+
# branch. Mirrors the windows-process-lifecycle job of dynamic-workflow.yml.
6+
on:
7+
push:
8+
branches: ['preview/**']
9+
10+
permissions:
11+
contents: read
12+
13+
jobs:
14+
windows-launcher:
15+
runs-on: windows-latest
16+
timeout-minutes: 15
17+
defaults:
18+
run:
19+
working-directory: plugins/hetaoBackend/mcode-dynamic-workflows
20+
steps:
21+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b18 # v7.0.1
22+
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
23+
with:
24+
node-version: 22
25+
- run: npm ci --ignore-scripts --registry=https://registry.npmjs.org
26+
- run: node --test checks/process-tree.check.mjs
27+
- run: node --test checks/win-launcher.check.mjs

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

Lines changed: 7 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
@@ -181,6 +181,7 @@ test('end-to-end argv fidelity: resolved node entry spawns with spaced cwd and f
181181
await release(root,'0.4.12','0.4.12','process.stdout.write(JSON.stringify(process.argv.slice(2)))\n');
182182
const r=await resolveMcode('mcode',{env:winEnv(root),home:f.official,platform:'win32'});
183183
assert.ok(r);assert.equal(r.command,process.execPath,'direct node entry');
184+
console.error('ENTRY=',JSON.stringify({entry:r.args[0],command:r.command,tmpdir:tmpdir(),cwd:process.cwd()}));
184185
const cwd=join(f.root,'work dir with spaces');
185186
await mkdir(cwd,{recursive:true});
186187
const sent=['exec','--input','-','--cwd',cwd,'--output-format','stream-json','--flag-shape-toy','-'];
@@ -199,3 +200,8 @@ test('last-resort PS hop prefers pwsh (PS7) over powershell (PS5.1 -File is brok
199200
assert.match(r.command,/pwsh\.exe$/i);
200201
}finally{await f.cleanup();}
201202
});
203+
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)',()=>{
204+
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');
205+
assert.equal(loadableEntry('\\\\?\\UNC\\srv\\share\\code\\cli.js'),'\\\\srv\\share\\code\\cli.js','UNC form maps back to \\\\server\\share');
206+
assert.equal(loadableEntry('/tmp/wf-winloc/cli.js'),'/tmp/wf-winloc/cli.js','posix passes through untouched');
207+
});

‎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)