From 376cc21f12a527c47391f21366e4935798f196bd Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Fri, 23 Mar 2018 13:25:37 -0700 Subject: [PATCH] Initial SQL Agent merge for March release (#595) * Initial SQL Agent WIP * Fix test build break * wip * wip2 * additonal SQL Agent history parsing code * Fix namespace sorting * Hook up agent\jobs method * Fix broken integration test build * Added handler for job history request (#586) * fixed agent service tests * added job history handler * code review refactoring * Turn off another failing test * Disable failing test * Feature/agent1 adbist (#592) * fixed agent service tests * added job history handler * code review refactoring * refactored code * small refactor * code review changes * Remove unused code * Remove unused test file * Feature/agent1 adbist (#593) * fixed agent service tests * added job history handler * code review refactoring * refactored code * small refactor * code review changes * changed constant casing * added handler for job actions * Reenable disabled test * cleaned up code --- .../Admin/AdminService.cs | 10 +- .../Agent/AgentInterfaces.cs | 20 + .../Agent/AgentService.cs | 227 ++++++ .../Agent/Contracts/AgentJobActionRequest.cs | 46 ++ .../Agent/Contracts/AgentJobHistoryInfo.cs | 38 + .../Agent/Contracts/AgentJobHistoryRequest.cs | 46 ++ .../Agent/Contracts/AgentJobInfo.cs | 34 + .../Agent/Contracts/AgentJobsRequest.cs | 47 ++ .../Agent/JobActivityFilter.cs | 347 ++++++++ .../Agent/JobFetcher.cs | 555 +++++++++++++ .../Agent/JobHelper.cs | 100 +++ .../Agent/JobHistoryItem.cs | 752 ++++++++++++++++++ .../Agent/JobUtilities.cs | 106 +++ .../Agent/LogAggregator.cs | 704 ++++++++++++++++ .../Agent/LogInterfaces.cs | 274 +++++++ .../HostLoader.cs | 4 + .../Agent/AgentServiceTests.cs | 97 +++ 17 files changed, 3402 insertions(+), 5 deletions(-) create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Agent/AgentInterfaces.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Agent/AgentService.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Agent/Contracts/AgentJobActionRequest.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Agent/Contracts/AgentJobHistoryInfo.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Agent/Contracts/AgentJobHistoryRequest.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Agent/Contracts/AgentJobInfo.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Agent/Contracts/AgentJobsRequest.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Agent/JobActivityFilter.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Agent/JobFetcher.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Agent/JobHelper.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Agent/JobHistoryItem.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Agent/JobUtilities.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Agent/LogAggregator.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Agent/LogInterfaces.cs create mode 100644 test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Agent/AgentServiceTests.cs diff --git a/src/Microsoft.SqlTools.ServiceLayer/Admin/AdminService.cs b/src/Microsoft.SqlTools.ServiceLayer/Admin/AdminService.cs index 19b3985b..316cad97 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Admin/AdminService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Admin/AdminService.cs @@ -3,16 +3,16 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using System; +using System.Collections.Concurrent; +using System.Threading.Tasks; +using System.Xml; using Microsoft.SqlTools.Hosting.Protocol; using Microsoft.SqlTools.ServiceLayer.Admin.Contracts; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.Hosting; -using System; -using System.Threading.Tasks; -using System.Xml; -using Microsoft.SqlServer.Management.Smo; -using System.Collections.Concurrent; using Microsoft.SqlTools.ServiceLayer.Utility; +using Microsoft.SqlServer.Management.Smo; namespace Microsoft.SqlTools.ServiceLayer.Admin { diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/AgentInterfaces.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/AgentInterfaces.cs new file mode 100644 index 00000000..9fd1e25e --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Agent/AgentInterfaces.cs @@ -0,0 +1,20 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.SqlTools.ServiceLayer.Agent +{ + interface IFilterDefinition + { + object ShallowClone(); + + void ShallowCopy(object template); + + void ResetToDefault(); + + bool IsDefault(); + + bool Enabled { get; set; } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/AgentService.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/AgentService.cs new file mode 100644 index 00000000..5b627578 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Agent/AgentService.cs @@ -0,0 +1,227 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.SqlServer.Management.Common; +using Microsoft.SqlServer.Management.Smo; +using Microsoft.SqlServer.Management.Smo.Agent; +using Microsoft.SqlTools.Hosting.Protocol; +using Microsoft.SqlTools.ServiceLayer.Agent.Contracts; +using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.Hosting; +using Microsoft.SqlTools.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.Agent +{ + /// + /// Main class for Profiler Service functionality + /// + public sealed class AgentService + { + private Dictionary jobs = null; + private ConnectionService connectionService = null; + private static readonly Lazy instance = new Lazy(() => new AgentService()); + + /// + /// Construct a new AgentService instance with default parameters + /// + public AgentService() + { + } + + /// + /// Gets the singleton instance object + /// + public static AgentService Instance + { + get { return instance.Value; } + } + + /// + /// Internal for testing purposes only + /// + internal ConnectionService ConnectionServiceInstance + { + get + { + if (connectionService == null) + { + connectionService = ConnectionService.Instance; + } + return connectionService; + } + + set + { + connectionService = value; + } + } + + + /// + /// Service host object for sending/receiving requests/events. + /// Internal for testing purposes. + /// + internal IProtocolEndpoint ServiceHost + { + get; + set; + } + + /// + /// Initializes the service instance + /// + public void InitializeService(ServiceHost serviceHost) + { + this.ServiceHost = serviceHost; + this.ServiceHost.SetRequestHandler(AgentJobsRequest.Type, HandleAgentJobsRequest); + this.ServiceHost.SetRequestHandler(AgentJobHistoryRequest.Type, HandleJobHistoryRequest); + this.ServiceHost.SetRequestHandler(AgentJobActionRequest.Type, HandleJobActionRequest); + } + + /// + /// Handle request to get Agent job activities + /// + internal async Task HandleAgentJobsRequest(AgentJobsParams parameters, RequestContext requestContext) + { + try + { + var result = new AgentJobsResult(); + ConnectionInfo connInfo; + ConnectionServiceInstance.TryFindConnection( + parameters.OwnerUri, + out connInfo); + + if (connInfo != null) + { + var sqlConnection = ConnectionService.OpenSqlConnection(connInfo); + var serverConnection = new ServerConnection(sqlConnection); + var fetcher = new JobFetcher(serverConnection); + var filter = new JobActivityFilter(); + this.jobs = fetcher.FetchJobs(filter); + + var agentJobs = new List(); + if (this.jobs != null) + { + + foreach (var job in this.jobs.Values) + { + agentJobs.Add(JobUtilities.ConvertToAgentJobInfo(job)); + } + } + result.Succeeded = true; + result.Jobs = agentJobs.ToArray(); + } + + await requestContext.SendResult(result); + } + catch (Exception e) + { + await requestContext.SendError(e); + } + } + + /// + /// Handle request to get Agent Job history + /// + internal async Task HandleJobHistoryRequest(AgentJobHistoryParams parameters, RequestContext requestContext) + { + try + { + var result = new AgentJobHistoryResult(); + ConnectionInfo connInfo; + ConnectionServiceInstance.TryFindConnection( + parameters.OwnerUri, + out connInfo); + if (connInfo != null) + { + Tuple tuple = CreateSqlConnection(connInfo, parameters.JobId); + SqlConnectionInfo sqlConnInfo = tuple.Item1; + DataTable dt = tuple.Item2; + int count = dt.Rows.Count; + var agentJobs = new List(); + for (int i = 0; i < count; ++i) + { + var job = dt.Rows[i]; + agentJobs.Add(JobUtilities.ConvertToAgentJobHistoryInfo(job, sqlConnInfo)); + } + result.Succeeded = true; + result.Jobs = agentJobs.ToArray(); + await requestContext.SendResult(result); + } + } + catch (Exception e) + { + await requestContext.SendError(e); + } + } + + /// + /// Handle request to Run a Job + /// + internal async Task HandleJobActionRequest(AgentJobActionParams parameters, RequestContext requestContext) + { + try + { + var result = new AgentJobActionResult(); + ConnectionInfo connInfo; + ConnectionServiceInstance.TryFindConnection( + parameters.OwnerUri, + out connInfo); + if (connInfo != null) + { + var sqlConnection = ConnectionService.OpenSqlConnection(connInfo); + var serverConnection = new ServerConnection(sqlConnection); + var jobHelper = new JobHelper(serverConnection); + jobHelper.JobName = parameters.JobName; + switch(parameters.Action) + { + case "run": + jobHelper.Start(); + break; + case "stop": + jobHelper.Stop(); + break; + case "delete": + jobHelper.Delete(); + break; + case "enable": + jobHelper.Enable(true); + break; + case "disable": + jobHelper.Enable(false); + break; + default: + break; + } + result.Succeeded = true; + await requestContext.SendResult(result); + } + } + catch (Exception e) + { + await requestContext.SendError(e); + } + } + + private Tuple CreateSqlConnection(ConnectionInfo connInfo, String jobId) + { + var sqlConnection = ConnectionService.OpenSqlConnection(connInfo); + var serverConnection = new ServerConnection(sqlConnection); + var server = new Server(serverConnection); + var filter = new JobHistoryFilter(); + filter.JobID = new Guid(jobId); + var dt = server.JobServer.EnumJobHistory(filter); + var sqlConnInfo = new SqlConnectionInfo(serverConnection, SqlServer.Management.Common.ConnectionType.SqlConnection); + return new Tuple(sqlConnInfo, dt); + } + + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/Contracts/AgentJobActionRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/Contracts/AgentJobActionRequest.cs new file mode 100644 index 00000000..4064c68a --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Agent/Contracts/AgentJobActionRequest.cs @@ -0,0 +1,46 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlTools.Hosting.Protocol.Contracts; +using Microsoft.SqlTools.ServiceLayer.Utility; +using Microsoft.SqlTools.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.Agent.Contracts +{ + /// + /// SQL Agent Job activity parameters + /// + public class AgentJobActionParams : GeneralRequestDetails + { + public string OwnerUri { get; set; } + + public string JobName { get; set; } + + public string Action { get; set; } + } + + /// + /// SQL Agent Job activity result + /// + public class AgentJobActionResult + { + public bool Succeeded { get; set; } + + public string ErrorMessage { get; set; } + } + + /// + /// SQL Agent Jobs request type + /// + public class AgentJobActionRequest + { + /// + /// Request definition + /// + public static readonly + RequestType Type = + RequestType.Create("agent/jobaction"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/Contracts/AgentJobHistoryInfo.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/Contracts/AgentJobHistoryInfo.cs new file mode 100644 index 00000000..2b322942 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Agent/Contracts/AgentJobHistoryInfo.cs @@ -0,0 +1,38 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Data; +using System.Collections.Generic; +using Microsoft.SqlServer.Management.Common; +using Microsoft.SqlTools.ServiceLayer.Agent; + +namespace Microsoft.SqlTools.ServiceLayer.Agent.Contracts +{ + /// + /// a class for storing various properties of agent jobs, + /// used by the Job Activity Monitor + /// + public class AgentJobHistoryInfo + { + public int InstanceId { get; set; } + public int SqlMessageId { get; set; } + public string Message { get; set; } + public int StepId { get; set; } + public string StepName { get; set; } + public int SqlSeverity { get; set; } + public Guid JobId { get; set; } + public string JobName { get; set; } + public int RunStatus { get; set; } + public DateTime RunDate { get; set; } + public int RunDuration { get; set; } + public string OperatorEmailed { get; set; } + public string OperatorNetsent { get; set; } + public string OperatorPaged { get; set; } + public int RetriesAttempted { get; set; } + public string Server { get; set; } + } + +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/Contracts/AgentJobHistoryRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/Contracts/AgentJobHistoryRequest.cs new file mode 100644 index 00000000..58a48604 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Agent/Contracts/AgentJobHistoryRequest.cs @@ -0,0 +1,46 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using Microsoft.SqlTools.Hosting.Protocol.Contracts; +using Microsoft.SqlTools.ServiceLayer.Utility; +using Microsoft.SqlTools.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.Agent.Contracts +{ + + public class AgentJobHistoryParams : GeneralRequestDetails + { + public string OwnerUri { get; set; } + + public string JobId { get; set; } + } + + /// + /// SQL Agent Job activity result + /// + public class AgentJobHistoryResult + { + + public bool Succeeded { get; set; } + + public string ErrorMessage { get; set; } + + public AgentJobHistoryInfo[] Jobs { get; set; } + } + + /// + /// SQL Agent Jobs request type + /// + public class AgentJobHistoryRequest + { + /// + /// Request definition + /// + public static readonly + RequestType Type = + RequestType.Create("agent/jobhistory"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/Contracts/AgentJobInfo.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/Contracts/AgentJobInfo.cs new file mode 100644 index 00000000..1cfe2f43 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Agent/Contracts/AgentJobInfo.cs @@ -0,0 +1,34 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using Microsoft.SqlTools.ServiceLayer.Agent; + +namespace Microsoft.SqlTools.ServiceLayer.Agent.Contracts +{ + /// + /// a class for storing various properties of agent jobs, + /// used by the Job Activity Monitor + /// + public class AgentJobInfo + { + public string Name { get; set; } + public int CurrentExecutionStatus { get; set; } + public int LastRunOutcome { get; set; } + public string CurrentExecutionStep { get; set; } + public bool Enabled { get; set; } + public bool HasTarget { get; set; } + public bool HasSchedule { get; set; } + public bool HasStep { get; set; } + public bool Runnable { get; set; } + public string Category { get; set; } + public int CategoryId { get; set; } + public int CategoryType { get; set; } + public string LastRun { get; set; } + public string NextRun { get; set; } + public string JobId { get; set; } + } + +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/Contracts/AgentJobsRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/Contracts/AgentJobsRequest.cs new file mode 100644 index 00000000..9c936d7d --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Agent/Contracts/AgentJobsRequest.cs @@ -0,0 +1,47 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlTools.Hosting.Protocol.Contracts; +using Microsoft.SqlTools.ServiceLayer.Utility; +using Microsoft.SqlTools.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.Agent.Contracts +{ + /// + /// SQL Agent Job activity parameters + /// + public class AgentJobsParams : GeneralRequestDetails + { + public string OwnerUri { get; set; } + + public string JobId { get; set; } + } + + /// + /// SQL Agent Job activity result + /// + public class AgentJobsResult + { + + public bool Succeeded { get; set; } + + public string ErrorMessage { get; set; } + + public AgentJobInfo[] Jobs { get; set; } + } + + /// + /// SQL Agent Jobs request type + /// + public class AgentJobsRequest + { + /// + /// Request definition + /// + public static readonly + RequestType Type = + RequestType.Create("agent/jobs"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/JobActivityFilter.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/JobActivityFilter.cs new file mode 100644 index 00000000..4d40cd9a --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Agent/JobActivityFilter.cs @@ -0,0 +1,347 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.ComponentModel; +using System.Text; +using Microsoft.SqlServer.Management.Sdk.Sfc; +using Microsoft.SqlServer.Management.Smo; + +namespace Microsoft.SqlTools.ServiceLayer.Agent +{ + // these map to the values for @execution_status + // that can be passed to sp_help_job (except the first one!) + // also the same as the smo enum JobExecutionStatus. + internal enum EnumStatus + { + All = -1, + NotIdleOrSuspended = 0, + Executing = 1, + WaitingForWorkerThread = 2, + BetweenRetries = 3, + Idle = 4, + Suspended = 5, + WaitingForStepToFinish = 6, + PerformingCompletionAction = 7 + } + + // + // these values map to CompletionResult values, except the first. + // + internal enum EnumCompletionResult + { + All = -1, + Failed = 0, + Succeeded = 1, + Retry = 2, + Cancelled = 3, + InProgress = 4, + Unknown = 5 + } + + // + // for boolean job properties + // + internal enum EnumThreeState + { + All, + Yes, + No + } + + /// + /// JobsFilter class - used to allow user to set filtering options for All Jobs Panel + /// + internal class JobActivityFilter : IFilterDefinition + { + /// + /// constructor + /// + public JobActivityFilter() + { + } + + + #region Properties + + private DateTime lastRunDate = new DateTime(); + private DateTime nextRunDate = new DateTime(); + private string name = string.Empty; + private string category = string.Empty; + private EnumStatus status = EnumStatus.All; + private EnumThreeState enabled = EnumThreeState.All; + private EnumThreeState runnable = EnumThreeState.All; + private EnumThreeState scheduled = EnumThreeState.All; + private EnumCompletionResult lastRunOutcome = EnumCompletionResult.All; + + private bool filterdefinitionEnabled = false; + + public EnumCompletionResult LastRunOutcome + { + get + { + return lastRunOutcome; + } + set + { + lastRunOutcome = value; + } + } + + public string Name + { + get + { + return name; + } + set + { + name = value.Trim(); + } + } + + public EnumThreeState Enabled + { + get + { + return enabled; + } + set + { + enabled = value; + } + } + + public EnumStatus Status + { + get + { + return status; + } + set + { + status = value; + } + } + + public DateTime LastRunDate + { + get + { + return lastRunDate; + } + set + { + lastRunDate = value; + } + } + + public DateTime NextRunDate + { + get + { + return nextRunDate; + } + set + { + nextRunDate = value; + } + } + + public string Category + { + get + { + return category; + } + set + { + category = value.Trim(); + } + } + + public EnumThreeState Runnable + { + get + { + return runnable; + } + set + { + runnable = value; + } + } + + public EnumThreeState Scheduled + { + get + { + return scheduled; + } + set + { + scheduled = value; + } + } + + #endregion + + #region IFilterDefinition - interface implementation + /// + /// resets values of this object to default contraint values + /// + void IFilterDefinition.ResetToDefault() + { + lastRunDate = new DateTime(); + nextRunDate = new DateTime(); + name = string.Empty; + category = string.Empty; + enabled = EnumThreeState.All; + status = EnumStatus.All; + runnable = EnumThreeState.All; + scheduled = EnumThreeState.All; + lastRunOutcome = EnumCompletionResult.All; + } + + /// + /// checks if the filter is the same with the default filter + /// + bool IFilterDefinition.IsDefault() + { + return (lastRunDate.Ticks == 0 && + nextRunDate.Ticks == 0 && + name.Length == 0 && + category.Length == 0 && + enabled == EnumThreeState.All && + status == EnumStatus.All && + runnable == EnumThreeState.All && + scheduled == EnumThreeState.All && + lastRunOutcome == EnumCompletionResult.All); + } + + /// + /// creates a shallow clone + /// + /// + object IFilterDefinition.ShallowClone() + { + JobActivityFilter clone = new JobActivityFilter(); + + clone.LastRunDate = this.LastRunDate; + clone.NextRunDate = this.NextRunDate; + clone.Name = this.Name; + clone.Category = this.Category; + clone.Enabled = this.Enabled; + clone.Status = this.Status; + clone.Runnable = this.Runnable; + clone.Scheduled = this.Scheduled; + clone.LastRunOutcome = this.LastRunOutcome; + + (clone as IFilterDefinition).Enabled = (this as IFilterDefinition).Enabled; + return clone; + } + + /// + /// setup-s filter definition based on a template + /// + /// + void IFilterDefinition.ShallowCopy(object template) + { + System.Diagnostics.Debug.Assert(template is JobActivityFilter); + + JobActivityFilter f = template as JobActivityFilter; + + this.LastRunDate = f.LastRunDate; + this.NextRunDate = f.NextRunDate; + this.Name = f.Name; + this.Category = f.Category; + this.Enabled = f.Enabled; + this.Status = f.Status; + this.Runnable = f.Runnable; + this.Scheduled = f.Scheduled; + this.LastRunOutcome = f.LastRunOutcome; + + (this as IFilterDefinition).Enabled = (template as IFilterDefinition).Enabled; + } + + + + /// + /// tells us if filtering is enabled or diabled + /// a disabled filter lets everything pass and filters nothing out + /// + bool IFilterDefinition.Enabled + { + get + { + return filterdefinitionEnabled; + } + set + { + filterdefinitionEnabled = value; + } + } + #endregion + + #region Build filter + + private void AddPrefix(StringBuilder sb, bool clauseAdded) + { + if (clauseAdded) + { + sb.Append(" and ( "); + } + else + { + sb.Append(" ( "); + } + } + + private void AddSuffix(StringBuilder sb) + { + sb.Append(" ) "); + } + + + /// + /// fetch an xpath clause used for filtering + /// jobs fetched by the enumerator. + /// note that all other properties must be filtered on the client + /// because enumerator will not filter properties that are fetched + /// at post-process time. We can't even filter on the job name here + /// since we have to do a case-insensitive "contains" comparision on the name. + /// + public string GetXPathClause() + { + if (this.enabled == EnumThreeState.All) + { + return string.Empty; + } + + bool clauseAdded = false; + StringBuilder sb = new StringBuilder(); + sb.Append("["); + + // + // enabled clause + // + if (this.enabled != EnumThreeState.All) + { + AddPrefix(sb, clauseAdded); + sb.Append("@IsEnabled = " + (this.enabled == EnumThreeState.Yes ? "true() " : "false() ")); + AddSuffix(sb); + clauseAdded = true; + } + + sb.Append("]"); + return sb.ToString(); + } + + #endregion + } +} + + + diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/JobFetcher.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/JobFetcher.cs new file mode 100644 index 00000000..70b79234 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Agent/JobFetcher.cs @@ -0,0 +1,555 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Text; +using System.Data; +using System.Globalization; +using System.Collections.Generic; +using Microsoft.SqlServer.Management.Common; +using Microsoft.SqlServer.Management.Sdk.Sfc; +using Microsoft.SqlServer.Management.Smo.Agent; +using SMO = Microsoft.SqlServer.Management.Smo; +using Microsoft.SqlTools.ServiceLayer.Agent.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.Agent +{ + internal class JobFetcher + { + private Enumerator enumerator = null; + private ServerConnection connection = null; + private SMO.Server server = null; + + public JobFetcher(ServerConnection connection) + { + System.Diagnostics.Debug.Assert(connection != null, "ServerConnection is null"); + this.enumerator = new Enumerator(); + this.connection = connection; + this.server = new SMO.Server(connection); + } + + // + // ServerConnection object should be passed from caller, + // who gets it from CDataContainer.ServerConnection + // + public Dictionary FetchJobs(JobActivityFilter filter) + { + string urn = server.JobServer.Urn.Value + "/Job"; + + if (filter != null) + { + urn += filter.GetXPathClause(); + return FilterJobs(FetchJobs(urn), filter); + } + + return FetchJobs(urn); + } + + /// + /// Filter Jobs that matches criteria specified in JobActivityFilter + /// here we filter jobs by properties that enumerator doesn't + /// support filtering on. + /// $ISSUE - - DevNote: Filtering Dictionaries can be easily done with Linq and System.Expressions in .NET 3.5 + /// This requires re-design of current code and might impact functionality / performance due to newer dependencies + /// We need to consider this change in future enhancements for Job Activity monitor + /// + /// + /// + /// + private Dictionary FilterJobs(Dictionary unfilteredJobs, + JobActivityFilter filter) + { + if (unfilteredJobs == null) + { + return null; + } + + if (filter == null || + (filter is IFilterDefinition && + ((filter as IFilterDefinition).Enabled == false || + (filter as IFilterDefinition).IsDefault()))) + { + return unfilteredJobs; + } + + Dictionary filteredJobs = new Dictionary(); + + // Apply Filter + foreach (JobProperties jobProperties in unfilteredJobs.Values) + { + // If this job passed all filter criteria then include in filteredJobs Dictionary + if (this.CheckIfNameMatchesJob(filter, jobProperties) && + this.CheckIfCategoryMatchesJob(filter, jobProperties) && + this.CheckIfEnabledStatusMatchesJob(filter, jobProperties) && + this.CheckIfScheduledStatusMatchesJob(filter, jobProperties) && + this.CheckIfJobStatusMatchesJob(filter, jobProperties) && + this.CheckIfLastRunOutcomeMatchesJob(filter, jobProperties) && + this.CheckIfLastRunDateIsGreater(filter, jobProperties) && + this.CheckifNextRunDateIsGreater(filter, jobProperties) && + this.CheckJobRunnableStatusMatchesJob(filter, jobProperties)) + { + filteredJobs.Add(jobProperties.JobID, jobProperties); + } + } + + return filteredJobs; + } + + /// + /// check if job runnable status in filter matches given job property + /// + /// + /// + /// + private bool CheckJobRunnableStatusMatchesJob(JobActivityFilter filter, JobProperties jobProperties) + { + bool isRunnableMatched = false; + // filter based on job runnable + switch (filter.Runnable) + { + // if All was selected, include in match + case EnumThreeState.All: + isRunnableMatched = true; + break; + + // if Yes was selected, include only if job that is runnable + case EnumThreeState.Yes: + if (jobProperties.Runnable) + { + isRunnableMatched = true; + } + break; + + // if Yes was selected, include only if job is not runnable + case EnumThreeState.No: + if (!jobProperties.Runnable) + { + isRunnableMatched = true; + } + break; + } + return isRunnableMatched; + } + + /// + /// Check if next run date for given job property is greater than the one specified in the filter + /// + /// + /// + /// + private bool CheckifNextRunDateIsGreater(JobActivityFilter filter, JobProperties jobProperties) + { + bool isNextRunOutDateMatched = false; + // filter next run date + if (filter.NextRunDate.Ticks == 0 || + jobProperties.NextRun >= filter.NextRunDate) + { + isNextRunOutDateMatched = true; + } + return isNextRunOutDateMatched; + } + + /// + /// Check if last run date for given job property is greater than the one specified in the filter + /// + /// + /// + /// + private bool CheckIfLastRunDateIsGreater(JobActivityFilter filter, JobProperties jobProperties) + { + bool isLastRunOutDateMatched = false; + // filter last run date + if (filter.LastRunDate.Ticks == 0 || + jobProperties.LastRun >= filter.LastRunDate) + { + isLastRunOutDateMatched = true; + } + + return isLastRunOutDateMatched; + } + + /// + /// check if last run status filter matches given job property + /// + /// + /// + /// + private bool CheckIfLastRunOutcomeMatchesJob(JobActivityFilter filter, JobProperties jobProperties) + { + bool isLastRunOutcomeMatched = false; + // filter - last run outcome + if (filter.LastRunOutcome == EnumCompletionResult.All || + jobProperties.LastRunOutcome == (int)filter.LastRunOutcome) + { + isLastRunOutcomeMatched = true; + } + + return isLastRunOutcomeMatched; + } + + /// + /// Check if job status filter matches given jobproperty + /// + /// + /// + /// + private bool CheckIfJobStatusMatchesJob(JobActivityFilter filter, JobProperties jobProperties) + { + bool isStatusMatched = false; + // filter - job run status + if (filter.Status == EnumStatus.All || + jobProperties.CurrentExecutionStatus == (int)filter.Status) + { + isStatusMatched = true; + } + + return isStatusMatched; + } + + /// + /// Check if job scheduled status filter matches job + /// + /// + /// + /// + private bool CheckIfScheduledStatusMatchesJob(JobActivityFilter filter, JobProperties jobProperties) + { + bool isScheduledMatched = false; + // apply filter - if job has schedules or not + switch (filter.Scheduled) + { + // if All was selected, include in match + case EnumThreeState.All: + isScheduledMatched = true; + break; + + // if Yes was selected, include only if job has schedule + case EnumThreeState.Yes: + if (jobProperties.HasSchedule) + { + isScheduledMatched = true; + } + break; + + // if Yes was selected, include only if job does not have schedule + case EnumThreeState.No: + if (!jobProperties.HasSchedule) + { + isScheduledMatched = true; + } + break; + } + + return isScheduledMatched; + } + + /// + /// Check if job enabled status matches job + /// + /// + /// + /// + private bool CheckIfEnabledStatusMatchesJob(JobActivityFilter filter, JobProperties jobProperties) + { + bool isEnabledMatched = false; + // apply filter - if job was enabled or not + switch (filter.Enabled) + { + // if All was selected, include in match + case EnumThreeState.All: + isEnabledMatched = true; + break; + + // if Yes was selected, include only if job has schedule + case EnumThreeState.Yes: + if (jobProperties.Enabled) + { + isEnabledMatched = true; + } + break; + + // if Yes was selected, include only if job does not have schedule + case EnumThreeState.No: + if (!jobProperties.Enabled) + { + isEnabledMatched = true; + } + break; + } + + return isEnabledMatched; + } + + /// + /// Check if a category matches given jobproperty + /// + /// + /// + /// + private bool CheckIfCategoryMatchesJob(JobActivityFilter filter, JobProperties jobProperties) + { + bool isCategoryMatched = false; + // Apply category filter if specified + if (filter.Category.Length > 0) + { + // + // we count it as a match if the job category contains + // a case-insensitive match for the filter string. + // + string jobCategory = jobProperties.Category.ToLower(CultureInfo.CurrentCulture); + if (String.Compare(jobCategory, filter.Category.Trim().ToLower(CultureInfo.CurrentCulture), StringComparison.Ordinal) == 0) + { + isCategoryMatched = true; + } + } + else + { + // No category filter was specified + isCategoryMatched = true; + } + + return isCategoryMatched; + } + + /// + /// Check if name filter specified matches given jobproperty + /// + /// + /// + /// + private bool CheckIfNameMatchesJob(JobActivityFilter filter, JobProperties jobProperties) + { + bool isNameMatched = false; + + // + // job name (can be comma-separated list) + // we count it as a match if the job name contains + // a case-insensitive match for any of the filter strings. + // + if (filter.Name.Length > 0) + { + string jobname = jobProperties.Name.ToLower(CultureInfo.CurrentCulture); + string[] jobNames = filter.Name.ToLower(CultureInfo.CurrentCulture).Split(','); + int length = jobNames.Length; + + for (int j = 0; j < length; ++j) + { + if (jobname.IndexOf(jobNames[j].Trim(), StringComparison.Ordinal) > -1) + { + isNameMatched = true; + break; + } + } + } + else + { + // No name filter was specified + isNameMatched = true; + } + + return isNameMatched; + } + + /// + /// Fetch jobs for a given Urn + /// + /// + /// + public Dictionary FetchJobs(string urn) + { + if(String.IsNullOrEmpty(urn)) + { + throw new ArgumentNullException("urn"); + } + + Request request = new Request(); + request.Urn = urn; + request.Fields = new string[] + { + "Name", + "IsEnabled", + "Category", + "CategoryID", + "CategoryType", + "CurrentRunStatus", + "CurrentRunStep", + "HasSchedule", + "HasStep", + "HasServer", + "LastRunDate", + "NextRunDate", + "LastRunOutcome", + "JobID" + }; + + DataTable dt = enumerator.Process(connection, request); + + int numJobs = dt.Rows.Count; + if (numJobs == 0) + { + return null; + } + + Dictionary foundJobs = new Dictionary(numJobs); + + for (int i = 0; i < numJobs; ++i) + { + JobProperties jobProperties = new JobProperties(dt.Rows[i]); + foundJobs.Add(jobProperties.JobID, jobProperties); + } + + return foundJobs; + } + } + + /// + /// a class for storing various properties of agent jobs, + /// used by the Job Activity Monitor + /// + public class JobProperties + { + private string name; + private int currentExecutionStatus; + private int lastRunOutcome; + private string currentExecutionStep; + private bool enabled; + private bool hasTarget; + private bool hasSchedule; + private bool hasStep; + private bool runnable; + private string category; + private int categoryID; + private int categoryType; + private DateTime lastRun; + private DateTime nextRun; + private Guid jobId; + + private JobProperties() + { + } + + public JobProperties(DataRow row) + { + System.Diagnostics.Debug.Assert(row["Name"] != DBNull.Value, "Name is null!"); + System.Diagnostics.Debug.Assert(row["IsEnabled"] != DBNull.Value, "IsEnabled is null!"); + System.Diagnostics.Debug.Assert(row["Category"] != DBNull.Value, "Category is null!"); + System.Diagnostics.Debug.Assert(row["CategoryID"] != DBNull.Value, "CategoryID is null!"); + System.Diagnostics.Debug.Assert(row["CategoryType"] != DBNull.Value, "CategoryType is null!"); + System.Diagnostics.Debug.Assert(row["CurrentRunStatus"] != DBNull.Value, "CurrentRunStatus is null!"); + System.Diagnostics.Debug.Assert(row["CurrentRunStep"] != DBNull.Value, "CurrentRunStep is null!"); + System.Diagnostics.Debug.Assert(row["HasSchedule"] != DBNull.Value, "HasSchedule is null!"); + System.Diagnostics.Debug.Assert(row["HasStep"] != DBNull.Value, "HasStep is null!"); + System.Diagnostics.Debug.Assert(row["HasServer"] != DBNull.Value, "HasServer is null!"); + System.Diagnostics.Debug.Assert(row["LastRunOutcome"] != DBNull.Value, "LastRunOutcome is null!"); + System.Diagnostics.Debug.Assert(row["JobID"] != DBNull.Value, "JobID is null!"); + + this.name = row["Name"].ToString(); + this.enabled = Convert.ToBoolean(row["IsEnabled"], CultureInfo.InvariantCulture); + this.category = row["Category"].ToString(); + this.categoryID = Convert.ToInt32(row["CategoryID"], CultureInfo.InvariantCulture); + this.categoryType = Convert.ToInt32(row["CategoryType"], CultureInfo.InvariantCulture); + this.currentExecutionStatus = Convert.ToInt32(row["CurrentRunStatus"], CultureInfo.InvariantCulture); + this.currentExecutionStep = row["CurrentRunStep"].ToString(); + this.hasSchedule = Convert.ToBoolean(row["HasSchedule"], CultureInfo.InvariantCulture); + this.hasStep = Convert.ToBoolean(row["HasStep"], CultureInfo.InvariantCulture); + this.hasTarget = Convert.ToBoolean(row["HasServer"], CultureInfo.InvariantCulture); + this.lastRunOutcome = Convert.ToInt32(row["LastRunOutcome"], CultureInfo.InvariantCulture); + this.jobId = Guid.Parse(row["JobID"].ToString()); ; + + // for a job to be runnable, it must: + // 1. have a target server + // 2. have some steps + this.runnable = this.hasTarget && this.hasStep; + + if (row["LastRunDate"] != DBNull.Value) + { + this.lastRun = Convert.ToDateTime(row["LastRunDate"], CultureInfo.InvariantCulture); + } + + if (row["NextRunDate"] != DBNull.Value) + { + this.nextRun = Convert.ToDateTime(row["NextRunDate"], CultureInfo.InvariantCulture); + } + } + + public bool Runnable + { + get{ return runnable;} + } + + public string Name + { + get{ return name;} + } + + public string Category + { + get{ return category;} + } + + public int CategoryID + { + get{ return categoryID;} + } + + public int CategoryType + { + get{ return categoryType;} + } + + public int LastRunOutcome + { + get{ return lastRunOutcome;} + } + + public int CurrentExecutionStatus + { + get{ return currentExecutionStatus;} + } + + public string CurrentExecutionStep + { + get{ return currentExecutionStep;} + } + + public bool Enabled + { + get{ return enabled;} + } + + public bool HasTarget + { + get{ return hasTarget;} + } + + public bool HasStep + { + get{ return hasStep;} + } + + public bool HasSchedule + { + get{ return hasSchedule;} + } + + public DateTime NextRun + { + get{ return nextRun;} + } + + public DateTime LastRun + { + get{ return lastRun;} + } + + public Guid JobID + { + get + { + return this.jobId; + } + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/JobHelper.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/JobHelper.cs new file mode 100644 index 00000000..178134c0 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Agent/JobHelper.cs @@ -0,0 +1,100 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.SqlServer.Management.Common; +using Microsoft.SqlServer.Management.Sdk.Sfc; +using Microsoft.SqlServer.Management.Smo.Agent; +using SMO = Microsoft.SqlServer.Management.Smo; + +namespace Microsoft.SqlTools.ServiceLayer.Agent +{ + internal class JobHelper + { + private JobHelper() + { + } + + private ServerConnection connection = null; + private string jobName = string.Empty; + private SMO.Server server = null; + private Job job = null; + + // + // ServerConnection object should be passed from caller, + // who gets it from CDataContainer.ServerConnection + // + public JobHelper(ServerConnection connection) + { + this.connection = connection; + server = new SMO.Server(connection); + } + + public string JobName + { + get + { + return jobName; + } + set + { + if (server != null) + { + Job j = server.JobServer.Jobs[value]; + if (j != null) + { + job = j; + jobName = value; + } + else + { + throw new InvalidOperationException("Job not found"); + } + } + } + } + + public void Stop() + { + if (job != null) + { + job.Stop(); + } + } + + public void Start() + { + if (job != null) + { + job.Start(); + } + } + + public void Delete() + { + if (job != null) + { + job.Drop(); + + // + // you can't do anything with + // a job after you drop it! + // + job = null; + } + } + + public void Enable(bool enable) + { + if (job != null && job.IsEnabled != enable) + { + job.IsEnabled = enable; + job.Alter(); + } + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/JobHistoryItem.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/JobHistoryItem.cs new file mode 100644 index 00000000..a4368bff --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Agent/JobHistoryItem.cs @@ -0,0 +1,752 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.SqlClient; +using System.Globalization; +using System.Text; +using System.Xml; +using Microsoft.SqlTools.ServiceLayer.Admin; +using Microsoft.SqlServer.Management.Common; +using Microsoft.SqlServer.Management.Sdk.Sfc; +using Microsoft.SqlServer.Management.Smo.Agent; +using SMO = Microsoft.SqlServer.Management.Smo; + + +namespace Microsoft.SqlTools.ServiceLayer.Agent +{ + + /// + /// severity associated with a log entry (ILogEntry) + // these should be ordered least severe to most severe where possible. + /// + public enum SeverityClass + { + Unknown = -1, + Success, + Information, + SuccessAudit, + InProgress, + Retry, + Warning, + FailureAudit, + Cancelled, + Error + } + + /// + /// command that can be executed by a log viewer (ILogViewer) + /// + internal enum LogViewerCommand + { + Load = 0, + Refresh, + Export, + Columns, + Filter, + Search, + Delete, + Help, + Close, + Cancel + } + + /// + /// command options + /// + internal enum LogViewerCommandOption + { + None = 0, + Hide, + Show + } + + public interface ILogEntry + { + string OriginalSourceTypeName {get;} + string OriginalSourceName {get;} + SeverityClass Severity {get;} + DateTime PointInTime {get;} + string this[string fieldName] {get;} + bool CanLoadSubEntries {get;} + List SubEntries {get;} + } + + + internal class LogSourceJobHistory : ILogSource, IDisposable //, ITypedColumns, ILogCommandTarget + { + internal const string JobDialogParameterTemplate = ""; + internal const string JobStepDialogParameterTemplate = "true"; + + internal enum DialogType + { + JobDialog = 0, + JobStepDialog = 1 + } + + #region Variables + private string m_jobName = null; + //assigning invalid jobCategoryId + public int m_jobCategoryId = -1; + private Guid m_jobId = Guid.Empty; + private SqlConnectionInfo m_sqlConnectionInfo = null; + + private List m_logEntries = null; + private string m_logName = null; + private bool m_logInitialized = false; + private string[] m_fieldNames = null; + private ILogEntry m_currentEntry = null; + private int m_index = 0; + private bool m_isClosed = false; + private TypedColumnCollection columnTypes; + private IServiceProvider serviceProvider = null; + + // ILogCommandHandler m_customCommandHandler = null; + + private static string historyTableDeclaration = "declare @tmp_sp_help_jobhistory table"; + private static string historyTableDeclaration80 = "create table #tmp_sp_help_jobhistory"; + private static string historyTableName = "@tmp_sp_help_jobhistory"; + private static string historyTableName80 = "#tmp_sp_help_jobhistory"; + private static string jobHistoryQuery = +@"{0} +( + instance_id int null, + job_id uniqueidentifier null, + job_name sysname null, + step_id int null, + step_name sysname null, + sql_message_id int null, + sql_severity int null, + message nvarchar(4000) null, + run_status int null, + run_date int null, + run_time int null, + run_duration int null, + operator_emailed sysname null, + operator_netsent sysname null, + operator_paged sysname null, + retries_attempted int null, + server sysname null +) + +insert into {1} +exec msdb.dbo.sp_help_jobhistory + @job_id = '{2}', + @mode='FULL' + +SELECT + tshj.instance_id AS [InstanceID], + tshj.sql_message_id AS [SqlMessageID], + tshj.message AS [Message], + tshj.step_id AS [StepID], + tshj.step_name AS [StepName], + tshj.sql_severity AS [SqlSeverity], + tshj.job_id AS [JobID], + tshj.job_name AS [JobName], + tshj.run_status AS [RunStatus], + CASE tshj.run_date WHEN 0 THEN NULL ELSE + convert(datetime, + stuff(stuff(cast(tshj.run_date as nchar(8)), 7, 0, '-'), 5, 0, '-') + N' ' + + stuff(stuff(substring(cast(1000000 + tshj.run_time as nchar(7)), 2, 6), 5, 0, ':'), 3, 0, ':'), + 120) END AS [RunDate], + tshj.run_duration AS [RunDuration], + tshj.operator_emailed AS [OperatorEmailed], + tshj.operator_netsent AS [OperatorNetsent], + tshj.operator_paged AS [OperatorPaged], + tshj.retries_attempted AS [RetriesAttempted], + tshj.server AS [Server], + getdate() as [CurrentDate] +FROM {1} as tshj +ORDER BY [InstanceID] ASC"; + + #endregion + + #region Public Property - used by Delete handler + public string JobName + { + get + { + return m_jobName; + } + } + #endregion + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (disposing) + { + // dispose child ILogSources... + ILogSource me = this as ILogSource; + if (me.SubSources != null) + { + for (int i = 0; i < me.SubSources.Length; ++i) + { + IDisposable d = me.SubSources[i] as IDisposable; + if (d != null) + { + d.Dispose(); + } + } + } + + if (m_currentEntry != null) + { + m_currentEntry = null; + } + } + } + + + + #region Constructor + + + public LogSourceJobHistory(string jobName, SqlConnectionInfo sqlCi, object customCommandHandler, int jobCategoryId, Guid JobId, IServiceProvider serviceProvider) + { + m_logName = jobName; + m_jobCategoryId = jobCategoryId; + m_jobId = JobId; + m_logEntries = new List(); + + m_jobName = jobName; + m_sqlConnectionInfo = sqlCi; + m_fieldNames = new string[] + { + "LogViewerSR.Field_StepID", + "LogViewerSR.Field_Server", + "LogViewerSR.Field_JobName", + "LogViewerSR.Field_StepName", + "LogViewerSR.Field_Notifications", + "LogViewerSR.Field_Message", + "LogViewerSR.Field_Duration", + "LogViewerSR.Field_SqlSeverity", + "LogViewerSR.Field_SqlMessageID", + "LogViewerSR.Field_OperatorEmailed", + "LogViewerSR.Field_OperatorNetsent", + "LogViewerSR.Field_OperatorPaged", + "LogViewerSR.Field_RetriesAttempted" + }; + + columnTypes = new TypedColumnCollection(); + for (int i = 0; i < m_fieldNames.Length; i++) + { + columnTypes.AddColumnType(m_fieldNames[i], SourceColumnTypes[i]); + } + + //m_customCommandHandler = customCommandHandler; + + this.serviceProvider = serviceProvider; + } + #endregion + + private static int[] SourceColumnTypes + { + get + { + return new int[] + { + GridColumnType.Hyperlink, + GridColumnType.Text, + GridColumnType.Hyperlink, + GridColumnType.Text, + GridColumnType.Text, + GridColumnType.Text, + GridColumnType.Text, + GridColumnType.Text, + GridColumnType.Text, + GridColumnType.Text, + GridColumnType.Text, + GridColumnType.Text, + GridColumnType.Text + }; + } + } + + public TypedColumnCollection ColumnTypes + { + get + { + return columnTypes; + } + } + #region ILogSource interface implementation + + bool ILogSource.OrderedByDateDescending + { + get { return false; } + } + + ILogEntry ILogSource.CurrentEntry + { + get + { + return m_currentEntry; + } + } + + bool ILogSource.ReadEntry() + { + if (!m_isClosed && m_index >= 0) + { + m_currentEntry = m_logEntries[m_index--]; + + return true; + } + else + { + return false; + } + } + + void ILogSource.CloseReader() + { + m_index = m_logEntries.Count -1; + m_isClosed = true; + m_currentEntry = null; + return; + } + + string ILogSource.Name + { + get + { + return m_logName; + } + } + + void ILogSource.Initialize() + { + if (m_logInitialized == true) + { + return; + } + + // do the actual initialization, retrieveing the ILogEntry-s + InitializeInternal(); + m_logInitialized = true; + } + + ILogSource[] ILogSource.SubSources + { + get { return null; } + } + + string[] ILogSource.FieldNames + { + get + { + return m_fieldNames; + } + } + + ILogSource ILogSource.GetRefreshedClone() + { + return new LogSourceJobHistory(m_jobName, m_sqlConnectionInfo, null, m_jobCategoryId, m_jobId, this.serviceProvider); + + } + #endregion + + #region Implementation + /// + /// does the actual initialization by retrieving Server/ErrorLog/Text via enumerator + /// + private void InitializeInternal() + { + m_logEntries.Clear(); + + IDbConnection connection = null; + try + { + connection = m_sqlConnectionInfo.CreateConnectionObject(); + connection.Open(); + + IDbCommand command = connection.CreateCommand(); + + string jobId = this.m_jobId.ToString(); + + string query = + (this.m_sqlConnectionInfo.ServerVersion == null + || this.m_sqlConnectionInfo.ServerVersion.Major >= 9) ? + + String.Format(jobHistoryQuery, + historyTableDeclaration, + historyTableName, + jobId) : + + String.Format(jobHistoryQuery, + historyTableDeclaration80, + historyTableName80, + jobId); + + + command.CommandType = CommandType.Text; + command.CommandText = query; + + DataTable dtJobHistory = new DataTable(); + SqlDataAdapter adapter = new SqlDataAdapter((SqlCommand)command); + adapter.Fill(dtJobHistory); + + int n = dtJobHistory.Rows.Count; + + // populate m_logEntries with ILogEntry-s that have (ref to us, rowno) + for (int rowno = 0; rowno < n; rowno++) + { + // we will create here only the job outcomes (0) - entries, and skip non 0 + // job outcome (step 0) it will extract the sub-entries (steps 1...n) itself + ILogEntry jobOutcome = new LogEntryJobHistory(m_jobName, dtJobHistory, rowno); + + int skippedSubentries = (jobOutcome.SubEntries == null) ? 0 : jobOutcome.SubEntries.Count; + rowno += skippedSubentries; // skip subentries + + m_logEntries.Add(jobOutcome); + } + + m_index = m_logEntries.Count - 1; + } + finally + { + if (connection != null) + { + connection.Close(); + } + } + } + #endregion + + #region internal class - LogEntryJobHistory + /// + /// LogEntryJobHistory - represents a SqlServer log entry + /// + internal class LogEntryJobHistory : ILogEntry + { + #region Constants + internal const string cUrnRunDate = "RunDate"; + internal const string cUrnRunStatus = "RunStatus"; + internal const string cUrnRunDuration = "RunDuration"; + internal const string cUrnJobName = "JobName"; + internal const string cUrnStepID = "StepID"; + internal const string cUrnStepName = "StepName"; + internal const string cUrnMessage = "Message"; + internal const string cUrnSqlSeverity = "SqlSeverity"; + internal const string cUrnSqlMessageID = "SqlMessageID"; + internal const string cUrnOperatorEmailed = "OperatorEmailed"; + internal const string cUrnOperatorNetsent = "OperatorNetsent"; + internal const string cUrnOperatorPaged = "OperatorPaged"; + internal const string cUrnRetriesAttempted = "RetriesAttempted"; + internal const string cUrnServerName = "Server"; + internal const string cUrnServerTime = "CurrentDate"; + + internal const string cUrnInstanceID = "InstanceID"; // internally used by Agent to mark order in which steps were executed + + #endregion + + #region Variables + string m_originalSourceName = null; + DateTime m_pointInTime = DateTime.MinValue; + SeverityClass m_severity = SeverityClass.Unknown; + + string m_fieldJobName = null; + string m_fieldStepID = null; + string m_fieldStepName = null; + string m_fieldMessage = null; + string m_fieldDuration = null; + string m_fieldSqlSeverity = null; + string m_fieldSqlMessageID = null; + string m_fieldOperatorEmailed = null; + string m_fieldOperatorNetsent = null; + string m_fieldOperatorPaged = null; + string m_fieldRetriesAttempted = null; + private string m_serverName = null; + List m_subEntries = null; + #endregion + + #region Constructor + /// + /// constructor used by log source to create 'job outcome' entries + /// + /// + /// data table containing all history info for a job + /// index for row that describes 'job outcome', rowno+1..n will describe 'job steps' + public LogEntryJobHistory(string sourceName, DataTable dt, int rowno) + { + InitializeJobHistoryStepSubEntries(sourceName, dt, rowno); // create subentries, until we hit job outcome or end of history + + // initialize job outcome + if ((m_subEntries != null) && (m_subEntries.Count > 0)) + { + // are we at the end of history data set? + if ((rowno + m_subEntries.Count) < dt.Rows.Count) + { + // row following list of subentries coresponds to an outcome job that already finished + InitializeJobHistoryFromDataRow(sourceName, dt.Rows[rowno + m_subEntries.Count]); + } + else + { + // there is no row with stepID=0 that coresponds to a job job outcome for a job that is running + // since agent will write the outcome only after it finishes the job therefore we will build ourselves + // an entry describing the running job, to which we will host the subentries for job steps already executed + InitializeJobHistoryForRunningJob(sourceName, dt.Rows[rowno]); + } + } + else + { + InitializeJobHistoryFromDataRow(sourceName, dt.Rows[rowno]); + } + } + + /// + /// constructor used by a parent log entry to create child 'job step' sub-entries + /// + /// + /// row describing subentry + public LogEntryJobHistory(string sourceName, DataRow dr) + { + InitializeJobHistoryFromDataRow(sourceName, dr); // initialize intself + } + #endregion + + #region Implementation + /// + /// builds an entry based on a row returned by enurerator - that can be either a job step or a job outcome + /// + /// + private void InitializeJobHistoryFromDataRow(string sourceName, DataRow dr) + { + try + { + m_originalSourceName = sourceName; + + m_pointInTime = Convert.ToDateTime(dr[cUrnRunDate], System.Globalization.CultureInfo.InvariantCulture); + m_serverName = Convert.ToString(dr[cUrnServerName], System.Globalization.CultureInfo.InvariantCulture); + m_fieldJobName = Convert.ToString(dr[cUrnJobName], System.Globalization.CultureInfo.InvariantCulture); + switch ((Microsoft.SqlServer.Management.Smo.Agent.CompletionResult)Convert.ToInt32(dr[cUrnRunStatus], System.Globalization.CultureInfo.InvariantCulture)) + { + case CompletionResult.Cancelled: + m_severity = SeverityClass.Cancelled; + break; + case CompletionResult.Failed: + m_severity = SeverityClass.Error; + break; + case CompletionResult.InProgress: + m_severity = SeverityClass.InProgress; + break; + case CompletionResult.Retry: + m_severity = SeverityClass.Retry; + break; + case CompletionResult.Succeeded: + m_severity = SeverityClass.Success; + break; + case CompletionResult.Unknown: + m_severity = SeverityClass.Unknown; + break; + default: + m_severity = SeverityClass.Unknown; + break; + } + + // + // check our subentries, see if any of them have a worse status than we do. + // if so, then we should set ourselves to SeverityClass.Warning + // + if (this.m_subEntries != null) + { + for (int i = 0; i < this.m_subEntries.Count; i++) + { + if (this.m_subEntries[i].Severity > this.m_severity && + (this.m_subEntries[i].Severity == SeverityClass.Retry || + this.m_subEntries[i].Severity == SeverityClass.Warning || + this.m_subEntries[i].Severity == SeverityClass.FailureAudit || + this.m_subEntries[i].Severity == SeverityClass.Error)) + { + this.m_severity = SeverityClass.Warning; + break; + } + } + } + + // if stepId is zero then dont show stepID and step name in log viewer + // Valid step Ids starts from index 1 + int currentStepId = (int)dr[cUrnStepID]; + if (currentStepId == 0) + { + m_fieldStepID = String.Empty; + m_fieldStepName = String.Empty; + + } + else + { + m_fieldStepID = Convert.ToString(currentStepId, System.Globalization.CultureInfo.CurrentCulture); + m_fieldStepName = Convert.ToString(dr[cUrnStepName], System.Globalization.CultureInfo.CurrentCulture); + } + + m_fieldMessage = Convert.ToString(dr[cUrnMessage], System.Globalization.CultureInfo.CurrentCulture); + m_fieldSqlSeverity = Convert.ToString(dr[cUrnSqlSeverity], System.Globalization.CultureInfo.CurrentCulture); + m_fieldSqlMessageID = Convert.ToString(dr[cUrnSqlMessageID], System.Globalization.CultureInfo.CurrentCulture); + m_fieldOperatorEmailed = Convert.ToString(dr[cUrnOperatorEmailed], System.Globalization.CultureInfo.CurrentCulture); + m_fieldOperatorNetsent = Convert.ToString(dr[cUrnOperatorNetsent], System.Globalization.CultureInfo.CurrentCulture); + m_fieldOperatorPaged = Convert.ToString(dr[cUrnOperatorPaged], System.Globalization.CultureInfo.CurrentCulture); + m_fieldRetriesAttempted = Convert.ToString(dr[cUrnRetriesAttempted], System.Globalization.CultureInfo.CurrentCulture); + + Int64 hhmmss = Convert.ToInt64(dr[cUrnRunDuration], System.Globalization.CultureInfo.InvariantCulture); // HHMMSS + int hh = Convert.ToInt32(hhmmss / 10000, System.Globalization.CultureInfo.InvariantCulture); + int mm = Convert.ToInt32((hhmmss / 100) % 100, System.Globalization.CultureInfo.InvariantCulture); + int ss = Convert.ToInt32(hhmmss % 100, System.Globalization.CultureInfo.InvariantCulture); + m_fieldDuration = Convert.ToString(new TimeSpan(hh, mm, ss), System.Globalization.CultureInfo.CurrentCulture); + } + catch (InvalidCastException) + { + // keep null data if we hit some invalid info + } + } + + /// + /// builds sub-entries (steps 1...n), until we find a 'job outcome' (step0) or end of history (meaning job is in progress) + /// + /// + /// points to 1st subentry => points to 1st 'job step' + private void InitializeJobHistoryStepSubEntries(string sourceName, DataTable dt, int rowno) + { + if (m_subEntries == null) + { + m_subEntries = new List(); + } + else + { + m_subEntries.Clear(); + } + + int i = rowno; + while (i < dt.Rows.Count) + { + DataRow dr = dt.Rows[i]; + + object o = dr[cUrnStepID]; + + try + { + int stepID = Convert.ToInt32(o, System.Globalization.CultureInfo.InvariantCulture); + + if (stepID == 0) + { + // we found the 'job outcome' for our set of steps + break; + } + + // + // we want to have the subentries ordered newest to oldest, + // which is the same time order as the parent nodes themselves. + // that's why we add each one to the head of the list + // + m_subEntries.Insert(0, new LogEntryJobHistory(sourceName, dr)); + } + catch (InvalidCastException) + { + } + + ++i; + } + } + + /// + /// builds an entry for a running job - in this case there is no row available since agent logs outcomes only after job finishes + /// + /// + /// points to last entry - which should corespond to first step - so we can compute job name and duration + private void InitializeJobHistoryForRunningJob(string sourceName, DataRow dr) + { + try + { + m_originalSourceName = sourceName; + + m_pointInTime = Convert.ToDateTime(dr[cUrnRunDate], System.Globalization.CultureInfo.InvariantCulture); + m_fieldJobName = Convert.ToString(dr[cUrnJobName], System.Globalization.CultureInfo.InvariantCulture); + + m_severity = SeverityClass.InProgress; + + m_fieldStepID = null; + m_fieldStepName = null; + m_fieldMessage = "DropObjectsSR.InProgressStatus"; // $FUTURE - assign its own string when string resources got un-freezed + m_fieldSqlSeverity = null; + m_fieldSqlMessageID = null; + m_fieldOperatorEmailed = null; + m_fieldOperatorNetsent = null; + m_fieldOperatorPaged = null; + m_fieldRetriesAttempted = null; + m_serverName = null; + + m_fieldDuration = Convert.ToString(Convert.ToDateTime(dr[cUrnServerTime]) - Convert.ToDateTime(dr[cUrnRunDate], System.Globalization.CultureInfo.InvariantCulture), System.Globalization.CultureInfo.InvariantCulture); + } + catch (InvalidCastException) + { + // keep null data if we hit some invalid info + } + } + #endregion + + #region ILogEntry interface implementation + string ILogEntry.OriginalSourceTypeName + { + get + { + return "LogViewerSR.LogSourceTypeJobHistory"; + } + } + + string ILogEntry.OriginalSourceName + { + get + { + return m_originalSourceName; + } + } + + SeverityClass ILogEntry.Severity + { + get + { + return m_severity; + } + } + + DateTime ILogEntry.PointInTime + { + get + { + return m_pointInTime; + } + } + + string ILogEntry.this[string fieldName] + { + get + { + { + return null; + } + } + } + + bool ILogEntry.CanLoadSubEntries + { + get { return ((m_subEntries != null) && (m_subEntries.Count > 0)); } + } + + List ILogEntry.SubEntries + { + get { return m_subEntries; } + } + #endregion + } + #endregion + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/JobUtilities.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/JobUtilities.cs new file mode 100644 index 00000000..2e2fb7bb --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Agent/JobUtilities.cs @@ -0,0 +1,106 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Data; +using System.Collections.Generic; +using System.Text; +using Microsoft.SqlServer.Management.Common; +using Microsoft.SqlServer.Management.Sdk.Sfc; +using Microsoft.SqlServer.Management.Smo.Agent; +using Microsoft.SqlTools.ServiceLayer.Agent.Contracts; +using SMO = Microsoft.SqlServer.Management.Smo; + +namespace Microsoft.SqlTools.ServiceLayer.Agent +{ + public class JobUtilities + { + public const string UrnJobName = "JobName"; + public const string UrnJobId = "JobId"; + public const string UrnRunStatus = "RunStatus"; + public const string UrnInstanceID = "InstanceId"; + public const string UrnSqlMessageID = "SqlMessageId"; + public const string UrnMessage = "Message"; + public const string UrnStepID = "StepId"; + public const string UrnStepName = "StepName"; + public const string UrnSqlSeverity = "SqlSeverity"; + public const string UrnRunDate = "RunDate"; + public const string UrnRunDuration = "RunDuration"; + public const string UrnOperatorEmailed = "OperatorEmailed"; + public const string UrnOperatorNetsent = "OperatorNetsent"; + public const string UrnOperatorPaged = "OperatorPaged"; + public const string UrnRetriesAttempted = "RetriesAttempted"; + public const string UrnServer = "Server"; + + public static AgentJobInfo ConvertToAgentJobInfo(JobProperties job) + { + return new AgentJobInfo + { + Name = job.Name, + CurrentExecutionStatus = job.CurrentExecutionStatus, + LastRunOutcome = job.LastRunOutcome, + CurrentExecutionStep = job.CurrentExecutionStep, + Enabled = job.Enabled, + HasTarget = job.HasTarget, + HasSchedule = job.HasSchedule, + HasStep = job.HasStep, + Runnable = job.Runnable, + Category = job.Category, + CategoryId = job.CategoryID, + CategoryType = job.CategoryType, + LastRun = job.LastRun != null ? job.LastRun.ToString() : string.Empty, + NextRun = job.NextRun != null ? job.NextRun.ToString() : string.Empty, + JobId = job.JobID != null ? job.JobID.ToString() : null + }; + } + + public static AgentJobHistoryInfo ConvertToAgentJobHistoryInfo(DataRow jobRow, SqlConnectionInfo sqlConnInfo) + { + // get all the values for a job history + int instanceId = Convert.ToInt32(jobRow[UrnInstanceID], System.Globalization.CultureInfo.InvariantCulture); + int sqlMessageId = Convert.ToInt32(jobRow[UrnSqlMessageID], System.Globalization.CultureInfo.InvariantCulture); + string message = Convert.ToString(jobRow[UrnMessage], System.Globalization.CultureInfo.InvariantCulture); + int stepId = Convert.ToInt32(jobRow[UrnStepID], System.Globalization.CultureInfo.InvariantCulture); + string stepName = Convert.ToString(jobRow[UrnStepName], System.Globalization.CultureInfo.InvariantCulture); + int sqlSeverity = Convert.ToInt32(jobRow[UrnSqlSeverity], System.Globalization.CultureInfo.InvariantCulture); + Guid jobId = (Guid) jobRow[UrnJobId]; + string jobName = Convert.ToString(jobRow[UrnJobName], System.Globalization.CultureInfo.InvariantCulture); + int runStatus = Convert.ToInt32(jobRow[UrnRunStatus], System.Globalization.CultureInfo.InvariantCulture); + DateTime runDate = Convert.ToDateTime(jobRow[UrnRunDate], System.Globalization.CultureInfo.InvariantCulture); + int runDuration = Convert.ToInt32(jobRow[UrnRunDuration], System.Globalization.CultureInfo.InvariantCulture); + string operatorEmailed = Convert.ToString(jobRow[UrnOperatorEmailed], System.Globalization.CultureInfo.InvariantCulture); + string operatorNetsent = Convert.ToString(jobRow[UrnOperatorNetsent], System.Globalization.CultureInfo.InvariantCulture); + string operatorPaged = Convert.ToString(jobRow[UrnOperatorPaged], System.Globalization.CultureInfo.InvariantCulture); + int retriesAttempted = Convert.ToInt32(jobRow[UrnRetriesAttempted], System.Globalization.CultureInfo.InvariantCulture); + string server = Convert.ToString(jobRow[UrnServer], System.Globalization.CultureInfo.InvariantCulture); + + // initialize logger + var t = new LogSourceJobHistory(jobName, sqlConnInfo, null, runStatus, jobId, null); + var tlog = t as ILogSource; + tlog.Initialize(); + + // return new job history object as a result + var jobHistoryInfo = new AgentJobHistoryInfo(); + jobHistoryInfo.InstanceId = instanceId; + jobHistoryInfo.SqlMessageId = sqlMessageId; + jobHistoryInfo.Message = message; + jobHistoryInfo.StepId = stepId; + jobHistoryInfo.StepName = stepName; + jobHistoryInfo.SqlSeverity = sqlSeverity; + jobHistoryInfo.JobId = jobId; + jobHistoryInfo.JobName = jobName; + jobHistoryInfo.RunStatus = runStatus; + jobHistoryInfo.RunDate = runDate; + jobHistoryInfo.RunDuration = runDuration; + jobHistoryInfo.OperatorEmailed = operatorEmailed; + jobHistoryInfo.OperatorNetsent = operatorNetsent; + jobHistoryInfo.OperatorPaged = operatorPaged; + jobHistoryInfo.RetriesAttempted = retriesAttempted; + jobHistoryInfo.Server = server; + + return jobHistoryInfo; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/LogAggregator.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/LogAggregator.cs new file mode 100644 index 00000000..f90e3f8e --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Agent/LogAggregator.cs @@ -0,0 +1,704 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlServer.Management.Sdk.Sfc; +using System; +using System.Threading; +using System.Collections; +using System.Collections.Generic; +using Microsoft.SqlServer.Management.Diagnostics; +using System.Runtime.CompilerServices; + +namespace Microsoft.SqlTools.ServiceLayer.Agent +{ + +#region LogSourceAggregation - ILogSource info built from multiple other sources + internal class LogSourceAggregation : ILogSource, ITypedColumns, IDisposable + { +#region Constants + private const int cMaximumNotificationChunkSize = 128; // 16384 high no: faster aggregation, low no: responsive ui +#endregion + +#region Variables + private string m_logName = null; + private bool m_logInitialized = false; + private string[] m_fieldNames = null; + private TypedColumnCollection m_columnTypes = null; + + List m_originalSources = null; + List m_sources = null; + ILogConstraints m_filter = null; + private LogAggregator m_owner = null; + private ILogEntry m_currentEntry = null; + private List m_currentEntrySources = null; + private List m_exceptionList = null; + +#endregion + +#region Reverse order Property + private bool ReverseOrder + { + get + { + return true; + } + } +#endregion + +#region Constructor + /// + /// + /// + /// + /// + /// + /// if null no filter, else use it to filter every ILogEntry + public LogSourceAggregation (LogAggregator owner, string name, ILogSource[] sources, ILogConstraints filterTemplate) + { + m_owner = owner; + + m_logName = name; + m_originalSources = new List(sources); + + m_fieldNames = AggregateFieldNames(sources); + + AggregateColumnTypes(sources); + + // if (filterTemplate != null) + // { + // m_filter = new LogConstraints(this, filterTemplate as LogConstraints); + // } + // else + { + m_filter = null; + } + } + + public void Dispose() + { + for (int i = 0; i < m_originalSources.Count; ++i) + { + if (m_originalSources[i] is IDisposable) + { + (m_originalSources[i] as IDisposable).Dispose(); + } + } + + m_currentEntry = null; + m_sources = null; + m_exceptionList = null; + } + +#endregion + +#region ILogSource interface implementation + + bool ILogSource.OrderedByDateDescending + { + get {return this.ReverseOrder;} + } + + ILogEntry ILogSource.CurrentEntry + { + get + { + return m_currentEntry; + } + } + + bool ILogSource.ReadEntry() + { + //the m_currentEntrySources list contains the list of currentEntries for each logSource + //when the readentry is called for the first time the list is null so we need to initialize it + if (m_currentEntrySources == null) + { + m_sources = new List(m_originalSources); + m_currentEntrySources = new List(m_sources.Count); + for (int i = 0; i < m_sources.Count; i++) + { + //the null value acts as a guard that indicates whether we have read all the entries from the current source + //or if an error happened. That is why I initialize all the sources with null + m_currentEntrySources.Add(null); + try + { + + if (m_sources[i].CurrentEntry != null || (m_sources[i].ReadEntry())) + { + m_currentEntrySources[i] = m_sources[i].CurrentEntry; + if (m_filter != null) + { + while (!m_filter.MatchLogEntry(m_sources[i].CurrentEntry)) + { + //check if cancel + if (IsCanceled()) + { + return false; + } + + if (m_sources[i].ReadEntry()) + { + m_currentEntrySources[i] = m_sources[i].CurrentEntry; + + if (m_filter != null && !m_filter.IsEntryWithinFilterTimeWindow(m_currentEntrySources[i])) + { + m_currentEntrySources[i] = null; + break; + } + } + else + { + m_currentEntrySources[i] = null; + break; + } + } + } + } + } + catch (Exception e) //whenever a source issued an exception, the exception is stored in the exception list and the source is removed from the list + { + AddExceptionToExceptionList(e, m_sources[i].Name); + m_currentEntrySources[i] = null; + } + + } + } + + //we check the currentEntrySources again to see if there are any entries read from the source + //if not it means that either the source has no entries (that satisfy the filter if a filter is defined) + //or an error happened so we need to close the reader and remove the source. + for (int i = 0; i < m_currentEntrySources.Count; i++) + { + if (m_currentEntrySources[i] == null) + { + m_sources[i].CloseReader(); + m_sources.RemoveAt(i); + m_currentEntrySources.RemoveAt(i); + i--; //we need this to make the indexer point at the previous log source + } + } + + int sourceindex = -1; + + if (m_sources.Count == 1 && m_currentEntrySources[0] != null) + { + sourceindex = 0; + } + else + { + DateTime maxtime = DateTime.MinValue; + for (int i = 0; i < m_sources.Count; i++) + { + if (maxtime.CompareTo(m_currentEntrySources[i].PointInTime) <= 0) + { + maxtime = m_currentEntrySources[i].PointInTime; + sourceindex = i; + } + } + } + + if (sourceindex > -1) + { + m_currentEntry = m_sources[sourceindex].CurrentEntry; + try + { + do + { + //check if cancel + if (IsCanceled()) + { + return false; + } + + if (m_sources[sourceindex].ReadEntry()) + { + m_currentEntrySources[sourceindex] = m_sources[sourceindex].CurrentEntry; + + if (m_filter != null && !m_filter.IsEntryWithinFilterTimeWindow(m_currentEntrySources[sourceindex])) + { + m_currentEntrySources[sourceindex] = null; + break; + } + } + else + { + m_currentEntrySources[sourceindex] = null; + break; + } + + } + while (m_filter != null && !m_filter.MatchLogEntry(m_sources[sourceindex].CurrentEntry)); + } + catch (Exception e) //whenever a source issued an exception, the exception is stored in the exception list and the source is removed from the list + { + AddExceptionToExceptionList(e, m_sources[sourceindex].Name); + m_currentEntrySources[sourceindex] = null; + } + + } + else + { + return false; + } + + return true; + } + + void ILogSource.CloseReader() + { + foreach (ILogSource source in m_originalSources) + { + source.CloseReader(); + } + + m_currentEntrySources = null; + m_currentEntry = null; + m_exceptionList = null; + + } + + string ILogSource.Name + { + get + { + return m_logName; + } + } + + void ILogSource.Initialize() + { + if (m_logInitialized == true) + { + return; + } + + // initialize original sources + int n = m_originalSources.Count; + int i = 0; + for (i = 0; i < m_originalSources.Count; i++) + { + + ILogSource s = m_originalSources[i]; + + try + { + // format notification message + m_owner.Raise_AggregationProgress("Raise_AggregationProgress", //LogViewerSR.AggregationProgress_Initialize(i + 1, n, (s.Name != null) ? s.Name.Trim() : String.Empty), + 0, + null); + + // initialize (load) inner source + s.Initialize(); + } + catch (Exception e) //whenever a source issued an exception, the exception is stored in the exception list and the source is removed from the list + { + AddExceptionToExceptionList(e, s.Name); + m_originalSources.RemoveAt(i); + s.CloseReader(); + i--; + } + + // check for cancel + if (IsCanceled()) + { + return; + } + } + + // report all inner source loaded + m_owner.Raise_AggregationProgress("LogViewerSR.AggregationProgress_InitializationDone", + LogAggregator.cProgressLoaded, + null); + + + m_logInitialized = true; + } + + string[] ILogSource.FieldNames + { + get + { + return m_fieldNames; + } + } + + TypedColumnCollection ITypedColumns.ColumnTypes + { + get + { + return m_columnTypes; + } + } + + ILogSource[] ILogSource.SubSources + { + get { return null;} + } + + ILogSource ILogSource.GetRefreshedClone() + { + return this; + } + +#endregion + +#region Implementation + /// + /// computes the available fields for the aggregated log source + /// + /// + internal static string[] AggregateFieldNames(ILogSource[] sources) + { + List ar = new List(); + + foreach(ILogSource s in sources) + { + if ((s != null) && (s.FieldNames != null)) + { + foreach(string fieldName in s.FieldNames) + { + if (ar.Contains(fieldName)) + { + continue; // do not add it again + } + ar.Add(fieldName); + } + } + } + + return ar.ToArray(); + } + + /// + /// computes the available column types for the aggregated log source + /// + /// + private void AggregateColumnTypes(ILogSource[] sources) + { + m_columnTypes = new TypedColumnCollection(); + + foreach (ILogSource s in sources) + { + if (s is ITypedColumns) + { + ITypedColumns cs = (ITypedColumns)s; + if ((cs != null) && (cs.ColumnTypes != null)) + { + foreach (string fieldName in s.FieldNames) + { + m_columnTypes.AddColumnType(fieldName, cs.ColumnTypes.GetColumnType(fieldName)); + } + } + } + } + + } + + /// + /// checks to see if somebody decided to cancel or stop the operation + /// + private bool IsCanceled() + { + return m_owner.CancelInternal || m_owner.StopInternal; + } + + + public IList ExceptionList + { + get + { + return m_exceptionList; + } + } + + public void ClearExceptionList() + { + m_exceptionList = null; + } + + private void AddExceptionToExceptionList(Exception e, string sourceName) + { + e.Source = sourceName; + + if (m_exceptionList == null) + { + m_exceptionList = new List(); + } + m_exceptionList.Add(e); + } + +#endregion + +#region [Conditional("DEBUG")] validate correctness of a log source + /// + /// validate if entries are in correct order + /// + /// costly operation so we compile this only if "DEBUG" is defined + /// iterates through all the entries and if their datetime is different the + /// DateTime.MinValue or DateTime.MaxValue compares it with adjacent entries + /// + /// we do not compare subentries as aggregation is performed + /// only at entries level (subentries are always linked to thier parent entry) + /// + /// the order should be ascending (newer logs are after older logs) if reverseOrder = false + /// the order should be descending (newer logs are before older logs) if reverseOrder = true + /// + /// + /// + [System.Diagnostics.Conditional("DEBUG")] + private static void ConditionalDEBUG_ValidateLogEntriesOrder(List entries, + bool reverseOrder) + { + System.Diagnostics.Debug.WriteLine("LogSourceAggregation.ConditionalDEBUG_ValidateLogEntriesOrder ------- reverseOrder=" + reverseOrder.ToString()); + + if ((entries == null) || (entries.Count < 2)) + { + return; + } + + for (int i = 0; i < (entries.Count - 1); ++i) + { + int j = i + 1; + + DateTime dti = entries[i].PointInTime; + DateTime dtj = entries[j].PointInTime; + + if ( + (dti != DateTime.MinValue) && (dti != DateTime.MaxValue) && + (dtj != DateTime.MinValue) && (dtj != DateTime.MaxValue) + ) + { + // if logs are comming from same source then we dont Assert since it is not + // the aggregator algoritm to blame but the log source provider who broke + // the assumption that log sources are coming already pre-sorted + if ((entries[i].OriginalSourceTypeName == entries[j].OriginalSourceTypeName) && + (entries[i].OriginalSourceName == entries[j].OriginalSourceName)) + { + continue; + } + } + } + } +#endregion + + + #region ITypedColumns Members + + + public void HyperLinkClicked(string sourcename, string columnname, string hyperlink, long row, int column) + { + foreach (ILogSource s in m_originalSources) + { + if ((s is ITypedColumns) && + (s.Name == sourcename)) // The original source for the row containing the hyperlink + { + ((ITypedColumns)s).HyperLinkClicked(sourcename, columnname, hyperlink, row, column); + } + } + } + + #endregion + } +#endregion + + #region TypedColumnCollection + + internal class TypedColumnCollection + { + private Dictionary m_typedColumns = null; + + internal TypedColumnCollection() + { + m_typedColumns = new Dictionary(); + } + + internal void AddColumnType(string columnName, int columnType) + { + if (!m_typedColumns.ContainsKey(columnName)) + { + m_typedColumns.Add(columnName, columnType); + } + } + + internal int GetColumnType(string columnName) + { + int returnType; + if (m_typedColumns.TryGetValue(columnName, out returnType)) + { + return returnType; + } + return GridColumnType.Text; + } + + internal bool IsEmpty + { + get + { + return m_typedColumns.Count == 0; + } + } + } + + #endregion + #region LogAggregator class - ILogAggregator algorithm + /// + /// Summary description for LogAggregator. + /// + internal class LogAggregator : ILogAggregator + { +#region Constants + internal const int cProgressLogCreated = 1; + internal const int cProgressLoaded = 15; + internal const int cProgressAlmostDone = 95; + internal const int cProgressDone = 100; +#endregion + +#region Properties - CancelInternal (lock-ed access) + private volatile bool m_boolCancelInternal = false; + internal bool CancelInternal + { + get + { + + return m_boolCancelInternal; + + } + set + { + lock (this) + { + m_boolCancelInternal = value; + } + } + } + + private volatile bool m_boolStopInternal = false; + internal bool StopInternal + { + get + { + + return m_boolStopInternal; + + } + set + { + lock (this) + { + m_boolStopInternal = value; + } + } + } + + + private bool m_reverseOrder = true; + internal bool ReverseOrder + { + get + { + return m_reverseOrder; + } + set + { + m_reverseOrder = value; + } + } +#endregion + +#region Variables + private LogSourceAggregation m_currentSource = null; +#endregion + +#region Constructor + /// + /// create an log aggregator using a default empty cache + /// + public LogAggregator() + { + } + +#endregion + +#region ILogAggregator interface implementation + ILogSource ILogAggregator.PrepareAggregation(string outputLogSourceName, ILogSource[] sources, ILogConstraints filterTemplate) + { + ILogSource outputSource = CreateUninitializedAggregation(outputLogSourceName, sources, filterTemplate); + + m_currentSource = outputSource as LogSourceAggregation; + + return outputSource; + } + + void ILogAggregator.CancelAsyncWork() + { + CancelInternal = true; + } + + void ILogAggregator.StopAsyncWork() + { + StopInternal = true; + } +#endregion + +#region CreateUninitializedAggregation algorithm + /// + /// agregates one or more sources -> creates a new (uninitialized) aggregation + /// + /// NOTE: + /// we also 'aggregate' only 1 source to gain the advantage offered by this algoritm + /// of being able to pump entry-s to ui thread in chucks instead of sending all source + /// in one shoot -> more responsive ui + /// + /// + /// + /// null if no filter + /// + private ILogSource CreateUninitializedAggregation(string outputLogSourceName, ILogSource[] sources, ILogConstraints filterTemplate) + { + // zero sources - nothing we can do + if ((sources == null) || (sources.Length==0)) + { + return null; + } + + ILogSource newAggregation = null; + try + { + // not in cache, so build it, add it to cache (if caching ok) and return it + newAggregation = new LogSourceAggregation(this, outputLogSourceName, sources, filterTemplate); + + return newAggregation; + + } + finally + { + Raise_AggregationProgress( "LogViewerSR.AggregationProgress_BeginInitialize", + cProgressLogCreated, + null); + } + } +#endregion + +#region DelegateAggregationWorkImplementation - entry for - asynchronous invocation with callback ***** via delegate + private List m_exceptionsList = new List(); +#endregion + +#region Report Progress + /// + /// if job not null and callbackProgress available -> invoke progress delegate in ui thread + /// + /// + /// + /// + internal void Raise_AggregationProgress(string message, + int percent, + IList exceptionList) + { + } + +#endregion + } +#endregion +} + diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/LogInterfaces.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/LogInterfaces.cs new file mode 100644 index 00000000..2fa300cc --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Agent/LogInterfaces.cs @@ -0,0 +1,274 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlServer.Management.Sdk.Sfc; +using System; +using Microsoft.SqlServer.Management.Common; +using System.Collections.Generic; +using System.Collections; +using Microsoft.SqlServer.Management.Diagnostics; +using System.Collections.ObjectModel; + +namespace Microsoft.SqlTools.ServiceLayer.Agent +{ + public class GridColumnType + { + public const int + Text = 1, + Button = 2, + Bitmap = 3, + Checkbox = 4, + Hyperlink = 5, + FirstCustomColumnType = 0x400; + }; + + // /// + /// ILogSourceTypeFactory knows to instantiate objects that deal with various types of logs + /// -- e.g. a factory that knows how to handle SqlServer, SqlAgent and Windows NT logs + /// + internal interface ILogSourceTypeFactory + { + ILogSourceType[] SourceTypes {get;} + } + + /// + /// ILogSourceType describes the interface for log source types + /// -- e.g. SqlServer, SqlAgent, Windows NT, file-stream, etc... + /// + internal interface ILogSourceType + { + string Name { get;} + ILogSource[] Sources { get;} + + void Refresh(); + } + + /// + /// ILogSource describes a log source + /// -- e.g. Current SqlServer log, Archive #3 of Sql Agent, NT Security log, etc... + /// + public interface ILogSource + { + string Name {get;} + /// + /// We allow only one initialization for the source. This is because upon aggregation we create a new LogSourceAggregation that + /// contains the seperate sources and initialize it. So if a source is already initialized from previous collection we shouldn't + /// initialize it again because we will have duplicate data. + /// + void Initialize(); + ILogSource[] SubSources {get;} + ILogEntry CurrentEntry { get;} + string[] FieldNames {get;} + ILogSource GetRefreshedClone(); + bool ReadEntry(); + void CloseReader(); + bool OrderedByDateDescending {get;} + } + + /// + /// ILogAggregator describes an algorithm that agregates multiple ILogSources + /// -- e.g. algorithm that interleaves multiple logs sources based on log entry times + /// + internal interface ILogAggregator + { + ILogSource PrepareAggregation (string outputLogSourceName, ILogSource[] sources, ILogConstraints filter); + void CancelAsyncWork(); //CancelAsyncWork stops the current aggregation and at the end closes all the open readers for the sources + void StopAsyncWork(); // StopAsyncWork does the same thing as CancelAsyncWork but instead it leaves the readers open because this is used in incremental aggregation so we will resume the collection by adding the new source selected + + } + + /// + /// used for filtering and searching log entries + /// + internal interface ILogConstraints + { + bool MatchLogEntry(ILogEntry entry); + bool IsEntryWithinFilterTimeWindow(ILogEntry entry); + } + + internal interface ITypedColumns + { + TypedColumnCollection ColumnTypes { get; } + void HyperLinkClicked(string sourcename, string columnname, string hyperlink, long row, int column); + } + +#if false + + /// + /// Interface for the storage view that creates a view of the data storage class + /// + internal interface ILogStorageView : IStorageView + { + ILogEntry EntryAt(long row); + LogColumnInfo.RowInfo GetRowInfoAt(int row); + ReadOnlyCollection VisibleFieldNames { get; } + void Clear(); + bool IsRowExpandable(int rowIndex); + bool IsRowExpanded(int rowIndex); + int RowLevel(int rowIndex); + int ExpandRow(int rowIndex); + int CollapseRow(int rowIndex); + ReadOnlyCollection VisibleSourceNames { get; } + } + + /// + /// Interface for the Data storage class that can be either memory based or disk based. + /// + internal interface ILogDataStorage : IDataStorage, ITypedColumns + { + void Initialize(); + /// + /// returns a view of the storage with the applied filter or a plain view if the filter is disabled. + /// + /// + ILogStorageView GetFilteredView(); + /// + /// stops the storing of the data, This is used in the incremental aggregation so as to stop the collection without closing the readers + /// + void StopStoringData(); + /// + /// cancels the collection and implies that the readers should be closed + /// + void CancelStoring(); + StorageNotifyDelegate StorageNotifyDelegate { get; set;} + ReadOnlyCollection FieldNames { get;} + ILogSource AggregationSource { get; set;} + + ReadOnlyCollection VisibleFieldNames { get; set; } + /// + /// is used when no filter is applied in order to avoid going into aggregation mode and just output all available entries to the consumer + /// + bool NotifyAll { get; set; } + bool IsCanceled { get;} + /// + /// is used to return a list with the row numbers that correspond to all the entries from the demanded sources + /// + /// + IList GetDemandedSourcesRowIndexList(); + /// + /// This returns the fitler that was defined when we started the collection. + /// This filter was pushed to the server + /// + LogConstraints CollectionFilter { get; set;} + /// + /// This returns the client side side filter that is currently set in order to + /// show a filtered view of the collected data + /// + LogConstraints ClientFilter { get; set;} + } + + /// + /// Interface for the the sorting view which keeps a mapping of relative rows and absolute rows + /// based on the column key + /// + internal interface ILogSortedView : IComparer + { + void Initialize(ILogStorageView view); + int KeyIndex { get; set; } + bool IsDescending { get; set; } + void SortData(); + int GetAbsoluteRowNumber(int iRelativeRow); + void StopSortingData(); + int SortedRows(); + void ReverseSorting(); + void ExpandRow(int iRow, int subRowsCount); + void CollapseRow(int iRow, int subRowsCount); + void ExpandParentRows(ILogStorageView view); + StorageNotifyDelegate StorageNotifyDelegate { get; set; } + } + + /// + /// used by ui thread to schedule async invocations (ui should use AsyncCallback to get notified when compleated) + /// + internal delegate void DelegateAggregationWork(ILogDataStorage storage); + + /// + /// delegate used to notify that asyncronous aggregation job was completed + /// -- e.g. an aggregator finished initializing some of its inner sources a ui component that displays progress of aggregation will be called + /// + internal delegate void DelegateAggregationProgress(object sender, + string message, + int percentage, + IList exceptionList); + + /// + /// event that is triggered when user changes visible columns or available columns + /// -- e.g. user pop-ed up the ui beoyind a IColumnManager and wants to apply changes + /// + internal delegate void DelegateColumnsInformationChanged (object sender, string[] columns); + + /// + /// severity associated with a log entry (ILogEntry) + // these should be ordered least severe to most severe where possible. + /// + public enum SeverityClass + { + Unknown = -1, + Success, + Information, + SuccessAudit, + InProgress, + Retry, + Warning, + FailureAudit, + Cancelled, + Error + } + + /// + /// command that can be executed by a log viewer (ILogViewer) + /// + internal enum LogViewerCommand + { + Load = 0, + Refresh, + Export, + Columns, + Filter, + Search, + Delete, + Help, + Close, + Cancel + } + + /// + /// command options + /// + internal enum LogViewerCommandOption + { + None = 0, + Hide, + Show + } + + /// + /// Event arguments for various events + /// related to LogViewer + /// + internal class LogViewerEventArgs : EventArgs + { + private LogViewerCommand command; + + public LogViewerEventArgs(LogViewerCommand command) + { + this.command = command; + } + + public LogViewerCommand Command + { + get { return command; } + } + } +#endif +} + + + + + + + + diff --git a/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs b/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs index ee4df1a1..f0aa83c8 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs @@ -9,6 +9,7 @@ using Microsoft.SqlTools.Extensibility; using Microsoft.SqlTools.Hosting; using Microsoft.SqlTools.Hosting.Protocol; using Microsoft.SqlTools.ServiceLayer.Admin; +using Microsoft.SqlTools.ServiceLayer.Agent; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.DisasterRecovery; using Microsoft.SqlTools.ServiceLayer.EditData; @@ -93,6 +94,9 @@ namespace Microsoft.SqlTools.ServiceLayer AdminService.Instance.InitializeService(serviceHost); serviceProvider.RegisterSingleService(AdminService.Instance); + AgentService.Instance.InitializeService(serviceHost); + serviceProvider.RegisterSingleService(AgentService.Instance); + DisasterRecoveryService.Instance.InitializeService(serviceHost); serviceProvider.RegisterSingleService(DisasterRecoveryService.Instance); diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Agent/AgentServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Agent/AgentServiceTests.cs new file mode 100644 index 00000000..12fc236e --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Agent/AgentServiceTests.cs @@ -0,0 +1,97 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Data.SqlClient; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SqlServer.Management.Smo; +using Microsoft.SqlServer.Management.XEvent; +using Microsoft.SqlTools.Hosting.Protocol; +using Microsoft.SqlTools.ServiceLayer.Agent; +using Microsoft.SqlTools.ServiceLayer.Agent.Contracts; +using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility; +using Microsoft.SqlTools.ServiceLayer.Profiler; +using Microsoft.SqlTools.ServiceLayer.Profiler.Contracts; +using Microsoft.SqlTools.ServiceLayer.Test.Common; +using Moq; +using Xunit; + +namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Agent +{ + public class AgentServiceTests + { + /// + /// Verify that a start profiling request starts a profiling session + /// + [Fact] + public async Task TestHandleAgentJobsRequest() + { + using (SelfCleaningTempFile queryTempFile = new SelfCleaningTempFile()) + { + var connectionResult = await LiveConnectionHelper.InitLiveConnectionInfoAsync("master", queryTempFile.FilePath); + + var requestParams = new AgentJobsParams() + { + OwnerUri = connectionResult.ConnectionInfo.OwnerUri + }; + + var requestContext = new Mock>(); + + AgentService service = new AgentService(); + await service.HandleAgentJobsRequest(requestParams, requestContext.Object); + requestContext.VerifyAll(); + } + } + + /// + /// Verify that a job history request returns the job history + /// + [Fact] + public async Task TestHandleJobHistoryRequests() + { + using (SelfCleaningTempFile queryTempFile = new SelfCleaningTempFile()) + { + var connectionResult = await LiveConnectionHelper.InitLiveConnectionInfoAsync("master", queryTempFile.FilePath); + + var requestParams = new AgentJobHistoryParams() + { + OwnerUri = connectionResult.ConnectionInfo.OwnerUri, + JobId = "e9420919-b8c2-4a3d-a26c-b7ffde5342cf" + }; + + var requestContext = new Mock>(); + + AgentService service = new AgentService(); + await service.HandleJobHistoryRequest(requestParams, requestContext.Object); + requestContext.VerifyAll(); + } + } + + [Fact] + public async Task TestHandleAgentJobActionRequest() + { + using (SelfCleaningTempFile queryTempFile = new SelfCleaningTempFile()) + { + var connectionResult = await LiveConnectionHelper.InitLiveConnectionInfoAsync("master", queryTempFile.FilePath); + + var requestParams = new AgentJobActionParams() + { + OwnerUri = connectionResult.ConnectionInfo.OwnerUri, + JobName = "Agent history clean up: distribution" + }; + + var requestContext = new Mock>(); + + AgentService service = new AgentService(); + await service.HandleJobActionRequest(requestParams, requestContext.Object); + requestContext.VerifyAll(); + } + } + } +} \ No newline at end of file