#region License, Terms and Author(s) // // ELMAH - Error Logging Modules and Handlers for ASP.NET // Copyright (c) 2004-9 Atif Aziz. All rights reserved. // // Author(s): // // James Driscoll, mailto:jamesdriscoll@btinternet.com // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // #endregion [assembly: Elmah.Scc("$Id$")] namespace Elmah { #region Imports using System; using System.Data; using System.Data.OleDb; using System.Diagnostics; using System.Globalization; using System.IO; using System.Text; using IDictionary = System.Collections.IDictionary; using IList = System.Collections.IList; #endregion /// /// An implementation that uses Microsoft Access /// as its backing store. /// /// /// The MDB file is automatically created at the path specified in the /// connection string if it does not already exist. /// public class AccessErrorLog : ErrorLog { private readonly string _connectionString; private const int _maxAppNameLength = 60; private const string _scriptResourceName = "mkmdb.vbs"; private static readonly object _mdbInitializationLock = new object(); /// /// Initializes a new instance of the class /// using a dictionary of configured settings. /// public AccessErrorLog(IDictionary config) { if (config == null) throw new ArgumentNullException("config"); string connectionString = ConnectionStringHelper.GetConnectionString(config); // // If there is no connection string to use then throw an // exception to abort construction. // if (connectionString.Length == 0) throw new ApplicationException("Connection string is missing for the Access error log."); _connectionString = connectionString; InitializeDatabase(); // // Set the application name as this implementation provides // per-application isolation over a single store. // string appName = Mask.NullString((string)config["applicationName"]); if (appName.Length > _maxAppNameLength) { throw new ApplicationException(string.Format( "Application name is too long. Maximum length allowed is {0} characters.", _maxAppNameLength.ToString("N0"))); } ApplicationName = appName; } /// /// Initializes a new instance of the class /// to use a specific connection string for connecting to the database. /// public AccessErrorLog(string connectionString) { if (connectionString == null) throw new ArgumentNullException("connectionString"); if (connectionString.Length == 0) throw new ArgumentException(null, "connectionString"); _connectionString = connectionString; } /// /// Gets the name of this error log implementation. /// public override string Name { get { return "Microsoft Access Error Log"; } } /// /// Gets the connection string used by the log to connect to the database. /// public virtual string ConnectionString { get { return _connectionString; } } /// /// Logs an error to the database. /// /// /// Use the stored procedure called by this implementation to set a /// policy on how long errors are kept in the log. The default /// implementation stores all errors for an indefinite time. /// public override string Log(Error error) { if (error == null) throw new ArgumentNullException("error"); string errorXml = ErrorXml.EncodeString(error); using (OleDbConnection connection = new OleDbConnection(this.ConnectionString)) using (OleDbCommand command = connection.CreateCommand()) { connection.Open(); command.CommandType = CommandType.Text; command.CommandText = @"INSERT INTO ELMAH_Error (Application, Host, Type, Source, Message, UserName, StatusCode, TimeUtc, AllXml) VALUES (@Application, @Host, @Type, @Source, @Message, @UserName, @StatusCode, @TimeUtc, @AllXml)"; command.CommandType = CommandType.Text; OleDbParameterCollection parameters = command.Parameters; parameters.Add("@Application", OleDbType.VarChar, _maxAppNameLength).Value = ApplicationName; parameters.Add("@Host", OleDbType.VarChar, 30).Value = error.HostName; parameters.Add("@Type", OleDbType.VarChar, 100).Value = error.Type; parameters.Add("@Source", OleDbType.VarChar, 60).Value = error.Source; parameters.Add("@Message", OleDbType.LongVarChar, error.Message.Length).Value = error.Message; parameters.Add("@User", OleDbType.VarChar, 50).Value = error.User; parameters.Add("@StatusCode", OleDbType.Integer).Value = error.StatusCode; parameters.Add("@TimeUtc", OleDbType.Date).Value = error.Time.ToUniversalTime(); parameters.Add("@AllXml", OleDbType.LongVarChar, errorXml.Length).Value = errorXml; command.ExecuteNonQuery(); using (OleDbCommand identityCommand = connection.CreateCommand()) { identityCommand.CommandType = CommandType.Text; identityCommand.CommandText = "SELECT @@IDENTITY"; return Convert.ToString(identityCommand.ExecuteScalar(), CultureInfo.InvariantCulture); } } } /// /// Returns a page of errors from the databse in descending order /// of logged time. /// public override int GetErrors(int pageIndex, int pageSize, IList errorEntryList) { if (pageIndex < 0) throw new ArgumentOutOfRangeException("pageIndex", pageIndex, null); if (pageSize < 0) throw new ArgumentOutOfRangeException("pageSize", pageSize, null); using (OleDbConnection connection = new OleDbConnection(this.ConnectionString)) using (OleDbCommand command = connection.CreateCommand()) { command.CommandType = CommandType.Text; command.CommandText = "SELECT COUNT(*) FROM ELMAH_Error"; connection.Open(); int totalCount = (int)command.ExecuteScalar(); if (errorEntryList != null && pageIndex * pageSize < totalCount) { int maxRecords = pageSize * (pageIndex + 1); if (maxRecords > totalCount) { maxRecords = totalCount; pageSize = totalCount - pageSize * (totalCount / pageSize); } StringBuilder sql = new StringBuilder(1000); sql.Append("SELECT e.* FROM ("); sql.Append("SELECT TOP "); sql.Append(pageSize.ToString(CultureInfo.InvariantCulture)); sql.Append(" TimeUtc, ErrorId FROM ("); sql.Append("SELECT TOP "); sql.Append(maxRecords.ToString(CultureInfo.InvariantCulture)); sql.Append(" TimeUtc, ErrorId FROM ELMAH_Error "); sql.Append("ORDER BY TimeUtc DESC, ErrorId DESC) "); sql.Append("ORDER BY TimeUtc ASC, ErrorId ASC) AS i "); sql.Append("INNER JOIN Elmah_Error AS e ON i.ErrorId = e.ErrorId "); sql.Append("ORDER BY e.TimeUtc DESC, e.ErrorId DESC"); command.CommandText = sql.ToString(); using (OleDbDataReader reader = command.ExecuteReader()) { Debug.Assert(reader != null); while (reader.Read()) { string id = Convert.ToString(reader["ErrorId"], CultureInfo.InvariantCulture); Error error = new Error(); error.ApplicationName = reader["Application"].ToString(); error.HostName = reader["Host"].ToString(); error.Type = reader["Type"].ToString(); error.Source = reader["Source"].ToString(); error.Message = reader["Message"].ToString(); error.User = reader["UserName"].ToString(); error.StatusCode = Convert.ToInt32(reader["StatusCode"]); error.Time = Convert.ToDateTime(reader["TimeUtc"]).ToLocalTime(); errorEntryList.Add(new ErrorLogEntry(this, id, error)); } reader.Close(); } } return totalCount; } } /// /// Returns the specified error from the database, or null /// if it does not exist. /// public override ErrorLogEntry GetError(string id) { if (id == null) throw new ArgumentNullException("id"); if (id.Length == 0) throw new ArgumentException(null, "id"); int errorId; try { errorId = int.Parse(id, CultureInfo.InvariantCulture); } catch (FormatException e) { throw new ArgumentException(e.Message, "id", e); } catch (OverflowException e) { throw new ArgumentException(e.Message, "id", e); } string errorXml; using (OleDbConnection connection = new OleDbConnection(this.ConnectionString)) using (OleDbCommand command = connection.CreateCommand()) { command.CommandText = @"SELECT AllXml FROM ELMAH_Error WHERE ErrorId = @ErrorId"; command.CommandType = CommandType.Text; OleDbParameterCollection parameters = command.Parameters; parameters.Add("@ErrorId", OleDbType.Integer).Value = errorId; connection.Open(); errorXml = (string)command.ExecuteScalar(); } if (errorXml == null) return null; Error error = ErrorXml.DecodeString(errorXml); return new ErrorLogEntry(this, id, error); } private void InitializeDatabase() { string connectionString = ConnectionString; Debug.AssertStringNotEmpty(connectionString); string dbFilePath = ConnectionStringHelper.GetDataSourceFilePath(connectionString); if (File.Exists(dbFilePath)) return; // // Make sure that we don't have multiple instances trying to create the database. // lock (_mdbInitializationLock) { // // Just double-check that no other thread has created the database while // we were waiting for the lock. // if (File.Exists(dbFilePath)) return; // // Create a temporary copy of the mkmdb.vbs script. // We do this in the same directory as the resulting database for security permission purposes. // string scriptPath = Path.Combine(Path.GetDirectoryName(dbFilePath), _scriptResourceName); using (FileStream scriptStream = new FileStream(scriptPath, FileMode.Create, FileAccess.Write, FileShare.None)) { ManifestResourceHelper.WriteResourceToStream(scriptStream, _scriptResourceName); } // // Run the script file to create the database using batch // mode (//B), which suppresses script errors and prompts // from displaying. // ProcessStartInfo psi = new ProcessStartInfo( "cscript", "\"" + scriptPath + "\" \"" + dbFilePath + "\" //B //NoLogo"); psi.UseShellExecute = false; // i.e. CreateProcess psi.CreateNoWindow = true; // Stay lean, stay mean try { using (Process process = Process.Start(psi)) { // // A few seconds should be plenty of time to create the database. // TimeSpan tolerance = TimeSpan.FromSeconds(2); if (!process.WaitForExit((int) tolerance.TotalMilliseconds)) { // // but it wasn't, so clean up and throw an exception! // Realistically, I don't expect to ever get here! // process.Kill(); throw new Exception(string.Format( "The Microsoft Access database creation script took longer than the allocated time of {0} seconds to execute. " + "The script was terminated prematurely.", tolerance.TotalSeconds)); } if (process.ExitCode != 0) { throw new Exception(string.Format( "The Microsoft Access database creation script failed with exit code {0}.", process.ExitCode)); } } } finally { // // Clean up after ourselves!! // File.Delete(scriptPath); } } } } }