PowerShell/src/Microsoft.PowerShell.ScheduledJob/ScheduledJobSourceAdapter.cs

1089 lines
35 KiB
C#

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Runtime.Serialization;
using System.Text;
namespace Microsoft.PowerShell.ScheduledJob
{
/// <summary>
/// This class provides functionality for retrieving scheduled job run results
/// from the scheduled job store. An instance of this object will be registered
/// with the PowerShell JobManager so that GetJobs commands will retrieve schedule
/// job runs from the file based scheduled job store. This allows scheduled job
/// runs to be managed from PowerShell in the same way workflow jobs are managed.
/// </summary>
public sealed class ScheduledJobSourceAdapter : JobSourceAdapter
{
#region Private Members
private static FileSystemWatcher StoreWatcher;
private static object SyncObject = new object();
private static ScheduledJobRepository JobRepository = new ScheduledJobRepository();
internal const string AdapterTypeName = "PSScheduledJob";
#endregion
#region Public Strings
/// <summary>
/// BeforeFilter.
/// </summary>
public const string BeforeFilter = "Before";
/// <summary>
/// AfterFilter.
/// </summary>
public const string AfterFilter = "After";
/// <summary>
/// NewestFilter.
/// </summary>
public const string NewestFilter = "Newest";
#endregion
#region Constructor
/// <summary>
/// Constructor.
/// </summary>
public ScheduledJobSourceAdapter()
{
Name = AdapterTypeName;
}
#endregion
#region JobSourceAdapter Implementation
/// <summary>
/// Create a new Job2 results instance.
/// </summary>
/// <param name="specification">Job specification.</param>
/// <returns>Job2.</returns>
public override Job2 NewJob(JobInvocationInfo specification)
{
if (specification == null)
{
throw new PSArgumentNullException("specification");
}
ScheduledJobDefinition scheduledJobDef = new ScheduledJobDefinition(
specification, null, null, null);
return new ScheduledJob(
specification.Command,
specification.Name,
scheduledJobDef);
}
/// <summary>
/// Creates a new Job2 object based on a definition name
/// that can be run manually. If the path parameter is
/// null then a default location will be used to find the
/// job definition by name.
/// </summary>
/// <param name="definitionName">ScheduledJob definition name.</param>
/// <param name="definitionPath">ScheduledJob definition file path.</param>
/// <returns>Job2 object.</returns>
public override Job2 NewJob(string definitionName, string definitionPath)
{
if (string.IsNullOrEmpty(definitionName))
{
throw new PSArgumentException("definitionName");
}
Job2 rtnJob = null;
try
{
ScheduledJobDefinition scheduledJobDef =
ScheduledJobDefinition.LoadFromStore(definitionName, definitionPath);
rtnJob = new ScheduledJob(
scheduledJobDef.Command,
scheduledJobDef.Name,
scheduledJobDef);
}
catch (FileNotFoundException)
{
// Return null if no job definition exists.
}
return rtnJob;
}
/// <summary>
/// Get the list of jobs that are currently available in this
/// store.
/// </summary>
/// <returns>Collection of job objects.</returns>
public override IList<Job2> GetJobs()
{
RefreshRepository();
List<Job2> rtnJobs = new List<Job2>();
foreach (var job in JobRepository.Jobs)
{
rtnJobs.Add(job);
}
return rtnJobs;
}
/// <summary>
/// Get list of jobs that matches the specified names.
/// </summary>
/// <param name="name">names to match, can support
/// wildcard if the store supports</param>
/// <param name="recurse"></param>
/// <returns>Collection of jobs that match the specified
/// criteria.</returns>
public override IList<Job2> GetJobsByName(string name, bool recurse)
{
if (string.IsNullOrEmpty(name))
{
throw new PSArgumentException("name");
}
RefreshRepository();
WildcardPattern namePattern = new WildcardPattern(name, WildcardOptions.IgnoreCase);
List<Job2> rtnJobs = new List<Job2>();
foreach (var job in JobRepository.Jobs)
{
if (namePattern.IsMatch(job.Name))
{
rtnJobs.Add(job);
}
}
return rtnJobs;
}
/// <summary>
/// Get list of jobs that run the specified command.
/// </summary>
/// <param name="command">Command to match.</param>
/// <param name="recurse"></param>
/// <returns>Collection of jobs that match the specified
/// criteria.</returns>
public override IList<Job2> GetJobsByCommand(string command, bool recurse)
{
if (string.IsNullOrEmpty(command))
{
throw new PSArgumentException("command");
}
RefreshRepository();
WildcardPattern commandPattern = new WildcardPattern(command, WildcardOptions.IgnoreCase);
List<Job2> rtnJobs = new List<Job2>();
foreach (var job in JobRepository.Jobs)
{
if (commandPattern.IsMatch(job.Command))
{
rtnJobs.Add(job);
}
}
return rtnJobs;
}
/// <summary>
/// Get job that has the specified id.
/// </summary>
/// <param name="instanceId">Guid to match.</param>
/// <param name="recurse"></param>
/// <returns>Job with the specified guid.</returns>
public override Job2 GetJobByInstanceId(Guid instanceId, bool recurse)
{
RefreshRepository();
foreach (var job in JobRepository.Jobs)
{
if (Guid.Equals(job.InstanceId, instanceId))
{
return job;
}
}
return null;
}
/// <summary>
/// Get job that has specific session id.
/// </summary>
/// <param name="id">Id to match.</param>
/// <param name="recurse"></param>
/// <returns>Job with the specified id.</returns>
public override Job2 GetJobBySessionId(int id, bool recurse)
{
RefreshRepository();
foreach (var job in JobRepository.Jobs)
{
if (id == job.Id)
{
return job;
}
}
return null;
}
/// <summary>
/// Get list of jobs that are in the specified state.
/// </summary>
/// <param name="state">State to match.</param>
/// <param name="recurse"></param>
/// <returns>Collection of jobs with the specified
/// state.</returns>
public override IList<Job2> GetJobsByState(JobState state, bool recurse)
{
RefreshRepository();
List<Job2> rtnJobs = new List<Job2>();
foreach (var job in JobRepository.Jobs)
{
if (state == job.JobStateInfo.State)
{
rtnJobs.Add(job);
}
}
return rtnJobs;
}
/// <summary>
/// Get list of jobs based on the adapter specific
/// filter parameters.
/// </summary>
/// <param name="filter">dictionary containing name value
/// pairs for adapter specific filters</param>
/// <param name="recurse"></param>
/// <returns>Collection of jobs that match the
/// specified criteria.</returns>
public override IList<Job2> GetJobsByFilter(Dictionary<string, object> filter, bool recurse)
{
if (filter == null)
{
throw new PSArgumentNullException("filter");
}
List<Job2> rtnJobs = new List<Job2>();
foreach (var filterItem in filter)
{
switch (filterItem.Key)
{
case BeforeFilter:
GetJobsBefore((DateTime)filterItem.Value, ref rtnJobs);
break;
case AfterFilter:
GetJobsAfter((DateTime)filterItem.Value, ref rtnJobs);
break;
case NewestFilter:
GetNewestJobs((int)filterItem.Value, ref rtnJobs);
break;
}
}
return rtnJobs;
}
/// <summary>
/// Remove a job from the store.
/// </summary>
/// <param name="job">Job object to remove.</param>
public override void RemoveJob(Job2 job)
{
if (job == null)
{
throw new PSArgumentNullException("job");
}
RefreshRepository();
try
{
JobRepository.Remove(job);
ScheduledJobStore.RemoveJobRun(
job.Name,
job.PSBeginTime ?? DateTime.MinValue);
}
catch (DirectoryNotFoundException)
{
}
catch (FileNotFoundException)
{
}
}
/// <summary>
/// Saves job to scheduled job run store.
/// </summary>
/// <param name="job">ScheduledJob.</param>
public override void PersistJob(Job2 job)
{
if (job == null)
{
throw new PSArgumentNullException("job");
}
SaveJobToStore(job as ScheduledJob);
}
#endregion
#region Save Job
/// <summary>
/// Serializes a ScheduledJob and saves it to store.
/// </summary>
/// <param name="job">ScheduledJob.</param>
internal static void SaveJobToStore(ScheduledJob job)
{
string outputPath = job.Definition.OutputPath;
if (string.IsNullOrEmpty(outputPath))
{
string msg = StringUtil.Format(ScheduledJobErrorStrings.CantSaveJobNoFilePathSpecified,
job.Name);
throw new ScheduledJobException(msg);
}
FileStream fsStatus = null;
FileStream fsResults = null;
try
{
// Check the job store results and if maximum number of results exist
// remove the oldest results folder to make room for these new results.
CheckJobStoreResults(outputPath, job.Definition.ExecutionHistoryLength);
fsStatus = ScheduledJobStore.CreateFileForJobRunItem(
outputPath,
job.PSBeginTime ?? DateTime.MinValue,
ScheduledJobStore.JobRunItem.Status);
// Save status only in status file stream.
SaveStatusToFile(job, fsStatus);
fsResults = ScheduledJobStore.CreateFileForJobRunItem(
outputPath,
job.PSBeginTime ?? DateTime.MinValue,
ScheduledJobStore.JobRunItem.Results);
// Save entire job in results file stream.
SaveResultsToFile(job, fsResults);
}
finally
{
if (fsStatus != null)
{
fsStatus.Close();
}
if (fsResults != null)
{
fsResults.Close();
}
}
}
/// <summary>
/// Writes the job status information to the provided
/// file stream.
/// </summary>
/// <param name="job">ScheduledJob job to save.</param>
/// <param name="fs">FileStream.</param>
private static void SaveStatusToFile(ScheduledJob job, FileStream fs)
{
StatusInfo statusInfo = new StatusInfo(
job.InstanceId,
job.Name,
job.Location,
job.Command,
job.StatusMessage,
job.JobStateInfo.State,
job.HasMoreData,
job.PSBeginTime,
job.PSEndTime,
job.Definition);
XmlObjectSerializer serializer = new System.Runtime.Serialization.NetDataContractSerializer();
serializer.WriteObject(fs, statusInfo);
fs.Flush();
}
/// <summary>
/// Writes the job (which implements ISerializable) to the provided
/// file stream.
/// </summary>
/// <param name="job">ScheduledJob job to save.</param>
/// <param name="fs">FileStream.</param>
private static void SaveResultsToFile(ScheduledJob job, FileStream fs)
{
XmlObjectSerializer serializer = new System.Runtime.Serialization.NetDataContractSerializer();
serializer.WriteObject(fs, job);
fs.Flush();
}
/// <summary>
/// Check the job store results and if maximum number of results exist
/// remove the oldest results folder to make room for these new results.
/// </summary>
/// <param name="outputPath">Output path.</param>
/// <param name="executionHistoryLength">Maximum size of stored job results.</param>
private static void CheckJobStoreResults(string outputPath, int executionHistoryLength)
{
// Get current results for this job definition.
Collection<DateTime> jobRuns = ScheduledJobStore.GetJobRunsForDefinitionPath(outputPath);
if (jobRuns.Count <= executionHistoryLength)
{
// There is room for another job run in the store.
return;
}
// Remove the oldest job run from the store.
DateTime jobRunToRemove = DateTime.MaxValue;
foreach (DateTime jobRun in jobRuns)
{
jobRunToRemove = (jobRun < jobRunToRemove) ? jobRun : jobRunToRemove;
}
try
{
ScheduledJobStore.RemoveJobRunFromOutputPath(outputPath, jobRunToRemove);
}
catch (UnauthorizedAccessException)
{ }
}
#endregion
#region Retrieve Job
/// <summary>
/// Finds and load the Job associated with this ScheduledJobDefinition object
/// having the job run date time provided.
/// </summary>
/// <param name="jobRun">DateTime of job run to load.</param>
/// <param name="definitionName">ScheduledJobDefinition name.</param>
/// <returns>Job2 job loaded from store.</returns>
internal static Job2 LoadJobFromStore(string definitionName, DateTime jobRun)
{
FileStream fsResults = null;
Exception ex = null;
bool corruptedFile = false;
Job2 job = null;
try
{
// Results
fsResults = ScheduledJobStore.GetFileForJobRunItem(
definitionName,
jobRun,
ScheduledJobStore.JobRunItem.Results,
FileMode.Open,
FileAccess.Read,
FileShare.Read);
job = LoadResultsFromFile(fsResults);
}
catch (ArgumentException e)
{
ex = e;
}
catch (DirectoryNotFoundException e)
{
ex = e;
}
catch (FileNotFoundException e)
{
ex = e;
corruptedFile = true;
}
catch (UnauthorizedAccessException e)
{
ex = e;
}
catch (IOException e)
{
ex = e;
}
catch (System.Runtime.Serialization.SerializationException)
{
corruptedFile = true;
}
catch (System.Runtime.Serialization.InvalidDataContractException)
{
corruptedFile = true;
}
catch (System.Xml.XmlException)
{
corruptedFile = true;
}
catch (System.TypeInitializationException)
{
corruptedFile = true;
}
finally
{
if (fsResults != null)
{
fsResults.Close();
}
}
if (corruptedFile)
{
// Remove the corrupted job results file.
ScheduledJobStore.RemoveJobRun(definitionName, jobRun);
}
if (ex != null)
{
string msg = StringUtil.Format(ScheduledJobErrorStrings.CantLoadJobRunFromStore, definitionName, jobRun);
throw new ScheduledJobException(msg, ex);
}
return job;
}
/// <summary>
/// Loads the Job2 object from provided files stream.
/// </summary>
/// <param name="fs">FileStream from which to read job object.</param>
/// <returns>Created Job2 from file stream.</returns>
private static Job2 LoadResultsFromFile(FileStream fs)
{
XmlObjectSerializer serializer = new System.Runtime.Serialization.NetDataContractSerializer();
return (Job2)serializer.ReadObject(fs);
}
#endregion
#region Static Methods
/// <summary>
/// Adds a Job2 object to the repository.
/// </summary>
/// <param name="job">Job2.</param>
internal static void AddToRepository(Job2 job)
{
if (job == null)
{
throw new PSArgumentNullException("job");
}
JobRepository.AddOrReplace(job);
}
/// <summary>
/// Clears all items in the repository.
/// </summary>
internal static void ClearRepository()
{
JobRepository.Clear();
}
/// <summary>
/// Clears all items for given job definition name in the
/// repository.
/// </summary>
/// <param name="definitionName">Scheduled job definition name.</param>
internal static void ClearRepositoryForDefinition(string definitionName)
{
if (string.IsNullOrEmpty(definitionName))
{
throw new PSArgumentException("definitionName");
}
// This returns a new list object of repository jobs.
List<Job2> jobList = JobRepository.Jobs;
foreach (var job in jobList)
{
if (string.Compare(definitionName, job.Name,
StringComparison.OrdinalIgnoreCase) == 0)
{
JobRepository.Remove(job);
}
}
}
#endregion
#region Private Methods
private void RefreshRepository()
{
ScheduledJobStore.CreateDirectoryIfNotExists();
CreateFileSystemWatcher();
IEnumerable<string> jobDefinitions = ScheduledJobStore.GetJobDefinitions();
foreach (string definitionName in jobDefinitions)
{
// Create Job2 objects for each job run in store.
Collection<DateTime> jobRuns = GetJobRuns(definitionName);
if (jobRuns == null)
{
continue;
}
ScheduledJobDefinition definition = null;
foreach (DateTime jobRun in jobRuns)
{
if (jobRun > JobRepository.GetLatestJobRun(definitionName))
{
Job2 job;
try
{
if (definition == null)
{
definition = ScheduledJobDefinition.LoadFromStore(definitionName, null);
}
job = LoadJobFromStore(definition.Name, jobRun);
}
catch (ScheduledJobException)
{
continue;
}
catch (DirectoryNotFoundException)
{
continue;
}
catch (FileNotFoundException)
{
continue;
}
catch (UnauthorizedAccessException)
{
continue;
}
catch (IOException)
{
continue;
}
JobRepository.AddOrReplace(job);
JobRepository.SetLatestJobRun(definitionName, jobRun);
}
}
}
}
private void CreateFileSystemWatcher()
{
// Lazily create the static file system watcher
// on first use.
if (StoreWatcher == null)
{
lock (SyncObject)
{
if (StoreWatcher == null)
{
StoreWatcher = new FileSystemWatcher(ScheduledJobStore.GetJobDefinitionLocation());
StoreWatcher.IncludeSubdirectories = true;
StoreWatcher.NotifyFilter = NotifyFilters.LastWrite;
StoreWatcher.Filter = "Results.xml";
StoreWatcher.EnableRaisingEvents = true;
StoreWatcher.Changed += (object sender, FileSystemEventArgs e) =>
{
UpdateRepositoryObjects(e);
};
}
}
}
}
private static void UpdateRepositoryObjects(FileSystemEventArgs e)
{
// Extract job run information from change file path.
string updateDefinitionName;
DateTime updateJobRun;
if (!GetJobRunInfo(e.Name, out updateDefinitionName, out updateJobRun))
{
System.Diagnostics.Debug.Assert(false, "All job run updates should have valid directory names.");
return;
}
// Find corresponding job in repository.
ScheduledJob updateJob = JobRepository.GetJob(updateDefinitionName, updateJobRun);
if (updateJob == null)
{
return;
}
// Load updated job information from store.
Job2 job = null;
try
{
job = LoadJobFromStore(updateDefinitionName, updateJobRun);
}
catch (ScheduledJobException)
{ }
catch (DirectoryNotFoundException)
{ }
catch (FileNotFoundException)
{ }
catch (UnauthorizedAccessException)
{ }
catch (IOException)
{ }
// Update job in repository based on new job store data.
if (job != null)
{
updateJob.Update(job as ScheduledJob);
}
}
/// <summary>
/// Parses job definition name and job run DateTime from provided path string.
/// Example:
/// path = "ScheduledJob1\\Output\\20111219-200921-369\\Results.xml"
/// 'ScheduledJob1' is the definition name.
/// '20111219-200921-369' is the jobRun DateTime.
/// </summary>
/// <param name="path"></param>
/// <param name="definitionName"></param>
/// <param name="jobRunReturn"></param>
/// <returns></returns>
private static bool GetJobRunInfo(
string path,
out string definitionName,
out DateTime jobRunReturn)
{
// Parse definition name from path.
string[] pathItems = path.Split(System.IO.Path.DirectorySeparatorChar);
if (pathItems.Length == 4)
{
definitionName = pathItems[0];
return ScheduledJobStore.ConvertJobRunNameToDateTime(pathItems[2], out jobRunReturn);
}
definitionName = null;
jobRunReturn = DateTime.MinValue;
return false;
}
internal static Collection<DateTime> GetJobRuns(string definitionName)
{
Collection<DateTime> jobRuns = null;
try
{
jobRuns = ScheduledJobStore.GetJobRunsForDefinition(definitionName);
}
catch (DirectoryNotFoundException)
{ }
catch (FileNotFoundException)
{ }
catch (UnauthorizedAccessException)
{ }
catch (IOException)
{ }
return jobRuns;
}
private void GetJobsBefore(
DateTime dateTime,
ref List<Job2> jobList)
{
foreach (var job in JobRepository.Jobs)
{
if (job.PSEndTime < dateTime &&
!jobList.Contains(job))
{
jobList.Add(job);
}
}
}
private void GetJobsAfter(
DateTime dateTime,
ref List<Job2> jobList)
{
foreach (var job in JobRepository.Jobs)
{
if (job.PSEndTime > dateTime &&
!jobList.Contains(job))
{
jobList.Add(job);
}
}
}
private void GetNewestJobs(
int maxNumber,
ref List<Job2> jobList)
{
List<Job2> allJobs = JobRepository.Jobs;
// Sort descending.
allJobs.Sort((firstJob, secondJob) =>
{
if (firstJob.PSEndTime > secondJob.PSEndTime)
{
return -1;
}
else if (firstJob.PSEndTime < secondJob.PSEndTime)
{
return 1;
}
else
{
return 0;
}
});
int count = 0;
foreach (var job in allJobs)
{
if (++count > maxNumber)
{
break;
}
if (!jobList.Contains(job))
{
jobList.Add(job);
}
}
}
#endregion
#region Private Repository Class
/// <summary>
/// Collection of Job2 objects.
/// </summary>
internal class ScheduledJobRepository
{
#region Private Members
private object _syncObject = new object();
private Dictionary<Guid, Job2> _jobs = new Dictionary<Guid, Job2>();
private Dictionary<string, DateTime> _latestJobRuns = new Dictionary<string, DateTime>();
#endregion
#region Public Properties
/// <summary>
/// Returns all job objects in the repository as a List.
/// </summary>
public List<Job2> Jobs
{
get
{
lock (_syncObject)
{
return new List<Job2>(_jobs.Values);
}
}
}
/// <summary>
/// Returns count of jobs in repository.
/// </summary>
public int Count
{
get
{
lock (_syncObject)
{
return _jobs.Count;
}
}
}
#endregion
#region Public Methods
/// <summary>
/// Add Job2 to repository.
/// </summary>
/// <param name="job">Job2 to add.</param>
public void Add(Job2 job)
{
if (job == null)
{
throw new PSArgumentNullException("job");
}
lock (_syncObject)
{
if (_jobs.ContainsKey(job.InstanceId))
{
string msg = StringUtil.Format(ScheduledJobErrorStrings.ScheduledJobAlreadyExistsInLocal, job.Name, job.InstanceId);
throw new ScheduledJobException(msg);
}
_jobs.Add(job.InstanceId, job);
}
}
/// <summary>
/// Add or replace passed in Job2 object to repository.
/// </summary>
/// <param name="job">Job2 to add.</param>
public void AddOrReplace(Job2 job)
{
if (job == null)
{
throw new PSArgumentNullException("job");
}
lock (_syncObject)
{
if (_jobs.ContainsKey(job.InstanceId))
{
_jobs.Remove(job.InstanceId);
}
_jobs.Add(job.InstanceId, job);
}
}
/// <summary>
/// Remove Job2 from repository.
/// </summary>
/// <param name="job"></param>
public void Remove(Job2 job)
{
if (job == null)
{
throw new PSArgumentNullException("job");
}
lock (_syncObject)
{
if (_jobs.ContainsKey(job.InstanceId) == false)
{
string msg = StringUtil.Format(ScheduledJobErrorStrings.ScheduledJobNotInRepository, job.Name);
throw new ScheduledJobException(msg);
}
_jobs.Remove(job.InstanceId);
}
}
/// <summary>
/// Clears all Job2 items from the repository.
/// </summary>
public void Clear()
{
lock (_syncObject)
{
_jobs.Clear();
}
}
/// <summary>
/// Gets the latest job run Date/Time for the given definition name.
/// </summary>
/// <param name="definitionName">ScheduledJobDefinition name.</param>
/// <returns>Job Run DateTime.</returns>
public DateTime GetLatestJobRun(string definitionName)
{
if (string.IsNullOrEmpty(definitionName))
{
throw new PSArgumentException("definitionName");
}
lock (_syncObject)
{
if (_latestJobRuns.ContainsKey(definitionName))
{
return _latestJobRuns[definitionName];
}
else
{
DateTime startJobRun = DateTime.MinValue;
_latestJobRuns.Add(definitionName, startJobRun);
return startJobRun;
}
}
}
/// <summary>
/// Sets the latest job run Date/Time for the given definition name.
/// </summary>
/// <param name="definitionName"></param>
/// <param name="jobRun"></param>
public void SetLatestJobRun(string definitionName, DateTime jobRun)
{
if (string.IsNullOrEmpty(definitionName))
{
throw new PSArgumentException("definitionName");
}
lock (_syncObject)
{
if (_latestJobRuns.ContainsKey(definitionName))
{
_latestJobRuns.Remove(definitionName);
_latestJobRuns.Add(definitionName, jobRun);
}
else
{
_latestJobRuns.Add(definitionName, jobRun);
}
}
}
/// <summary>
/// Search repository for specific job run.
/// </summary>
/// <param name="definitionName">Definition name.</param>
/// <param name="jobRun">Job run DateTime.</param>
/// <returns>Scheduled job if found.</returns>
public ScheduledJob GetJob(string definitionName, DateTime jobRun)
{
lock (_syncObject)
{
foreach (ScheduledJob job in _jobs.Values)
{
if (job.PSBeginTime == null)
{
continue;
}
DateTime PSBeginTime = job.PSBeginTime ?? DateTime.MinValue;
if (definitionName.Equals(job.Definition.Name, StringComparison.OrdinalIgnoreCase) &&
jobRun.Year == PSBeginTime.Year &&
jobRun.Month == PSBeginTime.Month &&
jobRun.Day == PSBeginTime.Day &&
jobRun.Hour == PSBeginTime.Hour &&
jobRun.Minute == PSBeginTime.Minute &&
jobRun.Second == PSBeginTime.Second &&
jobRun.Millisecond == PSBeginTime.Millisecond)
{
return job;
}
}
}
return null;
}
#endregion
}
#endregion
}
}