@livx.cc/appwrap 0.46.1 → 0.46.3
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/package.json +1 -1
- package/runtime/App_Resources/Android/src/main/AndroidManifest.xml +1 -0
- package/runtime/app/shell/custom-webview.android.ts +19 -2
- package/runtime-desktop/bridge-shim/shim.ts +12 -197
- package/runtime-desktop/chrome/appwrap-browser-chrome.html +120 -0
- package/runtime-desktop/crates/webview-control/Cargo.lock +330 -0
- package/runtime-desktop/crates/webview-control/Cargo.toml +28 -0
- package/runtime-desktop/crates/webview-control/examples/host_harness.rs +338 -0
- package/runtime-desktop/crates/webview-control/src/embedded.rs +121 -0
- package/runtime-desktop/crates/webview-control/src/lib.rs +99 -0
- package/runtime-desktop/crates/webview-control/src/popup.rs +522 -0
- package/runtime-desktop/crates/webview-control/src/profiles.rs +57 -0
- package/runtime-desktop/crates/webview-control/src/wkwebview_backend.rs +540 -0
- package/runtime-desktop/src-tauri/Cargo.lock +13 -0
- package/runtime-desktop/src-tauri/Cargo.toml +6 -1
- package/runtime-desktop/src-tauri/capabilities/default.json +1 -1
- package/runtime-desktop/src-tauri/src/bridge_mac.rs +126 -431
- package/runtime-desktop/src-tauri/src/browser_tab.rs +445 -0
- package/runtime-desktop/src-tauri/src/main.rs +90 -2
- package/runtime-desktop/src-tauri/src/popup_mac.rs +5 -519
- package/runtime-desktop/src-tauri/tauri.conf.json +1 -0
- package/src/cli.ts +26 -1
- package/src/config.ts +10 -1
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# This file is automatically @generated by Cargo.
|
|
2
|
+
# It is not intended for manual editing.
|
|
3
|
+
version = 4
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "bitflags"
|
|
7
|
+
version = "2.13.0"
|
|
8
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
9
|
+
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
|
|
10
|
+
|
|
11
|
+
[[package]]
|
|
12
|
+
name = "block2"
|
|
13
|
+
version = "0.6.2"
|
|
14
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
15
|
+
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
|
|
16
|
+
dependencies = [
|
|
17
|
+
"objc2",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[[package]]
|
|
21
|
+
name = "dispatch2"
|
|
22
|
+
version = "0.3.1"
|
|
23
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
24
|
+
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
|
|
25
|
+
dependencies = [
|
|
26
|
+
"bitflags",
|
|
27
|
+
"objc2",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[[package]]
|
|
31
|
+
name = "itoa"
|
|
32
|
+
version = "1.0.18"
|
|
33
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
34
|
+
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
|
35
|
+
|
|
36
|
+
[[package]]
|
|
37
|
+
name = "libc"
|
|
38
|
+
version = "0.2.186"
|
|
39
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
40
|
+
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
|
41
|
+
|
|
42
|
+
[[package]]
|
|
43
|
+
name = "memchr"
|
|
44
|
+
version = "2.8.3"
|
|
45
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
46
|
+
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
|
47
|
+
|
|
48
|
+
[[package]]
|
|
49
|
+
name = "objc2"
|
|
50
|
+
version = "0.6.4"
|
|
51
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
52
|
+
checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
|
|
53
|
+
dependencies = [
|
|
54
|
+
"objc2-encode",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
[[package]]
|
|
58
|
+
name = "objc2-app-kit"
|
|
59
|
+
version = "0.3.2"
|
|
60
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
61
|
+
checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
|
|
62
|
+
dependencies = [
|
|
63
|
+
"bitflags",
|
|
64
|
+
"block2",
|
|
65
|
+
"libc",
|
|
66
|
+
"objc2",
|
|
67
|
+
"objc2-cloud-kit",
|
|
68
|
+
"objc2-core-data",
|
|
69
|
+
"objc2-core-foundation",
|
|
70
|
+
"objc2-core-graphics",
|
|
71
|
+
"objc2-core-image",
|
|
72
|
+
"objc2-core-text",
|
|
73
|
+
"objc2-core-video",
|
|
74
|
+
"objc2-foundation",
|
|
75
|
+
"objc2-quartz-core",
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
[[package]]
|
|
79
|
+
name = "objc2-cloud-kit"
|
|
80
|
+
version = "0.3.2"
|
|
81
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
82
|
+
checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c"
|
|
83
|
+
dependencies = [
|
|
84
|
+
"bitflags",
|
|
85
|
+
"objc2",
|
|
86
|
+
"objc2-foundation",
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
[[package]]
|
|
90
|
+
name = "objc2-core-data"
|
|
91
|
+
version = "0.3.2"
|
|
92
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
93
|
+
checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"
|
|
94
|
+
dependencies = [
|
|
95
|
+
"bitflags",
|
|
96
|
+
"objc2",
|
|
97
|
+
"objc2-foundation",
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
[[package]]
|
|
101
|
+
name = "objc2-core-foundation"
|
|
102
|
+
version = "0.3.2"
|
|
103
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
104
|
+
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
|
|
105
|
+
dependencies = [
|
|
106
|
+
"bitflags",
|
|
107
|
+
"dispatch2",
|
|
108
|
+
"objc2",
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
[[package]]
|
|
112
|
+
name = "objc2-core-graphics"
|
|
113
|
+
version = "0.3.2"
|
|
114
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
115
|
+
checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
|
|
116
|
+
dependencies = [
|
|
117
|
+
"bitflags",
|
|
118
|
+
"dispatch2",
|
|
119
|
+
"objc2",
|
|
120
|
+
"objc2-core-foundation",
|
|
121
|
+
"objc2-io-surface",
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
[[package]]
|
|
125
|
+
name = "objc2-core-image"
|
|
126
|
+
version = "0.3.2"
|
|
127
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
128
|
+
checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006"
|
|
129
|
+
dependencies = [
|
|
130
|
+
"objc2",
|
|
131
|
+
"objc2-foundation",
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
[[package]]
|
|
135
|
+
name = "objc2-core-text"
|
|
136
|
+
version = "0.3.2"
|
|
137
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
138
|
+
checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d"
|
|
139
|
+
dependencies = [
|
|
140
|
+
"bitflags",
|
|
141
|
+
"objc2",
|
|
142
|
+
"objc2-core-foundation",
|
|
143
|
+
"objc2-core-graphics",
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
[[package]]
|
|
147
|
+
name = "objc2-core-video"
|
|
148
|
+
version = "0.3.2"
|
|
149
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
150
|
+
checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6"
|
|
151
|
+
dependencies = [
|
|
152
|
+
"bitflags",
|
|
153
|
+
"objc2",
|
|
154
|
+
"objc2-core-foundation",
|
|
155
|
+
"objc2-core-graphics",
|
|
156
|
+
"objc2-io-surface",
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
[[package]]
|
|
160
|
+
name = "objc2-encode"
|
|
161
|
+
version = "4.1.0"
|
|
162
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
163
|
+
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
|
|
164
|
+
|
|
165
|
+
[[package]]
|
|
166
|
+
name = "objc2-foundation"
|
|
167
|
+
version = "0.3.2"
|
|
168
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
169
|
+
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
|
170
|
+
dependencies = [
|
|
171
|
+
"bitflags",
|
|
172
|
+
"block2",
|
|
173
|
+
"libc",
|
|
174
|
+
"objc2",
|
|
175
|
+
"objc2-core-foundation",
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
[[package]]
|
|
179
|
+
name = "objc2-io-surface"
|
|
180
|
+
version = "0.3.2"
|
|
181
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
182
|
+
checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
|
|
183
|
+
dependencies = [
|
|
184
|
+
"bitflags",
|
|
185
|
+
"objc2",
|
|
186
|
+
"objc2-core-foundation",
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
[[package]]
|
|
190
|
+
name = "objc2-javascript-core"
|
|
191
|
+
version = "0.3.2"
|
|
192
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
193
|
+
checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586"
|
|
194
|
+
dependencies = [
|
|
195
|
+
"objc2",
|
|
196
|
+
"objc2-core-foundation",
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
[[package]]
|
|
200
|
+
name = "objc2-quartz-core"
|
|
201
|
+
version = "0.3.2"
|
|
202
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
203
|
+
checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
|
|
204
|
+
dependencies = [
|
|
205
|
+
"bitflags",
|
|
206
|
+
"objc2",
|
|
207
|
+
"objc2-foundation",
|
|
208
|
+
]
|
|
209
|
+
|
|
210
|
+
[[package]]
|
|
211
|
+
name = "objc2-security"
|
|
212
|
+
version = "0.3.2"
|
|
213
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
214
|
+
checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a"
|
|
215
|
+
dependencies = [
|
|
216
|
+
"bitflags",
|
|
217
|
+
"objc2",
|
|
218
|
+
"objc2-core-foundation",
|
|
219
|
+
]
|
|
220
|
+
|
|
221
|
+
[[package]]
|
|
222
|
+
name = "objc2-web-kit"
|
|
223
|
+
version = "0.3.2"
|
|
224
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
225
|
+
checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f"
|
|
226
|
+
dependencies = [
|
|
227
|
+
"bitflags",
|
|
228
|
+
"block2",
|
|
229
|
+
"objc2",
|
|
230
|
+
"objc2-app-kit",
|
|
231
|
+
"objc2-core-foundation",
|
|
232
|
+
"objc2-foundation",
|
|
233
|
+
"objc2-javascript-core",
|
|
234
|
+
"objc2-security",
|
|
235
|
+
]
|
|
236
|
+
|
|
237
|
+
[[package]]
|
|
238
|
+
name = "proc-macro2"
|
|
239
|
+
version = "1.0.106"
|
|
240
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
241
|
+
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
|
242
|
+
dependencies = [
|
|
243
|
+
"unicode-ident",
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
[[package]]
|
|
247
|
+
name = "quote"
|
|
248
|
+
version = "1.0.46"
|
|
249
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
250
|
+
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
|
|
251
|
+
dependencies = [
|
|
252
|
+
"proc-macro2",
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
[[package]]
|
|
256
|
+
name = "serde"
|
|
257
|
+
version = "1.0.228"
|
|
258
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
259
|
+
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
|
260
|
+
dependencies = [
|
|
261
|
+
"serde_core",
|
|
262
|
+
]
|
|
263
|
+
|
|
264
|
+
[[package]]
|
|
265
|
+
name = "serde_core"
|
|
266
|
+
version = "1.0.228"
|
|
267
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
268
|
+
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
|
269
|
+
dependencies = [
|
|
270
|
+
"serde_derive",
|
|
271
|
+
]
|
|
272
|
+
|
|
273
|
+
[[package]]
|
|
274
|
+
name = "serde_derive"
|
|
275
|
+
version = "1.0.228"
|
|
276
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
277
|
+
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
|
278
|
+
dependencies = [
|
|
279
|
+
"proc-macro2",
|
|
280
|
+
"quote",
|
|
281
|
+
"syn",
|
|
282
|
+
]
|
|
283
|
+
|
|
284
|
+
[[package]]
|
|
285
|
+
name = "serde_json"
|
|
286
|
+
version = "1.0.150"
|
|
287
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
288
|
+
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
|
|
289
|
+
dependencies = [
|
|
290
|
+
"itoa",
|
|
291
|
+
"memchr",
|
|
292
|
+
"serde",
|
|
293
|
+
"serde_core",
|
|
294
|
+
"zmij",
|
|
295
|
+
]
|
|
296
|
+
|
|
297
|
+
[[package]]
|
|
298
|
+
name = "syn"
|
|
299
|
+
version = "2.0.118"
|
|
300
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
301
|
+
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
|
|
302
|
+
dependencies = [
|
|
303
|
+
"proc-macro2",
|
|
304
|
+
"quote",
|
|
305
|
+
"unicode-ident",
|
|
306
|
+
]
|
|
307
|
+
|
|
308
|
+
[[package]]
|
|
309
|
+
name = "unicode-ident"
|
|
310
|
+
version = "1.0.24"
|
|
311
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
312
|
+
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
|
313
|
+
|
|
314
|
+
[[package]]
|
|
315
|
+
name = "webview-control"
|
|
316
|
+
version = "0.1.0"
|
|
317
|
+
dependencies = [
|
|
318
|
+
"block2",
|
|
319
|
+
"objc2",
|
|
320
|
+
"objc2-app-kit",
|
|
321
|
+
"objc2-foundation",
|
|
322
|
+
"objc2-web-kit",
|
|
323
|
+
"serde_json",
|
|
324
|
+
]
|
|
325
|
+
|
|
326
|
+
[[package]]
|
|
327
|
+
name = "zmij"
|
|
328
|
+
version = "1.0.21"
|
|
329
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
330
|
+
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# webview-control — appwrap's generalized, automation-agnostic native webview primitive.
|
|
2
|
+
# Carved out of src-tauri/src/bridge_mac.rs (Phase 1). Owns per-profile persistent+isolated
|
|
3
|
+
# WKWebsiteDataStore windows, CSP-immune eval, snapshot, the bridgeShim WKScriptMessageHandler,
|
|
4
|
+
# and the SSO-popup delegate (popup module). It emits RAW script-message payloads + RAW eval
|
|
5
|
+
# results via callbacks — it holds NO bod-browser protocol / transport knowledge (that stays in
|
|
6
|
+
# the control-channel adapter, bridge_mac.rs).
|
|
7
|
+
#
|
|
8
|
+
# objc2* versions MUST match src-tauri/Cargo.toml exactly — the same WKWebView pointer crosses the
|
|
9
|
+
# crate boundary (pw.inner() as *mut WKWebView), so a version mismatch would make that cast UB.
|
|
10
|
+
[package]
|
|
11
|
+
name = "webview-control"
|
|
12
|
+
version = "0.1.0"
|
|
13
|
+
edition = "2021"
|
|
14
|
+
|
|
15
|
+
[lib]
|
|
16
|
+
# macOS-only today; a WebKitGtkBackend is a future slot behind the same seam (see ARCHITECTURE.md).
|
|
17
|
+
name = "webview_control"
|
|
18
|
+
path = "src/lib.rs"
|
|
19
|
+
|
|
20
|
+
[dependencies]
|
|
21
|
+
|
|
22
|
+
[target.'cfg(target_os = "macos")'.dependencies]
|
|
23
|
+
serde_json = "1"
|
|
24
|
+
objc2 = "0.6"
|
|
25
|
+
objc2-foundation = "0.3"
|
|
26
|
+
objc2-app-kit = "0.3"
|
|
27
|
+
objc2-web-kit = "0.3"
|
|
28
|
+
block2 = "0.6"
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
//! Phase-2 host harness — proves a live WKWebView tears off (ChildOf pane -> TopLevel window) and
|
|
2
|
+
//! docks back (-> ChildOf pane) with page + WKWebsiteDataStore session (localStorage marker) +
|
|
3
|
+
//! WKScriptMessageHandler bridge all intact, driven THROUGH the embedded in-process API
|
|
4
|
+
//! (`webview_control::embedded::Controller`) — driving-adapter (a), no socket.
|
|
5
|
+
//!
|
|
6
|
+
//! Run: `cargo run --example host_harness` (macOS; add `-- --hold` to keep windows up for a look).
|
|
7
|
+
//!
|
|
8
|
+
//! It is a minimal bare-AppKit host: it owns a pane NSView inside a plain NSWindow (mirroring what a
|
|
9
|
+
//! Tauri pane / unclaw-app pane provides), serves a loopback page from an in-process HTTP server
|
|
10
|
+
//! (opaque about:/data: origins can't use localStorage), then runs the assertion sequence and exits
|
|
11
|
+
//! non-zero on any failure. The AppKit main runloop is pumped manually so each async step
|
|
12
|
+
//! (callAsyncJavaScript / page load / bridge post) completes before the next.
|
|
13
|
+
|
|
14
|
+
use std::cell::RefCell;
|
|
15
|
+
use std::ffi::c_void;
|
|
16
|
+
use std::io::{Read, Write};
|
|
17
|
+
use std::net::TcpListener;
|
|
18
|
+
use std::rc::Rc;
|
|
19
|
+
use std::sync::mpsc::{channel, Receiver};
|
|
20
|
+
use std::time::{Duration, Instant};
|
|
21
|
+
|
|
22
|
+
use objc2::rc::Retained;
|
|
23
|
+
use objc2::MainThreadMarker;
|
|
24
|
+
use objc2_app_kit::{
|
|
25
|
+
NSApplication, NSApplicationActivationPolicy, NSBackingStoreType, NSView, NSWindow,
|
|
26
|
+
NSWindowStyleMask,
|
|
27
|
+
};
|
|
28
|
+
use objc2_foundation::{
|
|
29
|
+
NSDate, NSDefaultRunLoopMode, NSPoint, NSRect, NSRunLoop, NSSize, NSString,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
use webview_control::embedded::Controller;
|
|
33
|
+
use webview_control::{CreateSpec, Mount, ViewHandle};
|
|
34
|
+
|
|
35
|
+
/// Pump the AppKit main runloop for up to `secs`, returning early once `done()` is true.
|
|
36
|
+
fn pump_until(secs: f64, mut done: impl FnMut() -> bool) {
|
|
37
|
+
let rl = NSRunLoop::currentRunLoop();
|
|
38
|
+
let deadline = Instant::now() + Duration::from_secs_f64(secs);
|
|
39
|
+
while Instant::now() < deadline {
|
|
40
|
+
if done() {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
let until = NSDate::dateWithTimeIntervalSinceNow(0.05);
|
|
44
|
+
unsafe { rl.runMode_beforeDate(NSDefaultRunLoopMode, &until) };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fn pump(secs: f64) {
|
|
49
|
+
pump_until(secs, || false);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// Synchronous eval through the embedded Controller: fire callAsyncJavaScript and pump until the
|
|
53
|
+
/// completion block lands (or timeout). Returns the RAW return string.
|
|
54
|
+
fn eval(ctrl: &Controller, mtm: MainThreadMarker, id: &str, js: &str) -> Result<String, String> {
|
|
55
|
+
let slot: Rc<RefCell<Option<Result<String, String>>>> = Rc::new(RefCell::new(None));
|
|
56
|
+
let s2 = slot.clone();
|
|
57
|
+
ctrl.eval_async(mtm, id, js.to_string(), move |r| *s2.borrow_mut() = Some(r));
|
|
58
|
+
pump_until(8.0, || slot.borrow().is_some());
|
|
59
|
+
let out = slot.borrow_mut().take();
|
|
60
|
+
out.unwrap_or(Err("eval timed out".into()))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Start a throwaway loopback HTTP server; returns its `http://127.0.0.1:PORT/` URL. The page just
|
|
64
|
+
/// carries a stable title — the session marker is written via localStorage by the harness.
|
|
65
|
+
fn start_loopback() -> String {
|
|
66
|
+
start_loopback_titled("HOST-HARNESS-PAGE")
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// Start a throwaway loopback HTTP server carrying `title` as its `<title>`; returns its URL.
|
|
70
|
+
fn start_loopback_titled(title: &str) -> String {
|
|
71
|
+
let listener = TcpListener::bind("127.0.0.1:0").expect("bind loopback");
|
|
72
|
+
let addr = listener.local_addr().unwrap();
|
|
73
|
+
let title = title.to_string();
|
|
74
|
+
std::thread::spawn(move || {
|
|
75
|
+
let body = format!(
|
|
76
|
+
"<!doctype html><html><head><title>{title}</title></head>\
|
|
77
|
+
<body style=\"margin:0;background:#0b5;color:#fff;font:700 40px system-ui;\
|
|
78
|
+
display:flex;align-items:center;justify-content:center;height:100vh\">\
|
|
79
|
+
<div id=\"tag\">HOST-HARNESS-LIVE</div></body></html>"
|
|
80
|
+
);
|
|
81
|
+
for stream in listener.incoming() {
|
|
82
|
+
let Ok(mut s) = stream else { continue };
|
|
83
|
+
let mut buf = [0u8; 1024];
|
|
84
|
+
let _ = s.read(&mut buf);
|
|
85
|
+
let resp = format!(
|
|
86
|
+
"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
|
87
|
+
body.len(),
|
|
88
|
+
body
|
|
89
|
+
);
|
|
90
|
+
let _ = s.write_all(resp.as_bytes());
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
format!("http://{addr}/")
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// Read `location.href` once (pumping until the eval completes).
|
|
97
|
+
fn href(ctrl: &Controller, mtm: MainThreadMarker, id: &str) -> String {
|
|
98
|
+
eval(ctrl, mtm, id, "return location.href;").unwrap_or_default()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// Issue a nav op (idempotent — navigate/go_back/go_forward/reload can all be safely re-issued) and
|
|
102
|
+
/// poll `location.href` until it contains `needle` or the deadline elapses. Re-issuing each iteration
|
|
103
|
+
/// makes it robust to a WKWebView dropping a single async load request. Returns the last href.
|
|
104
|
+
fn nav_settle(
|
|
105
|
+
ctrl: &Controller,
|
|
106
|
+
mtm: MainThreadMarker,
|
|
107
|
+
id: &str,
|
|
108
|
+
needle: &str,
|
|
109
|
+
secs: f64,
|
|
110
|
+
mut issue: impl FnMut(),
|
|
111
|
+
) -> String {
|
|
112
|
+
let deadline = Instant::now() + Duration::from_secs_f64(secs);
|
|
113
|
+
let mut last = String::new();
|
|
114
|
+
while Instant::now() < deadline {
|
|
115
|
+
issue();
|
|
116
|
+
pump(0.4);
|
|
117
|
+
last = href(ctrl, mtm, id);
|
|
118
|
+
if last.contains(needle) {
|
|
119
|
+
return last;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
last
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
fn main() {
|
|
126
|
+
let mtm = MainThreadMarker::new().expect("host_harness must run on the main thread");
|
|
127
|
+
|
|
128
|
+
// Scratch profile dir so we don't touch the real appwrap-desktop store.
|
|
129
|
+
let scratch = std::env::temp_dir().join(format!("webview-control-harness-{}", std::process::id()));
|
|
130
|
+
std::env::set_var("APPWRAP_PROFILE_DIR", &scratch);
|
|
131
|
+
|
|
132
|
+
// AppKit host app (accessory: no Dock icon; we pump the runloop manually rather than app.run()).
|
|
133
|
+
let app = NSApplication::sharedApplication(mtm);
|
|
134
|
+
app.setActivationPolicy(NSApplicationActivationPolicy::Accessory);
|
|
135
|
+
let _ = app.finishLaunching();
|
|
136
|
+
|
|
137
|
+
// The HOST-owned pane: a plain NSWindow whose content view is the container the tab docks into.
|
|
138
|
+
let pane_rect = NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(900.0, 640.0));
|
|
139
|
+
let style = NSWindowStyleMask::Titled | NSWindowStyleMask::Closable | NSWindowStyleMask::Resizable;
|
|
140
|
+
let pane_window = unsafe {
|
|
141
|
+
NSWindow::initWithContentRect_styleMask_backing_defer(
|
|
142
|
+
mtm.alloc::<NSWindow>(),
|
|
143
|
+
pane_rect,
|
|
144
|
+
style,
|
|
145
|
+
NSBackingStoreType::Buffered,
|
|
146
|
+
false,
|
|
147
|
+
)
|
|
148
|
+
};
|
|
149
|
+
unsafe {
|
|
150
|
+
pane_window.setReleasedWhenClosed(false);
|
|
151
|
+
pane_window.setTitle(&NSString::from_str("[host pane]"));
|
|
152
|
+
}
|
|
153
|
+
let pane = NSView::initWithFrame(mtm.alloc::<NSView>(), pane_rect);
|
|
154
|
+
pane_window.setContentView(Some(&pane));
|
|
155
|
+
pane_window.center();
|
|
156
|
+
pane_window.makeKeyAndOrderFront(None);
|
|
157
|
+
let pane_handle = unsafe { ViewHandle::from_ptr(Retained::as_ptr(&pane) as *mut c_void) };
|
|
158
|
+
|
|
159
|
+
let page_url = start_loopback();
|
|
160
|
+
let id = "harness-tab";
|
|
161
|
+
let ctrl = Controller::new();
|
|
162
|
+
|
|
163
|
+
// Bridge sink: the crate hands us RAW page->host payloads; capture them to prove the
|
|
164
|
+
// WKScriptMessageHandler survives reparenting.
|
|
165
|
+
let (tx, rx): (_, Receiver<String>) = channel();
|
|
166
|
+
let sink = Box::new(move |raw: String| {
|
|
167
|
+
let _ = tx.send(raw);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// (1) Create the tab as a CHILD of the host pane — an embedded in-app browser tab.
|
|
171
|
+
println!("== create_webview ChildOf(pane) id={id} url={page_url}");
|
|
172
|
+
let created = ctrl.create_webview(
|
|
173
|
+
mtm,
|
|
174
|
+
CreateSpec {
|
|
175
|
+
label: id.to_string(),
|
|
176
|
+
url: page_url.clone(),
|
|
177
|
+
profile_id: "harness".to_string(),
|
|
178
|
+
init_js: String::new(),
|
|
179
|
+
mount: Mount::ChildOf(pane_handle),
|
|
180
|
+
},
|
|
181
|
+
sink,
|
|
182
|
+
);
|
|
183
|
+
match &created {
|
|
184
|
+
Ok(c) => println!(" created: persistent={} store={}", c.persistent, c.store_uuid),
|
|
185
|
+
Err(e) => {
|
|
186
|
+
eprintln!("FAIL: create_webview: {e}");
|
|
187
|
+
std::process::exit(1);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
pump(3.0); // let the page load
|
|
191
|
+
|
|
192
|
+
// (2) Write the session marker + capture page identity while CHILD.
|
|
193
|
+
let set = eval(
|
|
194
|
+
&ctrl,
|
|
195
|
+
mtm,
|
|
196
|
+
id,
|
|
197
|
+
"localStorage.setItem('marker','HOST-HARNESS-SESSION-77'); \
|
|
198
|
+
return JSON.stringify({href:location.href,title:document.title,marker:localStorage.getItem('marker')});",
|
|
199
|
+
);
|
|
200
|
+
println!("== child, marker set: {set:?}");
|
|
201
|
+
|
|
202
|
+
// (3) Bridge ping while CHILD.
|
|
203
|
+
let _ = eval(&ctrl, mtm, id, "webkit.messageHandlers.bridgeShim.postMessage('PING-CHILD'); return 'posted';");
|
|
204
|
+
pump(0.5);
|
|
205
|
+
|
|
206
|
+
// (4) TEAR OFF -> TopLevel window.
|
|
207
|
+
println!("== set_mount TopLevel (tear off)");
|
|
208
|
+
if !ctrl.set_mount(mtm, id, Mount::TopLevel) {
|
|
209
|
+
eprintln!("FAIL: set_mount TopLevel returned false");
|
|
210
|
+
std::process::exit(1);
|
|
211
|
+
}
|
|
212
|
+
pump(1.5);
|
|
213
|
+
|
|
214
|
+
// (5) DOCK BACK -> ChildOf(pane).
|
|
215
|
+
println!("== set_mount ChildOf(pane) (dock back)");
|
|
216
|
+
if !ctrl.set_mount(mtm, id, Mount::ChildOf(pane_handle)) {
|
|
217
|
+
eprintln!("FAIL: set_mount ChildOf returned false");
|
|
218
|
+
std::process::exit(1);
|
|
219
|
+
}
|
|
220
|
+
pump(1.5);
|
|
221
|
+
|
|
222
|
+
// (6) After child->toplevel->child: read page identity + marker back, and ping the bridge again.
|
|
223
|
+
let after = eval(
|
|
224
|
+
&ctrl,
|
|
225
|
+
mtm,
|
|
226
|
+
id,
|
|
227
|
+
"return JSON.stringify({href:location.href,title:document.title,marker:localStorage.getItem('marker')});",
|
|
228
|
+
);
|
|
229
|
+
println!("== after reparent cycle, read-back: {after:?}");
|
|
230
|
+
let _ = eval(&ctrl, mtm, id, "webkit.messageHandlers.bridgeShim.postMessage('PING-AFTER'); return 'posted';");
|
|
231
|
+
pump(0.5);
|
|
232
|
+
|
|
233
|
+
// ---- assertions ----
|
|
234
|
+
let after = match after {
|
|
235
|
+
Ok(s) => s,
|
|
236
|
+
Err(e) => {
|
|
237
|
+
eprintln!("FAIL: read-back eval error after reparent: {e}");
|
|
238
|
+
std::process::exit(1);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
let page_ok = after.contains("HOST-HARNESS-PAGE"); // document.title preserved (page not reloaded/lost)
|
|
242
|
+
let session_ok = after.contains("HOST-HARNESS-SESSION-77"); // localStorage marker survived
|
|
243
|
+
let pings: Vec<String> = rx.try_iter().collect();
|
|
244
|
+
let bridge_ok = pings.iter().any(|p| p == "PING-CHILD") && pings.iter().any(|p| p == "PING-AFTER");
|
|
245
|
+
|
|
246
|
+
println!("\n---- Phase-2 embedded-API reparent assertions ----");
|
|
247
|
+
println!("page survived (title HOST-HARNESS-PAGE): {page_ok}");
|
|
248
|
+
println!("session survived (marker 77): {session_ok}");
|
|
249
|
+
println!("bridge survived (pings {:?}): {bridge_ok}", pings);
|
|
250
|
+
|
|
251
|
+
// ---- NAV ops (go_back / go_forward / reload via Controller) ----
|
|
252
|
+
// The tab is docked ChildOf(pane) on page 1. Navigate to a distinct page 2, then walk history.
|
|
253
|
+
let page1_href = href(&ctrl, mtm, id);
|
|
254
|
+
let page2_url = start_loopback_titled("HOST-HARNESS-PAGE-2");
|
|
255
|
+
println!("\n== NAV: navigate -> page2 {page2_url}");
|
|
256
|
+
let at2 = nav_settle(&ctrl, mtm, id, &page2_url, 10.0, || { let _ = ctrl.navigate(mtm, id, &page2_url); });
|
|
257
|
+
let nav_fwd_ok = at2.contains(&page2_url);
|
|
258
|
+
println!(" at page2: {at2:?} (ok={nav_fwd_ok})");
|
|
259
|
+
|
|
260
|
+
println!("== NAV: go_back -> page1");
|
|
261
|
+
let back1 = nav_settle(&ctrl, mtm, id, &page1_href, 10.0, || { let _ = ctrl.go_back(mtm, id); });
|
|
262
|
+
let nav_back_ok = back1 == page1_href && !page1_href.is_empty();
|
|
263
|
+
println!(" back at page1: {back1:?} (ok={nav_back_ok})");
|
|
264
|
+
|
|
265
|
+
println!("== NAV: go_forward -> page2");
|
|
266
|
+
let fwd2 = nav_settle(&ctrl, mtm, id, &page2_url, 10.0, || { let _ = ctrl.go_forward(mtm, id); });
|
|
267
|
+
let nav_fwd2_ok = fwd2.contains(&page2_url);
|
|
268
|
+
println!(" forward at page2: {fwd2:?} (ok={nav_fwd2_ok})");
|
|
269
|
+
|
|
270
|
+
println!("== NAV: reload (still page2, title present)");
|
|
271
|
+
let reloaded = nav_settle(&ctrl, mtm, id, &page2_url, 10.0, || { let _ = ctrl.reload(mtm, id); });
|
|
272
|
+
let title2 = eval(&ctrl, mtm, id, "return document.title;").unwrap_or_default();
|
|
273
|
+
let reload_ok = reloaded.contains(&page2_url) && title2.contains("HOST-HARNESS-PAGE-2");
|
|
274
|
+
println!(" after reload: href={reloaded:?} title={title2:?} (ok={reload_ok})");
|
|
275
|
+
let nav_ok = nav_fwd_ok && nav_back_ok && nav_fwd2_ok && reload_ok;
|
|
276
|
+
|
|
277
|
+
// ---- PROFILE SWAP: a second webview on a DIFFERENT profile → different store uuid ----
|
|
278
|
+
let first_store = created.as_ref().map(|c| c.store_uuid.clone()).unwrap_or_default();
|
|
279
|
+
let id_b = "harness-tab-b";
|
|
280
|
+
println!("\n== PROFILE SWAP: create id={id_b} profile=harnessB ChildOf(same pane)");
|
|
281
|
+
let created_b = ctrl.create_webview(
|
|
282
|
+
mtm,
|
|
283
|
+
CreateSpec {
|
|
284
|
+
label: id_b.to_string(),
|
|
285
|
+
url: page_url.clone(),
|
|
286
|
+
profile_id: "harnessB".to_string(),
|
|
287
|
+
init_js: String::new(),
|
|
288
|
+
mount: Mount::ChildOf(pane_handle),
|
|
289
|
+
},
|
|
290
|
+
Box::new(|_raw| {}),
|
|
291
|
+
);
|
|
292
|
+
let store_b = match &created_b {
|
|
293
|
+
Ok(c) => { println!(" harnessB store={}", c.store_uuid); c.store_uuid.clone() }
|
|
294
|
+
Err(e) => { eprintln!("FAIL: create harnessB: {e}"); std::process::exit(1); }
|
|
295
|
+
};
|
|
296
|
+
let profile_swap_ok = !first_store.is_empty() && store_b != first_store;
|
|
297
|
+
println!(" store isolation: harness={first_store} harnessB={store_b} (differ={profile_swap_ok})");
|
|
298
|
+
let _ = ctrl.close(mtm, id_b);
|
|
299
|
+
pump(0.3);
|
|
300
|
+
|
|
301
|
+
println!("\n---- nav + profile-swap assertions ----");
|
|
302
|
+
println!("nav ops (fwd/back/fwd/reload): {nav_ok}");
|
|
303
|
+
println!("profile swap (distinct store uuid): {profile_swap_ok}");
|
|
304
|
+
|
|
305
|
+
if std::env::args().any(|a| a == "--hold") {
|
|
306
|
+
println!("(--hold) leaving windows up 20s for inspection…");
|
|
307
|
+
pump(20.0);
|
|
308
|
+
}
|
|
309
|
+
// ---- zombie-close gate ----
|
|
310
|
+
// The tab is currently docked ChildOf(pane) (step 5/6). BEFORE close it is a subview of the host
|
|
311
|
+
// pane; AFTER close() the pane MUST have ZERO subviews — i.e. the WKWebView is actually gone from
|
|
312
|
+
// the host view tree, not left alive+visible as a zombie. This machine-gates the Phase-2 HIGH bug
|
|
313
|
+
// where close() only called window.close() and never removeFromSuperview() on the embedded webview.
|
|
314
|
+
let before_close = pane.subviews().count();
|
|
315
|
+
println!("\n== host pane subview count BEFORE close: {before_close} (expect 1: the docked tab)");
|
|
316
|
+
let _ = ctrl.close(mtm, id);
|
|
317
|
+
pump(0.5); // let AppKit settle the view-tree mutation
|
|
318
|
+
let after_close = pane.subviews().count();
|
|
319
|
+
println!("== host pane subview count AFTER close: {after_close} (expect 0: tab detached)");
|
|
320
|
+
let pane_empty_ok = after_close == 0;
|
|
321
|
+
|
|
322
|
+
println!("pane empty after close (no zombie webview): {pane_empty_ok}");
|
|
323
|
+
|
|
324
|
+
if page_ok && session_ok && bridge_ok && pane_empty_ok && nav_ok && profile_swap_ok {
|
|
325
|
+
println!("\nPASS: live webview reparented child->toplevel->child via EMBEDDED API; page+session+bridge intact; nav ops (back/forward/reload) + profile-swap (distinct store) verified; host pane empty after close.");
|
|
326
|
+
} else {
|
|
327
|
+
if !nav_ok { eprintln!("\nFAIL: nav ops (go_back/go_forward/reload) did not land as expected."); }
|
|
328
|
+
if !profile_swap_ok { eprintln!("\nFAIL: profile swap did not yield a distinct store uuid."); }
|
|
329
|
+
if !pane_empty_ok {
|
|
330
|
+
eprintln!(
|
|
331
|
+
"\nFAIL: ZOMBIE WEBVIEW — host pane still has {after_close} subview(s) after close(); \
|
|
332
|
+
the embedded WKWebView was not removed from the host view tree."
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
eprintln!("\nFAIL: one or more invariants did not survive reparent.");
|
|
336
|
+
std::process::exit(1);
|
|
337
|
+
}
|
|
338
|
+
}
|