From 030e33e92522ed164a0e8d8c554130016cac7f7f Mon Sep 17 00:00:00 2001 From: sama Pyb Date: Fri, 31 Jul 2026 19:17:20 +0800 Subject: [PATCH] fix: keep debugjava executable on POSIX --- .gitattributes | 1 + bundled/scripts/noConfigScripts/debugjava | 0 src/noConfigDebugInit.ts | 36 ++++++++ test/noConfigDebugInit.test.ts | 106 ++++++++++++++++++++++ 4 files changed, 143 insertions(+) create mode 100644 .gitattributes mode change 100644 => 100755 bundled/scripts/noConfigScripts/debugjava create mode 100644 test/noConfigDebugInit.test.ts diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..ddef66b4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +bundled/scripts/noConfigScripts/debugjava text eol=lf diff --git a/bundled/scripts/noConfigScripts/debugjava b/bundled/scripts/noConfigScripts/debugjava old mode 100644 new mode 100755 diff --git a/src/noConfigDebugInit.ts b/src/noConfigDebugInit.ts index ea4e3606..a46eafc4 100644 --- a/src/noConfigDebugInit.ts +++ b/src/noConfigDebugInit.ts @@ -13,6 +13,32 @@ import { applyAppendIfChanged, applyReplaceIfChanged } from "./envVarSync"; const ENV_VAR_COLLECTION_DESCRIPTION = "Java No-Config Debug"; +/** + * Ensures the POSIX no-config debug wrapper can be invoked from a terminal. + * + * Official VSIX packages are built on Windows, where executable bits are not + * preserved. Repair the installed script when the extension activates on a + * POSIX platform, while preserving all existing permissions and avoiding an + * unnecessary chmod when the owner can already execute it. + * + * @param scriptPath - The installed debugjava wrapper path. + * @param platform - The current operating system platform. + */ +export async function ensureDebugJavaScriptExecutable( + scriptPath: string, + platform: NodeJS.Platform = process.platform, +): Promise { + if (platform === "win32") { + return; + } + + const permissions = (await fs.promises.stat(scriptPath)).mode % 0o10000; + const ownerPermissions = Math.floor(permissions / 0o100); + if (ownerPermissions % 2 === 0) { + await fs.promises.chmod(scriptPath, permissions + 0o100); + } +} + /** * Registers the configuration-less debugging setup for the extension. * @@ -104,6 +130,16 @@ export async function registerNoConfigDebug( } const noConfigScriptsDir = path.join(extPath, 'bundled', 'scripts', 'noConfigScripts'); + const debugJavaScriptPath = path.join(noConfigScriptsDir, "debugjava"); + try { + await ensureDebugJavaScriptExecutable(debugJavaScriptPath); + } catch (err) { + const error: Error = { + name: "NoConfigDebugError", + message: `[Java Debug] Failed to make debugjava executable: ${err}`, + }; + sendError(error); + } applyAppendIfChanged(collection, 'PATH', buildNoConfigPathAppendValue(noConfigScriptsDir)); // create file system watcher for the debuggerAdapterEndpointFolder for when the communication port is written diff --git a/test/noConfigDebugInit.test.ts b/test/noConfigDebugInit.test.ts new file mode 100644 index 00000000..6d255f94 --- /dev/null +++ b/test/noConfigDebugInit.test.ts @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import * as assert from "assert"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; + +import { ensureDebugJavaScriptExecutable } from "../src/noConfigDebugInit"; + +suite("No-Config Debug scripts", () => { + test("the bundled POSIX wrapper uses LF and is executable", async () => { + const scriptPath = path.resolve( + __dirname, + "../../bundled/scripts/noConfigScripts/debugjava", + ); + const contents = await fs.promises.readFile(scriptPath, "utf8"); + + assert.ok(contents.startsWith("#!/bin/bash\n")); + assert.strictEqual(contents.includes("\r"), false); + + if (process.platform !== "win32") { + const mode = (await fs.promises.stat(scriptPath)).mode % 0o1000; + assert.strictEqual(mode, 0o755); + } + }); + + test("adds owner execute permission without changing other permissions", async () => { + if (process.platform === "win32") { + return; + } + + const tempDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), "vscode-java-debug-"), + ); + const scriptPath = path.join(tempDir, "debugjava"); + + try { + await fs.promises.writeFile(scriptPath, "#!/usr/bin/env bash\n"); + + const cases = [ + { initial: 0o444, expected: 0o544 }, + { initial: 0o600, expected: 0o700 }, + { initial: 0o644, expected: 0o744 }, + { initial: 0o6444, expected: 0o6544 }, + ]; + for (const testCase of cases) { + await fs.promises.chmod(scriptPath, testCase.initial); + await ensureDebugJavaScriptExecutable(scriptPath, "linux"); + + const mode = (await fs.promises.stat(scriptPath)).mode % 0o10000; + assert.strictEqual(mode, testCase.expected); + } + } finally { + await fs.promises.rm(tempDir, { recursive: true, force: true }); + } + }); + + test("does not chmod an already owner-executable POSIX wrapper", async () => { + if (process.platform === "win32") { + return; + } + + const tempDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), "vscode-java-debug-"), + ); + const scriptPath = path.join(tempDir, "debugjava"); + + try { + await fs.promises.writeFile(scriptPath, "#!/usr/bin/env bash\n"); + + const originalChmod = fs.promises.chmod; + let chmodCalls = 0; + fs.promises.chmod = async (...args): Promise => { + chmodCalls += 1; + return originalChmod(...args); + }; + try { + const executableModes = [0o700, 0o750, 0o775]; + for (const mode of executableModes) { + await originalChmod(scriptPath, mode); + await ensureDebugJavaScriptExecutable(scriptPath, "darwin"); + const actualMode = + (await fs.promises.stat(scriptPath)).mode % 0o1000; + assert.strictEqual(actualMode, mode); + } + } finally { + fs.promises.chmod = originalChmod; + } + + assert.strictEqual(chmodCalls, 0); + } finally { + await fs.promises.rm(tempDir, { recursive: true, force: true }); + } + }); + + test("does not change wrapper permissions on Windows", async () => { + const missingScriptPath = path.join( + os.tmpdir(), + "vscode-java-debug-missing", + "debugjava", + ); + + await ensureDebugJavaScriptExecutable(missingScriptPath, "win32"); + }); +});