mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-21 22:15:40 +00:00
refactor(cli): extract Linux single-instance IPC into linux_forward submodule
Pull the Linux-only single-instance D-Bus stack out of cli/mod.rs into a new cli/linux_forward.rs: - tauri_identifier (reads identifier from embedded tauri.conf.json) - single_instance_bus_name + single_instance_object_path (D-Bus path derivation matching tauri-plugin-single-instance) - linux_bus_name_has_owner (zbus NameHasOwner wrapper) - linux_is_primary_instance_running (pub) - LinuxPlayerForwardResult enum (pub) - linux_try_forward_player_cli_secondary (pub) — the heavy lifter: forwards argv via D-Bus ExecuteCallback, then for list/search commands reads the response file and prints to stdout The whole submodule is `#[cfg(target_os = "linux")]` gated at the mod declaration so non-Linux builds don't see it at all (cleaner than the previous per-item gating). Public surface preserved: lib.rs and main.rs keep importing cli::linux_is_primary_instance_running and cli::LinuxPlayerForwardResult unchanged via the linux-only pub use re-export. cli/mod.rs: 696 → 550 LOC. Behaviour-preserving — same D-Bus protocol, same response-file polling, same stdout output strings.
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
//! Linux-only single-instance D-Bus IPC: detect a running primary Psysonic
|
||||
//! and forward `--player ...` argv to it instead of starting a second app.
|
||||
|
||||
use std::sync::OnceLock;
|
||||
use std::time::Duration;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use super::describe_cli_command;
|
||||
use super::exchange::{
|
||||
cli_audio_device_response_path, cli_library_response_path, cli_search_response_path,
|
||||
cli_server_list_path, print_library_cli_stdout, print_search_cli_stdout,
|
||||
print_server_list_cli_stdout, read_library_cli_response_blocking,
|
||||
read_search_cli_response_blocking, read_server_list_cli_response_blocking,
|
||||
};
|
||||
use super::parse::{parse_cli_command, wants_cli_json_output, wants_quiet, CliCommand};
|
||||
use super::presenters::print_audio_devices_human;
|
||||
|
||||
fn tauri_identifier() -> &'static str {
|
||||
static ID: OnceLock<String> = OnceLock::new();
|
||||
ID.get_or_init(|| {
|
||||
let raw = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tauri.conf.json"));
|
||||
let v: serde_json::Value =
|
||||
serde_json::from_str(raw).expect("parse embedded tauri.conf.json");
|
||||
v["identifier"]
|
||||
.as_str()
|
||||
.expect("tauri.conf.json identifier")
|
||||
.to_string()
|
||||
})
|
||||
.as_str()
|
||||
}
|
||||
|
||||
fn single_instance_bus_name() -> String {
|
||||
format!("{}.SingleInstance", tauri_identifier())
|
||||
}
|
||||
|
||||
fn single_instance_object_path(dbus_name: &str) -> String {
|
||||
let mut dbus_path = dbus_name.replace('.', "/").replace('-', "_");
|
||||
if !dbus_path.starts_with('/') {
|
||||
dbus_path = format!("/{dbus_path}");
|
||||
}
|
||||
dbus_path
|
||||
}
|
||||
|
||||
fn linux_bus_name_has_owner(
|
||||
conn: &zbus::blocking::Connection,
|
||||
bus_name: &str,
|
||||
) -> Result<bool, String> {
|
||||
let reply = conn
|
||||
.call_method(
|
||||
Some("org.freedesktop.DBus"),
|
||||
"/org/freedesktop/DBus",
|
||||
Some("org.freedesktop.DBus"),
|
||||
"NameHasOwner",
|
||||
&(bus_name,),
|
||||
)
|
||||
.map_err(|e| format!("NameHasOwner: {e}"))?;
|
||||
reply
|
||||
.body()
|
||||
.deserialize::<bool>()
|
||||
.map_err(|e| format!("NameHasOwner reply: {e}"))
|
||||
}
|
||||
|
||||
/// Whether the main Psysonic instance holds the single-instance D-Bus name (Linux only).
|
||||
pub fn linux_is_primary_instance_running() -> Result<bool, String> {
|
||||
use zbus::blocking::Connection;
|
||||
let conn = Connection::session().map_err(|e| format!("D-Bus session: {e}"))?;
|
||||
let well_known = single_instance_bus_name();
|
||||
linux_bus_name_has_owner(&conn, &well_known)
|
||||
}
|
||||
|
||||
/// Linux: if a primary instance owns the single-instance bus name, forward argv and
|
||||
/// signal the caller process should exit successfully. Otherwise continue normal startup.
|
||||
pub enum LinuxPlayerForwardResult {
|
||||
Forwarded,
|
||||
ContinueStartup,
|
||||
}
|
||||
|
||||
pub fn linux_try_forward_player_cli_secondary(args: &[String]) -> Result<LinuxPlayerForwardResult, String> {
|
||||
use zbus::blocking::Connection;
|
||||
|
||||
let well_known = single_instance_bus_name();
|
||||
let conn = Connection::session().map_err(|e| format!("D-Bus session: {e}"))?;
|
||||
|
||||
if !linux_bus_name_has_owner(&conn, well_known.as_str())? {
|
||||
return Ok(LinuxPlayerForwardResult::ContinueStartup);
|
||||
}
|
||||
|
||||
let cwd = std::env::current_dir().unwrap_or_default();
|
||||
let cwd_s = cwd.to_str().unwrap_or("").to_string();
|
||||
let argv = args.to_vec();
|
||||
let path = single_instance_object_path(&well_known);
|
||||
|
||||
match parse_cli_command(args) {
|
||||
Some(CliCommand::AudioDeviceList) => {
|
||||
let _ = std::fs::remove_file(cli_audio_device_response_path());
|
||||
}
|
||||
Some(CliCommand::LibraryList) => {
|
||||
let _ = std::fs::remove_file(cli_library_response_path());
|
||||
}
|
||||
Some(CliCommand::ServerList) => {
|
||||
let _ = std::fs::remove_file(cli_server_list_path());
|
||||
}
|
||||
Some(CliCommand::Search { .. }) => {
|
||||
let _ = std::fs::remove_file(cli_search_response_path());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
conn.call_method(
|
||||
Some(well_known.as_str()),
|
||||
path.as_str(),
|
||||
Some("org.SingleInstance.DBus"),
|
||||
"ExecuteCallback",
|
||||
&(argv, cwd_s),
|
||||
)
|
||||
.map_err(|e| format!("forward to running instance: {e}"))?;
|
||||
|
||||
if let Some(CliCommand::AudioDeviceList) = parse_cli_command(args) {
|
||||
let resp_path = cli_audio_device_response_path();
|
||||
let text = std::fs::read_to_string(&resp_path).unwrap_or_else(|_| "{}".into());
|
||||
if wants_cli_json_output(args) {
|
||||
println!("{}", text.trim());
|
||||
} else if let Ok(v) = serde_json::from_str::<Value>(&text) {
|
||||
print_audio_devices_human(&v);
|
||||
} else {
|
||||
println!("{}", text.trim());
|
||||
}
|
||||
if !wants_quiet(args) {
|
||||
println!("OK: audio-device list");
|
||||
}
|
||||
} else if let Some(CliCommand::LibraryList) = parse_cli_command(args) {
|
||||
let json_out = wants_cli_json_output(args);
|
||||
let text = read_library_cli_response_blocking(Duration::from_secs(3));
|
||||
print_library_cli_stdout(&text, json_out);
|
||||
if !wants_quiet(args) {
|
||||
println!("OK: library list");
|
||||
}
|
||||
} else if let Some(CliCommand::ServerList) = parse_cli_command(args) {
|
||||
let json_out = wants_cli_json_output(args);
|
||||
let text = read_server_list_cli_response_blocking(Duration::from_secs(3));
|
||||
print_server_list_cli_stdout(&text, json_out);
|
||||
if !wants_quiet(args) {
|
||||
println!("OK: server list");
|
||||
}
|
||||
} else if let Some(CliCommand::Search { .. }) = parse_cli_command(args) {
|
||||
let json_out = wants_cli_json_output(args);
|
||||
let text = read_search_cli_response_blocking(Duration::from_secs(12));
|
||||
print_search_cli_stdout(&text, json_out);
|
||||
if !wants_quiet(args) {
|
||||
if let Some(cmd) = parse_cli_command(args) {
|
||||
println!("OK: {}", describe_cli_command(&cmd));
|
||||
}
|
||||
}
|
||||
} else if !wants_quiet(args) {
|
||||
if let Some(cmd) = parse_cli_command(args) {
|
||||
println!("OK: {}", describe_cli_command(&cmd));
|
||||
} else {
|
||||
println!("OK");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(LinuxPlayerForwardResult::Forwarded)
|
||||
}
|
||||
+4
-153
@@ -1,10 +1,14 @@
|
||||
//! CLI surface for scripting / compositor bindings (e.g. Hyprland `exec`).
|
||||
|
||||
mod exchange;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux_forward;
|
||||
mod parse;
|
||||
mod presenters;
|
||||
|
||||
pub use exchange::*;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use linux_forward::*;
|
||||
pub use parse::*;
|
||||
pub use presenters::print_audio_devices_human;
|
||||
use exchange::{
|
||||
@@ -19,7 +23,6 @@ use presenters::print_info_human;
|
||||
const COMPLETIONS_BASH: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../completions/psysonic.bash"));
|
||||
const COMPLETIONS_ZSH: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../completions/_psysonic"));
|
||||
|
||||
use std::sync::OnceLock;
|
||||
use std::time::Duration;
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
@@ -245,63 +248,6 @@ pub fn run_tail_and_exit(args: &[String]) -> ! {
|
||||
|
||||
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn tauri_identifier() -> &'static str {
|
||||
static ID: OnceLock<String> = OnceLock::new();
|
||||
ID.get_or_init(|| {
|
||||
let raw = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tauri.conf.json"));
|
||||
let v: serde_json::Value =
|
||||
serde_json::from_str(raw).expect("parse embedded tauri.conf.json");
|
||||
v["identifier"]
|
||||
.as_str()
|
||||
.expect("tauri.conf.json identifier")
|
||||
.to_string()
|
||||
})
|
||||
.as_str()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn single_instance_bus_name() -> String {
|
||||
format!("{}.SingleInstance", tauri_identifier())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn single_instance_object_path(dbus_name: &str) -> String {
|
||||
let mut dbus_path = dbus_name.replace('.', "/").replace('-', "_");
|
||||
if !dbus_path.starts_with('/') {
|
||||
dbus_path = format!("/{dbus_path}");
|
||||
}
|
||||
dbus_path
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn linux_bus_name_has_owner(
|
||||
conn: &zbus::blocking::Connection,
|
||||
bus_name: &str,
|
||||
) -> Result<bool, String> {
|
||||
let reply = conn
|
||||
.call_method(
|
||||
Some("org.freedesktop.DBus"),
|
||||
"/org/freedesktop/DBus",
|
||||
Some("org.freedesktop.DBus"),
|
||||
"NameHasOwner",
|
||||
&(bus_name,),
|
||||
)
|
||||
.map_err(|e| format!("NameHasOwner: {e}"))?;
|
||||
reply
|
||||
.body()
|
||||
.deserialize::<bool>()
|
||||
.map_err(|e| format!("NameHasOwner reply: {e}"))
|
||||
}
|
||||
|
||||
/// Whether the main Psysonic instance holds the single-instance D-Bus name (Linux only).
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn linux_is_primary_instance_running() -> Result<bool, String> {
|
||||
use zbus::blocking::Connection;
|
||||
let conn = Connection::session().map_err(|e| format!("D-Bus session: {e}"))?;
|
||||
let well_known = single_instance_bus_name();
|
||||
linux_bus_name_has_owner(&conn, &well_known)
|
||||
}
|
||||
|
||||
/// Print snapshot and `exit`. Used from `main` before `run()`.
|
||||
pub fn run_info_and_exit(args: &[String]) -> ! {
|
||||
@@ -607,98 +553,3 @@ pub fn emit_player_cli_cmd<R: Runtime>(app: &AppHandle<R>, cmd: PlayerCliCmd) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Linux: if a primary instance owns the single-instance bus name, forward argv and
|
||||
/// signal the caller process should exit successfully. Otherwise continue normal startup.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub enum LinuxPlayerForwardResult {
|
||||
Forwarded,
|
||||
ContinueStartup,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn linux_try_forward_player_cli_secondary(args: &[String]) -> Result<LinuxPlayerForwardResult, String> {
|
||||
use zbus::blocking::Connection;
|
||||
|
||||
let well_known = single_instance_bus_name();
|
||||
let conn = Connection::session().map_err(|e| format!("D-Bus session: {e}"))?;
|
||||
|
||||
if !linux_bus_name_has_owner(&conn, well_known.as_str())? {
|
||||
return Ok(LinuxPlayerForwardResult::ContinueStartup);
|
||||
}
|
||||
|
||||
let cwd = std::env::current_dir().unwrap_or_default();
|
||||
let cwd_s = cwd.to_str().unwrap_or("").to_string();
|
||||
let argv = args.to_vec();
|
||||
let path = single_instance_object_path(&well_known);
|
||||
|
||||
match parse_cli_command(args) {
|
||||
Some(CliCommand::AudioDeviceList) => {
|
||||
let _ = std::fs::remove_file(cli_audio_device_response_path());
|
||||
}
|
||||
Some(CliCommand::LibraryList) => {
|
||||
let _ = std::fs::remove_file(cli_library_response_path());
|
||||
}
|
||||
Some(CliCommand::ServerList) => {
|
||||
let _ = std::fs::remove_file(cli_server_list_path());
|
||||
}
|
||||
Some(CliCommand::Search { .. }) => {
|
||||
let _ = std::fs::remove_file(cli_search_response_path());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
conn.call_method(
|
||||
Some(well_known.as_str()),
|
||||
path.as_str(),
|
||||
Some("org.SingleInstance.DBus"),
|
||||
"ExecuteCallback",
|
||||
&(argv, cwd_s),
|
||||
)
|
||||
.map_err(|e| format!("forward to running instance: {e}"))?;
|
||||
|
||||
if let Some(CliCommand::AudioDeviceList) = parse_cli_command(args) {
|
||||
let resp_path = cli_audio_device_response_path();
|
||||
let text = std::fs::read_to_string(&resp_path).unwrap_or_else(|_| "{}".into());
|
||||
if wants_cli_json_output(args) {
|
||||
println!("{}", text.trim());
|
||||
} else if let Ok(v) = serde_json::from_str::<Value>(&text) {
|
||||
print_audio_devices_human(&v);
|
||||
} else {
|
||||
println!("{}", text.trim());
|
||||
}
|
||||
if !wants_quiet(args) {
|
||||
println!("OK: audio-device list");
|
||||
}
|
||||
} else if let Some(CliCommand::LibraryList) = parse_cli_command(args) {
|
||||
let json_out = wants_cli_json_output(args);
|
||||
let text = read_library_cli_response_blocking(Duration::from_secs(3));
|
||||
print_library_cli_stdout(&text, json_out);
|
||||
if !wants_quiet(args) {
|
||||
println!("OK: library list");
|
||||
}
|
||||
} else if let Some(CliCommand::ServerList) = parse_cli_command(args) {
|
||||
let json_out = wants_cli_json_output(args);
|
||||
let text = read_server_list_cli_response_blocking(Duration::from_secs(3));
|
||||
print_server_list_cli_stdout(&text, json_out);
|
||||
if !wants_quiet(args) {
|
||||
println!("OK: server list");
|
||||
}
|
||||
} else if let Some(CliCommand::Search { .. }) = parse_cli_command(args) {
|
||||
let json_out = wants_cli_json_output(args);
|
||||
let text = read_search_cli_response_blocking(Duration::from_secs(12));
|
||||
print_search_cli_stdout(&text, json_out);
|
||||
if !wants_quiet(args) {
|
||||
if let Some(cmd) = parse_cli_command(args) {
|
||||
println!("OK: {}", describe_cli_command(&cmd));
|
||||
}
|
||||
}
|
||||
} else if !wants_quiet(args) {
|
||||
if let Some(cmd) = parse_cli_command(args) {
|
||||
println!("OK: {}", describe_cli_command(&cmd));
|
||||
} else {
|
||||
println!("OK");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(LinuxPlayerForwardResult::Forwarded)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user