Files
AiQ_GUI/Camera/SSH.cs

402 lines
15 KiB
C#
Raw Normal View History

2025-09-02 15:32:24 +01:00
using Renci.SshNet;
2025-10-21 14:01:27 +01:00
using Renci.SshNet.Common;
2025-09-02 15:32:24 +01:00
namespace AiQ_GUI
{
internal class SSH
{
public const string SSHUsername = "mav";
public const string SSHPassword = "mavPA$$";
2025-10-21 14:01:27 +01:00
public const string SSHPasswordNEW = "#!mavsoftMESA19"; // New password for SSH after last one got leaked
private 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.");
}
return null;
}
2025-09-02 15:32:24 +01:00
// 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
{
2025-10-21 14:01:27 +01:00
SshClient client = SshConnect(IPAddress);
2025-09-02 15:32:24 +01:00
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}");
2025-10-21 14:01:27 +01:00
Data.FilesystemName = Data.FilesystemSize = "Unknown";
2025-09-02 15:32:24 +01:00
}
try
{
Data.tailscale = IsTailscaleInstalled(client);
}
catch (Exception ex)
{
MainForm.Instance.AddToActionsList($"Failed to check Tailscale: {ex.Message}");
Data.tailscale = false;
}
client.Disconnect();
2025-10-21 14:01:27 +01:00
client.Dispose();
2025-09-02 15:32:24 +01:00
}
catch (Exception ex)
{
MainForm.Instance.AddToActionsList($"SSH connection failed: {ex.Message}. Check password or network.");
}
string LogMssg = string.Join(" | ", typeof(SSHData).GetProperties().Select(p => $"{p.Name}: {p.GetValue(Data)}"));
Logging.LogMessage(LogMssg); // Log all of Data
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
{
2025-10-21 14:01:27 +01:00
SshClient client = SshConnect(IPAddress);
2025-09-02 15:32:24 +01:00
try
{
return GetRootFilesystemInfo(client);
}
catch (Exception ex)
{
MainForm.Instance.AddToActionsList($"Failed to get filesystem info: {ex.Message}");
}
client.Disconnect();
2025-10-21 14:01:27 +01:00
client.Dispose();
2025-09-02 15:32:24 +01:00
}
catch (Exception ex)
{
MainForm.Instance.AddToActionsList($"SSH connection failed: {ex.Message}. Check password or network.");
}
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
{
2025-10-21 14:01:27 +01:00
SshClient client = SshConnect(IPAddress);
2025-09-02 15:32:24 +01:00
(string FilesystemName, string FilesystemSize) = GetRootFilesystemInfo(client);
client.Disconnect();
2025-10-21 14:01:27 +01:00
client.Dispose();
2025-09-02 15:32:24 +01:00
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)
{
2025-10-21 14:01:27 +01:00
const double GoodSize = 128.0; // 128GB
const double Deviation = 20.0; // ±20GB
2025-09-02 15:32:24 +01:00
double currentSize = NormaliseFSSize(sshData.FilesystemSize);
LblFSSize.Text = $"Filesystem Size = {currentSize}GB";
2025-10-21 14:01:27 +01:00
if (Math.Abs(GoodSize - currentSize) < Deviation)
2025-09-02 15:32:24 +01:00
{
LblFSSize.ForeColor = Color.LightGreen;
return sshData;
}
2025-10-21 14:01:27 +01:00
if (currentSize < GoodSize - Deviation)
2025-09-02 15:32:24 +01:00
{
try
{
if (await ExpandFS(sshData.FilesystemName, IPAddress))
{
(sshData.FilesystemName, sshData.FilesystemSize) = GetRootFilesystemInfo(IPAddress);
double newSize = NormaliseFSSize(sshData.FilesystemSize);
LblFSSize.Text = $"Filesystem Size = {newSize}GB";
2025-10-21 14:01:27 +01:00
if (Math.Abs(GoodSize - newSize) < Deviation)
2025-09-02 15:32:24 +01:00
{
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());
2025-10-21 14:01:27 +01:00
// Return 0 if no match or invalid number
if (!match.Success || !double.TryParse(match.Groups["value"].Value, out double value))
2025-09-02 15:32:24 +01:00
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;
}
2025-10-21 14:01:27 +01:00
// Expands the filesystem to max
2025-09-02 15:32:24 +01:00
public async static Task<bool> ExpandFS(string device, string IPAddress)
{
try
{
2025-10-21 14:01:27 +01:00
SshClient client = SshConnect(IPAddress);
2025-09-02 15:32:24 +01:00
2025-10-21 14:01:27 +01:00
SshCommand checkDevice = client.RunCommand($"[ -b {device} ] && echo OK || echo NOT_FOUND");
2025-09-02 15:32:24 +01:00
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.");
return false;
}
2025-10-21 14:01:27 +01:00
SshCommand umountCmd = client.RunCommand($"sudo umount {device}");
2025-09-02 15:32:24 +01:00
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
2025-10-21 14:01:27 +01:00
SshCommand fsckCmd = client.RunCommand($"sudo e2fsck -f -y -v -C 0 {device}");
2025-09-02 15:32:24 +01:00
if (!string.IsNullOrWhiteSpace(fsckCmd.Error))
{
MainForm.Instance.AddToActionsList($"e2fsck error: {fsckCmd.Error}");
return false;
}
2025-10-21 14:01:27 +01:00
SshCommand resizeFs = client.RunCommand($"sudo resize2fs {device}");
client.Disconnect();
client.Dispose();
2025-09-02 15:32:24 +01:00
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 void Sync(string IPAddress)
{
try
{
2025-10-21 14:01:27 +01:00
SshClient client = SshConnect(IPAddress);
2025-09-02 15:32:24 +01:00
2025-10-21 14:01:27 +01:00
SshCommand checkDevice = client.RunCommand("sync");
2025-09-02 15:32:24 +01:00
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");
return;
}
2025-10-21 14:01:27 +01:00
client.Disconnect();
client.Dispose();
2025-09-02 15:32:24 +01:00
}
catch (Exception ex)
{
MainForm.Instance.AddToActionsList($"Cannot sync becuase: {ex.Message}. DO NOT TURN OFF, GET SUPERVISOR");
}
}
}
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; }
}
}