From 936052b95b1ff1e8931d5d825ebae4ad9d0267d5 Mon Sep 17 00:00:00 2001 From: Irfan Hardiyanto <52022757+devhardiyanto@users.noreply.github.com> Date: Sat, 25 Jul 2026 20:16:52 +0700 Subject: [PATCH 1/4] fix(tauri): resolve tool PATH for GUI-launched apps on Linux/macOS AppImage/.app yang dilaunch dari desktop launcher tidak mewarisi PATH shell interaktif, sehingga tool yang di-install via shell rc (fnm, phpvm) tak terdeteksi walau sudah terpasang. Tambah resolusi PATH via login-interactive shell (timeout 5s, cached) + merge dir install umum, diterapkan di silent_command. Route probe_version + docker spawn (services, stats) lewat silent_command agar konsisten. devhardiyanto --- src-tauri/src/commands/prereq.rs | 2 +- src-tauri/src/commands/services.rs | 19 ++----- src-tauri/src/commands/stats.rs | 12 ++--- src-tauri/src/commands/util.rs | 84 ++++++++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 26 deletions(-) diff --git a/src-tauri/src/commands/prereq.rs b/src-tauri/src/commands/prereq.rs index 4203b56..bd53388 100644 --- a/src-tauri/src/commands/prereq.rs +++ b/src-tauri/src/commands/prereq.rs @@ -88,7 +88,7 @@ async fn probe_version(cmd: &str, args: &[&str]) -> Option { }; #[cfg(not(target_os = "windows"))] - let output = Command::new(cmd) + let output = silent_command(cmd) .args(args) .stdout(Stdio::piped()) .stderr(Stdio::piped()) diff --git a/src-tauri/src/commands/services.rs b/src-tauri/src/commands/services.rs index 8fe2e65..f346f25 100644 --- a/src-tauri/src/commands/services.rs +++ b/src-tauri/src/commands/services.rs @@ -4,7 +4,7 @@ use std::process::Stdio; use tauri::{AppHandle, Manager}; use tokio::process::Command; -use crate::commands::util::stream_and_wait_app; +use crate::commands::util::{silent_command, stream_and_wait_app}; @@ -162,17 +162,11 @@ pub(crate) async fn load_services_internal(app: &AppHandle) -> Result Result, String> { - let mut cmd = Command::new("docker"); + let mut cmd = silent_command("docker"); cmd.args(["ps", "-a", "--filter", "name=servel_", "--format", "json"]) .stdout(Stdio::piped()) .stderr(Stdio::piped()); - #[cfg(target_os = "windows")] - { - const CREATE_NO_WINDOW: u32 = 0x08000000; - cmd.creation_flags(CREATE_NO_WINDOW); - } - let output = cmd .output() .await @@ -188,14 +182,7 @@ pub(crate) async fn services_status_internal() -> Result, Str } fn new_docker_cmd() -> Command { - #[cfg_attr(not(target_os = "windows"), allow(unused_mut))] - let mut cmd = Command::new("docker"); - #[cfg(target_os = "windows")] - { - const CREATE_NO_WINDOW: u32 = 0x08000000; - cmd.creation_flags(CREATE_NO_WINDOW); - } - cmd + silent_command("docker") } diff --git a/src-tauri/src/commands/stats.rs b/src-tauri/src/commands/stats.rs index 9b69ac6..b7ab5d8 100644 --- a/src-tauri/src/commands/stats.rs +++ b/src-tauri/src/commands/stats.rs @@ -1,6 +1,7 @@ use serde::Serialize; use std::process::Stdio; -use tokio::process::Command; + +use super::util::silent_command; /// Single container memory snapshot — payload entry untuk event `container-stats-changed`. #[derive(Serialize, Clone, Debug, PartialEq)] @@ -84,18 +85,11 @@ pub fn parse_docker_stats_json(stdout: &str) -> Vec { /// Panggil `docker stats --no-stream --format json` (tanpa `--filter` agar kompatibel /// di Docker lama). Filter prefix `servel_` dilakukan di Rust saat parse. pub async fn fetch_container_stats() -> Result, String> { - #[cfg_attr(not(target_os = "windows"), allow(unused_mut))] - let mut cmd = Command::new("docker"); + let mut cmd = silent_command("docker"); cmd.args(["stats", "--no-stream", "--format", "json"]) .stdout(Stdio::piped()) .stderr(Stdio::piped()); - #[cfg(target_os = "windows")] - { - const CREATE_NO_WINDOW: u32 = 0x08000000; - cmd.creation_flags(CREATE_NO_WINDOW); - } - let output = cmd .output() .await diff --git a/src-tauri/src/commands/util.rs b/src-tauri/src/commands/util.rs index 1ed0fc4..d8d44ac 100644 --- a/src-tauri/src/commands/util.rs +++ b/src-tauri/src/commands/util.rs @@ -19,9 +19,93 @@ pub fn silent_command(program: &str) -> Command { cmd.creation_flags(CREATE_NO_WINDOW); } + // GUI-launched apps (AppImage / .app dari launcher) TIDAK mewarisi PATH shell + // interaktif, jadi tool yang di-install via shell rc (fnm, phpvm → ~/.local/bin, + // ~/.fnm, dll.) tak ketemu. Inject PATH hasil resolusi login-shell + dir umum. + #[cfg(not(target_os = "windows"))] + { + cmd.env("PATH", resolved_path()); + } + cmd } +/// PATH yang sudah di-augment untuk spawn tool eksternal di Linux/macOS. +/// Di-resolve sekali (login-interactive shell → source .bashrc/.zshrc) lalu di-cache. +#[cfg(not(target_os = "windows"))] +pub fn resolved_path() -> &'static str { + use std::sync::OnceLock; + static CACHE: OnceLock = OnceLock::new(); + CACHE.get_or_init(build_resolved_path) +} + +/// Susun PATH: PATH login-shell (bila terbaca) ∪ PATH proses ∪ dir install umum. +/// Dedup jaga urutan; buang segmen kosong. +#[cfg(not(target_os = "windows"))] +fn build_resolved_path() -> String { + use std::collections::HashSet; + + let mut parts: Vec = Vec::new(); + + if let Some(shell_path) = capture_login_shell_path() { + parts.extend(shell_path.split(':').map(str::to_string)); + } + if let Ok(current) = std::env::var("PATH") { + parts.extend(current.split(':').map(str::to_string)); + } + + let home = std::env::var("HOME").unwrap_or_default(); + if !home.is_empty() { + parts.push(format!("{home}/.local/bin")); + parts.push(format!("{home}/.fnm")); + parts.push(format!("{home}/.local/share/fnm")); + parts.push(format!("{home}/.cargo/bin")); + } + parts.push("/usr/local/bin".to_string()); + parts.push("/opt/homebrew/bin".to_string()); + + let mut seen = HashSet::new(); + parts + .into_iter() + .filter(|p| !p.is_empty() && seen.insert(p.clone())) + .collect::>() + .join(":") +} + +/// Jalankan login-interactive shell user untuk membaca PATH final-nya (setelah +/// .bashrc/.zshrc di-source). Timeout 5s + stdin null supaya tak pernah hang. +/// Return None kalau shell gagal / tak ada / kosong (caller fallback ke dir umum). +#[cfg(not(target_os = "windows"))] +fn capture_login_shell_path() -> Option { + use std::process::{Command as StdCommand, Stdio as StdStdio}; + use std::sync::mpsc; + use std::time::Duration; + + let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".to_string()); + let (tx, rx) = mpsc::channel(); + + std::thread::spawn(move || { + let out = StdCommand::new(&shell) + .args(["-lic", "printf '%s' \"$PATH\""]) + .stdin(StdStdio::null()) + .stderr(StdStdio::null()) + .output(); + let _ = tx.send(out); + }); + + match rx.recv_timeout(Duration::from_secs(5)) { + Ok(Ok(out)) if out.status.success() => { + let s = String::from_utf8_lossy(&out.stdout).trim().to_string(); + if s.is_empty() { + None + } else { + Some(s) + } + } + _ => None, + } +} + /// Build a PowerShell command that executes `script_body` as a `-Command` string. /// Flags: -NoProfile -NoLogo -NonInteractive -ExecutionPolicy Bypass. /// Stdout/stderr default to null; caller overrides before spawning. From 375b7203c4e4b37537aadb14b1c139b8af10681a Mon Sep 17 00:00:00 2001 From: Irfan Hardiyanto <52022757+devhardiyanto@users.noreply.github.com> Date: Sat, 25 Jul 2026 20:44:15 +0700 Subject: [PATCH 2/4] feat(tauri): add get_platform command for platform-aware UI Command get_platform() mengembalikan "windows"/"macos"/"linux" via cfg, dipakai frontend untuk menampilkan instruksi prasyarat sesuai OS. devhardiyanto --- src-tauri/src/commands/prereq.rs | 14 ++++++++++++++ src-tauri/src/lib.rs | 1 + 2 files changed, 15 insertions(+) diff --git a/src-tauri/src/commands/prereq.rs b/src-tauri/src/commands/prereq.rs index bd53388..615100f 100644 --- a/src-tauri/src/commands/prereq.rs +++ b/src-tauri/src/commands/prereq.rs @@ -170,3 +170,17 @@ pub async fn start_docker() -> Result<(), String> { Err("Docker daemon harus distart manual via systemctl start docker.".to_string()) } } + +/// Return OS platform sebagai string: `"windows"` | `"macos"` | `"linux"`. +/// Dipakai frontend untuk menampilkan instruksi prasyarat yang sesuai OS +/// (mis. perintah install fnm, guidance Docker). +#[tauri::command] +pub fn get_platform() -> String { + if cfg!(target_os = "windows") { + "windows".to_string() + } else if cfg!(target_os = "macos") { + "macos".to_string() + } else { + "linux".to_string() + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 468a346..5d0d252 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -65,6 +65,7 @@ pub fn run() { .invoke_handler(tauri::generate_handler![ commands::prereq::check_prerequisites, commands::prereq::start_docker, + commands::prereq::get_platform, commands::php::php_list_installed, commands::php::php_get_active, commands::php::php_switch, From 8c80779643bdb7abba7edd2ab66e3357c0da9e46 Mon Sep 17 00:00:00 2001 From: Irfan Hardiyanto <52022757+devhardiyanto@users.noreply.github.com> Date: Sat, 25 Jul 2026 21:15:54 +0700 Subject: [PATCH 3/4] fix(frontend): platform-aware prerequisite instructions in onboarding Instruksi prasyarat sebelumnya hard-coded Windows. Sekarang ambil OS via get_platform: perintah install fnm per-OS (winget/brew/curl), actions Docker per-OS (Linux=Docker Engine + post-install, macOS=Desktop), dan pada Linux tampilkan instruksi `systemctl start docker` langsung (sembunyikan tombol Start Docker yang tak berlaku). devhardiyanto --- src/views/Onboarding.vue | 85 ++++++++++++++++++++++++++++++---------- 1 file changed, 64 insertions(+), 21 deletions(-) diff --git a/src/views/Onboarding.vue b/src/views/Onboarding.vue index 0bb6ad8..d35a76b 100644 --- a/src/views/Onboarding.vue +++ b/src/views/Onboarding.vue @@ -1,13 +1,24 @@