@livx.cc/appwrap 0.55.0 → 0.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/package.json +2 -3
  2. package/runtime/app/shell/capabilities.manifest.ts +3 -3
  3. package/runtime/tests/bridge-response-delivery.test.ts +10 -1
  4. package/runtime/tests/env-scheme.test.ts +6 -0
  5. package/scripts/stage-assets.mjs +1 -1
  6. package/src/cli.ts +40 -570
  7. package/src/config.ts +1 -1
  8. package/src/handlers.ts +1 -1
  9. package/src/packs.ts +1 -1
  10. package/runtime-desktop/autotest/index.html +0 -77
  11. package/runtime-desktop/bridge-shim/build.sh +0 -9
  12. package/runtime-desktop/bridge-shim/content-entry.ts +0 -87
  13. package/runtime-desktop/bridge-shim/shim.ts +0 -132
  14. package/runtime-desktop/chrome/appwrap-browser-chrome.html +0 -185
  15. package/runtime-desktop/crates/webview-control/Cargo.lock +0 -330
  16. package/runtime-desktop/crates/webview-control/Cargo.toml +0 -28
  17. package/runtime-desktop/crates/webview-control/examples/host_harness.rs +0 -362
  18. package/runtime-desktop/crates/webview-control/src/embedded.rs +0 -138
  19. package/runtime-desktop/crates/webview-control/src/lib.rs +0 -100
  20. package/runtime-desktop/crates/webview-control/src/popup.rs +0 -522
  21. package/runtime-desktop/crates/webview-control/src/profiles.rs +0 -57
  22. package/runtime-desktop/crates/webview-control/src/wkwebview_backend.rs +0 -558
  23. package/runtime-desktop/src-tauri/Cargo.lock +0 -5474
  24. package/runtime-desktop/src-tauri/Cargo.toml +0 -45
  25. package/runtime-desktop/src-tauri/build.rs +0 -3
  26. package/runtime-desktop/src-tauri/capabilities/default.json +0 -6
  27. package/runtime-desktop/src-tauri/icons/icon.png +0 -0
  28. package/runtime-desktop/src-tauri/shell_config.json +0 -10
  29. package/runtime-desktop/src-tauri/src/biometrics_mac.rs +0 -138
  30. package/runtime-desktop/src-tauri/src/bridge_mac.rs +0 -424
  31. package/runtime-desktop/src-tauri/src/browser_tab.rs +0 -804
  32. package/runtime-desktop/src-tauri/src/main.rs +0 -1228
  33. package/runtime-desktop/src-tauri/src/notifications_mac.rs +0 -202
  34. package/runtime-desktop/src-tauri/src/oauth_mac.rs +0 -139
  35. package/runtime-desktop/src-tauri/src/plugin_host.rs +0 -522
  36. package/runtime-desktop/src-tauri/src/popup_mac.rs +0 -5
  37. package/runtime-desktop/src-tauri/src/push_mac.rs +0 -193
  38. package/runtime-desktop/src-tauri/src/server.rs +0 -156
  39. package/runtime-desktop/src-tauri/src/sidecar.rs +0 -398
  40. package/runtime-desktop/src-tauri/tauri.conf.json +0 -14
  41. package/src/desktop.ts +0 -300
  42. package/src/plugin/host.ts +0 -242
