mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +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`).
|
//! CLI surface for scripting / compositor bindings (e.g. Hyprland `exec`).
|
||||||
|
|
||||||
mod exchange;
|
mod exchange;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
mod linux_forward;
|
||||||
mod parse;
|
mod parse;
|
||||||
mod presenters;
|
mod presenters;
|
||||||
|
|
||||||
pub use exchange::*;
|
pub use exchange::*;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub use linux_forward::*;
|
||||||
pub use parse::*;
|
pub use parse::*;
|
||||||
pub use presenters::print_audio_devices_human;
|
pub use presenters::print_audio_devices_human;
|
||||||
use exchange::{
|
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_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"));
|
const COMPLETIONS_ZSH: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../completions/_psysonic"));
|
||||||
|
|
||||||
use std::sync::OnceLock;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::{
|
use std::{
|
||||||
collections::VecDeque,
|
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()`.
|
/// Print snapshot and `exit`. Used from `main` before `run()`.
|
||||||
pub fn run_info_and_exit(args: &[String]) -> ! {
|
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