Skip to content

Commit 4a343bc

Browse files
committed
fix(desktop): allow Neovim console access on older WSL
1 parent a17341a commit 4a343bc

2 files changed

Lines changed: 73 additions & 2 deletions

File tree

‎apps/desktop/scripts/neovim-terminal/transport.mjs‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,11 +151,12 @@ export function wslArgs(route) {
151151
export function run(command, args, { input, timeout, cwd, capture = false, env } = {}) {
152152
return new Promise((resolve, reject) => {
153153
// Packaged Electron can inherit a pipe as fd 0 from PowerShell even in a
154-
// terminal window. Open the console explicitly for the interactive child.
154+
// terminal window. Open the console read/write for interactive children:
155+
// WSL 2.2.4 rejects a read-only handle with Wsl/Service/E_ACCESSDENIED.
155156
const consoleInput =
156157
// oxlint-disable-next-line t3code/no-global-process-runtime -- Standalone packaged helper owns its platform-specific console handles.
157158
input === undefined && process.platform === "win32"
158-
? NodeFS.openSync("\\\\.\\CONIN$", "r")
159+
? NodeFS.openSync("\\\\.\\CONIN$", "r+")
159160
: undefined;
160161
let child;
161162
try {

‎apps/desktop/scripts/neovim-terminal/transport.test.mjs‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,76 @@ const request = (overrides = {}) => ({
3434
...overrides,
3535
});
3636

37+
// oxlint-disable-next-line t3code/no-global-process-runtime -- Exercises native console handles in the standalone Windows helper.
38+
test.skipIf(process.platform !== "win32")(
39+
"interactive children receive console input with read and write access",
40+
async () => {
41+
const directory = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-console-access-"));
42+
const powershell = NodePath.join(
43+
process.env.SystemRoot,
44+
"System32/WindowsPowerShell/v1.0/powershell.exe",
45+
);
46+
const encode = (script) => Buffer.from(script, "utf16le").toString("base64");
47+
const checkAccess = encode(`
48+
$ErrorActionPreference = 'Stop'
49+
Add-Type @'
50+
using System;
51+
using System.Runtime.InteropServices;
52+
public static class InputAccess {
53+
[DllImport("kernel32.dll")] public static extern IntPtr GetStdHandle(int id);
54+
[DllImport("kernel32.dll", SetLastError=true)] public static extern bool GetConsoleMode(IntPtr handle, out uint mode);
55+
[DllImport("kernel32.dll", SetLastError=true)] public static extern bool FlushConsoleInputBuffer(IntPtr handle);
56+
}
57+
'@
58+
$handle = [InputAccess]::GetStdHandle(-10)
59+
[uint32]$mode = 0
60+
if (-not [InputAccess]::GetConsoleMode($handle, [ref]$mode)) { throw 'Console read access is missing' }
61+
if (-not [InputAccess]::FlushConsoleInputBuffer($handle)) { throw 'Console write access is missing' }
62+
Write-Output 'console-read-write-ok'
63+
`);
64+
// Allocate a private console so the access check never flushes a user's input.
65+
const launch = encode(`
66+
$ErrorActionPreference = 'Stop'
67+
Add-Type @'
68+
using System.Runtime.InteropServices;
69+
public static class PrivateConsole {
70+
[DllImport("kernel32.dll")] public static extern bool FreeConsole();
71+
[DllImport("kernel32.dll")] public static extern bool AllocConsole();
72+
}
73+
'@
74+
[PrivateConsole]::FreeConsole() | Out-Null
75+
if (-not [PrivateConsole]::AllocConsole()) { throw 'Could not allocate test console' }
76+
try {
77+
$child = Start-Process -FilePath $env:T3_TEST_CONSOLE_RUNTIME -ArgumentList ('"' + $env:T3_TEST_CONSOLE_SCRIPT + '"') -NoNewWindow -Wait -PassThru
78+
exit $child.ExitCode
79+
} finally { [PrivateConsole]::FreeConsole() | Out-Null }
80+
`);
81+
try {
82+
const script = NodePath.join(directory, "check.mjs");
83+
const transport = new URL("transport.mjs", import.meta.url).href;
84+
await NodeFSP.writeFile(
85+
script,
86+
`import { run } from ${JSON.stringify(transport)};
87+
console.log(await run(${JSON.stringify(powershell)}, ['-NoProfile', '-EncodedCommand', ${JSON.stringify(checkAccess)}], { capture: true, timeout: 15000 }));
88+
`,
89+
);
90+
const output = await run(powershell, ["-NoProfile", "-EncodedCommand", launch], {
91+
input: "",
92+
capture: true,
93+
timeout: 30000,
94+
env: {
95+
...process.env,
96+
T3_TEST_CONSOLE_RUNTIME: process.execPath,
97+
T3_TEST_CONSOLE_SCRIPT: script,
98+
},
99+
});
100+
NodeAssert.match(output, /console-read-write-ok/u);
101+
} finally {
102+
await NodeFSP.rm(directory, { recursive: true, force: true });
103+
}
104+
},
105+
);
106+
37107
test("target bytes remain data across token and Windows Terminal grammar", () => {
38108
for (const name of [
39109
"spaces here",

0 commit comments

Comments
 (0)