353 lines
10 KiB
C#
353 lines
10 KiB
C#
/*
|
|
Copyright (C) 2014-2019 de4dot@gmail.com
|
|
|
|
This file is part of dnSpy
|
|
|
|
dnSpy is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
dnSpy is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with dnSpy. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.ComponentModel;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Windows.Input;
|
|
using System.Windows.Threading;
|
|
using dnSpy.AsmEditor.Properties;
|
|
using dnSpy.Contracts.Documents;
|
|
using dnSpy.Contracts.ETW;
|
|
using dnSpy.Contracts.Hex;
|
|
using dnSpy.Contracts.MVVM;
|
|
|
|
namespace dnSpy.AsmEditor.SaveModule {
|
|
sealed class SaveMultiModuleVM : INotifyPropertyChanged {
|
|
enum SaveState {
|
|
/// <summary>
|
|
/// We haven't started saving yet
|
|
/// </summary>
|
|
Loaded,
|
|
|
|
/// <summary>
|
|
/// We're saving
|
|
/// </summary>
|
|
Saving,
|
|
|
|
/// <summary>
|
|
/// We're canceling
|
|
/// </summary>
|
|
Canceling,
|
|
|
|
/// <summary>
|
|
/// Final state even if some files weren't saved
|
|
/// </summary>
|
|
Saved,
|
|
}
|
|
|
|
SaveState State {
|
|
get => saveState;
|
|
set {
|
|
if (value != saveState) {
|
|
saveState = value;
|
|
OnPropertyChanged(nameof(IsLoaded));
|
|
OnPropertyChanged(nameof(IsSaving));
|
|
OnPropertyChanged(nameof(IsCanceling));
|
|
OnPropertyChanged(nameof(IsSaved));
|
|
OnPropertyChanged(nameof(CanSave));
|
|
OnPropertyChanged(nameof(CanCancel));
|
|
OnPropertyChanged(nameof(CanClose));
|
|
OnPropertyChanged(nameof(IsSavingOrCanceling));
|
|
OnModuleSettingsSaved();
|
|
|
|
if (saveState == SaveState.Saved && OnSavedEvent is not null)
|
|
OnSavedEvent(this, EventArgs.Empty);
|
|
}
|
|
}
|
|
}
|
|
SaveState saveState = SaveState.Loaded;
|
|
|
|
public ICommand SaveCommand => new RelayCommand(a => Save(), a => CanExecuteSave);
|
|
public ICommand CancelSaveCommand => new RelayCommand(a => CancelSave(), a => IsSaving && moduleSaver is not null);
|
|
public event EventHandler? OnSavedEvent;
|
|
public bool IsLoaded => State == SaveState.Loaded;
|
|
public bool IsSaving => State == SaveState.Saving;
|
|
public bool IsCanceling => State == SaveState.Canceling;
|
|
public bool IsSaved => State == SaveState.Saved;
|
|
public bool CanSave => IsLoaded;
|
|
public bool CanCancel => IsLoaded || IsSaving;
|
|
public bool CanClose => !CanCancel;
|
|
public bool IsSavingOrCanceling => IsSaving || IsCanceling;
|
|
public bool CanExecuteSave => string.IsNullOrEmpty(CanExecuteSaveError);
|
|
public bool CanShowModuleErrors => IsLoaded && !CanExecuteSave;
|
|
|
|
public string? CanExecuteSaveError {
|
|
get {
|
|
if (!IsLoaded)
|
|
return "It's only possible to save when loaded";
|
|
|
|
for (int i = 0; i < Modules.Count; i++) {
|
|
var module = Modules[i];
|
|
if (module.HasError)
|
|
return string.Format(dnSpy_AsmEditor_Resources.SaveModules_FileHasErrors, i + 1, module.FileName.Trim() == string.Empty ? dnSpy_AsmEditor_Resources.EmptyFilename : module.FileName);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public void OnModuleSettingsSaved() {
|
|
OnPropertyChanged(nameof(CanExecuteSaveError));
|
|
OnPropertyChanged(nameof(CanExecuteSave));
|
|
OnPropertyChanged(nameof(CanShowModuleErrors));
|
|
}
|
|
|
|
public bool HasError {
|
|
get => hasError;
|
|
private set {
|
|
if (hasError != value) {
|
|
hasError = value;
|
|
OnPropertyChanged(nameof(HasError));
|
|
OnPropertyChanged(nameof(HasNoError));
|
|
}
|
|
}
|
|
}
|
|
bool hasError;
|
|
|
|
public bool HasNoError => !HasError;
|
|
|
|
public int ErrorCount {
|
|
get => errorCount;
|
|
set {
|
|
if (errorCount != value) {
|
|
errorCount = value;
|
|
OnPropertyChanged(nameof(ErrorCount));
|
|
HasError = errorCount != 0;
|
|
}
|
|
}
|
|
}
|
|
int errorCount;
|
|
|
|
public string LogMessage => logMessage.ToString();
|
|
StringBuilder logMessage = new StringBuilder();
|
|
|
|
public double ProgressMinimum => 0;
|
|
public double ProgressMaximum => 100;
|
|
|
|
public double TotalProgress {
|
|
get => totalProgress;
|
|
private set {
|
|
if (totalProgress != value) {
|
|
totalProgress = value;
|
|
OnPropertyChanged(nameof(TotalProgress));
|
|
}
|
|
}
|
|
}
|
|
double totalProgress = 0;
|
|
|
|
public double CurrentFileProgress {
|
|
get => currentFileProgress;
|
|
private set {
|
|
if (currentFileProgress != value) {
|
|
currentFileProgress = value;
|
|
OnPropertyChanged(nameof(CurrentFileProgress));
|
|
}
|
|
}
|
|
}
|
|
double currentFileProgress = 0;
|
|
|
|
public string CurrentFileName {
|
|
get => currentFileName;
|
|
set {
|
|
if (currentFileName != value) {
|
|
currentFileName = value;
|
|
OnPropertyChanged(nameof(CurrentFileName));
|
|
}
|
|
}
|
|
}
|
|
string currentFileName = string.Empty;
|
|
|
|
public ObservableCollection<SaveOptionsVM> Modules { get; } = new ObservableCollection<SaveOptionsVM>();
|
|
|
|
readonly IMmapDisabler mmapDisabler;
|
|
readonly Dispatcher dispatcher;
|
|
|
|
public SaveMultiModuleVM(IMmapDisabler mmapDisabler, Dispatcher dispatcher, SaveOptionsVM options) {
|
|
this.mmapDisabler = mmapDisabler;
|
|
this.dispatcher = dispatcher;
|
|
Modules.Add(options);
|
|
}
|
|
|
|
public SaveMultiModuleVM(IMmapDisabler mmapDisabler, Dispatcher dispatcher, IEnumerable<object> objs) {
|
|
this.mmapDisabler = mmapDisabler;
|
|
this.dispatcher = dispatcher;
|
|
Modules.AddRange(objs.Select(m => Create(m)));
|
|
}
|
|
|
|
static SaveOptionsVM Create(object obj) {
|
|
if (obj is IDsDocument document)
|
|
return new SaveModuleOptionsVM(document);
|
|
|
|
if (obj is HexBuffer buffer)
|
|
return new SaveHexOptionsVM(buffer);
|
|
|
|
throw new InvalidOperationException();
|
|
}
|
|
|
|
SaveOptionsVM? GetSaveOptionsVM(object obj) => Modules.FirstOrDefault(a => a.UndoDocument == obj);
|
|
|
|
public bool WasSaved(object obj) {
|
|
var data = GetSaveOptionsVM(obj);
|
|
if (data is null)
|
|
return false;
|
|
savedFile.TryGetValue(data, out bool saved);
|
|
return saved;
|
|
}
|
|
|
|
public string? GetSavedFileName(object obj) => GetSaveOptionsVM(obj)?.FileName;
|
|
|
|
public void Save() {
|
|
if (!CanExecuteSave)
|
|
return;
|
|
State = SaveState.Saving;
|
|
TotalProgress = 0;
|
|
CurrentFileProgress = 0;
|
|
CurrentFileName = string.Empty;
|
|
savedFile.Clear();
|
|
|
|
var mods = Modules.ToArray();
|
|
mmapDisabler.Disable(mods.Select(a => a.FileName));
|
|
new Thread(() => SaveAsync(mods)).Start();
|
|
}
|
|
|
|
void ExecInOldThread(Action action) {
|
|
if (dispatcher.HasShutdownStarted || dispatcher.HasShutdownFinished)
|
|
return;
|
|
dispatcher.BeginInvoke(DispatcherPriority.Background, action);
|
|
}
|
|
|
|
ModuleSaver? moduleSaver;
|
|
void SaveAsync(SaveOptionsVM[] mods) {
|
|
DnSpyEventSource.Log.SaveDocumentsStart();
|
|
try {
|
|
moduleSaver = new ModuleSaver(mods);
|
|
moduleSaver.OnProgressUpdated += moduleSaver_OnProgressUpdated;
|
|
moduleSaver.OnLogMessage += moduleSaver_OnLogMessage;
|
|
moduleSaver.OnWritingFile += moduleSaver_OnWritingFile;
|
|
moduleSaver.SaveAll();
|
|
AsyncAddMessage(dnSpy_AsmEditor_Resources.SaveModules_Log_AllFilesWritten, false, false);
|
|
}
|
|
catch (TaskCanceledException) {
|
|
AsyncAddMessage(dnSpy_AsmEditor_Resources.SaveModules_Log_SaveWasCanceled, true, false);
|
|
}
|
|
catch (UnauthorizedAccessException ex) {
|
|
AsyncAddMessage(string.Format(dnSpy_AsmEditor_Resources.SaveModules_Log_AccessError, ex.Message), true, false);
|
|
}
|
|
catch (IOException ex) {
|
|
AsyncAddMessage(string.Format(dnSpy_AsmEditor_Resources.SaveModules_Log_FileError, ex.Message), true, false);
|
|
}
|
|
catch (Exception ex) {
|
|
AsyncAddMessage(string.Format(dnSpy_AsmEditor_Resources.SaveModules_Log_Exception, ex), true, false);
|
|
}
|
|
moduleSaver = null;
|
|
|
|
DnSpyEventSource.Log.SaveDocumentsStop();
|
|
ExecInOldThread(() => {
|
|
CurrentFileName = string.Empty;
|
|
State = SaveState.Saved;
|
|
});
|
|
}
|
|
|
|
void moduleSaver_OnWritingFile(object? sender, ModuleSaverWriteEventArgs e) {
|
|
if (e.Starting) {
|
|
ExecInOldThread(() => {
|
|
CurrentFileName = e.File.FileName;
|
|
});
|
|
AsyncAddMessage(string.Format(dnSpy_AsmEditor_Resources.SaveModules_Log_WritingFile, e.File.FileName), false, false);
|
|
}
|
|
else {
|
|
shownMessages.Clear();
|
|
savedFile.Add(e.File, true);
|
|
}
|
|
}
|
|
Dictionary<SaveOptionsVM, bool> savedFile = new Dictionary<SaveOptionsVM, bool>();
|
|
|
|
void moduleSaver_OnProgressUpdated(object? sender, EventArgs e) {
|
|
var moduleSaver = (ModuleSaver)sender!;
|
|
double totalProgress = 100 * moduleSaver.TotalProgress;
|
|
double currentFileProgress = 100 * moduleSaver.CurrentFileProgress;
|
|
ExecInOldThread(() => {
|
|
TotalProgress = totalProgress;
|
|
CurrentFileProgress = currentFileProgress;
|
|
});
|
|
}
|
|
|
|
void moduleSaver_OnLogMessage(object? sender, ModuleSaverLogEventArgs e) =>
|
|
AsyncAddMessage(e.Message, e.Event == ModuleSaverLogEvent.Error || e.Event == ModuleSaverLogEvent.Warning, true);
|
|
|
|
void AsyncAddMessage(string msg, bool isError, bool canIgnore) {
|
|
// If there are a lot of errors, we don't want to add a ton of extra delegates to be
|
|
// called in the old thread. Just use one so we don't slow down everything to a crawl.
|
|
lock (addMessageStringBuilder) {
|
|
if (!canIgnore || !shownMessages.Contains(msg)) {
|
|
addMessageStringBuilder.AppendLine(msg);
|
|
if (canIgnore)
|
|
shownMessages.Add(msg);
|
|
}
|
|
if (isError)
|
|
errors++;
|
|
if (!hasAddedMessage) {
|
|
hasAddedMessage = true;
|
|
ExecInOldThread(() => {
|
|
string logMsgTmp;
|
|
int errorsTmp;
|
|
lock (addMessageStringBuilder) {
|
|
logMsgTmp = addMessageStringBuilder.ToString();
|
|
errorsTmp = errors;
|
|
|
|
hasAddedMessage = false;
|
|
addMessageStringBuilder.Clear();
|
|
errors = 0;
|
|
}
|
|
|
|
ErrorCount += errorsTmp;
|
|
logMessage.Append(logMsgTmp);
|
|
OnPropertyChanged(nameof(LogMessage));
|
|
});
|
|
}
|
|
}
|
|
}
|
|
HashSet<string> shownMessages = new HashSet<string>(StringComparer.Ordinal);
|
|
StringBuilder addMessageStringBuilder = new StringBuilder();
|
|
int errors;
|
|
bool hasAddedMessage;
|
|
|
|
public void CancelSave() {
|
|
if (!IsSaving)
|
|
return;
|
|
var ms = moduleSaver;
|
|
if (ms is null)
|
|
return;
|
|
|
|
State = SaveState.Canceling;
|
|
ms.CancelAsync();
|
|
}
|
|
|
|
public event PropertyChangedEventHandler? PropertyChanged;
|
|
void OnPropertyChanged(string propName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
|
|
}
|
|
}
|