From 493e1555f77df32cf9cdb0ff95e5ab3b0161bb6d Mon Sep 17 00:00:00 2001 From: Chris Tacke <ctacke@opennetcf.com> Date: Wed, 27 Sep 2023 12:05:54 -0500 Subject: [PATCH 1/6] added file delete to v2 --- Meadow.CLI/Properties/launchSettings.json | 4 +- Source/v2/Meadow.Cli/AppManager.cs | 11 +++++ .../Current/File/FileDeleteCommand.cs | 41 +++++++++++++++++++ .../Meadow.Cli/Properties/launchSettings.json | 6 ++- .../Meadow.Hcom/Connections/ConnectionBase.cs | 1 + .../Connections/SerialConnection.cs | 10 +++++ .../Meadow.Hcom/Connections/TcpConnection.cs | 5 +++ Source/v2/Meadow.Hcom/IMeadowConnection.cs | 1 + Source/v2/Meadow.Hcom/IMeadowDevice.cs | 1 + Source/v2/Meadow.Hcom/MeadowDevice.cs | 5 +++ .../Serial Requests/FileDeleteRequest.cs | 23 +++++++++++ .../Serial Requests/InitFileReadRequest.cs | 2 +- 12 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Requests/FileDeleteRequest.cs diff --git a/Meadow.CLI/Properties/launchSettings.json b/Meadow.CLI/Properties/launchSettings.json index a9bc47bc..8260665f 100644 --- a/Meadow.CLI/Properties/launchSettings.json +++ b/Meadow.CLI/Properties/launchSettings.json @@ -26,7 +26,7 @@ }, "FileDelete": { "commandName": "Project", - "commandLineArgs": "file delete -f" + "commandLineArgs": "file delete -f dns.conf" }, "FileList": { "commandName": "Project", @@ -106,7 +106,7 @@ }, "UsePort": { "commandName": "Project", - "commandLineArgs": "use port COM10" + "commandLineArgs": "use port COM7" }, "Version": { "commandName": "Project", diff --git a/Source/v2/Meadow.Cli/AppManager.cs b/Source/v2/Meadow.Cli/AppManager.cs index f80f3f76..2c4b8661 100644 --- a/Source/v2/Meadow.Cli/AppManager.cs +++ b/Source/v2/Meadow.Cli/AppManager.cs @@ -72,6 +72,17 @@ public static async Task DeployApplication( // get a list of files on-device, with CRCs var deviceFiles = await connection.GetFileList(true, cancellationToken); + // get a list of files of the device files that are not in the list we intend to deploy + var removeFiles = deviceFiles + .Select(f => f.Name) + .Except(localFiles.Keys + .Select(f => Path.GetFileName(f))); + + // delete those files + foreach (var file in removeFiles) + { + await connection.DeleteFile(file, cancellationToken); + } // erase all files on device not in list of files to send diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs new file mode 100644 index 00000000..9c2ea68f --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs @@ -0,0 +1,41 @@ +using CliFx.Attributes; +using Meadow.Hcom; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("file delete", Description = "Deletes a file from the device")] +public class FileDeleteCommand : BaseDeviceCommand<FileDeleteCommand> +{ + [CommandParameter(0, Name = "MeadowFile", IsRequired = true)] + public string MeadowFile { get; set; } = default!; + + public FileDeleteCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + : base(connectionManager, loggerFactory) + { + } + + protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken) + { + var fileList = await connection.GetFileList(false); + var exists = fileList?.Any(f => Path.GetFileName(f.Name) == MeadowFile) ?? false; + + if (!exists) + { + Logger.LogError($"File '{MeadowFile}' not found on device."); + } + else + { + var wasRuntimeEnabled = await device.IsRuntimeEnabled(cancellationToken); + + if (wasRuntimeEnabled) + { + Logger.LogError($"The runtime must be disabled before doing any file management. Use 'meadow runtime disable' first."); + return; + } + + Logger.LogInformation($"Deleting file '{MeadowFile}' from device..."); + await device.DeleteFile(MeadowFile, cancellationToken); + } + } +} diff --git a/Source/v2/Meadow.Cli/Properties/launchSettings.json b/Source/v2/Meadow.Cli/Properties/launchSettings.json index 25caa7bc..81a4e378 100644 --- a/Source/v2/Meadow.Cli/Properties/launchSettings.json +++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json @@ -41,7 +41,7 @@ }, "Config: Set Route Serial": { "commandName": "Project", - "commandLineArgs": "config route COM4" + "commandLineArgs": "config route COM7" }, "Config: Set Route TCP": { "commandName": "Project", @@ -75,6 +75,10 @@ "commandName": "Project", "commandLineArgs": "file list --verbose" }, + "File Delete": { + "commandName": "Project", + "commandLineArgs": "file delete meadow.log" + }, "File Read": { "commandName": "Project", "commandLineArgs": "file read test.txt \"f:\\temp\\test2.txt\"" diff --git a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs index ec7984e8..e2ee803e 100644 --- a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs +++ b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs @@ -20,6 +20,7 @@ public abstract class ConnectionBase : IMeadowConnection, IDisposable public abstract Task<MeadowFileInfo[]?> GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null); public abstract Task<bool> WriteFile(string localFileName, string? meadowFileName = null, CancellationToken? cancellationToken = null); public abstract Task<bool> ReadFile(string meadowFileName, string? localFileName = null, CancellationToken? cancellationToken = null); + public abstract Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null); public abstract Task ResetDevice(CancellationToken? cancellationToken = null); public abstract Task<bool> IsRuntimeEnabled(CancellationToken? cancellationToken = null); public abstract Task RuntimeDisable(CancellationToken? cancellationToken = null); diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs index 61eff517..9f4aed6d 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -1069,4 +1069,14 @@ void OnFileError(object? sender, Exception exception) FileException -= OnFileError; } } + + public override async Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build<FileDeleteRequest>(); + command.MeadowFileName = meadowFileName; + + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs index 49eeffd3..e4aecba4 100644 --- a/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs @@ -112,6 +112,11 @@ public override Task<bool> ReadFile(string meadowFileName, string? localFileName throw new NotImplementedException(); } + public override Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + public override Task<bool> WriteRuntime(string localFileName, CancellationToken? cancellationToken = null) { throw new NotImplementedException(); diff --git a/Source/v2/Meadow.Hcom/IMeadowConnection.cs b/Source/v2/Meadow.Hcom/IMeadowConnection.cs index 959cce8c..c0ba7adf 100644 --- a/Source/v2/Meadow.Hcom/IMeadowConnection.cs +++ b/Source/v2/Meadow.Hcom/IMeadowConnection.cs @@ -15,6 +15,7 @@ public interface IMeadowConnection Task<bool> WriteFile(string localFileName, string? meadowFileName = null, CancellationToken? cancellationToken = null); Task<bool> ReadFile(string meadowFileName, string? localFileName = null, CancellationToken? cancellationToken = null); + Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null); Task<DeviceInfo?> GetDeviceInfo(CancellationToken? cancellationToken = null); Task<MeadowFileInfo[]?> GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null); Task ResetDevice(CancellationToken? cancellationToken = null); diff --git a/Source/v2/Meadow.Hcom/IMeadowDevice.cs b/Source/v2/Meadow.Hcom/IMeadowDevice.cs index fce2fec5..30069d4a 100644 --- a/Source/v2/Meadow.Hcom/IMeadowDevice.cs +++ b/Source/v2/Meadow.Hcom/IMeadowDevice.cs @@ -10,6 +10,7 @@ public interface IMeadowDevice Task<MeadowFileInfo[]?> GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null); Task<bool> ReadFile(string meadowFileName, string? localFileName = null, CancellationToken? cancellationToken = null); Task<bool> WriteFile(string localFileName, string? meadowFileName = null, CancellationToken? cancellationToken = null); + Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null); Task<bool> WriteRuntime(string localFileName, CancellationToken? cancellationToken = null); Task<DateTimeOffset?> GetRtcTime(CancellationToken? cancellationToken = null); Task SetRtcTime(DateTimeOffset dateTime, CancellationToken? cancellationToken = null); diff --git a/Source/v2/Meadow.Hcom/MeadowDevice.cs b/Source/v2/Meadow.Hcom/MeadowDevice.cs index 9e9bacaf..775a126a 100644 --- a/Source/v2/Meadow.Hcom/MeadowDevice.cs +++ b/Source/v2/Meadow.Hcom/MeadowDevice.cs @@ -129,5 +129,10 @@ public async Task SetDeveloperParameter(ushort parameter, uint value, Cancellati { await _connection.SetDeveloperParameter(parameter, value, cancellationToken); } + + public async Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null) + { + await _connection.DeleteFile(meadowFileName, cancellationToken); + } } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Serial Requests/FileDeleteRequest.cs b/Source/v2/Meadow.Hcom/Serial Requests/FileDeleteRequest.cs new file mode 100644 index 00000000..a5eca198 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Requests/FileDeleteRequest.cs @@ -0,0 +1,23 @@ +using System.Text; + +namespace Meadow.Hcom; + +internal class FileDeleteRequest : Request +{ + public override RequestType RequestType => RequestType.HCOM_MDOW_REQUEST_DELETE_FILE_BY_NAME; + + public string MeadowFileName + { + get + { + if (Payload == null) return string.Empty; + return Encoding.ASCII.GetString(Payload, 44, Payload.Length - 44); + } + set + { + var nameBytes = Encoding.ASCII.GetBytes(value); + Payload = new byte[4 + 4 + 4 + 32 + nameBytes.Length]; + Array.Copy(nameBytes, 0, Payload, 44, nameBytes.Length); // file name + } + } +} diff --git a/Source/v2/Meadow.Hcom/Serial Requests/InitFileReadRequest.cs b/Source/v2/Meadow.Hcom/Serial Requests/InitFileReadRequest.cs index 44f1c9d1..cdd9001d 100644 --- a/Source/v2/Meadow.Hcom/Serial Requests/InitFileReadRequest.cs +++ b/Source/v2/Meadow.Hcom/Serial Requests/InitFileReadRequest.cs @@ -19,4 +19,4 @@ public string MeadowFileName Payload = Encoding.ASCII.GetBytes(value); } } -} +} \ No newline at end of file From 4734c07889bb76d5b601a46687760283623f1245 Mon Sep 17 00:00:00 2001 From: Chris Tacke <ctacke@opennetcf.com> Date: Wed, 27 Sep 2023 13:18:42 -0500 Subject: [PATCH 2/6] more app deploy work --- Source/v2/Meadow.Cli/AppManager.cs | 24 +++++++-- .../Commands/Current/App/AppDeployCommand.cs | 52 +++++++++++++------ .../Commands/Current/App/AppTrimCommand.cs | 2 +- .../PackageManager.AssemblyManager.cs | 7 +-- .../Meadow.Cli/Properties/launchSettings.json | 8 +++ .../Connections/SerialConnection.cs | 2 + 6 files changed, 71 insertions(+), 24 deletions(-) diff --git a/Source/v2/Meadow.Cli/AppManager.cs b/Source/v2/Meadow.Cli/AppManager.cs index 2c4b8661..cfc51702 100644 --- a/Source/v2/Meadow.Cli/AppManager.cs +++ b/Source/v2/Meadow.Cli/AppManager.cs @@ -70,23 +70,37 @@ public static async Task DeployApplication( } // get a list of files on-device, with CRCs - var deviceFiles = await connection.GetFileList(true, cancellationToken); + var deviceFiles = await connection.GetFileList(true, cancellationToken) ?? Array.Empty<MeadowFileInfo>(); // get a list of files of the device files that are not in the list we intend to deploy var removeFiles = deviceFiles - .Select(f => f.Name) + .Select(f => Path.GetFileName(f.Name)) .Except(localFiles.Keys .Select(f => Path.GetFileName(f))); // delete those files foreach (var file in removeFiles) { + logger.LogInformation($"Deleting file '{file}'..."); await connection.DeleteFile(file, cancellationToken); } - // erase all files on device not in list of files to send - - // send any file that has a different CRC + // now send all files with differing CRCs + foreach (var localFile in localFiles) + { + var existing = deviceFiles.FirstOrDefault(f => Path.GetFileName(f.Name) == Path.GetFileName(localFile.Key)); + + if (existing != null) + { + if (int.Parse(existing.Crc.Substring(2), System.Globalization.NumberStyles.HexNumber) == localFile.Value) + { + // exists and has a matching CRC, skip it + continue; + } + } + + await connection?.WriteFile(localFile.Key, null, cancellationToken); + } if (wasRuntimeEnabled) diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs index 3ac340d5..8f021015 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs @@ -23,6 +23,24 @@ protected override async ValueTask ExecuteCommand(IMeadowConnection connection, : Path; // is the path a file? + FileInfo file; + + var lastFile = string.Empty; + + connection.FileWriteProgress += (s, e) => + { + var p = (e.completed / (double)e.total) * 100d; + + if (e.fileName != lastFile) + { + Console.Write("\n"); + lastFile = e.fileName; + } + + // Console instead of Logger due to line breaking for progress bar + Console.Write($"Writing {e.fileName}: {p:0}% \r"); + }; + if (!File.Exists(path)) { // is it a valid directory? @@ -31,27 +49,31 @@ protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Logger.LogError($"Invalid application path '{path}'"); return; } - } - else - { - // TODO: only deploy if it's App.dll - } - - // do we have the full app path, or just the project root? - // TODO: determine the latest build - - await AppManager.DeployApplication(connection, "", true, false, Logger, cancellationToken); + // does the directory have an App.dll in it? + file = new FileInfo(System.IO.Path.Combine(path, "App.dll")); + if (!file.Exists) + { + // it's a directory - we need to determine the latest build (they might have a Debug and a Release config) + var candidates = Cli.PackageManager.GetAvailableBuiltConfigurations(path, "App.dll"); - var success = false; + if (candidates.Length == 0) + { + Logger.LogError($"Cannot find a compiled application at '{path}'"); + return; + } - if (!success) - { - Logger.LogError($"Build failed!"); + file = candidates.OrderByDescending(c => c.LastWriteTime).First(); + } } else { - Logger.LogError($"Build success."); + // TODO: only deploy if it's App.dll + file = new FileInfo(path); } + + var targetDirectory = file.DirectoryName; + + await AppManager.DeployApplication(connection, targetDirectory, true, false, Logger, cancellationToken); } } diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs index 60b4d164..6fe51fec 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs @@ -39,7 +39,7 @@ protected override async ValueTask ExecuteCommand(CancellationToken cancellation return; } - // it's a directory - we need to determine the latest build (they might have a Debug and Release config) + // it's a directory - we need to determine the latest build (they might have a Debug and a Release config) var candidates = PackageManager.GetAvailableBuiltConfigurations(path, "App.dll"); if (candidates.Length == 0) diff --git a/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs b/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs index 392ce786..49431f76 100644 --- a/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs +++ b/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs @@ -8,6 +8,9 @@ namespace Meadow.Cli; public partial class PackageManager { + private const string IL_LINKER_DIR = "lib"; + public const string PostLinkDirectoryName = "postlink_bin"; + private readonly List<string> dependencyMap = new(); private string? _meadowAssembliesPath; @@ -29,8 +32,6 @@ private string? MeadowAssembliesPath } } - private const string IL_LINKER_DIR = "lib"; - public async Task<IEnumerable<string>?> TrimDependencies(FileInfo file, List<string> dependencies, IList<string>? noLink, ILogger? logger, bool includePdbs, bool verbose = false, string? linkerOptions = null) { var prelink_dir = Path.Combine(file.DirectoryName, "prelink_bin"); @@ -63,7 +64,7 @@ private string? MeadowAssembliesPath } } - var postlink_dir = Path.Combine(file.DirectoryName, "postlink_bin"); + var postlink_dir = Path.Combine(file.DirectoryName, PostLinkDirectoryName); if (Directory.Exists(postlink_dir)) { Directory.Delete(postlink_dir, recursive: true); diff --git a/Source/v2/Meadow.Cli/Properties/launchSettings.json b/Source/v2/Meadow.Cli/Properties/launchSettings.json index 81a4e378..fa80a0bc 100644 --- a/Source/v2/Meadow.Cli/Properties/launchSettings.json +++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json @@ -174,6 +174,14 @@ "Dfu Install 0.10": { "commandName": "Project", "commandLineArgs": "dfu install -v 0.10" + }, + "App Deploy (project folder)": { + "commandName": "Project", + "commandLineArgs": "app deploy F:\\temp\\MeadowApplication1" + }, + "App Deploy (untrimmed output)": { + "commandName": "Project", + "commandLineArgs": "app deploy F:\\temp\\MeadowApplication1\\bin\\Debug\\netstandard2.1" } } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs index 9f4aed6d..82ef0575 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -1075,6 +1075,8 @@ public override async Task DeleteFile(string meadowFileName, CancellationToken? var command = RequestBuilder.Build<FileDeleteRequest>(); command.MeadowFileName = meadowFileName; + _lastRequestConcluded = null; + EnqueueRequest(command); await WaitForConcluded(null, cancellationToken); From 1cfd9ab44289d52f71bf318f731e7ffd43402320 Mon Sep 17 00:00:00 2001 From: Chris Tacke <ctacke@opennetcf.com> Date: Wed, 27 Sep 2023 13:29:13 -0500 Subject: [PATCH 3/6] app deploy cleanup --- Source/v2/Meadow.Cli/AppManager.cs | 22 +------------------ .../Commands/Current/App/AppDeployCommand.cs | 17 ++++++++++++++ 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/Source/v2/Meadow.Cli/AppManager.cs b/Source/v2/Meadow.Cli/AppManager.cs index cfc51702..a957abbf 100644 --- a/Source/v2/Meadow.Cli/AppManager.cs +++ b/Source/v2/Meadow.Cli/AppManager.cs @@ -34,16 +34,6 @@ public static async Task DeployApplication( ILogger logger, CancellationToken cancellationToken) { - // in order to deploy, the runtime must be disabled - var wasRuntimeEnabled = await connection.IsRuntimeEnabled(); - - if (wasRuntimeEnabled) - { - logger.LogInformation("Disabling runtime..."); - - await connection.RuntimeDisable(cancellationToken); - } - // TODO: add sub-folder support when HCOM supports it var localFiles = new Dictionary<string, uint>(); @@ -92,7 +82,7 @@ public static async Task DeployApplication( if (existing != null) { - if (int.Parse(existing.Crc.Substring(2), System.Globalization.NumberStyles.HexNumber) == localFile.Value) + if (uint.Parse(existing.Crc.Substring(2), System.Globalization.NumberStyles.HexNumber) == localFile.Value) { // exists and has a matching CRC, skip it continue; @@ -101,15 +91,5 @@ public static async Task DeployApplication( await connection?.WriteFile(localFile.Key, null, cancellationToken); } - - - if (wasRuntimeEnabled) - { - // restore runtime state - logger.LogInformation("Enabling runtime..."); - - await connection.RuntimeEnable(cancellationToken); - } - } } diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs index 8f021015..b061b524 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs @@ -27,6 +27,15 @@ protected override async ValueTask ExecuteCommand(IMeadowConnection connection, var lastFile = string.Empty; + // in order to deploy, the runtime must be disabled + var wasRuntimeEnabled = await connection.IsRuntimeEnabled(); + if (wasRuntimeEnabled) + { + Logger.LogInformation("Disabling runtime..."); + + await connection.RuntimeDisable(cancellationToken); + } + connection.FileWriteProgress += (s, e) => { var p = (e.completed / (double)e.total) * 100d; @@ -75,5 +84,13 @@ protected override async ValueTask ExecuteCommand(IMeadowConnection connection, var targetDirectory = file.DirectoryName; await AppManager.DeployApplication(connection, targetDirectory, true, false, Logger, cancellationToken); + + if (wasRuntimeEnabled) + { + // restore runtime state + Logger.LogInformation("Enabling runtime..."); + + await connection.RuntimeEnable(cancellationToken); + } } } From 2129c50ee7d4ab95adc968c7f2f269bbe7a0ebe3 Mon Sep 17 00:00:00 2001 From: Chris Tacke <ctacke@opennetcf.com> Date: Wed, 27 Sep 2023 17:45:16 -0500 Subject: [PATCH 4/6] Added app run command --- Source/v2/Meadow.Cli/AppManager.cs | 5 +- .../Commands/Current/App/AppBuildCommand.cs | 2 +- .../Commands/Current/App/AppDeployCommand.cs | 7 +- .../Commands/Current/App/AppRunCommand.cs | 163 ++++++++++++++++++ Source/v2/Meadow.Cli/IPackageManager.cs | 1 + .../PackageManager.AssemblyManager.cs | 5 +- Source/v2/Meadow.Cli/PackageManager.cs | 12 +- .../Meadow.Cli/Properties/launchSettings.json | 4 + .../SerialConnection.ListenerProc.cs | 5 + .../Connections/SerialConnection.cs | 4 +- 10 files changed, 196 insertions(+), 12 deletions(-) create mode 100644 Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs diff --git a/Source/v2/Meadow.Cli/AppManager.cs b/Source/v2/Meadow.Cli/AppManager.cs index a957abbf..aa09722f 100644 --- a/Source/v2/Meadow.Cli/AppManager.cs +++ b/Source/v2/Meadow.Cli/AppManager.cs @@ -27,6 +27,7 @@ private static bool IsXmlDoc(string file) } public static async Task DeployApplication( + IPackageManager packageManager, IMeadowConnection connection, string localBinaryDirectory, bool includePdbs, @@ -39,8 +40,10 @@ public static async Task DeployApplication( var localFiles = new Dictionary<string, uint>(); // get a list of files to send + var dependencies = packageManager.GetDependencies(new FileInfo(Path.Combine(localBinaryDirectory, "App.dll"))); + logger.LogInformation("Generating the list of files to deploy..."); - foreach (var file in Directory.GetFiles(localBinaryDirectory)) + foreach (var file in dependencies) { // TODO: add any other filtering capability here diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs index 1296f80e..5513faa9 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs @@ -40,7 +40,7 @@ protected override async ValueTask ExecuteCommand(CancellationToken cancellation if (Configuration == null) Configuration = "Release"; - Logger.LogInformation($"Building {Configuration} configuration of of {path}..."); + Logger.LogInformation($"Building {Configuration} configuration of {path}..."); // TODO: enable cancellation of this call var success = _packageManager.BuildApplication(path, Configuration); diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs index b061b524..535c718c 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs @@ -8,12 +8,15 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("app deploy", Description = "Deploys a built Meadow application to a target device")] public class AppDeployCommand : BaseDeviceCommand<AppDeployCommand> { + private IPackageManager _packageManager; + [CommandParameter(0, Name = "Path to folder containing the built application", IsRequired = false)] public string? Path { get; set; } = default!; - public AppDeployCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + public AppDeployCommand(IPackageManager packageManager, MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) { + _packageManager = packageManager; } protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken) @@ -83,7 +86,7 @@ protected override async ValueTask ExecuteCommand(IMeadowConnection connection, var targetDirectory = file.DirectoryName; - await AppManager.DeployApplication(connection, targetDirectory, true, false, Logger, cancellationToken); + await AppManager.DeployApplication(_packageManager, connection, targetDirectory, true, false, Logger, cancellationToken); if (wasRuntimeEnabled) { diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs new file mode 100644 index 00000000..85caf8b2 --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs @@ -0,0 +1,163 @@ +using CliFx.Attributes; +using Meadow.Cli; +using Meadow.Hcom; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("app run", Description = "Builds, trims and deploys a Meadow application to a target device")] +public class AppRunCommand : BaseDeviceCommand<AppRunCommand> +{ + private IPackageManager _packageManager; + private string _lastFile; + + [CommandOption("no-prefix", 'n', IsRequired = false, Description = "When set, the message source prefix (e.g. 'stdout>') is suppressed during 'listen'")] + public bool NoPrefix { get; set; } + + [CommandOption('c', Description = "The build configuration to compile", IsRequired = false)] + public string? Configuration { get; set; } + + [CommandParameter(0, Name = "Path to folder containing the built application", IsRequired = false)] + public string? Path { get; set; } = default!; + + public AppRunCommand(IPackageManager packageManager, MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + : base(connectionManager, loggerFactory) + { + _packageManager = packageManager; + } + + protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken) + { + string path = Path == null + ? AppDomain.CurrentDomain.BaseDirectory + : Path; + + if (!Directory.Exists(path)) + { + Logger.LogError($"Target directory '{path}' not found."); + return; + } + + var lastFile = string.Empty; + + // in order to deploy, the runtime must be disabled + var wasRuntimeEnabled = await connection.IsRuntimeEnabled(); + if (wasRuntimeEnabled) + { + Logger.LogInformation("Disabling runtime..."); + + await connection.RuntimeDisable(cancellationToken); + } + + if (!await BuildApplication(path, cancellationToken)) + { + return; + } + + if (!await TrimApplication(path, cancellationToken)) + { + return; + } + + // illink returns before all files are actually written. That's not fun, but we must just wait a little while. + await Task.Delay(1000); + + if (!await DeployApplication(connection, path, cancellationToken)) + { + return; + } + + Logger.LogInformation("Enabling the runtime..."); + await connection.RuntimeEnable(cancellationToken); + + Logger.LogInformation("Listening for messages from Meadow...\n"); + connection.DeviceMessageReceived += OnDeviceMessageReceived; + + while (!cancellationToken.IsCancellationRequested) + { + await Task.Delay(1000); + } + + Logger.LogInformation("Listen cancelled..."); + } + + private Task<bool> BuildApplication(string path, CancellationToken cancellationToken) + { + if (Configuration == null) Configuration = "Debug"; + + Logger.LogInformation($"Building {Configuration} configuration of {path}..."); + + // TODO: enable cancellation of this call + return Task.FromResult(_packageManager.BuildApplication(path, Configuration)); + } + + private async Task<bool> TrimApplication(string path, CancellationToken cancellationToken) + { + // it's a directory - we need to determine the latest build (they might have a Debug and a Release config) + var candidates = Cli.PackageManager.GetAvailableBuiltConfigurations(path, "App.dll"); + + if (candidates.Length == 0) + { + Logger.LogError($"Cannot find a compiled application at '{path}'"); + return false; + } + + var file = candidates.OrderByDescending(c => c.LastWriteTime).First(); + + // if no configuration was provided, find the most recently built + Logger.LogInformation($"Trimming {file.FullName} (this may take a few seconds)..."); + + await _packageManager.TrimApplication(file, false, null, cancellationToken); + + return true; + } + + private async Task<bool> DeployApplication(IMeadowConnection connection, string path, CancellationToken cancellationToken) + { + connection.FileWriteProgress += OnFileWriteProgress; + + var candidates = Cli.PackageManager.GetAvailableBuiltConfigurations(path, "App.dll"); + + if (candidates.Length == 0) + { + Logger.LogError($"Cannot find a compiled application at '{path}'"); + return false; + } + + var file = candidates.OrderByDescending(c => c.LastWriteTime).First(); + + Logger.LogInformation($"Deploying app from {file.DirectoryName}..."); + + await AppManager.DeployApplication(_packageManager, connection, file.DirectoryName, true, false, Logger, cancellationToken); + + connection.FileWriteProgress -= OnFileWriteProgress; + + return true; + } + + private void OnFileWriteProgress(object? sender, (string fileName, long completed, long total) e) + { + var p = (e.completed / (double)e.total) * 100d; + + if (e.fileName != _lastFile) + { + Console.Write("\n"); + _lastFile = e.fileName; + } + + // Console instead of Logger due to line breaking for progress bar + Console.Write($"Writing {e.fileName}: {p:0}% \r"); + } + + private void OnDeviceMessageReceived(object? sender, (string message, string? source) e) + { + if (NoPrefix) + { + Logger.LogInformation($"{e.message.TrimEnd('\n', '\r')}"); + } + else + { + Logger.LogInformation($"{e.source}> {e.message.TrimEnd('\n', '\r')}"); + } + } +} diff --git a/Source/v2/Meadow.Cli/IPackageManager.cs b/Source/v2/Meadow.Cli/IPackageManager.cs index 55909a18..6012cd4c 100644 --- a/Source/v2/Meadow.Cli/IPackageManager.cs +++ b/Source/v2/Meadow.Cli/IPackageManager.cs @@ -2,6 +2,7 @@ public interface IPackageManager { + List<string> GetDependencies(FileInfo file); bool BuildApplication(string projectFilePath, string configuration = "Release"); Task TrimApplication( FileInfo applicationFilePath, diff --git a/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs b/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs index 49431f76..a2f66282 100644 --- a/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs +++ b/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs @@ -10,6 +10,7 @@ public partial class PackageManager { private const string IL_LINKER_DIR = "lib"; public const string PostLinkDirectoryName = "postlink_bin"; + public const string PreLinkDirectoryName = "prelink_bin"; private readonly List<string> dependencyMap = new(); @@ -34,7 +35,7 @@ private string? MeadowAssembliesPath public async Task<IEnumerable<string>?> TrimDependencies(FileInfo file, List<string> dependencies, IList<string>? noLink, ILogger? logger, bool includePdbs, bool verbose = false, string? linkerOptions = null) { - var prelink_dir = Path.Combine(file.DirectoryName, "prelink_bin"); + var prelink_dir = Path.Combine(file.DirectoryName, PreLinkDirectoryName); var prelink_app = Path.Combine(prelink_dir, file.Name); var prelink_os = Path.Combine(prelink_dir, "Meadow.dll"); @@ -149,7 +150,7 @@ private string? MeadowAssembliesPath return Directory.EnumerateFiles(postlink_dir); } - private List<string> GetDependencies(FileInfo file) + public List<string> GetDependencies(FileInfo file) { dependencyMap.Clear(); diff --git a/Source/v2/Meadow.Cli/PackageManager.cs b/Source/v2/Meadow.Cli/PackageManager.cs index 480b959c..7f9fbd58 100644 --- a/Source/v2/Meadow.Cli/PackageManager.cs +++ b/Source/v2/Meadow.Cli/PackageManager.cs @@ -108,10 +108,6 @@ await TrimDependencies( verbose: false); } - public async Task DeployApplication() - { - } - public static FileInfo[] GetAvailableBuiltConfigurations(string rootFolder, string appName = "App.dll") { if (!Directory.Exists(rootFolder)) throw new FileNotFoundException(); @@ -127,6 +123,13 @@ void FindApp(string directory, List<FileInfo> fileList) { foreach (var dir in Directory.GetDirectories(directory)) { + var shortname = System.IO.Path.GetFileName(dir); + + if (shortname == PackageManager.PostLinkDirectoryName || shortname == PackageManager.PreLinkDirectoryName) + { + continue; + } + var file = Directory.GetFiles(dir).FirstOrDefault(f => string.Compare(Path.GetFileName(f), appName, true) == 0); if (file != null) { @@ -134,6 +137,7 @@ void FindApp(string directory, List<FileInfo> fileList) } FindApp(dir, fileList); + } } diff --git a/Source/v2/Meadow.Cli/Properties/launchSettings.json b/Source/v2/Meadow.Cli/Properties/launchSettings.json index fa80a0bc..f656d678 100644 --- a/Source/v2/Meadow.Cli/Properties/launchSettings.json +++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json @@ -182,6 +182,10 @@ "App Deploy (untrimmed output)": { "commandName": "Project", "commandLineArgs": "app deploy F:\\temp\\MeadowApplication1\\bin\\Debug\\netstandard2.1" + }, + "App run": { + "commandName": "Project", + "commandLineArgs": "app run F:\\temp\\MeadowApplication1" } } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs index b8162443..53e599c3 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs @@ -257,6 +257,11 @@ private async Task ListenerProc() // common if the port is reset/closed (e.g. mono enable/disable) - don't spew confusing info Debug.WriteLine($"listen on closed port"); } + catch (OperationCanceledException) + { + Debug.WriteLine($"Operation Cancelled"); + // TODO: figure out why this happens occasionally + } catch (Exception ex) { Debug.WriteLine($"listen error {ex.Message}"); diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs index 82ef0575..e901ec8d 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -223,7 +223,7 @@ private void Close() } } - private void CommandManager() + private async void CommandManager() { while (!_isDisposed) { @@ -236,7 +236,7 @@ private void CommandManager() // if this is a file write, we need to packetize for progress var payload = command.Serialize(); - EncodeAndSendPacket(payload); + await EncodeAndSendPacket(payload); // TODO: re-queue on fail? } From 78783983e453910d9ecf5dee211786b8323db48d Mon Sep 17 00:00:00 2001 From: Chris Tacke <ctacke@opennetcf.com> Date: Thu, 28 Sep 2023 09:30:23 -0500 Subject: [PATCH 5/6] handling of device reset --- .../SerialConnection.ListenerProc.cs | 37 ++++++++++++++++++- .../Connections/SerialConnection.cs | 26 ++++--------- 2 files changed, 42 insertions(+), 21 deletions(-) diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs index 53e599c3..5c6075c8 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs @@ -32,6 +32,7 @@ private async Task ListenerProc() var decodedBuffer = new byte[8192]; var messageBytes = new CircularBuffer<byte>(8192 * 2); var delimiter = new byte[] { 0x00 }; + var receivedLength = 0; while (!_isDisposed) { @@ -41,7 +42,39 @@ private async Task ListenerProc() { Debug.WriteLine($"listening..."); - var receivedLength = _port.BaseStream.Read(readBuffer, 0, readBuffer.Length); + read: + try + { + receivedLength = _port.BaseStream.Read(readBuffer, 0, readBuffer.Length); + } + catch (OperationCanceledException) + { + Debug.WriteLine($"Device reset detected"); + + var timeout = 20; + + while (!_port.IsOpen) + { + await Task.Delay(500); + + if (timeout-- < 0) + { + return; + } + + try + { + _port.Open(); + Debug.WriteLine($"Port re-opened"); + } + catch + { + Debug.WriteLine($"Failed to re-open port"); + } + } + + goto read; + } Debug.WriteLine($"Received {receivedLength} bytes"); @@ -259,8 +292,8 @@ private async Task ListenerProc() } catch (OperationCanceledException) { + // this happens on disconnect - could be cable pulled, could be device reset Debug.WriteLine($"Operation Cancelled"); - // TODO: figure out why this happens occasionally } catch (Exception ex) { diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs index e901ec8d..f94a982b 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -461,7 +461,7 @@ private bool DecodeAndProcessPacket(Memory<byte> packetBuffer, CancellationToken return true; } - protected virtual void Dispose(bool disposing) + protected override void Dispose(bool disposing) { if (!_isDisposed) { @@ -475,12 +475,6 @@ protected virtual void Dispose(bool disposing) } } - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - // ---------------------------------------------- // ---------------------------------------------- // ---------------------------------------------- @@ -640,14 +634,11 @@ public override async Task RuntimeEnable(CancellationToken? cancellationToken = InfoMessages.Clear(); - EnqueueRequest(command); - - // we have to give time for the device to actually reset - await Task.Delay(500); + _lastRequestConcluded = null; - var success = await WaitForResponseText(RuntimeSucessfullyEnabledToken); + EnqueueRequest(command); - if (!success) throw new Exception("Unable to enable runtime"); + await WaitForConcluded(null, cancellationToken); } public override async Task RuntimeDisable(CancellationToken? cancellationToken = null) @@ -656,14 +647,11 @@ public override async Task RuntimeDisable(CancellationToken? cancellationToken = InfoMessages.Clear(); - EnqueueRequest(command); - - // we have to give time for the device to actually reset - await Task.Delay(500); + _lastRequestConcluded = null; - var success = await WaitForResponseText(RuntimeSucessfullyDisabledToken); + EnqueueRequest(command); - if (!success) throw new Exception("Unable to disable runtime"); + await WaitForConcluded(null, cancellationToken); } public override async Task TraceEnable(CancellationToken? cancellationToken = null) From 1d104870e57b1b1fe0307a69fff89ffc07f175d1 Mon Sep 17 00:00:00 2001 From: Chris Tacke <ctacke@opennetcf.com> Date: Thu, 28 Sep 2023 10:50:07 -0500 Subject: [PATCH 6/6] fix WaitForAttach when we have ever been attached (i.e. DFU -> boot) --- .../Meadow.Cli/Properties/launchSettings.json | 2 +- .../SerialConnection.ListenerProc.cs | 25 +++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/Source/v2/Meadow.Cli/Properties/launchSettings.json b/Source/v2/Meadow.Cli/Properties/launchSettings.json index f656d678..ee03220d 100644 --- a/Source/v2/Meadow.Cli/Properties/launchSettings.json +++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json @@ -113,7 +113,7 @@ }, "Firmware Write all": { "commandName": "Project", - "commandLineArgs": "firmware write runtime esp" + "commandLineArgs": "firmware write" }, "Firmware Write version": { "commandName": "Project", diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs index 5c6075c8..c253b398 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs @@ -18,9 +18,30 @@ public override async Task WaitForMeadowAttach(CancellationToken? cancellationTo if (cancellationToken?.IsCancellationRequested ?? false) throw new TaskCanceledException(); if (timeout <= 0) throw new TimeoutException(); - if (State == ConnectionState.MeadowAttached) return; + if (State == ConnectionState.MeadowAttached) + { + if (Device == null) + { + // no device set - this happens when we are waiting for attach from DFU mode + await Attach(cancellationToken, 5); + } + + return; + } await Task.Delay(500); + + if (!_port.IsOpen) + { + try + { + Open(); + } + catch (Exception ex) + { + Debug.WriteLine($"Unable to open port: {ex.Message}"); + } + } } throw new TimeoutException(); @@ -64,7 +85,7 @@ private async Task ListenerProc() try { - _port.Open(); + Open(); Debug.WriteLine($"Port re-opened"); } catch