Add `customize` command

pull/631/head
NextTurn 2020-07-30 00:00:00 +08:00 committed by Next Turn
parent 7e644f7944
commit 2a576e102e
7 changed files with 352 additions and 14 deletions

View File

@ -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);
}
}

View File

@ -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
{
/// <exception cref="CommandException" />
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);
}
}
}
}

View File

@ -59,6 +59,15 @@ namespace WinSW.Native
/// <exception cref="CommandException" />
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
internal static void Win32Exception()
{
Win32Exception inner = new Win32Exception();
Debug.Assert(inner.NativeErrorCode != 0);
throw new CommandException(inner);
}
/// <exception cref="CommandException" />
[MethodImpl(MethodImplOptions.NoInlining)]
internal static void Win32Exception(string message)
{
Win32Exception inner = new Win32Exception();

View File

@ -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));

View File

@ -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
}
}

View File

@ -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");
}
}

View File

@ -34,6 +34,21 @@ namespace WinSW
private static readonly ILog Log = LogManager.GetLogger(typeof(Program));
internal static Action<Exception, InvocationContext>? 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<int>("--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<string, string>(Customize),
};
customize.Add(new Option<string>(new[] { "--output", "-o" })
{
Required = true,
});
var manufacturer = new Option<string>("--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)
{