//Used by Run and RunConsole. static string _NormalizeFile(bool runConsole, string file, out bool isFullPath, out bool isShellPath) { isShellPath = isFullPath = false; file = APath.ExpandEnvVar(file); if (file.NE()) { throw new ArgumentException(); } if (runConsole || !(isShellPath = APath.IsShellPath_(file))) { if (isFullPath = APath.IsFullPath(file)) { var fl = runConsole ? PNFlags.DontExpandDosPath : PNFlags.DontExpandDosPath | PNFlags.DontPrefixLongPath; file = APath.Normalize_(file, fl, true); //ShellExecuteEx supports long path prefix for exe but not for documents. //Process.Run supports long path prefix, except when the exe is .NET. if (!runConsole) { file = APath.UnprefixLongPath(file); } if (ADisableFsRedirection.IsSystem64PathIn32BitProcess(file) && !AFile.ExistsAsAny(file)) { file = ADisableFsRedirection.GetNonRedirectedSystemPath(file); } } else if (!APath.IsUrl(file)) { //ShellExecuteEx searches everywhere except in app folder. //Process.Run prefers current directory. var s2 = AFile.SearchPath(file); if (s2 != null) { file = s2; isFullPath = true; } } } return(file); }
/// <summary> /// Runs/opens a program, document, directory (folder), URL, new email, Control Panel item etc. /// The returned <see cref="RResult"/> variable contains some process info - process id etc. /// </summary> /// <param name="file"> /// Examples: /// - <c>@"C:\file.txt"</c> /// - <c>AFolders.Documents</c> /// - <c>AFolders.System + "notepad.exe"</c> /// - <c>@"%AFolders.System%\notepad.exe"</c> /// - <c>@"%TMP%\file.txt"</c> /// - <c>"notepad.exe"</c> /// - <c>@"..\folder\x.exe"</c> /// - <c>"http://a.b.c/d"</c> /// - <c>"file:///path"</c> /// - <c>"mailto:[email protected]"</c> /// - <c>":: ITEMIDLIST"</c> /// - <c>@"::{CLSID}"</c> /// - <c>@"shell:AppsFolder\Microsoft.WindowsCalculator_8wekyb3d8bbwe!App"</c>. /// </param> /// <param name="args"> /// Command line arguments. /// This function expands environment variables if starts with <c>"%"</c> or <c>"\"%"</c>. /// </param> /// <param name="flags"></param> /// <param name="curDirEtc"> /// Allows to specify more parameters: current directory, verb, etc. /// If string, it sets initial current directory for the new process. If "", gets it from <i>file</i>. More info: <see cref="ROptions.CurrentDirectory"/>. /// </param> /// <exception cref="ArgumentException">Used both ROptions.Verb and RFlags.Admin.</exception> /// <exception cref="AuException">Failed. For example, the file does not exist.</exception> /// <remarks> /// It works like when you double-click a file icon. It may start new process or not. For example it may just activate window if the program is already running. /// Uses API <msdn>ShellExecuteEx</msdn>. /// Similar to <see cref="Process.Start(string, string)"/>. /// /// The <i>file</i> parameter can be: /// - Full path of a file or directory. Examples: <c>@"C:\file.txt"</c>, <c>AFolders.Documents</c>, <c>AFolders.System + "notepad.exe"</c>, <c>@"%AFolders.System%\notepad.exe"</c>. /// - Filename of a file or directory, like <c>"notepad.exe"</c>. The function calls <see cref="AFile.SearchPath"/>. /// - Path relative to <see cref="AFolders.ThisApp"/>. Examples: <c>"x.exe"</c>, <c>@"subfolder\x.exe"</c>, <c>@".\subfolder\x.exe"</c>, <c>@"..\another folder\x.exe"</c>. /// - URL. Examples: <c>"http://a.b.c/d"</c>, <c>"file:///path"</c>. /// - Email, like <c>"mailto:[email protected]"</c>. Subject, body etc also can be specified, and Google knows how. /// - Shell object's ITEMIDLIST like <c>":: ITEMIDLIST"</c>. See <see cref="APidl.ToBase64String"/>, <see cref="AFolders.Virtual"/>. Can be used to open virtual folders and items like Control Panel. /// - Shell object's parsing name, like <c>@"::{CLSID}"</c>. See <see cref="APidl.ToShellString"/>, <see cref="AFolders.VirtualPidl"/>. Can be used to open virtual folders and items like Control Panel. /// - To run a Windows Store App, use <c>@"shell:AppsFolder\WinStoreAppId"</c> format. Examples: <c>@"shell:AppsFolder\Microsoft.WindowsCalculator_8wekyb3d8bbwe!App"</c>, <c>@"shell:AppsFolder\windows.immersivecontrolpanel_cw5n1h2txyewy!microsoft.windows.immersivecontrolpanel"</c>. To discover the string use <see cref="AWnd.More.GetWindowsStoreAppId"/> or Google. /// /// Supports environment variables, like <c>@"%TMP%\file.txt"</c>. See <see cref="APath.ExpandEnvVar"/>. /// </remarks> /// <seealso cref="AWnd.FindOrRun"/> /// <example> /// Run notepad and wait for its window. /// <code><![CDATA[ /// AExec.Run("notepad.exe"); /// AWnd w = AWnd.Wait(10, true, "*- Notepad", "Notepad"); /// ]]></code> /// Run notepad or activate its window. /// <code><![CDATA[ /// AWnd w = AWnd.FindOrRun("*- Notepad", run: () => AExec.Run("notepad.exe")); /// ]]></code> /// </example> public static RResult Run(string file, string args = null, RFlags flags = 0, ROptions curDirEtc = null) { //SHOULDDO: from UAC IL admin run as user by default. Add flag UacInherit. Api.SHELLEXECUTEINFO x = default; x.cbSize = Api.SizeOf(x); x.fMask = Api.SEE_MASK_NOZONECHECKS | Api.SEE_MASK_NOASYNC | Api.SEE_MASK_NOCLOSEPROCESS | Api.SEE_MASK_CONNECTNETDRV | Api.SEE_MASK_UNICODE; x.nShow = Api.SW_SHOWNORMAL; bool curDirFromFile = false; var more = curDirEtc; if (more != null) { x.lpVerb = more.Verb; var cd = more.CurrentDirectory; if (cd != null) { if (cd.Length == 0) { curDirFromFile = true; } else { cd = APath.ExpandEnvVar(cd); } } x.lpDirectory = cd; if (!more.OwnerWindow.IsEmpty) { x.hwnd = more.OwnerWindow.Wnd.Window; } switch (more.WindowState) { case ProcessWindowStyle.Hidden: x.nShow = Api.SW_HIDE; break; case ProcessWindowStyle.Minimized: x.nShow = Api.SW_SHOWMINIMIZED; break; case ProcessWindowStyle.Maximized: x.nShow = Api.SW_SHOWMAXIMIZED; break; } } if (flags.Has(RFlags.Admin)) { if (more?.Verb != null && !more.Verb.Eqi("runas")) { throw new ArgumentException("Cannot use Verb with flag Admin"); } x.lpVerb = "runas"; } else if (x.lpVerb != null) { x.fMask |= Api.SEE_MASK_INVOKEIDLIST; //makes slower. But verbs are rarely used. } if (0 == (flags & RFlags.ShowErrorUI)) { x.fMask |= Api.SEE_MASK_FLAG_NO_UI; } if (0 == (flags & RFlags.WaitForExit)) { x.fMask |= Api.SEE_MASK_NO_CONSOLE; } file = _NormalizeFile(false, file, out bool isFullPath, out bool isShellPath); APidl pidl = null; if (isShellPath) //":: Base64ITEMIDLIST" or "::{CLSID}..." (we convert it too because the API does not support many) { pidl = APidl.FromString(file); //does not throw if (pidl != null) { x.lpIDList = pidl.UnsafePtr; x.fMask |= Api.SEE_MASK_INVOKEIDLIST; } else { x.lpFile = file; } } else { x.lpFile = file; if (curDirFromFile && isFullPath) { x.lpDirectory = APath.GetDirectoryPath(file); } } if (!args.NE()) { x.lpParameters = APath.ExpandEnvVar(args); } AWnd.More.EnableActivate(); if (!Api.ShellExecuteEx(ref x)) { throw new AuException(0, $"*run '{file}'"); } pidl?.Dispose(); var R = new RResult(); bool waitForExit = 0 != (flags & RFlags.WaitForExit); bool needHandle = flags.Has(RFlags.NeedProcessHandle); WaitHandle_ ph = null; if (!x.hProcess.Is0) { if (waitForExit || needHandle) { ph = new WaitHandle_(x.hProcess, true); } if (!waitForExit) { R.ProcessId = AProcess.ProcessIdFromHandle(x.hProcess); } } try { Api.AllowSetForegroundWindow(Api.ASFW_ANY); if (x.lpVerb != null && Thread.CurrentThread.GetApartmentState() == ApartmentState.STA) { Thread.CurrentThread.Join(50); //need min 5-10 for file Properties. And not Sleep. } if (ph != null) { if (waitForExit) { ph.WaitOne(); if (Api.GetExitCodeProcess(x.hProcess, out var ec)) { R.ProcessExitCode = ec; } } if (needHandle) { R.ProcessHandle = ph; } } } finally { if (R.ProcessHandle == null) { if (ph != null) { ph.Dispose(); } else { x.hProcess.Dispose(); } } } return(R); //tested: works well in MTA thread. //rejected: in QM2, run also has a 'window' parameter. However it just makes limited, unclear etc, and therefore rarely used. Instead use AWnd.FindOrRun or Find/Run/Wait like in the examples. //rejected: in QM2, run also has 'autodelay'. Better don't add such hidden things. Let the script decide what to do. }