Skip to content

Commit c48ef78

Browse files
committed
fix: allow process restart after update and detect Homebrew version mismatch
Add tauri-plugin-process and `process:allow-restart` ACL permission so the updater can relaunch the app after installing an update (fixes "Command plugin:process|restart not allowed by ACL" error on all platforms). Add `get_bundle_version` command that reads CFBundleShortVersionString from the on-disk Info.plist on macOS. When a Homebrew Cask upgrade replaces the app bundle while it's still running, the frontend detects the version mismatch and shows a "Restart Required" banner with a one-click restart button. Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>
1 parent fd2322e commit c48ef78

5 files changed

Lines changed: 102 additions & 9 deletions

File tree

Cargo.lock

Lines changed: 16 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/desktop/src-tauri/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ tauri-plugin-deep-link = "2"
2222
tauri-plugin-updater = "2"
2323
tauri-plugin-autostart = "2"
2424
tauri-plugin-dialog = "2"
25+
tauri-plugin-process = "2"
2526
image = { version = "0.25", default-features = false, features = ["png"] }
2627
serde.workspace = true
2728
serde_json.workspace = true

apps/desktop/src-tauri/capabilities/default.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"opener:default",
1818
"deep-link:default",
1919
"updater:default",
20+
"process:allow-restart",
2021
"dialog:default"
2122
]
2223
}

apps/desktop/src-tauri/src/lib.rs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,12 +122,48 @@ fn init_tracing() -> tracing_appender::non_blocking::WorkerGuard {
122122
guard
123123
}
124124

125-
/// Get app version
125+
/// Get app version (compiled into the binary)
126126
#[tauri::command]
127127
fn get_version() -> String {
128128
env!("CARGO_PKG_VERSION").to_string()
129129
}
130130

131+
/// Get the on-disk bundle version (macOS only).
132+
///
133+
/// After a Homebrew Cask upgrade, the `.app` bundle on disk has the new version
134+
/// but the running process still has the old compiled-in version. Comparing
135+
/// `get_version()` with `get_bundle_version()` lets the frontend detect this
136+
/// mismatch and prompt the user to restart.
137+
#[tauri::command]
138+
fn get_bundle_version() -> Option<String> {
139+
#[cfg(target_os = "macos")]
140+
{
141+
// Read CFBundleShortVersionString from the running app's Info.plist
142+
let exe = std::env::current_exe().ok()?;
143+
// exe is typically: Foo.app/Contents/MacOS/Foo
144+
let contents_dir = exe.parent()?.parent()?;
145+
let plist_path = contents_dir.join("Info.plist");
146+
let plist = std::fs::read_to_string(&plist_path).ok()?;
147+
148+
// Simple extraction — avoids adding a plist parsing dependency.
149+
// Looks for <key>CFBundleShortVersionString</key>\n<string>X.Y.Z</string>
150+
let key = "CFBundleShortVersionString";
151+
let key_pos = plist.find(key)?;
152+
let after_key = &plist[key_pos + key.len()..];
153+
let string_start = after_key.find("<string>")? + "<string>".len();
154+
let string_end = after_key[string_start..].find("</string>")?;
155+
Some(
156+
after_key[string_start..string_start + string_end]
157+
.trim()
158+
.to_string(),
159+
)
160+
}
161+
#[cfg(not(target_os = "macos"))]
162+
{
163+
None
164+
}
165+
}
166+
131167
/// Get the path to the logs directory
132168
#[tauri::command]
133169
fn get_logs_path() -> String {
@@ -195,6 +231,7 @@ pub fn run() {
195231
Some(vec!["--hidden"]), // Start minimized to tray
196232
))
197233
.plugin(tauri_plugin_updater::Builder::new().build())
234+
.plugin(tauri_plugin_process::init())
198235
.plugin(tauri_plugin_single_instance::init(|app, args, cwd| {
199236
// This callback is called when a second instance is launched
200237
info!("Second instance detected, focusing existing window");
@@ -691,6 +728,7 @@ pub fn run() {
691728
})
692729
.invoke_handler(tauri::generate_handler![
693730
get_version,
731+
get_bundle_version,
694732
// Space commands
695733
commands::list_spaces,
696734
commands::get_space,

apps/desktop/src/features/settings/UpdateChecker.tsx

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState } from 'react';
1+
import { useState, useEffect } from 'react';
22
import { check, Update } from '@tauri-apps/plugin-updater';
33
import { relaunch } from '@tauri-apps/plugin-process';
44
import {
@@ -9,7 +9,7 @@ import {
99
CardDescription,
1010
CardContent,
1111
} from '@mcpmux/ui';
12-
import { Download, Loader2, CheckCircle, AlertCircle, RefreshCw } from 'lucide-react';
12+
import { Download, Loader2, CheckCircle, AlertCircle, RefreshCw, RotateCcw } from 'lucide-react';
1313
import { invoke } from '@tauri-apps/api/core';
1414

1515
interface DownloadEvent {
@@ -27,6 +27,7 @@ export function UpdateChecker() {
2727
const [downloadProgress, setDownloadProgress] = useState({ downloaded: 0, total: 0 });
2828
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
2929
const [currentVersion, setCurrentVersion] = useState<string>('');
30+
const [bundleVersionMismatch, setBundleVersionMismatch] = useState<string | null>(null);
3031

3132
// Load current version on mount
3233
useState(() => {
@@ -35,6 +36,23 @@ export function UpdateChecker() {
3536
.catch((err) => console.error('Failed to get version:', err));
3637
});
3738

39+
// Check if the on-disk bundle version differs from the running version (Homebrew Cask upgrades)
40+
useEffect(() => {
41+
if (!currentVersion) return;
42+
invoke<string | null>('get_bundle_version')
43+
.then((bundleVersion) => {
44+
if (bundleVersion && bundleVersion !== currentVersion) {
45+
console.log(
46+
`[Updater] Bundle version mismatch: running=${currentVersion}, on-disk=${bundleVersion}`
47+
);
48+
setBundleVersionMismatch(bundleVersion);
49+
}
50+
})
51+
.catch(() => {
52+
// Expected to return null on non-macOS platforms
53+
});
54+
}, [currentVersion]);
55+
3856
const checkForUpdates = async () => {
3957
setChecking(true);
4058
setMessage(null);
@@ -149,8 +167,32 @@ export function UpdateChecker() {
149167
</p>
150168
</div>
151169

170+
{/* Bundle version mismatch (e.g., after brew upgrade) */}
171+
{bundleVersionMismatch && (
172+
<div
173+
className="border rounded-lg p-4 space-y-3 bg-surface-secondary"
174+
data-testid="restart-required"
175+
>
176+
<div>
177+
<p className="font-medium text-lg">Restart Required</p>
178+
<p className="text-sm text-[rgb(var(--muted))] mt-1">
179+
Version v{bundleVersionMismatch} has been installed on disk, but you are still
180+
running v{currentVersion}. Restart to apply the update.
181+
</p>
182+
</div>
183+
<Button
184+
onClick={() => relaunch()}
185+
variant="primary"
186+
data-testid="restart-now-btn"
187+
>
188+
<RotateCcw className="h-4 w-4 mr-2" />
189+
Restart Now
190+
</Button>
191+
</div>
192+
)}
193+
152194
{/* Check Button */}
153-
{!updateInfo && (
195+
{!updateInfo && !bundleVersionMismatch && (
154196
<Button
155197
onClick={checkForUpdates}
156198
disabled={checking || downloading}

0 commit comments

Comments
 (0)