1- using MicaSetup.Natives;
21using System;
32using System.Diagnostics;
43using System.Globalization;
54using System.IO;
6- using System.Security.Cryptography;
75using System.Text;
8- using System.Threading;
96
107namespace MicaSetup.Helper;
118
129public static class InstallerSelfCleanupHelper
1310{
14- private const string SelfCleanupArgument = "codexcliplus-self -cleanup";
11+ private const string InstallerCleanupArgument = "codexcliplus-installer -cleanup";
1512 private const string TargetArgument = "codexcliplus-cleanup-target";
1613 private const string ParentProcessArgument = "codexcliplus-cleanup-parent";
17- private const int DeleteRetryCount = 60;
18- private const int DeleteRetryDelayMilliseconds = 1000;
19- private const int ParentProcessWaitMilliseconds = 30000;
2014
21- public static bool TryRunSelfCleanupMode()
22- {
23- string[] args = Environment.GetCommandLineArgs();
24- if (!HasArgument(args, SelfCleanupArgument))
25- {
26- return false;
27- }
28-
29- try
30- {
31- RunSelfCleanup(args);
32- }
33- catch (Exception e)
34- {
35- Logger.Error("[InstallerCleanup] self cleanup failed:", e);
36- }
37-
38- return true;
39- }
40-
41- public static void ScheduleDelete(string installerPath)
15+ public static void ScheduleDelete(string installerPath, string cleanupHelperPath)
4216 {
4317 string targetPath = NormalizeInstallerPath(installerPath);
4418 string currentExecutablePath = GetCurrentExecutablePath();
@@ -47,13 +21,13 @@ public static class InstallerSelfCleanupHelper
4721 throw new InvalidOperationException("[InstallerCleanup] delete target must be the running installer.");
4822 }
4923
50- string helperPath = CreateHelperCopy(currentExecutablePath );
24+ string helperPath = NormalizeCleanupHelperPath(cleanupHelperPath, targetPath );
5125 int parentProcessId = Process.GetCurrentProcess().Id;
5226 string arguments = string.Join(
5327 " ",
54- "/ " + SelfCleanupArgument ,
55- "/ " + TargetArgument + "=" + EncodeArgument(targetPath),
56- "/ " + ParentProcessArgument + "=" + parentProcessId.ToString(CultureInfo.InvariantCulture)
28+ "-- " + InstallerCleanupArgument ,
29+ "-- " + TargetArgument + "=" + EncodeArgument(targetPath),
30+ "-- " + ParentProcessArgument + "=" + parentProcessId.ToString(CultureInfo.InvariantCulture)
5731 );
5832
5933 using Process process = new()
@@ -71,36 +45,6 @@ public static class InstallerSelfCleanupHelper
7145 process.Start();
7246 }
7347
74- private static void RunSelfCleanup(string[] args)
75- {
76- string? encodedTarget = GetArgumentValue(args, TargetArgument);
77- if (string.IsNullOrWhiteSpace(encodedTarget))
78- {
79- Logger.Warning("[InstallerCleanup] cleanup target argument is empty.");
80- return;
81- }
82-
83- string targetPath = NormalizeInstallerPath(DecodeArgument(encodedTarget!));
84- string helperPath = GetCurrentExecutablePath();
85- if (string.Equals(targetPath, helperPath, StringComparison.OrdinalIgnoreCase))
86- {
87- Logger.Warning("[InstallerCleanup] refusing to delete cleanup helper itself as target.");
88- return;
89- }
90-
91- int parentProcessId = ParseParentProcessId(GetArgumentValue(args, ParentProcessArgument));
92- WaitForParentProcess(parentProcessId);
93-
94- if (!IsSameFileContent(helperPath, targetPath))
95- {
96- Logger.Warning($"[InstallerCleanup] target does not match cleanup helper copy: {targetPath}");
97- return;
98- }
99-
100- TryDeleteInstaller(targetPath);
101- ScheduleHelperCleanupOnReboot(helperPath);
102- }
103-
10448 private static string NormalizeInstallerPath(string installerPath)
10549 {
10650 if (string.IsNullOrWhiteSpace(installerPath))
@@ -117,135 +61,30 @@ public static class InstallerSelfCleanupHelper
11761 return fullPath;
11862 }
11963
120- private static string CreateHelperCopy (string currentExecutablePath )
64+ private static string NormalizeCleanupHelperPath (string cleanupHelperPath, string targetPath )
12165 {
122- string helperRoot = Path.Combine(
123- Path.GetTempPath(),
124- "CodexCliPlus",
125- "InstallerCleanup",
126- Guid.NewGuid().ToString("N")
127- );
128- Directory.CreateDirectory(helperRoot);
129-
130- string helperPath = Path.Combine(helperRoot, "CodexCliPlus.InstallerCleanup.exe");
131- File.Copy(currentExecutablePath, helperPath, overwrite: false);
132- File.SetAttributes(helperPath, FileAttributes.Normal);
133- return helperPath;
134- }
135-
136- private static void WaitForParentProcess(int parentProcessId)
137- {
138- if (parentProcessId <= 0)
66+ if (string.IsNullOrWhiteSpace(cleanupHelperPath))
13967 {
140- return;
141- }
142-
143- try
144- {
145- using Process parentProcess = Process.GetProcessById(parentProcessId);
146- parentProcess.WaitForExit(ParentProcessWaitMilliseconds);
147- }
148- catch (ArgumentException)
149- {
150- }
151- catch (Exception e)
152- {
153- Logger.Warning($"[InstallerCleanup] cannot wait for parent process: {e.Message}");
154- }
155-
156- Thread.Sleep(DeleteRetryDelayMilliseconds);
157- }
158-
159- private static void TryDeleteInstaller(string targetPath)
160- {
161- Exception? lastError = null;
162- for (int attempt = 1; attempt <= DeleteRetryCount; attempt++)
163- {
164- try
165- {
166- if (!File.Exists(targetPath))
167- {
168- Logger.Information($"[InstallerCleanup] installer already deleted: {targetPath}");
169- return;
170- }
171-
172- File.SetAttributes(targetPath, FileAttributes.Normal);
173- File.Delete(targetPath);
174- if (!File.Exists(targetPath))
175- {
176- Logger.Information($"[InstallerCleanup] deleted installer: {targetPath}");
177- return;
178- }
179- }
180- catch (Exception e)
181- {
182- lastError = e;
183- }
184-
185- Thread.Sleep(DeleteRetryDelayMilliseconds);
68+ throw new ArgumentException("Cleanup helper path is empty.", nameof(cleanupHelperPath));
18669 }
18770
188- Logger.Error($"[InstallerCleanup] installer delete failed: {targetPath}", lastError?.Message ?? string.Empty);
189- TryScheduleDeleteOnReboot(targetPath);
190- }
191-
192- private static bool IsSameFileContent(string leftPath, string rightPath)
193- {
194- if (!File.Exists(leftPath) || !File.Exists(rightPath))
71+ string fullPath = Path.GetFullPath(cleanupHelperPath);
72+ if (!File.Exists(fullPath))
19573 {
196- return false ;
74+ throw new FileNotFoundException("[InstallerCleanup] cleanup helper is missing.", fullPath) ;
19775 }
19876
199- FileInfo left = new(leftPath);
200- FileInfo right = new(rightPath);
201- if (left.Length != right.Length)
77+ if (!string.Equals(Path.GetExtension(fullPath), ".exe", StringComparison.OrdinalIgnoreCase))
20278 {
203- return false ;
79+ throw new InvalidOperationException("[InstallerCleanup] cleanup helper must be an executable.") ;
20480 }
20581
206- string leftHash = ComputeSha256(leftPath);
207- string rightHash = ComputeSha256(rightPath);
208- return string.Equals(leftHash, rightHash, StringComparison.Ordinal);
209- }
210-
211- private static string ComputeSha256(string path)
212- {
213- using FileStream stream = new(
214- path,
215- FileMode.Open,
216- FileAccess.Read,
217- FileShare.ReadWrite | FileShare.Delete,
218- bufferSize: 1024 * 128,
219- FileOptions.SequentialScan
220- );
221- using SHA256 sha256 = SHA256.Create();
222- return Convert.ToBase64String(sha256.ComputeHash(stream));
223- }
224-
225- private static void ScheduleHelperCleanupOnReboot(string helperPath)
226- {
227- TryScheduleDeleteOnReboot(helperPath);
228-
229- string? helperDirectory = Path.GetDirectoryName(helperPath);
230- if (!string.IsNullOrWhiteSpace(helperDirectory))
82+ if (string.Equals(fullPath, targetPath, StringComparison.OrdinalIgnoreCase))
23183 {
232- TryScheduleDeleteOnReboot(helperDirectory );
84+ throw new InvalidOperationException("[InstallerCleanup] cleanup helper cannot be the installer target." );
23385 }
234- }
23586
236- private static void TryScheduleDeleteOnReboot(string path)
237- {
238- try
239- {
240- if (File.Exists(path) || Directory.Exists(path))
241- {
242- _ = Kernel32.MoveFileEx(path, null!, MoveFileFlags.MOVEFILE_DELAY_UNTIL_REBOOT);
243- }
244- }
245- catch (Exception e)
246- {
247- Logger.Warning($"[InstallerCleanup] cannot schedule cleanup on reboot: {e.Message}");
248- }
87+ return fullPath;
24988 }
25089
25190 private static string GetCurrentExecutablePath()
@@ -267,58 +106,8 @@ public static class InstallerSelfCleanupHelper
267106 return Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, AppDomain.CurrentDomain.FriendlyName));
268107 }
269108
270- private static bool HasArgument(string[] args, string name)
271- {
272- string slashName = "/" + name;
273- string dashName = "-" + name;
274- foreach (string arg in args)
275- {
276- if (
277- string.Equals(arg, slashName, StringComparison.OrdinalIgnoreCase)
278- || string.Equals(arg, dashName, StringComparison.OrdinalIgnoreCase)
279- )
280- {
281- return true;
282- }
283- }
284-
285- return false;
286- }
287-
288- private static string? GetArgumentValue(string[] args, string name)
289- {
290- string slashPrefix = "/" + name + "=";
291- string dashPrefix = "-" + name + "=";
292- foreach (string arg in args)
293- {
294- if (arg.StartsWith(slashPrefix, StringComparison.OrdinalIgnoreCase))
295- {
296- return arg.Substring(slashPrefix.Length);
297- }
298-
299- if (arg.StartsWith(dashPrefix, StringComparison.OrdinalIgnoreCase))
300- {
301- return arg.Substring(dashPrefix.Length);
302- }
303- }
304-
305- return null;
306- }
307-
308- private static int ParseParentProcessId(string? value)
309- {
310- return int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int processId)
311- ? processId
312- : 0;
313- }
314-
315109 private static string EncodeArgument(string value)
316110 {
317111 return Convert.ToBase64String(Encoding.UTF8.GetBytes(value));
318112 }
319-
320- private static string DecodeArgument(string value)
321- {
322- return Encoding.UTF8.GetString(Convert.FromBase64String(value));
323- }
324113}
0 commit comments