@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.
- package/package.json +2 -3
- package/runtime/app/shell/capabilities.manifest.ts +3 -3
- package/runtime/tests/bridge-response-delivery.test.ts +10 -1
- package/runtime/tests/env-scheme.test.ts +6 -0
- package/scripts/stage-assets.mjs +1 -1
- package/src/cli.ts +40 -570
- package/src/config.ts +1 -1
- package/src/handlers.ts +1 -1
- package/src/packs.ts +1 -1
- package/runtime-desktop/autotest/index.html +0 -77
- package/runtime-desktop/bridge-shim/build.sh +0 -9
- package/runtime-desktop/bridge-shim/content-entry.ts +0 -87
- package/runtime-desktop/bridge-shim/shim.ts +0 -132
- package/runtime-desktop/chrome/appwrap-browser-chrome.html +0 -185
- package/runtime-desktop/crates/webview-control/Cargo.lock +0 -330
- package/runtime-desktop/crates/webview-control/Cargo.toml +0 -28
- package/runtime-desktop/crates/webview-control/examples/host_harness.rs +0 -362
- package/runtime-desktop/crates/webview-control/src/embedded.rs +0 -138
- package/runtime-desktop/crates/webview-control/src/lib.rs +0 -100
- package/runtime-desktop/crates/webview-control/src/popup.rs +0 -522
- package/runtime-desktop/crates/webview-control/src/profiles.rs +0 -57
- package/runtime-desktop/crates/webview-control/src/wkwebview_backend.rs +0 -558
- package/runtime-desktop/src-tauri/Cargo.lock +0 -5474
- package/runtime-desktop/src-tauri/Cargo.toml +0 -45
- package/runtime-desktop/src-tauri/build.rs +0 -3
- package/runtime-desktop/src-tauri/capabilities/default.json +0 -6
- package/runtime-desktop/src-tauri/icons/icon.png +0 -0
- package/runtime-desktop/src-tauri/shell_config.json +0 -10
- package/runtime-desktop/src-tauri/src/biometrics_mac.rs +0 -138
- package/runtime-desktop/src-tauri/src/bridge_mac.rs +0 -424
- package/runtime-desktop/src-tauri/src/browser_tab.rs +0 -804
- package/runtime-desktop/src-tauri/src/main.rs +0 -1228
- package/runtime-desktop/src-tauri/src/notifications_mac.rs +0 -202
- package/runtime-desktop/src-tauri/src/oauth_mac.rs +0 -139
- package/runtime-desktop/src-tauri/src/plugin_host.rs +0 -522
- package/runtime-desktop/src-tauri/src/popup_mac.rs +0 -5
- package/runtime-desktop/src-tauri/src/push_mac.rs +0 -193
- package/runtime-desktop/src-tauri/src/server.rs +0 -156
- package/runtime-desktop/src-tauri/src/sidecar.rs +0 -398
- package/runtime-desktop/src-tauri/tauri.conf.json +0 -14
- package/src/desktop.ts +0 -300
- 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
|
-
}
|