ttsc 0.31.0 → 0.31.1
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/internal/nativeBinary.d.ts +3 -3
- package/lib/internal/nativeBinary.js +3 -3
- package/lib/plugin/internal/load/loadProjectPlugins.d.ts +12 -13
- package/lib/plugin/internal/load/loadProjectPlugins.js +61 -36
- package/lib/plugin/internal/load/loadProjectPlugins.js.map +1 -1
- package/lib/plugin/internal/source/GoEnvironmentReading.d.ts +42 -0
- package/lib/plugin/internal/source/GoEnvironmentReading.js +110 -0
- package/lib/plugin/internal/source/GoEnvironmentReading.js.map +1 -0
- package/lib/plugin/internal/source/NativeSourcePackages.d.ts +7 -7
- package/lib/plugin/internal/source/NativeSourcePackages.js +33 -20
- package/lib/plugin/internal/source/NativeSourcePackages.js.map +1 -1
- package/lib/plugin/internal/source/PluginBuildEnvironmentReadings.d.ts +21 -0
- package/lib/plugin/internal/source/PluginBuildEnvironmentReadings.js +36 -0
- 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/SourcePluginWorkspace.d.ts +50 -19
- package/lib/plugin/internal/source/SourcePluginWorkspace.js +109 -22
- package/lib/plugin/internal/source/SourcePluginWorkspace.js.map +1 -1
- package/lib/plugin/internal/source/buildSourcePlugin.d.ts +19 -16
- package/lib/plugin/internal/source/buildSourcePlugin.js +27 -22
- package/lib/plugin/internal/source/buildSourcePlugin.js.map +1 -1
- package/lib/plugin/internal/source/computeCacheKey.d.ts +4 -2
- package/lib/plugin/internal/source/computeCacheKey.js +15 -6
- package/lib/plugin/internal/source/computeCacheKey.js.map +1 -1
- package/lib/plugin/internal/source/hashPluginBuildEnvironment.js +2 -68
- package/lib/plugin/internal/source/hashPluginBuildEnvironment.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/package.json +8 -8
- package/src/internal/nativeBinary.ts +3 -3
- package/src/plugin/internal/load/loadProjectPlugins.ts +209 -117
- package/src/plugin/internal/source/GoEnvironmentReading.ts +109 -0
- package/src/plugin/internal/source/NativeSourcePackages.ts +103 -43
- package/src/plugin/internal/source/PluginBuildEnvironmentReadings.ts +42 -0
- package/src/plugin/internal/source/PluginBuildEnvironmentWitness.ts +18 -8
- package/src/plugin/internal/source/SourcePluginWorkspace.ts +143 -25
- package/src/plugin/internal/source/buildSourcePlugin.ts +62 -34
- package/src/plugin/internal/source/computeCacheKey.ts +26 -8
- package/src/plugin/internal/source/hashPluginBuildEnvironment.ts +8 -76
- package/src/plugin/internal/source/pluginModuleReplaceDirectories.ts +13 -25
package/internal/watchhelper/windows_notifications_preserve_mutations_without_access_noise_test.go
ADDED
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
//go:build windows
|
|
2
|
+
|
|
3
|
+
package watchhelper
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"bufio"
|
|
7
|
+
"bytes"
|
|
8
|
+
"encoding/binary"
|
|
9
|
+
"encoding/json"
|
|
10
|
+
"io"
|
|
11
|
+
"os"
|
|
12
|
+
"path/filepath"
|
|
13
|
+
"runtime"
|
|
14
|
+
"strings"
|
|
15
|
+
"testing"
|
|
16
|
+
"time"
|
|
17
|
+
|
|
18
|
+
"golang.org/x/sys/windows"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
// TestWindowsNotificationsPreserveMutationsWithoutAccessNoise verifies the
|
|
22
|
+
// actual native notification boundary without a runtime or compiler child.
|
|
23
|
+
// A delivered marker establishes a positive stream frontier; elapsed quiet is
|
|
24
|
+
// never used to certify that access or earlier writes were filtered correctly.
|
|
25
|
+
//
|
|
26
|
+
// 1. Force actual access-time updates and reads, then observe a native marker.
|
|
27
|
+
// 2. Preserve A-B-A writes, recursion, Unicode rename/removal and separate owners.
|
|
28
|
+
// 3. Refuse missing/file roots, classify literal malformed/lost batches as gaps,
|
|
29
|
+
// and stream ready/drain, subscriber joins/removal and EOF retirement.
|
|
30
|
+
//
|
|
31
|
+
// @evidence contracts/testing.md#behavioral-verification The actual Windows broker acquires handles, receives native notifications and emits decoded wire results; assertions distinguish access noise, real changes, recursion, independent registrations, failed acquisition, loss and actual streaming ready/drain/join/remove/EOF retirement.
|
|
32
|
+
// @evidence contracts/testing.md#independent-expectations Authored file names, native access timestamps and literal malformed notification bytes establish expectations independently of the implementation's mask or decoded results.
|
|
33
|
+
// @evidence contracts/testing.md#distinguishing-cases Real access-only updates contrast with content-preserving A-B-A writes; shallow and recursive registrations contrast on one native tree; live and removed owners, pre-join and shared mutations across actual request/reply frontiers, valid Unicode names and malformed/truncated batches distinguish coverage and loss.
|
|
34
|
+
// @evidence contracts/testing.md#execution-ownership This direct Go unit invokes the owning native broker and run in process using temporary files and memory streams; it installs no package and launches no compiler, runtime or helper child.
|
|
35
|
+
func TestWindowsNotificationsPreserveMutationsWithoutAccessNoise(t *testing.T) {
|
|
36
|
+
t.Run("access and ABA", func(t *testing.T) {
|
|
37
|
+
root := t.TempDir()
|
|
38
|
+
source := filepath.Join(root, "source.ts")
|
|
39
|
+
original := []byte("export const value = 1;\n")
|
|
40
|
+
if err := os.WriteFile(source, original, 0600); err != nil {
|
|
41
|
+
t.Fatal(err)
|
|
42
|
+
}
|
|
43
|
+
h, output := newWindowsTestBroker(t)
|
|
44
|
+
if err := h.request(windowsBrokerRequest{Op: "add", ID: 1, AllEvents: true, Locations: []windowsWatchLocation{{Directory: root}}}); err != nil {
|
|
45
|
+
t.Fatal(err)
|
|
46
|
+
}
|
|
47
|
+
// SetFileTime with nil last-write changes access alone, unlike utimes,
|
|
48
|
+
// which can round or write modification time and legitimately notify.
|
|
49
|
+
native, _ := windows.UTF16PtrFromString(source)
|
|
50
|
+
handle, err := windows.CreateFile(native, windows.FILE_WRITE_ATTRIBUTES, windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE, nil, windows.OPEN_EXISTING, 0, 0)
|
|
51
|
+
if err != nil {
|
|
52
|
+
t.Fatal(err)
|
|
53
|
+
}
|
|
54
|
+
access := windows.NsecToFiletime(time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC).UnixNano())
|
|
55
|
+
err = windows.SetFileTime(handle, nil, &access, nil)
|
|
56
|
+
var observed windows.ByHandleFileInformation
|
|
57
|
+
if err == nil {
|
|
58
|
+
err = windows.GetFileInformationByHandle(handle, &observed)
|
|
59
|
+
}
|
|
60
|
+
_ = windows.CloseHandle(handle)
|
|
61
|
+
if err != nil {
|
|
62
|
+
t.Fatal(err)
|
|
63
|
+
}
|
|
64
|
+
if observed.LastAccessTime != access {
|
|
65
|
+
t.Fatalf("native access update was not established: %+v", observed.LastAccessTime)
|
|
66
|
+
}
|
|
67
|
+
if _, err := os.ReadFile(source); err != nil {
|
|
68
|
+
t.Fatal(err)
|
|
69
|
+
}
|
|
70
|
+
rows := windowsTestFrontier(t, h, output, root, "after-access")
|
|
71
|
+
for _, row := range rows {
|
|
72
|
+
if row["filename"] == "source.ts" {
|
|
73
|
+
t.Fatalf("access-only update/read became mutation: %+v", row)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if err := os.WriteFile(source, []byte("export const value = 2;\n"), 0600); err != nil {
|
|
77
|
+
t.Fatal(err)
|
|
78
|
+
}
|
|
79
|
+
if err := os.WriteFile(source, original, 0600); err != nil {
|
|
80
|
+
t.Fatal(err)
|
|
81
|
+
}
|
|
82
|
+
rows = windowsTestFrontier(t, h, output, root, "after-ABA")
|
|
83
|
+
found := false
|
|
84
|
+
for _, row := range rows {
|
|
85
|
+
if row["filename"] == "source.ts" && row["eventType"] == "change" {
|
|
86
|
+
found = true
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if !found {
|
|
90
|
+
t.Fatalf("actual A-B-A writes disappeared: %+v", rows)
|
|
91
|
+
}
|
|
92
|
+
current, err := os.ReadFile(source)
|
|
93
|
+
if err != nil || !bytes.Equal(current, original) {
|
|
94
|
+
t.Fatalf("authored source restoration: %q, %v", current, err)
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
t.Run("live native directory loss", func(t *testing.T) {
|
|
98
|
+
parent := t.TempDir()
|
|
99
|
+
root := filepath.Join(parent, "watched")
|
|
100
|
+
if err := os.Mkdir(root, 0700); err != nil {
|
|
101
|
+
t.Fatal(err)
|
|
102
|
+
}
|
|
103
|
+
h, output := newWindowsTestBroker(t)
|
|
104
|
+
if err := h.request(windowsBrokerRequest{Op: "add", ID: 1, AllEvents: true, Locations: []windowsWatchLocation{{Directory: root}}}); err != nil {
|
|
105
|
+
t.Fatal(err)
|
|
106
|
+
}
|
|
107
|
+
if err := os.Remove(root); err != nil {
|
|
108
|
+
t.Fatal(err)
|
|
109
|
+
}
|
|
110
|
+
until := time.Now().Add(10 * time.Second)
|
|
111
|
+
for {
|
|
112
|
+
if err := h.drain(); err != nil {
|
|
113
|
+
t.Fatal(err)
|
|
114
|
+
}
|
|
115
|
+
_ = h.out.Flush()
|
|
116
|
+
rows := decodeWindowsTestRows(t, output)
|
|
117
|
+
failed := false
|
|
118
|
+
for _, row := range rows {
|
|
119
|
+
failed = failed || row["failed"] == true
|
|
120
|
+
}
|
|
121
|
+
if failed {
|
|
122
|
+
break
|
|
123
|
+
}
|
|
124
|
+
if time.Now().After(until) {
|
|
125
|
+
t.Fatalf("deleted native directory retained silent authority: %+v", rows)
|
|
126
|
+
}
|
|
127
|
+
time.Sleep(time.Millisecond)
|
|
128
|
+
}
|
|
129
|
+
if len(h.locations) != 0 {
|
|
130
|
+
t.Fatal("failed native watch remained live")
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
t.Run("sharing recursion and retirement", func(t *testing.T) {
|
|
134
|
+
root := t.TempDir()
|
|
135
|
+
child := filepath.Join(root, "child")
|
|
136
|
+
if err := os.Mkdir(child, 0700); err != nil {
|
|
137
|
+
t.Fatal(err)
|
|
138
|
+
}
|
|
139
|
+
h, output := newWindowsTestBroker(t)
|
|
140
|
+
for _, id := range []int64{1, 2, 3} {
|
|
141
|
+
if err := h.request(windowsBrokerRequest{Op: "add", ID: id, AllEvents: true, Locations: []windowsWatchLocation{{Directory: root, Recursive: id == 3}}}); err != nil {
|
|
142
|
+
t.Fatal(err)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if len(h.locations) != 2 {
|
|
146
|
+
t.Fatalf("shallow owners must share while recursion remains distinct: %d", len(h.locations))
|
|
147
|
+
}
|
|
148
|
+
deep := filepath.Join(child, "한글.ts")
|
|
149
|
+
if err := os.WriteFile(deep, []byte("x"), 0600); err != nil {
|
|
150
|
+
t.Fatal(err)
|
|
151
|
+
}
|
|
152
|
+
rows := windowsTestFrontier(t, h, output, root, "after-deep")
|
|
153
|
+
found := false
|
|
154
|
+
for _, row := range rows {
|
|
155
|
+
if row["filename"] != filepath.Join("child", "한글.ts") {
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
if row["id"] != float64(3) {
|
|
159
|
+
t.Fatalf("shallow owner heard deep entry: %+v", row)
|
|
160
|
+
}
|
|
161
|
+
found = true
|
|
162
|
+
}
|
|
163
|
+
if !found {
|
|
164
|
+
t.Fatalf("recursive Unicode creation missing: %+v", rows)
|
|
165
|
+
}
|
|
166
|
+
h.remove(1)
|
|
167
|
+
file := filepath.Join(root, "rename-before.ts")
|
|
168
|
+
renamed := filepath.Join(root, "rename-after.ts")
|
|
169
|
+
if err := os.WriteFile(file, []byte("x"), 0600); err != nil {
|
|
170
|
+
t.Fatal(err)
|
|
171
|
+
}
|
|
172
|
+
if err := os.Rename(file, renamed); err != nil {
|
|
173
|
+
t.Fatal(err)
|
|
174
|
+
}
|
|
175
|
+
if err := os.Remove(renamed); err != nil {
|
|
176
|
+
t.Fatal(err)
|
|
177
|
+
}
|
|
178
|
+
rows = windowsTestFrontier(t, h, output, root, "after-retirement")
|
|
179
|
+
old, next := false, false
|
|
180
|
+
for _, row := range rows {
|
|
181
|
+
if row["id"] == float64(1) {
|
|
182
|
+
t.Fatalf("retired owner heard later events: %+v", row)
|
|
183
|
+
}
|
|
184
|
+
if row["id"] != float64(2) || row["eventType"] != "rename" {
|
|
185
|
+
continue
|
|
186
|
+
}
|
|
187
|
+
old = old || row["filename"] == "rename-before.ts"
|
|
188
|
+
next = next || row["filename"] == "rename-after.ts"
|
|
189
|
+
}
|
|
190
|
+
if !old || !next {
|
|
191
|
+
t.Fatalf("remaining shared owner lost structural events: %+v", rows)
|
|
192
|
+
}
|
|
193
|
+
h.remove(2)
|
|
194
|
+
h.remove(3)
|
|
195
|
+
if len(h.locations) != 0 || len(h.registrations) != 0 {
|
|
196
|
+
t.Fatalf("last owner did not retire native registry")
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
t.Run("streaming ready drain sharing and EOF", func(t *testing.T) {
|
|
200
|
+
root := t.TempDir()
|
|
201
|
+
input, requests := io.Pipe()
|
|
202
|
+
responses, output := io.Pipe()
|
|
203
|
+
rows := make(chan map[string]any, 256)
|
|
204
|
+
scanResult := make(chan error, 1)
|
|
205
|
+
finished := make(chan int, 1)
|
|
206
|
+
stopScan := make(chan struct{})
|
|
207
|
+
go func() {
|
|
208
|
+
code := Run(input, output, io.Discard)
|
|
209
|
+
_ = input.Close()
|
|
210
|
+
_ = output.Close()
|
|
211
|
+
finished <- code
|
|
212
|
+
}()
|
|
213
|
+
go func() {
|
|
214
|
+
defer close(rows)
|
|
215
|
+
scanner := bufio.NewScanner(responses)
|
|
216
|
+
for scanner.Scan() {
|
|
217
|
+
var row map[string]any
|
|
218
|
+
if err := json.Unmarshal(scanner.Bytes(), &row); err != nil {
|
|
219
|
+
scanResult <- err
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
select {
|
|
223
|
+
case rows <- row:
|
|
224
|
+
case <-stopScan:
|
|
225
|
+
scanResult <- nil
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
scanResult <- scanner.Err()
|
|
230
|
+
}()
|
|
231
|
+
joined := false
|
|
232
|
+
scannerJoined := false
|
|
233
|
+
t.Cleanup(func() {
|
|
234
|
+
_ = requests.Close()
|
|
235
|
+
// Caller-owned pipe closure unblocks native output and the scanner on
|
|
236
|
+
// failure. Join both owned goroutines before temporary cleanup.
|
|
237
|
+
if !joined {
|
|
238
|
+
_ = responses.Close()
|
|
239
|
+
select {
|
|
240
|
+
case <-finished:
|
|
241
|
+
case <-time.After(10 * time.Second):
|
|
242
|
+
t.Error("streaming native owner did not retire after caller closure")
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
close(stopScan)
|
|
246
|
+
_ = responses.Close()
|
|
247
|
+
if !scannerJoined {
|
|
248
|
+
select {
|
|
249
|
+
case <-scanResult:
|
|
250
|
+
case <-time.After(10 * time.Second):
|
|
251
|
+
t.Error("protocol scanner did not retire after caller closure")
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
})
|
|
255
|
+
encoder := json.NewEncoder(requests)
|
|
256
|
+
send := func(request windowsBrokerRequest) {
|
|
257
|
+
t.Helper()
|
|
258
|
+
if err := encoder.Encode(request); err != nil {
|
|
259
|
+
t.Fatal(err)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
observed := []map[string]any{}
|
|
263
|
+
until := func(predicate func(map[string]any) bool) {
|
|
264
|
+
t.Helper()
|
|
265
|
+
deadline := time.NewTimer(10 * time.Second)
|
|
266
|
+
defer deadline.Stop()
|
|
267
|
+
for {
|
|
268
|
+
select {
|
|
269
|
+
case row, open := <-rows:
|
|
270
|
+
if !open {
|
|
271
|
+
t.Fatal("protocol output ended before its positive frontier")
|
|
272
|
+
}
|
|
273
|
+
observed = append(observed, row)
|
|
274
|
+
if row["failed"] == true || row["gap"] == true {
|
|
275
|
+
t.Fatalf("unexpected native coverage loss: %+v", row)
|
|
276
|
+
}
|
|
277
|
+
if predicate(row) {
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
case <-deadline.C:
|
|
281
|
+
t.Fatalf("streaming native frontier stalled: %+v", observed)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
ready := func(id int64) {
|
|
286
|
+
t.Helper()
|
|
287
|
+
send(windowsBrokerRequest{Op: "add", ID: id, AllEvents: true, Locations: []windowsWatchLocation{{Directory: root}}})
|
|
288
|
+
until(func(row map[string]any) bool { return row["id"] == float64(id) && row["ready"] == true })
|
|
289
|
+
}
|
|
290
|
+
drain := func(id int64) {
|
|
291
|
+
t.Helper()
|
|
292
|
+
send(windowsBrokerRequest{Op: "drain", ID: id})
|
|
293
|
+
until(func(row map[string]any) bool { return row["id"] == float64(id) && row["drained"] == true })
|
|
294
|
+
}
|
|
295
|
+
write := func(name string) {
|
|
296
|
+
t.Helper()
|
|
297
|
+
if err := os.WriteFile(filepath.Join(root, name), []byte("actual native mutation"), 0600); err != nil {
|
|
298
|
+
t.Fatal(err)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
ready(1)
|
|
302
|
+
write("before-join.ts")
|
|
303
|
+
until(func(row map[string]any) bool { return row["id"] == float64(1) && row["filename"] == "before-join.ts" })
|
|
304
|
+
drain(100)
|
|
305
|
+
ready(2)
|
|
306
|
+
write("shared.ts")
|
|
307
|
+
seen := map[float64]bool{}
|
|
308
|
+
until(func(row map[string]any) bool {
|
|
309
|
+
if row["filename"] == "shared.ts" {
|
|
310
|
+
id, ok := row["id"].(float64)
|
|
311
|
+
if ok {
|
|
312
|
+
seen[id] = true
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return seen[1] && seen[2]
|
|
316
|
+
})
|
|
317
|
+
drain(101)
|
|
318
|
+
for _, row := range observed {
|
|
319
|
+
if row["id"] == float64(2) && row["filename"] == "before-join.ts" {
|
|
320
|
+
t.Fatalf("new owner received a mutation before its observed join frontier: %+v", row)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
send(windowsBrokerRequest{Op: "remove", ID: 1})
|
|
324
|
+
drain(102)
|
|
325
|
+
observed = nil
|
|
326
|
+
write("after-remove.ts")
|
|
327
|
+
until(func(row map[string]any) bool { return row["id"] == float64(2) && row["filename"] == "after-remove.ts" })
|
|
328
|
+
drain(103)
|
|
329
|
+
for _, row := range observed {
|
|
330
|
+
if row["id"] == float64(1) {
|
|
331
|
+
t.Fatalf("removed owner received a later native row: %+v", row)
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if err := requests.Close(); err != nil {
|
|
335
|
+
t.Fatal(err)
|
|
336
|
+
}
|
|
337
|
+
select {
|
|
338
|
+
case code := <-finished:
|
|
339
|
+
joined = true
|
|
340
|
+
if code != 0 {
|
|
341
|
+
t.Fatalf("actual armed protocol did not retire successfully: %d", code)
|
|
342
|
+
}
|
|
343
|
+
case <-time.After(10 * time.Second):
|
|
344
|
+
t.Fatal("EOF did not retire the actual native loop")
|
|
345
|
+
}
|
|
346
|
+
err := <-scanResult
|
|
347
|
+
scannerJoined = true
|
|
348
|
+
if err != nil {
|
|
349
|
+
t.Fatalf("actual protocol framing failed: %v", err)
|
|
350
|
+
}
|
|
351
|
+
// A directory handle with no sharing cannot coexist with the helper's
|
|
352
|
+
// retired FILE_LIST_DIRECTORY handle. This checks actual native closure.
|
|
353
|
+
native, err := windows.UTF16PtrFromString(root)
|
|
354
|
+
if err != nil {
|
|
355
|
+
t.Fatal(err)
|
|
356
|
+
}
|
|
357
|
+
exclusive, err := windows.CreateFile(native, windows.FILE_LIST_DIRECTORY, 0, nil, windows.OPEN_EXISTING, windows.FILE_FLAG_BACKUP_SEMANTICS, 0)
|
|
358
|
+
if err != nil {
|
|
359
|
+
t.Fatalf("EOF retained a native directory owner: %v", err)
|
|
360
|
+
}
|
|
361
|
+
if err := windows.CloseHandle(exclusive); err != nil {
|
|
362
|
+
t.Fatal(err)
|
|
363
|
+
}
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
t.Run("failure loss and EOF", func(t *testing.T) {
|
|
367
|
+
root := t.TempDir()
|
|
368
|
+
file := filepath.Join(root, "file")
|
|
369
|
+
if err := os.WriteFile(file, []byte("x"), 0600); err != nil {
|
|
370
|
+
t.Fatal(err)
|
|
371
|
+
}
|
|
372
|
+
h, output := newWindowsTestBroker(t)
|
|
373
|
+
for index, location := range []string{filepath.Join(root, "missing"), file} {
|
|
374
|
+
if err := h.request(windowsBrokerRequest{Op: "add", ID: int64(index + 1), Locations: []windowsWatchLocation{{Directory: root}, {Directory: location}}}); err != nil {
|
|
375
|
+
t.Fatal(err)
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
_ = h.out.Flush()
|
|
379
|
+
rows := decodeWindowsTestRows(t, output)
|
|
380
|
+
if len(rows) != 2 || rows[0]["failed"] != true || rows[1]["failed"] != true || len(h.locations) != 0 {
|
|
381
|
+
t.Fatalf("failed partial acquisition retained success: %+v", rows)
|
|
382
|
+
}
|
|
383
|
+
for _, buffer := range [][]byte{nil, {0}, make([]byte, 12), windowsTestRecord(6, "x"), windowsTestRecord(3, "x")[:13]} {
|
|
384
|
+
if _, complete := decodeWindowsNotifications(buffer); complete {
|
|
385
|
+
t.Fatalf("malformed/lost native bytes became complete: %v", buffer)
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
valid := windowsTestRecord(3, "한글.ts")
|
|
389
|
+
decoded, complete := decodeWindowsNotifications(valid)
|
|
390
|
+
if !complete || len(decoded) != 1 || decoded[0].name != "한글.ts" || decoded[0].kind != "change" {
|
|
391
|
+
t.Fatalf("valid native record: %+v, %v", decoded, complete)
|
|
392
|
+
}
|
|
393
|
+
binary.LittleEndian.PutUint32(valid, 4)
|
|
394
|
+
if _, complete := decodeWindowsNotifications(valid); complete {
|
|
395
|
+
t.Fatal("overlapping native next offset accepted")
|
|
396
|
+
}
|
|
397
|
+
invalidName := windowsTestRecord(3, "x")
|
|
398
|
+
binary.LittleEndian.PutUint16(invalidName[12:], 0xd800)
|
|
399
|
+
if _, complete := decodeWindowsNotifications(invalidName); complete {
|
|
400
|
+
t.Fatal("unrepresentable native identity accepted")
|
|
401
|
+
}
|
|
402
|
+
request, _ := json.Marshal(windowsBrokerRequest{Op: "add", ID: 7, AllEvents: true, Locations: []windowsWatchLocation{{Directory: root}}})
|
|
403
|
+
var protocol bytes.Buffer
|
|
404
|
+
if code := run(strings.NewReader(string(request)+"\n"), &protocol, io.Discard); code != 0 {
|
|
405
|
+
t.Fatalf("EOF did not retire an armed native watch: %d", code)
|
|
406
|
+
}
|
|
407
|
+
if !strings.Contains(protocol.String(), `"ready":true`) {
|
|
408
|
+
t.Fatalf("actual native ready missing: %s", protocol.String())
|
|
409
|
+
}
|
|
410
|
+
})
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// newWindowsTestBroker opens a real completion port and retires every pending
|
|
414
|
+
// overlapped read before the test can release its buffer memory.
|
|
415
|
+
func newWindowsTestBroker(t *testing.T) (*windowsBroker, *bytes.Buffer) {
|
|
416
|
+
t.Helper()
|
|
417
|
+
runtime.LockOSThread()
|
|
418
|
+
t.Cleanup(runtime.UnlockOSThread)
|
|
419
|
+
port, err := windows.CreateIoCompletionPort(windows.InvalidHandle, 0, 0, 1)
|
|
420
|
+
if err != nil {
|
|
421
|
+
t.Fatal(err)
|
|
422
|
+
}
|
|
423
|
+
output := &bytes.Buffer{}
|
|
424
|
+
out := bufio.NewWriter(output)
|
|
425
|
+
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)}
|
|
426
|
+
t.Cleanup(func() {
|
|
427
|
+
for key, watch := range h.watches {
|
|
428
|
+
h.retire(key, watch)
|
|
429
|
+
}
|
|
430
|
+
for len(h.watches) != 0 {
|
|
431
|
+
if _, err := h.completion(windows.INFINITE); err != nil {
|
|
432
|
+
t.Errorf("native cleanup: %v", err)
|
|
433
|
+
break
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
_ = windows.CloseHandle(port)
|
|
437
|
+
})
|
|
438
|
+
return h, output
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// windowsTestFrontier waits for an actual marker from every live registration,
|
|
442
|
+
// collecting all earlier native rows. Its deadline only refuses stalled delivery.
|
|
443
|
+
func windowsTestFrontier(t *testing.T, h *windowsBroker, output *bytes.Buffer, root, marker string) []map[string]any {
|
|
444
|
+
t.Helper()
|
|
445
|
+
if err := os.WriteFile(filepath.Join(root, marker), []byte("frontier"), 0600); err != nil {
|
|
446
|
+
t.Fatal(err)
|
|
447
|
+
}
|
|
448
|
+
rows := []map[string]any{}
|
|
449
|
+
seen := map[int64]bool{}
|
|
450
|
+
until := time.Now().Add(10 * time.Second)
|
|
451
|
+
for {
|
|
452
|
+
if err := h.drain(); err != nil {
|
|
453
|
+
t.Fatal(err)
|
|
454
|
+
}
|
|
455
|
+
if err := h.out.Flush(); err != nil {
|
|
456
|
+
t.Fatal(err)
|
|
457
|
+
}
|
|
458
|
+
batch := decodeWindowsTestRows(t, output)
|
|
459
|
+
rows = append(rows, batch...)
|
|
460
|
+
for _, row := range batch {
|
|
461
|
+
if row["filename"] == marker {
|
|
462
|
+
seen[int64(row["id"].(float64))] = true
|
|
463
|
+
}
|
|
464
|
+
if row["failed"] == true || row["gap"] == true {
|
|
465
|
+
t.Fatalf("unexpected lost coverage: %+v", row)
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if len(seen) == len(h.registrations) {
|
|
469
|
+
return rows
|
|
470
|
+
}
|
|
471
|
+
if time.Now().After(until) {
|
|
472
|
+
t.Fatalf("native frontier stalled: %+v", rows)
|
|
473
|
+
}
|
|
474
|
+
time.Sleep(time.Millisecond)
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// decodeWindowsTestRows reads actual encoded broker output and resets only the
|
|
479
|
+
// test's observation buffer, never backend state.
|
|
480
|
+
func decodeWindowsTestRows(t *testing.T, output *bytes.Buffer) []map[string]any {
|
|
481
|
+
t.Helper()
|
|
482
|
+
var rows []map[string]any
|
|
483
|
+
decoder := json.NewDecoder(bytes.NewReader(output.Bytes()))
|
|
484
|
+
for decoder.More() {
|
|
485
|
+
var row map[string]any
|
|
486
|
+
if err := decoder.Decode(&row); err != nil {
|
|
487
|
+
t.Fatal(err)
|
|
488
|
+
}
|
|
489
|
+
rows = append(rows, row)
|
|
490
|
+
}
|
|
491
|
+
output.Reset()
|
|
492
|
+
return rows
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// windowsTestRecord authors one literal FILE_NOTIFY_INFORMATION layout.
|
|
496
|
+
func windowsTestRecord(action uint32, name string) []byte {
|
|
497
|
+
units, _ := windows.UTF16FromString(name)
|
|
498
|
+
units = units[:len(units)-1]
|
|
499
|
+
buffer := make([]byte, 12+len(units)*2)
|
|
500
|
+
binary.LittleEndian.PutUint32(buffer[4:], action)
|
|
501
|
+
binary.LittleEndian.PutUint32(buffer[8:], uint32(len(units)*2))
|
|
502
|
+
for i, unit := range units {
|
|
503
|
+
binary.LittleEndian.PutUint16(buffer[12+i*2:], unit)
|
|
504
|
+
}
|
|
505
|
+
return buffer
|
|
506
|
+
}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* The rules that find ttsc's platform binary (an absolute `TTSC_BINARY`, the
|
|
5
5
|
* per-platform package, then the repository's own dev layout) are ttsc's alone.
|
|
6
6
|
* A tool host that runs one of that binary's helper commands, as
|
|
7
|
-
* `@ttsc/unplugin` runs its
|
|
8
|
-
* helper always comes from the binary ttsc itself uses and no consumer
|
|
9
|
-
* a copy of the rules.
|
|
7
|
+
* `@ttsc/unplugin` runs its native watch helpers, imports them from here, so
|
|
8
|
+
* the helper always comes from the binary ttsc itself uses and no consumer
|
|
9
|
+
* carries a copy of the rules.
|
|
10
10
|
*/
|
|
11
11
|
export { resolveBinary } from "../compiler/internal/resolveBinary";
|
|
@@ -7,9 +7,9 @@ exports.resolveBinary = void 0;
|
|
|
7
7
|
* The rules that find ttsc's platform binary (an absolute `TTSC_BINARY`, the
|
|
8
8
|
* per-platform package, then the repository's own dev layout) are ttsc's alone.
|
|
9
9
|
* A tool host that runs one of that binary's helper commands, as
|
|
10
|
-
* `@ttsc/unplugin` runs its
|
|
11
|
-
* helper always comes from the binary ttsc itself uses and no consumer
|
|
12
|
-
* a copy of the rules.
|
|
10
|
+
* `@ttsc/unplugin` runs its native watch helpers, imports them from here, so
|
|
11
|
+
* the helper always comes from the binary ttsc itself uses and no consumer
|
|
12
|
+
* carries a copy of the rules.
|
|
13
13
|
*/
|
|
14
14
|
var resolveBinary_1 = require("../compiler/internal/resolveBinary");
|
|
15
15
|
Object.defineProperty(exports, "resolveBinary", { enumerable: true, get: function () { return resolveBinary_1.resolveBinary; } });
|
|
@@ -12,19 +12,18 @@ import type { ITtscParsedProjectConfig } from "../../../structures/internal/ITts
|
|
|
12
12
|
* under generic host manifests while preserving source-relative input layout.
|
|
13
13
|
* Main proposals must agree in their full owning module; linked proposals must
|
|
14
14
|
* agree inside every selected transform host. Errors or disagreement terminate
|
|
15
|
-
* the load rather than changing the proposed kind.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* linked-plugin host, are keyed but omitted from this report under the
|
|
15
|
+
* the load rather than changing the proposed kind. Watch callers first receive
|
|
16
|
+
* structurally selected source/module/contributor repair interests, before
|
|
17
|
+
* metadata can reject them. Valid records then narrow those interests to the
|
|
18
|
+
* exact build directories; interests alone never admit a plugin, certify a kind
|
|
19
|
+
* or supply reusable source-state proof. Returns the ordered native plugins,
|
|
20
|
+
* parsed project config, recorded JavaScript-host inputs and unresolved
|
|
21
|
+
* selection candidates, and the keyed state of reported Go source directories
|
|
22
|
+
* supplied to the builds (`pluginSources`): each plugin's module root and each
|
|
23
|
+
* contributor's source, with its state (`pluginSourceState`), the sources as
|
|
24
|
+
* the build read them together with the environment a build there is keyed on.
|
|
25
|
+
* Directories within ttsc's installed package, including its overlays and the
|
|
26
|
+
* fallback linked-plugin host, are keyed but omitted from this report under the
|
|
28
27
|
* installed-package/version ownership policy. That policy does not prove the
|
|
29
28
|
* installation cannot be edited in place. Descriptor completeness relies on the
|
|
30
29
|
* runtime recorder's status and the descriptor's explicit external-read
|