Compare commits

...
1 Commits
Author SHA1 Message Date
toorux f4886c4de2 fix(installer): handle nonblocking accepted sockets in download tests
Wait for complete bounded HTTP headers in the shared test server, cover delayed fragmented requests, and release installer 0.4.3.
2026-10-18 02:34:04 +08:00
7 changed files with 59 additions and 17 deletions

No files matched your search

+3 -3
View File
@@ -39,7 +39,7 @@ Deleted `/home` data cannot be recovered. OS updates may require manual interven
Device and installer share the repository with independent versions/tags/workflows:
- Device: root Cargo version, `v<version>` (currently `v0.8.3-preview.3`), `.github/workflows/release.yml`, Linux ARM64 and pinned CEF.
- Installer: `installer/Cargo.toml`, `installer-v<version>` (currently `installer-v0.4.2`), `.github/workflows/installer-release.yml`, Linux x64/ARM64, Windows x64, macOS Intel/Apple Silicon, pinned GPUI Kit.
- Installer: `installer/Cargo.toml`, `installer-v<version>` (currently `installer-v0.4.3`), `.github/workflows/installer-release.yml`, Linux x64/ARM64, Windows x64, macOS Intel/Apple Silicon, pinned GPUI Kit.
Versions need not match or ship together. Update the relevant Cargo.lock with version changes. Installer package/macOS versions use the installer version.
@@ -50,8 +50,8 @@ Both workflows cache dependencies/builds by product/platform/toolchain/configura
git tag v0.8.3-preview.3
git push origin v0.8.3-preview.3
# Independent installer release:
git tag installer-v0.4.2
git push origin installer-v0.4.2
git tag installer-v0.4.3
git push origin installer-v0.4.3
```
Prerelease suffixes create GitHub Prereleases without Latest. Installer releases are never Latest. Only stable device releases become Latest, keeping default installer/update URLs on the device product. The desktop installer filters device archive names and does not mistake ARM64 installer packages for device runtimes. Download desktop installers from their explicit installer tags.
+3 -3
View File
@@ -49,7 +49,7 @@ CEF 按内容哈希保存在 `/home/.framely/cef/<运行库标识>`,更新时
Framely 和桌面安装器保留在同一仓库,使用独立的版本号、标签和发布工作流:
- Framely:版本由根 `Cargo.toml` 定义,标签为 `v<版本>`(当前 `v0.8.3-preview.3`);`.github/workflows/release.yml` 只构建 Frame Linux ARM64 发行,使用锁定的 CEF 并执行自动检查。
- 安装器:版本由 `installer/Cargo.toml` 定义,标签为 `installer-v<版本>`(当前 `installer-v0.4.2`);`.github/workflows/installer-release.yml` 只构建 Linux x64/ARM64、Windows x64、macOS x64/ARM64 的原生安装器,使用锁定的 GPUI Kit。
- 安装器:版本由 `installer/Cargo.toml` 定义,标签为 `installer-v<版本>`(当前 `installer-v0.4.3`);`.github/workflows/installer-release.yml` 只构建 Linux x64/ARM64、Windows x64、macOS x64/ARM64 的原生安装器,使用锁定的 GPUI Kit。
两个版本不需要相同,也不需要同时发布。修改各自版本时同步对应的 `Cargo.lock`;安装器包名及 macOS 应用版本使用安装器版本。
@@ -63,8 +63,8 @@ git tag v0.8.3-preview.3
git push origin v0.8.3-preview.3
# 独立发布安装器,不触发 Framely 构建
git tag installer-v0.4.2
git push origin installer-v0.4.2
git tag installer-v0.4.3
git push origin installer-v0.4.3
```
带 `-preview.N` 等预发布后缀的标签会发布为 GitHub Prerelease,不设置为 Latest。安装器 Release 始终设置为非 Latest;只有 Framely 正式 Release 设置为 Latest。因此设备安装入口和默认更新清单的 `releases/latest/download/...` 继续指向 Framely 本体。安装器读取发行列表时按本体包名筛选,不把 Linux ARM64 安装器误当成设备发行包。安装器下载应使用具体的 `installer-v<版本>` Release。
+1 -1
View File
@@ -1985,7 +1985,7 @@ dependencies = [
[[package]]
name = "framely-installer"
version = "0.4.2"
version = "0.4.3"
dependencies = [
"anyhow",
"base64 0.22.1",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "framely-installer"
version = "0.4.2"
version = "0.4.3"
edition = "2024"
license = "AGPL-3.0-only"
+1 -1
View File
@@ -64,7 +64,7 @@ FRAMELY_INSTALLER_PREVIEW_DIR=installer/dist/previews installer/target/debug/fra
Preview requires an available GPU or software Vulkan renderer. Default release builds do not include preview support.
Installer versions are managed independently in this directory's `Cargo.toml` and `Cargo.lock`. Pushing an `installer-v<VERSION>` tag (currently `installer-v0.4.2`) triggers `.github/workflows/installer-release.yml` to build and publish only the installer. The core uses `v<VERSION>` tags and a separate workflow. Manual Actions runs produce build artifacts only. Installer releases do not become the repository's Latest release, preserving Frame's default install/update URLs.
Installer versions are managed independently in this directory's `Cargo.toml` and `Cargo.lock`. Pushing an `installer-v<VERSION>` tag (currently `installer-v0.4.3`) triggers `.github/workflows/installer-release.yml` to build and publish only the installer. The core uses `v<VERSION>` tags and a separate workflow. Manual Actions runs produce build artifacts only. Installer releases do not become the repository's Latest release, preserving Frame's default install/update URLs.
Actions builds Linux x64/ARM64, Windows x64, and macOS Intel/Apple Silicon. macOS produces an `.app` ZIP, Windows an EXE ZIP, and Linux a tar.gz. Apple notarization and Windows code signing are not configured; verify first-launch behavior on each system before publishing.
+1 -1
View File
@@ -64,7 +64,7 @@ FRAMELY_INSTALLER_PREVIEW_DIR=installer/dist/previews installer/target/debug/fra
预览依赖可用的 GPU 或软件 Vulkan 渲染器;默认发行构建不包含此预览功能。
安装器版本由本目录的 `Cargo.toml` 和 `Cargo.lock` 独立管理,不跟随 Framely 本体版本。推送 `installer-v<版本>` 标签(当前 `installer-v0.4.2`)触发 `.github/workflows/installer-release.yml`,只构建并发布安装器;本体使用 `v<版本>` 标签和独立工作流。也可在 Actions 手动运行安装器工作流,仅生成构建产物。安装器 Release 不占用仓库的 Latest,以免影响 Frame 的默认安装和更新地址。
安装器版本由本目录的 `Cargo.toml` 和 `Cargo.lock` 独立管理,不跟随 Framely 本体版本。推送 `installer-v<版本>` 标签(当前 `installer-v0.4.3`)触发 `.github/workflows/installer-release.yml`,只构建并发布安装器;本体使用 `v<版本>` 标签和独立工作流。也可在 Actions 手动运行安装器工作流,仅生成构建产物。安装器 Release 不占用仓库的 Latest,以免影响 Frame 的默认安装和更新地址。
Actions 构建 Linux x64/ARM64、Windows x64、macOS Intel/Apple Silicon。macOS 输出 `.app` ZIP,Windows 输出 EXE ZIP,Linux 输出 tar.gz。当前未配置 Apple 公证或 Windows 代码签名,发布前应分别验证系统的首次启动体验。
+49 -7
View File
@@ -47,17 +47,35 @@ pub(crate) fn local_truncated_server() -> (String, std::thread::JoinHandle<()>)
for body in [b"old".as_slice(), b"new-data".as_slice()] {
let deadline = Instant::now() + Duration::from_secs(5);
let mut stream = loop {
if let Ok((stream, _)) = listener.accept() {
break stream;
match listener.accept() {
Ok((stream, _)) => break stream,
Err(error) if error.kind() == io::ErrorKind::WouldBlock => {}
Err(error) => panic!("local fallback server accept failed: {error}"),
}
assert!(Instant::now() < deadline, "local fallback server timed out");
std::thread::sleep(Duration::from_millis(5));
};
stream
.set_read_timeout(Some(Duration::from_secs(2)))
.unwrap();
let mut request = [0u8; 8192];
let _ = stream.read(&mut request).unwrap();
// Accepted sockets can inherit O_NONBLOCK on macOS. Wait for
// the complete request even when its headers arrive separately.
stream.set_nonblocking(false).unwrap();
let request_deadline = Instant::now() + Duration::from_secs(2);
let mut request = Vec::new();
let mut chunk = [0u8; 1024];
while !request.windows(4).any(|bytes| bytes == b"\r\n\r\n") {
let remaining = request_deadline
.checked_duration_since(Instant::now())
.expect("local fallback server request timed out");
stream.set_read_timeout(Some(remaining)).unwrap();
let received = match stream.read(&mut chunk) {
Ok(received) => received,
Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
Err(error) => panic!("local fallback server read failed: {error}"),
};
assert!(received > 0, "local fallback server request ended before headers");
request.extend_from_slice(&chunk[..received]);
assert!(request.len() <= 8192, "local fallback server request too large");
}
stream.set_write_timeout(Some(Duration::from_secs(2))).unwrap();
stream
.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 8\r\nConnection: close\r\n\r\n")
.unwrap();
@@ -189,6 +207,30 @@ pub fn run<T>(
mod tests {
use super::*;
#[test]
fn local_fallback_server_waits_for_complete_fragmented_headers() {
use std::{io::{Read, Write}, net::TcpStream, time::Duration};
let (address, server) = local_truncated_server();
for expected in [b"old".as_slice(), b"new-data".as_slice()] {
let mut stream = TcpStream::connect(address.trim_start_matches("http://")).unwrap();
// Leave time for accept before sending a partial request. The
// server must wait for the final header delimiter before replying.
std::thread::sleep(Duration::from_millis(25));
stream.write_all(b"GET /asset HTTP/1.1\r\nHost: localhost\r\n").unwrap();
stream.set_read_timeout(Some(Duration::from_millis(50))).unwrap();
let mut byte = [0u8; 1];
let error = stream.read(&mut byte).expect_err("server replied before headers completed");
assert!(matches!(error.kind(), io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut));
stream.write_all(b"\r\n").unwrap();
stream.set_read_timeout(Some(Duration::from_secs(2))).unwrap();
let mut response = Vec::new();
stream.read_to_end(&mut response).unwrap();
let header_end = response.windows(4).position(|bytes| bytes == b"\r\n\r\n").unwrap() + 4;
assert_eq!(&response[header_end..], expected);
}
server.join().unwrap();
}
#[test]
fn github_fallback_only_public_exact_hosts_without_credentials() {
assert!(eligible(
"https://api.github.com/repos/a/b/releases?page=1&per_page=100",