namespace DSInternals.DataStore { using System; using System.IO; using Microsoft.Database.Isam; using Microsoft.Isam.Esent.Interop; public class DirectoryContext : IDisposable { private const string JetInstanceName = "DSInternals"; private IsamInstance instance; private IsamSession session; private IsamDatabase database; private bool isDBAttached = false; /// /// Creates a new Active Directory database context. /// /// dbFilePath must point to the DIT file on the local computer. /// The path should point to a writeable folder on the local computer, where ESE log files will be created. If not specified, then temp folder will be used. public DirectoryContext(string dbFilePath, bool readOnly, string logDirectoryPath = null) { if (!File.Exists(dbFilePath)) { // TODO: Extract as resource throw new FileNotFoundException("The specified database file does not exist.", dbFilePath); } this.DSADatabaseFile = dbFilePath; ValidateDatabaseState(this.DSADatabaseFile); this.DSAWorkingDirectory = Path.GetDirectoryName(this.DSADatabaseFile); string checkpointDirectoryPath = this.DSAWorkingDirectory; string tempDirectoryPath = this.DSAWorkingDirectory; this.DatabaseLogFilesPath = logDirectoryPath; if (this.DatabaseLogFilesPath != null) { if (!Directory.Exists(this.DatabaseLogFilesPath)) { // TODO: Extract as resource throw new FileNotFoundException("The specified log directory does not exist.", this.DatabaseLogFilesPath); } } else { this.DatabaseLogFilesPath = this.DSAWorkingDirectory; } // TODO: Exception handling? // HACK: IsamInstance constructor throws AccessDenied Exception when the path does not end with a backslash. this.instance = new IsamInstance(AddPathSeparator(checkpointDirectoryPath), AddPathSeparator(this.DatabaseLogFilesPath), AddPathSeparator(tempDirectoryPath), ADConstants.EseBaseName, JetInstanceName, readOnly, ADConstants.PageSize); try { var isamParameters = this.instance.IsamSystemParameters; // Set the size of the transaction log files to AD defaults. isamParameters.LogFileSize = ADConstants.EseLogFileSize; // Delete the log files that are not matching (generation wise) during soft recovery. isamParameters.DeleteOutOfRangeLogs = true; // Check the database for indexes over Unicode key columns that were built using an older version of the NLS library. isamParameters.EnableIndexChecking = true; // Automatically clean up indexes over Unicode key columns as necessary to avoid database format changes caused by changes to the NLS library. isamParameters.EnableIndexCleanup = true; // Retain only transaction log files that are younger than the current checkpoint. isamParameters.CircularLog = true; // Disable all database engine callbacks to application provided functions. This enables us to open Win2016 DBs on non-DC systems. isamParameters.DisableCallbacks = true; // TODO: Configure additional ISAM parameters // this.instance.IsamSystemParameters.EnableOnlineDefrag = false; this.session = this.instance.CreateSession(); this.session.AttachDatabase(this.DSADatabaseFile); this.isDBAttached = true; this.database = this.session.OpenDatabase(this.DSADatabaseFile); this.Schema = new DirectorySchema(this.database); this.SecurityDescriptorRersolver = new SecurityDescriptorRersolver(this.database); this.DistinguishedNameResolver = new DistinguishedNameResolver(this.database, this.Schema); this.LinkResolver = new LinkResolver(this.database, this.Schema); this.DomainController = new DomainController(this); } catch (EsentUnicodeTranslationFailException unicodeException) { // This typically happens while opening a Windows Server 2003 DIT on a newer system. this.Dispose(); throw new InvalidDatabaseStateException("There was a problem reading the database, which probably comes from a legacy system. Try defragmenting it first by running the 'esentutl /d ntds.dit' command.", this.DSADatabaseFile, unicodeException); } catch { // Free resources if anything failed this.Dispose(); throw; } } public string DSAWorkingDirectory { get; private set; } public string DSADatabaseFile { get; private set; } public string DatabaseLogFilesPath { get; private set; } public DirectorySchema Schema { get; private set; } public LinkResolver LinkResolver { get; private set; } public DomainController DomainController { get; private set; } public DistinguishedNameResolver DistinguishedNameResolver { get; private set; } public SecurityDescriptorRersolver SecurityDescriptorRersolver { get; private set; } public Cursor OpenDataTable() { return this.database.OpenCursor(ADConstants.DataTableName); } public Cursor OpenLinkTable() { return this.database.OpenCursor(ADConstants.LinkTableName); } public Cursor OpenSystemTable() { return this.database.OpenCursor(ADConstants.SystemTableName); } public IsamTransaction BeginTransaction() { return new IsamTransaction(this.session); } public void Dispose() { this.Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (!disposing) { // Do nothing return; } if(this.LinkResolver != null) { this.LinkResolver.Dispose(); this.LinkResolver = null; } if (this.SecurityDescriptorRersolver != null) { this.SecurityDescriptorRersolver.Dispose(); this.SecurityDescriptorRersolver = null; } if (this.DistinguishedNameResolver != null) { this.DistinguishedNameResolver.Dispose(); this.DistinguishedNameResolver = null; } if (this.DomainController != null) { this.DomainController.Dispose(); this.DomainController = null; } if (this.database != null) { this.database.Dispose(); this.database = null; } if (this.session != null) { if (this.isDBAttached) { this.session.DetachDatabase(this.DSADatabaseFile); this.isDBAttached = false; } this.session.Dispose(); this.session = null; } if (this.instance != null) { this.instance.Dispose(); this.instance = null; } } private static string AddPathSeparator(string path) { // TODO: Newer version of ISAM should implemet this if (string.IsNullOrEmpty(path) || path.EndsWith(Path.DirectorySeparatorChar.ToString())) { // No need to add path separator return path; } else { return path + Path.DirectorySeparatorChar; } } private static void ValidateDatabaseState(string dbFilePath) { // Retrieve info about the DB (Win Version, Page Size, State,...) JET_DBINFOMISC dbInfo; Api.JetGetDatabaseFileInfo(dbFilePath, out dbInfo, JET_DbInfo.Misc); if (dbInfo.dbstate != JET_dbstate.CleanShutdown) { // Database might be inconsistent // TODO: Extract message as a recource throw new InvalidDatabaseStateException("The database is not in a clean state. Try to recover it first by running the 'esentutl /r edb /d' command.", dbFilePath); } } } }