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) {