namespace Framework.Cli { using Framework.Cli.Command; using Framework.Cli.Config; using Microsoft.Extensions.CommandLineUtils; using System; using System.CodeDom; using System.CodeDom.Compiler; using System.Diagnostics; using System.Globalization; using System.IO; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; internal static class UtilCliInternal { /// /// Returns CommandOption flag. /// public static bool OptionGet(this CommandOption option) { return option?.Value() == "on" == true; } /// /// Sets CommandOption flag. /// public static void OptionSet(ref CommandOption option, bool value) { if (option == null) { option = new CommandOption("--null", CommandOptionType.NoValue); // For example if command calls command and options is not registered. } option.Values.Clear(); if (value) { option.Values.Add("on"); } UtilFramework.Assert(OptionGet(option) == value); } /// /// Run dotnet command. /// internal static void DotNet(string workingDirectory, string arguments, bool isWait = true) { Start(workingDirectory, "dotnet", arguments, isWait: isWait); } /// /// Run npm command. /// internal static void Npm(string workingDirectory, string arguments, bool isWait = true, bool isRedirectStdErr = false) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { UtilCliInternal.Start(workingDirectory, "cmd", "/c npm.cmd " + arguments, isWait: isWait, isRedirectStdErr: isRedirectStdErr); } else { UtilCliInternal.Start(workingDirectory, "npm", arguments, isWait: isWait, isRedirectStdErr: isRedirectStdErr); } } /// /// Start script. /// /// If true, do not write to stderr. Use this flag if shell command is known to write info (mistakenly) to stderr. internal static void Start(string workingDirectory, string fileName, string arguments, bool isWait = true, bool isRedirectStdErr = false) { string time = UtilFramework.DateTimeToString(DateTime.Now); UtilCliInternal.ConsoleWriteLinePassword(string.Format("### {4} Process Begin (FileName={1}; Arguments={2}; IsWait={3}; WorkingDirectory={0};)", workingDirectory, fileName, arguments, isWait, time), ConsoleColor.Green); ProcessStartInfo info = new ProcessStartInfo { WorkingDirectory = workingDirectory, FileName = fileName, Arguments = arguments }; if (isRedirectStdErr) { info.RedirectStandardError = true; // Do not write to stderr. } // info.UseShellExecute = true; using (var process = Process.Start(info)) { if (isWait) { if (isRedirectStdErr) { // process.WaitForExit(); // Can hang. For example Angular 9.1.1 build:ssr (May be when std buffer is full) string errorText = process.StandardError.ReadToEnd(); // Waits for process to exit. process.WaitForExit(); // Used for Ubuntu. Otherwise HasExited is not (yet) true. UtilFramework.Assert(process.HasExited); if (!string.IsNullOrEmpty(errorText)) { UtilCliInternal.ConsoleWriteLinePassword(string.Format("### {4} Process StdErr (FileName={1}; Arguments={2}; IsWait={3}; WorkingDirectory={0};)", workingDirectory, fileName, arguments, isWait, time), ConsoleColor.DarkGreen); // Write stderr to stdout. UtilCliInternal.ConsoleWriteLinePassword(errorText, ConsoleColor.DarkGreen); // Log DarkGreen because it is not treated like an stderr output. } } else { process.WaitForExit(); UtilFramework.Assert(process.HasExited); } if (process.ExitCode != 0) { throw new Exception("Script failed!"); } } } UtilCliInternal.ConsoleWriteLinePassword(string.Format("### {4} Process End (FileName={1}; Arguments={2}; IsWait={3}; WorkingDirectory={0};)", workingDirectory, fileName, arguments, isWait, time), ConsoleColor.DarkGreen); } /// /// Returns stdout of command. /// internal static string StartStdout(string workingDirectory, string fileName, string arguments) { ProcessStartInfo info = new ProcessStartInfo { WorkingDirectory = workingDirectory, FileName = fileName, Arguments = arguments, RedirectStandardOutput = true // Do not write to stdout. }; var process = Process.Start(info); process.WaitForExit(); string result = process.StandardOutput.ReadToEnd(); if (process.ExitCode != 0) { throw new Exception("Script failed!"); } return result; } internal static void OpenWebBrowser(string url) { Start(null, "cmd", $"/c start {url}", isWait: false); } internal static void FolderNameDelete(string folderName) { if (Directory.Exists(folderName)) { foreach (FileInfo fileInfo in new DirectoryInfo(folderName).GetFiles("*.*", SearchOption.AllDirectories)) { fileInfo.Attributes = FileAttributes.Normal; // See also: https://stackoverflow.com/questions/1701457/directory-delete-doesnt-work-access-denied-error-but-under-windows-explorer-it/30673648 } Directory.Delete(folderName, true); } } /// /// Adjustment for: If sequence of arguments is passed different than defined in CommandLineApplication values are wrong. /// /// /// /// private static CommandArgument ArgumentValue(CommandBase command, CommandArgument commandArgument) { CommandArgument result = null; foreach (CommandArgument item in command.Configuration.Arguments) { string name = item.Value; if (name?.IndexOf("=") != -1) { name = name?.Substring(0, name.IndexOf("=")); } if (name?.ToLower() == commandArgument.Name.ToLower()) { result = item; break; } } return result; } /// /// Returns true, if argument is used in command line. /// internal static bool ArgumentValueIsDelete(CommandBase command, CommandArgument commandArgument) { commandArgument = ArgumentValue(command, commandArgument); // Sequence of passed arguments might be wrong. bool result = commandArgument != null && commandArgument.Value != null; return result; } /// /// Returns true, if value has been set. (Use Argument=null to set a value to null). /// /// Returns value. internal static bool ArgumentValue(CommandBase command, CommandArgument commandArgument, out string value) { string name = commandArgument.Name; commandArgument = ArgumentValue(command, commandArgument); // Sequence of passed arguments might be wrong. bool isValue = false; string result = commandArgument.Value; UtilFramework.Assert(name.ToLower() == result.Substring(0, name.Length).ToLower()); if (result.ToUpper().StartsWith(name.ToUpper())) { result = result.Substring(name.Length); } if (result.StartsWith("=")) { result = result.Substring(1); } result = UtilFramework.StringNull(result); if (result != null) { isValue = true; } if (result?.ToLower() == "null") // User sets value to null. { result = null; } value = result; return isValue; } /// /// Copy folder. /// /// For example: "*.*" /// If true, includes subdirectories. internal static void FolderCopy(string folderNameSource, string folderNameDest, string searchPattern = "*.*", bool isAllDirectory = true) { var source = new DirectoryInfo(folderNameSource); var dest = new DirectoryInfo(folderNameDest); SearchOption searchOption = SearchOption.TopDirectoryOnly; foreach (FileInfo file in source.GetFiles(searchPattern, searchOption)) { string fileNameSource = file.FullName; string fileNameDest = Path.Combine(dest.FullName, file.FullName.Substring(source.FullName.Length)); FileCopy(fileNameSource, fileNameDest); } if (isAllDirectory) { foreach (var folderName in source.GetDirectories()) { if (folderName.Name == "node_modules") { // Skip folder node_modules/ continue; } FolderCopy(folderNameSource + folderName.Name + "/", folderNameDest + folderName.Name + "/"); } } } /// /// Create folder if it does not yet exist. /// internal static void FolderCreate(string fileName) { string folderName = new FileInfo(fileName).DirectoryName; if (!Directory.Exists(folderName)) { Directory.CreateDirectory(folderName); } } internal static void FileCopy(string fileNameSource, string fileNameDest) { FolderCreate(fileNameDest); File.Copy(fileNameSource, fileNameDest, true); // chmod+x for ./cli.sh file for Linux. if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { if (Path.GetExtension(fileNameSource) == ".sh") { // Console.WriteLine("chmode +x " + fileNameDest); Process.Start(new ProcessStartInfo { FileName = "/bin/bash", Arguments = "-c \"chmod +x " + fileNameDest, }).WaitForExit(); } } } internal static void FolderDelete(string folderName) { var count = 0; do { if (count > 0) { Task.Delay(1000).Wait(); // Wait for next attempt. } if (UtilCliInternal.FolderNameExist(folderName)) { foreach (FileInfo fileInfo in new DirectoryInfo(folderName).GetFiles("*.*", SearchOption.AllDirectories)) { fileInfo.Attributes = FileAttributes.Normal; // See also: https://stackoverflow.com/questions/1701457/directory-delete-doesnt-work-access-denied-error-but-under-windows-explorer-it/30673648 } try { Directory.Delete(folderName, true); } catch (IOException) { // Silent exception. Console.WriteLine(string.Format("Can not delete folder! ({0})", folderName)); UtilFramework.NodeClose(); } } count += 1; } while (UtilCliInternal.FolderNameExist(folderName) && count <= 3); UtilFramework.Assert(!UtilCliInternal.FolderNameExist(folderName), string.Format("Can not delete folder! Make sure it is not locked by another program. For example by a running server.ts or node.exe. ({0}", folderName)); } internal static bool FolderNameExist(string folderName) { return Directory.Exists(folderName); } /// /// Returns git commit sha. /// internal static string GitCommit() { string result = "Commit"; try { result = UtilCliInternal.StartStdout(UtilFramework.FolderName, "git", "rev-parse --short HEAD"); result = result.Replace("\n", ""); } catch { // Silent exception } return result; } /// /// Tag version build. /// internal static void VersionBuild(Action build) { // Read UtilFramework.cs string fileNameServer = UtilFramework.FolderName + "Framework/Framework/UtilFramework.cs"; string textServer = UtilFramework.FileLoad(fileNameServer); // string fileNameClient = UtilFramework.FolderName + "Framework/Framework.Angular/application/src/app/data.service.ts"; // TODO Tag all Angular sites. // string textClient = UtilFramework.FileLoad(fileNameClient); string versionBuild = string.Format("Build (WorkplaceX={3}; Commit={0}; Pc={1}; Time={2} (UTC);)", UtilCliInternal.GitCommit(), System.Environment.MachineName, UtilFramework.DateTimeToString(DateTime.Now.ToUniversalTime()), UtilFramework.Version); string findServer = "/* VersionBuild */"; // See also: method CommandBuild.Execute(); and method CommandStart.Execute(); string replaceServer = string.Format(" return \"{0}\"; /* VersionBuild */", versionBuild); // string findClient = "/* VersionBuild */"; // See also: file data.service.ts // string replaceClient = string.Format(" public VersionBuild: string = \"{0}\"; /* VersionBuild */", versionBuild); // Write UtilFramework.cs string textNewServer = UtilFramework.ReplaceLine(textServer, findServer, replaceServer); File.WriteAllText(fileNameServer, textNewServer); // string textNewClient = UtilFramework.ReplaceLine(textClient, findClient, replaceClient); // File.WriteAllText(fileNameClient, textNewClient); try { build(); } finally { File.WriteAllText(fileNameServer, textServer); // Back to original text. // File.WriteAllText(fileNameClient, textClient); // Back to original text. } } /// /// Returns password (ConnectionString or GitUrl) without sensitive data. /// /// For example ConnectionString or GitUrl. private static string ConsoleWriteLinePasswordHide(string password) { return "[Password]"; // Remove password from ConnectionString or GitUrl. } /// /// Returns text without password. It replaces password with PasswordHide. /// private static string ConsoleWriteLinePasswordHide(string text, string password) { if (text != null && password?.Length > 0) { while (text.ToLower().IndexOf(password.ToLower()) >= 0) { int indexStart = text.ToLower().IndexOf(password.ToLower()); int length = password.Length; string passwordHide = ConsoleWriteLinePasswordHide(password); text = text.Substring(0, indexStart) + passwordHide + text.Substring(indexStart + length); } } return text; } /// /// Write text which might contain sensitive data (ConnectionString and GitUrl) with this method to console. /// internal static void ConsoleWriteLinePassword(object value, ConsoleColor? color = null) { string text = string.Format("{0}", value); var configCli = ConfigCli.Load(); var environment = configCli.EnvironmentGet(); text = ConsoleWriteLinePasswordHide(text, environment.ConnectionStringFramework); text = ConsoleWriteLinePasswordHide(text, environment.ConnectionStringApplication); text = ConsoleWriteLinePasswordHide(text, environment.DeployAzureGitUrl); foreach (var item in configCli.ExternalGitList) { text = ConsoleWriteLinePasswordHide(text, item.ExternalGit); } text = text.Replace("{", "{{").Replace("}", "}}"); // Console.Write("{", ConsoleColor.Green); throws exception "Input string was not in a correct format". // TODO Bug report Console.WriteLine(text, color); } /// /// Write to console in color. /// internal static void ConsoleWriteLineColor(object value, ConsoleColor? color, bool isLine = true) { if (color == null) { if (isLine) { Console.WriteLine(value); } else { Console.Write(value); } } else { Console.ForegroundColor = color.Value; if (isLine) { Console.WriteLine(value); } else { Console.Write(value); } Console.ResetColor(); // ConsoleColor foregroundColor = Console.ForegroundColor; // Console.ForegroundColor = color.Value; // try // { // Console.WriteLine(value); // } // finally // { // Console.ForegroundColor = foregroundColor; // } } } /// /// Wait for user interaction. /// internal static bool ConsoleReadYesNo(string text) { string consoleReadLine; do { Console.Write(text + " [y/n] "); consoleReadLine = Console.ReadLine().ToUpper(); } while (!(consoleReadLine == "Y" || consoleReadLine == "N")); return consoleReadLine == "Y"; } /// /// Write to stderr. /// internal static void ConsoleWriteLineError(object value) { using TextWriter textWriter = Console.Error; textWriter.WriteLine(value); } /// /// Returns text escaped as CSharp code. Handles special characters. /// public static string EscapeCSharpString(string text) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("\""); var textList = UtilFramework.SplitChunk(text); // Because of line break after 80 characters! using (var writer = new StringWriter(CultureInfo.InvariantCulture)) { using var provider = CodeDomProvider.CreateProvider("CSharp"); foreach (var item in textList) { provider.GenerateCodeFromExpression(new CodePrimitiveExpression(item), writer, null); // Does a line break after 80 characters by default! string textCSharp = writer.ToString(); UtilFramework.Assert(textCSharp.StartsWith("\"")); UtilFramework.Assert(textCSharp.EndsWith("\"")); textCSharp = textCSharp[1..^1]; // Remove quotation marks. stringBuilder.Append(textCSharp); writer.GetStringBuilder().Clear(); // Reset writer for next chunk. } } stringBuilder.Append("\""); string result = stringBuilder.ToString(); return result; } /// /// Create new text file. /// public static void FileCreate(string fileName, string text = null) { FolderCreate(fileName); File.WriteAllText(fileName, text); } /// /// Rename file. /// public static void FileRename(string fileNameSource, string fileNameDest) { if (fileNameSource != fileNameDest) { FileCopy(fileNameSource, fileNameDest); File.Delete(fileNameSource); } } } }