/// <summary> /// Executes the monitor verb. This is the main method. /// </summary> /// <param name="verbOptions">The verb options.</param> /// <exception cref="DirectoryNotFoundException">Source Path ' + sourcePath + ' was not found.</exception> internal static void ExecuteMonitorVerb(MonitorVerbOptions verbOptions) { // Initialize Variables _isDeploying = false; _deploymentNumber = 1; // Normalize and show the options to the user so he knows what he's doing NormalizeMonitorVerbOptions(verbOptions); PrintMonitorOptions(verbOptions); // Create connection info var simpleConnectionInfo = new PasswordConnectionInfo( verbOptions.Host, verbOptions.Port, verbOptions.Username, verbOptions.Password); // Create a file watcher var watcher = new FileSystemWatcher { Path = verbOptions.SourcePath, NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName, Filter = Path.GetFileName(verbOptions.MonitorFile), }; // Validate source path exists if (Directory.Exists(verbOptions.SourcePath) == false) { throw new DirectoryNotFoundException($"Source Path \'{verbOptions.SourcePath}\' was not found."); } // Instantiate an SFTP client and an SSH client // SFTP will be used to transfer the files and SSH to execute pre-deployment and post-deployment commands using var sftpClient = new SftpClient(simpleConnectionInfo); // SSH will be used to execute commands and to get the output back from the program we are running using var sshClient = new SshClient(simpleConnectionInfo); // Connect SSH and SFTP clients EnsureMonitorConnection(sshClient, sftpClient, verbOptions); // Create the shell stream so we can get debugging info from the post-deployment command using var shellStream = CreateShellStream(sshClient); // Adds an onChange event and enables it watcher.Changed += (s, e) => CreateNewDeployment(sshClient, sftpClient, shellStream, verbOptions); watcher.EnableRaisingEvents = true; Terminal.WriteLine("File System Monitor is now running."); Terminal.WriteLine("Writing a new monitor file will trigger a new deployment."); Terminal.WriteLine("Press H for help!"); Terminal.WriteLine("Ground Control to Major Tom: Have a nice trip in space!.", ConsoleColor.DarkCyan); // Allows user interaction with the shell StartUserInteraction(sshClient, sftpClient, shellStream, verbOptions); // When we quit, we stop the monitor and disconnect the clients StopMonitorMode(sftpClient, sshClient, watcher); }
/// <summary> /// Starts the monitor mode. /// </summary> /// <param name="fsmonitor">The fs monitor.</param> /// <param name="sshClient">The SSH client.</param> /// <param name="sftpClient">The SFTP client.</param> /// <param name="shellStream">The shell stream.</param> /// <param name="verbOptions">The verb options.</param> private static void StartMonitorMode( FileSystemMonitor fsmonitor, SshClient sshClient, SftpClient sftpClient, ShellStream shellStream, MonitorVerbOptions verbOptions) { fsmonitor.FileSystemEntryChanged += (s, e) => { // Detect changes to the monitor file by ignoring deletions and checking file paths. if (e.ChangeType != FileSystemEntryChangeType.FileAdded && e.ChangeType != FileSystemEntryChangeType.FileModified) { return; } // If the change was not in the monitor file, then ignore it if (e.Path.ToLowerInvariant().Equals(verbOptions.MonitorFile.ToLowerInvariant()) == false) { return; } // Create a new deployment once CreateNewDeployment(sshClient, sftpClient, shellStream, verbOptions); }; Terminal.WriteLine("File System Monitor is now running."); Terminal.WriteLine("Writing a new monitor file will trigger a new deployment."); Terminal.WriteLine("Press H for help!"); Terminal.WriteLine("Ground Control to Major Tom: Have a nice trip in space!.", ConsoleColor.DarkCyan); }
/// <summary> /// Prepares the given target path for deployment. If clean target is false, it does nothing. /// </summary> /// <param name="sftpClient">The SFTP client.</param> /// <param name="targetPath">The target path.</param> /// <param name="cleanTarget">if set to <c>true</c> [clean target].</param> private static void PrepareTargetPath(SftpClient sftpClient, MonitorVerbOptions verbOptions) { if (verbOptions.CleanTarget == 0) { return; } ConsoleManager.WriteLine(" Cleaning Target Path '" + verbOptions.TargetPath + "'", ConsoleColor.Green); DeleteLinuxDirectoryRecursive(sftpClient, verbOptions.TargetPath); }
/// <summary> /// Creates the given directory structure on the target machine. /// </summary> /// <param name="sftpClient">The SFTP client.</param> /// <param name="targetPath">The target path.</param> private static void CreateTargetPath(SftpClient sftpClient, MonitorVerbOptions verbOptions) { if (sftpClient.Exists(verbOptions.TargetPath) == true) { return; } ConsoleManager.WriteLine(" Target Path '" + verbOptions.TargetPath + "' does not exist. -- Will attempt to create.", ConsoleColor.Green); CreateLinuxDirectoryRecursive(sftpClient, verbOptions.TargetPath); ConsoleManager.WriteLine(" Target Path '" + verbOptions.TargetPath + "' created successfully.", ConsoleColor.Green); }
/// <summary> /// Normalizes the monitor verb options. /// </summary> /// <param name="verbOptions">The verb options.</param> private static void NormalizeMonitorVerbOptions(MonitorVerbOptions verbOptions) { var sourcePath = verbOptions.SourcePath.Trim(); var targetPath = verbOptions.TargetPath.Trim(); var monitorFile = Path.IsPathRooted(verbOptions.MonitorFile) ? Path.GetFullPath(verbOptions.MonitorFile) : Path.Combine(sourcePath, verbOptions.MonitorFile); verbOptions.TargetPath = targetPath; verbOptions.MonitorFile = monitorFile; verbOptions.SourcePath = sourcePath; }
/// <summary> /// Creates a new deployment cycle. /// </summary> /// <param name="sshClient">The SSH client.</param> /// <param name="sftpClient">The SFTP client.</param> /// <param name="shellStream">The shell stream.</param> /// <param name="verbOptions">The verb options.</param> private static void CreateNewDeployment( SshClient sshClient, SftpClient sftpClient, ShellStream shellStream, MonitorVerbOptions verbOptions) { // At this point the change has been detected; Make sure we are not deploying string.Empty.WriteLine(); if (_isDeploying) { "WARNING: Deployment already in progress. Deployment will not occur." .WriteLine(ConsoleColor.DarkYellow); return; } // Lock Deployment _isDeploying = true; var stopwatch = new Stopwatch(); stopwatch.Start(); try { _forwardShellStreamOutput = false; PrintDeploymentNumber(_deploymentNumber); RunSshClientCommand(sshClient, verbOptions); CreateTargetPath(sftpClient, verbOptions); PrepareTargetPath(sftpClient, verbOptions); UploadFilesToTarget( sftpClient, verbOptions.SourcePath, verbOptions.TargetPath, verbOptions.ExcludeFileSuffixes); } catch (Exception ex) { PrintException(ex); } finally { // Unlock deployment _isDeploying = false; _deploymentNumber++; stopwatch.Stop(); $" Finished deployment in {Math.Round(stopwatch.Elapsed.TotalSeconds, 2)} seconds." .WriteLine(ConsoleColor.Green); _forwardShellStreamOutput = true; RunShellStreamCommand(shellStream, verbOptions); } }
/// <summary> /// Runs pre and post deployment commands over the SSH client /// </summary> /// <param name="sshClient">The SSH client.</param> /// <param name="commandText">The command text.</param> private static void RunShellStreamCommand(ShellStream shellStream, MonitorVerbOptions verbOptions) { var commandText = verbOptions.PostCommand; if (string.IsNullOrWhiteSpace(commandText) == true) { return; } ConsoleManager.WriteLine(" Executing shell command.", ConsoleColor.Green); shellStream.WriteLine(commandText); shellStream.Flush(); ConsoleManager.WriteLine(" TX: " + commandText, ConsoleColor.DarkYellow); }
/// <summary> /// Executes the monitor verb. Using a legacy method. /// </summary> /// <param name="verbOptions">The verb options.</param> /// <exception cref="DirectoryNotFoundException">Source Path ' + sourcePath + ' was not found.</exception> internal static void ExecuteMonitorVerbLegacy(MonitorVerbOptions verbOptions) { // Initialize Variables _isDeploying = false; _deploymentNumber = 1; // Normalize and show the options to the user so he knows what he's doing NormalizeMonitorVerbOptions(verbOptions); PrintMonitorOptions(verbOptions); // Create the FS Monitor and connection info var fsmonitor = new FileSystemMonitor(1, verbOptions.SourcePath); var simpleConnectionInfo = new PasswordConnectionInfo( verbOptions.Host, verbOptions.Port, verbOptions.Username, verbOptions.Password); // Validate source path exists if (Directory.Exists(verbOptions.SourcePath) == false) { throw new DirectoryNotFoundException("Source Path '" + verbOptions.SourcePath + "' was not found."); } // Instantiate an SFTP client and an SSH client // SFTP will be used to transfer the files and SSH to execute pre-deployment and post-deployment commands using (var sftpClient = new SftpClient(simpleConnectionInfo)) { // SSH will be used to execute commands and to get the output back from the program we are running using (var sshClient = new SshClient(simpleConnectionInfo)) { // Connect SSH and SFTP clients EnsureMonitorConnection(sshClient, sftpClient, verbOptions); // Create the shell stream so we can get debugging info from the post-deployment command using (var shellStream = CreateShellStream(sshClient)) { // Starts the FS Monitor and binds the event handler StartMonitorMode(fsmonitor, sshClient, sftpClient, shellStream, verbOptions); // Allows user interaction with the shell StartUserInteraction(sshClient, sftpClient, shellStream, verbOptions); // When we quit, we stop the monitor and disconnect the clients StopMonitorMode(sftpClient, sshClient, fsmonitor); } } } }
/// <summary> /// Prints the currently supplied monitor mode options. /// </summary> /// <param name="verbOptions">The verb options.</param> private static void PrintMonitorOptions(MonitorVerbOptions verbOptions) { Terminal.WriteLine(); Terminal.WriteLine("Monitor mode starting"); Terminal.WriteLine("Monitor parameters follow: "); Terminal.WriteLine($" Monitor File {verbOptions.MonitorFile}", ConsoleColor.DarkYellow); Terminal.WriteLine($" Source Path {verbOptions.SourcePath}", ConsoleColor.DarkYellow); Terminal.WriteLine($" Excluded Files {string.Join("|", verbOptions.ExcludeFileSuffixes)}", ConsoleColor.DarkYellow); Terminal.WriteLine($" Target Address {verbOptions.Host}:{verbOptions.Port}", ConsoleColor.DarkYellow); Terminal.WriteLine($" Username {verbOptions.Username}", ConsoleColor.DarkYellow); Terminal.WriteLine($" Target Path {verbOptions.TargetPath}", ConsoleColor.DarkYellow); Terminal.WriteLine($" Clean Target {(verbOptions.CleanTarget ? "YES" : "NO")}", ConsoleColor.DarkYellow); Terminal.WriteLine($" Pre Deployment {verbOptions.PreCommand}", ConsoleColor.DarkYellow); Terminal.WriteLine($" Post Deployment {verbOptions.PostCommand}", ConsoleColor.DarkYellow); }
/// <summary> /// Prints the currently supplied monitor mode options. /// </summary> /// <param name="verbOptions">The verb options.</param> /// <param name="monitorFile">The monitor file.</param> /// <param name="sourcePath">The source path.</param> /// <param name="targetPath">The target path.</param> private static void PrintMonitorOptions(MonitorVerbOptions verbOptions) { ConsoleManager.WriteLine(string.Empty); ConsoleManager.WriteLine("Monitor mode starting"); ConsoleManager.WriteLine("Monitor parameters follow: "); ConsoleManager.WriteLine(" Monitor File " + verbOptions.MonitorFile, ConsoleColor.DarkYellow); ConsoleManager.WriteLine(" Source Path " + verbOptions.SourcePath, ConsoleColor.DarkYellow); ConsoleManager.WriteLine(" Excluded Files " + verbOptions.ExcludeFileSuffixes, ConsoleColor.DarkYellow); ConsoleManager.WriteLine(" Target Address " + verbOptions.Host + ":" + verbOptions.Port.ToString(), ConsoleColor.DarkYellow); ConsoleManager.WriteLine(" Username " + verbOptions.Username, ConsoleColor.DarkYellow); ConsoleManager.WriteLine(" Target Path " + verbOptions.TargetPath, ConsoleColor.DarkYellow); ConsoleManager.WriteLine(" Clean Target " + (verbOptions.CleanTarget == 0 ? "NO" : "YES"), ConsoleColor.DarkYellow); ConsoleManager.WriteLine(" Pre Deployment " + verbOptions.PreCommand, ConsoleColor.DarkYellow); ConsoleManager.WriteLine(" Post Deployment " + verbOptions.PostCommand, ConsoleColor.DarkYellow); }
/// <summary> /// Runs the deployment command. /// </summary> /// <param name="sshClient">The SSH client.</param> /// <param name="commandText">The command text.</param> private static void RunSshClientCommand(SshClient sshClient, MonitorVerbOptions verbOptions) { var commandText = verbOptions.PreCommand; if (string.IsNullOrWhiteSpace(commandText) == true) { return; } ConsoleManager.WriteLine(" Executing SSH client command.", ConsoleColor.Green); var result = sshClient.RunCommand(commandText); ConsoleManager.WriteLine(string.Format(" SSH TX: {0}", commandText), ConsoleColor.DarkYellow); ConsoleManager.WriteLine(string.Format(" SSH RX: [{0}] ", result.ExitStatus, result.Result), ConsoleColor.DarkYellow); }
/// <summary> /// Uploads the files in the source Windows path to the target Linux path. /// </summary> /// <param name="sftpClient">The SFTP client.</param> /// <param name="targetPath">The target path.</param> /// <param name="sourcePath">The source path.</param> /// <param name="ignoreFileSuffixes">The ignore file suffixes.</param> private static void UploadFilesToTarget(SftpClient sftpClient, MonitorVerbOptions verbOptions) { var filesInSource = System.IO.Directory.GetFiles(verbOptions.SourcePath, FileSystemMonitor.AllFilesPattern, System.IO.SearchOption.AllDirectories); var filesToDeploy = new List <string>(); foreach (var file in filesInSource) { var ignore = false; foreach (var ignoreSuffix in verbOptions.ExcludeFileSuffixList) { if (file.EndsWith(ignoreSuffix)) { ignore = true; break; } } if (ignore) { continue; } filesToDeploy.Add(file); } ConsoleManager.WriteLine(" Deploying " + filesToDeploy.Count + " files.", ConsoleColor.Green); foreach (var file in filesToDeploy) { var relativePath = MakeRelativePath(file, verbOptions.SourcePath + Path.DirectorySeparatorChar); var fileTargetPath = Path.Combine(verbOptions.TargetPath, relativePath).Replace(WindowsDirectorySeparatorChar, LinuxDirectorySeparatorChar); var targetDirectory = Path.GetDirectoryName(fileTargetPath).Replace(WindowsDirectorySeparatorChar, LinuxDirectorySeparatorChar); CreateLinuxDirectoryRecursive(sftpClient, targetDirectory); using (var fileStream = System.IO.File.OpenRead(file)) { sftpClient.UploadFile(fileStream, fileTargetPath); } } }
/// <summary> /// Starts the user interaction. /// </summary> /// <param name="sshClient">The SSH client.</param> /// <param name="sftpClient">The SFTP client.</param> /// <param name="shellStream">The shell stream.</param> /// <param name="verbOptions">The verb options.</param> private static void StartUserInteraction( SshClient sshClient, SftpClient sftpClient, ShellStream shellStream, MonitorVerbOptions verbOptions) { _forwardShellStreamInput = false; while (true) { var readKey = Console.ReadKey(true); if (readKey.Key == ConsoleKey.F1) { _forwardShellStreamInput = !_forwardShellStreamInput; if (_forwardShellStreamInput) { Program.Title = "Monitor (Interactive)"; Terminal.WriteLine(" >> Entered console input forwarding.", ConsoleColor.Green); _forwardShellStreamOutput = true; } else { Program.Title = "Monitor (Press H for Help)"; Terminal.WriteLine(" >> Left console input forwarding.", ConsoleColor.Red); } continue; } if (_forwardShellStreamInput) { if (readKey.Key == ConsoleKey.Enter) { shellStream.Write("\r\n"); } else { shellStream.WriteByte((byte)readKey.KeyChar); } shellStream.Flush(); continue; } switch (readKey.Key) { case ConsoleKey.Q: return; case ConsoleKey.C: Console.Clear(); break; case ConsoleKey.N: CreateNewDeployment(sshClient, sftpClient, shellStream, verbOptions); break; case ConsoleKey.E: RunSshClientCommand(sshClient, verbOptions); break; case ConsoleKey.S: RunShellStreamCommand(shellStream, verbOptions); break; case ConsoleKey.H: const ConsoleColor helpColor = ConsoleColor.Cyan; Terminal.WriteLine("Console help", helpColor); Terminal.WriteLine(" H Prints this screen", helpColor); Terminal.WriteLine(" Q Quits this application", helpColor); Terminal.WriteLine(" C Clears the screen", helpColor); Terminal.WriteLine(" N Force a deployment cycle", helpColor); Terminal.WriteLine(" E Run the Pre-deployment command", helpColor); Terminal.WriteLine(" S Run the Post-deployment command", helpColor); Terminal.WriteLine(" F1 Toggle shell-interactive mode", helpColor); Terminal.WriteLine(); break; default: Terminal.WriteLine($"Unrecognized command '{readKey.KeyChar}' -- Press 'H' to get a list of available commands.", ConsoleColor.Red); break; } } }
/// <summary> /// Starts the user interaction. /// </summary> /// <param name="fsMonitor">The fs monitor.</param> /// <param name="sshClient">The SSH client.</param> /// <param name="sftpClient">The SFTP client.</param> /// <param name="shellStream">The shell stream.</param> /// <param name="verbOptions">The verb options.</param> private static void StartUserInteraction(FileSystemMonitor fsMonitor, SshClient sshClient, SftpClient sftpClient, ShellStream shellStream, MonitorVerbOptions verbOptions) { ForwardShellStreamInput = false; while (true) { var readKey = Console.ReadKey(true); if (readKey.Key == ConsoleKey.F1) { ForwardShellStreamInput = !ForwardShellStreamInput; if (ForwardShellStreamInput) { ConsoleManager.WriteLine(" >> Entered console input forwarding.", ConsoleColor.Green); ForwardShellStreamOutput = true; } else { ConsoleManager.WriteLine(" >> Left console input forwarding.", ConsoleColor.Red); } continue; } if (ForwardShellStreamInput) { if (readKey.Key == ConsoleKey.Enter) { shellStream.WriteLine(string.Empty); } else { shellStream.WriteByte((byte)readKey.KeyChar); } shellStream.Flush(); continue; } if (readKey.Key == ConsoleKey.Q) { break; } if (readKey.Key == ConsoleKey.C) { ConsoleManager.Clear(); continue; } if (readKey.Key == ConsoleKey.N) { CreateNewDeployment(sshClient, sftpClient, shellStream, verbOptions); continue; } if (readKey.Key == ConsoleKey.E) { RunSshClientCommand(sshClient, verbOptions); continue; } if (readKey.Key == ConsoleKey.S) { RunShellStreamCommand(shellStream, verbOptions); continue; } if (readKey.Key != ConsoleKey.H) { ConsoleManager.WriteLine("Unrecognized command '" + readKey.KeyChar + "' -- Press 'H' to get a list of available commands.", ConsoleColor.Red); } if (readKey.Key == ConsoleKey.H) { var helpColor = ConsoleColor.Cyan; ConsoleManager.WriteLine("Console help", helpColor); ConsoleManager.WriteLine(" H Prints this screen", helpColor); ConsoleManager.WriteLine(" Q Quits this application", helpColor); ConsoleManager.WriteLine(" C Clears the screen", helpColor); ConsoleManager.WriteLine(" N Force a deployment cycle", helpColor); ConsoleManager.WriteLine(" E Run the Pre-deployment command", helpColor); ConsoleManager.WriteLine(" S Run the Post-deployment command", helpColor); ConsoleManager.WriteLine(" F1 Toggle shell-interactive mode", helpColor); ConsoleManager.WriteLine(string.Empty); continue; } } }
/// <summary> /// Creates a new deployment cycle. /// </summary> /// <param name="sshClient">The SSH client.</param> /// <param name="sftpClient">The SFTP client.</param> /// <param name="shellStream">The shell stream.</param> /// <param name="verbOptions">The verb options.</param> private static void CreateNewDeployment(SshClient sshClient, SftpClient sftpClient, ShellStream shellStream, MonitorVerbOptions verbOptions) { // At this point the change has been detected; Make sure we are not deploying ConsoleManager.WriteLine(string.Empty); if (IsDeploying) { ConsoleManager.WriteLine("WARNING: Deployment already in progress. Deployment will not occur.", ConsoleColor.DarkYellow); return; } // Lock Deployment IsDeploying = true; var stopwatch = new System.Diagnostics.Stopwatch(); stopwatch.Start(); try { ForwardShellStreamOutput = false; PrintDeploymentNumber(DeploymentNumber); RunSshClientCommand(sshClient, verbOptions); CreateTargetPath(sftpClient, verbOptions); PrepareTargetPath(sftpClient, verbOptions); UploadFilesToTarget(sftpClient, verbOptions); } catch (Exception ex) { PrintException(ex); } finally { // Unlock deployment IsDeploying = false; DeploymentNumber++; stopwatch.Stop(); ConsoleManager.WriteLine(" Finished deployment in " + Math.Round(stopwatch.Elapsed.TotalSeconds, 2).ToString() + " seconds.", ConsoleColor.Green); ForwardShellStreamOutput = true; RunShellStreamCommand(shellStream, verbOptions); } }
/// <summary> /// Checks that both, SFTP and SSH clients have a working connection. If they don't it attempts to reconnect. /// </summary> /// <param name="sshClient">The SSH client.</param> /// <param name="sftpClient">The SFTP client.</param> /// <param name="verbOptions">The verb options.</param> private static void EnsureMonitorConnection(SshClient sshClient, SftpClient sftpClient, MonitorVerbOptions verbOptions) { if (sshClient.IsConnected == false) { ConsoleManager.WriteLine("Connecting to host " + verbOptions.Host + ":" + verbOptions.Port + " via SSH."); sshClient.Connect(); } if (sftpClient.IsConnected == false) { ConsoleManager.WriteLine("Connecting to host " + verbOptions.Host + ":" + verbOptions.Port + " via SFTP."); sftpClient.Connect(); } }