diff --git a/assets/the_man.png b/assets/the_man.png new file mode 100644 index 0000000..e9ba611 Binary files /dev/null and b/assets/the_man.png differ diff --git a/module.nix b/module.nix index 3e11613..50b7bc9 100644 --- a/module.nix +++ b/module.nix @@ -14,6 +14,8 @@ in enablePipewire = lib.mkEnableOption "pipewire" // { default = true; }; + enableDebug = lib.mkEnableOption "debug logs"; + # TODO: create some better descriptions settings = { host = lib.mkOption { @@ -81,25 +83,52 @@ in config = lib.mkMerge [ (lib.mkIf cfg.enable { - users = { - users.greg = { - isNormalUser = true; - group = "greg"; - uid = 2000; - description = "loud gym bro"; - }; - groups.greg.gid = 2000; - }; - systemd.user.services.greg-ng = { description = "greg-ng, an mpv based media player"; wantedBy = [ "graphical-session.target" ]; partOf = [ "graphical-session.target" ]; + environment.RUST_LOG = lib.mkIf cfg.enableDebug "greg_ng=trace,mpvipc=trace"; serviceConfig = { Type = "simple"; ExecStart = "${lib.getExe cfg.package} ${lib.cli.toGNUCommandLineShell { } cfg.settings}"; Restart = "always"; RestartSec = 3; + + RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ]; + AmbientCapabilities = [ "" ]; + CapabilityBoundingSet = [ "" ]; + DeviceAllow = [ "" ]; + LockPersonality = true; + # Might work, but wouldn't bet on it with embedded lua in mpv + MemoryDenyWriteExecute = false; + NoNewPrivileges = true; + # MPV and mesa tries to talk directly to the GPU. + PrivateDevices = false; + PrivateMounts = true; + PrivateTmp = true; + PrivateUsers = true; + ProcSubset = "pid"; + ProtectClock = true; + ProtectControlGroups = true; + # MPV wants ~/.cache + ProtectHome = false; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "full"; + RemoveIPC = true; + UMask = "0077"; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "~@privileged" + "~@resources" + ]; }; }; }) @@ -123,6 +152,16 @@ in extraPortals = [ pkgs.xdg-desktop-portal-gtk ]; }; + users = { + users.greg = { + isNormalUser = true; + group = "greg"; + uid = 2000; + description = "loud gym bro"; + }; + groups.greg.gid = 2000; + }; + services.greetd = { enable = true; settings = rec { diff --git a/src/main.rs b/src/main.rs index 35fb514..9def7d5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,12 @@ use anyhow::Context; use axum::{Router, Server}; use clap::Parser; -use mpvipc_async::Mpv; -use std::{ - fs::create_dir_all, - io::Write, - net::{IpAddr, SocketAddr}, - path::Path, -}; +use mpv_setup::{connect_to_mpv, create_mpv_config_file, show_grzegorz_image}; +use std::net::{IpAddr, SocketAddr}; use tempfile::NamedTempFile; -use tokio::process::{Child, Command}; mod api; +mod mpv_setup; #[derive(Parser)] struct Args { @@ -45,112 +40,6 @@ struct MpvConnectionArgs<'a> { force_auto_start: bool, } -const DEFAULT_MPV_CONFIG_CONTENT: &str = include_str!("../assets/default-mpv.conf"); - -fn create_mpv_config_file(args_config_file: Option) -> anyhow::Result { - let file_content = if let Some(path) = args_config_file { - if !Path::new(&path).exists() { - anyhow::bail!("Mpv config file not found at {}", &path); - } - - std::fs::read_to_string(&path).context("Failed to read mpv config file")? - } else { - DEFAULT_MPV_CONFIG_CONTENT.to_string() - }; - - let tmpfile = tempfile::Builder::new() - .prefix("mpv-") - .rand_bytes(8) - .suffix(".conf") - .tempfile()?; - - tmpfile.reopen()?.write_all(file_content.as_bytes())?; - - Ok(tmpfile) -} - -async fn connect_to_mpv<'a>(args: &MpvConnectionArgs<'a>) -> anyhow::Result<(Mpv, Option)> { - log::debug!("Connecting to mpv"); - - debug_assert!( - !args.force_auto_start || args.auto_start, - "force_auto_start requires auto_start" - ); - - let socket_path = Path::new(&args.socket_path); - - if !socket_path.exists() { - log::debug!("Mpv socket not found at {}", &args.socket_path); - if !args.auto_start { - panic!("Mpv socket not found at {}", &args.socket_path); - } - - log::debug!("Ensuring parent dir of mpv socket exists"); - let parent_dir = Path::new(&args.socket_path) - .parent() - .context("Failed to get parent dir of mpv socket")?; - - if !parent_dir.is_dir() { - create_dir_all(parent_dir).context("Failed to create parent dir of mpv socket")?; - } - } else { - log::debug!("Existing mpv socket found at {}", &args.socket_path); - if args.force_auto_start { - log::debug!("Removing mpv socket"); - std::fs::remove_file(&args.socket_path)?; - } - } - - let process_handle = if args.auto_start { - log::info!("Starting mpv with socket at {}", &args.socket_path); - - // TODO: try to fetch mpv from PATH - Some( - Command::new(args.executable_path.as_deref().unwrap_or("mpv")) - .arg(format!("--input-ipc-server={}", &args.socket_path)) - .arg("--idle") - .arg("--force-window") - .arg("--fullscreen") - .arg("--no-config") - .arg(format!( - "--include={}", - &args.config_file.path().to_string_lossy() - )) - // .arg("--no-terminal") - .arg("--load-unsafe-playlists") - .arg("--keep-open") // Keep last frame of video on end of video - .spawn() - .context("Failed to start mpv")?, - ) - } else { - None - }; - - // Wait for mpv to create the socket - if tokio::time::timeout(tokio::time::Duration::from_millis(500), async { - while !&socket_path.exists() { - log::debug!("Waiting for mpv socket at {}", &args.socket_path); - tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; - } - }) - .await - .is_err() - { - return Err(anyhow::anyhow!( - "Failed to connect to mpv socket: {}", - &args.socket_path - )); - } - - Ok(( - Mpv::connect(&args.socket_path).await.context(format!( - "Failed to connect to mpv socket: {}", - &args.socket_path - ))?, - process_handle, - )) -} - async fn resolve(host: &str) -> anyhow::Result { let addr = format!("{}:0", host); let addresses = tokio::net::lookup_host(addr).await?; @@ -177,6 +66,10 @@ async fn main() -> anyhow::Result<()> { }) .await?; + if let Err(e) = show_grzegorz_image(mpv.clone()).await { + log::warn!("Could not show Grzegorz image: {}", e); + } + let addr = SocketAddr::new(resolve(&args.host).await?, args.port); log::info!("Starting API on {}", addr); diff --git a/src/mpv_setup.rs b/src/mpv_setup.rs new file mode 100644 index 0000000..0ed40e2 --- /dev/null +++ b/src/mpv_setup.rs @@ -0,0 +1,135 @@ +use std::{fs::create_dir_all, io::Write, path::Path}; + +use anyhow::Context; +use mpvipc_async::{Mpv, MpvExt}; +use tempfile::NamedTempFile; +use tokio::process::{Child, Command}; + +use crate::MpvConnectionArgs; + +const DEFAULT_MPV_CONFIG_CONTENT: &str = include_str!("../assets/default-mpv.conf"); + +const THE_MAN_PNG: &[u8] = include_bytes!("../assets/the_man.png"); + +pub fn create_mpv_config_file(args_config_file: Option) -> anyhow::Result { + let file_content = if let Some(path) = args_config_file { + if !Path::new(&path).exists() { + anyhow::bail!("Mpv config file not found at {}", &path); + } + + std::fs::read_to_string(&path).context("Failed to read mpv config file")? + } else { + DEFAULT_MPV_CONFIG_CONTENT.to_string() + }; + + let tmpfile = tempfile::Builder::new() + .prefix("mpv-") + .rand_bytes(8) + .suffix(".conf") + .tempfile()?; + + tmpfile.reopen()?.write_all(file_content.as_bytes())?; + + Ok(tmpfile) +} + +pub async fn connect_to_mpv<'a>( + args: &MpvConnectionArgs<'a>, +) -> anyhow::Result<(Mpv, Option)> { + log::debug!("Connecting to mpv"); + + debug_assert!( + !args.force_auto_start || args.auto_start, + "force_auto_start requires auto_start" + ); + + let socket_path = Path::new(&args.socket_path); + + if !socket_path.exists() { + log::debug!("Mpv socket not found at {}", &args.socket_path); + if !args.auto_start { + panic!("Mpv socket not found at {}", &args.socket_path); + } + + log::debug!("Ensuring parent dir of mpv socket exists"); + let parent_dir = Path::new(&args.socket_path) + .parent() + .context("Failed to get parent dir of mpv socket")?; + + if !parent_dir.is_dir() { + create_dir_all(parent_dir).context("Failed to create parent dir of mpv socket")?; + } + } else { + log::debug!("Existing mpv socket found at {}", &args.socket_path); + if args.force_auto_start { + log::debug!("Removing mpv socket"); + std::fs::remove_file(&args.socket_path)?; + } + } + + let process_handle = if args.auto_start { + log::info!("Starting mpv with socket at {}", &args.socket_path); + + // TODO: try to fetch mpv from PATH + Some( + Command::new(args.executable_path.as_deref().unwrap_or("mpv")) + .arg(format!("--input-ipc-server={}", &args.socket_path)) + .arg("--idle") + .arg("--force-window") + .arg("--fullscreen") + .arg("--no-config") + .arg(format!( + "--include={}", + &args.config_file.path().to_string_lossy() + )) + // .arg("--no-terminal") + .arg("--load-unsafe-playlists") + .arg("--keep-open") // Keep last frame of video on end of video + .spawn() + .context("Failed to start mpv")?, + ) + } else { + None + }; + + // Wait for mpv to create the socket + if tokio::time::timeout(tokio::time::Duration::from_millis(500), async { + while !&socket_path.exists() { + log::debug!("Waiting for mpv socket at {}", &args.socket_path); + tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; + } + }) + .await + .is_err() + { + return Err(anyhow::anyhow!( + "Failed to connect to mpv socket: {}", + &args.socket_path + )); + } + + Ok(( + Mpv::connect(&args.socket_path).await.context(format!( + "Failed to connect to mpv socket: {}", + &args.socket_path + ))?, + process_handle, + )) +} + +pub async fn show_grzegorz_image(mpv: Mpv) -> anyhow::Result<()> { + let path = std::env::temp_dir().join("the_man.png"); + std::fs::write(path.as_path(), THE_MAN_PNG)?; + + mpv.playlist_clear().await?; + mpv.playlist_add( + path.to_string_lossy().as_ref(), + mpvipc_async::PlaylistAddTypeOptions::File, + mpvipc_async::PlaylistAddOptions::Append, + ) + .await?; + mpv.next().await?; + + Ok(()) +} +