using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using RoboSharp.Interfaces;
using RoboSharp.EventArgObjects;
namespace RoboSharp
{
///
/// Wrapper for the RoboCopy process
///
///
///
///
public class RoboCommand : IDisposable, IRoboCommand, ICloneable
{
///
/// The base object provided by the RoboSharp library.
///
public static RoboCommandFactory Factory { get; } = new RoboCommandFactory();
#region < Constructors >
/// Create a new RoboCommand object
public RoboCommand()
{
InitClassProperties();
Init();
}
///
/// Create a new RoboCommand object with the provided settings.
///
///
///
///
public RoboCommand(string source, string destination, CopyOptions.CopyActionFlags copyActionFlags, SelectionOptions.SelectionFlags selectionFlags = SelectionOptions.SelectionFlags.Default)
{
InitClassProperties();
Init("", true, source, destination);
this.copyOptions.ApplyActionFlags(copyActionFlags);
this.selectionOptions.ApplySelectionFlags(selectionFlags);
}
///
public RoboCommand(string name, bool stopIfDisposing = true)
{
InitClassProperties();
Init(name, stopIfDisposing);
}
///
public RoboCommand(string source, string destination, bool stopIfDisposing = true)
{
InitClassProperties();
Init("", stopIfDisposing, source, destination);
}
///
public RoboCommand(string source, string destination, string name, bool stopIfDisposing = true)
{
InitClassProperties();
Init(name, stopIfDisposing, source, destination);
}
/// Each of the Options objects can be specified within this constructor. If left = null, a new object will be generated using the default options for that object.
///
public RoboCommand(string name, string source = null, string destination = null, bool StopIfDisposing = true, RoboSharpConfiguration configuration = null, CopyOptions copyOptions = null, SelectionOptions selectionOptions = null, RetryOptions retryOptions = null, LoggingOptions loggingOptions = null, JobOptions jobOptions = null)
{
this.configuration = configuration ?? new RoboSharpConfiguration();
this.copyOptions = copyOptions ?? new CopyOptions();
this.selectionOptions = selectionOptions ?? new SelectionOptions();
this.retryOptions = retryOptions ?? new RetryOptions();
this.loggingOptions = loggingOptions ?? new LoggingOptions();
this.jobOptions = jobOptions ?? new JobOptions();
Init(name, StopIfDisposing, source ?? CopyOptions.Source, destination ?? CopyOptions.Destination);
}
///
/// Create a new RoboCommand with identical options at this RoboCommand
///
///
/// If Desired, the new RoboCommand object will share some of the same Property objects as the input .
/// For Example, that means that if a SelectionOption property changes, it will affect both RoboCommand objects since the property is shared between them.
/// If the Link* options are set to FALSE (default), then it will create new property objects whose settings match the current settings of .
/// Properties that can be linked:
/// ( Linked by default )
/// ( Linked by default )
///
///
///
///
/// RoboCommand to Clone
/// Specify a new source if desired. If left as null, will use Source from
/// Specify a new source if desired. If left as null, will use Destination from
/// Link the of the two commands ( True Default )
/// Link the of the two commands
/// Link the of the two commands ( True Default )
/// Link the of the two commands
/// Link the of the two commands
public RoboCommand(RoboCommand command, string NewSource = null, string NewDestination = null, bool LinkConfiguration = true, bool LinkRetryOptions = true, bool LinkSelectionOptions = false, bool LinkLoggingOptions = false, bool LinkJobOptions = false)
{
Name = command.Name;
StopIfDisposing = command.StopIfDisposing;
configuration = LinkConfiguration ? command.configuration : command.configuration.Clone();
copyOptions = new CopyOptions(command.CopyOptions, NewSource, NewDestination);
JobOptions = LinkJobOptions ? command.jobOptions : command.jobOptions.Clone();
loggingOptions = LinkLoggingOptions ? command.loggingOptions : command.loggingOptions.Clone();
retryOptions = LinkRetryOptions ? command.retryOptions : command.retryOptions.Clone();
selectionOptions = LinkSelectionOptions ? command.selectionOptions : command.SelectionOptions.Clone();
}
/// Create a new RoboCommand object
///
///
///
///
private void Init(string name = "", bool stopIfDisposing = true, string source = "", string destination = "")
{
Name = name;
StopIfDisposing = stopIfDisposing;
CopyOptions.Source = source;
CopyOptions.Destination = destination;
}
private void InitClassProperties()
{
copyOptions = new CopyOptions();
selectionOptions = new SelectionOptions();
retryOptions = new RetryOptions();
loggingOptions = new LoggingOptions();
configuration = new RoboSharpConfiguration();
jobOptions = new JobOptions();
}
///
public RoboCommand Clone(string NewSource = null, string NewDestination = null, bool LinkConfiguration = true, bool LinkRetryOptions = true, bool LinkSelectionOptions = false, bool LinkLoggingOptions = false, bool LinkJobOptions = false)
=> new RoboCommand(this, NewSource, NewDestination, LinkConfiguration, LinkRetryOptions, LinkSelectionOptions, LinkLoggingOptions, LinkJobOptions);
object ICloneable.Clone() => new RoboCommand(this, null, null, false, false, false, false, false);
#endregion
#region < Private Vars >
// set up in Constructor
private CopyOptions copyOptions;
private SelectionOptions selectionOptions;
private RetryOptions retryOptions;
private LoggingOptions loggingOptions;
private RoboSharpConfiguration configuration;
private JobOptions jobOptions;
// Modified while running
private Process process;
private Task backupTask;
private bool hasError;
//private bool hasExited; //No longer evaluated
private bool isPaused;
private bool isRunning;
private bool isCancelled;
private Results.ResultsBuilder resultsBuilder;
private Results.RoboCopyResults results;
/// Stores the LastData processed by
private string LastDataReceived = "";
#endregion Private Vars
#region < Public Vars >
/// ID Tag for the job - Allows consumers to find/sort/remove/etc commands within a list via string comparison
public string Name { get; set; }
/// Value indicating if process is currently paused
public bool IsPaused { get { return isPaused; } }
/// Value indicating if process is currently running
public bool IsRunning { get { return isRunning; } }
/// Value indicating if process was Cancelled
public bool IsCancelled { get { return isCancelled; } }
/// TRUE if is set up (Copy Operation is scheduled to only operate within specified timeframe). Otherwise False.
public bool IsScheduled { get => !String.IsNullOrWhiteSpace(CopyOptions.RunHours); }
/// Get the parameters string passed to RoboCopy based on the current setup
public string CommandOptions { get { return GenerateParameters(); } }
///
public CopyOptions CopyOptions
{
get { return copyOptions; }
set { copyOptions = value ?? copyOptions; }
}
///
public SelectionOptions SelectionOptions
{
get { return selectionOptions; }
set { selectionOptions = value ?? selectionOptions; }
}
///
public RetryOptions RetryOptions
{
get { return retryOptions; }
set { retryOptions = value ?? retryOptions; }
}
///
public LoggingOptions LoggingOptions
{
get { return loggingOptions; }
set { loggingOptions = value ?? loggingOptions; }
}
///
public JobOptions JobOptions
{
get { return jobOptions; }
set { jobOptions = value ?? jobOptions; }
}
///
public RoboSharpConfiguration Configuration
{
get { return configuration; }
}
///
///
/// A new object is created every time the method is called, but will not be created until called for the first time.
///
internal Results.ProgressEstimator ProgressEstimator { get; private set; }
///
public IProgressEstimator IProgressEstimator => this.ProgressEstimator;
///
/// Value indicating if the process should be killed when the method is called.
/// For example, if the RoboCopy process should exit when the program exits, this should be set to TRUE (default).
///
public bool StopIfDisposing { get; set; } = true;
#endregion Public Vars
#region < Events >
/// Handles
public delegate void FileProcessedHandler(IRoboCommand sender, FileProcessedEventArgs e);
/// Occurs each time a new item has started processing
public event FileProcessedHandler OnFileProcessed;
/// Handles
public delegate void CommandErrorHandler(IRoboCommand sender, CommandErrorEventArgs e);
/// Occurs when an error occurs while generating the command that prevents the RoboCopy process from starting.
public event CommandErrorHandler OnCommandError;
/// Handles
public delegate void ErrorHandler(IRoboCommand sender, ErrorEventArgs e);
/// Occurs an error is detected by RoboCopy
public event ErrorHandler OnError;
/// Handles
public delegate void CommandCompletedHandler(IRoboCommand sender, RoboCommandCompletedEventArgs e);
/// Occurs when the RoboCopy process has finished executing and results are available.
public event CommandCompletedHandler OnCommandCompleted;
/// Handles
public delegate void CopyProgressHandler(IRoboCommand sender, CopyProgressEventArgs e);
/// Occurs each time the current item's progress is updated
public event CopyProgressHandler OnCopyProgressChanged;
/// Handles
public delegate void ProgressUpdaterCreatedHandler(IRoboCommand sender, ProgressEstimatorCreatedEventArgs e);
///
/// Occurs when a is created during , allowing binding to occur within the event subscriber.
/// This event will occur once per Start.
///
public event ProgressUpdaterCreatedHandler OnProgressEstimatorCreated;
///
/// Occurs if the RoboCommand task is stopped due to an unhandled exception. Occurs instead of
///
public event UnhandledExceptionEventHandler TaskFaulted;
#endregion
#region < Pause / Stop / Resume >
/// Pause execution of the RoboCopy process when == false
public virtual void Pause()
{
if (process != null && !process.HasExited && isPaused == false)
{
Debugger.Instance.DebugMessage("RoboCommand execution paused.");
isPaused = process.Suspend();
}
}
/// Resume execution of the RoboCopy process when == true
public virtual void Resume()
{
if (process != null && !process.HasExited && isPaused == true)
{
Debugger.Instance.DebugMessage("RoboCommand execution resumed.");
process.Resume();
isPaused = false;
}
}
/// Immediately Kill the RoboCopy process
public virtual void Stop() => Stop(false);
private void Stop(bool DisposeProcess)
{
//Note: This previously checked for CopyOptions.RunHours.IsNullOrWhiteSpace() == TRUE prior to issuing the stop command
//If the removal of that check broke your application, please create a new issue thread on the repo.
if (process != null)
{
if (!isCancelled && (!process?.HasExited ?? true))
{
process?.Kill();
isCancelled = true;
}
//hasExited = true;
if (DisposeProcess)
{
process?.Dispose();
process = null;
}
}
isPaused = false;
}
#endregion
#region < Start Methods >
#if NET45_OR_GREATER || NETSTANDARD2_0_OR_GREATER || NETCOREAPP3_1_OR_GREATER
///
/// awaits then returns the results.
///
/// Returns the RoboCopy results once RoboCopy has finished executing.
///
public virtual async Task StartAsync(string domain = "", string username = "", string password = "")
{
await Start(domain, username, password);
return GetResults();
}
/// awaits then returns the results.
/// Returns the List-Only results once RoboCopy has finished executing.
///
public virtual async Task StartAsync_ListOnly(string domain = "", string username = "", string password = "")
{
await Start_ListOnly(domain, username, password);
return GetResults();
}
#endif
///
/// Run the currently selected options in ListOnly mode by setting = TRUE
///
/// Task that awaits , then resets the ListOnly option to original value.
///
public virtual async Task Start_ListOnly(string domain = "", string username = "", string password = "")
{
bool _listOnly = LoggingOptions.ListOnly;
LoggingOptions.ListOnly = true;
await Start(domain, username, password);
LoggingOptions.ListOnly = _listOnly;
return;
}
///
/// Start the RoboCopy Process.
///
///
/// If overridden by a derived class, the override affects all Start* methods within RoboCommand. Base.Start() must be called to start the robocopy process.
///
///
///
///
/// Returns a task that reports when the RoboCopy process has finished executing.
///
public virtual Task Start(string domain = "", string username = "", string password = "")
{
if (process != null | IsRunning) throw new InvalidOperationException("RoboCommand.Start() method cannot be called while process is already running / IsRunning = true.");
Debugger.Instance.DebugMessage("RoboCommand started execution.");
hasError = false;
isCancelled = false;
isPaused = false;
isRunning = true;
resultsBuilder = new Results.ResultsBuilder(this);
results = null;
#region Check Source and Destination
#if NET40_OR_GREATER
// Authenticate on Target Server -- Create user if username is provided, else null
ImpersonatedUser impersonation = username.IsNullOrWhiteSpace() ? null : impersonation = new ImpersonatedUser(username, domain, password);
#endif
// make sure source path is valid
if (!Directory.Exists(CopyOptions.Source))
{
Debugger.Instance.DebugMessage("The Source directory does not exist.");
hasError = true;
OnCommandError?.Invoke(this, new CommandErrorEventArgs(new DirectoryNotFoundException("The Source directory does not exist.")));
Debugger.Instance.DebugMessage("RoboCommand execution stopped due to error.");
}
#region Create Destination Directory
//Check that the Destination Drive is accessible insteead [fixes #106]
try
{
//Check if the destination drive is accessible -> should not cause exception [Fix for #106]
DirectoryInfo dInfo = new DirectoryInfo(CopyOptions.Destination).Root;
if (!dInfo.Exists)
{
Debugger.Instance.DebugMessage("The destination drive does not exist.");
hasError = true;
OnCommandError?.Invoke(this, new CommandErrorEventArgs(new DirectoryNotFoundException("The Destination Drive is invalid.")));
Debugger.Instance.DebugMessage("RoboCommand execution stopped due to error.");
}
//If not list only, verify that drive has write access -> should cause exception if no write access [Fix #101]
if (!LoggingOptions.ListOnly & !hasError)
{
dInfo = Directory.CreateDirectory(CopyOptions.Destination);
if (!dInfo.Exists)
{
Debugger.Instance.DebugMessage("The destination directory does not exist.");
hasError = true;
OnCommandError?.Invoke(this, new CommandErrorEventArgs(new DirectoryNotFoundException("Unable to create Destination Folder. Check Write Access.")));
Debugger.Instance.DebugMessage("RoboCommand execution stopped due to error.");
}
}
}
catch (Exception ex)
{
Debugger.Instance.DebugMessage(ex.Message);
hasError = true;
OnCommandError?.Invoke(this, new CommandErrorEventArgs("The Destination directory is invalid.", ex));
Debugger.Instance.DebugMessage("RoboCommand execution stopped due to error.");
}
#endregion
#if NET40_OR_GREATER
//Dispose Authentification
impersonation?.Dispose();
#endif
#endregion
if (hasError)
{
isRunning = false;
return Task.Delay(5);
}
else
{
//Raise EstimatorCreatedEvent to alert consumers that the Estimator can now be bound to
ProgressEstimator = resultsBuilder.Estimator;
OnProgressEstimatorCreated?.Invoke(this, new ProgressEstimatorCreatedEventArgs(resultsBuilder.Estimator));
return GetRoboCopyTask(resultsBuilder, domain, username, password);
}
}
///
/// Start the RoboCopy process and the watcher task
///
/// The continuation task that cleans up after the task that watches RoboCopy has finished executing.
///
private Task GetRoboCopyTask(Results.ResultsBuilder resultsBuilder, string domain = "", string username = "", string password = "")
{
if (process != null) throw new InvalidOperationException("Cannot start a new RoboCopy Process while this RoboCommand is already running.");
isRunning = true;
DateTime StartTime = DateTime.Now;
backupTask = Task.Run( async () =>
{
process = new Process();
//Declare Encoding
process.StartInfo.StandardOutputEncoding = Configuration.StandardOutputEncoding;
process.StartInfo.StandardErrorEncoding = Configuration.StandardErrorEncoding;
if (!string.IsNullOrEmpty(domain))
{
Debugger.Instance.DebugMessage(string.Format("RoboCommand running under domain - {0}", domain));
process.StartInfo.Domain = domain;
}
if (!string.IsNullOrEmpty(username))
{
Debugger.Instance.DebugMessage(string.Format("RoboCommand running under username - {0}", username));
process.StartInfo.UserName = username;
}
if (!string.IsNullOrEmpty(password))
{
Debugger.Instance.DebugMessage("RoboCommand password entered.");
var ssPwd = new System.Security.SecureString();
for (int x = 0; x < password.Length; x++)
{
ssPwd.AppendChar(password[x]);
}
process.StartInfo.Password = ssPwd;
}
Debugger.Instance.DebugMessage("Setting RoboCopy process up...");
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
process.StartInfo.CreateNoWindow = true;
process.StartInfo.FileName = Configuration.RoboCopyExe;
if (resultsBuilder != null)
{
resultsBuilder.Source = CopyOptions.Source;
resultsBuilder.Destination = CopyOptions.Destination;
resultsBuilder.CommandOptions = GenerateParameters();
}
process.StartInfo.Arguments = resultsBuilder?.CommandOptions ?? GenerateParameters();
process.OutputDataReceived += process_OutputDataReceived;
process.ErrorDataReceived += process_ErrorDataReceived;
process.EnableRaisingEvents = true;
//Setup the WaitForExitAsync Task
//hasExited = false;
var ProcessExitedAsync = new TaskCompletionSource