Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 24 additions & 7 deletions desktop/app/loading.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
.foot .warn { color:var(--warn); }
.diag { margin-top:10px; font-size:.72rem; color:var(--muted); font-family: ui-monospace, Menlo, monospace;
background: rgba(255,255,255,.04); border:1px solid rgba(255,255,255,.08); border-radius:8px;
padding:8px 10px; user-select:text; display:none; white-space:pre-wrap; text-align:left; }
padding:8px 10px; user-select:text; display:none; white-space:pre-wrap; text-align:left;
max-height:260px; overflow:auto; }
.actions { margin-top:18px; display:flex; gap:12px; align-items:center; justify-content:center; min-height:34px; }
.btn { font: inherit; font-size:.85rem; font-weight:600; padding:7px 16px; border-radius:9px; cursor:pointer;
border:1px solid rgba(255,255,255,.16); background:rgba(255,255,255,.05); color:var(--fg); }
Expand All @@ -56,7 +57,6 @@
<div class="diag" id="diag"></div>
<div class="actions">
<button class="btn hidden" id="retryBtn">Retry</button>
<a class="link hidden" id="openAnyway">Open anyway →</a>
<button class="btn hidden" id="quitBtn">Quit</button>
</div>
</div>
Expand Down Expand Up @@ -100,6 +100,20 @@
function stepUrl(s){ return 'http://127.0.0.1:' + PORTS[s.portKey] + s.path; }

var t0 = Date.now(), quipIdx = 0, quipStep = null, doneRedirect = false, failed = false, curStartedAt = {}, cur = null;
var STARTUP_LOG = '';
// Pull the teed backend logs (<app_data>/startup.log) from the Tauri host so the
// failure screen can show WHY the services didn't come up.
function fetchStartupLog(){
try {
if (window.__TAURI__ && window.__TAURI__.core) {
window.__TAURI__.core.invoke('read_startup_log')
.then(function(t){ STARTUP_LOG = (t && t.trim()) ? t : '(startup log is empty)'; render(); })
.catch(function(){ STARTUP_LOG = '(could not read startup log)'; render(); });
return;
}
} catch (e) {}
STARTUP_LOG = '(startup log unavailable)';
}
var $ = function(id){ return document.getElementById(id); };
STEPS.forEach(function(s){
var li = document.createElement('li'); li.id = 'li-'+s.id;
Expand Down Expand Up @@ -136,10 +150,15 @@
$('stage').textContent = "Orcabot couldn't finish starting up";
$('quip').textContent = 'The background services didn’t come up. A previous instance may still be running, or a port is in use.';
$('hint').textContent = '';
$('diag').style.display = 'block'; $('diag').textContent = diagText();
$('diag').style.display = 'block';
// Show the port/health summary plus the actual backend logs (workerd/D1),
// which explain WHY startup failed (e.g. a port bind error) — otherwise
// invisible in a Finder-launched .app.
$('diag').textContent = diagText()
+ '\n\n─── background server logs ───\n'
+ (STARTUP_LOG || '(fetching…)');
$('retryBtn').classList.remove('hidden');
$('quitBtn').classList.remove('hidden');
$('openAnyway').classList.remove('hidden');
return;
}
cur = current();
Expand All @@ -149,7 +168,6 @@
$('quip').textContent = cur.quips[quipIdx % cur.quips.length];
var waited = (Date.now() - (curStartedAt[cur.id]||t0))/1000;
$('hint').innerHTML = waited > (cur.slow||12) ? ' · <span class="warn">'+cur.label+' is taking longer than usual…</span>' : '';
if (elapsed() > MANUAL_AFTER){ $('openAnyway').classList.remove('hidden'); }
}

