Files
MeadeGeneric/Meade.net/LocalServer.cs
T
2019-10-01 19:12:34 +01:00

596 lines
23 KiB
C#

//
// ASCOM.Meade.net Local COM Server
//
// This is the core of a managed COM Local Server, capable of serving
// multiple instances of multiple interfaces, within a single
// executable. This implementes the equivalent functionality of VB6
// which has been extensively used in ASCOM for drivers that provide
// multiple interfaces to multiple clients (e.g. Meade Telescope
// and Focuser) as well as hubs (e.g., POTH).
//
// Written by: Robert B. Denny (Version 1.0.1, 29-May-2007)
// Modified by Chris Rowland and Peter Simpson to allow use with multiple devices of the same type March 2011
//
//
using System;
using System.Collections;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Principal;
using System.Threading;
using System.Windows.Forms;
using ASCOM.Meade.net.Properties;
using ASCOM.Utilities;
using Microsoft.Win32;
namespace ASCOM.Meade.net
{
public static class Server
{
private const string DriverName = "Meade Generic";
#region Access to kernel32.dll, user32.dll, and ole32.dll functions
//// CoInitializeEx() can be used to set the apartment model
//// of individual threads.
//[DllImport("ole32.dll")]
//static extern int CoInitializeEx(IntPtr pvReserved, uint dwCoInit);
//// CoUninitialize() is used to uninitialize a COM thread.
//[DllImport("ole32.dll")]
//static extern void CoUninitialize();
// PostThreadMessage() allows us to post a Windows Message to
// a specific thread (identified by its thread id).
// We will need this API to post a WM_QUIT message to the main
// thread in order to terminate this application.
[DllImport("user32.dll")]
static extern bool PostThreadMessage(uint idThread, uint msg, UIntPtr wParam,
IntPtr lParam);
// GetCurrentThreadId() allows us to obtain the thread id of the
// calling thread. This allows us to post the WM_QUIT message to
// the main thread.
[DllImport("kernel32.dll")]
static extern uint GetCurrentThreadId();
#endregion
#region Private Data
private static int _objsInUse; // Keeps a count on the total number of objects alive.
private static int _serverLocks; // Keeps a lock count on this application.
private static FrmMain _sMainForm; // Reference to our main form
//private static ArrayList _sComObjectAssys; // Dynamically loaded assemblies containing served COM objects
private static ArrayList _sComObjectTypes; // Served COM object types
private static ArrayList _sClassFactories; // Served COM object class factories
private static string _sAppId = "{4e68ec46-5ffc-49e7-b298-38a548df0bfd}"; // Our AppId
private static readonly Object LockObject = new object();
#endregion
// This property returns the main thread's id.
private static uint MainThreadId { get; set; } // Stores the main thread's thread id.
// Used to tell if started by COM or manually
private static bool StartedByCom { get; set; } // True if server started by COM (-embedding)
#region Server Lock, Object Counting, and AutoQuit on COM startup
// Returns the total number of objects alive currently.
private static int ObjectsCount
{
get
{
lock (LockObject)
{
return _objsInUse;
}
}
}
// This method performs a thread-safe incrementation of the objects count.
public static void CountObject()
{
// Increment the global count of objects.
Interlocked.Increment(ref _objsInUse);
}
// This method performs a thread-safe decrementation the objects count.
public static void UncountObject()
{
// Decrement the global count of objects.
Interlocked.Decrement(ref _objsInUse);
}
// Returns the current server lock count.
private static int ServerLockCount
{
get
{
lock (LockObject)
{
return _serverLocks;
}
}
}
// This method performs a thread-safe incrementation the
// server lock count.
public static void CountLock()
{
// Increment the global lock count of this server.
Interlocked.Increment(ref _serverLocks);
}
// This method performs a thread-safe decrementation the
// server lock count.
public static void UncountLock()
{
// Decrement the global lock count of this server.
Interlocked.Decrement(ref _serverLocks);
}
// AttemptToTerminateServer() will check to see if the objects count and the server
// lock count have both dropped to zero.
//
// If so, and if we were started by COM, we post a WM_QUIT message to the main thread's
// message loop. This will cause the message loop to exit and hence the termination
// of this application. If hand-started, then just trace that it WOULD exit now.
//
public static void ExitIf()
{
lock (LockObject)
{
if ((ObjectsCount <= 0) && (ServerLockCount <= 0))
{
if (StartedByCom)
{
UIntPtr wParam = new UIntPtr(0);
IntPtr lParam = new IntPtr(0);
PostThreadMessage(MainThreadId, 0x0012, wParam, lParam);
}
}
}
}
#endregion
// -----------------
// PRIVATE FUNCTIONS
// -----------------
#region Dynamic Driver Assembly Loader
//
// Load the assemblies that contain the classes that we will serve
// via COM. These will be located in the same folder as
// our executable.
//
private static bool LoadComObjectAssemblies()
{
//_sComObjectAssys = new ArrayList();
_sComObjectTypes = new ArrayList();
// put everything into one folder, the same as the server.
string assyPath = Assembly.GetEntryAssembly()?.Location;
assyPath = Path.GetDirectoryName(assyPath);
if (assyPath == null)
throw new System.InvalidOperationException();
DirectoryInfo d = new DirectoryInfo(assyPath);
foreach (FileInfo fi in d.GetFiles("*.dll"))
{
string aPath = fi.FullName;
//
// First try to load the assembly and get the types for
// the class and the class factory. If this doesn't work ????
//
try
{
Assembly so = Assembly.LoadFrom(aPath);
//PWGS Get the types in the assembly
Type[] types = so.GetTypes();
foreach (Type type in types)
{
// PWGS Now checks the type rather than the assembly
// Check to see if the type has the ServedClassName attribute, only use it if it does.
MemberInfo info = type;
object[] attrbutes = info.GetCustomAttributes(typeof(ServedClassNameAttribute), false);
if (attrbutes.Length > 0)
{
//MessageBox.Show("Adding Type: " + type.Name + " " + type.FullName);
_sComObjectTypes.Add(type); //PWGS - much simpler
//_sComObjectAssys.Add(so);
}
}
}
catch (BadImageFormatException)
{
// Probably an attempt to load a Win32 DLL (i.e. not a .net assembly)
// Just swallow the exception and continue to the next item.
}
catch (Exception e)
{
MessageBox.Show(string.Format(Resources.Server_LoadComObjectAssemblies_Failed_to_load_served_COM_class_assembly__0_____1_, fi.Name, e.Message),
DriverName, MessageBoxButtons.OK, MessageBoxIcon.Stop);
return false;
}
}
return true;
}
#endregion
#region COM Registration and Unregistration
//
// Test if running elevated
//
private static bool IsAdministrator
{
get
{
WindowsIdentity i = WindowsIdentity.GetCurrent();
WindowsPrincipal p = new WindowsPrincipal(i);
return p.IsInRole(WindowsBuiltInRole.Administrator);
}
}
//
// Elevate by re-running ourselves with elevation dialog
//
private static void ElevateSelf(string arg)
{
var si = new ProcessStartInfo
{
Arguments = arg,
WorkingDirectory = Environment.CurrentDirectory,
FileName = Application.ExecutablePath,
Verb = "runas"
};
try { Process.Start(si); }
catch (Win32Exception)
{
MessageBox.Show(string.Format(Resources.Server_ElevateSelf_The__0__was_not__1__because_you_did_not_allow_it_, DriverName, (arg == "/register" ? "registered" : "unregistered")), DriverName, MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString(), DriverName, MessageBoxButtons.OK, MessageBoxIcon.Stop);
}
}
//
// Do everything to register this for COM. Never use REGASM on
// this exe assembly! It would create InProcServer32 entries
// which would prevent proper activation!
//
// Using the list of COM object types generated during dynamic
// assembly loading, it registers each one for COM as served by our
// exe/local server, as well as registering it for ASCOM. It also
// adds DCOM info for the local server itself, so it can be activated
// via an outboiud connection from TheSky.
//
private static void RegisterObjects()
{
if (!IsAdministrator)
{
ElevateSelf("/register");
return;
}
//
// If reached here, we're running elevated
//
Assembly assy = Assembly.GetExecutingAssembly();
Attribute attr = Attribute.GetCustomAttribute(assy, typeof(AssemblyTitleAttribute));
string assyTitle = ((AssemblyTitleAttribute)attr).Title;
attr = Attribute.GetCustomAttribute(assy, typeof(AssemblyDescriptionAttribute));
string assyDescription = ((AssemblyDescriptionAttribute)attr).Description;
//
// Local server's DCOM/AppID information
//
try
{
//
// HKCR\APPID\appid
//
using (RegistryKey key = Registry.ClassesRoot.CreateSubKey("APPID\\" + _sAppId))
{
key?.SetValue(null, assyDescription);
key?.SetValue("AppID", _sAppId);
key?.SetValue("AuthenticationLevel", 1, RegistryValueKind.DWord);
}
//
// HKCR\APPID\exename.ext
//
using (RegistryKey key = Registry.ClassesRoot.CreateSubKey(
$"APPID\\{Application.ExecutablePath.Substring(Application.ExecutablePath.LastIndexOf('\\') + 1)}"))
{
key?.SetValue("AppID", _sAppId);
}
}
catch (Exception ex)
{
MessageBox.Show(string.Format(Resources.Server_RegisterObjects_, ex),
DriverName, MessageBoxButtons.OK, MessageBoxIcon.Stop);
return;
}
//
// For each of the driver assemblies
//
foreach (Type type in _sComObjectTypes)
{
bool bFail = false;
try
{
//
// HKCR\CLSID\clsid
//
string clsid = Marshal.GenerateGuidForType(type).ToString("B");
string progid = Marshal.GenerateProgIdForType(type);
//PWGS Generate device type from the Class name
string deviceType = type.Name;
using (RegistryKey key = Registry.ClassesRoot.CreateSubKey($"CLSID\\{clsid}"))
{
key?.SetValue(null, progid); // Could be assyTitle/Desc??, but .NET components show ProgId here
key?.SetValue("AppId", _sAppId);
using (RegistryKey key2 = key.CreateSubKey("Implemented Categories"))
{
key2?.CreateSubKey("{62C8FE65-4EBB-45e7-B440-6E39B2CDBF29}");
}
using (RegistryKey key2 = key.CreateSubKey("ProgId"))
{
key2?.SetValue(null, progid);
}
key.CreateSubKey("Programmable");
using (RegistryKey key2 = key.CreateSubKey("LocalServer32"))
{
key2?.SetValue(null, Application.ExecutablePath);
}
}
//
// HKCR\CLSID\progid
//
using (RegistryKey key = Registry.ClassesRoot.CreateSubKey(progid))
{
key?.SetValue(null, assyTitle);
using (RegistryKey key2 = key.CreateSubKey("CLSID"))
{
key2?.SetValue(null, clsid);
}
}
//
// ASCOM
//
//assy = type.Assembly;
// Pull the display name from the ServedClassName attribute.
attr = Attribute.GetCustomAttribute(type, typeof(ServedClassNameAttribute)); //PWGS Changed to search type for attribute rather than assembly
string chooserName = ((ServedClassNameAttribute)attr).DisplayName ?? "MultiServer";
using (var p = new Profile())
{
p.DeviceType = deviceType;
p.Register(progid, chooserName);
}
}
catch (Exception ex)
{
MessageBox.Show(string.Format(Resources.Server_RegisterObjects_, ex),
DriverName, MessageBoxButtons.OK, MessageBoxIcon.Stop);
bFail = true;
}
if (bFail) break;
}
}
//
// Remove all traces of this from the registry.
//
// **TODO** If the above does AppID/DCOM stuff, this would have
// to remove that stuff too.
//
private static void UnregisterObjects()
{
if (!IsAdministrator)
{
ElevateSelf("/unregister");
return;
}
//
// Local server's DCOM/AppID information
//
Registry.ClassesRoot.DeleteSubKey($"APPID\\{_sAppId}", false);
Registry.ClassesRoot.DeleteSubKey(
$"APPID\\{Application.ExecutablePath.Substring(Application.ExecutablePath.LastIndexOf('\\') + 1)}", false);
//
// For each of the driver assemblies
//
foreach (Type type in _sComObjectTypes)
{
string clsid = Marshal.GenerateGuidForType(type).ToString("B");
string progid = Marshal.GenerateProgIdForType(type);
string deviceType = type.Name;
//
// Best efforts
//
//
// HKCR\progid
//
Registry.ClassesRoot.DeleteSubKey($"{progid}\\CLSID", false);
Registry.ClassesRoot.DeleteSubKey(progid, false);
//
// HKCR\CLSID\clsid
//
Registry.ClassesRoot.DeleteSubKey($"CLSID\\{clsid}\\Implemented Categories\\{{62C8FE65-4EBB-45e7-B440-6E39B2CDBF29}}", false);
Registry.ClassesRoot.DeleteSubKey($"CLSID\\{clsid}\\Implemented Categories", false);
Registry.ClassesRoot.DeleteSubKey($"CLSID\\{clsid}\\ProgId", false);
Registry.ClassesRoot.DeleteSubKey($"CLSID\\{clsid}\\LocalServer32", false);
Registry.ClassesRoot.DeleteSubKey($"CLSID\\{clsid}\\Programmable", false);
Registry.ClassesRoot.DeleteSubKey($"CLSID\\{clsid}", false);
try
{
//
// ASCOM
//
using (var p = new Profile())
{
p.DeviceType = deviceType;
p.Unregister(progid);
}
}
catch (Exception)
{
// ignored
}
}
}
#endregion
#region Class Factory Support
//
// On startup, we register the class factories of the COM objects
// that we serve. This requires the class facgtory name to be
// equal to the served class name + "ClassFactory".
//
private static void RegisterClassFactories()
{
_sClassFactories = new ArrayList();
foreach (Type type in _sComObjectTypes)
{
ClassFactory factory = new ClassFactory(type); // Use default context & flags
_sClassFactories.Add(factory);
if (!factory.RegisterClassObject())
{
MessageBox.Show(string.Format(Resources.Server_RegisterClassFactories_Failed_to_register_class_factory_for__0_, type.Name),
DriverName, MessageBoxButtons.OK, MessageBoxIcon.Stop);
return;
}
}
ClassFactory.ResumeClassObjects(); // Served objects now go live
}
private static void RevokeClassFactories()
{
ClassFactory.SuspendClassObjects(); // Prevent race conditions
foreach (ClassFactory factory in _sClassFactories)
factory.RevokeClassObject();
}
#endregion
#region Command Line Arguments
//
// ProcessArguments() will process the command-line arguments
// If the return value is true, we carry on and start this application.
// If the return value is false, we terminate this application immediately.
//
private static bool ProcessArguments(string[] args)
{
bool bRet = true;
//
//**TODO** -Embedding is "ActiveX start". Prohibit non_AX starting?
//
if (args.Length > 0)
{
switch (args[0].ToLower())
{
case "-embedding":
StartedByCom = true; // Indicate COM started us
break;
case "-register":
case @"/register":
case "-regserver": // Emulate VB6
case @"/regserver":
RegisterObjects(); // Register each served object
bRet = false;
break;
case "-unregister":
case @"/unregister":
case "-unregserver": // Emulate VB6
case @"/unregserver":
UnregisterObjects(); //Unregister each served object
bRet = false;
break;
default:
MessageBox.Show(
string.Format(Resources.Server_ProcessArguments_, args[0]),
DriverName, MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
break;
}
}
else
StartedByCom = false;
return bRet;
}
#endregion
#region SERVER ENTRY POINT (main)
//
// ==================
// SERVER ENTRY POINT
// ==================
//
[STAThread]
static void Main(string[] args)
{
if (!LoadComObjectAssemblies()) return; // Load served COM class assemblies, get types
if (!ProcessArguments(args)) return; // Register/Unregister
// Initialize critical member variables.
_objsInUse = 0;
_serverLocks = 0;
MainThreadId = GetCurrentThreadId();
Thread.CurrentThread.Name = "Main Thread";
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
_sMainForm = new FrmMain();
if (StartedByCom) _sMainForm.WindowState = FormWindowState.Minimized;
// Register the class factories of the served objects
RegisterClassFactories();
// Start up the garbage collection thread.
var garbageCollector = new GarbageCollection(1000);
var gcThread = new Thread(garbageCollector.GcWatch)
{
Name = "Garbage Collection Thread"
};
gcThread.Start();
//
// Start the message loop. This serializes incoming calls to our
// served COM objects, making this act like the VB6 equivalent!
//
try
{
Application.Run(_sMainForm);
}
finally
{
// Revoke the class factories immediately.
// Don't wait until the thread has stopped before
// we perform revocation!!!
RevokeClassFactories();
// Now stop the Garbage Collector thread.
garbageCollector.StopThread();
garbageCollector.WaitForThreadToStop();
}
}
#endregion
}
}