refactor(core): improve iOS log messages from stdout/stderr (#14385)

* refactor(core): improve iOS log messages from stdout/stderr

move the stdout/stderr forward logic to Swift so it does not consume a Rust thread and never deadlocks on the simulator

I had to work on this because i'm facing #12172 again on latest Xcode (26.1)

* patch
This commit is contained in:
Lucas Fernandes Nogueira 2025-11-13 08:18:50 -03:00 committed by GitHub
parent 8e3bd63db9
commit 4b00130b86
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 99 additions and 31 deletions

View File

@ -0,0 +1,6 @@
---
"tauri": patch:bug
"tauri-macros": patch:bug
---
Fix iOS deadlock when running on the simulator from Xcode by properly piping stdout/stderr messages through the Xcode console and OSLog.

View File

@ -4,6 +4,86 @@
import os.log
import UIKit
import Foundation
class StdoutRedirector {
private var originalStdout: Int32 = -1
private var originalStderr: Int32 = -1
private var stdoutPipe: [Int32] = [-1, -1]
private var stderrPipe: [Int32] = [-1, -1]
private var stdoutReadSource: DispatchSourceRead?
private var stderrReadSource: DispatchSourceRead?
func start() {
originalStdout = dup(STDOUT_FILENO)
originalStderr = dup(STDERR_FILENO)
guard Darwin.pipe(&stdoutPipe) == 0,
Darwin.pipe(&stderrPipe) == 0 else {
Logger.error("Failed to create stdout/stderr pipes")
return
}
dup2(stdoutPipe[1], STDOUT_FILENO)
dup2(stderrPipe[1], STDERR_FILENO)
close(stdoutPipe[1])
close(stderrPipe[1])
stdoutReadSource = createReader(
readPipe: stdoutPipe[0],
writeToOriginal: originalStdout,
label: "stdout"
)
stderrReadSource = createReader(
readPipe: stderrPipe[0],
writeToOriginal: originalStderr,
label: "stderr"
)
}
private func createReader(
readPipe: Int32,
writeToOriginal: Int32,
label: String
) -> DispatchSourceRead {
let source = DispatchSource.makeReadSource(
fileDescriptor: readPipe,
queue: .global(qos: .utility)
)
source.setEventHandler {
let bufferSize = 4096
var buffer = [UInt8](repeating: 0, count: bufferSize)
let bytesRead = read(readPipe, &buffer, bufferSize)
if bytesRead > 0 {
let output = String(
bytes: buffer[0..<bytesRead],
encoding: .utf8
) ?? ""
let trimmed = output.trimmingCharacters(in: .newlines)
if !trimmed.isEmpty {
// we're sending stderr to oslog, so we need to avoid recursive calls
if trimmed.hasPrefix("OSLOG-") {
// make sure the system can parse the oslogs
write(writeToOriginal, &buffer, bytesRead)
} else {
Logger.info("[\(label)] \(trimmed)")
}
}
}
}
source.setCancelHandler {
close(readPipe)
}
source.resume()
return source
}
}
/// Wrapper class for os_log function
public class Logger {
@ -21,7 +101,7 @@ public class Logger {
}
}
static func log(_ items: Any..., category: String, type: OSLogType) {
static func log(_ items: [Any], category: String, type: OSLogType) {
if Logger.enabled {
var message = ""
let last = items.count - 1
@ -31,6 +111,7 @@ public class Logger {
message += " "
}
}
let log = OSLog(subsystem: Bundle.main.bundleIdentifier ?? "-", category: category)
os_log("%{public}@", log: log, type: type, String(message.prefix(4068)))
}

View File

@ -109,6 +109,14 @@ extension PluginManager: NSCopying {
}
}
private var stdoutRedirector: StdoutRedirector?
@_cdecl("log_stdout")
func logStdout() {
stdoutRedirector = StdoutRedirector()
stdoutRedirector!.start()
}
@_cdecl("register_plugin")
func registerPlugin(name: SRString, plugin: NSObject, config: SRString, webview: WKWebView?) {
PluginManager.shared.load(

View File

@ -46,3 +46,4 @@ swift!(pub fn register_plugin(
webview: *const c_void
));
swift!(pub fn on_webview_created(webview: *const c_void, controller: *const c_void));
swift!(pub fn log_stdout());

View File

@ -248,38 +248,10 @@ pub const VERSION: &str = env!("CARGO_PKG_VERSION");
#[cfg(target_os = "ios")]
#[doc(hidden)]
pub fn log_stdout() {
use std::{
ffi::CString,
fs::File,
io::{BufRead, BufReader},
os::unix::prelude::*,
thread,
};
let mut logpipe: [RawFd; 2] = Default::default();
#[cfg(target_os = "ios")]
unsafe {
libc::pipe(logpipe.as_mut_ptr());
libc::dup2(logpipe[1], libc::STDOUT_FILENO);
libc::dup2(logpipe[1], libc::STDERR_FILENO);
crate::ios::log_stdout();
}
thread::spawn(move || unsafe {
let file = File::from_raw_fd(logpipe[0]);
let mut reader = BufReader::new(file);
let mut buffer = String::new();
loop {
buffer.clear();
if let Ok(len) = reader.read_line(&mut buffer) {
if len == 0 {
break;
} else if let Ok(msg) = CString::new(buffer.as_bytes())
.map_err(|_| ())
.and_then(|c| c.into_string().map_err(|_| ()))
{
log::info!("{}", msg);
}
}
}
});
}
/// The user event type.