watcher.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. import { BusEvent } from "@/bus/bus-event"
  2. import { Bus } from "@/bus"
  3. import z from "zod"
  4. import { Instance } from "../project/instance"
  5. import { Log } from "../util/log"
  6. import { FileIgnore } from "./ignore"
  7. import { Config } from "../config/config"
  8. import path from "path"
  9. // @ts-ignore
  10. import { createWrapper } from "@parcel/watcher/wrapper"
  11. import { lazy } from "@/util/lazy"
  12. import { withTimeout } from "@/util/timeout"
  13. import type ParcelWatcher from "@parcel/watcher"
  14. import { $ } from "bun"
  15. import { Flag } from "@/flag/flag"
  16. import { readdir } from "fs/promises"
  17. const SUBSCRIBE_TIMEOUT_MS = 10_000
  18. declare const OPENCODE_LIBC: string | undefined
  19. export namespace FileWatcher {
  20. const log = Log.create({ service: "file.watcher" })
  21. export const Event = {
  22. Updated: BusEvent.define(
  23. "file.watcher.updated",
  24. z.object({
  25. file: z.string(),
  26. event: z.union([z.literal("add"), z.literal("change"), z.literal("unlink")]),
  27. }),
  28. ),
  29. }
  30. const watcher = lazy((): typeof import("@parcel/watcher") | undefined => {
  31. try {
  32. const binding = require(
  33. `@parcel/watcher-${process.platform}-${process.arch}${process.platform === "linux" ? `-${OPENCODE_LIBC || "glibc"}` : ""}`,
  34. )
  35. return createWrapper(binding) as typeof import("@parcel/watcher")
  36. } catch (error) {
  37. log.error("failed to load watcher binding", { error })
  38. return
  39. }
  40. })
  41. const state = Instance.state(
  42. async () => {
  43. log.info("init")
  44. const cfg = await Config.get()
  45. const backend = (() => {
  46. if (process.platform === "win32") return "windows"
  47. if (process.platform === "darwin") return "fs-events"
  48. if (process.platform === "linux") return "inotify"
  49. })()
  50. if (!backend) {
  51. log.error("watcher backend not supported", { platform: process.platform })
  52. return {}
  53. }
  54. log.info("watcher backend", { platform: process.platform, backend })
  55. const w = watcher()
  56. if (!w) return {}
  57. const subscribe: ParcelWatcher.SubscribeCallback = (err, evts) => {
  58. if (err) return
  59. for (const evt of evts) {
  60. if (evt.type === "create") Bus.publish(Event.Updated, { file: evt.path, event: "add" })
  61. if (evt.type === "update") Bus.publish(Event.Updated, { file: evt.path, event: "change" })
  62. if (evt.type === "delete") Bus.publish(Event.Updated, { file: evt.path, event: "unlink" })
  63. }
  64. }
  65. const subs: ParcelWatcher.AsyncSubscription[] = []
  66. const cfgIgnores = cfg.watcher?.ignore ?? []
  67. if (Flag.OPENCODE_EXPERIMENTAL_FILEWATCHER) {
  68. const pending = w.subscribe(Instance.directory, subscribe, {
  69. ignore: [...FileIgnore.PATTERNS, ...cfgIgnores],
  70. backend,
  71. })
  72. const sub = await withTimeout(pending, SUBSCRIBE_TIMEOUT_MS).catch((err) => {
  73. log.error("failed to subscribe to Instance.directory", { error: err })
  74. pending.then((s) => s.unsubscribe()).catch(() => {})
  75. return undefined
  76. })
  77. if (sub) subs.push(sub)
  78. }
  79. if (Instance.project.vcs === "git") {
  80. const vcsDir = await $`git rev-parse --git-dir`
  81. .quiet()
  82. .nothrow()
  83. .cwd(Instance.worktree)
  84. .text()
  85. .then((x) => path.resolve(Instance.worktree, x.trim()))
  86. .catch(() => undefined)
  87. if (vcsDir && !cfgIgnores.includes(".git") && !cfgIgnores.includes(vcsDir)) {
  88. const gitDirContents = await readdir(vcsDir).catch(() => [])
  89. const ignoreList = gitDirContents.filter((entry) => entry !== "HEAD")
  90. const pending = w.subscribe(vcsDir, subscribe, {
  91. ignore: ignoreList,
  92. backend,
  93. })
  94. const sub = await withTimeout(pending, SUBSCRIBE_TIMEOUT_MS).catch((err) => {
  95. log.error("failed to subscribe to vcsDir", { error: err })
  96. pending.then((s) => s.unsubscribe()).catch(() => {})
  97. return undefined
  98. })
  99. if (sub) subs.push(sub)
  100. }
  101. }
  102. return { subs }
  103. },
  104. async (state) => {
  105. if (!state.subs) return
  106. await Promise.all(state.subs.map((sub) => sub?.unsubscribe()))
  107. },
  108. )
  109. export function init() {
  110. if (Flag.OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER) {
  111. return
  112. }
  113. state()
  114. }
  115. }