From 289ef40e7fd03fcc564c005749a1c9aa466cbd7e Mon Sep 17 00:00:00 2001 From: Omid Asgari Date: Tue, 20 Oct 2026 22:05:48 +0330 Subject: [PATCH 1/4] Relaunch the replaced macOS app after the old process exits. Closing the window left the previous BiFlow process running, so the finished update never became the version on screen. Co-authored-by: Cursor --- AGENTS.md | 7 + Cargo.lock | 30 +-- Cargo.toml | 2 +- apps/desktop/package.json | 2 +- docs/adr/0127-macos-update-relaunch.md | 22 ++ docs/adr/README.md | 1 + package.json | 2 +- src-tauri/src/github_update.rs | 301 ++++++++++++++++++++----- src-tauri/tauri.conf.json | 2 +- version | 2 +- 10 files changed, 292 insertions(+), 79 deletions(-) create mode 100644 docs/adr/0127-macos-update-relaunch.md diff --git a/AGENTS.md b/AGENTS.md index fe4e9c1..cc5b546 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -772,3 +772,10 @@ SHA-256")`); launchd `KeepAlive` then restarts it in a crash loop and the `BiFlow_*_aarch64.dmg`. Sign that disk image in the release job and publish it as the `macos-aarch64` entry. The platform key must stay identical to `signed_target` for `InstallKind::Dmg` on `aarch64` (ADR 0126). +- macOS in-app update used to copy the new app into `/Applications` and then + tell the operator to quit and open BiFlow again. Closing the window only + hides the process in the menu bar, and a later launch is delivered to that + same process, so the version never changes. A running bundle also cannot be + replaced reliably. Stage the disk image with `ditto`, wait until this + process has exited, swap `/Applications/BiFlow.app`, and `open` that bundle + as the desktop user (ADR 0127). diff --git a/Cargo.lock b/Cargo.lock index 2c6a250..5f9dd6b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2046,7 +2046,7 @@ dependencies = [ [[package]] name = "iran-split-cli" -version = "6.2.75" +version = "6.2.76" dependencies = [ "async-trait", "chrono", @@ -2064,7 +2064,7 @@ dependencies = [ [[package]] name = "iran-split-clients" -version = "6.2.75" +version = "6.2.76" dependencies = [ "async-trait", "ipnet", @@ -2080,7 +2080,7 @@ dependencies = [ [[package]] name = "iran-split-config" -version = "6.2.75" +version = "6.2.76" dependencies = [ "hex", "rand 0.9.5", @@ -2093,7 +2093,7 @@ dependencies = [ [[package]] name = "iran-split-core" -version = "6.2.75" +version = "6.2.76" dependencies = [ "async-trait", "chrono", @@ -2110,7 +2110,7 @@ dependencies = [ [[package]] name = "iran-split-desktop" -version = "6.2.75" +version = "6.2.76" dependencies = [ "chrono", "dirs 6.0.0", @@ -2152,7 +2152,7 @@ dependencies = [ [[package]] name = "iran-split-helper" -version = "6.2.75" +version = "6.2.76" dependencies = [ "clap", "hex", @@ -2177,7 +2177,7 @@ dependencies = [ [[package]] name = "iran-split-helper-winacl" -version = "6.2.75" +version = "6.2.76" dependencies = [ "tokio", "windows", @@ -2185,7 +2185,7 @@ dependencies = [ [[package]] name = "iran-split-ipc" -version = "6.2.75" +version = "6.2.76" dependencies = [ "bytes", "chrono", @@ -2198,7 +2198,7 @@ dependencies = [ [[package]] name = "iran-split-mihomo" -version = "6.2.75" +version = "6.2.76" dependencies = [ "chrono", "futures-util", @@ -2221,7 +2221,7 @@ dependencies = [ [[package]] name = "iran-split-netstats" -version = "6.2.75" +version = "6.2.76" dependencies = [ "libc", "tempfile", @@ -2230,7 +2230,7 @@ dependencies = [ [[package]] name = "iran-split-platform-linux" -version = "6.2.75" +version = "6.2.76" dependencies = [ "async-trait", "chrono", @@ -2254,7 +2254,7 @@ dependencies = [ [[package]] name = "iran-split-platform-macos" -version = "6.2.75" +version = "6.2.76" dependencies = [ "async-trait", "chrono", @@ -2278,7 +2278,7 @@ dependencies = [ [[package]] name = "iran-split-platform-win" -version = "6.2.75" +version = "6.2.76" dependencies = [ "async-trait", "chrono", @@ -2301,7 +2301,7 @@ dependencies = [ [[package]] name = "iran-split-rules" -version = "6.2.75" +version = "6.2.76" dependencies = [ "async-trait", "chrono", @@ -2323,7 +2323,7 @@ dependencies = [ [[package]] name = "iran-split-taskbar" -version = "6.2.75" +version = "6.2.76" dependencies = [ "iran-split-config", "objc2", diff --git a/Cargo.toml b/Cargo.toml index de75fab..abf12ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ members = [ ] [workspace.package] -version = "6.2.75" +version = "6.2.76" edition = "2021" license = "MIT OR Apache-2.0" rust-version = "1.88" diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 0b70f3b..dee25b9 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@iran-split/desktop", - "version": "6.2.75", + "version": "6.2.76", "private": true, "type": "module", "scripts": { diff --git a/docs/adr/0127-macos-update-relaunch.md b/docs/adr/0127-macos-update-relaunch.md new file mode 100644 index 0000000..663e33d --- /dev/null +++ b/docs/adr/0127-macos-update-relaunch.md @@ -0,0 +1,22 @@ +# ADR 0127: Relaunch the replaced macOS app after the old process exits + +Status: Accepted (6.2.76) + +A finished macOS update told the operator to quit and open BiFlow again. The +download had completed and `/Applications/BiFlow.app` may already have been +replaced, but the version on screen did not change. + +Closing the window does not quit BiFlow. The close handler hides the window +and leaves the process in the menu bar. Opening the app again is delivered to +that same process by the single-instance plugin, so the operator keeps running +the binary that started the update. Replacing `/Applications/BiFlow.app` while +that process is still mapped is also unreliable, and a copy launched from the +disk image is not the bundle the operator reopens. + +The disk image is staged with `ditto` while BiFlow is still open. The +administrator prompt starts a detached helper and returns immediately. BiFlow +then exits the same way AppImage and NSIS updates do. The helper waits until +that process id is gone, checks the staged bundle's +`CFBundleShortVersionString`, swaps `/Applications/BiFlow.app`, and opens that +bundle as the desktop user. It does not launch the updated app as root. A +version mismatch leaves the installed app in place and opens it again. diff --git a/docs/adr/README.md b/docs/adr/README.md index 2583490..ee0d68d 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -130,3 +130,4 @@ Keep this index current. Add a new ADR for each non-obvious change, or update th | [0124](./0124-pr-validation-and-safe-archives.md) | Cross-platform PR validation and safe archive dependencies | Accepted | | [0125](./0125-native-release-bootstrap.md) | Native release bootstrap on macOS and Windows | Accepted | | [0126](./0126-macos-updater-platform.md) | Publish the Apple Silicon disk image as an updater platform | Accepted | +| [0127](./0127-macos-update-relaunch.md) | Relaunch the replaced macOS app after the old process exits | Accepted | diff --git a/package.json b/package.json index ef0648f..925d73e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "iran-split-desktop-workspace", - "version": "6.2.75", + "version": "6.2.76", "private": true, "packageManager": "pnpm@9.0.1", "engines": { diff --git a/src-tauri/src/github_update.rs b/src-tauri/src/github_update.rs index 871d317..d5c67b4 100644 --- a/src-tauri/src/github_update.rs +++ b/src-tauri/src/github_update.rs @@ -353,8 +353,8 @@ pub async fn apply_package( InstallKind::Dmg => { #[cfg(target_os = "macos")] { - install_dmg(package).await?; - Ok(ApplyOutcome::ManualRestart) + install_dmg(package, expected_version).await?; + Ok(ApplyOutcome::HelperRestart) } #[cfg(not(target_os = "macos"))] { @@ -435,11 +435,14 @@ async fn install_appimage(package: &Path) -> Result<(), String> { Ok(()) } -/// Installs a downloaded `.dmg` on macOS. Mounts the image, copies the bundled -/// `.app` into `/Applications` with administrator privileges, then unmounts. -/// The operator must restart `BiFlow` from `/Applications` afterwards. +/// Installs a downloaded `.dmg` on macOS. +/// +/// The bundle is staged with `ditto`. A detached privileged helper then waits +/// until this process has exited, replaces `/Applications/BiFlow.app`, and +/// opens that bundle as the desktop user. Closing the window only hides +/// `BiFlow` in the menu bar, so a manual relaunch keeps the old process. #[cfg(target_os = "macos")] -async fn install_dmg(package: &Path) -> Result<(), String> { +async fn install_dmg(package: &Path, expected_version: &str) -> Result<(), String> { info!( event = "update.dmg_install_started", section = "updater", @@ -462,10 +465,27 @@ async fn install_dmg(package: &Path) -> Result<(), String> { if !attach.status.success() { return Err("could not mount the update image".into()); } - let result = install_mounted_dmg(&mountpoint).await; + let staged = stage_mounted_app(&mountpoint).await; + detach_update_image(&mountpoint).await; + let staged = staged?; + let result = spawn_dmg_relaunch(&staged, expected_version).await; + info!( + event = "update.dmg_install_finished", + section = "updater", + initiator = "tauri_command", + cause = "installer_returned", + success = result.is_ok(), + trace_route = "ui->apply_app_update->install_dmg", + "macOS update installer finished" + ); + result +} + +#[cfg(target_os = "macos")] +async fn detach_update_image(mountpoint: &Path) { let detach = tokio::process::Command::new("hdiutil") .args(["detach", "-force"]) - .arg(&mountpoint) + .arg(mountpoint) .status() .await; if !detach.is_ok_and(|status| status.success()) { @@ -477,7 +497,9 @@ async fn install_dmg(package: &Path) -> Result<(), String> { trace_route = "ui->apply_app_update->install_dmg->cleanup", "could not detach the update image" ); - } else if tokio::fs::remove_dir(&mountpoint).await.is_err() { + return; + } + if tokio::fs::remove_dir(mountpoint).await.is_err() { warn!( event = "update.dmg_mount_cleanup_failed", section = "updater", @@ -487,69 +509,190 @@ async fn install_dmg(package: &Path) -> Result<(), String> { "could not remove the empty update mount directory" ); } - info!( - event = "update.dmg_install_finished", - section = "updater", - initiator = "tauri_command", - cause = "installer_returned", - success = result.is_ok(), - trace_route = "ui->apply_app_update->install_dmg", - "macOS update installer finished" - ); - result } #[cfg(target_os = "macos")] -async fn install_mounted_dmg(mountpoint: &Path) -> Result<(), String> { - if !mountpoint.join("BiFlow.app").is_dir() { +async fn stage_mounted_app(mountpoint: &Path) -> Result { + let source = mountpoint.join("BiFlow.app"); + if !source.is_dir() { return Err("the update image has no BiFlow.app bundle".into()); } - let script_path = std::env::temp_dir() + let staging_root = std::env::temp_dir() .join("biflow-update") - .join("apply-dmg.sh"); - tokio::fs::write(&script_path, dmg_install_script(mountpoint)) + .join("staged-app"); + if staging_root.exists() { + tokio::fs::remove_dir_all(&staging_root) + .await + .map_err(|error| error.to_string())?; + } + tokio::fs::create_dir_all(&staging_root) .await .map_err(|error| error.to_string())?; - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let mut permissions = tokio::fs::metadata(&script_path) - .await - .map_err(|error| error.to_string())? - .permissions(); - permissions.set_mode(0o700); - tokio::fs::set_permissions(&script_path, permissions) + let destination = staging_root.join("BiFlow.app"); + let status = tokio::process::Command::new("/usr/bin/ditto") + .arg(&source) + .arg(&destination) + .status() + .await + .map_err(|error| error.to_string())?; + if status.success() { + Ok(destination) + } else { + Err("could not stage the update bundle".into()) + } +} + +#[cfg(target_os = "macos")] +async fn spawn_dmg_relaunch(staged_app: &Path, expected_version: &str) -> Result<(), String> { + let uid = macos_desktop_uid().await?; + let script_path = std::env::temp_dir() + .join("biflow-update") + .join("apply-dmg.sh"); + if let Some(parent) = script_path.parent() { + tokio::fs::create_dir_all(parent) .await .map_err(|error| error.to_string())?; } - let apple_script = crate::helper_install::macos_admin_applescript(&script_path); - let install = tokio::process::Command::new("osascript") + tokio::fs::write( + &script_path, + dmg_relaunch_script(std::process::id(), uid, staged_app, expected_version), + ) + .await + .map_err(|error| error.to_string())?; + set_owner_execute(&script_path).await?; + let apple_script = dmg_detached_admin_applescript(&script_path); + let install = tokio::process::Command::new("/usr/bin/osascript") .args(["-e", &apple_script]) .output() .await .map_err(|error| error.to_string())?; if install.status.success() { - Ok(()) - } else if install.status.code() == Some(-128) { + info!( + event = "update.dmg_relaunch_helper_started", + section = "updater", + initiator = "tauri_command", + cause = "verified_package", + trace_route = "ui->apply_app_update->install_dmg->relaunch", + "macOS update will replace /Applications/BiFlow.app after this process exits" + ); + return Ok(()); + } + if let Some(parent) = staged_app.parent() { + if let Err(cause) = tokio::fs::remove_dir_all(parent).await { + warn!( + event = "update.dmg_stage_cleanup_failed", + section = "updater", + initiator = "tauri_command", + cause = %cause, + trace_route = "ui->apply_app_update->install_dmg->cleanup", + "staged macOS update could not be removed after the installer was declined" + ); + } + } + if install.status.code() == Some(-128) { Err("package install was cancelled".into()) } else { Err("package install failed".into()) } } +#[cfg(target_os = "macos")] +async fn macos_desktop_uid() -> Result { + let output = tokio::process::Command::new("/usr/bin/id") + .arg("-u") + .output() + .await + .map_err(|error| error.to_string())?; + if !output.status.success() { + return Err("could not read the desktop user id".into()); + } + String::from_utf8_lossy(&output.stdout) + .trim() + .parse::() + .map_err(|_| "could not read the desktop user id".to_owned()) +} + +#[cfg(target_os = "macos")] +async fn set_owner_execute(path: &Path) -> Result<(), String> { + use std::os::unix::fs::PermissionsExt; + let mut permissions = tokio::fs::metadata(path) + .await + .map_err(|error| error.to_string())? + .permissions(); + permissions.set_mode(0o700); + tokio::fs::set_permissions(path, permissions) + .await + .map_err(|error| error.to_string()) +} + +/// Shell script that runs as root after `BiFlow` exits. +/// +/// It must not remove `/Applications/BiFlow.app` until `process_id` is gone, +/// and it must open the replaced bundle as `uid` rather than as root. #[cfg(any(target_os = "macos", test))] -fn dmg_install_script(mountpoint: &Path) -> String { +fn dmg_relaunch_script( + process_id: u32, + uid: u32, + staged_app: &Path, + expected_version: &str, +) -> String { format!( - "set -e\n\ - staging=$(mktemp -d '/Applications/.biflow-update.XXXXXX')\n\ - trap 'rm -rf \"$staging\"' EXIT\n\ - cp -R {source} \"$staging/BiFlow.app\"\n\ - rm -rf '/Applications/BiFlow.app'\n\ - mv \"$staging/BiFlow.app\" '/Applications/BiFlow.app'\n", - source = sh_single_quote(&mountpoint.join("BiFlow.app").to_string_lossy()), + "#!/bin/sh\n\ + set -e\n\ + pid={process_id}\n\ + uid={uid}\n\ + expected={expected}\n\ + staged={staged}\n\ + dest='/Applications/BiFlow.app'\n\ + staging=''\n\ + cleanup() {{\n\ + status=$?\n\ + if [ ! -d \"$dest\" ] && [ -n \"$staging\" ] && [ -d \"$staging/BiFlow.app\" ]; then\n\ + mv \"$staging/BiFlow.app\" \"$dest\" || true\n\ + fi\n\ + if [ -n \"$staging\" ] && [ -d \"$dest\" ]; then\n\ + rm -rf \"$staging\"\n\ + fi\n\ + if [ \"$status\" -ne 0 ] && [ -d \"$dest\" ]; then\n\ + /bin/launchctl asuser \"$uid\" /usr/bin/open \"$dest\" || true\n\ + fi\n\ + exit \"$status\"\n\ + }}\n\ + trap cleanup EXIT\n\ + while /bin/kill -0 \"$pid\" 2>/dev/null; do\n\ + /bin/sleep 1\n\ + done\n\ + /bin/sleep 1\n\ + staging=$(/usr/bin/mktemp -d '/Applications/.biflow-update.XXXXXX')\n\ + /usr/bin/ditto \"$staged\" \"$staging/BiFlow.app\"\n\ + found=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' \"$staging/BiFlow.app/Contents/Info.plist\" | /usr/bin/tr -d '[:space:]')\n\ + if [ \"$found\" != \"$expected\" ]; then\n\ + echo 'staged update version does not match the downloaded release' >&2\n\ + exit 1\n\ + fi\n\ + rm -rf \"$dest\"\n\ + mv \"$staging/BiFlow.app\" \"$dest\"\n\ + /usr/bin/xattr -dr com.apple.quarantine \"$dest\" || true\n\ + rm -rf \"$staged\"\n\ + /bin/launchctl asuser \"$uid\" /usr/bin/open \"$dest\"\n", + expected = sh_single_quote(expected_version), + staged = sh_single_quote(&staged_app.to_string_lossy()), ) } +/// Starts the relaunch script in its own session so the administrator prompt +/// returns before `BiFlow` exits. A foreground script that waited on this +/// process would deadlock. +#[cfg(any(target_os = "macos", test))] +fn dmg_detached_admin_applescript(script_path: &Path) -> String { + let command = format!( + "((/bin/sh {} >/tmp/biflow-dmg-update.log 2>&1 Date: Fri, 23 Oct 2026 19:35:18 +0330 Subject: [PATCH 2/4] Download the macOS update without replacing the running app. Automatic install left the menu-bar process on the old version, and resizing the status item every second could freeze the app. Open the signed disk image for a drag install and leave the tunnel running. Co-authored-by: Cursor --- AGENTS.md | 26 +- Cargo.lock | 30 +- Cargo.toml | 2 +- apps/desktop/package.json | 2 +- apps/desktop/src/api/models.ts | 1 + apps/desktop/src/components/About.test.tsx | 37 ++ apps/desktop/src/components/About.tsx | 7 +- apps/desktop/src/i18n/config.ts | 6 + build.sh | 28 ++ crates/iran-split-taskbar/src/lib.rs | 85 +++++ crates/iran-split-taskbar/src/macos.rs | 121 +++++-- docs/adr/0122-macos-menu-bar-speed.md | 13 + docs/adr/0127-macos-update-relaunch.md | 2 +- docs/adr/0128-macos-drag-install.md | 21 ++ docs/adr/0129-macos-update-downloads-only.md | 17 + docs/adr/README.md | 4 +- e2e/primary-flows.spec.ts | 4 +- package.json | 2 +- packaging/macos/dmg-background.png | Bin 0 -> 1508 bytes scripts/build-plan.test.mjs | 32 ++ scripts/tauri-contract.test.mjs | 15 +- src-tauri/src/github_update.rs | 340 ++----------------- src-tauri/src/lib.rs | 211 ++++++++---- src-tauri/src/tray.rs | 33 ++ src-tauri/tauri.conf.json | 2 +- src-tauri/tauri.macos.conf.json | 17 +- version | 2 +- 27 files changed, 644 insertions(+), 416 deletions(-) create mode 100644 docs/adr/0128-macos-drag-install.md create mode 100644 docs/adr/0129-macos-update-downloads-only.md create mode 100644 packaging/macos/dmg-background.png diff --git a/AGENTS.md b/AGENTS.md index cc5b546..ab8c9a6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -752,6 +752,16 @@ SHA-256")`); launchd `KeepAlive` then restarts it in a crash loop and the default gateway, and delete `netstat` routes that still name the TUN. Kill a scoped Mihomo orphan on stop when the child handle is already empty (ADR 0121). +- Calling `sizeToFit` and `setFrame` on the macOS status button every second + enters `NSStatusItemScene updateSettings`, which `performAsyncAndWait`s. + On macOS 26 that wait can sit on the main thread until force quit (73 + minutes on 23 October 2026) while `MenuItem::with_id` from a worker blocks + forever on the same thread. Change only the attributed title when the + column width is unchanged, and rebuild the tray menu on the main thread + only when phase or busy changes (ADR 0122). +- A `--skip-jenkins` DMG retry that drops `--app-drop-link` ships a disk + image with no Applications alias. Keep the same positions and background + as `tauri.macos.conf.json` on that retry (ADR 0128). - macOS `NET_RT_IFLIST2` `if_data64.ifi_ibytes` does not match `netstat -ib` on current Darwin. Menu bar speeds must read `IFMIB_IFDATA` / `IFDATA_GENERAL`. Wi-Fi reports `IFT_ETHER`, so classify with @@ -772,10 +782,12 @@ SHA-256")`); launchd `KeepAlive` then restarts it in a crash loop and the `BiFlow_*_aarch64.dmg`. Sign that disk image in the release job and publish it as the `macos-aarch64` entry. The platform key must stay identical to `signed_target` for `InstallKind::Dmg` on `aarch64` (ADR 0126). -- macOS in-app update used to copy the new app into `/Applications` and then - tell the operator to quit and open BiFlow again. Closing the window only - hides the process in the menu bar, and a later launch is delivered to that - same process, so the version never changes. A running bundle also cannot be - replaced reliably. Stage the disk image with `ditto`, wait until this - process has exited, swap `/Applications/BiFlow.app`, and `open` that bundle - as the desktop user (ADR 0127). +- macOS in-app update must not replace `/Applications/BiFlow.app` or quit. + Closing the window only hides the menu-bar process, so an automatic swap + and relaunch was easy to miss and left the operator on the old version + when the helper never ran. Download the signed disk image, open it with + `/usr/bin/open`, and leave the tunnel up. The new version starts only + after the operator drags BiFlow onto Applications, quits the menu-bar + process, and opens that Applications copy (ADR 0129). Linux `.deb` still + waits for a manual restart. AppImage and NSIS still use their replace + helpers. diff --git a/Cargo.lock b/Cargo.lock index 5f9dd6b..c0e9bc4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2046,7 +2046,7 @@ dependencies = [ [[package]] name = "iran-split-cli" -version = "6.2.76" +version = "6.2.78" dependencies = [ "async-trait", "chrono", @@ -2064,7 +2064,7 @@ dependencies = [ [[package]] name = "iran-split-clients" -version = "6.2.76" +version = "6.2.78" dependencies = [ "async-trait", "ipnet", @@ -2080,7 +2080,7 @@ dependencies = [ [[package]] name = "iran-split-config" -version = "6.2.76" +version = "6.2.78" dependencies = [ "hex", "rand 0.9.5", @@ -2093,7 +2093,7 @@ dependencies = [ [[package]] name = "iran-split-core" -version = "6.2.76" +version = "6.2.78" dependencies = [ "async-trait", "chrono", @@ -2110,7 +2110,7 @@ dependencies = [ [[package]] name = "iran-split-desktop" -version = "6.2.76" +version = "6.2.78" dependencies = [ "chrono", "dirs 6.0.0", @@ -2152,7 +2152,7 @@ dependencies = [ [[package]] name = "iran-split-helper" -version = "6.2.76" +version = "6.2.78" dependencies = [ "clap", "hex", @@ -2177,7 +2177,7 @@ dependencies = [ [[package]] name = "iran-split-helper-winacl" -version = "6.2.76" +version = "6.2.78" dependencies = [ "tokio", "windows", @@ -2185,7 +2185,7 @@ dependencies = [ [[package]] name = "iran-split-ipc" -version = "6.2.76" +version = "6.2.78" dependencies = [ "bytes", "chrono", @@ -2198,7 +2198,7 @@ dependencies = [ [[package]] name = "iran-split-mihomo" -version = "6.2.76" +version = "6.2.78" dependencies = [ "chrono", "futures-util", @@ -2221,7 +2221,7 @@ dependencies = [ [[package]] name = "iran-split-netstats" -version = "6.2.76" +version = "6.2.78" dependencies = [ "libc", "tempfile", @@ -2230,7 +2230,7 @@ dependencies = [ [[package]] name = "iran-split-platform-linux" -version = "6.2.76" +version = "6.2.78" dependencies = [ "async-trait", "chrono", @@ -2254,7 +2254,7 @@ dependencies = [ [[package]] name = "iran-split-platform-macos" -version = "6.2.76" +version = "6.2.78" dependencies = [ "async-trait", "chrono", @@ -2278,7 +2278,7 @@ dependencies = [ [[package]] name = "iran-split-platform-win" -version = "6.2.76" +version = "6.2.78" dependencies = [ "async-trait", "chrono", @@ -2301,7 +2301,7 @@ dependencies = [ [[package]] name = "iran-split-rules" -version = "6.2.76" +version = "6.2.78" dependencies = [ "async-trait", "chrono", @@ -2323,7 +2323,7 @@ dependencies = [ [[package]] name = "iran-split-taskbar" -version = "6.2.76" +version = "6.2.78" dependencies = [ "iran-split-config", "objc2", diff --git a/Cargo.toml b/Cargo.toml index abf12ea..453b525 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ members = [ ] [workspace.package] -version = "6.2.76" +version = "6.2.78" edition = "2021" license = "MIT OR Apache-2.0" rust-version = "1.88" diff --git a/apps/desktop/package.json b/apps/desktop/package.json index dee25b9..a1d9cc6 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@iran-split/desktop", - "version": "6.2.76", + "version": "6.2.78", "private": true, "type": "module", "scripts": { diff --git a/apps/desktop/src/api/models.ts b/apps/desktop/src/api/models.ts index d380b8b..632ffaa 100644 --- a/apps/desktop/src/api/models.ts +++ b/apps/desktop/src/api/models.ts @@ -387,6 +387,7 @@ export type UpdatePhase = | "installing" | "restarting" | "installed" + | "downloaded" | "failed"; export interface UpdateProgress { diff --git a/apps/desktop/src/components/About.test.tsx b/apps/desktop/src/components/About.test.tsx index 6929fb1..487489d 100644 --- a/apps/desktop/src/components/About.test.tsx +++ b/apps/desktop/src/components/About.test.tsx @@ -3,6 +3,7 @@ import userEvent from "@testing-library/user-event"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { About } from "./About"; import { useAppStore } from "../store/app"; +import type { BootstrapResult } from "../api/models"; import i18n from "../i18n/config"; vi.mock("../version", () => ({ @@ -17,6 +18,7 @@ describe("About", () => { beforeEach(() => { useAppStore.setState({ + boot: null, update: { phase: "idle", percent: null, @@ -79,6 +81,41 @@ describe("About", () => { ); }); + it("tells the operator to drag a downloaded macOS disk image", () => { + useAppStore.setState({ + update: { + phase: "downloaded", + percent: 100, + version: "1.3.0", + error: null, + }, + }); + render(); + expect(screen.getByRole("status")).toHaveTextContent( + /drag BiFlow onto Applications/i, + ); + expect(screen.getByRole("status")).toHaveTextContent(/menu bar/i); + }); + + it("offers a download on macOS and an install elsewhere", async () => { + const installUpdate = vi.fn(async () => undefined); + useAppStore.setState({ + boot: { platform: "macos" } as BootstrapResult, + update: { + phase: "available", + percent: null, + version: "1.3.0", + error: null, + }, + installUpdate, + }); + render(); + await userEvent.click( + screen.getByRole("button", { name: /download 1\.3\.0/i }), + ); + expect(installUpdate).toHaveBeenCalledOnce(); + }); + it("offers install when an update is available", async () => { const installUpdate = vi.fn(async () => undefined); useAppStore.setState({ diff --git a/apps/desktop/src/components/About.tsx b/apps/desktop/src/components/About.tsx index 43474d3..1abd81d 100644 --- a/apps/desktop/src/components/About.tsx +++ b/apps/desktop/src/components/About.tsx @@ -9,6 +9,7 @@ const REPOSITORY_URL = "https://github.com/devlifeX/BiFlow"; export function About() { const { t } = useTranslation(); const update = useAppStore((state) => state.update); + const platform = useAppStore((state) => state.boot?.platform); const checkForUpdate = useAppStore((state) => state.checkForUpdate); const installUpdate = useAppStore((state) => state.installUpdate); const retryUpdate = useAppStore((state) => state.retryUpdate); @@ -120,7 +121,9 @@ export function About() { disabled={busy} onClick={() => void installUpdate()} > - {t("updateInstall", { version: update.version ?? "" })} + {platform === "macos" + ? t("updateDownload", { version: update.version ?? "" }) + : t("updateInstall", { version: update.version ?? "" })} ) : null} {showRetry ? ( @@ -171,6 +174,8 @@ function updateMessage( return t("updateRestarting"); case "installed": return t("updateInstalled"); + case "downloaded": + return t("updateDownloaded", { version: update.version ?? "" }); case "failed": return t("updateFailed"); default: diff --git a/apps/desktop/src/i18n/config.ts b/apps/desktop/src/i18n/config.ts index 6e89fe5..0d2b3ce 100644 --- a/apps/desktop/src/i18n/config.ts +++ b/apps/desktop/src/i18n/config.ts @@ -265,12 +265,15 @@ const resources = { updateAvailable: "Version {{version}} is available.", updateFailed: "Could not check for updates.", updateInstall: "Install update {{version}}", + updateDownload: "Download {{version}}", updateRetry: "Retry update", updateDownloading: "Downloading version {{version}}…", updateInstalling: "Installing update…", updateRestarting: "Restarting BiFlow…", updateInstalled: "BiFlow was updated. Quit and open the app again to use the new version.", + updateDownloaded: + "Version {{version}} is downloaded. The disk image is open. Drag BiFlow onto Applications, quit this BiFlow from the menu bar, then open the copy in Applications.", updateDownloadProgress: "Download progress", updateIdle: "Check GitHub Releases when you are ready to update.", cloudRules: "Iran cloud rules", @@ -685,12 +688,15 @@ const resources = { updateAvailable: "نسخه {{version}} در دسترس است.", updateFailed: "بررسی به‌روزرسانی انجام نشد.", updateInstall: "نصب به‌روزرسانی {{version}}", + updateDownload: "دانلود {{version}}", updateRetry: "تلاش دوباره", updateDownloading: "در حال دانلود نسخه {{version}}…", updateInstalling: "در حال نصب به‌روزرسانی…", updateRestarting: "در حال راه‌اندازی مجدد BiFlow…", updateInstalled: "BiFlow به‌روزرسانی شد. برنامه را ببندید و دوباره باز کنید تا نسخه جدید دیده شود.", + updateDownloaded: + "نسخه {{version}} دانلود شد. تصویر دیسک باز است. BiFlow را روی Applications بکشید، این برنامه را از نوار منو ببندید، سپس نسخه داخل Applications را باز کنید.", updateDownloadProgress: "پیشرفت دانلود", updateIdle: "وقتی آماده بودید، به‌روزرسانی GitHub Releases را بررسی کنید.", diff --git a/build.sh b/build.sh index 4c32458..fe2522e 100755 --- a/build.sh +++ b/build.sh @@ -64,6 +64,26 @@ macos_app_name() { command printf 'BiFlow.app\n' } +# Positions match `src-tauri/tauri.macos.conf.json`. `--skip-jenkins` skips +# Finder layout, but `--app-drop-link` still creates the Applications alias. +macos_dmg_drag_args() { + local app + app="$(macos_app_name)" + command printf '%s\n' \ + --volname BiFlow \ + --icon "${app}" \ + 180 \ + 170 \ + --app-drop-link \ + 480 \ + 170 \ + --window-size \ + 660 \ + 400 \ + --hide-extension "${app}" \ + --background "${PROJECT_DIR}/packaging/macos/dmg-background.png" +} + usage() { command cat <<'EOF' BiFlow release builder @@ -879,7 +899,15 @@ build_macos() { "${app_dir}"/rw.*.dmg \ "${dmg_dir}"/rw.*.dmg \ "${dmg_dir}/$(macos_dmg_name)" + [[ -f "${PROJECT_DIR}/packaging/macos/dmg-background.png" ]] || \ + die "dmg background is missing" + local drag_args=() + local drag_arg + while IFS= read -r drag_arg; do + drag_args+=("${drag_arg}") + done < <(macos_dmg_drag_args) ( cd -- "${dmg_dir}" && bash "${dmg_script}" --skip-jenkins \ + ${drag_args[@]+"${drag_args[@]}"} \ "$(macos_dmg_name)" "${app_dir}" ) \ || die "bundle_dmg.sh --skip-jenkins failed" fi diff --git a/crates/iran-split-taskbar/src/lib.rs b/crates/iran-split-taskbar/src/lib.rs index 2189cee..2f23369 100644 --- a/crates/iran-split-taskbar/src/lib.rs +++ b/crates/iran-split-taskbar/src/lib.rs @@ -96,6 +96,56 @@ pub fn format_rate_in(rate: Option, unit: SpeedUnit) -> String { /// is the longest automatic rate, so normal updates do not resize the item. const MENU_BAR_COLUMNS: usize = 11; +/// What the menu bar already shows, if this process has drawn it. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum MenuBarRecord<'a> { + /// No title has been applied in this process. + Unknown, + /// The status icon is visible and the speed title is clear. + Hidden, + /// The stacked speed title currently on the status item. + Shown(&'a str), +} + +/// How the next menu-bar sample may touch `AppKit`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum MenuBarUpdate { + /// The visible title already matches. Do not call `AppKit`. + Unchanged, + /// Replace the attributed title. The item width stays the same. + Title, + /// Show or hide the speeds, or change the line width. + Layout, +} + +/// Digit changes inside the fixed column width must not resize the status item. +/// +/// Resizing enters the status-item scene update, which can block the main +/// thread until the process is force-quit. +#[must_use] +pub fn menu_bar_update(previous: MenuBarRecord<'_>, next: Option<&str>) -> MenuBarUpdate { + match (previous, next) { + (MenuBarRecord::Unknown | MenuBarRecord::Hidden, None) => MenuBarUpdate::Unchanged, + (MenuBarRecord::Shown(previous), Some(next)) if previous == next => { + MenuBarUpdate::Unchanged + } + (MenuBarRecord::Shown(previous), Some(next)) + if menu_bar_line_width(previous) == menu_bar_line_width(next) => + { + MenuBarUpdate::Title + } + _ => MenuBarUpdate::Layout, + } +} + +fn menu_bar_line_width(label: &str) -> usize { + label + .lines() + .map(|line| line.chars().count()) + .max() + .unwrap_or(0) +} + /// Upload above download, each line the same width, arrows aligned. #[must_use] pub fn menu_bar_label(upload: Option, download: Option, unit: SpeedUnit) -> String { @@ -194,6 +244,41 @@ mod tests { assert!(wide[0].chars().count() > MENU_BAR_COLUMNS); } + #[test] + fn digit_changes_do_not_resize_the_menu_bar_item() { + let idle = menu_bar_label(None, Some(0.0), SpeedUnit::AutoBits); + let live = menu_bar_label(Some(14_000.0), Some(450_000.0), SpeedUnit::AutoBits); + assert_eq!( + menu_bar_update(MenuBarRecord::Shown(&idle), Some(&live)), + MenuBarUpdate::Title + ); + assert_eq!( + menu_bar_update(MenuBarRecord::Shown(&live), Some(&live)), + MenuBarUpdate::Unchanged + ); + assert_eq!( + menu_bar_update(MenuBarRecord::Unknown, Some(&live)), + MenuBarUpdate::Layout + ); + assert_eq!( + menu_bar_update(MenuBarRecord::Shown(&live), None), + MenuBarUpdate::Layout + ); + assert_eq!( + menu_bar_update(MenuBarRecord::Hidden, None), + MenuBarUpdate::Unchanged + ); + assert_eq!( + menu_bar_update(MenuBarRecord::Unknown, None), + MenuBarUpdate::Unchanged + ); + let wide = menu_bar_label(Some(2_000_000_000.0), Some(1.0), SpeedUnit::Bits); + assert_eq!( + menu_bar_update(MenuBarRecord::Shown(&live), Some(&wide)), + MenuBarUpdate::Layout + ); + } + #[test] fn upload_is_above_download_with_distinct_values() { assert_eq!( diff --git a/crates/iran-split-taskbar/src/macos.rs b/crates/iran-split-taskbar/src/macos.rs index a6fbb7b..93f37d2 100644 --- a/crates/iran-split-taskbar/src/macos.rs +++ b/crates/iran-split-taskbar/src/macos.rs @@ -1,5 +1,8 @@ //! Two-line menu bar title. A plain status-item title stays one proportional //! line, so the item changes width every time the rate changes. +use std::cell::Cell; +use std::sync::Mutex; + use objc2::runtime::AnyObject; use objc2::{AnyThread, MainThreadMarker, Message}; use objc2_app_kit::{ @@ -8,6 +11,19 @@ use objc2_app_kit::{ }; use objc2_foundation::{NSAttributedString, NSDictionary, NSRect, NSString}; +use crate::{MenuBarRecord, MenuBarUpdate}; + +#[derive(Clone)] +enum SavedLabel { + Hidden, + Shown(String), +} + +static SAVED_LABEL: Mutex> = Mutex::new(None); +thread_local! { + static APPLYING: Cell = const { Cell::new(false) }; +} + /// True when `AppKit` calls are legal on this thread. #[must_use] pub fn menu_bar_is_main_thread() -> bool { @@ -16,44 +32,109 @@ pub fn menu_bar_is_main_thread() -> bool { /// Draws `label` on `BiFlow`'s status item. `None` clears the title. /// -/// Returns whether that status item was found. +/// Returns whether that status item was found, or whether nothing needed to change. #[must_use] pub fn apply_menu_bar_label(label: Option<&str>) -> bool { let Some(marker) = MainThreadMarker::new() else { return false; }; + if APPLYING.with(Cell::get) { + tracing::warn!( + event = "taskbar.menu_bar_reentered", + section = "taskbar", + initiator = "interface_sampler", + cause = "appkit_reentry", + trace_route = "shared_stats->tray_label->menu_bar", + "menu bar update skipped because AppKit reentered it" + ); + return true; + } + let previous = saved_record(); + let record = match previous.as_ref() { + None => MenuBarRecord::Unknown, + Some(SavedLabel::Hidden) => MenuBarRecord::Hidden, + Some(SavedLabel::Shown(text)) => MenuBarRecord::Shown(text), + }; + let update = crate::menu_bar_update(record, label); + if update == MenuBarUpdate::Unchanged { + return true; + } let Some(button) = status_button(marker) else { return false; }; - let tip = NSString::from_str("BiFlow"); - button.setToolTip(Some(&tip)); - if let Some(label) = label { - let font = NSFont::monospacedSystemFontOfSize_weight(9.0, 0.0); - let style = NSMutableParagraphStyle::new(); - style.setMaximumLineHeight(9.0); - style.setLineSpacing(-2.0); - let text = NSString::from_str(label); - let attrs = attributes(&font, &style, &NSColor::labelColor()); - // SAFETY: the dictionary uses the font, paragraph, and foreground - // attribute names, and each value is the matching AppKit object. - let title = unsafe { - NSAttributedString::initWithString_attributes( - NSAttributedString::alloc(), - &text, - Some(&attrs), - ) + let _guard = ApplyGuard::enter(); + // A title-only change must not call `sizeToFit`. If the logo is visible + // again, the status item was recreated and needs one layout. + if update == MenuBarUpdate::Title && button.image().is_none() { + let Some(label) = label else { + return false; }; - button.setFont(Some(&font)); - button.setAttributedTitle(&title); + set_stacked_title(&button, label); + remember(label); + return true; + } + button.setToolTip(Some(&NSString::from_str("BiFlow"))); + if let Some(label) = label { + set_stacked_title(&button, label); show_logo(&button, false); } else { button.setTitle(&NSString::from_str("")); show_logo(&button, true); } fit_click_target(&button); + remember_layout(label); true } +fn saved_record() -> Option { + SAVED_LABEL.lock().expect("menu bar label").clone() +} + +fn remember(label: &str) { + *SAVED_LABEL.lock().expect("menu bar label") = Some(SavedLabel::Shown(label.to_owned())); +} + +fn remember_layout(label: Option<&str>) { + *SAVED_LABEL.lock().expect("menu bar label") = Some(match label { + Some(label) => SavedLabel::Shown(label.to_owned()), + None => SavedLabel::Hidden, + }); +} + +struct ApplyGuard; + +impl ApplyGuard { + fn enter() -> Self { + APPLYING.with(|applying| applying.set(true)); + Self + } +} + +impl Drop for ApplyGuard { + fn drop(&mut self) { + APPLYING.with(|applying| applying.set(false)); + } +} + +fn set_stacked_title(button: &NSButton, label: &str) { + let font = NSFont::monospacedSystemFontOfSize_weight(9.0, 0.0); + let style = NSMutableParagraphStyle::new(); + style.setMaximumLineHeight(9.0); + style.setLineSpacing(-2.0); + let text = NSString::from_str(label); + let attrs = attributes(&font, &style, &NSColor::labelColor()); + // SAFETY: the dictionary uses the font, paragraph, and foreground + // attribute names, and each value is the matching AppKit object. + let title = unsafe { + NSAttributedString::initWithString_attributes( + NSAttributedString::alloc(), + &text, + Some(&attrs), + ) + }; + button.setAttributedTitle(&title); +} + fn attributes( font: &NSFont, style: &NSMutableParagraphStyle, diff --git a/docs/adr/0122-macos-menu-bar-speed.md b/docs/adr/0122-macos-menu-bar-speed.md index 7043c56..e80ac7d 100644 --- a/docs/adr/0122-macos-menu-bar-speed.md +++ b/docs/adr/0122-macos-menu-bar-speed.md @@ -42,3 +42,16 @@ BiFlow's status button was found and the stacked title was applied. Coverage includes virtual-name and media classification, default-route selection, a host read without elevation, equal-width menu bar lines, macOS settings unit tests, and the English/Persian speed-save flow. + +Amendment (6.2.77): the one-second sampler used to call `sizeToFit`, move the +click overlay, and clear the status image on every tick, including while the +speeds were hidden. On macOS 26 that enters `NSStatusItemScene updateSettings`, +which waits with `performAsyncAndWait`. A hang on 23 October 2026 left the +main thread there for 73 minutes (`blocked by queue in a transient state`). +A tray-menu rebuild on a worker called `MenuItem::with_id`, which waits for +that same thread, so the menu never returned and the window had to be force +quit. Digit changes that keep the fixed column width now replace only the +attributed title. `sizeToFit`, the overlay frame, and the icon change only +when speeds are shown or hidden, when a line gets wider, or when the logo is +visible again. Identical text does not touch `AppKit`. Tray menu rebuilds run +on the main thread and only when the phase or busy flag changes. diff --git a/docs/adr/0127-macos-update-relaunch.md b/docs/adr/0127-macos-update-relaunch.md index 663e33d..ce4b8b3 100644 --- a/docs/adr/0127-macos-update-relaunch.md +++ b/docs/adr/0127-macos-update-relaunch.md @@ -1,6 +1,6 @@ # ADR 0127: Relaunch the replaced macOS app after the old process exits -Status: Accepted (6.2.76) +Status: Superseded by [ADR 0129](./0129-macos-update-downloads-only.md) (6.2.78) A finished macOS update told the operator to quit and open BiFlow again. The download had completed and `/Applications/BiFlow.app` may already have been diff --git a/docs/adr/0128-macos-drag-install.md b/docs/adr/0128-macos-drag-install.md new file mode 100644 index 0000000..93e005f --- /dev/null +++ b/docs/adr/0128-macos-drag-install.md @@ -0,0 +1,21 @@ +# ADR 0128: macOS disk image is a drag-to-Applications install + +Status: Accepted (6.2.77) + +A macOS install is a disk image the operator opens and drags onto +Applications. Tauri's bundler creates that alias only when it passes +`--app-drop-link`. The positions live in `src-tauri/tauri.macos.conf.json`: +`BiFlow.app` at (180, 170), Applications at (480, 170), and a 660 by 400 +window. `packaging/macos/dmg-background.png` draws the arrow between them. + +The non-GUI retry used to run `bundle_dmg.sh --skip-jenkins` with only the +file name and the app directory. Finder's layout script is what `--skip-jenkins` +skips, but omitting `--app-drop-link` also omitted the Applications alias, so +a failed AppleScript produced a disk image with nothing to drag onto. +`--skip-jenkins` still skips icon positions. The retry now passes the same +volume name, icon position, Applications link, window size, and background as +the normal bundle. The alias is on the image either way. + +The in-app updater opens this same disk image and leaves the running app in +place (ADR 0129). This decision is the disk image a person opens from the +release or from About. diff --git a/docs/adr/0129-macos-update-downloads-only.md b/docs/adr/0129-macos-update-downloads-only.md new file mode 100644 index 0000000..96feaf8 --- /dev/null +++ b/docs/adr/0129-macos-update-downloads-only.md @@ -0,0 +1,17 @@ +# ADR 0129: macOS update downloads a disk image and does not restart + +Status: Accepted (6.2.78) + +Supersedes the install half of [ADR 0127](./0127-macos-update-relaunch.md). + +About → Install on macOS verifies the signed disk image, then opens it with +`/usr/bin/open`. Finder shows the drag-to-Applications window from +[ADR 0128](./0128-macos-drag-install.md). BiFlow does not mount the image +itself, does not replace `/Applications/BiFlow.app`, does not ask for an +administrator password, does not pause the tunnel, and does not quit. + +The running menu-bar process is still the old version. Closing the window +only hides it, and the single-instance plugin delivers a later launch to that +same process. The new version runs after the operator quits BiFlow from the +menu bar and opens the Applications copy. Linux `.deb` still waits for a +manual restart. AppImage and NSIS still hand off to their replace helpers. diff --git a/docs/adr/README.md b/docs/adr/README.md index ee0d68d..f667a23 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -130,4 +130,6 @@ Keep this index current. Add a new ADR for each non-obvious change, or update th | [0124](./0124-pr-validation-and-safe-archives.md) | Cross-platform PR validation and safe archive dependencies | Accepted | | [0125](./0125-native-release-bootstrap.md) | Native release bootstrap on macOS and Windows | Accepted | | [0126](./0126-macos-updater-platform.md) | Publish the Apple Silicon disk image as an updater platform | Accepted | -| [0127](./0127-macos-update-relaunch.md) | Relaunch the replaced macOS app after the old process exits | Accepted | +| [0127](./0127-macos-update-relaunch.md) | Relaunch the replaced macOS app after the old process exits | Superseded | +| [0128](./0128-macos-drag-install.md) | macOS disk image is a drag-to-Applications install | Accepted | +| [0129](./0129-macos-update-downloads-only.md) | macOS update downloads a disk image and does not restart | Accepted | diff --git a/e2e/primary-flows.spec.ts b/e2e/primary-flows.spec.ts index 1435730..83cc027 100644 --- a/e2e/primary-flows.spec.ts +++ b/e2e/primary-flows.spec.ts @@ -836,7 +836,9 @@ test.describe("primary BiFlow flows", () => { ); await page.getByRole("button", { name: "Check for updates" }).click(); await expect(page.getByText(/Version 9\.9\.9 is available/i)).toBeVisible(); - await page.getByRole("button", { name: /Install update 9\.9\.9/i }).click(); + await page + .getByRole("button", { name: /(?:Install update|Download) 9\.9\.9/i }) + .click(); await expect(page.getByRole("progressbar")).toBeVisible(); await expect( page.getByText("an update is already in progress"), diff --git a/package.json b/package.json index 925d73e..f40ae6b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "iran-split-desktop-workspace", - "version": "6.2.76", + "version": "6.2.78", "private": true, "packageManager": "pnpm@9.0.1", "engines": { diff --git a/packaging/macos/dmg-background.png b/packaging/macos/dmg-background.png new file mode 100644 index 0000000000000000000000000000000000000000..04e9ea9565f8c77aaae8d411036d3a3a1be7ab3c GIT binary patch literal 1508 zcmeAS@N?(olHy`uVBq!ia0y~yV4A|fz&L?}2`DoEO8Nr^2G-S{E{-7;x87dc$a%m( zfW>j@+6$rHE3*u^&OB;d`Jm#4e0DB_LdpyVh9lAp0{ILMdl-R;;UgP}PYt293=EHW z89K}v6z+jgLj^O4Pfa19*%0G_Wo9lFyyxb;htccfHi<{P0bSd73gkBg?a6JeV4m_Q zSL2BE0+D=e#d{1(D`swRCXuA&r~WZ rVD3aJc6@18dW131Cich { assert.match(source, /^#!\[cfg\(target_os = "linux"\)\]/); }); + it("keeps the Applications alias when the macOS disk image skips Finder layout", () => { + const source = readFileSync(join(root, "build.sh"), "utf8"); + const dragArgs = source.match(/macos_dmg_drag_args\(\) \{([\s\S]*?)\n\}/); + assert.ok(dragArgs); + assert.match(dragArgs[1], /--app-drop-link/); + assert.match(dragArgs[1], /480/); + assert.match(dragArgs[1], /170/); + assert.match(dragArgs[1], /dmg-background\.png/); + const fallback = source.match( + /bash "\$\{dmg_script\}" --skip-jenkins([\s\S]*?)macos_dmg_name/, + ); + assert.ok(fallback); + assert.match(fallback[1], /drag_args/); + const macos = JSON.parse( + readFileSync(join(root, "src-tauri/tauri.macos.conf.json"), "utf8"), + ); + assert.equal(macos.bundle.macOS.dmg.appPosition.x, 180); + assert.equal(macos.bundle.macOS.dmg.appPosition.y, 170); + assert.equal(macos.bundle.macOS.dmg.applicationFolderPosition.x, 480); + assert.equal(macos.bundle.macOS.dmg.applicationFolderPosition.y, 170); + assert.equal(macos.bundle.macOS.dmg.windowSize.width, 660); + assert.equal(macos.bundle.macOS.dmg.windowSize.height, 400); + assert.equal( + macos.bundle.macOS.dmg.background, + "packaging/macos/dmg-background.png", + ); + assert.equal( + existsSync(join(root, macos.bundle.macOS.dmg.background)), + true, + ); + }); + it("pins one build version and validates exact package metadata", () => { const source = readFileSync(join(root, "build.sh"), "utf8"); assert.match(source, /BUILD_VERSION="\$\(plan version\)"/); diff --git a/scripts/tauri-contract.test.mjs b/scripts/tauri-contract.test.mjs index d5bb1d8..dc21cea 100644 --- a/scripts/tauri-contract.test.mjs +++ b/scripts/tauri-contract.test.mjs @@ -170,6 +170,8 @@ describe("Tauri frontend contract", () => { assert.match(rust, /MenuItem::with_id\(\s*app,\s*"dashboard"/); assert.match(rust, /app.emit\("app-navigate", "dashboard"\)/); assert.match(rust, /apply_tray_menu/); + assert.match(rust, /run_on_main_thread\(move \|\| \{/); + assert.match(rust, /fn replace_tray_menu/); const tray = readFileSync(join(root, "src-tauri/src/tray.rs"), "utf8"); assert.match(tray, /fn labels_for/); assert.match(tray, /fn actions_enabled/); @@ -421,7 +423,18 @@ describe("Tauri frontend contract", () => { assert.match(githubUpdate, /\$process\.ExitCode -ne 0/); assert.match(githubUpdate, /1223/); assert.match(rust, /\.try_begin\(/); - assert.match(rust, /phase: "installed"/); + assert.match( + rust, + /ManualRestart =>[\s\S]*?emit_finished_update\([\s\S]*?"installed"/, + ); + assert.match( + rust, + /DiskImageOpened =>[\s\S]*?emit_finished_update\([\s\S]*?"downloaded"/, + ); + const updater = githubUpdate.split("mod tests")[0]; + assert.match(updater, /async fn open_downloaded_dmg/); + assert.doesNotMatch(updater, /fn install_dmg/); + assert.doesNotMatch(updater, /fn dmg_relaunch_script/); assert.doesNotMatch(rust, /fn spawn_background_update_checks\(/); assert.doesNotMatch(rust, /fn fetch_signed_update\(/); assert.doesNotMatch(rust, /open_linux_deb_release/); diff --git a/src-tauri/src/github_update.rs b/src-tauri/src/github_update.rs index d5c67b4..6996ed3 100644 --- a/src-tauri/src/github_update.rs +++ b/src-tauri/src/github_update.rs @@ -2,7 +2,9 @@ //! //! Check uses the public Releases API. Install downloads the platform package //! and applies it with `pkexec apt-get` (`.deb`), a replace helper (`AppImage`), -//! or an elevated NSIS installer (Windows). Do not log asset URLs. +//! or an elevated NSIS installer (Windows). On macOS the verified disk image +//! is opened for a drag install and the running app is left in place. Do not +//! log asset URLs. use serde::Deserialize; use std::{ @@ -52,6 +54,8 @@ pub enum ApplyOutcome { ManualRestart, /// A helper script will replace this process after exit. HelperRestart, + /// The signed macOS disk image is open. Nothing was replaced or restarted. + DiskImageOpened, } #[must_use] @@ -325,12 +329,15 @@ async fn fetch_latest( parse_release(&bytes) } -/// Applies a downloaded package. The caller must pause the stack first. +/// Applies a downloaded package. +/// +/// Linux and Windows callers pause the stack first. A macOS disk image is +/// only opened, so that caller must leave the stack and this process running. /// /// # Errors /// -/// Returns a platform error when `pkexec`, the helper script, or NSIS cannot -/// start. +/// Returns a platform error when `pkexec`, the helper script, NSIS, or +/// `open` cannot start. pub async fn apply_package( kind: InstallKind, package: &Path, @@ -353,11 +360,12 @@ pub async fn apply_package( InstallKind::Dmg => { #[cfg(target_os = "macos")] { - install_dmg(package, expected_version).await?; - Ok(ApplyOutcome::HelperRestart) + open_downloaded_dmg(package).await?; + Ok(ApplyOutcome::DiskImageOpened) } #[cfg(not(target_os = "macos"))] { + let _ = (package, expected_version); Err("macOS packages cannot be installed on this platform".into()) } } @@ -435,264 +443,33 @@ async fn install_appimage(package: &Path) -> Result<(), String> { Ok(()) } -/// Installs a downloaded `.dmg` on macOS. +/// Opens the verified disk image in Finder. /// -/// The bundle is staged with `ditto`. A detached privileged helper then waits -/// until this process has exited, replaces `/Applications/BiFlow.app`, and -/// opens that bundle as the desktop user. Closing the window only hides -/// `BiFlow` in the menu bar, so a manual relaunch keeps the old process. +/// The user drags `BiFlow` onto Applications. This process stays running, so +/// the menu-bar copy keeps the old version until they quit it and open the +/// Applications bundle. #[cfg(target_os = "macos")] -async fn install_dmg(package: &Path, expected_version: &str) -> Result<(), String> { +async fn open_downloaded_dmg(package: &Path) -> Result<(), String> { info!( - event = "update.dmg_install_started", + event = "update.dmg_opened", section = "updater", initiator = "tauri_command", cause = "verified_package", - trace_route = "ui->apply_app_update->install_dmg", - "installing the verified macOS update" + trace_route = "ui->apply_app_update->open_downloaded_dmg", + "opening the verified disk image for drag install" ); - let mountpoint = std::env::temp_dir().join("biflow-update").join("dmg-mount"); - tokio::fs::create_dir_all(&mountpoint) - .await - .map_err(|error| error.to_string())?; - let attach = tokio::process::Command::new("hdiutil") - .args(["attach", "-nobrowse", "-mountpoint"]) - .arg(&mountpoint) + let status = tokio::process::Command::new("/usr/bin/open") .arg(package) - .output() - .await - .map_err(|error| error.to_string())?; - if !attach.status.success() { - return Err("could not mount the update image".into()); - } - let staged = stage_mounted_app(&mountpoint).await; - detach_update_image(&mountpoint).await; - let staged = staged?; - let result = spawn_dmg_relaunch(&staged, expected_version).await; - info!( - event = "update.dmg_install_finished", - section = "updater", - initiator = "tauri_command", - cause = "installer_returned", - success = result.is_ok(), - trace_route = "ui->apply_app_update->install_dmg", - "macOS update installer finished" - ); - result -} - -#[cfg(target_os = "macos")] -async fn detach_update_image(mountpoint: &Path) { - let detach = tokio::process::Command::new("hdiutil") - .args(["detach", "-force"]) - .arg(mountpoint) - .status() - .await; - if !detach.is_ok_and(|status| status.success()) { - warn!( - event = "update.dmg_detach_failed", - section = "updater", - initiator = "tauri_command", - cause = "image_detach_failed", - trace_route = "ui->apply_app_update->install_dmg->cleanup", - "could not detach the update image" - ); - return; - } - if tokio::fs::remove_dir(mountpoint).await.is_err() { - warn!( - event = "update.dmg_mount_cleanup_failed", - section = "updater", - initiator = "tauri_command", - cause = "mount_directory_removal_failed", - trace_route = "ui->apply_app_update->install_dmg->cleanup", - "could not remove the empty update mount directory" - ); - } -} - -#[cfg(target_os = "macos")] -async fn stage_mounted_app(mountpoint: &Path) -> Result { - let source = mountpoint.join("BiFlow.app"); - if !source.is_dir() { - return Err("the update image has no BiFlow.app bundle".into()); - } - let staging_root = std::env::temp_dir() - .join("biflow-update") - .join("staged-app"); - if staging_root.exists() { - tokio::fs::remove_dir_all(&staging_root) - .await - .map_err(|error| error.to_string())?; - } - tokio::fs::create_dir_all(&staging_root) - .await - .map_err(|error| error.to_string())?; - let destination = staging_root.join("BiFlow.app"); - let status = tokio::process::Command::new("/usr/bin/ditto") - .arg(&source) - .arg(&destination) .status() .await .map_err(|error| error.to_string())?; if status.success() { - Ok(destination) - } else { - Err("could not stage the update bundle".into()) - } -} - -#[cfg(target_os = "macos")] -async fn spawn_dmg_relaunch(staged_app: &Path, expected_version: &str) -> Result<(), String> { - let uid = macos_desktop_uid().await?; - let script_path = std::env::temp_dir() - .join("biflow-update") - .join("apply-dmg.sh"); - if let Some(parent) = script_path.parent() { - tokio::fs::create_dir_all(parent) - .await - .map_err(|error| error.to_string())?; - } - tokio::fs::write( - &script_path, - dmg_relaunch_script(std::process::id(), uid, staged_app, expected_version), - ) - .await - .map_err(|error| error.to_string())?; - set_owner_execute(&script_path).await?; - let apple_script = dmg_detached_admin_applescript(&script_path); - let install = tokio::process::Command::new("/usr/bin/osascript") - .args(["-e", &apple_script]) - .output() - .await - .map_err(|error| error.to_string())?; - if install.status.success() { - info!( - event = "update.dmg_relaunch_helper_started", - section = "updater", - initiator = "tauri_command", - cause = "verified_package", - trace_route = "ui->apply_app_update->install_dmg->relaunch", - "macOS update will replace /Applications/BiFlow.app after this process exits" - ); - return Ok(()); - } - if let Some(parent) = staged_app.parent() { - if let Err(cause) = tokio::fs::remove_dir_all(parent).await { - warn!( - event = "update.dmg_stage_cleanup_failed", - section = "updater", - initiator = "tauri_command", - cause = %cause, - trace_route = "ui->apply_app_update->install_dmg->cleanup", - "staged macOS update could not be removed after the installer was declined" - ); - } - } - if install.status.code() == Some(-128) { - Err("package install was cancelled".into()) + Ok(()) } else { - Err("package install failed".into()) + Err("could not open the update disk image".into()) } } -#[cfg(target_os = "macos")] -async fn macos_desktop_uid() -> Result { - let output = tokio::process::Command::new("/usr/bin/id") - .arg("-u") - .output() - .await - .map_err(|error| error.to_string())?; - if !output.status.success() { - return Err("could not read the desktop user id".into()); - } - String::from_utf8_lossy(&output.stdout) - .trim() - .parse::() - .map_err(|_| "could not read the desktop user id".to_owned()) -} - -#[cfg(target_os = "macos")] -async fn set_owner_execute(path: &Path) -> Result<(), String> { - use std::os::unix::fs::PermissionsExt; - let mut permissions = tokio::fs::metadata(path) - .await - .map_err(|error| error.to_string())? - .permissions(); - permissions.set_mode(0o700); - tokio::fs::set_permissions(path, permissions) - .await - .map_err(|error| error.to_string()) -} - -/// Shell script that runs as root after `BiFlow` exits. -/// -/// It must not remove `/Applications/BiFlow.app` until `process_id` is gone, -/// and it must open the replaced bundle as `uid` rather than as root. -#[cfg(any(target_os = "macos", test))] -fn dmg_relaunch_script( - process_id: u32, - uid: u32, - staged_app: &Path, - expected_version: &str, -) -> String { - format!( - "#!/bin/sh\n\ - set -e\n\ - pid={process_id}\n\ - uid={uid}\n\ - expected={expected}\n\ - staged={staged}\n\ - dest='/Applications/BiFlow.app'\n\ - staging=''\n\ - cleanup() {{\n\ - status=$?\n\ - if [ ! -d \"$dest\" ] && [ -n \"$staging\" ] && [ -d \"$staging/BiFlow.app\" ]; then\n\ - mv \"$staging/BiFlow.app\" \"$dest\" || true\n\ - fi\n\ - if [ -n \"$staging\" ] && [ -d \"$dest\" ]; then\n\ - rm -rf \"$staging\"\n\ - fi\n\ - if [ \"$status\" -ne 0 ] && [ -d \"$dest\" ]; then\n\ - /bin/launchctl asuser \"$uid\" /usr/bin/open \"$dest\" || true\n\ - fi\n\ - exit \"$status\"\n\ - }}\n\ - trap cleanup EXIT\n\ - while /bin/kill -0 \"$pid\" 2>/dev/null; do\n\ - /bin/sleep 1\n\ - done\n\ - /bin/sleep 1\n\ - staging=$(/usr/bin/mktemp -d '/Applications/.biflow-update.XXXXXX')\n\ - /usr/bin/ditto \"$staged\" \"$staging/BiFlow.app\"\n\ - found=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' \"$staging/BiFlow.app/Contents/Info.plist\" | /usr/bin/tr -d '[:space:]')\n\ - if [ \"$found\" != \"$expected\" ]; then\n\ - echo 'staged update version does not match the downloaded release' >&2\n\ - exit 1\n\ - fi\n\ - rm -rf \"$dest\"\n\ - mv \"$staging/BiFlow.app\" \"$dest\"\n\ - /usr/bin/xattr -dr com.apple.quarantine \"$dest\" || true\n\ - rm -rf \"$staged\"\n\ - /bin/launchctl asuser \"$uid\" /usr/bin/open \"$dest\"\n", - expected = sh_single_quote(expected_version), - staged = sh_single_quote(&staged_app.to_string_lossy()), - ) -} - -/// Starts the relaunch script in its own session so the administrator prompt -/// returns before `BiFlow` exits. A foreground script that waited on this -/// process would deadlock. -#[cfg(any(target_os = "macos", test))] -fn dmg_detached_admin_applescript(script_path: &Path) -> String { - let command = format!( - "((/bin/sh {} >/tmp/biflow-dmg-update.log 2>&1 Result { - let target_version = info.latest_version.clone(); + version: String, + phase: &str, +) -> OperationAccepted { emit_update_progress( app, UpdateProgress { - phase: "installing".into(), + phase: phase.into(), percent: Some(100), - version: Some(target_version.clone()), + version: Some(version), error: None, operation_id: Some(operation_id), app_available: None, @@ -3008,55 +3007,71 @@ async fn apply_downloaded_package( thirdparty_available: None, }, ); + OperationAccepted { + operation_id, + already_complete: true, + } +} + +async fn apply_downloaded_package( + app: &AppHandle, + operation_id: Uuid, + info: &github_update::UpdateInfo, + package: &Path, +) -> Result { + let target_version = info.latest_version.clone(); + let kind = github_update::detect_install_kind(); + if kind != github_update::InstallKind::Dmg { + emit_update_progress( + app, + UpdateProgress { + phase: "installing".into(), + percent: Some(100), + version: Some(target_version.clone()), + error: None, + operation_id: Some(operation_id), + app_available: None, + rules_available: None, + thirdparty_available: None, + }, + ); + } let current_exe = std::env::current_exe().map_err(|error| error.to_string())?; - let outcome = match github_update::apply_package( - github_update::detect_install_kind(), - package, - ¤t_exe, - &target_version, - ) - .await - { - Ok(outcome) => outcome, - Err(cause) => { - restore_stack_after_failed_install(services(app)?).await; - emit_update_progress( - app, - UpdateProgress { - phase: "failed".into(), - percent: None, - version: Some(target_version), - error: Some(cause.clone()), - operation_id: Some(operation_id), - app_available: None, - rules_available: None, - thirdparty_available: None, - }, - ); - return Err(cause); - } - }; + let outcome = + match github_update::apply_package(kind, package, ¤t_exe, &target_version).await { + Ok(outcome) => outcome, + Err(cause) => { + restore_stack_after_failed_install(services(app)?).await; + emit_update_progress( + app, + UpdateProgress { + phase: "failed".into(), + percent: None, + version: Some(target_version), + error: Some(cause.clone()), + operation_id: Some(operation_id), + app_available: None, + rules_available: None, + thirdparty_available: None, + }, + ); + return Err(cause); + } + }; record_package_apply_outcome(operation_id, &target_version, outcome); match outcome { - github_update::ApplyOutcome::ManualRestart => { - emit_update_progress( - app, - UpdateProgress { - phase: "installed".into(), - percent: Some(100), - version: Some(target_version), - error: None, - operation_id: Some(operation_id), - app_available: None, - rules_available: None, - thirdparty_available: None, - }, - ); - Ok(OperationAccepted { - operation_id, - already_complete: true, - }) - } + github_update::ApplyOutcome::ManualRestart => Ok(emit_finished_update( + app, + operation_id, + target_version, + "installed", + )), + github_update::ApplyOutcome::DiskImageOpened => Ok(emit_finished_update( + app, + operation_id, + target_version, + "downloaded", + )), github_update::ApplyOutcome::HelperRestart => { emit_update_progress( app, @@ -3102,6 +3117,16 @@ fn record_package_apply_outcome( update_version = %target_version, "application update helper was launched; package installation is pending" ), + github_update::ApplyOutcome::DiskImageOpened => info!( + event = "update.dmg_ready", + section = "updates", + initiator = "install_update", + cause = "disk_image_opened", + trace_route = "tauri_command->github_update->open_downloaded_dmg", + trace_id = %operation_id, + update_version = %target_version, + "verified disk image is open; the running app was not replaced" + ), } } @@ -3137,8 +3162,12 @@ async fn perform_complete_update_install( }); } let package = download_github_package(app, operation_id, &update).await?; - record_direct_rules_upgrade_guard(&services(app)?.paths.data)?; - pause_stack_for_update(services(app)?).await?; + // A disk image is opened for the user to drag. Pausing the tunnel or + // writing an upgrade guard would treat a download as an in-place install. + if github_update::detect_install_kind() != github_update::InstallKind::Dmg { + record_direct_rules_upgrade_guard(&services(app)?.paths.data)?; + pause_stack_for_update(services(app)?).await?; + } apply_downloaded_package(app, operation_id, &update, &package).await } @@ -3663,7 +3692,32 @@ fn build_tray_menu( } fn apply_tray_menu(app: &AppHandle, snapshot: &StackSnapshot) { - let Ok(menu) = build_tray_menu(app, snapshot.phase, snapshot.busy) else { + let key = tray::menu_key(snapshot.phase, snapshot.busy); + if !tray::menu_changed(TRAY_MENU.lock().expect("tray menu").as_ref().copied(), key) { + return; + } + let app = app.clone(); + if let Err(cause) = app.clone().run_on_main_thread(move || { + if !tray::menu_changed(TRAY_MENU.lock().expect("tray menu").as_ref().copied(), key) { + return; + } + if replace_tray_menu(&app, key) { + *TRAY_MENU.lock().expect("tray menu") = Some(key); + } + }) { + warn!( + event = "tray.menu_update_failed", + section = "tray", + initiator = "apply_tray_menu", + cause = %cause, + trace_route = "snapshot_watcher->run_on_main_thread", + "tray menu could not be queued on the main thread" + ); + } +} + +fn replace_tray_menu(app: &AppHandle, key: tray::TrayMenuKey) -> bool { + let Ok(menu) = build_tray_menu(app, key.phase, key.busy) else { warn!( event = "tray.menu_build_failed", section = "tray", @@ -3672,10 +3726,10 @@ fn apply_tray_menu(app: &AppHandle, snapshot: &StackSnapshot) { trace_route = "snapshot_watcher->build_tray_menu", "tray menu could not be rebuilt" ); - return; + return false; }; let Some(icon) = app.tray_by_id("main") else { - return; + return false; }; if let Err(cause) = icon.set_menu(Some(menu)) { warn!( @@ -3686,9 +3740,13 @@ fn apply_tray_menu(app: &AppHandle, snapshot: &StackSnapshot) { trace_route = "snapshot_watcher->tray.set_menu", "tray menu could not be replaced" ); + return false; } + true } +static TRAY_MENU: std::sync::Mutex> = std::sync::Mutex::new(None); + fn setup_tray(app: &tauri::App) -> tauri::Result<()> { let snapshot = app .try_state::() @@ -3706,6 +3764,7 @@ fn setup_tray(app: &tauri::App) -> tauri::Result<()> { .on_tray_icon_event(|tray, event| handle_tray_icon(tray, &event)) .on_menu_event(|app, event| handle_tray_menu(app, &event)) .build(app)?; + *TRAY_MENU.lock().expect("tray menu") = Some(tray::menu_key(snapshot.phase, snapshot.busy)); Ok(()) } @@ -4244,6 +4303,42 @@ mod tests { assert!(!exit_releases_owned_network(true)); } + #[test] + fn macos_disk_image_update_stays_running() { + let source = include_str!("lib.rs"); + let production = source.split("mod tests").next().expect("production half"); + let start = production + .find("async fn perform_complete_update_install") + .expect("install"); + let end = production[start..] + .find("async fn install_update") + .expect("command"); + let body = &production[start..start + end]; + let skip = body + .find("!= github_update::InstallKind::Dmg") + .expect("dmg skip"); + let pause = body.find("pause_stack_for_update").expect("pause"); + assert!( + skip < pause, + "a disk image download must not pause the tunnel" + ); + let apply = production + .find("async fn apply_downloaded_package") + .expect("apply"); + let record = production[apply..] + .find("fn record_package_apply_outcome") + .expect("record"); + let apply_body = &production[apply..apply + record]; + let opened = apply_body.find("DiskImageOpened").expect("opened"); + let helper = apply_body.find("HelperRestart").expect("helper"); + assert!(opened < helper); + assert!(apply_body.contains("\"downloaded\"")); + assert!( + !apply_body[opened..helper].contains("schedule_update_exit"), + "opening a disk image must not quit BiFlow" + ); + } + /// Regression (ADR 0115): the application's data roots and the /// diagnostics log came from two independent reconstructions of /// `BIFLOW_DEV_PROFILE`. Assert they are now one policy, and that a diff --git a/src-tauri/src/tray.rs b/src-tauri/src/tray.rs index 26ab7c3..be76653 100644 --- a/src-tauri/src/tray.rs +++ b/src-tauri/src/tray.rs @@ -29,6 +29,27 @@ pub const fn actions_enabled(busy: Option) -> bool { busy.is_none() } +/// Phase and in-flight action that the tray menu already describes. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct TrayMenuKey { + pub phase: StackPhase, + pub busy: Option, +} + +#[must_use] +pub const fn menu_key(phase: StackPhase, busy: Option) -> TrayMenuKey { + TrayMenuKey { phase, busy } +} + +/// A snapshot that still describes the menu on screen does not need a rebuild. +#[must_use] +pub fn menu_changed(previous: Option, next: TrayMenuKey) -> bool { + match previous { + Some(previous) => previous.phase != next.phase || previous.busy != next.busy, + None => true, + } +} + #[cfg(test)] mod tests { use super::*; @@ -58,6 +79,18 @@ mod tests { assert_eq!(labels.pause_label, "Resume"); } + #[test] + fn menu_rebuilds_only_when_the_phase_or_busy_flag_changes() { + let running = menu_key(StackPhase::Running, None); + assert!(menu_changed(None, running)); + assert!(!menu_changed(Some(running), running)); + let connecting = menu_key(StackPhase::Running, Some(LifecycleBusy::Connecting)); + assert!(menu_changed(Some(running), connecting)); + assert!(!menu_changed(Some(connecting), connecting)); + let paused = menu_key(StackPhase::Paused, None); + assert!(menu_changed(Some(running), paused)); + } + #[test] fn never_emits_both_options_from_the_same_pair() { for phase in [ diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 0ae5ff8..b801484 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "BiFlow", - "version": "6.2.76", + "version": "6.2.78", "identifier": "app.biflow.desktop", "build": { "beforeDevCommand": "pnpm bundle:check && pnpm --dir apps/desktop dev", diff --git a/src-tauri/tauri.macos.conf.json b/src-tauri/tauri.macos.conf.json index 43ef429..59f3bea 100644 --- a/src-tauri/tauri.macos.conf.json +++ b/src-tauri/tauri.macos.conf.json @@ -3,7 +3,22 @@ "macOS": { "minimumSystemVersion": "11.0", "signingIdentity": null, - "hardenedRuntime": true + "hardenedRuntime": true, + "dmg": { + "background": "packaging/macos/dmg-background.png", + "windowSize": { + "width": 660, + "height": 400 + }, + "appPosition": { + "x": 180, + "y": 170 + }, + "applicationFolderPosition": { + "x": 480, + "y": 170 + } + } }, "resources": { "../vendor/mihomo/darwin/mihomo": "dependencies/mihomo", diff --git a/version b/version index 0c90244..f1e7d0a 100644 --- a/version +++ b/version @@ -1 +1 @@ -6.2.76 +6.2.78 From dd6694482b26325ec5ed3e570bcaf122f3c8c047 Mon Sep 17 00:00:00 2001 From: Omid Asgari Date: Fri, 23 Oct 2026 19:41:55 +0330 Subject: [PATCH 3/4] Keep the macOS update outcome on macOS and refresh patched crates. Linux Clippy treats DiskImageOpened as dead code, and cargo deny rejects rustls-webpki, tokio, and the webpki roots still in the lockfile. Co-authored-by: Cursor --- AGENTS.md | 8 ++++ Cargo.lock | 82 +++++++++++++++++----------------- Cargo.toml | 2 +- apps/desktop/package.json | 2 +- package.json | 2 +- src-tauri/src/github_update.rs | 4 ++ src-tauri/src/lib.rs | 22 ++++----- src-tauri/tauri.conf.json | 2 +- version | 2 +- 9 files changed, 70 insertions(+), 56 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ab8c9a6..a4915b8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -782,6 +782,14 @@ SHA-256")`); launchd `KeepAlive` then restarts it in a crash loop and the `BiFlow_*_aarch64.dmg`. Sign that disk image in the release job and publish it as the `macos-aarch64` entry. The platform key must stay identical to `signed_target` for `InstallKind::Dmg` on `aarch64` (ADR 0126). +- A macOS-only update outcome must be `#[cfg(target_os = "macos")]` on the + enum variant and on every match arm. Linux and Windows Clippy compile + that variant as dead code when only the macOS installer constructs it + (ADR 0129). +- `cargo deny check` fails a pull request when `rustls-webpki`, `tokio`, + `webpki-roots`, or `webpki-root-certs` fall inside a new RustSec range. + Update them with `cargo update -p`. `webpki-roots` has two majors, so + name `webpki-roots@1` (RUSTSEC-2026-0381, 0382, 0383, 0386). - macOS in-app update must not replace `/Applications/BiFlow.app` or quit. Closing the window only hides the menu-bar process, so an automatic swap and relaunch was easy to miss and left the operator on the old version diff --git a/Cargo.lock b/Cargo.lock index c0e9bc4..3142143 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,7 +77,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -88,7 +88,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -953,7 +953,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1145,7 +1145,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1820,7 +1820,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.9", + "webpki-roots 1.0.11", ] [[package]] @@ -2046,7 +2046,7 @@ dependencies = [ [[package]] name = "iran-split-cli" -version = "6.2.78" +version = "6.2.79" dependencies = [ "async-trait", "chrono", @@ -2064,7 +2064,7 @@ dependencies = [ [[package]] name = "iran-split-clients" -version = "6.2.78" +version = "6.2.79" dependencies = [ "async-trait", "ipnet", @@ -2080,7 +2080,7 @@ dependencies = [ [[package]] name = "iran-split-config" -version = "6.2.78" +version = "6.2.79" dependencies = [ "hex", "rand 0.9.5", @@ -2093,7 +2093,7 @@ dependencies = [ [[package]] name = "iran-split-core" -version = "6.2.78" +version = "6.2.79" dependencies = [ "async-trait", "chrono", @@ -2110,7 +2110,7 @@ dependencies = [ [[package]] name = "iran-split-desktop" -version = "6.2.78" +version = "6.2.79" dependencies = [ "chrono", "dirs 6.0.0", @@ -2152,7 +2152,7 @@ dependencies = [ [[package]] name = "iran-split-helper" -version = "6.2.78" +version = "6.2.79" dependencies = [ "clap", "hex", @@ -2177,7 +2177,7 @@ dependencies = [ [[package]] name = "iran-split-helper-winacl" -version = "6.2.78" +version = "6.2.79" dependencies = [ "tokio", "windows", @@ -2185,7 +2185,7 @@ dependencies = [ [[package]] name = "iran-split-ipc" -version = "6.2.78" +version = "6.2.79" dependencies = [ "bytes", "chrono", @@ -2198,7 +2198,7 @@ dependencies = [ [[package]] name = "iran-split-mihomo" -version = "6.2.78" +version = "6.2.79" dependencies = [ "chrono", "futures-util", @@ -2221,7 +2221,7 @@ dependencies = [ [[package]] name = "iran-split-netstats" -version = "6.2.78" +version = "6.2.79" dependencies = [ "libc", "tempfile", @@ -2230,7 +2230,7 @@ dependencies = [ [[package]] name = "iran-split-platform-linux" -version = "6.2.78" +version = "6.2.79" dependencies = [ "async-trait", "chrono", @@ -2254,7 +2254,7 @@ dependencies = [ [[package]] name = "iran-split-platform-macos" -version = "6.2.78" +version = "6.2.79" dependencies = [ "async-trait", "chrono", @@ -2278,7 +2278,7 @@ dependencies = [ [[package]] name = "iran-split-platform-win" -version = "6.2.78" +version = "6.2.79" dependencies = [ "async-trait", "chrono", @@ -2301,7 +2301,7 @@ dependencies = [ [[package]] name = "iran-split-rules" -version = "6.2.78" +version = "6.2.79" dependencies = [ "async-trait", "chrono", @@ -2323,7 +2323,7 @@ dependencies = [ [[package]] name = "iran-split-taskbar" -version = "6.2.78" +version = "6.2.79" dependencies = [ "iran-split-config", "objc2", @@ -2733,7 +2733,7 @@ dependencies = [ "png 0.18.1", "serde", "thiserror 2.0.20", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -2806,7 +2806,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3096,7 +3096,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.45.0", ] [[package]] @@ -3508,7 +3508,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3708,7 +3708,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.9", + "webpki-roots 1.0.11", ] [[package]] @@ -3813,7 +3813,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3870,7 +3870,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3881,9 +3881,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.14" +version = "0.103.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a" +checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281" dependencies = [ "ring", "rustls-pki-types", @@ -4307,7 +4307,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4912,7 +4912,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -5044,9 +5044,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.53.1" +version = "1.54.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3" dependencies = [ "bytes", "libc", @@ -5385,7 +5385,7 @@ dependencies = [ "png 0.18.1", "serde", "thiserror 2.0.20", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -5450,7 +5450,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -5861,9 +5861,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.9" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +checksum = "e49d0e65abc00c142e8f9428da991da9f1a250c39be97459901eb12b9b49f62a" dependencies = [ "rustls-pki-types", ] @@ -5874,14 +5874,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.9", + "webpki-roots 1.0.11", ] [[package]] name = "webpki-roots" -version = "1.0.9" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +checksum = "f5f280bbb2b287bc8e7de7e3852945a0ff50d1bbcb5ed42b6c7a568c546df171" dependencies = [ "rustls-pki-types", ] @@ -5950,7 +5950,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 453b525..3d22b6d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ members = [ ] [workspace.package] -version = "6.2.78" +version = "6.2.79" edition = "2021" license = "MIT OR Apache-2.0" rust-version = "1.88" diff --git a/apps/desktop/package.json b/apps/desktop/package.json index a1d9cc6..bbb176a 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@iran-split/desktop", - "version": "6.2.78", + "version": "6.2.79", "private": true, "type": "module", "scripts": { diff --git a/package.json b/package.json index f40ae6b..284ff02 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "iran-split-desktop-workspace", - "version": "6.2.78", + "version": "6.2.79", "private": true, "packageManager": "pnpm@9.0.1", "engines": { diff --git a/src-tauri/src/github_update.rs b/src-tauri/src/github_update.rs index 6996ed3..67703de 100644 --- a/src-tauri/src/github_update.rs +++ b/src-tauri/src/github_update.rs @@ -55,6 +55,10 @@ pub enum ApplyOutcome { /// A helper script will replace this process after exit. HelperRestart, /// The signed macOS disk image is open. Nothing was replaced or restarted. + /// + /// Only macOS constructs this variant. Leaving it in the Linux and Windows + /// builds makes Clippy reject it as dead code. + #[cfg(target_os = "macos")] DiskImageOpened, } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c6416d7..b0dced6 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -3066,6 +3066,7 @@ async fn apply_downloaded_package( target_version, "installed", )), + #[cfg(target_os = "macos")] github_update::ApplyOutcome::DiskImageOpened => Ok(emit_finished_update( app, operation_id, @@ -3107,16 +3108,7 @@ fn record_package_apply_outcome( update_version = %target_version, "application package installation completed; restart is required" ), - github_update::ApplyOutcome::HelperRestart => info!( - event = "update.restart_helper_started", - section = "updates", - initiator = "install_update", - cause = "package_install_pending", - trace_route = "tauri_command->github_update->apply_helper", - trace_id = %operation_id, - update_version = %target_version, - "application update helper was launched; package installation is pending" - ), + #[cfg(target_os = "macos")] github_update::ApplyOutcome::DiskImageOpened => info!( event = "update.dmg_ready", section = "updates", @@ -3127,6 +3119,16 @@ fn record_package_apply_outcome( update_version = %target_version, "verified disk image is open; the running app was not replaced" ), + github_update::ApplyOutcome::HelperRestart => info!( + event = "update.restart_helper_started", + section = "updates", + initiator = "install_update", + cause = "package_install_pending", + trace_route = "tauri_command->github_update->apply_helper", + trace_id = %operation_id, + update_version = %target_version, + "application update helper was launched; package installation is pending" + ), } } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index b801484..2fac5bd 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "BiFlow", - "version": "6.2.78", + "version": "6.2.79", "identifier": "app.biflow.desktop", "build": { "beforeDevCommand": "pnpm bundle:check && pnpm --dir apps/desktop dev", diff --git a/version b/version index f1e7d0a..d3487ae 100644 --- a/version +++ b/version @@ -1 +1 @@ -6.2.78 +6.2.79 From f9b8a6068ee5fe44707789851ec73bf2718ad1a3 Mon Sep 17 00:00:00 2001 From: Dariush vesal Date: Sat, 24 Oct 2026 15:56:55 +0330 Subject: [PATCH 4/4] Preserve paused connections after a failed macOS update download --- AGENTS.md | 6 +++ Cargo.lock | 30 +++++++-------- Cargo.toml | 2 +- apps/desktop/package.json | 2 +- apps/desktop/src/api/mock.test.ts | 21 +++++++++- apps/desktop/src/api/mock.ts | 9 +++++ docs/adr/0129-macos-update-downloads-only.md | 5 +++ docs/screenshots/settings-speed.png | Bin 104064 -> 104065 bytes e2e/primary-flows.spec.ts | 34 +++++++++++++++++ package.json | 2 +- src-tauri/src/lib.rs | 38 +++++++++++++++++-- src-tauri/tauri.conf.json | 2 +- version | 2 +- 13 files changed, 129 insertions(+), 24 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a4915b8..746e2f2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -71,6 +71,12 @@ If a required command fails or emits a warning from project code, fix it in the ## Lessons +- A failed macOS disk-image open must not resume a user-paused stack. Only + package kinds that pause the stack for installation may restore it after + failure; pass the install kind into the recovery decision (ADR 0129). +- Scope About update assertions to its region: the network footer also has + a `status` role, so a page-wide status locator matches two elements. + - macOS ships Bash 3.2, where expanding an empty array under `set -u` fails even after `array=()`. Guard optional build arguments with `${array[@]+"${array[@]}"}` and test signed/unsigned argument forwarding diff --git a/Cargo.lock b/Cargo.lock index 3142143..c5b0f4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2046,7 +2046,7 @@ dependencies = [ [[package]] name = "iran-split-cli" -version = "6.2.79" +version = "6.2.80" dependencies = [ "async-trait", "chrono", @@ -2064,7 +2064,7 @@ dependencies = [ [[package]] name = "iran-split-clients" -version = "6.2.79" +version = "6.2.80" dependencies = [ "async-trait", "ipnet", @@ -2080,7 +2080,7 @@ dependencies = [ [[package]] name = "iran-split-config" -version = "6.2.79" +version = "6.2.80" dependencies = [ "hex", "rand 0.9.5", @@ -2093,7 +2093,7 @@ dependencies = [ [[package]] name = "iran-split-core" -version = "6.2.79" +version = "6.2.80" dependencies = [ "async-trait", "chrono", @@ -2110,7 +2110,7 @@ dependencies = [ [[package]] name = "iran-split-desktop" -version = "6.2.79" +version = "6.2.80" dependencies = [ "chrono", "dirs 6.0.0", @@ -2152,7 +2152,7 @@ dependencies = [ [[package]] name = "iran-split-helper" -version = "6.2.79" +version = "6.2.80" dependencies = [ "clap", "hex", @@ -2177,7 +2177,7 @@ dependencies = [ [[package]] name = "iran-split-helper-winacl" -version = "6.2.79" +version = "6.2.80" dependencies = [ "tokio", "windows", @@ -2185,7 +2185,7 @@ dependencies = [ [[package]] name = "iran-split-ipc" -version = "6.2.79" +version = "6.2.80" dependencies = [ "bytes", "chrono", @@ -2198,7 +2198,7 @@ dependencies = [ [[package]] name = "iran-split-mihomo" -version = "6.2.79" +version = "6.2.80" dependencies = [ "chrono", "futures-util", @@ -2221,7 +2221,7 @@ dependencies = [ [[package]] name = "iran-split-netstats" -version = "6.2.79" +version = "6.2.80" dependencies = [ "libc", "tempfile", @@ -2230,7 +2230,7 @@ dependencies = [ [[package]] name = "iran-split-platform-linux" -version = "6.2.79" +version = "6.2.80" dependencies = [ "async-trait", "chrono", @@ -2254,7 +2254,7 @@ dependencies = [ [[package]] name = "iran-split-platform-macos" -version = "6.2.79" +version = "6.2.80" dependencies = [ "async-trait", "chrono", @@ -2278,7 +2278,7 @@ dependencies = [ [[package]] name = "iran-split-platform-win" -version = "6.2.79" +version = "6.2.80" dependencies = [ "async-trait", "chrono", @@ -2301,7 +2301,7 @@ dependencies = [ [[package]] name = "iran-split-rules" -version = "6.2.79" +version = "6.2.80" dependencies = [ "async-trait", "chrono", @@ -2323,7 +2323,7 @@ dependencies = [ [[package]] name = "iran-split-taskbar" -version = "6.2.79" +version = "6.2.80" dependencies = [ "iran-split-config", "objc2", diff --git a/Cargo.toml b/Cargo.toml index 3d22b6d..5461950 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ members = [ ] [workspace.package] -version = "6.2.79" +version = "6.2.80" edition = "2021" license = "MIT OR Apache-2.0" rust-version = "1.88" diff --git a/apps/desktop/package.json b/apps/desktop/package.json index bbb176a..e3514c5 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@iran-split/desktop", - "version": "6.2.79", + "version": "6.2.80", "private": true, "type": "module", "scripts": { diff --git a/apps/desktop/src/api/mock.test.ts b/apps/desktop/src/api/mock.test.ts index 907a205..0247c97 100644 --- a/apps/desktop/src/api/mock.test.ts +++ b/apps/desktop/src/api/mock.test.ts @@ -1,10 +1,12 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { MOCK_HIDDIFY_ID } from "../lib/outbound"; import { APP_VERSION } from "../version"; import type { StackSnapshot } from "./models"; import { mockApi, resetMockState } from "./mock"; describe("mock transport", () => { + afterEach(() => vi.restoreAllMocks()); + beforeEach(() => { sessionStorage.removeItem("biflow-mock-force-missing-helper"); resetMockState(); @@ -172,6 +174,7 @@ describe("mock transport", () => { }); it("emits download progress during mock install", async () => { + vi.spyOn(navigator, "platform", "get").mockReturnValue("Linux x86_64"); sessionStorage.setItem("biflow-mock-update-available", "1"); const phases: string[] = []; const unsubscribe = mockApi.subscribeUpdateProgress((progress) => { @@ -183,6 +186,22 @@ describe("mock transport", () => { expect(phases.at(-1)).toBe("restarting"); }); + it("finishes a macOS update as downloaded without starting an installer", async () => { + vi.spyOn(navigator, "platform", "get").mockReturnValue("MacIntel"); + sessionStorage.setItem("biflow-mock-update-available", "1"); + const before = await mockApi.getSnapshot(); + const phases: string[] = []; + const unsubscribe = mockApi.subscribeUpdateProgress((progress) => { + phases.push(progress.phase); + }); + await mockApi.installUpdate(); + unsubscribe(); + expect(phases.at(-1)).toBe("downloaded"); + expect(phases).not.toContain("installing"); + expect(phases).not.toContain("restarting"); + expect(await mockApi.getSnapshot()).toEqual(before); + }); + it("publishes real start stages on the snapshot", async () => { const stages: Array = []; const componentSnapshots: StackSnapshot[] = []; diff --git a/apps/desktop/src/api/mock.ts b/apps/desktop/src/api/mock.ts index e26b709..96a26b4 100644 --- a/apps/desktop/src/api/mock.ts +++ b/apps/desktop/src/api/mock.ts @@ -637,6 +637,15 @@ async function simulateInstallProgress(version: string) { }); await new Promise((resolve) => setTimeout(resolve, 20)); } + if (/mac/i.test(navigator.platform)) { + emitUpdateProgress({ + phase: "downloaded", + percent: 100, + version, + error: null, + }); + return; + } emitUpdateProgress({ phase: "installing", percent: 100, diff --git a/docs/adr/0129-macos-update-downloads-only.md b/docs/adr/0129-macos-update-downloads-only.md index 96feaf8..0659ac0 100644 --- a/docs/adr/0129-macos-update-downloads-only.md +++ b/docs/adr/0129-macos-update-downloads-only.md @@ -15,3 +15,8 @@ only hides it, and the single-instance plugin delivers a later launch to that same process. The new version runs after the operator quits BiFlow from the menu bar and opens the Applications copy. Linux `.deb` still waits for a manual restart. AppImage and NSIS still hand off to their replace helpers. + +Amendment (6.2.80): failure to open the downloaded disk image also preserves +the stack state. The recovery path receives the install kind and resumes a +paused stack only for an in-place installer. A user-paused macOS connection +must remain paused when Finder cannot open the image. diff --git a/docs/screenshots/settings-speed.png b/docs/screenshots/settings-speed.png index 9d28f2836a61acb61473b1f52e23ff6773f1e984..1a9e3b51bac4f66929e36012b0e355248de6c937 100644 GIT binary patch delta 68562 zcmagFbwHHe6E-B<<_ z=N_K>>PM(F-MYsNuw2rzX0}I8f=xgIwEz8rBs_3+nZkl)arpU!9Xazc0Ud%DdK0pbTPg5wiYuf((U#Boe5e+~Hf_Z+T~ zx5*1dXpRhf?27j(&#?S@;d0eQ|4UR*W3ULZAqgP|bxIgWr%SL*)<~1~z3b1gzP_Rv zhBH%5^Cb^c=yK7va(}C8` z`hzn=)6%H!k6rw{6g0u@pMR9<%X+$$d^aK=Y*F|MJbil55%=EAcmLOr1#WK^y#oJwXy#-HXHrT2( zX{}JCO{aQ!_FQ}I>*G^;6jM`90O6TI=@PV};$KvplJ&sHjTM1mQXerP}<;JM+6?u+hR;J-tC% z6nj8DAI33pcGj)cqar*C2cO5?RtQh8KcDZ^e_NyxSg7y8u9iAp2VH9b0niIKgB7*w z5F=L_FlYMU1nweo;x2=O_six?%R<6=AJar{x<~HSTc7ewRMiI+rCj4uk)Lx4CncU7 zm3w2XFlXl~x+aiqjODgR{Z;_Ps^2hkA{=9*3I7ByQ=AJLQs13yP|wefX~Z_`xfz&t zPg^%+=PE^Y<_;wEs>wwHGspBDCM5I+Z-NaRlHKY~j*yR)!>wK0=L;4}z{!PX9#WZu zgBPkZlf4D1b>$Do^NdAPg|^`zYPZ%i#|#sB;d7h++=OcFYDI$K++BzjkAX4FI|t%a z{q^%NKg}yw$gU{K$!N#Ev=YJ}v10S>CzehTax>v!(_P<%0j@~bn|)$Qr*96n*y?A( z&!GlsRO8mYah&?O+QIrqtIdh0QaJ1wPO{qTpn~hsc*EK}`+ZCA!}N0OX%89)IosRq z&07$#Z)yl{;Iq8gSy$bnRt_?(wigm#+#gyo#LiVZbneM2gq6k@mA!iIQ{I76{RZr} zlfEx4Rlz+1aC4n7wN$B%kT+Ch!^PVS>g9!9_@s9j$Or7!vusE+)Wg-s?k=$ogji48uT|g6T8&!_iX)gw>raVg}UPLEc@g zv9RwBA!wpRLr6^Q&gb#_<(2ckn+LnS)}uuxH48r@tp`)BQG}!n7RIN$LpCd)^IKsI zU21_39~7nON5L9T*Aj-{uvC$J+qFA=nqpp@iDQ5`w5Jyd#f}q&JLq33z)E&I)Jb-e zY5-h!JZy8p4~1?r7Www*Xn2~~u&A@>b@}^_oiAFAIOnOp+jXb()o!C&%DzTM_NQUR z)YyX+yq_OTq@ zVsQv`2n_n3U59wA66;ku&3}x23Cx${g6e}qe(ZqJ%^v0PC0E;WuD{pl-ngGHz%*+% zyVOE;HkGZRNm~QgGU|T*3HIWgIqfDXC5);fY#QOU+;+a7_KWu6L;k%ZkK4y6& zaL~HBdLKI{?pRymcGQuhd>`^VZK-d^OqjIra&`NFrG#NL&oHp9xoh=UIo_QovB{*YN6jJ0@DyvU+pk4Br<3_wgQ}*sOrf*A_gcDf znfWf(FECL@x8!AaoGJ|ogYcV{n9XrBk9TuE2VW~+zA?+m>QtYFpiuxJ3r(+Xv3gLr zgAw9n_Din_t}a?1MA;jg4;bWMqCw!&_q`|by<@Q9rL0aU_G0d;Z|zv2v2niH#NIbj zL>m0vc??qOZQO$e>hen7>^ubpc>zI~O_sOEyiY229OE(HMxg(cBXY{+VPax*E~cLo zIPtuqDEySEVMHp6pplFji_A@PGwg@pkiEWP}V&3EW!OfsQq z`+B*eIg{{G5z(zLd#TeV5FL>FC2Hch&@`_DA9IR3`RsOEC(;T`a?E4Du9^5=G$-P? zryG!rh1No?fyCT~wQrNDR7-XjT<3BfE)*Q=mLBQa)c)TNA(HMFQAxQ4)jU zQPTNJSdazfJsRaC!yrs8;&qOE+Cv`@Ck(aeY>vZll`k7>no1 z1X%>Pm&hlConH4IQgVE8!nwPY8FNj#KTC^eYC|3>diSg9c?9BUu_0FXmehjkTEk%0O!qkpNf}mj zv*%_w8eb?OTLQ07*PGMPsdb0BWoNn8QE~4at*RhNYAuiB*tTA{AG|LeXVL%Ns!c&h zNok+4W-*9J^;zF3I#)>teC@TZtkw&WzRmUt?Ofya=-~%;;_u5kn7-%gnSgO3&T_5Z zm9M|c)`ZwM#m<$ydmW$1&o$v_ROYff=39G{ATiPgbKlkM$#eUnnr#R1(^-KJO}$8; z`7AA(IqVSs52sV8q!33_9n3S(!q^WIx&|bv=ED9eF<&?i5Ok}N&subtrb)z zKNq(15c>y;@>04SQPl}@d{#l=Qm)L`S`W_9x^;+dP;Vq8X`DcXJ-{Hgd`e96q=F=V07un{^+#4vyS+YSch z$Np9l+MHG2tX|ldMIG0{L%mYRaKexS8dFt&pK#z>O!Dfr@oF-m39aOd{r8tCh*m>( zcUsc7qdQ{c)#bc_d_Y9ta%E*N6qAh6DO%V!LD;rpH34c@D?I5pg^^ znO2p`JC=9^DgwSt`ttEOLR;}?uKS+ocUDj$*+cRScJk7UDyO;7M3ay>*xpzr9nnM7 z?^z&D-jj;Qbfnb5wKx@XkxQNzt>wNe*+ppbk{vo&+$!a{)?GnFo#wSGyUDMi5$Sh< zN)YtgX_i$$8KPp6e?1r;5gfOlZ22rob{ENQSed4I#AbEW!7XOjJ653mc0Cd6GBJ;d zpn0>=h+H(2H>oqTv$E11Q>N4{p6R=r=K+||jy!&#HQjJ3-Q~WcM)@@!pI`6pZF-C zj!5KlI=FKp;(PLWVjfo};xQO|pT&u;r|3J_x*r_v;@)#SwV-;rn>G``;pApJvAoRy z?9LT897a9l0b?!{I)!)NkCyuKx<;b;K->*O~EM=51RUF zBEzKy8>jm|o5#ZM(o`dxmO9fQ+f5l+Xj1xf_sjd1;9!Vj9dxI<+X6^xE2`|DasT-+vRM9df<;C|U+S;!<3LB=4mUIWL@)p)e70}7H@&2Tk`LcRR1qST{& z8$&GdZ2uX)Pytrh)k&wq{s*G{yZQ}xN`C11$$Ls&F2Sc%#wTs^O8NQ6IAivP*S}`B z+$MW0?6^>FJ~z(fd!6R6WTxt9|3C{g3)tDeTqMY@3pi=Obi+lElUfKX$}TDafpW)& z-1w-d(FWsk5c`i)nSRs85|!-^)h&XQV?7VRuFkww_{qCPko?sXZLK8Vshf8?$*`L* z`d)Z14Z4yL`VqBtwEJM^yX%44KJFoULCE(}VvC0us@q$1|8vH=a2u-SuJ z!z;mGZ^cBKIQe(BV(b0+Fs<86^!Lszw#>fY7p5TkE%uTlwesCqZE@W|Ubl#xl)J!r z+h&t4F%p9`8K?7asayDw{FJXvPGL^qr*$6Hx7ToBzz{p}nGz}K77ksN9Q6H_rxn5_ zUgdK-qZCO#4?s!~%K5nWTRX;tBsizH()VEiKa63T$9{8GyKD%-sr=}E4nTOxMyFIEpj zjeYLde~h|EbWgd?TV?Z6I7;!s*_6#G;dSQl$aD% zgG!~vzO15evi}yw^N1i^Vy&#d=`nF-0r0a)V`5xY>ftFwSGQ@AL$CHMc*J^`_wAF; z6cttR*c8o-mrb^zVP7TUy=8D`kt+e?+XtR!yLq$U<%XEiqz{#L3B%Jq;>hj{Q;~d4 zRBnkewvb*)$oF8c%zu>lzz!{QLM8r@u7iZ&ABc9V4Ir<%q|N4c^6_F~Dc1%LvW1e} z`wS{B{H$N-dlYJM$5l`A>Abv#Pc#^8JedA;W0nT(VW(YZkZ-P(uAU-XVM?7oh+Ae z4(Y#i8(BQR%ZRi=<$WRQVkE{UG&8Ug%}=@mcH!9eXBj#0B=Yh&BGIcME$;ZUY8{qJ!W$=!jfKMYh!~Q zsmfA@@E19SbyZWPxQIil@?5F&_gg#`CWxg1M~oQUWyA({(m$A;g7dj#$fq7o7Wy%j zMrH*jUyoLRdh(joXs!jq`iPcXm4x?sN-3I);E|1m#KV|D#8F)(0!GUrq-1zExZ z%wH&?v;T)Z#Z>P~

jDf|4gXKS?m@Omj$!LBrYc285cFiQHsn+5n+P}$Yl?5mMDrf=k; zJuec0Aw1`e2bfnxJ2qEiY8@^dry7a=r&5Qd6{Fd!j3${7B68mHAm@Y3S17YLn>}s~ zgbNy_8u5q3Vy861F0(K|T1I;5`2lKs^odUWgXr*diLsTWnJfR;;##EfB_4m5Mqh(h zalQVv{CqTs2!FmI&&TTIt*pj(NiShCsY%GjOS#hQHow4dZ;6|&zKbYtb|@;0S^eDg z--IO1=<=}_MJ0Zhjgpp(m-#+-eojZ#yd(*o)8K>i36}kx4Tjw!pHi;`_ z)O;K;zQ}M36dw3DKHic$Yf^ngcK$$)<1;za^D?G7g|~0pss|Tn#T{Kbu_6t)c2azW zcIJUZ9OdFkH!4+SpOq;E0RcgFRdlir^k^iz&WUcg*kXHGKAN7vSG*8UZ{-_Hmghq+ zEvE&;LyNS^4W@?KO!3?mcRFc{9B``U0Bb1Fi)?#7VLAeXramHyEBP;1%9Si13P(62 zkv?~QwZtCPdxU8w5sPwHIL>>vFHofW9?$FJxE)6zztWr@y#P+1C zNnQUhgc`N4!Ik_ds~u#q1R*#1M?T*;eRE6JitR07>P+SD(W>)kP=omIxFN&1A2d_v z7%1lBk1oaZ6Bj&lOiVL6F&$J19U1>vLrHGtvyz#~%yj?taWK%`$N67WS!H`J7Xi53 zGZlTm^)gO;nSgMe;uGqCaA54igj{TL)zB35AoJw8)vu$l>*`x=p! zR%+EsMb%*DlKMzTuW0HWrn;h0^GjLLRPpr4R{;S5{g>Sr`c~Ht-e(v?PGp}21?^w7 zAY4iRy?7Zcq)w%f*X5|Q~Gseb+6McsqrTdsPBjq-froWF(`-PiT#%e zaL=3QWGP6q9TJ$TGbF^!4K}7$+mBt~DgAFg$6knZlC23poq5+8ltrg1J$fX2q?;od zzUzs?fYei^EcDXJ7(6gSzUlZ1o=REF`S)*(dc*S3d2#Ez;qL=rAVkmp++!m#rW}7| z-VZaBGt!P0>3XeMW8%p2CHPd*dYvu9weF2mF&_v zx8fvxu^_>9bx$Nm7h8&Yo%42r$b#oi*jG!HlDRV19gyHWdOE6`Z z|38So_ESmrh1K}6S8sgLl+R+f- zaw71YXKyxw;XAnz*KPK?{K1A#j1>RIK3>w*#!nwkjJANh6{vbcgxV);x&7`s9pv>| zX>|9_nk$13H8q9r#4qTVd&@>u?o5&^iKmT{ZtM2xBs?YCBmDU84k~xIJ^CJIv}@ss zX>QvO8d!k+Qq$6kf?7(IFO3ttuHseS=+29X|I*E;TNE`;jJseTpS6x%Jekd%^!f>t z2{0F%aGSfpO$ybrPKTbn>Sm|yGBxcF#)>AU9G*~kP>0+U-<7hY@A^c7_YrV}Z4AG@Q&73Xz1kfDwMPIuLj5*f4Rr>8AC zePWM%kD9XEo^y%VTja+4gwYkuQa?Or{H)Dd`U;;X{%uZws+NcdoblP9?Z)@PR8%dj zLVH)r%Y2CR9P1AX9J$Z53Ot7^CB=V!9NUa>?WyBF9&PmU^RG@AD1H>rq48k_QCH0K z_U+pXOvP(W0FMt!cYflnag`ZKHte4f(LYJvT*36;y?2j5le1ca#1StgbvT3VCZte- zc`JM3KHQQlN8KJBhq2b01MHM)Z=RoPwxfEY+P|i#s+L#%sx%&{H1wE1y;Mjw5Z)QD z;waDZhrizln3eBJWc?#zossFUE8_8A*_T<7h_ClaB$%yc_An?rUEzjyPSo{T#!~0{ ziIpkH{=3#`on|2^A1m$P%;52h-#>mtq<;(k>f^GCA`DMWsdOG|h56w5JTCD{`?pfJ zJRG}t{8NdVU|!)n^XUXTy&tES7Ot z(T%}$ET?C4@@+O9!$8KFKRRWrcBe^8GP3EG9y@!KtXSYQv-0ox7|7>qC^_-?QsL5;&PRrpP>nxm)qq zU$K!AWiaD;2DQ!{PIC89==l(?I;K{ zx6sg0>Q}!qS~>%s!9+}vVj+07;%NiJXjM3=UR_qux^Iy_8@OKARtnDTIKPB4_%iF|C$xzF&8@8m_@XWKURih__rige^+1|> z2>wr9QSs~BAZNr?7QB0`lG)Ss_Vx92^-09XL3dVxrIv%z_K?Rf0$jtz{y#cD|(h_j)dYXD_w) zU(|#=lrd+Ja6jzBM-0o}7<2F2_)13(D~*SR?@Rgdc4>%6)5v_sOi)37>jpD2A{CO< zY7k4YY(B+cl-9HWvL@5(d^b*lpN(O_ZyTKGQ{^)?{Em)^Ea;M;innxZ^X*~HYRu7} z02U-6J4tYF>vHKTL;UJT;HOK(wOsWCJjWiNk6*u;8#i=(P*> z#!lPZJVUG7`ft|ty8bkR_vWt`nu{&|8s;+13W$5E>ngWid;@;e+_)w3IVN#Ur6EG2 zLXr9JzF(APGjEIx3znDt&0bzt@L!Yj#oXX!Q7ro2{Nb%FJK;=5Q}2>fWTXqP5EB#q zQ5rekT!e=Clisl|B*-$^Kl{A;NHHknX5r=%FQL-Z52n|(5zVZ1?TO_ZD=X%I*1dCW zc?B>ZOjEh}C)%!F{Uu$S%^2_D7*%MzbBmhJMdKPM@DtJILygF4SGsGTo;(N&`}(pW zg5*zH>bm~@Q%t&Z&6HvZv*HEDh1FiPsPxqqYMEsVuSxS2Lzv6qGy6a5K5ME^<>%ki zd~S5-_YqX|a=+&G(Y~ns^UyAN-)G-}W=!)R3*EW)c~J1mUp;QJlCv7Q9NlzQcfOv2 zVUY`bK8~aQRfGHqYD~WJA2<)W)-2sJsR3Z9Lvy&7@qgvaX1@V$?;i}4G&aNF@}mv$GXaG&i~Q1a7)g#s^% zwVYm2!0Y$?Os?wLVX9u*-gy38{DP|IZo}X)TtKk=F-FQ_|07q*oF^HgpO{k~x*BK+Xd&X>lIgfn@Eo@4=IfpVPf5e*J`!HwSxTN4UVy@pq zvcdyF-a1u&dPThYc|#Sq*i*CNr@BWQ3rDYsY;35HEezZkj@yWwP8!##9#a=i$H?1^ zJI(g2;@jzEd}?DIbg+i=G|{8@Cxi@QE;}da^fw0#!V-`9%9Wk0m$ShHq`$WTpS!ZQ z2GbMZm$dI&E6w_E%c3}Ar{RuJ%W$>RIj^HiJ=9cLKi>v6shp9Csl>R@>Lu2!C58(| zqtWP?T!L{r`>>;4l-8t&4dw9jW-@khV-vCZ`m#KXHBuCZ>KZryv0@+dr zfiYbZ;LE;co}#p=>mu_9i>k5&js{{!d*mV#r(1O|k`LZfa>byrBk&EJDAVrqi&pi9 zB81TU!wT^L(z%Lb7~x*6+o;7Lk8x=(B(DnJOcJ!}*CKh-UxtW;1M_^?ZPLl!=sMlh zDR_w)W*R?MRJRugN!%=R*&C$^B%$@5d1nPGy>8;U;+OFIw>|>1-`NIB7A`QY`k<6U zXQj0}TMh%#9Z5Q$B-Fi0>|8O+4MU6aFX7v@Dtg76LzN9CM@OqR6DGcFMDe@$HrqB-wLn90sy`XiRc%WYJf@vH5xlE!ZZhtbgxtY+Flnz|C<%dBM$N zs;3g~e9)zC!rs`wDVD#jPK@RNo(6Jz~bYqg*BPWI+E>uyK0;Cmd6W$2jA@|f$n{V6kL`YVTP z%EvS(?R))%rr{y zFbDJpz7Y~C&~unBBL8z` zm!n!@Kg9%|ax%HxY+DB*K2`tVm#uWa1qb5ccA}(1_@RluzCM78*W%FM6u=K}GAyhl z5hvpdXsvn!t@a0=%QYfuDbL!sPU)X%!*$&-KfT>-w+`{GRC?cwG2J;r%EIa6t!<

