* Harden Tauri backend preflight and startup
Require managed Studio root IDs to match before attaching to existing backends, close the concurrent backend-start window, and tighten frontend Tauri detection to Tauri-specific signals.
* Add Tauri backend manageability guards
Gate desktop backend compatibility on explicit manageability fields, add external-conflict handling for unsafe backend states, and protect update/repair paths from mutating active non-owned Studio backends. Track Tauri-owned backends with local owner metadata for verified orphan cleanup only.
* Split Tauri preflight probes into modules
Move preflight types, version checks, managed install probing, and backend probing into focused submodules while preserving behavior and keeping implementation files under the release-readiness size target.
* Use desktop-specific Tauri updater channel
Point the desktop updater at a same-repo desktop-latest manifest and publish that channel from non-draft desktop releases after validating the Tauri-generated latest.json.
* Add Linux desktop update policy
* Add owned backend lifecycle guards
* Adopt verified desktop-owned backends
* Validate desktop backend readiness
* Trim Tauri release hardening code
* Require desktop backend 2026.5.3
* Handle desktop backend edge cases
* Fail stalled desktop backend startup
* Fix desktop update edge cases
* Avoid secret-gating adopted watchdog
* Fix desktop update comparison guards
* Automate desktop release versioning
* Serialize desktop release workflow
* tests: follow preflight.rs split into preflight/{backend,managed,types,version}.rs
PR #5341 splits studio/src-tauri/src/preflight.rs into a directory of
submodules. The cmd.env_remove("UNSLOTH_STUDIO_HOME") + STUDIO_HOME
calls now live in preflight/managed.rs instead of preflight.rs, so
test_tauri_preflight_scrubs_studio_home_env counted zero matches in
the old single-file location and failed with "assert 0 >= 2".
Read whichever shape is on disk: preflight.rs at the old path plus
every *.rs under preflight/ (current PR has 2 occurrences in
preflight/managed.rs). The guard intent is unchanged: at least 2
env_remove calls covering run_cli_probe and probe_cli_capability,
plus the single commands.rs scrub in check_install_status. Verified
locally: pytest tests/test_studio_install_workspace_guard.py::test_tauri_preflight_scrubs_studio_home_env passes.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Avoid browser Tauri hostname detection
* Restore shutdown flag after failed stop
---------
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
609 lines
22 KiB
Rust
609 lines
22 KiB
Rust
use std::fs::{self, File};
|
||
use std::io::{Read, Seek, SeekFrom};
|
||
use std::path::Path;
|
||
use std::time::UNIX_EPOCH;
|
||
|
||
use super::phase_log::{collect_phase_groups, path_has_symlink};
|
||
use super::redaction::{redact_text, RedactionReport};
|
||
use super::state::{AttemptSummary, CappedStrings, DiagnosticsSnapshot, FrontendSupportSnapshot};
|
||
use super::{
|
||
cap_string, logs_dir, now_ms, studio_dir, valid_utf8_boundary, FOOTER_BUDGET_BYTES,
|
||
MAX_PHASE_LINE_BYTES, REPORT_MAX_BYTES, SCHEMA_VERSION, TAIL_MAX_BYTES, TAIL_MAX_LINES,
|
||
};
|
||
|
||
#[derive(Clone, Debug)]
|
||
pub(crate) struct BackendHealthSection {
|
||
pub(crate) port: u16,
|
||
pub(crate) fields: Vec<(String, String)>,
|
||
pub(crate) warning: Option<String>,
|
||
}
|
||
|
||
pub(crate) fn render_report(
|
||
snapshot: DiagnosticsSnapshot,
|
||
frontend: FrontendSupportSnapshot,
|
||
health: Option<BackendHealthSection>,
|
||
) -> String {
|
||
let mut warnings = Vec::new();
|
||
let mut raw = String::new();
|
||
|
||
raw.push_str("Unsloth Studio Support Diagnostics\n");
|
||
raw.push_str(&format!("diag_report_schema={SCHEMA_VERSION}\n"));
|
||
raw.push_str(&format!("created_at_ms={}\n", now_ms()));
|
||
raw.push_str(&format!("app_version={}\n", env!("CARGO_PKG_VERSION")));
|
||
raw.push_str(&format!("os={}\n", std::env::consts::OS));
|
||
raw.push_str(&format!("arch={}\n", std::env::consts::ARCH));
|
||
raw.push_str(&format!("debug_build={}\n", cfg!(debug_assertions)));
|
||
raw.push_str(&format!(
|
||
"appimage={}\n",
|
||
std::env::var_os("APPIMAGE").is_some()
|
||
));
|
||
raw.push_str(&format!(
|
||
"cpu_logical_count={}\n",
|
||
std::thread::available_parallelism()
|
||
.map(|n| n.get().to_string())
|
||
.unwrap_or_else(|_| "unavailable".to_string())
|
||
));
|
||
raw.push_str(&format!("studio_dir={}\n", studio_dir().display()));
|
||
raw.push_str(&format!("logs_dir={}\n", logs_dir().display()));
|
||
raw.push_str(&format!("app_session_id={}\n", snapshot.app_session_id));
|
||
raw.push_str(&format!(
|
||
"app_started_at_ms={}\n",
|
||
snapshot.app_started_at_ms
|
||
));
|
||
raw.push_str(
|
||
"disk_budget=5MiB x 3 segments x 5 groups x 4 kinds ~= 300MiB plus tauri.log/.1\n",
|
||
);
|
||
|
||
append_frontend_section(&mut raw, &frontend);
|
||
append_preflight_section(&mut raw, &snapshot);
|
||
append_flow_section(&mut raw, &snapshot);
|
||
append_backend_section(&mut raw, &snapshot, health.as_ref());
|
||
append_device_signals_section(&mut raw, &snapshot, health.as_ref());
|
||
append_log_sections(&mut raw, &snapshot, &mut warnings);
|
||
|
||
if let Some(health) = &health {
|
||
if let Some(warning) = &health.warning {
|
||
warnings.push(format!("backend /api/health unavailable: {warning}"));
|
||
}
|
||
}
|
||
|
||
let mut redaction = RedactionReport::default();
|
||
let redacted_body = redact_text(&raw, &mut redaction);
|
||
|
||
// Redact collection warnings/footer too. Some warnings include raw local paths
|
||
// from failed log reads, so the footer must not be appended after redaction.
|
||
let footer_for_count = render_footer(&warnings, &redaction);
|
||
let _ = redact_text(&footer_for_count, &mut redaction);
|
||
let footer = redact_text(
|
||
&render_footer(&warnings, &redaction),
|
||
&mut RedactionReport::default(),
|
||
);
|
||
|
||
enforce_report_limit(redacted_body, footer)
|
||
}
|
||
|
||
fn append_frontend_section(out: &mut String, frontend: &FrontendSupportSnapshot) {
|
||
out.push_str("\n== Trigger/UI state ==\n");
|
||
append_opt(out, "status", frontend.status.as_deref());
|
||
append_opt(out, "error", frontend.error.as_deref());
|
||
append_opt(
|
||
out,
|
||
"current_step_index",
|
||
frontend
|
||
.current_step_index
|
||
.map(|v| v.to_string())
|
||
.as_deref(),
|
||
);
|
||
append_opt(out, "progress_detail", frontend.progress_detail.as_deref());
|
||
append_opt(out, "flow", frontend.flow.as_deref());
|
||
append_opt(out, "update_phase", frontend.update_phase.as_deref());
|
||
let update_progress = frontend.update_progress.as_ref().map(selected_json_value);
|
||
append_opt(out, "update_progress", update_progress.as_deref());
|
||
if let Some(packages) = &frontend.elevation_packages {
|
||
out.push_str(&format!("elevation_packages={}\n", packages.join(", ")));
|
||
}
|
||
out.push_str("last_ui_log_tail:\n");
|
||
match &frontend.last_ui_log_lines {
|
||
Some(lines) if !lines.is_empty() => {
|
||
for line in tail_lines(lines, TAIL_MAX_LINES) {
|
||
out.push_str(" ");
|
||
out.push_str(line);
|
||
out.push('\n');
|
||
}
|
||
}
|
||
_ => out.push_str(" unavailable\n"),
|
||
}
|
||
}
|
||
|
||
fn append_preflight_section(out: &mut String, snapshot: &DiagnosticsSnapshot) {
|
||
out.push_str("\n== Preflight/install state ==\n");
|
||
match &snapshot.latest_preflight {
|
||
Some(preflight) => {
|
||
out.push_str("source=live-state\n");
|
||
out.push_str(&format!("recorded_at_ms={}\n", preflight.recorded_at_ms));
|
||
out.push_str(&format!("disposition={}\n", preflight.disposition));
|
||
append_opt(out, "reason", preflight.reason.as_deref());
|
||
append_opt(
|
||
out,
|
||
"port",
|
||
preflight.port.map(|p| p.to_string()).as_deref(),
|
||
);
|
||
out.push_str(&format!("can_auto_repair={}\n", preflight.can_auto_repair));
|
||
if let Some(path) = &preflight.managed_bin {
|
||
out.push_str(&format!("managed_bin={}\n", path.display()));
|
||
out.push_str(&format!("managed_bin_exists={}\n", path.exists()));
|
||
} else {
|
||
out.push_str("managed_bin=unavailable\n");
|
||
}
|
||
}
|
||
None => out.push_str("source=live-state unavailable\n"),
|
||
}
|
||
}
|
||
|
||
fn append_flow_section(out: &mut String, snapshot: &DiagnosticsSnapshot) {
|
||
out.push_str("\n== Flow summaries ==\n");
|
||
append_attempt_summary(out, "install", snapshot.install.as_ref(), "live-state");
|
||
if !snapshot.install_history.is_empty() {
|
||
out.push_str("install_history:\n");
|
||
for install in snapshot.install_history.iter().rev().take(4).rev() {
|
||
append_attempt_summary(
|
||
out,
|
||
" previous_install",
|
||
Some(install),
|
||
"live-state-history",
|
||
);
|
||
}
|
||
}
|
||
append_attempt_summary(out, "update", snapshot.update.as_ref(), "live-state");
|
||
|
||
out.push_str("repair_groups:\n");
|
||
if snapshot.repair_groups.is_empty() {
|
||
out.push_str(" unavailable\n");
|
||
} else {
|
||
for group in snapshot.repair_groups.iter().rev().take(5).rev() {
|
||
out.push_str(&format!(
|
||
" repair_group_id={} started_at_ms={} ended_at_ms={} final_status={} last_error={}\n",
|
||
group.repair_group_id,
|
||
group.started_at_ms,
|
||
opt_u64(group.ended_at_ms),
|
||
group.final_status.as_deref().unwrap_or("unavailable"),
|
||
group.last_error.as_deref().unwrap_or("unavailable")
|
||
));
|
||
for child in &group.children {
|
||
append_attempt_summary(out, " child", Some(child), "live-state");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fn append_attempt_summary(
|
||
out: &mut String,
|
||
label: &str,
|
||
attempt: Option<&AttemptSummary>,
|
||
source: &str,
|
||
) {
|
||
match attempt {
|
||
Some(attempt) => {
|
||
out.push_str(&format!(
|
||
"{label}: source={source} flow={} attempt_id={} group_id={} child_type={} started_at_ms={} ended_at_ms={} exit_status={} intentional_stop={} last_error={} log_segments={}\n",
|
||
attempt.flow,
|
||
attempt.attempt_id,
|
||
attempt.repair_group_id.as_deref().unwrap_or("none"),
|
||
attempt.child_type.as_deref().unwrap_or("none"),
|
||
attempt.started_at_ms,
|
||
opt_u64(attempt.ended_at_ms),
|
||
attempt.exit_status.as_deref().unwrap_or("unavailable"),
|
||
attempt.intentional_stop,
|
||
attempt.last_error.as_deref().unwrap_or("unavailable"),
|
||
attempt
|
||
.log_segments
|
||
.iter()
|
||
.map(|p| p.display().to_string())
|
||
.collect::<Vec<_>>()
|
||
.join(",")
|
||
));
|
||
if !attempt.elevation_packages.is_empty() {
|
||
out.push_str(&format!(
|
||
"{label}.elevation_packages={}\n",
|
||
attempt.elevation_packages.join(", ")
|
||
));
|
||
}
|
||
append_capped(out, &format!("{label}.steps"), &attempt.steps);
|
||
append_capped(
|
||
out,
|
||
&format!("{label}.progress_details"),
|
||
&attempt.progress_details,
|
||
);
|
||
append_capped(out, &format!("{label}.diag_markers"), &attempt.diag_markers);
|
||
}
|
||
None => out.push_str(&format!("{label}: unavailable\n")),
|
||
}
|
||
}
|
||
|
||
fn append_backend_section(
|
||
out: &mut String,
|
||
snapshot: &DiagnosticsSnapshot,
|
||
health: Option<&BackendHealthSection>,
|
||
) {
|
||
out.push_str("\n== Backend/auth/watchdog ==\n");
|
||
match &snapshot.backend {
|
||
Some(backend) => {
|
||
out.push_str("source=live-state\n");
|
||
out.push_str(&format!("backend_kind={}\n", backend.backend_kind));
|
||
append_opt(out, "session_id", backend.session_id.as_deref());
|
||
append_opt(
|
||
out,
|
||
"requested_port",
|
||
backend.requested_port.map(|p| p.to_string()).as_deref(),
|
||
);
|
||
append_opt(
|
||
out,
|
||
"reported_port",
|
||
backend.reported_port.map(|p| p.to_string()).as_deref(),
|
||
);
|
||
append_opt(
|
||
out,
|
||
"generation",
|
||
backend.generation.map(|g| g.to_string()).as_deref(),
|
||
);
|
||
append_opt(
|
||
out,
|
||
"started_at_ms",
|
||
backend.started_at_ms.map(|t| t.to_string()).as_deref(),
|
||
);
|
||
append_opt(
|
||
out,
|
||
"ended_at_ms",
|
||
backend.ended_at_ms.map(|t| t.to_string()).as_deref(),
|
||
);
|
||
append_opt(out, "exit_status", backend.exit_status.as_deref());
|
||
out.push_str(&format!("intentional_stop={}\n", backend.intentional_stop));
|
||
append_opt(out, "terminal_reason", backend.terminal_reason.as_deref());
|
||
append_opt(out, "last_error", backend.last_error.as_deref());
|
||
if backend.backend_kind == "attached_external" {
|
||
out.push_str("managed_process_logs=not_owned_by_tauri\n");
|
||
}
|
||
}
|
||
None => out.push_str("backend=unavailable backend_kind=unknown\n"),
|
||
}
|
||
|
||
match &snapshot.auth {
|
||
Some(auth) => {
|
||
out.push_str(&format!(
|
||
"auth_failure: failed_at_ms={} stage={} port={} message={}\n",
|
||
auth.failed_at_ms,
|
||
auth.stage,
|
||
auth.port
|
||
.map(|p| p.to_string())
|
||
.unwrap_or_else(|| "unavailable".to_string()),
|
||
auth.message
|
||
));
|
||
}
|
||
None => out.push_str("auth_failure=unavailable\n"),
|
||
}
|
||
|
||
out.push_str("runtime_health:\n");
|
||
match health {
|
||
Some(health) => {
|
||
out.push_str(&format!(" port={}\n", health.port));
|
||
if health.fields.is_empty() {
|
||
out.push_str(" selected_fields=unavailable\n");
|
||
} else {
|
||
for (key, value) in &health.fields {
|
||
out.push_str(&format!(" {key}={value}\n"));
|
||
}
|
||
}
|
||
if let Some(warning) = &health.warning {
|
||
out.push_str(&format!(" warning={warning}\n"));
|
||
}
|
||
}
|
||
None => out.push_str(" unavailable: no known backend port\n"),
|
||
}
|
||
}
|
||
|
||
fn append_device_signals_section(
|
||
out: &mut String,
|
||
snapshot: &DiagnosticsSnapshot,
|
||
health: Option<&BackendHealthSection>,
|
||
) {
|
||
out.push_str("\n== Lightweight device signals ==\n");
|
||
out.push_str("scope=setup/runtime signals, not full hardware inventory\n");
|
||
out.push_str(&format!(
|
||
"os={} arch={}\n",
|
||
std::env::consts::OS,
|
||
std::env::consts::ARCH
|
||
));
|
||
out.push_str("installer_diag_markers:\n");
|
||
let mut any = false;
|
||
if let Some(install) = &snapshot.install {
|
||
append_marker_tail(out, "install", install);
|
||
any |= !install.diag_markers.items.is_empty();
|
||
}
|
||
if let Some(update) = &snapshot.update {
|
||
append_marker_tail(out, "update", update);
|
||
any |= !update.diag_markers.items.is_empty();
|
||
}
|
||
for group in &snapshot.repair_groups {
|
||
for child in &group.children {
|
||
append_marker_tail(out, "repair_child", child);
|
||
any |= !child.diag_markers.items.is_empty();
|
||
}
|
||
}
|
||
if !any {
|
||
out.push_str(" unavailable\n");
|
||
}
|
||
if let Some(health) = health {
|
||
out.push_str("runtime_health_selected_fields:\n");
|
||
for (key, value) in &health.fields {
|
||
if matches!(
|
||
key.as_str(),
|
||
"version"
|
||
| "device_type"
|
||
| "chat_only"
|
||
| "desktop_protocol_version"
|
||
| "desktop_manageability_version"
|
||
| "supports_api_only"
|
||
| "supports_desktop_auth"
|
||
| "supports_desktop_backend_ownership"
|
||
) {
|
||
out.push_str(&format!(" {key}={value}\n"));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fn append_log_sections(
|
||
out: &mut String,
|
||
snapshot: &DiagnosticsSnapshot,
|
||
warnings: &mut Vec<String>,
|
||
) {
|
||
out.push_str("\n== Log tails ==\n");
|
||
for name in ["tauri.log", "tauri.log.1"] {
|
||
let path = studio_dir().join(name);
|
||
append_tail_section(out, &path, name, "local-app-log", warnings);
|
||
}
|
||
|
||
let phase_groups = collect_phase_groups(snapshot, warnings);
|
||
if phase_groups.is_empty() {
|
||
out.push_str("phase_logs=unavailable\n");
|
||
}
|
||
for group in phase_groups {
|
||
out.push_str(&format!(
|
||
"\n-- phase_log_group label={} source={} --\n",
|
||
group.label, group.source
|
||
));
|
||
if let Some(note) = &group.note {
|
||
out.push_str(&format!("note={}\n", note));
|
||
}
|
||
for path in group.paths {
|
||
append_tail_section(out, &path, &group.label, &group.source, warnings);
|
||
}
|
||
}
|
||
}
|
||
|
||
fn append_tail_section(
|
||
out: &mut String,
|
||
path: &Path,
|
||
label: &str,
|
||
source: &str,
|
||
warnings: &mut Vec<String>,
|
||
) {
|
||
match read_tail(path, TAIL_MAX_LINES, TAIL_MAX_BYTES) {
|
||
Ok(tail) => {
|
||
out.push_str(&format!(
|
||
"file={} label={} source={} size_bytes={} mtime_ms={} included_bytes={} included_lines={} truncated={}\n",
|
||
path.display(),
|
||
label,
|
||
source,
|
||
tail.size_bytes,
|
||
opt_u64(tail.mtime_ms),
|
||
tail.included_bytes,
|
||
tail.included_lines,
|
||
tail.truncated
|
||
));
|
||
out.push_str("```text\n");
|
||
out.push_str(&tail.text);
|
||
if !tail.text.ends_with('\n') {
|
||
out.push('\n');
|
||
}
|
||
out.push_str("```\n");
|
||
}
|
||
Err(error) => {
|
||
out.push_str(&format!(
|
||
"file={} label={} source={} unavailable={}\n",
|
||
path.display(),
|
||
label,
|
||
source,
|
||
error
|
||
));
|
||
warnings.push(format!("{} unavailable: {}", path.display(), error));
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Debug)]
|
||
struct TailRead {
|
||
text: String,
|
||
size_bytes: u64,
|
||
mtime_ms: Option<u64>,
|
||
included_bytes: usize,
|
||
included_lines: usize,
|
||
truncated: bool,
|
||
}
|
||
|
||
fn read_tail(path: &Path, max_lines: usize, max_bytes: usize) -> Result<TailRead, String> {
|
||
if path_has_symlink(path) {
|
||
return Err("refusing to read symlink".to_string());
|
||
}
|
||
let metadata = fs::metadata(path).map_err(|e| e.to_string())?;
|
||
let size = metadata.len();
|
||
let mut file = File::open(path).map_err(|e| e.to_string())?;
|
||
let start = size.saturating_sub(max_bytes as u64);
|
||
file.seek(SeekFrom::Start(start))
|
||
.map_err(|e| e.to_string())?;
|
||
let mut bytes = Vec::new();
|
||
file.take(max_bytes as u64)
|
||
.read_to_end(&mut bytes)
|
||
.map_err(|e| e.to_string())?;
|
||
let mut text = String::from_utf8_lossy(&bytes).into_owned();
|
||
let mut truncated = start > 0;
|
||
if truncated {
|
||
text = format!("[tail truncated to last {max_bytes} bytes]\n{text}");
|
||
}
|
||
let mut lines: Vec<String> = text
|
||
.lines()
|
||
.map(|line| {
|
||
if line.len() > MAX_PHASE_LINE_BYTES {
|
||
let boundary = valid_utf8_boundary(line, MAX_PHASE_LINE_BYTES);
|
||
format!("{} [line truncated]", &line[..boundary])
|
||
} else {
|
||
line.to_string()
|
||
}
|
||
})
|
||
.collect();
|
||
if lines.len() > max_lines {
|
||
truncated = true;
|
||
let omitted = lines.len() - max_lines;
|
||
lines = lines.split_off(omitted);
|
||
lines.insert(0, format!("[tail truncated to last {max_lines} lines]"));
|
||
}
|
||
let included_lines = lines.len();
|
||
let mut text = lines.join("\n");
|
||
if !text.is_empty() {
|
||
text.push('\n');
|
||
}
|
||
Ok(TailRead {
|
||
text,
|
||
size_bytes: size,
|
||
mtime_ms: metadata
|
||
.modified()
|
||
.ok()
|
||
.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
|
||
.map(|d| d.as_millis() as u64),
|
||
included_bytes: bytes.len(),
|
||
included_lines,
|
||
truncated,
|
||
})
|
||
}
|
||
|
||
fn render_footer(warnings: &[String], redaction: &RedactionReport) -> String {
|
||
let mut footer = String::new();
|
||
footer.push_str("\n== Collection warnings ==\n");
|
||
if warnings.is_empty() {
|
||
footer.push_str("none\n");
|
||
} else {
|
||
for warning in warnings.iter().take(100) {
|
||
footer.push_str("- ");
|
||
footer.push_str(warning);
|
||
footer.push('\n');
|
||
}
|
||
if warnings.len() > 100 {
|
||
footer.push_str(&format!(
|
||
"- omitted {} additional warnings\n",
|
||
warnings.len() - 100
|
||
));
|
||
}
|
||
}
|
||
footer.push_str("\n== Redaction/omission summary ==\n");
|
||
footer.push_str(&format!(
|
||
"redaction_replacements={}\n",
|
||
redaction.replacements
|
||
));
|
||
footer.push_str("redaction_scope=ANSI, private keys, URL credentials, auth headers, cookies, token patterns, assignment-style secrets, studio/home paths, emails\n");
|
||
footer.push_str(
|
||
"report_bounds=log sections <=1000 lines/200KiB each; total clipboard text <=1MiB\n",
|
||
);
|
||
footer.push_str("known_v1_gap=elevated apt helper may buffer subprocess output before diagnostics caps it\n");
|
||
footer.push_str("known_v1_gap=normal install elevation resume is linked in same-run state/report history; disk fallback conservatively includes recent install attempts but has no explicit install_group_id in V1\n");
|
||
footer
|
||
}
|
||
|
||
fn enforce_report_limit(body: String, footer: String) -> String {
|
||
if body.len() + footer.len() <= REPORT_MAX_BYTES {
|
||
return format!("{body}{footer}");
|
||
}
|
||
let notice = "\n[report truncated to fit clipboard budget; footer preserved]\n";
|
||
let footer_budget = footer.len().min(FOOTER_BUDGET_BYTES).max(footer.len());
|
||
let budget = REPORT_MAX_BYTES
|
||
.saturating_sub(footer_budget)
|
||
.saturating_sub(notice.len());
|
||
let boundary = valid_utf8_boundary(&body, budget);
|
||
format!("{}{}{}", &body[..boundary], notice, footer)
|
||
}
|
||
fn append_opt(out: &mut String, key: &str, value: Option<&str>) {
|
||
out.push_str(key);
|
||
out.push('=');
|
||
out.push_str(value.unwrap_or("unavailable"));
|
||
out.push('\n');
|
||
}
|
||
|
||
fn append_capped(out: &mut String, label: &str, values: &CappedStrings) {
|
||
out.push_str(&format!("{label}: omitted={}\n", values.omitted));
|
||
if values.items.is_empty() {
|
||
out.push_str(" unavailable\n");
|
||
} else {
|
||
for value in values.items.iter().rev().take(20).rev() {
|
||
out.push_str(" ");
|
||
out.push_str(value);
|
||
out.push('\n');
|
||
}
|
||
}
|
||
}
|
||
|
||
fn append_marker_tail(out: &mut String, label: &str, attempt: &AttemptSummary) {
|
||
for marker in attempt.diag_markers.items.iter().rev().take(10).rev() {
|
||
out.push_str(&format!(" {label}: {marker}\n"));
|
||
}
|
||
}
|
||
|
||
fn tail_lines(lines: &[String], max_lines: usize) -> &[String] {
|
||
if lines.len() > max_lines {
|
||
&lines[lines.len() - max_lines..]
|
||
} else {
|
||
lines
|
||
}
|
||
}
|
||
|
||
fn opt_u64(value: Option<u64>) -> String {
|
||
value
|
||
.map(|v| v.to_string())
|
||
.unwrap_or_else(|| "unavailable".to_string())
|
||
}
|
||
|
||
pub(crate) fn selected_json_value(value: &serde_json::Value) -> String {
|
||
match value {
|
||
serde_json::Value::String(s) => cap_string(s, 256),
|
||
serde_json::Value::Bool(b) => b.to_string(),
|
||
serde_json::Value::Number(n) => n.to_string(),
|
||
serde_json::Value::Null => "null".to_string(),
|
||
_ => "<non-scalar>".to_string(),
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn report_redacts_footer_collection_warning_paths() {
|
||
let snapshot = DiagnosticsSnapshot::default();
|
||
let report = render_report(snapshot, FrontendSupportSnapshot::default(), None);
|
||
let studio = studio_dir().display().to_string();
|
||
assert!(!report.contains(&studio));
|
||
assert!(report.contains("<studio_home>"));
|
||
}
|
||
|
||
#[test]
|
||
fn tail_handles_missing_and_non_utf8() {
|
||
let missing =
|
||
std::env::temp_dir().join(format!("unsloth-missing-tail-{}", std::process::id()));
|
||
assert!(read_tail(&missing, 10, 100).is_err());
|
||
|
||
let path =
|
||
std::env::temp_dir().join(format!("unsloth-nonutf8-tail-{}", std::process::id()));
|
||
fs::write(&path, [0xff, b'a', b'\n', b'b']).unwrap();
|
||
let tail = read_tail(&path, 10, 100).unwrap();
|
||
assert!(tail.text.contains('<27>'));
|
||
let _ = fs::remove_file(path);
|
||
}
|
||
}
|