diff --git a/packages/cli-v3/src/commands/dev.ts b/packages/cli-v3/src/commands/dev.ts index b6c652d786..8634ad422d 100644 --- a/packages/cli-v3/src/commands/dev.ts +++ b/packages/cli-v3/src/commands/dev.ts @@ -11,6 +11,7 @@ import { runtimeChecks } from "../utilities/runtimeCheck.js"; import { getProjectClient, LoginResultOk } from "../utilities/session.js"; import { login } from "./login.js"; import { updateTriggerPackages } from "./update.js"; +import { createLockFile } from "../dev/lock.js"; const DevCommandOptions = CommonCommandOptions.extend({ debugOtel: z.boolean().default(false), @@ -120,6 +121,8 @@ async function startDev(options: StartDevOptions) { displayedUpdateMessage = await updateTriggerPackages(options.cwd, { ...options }, true, true); } + const removeLockFile = await createLockFile(options.cwd); + let devInstance: DevSessionInstance | undefined; printDevBanner(displayedUpdateMessage); @@ -178,6 +181,7 @@ async function startDev(options: StartDevOptions) { stop: async () => { devInstance?.stop(); await watcher?.stop(); + removeLockFile(); }, waitUntilExit, }; diff --git a/packages/cli-v3/src/dev/lock.ts b/packages/cli-v3/src/dev/lock.ts new file mode 100644 index 0000000000..602607a4c0 --- /dev/null +++ b/packages/cli-v3/src/dev/lock.ts @@ -0,0 +1,93 @@ +import path from "node:path"; +import { readFile } from "../utilities/fileSystem.js"; +import { tryCatch } from "@trigger.dev/core/utils"; +import { logger } from "../utilities/logger.js"; +import { mkdir, writeFile } from "node:fs/promises"; +import { existsSync, unlinkSync } from "node:fs"; +import { onExit } from "signal-exit"; + +const LOCK_FILE_NAME = "dev.lock"; + +export async function createLockFile(cwd: string) { + const currentPid = process.pid; + const lockFilePath = path.join(cwd, ".trigger", LOCK_FILE_NAME); + + logger.debug("Checking for lockfile", { lockFilePath, currentPid }); + + const removeLockFile = () => { + try { + logger.debug("Removing lockfile", { lockFilePath }); + return unlinkSync(lockFilePath); + } catch (e) { + // This sometimes fails on Windows with EBUSY + } + }; + const removeExitListener = onExit(removeLockFile); + + const [, existingLockfileContents] = await tryCatch(readFile(lockFilePath)); + + if (existingLockfileContents) { + // Read the pid number from the lockfile + const existingPid = Number(existingLockfileContents); + + logger.debug("Lockfile exists", { lockFilePath, existingPid, currentPid }); + + if (existingPid === currentPid) { + logger.debug("Lockfile exists and is owned by current process", { + lockFilePath, + existingPid, + currentPid, + }); + + return () => { + removeExitListener(); + removeLockFile(); + }; + } + + // If the pid is different, try and kill the existing pid + logger.debug("Lockfile exists and is owned by another process, killing it", { + lockFilePath, + existingPid, + currentPid, + }); + + try { + process.kill(existingPid); + // If it did kill the process, it will have exited, deleting the lockfile, so let's wait for that to happen + // But let's not wait forever + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + clearInterval(interval); + reject(new Error("Timed out waiting for lockfile to be deleted")); + }, 5000); + + const interval = setInterval(() => { + if (!existsSync(lockFilePath)) { + clearInterval(interval); + clearTimeout(timeout); + resolve(true); + } + }, 100); + }); + } catch (error) { + logger.debug("Failed to kill existing process, lets assume it's not running", { error }); + } + } + + // Now write the current pid to the lockfile + await writeFileAndEnsureDirExists(lockFilePath, currentPid.toString()); + + logger.debug("Lockfile created", { lockFilePath, currentPid }); + + return () => { + removeExitListener(); + removeLockFile(); + }; +} + +async function writeFileAndEnsureDirExists(filePath: string, data: string) { + const dir = path.dirname(filePath); + await mkdir(dir, { recursive: true }); + await writeFile(filePath, data); +}