Skip to content

Commit 0f41838

Browse files
Fix WebUI sync bridge for module state loading
1 parent 06f98ce commit 0f41838

2 files changed

Lines changed: 152 additions & 21 deletions

File tree

desktop/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@ serde = { version = "1.0", features = ["derive"] }
1515
serde_json = "1.0"
1616
shell-words = "1.1"
1717
urlencoding = "2.1"
18-
webkit2gtk = { version = "2.0.2", features = ["v2_22"] }
18+
webkit2gtk = { version = "2.0.2", features = ["v2_24"] }

desktop/src/bin/abk-webui.rs

Lines changed: 151 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
use anyhow::{anyhow, Result};
22
use gtk3::prelude::*;
33
use serde_json::to_string;
4+
use serde_json::{json, Value};
45
use urlencoding::encode;
56
use webkit2gtk::{
6-
LoadEvent, SettingsExt, UserContentInjectedFrames, UserContentManager, UserContentManagerExt,
7-
UserScript, UserScriptInjectionTime, WebView, WebViewExt,
7+
LoadEvent, ScriptDialogType, SettingsExt, UserContentInjectedFrames, UserContentManager,
8+
UserContentManagerExt, UserScript, UserScriptInjectionTime, WebView, WebViewExt,
89
};
910

1011
fn main() {
@@ -28,7 +29,8 @@ fn run_module_webui_window(port: u16, module_id: &str, module_name: &str) -> Res
2829

2930
let encoded_id = encode(module_id.trim());
3031
let bridge_base = format!("http://127.0.0.1:{port}/api/v1/runtime/modules/{encoded_id}/webui");
31-
let page_url = format!("{bridge_base}/files");
32+
let page_base_url = format!("{bridge_base}/files/");
33+
let page_url = format!("{page_base_url}index.html");
3234
let title = if module_name.trim().is_empty() {
3335
format!("Module WebUI · {module_id}")
3436
} else {
@@ -59,7 +61,30 @@ fn run_module_webui_window(port: u16, module_id: &str, module_name: &str) -> Res
5961
}
6062

6163
{
62-
let page_url = page_url.clone();
64+
let module_id = module_id.to_string();
65+
webview.connect_script_dialog(move |_view, dialog| {
66+
if dialog.dialog_type() != ScriptDialogType::Prompt {
67+
return false;
68+
}
69+
let Some(message) = dialog.message().map(|value| value.to_string()) else {
70+
return false;
71+
};
72+
let Some(method) = message.strip_prefix("__abk__:") else {
73+
return false;
74+
};
75+
let payload = dialog
76+
.prompt_get_default_text()
77+
.map(|value| value.to_string())
78+
.unwrap_or_default();
79+
let response = handle_sync_bridge_call(port, &module_id, method, &payload)
80+
.unwrap_or_else(|error| error.to_string());
81+
dialog.prompt_set_text(&response);
82+
true
83+
});
84+
}
85+
86+
{
87+
let page_base_url = page_base_url.clone();
6388
webview.connect_load_failed(move |view, event, uri, error| {
6489
if event == LoadEvent::Finished {
6590
return false;
@@ -69,7 +94,7 @@ fn run_module_webui_window(port: u16, module_id: &str, module_name: &str) -> Res
6994
"Module WebUI load failed",
7095
&format!("{uri}\n\n{}", error.message()),
7196
),
72-
Some(&page_url),
97+
Some(&page_base_url),
7398
);
7499
true
75100
});
@@ -141,11 +166,19 @@ fn build_ksu_bridge_script(bridge_base: &str) -> Result<String> {
141166
const bridgeBase = {base};
142167
const rootBase = new URL(bridgeBase).origin;
143168
const packageIconBase = rootBase + "/api/v1/root-grants/";
169+
let moduleInfoCache = null;
144170
145171
function buildUrl(path) {{
146172
if (typeof path === "string" && /^(https?:)?\/\//.test(path)) {{
147173
return path;
148174
}}
175+
if (typeof path === "string" && (
176+
path === "/exec" ||
177+
path === "/spawn" ||
178+
path === "/module-info"
179+
)) {{
180+
return bridgeBase + path;
181+
}}
149182
if (typeof path === "string" && path.startsWith("/")) {{
150183
return rootBase + path;
151184
}}
@@ -231,6 +264,14 @@ fn build_ksu_bridge_script(bridge_base: &str) -> Result<String> {
231264
return payload;
232265
}}
233266
267+
function syncNative(method, payload) {{
268+
try {{
269+
return window.prompt(`__abk__:${{method}}`, JSON.stringify(payload ?? null)) ?? "";
270+
}} catch (_error) {{
271+
return "";
272+
}}
273+
}}
274+
234275
function resolveCallback(callbackRef) {{
235276
if (typeof callbackRef === "function") {{
236277
return callbackRef;
@@ -276,13 +317,20 @@ fn build_ksu_bridge_script(bridge_base: &str) -> Result<String> {
276317
callback && maybeCallback === undefined ? undefined : optionsOrCallback;
277318
278319
if (!callback) {{
279-
const payload = syncRequest("POST", "/exec", {{ command, options }});
280-
return normalizeOutput(payload);
320+
return syncNative("exec", {{ command, options }});
281321
}}
282322
283323
asyncRequest("POST", "/exec", {{ command, options }})
284-
.then((payload) => callback(payload.code ?? 0, normalizeOutput(payload), ""))
285-
.catch((error) => callback(1, String(error), ""));
324+
.then((payload) => {{
325+
const output = normalizeOutput(payload);
326+
const isOk = payload && payload.success !== false && (payload.code ?? 0) === 0;
327+
callback(
328+
payload.code ?? (isOk ? 0 : 1),
329+
isOk ? output : "",
330+
isOk ? "" : (payload.stderr || output || "command failed")
331+
);
332+
}})
333+
.catch((error) => callback(1, "", String(error)));
286334
}}
287335
288336
function spawn(command, args, options, callbackRef) {{
@@ -313,8 +361,11 @@ fn build_ksu_bridge_script(bridge_base: &str) -> Result<String> {
313361
}}
314362
315363
function moduleInfo() {{
316-
const payload = syncRequest("GET", "/module-info");
317-
return payload.raw || JSON.stringify(payload.info || {{}});
364+
if (moduleInfoCache !== null) {{
365+
return moduleInfoCache;
366+
}}
367+
moduleInfoCache = syncNative("moduleInfo");
368+
return moduleInfoCache;
318369
}}
319370
320371
function moduleInfoObject() {{
@@ -328,9 +379,15 @@ fn build_ksu_bridge_script(bridge_base: &str) -> Result<String> {
328379
function fullScreen(enabled) {{
329380
try {{
330381
if (enabled) {{
331-
document.documentElement.requestFullscreen?.();
382+
const result = document.documentElement.requestFullscreen?.();
383+
if (result && typeof result.catch === "function") {{
384+
result.catch(() => {{}});
385+
}}
332386
}} else {{
333-
document.exitFullscreen?.();
387+
const result = document.exitFullscreen?.();
388+
if (result && typeof result.catch === "function") {{
389+
result.catch(() => {{}});
390+
}}
334391
}}
335392
}} catch (_error) {{
336393
}}
@@ -340,11 +397,7 @@ fn build_ksu_bridge_script(bridge_base: &str) -> Result<String> {
340397
}}
341398
342399
function listPackages(type) {{
343-
const payload = syncRequest(
344-
"GET",
345-
"/api/v1/packages?type=" + encodeURIComponent(type || "all")
346-
);
347-
return JSON.stringify(payload.packages || []);
400+
return syncNative("listPackages", {{ type: type || "all" }});
348401
}}
349402
350403
function getPackagesInfo(packages) {{
@@ -356,10 +409,9 @@ fn build_ksu_bridge_script(bridge_base: &str) -> Result<String> {
356409
values = [];
357410
}}
358411
}}
359-
const payload = syncRequest("POST", "/api/v1/packages/info", {{
412+
return syncNative("getPackagesInfo", {{
360413
packages: Array.isArray(values) ? values : [],
361414
}});
362-
return JSON.stringify(payload.packages || []);
363415
}}
364416
365417
const originalFetch = window.fetch?.bind(window);
@@ -529,6 +581,85 @@ fn html_escape(value: &str) -> String {
529581
.replace('>', "&gt;")
530582
}
531583

584+
fn handle_sync_bridge_call(
585+
port: u16,
586+
module_id: &str,
587+
method: &str,
588+
payload_raw: &str,
589+
) -> Result<String> {
590+
let client = reqwest::blocking::Client::builder()
591+
.timeout(std::time::Duration::from_secs(20))
592+
.build()?;
593+
let encoded_module_id = encode(module_id);
594+
let bridge_base =
595+
format!("http://127.0.0.1:{port}/api/v1/runtime/modules/{encoded_module_id}/webui");
596+
let payload = if payload_raw.trim().is_empty() {
597+
Value::Null
598+
} else {
599+
serde_json::from_str::<Value>(payload_raw).unwrap_or(Value::Null)
600+
};
601+
602+
match method {
603+
"moduleInfo" => {
604+
let response = client.get(format!("{bridge_base}/module-info")).send()?;
605+
let value = response.json::<Value>()?;
606+
Ok(value
607+
.get("raw")
608+
.and_then(Value::as_str)
609+
.unwrap_or("{}")
610+
.to_string())
611+
}
612+
"listPackages" => {
613+
let package_type = payload.get("type").and_then(Value::as_str).unwrap_or("all");
614+
let response = client
615+
.get(format!(
616+
"http://127.0.0.1:{port}/api/v1/packages?type={}",
617+
encode(package_type)
618+
))
619+
.send()?;
620+
let value = response.json::<Value>()?;
621+
Ok(serde_json::to_string(
622+
value.get("packages").unwrap_or(&Value::Array(vec![])),
623+
)?)
624+
}
625+
"getPackagesInfo" => {
626+
let packages = payload
627+
.get("packages")
628+
.cloned()
629+
.unwrap_or_else(|| Value::Array(vec![]));
630+
let response = client
631+
.post(format!("http://127.0.0.1:{port}/api/v1/packages/info"))
632+
.json(&json!({ "packages": packages }))
633+
.send()?;
634+
let value = response.json::<Value>()?;
635+
Ok(serde_json::to_string(
636+
value.get("packages").unwrap_or(&Value::Array(vec![])),
637+
)?)
638+
}
639+
"exec" => {
640+
let command = payload
641+
.get("command")
642+
.and_then(Value::as_str)
643+
.unwrap_or_default();
644+
let options = payload.get("options").cloned().unwrap_or(Value::Null);
645+
let response = client
646+
.post(format!("{bridge_base}/exec"))
647+
.json(&json!({
648+
"command": command,
649+
"options": if options.is_null() { Value::Null } else { options }
650+
}))
651+
.send()?;
652+
let value = response.json::<Value>()?;
653+
Ok(value
654+
.get("stdout")
655+
.and_then(Value::as_str)
656+
.unwrap_or_default()
657+
.to_string())
658+
}
659+
other => Err(anyhow!("unsupported sync bridge method: {other}")),
660+
}
661+
}
662+
532663
#[derive(Debug, Clone)]
533664
struct CliArgs {
534665
port: u16,

0 commit comments

Comments
 (0)