using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using System.Windows.Forms; using BizHawk.Emulation.Common; using BizHawk.Client.Common; using BizHawk.Client.EmuHawk.WinFormExtensions; namespace BizHawk.Client.EmuHawk { public partial class TraceLogger : Form, IToolFormAutoConfig { [RequiredService] private ITraceable Tracer { get; set; } [ConfigPersist] private int MaxLines { get; set; } [ConfigPersist] private int FileSizeCap { get; set; } [ConfigPersist] private int DisasmColumnWidth { get { return this.Disasm.Width; } set { this.Disasm.Width = value; } } [ConfigPersist] private int RegistersColumnWidth { get { return this.Registers.Width; } set { this.Registers.Width = value; } } private FileInfo _logFile; private FileInfo LogFile { get { return _logFile; } set { _logFile = value; _baseName = Path.ChangeExtension(value.FullName, null); } } private List _instructions = new List(); private StreamWriter _streamWriter; private bool _splitFile; private string _baseName; private string _extension = ".log"; private int _segmentCount; private ulong _currentSize; public TraceLogger() { InitializeComponent(); TraceView.QueryItemText += TraceView_QueryItemText; TraceView.VirtualMode = true; Closing += (o, e) => SaveConfigSettings(); MaxLines = 10000; FileSizeCap = 150; // make 1 frame of tracelog for n64/psx fit in _splitFile = FileSizeCap != 0; } public bool UpdateBefore { get { return false; } } public bool AskSaveChanges() { return true; } private void SaveConfigSettings() { //Tracer.Enabled = LoggingEnabled.Checked; } private void TraceView_QueryItemText(int index, int column, out string text) { text = ""; if (index < _instructions.Count) { switch (column) { case 0: text = _instructions[index].Disassembly.TrimEnd(); break; case 1: text = _instructions[index].RegisterInfo; break; } } } private void TraceLogger_Load(object sender, EventArgs e) { ClearList(); OpenLogFile.Enabled = false; LoggingEnabled.Checked = false; Tracer.Sink = null; SetTracerBoxTitle(); } class CallbackSink : ITraceSink { public void Put(TraceInfo info) { putter(info); } public Action putter; } public void UpdateValues() { } public void NewUpdate(ToolFormUpdateType type) { if (type == ToolFormUpdateType.PostFrame) { if (ToWindowRadio.Checked) { TraceView.VirtualListSize = _instructions.Count; } else { CloseFile(); } } if (type == ToolFormUpdateType.PreFrame) { if (LoggingEnabled.Checked) { //connect tracer to sink for next frame if (ToWindowRadio.Checked) { //update listview with most recentr results TraceView.BlazingFast = !GlobalWin.MainForm.EmulatorPaused; Tracer.Sink = new CallbackSink() { putter = (info) => { if (_instructions.Count >= MaxLines) { _instructions.RemoveRange(0, _instructions.Count - MaxLines); } _instructions.Add(info); } }; _instructions.Clear(); } else { if (_streamWriter == null) { StartLogFile(true); } Tracer.Sink = new CallbackSink { putter = (info) => { //no padding supported. core should be doing this! var data = string.Format("{0} {1}", info.Disassembly, info.RegisterInfo); _streamWriter.WriteLine(data); _currentSize += (ulong)data.Length; if (_splitFile) CheckSplitFile(); } }; } } else Tracer.Sink = null; } } public void FastUpdate() { } public void Restart() { CloseFile(); ClearList(); LoggingEnabled.Checked = false; ToFileRadio.Checked = false; ToWindowRadio.Checked = true; OpenLogFile.Enabled = false; Tracer.Sink = null; SetTracerBoxTitle(); } private void ClearList() { _instructions.Clear(); TraceView.ItemCount = 0; SetTracerBoxTitle(); } private void DumpToDisk() { foreach (var instruction in _instructions) { //no padding supported. core should be doing this! var data = string.Format("{0} {1}", instruction.Disassembly, instruction.RegisterInfo); _streamWriter.WriteLine(data); _currentSize += (ulong)data.Length; if (_splitFile) CheckSplitFile(); } } private void LogToWindow() { if (_instructions.Count >= MaxLines) { _instructions.RemoveRange(0, _instructions.Count - MaxLines); } TraceView.ItemCount = _instructions.Count; } private void SetTracerBoxTitle() { if (LoggingEnabled.Checked) { if (ToFileRadio.Checked) { TracerBox.Text = "Trace log - logging to file..."; } else if (_instructions.Any()) { TracerBox.Text = "Trace log - logging - " + _instructions.Count + " instructions"; } else { TracerBox.Text = "Trace log - logging..."; } } else { if (_instructions.Any()) { TracerBox.Text = "Trace log - " + _instructions.Count + " instructions"; } else { TracerBox.Text = "Trace log"; } } } private void CloseFile() { // TODO: save the remaining instructions in CoreComm if (_streamWriter != null) { _streamWriter.Close(); _streamWriter = null; } } private FileInfo GetFileFromUser() { var sfd = new SaveFileDialog(); if (LogFile == null) { sfd.FileName = PathManager.FilesystemSafeName(Global.Game) + _extension; sfd.InitialDirectory = PathManager.MakeAbsolutePath(Global.Config.PathEntries.LogPathFragment, null); } else if (!string.IsNullOrWhiteSpace(LogFile.FullName)) { sfd.FileName = PathManager.FilesystemSafeName(Global.Game); sfd.InitialDirectory = Path.GetDirectoryName(LogFile.FullName); } else { sfd.FileName = Path.GetFileNameWithoutExtension(LogFile.FullName); sfd.InitialDirectory = PathManager.MakeAbsolutePath(Global.Config.PathEntries.LogPathFragment, null); } sfd.Filter = "Log Files (*.log)|*.log|Text Files (*.txt)|*.txt|All Files|*.*"; sfd.RestoreDirectory = true; var result = sfd.ShowHawkDialog(); if (result == DialogResult.OK) { return new FileInfo(sfd.FileName); } else { return null; } } #region Events #region Menu Items private void SaveLogMenuItem_Click(object sender, EventArgs e) { LogFile = GetFileFromUser(); if (LogFile != null) { StartLogFile(); DumpToDisk(); GlobalWin.OSD.AddMessage("Log dumped to " + LogFile.FullName); CloseFile(); } } private void ExitMenuItem_Click(object sender, EventArgs e) { Close(); } private void CopyMenuItem_Click(object sender, EventArgs e) { var indices = TraceView.SelectedIndices; if (indices.Count > 0) { var blob = new StringBuilder(); foreach (int index in indices) { blob.Append(string.Format("{0} {1}\n", _instructions[index].Disassembly, _instructions[index].RegisterInfo)); } Clipboard.SetDataObject(blob.ToString()); } } private void SelectAllMenuItem_Click(object sender, EventArgs e) { for (var i = 0; i < _instructions.Count; i++) { TraceView.SelectItem(i, true); } } private void MaxLinesMenuItem_Click(object sender, EventArgs e) { var prompt = new InputPrompt { StartLocation = this.ChildPointToScreen(TraceView), TextInputType = InputPrompt.InputType.Unsigned, Message = "Max lines to display in the window", InitialValue = MaxLines.ToString() }; var result = prompt.ShowHawkDialog(); if (result == DialogResult.OK) { var max = int.Parse(prompt.PromptText); if (max > 0) { MaxLines = max; } } } private void SegmentSizeMenuItem_Click(object sender, EventArgs e) { var prompt = new InputPrompt { StartLocation = this.ChildPointToScreen(TraceView), TextInputType = InputPrompt.InputType.Unsigned, Message = "Log file segment size in megabytes\nSetting 0 disables segmentation", InitialValue = FileSizeCap.ToString() }; var result = prompt.ShowHawkDialog(); if (result == DialogResult.OK) { FileSizeCap = int.Parse(prompt.PromptText); _splitFile = FileSizeCap != 0; } } #endregion #region Dialog and ListView Events private void LoggingEnabled_CheckedChanged(object sender, EventArgs e) { //Tracer.Enabled = LoggingEnabled.Checked; SetTracerBoxTitle(); if (LogFile != null && ToFileRadio.Checked) { OpenLogFile.Enabled = true; if (LoggingEnabled.Checked) { StartLogFile(); } else { CloseFile(); } } } private void StartLogFile(bool append = false) { var data = Tracer.Header; var segment = _segmentCount > 0 ? "_" + _segmentCount.ToString() : ""; _streamWriter = new StreamWriter(_baseName + segment + _extension, append); _streamWriter.WriteLine(data); if (append) { _currentSize += (ulong)data.Length; } else { _currentSize = (ulong)data.Length; } } private void CheckSplitFile() { if (_currentSize / 1024 / 1024 >= (ulong)FileSizeCap) { _segmentCount++; CloseFile(); StartLogFile(); } } private void BrowseBox_Click(object sender, EventArgs e) { var file = GetFileFromUser(); if (file != null) { LogFile = file; FileBox.Text = LogFile.FullName; _segmentCount = 0; } } private void ToFileRadio_CheckedChanged(object sender, EventArgs e) { if (ToFileRadio.Checked) { FileBox.Visible = true; BrowseBox.Visible = true; var name = PathManager.FilesystemSafeName(Global.Game); var filename = Path.Combine(PathManager.MakeAbsolutePath(Global.Config.PathEntries.LogPathFragment, null), name) + _extension; LogFile = new FileInfo(filename); if (LogFile.Directory != null && !LogFile.Directory.Exists) { LogFile.Directory.Create(); } // never delete, especially from ticking checkboxes // append = false is enough, and even that only happens when actually enabling logging //if (LogFile.Exists) //{ // LogFile.Delete(); //} //using (LogFile.Create()) { } // created automatically FileBox.Text = LogFile.FullName; if (LoggingEnabled.Checked && LogFile != null) { StartLogFile(); OpenLogFile.Enabled = true; } } else { _currentSize = 0; _segmentCount = 0; CloseFile(); FileBox.Visible = false; BrowseBox.Visible = false; OpenLogFile.Enabled = false; } SetTracerBoxTitle(); } #endregion #endregion private void ClearMenuItem_Click(object sender, EventArgs e) { ClearList(); } private void OpenLogFile_Click(object sender, EventArgs e) { if (LogFile != null) { Process.Start(LogFile.FullName); } } } }