From c84b0748b7ae8befec8a0c752d1e27e342915fbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Boschi?= Date: Wed, 4 Nov 2026 15:22:55 +0100 Subject: [PATCH] fix: don't fail on non-UTF-8 postmaster.pid and server logs On Chinese Windows, PostgreSQL writes the data directory path in postmaster.pid in the ANSI code page (GBK). pg0 read the file as strict UTF-8, so start failed with 'stream did not contain valid UTF-8'. Read it, the log tail and pg0 logs loosely. Fixes #46. --- src/main.rs | 48 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/src/main.rs b/src/main.rs index abe1d71..0b0a875 100644 --- a/src/main.rs +++ b/src/main.rs @@ -375,7 +375,9 @@ fn running_pid(info: &InstanceInfo) -> Option { /// Read the PID from PostgreSQL's postmaster.pid file fn read_postmaster_pid(data_dir: &PathBuf) -> Result { let pid_file = data_dir.join("postmaster.pid"); - let content = fs::read_to_string(&pid_file)?; + // Read loosely: postmaster.pid also holds the data directory path, which + // PostgreSQL on Windows writes in the ANSI code page (e.g. GBK), not UTF-8 (#46). + let content = String::from_utf8_lossy(&fs::read(&pid_file)?).into_owned(); // First line of postmaster.pid is the PID content .lines() @@ -431,7 +433,8 @@ fn read_latest_pg_log(data_dir: &PathBuf) -> Option { }); let log_file = log_files.first()?; - let content = fs::read_to_string(log_file.path()).ok()?; + // The server log may be in the OS code page, not UTF-8 (#46). + let content = String::from_utf8_lossy(&fs::read(log_file.path()).ok()?).into_owned(); // Get last 20 lines let lines: Vec<&str> = content.lines().collect(); @@ -1473,6 +1476,12 @@ fn psql(name: String, args: Vec) -> Result<(), CliError> { Ok(()) } +/// One log line as text. The server log may be in the OS code page rather +/// than UTF-8 (e.g. GBK on Chinese Windows, #46), so never fail on it. +fn lossy_line(bytes: &[u8]) -> String { + String::from_utf8_lossy(bytes).trim_end_matches('\r').to_string() +} + fn logs(name: String, lines: Option, follow: bool) -> Result<(), CliError> { let instance_dir = get_instance_dir(&name)?; let log_dir = instance_dir.join("data").join("log"); @@ -1517,16 +1526,16 @@ fn logs(name: String, lines: Option, follow: bool) -> Result<(), CliError use std::io::{BufRead, BufReader, Seek, SeekFrom}; file.seek(SeekFrom::Start(0))?; let reader = BufReader::new(&file); - for line in reader.lines() { - println!("{}", line?); + for line in reader.split(b'\n') { + println!("{}", lossy_line(&line?)); } // Now follow new content loop { file.seek(SeekFrom::Start(pos))?; let reader = BufReader::new(&file); - for line in reader.lines() { - println!("{}", line?); + for line in reader.split(b'\n') { + println!("{}", lossy_line(&line?)); } pos = file.metadata()?.len(); std::thread::sleep(std::time::Duration::from_millis(100)); @@ -1536,7 +1545,10 @@ fn logs(name: String, lines: Option, follow: bool) -> Result<(), CliError use std::io::{BufRead, BufReader}; let file = fs::File::open(log_file)?; let reader = BufReader::new(file); - let all_lines: Vec<_> = reader.lines().collect::>()?; + let all_lines: Vec = reader + .split(b'\n') + .map(|line| line.map(|l| lossy_line(&l))) + .collect::>()?; let lines_to_show = if let Some(n) = lines { &all_lines[all_lines.len().saturating_sub(n)..] @@ -1748,6 +1760,28 @@ fn init_logging(verbose: bool) { mod directory_tests { use super::*; + #[test] + fn postmaster_pid_with_non_utf8_path_is_readable() { + // Regression for #46: PostgreSQL on Chinese Windows writes the data + // directory (line 2) in GBK. "用户" in GBK is not valid UTF-8. + let dir = std::env::temp_dir().join(format!("pg0-gbk-{}", process::id())); + fs::create_dir_all(&dir).unwrap(); + let mut content = b"4242\nC:\\Users\\".to_vec(); + content.extend_from_slice(&[0xd3, 0xc3, 0xbb, 0xa7]); + content.extend_from_slice(b"\\.pg0\\data\n1700000000\n5432\n"); + fs::write(dir.join("postmaster.pid"), content).unwrap(); + + let pid = read_postmaster_pid(&dir); + fs::remove_dir_all(&dir).unwrap(); + assert_eq!(pid.unwrap(), 4242); + } + + #[test] + fn lossy_line_handles_non_utf8_and_crlf() { + assert_eq!(lossy_line(b"LOG: ready\r"), "LOG: ready"); + assert!(lossy_line(&[0xd3, 0xc3, b'!']).ends_with('!')); + } + #[test] fn unset_directory_keeps_the_default() { assert!(configured_directory("PG0_HOME", None).unwrap().is_none());