diff --git a/DICUI/App.config b/DICUI/App.config
index 4decd164..42ef3b7c 100644
--- a/DICUI/App.config
+++ b/DICUI/App.config
@@ -8,5 +8,7 @@
+
+
diff --git a/DICUI/DICUI.csproj b/DICUI/DICUI.csproj
index b0f55549..52a652c3 100644
--- a/DICUI/DICUI.csproj
+++ b/DICUI/DICUI.csproj
@@ -97,6 +97,9 @@
MSBuild:Compile
Designer
+
+ LogWindow.xaml
+
@@ -114,6 +117,10 @@
OptionsWindow.xaml
+
+ Designer
+ MSBuild:Compile
+
MSBuild:Compile
Designer
diff --git a/DICUI/LogWindow.xaml b/DICUI/LogWindow.xaml
new file mode 100644
index 00000000..c76c5f8a
--- /dev/null
+++ b/DICUI/LogWindow.xaml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/DICUI/LogWindow.xaml.cs b/DICUI/LogWindow.xaml.cs
new file mode 100644
index 00000000..12da66e1
--- /dev/null
+++ b/DICUI/LogWindow.xaml.cs
@@ -0,0 +1,386 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Interop;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Shapes;
+using DICUI.UI;
+
+namespace DICUI
+{
+ public partial class LogWindow : Window
+ {
+ private const int GWL_STYLE = -16;
+ private const int WS_SYSMENU = 0x80000;
+ [DllImport("user32.dll", SetLastError = true)]
+ private static extern int GetWindowLong(IntPtr hWnd, int nIndex);
+ [DllImport("user32.dll")]
+ private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
+
+ private MainWindow _mainWindow;
+
+ private FlowDocument _document;
+ private Paragraph _paragraph;
+ private List _matchers;
+
+ volatile Process _process;
+
+ public LogWindow(MainWindow mainWindow)
+ {
+ InitializeComponent();
+
+ this._mainWindow = mainWindow;
+
+ _document = new FlowDocument();
+ _paragraph = new Paragraph();
+ _document.Blocks.Add(_paragraph);
+ output.Document = _document;
+
+ _matchers = new List();
+
+ _matchers.Add(new Matcher(
+ "Descrambling data sector of img (LBA)",
+ @"\s*(\d+)\/\s*(\d+)$",
+ match => {
+ if (UInt32.TryParse(match.Groups[1].Value, out uint current) && UInt32.TryParse(match.Groups[2].Value, out uint total))
+ {
+ float percentProgress = (current / (float)total) * 100;
+ progressBar.Value = percentProgress;
+ progressLabel.Text = string.Format("Descrambling image.. ({0:##.##}%)", percentProgress);
+ }
+ }));
+
+ _matchers.Add(new Matcher(
+ @"Creating .scm (LBA)",
+ @"\s*(\d+)\/\s*(\d+)$",
+ match => {
+ if (UInt32.TryParse(match.Groups[1].Value, out uint current) && UInt32.TryParse(match.Groups[2].Value, out uint total))
+ {
+ float percentProgress = (current / (float)total) * 100;
+ progressBar.Value = percentProgress;
+ progressLabel.Text = string.Format("Creating scrambled image.. ({0:##.##}%)", percentProgress);
+ }
+ }));
+
+ _matchers.Add(new Matcher(
+ "Checking sectors (LBA)",
+ @"\s*(\d+)\/\s*(\d+)$",
+ match => {
+ if (UInt32.TryParse(match.Groups[1].Value, out uint current) && UInt32.TryParse(match.Groups[2].Value, out uint total))
+ {
+ float percentProgress = (current / (float)total) * 100;
+ progressBar.Value = percentProgress;
+ progressLabel.Text = string.Format("Checking for errors.. ({0:##.##}%)", percentProgress);
+ }
+ }));
+
+ _matchers.Add(new Matcher(
+ "Scanning sector (LBA)",
+ @"\s*(\d+)\/\s*(\d+)$",
+ match => {
+ if (UInt32.TryParse(match.Groups[1].Value, out uint current) && UInt32.TryParse(match.Groups[2].Value, out uint total))
+ {
+ float percentProgress = (current / (float)total) * 100;
+ progressBar.Value = percentProgress;
+ progressLabel.Text = string.Format("Scanning sectors for protection.. ({0:##.##}%)", percentProgress);
+ }
+ }));
+ }
+
+ public void StartDump(string args)
+ {
+ AppendToTextBox(string.Format("Launching DIC with args: {0}\r\n", args), Brushes.Orange);
+
+ Task.Run(() =>
+ {
+ _process = new Process()
+ {
+ StartInfo = new ProcessStartInfo()
+ {
+ FileName = @"Programs/DiscImageCreator.exe",
+ Arguments = args,
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ },
+ };
+
+ StreamState stdoutState = new StreamState(false);
+ StreamState stderrState = new StreamState(true);
+
+ //_cmd.ErrorDataReceived += (process, text) => Dispatcher.Invoke(() => UpdateConsole(text.Data, Brushes.Red));
+ _process.Start();
+
+ var _1 = ConsumeOutput(_process.StandardOutput, s => Dispatcher.Invoke(() => UpdateConsole(s, stdoutState)));
+ var _2 = ConsumeOutput(_process.StandardError, s => Dispatcher.Invoke(() => UpdateConsole(s, stderrState)));
+
+ _process.EnableRaisingEvents = true;
+ _process.Exited += OnProcessExit;
+ });
+ }
+
+ public void AdjustPositionToMainWindow()
+ {
+ this.Left = _mainWindow.Left;
+ this.Top = _mainWindow.Top + _mainWindow.Height + 10;
+ }
+
+ private void GracefullyTerminateProcess()
+ {
+ if (_process != null)
+ {
+ _process.Exited -= OnProcessExit;
+ bool isForced = !_process.HasExited;
+
+ if (isForced)
+ {
+ AppendToTextBox("\r\nForcefully Killing the process\r\n", Brushes.Red);
+ _process.Kill();
+ _process.WaitForExit();
+ }
+
+ AppendToTextBox(string.Format("\r\nExit Code: {0}\r\n", _process.ExitCode), _process.ExitCode == 0 ? Brushes.Green : Brushes.Red);
+
+ if (_process.ExitCode == 0)
+ {
+ Dispatcher.Invoke(() =>
+ {
+ progressLabel.Text = "Done!";
+ progressBar.Value = 100;
+ progressBar.Foreground = Brushes.Green;
+ });
+ }
+ else
+ {
+ Dispatcher.Invoke(() =>
+ {
+ progressLabel.Text = isForced ? "Aborted by user" : "Error, please check log!";
+ progressBar.Value = 100;
+ progressBar.Foreground = Brushes.Red;
+ });
+ }
+
+ _process.Close();
+ }
+
+ _process = null;
+ }
+
+ private void ScrollViewer_SizeChanged(object sender, SizeChangedEventArgs e)
+ {
+ outputViewer.ScrollToBottom();
+ }
+
+ async Task ConsumeOutput(TextReader reader, Action callback)
+ {
+ char[] buffer = new char[256];
+ int cch;
+
+ while ((cch = await reader.ReadAsync(buffer, 0, buffer.Length)) > 0)
+ {
+ callback(new string(buffer, 0, cch));
+ }
+ }
+
+ // this is used to optimize the work since we need to process A LOT of text
+ struct Matcher
+ {
+ private readonly String prefix;
+ private readonly Regex regex;
+ private readonly int start;
+ private readonly Action lambda;
+
+ public Matcher(String prefix, String regex, Action lambda)
+ {
+ this.prefix = prefix;
+ this.regex = new Regex(regex);
+ this.start = prefix.Length;
+ this.lambda = lambda;
+ }
+
+ public bool Matches(ref string text) => text.StartsWith(prefix);
+
+ public void Apply(ref string text)
+ {
+ Match match = regex.Match(text, start);
+ lambda.Invoke(match);
+ }
+ }
+
+ private void ProcessStringForProgressBar(string text)
+ {
+ foreach (Matcher matcher in _matchers)
+ {
+ if (matcher.Matches(ref text))
+ {
+ matcher.Apply(ref text);
+ return;
+ }
+ }
+ }
+
+ class StreamState
+ {
+ public enum State
+ {
+ BEGIN,
+ READ_CARRIAGE,
+ };
+
+ public State state;
+ public readonly StringBuilder buffer;
+ public readonly bool isError;
+
+ public StreamState(bool isError)
+ {
+ this.state = State.BEGIN;
+ this.buffer = new StringBuilder();
+ this.isError = isError;
+ }
+
+ public bool HasData() => buffer.Length > 0;
+ public string Fetch() => buffer.ToString();
+ public void Clear() => buffer.Clear();
+ public void Append(char c) => buffer.Append(c);
+
+ public bool Is(State state) => this.state == state;
+ public void Set(State state) => this.state = state;
+ }
+
+ public void AppendToTextBox(string text, Brush color)
+ {
+ if (Application.Current.Dispatcher.CheckAccess())
+ {
+ Run run = new Run(text) { Foreground = color };
+ _paragraph.Inlines.Add(run);
+ }
+ else
+ Dispatcher.Invoke(() =>
+ {
+ Run run = new Run(text) { Foreground = color };
+ _paragraph.Inlines.Add(run);
+ });
+ }
+
+ private void UpdateConsole(string text, StreamState state)
+ {
+ /*if (c == '\r') { output.Inlines.Add(@"\r"); file.Write("\\r"); }
+ else if (c == '\n') { output.Inlines.Add(@"\n"); file.Write("\\n\n"); }
+ output.Inlines.Add(""+c);
+ file.Write(c);
+ file.Flush();
+ continue;*/
+
+ if (text != null)
+ {
+ foreach (char c in text)
+ {
+ switch (c)
+ {
+ case '\r' when (state.Is(StreamState.State.BEGIN)):
+ {
+ state.Set(StreamState.State.READ_CARRIAGE);
+ break;
+ }
+
+ case '\n' when (state.Is(StreamState.State.READ_CARRIAGE)):
+ {
+ if (state.buffer.Length > 0)
+ {
+ string buffer = state.Fetch();
+
+ AppendToTextBox(buffer, state.isError ? Brushes.Red : Brushes.White);
+
+ if (!state.isError)
+ ProcessStringForProgressBar(buffer);
+ }
+ _paragraph.Inlines.Add(new LineBreak());
+ state.Clear();
+ state.Set(StreamState.State.BEGIN);
+ break;
+ }
+
+ default:
+ if (state.Is(StreamState.State.READ_CARRIAGE) && state.HasData())
+ {
+ if (!(_paragraph.Inlines.LastInline is LineBreak))
+ _paragraph.Inlines.Remove(_paragraph.Inlines.LastInline);
+
+ string buffer = state.Fetch();
+
+ AppendToTextBox(buffer, state.isError ? Brushes.Red : Brushes.White);
+
+ if (!state.isError)
+ ProcessStringForProgressBar(buffer);
+
+ state.Clear();
+ }
+
+ state.Set(StreamState.State.BEGIN);
+ state.Append(c);
+ break;
+
+ }
+ }
+ }
+ }
+
+ #region EventHandlers
+
+ private void OnWindowClosed(object sender, EventArgs e)
+ {
+ GracefullyTerminateProcess();
+ }
+
+ private void OnWindowLoaded(object sender, EventArgs e)
+ {
+ var hwnd = new WindowInteropHelper(this).Handle;
+ SetWindowLong(hwnd, GWL_STYLE, GetWindowLong(hwnd, GWL_STYLE) & ~WS_SYSMENU);
+ }
+
+ void OnProcessExit(object sender, EventArgs e)
+ {
+ Dispatcher.Invoke(() => AbortButton.IsEnabled = false);
+ GracefullyTerminateProcess();
+ }
+
+ private void OnHideButton(object sender, EventArgs e)
+ {
+ ViewModels.LoggerViewModel.WindowVisible = false;
+ //TODO: this should be bound directly to WindowVisible property in two way fashion
+ // we need to study how to properly do it in XAML
+ _mainWindow.ShowLogMenuItem.IsChecked = false;
+ }
+
+ private void OnClearButton(object sender, EventArgs e)
+ {
+ output.Document.Blocks.Clear();
+ }
+
+ private void OnAbortButton(object sender, EventArgs args)
+ {
+ GracefullyTerminateProcess();
+ }
+
+ private void OnStartButton(object sender, EventArgs args)
+ {
+ StartDump("cd e Gam.iso 16");
+ }
+
+ #endregion
+
+ }
+}
diff --git a/DICUI/MainWindow.xaml b/DICUI/MainWindow.xaml
index 84fd0b82..4b2390f7 100644
--- a/DICUI/MainWindow.xaml
+++ b/DICUI/MainWindow.xaml
@@ -4,10 +4,11 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DICUI"
+ xmlns:lui="clr-namespace:DICUI.UI"
xmlns:utilities="clr-namespace:DICUI.Utilities"
mc:Ignorable="d"
- x:Name="mainWindow"
- Title="Disc Image Creator GUI" Height="450" Width="600" WindowStartupLocation="CenterScreen" ResizeMode="CanMinimize">
+ Title="Disc Image Creator GUI" Height="450" Width="600" WindowStartupLocation="CenterScreen" ResizeMode="CanMinimize"
+ LocationChanged="MainWindowLocationChanged" Activated="MainWindowLocationActivated" Closing="MainWindowClosing">
@@ -32,6 +33,10 @@
diff --git a/DICUI/MainWindow.xaml.cs b/DICUI/MainWindow.xaml.cs
index 942e9923..d0f1af91 100644
--- a/DICUI/MainWindow.xaml.cs
+++ b/DICUI/MainWindow.xaml.cs
@@ -26,12 +26,10 @@ namespace DICUI
private Options _options;
private OptionsWindow _optionsWindow;
- public bool QuietMode { get; set; }
+ private LogWindow _logWindow;
public MainWindow()
{
- QuietMode = true;
-
InitializeComponent();
// Initializes and load Options object
@@ -39,6 +37,9 @@ namespace DICUI
_options.Load();
ViewModels.OptionsViewModel = new OptionsViewModel(_options);
+ _logWindow = new LogWindow(this);
+ ViewModels.LoggerViewModel.SetWindow(_logWindow);
+
// Disable buttons until we load fully
StartStopButton.IsEnabled = false;
DiskScanButton.IsEnabled = false;
@@ -57,6 +58,14 @@ namespace DICUI
_alreadyShown = true;
+ if (_options.OpenLogWindowAtStartup)
+ {
+ //TODO: this should be bound directly to WindowVisible property in two way fashion
+ // we need to study how to properly do it in XAML
+ ShowLogMenuItem.IsChecked = true;
+ ViewModels.LoggerViewModel.WindowVisible = true;
+ }
+
// Populate the list of systems
StatusLabel.Content = "Creating system list, please wait!";
PopulateSystems();
@@ -149,6 +158,27 @@ namespace DICUI
EnsureDiscInformation();
}
+ private void MainWindowLocationChanged(object sender, EventArgs e)
+ {
+ if (_logWindow.IsVisible)
+ _logWindow.AdjustPositionToMainWindow();
+ }
+
+ private void MainWindowLocationActivated(object sender, EventArgs e)
+ {
+ if (_logWindow.IsVisible)
+ {
+ _logWindow.Topmost = true;
+ this.Topmost = true;
+ }
+ }
+
+ private void MainWindowClosing(object sender, System.ComponentModel.CancelEventArgs e)
+ {
+ if (_logWindow.IsVisible)
+ _logWindow.Close();
+ }
+
// Toolbar Events
private void AppExitClick(object sender, RoutedEventArgs e)
@@ -224,7 +254,9 @@ namespace DICUI
{
_systems = Validators.CreateListOfSystems();
- Dictionary> mapping = _systems
+ ViewModels.LoggerViewModel.VerboseLogLn("Populating systems, {0} systems found.", _systems.Count);
+
+ Dictionary > mapping = _systems
.GroupBy(s => s.Category())
.ToDictionary(
k => k.Key,
@@ -255,6 +287,8 @@ namespace DICUI
/// TODO: Find a way for this to periodically run, or have it hook to a "drive change" event
private void PopulateDrives()
{
+ ViewModels.LoggerViewModel.VerboseLogLn("Scanning for drives..");
+
// Always enable the disk scan
DiskScanButton.IsEnabled = true;
@@ -268,6 +302,8 @@ namespace DICUI
StatusLabel.Content = "Valid media found! Choose your Media Type";
StartStopButton.IsEnabled = true;
CopyProtectScanButton.IsEnabled = true;
+
+ ViewModels.LoggerViewModel.VerboseLogLn("Found {0} drives containing media: {1}", _drives.Count, String.Join(", ", _drives.Select(d => d.Letter)));
}
else
{
@@ -275,6 +311,8 @@ namespace DICUI
StatusLabel.Content = "No valid media found!";
StartStopButton.IsEnabled = false;
CopyProtectScanButton.IsEnabled = false;
+
+ ViewModels.LoggerViewModel.VerboseLogLn("Found no drives contaning valid media.");
}
}
@@ -334,6 +372,7 @@ namespace DICUI
StartStopButton.Content = UIElements.StopDumping;
CopyProtectScanButton.IsEnabled = false;
StatusLabel.Content = "Beginning dumping process";
+ ViewModels.LoggerViewModel.VerboseLogLn("Starting dumping process..");
Result result = await _env.StartDumping();
@@ -468,6 +507,8 @@ namespace DICUI
if (speed == -1)
return;
+ ViewModels.LoggerViewModel.VerboseLogLn("Determined max drive speed for {0}: {0}.", _env.Drive.Letter, speed);
+
// Choose the lower of the two speeds between the allowed speeds and the user-defined one
int chosenSpeed = Math.Min(
AllowedSpeeds.GetForMediaType(_currentMediaType).Where(s => s <= speed).Last(),
@@ -489,7 +530,11 @@ namespace DICUI
// Get the current optical disc type
if (!_options.SkipMediaTypeDetection)
+ {
+ ViewModels.LoggerViewModel.VerboseLog("Trying to detect media type for drive {0}.. ", drive.Letter);
_currentMediaType = Validators.GetDiscType(drive.Letter);
+ ViewModels.LoggerViewModel.VerboseLogLn(_currentMediaType != null ? "unable to detect." : ("detected " + _currentMediaType.Name() + "."));
+ }
}
///
diff --git a/DICUI/Options.cs b/DICUI/Options.cs
index eb5cecb2..8a7404e2 100644
--- a/DICUI/Options.cs
+++ b/DICUI/Options.cs
@@ -20,6 +20,9 @@ namespace DICUI
public bool SkipMediaTypeDetection { get; set; }
+ public bool VerboseLogging { get; set; }
+ public bool OpenLogWindowAtStartup { get; set; }
+
public void Save()
{
Configuration configFile = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
@@ -51,6 +54,8 @@ namespace DICUI
this.ParanoidMode = Boolean.TryParse(ConfigurationManager.AppSettings["ParanoidMode"], out bool paranoidMode) ? paranoidMode : false;
this.SkipMediaTypeDetection = Boolean.TryParse(ConfigurationManager.AppSettings["SkipMediaTypeDetection"], out bool skipMediaTypeDetection) ? skipMediaTypeDetection : false;
this.RereadAmountForC2 = Int32.TryParse(ConfigurationManager.AppSettings["RereadAmountForC2"], out int rereadAmountForC2) ? rereadAmountForC2 : 20;
+ this.VerboseLogging = Boolean.TryParse(ConfigurationManager.AppSettings["VerboseLogging"], out bool verboseLogging) ? verboseLogging : true;
+ this.OpenLogWindowAtStartup = Boolean.TryParse(ConfigurationManager.AppSettings["OpenLogWindowAtStartup"], out bool openLogWindowAtStartup) ? openLogWindowAtStartup : true;
}
diff --git a/DICUI/UI/ViewModels.cs b/DICUI/UI/ViewModels.cs
index 7693f750..9a6646d1 100644
--- a/DICUI/UI/ViewModels.cs
+++ b/DICUI/UI/ViewModels.cs
@@ -3,6 +3,8 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Media;
namespace DICUI.UI
{
@@ -42,11 +44,63 @@ namespace DICUI.UI
_options.RereadAmountForC2 = result;
}
}
+
+ public bool VerboseLogging
+ {
+ get { return _options.VerboseLogging; }
+ set
+ {
+ _options.VerboseLogging = value;
+ _options.Save();
+ }
+ }
+
+ public bool OpenLogWindowAtStartup
+ {
+ get { return _options.OpenLogWindowAtStartup; }
+ set
+ {
+ _options.OpenLogWindowAtStartup = value;
+ _options.Save();
+ }
+ }
+ }
+
+ public class LoggerViewModel
+ {
+ private LogWindow _logWindow;
+
+ public void SetWindow(LogWindow logWindow) => _logWindow = logWindow;
+
+ public bool WindowVisible
+ {
+ get => _logWindow != null ? _logWindow.IsVisible : false;
+ set
+ {
+ if (value)
+ {
+ _logWindow.AdjustPositionToMainWindow();
+ _logWindow.Show();
+ }
+ else
+ _logWindow.Hide();
+ }
+ }
+
+ public void VerboseLog(string text)
+ {
+ if (ViewModels.OptionsViewModel.VerboseLogging)
+ _logWindow.AppendToTextBox(text, Brushes.Yellow);
+ }
+
+ public void VerboseLog(string format, params object[] args) => VerboseLog(string.Format(format, args));
+ public void VerboseLogLn(string format, params object[] args) => VerboseLog(string.Format(format, args) + "\n");
}
public static class ViewModels
{
public static OptionsViewModel OptionsViewModel { get; set; }
+ public static LoggerViewModel LoggerViewModel { get; set; } = new LoggerViewModel();
}
}
diff --git a/DICUI/Utilities/Converters.cs b/DICUI/Utilities/Converters.cs
index c18a7935..e7d2fbc1 100644
--- a/DICUI/Utilities/Converters.cs
+++ b/DICUI/Utilities/Converters.cs
@@ -1,9 +1,13 @@
using System;
+using System.ComponentModel;
using System.Globalization;
using System.Windows.Data;
+using System.Windows.Data;
+using System.Reflection;
using IMAPI2;
using DICUI.Data;
+
namespace DICUI.Utilities
{
///
diff --git a/DICUI/Utilities/DumpEnvironment.cs b/DICUI/Utilities/DumpEnvironment.cs
index b6914124..9c871c23 100644
--- a/DICUI/Utilities/DumpEnvironment.cs
+++ b/DICUI/Utilities/DumpEnvironment.cs
@@ -8,6 +8,7 @@ using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows;
using DICUI.Data;
+using DICUI.UI;
namespace DICUI.Utilities
{
@@ -80,6 +81,8 @@ namespace DICUI.Utilities
///
public async void EjectDisc()
{
+ ViewModels.LoggerViewModel.VerboseLogLn("Ejecting Disc..");
+
// Validate that the required program exists
if (!File.Exists(DICPath))
return;
@@ -327,6 +330,8 @@ namespace DICUI.Utilities
///
private void ExecuteDiskImageCreator()
{
+ ViewModels.LoggerViewModel.VerboseLogLn("Launching DiskImageCreator process.");
+
dicProcess = new Process()
{
StartInfo = new ProcessStartInfo()