Skip to content

Commit

Permalink
feat(csharp): A Windows Installer for the Deephaven Excel Add-In (dee…
Browse files Browse the repository at this point in the history
  • Loading branch information
kosak authored Nov 20, 2024
1 parent 6127a39 commit ba53da2
Show file tree
Hide file tree
Showing 20 changed files with 1,205 additions and 0 deletions.
1 change: 1 addition & 0 deletions csharp/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
*~
.vs/
bin/
obj/
6 changes: 6 additions & 0 deletions csharp/ExcelAddIn/ExcelAddIn.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,10 @@
<ItemGroup>
<ProjectReference Include="..\client\DeephavenClient\DeephavenClient.csproj" />
</ItemGroup>

<PropertyGroup>
<ExcelDnaCreate32BitAddIn>false</ExcelDnaCreate32BitAddIn>
<ExcelDnaPack64BitXllName>DeephavenExcelAddIn64</ExcelDnaPack64BitXllName>
</PropertyGroup>

</Project>
2 changes: 2 additions & 0 deletions csharp/ExcelAddInInstaller/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ExcelAddInInstaller-SetupFiles/
ExcelAddInInstaller-cache/
58 changes: 58 additions & 0 deletions csharp/ExcelAddInInstaller/CustomActions/CustomActions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System;

namespace Deephaven.ExcelAddInInstaller.CustomActions {
public static class ErrorCodes {
// There are many, many possible error codes.
public const int Success = 0;
public const int Failure = 1603;
}

public static class Functions {
public static int RegisterAddIn(string msiHandle) {
return RunHelper(msiHandle, "RegisterAddIn", sess => DoRegisterAddIn(sess, true));
}

public static int UnregisterAddIn(string msiHandle) {
return RunHelper(msiHandle, "UnregisterAddIn", sess => DoRegisterAddIn(sess, false));
}

private static int RunHelper(string msiHandle, string what, Action<MsiSession> action) {
// First try to get a session
MsiSession session;
try {
session = new MsiSession(msiHandle);
} catch (Exception) {
// Didn't get very far
return ErrorCodes.Failure;
}

// Now that we have a session, we can log failures to the session if we need to
try {
session.Log($"{what} starting", MsiSession.InstallMessage.INFO);
action(session);
session.Log($"{what} completed successfully", MsiSession.InstallMessage.INFO);
return ErrorCodes.Success;
} catch (Exception ex) {
session.Log(ex.Message, MsiSession.InstallMessage.ERROR);
session.Log($"{what} exited with error", MsiSession.InstallMessage.ERROR);
return ErrorCodes.Failure;
}
}

private static void DoRegisterAddIn(MsiSession session, bool wantAddIn) {
var addInName = session.CustomActionData;
session.Log($"DoRegisterAddIn({wantAddIn}) with addin={addInName}", MsiSession.InstallMessage.INFO);
if (string.IsNullOrEmpty(addInName)) {
throw new ArgumentException("Expected addin path, got null or empty");
}

Action<string> logger = s => session.Log(s, MsiSession.InstallMessage.INFO);

if (!RegistryManager.TryMakeAddInEntryFromPath(addInName, out var addInEntry, out var failureReason) ||
!RegistryManager.TryCreate(logger, out var rm, out failureReason) ||
!rm.TryUpdateAddInKeys(addInEntry, wantAddIn, out failureReason)) {
throw new Exception(failureReason);
}
}
}
}
58 changes: 58 additions & 0 deletions csharp/ExcelAddInInstaller/CustomActions/CustomActions.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProductVersion>8.0.30703</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{2E432229-2429-499B-A2AB-69AB78A7EB21}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>CustomActions</RootNamespace>
<AssemblyName>CustomActions</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="CustomActions.cs" />
<Compile Include="MsiSession.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="RegistryKeys.cs" />
<Compile Include="RegistryManager.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>
31 changes: 31 additions & 0 deletions csharp/ExcelAddInInstaller/CustomActions/CustomActions.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.11.35222.181
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CustomActions", "CustomActions.csproj", "{2E432229-2429-499B-A2AB-69AB78A7EB21}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestCustomActions", "..\TestCustomActions\TestCustomActions.csproj", "{8DD17371-1835-49D6-A8D6-741B9AE504DC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{2E432229-2429-499B-A2AB-69AB78A7EB21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2E432229-2429-499B-A2AB-69AB78A7EB21}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2E432229-2429-499B-A2AB-69AB78A7EB21}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2E432229-2429-499B-A2AB-69AB78A7EB21}.Release|Any CPU.Build.0 = Release|Any CPU
{8DD17371-1835-49D6-A8D6-741B9AE504DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8DD17371-1835-49D6-A8D6-741B9AE504DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8DD17371-1835-49D6-A8D6-741B9AE504DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8DD17371-1835-49D6-A8D6-741B9AE504DC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {24CAE7D4-A5F6-4CEE-BA2A-03D290ED784F}
EndGlobalSection
EndGlobal
136 changes: 136 additions & 0 deletions csharp/ExcelAddInInstaller/CustomActions/MsiSession.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
using System;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;

namespace Deephaven.ExcelAddInInstaller.CustomActions {
public class MsiSession {
public class NativeMethods {
public const ulong WS_VISIBLE = 0x10000000L;

public const int GWL_STYLE = -16;

// Declare the delegate for EnumWindows callback
public delegate bool EnumWindowsCallback(IntPtr hwnd, int lParam);

// Import the user32.dll library
[DllImport("user32.dll")]
public static extern int EnumWindows(EnumWindowsCallback callback, int lParam);

[DllImport("user32.dll")]
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

[DllImport("user32.dll", SetLastError = true)]
public static extern UInt32 GetWindowLong(IntPtr hWnd, int nIndex);

[DllImport("msi.dll", CharSet = CharSet.Unicode)]
public static extern uint MsiGetProperty(
int hInstall,
string szName,
StringBuilder szValueBuf,
ref uint pcchValueBuf);

[DllImport("msi.dll", CharSet = CharSet.Unicode)]
public static extern uint MsiSetProperty(int hInstall, string szName, string szValue);

[DllImport("msi.dll", CharSet = CharSet.Unicode)]
public static extern int MsiCreateRecord(uint cParams);

[DllImport("msi.dll", CharSet = CharSet.Unicode)]
public static extern uint MsiRecordSetString(int hRecord, uint iField, string szValue);

[DllImport("msi.dll", CharSet = CharSet.Unicode)]
public static extern int MsiProcessMessage(int hInstall, uint eMessageType, int hRecord);
}

public enum InstallMessage : uint {
FATALEXIT = 0x00000000, // premature termination, possibly fatal OOM
ERROR = 0x01000000, // formatted error message
WARNING = 0x02000000, // formatted warning message
USER = 0x03000000, // user request message
INFO = 0x04000000, // informative message for log
FILESINUSE = 0x05000000, // list of files in use that need to be replaced
RESOLVESOURCE = 0x06000000, // request to determine a valid source location
OUTOFDISKSPACE = 0x07000000, // insufficient disk space message
ACTIONSTART = 0x08000000, // start of action: action name & description
ACTIONDATA = 0x09000000, // formatted data associated with individual action item
PROGRESS = 0x0A000000, // progress gauge info: units so far, total
COMMONDATA = 0x0B000000, // product info for dialog: language Id, dialog caption
INITIALIZE = 0x0C000000, // sent prior to UI initialization, no string data
TERMINATE = 0x0D000000, // sent after UI termination, no string data
SHOWDIALOG = 0x0E000000, // sent prior to display or authored dialog or wizard
}

private IntPtr mMsiWindowHandle = IntPtr.Zero;

private bool EnumWindowCallback(IntPtr hwnd, int lParam) {
uint wnd_proc = 0;
NativeMethods.GetWindowThreadProcessId(hwnd, out wnd_proc);

if (wnd_proc == lParam) {
UInt32 style = NativeMethods.GetWindowLong(hwnd, NativeMethods.GWL_STYLE);
if ((style & NativeMethods.WS_VISIBLE) != 0) {
mMsiWindowHandle = hwnd;
return false;
}
}

return true;
}

public IntPtr MsiHandle { get; private set; }

public string CustomActionData { get; private set; }

public MsiSession(string aMsiHandle) {
if (string.IsNullOrEmpty(aMsiHandle))
throw new ArgumentNullException();

int msiHandle = 0;
if (!int.TryParse(aMsiHandle, out msiHandle))
throw new ArgumentException("Invalid msi handle");

MsiHandle = new IntPtr(msiHandle);

string allData = GetProperty("CustomActionData");
CustomActionData = allData.Split(new char[] { '|' }).First();
}

public string GetProperty(string aProperty) {
// Get buffer size
uint pSize = 0;
StringBuilder valueBuffer = new StringBuilder();
NativeMethods.MsiGetProperty(MsiHandle.ToInt32(), aProperty, valueBuffer, ref pSize);

// Get property value
pSize++; // null terminated
valueBuffer.Capacity = (int)pSize;
NativeMethods.MsiGetProperty(MsiHandle.ToInt32(), aProperty, valueBuffer, ref pSize);

return valueBuffer.ToString();
}

public void SetProperty(string aProperty, string aValue) {
NativeMethods.MsiSetProperty(MsiHandle.ToInt32(), aProperty, aValue);
}

public void Log(string aMessage, InstallMessage aMessageType) {
int hRecord = NativeMethods.MsiCreateRecord(1);
NativeMethods.MsiRecordSetString(hRecord, 0, "[1]");
NativeMethods.MsiRecordSetString(hRecord, 1, aMessage);
NativeMethods.MsiProcessMessage(MsiHandle.ToInt32(), (uint)aMessageType, hRecord);
}

public IntPtr GetMsiWindowHandle() {
string msiProcId = GetProperty("CLIENTPROCESSID");
if (string.IsNullOrEmpty(msiProcId))
return IntPtr.Zero;

IntPtr handle = new IntPtr(Convert.ToInt32(msiProcId));
mMsiWindowHandle = IntPtr.Zero;
NativeMethods.EnumWindows(EnumWindowCallback, (int)handle);

return mMsiWindowHandle;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("CustomActions")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("CustomActions")]
[assembly: AssemblyCopyright("Copyright © 2024")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]

// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]

// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("2e432229-2429-499b-a2ab-69ab78a7eb21")]

// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
Loading

0 comments on commit ba53da2

Please sign in to comment.