diff --git a/README.md b/README.md index 69fa7d2..e54fea1 100644 --- a/README.md +++ b/README.md @@ -27,10 +27,15 @@ Here are the key development loops: ```sh pnpm install -pnpm dev:website # http://localhost:5173/playground -pnpm storybook # http://localhost:6006 -pnpm test # runs all tests +pnpm dev:website # vite hotreload at http://localhost:5173/playground +pnpm dev:standalone # tauri hotreload + pnpm dogfood:vscode # builds the VSCode extension and installs it into your local VSCode +pnpm dogfood:standalone # builds and runs the standalone app +pnpm dogfood:standalone --install # installs your local build overtop of your existing system installation + +pnpm storybook # http://localhost:6006 +pnpm test # runs all tests ``` ### Folder structure diff --git a/package.json b/package.json index d2a4c02..bcde8fb 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "build:standalone": "pnpm --filter mouseterm-standalone tauri build", "build:website": "pnpm --filter mouseterm-website build", "dogfood:vscode": "pnpm run build:vscode && pnpm --filter mouseterm dogfood", + "dogfood:standalone": "bash standalone/scripts/dogfood.sh", "storybook": "pnpm --filter mouseterm-lib storybook" }, "pnpm": { diff --git a/standalone/scripts/dogfood.sh b/standalone/scripts/dogfood.sh new file mode 100755 index 0000000..943fac9 --- /dev/null +++ b/standalone/scripts/dogfood.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# +# Builds the standalone app and either launches or installs it. +# +# Usage: +# pnpm dogfood:standalone Build and launch from the build directory. +# pnpm dogfood:standalone --install Build and copy into the system install location. +# +# Launch mode (default): +# Runs the built binary directly from target/release. Works on Windows, macOS, +# and Linux with no prior setup. This is the fastest way to test changes. +# +# Install mode (--install): +# Copies the built files over the system-installed copy, bypassing the slow +# bundling/installer step. Requires a one-time install via the NSIS installer +# so that registry entries, shortcuts, etc. are in place. Currently Windows only. +# +set -euo pipefail + +# Skip past "--" that pnpm injects when forwarding arguments +[[ "${1:-}" == "--" ]] && shift + +RELEASE_DIR="standalone/src-tauri/target/release" + +if [[ "${1:-}" == "--install" ]]; then + # Full build with bundling, but disable updater artifact signing + pnpm --filter mouseterm-standalone tauri build \ + -c '{"bundle":{"createUpdaterArtifacts":false}}' +else + # Fast build: skip bundling entirely since we just need the exe + pnpm --filter mouseterm-standalone tauri build --no-bundle +fi + +if [[ "${1:-}" == "--install" ]]; then + # --- Install mode --- + # Platform-specific: copy built files to system install location + case "$(uname -s)" in + MINGW*|MSYS*|CYGWIN*|Windows_NT) + INSTALL_DIR="$LOCALAPPDATA/MouseTerm" + if [[ ! -f "$INSTALL_DIR/uninstall.exe" ]]; then + echo "MouseTerm is not installed yet." + echo "Run the installer once first:" + echo " $RELEASE_DIR/bundle/nsis/MouseTerm_*-setup.exe" + echo "" + echo "After that, 'dogfood:standalone --install' will work from then on." + exit 1 + fi + # Wipe everything except uninstall.exe (managed by NSIS), then copy + TMP_UNINSTALL="$(mktemp)" + cp "$INSTALL_DIR/uninstall.exe" "$TMP_UNINSTALL" + rm -rf "$INSTALL_DIR" + mkdir -p "$INSTALL_DIR" + mv "$TMP_UNINSTALL" "$INSTALL_DIR/uninstall.exe" + cp "$RELEASE_DIR/mouseterm.exe" "$INSTALL_DIR/" + cp "$RELEASE_DIR/node.exe" "$INSTALL_DIR/" + cp -r "$RELEASE_DIR/_up_/" "$INSTALL_DIR/_up_/" + echo "✦ Installed to $INSTALL_DIR" + ;; + *) + echo "--install is not yet implemented for this platform." + exit 1 + ;; + esac +else + # --- Launch mode (default) --- + case "$(uname -s)" in + MINGW*|MSYS*|CYGWIN*|Windows_NT) + "$RELEASE_DIR/mouseterm.exe" ;; + Darwin) + "$RELEASE_DIR/mouseterm" ;; + Linux) + "$RELEASE_DIR/mouseterm" ;; + *) + echo "Unsupported platform: $(uname -s)" + exit 1 ;; + esac +fi diff --git a/standalone/src-tauri/Cargo.lock b/standalone/src-tauri/Cargo.lock index cacc55c..74c181a 100644 --- a/standalone/src-tauri/Cargo.lock +++ b/standalone/src-tauri/Cargo.lock @@ -1940,6 +1940,7 @@ dependencies = [ name = "mouseterm" version = "0.1.0" dependencies = [ + "libc", "serde", "serde_json", "tauri", diff --git a/standalone/src-tauri/Cargo.toml b/standalone/src-tauri/Cargo.toml index f8d90ed..6c11a13 100644 --- a/standalone/src-tauri/Cargo.toml +++ b/standalone/src-tauri/Cargo.toml @@ -23,6 +23,9 @@ tauri-plugin-updater = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" +[target.'cfg(unix)'.dependencies] +libc = "0.2" + [profile.release] strip = true lto = true diff --git a/standalone/src-tauri/src/lib.rs b/standalone/src-tauri/src/lib.rs index 4eba88e..7e48f68 100644 --- a/standalone/src-tauri/src/lib.rs +++ b/standalone/src-tauri/src/lib.rs @@ -11,7 +11,7 @@ use std::{ sync::{Arc, Mutex}, time::{Duration, SystemTime, UNIX_EPOCH}, }; -use tauri::{AppHandle, Emitter, Manager}; +use tauri::{AppHandle, Emitter, Manager, RunEvent}; use tauri_plugin_shell::{process::CommandEvent, ShellExt}; enum SidecarMsg { @@ -26,6 +26,7 @@ struct SidecarState { tx: SidecarSender, pending_requests: PendingRequests, next_request_id: AtomicU64, + child_pid: u32, } const LOG_FILE_ENV: &str = "MOUSETERM_LOG_FILE"; @@ -215,6 +216,28 @@ fn pty_get_scrollback( #[tauri::command] fn shutdown_sidecar(state: tauri::State<'_, SidecarState>) { let _ = state.tx.send(SidecarMsg::Shutdown); + kill_process_tree(state.child_pid); +} + +/// Kill the sidecar process. On Windows, `taskkill /T` kills the entire +/// process tree so that child shell processes don't outlive the sidecar. +/// On Unix, a single SIGTERM to the sidecar is sufficient because node-pty +/// manages its own child processes and cleans them up on exit. +fn kill_process_tree(pid: u32) { + append_log(format!("[sidecar] killing process tree (pid={pid})")); + #[cfg(target_os = "windows")] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x08000000; + let _ = std::process::Command::new("taskkill") + .args(["/F", "/T", "/PID", &pid.to_string()]) + .creation_flags(CREATE_NO_WINDOW) + .output(); + } + #[cfg(unix)] + { + unsafe { libc::kill(pid as i32, libc::SIGTERM); } + } } #[tauri::command] @@ -298,7 +321,8 @@ fn start_sidecar(app: &AppHandle) -> Result { .set_raw_out(false) .spawn() .map_err(|err| format!("failed to start Node.js sidecar: {err}"))?; - append_log("[sidecar] spawned Node.js runtime"); + let child_pid = child.pid(); + append_log(format!("[sidecar] spawned Node.js runtime (pid={child_pid})")); let handle = app.clone(); let pending_requests: PendingRequests = Arc::new(Mutex::new(HashMap::new())); @@ -392,6 +416,7 @@ fn start_sidecar(app: &AppHandle) -> Result { tx, pending_requests, next_request_id: AtomicU64::new(0), + child_pid, }) } @@ -437,8 +462,17 @@ pub fn run() { get_project_dir, get_available_shells, ]) - .run(tauri::generate_context!()) - .expect("error while running MouseTerm"); + .build(tauri::generate_context!()) + .expect("error while building MouseTerm") + .run(|app, event| { + if let RunEvent::Exit = event { + if let Some(state) = app.try_state::() { + append_log("[app] exit — killing sidecar"); + let _ = state.tx.send(SidecarMsg::Shutdown); + kill_process_tree(state.child_pid); + } + } + }); } #[cfg(test)]