ttsc 0.31.0 → 0.31.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/cmd/platform/main.go +2 -2
  2. package/internal/sourceprocess/contain_windows.go +10 -13
  3. package/internal/sourceprocess/started_windows.go +41 -0
  4. package/internal/sourceprocess/windows_started_publication_distinguishes_pending_from_refusal_test.go +76 -0
  5. package/internal/watchhelper/run.go +30 -20
  6. package/internal/watchhelper/run_other.go +4 -4
  7. package/internal/watchhelper/run_windows.go +471 -0
  8. package/internal/watchhelper/watchhelper.go +8 -2
  9. package/internal/watchhelper/windows_notifications_preserve_mutations_without_access_noise_test.go +506 -0
  10. package/lib/compiler/internal/build/BuildExecution.d.ts +4 -2
  11. package/lib/compiler/internal/build/BuildExecution.js +40 -12
  12. package/lib/compiler/internal/build/BuildExecution.js.map +1 -1
  13. package/lib/compiler/internal/startResidentTransform.js +5 -4
  14. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  15. package/lib/compiler/internal/transformProjectInMemory.js +9 -7
  16. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  17. package/lib/internal/javascriptRuntimeCapabilities.d.ts +13 -4
  18. package/lib/internal/javascriptRuntimeCapabilities.js +56 -6
  19. package/lib/internal/javascriptRuntimeCapabilities.js.map +1 -1
  20. package/lib/internal/nativeBinary.d.ts +3 -3
  21. package/lib/internal/nativeBinary.js +3 -3
  22. package/lib/internal/pluginSource.d.ts +1 -0
  23. package/lib/internal/pluginSource.js +1 -0
  24. package/lib/internal/pluginSource.js.map +1 -1
  25. package/lib/internal/resolveNodeBinary.d.ts +6 -1
  26. package/lib/internal/resolveNodeBinary.js +6 -2
  27. package/lib/internal/resolveNodeBinary.js.map +1 -1
  28. package/lib/internal/runtimeExecutableIdentity.d.ts +22 -13
  29. package/lib/internal/runtimeExecutableIdentity.js +192 -102
  30. package/lib/internal/runtimeExecutableIdentity.js.map +1 -1
  31. package/lib/launcher/internal/runtime/installRuntimeHooks.js +58 -4
  32. package/lib/launcher/internal/runtime/installRuntimeHooks.js.map +1 -1
  33. package/lib/launcher/internal/ttscserver/captureLSPPluginSelectionInputs.d.ts +5 -1
  34. package/lib/launcher/internal/ttscserver/captureLSPPluginSelectionInputs.js +7 -2
  35. package/lib/launcher/internal/ttscserver/captureLSPPluginSelectionInputs.js.map +1 -1
  36. package/lib/launcher/internal/ttscserver/runTtscserver.js +8 -1
  37. package/lib/launcher/internal/ttscserver/runTtscserver.js.map +1 -1
  38. package/lib/plugin/internal/CapabilityResolutionFormat.d.ts +21 -62
  39. package/lib/plugin/internal/CapabilityResolutionFormat.js +15 -106
  40. package/lib/plugin/internal/CapabilityResolutionFormat.js.map +1 -1
  41. package/lib/plugin/internal/load/PluginDescriptorEvaluationCache.d.ts +6 -0
  42. package/lib/plugin/internal/load/PluginDescriptorEvaluationCache.js +4 -4
  43. package/lib/plugin/internal/load/PluginDescriptorEvaluationCache.js.map +1 -1
  44. package/lib/plugin/internal/load/loadProjectPlugins.d.ts +24 -14
  45. package/lib/plugin/internal/load/loadProjectPlugins.js +141 -56
  46. package/lib/plugin/internal/load/loadProjectPlugins.js.map +1 -1
  47. package/lib/plugin/internal/readCapabilityResolution.d.ts +6 -0
  48. package/lib/plugin/internal/readCapabilityResolution.js +8 -5
  49. package/lib/plugin/internal/readCapabilityResolution.js.map +1 -1
  50. package/lib/plugin/internal/source/GoEnvironmentReading.d.ts +48 -0
  51. package/lib/plugin/internal/source/GoEnvironmentReading.js +156 -0
  52. package/lib/plugin/internal/source/GoEnvironmentReading.js.map +1 -0
  53. package/lib/plugin/internal/source/NativeSourcePackages.d.ts +51 -13
  54. package/lib/plugin/internal/source/NativeSourcePackages.js +182 -27
  55. package/lib/plugin/internal/source/NativeSourcePackages.js.map +1 -1
  56. package/lib/plugin/internal/source/PluginBuildEnvironmentReadings.d.ts +42 -3
  57. package/lib/plugin/internal/source/PluginBuildEnvironmentReadings.js +68 -7
  58. package/lib/plugin/internal/source/PluginBuildEnvironmentReadings.js.map +1 -1
  59. package/lib/plugin/internal/source/PluginBuildEnvironmentWitness.d.ts +5 -3
  60. package/lib/plugin/internal/source/PluginBuildEnvironmentWitness.js +14 -8
  61. package/lib/plugin/internal/source/PluginBuildEnvironmentWitness.js.map +1 -1
  62. package/lib/plugin/internal/source/PluginContentIdentities.d.ts +265 -0
  63. package/lib/plugin/internal/source/PluginContentIdentities.js +452 -0
  64. package/lib/plugin/internal/source/PluginContentIdentities.js.map +1 -0
  65. package/lib/plugin/internal/source/PluginLoadAnswers.d.ts +63 -0
  66. package/lib/plugin/internal/source/PluginLoadAnswers.js +128 -0
  67. package/lib/plugin/internal/source/PluginLoadAnswers.js.map +1 -0
  68. package/lib/plugin/internal/source/SourceBuildCacheLayout.d.ts +10 -0
  69. package/lib/plugin/internal/source/SourceBuildCacheLayout.js +12 -0
  70. package/lib/plugin/internal/source/SourceBuildCacheLayout.js.map +1 -1
  71. package/lib/plugin/internal/source/SourcePluginWorkspace.d.ts +53 -19
  72. package/lib/plugin/internal/source/SourcePluginWorkspace.js +111 -22
  73. package/lib/plugin/internal/source/SourcePluginWorkspace.js.map +1 -1
  74. package/lib/plugin/internal/source/buildSourcePlugin.d.ts +34 -18
  75. package/lib/plugin/internal/source/buildSourcePlugin.js +52 -24
  76. package/lib/plugin/internal/source/buildSourcePlugin.js.map +1 -1
  77. package/lib/plugin/internal/source/computeCacheKey.d.ts +25 -7
  78. package/lib/plugin/internal/source/computeCacheKey.js +85 -34
  79. package/lib/plugin/internal/source/computeCacheKey.js.map +1 -1
  80. package/lib/plugin/internal/source/hashPluginBuildEnvironment.d.ts +7 -3
  81. package/lib/plugin/internal/source/hashPluginBuildEnvironment.js +75 -102
  82. package/lib/plugin/internal/source/hashPluginBuildEnvironment.js.map +1 -1
  83. package/lib/plugin/internal/source/pluginBuildEnvironment.d.ts +8 -3
  84. package/lib/plugin/internal/source/pluginBuildEnvironment.js +8 -4
  85. package/lib/plugin/internal/source/pluginBuildEnvironment.js.map +1 -1
  86. package/lib/plugin/internal/source/pluginBuildEnvironmentWorker.js +12 -2
  87. package/lib/plugin/internal/source/pluginBuildEnvironmentWorker.js.map +1 -1
  88. package/lib/plugin/internal/source/pluginBuildVersions.d.ts +2 -2
  89. package/lib/plugin/internal/source/pluginBuildVersions.js +4 -17
  90. package/lib/plugin/internal/source/pluginBuildVersions.js.map +1 -1
  91. package/lib/plugin/internal/source/pluginModuleReplaceDirectories.d.ts +6 -5
  92. package/lib/plugin/internal/source/pluginModuleReplaceDirectories.js +10 -17
  93. package/lib/plugin/internal/source/pluginModuleReplaceDirectories.js.map +1 -1
  94. package/lib/plugin/internal/source/pluginSourceState.d.ts +6 -2
  95. package/lib/plugin/internal/source/pluginSourceState.js +20 -8
  96. package/lib/plugin/internal/source/pluginSourceState.js.map +1 -1
  97. package/lib/plugin/internal/source/pluginSourceStateHolds.d.ts +7 -2
  98. package/lib/plugin/internal/source/pluginSourceStateHolds.js +12 -7
  99. package/lib/plugin/internal/source/pluginSourceStateHolds.js.map +1 -1
  100. package/lib/plugin/internal/source/processPluginBuildEnvironment.d.ts +7 -2
  101. package/lib/plugin/internal/source/processPluginBuildEnvironment.js +8 -3
  102. package/lib/plugin/internal/source/processPluginBuildEnvironment.js.map +1 -1
  103. package/lib/plugin/internal/source/provenPluginSourceDigest.d.ts +28 -0
  104. package/lib/plugin/internal/source/provenPluginSourceDigest.js +41 -0
  105. package/lib/plugin/internal/source/provenPluginSourceDigest.js.map +1 -0
  106. package/lib/plugin/internal/source/pruneCacheFileRoot.d.ts +3 -2
  107. package/lib/plugin/internal/source/pruneCacheFileRoot.js +3 -2
  108. package/lib/plugin/internal/source/pruneCacheFileRoot.js.map +1 -1
  109. package/lib/plugin/internal/source/ttscPackageVersion.d.ts +26 -0
  110. package/lib/plugin/internal/source/ttscPackageVersion.js +47 -0
  111. package/lib/plugin/internal/source/ttscPackageVersion.js.map +1 -0
  112. package/lib/plugin/internal/writeCapabilityResolution.d.ts +6 -0
  113. package/lib/plugin/internal/writeCapabilityResolution.js +11 -7
  114. package/lib/plugin/internal/writeCapabilityResolution.js.map +1 -1
  115. package/lib/plugin/resolveCapabilityPlugins.js +67 -15
  116. package/lib/plugin/resolveCapabilityPlugins.js.map +1 -1
  117. package/package.json +8 -8
  118. package/src/compiler/internal/build/BuildExecution.ts +46 -13
  119. package/src/compiler/internal/startResidentTransform.ts +12 -3
  120. package/src/compiler/internal/transformProjectInMemory.ts +10 -2
  121. package/src/internal/javascriptRuntimeCapabilities.ts +71 -5
  122. package/src/internal/nativeBinary.ts +3 -3
  123. package/src/internal/pluginSource.ts +1 -0
  124. package/src/internal/resolveNodeBinary.ts +12 -1
  125. package/src/internal/runtimeExecutableIdentity.ts +227 -141
  126. package/src/launcher/internal/runtime/installRuntimeHooks.ts +68 -3
  127. package/src/launcher/internal/ttscserver/captureLSPPluginSelectionInputs.ts +11 -2
  128. package/src/launcher/internal/ttscserver/runTtscserver.ts +18 -1
  129. package/src/plugin/internal/CapabilityResolutionFormat.ts +23 -107
  130. package/src/plugin/internal/load/PluginDescriptorEvaluationCache.ts +14 -4
  131. package/src/plugin/internal/load/loadProjectPlugins.ts +421 -211
  132. package/src/plugin/internal/readCapabilityResolution.ts +17 -6
  133. package/src/plugin/internal/source/GoEnvironmentReading.ts +178 -0
  134. package/src/plugin/internal/source/NativeSourcePackages.ts +365 -48
  135. package/src/plugin/internal/source/PluginBuildEnvironmentReadings.ts +83 -5
  136. package/src/plugin/internal/source/PluginBuildEnvironmentWitness.ts +18 -8
  137. package/src/plugin/internal/source/PluginContentIdentities.ts +563 -0
  138. package/src/plugin/internal/source/PluginLoadAnswers.ts +151 -0
  139. package/src/plugin/internal/source/SourceBuildCacheLayout.ts +14 -0
  140. package/src/plugin/internal/source/SourcePluginWorkspace.ts +148 -25
  141. package/src/plugin/internal/source/buildSourcePlugin.ts +104 -36
  142. package/src/plugin/internal/source/computeCacheKey.ts +129 -55
  143. package/src/plugin/internal/source/hashPluginBuildEnvironment.ts +152 -105
  144. package/src/plugin/internal/source/pluginBuildEnvironment.ts +9 -2
  145. package/src/plugin/internal/source/pluginBuildEnvironmentWorker.ts +18 -2
  146. package/src/plugin/internal/source/pluginBuildVersions.ts +5 -26
  147. package/src/plugin/internal/source/pluginModuleReplaceDirectories.ts +13 -25
  148. package/src/plugin/internal/source/pluginSourceState.ts +28 -12
  149. package/src/plugin/internal/source/pluginSourceStateHolds.ts +15 -8
  150. package/src/plugin/internal/source/processPluginBuildEnvironment.ts +8 -2
  151. package/src/plugin/internal/source/provenPluginSourceDigest.ts +45 -0
  152. package/src/plugin/internal/source/pruneCacheFileRoot.ts +3 -2
  153. package/src/plugin/internal/source/ttscPackageVersion.ts +45 -0
  154. package/src/plugin/internal/writeCapabilityResolution.ts +29 -8
  155. package/src/plugin/resolveCapabilityPlugins.ts +87 -13
