using System; using System.Collections.Generic; using System.IO; using System.Reflection; using System.Text.RegularExpressions; using Microsoft.Win32; using Serilog; using SyncEngine.Configuration; namespace SyncEngine.Access { /// /// Opens Access metadata via full Access.Application when available; on Access Runtime /// (CreateInstance fails with CO_E_SERVER_EXEC_FAILURE) opens the backend with /// DAO.DBEngine.OpenDatabase + ;PWD=… — no MSACCESS.EXE UI, so no modal dialogs. /// Note: MSACCESS /pwd is workgroup account password only — never the database encrypt password. /// internal static class AccessApplicationSysReader { public static void TryGrantAndReadMetadata( SyncOptions options, string configuredUser, IList queries, IList keys, HashSet tableSet, ILogger logger) { object app = null; object db = null; object engine = null; try { logger.Information("Opening Access for GRANT and query export..."); if (!TryOpenAccessApplication(options, logger, out app, out db)) { logger.Warning( "Access.Application unavailable; opening via DAO.DBEngine (no MSACCESS UI)..."); OpenDaoDatabase(options, out engine, out db); logger.Information("Skipping MSys GRANT on DAO-only path (requires Access.Application)."); } else { var grantUser = ResolveGrantUser(app, configuredUser); logger.Information("Granting MSys access to {User}...", grantUser); var connection = GetProjectConnection(app); TryGrant(connection, grantUser, logger); AccessDaoHelper.ReleaseCom(connection); } logger.Information("Reading saved queries from QueryDefs..."); AccessDaoHelper.ReadQueryDefs(db, queries, logger); logger.Information("Read {Count} QueryDefs", queries.Count); logger.Information("Reading table relationships from Relations..."); AccessDaoHelper.ReadRelations(db, keys, tableSet, logger); logger.Information("Read {Count} relationships", keys.Count); } catch (Exception ex) { logger.Warning( "Access metadata step failed ({Message})", AccessDaoHelper.FormatException(ex)); } finally { CloseDaoSession(app, db, engine, logger); } } /// Reads field DefaultValue and ValidationRule via DAO. public static void TryReadFieldMetadataViaDao( SyncOptions options, IList tables, HashSet tableSet, ILogger logger) { object app = null; object db = null; object engine = null; try { logger.Information("Opening Access for field metadata (defaults, validation)..."); if (!TryOpenAccessApplication(options, logger, out app, out db)) { logger.Warning( "Access.Application unavailable; opening via DAO.DBEngine (no MSACCESS UI)..."); OpenDaoDatabase(options, out engine, out db); } AccessDaoHelper.ReadFieldMetadata(db, tables, tableSet, logger); } catch (Exception ex) { logger.Warning( "Access field metadata read failed ({Message})", AccessDaoHelper.FormatException(ex)); } finally { CloseDaoSession(app, db, engine, null); } } /// Reads TableDefs.Indexes when OleDb MSysIndexes is unavailable. public static void TryReadIndexesViaDao( SyncOptions options, IList indexes, HashSet tableSet, ILogger logger) { object app = null; object db = null; object engine = null; try { logger.Information("Opening Access for TableDefs.Indexes fallback..."); if (!TryOpenAccessApplication(options, logger, out app, out db)) { logger.Warning( "Access.Application unavailable; opening via DAO.DBEngine (no MSACCESS UI)..."); OpenDaoDatabase(options, out engine, out db); } AccessDaoHelper.ReadTableIndexes(db, indexes, tableSet, logger); logger.Information("Read {Count} indexes from TableDefs", indexes.Count); } catch (Exception ex) { logger.Warning( "Access index read failed ({Message})", AccessDaoHelper.FormatException(ex)); } finally { CloseDaoSession(app, db, engine, null); } } private static bool TryOpenAccessApplication( SyncOptions options, ILogger logger, out object app, out object db) { app = null; db = null; var appType = Type.GetTypeFromProgID("Access.Application"); if (appType == null) { return false; } try { app = OpenViaCreateInstance(appType, options); db = InvokeCurrentDb(app); return true; } catch (Exception ex) { logger.Debug( "Access.Application CreateInstance/OpenCurrentDatabase failed ({Message})", AccessDaoHelper.FormatException(ex)); CloseApplication(app); app = null; AccessDaoHelper.ReleaseCom(db); db = null; return false; } } private static void OpenDaoDatabase(SyncOptions options, out object engine, out object db) { if (string.IsNullOrWhiteSpace(options.AccessDbPath) || !File.Exists(options.AccessDbPath)) { throw new InvalidOperationException("ACCESS_DB_PATH is missing or not found for DAO open."); } var engineType = Type.GetTypeFromProgID("DAO.DBEngine.120") ?? Type.GetTypeFromProgID("DAO.DBEngine.36"); if (engineType == null) { throw new InvalidOperationException( "DAO.DBEngine is not registered (install ACE / Access Runtime)."); } engine = Activator.CreateInstance(engineType); var connect = string.IsNullOrEmpty(options.AccessDbPassword) ? string.Empty : ";PWD=" + options.AccessDbPassword; try { // OpenDatabase(Name, Exclusive, ReadOnly, Connect) db = engineType.InvokeMember( "OpenDatabase", BindingFlags.InvokeMethod, null, engine, new object[] { options.AccessDbPath, false, false, connect }); } catch { AccessDaoHelper.ReleaseCom(engine); engine = null; throw; } } private static void CloseDaoSession(object app, object db, object engine, ILogger logger) { if (app != null) { AccessDaoHelper.ReleaseCom(db); if (logger != null) { logger.Information("Closing Access.Application..."); } CloseApplication(app); if (logger != null) { logger.Information("Access.Application closed."); } return; } if (db != null) { try { db.GetType().InvokeMember( "Close", BindingFlags.InvokeMethod, null, db, null); } catch { // ignore } AccessDaoHelper.ReleaseCom(db); } AccessDaoHelper.ReleaseCom(engine); } private static string ResolveGrantUser(object app, string configuredUser) { if (!string.IsNullOrWhiteSpace(configuredUser) && !string.Equals(configuredUser, "current", StringComparison.OrdinalIgnoreCase)) { return configuredUser.Trim(); } var appType = app.GetType(); try { return Convert.ToString(appType.InvokeMember( "CurrentUser", BindingFlags.InvokeMethod, null, app, null)); } catch (Exception) { return Convert.ToString(appType.InvokeMember( "CurrentUser", BindingFlags.GetProperty, null, app, null)); } } private static object InvokeCurrentDb(object app) { var appType = app.GetType(); try { return appType.InvokeMember( "CurrentDb", BindingFlags.InvokeMethod, null, app, null); } catch (Exception) { return appType.InvokeMember( "CurrentDb", BindingFlags.GetProperty, null, app, null); } } private static void TryGrant(object connection, string userName, ILogger logger) { foreach (var table in new[] { "MSysObjects", "MSysRelationships", "MSysIndexes" }) { try { var ddl = string.Format("GRANT SELECT ON {0} TO [{1}];", table, userName); connection.GetType().InvokeMember( "Execute", BindingFlags.InvokeMethod, null, connection, new object[] { ddl }); logger.Information( "Granted SELECT on {Table} to {User} (via Access.Application)", table, userName); } catch (Exception ex) { logger.Debug( "GRANT SELECT on {Table} via Access.Application ({Message})", table, AccessDaoHelper.FormatException(ex)); } } } private static object OpenViaCreateInstance(Type appType, SyncOptions options) { var app = Activator.CreateInstance(appType); try { ConfigureHeadless(app); OpenCurrentDatabase(app, options); return app; } catch { CloseApplication(app); throw; } } private static void ConfigureHeadless(object app) { try { app.GetType().InvokeMember( "Visible", BindingFlags.SetProperty, null, app, new object[] { false }); } catch { // Runtime may reject Visible; continue } try { app.GetType().InvokeMember( "UserControl", BindingFlags.SetProperty, null, app, new object[] { false }); } catch { // Runtime may reject UserControl; continue } } private static void OpenCurrentDatabase(object app, SyncOptions options) { app.GetType().InvokeMember( "OpenCurrentDatabase", BindingFlags.InvokeMethod, null, app, new object[] { options.AccessDbPath, false, string.IsNullOrEmpty(options.AccessDbPassword) ? Type.Missing : (object)options.AccessDbPassword }); } /// Shell /user value: ACCESS_DB_USER when set; otherwise Admin. internal static string ResolveShellUser(string accessDbUser) { if (string.IsNullOrWhiteSpace(accessDbUser) || string.Equals(accessDbUser, "current", StringComparison.OrdinalIgnoreCase)) { return "Admin"; } return accessDbUser.Trim(); } /// /// Builds MSACCESS.EXE args if Shell is ever needed: stub path + /user. /// /pwd is workgroup account password only — never the database encrypt password. /// internal static string BuildShellArguments(string stubDbPath, string user, string workgroupPassword) { var args = "\"" + stubDbPath + "\" /user " + QuoteShellArg(user ?? "Admin"); if (!string.IsNullOrEmpty(workgroupPassword)) { args += " /pwd " + QuoteShellArg(workgroupPassword); } return args; } internal static string QuoteShellArg(string value) { if (value == null) { return "\"\""; } if (value.IndexOfAny(new[] { ' ', '\t', '"' }) < 0) { return value; } return "\"" + value.Replace("\"", "\\\"") + "\""; } internal static string MaskPasswordArgs(string args, string password) { if (string.IsNullOrEmpty(password) || string.IsNullOrEmpty(args)) { return args; } return args.Replace(password, "***"); } internal static string TryExtractExeFromOpenCommand(string command) { if (string.IsNullOrWhiteSpace(command)) { return null; } var quoted = Regex.Match(command, "\"([^\"]*MSACCESS\\.EXE)\"", RegexOptions.IgnoreCase); if (quoted.Success) { return quoted.Groups[1].Value; } var unquoted = Regex.Match(command, @"(?i)([A-Za-z]:\\[^\s]*MSACCESS\.EXE)"); if (unquoted.Success) { return unquoted.Groups[1].Value; } return null; } internal static string ResolveMsAccessExe() { foreach (var hive in new[] { RegistryHive.LocalMachine }) { foreach (var view in new[] { RegistryView.Registry64, RegistryView.Registry32 }) { try { using (var baseKey = RegistryKey.OpenBaseKey(hive, view)) using (var classes = baseKey.OpenSubKey(@"Software\Classes")) { if (classes == null) { continue; } var exe = ResolveMsAccessExeFromClasses(classes); if (!string.IsNullOrEmpty(exe) && File.Exists(exe)) { return exe; } } } catch { // try next view } } } foreach (var candidate in WellKnownMsAccessPaths()) { if (File.Exists(candidate)) { return candidate; } } return null; } private static string ResolveMsAccessExeFromClasses(RegistryKey classes) { using (var curVerKey = classes.OpenSubKey(@"Access.Application\CurVer")) { if (curVerKey == null) { return null; } var curVer = curVerKey.GetValue(null) as string; if (string.IsNullOrWhiteSpace(curVer)) { return null; } using (var cmdKey = classes.OpenSubKey(curVer + @"\shell\Open\command")) { if (cmdKey == null) { return null; } return TryExtractExeFromOpenCommand(cmdKey.GetValue(null) as string); } } } private static IEnumerable WellKnownMsAccessPaths() { var pf = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); var pf86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); yield return Path.Combine(pf, @"Microsoft Office\root\Office16\MSACCESS.EXE"); yield return Path.Combine(pf, @"Microsoft Office\Office16\MSACCESS.EXE"); if (!string.IsNullOrEmpty(pf86)) { yield return Path.Combine(pf86, @"Microsoft Office\root\Office16\MSACCESS.EXE"); yield return Path.Combine(pf86, @"Microsoft Office\Office16\MSACCESS.EXE"); } } private static object GetProjectConnection(object app) { var currentProject = app.GetType().InvokeMember( "CurrentProject", BindingFlags.GetProperty, null, app, null); var connection = currentProject.GetType().InvokeMember( "Connection", BindingFlags.GetProperty, null, currentProject, null); AccessDaoHelper.ReleaseCom(currentProject); return connection; } private static void CloseApplication(object app) { if (app == null) { return; } try { app.GetType().InvokeMember( "Quit", BindingFlags.InvokeMethod, null, app, new object[] { 1 }); } catch { // ignore quit errors during cleanup } try { app.GetType().InvokeMember( "CloseCurrentDatabase", BindingFlags.InvokeMethod, null, app, null); } catch { // ignore close errors during cleanup } AccessDaoHelper.ReleaseCom(app); } } }