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
79 changes: 79 additions & 0 deletions .github/workflows/pr-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,82 @@ jobs:
name: codex-plus-plus-macos-universal-dmg
path: dist/macos/*.dmg
if-no-files-found: error

linux-deb:
name: Linux DEB (Ubuntu 24.04, amd64)
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "22"
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Install Linux build dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends \
build-essential curl git pkg-config libssl-dev libsqlite3-dev \
libdbus-1-dev libglib2.0-dev libgtk-3-dev \
libayatana-appindicator3-dev libjavascriptcoregtk-4.1-dev \
libwebkit2gtk-4.1-dev libsoup-3.0-dev libz3-dev
- name: Install frontend dependencies
working-directory: apps/codex-plus-manager
run: npm ci

- name: Frontend tests
working-directory: apps/codex-plus-manager
run: npm test

- name: Check native browser inspector bundle
run: node scripts/assemble-native-browser-inspector.mjs --check

- name: TypeScript check
working-directory: apps/codex-plus-manager
run: npm run check

- name: Build frontend
working-directory: apps/codex-plus-manager
run: npm run vite:build

- name: Rust tests
run: cargo test --workspace

- name: Build Linux application
run: cargo build --release -p codex-plus-manager
- name: Check default version outside repository
shell: bash
run: |
set -euo pipefail
cd /tmp
"$GITHUB_WORKSPACE/scripts/installer/linux/build-deb.sh" --help
test -f "$GITHUB_WORKSPACE/apps/codex-plus-manager/package.json"
- name: Build DEB from outside repository
shell: bash
run: |
set -euo pipefail
cd /tmp
bash "$GITHUB_WORKSPACE/scripts/installer/linux/build-deb.sh" --package-only
- name: Verify package and install
shell: bash
run: |
set -euo pipefail
deb_file="$(find dist/linux -maxdepth 1 -name "*.deb" -print -quit)"
test -n "$deb_file"
test "$(dpkg-deb -f "$deb_file" Package)" = "codex-plus-plus"
test "$(dpkg-deb -f "$deb_file" Version)" = "$(node -p 'require("./apps/codex-plus-manager/package.json").version')"
dpkg-deb -c "$deb_file" | grep -F "./usr/bin/codex-plus-plus"
if dpkg-deb -c "$deb_file" | grep -Fq "./usr/bin/codex-plus-plus-manager"; then exit 1; fi
sudo apt-get install -y "./$deb_file"
test -x /usr/bin/codex-plus-plus
dpkg -V codex-plus-plus
ldd /usr/bin/codex-plus-plus | tee /tmp/codex-plus-plus-ldd.log
! grep -q "not found" /tmp/codex-plus-plus-ldd.log
- name: Upload DEB
uses: actions/upload-artifact@v4
with:
name: codex-plus-plus-linux-amd64-deb
path: dist/linux/*.deb
if-no-files-found: error
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ venv/
*.egg-info/
build/
dist/
.linux-build/
target/
node_modules/
apps/codex-plus-manager/src-tauri/gen/
Expand Down
9 changes: 6 additions & 3 deletions apps/codex-plus-manager/src-tauri/src/auto_takeover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ fn inner() -> &'static Mutex<Inner> {
INNER.get_or_init(|| {
Mutex::new(Inner {
state: "disabled",
platform_supported: cfg!(target_os = "macos"),
platform_supported: cfg!(any(target_os = "macos", target_os = "linux")),
..Inner::default()
})
})
Expand Down Expand Up @@ -422,7 +422,7 @@ fn collect_inputs() -> Result<TakeoverInputs, String> {
// 此前这里单独用 `any(macos, windows)`,Windows 上守护线程确实会运行,
// 而 UI 读到的 platform_supported 却是 false,用户勾选开关后表现为
// 「反复拉起白色窗口、界面却显示不支持」(#2573)。
let platform_supported = cfg!(target_os = "macos");
let platform_supported = cfg!(any(target_os = "macos", target_os = "linux"));
let enabled = is_enabled();
let session_snapshot = codex_plus_launcher::session_snapshot();
let latest = StatusStore::default().load_latest().ok().flatten();
Expand Down Expand Up @@ -794,7 +794,10 @@ mod tests {
#[test]
fn platform_support_matches_the_build_target() {
let payload = payload();
assert_eq!(payload.platform_supported, cfg!(target_os = "macos"));
assert_eq!(
payload.platform_supported,
cfg!(any(target_os = "macos", target_os = "linux"))
);
}

#[test]
Expand Down
14 changes: 11 additions & 3 deletions apps/codex-plus-manager/src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6825,7 +6825,7 @@ pub async fn check_update() -> CommandResult<Value> {
}
}

#[cfg(any(windows, target_os = "macos"))]
#[cfg(any(windows, target_os = "macos", target_os = "linux"))]
#[tauri::command]
pub async fn perform_update(
app: tauri::AppHandle,
Expand Down Expand Up @@ -6862,7 +6862,7 @@ pub async fn perform_update(
result
}

#[cfg(not(any(windows, target_os = "macos")))]
#[cfg(not(any(windows, target_os = "macos", target_os = "linux")))]
#[tauri::command]
pub async fn perform_update(
release: Option<codex_plus_core::update::Release>,
Expand Down Expand Up @@ -9247,14 +9247,22 @@ fn open_url(url: &str) -> anyhow::Result<()> {
{
codex_plus_core::windows_open_url(url)
}
#[cfg(not(windows))]
#[cfg(target_os = "macos")]
{
std::process::Command::new("open")
.arg(url)
.spawn()
.map(|_| ())
.map_err(|error| anyhow::anyhow!("启动系统浏览器失败:{error}"))
}
#[cfg(not(any(windows, target_os = "macos")))]
{
std::process::Command::new("xdg-open")
.arg(url)
.spawn()
.map(|_| ())
.map_err(|error| anyhow::anyhow!("启动系统浏览器失败:{error}"))
}
}

fn settings_payload(message: &str, failure_context: &str) -> CommandResult<SettingsPayload> {
Expand Down
2 changes: 1 addition & 1 deletion apps/codex-plus-manager/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10276,7 +10276,7 @@ function MaintenanceScreen({
{autoTakeover?.lastTakeoverAtMs ? (
<p className="hint">{tf("上次接管:{0}", [new Date(autoTakeover.lastTakeoverAtMs).toLocaleString()])}</p>
) : null}
{autoTakeover?.platformSupported ? null : <p className="hint">{t("自动接管目前只支持 macOS。")}</p>}
{autoTakeover?.platformSupported ? null : <p className="hint">{t("自动接管目前只支持 macOS 和 Linux。")}</p>}
<p className="hint">{t("仅在 Codex++ 正在运行时生效;需要重启官方 App 时会自动重启。")}</p>
<Toolbar>
<Button variant="secondary" onClick={() => void actions.takeOverNow()}>{t("立即接管")}</Button>
Expand Down
1 change: 1 addition & 0 deletions apps/codex-plus-manager/src/i18n-en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2285,6 +2285,7 @@ export const EN_PLAIN: Record<string, string> = {
"刷新状态": "Refresh state",
"本次已跳过自动接管,重启官方 App 后恢复。": "Auto-takeover is skipped for this instance; it resumes after you restart the official app.",
"自动接管目前只支持 macOS。": "Auto-takeover is currently macOS only.",
"自动接管目前只支持 macOS 和 Linux。": "Auto takeover currently only supports macOS and Linux.",
"仅在 Codex++ 正在运行时生效;需要重启官方 App 时会自动重启。": "Only effective while Codex++ is running; the official app is restarted automatically when needed.",
"仅在 Codex++ 正在运行时生效;建议同时开启上面的「开机打开 Codex++」。": "Only effective while Codex++ is running; keeping the login item above enabled is recommended.",
"开启后,你自己启动官方 App 时 Codex++ 会自动接管。官方 App 已在运行且没有调试端口时,会先退出再带参数重开(可能中断正在进行的操作)。": "Once enabled, Codex++ takes over whenever you launch the official app yourself. If it is already running without a debug port, it is quit and relaunched with debug flags (this may interrupt work in progress).",
Expand Down
14 changes: 9 additions & 5 deletions crates/codex-plus-core/src/app_paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -440,15 +440,19 @@ pub fn resolve_codex_app_dir(app_dir: Option<&Path>) -> Option<PathBuf> {
if let Some(app_dir) = app_dir {
return normalize_codex_app_path(app_dir);
}
if cfg!(target_os = "macos") {
return find_macos_codex_app_default();
#[cfg(target_os = "macos")]
{
find_macos_codex_app_default()
}
#[cfg(target_os = "linux")]
{
return find_linux_codex_app_default();
find_linux_codex_app_default()
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
{
// Windows: try MS Store version first, then standalone install
find_latest_codex_app_dir_default().or_else(|| find_standalone_codex_app_dir())
}
// Windows: try MS Store version first, then standalone install
find_latest_codex_app_dir_default().or_else(|| find_standalone_codex_app_dir())
}

/// Search for standalone Codex installations (non-MS Store).
Expand Down
101 changes: 98 additions & 3 deletions crates/codex-plus-core/src/auto_takeover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,28 @@ pub fn has_other_desktop_instance(
!debug_port_pids.contains(process_id) && Some(*process_id) != current_pid
});
}
#[cfg(not(any(target_os = "macos", windows)))]
#[cfg(target_os = "linux")]
{
let expected = crate::app_paths::build_codex_executable(app_dir);
let expected_str = expected.to_string_lossy();
let all = crate::watcher::find_linux_codex_desktop_main_processes(None);
let excluded = crate::watcher::find_linux_codex_desktop_main_processes(Some(exclude_port));
let mut excluded = excluded
.into_iter()
.map(|process| process.pid)
.collect::<Vec<_>>();
if let Some(pid) = current_pid {
excluded.push(pid);
}
return all.iter().any(|process| {
let matches_expected = process.executable == expected_str
|| std::fs::read_link(format!("/proc/{}/exe", process.pid))
.map(|target| target == expected || target.starts_with(app_dir))
.unwrap_or(false);
matches_expected && !excluded.contains(&process.pid)
});
}
#[cfg(not(any(target_os = "macos", windows, target_os = "linux")))]
{
let _ = (app_dir, exclude_port, current_pid);
false
Expand Down Expand Up @@ -246,13 +267,36 @@ pub fn probe_desktop_app(app_dir: &Path) -> AppProbe {
{
return probe_windows_desktop_app(app_dir);
}
#[cfg(not(any(target_os = "macos", windows)))]
#[cfg(target_os = "linux")]
{
return probe_linux_desktop_app(app_dir);
}
#[cfg(not(any(target_os = "macos", windows, target_os = "linux")))]
{
let _ = app_dir;
AppProbe::Unknown
}
}

#[cfg(target_os = "linux")]
fn probe_linux_desktop_app(app_dir: &Path) -> AppProbe {
let expected = crate::app_paths::build_codex_executable(app_dir);
let expected_str = expected.to_string_lossy();
let processes = crate::watcher::find_linux_codex_desktop_main_processes(None);
match processes.into_iter().find(|process| {
process.executable == expected_str
|| std::fs::read_link(format!("/proc/{}/exe", process.pid))
.map(|target| target == expected || target.starts_with(app_dir))
.unwrap_or(false)
}) {
Some(process) => AppProbe::Running(InstanceKey::new(
process.pid,
process.lstart.clone(),
)),
None => AppProbe::NotRunning,
}
}

#[cfg(windows)]
/// 进程是否「与官方 App 主进程同名同路径」。Windows 上 renderer / gpu / utility /
/// crashpad 全都叫 `ChatGPT.exe` 且在同一目录,只能靠命令行区分。
Expand Down Expand Up @@ -364,12 +408,63 @@ pub fn request_desktop_exit(app_dir: &Path, instance: &InstanceKey, graceful: bo
{
return request_windows_exit(instance, graceful);
}
#[cfg(not(any(target_os = "macos", windows)))]
#[cfg(target_os = "linux")]
{
return request_linux_exit(instance, graceful);
}
#[cfg(not(any(target_os = "macos", windows, target_os = "linux")))]
{
false
}
}

#[cfg(target_os = "linux")]
fn request_linux_exit(instance: &InstanceKey, graceful: bool) -> bool {
// 结束前复核进程启动时间,防止 PID 被操作系统回收并复用后误杀无关进程
if !instance.lstart.is_empty() {
if let Some(current_starttime) = crate::watcher::linux_process_starttime(instance.pid) {
if current_starttime != instance.lstart {
let _ = crate::diagnostic_log::append_diagnostic_log(
"takeover.exit_request_skipped_reused_pid",
serde_json::json!({
"pid": instance.pid,
"expected_lstart": instance.lstart,
"actual_lstart": current_starttime,
}),
);
return false;
}
} else {
// /proc/<pid>/stat 已不存在,说明进程已经退出
return true;
}
}

let sig = if graceful { "-15" } else { "-9" };
#[cfg(target_os = "linux")]
let accepted = std::process::Command::new("kill")
.arg(sig)
.arg(instance.pid.to_string())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false);
let _ = crate::diagnostic_log::append_diagnostic_log(
if accepted {
"takeover.exit_request"
} else {
"takeover.exit_request_failed"
},
serde_json::json!({
"pid": instance.pid,
"force": !graceful,
"accepted": accepted,
}),
);
accepted
}

#[cfg(target_os = "macos")]
fn request_macos_exit(app_dir: &Path, _instance: &InstanceKey, _graceful: bool) -> bool {
let app_name = app_dir
Expand Down
Loading
Loading