Team Ai
Datasetpublic

MegaBites-AI/Windows-powershell

sourceHugging Facemitupdated 6mo agoView on Hugging Face
0likes372downloads
HostUtilities.cs606 linesDownload Raw Back to hostifaces
1// Copyright (c) Microsoft Corporation.2// Licensed under the MIT License.3 4using System.Collections.Generic;5using System.Collections.ObjectModel;6using System.Globalization;7using System.Management.Automation.Host;8using System.Management.Automation.Internal;9using System.Management.Automation.Runspaces;10using System.Management.Automation.Subsystem.Feedback;11using System.Runtime.InteropServices;12using System.Text;13using Microsoft.PowerShell.Commands.Internal.Format;14 15namespace System.Management.Automation16{17    #region Public HostUtilities Class18 19    /// <summary>20    /// Implements utility methods that might be used by Hosts.21    /// </summary>22    public static class HostUtilities23    {24        #region Internal Access25 26        private static readonly char s_actionIndicator = HostSupportUnicode() ? '\u27a4' : '>';27 28        private static bool HostSupportUnicode()29        {30            // Reference: https://github.com/zkat/supports-unicode/blob/main/src/lib.rs31            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))32            {33                return Environment.GetEnvironmentVariable("WT_SESSION") is not null ||34                    Environment.GetEnvironmentVariable("TERM_PROGRAM") is "vscode" ||35                    Environment.GetEnvironmentVariable("ConEmuTask") is "{cmd:Cmder}" ||36                    Environment.GetEnvironmentVariable("TERM") is "xterm-256color" or "alacritty";37            }38 39            string ctype = Environment.GetEnvironmentVariable("LC_ALL") ??40                Environment.GetEnvironmentVariable("LC_CTYPE") ??41                Environment.GetEnvironmentVariable("LANG") ??42                string.Empty;43 44            return ctype.EndsWith("UTF8") || ctype.EndsWith("UTF-8");45        }46 47        #region GetProfileCommands48        /// <summary>49        /// Gets a PSObject whose base object is currentUserCurrentHost and with notes for the other 4 parameters.50        /// </summary>51        /// <param name="allUsersAllHosts">The profile file name for all users and all hosts.</param>52        /// <param name="allUsersCurrentHost">The profile file name for all users and current host.</param>53        /// <param name="currentUserAllHosts">The profile file name for current user and all hosts.</param>54        /// <param name="currentUserCurrentHost">The profile name for current user and current host.</param>55        /// <returns>A PSObject whose base object is currentUserCurrentHost and with notes for the other 4 parameters.</returns>56        internal static PSObject GetDollarProfile(string allUsersAllHosts, string allUsersCurrentHost, string currentUserAllHosts, string currentUserCurrentHost)57        {58            PSObject returnValue = new PSObject(currentUserCurrentHost);59            returnValue.Properties.Add(new PSNoteProperty("AllUsersAllHosts", allUsersAllHosts));60            returnValue.Properties.Add(new PSNoteProperty("AllUsersCurrentHost", allUsersCurrentHost));61            returnValue.Properties.Add(new PSNoteProperty("CurrentUserAllHosts", currentUserAllHosts));62            returnValue.Properties.Add(new PSNoteProperty("CurrentUserCurrentHost", currentUserCurrentHost));63            return returnValue;64        }65 66        /// <summary>67        /// Gets the object that serves as a value to $profile and the paths on it.68        /// </summary>69        /// <param name="shellId">The id identifying the host or shell used in profile file names.</param>70        /// <param name="useTestProfile">Used from test not to overwrite the profile file names from development boxes.</param>71        /// <param name="allUsersAllHosts">Path for all users and all hosts.</param>72        /// <param name="currentUserAllHosts">Path for current user and all hosts.</param>73        /// <param name="allUsersCurrentHost">Path for all users current host.</param>74        /// <param name="currentUserCurrentHost">Path for current user and current host.</param>75        /// <param name="dollarProfile">The object that serves as a value to $profile.</param>76        /// <returns></returns>77        internal static void GetProfileObjectData(string shellId, bool useTestProfile, out string allUsersAllHosts, out string allUsersCurrentHost, out string currentUserAllHosts, out string currentUserCurrentHost, out PSObject dollarProfile)78        {79            allUsersAllHosts = HostUtilities.GetFullProfileFileName(null, false, useTestProfile);80            allUsersCurrentHost = HostUtilities.GetFullProfileFileName(shellId, false, useTestProfile);81            currentUserAllHosts = HostUtilities.GetFullProfileFileName(null, true, useTestProfile);82            currentUserCurrentHost = HostUtilities.GetFullProfileFileName(shellId, true, useTestProfile);83            dollarProfile = HostUtilities.GetDollarProfile(allUsersAllHosts, allUsersCurrentHost, currentUserAllHosts, currentUserCurrentHost);84        }85 86        /// <summary>87        /// Gets an array of commands that can be run sequentially to set $profile and run the profile commands.88        /// </summary>89        /// <param name="shellId">The id identifying the host or shell used in profile file names.</param>90        /// <param name="useTestProfile">Used from test not to overwrite the profile file names from development boxes.</param>91        /// <returns></returns>92        internal static PSCommand[] GetProfileCommands(string shellId, bool useTestProfile)93        {94            List<PSCommand> commands = new List<PSCommand>();95            string allUsersAllHosts, allUsersCurrentHost, currentUserAllHosts, currentUserCurrentHost;96            PSObject dollarProfile;97            HostUtilities.GetProfileObjectData(shellId, useTestProfile, out allUsersAllHosts, out allUsersCurrentHost, out currentUserAllHosts, out currentUserCurrentHost, out dollarProfile);98 99            PSCommand command = new PSCommand();100            command.AddCommand("set-variable");101            command.AddParameter("Name", "profile");102            command.AddParameter("Value", dollarProfile);103            command.AddParameter("Option", ScopedItemOptions.None);104            commands.Add(command);105 106            string[] profilePaths = new string[] { allUsersAllHosts, allUsersCurrentHost, currentUserAllHosts, currentUserCurrentHost };107            foreach (string profilePath in profilePaths)108            {109                if (!System.IO.File.Exists(profilePath))110                {111                    continue;112                }113 114                command = new PSCommand();115                command.AddCommand(profilePath, false);116                commands.Add(command);117            }118 119            return commands.ToArray();120        }121 122        /// <summary>123        /// Used to get all profile file names for the current or all hosts and for the current or all users.124        /// </summary>125        /// <param name="shellId">Null for all hosts, not null for the specified host.</param>126        /// <param name="forCurrentUser">False for all users, true for the current user.</param>127        /// <returns>The profile file name matching the parameters.</returns>128        internal static string GetFullProfileFileName(string shellId, bool forCurrentUser)129        {130            return HostUtilities.GetFullProfileFileName(shellId, forCurrentUser, false);131        }132 133        /// <summary>134        /// Used to get all profile file names for the current or all hosts and for the current or all users.135        /// </summary>136        /// <param name="shellId">Null for all hosts, not null for the specified host.</param>137        /// <param name="forCurrentUser">False for all users, true for the current user.</param>138        /// <param name="useTestProfile">Used from test not to overwrite the profile file names from development boxes.</param>139        /// <returns>The profile file name matching the parameters.</returns>140        internal static string GetFullProfileFileName(string shellId, bool forCurrentUser, bool useTestProfile)141        {142            string basePath = null;143 144            if (forCurrentUser)145            {146                basePath = Platform.ConfigDirectory;147            }148            else149            {150                basePath = GetAllUsersFolderPath(shellId);151            }152 153            if (string.IsNullOrEmpty(basePath))154            {155                return string.Empty;156            }157 158            string profileName = useTestProfile ? "profile_test.ps1" : "profile.ps1";159 160            if (!string.IsNullOrEmpty(shellId))161            {162                profileName = shellId + "_" + profileName;163            }164 165            string fullPath = basePath = IO.Path.Combine(basePath, profileName);166 167            return fullPath;168        }169 170        /// <summary>171        /// Used internally in GetFullProfileFileName to get the base path for all users profiles.172        /// </summary>173        /// <param name="shellId">The shellId to use.</param>174        /// <returns>The base path for all users profiles.</returns>175        private static string GetAllUsersFolderPath(string shellId)176        {177            string folderPath = string.Empty;178            try179            {180                folderPath = Utils.GetApplicationBase(shellId);181            }182            catch (System.Security.SecurityException)183            {184            }185 186            return folderPath;187        }188        #endregion GetProfileCommands189 190        /// <summary>191        /// Gets the first <paramref name="maxLines"/> lines of <paramref name="source"/>.192        /// </summary>193        /// <param name="source">String we want to limit the number of lines.</param>194        /// <param name="maxLines">Maximum number of lines to be returned.</param>195        /// <returns>The first lines of <paramref name="source"/>.</returns>196        internal static string GetMaxLines(string source, int maxLines)197        {198            if (string.IsNullOrEmpty(source))199            {200                return string.Empty;201            }202 203            StringBuilder returnValue = new StringBuilder();204 205            for (int i = 0, lineCount = 1; i < source.Length; i++)206            {207                char c = source[i];208 209                if (c == '\n')210                {211                    lineCount++;212                }213 214                returnValue.Append(c);215 216                if (lineCount == maxLines)217                {218                    returnValue.Append(PSObjectHelper.Ellipsis);219                    break;220                }221            }222 223            return returnValue.ToString();224        }225 226        /// <summary>227        /// Returns the prompt used in remote sessions: "[machine]: basePrompt"228        /// </summary>229        internal static string GetRemotePrompt(RemoteRunspace runspace, string basePrompt, bool configuredSession = false)230        {231            if (configuredSession ||232                runspace.ConnectionInfo is NamedPipeConnectionInfo ||233                runspace.ConnectionInfo is VMConnectionInfo ||234                runspace.ConnectionInfo is ContainerConnectionInfo)235            {236                return basePrompt;237            }238 239            SSHConnectionInfo sshConnectionInfo = runspace.ConnectionInfo as SSHConnectionInfo;240 241            // Usernames are case-sensitive on Unix systems242            if (sshConnectionInfo != null &&243                !string.IsNullOrEmpty(sshConnectionInfo.UserName) &&244                !System.Environment.UserName.Equals(sshConnectionInfo.UserName, StringComparison.Ordinal))245            {246                return string.Format(247                    CultureInfo.InvariantCulture,248                    "[{0}@{1}]: {2}",249                    sshConnectionInfo.UserName,250                    sshConnectionInfo.ComputerName,251                    basePrompt);252            }253 254            return string.Format(255                CultureInfo.InvariantCulture,256                "[{0}]: {1}",257                runspace.ConnectionInfo.ComputerName,258                basePrompt);259        }260 261        /// <summary>262        /// Create a configured remote runspace from provided name.263        /// </summary>264        /// <param name="configurationName"></param>265        /// <param name="host"></param>266        /// <returns></returns>267        internal static RemoteRunspace CreateConfiguredRunspace(268            string configurationName,269            PSHost host)270        {271            // Create a loop-back remote runspace with network access enabled, and272            // with the provided endpoint configurationname.273            TypeTable typeTable = TypeTable.LoadDefaultTypeFiles();274            var connectInfo = new WSManConnectionInfo();275            connectInfo.ShellUri = configurationName.Trim();276            connectInfo.EnableNetworkAccess = true;277 278            RemoteRunspace remoteRunspace = null;279            try280            {281                remoteRunspace = (RemoteRunspace)RunspaceFactory.CreateRunspace(connectInfo, host, typeTable);282                remoteRunspace.Open();283            }284            catch (Exception e)285            {286                throw new PSInvalidOperationException(287                    StringUtil.Format(RemotingErrorIdStrings.CannotCreateConfiguredRunspace, configurationName),288                    e);289            }290 291            remoteRunspace.IsConfiguredLoopBack = true;292            return remoteRunspace;293        }294 295        #endregion296 297        #region Public Access298 299        #region Runspace Invoke300 301        /// <summary>302        /// Helper method to invoke a PSCommand on a given runspace.  This method correctly invokes the command for303        /// these runspace cases:304        ///   1. Local runspace.  If the local runspace is busy it will invoke as a nested command.305        ///   2. Remote runspace.306        ///   3. Runspace that is stopped in the debugger at a breakpoint.307        ///308        /// Error and information streams are ignored and only the command result output is returned.309        ///310        /// This method is NOT thread safe.  It does not support running commands from different threads on the311        /// provided runspace.  It assumes the thread invoking this method is the same that runs all other312        /// commands on the provided runspace.313        /// </summary>314        /// <param name="runspace">Runspace to invoke the command on.</param>315        /// <param name="command">Command to invoke.</param>316        /// <returns>Collection of command output result objects.</returns>317        public static Collection<PSObject> InvokeOnRunspace(PSCommand command, Runspace runspace)318        {319            if (command == null)320            {321                throw new PSArgumentNullException(nameof(command));322            }323 324            if (runspace == null)325            {326                throw new PSArgumentNullException(nameof(runspace));327            }328 329            if ((runspace.Debugger != null) && runspace.Debugger.InBreakpoint)330            {331                // Use the Debugger API to run the command when a runspace is stopped in the debugger.332                PSDataCollection<PSObject> output = new PSDataCollection<PSObject>();333                runspace.Debugger.ProcessCommand(334                    command,335                    output);336 337                return new Collection<PSObject>(output);338            }339 340            // Otherwise run command directly in runspace.341            PowerShell ps = PowerShell.Create();342            ps.Runspace = runspace;343            ps.IsRunspaceOwner = false;344            if (runspace.ConnectionInfo == null)345            {346                // Local runspace.  Make a nested PowerShell object as needed.347                ps.SetIsNested(runspace.GetCurrentlyRunningPipeline() != null);348            }349 350            using (ps)351            {352                ps.Commands = command;353                return ps.Invoke<PSObject>();354            }355        }356 357        #endregion358 359        #region PSEdit Support360 361        /// <summary>362        /// PSEditFunction script string.363        /// </summary>364        public const string PSEditFunction = @"365            param (366                [Parameter(Mandatory=$true)] [string[]] $FileName367            )368 369            foreach ($file in $FileName)370            {371                Get-ChildItem $file -File | ForEach-Object {372                    $filePathName = $_.FullName373 374                    # Get file contents375                    $contentBytes = Get-Content -Path $filePathName -Raw -Encoding Byte376 377                    # Notify client for file open.378                    New-Event -SourceIdentifier PSISERemoteSessionOpenFile -EventArguments @($filePathName, $contentBytes) > $null379                }380            }381        ";382 383        /// <summary>384        /// CreatePSEditFunction script string.385        /// </summary>386        public const string CreatePSEditFunction = @"387            param (388                [string] $PSEditFunction389            )390 391            Register-EngineEvent -SourceIdentifier PSISERemoteSessionOpenFile -Forward -SupportEvent392 393            if ((Test-Path -Path 'function:\global:PSEdit') -eq $false)394            {395                Set-Item -Path 'function:\global:PSEdit' -Value $PSEditFunction396            }397        ";398 399        /// <summary>400        /// RemovePSEditFunction script string.401        /// </summary>402        public const string RemovePSEditFunction = @"403            if ((Test-Path -Path 'function:\global:PSEdit') -eq $true)404            {405                Remove-Item -Path 'function:\global:PSEdit' -Force406            }407 408            Unregister-Event -SourceIdentifier PSISERemoteSessionOpenFile -Force -ErrorAction Ignore409        ";410 411        /// <summary>412        /// Open file event.413        /// </summary>414        public const string RemoteSessionOpenFileEvent = "PSISERemoteSessionOpenFile";415 416        #endregion417 418        #region Feedback Rendering419 420        /// <summary>421        /// Render the feedbacks to the specified host.422        /// </summary>423        /// <param name="feedbacks">The feedback results.</param>424        /// <param name="ui">The host to render to.</param>425        public static void RenderFeedback(List<FeedbackResult> feedbacks, PSHostUserInterface ui)426        {427            // Caption style is dimmed bright white with italic effect, used for fixed captions, such as '[' and ']'.428            string captionStyle = "\x1b[97;2;3m";429            string italics = "\x1b[3m";430            string nameStyle = PSStyle.Instance.Formatting.FeedbackName;431            string textStyle = PSStyle.Instance.Formatting.FeedbackText;432            string actionStyle = PSStyle.Instance.Formatting.FeedbackAction;433            string ansiReset = PSStyle.Instance.Reset;434 435            if (!ui.SupportsVirtualTerminal)436            {437                captionStyle = string.Empty;438                italics = string.Empty;439                nameStyle = string.Empty;440                textStyle = string.Empty;441                actionStyle = string.Empty;442                ansiReset = string.Empty;443            }444 445            var output = new StringBuilder();446            var chkset = new HashSet<FeedbackItem>();447 448            foreach (FeedbackResult entry in feedbacks)449            {450                output.AppendLine();451                output.Append($"{captionStyle}[{ansiReset}")452                    .Append($"{nameStyle}{italics}{entry.Name}{ansiReset}")453                    .Append($"{captionStyle}]{ansiReset}");454 455                FeedbackItem item = entry.Item;456                chkset.Add(item);457 458                do459                {460                    RenderText(output, item.Header, textStyle, ansiReset, indent: 2, startOnNewLine: true);461                    RenderActions(output, item, textStyle, actionStyle, ansiReset);462                    RenderText(output, item.Footer, textStyle, ansiReset, indent: 2, startOnNewLine: true);463 464                    // A feedback provider may return multiple feedback items, though that may be rare.465                    item = item.Next;466                }467                while (item is not null && chkset.Add(item));468 469                ui.Write(output.ToString());470                output.Clear();471                chkset.Clear();472            }473 474            // Feedback section ends with a new line.475            ui.WriteLine();476        }477 478        /// <summary>479        /// Helper function to render feedback message.480        /// </summary>481        /// <param name="output">The output string builder to write to.</param>482        /// <param name="text">The text to be rendered.</param>483        /// <param name="style">The style to be used.</param>484        /// <param name="ansiReset">The ANSI code to reset.</param>485        /// <param name="indent">The number of spaces for indentation.</param>486        /// <param name="startOnNewLine">Indicates whether to start writing from a new line.</param>487        internal static void RenderText(StringBuilder output, string text, string style, string ansiReset, int indent, bool startOnNewLine)488        {489            if (text is null)490            {491                return;492            }493 494            if (startOnNewLine)495            {496                // Start writing the text on the next line.497                output.AppendLine();498            }499 500            // Apply the style.501            output.Append(style);502 503            int count = 0;504            var trimChars = "\r\n".AsSpan();505            var span = text.AsSpan().Trim(trimChars);506 507            // This loop renders the text with minimal allocation.508            while (true)509            {510                int index = span.IndexOf('\n');511                var line = index is -1 ? span : span.Slice(0, index);512 513                if (startOnNewLine || count > 0)514                {515                    output.Append(' ', indent);516                }517 518                output.Append(line.TrimEnd('\r')).AppendLine();519 520                // Break out the loop if we are done with the last line.521                if (index is -1)522                {523                    break;524                }525 526                // Point to the rest of feedback text.527                span = span.Slice(index + 1);528                count++;529            }530 531            output.Append(ansiReset);532        }533 534        /// <summary>535        /// Helper function to render feedback actions.536        /// </summary>537        /// <param name="output">The output string builder to write to.</param>538        /// <param name="item">The feedback item to be rendered.</param>539        /// <param name="textStyle">The style used for feedback messages.</param>540        /// <param name="actionStyle">The style used for feedback actions.</param>541        /// <param name="ansiReset">The ANSI code to reset.</param>542        internal static void RenderActions(StringBuilder output, FeedbackItem item, string textStyle, string actionStyle, string ansiReset)543        {544            if (item.RecommendedActions is null || item.RecommendedActions.Count is 0)545            {546                return;547            }548 549            List<string> actions = item.RecommendedActions;550            if (item.Layout is FeedbackDisplayLayout.Landscape)551            {552                // Add 4-space indentation and write the indicator.553                output.Append($"    {textStyle}{s_actionIndicator}{ansiReset} ");554 555                // Then concatenate the action texts.556                for (int i = 0; i < actions.Count; i++)557                {558                    string action = actions[i];559                    if (i > 0)560                    {561                        output.Append(", ");562                    }563 564                    output.Append(actionStyle).Append(action).Append(ansiReset);565                }566 567                output.AppendLine();568            }569            else570            {571                int lastIndex = actions.Count - 1;572                for (int i = 0; i < actions.Count; i++)573                {574                    string action = actions[i];575 576                    // Add 4-space indentation and write the indicator, then write the action.577                    output.Append($"    {textStyle}{s_actionIndicator}{ansiReset} ");578 579                    if (action.Contains('\n'))580                    {581                        // If the action is a code snippet, properly render it with the right indentation.582                        RenderText(output, action, actionStyle, ansiReset, indent: 6, startOnNewLine: false);583 584                        // Append an extra line unless it's the last action.585                        if (i != lastIndex)586                        {587                            output.AppendLine();588                        }589                    }590                    else591                    {592                        output.Append(actionStyle).Append(action).Append(ansiReset)593                            .AppendLine();594                    }595                }596            }597        }598 599        #endregion600 601        #endregion602    }603 604    #endregion605}606