From e2270c668e9ba1f79614ead0415e919e3e650e58 Mon Sep 17 00:00:00 2001 From: densogiaichned Date: Sun, 18 Feb 2024 20:02:46 +0100 Subject: [PATCH] chore([no ci]/CLI.Installer): Sketch out install process --- src/TcHaxx.Snappy.CLI.Installer/Constants.cs | 12 ++++ .../IInstallerService.cs | 9 +++ .../InstallerService.cs | 24 +++++++ .../Options/DefaultOptions.cs | 9 +++ .../Options/IInstallerOptions.cs | 15 ++++ .../RegistryHelper.cs | 11 +++ .../RepToolProcess.cs | 69 +++++++++++++++++++ .../TcHaxx.Snappy.CLI.Installer.csproj | 24 ++++++- src/TcHaxx.Snappy.CLI.Installer/TcProfile.cs | 41 +++++++++++ .../TwincatProfile.cs | 2 + src/TcHaxx.Snappy.CLI/CLI/InstallOptions.cs | 12 +++- .../Commands/CommandInstall.cs | 10 +-- .../Commands/CommandVerify.cs | 5 +- .../Commands/ICommandInstall.cs | 3 +- .../Commands/ICommandVerify.cs | 3 +- src/TcHaxx.Snappy.CLI/Program.cs | 8 ++- .../Properties/launchSettings.json | 2 +- .../CLI => TcHaxx.Snappy.Common}/ExitCodes.cs | 10 ++- 18 files changed, 250 insertions(+), 19 deletions(-) create mode 100644 src/TcHaxx.Snappy.CLI.Installer/Constants.cs create mode 100644 src/TcHaxx.Snappy.CLI.Installer/IInstallerService.cs create mode 100644 src/TcHaxx.Snappy.CLI.Installer/InstallerService.cs create mode 100644 src/TcHaxx.Snappy.CLI.Installer/Options/DefaultOptions.cs create mode 100644 src/TcHaxx.Snappy.CLI.Installer/Options/IInstallerOptions.cs create mode 100644 src/TcHaxx.Snappy.CLI.Installer/RegistryHelper.cs create mode 100644 src/TcHaxx.Snappy.CLI.Installer/RepToolProcess.cs create mode 100644 src/TcHaxx.Snappy.CLI.Installer/TcProfile.cs create mode 100644 src/TcHaxx.Snappy.CLI.Installer/TwincatProfile.cs rename src/{TcHaxx.Snappy.CLI/CLI => TcHaxx.Snappy.Common}/ExitCodes.cs (66%) diff --git a/src/TcHaxx.Snappy.CLI.Installer/Constants.cs b/src/TcHaxx.Snappy.CLI.Installer/Constants.cs new file mode 100644 index 0000000..3db996b --- /dev/null +++ b/src/TcHaxx.Snappy.CLI.Installer/Constants.cs @@ -0,0 +1,12 @@ +namespace TcHaxx.Snappy.CLI.Installer; +public static class Constants +{ + public const string TC31_PROFILES_DIRECTORY = @"Components\Plc\Profiles"; + public const string TC31_REPTOOL_EXE = @"Components\Plc\Common\RepTool.exe"; + public const string TC31_GLOB_PROFILE = "TwinCAT PLC Control_Build_*.profile"; + + public const string DEFAULT_OPTION_TCPROFILE = "latest"; + public const string DEFAULT_OPTION_TOOLSPATH = @"%USERPROFILE%\.dotnet\tools\.store\tchaxx.snappy.cli"; + + public static readonly int REPTOOL_EXE_TIMEOUT_MS = 180_000; +} diff --git a/src/TcHaxx.Snappy.CLI.Installer/IInstallerService.cs b/src/TcHaxx.Snappy.CLI.Installer/IInstallerService.cs new file mode 100644 index 0000000..a335ccf --- /dev/null +++ b/src/TcHaxx.Snappy.CLI.Installer/IInstallerService.cs @@ -0,0 +1,9 @@ +using Serilog; +using TcHaxx.Snappy.CLI.Installer.Options; +using TcHaxx.Snappy.Common; + +namespace TcHaxx.Snappy.CLI.Installer; +public interface IInstallerService +{ + public Task Install(IInstallerOptions options, ILogger? logger); +} diff --git a/src/TcHaxx.Snappy.CLI.Installer/InstallerService.cs b/src/TcHaxx.Snappy.CLI.Installer/InstallerService.cs new file mode 100644 index 0000000..afa0aa4 --- /dev/null +++ b/src/TcHaxx.Snappy.CLI.Installer/InstallerService.cs @@ -0,0 +1,24 @@ +using Serilog; +using TcHaxx.Snappy.CLI.Installer.Options; +using TcHaxx.Snappy.Common; + +namespace TcHaxx.Snappy.CLI.Installer; +public class InstallerService : IInstallerService +{ + public async Task Install(IInstallerOptions options, ILogger? logger) + { + + var tcProfile = TcProfile.GetTwinCatProfile(options, logger); + if (tcProfile is null) + { + return ExitCodes.E_ERROR; + } + + + var expandedPath = Environment.ExpandEnvironmentVariables(options.ToolsPath); + var sourceDirectoryInfo = new DirectoryInfo(expandedPath ?? options.ToolsPath); + + var exitCode = await RepToolProcess.RunRepToolAsync(tcProfile, sourceDirectoryInfo, logger); + return exitCode; + } +} diff --git a/src/TcHaxx.Snappy.CLI.Installer/Options/DefaultOptions.cs b/src/TcHaxx.Snappy.CLI.Installer/Options/DefaultOptions.cs new file mode 100644 index 0000000..3639b0b --- /dev/null +++ b/src/TcHaxx.Snappy.CLI.Installer/Options/DefaultOptions.cs @@ -0,0 +1,9 @@ +namespace TcHaxx.Snappy.CLI.Installer.Options; +internal class DefaultOptions : IInstallerOptions +{ + /// + public string TcProfile { get; init; } = Constants.DEFAULT_OPTION_TCPROFILE; + + /// + public string ToolsPath { get; init; } = Constants.DEFAULT_OPTION_TOOLSPATH; +} diff --git a/src/TcHaxx.Snappy.CLI.Installer/Options/IInstallerOptions.cs b/src/TcHaxx.Snappy.CLI.Installer/Options/IInstallerOptions.cs new file mode 100644 index 0000000..323cf8c --- /dev/null +++ b/src/TcHaxx.Snappy.CLI.Installer/Options/IInstallerOptions.cs @@ -0,0 +1,15 @@ +namespace TcHaxx.Snappy.CLI.Installer.Options; + +public interface IInstallerOptions +{ + /// + /// TwinCAT profile to use, i.e "latest" or specific version "TwinCAT PLC Control_Build_4024.54" + /// + public string TcProfile { get; init; } + + /// + /// Directory, where Dotnet global tools are installed, e.g. %USERPROFILE%\.dotnet\tools. + /// + public string ToolsPath { get; init; } + +} diff --git a/src/TcHaxx.Snappy.CLI.Installer/RegistryHelper.cs b/src/TcHaxx.Snappy.CLI.Installer/RegistryHelper.cs new file mode 100644 index 0000000..b44544b --- /dev/null +++ b/src/TcHaxx.Snappy.CLI.Installer/RegistryHelper.cs @@ -0,0 +1,11 @@ +using Microsoft.Win32; + +namespace TcHaxx.Snappy.CLI.Installer; +internal static class RegistryHelper +{ + internal static string GetTwincatInstallDirectory() + { + var regKey = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Beckhoff\\TwinCAT3\\3.1"); + return (regKey?.GetValue("InstallDir") as string) ?? string.Empty; + } +} diff --git a/src/TcHaxx.Snappy.CLI.Installer/RepToolProcess.cs b/src/TcHaxx.Snappy.CLI.Installer/RepToolProcess.cs new file mode 100644 index 0000000..236ca21 --- /dev/null +++ b/src/TcHaxx.Snappy.CLI.Installer/RepToolProcess.cs @@ -0,0 +1,69 @@ +using System.Diagnostics; +using Serilog; +using TcHaxx.Snappy.Common; + +namespace TcHaxx.Snappy.CLI.Installer; +internal static class RepToolProcess +{ + internal static async Task RunRepToolAsync(TwincatProfile tcProfile, DirectoryInfo sourceDirectory, ILogger? logger) + { + var tcDir = RegistryHelper.GetTwincatInstallDirectory(); + if (string.IsNullOrWhiteSpace(tcDir)) + { + logger?.Error("Couldn't read TwinCAT installation directory from Registry."); + return ExitCodes.E_ERROR; + } + + var repToolExe = Path.Join(tcDir, Constants.TC31_REPTOOL_EXE); + if (!File.Exists(repToolExe)) + { + throw new FileNotFoundException("RepTool.exe doesn't exist.", repToolExe); + } + + try + { + using var process = new Process(); + process.StartInfo = new ProcessStartInfo + { + FileName = repToolExe, + Arguments = $"--profile='{tcProfile.Profile}' --installLibsRecursNoOverwrite \"{sourceDirectory.FullName}\"", + UseShellExecute = false, + RedirectStandardInput = false, + RedirectStandardError = true, + RedirectStandardOutput = true + }; + + process.OutputDataReceived += (sender, args) => + { + if (string.IsNullOrEmpty(args.Data)) + { + return; + } + logger?.Information("RepTool.exe: {StandardOutput}", args.Data ?? string.Empty); + }; + process.ErrorDataReceived += (sender, args) => + { + if (string.IsNullOrEmpty(args.Data)) + { + return; + } + + logger?.Error("RepTool.exe: {StandardError}", args.Data ?? string.Empty); + }; + + logger?.Information("Installing TwinCAT libraries ..."); + + process.Start(); + var stdout = await process.StandardOutput.ReadToEndAsync(); + logger?.Information("RepTool.exe: {StandardOutput}", stdout ?? string.Empty); + + process.WaitForExit(Constants.REPTOOL_EXE_TIMEOUT_MS); + return (ExitCodes)process.ExitCode; + } + catch (Exception ex) + { + logger?.Fatal(ex, "Couldn't install TwinCAT libraries."); + return ExitCodes.E_EXCEPTION; + } + } +} diff --git a/src/TcHaxx.Snappy.CLI.Installer/TcHaxx.Snappy.CLI.Installer.csproj b/src/TcHaxx.Snappy.CLI.Installer/TcHaxx.Snappy.CLI.Installer.csproj index d03ff0b..700a737 100644 --- a/src/TcHaxx.Snappy.CLI.Installer/TcHaxx.Snappy.CLI.Installer.csproj +++ b/src/TcHaxx.Snappy.CLI.Installer/TcHaxx.Snappy.CLI.Installer.csproj @@ -1,5 +1,27 @@  - + + $(NoWarn);CS1591;CS1573;CA1416 + + + + $(NoWarn);CS1591;CS1573;CA1416 + + + + $(NoWarn);CS1591;CS1573;CA1416 + + + + $(NoWarn);CS1591;CS1573;CA1416 + + + + + + + + + diff --git a/src/TcHaxx.Snappy.CLI.Installer/TcProfile.cs b/src/TcHaxx.Snappy.CLI.Installer/TcProfile.cs new file mode 100644 index 0000000..59e774a --- /dev/null +++ b/src/TcHaxx.Snappy.CLI.Installer/TcProfile.cs @@ -0,0 +1,41 @@ +using Serilog; +using TcHaxx.Snappy.CLI.Installer.Options; + +namespace TcHaxx.Snappy.CLI.Installer; +internal static class TcProfile +{ + internal static TwincatProfile? GetTwinCatProfile(IInstallerOptions options, ILogger? logger) + { + if (!options.TcProfile.Equals(Constants.DEFAULT_OPTION_TCPROFILE, StringComparison.OrdinalIgnoreCase)) + { + logger?.Information("Using provided TwinCAT profile \"{TwinCatProfile}\"", options.TcProfile); + return new TwincatProfile(options.TcProfile); + } + + var tcDir = RegistryHelper.GetTwincatInstallDirectory(); + if (string.IsNullOrWhiteSpace(tcDir)) + { + logger?.Error("Couldn't read TwinCAT installation directory from Registry."); + return default; + } + + var profilesDir = Path.Join(tcDir, Constants.TC31_PROFILES_DIRECTORY); + if (!Directory.Exists(profilesDir)) + { + logger?.Error("Directory \"{TwinCatProfileDirectory}\" doesn't exist.", profilesDir); + return default; + } + + var profiles = Directory.GetFiles(profilesDir, Constants.TC31_GLOB_PROFILE); + if (!profiles.Any()) + { + logger?.Error("Couldn't find any profiles \"{TwinCatProfileGlob}\" in \"{TwinCatProfileDirectory}\".", + Constants.TC31_GLOB_PROFILE, profilesDir); + } + + var profileToUse = profiles.OrderByDescending(x => x).First(); + logger?.Information("Using TwinCAT profile \"{TwinCatProfile}\"", profileToUse); + return new TwincatProfile(Path.GetFileNameWithoutExtension(profileToUse)); + } +} + diff --git a/src/TcHaxx.Snappy.CLI.Installer/TwincatProfile.cs b/src/TcHaxx.Snappy.CLI.Installer/TwincatProfile.cs new file mode 100644 index 0000000..3c00557 --- /dev/null +++ b/src/TcHaxx.Snappy.CLI.Installer/TwincatProfile.cs @@ -0,0 +1,2 @@ +namespace TcHaxx.Snappy.CLI.Installer; +internal record TwincatProfile(string Profile); diff --git a/src/TcHaxx.Snappy.CLI/CLI/InstallOptions.cs b/src/TcHaxx.Snappy.CLI/CLI/InstallOptions.cs index db813a2..147a42d 100644 --- a/src/TcHaxx.Snappy.CLI/CLI/InstallOptions.cs +++ b/src/TcHaxx.Snappy.CLI/CLI/InstallOptions.cs @@ -1,8 +1,16 @@ using CommandLine; +using TcHaxx.Snappy.CLI.Installer.Options; namespace TcHaxx.Snappy.CLI.CLI; -[Verb("install", false, HelpText = "Install TcHaxx.Snappy.library.")] -internal class InstallOptions : BaseOptions +[Verb("install", false, HelpText = "Install snappy.library and dependencies.")] +public class InstallOptions : BaseOptions, IInstallerOptions { + /// + [Option("tc-profile", Default = Installer.Constants.DEFAULT_OPTION_TCPROFILE, Required = false, HelpText = "TwinCAT profile to use, i.e \"latest\" or specific version \"TwinCAT PLC Control_Build_4024.54\".")] + public string TcProfile { get; init; } = string.Empty; + + /// + [Option("tool-path", Default = Installer.Constants.DEFAULT_OPTION_TOOLSPATH, Required = false, HelpText = "Directory, where dotnet global tools are installed.")] + public string ToolsPath { get; init; } = string.Empty; } diff --git a/src/TcHaxx.Snappy.CLI/Commands/CommandInstall.cs b/src/TcHaxx.Snappy.CLI/Commands/CommandInstall.cs index 3f6f887..459b731 100644 --- a/src/TcHaxx.Snappy.CLI/Commands/CommandInstall.cs +++ b/src/TcHaxx.Snappy.CLI/Commands/CommandInstall.cs @@ -1,17 +1,17 @@ using Serilog; using TcHaxx.Snappy.CLI.CLI; +using TcHaxx.Snappy.CLI.Installer; +using TcHaxx.Snappy.Common; namespace TcHaxx.Snappy.CLI.Commands; internal class CommandInstall(ILogger? logger) : ICommandInstall { private readonly ILogger? _logger = logger?.ForContext(); + private readonly IInstallerService _installerService = new InstallerService(); - public async Task RunAndReturnExitCode(InstallOptions options) + public async Task RunAndReturnExitCode(InstallOptions options) { - _logger?.Information("Installing ..."); - await Task.Delay(1000); - return 0; + return await _installerService.Install(options, _logger); } - } diff --git a/src/TcHaxx.Snappy.CLI/Commands/CommandVerify.cs b/src/TcHaxx.Snappy.CLI/Commands/CommandVerify.cs index 8e574bf..7c0373c 100644 --- a/src/TcHaxx.Snappy.CLI/Commands/CommandVerify.cs +++ b/src/TcHaxx.Snappy.CLI/Commands/CommandVerify.cs @@ -1,5 +1,6 @@ using Serilog; using TcHaxx.Snappy.CLI.CLI; +using TcHaxx.Snappy.Common; using TcHaxx.Snappy.TcADS; using TcHaxx.Snappy.Verifier; @@ -11,7 +12,7 @@ internal class CommandVerify(IEnumerable verifyServices, ISymbol private readonly ISymbolicServerFactory _symbolicServerFactory = symbolicServerFactory ?? throw new ArgumentNullException(nameof(symbolicServerFactory)); private readonly ILogger? _logger = logger; - public async Task RunAndReturnExitCode(VerifyOptions options) + public async Task RunAndReturnExitCode(VerifyOptions options) { foreach (var service in _verifyServices) { @@ -23,6 +24,6 @@ public async Task RunAndReturnExitCode(VerifyOptions options) _logger?.Information("Starting AdsSymbolicServer..."); var adsRetVal = await symbolicServer.ConnectServerAndWaitAsync(new CancellationToken()); - return (int)adsRetVal; + return (ExitCodes)adsRetVal; } } diff --git a/src/TcHaxx.Snappy.CLI/Commands/ICommandInstall.cs b/src/TcHaxx.Snappy.CLI/Commands/ICommandInstall.cs index 01344c8..e27901b 100644 --- a/src/TcHaxx.Snappy.CLI/Commands/ICommandInstall.cs +++ b/src/TcHaxx.Snappy.CLI/Commands/ICommandInstall.cs @@ -1,8 +1,9 @@ using TcHaxx.Snappy.CLI.CLI; +using TcHaxx.Snappy.Common; namespace TcHaxx.Snappy.CLI.Commands; internal interface ICommandInstall { - Task RunAndReturnExitCode(InstallOptions options); + Task RunAndReturnExitCode(InstallOptions options); } diff --git a/src/TcHaxx.Snappy.CLI/Commands/ICommandVerify.cs b/src/TcHaxx.Snappy.CLI/Commands/ICommandVerify.cs index 783c27e..490ac07 100644 --- a/src/TcHaxx.Snappy.CLI/Commands/ICommandVerify.cs +++ b/src/TcHaxx.Snappy.CLI/Commands/ICommandVerify.cs @@ -1,8 +1,9 @@ using TcHaxx.Snappy.CLI.CLI; +using TcHaxx.Snappy.Common; namespace TcHaxx.Snappy.CLI.Commands; internal interface ICommandVerify { - Task RunAndReturnExitCode(VerifyOptions options); + Task RunAndReturnExitCode(VerifyOptions options); } diff --git a/src/TcHaxx.Snappy.CLI/Program.cs b/src/TcHaxx.Snappy.CLI/Program.cs index 13a172a..72e31e9 100644 --- a/src/TcHaxx.Snappy.CLI/Program.cs +++ b/src/TcHaxx.Snappy.CLI/Program.cs @@ -7,6 +7,7 @@ using TcHaxx.Snappy.CLI.CLI; using TcHaxx.Snappy.CLI.Commands; using TcHaxx.Snappy.CLI.Logging; +using TcHaxx.Snappy.Common; using TcHaxx.Snappy.Common.RPC; using TcHaxx.Snappy.TcADS; using TcHaxx.Snappy.Verifier; @@ -17,12 +18,13 @@ using var host = BuildHost(args); - return await Parser.Default.ParseArguments(args) + var exitCode = await Parser.Default.ParseArguments(args) .MapResult( async (InstallOptions options) => await host.Services.GetService()!.RunAndReturnExitCode(options), async (VerifyOptions options) => await host.Services.GetService()!.RunAndReturnExitCode(options), - errs => Task.FromResult((int)ExitCodes.E_CLIOPTIONS) - ); + errs => Task.FromResult(ExitCodes.E_CLIOPTIONS)); + + return (int)exitCode; } catch (Exception ex) { diff --git a/src/TcHaxx.Snappy.CLI/Properties/launchSettings.json b/src/TcHaxx.Snappy.CLI/Properties/launchSettings.json index a0e5be6..d4f1a7d 100644 --- a/src/TcHaxx.Snappy.CLI/Properties/launchSettings.json +++ b/src/TcHaxx.Snappy.CLI/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "TcHaxx.Snappy.CLI": { "commandName": "Project", - "commandLineArgs": "verify -d D:\\densogiaichned\\snappy\\examples\\examples\\.snappy-verified" + "commandLineArgs": "install" } } } \ No newline at end of file diff --git a/src/TcHaxx.Snappy.CLI/CLI/ExitCodes.cs b/src/TcHaxx.Snappy.Common/ExitCodes.cs similarity index 66% rename from src/TcHaxx.Snappy.CLI/CLI/ExitCodes.cs rename to src/TcHaxx.Snappy.Common/ExitCodes.cs index 8949284..43d5aae 100644 --- a/src/TcHaxx.Snappy.CLI/CLI/ExitCodes.cs +++ b/src/TcHaxx.Snappy.Common/ExitCodes.cs @@ -1,9 +1,9 @@ -namespace TcHaxx.Snappy.CLI.CLI; +namespace TcHaxx.Snappy.Common; /// /// Exit codes for this application. /// -internal enum ExitCodes +public enum ExitCodes { /// /// Exit code: No error. @@ -18,6 +18,10 @@ internal enum ExitCodes /// /// Exit code: Parsing arguments and/or arguments missing/wrong. /// - E_CLIOPTIONS = 2 + E_CLIOPTIONS = 2, + /// + /// Exit code: General error occured, see logs. + /// + E_ERROR = 3 }