@@ -0,0 +1,471 @@
1
+ //go:build windows
2
+
3
+ package watchhelper
4
+
5
+ import (
6
+ "bufio"
7
+ "encoding/binary"
8
+ "encoding/json"
9
+ "errors"
10
+ "fmt"
11
+ "io"
12
+ "runtime"
13
+ "sort"
14
+ "sync"
15
+ "unicode/utf16"
16
+
17
+ "golang.org/x/sys/windows"
18
+ )
19
+
20
+ // Windows compiler-input watching deliberately omits LAST_ACCESS. A read can
21
+ // update NTFS access time and libuv reports that as an indistinguishable change
22
+ // (#1719). NEVER filter a delivered change by equal hashes/metadata instead:
23
+ // that loses real A-B-A writes. Preserve every other libuv mutation filter.
24
+ const windowsWatchMask = windows.FILE_NOTIFY_CHANGE_FILE_NAME |
25
+ windows.FILE_NOTIFY_CHANGE_DIR_NAME | windows.FILE_NOTIFY_CHANGE_ATTRIBUTES |
26
+ windows.FILE_NOTIFY_CHANGE_SIZE | windows.FILE_NOTIFY_CHANGE_LAST_WRITE |
27
+ windows.FILE_NOTIFY_CHANGE_CREATION | windows.FILE_NOTIFY_CHANGE_SECURITY
28
+
29
+ // windowsBrokerRequest is the existing unplugin broker wire shape. Linux retains
30
+ // its original protocol. Registration readiness and drain replies carry independent
31
+ // IDs; the helper observes native mutations, never compiler/cache state.
32
+ type windowsBrokerRequest struct {
33
+ // Op selects add, remove, or drain.
34
+ Op string `json:"op"`
35
+
36
+ // ID names an independent registration or drain.
37
+ ID int64 `json:"id"`
38
+
39
+ // AllEvents admits content changes as well as structural notifications.
40
+ AllEvents bool `json:"allEvents"`
41
+
42
+ // Locations name canonical native directory subscriptions.
43
+ Locations []windowsWatchLocation `json:"locations"`
44
+ }
45
+
46
+ // windowsWatchLocation identifies an exact native directory and recursion policy.
47
+ // The parent supplies canonical spelling; no universal case folding is performed.
48
+ // A shallow subscriber cannot borrow recursive coverage, while independently
49
+ // spelled aliases may conservatively keep separate handles.
50
+ type windowsWatchLocation struct {
51
+ // Directory is the native absolute directory, already canonicalized by the caller.
52
+ Directory string `json:"directory"`
53
+
54
+ // Recursive requests notifications below descendants.
55
+ Recursive bool `json:"recursive"`
56
+ }
57
+
58
+ // windowsDirectoryWatch separates live subscriber ownership from pending kernel
59
+ // ownership. One exact directory/recursion key shares its native handle and 64 KiB
60
+ // buffer among subscribers with distinct event policies.
61
+ //
62
+ // NEVER release or reuse the overlapped record or buffer until its completion,
63
+ // including cancellation. Closing the handle or removing its last subscriber does
64
+ // not end that memory lifetime. The completion index keeps retiring watches rooted.
65
+ type windowsDirectoryWatch struct {
66
+ location windowsWatchLocation
67
+ handle windows.Handle
68
+ overlapped windows.Overlapped
69
+ buffer []byte
70
+ subscribers map[int64]bool
71
+ ordered []int64
72
+ pending bool
73
+ retiring bool
74
+ }
75
+
76
+ // windowsBroker owns one completion port and all subscription state on one loop.
77
+ // Location, completion and registration indexes serve acquisition, native memory
78
+ // ownership and independent subscriber removal respectively.
79
+ //
80
+ // Completion keys are never reused: a late cancellation must not address a newly
81
+ // opened watch. Active subscriptions have no numeric cap; removal and cancellation
82
+ // completion reclaim them. Native observation is shared, not per-owner readiness.
83
+ type windowsBroker struct {
84
+ port windows.Handle
85
+ nextKey uintptr
86
+ watches map[uintptr]*windowsDirectoryWatch
87
+ locations map[windowsWatchLocation]uintptr
88
+ registrations map[int64]map[uintptr]struct{}
89
+ out *bufio.Writer
90
+ encoder *json.Encoder
91
+ }
92
+
93
+ // run serves Windows watches without Node access-time notifications. A bounded
94
+ // stdin reader posts wakes; one OS-thread-locked loop owns handles and ordered
95
+ // output. Each request follows a drain to the currently observed empty completion
96
+ // port. This does not atomically classify concurrent kernel writes; no quiet delay
97
+ // can replace that native frontier.
98
+ //
99
+ // EOF retires all watches and waits for cancellation completions before releasing
100
+ // buffers and the port. Wake posting is synchronized with closure, and return
101
+ // cancels queued sends. A library caller still owns closing blocked stdin/output.
102
+ // Scanner framing is limited to 16 MiB and requests to 256 queued values; active
103
+ // subscriptions and output backpressure retain caller-dependent lifetimes.
104
+ func run(stdin io.Reader, stdout io.Writer, stderr io.Writer) int {
105
+ // IOCP associates the receiving OS thread with its concurrency budget.
106
+ // A migrating goroutine can leave an old thread active and block its next
107
+ // receive behind that same budget. Keep the one native owner on one thread.
108
+ runtime.LockOSThread()
109
+ defer runtime.UnlockOSThread()
110
+ port, err := windows.CreateIoCompletionPort(windows.InvalidHandle, 0, 0, 1)
111
+ if err != nil {
112
+ fmt.Fprintln(stderr, err)
113
+ return 1
114
+ }
115
+ out := bufio.NewWriter(stdout)
116
+ h := &windowsBroker{port: port, nextKey: 1, watches: map[uintptr]*windowsDirectoryWatch{}, locations: map[windowsWatchLocation]uintptr{}, registrations: map[int64]map[uintptr]struct{}{}, out: out, encoder: json.NewEncoder(out)}
117
+ requests := make(chan windowsBrokerRequest, 256)
118
+ done := make(chan struct{})
119
+ var wakeMu sync.Mutex
120
+ wake := func() {
121
+ wakeMu.Lock()
122
+ defer wakeMu.Unlock()
123
+ select {
124
+ case <-done:
125
+ return
126
+ default:
127
+ _ = windows.PostQueuedCompletionStatus(port, 0, 0, nil)
128
+ }
129
+ }
130
+ defer func() {
131
+ wakeMu.Lock()
132
+ close(done)
133
+ wakeMu.Unlock()
134
+ for key, watch := range h.watches {
135
+ h.retire(key, watch)
136
+ }
137
+ for len(h.watches) > 0 {
138
+ if _, err := h.completion(windows.INFINITE); err != nil {
139
+ break
140
+ }
141
+ }
142
+ _ = windows.CloseHandle(port)
143
+ }()
144
+ go func() {
145
+ defer wake()
146
+ defer close(requests)
147
+ scanner := bufio.NewScanner(stdin)
148
+ scanner.Buffer(make([]byte, 64*1024), 16*1024*1024)
149
+ for scanner.Scan() {
150
+ var request windowsBrokerRequest
151
+ if json.Unmarshal(scanner.Bytes(), &request) != nil {
152
+ continue
153
+ }
154
+ select {
155
+ case requests <- request:
156
+ wake()
157
+ case <-done:
158
+ return
159
+ }
160
+ }
161
+ }()
162
+ for {
163
+ if err := h.drain(); err != nil {
164
+ fmt.Fprintln(stderr, err)
165
+ return 1
166
+ }
167
+ select {
168
+ case request, open := <-requests:
169
+ if !open {
170
+ _ = out.Flush()
171
+ return 0
172
+ }
173
+ if err := h.request(request); err != nil {
174
+ fmt.Fprintln(stderr, err)
175
+ return 1
176
+ }
177
+ default:
178
+ if err := out.Flush(); err != nil {
179
+ return 0
180
+ }
181
+ if _, err := h.completion(windows.INFINITE); err != nil {
182
+ fmt.Fprintln(stderr, err)
183
+ return 1
184
+ }
185
+ }
186
+ }
187
+ }
188
+
189
+ // request dispatches an operation after the loop consumed queued completions.
190
+ // A new subscriber cannot receive those older batches. Ready follows actual native
191
+ // arming; drain replies follow a current empty-queue observation, not elapsed time.
192
+ //
193
+ // Add shares exact live location/recursion handles. Partial acquisition rolls back
194
+ // the entire registration and reports failure; it cannot lend partial coverage.
195
+ // Remove visits only that owner's keys and preserves other subscribers.
196
+ func (h *windowsBroker) request(request windowsBrokerRequest) error {
197
+ switch request.Op {
198
+ case "add":
199
+ h.remove(request.ID)
200
+ keys := map[uintptr]struct{}{}
201
+ h.registrations[request.ID] = keys
202
+ for _, location := range request.Locations {
203
+ key, err := h.acquire(location)
204
+ if err != nil {
205
+ h.remove(request.ID)
206
+ return h.encoder.Encode(map[string]any{"id": request.ID, "ready": true, "failed": true})
207
+ }
208
+ keys[key] = struct{}{}
209
+ h.watches[key].subscribers[request.ID] = request.AllEvents
210
+ h.watches[key].ordered = nil
211
+ }
212
+ return h.encoder.Encode(map[string]any{"id": request.ID, "ready": true, "failed": false})
213
+ case "remove":
214
+ h.remove(request.ID)
215
+ case "drain":
216
+ if err := h.drain(); err != nil {
217
+ return err
218
+ }
219
+ return h.encoder.Encode(map[string]any{"id": request.ID, "drained": true, "unproven": []any{}})
220
+ }
221
+ return nil
222
+ }
223
+
224
+ // acquire shares an exact live directory/recursion key or opens, validates and
225
+ // arms a native directory before publishing readiness. UTF-16 conversion, native
226
+ // directory attributes, port association and initial read have one rollback owner.
227
+ // Cold acquisition allocates one handle and 64 KiB without traversing input files;
228
+ // alternate spellings may conservatively remain separate watches.
229
+ func (h *windowsBroker) acquire(location windowsWatchLocation) (uintptr, error) {
230
+ if key, ok := h.locations[location]; ok {
231
+ return key, nil
232
+ }
233
+ native, err := windows.UTF16PtrFromString(location.Directory)
234
+ if err != nil {
235
+ return 0, err
236
+ }
237
+ handle, err := windows.CreateFile(native, windows.FILE_LIST_DIRECTORY, windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE, nil, windows.OPEN_EXISTING, windows.FILE_FLAG_BACKUP_SEMANTICS|windows.FILE_FLAG_OVERLAPPED, 0)
238
+ if err != nil {
239
+ return 0, err
240
+ }
241
+ var info windows.ByHandleFileInformation
242
+ if err = windows.GetFileInformationByHandle(handle, &info); err != nil || info.FileAttributes&windows.FILE_ATTRIBUTE_DIRECTORY == 0 {
243
+ _ = windows.CloseHandle(handle)
244
+ if err == nil {
245
+ err = windows.ERROR_DIRECTORY
246
+ }
247
+ return 0, err
248
+ }
249
+ key := h.nextKey
250
+ h.nextKey++
251
+ if _, err = windows.CreateIoCompletionPort(handle, h.port, key, 1); err != nil {
252
+ _ = windows.CloseHandle(handle)
253
+ return 0, err
254
+ }
255
+ watch := &windowsDirectoryWatch{location: location, handle: handle, buffer: make([]byte, 64*1024), subscribers: map[int64]bool{}}
256
+ if err = watch.arm(); err != nil {
257
+ _ = windows.CloseHandle(handle)
258
+ return 0, err
259
+ }
260
+ h.watches[key] = watch
261
+ h.locations[location] = key
262
+ return key, nil
263
+ }
264
+
265
+ // arm transfers the retained buffer to one overlapped native read. Initial and
266
+ // subsequent reads use the same mutation mask and recursion policy. Only successful
267
+ // arming marks pending; a caller must keep the watch rooted through completion.
268
+ // Reuse is allowed only after the preceding batch was decoded, never while the
269
+ // kernel still owns its buffer. The shared mask explains access versus A-B-A writes.
270
+ func (watch *windowsDirectoryWatch) arm() error {
271
+ watch.overlapped = windows.Overlapped{}
272
+ err := windows.ReadDirectoryChanges(watch.handle, &watch.buffer[0], uint32(len(watch.buffer)), watch.location.Recursive, windowsWatchMask, nil, &watch.overlapped, 0)
273
+ if err == nil {
274
+ watch.pending = true
275
+ }
276
+ return err
277
+ }
278
+
279
+ // remove releases this registration alone through its indexed watch keys.
280
+ // Remaining subscribers keep their native handle; membership changes invalidate
281
+ // only sorted fanout identities. Last detach removes live location authority and
282
+ // retires the handle, while pending completion memory remains rooted. Removing a
283
+ // map entry is not a native close/completion acknowledgment.
284
+ func (h *windowsBroker) remove(id int64) {
285
+ keys := h.registrations[id]
286
+ delete(h.registrations, id)
287
+ for key := range keys {
288
+ if watch := h.watches[key]; watch != nil {
289
+ delete(watch.subscribers, id)
290
+ watch.ordered = nil
291
+ if len(watch.subscribers) == 0 {
292
+ h.retire(key, watch)
293
+ }
294
+ }
295
+ }
296
+ }
297
+
298
+ // retire withdraws live coverage and attempts cancellation/close once. Pending
299
+ // watches remain in the completion index until their actual completion; nonpending
300
+ // watches can leave immediately. Native cancellation/close attempts alone do not
301
+ // certify success. The key is never reused, so late native completion cannot lend
302
+ // a retired buffer or event to another subscription.
303
+ func (h *windowsBroker) retire(key uintptr, watch *windowsDirectoryWatch) {
304
+ if watch.retiring {
305
+ return
306
+ }
307
+ watch.retiring = true
308
+ delete(h.locations, watch.location)
309
+ _ = windows.CancelIoEx(watch.handle, &watch.overlapped)
310
+ _ = windows.CloseHandle(watch.handle)
311
+ if !watch.pending {
312
+ delete(h.watches, key)
313
+ }
314
+ }
315
+
316
+ // drain consumes currently available native completions and wake packets until
317
+ // the port reports empty. Each batch is consumed once; work follows decoded bytes
318
+ // and subscriber fanout, with no event-count or time shortcut. Continuous activity
319
+ // can prolong the drain. Native/output errors propagate instead of upgrading an
320
+ // unavailable frontier to silent success; no historical results are retained.
321
+ func (h *windowsBroker) drain() error {
322
+ for {
323
+ available, err := h.completion(0)
324
+ if err != nil || !available {
325
+ return err
326
+ }
327
+ }
328
+ }
329
+
330
+ // completion selects the rooted buffer by native key and overlapped identity,
331
+ // decodes before reuse, then rearms before output backpressure can block. Every
332
+ // actual mutation survives, including A-B-A. Zero bytes, malformed records and
333
+ // unrepresentable names emit a gap; failed reads/rearming withdraw coverage and
334
+ // retire the handle. Neither loss nor partial success becomes empty proof.
335
+ //
336
+ // One batch is decoded once for all subscribers. Ordered IDs are sorted once per
337
+ // membership epoch, then reused. Work follows native bytes, subscriber count and
338
+ // encoded name bytes. Canceled watches remain rooted until completion; only
339
+ // detached decoded text can outlive buffer reuse.
340
+ func (h *windowsBroker) completion(timeout uint32) (bool, error) {
341
+ var count uint32
342
+ var key uintptr
343
+ var overlapped *windows.Overlapped
344
+ err := windows.GetQueuedCompletionStatus(h.port, &count, &key, &overlapped, timeout)
345
+ if overlapped == nil {
346
+ if errors.Is(err, windows.WAIT_TIMEOUT) {
347
+ return false, nil
348
+ }
349
+ return true, err
350
+ }
351
+ watch := h.watches[key]
352
+ if watch == nil || overlapped != &watch.overlapped {
353
+ return true, fmt.Errorf("unknown native directory completion")
354
+ }
355
+ watch.pending = false
356
+ if watch.retiring {
357
+ delete(h.watches, key)
358
+ return true, nil
359
+ }
360
+ if err != nil {
361
+ for id := range watch.subscribers {
362
+ if encodeErr := h.encoder.Encode(map[string]any{"id": id, "failed": true}); encodeErr != nil {
363
+ return true, encodeErr
364
+ }
365
+ }
366
+ h.retire(key, watch)
367
+ return true, nil
368
+ }
369
+ var events []windowsNotification
370
+ complete := false
371
+ if uint64(count) <= uint64(len(watch.buffer)) {
372
+ events, complete = decodeWindowsNotifications(watch.buffer[:count])
373
+ }
374
+ // The handle's native buffer retains changes between requests. Rearm before
375
+ // output can block, keeping caller backpressure out of the subscription gap.
376
+ if err := watch.arm(); err != nil {
377
+ for id := range watch.subscribers {
378
+ if encodeErr := h.encoder.Encode(map[string]any{"id": id, "failed": true}); encodeErr != nil {
379
+ return true, encodeErr
380
+ }
381
+ }
382
+ h.retire(key, watch)
383
+ return true, nil
384
+ }
385
+ if watch.ordered == nil {
386
+ watch.ordered = make([]int64, 0, len(watch.subscribers))
387
+ for id := range watch.subscribers {
388
+ watch.ordered = append(watch.ordered, id)
389
+ }
390
+ sort.Slice(watch.ordered, func(i, j int) bool { return watch.ordered[i] < watch.ordered[j] })
391
+ }
392
+ for _, id := range watch.ordered {
393
+ if !complete {
394
+ if err := h.encoder.Encode(map[string]any{"id": id, "gap": true}); err != nil {
395
+ return true, err
396
+ }
397
+ }
398
+ for _, event := range events {
399
+ if event.kind == "change" && !watch.subscribers[id] && !watch.location.Recursive {
400
+ continue
401
+ }
402
+ if err := h.encoder.Encode(map[string]any{"id": id, "directory": watch.location.Directory, "eventType": event.kind, "filename": event.name}); err != nil {
403
+ return true, err
404
+ }
405
+ }
406
+ }
407
+ return true, nil
408
+ }
409
+
410
+ // windowsNotification carries detached kind/name text, never a slice of the
411
+ // rearmed native buffer. The decoder owns Windows action and UTF-16 validation;
412
+ // unknown or unrepresentable identities cannot become guessed successful names.
413
+ type windowsNotification struct{ kind, name string }
414
+
415
+ // decodeWindowsNotifications validates linked FILE_NOTIFY_INFORMATION records
416
+ // with one forward scan. Bounds and aligned advancing offsets prevent overlap,
417
+ // overflow and cycles. Known actions and paired UTF-16 preserve native relative
418
+ // names; an unpaired surrogate cannot be losslessly represented in Go/JSON.
419
+ //
420
+ // Zero/truncated/unknown/malformed batches withdraw completeness even when earlier
421
+ // records were valid. Only detached events escape; the completion owner shares that
422
+ // parse with subscribers and retains the input buffer. Temporary storage follows
423
+ // name bytes, with no historical parse memo or filesystem traversal.
424
+ func decodeWindowsNotifications(buffer []byte) ([]windowsNotification, bool) {
425
+ if len(buffer) == 0 {
426
+ return nil, false
427
+ }
428
+ events := []windowsNotification{}
429
+ for offset := 0; ; {
430
+ if offset > len(buffer)-12 {
431
+ return events, false
432
+ }
433
+ record := buffer[offset:]
434
+ next, action, size := binary.LittleEndian.Uint32(record), binary.LittleEndian.Uint32(record[4:]), binary.LittleEndian.Uint32(record[8:])
435
+ if size == 0 || size%2 != 0 || uint64(size)+12 > uint64(len(record)) {
436
+ return events, false
437
+ }
438
+ end := 12 + int(size)
439
+ units := make([]uint16, size/2)
440
+ for i := range units {
441
+ units[i] = binary.LittleEndian.Uint16(record[12+i*2:])
442
+ }
443
+ // Windows can represent an unpaired surrogate. JSON/Go replacement text
444
+ // cannot preserve that exact native identity, so withdraw coverage instead.
445
+ for i := 0; i < len(units); i++ {
446
+ if units[i] >= 0xd800 && units[i] <= 0xdbff {
447
+ if i+1 == len(units) || units[i+1] < 0xdc00 || units[i+1] > 0xdfff {
448
+ return events, false
449
+ }
450
+ i++
451
+ } else if units[i] >= 0xdc00 && units[i] <= 0xdfff {
452
+ return events, false
453
+ }
454
+ }
455
+ name := string(utf16.Decode(units))
456
+ kind := "rename"
457
+ if action == windows.FILE_ACTION_MODIFIED {
458
+ kind = "change"
459
+ } else if action < windows.FILE_ACTION_ADDED || action > windows.FILE_ACTION_RENAMED_NEW_NAME {
460
+ return events, false
461
+ }
462
+ events = append(events, windowsNotification{kind: kind, name: name})
463
+ if next == 0 {
464
+ return events, true
465
+ }
466
+ if next%4 != 0 || uint64(next) < uint64(end) || uint64(next) >= uint64(len(record)) {
467
+ return events, false
468
+ }
469
+ offset += int(next)
470
+ }
471
+ }
@@ -1,5 +1,5 @@
1
1
  // Package watchhelper implements `ttsc __watch`, the directory notification
2
- // helper the unplugin adapter runs on Linux.
2
+ // helper the unplugin adapter runs on Linux and Windows.
3
3
  //
4
4
  // An inotify instance holds a bounded queue. Events beyond its capacity are
5
5
  // dropped and an IN_Q_OVERFLOW event reports that loss, which libuv, and so
@@ -7,7 +7,13 @@
7
7
  // notice. The helper owns its own inotify instance and reports the overflow,
8
8
  // so the adapter can stop trusting its watches' silence instead.
9
9
  //
10
- // The protocol is newline-delimited JSON over stdio. The client writes
10
+ // On Windows the same command serves the unplugin broker add/remove/drain
11
+ // protocol over ordered JSON lines. Its completion-port backend shares directory
12
+ // and recursion keys, omits access-time notifications, reports loss as gap and
13
+ // acquisition/read failure as failed, and retains canceled buffers until native
14
+ // completion. No compiler state is stored in either backend.
15
+ //
16
+ // The Linux protocol is newline-delimited JSON over stdio. The client writes
11
17
  // requests:
12
18
  //
13
19
  // - `{"op":"add","id":N,"path":"/abs/dir"}` watches one directory. The reply