using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Drawing; using System.Globalization; using System.IO; using System.Runtime.InteropServices; using System.Security; using System.Text; using System.Threading; using System.Windows.Forms; namespace PaintDotNet.SystemLayer { public static class Shell { public static bool ReplaceMissingFiles(string[] missingFiles) { StringBuilder missingFilesSB = new StringBuilder(); for (int i = 0; i < missingFiles.Length; ++i) { missingFilesSB.Append(missingFiles[i]); if (i != missingFiles.Length - 1) { missingFilesSB.Append(", "); } } try { // If they are not an admin and have no possibility of elevating, such as for a standard User // in XP, then give them an error. Unfortunately we do not know if we can even load text // resources at this point, and so must provide an English-only error message. if (!Security.IsAdministrator && !Security.CanElevateToAdministrator) { MessageBox.Show( null, "Metis Vision has detected that some important installation files are missing. Repairing " + "this requires administrator privilege. Please run the 'PdnRepair.exe' program in the installation " + "directory after logging in with a user that has administrator privilege." + Environment.NewLine + Environment.NewLine + "The missing files are: " + missingFilesSB.ToString(), "Metis Vision", MessageBoxButtons.OK, MessageBoxIcon.Error); return false; } const int hMargin = 8; const int vMargin = 8; Form form = new Form(); form.Text = "Metis Vision"; form.ClientSize = new Size(400, 10); form.StartPosition = FormStartPosition.CenterScreen; Label infoLabel = new Label(); form.Controls.Add(infoLabel); infoLabel.Text = "Metis Vision has detected that some important installation files are missing. If you click " + "the Repair button it will attempt to repair this and then continue loading." + Environment.NewLine + Environment.NewLine + "The missing files are: " + missingFilesSB.ToString(); #if DEBUG infoLabel.Text += Environment.NewLine + Environment.NewLine + "*** Since this is a DEBUG build, you should probably add /skipRepairAttempt to the command-line."; #endif infoLabel.Location = new Point(hMargin, vMargin); infoLabel.Width = form.ClientSize.Width - hMargin * 2; infoLabel.Height = infoLabel.GetPreferredSize(new Size(infoLabel.Width, 1)).Height; Button repairButton = new Button(); form.Controls.Add(repairButton); repairButton.Text = "&Repair"; Exception exception = null; repairButton.Click += delegate (object sender, EventArgs e) { form.DialogResult = DialogResult.Yes; repairButton.Enabled = false; try { Shell.Execute(form, "PdnRepair.exe", "/noPause", ExecutePrivilege.AsInvokerOrAsManifest, ExecuteWaitType.WaitForExit); } catch (Exception ex) { exception = ex; } }; repairButton.AutoSize = true; repairButton.PerformLayout(); repairButton.Width += 20; repairButton.Location = new Point((form.ClientSize.Width - repairButton.Width) / 2, infoLabel.Bottom + vMargin * 2); repairButton.FlatStyle = FlatStyle.System; UI.EnableShield(repairButton, true); form.FormBorderStyle = FormBorderStyle.FixedDialog; form.MinimizeBox = false; form.MaximizeBox = false; form.ShowInTaskbar = true; form.Icon = null; form.ClientSize = new Size(form.ClientRectangle.Width, repairButton.Bottom + vMargin); DialogResult result = form.ShowDialog(null); form.Dispose(); form = null; if (result == DialogResult.Yes) { return true; } else if (exception == null) { return false; } else { throw new Exception("", exception); // ("Error while attempting to repair", exception); } } catch (Exception ex) { throw new Exception("", ex); // //("Could not repair installation after it was determined that the following files are missing: " + //missingFilesSB.ToString(), ex); } } /// /// Opens the requested directory in the shell's file/folder browser. /// /// The window that is currently in the foreground. /// The folder to open. /// /// This UI is presented modelessly, in another process, and in the foreground. /// Error handling and messaging (error dialogs) will be handled by the shell, /// and these errors will not be communicated to the caller of this method. /// public static void BrowseFolder(IWin32Window parent, string folderPath) { NativeStructs.SHELLEXECUTEINFO sei = new NativeStructs.SHELLEXECUTEINFO(); sei.cbSize = (uint)Marshal.SizeOf(typeof(NativeStructs.SHELLEXECUTEINFO)); sei.fMask = NativeConstants.SEE_MASK_NO_CONSOLE; sei.lpVerb = "open"; sei.lpFile = folderPath; sei.nShow = NativeConstants.SW_SHOWNORMAL; sei.hwnd = parent.Handle; bool bResult = NativeMethods.ShellExecuteExW(ref sei); if (bResult) { if (sei.hProcess != IntPtr.Zero) { SafeNativeMethods.CloseHandle(sei.hProcess); sei.hProcess = IntPtr.Zero; } } else { NativeMethods.ThrowOnWin32Error("ShellExecuteW returned FALSE"); } GC.KeepAlive(parent); } #if false [Obsolete("Do not use this method.", true)] public static void Execute( IWin32Window parent, string exePath, string args, bool requireAdmin) { Execute(parent, exePath, args, requireAdmin ? ExecutePrivilege.RequireAdmin : ExecutePrivilege.AsInvokerOrAsManifest, ExecuteWaitType.ReturnImmediately); } #endif private const string updateExeFileName = "UpdateMonitor.exe"; private delegate int ExecuteHandOff(IWin32Window parent, string exePath, string args, out IntPtr hProcess); /// /// Uses the shell to execute the command. /// and not by plugins. /// /// /// The window that is currently in the foreground. This may be null if requireAdmin /// is false and the executable that exePath refers to is not marked (e.g. via a /// manifest) as requiring administrator privilege. /// /// /// The path to the executable to launch. /// /// /// The command-line arguments for the executable. /// /// /// The privileges to execute the new process with. /// If the executable is already marked as requiring administrator privilege /// (e.g. via a "requiresAdministrator" UAC manifest), this parameter should be /// set to AsInvokerOrAsManifest. /// /// /// If administrator privilege is required, a consent UI may be displayed asking the /// user to approve the action. A parent window must be provided in this case so that /// the consent UI will know where to position itself. Administrator privilege is /// required if execPrivilege is set to RequireAdmin, or if the executable being launched /// has a manifest declaring that it requires this privilege and if the operating /// system recognizes the manifest. /// /// /// execPrivilege was RequireAdmin, but parent was null. /// /// /// execPrivilege was RequireAdmin, but the user does not have this privilege, nor do they /// have the ability to acquire or elevate to obtain this privilege. /// /// /// There was an error launching the program. /// public static void Execute( IWin32Window parent, string exePath, string args, ExecutePrivilege execPrivilege, ExecuteWaitType execWaitType) { if (exePath == null) { throw new ArgumentNullException("exePath"); } if (execPrivilege == ExecutePrivilege.RequireAdmin && parent == null) { throw new ArgumentException("If requireAdmin is true, a parent window must be provided"); } // If this action requires admin privilege, but the user does not have this // privilege and is not capable of acquiring this privilege, then we will // throw an exception. if (execPrivilege == ExecutePrivilege.RequireAdmin && !Security.IsAdministrator && !Security.CanElevateToAdministrator) { throw new SecurityException("Executable requires administrator privilege, but user is not an administrator and cannot elevate"); } ExecuteHandOff executeHandOff = null; switch (execPrivilege) { case ExecutePrivilege.AsInvokerOrAsManifest: executeHandOff = new ExecuteHandOff(ExecAsInvokerOrAsManifest); break; case ExecutePrivilege.RequireAdmin: executeHandOff = new ExecuteHandOff(ExecRequireAdmin); break; case ExecutePrivilege.RequireNonAdminIfPossible: if (Security.CanLaunchNonAdminProcess) { executeHandOff = new ExecuteHandOff(ExecRequireNonAdmin); } else { executeHandOff = new ExecuteHandOff(ExecAsInvokerOrAsManifest); } break; default: throw new InvalidEnumArgumentException("ExecutePrivilege"); } string updateMonitorExePath = null; if (execWaitType == ExecuteWaitType.RelaunchPdnOnExit) { RelaunchPdnHelperPart1(out updateMonitorExePath); } IntPtr hProcess = IntPtr.Zero; int nResult = executeHandOff(parent, exePath, args, out hProcess); if (nResult == NativeConstants.ERROR_SUCCESS) { if (execWaitType == ExecuteWaitType.WaitForExit) { SafeNativeMethods.WaitForSingleObject(hProcess, NativeConstants.INFINITE); } else if (execWaitType == ExecuteWaitType.RelaunchPdnOnExit) { bool bResult2 = SafeNativeMethods.SetHandleInformation( hProcess, NativeConstants.HANDLE_FLAG_INHERIT, NativeConstants.HANDLE_FLAG_INHERIT); RelaunchPdnHelperPart2(updateMonitorExePath, hProcess); // Ensure that we don't close the process handle right away in the next few lines of code. // It must be inherited by the child process. Yes, this is technically a leak but we are // planning to terminate in just a moment anyway. hProcess = IntPtr.Zero; } else if (execWaitType == ExecuteWaitType.ReturnImmediately) { } if (hProcess != IntPtr.Zero) { SafeNativeMethods.CloseHandle(hProcess); hProcess = IntPtr.Zero; } } else { if (nResult == NativeConstants.ERROR_CANCELLED || nResult == NativeConstants.ERROR_TIMEOUT) { // no problem } else { NativeMethods.ThrowOnWin32Error("ExecuteHandoff failed", nResult); } if (updateMonitorExePath != null) { try { File.Delete(updateMonitorExePath); } catch (Exception) { } updateMonitorExePath = null; } } GC.KeepAlive(parent); } private static int ExecAsInvokerOrAsManifest(IWin32Window parent, string exePath, string args, out IntPtr hProcess) { return ExecShellExecuteEx(parent, exePath, args, null, out hProcess); } private static int ExecRequireAdmin(IWin32Window parent, string exePath, string args, out IntPtr hProcess) { const string runAs = "runas"; string verb; if (Security.IsAdministrator) { verb = null; } else { verb = runAs; } return ExecShellExecuteEx(parent, exePath, args, verb, out hProcess); } private static int ExecRequireNonAdmin(IWin32Window parent, string exePath, string args, out IntPtr hProcess) { int nError = NativeConstants.ERROR_SUCCESS; string commandLine = "\"" + exePath + "\"" + (args == null ? "" : (" " + args)); string dir; try { dir = Path.GetDirectoryName(exePath); } catch (Exception) { dir = null; } IntPtr hWndShell = IntPtr.Zero; IntPtr hShellProcess = IntPtr.Zero; IntPtr hShellProcessToken = IntPtr.Zero; IntPtr hTokenCopy = IntPtr.Zero; IntPtr bstrExePath = IntPtr.Zero; IntPtr bstrCommandLine = IntPtr.Zero; IntPtr bstrDir = IntPtr.Zero; NativeStructs.PROCESS_INFORMATION procInfo = new NativeStructs.PROCESS_INFORMATION(); try { hWndShell = SafeNativeMethods.FindWindowW("Progman", null); if (hWndShell == IntPtr.Zero) { NativeMethods.ThrowOnWin32Error("FindWindowW() returned NULL"); } uint dwPID; uint dwThreadId = SafeNativeMethods.GetWindowThreadProcessId(hWndShell, out dwPID); if (0 == dwPID) { NativeMethods.ThrowOnWin32Error("GetWindowThreadProcessId returned 0", NativeErrors.ERROR_FILE_NOT_FOUND); } hShellProcess = NativeMethods.OpenProcess(NativeConstants.PROCESS_QUERY_INFORMATION, false, dwPID); if (IntPtr.Zero == hShellProcess) { NativeMethods.ThrowOnWin32Error("OpenProcess() returned NULL"); } bool optResult = NativeMethods.OpenProcessToken( hShellProcess, NativeConstants.TOKEN_ASSIGN_PRIMARY | NativeConstants.TOKEN_DUPLICATE | NativeConstants.TOKEN_QUERY, out hShellProcessToken); if (!optResult) { NativeMethods.ThrowOnWin32Error("OpenProcessToken() returned FALSE"); } bool dteResult = NativeMethods.DuplicateTokenEx( hShellProcessToken, NativeConstants.MAXIMUM_ALLOWED, IntPtr.Zero, NativeConstants.SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, NativeConstants.TOKEN_TYPE.TokenPrimary, out hTokenCopy); if (!dteResult) { NativeMethods.ThrowOnWin32Error("DuplicateTokenEx() returned FALSE"); } bstrExePath = Marshal.StringToBSTR(exePath); bstrCommandLine = Marshal.StringToBSTR(commandLine); bstrDir = Marshal.StringToBSTR(dir); bool cpwtResult = NativeMethods.CreateProcessWithTokenW( hTokenCopy, 0, bstrExePath, bstrCommandLine, 0, IntPtr.Zero, bstrDir, IntPtr.Zero, out procInfo); if (cpwtResult) { hProcess = procInfo.hProcess; procInfo.hProcess = IntPtr.Zero; nError = NativeConstants.ERROR_SUCCESS; } else { hProcess = IntPtr.Zero; nError = Marshal.GetLastWin32Error(); } } catch (Win32Exception ex) { Tracing.Ping(ex.ToString()); nError = ex.ErrorCode; hProcess = IntPtr.Zero; } finally { if (bstrExePath != IntPtr.Zero) { Marshal.FreeBSTR(bstrExePath); bstrExePath = IntPtr.Zero; } if (bstrCommandLine != IntPtr.Zero) { Marshal.FreeBSTR(bstrCommandLine); bstrCommandLine = IntPtr.Zero; } if (bstrDir != IntPtr.Zero) { Marshal.FreeBSTR(bstrDir); bstrDir = IntPtr.Zero; } if (hShellProcess != IntPtr.Zero) { SafeNativeMethods.CloseHandle(hShellProcess); hShellProcess = IntPtr.Zero; } if (hShellProcessToken != IntPtr.Zero) { SafeNativeMethods.CloseHandle(hShellProcessToken); hShellProcessToken = IntPtr.Zero; } if (hTokenCopy != IntPtr.Zero) { SafeNativeMethods.CloseHandle(hTokenCopy); hTokenCopy = IntPtr.Zero; } if (procInfo.hThread != IntPtr.Zero) { SafeNativeMethods.CloseHandle(procInfo.hThread); procInfo.hThread = IntPtr.Zero; } if (procInfo.hProcess != IntPtr.Zero) { SafeNativeMethods.CloseHandle(procInfo.hProcess); procInfo.hProcess = IntPtr.Zero; } } return nError; } private static int ExecShellExecuteEx(IWin32Window parent, string exePath, string args, string verb, out IntPtr hProcess) { string dir; try { dir = Path.GetDirectoryName(exePath); } catch (Exception) { dir = null; } NativeStructs.SHELLEXECUTEINFO sei = new NativeStructs.SHELLEXECUTEINFO(); sei.cbSize = (uint)Marshal.SizeOf(typeof(NativeStructs.SHELLEXECUTEINFO)); sei.fMask = NativeConstants.SEE_MASK_NOCLOSEPROCESS | NativeConstants.SEE_MASK_NO_CONSOLE | NativeConstants.SEE_MASK_FLAG_DDEWAIT; sei.lpVerb = verb; sei.lpDirectory = dir; sei.lpFile = exePath; sei.lpParameters = args; sei.nShow = NativeConstants.SW_SHOWNORMAL; if (parent != null) { sei.hwnd = parent.Handle; } bool bResult = NativeMethods.ShellExecuteExW(ref sei); hProcess = sei.hProcess; sei.hProcess = IntPtr.Zero; int nResult = NativeConstants.ERROR_SUCCESS; if (!bResult) { nResult = Marshal.GetLastWin32Error(); } return nResult; } private static void RelaunchPdnHelperPart1(out string updateMonitorExePath) { string srcDir = Application.StartupPath; string srcPath = Path.Combine(srcDir, updateExeFileName); string srcPath2 = srcPath + ".config"; string dstDir = Environment.ExpandEnvironmentVariables(@"%TEMP%\PdnSetup"); string dstPath = Path.Combine(dstDir, updateExeFileName); string dstPath2 = dstPath + ".config"; if (!Directory.Exists(dstDir)) { Directory.CreateDirectory(dstDir); } File.Copy(srcPath, dstPath, true); File.Copy(srcPath2, dstPath2, true); updateMonitorExePath = dstPath; } private static void RelaunchPdnHelperPart2(string updateMonitorExePath, IntPtr hProcess) { string args = hProcess.ToInt64().ToString(CultureInfo.InstalledUICulture); ProcessStartInfo psi = new ProcessStartInfo(updateMonitorExePath, args); psi.UseShellExecute = false; psi.WindowStyle = ProcessWindowStyle.Hidden; Process process = Process.Start(psi); process.Dispose(); } /// /// 用默认浏览器打开Url /// /// url地址,最长长度512 public static bool LaunchUrl(IWin32Window owner, string url) { if (url.Length > 512) { throw new ArgumentOutOfRangeException("url.Length must be <= 512"); } bool success = false; string quotedUrl = "\"" + url + "\""; ExecutePrivilege executePrivilege; if (!Security.IsAdministrator || (Security.IsAdministrator && !Security.CanLaunchNonAdminProcess)) { executePrivilege = ExecutePrivilege.AsInvokerOrAsManifest; } else { executePrivilege = ExecutePrivilege.RequireNonAdminIfPossible; } // Method 1. Just launch the url, and hope that the shell figures out the association correctly. // This method will not work with ExecutePrivilege.RequireNonAdmin though. if (!success && executePrivilege != ExecutePrivilege.RequireNonAdminIfPossible) { try { Execute(owner, quotedUrl, null, executePrivilege, ExecuteWaitType.ReturnImmediately); success = true; } catch (Exception ex) { Tracing.Ping("Exception while using method 1 to launch url, " + quotedUrl + ", :" + ex.ToString()); success = false; } } // Method 2. Launch the url through explorer if (!success) { const string shellFileLoc = @"%WINDIR%\explorer.exe"; string shellExePath = "(n/a)"; try { shellExePath = Environment.ExpandEnvironmentVariables(shellFileLoc); Execute(owner, shellExePath, quotedUrl, executePrivilege, ExecuteWaitType.ReturnImmediately); success = true; } catch (Exception ex) { Tracing.Ping("Exception while using method 2 to launch url through '" + shellExePath + "', " + quotedUrl + ", : " + ex.ToString()); success = false; } } return success; } public static void AddToRecentDocumentsList(string fileName) { // Apparently SHAddToRecentDocs can block for a very long period of time when certain // conditions are met: so we just stick it on "the backburner." ThreadPool.QueueUserWorkItem(new WaitCallback(AddToRecentDocumentsListImpl), fileName); } private static void AddToRecentDocumentsListImpl(object fileNameObj) { string fileName = (string)fileNameObj; IntPtr bstrFileName = IntPtr.Zero; try { bstrFileName = Marshal.StringToBSTR(fileName); NativeMethods.SHAddToRecentDocs(NativeConstants.SHARD_PATHW, bstrFileName); } finally { if (bstrFileName != IntPtr.Zero) { Marshal.FreeBSTR(bstrFileName); bstrFileName = IntPtr.Zero; } } } private static T2 Map(T1 mapFrom, Pair[] mappings) { foreach (Pair mapping in mappings) { if (mapping.First.Equals(mapFrom)) { return mapping.Second; } } throw new KeyNotFoundException(); } private static string GetCSIDLPath(int csidl, bool tryCreateIfAbsent) { // First, try calling SHGetFolderPathW with the "CSIDL_FLAG_CREATE" flag. However, if it // returns an error then ignore it. We've had some crash logs with "access denied" coming // from this function. int csidlWithFlags = csidl | (tryCreateIfAbsent ? NativeConstants.CSIDL_FLAG_CREATE : 0); StringBuilder sbWithFlags = new StringBuilder(NativeConstants.MAX_PATH); Do.TryBool(() => NativeMethods.SHGetFolderPathW(IntPtr.Zero, csidlWithFlags, IntPtr.Zero, NativeConstants.SHGFP_TYPE_CURRENT, sbWithFlags)); StringBuilder sb = new StringBuilder(NativeConstants.MAX_PATH); NativeMethods.SHGetFolderPathW(IntPtr.Zero, csidl, IntPtr.Zero, NativeConstants.SHGFP_TYPE_CURRENT, sb); // If we get back something like 'Z:' then we need to put a backslash on it. // Otherwise other path-related functions will freak out. if (sb.Length == 2 && sb[1] == ':') { sb.Append(Path.DirectorySeparatorChar); } string path = sb.ToString(); return path; } private static readonly Pair[] pathMappings = new Pair[] { Pair.Create(VirtualFolderName.SystemProgramFiles, NativeConstants.CSIDL_PROGRAM_FILES), Pair.Create(VirtualFolderName.UserDesktop, NativeConstants.CSIDL_DESKTOP_DIRECTORY), Pair.Create(VirtualFolderName.UserDocuments, NativeConstants.CSIDL_PERSONAL), Pair.Create(VirtualFolderName.UserLocalAppData, NativeConstants.CSIDL_LOCAL_APPDATA), Pair.Create(VirtualFolderName.UserPictures, NativeConstants.CSIDL_MYPICTURES), Pair.Create(VirtualFolderName.UserRoamingAppData, NativeConstants.CSIDL_APPDATA) }; public static string GetVirtualPath(VirtualFolderName folderName, bool tryCreateIfAbsent) { try { int csidl = Map(folderName, pathMappings); string path = GetCSIDLPath(csidl, tryCreateIfAbsent); return path; } catch (KeyNotFoundException) { throw new InvalidEnumArgumentException("folderName", (int)folderName, typeof(VirtualFolderName)); } } } }