src/Program.cs
Rama main · réplica de lectura del repositorio original.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
using System.Threading;
using System.Windows.Forms;
namespace Clipman
{
internal static partial class Program
{
private const long MaximumLogBytes = 1024L * 1024L;
private const int RetainedLogGenerations = 3;
private static readonly object LogWriteLock = new object();
public const string MutexName = "Local\\ClipmanPortableSingleInstance";
public const string CloseEventName = "Local\\ClipmanCloseRequest";
public const string ShowEventName = "Local\\ClipmanShowHistoryRequest";
public const string RecoverEventName = "Local\\ClipmanRecoverStorageRequest";
public const string PauseEventName = "Local\\ClipmanPauseRequest";
public const string ResumeEventName = "Local\\ClipmanResumeRequest";
public const string ToggleEventName = "Local\\ClipmanToggleRequest";
public const string HistoryChangedEventName = "Local\\ClipmanHistoryChangedRequest";
public const string HistoryAddedEventName = "Local\\ClipmanHistoryAddedRequest";
private static readonly HashSet<string> SupportedTextExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
".txt", ".log", ".md", ".markdown",
".csv", ".tsv",
".json", ".xml", ".html", ".htm",
".css", ".js", ".ts",
".ps1", ".cmd", ".bat",
".ini", ".cfg", ".conf", ".yaml", ".yml",
".cs", ".vb", ".py", ".psm1", ".psd1",
".ahk", ".reg", ".rtf"
};
private static readonly HashSet<string> SupportedExtensionlessTextFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"README", "LICENSE", "LICENCE", "NOTICE", "CHANGELOG", "COPYING", "AUTHORS"
};
[STAThread]
private static void Main(string[] args)
{
if (args.Length > 0 && string.Equals(args[0], "--apply-update", StringComparison.OrdinalIgnoreCase))
{
ApplyUpdateFromCommandLine(args);
return;
}
if (args.Length > 0 && string.Equals(args[0], "--wait-restart", StringComparison.OrdinalIgnoreCase))
{
WaitRestartFromCommandLine(args);
return;
}
CleanupStartupArtifacts();
if (args.Length > 0 && string.Equals(args[0], "--close", StringComparison.OrdinalIgnoreCase))
{
SignalEvent(CloseEventName);
return;
}
if (args.Length > 0 && string.Equals(args[0], "--show", StringComparison.OrdinalIgnoreCase))
{
SignalEvent(ShowEventName);
return;
}
if (args.Length > 0 && string.Equals(args[0], "--pause", StringComparison.OrdinalIgnoreCase))
{
SignalEvent(PauseEventName);
return;
}
if (args.Length > 0 && string.Equals(args[0], "--resume", StringComparison.OrdinalIgnoreCase))
{
SignalEvent(ResumeEventName);
return;
}
if (args.Length > 0 && string.Equals(args[0], "--toggle", StringComparison.OrdinalIgnoreCase))
{
SignalEvent(ToggleEventName);
return;
}
if (args.Length > 1 && string.Equals(args[0], "--add", StringComparison.OrdinalIgnoreCase))
{
var entryId = AddText(string.Join(" ", args.Skip(1).ToArray()));
if (!string.IsNullOrEmpty(entryId))
{
InstanceStateStore.PublishPendingCommandEntry(entryId);
SignalEvent(HistoryAddedEventName);
SignalEvent(ShowEventName);
}
return;
}
if (args.Length > 1 && string.Equals(args[0], "--export", StringComparison.OrdinalIgnoreCase))
{
ExportDatabase(args[1]);
return;
}
if (args.Length > 1 && string.Equals(args[0], "--import", StringComparison.OrdinalIgnoreCase))
{
if (ImportDatabase(args[1], false))
{
SignalEvent(HistoryChangedEventName);
SignalEvent(ShowEventName);
}
return;
}
if (args.Length > 1 && string.Equals(args[0], "--import-replace", StringComparison.OrdinalIgnoreCase))
{
var silent = args.Any(arg => string.Equals(arg, "--yes", StringComparison.OrdinalIgnoreCase));
if (ImportDatabase(args[1], true, silent))
{
SignalEvent(HistoryChangedEventName);
SignalEvent(ShowEventName);
}
return;
}
if (HasImportFiles(args))
{
bool showHistory;
string entryId;
if (ImportFiles(args, out showHistory, out entryId))
{
InstanceStateStore.PublishPendingCommandEntry(entryId);
SignalEvent(HistoryAddedEventName);
if (showHistory)
{
SignalEvent(ShowEventName);
}
}
return;
}
Mutex mutex;
if (!AcquireSingleInstanceMutex(out mutex))
{
return;
}
using (mutex)
{
var appDirectory = AppDomain.CurrentDomain.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar);
InstanceStateStore.PublishCurrent(appDirectory);
try
{
RunApplication();
}
finally
{
InstanceStateStore.ClearIfCurrent(appDirectory);
}
GC.KeepAlive(mutex);
}
}
private static void CleanupStartupArtifacts()
{
var appDirectory = AppDomain.CurrentDomain.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar);
CleanupStaleUpdaterLaunchers(appDirectory);
CleanupObsoleteRootUpdateFolders(appDirectory);
CleanupObsoleteFactorySoundBackups(appDirectory);
CleanupEmptyBackupFolders(appDirectory);
}
private static bool AcquireSingleInstanceMutex(out Mutex mutex)
{
mutex = null;
if (TryAcquireMutex(out mutex))
{
return true;
}
var appDirectory = AppDomain.CurrentDomain.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar);
if (InstanceStateStore.IsSameRunningFolder(appDirectory))
{
SignalEvent(RecoverEventName);
SignalEvent(ShowEventName);
return false;
}
SignalEvent(CloseEventName);
WaitForPreviousInstanceToExit();
ResetEvent(CloseEventName);
var deadline = DateTime.UtcNow.AddSeconds(15);
while (DateTime.UtcNow < deadline)
{
if (TryAcquireMutex(out mutex))
{
return true;
}
Thread.Sleep(250);
}
SignalEvent(ShowEventName);
return false;
}
private static bool TryAcquireMutex(out Mutex mutex)
{
bool created;
mutex = new Mutex(true, MutexName, out created);
if (created)
{
return true;
}
mutex.Dispose();
mutex = null;
return false;
}
private static void WaitForPreviousInstanceToExit()
{
var processId = InstanceStateStore.RunningProcessId();
if (processId > 0)
{
try
{
using (var process = Process.GetProcessById(processId))
{
if (process.WaitForExit(10000))
{
return;
}
}
}
catch
{
return;
}
}
var current = Process.GetCurrentProcess();
var processName = Path.GetFileNameWithoutExtension(Application.ExecutablePath);
foreach (var process in Process.GetProcessesByName(processName).Where(p => p.Id != current.Id).ToList())
{
using (process)
{
try
{
process.WaitForExit(2000);
}
catch
{
}
}
}
}
private static void RunApplication()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
Application.ThreadException += (sender, e) =>
{
WriteRuntimeLog("Unhandled UI thread exception.", e.Exception);
};
AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
{
WriteRuntimeLog("Unhandled application exception.", e.ExceptionObject as Exception);
};
try
{
using (var contextHelp = new ContextHelp())
using (var app = new ClipmanApplicationContext())
{
Application.Run(app);
}
}
catch (OperationCanceledException)
{
WriteStartupLog("Startup cancelled by user.", null);
}
catch (Exception ex)
{
WriteStartupLog("Startup failed.", ex);
MessageBox.Show(
"Clipman could not start.\r\n\r\n" + ex.Message + "\r\n\r\nDetails were written to Logs\\Startup.log beside clipman.exe.",
"Clipman startup failed",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
}
private static void WriteStartupLog(string message, Exception exception)
{
WriteLogFile("Startup.log", message, exception);
}
internal static string RuntimeLogPath()
{
return Path.Combine(LogDirectory(), "Runtime.log");
}
internal static void WriteRuntimeLog(string message, Exception exception)
{
WriteLogFile("Runtime.log", message, exception);
}
private static void WriteLogFile(string fileName, string message, Exception exception)
{
try
{
var path = Path.Combine(LogDirectory(), fileName);
lock (LogWriteLock)
{
RotateLogFiles(path, MaximumLogBytes, RetainedLogGenerations);
using (var writer = new StreamWriter(path, true))
{
writer.WriteLine("[" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "] " + message);
writer.WriteLine("App: " + Assembly.GetExecutingAssembly().Location);
writer.WriteLine("Version: " + Assembly.GetExecutingAssembly().GetName().Version);
writer.WriteLine("Build stamp: " + BuildInfo.BuildStampUtcMs);
writer.WriteLine("OS: " + Environment.OSVersion);
writer.WriteLine(".NET: " + Environment.Version);
writer.WriteLine("Device: " + Environment.MachineName);
if (exception != null)
{
writer.WriteLine(exception);
}
else
{
writer.WriteLine("(No exception object.)");
}
writer.WriteLine();
}
}
}
catch
{
}
}
internal static void RotateLogFiles(string path, long maximumBytes, int retainedGenerations)
{
if (string.IsNullOrWhiteSpace(path) || maximumBytes < 1 || retainedGenerations < 1) return;
if (!File.Exists(path) || new FileInfo(path).Length < maximumBytes) return;
for (var generation = retainedGenerations; generation >= 1; generation--)
{
var destination = RotatedLogPath(path, generation);
if (File.Exists(destination)) File.Delete(destination);
var source = generation == 1 ? path : RotatedLogPath(path, generation - 1);
if (File.Exists(source)) File.Move(source, destination);
}
}
private static string RotatedLogPath(string path, int generation)
{
var directory = Path.GetDirectoryName(path) ?? string.Empty;
var name = Path.GetFileNameWithoutExtension(path);
var extension = Path.GetExtension(path);
return Path.Combine(directory, name + "." + generation + extension);
}
private static string LogDirectory()
{
var appDirectory = AppDomain.CurrentDomain.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar);
var logDirectory = Path.Combine(appDirectory, "Logs");
Directory.CreateDirectory(logDirectory);
return logDirectory;
}
private static void SignalEvent(string name)
{
try
{
bool created;
using (var ev = new EventWaitHandle(false, EventResetMode.ManualReset, name, out created))
{
ev.Set();
}
}
catch
{
}
}
private static void ResetEvent(string name)
{
try
{
bool created;
using (var ev = new EventWaitHandle(false, EventResetMode.ManualReset, name, out created))
{
ev.Reset();
}
}
catch
{
}
}
private static bool HasImportFiles(string[] args)
{
return args != null && args.Any(arg => !IsSwitch(arg) && File.Exists(arg));
}
private static bool ImportFiles(string[] args, out bool showHistory, out string latestEntryId)
{
var appDirectory = AppDomain.CurrentDomain.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar);
var settingsStore = new SettingsStore(appDirectory);
var settings = settingsStore.Load();
showHistory = settings.ShowHistoryAfterSendTo;
latestEntryId = string.Empty;
var imported = false;
using (var store = new ClipStore(settingsStore.EffectiveTextHistoryDatabasePath(settings), settingsStore.DatabasePassword(settings), settings.DeviceName))
{
foreach (var file in args.Where(arg => !IsSwitch(arg) && File.Exists(arg)))
{
var entry = ImportFile(store, file, settings);
if (entry == null) continue;
imported = true;
latestEntryId = entry.Id ?? string.Empty;
}
}
return imported;
}
private static ClipEntry ImportFile(ClipStore store, string file, AppSettings settings)
{
if (!IsSupportedTextFile(file))
{
return null;
}
string text;
try
{
text = File.ReadAllText(file);
}
catch
{
return null;
}
if (!string.IsNullOrEmpty(text))
{
if (settings.AutoRemoveUrlTracking)
{
text = UrlTrackingCleaner.CleanText(text);
}
var group = settings.AutoGroupByApp ? "Send To" : string.Empty;
return store.AddText(text, settings.DuplicateMode, settings.MaxHistoryEntries, settings.MaxHistoryDays, group);
}
return null;
}
internal static bool IsSupportedTextFile(string file)
{
var extension = Path.GetExtension(file);
if (!string.IsNullOrEmpty(extension))
{
return SupportedTextExtensions.Contains(extension);
}
return SupportedExtensionlessTextFiles.Contains(Path.GetFileName(file));
}
private static string AddText(string text)
{
if (string.IsNullOrEmpty(text)) return string.Empty;
var appDirectory = AppDomain.CurrentDomain.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar);
var settingsStore = new SettingsStore(appDirectory);
var settings = settingsStore.Load();
using (var store = new ClipStore(settingsStore.EffectiveTextHistoryDatabasePath(settings), settingsStore.DatabasePassword(settings), settings.DeviceName))
{
if (settings.AutoRemoveUrlTracking)
{
text = UrlTrackingCleaner.CleanText(text);
}
var entry = store.AddText(text, settings.DuplicateMode, settings.MaxHistoryEntries, settings.MaxHistoryDays);
return entry == null ? string.Empty : (entry.Id ?? string.Empty);
}
}
private static void ExportDatabase(string targetPath)
{
if (string.IsNullOrWhiteSpace(targetPath)) return;
if (File.Exists(targetPath)) return;
var dir = Path.GetDirectoryName(Path.GetFullPath(targetPath));
if (!string.IsNullOrEmpty(dir))
{
Directory.CreateDirectory(dir);
}
var appDirectory = AppDomain.CurrentDomain.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar);
var settingsStore = new SettingsStore(appDirectory);
var settings = settingsStore.Load();
using (var store = new ClipStore(settingsStore.EffectiveTextHistoryDatabasePath(settings), settingsStore.DatabasePassword(settings), settings.DeviceName))
{
store.ExportToFile(targetPath);
}
}
private static bool ImportDatabase(string sourcePath, bool replace)
{
return ImportDatabase(sourcePath, replace, false);
}
private static bool ImportDatabase(string sourcePath, bool replace, bool silent)
{
if (string.IsNullOrWhiteSpace(sourcePath) || !File.Exists(sourcePath)) return false;
var appDirectory = AppDomain.CurrentDomain.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar);
var settingsStore = new SettingsStore(appDirectory);
var settings = settingsStore.Load();
if (replace)
{
if (!silent && !ConfirmReplace(sourcePath))
{
return false;
}
BackupDatabase(settingsStore.EffectiveTextHistoryDatabasePath(settings));
}
using (var store = new ClipStore(settingsStore.EffectiveTextHistoryDatabasePath(settings), settingsStore.DatabasePassword(settings), settings.DeviceName))
{
if (!ImportDatabaseIntoStore(store, sourcePath, replace))
{
return false;
}
}
return true;
}
private static bool ImportDatabaseIntoStore(ClipStore store, string sourcePath, bool replace)
{
try
{
store.ImportFromFile(sourcePath, replace);
return true;
}
catch (DatabasePasswordRequiredException)
{
var password = PasswordPromptForm.Ask(
"Clipman import password",
"The selected Clipman import file is encrypted. Enter its history password.");
if (string.IsNullOrEmpty(password))
{
return false;
}
try
{
store.ImportFromFile(sourcePath, replace, password);
return true;
}
catch (DatabasePasswordRequiredException ex)
{
MessageBox.Show(ex.Message, "Clipman import", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return false;
}
}
catch (Exception ex)
{
MessageBox.Show("Clipman could not import the selected file.\r\n\r\n" + ex.Message, "Clipman import", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return false;
}
}
private static bool ConfirmReplace(string sourcePath)
{
var message =
"Replace Clipman history?\r\n\r\n" +
"This will replace the current clipboard history with:\r\n" +
sourcePath + "\r\n\r\n" +
"A timestamped backup of the current database will be created first.";
return MessageBox.Show(message, "Clipman Import Replace", MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2) == DialogResult.Yes;
}
private static void BackupDatabase(string databasePath)
{
if (string.IsNullOrWhiteSpace(databasePath) || !File.Exists(databasePath)) return;
var directory = Path.GetDirectoryName(databasePath);
var name = Path.GetFileNameWithoutExtension(databasePath);
var extension = Path.GetExtension(databasePath);
if (string.IsNullOrEmpty(directory)) directory = AppDomain.CurrentDomain.BaseDirectory;
if (string.IsNullOrEmpty(name)) name = "clipman-history";
if (string.IsNullOrEmpty(extension)) extension = ".json";
var stamp = DateTime.Now.ToString("yyyyMMdd-HHmmss");
var backupPath = Path.Combine(directory, name + ".before-import-replace-" + stamp + extension);
File.Copy(databasePath, backupPath, false);
}
private static bool IsSwitch(string arg)
{
return !string.IsNullOrEmpty(arg) && (arg.StartsWith("-", StringComparison.Ordinal) || arg.StartsWith("/", StringComparison.Ordinal));
}
private static bool TryGetOptionValue(string[] args, string optionName, out string value)
{
value = string.Empty;
if (args == null || string.IsNullOrWhiteSpace(optionName)) return false;
for (var i = 0; i < args.Length - 1; i++)
{
if (string.Equals(args[i], optionName, StringComparison.OrdinalIgnoreCase))
{
value = args[i + 1];
return true;
}
}
return false;
}
private static void WaitRestartFromCommandLine(string[] args)
{
string exePath;
string workingDirectory;
string statePath;
string buildText;
string pidText;
string timeoutText;
TryGetOptionValue(args, "--restart-exe", out exePath);
TryGetOptionValue(args, "--restart-working-dir", out workingDirectory);
TryGetOptionValue(args, "--restart-state", out statePath);
TryGetOptionValue(args, "--restart-current-build", out buildText);
TryGetOptionValue(args, "--restart-wait-pid", out pidText);
TryGetOptionValue(args, "--restart-timeout-ms", out timeoutText);
long currentBuild;
int processId;
int timeoutMs;
long.TryParse(buildText, out currentBuild);
int.TryParse(pidText, out processId);
if (!int.TryParse(timeoutText, out timeoutMs) || timeoutMs <= 0) timeoutMs = 120000;
if (processId > 0)
{
try
{
using (var process = Process.GetProcessById(processId))
{
process.WaitForExit(30000);
}
}
catch
{
}
}
var start = Environment.TickCount;
while (unchecked(Environment.TickCount - start) < timeoutMs)
{
try
{
if (File.Exists(exePath) && File.Exists(statePath))
{
var state = JsonUtil.Load<SharedUpdateState>(statePath);
if (state != null &&
state.BuildStampUtcMs > currentBuild &&
string.Equals(HashFileForRestart(exePath), state.ExeSha256, StringComparison.OrdinalIgnoreCase))
{
Process.Start(new ProcessStartInfo
{
FileName = exePath,
WorkingDirectory = Directory.Exists(workingDirectory) ? workingDirectory : Path.GetDirectoryName(exePath),
UseShellExecute = true
});
return;
}
}
}
catch
{
}
Thread.Sleep(1500);
}
}
private static string HashFileForRestart(string path)
{
using (var sha = SHA256.Create())
using (var stream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
{
return BitConverter.ToString(sha.ComputeHash(stream)).Replace("-", string.Empty).ToLowerInvariant();
}
}
}
internal static class SendToInstaller
{
private const string SendToShortcutName = "&Clipman - add text to history.lnk";
private const string OldSendToShortcutName = "Clipman - add text to history.lnk";
public static void SetInstalled(bool installed)
{
var path = GetSendToPath();
if (installed)
{
DeleteShortcutIfExists(OldSendToShortcutName);
var shellType = Type.GetTypeFromProgID("WScript.Shell");
if (shellType == null)
{
throw new InvalidOperationException("WScript.Shell is not available.");
}
var shell = Activator.CreateInstance(shellType);
var shortcut = shellType.InvokeMember("CreateShortcut", BindingFlags.InvokeMethod, null, shell, new object[] { path });
var shortcutType = shortcut.GetType();
shortcutType.InvokeMember("TargetPath", BindingFlags.SetProperty, null, shortcut, new object[] { Application.ExecutablePath });
shortcutType.InvokeMember("WorkingDirectory", BindingFlags.SetProperty, null, shortcut, new object[] { Application.StartupPath });
shortcutType.InvokeMember("Description", BindingFlags.SetProperty, null, shortcut, new object[] { "Add text files to Clipman history" });
shortcutType.InvokeMember("Save", BindingFlags.InvokeMethod, null, shortcut, null);
}
else if (File.Exists(path))
{
File.Delete(path);
DeleteShortcutIfExists(OldSendToShortcutName);
}
}
private static string GetSendToPath()
{
var sendToFolder = Environment.GetFolderPath(Environment.SpecialFolder.SendTo);
if (string.IsNullOrWhiteSpace(sendToFolder))
{
throw new InvalidOperationException("The Windows Send To folder could not be found.");
}
Directory.CreateDirectory(sendToFolder);
return Path.Combine(sendToFolder, SendToShortcutName);
}
private static void DeleteShortcutIfExists(string shortcutName)
{
var sendToFolder = Environment.GetFolderPath(Environment.SpecialFolder.SendTo);
if (string.IsNullOrWhiteSpace(sendToFolder)) return;
var path = Path.Combine(sendToFolder, shortcutName);
if (File.Exists(path))
{
File.Delete(path);
}
}
}
}