Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

[简体中文](CHANGELOG.zh-CN.md) · English

## v0.9.3 — 2026-11-13

- Fixed Chrome Beta being incorrectly reported as Stable or showing a channel mismatch after installation; the same correction applies to Edge and Brave.
- Switching the selected channel no longer changes the recorded installed channel. Channel-switch and downgrade confirmations remain separate.
- For Chrome, Edge, or Brave installed with an older Browser Harbor version, reinstall the desired channel once using v0.9.3 to establish its channel record.

## v0.9.2 — 2026-11-08

- When an installation or upgrade is blocked because the browser is still running, the message now lists the detected process names and PIDs (up to three), so you can see exactly what is holding the program files.
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

[English](CHANGELOG.md) · 简体中文

## v0.9.3 — 2026-11-13

- 修复 Chrome Beta 安装后被误判为 Stable 或反复提示“渠道不同”的问题,同时修正 Edge、Brave 的同类识别逻辑。
- 切换所选渠道不再影响已安装渠道记录;切换渠道与降级仍分别要求确认。
- 旧版 Browser Harbor 安装的 Chrome、Edge、Brave,请使用 v0.9.3 重新安装所需渠道一次,以建立渠道记录。

## v0.9.2 — 2026-11-08

- 安装或升级被“浏览器仍在运行”挡住时,提示现在会列出检测到的进程名称与 PID(最多 3 个),便于确认是哪个进程占用了程序文件。
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "browser-harbor"
version = "0.9.2"
version = "0.9.3"
edition = "2024"
rust-version = "1.99"
license = "MIT OR Apache-2.0"
Expand Down
4 changes: 3 additions & 1 deletion docs/USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ Browser Harbor checks the browser version and download integrity before changing

Downgrading also requires confirmation: an older browser may be unable to read user data migrated by a newer version. Back up your data before proceeding.

If the selected channel differs from the channel detected in the existing local executable, Browser Harbor asks before replacing the program directory.
If the selected channel differs from the verified installed channel, Browser Harbor asks before replacing the program directory. Switching the channel selector does not change which channel is already installed; downgrades still require a separate confirmation.

Chrome, Edge, and Brave channel records are checked against the installed program files. Older installations without a record cannot be reliably identified from a generic product name alone. Reinstall the desired channel once through Browser Harbor v0.9.3 or later to establish its record. External changes to the program files may invalidate that record.

The browser page checks the selected target, channel, and build once per session. A manual check is still available. An installation started within ten minutes can reuse the verified upstream result.

Expand Down
4 changes: 3 additions & 1 deletion docs/USER_GUIDE.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ Browser Harbor 会在修改安装前检查浏览器版本和下载完整性。

降级也需要明确确认:旧版浏览器可能无法读取已被新版迁移的用户数据,请先备份再操作。

如果所选渠道与本地 EXE 探测到的渠道不同,Browser Harbor 会在替换程序目录前要求确认。
如果所选渠道与已核验的安装渠道不同,Browser Harbor 会在替换程序目录前要求确认。切换渠道下拉框不会改变已经安装的渠道;涉及降级时仍需单独确认。

Chrome、Edge 和 Brave 的渠道记录会与实际程序文件核对。旧安装没有渠道记录时,无法仅凭通用产品名称可靠识别;请使用 Browser Harbor v0.9.3 或更新版本重新安装所需渠道一次,以建立记录。外部替换程序文件后,原记录可能失效。

进入浏览器页后,程序会对当前目标、渠道和构建自动检查一次;也可以手动刷新。十分钟内开始安装时会复用已验证的上游结果。

Expand Down
27 changes: 27 additions & 0 deletions docs/releases/v0.9.3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Browser Harbor v0.9.3

## 下载 · Download

普通使用只需下载 **BrowserHarbor-0.9.3-x64.exe**。Windows 10/11 x64,无需安装。

Download **BrowserHarbor-0.9.3-x64.exe** for Windows 10/11 x64. No installation required.

