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 SupportedTextExtensions = new HashSet(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 SupportedExtensionlessTextFiles = new HashSet(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(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); } } } }