дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 03:05:40 +02:00
feat(remote): Reload the environment and apply devcontainer settings on the remote server (#183)
The remote server read the login shell's PATH once and cached it, had no project-level environment, and changed environment variables only took effect after reinstalling or a manual restart.
- Every openidea-server start re-reads the remote login shell's full environment (`$SHELL -ilc env -0`), with the cached PATH only as a fallback when the shell doesn't answer.
- A project's `.devcontainer/devcontainer.json` (or `.devcontainer.json`) applies on remote hosts: `remoteEnv` (with `${localEnv|containerEnv|env:X[:default]}`, workspace variables and `null` to unset) reaches every child process through one environment overlay (terminals, run configurations, language servers, debuggers, build tools, git), and `postStartCommand` (string, array or parallel object) runs once per server start in the project root as a cancellable background task; a failure raises a notification with the exit code and the last output lines without blocking the project.
- Reload Environment (`remote.reloadEnvironment`, File menu and Find Action) re-resolves the login shell and `remoteEnv` without restarting the server, starting from the process's original environment so prepended PATH entries don't pile up; it lists the changed variables, notes that running processes keep their environment, and offers to restart language servers.
- The JSONC reader moves to `ide_core::jsonc` and is shared with ide-run.
Claude-Session: https://claude.ai/code/session_01Dq6jQQn8ZhRfkkyj3PRutA
Цей коміт міститься в:
1 джерело
b6a371559a
коміт
1f9be73174
28 files changed
+1527
-123
No files matched your search
@@ -0,0 +1,66 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { EnvReloaded, IdeError } from "@/bindings";
|
||||
import type { NotificationService } from "@/workbench/contributions";
|
||||
|
||||
type Outcome = { status: "ok"; data: EnvReloaded } | { status: "error"; error: IdeError };
|
||||
let outcome: Outcome;
|
||||
const envReload = vi.fn(async () => outcome);
|
||||
const lspServers = vi.fn(async () => ({ status: "ok" as const, data: [{ id: 1 }, { id: 2 }] }));
|
||||
const lspRestart = vi.fn(async (_id: number) => ({ status: "ok" as const, data: null }));
|
||||
vi.mock("@/platform/ipc", async (orig) => {
|
||||
const actual = await orig<typeof import("@/platform/ipc")>();
|
||||
return { ...actual, commands: { ...actual.commands, envReload, lspServers, lspRestart } };
|
||||
});
|
||||
|
||||
const { reloadEnvironment, reloadSummary } = await import("./environment");
|
||||
const { provideNotificationService } = await import("@/workbench/contributions");
|
||||
|
||||
type Note = Parameters<NotificationService["notify"]>[0];
|
||||
let notes: Note[];
|
||||
|
||||
beforeEach(() => {
|
||||
notes = [];
|
||||
provideNotificationService({ notify: (n) => void notes.push(n) });
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const reloaded = (changed: string[]): EnvReloaded => ({ changed, devcontainer: null, shellRead: true });
|
||||
|
||||
describe("Reload Environment", () => {
|
||||
it("says what changed and that running processes keep their environment", () => {
|
||||
expect(reloadSummary(reloaded([]))).toBe(
|
||||
"No variables changed. Terminals and processes already running keep their old environment; new ones get the new one.",
|
||||
);
|
||||
expect(reloadSummary(reloaded(["JAVA_HOME", "PATH"]))).toMatch(/^Changed: JAVA_HOME, PATH\. Terminals/);
|
||||
const many = Array.from({ length: 10 }, (_, i) => `V${i}`);
|
||||
expect(reloadSummary(reloaded(many))).toMatch(/^Changed: V0, V1, V2, V3, V4, V5, V6, V7 and 2 more\./);
|
||||
});
|
||||
|
||||
it("offers to restart the language servers when something changed", async () => {
|
||||
outcome = { status: "ok", data: reloaded(["PATH"]) };
|
||||
await reloadEnvironment();
|
||||
expect(envReload).toHaveBeenCalledOnce();
|
||||
expect(notes).toHaveLength(1);
|
||||
expect(notes[0]).toMatchObject({ level: "info", title: "Environment reloaded" });
|
||||
const restart = notes[0]!.actions!.find((a) => a.label === "Restart Language Servers")!;
|
||||
restart.run();
|
||||
await vi.waitFor(() => expect(lspRestart).toHaveBeenCalledTimes(2));
|
||||
expect(lspRestart.mock.calls.map((c) => c[0])).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
it("has nothing to restart when nothing changed", async () => {
|
||||
outcome = { status: "ok", data: reloaded([]) };
|
||||
await reloadEnvironment();
|
||||
expect(notes[0]!.actions).toEqual([]);
|
||||
});
|
||||
|
||||
it("reports a failure as an error", async () => {
|
||||
outcome = {
|
||||
status: "error",
|
||||
error: { kind: "unavailable", message: "could not read the environment of the login shell (/bin/zsh)" },
|
||||
} as Outcome;
|
||||
await reloadEnvironment();
|
||||
expect(notes[0]).toMatchObject({ level: "error", title: "Could not reload the environment" });
|
||||
expect(String(notes[0]!.body)).toContain("login shell");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Reload Environment: new terminals, runs, language servers and tools get the login
|
||||
* shell's current environment (and, on a remote host, the project's `devcontainer.json`
|
||||
* `remoteEnv`) without restarting the backend. What is already running keeps its own.
|
||||
*/
|
||||
import { tx } from "@/platform/i18n";
|
||||
import { call, commands, type EnvReloaded } from "@/platform/ipc";
|
||||
import { notifications } from "@/workbench/contributions";
|
||||
|
||||
const KEY = "remote.reloadEnvironment";
|
||||
/** Variable names listed in the notification before "and N more". */
|
||||
const LISTED = 8;
|
||||
|
||||
/** The notification text for a reload result. */
|
||||
export function reloadSummary(r: EnvReloaded): string {
|
||||
const names = r.changed.slice(0, LISTED).join(", ");
|
||||
const more = r.changed.length - LISTED;
|
||||
const changed =
|
||||
r.changed.length === 0
|
||||
? tx("No variables changed.")
|
||||
: more > 0
|
||||
? tx`Changed: ${names} and ${more} more.`
|
||||
: tx`Changed: ${names}.`;
|
||||
const keep = tx("Terminals and processes already running keep their old environment; new ones get the new one.");
|
||||
return [changed, keep].join(" ");
|
||||
}
|
||||
|
||||
async function restartLanguageServers() {
|
||||
try {
|
||||
const servers = await call(commands.lspServers());
|
||||
await Promise.all(servers.map((s) => call(commands.lspRestart(s.id))));
|
||||
} catch (e) {
|
||||
notifications().notify({
|
||||
group: "lsp",
|
||||
level: "error",
|
||||
title: "Failed to restart language server",
|
||||
body: e instanceof Error ? e.message : String(e),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function reloadEnvironment() {
|
||||
let result: EnvReloaded;
|
||||
try {
|
||||
result = await call(commands.envReload());
|
||||
} catch (e) {
|
||||
notifications().notify({
|
||||
key: KEY,
|
||||
level: "error",
|
||||
title: "Could not reload the environment",
|
||||
body: e instanceof Error ? e.message : String(e),
|
||||
});
|
||||
return;
|
||||
}
|
||||
notifications().notify({
|
||||
key: KEY,
|
||||
level: "info",
|
||||
title: "Environment reloaded",
|
||||
body: reloadSummary(result),
|
||||
actions: result.changed.length
|
||||
? [{ label: "Restart Language Servers", run: () => void restartLanguageServers() }]
|
||||
: [],
|
||||
});
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { registerActions } from "@/platform/actions";
|
||||
import { disconnectRemote } from "@/platform/remote";
|
||||
import { registerOverlay } from "@/workbench/contributions";
|
||||
import { ConnectHost } from "./ConnectDialog";
|
||||
import { reloadEnvironment } from "./environment";
|
||||
import { closeConnectDialog, openConnectDialog } from "./model";
|
||||
|
||||
export function activate(): () => void {
|
||||
@@ -30,6 +31,16 @@ export function activate(): () => void {
|
||||
when: "projectOpen",
|
||||
run: () => disconnectRemote(),
|
||||
},
|
||||
{
|
||||
id: "remote.reloadEnvironment",
|
||||
title: "Reload Environment",
|
||||
description:
|
||||
"Reads the login shell's environment (and devcontainer.json remoteEnv on a remote host) again for new terminals and processes.",
|
||||
category: "File",
|
||||
keywords: ["env", "environment", "variables", "path", "shell", "remoteEnv", "devcontainer", "reload"],
|
||||
when: "projectOpen",
|
||||
run: () => reloadEnvironment(),
|
||||
},
|
||||
]),
|
||||
];
|
||||
return () => {
|
||||
|
||||
Reference in new issue
Заблокувати користувача