From 1fdf211f42c665bb5a3a173e07240aa316c02ffa Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Tue, 25 Jun 2024 16:43:35 +0100 Subject: [PATCH 1/3] Add Mac and Linux support to dfu install. Refactor to centralise Process launching and use more modern async pattern. --- .../Commands/Current/Dfu/DfuInstallCommand.cs | 36 +- Source/v2/Meadow.Dfu/DfuUtils.cs | 554 ++++++++++++++---- .../Connection/MeadowConnectionManager.cs | 4 +- 3 files changed, 449 insertions(+), 145 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/Dfu/DfuInstallCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Dfu/DfuInstallCommand.cs index da88b689..5ab96018 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Dfu/DfuInstallCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Dfu/DfuInstallCommand.cs @@ -10,8 +10,6 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("dfu install", Description = "Install dfu-util to the host operating system")] public class DfuInstallCommand : BaseSettingsCommand { - public const string DefaultVersion = "0.11"; - [CommandOption("version", 'v', IsRequired = false)] public string? Version { get; set; } @@ -27,7 +25,7 @@ public DfuInstallCommand(ISettingsManager settingsManager, ILoggerFactory logger protected override async ValueTask ExecuteCommand() { - Version ??= DefaultVersion; + Version ??= DfuUtils.DEFAULT_DFU_VERSION; switch (Version) { @@ -40,32 +38,30 @@ protected override async ValueTask ExecuteCommand() return; } - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + bool successfullyInstalled = false; + try { - if (IsAdministrator()) + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - try - { - await DfuUtils.InstallDfuUtil(FileManager.WildernessTempFolderPath, Version, CancellationToken); - } - catch (Exception ex) - { - throw new CommandException($"Failed to install DFU {Version}: " + ex.Message); - } - Logger?.LogInformation($"DFU {Version} installed successfully"); + successfullyInstalled = await DfuUtils.CheckIfDfuUtilIsInstalledOnWindows(FileManager.WildernessTempFolderPath, Version, CancellationToken); } - else + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - Logger?.LogError("To install DFU on Windows, you'll need to run the command as an Administrator"); + successfullyInstalled = await DfuUtils.CheckIfDfuUtilIsInstalledOnMac(Version, CancellationToken); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + successfullyInstalled = await DfuUtils.CheckIfDfuUtilIsInstalledOnLinux(Version, CancellationToken); } } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + catch (Exception ex) { - Logger?.LogWarning("To install DFU on macOS, run: brew install dfu-util"); + throw new CommandException($"Failed to install DFU {Version}: " + ex.Message); } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + + if (successfullyInstalled) { - Logger?.LogWarning("To install DFU on Linux, use the package manager to install the dfu-util package"); + Logger?.LogInformation($"DFU Is installed"); } } diff --git a/Source/v2/Meadow.Dfu/DfuUtils.cs b/Source/v2/Meadow.Dfu/DfuUtils.cs index 19f363ff..b83e0bec 100644 --- a/Source/v2/Meadow.Dfu/DfuUtils.cs +++ b/Source/v2/Meadow.Dfu/DfuUtils.cs @@ -1,15 +1,17 @@ -using Meadow.Hcom; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using System; +using System; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Net.Http; using System.Runtime.InteropServices; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using Meadow.Hcom; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using static System.Net.Mime.MediaTypeNames; namespace Meadow.CLI.Core.Internals.Dfu; @@ -17,6 +19,14 @@ public static class DfuUtils { private static readonly int _osAddress = 0x08000000; + private const string DFU_UTIL_UBUNTU_AMD64_URL = "http://ftp.de.debian.org/debian/pool/main/d/dfu-util/dfu-util_0.11-1_amd64.deb"; + private const string DFU_UTIL_UBUNTU_ARM64_URL = "http://ftp.de.debian.org/debian/pool/main/d/dfu-util/dfu-util_0.11-1_arm64.deb"; + private const string DFU_UTIL_WINDOWS_URL = $"https://s3-us-west-2.amazonaws.com/downloads.wildernesslabs.co/public/dfu-util-{DEFAULT_DFU_VERSION}-binaries.zip"; + private const string DFU_UTIL = "dfu-util"; + private const int STREAM_BUFFER_SIZE = 81920; // 80 KB buffer size + + public const string DEFAULT_DFU_VERSION = "0.11"; + public enum DfuFlashFormat { /// @@ -31,6 +41,10 @@ public enum DfuFlashFormat /// Console.WriteLine for CLI - ToDo - remove /// ConsoleOut, + /// + /// No Console Output + /// + None, } private static void FormatDfuOutput(string logLine, ILogger? logger, DfuFlashFormat format = DfuFlashFormat.Percent) @@ -79,43 +93,19 @@ public static async Task FlashFile(string fileName, string? dfuSerialNumbe logger.LogInformation($"Flashing OS with {fileName}"); - var dfuUtilVersion = new Version(GetDfuUtilVersion()); + var dfuUtilVersion = new Version(await GetDfuUtilVersion()); logger.LogDebug("Detected OS: {os}", RuntimeInformation.OSDescription); + var expectedDfuUtilVersion = new Version(DEFAULT_DFU_VERSION); if (dfuUtilVersion == null) { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - logger.LogError("dfu-util not found - to install, run: `meadow dfu install` (may require administrator mode)"); - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - logger.LogError("dfu-util not found - to install run: `brew install dfu-util`"); - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - logger.LogError("dfu-util not found - install using package manager, for example: `apt install dfu-util` or the equivalent for your Linux distribution"); - } + logger.LogError("dfu-util not found - to install, run: `meadow dfu install`"); return false; } - else if (dfuUtilVersion.CompareTo(new Version("0.11")) < 0) + else if (dfuUtilVersion.CompareTo(expectedDfuUtilVersion) < 0) { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - logger.LogError("dfu-util update required - to update, run in administrator mode: `meadow install dfu-util`"); - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - logger.LogError("dfu-util update required - to update, run: `brew upgrade dfu-util`"); - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - logger.LogError("dfu-util update required - to update , run: `apt upgrade dfu-util` or the equivalent for your Linux distribution"); - } - else - { - return false; - } + logger.LogError($"dfu-util update required. Expected: {expectedDfuUtilVersion}, Found: {dfuUtilVersion} - to update, run: `meadow dfu install`"); + return false; } try @@ -144,79 +134,53 @@ public static async Task FlashFile(string fileName, string? dfuSerialNumbe private static async Task RunDfuUtil(string args, ILogger? logger, DfuFlashFormat format = DfuFlashFormat.Percent) { - var startInfo = new ProcessStartInfo("dfu-util", args) + try { - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - RedirectStandardInput = false, - CreateNoWindow = true - }; - using var process = Process.Start(startInfo); - - if (process == null) - { - throw new Exception("Failed to start dfu-util"); - } - - var informationLogger = logger != null - ? Task.Factory.StartNew( - () => - { - var lastProgress = string.Empty; - - while (process.HasExited == false) - { - var logLine = process.StandardOutput.ReadLine(); - // Ignore empty output - if (logLine == null) - continue; - - FormatDfuOutput(logLine, logger, format); - } - }) : Task.CompletedTask; - - var errorLogger = logger != null - ? Task.Factory.StartNew( - () => - { - while (process.HasExited == false) - { - var logLine = process.StandardError.ReadLine(); - logger.LogError(logLine); - } - }) : Task.CompletedTask; - await informationLogger; - await errorLogger; - process.WaitForExit(); + var result = await RunProcessCommand(DFU_UTIL, args, + outputLogLine => + { + // Ignore empty output + if (!string.IsNullOrWhiteSpace(outputLogLine) + && format != DfuFlashFormat.None) + { + FormatDfuOutput(outputLogLine, logger, format); + } + }, + errorLogLine => + { + if (!string.IsNullOrWhiteSpace(errorLogLine)) + { + logger?.LogError(errorLogLine); + } + }); + } + catch (Exception ex) + { + throw new Exception($"dfu-util failed. Error: {ex.Message}"); + } } - private static string GetDfuUtilVersion() + private static async Task GetDfuUtilVersion() { try { - using (var process = new Process()) - { - process.StartInfo.FileName = "dfu-util"; - process.StartInfo.Arguments = $"--version"; - process.StartInfo.UseShellExecute = false; - process.StartInfo.RedirectStandardOutput = true; - process.Start(); - - var reader = process.StandardOutput; - var output = reader.ReadLine(); - if (output != null && output.StartsWith("dfu-util")) + var version = string.Empty; + + var result = await RunProcessCommand(DFU_UTIL, "--version", + output => { - var split = output.Split(new char[] { ' ' }); - if (split.Length == 2) + if (!string.IsNullOrWhiteSpace(output) + && output.StartsWith(DFU_UTIL)) { - return split[1]; + var split = output.Split(new char[] { ' ' }); + if (split.Length == 2) + { + version = split[1]; + } } - } + }); - process.WaitForExit(); - return string.Empty; - } + return string.IsNullOrWhiteSpace(version) ? string.Empty : version; } catch (Win32Exception ex) { @@ -237,43 +201,54 @@ private static string GetDfuUtilVersion() } } - public static async Task InstallDfuUtil( + public static async Task CheckIfDfuUtilIsInstalledOnWindows( string tempFolder, - string dfuUtilVersion = "0.11", + string dfuUtilVersion = DEFAULT_DFU_VERSION, CancellationToken cancellationToken = default) { - try + if (cancellationToken.IsCancellationRequested) { - if (Directory.Exists(tempFolder)) + return false; + } + + // Check if dfu-util is installed. + bool isDfuUtilInstalled = await IsCommandInstalled(DFU_UTIL); + if (isDfuUtilInstalled) + { + var version = await GetDfuUtilVersion(); + if (!dfuUtilVersion.Equals(version)) { - Directory.Delete(tempFolder, true); + return await InstallDfuUtilOnWindows(tempFolder, dfuUtilVersion, cancellationToken); } - using var client = new HttpClient(); - - Directory.CreateDirectory(tempFolder); + return true; + } + else + { + return await InstallDfuUtilOnWindows(tempFolder, dfuUtilVersion, cancellationToken); + } + } - var downloadUrl = $"https://s3-us-west-2.amazonaws.com/downloads.wildernesslabs.co/public/dfu-util-{dfuUtilVersion}-binaries.zip"; + private static async Task InstallDfuUtilOnWindows(string tempFolder, string dfuUtilVersion, CancellationToken cancellationToken) + { + try + { + await DeleteDirectory(tempFolder); - var downloadFileName = downloadUrl.Substring(downloadUrl.LastIndexOf("/", StringComparison.Ordinal) + 1); + using var client = new HttpClient(); - var response = await client.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + Directory.CreateDirectory(tempFolder); - if (response.IsSuccessStatusCode == false) - { - throw new Exception("Failed to download dfu-util"); - } + var downloadedFileName = Path.GetFileName(new Uri(DFU_UTIL_WINDOWS_URL).LocalPath); + var downloadedFilePath = Path.Combine(tempFolder, downloadedFileName); + await DownloadFile(DFU_UTIL_WINDOWS_URL, downloadedFilePath, cancellationToken); - using (var stream = await response.Content.ReadAsStreamAsync()) - using (var downloadFileStream = new DownloadFileStream(stream)) - using (var fs = File.OpenWrite(Path.Combine(tempFolder, downloadFileName))) + await Task.Run(() => { - await downloadFileStream.CopyToAsync(fs); - } - - ZipFile.ExtractToDirectory( - Path.Combine(tempFolder, downloadFileName), + ZipFile.ExtractToDirectory( + downloadedFilePath, tempFolder); + }); var is64Bit = Environment.Is64BitOperatingSystem; @@ -290,15 +265,346 @@ public static async Task InstallDfuUtil( ? Environment.GetFolderPath(Environment.SpecialFolder.System) : Environment.GetFolderPath(Environment.SpecialFolder.SystemX86); - File.Copy(dfuUtilExe.FullName, Path.Combine(targetDir, dfuUtilExe.Name), true); - File.Copy(libUsbDll.FullName, Path.Combine(targetDir, libUsbDll.Name), true); + using (FileStream sourceStream = new FileStream(dfuUtilExe.FullName, FileMode.Open, FileAccess.Read, FileShare.Read, STREAM_BUFFER_SIZE, useAsync: true)) + { + await CopyFile(sourceStream, Path.Combine(targetDir, dfuUtilExe.Name)); + } + + using (FileStream sourceStream = new FileStream(libUsbDll.FullName, FileMode.Open, FileAccess.Read, FileShare.Read, STREAM_BUFFER_SIZE, useAsync: true)) + { + await CopyFile(sourceStream, Path.Combine(targetDir, libUsbDll.Name)); + } + + return true; } finally { - if (Directory.Exists(tempFolder)) + await DeleteDirectory(tempFolder); + } + } + + public static async Task CheckIfDfuUtilIsInstalledOnMac( + string dfuUtilVersion = DEFAULT_DFU_VERSION, + CancellationToken cancellationToken = default) + { + if (cancellationToken.IsCancellationRequested) + { + return false; + } + + // Check if brew is intalled. + bool isBrewInstalled = await IsCommandInstalled("brew"); + if (!isBrewInstalled) + { + await InstallHomebrewOnMac(); + } + + if (cancellationToken.IsCancellationRequested) + { + return false; + } + + // Check if dfu-util is installed + bool isDfuUtilInstalled = await IsCommandInstalled(DFU_UTIL); + if (isDfuUtilInstalled) + { + var version = await GetDfuUtilVersion(); + if (!dfuUtilVersion.Equals(version)) + { + return await InstallDfuUtilOnMac(); + } + + return true; + } + else + { + return await InstallDfuUtilOnMac(); + } + } + + public static async Task IsCommandInstalled(string command) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return await IsCommandInstalledOnWindows(command); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return await IsCommandInstalledOnNix(command); + } + else + { + // Unsupported platform + Console.Error.WriteLine("Unsupported platform."); + return false; + } + } + + public static async Task IsCommandInstalledOnNix(string command) + { + try + { + var exitCode = await RunProcessCommand("/bin/bash", $"-c \"which {command}\""); + return exitCode == 0; + } + catch (Exception ex) + { + // Handle exceptions + Console.WriteLine($"An error occurred: {ex.Message}"); + return false; + } + } + + public static async Task InstallHomebrewOnMac() + { + var exitCode = await RunProcessCommand("/bin/bash", "-c '/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"'"); + if (exitCode != 0) + { + throw new Exception($"Unable to install Homebrew. Error:{exitCode}"); + } + Console.WriteLine("Homebrew installated successfully."); + } + + public static async Task InstallDfuUtilOnMac() + { + // Use brew to to install dfu-util + var exitCode = await RunProcessCommand("/bin/bash", "-c 'brew install dfu-util'"); + if (exitCode != 0) + { + throw new Exception($"Unable to install dfu-util. Error:{exitCode}"); + } + return true; + } + + public static async Task IsCommandInstalledOnWindows(string command) + { + try + { + // -Verb RunAs elevates the command and asks for UAC + var exitCode = await RunProcessCommand("powershell", $"-Command \"Start-Process -Verb RunAs -FilePath '{command}'\""); + return exitCode == 0; + } + catch (Exception ex) + { + // Handle exceptions + Console.Error.WriteLine($"An error occurred: {ex.Message}"); + return false; + } + } + + public static async Task CheckIfDfuUtilIsInstalledOnLinux( + string dfuUtilVersion = DEFAULT_DFU_VERSION, + CancellationToken cancellationToken = default) + { + if (cancellationToken.IsCancellationRequested) + { + return false; + } + + // Check if dfu-util is installed. + bool isDfuUtilInstalled = await IsCommandInstalled(DFU_UTIL); + if (isDfuUtilInstalled) + { + var version = await GetDfuUtilVersion(); + if (!dfuUtilVersion.Equals(version)) + { + return await InstallPackageOnLinux(DFU_UTIL); + } + + return true; + } + else + { + return await InstallPackageOnLinux(DFU_UTIL); + } + } + + private static async Task InstallPackageOnLinux(string package) + { + string osReleaseFile = "/etc/os-release"; + + if (File.Exists(osReleaseFile)) + { + var lines = File.ReadAllLines(osReleaseFile); + var distroName = string.Empty; + var distroVersion = string.Empty; + + foreach (var line in lines) { - Directory.Delete(tempFolder, true); + if (line.StartsWith("NAME=")) + { + distroName = line.Substring(5).Trim('"').ToLower(); + } + else if (line.StartsWith("VERSION=")) + { + distroVersion = line.Substring(8).Trim('"'); + } + } + + switch (distroName) + { + case "ubuntu": + // If need be we can check distroVersion here too + + // Install the default package for this distro + await InstallPackageOnUbuntu(package); + + // We check the version again, because on some versions of Ubuntu the default dfu-util version is 0.9 :( + var installedDfuUtilVersion = new Version(await GetDfuUtilVersion()); + var expectedDfuUtilVersion = new Version(DEFAULT_DFU_VERSION); + if (installedDfuUtilVersion.CompareTo(expectedDfuUtilVersion) < 0) + { + var dfuPackageUrl = RuntimeInformation.OSArchitecture switch + { + Architecture.Arm64 => DFU_UTIL_UBUNTU_ARM64_URL, + Architecture.X64 => DFU_UTIL_UBUNTU_AMD64_URL, + _ => throw new PlatformNotSupportedException("Unsupported architecture") + }; + + var downloadedFileName = Path.GetFileName(new Uri(dfuPackageUrl).LocalPath); + var downloadedFilePath = Path.Combine(Path.GetTempPath(), downloadedFileName); + await DownloadFile(dfuPackageUrl, downloadedFilePath); + + await InstallDownloadedDebianPackage(downloadedFilePath); + + // We've finished with it, let's delete it. + await DeleteFile(downloadedFilePath); + + var recentlyInstalledDfuUtilVersion = new Version(await GetDfuUtilVersion()); + if (recentlyInstalledDfuUtilVersion.CompareTo(expectedDfuUtilVersion) != 0) + { + throw new Exception($"Unable to install the version {expectedDfuUtilVersion} of {package}."); + } + } + + return true; + + default: + Console.WriteLine($"To install {package} on Linux, use your distro's package manager to install the {package} package"); + return false; } } + else + { + Console.Error.WriteLine($"The {osReleaseFile} file does not exist. unable to proceed"); + return false; + } + } + + private static async Task InstallDownloadedDebianPackage(string fileName) + { + await RunProcessCommand("sudo", $"dpkg -i {fileName}"); + } + + public static async Task DeleteFile(string filePath) + { + if (string.IsNullOrEmpty(filePath)) + { + throw new ArgumentException("File path cannot be null or empty.", nameof(filePath)); + } + + await Task.Run(() => + { + if (File.Exists(filePath)) + { + File.Delete(filePath); + } + }); + } + + public static async Task DeleteDirectory(string directoryPath, bool recursive = true) + { + if (string.IsNullOrEmpty(directoryPath)) + { + throw new ArgumentException("Directory path cannot be null or empty.", nameof(directoryPath)); + } + + await Task.Run(() => + { + if (Directory.Exists(directoryPath)) + { + Directory.Delete(directoryPath, recursive: recursive); + } + }); + } + + private static async Task DownloadFile(string downloadUrl, string downloadedFileName, CancellationToken cancellationToken = default) + { + using var client = new HttpClient(); + var response = await client.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + if (!response.IsSuccessStatusCode) + { + throw new HttpRequestException($"Failed to download {downloadedFileName} from {downloadUrl}"); + } + + using Stream contentStream = await response.Content.ReadAsStreamAsync(); + await CopyFile(contentStream, downloadedFileName); + } + + public static async Task CopyFile(Stream sourceStream, string destinationFilePath) + { + using (FileStream destinationStream = new FileStream(destinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, STREAM_BUFFER_SIZE, useAsync: true)) + { + await sourceStream.CopyToAsync(destinationStream); + } + } + + public static async Task InstallPackageOnUbuntu(string package) + { + return await RunProcessCommand("sudo", $"apt-get --reinstall install {package}"); + } + + public static async Task RunProcessCommand(string command, string args, Action? handleOutput = null, Action? handleError = null) + { + var processStartInfo = new ProcessStartInfo + { + FileName = command, + Arguments = args, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using (var process = new Process { StartInfo = processStartInfo }) + { + process.Start(); + + var outputCompletion = ReadLinesAsync(process.StandardOutput, handleOutput); + var errorCompletion = ReadLinesAsync(process.StandardError, handleError); + + await Task.WhenAll(outputCompletion, errorCompletion, process.WaitForExitAsync()); + + return process.ExitCode; + } + } + + private static async Task ReadLinesAsync(StreamReader reader, Action? handleLine) + { + while (!reader.EndOfStream) + { + var line = await reader.ReadLineAsync(); + if (!string.IsNullOrWhiteSpace(line) + && handleLine != null) + { + handleLine(line); + } + } + } +} + +public static class ProcessExtensions +{ + public static Task WaitForExitAsync(this Process process) + { + var tcs = new TaskCompletionSource(); + + process.EnableRaisingEvents = true; + process.Exited += (sender, args) => + { + tcs.SetResult(process.ExitCode == 0); + }; + + return tcs.Task; } } \ No newline at end of file diff --git a/Source/v2/Meadow.Tooling.Core/Connection/MeadowConnectionManager.cs b/Source/v2/Meadow.Tooling.Core/Connection/MeadowConnectionManager.cs index 435f5434..c2aa15c3 100644 --- a/Source/v2/Meadow.Tooling.Core/Connection/MeadowConnectionManager.cs +++ b/Source/v2/Meadow.Tooling.Core/Connection/MeadowConnectionManager.cs @@ -210,8 +210,10 @@ public static async Task> GetMeadowSerialPortsForLinux() { FileName = "ls", Arguments = $"-l {devicePath}", + RedirectStandardOutput = true, + RedirectStandardError = true, UseShellExecute = false, - RedirectStandardOutput = true + CreateNoWindow = true }; using var proc = Process.Start(psi); From b8bee31356c959e44433873c6ff3faa3bf32cea7 Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Fri, 28 Jun 2024 15:32:21 +0100 Subject: [PATCH 2/3] Add meadow provision command --- .github/workflows/dotnet.yml | 2 +- .../Current/Firmware/FirmwareUpdater.cs | 579 ++++++++++++++++++ .../Current/Provision/ProvisionCommand.cs | 327 ++++++++++ .../Current/Provision/ProvisionSettings.cs | 9 + Source/v2/Meadow.CLI/provision.json | 6 + .../Commands/Current/BaseDeviceCommand.cs | 8 +- .../Current/Firmware/FirmwareWriteCommand.cs | 512 +--------------- Source/v2/Meadow.Cli/Meadow.CLI.csproj | 1 + .../Meadow.Cli/Properties/launchSettings.json | 4 + Source/v2/Meadow.Cli/Strings.cs | 40 ++ Source/v2/Meadow.Dfu/DfuUtils.cs | 5 +- Source/v2/Meadow.Firmware/FirmwareWriter.cs | 2 +- .../Connections/SerialConnection.cs | 47 +- Source/v2/Meadow.Hcom/Meadow.HCom.csproj | 2 +- .../Connection/MeadowConnectionManager.cs | 134 ++-- Source/v2/Meadow.UsbLib.Core/ILibUsbDevice.cs | 2 +- Source/v2/Meadow.UsbLib/LibUsbDevice.cs | 26 +- .../ClassicLibUsbDevice.cs | 83 +-- 18 files changed, 1157 insertions(+), 632 deletions(-) create mode 100644 Source/v2/Meadow.CLI/Commands/Current/Firmware/FirmwareUpdater.cs create mode 100644 Source/v2/Meadow.CLI/Commands/Current/Provision/ProvisionCommand.cs create mode 100644 Source/v2/Meadow.CLI/Commands/Current/Provision/ProvisionSettings.cs create mode 100644 Source/v2/Meadow.CLI/provision.json mode change 100755 => 100644 Source/v2/Meadow.Hcom/Connections/SerialConnection.cs diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 855182ac..f418f3c9 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -1,7 +1,7 @@ name: Meadow.CLI Packaging env: CLI_RELEASE_VERSION_1: 1.9.4.0 - CLI_RELEASE_VERSION_2: 2.0.17.0 + CLI_RELEASE_VERSION_2: 2.0.54.0 IDE_TOOLS_RELEASE_VERSION: 1.9.4 MEADOW_OS_VERSION: 1.9.0.0 VS_MAC_2019_VERSION: 8.10 diff --git a/Source/v2/Meadow.CLI/Commands/Current/Firmware/FirmwareUpdater.cs b/Source/v2/Meadow.CLI/Commands/Current/Firmware/FirmwareUpdater.cs new file mode 100644 index 00000000..52b40c10 --- /dev/null +++ b/Source/v2/Meadow.CLI/Commands/Current/Firmware/FirmwareUpdater.cs @@ -0,0 +1,579 @@ +using System.Runtime.InteropServices; +using Meadow.CLI.Core.Internals.Dfu; +using Meadow.Hcom; +using Meadow.LibUsb; +using Meadow.Software; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +public class FirmwareUpdater where T : BaseDeviceCommand +{ + private string? individualFile; + private FirmwareType[]? firmwareFileTypes; + private bool useDfu; + private string? osVersion; + private string? serialNumber; + private readonly MeadowConnectionManager connectionManager; + private readonly ILogger? logger; + private readonly bool provisioningInProgress; + private readonly CancellationToken cancellationToken; + private readonly ISettingsManager settings; + private readonly FileManager fileManager; + + private int lastWriteProgress = 0; + + private BaseDeviceCommand command; + + public event EventHandler<(string message, double percentage)> UpdateProgress = default!; + + public FirmwareUpdater(BaseDeviceCommand command, ISettingsManager settings, FileManager fileManager, MeadowConnectionManager connectionManager, string? individualFile, FirmwareType[]? firmwareFileTypes, bool useDfu, string? osVersion, string? serialNumber, ILogger? logger, CancellationToken cancellationToken) + { + this.command = command; + this.settings = settings; + this.fileManager = fileManager; + this.connectionManager = connectionManager; + this.individualFile = individualFile; + this.firmwareFileTypes = firmwareFileTypes; + this.useDfu = useDfu; + this.osVersion = osVersion; + this.serialNumber = serialNumber; + this.logger = logger; + this.provisioningInProgress = logger == null; + this.cancellationToken = cancellationToken; + } + + public async Task UpdateFirmware() + { + var package = await GetSelectedPackage(); + + if (package == null) + { + return false; + } + + if (individualFile != null) + { + // check the file exists + var fullPath = Path.GetFullPath(individualFile); + if (!File.Exists(fullPath)) + { + throw new CommandException(string.Format(Strings.InvalidFirmwareForSpecifiedPath, fullPath), CommandExitCode.FileNotFound); + } + + // set the file type + firmwareFileTypes = Path.GetFileName(individualFile) switch + { + F7FirmwarePackageCollection.F7FirmwareFiles.OSWithBootloaderFile => new[] { FirmwareType.OS }, + F7FirmwarePackageCollection.F7FirmwareFiles.OsWithoutBootloaderFile => new[] { FirmwareType.OS }, + F7FirmwarePackageCollection.F7FirmwareFiles.RuntimeFile => new[] { FirmwareType.Runtime }, + F7FirmwarePackageCollection.F7FirmwareFiles.CoprocApplicationFile => new[] { FirmwareType.ESP }, + F7FirmwarePackageCollection.F7FirmwareFiles.CoprocBootloaderFile => new[] { FirmwareType.ESP }, + F7FirmwarePackageCollection.F7FirmwareFiles.CoprocPartitionTableFile => new[] { FirmwareType.ESP }, + _ => throw new CommandException(string.Format(Strings.UnknownSpecifiedFirmwareFile, Path.GetFileName(individualFile))) + }; + + logger?.LogInformation(string.Format($"{Strings.WritingSpecifiedFirmwareFile}...", fullPath)); + } + else if (firmwareFileTypes == null) + { + logger?.LogInformation(string.Format(Strings.WritingAllFirmwareForSpecifiedVersion, package.Version)); + + firmwareFileTypes = new FirmwareType[] + { + FirmwareType.OS, + FirmwareType.Runtime, + FirmwareType.ESP + }; + } + else if (firmwareFileTypes.Length == 1 && firmwareFileTypes[0] == FirmwareType.Runtime) + { //use the "DFU" path when only writing the runtime + useDfu = true; + } + + IMeadowConnection? connection = null; + DeviceInfo? deviceInfo = null; + + if (firmwareFileTypes.Contains(FirmwareType.OS)) + { + UpdateProgress?.Invoke(this, (Strings.FirmwareUpdater.FlashingOS, 20)); + await WriteOSFiles(connection, deviceInfo, package, useDfu); + } + + if (!string.IsNullOrWhiteSpace(serialNumber)) + { + connection = await GetConnectionAndDisableRuntime(await MeadowConnectionManager.GetRouteFromSerialNumber(serialNumber)); + if (connection != null) + { + if (provisioningInProgress) + { + connection.ConnectionMessage += (o, e) => + { + UpdateProgress?.Invoke(this, (e, 0)); + }; + } + + deviceInfo = await connection.GetDeviceInfo(cancellationToken); + } + } + + if (firmwareFileTypes.Contains(FirmwareType.Runtime) || Path.GetFileName(individualFile) == F7FirmwarePackageCollection.F7FirmwareFiles.RuntimeFile) + { + UpdateProgress?.Invoke(this, (Strings.FirmwareUpdater.WritingRuntime, 40)); + await WriteRuntimeFiles(connection, deviceInfo, package, individualFile); + } + + if (firmwareFileTypes.Contains(FirmwareType.ESP) + || Path.GetFileName(individualFile) == F7FirmwarePackageCollection.F7FirmwareFiles.CoprocPartitionTableFile + || Path.GetFileName(individualFile) == F7FirmwarePackageCollection.F7FirmwareFiles.CoprocApplicationFile + || Path.GetFileName(individualFile) == F7FirmwarePackageCollection.F7FirmwareFiles.CoprocBootloaderFile) + { + UpdateProgress?.Invoke(this, (Strings.FirmwareUpdater.WritingESP, 60)); + await WriteEspFiles(connection, deviceInfo, package); + } + + // reset device + if (connection != null && connection.Device != null) + { + await connection.Device.Reset(); + } + + return true; + } + + private async Task WriteEspFiles(IMeadowConnection? connection, DeviceInfo? deviceInfo, FirmwarePackage package) + { + connection ??= await GetConnectionAndDisableRuntime(await MeadowConnectionManager.GetRouteFromSerialNumber(serialNumber)); + + await WriteEsp(connection, deviceInfo, package); + + // reset device + if (connection != null && connection.Device != null) + { + await connection.Device.Reset(); + } + } + + private async Task WriteRuntimeFiles(IMeadowConnection? connection, DeviceInfo? deviceInfo, FirmwarePackage package, string? individualFile) + { + if (string.IsNullOrEmpty(individualFile)) + { + connection = await WriteRuntime(connection, deviceInfo, package); + } + else + { + connection = await WriteRuntime(connection, deviceInfo, individualFile, Path.GetFileName(individualFile)); + } + + if (connection == null) + { + throw CommandException.MeadowDeviceNotFound; + } + + await connection.WaitForMeadowAttach(); + } + + private async Task WriteOSFiles(IMeadowConnection? connection, DeviceInfo? deviceInfo, FirmwarePackage package, bool useDfu) + { + var osFileWithBootloader = package.GetFullyQualifiedPath(package.OSWithBootloader); + var osFileWithoutBootloader = package.GetFullyQualifiedPath(package.OsWithoutBootloader); + + if (osFileWithBootloader == null && osFileWithoutBootloader == null) + { + throw new CommandException(string.Format(Strings.OsFileNotFoundForSpecifiedVersion, package.Version)); + } + + var provider = new LibUsbProvider(); + var dfuDevice = GetLibUsbDeviceForCurrentEnvironment(provider, serialNumber); + bool ignoreSerial = IgnoreSerialNumberForDfu(provider); + + if (dfuDevice != null) + { + logger?.LogInformation($"{Strings.DfuDeviceDetected} - {Strings.UsingDfuToWriteOs}"); + useDfu = true; + } + else + { + if (useDfu) + { + throw new CommandException(Strings.NoDfuDeviceDetected); + } + + connection = await GetConnectionAndDisableRuntime(); + + deviceInfo = await connection.GetDeviceInfo(cancellationToken); + } + + if (useDfu || dfuDevice != null || osFileWithoutBootloader == null || RequiresDfuForRuntimeUpdates(deviceInfo!)) + { + // get a list of ports - it will not have our meadow in it (since it should be in DFU mode) + var initialPorts = await MeadowConnectionManager.GetSerialPorts(); + + await WriteOsWithDfu(dfuDevice!, osFileWithBootloader!, ignoreSerial); + + await Task.Delay(1500); + + connection ??= await FindMeadowConnection(initialPorts); + + await connection.WaitForMeadowAttach(cancellationToken); + } + else + { + await connection!.Device!.WriteFile(osFileWithoutBootloader, $"/{AppTools.MeadowRootFolder}/update/os/{package.OsWithoutBootloader}"); + } + } + + private async Task GetSelectedPackage() + { + await fileManager.Refresh(); + + var collection = fileManager.Firmware["Meadow F7"]; + FirmwarePackage package; + + if (osVersion != null) + { + // make sure the requested version exists + var existing = collection.FirstOrDefault(v => v.Version == osVersion); + + if (existing == null) + { + logger?.LogError(string.Format(Strings.SpecifiedFirmwareVersionNotFound, osVersion)); + return null; + } + package = existing; + } + else + { + osVersion = collection.DefaultPackage?.Version ?? throw new CommandException($"{Strings.NoDefaultVersionSet}. {Strings.UseCommandFirmwareDefault}."); + + package = collection.DefaultPackage; + } + + return package; + } + + private ILibUsbDevice? GetLibUsbDeviceForCurrentEnvironment(LibUsbProvider? provider, string? serialNumber = null) + { + provider ??= new LibUsbProvider(); + + var devices = provider.GetDevicesInBootloaderMode(); + + var meadowsInDFU = devices.Where(device => device.IsMeadow()).ToList(); + + if (meadowsInDFU.Count == 0) + { + return null; + } + + if (!string.IsNullOrWhiteSpace(serialNumber)) + { + return meadowsInDFU.Where(device => device.SerialNumber == serialNumber).FirstOrDefault(); + } + else if (meadowsInDFU.Count == 1 || IgnoreSerialNumberForDfu(provider)) + { //IgnoreSerialNumberForDfu is a macOS-specific hack for Mark's machine + return meadowsInDFU.FirstOrDefault(); + } + + throw new CommandException(Strings.MultipleDfuDevicesFound); + } + + private bool IgnoreSerialNumberForDfu(LibUsbProvider provider) + { //hack check for Mark's Mac + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + var devices = provider.GetDevicesInBootloaderMode(); + + if (devices.Count == 2) + { + if (devices[0].SerialNumber.Length > 12 || devices[1].SerialNumber.Length > 12) + { + return true; + } + } + } + + return false; + } + + private async Task GetConnectionAndDisableRuntime(string? route = null) + { + IMeadowConnection connection; + + if (!string.IsNullOrWhiteSpace(route)) + { + connection = await command.GetConnectionForRoute(route, true); + } + else + { + connection = await command.GetCurrentConnection(true); + } + + if (await connection.Device!.IsRuntimeEnabled()) + { + logger?.LogInformation($"{Strings.DisablingRuntime}..."); + await connection.Device.RuntimeDisable(); + } + + lastWriteProgress = 0; + + connection.FileWriteProgress += (s, e) => + { + var p = (int)(e.completed / (double)e.total * 100d); + // don't report < 10% increments (decrease spew on large files) + if (p - lastWriteProgress < 10) { return; } + + lastWriteProgress = p; + + logger?.LogInformation($"{Strings.Writing} {e.fileName}: {p:0}% {(p < 100 ? string.Empty : "\r\n")}"); + }; + connection.DeviceMessageReceived += (s, e) => + { + if (e.message.Contains("% downloaded")) + { // don't echo this, as we're already reporting % written + } + else + { + logger?.LogInformation(e.message); + } + }; + connection.ConnectionMessage += (s, message) => + { + logger?.LogInformation(message); + }; + + return connection; + } + + private bool RequiresDfuForRuntimeUpdates(DeviceInfo info) + { + return true; + /* + restore this when we support OtA-style updates again + if (System.Version.TryParse(info.OsVersion, out var version)) + { + return version.Major >= 2; + } + */ + } + + private async Task WriteOsWithDfu(ILibUsbDevice libUsbDevice, string osFile, bool ignoreSerialNumber = false) + { + try + { //validate device + if (string.IsNullOrWhiteSpace(serialNumber)) + { + serialNumber = libUsbDevice.SerialNumber; + } + } + catch + { + throw new CommandException($"{Strings.FirmwareWriteFailed} - {Strings.UnableToReadSerialNumber} ({Strings.MakeSureDeviceisConnected})"); + } + + try + { + if (ignoreSerialNumber) + { + serialNumber = string.Empty; + } + + await DfuUtils.FlashFile( + osFile, + serialNumber, + logger: logger, + format: provisioningInProgress ? DfuUtils.DfuFlashFormat.None : DfuUtils.DfuFlashFormat.ConsoleOut); + } + catch (ArgumentException) + { + throw new CommandException($"{Strings.FirmwareWriteFailed} - {Strings.IsDfuUtilInstalled} {Strings.RunMeadowDfuInstall}"); + } + catch (Exception ex) + { + logger?.LogError($"Exception type: {ex.GetType().Name}"); + + // TODO: scope this to the right exception type for Win 10 access violation thing + // TODO: catch the Win10 DFU error here and change the global provider configuration to "classic" + settings.SaveSetting(SettingsManager.PublicSettings.LibUsb, "classic"); + + throw new CommandException(Strings.FirmwareUpdater.SwitchingToLibUsbClassic); + } + } + + private async Task FindMeadowConnection(IList portsToIgnore) + { + IMeadowConnection? connection = null; + + var newPorts = await WaitForNewSerialPorts(portsToIgnore); + string newPort = string.Empty; + + if (newPorts == null) + { + throw CommandException.MeadowDeviceNotFound; + } + + if (newPorts.Count == 1) + { + connection = await GetConnectionAndDisableRuntime(newPorts[0]); + newPort = newPorts[0]; + } + else + { + foreach (var port in newPorts) + { + try + { + connection = await GetConnectionAndDisableRuntime(port); + newPort = port; + break; + } + catch + { + throw CommandException.MeadowDeviceNotFound; + } + } + } + + logger?.LogInformation($"{Strings.MeadowFoundAt} {newPort}"); + + await connection!.WaitForMeadowAttach(); + + // configure the route to that port for the user + settings.SaveSetting(SettingsManager.PublicSettings.Route, newPort); + + return connection; + } + + private async Task> WaitForNewSerialPorts(IList? ignorePorts) + { + var ports = await MeadowConnectionManager.GetSerialPorts(); + + var retryCount = 0; + + while (ports.Count == 0) + { + if (retryCount++ > 10) + { + throw new CommandException(Strings.NewMeadowDeviceNotFound); + } + await Task.Delay(500); + ports = await MeadowConnectionManager.GetSerialPorts(); + } + + if (ignorePorts != null) + { + return ports.Except(ignorePorts).ToList(); + } + return ports.ToList(); + } + + private async Task WaitForNewSerialPort(IList? ignorePorts) + { + var ports = await WaitForNewSerialPorts(ignorePorts); + + return ports.FirstOrDefault(); + } + + private async Task WriteRuntime(IMeadowConnection? connection, DeviceInfo? deviceInfo, FirmwarePackage package) + { + logger?.LogInformation($"{Environment.NewLine}{Strings.GettingRuntimeFor} {package.Version}..."); + + if (package.Runtime == null) { return null; } + + // get the path to the runtime file + var rtpath = package.GetFullyQualifiedPath(package.Runtime); + + return await WriteRuntime(connection, deviceInfo, rtpath, package.Runtime); + } + + private async Task WriteRuntime(IMeadowConnection? connection, DeviceInfo? deviceInfo, string runtimePath, string destinationFilename) + { + connection ??= await GetConnectionAndDisableRuntime(await MeadowConnectionManager.GetRouteFromSerialNumber(serialNumber)); + + logger?.LogInformation($"{Environment.NewLine}{Strings.WritingRuntime}..."); + + deviceInfo ??= await connection.GetDeviceInfo(cancellationToken); + + if (deviceInfo == null) + { + throw new CommandException(Strings.UnableToGetDeviceInfo); + } + + if (useDfu || RequiresDfuForRuntimeUpdates(deviceInfo)) + { + var initialPorts = await MeadowConnectionManager.GetSerialPorts(); + + write_runtime: + if (!await connection!.Device!.WriteRuntime(runtimePath, cancellationToken)) + { + // TODO: implement a retry timeout + logger?.LogInformation($"{Strings.ErrorWritingRuntime} - {Strings.Retrying}"); + goto write_runtime; + } + + connection ??= await command.GetCurrentConnection(true); + + if (connection == null) + { + var newPort = await WaitForNewSerialPort(initialPorts) ?? throw CommandException.MeadowDeviceNotFound; + connection = await command.GetCurrentConnection(true); + + logger?.LogInformation($"{Strings.MeadowFoundAt} {newPort}"); + + // configure the route to that port for the user + settings.SaveSetting(SettingsManager.PublicSettings.Route, newPort); + } + } + else + { + await connection.Device!.WriteFile(runtimePath, $"/{AppTools.MeadowRootFolder}/update/os/{destinationFilename}"); + } + + return connection; + } + + private async Task WriteEsp(IMeadowConnection? connection, DeviceInfo? deviceInfo, FirmwarePackage package) + { + connection ??= await GetConnectionAndDisableRuntime(await MeadowConnectionManager.GetRouteFromSerialNumber(serialNumber)); + + if (connection == null) { return; } // couldn't find a connected device + + logger?.LogInformation($"{Environment.NewLine}{Strings.WritingCoprocessorFiles}..."); + + string[] fileList; + + if (individualFile != null) + { + fileList = new string[] { individualFile }; + } + else + { + fileList = new string[] + { + package.GetFullyQualifiedPath(package.CoprocApplication), + package.GetFullyQualifiedPath(package.CoprocBootloader), + package.GetFullyQualifiedPath(package.CoprocPartitionTable), + }; + } + + deviceInfo ??= await connection.GetDeviceInfo(cancellationToken); + + if (deviceInfo == null) { throw new CommandException(Strings.UnableToGetDeviceInfo); } + + if (useDfu || RequiresDfuForEspUpdates(deviceInfo)) + { + await connection.Device!.WriteCoprocessorFiles(fileList, cancellationToken); + } + else + { + foreach (var file in fileList) + { + await connection!.Device!.WriteFile(file, $"/{AppTools.MeadowRootFolder}/update/os/{Path.GetFileName(file)}"); + } + } + } + + private bool RequiresDfuForEspUpdates(DeviceInfo info) + { + return true; + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.CLI/Commands/Current/Provision/ProvisionCommand.cs b/Source/v2/Meadow.CLI/Commands/Current/Provision/ProvisionCommand.cs new file mode 100644 index 00000000..411968fe --- /dev/null +++ b/Source/v2/Meadow.CLI/Commands/Current/Provision/ProvisionCommand.cs @@ -0,0 +1,327 @@ +using System.Collections.Concurrent; +using System.Diagnostics; +using CliFx.Attributes; +using Meadow.CLI.Commands.DeviceManagement; +using Meadow.Cloud.Client; +using Meadow.LibUsb; +using Meadow.Package; +using Meadow.Software; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Spectre.Console; + +namespace Meadow.CLI.Commands.Provision; + +[Command("provision", Description = Strings.Provision.CommandDescription)] +public class ProvisionCommand : BaseDeviceCommand +{ + public const string DefaultFirmwareVersion = "1.12.2.0"; + private string? appPath; + private string? configuration = "Release"; + + [CommandOption("version", 'v', Description = Strings.Provision.CommandOptionVersion, IsRequired = false)] + public string? FirmwareVersion { get; set; } = DefaultFirmwareVersion; + + [CommandOption("path", 'p', Description = Strings.Provision.CommandOptionPath, IsRequired = false)] + public string? Path { get; set; } = "."; + + private ConcurrentQueue bootloaderDeviceQueue = new ConcurrentQueue(); + + private List selectedDeviceList = default!; + private ISettingsManager settingsManager; + private FileManager fileManager; + private IMeadowCloudClient meadowCloudClient; + private MeadowConnectionManager connectionManager; + private IPackageManager packageManager; + private bool? deployApp = true; + + public ProvisionCommand(ISettingsManager settingsManager, FileManager fileManager, + IMeadowCloudClient meadowCloudClient, IPackageManager packageManager, MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + : base(connectionManager, loggerFactory) + { + this.settingsManager = settingsManager; + this.fileManager = fileManager; + this.meadowCloudClient = meadowCloudClient; + this.connectionManager = connectionManager; + this.packageManager = packageManager; + } + + protected override async ValueTask ExecuteCommand() + { + try + { + AnsiConsole.MarkupLine(Strings.Provision.RunningTitle); + + bool refreshDeviceList = false; + do + { + UpdateDeviceList(CancellationToken); + + if (bootloaderDeviceQueue.Count == 0) + { + Logger?.LogError(Strings.Provision.NoDevicesFound); + return; + } + + var multiSelectionPrompt = new MultiSelectionPrompt() + .Title(Strings.Provision.PromptTitle) + .PageSize(15) + .NotRequired() // Can be Blank to exit + .MoreChoicesText($"[grey]{Strings.Provision.MoreChoicesInstructions}[/]") + .InstructionsText(string.Format($"[grey]{Strings.Provision.Instructions}[/]", $"[blue]<{Strings.Space}>[/]", $"[green]<{Strings.Enter}>[/]")) + .UseConverter(x => x); + + foreach (var device in bootloaderDeviceQueue) + { + multiSelectionPrompt.AddChoices(device.SerialNumber); + } + + selectedDeviceList = AnsiConsole.Prompt(multiSelectionPrompt); + + if (selectedDeviceList.Count == 0) + { + AnsiConsole.MarkupLine($"[yellow]{Strings.Provision.NoDeviceSelected}[/]"); + return; + } + + var selectedDeviceTable = new Table(); + selectedDeviceTable.AddColumn(Strings.Provision.ColumnTitle); + + foreach (var device in selectedDeviceList) + { + selectedDeviceTable.AddRow(device); + } + + AnsiConsole.Write(selectedDeviceTable); + + refreshDeviceList = AnsiConsole.Confirm(Strings.Provision.RefreshDeviceList); + } while (!refreshDeviceList); + + string path = System.IO.Path.Combine(Path, "provision.json"); + + if (!string.IsNullOrWhiteSpace(path) + && !File.Exists(path)) + { + deployApp = false; + AnsiConsole.MarkupLine($"[red]{Strings.Provision.FileNotFound}[/]", $"[yellow]{path}[/]"); + } + else + { + Path = path; + } + + if (deployApp.HasValue && deployApp.Value) + { + try + { + var provisionSettings = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(Path!)); + if (provisionSettings == null) + { + throw new Exception($"{Strings.Provision.FailedToReadProvisionFile}."); + } + + // Use the settings from provisionSettings as needed + configuration = provisionSettings.Configuration; + FirmwareVersion = provisionSettings.FirmwareVersion; + deployApp = provisionSettings.DeployApp; + + if (deployApp.HasValue && deployApp.Value) + { + appPath = AppTools.ValidateAndSanitizeAppPath(provisionSettings.AppPath); + + if (!File.Exists(appPath)) + { + throw new FileNotFoundException($"{Strings.Provision.AppDllNotFound}:{appPath}"); + } + + AnsiConsole.MarkupLine(Strings.Provision.TrimmingApp); + await AppTools.TrimApplication(appPath!, packageManager, FirmwareVersion!, configuration, null, null, Console, CancellationToken); + } + } + catch (Exception ex) + { + // Eat the exception and keep going. + deployApp = false; + AnsiConsole.MarkupLine($"[red]{ex.Message}[/]"); + Debug.WriteLine($"{ex.Message + Environment.NewLine + ex.StackTrace}"); + } + } + + if(deployApp.HasValue && !deployApp.Value) + { + AnsiConsole.MarkupLine(Strings.Provision.NoAppDeployment, $"[yellow]{FirmwareVersion}[/]"); + } + + if (string.IsNullOrEmpty(FirmwareVersion)) + { + FirmwareVersion = DefaultFirmwareVersion; + } + + // Install DFU, if it's not already installed. + var dfuInstallCommand = new DfuInstallCommand(settingsManager, LoggerFactory); + await dfuInstallCommand.ExecuteAsync(Console); + + // Make sure we've downloaded the osVersion or default + var firmwareDownloadCommand = new FirmwareDownloadCommand(fileManager, meadowCloudClient, LoggerFactory) + { + Version = FirmwareVersion, + Force = true + }; + await firmwareDownloadCommand.ExecuteAsync(Console); + + + // If we've reached here we're ready to Flash + await FlashingAttachedDevices(); + } + catch (Exception ex) + { + + var message = ex.Message; +#if DEBUG + var stackTrace = ex.StackTrace; + message += Environment.NewLine + stackTrace; +#endif + AnsiConsole.MarkupLine($"[red]{message}[/]"); + } + } + + private void UpdateDeviceList(CancellationToken cancellationToken) + { + var ourDevices = GetValidUsbDevices(); + + if (ourDevices?.Count() > 0) + { + bootloaderDeviceQueue.Clear(); + + foreach (ILibUsbDevice device in ourDevices) + { + if (bootloaderDeviceQueue != null) + { + if (device != null) + { + bootloaderDeviceQueue.Enqueue(device); + } + } + } + } + } + + private IEnumerable? GetValidUsbDevices() + { + try + { + var provider = new LibUsbProvider(); + + var devices = provider.GetDevicesInBootloaderMode(); + + return devices; + } + catch (Exception) + { + return null; + } + } + + public async Task FlashingAttachedDevices() + { + var succeedCount = 0; + var errorList = new List<(string SerialNumber, string Message, string StackTrace)>(); + + await AnsiConsole.Progress() + .AutoRefresh(true) + .HideCompleted(false) + .Columns(new ProgressColumn[] + { + new TaskDescriptionColumn(), // Task description + new ProgressBarColumn(), // Progress bar + new PercentageColumn(), // Percentage + new SpinnerColumn(), // Spinner + }) + .StartAsync(async ctx => + { + foreach (var deviceSerialNumber in selectedDeviceList) + { + var formatedDevice = $"[green]{deviceSerialNumber}[/]"; + var task = ctx.AddTask(formatedDevice, maxValue: 100); + + try + { + var firmareUpdater = new FirmwareUpdater(this, settingsManager, fileManager, this.connectionManager, null, null, true, FirmwareVersion, deviceSerialNumber, null, CancellationToken); + firmareUpdater.UpdateProgress += (o, e) => + { + if (e.percentage > 0) + { + task.Value = e.percentage; + } + task.Description = $"{formatedDevice}: {e.message}"; + }; + + if (!await firmareUpdater.UpdateFirmware()) + { + task.Description = $"{formatedDevice}: [red]{Strings.Provision.UpdateFailed}[/]"; + task.StopTask(); + } + + if (deployApp.HasValue && deployApp.Value) + { + task.Increment(20.00); + task.Description = $"{formatedDevice}: [yellow]{Strings.Provision.DeployingApp}[/]"; + + var route = await MeadowConnectionManager.GetRouteFromSerialNumber(deviceSerialNumber!); + if (!string.IsNullOrWhiteSpace(route)) + { + var connection = await GetConnectionForRoute(route, true); + var appDir = System.IO.Path.GetDirectoryName(appPath); + await AppManager.DeployApplication(packageManager, connection, FirmwareVersion!, appDir!, true, false, null, CancellationToken); + + await connection?.Device?.RuntimeEnable(CancellationToken); + } + } + + task.Value = 100.00; + task.Description = $"{formatedDevice}: [green]{Strings.Provision.UpdateComplete}[/]"; + + task.StopTask(); + + await Task.Delay(2000); // TODO May not be required, futher testing needed + + succeedCount++; + } + catch (Exception ex) + { + task.Description = $"{formatedDevice}: [red]{ex.Message}[/]"; + task.StopTask(); + + if (!string.IsNullOrWhiteSpace(ex.StackTrace)) + { + errorList.Add((deviceSerialNumber, ex.Message, ex.StackTrace)); + } + } + } + }); + + if (succeedCount == selectedDeviceList.Count) + { + AnsiConsole.MarkupLine($"[green]{Strings.Provision.AllDevicesFlashed}[/]"); + } + else + { + AnsiConsole.MarkupLine($"[yellow]{Strings.Provision.IssuesFound}[/]"); + var showErrorMessages = AnsiConsole.Confirm(Strings.Provision.ShowErrorMessages); + if (showErrorMessages) + { + var errorTable = new Table(); + errorTable.AddColumn(Strings.Provision.ErrorSerialNumberColumnTitle); + errorTable.AddColumn(Strings.Provision.ErrorMessageColumnTitle); + errorTable.AddColumn(Strings.Provision.ErrorStackTraceColumnTitle); + + foreach (var error in errorList) + { + errorTable.AddRow(error.SerialNumber, error.Message, error.StackTrace); + } + + AnsiConsole.Write(errorTable); + } + } + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.CLI/Commands/Current/Provision/ProvisionSettings.cs b/Source/v2/Meadow.CLI/Commands/Current/Provision/ProvisionSettings.cs new file mode 100644 index 00000000..46016d42 --- /dev/null +++ b/Source/v2/Meadow.CLI/Commands/Current/Provision/ProvisionSettings.cs @@ -0,0 +1,9 @@ +namespace Meadow.CLI.Commands.Provision; + +public class ProvisionSettings +{ + public string? AppPath { get; set; } + public string? Configuration { get; set; } + public bool? DeployApp { get; set; } + public string? FirmwareVersion { get; set; } +} \ No newline at end of file diff --git a/Source/v2/Meadow.CLI/provision.json b/Source/v2/Meadow.CLI/provision.json new file mode 100644 index 00000000..997e4bd1 --- /dev/null +++ b/Source/v2/Meadow.CLI/provision.json @@ -0,0 +1,6 @@ +{ + "AppPath": "C:\\Users\\willo\\Downloads\\Blinky\\Debug\\netstandard2.1\\App.dll", + "Configuration": "Debug", + "DeployApp": "false", + "FirmwareVersion": "1.12.2.0" +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs index a94358c0..58df2c60 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs @@ -17,10 +17,10 @@ protected async Task GetCurrentDevice() return (await GetCurrentConnection()).Device ?? throw CommandException.MeadowDeviceNotFound; } - protected Task GetCurrentConnection(bool forceReconnect = false) + internal Task GetCurrentConnection(bool forceReconnect = false) => GetConnection(null, forceReconnect); - protected Task GetConnectionForRoute(string route, bool forceReconnect = false) + internal Task GetConnectionForRoute(string route, bool forceReconnect = false) => GetConnection(route, forceReconnect); private async Task GetConnection(string? route, bool forceReconnect = false) @@ -29,11 +29,11 @@ private async Task GetConnection(string? route, bool forceRec if (route != null) { - connection = ConnectionManager.GetConnectionForRoute(route, forceReconnect); + connection = await MeadowConnectionManager.GetConnectionForRoute(route, forceReconnect); } else { - connection = ConnectionManager.GetCurrentConnection(forceReconnect); + connection = await ConnectionManager.GetCurrentConnection(forceReconnect); } if (connection != null) diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs index b1d436b9..dd8f9470 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs @@ -1,10 +1,10 @@ -using CliFx.Attributes; +using System.Runtime.InteropServices; +using CliFx.Attributes; using Meadow.CLI.Core.Internals.Dfu; using Meadow.Hcom; using Meadow.LibUsb; using Meadow.Software; using Microsoft.Extensions.Logging; -using System.Runtime.InteropServices; namespace Meadow.CLI.Commands.DeviceManagement; @@ -27,6 +27,9 @@ public class FirmwareWriteCommand : BaseDeviceCommand [CommandOption("file", 'f', IsRequired = false, Description = "Send only the specified file")] public string? IndividualFile { get; set; } + [CommandOption("serialnumber", 's', IsRequired = false, Description = "Flash the specified device")] + public string? SerialNumber { get; set; } + [CommandParameter(0, Description = "Files to write", IsRequired = false)] public FirmwareType[]? FirmwareFileTypes { get; set; } = default!; @@ -40,512 +43,13 @@ public FirmwareWriteCommand(ISettingsManager settingsManager, FileManager fileMa Settings = settingsManager; } - private int _lastWriteProgress = 0; - - private async Task GetConnectionAndDisableRuntime(string? route = null) - { - IMeadowConnection connection; - - if (route != null) - { - connection = await GetConnectionForRoute(route, true); - } - else - { - connection = await GetCurrentConnection(true); - } - - if (await connection.Device!.IsRuntimeEnabled()) - { - Logger?.LogInformation($"{Strings.DisablingRuntime}..."); - await connection.Device.RuntimeDisable(); - } - - _lastWriteProgress = 0; - - connection.FileWriteProgress += (s, e) => - { - var p = (int)(e.completed / (double)e.total * 100d); - // don't report < 10% increments (decrease spew on large files) - if (p - _lastWriteProgress < 10) { return; } - - _lastWriteProgress = p; - - Logger?.LogInformation($"{Strings.Writing} {e.fileName}: {p:0}% {(p < 100 ? string.Empty : "\r\n")}"); - }; - connection.DeviceMessageReceived += (s, e) => - { - if (e.message.Contains("% downloaded")) - { // don't echo this, as we're already reporting % written - } - else - { - Logger?.LogInformation(e.message); - } - }; - connection.ConnectionMessage += (s, message) => - { - Logger?.LogInformation(message); - }; - - return connection; - } - - private bool RequiresDfuForRuntimeUpdates(DeviceInfo info) - { - return true; - /* - restore this when we support OtA-style updates again - if (System.Version.TryParse(info.OsVersion, out var version)) - { - return version.Major >= 2; - } - */ - } - - private bool RequiresDfuForEspUpdates(DeviceInfo info) - { - return true; - } - protected override async ValueTask ExecuteCommand() { - var package = await GetSelectedPackage(); - - if (package == null) - { - return; - } - - if (IndividualFile != null) - { - // check the file exists - var fullPath = Path.GetFullPath(IndividualFile); - if (!File.Exists(fullPath)) - { - throw new CommandException(string.Format(Strings.InvalidFirmwareForSpecifiedPath, fullPath), CommandExitCode.FileNotFound); - } - - // set the file type - FirmwareFileTypes = Path.GetFileName(IndividualFile) switch - { - F7FirmwarePackageCollection.F7FirmwareFiles.OSWithBootloaderFile => new[] { FirmwareType.OS }, - F7FirmwarePackageCollection.F7FirmwareFiles.OsWithoutBootloaderFile => new[] { FirmwareType.OS }, - F7FirmwarePackageCollection.F7FirmwareFiles.RuntimeFile => new[] { FirmwareType.Runtime }, - F7FirmwarePackageCollection.F7FirmwareFiles.CoprocApplicationFile => new[] { FirmwareType.ESP }, - F7FirmwarePackageCollection.F7FirmwareFiles.CoprocBootloaderFile => new[] { FirmwareType.ESP }, - F7FirmwarePackageCollection.F7FirmwareFiles.CoprocPartitionTableFile => new[] { FirmwareType.ESP }, - _ => throw new CommandException(string.Format(Strings.UnknownSpecifiedFirmwareFile, Path.GetFileName(IndividualFile))) - }; - - Logger?.LogInformation(string.Format($"{Strings.WritingSpecifiedFirmwareFile}...", fullPath)); - } - else if (FirmwareFileTypes == null) - { - Logger?.LogInformation(string.Format(Strings.WritingAllFirmwareForSpecifiedVersion, package.Version)); - - FirmwareFileTypes = new FirmwareType[] - { - FirmwareType.OS, - FirmwareType.Runtime, - FirmwareType.ESP - }; - } - else if (FirmwareFileTypes.Length == 1 && FirmwareFileTypes[0] == FirmwareType.Runtime) - { //use the "DFU" path when only writing the runtime - UseDfu = true; - } - - IMeadowConnection? connection = null; - DeviceInfo? deviceInfo = null; - - if (FirmwareFileTypes.Contains(FirmwareType.OS)) - { - string? osFileWithBootloader = null; - string? osFileWithoutBootloader = null; - - if (string.IsNullOrWhiteSpace(IndividualFile)) - { - osFileWithBootloader = package.GetFullyQualifiedPath(package.OSWithBootloader); - osFileWithoutBootloader = package.GetFullyQualifiedPath(package.OsWithoutBootloader); - - if (osFileWithBootloader == null && osFileWithoutBootloader == null) - { - throw new CommandException(string.Format(Strings.OsFileNotFoundForSpecifiedVersion, package.Version)); - } - } - else - { - osFileWithBootloader = IndividualFile; - } - - // do we have a dfu device attached, or is DFU specified? - var provider = new LibUsbProvider(); - var dfuDevice = GetLibUsbDeviceForCurrentEnvironment(provider); - bool ignoreSerial = IgnoreSerialNumberForDfu(provider); - - if (dfuDevice != null) - { - Logger?.LogInformation($"{Strings.DfuDeviceDetected} - {Strings.UsingDfuToWriteOs}"); - UseDfu = true; - } - else - { - if (UseDfu) - { - throw new CommandException(Strings.NoDfuDeviceDetected); - } - - connection = await GetConnectionAndDisableRuntime(); - - deviceInfo = await connection.GetDeviceInfo(CancellationToken); - } - - if (UseDfu || dfuDevice != null || osFileWithoutBootloader == null || RequiresDfuForRuntimeUpdates(deviceInfo!)) - { - // get a list of ports - it will not have our meadow in it (since it should be in DFU mode) - var initialPorts = await MeadowConnectionManager.GetSerialPorts(); - - try - { - await WriteOsWithDfu(dfuDevice!, osFileWithBootloader!, ignoreSerial); - } - finally - { - dfuDevice?.Dispose(); - } - - await Task.Delay(1500); - - connection = await FindMeadowConnection(initialPorts); - - await connection.WaitForMeadowAttach(); - } - else - { - await connection!.Device!.WriteFile(osFileWithoutBootloader, $"/{AppTools.MeadowRootFolder}/update/os/{package.OsWithoutBootloader}"); - } - } - - if (FirmwareFileTypes.Contains(FirmwareType.Runtime) || Path.GetFileName(IndividualFile) == F7FirmwarePackageCollection.F7FirmwareFiles.RuntimeFile) - { - if (string.IsNullOrEmpty(IndividualFile)) - { - connection = await WriteRuntime(connection, deviceInfo, package); - } - else - { - connection = await WriteRuntime(connection, deviceInfo, IndividualFile, Path.GetFileName(IndividualFile)); - } - - if (connection == null) - { - throw CommandException.MeadowDeviceNotFound; - } - - await connection.WaitForMeadowAttach(); - } - - if (FirmwareFileTypes.Contains(FirmwareType.ESP) - || Path.GetFileName(IndividualFile) == F7FirmwarePackageCollection.F7FirmwareFiles.CoprocPartitionTableFile - || Path.GetFileName(IndividualFile) == F7FirmwarePackageCollection.F7FirmwareFiles.CoprocApplicationFile - || Path.GetFileName(IndividualFile) == F7FirmwarePackageCollection.F7FirmwareFiles.CoprocBootloaderFile) - { - connection = await GetConnectionAndDisableRuntime(); - - await WriteEspFiles(connection, deviceInfo, package); - - // reset device - if (connection != null && connection.Device != null) - { - await connection.Device.Reset(); - } - } - - Logger?.LogInformation(Strings.FirmwareUpdatedSuccessfully); - } - - private async Task FindMeadowConnection(IList portsToIgnore) - { - IMeadowConnection? connection = null; - - var newPorts = await WaitForNewSerialPorts(portsToIgnore); - string newPort = string.Empty; - - if (newPorts == null) - { - throw CommandException.MeadowDeviceNotFound; - } + var firmareUpdater = new FirmwareUpdater(this, Settings, FileManager, ConnectionManager, IndividualFile, FirmwareFileTypes, UseDfu, Version, SerialNumber, Logger, CancellationToken); - if (newPorts.Count == 1) - { - connection = await GetConnectionAndDisableRuntime(newPorts[0]); - newPort = newPorts[0]; - } - else + if (await firmareUpdater.UpdateFirmware()) { - foreach (var port in newPorts) - { - try - { - connection = await GetConnectionAndDisableRuntime(port); - newPort = port; - break; - } - catch - { - throw CommandException.MeadowDeviceNotFound; - } - } + Logger?.LogInformation(Strings.FirmwareUpdatedSuccessfully); } - - Logger?.LogInformation($"{Strings.MeadowFoundAt} {newPort}"); - - await connection!.WaitForMeadowAttach(); - - // configure the route to that port for the user - Settings.SaveSetting(SettingsManager.PublicSettings.Route, newPort); - - return connection; - } - - private async Task WriteRuntime(IMeadowConnection? connection, DeviceInfo? deviceInfo, FirmwarePackage package) - { - Logger?.LogInformation($"{Environment.NewLine}{Strings.GettingRuntimeFor} {package.Version}..."); - - if (package.Runtime == null) { return null; } - - // get the path to the runtime file - var rtpath = package.GetFullyQualifiedPath(package.Runtime); - - return await WriteRuntime(connection, deviceInfo, rtpath, package.Runtime); - } - - private async Task WriteRuntime(IMeadowConnection? connection, DeviceInfo? deviceInfo, string runtimePath, string destinationFilename) - { - connection ??= await GetConnectionAndDisableRuntime(); - - Logger?.LogInformation($"{Environment.NewLine}{Strings.WritingRuntime}..."); - - deviceInfo ??= await connection.GetDeviceInfo(CancellationToken); - - if (deviceInfo == null) - { - throw new CommandException(Strings.UnableToGetDeviceInfo); - } - - if (UseDfu || RequiresDfuForRuntimeUpdates(deviceInfo)) - { - var initialPorts = await MeadowConnectionManager.GetSerialPorts(); - - write_runtime: - if (!await connection!.Device!.WriteRuntime(runtimePath, CancellationToken)) - { - // TODO: implement a retry timeout - Logger?.LogInformation($"{Strings.ErrorWritingRuntime} - {Strings.Retrying}"); - goto write_runtime; - } - - connection = await GetCurrentConnection(true); - - if (connection == null) - { - var newPort = await WaitForNewSerialPort(initialPorts) ?? throw CommandException.MeadowDeviceNotFound; - connection = await GetCurrentConnection(true); - - Logger?.LogInformation($"{Strings.MeadowFoundAt} {newPort}"); - - // configure the route to that port for the user - Settings.SaveSetting(SettingsManager.PublicSettings.Route, newPort); - } - } - else - { - await connection.Device!.WriteFile(runtimePath, $"/{AppTools.MeadowRootFolder}/update/os/{destinationFilename}"); - } - - return connection; - } - - private async Task WriteEspFiles(IMeadowConnection? connection, DeviceInfo? deviceInfo, FirmwarePackage package) - { - connection ??= await GetConnectionAndDisableRuntime(); - - if (connection == null) { return; } // couldn't find a connected device - - Logger?.LogInformation($"{Environment.NewLine}{Strings.WritingCoprocessorFiles}..."); - - string[] fileList; - - if (IndividualFile != null) - { - fileList = new string[] { IndividualFile }; - } - else - { - fileList = new string[] - { - package.GetFullyQualifiedPath(package.CoprocApplication), - package.GetFullyQualifiedPath(package.CoprocBootloader), - package.GetFullyQualifiedPath(package.CoprocPartitionTable), - }; - } - - deviceInfo ??= await connection.GetDeviceInfo(CancellationToken); - - if (deviceInfo == null) { throw new CommandException(Strings.UnableToGetDeviceInfo); } - - if (UseDfu || RequiresDfuForEspUpdates(deviceInfo)) - { - await connection.Device!.WriteCoprocessorFiles(fileList, CancellationToken); - } - else - { - foreach (var file in fileList) - { - await connection!.Device!.WriteFile(file, $"/{AppTools.MeadowRootFolder}/update/os/{Path.GetFileName(file)}"); - await Task.Delay(500); - } - } - } - - private ILibUsbDevice? GetLibUsbDeviceForCurrentEnvironment(LibUsbProvider? provider) - { - provider ??= new LibUsbProvider(); - - var devices = provider.GetDevicesInBootloaderMode(); - - var meadowsInDFU = devices.Where(device => device.IsMeadow()).ToList(); - - if (meadowsInDFU.Count == 0) - { - return null; - } - - if (meadowsInDFU.Count == 1 || IgnoreSerialNumberForDfu(provider)) - { //IgnoreSerialNumberForDfu is a macOS-specific hack for Mark's machine - return meadowsInDFU.FirstOrDefault(); - } - - throw new CommandException(Strings.MultipleDfuDevicesFound); - } - - private async Task GetSelectedPackage() - { - await FileManager.Refresh(); - - var collection = FileManager.Firmware["Meadow F7"]; - FirmwarePackage package; - - if (Version != null) - { - // make sure the requested version exists - var existing = collection.FirstOrDefault(v => v.Version == Version); - - if (existing == null) - { - Logger?.LogError(string.Format(Strings.SpecifiedFirmwareVersionNotFound, Version)); - return null; - } - package = existing; - } - else - { - Version = collection.DefaultPackage?.Version ?? throw new CommandException($"{Strings.NoDefaultVersionSet}. {Strings.UseCommandFirmwareDefault}."); - - package = collection.DefaultPackage; - } - - return package; - } - - private async Task WriteOsWithDfu(ILibUsbDevice libUsbDevice, string osFile, bool ignoreSerialNumber = false) - { - string serialNumber; - - try - { //validate device - serialNumber = libUsbDevice.GetDeviceSerialNumber(); - } - catch - { - throw new CommandException($"{Strings.FirmwareWriteFailed} - {Strings.UnableToReadSerialNumber} ({Strings.MakeSureDeviceisConnected})"); - } - - try - { - if (ignoreSerialNumber) - { - serialNumber = string.Empty; - } - - await DfuUtils.FlashFile( - osFile, - serialNumber, - logger: Logger, - format: DfuUtils.DfuFlashFormat.ConsoleOut); - } - catch (ArgumentException) - { - throw new CommandException($"{Strings.FirmwareWriteFailed} - {Strings.IsDfuUtilInstalled} {Strings.RunMeadowDfuInstall}"); - } - catch (Exception ex) - { - Logger?.LogError($"Exception type: {ex.GetType().Name}"); - - // TODO: scope this to the right exception type for Win 10 access violation thing - // TODO: catch the Win10 DFU error here and change the global provider configuration to "classic" - Settings.SaveSetting(SettingsManager.PublicSettings.LibUsb, "classic"); - - throw new CommandException("This machine requires an older version of LibUsb. The CLI settings have been updated, re-run the 'firmware write' command to update your device."); - } - } - - private async Task> WaitForNewSerialPorts(IList? ignorePorts) - { - var ports = await MeadowConnectionManager.GetSerialPorts(); - - var retryCount = 0; - - while (ports.Count == 0) - { - if (retryCount++ > 10) - { - throw new CommandException(Strings.NewMeadowDeviceNotFound); - } - await Task.Delay(500); - ports = await MeadowConnectionManager.GetSerialPorts(); - } - - if (ignorePorts != null) - { - return ports.Except(ignorePorts).ToList(); - } - return ports.ToList(); - } - - private async Task WaitForNewSerialPort(IList? ignorePorts) - { - var ports = await WaitForNewSerialPorts(ignorePorts); - - return ports.FirstOrDefault(); - } - - private bool IgnoreSerialNumberForDfu(LibUsbProvider provider) - { //hack check for Mark's Mac - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - var devices = provider.GetDevicesInBootloaderMode(); - - if (devices.Count == 2) - { - if (devices[0].GetDeviceSerialNumber().Length > 12 || devices[1].GetDeviceSerialNumber().Length > 12) - { - return true; - } - } - } - - return false; } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Meadow.CLI.csproj b/Source/v2/Meadow.Cli/Meadow.CLI.csproj index 3624b1bf..d2209538 100644 --- a/Source/v2/Meadow.Cli/Meadow.CLI.csproj +++ b/Source/v2/Meadow.Cli/Meadow.CLI.csproj @@ -47,6 +47,7 @@ + diff --git a/Source/v2/Meadow.Cli/Properties/launchSettings.json b/Source/v2/Meadow.Cli/Properties/launchSettings.json index 2075af16..217050de 100644 --- a/Source/v2/Meadow.Cli/Properties/launchSettings.json +++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json @@ -279,6 +279,10 @@ "commandName": "Project", "commandLineArgs": "cloud package publish d867bb8b6e56418ba26ebe4e2b3ef6db -c 9d5f9780e6964c22b4ec15c44b886545" }, + "Provision Multiple Devices": { + "commandName": "Project", + "commandLineArgs": "provision" + }, "legacy list ports": { "commandName": "Project", "commandLineArgs": "list ports" diff --git a/Source/v2/Meadow.Cli/Strings.cs b/Source/v2/Meadow.Cli/Strings.cs index 4a0fb946..f6a7b35d 100644 --- a/Source/v2/Meadow.Cli/Strings.cs +++ b/Source/v2/Meadow.Cli/Strings.cs @@ -87,4 +87,44 @@ public static class Telemetry public const string AskToParticipate = "Would you like to participate?"; } + + public const string Enter = "Enter"; + public const string Space = "Space"; + + public static class Provision + { + public const string CommandDescription = "Provision 1 or more devices that are in DFU mode."; + public const string CommandOptionVersion = "Target OS version for devices to be provisioned with"; + public const string CommandOptionPath = "Path to the provision.json file"; + public const string RefreshDeviceList = "Flash devices (y=Flash selected devices, n=Refresh List)?"; + public const string MoreChoicesInstructions = "(Move up and down to reveal more devices)"; + public const string Instructions = "Press {0} to toggle a device, {1} to accept and flash the selected device"; + public const string RunningTitle = "Provisioning"; + public const string PromptTitle = "Devices in Bootloader mode"; + public const string NoDevicesFound = "No devices found in bootloader mode. Rerun this command when at least 1 connected device is in bootloader mode."; + public const string ColumnTitle = "Selected Devices"; + public const string NoDeviceSelected = "No devices selected to provision. Exiting."; + public const string UpdateFailed = "Update failed"; + public const string UpdateComplete = "Update completed"; + public const string AllDevicesFlashed = "All devices updated!"; + public const string FileNotFound = "Provision Settings file (provision.json), not found at location: {0}."; + public const string NoAppDeployment = "Skipping App Deployment and using default version: {0}"; + public const string DeployingApp = "Deploying App"; + public const string TrimmingApp = "Trimming App, before we get started"; + public const string ShowErrorMessages = "Show all error messages (y=Show Messages, n=Exit Immediately)?"; + public const string IssuesFound = "There were issues during the last provision."; + public const string ErrorSerialNumberColumnTitle = "Serial Number"; + public const string ErrorMessageColumnTitle = "Message"; + public const string ErrorStackTraceColumnTitle = "Stack Trace"; + public const string AppDllNotFound = "App.dll Not found at location"; + public const string FailedToReadProvisionFile = "Failed to read provision.json file"; + } + + public static class FirmwareUpdater + { + public const string FlashingOS = "Flashing OS"; + public const string WritingRuntime = "Writing Runtime"; + public const string WritingESP = "Writing ESP"; + public const string SwitchingToLibUsbClassic = "This machine requires an older version of LibUsb. The CLI settings have been updated, re-run the 'firmware write' command to update your device."; + } } \ No newline at end of file diff --git a/Source/v2/Meadow.Dfu/DfuUtils.cs b/Source/v2/Meadow.Dfu/DfuUtils.cs index b83e0bec..57d704b1 100644 --- a/Source/v2/Meadow.Dfu/DfuUtils.cs +++ b/Source/v2/Meadow.Dfu/DfuUtils.cs @@ -1,17 +1,14 @@ -using System; +using System; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Net.Http; using System.Runtime.InteropServices; -using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; -using Meadow.Hcom; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using static System.Net.Mime.MediaTypeNames; namespace Meadow.CLI.Core.Internals.Dfu; diff --git a/Source/v2/Meadow.Firmware/FirmwareWriter.cs b/Source/v2/Meadow.Firmware/FirmwareWriter.cs index 2541c9e9..07ca5c5d 100644 --- a/Source/v2/Meadow.Firmware/FirmwareWriter.cs +++ b/Source/v2/Meadow.Firmware/FirmwareWriter.cs @@ -47,7 +47,7 @@ public Task WriteOsWithDfu(string osFile, ILogger? logger = null, bool useLegacy default: throw new Exception("Multiple devices found in bootloader mode - only connect one device"); } - var serialNumber = devices.First().GetDeviceSerialNumber(); + var serialNumber = devices.First().SerialNumber; Debug.WriteLine($"DFU Writing file {osFile}"); diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs old mode 100755 new mode 100644 index 1d175e75..a6fadbe8 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -189,46 +189,51 @@ public override void Detach() public override async Task Attach(CancellationToken? cancellationToken = null, int timeoutSeconds = 10) { + CancellationTokenSource? timeoutCts = null; try { // ensure the port is open Open(); + timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds * 2)); + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, cancellationToken ?? CancellationToken.None); + var ct = linkedCts.Token; + // search for the device via HCOM - we'll use a simple command since we don't have a "ping" var command = RequestBuilder.Build(); // sequence numbers are only for file retrieval - Setting it to non-zero will cause it to hang - _port.DiscardInBuffer(); - // wait for a response - var timeout = timeoutSeconds * 2; - var dataReceived = false; - // local function so we can unsubscribe var count = _messageCount; - _pendingCommands.Enqueue(command); - _commandEvent.Set(); + EnqueueRequest(command); + + // Wait for a response + var taskCompletionSource = new TaskCompletionSource(); - while (timeout-- > 0) + _ = Task.Run(async () => { - if (cancellationToken?.IsCancellationRequested ?? false) return null; - if (timeout <= 0) throw new TimeoutException(); - - if (count != _messageCount) + while (!ct.IsCancellationRequested) { - dataReceived = true; - break; + if (count != _messageCount) + { + taskCompletionSource.TrySetResult(true); + break; + } + await Task.Delay(500, ct); } - - await Task.Delay(500); - } + if (!taskCompletionSource.Task.IsCompleted) + { + taskCompletionSource.TrySetException(new TimeoutException("Timeout waiting for device response")); + } + }, ct); // if HCOM fails, check for DFU/bootloader mode? only if we're doing an OS thing, so maybe no - // create the device instance - if (dataReceived) + // Create the device instance if the response is received + if (await taskCompletionSource.Task) { Device = new MeadowDevice(this); } @@ -240,6 +245,10 @@ public override void Detach() _logger?.LogError(e, "Failed to connect"); throw; } + finally + { + timeoutCts?.Dispose(); + } } private void CommandManager() diff --git a/Source/v2/Meadow.Hcom/Meadow.HCom.csproj b/Source/v2/Meadow.Hcom/Meadow.HCom.csproj index 6c0525fa..78f63c2b 100644 --- a/Source/v2/Meadow.Hcom/Meadow.HCom.csproj +++ b/Source/v2/Meadow.Hcom/Meadow.HCom.csproj @@ -16,7 +16,7 @@ - + diff --git a/Source/v2/Meadow.Tooling.Core/Connection/MeadowConnectionManager.cs b/Source/v2/Meadow.Tooling.Core/Connection/MeadowConnectionManager.cs index c2aa15c3..95b0b610 100644 --- a/Source/v2/Meadow.Tooling.Core/Connection/MeadowConnectionManager.cs +++ b/Source/v2/Meadow.Tooling.Core/Connection/MeadowConnectionManager.cs @@ -19,14 +19,17 @@ public class MeadowConnectionManager private static readonly object _lockObject = new(); private readonly ISettingsManager _settingsManager; - private IMeadowConnection? _currentConnection; + private static IMeadowConnection? _currentConnection; + + private static readonly int RETRY_COUNT = 10; + private static readonly int RETRY_DELAY = 500; public MeadowConnectionManager(ISettingsManager settingsManager) { _settingsManager = settingsManager; } - public IMeadowConnection? GetCurrentConnection(bool forceReconnect = false) + public async Task GetCurrentConnection(bool forceReconnect = false) { var route = _settingsManager.GetSetting(SettingsManager.PublicSettings.Route); @@ -35,22 +38,36 @@ public MeadowConnectionManager(ISettingsManager settingsManager) throw new Exception($"No 'route' configuration set.{Environment.NewLine}Use the `meadow config route` command. For example:{Environment.NewLine} > meadow config route COM5"); } - return GetConnectionForRoute(route, forceReconnect); + return await GetConnectionForRoute(route, forceReconnect); } - public IMeadowConnection? GetConnectionForRoute(string route, bool forceReconnect = false) + public static async Task GetConnectionForRoute(string route, bool forceReconnect = false) { // TODO: support connection changing (CLI does this rarely as it creates a new connection with each command) - if (_currentConnection != null && forceReconnect == false) + lock (_lockObject) { - return _currentConnection; + if (_currentConnection != null + && _currentConnection.Name == route + && forceReconnect == false) + { + return _currentConnection; + } + else if (_currentConnection != null) + { + _currentConnection.Detach(); + _currentConnection.Dispose(); + _currentConnection = null; + } } - _currentConnection?.Detach(); - _currentConnection?.Dispose(); if (route == "local") { - return new LocalConnection(); + var newConnection = new LocalConnection(); + lock (_lockObject) + { + _currentConnection = newConnection; + } + return _currentConnection; } // try to determine what the route is @@ -74,50 +91,59 @@ public MeadowConnectionManager(ISettingsManager settingsManager) if (uri != null) { - _currentConnection = new TcpConnection(uri); + var newConnection = new TcpConnection(uri); + lock (_lockObject) + { + _currentConnection = newConnection; + } + return _currentConnection; } else { - var retryCount = 0; - - get_serial_connection: - try - { - _currentConnection = new SerialConnection(route); - } - catch + for (int retryCount = 0; retryCount <= RETRY_COUNT; retryCount++) { - retryCount++; - if (retryCount > 10) + try + { + var newConnection = new SerialConnection(route); + lock (_lockObject) + { + _currentConnection = newConnection; + } + return _currentConnection; + } + catch { - throw new Exception($"Cannot find port {route}"); + if (retryCount == RETRY_COUNT) + { + throw new Exception($"Cannot create SerialConnection on route: {route}"); + } + + await Task.Delay(RETRY_DELAY); } - Thread.Sleep(500); - goto get_serial_connection; } - } - return _currentConnection; + // This line should never be reached because the loop will either return or throw + throw new Exception("Unexpected error in GetCurrentConnection"); + } } - public static async Task> GetSerialPorts() + public static async Task> GetSerialPorts(string? serialNumber = null) { try { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - return await GetMeadowSerialPortsForLinux(); + return await GetMeadowSerialPortsForLinux(serialNumber); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - return await GetMeadowSerialPortsForOsx(); + return await GetMeadowSerialPortsForOsx(serialNumber); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { lock (_lockObject) { - return GetMeadowSerialPortsForWindows(); + return GetMeadowSerialPortsForWindows(serialNumber); } } else @@ -131,9 +157,9 @@ public static async Task> GetSerialPorts() } } - public static async Task> GetMeadowSerialPortsForOsx() + public static async Task> GetMeadowSerialPortsForOsx(string? serialNumber) { - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) == false) + if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { throw new PlatformNotSupportedException("This method is only supported on macOS"); } @@ -187,6 +213,12 @@ public static async Task> GetMeadowSerialPortsForOsx() var port = line.Substring(startIndex, endIndex - startIndex); + if (!string.IsNullOrWhiteSpace(serialNumber) + && !port.Contains(serialNumber)) + { + continue; + } + ports.Add(port); foundMeadow = false; } @@ -196,16 +228,16 @@ public static async Task> GetMeadowSerialPortsForOsx() }); } - public static async Task> GetMeadowSerialPortsForLinux() + public static async Task> GetMeadowSerialPortsForLinux(string? serialNumber) { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) == false) + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { throw new PlatformNotSupportedException("This method is only supported on Linux"); } return await Task.Run(() => { - const string devicePath = "/dev/serial/by-id"; + const string devicePath = "/dev/serial/by-id/"; var psi = new ProcessStartInfo() { FileName = "ls", @@ -225,7 +257,8 @@ public static async Task> GetMeadowSerialPortsForLinux() return Array.Empty(); } - return output.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries) + var wlSerialPorts = output + .Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries) .Where(x => x.Contains("Wilderness_Labs")) .Select( line => @@ -234,13 +267,24 @@ public static async Task> GetMeadowSerialPortsForLinux() var target = parts[1]; var port = Path.GetFullPath(Path.Combine(devicePath, target)); return port; - }).ToArray(); + }); + + if (string.IsNullOrWhiteSpace(serialNumber)) + { + return wlSerialPorts.ToArray(); + } + else + { + return wlSerialPorts + .Where(line => !string.IsNullOrWhiteSpace(serialNumber) && line.Contains(serialNumber)) + .ToArray(); + } }); } - public static IList GetMeadowSerialPortsForWindows() + public static IList GetMeadowSerialPortsForWindows(string? serialNumber) { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) == false) + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { throw new PlatformNotSupportedException("This method is only supported on Windows"); } @@ -288,7 +332,11 @@ public static IList GetMeadowSerialPortsForWindows() // the characters: USB\VID_XXXX&PID_XXXX\ // so we'll just split is on \ and grab the 3rd element as the format is standard, but the length may vary. var splits = pnpDeviceId.Split('\\'); - var serialNumber = splits[2]; + + if (!string.IsNullOrWhiteSpace(serialNumber) + && !string.IsNullOrWhiteSpace(splits[2]) + && !splits[2].Contains(serialNumber)) + continue; results.Add($"{port}"); // removed serial number for consistency and will break fallback ({serialNumber})"); } @@ -306,4 +354,10 @@ public static IList GetMeadowSerialPortsForWindows() return ports; } } + + public static async Task GetRouteFromSerialNumber(string? serialNumber) + { + var results = await GetSerialPorts(serialNumber); + return results.FirstOrDefault(); + } } \ No newline at end of file diff --git a/Source/v2/Meadow.UsbLib.Core/ILibUsbDevice.cs b/Source/v2/Meadow.UsbLib.Core/ILibUsbDevice.cs index 61fb302f..9942fb83 100644 --- a/Source/v2/Meadow.UsbLib.Core/ILibUsbDevice.cs +++ b/Source/v2/Meadow.UsbLib.Core/ILibUsbDevice.cs @@ -10,7 +10,7 @@ public interface ILibUsbProvider public interface ILibUsbDevice : IDisposable { - string GetDeviceSerialNumber(); + string SerialNumber { get; } bool IsMeadow(); } \ No newline at end of file diff --git a/Source/v2/Meadow.UsbLib/LibUsbDevice.cs b/Source/v2/Meadow.UsbLib/LibUsbDevice.cs index 96cb39f9..11682562 100644 --- a/Source/v2/Meadow.UsbLib/LibUsbDevice.cs +++ b/Source/v2/Meadow.UsbLib/LibUsbDevice.cs @@ -7,9 +7,11 @@ namespace Meadow.LibUsb; public class LibUsbProvider : ILibUsbProvider { private const int UsbBootLoaderVendorID = 1155; + private const int UsbMeadowVendorID = 11882; internal static UsbContext _context; internal static List? _devices; + static LibUsbProvider() { // only ever create one of these - there's a bug in the LibUsbDotNet library and when this disposes, things go sideways @@ -30,20 +32,13 @@ public List GetDevicesInBootloaderMode() public class LibUsbDevice : ILibUsbDevice { private readonly IUsbDevice _device; + private string? serialNumber; + + public string? SerialNumber => serialNumber; public LibUsbDevice(IUsbDevice usbDevice) { _device = usbDevice; - } - - public void Dispose() - { - _device?.Dispose(); - } - - public string GetDeviceSerialNumber() - { - var serialNumber = string.Empty; _device.Open(); if (_device.IsOpen) @@ -51,17 +46,16 @@ public string GetDeviceSerialNumber() serialNumber = _device.Info?.SerialNumber ?? string.Empty; _device.Close(); } + } - return serialNumber; + public void Dispose() + { + _device?.Dispose(); } public bool IsMeadow() { - if (_device.VendorId != 1155) - { - return false; - } - if (GetDeviceSerialNumber().Length > 12) + if (serialNumber?.Length > 12) { return false; } diff --git a/Source/v2/Meadow.UsbLibClassic/ClassicLibUsbDevice.cs b/Source/v2/Meadow.UsbLibClassic/ClassicLibUsbDevice.cs index 5161e084..b9a30931 100644 --- a/Source/v2/Meadow.UsbLibClassic/ClassicLibUsbDevice.cs +++ b/Source/v2/Meadow.UsbLibClassic/ClassicLibUsbDevice.cs @@ -10,70 +10,71 @@ public class ClassicLibUsbProvider : ILibUsbProvider { private const string UsbStmName = "STM32 BOOTLOADER"; private const int UsbBootLoaderVendorID = 1155; + private const int UsbMeadowVendorID = 11882; - public List GetDevicesInBootloaderMode() - { - var propName = (Environment.OSVersion.Platform == PlatformID.Win32NT) + private string propName = (Environment.OSVersion.Platform == PlatformID.Win32NT) ? "FriendlyName" : "DeviceDesc"; + public List GetDevicesInBootloaderMode() + { return UsbDevice .AllDevices .Where(d => d.DeviceProperties[propName].ToString() == UsbStmName) .Select(d => new ClassicLibUsbDevice(d)) - .Cast() - .ToList(); + .ToList(); } -} - -public class ClassicLibUsbDevice : ILibUsbDevice -{ - private readonly UsbRegistry _device; - public ClassicLibUsbDevice(UsbRegistry usbDevice) + public class ClassicLibUsbDevice : ILibUsbDevice { - _device = usbDevice; - } + private readonly UsbRegistry _device; + private string? serialNumber; - public void Dispose() - { - _device.Device.Close(); - } + public string? SerialNumber => serialNumber; - public string GetDeviceSerialNumber() - { - if (_device != null && _device.DeviceProperties != null) + public ClassicLibUsbDevice(UsbRegistry usbDevice) { - switch (Environment.OSVersion.Platform) + _device = usbDevice; + + _device.Device.Open(); + if (_device.Device.IsOpen) { - case PlatformID.Win32NT: - var deviceID = _device.DeviceProperties["DeviceID"].ToString(); - if (!string.IsNullOrWhiteSpace(deviceID)) - { - return deviceID.Substring(deviceID.LastIndexOf("\\") + 1); - } - else + if (_device.DeviceProperties != null) + { + switch (Environment.OSVersion.Platform) { - return string.Empty; + case PlatformID.Win32NT: + var deviceID = _device.DeviceProperties["DeviceID"].ToString(); + if (!string.IsNullOrWhiteSpace(deviceID)) + { + serialNumber = deviceID.Substring(deviceID.LastIndexOf("\\") + 1); + } + else + { + serialNumber = string.Empty; + } + break; + default: + serialNumber = _device.DeviceProperties["SerialNumber"].ToString() ?? string.Empty; + break; } - default: - return _device.DeviceProperties["SerialNumber"].ToString() ?? string.Empty; + _device.Device.Close(); + } } } - return string.Empty; - } - - public bool IsMeadow() - { - if (_device.Vid != 1155) + public void Dispose() { - return false; + _device.Device.Close(); } - if (GetDeviceSerialNumber().Length > 12) + + public bool IsMeadow() { - return false; + if (SerialNumber?.Length > 12) + { + return false; + } + return true; } - return true; } } \ No newline at end of file From 1175815ddfb54b4273456382f4647ad36f979e37 Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Tue, 22 Oct 2024 15:02:30 +0100 Subject: [PATCH 3/3] Update CI versions. --- .github/workflows/dotnet.yml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index f418f3c9..1e99744f 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -1,7 +1,7 @@ name: Meadow.CLI Packaging env: CLI_RELEASE_VERSION_1: 1.9.4.0 - CLI_RELEASE_VERSION_2: 2.0.54.0 + CLI_RELEASE_VERSION_2: 2.0.17.0 IDE_TOOLS_RELEASE_VERSION: 1.9.4 MEADOW_OS_VERSION: 1.9.0.0 VS_MAC_2019_VERSION: 8.10 @@ -88,7 +88,7 @@ jobs: run: dotnet build main/MeadowCLI.Classic.sln /p:Configuration=Release - name: Upload nuget Artifacts for internal testing - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: Meadow.CLI.Classic.nuget.${{ ENV.CLI_RELEASE_VERSION_1 }} path: 'main\Meadow.CLI.Classic\bin\Release\*.nupkg' @@ -100,7 +100,7 @@ jobs: run: dotnet build main/MeadowCLI.sln /p:Configuration=Release - name: Upload nuget Artifacts for internal testing - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: Meadow.CLI.nuget.${{ ENV.CLI_RELEASE_VERSION_1 }} path: 'main\Meadow.CLI\bin\Release\*.nupkg' @@ -117,7 +117,7 @@ jobs: run: dotnet build main/Source/v2/Meadow.CLI.v2.sln /p:Configuration=Release - name: Upload nuget Artifacts for internal testing - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: Meadow.CLI.nuget.${{ ENV.CLI_RELEASE_VERSION_2 }} path: 'main\Source\v2\Meadow.CLI\bin\Release\*.nupkg' @@ -185,7 +185,7 @@ jobs: # DevEnvDir: 'C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\IDE' # - name: Upload VS2019 VSIX Artifacts - # uses: actions/upload-artifact@v2 + # uses: actions/upload-artifact@v4 # with: # name: Meadow.Win.VS2019.vsix.${{ ENV.IDE_TOOLS_RELEASE_VERSION }} # path: 'vs-win\VS_Meadow_Extension\VS_Meadow_Extension.2019\bin\Release\*.vsix' @@ -254,7 +254,7 @@ jobs: DevEnvDir: 'C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\IDE' - name: Upload VS2022 VSIX Artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: Meadow.Win.VS2022.vsix.${{ ENV.IDE_TOOLS_RELEASE_VERSION }} path: 'vs-win\VS_Meadow_Extension\VS_Meadow_Extension.2022\bin\Release\*.vsix' @@ -326,7 +326,7 @@ jobs: # msbuild vs-mac/VS4Mac_Meadow_Extension.sln /t:Build /p:Configuration=Release /p:CreatePackage=true # - name: Upload Mac VS2019 mpack Artifacts - # uses: actions/upload-artifact@v2 + # uses: actions/upload-artifact@v4 # with: # name: Meadow.Mac.2019.mpack.${{ ENV.IDE_TOOLS_RELEASE_VERSION }} # path: 'vs-mac/VS4Mac_Meadow_Extension/bin/Release/net472/*.mpack' @@ -435,7 +435,7 @@ jobs: dotnet msbuild vs-mac/VS4Mac_Meadow_Extension/Meadow.Sdks.IdeExtensions.Vs4Mac.2022.csproj /t:Build /p:Configuration=Release /p:CreatePackage=true - name: Upload VS2022 mpack Artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: Meadow.Mac.2022.mpack.${{ ENV.IDE_TOOLS_RELEASE_VERSION }} path: 'vs-mac/VS4Mac_Meadow_Extension/bin/Release/net7.0/*.mpack' @@ -532,10 +532,10 @@ jobs: - name: Setup Nuget uses: Nuget/setup-nuget@v1.0.5 - - name: Setup Node.js 16 - uses: actions/setup-node@v2 + - name: Setup Node.js 20 + uses: actions/setup-node@v4 with: - node-version: '16' + node-version: '20' - name: Install NPM run: | @@ -584,7 +584,7 @@ jobs: vsce package - name: Upload VSIX Artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: Meadow.VSCode.vsix.${{ ENV.IDE_TOOLS_RELEASE_VERSION }} path: 'vs-code/*.vsix' @@ -593,4 +593,4 @@ jobs: name: Publish VSCode Extension run: | cd vs-code - vsce publish -p ${{ secrets.MARKETPLACE_PUBLISH_PAT }} + vsce publish -p ${{ secrets.MARKETPLACE_PUBLISH_PAT }} \ No newline at end of file