diff --git a/.github/workflows/application-release.yaml b/.github/workflows/application-release.yaml new file mode 100644 index 0000000..8fdfde9 --- /dev/null +++ b/.github/workflows/application-release.yaml @@ -0,0 +1,102 @@ +name: application-release step + +on: + workflow_run: + workflows: ["code coverage"] + types: + - completed + +permissions: + contents: write + +jobs: + # Auto tagging for release version + tag: + runs-on: ubuntu-latest + outputs: + new_tag: ${{ steps.tag_version.outputs.new_tag }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # gives it full history of commits to find the last tag + + - name: Auto-increment Tag + id: tag_version + uses: anothrNick/github-tag-action@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + build-and-release: + needs: tag + runs-on: ${{ matrix.platform }} + strategy: # This is for building the tauri app to work on mac, linux, and windows + fail-fast: false + matrix: + include: + - platform: 'macos-latest' # for Arm based macs (M1 and above). + args: '--target aarch64-apple-darwin' + rust_target: 'aarch64-apple-darwin' + go_binary_ext: '' + - platform: 'macos-latest' # for Intel based macs. + args: '--target x86_64-apple-darwin' + rust_target: 'x86_64-apple-darwin' + go_binary_ext: '' + - platform: 'ubuntu-24.04' # for linux ubuntu + args: '' + rust_target: 'x86_64-unknown-linux-gnu' + go_binary_ext: '' + - platform: 'windows-latest' + args: '' + rust_target: 'x86_64-pc-windows-msvc' + go_binary_ext: '.exe' + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: setup node + uses: actions/setup-node@v5 + with: + node-version: '>=23.6.1' + + - name: install Rust stable + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} + + - name: install frontend dependencies + run: npm install + working-directory: frontend + + - name: Build Frontend + run: npm run build + working-directory: frontend + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: "1.24.0" + + - name: Build Go backend client + run: go build -o ../frontend/src-tauri/binaries/backend-client-${{ matrix.rust_target }}${{ matrix.go_binary_ext }} ./cmd/main.go + working-directory: backend/client + + - uses: tauri-apps/tauri-action@v0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + args: ${{ matrix.args }} + projectPath: frontend + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + body_path: ${{ github.workspace }}/CHANGELOG.txt + tag_name: ${{ needs.tag.outputs.new_tag }} + name: Release ${{ needs.tag.outputs.new_tag }} + files: | + frontend/src-tauri/target/**/release/bundle/nsis/*.exe + frontend/src-tauri/target/**/release/bundle/dmg/*.dmg + frontend/src-tauri/target/**/release/bundle/macos/*.app + frontend/src-tauri/target/**/release/bundle/deb/*.deb + frontend/src-tauri/target/**/release/bundle/appimage/*.AppImage + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/backend/client/cmd/main.go b/backend/client/cmd/main.go index 3a4ea96..645cd1c 100644 --- a/backend/client/cmd/main.go +++ b/backend/client/cmd/main.go @@ -9,7 +9,6 @@ import ( "os/signal" "syscall" - "github.com/joho/godotenv" "github.com/kelseyhightower/envconfig" at "mosaic-client.com/gen/audio_transcription" cb "mosaic-client.com/gen/conversation_briefing" @@ -91,14 +90,9 @@ func websocketServer( // method to load config values func loadConfig() (*Config, error) { - err := godotenv.Load(".env") - if err != nil { - return nil, err - } - var cfg Config - err = envconfig.Process("", &cfg) + err := envconfig.Process("", &cfg) if err != nil { return nil, err } diff --git a/frontend/package.json b/frontend/package.json index 34315f1..84fc724 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,6 +9,7 @@ "tauri:dev:ubuntu": "env -u GTK_PATH -u GTK_EXE_PREFIX -u GIO_MODULE_DIR -u GSETTINGS_SCHEMA_DIR -u GTK_IM_MODULE_FILE -u LOCPATH tauri dev", "preview": "vite preview", "tauri": "tauri", + "tauri:build": "tauri build", "lint": "eslint .", "test": "vitest", "test:ui": "vitest --ui", diff --git a/frontend/src-tauri/Cargo.lock b/frontend/src-tauri/Cargo.lock index aa914b0..25d378c 100644 --- a/frontend/src-tauri/Cargo.lock +++ b/frontend/src-tauri/Cargo.lock @@ -787,6 +787,15 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "endi" version = "1.1.1" @@ -960,6 +969,7 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-opener", + "tauri-plugin-shell", ] [[package]] @@ -2248,6 +2258,16 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "pango" version = "0.18.3" @@ -3136,12 +3156,44 @@ dependencies = [ "digest", ] +[[package]] +name = "shared_child" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" +dependencies = [ + "libc", + "sigchld", + "windows-sys 0.60.2", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "sigchld" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" +dependencies = [ + "libc", + "os_pipe", + "signal-hook", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -3551,6 +3603,27 @@ dependencies = [ "zbus", ] +[[package]] +name = "tauri-plugin-shell" +version = "2.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8457dbf9e2bab1edd8df22bb2c20857a59a9868e79cb3eac5ed639eec4d0c73b" +dependencies = [ + "encoding_rs", + "log", + "open", + "os_pipe", + "regex", + "schemars 0.8.22", + "serde", + "serde_json", + "shared_child", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "tokio", +] + [[package]] name = "tauri-runtime" version = "2.10.1" diff --git a/frontend/src-tauri/Cargo.toml b/frontend/src-tauri/Cargo.toml index dab7755..8382af9 100644 --- a/frontend/src-tauri/Cargo.toml +++ b/frontend/src-tauri/Cargo.toml @@ -22,4 +22,5 @@ tauri = { version = "2", features = [] } tauri-plugin-opener = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" +tauri-plugin-shell = "2" diff --git a/frontend/src-tauri/capabilities/default.json b/frontend/src-tauri/capabilities/default.json index c6d044c..70f7709 100644 --- a/frontend/src-tauri/capabilities/default.json +++ b/frontend/src-tauri/capabilities/default.json @@ -10,6 +10,8 @@ "core:window:allow-current-monitor", "core:window:allow-destroy", "core:window:allow-set-size", - "opener:default" + "opener:default", + "shell:allow-execute", + "shell:allow-kill" ] } diff --git a/frontend/src-tauri/src/backend_utils.rs b/frontend/src-tauri/src/backend_utils.rs index b0e7550..dc65136 100644 --- a/frontend/src-tauri/src/backend_utils.rs +++ b/frontend/src-tauri/src/backend_utils.rs @@ -1,12 +1,12 @@ -use std::process::{Command, Stdio}; use tauri::State; -use crate::{BackendProcesses, port_utils, path_utils}; +use tauri_plugin_shell::ShellExt; +use tauri_plugin_shell::process::CommandEvent; +use crate::{BackendProcesses, port_utils}; #[tauri::command] pub fn start_backend_api(state: State, app_handle: tauri::AppHandle) -> Result { println!("[Rust] start_backend_api called"); - // Check if already running { let children = state.process_children.lock().unwrap(); if !children.is_empty() { @@ -24,79 +24,52 @@ pub fn start_backend_api(state: State, app_handle: tauri::AppH *is_starting = true; } - // Clone state for background thread - let state_clone = state.inner().clone(); - - // Spawn backend startup in background thread to avoid blocking UI - std::thread::spawn(move || { - println!("[Rust] Background thread: Starting backend startup sequence"); - - // Check and clean up ports if needed - if port_utils::ports_in_use() { - println!("[Rust] Port 8080 in use by untracked process, cleaning up..."); - port_utils::kill_processes_on_ports(); - port_utils::wait_for_ports_free(std::time::Duration::from_secs(5)); - } + if port_utils::ports_in_use() { + println!("[Rust] Port 8080 in use by untracked process, cleaning up..."); + port_utils::kill_processes_on_ports(); + port_utils::wait_for_ports_free(std::time::Duration::from_secs(5)); + } - let backend_path = match path_utils::get_backend_path(&app_handle) { - Ok(p) => p, - Err(e) => { - eprintln!("[Rust] Failed to get backend path: {}", e); - *state_clone.is_starting.lock().unwrap() = false; - return; + let sidecar = app_handle + .shell() + .sidecar("backend-client") + .map_err(|e| { + *state.is_starting.lock().unwrap() = false; + format!("Failed to create sidecar command: {}", e) + })?; + + let (rx, child) = sidecar + .spawn() + .map_err(|e| { + *state.is_starting.lock().unwrap() = false; + format!("Failed to spawn backend sidecar: {}", e) + })?; + + let pid = child.pid(); + println!("[Rust] Backend sidecar started successfully! PID: {}", pid); + + // Forward backend stdout/stderr so logs remain visible (mirrors the old Stdio::inherit() behaviour) + let state_clone = state.inner().clone(); + tauri::async_runtime::spawn(async move { + let mut rx = rx; + while let Some(event) = rx.recv().await { + match event { + CommandEvent::Stdout(line) => print!("[backend] {}", String::from_utf8_lossy(&line)), + CommandEvent::Stderr(line) => eprint!("[backend] {}", String::from_utf8_lossy(&line)), + CommandEvent::Terminated(status) => { + println!("[Rust] Backend process terminated: {:?}", status); + state_clone.process_children.lock().unwrap().clear(); + break; + } + _ => {} } - }; - - println!("[Rust] Backend resource path: {}", backend_path.display()); - - // Go client is in backend/client directory - let go_client_path = backend_path.join("client"); - - if !go_client_path.exists() { - eprintln!("[Rust] Go client directory not found at: {}", go_client_path.display()); - *state_clone.is_starting.lock().unwrap() = false; - return; } - - let workspace_root = backend_path - .parent() - .expect("failed to get workspace root from backend path"); - - println!("[Rust] Workspace root: {}", workspace_root.display()); - println!("[Rust] Go client path: {}", go_client_path.display()); - - let mut backend_cmd = Command::new("go"); - backend_cmd - .args(&["run", "./cmd/main.go"]) - .current_dir(&go_client_path) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()); - - let backend_process = match backend_cmd.spawn() { - Ok(p) => p, - Err(e) => { - eprintln!("[Rust] Failed to start Go backend: {}\nWorkspace: {}\nGo client path: {}", - e, workspace_root.display(), go_client_path.display()); - *state_clone.is_starting.lock().unwrap() = false; - return; - } - }; - - let backend_pid = backend_process.id(); - - // Store the process - let mut children = state_clone.process_children.lock().unwrap(); - children.clear(); - children.push(backend_process); - - println!("[Rust] Backend started successfully! PID: {}", backend_pid); - - // Reset the starting flag now that we've successfully started - *state_clone.is_starting.lock().unwrap() = false; }); - // Return immediately to UI - Ok("Backend starting in background...".to_string()) + state.process_children.lock().unwrap().push(child); + *state.is_starting.lock().unwrap() = false; + + Ok("Backend started".to_string()) } #[tauri::command] @@ -106,7 +79,7 @@ pub fn stop_backend_api(state: State) -> Result = children.iter().map(|c| c.id()).collect(); + let pids: Vec = children.iter().map(|c| c.pid()).collect(); println!("[Rust] stop_backend_api called. Stored PIDs: {:?}", pids); if pids.is_empty() && !port_utils::ports_in_use() { @@ -114,20 +87,12 @@ pub fn stop_backend_api(state: State) -> Result { - println!("[Rust] kill() succeeded for PID {}. Waiting for exit...", pid); - match child.wait() { - Ok(status) => println!("[Rust] PID {} exited with status {}", pid, status), - Err(e) => println!("[Rust] waiting for PID {} failed: {}", pid, e), - } - } - Err(e) => { - println!("[Rust] Child::kill() failed for PID {}: {}", pid, e); - } + Ok(_) => println!("[Rust] kill() succeeded for PID {}", pid), + Err(e) => println!("[Rust] kill() failed for PID {}: {}", pid, e), } } @@ -139,6 +104,9 @@ pub fn stop_backend_api(state: State) -> Result>>, + process_children: Arc>>, is_starting: Arc>, } @@ -21,9 +22,25 @@ pub fn run() { backend_utils::stop_backend_api, ]) .plugin(tauri_plugin_opener::init()) + .plugin(tauri_plugin_shell::init()) .setup(|_app| { Ok(()) }) - .run(tauri::generate_context!()) - .expect("error while running tauri application"); + .build(tauri::generate_context!()) + .expect("error while building tauri application") + .run(|app, event| { + if let tauri::RunEvent::ExitRequested { .. } = event { + let state = app.state::(); + let mut children = state.process_children.lock().unwrap(); + for child in children.drain(..) { + let pid = child.pid(); + if let Err(e) = child.kill() { + eprintln!("[Rust] Failed to kill backend PID {} on exit: {}", pid, e); + } else { + println!("[Rust] Killed backend PID {} on app exit", pid); + } + } + port_utils::kill_processes_on_ports(); + } + }); } diff --git a/frontend/src-tauri/src/path_utils.rs b/frontend/src-tauri/src/path_utils.rs deleted file mode 100644 index c2dfe4e..0000000 --- a/frontend/src-tauri/src/path_utils.rs +++ /dev/null @@ -1,34 +0,0 @@ -use std::path::PathBuf; -use tauri::Manager; - -pub fn get_backend_path(app_handle: &tauri::AppHandle) -> Result { - let resource_dir = app_handle - .path() - .resource_dir() - .expect("failed to get resource dir"); - - let is_dev_mode = resource_dir.to_string_lossy().contains("debug"); - - if is_dev_mode { - println!("[Rust] Development mode detected, using actual backend"); - let current_dir = std::env::current_dir().expect("failed to get current dir"); - Ok(current_dir - .parent() - .and_then(|p| p.parent()) - .expect("failed to determine workspace root") - .join("backend")) - } else { - let backend_bundled_path = resource_dir.join("_up_").join("_up_").join("backend"); - let backend_direct_path = resource_dir.join("backend"); - - if backend_bundled_path.exists() { - println!("[Rust] Production - using bundled backend (_up_/_up_/backend)"); - Ok(backend_bundled_path) - } else if backend_direct_path.exists() { - println!("[Rust] Production - using bundled backend (direct)"); - Ok(backend_direct_path) - } else { - Err("Backend not found in production build".to_string()) - } - } -} diff --git a/frontend/src-tauri/tauri.conf.json b/frontend/src-tauri/tauri.conf.json index e28bbbd..68b4725 100644 --- a/frontend/src-tauri/tauri.conf.json +++ b/frontend/src-tauri/tauri.conf.json @@ -30,6 +30,9 @@ "bundle": { "active": true, "targets": "all", + "externalBin": [ + "binaries/backend-client" + ], "icon": [ "icons/32x32.png", "icons/128x128.png",