Files
sqltoolsservice/src/Microsoft.SqlTools.ServiceLayer/Agent/Jobs/JobStepsData.cs
Karl Burtram d2cc376b87 Agent Proxy and Credential request handlers (#637)
* Agent Proxy account WIP

* Fixup Credential create\update\delete handlers

* Use current user for test credential

* Cleanup and delete code

* Convert tabs to spaces
2018-06-14 11:55:38 -07:00

631 lines
20 KiB
C#

//
// 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;
using System.Collections.Generic;
using System.Collections.Specialized;
using Microsoft.SqlServer.Management.Common;
using Microsoft.SqlServer.Management.Smo.Agent;
using Microsoft.SqlTools.ServiceLayer.Management;
namespace Microsoft.SqlTools.ServiceLayer.Agent
{
internal class JobStepsData
{
#region fields
/// <summary>
/// collection of job steps.
/// </summary>
private ArrayList jobSteps;
/// <summary>
/// List of job steps to be deleted
/// </summary>
private ArrayList deletedJobSteps;
/// <summary>
/// Parent JobData
/// </summary>
private JobData parent;
/// <summary>
/// Server context
/// </summary>
private CDataContainer context;
/// <summary>
/// Start Step
/// </summary>
private JobStepData startStep;
/// <summary>
/// list of available databases
/// </summary>
private string[] databases = null;
#endregion
#region public properties
/// <summary>
/// JobData structure this object is part of
/// </summary>
public JobData Parent
{
get
{
return this.parent;
}
}
/// <summary>
/// Server Version
/// </summary>
public Version Version
{
get
{
return this.parent.Version;
}
}
/// <summary>
/// Mode in which the dialog has been launched
/// </summary>
JobData.ActionMode Mode
{
get
{
if (this.parent != null)
{
return this.parent.Mode;
}
else
{
return JobData.ActionMode.Unknown;
}
}
}
/// <summary>
/// List of steps in this job
/// </summary>
public ArrayList Steps
{
get
{
return this.jobSteps;
}
}
/// <summary>
/// The default start step
/// </summary>
public JobStepData StartStep
{
get
{
// we can't point to a step that is marked for deletion
if (this.startStep != null && this.startStep.ToBeDeleted == true)
{
this.startStep = null;
}
// if the start step is null, and we have job steps, just point
// the start step to the first one.
if (this.startStep == null && this.jobSteps.Count > 0)
{
this.startStep = (JobStepData)this.jobSteps[0];
}
return this.startStep;
}
set
{
this.startStep = value;
}
}
/// <summary>
/// List of all available databases on the server
/// </summary>
public string[] Databases
{
get
{
CheckAndLoadDatabases();
return this.databases;
}
}
/// <summary>
/// Indicates whether or not the order of the steps has changed
/// </summary>
public bool HasStepOrderChanged
{
get
{
bool orderChanged = false;
foreach (JobStepData jsd in this.jobSteps)
{
if (jsd.StepIdChanged == true)
{
orderChanged = true;
break;
}
}
return orderChanged;
}
}
/// <summary>
/// Indicates whether or not the Job is read only
/// </summary>
public bool IsReadOnly
{
get { return parent.IsReadOnly; }
}
#endregion
#region Events
public event EventHandler StepOrderChanged;
#endregion
#region construction
/// <summary>
/// Create a new JobStepsData object with a new job step
/// </summary>
/// <param name="context">server context</param>
/// <param name="script">script for the job step</param>
/// <param name="parent">owning data object</param>
public JobStepsData(CDataContainer context, string script, JobData parent)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
if (script == null)
{
throw new ArgumentNullException("strint");
}
if (parent == null)
{
throw new ArgumentNullException("parent");
}
CommonInit(context, parent, script);
}
/// <summary>
/// Create a new jobsteps data object
/// </summary>
/// <param name="context">server context</param>
/// <param name="parent">owning data object</param>
public JobStepsData(CDataContainer context, JobData parent)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
if (parent == null)
{
throw new ArgumentNullException("parent");
}
CommonInit(context, parent, null);
}
/// <summary>
/// Common initialization routines for constructrs
/// </summary>
/// <param name="context"></param>
/// <param name="parent"></param>
/// <param name="script"></param>
private void CommonInit(CDataContainer context, JobData parent, string script)
{
this.context = context;
this.parent = parent;
this.deletedJobSteps = new ArrayList();
// if we're creating a new job
if (this.parent.Mode != JobData.ActionMode.Edit)
{
SetDefaults();
if (script != null && script.Length != 0)
{
LoadFromScript(script);
}
}
else
{
// load the JobStep objects
LoadData();
}
}
#endregion
#region public methods
/// <summary>
/// Add a new existing step to the end of the job step collection
/// </summary>
/// <param name="step"></param>
public void AddStep(JobStepData step)
{
this.jobSteps.Add(step);
RecalculateStepIds();
}
/// <summary>
/// Insert a jobstep into an existing location
/// </summary>
/// <param name="index"></param>
/// <param name="step"></param>
public void InsertStep(int index, JobStepData step)
{
this.jobSteps.Insert(index, step);
RecalculateStepIds();
}
/// <summary>
/// Delete a jobstep
/// </summary>
/// <param name="step"></param>
public void DeleteStep(JobStepData step)
{
if (step == null)
{
throw new ArgumentNullException("step");
}
if (this.jobSteps.Contains(step))
{
this.jobSteps.Remove(step);
// make a note to delete the step
this.deletedJobSteps.Add(step);
step.ToBeDeleted = true;
}
RecalculateStepIds();
}
/// <summary>
/// Get a JobStepData object for a step id
/// </summary>
/// <param name="stepId"></param>
/// <returns></returns>
public JobStepData GetObjectForStep(int stepId)
{
JobStepData jobStep = null;
if (this.jobSteps != null)
{
foreach (JobStepData jsd in this.jobSteps)
{
if (jsd.ID == stepId)
{
jobStep = jsd;
break;
}
}
}
return jobStep;
}
/// <summary>
/// Check for any job steps that are unreachable.
/// Because there are only two paths and we don't care about circular references
/// we can use a simplified search, rather than a full graph dfs or bfs.
/// </summary>
/// <returns>List of unreachable steps, or an empty list if there are none</returns>
public List<JobStepData> FindUnreachableJobSteps()
{
// array used to keep track of whether or not a step is reachable
bool[] stepReachable = new bool[this.jobSteps.Count];
// mark the start step as reachable
if (this.startStep != null && this.startStep.ID > 0 && this.startStep.ID <= this.jobSteps.Count)
{
stepReachable[this.startStep.ID - 1] = true;
}
// steps indexes start at 1
foreach (JobStepData step in this.jobSteps)
{
// check success actions
if (step.SuccessAction == StepCompletionAction.GoToNextStep)
{
// if we aren't on the last step mark the next step as valid
if (step.ID < this.jobSteps.Count)
{
stepReachable[step.ID] = true;
}
}
else if (step.SuccessAction == StepCompletionAction.GoToStep)
{
if (step.SuccessStep != null && step.SuccessStep.ID <= this.jobSteps.Count)
{
stepReachable[step.SuccessStep.ID - 1] = true;
}
}
// check failure actions
if (step.FailureAction == StepCompletionAction.GoToNextStep)
{
// if we aren't on the last step mark the next step as valid
if (step.ID < this.jobSteps.Count)
{
stepReachable[step.ID] = true;
}
}
else if (step.FailureAction == StepCompletionAction.GoToStep)
{
if (step.FailStep != null && step.FailStep.ID <= this.jobSteps.Count)
{
stepReachable[step.FailStep.ID - 1] = true;
}
}
}
// walk through the array indicating if a step is reachable, and
// add any that are not to the list of unreachable steps
List<JobStepData> unreachableSteps = new List<JobStepData>();
for (int i = 0; i < stepReachable.Length; i++)
{
if (stepReachable[i] == false)
{
unreachableSteps.Add(this.jobSteps[i] as JobStepData);
}
}
return unreachableSteps;
}
/// <summary>
/// Checks to see if the Last steps success completion action will change.
/// It will if we are editing a job, and the last steps Success Completion
/// action is GoToNextStep
/// </summary>
/// <returns>true if changes will be automatically made to the last step</returns>
public bool CheckIfLastStepCompletionActionWillChange()
{
bool lastStepCompletionActionWillChange = false;
if(this.jobSteps.Count > 0)
{
// get the last step
JobStepData lastStep = this.jobSteps[this.jobSteps.Count-1] as JobStepData;
if (lastStep != null && parent.Mode == JobData.ActionMode.Edit && lastStep.SuccessAction == StepCompletionAction.GoToNextStep)
{
lastStepCompletionActionWillChange = true;
}
}
return lastStepCompletionActionWillChange;
}
#endregion
#region private/internal helpers
/// <summary>
/// Recalculate the step ids of the contained job steps
/// </summary>
internal void RecalculateStepIds()
{
for (int i = 0; i < this.jobSteps.Count; i++)
{
JobStepData jsd = jobSteps[i] as JobStepData;
if (jsd != null)
{
jsd.ID = i + 1;
}
}
OnStepOrderChanged(EventArgs.Empty);
}
/// <summary>
/// Delayed loading of database information
/// </summary>
private void CheckAndLoadDatabases()
{
if (this.databases != null)
{
return;
}
// load databases collection
this.databases = new string[this.context.Server.Databases.Count];
for (int i = 0; i < this.context.Server.Databases.Count; i++)
{
this.databases[i] = this.context.Server.Databases[i].Name;
}
}
/// <summary>
/// fire the StepOrderChanged event
/// </summary>
private void OnStepOrderChanged(EventArgs args)
{
if (this.StepOrderChanged != null)
{
this.StepOrderChanged(this, args);
}
}
/// <summary>
/// SMO job object we are manipulating
/// </summary>
internal Job Job
{
get
{
Job job = null;
if (this.parent != null)
{
job = parent.Job;
}
return job;
}
}
#endregion
#region data loading
/// <summary>
/// Load a job step from a script
/// </summary>
/// <param name="script"></param>
private void LoadFromScript(string script)
{
this.jobSteps = new ArrayList();
JobStepData jsd = new JobStepData(this);
jsd.Command = script;
jsd.SubSystem = AgentSubSystem.TransactSql;
jsd.ID = 1;
jsd.Name = "1";
this.jobSteps.Add(jsd);
}
/// <summary>
/// Load job steps from the server
/// </summary>
private void LoadData()
{
STParameters parameters = new STParameters(this.context.Document);
string urn = string.Empty;
string jobIdString = string.Empty;
parameters.GetParam("urn", ref urn);
parameters.GetParam("jobid", ref jobIdString);
// save current state of default fields
StringCollection originalFields = this.context.Server.GetDefaultInitFields(typeof(JobStep));
// Get all JobStep properties since the JobStepData class is going to use themn
this.context.Server.SetDefaultInitFields(typeof(JobStep), true);
try
{
Job job = null;
// If JobID is passed in look up by jobID
if (!string.IsNullOrEmpty(jobIdString))
{
job = this.context.Server.JobServer.Jobs.ItemById(Guid.Parse(jobIdString));
}
else
{
// or use urn path to query job
job = this.context.Server.GetSmoObject(urn) as Job;
}
// load the data
JobStepCollection steps = job.JobSteps;
// allocate the array list
this.jobSteps = new ArrayList(steps.Count);
for (int i = 0; i < steps.Count; i++)
{
// add them in step id order
int ii = 0;
for (; ii < this.jobSteps.Count; ii++)
{
if (steps[i].ID < ((JobStepData)this.jobSteps[ii]).ID)
{
break;
}
}
this.jobSteps.Insert(ii, new JobStepData(steps[i], this));
}
// figure out the start step
this.startStep = GetObjectForStep(job.StartStepID);
// fixup all of the jobsteps failure/completion actions
foreach (JobStepData jobStep in this.jobSteps)
{
jobStep.LoadCompletionActions();
}
}
finally
{
// revert to initial default fields for this type
this.context.Server.SetDefaultInitFields(typeof(JobStep), originalFields);
}
}
/// <summary>
/// Set default values for a new empty job
/// </summary>
private void SetDefaults()
{
this.jobSteps = new ArrayList();
}
#endregion
#region saving
/// <summary>
/// Save changes to all job steps
/// </summary>
/// <param name="job">owner job</param>
/// <returns>True if any changes were saved</returns>
public bool ApplyChanges(Job job)
{
bool changesMade = false;
if (this.IsReadOnly)
{
return false;
}
bool scripting = this.context.Server.ConnectionContext.SqlExecutionModes == SqlExecutionModes.CaptureSql;
// delete all of the deleted steps
for (int i = deletedJobSteps.Count - 1; i >= 0; i--)
{
JobStepData step = this.deletedJobSteps[i] as JobStepData;
if (step != null)
{
if (step.Created)
{
step.Delete();
changesMade = true;
}
}
// don't clear the list if we are just scripting the action.
if (!scripting)
{
deletedJobSteps.RemoveAt(i);
}
}
bool forceRebuildingOfSteps = HasStepOrderChanged;
// check to see if the step id's have changed. if so we will have to
// drop and recreate all of the steps
if (forceRebuildingOfSteps)
{
for (int i = this.jobSteps.Count - 1; i >= 0; --i)
{
JobStepData step = this.jobSteps[i] as JobStepData;
// only delete steps that exist on the server
if (step.Created)
{
step.Delete();
changesMade = true;
}
}
}
// update the remaining steps
foreach (JobStepData step in this.jobSteps)
{
if (step.ApplyChanges(job, forceRebuildingOfSteps))
{
changesMade = true;
}
}
// update the start step
if (StartStep == null && job.StartStepID != 0)
{
job.StartStepID = 0;
changesMade = true;
}
else if (parent.Mode == JobData.ActionMode.Create || job.StartStepID != this.startStep.ID)
{
job.StartStepID = this.startStep.ID;
job.Alter();
changesMade = true;
}
return changesMade;
}
#endregion
}
}