P%M>Eu^(qz8XcJwSph}%u$ZLg*VR=QALUl=>C2K+*kM{ zuQMGYo4?_jzBQK5zjcyaOIp1-uaK@I{OJ`L4UJa)yJ38j{_3YET|uEi5BPmvuk}z; zgN*rY4jGC9VngCficE@oid%ueGLOF*GXDOOd<0*Nc2fjSHTTym2l^h#tK*d^?B8WP zCDOJDj6}^2@pAyf>}uyn7uzKQbh_ot)Za&ZflT(UK1?0v}tv=SV&o~>t1=v$^5 zj8XTWoqyY4Fs*fQh$iuky=0cU*|qn8ZbNJ2-tuxwD#b6C2yeIDeT0HS!+x7zx(_BR zN-1YUisKEk#Pn&ZzF#ZS>o|kmba*$U)3DYE_E84zgOCCJqNbi_#~-^Inj4<0_B6slwi5++2pg{^V^u zF|$3Gh`YoLwvad&#DEOH>b#Ki)ZaWhkM%3mE3P;1_Z9~XOYs-c6xinsnYp|$Ts#3w z-t1a>K!+dM+0fKtN)e6xO#Dnh;PasGaHMwjzHw_HX1`tB0=N*8l$6bXsU%9xz(9&m z_zK$EZ<}5eXn*R~DbzL>_-BLaj3N<1D=Ko$FQ7pjUv?&5ZL;w*Pgu@p>Y_N^>?S-9 z+u=X}$(FTup~Vapo+Yl{SjrP|m`d|m=O!9>Qb&E4-g)h^y%%u_&D1=fJ5%a82>wD8 z%cJEovJNEH;}(}rrD%HL!q)Rq`GQt?DUzK$Hzv`YoWob9ni8CsM>px} zPU5(HWT!1Nb%cGR_vk=qGP*; z4|TSC;&>m@(cP*n{YgplvgSKSkn|;ClYRD#!mP6;(h)xA-C%=I_ycu}o-YEz3-OY? zVL5U}2s?W9YK^QAY8mqf5`1qojK(^BAT(MR?6`gX)TA*d8p&KuKX(tw{cLmCSiTMh zJ-C2f&okYs+QD|~fh9f+;wS!0ybm+@2N7wnONZQAk-l4zXiw8a1t%%oe5VE1s2%!* z-4wVOC%VWA+!`-2$?%!-0z%?W@uPf-^7n`&Punnqh!beyG!Gm(+iI=~s9j!YySSWQ z6&bagi=Hwr<;Pd*tqNh4yKX$&h2GHvJjo(8X8SsbOW_8kR`1J|@S@Ir_Q@YI#&rt! zruu`g;k&vnZZi@`J$2VhcFxz2)SuL->$!?*^YimN0hc5AzhaI?L7?ZEV^AXePNHe{Uq4>IM#Q)vxZiVLK*V#C|PhcD#38Y1b{wdKBt9(%J$ z(4TCuwe>2K9z--TER}JD-pvWgjAG{Hbm6)y+eQ#<$>PXK|Io+lZ1SM%Z278PpuGQa z-Og+mE1Aq;UK+q{F{Ri;oVWyZZvRqpO+Uu zjK5X6b&j!i+7?xzzN#*PThGYqjCsm=<686cI+UHsjt&t%K_2qpFKV1N_xCdnWlL$w zyYsJB|39fU;7@~`1K(bvl_^7QlvxHo;{PmHQysyTIDE~l1g4M3S8l%(VT>=X5$k<4 zlW=yU4{hxo^8!0bFzDGsUMl=#L^7YvbeAFQU#c{y{stcQOGsCDL{VxxalG5D1_d+8 zK6`UADz9j0-uO*K>dYL|QH%$`SSgDEIIsex6Zq*QJZ;7RhU`fYF<4rj6Id61Nm*%z zyx=yhu{z&EIcx0iv0Fggw|`}FNHG(9y2V)S#5(^M4tRa0-A(kf6J&--Ytsa~kTadK z`7_9T-S^g)e#{*15&1PZ3~SvyOucXx;iip`*qsctUMmq-$)v0X(tw%Y?9)J%y%@Ie z6r^+VJj4GiWtR2*i*IeRzM zrCh5#hvcLgTdq;dT$@|xUnN5+OWbOadHF%x`-PPWjiP+?JzT;Wk5U;Xjw z{X}s-g1i4gGW9Z!^X7mDp85M6OT*x^R?d(&plO?cpE_1Pp<*Q4YhGi_9}hcx%Bth8 z&p@!eVm^?Udlm1F_ix;wAE#v?`lk$fyI|_*t2-R+1o)nvaojn6d#Ilib(>mX7<>s; zPP`|-;A_juNXBbsZ`&#F-@?->^W13XKuh6LKEzWZAt4aYKOG8PLUHFu8@=~C>jeK4 znd%XCwTsPJ>@g>gqYch9>APJ!W~{Podb!7t!0SWjjf#}$OTGXLq%rArCzhl55bvrCk$#q;UWK)!10Q8&PzK}u4MFW;QzP+ zAYT?9Dez^;9uc?|270#4{YWlR8|-@9a;k;p)vNW4*HukO z(SCN-#V+L>r6Jlo?!co}_QYs_u|unoSMA2_&obEpY+vc6T^ z`DV_g!x|9&$E1T1c3ef~_OJQ{mK|7gqUaV+N8pVY`rX8UekpXD8YH=T`GAnhSM$dD z2q-OP^1CkO+FC0N>OL|yCvsT_8Hf@xHV`q3#J_W4ICJb-K$FmM=@q;#C#Wphn;)m? zW34#?kR-b^HCigDDGv1p!78P>EkAGk`;?N&pS$?M9XiQGql}lqhOkZJx#fu7uD%Pe zi+aj!cn)A~Rb{UYaZVq=9}83&k$!o4;ZPTrE~ZuaJQ0c)T5Re^w{zgR&TA~P3A<@M zkP0)*8ZN#LiQ`Vot`KxxDo7;Kr4@I}eUam0-^6_@cQjp1^b`dW6n0vmpr#n9cC566 zcyOnXogS!bO*Ku{X<378G+`GB;sH&TK_O{?Up6BL|0m;Pe1|oT$M_D46c0>b5s{Hz zzq^La!s<`j^j>81%_7u<+ zC6w&M;A1%z)eb$kYgS>voejC3wzuCp)%6wJg!w6`&3YVtJez#*7=dj+vwku66f$J{ z&*+J!c}6Tv6(yJEYtd(@=NDxtL;5-kKEaz{8t`tRsg(qD;*~DK6SaOYvOX_a#893z z?ljjvZ*FC_{n#z>8g5ttto;NqJd9OvJVIZBFNM)lrwE8^Un)x=3hXA9qx?l69>wDx z&-^+2BY$FIGa)G*hbC(&zFzqgJ$ydm`&{^EVtnk3M_GTu0^{MmnkuF1pZL6>MzlLKz%p!NfZ||_Zr7jY*dM8wDT8`(xmqTxIB_@8*nGh~g zrTXy5$1<;`cbwyNT{KI*ld7^n_el(y$H>RMzIXcdwiGx=-8iSEo^SZ!pl@G-l_}M0 zbFBmW#ZTDsQY@q-sP1sZX(zZ%)ab0>>C?^5@*f)K0&G38yz}_k!(#DM4itVCZ5%%+ zvrcSRdT!>f5lBT0SofJWZ%sJNd7Yi*ixZsk=vOFi;>u6y_oIx*5|Pt7Zb!SbQ3kgy znV4?-vwn^sdqd;z%sfM%++5adL)$TF0Siq@_+h9ggLD5uSlTFMh(%57`DVXM&XgntHDU+^{6 zoMUsmclA(IQS!Hm(l|pN*ka;vc-fB>d@|aIaXljH4&jBAWna1SU1WOK{V zr3|Gm<_4u6gsy1NUA@xuh_4Go-mp#?->_jSg)0(NuUWk;FJC2IXxv_C^u#2a97&Fz z-%IkGQLhKEgY)MZZ&4Rjg{G#Lm|AmQEA_~jhwKc%%M(OidOcUZPTFi8?jbv^;Uy#c zuI6dMgl#vAMCqvwb|f%;GFn?7fwD#}cDE6u_j*B%=L2Zyfd}p)2jqFu3#xmT?@&hA z-KgbpIpCneRaopUuCxc;+T;QuSRwu+YO|BQ5Jx4Sc6~#NMaqeNS-b0EoLE#M@6NRbMv!QmsJPmi0=Lfd)n9 z&<}P*NbY0yD&#c3prq23D_5@FJ@MO-p;83z8^=o>pQVAZ=+ku9i*7@BUr&|9Q@C>{ z;7f{FJs-r*$JaB`xoJwJjyA;2iCRneUA*z(UXZCDv#k+P#FkWdzx}3iRqngp8f*be zBo3c}s9uf_Vt%hcPJGu37cOJLXEX4}VLI?m4lcJHcDI0u9~1TGd*NF*o^?q5Z0UM1 z6dj+L=5xakX`viVd7Fw2uiTX@ENW%IDn;d|;!MJN_I1$ZBxxh+8_dASpb@(+4#S;G*3=6{y$Tx+KhGX?xCsp#MbIcWFvLp znkE2xgiV-F%AboP1!J|n<3rmXDNB@yBY`$tcITewk)7$mMPbR=PNj$jaIhaXckpZF zJV1q1q6+Q5BDTOI@qS{U0Zq!wTbzAf^QAqHlvXU??#r01Py*#v=w_d6o>&jpq+E2@ zk#+>qh}!?}R+=v2y`dzR!b{;o?B95x?#A{LR08eZsMk#&uK_nGUkc#!QPa| z;?p^}V1l*JkUS~45x{#WWOw#*d5%=#Vvqs1%w^)Ebb)CYQJY8g&+JXXaj!HXml!bM z<7gI)C-qHh4oc7rN}NA%Xi&OCrD43Wixh72c(tw9#B&LPMI9~F?#_S7SrbP>RRDZpEef$3em!(_`kAg|^PSlxMm6R;qJsQxeqcf=i{7Vuus$kz?t zm=z40Dcwb4&n+NnQa{o8dAJiv2JyAFm0^{1cFIv6kJ;1Jc}%zQJHISCCVgYL-M2BT z=>N=6wputwc+y{OX54jV3Gy6J07G`aEz~5RvS`3-6~BIqmq1QEeRi*KV;79eB?W+W zI|gi@o^@_6Z{1nFNaI${W;pZ^rH5GwlsYd2&Jemu{ks&@oKW^(N`Rn zlp(?NwHPXU1=~A_=&toQ-q>@h-pWeH^Jh}$IfV36sh3U$J8V?*>4>A&O?k!v&yEAc zAq5OXKfe~AvS42+g>8r71_7YfYGS}a53v^;Z}clM+PnB99avIjID>;4FrfL$+Ufem z#Jn861{KdS6;o%lBq?SYKyQZ(g10B;FJvp`H#ffCHbyEY@nbqtli`^^zVYyY)YX^7 zUH+OwL9jc@$=>HHf&VjXIF?%n37sqK+^RhAwx~B@C>-mcNd=zi!bEHHDWQt5oKFyGpr{j`~{1SBZ_V=#tYcL)?}=HBc6h*p+6 zLQ_rhpnBD`nfQh+>nphmkG8CV2|E-Ue<^M;W7iN)_V=S_wTf|GsDp0r2H~dS2m*kz zj=sqn&FH^P6#UV&&6agVc!gplq<2>~aO2kB4`}6CiR!Hk2m1PcNk0s}7W|3@_;iI1 z^aE|WM&x$cVH9|1G+~=5Rq5sU-ty|iOTs@NJu^^@a}Vj|N)~o_=*iyxQ|?bUqv|cx$qLa}x9P+}od(_-mMtZfeVPbV6WqGkHNn* zPVy;eY|O1@E)R!?=P_CoE9HV(-w31_|9nsxn8oNc!#Xp||SF&X5<{X6$3;V4!AMwD7 zSVL+$CfoKyvqFb>B8DqDeX@ze2dSfcHw}uVM1^FZ*436t!0Wl=;{EO2ocu?K|19!G z`#02+M{I7jd)dKzq^<2)@ondbrXY%wuw_X$dVQ$9`R9&KKll!ZdkH7`xMb(2P@eE5 zC=Wy-{}Q+)nxj0$`!#-w^Y%g!xg)t0Q`xzO=aGY^PQ*%Ldt z{l(8kGP>MGX}8wtm_j z7jmk+9W;5xLS8r<6z<P=*f z51`B{4#98mZj_rA<82J7Jzv?_7~fOe>f~t4uQK*jVE*OQscWcumaA$Xh`9`$NV54E zzefX!-u~=yhI!9xwL-VzMLVM^ z`br@s_j^jZA0I%;C+rixTo)jSPm{9+zE3?X=RXol;Z7|^$wT=P$1N-t4wV@a%e(`E z^vJPy{Er`&)U)hTuNmA>h*Gzs%Bc{VS(`di8?}zqD(5dAWY$4j8qB>wq{t%}LOn_` z17m}h4O~j##txs(HK$vcfv;X7O8RGNW_hGnu6(>&hNWedc_V99QbACJ0gp)n1W+E% zcMX~S;#~Mm9u4LN_o-*Ce6V4a4XIA><$8H5Dpw^A#NB;fA^yTT{WIEx43ZBE91I~U zC}ZY+!Zf0j%ZX08Sq_dWc18RsWIGKzT+sd;{v`ZU^MRQDPfD*};>$@sLV$-v%x>Dw zanmXAR!h>Y~0=9+61# z=s|{LMxupDh$y3^V01wcU35l^Hu`-eAxaFR6D4{ldYzFVdWqggq8rRGdjD?TcfH?Q z{;;g&-gEZe=ePI%?Y+-cQCNae8Nb+Om=7 z2CYY2l9ryj@Mf*)Py3uSl!9g$xNscC#U!x7FDF#_Fszw~{U!3Bxbni7U7bP3#KWK1 zqP72{1Gac;as#FnFoHmf!P6z>SBj~Y6{1lxak~(#zm0PD@87|EI}oqyN}?X^^KHJ# zhZWbGp7KY($HU(Gl76>tGB)@j_x5*W#mcQ$@yru>9h;a-@N`k{+11#Fi!Bg8?OD|* zWY??+xYi(9GL)k*TR|Zw#4k|`esPB(V)*mZMu9`olOwbVOpQaQGpWicJU2`{}64+6a z7=j*SUOoE~(ZUB^J^r2?E-XwhlmZvp?(CkvfC?cIevBSXcT{oYAgwbwBysoUiq$X2C9uSr@^n(XTRW z#VuRh&Jb8ER~c6>37?=YPWSB**rkc(8AdMjY86-ld#5~I($D{GKKMr>>v~4g?ewKj zi}$;gfz$);3A>YtsLwkHeNF$aH)(e5Hn_fM3)Ei5qD0DLDQ7Gs?%k+2=mwIbQo8c3 z+7-O+%iHJoIC1$Vxk&xS!)$4vY-40gDn!aSV%cbivUJa(z^mfGN;kYF8An!F-N~zv zXcY$NL587oEwBH35jS6|};M$#=SJ@mxz9e&ZPLwY@^FL*vb`({xs)YTFQVgRc2-f!)1DS64@L15iPf%_Ylxu#Gl{~7GGOC$Y^ag?| zvwLWBpPw&(hoWDA?Y&o4{I`}(Vx^nR*;GxLCY4+E3l1Wy4PrYk{b2H zb8GN0^ZKF33*@_mkcCv}pRQga0y>K3I)$faj@-^$5PjCuQ1u&vJVanl$iTyw*UzMd zW>x6{zl#@4J6`ig-hx-9r8>>mY!7_8dcw}wDeh{TE>&ZAI=m$=sd+?!^1*S3&lm$I zLw&=)l$yy^!iUv|A-4=!WN|lyeZq&8v#tAs10+lDs|aj#oI-ufco+-k1@2MtlMXZ| zjK8yod96Z5=m=zm`l>>>2LPV0lX|Ks)#uXmxtAV8ACqk&{g4)WcNbx0?a%NURL7X% zI)}!OMgBu@YvydYx83j@+U*Ad6VZO&2R-YH!rL^WK!w~0r=c30D^OvsjrxAY>_5w> zb>{Kw{7n6Kn_A|F$|HR5c3oAaN)v22RKk`P?Am9YGf^e^nWfVJ|ZfWs30z9SfQ})wxwgUH(tLSf+oYv zERkm&M~gj`-Ymg@q4mHXhK4XeMV39qbUVr#UN?xxfB3tjFXNYN>v$Qc|N9;!8*>`A z{M!s69$zrsLfT88BYjOMNpIYrjK0?p!DI0DSP}{)K1a&WCDq1JB}pPm@+Q982XOP4 z6s)%Qk4?AAbYcdgMs7#hr}$A+gCZ6fpS;c!rU~QqXawx;smjk{y)x|w)chespfO4E zu_g<@N-QQ<#Z5?vvEM6m_o^<}RbDzV6@siIx=>e)ao-ABa<3m@c9LshbpFMel(1wx zEWeMZllc9_Z2)dvvYSZW#!!~;do=p@rj4CJ=+7~Np4|0hp%NHD^#mgO_W-G{%ds)* zashPyB9~>Ttb^QyLPDMl1k1!{{rjRT;Ad=H?Q&49%gHCpvb=Q*?zXR#iahAf~P5puS^_z!9VwXyJFI=#fjEY@Y z|2QtL<+D`MHqkEfp#92;!PA0gcFOwpT@unj!=(?VwpFf12P$3O8 zIN#tF2V&!Tq^n{B`hzSFt=qKS`>~&SASrShc`Xe;em&b|!%jTPXI?0KHg3Iru{XXX zPB7dLGy8e;S5WY0Pj7203zF6C9JM1YL;2O)Otdydc3-OQsHv&id8ozKook!O zW1@2$H|~L_-3Vvw30uz{(9MhHP;K`5N4n;93|eFt%lSy>-fCP;$~pg=8GU|JmnVU_ z)_3PcGYu=~U&ct`Qa=qth*^*nznKWYrq|f{!`vU;2H~3rH?F0){nd?Zh3c40Ek)EM zwF$M)ck?Qvg6T@_+J^(-E+1U&D=twF&OfMqK@-<)ic<8+t%$69M>3B|k#KlLn!I7U z+5vP?m@Ih3oKlh_O}X4=;6h`DLeqI%HnhGgbj$e!U(4*eENy8T#}y%ap8yO@(kI~v zkgP#rE{gr?TS@T!SAMueOM4Ct>XMB#PI0Gn+IL0DR_Si@6c(=t^Bsk{wp~iB6RnY| zrc@p4YA@H1{dp%`LJ}utDx9}$Lp6O$m8^*;1#bG*>MCpMsPVT~k=IL7qN-bO{>C;x zc1(DD75Rx}+z(bDS>_b?5y%`$s^~!I7s}94QFGy3Z;P{MTL*uQG_hEX+WM<2woW(! zPQdwg!&Hf1@-F44DXpjG>UYXq`cLD}=TJA#OGoLrV)g0jJB_L%NYXtk<^XD4umIJ<(>8QiFQGVK#Cd71(0i_giOVefPz^zULy3S)cNUI;QcBG96xfU{}1mg(hzPd>s;FZM~cd)rq}y^zlZDpbDa26vSp z>f>X?wH--GsyW74*_Hxkjetk3&~hqd>eRUS)2W_YJs#HFrQlF_OI1?YGP1dBdj0z zjLrO{xb=L;-@3qmDN>5ot|Vx`3zYs!NCR3FE>X8HWF1L07*EzyE68j*E)(cX8=2xX z!L2NmRSGE{a5{~j__3%EPFA`qFq{Ss1O2%D-|-}4@54HVpS0ul1905=FYyLq)A@&? zDJwhmJN7K+f9g=^?EpV7uj)#^CcwAfPV;W|xk2fGj1eyO`%y-O;8&?n`ehMcnS+>) zf|IFo226(0Dx5X7O?h^Nkv3V~9@+#YI6R%4m&X0neBq0EFYVXni`w{>yM2+^#M_`s zI5-fGgl-L{sC(dX9zCCH7lyZVrBP=6fA-+O?_c2R6rSk7@@MX&FSPFX z|Iq!Zquu=iUfnop5&pk`St1rC1clJpW28B4{`bKf%>Hy6r46W?upeu&^i z?DT%p(MfV!Wu13ACM8;v*cR3=%J{imtePB%ct+=?8_f+bl%@N;I$z%}cV->Y)sS@5 zL7PA^hMCRo5^d!S?%mp~bu(NOsoatK1K*et)AU1TeNxB21VV6^<$8nORd-~V$K%5P zqD;~+qEkf1bN|`(b`vo_!q%?n+3DGr29~?$rJRV9A7A475N{tI#s@C|(F-?eiym0^ zP;r089R_M;(|R_?O;+=6)iuNw`%?CgRnMJGtzP(2SZdB|ILCJYeAz*6S|Vcqpsj(> zc>i$qauWco!vN=XS9P{yN-61fVJG_hAZ`KVWRI#!sKe*7Jl}wBds|r3H~b zpCoU~PBVPE@@E4vs8R0l{fe~m;~0)t11ft@*i$R+J1<>TMq>U7i*0b7>_YwFVdPWw z5b$_7R3#k@Z=_^EAHSvNT%FN%Qam#{ED9-|o>KH*Dp}l0I%pSlQtkRONNqp73Y;Go z17zIWKLt(RN9V_b%>h)q$kiii-88Y`BOK?rTl47Awi@L8&$6oDPMl>y$EfYrxuJN4 zYxUloaC_77bcmDWuXpuJajOh90_R7uU;1XWfwsNLv_=otisA>Jw%SU()&0Y2GC5~p zaXB^R?lhTg8w5mZ?-38&*0HC4Zvl&yUvOQ8)S;FwU#N@F2iRM1yWb>W) zB!u!k(o)^Q{Eqtm>5MM( zAo~ZA=kRFi40pzdn$Py{SXMH(ZAT&0xM!Pkf=}2qHmi`G!h>6w;0)ni z7{Z{vX)y7F^9gitm%CI>?g#L>mkL@?s|R(QJ>vpZ|6#kt{Evgtx6fD9-2>`i!>oT1S}2XH9~TCvP~woMvir6 z-8Y)k0ROY3G(r$ZmPmj=^6{925;N~tSZ4541y8tz{H%AW*#GX3ZV1hIk|o2~IY5QI zGqYWGkb$WD0pq`w!5(&u;R)aNF?Krrpl{2X?yx`Z*GmxA#q4e`(4VK6nAC2B zj3NO?0O_rVs{v+nk`j)GH@Zch&mOOb%=#|cNBI(L%{h0jC% zNrviXwy_)&PKN)?Q-s?DWz4%H&RPO!XBrQHFQwD6rU`Ci?dwG6BW0PH5mztp4u5NnRHaS5X-32h&WbJ(m^m0U=sr>9~Nsk@3poyJ79n58#Ak}N@g zr2X$<%_>DOUPZUwU*D;$re*iuxMt&E^nTcJs}wEIQkOd~u1{0ub!um-)!nn7YiGBB zx84e+aF7`rFa_P5c)L4?L;s&qU6SoAzQJoi(HuBz`BIT)eEysG$;sGzw4`lXX_-7O z9KjC|zi`?T4mOG4|EMGfS{|~Pyz%UuNU2{8wZ^2qQ<-0$MgK-O0o#gGcC)pB<+*s-e>NC*FQ$d>yAE5mZiE5O@W`devYy@hF>n;N6PyO=u5tGB7|y=tY`D+{;0k* zAKwo>mgCBOY`|=|@;=$2+&yYWD@#|+m#T_GWH*kiBm3Sqb!<&JU&srfTAY~=zSKRa zFth#V$hj|#^7hsh*5WYg;qcWkVF^EE@~`JHe{BetA~*3eH0l#rlW-n>)NyGwwBo^E zyjg3YwMsN;a#>n7k%$gHKRi5?IqdO`lR0l~v$XykFa3M?_UVxOb=q~*2+D1=EL>L- zb+T+c=vCQz_vp8}6L31z-*dk1bBjwRg_T^ZJ$%}DK30-^8m-=c`h$B}nj$;}m#q5> zd&kwZ5b0%f>d}U7@7d=4#j#%HgXl4bvvAZc+?i8aYAP5IN^gHBSS8K+)%uN?kd)8= z#UC|?h~n-N2cF5ejorbK9}M^VMSGG;`X02O&$2A1xxWDBEv?lXPt8Bo9zQ_{`Y#+t zciL_e*jY%c>k9mOatj9UN9=Nwh4v^zZ62yxnR3)iNwm@cJl~^)1 z5yButnJzQh7`1jZJZigFt;BSgBr*tR3q>&sSK%~QuYHpH$+}{BHWt^l-Pu?9HccG4 zu6^agg>OK0@cj8ozveD!4Ch^B%w8(mwzgZQ@6b39V!rF0sWXM(M!gu8D^5K@2CFHI z`alwkuQo)COe9sDmXRMyky*`IlAr9QNxN;rY97v>?U@@mNbGO;7z10aX^oEE0ud#I zolWAt(Ic;(oQo!$W(ti9>ib5g8Tp(|mCZF5p944Ao$}+s-9)Ra+7od@>k%?i%e~jP z=dCEkp7h4vd#&Al9Kdn=E$$HwGo!Py2uStlOU3lF92T{-#^SSnQ9;74MKn#`NMC~~ z@h10|<&);FhNfy?lf^Z72^~T$$$Kqu7l(d`pU3O)Cr`UH4;#SkdNhyQoF4kF^D0UK zSQGaDGR7KsGi$^kAxy5>-nLRj6zZqV7h}cpFraVw2Rr!Y)5(%{#fBqaNN1A{;&9AS zal*a^ajsuH;^C7$5xy~MS6DY`XX4k396Y{tAauFivE;Nh?6ttw$vdY*EA)a5Hw znUghUG7Q$3v&t4Yiy2Z=<0B+zBrO4iVB4dI)A~Q3qbY6;dVIE_E4TQLZI}66KgA-LZskhhwbA@8J;{E$@Cy8fTuBlgitZi8x4bvg`}L1`4T&JCU)=7y*F+0REv zj~rSVK?TJv{f6bvJvW-K84&xW%djWnC;jIxo?~%G*}0`(LP<8VV|?Ybs?RsN-9{x> z>$ykJ8;+^s=!m*KI^jzv}|kYNI0LnuGV6BX&PfD;OgA zun^u^7mAN~{;4zXpD~^l=S@;B_Te--?3wBHbMU!gfMEqDY=LmC=yvjS(do)iuand_ zm+rH^%YEy&eGXa8a~%tvAknU_#pG$+Nnra_Z^+Sgc!KXlS<1ZB>h$WlucMhqF@8B{ z#5^^Ti%K1Tj8bkus~G!fSVy*-)NFS8xh#FmH>u4V7;mBkd|DZ-Xje-iNzQ+WfCGD^ zB@&6Q+k2xK*nZHo{L$jYLV#bPiSLWXlasQEz>$4_ht-lCu|?K-xO93M%GIIz-ty8^ z1jz_TN*(zWHy#rv;4!U0QAaPs z+?-Y$VLxso+WY_^W@^V|^_u2hCH%FjmRp);wVP0>NbcOU%kj%ueKRlf{HjUfv{jL# zz^{Y(;H4KRGW|w!mR5gaV?otPY`X#V)S<%z(XqZ+Y~%BVK|AZUZ_ys%O>g7kN(}s+ z$;3>cFkHCZE|*lGDmh~Kplmy#tz;pnc&S*uI-0n0Z1Twc?J^(z48N$_Xur5CF>4|l zJzZHJVG?DLu~mNkf#CXNy|S{4i_p6OHfj!&y8HPUOrqz|{rQIaScB-+b^G~1mJzh- zL*t54kCT|Qk#CcP29@5rJz@fPorU3?MsWoYJwE>Qj9gt9i#9L!7-iL}sNC#B=qqa` zyPgdfsu#GAt(Z;qZH=vW=w>q*v^SlfY^9`xU|sp2Ahcw1ACPN--FxB}s7+tVN^kt8 zE4q>=#*TbDtC5Y^XNc%Pfz{#P3JltJN9>wUG)<^RHEd&CcEfT?{plnyLCU@X!#E&R zbg|)@_V8=wPKNx5Mw1$51CzQ^uR*WyiL#^nENUh$#%pa;anjp14O0LYr=RH}j#|fq zYzyIf*EON#BvC;~a7b?OS_H2|agrL5-u^h*2MIR{AAPx@mL1b*{)IYepCil2Is#pg zsk2rnD(L@~b3@XLqRR5bDuNYZtqg3Vj<;)2xO>$e@Y>q_v%sb%+=%_KnpCxiX{icf zWI0)UG#|61L-1*#7pI91x85na_6jBJI3wzC0-vqSiv)Xcy=_WUzA`ueYc|`Qgjai-S}Aa=@gS;r5*Qk zjy`w&A&nFl7(8VrA=&kR~JT@SoFU_TA10JpOm7G4NTTZt=+-B_4Xftffw-x+!B!ZKCvO`$N zRj)BH+pTYBUryFm&$YA7_j~qBP_JRrOE{P0K-m1<6|P3} z5TD!s9$}6UuJzpIKoj|@2t5c5Gaxj~yO#|{{qz051e?}U!2{|#oE_hLw zwg#tR=+Za)UaJ%Hoe?EC;6CNu(tfG`I;xjlEy=T&D#RV8CS`e&jMSgCAB9VspS*`u zrBqZ+)cegIp7#2%v~;awRgVtyU01cG2gNIn@zZ^r z)^&%02JvjmD#=Z9Dt%7{#Y^9vDe|9t4^dLM9yH+GLkeU4)zvk+{do_Rbe|TI@bW6% zr`zQ-MuDOT-AKAsecz%7Y$@5<$zS4&NviQW7FwE_$3u&k!ABI<5{t^BAGD69n8Rl$ zJW2E2oItJRZYMK4=2!jQs7xO@3H@>});WDW`L$pFC785UX+Sh$YdV~Hq`&4NK9UT@ zgdx+yp*M=H>>Z5Xf%#dctYJXRv|ZqZ+l&lMg15|`#x8RbVt_eA`op}HP8Jsq+P|(-CN2lDEmV%JExPh zkPdK|Xzul+e7fEpeH=A}LPXd~)=bn*OluX=3VLu@1887!=`6dc$#J){*)Nh-y6Wf$ z=ng%?O}yLQxos|!Lp(XGy>oBy3>`7N{?$=Zog@un9)|&q=YB| z%W`>(i3_(B%ZAr&9wT_^r}FO0nj1bTS9IuX=g>czjVO`a9^PU}lkgaf8TUFl2t(IM zAG~Quy8P%WG|jmdW$DkR!}dzX4~eQeA)|DT6B6xQYuif2{-!i{?SwPrjZ{XrkQZ+R zYt4J6-M-3tMH6zNV6xY5k45S^-Uy6zCI@OQE^Dj;(grI8xj{pBc#w@ zyz7r%pC&dCg-3nrHI$$)%t7P>K4$m)FzA` zmI!*?;2MCp*sL&UwGV&rp!H)%wD1>1=N6Zq@DZk*rzZg?uN+o_vtGI{%17D@Fmg<+4PN5u%t9kg$ zsv}hs-)9xry=8HLjL{ zbqgVOW+-cRSe=gvDqn!yXnjcNjkZ>cIPFkgqmHw6$#|-ldpY9aSucLirDFGcvZU*m zaqs5gDK)OT!N#TTu$rQ3j^uc2N2V#Ygrh|U`u&2#KX3uUm>*yF^R63`C;@`Rbp2cP zSt`1d>o@1$&c;D;aBjjUR zt8OR}97%p_w*&bpf6C7_mTeA(rOr?27w>8;o7+qZG5MHZcwwJHU!|XIdBy2cUdiG7 zf%|5zYj=nd6q8uYB-8wOrZ2tDQqTEI-Dxje5*YS%OHXn1e^T?Sew*+UJv%8m&fLKH z=FEG!eA;XcQ|6kp5H2dY5H$l9N+P`bQT6 z>PA0M>$@&VD-`GW?mS!Gvd%>u-xYw^`=2iTL~*QEUJxLgoPKu?Gx2MzLu=col!#1j z0DZ9awOcM#e)GlMvx#eWUY8piN(_@EH-hEd=MV_U1c8o;_*PQ_Xjw$_)Vg|FNnFkSF0w4E&o)8_RxK4;w}2h% zeDD1=V=0}|@G_fPYElnW!^ZA!u#jM7sIHkKqm2~7gdh!?%W>d1S{a_$L}Wp zzG}_6_IPT>aYWy_Th*xZo8%<%0w^{SdpAc z^WWPvB(yW=s}@&i>)3KCzqVG^VC?rL?L;KM$vlijn>6^&YGxnb@|PGpxpsW}BYcm5 zuB_e>Yk7Z~vtk}+mg;Yj1CpW7sIuz#2b_#Wrbsza2`8wkXka5Qrq`okyL3d~p++}p zX56v3I;pUDZ)D7u3+EJQb@&lbjjjs00x^vGabMtqbkxs@z14*Ux>)r)=`zEyBkpJa zQb`;PwK5)6^!KJ!6k=%IBa{#t?h7<^nunWDVCak+5mIz1<~XCuCLw9rBiU~b_*9+)Ku z?>v7+EBvs9_W7bnL~;JgX9=@X>Ax8IRX;#>W{(siUoNP^CvfR*P*xs6NZV;=?`Cgm zV`k6f+rp$s!6RFjRA4^9V**x`eJi}Ky|F4@BS3KPeg+Qqj+Ces@08OQi)XQQDexH~ zd#ttjAPj_=iXD9tQu%nA{86wGo1b1m^#-C=TT z##qo&QUBjk%Y_B5m*};#js{G;PJIi~LjBKNo-m~t?Unxf8VRx&_g1r?DJkmU7|c^- z?o4pf?%WN(h19Oo-%NLWNBGb%d94@-N|9-{V8ZMWpV!Mjte!;AlzEPA4AiJ!TMg)W z85;oHxc-As#$;WkKdaF-2}$q^_Um<~T{iHQeE+}l&)mOs?XwU;hPYO4*A>4(_^JGs zaPFL)Sk=k-Him_9(2CJ&Sajpvef@iy=^XzIvdsLr#lk9em)rXYqlC+>LVQb}yG&fX zXY~SgV{fGUL@UYtv@j@Exvh=4?wxPnzMpY7R1u%3c^<-k7^9h)2o6nUr(3M zs2!h8|8zC>8li=aIb-Z%A|}!q#_KRv#`+)G z$ZWtR+Yk4A&*j-R=sJnsRvDgHIlBLOzQJ|oeYSi{izSvsoc!xwC|Ue2z$3Tz{_ks? z#`{_EHb>sV&dXhc`)wB9icM6k%bI$=$?H~un%40aJ?1g#!u+zmK;^TAB< zb?0pv7ioxkTmV!MSkI~V4i3NajEz&ejZ&Pn7B81`^^r==&yWW%_YKd`36?!=-DtgUf*rFXSx-FIuKyPy$7m zuvcfSQz+uSr)<&@!8`>`3Tz^|{!cWdz$k&0p>yEFcLO0$+IQiRmX9TCaO-Hd1;Rh+ zmkT_s`Fti`Sl?ypmEZaw;WQ=t+nbE(Jq!4!;2z)EHX+3~>@7gvP&LU*l6F9*$h>;S za$Z@GQ`LDLyUf4C@~$|us+yTFne&M)WMul=09p_QNZ}>*WPzVMApP(1*+l9ojfzBCu;rrm zBl8_0=*z<-V}T+Aif*C zW4$m5FUWw4rN5hCHGfSRs9%4n#p!huVNX{qr?9r7^O=~NK4L*DLjO}PbQCuicZI<7 zNjqs*;%-KyNvp^aCZkW2CM~gbiSJ|+#Hmpc-DHuoV%YXpm*jAYjL4}Ho3q$zCG8L zts>2cuX8`Je^_```OJ4AbYS7PYeK+t(-z<~U% z2dVD%^8)d`J6?*>Z?)#=4l}Yc&YoQ!pK-yBsR}<-yN2c&hip>S)Wc z0ayZo7}|3hNAU}=K^i&eG&BIZF`X<}Z9GtY>>0DSV-AC3?-BL2ovkGwyR*?MT&r1q7XHHvHd61b`Xq&7 z@Jjd;#Xn{7*EHw1YumEkH)ed6i#J>|t@q*b9s4#sUHlDt61n>Ev_vuSMZ`dRDeDP#CNJ64nPP8T#EO z&}5kjYxK!&c_{B~0(zmfmc@X=6W&1-M895Ow>IY49MB}eG`=G!$(jV73L7uv5myE5 zFhkI$UN{vg^*}_eYwfL>x#?uG+EYFchNpkWW{}k2Y(^0BplZb+EK(2%fquKnX#Zn% zs66xm0{+JM6?T;W)ywY2-}FVBtA2B`0e#*7{K$N`i)qB(5P0a&It`D~OVz!!*R8k0 z%L);hMs(e|of~v-2theekl)CXgqEZz1l^mq)@V=Zu!V(;3nPu;|t*>Egi53U=Z=Xui%n7pSkGo1ZsP8hJh#A$hc}(ff{uj?aVk zk8kJSBr62_K39{TsVkg9wQ^&T-0(X_VtQDdyk^9&&9D~|h0t0O$Mv!^DBl`F6CSl` ztCtaL!u1d!2lMSgs4^EJie^Sqnd9h~z95dbC7oy0T;h}Loh0c{BDjvUxt{LwPw@Nc zPcMPyMGwN@W!PzbD6+c2@a6%pn~@Pufv+>(FjrGgE+*;;bQTUKgurwiVJ=}Q zG_MZ17q+lh2xtpWq@&zS4OU zfLAR8Fo#mFuuSKStPWz z3$YdGKE|xCtRbM~$Cwwh;uacW_3VM!y?9s)qKj4|oI7Hb|E=Ag70o{6UCQipsd(on zU!Ly#oe*XW5?b7_Ygi!iN7@7A}VEp0ktNS<7y8goeiU}rJ@e`|el&qyEh>7FG#Lpqs+Y@xP{JY?YLL!Qo zN$yw?>GFF7+Tdn%%dckECJmUPEh>{7tU?afc9hMz2i+j5Nz^xzfnb!!p34qx;^m`4m`%x_K{6Jk@g)c2vG6im+o*qB7S44E8`(PHj(Ozj9c^) zs;aUnDym>&uN#SxaoS_q8hjS1C%uv@1gBA?&csujU4}qG6p&wUEYS1#e0# z#o|y7Y-T_C=?a*7+=T)D@3ai=TPDt0VKPh;a&~$p^m3g_w(zfk=NT>8M zW)P_Z%Tn?~8d{f`Ho-*rPA_}Zvri_Nk|17x)6ef3pgbQDEpjd$GRwto<9TSd)3cMf zC6u3wFg~tTDCbf4_+jQP>p|e*EEloFnzX_+o=XER#NCf=J8X)2s}k;XWj>!Bhezo0 znW`<|S-^>!(Y@NfJ5lQ^l72}0$E4QU=*&j4f+y^7J4W}vvr~+$?JQOcTb^(z_v9lU zc4}AVBv+tdf5Rf>$ZA14N?dz_;cv%F<8y49FQE6gUPS6xBb{8l(>D9}SRA7=D#vSkZ?b`Pf);)dr$`gmJs!iOl#Kx4jElZ7;2!#YU6m->ydCGb2sF zu5yvd^^vIc-gKw5i12aGV5#fZkQJTL69Dbsy41|~=a(`uWzV4IWv}%bICqu_fyY{~ zQs&n$N?XdSVx)?M1iGH=mK?9cGa8Gci+ZppBae&|6pF1x(mrMQg$Gmcnf5neeN*rK*|>DkZW%S(OGB)GXfo5$XB#%T^u-nLq?e8 znqqZ?Gb@9M2+Hb>;@xqDF^l)OvVRa>swuMp$8zs)>V4~En0O5?RIL85EK@q}YsJL% zQJ)T;54maZb$q+oQFFJbNE!AMg|%H&%Sw8$za$rJ4bLRW;4d($4Z{`V-nqZMV^fM% zpVGix05LJ2S@wO?CeO*V@zH@KGV%=ozA-v z#y70@q~Y_&0($|~3HI-q*_kPS*wZ3FUp9UL6ddfPiO|hQQA{a|cQi~7MTZpvCmEzT zG!(A4BL_KS7`*m`=JY4MI)zFEKU3l44f%6R7A(UsGxGwNrTR1=LLBt@yFoj0_cG8D z^h|0=MPtDQyAUeqB9rOyO?dfe#KZSKmTkIjyCfvpI~abzgjRC6ylNm|x%R{k-TwWOBVC!~ik13in%GG0 zFyv$@j=c<^9MyXwl)riXnq-$F!Nj>EcTlS~8!T9g;%A!?0jLjEtqJ3x#H|u?2L_La zWeiM_gHh@7h3+1w^(5`>f%%(CeIkc@yn2WRhEt2Phi>?*4T$>5|} zN=`M2pjwVy8Hmcm&(e)~+fRI{ZO6l;geM{g(BSIWA)xPC*b->Dl4I_#pIie2_TLAu z+Ih7gglI1Q;iZv4GZpzdABX+3`1p$sKl9R|ga}jztOV(HE*!|c7&mc;R+G5X&{-zm zOiQs1?btCGv?27W-3G++RE}{4Vdfq$au&r<0*(s0G*xix-R~@@eedlg=7*GR87C_o z?IS|L+Au>84(8K*^?!&yK;du3(q{BI_zvM zWI3Gj09W4|srIm^VQu(*#|g9~@GcpY)_$oySz)bXT#8`7P}CiKEC&W{8o^M`&fqK~ zI{v6BbZgw7fZNh#q5ERxrA&e}&iy}37jn(r=2dqiPsMTA z&ZP+WP6D%(ZPlBbYrcPqQy9v@!5=@Bz$BLlo~#W#3`SWS*7!YTW}$tZqj-(xk2lhT ztA@-Yvtb5QV!-6<2eB|u9(v3ZtZ?KL{`x*Ib^R<|;1TA9KixZlhf-O1Sk5&N-`g%z z(cgBU{7k>|-fAsZWME55&T#FAysb{b!lDM=xUzCxvGAuab~J4B_0(8C3;@r&frfys z6c~>wS+`8g^AuhzC!^>V-7kgkKbn1|-;TV0%#xi%(N1?ZUg=0W4 z&A!+m@?Mx^#_bd@q{V@p;dQ?+&0Eyw_Olws>PM9|gYyKe1PwHooOd$3T*WzLRf_$3 zaUZf}oGHDBn1lP9QowcCtNa2_9lJ)^B&$N!i;a--CvZK&I*f0??sR!bwoL>xQKZNn zMCns{0`)!)iy7tr{g#YsbvHEuV8U_6xfoe2noDwW1p#HslmOFHyjTn#R?go-I1T!~V#O+*3@g4s|OI*6`or2hhWPd;Axb*{xNDUqKFdCnWGDJXyP; znrp8J19s2*+&tKC8gzx=tMp^)^*t$3DrALT?dn_1mnX-9Q5wM43Y&d4>>mhLqm-h@nN{j=&4N+y#$E?XGRut@f7o z;^lqi9QOgX?EbkOHY~cBY*%WsFog9(Hj6R;`P}rVH1oB#Nr5(FGZ8U}#Oi(-+zNZK zYwXP^8-RypMp@)~VoB1a7O;WL`;W&@F<>_vX8iWB`BHRy8>kP{mzO|vetOsywA1C} zd>CxNM!;^pt-tzky$%_}B9vi~2Hk!-x%KZ1(9YbdYu_9a=!GYJ4)$e2R2supJEI>p z5nC~GUJoax?TkiI7KTQ5OW)*8!-Yg#L-Q)h9b$2f^uqi%VQgL-jC6GbY$Z61rJ>*f zo%F{RtB!>!FdH{OH{&)(E9*?2q^1ZDcjD7Nsp_M<~;Sq)hPY(7Bk4G-X<=K$Y zStLtF<^}xy#>d-XHm{B$AqBuOWUGTfL;O$HA1a2W1;e-tfBnao6LZwkLgb1RxhTz> z3=160Q4u_H|#|av>X7@_b3pNlb(D_;uG*${|sYK}M5@%b7GVjv>>*$4jjBjj#b~}x7 zM6*BAu+W<8kIhr;--!1@;R^`RU2yt>N*{<+WgOCyO|3E~>nBOL z%Qa63hOK4(5zEf@lnI zAo((h>hwTeFWn&CTU|n;sjbBnOa)oB^ksJ;hi*ZmgJAkj4!+6h2Quv!PX0z{;rb9O z7BvIEn589(c#E7?e;|+eiiv{LYNl=XH^&5|1(bA={G|QyP={z3LNq`Gp;} zYt>!*cD$K5Sm>$<{n?hX|3eEMS;B2fnCC2j<*f?a&as&UJv z3Fg8L_9a{}{rlpZu1-N;l^uC$a4?OWknyUJ5q;t8H{BVHZf@!WxTh&KAhJz~bd4>x zj;{H&yQN;wd;~3|F{?Dtuo2k)JV@np=dfuzE`>_EiHA}77~@veQYjjxb)1i701%c4$b6pb?zdU%q=HXs&R&))ARtw=}o6EyUvTC7RPGO*^ z(>@DmUSYvKyCuNL7rD?d3mc8E6+AZt!W(;TeeNi^DVYQhS8WQ8;hdADAD-es;=S|Z z@vuEJ#t{YCn0*M{gdg%fLO2SLcWZ)`RP6H}fz5^&itaDoisHI}xkIO>##z4*2TkU# z;uAd%4z_hNPO8~j@)@#wCs;x4AqDmSGI$2EW6LdBqPm}Q*2 z%QKujh>zv&tsKacHy>RMn}dt&gpM)6LBnX-AT-yyQG`6YDRccW7})T8@)-|H_^aa= ze?_f|&R%)v^1@43>p3C%&;A@M7NxoW&0O8sk~Yb+q;Wp?PB z(o0$OCHlUe)gaX@i2R4_wkgJC7BvOW?TgJ#<^^|j=J9yGR-?ln_u>L}ARy9iVPF$Z zvvhPIcYcu!pj&;yU-%&<>lxr13|xiVy`5U|v>;o2;ZEqCtR48Rv*7<=!E2C^|8lur zB+*bDoBoa9Q}^IvGc_*%+|CjjK3JFt&w^^a12n#M2VbG<{53eJHeBe1Q`$R{lV|i# z(UtXZzXuKiX9QzlBuK1Wr}+{~9R%6hmWZN20-FHFyTB~=3Bs$WxjAHzQ@APk$_SdP zVDB#~Xuh8bQccC2B)P9Y1Ht_QH5j{Pz4N=*>Bd7Q1f59q+Z-Hn{4(pU(wA?uYiLrq z(g>?A5pW5YbuL~HVH|cW#SU1?+ArvTA!ztV*3;kRR%#k$3S5?O_vkr=W}5v8BbgNg z=8C1*(ZLNdsPXy136z6(SDvEzeO-it($uKcF3hfqk6n|D+ksG>WH1&7X6KVnF~ebC z|Jb*8QEc2kkHA%Zzb?3aQ=YB*ARf0W7 z@k?C=qnoirX-LV@PT9Xqdysr$3&*{Qq_K}G+kE;(Yp0kjjhw&wF#bkMf7{POU0;4- z-bwG^YWb^u$VMs@+E9_#0}Ca>i9g^pazbtq$7~a>JUoMlIVQQKvW1E)glp|Bzy99P zg_ZIIz)O~Kd_Gv)Y%k#hwqrN!2DzhYNJI9!89Sg?SP7+M6SB`8gdqFAVk2?3N6s(=s`P>PBmEwo!vG;~506p$uO zh%}XMK?zMDbPx~-y(bVloE4sTpP%3P&KP@-?eK@?W|g_tnrqH$UUM!9_mEq2nRCja z-YWW<1EfLZ!Fvy(*9ynkbM~=!mnw3WxYM#OJdKmO41+9;FRfc5FxXfGd0Li^Hjw>-`ov{&GK86I1%I)}t6cSqoXa zcbk|3aordcZ4O0u$UnI*xLV|nN8S;ElSOV?3@!u_@7HZ05LH0`gy!tatwaj+x})7f zpqg{vJIYJ>O$TtJLRmNg*Ghb$WI?979&iIv6&I)~PI7ZZB_38fXf^9HREf~6mE&Uw zn@EI9{a)q`VoGV9%X3RPfHf=JLI{^l^{m+Xt_M{qAkH{`Co=*y*2B3Z6%I)LW>491uhXzhJB|}2OxLrwtnNZ`7zE3nMV2C zpT5|&m!QKd@?gnb{T$od$%i)$UlxmYe`YSj{ORAv;4t%CZu5*^4vK-dcSv1dMcT`}alSaakc*A0KSu zoJ>K4pAl3-{ViD)RIp|mcB_3bt|Qfk?}5gBTUptPlj}kUqgdC1X9|4(LCEE6{C5)z z?0Jq)94^I~DK_QIpATev%RZ$RovWs!fpx*YE|?z|+$?8yOqi5g;e-9X(6bb#;1=<- zWl}!n$^b0qx{pGh;Nr4SiAjk@4%c-~EMqNv0xFv9KrR~%Qx^e798|p?`f&j8Po6IA z0$Yv#B_|KbyT1#|OEpd#nv*A`?mt@F%(pCu=zvfg0O2U(+f7Ei80)oFAAm_&EdF!R zF|F?GlzhtL0i3T3;&epO)YnB~DR934;*&G=R#IptB-CA;!%cyjbT|q<&2q@0+d}$7 zqJ!i^m*|-z#+8$F*OkSHxiQ$<6cQn4l4P71Xp(L!jx`Ci9%I2;7a=OFRy4c_<8P?rTns2ir$#ds}_0H%ds`Zl^u5)~@T6Q8`@(5Ud8ALiJC9Nf#F5#&VsEC8Af zglYaeb%e11w<7bAN_h>*)IY*vs-P$AL-x%B_?PW71!|Du-ru{`_@w^Xy#*rEO(i!p>FOx>3GHsGW=TLH$-9Jqx^Oz>PPxQ>7 zhI)D((v16mZSXch0rL(*jh0CH>xe(@X%2>(dHgEcz6qtLCo+rnSS<_T+>RNmf7H74 zuH7~y|KM1>t0C1rO1Vs+)vTuNgXfQb*Fi)WCWSQN?%e9m{|(_zB^;{o2wCi_XzYakBiDnM#+_|Jw^1$vTn(!AoQ`}(4Y z)bZM5S$bqA@oV+oC$!nu0fhsJPZ*oJZQ&Z8r&(8I&UZBWIaBwGdo@x@w&8RtNKb~G zT}&G`5P&N|9U$_|=I=#bUb^Gyf(Gt>+7#8hh!#c_Nx!&OiHtWHT|F?2=vEYhF=C7KY8}DzFv@qgM)C7p8Xy+z(Tc_?U{o?@V@Ok$G$ECXH)ETB&?_fY}WFdcAVsU181`+rJ zdpo{;ubh1=fhTMuhI?;C%aXH_Bp)dEQ)qd0p$MkTWF zf>k@&+O;6HmVAOVC3Fx!0AsH})@F}9oef17I89oKkuE!L@a&RTyFPU>nXmO+6W9aS z{#Dr8$F|L9#n2a^ z4np#8gn$|CtdN1z04!{l^TZ6#nf0{iN<6Gg;6s1`_N?V4d9a{8I;w(ZAymgZ)+wrg z2IQu-K!Zd!_a=-(E+$5Yp7i3 zkg~q^47!J-MFUiIZs*pmcnxD^p=>C*!{ z_?G!O($VtMZZR9UU!wsd+Ons#U-BC*9$11qC9JA_!dg2xf=F5tRW(U`*eVO3R@89( z=r{X&NufDM_m|heI(3$SyK%4+xPQw+5Wo@j21uzk6h*mI$01UqMqxANxc4x9Y6PK~MC{-YgA=_Dbm<#O3M`3UAk;jAxtpNiZ{ zofEL8<*m3iHic`atbQ&ALxT!s_Yy>F0f2VxNQv1>k#GQr)q1dEwQQH<4vk8?t?b$N zlHHtrQu6JMJBcX_rE9p=u1yf-CPx^F=$ML-Obq(Fn@oCb6gF}{YV=(L+#gT*x2TXt z!s&)sOjdTYVNN%HT#ed$2k`<0(_VhDnOwfOm{2#bkhDCjkU2=XfcnhnFUAZEb+E`i z%`=neZIWYPRWB^+1-Rw@K=-_H=x~U#Q_kKv{PK5VOGbJFHvOJd(*HZe0-*aTU;oy2 z=vN!(xV^ud_*Mz?CnC7KPtkgG{~s(`M;EuO8j2TRU-BO+niBfC3}5JdJohsW`b-z2 zw?k^i!GAgd$||0dALRPw?s;$#{s!@G*8~M4$u2Wq`#V#AT+e!CD)qxi6Ud3+iwYI$ zQ3PzZToVJmwx5?Cw8AMl!*hWV>b;`?J}6fxYNVWzI{5TzIJsyaTnaHfs{L^JaNO#+ z3c@QXMF};tg%}#3$aNOb8n4TU%s?=KmPq#`FOyK4Oq*ZmAN%ZWhqBN}M zzy?xYOPbv$wd4?5RJ8}gS9Dxa8hm=?xxuLXYyJu{0suaoE1r=d_4TLgd~nBALY|E+)Es%CE8YN=e}{I0bVf z18-eilQ0g7n=P!8Y<-}O>M|S1dlGC?O;KzonaW4cIiS@co#iZgPncTihnG=#pg^f~ z|1gC~q7F;9O}dIj!!Q6=TMNCV?J3^vrF|ZMrZtK2JA z``19>uz=PUi!1GG{+aqBI8%t!JarHHf#iPPT!8Jn58<*gqlybl73m@NucoClPsOZZ zXp2I27oQW*VnQaGW)mXs-6iw}}s{?P{N-E~%c@La}D z*h;WaX3r3D_mDptPG`H^tUheGLg(qCU# zx7ke=xyOIEB7dEBWcaN5;J6Yv5QCve{rCn-Kge6U={d8`x{scc?t~l{*I|Cv<7TtT zbbZ}A*lO|FpN=R{%>06vjJhv#d(IdA9xDzh3F9cJ6lqI6K3Q4WqLbz|%W#1K%lgjc z4}xMb8Q;B4qq22$iaq-nTL=IkzQ~JY5N;c@$9?iYl`Wf|1ESV(K3LwxP%4RcayC=^ zScZc746jYiYa6u6J!8i)-KiCg>D*|WG{8+s-3WQTx#i|+xlfK2X>wrty;Io@fweI^ zUYpZ3NJbE^uI{mqzUQ)}*JN?!gF?ZY-?Vw)I~|8T$@1n-%)xEF0JxzMCys_(YQ|gl zS>_JQCYX4ZUYK9OpkeK>D(dqlBmjTrG!&0EaMsBSDCB(yfV5Tuzon$!Gp1oQ+Ik6o z(Ag_QW8I@$mT?wqE2Ez|2T2O$s30tavqX+Ch+i&P-kgP(?2oTHuyPFu@pjv#-G{j) z)>|bHvT;pFhY-sxl#LjO*UQt2v=udU9?CjTev=>&$MvDDPbn<9+;a1J3>OowS&x*S zLAPxIJo(?fl5-ncqoW@53o< z#VFfdM$b%04aF;YvTb5+@pLM2`^zBYlo5HH8wlsM&?tjs*#N}Ma?k^zUI)m!v#UC^ zzU6z%_l|qO77|ad!RlkL`UCFSfK${e2=F?sfzrXhpLtpyXD!By%ionX#6DELI}>%` z%=fb2LG?A`E9Qqag4Be}8jq1)9+buyN6sgfWh4G3-qioQ5p4Nj1;KgR7wlLurFbR*|A@biM*(PCAUW++CS%Ty4RBAz@aaqgFrN?B!0}H8r>$!P0(*2)@qY18`95;mqP`q`=gmIF(ryM=eClW9bvJciy%r8bOTFK zixANiDoSSWTs>x@3UZU!KKkB1OkLZ!_N!iDLrO(qq4fN7>K>2194o3e@eeGr!YGTO zQTH62NaW6vlO{n#Z=5SS-%9{ivM!jOsA;)`d!)>33k|l?I;riI4y~jo2?|TS{!`_5 zPjyCwm^!39CX;A+B*7oFdmVG#2N~P1< z?d4D2H0!=)DbjVqlKq#JEZ0-DjB!zW*JkL*QO6@)nAmpFh zenV@cgT+`r|8dqUJTQODOeEx$$N5xC6xqr^ey%k!N&l}UZ;sb2a>$IF&EBnyDSsr@ zV4%BNz#+YKt;qPWz7qr+fxHB2Gb@50Tu1zcUEa|XrSCc=A15fpDaxKzU=Sr0$y6%| z2jPNXN{z0yEu9uHQn=l6FQmw&!$JH>Zq0*=Z|JRpb%667n01P(5CtOn>UrPF;^$$g zvJ>fB^D7q4P7tW8zm3#&q%vL$B_7cIp4-$#Kh#+@(gS3e+Rm2MW z4{jkX)On&>&yJdym9KL#KEi7^XbapBoS5B`&;lWxI((x#~O!RbTuCBR^5v3Ts-;xAIO@^0{h5__pT1^6-zZSl~Fa zpbB#AxR$1-OuSiKMVXaDT%9gvVCZ83Bek`KtDW(b8Tk~z$J4JO`j~{j9jAd?L(&I_ zb~{c=MOuMi1sy5>OZPXWmfUC{3W#}kW)BMh0bEk}s-kh*1*h_KW_#srQa|I*=YU49 zH9zZqEY0{pe{bYPYI70Ujk`qw^q)x+3=9lEUls+7VR)GKJDr92bH1)q8IgQx%ZwHwMN**O*EkD&O6O%ge0Yur}E~e zrR&a8 zyz_e`GMUWC==2rKgZv#|ba*Og8E9PJR;a8xS_0vc@n~yiFU?XfnCXc%Oj}Y?R46Co zI$lL~!q|5lkE8JgR%49tbqo#A?o-NV9vB@C?= z1T7MW80x1Oz*?yU0g1z!6D1XS=EMoT#Mda^W^0E*TarE9uD~m#z}r$0ZLhm@-P#N7 zx3P&)17SwjG{ejaQt;GtfYN&;b4e(RWKIe!if60k)C|L0SAN{PgWA5_byy$e0q1yK zmHhYAfI0%wNB6h$lIG;^Rfgl=;1)aZ)mD;o%b_<9mA{V|Nr!=uU3Vw3QR4F1C<)_% z9-ODu9WtrUDN5k<0;zz|Y=)^TI)h;UrR)3ftsKB3TjqnTRgMG8nC-%WmctQ}8+f*t zFKXfhGKKq*a8;|T*2R(Inz2lHZU0(OutLTc1*o7Ld1oE-Ky!8qMMa{fjUnocq@D3B29#YcIve=a5a{Rb<64DDvb z0ku6_xK5?C5v{cHeWPtJd$ct3?4d3ly|>TNgkiou6CU$ z9?i6eNDY6uXI86iP2Xo}K(Whk&9W+xu8<;Kq0TY}-vEUZ=~6rb`)Lav`*hSx%0 zKs2rTJsHy7IwVDb4R#hkg+)z$8R{PQtTyze?9j_(FDBvTk-mz6FQ><)QVPI>L66&= z2gehUuvVSKLt(@rrW_c1=ljA`aVSyFyt>){EQ3fHgW#t9FjEppxW%ATJ1GEQ37k|W zg)3ZMkP#G)}fiad1~zUp>S4rzmWm-y<`=zYl9 zGnmldp+hF+8D-BprJX;PoAdCZX2DLl?`g=5=U-vp&HOcXtF#3+XQ}UtUT#z3rC?38 zjhIM|k)So=&{|MIJUVXoS+gj+z>WsE79;EQ464q>(bhxpk&5dcr7f=dW0prqy;xpK#-pdr@vr6I@%N;g#n=V5tLu_(D=_^3Sf=Lb zBIpFO5&I2!7vVvX?g=V&ll<{5Rcwh2qmXuuB)P|Ue7#- z_uinhl!bot^YhLgu~NrYe!|k_Yc(*bcuM2-u?tilcB4Oq8n!4Ivi^$N7k#jhHxtu< zzm6;6dCy;RGN&_hU2Ci!x<5EaJ5m3oCxCiPJB&ibRH*y>x(&K~F#F($BEJb^2=_(e zq@wvx^{aHRG9rB8 z+RJ|&j5oW$+YKRr@cO>ky;}W|7q+@AxQ#O>2?G8E8=K&wXs*Pc^x(8q;bE)C9NtPg zyCgDh)(kj*U571{fB2}28%ada*VK6D{p*Td8g&oeu9eVRuWIyeT=M||cebu~`|W&?Z=uv+BIObR zYK^;7tZi7?)gG`+s8bS=ruz{kKZR+;`=I@N+H`&F$yjtgxOG`c6>Am9s|5FuJQ$)l z`&(eq$09%`AS}!4_XjC356pyE+rmO42w3#yv=b1+xtBVxnvd;3p?e;Ozeqjt45?u| za!wx=BzcT$J!_&t?`wX5+Y3l3LDdlV%F3=vZ~8DsP0`9yd0wNsD3Z$y-L`J2{s$Qt zTA);SgEONO98sxG4LSEr3Mvr94<>@X6AfqKPO`^kzqngtzrBAYa)c#HwL{;oeg~Hk z+Hm9I{QbEl=GE;C`~K9NHH?i)L@x^PPM9!~dD7m+uSlmbjc|xurNKLXg-kln=%)6! zoC>rBu~+sUC_!NazEn&m2@AJtqgM{u0GfY8brNo>@g6PY888`ZY5eBj?+1S`G zu~V;X`3{B6uVFt7)=Rz}i&p>c>a0Y-#l${J-0gE!TxL$OI<*^%{#86G`4@(Z;Hq?-a-@YQMO%pLNT;3r=1V9YihQo}Jmw9bwp)TmHlT=S}wNm&$kFeZ?;7=C1?N z${pEyI&Tg}<4KstQD0K<1WI~2Xg3SmcW8w}+<-5{o#+!n856`6G1WPi1k$#7YI zh?Foi>x3hPS)*s*-MFI}2a{Q#F`G2cnCL-lHFBKdapd;i@m2Qw?os`RMWm-U0$R>) zs__Q?*>Vf-*c2h4hry(E5B>3M5P;QRia(`cs)*b`Y>2sfic&gDpKy*;R66r<$sg2(!%%IU<;lBMFyywnqdayVZSHf0~ zr!^{j)jW21S8EIDgFpLH~im!PX2Udr+6%AllXeXJrXCF)m>h5=d%rwI>( zxVIHOvWbCGdWt9R0ycD2J@F7AYxa}oj%H~bvpES&(71cI-u#9TEa>a@$K%PuX}ebj zypC#p&9vq~EG1-oQWO`aBwX(;^+z2dya!{&(pmqm_XF+Hc_!s9K#McKv5Jf)WCI2l0W8>>vx50GSj$TR~6D4SN?uH`1zWm{A z797@`%J^0jP-!D#_RE3pFb6bcUPfk(GT>EJIMfDnncNT6)AK3|rkvN~IB)X`F|AQu z>~cN_uDmYj$|~rna3s~u%jPjHxz0a+Tf8(UK&EKaWN;% zVt*t~MCGyKX03Wccj)1C~1BeAmv)&%r}CIrWdS__VU z@;i7g=CYdMr&tor7Zzrii3F+jO}QN)^>qZC%bfjmTWBIE8RRyBc0ZcFV&}G#somTk zs&Wc1N!#EZ^{*Lp@)5rJyvtoO?TKneh6%iM? zleWq4s}dgBbk}Xv8k@}kczOnZSWDu+6m^R>@mKsu!L5a5}dk36^5?9sieT%TP?kdUf zCSbcL@d(6A(^liIRgb1UcPVPCx%R9w_1QE?Dw=$gFU6a-37T+X*&9!bK^QOmfHBqc zTGXcDzB143QDvd4kA4BK3)S8RT^GSkS{x|Mki`F3&cY<~=bd!yU-aKw`+YO$t&*uP zY$>;bv|*t>$;&zCkT$BM>L$HN1U7a<`3P)bB5D`w{l}-pW0^AH`X4`DvOg0O`SP5B zyhx8WrE&2CW@(d151xYM+k7cxo0HIO;N%#mNCq-dRAC~Z1-dM*szH_BN%?h^>iTe% z%FAL^Oy1;EWGj{HT2mTnGZ)w2u{6m?=JE9Ztc?18T>QHSViQyISutF5CwUjWv+Ye^ zkFCQl`=F!x5(`L?eEyMQHQNETaQl&&YCF>+2v+;<pyOVNa! zpxUxIy$>ksn(Wo@;EK>~nSv3D<~`6SEYvZTdLksb&w38JWRN~bq`q!C#(+TnpQmV8 zOtMB=?j@t{q@ZF!@FLS%z%z>Nm^L=qGE0$MG@D5>cc5Y7XR{G37DU3uiPz*39<49wp%5`_HK~K z>gOO=f!ruyjbA90dEkHf(>2=@Rxg&9# zzoI{%u{Xl5dAjeH{(bB)7rUb0%L1^JvK)->B_VH6ZSS{{5M>S)a77pQcsoUY3N-}c z%RD7`Yo^cyof2%A79#iQ41Psl<08wBehS-nSpWP)sXF(~>q>ct!=9B7_s>frg1re6%~+6+=lz-$3Bw%rlZp@-`lPV0iWK**OuX1|P)nUf#N&jjV4A|8MO zkziMtLVrb#(BshUklUO_jwZTS6A_Gs&*C*r0_y!OI=squN$!PSAIAQRSb#PJVt8<$ zbSwY?h*bRk@zF`%jOC*VajZkGh#KP9W{y5w--K7?J@X+q! zp&2cZx-?Rn;Z-Z&N$q$v7oH`R0##8mDtuwE=tbUWySn#!T_mD|X{Kr8B%p_t5!1%b zR23$tXJMi)0A9*Tj%C4vQ#+|3XV$)66%_QFFIf=1Ei?FSDsY)w*{+4m&tB@Cl%fa0 z_@+%kDrMW}(;>J`OnC-n7F9<#CA4i`jY-b-B@GMtr}~zp<$!Gfv3)%J?D=_Xs!@b^ zv&`KWDbt$ELg^;hX#+Ih@+s-bQwaL1+Md8@z%V+StN&+}YXxK%@PGQAY9kpz?a$h| zWE9vBQ{zP&pm)3Dj-A`99E|115L!Mn31}7B_3lCj>%!j?lR zQ@pTMB}8_(M2tkzGsMjbD}Ws)R0wZbBJIYzDuTb<)7AS>SVBb!d9t$no`$d=Ps6FL zY&~>ot66%<|GW;~Ux3x_FP~t%V6?9hp}Wgc+z8oZ${6QTMg_@_Fc@Y>8kz#HM_OCF z`5kJQ#t;l^utXYH3iy(?(*c(TwE5U&&we|aW+y)4f^>OK^%Qk6fveB?9ZrGdg&y%i zgNFxGb}@N1lQ`SV({5X?rJUJ7G*zmnAxkR5VYTEXo->Vf1&|Mk^2|5_fydz; z2mLxe=UShBw5fVx?xEbP3`Zd%$n0rC((NV&GAJ|1Nt@4zluLNx+e0!PKa??IjXHZf zk@Q9QLAU!HcDVQHSVdn_H%QJMi(QJv=}h(o%Xjq3 zci9BHMb2nDT%|4!GS$VA;|uDHSsPCvHys2V&6eR42n|(}eCC>(P0vCQz!(41sNm!9 zP<1qUTODTFc6g{2fk>L%HH&J)is0hz36qY3@S~W8VCpi8h5SrRpg`7QT5aPwcMwZT zxFo!DQ~krnA^!Ddp`2#*z}J~`0h3Hf>UaH>Uz>Kq7~Rqj_{>ZCH!k8nz|X%pnh(L) zm*rAUS-Aq(!k9%#W>0^xc)+hyB!RjRPkq58B~gbM;Y0Y;+Zx zQHCF7NHxd;B+GQA(E_BG;N6ws66B(Jd%Otg%zDt>WjSu6qCuDd$di%%0=;iQfw#Fp ziyxh?a&b$hdXOaT#&rkqpSk#4clodR)iL3L-R{JZip~UNVVGnGXI<4f{@4tnQnz}* z3@3qJsw(mMVG0u!LBLt9VZ>&Z{OgK}TtDm^?P8@*;nIMMR_l@*~L$(mJ zLYlRSb#1dzsYw*ObuC>r4`sW3+aP9j6gwD_gFR zl3^>kiOI`oGuQz`Ulg!ZI@=*IhNAp3Ps!#PgA)UhoNW$O&F_lh|B={72N!6n>n!Xa zGDwBQnH~c!{n&jS694Y#oqPQOi?{h}b~5?OXI;bo$=#DJ>?z1>uTUJMj7e4w%4Wis z205c`hc+<{C;$!$lkhyZSSo>SFDo0C>jzVk@gy5Pc}`!~6MYx&#lP^HU&^nc>)RBk zQ9IBot?o1rtlKG%U95|ZjbrC$YF%$h=%PpnxqFFWuTp>)XLv&QTlow-f$ksgIf0_e zzpRr{JPD;?$Y$yw(jeEkELSJY%J^h^$Gaoypl+4EHwf*-8{;vXV^RoRl{Y_PKUawA zR_YmyfZWF*QRPo}8x!CCK}s`1|5zeTJMd9ZjSt~W~n%lW8 z@Ib|-9(h#ibu4=iPwkt|pTvwRqH`57k`5?)^AZJh+;daeMfF*_7 zSHKvqy{Fn6T`Dcg$r$iniD`S1XvbmS@|oXKdn*wE?C77frcib9xY6~(44It!hpI9` zQr$feAz#>0$81lxSk#X<$?m&g>sfO#6|}hb?_zu$?)_L<1KaZDs4^zsi{D9BTh^0` z=)$MDc;2$9P@k|=f6P+6Y-c3Fnipe^F!&Ld^ewJzVmUw^mr%vzz(WX}o(#s{6h}G|kr?ELh^$W!BBsqMh z9#18f)%;T&C_{DB$+H`oY0ukIT7)N#1yH6%Cu_F_q5!`XUAu(|#nXb)&aVZr2J_K( zhg1ZaG(orN3R==Vc%p;miA}Yu_{?le9uYe>P}#y@6qa1IqjjeEz!FzWO-)WnIIgSa zE2}-9hI81}k0slpG6Q2`F02zhDvL6RAOFRYLpi{{gVj`VHs5AcZ-_WRVx;vmaxwv> zs>I)1)eJFReE2Y@5oPQ*SBd2j8)yr>R;jg#+1Yy^PqT(4_yi?7+dtps)ju|Ppem{L zNk1K0Yw-h)6sJJGxX7OjN-TU@kDdswxKn->JLi4rFxa$1k`gr_kBq#3$@Xrp3k2xe z7Crc=WnL0hzhEag1fsD3C%IkiB$W7V;;OP4psb-Gp=l(CT}rkWzv(6>>N4g`FiSp< z+jfCHru4)0Ug*=Y*2woy-O;zO82b3@EkuFLb=DV8Q;&eAXi~0rYt(CAw0>_f7?n19 zqQ3sMh88E*3kg&BSI~p3Revj{sPw~&CN10|xCla=25G_#Z+Gm|r~g0)SR?0EIJY^5 z8Axmr-AE~(L&|vuiof020&MKyzv*4SZ+u;ykR6y(Q8QwEe>2v1RahioH;~>Im(et` zE&-98A-=t{DR+t&i1a>7eu@R}O4fRKClbA*0TA1?EW|OAtAY^T^g(kgV^}j_i)INM zdV6L0_g8w}wm}yhy=NSbz~#FW9q6=16-1K?Mv}PmC4%+k+dJ;N*denvINE24)3oAR zn*rdSyQjZvK>8W&tezf>B@tMAsm~Cj97S0mO8$i}_$GhR;4Au&*}BdVrMA=99zdx) z`tVB=GKKXa1iqZvP6kGiWeU@Kqx5`)z{1F7)`w5O?HttSQ%on%Jd+7ge zoBWUM66k+KtN)*7AK1^C=4O8SGzxX<^u;$&?!6q@bY8V1Z0Op!=FHAtf=h7|wOIiR zX27Z3Y38QLqP}Qx&&;(~@R*o4kvzjZJduUxmiD&zvx#bDj~M4f8~$bl$>@r_O^Op7 zD6lKF%-L}a079!hvfIuntB}@r{+v%ksJ0#EMfQQ4F2~bOM@I70ZvRG|QmKT-zW&ZF zG>9DjR}Nnf80212cx>zI>qHp)lxy|RdDOF;_uglm{8hsgzVwA~PdYzh$0h|9Z~FJo ziKFlsF4U=+u201vdCyS%^8JW&V}Jh){rS%Q?+tn;yE;bUdVS9N;om}qJ`NR@5_;@c z3oKLPj~tuPXMY&C0Y&CssGaRR6M63bdWY;fQzWy~As`j~uKq^9AsHv7-9Ch_Oa zjSFY_Me(=_<_rt+CukleyPp(qDLmI2RG(8T0PL_G{U0ohW2aJw=+)In^M-_U@4jw( zQN5)quoR!!DcN_*!QT0z9$mX|aCggri22Y{f(cJ@tq|HeHyTiR zJLU)wJl618QE89;yLHKUohe+%?JuS-7qFA@8X@XFgksJAnjFq1OdK-JG5R99vM*zO zQ^61`9AP(cLnk-JvhZK=J;fojr@uLWk{(x~M|6awn4kQwrQKh7>7s{1EJF0JSXP+c z4j>PfWFPx~B)}%AC;D#_Y)^+Y_%-%(HAy(tzZclCi-2th`%AQVGu!;l z+VXsK=j`=4=dTK8~S>Gg?$g=b;ged}A%|4suu zu=co`BTaFZAj;?7zplrf z*EBwS=zfyMmORvZULkli^WV0{pCLP*yVPuQsV~`1L8$Ih8-s5?k=dia*M)V^sg}JZK2ry4X1^?!jNv_72!;t2>s+&{=R56%R?|0? zz0B@c`RF1NpWGcD9Z+G__F(;T_)U@uALWk`h9KuTcMDIPzG%f5Uggck-1H*N2<0<4 zo97foe7*njqp?rj6InHhf8MO9UhH#g3MdOD{5n{pkJ;Mz3_VkS4y~=JaevkgG8c9@ z`IBl|)3FoG10LR|O= z#WpFrPJhPBuPcw;5dXYdk80Q+TIZ@@G@7fg{se5djJWw@3ZLOL0k=7L6`jb!}B(vBz7QEfg<=B1MT;}Kw?*=4<#LG*E z$LBKpxRktm`?&f&$9$Gm*U%A!J{fYCYXcKqKFWvE(Tdmgo;a3~Tvzmmod?|0q)p@}xR%%bld+LF@>e0uZq_xH{;jO>gL3Ym; zlO1JGEYu_pv}m4X_?H=568fu0H5U?(@mDRA6Dx<^LzR6p3etSHeD<5fJ9mz*ffooI zx4Q=KCv|x&`A4q9X`I`(b3Ctl46IyQ&i$(N9H4hhH0n0h*Kb+RwK8TdS}HBkk~>tP zCleht@dMCWuGOz&-*Eq)_>Xy}LA#~$ZGD$f1FN+Dj0_2hCWWQ6i?^1jX-5<`X~V5a zm9x<(!tcW0Yrid)`_}(jC2jVEh-^-qgmqUgRaMN$7PdzIj~{Av+xt+ndd%0!}w z&r&_PbclW8qg}4Y@1RY<)XYYNdD~O2R;*GdXxM@6>41_dy+Tsg)ueKt%L_q} z&cNm9d&cJUPoLc<(>piGn<6DP6HnY_CD`@lo~9`%xMDYB22@T;7 zWO7+3twPXU2j?1-?Z&MzUoWZr3`^H7w5{R4>g9t&N6HoZ3+DR|H`eC;(7Rs#Qs;fG zW3&CI2+0>3+wQ)1oNQd9Gt;gpJc7nPH5Q)pV$P^~cx*lC>|fltpX9ZXujr<_w76eo z;U`uYa$fGw=$-j(a9UPYV!`i;&VZ6)!p5S>)<9L|_(mc0*Xy+IR;+5z;Hv)*hR}7| z(EWsjdmoM+3bVe~?|!Xjn=s>_Ab3*BWNW3Bq50E!V#1u8Yi!{?Fgkr-Uv>Gwnlln`{ue^ zy*+)E&j0JygNH&u{ZG+{1Emv=(45H}VV-0@SGwJyICq`nwfO4B*xU6WM_&uX)>xT} z%ciWm_|Rfx==Ln@1jc0~xlY!Gc&?gEQ-#ye8xz}GV~%~kO%hd$Avpu{+3w!+1CPqa zzCCGD8rfepMjwf8hc^CJrC&@;NL-t%S{14E-jof{bREux-1}U5`=AvE)kxpy)9LT4 z+Ab<9V;t%;=5F>Y4oIOaS8BWpY5OZn7GqV5POJ}%yg#1ft`)1UjKcU%ueNGFb7^c0 z3J8eWn5%;BWTccaE8Fo~f0NZie3#cRT6oaA<~Dl=9p>;Ox}R!>&Qq?}%UqX{3Ai~?sz2d6U%8wfYq|3OwDy%zacoVy1Shx! zcMDD+xVt;SEx1E)pFnVTcL?qd0}KRrcXxO9Z_b$~%XP|E21d^(9nEJO0sX+UK?f- z5X4*1yXViq(*Ekb=H%1g!ei?7T3}o1-0~P}ro!s87qzRA#rcJeX-{LP$lk#B)QJZ4 zcyUBd@4Du2xij9L9Jk{KlRCpu<*Q&0dG1~76ds=S<&L20MAv82P0*Li49T2$nH#gi z;a;Y0l{V>YeOcxRkcFsl;@y8R=2PSm3WPn?G1v3YOIS zTc9f7dN(lpp$O`I+bJ?Q+irGo7tM^O$C@d_^PLkXenf{Uo%p;Xg~smG+vlt>SmS;B z8I?%W1^{^VD_+_?k}bllI*eK!MfANdshHjhg3}PZs#^w}z^DSQQd^d@Ec73q72A(q z2G99AJWqY-!@;?eiT`&YOkN1@y&X*MxU3)mIo*#ot*65uEwR%OHeKHVN!nh|;4qXu z`C*wA&sG8BzoMg}*)g*B1>brI^Z^$cPVqa@Z=LrxX{oLEoBi=Lp1s)P|16Qy1T^EU z9CHsV4eKutIF;57%b~F@Ta}q}P#t?iIH0>B`bOJD=XKF&s&8|PMcH!Q`)TRP81!b# z_3phbm|Co-r;i7WveJ(?{Z*?k-N|y)eF`eqFO$|B2EKb-Ag`A@og}dSjRqktCak8? zU63fPQd5vhpUr+rkt|2m^d8PUBIufPJH}}ri6^0g<#Q`|`jX|;m(8^xV^td8IIFm- zyv|2k`P`5Rx~WYa{BoIWkn;YbN6+k4h;MTFuSTg^^ly4~fd2bYji+tf8gA8mzg5Gf z1WnZJaisPs;F2a=U~|Fvs6o4(+A8l_(V16sYM9Q%mR=-eVww_V%An);SLj0^CrIFw z*|Q%`@bdSuy#n!0*gpS$f0+&7>r=gUMJj^L^^v)6%eSieZ1<}YRYktSo4&!(#`Djh zf8xQw-r5bRl8!OT+xJFbiK|Ba(jdrHcE}94`Zy1;s%TuMAGNb-Qr8)sRewIR7Kju0 zvvf=PdZpMFYd*$*J<~HT-NS93^&fT&xGWaP@=1*o-7((dnZe7Re2h@ie`)}Ka6ilq z2sf7KlW~ZS1d!!zxEG=VZr~x0AUoau|fF+ zp?a!5hT8urxHANtLK%UKa<1{4MZ+>Q;gqHH*l%s*#Q(s=5V+!bb0schOPZIG@iSm+ z?noMSus-ChMXd_queD)#6G9i$*M~nkOYLW&p3dlQ_h~So{Hso=rk-sSfc@xS%ku#J z8>xI`G-FjIS_s7#X{nhcbtM&iN=$Mw#?%b(1b#U9*}Rd3*{m zWW|a-7(hAg=x*oB%AI}GEcFlH68tvBjpj>qI2D;G!+o5|#pAJ{A|;G}Q=u|a-s!N_ zC~+jfBoo>=4h;Iy1cRm$2GuArYiny;o9o+rjv4;Gc9ysvJFIl?1CZ_Y&D$@y=+zqj zFOu*HvA&Z`?H0?9b*pEMl(v8j{F3RA*$NCyX2$g3S2$GLb=K+5$G0q({iNy7>wx4qn)rQNhcK9GORgAXzMY3ODco>^5rmV;E~BL%8vb2qUiz7~l#U)<3dRr%@)9RL zGafO$v9*+(Nk91Ct8F`$(V%U0p}14)Z_0ai_{3p6xV5yj@Q5#xts%rPsuK~QAq5O# z43uJ$0R}mwG{!lq$dYqp|EY@fTQ&(EEgYksUWFA5%*%`apSHF(8;g(A^k_K}BDkb9 z0~gIPiaWa|GYPX;JEjQG!f5{-kC+daC1MjtrV?^BrSLEq{|ZhD_}V-N`QINQ@K{oR zD*>O$l}N!g0{)5|BIFnF_ZulZIpqJ|{{Oh+|I-aBzoliR$3 zxs{HCj6;JDgd8HD7?{ z36(bVDeXAjEkQ9gIT0HXR==0v4Ji`?SP9l2N;v3`Y?8zk)@RHGF?83+M?SDE`J9ZN9(v z#ek7f&ME~T4TM3#Zwmrs>08ms6i2-o$6!z#QwA?=no#~%x)@XUI8*2cD($VE-^+00 zddabyc!yTK)P-CNGmqj&pP501-5t|f+%0ig`yyw%I9R@6vaD<~wz?qoWDZ979mOEk z6)-IIw}X^$sRU0YPFI(|a7zi3-3t@3m+CmZmIGqPkfG00w-Z zwQHu5iW=@>fAHm|BS6*qPD)A2L~W%0JHN^_h!~+&tEv#MG#`XVX!m!3T_uZR2uhiv z)I48s3p~l@m9;m7#X)Wxe=pBYN=-~n6n!xYa=i!p$P8L<2a>w?UA~q@9u`YzFOQKPIISPZMzdMKRS{=^@)%n{6YA0!tdd+TmKzA zy#fCAQ>jI@LYTonn%UiC0)0Kn7xs(wn?c<9ux_@h(=(k)8f6BO_kFk4zpTLhi;C`7 zPlB@^ql=QHVYNROP6SC#0cm#DWnvDaEbU?Hs7*y4h*H4&GDzr6$RUL2@8ol}Tr7te zmr)+!7d1BvRPg0*A0A_U=D6;vT_h7RTY@?u+9s49fz0VXNH z!eujac?!8GR4wi|xUmFGSl>XF_T?r@-!C~)Q>=B!E)CIe!GJWpqh88_4A75k zp%@Z>%Fub~!RlBp+N1ZO3u&iaWLteuS%6lcyK&E3VtD>X^rMGY7A%>4t!2N^W-f`H^&V_)TD~c0V!;^?W>xHTkOFYrOagO4B zo70ueiOcKq_hMQ^8-R|hU&ZqVLWD~@!;&+>$K>QiXNL4-r*g;U_Xn7htVBKM!sx7a ztF1{rpetJlB`;0#Wk7=a?G@Cx*{P>wn}>ly01Qmv`wQ?-v+%Hlo!%W0qZWuk2PC>U z6IIN&6{oUq%fsS(nRHEdKc)xbwAV~T?3pjT)ujT zMB%ZR!CpG4UEnSH926wxo#|g@Kjguc`5V`Wj<&96E9%4M4tat=`b3diDJe#%-lt(Rc}V0 zV5SuJ>-o>Dj>DJicJI-*kK(^gw!i?XFa%Y+g#7Yd2B5{F0=&Y+3~j^Na!p83a?g;U z1b0{dL0bGXId^1iQ-_&sSG9)6mDkkd-HzX<0XNesgTf7{-e8Tk}t}03{ttCH(`W^eeGRfb^ ztk3Bb5a-WFnbX)ZnTG(SGR2l#9N^mW4$A*60f1pYEAKrf7DTPm>&}qBYr{^TVq-nm z=X-fN4gQdC-u%_xD5nStIlPG3AUI>Kf|U83*J^2RHMl5}NJahQG#gtt6QPZXUh?Bm z55ujd>4z@rQ7vkZ=$s{D7SAWpdxX{DPPM~r61VJpRR$-_3mGn#mEPQ}reI|H$$>HX##nY0MeYJ)st z-bWVKCvv7_pzGv~<93(qtYz1KZV} z&rMh)@BI_=?BpCFtKD7bbuzC<>o0(j<55lx8w}+4$HAZ3H!cl|S)i4UhT*|$4FEWu z5LGwo>3U?SA2Oe-#rm!jjby)fYK_BbT*ER4L#Ff#8qKfDfeVwi<>n+zSQI$LuUMoW z9*<<-QCm?6aenN3o6d4Y$=ftmq`qILzQYld9}FFSG1=;>pf5kxvRikvxtW3KcdKHi zXDMDk7_U)+2fPsSI^0B<-8BY1wE}h%41dQLRh0NX{h9sH&eQceJ2@32clt!d7J+N#45Bq$lkjah5;eb?Yof4A>Bgoi_{;_K) zbZ4D5e??pAoFPllHX>a$m6=&VcASvc_%h!-v9Z$FGKbmf?c;mFj#^&W<+}{7W=i_v z-0kU3ZrSh3%EGO~DKT+r^sj)yn_1uji(%hmT#Cp9|9#b(#4qWh@g38M_h-c)sv6KE z>teU70-^4x@<&~4^C$c+;zIYEL(`mtr~3m^W}0X6 zwh_xb2Z?wII?XLFSja0`&D;>pQRx(9A}bMHS7uzgcey-7<&dLxINy91 z%2;cc+HERO(k7n4M{j!E9 z4p&m0@iy}%LQk~g>s&iM=N~#;O=01VigZl)mSlAMP*yuJ6&X6*dY5Ca7T1f!`g*yP z2CNgxy9%!pZtt*5XdlQTSPr~qu@f?HNyXr>U#~}ig+?4>?O0C+mVr(p`^6tZ<7DxU z;p;Liha+08exGp;IMUb$Q46BNYU7H4!`(%@>=e;iC|MkK#WlHE@uK;a0u_oddQ?M< zT{2Ka(xj0aZ)@PpTeFXb1Oed?8hcBlK&n62?tJiOs_S$dZ}Zyl@48`!IMUqR&XA4^ zXYNseg0_+rm{3uA7N-1et_V~wwZ0I_O-UhfBjaxr8=4n>b@Krtlbi0kc2Kg2N|Ff?z;(idippBPFu|u^g(3;%#|GVfG(&*GI)4+#oevQ_~#(+vA52; z&DistK5Z}{A#;Dok8yPPgY8qdE3HGYY#nv7=pT+VE{FY1wr2_C9Q4Kd8sxh3U&E5G zZbT>u?e|Z1XbFj*%=#QcI@xe;og3ip4_S>275$0bKJW)BF?H7=RH!VxY9H`}2AvQ9 zt*k%CUzo=!F%kO8L}|uAVcz?5d52*w@wq@m3_h>HCpYCu#&ljeLT-pDp^3iNso-Cw zvg1PtD3>>;Jr%&{isQK~?#Zg(@%#Y*oLkGwG~2FuyVP{tNWz&idrtd{7c7D%i`%)4 zvZk9`X9=W(581yl8$IT~ON~DlkK3*QGCs+XSexV^R>V(Ef6D4`7%j}An)xDT3Xx~} z=jRwUD(L;B!7o!sk$5ICN8d6A$9em$Kv*y5tE|GXX5DkoL5IL>kU)e2mE z^d?h_AJP@K)k1dhnEgn-chUgF5hW3xUF=$&tSBiadU@*Jk59J$H91(@3K+b|%4$tJ zs#+7z2KtWI&Za9@f6Js}5BMEt5tD6U#8fddAzC@j8ABjF(uv3#gp7v|1it4cdyA;K!YCNyA zT825N78BWgHt|1?=HSNS1Ev;)syeZ>0r=+vIy-LX4+AU+K!C^HLJx6-@ z-18;F4rLqj&R@Vl(o}VDp7Z$&FdS3_SLN)u*=x#q3xa31&h3e{?!eVcY(_Fq*Smm7 zf#cQn2*2UouXXm$TPwRwBzZv3rtf?{ON{|nm{yS?oFp>mkTB<|fDP+Y9Uuk>@c_KJ z`*`uU)rimA5;wQ$A2oc3hg!z<#YJZKCKh1Tpl5V15u|hX??pXkSL)oR`;fa9AOm4D z@Qdf(D2i$>-OeXmuQGO>-tZ94LO9QJTMu{s=@gv~$U~S6mNBJ!`aYh{F9K|cD}`h} zK-RM|&hu(m*OCB$WCtfFyU~hL`IlD#(?mm+nuf-uYFk0vSZywjsIaK-d}No^xA;6I z&h7{H{&FxC{(}^7XaIhjZ%QV^n=BoKv7a#1XL8i!NWbv1X_tQ1k4+4_+jg{o3$D@f z)7%PS$54LpVV|JlFczy4>41>AK3ITuGUk^>#00}}mpb|Y>uV}nc6v@XXF-x9-JUEy zJbhH=dp-el$JU09 ztjyf5fISoe^`xjmY8?_jmAFKD=FAzkUb4mMO^3b_&6T=%bM3x)HXP9rIplui>k6Hs zz2ke57-%m2G8)7ky4N!7USS`tB_0LbZGd`- ziB&E(tMMlO!Q9cx+V)T+u$4!9ynDU=oNY=-5vT#csp@1){v~P`oUvr93W}QP9vm9* zfB(a-qnExcYG8KJFky)}zk)0P&F@;lgX$mmeKX=m|ClZzpKqfnBVsW_E-JIYm){!Z z;{$HWU?^&WOoRNp?EHBO(1wDrHC!>>W0=#_%#>)k<;67u*>Dvk+-gGc*l1I34MZK6~n9#+jDqA2_oJT#vk zl-!E^OkIFaYEC3bdv$YX)Ux85Cpkud1#-bYXCg^0!x1EB^QZ&l1}|i`?!#^`j46c{ zG5>US+j^JvFD#h+9_T;LrXpR>7&mCVc&!0w#M%udi8exBs3)r}8M5ZnIVdar4dI2N zpYYjT{Pem#C#+(9e+)@(5rdImI$AoBek)My^wj#gs;a8Ig8VPonA%zuU8CBTisw|Q zbTzev`01(PXu_j31gE4%#~%S~{xN9XK|g=tpxBSXE#n8P(wg=bHH%f7mXv-L9u)^T zO1-xj=adp~`Rt}2EG+Fw@Zs6n7=@L5_0bWXHOYF_#AJu!CZ>Aq6w&M)S5yd5P$gx( z0_|N~=Nu>{tFo!ge8Z+oOjJ|Jn?UrrIg(k35B=2&5B+Br7%i$FXaoBetDGHtf1{$J zq6s|iX^D9zT_Sr~Ry06zZkQXnQB?=1{Tw-5Yn$X+SC}-wAlq)?(mb}d?u_~#-5q>; zqms06nQjR&z4%CQPcudIVFT(ltKDZsNQ8O)2gA;rQ}kYB=k;Dbj`1_L3$&(2oRD_4 zK1?~6n~a6JT;uo2mPZ&&$%iDAk=;us64akjiMG#>G-Fxn6hZ#gg zT_oUsM|Uv5AytrQDsO8me_#(u0bzp8X)fsju2`Hr5!I@x1SA*ucfb;vv zS&@HLl@4EK_ikxvX}y!10G_jHsXL!TC;{}Uc{lY~`}>YK<}z%s0<_EV0_<7IjB!`! zdh@lzbnW7X7Rc_juMwHDY9ZgcKlCGxHEFB*(>YJwIw<2Jl-5#4CJYd%?E-_RTNcD! zEH+3DG(k|GV-K*Nnk<`*yXEcV=y05_GPI<+dTdb%>w1TX2#dvq zlT>>D>_YhkMd8nss><`kKtfakJDVSr=cE)d5i&M91&GUNR*X9}*T*DQmk#!G9&E$t zcM4x-v!GTToCxL$e{BHwk_geSBlt(^oN%+2#-HBpyKniH$(2@gbe?mShsy)7zOB!+ zdlT2mIpIl7KS-t)KKY}s5B}6A;$Td_-t(z%a0*5xG4A?=%H9=vt+Aq)_K18StOO-{ z*UbhKNRena!KU<~Sa14eW(~QNP8^eLjCjSys9<~_5rr5ah=VQ9$ObW21TIez zj}3RFh*=jTjSBRqzREYRg1tY~u2QJUeGFEFRR$8S=T?+-J)H1*YLlX!tkz>iovoBh zSLMb>XSa(XP+A5X2i5+(LJ(+0p3*HUnP0vq>DqrfeZCAGB%R9PJ~K359jG|Iop_%7 z^5csEh6M6igB9RSjpG9q2JC9ykWARJ`e=M&qQ}F{>|*#l*^*PT{lM^W7nzWfu-D}Q zcylYo{L=E2*hE`73k{aGP~RG}FUv9tQqrhi7$^kS*fs(Yzw`!Gh4rHh&bUS}G}8!} zGgy8;keMmsf3dt{tqZEi;a!~W*3t>be+3x>3>{^j(w_l13E0>gwo3?e1+$2SiP+sO zztZs}!A5peMytc~nejA}e$RMrVJQYdGK6c^V!Y(Qz(wbgWU`n5C8Ww~o$ zdZ>jKp7P>DzaJyLCsHbWV7J5i*>5*La&#HKh0ZY+!x6d9`8EWae!E(;ZC?oXw)Zt< zNR|D>QE8+5vAxIZv}lV+KF_syndR^KNx8wPZVUFCE?R1b)~R-4PpVFblpk ze+T#^zw&Z3@?#E}oC{EhDxCWX;N%$z(I6&tE2Sq~1V^zn+vywerkE_{5Fnli=GbQk zOLExC&8CD()Bd)HPS{U7nw8In2d28(Pl$YI*P;{u^J$j|ZZ)&RlmD#jz8v|RT^O}RXvY-5f&wGo?z4}m%6s4+ z@)AmyyVrC9_Wxo9F%c#OM$)}0p;B?TMH?|{NQ5D=-KJkLdK6Hbx3NlC@&H3Qm8A?W zl?l}Roxg}NZkOX}?3X_7uTeKcy zSEEAh-2#?BO5}avzLb_e$(1#jdI+FoLAS6hu6r{$zy%vjo&8GD zo?3XC?kQqP0)%zRfuZQz6Z6wyk&4iqYHHB40jux?n2ZlXLtVEc_jMYSpYHR$6>mx}cVE8qU_pgHjYlIK-lU=M+WIRPip(72c%>XQrCZ_vw7!--& z;9R{$7bUr+Geb3-7sUF#VeOEtiIRyLw`*th}0B2)=meYh40Hhft!uQT>zTwS0wrYtnD|E$N3Y`e13s4Jxg* zpc zVF-=7g@U3zz40m`E-@|2G@$Sv`B+PTylSsi%F{5d!e$Yzr1_t<-=8$3h1Djtrp<_{ zyoTni!a&BQ>h*12L_|6<>8Ut4kW!hcAtE9MZWT#bQQCY%0=!NvN9!*zW!>&fRHIz~ zqI< zW12DY*BZM#&oP}ua(R7$PM}-xkfBkLGUcEH$>VP70GhO(k8|&J)0eG9PgRa2gcP;3 zC8z_;zfN@*G3O?Wsph9uHqI@+zBSINV-%^DJ0|d1aAr3`NhsdmIDEj~-yCzdpEC%+WyjkEzb@K zisO|s0Cy?f0HaWx*N?qk_ffXX&IFAncVb)Tg%7`T%hU_^u){!c;x6$Ik2JkYMVhej zX2KZf^mI~U1dPGW)T&@5r8JnKso3f_ zkAtY#tkrSY*DI_vKwv*Bu!_qpsyrh?=_P|rauVk7&*s;rrcCz1XoS9q)?8ZCgPBk{YTuZNgq;)6eB^775; z0o2-1{F%vB>we}l`^>#lN_^j8VLc~Q0T#=2Od4uApXuojkE}o*B#D>oh)q6g zzO44myd1g>#{s3awK}Q(C-?NsINL^A;n)da!AmO#Dsr+>R(dP% z)ms~K+jskNN-pD5&nVTGXYf@MA@pj807%1p{Mip}oy%){Yb5yMzZ4LMkWmXf&g?Ez zJh@-XxV*+~^{^P^ZgVhw@puHq3wXc0?07ufv0rb}0E1krf&~s%dkH*knb(`A2MUP^ zh&@ASMz!!g?jZ@%6+z0!IO7+V$9D&d!~0e1<@#UsuF9#Rdp9OpgNuOIAXSM(KwAgK zeQ{zPg|jV~lldt^3W$=8KT#H6p21+F*RDgtC?oB1byVjw z3TZAGtDxWN3pY>-R(;Vi5Ztk;wMX&fRLmOB@JP- z{EYpOMcSd{{`KKgit|udc6Mwm;QECd51UF#k>=4mDvw-IK5}A)5*y2rf`Z#ni(Fb* zUHuN|)?#HPA}U%E8-uY3%hT3hZ@1Q_%zvk${5nR~GoJRjOv;SK@N_MneKcY(`n)PX zSHQFvg~K3mP1y=RKuyhNAdi>W{mad0e+mK=55a-e8MlY{z>cOBs=}c+;4R+0$#zge zJZzjuKxY~Q+`L@6XAgL?koV3g|7}uS{I~otunuBalEzz&qtre2sxYW=cg_wg7dGS7F=ke@xR(`E>-71fj33X^{bSB z<#~o0zdc-fyS%<=?%;X3c~26=S3l7a0Q6Bz#NTF<5Y3h zSE|yYZji`N!fXISc4L|+Fts9a2YX5>;$oEedSj|dLrEAz55$1N6n?T-pAq98%sPLh zzS%s;i{Rvnh|poik-mY(dFoS+{m24Id5HNuw~d|eJ!fV{G8MCY#$G%N?W0>;d3u^u z;KeC#{c%u!9KTIdN0z6yMtVfya!!vz^zt<4*2*aiLzDqLB^B@Gc~M~qdBE;{gY#zNQ(pln?qGx?hP-Bp=h_twSWwTk(bH?zFWQN#f_pZUXH=x{DHzvPeZk=VkZ zR)xFUeufpBg0gQ#L7^=*9S=eo7#dmf5GSnV=0JexzI^~FItZgKT9ICHpBj30f;KIt zvz0#s|CHN@YU82T_PU>b>Q_TfTVay>9@=)YjTUcrG}gVbG)8j96X{EZz5WMY8)k0y z(UrMOP*9Vt+byvM|=rC-Ee0N*Mkq^C|l$kLjAsduDKo zY=K5HzRwfuO+-uR;m8L7hnpW$ zpqihFX+FO*`GK+A3Hqgq#xGAdeNcf#gAw2|Uzxk!#m@0?Y#TG#-py_`120@$<+v`` zY=5`OajFFVrqR7nK2jgLxAr7>USE35+OY5BmA`vEMqyfGd|6%!TFKvZ>S_Z~_zeDPR4OS`hENnI9dQg0D(b+x$#2-;Urz_j@dEIZ28`GaH)tve#zR~Y1nod*PY8nUK z!XU%W=f)QSHPVv`#OTu)*aJJh3TxOE7MLzH9dUrPD74(}q-2a?>e+4R2{N{%K%-OeI8`Qfd zFZ%|PFD!)^jOs@ZbIgz7*bd>0I}5<5P2!sC=?NqK6xGE#DLd-wau>UOY0{#rMXV7; z1C_mz^qqq^S@Krbsd>UrUMV{!|3I{ib(*OYau--ZCd*>Qe z*tgN~dU!n+tO(|AX_)kb9Zu&w&T&7o7wVXptE%t8qr5OGbz@$Tp0~fcBOF+_9m;>a zSwhHhxlU2UBqZo**r}boCoe?Cpa-O&P1Jr_DsmZq)kzjL7X&h6=9Wd<_bj6#pf`2$ zn-4yL=NK50#kr9+00#%tkCFO74ZAh6;$4-mO_f=2j=QqnMh2h~)a>?|xe0d+#|&(# zRD-%t}*#v zqF|M%LcR8dqYcCgI)0=4qm=e&$)<9)&efGmH#8#OR<0lcbQ&!0xA)Nlz2BB#RCAeV zV_C#u0s(uKKaZf7&T4B}*=Qc*2{#F@sk(sUA@?ML%X3ZsYrBv1uOzzMX3~lPkr)}8 zt=A%$H@HINV@q4h4+hyCZ`C%-u`R|=+S(}z4Z}sY%dGv8zwos>zqTASM7W>GLZ(J} z`wvjVXCe~-7=on$sIw*X-?O*l`)*E!CWangBE8%;7qYj7isT6=S`=L3AJgxe9BpRW z&}UVhjyH#^i#An*olybrv>O%|rH9=qQpD|^Zkv6t5mk$QuoM0}%?mo5<*Ir|QE=md zLK=ZP+EK!s=OjE{L{CD%`X5Ipn=|H9Q=`{?tREQxOtdcZp|UL=GY_4xNuRtsa-!Xa zjk}tr*y@5@i{_xAHotl-HfDy?dwlHaS(MGSB;&S5e|#0H>aT|>>#8w2+u7{khlegH z4Pi$KR+fq2j;a|!v>fKP(EdQXgBajV)`0zo8toJO!v(?_q@Q6Y!eqL4IP}z*!dfjK zaan99fU!H5FhXMMhozrbYHf+4HtCPUu%BlVX0R){LLzjDV1xKqDOn8e)b-Vds-Cc) z5ZaF5Xv^_^6wG=COLcgRzF1|nTbb)v+l%|&?yyy3Bdez;A#rqeW+n+qHV$Sy z0cN+N)P}h!x>6H9?&swt{JY~Kv+u%cAJoqt0fWQfh)tM}{{$f**_i3zaO8dLZW|t+ z-cAPbqSIzy)+LGXI?TS%Li?+I?{S)4_;$#3Bjq6?6~}EI-k!;nP@cX(=H$_tEN4 z$b)zWw4GgqtLwnW#{eNAY~8CXG$94~ikD(vf*OPp*IO%%*q#LOboCjbQM#-F(1*T| zU{1)AEi8lT0LpOcdyIM>4AyCs#lh4Oz=^q;nMNp?E0sn~{-~k|Q?Mr{9zpm|eF%RQ z$(tJrPX{9G3{gdFEYya4b6#?(-49yILLf^`74%mUm?`IdGr7`#1V46fU9v7N6Zv*v zsJEZ4$gd$DNPlLI)ebGHp9QB=67#JHLhBB-A&d2s1-$=@8yYAiB+a~sR$B)k%n)`E zL^YTO5GxJVq7GxQ#^$a0icygwOv|V;kjp1_1n>MQ+EbY!SfJQ~8iSuPUSAz5c!4Yt znPRLgN=O84r|QpIkmqaiMwJ2XM`5&`XrcOQ4z`XcUw-9Sn9!`BlqzCdgHj@S7UjC3 zroQ0_1>}~Llr1w)-IT$I_@QWk5{e-kv8w6L-w9}@a(QoYgg?BnVK7u$E~KquED#j` zOBk0z2wTGwZ~CVdc_!_k=j&-zZ}iKDg-S#&p*Ux7_^X9SYw-*F8#<9HDcq$Jh!HT*R$j z3{zu@aYKZG+AJdnreUAVD5_$3eY zMnA~uRZ4z7e$Hk#Tqze4sri>}p8yc657V1GHWELQvJt?TN}|*z4OO=`)W@MGwxDrA z!Dz4{8q77ts3UET8U7uBlT3ld0vT#jfS$}sjUK@hmmpywEJBB4hN6O?s9ed6jT?yh zAHMB?0U|G)^oiM5jNjo>OITtu43hm3RomgeV}IV6=hK-9=fN&~f%}PD0%*WV(dbvH z4UrsG$V)Qcn6O?#zp*C%EFLfg|7eN@4+ArT`021J4h53^-NS{= z4fKr*oNPS`QsGp-TEpImVpaaX_i$q&QM$HS%X~yFJS$+C*vg~Qaw#drn1dNrB)M8q zO1wY9kj6axjjaIlkJaE6E_YAFoMLc;-Me@uA=e>g-b2)D;sxMBYw5q>OOu)eO(;uH zfO-7ip)m%TMmn*pMmzDNs+Wl$v9Ue|25x=G<$^=_%a*1knX?cLGpDFI3SrPL_0t|x z7{HHB!ReJDGU9xef%hZ*hA>d3t90_WJ>jo_!ZBoj$>9H-@&DV~!GDv#f%i3gf0she iPyk26!2UBoLP0$4RZvxfalwBAWTeFv#41G${r?9sYz&$J delta 68429 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 { ).toHaveCount(0); }); + test("downloads the macOS update and preserves a paused connection", async ({ + page, + }) => { + await page.addInitScript(() => { + Object.defineProperty(navigator, "platform", { get: () => "MacIntel" }); + }); + await openFresh(page); + await connectButton(page).click(); + const pause = page.locator("[data-connection-action='pause']"); + await expect(pause).toBeEnabled(); + await pause.click(); + await expect( + page.getByRole("heading", { name: "Paused", exact: true }), + ).toBeVisible(); + await goTo(page, "Settings", "About"); + await page.evaluate(() => + sessionStorage.setItem("biflow-mock-update-available", "1"), + ); + await page.getByRole("button", { name: "Check for updates" }).click(); + await page.getByRole("button", { name: "Download 9.9.9" }).click(); + await expect( + page + .getByRole("region", { name: "About", exact: true }) + .getByRole("status"), + ).toContainText("Drag BiFlow onto Applications"); + await expect( + page.getByRole("button", { name: "Check for updates" }), + ).toBeEnabled(); + await goTo(page, "Home"); + await expect( + page.getByRole("heading", { name: "Paused", exact: true }), + ).toBeVisible(); + }); + test("keeps a subdomain pin exact while connected", async ({ page }) => { await openFresh(page); await goTo(page, "Routing", "Sites"); diff --git a/package.json b/package.json index 284ff02..233659c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "iran-split-desktop-workspace", - "version": "6.2.79", + "version": "6.2.80", "private": true, "packageManager": "pnpm@9.0.1", "engines": { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b0dced6..6099886 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -2807,8 +2807,15 @@ fn verify_direct_rules_after_upgrade(data: &Path) -> Result<(), String> { .map_err(|cause| format!("could not clear route-pin upgrade guard: {cause}")) } -async fn restore_stack_after_failed_install(services: &AppServices) { - if matches!(services.engine.snapshot().phase, StackPhase::Paused) { +fn failed_update_requires_resume(kind: github_update::InstallKind, phase: StackPhase) -> bool { + kind != github_update::InstallKind::Dmg && phase == StackPhase::Paused +} + +async fn restore_stack_after_failed_install( + services: &AppServices, + kind: github_update::InstallKind, +) { + if failed_update_requires_resume(kind, services.engine.snapshot().phase) { if let Err(cause) = services.engine.resume_stack().await { warn!( event = "update.stack_restore_failed", @@ -2819,6 +2826,16 @@ async fn restore_stack_after_failed_install(services: &AppServices) { "paused stack could not be restored after a failed install" ); } + } else { + info!( + event = "update.stack_restore_skipped", + section = "updates", + initiator = "install_update", + cause = "no_installer_pause_to_restore", + trace_route = "install_update->restore_stack_after_failed_install", + install_kind = ?kind, + "failed update preserves the existing stack state" + ); } } @@ -3041,7 +3058,7 @@ async fn apply_downloaded_package( match github_update::apply_package(kind, package, ¤t_exe, &target_version).await { Ok(outcome) => outcome, Err(cause) => { - restore_stack_after_failed_install(services(app)?).await; + restore_stack_after_failed_install(services(app)?, kind).await; emit_update_progress( app, UpdateProgress { @@ -4305,6 +4322,21 @@ mod tests { assert!(!exit_releases_owned_network(true)); } + #[test] + fn failed_disk_image_open_preserves_a_user_paused_stack() { + use github_update::InstallKind; + + assert!( + !failed_update_requires_resume(InstallKind::Dmg, StackPhase::Paused), + "opening a DMG never pauses the stack, so failure must not resume it" + ); + for kind in [InstallKind::Deb, InstallKind::AppImage, InstallKind::Nsis] { + assert!(failed_update_requires_resume(kind, StackPhase::Paused)); + assert!(!failed_update_requires_resume(kind, StackPhase::Running)); + assert!(!failed_update_requires_resume(kind, StackPhase::Stopped)); + } + } + #[test] fn macos_disk_image_update_stays_running() { let source = include_str!("lib.rs"); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 2fac5bd..a2a754a 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "BiFlow", - "version": "6.2.79", + "version": "6.2.80", "identifier": "app.biflow.desktop", "build": { "beforeDevCommand": "pnpm bundle:check && pnpm --dir apps/desktop dev", diff --git a/version b/version index d3487ae..6d2db09 100644 --- a/version +++ b/version @@ -1 +1 @@ -6.2.79 +6.2.80