Functions/GenXdev.Windows/Start-ProcessElevated.cs

// ################################################################################
// Part of PowerShell module : GenXdev.Windows
// Original cmdlet filename : Start-ProcessElevated.cs
// Original author : René Vaessen / GenXdev
// Version : 3.29.2026
// ################################################################################
// Copyright (c) 2026 René Vaessen / GenXdev
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ################################################################################



using System.Collections;
using System.Diagnostics;
using System.Management.Automation;
using System.Text;

namespace GenXdev.Windows
{
    [System.ComponentModel.Description(@"
.SYNOPSIS
Starts a process with elevated administrator privileges.

.DESCRIPTION
Wraps the built-in Start-Process cmdlet with automatic elevation. When the
current session is not elevated, the command is delegated to a persistent
elevated helper process via named pipe, avoiding repeated UAC prompts after
the first elevation. When already elevated, delegates directly to
Start-Process.

All parameters except -Verb and -Credential are inherited from Start-Process.
The -Verb parameter is forced to 'RunAs'.
.PARAMETER Elevated
When true (default), the process is started with elevated privileges. Set to
false to delegate directly to Start-Process without elevation.
.EXAMPLE
Start-ProcessElevated -FilePath 'notepad.exe'
Launches Notepad with administrator privileges.
.EXAMPLE
Start-ProcessElevated -FilePath 'winget.exe' -ArgumentList 'install',
'SomePackage' -Wait
Installs a package via winget with admin rights and waits for completion.
")]
    [Cmdlet(VerbsLifecycle.Start, "ProcessElevated")]
    [OutputType(typeof(System.Diagnostics.Process))]
    public class StartProcessElevatedCommand : PSGenXdevCmdlet
    {
        /// <summary>
        /// Path to the executable file. Identical to Start-Process -FilePath.
        /// </summary>
        [Alias("PSPath", "Path")]
        [Parameter(
            Mandatory = true,
            Position = 0,
            HelpMessage = "Path to the executable file")]
        [ValidateNotNullOrEmpty]
        public string FilePath { get; set; }

        /// <summary>
        /// Arguments to pass to the executable. Identical to Start-Process
        /// -ArgumentList.
        /// </summary>
        [Alias("Args")]
        [Parameter(
            Mandatory = false,
            Position = 1,
            HelpMessage = "Arguments to pass to the executable")]
        public string[] ArgumentList { get; set; } = Array.Empty<string>();

        /// <summary>
        /// Working directory for the new process. Identical to Start-Process
        /// -WorkingDirectory.
        /// </summary>
        [Parameter(
            Mandatory = false,
            HelpMessage = "Working directory for the new process")]
        public string WorkingDirectory { get; set; }

        /// <summary>
        /// Wait for the process to exit before continuing. Identical to
        /// Start-Process -Wait.
        /// </summary>
        [Parameter(
            Mandatory = false,
            HelpMessage = "Wait for the process to exit")]
        public SwitchParameter Wait { get; set; }

        /// <summary>
        /// Return the process object to the pipeline. Identical to
        /// Start-Process -PassThru.
        /// </summary>
        [Parameter(
            Mandatory = false,
            HelpMessage = "Return the process object")]
        public SwitchParameter PassThru { get; set; }

        /// <summary>
        /// Run the process in the current console window. Identical to
        /// Start-Process -NoNewWindow.
        /// </summary>
        [Alias("nnw")]
        [Parameter(
            Mandatory = false,
            HelpMessage = "Run in the current console window")]
        public SwitchParameter NoNewWindow { get; set; }

        /// <summary>
        /// Window style for the new process. Identical to Start-Process
        /// -WindowStyle.
        /// </summary>
        [Parameter(
            Mandatory = false,
            HelpMessage = "Window style for the new process")]
        [ValidateSet(
            "Normal", "Hidden", "Minimized", "Maximized")]
        public string WindowStyle { get; set; }

        /// <summary>
        /// Environment variables for the new process. Identical to
        /// Start-Process -Environment.
        /// </summary>
        [Parameter(
            Mandatory = false,
            HelpMessage = "Environment variables for the new process")]
        public Hashtable Environment { get; set; }

        /// <summary>
        /// Redirect standard error to a file. Identical to Start-Process
        /// -RedirectStandardError.
        /// </summary>
        [Alias("RSE")]
        [Parameter(
            Mandatory = false,
            HelpMessage = "File to redirect standard error to")]
        public string RedirectStandardError { get; set; }

        /// <summary>
        /// Redirect standard input from a file. Identical to Start-Process
        /// -RedirectStandardInput.
        /// </summary>
        [Alias("RSI")]
        [Parameter(
            Mandatory = false,
            HelpMessage = "File to redirect standard input from")]
        public string RedirectStandardInput { get; set; }

        /// <summary>
        /// Redirect standard output to a file. Identical to Start-Process
        /// -RedirectStandardOutput.
        /// </summary>
        [Alias("RSO")]
        [Parameter(
            Mandatory = false,
            HelpMessage = "File to redirect standard output to")]
        public string RedirectStandardOutput { get; set; }

        /// <summary>
        /// Load the user's Windows profile. Identical to Start-Process
        /// -LoadUserProfile.
        /// </summary>
        [Alias("Lup")]
        [Parameter(
            Mandatory = false,
            HelpMessage = "Load the user's Windows profile")]
        public SwitchParameter LoadUserProfile { get; set; }

        /// <summary>
        /// Use a new environment for the process. Identical to Start-Process
        /// -UseNewEnvironment.
        /// </summary>
        [Parameter(
            Mandatory = false,
            HelpMessage = "Use a new environment for the process")]
        public SwitchParameter UseNewEnvironment { get; set; }

        /// <summary>
        /// When true (default), starts the process with elevated privileges
        /// via -Verb RunAs. Set to false for normal unelevated execution.
        /// </summary>
        [Parameter(
            Mandatory = false,
            HelpMessage = "Start with elevated privileges (default: true)")]
        public SwitchParameter Elevated { get; set; } =
            new SwitchParameter(true);

        // =================================================================
        // Processing
        // =================================================================

        /// <summary>
        /// Begin processing
        /// </summary>
        protected override void BeginProcessing()
        {
            WriteVerbose("Starting process with elevation");
        }

        /// <summary>
        /// Process record - main cmdlet logic
        /// </summary>
        protected override void ProcessRecord()
        {
            if (!ShouldProcess("Start process with elevation"))
            {
                return;
            }

            // Copy all parameters matching Start-Process
            var startParams = CopyIdenticalParamValues("Microsoft.PowerShell.Management\\Start-Process");
            // Force -Verb RunAs when elevating
            startParams["Verb"] = "RunAs";

            // Check if we're already elevated
            bool isElevated = CheckIfElevated();

            // Already elevated or elevation not requested:
            // delegate directly to Start-Process
            if (isElevated || !Elevated.ToBool())
            {
                if (!Elevated.ToBool())
                {
                    // Remove forced RunAs — user wants normal execution
                    startParams.Remove("Verb");
                }

                WriteVerbose(
                    "Delegating directly to Start-Process");

                var directScript = ScriptBlock.Create(
                    "param($P) Microsoft.PowerShell.Management\\Start-Process @P");
                var results2 = directScript.Invoke(startParams);

                if (PassThru.ToBool() && results2.Count > 0)
                {
                    foreach (var r in results2)
                    {
                        WriteObject(r?.BaseObject);
                    }
                }
                return;
            }

            // Not elevated: send Start-Process command via pipe
            WriteVerbose(
                "Delegating to elevated helper via named pipe");

            // Serialize the hashtable to JSON for pipe transport
            string json = ConvertToJson(startParams);
            string encodedParams = Convert.ToBase64String(
                Encoding.UTF8.GetBytes(json));

            // Build the elevated ScriptBlock
            string scriptText =
                "$json=[System.Text.Encoding]::UTF8.GetString(" +
                "[System.Convert]::FromBase64String('" +
                encodedParams + "'));" +
                "$p=$json|ConvertFrom-Json -AsHashtable;" +
                "Start-Process @p";

            // Execute via the elevated helper pipe
            var invokeScript = ScriptBlock.Create(
                "param($S) GenXdev\\Invoke-CommandElevated " +
                "-ScriptBlock ([ScriptBlock]::Create($S))");
            var results = invokeScript.Invoke(scriptText);

            if (PassThru.ToBool() && results.Count > 0)
            {
                foreach (var r in results)
                {
                    // Try to resolve a PID into a Process object
                    if (r?.BaseObject is int pid ||
                        (r?.BaseObject is string s &&
                         int.TryParse(s, out pid)))
                    {
                        try
                        {
                            WriteObject(
                                Process.GetProcessById(pid));
                            continue;
                        }
                        catch { }
                    }
                    WriteObject(r?.BaseObject);
                }
            }
        }

        /// <summary>
        /// End processing
        /// </summary>
        protected override void EndProcessing()
        {
        }

        // =================================================================
        // Helpers
        // =================================================================

        /// <summary>
        /// Check if the current process is elevated
        /// </summary>
        private bool CheckIfElevated()
        {
            try
            {
                return InvokeScript<bool>(
                    "GenXdev\\CurrentUserHasElevatedRights");
            }
            catch
            {
                return false;
            }
        }
    }
}