Skip to content

Commit a28a3c8

Browse files
committed
Improve windows debugging.
1 parent 7fb0b36 commit a28a3c8

2 files changed

Lines changed: 139 additions & 40 deletions

File tree

docs/specs/deploy.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,10 @@ codesign/jsign the executable
202202
- Verify the tag exists on the remote before creating or publishing the release
203203
7. **Verify** — spot-check signatures, confirm release assets are correct
204204
205+
### Packaged app logging
206+
207+
Windows release builds use the GUI subsystem, so launching `mouseterm.exe` from a terminal returns immediately and does not stream stdout/stderr. The Tauri backend writes sidecar diagnostics to `%LOCALAPPDATA%\MouseTerm\mouseterm.log` on Windows, or to `$TMPDIR/mouseterm.log` on other platforms. Set `MOUSETERM_LOG_FILE` to override the path.
208+
205209
### Resuming after failure
206210
207211
```bash

standalone/src-tauri/src/lib.rs

Lines changed: 135 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
use serde::{Deserialize, Serialize};
22
use serde_json::{Map as JsonMap, Value as JsonValue};
3-
use std::collections::HashMap;
4-
use std::sync::atomic::{AtomicU64, Ordering};
5-
use std::sync::mpsc;
6-
use std::sync::{Arc, Mutex};
7-
use std::time::Duration;
8-
use std::path::{Path, PathBuf};
3+
use std::{
4+
collections::HashMap,
5+
env,
6+
fs::{create_dir_all, OpenOptions},
7+
io::Write,
8+
path::{Path, PathBuf},
9+
sync::atomic::{AtomicU64, Ordering},
10+
sync::mpsc,
11+
sync::{Arc, Mutex},
12+
time::{Duration, SystemTime, UNIX_EPOCH},
13+
};
914
use tauri::{AppHandle, Emitter, Manager};
1015
use tauri_plugin_shell::{process::CommandEvent, ShellExt};
1116

@@ -23,6 +28,62 @@ struct SidecarState {
2328
next_request_id: AtomicU64,
2429
}
2530

31+
const LOG_FILE_ENV: &str = "MOUSETERM_LOG_FILE";
32+
33+
fn log_timestamp() -> u64 {
34+
SystemTime::now()
35+
.duration_since(UNIX_EPOCH)
36+
.map(|duration| duration.as_secs())
37+
.unwrap_or_default()
38+
}
39+
40+
fn default_log_path() -> PathBuf {
41+
if let Some(path) = env::var_os(LOG_FILE_ENV) {
42+
return PathBuf::from(path);
43+
}
44+
45+
#[cfg(target_os = "windows")]
46+
if let Some(local_app_data) = env::var_os("LOCALAPPDATA") {
47+
return PathBuf::from(local_app_data)
48+
.join("MouseTerm")
49+
.join("mouseterm.log");
50+
}
51+
52+
env::temp_dir().join("mouseterm.log")
53+
}
54+
55+
fn init_log() {
56+
let path = default_log_path();
57+
if let Some(parent) = path.parent() {
58+
let _ = create_dir_all(parent);
59+
}
60+
61+
if let Ok(mut file) = OpenOptions::new()
62+
.create(true)
63+
.write(true)
64+
.truncate(true)
65+
.open(&path)
66+
{
67+
let _ = writeln!(
68+
file,
69+
"[{}] MouseTerm log started at {}",
70+
log_timestamp(),
71+
path.display()
72+
);
73+
}
74+
}
75+
76+
fn append_log(message: impl AsRef<str>) {
77+
let path = default_log_path();
78+
if let Some(parent) = path.parent() {
79+
let _ = create_dir_all(parent);
80+
}
81+
82+
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) {
83+
let _ = writeln!(file, "[{}] {}", log_timestamp(), message.as_ref());
84+
}
85+
}
86+
2687
#[derive(Serialize, Deserialize, Clone)]
2788
struct PtySpawnOptions {
2889
cols: Option<u16>,
@@ -39,7 +100,10 @@ fn request_from_sidecar(
39100
event: &str,
40101
data: JsonValue,
41102
) -> Result<JsonValue, String> {
42-
let request_id = format!("req-{}", state.next_request_id.fetch_add(1, Ordering::Relaxed));
103+
let request_id = format!(
104+
"req-{}",
105+
state.next_request_id.fetch_add(1, Ordering::Relaxed)
106+
);
43107
let (tx, rx) = mpsc::channel();
44108
state
45109
.pending_requests
@@ -73,11 +137,7 @@ fn request_from_sidecar(
73137
// ── Tauri commands ──────────────────────────────────────────────────────────
74138

75139
#[tauri::command]
76-
fn pty_spawn(
77-
state: tauri::State<'_, SidecarState>,
78-
id: String,
79-
options: Option<PtySpawnOptions>,
80-
) {
140+
fn pty_spawn(state: tauri::State<'_, SidecarState>, id: String, options: Option<PtySpawnOptions>) {
81141
let msg = serde_json::json!({
82142
"event": "pty:spawn",
83143
"data": { "id": id, "options": options }
@@ -95,12 +155,7 @@ fn pty_write(state: tauri::State<'_, SidecarState>, id: String, data: String) {
95155
}
96156

97157
#[tauri::command]
98-
fn pty_resize(
99-
state: tauri::State<'_, SidecarState>,
100-
id: String,
101-
cols: u16,
102-
rows: u16,
103-
) {
158+
fn pty_resize(state: tauri::State<'_, SidecarState>, id: String, cols: u16, rows: u16) {
104159
let msg = serde_json::json!({
105160
"event": "pty:resize",
106161
"data": { "id": id, "cols": cols, "rows": rows }
@@ -124,16 +179,23 @@ fn pty_request_init(state: tauri::State<'_, SidecarState>) {
124179
}
125180

126181
#[tauri::command]
127-
fn pty_get_cwd(state: tauri::State<'_, SidecarState>, id: String) -> Result<Option<String>, String> {
182+
fn pty_get_cwd(
183+
state: tauri::State<'_, SidecarState>,
184+
id: String,
185+
) -> Result<Option<String>, String> {
128186
let response = request_from_sidecar(&state, "pty:getCwd", serde_json::json!({ "id": id }))?;
129187
Ok(response
130188
.get("cwd")
131189
.and_then(|cwd| cwd.as_str().map(String::from)))
132190
}
133191

134192
#[tauri::command]
135-
fn pty_get_scrollback(state: tauri::State<'_, SidecarState>, id: String) -> Result<Option<String>, String> {
136-
let response = request_from_sidecar(&state, "pty:getScrollback", serde_json::json!({ "id": id }))?;
193+
fn pty_get_scrollback(
194+
state: tauri::State<'_, SidecarState>,
195+
id: String,
196+
) -> Result<Option<String>, String> {
197+
let response =
198+
request_from_sidecar(&state, "pty:getScrollback", serde_json::json!({ "id": id }))?;
137199
Ok(response
138200
.get("data")
139201
.and_then(|data| data.as_str().map(String::from)))
@@ -165,15 +227,17 @@ fn get_default_shell() -> ShellInfo {
165227
.unwrap_or_else(|_| String::from("C:\\Windows\\System32\\cmd.exe"));
166228

167229
#[cfg(not(target_os = "windows"))]
168-
let shell_path = std::env::var("SHELL")
169-
.unwrap_or_else(|_| String::from("/bin/sh"));
230+
let shell_path = std::env::var("SHELL").unwrap_or_else(|_| String::from("/bin/sh"));
170231

171232
let name = Path::new(&shell_path)
172233
.file_name()
173234
.map(|n| n.to_string_lossy().into_owned())
174235
.unwrap_or_else(|| shell_path.clone());
175236

176-
ShellInfo { name, path: shell_path }
237+
ShellInfo {
238+
name,
239+
path: shell_path,
240+
}
177241
}
178242

179243
fn resolve_sidecar_path(resource_dir: Option<PathBuf>, manifest_dir: &Path) -> PathBuf {
@@ -190,20 +254,25 @@ fn resolve_sidecar_path(resource_dir: Option<PathBuf>, manifest_dir: &Path) -> P
190254
manifest_dir.join("..").join("sidecar").join("main.js")
191255
}
192256

193-
fn start_sidecar(app: &AppHandle) -> SidecarState {
257+
fn start_sidecar(app: &AppHandle) -> Result<SidecarState, String> {
194258
let sidecar_path = resolve_sidecar_path(
195259
app.path().resource_dir().ok(),
196260
Path::new(env!("CARGO_MANIFEST_DIR")),
197261
);
262+
append_log(format!(
263+
"[sidecar] resolved script: {}",
264+
sidecar_path.display()
265+
));
198266

199267
let (mut rx, mut child) = app
200268
.shell()
201269
.sidecar("node")
202-
.expect("failed to resolve bundled Node.js runtime")
270+
.map_err(|err| format!("failed to resolve bundled Node.js runtime: {err}"))?
203271
.arg(&sidecar_path)
204272
.set_raw_out(false)
205273
.spawn()
206-
.expect("failed to start Node.js sidecar");
274+
.map_err(|err| format!("failed to start Node.js sidecar: {err}"))?;
275+
append_log("[sidecar] spawned Node.js runtime");
207276

208277
let handle = app.clone();
209278
let pending_requests: PendingRequests = Arc::new(Mutex::new(HashMap::new()));
@@ -214,14 +283,21 @@ fn start_sidecar(app: &AppHandle) -> SidecarState {
214283
while let Some(event) = rx.recv().await {
215284
match event {
216285
CommandEvent::Stdout(line) => {
217-
let Ok(line) = String::from_utf8(line) else { continue };
286+
let Ok(line) = String::from_utf8(line) else {
287+
append_log("[sidecar stdout] invalid UTF-8");
288+
continue;
289+
};
218290
let Ok(mut msg) = serde_json::from_str::<serde_json::Value>(&line) else {
291+
append_log(format!("[sidecar stdout] {}", line.trim_end()));
219292
continue;
220293
};
221-
let Some(event) = msg.get("event").and_then(|e| e.as_str()).map(String::from) else {
294+
let Some(event) = msg.get("event").and_then(|e| e.as_str()).map(String::from)
295+
else {
296+
append_log("[sidecar stdout] JSON line missing event");
222297
continue;
223298
};
224-
let data = msg.as_object_mut()
299+
let data = msg
300+
.as_object_mut()
225301
.and_then(|m| m.remove("data"))
226302
.unwrap_or(serde_json::Value::Null);
227303

@@ -241,18 +317,23 @@ fn start_sidecar(app: &AppHandle) -> SidecarState {
241317
}
242318
CommandEvent::Stderr(line) => {
243319
if let Ok(line) = String::from_utf8(line) {
244-
eprintln!("[sidecar] {}", line.trim_end());
320+
let message = format!("[sidecar] {}", line.trim_end());
321+
eprintln!("{message}");
322+
append_log(message);
245323
}
246324
}
247325
CommandEvent::Error(err) => {
248-
eprintln!("[sidecar] {}", err);
326+
let message = format!("[sidecar] {err}");
327+
eprintln!("{message}");
328+
append_log(message);
249329
}
250330
CommandEvent::Terminated(payload) => {
251-
eprintln!(
331+
let message = format!(
252332
"[sidecar] exited (code: {:?}, signal: {:?})",
253-
payload.code,
254-
payload.signal
333+
payload.code, payload.signal
255334
);
335+
eprintln!("{message}");
336+
append_log(message);
256337
break;
257338
}
258339
_ => {}
@@ -266,22 +347,26 @@ fn start_sidecar(app: &AppHandle) -> SidecarState {
266347
std::thread::spawn(move || {
267348
while let Ok(msg) = writer_rx.recv() {
268349
match msg {
269-
SidecarMsg::Shutdown => break,
350+
SidecarMsg::Shutdown => {
351+
append_log("[sidecar] shutdown requested");
352+
break;
353+
}
270354
SidecarMsg::Json(line) => {
271355
let payload = format!("{}\n", line);
272356
if child.write(payload.as_bytes()).is_err() {
357+
append_log("[sidecar] stdin write failed");
273358
break;
274359
}
275360
}
276361
}
277362
}
278363
});
279364

280-
SidecarState {
365+
Ok(SidecarState {
281366
tx,
282367
pending_requests,
283368
next_request_id: AtomicU64::new(0),
284-
}
369+
})
285370
}
286371

287372
// ── App entry point ─────────────────────────────────────────────────────────
@@ -292,8 +377,15 @@ pub fn run() {
292377
.plugin(tauri_plugin_shell::init())
293378
.plugin(tauri_plugin_updater::Builder::new().build())
294379
.setup(|app| {
295-
let sidecar_state = start_sidecar(app.handle());
380+
init_log();
381+
append_log("[app] setup started");
382+
383+
let sidecar_state = start_sidecar(app.handle()).map_err(|err| {
384+
append_log(format!("[sidecar] {err}"));
385+
std::io::Error::new(std::io::ErrorKind::Other, err)
386+
})?;
296387
app.manage(sidecar_state);
388+
append_log("[app] sidecar state registered");
297389

298390
// On non-macOS, remove native decorations for a fully custom title bar.
299391
// macOS uses titleBarStyle "Overlay" from config instead, which preserves
@@ -380,6 +472,9 @@ mod tests {
380472

381473
let resolved = resolve_sidecar_path(None, manifest_dir);
382474

383-
assert_eq!(resolved, manifest_dir.join("..").join("sidecar").join("main.js"));
475+
assert_eq!(
476+
resolved,
477+
manifest_dir.join("..").join("sidecar").join("main.js")
478+
);
384479
}
385480
}

0 commit comments

Comments
 (0)