From: Simon Tatham Date: Thu, 4 Jan 2024 10:17:27 +0000 (+0000) Subject: CLI option to log HTTP transactions to a file. X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~ian/git?a=commitdiff_plain;h=42f5c60226cc4a86708b2e485c423299ecce614d;p=mastodonochrome.git CLI option to log HTTP transactions to a file. This allows me to actually see what's going on when the client misbehaves. --- diff --git a/src/client.rs b/src/client.rs index 1f94d61..46ce4a5 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,6 +1,7 @@ use reqwest::Url; use std::collections::{HashMap, HashSet, VecDeque}; -use std::io::Read; +use std::io::{Read, Write, IoSlice}; +use std::fs::File; use super::auth::{AuthConfig,AuthError}; use super::config::ConfigLocation; @@ -61,6 +62,7 @@ pub struct Client { feeds: HashMap, instance: Option, permit_write: bool, + logfile: Option, } #[derive(Debug, PartialEq, Eq, Clone)] @@ -299,6 +301,20 @@ impl TransactionLogEntry { lines } + pub fn write_to(&self, logfile: &mut Option) { + if let Some(ref mut file) = logfile { + for line in self.format_full() { + // Deliberately squash any error from writing to the + // log file. If the disk fills up, it's a shame to + // lose the logs, but we don't _also_ want to + // terminate the client in a panic. + let _ = file.write_vectored(&[IoSlice::new(line.as_bytes()), + IoSlice::new(b"\n")]); + } + } + } +} + pub fn execute_and_log_request(client: &reqwest::blocking::Client, req: reqwest::blocking::Request) -> Result<(reqwest::blocking::Response, TransactionLogEntry), ClientError> @@ -334,6 +350,7 @@ impl Client { feeds: HashMap::new(), instance: None, permit_write: false, + logfile: None, }) } @@ -341,6 +358,10 @@ impl Client { self.permit_write = permit; } + pub fn set_logfile(&mut self, file: Option) { + self.logfile = file; + } + pub fn fq(&self, acct: &str) -> String { match acct.contains('@') { true => acct.to_owned(), @@ -357,6 +378,7 @@ impl Client { } fn consume_transaction_log(&mut self, log: TransactionLogEntry) { + log.write_to(&mut self.logfile) } fn api_request_cl(&self, client: &reqwest::blocking::Client, req: Req) -> diff --git a/src/login.rs b/src/login.rs index 2ef2303..7c771ef 100644 --- a/src/login.rs +++ b/src/login.rs @@ -1,5 +1,6 @@ use reqwest::Url; use std::io::Write; +use std::fs::File; use super::TopLevelError; use super::auth::{AuthConfig, AuthError}; @@ -10,6 +11,7 @@ use super::types::{Account, Application, Instance, Token}; struct Login { instance_url: String, client: reqwest::blocking::Client, + logfile: Option, } enum AppTokenType<'a> { @@ -21,22 +23,26 @@ use AppTokenType::*; const REDIRECT_MAGIC_STRING: &'static str = "urn:ietf:wg:oauth:2.0:oob"; impl Login { - fn new(instance_url: &str) -> Result { + fn new(instance_url: &str, logfile: Option) + -> Result + { Ok(Login { instance_url: instance_url.to_owned(), client: reqwest_client()?, + logfile, }) } - fn execute_request(&self, req: reqwest::blocking::RequestBuilder) + fn execute_request(&mut self, req: reqwest::blocking::RequestBuilder) -> Result { - let (rsp, _log) = execute_and_log_request( + let (rsp, log) = execute_and_log_request( &self.client, req.build()?)?; + log.write_to(&mut self.logfile); Ok(rsp) } - fn register_client(&self) -> Result { + fn register_client(&mut self) -> Result { let (url, req) = Req::post("/api/v1/apps") .param("redirect_uris", REDIRECT_MAGIC_STRING) .param("client_name", "Mastodonochrome") @@ -57,7 +63,7 @@ impl Login { } } - fn get_token(&self, app: &Application, toktype: AppTokenType) -> + fn get_token(&mut self, app: &Application, toktype: AppTokenType) -> Result { let client_id = match &app.client_id { @@ -96,8 +102,8 @@ impl Login { } } - fn get serde::Deserialize<'a>>(&self, path: &str, token: &str) -> - Result + fn get serde::Deserialize<'a>>( + &mut self, path: &str, token: &str) -> Result { let (url, req) = Req::get(path) .build(&self.instance_url, &self.client, Some(token))?; @@ -131,7 +137,8 @@ impl Login { } } -pub fn login(cfgloc: &ConfigLocation, instance_url: &str) -> +pub fn login(cfgloc: &ConfigLocation, instance_url: &str, + logfile: Option) -> Result<(), TopLevelError> { // First, check we aren't logged in already, and give some @@ -157,7 +164,7 @@ pub fn login(cfgloc: &ConfigLocation, instance_url: &str) -> }?; let instance_url = url.as_str().trim_end_matches('/'); - let login = Login::new(instance_url)?; + let mut login = Login::new(instance_url, logfile)?; // Register the client and get its details let app = login.register_client()?; diff --git a/src/main.rs b/src/main.rs index 2545bb7..7ec6ba2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,6 +16,10 @@ struct Args { #[arg(short, long, action(clap::ArgAction::SetTrue))] readonly: bool, + /// HTTP logging mode: the client logs all its transactions to a file. + #[arg(long)] + loghttp: Option, + /// Log in to a server, instead of running the main user interface. /// Provide the top-level URL of the instance website. #[arg(long, conflicts_with("readonly"))] @@ -28,9 +32,13 @@ fn main_inner() -> Result<(), TopLevelError> { None => ConfigLocation::default()?, Some(dir) => ConfigLocation::from_pathbuf(dir), }; + let httplogfile = match cli.loghttp { + None => None, + Some(path) => Some(std::fs::File::create(path)?), + }; match cli.login { - None => Tui::run(&cfgloc, cli.readonly)?, - Some(ref server) => login(&cfgloc, server)?, + None => Tui::run(&cfgloc, cli.readonly, httplogfile)?, + Some(ref server) => login(&cfgloc, server, httplogfile)?, } Ok(()) } diff --git a/src/tui.rs b/src/tui.rs index e6756d3..626f2b2 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -13,6 +13,7 @@ use ratatui::{ use std::cmp::min; use std::collections::HashSet; use std::io::{Stdout, Write, stdout}; +use std::fs::File; use unicode_width::UnicodeWidthStr; use super::activity_stack::*; @@ -195,7 +196,8 @@ impl std::fmt::Display for TuiError { } impl Tui { - pub fn run(cfgloc: &ConfigLocation, readonly: bool) -> + pub fn run(cfgloc: &ConfigLocation, readonly: bool, + logfile: Option) -> Result<(), TuiError> { let (sender, receiver) = std::sync::mpsc::sync_channel(1); @@ -213,6 +215,7 @@ impl Tui { let mut client = Client::new(cfgloc)?; client.set_writable(!readonly); + client.set_logfile(logfile); stdout().execute(EnterAlternateScreen)?; enable_raw_mode()?;