using System; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.IO; using System.Text; using Xharness.Logging; namespace Xharness { public class LogFile : Log, ILogFile { object lock_obj = new object (); public string Path { get; private set; } FileStream writer; bool disposed; public LogFile (ILogs logs, string description, string path, bool append = true) : base (logs, description) { Path = path; if (!append) File.WriteAllText (path, string.Empty); } public override void Write (byte [] buffer, int offset, int count) { try { // We don't want to open the file every time someone writes to the log, so we keep it as an instance // variable until we're disposed. Due to the async nature of how we run tests, writes may still // happen after we're disposed, in which case we create a temporary stream we close after writing lock (lock_obj) { var fs = writer; if (fs == null) { fs = new FileStream (Path, FileMode.Append, FileAccess.Write, FileShare.Read); } fs.Write (buffer, offset, count); if (disposed) { fs.Dispose (); } else { writer = fs; } } } catch (Exception e) { Console.WriteLine ($"Failed to write to the file {Path}: {e.Message}."); return; } } public override void Flush() { base.Flush(); if (writer != null && !disposed) writer.Flush (); } public override string FullPath { get { return Path; } } public override StreamReader GetReader () { return new StreamReader (new FileStream (Path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)); } protected override void Dispose (bool disposing) { base.Dispose (disposing); if (writer != null) { writer.Dispose (); writer = null; } disposed = true; } } // A log that writes to standard output public class ConsoleLog : Log { StringBuilder captured = new StringBuilder (); public ConsoleLog () : base (null) { } public override void WriteImpl (string value) { captured.Append (value); Console.Write (value); } public override string FullPath { get { throw new NotSupportedException (); } } public override StreamReader GetReader () { var str = new MemoryStream (System.Text.Encoding.UTF8.GetBytes (captured.ToString ())); return new StreamReader (str, System.Text.Encoding.UTF8, false); } } // A log that captures data written to a separate file between two moments in time // (between StartCapture and StopCapture). public class CaptureLog : Log { public string CapturePath { get; private set; } public string Path { get; set; } long startPosition; long endPosition; bool entire_file; public CaptureLog (ILogs logs, string capture_path, bool entire_file = false) : base (logs) { CapturePath = capture_path; this.entire_file = entire_file; } public void StartCapture () { if (entire_file) return; if (File.Exists (CapturePath)) startPosition = new FileInfo (CapturePath).Length; } public void StopCapture () { if (!File.Exists (CapturePath)) { File.WriteAllText (Path, $"Could not capture the file '{CapturePath}' because it doesn't exist."); return; } if (entire_file) { File.Copy (CapturePath, Path, true); return; } endPosition = new FileInfo (CapturePath).Length; Capture (); } void Capture () { if (startPosition == 0 || entire_file) return; if (!File.Exists (CapturePath)) { File.WriteAllText (Path, $"Could not capture the file '{CapturePath}' because it does not exist."); return; } var currentEndPosition = endPosition; if (currentEndPosition == 0) currentEndPosition = new FileInfo (CapturePath).Length; var length = (int) (currentEndPosition - startPosition); var currentLength = new FileInfo (CapturePath).Length; var capturedLength = 0L; if (length < 0) { // The file shrank? return; } if (File.Exists (Path)) capturedLength = new FileInfo (Path).Length; // capture 1k more data than when we stopped, since the system log // is cached in memory and flushed once in a while (so when the app // requests the system log to be captured, it's usually not complete). var availableLength = currentLength - startPosition; if (availableLength <= capturedLength) return; // We've captured before, and nothing new as added since last time. // Capture at most 1k more availableLength = Math.Min (availableLength, length + 1024); using (var reader = new FileStream (CapturePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) { using (var writer = new FileStream (Path, FileMode.Create, FileAccess.Write, FileShare.Read)) { var buffer = new byte [4096]; reader.Position = startPosition; while (availableLength > 0) { int read = reader.Read (buffer, 0, Math.Min (buffer.Length, length)); if (read > 0) { writer.Write (buffer, 0, read); availableLength -= read; } else { // There's nothing more to read. // I can't see how we get here, since we calculate the amount to read based on what's available, but it does happen randomly. break; } } } } } public override StreamReader GetReader () { if (File.Exists (CapturePath)) { return new StreamReader (new FileStream (CapturePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)); } else { return new StreamReader (new MemoryStream ()); } } public override void Flush () { base.Flush (); Capture (); } public override void WriteImpl (string value) { throw new InvalidOperationException (); } public override string FullPath { get { return Path; } } } // A log that forwards all written data to a callback public class CallbackLog : Log { public Action OnWrite; public CallbackLog (Action onWrite) : base (null) { OnWrite = onWrite; } public override string FullPath => throw new NotImplementedException (); public override void WriteImpl (string value) { OnWrite (value); } } }