MegaBites-AI/Windows-powershell
0372
1// Copyright (c) Microsoft Corporation.2// Licensed under the MIT License.3 4#if LEGACYTELEMETRY5 6using System;7using System.Collections.Generic;8using System.Diagnostics;9using System.Management.Automation.Internal;10using System.Diagnostics.CodeAnalysis;11using System.Management.Automation;12using System.Management.Automation.Language;13using System.Threading;14using System.Threading.Tasks;15 16namespace Microsoft.PowerShell.Telemetry.Internal17{18 /// <summary>19 /// </summary>20 [SuppressMessage("Microsoft.MSInternal", "CA903:InternalNamespaceShouldNotContainPublicTypes")]21 public static class TelemetryAPI22 {23#region Public API24 25 /// <summary>26 /// Public API to expose Telemetry in PowerShell27 /// Provide meaningful message. Ex: PSCONSOLE_START, PSRUNSPACE_START28 /// arguments are of anonymous type. Ex: new { PSVersion = "5.0", PSRemotingProtocolVersion = "2.2"}29 /// </summary>30 public static void TraceMessage<T>(string message, T arguments)31 {32 TelemetryWrapper.TraceMessage(message, arguments);33 }34 35#endregion36 37 private static int s_anyPowerShellSessionOpen;38 private static DateTime s_sessionStartTime;39 40 private enum HostIsInteractive41 {42 Unknown,43 Interactive,44 NonInteractive45 }46 47 /// <summary>48 /// Called either after opening a runspace (the default), or by the host application.49 /// </summary>50 public static void ReportStartupTelemetry(IHostProvidesTelemetryData ihptd)51 {52 // Avoid reporting startup more than once, except if we report "exited" and53 // another runspace gets opened.54 if (Interlocked.CompareExchange(ref s_anyPowerShellSessionOpen, 1, 0) == 1)55 return;56 57 bool is32Bit = !Environment.Is64BitProcess;58 var psversion = PSVersionInfo.PSVersion.ToString();59 var hostName = Process.GetCurrentProcess().ProcessName;60 if (ihptd != null)61 {62 TelemetryWrapper.TraceMessage("PSHostStart", new63 {64 Interactive = ihptd.HostIsInteractive ? HostIsInteractive.Interactive : HostIsInteractive.NonInteractive,65 ProfileLoadTime = ihptd.ProfileLoadTimeInMS,66 ReadyForInputTime = ihptd.ReadyForInputTimeInMS,67 Is32Bit = is32Bit,68 PSVersion = psversion,69 ProcessName = hostName,70 });71 }72 else73 {74 TelemetryWrapper.TraceMessage("PSHostStart", new75 {76 Interactive = HostIsInteractive.Unknown,77 ProfileLoadTime = 0,78 ReadyForInputTime = 0,79 Is32Bit = is32Bit,80 PSVersion = psversion,81 ProcessName = hostName,82 });83 }84 85 s_sessionStartTime = DateTime.Now;86 }87 88 /// <summary>89 /// Called after there are no more open runspaces. In some host applications, this could90 /// report multiple exits.91 /// </summary>92 public static void ReportExitTelemetry(IHostProvidesTelemetryData ihptd)93 {94 TelemetryWrapper.TraceMessage("PSHostStop", new95 {96 InteractiveCommandCount = ihptd != null ? ihptd.InteractiveCommandCount : 0,97 TabCompletionTimes = s_tabCompletionTimes,98 TabCompletionCounts = s_tabCompletionCounts,99 TabCompletionResultCounts = s_tabCompletionResultCounts,100 SessionTime = (DateTime.Now - s_sessionStartTime).TotalMilliseconds101 });102 103 // In case a host opens another runspace, we will want another PSHostStart event,104 // so reset our flag here to allow that event to fire.105 s_anyPowerShellSessionOpen = 0;106 }107 108 /// <summary>109 /// Report Get-Help requests, how many results are returned, and how long it took.110 /// </summary>111 internal static void ReportGetHelpTelemetry(string name, int topicsFound, long timeInMS, bool updatedHelp)112 {113 TelemetryWrapper.TraceMessage("PSHelpRequest", new114 {115 TopicCount = topicsFound,116 TimeInMS = timeInMS,117 RanUpdateHelp = updatedHelp,118 HelpTopic = name,119 });120 }121 122 /// <summary>123 /// Report when Get-Command fails to find something.124 /// </summary>125 internal static void ReportGetCommandFailed(string[] name, long timeInMS)126 {127 TelemetryWrapper.TraceMessage("PSGetCommandFailed", new { TimeInMS = timeInMS, CommandNames = name });128 }129 130 private static long[] s_tabCompletionTimes = new long[(int)CompletionResultType.DynamicKeyword + 1];131 private static int[] s_tabCompletionCounts = new int[(int)CompletionResultType.DynamicKeyword + 1];132 private static int[] s_tabCompletionResultCounts = new int[(int)CompletionResultType.DynamicKeyword + 1];133 internal static void ReportTabCompletionTelemetry(long elapsedMilliseconds, int count, CompletionResultType completionResultType)134 {135 // We'll collect some general statistics.136 int idx = (int)completionResultType;137 if (idx >= 0 && idx <= (int)CompletionResultType.DynamicKeyword)138 {139 s_tabCompletionTimes[idx] += elapsedMilliseconds;140 s_tabCompletionCounts[idx]++;141 s_tabCompletionResultCounts[idx] += count;142 }143 144 // Also write an event for any slow tab completion (> 250ms).145 if (elapsedMilliseconds > 250)146 {147 TelemetryWrapper.TraceMessage("PSSlowTabCompletion", new148 {149 Time = elapsedMilliseconds,150 Count = count,151 Type = completionResultType,152 });153 }154 }155 156 /// <summary>157 /// Report that a module was loaded, but only do so for modules that *might* be authored by Microsoft. We can't158 /// be 100% certain, but we'll ignore non-Microsoft module names when looking at any data, so it's best to159 /// at least attempt avoiding collecting data we'll ignore.160 /// </summary>161 internal static void ReportModuleLoad(PSModuleInfo foundModule)162 {163 var modulePath = foundModule.Path;164 var companyName = foundModule.CompanyName;165 bool couldBeMicrosoftModule =166 (modulePath != null &&167 (modulePath.StartsWith(Utils.DefaultPowerShellAppBase, StringComparison.OrdinalIgnoreCase) ||168 // The following covers both 64 and 32 bit Program Files by assuming 32bit is just ...\Program Files + " (x86)"169 modulePath.StartsWith(Platform.GetFolderPath(Environment.SpecialFolder.ProgramFiles), StringComparison.OrdinalIgnoreCase))) ||170 (companyName != null &&171 foundModule.CompanyName.StartsWith("Microsoft", StringComparison.OrdinalIgnoreCase));172 if (couldBeMicrosoftModule)173 {174 TelemetryWrapper.TraceMessage("PSImportModule", new175 {176 ModuleName = foundModule.Name,177 Version = foundModule.Version.ToString()178 });179 }180 }181 182 /// <summary>183 /// Report that a new local session (runspace) is created.184 /// </summary>185 internal static void ReportLocalSessionCreated(186 System.Management.Automation.Runspaces.InitialSessionState iss,187 System.Management.Automation.Host.TranscriptionData transcriptionData)188 {189 bool isConstrained = (iss != null) && (iss.DefaultCommandVisibility != SessionStateEntryVisibility.Public) && (iss.LanguageMode != PSLanguageMode.FullLanguage);190 bool isTranscripting = (transcriptionData != null) && (transcriptionData.SystemTranscript != null);191 192 TelemetryWrapper.TraceMessage("PSNewLocalSession", new193 {194 Constrained = isConstrained,195 Transcripting = isTranscripting196 });197 }198 199 private enum RemoteSessionType200 {201 Unknown,202 LocalProcess,203 WinRMRemote,204 HyperVRemote,205 ContainerRemote206 }207 208 private enum RemoteConfigurationType209 {210 Unknown,211 PSDefault,212 PSWorkflow,213 ServerManagerWorkflow,214 Custom215 }216 217 /// <summary>218 /// Report that a new remote session (runspace) is created.219 /// </summary>220 internal static void ReportRemoteSessionCreated(221 System.Management.Automation.Runspaces.RunspaceConnectionInfo connectionInfo)222 {223 RemoteSessionType sessionType = RemoteSessionType.Unknown;224 RemoteConfigurationType configurationType = RemoteConfigurationType.Unknown;225 if (connectionInfo is System.Management.Automation.Runspaces.NewProcessConnectionInfo)226 {227 sessionType = RemoteSessionType.LocalProcess;228 configurationType = RemoteConfigurationType.PSDefault;229 }230 else231 {232 System.Management.Automation.Runspaces.WSManConnectionInfo wsManConnectionInfo = connectionInfo as System.Management.Automation.Runspaces.WSManConnectionInfo;233 if (wsManConnectionInfo != null)234 {235 sessionType = RemoteSessionType.WinRMRemote;236 237 // Parse configuration name from ShellUri:238 // ShellUri = 'http://schemas.microsoft.com/powershell/Microsoft.PowerShell'239 // ConfigName = 'Microsoft.PowerShell'240 string configurationName = wsManConnectionInfo.ShellUri;241 if (!string.IsNullOrEmpty(configurationName))242 {243 int index = configurationName.LastIndexOf('/');244 if (index > -1)245 {246 configurationName = configurationName.Substring(index + 1);247 }248 }249 250 configurationType = GetConfigurationTypefromName(configurationName);251 }252 else253 {254 System.Management.Automation.Runspaces.VMConnectionInfo vmConnectionInfo = connectionInfo as System.Management.Automation.Runspaces.VMConnectionInfo;255 if (vmConnectionInfo != null)256 {257 sessionType = RemoteSessionType.HyperVRemote;258 configurationType = GetConfigurationTypefromName(vmConnectionInfo.ConfigurationName);259 }260 else261 {262 System.Management.Automation.Runspaces.ContainerConnectionInfo containerConnectionInfo = connectionInfo as System.Management.Automation.Runspaces.ContainerConnectionInfo;263 if (containerConnectionInfo != null)264 {265 sessionType = RemoteSessionType.ContainerRemote;266 configurationType = GetConfigurationTypefromName(267 (containerConnectionInfo.ContainerProc != null) ? containerConnectionInfo.ContainerProc.ConfigurationName : string.Empty);268 }269 }270 }271 }272 273 TelemetryWrapper.TraceMessage("PSNewRemoteSession", new274 {275 Type = sessionType,276 Configuration = configurationType277 });278 }279 280 private static RemoteConfigurationType GetConfigurationTypefromName(string name)281 {282 string configName = (name != null) ? name.Trim() : string.Empty;283 284 if (string.IsNullOrEmpty(configName) ||285 configName.Equals("microsoft.powershell", StringComparison.OrdinalIgnoreCase) ||286 configName.Equals("microsoft.powershell32", StringComparison.OrdinalIgnoreCase))287 {288 return RemoteConfigurationType.PSDefault;289 }290 else if (configName.Equals("microsoft.powershell.workflow", StringComparison.OrdinalIgnoreCase))291 {292 return RemoteConfigurationType.PSWorkflow;293 }294 else if (configName.Equals("microsoft.windows.servermanagerworkflows", StringComparison.OrdinalIgnoreCase))295 {296 return RemoteConfigurationType.ServerManagerWorkflow;297 }298 else299 {300 return RemoteConfigurationType.Custom;301 }302 }303 304 private enum ScriptFileType305 {306 None = 0,307 Ps1 = 1,308 Psd1 = 2,309 Psm1 = 3,310 Other = 4,311 }312 313 private static readonly int s_promptHashCode = "prompt".GetHashCode();314 315 /// <summary>316 /// Report some telemetry about the scripts that are run.317 /// </summary>318 internal static void ReportScriptTelemetry(Ast ast, bool dotSourced, long compileTimeInMS)319 {320 if (ast.Parent != null || !TelemetryWrapper.IsEnabled)321 return;322 323 Task.Run(() =>324 {325 var extent = ast.Extent;326 var text = extent.Text;327 var hash = text.GetHashCode();328 329 // Ignore 'prompt' so we don't generate an event for every 'prompt' that is invoked.330 // (We really should only create 'prompt' once, but we don't.331 if (hash == s_promptHashCode)332 return;333 334 var visitor = new ScriptBlockTelemetry();335 336 ast.Visit(visitor);337 338 var scriptFileType = ScriptFileType.None;339 var fileName = extent.File;340 if (fileName != null)341 {342 var ext = System.IO.Path.GetExtension(fileName);343 if (".ps1".Equals(ext, StringComparison.OrdinalIgnoreCase))344 {345 scriptFileType = ScriptFileType.Ps1;346 }347 else if (".psd1".Equals(ext, StringComparison.OrdinalIgnoreCase))348 {349 scriptFileType = ScriptFileType.Psd1;350 }351 else if (".psm1".Equals(ext, StringComparison.OrdinalIgnoreCase))352 {353 scriptFileType = ScriptFileType.Psm1;354 }355 else356 {357 // Reachable?358 scriptFileType = ScriptFileType.Other;359 }360 }361 362 TelemetryWrapper.TraceMessage("PSScriptDetails", new363 {364 Hash = hash,365 IsDotSourced = dotSourced,366 ScriptFileType = scriptFileType,367 Length = text.Length,368 LineCount = extent.EndLineNumber - extent.StartLineNumber,369 CompileTimeInMS = compileTimeInMS,370 StatementCount = visitor.StatementCount,371 CountOfCommands = visitor.CountOfCommands,372 CountOfDotSourcedCommands = visitor.CountOfDotSourcedCommands,373 MaxArrayLength = visitor.MaxArraySize,374 ArrayLiteralCount = visitor.ArrayLiteralCount,375 ArrayLiteralCumulativeSize = visitor.ArrayLiteralCumulativeSize,376 MaxStringLength = visitor.MaxStringSize,377 StringLiteralCount = visitor.StringLiteralCount,378 StringLiteralCumulativeSize = visitor.StringLiteralCumulativeSize,379 MaxPipelineDepth = visitor.MaxPipelineDepth,380 PipelineCount = visitor.PipelineCount,381 FunctionCount = visitor.FunctionCount,382 ScriptBlockCount = visitor.ScriptBlockCount,383 ClassCount = visitor.ClassCount,384 EnumCount = visitor.EnumCount,385 CommandsCalled = visitor.CommandsCalled,386 });387 });388 }389 }390 391 internal class ScriptBlockTelemetry : AstVisitor2392 {393 internal ScriptBlockTelemetry()394 {395 CommandsCalled = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);396 }397 398 internal Dictionary<string, int> CommandsCalled { get; private set; }399 400 internal int CountOfCommands { get; private set; }401 402 internal int CountOfDotSourcedCommands { get; private set; }403 404 public override AstVisitAction VisitCommand(CommandAst commandAst)405 {406 CountOfCommands++;407 var commandName = commandAst.GetCommandName();408 if (commandName != null)409 {410 int commandCount;411 CommandsCalled.TryGetValue(commandName, out commandCount);412 CommandsCalled[commandName] = commandCount + 1;413 }414 415 if (commandAst.InvocationOperator == TokenKind.Dot)416 CountOfDotSourcedCommands++;417 418 return AstVisitAction.Continue;419 }420 421 internal int MaxStringSize { get; private set; }422 423 internal int StringLiteralCount { get; private set; }424 425 internal int StringLiteralCumulativeSize { get; private set; }426 427 public override AstVisitAction VisitStringConstantExpression(StringConstantExpressionAst stringConstantExpressionAst)428 {429 var stringSize = stringConstantExpressionAst.Value.Length;430 StringLiteralCount += 1;431 StringLiteralCumulativeSize += stringSize;432 MaxStringSize = Math.Max(MaxStringSize, stringSize);433 return AstVisitAction.Continue;434 }435 436 public override AstVisitAction VisitExpandableStringExpression(ExpandableStringExpressionAst expandableStringExpressionAst)437 {438 var stringSize = expandableStringExpressionAst.Value.Length;439 StringLiteralCount += 1;440 StringLiteralCumulativeSize += stringSize;441 MaxStringSize = Math.Max(MaxStringSize, stringSize);442 return AstVisitAction.Continue;443 }444 445 internal int MaxArraySize { get; private set; }446 447 internal int ArrayLiteralCount { get; private set; }448 449 internal int ArrayLiteralCumulativeSize { get; private set; }450 451 public override AstVisitAction VisitArrayLiteral(ArrayLiteralAst arrayLiteralAst)452 {453 var elementCount = arrayLiteralAst.Elements.Count;454 ArrayLiteralCount += 1;455 ArrayLiteralCumulativeSize += elementCount;456 MaxArraySize = Math.Max(MaxArraySize, elementCount);457 return AstVisitAction.Continue;458 }459 460 internal int StatementCount { get; private set; }461 462 public override AstVisitAction VisitBlockStatement(BlockStatementAst blockStatementAst)463 {464 StatementCount += blockStatementAst.Body.Statements.Count;465 return AstVisitAction.Continue;466 }467 468 public override AstVisitAction VisitNamedBlock(NamedBlockAst namedBlockAst)469 {470 StatementCount += namedBlockAst.Statements.Count;471 return AstVisitAction.Continue;472 }473 474 internal int FunctionCount { get; private set; }475 476 public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst)477 {478 FunctionCount += 1;479 return AstVisitAction.Continue;480 }481 482 internal int ScriptBlockCount { get; private set; }483 484 public override AstVisitAction VisitScriptBlockExpression(ScriptBlockExpressionAst scriptBlockExpressionAst)485 {486 ScriptBlockCount += 1;487 return AstVisitAction.Continue;488 }489 490 internal int MaxPipelineDepth { get; private set; }491 492 internal int PipelineCount { get; private set; }493 494 public override AstVisitAction VisitPipeline(PipelineAst pipelineAst)495 {496 MaxPipelineDepth = Math.Max(MaxPipelineDepth, pipelineAst.PipelineElements.Count);497 PipelineCount += 1;498 return AstVisitAction.Continue;499 }500 501 internal int ClassCount { get; private set; }502 503 internal int EnumCount { get; private set; }504 505 public override AstVisitAction VisitTypeDefinition(TypeDefinitionAst typeDefinitionAst)506 {507 if (typeDefinitionAst.IsClass)508 ClassCount += 1;509 else if (typeDefinitionAst.IsEnum)510 EnumCount += 1;511 return AstVisitAction.Continue;512 }513 }514 515 /// <summary>516 /// If implemented by the host, the host should call <see cref="TelemetryAPI.ReportStartupTelemetry"/> and <see cref="TelemetryAPI.ReportExitTelemetry"/>517 /// and track the data defined by this interface.518 /// </summary>519 public interface IHostProvidesTelemetryData520 {521 /// <summary>A host sets this property as appropriate - used when reporting telemetry.</summary>522 bool HostIsInteractive { get; }523 524 /// <summary>A host sets this property as appropriate - used when reporting telemetry.</summary>525 double ProfileLoadTimeInMS { get; }526 527 /// <summary>A host sets this property as appropriate - used when reporting telemetry.</summary>528 double ReadyForInputTimeInMS { get; }529 530 /// <summary>A host sets this property as appropriate - used when reporting telemetry.</summary>531 int InteractiveCommandCount { get; }532 }533}534#endif535 