Files
AiQ_GUI/Camera/SSH.cs
Bradley Born 4c74e237c2 • Class rename: MobilePreTest → MobileTests
• New method added: RunFinalTestAsync() - Performs final mobile tests including SSH verification of setup files
• Calls SSH.MobiletxtCheck() to verify /home/mav/Mobile-Setup-configuration-marker.txt exists on device
• Updates UI with "MobileSetup.sh" pass/fail status
• Mobile Tests/Mobile API.cs (New File Created)
New Method Added: MobiletxtCheck(string IPAddress) - Boolean method to verify setup marker file
• Checks if /home/mav/Mobile-Setup-configuration-marker.txt exists on device via SSH
• Returns exit status 0 (true) if file exists, false otherwise
• Used in RunFinalTestAsync() for setup verification
2026-01-14 11:49:32 +00:00

433 lines
16 KiB
C#

using Renci.SshNet;
using Renci.SshNet.Common;
namespace AiQ_GUI
{
internal class SSH
{
public const string SSHUsername = "mav";
public const string SSHPassword = "mavPA$$";
public const string SSHPasswordNEW = "#!mavsoftMESA19"; // New password for SSH after last one got leaked
public static SshClient SshConnect(string IPAddress)
{
SshClient client = new(IPAddress, SSHUsername, SSHPasswordNEW);
try
{
client.Connect();
return client;
}
catch (SshAuthenticationException)
{
client = new(IPAddress, SSHUsername, SSHPassword);
client.Connect();
return client;
}
catch (Exception Ex)
{
MainForm.Instance.AddToActionsList($"SSH connection failed: {Ex.Message}. Check password or network. ", Level.WARNING);
}
return null;
}
// Connects to camera over SSH and collects the Vaxtor packages, filesystem name, filesystem size, and tailscale status.
public static SSHData CollectSSHData(string IPAddress)
{
SSHData Data = new();
try
{
SshClient client = SshConnect(IPAddress);
// CRITICAL: Check if connection actually succeeded
if (client == null)
{
MainForm.Instance.AddToActionsList($"SSH connection failed for {IPAddress}", Level.ERROR);
return Data; // Return empty data
}
try
{
Data.packages = GetVaxtorPackages(client);
}
catch (Exception ex)
{
MainForm.Instance.AddToActionsList($"Failed to get Vaxtor packages: {ex.Message}");
Data.packages = "Error";
}
try
{
(Data.FilesystemName, Data.FilesystemSize) = GetRootFilesystemInfo(client);
}
catch (Exception ex)
{
MainForm.Instance.AddToActionsList($"Failed to get filesystem info: {ex.Message}");
Data.FilesystemName = Data.FilesystemSize = "Unknown";
}
try
{
Data.tailscale = IsTailscaleInstalled(client);
}
catch (Exception ex)
{
MainForm.Instance.AddToActionsList($"Failed to check Tailscale: {ex.Message}");
Data.tailscale = false;
}
client.Disconnect();
client.Dispose();
}
catch (Exception ex)
{
MainForm.Instance.AddToActionsList($"SSH connection failed: {ex.Message}. Check password or network. ", Level.WARNING);
}
return Data;
}
// Gets a list of packages with Vaxtor in the name
public static string GetVaxtorPackages(SshClient client)
{
try
{
SshCommand cmd = client.RunCommand("dpkg -l | grep vaxtor");
if (!string.IsNullOrWhiteSpace(cmd.Error))
throw new Exception(cmd.Error);
string result = cmd.Result;
if (string.IsNullOrWhiteSpace(result))
return "No Vaxtor packages found.";
string[] lines = result.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
List<string> packages = [];
foreach (string line in lines)
{
string[] parts = line.Split([' '], StringSplitOptions.RemoveEmptyEntries);
if (parts.Length >= 3)
packages.Add($"{parts[1]} - {parts[2]}"); // Package name - Version
}
return packages.Count > 0 ? string.Join("\n", packages) : "No Vaxtor packages found.";
}
catch (Exception ex)
{
MainForm.Instance.AddToActionsList($"Error getting Vaxtor packages: {ex.Message}");
return "Error";
}
}
// Returns true if Tailscale is installed on the camera
public static bool IsTailscaleInstalled(SshClient client)
{
try
{
SshCommand cmd = client.RunCommand("dpkg -l | grep '^ii' | grep tailscale");
if (!string.IsNullOrWhiteSpace(cmd.Error))
throw new Exception(cmd.Error);
string command = cmd.Result;
if (string.IsNullOrWhiteSpace(command))
return false;
string[] lines = command.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (string line in lines)
{
string[] parts = line.Split([' '], StringSplitOptions.RemoveEmptyEntries);
if (parts.Length >= 3 && parts[1].Equals("tailscale", StringComparison.OrdinalIgnoreCase))
return true; // Return true if any line contains "tailscale" as the package name
}
return false;
}
catch (Exception ex)
{
MainForm.Instance.AddToActionsList($"Error checking Tailscale: {ex.Message}");
return false;
}
}
// Connects to camera over SSH and collects the Vaxtor packages, filesystem name, filesystem size, and tailscale status.
public static (string filesystem, string size) CollectFSData(string IPAddress)
{
try
{
SshClient client = SshConnect(IPAddress);
try
{
return GetRootFilesystemInfo(client);
}
catch (Exception ex)
{
MainForm.Instance.AddToActionsList($"Failed to get filesystem info: {ex.Message}");
}
client.Disconnect();
client.Dispose();
}
catch (Exception ex)
{
MainForm.Instance.AddToActionsList($"SSH connection failed: {ex.Message}. Check password or network. ", Level.WARNING);
}
return (string.Empty, string.Empty);
}
// Gets the filesystem size and partition name
public static (string filesystem, string size) GetRootFilesystemInfo(SshClient client)
{
try
{
SshCommand cmd = client.RunCommand("df -h");
if (!string.IsNullOrWhiteSpace(cmd.Error))
throw new Exception(cmd.Error);
string result = cmd.Result;
if (string.IsNullOrWhiteSpace(result))
return ("Unknown", "Unknown");
IEnumerable<string> lines = result.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries).Skip(1);
foreach (string line in lines)
{
string[] parts = line.Split([' '], StringSplitOptions.RemoveEmptyEntries);
// Long enough & Filesystem name isn't tmpfs varient or none & Mountpoint is root
if (parts.Length >= 6 && !parts[0].Contains("tmpfs") && parts[0] != "none" && parts[5] == "/")
return (parts[0], parts[1]); // Return name & size of partition
}
return ("Unknown", "Unknown");
}
catch (Exception ex)
{
MainForm.Instance.AddToActionsList($"Error getting filesystem info: {ex.Message}");
return ("Unknown", "Unknown");
}
}
// Sorts the SSH client for the filesystem call
public static (string filesystem, string size) GetRootFilesystemInfo(string IPAddress)
{
try
{
SshClient client = SshConnect(IPAddress);
(string FilesystemName, string FilesystemSize) = GetRootFilesystemInfo(client);
client.Disconnect();
client.Dispose();
return (FilesystemName, FilesystemSize);
}
catch (Exception ex)
{
MainForm.Instance.AddToActionsList($"Error getting root filesystem info: {ex.Message}");
return ("Unknown", "Unknown");
}
}
// Checks the filesystem size and expands it if necessary, displays on the label how big the SD card is.
public static async Task<SSHData> CheckFSSize(string IPAddress, Label LblFSSize, SSHData sshData)
{
const double GoodSize = 128.0; // 128GB
const double Deviation = 20.0; // ±20GB
double currentSize = NormaliseFSSize(sshData.FilesystemSize);
// Create label dynamically if not provided
if (LblFSSize == null)
{
MainForm.Instance.AddLabelToPanel($"Filesystem Size = {currentSize}GB", currentSize < (GoodSize - Deviation) || currentSize > (GoodSize + Deviation));
return sshData;
}
LblFSSize.Text = $"Filesystem Size = {currentSize}GB";
if (Math.Abs(GoodSize - currentSize) < Deviation)
{
LblFSSize.ForeColor = Color.LightGreen;
return sshData;
}
if (currentSize < GoodSize - Deviation)
{
try
{
if (await ExpandFS(sshData.FilesystemName, IPAddress))
{
(sshData.FilesystemName, sshData.FilesystemSize) = GetRootFilesystemInfo(IPAddress);
double newSize = NormaliseFSSize(sshData.FilesystemSize);
LblFSSize.Text = $"Filesystem Size = {newSize}GB";
if (Math.Abs(GoodSize - newSize) < Deviation)
{
LblFSSize.ForeColor = Color.LightGreen;
return sshData;
}
}
}
catch (Exception ex)
{
MainForm.Instance.AddToActionsList($"Error expanding filesystem: {ex.Message}");
}
LblFSSize.ForeColor = Color.Red;
MainForm.Instance.AddToActionsList("Size is too small, failed to expand FS, please try manually.");
return sshData;
}
LblFSSize.ForeColor = Color.Red;
LblFSSize.Text += " Size is too big.";
return sshData;
}
// Makes sure the units given are accounted for when calcualting the size of the SD card.
public static double NormaliseFSSize(string rootSize)
{
try
{
if (string.IsNullOrWhiteSpace(rootSize))
return 0;
// Extract value & unit
System.Text.RegularExpressions.Match match = RegexCache.FileSizeRegex().Match(rootSize.Trim());
// Return 0 if no match or invalid number
if (!match.Success || !double.TryParse(match.Groups["value"].Value, out double value))
return 0;
string unit = match.Groups["unit"].Value.ToUpperInvariant();
switch (unit) // Normalize to gigabytes
{
case "T": value *= 1024; break;
case "G": break;
case "M": value /= 1024; break;
case "K": value /= 1024 * 1024; break;
case "": value /= 1024 * 1024 * 1024; break; // assume bytes
default: value = 0; break;
}
return value;
}
catch (Exception ex)
{
MainForm.Instance.AddToActionsList($"Error normalizing FS size: {ex.Message}");
}
return 0;
}
// Expands the filesystem to max
public async static Task<bool> ExpandFS(string device, string IPAddress)
{
try
{
SshClient client = SshConnect(IPAddress);
SshCommand checkDevice = client.RunCommand($"[ -b {device} ] && echo OK || echo NOT_FOUND");
if (!string.IsNullOrWhiteSpace(checkDevice.Error))
throw new Exception(checkDevice.Error);
if (checkDevice.Result.Trim() != "OK") // Device not found
{
MainForm.Instance.AddToActionsList($"Block device {device} not found. ", Level.WARNING);
return false;
}
SshCommand umountCmd = client.RunCommand($"sudo umount {device}");
if (!string.IsNullOrWhiteSpace(umountCmd.Error) && !umountCmd.Error.Contains("not mounted"))
{
MainForm.Instance.AddToActionsList($"Unmount error: {umountCmd.Error}");
return false;
}
await Task.Delay(1000); // Wait for mount to settle
SshCommand fsckCmd = client.RunCommand($"sudo e2fsck -f -y -v -C 0 {device}");
if (!string.IsNullOrWhiteSpace(fsckCmd.Error))
{
MainForm.Instance.AddToActionsList($"e2fsck error: {fsckCmd.Error}");
return false;
}
SshCommand resizeFs = client.RunCommand($"sudo resize2fs {device}");
client.Disconnect();
client.Dispose();
if (!string.IsNullOrWhiteSpace(resizeFs.Error))
{
MainForm.Instance.AddToActionsList($"resize2fs error: {resizeFs.Error}");
return false;
}
if (resizeFs.ExitStatus == 0)
return true;
MainForm.Instance.AddToActionsList($"resize2fs failed with exit code {resizeFs.ExitStatus}");
return false;
}
catch (Exception ex)
{
MainForm.Instance.AddToActionsList($"Error expanding filesystem: {ex.Message}");
return false;
}
}
public static bool MobiletxtCheck(string IPAddress)
{
try
{
using (SshClient client = SshConnect(IPAddress))
{
// test -f returns exit status 0 if file exists
SshCommand checkDevice = client.RunCommand("test -f /home/mav/Mobile-Setup-configuration-marker.txt"
);
return checkDevice.ExitStatus == 0;
}
}
catch (Exception ex)
{
MainForm.Instance.AddToActionsList($"Can't check txt file: Error {ex}", Level.ERROR);
return false;
}
}
public static void Sync(string IPAddress)
{
try
{
SshClient client = SshConnect(IPAddress);
SshCommand checkDevice = client.RunCommand("sync");
if (!string.IsNullOrWhiteSpace(checkDevice.Error))
throw new Exception(checkDevice.Error);
if (checkDevice.Result.Trim().Length > 1) // Device not found
{
MainForm.Instance.AddToActionsList($"Cannot sync files to disk. Replied: {checkDevice.Result}. DO NOT TURN OFF, GET SUPERVISOR", Level.ERROR);
return;
}
client.Disconnect();
client.Dispose();
}
catch (Exception ex)
{
MainForm.Instance.AddToActionsList($"Cannot sync becuase: {ex.Message}. DO NOT TURN OFF, GET SUPERVISOR", Level.ERROR);
}
}
}
public class SSHData
{
public string packages { get; set; } = string.Empty;
public string FilesystemName { get; set; } = string.Empty;
public string FilesystemSize { get; set; } = string.Empty;
public bool tailscale { get; set; }
}
}