[GitHub](https://github.com/MagicCube99/browser-harbor/releases/tag/v0.9.3) · [Gitee 国内下载](https://gitee.com/MagicCube99/browser-harbor/releases/tag/v0.9.3)

## 本次更新 · What's new

- 修复 Chrome Beta 安装后被误判为 Stable 或反复提示“渠道不同”的问题,同时修正 Edge、Brave 的同类识别逻辑。
- 切换所选渠道不再影响已安装渠道记录。安装渠道不同会先提示切换,涉及降级时仍需单独确认。

- Fixed Chrome Beta being incorrectly reported as Stable or showing a channel mismatch after installation; the same correction applies to Edge and Brave.
- Selecting another channel no longer changes the recorded installed channel. Channel switching and downgrading still require separate confirmations.

## 升级 · Upgrade

替换旧 EXE 即可,无需删除 `BrowserHarbor.json` 或浏览器用户数据。

**旧安装提示:** 对于旧版 Browser Harbor 安装的 Chrome、Edge、Brave,请使用 v0.9.3 重新安装所需渠道一次,以建立渠道记录;之后重启工具或切换 Stable/Beta 即可核验已安装渠道。

Replace the old EXE without deleting `BrowserHarbor.json` or browser user data.

**Existing installations:** For Chrome, Edge, or Brave installed with an older Browser Harbor version, reinstall the desired channel once using v0.9.3 to establish its channel record. The installed channel can then be verified after restarting the tool or switching between Stable and Beta.
2 changes: 2 additions & 0 deletions src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,7 @@ impl AppState {
})?;
config.installed_version = Some(receipt.version.clone());
config.catsxp_build = receipt.catsxp_build.clone();
config.installed_channel = receipt.installed_channel.clone();
config.browser_directory = Some(receipt.browser_directory.clone());
config.data_directory = Some(receipt.data_directory.clone());
config.profile_directory = Some(receipt.profile_directory.clone());
Expand Down Expand Up @@ -707,6 +708,7 @@ mod tests {
let receipt = crate::services::install::InstallReceipt {
version: "1.2.3".to_owned(),
catsxp_build: None,
installed_channel: None,
browser_directory: PathBuf::from(r"D:\Browsers\Chrome\APP"),
data_directory: PathBuf::from(r"D:\Browsers\Chrome\Data"),
profile_directory: PathBuf::from(r"D:\Browsers\Chrome\Data"),
Expand Down
120 changes: 115 additions & 5 deletions src/app/operation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ pub enum OperationPhase {
CatsxpIdentified {
observation: Option<crate::services::catsxp::Observation>,
},
ChannelIdentified {
observation: Option<crate::services::installed_channel::Observation>,
},
ChannelMismatch {
current_version: String,
current_channel: ReleaseChannel,
Expand All @@ -73,6 +76,7 @@ pub enum OperationPhase {
current_version: String,
upstream: UpstreamCheck,
release: ReleaseDescriptor,
channel_change_confirmed: bool,
},
Installed {
receipt: InstallReceipt,
Expand Down Expand Up @@ -238,13 +242,15 @@ pub fn start_operation(request: StartOperationRequest) -> OperationController {
current_version,
upstream,
release,
channel_change_confirmed,
}) => {
let _ = sender.send(OperationEvent::new(
target,
OperationPhase::SameVersion {
current_version,
upstream,
release,
channel_change_confirmed,
},
));
}
Expand Down Expand Up @@ -382,7 +388,28 @@ async fn run_operation(
}
let provider = ProviderResolver::new(http.clone());
let mut current_version = request.current_version.clone();
let mut catsxp_channel = None;
let mut installed_channel = None;
if matches!(target, TargetId::Chrome | TargetId::Edge | TargetId::Brave) {
let observation = if let Some(program) = request.target_config.browser_directory.as_deref()
{
match crate::services::installed_channel::observe(
target,
program,
request.target_config.installed_channel.as_ref(),
&cancellation,
) {
Ok(value) => value,
Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {
return Err(Box::new(crate::services::ServiceError::Cancelled));
}
Err(_) => None,
}
} else {
None
};
installed_channel = observation.as_ref().map(|value| value.identity.channel);
emit(OperationPhase::ChannelIdentified { observation });
}
if target == TargetId::Catsxp {
let observation = if let Some(program) = request.target_config.browser_directory.as_deref()
{
Expand Down Expand Up @@ -413,7 +440,7 @@ async fn run_operation(
})
.map(|probe| probe.version)
});
catsxp_channel = observation.as_ref().map(|value| value.build.channel);
installed_channel = observation.as_ref().map(|value| value.build.channel);
emit(OperationPhase::CatsxpIdentified { observation });
}
let mut metadata_source = MetadataSource::Official;
Expand Down Expand Up @@ -548,9 +575,8 @@ async fn run_operation(
});
}
if !request.channel_change_confirmed
&& !request.replace_same_version
&& let Some(current_channel) =
catsxp_channel.filter(|channel| *channel != descriptor.channel)
installed_channel.filter(|channel| *channel != descriptor.channel)
{
return Ok(OperationResult::ChannelMismatch {
current_channel,
Expand All @@ -564,7 +590,7 @@ async fn run_operation(
&descriptor.version,
) || (!request.replace_same_version
&& target == TargetId::Catsxp
&& catsxp_channel.is_none()
&& installed_channel.is_none()
&& crate::services::catsxp::base_version(&descriptor.version, descriptor.channel)
.as_deref()
== Some(current_version)))
Expand All @@ -573,6 +599,7 @@ async fn run_operation(
current_version: current_version.to_owned(),
upstream: check,
release: descriptor,
channel_change_confirmed: request.channel_change_confirmed,
});
}

