Refactor to ETW logging

This commit is contained in:
Frans van Dorsselaer committed 2026-08-20 15:02:26 +02:00
1 parent 33a9ea5cd3
commit 9d1b8ca7ad
30 files changed
+1185 -71

No files matched your search

+13 -1
View File
@@ -191,7 +191,7 @@ charset = utf-8-bom
indent_size = 2
insert_final_newline = false
[*.{manifest,props,targets}]
[*.{props,targets}]
charset = utf-8-bom
indent_size = 2
@@ -214,3 +214,15 @@ insert_final_newline = false
[{global.json,dotnet-tools.json,Directory.Packages.props}]
charset = utf-8
insert_final_newline = false
[*.{xml,manifest}]
charset = utf-8
indent_size = 2
tab_width = 2
insert_final_newline = true
[*.{rc,rc2}]
charset = utf-8
indent_size = 4
tab_width = 4
insert_final_newline = true
+7 -4
View File
@@ -14,14 +14,17 @@
*.license text
*.json text
*.yml text
*.xml text
*.slnx text
*.snk binary
*.csproj text
*.cs text eol=crlf
*.props text
*.targets text
*.csproj text
*.cs text eol=crlf
*.props text
*.targets text
*.manifest text
*.rc2 text
*.wixproj text
*.wxs text
+10 -4
View File
@@ -52,12 +52,18 @@ jobs:
dotnet restore
dotnet restore Installer
- name: Build
- name: Build x64
run: |
dotnet build --configuration Release --no-restore --property:Platform=x64 --binaryLogger:build-x64.binlog
dotnet build --configuration Release --no-restore --property:Platform=ARM64 Usbipd --binaryLogger:build-arm64.binlog
dotnet build --configuration Release --no-restore --property:Platform=x64 `
--binaryLogger:build-x64.binlog
- name: Test
- name: Build arm64
run: |
dotnet build --configuration Release --no-restore --property:Platform=ARM64 --property:BuildProjectReferences=false Usbipd `
--binaryLogger:build-arm64.binlog
# NOTE: Only build Usbipd for arm64. All platform-independent projects have already been built.
- name: Test (x64 only)
run: |
dotnet test --configuration Release --no-build --property:Platform=x64 `
--report-spekt-junit `
+1 -1
View File
@@ -27,7 +27,7 @@ SPDX-License-Identifier: GPL-3.0-only
<NuGetAuditLevel>low</NuGetAuditLevel>
<NuGetAuditMode>all</NuGetAuditMode>
<!-- This allows building on Linux (required for Dependabot); however, building the installer requires Windows -->
<!-- This allows NuGet restore on Linux (required for Dependabot); however, building requires Windows -->
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<!-- Common defaults -->
+3
View File
@@ -22,5 +22,8 @@ SPDX-License-Identifier: GPL-3.0-only
<!-- UnitTests -->
<PackageVersion Include="JunitXml.TestLogger" Version="8.0.0" />
<PackageVersion Include="Moq" Version="4.20.72" />
<!-- Win32ResourceGenerator -->
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.6.0" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.2526" />
</ItemGroup>
</Project>
+8
View File
@@ -7,6 +7,7 @@ SPDX-License-Identifier: GPL-3.0-only
<Wix
xmlns="http://wixtoolset.org/schemas/v4/wxs"
xmlns:fw="http://wixtoolset.org/schemas/v4/wxs/firewall"
xmlns:util="http://wixtoolset.org/schemas/v4/wxs/util"
>
<Fragment>
<DirectoryRef Id="APPLICATIONFOLDER">
@@ -92,6 +93,12 @@ SPDX-License-Identifier: GPL-3.0-only
<Component Id="COPYING.md">
<File Id="COPYING.md" Name="COPYING.md" />
</Component>
<Component Id="EventManifest.xml">
<File Id="EventManifest.xml" Name="EventManifest.xml">
<util:EventManifest MessageFile="[#usbipd.exe]" ResourceFile="[#usbipd.exe]" />
</File>
</Component>
</DirectoryRef>
<!-- See: https://github.com/dorssel/usbipd-win/wiki/Installer-design -->
@@ -197,6 +204,7 @@ SPDX-License-Identifier: GPL-3.0-only
<ComponentGroup Id="usbipd">
<ComponentRef Id="COPYING.md" />
<ComponentRef Id="usbipd.exe" />
<ComponentRef Id="EventManifest.xml" />
</ComponentGroup>
</Fragment>
</Wix>
+26 -37
View File
@@ -2,8 +2,6 @@
//
// SPDX-License-Identifier: GPL-3.0-only
using System.Diagnostics;
using Microsoft.Extensions.Hosting.WindowsServices;
using static Usbipd.ConsoleTools;
namespace Usbipd;
@@ -34,43 +32,34 @@ sealed partial class CommandHandlers : ICommandHandlers
using var host = Host.CreateDefaultBuilder()
.UseWindowsService()
.ConfigureAppConfiguration((context, builder) =>
{
var defaultConfig = new Dictionary<string, string?>();
if (WindowsServiceHelpers.IsWindowsService())
{
// EventLog defaults to Warning, which is OK for .NET components,
// but we want to specifically log Information from our own component.
defaultConfig.Add($"Logging:EventLog:LogLevel:{nameof(Usbipd)}", "Information");
}
else
{
// When not running as a Windows service, do not spam the EventLog.
defaultConfig.Add("Logging:EventLog:LogLevel:Default", "None");
}
// set the above as defaults
_ = builder.AddInMemoryCollection(defaultConfig);
.ConfigureAppConfiguration((context, builder) => _ = builder
// set the defaults
.AddInMemoryCollection(new Dictionary<string, string?>
{
// Our ETW logger only logs explicit LoggerMessages (and not ASP.NET and what not).
{ $"Logging:Etw:LogLevel:Default", "None" },
// It is smart enough to only log events when a listener is attached.
{ $"Logging:Etw:LogLevel:{nameof(Usbipd)}", "Trace" }
})
// allow overrides from the environment
_ = builder.AddEnvironmentVariables();
.AddEnvironmentVariables()
// allow overrides from the command line
_ = builder.AddCommandLine(args);
})
.ConfigureLogging((context, logging) =>
{
if (!EventLog.SourceExists(Program.Product))
{
EventLog.CreateEventSource(Program.Product, "Application");
}
_ = logging.AddEventLog(settings => settings.SourceName = Program.Product);
})
.ConfigureServices((hostContext, services) =>
{
_ = services.AddHostedService<Server>();
_ = services.AddSingleton<PcapNg>();
_ = services.AddScoped<ClientContext>();
_ = services.AddScoped<ConnectedClient>();
_ = services.AddScoped<AttachedClient>();
})
.AddCommandLine(args)
)
.ConfigureLogging((context, logging) => _ = logging
// The default builder also adds an EventLog provider, but we don't want that.
.ClearProviders()
.AddConsole()
.AddDebug()
.AddEtwLogger()
)
.ConfigureServices((hostContext, services) => _ = services
.AddHostedService<Server>()
.AddSingleton<PcapNg>()
.AddScoped<ClientContext>()
.AddScoped<ConnectedClient>()
.AddScoped<AttachedClient>()
)
.Build();
await host.RunAsync(cancellationToken);
+130
View File
@@ -0,0 +1,130 @@
// SPDX-FileCopyrightText: 2026 Frans van Dorsselaer
//
// SPDX-License-Identifier: GPL-3.0-only
using System.Diagnostics.Eventing.Reader;
using Windows.Win32;
using Windows.Win32.System.Diagnostics.Etw;
namespace Usbipd;
sealed class EtwLogger : ILogger
{
readonly REGHANDLE RegistrationHandle;
public EtwLogger(REGHANDLE registrationHandle)
{
RegistrationHandle = registrationHandle;
}
public IDisposable? BeginScope<TState>(TState state) where TState : notnull
{
return null;
}
public bool IsEnabled(LogLevel logLevel)
{
return RegistrationHandle != default && logLevel switch
{
LogLevel.Critical => PInvoke.EventProviderEnabled(RegistrationHandle, (byte)StandardEventLevel.Critical, 0x8000000000000000)
|| PInvoke.EventProviderEnabled(RegistrationHandle, (byte)StandardEventLevel.Critical, 0x4000000000000000),
LogLevel.Error => PInvoke.EventProviderEnabled(RegistrationHandle, (byte)StandardEventLevel.Error, 0x8000000000000000)
|| PInvoke.EventProviderEnabled(RegistrationHandle, (byte)StandardEventLevel.Error, 0x4000000000000000),
LogLevel.Warning => PInvoke.EventProviderEnabled(RegistrationHandle, (byte)StandardEventLevel.Warning, 0x8000000000000000)
|| PInvoke.EventProviderEnabled(RegistrationHandle, (byte)StandardEventLevel.Warning, 0x4000000000000000),
LogLevel.Information => PInvoke.EventProviderEnabled(RegistrationHandle, (byte)StandardEventLevel.Informational, 0x8000000000000000)
|| PInvoke.EventProviderEnabled(RegistrationHandle, (byte)StandardEventLevel.Informational, 0x4000000000000000),
LogLevel.Debug or LogLevel.Trace => (bool)PInvoke.EventProviderEnabled(RegistrationHandle, (byte)StandardEventLevel.Verbose, 0x4000000000000000),
LogLevel.None or _ => false,
};
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
if ((RegistrationHandle == default) || !IsEnabled(logLevel))
{
return;
}
// We want to call the formatter at most once.
string? message = null;
// Log to the Operational channel, but only events that are not specifically Debug or Trace level.
if (logLevel is not (LogLevel.Debug or LogLevel.Trace))
{
var descriptor = new EVENT_DESCRIPTOR
{
Id = (ushort)eventId.Id, // This works for Event Viewer
Version = 0, // Not used by us
Channel = 16, // Must be 16 to match the manifest Operational channel
Level = (byte)(logLevel switch
{
LogLevel.Critical => StandardEventLevel.Critical,
LogLevel.Error => StandardEventLevel.Error,
LogLevel.Warning => StandardEventLevel.Warning,
_ => StandardEventLevel.Informational
}),
Opcode = 0,
Task = 0,
Keyword = 0x8000000000000000
};
if (PInvoke.EventEnabled(RegistrationHandle, descriptor))
{
message = formatter(state, exception);
unsafe // DevSkim: ignore DS172412
{
fixed (char* pMessageChars = message)
{
Span<EVENT_DATA_DESCRIPTOR> dataDescriptors = [
new EVENT_DATA_DESCRIPTOR {
Ptr = (ulong)pMessageChars,
Size = (uint)((message.Length + 1) * sizeof(char)),
Reserved = 0
},
];
_ = PInvoke.EventWrite(RegistrationHandle, descriptor, dataDescriptors);
}
}
}
}
// Log to the Debug channel.
{
var descriptor = new EVENT_DESCRIPTOR
{
Id = (ushort)eventId.Id, // This works for Event Viewer
Version = 0, // Not used by us
Channel = 17, // Must be 17 to match the manifest Debug channel
Level = (byte)(logLevel switch
{
LogLevel.Critical => StandardEventLevel.Critical,
LogLevel.Error => StandardEventLevel.Error,
LogLevel.Warning => StandardEventLevel.Warning,
LogLevel.Information => StandardEventLevel.Informational,
_ => StandardEventLevel.Verbose
}),
Opcode = 0,
Task = 0,
Keyword = 0x4000000000000000
};
if (PInvoke.EventEnabled(RegistrationHandle, descriptor))
{
message ??= formatter(state, exception);
unsafe // DevSkim: ignore DS172412
{
fixed (char* pMessageChars = message)
{
Span<EVENT_DATA_DESCRIPTOR> dataDescriptors = [
new EVENT_DATA_DESCRIPTOR {
Ptr = (ulong)pMessageChars,
Size = (uint)((message.Length + 1) * sizeof(char)),
Reserved = 0
},
];
_ = PInvoke.EventWrite(RegistrationHandle, descriptor, dataDescriptors);
}
}
}
}
}
}
+16
View File
@@ -0,0 +1,16 @@
// SPDX-FileCopyrightText: 2026 Frans van Dorsselaer
//
// SPDX-License-Identifier: GPL-3.0-only
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Usbipd;
static class EtwLoggerExtensions
{
public static ILoggingBuilder AddEtwLogger(this ILoggingBuilder builder)
{
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, EtwLoggerProvider>());
return builder;
}
}
+51
View File
@@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: 2026 Frans van Dorsselaer
//
// SPDX-License-Identifier: GPL-3.0-only
using Windows.Win32;
using Windows.Win32.System.Diagnostics.Etw;
namespace Usbipd;
[ProviderAlias("Etw")]
sealed class EtwLoggerProvider : ILoggerProvider
{
REGHANDLE RegistrationHandle;
public EtwLoggerProvider()
{
// The GUID is derived from the provider name "usbipd" using the algorithm described in
// https://learn.microsoft.com/en-us/windows/win32/etw/guid-generation-algorithm
unsafe // DevSkim: ignore DS172412
{
var result = PInvoke.EventRegister(new("{766A7E49-AC4D-5405-97C9-C9A2F7C4C458}"), null, null, out RegistrationHandle);
if (result != 0)
{
RegistrationHandle = default;
}
}
}
public ILogger CreateLogger(string categoryName)
{
return new EtwLogger(RegistrationHandle);
}
bool IsDisposed;
public void Dispose()
{
if (!IsDisposed)
{
if (RegistrationHandle != default)
{
unsafe // DevSkim: ignore DS172412
{
_ = PInvoke.EventUnregister(RegistrationHandle);
RegistrationHandle = default;
}
}
IsDisposed = true;
}
}
}
+7 -7
View File
@@ -9,24 +9,24 @@ namespace Usbipd;
static partial class LogEvents
{
[LoggerMessage(EventId = 1, Level = LogLevel.Information, Message = "Client {clientAddress} claimed device at {busId} ({instanceId}).")]
[LoggerMessage(1, LogLevel.Information, "Client {clientAddress} claimed device at {busId} ({instanceId}).")]
public static partial void ClientAttach(this ILogger logger, IPAddress clientAddress, BusId busId, string instanceId);
[LoggerMessage(EventId = 2, Level = LogLevel.Information, Message = "Client {clientAddress} released device at {busId} ({instanceId}).")]
[LoggerMessage(2, LogLevel.Information, "Client {clientAddress} released device at {busId} ({instanceId}).")]
public static partial void ClientDetach(this ILogger logger, IPAddress clientAddress, BusId busId, string instanceId);
[LoggerMessage(EventId = 3, Level = LogLevel.Error, Message = "An exception occurred while communicating with the client:")]
[LoggerMessage(3, LogLevel.Error, "An exception occurred while communicating with the client.")]
public static partial void ClientError(this ILogger logger, Exception ex);
[LoggerMessage(EventId = 4, Level = LogLevel.Error, Message = "An internal error occurred: {text}")]
[LoggerMessage(4, LogLevel.Error, "An internal error occurred: {text}")]
public static partial void InternalError(this ILogger logger, string text, Exception? ex = null);
[LoggerMessage(EventId = 5, Level = LogLevel.Information, Message = "Auto-bind of device at {busId} ({instanceId}) by client {clientAddress}.")]
[LoggerMessage(5, LogLevel.Information, "Auto-bind of device at {busId} ({instanceId}) by client {clientAddress}.")]
public static partial void AutoBind(this ILogger logger, IPAddress clientAddress, BusId busId, string instanceId);
[LoggerMessage(EventId = 1000, Level = LogLevel.Debug, Message = "{text}")]
[LoggerMessage(1000, LogLevel.Debug, "{text}")]
public static partial void Debug(this ILogger logger, string text);
[LoggerMessage(EventId = 1001, Level = LogLevel.Trace, Message = "{text}")]
[LoggerMessage(1001, LogLevel.Trace, "{text}")]
public static partial void Trace(this ILogger logger, string text);
}
+6
View File
@@ -131,3 +131,9 @@ IOCTL_USB_HUB_CYCLE_PORT
// USB_NODE_CONNECTION_INFORMATION_EX
USB_NODE_CONNECTION_INFORMATION_EX_V2
USB_CYCLE_PORT_PARAMS
EventEnabled
EventProviderEnabled
EventRegister
EventUnregister
EventWrite
+45 -10
View File
@@ -11,7 +11,6 @@ SPDX-License-Identifier: GPL-3.0-only
<PropertyGroup>
<TargetFramework>$(MainTargetFramework)</TargetFramework>
<OutputType>Exe</OutputType>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
@@ -23,6 +22,25 @@ SPDX-License-Identifier: GPL-3.0-only
<AssemblyName>usbipd</AssemblyName>
</PropertyGroup>
<ItemGroup>
<CompilerVisibleProperty Include="ProjectDir" />
<CompilerVisibleProperty Include="IntermediateOutputPath" />
<CompilerVisibleProperty Include="Configuration" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Win32ResourceGenerator\Win32ResourceGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false">
<SetPlatform>Platform=AnyCPU</SetPlatform>
<SetConfiguration>Configuration=Release</SetConfiguration>
</ProjectReference>
</ItemGroup>
<Target Name="SetWin32Resource" BeforeTargets="BeforeCompile">
<PropertyGroup>
<Win32Resource>$(IntermediateOutputPath)generated\Win32ResourceGenerator\Resources.res</Win32Resource>
</PropertyGroup>
</Target>
<ItemGroup>
<Compile Include="..\Usbipd.Automation\*.cs" LinkBase="Usbipd.Automation" />
</ItemGroup>
@@ -47,25 +65,42 @@ SPDX-License-Identifier: GPL-3.0-only
<PackageReference Include="System.CommandLine" />
</ItemGroup>
<ItemGroup>
<!-- (generated) event provider manifest -->
<Content
Include="$(IntermediateOutputPath)generated\Win32ResourceGenerator\EventManifest.xml"
Link="EventManifest.xml"
CopyToOutputDirectory="PreserveNewest"
/>
</ItemGroup>
<Target Name="CopyManifestOnPublishNoBuild" AfterTargets="Publish">
<Copy
SourceFiles="$(IntermediateOutputPath)generated\Win32ResourceGenerator\EventManifest.xml"
DestinationFiles="$(PublishDir)EventManifest.xml"
SkipUnchangedFiles="true"
/>
</Target>
<ItemGroup>
<!-- WSL -->
<!-- Copy everything at the top level only, except the REUSE .license files (if any). -->
<None Update="WSL\*" CopyToOutputDirectory="PreserveNewest" />
<None Remove="WSL\*\*" />
<None Remove="WSL\*.license" />
<None Remove="WSL\**\*" />
<Content Include="WSL\*" CopyToOutputDirectory="PreserveNewest" />
<Content Remove="WSL\*.license" />
<!-- Also copy everything in the platform directory moving it to the top level, except the REUSE .license files (if any). -->
<None Include="WSL\$(Platform)\*" Link="WSL\%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" />
<None Remove="WSL\$(Platform)\*.license" />
<Content Include="WSL\$(Platform)\*" Link="WSL\%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" />
<Content Remove="WSL\$(Platform)\*.license" />
</ItemGroup>
<ItemGroup>
<!-- Drivers -->
<!-- Copy everything at the top level only, except the REUSE .license files (if any). -->
<None Include="..\Drivers\*" Link="Drivers\%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" />
<None Remove="..\Drivers\*.license" />
<Content Include="..\Drivers\*" Link="Drivers\%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" />
<Content Remove="..\Drivers\*.license" />
<!-- Also copy everything in the platform directory moving it to the top level, except the REUSE .license files (if any). -->
<None Include="..\Drivers\$(Platform)\*" Link="Drivers\%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" />
<None Remove="..\Drivers\$(Platform)\*.license" />
<Content Include="..\Drivers\$(Platform)\*" Link="Drivers\%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" />
<Content Remove="..\Drivers\$(Platform)\*.license" />
</ItemGroup>
</Project>
+1 -7
View File
@@ -1,9 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
SPDX-FileCopyrightText: 2020 Frans van Dorsselaer
SPDX-License-Identifier: GPL-3.0-only
-->
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
@@ -16,7 +11,6 @@ SPDX-License-Identifier: GPL-3.0-only
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
+7
View File
@@ -0,0 +1,7 @@
SPDX-FileCopyrightText: 2020 Frans van Dorsselaer
SPDX-License-Identifier: GPL-3.0-only
NOTE:
The application manifest is shipped as part of the binary release, which is covered by the COPYING file.
The SPDX tags are for source licenses only.
+10
View File
@@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2026 Frans van Dorsselaer
//
// SPDX-License-Identifier: GPL-3.0-only
namespace Win32ResourceGenerator;
record struct BuildOptions(
string ProjectDir,
string IntermediateOutputPath,
string Configuration);
+40
View File
@@ -0,0 +1,40 @@
// SPDX-FileCopyrightText: 2026 Frans van Dorsselaer
//
// SPDX-License-Identifier: GPL-3.0-only
using Microsoft.CodeAnalysis;
namespace Win32ResourceGenerator;
static class DiagnosticRules
{
const string Category = "Win32ResourceGenerator.LoggerMessage";
#pragma warning disable RS2008 // Enable analyzer release tracking
public static readonly DiagnosticDescriptor DuplicateIdRule = new("EM001", "Duplicate EventId detected",
"The EventId '{0}' is already used by method '{1}'. Event IDs must be unique across the project.",
Category, DiagnosticSeverity.Error, true);
public static readonly DiagnosticDescriptor MissingPropertyRule = new("EM002", "Property missing",
"The property '{0}' is missing",
Category, DiagnosticSeverity.Error, true);
public static readonly DiagnosticDescriptor DuplicatePropertyRule = new("EM003", "Duplicate Property",
"The property '{0}' is specified more than once. Properties should be set either by constructor arguments or by named arguments, but not both.",
Category, DiagnosticSeverity.Error, true);
public static readonly DiagnosticDescriptor InvalidEventIdRule = new("EM004", "Invalid EventId",
"The EventId '{0}' is not valid. Event IDs must be between 1 and 65535, id 0 is not allowed.",
Category, DiagnosticSeverity.Error, true);
public static readonly DiagnosticDescriptor InvalidLevelRule = new("EM005", "Invalid Level",
"The level '{0}' is not valid. Levels must be between LogLevel.Trace and LogLevel.Critical, LogLevel.None is not supported.",
Category, DiagnosticSeverity.Error, true);
public static readonly DiagnosticDescriptor InvalidMessageRule = new("EM006", "Invalid Message",
"The message '{0}' is not valid. Messages must be non-empty and non-whitespace.",
Category, DiagnosticSeverity.Error, true);
#pragma warning restore RS2008 // Enable analyzer release tracking
}
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<instrumentationManifest xmlns="{{DefaultXmlNs}}" xmlns:win="{{WinXmlNs}}">
<instrumentation>
<events>
<provider name="{{ProviderName}}" guid="{{ProviderGuid}}" symbol="P" resourceFileName="usbipd.exe" messageFileName="usbipd.exe" message="$(string.Provider.Name)">
<channels>
<channel name="{{ProviderName}}/Operational" chid="O" type="Operational" enabled="true" message="$(string.Channel.Operational.Name)" />
<channel name="{{ProviderName}}/Debug" chid="D" type="Debug" enabled="false" message="$(string.Channel.Debug.Name)" />
</channels>
<templates>
<template tid="T">
<data name="Message" inType="win:UnicodeString"/>
</template>
</templates>
<events>
{{eventsXml}}
</events>
</provider>
</events>
</instrumentation>
<localization>
<resources culture="en-US">
<stringTable>
<string id="Provider.Name" value="{{ProviderName}}" />
<string id="Channel.Operational.Name" value="Operational" />
<string id="Channel.Debug.Name" value="Debug" />
<string id="M" value="%1" />
</stringTable>
</resources>
</localization>
</instrumentationManifest>
@@ -0,0 +1,7 @@
SPDX-FileCopyrightText: 2026 Frans van Dorsselaer
SPDX-License-Identifier: GPL-3.0-only
NOTE:
The generated EventManifest.xml generated from this template is shipped as part of the binary release, which is covered
by the COPYING file. The SPDX tags are for source licenses only.
@@ -0,0 +1,26 @@
// SPDX-FileCopyrightText: 2026 Frans van Dorsselaer
//
// SPDX-License-Identifier: GPL-3.0-only
#ifdef APSTUDIO_INVOKED
#error This file is not editable with the visual studio resource editor.
#endif
// Instead of pulling in the entire Windows SDK, we can just define the constants we need here.
#define LANG_NEUTRAL 0x00
#define SUBLANG_NEUTRAL 0x00
#define RT_MANIFEST 24
// app.manifest is provided by the main (compiling) project.
LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL
1 RT_MANIFEST "app.manifest"
// This we generate ourselves from VersionInfoTemplate.rc2.
#include "VersionInfo.rc"
// This one is generated by mc.exe, after we generated EventManifest.xml from EventManifestTemplate.xml.
#include "EventManifest.rc"
@@ -0,0 +1,40 @@
// SPDX-FileCopyrightText: 2026 Frans van Dorsselaer
//
// SPDX-License-Identifier: GPL-3.0-only
#ifdef APSTUDIO_INVOKED
#error This file is not editable with the visual studio resource editor.
#endif
LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL
VS_VERSION_INFO VERSIONINFO
FILEVERSION VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH, CI_BUILD_NUMBER
PRODUCTVERSION VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH, COMMITS_SINCE_VERSION_SOURCE
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
FILEFLAGS FILE_FLAGS
FILEOS VOS_NT_WINDOWS32
FILETYPE VFT_APP
FILESUBTYPE 0
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "{{BlockCulture}}"
BEGIN
VALUE "CompanyName", "{{CompanyName}}"
VALUE "FileDescription", "Utility for sharing locally connected USB devices to other machines"
VALUE "FileVersion", "{{FileVersion}}"
VALUE "InternalName", "usbipd"
VALUE "LegalCopyright", "{{LegalCopyright}}"
VALUE "OriginalFilename", "usbipd.exe"
VALUE "ProductName", "{{ProductName}}"
VALUE "ProductVersion", "{{ProductVersion}}"
{{OptionalPrivateBuild}}
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", {{LCID}}, {{CodePage}}
END
END
+300
View File
@@ -0,0 +1,300 @@
// SPDX-FileCopyrightText: 2026 Frans van Dorsselaer
//
// SPDX-License-Identifier: GPL-3.0-only
using System.Diagnostics;
using System.Diagnostics.Tracing;
using System.Globalization;
using System.Reflection;
using System.Text;
using Microsoft.CodeAnalysis;
using Windows.Win32;
using Windows.Win32.Storage.FileSystem;
namespace Win32ResourceGenerator;
[Generator]
public class Generator : IIncrementalGenerator
{
readonly string WindowsSDKBuildToolsBinVersionedArchFolder;
readonly byte[] Resources;
readonly FileVersionInfo FileVersionInfo;
readonly string EventManifestTemplate;
readonly string VersionInfoTemplate;
const string DefaultXmlNs = "http://schemas.microsoft.com/win/2004/08/events"; // DevSkim: ignore DS137138
const string WinXmlNs = "http://manifests.microsoft.com/win/2004/08/windows/events"; // DevSkim: ignore DS137138
const string ProviderName = "usbipd";
readonly string ProviderGuid;
public Generator()
{
// Initialize some constants.
{
// The provider GUID is generated based on the provider name.
using var eventSource = new EventSource(ProviderName);
ProviderGuid = eventSource.Guid.ToString("B").ToUpperInvariant();
}
// Get some embedded data from the generator assembly.
{
var assembly = typeof(Generator).Assembly;
// MSBuild properties of the source generator project are embedded as custom attributes.
{
var metadata = assembly.GetCustomAttributes<AssemblyMetadataAttribute>();
WindowsSDKBuildToolsBinVersionedArchFolder = metadata.Single(a => a.Key == "WindowsSDKBuildToolsBinVersionedArchFolder").Value;
}
// (Template) resource files are embedded as resources.
{
var resourceNames = assembly.GetManifestResourceNames();
{
using var stream = assembly.GetManifestResourceStream(resourceNames.Single(r => r.EndsWith(".Resources.rc2")));
using var memoryStream = new MemoryStream();
stream.CopyTo(memoryStream);
Resources = memoryStream.ToArray();
}
{
using var stream = assembly.GetManifestResourceStream(resourceNames.Single(r => r.EndsWith(".EventManifestTemplate.xml")));
using var reader = new StreamReader(stream);
EventManifestTemplate = reader.ReadToEnd();
}
{
using var stream = assembly.GetManifestResourceStream(resourceNames.Single(r => r.EndsWith(".VersionInfoTemplate.rc2")));
using var reader = new StreamReader(stream);
VersionInfoTemplate = reader.ReadToEnd();
}
}
// Our own FileVersionInfo is used to generate the VersionInfo.rc file.
FileVersionInfo = FileVersionInfo.GetVersionInfo(assembly.Location);
}
}
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// We need to filter out generated sources, which either have no file path or end with .g.cs.
var loggerMessages = context.SyntaxProvider.ForAttributeWithMetadataName(
"Microsoft.Extensions.Logging.LoggerMessageAttribute",
(syntaxNode, _) =>
!string.IsNullOrEmpty(syntaxNode.SyntaxTree.FilePath)
&& !syntaxNode.SyntaxTree.FilePath.EndsWith(".g.cs", StringComparison.OrdinalIgnoreCase),
(context, _) => LoggerMessage.Create(context));
var buildOptions = context.AnalyzerConfigOptionsProvider.Select((provider, ct) =>
{
_ = provider.GlobalOptions.TryGetValue("build_property.ProjectDir", out var projectDir);
_ = provider.GlobalOptions.TryGetValue("build_property.intermediateOutputPath", out var intermediateOutputPath);
_ = provider.GlobalOptions.TryGetValue("build_property.Configuration", out var configuration);
return new BuildOptions(projectDir ?? string.Empty, intermediateOutputPath ?? string.Empty, configuration ?? string.Empty);
});
var loggerMessagesWithOptions = loggerMessages.Collect().Combine(buildOptions);
context.RegisterSourceOutput(loggerMessagesWithOptions, (context, loggerMessagesWithOptions) =>
{
var loggerMessages = loggerMessagesWithOptions.Left;
var buildOptions = loggerMessagesWithOptions.Right;
var seenIds = new Dictionary<int, LoggerMessage>();
var hasErrors = false;
foreach (var loggerMessage in loggerMessages)
{
if (loggerMessage.ReportDiagnostics(context))
{
hasErrors = true;
}
else if (seenIds.TryGetValue(loggerMessage.Id, out var existingMethod))
{
context.ReportDiagnostic(Diagnostic.Create(DiagnosticRules.DuplicateIdRule, loggerMessage.Location,
loggerMessage.Id, existingMethod.MethodName));
hasErrors = true;
}
else
{
seenIds[loggerMessage.Id] = loggerMessage;
}
}
if (hasErrors)
{
return;
}
var eventsXml = new StringBuilder();
foreach (var loggerMessage in loggerMessages)
{
var isDebug = loggerMessage.Level == EventLevel.Verbose;
_ = eventsXml.AppendLine(
$" <event value=\"{loggerMessage.Id}\" channel=\"{(isDebug ? 'D' : 'O')}\" template=\"T\" level=\"win:{loggerMessage.Level}\" message=\"$(string.M)\" />"
);
}
var eventManifestXml = EventManifestTemplate
.Replace($"{{{{{nameof(DefaultXmlNs)}}}}}", DefaultXmlNs)
.Replace($"{{{{{nameof(WinXmlNs)}}}}}", WinXmlNs)
.Replace($"{{{{{nameof(ProviderName)}}}}}", ProviderName)
.Replace($"{{{{{nameof(ProviderGuid)}}}}}", ProviderGuid)
.Replace($"{{{{{nameof(eventsXml)}}}}}", eventsXml.ToString().Trim())
;
var eventManifestBytes = new UTF8Encoding(false).GetBytes(eventManifestXml);
var generatorOutputDir = Path.Combine(
buildOptions.ProjectDir,
buildOptions.IntermediateOutputPath,
"generated",
nameof(Win32ResourceGenerator));
var eventManifestPath = Path.Combine(generatorOutputDir, "EventManifest.xml");
_ = Directory.CreateDirectory(generatorOutputDir);
var manifestChanged = UpdateIfContentChanged(eventManifestPath, eventManifestBytes);
if (manifestChanged)
{
var startInfo = new ProcessStartInfo
{
FileName = Path.Combine(WindowsSDKBuildToolsBinVersionedArchFolder, "mc.exe"),
Arguments = "-um EventManifest.xml",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
WorkingDirectory = generatorOutputDir,
};
using var process = Process.Start(startInfo) ?? throw new InvalidOperationException("Failed to start the process.");
var output = process.StandardOutput.ReadToEnd();
var error = process.StandardError.ReadToEnd();
process.WaitForExit();
if (process.ExitCode != 0)
{
throw new InvalidOperationException($"mc.exe error (Code {process.ExitCode}): {error}");
}
}
_ = UpdateIfContentChanged(Path.Combine(generatorOutputDir, "Resources.rc"), Resources);
var applicationManifest = File.ReadAllBytes(Path.Combine(buildOptions.ProjectDir, "app.manifest"));
_ = UpdateIfContentChanged(Path.Combine(generatorOutputDir, "app.manifest"), applicationManifest);
_ = GenerateVersionInfo(generatorOutputDir, buildOptions.Configuration == "Release");
if (IsUpdateRequired(generatorOutputDir,
["Resources.rc", "app.manifest", "VersionInfo.rc", "EventManifest.rc", "EventManifestTEMP.BIN", "MSG00001.bin"],
["Resources.res"]))
{
var startInfo = new ProcessStartInfo
{
FileName = Path.Combine(WindowsSDKBuildToolsBinVersionedArchFolder, "rc.exe"),
Arguments = "/NoLogo /8 Resources.rc",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
WorkingDirectory = generatorOutputDir,
};
using var process = Process.Start(startInfo) ?? throw new InvalidOperationException("Failed to start the process.");
var output = process.StandardOutput.ReadToEnd();
var error = process.StandardError.ReadToEnd();
process.WaitForExit();
if (process.ExitCode != 0)
{
throw new InvalidOperationException($"rc.exe error (Code {process.ExitCode}): {error}");
}
}
});
}
static bool IsUpdateRequired(string generatorOutputDir, string[] inputs, string[] outputs)
{
var outputFiles = outputs.Select(output => Path.Combine(generatorOutputDir, output)).ToArray();
var inputFiles = inputs.Select(input => Path.Combine(generatorOutputDir, input)).ToArray();
if (outputFiles.Any(outputFile => !File.Exists(outputFile)))
{
return true;
}
var outputLastWriteTime = outputFiles.Max(outputFile => File.GetLastWriteTimeUtc(outputFile));
var inputLastWriteTime = inputFiles.Max(inputFile => File.GetLastWriteTimeUtc(inputFile));
return inputLastWriteTime > outputLastWriteTime;
}
static bool UpdateIfContentChanged(string filePath, byte[] newContent)
{
if (File.Exists(filePath))
{
var currentContent = File.ReadAllBytes(filePath);
if (currentContent.SequenceEqual(newContent))
{
return false;
}
}
File.WriteAllBytes(filePath, newContent);
return true;
}
bool GenerateVersionInfo(string generatorOutputDir, bool release)
{
var culture = new CultureInfo("en-US", false);
var versionInfoPath = Path.Combine(generatorOutputDir, "VersionInfo.rc");
var version = new Version(GitVersionInformation.AssemblySemFileVer);
VS_FIXEDFILEINFO_FILE_FLAGS fileFlags = 0;
string? privateBuild = null;
if (!release)
{
fileFlags |= VS_FIXEDFILEINFO_FILE_FLAGS.VS_FF_DEBUG | VS_FIXEDFILEINFO_FILE_FLAGS.VS_FF_PRIVATEBUILD;
privateBuild = "This is an unofficial (debug) build.";
}
else if (version.Revision == 0)
{
fileFlags |= VS_FIXEDFILEINFO_FILE_FLAGS.VS_FF_PRIVATEBUILD;
privateBuild = "This is an unofficial (non-CI) build.";
}
privateBuild = privateBuild is not null
? $"""
VALUE "PrivateBuild", "{privateBuild}"
"""
: string.Empty;
var versionInfoContent = VersionInfoTemplate
.Replace("VS_VERSION_INFO", $"{PInvoke.VS_VERSION_INFO}")
.Replace("VERSION_MAJOR", $"{version.Major}")
.Replace("VERSION_MINOR", $"{version.Minor}")
.Replace("VERSION_PATCH", $"{version.Build}")
.Replace("CI_BUILD_NUMBER", $"{version.Revision}")
.Replace("COMMITS_SINCE_VERSION_SOURCE", $"{GitVersionInformation.CommitsSinceVersionSource}")
.Replace("VOS_NT_WINDOWS32", $"0x{(uint)VS_FIXEDFILEINFO_FILE_OS.VOS_NT_WINDOWS32:x8}L")
.Replace("VFT_APP", $"0x{(uint)VS_FIXEDFILEINFO_FILE_TYPE.VFT_APP:x8}L")
.Replace("VS_FFI_FILEFLAGSMASK", $"0x{PInvoke.VS_FFI_FILEFLAGSMASK:x8}L")
.Replace("FILE_FLAGS", $"0x{(uint)fileFlags:x8}L")
.Replace("{{OptionalPrivateBuild}}", privateBuild)
.Replace("{{BlockCulture}}", $"{culture.LCID:x4}{Encoding.Unicode.CodePage:x4}")
.Replace("{{CompanyName}}", $"{FileVersionInfo.CompanyName}")
.Replace("{{FileVersion}}", $"{FileVersionInfo.FileVersion}")
.Replace("{{LegalCopyright}}", $"{FileVersionInfo.LegalCopyright}")
.Replace("{{ProductName}}", $"{FileVersionInfo.ProductName}")
.Replace("{{ProductVersion}}", $"{FileVersionInfo.ProductVersion}")
.Replace("{{LCID}}", $"{culture.LCID}")
.Replace("{{CodePage}}", $"{Encoding.Unicode.CodePage}")
;
var versionInfoBytes = new UTF8Encoding(false).GetBytes(versionInfoContent);
return UpdateIfContentChanged(versionInfoPath, versionInfoBytes);
}
}
@@ -0,0 +1,7 @@
// SPDX-FileCopyrightText: 2026 Frans van Dorsselaer
//
// SPDX-License-Identifier: GPL-3.0-only
using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("MicrosoftCodeAnalysisCorrectness", "RS1035:Do not use APIs banned for analyzers", Justification = "We have to, since we output binary files.")]
+168
View File
@@ -0,0 +1,168 @@
// SPDX-FileCopyrightText: 2026 Frans van Dorsselaer
//
// SPDX-License-Identifier: GPL-3.0-only
using System.Diagnostics.Tracing;
using Microsoft.CodeAnalysis;
namespace Win32ResourceGenerator;
public class LoggerMessage
{
LoggerMessage(string methodName, Location location,
int? constructorId, int? constructorLevel, string? constructorMessage,
int? namedId, int? namedLevel, string? namedMessage)
{
MethodName = methodName;
Location = location;
ConstructorId = constructorId;
ConstructorLevel = constructorLevel;
ConstructorMessage = constructorMessage;
NamedId = namedId;
NamedLevel = namedLevel;
NamedMessage = namedMessage;
}
public string MethodName { get; }
public Location Location { get; }
readonly int? ConstructorId;
readonly int? ConstructorLevel;
readonly string? ConstructorMessage;
readonly int? NamedId;
readonly int? NamedLevel;
readonly string? NamedMessage;
public bool IsValid { get; private set; }
public ushort Id => IsValid ? (ushort)(ConstructorId ?? NamedId)! : throw new InvalidOperationException("Not valid");
public EventLevel Level => (IsValid ? (ConstructorLevel ?? NamedLevel)! : throw new InvalidOperationException("Not valid")) switch
{
// NOTE: We do not want to reference Microsoft.Extensions.Logging.Abstractions in this project, so we cannot use LogLevel enum directly.
5 /* LogLevel.Critical */ => EventLevel.Critical,
4 /* LogLevel.Error */ => EventLevel.Error,
3 /* LogLevel.Warning */ => EventLevel.Warning,
2 /* LogLevel.Information */ => EventLevel.Informational,
1 /* LogLevel.Debug */ => EventLevel.Verbose,
0 /* LogLevel.Trace */ => EventLevel.Verbose,
// NOTE: We cannot support LogLevel.None (6).
6 or _ => throw new InvalidOperationException("Not valid"),
};
public string Message => IsValid ? (ConstructorMessage ?? NamedMessage)! : throw new InvalidOperationException("Not valid");
static public LoggerMessage Create(GeneratorAttributeSyntaxContext context)
{
var diagnosticDescriptors = new List<DiagnosticDescriptor>();
int? constructorId = null;
int? constructorLevel = null;
string? constructorMessage = null;
var attribute = context.Attributes.Single();
foreach (var arg in attribute.ConstructorArguments)
{
switch (arg.Type?.ToDisplayString())
{
case "int":
constructorId = (int?)arg.Value;
break;
case "Microsoft.Extensions.Logging.LogLevel":
constructorLevel = (int?)arg.Value;
break;
case "string":
constructorMessage = (string?)arg.Value;
break;
}
}
int? namedId = null;
int? namedLevel = null;
string? namedMessage = null;
foreach (var arg in attribute.NamedArguments)
{
switch (arg.Key)
{
case "EventId":
namedId = (int)(arg.Value.Value ?? 0);
break;
case "Level":
namedLevel = (int?)arg.Value.Value;
break;
case "Message":
namedMessage = (string?)arg.Value.Value;
break;
}
}
// Decouple the location details from the syntax to allow caching and avoid holding onto syntax trees.
var syntaxLocation = context.TargetNode.GetLocation();
return new(context.TargetSymbol.Name,
Location.Create(syntaxLocation.SourceTree!.FilePath, syntaxLocation.SourceSpan, syntaxLocation.GetLineSpan().Span),
constructorId, constructorLevel, constructorMessage,
namedId, namedLevel, namedMessage);
}
public bool ReportDiagnostics(SourceProductionContext context)
{
var hasErrors = false;
if (ConstructorId is null && NamedId is null)
{
context.ReportDiagnostic(Diagnostic.Create(DiagnosticRules.MissingPropertyRule, Location, "EventId"));
hasErrors = true;
}
else if (ConstructorId is not null && NamedId is not null)
{
context.ReportDiagnostic(Diagnostic.Create(DiagnosticRules.DuplicatePropertyRule, Location, "EventId"));
hasErrors = true;
}
else if ((ConstructorId ?? NamedId) is <= 0 or > ushort.MaxValue)
{
context.ReportDiagnostic(Diagnostic.Create(DiagnosticRules.InvalidEventIdRule, Location, ConstructorId ?? NamedId));
hasErrors = true;
}
if (ConstructorLevel is null && NamedLevel is null)
{
context.ReportDiagnostic(Diagnostic.Create(DiagnosticRules.MissingPropertyRule, Location, "Level"));
hasErrors = true;
}
else if (ConstructorLevel is not null && NamedLevel is not null)
{
context.ReportDiagnostic(Diagnostic.Create(DiagnosticRules.DuplicatePropertyRule, Location, "Level"));
hasErrors = true;
}
else if ((ConstructorLevel ?? NamedLevel) is < 0 or > 5)
{
context.ReportDiagnostic(Diagnostic.Create(DiagnosticRules.InvalidLevelRule, Location, ConstructorLevel ?? NamedLevel));
hasErrors = true;
}
if (ConstructorMessage is null && NamedMessage is null)
{
context.ReportDiagnostic(Diagnostic.Create(DiagnosticRules.MissingPropertyRule, Location, "Message"));
hasErrors = true;
}
else if (ConstructorMessage is not null && NamedMessage is not null)
{
context.ReportDiagnostic(Diagnostic.Create(DiagnosticRules.DuplicatePropertyRule, Location, "Message"));
hasErrors = true;
}
else if (string.IsNullOrWhiteSpace(ConstructorMessage ?? NamedMessage))
{
context.ReportDiagnostic(Diagnostic.Create(DiagnosticRules.InvalidMessageRule, Location, ConstructorMessage ?? NamedMessage));
hasErrors = true;
}
IsValid = !hasErrors;
return hasErrors;
}
}
+14
View File
@@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: 2026 Frans van Dorsselaer
//
// SPDX-License-Identifier: GPL-3.0-only
LANG_ENGLISH
SUBLANG_ENGLISH_US
VS_VERSION_INFO
VS_FFI_FILEFLAGSMASK
VS_FIXEDFILEINFO_FILE_FLAGS
VS_FIXEDFILEINFO_FILE_OS
VS_FIXEDFILEINFO_FILE_TYPE
@@ -0,0 +1,9 @@
// SPDX-FileCopyrightText: 2026 Frans van Dorsselaer
//
// SPDX-License-Identifier: GPL-3.0-only
#pragma warning disable IDE0130 // Namespace does not match folder structure
namespace System.Runtime.CompilerServices;
#pragma warning restore IDE0130 // Namespace does not match folder structure
public class IsExternalInit { }
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
SPDX-FileCopyrightText: 2026 Frans van Dorsselaer
SPDX-License-Identifier: GPL-3.0-only
-->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<Configurations>Release</Configurations>
<Configuration>Release</Configuration>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
<PackageReference Include="Microsoft.Windows.CsWin32" PrivateAssets="all" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="EmbeddedResources\**" />
<EmbeddedResource Remove="EmbeddedResources\**\*.license" />
</ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
<_Parameter1>WindowsSDKBuildToolsBinVersionedArchFolder</_Parameter1>
<_Parameter2>$(WindowsSDKBuildToolsBinVersionedArchFolder)</_Parameter2>
</AssemblyAttribute>
</ItemGroup>
</Project>
+160
View File
@@ -0,0 +1,160 @@
{
"version": 2,
"dependencies": {
".NETStandard,Version=v2.0": {
"GitVersion.MsBuild": {
"type": "Direct",
"requested": "[6.8.2, )",
"resolved": "6.8.2",
"contentHash": "eTd8jUiogUXpVssFyDWWdMrPr5Cdc57tWaQ3Cv/eMIhnumOHZhDkc/wcy8hYU8aH7NS4VT2w/pHjVITDSTXMGw=="
},
"Microsoft.CodeAnalysis.CSharp": {
"type": "Direct",
"requested": "[5.6.0, )",
"resolved": "5.6.0",
"contentHash": "r1DrKQ/L0xTw03wJrLr36AMQNslyaeEKBFyFmQcOKa8HX3YvmhY//JEUafb6IR/m0gmaUVCfBTWitKJRNb7YAA==",
"dependencies": {
"Microsoft.CodeAnalysis.Analyzers": "5.3.0",
"Microsoft.CodeAnalysis.Common": "[5.6.0]",
"System.Buffers": "4.6.1",
"System.Collections.Immutable": "10.0.1",
"System.Memory": "4.6.3",
"System.Numerics.Vectors": "4.6.1",
"System.Reflection.Metadata": "10.0.1",
"System.Runtime.CompilerServices.Unsafe": "6.1.2",
"System.Text.Encoding.CodePages": "8.0.0",
"System.Threading.Tasks.Extensions": "4.6.3"
}
},
"Microsoft.Windows.CsWin32": {
"type": "Direct",
"requested": "[0.3.298, )",
"resolved": "0.3.298",
"contentHash": "moz/CojV5HSeJYV9pNVE6mWMKnJGvPgdD31EhBLyUJ8Ui06PBrH9oPFK1ljXbJJMM4EzKnkE1a0AtBbH1BHDGA==",
"dependencies": {
"Microsoft.Windows.SDK.Win32Docs": "0.1.42-alpha",
"Microsoft.Windows.SDK.Win32Metadata": "70.0.11-preview",
"Microsoft.Windows.WDK.Win32Metadata": "0.13.25-experimental"
}
},
"Microsoft.Windows.SDK.BuildTools": {
"type": "Direct",
"requested": "[10.0.28000.2526, )",
"resolved": "10.0.28000.2526",
"contentHash": "voeXXRC5EKuvnUCkPxkEhCbNuv/FM1RzjCQ3lj58Ju5AlVzh/9Lg+a7hqy57mh/00BH/9YNZCJRUjIZJrzTJ4w=="
},
"NETStandard.Library": {
"type": "Direct",
"requested": "[2.0.3, )",
"resolved": "2.0.3",
"contentHash": "st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0"
}
},
"Microsoft.CodeAnalysis.Analyzers": {
"type": "Transitive",
"resolved": "5.3.0",
"contentHash": "KuLhbZwB0L8JikL86AE5VWEp3RLNjIcp+j8yz9EJ/UBgRz4+qDEjHg/tluRFbpYpD/e37BqaaNFbQ0vqawBwWQ=="
},
"Microsoft.CodeAnalysis.Common": {
"type": "Transitive",
"resolved": "5.6.0",
"contentHash": "eWYNB5e92PSdkQ0xcmy2aLtrvBXNydnVi0Hj/VjaAely6XBqA3By+ClGAJaj4d16pzQmrXPLLK9RDVuS1Ec9xQ==",
"dependencies": {
"Microsoft.CodeAnalysis.Analyzers": "5.3.0",
"System.Buffers": "4.6.1",
"System.Collections.Immutable": "10.0.1",
"System.Memory": "4.6.3",
"System.Numerics.Vectors": "4.6.1",
"System.Reflection.Metadata": "10.0.1",
"System.Runtime.CompilerServices.Unsafe": "6.1.2",
"System.Text.Encoding.CodePages": "8.0.0",
"System.Threading.Tasks.Extensions": "4.6.3"
}
},
"Microsoft.NETCore.Platforms": {
"type": "Transitive",
"resolved": "1.1.0",
"contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A=="
},
"Microsoft.Windows.SDK.Win32Docs": {
"type": "Transitive",
"resolved": "0.1.42-alpha",
"contentHash": "Z/9po23gUA9aoukirh2ItMU2ZS9++Js9Gdds9fu5yuMojDrmArvY2y+tq9985tR3cxFxpZO1O35Wjfo0khj5HA=="
},
"Microsoft.Windows.SDK.Win32Metadata": {
"type": "Transitive",
"resolved": "70.0.11-preview",
"contentHash": "i8Dv2KCKzH8MQAsgLeUa5zFGyQOoz+TlvnbkFUkjsUtqhj4oguVj9rDpVACs5fLayZbwItOSllf+bhwr+z2yeg=="
},
"Microsoft.Windows.WDK.Win32Metadata": {
"type": "Transitive",
"resolved": "0.13.25-experimental",
"contentHash": "IM50tb/+UIwBr9FMr6ZKcZjCMW+Axo6NjGqKxgjUfyCY8dRnYUfrJEXxAaXoWtYP4X8EmASmC1Jtwh4XucseZg==",
"dependencies": {
"Microsoft.Windows.SDK.Win32Metadata": "63.0.31-preview"
}
},
"System.Buffers": {
"type": "Transitive",
"resolved": "4.6.1",
"contentHash": "N8GXpmiLMtljq7gwvyS+1QvKT/W2J8sNAvx+HVg4NGmsG/H+2k/y9QI23auLJRterrzCiDH+IWAw4V/GPwsMlw=="
},
"System.Collections.Immutable": {
"type": "Transitive",
"resolved": "10.0.1",
"contentHash": "kdTe61B8P7i2M1pODC3MLbZ/CfFGjpC6c6jzxjQoB5DHZNewayCRqgFUmx3JKB6vLQtozpMQEiw+R5fO32Jv4g==",
"dependencies": {
"System.Memory": "4.6.3",
"System.Runtime.CompilerServices.Unsafe": "6.1.2"
}
},
"System.Memory": {
"type": "Transitive",
"resolved": "4.6.3",
"contentHash": "qdcDOgnFZY40+Q9876JUHnlHu7bosOHX8XISRoH94fwk6hgaeQGSgfZd8srWRZNt5bV9ZW2TljcegDNxsf+96A==",
"dependencies": {
"System.Buffers": "4.6.1",
"System.Numerics.Vectors": "4.6.1",
"System.Runtime.CompilerServices.Unsafe": "6.1.2"
}
},
"System.Numerics.Vectors": {
"type": "Transitive",
"resolved": "4.6.1",
"contentHash": "sQxefTnhagrhoq2ReR0D/6K0zJcr9Hrd6kikeXsA1I8kOCboTavcUC4r7TSfpKFeE163uMuxZcyfO1mGO3EN8Q=="
},
"System.Reflection.Metadata": {
"type": "Transitive",
"resolved": "10.0.1",
"contentHash": "zpcfT/wacPPhE17zcudozlxQtWN/84qyiMyZNGLnK4cj2IMBtLsZYwYjVnALUhPliwyUVj/P7kaZvBWYBCnf2Q==",
"dependencies": {
"System.Collections.Immutable": "10.0.1"
}
},
"System.Runtime.CompilerServices.Unsafe": {
"type": "Transitive",
"resolved": "6.1.2",
"contentHash": "2hBr6zdbIBTDE3EhK7NSVNdX58uTK6iHW/P/Axmm9sl1xoGSLqDvMtpecn226TNwHByFokYwJmt/aQQNlO5CRw=="
},
"System.Text.Encoding.CodePages": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "OZIsVplFGaVY90G2SbpgU7EnCoOO5pw1t4ic21dBF3/1omrJFpAGoNAVpPyMVOC90/hvgkGG3VFqR13YgZMQfg==",
"dependencies": {
"System.Memory": "4.5.5",
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
}
},
"System.Threading.Tasks.Extensions": {
"type": "Transitive",
"resolved": "4.6.3",
"contentHash": "7sCiwilJLYbTZELaKnc7RecBBXWXA+xMLQWZKWawBxYjp6DBlSE3v9/UcvKBvr1vv2tTOhipiogM8rRmxlhrVA==",
"dependencies": {
"System.Runtime.CompilerServices.Unsafe": "6.1.2"
}
}
}
}
}
@@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2026 Frans van Dorsselaer
SPDX-License-Identifier: GPL-3.0-only
+3
View File
@@ -22,6 +22,9 @@ SPDX-License-Identifier: GPL-3.0-only
<File Path="Platform.Build.props" />
<File Path="README.md" />
</Folder>
<Project Path="Win32ResourceGenerator/Win32ResourceGenerator.csproj">
<BuildType Solution="Debug|*" Project="Release" />
</Project>
<Project Path="Installer/Installer.wixproj" Type="b7dd6f7e-def8-4e67-b5b7-07ef123db6f0">
<Build Project="false" />
</Project>