/* 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 . */ 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 { /// /// We haven't started saving yet /// Loaded, /// /// We're saving /// Saving, /// /// We're canceling /// Canceling, /// /// Final state even if some files weren't saved /// 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 Modules { get; } = new ObservableCollection(); 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 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 savedFile = new Dictionary(); 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 shownMessages = new HashSet(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)); } }