// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
using System.Runtime.Serialization;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.IO;
using System.ComponentModel;
using System.Security.Permissions;
using System.Management.Automation.Host;
namespace Microsoft.PowerShell.ScheduledJob
{
///
/// This is a Job2 derived class that contains a DefinitionJob for
/// running job definition based jobs but can also save and load job
/// results data from file. This class is used to load job result
/// data from previously run jobs so that a user can view results of
/// scheduled job runs. This class also contains the definition of
/// the scheduled job and so can run an instance of the scheduled
/// job and optionally save results to file.
///
[Serializable]
public sealed class ScheduledJob : Job2, ISerializable
{
#region Private Members
private ScheduledJobDefinition _jobDefinition;
private Runspace _runspace;
private System.Management.Automation.PowerShell _powerShell;
private Job _job = null;
private bool _asyncJobStop;
private bool _allowSetShouldExit;
private PSHost _host;
private const string AllowHostSetShouldExit = "AllowSetShouldExitFromRemote";
private StatusInfo _statusInfo;
#endregion
#region Public Properties
///
/// ScheduledJobDefinition.
///
public ScheduledJobDefinition Definition
{
get { return _jobDefinition; }
internal set { _jobDefinition = value; }
}
///
/// Location of job being run.
///
public override string Location
{
get
{
return Status.Location;
}
}
///
/// Status Message associated with the Job.
///
public override string StatusMessage
{
get
{
return Status.StatusMessage;
}
}
///
/// Indicates whether more data is available from Job.
///
public override bool HasMoreData
{
get
{
return (_job != null) ?
_job.HasMoreData
:
(Output.Count > 0 ||
Error.Count > 0 ||
Warning.Count > 0 ||
Verbose.Count > 0 ||
Progress.Count > 0 ||
Debug.Count > 0 ||
Information.Count > 0
);
}
}
///
/// Job command string.
///
public new string Command
{
get
{
return Status.Command;
}
}
///
/// Internal property indicating whether a SetShouldExit is honored
/// while running the scheduled job script.
///
internal bool AllowSetShouldExit
{
get { return _allowSetShouldExit; }
set { _allowSetShouldExit = value; }
}
#endregion
#region Constructors
///
/// Constructor.
///
/// Job command string for display.
/// Name of job.
/// ScheduledJobDefinition defining job to run.
public ScheduledJob(
string command,
string name,
ScheduledJobDefinition jobDefinition) :
base(command, name)
{
if (command == null)
{
throw new PSArgumentNullException("command");
}
if (name == null)
{
throw new PSArgumentNullException("name");
}
if (jobDefinition == null)
{
throw new PSArgumentNullException("jobDefinition");
}
_jobDefinition = jobDefinition;
PSJobTypeName = ScheduledJobSourceAdapter.AdapterTypeName;
}
#endregion
#region Public Overrides
///
/// Starts a job as defined by the contained ScheduledJobDefinition object.
///
public override void StartJob()
{
lock (SyncRoot)
{
if (_job != null && !IsFinishedState(_job.JobStateInfo.State))
{
string msg = StringUtil.Format(ScheduledJobErrorStrings.JobAlreadyRunning, _jobDefinition.Name);
throw new PSInvalidOperationException(msg);
}
_statusInfo = null;
_asyncJobStop = false;
PSBeginTime = DateTime.Now;
if (_powerShell == null)
{
InitialSessionState iss = InitialSessionState.CreateDefault2();
iss.Commands.Clear();
iss.Formats.Clear();
iss.Commands.Add(
new SessionStateCmdletEntry("Start-Job", typeof(Microsoft.PowerShell.Commands.StartJobCommand), null));
// Get the default host from the default runspace.
_host = GetDefaultHost();
_runspace = RunspaceFactory.CreateRunspace(_host, iss);
_runspace.Open();
_powerShell = System.Management.Automation.PowerShell.Create();
_powerShell.Runspace = _runspace;
// Indicate SetShouldExit to host.
AddSetShouldExitToHost();
}
else
{
_powerShell.Commands.Clear();
}
_job = StartJobCommand(_powerShell);
_job.StateChanged += new EventHandler(HandleJobStateChanged);
SetJobState(_job.JobStateInfo.State);
// Add all child jobs to this object's list so that
// the user and Receive-Job can retrieve results.
foreach (Job childJob in _job.ChildJobs)
{
this.ChildJobs.Add(childJob);
}
// Add this job to the local repository.
ScheduledJobSourceAdapter.AddToRepository(this);
}
}
///
/// Start job asynchronously.
///
public override void StartJobAsync()
{
//StartJob();
throw new PSNotSupportedException();
}
///
/// Stop the job.
///
public override void StopJob()
{
Job job;
JobState state;
lock (SyncRoot)
{
job = _job;
state = Status.State;
_asyncJobStop = false;
}
if (IsFinishedState(state))
{
return;
}
if (job == null)
{
// Set job state to failed so that it can be removed from the
// cache using Remove-Job.
SetJobState(JobState.Failed);
}
else
{
job.StopJob();
}
}
///
/// Stop the job asynchronously.
///
public override void StopJobAsync()
{
Job job;
JobState state;
lock (SyncRoot)
{
job = _job;
state = Status.State;
_asyncJobStop = true;
}
if (IsFinishedState(state))
{
return;
}
if (job == null)
{
// Set job state to failed so that it can be removed from the
// cache using Remove-Job.
SetJobState(JobState.Failed);
HandleJobStateChanged(this,
new JobStateEventArgs(
new JobStateInfo(JobState.Failed)));
}
else
{
job.StopJob();
}
}
///
/// SuspendJob.
///
public override void SuspendJob()
{
throw new PSNotSupportedException();
}
///
/// SuspendJobAsync.
///
public override void SuspendJobAsync()
{
throw new PSNotSupportedException();
}
///
/// ResumeJob.
///
public override void ResumeJob()
{
throw new PSNotSupportedException();
}
///
/// ResumeJobAsync.
///
public override void ResumeJobAsync()
{
throw new PSNotSupportedException();
}
///
/// UnblockJob.
///
public override void UnblockJob()
{
throw new PSNotSupportedException();
}
///
/// UnblockJobAsync.
///
public override void UnblockJobAsync()
{
throw new PSNotSupportedException();
}
///
/// StopJob.
///
///
///
public override void StopJob(bool force, string reason)
{
throw new PSNotSupportedException();
}
///
/// StopJobAsync.
///
///
///
public override void StopJobAsync(bool force, string reason)
{
throw new PSNotSupportedException();
}
///
/// SuspendJob.
///
///
///
public override void SuspendJob(bool force, string reason)
{
throw new PSNotSupportedException();
}
///
/// SuspendJobAsync.
///
///
///
public override void SuspendJobAsync(bool force, string reason)
{
throw new PSNotSupportedException();
}
#endregion
#region Implementation of ISerializable
///
/// Deserialize constructor.
///
/// SerializationInfo.
/// StreamingContext.
[SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)]
private ScheduledJob(
SerializationInfo info,
StreamingContext context)
{
if (info == null)
{
throw new PSArgumentNullException("info");
}
DeserializeStatusInfo(info);
DeserializeResultsInfo(info);
PSJobTypeName = ScheduledJobSourceAdapter.AdapterTypeName;
}
///
/// Serialize method.
///
/// SerializationInfo.
/// StreamingContext.
[SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)]
public void GetObjectData(
SerializationInfo info,
StreamingContext context)
{
if (info == null)
{
throw new PSArgumentException("info");
}
SerializeStatusInfo(info);
SerializeResultsInfo(info);
}
private void SerializeStatusInfo(SerializationInfo info)
{
StatusInfo statusInfo = new StatusInfo(
InstanceId,
Name,
Location,
Command,
StatusMessage,
(_job != null) ? _job.JobStateInfo.State : JobStateInfo.State,
HasMoreData,
PSBeginTime,
PSEndTime,
_jobDefinition);
info.AddValue("StatusInfo", statusInfo);
}
private void SerializeResultsInfo(SerializationInfo info)
{
// All other job information is in the child jobs.
Collection output = new Collection();
Collection error = new Collection();
Collection warning = new Collection();
Collection verbose = new Collection();
Collection progress = new Collection();
Collection debug = new Collection();
Collection information = new Collection();
if (_job != null)
{
// Collect data from "live" job.
if (JobStateInfo.Reason != null)
{
error.Add(new ErrorRecord(JobStateInfo.Reason, "ScheduledJobFailedState", ErrorCategory.InvalidResult, null));
}
foreach (var item in _job.Error)
{
error.Add(item);
}
foreach (Job childJob in ChildJobs)
{
if (childJob.JobStateInfo.Reason != null)
{
error.Add(new ErrorRecord(childJob.JobStateInfo.Reason, "ScheduledJobFailedState", ErrorCategory.InvalidResult, null));
}
foreach (var item in childJob.Output)
{
output.Add(item);
}
foreach (var item in childJob.Error)
{
error.Add(item);
}
foreach (var item in childJob.Warning)
{
warning.Add(item);
}
foreach (var item in childJob.Verbose)
{
verbose.Add(item);
}
foreach (var item in childJob.Progress)
{
progress.Add(item);
}
foreach (var item in childJob.Debug)
{
debug.Add(item);
}
foreach (var item in childJob.Information)
{
information.Add(item);
}
}
}
else
{
// Collect data from object collections.
foreach (var item in Output)
{
// Wrap the base object in a new PSObject. This is necessary because the
// source deserialized PSObject doesn't serialize again correctly and breaks
// PS F&O. Not sure if this is a PSObject serialization bug or not.
output.Add(new PSObject(item.BaseObject));
}
foreach (var item in Error)
{
error.Add(item);
}
foreach (var item in Warning)
{
warning.Add(item);
}
foreach (var item in Verbose)
{
verbose.Add(item);
}
foreach (var item in Progress)
{
progress.Add(item);
}
foreach (var item in Debug)
{
debug.Add(item);
}
foreach (var item in Information)
{
information.Add(item);
}
}
ResultsInfo resultsInfo = new ResultsInfo(
output, error, warning, verbose, progress, debug, information);
info.AddValue("ResultsInfo", resultsInfo);
}
private void DeserializeStatusInfo(SerializationInfo info)
{
StatusInfo statusInfo = (StatusInfo)info.GetValue("StatusInfo", typeof(StatusInfo));
Name = statusInfo.Name;
PSBeginTime = statusInfo.StartTime;
PSEndTime = statusInfo.StopTime;
_jobDefinition = statusInfo.Definition;
SetJobState(statusInfo.State, null);
lock (SyncRoot)
{
_statusInfo = statusInfo;
}
}
private void DeserializeResultsInfo(SerializationInfo info)
{
ResultsInfo resultsInfo = (ResultsInfo)info.GetValue("ResultsInfo", typeof(ResultsInfo));
// Output
CopyOutput(resultsInfo.Output);
// Error
CopyError(resultsInfo.Error);
// Warning
CopyWarning(resultsInfo.Warning);
// Verbose
CopyVerbose(resultsInfo.Verbose);
// Progress
CopyProgress(resultsInfo.Progress);
// Debug
CopyDebug(resultsInfo.Debug);
// Information
CopyInformation(resultsInfo.Information);
}
#endregion
#region Internal Methods
///
/// Method to update a ScheduledJob based on new state and
/// result data from a provided Job.
///
/// ScheduledJob to update from.
internal void Update(ScheduledJob fromJob)
{
// We do not update "live" jobs.
if (_job != null || fromJob == null)
{
return;
}
//
// Update status.
//
PSEndTime = fromJob.PSEndTime;
JobState state = fromJob.JobStateInfo.State;
if (Status.State != state)
{
SetJobState(state, null);
}
lock (SyncRoot)
{
_statusInfo = new StatusInfo(
fromJob.InstanceId,
fromJob.Name,
fromJob.Location,
fromJob.Command,
fromJob.StatusMessage,
state,
fromJob.HasMoreData,
fromJob.PSBeginTime,
fromJob.PSEndTime,
fromJob._jobDefinition);
}
//
// Update results.
//
CopyOutput(fromJob.Output);
CopyError(fromJob.Error);
CopyWarning(fromJob.Warning);
CopyVerbose(fromJob.Verbose);
CopyProgress(fromJob.Progress);
CopyDebug(fromJob.Debug);
CopyInformation(fromJob.Information);
}
#endregion
#region Private Methods
private System.Management.Automation.Host.PSHost GetDefaultHost()
{
System.Management.Automation.PowerShell ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace).AddScript("$host");
Collection hosts = ps.Invoke();
if (hosts == null || hosts.Count == 0)
{
System.Diagnostics.Debug.Assert(false, "Current runspace should always return default host.");
return null;
}
return hosts[0];
}
private Job StartJobCommand(System.Management.Automation.PowerShell powerShell)
{
Job job = null;
// Use PowerShell Start-Job cmdlet to run job.
powerShell.AddCommand("Start-Job");
powerShell.AddParameter("Name", _jobDefinition.Name);
// Add job parameters from the JobInvocationInfo object.
CommandParameterCollection parameters = _jobDefinition.InvocationInfo.Parameters[0];
foreach (CommandParameter parameter in parameters)
{
switch (parameter.Name)
{
case "ScriptBlock":
powerShell.AddParameter("ScriptBlock", parameter.Value as ScriptBlock);
break;
case "FilePath":
powerShell.AddParameter("FilePath", parameter.Value as string);
break;
case "RunAs32":
powerShell.AddParameter("RunAs32", (bool)parameter.Value);
break;
case "Authentication":
powerShell.AddParameter("Authentication", (AuthenticationMechanism)parameter.Value);
break;
case "InitializationScript":
powerShell.AddParameter("InitializationScript", parameter.Value as ScriptBlock);
break;
case "ArgumentList":
powerShell.AddParameter("ArgumentList", parameter.Value as object[]);
break;
}
}
// Start the job.
Collection rtn = powerShell.Invoke();
if (rtn != null && rtn.Count == 1)
{
job = rtn[0].BaseObject as Job;
}
return job;
}
private void HandleJobStateChanged(object sender, JobStateEventArgs e)
{
SetJobState(e.JobStateInfo.State);
if (IsFinishedState(e.JobStateInfo.State))
{
PSEndTime = DateTime.Now;
// Dispose the PowerShell and Runspace objects.
System.Management.Automation.PowerShell disposePowerShell = null;
Runspace disposeRunspace = null;
lock (SyncRoot)
{
if (_job != null &&
IsFinishedState(_job.JobStateInfo.State))
{
disposePowerShell = _powerShell;
_powerShell = null;
disposeRunspace = _runspace;
_runspace = null;
}
}
if (disposePowerShell != null)
{
disposePowerShell.Dispose();
}
if (disposeRunspace != null)
{
disposeRunspace.Dispose();
}
// Raise async job stopped event, if needed.
if (_asyncJobStop)
{
_asyncJobStop = false;
OnStopJobCompleted(new AsyncCompletedEventArgs(null, false, null));
}
// Remove AllowSetShouldExit from host.
RemoveSetShouldExitFromHost();
}
}
internal bool IsFinishedState(JobState state)
{
return (state == JobState.Completed || state == JobState.Failed || state == JobState.Stopped);
}
private StatusInfo Status
{
get
{
StatusInfo statusInfo;
lock (SyncRoot)
{
if (_statusInfo != null)
{
// Pass back static status.
statusInfo = _statusInfo;
}
else if (_job != null)
{
// Create current job status.
statusInfo = new StatusInfo(
_job.InstanceId,
_job.Name,
_job.Location,
_job.Command,
_job.StatusMessage,
_job.JobStateInfo.State,
_job.HasMoreData,
PSBeginTime,
PSEndTime,
_jobDefinition);
}
else
{
// Create default static empty status.
_statusInfo = new StatusInfo(
Guid.Empty,
string.Empty,
string.Empty,
string.Empty,
string.Empty,
JobState.NotStarted,
false,
PSBeginTime,
PSEndTime,
_jobDefinition);
statusInfo = _statusInfo;
}
}
return statusInfo;
}
}
private void CopyOutput(ICollection fromOutput)
{
PSDataCollection output = CopyResults(fromOutput);
if (output != null)
{
try
{
Output = output;
}
catch (InvalidJobStateException) { }
}
}
private void CopyError(ICollection fromError)
{
PSDataCollection error = CopyResults(fromError);
if (error != null)
{
try
{
Error = error;
}
catch (InvalidJobStateException) { }
}
}
private void CopyWarning(ICollection fromWarning)
{
PSDataCollection warning = CopyResults(fromWarning);
if (warning != null)
{
try
{
Warning = warning;
}
catch (InvalidJobStateException) { }
}
}
private void CopyVerbose(ICollection fromVerbose)
{
PSDataCollection verbose = CopyResults(fromVerbose);
if (verbose != null)
{
try
{
Verbose = verbose;
}
catch (InvalidJobStateException) { }
}
}
private void CopyProgress(ICollection fromProgress)
{
PSDataCollection progress = CopyResults(fromProgress);
if (progress != null)
{
try
{
Progress = progress;
}
catch (InvalidJobStateException) { }
}
}
private void CopyDebug(ICollection fromDebug)
{
PSDataCollection debug = CopyResults(fromDebug);
if (debug != null)
{
try
{
Debug = debug;
}
catch (InvalidJobStateException) { }
}
}
private void CopyInformation(ICollection fromInformation)
{
PSDataCollection information = CopyResults(fromInformation);
if (information != null)
{
try
{
Information = information;
}
catch (InvalidJobStateException) { }
}
}
private PSDataCollection CopyResults(ICollection fromResults)
{
if (fromResults != null && fromResults.Count > 0)
{
PSDataCollection returnResults = new PSDataCollection();
foreach (var item in fromResults)
{
returnResults.Add(item);
}
return returnResults;
}
return null;
}
private void AddSetShouldExitToHost()
{
if (!_allowSetShouldExit || _host == null) { return; }
PSObject hostPrivateData = _host.PrivateData as PSObject;
if (hostPrivateData != null)
{
// Adds or replaces.
hostPrivateData.Properties.Add(new PSNoteProperty(AllowHostSetShouldExit, true));
}
}
private void RemoveSetShouldExitFromHost()
{
if (!_allowSetShouldExit || _host == null) { return; }
PSObject hostPrivateData = _host.PrivateData as PSObject;
if (hostPrivateData != null)
{
// Removes if exists.
hostPrivateData.Properties.Remove(AllowHostSetShouldExit);
}
}
#endregion
#region Private ResultsInfo class
[Serializable]
private class ResultsInfo : ISerializable
{
// Private Members
private Collection _output;
private Collection _error;
private Collection _warning;
private Collection _verbose;
private Collection _progress;
private Collection _debug;
private Collection _information;
// Properties
internal Collection Output
{
get { return _output; }
}
internal Collection Error
{
get { return _error; }
}
internal Collection Warning
{
get { return _warning; }
}
internal Collection Verbose
{
get { return _verbose; }
}
internal Collection Progress
{
get { return _progress; }
}
internal Collection Debug
{
get { return _debug; }
}
internal Collection Information
{
get { return _information; }
}
// Constructors
internal ResultsInfo(
Collection output,
Collection error,
Collection warning,
Collection verbose,
Collection progress,
Collection debug,
Collection information
)
{
if (output == null)
{
throw new PSArgumentNullException("output");
}
if (error == null)
{
throw new PSArgumentNullException("error");
}
if (warning == null)
{
throw new PSArgumentNullException("warning");
}
if (verbose == null)
{
throw new PSArgumentNullException("verbose");
}
if (progress == null)
{
throw new PSArgumentNullException("progress");
}
if (debug == null)
{
throw new PSArgumentNullException("debug");
}
if (information == null)
{
throw new PSArgumentNullException("information");
}
_output = output;
_error = error;
_warning = warning;
_verbose = verbose;
_progress = progress;
_debug = debug;
_information = information;
}
// ISerializable
[SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)]
private ResultsInfo(
SerializationInfo info,
StreamingContext context)
{
if (info == null)
{
throw new PSArgumentNullException("info");
}
_output = (Collection)info.GetValue("Results_Output", typeof(Collection));
_error = (Collection)info.GetValue("Results_Error", typeof(Collection));
_warning = (Collection)info.GetValue("Results_Warning", typeof(Collection));
_verbose = (Collection)info.GetValue("Results_Verbose", typeof(Collection));
_progress = (Collection)info.GetValue("Results_Progress", typeof(Collection));
_debug = (Collection)info.GetValue("Results_Debug", typeof(Collection));
try
{
_information = (Collection)info.GetValue("Results_Information", typeof(Collection));
}
catch(SerializationException)
{
// The job might not have the info stream. Ignore.
_information = new Collection();
}
}
[SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)]
public void GetObjectData(
SerializationInfo info,
StreamingContext context)
{
if (info == null)
{
throw new PSArgumentException("info");
}
info.AddValue("Results_Output", _output);
info.AddValue("Results_Error", _error);
info.AddValue("Results_Warning", _warning);
info.AddValue("Results_Verbose", _verbose);
info.AddValue("Results_Progress", _progress);
info.AddValue("Results_Debug", _debug);
info.AddValue("Results_Information", _information);
}
}
#endregion
}
#region Internal StatusInfo Class
[Serializable]
internal class StatusInfo : ISerializable
{
// Private Members
private Guid _instanceId;
private string _name;
private string _location;
private string _command;
private string _statusMessage;
private JobState _jobState;
private bool _hasMoreData;
private DateTime? _startTime;
private DateTime? _stopTime;
private ScheduledJobDefinition _definition;
// Properties
internal Guid InstanceId
{
get { return _instanceId; }
}
internal string Name
{
get { return _name; }
}
internal string Location
{
get { return _location; }
}
internal string Command
{
get { return _command; }
}
internal string StatusMessage
{
get { return _statusMessage; }
}
internal JobState State
{
get { return _jobState; }
}
internal bool HasMoreData
{
get { return _hasMoreData; }
}
internal DateTime? StartTime
{
get { return _startTime; }
}
internal DateTime? StopTime
{
get { return _stopTime; }
}
internal ScheduledJobDefinition Definition
{
get { return _definition; }
}
// Constructors
internal StatusInfo(
Guid instanceId,
string name,
string location,
string command,
string statusMessage,
JobState jobState,
bool hasMoreData,
DateTime? startTime,
DateTime? stopTime,
ScheduledJobDefinition definition)
{
if (definition == null)
{
throw new PSArgumentNullException("definition");
}
_instanceId = instanceId;
_name = name;
_location = location;
_command = command;
_statusMessage = statusMessage;
_jobState = jobState;
_hasMoreData = hasMoreData;
_startTime = startTime;
_stopTime = stopTime;
_definition = definition;
}
// ISerializable
[SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)]
private StatusInfo(
SerializationInfo info,
StreamingContext context)
{
if (info == null)
{
throw new PSArgumentNullException("info");
}
_instanceId = Guid.Parse(info.GetString("Status_InstanceId"));
_name = info.GetString("Status_Name");
_location = info.GetString("Status_Location");
_command = info.GetString("Status_Command");
_statusMessage = info.GetString("Status_Message");
_jobState = (JobState)info.GetValue("Status_State", typeof(JobState));
_hasMoreData = info.GetBoolean("Status_MoreData");
_definition = (ScheduledJobDefinition)info.GetValue("Status_Definition", typeof(ScheduledJobDefinition));
DateTime startTime = info.GetDateTime("Status_StartTime");
if (startTime != DateTime.MinValue)
{
_startTime = startTime;
}
else
{
_startTime = null;
}
DateTime stopTime = info.GetDateTime("Status_StopTime");
if (stopTime != DateTime.MinValue)
{
_stopTime = stopTime;
}
else
{
_stopTime = null;
}
}
[SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)]
public void GetObjectData(
SerializationInfo info,
StreamingContext context)
{
if (info == null)
{
throw new PSArgumentNullException("info");
}
info.AddValue("Status_InstanceId", _instanceId);
info.AddValue("Status_Name", _name);
info.AddValue("Status_Location", _location);
info.AddValue("Status_Command", _command);
info.AddValue("Status_Message", _statusMessage);
info.AddValue("Status_State", _jobState);
info.AddValue("Status_MoreData", _hasMoreData);
info.AddValue("Status_Definition", _definition);
if (_startTime != null)
{
info.AddValue("Status_StartTime", _startTime);
}
else
{
info.AddValue("Status_StartTime", DateTime.MinValue);
}
if (_stopTime != null)
{
info.AddValue("Status_StopTime", _stopTime);
}
else
{
info.AddValue("Status_StopTime", DateTime.MinValue);
}
}
}
#endregion
}