function tick(){
Expand All @@ -158,12 +176,11 @@
.then(function(){
var ready = STEPS.every(function(s){ return !s.required || s.up; });
if (ready && !doneRedirect){ doneRedirect = true; render(); setTimeout(function(){ window.location.replace(redirectUrl()); }, 500); return; }
if (!ready && elapsed() >= FAIL_AFTER){ failed = true; }
if (!ready && elapsed() >= FAIL_AFTER){ failed = true; fetchStartupLog(); }
render();
});
}

$('openAnyway').addEventListener('click', function(){ window.location.replace(redirectUrl()); });
$('retryBtn').addEventListener('click', function(){ window.location.reload(); });
$('quitBtn').addEventListener('click', function(){
try { window.__TAURI__.core.invoke('quit_app'); } catch(e) { try { window.close(); } catch(_){} }
Expand Down
2 changes: 1 addition & 1 deletion desktop/app/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion desktop/app/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "orcabot-desktop"
version = "0.6.0"
version = "0.8.0"
edition = "2021"
description = "Orcabot Desktop shell"
license = "UNLICENSED"
Expand Down
14 changes: 14 additions & 0 deletions desktop/app/src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,20 @@ pub fn get_app_version(app: tauri::AppHandle) -> String {
app.package_info().version.to_string()
}

/// Read this boot's startup log (`<app_data>/startup.log`) — the teed workerd / D1
/// output plus the chosen ports — so the loading screen can show WHY the backend
/// failed to start (a Finder-launched .app has no console). Empty string if none.
#[tauri::command]
pub fn read_startup_log(app: tauri::AppHandle) -> String {
use tauri::Manager;
app.path()
.app_data_dir()
.ok()
.map(|d| d.join("startup.log"))
.and_then(|p| std::fs::read_to_string(p).ok())
.unwrap_or_default()
}

#[derive(Serialize, Clone)]
pub struct OrcabotAccount {
pub email: String,
Expand Down
132 changes: 128 additions & 4 deletions desktop/app/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ fn pid_file_path(data_dir: &Path) -> PathBuf {
data_dir.join("desktop-services.pid")
}

/// Set once the user accepts an auto-update ("Update & restart"). The whole stack is
/// about to be torn down and relaunched, so we must NOT keep spinning up heavy
/// processes (especially the sandbox VM boot / image download) during the ~minutes
/// download — `start_sandbox_vm` checks this and bails.
static UPDATING: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);

fn is_updating() -> bool {
UPDATING.load(std::sync::atomic::Ordering::SeqCst)
}

/// Progress of an in-flight auto-update, emitted to the GUI as `update-progress`
/// so the frontend can show a download bar (the native "Update available" dialog
/// otherwise gives no feedback between "Update & restart" and the relaunch).
Expand Down Expand Up @@ -201,6 +211,34 @@ fn ensure_port_env(var: &str, preferred: u16, used: &[u16]) -> u16 {
port
}

/// Tee a child process stream line-by-line to the console AND (if available) the
/// per-boot startup log, each line prefixed with the service label. Runs on its own
/// thread so it drains the pipe continuously (never blocking the child on a full buffer).
fn tee_child_stream<R: std::io::Read + Send + 'static>(
stream: R,
label: String,
log_path: Option<PathBuf>,
is_err: bool,
) {
use std::io::{BufRead, BufReader, Write};
std::thread::spawn(move || {
let mut logf =
log_path.and_then(|p| std::fs::OpenOptions::new().create(true).append(true).open(p).ok());
for line in BufReader::new(stream).lines() {
let Ok(line) = line else { break };
let out = format!("[{}] {}", label, line);
if is_err {
eprintln!("{}", out);
} else {
println!("{}", out);
}
if let Some(ref mut f) = logf {
let _ = writeln!(f, "{}", out);
}
}
});
}

/// Per-boot token that gates dev-auth to the trusted host frontend. Generated
/// once at startup, passed to the control-plane worker (`SURFACE_TOKEN`) and
/// handed to the GUI webview via the `get_surface_token` command. The sandbox VM
Expand Down Expand Up @@ -461,6 +499,13 @@ impl DesktopServices {
);
}

