mirror of
https://github.com/lmstudio-ai/lms.git
synced 2025-09-24 08:25:42 +08:00
Plugin registration
This commit is contained in:
@ -1,11 +1,14 @@
|
|||||||
import { SimpleLogger, Validator } from "@lmstudio/lms-common";
|
import { SimpleLogger, Validator } from "@lmstudio/lms-common";
|
||||||
import { Esbuild, EsPluginRunnerWatcher } from "@lmstudio/lms-es-plugin-runner";
|
import { Esbuild, EsPluginRunnerWatcher } from "@lmstudio/lms-es-plugin-runner";
|
||||||
|
import { generateRandomBase64 } from "@lmstudio/lms-isomorphic";
|
||||||
import { pluginManifestSchema } from "@lmstudio/lms-shared-types/dist/PluginManifest";
|
import { pluginManifestSchema } from "@lmstudio/lms-shared-types/dist/PluginManifest";
|
||||||
import { type ChildProcess, spawn } from "child_process";
|
import { type LMStudioClient, type RegisterDevelopmentPluginOpts } from "@lmstudio/sdk";
|
||||||
|
import { type ChildProcessWithoutNullStreams, spawn } from "child_process";
|
||||||
import { command } from "cmd-ts";
|
import { command } from "cmd-ts";
|
||||||
import { access, readFile } from "fs/promises";
|
import { access, readFile } from "fs/promises";
|
||||||
import { dirname, join, resolve } from "path";
|
import { dirname, join, resolve } from "path";
|
||||||
import { cwd } from "process";
|
import { cwd } from "process";
|
||||||
|
import { createClient, createClientArgs } from "../createClient";
|
||||||
import { createLogger, logLevelArgs } from "../logLevel";
|
import { createLogger, logLevelArgs } from "../logLevel";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -36,36 +39,50 @@ async function findProjectFolder(logger: SimpleLogger, cwd: string) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
type PluginProcessStatus = "stopped" | "running" | "restarting";
|
type PluginProcessStatus = "stopped" | "starting" | "running" | "restarting";
|
||||||
|
|
||||||
class PluginProcess {
|
class PluginProcess {
|
||||||
public constructor(
|
public constructor(
|
||||||
|
private readonly client: LMStudioClient,
|
||||||
|
private readonly registerDevelopmentPluginOpts: RegisterDevelopmentPluginOpts,
|
||||||
private readonly cwd: string,
|
private readonly cwd: string,
|
||||||
private readonly executable: string,
|
private readonly executable: string,
|
||||||
private readonly args: Array<string>,
|
private readonly args: Array<string>,
|
||||||
private readonly env: Record<string, string>,
|
private readonly env: Record<string, string>,
|
||||||
private readonly logger: SimpleLogger,
|
private readonly logger: SimpleLogger,
|
||||||
) {}
|
) {}
|
||||||
private currentProcess: ChildProcess | null = null;
|
private serverLogger = new SimpleLogger("plugin-server", this.logger);
|
||||||
|
private stderrLogger = new SimpleLogger("stderr", this.logger);
|
||||||
|
private currentProcess: ChildProcessWithoutNullStreams | null = null;
|
||||||
private status: PluginProcessStatus = "stopped";
|
private status: PluginProcessStatus = "stopped";
|
||||||
|
private endPlugin: (() => Promise<void>) | null = null;
|
||||||
|
|
||||||
private startProcess() {
|
private async startProcess() {
|
||||||
|
this.status = "starting";
|
||||||
|
this.endPlugin = await this.client.plugins.registerDevelopmentPlugin(
|
||||||
|
this.registerDevelopmentPluginOpts,
|
||||||
|
);
|
||||||
this.currentProcess = spawn(this.executable, this.args, {
|
this.currentProcess = spawn(this.executable, this.args, {
|
||||||
stdio: "inherit",
|
|
||||||
env: {
|
env: {
|
||||||
FORCE_COLOR: "1",
|
FORCE_COLOR: "1",
|
||||||
...this.env,
|
...this.env,
|
||||||
},
|
},
|
||||||
cwd: this.cwd,
|
cwd: this.cwd,
|
||||||
});
|
});
|
||||||
this.currentProcess.on("exit", (code, signal) => {
|
this.currentProcess.stdout.on("data", data => this.logger.info(data.toString("utf-8").trim()));
|
||||||
|
this.currentProcess.stderr.on("data", data =>
|
||||||
|
this.stderrLogger.error(data.toString("utf-8").trim()),
|
||||||
|
);
|
||||||
|
this.currentProcess.on("exit", async (code, signal) => {
|
||||||
|
await this.endPlugin?.();
|
||||||
|
this.endPlugin = null;
|
||||||
if (code !== null) {
|
if (code !== null) {
|
||||||
this.logger.warn(`Plugin process exited with code ${code}`);
|
this.serverLogger.warn(`Plugin process exited with code ${code}`);
|
||||||
} else {
|
} else {
|
||||||
if (signal === "SIGKILL") {
|
if (signal === "SIGKILL") {
|
||||||
// OK to ignore because we killed it
|
// OK to ignore because we killed it
|
||||||
} else {
|
} else {
|
||||||
this.logger.warn(`Plugin process exited with signal ${signal}`);
|
this.serverLogger.warn(`Plugin process exited with signal ${signal}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.status === "restarting") {
|
if (this.status === "restarting") {
|
||||||
@ -82,10 +99,22 @@ class PluginProcess {
|
|||||||
this.startProcess();
|
this.startProcess();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "starting": {
|
||||||
|
// Already starting. Do nothing.
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "running": {
|
case "running": {
|
||||||
this.status = "restarting";
|
this.status = "restarting";
|
||||||
this.currentProcess?.kill("SIGKILL");
|
if (this.endPlugin === null) {
|
||||||
this.currentProcess = null;
|
this.currentProcess?.kill("SIGKILL");
|
||||||
|
this.currentProcess = null;
|
||||||
|
} else {
|
||||||
|
this.endPlugin().then(() => {
|
||||||
|
this.endPlugin = null;
|
||||||
|
this.currentProcess?.kill("SIGKILL");
|
||||||
|
this.currentProcess = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "restarting": {
|
case "restarting": {
|
||||||
@ -101,9 +130,11 @@ export const dev = command({
|
|||||||
description: "Starts the development server for the plugin in the current folder.",
|
description: "Starts the development server for the plugin in the current folder.",
|
||||||
args: {
|
args: {
|
||||||
...logLevelArgs,
|
...logLevelArgs,
|
||||||
|
...createClientArgs,
|
||||||
},
|
},
|
||||||
handler: async args => {
|
handler: async args => {
|
||||||
const logger = createLogger(args);
|
const logger = createLogger(args);
|
||||||
|
const client = await createClient(logger, args);
|
||||||
const projectPath = await findProjectFolder(logger, cwd());
|
const projectPath = await findProjectFolder(logger, cwd());
|
||||||
if (projectPath === null) {
|
if (projectPath === null) {
|
||||||
logger.errorText`
|
logger.errorText`
|
||||||
@ -129,19 +160,26 @@ export const dev = command({
|
|||||||
|
|
||||||
logger.info(`Starting the development server for ${manifest.owner}/${manifest.name}...`);
|
logger.info(`Starting the development server for ${manifest.owner}/${manifest.name}...`);
|
||||||
|
|
||||||
const pluginServerLogger = new SimpleLogger("plugin-server", logger);
|
const pluginClientIdentifier = `plugin:dev:${manifest.owner}/${manifest.name}`;
|
||||||
|
const pluginClientPasskey = generateRandomBase64();
|
||||||
|
|
||||||
const watcher = new EsPluginRunnerWatcher(new Esbuild(), cwd(), logger);
|
const watcher = new EsPluginRunnerWatcher(new Esbuild(), cwd(), logger);
|
||||||
|
|
||||||
const pluginProcess = new PluginProcess(
|
const pluginProcess = new PluginProcess(
|
||||||
|
client,
|
||||||
|
{
|
||||||
|
clientIdentifier: pluginClientIdentifier,
|
||||||
|
clientPasskey: pluginClientPasskey,
|
||||||
|
manifest,
|
||||||
|
},
|
||||||
projectPath,
|
projectPath,
|
||||||
process.platform === "win32" ? "node.exe" : "node",
|
process.platform === "win32" ? "node.exe" : "node",
|
||||||
["--enable-source-maps", join(".lmstudio", "dev.js")],
|
["--enable-source-maps", join(".lmstudio", "dev.js")],
|
||||||
{
|
{
|
||||||
LMS_PLUGIN_CLIENT_IDENTIFIER: `dev-plugin-${manifest.owner}/${manifest.name}`,
|
LMS_PLUGIN_CLIENT_IDENTIFIER: pluginClientIdentifier,
|
||||||
LMS_PLUGIN_CLIENT_PASSKEY: `dev-plugin-${manifest.owner}/${manifest.name}`,
|
LMS_PLUGIN_CLIENT_PASSKEY: pluginClientPasskey,
|
||||||
},
|
},
|
||||||
pluginServerLogger,
|
logger,
|
||||||
);
|
);
|
||||||
|
|
||||||
watcher.updatedEvent.subscribe(() => {
|
watcher.updatedEvent.subscribe(() => {
|
||||||
|
Reference in New Issue
Block a user