Add project files.

This commit is contained in:
2025-09-02 15:32:24 +01:00
parent 6a633eed7a
commit 50bb9c9781
53 changed files with 9925 additions and 0 deletions

378
Camera/SSH.cs Normal file
View File

@@ -0,0 +1,378 @@
using Renci.SshNet;
namespace AiQ_GUI
{
internal class SSH
{
public const string SSHUsername = "mav";
public const string SSHPassword = "mavPA$$";
// 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
{
using SshClient client = new SshClient(IPAddress, SSHUsername, SSHPassword);
client.Connect();
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 = "Unknown";
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();
}
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
{
using SshClient client = new SshClient(IPAddress, SSHUsername, SSHPassword);
client.Connect();
try
{
return GetRootFilesystemInfo(client);
}
catch (Exception ex)
{
MainForm.Instance.AddToActionsList($"Failed to get filesystem info: {ex.Message}");
}
client.Disconnect();
}
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
{
using SshClient client = new(IPAddress, SSHUsername, SSHPassword);
client.Connect();
(string FilesystemName, string FilesystemSize) = GetRootFilesystemInfo(client);
client.Disconnect();
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 MinGoodSize = 100.0; // 100GB
const double MaxGoodSize = 150.0; // 150GB
double currentSize = NormaliseFSSize(sshData.FilesystemSize);
LblFSSize.Text = $"Filesystem Size = {currentSize}GB";
if (currentSize >= MinGoodSize && currentSize <= MaxGoodSize)
{
LblFSSize.ForeColor = Color.LightGreen;
return sshData;
}
if (currentSize < MinGoodSize)
{
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 (newSize >= MinGoodSize && newSize <= MaxGoodSize)
{
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());
if (!match.Success)
return 0;
if (!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
{
using SshClient ssh = new SshClient(IPAddress, SSHUsername, SSHPassword);
ssh.Connect();
SshCommand checkDevice = ssh.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.");
return false;
}
SshCommand umountCmd = ssh.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 = ssh.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 = ssh.RunCommand($"sudo resize2fs {device}");
ssh.Disconnect();
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
{
using SshClient ssh = new SshClient(IPAddress, SSHUsername, SSHPassword);
ssh.Connect();
SshCommand checkDevice = ssh.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");
return;
}
}
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; }
}
}