Skip to content

Commit ba53da2

Browse files
authored
feat(csharp): A Windows Installer for the Deephaven Excel Add-In (#6378)
1 parent 6127a39 commit ba53da2

20 files changed

+1205
-0
lines changed

csharp/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
*~
12
.vs/
23
bin/
34
obj/

csharp/ExcelAddIn/ExcelAddIn.csproj

+6
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,10 @@
2121
<ItemGroup>
2222
<ProjectReference Include="..\client\DeephavenClient\DeephavenClient.csproj" />
2323
</ItemGroup>
24+
25+
<PropertyGroup>
26+
<ExcelDnaCreate32BitAddIn>false</ExcelDnaCreate32BitAddIn>
27+
<ExcelDnaPack64BitXllName>DeephavenExcelAddIn64</ExcelDnaPack64BitXllName>
28+
</PropertyGroup>
29+
2430
</Project>

csharp/ExcelAddInInstaller/.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ExcelAddInInstaller-SetupFiles/
2+
ExcelAddInInstaller-cache/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using System;
2+
3+
namespace Deephaven.ExcelAddInInstaller.CustomActions {
4+
public static class ErrorCodes {
5+
// There are many, many possible error codes.
6+
public const int Success = 0;
7+
public const int Failure = 1603;
8+
}
9+
10+
public static class Functions {
11+
public static int RegisterAddIn(string msiHandle) {
12+
return RunHelper(msiHandle, "RegisterAddIn", sess => DoRegisterAddIn(sess, true));
13+
}
14+
15+
public static int UnregisterAddIn(string msiHandle) {
16+
return RunHelper(msiHandle, "UnregisterAddIn", sess => DoRegisterAddIn(sess, false));
17+
}
18+
19+
private static int RunHelper(string msiHandle, string what, Action<MsiSession> action) {
20+
// First try to get a session
21+
MsiSession session;
22+
try {
23+
session = new MsiSession(msiHandle);
24+
} catch (Exception) {
25+
// Didn't get very far
26+
return ErrorCodes.Failure;
27+
}
28+
29+
// Now that we have a session, we can log failures to the session if we need to
30+
try {
31+
session.Log($"{what} starting", MsiSession.InstallMessage.INFO);
32+
action(session);
33+
session.Log($"{what} completed successfully", MsiSession.InstallMessage.INFO);
34+
return ErrorCodes.Success;
35+
} catch (Exception ex) {
36+
session.Log(ex.Message, MsiSession.InstallMessage.ERROR);
37+
session.Log($"{what} exited with error", MsiSession.InstallMessage.ERROR);
38+
return ErrorCodes.Failure;
39+
}
40+
}
41+
42+
private static void DoRegisterAddIn(MsiSession session, bool wantAddIn) {
43+
var addInName = session.CustomActionData;
44+
session.Log($"DoRegisterAddIn({wantAddIn}) with addin={addInName}", MsiSession.InstallMessage.INFO);
45+
if (string.IsNullOrEmpty(addInName)) {
46+
throw new ArgumentException("Expected addin path, got null or empty");
47+
}
48+
49+
Action<string> logger = s => session.Log(s, MsiSession.InstallMessage.INFO);
50+
51+
if (!RegistryManager.TryMakeAddInEntryFromPath(addInName, out var addInEntry, out var failureReason) ||
52+
!RegistryManager.TryCreate(logger, out var rm, out failureReason) ||
53+
!rm.TryUpdateAddInKeys(addInEntry, wantAddIn, out failureReason)) {
54+
throw new Exception(failureReason);
55+
}
56+
}
57+
}
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
3+
<PropertyGroup>
4+
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
5+
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
6+
<ProductVersion>8.0.30703</ProductVersion>
7+
<SchemaVersion>2.0</SchemaVersion>
8+
<ProjectGuid>{2E432229-2429-499B-A2AB-69AB78A7EB21}</ProjectGuid>
9+
<OutputType>Library</OutputType>
10+
<AppDesignerFolder>Properties</AppDesignerFolder>
11+
<RootNamespace>CustomActions</RootNamespace>
12+
<AssemblyName>CustomActions</AssemblyName>
13+
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
14+
<FileAlignment>512</FileAlignment>
15+
</PropertyGroup>
16+
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
17+
<DebugSymbols>true</DebugSymbols>
18+
<DebugType>full</DebugType>
19+
<Optimize>false</Optimize>
20+
<OutputPath>bin\Debug\</OutputPath>
21+
<DefineConstants>DEBUG;TRACE</DefineConstants>
22+
<ErrorReport>prompt</ErrorReport>
23+
<WarningLevel>4</WarningLevel>
24+
</PropertyGroup>
25+
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
26+
<DebugType>pdbonly</DebugType>
27+
<Optimize>true</Optimize>
28+
<OutputPath>bin\Release\</OutputPath>
29+
<DefineConstants>TRACE</DefineConstants>
30+
<ErrorReport>prompt</ErrorReport>
31+
<WarningLevel>4</WarningLevel>
32+
</PropertyGroup>
33+
<ItemGroup>
34+
<Reference Include="System" />
35+
<Reference Include="System.Core" />
36+
<Reference Include="System.Windows.Forms" />
37+
<Reference Include="System.Xml.Linq" />
38+
<Reference Include="System.Data.DataSetExtensions" />
39+
<Reference Include="Microsoft.CSharp" />
40+
<Reference Include="System.Data" />
41+
<Reference Include="System.Xml" />
42+
</ItemGroup>
43+
<ItemGroup>
44+
<Compile Include="CustomActions.cs" />
45+
<Compile Include="MsiSession.cs" />
46+
<Compile Include="Properties\AssemblyInfo.cs" />
47+
<Compile Include="RegistryKeys.cs" />
48+
<Compile Include="RegistryManager.cs" />
49+
</ItemGroup>
50+
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
51+
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
52+
Other similar extension points exist, see Microsoft.Common.targets.
53+
<Target Name="BeforeBuild">
54+
</Target>
55+
<Target Name="AfterBuild">
56+
</Target>
57+
-->
58+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.11.35222.181
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CustomActions", "CustomActions.csproj", "{2E432229-2429-499B-A2AB-69AB78A7EB21}"
7+
EndProject
8+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestCustomActions", "..\TestCustomActions\TestCustomActions.csproj", "{8DD17371-1835-49D6-A8D6-741B9AE504DC}"
9+
EndProject
10+
Global
11+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
12+
Debug|Any CPU = Debug|Any CPU
13+
Release|Any CPU = Release|Any CPU
14+
EndGlobalSection
15+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
16+
{2E432229-2429-499B-A2AB-69AB78A7EB21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
17+
{2E432229-2429-499B-A2AB-69AB78A7EB21}.Debug|Any CPU.Build.0 = Debug|Any CPU
18+
{2E432229-2429-499B-A2AB-69AB78A7EB21}.Release|Any CPU.ActiveCfg = Release|Any CPU
19+
{2E432229-2429-499B-A2AB-69AB78A7EB21}.Release|Any CPU.Build.0 = Release|Any CPU
20+
{8DD17371-1835-49D6-A8D6-741B9AE504DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21+
{8DD17371-1835-49D6-A8D6-741B9AE504DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
22+
{8DD17371-1835-49D6-A8D6-741B9AE504DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
23+
{8DD17371-1835-49D6-A8D6-741B9AE504DC}.Release|Any CPU.Build.0 = Release|Any CPU
24+
EndGlobalSection
25+
GlobalSection(SolutionProperties) = preSolution
26+
HideSolutionNode = FALSE
27+
EndGlobalSection
28+
GlobalSection(ExtensibilityGlobals) = postSolution
29+
SolutionGuid = {24CAE7D4-A5F6-4CEE-BA2A-03D290ED784F}
30+
EndGlobalSection
31+
EndGlobal
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
using System;
2+
using System.Linq;
3+
using System.Runtime.InteropServices;
4+
using System.Text;
5+
6+
namespace Deephaven.ExcelAddInInstaller.CustomActions {
7+
public class MsiSession {
8+
public class NativeMethods {
9+
public const ulong WS_VISIBLE = 0x10000000L;
10+
11+
public const int GWL_STYLE = -16;
12+
13+
// Declare the delegate for EnumWindows callback
14+
public delegate bool EnumWindowsCallback(IntPtr hwnd, int lParam);
15+
16+
// Import the user32.dll library
17+
[DllImport("user32.dll")]
18+
public static extern int EnumWindows(EnumWindowsCallback callback, int lParam);
19+
20+
[DllImport("user32.dll")]
21+
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
22+
23+
[DllImport("user32.dll", SetLastError = true)]
24+
public static extern UInt32 GetWindowLong(IntPtr hWnd, int nIndex);
25+
26+
[DllImport("msi.dll", CharSet = CharSet.Unicode)]
27+
public static extern uint MsiGetProperty(
28+
int hInstall,
29+
string szName,
30+
StringBuilder szValueBuf,
31+
ref uint pcchValueBuf);
32+
33+
[DllImport("msi.dll", CharSet = CharSet.Unicode)]
34+
public static extern uint MsiSetProperty(int hInstall, string szName, string szValue);
35+
36+
[DllImport("msi.dll", CharSet = CharSet.Unicode)]
37+
public static extern int MsiCreateRecord(uint cParams);
38+
39+
[DllImport("msi.dll", CharSet = CharSet.Unicode)]
40+
public static extern uint MsiRecordSetString(int hRecord, uint iField, string szValue);
41+
42+
[DllImport("msi.dll", CharSet = CharSet.Unicode)]
43+
public static extern int MsiProcessMessage(int hInstall, uint eMessageType, int hRecord);
44+
}
45+
46+
public enum InstallMessage : uint {
47+
FATALEXIT = 0x00000000, // premature termination, possibly fatal OOM
48+
ERROR = 0x01000000, // formatted error message
49+
WARNING = 0x02000000, // formatted warning message
50+
USER = 0x03000000, // user request message
51+
INFO = 0x04000000, // informative message for log
52+
FILESINUSE = 0x05000000, // list of files in use that need to be replaced
53+
RESOLVESOURCE = 0x06000000, // request to determine a valid source location
54+
OUTOFDISKSPACE = 0x07000000, // insufficient disk space message
55+
ACTIONSTART = 0x08000000, // start of action: action name & description
56+
ACTIONDATA = 0x09000000, // formatted data associated with individual action item
57+
PROGRESS = 0x0A000000, // progress gauge info: units so far, total
58+
COMMONDATA = 0x0B000000, // product info for dialog: language Id, dialog caption
59+
INITIALIZE = 0x0C000000, // sent prior to UI initialization, no string data
60+
TERMINATE = 0x0D000000, // sent after UI termination, no string data
61+
SHOWDIALOG = 0x0E000000, // sent prior to display or authored dialog or wizard
62+
}
63+
64+
private IntPtr mMsiWindowHandle = IntPtr.Zero;
65+
66+
private bool EnumWindowCallback(IntPtr hwnd, int lParam) {
67+
uint wnd_proc = 0;
68+
NativeMethods.GetWindowThreadProcessId(hwnd, out wnd_proc);
69+
70+
if (wnd_proc == lParam) {
71+
UInt32 style = NativeMethods.GetWindowLong(hwnd, NativeMethods.GWL_STYLE);
72+
if ((style & NativeMethods.WS_VISIBLE) != 0) {
73+
mMsiWindowHandle = hwnd;
74+
return false;
75+
}
76+
}
77+
78+
return true;
79+
}
80+
81+
public IntPtr MsiHandle { get; private set; }
82+
83+
public string CustomActionData { get; private set; }
84+
85+
public MsiSession(string aMsiHandle) {
86+
if (string.IsNullOrEmpty(aMsiHandle))
87+
throw new ArgumentNullException();
88+
89+
int msiHandle = 0;
90+
if (!int.TryParse(aMsiHandle, out msiHandle))
91+
throw new ArgumentException("Invalid msi handle");
92+
93+
MsiHandle = new IntPtr(msiHandle);
94+
95+
string allData = GetProperty("CustomActionData");
96+
CustomActionData = allData.Split(new char[] { '|' }).First();
97+
}
98+
99+
public string GetProperty(string aProperty) {
100+
// Get buffer size
101+
uint pSize = 0;
102+
StringBuilder valueBuffer = new StringBuilder();
103+
NativeMethods.MsiGetProperty(MsiHandle.ToInt32(), aProperty, valueBuffer, ref pSize);
104+
105+
// Get property value
106+
pSize++; // null terminated
107+
valueBuffer.Capacity = (int)pSize;
108+
NativeMethods.MsiGetProperty(MsiHandle.ToInt32(), aProperty, valueBuffer, ref pSize);
109+
110+
return valueBuffer.ToString();
111+
}
112+
113+
public void SetProperty(string aProperty, string aValue) {
114+
NativeMethods.MsiSetProperty(MsiHandle.ToInt32(), aProperty, aValue);
115+
}
116+
117+
public void Log(string aMessage, InstallMessage aMessageType) {
118+
int hRecord = NativeMethods.MsiCreateRecord(1);
119+
NativeMethods.MsiRecordSetString(hRecord, 0, "[1]");
120+
NativeMethods.MsiRecordSetString(hRecord, 1, aMessage);
121+
NativeMethods.MsiProcessMessage(MsiHandle.ToInt32(), (uint)aMessageType, hRecord);
122+
}
123+
124+
public IntPtr GetMsiWindowHandle() {
125+
string msiProcId = GetProperty("CLIENTPROCESSID");
126+
if (string.IsNullOrEmpty(msiProcId))
127+
return IntPtr.Zero;
128+
129+
IntPtr handle = new IntPtr(Convert.ToInt32(msiProcId));
130+
mMsiWindowHandle = IntPtr.Zero;
131+
NativeMethods.EnumWindows(EnumWindowCallback, (int)handle);
132+
133+
return mMsiWindowHandle;
134+
}
135+
}
136+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using System.Reflection;
2+
using System.Runtime.CompilerServices;
3+
using System.Runtime.InteropServices;
4+
5+
// General Information about an assembly is controlled through the following
6+
// set of attributes. Change these attribute values to modify the information
7+
// associated with an assembly.
8+
[assembly: AssemblyTitle("CustomActions")]
9+
[assembly: AssemblyDescription("")]
10+
[assembly: AssemblyConfiguration("")]
11+
[assembly: AssemblyCompany("")]
12+
[assembly: AssemblyProduct("CustomActions")]
13+
[assembly: AssemblyCopyright("Copyright © 2024")]
14+
[assembly: AssemblyTrademark("")]
15+
[assembly: AssemblyCulture("")]
16+
17+
// Setting ComVisible to false makes the types in this assembly not visible
18+
// to COM components. If you need to access a type in this assembly from
19+
// COM, set the ComVisible attribute to true on that type.
20+
[assembly: ComVisible(false)]
21+
22+
// The following GUID is for the ID of the typelib if this project is exposed to COM
23+
[assembly: Guid("2e432229-2429-499b-a2ab-69ab78a7eb21")]
24+
25+
// Version information for an assembly consists of the following four values:
26+
//
27+
// Major Version
28+
// Minor Version
29+
// Build Number
30+
// Revision
31+
//
32+
// You can specify all the values or you can default the Build and Revision Numbers
33+
// by using the '*' as shown below:
34+
// [assembly: AssemblyVersion("1.0.*")]
35+
[assembly: AssemblyVersion("1.0.0.0")]
36+
[assembly: AssemblyFileVersion("1.0.0.0")]

0 commit comments

Comments
 (0)