Update links that contain 'en-us' culture to remove 'en-us' culture (if possible) and in some cases update to newer re-directed link to docs.microsoft.com
530 lines
17 KiB
PowerShell
530 lines
17 KiB
PowerShell
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
# Licensed under the MIT License.
|
|
<#
|
|
Enumerate all events in the manifest and create a hash table of event id to message id.
|
|
> $manifest.assembly.instrumentation.events.provider.events.event
|
|
|
|
Enumerate all messages in the manifest and create a hash table of message id to message data.
|
|
> $manifest.assembly.localization.resources.stringTable.string
|
|
> Message data will be the message text and the number of replaceable parameters in the message.
|
|
> Only messages referenced by event ids will be in the table.
|
|
|
|
Generate a resx file containing the messages.
|
|
Generate a static C# class containing
|
|
> A hash table mapping event id to message data (resource path, resource id, and the number of replaceable parameters)
|
|
> A static method for formatting the message to log and calling the native SysLog.
|
|
|
|
NOTE: A native binary will also need to be generated that wraps the call to syslog and exports a function to call from
|
|
managed code. The static method mentioned above will call this export through PInvoke.
|
|
#>
|
|
|
|
using namespace System.Collections.Generic
|
|
using namespace System.Globalization
|
|
using namespace System.Xml
|
|
|
|
#region resx string templates
|
|
|
|
# Defines the start of the resx file.
|
|
# String.Format arguments
|
|
# {0} The name of the manifest file used to produce the resx
|
|
[string] $resxPrologue = @"
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
<root>
|
|
<!--
|
|
This code was generated by the tools\ResxGen\ResxGen.ps1 run against {0}.
|
|
To add or change logged events and the associated resources, edit {0}
|
|
then rerun ResxGen.ps1 to produce an updated CS and Resx file.
|
|
-->
|
|
<xsd:schema id="root" xmlns="" xmlns:xsd="https://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
|
<xsd:import namespace="https://www.w3.org/XML/1998/namespace" />
|
|
<xsd:element name="root" msdata:IsDataSet="true">
|
|
<xsd:complexType>
|
|
<xsd:choice maxOccurs="unbounded">
|
|
<xsd:element name="metadata">
|
|
<xsd:complexType>
|
|
<xsd:sequence>
|
|
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
|
</xsd:sequence>
|
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
|
<xsd:attribute name="type" type="xsd:string" />
|
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
|
<xsd:attribute ref="xml:space" />
|
|
</xsd:complexType>
|
|
</xsd:element>
|
|
<xsd:element name="assembly">
|
|
<xsd:complexType>
|
|
<xsd:attribute name="alias" type="xsd:string" />
|
|
<xsd:attribute name="name" type="xsd:string" />
|
|
</xsd:complexType>
|
|
</xsd:element>
|
|
<xsd:element name="data">
|
|
<xsd:complexType>
|
|
<xsd:sequence>
|
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
|
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
|
</xsd:sequence>
|
|
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
|
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
|
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
|
<xsd:attribute ref="xml:space" />
|
|
</xsd:complexType>
|
|
</xsd:element>
|
|
<xsd:element name="resheader">
|
|
<xsd:complexType>
|
|
<xsd:sequence>
|
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
|
</xsd:sequence>
|
|
<xsd:attribute name="name" type="xsd:string" use="required" />
|
|
</xsd:complexType>
|
|
</xsd:element>
|
|
</xsd:choice>
|
|
</xsd:complexType>
|
|
</xsd:element>
|
|
</xsd:schema>
|
|
<resheader name="resmimetype">
|
|
<value>text/microsoft-resx</value>
|
|
</resheader>
|
|
<resheader name="version">
|
|
<value>2.0</value>
|
|
</resheader>
|
|
<resheader name="reader">
|
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
</resheader>
|
|
<resheader name="writer">
|
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
</resheader>
|
|
<data name="MissingEventIdMessage" xml:space="preserve">
|
|
<value>A message was not found for event id {0}.</value>
|
|
</data>
|
|
"@
|
|
|
|
# Defines a template for each named string resource in the resx file.
|
|
# String.Format arguments
|
|
# {0} The name of the resource
|
|
# {1} The value of the resource
|
|
[string] $resxEntryTemplate = @"
|
|
|
|
<data name="{0}" xml:space="preserve">
|
|
<value>{1}</value>
|
|
</data>
|
|
"@
|
|
|
|
# Defines the end of the resx file.
|
|
# This should be appended after adding each named resource.
|
|
# String.Format arguments: None
|
|
[string] $resxEpilogue = @"
|
|
|
|
</root>
|
|
"@
|
|
|
|
#endregion resx string templates
|
|
|
|
#region C# code template strings
|
|
|
|
# Defines the start of the generated code.
|
|
# String.Format arguments
|
|
# {0} The namespace for the class
|
|
# {1} The class name
|
|
# {2} The name of the manifest file used to produce the code.
|
|
[string] $codePrologue = @'
|
|
#if UNIX
|
|
/*
|
|
This code was generated by the tools\ResxGen\ResxGen.ps1 run against {2}.
|
|
To add or change logged events and the associated resources, edit {2}
|
|
then rerun ResxGen.ps1 to produce an updated CS and Resx file.
|
|
*/
|
|
using System.Collections.Generic;
|
|
using System.Management.Automation.Internal;
|
|
using System.Runtime.InteropServices;
|
|
|
|
namespace {0}
|
|
{{
|
|
/// <summary>
|
|
/// Provides a class for describing a message resource for an ETW event.
|
|
/// </summary>
|
|
internal static class {1}
|
|
{{
|
|
// Defines the resource id of the message to use when an event id is not valid.
|
|
private const string MissingEventIdResourceName = "MissingEventIdMessage";
|
|
|
|
/// <summary>
|
|
/// Gets the name of the message resource to use for event ids that are not found.
|
|
/// is not found.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This method is called when GetMessage returns a null value indicating the passed
|
|
/// in event id was not found. The message should be used as the format string
|
|
/// with the event id as the single variable argument.
|
|
/// <remarks>
|
|
public static string GetMissingEventMessage(out int parameterCount)
|
|
{{
|
|
parameterCount = 1;
|
|
return MissingEventIdResourceName;
|
|
}}
|
|
|
|
/// <summary>
|
|
/// Gets the message resource id for the specified event id
|
|
/// </summary>
|
|
/// <param name="eventId">The event id for the message resource to retrieve.</param>
|
|
/// <param name="parameterCount">The number of parameters required by the message resource</param>
|
|
/// <returns>The string resource id of the associated event message; otherwise, a null reference if the event id is not valid.</returns>
|
|
public static string GetMessage(int eventId, out int parameterCount)
|
|
{{
|
|
switch (eventId)
|
|
{{
|
|
'@
|
|
|
|
# Adds an entry to the eventid -> resource name dictionary
|
|
# String.Format arguments
|
|
# {0} - event id
|
|
# {1} - the resource id for the event message
|
|
# {2} - the number of parameters required to format the message. May be zero.
|
|
[string] $codeEventEntryTemplate = @"
|
|
|
|
case {0}:
|
|
parameterCount = {2};
|
|
return "{1}";
|
|
"@
|
|
|
|
# defines the end of the generated C# code.
|
|
# String.Format arguments: None
|
|
[string] $codeEpilogue = @"
|
|
|
|
}}
|
|
parameterCount = 0;
|
|
return null;
|
|
}}
|
|
}}
|
|
}}
|
|
#endif
|
|
"@
|
|
|
|
#endregion C# code template strings
|
|
|
|
<#
|
|
Provides a class for encapsulating a resource string entry from an ETW manifest
|
|
#>
|
|
class EventMessage
|
|
{
|
|
#region properties
|
|
|
|
<#
|
|
Gets the message id.
|
|
This is used as a resource name.
|
|
#>
|
|
[string] $Id
|
|
|
|
<#
|
|
Gets the identifier used by an event to reference the message.
|
|
#>
|
|
[string] $EventReference
|
|
|
|
<#
|
|
The number of replaceable parameters in the message; from 0 through 99
|
|
Used to determine if string.Format is needed.
|
|
#>
|
|
[int] $ParameterCount
|
|
|
|
<#
|
|
Gets the message text
|
|
#>
|
|
[string] $Value
|
|
|
|
#endregion properties
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
replaces FormatMessage format specifiers with String.Format equivalent.
|
|
|
|
.PARAMETER message
|
|
The message string to update.
|
|
|
|
.NOTES
|
|
See https://msdn.microsoft.com/library/windows/desktop/ms679351(v=vs.85).aspx.
|
|
Replaceable parameters are limited to %1 ... %99. Width and precision specifiers are
|
|
not currently supported since the manifest does not use them at the time of this writing.
|
|
#>
|
|
hidden [void] SetMessage([string] $message)
|
|
{
|
|
foreach ($source in [EventMessage]::escapeStrings.Keys)
|
|
{
|
|
$dest = [EventMessage]::escapeStrings[$source]
|
|
$message = $message.Replace($source, $dest)
|
|
}
|
|
|
|
[int] $paramCount = 0
|
|
for ($index = 1; $index -le 99; $index++)
|
|
{
|
|
[string] $source = [string]::Format([CultureInfo]::InvariantCulture, '%{0}', $index)
|
|
|
|
if ($message.Contains($source))
|
|
{
|
|
$paramCount = $index;
|
|
# convert %1->%99 to {0}->{98}
|
|
[string] $target = [string]::Format([CultureInfo]::InvariantCulture, '{0}{1}{2}', '{', $index - 1, '}')
|
|
$message = $message.Replace($source, $target)
|
|
}
|
|
}
|
|
$this.Value = $message
|
|
$this.ParameterCount = $paramCount
|
|
}
|
|
|
|
EventMessage([XmlElement] $element)
|
|
{
|
|
$this.EventReference =[string]::Format([System.Globalization.CultureInfo]::InvariantCulture, '$(string.{0})', $element.Id)
|
|
[string] $messageId = $element.id
|
|
if ($messageId.EndsWith('.message'))
|
|
{
|
|
$messageId = $messageId.Substring(0, $messageId.Length - '.message'.Length)
|
|
}
|
|
if ($messageId.Contains('.'))
|
|
{
|
|
$messageId = $messageId.Replace('.', '')
|
|
}
|
|
if ($messageId.Contains('-'))
|
|
{
|
|
$messageId = $messageId.Replace('-', '')
|
|
}
|
|
$this.Id = $messageId
|
|
$this.SetMessage($element.value)
|
|
}
|
|
|
|
static hidden $escapeStrings =
|
|
@{
|
|
'%t' = "`t";
|
|
'%n'="`n";
|
|
'%r'="`r";
|
|
'%%'='%';
|
|
'%space'=' ';
|
|
'%.'='.'
|
|
}
|
|
}
|
|
|
|
enum LogLevel
|
|
{
|
|
Always = 0
|
|
Critical = 1
|
|
Error = 2
|
|
Warning = 3
|
|
Information = 4
|
|
Verbose = 5
|
|
}
|
|
|
|
class EventEntry
|
|
{
|
|
[int] $EventId
|
|
[string] $MessageReference
|
|
[EventMessage] $EventMessage
|
|
[string] $Channel
|
|
[LogLevel] $Level
|
|
[string] $Task
|
|
|
|
EventEntry ([XmlElement] $element)
|
|
{
|
|
$idValue = $element.value.Trim()
|
|
if ($idValue.StartsWith('0x', [StringComparison]::OrdinalIgnoreCase))
|
|
{
|
|
$idValue = $idValue.SubString(2)
|
|
}
|
|
$this.EventId = [Int32]::Parse($idValue, [System.Globalization.NumberStyles]::HexNumber )
|
|
$this.Channel = $element.channel
|
|
$this.Level = [EventEntry]::levelNames[$element.level]
|
|
$this.MessageReference = $element.message
|
|
$this.Task = $element.Task
|
|
}
|
|
|
|
static hidden $levelNames =
|
|
@{
|
|
'win:Always' = [LogLevel]::Always;
|
|
'win:Verbose' = [LogLevel]::Verbose;
|
|
'win:Informational' = [LogLevel]::Information;
|
|
'win:Warning' = [LogLevel]::Warning;
|
|
'win:Error' = [LogLevel]::Error;
|
|
'win:Critical' = [LogLevel]::Critical;
|
|
}
|
|
}
|
|
|
|
class Manifest
|
|
{
|
|
[string] $FileName
|
|
[Dictionary[int, EventEntry]] $Events
|
|
[Dictionary[string, EventMessage]] $Messages
|
|
[Dictionary[string, string]] $Tasks
|
|
[Dictionary[string, string]] $Opcodes
|
|
[Dictionary[string, string]] $Channels
|
|
|
|
Manifest([string] $Path)
|
|
{
|
|
if (-not (Test-Path -Path $Path))
|
|
{
|
|
throw "The manifest file was not found: $Path"
|
|
}
|
|
Write-Verbose -Message "Parsing $Path" -Verbose
|
|
$this.FileName = Split-Path -Path $Path -Leaf -Resolve
|
|
|
|
[xml] $man = Get-Content -Path $Path
|
|
|
|
$messageTable = [Dictionary[string, EventMessage]]::new()
|
|
foreach ($item in $man.assembly.localization.resources.stringTable.string)
|
|
{
|
|
$eventMessage = [EventMessage]::new($item)
|
|
$messageTable.Add($eventMessage.EventReference, $eventMessage)
|
|
}
|
|
|
|
$this.Tasks = [Dictionary[string, string]]::new()
|
|
foreach ($item in $man.assembly.instrumentation.events.provider.tasks.task)
|
|
{
|
|
$this.Tasks.Add($item.Symbol, $item.Name)
|
|
}
|
|
|
|
$this.Opcodes = [Dictionary[string, string]]::new()
|
|
foreach ($item in $man.assembly.instrumentation.events.provider.opcodes.opcode)
|
|
{
|
|
$this.Opcodes.Add($item.Symbol, $item.Name)
|
|
}
|
|
|
|
$this.Channels = [Dictionary[string, string]]::new()
|
|
foreach ($item in $man.assembly.instrumentation.events.provider.channels.channel)
|
|
{
|
|
$this.Channels.Add($item.Symbol, $item.Type)
|
|
}
|
|
|
|
$this.Events = [Dictionary[int, EventMessage]]::new()
|
|
foreach ($event in $man.assembly.instrumentation.events.provider.events.event)
|
|
{
|
|
[EventEntry] $eventEntry = [EventEntry]::new($event)
|
|
$eventEntry.EventMessage = $messageTable[$eventEntry.MessageReference]
|
|
$this.Events.Add($eventEntry.EventId, $eventEntry)
|
|
}
|
|
|
|
# NOTE: Build the final message dictionary.
|
|
# $messageTable contains all strings defined in the manifest but not all are needed.
|
|
# Some are for tasks, opcodes, channels, etc., and some events reference the same
|
|
# message.
|
|
$this.Messages = [Dictionary[int, EventMessage]]::new()
|
|
foreach ($event in $this.Events.Values)
|
|
{
|
|
$eventMessage = $event.EventMessage
|
|
if (!$this.Messages.ContainsKey($eventMessage.EventReference))
|
|
{
|
|
$this.Messages.Add($eventMessage.EventReference, $eventMessage)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function New-ResourceCode
|
|
{
|
|
param
|
|
(
|
|
[Parameter(Mandatory)]
|
|
[ValidateNotNull()]
|
|
[Manifest] $manifest,
|
|
|
|
[Parameter(Mandatory)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string] $namespaceName,
|
|
|
|
[Parameter(Mandatory)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string] $className
|
|
)
|
|
$sb = [System.Text.StringBuilder]::new()
|
|
$null = $sb.AppendFormat($codePrologue, $namespaceName, $className, $manifest.FileName)
|
|
# sort by event id for readability.
|
|
$values = ($manifest.Events.Values | Sort-Object -Property 'EventId')
|
|
foreach ($eventEntry in $values)
|
|
{
|
|
$null = $sb.AppendFormat($codeEventEntryTemplate, $eventEntry.EventId, $eventEntry.EventMessage.Id, $eventEntry.EventMessage.ParameterCount)
|
|
}
|
|
$null = $sb.Append($codeEpilogue)
|
|
$code = $sb.ToString().Replace('}}', '}')
|
|
return $code
|
|
}
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Creates a resx file containing the messages from a manifest
|
|
|
|
.PARAMETER messages
|
|
The EventMessage hash table containing the manifest messages
|
|
#>
|
|
function New-Resx
|
|
{
|
|
param
|
|
(
|
|
[Manifest] $manifest
|
|
)
|
|
$messages = $manifest.Messages
|
|
$sb = [System.Text.StringBuilder]::new()
|
|
|
|
$null = $sb.AppendFormat($resxPrologue, $manifest.FileName)
|
|
foreach ($message in $messages.Values)
|
|
{
|
|
$null = $sb.AppendFormat($resxEntryTemplate, $message.Id, $message.Value)
|
|
}
|
|
$null = $sb.Append($resxEpilogue)
|
|
return $sb.ToString()
|
|
}
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Generates a resx file and code file from an ETW manifest.
|
|
|
|
.PARAMETER Manifest
|
|
The path to the ETW manifest file to read.
|
|
|
|
.PARAMETER Name
|
|
The name to use for the C# class, the code file, and the resx file.
|
|
The default value is EventResource.
|
|
|
|
.PARAMETER Namespace
|
|
The namespace to place the C# class.
|
|
The default is System.Management.Automation.Tracing.
|
|
|
|
.PARAMETER ResxPath
|
|
The path to the directory to use to create the resx file.
|
|
|
|
.PARAMETER CodePath
|
|
The path to the directory to use to create the C# code file.
|
|
#>
|
|
function ConvertTo-Resx
|
|
{
|
|
[CmdletBinding()]
|
|
param
|
|
(
|
|
[Parameter(Mandatory)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string] $Manifest,
|
|
|
|
[string] $Name = 'EventResource',
|
|
|
|
[Parameter(Mandatory)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string] $Namespace = 'System.Management.Automation.Tracing',
|
|
|
|
[Parameter(Mandatory)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string] $ResxPath,
|
|
|
|
[Parameter(Mandatory)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string] $CodePath
|
|
)
|
|
|
|
[Manifest] $etwmanifest = [Manifest]::new($Manifest)
|
|
|
|
$resxFileName = Join-Path -Path $ResxPath -ChildPath "$($Name).resx"
|
|
Write-Verbose -Message "Creating $resxFileName" -Verbose
|
|
|
|
$resx = New-Resx -manifest $etwmanifest
|
|
$resx | Set-Content -Path $resxFileName -Encoding 'ASCII'
|
|
|
|
$codeFileName = Join-Path -Path $CodePath -ChildPath "$($Name).cs"
|
|
Write-Verbose -Message "Creating $codeFileName" -Verbose
|
|
|
|
$code = New-ResourceCode -manifest $etwmanifest -Namespace $Namespace -ClassName $Name
|
|
$code | Set-Content -Path $codeFileName -Encoding 'ASCII'
|
|
}
|
|
|
|
Export-ModuleMember -Function ConvertTo-Resx
|