From 2a576e102e997395166374e19af91d96d10e0589 Mon Sep 17 00:00:00 2001
From: NextTurn <45985406+NextTurn@users.noreply.github.com>
Date: Thu, 30 Jul 2020 00:00:00 +0800
Subject: [PATCH] Add `customize` command
---
src/WinSW.Core/Native/ResourceApis.cs | 38 ++++
src/WinSW.Core/Native/Resources.cs | 170 ++++++++++++++++++
src/WinSW.Core/Native/Throw.cs | 9 +
src/WinSW.Tests/Configuration/ExamplesTest.cs | 12 +-
src/WinSW.Tests/MainTest.cs | 32 ++++
src/WinSW.Tests/Util/Layout.cs | 39 ++++
src/WinSW/Program.cs | 66 ++++++-
7 files changed, 352 insertions(+), 14 deletions(-)
create mode 100644 src/WinSW.Core/Native/ResourceApis.cs
create mode 100644 src/WinSW.Core/Native/Resources.cs
create mode 100644 src/WinSW.Tests/Util/Layout.cs
diff --git a/src/WinSW.Core/Native/ResourceApis.cs b/src/WinSW.Core/Native/ResourceApis.cs
new file mode 100644
index 0000000..8df76f4
--- /dev/null
+++ b/src/WinSW.Core/Native/ResourceApis.cs
@@ -0,0 +1,38 @@
+#pragma warning disable SA1310 // Field names should not contain underscore
+
+using System;
+using System.Runtime.InteropServices;
+
+namespace WinSW.Native
+{
+ internal static class ResourceApis
+ {
+ internal static readonly IntPtr VS_VERSION_INFO = new IntPtr(1);
+
+ internal static readonly IntPtr RT_VERSION = new IntPtr(16);
+
+ [DllImport(Libraries.Kernel32, SetLastError = true, CharSet = CharSet.Unicode)]
+ internal static extern IntPtr BeginUpdateResourceW(string fileName, bool deleteExistingResources);
+
+ [DllImport(Libraries.Kernel32, SetLastError = true)]
+ internal static extern bool EndUpdateResourceW(IntPtr update, bool discard);
+
+ [DllImport(Libraries.Kernel32, SetLastError = true)]
+ internal static extern IntPtr FindResourceW(IntPtr module, IntPtr name, IntPtr type);
+
+ [DllImport(Libraries.Kernel32)]
+ internal static extern bool FreeLibrary(IntPtr libModule);
+
+ [DllImport(Libraries.Kernel32, CharSet = CharSet.Unicode)]
+ internal static extern IntPtr LoadLibraryW(string libFileName);
+
+ [DllImport(Libraries.Kernel32, SetLastError = true)]
+ internal static extern IntPtr LoadResource(IntPtr module, IntPtr resInfo);
+
+ [DllImport(Libraries.Kernel32)]
+ internal static extern IntPtr LockResource(IntPtr resData);
+
+ [DllImport(Libraries.Kernel32, SetLastError = true)]
+ internal static extern bool UpdateResourceW(IntPtr update, IntPtr type, IntPtr name, ushort language, IntPtr data, int size);
+ }
+}
diff --git a/src/WinSW.Core/Native/Resources.cs b/src/WinSW.Core/Native/Resources.cs
new file mode 100644
index 0000000..d972459
--- /dev/null
+++ b/src/WinSW.Core/Native/Resources.cs
@@ -0,0 +1,170 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Runtime.InteropServices;
+using static WinSW.Native.ResourceApis;
+
+namespace WinSW.Native
+{
+ internal static class Resources
+ {
+ ///
+ internal static unsafe bool UpdateCompanyName(string path, string outputPath, string companyName)
+ {
+ IntPtr module = LoadLibraryW(path);
+ try
+ {
+ IntPtr verInfo = FindResourceW(module, VS_VERSION_INFO, RT_VERSION);
+ if (verInfo == IntPtr.Zero)
+ {
+ Exit();
+ }
+
+ IntPtr resData = LoadResource(module, verInfo);
+ if (resData == IntPtr.Zero)
+ {
+ Exit();
+ }
+
+ IntPtr resAddr = LockResource(resData);
+
+ IntPtr address = resAddr;
+ int offset = 0;
+
+ short length = ((short*)address)[0];
+ short valueLength = ReadHeaderAndAdvance();
+ string key = ReadKeyAndAdvance();
+ Debug.Assert(key == "VS_VERSION_INFO");
+ offset += valueLength;
+ Align();
+
+ valueLength = ReadHeaderAndAdvance();
+ key = ReadKeyAndAdvance();
+ Debug.Assert(key == "VarFileInfo");
+ offset += valueLength;
+ Align();
+
+ valueLength = ReadHeaderAndAdvance();
+ key = ReadKeyAndAdvance();
+ Debug.Assert(key == "Translation");
+ ushort language = ((ushort*)address)[0];
+ ushort codePage = ((ushort*)address)[1];
+ offset += valueLength;
+ address = resAddr + offset;
+
+ valueLength = ReadHeaderAndAdvance();
+ key = ReadKeyAndAdvance();
+ Debug.Assert(key == "StringFileInfo");
+ offset += valueLength;
+ Align();
+
+ short stringTableLength = ((short*)address)[0];
+ int stringTableEndOffset = offset + stringTableLength;
+ valueLength = ReadHeaderAndAdvance();
+ key = ReadKeyAndAdvance();
+ Debug.Assert(key == $"{language:x4}{codePage:x4}");
+
+ do
+ {
+ int valueLengthOffset = offset + sizeof(short);
+ valueLength = ReadHeaderAndAdvance();
+ key = ReadKeyAndAdvance();
+
+ if (key != "CompanyName")
+ {
+ offset += sizeof(short) * valueLength;
+ Align(); // ?
+ continue;
+ }
+
+ // int oldLength = "CloudBees, Inc.".Length + 1; // 16
+ int newLength = companyName.Length + 1;
+ Debug.Assert(newLength > 12 && newLength <= 16);
+
+ IntPtr newAddress = Marshal.AllocHGlobal(length);
+ try
+ {
+ Buffer.MemoryCopy((void*)resAddr, (void*)newAddress, length, length);
+
+ *(short*)(newAddress + valueLengthOffset) = (short)newLength;
+ fixed (char* ptr = companyName)
+ {
+ Buffer.MemoryCopy(ptr, (void*)(newAddress + offset), newLength * sizeof(char), newLength * sizeof(char));
+ }
+
+ File.Copy(path, outputPath, true);
+
+ IntPtr update = BeginUpdateResourceW(outputPath, false);
+ if (update == IntPtr.Zero)
+ {
+ Exit();
+ }
+
+ try
+ {
+ if (!UpdateResourceW(update, RT_VERSION, VS_VERSION_INFO, language, newAddress, length))
+ {
+ Exit();
+ }
+
+ if (!EndUpdateResourceW(update, false))
+ {
+ Exit();
+ }
+
+ return true;
+ }
+ catch
+ {
+ _ = EndUpdateResourceW(update, true);
+ throw;
+ }
+ }
+ finally
+ {
+ Marshal.FreeHGlobal(newAddress);
+ }
+ }
+ while (offset < stringTableEndOffset);
+
+ return false;
+
+ static void Exit()
+ {
+ Throw.Command.Win32Exception();
+ }
+
+ void Align()
+ {
+ if ((offset & 3) != 0)
+ {
+ offset &= ~3;
+ offset += 4;
+ }
+
+ address = resAddr + offset;
+ }
+
+ short ReadHeaderAndAdvance()
+ {
+ valueLength = ((short*)address)[1];
+ offset += sizeof(short) * 3;
+ address = resAddr + offset;
+ return valueLength;
+ }
+
+ string ReadKeyAndAdvance()
+ {
+ string key = Marshal.PtrToStringUni(address)!;
+ offset += sizeof(char) * (key.Length + 1);
+ Align();
+ return key;
+ }
+ }
+ finally
+ {
+ _ = FreeLibrary(module);
+ }
+ }
+ }
+}
diff --git a/src/WinSW.Core/Native/Throw.cs b/src/WinSW.Core/Native/Throw.cs
index bb935fd..73b4164 100644
--- a/src/WinSW.Core/Native/Throw.cs
+++ b/src/WinSW.Core/Native/Throw.cs
@@ -59,6 +59,15 @@ namespace WinSW.Native
///
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
+ internal static void Win32Exception()
+ {
+ Win32Exception inner = new Win32Exception();
+ Debug.Assert(inner.NativeErrorCode != 0);
+ throw new CommandException(inner);
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.NoInlining)]
internal static void Win32Exception(string message)
{
Win32Exception inner = new Win32Exception();
diff --git a/src/WinSW.Tests/Configuration/ExamplesTest.cs b/src/WinSW.Tests/Configuration/ExamplesTest.cs
index c10da65..cdc8018 100644
--- a/src/WinSW.Tests/Configuration/ExamplesTest.cs
+++ b/src/WinSW.Tests/Configuration/ExamplesTest.cs
@@ -36,17 +36,7 @@ namespace WinSW.Tests.Configuration
private static XmlServiceConfig Load(string exampleName)
{
- string directory = Environment.CurrentDirectory;
- while (true)
- {
- if (File.Exists(Path.Combine(directory, ".gitignore")))
- {
- break;
- }
-
- directory = Path.GetDirectoryName(directory);
- Assert.NotNull(directory);
- }
+ string directory = Layout.RepositoryRoot;
string path = Path.Combine(directory, $@"samples\sample-{exampleName}.xml");
Assert.True(File.Exists(path));
diff --git a/src/WinSW.Tests/MainTest.cs b/src/WinSW.Tests/MainTest.cs
index 2aa8610..8ea7e27 100644
--- a/src/WinSW.Tests/MainTest.cs
+++ b/src/WinSW.Tests/MainTest.cs
@@ -1,4 +1,6 @@
using System;
+using System.Diagnostics;
+using System.IO;
using System.ServiceProcess;
using WinSW.Tests.Util;
using Xunit;
@@ -47,5 +49,35 @@ namespace WinSW.Tests
string cliOut = CommandLineTestHelper.Test(new[] { "status" });
Assert.Equal("NonExistent" + Environment.NewLine, cliOut);
}
+
+#if NET461
+ [Fact]
+ public void Customize()
+ {
+ const string OldCompanyName = "CloudBees, Inc.";
+ const string NewCompanyName = "CLOUDBEES, INC.";
+
+ string inputPath = Path.Combine(Layout.ArtifactsDirectory, "WinSW.NET461.exe");
+
+ Assert.Equal(OldCompanyName, FileVersionInfo.GetVersionInfo(inputPath).CompanyName);
+
+ // deny write access
+ using FileStream file = File.OpenRead(inputPath);
+
+ string outputPath = Path.GetTempFileName();
+ Program.TestExecutablePath = inputPath;
+ try
+ {
+ _ = CommandLineTestHelper.Test(new[] { "customize", "-o", outputPath, "--manufacturer", NewCompanyName });
+
+ Assert.Equal(NewCompanyName, FileVersionInfo.GetVersionInfo(outputPath).CompanyName);
+ }
+ finally
+ {
+ Program.TestExecutablePath = null;
+ File.Delete(outputPath);
+ }
+ }
+#endif
}
}
diff --git a/src/WinSW.Tests/Util/Layout.cs b/src/WinSW.Tests/Util/Layout.cs
new file mode 100644
index 0000000..733f28a
--- /dev/null
+++ b/src/WinSW.Tests/Util/Layout.cs
@@ -0,0 +1,39 @@
+using System;
+using System.IO;
+using Xunit;
+
+namespace WinSW.Tests.Util
+{
+ internal static class Layout
+ {
+ private static string repositoryRoot;
+ private static string artifactsDirectory;
+
+ internal static string RepositoryRoot
+ {
+ get
+ {
+ if (repositoryRoot != null)
+ {
+ return repositoryRoot;
+ }
+
+ string directory = Environment.CurrentDirectory;
+ while (true)
+ {
+ if (File.Exists(Path.Combine(directory, ".gitignore")))
+ {
+ break;
+ }
+
+ directory = Path.GetDirectoryName(directory);
+ Assert.NotNull(directory);
+ }
+
+ return repositoryRoot = directory;
+ }
+ }
+
+ internal static string ArtifactsDirectory => artifactsDirectory ??= Path.Combine(RepositoryRoot, "artifacts");
+ }
+}
diff --git a/src/WinSW/Program.cs b/src/WinSW/Program.cs
index 31f8489..bb83083 100644
--- a/src/WinSW/Program.cs
+++ b/src/WinSW/Program.cs
@@ -34,6 +34,21 @@ namespace WinSW
private static readonly ILog Log = LogManager.GetLogger(typeof(Program));
internal static Action? TestExceptionHandler;
+ internal static string? TestExecutablePath;
+
+ private static string ExecutablePath
+ {
+ get
+ {
+ if (TestExecutablePath != null)
+ {
+ return TestExecutablePath;
+ }
+
+ using Process current = Process.GetCurrentProcess();
+ return current.MainModule.FileName;
+ }
+ }
internal static int Main(string[] args)
{
@@ -205,12 +220,12 @@ namespace WinSW
test.Add(config);
test.Add(noElevate);
- const int minTimeout = -1;
- const int maxTimeout = int.MaxValue / 1000;
-
var timeout = new Option("--timeout", "Specifies the number of seconds to wait before the service is stopped.");
timeout.Argument.AddValidator(argument =>
{
+ const int minTimeout = -1;
+ const int maxTimeout = int.MaxValue / 1000;
+
string token = argument.Tokens.Single().Value;
return !int.TryParse(token, out int value) ? null :
value < minTimeout ? $"Argument '{token}' must be greater than or equal to {minTimeout}." :
@@ -236,6 +251,39 @@ namespace WinSW
root.Add(refresh);
}
+ {
+ var customize = new Command("customize")
+ {
+ Handler = CommandHandler.Create(Customize),
+ };
+
+ customize.Add(new Option(new[] { "--output", "-o" })
+ {
+ Required = true,
+ });
+
+ var manufacturer = new Option("--manufacturer")
+ {
+ Required = true,
+ };
+ manufacturer.Argument.AddValidator(argument =>
+ {
+ const int minLength = 12;
+ const int maxLength = 15;
+
+ string token = argument.Tokens.Single().Value;
+ int length = token.Length;
+ return
+ length < minLength ? $"The length of argument '{token}' must be greater than or equal to {minLength}." :
+ length > maxLength ? $"The length of argument '{token}' must be less than or equal to {maxLength}." :
+ null;
+ });
+
+ customize.Add(manufacturer);
+
+ root.Add(customize);
+ }
+
{
var dev = new Command("dev", "Experimental commands.")
{
@@ -878,6 +926,18 @@ namespace WinSW
}
}
+ static void Customize(string output, string manufacturer)
+ {
+ if (Resources.UpdateCompanyName(ExecutablePath, output, manufacturer))
+ {
+ Console.WriteLine("The operation succeeded.");
+ }
+ else
+ {
+ Console.Error.WriteLine("The operation failed.");
+ }
+ }
+
// [DoesNotReturn]
static void Elevate(bool noElevate)
{