PowerShell/tools/ResxGen/ResxGen.psm1

531 lines
17 KiB
PowerShell

# Copyright (c) Microsoft Corporation.
# 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