Compare commits

..
Author SHA1 Message Date
Lotus CIandClaude Opus 5.5 261d7852cb fix(updater): package installs update through their package manager
On CachyOS the in-app update failed with "Permission denied (os error 13)
at path /usr/bin/tauri_current_app…": the app was installed from the
Arch package, and Tauri's Linux updater can only replace an AppImage — for
anything else it tries to write next to the binary in /usr/bin.

- update_install_kind command: "in-app" on Windows and for an AppImage
  ($APPIMAGE set); on Linux package installs "pacman" (ID/ID_LIKE arch:
  Arch, CachyOS, Manjaro, EndeavourOS…), "deb" (debian/ubuntu and
  derivatives) or "manual". The web UI shows the matching update command.
- install_update refuses up front on a package install
  ("install: package-managed (…)") instead of downloading the whole
  update and failing at the last step.

Tests: CachyOS/Arch → pacman; Ubuntu/Debian/Mint → deb; Fedora/unknown →
manual; AppImage and Windows → in-app.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
2026-09-30 18:46:11 -04:00
jared 06fe43c3cd Merge pull request 'Windows smoke: foreign-page mic check on a local origin (#19)' (#31) from smoke-foreign-origin into main
Build Lotus Chat Desktop / prepare (push) Successful in 2s
Build Lotus Chat Desktop / build-linux (push) Successful in 24m28s
Build Lotus Chat Desktop / build-arch (push) Successful in 14s
Build Lotus Chat Desktop / build-windows (push) Successful in 26m6s
Build Lotus Chat Desktop / update-manifest (push) Successful in 6s
Merge pull request #31: smoke foreign-page check (#19)
2026-09-30 12:46:31 -04:00
Lotus CIandClaude Opus 5.5 bcbc5ecdfb windows smoke: foreign-page mic check on a local origin, navigation verified
The example.com check could pass vacuously if the runner can't reach the
internet (goto failed silently, the mic request then came from the app's
own page). Serve a page on http://localhost:9333 instead, confirm the
navigation happened, and fail on builds that should refuse it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
2026-09-30 12:46:30 -04:00
jared 61ab464b45 Merge pull request 'CI: Windows smoke test of the installed app (#19)' (#30) from ci-windows-smoke into main
Build Lotus Chat Desktop / prepare (push) Successful in 36s
Build Lotus Chat Desktop / build-linux (push) Successful in 24m53s
Build Lotus Chat Desktop / build-windows (push) Successful in 25m1s
Build Lotus Chat Desktop / build-arch (push) Successful in 38s
Build Lotus Chat Desktop / update-manifest (push) Successful in 6s
Merge pull request #30: Windows smoke test (#19)
2026-09-30 10:59:37 -04:00
Lotus CI 2be36d14a9 chore: bump cinny submodule to 747400ea (nightly catch-up)
Build Lotus Chat Desktop / prepare (push) Successful in 2s
Build Lotus Chat Desktop / build-linux (push) Successful in 23m13s
Build Lotus Chat Desktop / build-arch (push) Successful in 15s
Build Lotus Chat Desktop / build-windows (push) Successful in 24m37s
Build Lotus Chat Desktop / update-manifest (push) Successful in 2s
2026-09-30 04:01:05 +00:00
3 changed files with 114 additions and 15 deletions
+1 -1
Submodule cinny updated: d6548c56fd...747400ea25
+32 -14
View File
@@ -11,6 +11,7 @@
// instead of failing, so the same script runs on main and on PR branches.
import { writeFileSync, mkdirSync } from 'node:fs';
import { execSync } from 'node:child_process';
import { createServer } from 'node:http';
import { chromium } from 'playwright-core';
const OUT = process.argv[2] || 'smoke-out';
@@ -150,21 +151,38 @@ else if (!kc.supported) record('keychain round trip', 'fail', 'secure_session_su
else record('keychain round trip', kc.roundTrip && kc.restored ? 'pass' : 'fail', JSON.stringify(kc));
// 7. A foreign page loaded in the window must not get the microphone (#22).
await page.goto('https://example.com/').catch(() => undefined);
const foreignMic = await page.evaluate(async () => {
try {
const s = await navigator.mediaDevices.getUserMedia({ audio: true });
s.getTracks().forEach((t) => t.stop());
return 'ok';
} catch (e) {
return e.name;
}
// Served locally on another port (a different origin, still a secure context),
// so the check doesn't depend on the runner reaching the internet.
const foreign = createServer((_, res) => {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end('<!doctype html><title>foreign</title>foreign page');
});
record(
'microphone refused to a foreign page',
foreignMic === 'NotAllowedError' ? 'pass' : 'info',
`${foreignMic}${foreignMic === 'ok' ? ' (build without the #22 origin check)' : ''}`,
);
await new Promise((r) => foreign.listen(9333, '127.0.0.1', r));
const FOREIGN = 'http://localhost:9333/';
await page.goto(FOREIGN).catch(() => undefined);
if (!page.url().startsWith(FOREIGN)) {
record('microphone refused to a foreign page', 'fail', `navigation did not happen (at ${page.url()})`);
} else {
const foreignMic = await page.evaluate(async () => {
try {
const s = await navigator.mediaDevices.getUserMedia({ audio: true });
s.getTracks().forEach((t) => t.stop());
return 'ok';
} catch (e) {
return e.name;
}
});
const guarded = cfg.desktopCallOrigin !== undefined; // builds with #22 also carry #43
let status = 'info';
if (foreignMic === 'NotAllowedError') status = 'pass';
else if (guarded) status = 'fail';
record(
'microphone refused to a foreign page',
status,
`${foreignMic} at ${page.url()}${status === 'info' ? ' (build without the #22 origin check)' : ''}`,
);
}
foreign.close();
await page.goto(APP).catch(() => undefined);
await page.screenshot({ path: `${OUT}/02-end.png` }).catch(() => undefined);
+81
View File
@@ -189,6 +189,47 @@ mod update_retry {
}
}
/// How this copy of the app gets updated.
///
/// Tauri's updater can replace the Windows install and a Linux AppImage, but
/// not a copy installed by a package manager: it tries to write next to the
/// binary in /usr/bin and fails with "Permission denied (os error 13)"
/// (reported on CachyOS). Those installs update through their package
/// manager instead; the web UI shows the right command.
pub(crate) fn install_kind(linux: bool, appimage: bool, os_release: &str) -> &'static str {
if !linux || appimage {
return "in-app";
}
let field = |key: &str| {
os_release
.lines()
.find_map(|l| l.strip_prefix(key).and_then(|v| v.strip_prefix('=')))
.map(|v| v.trim().trim_matches('"').to_ascii_lowercase())
.unwrap_or_default()
};
let ids = format!("{} {}", field("ID"), field("ID_LIKE"));
let has = |name: &str| ids.split_whitespace().any(|w| w == name);
if has("arch") {
"pacman"
} else if has("debian") || has("ubuntu") {
"deb"
} else {
"manual"
}
}
#[tauri::command]
fn update_install_kind() -> &'static str {
let linux = cfg!(target_os = "linux");
let appimage = std::env::var_os("APPIMAGE").is_some();
let os_release = if linux {
std::fs::read_to_string("/etc/os-release").unwrap_or_default()
} else {
String::new()
};
install_kind(linux, appimage, &os_release)
}
#[tauri::command]
async fn check_for_update(app: tauri::AppHandle) -> Result<UpdateInfo, String> {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
@@ -211,6 +252,15 @@ async fn install_update(app: tauri::AppHandle) -> Result<(), String> {
{
use std::time::{Duration, Instant};
// A package-manager install can't be replaced in place (see
// install_kind); refuse before downloading anything.
let kind = update_install_kind();
if kind != "in-app" {
return Err(format!(
"install: package-managed ({kind}): update Lotus Chat with your package manager"
));
}
let emit = |detail: serde_json::Value| {
native::emit_to_web(&app, "lotus-update-progress", &detail.to_string());
};
@@ -939,6 +989,7 @@ pub fn run() {
send_notification,
check_for_update,
install_update,
update_install_kind,
native::power::set_call_active,
native::jumplist::set_jump_list,
native::thumbbar::set_thumbbar,
@@ -1333,6 +1384,36 @@ mod webview2_args_tests {
}
}
#[cfg(test)]
mod install_kind_tests {
use super::install_kind;
#[test]
fn package_installs_update_through_their_package_manager() {
let cachy = "NAME=\"CachyOS Linux\"\nID=cachyos\nID_LIKE=arch\n";
let arch = "NAME=\"Arch Linux\"\nID=arch\n";
let ubuntu = "NAME=\"Ubuntu\"\nID=ubuntu\nID_LIKE=debian\n";
let debian = "ID=debian\n";
let mint = "ID=linuxmint\nID_LIKE=\"ubuntu debian\"\n";
let fedora = "ID=fedora\n";
assert_eq!(install_kind(true, false, cachy), "pacman");
assert_eq!(install_kind(true, false, arch), "pacman");
assert_eq!(install_kind(true, false, ubuntu), "deb");
assert_eq!(install_kind(true, false, debian), "deb");
assert_eq!(install_kind(true, false, mint), "deb");
assert_eq!(install_kind(true, false, fedora), "manual");
assert_eq!(install_kind(true, false, ""), "manual");
// ID_LIKE mentioning arch only as part of a longer word doesn't count.
assert_eq!(install_kind(true, false, "ID=x\nID_LIKE=archlike\n"), "manual");
}
#[test]
fn appimage_and_windows_update_in_app() {
assert_eq!(install_kind(true, true, "ID=cachyos\nID_LIKE=arch\n"), "in-app");
assert_eq!(install_kind(false, false, ""), "in-app");
}
}
#[cfg(test)]
mod tray_tests {
use super::*;