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;