mirror of https://github.com/winsw/winsw
Add `customize` command
parent
7e644f7944
commit
2a576e102e
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue