watcher.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. export * as Watcher from "./watcher"
  2. // @ts-ignore
  3. import { createWrapper } from "@parcel/watcher/wrapper"
  4. import type ParcelWatcher from "@parcel/watcher"
  5. import { Cause, Context, Effect, Layer, Schema } from "effect"
  6. import path from "path"
  7. import { Config } from "../config"
  8. import { EventV2 } from "../event"
  9. import { Flag } from "../flag/flag"
  10. import { FSUtil } from "../fs-util"
  11. import { Git } from "../git"
  12. import { Location } from "../location"
  13. import { lazy } from "../util/lazy"
  14. import * as Log from "../util/log"
  15. import { Ignore } from "./ignore"
  16. import { Protected } from "./protected"
  17. declare const OPENCODE_LIBC: string | undefined
  18. const log = Log.create({ service: "file.watcher" })
  19. const SUBSCRIBE_TIMEOUT_MS = 10_000
  20. export const Event = {
  21. Updated: EventV2.define({
  22. type: "file.watcher.updated",
  23. schema: {
  24. file: Schema.String,
  25. event: Schema.Literals(["add", "change", "unlink"]),
  26. },
  27. }),
  28. }
  29. const watcher = lazy((): typeof import("@parcel/watcher") | undefined => {
  30. try {
  31. const libc = typeof OPENCODE_LIBC === "undefined" ? undefined : OPENCODE_LIBC
  32. const binding = require(
  33. `@parcel/watcher-${process.platform}-${process.arch}${process.platform === "linux" ? `-${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. function getBackend() {
  42. if (process.platform === "win32") return "windows"
  43. if (process.platform === "darwin") return "fs-events"
  44. if (process.platform === "linux") return "inotify"
  45. }
  46. function protecteds(dir: string) {
  47. return Protected.paths().filter((item) => {
  48. const relative = path.relative(dir, item)
  49. return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative)
  50. })
  51. }
  52. export const hasNativeBinding = () => !!watcher()
  53. export interface Interface {}
  54. export class Service extends Context.Service<Service, Interface>()("@opencode/v2/FileWatcher") {}
  55. export const layer = Layer.effect(
  56. Service,
  57. Effect.gen(function* () {
  58. if (yield* Flag.OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER) return Service.of({})
  59. const backend = getBackend()
  60. const location = yield* Location.Service
  61. if (!backend) {
  62. log.error("watcher backend not supported", { directory: location.directory, platform: process.platform })
  63. return Service.of({})
  64. }
  65. const w = watcher()
  66. if (!w) return Service.of({})
  67. log.info("watcher backend", { directory: location.directory, platform: process.platform, backend })
  68. const events = yield* EventV2.Service
  69. const fs = yield* FSUtil.Service
  70. const git = yield* Git.Service
  71. const context = yield* Effect.context()
  72. const runFork = Effect.runForkWith(context)
  73. const subscriptions: ParcelWatcher.AsyncSubscription[] = []
  74. yield* Effect.addFinalizer(() =>
  75. Effect.promise(() => Promise.allSettled(subscriptions.map((subscription) => subscription.unsubscribe()))),
  76. )
  77. const callback: ParcelWatcher.SubscribeCallback = (_error, updates) => {
  78. for (const update of updates) {
  79. if (update.type === "create") runFork(events.publish(Event.Updated, { file: update.path, event: "add" }))
  80. if (update.type === "update") runFork(events.publish(Event.Updated, { file: update.path, event: "change" }))
  81. if (update.type === "delete") runFork(events.publish(Event.Updated, { file: update.path, event: "unlink" }))
  82. }
  83. }
  84. const subscribe = (directory: string, ignore: string[]) => {
  85. const pending = w.subscribe(directory, callback, { ignore, backend })
  86. return Effect.promise(() => pending).pipe(
  87. Effect.tap((subscription) => Effect.sync(() => subscriptions.push(subscription))),
  88. Effect.timeout(SUBSCRIBE_TIMEOUT_MS),
  89. Effect.catchCause((cause) => {
  90. log.error("failed to subscribe", { directory, cause: Cause.pretty(cause) })
  91. pending.then((subscription) => subscription.unsubscribe()).catch(() => {})
  92. return Effect.void
  93. }),
  94. )
  95. }
  96. const config = (yield* (yield* Config.Service).get()).flatMap((item) => item.info.watcher?.ignore ?? [])
  97. if (yield* Flag.OPENCODE_EXPERIMENTAL_FILEWATCHER) {
  98. yield* Effect.forkScoped(
  99. subscribe(location.directory, [...Ignore.PATTERNS, ...config, ...protecteds(location.directory)]),
  100. )
  101. }
  102. if (location.vcs?.type === "git") {
  103. const resolved = yield* git.dir(location.directory)
  104. const vcs = resolved ? yield* fs.realPath(resolved).pipe(Effect.catch(() => Effect.succeed(resolved))) : undefined
  105. if (vcs && !config.includes(".git") && !config.includes(vcs) && (!resolved || !config.includes(resolved))) {
  106. const ignore = (yield* fs.readDirectoryEntries(vcs).pipe(Effect.catch(() => Effect.succeed([])))).flatMap(
  107. (entry) => (entry.name === "HEAD" ? [] : [entry.name]),
  108. )
  109. yield* Effect.forkScoped(subscribe(vcs, ignore))
  110. }
  111. }
  112. return Service.of({})
  113. }).pipe(
  114. Effect.catchCause((cause) => {
  115. log.error("failed to init watcher service", { cause: Cause.pretty(cause) })
  116. return Effect.succeed(Service.of({}))
  117. }),
  118. ),
  119. )
  120. export const locationLayer = layer.pipe(Layer.provide(Config.locationLayer), Layer.provide(Git.defaultLayer))