{"version":3,"file":"port-ownership.mjs","names":[],"sources":["../../src/infra/port-ownership.ts"],"sourcesContent":["import { execFile } from \"node:child_process\";\nimport { Effect } from \"effect\";\n\nexport interface PortOwner {\n  pid: number;\n  command: string;\n}\n\nexport interface PortOwnershipOptions {\n  lsofPath?: string;\n  psPath?: string;\n}\n\nlet lsofCached: boolean | null = null;\n\nconst execFileText = (path: string, args: string[]): Promise<string> =>\n  new Promise((resolve, reject) => {\n    execFile(path, args, { timeout: 5000 }, (error, stdout) => {\n      if (error && !stdout) reject(error);\n      else resolve(stdout);\n    });\n  });\n\nconst isValidPort = (port: number): boolean => Number.isInteger(port) && port > 0 && port <= 65535;\n\n// ownerOfPort identifies the process listening on a port via lsof + ps.\n// Returns null when the port is free or when lsof is unavailable (degraded:\n// callers report \"unknown owner\" rather than failing).\nexport const ownerOfPort = (\n  port: number,\n  options?: PortOwnershipOptions,\n): Effect.Effect<PortOwner | null> =>\n  Effect.callback((resume) => {\n    void (async () => {\n      if (!isValidPort(port)) {\n        resume(Effect.succeed(null));\n        return;\n      }\n      const lsof = options?.lsofPath ?? \"lsof\";\n      try {\n        const stdout = await execFileText(lsof, [\"-ti\", `:${port}`, \"-sTCP:LISTEN\"]);\n        const pid = Number.parseInt(stdout.trim().split(\"\\n\")[0] ?? \"\", 10);\n        if (!Number.isInteger(pid) || pid <= 0) {\n          resume(Effect.succeed(null));\n          return;\n        }\n        try {\n          const psOut = await execFileText(options?.psPath ?? \"ps\", [\n            \"-o\",\n            \"command=\",\n            \"-p\",\n            String(pid),\n          ]);\n          resume(Effect.succeed({ pid, command: psOut.trim() || \"unknown\" }));\n        } catch {\n          resume(Effect.succeed({ pid, command: \"unknown\" }));\n        }\n      } catch (error) {\n        if ((error as NodeJS.ErrnoException).code === \"ENOENT\") lsofCached = false;\n        resume(Effect.succeed(null));\n      }\n    })();\n  });\n\n// isLsofAvailable probes (once per process) whether the lsof binary exists —\n// ownership reporting degrades gracefully when it does not.\nexport const isLsofAvailable = (options?: PortOwnershipOptions): Effect.Effect<boolean> =>\n  Effect.callback((resume) => {\n    if (lsofCached !== null) {\n      resume(Effect.succeed(lsofCached));\n      return;\n    }\n    void (async () => {\n      try {\n        await execFileText(options?.lsofPath ?? \"lsof\", [\"-v\"]);\n        lsofCached = true;\n      } catch (error) {\n        if ((error as NodeJS.ErrnoException).code === \"ENOENT\") lsofCached = false;\n        else lsofCached = true;\n      }\n      resume(Effect.succeed(lsofCached));\n    })();\n  });\n\nexport const resetLsofCacheForTests = (): void => {\n  lsofCached = null;\n};\n"],"mappings":";;;;AAaA,IAAI,aAA6B;AAEjC,MAAM,gBAAgB,MAAc,SAClC,IAAI,SAAS,SAAS,WAAW;CAC/B,SAAS,MAAM,MAAM,EAAE,SAAS,IAAK,IAAI,OAAO,WAAW;EACzD,IAAI,SAAS,CAAC,QAAQ,OAAO,KAAK;OAC7B,QAAQ,MAAM;CACrB,CAAC;AACH,CAAC;AAEH,MAAM,eAAe,SAA0B,OAAO,UAAU,IAAI,KAAK,OAAO,KAAK,QAAQ;AAK7F,MAAa,eACX,MACA,YAEA,OAAO,UAAU,WAAW;CAC1B,CAAM,YAAY;EAChB,IAAI,CAAC,YAAY,IAAI,GAAG;GACtB,OAAO,OAAO,QAAQ,IAAI,CAAC;GAC3B;EACF;EACA,MAAM,OAAO,SAAS,YAAY;EAClC,IAAI;GACF,MAAM,SAAS,MAAM,aAAa,MAAM;IAAC;IAAO,IAAI;IAAQ;GAAc,CAAC;GAC3E,MAAM,MAAM,OAAO,SAAS,OAAO,KAAK,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,MAAM,IAAI,EAAE;GAClE,IAAI,CAAC,OAAO,UAAU,GAAG,KAAK,OAAO,GAAG;IACtC,OAAO,OAAO,QAAQ,IAAI,CAAC;IAC3B;GACF;GACA,IAAI;IACF,MAAM,QAAQ,MAAM,aAAa,SAAS,UAAU,MAAM;KACxD;KACA;KACA;KACA,OAAO,GAAG;IACZ,CAAC;IACD,OAAO,OAAO,QAAQ;KAAE;KAAK,SAAS,MAAM,KAAK,KAAK;IAAU,CAAC,CAAC;GACpE,QAAQ;IACN,OAAO,OAAO,QAAQ;KAAE;KAAK,SAAS;IAAU,CAAC,CAAC;GACpD;EACF,SAAS,OAAO;GACd,IAAK,MAAgC,SAAS,UAAU,aAAa;GACrE,OAAO,OAAO,QAAQ,IAAI,CAAC;EAC7B;CACF,EAAC,CAAE;AACL,CAAC"}