diff --git a/Directory.Build.props b/Directory.Build.props index f3fe61328..2d94db4f3 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,10 +1,12 @@ - 0.0.0.1 + 0.0.0.0 + 0.0.0.0 + 0.0.0-local Martin Hinshelwood naked Agility with Martin Hinshelwood - 9.0 MigrationTools.CommandLine + default 1701;1702;1591 diff --git a/MigrationTools.sln b/MigrationTools.sln index 3c1027118..d0209cc7e 100644 --- a/MigrationTools.sln +++ b/MigrationTools.sln @@ -126,6 +126,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".build", ".build", "{88C358 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{BB497233-248C-49DF-AE12-F7A76F775E74}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NKDAgility.AzureDevOps.Tools.CommandHost", "src\NKDAgility.AzureDevOps.Tools.CommandHost\NKDAgility.AzureDevOps.Tools.CommandHost.csproj", "{60EF98A1-5AA4-4589-8B6F-A77B3940025D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -208,6 +210,10 @@ Global {6A259EA6-860B-448A-8943-594DC1A15105}.Debug|Any CPU.Build.0 = Debug|Any CPU {6A259EA6-860B-448A-8943-594DC1A15105}.Release|Any CPU.ActiveCfg = Release|Any CPU {6A259EA6-860B-448A-8943-594DC1A15105}.Release|Any CPU.Build.0 = Release|Any CPU + {60EF98A1-5AA4-4589-8B6F-A77B3940025D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {60EF98A1-5AA4-4589-8B6F-A77B3940025D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {60EF98A1-5AA4-4589-8B6F-A77B3940025D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {60EF98A1-5AA4-4589-8B6F-A77B3940025D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -241,6 +247,7 @@ Global {6A259EA6-860B-448A-8943-594DC1A15105} = {83F36820-E9BC-4F48-8202-5EAF9530405E} {AC3B5101-83F5-4C28-976C-C325425D1988} = {1F5E9C8C-AD05-4C4F-B370-FF3D080A6541} {BB497233-248C-49DF-AE12-F7A76F775E74} = {83F36820-E9BC-4F48-8202-5EAF9530405E} + {60EF98A1-5AA4-4589-8B6F-A77B3940025D} = {BB497233-248C-49DF-AE12-F7A76F775E74} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {62EE0B27-C55A-46EE-8D17-1691DE9BBD50} diff --git a/docs/Reference/Generated/MigrationTools.xml b/docs/Reference/Generated/MigrationTools.xml index c2aa0f267..d51c4e74b 100644 --- a/docs/Reference/Generated/MigrationTools.xml +++ b/docs/Reference/Generated/MigrationTools.xml @@ -594,7 +594,7 @@ - => @"true" + => @"false" @@ -604,37 +604,37 @@ - => @"main" + => @"topic/move-cmd" - => @"d10bad5" + => @"3a2c737" - => @"d10bad52221de4cd9849d63033095613211d5ce0" + => @"3a2c737011911c5b1e68261733d02c1ef8f40cc1" - => @"2024-07-24T16:56:23+01:00" + => @"2024-07-25T13:50:46+01:00" - => @"0" + => @"11" - => @"v15.1.4-Preview.7" + => @"v15.1.5-Preview.2-11-g3a2c737" - => @"v15.1.4-Preview.7" + => @"v15.1.5-Preview.2" @@ -649,7 +649,7 @@ - => @"4" + => @"5" @@ -664,17 +664,17 @@ - => @"4" + => @"16" - => @"Preview.7" + => @"Preview.2" - => @"-Preview.7" + => @"-Preview.2" diff --git a/src/MigrationTools.Clients.AzureDevops.ObjectModel.Tests/MigrationTools.Clients.AzureDevops.ObjectModel.Tests.csproj b/src/MigrationTools.Clients.AzureDevops.ObjectModel.Tests/MigrationTools.Clients.AzureDevops.ObjectModel.Tests.csproj index 5e080088e..5d9ce7e47 100644 --- a/src/MigrationTools.Clients.AzureDevops.ObjectModel.Tests/MigrationTools.Clients.AzureDevops.ObjectModel.Tests.csproj +++ b/src/MigrationTools.Clients.AzureDevops.ObjectModel.Tests/MigrationTools.Clients.AzureDevops.ObjectModel.Tests.csproj @@ -13,8 +13,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/MigrationTools.Clients.AzureDevops.Rest.Tests/MigrationTools.Clients.AzureDevops.Rest.Tests.csproj b/src/MigrationTools.Clients.AzureDevops.Rest.Tests/MigrationTools.Clients.AzureDevops.Rest.Tests.csproj index 87b4321b5..1fa84dae1 100644 --- a/src/MigrationTools.Clients.AzureDevops.Rest.Tests/MigrationTools.Clients.AzureDevops.Rest.Tests.csproj +++ b/src/MigrationTools.Clients.AzureDevops.Rest.Tests/MigrationTools.Clients.AzureDevops.Rest.Tests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/MigrationTools.Clients.FileSystem.Tests/MigrationTools.Clients.FileSystem.Tests.csproj b/src/MigrationTools.Clients.FileSystem.Tests/MigrationTools.Clients.FileSystem.Tests.csproj index d98ce49cb..4558dd485 100644 --- a/src/MigrationTools.Clients.FileSystem.Tests/MigrationTools.Clients.FileSystem.Tests.csproj +++ b/src/MigrationTools.Clients.FileSystem.Tests/MigrationTools.Clients.FileSystem.Tests.csproj @@ -12,8 +12,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/MigrationTools.Clients.InMemory.Tests/MigrationTools.Clients.InMemory.Tests.csproj b/src/MigrationTools.Clients.InMemory.Tests/MigrationTools.Clients.InMemory.Tests.csproj index eef5132cf..51b90f725 100644 --- a/src/MigrationTools.Clients.InMemory.Tests/MigrationTools.Clients.InMemory.Tests.csproj +++ b/src/MigrationTools.Clients.InMemory.Tests/MigrationTools.Clients.InMemory.Tests.csproj @@ -12,8 +12,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/MigrationTools.ConsoleCore/Properties/launchSettings.json b/src/MigrationTools.ConsoleCore/Properties/launchSettings.json index 0ad86d657..cb8b11e02 100644 --- a/src/MigrationTools.ConsoleCore/Properties/launchSettings.json +++ b/src/MigrationTools.ConsoleCore/Properties/launchSettings.json @@ -1,6 +1,9 @@ { "profiles": { - "MigrationTools.ConsoleUI": { + "empty": { + "commandName": "Project" + }, + "execute ": { "commandName": "Project", "commandLineArgs": "execute --config \"configuration.json\"" } diff --git a/src/MigrationTools.ConsoleDataGenerator/MigrationTools.ConsoleDataGenerator.csproj b/src/MigrationTools.ConsoleDataGenerator/MigrationTools.ConsoleDataGenerator.csproj index cdb982d3c..d0e7d87e5 100644 --- a/src/MigrationTools.ConsoleDataGenerator/MigrationTools.ConsoleDataGenerator.csproj +++ b/src/MigrationTools.ConsoleDataGenerator/MigrationTools.ConsoleDataGenerator.csproj @@ -16,7 +16,7 @@ - + diff --git a/src/MigrationTools.ConsoleFull/Properties/launchSettings.json b/src/MigrationTools.ConsoleFull/Properties/launchSettings.json index 1e63b28b4..338a832c3 100644 --- a/src/MigrationTools.ConsoleFull/Properties/launchSettings.json +++ b/src/MigrationTools.ConsoleFull/Properties/launchSettings.json @@ -19,6 +19,9 @@ "init2": { "commandName": "Project", "commandLineArgs": "init -c configuration2.json --options Fullv2" + }, + "empty": { + "commandName": "Project" } } } \ No newline at end of file diff --git a/src/MigrationTools.Host.Tests/MigrationTools.Host.Tests.csproj b/src/MigrationTools.Host.Tests/MigrationTools.Host.Tests.csproj index ba9e76aae..757a1e4c5 100644 --- a/src/MigrationTools.Host.Tests/MigrationTools.Host.Tests.csproj +++ b/src/MigrationTools.Host.Tests/MigrationTools.Host.Tests.csproj @@ -17,8 +17,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/MigrationTools.Host/CommandLine/ExecuteOptions.cs b/src/MigrationTools.Host/CommandLine/ExecuteOptions.cs deleted file mode 100644 index 1a327176e..000000000 --- a/src/MigrationTools.Host/CommandLine/ExecuteOptions.cs +++ /dev/null @@ -1,32 +0,0 @@ -using CommandLine; - -namespace MigrationTools.Host.CommandLine -{ - [Verb("execute", HelpText = "Record changes to the repository.")] - public class ExecuteOptions - { - [Option('c', "config", Required = true, HelpText = "Configuration file to be processed.")] - public string ConfigFile { get; set; } - - [Option("sourceDomain", Required = false, HelpText = "Domain used to connect to the source TFS instance.")] - public string SourceDomain { get; set; } - - [Option("sourceUserName", Required = false, HelpText = "User Name used to connect to the source TFS instance.")] - public string SourceUserName { get; set; } - - [Option("sourcePassword", Required = false, HelpText = "Password used to connect to source TFS instance.")] - public string SourcePassword { get; set; } - - [Option("targetDomain", Required = false, HelpText = "Domain used to connect to the target TFS instance.")] - public string TargetDomain { get; set; } - - [Option("targetUserName", Required = false, HelpText = "User Name used to connect to the target TFS instance.")] - public string TargetUserName { get; set; } - - [Option("targetPassword", Required = false, HelpText = "Password used to connect to target TFS instance.")] - public string TargetPassword { get; set; } - - [Option("disableTelemetry", Required = false, HelpText = "Pass 'true' to turn temimetery off. No data will be colected.")] - public string DisableTelemetry { get; set; } - } -} \ No newline at end of file diff --git a/src/MigrationTools.Host/CommandLine/InitOptions.cs b/src/MigrationTools.Host/CommandLine/InitOptions.cs deleted file mode 100644 index 41ed4921d..000000000 --- a/src/MigrationTools.Host/CommandLine/InitOptions.cs +++ /dev/null @@ -1,14 +0,0 @@ -using CommandLine; - -namespace MigrationTools.Host.CommandLine -{ - [Verb("init", HelpText = "Creates initial config file")] - public class InitOptions - { - [Option('c', "config", Required = false, HelpText = "Configuration file to be processed.")] - public string ConfigFile { get; set; } - - [Option('o', "options", Required = false, Default = OptionsMode.Basic, HelpText = "Configuration file to be generated: Basic, Reference, WorkItemTracking, Fullv2, WorkItemTrackingv2")] - public OptionsMode Options { get; set; } - } -} \ No newline at end of file diff --git a/src/MigrationTools.Host/CommandLine/OptionsMode.cs b/src/MigrationTools.Host/CommandLine/OptionsMode.cs deleted file mode 100644 index 5e2feb3e4..000000000 --- a/src/MigrationTools.Host/CommandLine/OptionsMode.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace MigrationTools.Host.CommandLine -{ - public enum OptionsMode - { - Reference = 0, - WorkItemTracking = 1, - Fullv2 = 2, - WorkItemTrackingv2 = 3, - Basic = 4 - } -} \ No newline at end of file diff --git a/src/MigrationTools.Host/Commands/CommandBase.cs b/src/MigrationTools.Host/Commands/CommandBase.cs new file mode 100644 index 000000000..29dfc42e3 --- /dev/null +++ b/src/MigrationTools.Host/Commands/CommandBase.cs @@ -0,0 +1,176 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using MigrationTools.Host.Services; +using Serilog; +using Spectre.Console; +using Spectre.Console.Cli; + +namespace MigrationTools.Host.Commands +{ + internal abstract class CommandBase : AsyncCommand where TSettings : CommandSettingsBase + { + private readonly IHostApplicationLifetime _LifeTime; + private readonly IDetectOnlineService _detectOnlineService; + private readonly IDetectVersionService2 _detectVersionService; + private readonly ILogger> _logger; + private readonly ITelemetryLogger _telemetryLogger; + private static Stopwatch _mainTimer = new Stopwatch(); + + public CommandBase(IHostApplicationLifetime appLifetime, IDetectOnlineService detectOnlineService, IDetectVersionService2 detectVersionService, ILogger> logger, ITelemetryLogger telemetryLogger) + { + _LifeTime = appLifetime; + _detectOnlineService = detectOnlineService; + _detectVersionService = detectVersionService; + _logger = logger; + _telemetryLogger = telemetryLogger; + } + + public override async Task ExecuteAsync(CommandContext context, TSettings settings) + { + _mainTimer.Start(); + _logger.LogTrace("Starting {CommandName}", this.GetType().Name); + _telemetryLogger.TrackEvent(this.GetType().Name); + RunStartupLogic(settings); + try + { + return await ExecuteInternalAsync(context, settings); + } + catch (Exception ex) + { + _telemetryLogger.TrackException(ex, null, null); + _logger.LogError(ex, "Unhandled exception!"); + return 1; + } + finally + { + _LifeTime.StopApplication(); + _mainTimer.Stop(); + _logger.LogInformation("Command {CommandName} completed in {Elapsed}", this.GetType().Name, _mainTimer.Elapsed); + } + } + + internal virtual async Task ExecuteInternalAsync(CommandContext context, TSettings settings) + { + // no-op + return 0; + } + + public void RunStartupLogic(TSettings settings) + { + ApplicationStartup(settings); + if (!settings.skipVersionCheck && _detectOnlineService.IsOnline()) + { + _logger.LogTrace("Package Management Info:"); + Log.Debug(" IsPackageManagerInstalled: {IsPackageManagerInstalled}", _detectVersionService.IsPackageManagerInstalled); + Log.Debug(" IsPackageInstalled: {IsPackageInstalled}", _detectVersionService.IsPackageInstalled); + Log.Debug(" IsUpdateAvailable: {IsUpdateAvailable}", _detectVersionService.IsUpdateAvailable); + Log.Debug(" IsNewLocalVersionAvailable: {IsNewLocalVersionAvailable}", _detectVersionService.IsNewLocalVersionAvailable); + Log.Debug(" IsRunningInDebug: {IsRunningInDebug}", _detectVersionService.IsRunningInDebug); + Log.Verbose("Full version data: ${_detectVersionService}", _detectVersionService); + + Log.Information("Verion Info:"); + Log.Information(" Running: {RunningVersion}", _detectVersionService.RunningVersion); + Log.Information(" Installed: {InstalledVersion}", _detectVersionService.InstalledVersion); + Log.Information(" Available: {AvailableVersion}", _detectVersionService.AvailableVersion); + + if (_detectVersionService.RunningVersion.Major == 0) + { + Log.Information("Git Info:"); + Log.Information(" Repo: {GitRepositoryUrl}", ThisAssembly.Git.RepositoryUrl); + Log.Information(" Tag: {GitTag}", ThisAssembly.Git.Tag); + Log.Information(" Branch: {GitBranch}", ThisAssembly.Git.Branch); + Log.Information(" Commits: {GitCommits}", ThisAssembly.Git.Commits); + + } + + if (!_detectVersionService.IsPackageManagerInstalled) + { + Log.Warning("Windows Client: The Windows Package Manager is not installed, we use it to determine if you have the latest version, and to make sure that this application is up to date. You can download and install it from https://aka.ms/getwinget. After which you can call `winget install {PackageId}` from the Windows Terminal to get a manged version of this program.", _detectVersionService.PackageId); + Log.Warning("Windows Server: If you are running on Windows Server you can use the experimental version of Winget, or you can still use Chocolatey to manage the install. Install chocolatey from https://chocolatey.org/install and then use `choco install vsts-sync-migrator` to install, and `choco upgrade vsts-sync-migrator` to upgrade to newer versions.", _detectVersionService.PackageId); + } + else + { + if (!_detectVersionService.IsRunningInDebug) + { + if (!_detectVersionService.IsPackageInstalled) + { + Log.Information("It looks like this application has been installed from a zip, would you like to use the managed version?"); + Console.WriteLine("Do you want exit and install the managed version? (y/n)"); + if (Console.ReadKey().Key == ConsoleKey.Y) + { + Thread.Sleep(2000); + Environment.Exit(0); + } + } + if (_detectVersionService.IsUpdateAvailable && _detectVersionService.IsPackageInstalled) + { + Log.Information("It looks like an updated version is available from Winget, would you like to exit and update?"); + Console.WriteLine("Do you want to exit and update? (y/n)"); + if (Console.ReadKey().Key == ConsoleKey.Y) + { + Thread.Sleep(2000); + Environment.Exit(0); + } + } + } + else + { + Log.Information("Running in Debug! No further version checkes....."); + } + } + } + else + { + /// not online or you have specified not to + Log.Warning("You are either not online or have chosen `skipVersionCheck`. We will not check for a newer version of the tools.", _detectVersionService.PackageId); + } + } + + + private void ApplicationStartup( TSettings settings) + { + _mainTimer.Start(); + AsciiLogo(DetectVersionService2.GetRunningVersion().versionString); + TelemetryNote(); + _logger.LogInformation("Start Time: {StartTime}", DateTime.Now.ToUniversalTime().ToLocalTime()); + _logger.LogInformation("Running with settings: {@settings}", settings); + _logger.LogInformation("OSVersion: {OSVersion}", Environment.OSVersion.ToString()); + _logger.LogInformation("Version (Assembly): {Version}", DetectVersionService2.GetRunningVersion().versionString); + } + + private void TelemetryNote() + { + _logger.LogInformation("Telemetry Note:"); + _logger.LogInformation(" We use Application Insights to collect usage and error information in order to improve the quality of the tools."); + _logger.LogInformation(" Currently we collect the following anonymous data:"); + _logger.LogInformation(" -Event data: application version, client city/country, hosting type, item count, error count, warning count, elapsed time."); + _logger.LogInformation(" -Exceptions: application errors and warnings."); + _logger.LogInformation(" -Dependencies: REST/ObjectModel calls to Azure DevOps to help us understand performance issues."); + _logger.LogInformation(" This data is tied to a session ID that is generated on each run of the application and shown in the logs. This can help with debugging. If you want to disable telemetry you can run the tool with '--disableTelemetry' on the command prompt."); + _logger.LogInformation(" Note: Exception data cannot be 100% guaranteed to not leak production data"); + _logger.LogInformation("--------------------------------------"); + } + + private void AsciiLogo(string thisVersion) + { + AnsiConsole.Write(new FigletText("Azure DevOps").LeftJustified().Color(Color.Purple)); + AnsiConsole.Write(new FigletText("Migration Tools").LeftJustified().Color(Color.Purple)); + var productName = ((AssemblyProductAttribute)Assembly.GetEntryAssembly() + .GetCustomAttributes(typeof(AssemblyProductAttribute), true)[0]).Product; + _logger.LogInformation("{productName} ", productName); + _logger.LogInformation("{thisVersion}", thisVersion); + var companyName = ((AssemblyCompanyAttribute)Assembly.GetEntryAssembly() + .GetCustomAttributes(typeof(AssemblyCompanyAttribute), true)[0]).Company; + _logger.LogInformation("{companyName} ", companyName); + _logger.LogInformation("==============================================================================="); + } + } +} diff --git a/src/MigrationTools.Host/Commands/CommandSettingsBase.cs b/src/MigrationTools.Host/Commands/CommandSettingsBase.cs new file mode 100644 index 000000000..fe6071e9b --- /dev/null +++ b/src/MigrationTools.Host/Commands/CommandSettingsBase.cs @@ -0,0 +1,40 @@ +using System.ComponentModel; +using Newtonsoft.Json; +using Spectre.Console.Cli; +using YamlDotNet.Serialization; +using System.CommandLine; + +namespace MigrationTools.Host.Commands +{ + internal class CommandSettingsBase : CommandSettings + { + [Description("Pre configure paramiters using this config file. Run `Init` to create it.")] + [CommandOption("--config|--configFile|-c")] + [DefaultValue("configuration.json")] + [JsonIgnore, YamlIgnore] + public string ConfigFile { get; set; } + + [Description("Add this paramiter to turn Telemetry off")] + [CommandOption("--disableTelemetry")] + public bool DisableTelemetry { get; set; } + + [Description("Add this paramiter to turn version check off")] + [CommandOption("--skipVersionCheck")] + public bool skipVersionCheck { get; set; } + + public static string ForceGetConfigFile(string[] args) + { + var fileOption = new Option("--config"); + fileOption.AddAlias("-c"); + fileOption.AddAlias("--configFile"); + + var rootCommand = new RootCommand(); + rootCommand.AddOption(fileOption); + + var file = rootCommand.Parse(args); + return file.GetValueForOption(fileOption); + + } + } + +} diff --git a/src/MigrationTools.Host/Commands/ExecuteMigrationCommand.cs b/src/MigrationTools.Host/Commands/ExecuteMigrationCommand.cs new file mode 100644 index 000000000..2ef1afa75 --- /dev/null +++ b/src/MigrationTools.Host/Commands/ExecuteMigrationCommand.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using MigrationTools.Host.Commands; +using MigrationTools.Host.Services; +using Spectre.Console.Cli; + +namespace MigrationTools.Host.Commands +{ + internal class ExecuteMigrationCommand : CommandBase + { + private readonly IServiceProvider _services; + private readonly ILogger _logger; + private readonly IHostApplicationLifetime _appLifetime; + private readonly ITelemetryLogger Telemetery; + + public ExecuteMigrationCommand(IServiceProvider services, + ILogger logger, + IHostApplicationLifetime appLifetime, ITelemetryLogger telemetryLogger, IDetectOnlineService detectOnlineService, IDetectVersionService2 detectVersionService) : base(appLifetime, detectOnlineService, detectVersionService, logger, telemetryLogger) + { + Telemetery = telemetryLogger; + _services = services; + _logger = logger; + _appLifetime = appLifetime; + } + + internal override async Task ExecuteInternalAsync(CommandContext context, ExecuteMigrationCommandSettings settings) + { + int _exitCode; + try + { + var migrationEngine = _services.GetRequiredService(); + migrationEngine.Run(); + _exitCode = 0; + } + catch (Exception ex) + { + Telemetery.TrackException(ex, null, null); + _logger.LogError(ex, "Unhandled exception!"); + _exitCode = 1; + } + finally + { + // Stop the application once the work is done + _appLifetime.StopApplication(); + } + return _exitCode; + } + } +} + diff --git a/src/MigrationTools.Host/Commands/ExecuteMigrationCommandSettings.cs b/src/MigrationTools.Host/Commands/ExecuteMigrationCommandSettings.cs new file mode 100644 index 000000000..54f275418 --- /dev/null +++ b/src/MigrationTools.Host/Commands/ExecuteMigrationCommandSettings.cs @@ -0,0 +1,33 @@ +using System.ComponentModel; +using Microsoft.VisualStudio.Services.Common.CommandLine; +using Spectre.Console.Cli; + +namespace MigrationTools.Host.Commands +{ + internal class ExecuteMigrationCommandSettings : CommandSettingsBase + { + [Description("Domain used to connect to the source TFS instance.")] + [CommandOption("--sourceDomain")] + public string SourceDomain { get; set; } + + [Description("User Name used to connect to the source TFS instance.")] + [CommandOption("--sourceUserName")] + public string SourceUserName { get; set; } + + [Description("Password used to connect to source TFS instance.")] + [CommandOption("--sourcePassword")] + public string SourcePassword { get; set; } + + [Description("Domain used to connect to the target TFS instance.")] + [CommandOption("--targetDomain")] + public string TargetDomain { get; set; } + + [Description("User Name used to connect to the target TFS instance.")] + [CommandOption("--targetUserName")] + public string TargetUserName { get; set; } + + [Description("Password used to connect to target TFS instance.")] + [CommandOption("--targetPassword")] + public string TargetPassword { get; set; } + } +} \ No newline at end of file diff --git a/src/MigrationTools.Host/Commands/InitMigrationCommand.cs b/src/MigrationTools.Host/Commands/InitMigrationCommand.cs new file mode 100644 index 000000000..4beb5730d --- /dev/null +++ b/src/MigrationTools.Host/Commands/InitMigrationCommand.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.ApplicationInsights.DataContracts; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MigrationTools._EngineV1.Configuration; +using Spectre.Console.Cli; + +namespace MigrationTools.Host.Commands +{ + internal class InitMigrationCommand : AsyncCommand + { + private readonly IEngineConfigurationBuilder _configurationBuilder; + private readonly ISettingsWriter _settingWriter; + private readonly ILogger _logger; + private readonly ITelemetryLogger Telemetery; + private readonly IHostApplicationLifetime _appLifetime; + + public InitMigrationCommand( + IEngineConfigurationBuilder configurationBuilder, + ISettingsWriter settingsWriter, + ILogger logger, + ITelemetryLogger telemetryLogger, + IHostApplicationLifetime appLifetime) + { + _configurationBuilder = configurationBuilder; + _settingWriter = settingsWriter; + _logger = logger; + Telemetery = telemetryLogger; + _appLifetime = appLifetime; + } + + + public override async Task ExecuteAsync(CommandContext context, InitMigrationCommandSettings settings) + { + int _exitCode; + try + { + Telemetery.TrackEvent(new EventTelemetry("InitCommand")); + string configFile = settings.ConfigFile; + if (string.IsNullOrEmpty(configFile)) + { + configFile = "configuration.json"; + } + _logger.LogInformation("ConfigFile: {configFile}", configFile); + if (File.Exists(configFile)) + { + _logger.LogInformation("Deleting old configuration.json reference file"); + File.Delete(configFile); + } + if (!File.Exists(configFile)) + { + _logger.LogInformation("Populating config with {Options}", settings.Options.ToString()); + EngineConfiguration config; + switch (settings.Options) + { + case OptionsMode.Reference: + config = _configurationBuilder.BuildReference(); + break; + case OptionsMode.Basic: + config = _configurationBuilder.BuildGettingStarted(); + break; + + case OptionsMode.WorkItemTracking: + config = _configurationBuilder.BuildWorkItemMigration(); + break; + + case OptionsMode.Fullv2: + config = _configurationBuilder.BuildDefault2(); + break; + + case OptionsMode.WorkItemTrackingv2: + config = _configurationBuilder.BuildWorkItemMigration2(); + break; + + default: + config = _configurationBuilder.BuildGettingStarted(); + break; + } + _settingWriter.WriteSettings(config, configFile); + _logger.LogInformation($"New {configFile} file has been created"); + } + _exitCode = 0; + } + catch (Exception ex) + { + Telemetery.TrackException(ex, null, null); + _logger.LogError(ex, "Unhandled exception!"); + _exitCode = 1; + } + finally + { + // Stop the application once the work is done + _appLifetime.StopApplication(); + } + return _exitCode; + } + } +} diff --git a/src/MigrationTools.Host/Commands/InitMigrationCommandSettings.cs b/src/MigrationTools.Host/Commands/InitMigrationCommandSettings.cs new file mode 100644 index 000000000..19245b65a --- /dev/null +++ b/src/MigrationTools.Host/Commands/InitMigrationCommandSettings.cs @@ -0,0 +1,22 @@ +using System.ComponentModel; +using Spectre.Console.Cli; + +namespace MigrationTools.Host.Commands +{ + internal class InitMigrationCommandSettings : CommandSettingsBase + { + [Description("What type of config do you want to output? WorkItemTracking is the default.")] + [CommandOption("--outputMode|--options")] + [DefaultValue(OptionsMode.WorkItemTracking)] + public OptionsMode Options { get; set; } + } + + public enum OptionsMode + { + Reference = 0, + WorkItemTracking = 1, + Fullv2 = 2, + WorkItemTrackingv2 = 3, + Basic = 4 + } +} \ No newline at end of file diff --git a/src/MigrationTools.Host/ExecuteHostedService.cs b/src/MigrationTools.Host/ExecuteHostedService.cs deleted file mode 100644 index ef7e59137..000000000 --- a/src/MigrationTools.Host/ExecuteHostedService.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.ApplicationInsights.Channel; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -namespace MigrationTools.Host -{ - public class ExecuteHostedService : IHostedService - { - private readonly IServiceProvider _services; - private readonly ILogger _logger; - private readonly IHostApplicationLifetime _appLifetime; - private readonly ITelemetryLogger Telemetery; - - private int? _exitCode; - - public ExecuteHostedService( - IServiceProvider services, - ILogger logger, - IHostApplicationLifetime appLifetime, ITelemetryLogger telemetryLogger) - { - Telemetery = telemetryLogger; - _services = services; - _logger = logger; - _appLifetime = appLifetime; - } - - public Task StartAsync(CancellationToken cancellationToken) - { - _appLifetime.ApplicationStarted.Register(() => - { - Task.Run(() => - { - try - { - var migrationEngine = _services.GetRequiredService(); - migrationEngine.Run(); - _exitCode = 0; - } - catch (Exception ex) - { - Telemetery.TrackException(ex, null, null); - _logger.LogError(ex, "Unhandled exception!"); - _exitCode = 1; - } - finally - { - // Stop the application once the work is done - _appLifetime.StopApplication(); - } - }); - }); - - return Task.CompletedTask; - } - - public Task StopAsync(CancellationToken cancellationToken) - { - _logger.LogDebug($"Exiting with return code: {_exitCode}"); - - if (_exitCode.HasValue) - { - Environment.ExitCode = _exitCode.Value; - } - return Task.CompletedTask; - } - } -} \ No newline at end of file diff --git a/src/MigrationTools.Host/HostExtensions.cs b/src/MigrationTools.Host/HostExtensions.cs index ac26aace5..9e07b8294 100644 --- a/src/MigrationTools.Host/HostExtensions.cs +++ b/src/MigrationTools.Host/HostExtensions.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using MigrationTools.Host.CommandLine; namespace MigrationTools.Host { diff --git a/src/MigrationTools.Host/InitHostedService.cs b/src/MigrationTools.Host/InitHostedService.cs deleted file mode 100644 index 0140ff544..000000000 --- a/src/MigrationTools.Host/InitHostedService.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.ApplicationInsights.DataContracts; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using MigrationTools._EngineV1.Configuration; -using MigrationTools.Host.CommandLine; - -namespace MigrationTools.Host -{ - public class InitHostedService : IHostedService - { - private readonly IEngineConfigurationBuilder _configurationBuilder; - private readonly ISettingsWriter _settingWriter; - private readonly InitOptions _initOptions; - private readonly ILogger _logger; - private readonly ITelemetryLogger _telemetryLogger; - private readonly IHostApplicationLifetime _appLifetime; - private int? _exitCode; - - public InitHostedService( - IEngineConfigurationBuilder configurationBuilder, - ISettingsWriter settingsWriter, - IOptions initOptions, - ILogger logger, - ITelemetryLogger telemetryLogger, - IHostApplicationLifetime appLifetime) - { - _configurationBuilder = configurationBuilder; - _settingWriter = settingsWriter; - _initOptions = initOptions.Value; - _logger = logger; - _telemetryLogger = telemetryLogger; - _appLifetime = appLifetime; - } - - public Task StartAsync(CancellationToken cancellationToken) - { - _appLifetime.ApplicationStarted.Register(() => - { - Task.Run(() => - { - try - { - _telemetryLogger.TrackEvent(new EventTelemetry("InitCommand")); - string configFile = _initOptions.ConfigFile; - if (string.IsNullOrEmpty(configFile)) - { - configFile = "configuration.json"; - } - _logger.LogInformation("ConfigFile: {configFile}", configFile); - if (File.Exists(configFile)) - { - _logger.LogInformation("Deleting old configuration.json reference file"); - File.Delete(configFile); - } - if (!File.Exists(configFile)) - { - _logger.LogInformation("Populating config with {Options}", _initOptions.Options.ToString()); - EngineConfiguration config; - switch (_initOptions.Options) - { - case OptionsMode.Reference: - config = _configurationBuilder.BuildReference(); - break; - case OptionsMode.Basic: - config = _configurationBuilder.BuildGettingStarted(); - break; - - case OptionsMode.WorkItemTracking: - config = _configurationBuilder.BuildWorkItemMigration(); - break; - - case OptionsMode.Fullv2: - config = _configurationBuilder.BuildDefault2(); - break; - - case OptionsMode.WorkItemTrackingv2: - config = _configurationBuilder.BuildWorkItemMigration2(); - break; - - default: - config = _configurationBuilder.BuildGettingStarted(); - break; - } - _settingWriter.WriteSettings(config, configFile); - _logger.LogInformation($"New {configFile} file has been created"); - } - _exitCode = 0; - } - catch (Exception ex) - { - _telemetryLogger.TrackException(ex, null, null); - _logger.LogError(ex, "Unhandled exception!"); - _exitCode = 1; - } - finally - { - // Stop the application once the work is done - _appLifetime.StopApplication(); - } - }); - }); - return Task.CompletedTask; - } - - public Task StopAsync(CancellationToken cancellationToken) - { - _logger.LogDebug($"Exiting with return code: {_exitCode}"); - if (_exitCode.HasValue) - { - Environment.ExitCode = _exitCode.Value; - } - return Task.CompletedTask; - } - } -} \ No newline at end of file diff --git a/src/MigrationTools.Host/MigrationService.cs b/src/MigrationTools.Host/MigrationService.cs new file mode 100644 index 000000000..d4b2f69ff --- /dev/null +++ b/src/MigrationTools.Host/MigrationService.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Serilog.Core; +using Spectre.Console.Cli; + +namespace MigrationTools.Host +{ + internal class MigrationService : BackgroundService + { + private ICommandApp AppCommand { get; } + private IHostApplicationLifetime AppLifetime { get; } + private ILogger Logger { get; } + + public MigrationService(ICommandApp appCommand, IHostApplicationLifetime appLifetime, ILogger logger) + { + AppCommand = appCommand; + AppLifetime = appLifetime; + Logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await AppCommand.RunAsync(Environment.GetCommandLineArgs().Skip(1)); + AppLifetime.StopApplication(); + } + } +} diff --git a/src/MigrationTools.Host/MigrationToolHost.cs b/src/MigrationTools.Host/MigrationToolHost.cs index acbc9869f..c20b79e62 100644 --- a/src/MigrationTools.Host/MigrationToolHost.cs +++ b/src/MigrationTools.Host/MigrationToolHost.cs @@ -2,7 +2,6 @@ using System.IO; using System.Reflection; using System.Threading.Tasks; -using CommandLine; using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.Extensibility; using Microsoft.ApplicationInsights.WorkerService; @@ -12,7 +11,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using MigrationTools._EngineV1.Configuration; -using MigrationTools.Host.CommandLine; using MigrationTools.Host.CustomDiagnostics; using MigrationTools.Host.Services; using MigrationTools.Options; @@ -20,70 +18,76 @@ using Serilog.Core; using Serilog.Events; using Serilog.Sinks.SystemConsole.Themes; +using Spectre.Console.Cli.Extensions.DependencyInjection; +using Spectre.Console.Cli; +using Serilog.Filters; +using MigrationTools.Host.Commands; +using System.Diagnostics; +using System.Text.RegularExpressions; namespace MigrationTools.Host { public static class MigrationToolHost { + static int logs = 1; + public static IHostBuilder CreateDefaultBuilder(string[] args) { - (var initOptions, var executeOptions) = ParseOptions(args); - - if (initOptions is null && executeOptions is null) - { - return null; - } - + var configFile = CommandSettingsBase.ForceGetConfigFile(args); + var hostBuilder = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder(args); - var hostBuilder = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder(args) - .UseSerilog((hostingContext, services, loggerConfiguration) => - { - string outputTemplate = "[{Timestamp:HH:mm:ss} {Level:u3}] [" + GetVersionTextForLog() + "] {Message:lj}{NewLine}{Exception}"; - string logsPath = CreateLogsPath(); - var logPath = Path.Combine(logsPath, "migration.log"); - var logLevel = hostingContext.Configuration.GetValue("LogLevel"); - var levelSwitch = new LoggingLevelSwitch(logLevel); - loggerConfiguration - .MinimumLevel.ControlledBy(levelSwitch) - .ReadFrom.Configuration(hostingContext.Configuration) - .Enrich.FromLogContext() - .Enrich.WithMachineName() - .Enrich.WithProcessId() - .WriteTo.Console(restrictedToMinimumLevel: LogEventLevel.Debug, theme: AnsiConsoleTheme.Code, outputTemplate: outputTemplate) - .WriteTo.ApplicationInsights(services.GetService(), new CustomConverter(), LogEventLevel.Error) - .WriteTo.File(logPath, LogEventLevel.Verbose, outputTemplate: outputTemplate); - }) - .ConfigureLogging((context, logBuilder) => - { - }) - .ConfigureAppConfiguration(builder => + hostBuilder.UseSerilog((hostingContext, services, loggerConfiguration) => + { + string outputTemplate = "[{Timestamp:HH:mm:ss} {Level:u3}] [" + DetectVersionService2.GetRunningVersion().versionString + "] {Message:lj}{NewLine}{Exception}"; // {SourceContext} + string logsPath = CreateLogsPath(); + var logPath = Path.Combine(logsPath, $"migration{logs}.log"); + + var logLevel = hostingContext.Configuration.GetValue("LogLevel"); + var levelSwitch = new LoggingLevelSwitch(logLevel); + loggerConfiguration + .MinimumLevel.ControlledBy(levelSwitch) + .ReadFrom.Configuration(hostingContext.Configuration) + .Enrich.FromLogContext() + .Enrich.WithMachineName() + .Enrich.WithProcessId() + .WriteTo.File(logPath, LogEventLevel.Verbose, outputTemplate) + .WriteTo.Logger(lc => lc + .Filter.ByExcluding(Matching.FromSource("Microsoft")) + .Filter.ByExcluding(Matching.FromSource("MigrationTools.Host.StartupService")) + .WriteTo.Console(restrictedToMinimumLevel: LogEventLevel.Debug, theme: AnsiConsoleTheme.Code, outputTemplate: outputTemplate)) + .WriteTo.Logger(lc => lc + .Filter.ByExcluding(Matching.FromSource("Microsoft")) + .WriteTo.ApplicationInsights(services.GetService(), new CustomConverter(), LogEventLevel.Error)); + logs++; + }); + + hostBuilder.ConfigureLogging((context, logBuilder) => { - if (executeOptions is not null) - { - builder.AddJsonFile(executeOptions.ConfigFile); - } }) - .ConfigureServices((context, services) => + .ConfigureAppConfiguration(builder => + { + if (!string.IsNullOrEmpty(configFile) && File.Exists(configFile)) + { + builder.AddJsonFile(configFile); + } + }); + + hostBuilder.ConfigureServices((context, services) => { services.AddOptions(); services.Configure((config) => { - if(executeOptions is null) - { - return; - } - var sp = services.BuildServiceProvider(); var logger = sp.GetService().CreateLogger(); - if (!File.Exists(executeOptions.ConfigFile)) + if (!File.Exists(configFile)) { - logger.LogInformation("The config file {ConfigFile} does not exist, nor does the default 'configuration.json'. Use '{ExecutableName}.exe init' to create a configuration file first", executeOptions.ConfigFile, Assembly.GetEntryAssembly().GetName().Name); + logger.LogInformation("The config file {ConfigFile} does not exist, nor does the default 'configuration.json'. Use '{ExecutableName}.exe init' to create a configuration file first", configFile, Assembly.GetEntryAssembly().GetName().Name); throw new ArgumentException("missing configfile"); } logger.LogInformation("Config Found, creating engine host"); var reader = sp.GetRequiredService(); - var parsed = reader.BuildFromFile(executeOptions.ConfigFile); + var parsed = reader.BuildFromFile(configFile); config.ChangeSetMappingFile = parsed.ChangeSetMappingFile; config.FieldMaps = parsed.FieldMaps; config.GitRepoMapping = parsed.GitRepoMapping; @@ -96,15 +100,16 @@ public static IHostBuilder CreateDefaultBuilder(string[] args) config.WorkItemTypeDefinition = parsed.WorkItemTypeDefinition; }); + // Application Insights ApplicationInsightsServiceOptions aiso = new ApplicationInsightsServiceOptions(); aiso.ApplicationVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString(); - aiso.ConnectionString = "InstrumentationKey=2d666f84-b3fb-4dcf-9aad-65de038d2772"; + aiso.ConnectionString = "InstrumentationKey=2d666f84-b3fb-4dcf-9aad-65de038d2772;IngestionEndpoint=https://northeurope-0.in.applicationinsights.azure.com/;LiveEndpoint=https://northeurope.livediagnostics.monitor.azure.com/;ApplicationId=9146fe72-5c18-48d7-a0f2-8fb891ef1277"; //# if DEBUG //aiso.DeveloperMode = true; //#endif services.AddApplicationInsightsTelemetryWorkerService(aiso); - + // Services services.AddTransient(); //services.AddTransient(); @@ -122,46 +127,33 @@ public static IHostBuilder CreateDefaultBuilder(string[] args) // Host Services services.AddTransient(); - if (initOptions is not null) - { - services.Configure((opts) => - { - opts.ConfigFile = initOptions.ConfigFile; - opts.Options = initOptions.Options; - }); - services.AddHostedService(); - } - if (executeOptions is not null) - { - services.Configure(cred => - { - cred.Source = new Credentials - { - Domain = executeOptions.SourceDomain, - UserName = executeOptions.SourceUserName, - Password = executeOptions.SourcePassword - }; - cred.Target = new Credentials - { - Domain = executeOptions.TargetDomain, - UserName = executeOptions.TargetUserName, - Password = executeOptions.TargetPassword - }; - }); - services.AddHostedService(); - } - }) - .UseConsoleLifetime(); + }); - return hostBuilder; - } + hostBuilder.ConfigureServices((context, services) => + { + using var registrar = new DependencyInjectionRegistrar(services); + var app = new CommandApp(registrar); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddCommand("execute"); + config.AddCommand("init"); + + }); + services.AddSingleton(app); + }); + + hostBuilder.ConfigureServices((context, services) => + { + services.AddHostedService(); + }); - private static string GetVersionTextForLog() - { - Version runningVersion = DetectVersionService2.GetRunningVersion().version; - string textVersion = "v" + DetectVersionService2.GetRunningVersion().version + "-" + DetectVersionService2.GetRunningVersion().PreReleaseLabel; - return textVersion; + hostBuilder.UseConsoleLifetime(); + + + + return hostBuilder; } public static async Task RunMigrationTools(this IHostBuilder hostBuilder, string[] args) @@ -175,28 +167,25 @@ public static async Task RunMigrationTools(this IHostBuilder hostBuilder, string // Disanle telemitery from options - (var initOptions, var executeOptions) = ParseOptions(args); - if (initOptions is null && executeOptions is null) - { - return; - } - bool DisableTelemetry = false; - Serilog.ILogger logger = host.Services.GetService(); - if (executeOptions is not null && bool.TryParse(executeOptions.DisableTelemetry, out DisableTelemetry)) - { - TelemetryConfiguration ai = host.Services.GetService(); - ai.DisableTelemetry = DisableTelemetry; - } - logger.Information("Telemetry: {status}", !DisableTelemetry); + //bool DisableTelemetry = false; + //Serilog.ILogger logger = host.Services.GetService(); + //if (executeOptions is not null && bool.TryParse(executeOptions.DisableTelemetry, out DisableTelemetry)) + //{ + // TelemetryConfiguration ai = host.Services.GetService(); + // ai.DisableTelemetry = DisableTelemetry; + //} + //logger.Information("Telemetry: {status}", !DisableTelemetry); await host.RunAsync(); } + static string logDate = DateTime.Now.ToString("yyyyMMddHHmmss"); + private static string CreateLogsPath() { string exportPath; string assPath = Assembly.GetEntryAssembly().Location; - exportPath = Path.Combine(Path.GetDirectoryName(assPath), "logs", DateTime.Now.ToString("yyyyMMddHHmmss")); + exportPath = Path.Combine(Path.GetDirectoryName(assPath), "logs", logDate); if (!Directory.Exists(exportPath)) { Directory.CreateDirectory(exportPath); @@ -204,21 +193,5 @@ private static string CreateLogsPath() return exportPath; } - - private static (InitOptions init, ExecuteOptions execute) ParseOptions(string[] args) - { - InitOptions initOptions = null; - ExecuteOptions executeOptions = null; - Parser.Default.ParseArguments(args) - .WithParsed(opts => - { - initOptions = opts; - }) - .WithParsed(opts => - { - executeOptions = opts; - }); - return (initOptions, executeOptions); - } } } \ No newline at end of file diff --git a/src/MigrationTools.Host/MigrationTools.Host.csproj b/src/MigrationTools.Host/MigrationTools.Host.csproj index 943535143..966f7654b 100644 --- a/src/MigrationTools.Host/MigrationTools.Host.csproj +++ b/src/MigrationTools.Host/MigrationTools.Host.csproj @@ -13,7 +13,6 @@ - all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -28,11 +27,15 @@ - + + + + + diff --git a/src/MigrationTools.Host/Services/DetectVersionService2.cs b/src/MigrationTools.Host/Services/DetectVersionService2.cs index e2d1937c4..b11300eb0 100644 --- a/src/MigrationTools.Host/Services/DetectVersionService2.cs +++ b/src/MigrationTools.Host/Services/DetectVersionService2.cs @@ -175,7 +175,20 @@ public static (Version version, string PreReleaseLabel, string versionString) Ge FileVersionInfo myFileVersionInfo = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly()?.Location); var matches = Regex.Matches(myFileVersionInfo.ProductVersion, @"^(?0|[1-9]\d*)\.(?0|[1-9]\d*)\.(?0|[1-9]\d*)(?:-((?