[fix](trx-client): give AppKit the process main thread

Replace #[tokio::main] with a manual fn main() that builds the tokio
runtime explicitly. All async initialization moves into async_init().

When the appkit frontend is requested, the runtime context is entered
on the main thread and run_appkit_main_thread() is called directly,
giving AppKit thread 0 as required by MainThreadMarker. Ctrl+C is
handled via a spawned task that calls process::exit.

When appkit is not requested, behaviour is unchanged: block on Ctrl+C.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
2026-02-07 13:36:54 +01:00
parent e7ddaa7300
commit a664a5f1a1
+59 -11
View File
@@ -27,14 +27,14 @@ use trx_frontend_rigctl::register_frontend as register_rigctl_frontend;
#[cfg(feature = "appkit-frontend")]
use trx_frontend_appkit::register_frontend as register_appkit_frontend;
#[cfg(feature = "appkit-frontend")]
use trx_frontend_appkit::run_appkit_main_thread;
use config::ClientConfig;
use remote_client::{parse_remote_url, RemoteClientConfig};
const PKG_DESCRIPTION: &str = concat!(env!("CARGO_PKG_NAME"), " - remote rig client");
const RIG_TASK_CHANNEL_BUFFER: usize = 32;
const APPKIT_FRONTEND_LISTEN_ADDR: ([u8; 4], u16) = ([127, 0, 0, 1], 0);
#[derive(Debug, Parser)]
#[command(
author = env!("CARGO_PKG_AUTHORS"),
@@ -91,8 +91,48 @@ fn normalize_name(name: &str) -> String {
.collect()
}
#[tokio::main]
async fn main() -> DynResult<()> {
fn main() -> DynResult<()> {
let rt = tokio::runtime::Runtime::new()?;
#[allow(unused_variables)]
let app_state = rt.block_on(async_init())?;
#[cfg(feature = "appkit-frontend")]
if app_state.has_appkit {
// Keep a runtime context active on the main thread so that
// tokio::spawn inside run_appkit_main_thread works.
let _guard = rt.enter();
// AppKit needs the process main thread. Spawn Ctrl+C handler on the
// runtime, then hand main thread to AppKit (blocks forever).
rt.spawn(async {
signal::ctrl_c().await.ok();
info!("Ctrl+C received, shutting down");
std::process::exit(0);
});
run_appkit_main_thread(app_state.state_rx, app_state.rig_tx);
unreachable!();
}
// No AppKit — block on Ctrl+C as before.
rt.block_on(async {
signal::ctrl_c().await?;
info!("Ctrl+C received, shutting down");
Ok(())
})
}
/// Holds the state needed after async initialization completes.
struct AppState {
#[allow(dead_code)]
has_appkit: bool,
#[cfg(feature = "appkit-frontend")]
state_rx: watch::Receiver<RigState>,
#[cfg(feature = "appkit-frontend")]
rig_tx: mpsc::Sender<RigRequest>,
}
async fn async_init() -> DynResult<AppState> {
tracing_subscriber::fmt().with_target(false).init();
register_http_frontend();
@@ -106,7 +146,7 @@ async fn main() -> DynResult<()> {
if cli.print_config {
println!("{}", ClientConfig::example_toml());
return Ok(());
std::process::exit(0);
}
let (cfg, config_path) = if let Some(ref path) = cli.config {
@@ -142,7 +182,7 @@ async fn main() -> DynResult<()> {
.unwrap_or(cfg.remote.poll_interval_ms);
// Resolve frontends: CLI > config > default to http
let frontends = if let Some(ref fes) = cli.frontends {
let frontends: Vec<String> = if let Some(ref fes) = cli.frontends {
fes.iter().map(|f| normalize_name(f)).collect()
} else {
let mut fes = Vec::new();
@@ -187,6 +227,8 @@ async fn main() -> DynResult<()> {
.clone()
.or_else(|| cfg.general.callsign.clone());
let has_appkit = frontends.iter().any(|f| f == "appkit");
info!(
"Starting trx-client (remote: {}, frontends: {})",
remote_addr,
@@ -232,14 +274,16 @@ async fn main() -> DynResult<()> {
let _remote_handle =
tokio::spawn(remote_client::run_remote_client(remote_cfg, rx, state_tx));
// Spawn frontends
// Spawn frontends (skip appkit — it will be driven from main thread)
for frontend in &frontends {
if frontend == "appkit" {
continue;
}
let frontend_state_rx = state_rx.clone();
let addr = match frontend.as_str() {
"http" => SocketAddr::from((http_listen, http_port)),
"rigctl" => SocketAddr::from((rigctl_listen, rigctl_port)),
"httpjson" => SocketAddr::from((http_json_listen, http_json_port)),
"appkit" => SocketAddr::from(APPKIT_FRONTEND_LISTEN_ADDR),
other => {
return Err(format!("Frontend missing listen configuration: {}", other).into());
}
@@ -253,7 +297,11 @@ async fn main() -> DynResult<()> {
)?;
}
signal::ctrl_c().await?;
info!("Ctrl+C received, shutting down");
Ok(())
Ok(AppState {
has_appkit,
#[cfg(feature = "appkit-frontend")]
state_rx,
#[cfg(feature = "appkit-frontend")]
rig_tx: tx,
})
}