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" />
|
/// <exception cref="CommandException" />
|
||||||
[DoesNotReturn]
|
[DoesNotReturn]
|
||||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
[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)
|
internal static void Win32Exception(string message)
|
||||||
{
|
{
|
||||||
Win32Exception inner = new Win32Exception();
|
Win32Exception inner = new Win32Exception();
|
||||||
|
|
|
@ -36,17 +36,7 @@ namespace WinSW.Tests.Configuration
|
||||||
|
|
||||||
private static XmlServiceConfig Load(string exampleName)
|
private static XmlServiceConfig Load(string exampleName)
|
||||||
{
|
{
|
||||||
string directory = Environment.CurrentDirectory;
|
string directory = Layout.RepositoryRoot;
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
if (File.Exists(Path.Combine(directory, ".gitignore")))
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
directory = Path.GetDirectoryName(directory);
|
|
||||||
Assert.NotNull(directory);
|
|
||||||
}
|
|
||||||
|
|
||||||
string path = Path.Combine(directory, $@"samples\sample-{exampleName}.xml");
|
string path = Path.Combine(directory, $@"samples\sample-{exampleName}.xml");
|
||||||
Assert.True(File.Exists(path));
|
Assert.True(File.Exists(path));
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
using System.ServiceProcess;
|
using System.ServiceProcess;
|
||||||
using WinSW.Tests.Util;
|
using WinSW.Tests.Util;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
@ -47,5 +49,35 @@ namespace WinSW.Tests
|
||||||
string cliOut = CommandLineTestHelper.Test(new[] { "status" });
|
string cliOut = CommandLineTestHelper.Test(new[] { "status" });
|
||||||
Assert.Equal("NonExistent" + Environment.NewLine, cliOut);
|
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));
|
private static readonly ILog Log = LogManager.GetLogger(typeof(Program));
|
||||||
|
|
||||||
internal static Action<Exception, InvocationContext>? TestExceptionHandler;
|
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)
|
internal static int Main(string[] args)
|
||||||
{
|
{
|
||||||
|
@ -205,12 +220,12 @@ namespace WinSW
|
||||||
test.Add(config);
|
test.Add(config);
|
||||||
test.Add(noElevate);
|
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.");
|
var timeout = new Option<int>("--timeout", "Specifies the number of seconds to wait before the service is stopped.");
|
||||||
timeout.Argument.AddValidator(argument =>
|
timeout.Argument.AddValidator(argument =>
|
||||||
{
|
{
|
||||||
|
const int minTimeout = -1;
|
||||||
|
const int maxTimeout = int.MaxValue / 1000;
|
||||||
|
|
||||||
string token = argument.Tokens.Single().Value;
|
string token = argument.Tokens.Single().Value;
|
||||||
return !int.TryParse(token, out int value) ? null :
|
return !int.TryParse(token, out int value) ? null :
|
||||||
value < minTimeout ? $"Argument '{token}' must be greater than or equal to {minTimeout}." :
|
value < minTimeout ? $"Argument '{token}' must be greater than or equal to {minTimeout}." :
|
||||||
|
@ -236,6 +251,39 @@ namespace WinSW
|
||||||
root.Add(refresh);
|
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.")
|
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]
|
// [DoesNotReturn]
|
||||||
static void Elevate(bool noElevate)
|
static void Elevate(bool noElevate)
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in New Issue