src/Program.Updater.cs
Rama main · réplica de lectura del repositorio original.
using System;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Threading;
using System.Windows.Forms;
namespace Clipman
{
internal static partial class Program
{
private static void ApplyUpdateFromCommandLine(string[] args)
{
try
{
string zipUrl;
string targetDir;
string exePath;
string tempBase;
string pidText;
var noRestart = args.Any(arg => string.Equals(arg, "--update-no-restart", StringComparison.OrdinalIgnoreCase));
TryGetOptionValue(args, "--update-url", out zipUrl);
TryGetOptionValue(args, "--update-target", out targetDir);
TryGetOptionValue(args, "--update-exe", out exePath);
TryGetOptionValue(args, "--update-temp", out tempBase);
TryGetOptionValue(args, "--update-wait-pid", out pidText);
if (string.IsNullOrWhiteSpace(zipUrl) ||
string.IsNullOrWhiteSpace(targetDir) ||
string.IsNullOrWhiteSpace(exePath))
{
throw new InvalidOperationException("The updater was not given enough information to install the update.");
}
WriteUpdateHistory(targetDir, "Update command received.");
int processId;
if (int.TryParse(pidText, out processId) && processId > 0)
{
WriteUpdateHistory(targetDir, "Waiting for Clipman process " + processId + " to exit.");
WaitForProcessExit(processId);
}
ApplyUpdate(zipUrl, targetDir, exePath, string.IsNullOrWhiteSpace(tempBase) ? Path.GetTempPath() : tempBase, noRestart);
}
catch (Exception ex)
{
WriteUpdateHistory(args, "ERROR: " + ex.Message);
WriteUpdaterLog(args, ex);
MessageBox.Show(
"Clipman update failed:" + Environment.NewLine + Environment.NewLine + ex.Message,
"Clipman updater",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
}
private static void ApplyUpdate(string zipUrl, string targetDir, string exePath, string tempBase, bool noRestart)
{
Directory.CreateDirectory(tempBase);
var root = Path.Combine(tempBase, "ClipmanUpdate_" + Guid.NewGuid().ToString("N"));
var zip = Path.Combine(root, "update.zip");
var stage = Path.Combine(root, "stage");
Directory.CreateDirectory(root);
Directory.CreateDirectory(stage);
try
{
WriteUpdateHistory(targetDir, "Downloading update ZIP.");
DownloadUpdateZip(zipUrl, zip);
WriteUpdateHistory(targetDir, "Extracting update ZIP.");
ZipFile.ExtractToDirectory(zip, stage);
var source = FindUpdateSourceFolder(stage);
if (string.IsNullOrWhiteSpace(source))
{
throw new InvalidOperationException("The update ZIP does not contain clipman.exe.");
}
WriteUpdateHistory(targetDir, "Requesting other shared-folder instances to close.");
SharedUpdateStateStore.PublishCloseRequest(Path.Combine(targetDir, "Settings"), exePath, 90);
Thread.Sleep(5000);
WriteUpdateHistory(targetDir, "Update source located. Applying files.");
Directory.CreateDirectory(targetDir);
CleanupObsoleteRootUpdateFolders(targetDir);
CleanupObsoleteFactorySoundBackups(targetDir);
foreach (var item in Directory.GetFileSystemEntries(source))
{
var name = Path.GetFileName(item);
if (IsPreservedRuntimeItem(name))
{
continue;
}
var destination = Path.Combine(targetDir, name);
if (Directory.Exists(item))
{
if (IsFactoryReplacedDirectory(name))
{
ReplaceFactoryDirectory(item, destination);
}
else
{
ReplaceDirectory(item, destination);
}
}
else
{
CopyFileWithRetry(item, destination);
}
}
TryDeleteFile(Path.Combine(targetDir, "README.md"));
CleanupObsoleteFactorySoundBackups(targetDir);
CleanupEmptyBackupFolders(targetDir);
}
finally
{
TryDeleteDirectory(root);
}
if (noRestart)
{
WriteUpdateHistory(targetDir, "Update applied. Restart skipped by command line.");
return;
}
WriteUpdateHistory(targetDir, "Update applied. Restarting Clipman.");
TryRestartUpdatedApp(exePath, targetDir);
}
private static bool IsPreservedRuntimeItem(string name)
{
foreach (var preserved in new[] { "Settings", "Logs", "Backups" })
{
if (string.Equals(name, preserved, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
private static bool IsFactoryReplacedDirectory(string name)
{
return string.Equals(name, "sounds", StringComparison.OrdinalIgnoreCase);
}
private static void ReplaceFactoryDirectory(string source, string destination)
{
Directory.CreateDirectory(destination);
CopyFactoryDirectoryBestEffort(source, destination);
PruneFactoryDirectoryBestEffort(source, destination);
}
private static void ReplaceDirectory(string source, string destination)
{
if (Directory.Exists(destination))
{
DeleteDirectoryWithRetry(destination);
}
CopyDirectory(source, destination);
}
private static void CleanupObsoleteRootUpdateFolders(string targetDir)
{
var rootUpdateBackups = Path.Combine(targetDir, "Update Backups");
if (Directory.Exists(rootUpdateBackups))
{
TryDeleteDirectory(rootUpdateBackups);
}
TryDeleteDirectory(Path.Combine(targetDir, "Backups\\Updates"));
TryDeleteDirectory(Path.Combine(targetDir, "Update Temp"));
CleanupEmptyBackupFolders(targetDir);
}
private static void CleanupStaleUpdaterLaunchers(string appDirectory)
{
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var roots = new[]
{
string.IsNullOrWhiteSpace(localAppData) ? string.Empty : Path.Combine(localAppData, "Temp"),
Path.GetTempPath(),
Path.Combine(appDirectory, "Settings", "Update Temp")
}
.Where(path => !string.IsNullOrWhiteSpace(path))
.Select(path => Path.GetFullPath(path))
.Distinct(StringComparer.OrdinalIgnoreCase);
var cutoff = DateTime.UtcNow.AddMinutes(-5);
foreach (var root in roots)
{
try
{
if (!Directory.Exists(root)) continue;
foreach (var folder in Directory.GetDirectories(root, "ClipmanUpdater-*", SearchOption.TopDirectoryOnly))
{
try
{
if (Directory.GetLastWriteTimeUtc(folder) > cutoff) continue;
Directory.Delete(folder, true);
}
catch
{
}
}
}
catch
{
}
}
}
private static void CleanupObsoleteFactorySoundBackups(string targetDir)
{
try
{
var backupRoots = new[]
{
Path.Combine(targetDir, "Backups\\Updates"),
Path.Combine(targetDir, "Update Backups")
};
foreach (var root in backupRoots)
{
if (!Directory.Exists(root)) continue;
foreach (var file in Directory.GetFiles(root, "Previous-sounds*.zip", SearchOption.AllDirectories))
{
TryDeleteFile(file);
}
foreach (var folder in Directory.GetDirectories(root, "Previous-sounds*", SearchOption.AllDirectories)
.OrderByDescending(path => path.Length))
{
TryDeleteDirectory(folder);
}
}
}
catch
{
}
}
private static void DownloadUpdateZip(string zipUrl, string destination)
{
NetworkSecurity.EnableModernTls();
using (var client = new WebClient())
{
client.Headers[HttpRequestHeader.UserAgent] = "Clipman updater";
client.DownloadFile(zipUrl, destination);
}
}
private static string FindUpdateSourceFolder(string stage)
{
var direct = Path.Combine(stage, "clipman.exe");
if (File.Exists(direct))
{
return stage;
}
var candidates = Directory.GetFiles(stage, "clipman.exe", SearchOption.AllDirectories);
return candidates.Length == 0 ? string.Empty : Path.GetDirectoryName(candidates[0]);
}
private static void WaitForProcessExit(int processId)
{
try
{
using (var process = Process.GetProcessById(processId))
{
process.WaitForExit(30000);
}
}
catch
{
}
}
private static void TryRestartUpdatedApp(string exePath, string targetDir)
{
try
{
if (string.IsNullOrWhiteSpace(exePath) || !File.Exists(exePath))
{
throw new FileNotFoundException("The updated Clipman executable could not be found.", exePath ?? string.Empty);
}
var workingDirectory = Directory.Exists(targetDir) ? targetDir : Path.GetDirectoryName(exePath);
Process.Start(new ProcessStartInfo
{
FileName = exePath,
WorkingDirectory = workingDirectory,
UseShellExecute = true
});
}
catch (Exception ex)
{
WriteUpdaterLog(null, ex);
MessageBox.Show(
"Clipman was updated, but it could not be restarted automatically." +
Environment.NewLine +
Environment.NewLine +
"Please start Clipman from its installed folder or shortcut." +
Environment.NewLine +
Environment.NewLine +
ex.Message,
"Clipman updater",
MessageBoxButtons.OK,
MessageBoxIcon.Information);
}
}
private static void CopyDirectory(string source, string destination)
{
Directory.CreateDirectory(destination);
foreach (var directory in Directory.GetDirectories(source, "*", SearchOption.AllDirectories))
{
Directory.CreateDirectory(Path.Combine(destination, RelativePath(source, directory)));
}
foreach (var file in Directory.GetFiles(source, "*", SearchOption.AllDirectories))
{
var target = Path.Combine(destination, RelativePath(source, file));
Directory.CreateDirectory(Path.GetDirectoryName(target));
CopyFileWithRetry(file, target);
}
}
private static void CopyFactoryDirectoryBestEffort(string source, string destination)
{
foreach (var directory in Directory.GetDirectories(source, "*", SearchOption.AllDirectories))
{
Directory.CreateDirectory(Path.Combine(destination, RelativePath(source, directory)));
}
foreach (var file in Directory.GetFiles(source, "*", SearchOption.AllDirectories))
{
var target = Path.Combine(destination, RelativePath(source, file));
try
{
Directory.CreateDirectory(Path.GetDirectoryName(target));
CopyFileWithRetry(file, target);
}
catch (Exception ex)
{
WriteUpdateHistory(Path.GetDirectoryName(destination), "Warning: could not update factory file " + target + ": " + ex.Message);
}
}
}
private static void PruneFactoryDirectoryBestEffort(string source, string destination)
{
try
{
foreach (var file in Directory.GetFiles(destination, "*", SearchOption.AllDirectories))
{
var relative = RelativePath(destination, file);
if (File.Exists(Path.Combine(source, relative)))
{
continue;
}
TryDeleteFile(file);
}
foreach (var folder in Directory.GetDirectories(destination, "*", SearchOption.AllDirectories)
.OrderByDescending(path => path.Length))
{
RemoveEmptyDirectory(folder);
}
}
catch (Exception ex)
{
WriteUpdateHistory(Path.GetDirectoryName(destination), "Warning: could not fully prune factory folder " + destination + ": " + ex.Message);
}
}
private static void CopyFileWithRetry(string source, string destination)
{
Exception last = null;
for (var attempt = 0; attempt < 30; attempt++)
{
try
{
Directory.CreateDirectory(Path.GetDirectoryName(destination));
File.Copy(source, destination, true);
return;
}
catch (Exception ex)
{
last = ex;
Thread.Sleep(1000);
}
}
throw new IOException("Could not replace " + destination + " after waiting for other instances to close.", last);
}
private static void DeleteDirectoryWithRetry(string path)
{
Exception last = null;
for (var attempt = 0; attempt < 30; attempt++)
{
try
{
if (Directory.Exists(path))
{
Directory.Delete(path, true);
}
return;
}
catch (Exception ex)
{
last = ex;
Thread.Sleep(1000);
}
}
throw new IOException("Could not replace " + path + " after waiting for other instances to close.", last);
}
private static string RelativePath(string root, string path)
{
var fullRoot = Path.GetFullPath(root).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
var fullPath = Path.GetFullPath(path);
return fullPath.StartsWith(fullRoot, StringComparison.OrdinalIgnoreCase) ? fullPath.Substring(fullRoot.Length) : Path.GetFileName(path);
}
private static void RemoveEmptyDirectory(string folder)
{
try
{
if (Directory.Exists(folder) && !Directory.GetFileSystemEntries(folder).Any())
{
Directory.Delete(folder);
}
}
catch
{
}
}
private static void CleanupEmptyBackupFolders(string targetDir)
{
try
{
var backups = Path.Combine(targetDir, "Backups");
if (!Directory.Exists(backups)) return;
foreach (var folder in Directory.GetDirectories(backups, "*", SearchOption.AllDirectories).OrderByDescending(p => p.Length))
{
RemoveEmptyDirectory(folder);
}
RemoveEmptyDirectory(Path.Combine(backups, "Updates"));
RemoveEmptyDirectory(backups);
}
catch
{
}
}
private static void TryDeleteDirectory(string path)
{
try
{
if (Directory.Exists(path))
{
Directory.Delete(path, true);
}
}
catch
{
}
}
private static void TryDeleteFile(string path)
{
try
{
if (File.Exists(path))
{
File.Delete(path);
}
}
catch
{
}
}
private static void WriteUpdaterLog(string[] args, Exception exception)
{
try
{
string targetDir;
if (args == null || !TryGetOptionValue(args, "--update-target", out targetDir) || string.IsNullOrWhiteSpace(targetDir))
{
targetDir = AppDomain.CurrentDomain.BaseDirectory;
}
var logRoot = Path.Combine(targetDir, "Logs");
Directory.CreateDirectory(logRoot);
File.AppendAllText(
Path.Combine(logRoot, "Updater.log"),
DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") +
" Clipman updater error" +
Environment.NewLine +
(exception == null ? "(No exception object.)" : exception.ToString()) +
Environment.NewLine +
Environment.NewLine);
}
catch
{
}
}
private static void WriteUpdateHistory(string[] args, string message)
{
try
{
string targetDir;
if (args == null || !TryGetOptionValue(args, "--update-target", out targetDir) || string.IsNullOrWhiteSpace(targetDir))
{
targetDir = AppDomain.CurrentDomain.BaseDirectory;
}
WriteUpdateHistory(targetDir, message);
}
catch
{
}
}
private static void WriteUpdateHistory(string targetDir, string message)
{
try
{
if (string.IsNullOrWhiteSpace(targetDir))
{
targetDir = AppDomain.CurrentDomain.BaseDirectory;
}
var logRoot = Path.Combine(targetDir, "Logs");
Directory.CreateDirectory(logRoot);
File.AppendAllText(
Path.Combine(logRoot, "Update.log"),
DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") +
" " +
(string.IsNullOrWhiteSpace(message) ? "(no update message)" : message) +
Environment.NewLine);
}
catch
{
}
}
}
}