mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 22:55:24 +00:00
feat: collapse plugin runtime to one tier behind a plugin SDK
This commit is contained in:
1 parent
6e058f6e9e
commit
4758016f41
96 files changed
+34631
-8551
No files matched your search
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* Core owns shutdown.
|
||||
*
|
||||
* gracefulShutdown in starter.ts calls shutdownPlugins(), which runs every
|
||||
* plugin's deactivate and disposes what it registered. A plugin does not need
|
||||
* its own process signal handler, and must not have one: the docker console
|
||||
* used to install a SIGTERM listener that called process.exit(0), which
|
||||
* skipped the rest of the sequence including other plugins and the database
|
||||
* flush.
|
||||
*/
|
||||
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import fs from "node:fs";
|
||||
import net from "node:net";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { PluginLoader } from "../../plugins/loader.js";
|
||||
import { createFixturePlugin } from "./fixture-plugin.js";
|
||||
|
||||
vi.mock("../../utils/logger.js", () => ({
|
||||
pluginLogger: {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
success: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../../plugins/permissions.js", () => ({
|
||||
assertCapability: vi.fn().mockResolvedValue(undefined),
|
||||
hasCapability: vi.fn().mockResolvedValue(true),
|
||||
PluginCapabilityError: class extends Error {},
|
||||
invalidatePluginPermissionCache: vi.fn(),
|
||||
}));
|
||||
|
||||
const CONSOLE_PORT = 39109;
|
||||
const cleanups: Array<() => void> = [];
|
||||
|
||||
function tempRoot(prefix: string): string {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
||||
cleanups.push(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||
return root;
|
||||
}
|
||||
|
||||
function isListening(port: number): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const socket = net.connect({ port, host: "127.0.0.1" });
|
||||
socket.once("connect", () => {
|
||||
socket.destroy();
|
||||
resolve(true);
|
||||
});
|
||||
socket.once("error", () => resolve(false));
|
||||
});
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.TERMIX_BUNDLED_PLUGINS_DIR;
|
||||
delete process.env.DATA_DIR;
|
||||
while (cleanups.length) cleanups.pop()?.();
|
||||
});
|
||||
|
||||
/**
|
||||
* Shaped like the docker plugin: a REST server and a console WebSocket
|
||||
* server, both started inside activate and closed through disposables.
|
||||
*/
|
||||
const DOCKER_LIKE = `
|
||||
import net from "node:net";
|
||||
|
||||
export async function activate(ctx) {
|
||||
const console = net.createServer();
|
||||
await new Promise((resolve) =>
|
||||
console.listen(${CONSOLE_PORT}, "127.0.0.1", resolve),
|
||||
);
|
||||
ctx.disposables.add(
|
||||
() => new Promise((resolve) => console.close(() => resolve())),
|
||||
);
|
||||
}
|
||||
`;
|
||||
|
||||
describe("shutdownPlugins closes what plugins own", () => {
|
||||
it("closes a console-style WebSocket server on shutdown", async () => {
|
||||
const bundled = tempRoot("termix-bundled-");
|
||||
const user = tempRoot("termix-data-");
|
||||
createFixturePlugin({
|
||||
id: "docker",
|
||||
root: bundled,
|
||||
backendSource: DOCKER_LIKE,
|
||||
});
|
||||
process.env.TERMIX_BUNDLED_PLUGINS_DIR = bundled;
|
||||
process.env.DATA_DIR = user;
|
||||
|
||||
const loader = new PluginLoader();
|
||||
await loader.loadAll();
|
||||
await loader.activate("docker");
|
||||
|
||||
expect(await isListening(CONSOLE_PORT)).toBe(true);
|
||||
|
||||
await loader.shutdown();
|
||||
|
||||
expect(await isListening(CONSOLE_PORT)).toBe(false);
|
||||
});
|
||||
|
||||
// One plugin failing to shut down cleanly must not strand the others,
|
||||
// which is the other half of why core drives this rather than each plugin.
|
||||
it("keeps going when one plugin's deactivate throws", async () => {
|
||||
const marker = path.join(tempRoot("termix-marker-"), "stopped.txt");
|
||||
const bundled = tempRoot("termix-bundled-");
|
||||
const user = tempRoot("termix-data-");
|
||||
|
||||
createFixturePlugin({
|
||||
id: "alpha",
|
||||
root: bundled,
|
||||
backendSource: `
|
||||
export async function activate() {}
|
||||
export async function deactivate() {
|
||||
throw new Error("alpha refuses to stop");
|
||||
}
|
||||
`,
|
||||
});
|
||||
createFixturePlugin({
|
||||
id: "beta",
|
||||
root: bundled,
|
||||
backendSource: `
|
||||
import fs from "node:fs";
|
||||
export async function activate() {}
|
||||
export async function deactivate() {
|
||||
fs.appendFileSync(${JSON.stringify(marker)}, "beta stopped\\n");
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
process.env.TERMIX_BUNDLED_PLUGINS_DIR = bundled;
|
||||
process.env.DATA_DIR = user;
|
||||
|
||||
const loader = new PluginLoader();
|
||||
await loader.loadAll();
|
||||
await loader.activateAll(["alpha", "beta"]);
|
||||
|
||||
await loader.shutdown();
|
||||
|
||||
expect(loader.get("alpha")?.state).toBe("stopped");
|
||||
expect(loader.get("beta")?.state).toBe("stopped");
|
||||
expect(fs.readFileSync(marker, "utf8")).toBe("beta stopped\n");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user