Functions/GenXdev.Helpers/Test-UnattendedMode.cs
|
// ################################################################################
// Part of PowerShell module : GenXdev.Helpers // Original cmdlet filename : Test-UnattendedMode.cs // Original author : René Vaessen / GenXdev // Version : 3.26.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.Management.Automation; namespace GenXdev.Helpers { [System.ComponentModel.Description(@" .SYNOPSIS Detects if PowerShell is running in unattended/automated mode. .DESCRIPTION * Analyzes various indicators to determine if PowerShell is running in an unattended or automated context. * Checks pipeline analysis, environment variables, console redirection, and invocation context. * When CallersInvocation is provided, analyzes the pipeline position and count to detect automated pipeline or script execution. .EXAMPLE ```powershell Test-UnattendedMode ``` Returns a boolean indicating if running in unattended mode. .EXAMPLE ```powershell Test-UnattendedMode -CallersInvocation $MyInvocation ``` Analyzes the caller's invocation context and returns a boolean. .EXAMPLE ```powershell Test-UnattendedMode -CallersInvocation $MyInvocation -Detailed ``` Returns detailed analysis object with all indicators. .EXAMPLE ```powershell function My-Function { $isUnattended = Test-UnattendedMode -CallersInvocation $MyInvocation if ($isUnattended) { Write-Verbose ""Running in unattended mode"" } } ``` Example usage in a function to check for unattended mode. ")] [Cmdlet(VerbsDiagnostic.Test, "UnattendedMode")] [OutputType(typeof(bool), typeof(PSObject))] public partial class TestUnattendedModeCommand : PSGenXdevCmdlet { /// <summary> /// The caller's invocation information for pipeline and automation detection /// </summary> [Parameter( Mandatory = false, Position = 0, HelpMessage = "Caller's invocation info for pipeline and automation detection" )] public InvocationInfo CallersInvocation { get; set; } /// <summary> /// Return detailed analysis object instead of simple boolean /// </summary> [Parameter( Mandatory = false, HelpMessage = "Return detailed analysis object instead of simple boolean" )] public SwitchParameter Detailed { get; set; } /// <summary> /// Process record - main cmdlet logic /// </summary> protected override void ProcessRecord() { // Initialize list for unattended indicators var unattendedIndicators = new List<string>(); // Check 1: Environment variables indicating CI/CD or automation var automationEnvVars = new[] { "JENKINS_URL", "GITHUB_ACTIONS", "TF_BUILD", "CI", "BUILD_ID", "RUNNER_OS", "SYSTEM_TEAMPROJECT", "TEAMCITY_VERSION", "TRAVIS", "APPVEYOR", "CIRCLECI", "GITLAB_CI", "AZURE_PIPELINES" }; // Check if any automation environment variables are set var hasAutomationEnv = automationEnvVars.Any(envVar => !string.IsNullOrEmpty(System.Environment.GetEnvironmentVariable(envVar))); if (hasAutomationEnv) { unattendedIndicators.Add("AutomationEnvironment"); } // Check 2: Console redirection var hasRedirection = false; try { hasRedirection = System.Console.IsInputRedirected || System.Console.IsOutputRedirected; } catch { // If checking redirection fails, assume no redirection } if (hasRedirection) { unattendedIndicators.Add("ConsoleRedirection"); } // Check 3: Non-interactive environment var isNonInteractive = false; try { // Check if we're running in a service or without a window station isNonInteractive = string.IsNullOrEmpty(System.Environment.GetEnvironmentVariable("SESSIONNAME")) && string.IsNullOrEmpty(System.Environment.GetEnvironmentVariable("USERDOMAIN")); } catch { // If check fails, assume interactive } if (isNonInteractive) { unattendedIndicators.Add("NonInteractiveEnvironment"); } // Check 4: PowerShell host indicators var automationHosts = new[] { "ServerRemoteHost", "Default Host", "BackgroundHost" }; // Get host name via PowerShell var hostName = (string)this.InvokeScript<string>("$Host.Name"); var isAutomationHost = automationHosts.Contains(hostName); if (isAutomationHost) { unattendedIndicators.Add($"AutomationHost:{hostName}"); } // Check 5: No console window (for GUI apps calling PowerShell) var hasNoConsole = false; try { hasNoConsole = System.Console.WindowWidth == 0; } catch { hasNoConsole = true; } if (hasNoConsole) { unattendedIndicators.Add("NoConsoleWindow"); } // Check 6: PowerShell execution parameters var hasNonInteractiveParam = false; try { // Check if NonInteractive was passed hasNonInteractiveParam = this.MyInvocation.BoundParameters.ContainsKey("NonInteractive") || (bool)this.InvokeScript<bool>( "$PSBoundParameters.NonInteractive -or " + "(Get-Variable -Name PSBoundParameters -Scope 1 " + "-ErrorAction SilentlyContinue -ValueOnly).NonInteractive"); } catch { // If check fails, assume no non-interactive parameter } if (hasNonInteractiveParam) { unattendedIndicators.Add("NonInteractiveParameter"); } // Check 7: Pipeline analysis (if CallersInvocation provided) int? pipelinePosition = null; int? pipelineLength = null; string scriptName = null; string commandLine = null; if (this.CallersInvocation != null) { pipelinePosition = this.CallersInvocation.PipelinePosition; pipelineLength = this.CallersInvocation.PipelineLength; scriptName = this.CallersInvocation.ScriptName; commandLine = this.CallersInvocation.Line; // If we're in a multi-command pipeline (not just a single command) var isInPipeline = pipelineLength > 1; // If we're not at the end of the pipeline (suggesting automated processing) var isNotPipelineEnd = pipelinePosition < pipelineLength; // Check if called from a script file (not interactive) var isFromScript = !string.IsNullOrEmpty(scriptName); // Check command line context var isAutomatedCommand = !string.IsNullOrEmpty(commandLine) && (System.Text.RegularExpressions.Regex.IsMatch(commandLine, @"^\s*(foreach|%|\||;|&)") || System.Text.RegularExpressions.Regex.IsMatch(commandLine, @"(Get-|Set-|Invoke-|Start-|Stop-).+\|") || System.Text.RegularExpressions.Regex.IsMatch(commandLine, @"\$\w+\s*\|\s*")); // Only flag as unattended if we have strong indicators // Being in a simple function call from console should not count as unattended var isInteractiveFunction = hostName == "ConsoleHost" && pipelineLength == 1 && string.IsNullOrEmpty(scriptName); if (isInPipeline && !isInteractiveFunction) { unattendedIndicators.Add($"MultiCommandPipeline:{pipelinePosition}/{pipelineLength}"); } if (isNotPipelineEnd && !isInteractiveFunction) { unattendedIndicators.Add("NotPipelineEnd"); } if (isAutomatedCommand && !isInteractiveFunction) { unattendedIndicators.Add("AutomatedCommandPattern"); } } // Final determination var isUnattended = unattendedIndicators.Count > 0; // Return detailed object or simple boolean if (this.Detailed.ToBool()) { var result = new PSObject(); result.Properties.Add(new PSNoteProperty("IsUnattended", isUnattended)); result.Properties.Add(new PSNoteProperty("Indicators", unattendedIndicators.ToArray())); result.Properties.Add(new PSNoteProperty("IndicatorCount", unattendedIndicators.Count)); result.Properties.Add(new PSNoteProperty("HostName", hostName)); result.Properties.Add(new PSNoteProperty("PipelinePosition", pipelinePosition)); result.Properties.Add(new PSNoteProperty("PipelineLength", pipelineLength)); result.Properties.Add(new PSNoteProperty("ScriptName", scriptName)); result.Properties.Add(new PSNoteProperty("CommandLine", commandLine)); this.WriteObject(result); } else { // Return simple boolean by default this.WriteObject(isUnattended); } } } } |