tauri-agent-tools 0.6.0 → 0.7.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/.agents/skills/tauri-agent-tools/SKILL.md +94 -5
- package/.agents/skills/tauri-bridge-setup/SKILL.md +45 -13
- package/.agents/skills/tauri-debug-quickstart/SKILL.md +80 -0
- package/README.md +72 -4
- package/dist/bridge/client.d.ts +17 -1
- package/dist/bridge/client.js +82 -1
- package/dist/bridge/client.js.map +1 -1
- package/dist/bridge/tokenDiscovery.js +29 -29
- package/dist/bridge/tokenDiscovery.js.map +1 -1
- package/dist/cli.js +25 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/appPaths.d.ts +2 -0
- package/dist/commands/appPaths.js +97 -0
- package/dist/commands/appPaths.js.map +1 -0
- package/dist/commands/capabilitiesAudit.d.ts +2 -0
- package/dist/commands/capabilitiesAudit.js +105 -0
- package/dist/commands/capabilitiesAudit.js.map +1 -0
- package/dist/commands/capture.js +1 -1
- package/dist/commands/configInspect.d.ts +2 -0
- package/dist/commands/configInspect.js +223 -0
- package/dist/commands/configInspect.js.map +1 -0
- package/dist/commands/diagnose.d.ts +2 -0
- package/dist/commands/diagnose.js +311 -0
- package/dist/commands/diagnose.js.map +1 -0
- package/dist/commands/forensics.d.ts +2 -0
- package/dist/commands/forensics.js +331 -0
- package/dist/commands/forensics.js.map +1 -0
- package/dist/commands/health.d.ts +2 -0
- package/dist/commands/health.js +39 -0
- package/dist/commands/health.js.map +1 -0
- package/dist/commands/invoke.js +14 -4
- package/dist/commands/invoke.js.map +1 -1
- package/dist/commands/ipcMonitor.d.ts +3 -0
- package/dist/commands/ipcMonitor.js +22 -10
- package/dist/commands/ipcMonitor.js.map +1 -1
- package/dist/commands/osLogs.d.ts +2 -0
- package/dist/commands/osLogs.js +130 -0
- package/dist/commands/osLogs.js.map +1 -0
- package/dist/commands/pageState.d.ts +1 -0
- package/dist/commands/pageState.js +2 -2
- package/dist/commands/pageState.js.map +1 -1
- package/dist/commands/processTree.d.ts +2 -0
- package/dist/commands/processTree.js +45 -0
- package/dist/commands/processTree.js.map +1 -0
- package/dist/commands/sidecarReplay.d.ts +7 -0
- package/dist/commands/sidecarReplay.js +93 -0
- package/dist/commands/sidecarReplay.js.map +1 -0
- package/dist/commands/sidecarTap.d.ts +2 -0
- package/dist/commands/sidecarTap.js +118 -0
- package/dist/commands/sidecarTap.js.map +1 -0
- package/dist/commands/snapshot.js +1 -1
- package/dist/commands/webviewAttach.d.ts +2 -0
- package/dist/commands/webviewAttach.js +64 -0
- package/dist/commands/webviewAttach.js.map +1 -0
- package/dist/platform/oslog/darwin.d.ts +21 -0
- package/dist/platform/oslog/darwin.js +72 -0
- package/dist/platform/oslog/darwin.js.map +1 -0
- package/dist/platform/oslog/linux.d.ts +16 -0
- package/dist/platform/oslog/linux.js +47 -0
- package/dist/platform/oslog/linux.js.map +1 -0
- package/dist/platform/oslog/windows.d.ts +15 -0
- package/dist/platform/oslog/windows.js +16 -0
- package/dist/platform/oslog/windows.js.map +1 -0
- package/dist/schemas/bridge.d.ts +222 -0
- package/dist/schemas/bridge.js +44 -0
- package/dist/schemas/bridge.js.map +1 -1
- package/dist/schemas/osLog.d.ts +34 -0
- package/dist/schemas/osLog.js +18 -0
- package/dist/schemas/osLog.js.map +1 -0
- package/dist/schemas/sidecar.d.ts +33 -0
- package/dist/schemas/sidecar.js +17 -0
- package/dist/schemas/sidecar.js.map +1 -0
- package/dist/schemas/tauriConfig.d.ts +825 -0
- package/dist/schemas/tauriConfig.js +102 -0
- package/dist/schemas/tauriConfig.js.map +1 -0
- package/dist/util/ndjson.d.ts +37 -0
- package/dist/util/ndjson.js +82 -0
- package/dist/util/ndjson.js.map +1 -0
- package/dist/util/tauriConfig.d.ts +63 -0
- package/dist/util/tauriConfig.js +235 -0
- package/dist/util/tauriConfig.js.map +1 -0
- package/examples/frontend-stub/index.html +1 -0
- package/examples/tauri-bridge/Cargo.toml +6 -0
- package/examples/tauri-bridge/build.rs +3 -0
- package/examples/tauri-bridge/icons/icon.png +0 -0
- package/examples/tauri-bridge/src/dev_bridge.rs +536 -43
- package/examples/tauri-bridge/tauri.conf.json +25 -0
- package/package.json +3 -1
- package/rust-bridge/README.md +10 -1
|
@@ -2,15 +2,20 @@ use rand::Rng;
|
|
|
2
2
|
use serde::{Deserialize, Serialize};
|
|
3
3
|
use std::collections::{HashMap, VecDeque};
|
|
4
4
|
use std::fs;
|
|
5
|
-
use std::io::{BufRead, BufReader
|
|
5
|
+
use std::io::{BufRead, BufReader};
|
|
6
6
|
use std::process::{Command, Stdio};
|
|
7
7
|
use std::sync::{Arc, Condvar, Mutex};
|
|
8
8
|
use std::thread;
|
|
9
|
+
use std::time::Instant;
|
|
9
10
|
use tauri::{AppHandle, Manager};
|
|
10
11
|
use tiny_http::{Header, Response, Server};
|
|
11
12
|
use tracing_subscriber::layer::SubscriberExt;
|
|
12
13
|
use tracing_subscriber::util::SubscriberInitExt;
|
|
13
14
|
|
|
15
|
+
/// Bridge protocol version exposed via `GET /version`. Bumped whenever the
|
|
16
|
+
/// HTTP surface changes shape so CLI clients can feature-detect.
|
|
17
|
+
pub const BRIDGE_VERSION: &str = "0.7.0";
|
|
18
|
+
|
|
14
19
|
#[derive(Deserialize)]
|
|
15
20
|
struct EvalRequest {
|
|
16
21
|
js: String,
|
|
@@ -69,6 +74,71 @@ struct VersionResponse {
|
|
|
69
74
|
endpoints: Vec<String>,
|
|
70
75
|
}
|
|
71
76
|
|
|
77
|
+
#[derive(Deserialize)]
|
|
78
|
+
struct AuthedRequest {
|
|
79
|
+
token: String,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#[derive(Serialize, Clone)]
|
|
83
|
+
struct SidecarSummary {
|
|
84
|
+
name: String,
|
|
85
|
+
pid: u32,
|
|
86
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
87
|
+
exe: Option<String>,
|
|
88
|
+
args: Vec<String>,
|
|
89
|
+
alive: Option<bool>,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
#[derive(Serialize)]
|
|
93
|
+
struct ProcessResponse {
|
|
94
|
+
tauri: TauriProcessInfo,
|
|
95
|
+
sidecars: Vec<SidecarSummary>,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#[derive(Serialize)]
|
|
99
|
+
struct TauriProcessInfo {
|
|
100
|
+
pid: u32,
|
|
101
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
102
|
+
exe: Option<String>,
|
|
103
|
+
args: Vec<String>,
|
|
104
|
+
uptime_ms: u64,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#[derive(Serialize, Clone)]
|
|
108
|
+
struct CapabilityEntry {
|
|
109
|
+
identifier: String,
|
|
110
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
111
|
+
description: Option<String>,
|
|
112
|
+
windows: Vec<String>,
|
|
113
|
+
permissions: Vec<String>,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#[derive(Serialize)]
|
|
117
|
+
struct CapabilitiesResponse {
|
|
118
|
+
/// Capabilities as declared in tauri.conf.json (best-effort: tauri 2 stores
|
|
119
|
+
/// these as either bare strings or inline objects; we surface both shapes).
|
|
120
|
+
declared: Vec<CapabilityEntry>,
|
|
121
|
+
/// Window labels currently registered with Tauri.
|
|
122
|
+
windows: Vec<String>,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#[derive(Serialize)]
|
|
126
|
+
struct DevtoolsResponse {
|
|
127
|
+
platform: String,
|
|
128
|
+
inspectable: bool,
|
|
129
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
130
|
+
url: Option<String>,
|
|
131
|
+
hint: String,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#[derive(Serialize)]
|
|
135
|
+
struct HealthResponse {
|
|
136
|
+
uptime_ms: u64,
|
|
137
|
+
webview_ready: bool,
|
|
138
|
+
sidecars_alive: bool,
|
|
139
|
+
sidecars: Vec<SidecarSummary>,
|
|
140
|
+
}
|
|
141
|
+
|
|
72
142
|
#[derive(Serialize)]
|
|
73
143
|
struct TokenFile {
|
|
74
144
|
port: u16,
|
|
@@ -76,6 +146,92 @@ struct TokenFile {
|
|
|
76
146
|
pid: u32,
|
|
77
147
|
}
|
|
78
148
|
|
|
149
|
+
/// Per-process sidecar metadata captured at spawn time. Used by `/process`
|
|
150
|
+
/// and `/health` so an external diagnostic tool can see the process tree
|
|
151
|
+
/// without scraping `ps`.
|
|
152
|
+
struct SidecarRecord {
|
|
153
|
+
name: String,
|
|
154
|
+
pid: u32,
|
|
155
|
+
exe: Option<String>,
|
|
156
|
+
args: Vec<String>,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/// Thread-safe registry of sidecars known to this bridge. Populated by
|
|
160
|
+
/// `spawn_sidecar_monitored` automatically; users with their own spawn flow
|
|
161
|
+
/// can call `register_sidecar` after spawning. Aliveness is computed at
|
|
162
|
+
/// request time via a cheap signal-0 check.
|
|
163
|
+
pub struct SidecarRegistry {
|
|
164
|
+
records: Mutex<Vec<SidecarRecord>>,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
impl SidecarRegistry {
|
|
168
|
+
pub fn new() -> Self {
|
|
169
|
+
Self {
|
|
170
|
+
records: Mutex::new(Vec::new()),
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
fn add(&self, record: SidecarRecord) {
|
|
175
|
+
let mut recs = self.records.lock().unwrap();
|
|
176
|
+
recs.push(record);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
fn snapshot(&self) -> Vec<SidecarSummary> {
|
|
180
|
+
let recs = self.records.lock().unwrap();
|
|
181
|
+
recs.iter()
|
|
182
|
+
.map(|r| SidecarSummary {
|
|
183
|
+
name: r.name.clone(),
|
|
184
|
+
pid: r.pid,
|
|
185
|
+
exe: r.exe.clone(),
|
|
186
|
+
args: r.args.clone(),
|
|
187
|
+
alive: pid_alive(r.pid),
|
|
188
|
+
})
|
|
189
|
+
.collect()
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
impl Default for SidecarRegistry {
|
|
194
|
+
fn default() -> Self {
|
|
195
|
+
Self::new()
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/// Best-effort liveness probe for a sidecar PID. Returns `Some(true)` if the
|
|
200
|
+
/// process is running, `Some(false)` if it has exited, and `None` when we
|
|
201
|
+
/// can't determine (e.g., on Windows where we don't ship a probe in v1).
|
|
202
|
+
#[cfg(unix)]
|
|
203
|
+
fn pid_alive(pid: u32) -> Option<bool> {
|
|
204
|
+
// SAFETY: libc::kill with signal 0 just checks process existence and never
|
|
205
|
+
// delivers a signal. Returns 0 on success, -1 on error (e.g., ESRCH).
|
|
206
|
+
let rc = unsafe { libc::kill(pid as libc::pid_t, 0) };
|
|
207
|
+
Some(rc == 0)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#[cfg(not(unix))]
|
|
211
|
+
fn pid_alive(_pid: u32) -> Option<bool> {
|
|
212
|
+
None
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/// Register a sidecar process with the bridge so it shows up in `/process`
|
|
216
|
+
/// and `/health` responses. Callers that use `spawn_sidecar_monitored` get
|
|
217
|
+
/// this for free; callers who spawn their own children can register them
|
|
218
|
+
/// here. Idempotent in the sense that re-registering a name is allowed
|
|
219
|
+
/// (both entries will be reported).
|
|
220
|
+
pub fn register_sidecar(
|
|
221
|
+
registry: &Arc<SidecarRegistry>,
|
|
222
|
+
name: &str,
|
|
223
|
+
pid: u32,
|
|
224
|
+
exe: Option<String>,
|
|
225
|
+
args: Vec<String>,
|
|
226
|
+
) {
|
|
227
|
+
registry.add(SidecarRecord {
|
|
228
|
+
name: name.to_string(),
|
|
229
|
+
pid,
|
|
230
|
+
exe,
|
|
231
|
+
args,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
79
235
|
/// Ring buffer for log entries. Thread-safe, capped at 1000 entries.
|
|
80
236
|
pub struct LogBuffer {
|
|
81
237
|
entries: Mutex<VecDeque<LogEntry>>,
|
|
@@ -179,12 +335,15 @@ pub fn create_log_layer(
|
|
|
179
335
|
|
|
180
336
|
/// Spawn a sidecar process with monitored stdout/stderr.
|
|
181
337
|
/// Lines from stdout are logged as "info", lines from stderr as "warn".
|
|
182
|
-
/// Returns the `std::process::Child` handle.
|
|
338
|
+
/// Returns the `std::process::Child` handle. If a `SidecarRegistry` is
|
|
339
|
+
/// supplied (recommended), the child is also recorded for `/process` and
|
|
340
|
+
/// `/health` responses; pass `None` to opt out of registry tracking.
|
|
183
341
|
pub fn spawn_sidecar_monitored(
|
|
184
342
|
name: &str,
|
|
185
343
|
command: &str,
|
|
186
344
|
args: &[&str],
|
|
187
345
|
log_buffer: &Arc<LogBuffer>,
|
|
346
|
+
registry: Option<&Arc<SidecarRegistry>>,
|
|
188
347
|
) -> Result<std::process::Child, String> {
|
|
189
348
|
let mut child = Command::new(command)
|
|
190
349
|
.args(args)
|
|
@@ -193,6 +352,15 @@ pub fn spawn_sidecar_monitored(
|
|
|
193
352
|
.spawn()
|
|
194
353
|
.map_err(|e| format!("Failed to spawn sidecar {name}: {e}"))?;
|
|
195
354
|
|
|
355
|
+
if let Some(reg) = registry {
|
|
356
|
+
reg.add(SidecarRecord {
|
|
357
|
+
name: name.to_string(),
|
|
358
|
+
pid: child.id(),
|
|
359
|
+
exe: Some(command.to_string()),
|
|
360
|
+
args: args.iter().map(|s| s.to_string()).collect(),
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
196
364
|
let source = format!("sidecar:{name}");
|
|
197
365
|
|
|
198
366
|
// Monitor stdout
|
|
@@ -262,10 +430,68 @@ pub fn __dev_bridge_result(
|
|
|
262
430
|
state.notify.notify_all();
|
|
263
431
|
}
|
|
264
432
|
|
|
433
|
+
const EVAL_TIMEOUT_MESSAGE: &str = "Eval timeout: no result callback received. Re-copy examples/tauri-bridge/src/dev_bridge.rs from tauri-agent-tools 0.7.0+ and verify Tauri IPC is available.";
|
|
434
|
+
|
|
435
|
+
fn build_eval_callback_js(js: &str, request_id: &str) -> String {
|
|
436
|
+
format!(
|
|
437
|
+
r#"
|
|
438
|
+
(async () => {{
|
|
439
|
+
const __getDevBridgeInvoke = () => {{
|
|
440
|
+
if (window.__TAURI_INTERNALS__ && typeof window.__TAURI_INTERNALS__.invoke === "function") {{
|
|
441
|
+
return window.__TAURI_INTERNALS__.invoke.bind(window.__TAURI_INTERNALS__);
|
|
442
|
+
}}
|
|
443
|
+
if (window.__TAURI__ && window.__TAURI__.core && typeof window.__TAURI__.core.invoke === "function") {{
|
|
444
|
+
return window.__TAURI__.core.invoke.bind(window.__TAURI__.core);
|
|
445
|
+
}}
|
|
446
|
+
return null;
|
|
447
|
+
}};
|
|
448
|
+
|
|
449
|
+
let __devBridgeInvoke = __getDevBridgeInvoke();
|
|
450
|
+
try {{
|
|
451
|
+
if (!__devBridgeInvoke) {{
|
|
452
|
+
throw new Error("Tauri invoke API not found: expected window.__TAURI_INTERNALS__.invoke or window.__TAURI__.core.invoke");
|
|
453
|
+
}}
|
|
454
|
+
let __result = await eval({js});
|
|
455
|
+
if (typeof __result === "undefined") {{
|
|
456
|
+
__result = null;
|
|
457
|
+
}} else if (typeof __result === "object" && __result !== null) {{
|
|
458
|
+
__result = JSON.stringify(__result);
|
|
459
|
+
}} else if (typeof __result !== "string") {{
|
|
460
|
+
__result = String(__result);
|
|
461
|
+
}}
|
|
462
|
+
await __devBridgeInvoke("__dev_bridge_result", {{
|
|
463
|
+
id: {id},
|
|
464
|
+
value: __result
|
|
465
|
+
}});
|
|
466
|
+
}} catch(e) {{
|
|
467
|
+
__devBridgeInvoke = __devBridgeInvoke || __getDevBridgeInvoke();
|
|
468
|
+
if (!__devBridgeInvoke) {{
|
|
469
|
+
throw e;
|
|
470
|
+
}}
|
|
471
|
+
const __message = e && e.message ? e.message : String(e);
|
|
472
|
+
await __devBridgeInvoke("__dev_bridge_result", {{
|
|
473
|
+
id: {id},
|
|
474
|
+
value: "ERROR: " + __message
|
|
475
|
+
}});
|
|
476
|
+
}}
|
|
477
|
+
}})();
|
|
478
|
+
"#,
|
|
479
|
+
js = serde_json::to_string(js).unwrap(),
|
|
480
|
+
id = serde_json::to_string(request_id).unwrap(),
|
|
481
|
+
)
|
|
482
|
+
}
|
|
483
|
+
|
|
265
484
|
/// Start the development bridge HTTP server.
|
|
266
|
-
///
|
|
267
|
-
///
|
|
268
|
-
|
|
485
|
+
///
|
|
486
|
+
/// Returns the bound port, a shared log buffer, and a sidecar registry. Both
|
|
487
|
+
/// the buffer and registry are intended to be passed back to
|
|
488
|
+
/// `spawn_sidecar_monitored` for any sidecar processes you launch; the
|
|
489
|
+
/// registry is what powers the `/process` and `/health` endpoints' visibility
|
|
490
|
+
/// into the process tree. Callers that don't spawn sidecars can ignore the
|
|
491
|
+
/// registry handle.
|
|
492
|
+
pub fn start_bridge(
|
|
493
|
+
app: &AppHandle,
|
|
494
|
+
) -> Result<(u16, Arc<LogBuffer>, Arc<SidecarRegistry>), String> {
|
|
269
495
|
let server =
|
|
270
496
|
Server::http("127.0.0.1:0").map_err(|e| format!("Failed to start bridge: {e}"))?;
|
|
271
497
|
let port = server
|
|
@@ -311,27 +537,47 @@ pub fn start_bridge(app: &AppHandle) -> Result<(u16, Arc<LogBuffer>), String> {
|
|
|
311
537
|
});
|
|
312
538
|
app.manage(pending.clone());
|
|
313
539
|
|
|
540
|
+
// Sidecar registry — exposed to integrators via the return tuple and
|
|
541
|
+
// consulted by /process and /health.
|
|
542
|
+
let sidecar_registry = Arc::new(SidecarRegistry::new());
|
|
543
|
+
app.manage(sidecar_registry.clone());
|
|
544
|
+
|
|
545
|
+
// Capture process start metadata once so /process and /health don't pay
|
|
546
|
+
// for the lookup on every request.
|
|
547
|
+
let start_instant = Instant::now();
|
|
548
|
+
let tauri_pid = std::process::id();
|
|
549
|
+
let tauri_exe = std::env::current_exe()
|
|
550
|
+
.ok()
|
|
551
|
+
.and_then(|p| p.to_str().map(|s| s.to_string()));
|
|
552
|
+
let tauri_args: Vec<String> = std::env::args().collect();
|
|
553
|
+
|
|
314
554
|
let app_handle = app.clone();
|
|
315
555
|
let expected_token = token.clone();
|
|
316
556
|
let server_log_buffer = log_buffer.clone();
|
|
557
|
+
let server_registry = sidecar_registry.clone();
|
|
317
558
|
|
|
318
559
|
thread::spawn(move || {
|
|
319
560
|
// Keep _guard alive for the lifetime of the server thread
|
|
320
561
|
let _cleanup = _guard;
|
|
321
562
|
|
|
322
|
-
for request in server.incoming_requests() {
|
|
563
|
+
for mut request in server.incoming_requests() {
|
|
323
564
|
let is_post = request.method().as_str() == "POST";
|
|
324
565
|
let url = request.url().to_string();
|
|
325
566
|
|
|
326
|
-
// Handle GET /version (no auth needed)
|
|
567
|
+
// Handle GET /version (no auth needed). Clients feature-detect
|
|
568
|
+
// newer endpoints by checking the `endpoints` array.
|
|
327
569
|
if url == "/version" && request.method().as_str() == "GET" {
|
|
328
570
|
let resp = VersionResponse {
|
|
329
|
-
version:
|
|
571
|
+
version: BRIDGE_VERSION.to_string(),
|
|
330
572
|
endpoints: vec![
|
|
331
573
|
"/eval".to_string(),
|
|
332
574
|
"/logs".to_string(),
|
|
333
575
|
"/describe".to_string(),
|
|
334
576
|
"/version".to_string(),
|
|
577
|
+
"/process".to_string(),
|
|
578
|
+
"/capabilities".to_string(),
|
|
579
|
+
"/devtools".to_string(),
|
|
580
|
+
"/health".to_string(),
|
|
335
581
|
],
|
|
336
582
|
};
|
|
337
583
|
let json = serde_json::to_string(&resp).unwrap();
|
|
@@ -340,7 +586,11 @@ pub fn start_bridge(app: &AppHandle) -> Result<(u16, Arc<LogBuffer>), String> {
|
|
|
340
586
|
continue;
|
|
341
587
|
}
|
|
342
588
|
|
|
343
|
-
|
|
589
|
+
let known_post = matches!(
|
|
590
|
+
url.as_str(),
|
|
591
|
+
"/eval" | "/logs" | "/describe" | "/process" | "/capabilities" | "/devtools" | "/health"
|
|
592
|
+
);
|
|
593
|
+
if !is_post || !known_post {
|
|
344
594
|
let _ = request.respond(Response::from_string("Not found").with_status_code(404));
|
|
345
595
|
continue;
|
|
346
596
|
}
|
|
@@ -378,6 +628,113 @@ pub fn start_bridge(app: &AppHandle) -> Result<(u16, Arc<LogBuffer>), String> {
|
|
|
378
628
|
continue;
|
|
379
629
|
}
|
|
380
630
|
|
|
631
|
+
// Handle /process endpoint — Tauri PID + sidecar registry snapshot.
|
|
632
|
+
if url == "/process" {
|
|
633
|
+
let req: AuthedRequest = match serde_json::from_str(&body) {
|
|
634
|
+
Ok(r) => r,
|
|
635
|
+
Err(_) => {
|
|
636
|
+
let _ = request
|
|
637
|
+
.respond(Response::from_string("Invalid JSON").with_status_code(400));
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
if req.token != expected_token {
|
|
642
|
+
let _ = request
|
|
643
|
+
.respond(Response::from_string("Unauthorized").with_status_code(401));
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
let resp = ProcessResponse {
|
|
647
|
+
tauri: TauriProcessInfo {
|
|
648
|
+
pid: tauri_pid,
|
|
649
|
+
exe: tauri_exe.clone(),
|
|
650
|
+
args: tauri_args.clone(),
|
|
651
|
+
uptime_ms: start_instant.elapsed().as_millis() as u64,
|
|
652
|
+
},
|
|
653
|
+
sidecars: server_registry.snapshot(),
|
|
654
|
+
};
|
|
655
|
+
let json = serde_json::to_string(&resp).unwrap();
|
|
656
|
+
let header = Header::from_bytes("Content-Type", "application/json").unwrap();
|
|
657
|
+
let _ = request.respond(Response::from_string(json).with_header(header));
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Handle /capabilities endpoint — declared Tauri capability set per window.
|
|
662
|
+
if url == "/capabilities" {
|
|
663
|
+
let req: AuthedRequest = match serde_json::from_str(&body) {
|
|
664
|
+
Ok(r) => r,
|
|
665
|
+
Err(_) => {
|
|
666
|
+
let _ = request
|
|
667
|
+
.respond(Response::from_string("Invalid JSON").with_status_code(400));
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
if req.token != expected_token {
|
|
672
|
+
let _ = request
|
|
673
|
+
.respond(Response::from_string("Unauthorized").with_status_code(401));
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
let windows: Vec<String> = app_handle.webview_windows().keys().cloned().collect();
|
|
678
|
+
let declared = collect_declared_capabilities(&app_handle);
|
|
679
|
+
let resp = CapabilitiesResponse { declared, windows };
|
|
680
|
+
let json = serde_json::to_string(&resp).unwrap();
|
|
681
|
+
let header = Header::from_bytes("Content-Type", "application/json").unwrap();
|
|
682
|
+
let _ = request.respond(Response::from_string(json).with_header(header));
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Handle /devtools endpoint — inspector URL or platform hint.
|
|
687
|
+
if url == "/devtools" {
|
|
688
|
+
let req: AuthedRequest = match serde_json::from_str(&body) {
|
|
689
|
+
Ok(r) => r,
|
|
690
|
+
Err(_) => {
|
|
691
|
+
let _ = request
|
|
692
|
+
.respond(Response::from_string("Invalid JSON").with_status_code(400));
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
if req.token != expected_token {
|
|
697
|
+
let _ = request
|
|
698
|
+
.respond(Response::from_string("Unauthorized").with_status_code(401));
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
let resp = devtools_response();
|
|
702
|
+
let json = serde_json::to_string(&resp).unwrap();
|
|
703
|
+
let header = Header::from_bytes("Content-Type", "application/json").unwrap();
|
|
704
|
+
let _ = request.respond(Response::from_string(json).with_header(header));
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Handle /health endpoint — quick "is this app sick" check.
|
|
709
|
+
if url == "/health" {
|
|
710
|
+
let req: AuthedRequest = match serde_json::from_str(&body) {
|
|
711
|
+
Ok(r) => r,
|
|
712
|
+
Err(_) => {
|
|
713
|
+
let _ = request
|
|
714
|
+
.respond(Response::from_string("Invalid JSON").with_status_code(400));
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
if req.token != expected_token {
|
|
719
|
+
let _ = request
|
|
720
|
+
.respond(Response::from_string("Unauthorized").with_status_code(401));
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
let sidecars = server_registry.snapshot();
|
|
724
|
+
let sidecars_alive = sidecars.iter().all(|s| matches!(s.alive, Some(true) | None));
|
|
725
|
+
let webview_ready = !app_handle.webview_windows().is_empty();
|
|
726
|
+
let resp = HealthResponse {
|
|
727
|
+
uptime_ms: start_instant.elapsed().as_millis() as u64,
|
|
728
|
+
webview_ready,
|
|
729
|
+
sidecars_alive,
|
|
730
|
+
sidecars,
|
|
731
|
+
};
|
|
732
|
+
let json = serde_json::to_string(&resp).unwrap();
|
|
733
|
+
let header = Header::from_bytes("Content-Type", "application/json").unwrap();
|
|
734
|
+
let _ = request.respond(Response::from_string(json).with_header(header));
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
|
|
381
738
|
// Handle /describe endpoint
|
|
382
739
|
if url == "/describe" {
|
|
383
740
|
let desc_req: DescribeRequest = match serde_json::from_str(&body) {
|
|
@@ -441,36 +798,18 @@ pub fn start_bridge(app: &AppHandle) -> Result<(u16, Arc<LogBuffer>), String> {
|
|
|
441
798
|
let window_label = eval_req.window.as_deref().unwrap_or("main");
|
|
442
799
|
if let Some(window) = app_handle.get_webview_window(window_label) {
|
|
443
800
|
// Build JS that evaluates the expression, then calls back into Rust
|
|
444
|
-
// via
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
}}
|
|
457
|
-
await window.__TAURI__.core.invoke("__dev_bridge_result", {{
|
|
458
|
-
id: {id},
|
|
459
|
-
value: __result
|
|
460
|
-
}});
|
|
461
|
-
}} catch(e) {{
|
|
462
|
-
await window.__TAURI__.core.invoke("__dev_bridge_result", {{
|
|
463
|
-
id: {id},
|
|
464
|
-
value: "ERROR: " + e.message
|
|
465
|
-
}});
|
|
466
|
-
}}
|
|
467
|
-
}})();
|
|
468
|
-
"#,
|
|
469
|
-
js = serde_json::to_string(&eval_req.js).unwrap(),
|
|
470
|
-
id = serde_json::to_string(&request_id).unwrap(),
|
|
471
|
-
);
|
|
472
|
-
|
|
473
|
-
let _ = window.eval(&callback_js);
|
|
801
|
+
// via Tauri's invoke API to deliver the result. Prefer the
|
|
802
|
+
// internal global because Tauri 2 does not expose __TAURI__
|
|
803
|
+
// unless app.withGlobalTauri is enabled.
|
|
804
|
+
let callback_js = build_eval_callback_js(&eval_req.js, &request_id);
|
|
805
|
+
|
|
806
|
+
if let Err(e) = window.eval(&callback_js) {
|
|
807
|
+
let _ = request.respond(
|
|
808
|
+
Response::from_string(format!("Eval injection failed: {e}"))
|
|
809
|
+
.with_status_code(500),
|
|
810
|
+
);
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
474
813
|
|
|
475
814
|
// Wait for the result with a 5-second timeout
|
|
476
815
|
let mut results = pending.results.lock().unwrap();
|
|
@@ -493,7 +832,7 @@ pub fn start_bridge(app: &AppHandle) -> Result<(u16, Arc<LogBuffer>), String> {
|
|
|
493
832
|
// Timeout — clean up and respond with 504
|
|
494
833
|
results.remove(&request_id);
|
|
495
834
|
let _ = request.respond(
|
|
496
|
-
Response::from_string(
|
|
835
|
+
Response::from_string(EVAL_TIMEOUT_MESSAGE).with_status_code(504),
|
|
497
836
|
);
|
|
498
837
|
break;
|
|
499
838
|
}
|
|
@@ -506,7 +845,7 @@ pub fn start_bridge(app: &AppHandle) -> Result<(u16, Arc<LogBuffer>), String> {
|
|
|
506
845
|
if timeout_result.timed_out() && !results.contains_key(&request_id) {
|
|
507
846
|
results.remove(&request_id);
|
|
508
847
|
let _ = request.respond(
|
|
509
|
-
Response::from_string(
|
|
848
|
+
Response::from_string(EVAL_TIMEOUT_MESSAGE).with_status_code(504),
|
|
510
849
|
);
|
|
511
850
|
break;
|
|
512
851
|
}
|
|
@@ -522,8 +861,162 @@ pub fn start_bridge(app: &AppHandle) -> Result<(u16, Arc<LogBuffer>), String> {
|
|
|
522
861
|
}
|
|
523
862
|
});
|
|
524
863
|
|
|
525
|
-
eprintln!("Dev bridge started on port {port}");
|
|
864
|
+
eprintln!("Dev bridge {BRIDGE_VERSION} started on port {port}");
|
|
526
865
|
eprintln!("Token file: {token_path}");
|
|
527
866
|
|
|
528
|
-
Ok((port, log_buffer))
|
|
867
|
+
Ok((port, log_buffer, sidecar_registry))
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
#[cfg(test)]
|
|
871
|
+
mod tests {
|
|
872
|
+
use super::*;
|
|
873
|
+
|
|
874
|
+
#[test]
|
|
875
|
+
fn eval_callback_prefers_tauri_internals() {
|
|
876
|
+
let script = build_eval_callback_js("document.title", "request-1");
|
|
877
|
+
let internals = script.find("window.__TAURI_INTERNALS__.invoke").unwrap();
|
|
878
|
+
let global = script.find("window.__TAURI__.core.invoke").unwrap();
|
|
879
|
+
|
|
880
|
+
assert!(internals < global);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
#[test]
|
|
884
|
+
fn eval_callback_keeps_global_tauri_fallback() {
|
|
885
|
+
let script = build_eval_callback_js("document.title", "request-1");
|
|
886
|
+
|
|
887
|
+
assert!(script.contains("window.__TAURI__.core.invoke"));
|
|
888
|
+
assert!(!script.contains("app.withGlobalTauri"));
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
#[test]
|
|
892
|
+
fn eval_callback_safely_embeds_js_and_request_id() {
|
|
893
|
+
let js = r#"document.querySelector("[data-name=\"x\"]").textContent"#;
|
|
894
|
+
let request_id = r#"request-"quoted""#;
|
|
895
|
+
let script = build_eval_callback_js(js, request_id);
|
|
896
|
+
|
|
897
|
+
assert!(script.contains(&serde_json::to_string(js).unwrap()));
|
|
898
|
+
assert!(script.contains(&serde_json::to_string(request_id).unwrap()));
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
#[test]
|
|
902
|
+
fn eval_callback_uses_dev_bridge_result_command() {
|
|
903
|
+
let script = build_eval_callback_js("1 + 1", "request-1");
|
|
904
|
+
|
|
905
|
+
assert!(script.contains("__dev_bridge_result"));
|
|
906
|
+
assert!(!script.contains("await window.__TAURI__.core.invoke"));
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
#[test]
|
|
910
|
+
fn eval_timeout_message_is_actionable() {
|
|
911
|
+
assert!(EVAL_TIMEOUT_MESSAGE.contains("no result callback received"));
|
|
912
|
+
assert!(EVAL_TIMEOUT_MESSAGE.contains("Re-copy"));
|
|
913
|
+
assert!(EVAL_TIMEOUT_MESSAGE.contains("dev_bridge.rs"));
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/// Read declared capabilities from tauri.conf.json via `app.config()`. Returns
|
|
918
|
+
/// a flat list of capability entries. Tauri 2 lets capabilities be either bare
|
|
919
|
+
/// permission identifiers (strings) or full inline definitions; we surface
|
|
920
|
+
/// both as `CapabilityEntry` rows with `permissions` populated where possible.
|
|
921
|
+
fn collect_declared_capabilities(app: &AppHandle) -> Vec<CapabilityEntry> {
|
|
922
|
+
let config = app.config();
|
|
923
|
+
let security = &config.app.security;
|
|
924
|
+
let mut out = Vec::new();
|
|
925
|
+
for cap in &security.capabilities {
|
|
926
|
+
let raw = serde_json::to_value(cap).unwrap_or(serde_json::Value::Null);
|
|
927
|
+
match &raw {
|
|
928
|
+
serde_json::Value::String(s) => {
|
|
929
|
+
// Capability declared by reference to a JSON file. We don't have
|
|
930
|
+
// the resolved contents at runtime here, but we surface the
|
|
931
|
+
// identifier so callers know what was requested.
|
|
932
|
+
out.push(CapabilityEntry {
|
|
933
|
+
identifier: s.clone(),
|
|
934
|
+
description: None,
|
|
935
|
+
windows: Vec::new(),
|
|
936
|
+
permissions: Vec::new(),
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
serde_json::Value::Object(map) => {
|
|
940
|
+
let identifier = map
|
|
941
|
+
.get("identifier")
|
|
942
|
+
.and_then(|v| v.as_str())
|
|
943
|
+
.unwrap_or("<inline>")
|
|
944
|
+
.to_string();
|
|
945
|
+
let description = map
|
|
946
|
+
.get("description")
|
|
947
|
+
.and_then(|v| v.as_str())
|
|
948
|
+
.map(|s| s.to_string());
|
|
949
|
+
let windows: Vec<String> = map
|
|
950
|
+
.get("windows")
|
|
951
|
+
.and_then(|v| v.as_array())
|
|
952
|
+
.map(|arr| {
|
|
953
|
+
arr.iter()
|
|
954
|
+
.filter_map(|v| v.as_str().map(|s| s.to_string()))
|
|
955
|
+
.collect()
|
|
956
|
+
})
|
|
957
|
+
.unwrap_or_default();
|
|
958
|
+
let permissions: Vec<String> = map
|
|
959
|
+
.get("permissions")
|
|
960
|
+
.and_then(|v| v.as_array())
|
|
961
|
+
.map(|arr| {
|
|
962
|
+
arr.iter()
|
|
963
|
+
.filter_map(|v| match v {
|
|
964
|
+
serde_json::Value::String(s) => Some(s.clone()),
|
|
965
|
+
serde_json::Value::Object(o) => o
|
|
966
|
+
.get("identifier")
|
|
967
|
+
.and_then(|i| i.as_str())
|
|
968
|
+
.map(|s| s.to_string()),
|
|
969
|
+
_ => None,
|
|
970
|
+
})
|
|
971
|
+
.collect()
|
|
972
|
+
})
|
|
973
|
+
.unwrap_or_default();
|
|
974
|
+
out.push(CapabilityEntry {
|
|
975
|
+
identifier,
|
|
976
|
+
description,
|
|
977
|
+
windows,
|
|
978
|
+
permissions,
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
_ => {}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
out
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/// Build a `/devtools` response for the current platform. v1 emits useful
|
|
988
|
+
/// hints rather than always producing a hot inspector URL — Safari attach on
|
|
989
|
+
/// macOS requires UI activation, Windows WebView2 needs a launch-time arg.
|
|
990
|
+
fn devtools_response() -> DevtoolsResponse {
|
|
991
|
+
if cfg!(target_os = "macos") {
|
|
992
|
+
DevtoolsResponse {
|
|
993
|
+
platform: "wkwebview".to_string(),
|
|
994
|
+
inspectable: cfg!(debug_assertions),
|
|
995
|
+
url: None,
|
|
996
|
+
hint: "Open Safari > Develop > <Mac name> > <App name> to attach. \
|
|
997
|
+
Requires the app to be built with debug_assertions (i.e., `tauri dev`)."
|
|
998
|
+
.to_string(),
|
|
999
|
+
}
|
|
1000
|
+
} else if cfg!(target_os = "windows") {
|
|
1001
|
+
let port = std::env::var("WEBVIEW2_REMOTE_DEBUGGING_PORT").ok();
|
|
1002
|
+
let url = port.as_ref().map(|p| format!("http://127.0.0.1:{p}"));
|
|
1003
|
+
DevtoolsResponse {
|
|
1004
|
+
platform: "webview2".to_string(),
|
|
1005
|
+
inspectable: url.is_some(),
|
|
1006
|
+
url,
|
|
1007
|
+
hint: "Set WEBVIEW2_REMOTE_DEBUGGING_PORT=9222 before launching, \
|
|
1008
|
+
then open http://127.0.0.1:9222 in Chrome/Edge to inspect."
|
|
1009
|
+
.to_string(),
|
|
1010
|
+
}
|
|
1011
|
+
} else {
|
|
1012
|
+
let inspector = std::env::var("WEBKIT_INSPECTOR_SERVER").ok();
|
|
1013
|
+
DevtoolsResponse {
|
|
1014
|
+
platform: "webkitgtk".to_string(),
|
|
1015
|
+
inspectable: inspector.is_some(),
|
|
1016
|
+
url: inspector.as_ref().map(|s| format!("http://{s}")),
|
|
1017
|
+
hint: "Export WEBKIT_INSPECTOR_SERVER=127.0.0.1:9222 before launching, \
|
|
1018
|
+
then open http://127.0.0.1:9222 to inspect."
|
|
1019
|
+
.to_string(),
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
529
1022
|
}
|