@joebigelow / wix / commits / 3dac27d5

Add retries to several file system operations

Closes #6815

Rob Mensching committed Jul 7, 2022 at 17:01 UTC 3dac27d54c104796b85b6582d6f878a553f335fa
3 files changed +106 -9
src/wix/WixToolset.Core.Native/FileSystem.cs
+38 -6
@@ -7,6 +7,7 @@ namespace WixToolset.Core.Native
7 using System.IO;
8 using System.Runtime.InteropServices;
9 using System.Security.AccessControl;
10 + using System.Threading;
11
12 /// <summary>
13 /// File system helpers.
@@ -23,13 +24,20 @@ namespace WixToolset.Core.Native
24 {
25 EnsureDirectoryWithoutFile(destination);
26
26 - if (!allowHardlink || !CreateHardLink(destination, source, IntPtr.Zero))
27 + var hardlinked = false;
28 +
29 + if (allowHardlink)
30 + {
31 + ActionWithRetries(() => hardlinked = CreateHardLink(destination, source, IntPtr.Zero));
32 + }
33 +
34 + if (!hardlinked)
35 {
36 #if DEBUG
37 var er = Marshal.GetLastWin32Error();
38 #endif
39
32 - File.Copy(source, destination, overwrite: true);
40 + ActionWithRetries(() => File.Copy(source, destination, overwrite: true));
41 }
42 }
43
@@ -42,7 +50,7 @@ namespace WixToolset.Core.Native
50 {
51 EnsureDirectoryWithoutFile(destination);
52
45 - File.Move(source, destination);
53 + ActionWithRetries(() => File.Move(source, destination));
54 }
55
56 /// <summary>
@@ -56,7 +64,31 @@ namespace WixToolset.Core.Native
64
65 foreach (var file in files)
66 {
59 - new FileInfo(file).SetAccessControl(aclReset);
67 + var fileInfo = new FileInfo(file);
68 + ActionWithRetries(() => fileInfo.SetAccessControl(aclReset));
69 + }
70 + }
71 +
72 + /// <summary>
73 + /// Executes an action and retries on any exception up to a few times. Primarily
74 + /// intended for use with file system operations that might get interrupted by
75 + /// external systems (usually anti-virus).
76 + /// </summary>
77 + /// <param name="action">Action to execute.</param>
78 + /// <param name="maxRetries">Maximum retry attempts.</param>
79 + public static void ActionWithRetries(Action action, int maxRetries = 3)
80 + {
81 + for (var attempt = 1; attempt <= maxRetries; ++attempt)
82 + {
83 + try
84 + {
85 + action();
86 + break;
87 + }
88 + catch when (attempt < maxRetries)
89 + {
90 + Thread.Sleep(250);
91 + }
92 }
93 }
94
@@ -66,10 +98,10 @@ namespace WixToolset.Core.Native
98
99 if (!String.IsNullOrEmpty(directory))
100 {
69 - Directory.CreateDirectory(directory);
101 + ActionWithRetries(() => Directory.CreateDirectory(directory));
102 }
103
72 - File.Delete(path);
104 + ActionWithRetries(() => File.Delete(path));
105 }
106
107 [DllImport("Kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
src/wix/WixToolset.Core.WindowsInstaller/Bind/MergeModulesCommand.cs
+5 -3
@@ -9,6 +9,8 @@ namespace WixToolset.Core.WindowsInstaller.Bind
9 using System.Linq;
10 using System.Runtime.InteropServices;
11 using System.Text;
12 + using System.Threading;
13 + using WixToolset.Core.Native;
14 using WixToolset.Core.Native.Msi;
15 using WixToolset.Core.Native.Msm;
16 using WixToolset.Data;
@@ -69,10 +71,10 @@ namespace WixToolset.Core.WindowsInstaller.Bind
71 {
72 merge = MsmInterop.GetMsmMerge();
73
72 - merge.OpenLog(logPath);
74 + FileSystem.ActionWithRetries(() => merge.OpenLog(logPath));
75 logOpen = true;
76
75 - merge.OpenDatabase(this.OutputPath);
77 + FileSystem.ActionWithRetries(() => merge.OpenDatabase(this.OutputPath));
78 databaseOpen = true;
79
80 var featureModulesByMergeId = this.Section.Symbols.OfType<WixFeatureModulesSymbol>().GroupBy(t => t.WixMergeRef).ToDictionary(g => g.Key);
@@ -97,7 +99,7 @@ namespace WixToolset.Core.WindowsInstaller.Bind
99 }
100
101 this.Messaging.Write(VerboseMessages.OpeningMergeModule(wixMergeRow.SourceFile, mergeLanguage));
100 - merge.OpenModule(wixMergeRow.SourceFile, mergeLanguage);
102 + FileSystem.ActionWithRetries(() => merge.OpenModule(wixMergeRow.SourceFile, mergeLanguage));
103 moduleOpen = true;
104
105 trackedFiles.Add(this.BackendHelper.TrackFile(wixMergeRow.SourceFile, TrackedFileType.Input, wixMergeRow.SourceLineNumbers));
src/wix/test/WixToolsetTest.CoreIntegration/ModuleFixture.cs
+63
@@ -3,8 +3,12 @@
3 namespace WixToolsetTest.CoreIntegration
4 {
5 using System.IO;
6 + using System.Linq;
7 using WixBuildTools.TestSupport;
8 using WixToolset.Core.TestPackage;
9 + using WixToolset.Data;
10 + using WixToolset.Data.Symbols;
11 + using WixToolset.Data.WindowsInstaller;
12 using Xunit;
13
14 public class ModuleFixture
@@ -42,5 +46,64 @@ namespace WixToolsetTest.CoreIntegration
46 }, rows);
47 }
48 }
49 +
50 + [Fact]
51 + public void CanMergeModuleAndValidate()
52 + {
53 + var msmFolder = TestData.Get(@"TestData\SimpleModule");
54 + var folder = TestData.Get(@"TestData\SimpleMerge");
55 +
56 + using (var fs = new DisposableFileSystem())
57 + {
58 + var intermediateFolder = fs.GetFolder();
59 + var msiPath = Path.Combine(intermediateFolder, @"bin\test.msi");
60 + var cabPath = Path.Combine(intermediateFolder, @"bin\cab1.cab");
61 +
62 + var msmResult = WixRunner.Execute(new[]
63 + {
64 + "build",
65 + Path.Combine(msmFolder, "Module.wxs"),
66 + "-loc", Path.Combine(msmFolder, "Module.en-us.wxl"),
67 + "-bindpath", Path.Combine(msmFolder, "data"),
68 + "-intermediateFolder", intermediateFolder,
69 + "-o", Path.Combine(intermediateFolder, "bin", "test", "test.msm")
70 + });
71 +
72 + msmResult.AssertSuccess();
73 +
74 + var result = WixRunner.Execute(new[]
75 + {
76 + "build",
77 + Path.Combine(folder, "Package.wxs"),
78 + "-loc", Path.Combine(folder, "Package.en-us.wxl"),
79 + "-bindpath", Path.Combine(intermediateFolder, "bin", "test"),
80 + "-intermediateFolder", intermediateFolder,
81 + "-o", msiPath
82 + });
83 +
84 + result.AssertSuccess();
85 +
86 + var intermediate = Intermediate.Load(Path.Combine(intermediateFolder, @"bin\test.wixpdb"));
87 + var section = intermediate.Sections.Single();
88 + Assert.Empty(section.Symbols.OfType<FileSymbol>());
89 +
90 + var data = WindowsInstallerData.Load(Path.Combine(intermediateFolder, @"bin\test.wixpdb"));
91 + Assert.Empty(data.Tables["File"].Rows);
92 +
93 + var results = Query.QueryDatabase(msiPath, new[] { "File" });
94 + WixAssert.CompareLineByLine(new[]
95 + {
96 + "File:File1.243FB739_4D05_472F_9CFB_EF6B1017B6DE\tModuleComponent1.243FB739_4D05_472F_9CFB_EF6B1017B6DE\tfile1.txt\t17\t\t\t512\t1",
97 + "File:File2.243FB739_4D05_472F_9CFB_EF6B1017B6DE\tModuleComponent2.243FB739_4D05_472F_9CFB_EF6B1017B6DE\tfile2.txt\t17\t\t\t512\t2",
98 + }, results);
99 +
100 + var files = Query.GetCabinetFiles(cabPath);
101 + WixAssert.CompareLineByLine(new[]
102 + {
103 + "File1.243FB739_4D05_472F_9CFB_EF6B1017B6DE",
104 + "File2.243FB739_4D05_472F_9CFB_EF6B1017B6DE"
105 + }, files.Select(f => f.Name).ToArray());
106 + }
107 + }
108 }
109 }