using System; using System.IO; using Microsoft.Win32; namespace MergeHub.Services { /// /// 탐색기에서 이 프로그램을 부를 수 있게 한다 - 보내기(SendTo) 메뉴에 바로가기를 둔다. /// /// 처음에는 우클릭 메뉴에 바로 넣으려고 레지스트리에 정적 verb 를 등록했다. /// (HKCU\Software\Classes\AllFilesystemObjects\shell\...) /// 이 PC 에서는 뜨지 않았다. 확인해 보니 우클릭 메뉴에 나오는 다른 프로그램 /// (Notepad++ · Git Bash · WinMerge)은 전부 HKLM(전체 사용자)에 등록돼 있었고, /// HKCU 에 넣은 항목은 - 우리 것이든 실험용으로 넣은 것이든 - 하나도 그려지지 않았다. /// HKLM 은 관리자 권한이 필요해 쓸 수 없다. /// /// 그래서 보내기로 간다. 이쪽은 레지스트리를 아예 쓰지 않고 /// %APPDATA%\Microsoft\Windows\SendTo 에 바로가기 파일 하나를 두는 것이 전부라 /// 권한도 필요 없고 윈도우 판을 타지 않는다. 덤이 하나 있는데, 여러 개를 골라 /// 한 번에 보낼 수 있다 - 파일 두 개를 골라 바로 비교하는 것이 그냥 된다. /// /// 단점은 한 단계 더 들어간다는 것뿐이다(우클릭 → 보내기 → MergeHub 로 비교). /// public static class ShellIntegration { /// 보내기 메뉴에 보일 이름. public const string MenuLabel = "MergeHub 로 비교"; private static string ShortcutPath => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.SendTo), MenuLabel + ".lnk"); public static bool IsRegistered { get { try { return File.Exists(ShortcutPath); } catch (IOException) { return false; } } } /// 보내기 메뉴에 바로가기를 만든다. 이미 있으면 경로만 새로 쓴다(프로그램을 옮겼을 때). public static void Register(string executablePath) { if (string.IsNullOrWhiteSpace(executablePath)) throw new ArgumentException("실행 파일 경로가 비었습니다."); var shell = Type.GetTypeFromProgID("WScript.Shell") ?? throw new InvalidOperationException("WScript.Shell 을 찾을 수 없습니다."); var host = Activator.CreateInstance(shell) ?? throw new InvalidOperationException("WScript.Shell 을 만들지 못했습니다."); // 바로가기(.lnk)는 COM 으로만 만들 수 있다. 참조를 더하지 않으려고 늦은 바인딩으로 부른다. var link = shell.InvokeMember("CreateShortcut", System.Reflection.BindingFlags.InvokeMethod, null, host, new object[] { ShortcutPath }) ?? throw new InvalidOperationException("바로가기를 만들지 못했습니다."); var type = link.GetType(); Set(type, link, "TargetPath", executablePath); Set(type, link, "WorkingDirectory", Path.GetDirectoryName(executablePath) ?? string.Empty); Set(type, link, "Description", "고른 파일·폴더를 MergeHub 으로 비교합니다"); type.InvokeMember("Save", System.Reflection.BindingFlags.InvokeMethod, null, link, null); // 예전 판이 레지스트리에 써 둔 것이 남아 있으면 같이 치운다. RemoveLegacyRegistryEntries(); } /// /// 등록해 둔 바로가기가 옛 자리를 가리키면 지금 실행 파일로 고친다(11차 F-3, 사용자 결정 "켜질 때 저절로"). /// 배포 묶음을 다른 폴더로 옮기면 바로가기가 없는 파일을 가리켜 보내기가 동작하지 않았다. /// /// 등록하지 않은 사람은 건드리지 않는다. 바로가기 파일만 다시 쓰고 레지스트리는 손대지 않는다 /// ( 의 옛 흔적 치우기도 부르지 않는다). /// /// 고쳤으면 옛 대상, 아니면 null. public static string? RepairIfMoved(string executablePath) { if (!IsRegistered || string.IsNullOrWhiteSpace(executablePath)) return null; var shell = Type.GetTypeFromProgID("WScript.Shell"); if (shell is null) return null; var host = Activator.CreateInstance(shell); if (host is null) return null; var link = shell.InvokeMember("CreateShortcut", System.Reflection.BindingFlags.InvokeMethod, null, host, new object[] { ShortcutPath }); if (link is null) return null; var type = link.GetType(); var old = type.InvokeMember("TargetPath", System.Reflection.BindingFlags.GetProperty, null, link, null) as string ?? string.Empty; if (!PointsElsewhere(old, executablePath)) return null; Set(type, link, "TargetPath", executablePath); Set(type, link, "WorkingDirectory", Path.GetDirectoryName(executablePath) ?? string.Empty); type.InvokeMember("Save", System.Reflection.BindingFlags.InvokeMethod, null, link, null); return old; } /// 바로가기 대상이 지금 실행 파일과 다른 곳인지(대소문자 · 경로 모양은 가리지 않음). public static bool PointsElsewhere(string? linkTarget, string? currentExecutable) { if (string.IsNullOrWhiteSpace(currentExecutable)) return false; if (string.IsNullOrWhiteSpace(linkTarget)) return true; try { return !string.Equals(Path.GetFullPath(linkTarget!.Trim()), Path.GetFullPath(currentExecutable!.Trim()), StringComparison.OrdinalIgnoreCase); } catch (Exception e) when (e is ArgumentException || e is NotSupportedException || e is PathTooLongException) { return true; } } private static void Set(Type type, object link, string name, string value) => type.InvokeMember(name, System.Reflection.BindingFlags.SetProperty, null, link, new object[] { value }); public static void Unregister() { try { if (File.Exists(ShortcutPath)) File.Delete(ShortcutPath); } catch (IOException) { // 지우지 못하면 그대로 둔다. 다시 누르면 된다. } RemoveLegacyRegistryEntries(); } /// /// 레지스트리로 등록하던 시절의 흔적. 이 PC 에서는 뜨지도 않던 것이라 남겨 둘 이유가 없다. /// private static void RemoveLegacyRegistryEntries() { string[] legacy = { @"Software\Classes\AllFilesystemObjects\shell\MergeHub.Compare", @"Software\Classes\Directory\Background\shell\MergeHub.Compare", @"Software\Classes\*\shell\MergeHub.Compare", @"Software\Classes\Directory\shell\MergeHub.Compare", }; foreach (var path in legacy) { try { Registry.CurrentUser.DeleteSubKeyTree(path, throwOnMissingSubKey: false); } catch (Exception e) when (e is ArgumentException || e is UnauthorizedAccessException) { // 이미 없거나 손댈 수 없으면 그만이다. } } } /// 지금 돌고 있는 실행 파일. net48 에는 Environment.ProcessPath 가 없다. public static string CurrentExecutable() { var path = System.Diagnostics.Process.GetCurrentProcess().MainModule?.FileName; return string.IsNullOrEmpty(path) ? Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "UbisamBase.Shell.exe") : path!; } } }