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.
- package/cmd/platform/main.go +2 -2
- package/internal/sourceprocess/contain_windows.go +10 -13
- package/internal/sourceprocess/started_windows.go +41 -0
- package/internal/sourceprocess/windows_started_publication_distinguishes_pending_from_refusal_test.go +76 -0
- package/internal/watchhelper/run.go +30 -20
- package/internal/watchhelper/run_other.go +4 -4
- package/internal/watchhelper/run_windows.go +471 -0
- package/internal/watchhelper/watchhelper.go +8 -2
- package/internal/watchhelper/windows_notifications_preserve_mutations_without_access_noise_test.go +506 -0
- package/lib/compiler/internal/build/BuildExecution.d.ts +4 -2
- package/lib/compiler/internal/build/BuildExecution.js +40 -12
- package/lib/compiler/internal/build/BuildExecution.js.map +1 -1
- package/lib/compiler/internal/startResidentTransform.js +5 -4
- package/lib/compiler/internal/startResidentTransform.js.map +1 -1
- package/lib/compiler/internal/transformProjectInMemory.js +9 -7
- package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
- package/lib/internal/javascriptRuntimeCapabilities.d.ts +13 -4
- package/lib/internal/javascriptRuntimeCapabilities.js +56 -6
- package/lib/internal/javascriptRuntimeCapabilities.js.map +1 -1
- package/lib/internal/nativeBinary.d.ts +3 -3
- package/lib/internal/nativeBinary.js +3 -3
- package/lib/internal/pluginSource.d.ts +1 -0
- package/lib/internal/pluginSource.js +1 -0
- package/lib/internal/pluginSource.js.map +1 -1
- package/lib/internal/resolveNodeBinary.d.ts +6 -1
- package/lib/internal/resolveNodeBinary.js +6 -2
- package/lib/internal/resolveNodeBinary.js.map +1 -1
- package/lib/internal/runtimeExecutableIdentity.d.ts +22 -13
- package/lib/internal/runtimeExecutableIdentity.js +192 -102
- package/lib/internal/runtimeExecutableIdentity.js.map +1 -1
- package/lib/launcher/internal/runtime/installRuntimeHooks.js +58 -4
- package/lib/launcher/internal/runtime/installRuntimeHooks.js.map +1 -1
- package/lib/launcher/internal/ttscserver/captureLSPPluginSelectionInputs.d.ts +5 -1
- package/lib/launcher/internal/ttscserver/captureLSPPluginSelectionInputs.js +7 -2
- package/lib/launcher/internal/ttscserver/captureLSPPluginSelectionInputs.js.map +1 -1
- package/lib/launcher/internal/ttscserver/runTtscserver.js +8 -1
- package/lib/launcher/internal/ttscserver/runTtscserver.js.map +1 -1
- package/lib/plugin/internal/CapabilityResolutionFormat.d.ts +21 -62
- package/lib/plugin/internal/CapabilityResolutionFormat.js +15 -106
- package/lib/plugin/internal/CapabilityResolutionFormat.js.map +1 -1
- package/lib/plugin/internal/load/PluginDescriptorEvaluationCache.d.ts +6 -0
- package/lib/plugin/internal/load/PluginDescriptorEvaluationCache.js +4 -4
- package/lib/plugin/internal/load/PluginDescriptorEvaluationCache.js.map +1 -1
- package/lib/plugin/internal/load/loadProjectPlugins.d.ts +24 -14
- package/lib/plugin/internal/load/loadProjectPlugins.js +141 -56
- package/lib/plugin/internal/load/loadProjectPlugins.js.map +1 -1
- package/lib/plugin/internal/readCapabilityResolution.d.ts +6 -0
- package/lib/plugin/internal/readCapabilityResolution.js +8 -5
- package/lib/plugin/internal/readCapabilityResolution.js.map +1 -1
- package/lib/plugin/internal/source/GoEnvironmentReading.d.ts +48 -0
- package/lib/plugin/internal/source/GoEnvironmentReading.js +156 -0
- package/lib/plugin/internal/source/GoEnvironmentReading.js.map +1 -0
- package/lib/plugin/internal/source/NativeSourcePackages.d.ts +51 -13
- package/lib/plugin/internal/source/NativeSourcePackages.js +182 -27
- package/lib/plugin/internal/source/NativeSourcePackages.js.map +1 -1
- package/lib/plugin/internal/source/PluginBuildEnvironmentReadings.d.ts +42 -3
- package/lib/plugin/internal/source/PluginBuildEnvironmentReadings.js +68 -7
- package/lib/plugin/internal/source/PluginBuildEnvironmentReadings.js.map +1 -1
- package/lib/plugin/internal/source/PluginBuildEnvironmentWitness.d.ts +5 -3
- package/lib/plugin/internal/source/PluginBuildEnvironmentWitness.js +14 -8
- package/lib/plugin/internal/source/PluginBuildEnvironmentWitness.js.map +1 -1
- package/lib/plugin/internal/source/PluginContentIdentities.d.ts +265 -0
- package/lib/plugin/internal/source/PluginContentIdentities.js +452 -0
- package/lib/plugin/internal/source/PluginContentIdentities.js.map +1 -0
- package/lib/plugin/internal/source/PluginLoadAnswers.d.ts +63 -0
- package/lib/plugin/internal/source/PluginLoadAnswers.js +128 -0
- package/lib/plugin/internal/source/PluginLoadAnswers.js.map +1 -0
- package/lib/plugin/internal/source/SourceBuildCacheLayout.d.ts +10 -0
- package/lib/plugin/internal/source/SourceBuildCacheLayout.js +12 -0
- package/lib/plugin/internal/source/SourceBuildCacheLayout.js.map +1 -1
- package/lib/plugin/internal/source/SourcePluginWorkspace.d.ts +53 -19
- package/lib/plugin/internal/source/SourcePluginWorkspace.js +111 -22
- package/lib/plugin/internal/source/SourcePluginWorkspace.js.map +1 -1
- package/lib/plugin/internal/source/buildSourcePlugin.d.ts +34 -18
- package/lib/plugin/internal/source/buildSourcePlugin.js +52 -24
- package/lib/plugin/internal/source/buildSourcePlugin.js.map +1 -1
- package/lib/plugin/internal/source/computeCacheKey.d.ts +25 -7
- package/lib/plugin/internal/source/computeCacheKey.js +85 -34
- package/lib/plugin/internal/source/computeCacheKey.js.map +1 -1
- package/lib/plugin/internal/source/hashPluginBuildEnvironment.d.ts +7 -3
- package/lib/plugin/internal/source/hashPluginBuildEnvironment.js +75 -102
- package/lib/plugin/internal/source/hashPluginBuildEnvironment.js.map +1 -1
- package/lib/plugin/internal/source/pluginBuildEnvironment.d.ts +8 -3
- package/lib/plugin/internal/source/pluginBuildEnvironment.js +8 -4
- package/lib/plugin/internal/source/pluginBuildEnvironment.js.map +1 -1
- package/lib/plugin/internal/source/pluginBuildEnvironmentWorker.js +12 -2
- package/lib/plugin/internal/source/pluginBuildEnvironmentWorker.js.map +1 -1
- package/lib/plugin/internal/source/pluginBuildVersions.d.ts +2 -2
- package/lib/plugin/internal/source/pluginBuildVersions.js +4 -17
- package/lib/plugin/internal/source/pluginBuildVersions.js.map +1 -1
- package/lib/plugin/internal/source/pluginModuleReplaceDirectories.d.ts +6 -5
- package/lib/plugin/internal/source/pluginModuleReplaceDirectories.js +10 -17
- package/lib/plugin/internal/source/pluginModuleReplaceDirectories.js.map +1 -1
- package/lib/plugin/internal/source/pluginSourceState.d.ts +6 -2
- package/lib/plugin/internal/source/pluginSourceState.js +20 -8
- package/lib/plugin/internal/source/pluginSourceState.js.map +1 -1
- package/lib/plugin/internal/source/pluginSourceStateHolds.d.ts +7 -2
- package/lib/plugin/internal/source/pluginSourceStateHolds.js +12 -7
- package/lib/plugin/internal/source/pluginSourceStateHolds.js.map +1 -1
- package/lib/plugin/internal/source/processPluginBuildEnvironment.d.ts +7 -2
- package/lib/plugin/internal/source/processPluginBuildEnvironment.js +8 -3
- package/lib/plugin/internal/source/processPluginBuildEnvironment.js.map +1 -1
- package/lib/plugin/internal/source/provenPluginSourceDigest.d.ts +28 -0
- package/lib/plugin/internal/source/provenPluginSourceDigest.js +41 -0
- package/lib/plugin/internal/source/provenPluginSourceDigest.js.map +1 -0
- package/lib/plugin/internal/source/pruneCacheFileRoot.d.ts +3 -2
- package/lib/plugin/internal/source/pruneCacheFileRoot.js +3 -2
- package/lib/plugin/internal/source/pruneCacheFileRoot.js.map +1 -1
- package/lib/plugin/internal/source/ttscPackageVersion.d.ts +26 -0
- package/lib/plugin/internal/source/ttscPackageVersion.js +47 -0
- package/lib/plugin/internal/source/ttscPackageVersion.js.map +1 -0
- package/lib/plugin/internal/writeCapabilityResolution.d.ts +6 -0
- package/lib/plugin/internal/writeCapabilityResolution.js +11 -7
- package/lib/plugin/internal/writeCapabilityResolution.js.map +1 -1
- package/lib/plugin/resolveCapabilityPlugins.js +67 -15
- package/lib/plugin/resolveCapabilityPlugins.js.map +1 -1
- package/package.json +8 -8
- package/src/compiler/internal/build/BuildExecution.ts +46 -13
- package/src/compiler/internal/startResidentTransform.ts +12 -3
- package/src/compiler/internal/transformProjectInMemory.ts +10 -2
- package/src/internal/javascriptRuntimeCapabilities.ts +71 -5
- package/src/internal/nativeBinary.ts +3 -3
- package/src/internal/pluginSource.ts +1 -0
- package/src/internal/resolveNodeBinary.ts +12 -1
- package/src/internal/runtimeExecutableIdentity.ts +227 -141
- package/src/launcher/internal/runtime/installRuntimeHooks.ts +68 -3
- package/src/launcher/internal/ttscserver/captureLSPPluginSelectionInputs.ts +11 -2
- package/src/launcher/internal/ttscserver/runTtscserver.ts +18 -1
- package/src/plugin/internal/CapabilityResolutionFormat.ts +23 -107
- package/src/plugin/internal/load/PluginDescriptorEvaluationCache.ts +14 -4
- package/src/plugin/internal/load/loadProjectPlugins.ts +421 -211
- package/src/plugin/internal/readCapabilityResolution.ts +17 -6
- package/src/plugin/internal/source/GoEnvironmentReading.ts +178 -0
- package/src/plugin/internal/source/NativeSourcePackages.ts +365 -48
- package/src/plugin/internal/source/PluginBuildEnvironmentReadings.ts +83 -5
- package/src/plugin/internal/source/PluginBuildEnvironmentWitness.ts +18 -8
- package/src/plugin/internal/source/PluginContentIdentities.ts +563 -0
- package/src/plugin/internal/source/PluginLoadAnswers.ts +151 -0
- package/src/plugin/internal/source/SourceBuildCacheLayout.ts +14 -0
- package/src/plugin/internal/source/SourcePluginWorkspace.ts +148 -25
- package/src/plugin/internal/source/buildSourcePlugin.ts +104 -36
- package/src/plugin/internal/source/computeCacheKey.ts +129 -55
- package/src/plugin/internal/source/hashPluginBuildEnvironment.ts +152 -105
- package/src/plugin/internal/source/pluginBuildEnvironment.ts +9 -2
- package/src/plugin/internal/source/pluginBuildEnvironmentWorker.ts +18 -2
- package/src/plugin/internal/source/pluginBuildVersions.ts +5 -26
- package/src/plugin/internal/source/pluginModuleReplaceDirectories.ts +13 -25
- package/src/plugin/internal/source/pluginSourceState.ts +28 -12
- package/src/plugin/internal/source/pluginSourceStateHolds.ts +15 -8
- package/src/plugin/internal/source/processPluginBuildEnvironment.ts +8 -2
- package/src/plugin/internal/source/provenPluginSourceDigest.ts +45 -0
- package/src/plugin/internal/source/pruneCacheFileRoot.ts +3 -2
- package/src/plugin/internal/source/ttscPackageVersion.ts +45 -0
- package/src/plugin/internal/writeCapabilityResolution.ts +29 -8
- 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
|
-
//
|
|
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
|