first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -1,19 +1,18 @@
|
||||
use std::env;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{IsTerminal, Write, copy};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{
|
||||
env,
|
||||
fs::{self, File},
|
||||
io::{IsTerminal, Write, copy},
|
||||
};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use anyhow::Result;
|
||||
use chrono::Local;
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use log::LevelFilter;
|
||||
use std::sync::OnceLock;
|
||||
use zip::{CompressionMethod, ZipWriter, write::SimpleFileOptions};
|
||||
use galaxy_core::channel::ChannelState;
|
||||
use zip::write::SimpleFileOptions;
|
||||
use zip::{CompressionMethod, ZipWriter};
|
||||
|
||||
use crate::{LogConfig, LogDestination};
|
||||
use galaxy_core::channel::ChannelState;
|
||||
|
||||
const MAX_FILES_IN_GUI_ROTATION: usize = 5;
|
||||
const MAX_FILES_IN_CLI_ROTATION: usize = 10;
|
||||
@@ -159,19 +158,37 @@ pub async fn rotate_files(channel_file_name: &str, max_rotation: usize) -> Resul
|
||||
}
|
||||
};
|
||||
|
||||
// Delete the oldest log file.
|
||||
// Delete the oldest log file (and any nested .in_session.M chunks that
|
||||
// belonged to that oldest startup-rotation slot).
|
||||
let largest_log_file_suffix = max_rotation.saturating_sub(1);
|
||||
let _ = fs::remove_file(
|
||||
log_directory.join(format!("{channel_file_name}.old.{largest_log_file_suffix}")),
|
||||
);
|
||||
remove_old_session_in_session_chunks(
|
||||
&log_directory,
|
||||
channel_file_name,
|
||||
largest_log_file_suffix,
|
||||
);
|
||||
|
||||
// Rotate the log files.
|
||||
// Rotate the .old.N startup-rotation slots, and along with each one any
|
||||
// nested `<name>.log.old.{N}.in_session.M` chunks left by the session
|
||||
// that produced the .old.N slot. Nested chunks shift with their parent
|
||||
// so they stay associated with the same logical session.
|
||||
for file_no in (0..largest_log_file_suffix).rev() {
|
||||
let old_file_path = log_directory.join(format!("{channel_file_name}.old.{file_no}"));
|
||||
let new_file_path = log_directory.join(format!("{channel_file_name}.old.{}", file_no + 1));
|
||||
let _ = fs::rename(old_file_path, new_file_path);
|
||||
|
||||
shift_old_session_in_session_chunks(&log_directory, channel_file_name, file_no);
|
||||
}
|
||||
|
||||
// Migrate the previous session's `<name>.log.in_session.M` files into
|
||||
// the `<name>.log.old.0.in_session.M` namespace, so the next session
|
||||
// opens with a clean `.in_session.*` window. The active log it produced
|
||||
// is renamed below from `.log.old.temp` to `.log.old.0`, so this naming
|
||||
// co-locates each old session's final state with its mid-session chunks.
|
||||
migrate_previous_session_in_session_chunks(&log_directory, channel_file_name);
|
||||
|
||||
// Rename `warp.log.old.temp` (the temporary file) to `warp.log.old.0`.
|
||||
let temp_file_path = temp_log_file_path(&log_directory);
|
||||
|
||||
@@ -183,12 +200,89 @@ pub async fn rotate_files(channel_file_name: &str, max_rotation: usize) -> Resul
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove every `<channel_file_name>.old.{slot_index}.in_session.M` file.
|
||||
/// Called when an entire `.old.{slot_index}` slot is being discarded so its
|
||||
/// nested mid-session chunks are discarded alongside it.
|
||||
fn remove_old_session_in_session_chunks(
|
||||
log_directory: &Path,
|
||||
channel_file_name: &str,
|
||||
slot_index: usize,
|
||||
) {
|
||||
let prefix = format!("{channel_file_name}.old.{slot_index}.in_session.");
|
||||
let Ok(read_dir) = fs::read_dir(log_directory) else {
|
||||
return;
|
||||
};
|
||||
for entry in read_dir.filter_map(Result::ok) {
|
||||
let path = entry.path();
|
||||
let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(rest) = name.strip_prefix(&prefix)
|
||||
&& rest.parse::<usize>().is_ok()
|
||||
{
|
||||
let _ = fs::remove_file(&path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Rename `<channel_file_name>.old.{from}.in_session.M` files to
|
||||
/// `<channel_file_name>.old.{from+1}.in_session.M`, shifting a previous
|
||||
/// session's nested chunks one slot older alongside their parent `.old.N`.
|
||||
fn shift_old_session_in_session_chunks(log_directory: &Path, channel_file_name: &str, from: usize) {
|
||||
let prefix = format!("{channel_file_name}.old.{from}.in_session.");
|
||||
let Ok(read_dir) = fs::read_dir(log_directory) else {
|
||||
return;
|
||||
};
|
||||
for entry in read_dir.filter_map(Result::ok) {
|
||||
let path = entry.path();
|
||||
let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(rest) = name.strip_prefix(&prefix)
|
||||
&& let Ok(chunk_index) = rest.parse::<usize>()
|
||||
{
|
||||
let new_path = log_directory.join(format!(
|
||||
"{channel_file_name}.old.{}.in_session.{chunk_index}",
|
||||
from + 1
|
||||
));
|
||||
let _ = fs::rename(path, new_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Rename the previous session's `<channel_file_name>.in_session.M` files
|
||||
/// into `<channel_file_name>.old.0.in_session.M`. Co-locates each old
|
||||
/// session's mid-session chunks with the `.old.0` slot that holds its
|
||||
/// final-state log, and frees the `.in_session.*` namespace for the new
|
||||
/// session that just started.
|
||||
fn migrate_previous_session_in_session_chunks(log_directory: &Path, channel_file_name: &str) {
|
||||
let prefix = format!("{channel_file_name}.in_session.");
|
||||
let Ok(read_dir) = fs::read_dir(log_directory) else {
|
||||
return;
|
||||
};
|
||||
for entry in read_dir.filter_map(Result::ok) {
|
||||
let path = entry.path();
|
||||
let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(rest) = name.strip_prefix(&prefix)
|
||||
&& let Ok(chunk_index) = rest.parse::<usize>()
|
||||
{
|
||||
let new_path = log_directory.join(format!(
|
||||
"{channel_file_name}.old.0.in_session.{chunk_index}"
|
||||
));
|
||||
let _ = fs::rename(path, new_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes the logger for the crash recovery process.
|
||||
pub fn init_for_crash_recovery_process() -> Result<()> {
|
||||
init_internal(
|
||||
true, /* is_from_crash_recovery_process */
|
||||
false, /* is_cli */
|
||||
None, /* log_destination */
|
||||
None, /* max_file_size_bytes — crash recovery uses its own short-lived log */
|
||||
)
|
||||
}
|
||||
|
||||
@@ -201,6 +295,7 @@ pub fn init(config: LogConfig) -> Result<()> {
|
||||
false, /* is_from_crash_recovery_process */
|
||||
config.is_cli,
|
||||
config.log_destination,
|
||||
config.max_file_size_bytes,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -228,42 +323,106 @@ pub fn log_file_path() -> Result<PathBuf> {
|
||||
Ok(main_process_log_file_path(&dir))
|
||||
}
|
||||
|
||||
/// Collects a list of the paths to both the current warp instance's log file,
|
||||
/// and any older log files (we keep up to 6 log files around at any time,
|
||||
/// all of which are potentially useful for debugging).
|
||||
/// Collects paths to the current warp instance's log file and any older
|
||||
/// log files (up to 6 retained, all potentially useful for debugging).
|
||||
///
|
||||
/// Returned ordering is newest-first, grouped by session:
|
||||
///
|
||||
/// - The active `<name>.log` (current session's most recent writes).
|
||||
/// - `<name>.log.in_session.N` files produced by mid-session size rotation
|
||||
/// of the current session, sorted by index (`.in_session.0` is the most
|
||||
/// recent rotation).
|
||||
/// - For each previous-startup slot `K = 0..max_rotation`, in order:
|
||||
/// `<name>.log.old.K` (that session's final-state log) immediately
|
||||
/// followed by its `<name>.log.old.K.in_session.N` chunks, sorted by N.
|
||||
fn current_and_rotated_log_paths() -> Result<Vec<PathBuf>> {
|
||||
let log_directory = log_directory()?;
|
||||
let current_log_path = main_process_log_file_path(&log_directory);
|
||||
let logfile_name = ChannelState::logfile_name();
|
||||
collect_log_paths_in(&log_directory, &logfile_name)
|
||||
}
|
||||
|
||||
let mut rotated_logs: Vec<(usize, PathBuf)> = fs::read_dir(&log_directory)?
|
||||
.filter_map(Result::ok)
|
||||
.map(|entry| entry.path())
|
||||
.filter_map(|path| {
|
||||
let file_name = path.file_name()?.to_str()?;
|
||||
let suffix =
|
||||
file_name.strip_prefix(&format!("{}.old.", ChannelState::logfile_name()))?;
|
||||
let index = suffix.parse::<usize>().ok()?;
|
||||
Some((index, path))
|
||||
})
|
||||
.collect();
|
||||
rotated_logs.sort_by_key(|(index, _)| *index);
|
||||
/// Directory-scanning core of [`current_and_rotated_log_paths`], parameterized
|
||||
/// for testability. See the parent docs for ordering semantics.
|
||||
fn collect_log_paths_in(log_directory: &Path, logfile_name: &str) -> Result<Vec<PathBuf>> {
|
||||
let current_log_path = log_directory.join(logfile_name);
|
||||
let in_session_prefix = format!("{logfile_name}.in_session.");
|
||||
let old_prefix = format!("{logfile_name}.old.");
|
||||
|
||||
// Current session's mid-session rotation slots: <name>.log.in_session.N.
|
||||
let mut current_in_session: Vec<(usize, PathBuf)> = Vec::new();
|
||||
// Previous-startup final logs: <name>.log.old.K.
|
||||
let mut old_logs: Vec<(usize, PathBuf)> = Vec::new();
|
||||
// Previous sessions' nested mid-session chunks: <name>.log.old.K.in_session.M.
|
||||
// Keyed by (K, M) so each K's chunks group together with their .old.K parent.
|
||||
let mut old_nested: Vec<(usize, usize, PathBuf)> = Vec::new();
|
||||
|
||||
for entry in fs::read_dir(log_directory)?.filter_map(Result::ok) {
|
||||
let path = entry.path();
|
||||
let Some(file_name) = path.file_name().and_then(|n| n.to_str()) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(suffix) = file_name.strip_prefix(&in_session_prefix) {
|
||||
if let Ok(index) = suffix.parse::<usize>() {
|
||||
current_in_session.push((index, path));
|
||||
}
|
||||
} else if let Some(suffix) = file_name.strip_prefix(&old_prefix) {
|
||||
// suffix can be either `K` (an old log) or `K.in_session.M`
|
||||
// (a previous session's nested mid-rotation chunk).
|
||||
if let Ok(index) = suffix.parse::<usize>() {
|
||||
old_logs.push((index, path));
|
||||
} else if let Some((slot_str, chunk_str)) = suffix.split_once(".in_session.")
|
||||
&& let (Ok(slot), Ok(chunk)) =
|
||||
(slot_str.parse::<usize>(), chunk_str.parse::<usize>())
|
||||
{
|
||||
old_nested.push((slot, chunk, path));
|
||||
}
|
||||
}
|
||||
}
|
||||
current_in_session.sort_by_key(|(index, _)| *index);
|
||||
old_logs.sort_by_key(|(index, _)| *index);
|
||||
old_nested.sort_by_key(|(slot, chunk, _)| (*slot, *chunk));
|
||||
|
||||
let mut files = Vec::new();
|
||||
if current_log_path.is_file() {
|
||||
files.push(current_log_path);
|
||||
}
|
||||
|
||||
files.extend(
|
||||
rotated_logs
|
||||
current_in_session
|
||||
.into_iter()
|
||||
.map(|(_, path)| path)
|
||||
.filter(|path| path.is_file()),
|
||||
);
|
||||
|
||||
// Interleave each .old.K with its nested .old.K.in_session.M chunks so
|
||||
// a session's final state is immediately followed by that session's
|
||||
// mid-session chunks before the next-older session begins.
|
||||
let mut nested_iter = old_nested.into_iter().peekable();
|
||||
for (slot, old_path) in old_logs {
|
||||
if old_path.is_file() {
|
||||
files.push(old_path);
|
||||
}
|
||||
while let Some((nslot, _, _)) = nested_iter.peek() {
|
||||
if *nslot != slot {
|
||||
break;
|
||||
}
|
||||
let (_, _, npath) = nested_iter.next().expect("peek matched");
|
||||
if npath.is_file() {
|
||||
files.push(npath);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Any nested chunks whose parent .old.K is missing on disk still get
|
||||
// included after their slot has been skipped above — they show up here
|
||||
// grouped by (slot, chunk) ordering since they were never paired.
|
||||
for (_, _, npath) in nested_iter {
|
||||
if npath.is_file() {
|
||||
files.push(npath);
|
||||
}
|
||||
}
|
||||
|
||||
if files.is_empty() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"No warp logs were found for {}",
|
||||
ChannelState::logfile_name()
|
||||
"No warp logs were found for {logfile_name}"
|
||||
));
|
||||
}
|
||||
|
||||
@@ -332,7 +491,7 @@ fn sentry_log_filter(md: &log::Metadata) -> sentry_log::LogFilter {
|
||||
}
|
||||
|
||||
// Filter out the "redraw_frame" logging from breadcrumbs.
|
||||
"galaxyui::core::redraw_frame" => sentry_log::LogFilter::Ignore,
|
||||
"galaxyui_core::core::redraw_frame" => sentry_log::LogFilter::Ignore,
|
||||
|
||||
// Filter out logs from the crash-reporting implementation, in case it logs
|
||||
// anything in the process of forwarding logs to Sentry.
|
||||
@@ -346,6 +505,7 @@ fn init_internal(
|
||||
is_from_crash_recovery_process: bool,
|
||||
is_cli: bool,
|
||||
log_destination: Option<LogDestination>,
|
||||
max_file_size_bytes: Option<u64>,
|
||||
) -> Result<()> {
|
||||
/// Returns an empty file named `warp.log` to log the current execution, and
|
||||
/// renames the previous execution's log to a temporary name.
|
||||
@@ -421,9 +581,24 @@ fn init_internal(
|
||||
log_directory = log_directory.join(CLI_LOG_SUBDIRECTORY);
|
||||
}
|
||||
if use_logfile {
|
||||
base_logger.target(env_logger::Target::Pipe(Box::new(
|
||||
setup_log_files_for_current_execution(&log_directory, is_from_crash_recovery_process)?,
|
||||
)));
|
||||
let file =
|
||||
setup_log_files_for_current_execution(&log_directory, is_from_crash_recovery_process)?;
|
||||
// Crash-recovery logs are short-lived (the file is renamed into place
|
||||
// by the parent on crash, and otherwise deleted on clean exit), so
|
||||
// skip in-session rotation for them — `max_file_size_bytes` only
|
||||
// applies to the main process's `warp.log`.
|
||||
let target: Box<dyn std::io::Write + Send + 'static> = if is_from_crash_recovery_process {
|
||||
Box::new(file)
|
||||
} else {
|
||||
crate::rotation::wrap_for_rotation(
|
||||
file,
|
||||
&log_directory,
|
||||
&ChannelState::logfile_name(),
|
||||
max_file_size_bytes,
|
||||
max_rotation,
|
||||
)?
|
||||
};
|
||||
base_logger.target(env_logger::Target::Pipe(target));
|
||||
base_logger.format(format_for_file_output);
|
||||
} else {
|
||||
// Agent mode eval outputs are written to stdout but redirected to a file, so we don't want terminal styling.
|
||||
@@ -480,7 +655,7 @@ fn init_log_directory() -> Result<std::path::PathBuf> {
|
||||
anyhow::anyhow!("could not locate home directory in order to create a log file")
|
||||
})?
|
||||
.join("Library/Logs/"))
|
||||
} else if #[cfg(target_os = "linux")] {
|
||||
} else if #[cfg(any(target_os = "linux", target_os = "freebsd"))] {
|
||||
Ok(galaxy_core::paths::state_dir())
|
||||
} else if #[cfg(windows)] {
|
||||
Ok(galaxy_core::paths::state_dir().join(galaxy_core::paths::WARP_LOGS_DIR))
|
||||
@@ -504,3 +679,7 @@ pub fn init_logging_for_unit_tests() {
|
||||
.format(format_for_terminal_output)
|
||||
.init();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "native_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
Reference in New Issue
Block a user