fix(desktop): reject conflicting headless modes

This commit is contained in:
Fini 2026-07-31 00:32:56 +08:00
parent 87dbb21d8f
commit 0641cb3f3e
2 changed files with 65 additions and 0 deletions

View file

@ -98,6 +98,7 @@ mod window_resize;
mod window_state;
use op_host_native::{NativeBackend, SharedSkiaContext, SharedSkiaError, WidgetHostNative};
use std::ffi::OsStr;
use std::path::PathBuf;
use std::time::Instant;
use winit::event_loop::{ControlFlow, EventLoop, EventLoopProxy};
@ -538,6 +539,31 @@ fn init_tracing() {
.try_init();
}
fn conflicting_headless_modes<I, S>(args: I) -> Option<Vec<&'static str>>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
const MODE_FLAGS: [&str; 6] = [
"--mcp",
"--mcp-http",
"--serve-web",
"--tcc-selftest",
"--render-shots",
"--enrich-images",
];
let mut requested = Vec::new();
for arg in args {
let arg = arg.as_ref();
for mode in MODE_FLAGS {
if arg == OsStr::new(mode) && !requested.contains(&mode) {
requested.push(mode);
}
}
}
(requested.len() > 1).then_some(requested)
}
fn main() {
// FIRST, before any thread exists: graft the login-shell PATH and proxy
// exports onto this process. A Dock/Finder launch inherits launchd's
@ -566,6 +592,13 @@ fn main() {
Err(err) => eprintln!("[fonts] skipping imported-font rescan: {err}"),
}
init_tracing();
if let Some(modes) = conflicting_headless_modes(std::env::args_os().skip(1)) {
eprintln!(
"openpencil-desktop: headless modes are mutually exclusive: {}",
modes.join(", ")
);
std::process::exit(2);
}
// `--mcp` / `--mcp-http` swap the GUI for an MCP server mode;
// when one of those ran, exit instead of opening a window.
if mcp_serve::run_cli_if_requested() {

View file

@ -4,6 +4,38 @@
use super::*;
use winit::keyboard::{Key, NamedKey};
#[test]
fn distinct_headless_modes_are_rejected_before_dispatch() {
assert_eq!(
conflicting_headless_modes([
"--render-shots",
"input.op",
"shots",
"--enrich-images",
"input.op",
"output.op",
]),
Some(vec!["--render-shots", "--enrich-images"])
);
assert_eq!(
conflicting_headless_modes(["--mcp-http", "3100", "input.op", "--tcc-selftest"]),
Some(vec!["--mcp-http", "--tcc-selftest"])
);
}
#[test]
fn one_headless_mode_or_repeated_same_flag_is_not_a_conflict() {
assert_eq!(
conflicting_headless_modes(["--render-shots", "input.op", "shots"]),
None
);
assert_eq!(
conflicting_headless_modes(["--enrich-images", "input.op", "--enrich-images"]),
None
);
assert_eq!(conflicting_headless_modes(["document.op"]), None);
}
#[test]
fn cursor_only_redraw_without_visible_state_change_skips_present() {
let mut app = DesktopApp::new(None);