MegaBites-AI/Windows-powershell
0372
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 