Expand Down Expand Up @@ -1057,6 +1084,7 @@ enum OperationResult {
current_version: String,
upstream: UpstreamCheck,
release: ReleaseDescriptor,
channel_change_confirmed: bool,
},
Installed(InstallReceipt),
ChromePlusChecked {
Expand Down Expand Up @@ -1892,6 +1920,7 @@ mod tests {
checked_at: 0,
},
release: descriptor(),
channel_change_confirmed: false,
}
.is_terminal()
);
Expand Down Expand Up @@ -2059,6 +2088,87 @@ mod tests {
);
}

#[tokio::test]
async fn verified_beta_record_blocks_stable_before_download_and_requires_downgrade_confirmation()
{
let directory = tempfile::tempdir().unwrap();
let target = crate::domain::target::TargetId::Chrome;
let root = directory.path().join("Chrome");
let program = root.join("App");
std::fs::create_dir_all(&program).unwrap();
std::fs::copy(std::env::current_exe().unwrap(), program.join("chrome.exe")).unwrap();
let version = crate::services::version_probe::probe_local_version(target, &program)
.unwrap()
.version;
std::fs::create_dir(program.join(&version)).unwrap();
std::fs::write(program.join(&version).join("chrome.dll"), b"beta core").unwrap();
let mut config = crate::domain::settings::TargetConfig::for_target(target);
config.browser_root = Some(root);
config.derive_paths_from_root(target);
config.installed_channel = crate::services::installed_channel::capture(
target,
&program,
&version,
ReleaseChannel::Beta,
)
.unwrap();
// A nonexistent local package proves confirmation happens before artifact access.
config.local_package = Some(directory.path().join("must-not-open.zip"));
let mut release = descriptor();
release.target = target;
release.channel = ReleaseChannel::Stable;
release.version = "0.0.1".into();
for (confirmed, replace_same_version) in [(false, false), (false, true), (true, false)] {
let (sender, receiver) = std::sync::mpsc::channel();
let result = super::run_operation(
super::StartOperationRequest {
target,
channel_change_confirmed: confirmed,
mode: super::OperationMode::Install,
target_config: config.clone(),
network: Default::default(),
cache_dir: directory.path().join("cache"),
current_version: Some(version.clone()),
chrome_plus_expected_version: None,
replace_same_version,
allow_local_package: true,
allow_signed_metadata_cache: true,
cached_release: Some(CachedReleaseMetadata {
check: super::UpstreamCheck::fresh(
"0.0.1".into(),
ReleaseChannel::Stable,
CpuBuild::X64,
),
release: release.clone(),
observed_at: unix_now(),
}),
},
sender,
std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
)
.await
.unwrap();
if confirmed {
assert!(matches!(result, super::OperationResult::SameVersion { .. }));
} else {
assert!(matches!(
result,
super::OperationResult::ChannelMismatch {
current_channel: ReleaseChannel::Beta,
..
}
));
}
assert!(receiver.try_iter().any(|event| matches!(
event.phase,
OperationPhase::ChannelIdentified {
observation: Some(_)
}
)));
assert!(!directory.path().join("cache").exists());
}
}

#[tokio::test]
async fn unsupported_libportable_is_rejected_before_using_a_browser_package() {
let directory = tempfile::tempdir().unwrap();
Expand Down
11 changes: 11 additions & 0 deletions src/domain/release.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ pub struct CatsxpBuildIdentity {
pub core_sha256: String,
}

/// Local receipt evidence, not a replacement for artifact verification.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InstalledChannelIdentity {
pub target: TargetId,
pub version: String,
pub channel: ReleaseChannel,
pub executable_sha256: String,
pub core_sha256: String,
}

impl ReleaseChannel {
pub const fn display_name(self) -> &'static str {
match self {
Expand Down
5 changes: 4 additions & 1 deletion src/domain/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::{collections::BTreeMap, path::PathBuf};
use serde::{Deserialize, Serialize};

use super::{
release::{CatsxpBuildIdentity, CpuBuild, ReleaseChannel},
release::{CatsxpBuildIdentity, CpuBuild, InstalledChannelIdentity, ReleaseChannel},
runtime::ChromePlusConfig,
target::TargetId,
};
Expand Down Expand Up @@ -117,6 +117,8 @@ pub struct TargetConfig {
pub installed_version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub catsxp_build: Option<CatsxpBuildIdentity>,
#[serde(skip_serializing_if = "Option::is_none")]
pub installed_channel: Option<InstalledChannelIdentity>,
pub local_package: Option<PathBuf>,
pub chrome_plus: ChromePlusConfig,
pub chrome_plus_version: Option<String>,
Expand Down Expand Up @@ -228,6 +230,7 @@ impl Default for TargetConfig {
cpu_build: CpuBuild::X64,
installed_version: None,
catsxp_build: None,
installed_channel: None,
local_package: None,
chrome_plus: ChromePlusConfig::default(),
chrome_plus_version: None,
Expand Down
Loading
Loading