Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 43 additions & 42 deletions jetsocat/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@ use jetsocat::pipe::PipeMode;
use jetsocat::proxy::{ProxyConfig, ProxyType, detect_proxy};
use jmux_proxy::JmuxConfig;
use seahorse::{App, Command, Context, Flag, FlagType};
use std::env;
use std::cmp::PartialEq;
use std::error::Error;
use std::future::Future;
use std::io::IsTerminal;
use std::path::PathBuf;
use std::time::Duration;
use std::{env, io};
use tokio::runtime;

fn main() {
Expand Down Expand Up @@ -462,7 +464,7 @@ fn apply_common_flags(cmd: Command) -> Command {
.flag(Flag::new("watch-process", FlagType::Int).description("Watch given process and stop piping when it dies"))
}

#[derive(Debug)]
#[derive(Debug, PartialEq)]
enum Logging {
Term,
File { filepath: PathBuf, clean_old: bool },
Expand Down Expand Up @@ -559,41 +561,17 @@ impl CommonArgs {
let coloring = match opt_string_flag(c, "color")?.as_deref() {
Some("never") => Coloring::Never,
Some("always") => Coloring::Always,
Some("auto") => Coloring::Auto,
Some("auto") | None => Coloring::Auto,
Some(_) => anyhow::bail!("invalid value for 'color'; expect: `never`, `always` or `auto`"),
None => {
// Infer using the environment.
parse_env_for_coloring()
}
};

return Ok(Self {
Ok(Self {
logging,
coloring,
proxy_cfg,
pipe_timeout,
watch_process,
});

fn parse_env_for_coloring() -> Coloring {
// https://no-color.org/
if env::var("NO_COLOR").is_ok() {
return Coloring::Never;
}

match env::var("FORCE_COLOR").as_deref() {
Ok("0" | "false" | "no" | "off") => return Coloring::Never,
Ok(_) => return Coloring::Always,
Err(_) => {}
}

match env::var("TERM").as_deref() {
Ok("dumb") => return Coloring::Never,
_ => {}
}

Coloring::Auto
}
})
}
}

Expand Down Expand Up @@ -976,6 +954,39 @@ struct LoggerGuard {
_worker_guard: tracing_appender::non_blocking::WorkerGuard,
}

fn is_ansi_supported(logging: &Logging, coloring: Coloring) -> bool {
match coloring {
Coloring::Never => false,
Coloring::Always => true,
Coloring::Auto => {
if env::var("NO_COLOR").is_ok() {
return false;
}

match env::var("FORCE_COLOR").as_deref() {
Ok("0" | "false" | "no" | "off") => return false,
Ok(_) => return true,
Err(_) => {}
}

if logging == &Logging::Term {
// Check whether stderr refers to a terminal. If it's redirected or piped, ANSI is disabled.
return if io::stderr().is_terminal() {
if let Ok("dumb") = env::var("TERM").as_deref() {
return false;
}

true
} else {
false
};
}

false
}
}
}

fn setup_logger(logging: &Logging, coloring: Coloring) -> LoggerGuard {
use std::fs::OpenOptions;
use std::panic;
Expand All @@ -984,26 +995,16 @@ fn setup_logger(logging: &Logging, coloring: Coloring) -> LoggerGuard {
use tracing_subscriber::prelude::*;
use tracing_subscriber::{EnvFilter, fmt};

let ansi = is_ansi_supported(logging, coloring);

let (layer, guard) = match &logging {
Logging::Term => {
let ansi = match coloring {
Coloring::Never => false,
Coloring::Always => true,
Coloring::Auto => true,
};

let (non_blocking_stdio, guard) = tracing_appender::non_blocking(std::io::stdout());
let (non_blocking_stdio, guard) = tracing_appender::non_blocking(io::stderr());
let stdio_layer = fmt::layer().with_writer(non_blocking_stdio).with_ansi(ansi);

(stdio_layer, guard)
}
Logging::File { filepath, clean_old: _ } => {
let ansi = match coloring {
Coloring::Never => false,
Coloring::Always => true,
Coloring::Auto => false,
};

let file = OpenOptions::new()
.create(true)
.write(true)
Expand Down
42 changes: 20 additions & 22 deletions testsuite/tests/cli/jetsocat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,15 @@ fn all_subcommands() {
}

#[rstest]
#[case::default(&[], &[], true)]
#[case::default(&[], &[], false)] // is_terminal = false
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In tests, assert_cmd is used to run jetsocat in a child process, where stderr is piped. This means that we always have is_terminal = false. That's why, for example, log_term_coloring with the default case will return false.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ll investigate if we can still check the TTY case properly.

#[case::cli_always(&["--color=always"], &[], true)]
#[case::cli_never(&["--color=never"], &[], false)]
#[case::cli_auto(&["--color=auto"], &[], true)]
#[case::cli_auto(&["--color=auto"], &[], false)] // is_terminal = false
#[case::cli_always_and_env(&["--color=always"], &[("NO_COLOR", "")], true)]
#[case::cli_auto_and_env(&["--color=auto"], &[("NO_COLOR", "")], true)]
#[case::cli_auto_and_env(&["--color=auto"], &[("NO_COLOR", "")], false)] // is_terminal = false
#[case::env_no_color(&[], &[("NO_COLOR", ""), ("FORCE_COLOR", "1")], false)]
#[case::env_term_dumb(&[], &[("TERM", "dumb")], false)]
#[case::env_term_other(&[], &[("TERM", "other")], true)]
#[case::env_term_other(&[], &[("TERM", "other")], false)] // is_terminal = false
#[case::env_force_color_0(&[], &[("FORCE_COLOR", "0")], false)]
#[case::env_force_color_1(&[], &[("FORCE_COLOR", "1"), ("TERM", "dumb")], true)]
fn log_term_coloring(#[case] args: &[&str], #[case] envs: &[(&str, &str)], #[case] expect_ansi: bool) {
Expand All @@ -57,12 +57,12 @@ fn log_term_coloring(#[case] args: &[&str], #[case] envs: &[(&str, &str)], #[cas
.assert()
.success();

let stdout = std::str::from_utf8(&output.get_output().stdout).unwrap();
let stderr = std::str::from_utf8(&output.get_output().stderr).unwrap();

if expect_ansi {
assert!(stdout.contains("  INFO jetsocat"), "{stdout}");
assert!(stderr.contains("  INFO jetsocat"), "{stderr}");
} else {
assert!(stdout.contains(" INFO jetsocat"), "{stdout}");
assert!(stderr.contains(" INFO jetsocat"), "{stderr}");
}
}

Expand All @@ -72,7 +72,7 @@ fn log_term_coloring(#[case] args: &[&str], #[case] envs: &[(&str, &str)], #[cas
#[case::cli_never(&["--color", "never"], &[], false)]
#[case::cli_auto(&["--color", "auto"], &[], false)]
#[case::cli_always_and_env(&["--color", "always"], &[("NO_COLOR", "1")], true)]
#[case::cli_auto_and_env(&["--color", "auto"], &[("FORCE_COLOR", "1")], false)]
#[case::cli_auto_and_env(&["--color", "auto"], &[("FORCE_COLOR", "1")], true)]
#[case::env_no_color(&[], &[("NO_COLOR", "1"), ("FORCE_COLOR", "1")], false)]
#[case::env_term_dumb(&[], &[("TERM", "dumb")], false)]
#[case::env_term_other(&[], &[("TERM", "other")], false)]
Expand Down Expand Up @@ -481,11 +481,9 @@ fn jetsocat_log_environment_variable() {
.timeout(COMMAND_TIMEOUT)
.assert();

let stdout = std::str::from_utf8(&output.get_output().stdout).unwrap();
assert!(stdout.contains("DEBUG"));
assert!(stdout.contains("hello"));

let stderr = std::str::from_utf8(&output.get_output().stderr).unwrap();
assert!(stderr.contains("DEBUG"));
assert!(stderr.contains("hello"));
assert!(!stderr.contains("bad"));
assert!(!stderr.contains("invalid"));
assert!(!stderr.contains("unknown"));
Expand Down Expand Up @@ -874,7 +872,7 @@ async fn execute_mcp_request(request: &str) -> String {
let mut jetsocat_process = jetsocat_tokio_cmd()
.args(&["mcp-proxy", "stdio", &server_url, "--log-term", "--color=never"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.kill_on_drop(true)
.spawn()
.expect("start jetsocat mcp-proxy");
Expand All @@ -894,23 +892,23 @@ async fn execute_mcp_request(request: &str) -> String {
jetsocat_process.start_kill().unwrap();

let output = jetsocat_process.wait_with_output().await.unwrap();
String::from_utf8(output.stdout).unwrap()
String::from_utf8(output.stderr).unwrap()
}

#[tokio::test]
async fn mcp_proxy_malformed_request_with_id() {
let stdout = execute_mcp_request("{\"jsonrpc\":\"2.0\",\"decoy\":\":\",\"id\":1\n").await;
assert!(stdout.contains("Malformed JSON-RPC request from client"), "{stdout}");
assert!(stdout.contains("Unexpected EOF"), "{stdout}");
assert!(stdout.contains("id=1"), "{stdout}");
let stderr = execute_mcp_request("{\"jsonrpc\":\"2.0\",\"decoy\":\":\",\"id\":1\n").await;
assert!(stderr.contains("Malformed JSON-RPC request from client"), "{stderr}");
assert!(stderr.contains("Unexpected EOF"), "{stderr}");
assert!(stderr.contains("id=1"), "{stderr}");
}

#[tokio::test]
async fn mcp_proxy_malformed_request_no_id() {
let stdout = execute_mcp_request("{\"jsonrpc\":\"2.0\",}\n").await;
assert!(stdout.contains("Malformed JSON-RPC request from client"), "{stdout}");
assert!(stdout.contains("Invalid character"), "{stdout}");
assert!(!stdout.contains("id=1"), "{stdout}");
let stderr = execute_mcp_request("{\"jsonrpc\":\"2.0\",}\n").await;
assert!(stderr.contains("Malformed JSON-RPC request from client"), "{stderr}");
assert!(stderr.contains("Invalid character"), "{stderr}");
assert!(!stderr.contains("id=1"), "{stderr}");
}

#[tokio::test]
Expand Down