@@ -1,193 +0,0 @@
1
- // APNs device-token registration for the SIGNED desktop lane (push.register / push.unregister).
2
- //
3
- // macOS delivers the token via NSApplicationDelegate callbacks. Tauri (tao) owns the app delegate
4
- // and does not implement the two APNs selectors, so we ADD them to tao's delegate class at runtime
5
- // (class_addMethod — pure addition, no swizzle, nothing existing is displaced). If a future tao
6
- // ever implements them, class_addMethod returns false and we fail the request honestly instead of
7
- // silently double-handling. Envelope mirrors the mobile shell (runtime/app/shell/handlers-push.ts):
8
- // result = { platform: 'apns', token: <hex>, topic: <bundle id> }, 25s timeout, single-flight.
9
- use std::ffi::CStr;
10
- use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
11
- use std::sync::Mutex;
12
-
13
- use objc2::runtime::{AnyClass, AnyObject, Sel};
14
- use objc2::{msg_send, sel, MainThreadMarker};
15
- use objc2_app_kit::NSApplication;
16
- use objc2_foundation::{NSData, NSError};
17
- use serde_json::json;
18
-
19
- use crate::{deliver, deliver_error, shell};
20
-
21
- /// Pending register() waiting for the delegate callback (single-flight, like oauth).
22
- static PENDING: Mutex<Option<(tauri::WebviewWindow, String)>> = Mutex::new(None);
23
- /// Cached token — APNs tokens are stable per launch; later register() calls resolve instantly.
24
- static TOKEN: Mutex<Option<String>> = Mutex::new(None);
25
- static HOOKS_INSTALLED: AtomicBool = AtomicBool::new(false);
26
- /// Monotonic id of the current in-flight register. Each new flight bumps it; the timeout thread
27
- /// captures its own id and only fires if it's STILL the current flight — so a stale timer from a
28
- /// fast-failed-then-retried flight can't cancel the later one (see `timeout_should_fire`).
29
- static FLIGHT: AtomicU64 = AtomicU64::new(0);
30
-
31
- /// Guard for the timeout thread: fire only if this flight is still the current one. Pure — unit-tested.
32
- fn timeout_should_fire(my_flight: u64, current_flight: u64) -> bool {
33
- my_flight == current_flight
34
- }
35
-
36
- fn deliver_token(window: &tauri::WebviewWindow, id: &str, token: &str) {
37
- let resp = json!({ "v": 1, "id": id, "kind": "response", "result": {
38
- "platform": "apns", "token": token, "topic": shell().identifier,
39
- }});
40
- deliver(window, &resp);
41
- }
42
-
43
- /// `push.register` — resolve the APNs device token. Called from appwrap_invoke (any thread);
44
- /// replies asynchronously from the injected delegate callback (main thread).
45
- pub fn register(window: tauri::WebviewWindow, id: String) {
46
- if let Some(tok) = TOKEN.lock().unwrap().clone() {
47
- return deliver_token(&window, &id, &tok);
48
- }
49
- let my_flight;
50
- {
51
- let mut p = PENDING.lock().unwrap();
52
- if p.is_some() {
53
- return deliver_error(&window, &id, "NATIVE_ERROR", "push.register already in progress");
54
- }
55
- // fetch_add returns the prior value; FLIGHT now holds this flight's unique id.
56
- my_flight = FLIGHT.fetch_add(1, Ordering::SeqCst) + 1;
57
- *p = Some((window.clone(), id.clone()));
58
- }
59
- let res = window.run_on_main_thread(|| {
60
- let mtm = MainThreadMarker::new().expect("run_on_main_thread guarantees main");
61
- if let Err(msg) = install_hooks(mtm) {
62
- if let Some((w, id)) = PENDING.lock().unwrap().take() {
63
- deliver_error(&w, &id, "NATIVE_ERROR", &msg);
64
- }
65
- return;
66
- }
67
- let app = NSApplication::sharedApplication(mtm);
68
- app.registerForRemoteNotifications();
69
- eprintln!("[push] registerForRemoteNotifications requested");
70
- });
71
- if let Err(e) = res {
72
- if let Some((w, id)) = PENDING.lock().unwrap().take() {
73
- deliver_error(&w, &id, "NATIVE_ERROR", &format!("failed to reach main thread: {e}"));
74
- }
75
- return;
76
- }
77
- // Mobile-shell parity: 25s cap (kit's push.register timeout is 30s). Only fire if THIS flight
78
- // is still current — hold the PENDING lock across the check+take so a concurrent new flight
79
- // can't slip in between (else a stale timer could cancel a later register).
80
- std::thread::spawn(move || {
81
- std::thread::sleep(std::time::Duration::from_secs(25));
82
- let mut p = PENDING.lock().unwrap();
83
- if timeout_should_fire(my_flight, FLIGHT.load(Ordering::SeqCst)) {
84
- if let Some((w, id)) = p.take() {
85
- deliver_error(&w, &id, "TIMEOUT", "APNs registration timed out");
86
- }
87
- }
88
- });
89
- }
90
-
91
- /// `push.unregister` — stop receiving remote pushes + drop the cached token.
92
- pub fn unregister(window: tauri::WebviewWindow, id: String) {
93
- TOKEN.lock().unwrap().take();
94
- let res = window.clone().run_on_main_thread(move || {
95
- let mtm = MainThreadMarker::new().expect("main");
96
- NSApplication::sharedApplication(mtm).unregisterForRemoteNotifications();
97
- let resp = json!({ "v": 1, "id": id, "kind": "response", "result": null });
98
- deliver(&window, &resp);
99
- });
100
- if let Err(e) = res {
101
- eprintln!("[push] unregister failed to reach main thread: {e}");
102
- }
103
- }
104
-
105
- /// Add the two APNs delegate methods to tao's NSApplicationDelegate class (once).
106
- fn install_hooks(mtm: MainThreadMarker) -> Result<(), String> {
107
- if HOOKS_INSTALLED.load(Ordering::SeqCst) {
108
- return Ok(());
109
- }
110
- let app = NSApplication::sharedApplication(mtm);
111
- let delegate: *mut AnyObject = unsafe { msg_send![&*app, delegate] };
112
- if delegate.is_null() {
113
- return Err("no NSApplication delegate to hook".into());
114
- }
115
- let cls: &AnyClass = unsafe { msg_send![delegate, class] };
116
- // v@:@@ = void ret, self, _cmd, NSApplication*, NSData*/NSError*.
117
- let types: &CStr = c"v@:@@";
118
- let ok = unsafe {
119
- objc2::ffi::class_addMethod(
120
- cls as *const AnyClass as *mut AnyClass,
121
- sel!(application:didRegisterForRemoteNotificationsWithDeviceToken:),
122
- std::mem::transmute::<
123
- extern "C-unwind" fn(*mut AnyObject, Sel, *mut AnyObject, *mut NSData),
124
- unsafe extern "C-unwind" fn(),
125
- >(did_register),
126
- types.as_ptr(),
127
- )
128
- };
129
- let ok2 = unsafe {
130
- objc2::ffi::class_addMethod(
131
- cls as *const AnyClass as *mut AnyClass,
132
- sel!(application:didFailToRegisterForRemoteNotificationsWithError:),
133
- std::mem::transmute::<
134
- extern "C-unwind" fn(*mut AnyObject, Sel, *mut AnyObject, *mut NSError),
135
- unsafe extern "C-unwind" fn(),
136
- >(did_fail),
137
- types.as_ptr(),
138
- )
139
- };
140
- if !ok.as_bool() || !ok2.as_bool() {
141
- return Err(format!(
142
- "delegate class {} already implements an APNs selector — refusing to double-handle",
143
- cls.name().to_string_lossy()
144
- ));
145
- }
146
- HOOKS_INSTALLED.store(true, Ordering::SeqCst);
147
- eprintln!("[push] APNs delegate hooks installed on {}", cls.name().to_string_lossy());
148
- Ok(())
149
- }
150
-
151
- extern "C-unwind" fn did_register(_this: *mut AnyObject, _cmd: Sel, _app: *mut AnyObject, token: *mut NSData) {
152
- // A null/empty NSData is a failed registration masquerading as success — never cache it or
153
- // hand back an empty-string "token" (the app would upload garbage and silently get no pushes).
154
- let bytes = if token.is_null() { Vec::new() } else { unsafe { (*token).to_vec() } };
155
- if bytes.is_empty() {
156
- eprintln!("[push] APNs delivered a null/empty device token — treating as failure");
157
- if let Some((w, id)) = PENDING.lock().unwrap().take() {
158
- deliver_error(&w, &id, "NATIVE_ERROR", "APNs returned an empty device token");
159
- }
160
- return;
161
- }
162
- let hex = bytes.iter().map(|b| format!("{b:02x}")).collect::<String>();
163
- eprintln!("[push] APNs token: {hex}");
164
- *TOKEN.lock().unwrap() = Some(hex.clone());
165
- if let Some((w, id)) = PENDING.lock().unwrap().take() {
166
- deliver_token(&w, &id, &hex);
167
- }
168
- }
169
-
170
- extern "C-unwind" fn did_fail(_this: *mut AnyObject, _cmd: Sel, _app: *mut AnyObject, error: *mut NSError) {
171
- let msg = if error.is_null() {
172
- "APNs registration failed".to_string()
173
- } else {
174
- unsafe { (*error).localizedDescription() }.to_string()
175
- };
176
- eprintln!("[push] APNs registration failed: {msg}");
177
- if let Some((w, id)) = PENDING.lock().unwrap().take() {
178
- deliver_error(&w, &id, "NATIVE_ERROR", &msg);
179
- }
180
- }
181
-
182
- #[cfg(test)]
183
- mod tests {
184
- use super::*;
185
-
186
- #[test]
187
- fn timeout_fires_only_for_the_current_flight() {
188
- // Flight #1's timer must NOT fire once flight #2 has started (fast-fail + retry scenario).
189
- assert!(timeout_should_fire(1, 1)); // still current → fire
190
- assert!(!timeout_should_fire(1, 2)); // superseded → do NOT cancel the later flight
191
- assert!(timeout_should_fire(2, 2));
192
- }
193
- }
@@ -1,156 +0,0 @@
1
- //! `desktop.server` mode: boot a local server command, wait for its port, then the shell navigates the
2
- //! window to it (see main.rs setup). This is the desktop analogue of iOS/Android `loader:'server'`, but
3
- //! the URL is served by a child process THIS shell owns — so we also tear it down on window-close.
4
- //!
5
- //! Teardown correctness: the server (e.g. `para-chat`) itself spawns children (a backend + a vite
6
- //! preview). A plain SIGKILL of the direct child would orphan those grandchildren. So we `setsid()` the
7
- //! child into its OWN process group at spawn, then SIGTERM the WHOLE group on shutdown — every
8
- //! descendant gets the signal and the server's own exit-guard can reap cleanly.
9
-
10
- use std::io::{BufRead, BufReader};
11
- use std::net::TcpStream;
12
- use std::os::unix::process::CommandExt;
13
- use std::process::{Child, Command, Stdio};
14
- use std::sync::mpsc::{self, Receiver};
15
- use std::sync::{Mutex, OnceLock};
16
- use std::time::{Duration, Instant};
17
-
18
- /// The booted child + its process-group id (== pid after setsid). Recorded for teardown.
19
- struct Running {
20
- child: Child,
21
- pgid: i32,
22
- }
23
-
24
- static RUNNING: OnceLock<Mutex<Option<Running>>> = OnceLock::new();
25
-
26
- fn slot() -> &'static Mutex<Option<Running>> {
27
- RUNNING.get_or_init(|| Mutex::new(None))
28
- }
29
-
30
- /// Resolved server config (mirrors `ServerConfig` in main.rs).
31
- pub struct ServerCfg<'a> {
32
- pub command: &'a str,
33
- pub args: &'a [String],
34
- pub cwd: &'a str,
35
- pub path: &'a str,
36
- /// Non-empty → capture the printed URL from stdout (see `spawn`). Empty → caller uses wait_for_port.
37
- pub url_marker: &'a str,
38
- }
39
-
40
- /// Extract the first `http://…`/`https://…` token from a line (up to the next whitespace). Pure.
41
- fn extract_url(line: &str) -> Option<String> {
42
- let start = line.find("http://").or_else(|| line.find("https://"))?;
43
- let rest = &line[start..];
44
- let end = rest.find(char::is_whitespace).unwrap_or(rest.len());
45
- Some(rest[..end].to_string())
46
- }
47
-
48
- /// Spawn the server command in its own process group (setsid), with the CLI-stamped PATH (a
49
- /// GUI-launched .app has a minimal launchd PATH). Records the child for teardown.
50
- ///
51
- /// When `url_marker` is non-empty, stdout is piped and a reader thread tees each line to the shell's
52
- /// stderr AND, on the first line containing the marker, extracts the printed URL and sends it back —
53
- /// so the shell loads whatever port the server auto-picked (no hardcoded port). Returns that receiver.
54
- /// When `url_marker` is empty, stdout is inherited and `None` is returned (caller uses `wait_for_port`).
55
- pub fn spawn(cfg: &ServerCfg) -> std::io::Result<Option<Receiver<String>>> {
56
- let capture = !cfg.url_marker.is_empty();
57
- let mut cmd = Command::new(cfg.command);
58
- cmd.args(cfg.args);
59
- if !cfg.cwd.is_empty() {
60
- cmd.current_dir(cfg.cwd);
61
- }
62
- if !cfg.path.is_empty() {
63
- cmd.env("PATH", cfg.path);
64
- }
65
- cmd.stdin(Stdio::null()).stderr(Stdio::inherit());
66
- cmd.stdout(if capture { Stdio::piped() } else { Stdio::inherit() });
67
- // New session/process group so we can signal every descendant at teardown. setsid() also makes the
68
- // child the group leader → its pgid equals its pid.
69
- unsafe {
70
- cmd.pre_exec(|| {
71
- if libc::setsid() == -1 {
72
- return Err(std::io::Error::last_os_error());
73
- }
74
- Ok(())
75
- });
76
- }
77
- let mut child = cmd.spawn()?;
78
- let pgid = child.id() as i32;
79
-
80
- let rx = if capture {
81
- let stdout = child.stdout.take().expect("piped stdout");
82
- let marker = cfg.url_marker.to_string();
83
- let (tx, rx) = mpsc::channel::<String>();
84
- std::thread::spawn(move || {
85
- let mut sent = false;
86
- for line in BufReader::new(stdout).lines().map_while(Result::ok) {
87
- eprintln!("[server] {line}");
88
- if !sent && line.contains(&marker) {
89
- if let Some(url) = extract_url(&line) {
90
- let _ = tx.send(url);
91
- sent = true; // keep draining stdout so the child never blocks on a full pipe
92
- }
93
- }
94
- }
95
- });
96
- Some(rx)
97
- } else {
98
- None
99
- };
100
-
101
- *slot().lock().unwrap() = Some(Running { child, pgid });
102
- Ok(rx)
103
- }
104
-
105
- /// Poll `127.0.0.1:port` with a TCP connect until it accepts (server is listening) or the timeout
106
- /// elapses. TCP-accept is a sufficient readiness signal — vite/preview binds the port only once it's
107
- /// ready to serve. Returns false on timeout.
108
- pub fn wait_for_port(port: u16, timeout_ms: u64) -> bool {
109
- let deadline = Instant::now() + Duration::from_millis(timeout_ms);
110
- let addr: std::net::SocketAddr = ([127, 0, 0, 1], port).into();
111
- loop {
112
- if TcpStream::connect_timeout(&addr, Duration::from_millis(500)).is_ok() {
113
- return true;
114
- }
115
- if Instant::now() >= deadline {
116
- return false;
117
- }
118
- std::thread::sleep(Duration::from_millis(300));
119
- }
120
- }
121
-
122
- /// Signal the child's whole process group and reap (idempotent). Called on app exit so the local stack
123
- /// never outlives the shell. SIGTERM first (lets the server run its exit-guard + reap its own children),
124
- /// then a short grace, then SIGKILL as a backstop — the server may TRAP SIGTERM without exiting, and a
125
- /// blocking `wait()` on such a child would hang the exit forever, so we hard-kill the group and only
126
- /// `try_wait()` (non-blocking) to reap. The negative pgid targets every descendant.
127
- pub fn shutdown() {
128
- if let Some(m) = RUNNING.get() {
129
- if let Some(mut r) = m.lock().unwrap().take() {
130
- unsafe { libc::kill(-r.pgid, libc::SIGTERM) };
131
- std::thread::sleep(Duration::from_millis(1500));
132
- unsafe { libc::kill(-r.pgid, libc::SIGKILL) };
133
- let _ = r.child.try_wait();
134
- }
135
- }
136
- }
137
-
138
- #[cfg(test)]
139
- mod tests {
140
- use super::extract_url;
141
-
142
- #[test]
143
- fn extracts_first_url_up_to_whitespace() {
144
- assert_eq!(
145
- extract_url(" Frontend: https://localhost:5050 (ready)"),
146
- Some("https://localhost:5050".to_string())
147
- );
148
- assert_eq!(extract_url("Backend: http://localhost:5051"), Some("http://localhost:5051".to_string()));
149
- }
150
-
151
- #[test]
152
- fn none_when_no_url() {
153
- assert_eq!(extract_url("Frontend ready on port 5050"), None);
154
- assert_eq!(extract_url(""), None);
155
- }
156
- }