Skip to content
Closed
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ Starting from 0.2.0, CLI / Extension / DSH Plugin share the same version number.

### Fixed

- CLI: switch the attached Windows console to UTF-8 for the lifetime of the
process, so non-ASCII page text is printed instead of mojibake on a console
with an OEM code page. The previous code page is restored on exit; redirected
streams, non-Windows hosts and consoles already using UTF-8 are left alone.
- Extension: input to a background Agent Window tab no longer keeps failing with
`input_not_ready` after Chrome drops the session's focus override without a detach
([#355](https://github.com/Tencent/BrowserSkill/issues/355)). The session's
Expand Down
1 change: 1 addition & 0 deletions crates/bsk-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ windows-sys = { version = "0.59", features = [
"Win32_System_Threading",
"Win32_System_JobObjects",
"Win32_System_Pipes",
"Win32_System_Console",
"Win32_Security",
"Win32_Storage_FileSystem",
] }
Expand Down
122 changes: 122 additions & 0 deletions crates/bsk-cli/src/cli/console_encoding.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
//! Keep console output readable when the text is not ASCII.
//!
//! `bsk` prints UTF-8 on every platform. A Windows console, however, starts
//! on an OEM code page — 936/GBK on a Simplified Chinese system — and
//! reinterprets those bytes, so the non-ASCII text a page contributes
//! (snapshot names, element labels, error hints) reaches the user as
//! mojibake: `微众银行` renders as `寰紬閾惰`. Pointing the console at UTF-8
//! for the lifetime of the process fixes the rendering, and the previous
//! code page is restored on drop so the user's shell is left as found.

/// Code page identifier for UTF-8.
#[cfg(windows)]
const UTF8_CODE_PAGE: u32 = 65001;

/// Switches the attached Windows console to UTF-8 output for its lifetime.
///
/// Holds the previous code page and restores it when dropped, so the guard
/// must outlive every write. A no-op on non-Windows hosts, when no console is
/// attached, and when the console already uses UTF-8. Redirected streams are
/// never touched: those receive the raw bytes verbatim, with no code page in
/// the path.
#[must_use = "the console reverts to its previous code page when the guard drops"]
pub struct Utf8Console {
#[cfg(windows)]
restore: Option<u32>,
}

impl Utf8Console {
/// Enable UTF-8 console output when a console is attached and not UTF-8.
pub fn enable() -> Self {
#[cfg(windows)]
{
// SAFETY: no pointer arguments; a missing console is reported as
// code page 0, which `restore_code_page` rejects.
let current = unsafe { GetConsoleOutputCP() };
let mut restore = None;
if let Some(code_page) = restore_code_page(console_attached(), current) {
// SAFETY: plain code page id; no pointers.
if unsafe { SetConsoleOutputCP(UTF8_CODE_PAGE) } != 0 {
restore = Some(code_page);
}
}
Self { restore }
}
#[cfg(not(windows))]
{
Self {}
}
}
}

#[cfg(windows)]
impl Drop for Utf8Console {
fn drop(&mut self) {
if let Some(code_page) = self.restore {
// SAFETY: plain code page id; no pointers.
unsafe {
SetConsoleOutputCP(code_page);
}
}
}
}

/// The code page to restore afterwards, or `None` when nothing must change.
///
/// Split out from [`Utf8Console::enable`] so the decision can be tested
/// without an attached console.
#[cfg(windows)]
fn restore_code_page(attached: bool, current: u32) -> Option<u32> {
if !attached || current == 0 || current == UTF8_CODE_PAGE {
return None;
}
Some(current)
}

/// Whether the process owns a console on either output stream.
///
/// `stderr` counts because diagnostics and the update hint go there, and a
/// single console serves both streams.
#[cfg(windows)]
fn console_attached() -> bool {
std::io::stdout().is_terminal() || std::io::stderr().is_terminal()
}

#[cfg(windows)]
use std::io::IsTerminal;
#[cfg(windows)]
use windows_sys::Win32::System::Console::{GetConsoleOutputCP, SetConsoleOutputCP};

#[cfg(all(test, windows))]
mod tests {
use super::*;

#[test]
fn redirected_output_is_left_alone() {
assert_eq!(restore_code_page(false, 936), None);
}

#[test]
fn a_utf8_console_is_left_alone() {
assert_eq!(restore_code_page(true, UTF8_CODE_PAGE), None);
}

#[test]
fn a_missing_console_is_left_alone() {
assert_eq!(restore_code_page(true, 0), None);
}

#[test]
fn an_oem_code_page_is_remembered_for_restore() {
assert_eq!(restore_code_page(true, 936), Some(936));
assert_eq!(restore_code_page(true, 437), Some(437));
}

#[test]
fn the_guard_is_inert_under_a_redirected_harness() {
// `cargo test` captures both streams, so no console is attached and
// enabling the guard must not touch global console state.
let guard = Utf8Console::enable();
assert!(guard.restore.is_none());
}
}
1 change: 1 addition & 0 deletions crates/bsk-cli/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub mod browser_wait;
pub mod browsers;
pub mod business_rpc;
pub mod console;
pub mod console_encoding;
pub mod daemon;
pub mod debug;
pub mod dialogs;
Expand Down
5 changes: 5 additions & 0 deletions crates/bsk-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ use bsk::cli::status::Output;
use bsk::{Cli, Command, cli};

fn main() -> ExitCode {
// A Windows console starts on an OEM code page, so the UTF-8 bytes bsk
// prints would reach a human as mojibake for any non-ASCII page text.
// The guard stays alive for the whole run and restores the code page on
// the way out; it is inert when output is redirected.
let _console = bsk::cli::console_encoding::Utf8Console::enable();
let cli = match Cli::try_parse() {
Ok(cli) => cli,
Err(err) => {
Expand Down