// Start the per-boot startup log with the chosen ports — the first thing to check
// when startup fails (was a default busy? did allocation move a service?).
self.reset_startup_log(&format!(
"ports: control-plane={} frontend={} d1-shim={} sandbox={}",
cp_port, fe_port, d1_port, sandbox_host_port
));

if cp_port != 8787 || fe_port != 8788 || d1_port != 9001 || sandbox_host_port != 8080 {
eprintln!(
"[ports] a default port was busy — using control-plane={} frontend={} d1-shim={} sandbox={}",
Expand Down Expand Up @@ -665,6 +710,13 @@ impl DesktopServices {
data_dir: &Path,
resource_root: &Path,
) -> Result<(), vm::VMError> {
// The user accepted an app update → don't spin the VM up (or download its image)
// just to tear it all down on the imminent relaunch.
if is_updating() {
eprintln!("[vm] app update accepted — skipping sandbox VM startup");
return Ok(());
}

// Check if VM resources exist
let vm_resource_paths = vm::image::VMResourcePaths::from_resource_root(resource_root);

Expand Down Expand Up @@ -775,9 +827,20 @@ impl DesktopServices {
};
config = config.with_cmdline(cmdline);

// Create and start VM
// Create and start VM — unless an update was accepted while we were staging.
if is_updating() {
eprintln!("[vm] app update accepted — not booting sandbox VM");
return Ok(());
}
let mut vm = create_platform_vm();
vm.start(&config)?;
// If the update landed during boot, stop the VM we just started rather than
// waiting 120s for health only to tear it down on relaunch.
if is_updating() {
eprintln!("[vm] app update accepted mid-boot — stopping sandbox VM");
let _ = vm.stop();
return Ok(());
}

// Wait for sandbox to be healthy
eprintln!("Waiting for sandbox VM to become healthy...");
Expand Down Expand Up @@ -818,24 +881,76 @@ impl DesktopServices {

let mut command = Command::new(binary_path);
command.args(args);
command.stdout(Stdio::inherit());
command.stderr(Stdio::inherit());
// Tee stdout+stderr to the console AND <data_dir>/startup.log. A Finder-launched
// .app has no attached terminal, so inherited output vanishes — this keeps a
// per-boot record of WHY a service failed (surfaced in the loading screen and
// recoverable as a file).
command.stdout(Stdio::piped());
command.stderr(Stdio::piped());
for (key, value) in envs {
command.env(key, value);
}

match command.spawn() {
Ok(child) => {
Ok(mut child) => {
let log_path = self.startup_log_path();
if let Some(out) = child.stdout.take() {
tee_child_stream(out, label.to_string(), log_path.clone(), false);
}
if let Some(err) = child.stderr.take() {
tee_child_stream(err, label.to_string(), log_path, true);
}
if let Ok(mut children) = self.children.lock() {
children.push(child);
}
}
Err(err) => {
eprintln!("Failed to start {}: {}", label, err);
self.append_startup_log(&format!("[{}] FAILED TO START: {}", label, err));
}
}
}

/// `<data_dir>/startup.log` — where service output is teed for post-mortem.
fn startup_log_path(&self) -> Option<PathBuf> {
self
.data_dir
.lock()
.ok()
.and_then(|dd| dd.clone())
.map(|d| d.join("startup.log"))
}

fn append_startup_log(&self, line: &str) {
if let Some(p) = self.startup_log_path() {
use std::io::Write;
if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(p) {
let _ = writeln!(f, "{}", line);
}
}
}

/// Truncate the startup log at the start of a boot and write a header, so it only
/// ever reflects the CURRENT startup attempt (not accumulated across launches).
fn reset_startup_log(&self, header: &str) {
if let Some(p) = self.startup_log_path() {
let _ = std::fs::write(p, format!("=== Orcabot desktop startup ===\n{}\n", header));
}
}

/// Stop ONLY the sandbox VM (leave workerd/frontend running). Used when the user
/// accepts an update: the heavy VM shouldn't keep running/booting during the
/// download, but the frontend must stay up so the update-progress bar keeps working.
fn stop_sandbox_vm(&self) {
if let Ok(mut vm_lock) = self.sandbox_vm.lock() {
if let Some(ref mut vm) = *vm_lock {
eprintln!("Stopping sandbox VM (app update in progress)...");
let _ = vm.stop();
}
*vm_lock = None;
}
}

fn shutdown(&self) {
// Stop sandbox VM first
if let Ok(mut vm_lock) = self.sandbox_vm.lock() {
Expand Down Expand Up @@ -1012,6 +1127,7 @@ fn main() {
commands::reveal_workspace,
commands::get_ports,
commands::get_app_version,
commands::read_startup_log,
commands::verify_orcabot_account,
commands::set_cloud_credential,
commands::sign_in_google_loopback,
Expand Down Expand Up @@ -1130,6 +1246,7 @@ fn main() {
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons};
use tauri_plugin_updater::UpdaterExt;
let handle = app.handle().clone();
let upd_services = Arc::clone(&services);
tauri::async_runtime::spawn(async move {
let updater = match handle.updater() {
Ok(u) => u,
Expand Down Expand Up @@ -1158,6 +1275,13 @@ fn main() {
return;
}

// The user chose update+restart. Everything is about to be torn down and
// relaunched, so stop spinning up the heavy sandbox VM during the ~minutes
// download: flag it so start_sandbox_vm won't (re)boot, and stop any VM that
// already came up. Leave workerd/frontend running so this progress UI works.
UPDATING.store(true, std::sync::atomic::Ordering::SeqCst);
upd_services.stop_sandbox_vm();

// Tell the GUI a download is starting so it can show a progress bar,
// then stream progress from the download closure. Emits are throttled
// to once per MB so a ~190MB download doesn't flood IPC.
Expand Down
4 changes: 2 additions & 2 deletions desktop/app/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Orcabot",
"version": "0.6.0",
"version": "0.8.0",
"identifier": "com.orcabot.desktop",
"build": {
"beforeDevCommand": "sh -c \"cd ../../frontend && NEXT_PUBLIC_API_URL=http://localhost:8787 NEXT_PUBLIC_SITE_URL=http://localhost:8788 NEXT_PUBLIC_DEV_MODE_ENABLED=true NEXT_PUBLIC_DESKTOP_MODE=true npx wrangler dev -c wrangler.toml --port 8788\"",
Expand Down Expand Up @@ -48,7 +48,7 @@
"macOS": {
"entitlements": "entitlements.plist",
"signingIdentity": "Developer ID Application: Robert Macrae (3927MKQNPA)",
"minimumSystemVersion": "13.0"
"minimumSystemVersion": "13.5"
}
},
"plugins": {
Expand Down
6 changes: 3 additions & 3 deletions desktop/app/src-tauri/vm-image.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "v2",
"sha256": "eba0a9e5c17778f5de08371dd9bcb46aa3697a827d8c0ed7c801a465c907f633",
"url": "https://github.com/Hyper-Int/OrcaBot/releases/download/vm-image-v2/sandbox.img.gz"
"version": "v3",
"sha256": "a040638617597b660ea8baef915d4de70267fa48632f910fc892d9115313daa4",
"url": "https://github.com/Hyper-Int/OrcaBot/releases/download/vm-image-v3/sandbox.img.gz"
}
5 changes: 5 additions & 0 deletions frontend/src/app/download/download.css
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@
margin-bottom: 1rem;
}

.dl-req {
margin: -0.5rem 0 1rem;
font-size: 0.85rem;
}

.dl-btn {
display: inline-flex;
flex-direction: column;
Expand Down
Loading
Loading