diff --git a/ArgParser.cs b/ArgParser.cs index b961a16..7f3cfdc 100644 --- a/ArgParser.cs +++ b/ArgParser.cs @@ -75,6 +75,10 @@ namespace NcProgramManager if (!string.IsNullOrEmpty(pathprogramma)) inputArgs.pathLocaleProgramma = pathprogramma.Substring(15); + var ftpDir = arguments.Find(it => new Regex("-ftpdir=.*", RegexOptions.IgnoreCase).IsMatch(it)); + if (!string.IsNullOrEmpty(ftpDir)) + inputArgs.ftpDir = ftpDir.Substring(8); + var hasToolOffsetData = arguments.Find(it => new Regex("-tooloffset.*", RegexOptions.IgnoreCase).IsMatch(it)); if (!string.IsNullOrEmpty(hasToolOffsetData)) inputArgs.hasToolOffsetData = true; diff --git a/Cnc/CncMachineFactory.cs b/Cnc/CncMachineFactory.cs index 397ab66..54f2122 100644 --- a/Cnc/CncMachineFactory.cs +++ b/Cnc/CncMachineFactory.cs @@ -63,7 +63,8 @@ namespace NcProgramManager.Cnc IpAddress = args.ip, Port = string.IsNullOrEmpty(args.porta) ? 21 : int.Parse(args.porta), Username = args.username, - Password = args.password + Password = args.password, + ProgramDirectory = string.IsNullOrEmpty(args.ftpDir) ? "/" : args.ftpDir }); default: // Fanuc diff --git a/Cnc/Mazak/FtpEntry.cs b/Cnc/Mazak/FtpEntry.cs new file mode 100644 index 0000000..be6e3c8 --- /dev/null +++ b/Cnc/Mazak/FtpEntry.cs @@ -0,0 +1,24 @@ +namespace NcProgramManager.Cnc.Mazak +{ + /// Single entry of an FTP directory listing (file or directory). + public sealed class FtpEntry + { + /// Entry name (no path). + public string Name { get; set; } + /// Full FTP path of the entry (parent dir + name). + public string FullPath { get; set; } + /// True if the entry is a directory. + public bool IsDirectory { get; set; } + /// File size in bytes; -1 when unknown (directories). + public long Size { get; set; } = -1; + /// Raw LIST line as returned by the server (diagnostics). + public string Raw { get; set; } + + public override string ToString() + { + return IsDirectory + ? string.Format("[DIR] {0}", Name) + : string.Format(" {0} ({1} B)", Name, Size >= 0 ? Size.ToString() : "?"); + } + } +} diff --git a/Cnc/Mazak/FtpListParser.cs b/Cnc/Mazak/FtpListParser.cs new file mode 100644 index 0000000..0e0ca76 --- /dev/null +++ b/Cnc/Mazak/FtpListParser.cs @@ -0,0 +1,91 @@ +using System; +using System.Globalization; +using System.Text.RegularExpressions; + +namespace NcProgramManager.Cnc.Mazak +{ + /// + /// Parsing puro (senza I/O) delle righe LIST/ListDirectoryDetails FTP dei + /// controlli Mazak Smooth/Matrix. Supporta formato Unix (drwx...) e DOS/IIS + /// (MM-DD-YY ... <DIR>), piu' fallback NLST (solo nome). + /// + internal static class FtpListParser + { + /// Assicura path con slash iniziale e finale per uso come directory. + public static string NormalizeDir(string path) + { + string dir = string.IsNullOrEmpty(path) ? "/" : path; + if (!dir.StartsWith("/")) dir = "/" + dir; + if (!dir.EndsWith("/")) dir += "/"; + return dir; + } + + // DOS/IIS: "07-18-26 10:00AM NAME" + // "07-18-26 10:00AM 1234 FILE.EIA" + private static readonly Regex _dosLine = new Regex( + @"^\d{2}-\d{2}-\d{2,4}\s+\d{1,2}:\d{2}\s*(AM|PM)?\s+(?|\d+)\s+(?.+)$", + RegexOptions.IgnoreCase); + + // Whitespace chars used to detect multi-token (non-NLST) lines in fallback. + private static readonly char[] _whitespace = { ' ', '\t', '\v', '\f' }; + + // Unix: "drwxr-xr-x 1 owner group 4096 Jul 18 10:00 NAME" + private static readonly Regex _unixLine = new Regex( + @"^(?[dl\-][rwxsStT\-]{9})\s+\d+\s+\S+\s+\S+\s+(?\d+)\s+\S+\s+\S+\s+\S+\s+(?.+)$"); + + /// Parsa una riga LIST. Ritorna null se non riconosciuta o '.'/'..'. + public static FtpEntry Parse(string line, string dir) + { + if (string.IsNullOrWhiteSpace(line)) return null; + + string name = null; + bool isDir = false; + long size = -1; + + var dos = _dosLine.Match(line); + if (dos.Success) + { + name = dos.Groups["name"].Value.Trim(); + if (string.Equals(dos.Groups["dir"].Value, "", StringComparison.OrdinalIgnoreCase)) + isDir = true; + else + long.TryParse(dos.Groups["dir"].Value, out size); + } + else + { + var unix = _unixLine.Match(line); + if (unix.Success) + { + name = unix.Groups["name"].Value.Trim(); + isDir = unix.Groups["perm"].Value[0] == 'd'; + if (!isDir) + long.TryParse(unix.Groups["size"].Value, NumberStyles.Integer, + CultureInfo.InvariantCulture, out size); + } + else + { + // Fallback: NLST-style (bare name only). Only a single token + // (no internal whitespace) is a real name-only listing; a line + // with internal whitespace that matched neither regex is an + // unrecognized header/garbage -> skip (avoid whole-line-as-name). + string trimmed = line.Trim(); + if (trimmed.IndexOfAny(_whitespace) >= 0) + return null; + name = trimmed; + } + } + + if (string.IsNullOrEmpty(name) || name == "." || name == "..") + return null; + + return new FtpEntry + { + Name = name, + FullPath = dir + name, + IsDirectory = isDir, + Size = size, + Raw = line + }; + } + } +} diff --git a/Cnc/Mazak/IMazakFtpClient.cs b/Cnc/Mazak/IMazakFtpClient.cs index 55e637e..92f58a6 100644 --- a/Cnc/Mazak/IMazakFtpClient.cs +++ b/Cnc/Mazak/IMazakFtpClient.cs @@ -1,8 +1,11 @@ +using System.Collections.Generic; + namespace NcProgramManager.Cnc.Mazak { internal interface IMazakFtpClient { bool Ping(); + IReadOnlyList ListDirectory(string path); string DownloadFile(string ncPath); void UploadFile(string ncPath, string content); void DeleteFile(string ncPath); diff --git a/Cnc/Mazak/MazakFtpClient.cs b/Cnc/Mazak/MazakFtpClient.cs index 5292907..0c75dba 100644 --- a/Cnc/Mazak/MazakFtpClient.cs +++ b/Cnc/Mazak/MazakFtpClient.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Net; using System.Text; @@ -29,6 +30,31 @@ namespace NcProgramManager.Cnc.Mazak catch { return false; } } + /// + /// Lista una directory FTP (LIST/ListDirectoryDetails). Ritorna file e + /// sotto-cartelle con nome, path completo, flag directory e dimensione. + /// Parsa sia il formato Unix (drwx...) sia quello DOS/IIS (MM-DD-YY ... <DIR>), + /// cioe' i due layout esposti dai controlli Mazak Smooth/Matrix. + /// + public IReadOnlyList ListDirectory(string path) + { + string dir = FtpListParser.NormalizeDir(path); + var req = CreateRequest(dir, WebRequestMethods.Ftp.ListDirectoryDetails); + var entries = new List(); + using (var resp = (FtpWebResponse)req.GetResponse()) + using (var reader = new StreamReader(resp.GetResponseStream(), Encoding.ASCII)) + { + string line; + while ((line = reader.ReadLine()) != null) + { + var entry = FtpListParser.Parse(line, dir); + if (entry != null) + entries.Add(entry); + } + } + return entries; + } + /// Scarica file dal Mazak in stringa. public string DownloadFile(string ncPath) { diff --git a/Cnc/Mazak/MazakMachine.cs b/Cnc/Mazak/MazakMachine.cs index bcdbc88..b8a3bb9 100644 --- a/Cnc/Mazak/MazakMachine.cs +++ b/Cnc/Mazak/MazakMachine.cs @@ -135,6 +135,85 @@ namespace NcProgramManager.Cnc.Mazak }, ct); } + /// + /// Lista una directory FTP del Mazak (un solo livello). Utile per sondare + /// la connessione e verificare che + /// sia vista correttamente e quali file EIA/ISO siano disponibili. + /// Path vuoto = ProgramDirectory configurata. + /// + public Task>> ListDirectoryAsync(string path = null, CancellationToken ct = default(CancellationToken)) + { + return RunGuardedAsync(() => + { + EnsureConnected(); + string dir = string.IsNullOrEmpty(path) ? _config.ProgramDirectory : path; + try { return _ftp.ListDirectory(dir); } + catch (Exception ex) + { + _errors.Add(new CncError(0, "ListDirectory", ex.Message)); + return null; + } + }, ct); + } + + /// + /// Costruisce un albero testuale (tree) della directory FTP a partire da + /// , scendendo ricorsivamente fino a + /// livelli. Diagnostica: mostra come il server + /// FTP del Mazak espone la cartella programmi e quali file contiene. + /// Errori per singola sottocartella sono annotati inline e non interrompono + /// l'intero albero. + /// + public Task> BuildTreeAsync(string path = null, int maxDepth = 2, CancellationToken ct = default(CancellationToken)) + { + return RunGuardedAsync(() => + { + EnsureConnected(); + string root = string.IsNullOrEmpty(path) ? _config.ProgramDirectory : path; + var sb = new System.Text.StringBuilder(); + sb.AppendLine(string.IsNullOrEmpty(root) ? "/" : root); + // Root-level list failure MUST propagate -> RunGuardedAsync -> Fail. + // A diagnostic probe that can't even list its root must not exit 0. + var rootEntries = _ftp.ListDirectory(root); + AppendEntries(sb, rootEntries, "", maxDepth - 1, ct); + return sb.ToString(); + }, ct); + } + + /// + /// Rende gli entry di una directory e ricorre nelle sottocartelle finche' + /// > 0. Best-effort per i figli: gli errori + /// di lista delle sottocartelle sono annotati inline e non interrompono + /// l'intero albero (la root e' gestita a monte e propaga). + /// + private void AppendEntries(System.Text.StringBuilder sb, IReadOnlyList entries, string prefix, int depthLeft, CancellationToken ct) + { + ct.ThrowIfCancellationRequested(); + + for (int i = 0; i < entries.Count; i++) + { + var e = entries[i]; + bool last = i == entries.Count - 1; + string connector = last ? "└─ " : "├─ "; // "└─ " / "├─ " + sb.AppendLine(string.Format("{0}{1}{2}{3}", + prefix, connector, e.Name, + e.IsDirectory ? "/" : (e.Size >= 0 ? string.Format(" ({0} B)", e.Size) : ""))); + + if (e.IsDirectory && depthLeft > 0) + { + string childPrefix = prefix + (last ? " " : "│ "); // "│ " + IReadOnlyList childEntries; + try { childEntries = _ftp.ListDirectory(e.FullPath); } + catch (Exception ex) + { + sb.AppendLine(string.Format("{0}!! ", childPrefix, ex.Message)); + continue; + } + AppendEntries(sb, childEntries, childPrefix, depthLeft - 1, ct); + } + } + } + public Task> ReadAsync(CancellationToken ct = default(CancellationToken)) { return RunGuardedAsync(() => diff --git a/InputArgs.cs b/InputArgs.cs index 628bd7b..58a2da7 100755 --- a/InputArgs.cs +++ b/InputArgs.cs @@ -15,6 +15,7 @@ namespace NcProgramManager public string password = ""; public string commentoProgramma = ""; public string pathLocaleProgramma = ""; + public string ftpDir = ""; // Mazak: remote FTP program directory (LISTA/probe) public bool compatibilityMode = false; public bool hasToolOffsetData = false; public bool hasWorkZeroOffsetData = false; diff --git a/NcProgramManager.Tests/NcProgramManager.Tests.csproj b/NcProgramManager.Tests/NcProgramManager.Tests.csproj index 76af27f..804f40e 100644 --- a/NcProgramManager.Tests/NcProgramManager.Tests.csproj +++ b/NcProgramManager.Tests/NcProgramManager.Tests.csproj @@ -63,6 +63,7 @@ + diff --git a/NcProgramManager.Tests/Unit/FtpListParserTests.cs b/NcProgramManager.Tests/Unit/FtpListParserTests.cs new file mode 100644 index 0000000..3164f6c --- /dev/null +++ b/NcProgramManager.Tests/Unit/FtpListParserTests.cs @@ -0,0 +1,189 @@ +using NUnit.Framework; +using NcProgramManager.Cnc.Mazak; + +namespace NcProgramManager.Tests.Unit +{ + [TestFixture] + public class FtpListParserTests + { + // NormalizeDir + + [Test] + public void NormalizeDir_Empty_ReturnsRoot() + { + Assert.That(FtpListParser.NormalizeDir(""), Is.EqualTo("/")); + } + + [Test] + public void NormalizeDir_Null_ReturnsRoot() + { + Assert.That(FtpListParser.NormalizeDir(null), Is.EqualTo("/")); + } + + [Test] + public void NormalizeDir_NoSlashes_AddsBoth() + { + Assert.That(FtpListParser.NormalizeDir("PRG"), Is.EqualTo("/PRG/")); + } + + [Test] + public void NormalizeDir_NoTrailingSlash_AddsTrailing() + { + Assert.That(FtpListParser.NormalizeDir("/a/b"), Is.EqualTo("/a/b/")); + } + + [Test] + public void NormalizeDir_AlreadyNormalized_Unchanged() + { + Assert.That(FtpListParser.NormalizeDir("/a/b/"), Is.EqualTo("/a/b/")); + } + + // Parse - DOS/IIS format + + [Test] + public void Parse_DosDir_DirectoryEntry() + { + var e = FtpListParser.Parse("07-18-26 10:00AM MYDIR", "/PRG/"); + + Assert.That(e, Is.Not.Null); + Assert.That(e.Name, Is.EqualTo("MYDIR")); + Assert.That(e.IsDirectory, Is.True); + Assert.That(e.Size, Is.EqualTo(-1)); + Assert.That(e.FullPath, Is.EqualTo("/PRG/MYDIR")); + } + + [Test] + public void Parse_DosFile_FileEntryWithSize() + { + var e = FtpListParser.Parse("07-18-26 10:00AM 1234 O1000.EIA", "/PRG/"); + + Assert.That(e, Is.Not.Null); + Assert.That(e.Name, Is.EqualTo("O1000.EIA")); + Assert.That(e.IsDirectory, Is.False); + Assert.That(e.Size, Is.EqualTo(1234)); + } + + // Parse - DOS/IIS format, T4 tolerances (24h time, 4-digit year, spaces) + + [Test] + public void Parse_Dos24hTime_DirectoryEntry() + { + // 24h time, no AM/PM: _dosLine \s*(AM|PM)? optional absorbs this. + var e = FtpListParser.Parse("07-18-26 22:00 NIGHTDIR", "/PRG/"); + + Assert.That(e, Is.Not.Null); + Assert.That(e.Name, Is.EqualTo("NIGHTDIR")); + Assert.That(e.IsDirectory, Is.True); + Assert.That(e.Size, Is.EqualTo(-1)); + } + + [Test] + public void Parse_Dos4DigitYear_FileEntryWithSize() + { + // 4-digit year: date group is \d{2}-\d{2}-\d{2,4}. + var e = FtpListParser.Parse("07-18-2026 10:00AM 2048 O4000.EIA", "/PRG/"); + + Assert.That(e, Is.Not.Null); + Assert.That(e.Name, Is.EqualTo("O4000.EIA")); + Assert.That(e.IsDirectory, Is.False); + Assert.That(e.Size, Is.EqualTo(2048)); + } + + [Test] + public void Parse_DosFileNameWithSpaces_KeepsSpaces() + { + // name group is .+ (greedy to $) -> internal spaces preserved. + var e = FtpListParser.Parse("07-18-26 10:00AM 128 MY PROG.EIA", "/PRG/"); + + Assert.That(e, Is.Not.Null); + Assert.That(e.Name, Is.EqualTo("MY PROG.EIA")); + Assert.That(e.IsDirectory, Is.False); + Assert.That(e.Size, Is.EqualTo(128)); + } + + // Parse - Unix format + + [Test] + public void Parse_UnixFileNameWithSpaces_KeepsSpaces() + { + // Unix name group is .+ -> internal spaces preserved. + var e = FtpListParser.Parse("-rw-r--r-- 1 own grp 64 Jul 18 10:00 MY PROG", "/PRG/"); + + Assert.That(e, Is.Not.Null); + Assert.That(e.Name, Is.EqualTo("MY PROG")); + Assert.That(e.IsDirectory, Is.False); + Assert.That(e.Size, Is.EqualTo(64)); + } + + [Test] + public void Parse_UnixDir_DirectoryEntry() + { + var e = FtpListParser.Parse("drwxr-xr-x 1 own grp 4096 Jul 18 10:00 sub", "/PRG/"); + + Assert.That(e, Is.Not.Null); + Assert.That(e.IsDirectory, Is.True); + Assert.That(e.Name, Is.EqualTo("sub")); + } + + [Test] + public void Parse_UnixFile_FileEntryWithSize() + { + var e = FtpListParser.Parse("-rw-r--r-- 1 own grp 512 Jul 18 10:00 O2000", "/PRG/"); + + Assert.That(e, Is.Not.Null); + Assert.That(e.IsDirectory, Is.False); + Assert.That(e.Size, Is.EqualTo(512)); + Assert.That(e.Name, Is.EqualTo("O2000")); + } + + // Parse - skip '.' and '..' + + [Test] + public void Parse_DotDir_ReturnsNull() + { + var e = FtpListParser.Parse("07-18-26 10:00AM .", "/PRG/"); + + Assert.That(e, Is.Null); + } + + [Test] + public void Parse_DotDotDir_ReturnsNull() + { + var e = FtpListParser.Parse("07-18-26 10:00AM ..", "/PRG/"); + + Assert.That(e, Is.Null); + } + + // Parse - fallback (NLST bare name) + + [Test] + public void Parse_BareName_FallbackFileEntry() + { + var e = FtpListParser.Parse("O3000", "/PRG/"); + + Assert.That(e, Is.Not.Null); + Assert.That(e.Name, Is.EqualTo("O3000")); + Assert.That(e.IsDirectory, Is.False); + Assert.That(e.Size, Is.EqualTo(-1)); + } + + // Parse - garbage/header multi-token line matches neither regex -> skip + + [Test] + public void Parse_GarbageMultiToken_ReturnsNull() + { + // "total 8": DOS regex needs leading MM-DD-YY date (no); Unix perm group is + // [dl-][rwxsStT-]{9} and 't' is not in [dl-] (no). Trimmed line has internal + // whitespace -> T4 fallback returns null (not treated as bare NLST name). + Assert.That(FtpListParser.Parse("total 8", "/PRG/"), Is.Null); + } + + // Parse - whitespace/empty + + [Test] + public void Parse_Whitespace_ReturnsNull() + { + Assert.That(FtpListParser.Parse(" ", "/PRG/"), Is.Null); + } + } +} diff --git a/NcProgramManager.Tests/Unit/MazakMachineTests.cs b/NcProgramManager.Tests/Unit/MazakMachineTests.cs index a6c8a51..bc2bc20 100644 --- a/NcProgramManager.Tests/Unit/MazakMachineTests.cs +++ b/NcProgramManager.Tests/Unit/MazakMachineTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using NUnit.Framework; using Moq; using NcProgramManager.Cnc; @@ -213,5 +214,166 @@ namespace NcProgramManager.Tests.Unit Assert.That(result.Success, Is.False); } + + // ListDirectoryAsync + + private void Connect() + { + _ftpMock.Setup(f => f.Ping()).Returns(true); + _machine.ConnectAsync().GetAwaiter().GetResult(); + } + + [Test] + public void ListDirectoryAsync_NullPath_UsesProgramDirectory() + { + Connect(); + var entries = new List + { + new FtpEntry { Name = "O1000", FullPath = "/PRG/O1000", IsDirectory = false, Size = 10 }, + new FtpEntry { Name = "O2000", FullPath = "/PRG/O2000", IsDirectory = false, Size = 20 } + }; + _ftpMock.Setup(f => f.ListDirectory("/PRG/")).Returns(entries); + + var result = _machine.ListDirectoryAsync(null).GetAwaiter().GetResult(); + + Assert.That(result.Success, Is.True); + Assert.That(result.Value.Count, Is.EqualTo(2)); + Assert.That(result.Value[0].Name, Is.EqualTo("O1000")); + _ftpMock.Verify(f => f.ListDirectory("/PRG/"), Times.Once); + } + + [Test] + public void ListDirectoryAsync_ExplicitPath_PassesThrough() + { + Connect(); + var entries = new List + { + new FtpEntry { Name = "O3000", FullPath = "/OTHER/O3000", IsDirectory = false, Size = 30 } + }; + _ftpMock.Setup(f => f.ListDirectory("/OTHER/")).Returns(entries); + + var result = _machine.ListDirectoryAsync("/OTHER/").GetAwaiter().GetResult(); + + Assert.That(result.Success, Is.True); + Assert.That(result.Value.Count, Is.EqualTo(1)); + _ftpMock.Verify(f => f.ListDirectory("/OTHER/"), Times.Once); + } + + [Test] + public void ListDirectoryAsync_FtpThrows_ResultFail() + { + Connect(); + _ftpMock.Setup(f => f.ListDirectory("/PRG/")).Throws(new Exception("boom")); + + var result = _machine.ListDirectoryAsync(null).GetAwaiter().GetResult(); + + Assert.That(result.Success, Is.False); + } + + [Test] + public void ListDirectoryAsync_NotConnected_Fail() + { + // No ConnectAsync -> EnsureConnected throws, wrapped as Fail. + var result = _machine.ListDirectoryAsync(null).GetAwaiter().GetResult(); + + Assert.That(result.Success, Is.False); + } + + // BuildTreeAsync + + [Test] + public void BuildTreeAsync_FlatDir_RendersEntries() + { + Connect(); + var entries = new List + { + new FtpEntry { Name = "O1000", FullPath = "/PRG/O1000", IsDirectory = false, Size = 10 }, + new FtpEntry { Name = "O2000", FullPath = "/PRG/O2000", IsDirectory = false, Size = 20 } + }; + _ftpMock.Setup(f => f.ListDirectory("/PRG/")).Returns(entries); + + var result = _machine.BuildTreeAsync().GetAwaiter().GetResult(); + + Assert.That(result.Success, Is.True); + Assert.That(result.Value, Does.Contain("O1000")); + Assert.That(result.Value, Does.Contain("O2000")); + } + + [Test] + public void BuildTreeAsync_NestedDir_RecursesWithinDepth() + { + Connect(); + var top = new List + { + new FtpEntry { Name = "O1", FullPath = "/PRG/O1", IsDirectory = false, Size = 10 }, + // FullPath has NO trailing slash: real FtpListParser.Parse produces + // "/PRG/SUB" (dir + name), and recursion keys ListDirectory on it verbatim. + new FtpEntry { Name = "SUB", FullPath = "/PRG/SUB", IsDirectory = true, Size = -1 } + }; + var sub = new List + { + new FtpEntry { Name = "O2", FullPath = "/PRG/SUB/O2", IsDirectory = false, Size = 20 } + }; + _ftpMock.Setup(f => f.ListDirectory("/PRG/")).Returns(top); + _ftpMock.Setup(f => f.ListDirectory("/PRG/SUB")).Returns(sub); + + var result = _machine.BuildTreeAsync(null, 2).GetAwaiter().GetResult(); + + Assert.That(result.Success, Is.True); + Assert.That(result.Value, Does.Contain("O1")); + Assert.That(result.Value, Does.Contain("SUB")); + Assert.That(result.Value, Does.Contain("O2")); + } + + [Test] + public void BuildTreeAsync_MaxDepthCutoff() + { + Connect(); + var top = new List + { + new FtpEntry { Name = "SUB", FullPath = "/PRG/SUB", IsDirectory = true, Size = -1 } + }; + // maxDepth 1 -> only top level; deeper ListDirectory NOT set up. + // Strict mock: if "/PRG/SUB" were listed the test would fail (proves no descent). + _ftpMock.Setup(f => f.ListDirectory("/PRG/")).Returns(top); + + var result = _machine.BuildTreeAsync(null, 1).GetAwaiter().GetResult(); + + Assert.That(result.Success, Is.True); + Assert.That(result.Value, Does.Contain("SUB")); + Assert.That(result.Value, Does.Not.Contain("O2")); + } + + [Test] + public void BuildTreeAsync_SubDirListThrows_InlineErrorNotFatal() + { + Connect(); + var top = new List + { + new FtpEntry { Name = "SUB", FullPath = "/PRG/SUB", IsDirectory = true, Size = -1 } + }; + _ftpMock.Setup(f => f.ListDirectory("/PRG/")).Returns(top); + _ftpMock.Setup(f => f.ListDirectory("/PRG/SUB")).Throws(new Exception("denied")); + + var result = _machine.BuildTreeAsync(null, 2).GetAwaiter().GetResult(); + + // Child (depth>0) list error is swallowed inline -> Success stays true. + Assert.That(result.Success, Is.True); + Assert.That(result.Value, Does.Contain("errore lista")); + } + + [Test] + public void BuildTreeAsync_RootListThrows_ResultFail() + { + // Root-level list failure now fetched DIRECTLY and MUST propagate -> + // RunGuardedAsync catch -> Fail. Contrast with SubDirListThrows (child, + // swallowed inline, Success stays true). + Connect(); + _ftpMock.Setup(f => f.ListDirectory("/PRG/")).Throws(new Exception("530")); + + var result = _machine.BuildTreeAsync(null).GetAwaiter().GetResult(); + + Assert.That(result.Success, Is.False); + } } } diff --git a/NcProgramManager.csproj b/NcProgramManager.csproj index 5d88432..55a8ca8 100755 --- a/NcProgramManager.csproj +++ b/NcProgramManager.csproj @@ -123,6 +123,8 @@ + + diff --git a/Program.cs b/Program.cs index 774e1a3..d9c358b 100755 --- a/Program.cs +++ b/Program.cs @@ -51,7 +51,9 @@ namespace NcProgramManager #endif bool needsIp = inputArgs.manufacturer != CncManufacturer.Fanuc || inputArgs.nodoHssb == -1; - if (inputArgs.azione == "" || (needsIp && string.IsNullOrEmpty(inputArgs.ip)) || inputArgs.pathLocaleProgramma == "") + // LISTA (probe FTP) non richiede un programma locale. + bool needsLocalProgram = !string.Equals(inputArgs.azione, "LISTA", StringComparison.OrdinalIgnoreCase); + if (inputArgs.azione == "" || (needsIp && string.IsNullOrEmpty(inputArgs.ip)) || (needsLocalProgram && inputArgs.pathLocaleProgramma == "")) { VediErrore(-102, inputArgs.debugMode); return -102; @@ -74,6 +76,7 @@ namespace NcProgramManager { case "SCARICA": return Scarica(machine, inputArgs); case "INVIA": return Invia(machine, inputArgs); + case "LISTA": return Lista(machine, inputArgs); default: VediErrore(-103, inputArgs.debugMode); return -103; @@ -270,6 +273,59 @@ namespace NcProgramManager } } + static int Lista(ICncMachine macchina, InputArgs inputArgs) + { + _log.Info("LISTA FTP (probe)-->"); + var mazak = macchina as NcProgramManager.Cnc.Mazak.MazakMachine; + if (mazak == null) + { + _log.Info("Azione LISTA supportata solo per manufacturer=mazak"); + VediErrore(-103, inputArgs.debugMode); + return -103; + } + + bool connected = false; + try + { + var connResult = macchina.ConnectAsync().GetAwaiter().GetResult(); + if (!connResult.Success) + { + _log.Info("Macchina non connessa"); + PrintErrors(connResult.Errors); + VediErrore(-201, inputArgs.debugMode); + return -201; + } + connected = true; + + _log.Info("Directory FTP: {0}", string.IsNullOrEmpty(inputArgs.ftpDir) ? "/" : inputArgs.ftpDir); + var treeResult = mazak.BuildTreeAsync(inputArgs.ftpDir, 2).GetAwaiter().GetResult(); + if (!treeResult.Success) + { + PrintErrors(treeResult.Errors); + VediErrore(-204, inputArgs.debugMode); + return -204; + } + + foreach (var riga in treeResult.Value.Split('\n')) + _log.Info("{0}", riga.TrimEnd('\r')); + + VediErrore(0, inputArgs.debugMode); + return 0; + } + catch (Exception ex) + { + _log.Error(ex, "Lista failed"); + _log.Info("{0}", ex.Message); + VediErrore(-202, inputArgs.debugMode); + return -202; + } + finally + { + if (connected) + macchina.DisconnectAsync().GetAwaiter().GetResult(); + } + } + static void PrintErrors(IReadOnlyList errors) { if (errors == null) return;