From b2d07d953b4c5fdb0795c99beda7fee811fd8c3c Mon Sep 17 00:00:00 2001 From: Scott Willeke Date: Sun, 25 Nov 2018 23:04:31 -0800 Subject: [PATCH] Issue #116 in progress in GUI. --- src/LessMsi.Core/Msi/Wixtracts.cs | 162 ++++++++++++++------ src/LessMsi.Gui/ExtractionProgressDialog.cs | 37 +++++ src/LessMsi.Gui/MainForm.cs | 11 +- 3 files changed, 156 insertions(+), 54 deletions(-) diff --git a/src/LessMsi.Core/Msi/Wixtracts.cs b/src/LessMsi.Core/Msi/Wixtracts.cs index 7a6fb55..ba8a06d 100644 --- a/src/LessMsi.Core/Msi/Wixtracts.cs +++ b/src/LessMsi.Core/Msi/Wixtracts.cs @@ -40,8 +40,7 @@ namespace LessMsi.Msi { public class Wixtracts { - #region class ExtractionProgress - + #region class ExtractionProgress Stuff /// /// Provides progress information during an extraction operatation. /// @@ -50,15 +49,22 @@ public class ExtractionProgress : IAsyncResult private string _currentFileName; private ExtractionActivity _activity; private readonly ManualResetEvent _waitSignal; - private readonly AsyncCallback _callback; + private readonly AsyncCallback _progressCallback; + private readonly ExtractionErrorHandler _errorCallback; private readonly int _totalFileCount; private int _filesExtracted; public ExtractionProgress(AsyncCallback progressCallback, int totalFileCount) + : this(progressCallback, totalFileCount, null) + { + } + + public ExtractionProgress(AsyncCallback progressCallback, int totalFileCount, ExtractionErrorHandler errorCallback) { _activity = ExtractionActivity.Initializing; _currentFileName = ""; - _callback = progressCallback; + _progressCallback = progressCallback; + _errorCallback = errorCallback; _waitSignal = new ManualResetEvent(false); _totalFileCount = totalFileCount; _filesExtracted = 0; @@ -75,11 +81,23 @@ internal void ReportProgress(ExtractionActivity activity, string currentFileName if (this.IsCompleted) _waitSignal.Set(); - if (_callback != null) - _callback(this); + if (_progressCallback != null) + _progressCallback(this); } } + /// + /// Called internally to report errors to the calling application. + /// + /// + internal ExtractionErrorResponse ReportError(string error, string fileName, string cabinetName) + { + if (_errorCallback != null) + return _errorCallback(this, error, fileName, cabinetName); + else + return ExtractionErrorResponse.Abort; + } + /// /// The total number of files to be extracted for this operation. /// @@ -186,8 +204,6 @@ public bool IsCompleted #endregion } - #endregion - #region enum ExtractionActivity /// @@ -203,6 +219,33 @@ public enum ExtractionActivity #endregion + #region ExtractionErrorResponse + /// + /// Allows + /// + public enum ExtractionErrorResponse + { + /// + /// Indicates extraction should attempt to continue. + /// + Continue, + /// + /// Indicates extraction should be aborted. + /// + Abort + } + #endregion + + /// + /// Defines the signature for a method to be notified of errors. + /// + /// The error message. + /// The name of the file that an error occurred while extracting. + /// The name of the cabinet containing the file that the extraction error occurred with. + /// Indicates how to handle the error. + public delegate ExtractionErrorResponse ExtractionErrorHandler(ExtractionProgress progressState, string error, string fileName, string cabinetName); + #endregion + public static void ExtractFiles(Path msi, string outputDir) { ExtractFiles(msi, outputDir, new string[0], null); @@ -220,49 +263,19 @@ public static void ExtractFiles(Path msi, string outputDir, string[] fileNamesTo ExtractFiles(msi, outputDir, msiFiles, progressCallback); } - private static MsiFile[] GetMsiFileFromFileNames(Path msi, string[] fileNamesToExtract) - { - var msiFiles = MsiFile.CreateMsiFilesFromMSI(msi); - Array.Sort(msiFiles, (f1, f2) => string.Compare(f1.LongFileName, f2.LongFileName, StringComparison.InvariantCulture)); - - var fileNamesToExtractAsMsiFiles = new List(); - foreach (var fileName in fileNamesToExtract) - { - var found = Array.BinarySearch(msiFiles, fileName, FileNameComparer.Default); - if (found >= 0) - fileNamesToExtractAsMsiFiles.Add(msiFiles[found]); - else - Console.WriteLine("File {0} was not found in the msi.", fileName); - } - return fileNamesToExtractAsMsiFiles.ToArray(); - } - - private sealed class FileNameComparer : IComparer - { - static readonly FileNameComparer _default = new FileNameComparer(); - - public static FileNameComparer Default - { - get { return _default; } - } - - int IComparer.Compare(object x, object y) - { - //expect two MsiFile or one MsiFile and one string: - var getName = new Func((object fileOrName) => fileOrName is MsiFile ? ((MsiFile) fileOrName).LongFileName : (string)fileOrName); - var xName = getName(x); - var yName = getName(y); - return string.Compare(xName, yName, StringComparison.InvariantCulture); - } - } + public static void ExtractFiles(Path msi, string outputDir, MsiFile[] filesToExtract, AsyncCallback progressCallback) + { + ExtractFiles(msi, outputDir, filesToExtract, progressCallback, null); + } - /// + /// /// Extracts the compressed files from the specified MSI file to the specified output directory. /// If specified, the list of objects are the only files extracted. /// /// The files to extract or null or empty to extract all files. /// Will be called during during the operation with progress information, and upon completion. The argument will be of type . - public static void ExtractFiles(Path msi, string outputDir, MsiFile[] filesToExtract, AsyncCallback progressCallback) + /// Will be called during the operation with any error information. + public static void ExtractFiles(Path msi, string outputDir, MsiFile[] filesToExtract, AsyncCallback progressCallback, ExtractionErrorHandler errorCallback) { if (msi.IsEmpty) throw new ArgumentNullException("msi"); @@ -278,7 +291,7 @@ public static void ExtractFiles(Path msi, string outputDir, MsiFile[] filesToExt if (filesToExtract == null || filesToExtract.Length < 1) filesToExtract = MsiFile.CreateMsiFilesFromMSI(msidb); - progress = new ExtractionProgress(progressCallback, filesToExtract.Length); + progress = new ExtractionProgress(progressCallback, filesToExtract.Length, errorCallback); if (!FileSystem.Exists(msi)) { @@ -339,7 +352,17 @@ public static void ExtractFiles(Path msi, string outputDir, MsiFile[] filesToExt // ReSharper restore HeuristicUnreachableCode } Trace.WriteLine(string.Concat("Extracting File \'", compressedFile.Filename, "\' to \'", destName, "\'")); - compressedFile.ExtractTo(destName.PathString); + try + { + compressedFile.ExtractTo(destName.PathString); + } + catch (Exception e) + { + var errorMessage = string.Format("Error extracting file \"{0}\" from cab \"{1}\". Error detail: {2}", entry.LongFileName, decompressor.LocalFilePath, e.Message); + var result = progress.ReportError(errorMessage, entry.LongFileName, decompressor.LocalFilePath); + if (result == ExtractionErrorResponse.Abort) + throw new Exception(errorMessage); + } filesExtractedSoFar++; } } @@ -362,10 +385,47 @@ public static void ExtractFiles(Path msi, string outputDir, MsiFile[] filesToExt } } - /// - /// Deletes a file even if it is readonly. - /// - private static void DeleteFileForcefully(Path localFilePath) + private static MsiFile[] GetMsiFileFromFileNames(Path msi, string[] fileNamesToExtract) + { + var msiFiles = MsiFile.CreateMsiFilesFromMSI(msi); + Array.Sort(msiFiles, (f1, f2) => string.Compare(f1.LongFileName, f2.LongFileName, StringComparison.InvariantCulture)); + + var fileNamesToExtractAsMsiFiles = new List(); + foreach (var fileName in fileNamesToExtract) + { + var found = Array.BinarySearch(msiFiles, fileName, FileNameComparer.Default); + if (found >= 0) + fileNamesToExtractAsMsiFiles.Add(msiFiles[found]); + else + Console.WriteLine("File {0} was not found in the msi.", fileName); + } + return fileNamesToExtractAsMsiFiles.ToArray(); + } + + private sealed class FileNameComparer : IComparer + { + static readonly FileNameComparer _default = new FileNameComparer(); + + public static FileNameComparer Default + { + get { return _default; } + } + + int IComparer.Compare(object x, object y) + { + //expect two MsiFile or one MsiFile and one string: + var getName = new Func((object fileOrName) => fileOrName is MsiFile ? ((MsiFile)fileOrName).LongFileName : (string)fileOrName); + var xName = getName(x); + var yName = getName(y); + return string.Compare(xName, yName, StringComparison.InvariantCulture); + } + } + + + /// + /// Deletes a file even if it is readonly. + /// + private static void DeleteFileForcefully(Path localFilePath) { // In github issue #4 found that the cab files in the Win7SDK have the readonly attribute set and File.Delete fails to delete them. Explicitly unsetting that bit before deleting works okay... FileSystem.RemoveFile(localFilePath, true); diff --git a/src/LessMsi.Gui/ExtractionProgressDialog.cs b/src/LessMsi.Gui/ExtractionProgressDialog.cs index 85dd4dc..a1bd5f3 100644 --- a/src/LessMsi.Gui/ExtractionProgressDialog.cs +++ b/src/LessMsi.Gui/ExtractionProgressDialog.cs @@ -26,6 +26,7 @@ using System.Drawing; using System.Windows.Forms; using LessMsi.Msi; +using System.Collections.Generic; namespace LessMsi.Gui { @@ -33,6 +34,8 @@ internal class ExtractionProgressDialog : Form { private ProgressBar _progressBar; private Label _label; + private readonly List _errorFiles = new List(); + private bool _continuePromptingErrors = true; public ExtractionProgressDialog(Form owner) { @@ -94,5 +97,39 @@ public void UpdateProgress(Wixtracts.ExtractionProgress progress) this.Invalidate(true); this.Update(); } + + internal void ShowAnyFinalMessages() + { + if (this._errorFiles.Count > 0) + { + const int MAX_FILES = 10; + var displayList = this._errorFiles.GetRange(0, Math.Min(MAX_FILES, this._errorFiles.Count)); + var displayString = string.Join(", ", displayList); + var displayDelta = _errorFiles.Count - displayList.Count; + string postfix = displayDelta > 0 ? string.Format("...and {0} more", displayDelta) : ""; + MessageBox.Show(this, + string.Format("The following files failed to extract: {0}{1}.", displayString, postfix) + ); + } + } + + internal Wixtracts.ExtractionErrorResponse ExtractionErrorHandler(Wixtracts.ExtractionProgress progressState, string error, string fileName, string cabinetName) + { + this._errorFiles.Add(fileName); + if (this._continuePromptingErrors) + { + var result = MessageBox.Show(this, + string.Format("The following error occurred extracting a file:\r\n{0}\r\n Choose 'Abort' to abort extraction. Choose 'Retry' to attempt to continue extracting files, but continue reporting future errors. Choose 'Ignore' to ignore all future errors and continue extraction.", fileName), + "Extraction Error", + MessageBoxButtons.AbortRetryIgnore + ); + this._continuePromptingErrors = (result != DialogResult.Ignore); + return result == DialogResult.Abort ? Wixtracts.ExtractionErrorResponse.Abort : Wixtracts.ExtractionErrorResponse.Continue; + } + else + { + return Wixtracts.ExtractionErrorResponse.Continue; + } + } } } \ No newline at end of file diff --git a/src/LessMsi.Gui/MainForm.cs b/src/LessMsi.Gui/MainForm.cs index 0ee5f8f..60c5019 100644 --- a/src/LessMsi.Gui/MainForm.cs +++ b/src/LessMsi.Gui/MainForm.cs @@ -920,9 +920,14 @@ private void btnExtract_Click(object sender, EventArgs e) var filesToExtract = selectedFiles.ToArray(); - Wixtracts.ExtractFiles(msiFile, outputDir, filesToExtract, - new AsyncCallback(progressDialog.UpdateProgress)); - } + Wixtracts.ExtractFiles(msiFile, + outputDir, + filesToExtract, + new AsyncCallback(progressDialog.UpdateProgress), + progressDialog.ExtractionErrorHandler + ); + progressDialog.ShowAnyFinalMessages(); + } catch (Exception err) { MessageBox.Show(this,