create-ekka-desktop-app 0.3.12 → 0.4.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/bin/cli.js +5 -1
- package/package.json +1 -1
- package/template/src/demo/layout/Sidebar.tsx +2 -1
- package/template/src/demo/pages/LoginPage.tsx +2 -1
- package/template/src-tauri/Cargo.toml +19 -1
- package/template/src-tauri/resources/ekka-engine-bootstrap +0 -0
- package/template/src-tauri/src/commands.rs +150 -35
- package/template/src-tauri/src/config.rs +33 -0
- package/template/src-tauri/src/engine_process.rs +123 -125
- package/template/src-tauri/src/main.rs +270 -369
- package/template/src-tauri/src/node_runner.rs +283 -140
- package/template/src-tauri/src/state.rs +0 -15
- package/template/src-tauri/src/well_known.rs +6 -1
|
@@ -1,27 +1,30 @@
|
|
|
1
1
|
//! Engine Process Management
|
|
2
2
|
//!
|
|
3
|
-
//! Handles spawning
|
|
3
|
+
//! Handles spawning and readiness checking for the external ekka-engine binary.
|
|
4
4
|
//!
|
|
5
5
|
//! ═══════════════════════════════════════════════════════════════════════════════════════════
|
|
6
|
-
//!
|
|
6
|
+
//! ARCHITECTURE NOTE
|
|
7
7
|
//! ═══════════════════════════════════════════════════════════════════════════════════════════
|
|
8
|
-
//!
|
|
8
|
+
//! The spawned engine is a RUNNER RUNTIME (ekka-runner-local), NOT a request router.
|
|
9
|
+
//! It provides:
|
|
10
|
+
//! - /health endpoint for readiness check
|
|
11
|
+
//! - Task execution via runner_tasks_v2 polling
|
|
9
12
|
//!
|
|
10
|
-
//!
|
|
13
|
+
//! It does NOT provide:
|
|
14
|
+
//! - /request endpoint (removed - never existed in runner)
|
|
15
|
+
//! - Command routing (all commands go to local Rust handlers + cloud API)
|
|
16
|
+
//!
|
|
17
|
+
//! Responsibilities:
|
|
11
18
|
//! - Spawn ekka-engine binary on startup
|
|
12
|
-
//! - Check readiness via health endpoint
|
|
13
|
-
//! - Route requests to engine (or fallback to stub)
|
|
14
|
-
//! - One-way disable on failure
|
|
19
|
+
//! - Check readiness via /health endpoint
|
|
15
20
|
//! - Clean shutdown on Desktop exit
|
|
16
|
-
//! - Read-only status visibility (installed, running, available, pid
|
|
21
|
+
//! - Read-only status visibility (installed, running, available, pid)
|
|
17
22
|
//! - Log streaming (stdout/stderr forwarding)
|
|
18
|
-
//!
|
|
19
|
-
//! Any changes require explicit architecture review.
|
|
20
23
|
//! ═══════════════════════════════════════════════════════════════════════════════════════════
|
|
21
24
|
|
|
22
25
|
use crate::bootstrap::resolve_home_path;
|
|
23
26
|
use crate::node_credentials;
|
|
24
|
-
use
|
|
27
|
+
use regex::Regex;
|
|
25
28
|
use serde::Serialize;
|
|
26
29
|
use std::fs;
|
|
27
30
|
use std::io::{BufRead, BufReader, Write};
|
|
@@ -30,6 +33,44 @@ use std::process::{Child, Command, Stdio};
|
|
|
30
33
|
use std::sync::Mutex;
|
|
31
34
|
use std::time::{Duration, Instant};
|
|
32
35
|
|
|
36
|
+
// =============================================================================
|
|
37
|
+
// Log Cleaning Utilities
|
|
38
|
+
// =============================================================================
|
|
39
|
+
|
|
40
|
+
/// Strip ANSI escape codes from a string (colors, formatting, etc.)
|
|
41
|
+
fn strip_ansi_codes(s: &str) -> String {
|
|
42
|
+
// Match ANSI escape sequences: ESC [ ... m (and other variants)
|
|
43
|
+
lazy_static::lazy_static! {
|
|
44
|
+
static ref ANSI_RE: Regex = Regex::new(r"\x1b\[[0-9;]*[a-zA-Z]").unwrap();
|
|
45
|
+
}
|
|
46
|
+
ANSI_RE.replace_all(s, "").to_string()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// Log engine output with clean formatting
|
|
50
|
+
///
|
|
51
|
+
/// Bootstrap lines are prefixed with [BOOTSTRAP] and logged as bootstrap op.
|
|
52
|
+
/// Engine lines with ERROR/error are logged as errors.
|
|
53
|
+
/// Everything else is logged as engine info.
|
|
54
|
+
fn log_engine_output(line: &str, stream: &str) {
|
|
55
|
+
let clean = strip_ansi_codes(line);
|
|
56
|
+
let trimmed = clean.trim();
|
|
57
|
+
|
|
58
|
+
if trimmed.is_empty() {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if trimmed.starts_with("[BOOTSTRAP]") {
|
|
63
|
+
// Bootstrap output - log with bootstrap op
|
|
64
|
+
tracing::info!(op = "bootstrap", stream = stream, "{}", trimmed);
|
|
65
|
+
} else if trimmed.contains("ERROR") || trimmed.contains("error:") {
|
|
66
|
+
// Error line
|
|
67
|
+
tracing::error!(op = "engine", stream = stream, "{}", trimmed);
|
|
68
|
+
} else {
|
|
69
|
+
// Regular engine output
|
|
70
|
+
tracing::info!(op = "engine", stream = stream, "{}", trimmed);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
33
74
|
// =============================================================================
|
|
34
75
|
// Engine Environment Builder
|
|
35
76
|
// =============================================================================
|
|
@@ -51,43 +92,6 @@ fn build_engine_env() -> Result<Vec<(&'static str, String)>, &'static str> {
|
|
|
51
92
|
env.push(("EKKA_ENGINE_URL", url.to_string()));
|
|
52
93
|
}
|
|
53
94
|
|
|
54
|
-
// EKKA_INTERNAL_SERVICE_KEY - optional for desktop (only needed for engine-bootstrap binary)
|
|
55
|
-
// Desktop runner uses node session auth, not internal key
|
|
56
|
-
let internal_key = std::env::var("EKKA_INTERNAL_SERVICE_KEY")
|
|
57
|
-
.or_else(|_| std::env::var("INTERNAL_SERVICE_KEY"))
|
|
58
|
-
.ok();
|
|
59
|
-
|
|
60
|
-
if let Some(ref key) = internal_key {
|
|
61
|
-
env.push(("EKKA_INTERNAL_SERVICE_KEY", key.clone()));
|
|
62
|
-
tracing::info!(
|
|
63
|
-
op = "desktop.internal_key.optional",
|
|
64
|
-
present = true,
|
|
65
|
-
"Internal service key configured (for engine-bootstrap)"
|
|
66
|
-
);
|
|
67
|
-
} else {
|
|
68
|
-
tracing::info!(
|
|
69
|
-
op = "desktop.internal_key.optional",
|
|
70
|
-
present = false,
|
|
71
|
-
"Internal service key not set (desktop runner uses node session auth)"
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// EKKA_TENANT_ID - optional for desktop (only needed for engine-bootstrap binary)
|
|
76
|
-
// Desktop runner gets tenant_id from node session
|
|
77
|
-
if let Ok(tenant_id) = std::env::var("EKKA_TENANT_ID") {
|
|
78
|
-
if uuid::Uuid::parse_str(&tenant_id).is_ok() {
|
|
79
|
-
env.push(("EKKA_TENANT_ID", tenant_id));
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// EKKA_WORKSPACE_ID - optional for desktop (only needed for engine-bootstrap binary)
|
|
84
|
-
// Desktop runner gets workspace_id from node session
|
|
85
|
-
if let Ok(workspace_id) = std::env::var("EKKA_WORKSPACE_ID") {
|
|
86
|
-
if uuid::Uuid::parse_str(&workspace_id).is_ok() {
|
|
87
|
-
env.push(("EKKA_WORKSPACE_ID", workspace_id));
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
95
|
// Node credentials: Try keychain first, fall back to env vars
|
|
92
96
|
// This enables headless engine startup without manual env exports
|
|
93
97
|
if let Ok((node_id, node_secret)) = node_credentials::load_credentials() {
|
|
@@ -198,10 +202,6 @@ impl EngineProcess {
|
|
|
198
202
|
}
|
|
199
203
|
}
|
|
200
204
|
|
|
201
|
-
/// Permanently disable engine for this session (one-way switch)
|
|
202
|
-
pub fn disable(&self) {
|
|
203
|
-
self.set_available(false);
|
|
204
|
-
}
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
impl Default for EngineProcess {
|
|
@@ -210,7 +210,15 @@ impl Default for EngineProcess {
|
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
/// Compute bootstrap binary path using ekka_home_folder
|
|
214
|
+
/// Bootstrap is installed from resources and always runs first
|
|
215
|
+
fn compute_bootstrap_path() -> Result<PathBuf, String> {
|
|
216
|
+
let home = resolve_home_path()?;
|
|
217
|
+
Ok(home.join("engine").join("ekka-engine-bootstrap"))
|
|
218
|
+
}
|
|
219
|
+
|
|
213
220
|
/// Compute engine binary path using ekka_home_folder
|
|
221
|
+
/// Engine is downloaded by bootstrap and exec'd into
|
|
214
222
|
fn compute_engine_path() -> Result<PathBuf, String> {
|
|
215
223
|
let home = resolve_home_path()?;
|
|
216
224
|
Ok(home.join("engine").join("ekka-engine"))
|
|
@@ -218,15 +226,15 @@ fn compute_engine_path() -> Result<PathBuf, String> {
|
|
|
218
226
|
|
|
219
227
|
/// Ensure bootstrap engine is installed from embedded resources
|
|
220
228
|
///
|
|
221
|
-
/// Extracts the bundled ekka-engine-bootstrap binary to ekka_home_folder/engine/ekka-engine
|
|
222
|
-
/// if it doesn't already exist.
|
|
229
|
+
/// Extracts the bundled ekka-engine-bootstrap binary to ekka_home_folder/engine/ekka-engine-bootstrap
|
|
230
|
+
/// if it doesn't already exist. The bootstrap will then download the real engine to ekka-engine.
|
|
223
231
|
///
|
|
224
232
|
/// Returns Ok(true) if installed, Ok(false) if already present.
|
|
225
233
|
pub fn ensure_bootstrap_installed_from_resources(resource_path: Option<PathBuf>) -> Result<bool, String> {
|
|
226
|
-
let
|
|
234
|
+
let bootstrap_path = compute_bootstrap_path()?;
|
|
227
235
|
|
|
228
236
|
// Already installed - return silently
|
|
229
|
-
if
|
|
237
|
+
if bootstrap_path.exists() {
|
|
230
238
|
return Ok(false);
|
|
231
239
|
}
|
|
232
240
|
|
|
@@ -260,12 +268,12 @@ pub fn ensure_bootstrap_installed_from_resources(resource_path: Option<PathBuf>)
|
|
|
260
268
|
})?;
|
|
261
269
|
|
|
262
270
|
// Create engine directory
|
|
263
|
-
let engine_dir =
|
|
271
|
+
let engine_dir = bootstrap_path.parent().ok_or("Invalid bootstrap path")?;
|
|
264
272
|
fs::create_dir_all(engine_dir)
|
|
265
273
|
.map_err(|e| format!("Failed to create engine directory: {}", e))?;
|
|
266
274
|
|
|
267
275
|
// Write atomically: tmp file -> chmod +x -> rename
|
|
268
|
-
let tmp_path =
|
|
276
|
+
let tmp_path = bootstrap_path.with_extension("tmp");
|
|
269
277
|
|
|
270
278
|
let mut file = fs::File::create(&tmp_path)
|
|
271
279
|
.map_err(|e| format!("Failed to create temp file: {}", e))?;
|
|
@@ -288,13 +296,13 @@ pub fn ensure_bootstrap_installed_from_resources(resource_path: Option<PathBuf>)
|
|
|
288
296
|
}
|
|
289
297
|
|
|
290
298
|
// Atomic rename
|
|
291
|
-
fs::rename(&tmp_path, &
|
|
299
|
+
fs::rename(&tmp_path, &bootstrap_path)
|
|
292
300
|
.map_err(|e| format!("Failed to rename to final path: {}", e))?;
|
|
293
301
|
|
|
294
302
|
tracing::info!(
|
|
295
303
|
op = "engine.bootstrap.install",
|
|
296
|
-
path = %
|
|
297
|
-
"Bootstrap
|
|
304
|
+
path = %bootstrap_path.display(),
|
|
305
|
+
"Bootstrap installed from resources"
|
|
298
306
|
);
|
|
299
307
|
|
|
300
308
|
Ok(true)
|
|
@@ -302,9 +310,29 @@ pub fn ensure_bootstrap_installed_from_resources(resource_path: Option<PathBuf>)
|
|
|
302
310
|
|
|
303
311
|
/// Spawn and wait for engine readiness
|
|
304
312
|
///
|
|
313
|
+
/// Two-binary architecture:
|
|
314
|
+
/// 1. Desktop spawns ekka-engine-bootstrap
|
|
315
|
+
/// 2. Bootstrap checks release service and downloads ekka-engine if needed
|
|
316
|
+
/// 3. Bootstrap execs into ekka-engine (same PID)
|
|
317
|
+
/// 4. Desktop waits for engine health check
|
|
318
|
+
///
|
|
305
319
|
/// Returns true if engine is ready, false otherwise.
|
|
306
320
|
/// Stores result in EngineProcess.available.
|
|
307
321
|
pub fn spawn_and_wait(engine: &EngineProcess) -> bool {
|
|
322
|
+
// Get both paths
|
|
323
|
+
let bootstrap_path = match compute_bootstrap_path() {
|
|
324
|
+
Ok(p) => p,
|
|
325
|
+
Err(e) => {
|
|
326
|
+
tracing::warn!(
|
|
327
|
+
op = "engine.path.error",
|
|
328
|
+
error = %e,
|
|
329
|
+
"Failed to compute bootstrap path"
|
|
330
|
+
);
|
|
331
|
+
engine.set_available(false);
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
|
|
308
336
|
let engine_path = match compute_engine_path() {
|
|
309
337
|
Ok(p) => p,
|
|
310
338
|
Err(e) => {
|
|
@@ -318,24 +346,25 @@ pub fn spawn_and_wait(engine: &EngineProcess) -> bool {
|
|
|
318
346
|
}
|
|
319
347
|
};
|
|
320
348
|
|
|
321
|
-
// Check if binary exists
|
|
322
|
-
let installed =
|
|
349
|
+
// Check if bootstrap binary exists (must be installed from resources)
|
|
350
|
+
let installed = bootstrap_path.exists();
|
|
323
351
|
engine.set_installed(installed);
|
|
324
352
|
|
|
325
353
|
if !installed {
|
|
326
354
|
tracing::info!(
|
|
327
355
|
op = "engine.spawn.missing",
|
|
328
|
-
path = %
|
|
329
|
-
"
|
|
356
|
+
path = %bootstrap_path.display(),
|
|
357
|
+
"Bootstrap binary not found, using local handlers"
|
|
330
358
|
);
|
|
331
359
|
engine.set_available(false);
|
|
332
360
|
return false;
|
|
333
361
|
}
|
|
334
362
|
|
|
335
363
|
tracing::info!(
|
|
336
|
-
op = "
|
|
337
|
-
path = %
|
|
338
|
-
|
|
364
|
+
op = "desktop.bootstrap.start",
|
|
365
|
+
path = %bootstrap_path.display(),
|
|
366
|
+
exists = bootstrap_path.exists(),
|
|
367
|
+
"Starting bootstrap"
|
|
339
368
|
);
|
|
340
369
|
|
|
341
370
|
// Build engine environment from process env
|
|
@@ -352,14 +381,14 @@ pub fn spawn_and_wait(engine: &EngineProcess) -> bool {
|
|
|
352
381
|
}
|
|
353
382
|
};
|
|
354
383
|
|
|
355
|
-
tracing::
|
|
384
|
+
tracing::debug!(
|
|
356
385
|
op = "engine.spawn.env",
|
|
357
386
|
keys = ?engine_env.iter().map(|(k, _)| *k).collect::<Vec<_>>(),
|
|
358
387
|
"Setting engine environment"
|
|
359
388
|
);
|
|
360
389
|
|
|
361
|
-
// Spawn the
|
|
362
|
-
let mut child = match Command::new(&
|
|
390
|
+
// Spawn the BOOTSTRAP process (not engine directly) with piped stdout/stderr
|
|
391
|
+
let mut child = match Command::new(&bootstrap_path)
|
|
363
392
|
.envs(engine_env)
|
|
364
393
|
.stdout(Stdio::piped())
|
|
365
394
|
.stderr(Stdio::piped())
|
|
@@ -370,20 +399,21 @@ pub fn spawn_and_wait(engine: &EngineProcess) -> bool {
|
|
|
370
399
|
tracing::warn!(
|
|
371
400
|
op = "engine.spawn.failed",
|
|
372
401
|
error = %e,
|
|
373
|
-
|
|
402
|
+
path = %bootstrap_path.display(),
|
|
403
|
+
"Failed to spawn bootstrap process"
|
|
374
404
|
);
|
|
375
405
|
engine.set_available(false);
|
|
376
406
|
return false;
|
|
377
407
|
}
|
|
378
408
|
};
|
|
379
409
|
|
|
380
|
-
// Spawn log reader threads
|
|
410
|
+
// Spawn log reader threads with clean formatting
|
|
381
411
|
if let Some(stdout) = child.stdout.take() {
|
|
382
412
|
std::thread::spawn(move || {
|
|
383
413
|
let reader = BufReader::new(stdout);
|
|
384
414
|
for line in reader.lines() {
|
|
385
415
|
match line {
|
|
386
|
-
Ok(l) =>
|
|
416
|
+
Ok(l) => log_engine_output(&l, "stdout"),
|
|
387
417
|
Err(_) => break,
|
|
388
418
|
}
|
|
389
419
|
}
|
|
@@ -396,7 +426,7 @@ pub fn spawn_and_wait(engine: &EngineProcess) -> bool {
|
|
|
396
426
|
let reader = BufReader::new(stderr);
|
|
397
427
|
for line in reader.lines() {
|
|
398
428
|
match line {
|
|
399
|
-
Ok(l) =>
|
|
429
|
+
Ok(l) => log_engine_output(&l, "stderr"),
|
|
400
430
|
Err(_) => break,
|
|
401
431
|
}
|
|
402
432
|
}
|
|
@@ -412,9 +442,9 @@ pub fn spawn_and_wait(engine: &EngineProcess) -> bool {
|
|
|
412
442
|
*guard = Some(child);
|
|
413
443
|
}
|
|
414
444
|
|
|
415
|
-
tracing::info!(op = "engine.spawn.success", pid = pid, "
|
|
445
|
+
tracing::info!(op = "engine.spawn.success", pid = pid, "Bootstrap process spawned");
|
|
416
446
|
|
|
417
|
-
// Allow bootstrap time to
|
|
447
|
+
// Allow bootstrap time to download engine (if needed) and exec into it
|
|
418
448
|
// Bootstrap either: execs real engine (same PID), or exits with error code
|
|
419
449
|
std::thread::sleep(Duration::from_secs(2));
|
|
420
450
|
|
|
@@ -443,18 +473,28 @@ pub fn spawn_and_wait(engine: &EngineProcess) -> bool {
|
|
|
443
473
|
};
|
|
444
474
|
|
|
445
475
|
if bootstrap_failed {
|
|
476
|
+
tracing::error!(
|
|
477
|
+
op = "engine.bootstrap.fatal",
|
|
478
|
+
pid = pid,
|
|
479
|
+
"FATAL: Bootstrap failed to start engine. Check logs above for error details."
|
|
480
|
+
);
|
|
446
481
|
engine.set_available(false);
|
|
447
482
|
return false;
|
|
448
483
|
}
|
|
449
484
|
|
|
450
|
-
tracing::debug!(
|
|
485
|
+
tracing::debug!(
|
|
486
|
+
op = "engine.bootstrap.exec",
|
|
487
|
+
pid = pid,
|
|
488
|
+
engine_path = %engine_path.display(),
|
|
489
|
+
"Bootstrap exec'd into real engine, starting readiness check"
|
|
490
|
+
);
|
|
451
491
|
|
|
452
492
|
// Wait for readiness (real engine should now be running)
|
|
453
493
|
let ready = wait_for_ready(15);
|
|
454
494
|
engine.set_available(ready);
|
|
455
495
|
|
|
456
496
|
if ready {
|
|
457
|
-
tracing::info!(op = "engine.ready", "Engine is ready");
|
|
497
|
+
tracing::info!(op = "desktop.engine.ready", "Engine is ready");
|
|
458
498
|
} else {
|
|
459
499
|
tracing::warn!(op = "engine.ready.timeout", "Engine readiness timeout");
|
|
460
500
|
}
|
|
@@ -507,49 +547,7 @@ pub fn shutdown(engine: &EngineProcess) {
|
|
|
507
547
|
engine.set_available(false);
|
|
508
548
|
}
|
|
509
549
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
pub fn route_to_engine(req: &EngineRequest) -> Option<EngineResponse> {
|
|
515
|
-
let port: u16 = std::env::var("EKKA_ENGINE_PORT")
|
|
516
|
-
.ok()
|
|
517
|
-
.and_then(|s| s.parse().ok())
|
|
518
|
-
.unwrap_or(9473);
|
|
519
|
-
|
|
520
|
-
let url = format!("http://127.0.0.1:{}/request", port);
|
|
521
|
-
|
|
522
|
-
let client = match reqwest::blocking::Client::builder()
|
|
523
|
-
.timeout(Duration::from_secs(30))
|
|
524
|
-
.build()
|
|
525
|
-
{
|
|
526
|
-
Ok(c) => c,
|
|
527
|
-
Err(e) => {
|
|
528
|
-
tracing::warn!(op = "engine.route.client_error", error = %e, "Failed to create HTTP client");
|
|
529
|
-
return None;
|
|
530
|
-
}
|
|
531
|
-
};
|
|
532
|
-
|
|
533
|
-
match client.post(&url).json(req).send() {
|
|
534
|
-
Ok(resp) if resp.status().is_success() => {
|
|
535
|
-
match resp.json::<EngineResponse>() {
|
|
536
|
-
Ok(engine_resp) => {
|
|
537
|
-
tracing::debug!(op = "engine.route.success", op_name = %req.op, "Routed to engine");
|
|
538
|
-
Some(engine_resp)
|
|
539
|
-
}
|
|
540
|
-
Err(e) => {
|
|
541
|
-
tracing::warn!(op = "engine.route.parse_error", error = %e, "Failed to parse engine response");
|
|
542
|
-
None
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
Ok(resp) => {
|
|
547
|
-
tracing::warn!(op = "engine.route.error", status = %resp.status(), "Engine returned error");
|
|
548
|
-
None
|
|
549
|
-
}
|
|
550
|
-
Err(e) => {
|
|
551
|
-
tracing::warn!(op = "engine.route.failed", error = %e, "Failed to route to engine");
|
|
552
|
-
None
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
}
|
|
550
|
+
// NOTE: route_to_engine() was removed.
|
|
551
|
+
// The spawned engine is a runner runtime (ekka-runner-local), not a request router.
|
|
552
|
+
// All commands go directly to local Rust handlers + cloud API.
|
|
553
|
+
// See commit history for removed code if needed.
|