From f93792ac4809e3960ca0136ce3421b770e5f7974 Mon Sep 17 00:00:00 2001 From: Nikolay Arhipov Date: Mon, 6 Nov 2023 16:31:36 +0200 Subject: [PATCH] Added logs sumbcommand to edit PrincessLog config remotely (#10) * Added logs sumbcommand to edit PrincessLog config remotely * Updated README --- Cargo.lock | 69 +++++++++++++++-- Cargo.toml | 12 +-- README.md | 68 ++++++++++++++++- src/commands/logs.rs | 176 +++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 298 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5a6a38b..298b5cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -137,6 +137,12 @@ version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "camino" version = "1.1.6" @@ -166,6 +172,7 @@ dependencies = [ "either", "enum_dispatch", "env_logger", + "local-ip-address", "log", "rustc_version", "serde", @@ -248,7 +255,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.38", ] [[package]] @@ -295,7 +302,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.38", ] [[package]] @@ -431,7 +438,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn", + "syn 2.0.38", ] [[package]] @@ -452,6 +459,18 @@ version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45786cec4d5e54a224b15cb9f06751883103a27c19c93eda09b0b4f5f08fefac" +[[package]] +name = "local-ip-address" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66357e687a569abca487dc399a9c9ac19beb3f13991ed49f00c144e02cbd42ab" +dependencies = [ + "libc", + "neli", + "thiserror", + "windows-sys", +] + [[package]] name = "log" version = "0.4.20" @@ -473,6 +492,31 @@ dependencies = [ "adler", ] +[[package]] +name = "neli" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1100229e06604150b3becd61a4965d5c70f3be1759544ea7274166f4be41ef43" +dependencies = [ + "byteorder", + "libc", + "log", + "neli-proc-macros", +] + +[[package]] +name = "neli-proc-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c168194d373b1e134786274020dae7fc5513d565ea2ebb9bc9ff17ffb69106d4" +dependencies = [ + "either", + "proc-macro2", + "quote", + "serde", + "syn 1.0.109", +] + [[package]] name = "num-traits" version = "0.2.17" @@ -622,7 +666,7 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.38", ] [[package]] @@ -654,6 +698,17 @@ dependencies = [ "thiserror", ] +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.38" @@ -710,7 +765,7 @@ checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.38", ] [[package]] @@ -756,7 +811,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.38", "wasm-bindgen-shared", ] @@ -778,7 +833,7 @@ checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.38", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/Cargo.toml b/Cargo.toml index d52756d..a02fa6f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,14 +27,4 @@ suppaftp = { version = "5.2.1" } tee = "0.1.0" tempfile = "3.8.0" walkdir = "2.4.0" - -[package.metadata.vita] -title_id = "VITASHELL" -title_name = "Test app" -assets = "static" -# You can choose a subset of std or use panic_abort if you don't need unwinding -build_std = "std,panic_unwind" -# You can provide a custom JSON file spec -vita_strip_flags = ["-g"] -vita_make_fself_flags = ["-s"] -vita_mksfoex_flags = ["-d", "ATTRIBUTE2=12"] +local-ip-address = "0.5.6" diff --git a/README.md b/README.md index 70077cf..e7ff25d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Cargo command to work with Sony PlayStation Vita rust project binaries. -For general guidelines see [vita-rust book](https://vita-rust.github.io/book) +For general guidelines see [vita-rust book](https://vita-rust.github.io/book). ## Requirements @@ -84,7 +84,7 @@ vita_mksfoex_flags = ["-d", "ATTRIBUTE2=12"] ## Examples -``` +```sh # Build all current/all workspace projects in release mode as vpk cargo vita build vpk -- --release @@ -101,6 +101,70 @@ cargo vita build eboot --update --run -- --release cargo vita logs ``` +## Additional tools + +For a better development experience it is recommended to install additional modules on your Vita. + +### vitacompanion + +When enabled, this module keeps a FTP server on your Vita running on port `1337`, as well as a TCP command server running on port `1338`. + +- The FTP server allows you to easily upload `vpk` and `eboot` files to your Vita. This is FTP server is used by `cargo-vita` for the following commands and flags: + + ```sh + # Builds a eboot.bin, and uploads it to ux0:/app/TITLEID/eboot.bin + cargo vita build eboot --update + + # Builds a vpk, and uploads it to ux0:/download/project_name.vpk + cargo vita build vpk --upload + + # Recursively upload ~/test to ux0:/download + cargo vita upload -s ~/test -d ux0:/download/ + ``` + +- The command server allows you to kill and launch applications and reboot your Vita: + + ```sh + # Reboot your Vita + cargo vita reboot + + # After uploading the eboot.bin this command will kill the current app, + # and launch your TITLEID + cargo vita build eboot --update --run + ``` + +### PrincessLog + +This module allows capturing stdout and stderr from your Vita. +In order to capture the logs you need to start a TCP server on your computer, and configure +PrincessLog to connect to it. + +For convenience `cargo-vita` provides two commands to work with logs: + + - A command to start a TCP server Vita will connect to: + + ```sh + # Start a TCP server on 0.0.0.0, and print all bytes received via the socket to stdout + cargo vita logs + ``` + - A command to reconfigure PrincessLog with the new ip/port. This will use + the FTP server provided by `vitacompanion` to upload a new config. + If an IP address of your machine is not explicitly provided, it will be guessed + using [local-ip-address](https://crates.io/crates/local-ip-address) crate. + When a configuration file is updated, the changes are not applied until Vita is rebooted. + + ```sh + # Generate and upload a new config for PrincessLog to your Vita. + # Will guess a local IP address of the machine where this command is executed. + # After reconfiguration reboots the Vita. + cargo vita logs configure && cargo vita reboot + + + # Explicitly sets the IP address Vita will connect to. + # Also enables kernel debug messages in the log. + cargo vita logs configure --host-ip-address 10.10.10.10 --kernel-debug + ``` + ## License Except where noted (below and/or in individual files), all code in this repository is dual-licensed at your option under either: diff --git a/src/commands/logs.rs b/src/commands/logs.rs index 3dbf8b4..2904d20 100644 --- a/src/commands/logs.rs +++ b/src/commands/logs.rs @@ -1,20 +1,173 @@ -use std::{io::Read, net::TcpListener}; +use std::{ + io::{Cursor, Read}, + net::{Ipv4Addr, TcpListener}, + str::FromStr, +}; -use anyhow::Context; -use clap::Args; +use anyhow::{bail, Context}; +use clap::{Args, Subcommand}; use colored::Colorize; -use log::{debug, error, info}; +use log::{debug, error, info, warn}; -use super::Executor; +use crate::ftp; + +use super::{ConnectionArgs, Executor}; #[derive(Args, Debug)] pub struct Logs { + #[command(subcommand)] + cmd: Option, + #[arg(long, short = 'p', env = "VITA_LOG_PORT", default_value_t = 8888)] port: u16, } -impl Executor for Logs { - fn execute(&self) -> anyhow::Result<()> { +#[derive(Subcommand, Debug)] +pub enum LogsCmd { + /// Start a TCP server on 0.0.0.0 and print to stdout all bytes read from the socket + Listen, + /// Reconfigures PrincessLog via vitacompanion. + /// This will upload the configuration file with the ip address of your host and a port to your Vita. + Configure(Configure), +} + +#[derive(Args, Debug)] +pub struct Configure { + #[command(flatten)] + pub connection: ConnectionArgs, + + /// The IP address of the host to send logs to. + /// Vita will connect to this address via TCP write logs to the socket. + #[arg(long)] + host_ip_address: Option, + + /// Enabled kernel debug logs. + #[arg(long, default_value = "false")] + kernel_debug: bool, +} + +static MAGIC: [u8; 4] = [b'N', b'L', b'M', 0]; +static NLM_CONFIG_FLAGS_BIT_QAF_DEBUG_PRINTF: u32 = 1 << 0; + +struct PrincessLogConfig { + magic: [u8; 4], + ip: Ipv4Addr, + port: u16, + kernel_debug: bool, +} + +impl PrincessLogConfig { + fn new(ip: Ipv4Addr, port: u16, kernel_debug: bool) -> Self { + Self { + magic: MAGIC, + ip, + port, + kernel_debug, + } + } + + fn parse(config: &mut R) -> anyhow::Result { + let mut magic = [0; 4]; + config.read_exact(&mut magic)?; + + let mut buffer = [0; 4]; + config.read_exact(&mut buffer)?; + let ip = Ipv4Addr::from(buffer); + config.read_exact(&mut buffer)?; + let flags = u32::from_le_bytes(buffer); + let mut buffer = [0; 2]; + config.read_exact(&mut buffer)?; + let port = u16::from_le_bytes(buffer); + + let kernel_debug = (flags & NLM_CONFIG_FLAGS_BIT_QAF_DEBUG_PRINTF) != 0; + + Ok(Self { + magic, + ip, + port, + kernel_debug, + }) + } + + fn serialize(&self) -> Vec { + let flags = match self.kernel_debug { + true => NLM_CONFIG_FLAGS_BIT_QAF_DEBUG_PRINTF, + false => 0, + }; + + let mut res = Vec::with_capacity(16); + res.extend_from_slice(&MAGIC); + res.extend_from_slice(&self.ip.octets()); + res.extend_from_slice(&flags.to_le_bytes()); + res.extend_from_slice(&self.port.to_le_bytes()); + res.extend_from_slice(&[0, 0]); + + res + } +} + +impl Logs { + fn configure(&self, configure: &Configure) -> anyhow::Result<()> { + let filename = "ur0:/data/NetLoggingMgrConfig.bin"; + debug!( + "{} {filename}", + "Downloading the existing config from".blue() + ); + let mut ftp = ftp::connect(&configure.connection)?; + + match ftp.retr_as_buffer(filename) { + Ok(mut file) => { + info!("{}", "Found existing config".blue()); + + match PrincessLogConfig::parse(&mut file) { + Ok(c) => match c.magic == MAGIC { + true => info!( + "{} {ip}:{port} {} {kdbg}", + "Existing config has address".yellow(), + "and kernel debug print is".yellow(), + ip = c.ip, + port = c.port, + kdbg = c.kernel_debug + ), + false => warn!("{}", "Existing config has invalid magic".red()), + }, + Err(err) => warn!("{}: {err}", "Failed to parse existing config".red()), + }; + } + Err(err) => { + warn!("{}: {err}", "Failed to download existing config".red()); + } + } + + let ip = match &configure.host_ip_address { + Some(ip) => Ipv4Addr::from_str(ip)?, + None => match local_ip_address::local_ip()? { + std::net::IpAddr::V4(ip) => ip, + std::net::IpAddr::V6(_) => bail!("Unable to guess host ip address"), + }, + }; + + info!( + "{} {ip}:{port} {} {kdbg}", + "Setting log address to".blue(), + "and kernel debug print to".blue(), + port = self.port, + kdbg = configure.kernel_debug, + ); + + info!("{} {}", "Saving config to".blue(), filename); + let _ = ftp.mkdir("ur0:/data/"); + let cfg = PrincessLogConfig::new(ip, self.port, configure.kernel_debug).serialize(); + ftp.put_file(filename, &mut Cursor::new(cfg))?; + + info!( + "{}", + "Config will take effect after Vita is rebooted".yellow() + ); + + Ok(()) + } + fn listen(&self) -> anyhow::Result<()> { info!("{} {}", "Starting TCP server on port".blue(), self.port); let listener = @@ -57,3 +210,12 @@ impl Executor for Logs { Ok(()) } } + +impl Executor for Logs { + fn execute(&self) -> anyhow::Result<()> { + match self.cmd.as_ref() { + Some(LogsCmd::Configure(cmd)) => self.configure(cmd), + Some(LogsCmd::Listen) | None => self.listen(), + } + } +}