Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bundled/scripts/noConfigScripts/debugjava text eol=lf
Empty file modified bundled/scripts/noConfigScripts/debugjava
100644 → 100755
Empty file.
36 changes: 36 additions & 0 deletions src/noConfigDebugInit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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.
*
Expand Down Expand Up @@ -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
Expand Down
106 changes: 106 additions & 0 deletions test/noConfigDebugInit.test.ts
Original file line number Diff line number Diff line change
@@ -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<void> => {
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");
});
});