diff --git a/core/src/index.ts b/core/src/index.ts index a1ff9fc4991..6576ef6109f 100644 --- a/core/src/index.ts +++ b/core/src/index.ts @@ -30,6 +30,7 @@ export type { FrameworkPluginFactory, } from "./plugins/framework.js"; export type { OnServerStart } from "./preview-env.js"; +export { toVitePath } from "./vite/vite-paths.js"; const require = createRequire(import.meta.url); diff --git a/core/src/vite/plugins/virtual-plugin.ts b/core/src/vite/plugins/virtual-plugin.ts index f5c84001f32..aa823c71b06 100644 --- a/core/src/vite/plugins/virtual-plugin.ts +++ b/core/src/vite/plugins/virtual-plugin.ts @@ -4,6 +4,7 @@ import path from "path"; import type { Logger } from "pino"; import type * as vite from "vite"; import { transformWithEsbuild } from "vite"; +import { toVitePath } from "../vite-paths.js"; const VIRTUAL_PREFIX = `/@previewjs-virtual:`; const VIRTUAL_PREFIX2 = `/@id/__x00__`; @@ -172,9 +173,7 @@ export function virtualPlugin(options: { if (!entry || entry.kind !== "file") { return; } - // Note: backslash handling is Windows-specific. - const virtualModuleId = - VIRTUAL_PREFIX + absoluteFilePath.replace(/\\/g, "/"); + const virtualModuleId = VIRTUAL_PREFIX + toVitePath(absoluteFilePath); const node = moduleGraph.getModuleById(virtualModuleId); return node && [node]; }, diff --git a/core/src/vite/vite-manager.ts b/core/src/vite/vite-manager.ts index 0d5c8714f97..82be49dcfed 100644 --- a/core/src/vite/vite-manager.ts +++ b/core/src/vite/vite-manager.ts @@ -35,6 +35,7 @@ import { } from "./plugins/preview-script.js"; import { publicAssetImportPluginPlugin } from "./plugins/public-asset-import-plugin.js"; import { virtualPlugin } from "./plugins/virtual-plugin.js"; +import { toVitePath } from "./vite-paths.js"; const POSTCSS_CONFIG_FILE = [ ".postcssrc", @@ -513,7 +514,7 @@ export class ViteManager { const { viteServer, config } = state; if (info.virtual) { const modules = await viteServer.moduleGraph.getModulesByFile( - absoluteFilePath + toVitePath(absoluteFilePath) ); for (const module of modules || []) { if (!module.id) { diff --git a/core/src/vite/vite-paths.ts b/core/src/vite/vite-paths.ts new file mode 100644 index 00000000000..1e36a5ed488 --- /dev/null +++ b/core/src/vite/vite-paths.ts @@ -0,0 +1,9 @@ +export function toVitePath(absoluteFilePath: string) { + if (absoluteFilePath.match(/^[a-z]:/)) { + // Vite uses uppercase drive letters on Windows. + absoluteFilePath = + absoluteFilePath[0]?.toUpperCase() + absoluteFilePath.substring(1); + } + // Vite uses forward slash even on Windows. + return absoluteFilePath.replace(/\\/g, "/"); +} diff --git a/daemon/package.json b/daemon/package.json index 8cb8adf3e9d..0adb4728d1d 100644 --- a/daemon/package.json +++ b/daemon/package.json @@ -42,8 +42,6 @@ "@previewjs/loader": "workspace:*", "@types/node": "^20.10.0", "exclusive-promises": "^1.0.3", - "exit-hook": "^3.2.0", - "is-wsl": "^3.1.0", "rimraf": "^5.0.5", "ts-node": "^10.9.1", "typescript": "^5.2.2", diff --git a/daemon/src/api.ts b/daemon/src/api.ts index 80c4eb776e1..9c4f97cf67e 100644 --- a/daemon/src/api.ts +++ b/daemon/src/api.ts @@ -1,9 +1,3 @@ -export type KillRequest = Record; - -export type KillResponse = { - pid: number; -}; - export type CrawlFileRequest = { absoluteFilePath: string; }; diff --git a/daemon/src/client.ts b/daemon/src/client.ts index fe2eb103f3f..39046f7cdc5 100644 --- a/daemon/src/client.ts +++ b/daemon/src/client.ts @@ -1,13 +1,10 @@ import { exclusivePromiseRunner } from "exclusive-promises"; -import { existsSync, readFileSync, unlinkSync } from "fs"; import http from "http"; import type { CheckPreviewStatusRequest, CheckPreviewStatusResponse, CrawlFileRequest, CrawlFileResponse, - KillRequest, - KillResponse, StartPreviewRequest, StartPreviewResponse, StopPreviewRequest, @@ -71,7 +68,6 @@ export function createClient(baseUrl: string): Client { } const client: Client = { - kill: () => makeRPC("/previewjs/kill")({}), crawlFile: makeRPC("/crawl-file"), startPreview: makeRPC("/previews/start"), checkPreviewStatus: makeRPC("/previews/status"), @@ -81,20 +77,7 @@ export function createClient(baseUrl: string): Client { return client; } -export function destroyDaemon(lockFilePath: string) { - if (existsSync(lockFilePath)) { - const pid = parseInt(readFileSync(lockFilePath, "utf8")); - try { - process.kill(pid, "SIGKILL"); - } catch { - // The daemon was already dead. - } - unlinkSync(lockFilePath); - } -} - export interface Client { - kill(): Promise; crawlFile(request: CrawlFileRequest): Promise; startPreview(request: StartPreviewRequest): Promise; checkPreviewStatus( diff --git a/daemon/src/index.ts b/daemon/src/index.ts index 335fb37f58b..a38dee16a89 100644 --- a/daemon/src/index.ts +++ b/daemon/src/index.ts @@ -1,23 +1,13 @@ import type { PreviewServer, Workspace } from "@previewjs/core"; import { load } from "@previewjs/loader/runner"; -import exitHook from "exit-hook"; -import { - appendFileSync, - existsSync, - readFileSync, - unlinkSync, - writeFileSync, -} from "fs"; +import { appendFileSync, writeFileSync } from "fs"; import http from "http"; -import isWsl from "is-wsl"; import path from "path"; import type { CheckPreviewStatusRequest, CheckPreviewStatusResponse, CrawlFileRequest, CrawlFileResponse, - KillRequest, - KillResponse, StartPreviewRequest, StartPreviewResponse, StopPreviewRequest, @@ -25,51 +15,6 @@ import type { UpdatePendingFileRequest, UpdatePendingFileResponse, } from "./api.js"; -import { createClient } from "./client.js"; - -const lockFilePath = process.env.PREVIEWJS_LOCK_FILE; -if (lockFilePath) { - if (existsSync(lockFilePath)) { - const pid = parseInt(readFileSync(lockFilePath, "utf8")); - try { - // Test if PID is still running. This will fail if not. - process.kill(pid, 0); - } catch { - // Previous process ended prematurely (e.g. hardware crash). - try { - unlinkSync(lockFilePath); - } catch { - // It's possible for several processes to try unlinking at the same time. - // For example, a running daemon that is exiting at the same time. - // Ignore. - } - } - } - try { - writeFileSync(lockFilePath, process.pid.toString(10), { - flag: "wx", - }); - exitHook((signal) => { - // Note: The bracketed tag is required for VS Code and IntelliJ to detect exit. - process.stdout.write( - `[exit] Preview.js daemon shutting down with signal: ${signal}\n` - ); - try { - unlinkSync(lockFilePath); - } catch { - // It's possible for several processes to try unlinking at the same time. - // For example, a new daemon that will replace this one. - // Ignore. - } - }); - } catch { - // eslint-disable-next-line no-console - console.error( - `Unable to obtain lock: ${lockFilePath}\nYou can delete this file manually if you wish to override the lock.` - ); - process.exit(1); - } -} const logFilePath = process.env.PREVIEWJS_LOG_FILE; @@ -112,8 +57,30 @@ export async function startDaemon({ versionCode, port, }: DaemonStartOptions) { + const parentProcessId = parseInt( + process.env["PREVIEWJS_PARENT_PROCESS_PID"] || "0" + ); + if (!parentProcessId) { + throw new Error( + "Missing environment variable: PREVIEWJS_PARENT_PROCESS_PID" + ); + } + + // Kill the daemon if the parent process dies. + setInterval(() => { + try { + process.kill(parentProcessId, 0); + // Parent process is still alive, see https://stackoverflow.com/a/21296291. + } catch (e) { + process.stdout.write( + `[exit] Parent process with PID ${parentProcessId} exited. Daemon exiting.\n` + ); + process.exit(0); + } + }, 1000); + const previewjs = await load({ - installDir: loaderInstallDir, + installDir: loaderInstallDir.replace(/\//g, path.sep), workerFilePath: loaderWorkerPath, onServerStartModuleName, }); @@ -121,50 +88,6 @@ export async function startDaemon({ const previewServers: Record = {}; const endpoints: Record Promise> = {}; - let wslRoot: string | null = null; - - function transformAbsoluteFilePath(absoluteFilePath: string) { - if (!isWsl) { - return absoluteFilePath; - } - if (absoluteFilePath.match(/^[a-z]:.*$/i)) { - if (!wslRoot) { - wslRoot = detectWslRoot(); - } - // This is a Windows path, which needs to be converted to Linux format inside WSL. - return `${wslRoot}/${absoluteFilePath - .substring(0, 1) - .toLowerCase()}/${absoluteFilePath.substring(3).replace(/\\/g, "/")}`; - } - // This is already a Linux path. - return absoluteFilePath; - } - - function detectWslRoot() { - const wslConfPath = "/etc/wsl.conf"; - const defaultRoot = "/mnt"; - try { - if (!existsSync(wslConfPath)) { - return defaultRoot; - } - const configText = readFileSync(wslConfPath, "utf8"); - const match = configText.match(/root\s*=\s*(.*)/); - if (!match) { - return defaultRoot; - } - const detectedRoot = match[1]!.trim(); - if (detectedRoot.endsWith("/")) { - return detectedRoot.substring(0, detectedRoot.length - 1); - } else { - return detectedRoot; - } - } catch (e) { - logger.warn( - `Unable to read WSL config, assuming default root: ${defaultRoot}` - ); - return defaultRoot; - } - } const app = http.createServer((req, res) => { if (req.headers["origin"]) { @@ -236,23 +159,13 @@ export async function startDaemon({ class NotFoundError extends Error {} - endpoint("/previewjs/kill", async () => { - setTimeout(() => { - logger.info("Seppuku was requested. Bye bye."); - process.exit(0); - }, 1000); - return { - pid: process.pid, - }; - }); - const inWorkspace = ( absoluteFilePath: string, run: (workspace: Workspace | null) => Promise ) => previewjs.inWorkspace({ versionCode, - absoluteFilePath: transformAbsoluteFilePath(absoluteFilePath), + absoluteFilePath, run, }); @@ -265,10 +178,7 @@ export async function startDaemon({ } const { components, stories } = await workspace.crawlFiles([ path - .relative( - workspace.rootDir, - transformAbsoluteFilePath(absoluteFilePath) - ) + .relative(workspace.rootDir, absoluteFilePath) .replace(/\\/g, "/"), ]); return { @@ -326,47 +236,14 @@ export async function startDaemon({ endpoint( "/pending-files/update", async (req) => { - await previewjs.updateFileInMemory( - transformAbsoluteFilePath(req.absoluteFilePath), - req.utf8Content - ); + await previewjs.updateFileInMemory(req.absoluteFilePath, req.utf8Content); return {}; } ); await new Promise((resolve, reject) => { app.listen(port, resolve).on("error", async (e: any) => { - if (e.code !== "EADDRINUSE") { - return reject(e); - } - try { - // There's another daemon running already on the same port. - // Attempt to kill it and try again. This can happen for example - // when upgrading from one version to another of Preview.js. - const client = createClient(`http://localhost:${port}`); - const { pid } = await client.kill(); - // Wait for daemon to be killed. - let oldDaemonDead = false; - for (let i = 0; !oldDaemonDead && i < 10; i++) { - try { - // Test if PID is still running. This will fail if not. - process.kill(pid, 0); - await new Promise((resolve) => setTimeout(resolve, 1000)); - } catch { - oldDaemonDead = true; - app.listen(port, resolve).on("error", reject); - } - } - if (!oldDaemonDead) { - reject( - new Error( - `Unable to kill old daemon server running on port ${port}` - ) - ); - } - } catch (e) { - reject(e); - } + reject(e); }); }); diff --git a/integrations/intellij/build.gradle.kts b/integrations/intellij/build.gradle.kts index eea5f192e89..4ba4ef99310 100644 --- a/integrations/intellij/build.gradle.kts +++ b/integrations/intellij/build.gradle.kts @@ -69,7 +69,7 @@ tasks { } withType { kotlinOptions.jvmTarget = "17" - kotlinOptions.freeCompilerArgs = listOf("-Xjvm-default=enable", "-Xopt-in=kotlin.RequiresOptIn") + kotlinOptions.freeCompilerArgs = listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn") } withType { @@ -82,7 +82,15 @@ tasks { files(layout.buildDirectory) ) exec { - commandLine("sh", "-c", "../../node_modules/turbo/bin/turbo run build --scope=@previewjs/intellij-daemon") + if (System.getProperty("os.name").lowercase().contains("win")) { + commandLine( + "cmd.exe", + "/c", + "node " + System.getProperty("user.dir") + "\\..\\..\\node_modules\\turbo\\bin\\turbo run build --scope=@previewjs/intellij-daemon" + ) + } else { + commandLine("sh", "-c", "../../node_modules/turbo/bin/turbo run build --scope=@previewjs/intellij-daemon") + } } from(daemonDir) { into("${properties("pluginName")}/daemon") diff --git a/integrations/intellij/src/main/kotlin/com/previewjs/intellij/plugin/services/PreviewJsSharedService.kt b/integrations/intellij/src/main/kotlin/com/previewjs/intellij/plugin/services/PreviewJsSharedService.kt index c3a1bf4fb25..7fffe797e61 100644 --- a/integrations/intellij/src/main/kotlin/com/previewjs/intellij/plugin/services/PreviewJsSharedService.kt +++ b/integrations/intellij/src/main/kotlin/com/previewjs/intellij/plugin/services/PreviewJsSharedService.kt @@ -154,39 +154,24 @@ ${e.stackTraceToString()}""", throw Error("No port is available to run Preview.js daemon") } val nodeVersionProcess = - processBuilder("node --version", useWsl = false).directory(nodeDirPath.toFile()).start() - var useWsl = false - try { - if (nodeVersionProcess.waitFor() != 0) { - throw Error("Preview.js was unable to run node.\\n\\nIs it installed? You may need to restart your IDE.") - } - checkNodeVersion(nodeVersionProcess) - } catch (e: Error) { - // Unable to start Node. Check WSL if we're on Windows. - if (!isWindows()) { - throw e - } - val nodeVersionProcessWsl = - processBuilder("node --version", useWsl = true).directory(nodeDirPath.toFile()).start() - if (nodeVersionProcessWsl.waitFor() == 0) { - checkNodeVersion(nodeVersionProcessWsl) - useWsl = true - } else { - // If WSL failed, just ignore it. - throw e - } + processBuilder("node --version").directory(nodeDirPath.toFile()).start() + if (nodeVersionProcess.waitFor() != 0) { + throw Error("Preview.js was unable to run node.\\n\\nIs it installed? You may need to restart your IDE.") } - val builder = processBuilder("node --trace-warnings dist/main.js $port", useWsl).redirectErrorStream(true) + checkNodeVersion(nodeVersionProcess) + val builder = processBuilder("node --trace-warnings dist/main.js $port").redirectErrorStream(true) .directory(nodeDirPath.toFile()) + builder.environment()["PREVIEWJS_PARENT_PROCESS_PID"] = ProcessHandle.current().pid().toString() val process = builder.start() daemonProcess = process val daemonOutputReader = BufferedReader(InputStreamReader(process.inputStream)) val ready = CompletableDeferred() coroutineScope.launch { + var lines = mutableListOf() while (!disposed) { while (!daemonOutputReader.ready()) { if (!process.isAlive) { - throw Error("Daemon process died") + ready.completeExceptionally(Error("Daemon process died:\n${lines.joinToString("\n")}")) } delay(100) } @@ -206,6 +191,7 @@ ${e.stackTraceToString()}""", if (line.contains("[ready]")) { ready.complete(Unit) } + lines.add(line) everyProject(project) { printToConsole(cleanStdOut(line + "\n")) } @@ -263,22 +249,13 @@ ${e.stackTraceToString()}""", } } - private fun processBuilder(command: String, useWsl: Boolean): ProcessBuilder { + private fun processBuilder(command: String): ProcessBuilder { return if (isWindows()) { - if (useWsl) { - ProcessBuilder( - "wsl", - "bash", - "-lic", - command - ) - } else { - ProcessBuilder( - "cmd.exe", - "/C", - command - ) - } + ProcessBuilder( + "cmd.exe", + "/C", + command + ) } else { // Note: in production builds of IntelliJ / WebStorm, PATH is not initialised // from the shell. This means that /usr/local/bin or nvm paths may not be diff --git a/integrations/vscode/esbuild.mjs b/integrations/vscode/esbuild.mjs index 5039bf3e581..0260bc39aae 100644 --- a/integrations/vscode/esbuild.mjs +++ b/integrations/vscode/esbuild.mjs @@ -19,11 +19,6 @@ try { "process.env.PREVIEWJS_PACKAGE_NAME": JSON.stringify( process.env.PREVIEWJS_PACKAGE_NAME || "@previewjs/pro" ), - ...(process.env.PREVIEWJS_PORT && { - "process.env.PREVIEWJS_PORT": JSON.stringify( - process.env.PREVIEWJS_PORT - ), - }), ...(process.env.PREVIEWJS_MODULES_DIR && { "process.env.PREVIEWJS_MODULES_DIR": JSON.stringify( path.join(__dirname, process.env.PREVIEWJS_MODULES_DIR) diff --git a/integrations/vscode/package.json b/integrations/vscode/package.json index b6005822852..cf305433499 100644 --- a/integrations/vscode/package.json +++ b/integrations/vscode/package.json @@ -106,6 +106,7 @@ "esbuild": "^0.19.5", "exclusive-promises": "^1.0.3", "execa": "^8.0.1", + "get-port": "^7.0.0", "ovsx": "^0.8.3", "rimraf": "^5.0.5", "strip-ansi": "^7.1.0", diff --git a/integrations/vscode/src/index.ts b/integrations/vscode/src/index.ts index 0fc0d4f46af..34800c97ce2 100644 --- a/integrations/vscode/src/index.ts +++ b/integrations/vscode/src/index.ts @@ -1,4 +1,3 @@ -import { destroyDaemon } from "@previewjs/daemon/client"; import path from "path"; import vscode from "vscode"; import { closePreviewPanel, updatePreviewPanel } from "./preview-panel.js"; @@ -6,7 +5,6 @@ import { ensurePreviewServerStarted, ensurePreviewServerStopped, } from "./preview-server.js"; -import { daemonLockFilePath } from "./start-daemon.js"; import { createState } from "./state.js"; // Note: all commands in package.json must appear here. The reverse is not true. @@ -183,8 +181,6 @@ export async function activate({ subscriptions }: vscode.ExtensionContext) { if (state) { state.dispose(); } - currentState = Promise.resolve(null); - destroyDaemon(daemonLockFilePath); currentState = createState({ outputChannel, runningServerStatusBarItem, diff --git a/integrations/vscode/src/start-daemon.ts b/integrations/vscode/src/start-daemon.ts index 4a5976d8c1d..34c1ef4201e 100644 --- a/integrations/vscode/src/start-daemon.ts +++ b/integrations/vscode/src/start-daemon.ts @@ -4,32 +4,34 @@ import type { ExecaChildProcess, ExecaReturnValue, Options } from "execa"; import { execa } from "execa"; import type { FSWatcher } from "fs"; import { closeSync, openSync, readFileSync, utimesSync, watch } from "fs"; +import getPort from "get-port"; import path from "path"; import stripAnsi from "strip-ansi"; import type { OutputChannel } from "vscode"; import vscode from "vscode"; -const port = process.env.PREVIEWJS_PORT || "9315"; -const logsPath = path.join(__dirname, "daemon.log"); - -export const daemonLockFilePath = path.join(__dirname, "process.lock"); - -export async function ensureDaemonRunning( - outputChannel: OutputChannel -): Promise<{ +export async function startDaemon(outputChannel: OutputChannel): Promise<{ client: Client; watcher: FSWatcher; daemonProcess: ExecaChildProcess; } | null> { + const port = await getPort(); + const now = new Date(); + const logsPath = path.join( + __dirname, + `daemon-${now.getFullYear()}${ + now.getMonth() + 1 + }${now.getDate()}${now.getHours()}${now.getMinutes()}-${port}.log` + ); const client = createClient(`http://localhost:${port}`); - const daemon = await startDaemon(outputChannel); + const daemon = await startDaemonProcess(port, logsPath, outputChannel); if (!daemon) { return null; } // Note: we expect startDaemon().process to exit 1 almost immediately when there is another // daemon running already (e.g. from another workspace) because of the lock file. This is // fine and working by design. - const ready = streamDaemonLogs(outputChannel); + const ready = streamDaemonLogs(logsPath, outputChannel); const watcher = await ready; return { client, @@ -40,11 +42,13 @@ export async function ensureDaemonRunning( // Important: we wrap daemonProcess into a Promise so that awaiting startDaemon() // doesn't automatically await the process itself (which may not exit for a long time!). -async function startDaemon(outputChannel: OutputChannel): Promise<{ +async function startDaemonProcess( + port: number, + logsPath: string, + outputChannel: OutputChannel +): Promise<{ daemonProcess: ExecaChildProcess; } | null> { - const isWindows = process.platform === "win32"; - let useWsl = false; const nodeVersionCommand = "node --version"; outputChannel.appendLine(`$ ${nodeVersionCommand}`); const [command, commandArgs] = @@ -70,71 +74,34 @@ async function startDaemon(outputChannel: OutputChannel): Promise<{ if (checkNodeVersion.kind === "valid") { outputChannel.appendLine(`✅ Detected compatible NodeJS version`); } - invalidNode: if (checkNodeVersion.kind === "invalid") { + if (checkNodeVersion.kind === "invalid") { outputChannel.appendLine(checkNodeVersion.message); - if (!isWindows) { - return null; - } - // On Windows, try WSL as well. - outputChannel.appendLine(`Attempting again with WSL...`); - const wslArgs = wslCommandArgs("node --version"); - outputChannel.appendLine( - `$ wsl ${wslArgs.map((a) => (a.includes(" ") ? `"${a}"` : a)).join(" ")}` - ); - const nodeVersionWsl = await execa("wsl", wslArgs, { - cwd: __dirname, - reject: false, - }); - const checkNodeVersionWsl = checkNodeVersionResult(nodeVersionWsl); - if (checkNodeVersionWsl.kind === "valid") { - outputChannel.appendLine(`✅ Detected compatible NodeJS version in WSL`); - // The right version of Node is available through WSL. No need to crash, perfect. - useWsl = true; - break invalidNode; - } - outputChannel.appendLine(checkNodeVersionWsl.message); return null; } - outputChannel.appendLine( - `🚀 Starting Preview.js daemon${useWsl ? " from WSL" : ""}...` - ); + outputChannel.appendLine(`🚀 Starting Preview.js daemon...`); outputChannel.appendLine(`Streaming daemon logs to: ${logsPath}`); const nodeDaemonCommand = "node --trace-warnings daemon.js"; const daemonOptions: Options = { cwd: __dirname, - // https://nodejs.org/api/child_process.html#child_process_options_detached - // If we use "inherit", we end up with a "write EPIPE" crash when the child process - // tries to log after the parent process exited (even when detached properly). - stdio: "ignore", env: { - PREVIEWJS_LOCK_FILE: daemonLockFilePath, PREVIEWJS_LOG_FILE: logsPath, - PREVIEWJS_PORT: port, + PREVIEWJS_PORT: port.toString(10), + PREVIEWJS_PARENT_PROCESS_PID: process.pid.toString(10), }, }; - let daemonProcess: ExecaChildProcess; - if (useWsl) { - daemonProcess = execa( - "wsl", - wslCommandArgs(nodeDaemonCommand, true), - daemonOptions - ); - } else { - const [command, commandArgs] = - wrapCommandWithShellIfRequired(nodeDaemonCommand); - daemonProcess = execa(command, commandArgs, { - ...daemonOptions, - detached: true, - }); - } - daemonProcess.unref(); + const [daemonCommand, daemonCommandArgs] = + wrapCommandWithShellIfRequired(nodeDaemonCommand); + const daemonProcess = execa(daemonCommand, daemonCommandArgs, daemonOptions); daemonProcess.on("error", (error) => { outputChannel.append(`${error}`); }); return { daemonProcess }; } -function streamDaemonLogs(outputChannel: OutputChannel): Promise { +function streamDaemonLogs( + logsPath: string, + outputChannel: OutputChannel +): Promise { const ready = new Promise((resolve) => { let lastKnownLogsLength = 0; let resolved = false; @@ -270,7 +237,3 @@ function wrapCommandWithShellIfRequired(command: string) { ]; return [segments[0]!, segments.slice(1)] as const; } - -function wslCommandArgs(command: string, longRunning = false) { - return ["bash", "-lic", longRunning ? `${command} &` : command]; -} diff --git a/integrations/vscode/src/state.ts b/integrations/vscode/src/state.ts index bf46af4911d..411cc6709b9 100644 --- a/integrations/vscode/src/state.ts +++ b/integrations/vscode/src/state.ts @@ -3,7 +3,7 @@ import vscode from "vscode"; import type { FileAnalyzer } from "./file-analyzer.js"; import { createFileAnalyzer } from "./file-analyzer.js"; import { closePreviewPanel } from "./preview-panel.js"; -import { ensureDaemonRunning } from "./start-daemon.js"; +import { startDaemon } from "./start-daemon.js"; const PING_INTERVAL_MILLIS = 1000; @@ -16,7 +16,7 @@ export async function createState({ runningServerStatusBarItem: vscode.StatusBarItem; onDispose: () => void; }): Promise { - const daemon = await ensureDaemonRunning(outputChannel) + const daemon = await startDaemon(outputChannel) .catch((e) => { outputChannel.appendLine(e.stack); return null; @@ -66,6 +66,7 @@ export async function createState({ state.previewPanel.dispose(); state.previewPanel = null; } + daemon.daemonProcess.kill("SIGKILL"); }, pendingFileChanges, crawlFile, diff --git a/loader/src/runner.ts b/loader/src/runner.ts index 91374a1a77a..c1d3b2dbc01 100644 --- a/loader/src/runner.ts +++ b/loader/src/runner.ts @@ -1,8 +1,8 @@ import type { Workspace } from "@previewjs/core"; import type { ReaderListener } from "@previewjs/vfs"; import { assertNever } from "assert-never"; +import { execaNode } from "execa"; import fs from "fs-extra"; -import { fork } from "node:child_process"; import path from "path"; import type { Logger } from "pino"; import pino from "pino"; @@ -113,6 +113,7 @@ export async function load({ core, logger: globalLogger, async updateFileInMemory(absoluteFilePath: string, text: string | null) { + absoluteFilePath = core.toVitePath(absoluteFilePath); memoryReader.updateFile(absoluteFilePath, text); await Promise.all( Object.values(serverWorkers).map((worker) => @@ -342,11 +343,14 @@ export async function load({ return existingWorker; } } - const workerProcess = fork(workerFilePath, { + const workerProcess = execaNode(workerFilePath, { // Note: this is required for PostCSS. cwd: rootDir, - stdio: "inherit", + env: { + PREVIEWJS_PARENT_PROCESS_PID: process.pid.toString(10), + }, }); + let workerPromise: ResolvablePromise | null = null; const killWorker = async () => { if (workerPromise?.resolved) { workerPromise.resolved.exiting = true; @@ -457,7 +461,7 @@ export async function load({ }); return promise; }; - const workerPromise = (serverWorkers[rootDir] = resolvablePromise( + workerPromise = serverWorkers[rootDir] = resolvablePromise( workerReadyPromise.then( ({ port }): ServerWorker => ({ port, @@ -467,7 +471,7 @@ export async function load({ onStop: onStopListeners, }) ) - )); + ); return workerPromise; } } diff --git a/loader/src/worker.ts b/loader/src/worker.ts index 4a04a1a4248..578e4401c8c 100644 --- a/loader/src/worker.ts +++ b/loader/src/worker.ts @@ -141,3 +141,17 @@ const waitForInit = (message: ToWorkerMessage) => { } }; process.on("message", waitForInit); + +const parentProcessId = parseInt(process.env["PREVIEWJS_PARENT_PROCESS_PID"] || "0"); +if (!parentProcessId) { + throw new Error("Missing environment variable: PREVIEWJS_PARENT_PROCESS_PID") +} +// Kill the worker if the parent process dies. +setInterval(() => { + try { + process.kill(parentProcessId, 0); + // Parent process is still alive, see https://stackoverflow.com/a/21296291. + } catch(e) { + process.exit(0); + } +}, 1000) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 64011566054..94ac72f38cc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -416,12 +416,6 @@ importers: exclusive-promises: specifier: ^1.0.3 version: 1.0.3 - exit-hook: - specifier: ^3.2.0 - version: 3.2.0 - is-wsl: - specifier: ^3.1.0 - version: 3.1.0 rimraf: specifier: ^5.0.5 version: 5.0.5 @@ -3085,6 +3079,9 @@ importers: execa: specifier: ^8.0.1 version: 8.0.1 + get-port: + specifier: ^7.0.0 + version: 7.0.0 ovsx: specifier: ^0.8.3 version: 0.8.3 @@ -3865,6 +3862,60 @@ packages: '@babel/helper-split-export-declaration': 7.22.6 semver: 6.3.1 + /@babel/helper-create-class-features-plugin@7.22.5(@babel/core@7.21.4): + resolution: {integrity: sha512-xkb58MyOYIslxu3gKmVXmjTtUPvBU4odYzbiIQbWwLKIHCsx6UGZGX6F1IznMFVnDdirseUZopzN+ZRt8Xb33Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.21.4 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-member-expression-to-functions': 7.23.0 + '@babel/helper-optimise-call-expression': 7.22.5 + '@babel/helper-replace-supers': 7.22.9(@babel/core@7.21.4) + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + semver: 6.3.1 + dev: true + + /@babel/helper-create-class-features-plugin@7.22.5(@babel/core@7.21.8): + resolution: {integrity: sha512-xkb58MyOYIslxu3gKmVXmjTtUPvBU4odYzbiIQbWwLKIHCsx6UGZGX6F1IznMFVnDdirseUZopzN+ZRt8Xb33Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-member-expression-to-functions': 7.23.0 + '@babel/helper-optimise-call-expression': 7.22.5 + '@babel/helper-replace-supers': 7.22.9(@babel/core@7.21.8) + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + semver: 6.3.1 + dev: false + + /@babel/helper-create-class-features-plugin@7.22.5(@babel/core@7.22.20): + resolution: {integrity: sha512-xkb58MyOYIslxu3gKmVXmjTtUPvBU4odYzbiIQbWwLKIHCsx6UGZGX6F1IznMFVnDdirseUZopzN+ZRt8Xb33Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.22.20 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-member-expression-to-functions': 7.23.0 + '@babel/helper-optimise-call-expression': 7.22.5 + '@babel/helper-replace-supers': 7.22.9(@babel/core@7.22.20) + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + semver: 6.3.1 + dev: true + /@babel/helper-create-class-features-plugin@7.22.5(@babel/core@7.23.3): resolution: {integrity: sha512-xkb58MyOYIslxu3gKmVXmjTtUPvBU4odYzbiIQbWwLKIHCsx6UGZGX6F1IznMFVnDdirseUZopzN+ZRt8Xb33Q==} engines: {node: '>=6.9.0'} @@ -4666,11 +4717,13 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.21.4 - '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.21.4) + '@babel/helper-create-class-features-plugin': 7.22.5(@babel/core@7.21.4) '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-replace-supers': 7.22.9(@babel/core@7.21.4) - '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-replace-supers': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.5 '@babel/plugin-syntax-decorators': 7.22.5(@babel/core@7.21.4) + transitivePeerDependencies: + - supports-color dev: true /@babel/plugin-proposal-decorators@7.22.5(@babel/core@7.21.8): @@ -4680,11 +4733,13 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.21.8 - '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.21.8) + '@babel/helper-create-class-features-plugin': 7.22.5(@babel/core@7.21.8) '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-replace-supers': 7.22.9(@babel/core@7.21.8) - '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-replace-supers': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.5 '@babel/plugin-syntax-decorators': 7.22.5(@babel/core@7.21.8) + transitivePeerDependencies: + - supports-color dev: false /@babel/plugin-proposal-decorators@7.22.5(@babel/core@7.22.20): @@ -4694,11 +4749,13 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.22.20 - '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.22.20) + '@babel/helper-create-class-features-plugin': 7.22.5(@babel/core@7.22.20) '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-replace-supers': 7.22.9(@babel/core@7.22.20) - '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-replace-supers': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.5 '@babel/plugin-syntax-decorators': 7.22.5(@babel/core@7.22.20) + transitivePeerDependencies: + - supports-color dev: true /@babel/plugin-proposal-decorators@7.22.5(@babel/core@7.23.3): @@ -27636,11 +27693,6 @@ packages: strip-final-newline: 3.0.0 dev: true - /exit-hook@3.2.0: - resolution: {integrity: sha512-aIQN7Q04HGAV/I5BszisuHTZHXNoC23WtLkxdCLuYZMdWviRD0TMIt2bnUBi9MrHaF/hH8b3gwG9iaAUHKnJGA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true - /exit@0.1.2: resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} engines: {node: '>= 0.8.0'} @@ -28952,7 +29004,6 @@ packages: /get-port@7.0.0: resolution: {integrity: sha512-mDHFgApoQd+azgMdwylJrv2DX47ywGq1i5VFJE7fZ0dttNq3iQMfsU4IvEgBHojA3KqEudyu7Vq+oN8kNaNkWw==} engines: {node: '>=16'} - dev: false /get-stdin@4.0.1: resolution: {integrity: sha512-F5aQMywwJ2n85s4hJPTT9RPxGmubonuB10MNYo17/xph174n2MIR33HRguhzVag10O/npM7SPk73LMZNP+FaWw==} @@ -30879,13 +30930,6 @@ packages: dependencies: is-docker: 2.2.1 - /is-wsl@3.1.0: - resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} - engines: {node: '>=16'} - dependencies: - is-inside-container: 1.0.0 - dev: true - /is-yarn-global@0.3.0: resolution: {integrity: sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==} dev: true