// 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 }