Upload SshBridge.cs with huggingface_hub
Browse files- SshBridge.cs +856 -24
SshBridge.cs
CHANGED
|
@@ -1,9 +1,12 @@
|
|
| 1 |
using System;
|
|
|
|
| 2 |
using System.Drawing;
|
| 3 |
using System.IO;
|
|
|
|
| 4 |
using System.Net;
|
| 5 |
using System.Net.Sockets;
|
| 6 |
using System.Text;
|
|
|
|
| 7 |
using System.Threading;
|
| 8 |
using System.Threading.Tasks;
|
| 9 |
using System.Windows.Forms;
|
|
@@ -38,12 +41,33 @@ namespace SshBridge
|
|
| 38 |
private Thread? _serverThread;
|
| 39 |
private CancellationTokenSource? _cts;
|
| 40 |
private SshClient? _sshClient;
|
|
|
|
| 41 |
private bool _isConnected;
|
| 42 |
private int _commandCount;
|
| 43 |
private string _currentHost = "";
|
| 44 |
private string _currentUser = "";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
private const int PORT = 52718;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
public SshBridgeForm()
|
| 49 |
{
|
|
@@ -142,10 +166,48 @@ namespace SshBridge
|
|
| 142 |
Height = 28,
|
| 143 |
Anchor = AnchorStyles.Right,
|
| 144 |
};
|
| 145 |
-
_disconnectButton.Location = new Point(topBar.Width - _disconnectButton.Width - 10, 6);
|
| 146 |
_disconnectButton.Click += (s, e) => Disconnect();
|
| 147 |
topBar.Controls.Add(_disconnectButton);
|
| 148 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
|
| 150 |
_outputBox = new RichTextBox
|
| 151 |
{
|
|
@@ -157,11 +219,45 @@ namespace SshBridge
|
|
| 157 |
WordWrap = false,
|
| 158 |
};
|
| 159 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
_sessionPanel.Controls.Add(_outputBox);
|
| 161 |
_sessionPanel.Controls.Add(topBar);
|
| 162 |
|
| 163 |
this.Controls.Add(_loginPanel);
|
| 164 |
this.Controls.Add(_sessionPanel);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
}
|
| 166 |
|
| 167 |
private void Connect()
|
|
@@ -196,10 +292,27 @@ namespace SshBridge
|
|
| 196 |
_currentUser = user;
|
| 197 |
_commandCount = 0;
|
| 198 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
this.Invoke(() =>
|
| 200 |
{
|
|
|
|
| 201 |
_passwordBox.Clear();
|
| 202 |
OnConnected();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
});
|
| 204 |
}
|
| 205 |
else
|
|
@@ -232,23 +345,102 @@ namespace SshBridge
|
|
| 232 |
private void Disconnect()
|
| 233 |
{
|
| 234 |
_isConnected = false;
|
|
|
|
|
|
|
|
|
|
| 235 |
|
| 236 |
try
|
| 237 |
{
|
|
|
|
| 238 |
_sshClient?.Disconnect();
|
| 239 |
_sshClient?.Dispose();
|
| 240 |
}
|
| 241 |
catch { }
|
| 242 |
|
|
|
|
| 243 |
_sshClient = null;
|
| 244 |
_currentHost = "";
|
| 245 |
_currentUser = "";
|
|
|
|
| 246 |
|
| 247 |
_loginPanel.Visible = true;
|
| 248 |
_sessionPanel.Visible = false;
|
| 249 |
_connectButton.Enabled = true;
|
| 250 |
_connectButton.Text = "Connect";
|
| 251 |
_statusLabel.Text = "Disconnected";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
}
|
| 253 |
|
| 254 |
private void AppendOutput(string? text, Color? color = null)
|
|
@@ -274,44 +466,490 @@ namespace SshBridge
|
|
| 274 |
|
| 275 |
public string ExecuteCommand(string command)
|
| 276 |
{
|
| 277 |
-
if (!_isConnected || _sshClient == null || !_sshClient.IsConnected)
|
| 278 |
{
|
| 279 |
return "ERROR: Not connected. Open SSH Bridge and connect first.";
|
| 280 |
}
|
| 281 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
try
|
| 283 |
{
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
var error = cmd.Error;
|
| 293 |
|
| 294 |
-
|
| 295 |
-
|
|
|
|
|
|
|
|
|
|
| 296 |
{
|
| 297 |
-
|
|
|
|
|
|
|
|
|
|
| 298 |
}
|
|
|
|
|
|
|
|
|
|
| 299 |
|
| 300 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
{
|
| 302 |
-
|
| 303 |
}
|
|
|
|
|
|
|
|
|
|
| 304 |
|
| 305 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 306 |
|
| 307 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
}
|
| 309 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 310 |
{
|
| 311 |
-
|
| 312 |
-
AppendOutput(error, Color.Red);
|
| 313 |
-
return error;
|
| 314 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
}
|
| 316 |
|
| 317 |
private void StartTcpServer()
|
|
@@ -353,6 +991,193 @@ namespace SshBridge
|
|
| 353 |
{
|
| 354 |
response = _isConnected ? $"CONNECTED:{_currentUser}@{_currentHost}" : "DISCONNECTED";
|
| 355 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 356 |
else if (command.StartsWith("__PREFILL__:"))
|
| 357 |
{
|
| 358 |
// Format: __PREFILL__:host:port:user:password
|
|
@@ -386,12 +1211,19 @@ namespace SshBridge
|
|
| 386 |
this.Invoke(() => Connect());
|
| 387 |
response = "CONNECTING";
|
| 388 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 389 |
else
|
| 390 |
{
|
| 391 |
response = ExecuteCommand(command);
|
| 392 |
}
|
| 393 |
// Encode newlines so they survive the single-line protocol
|
| 394 |
-
response = response.Replace("\r\n", "<<CRLF>>").Replace("\n", "<<LF>>");
|
| 395 |
writer.WriteLine(response);
|
| 396 |
}
|
| 397 |
}
|
|
|
|
| 1 |
using System;
|
| 2 |
+
using System.Collections.Generic;
|
| 3 |
using System.Drawing;
|
| 4 |
using System.IO;
|
| 5 |
+
using System.Linq;
|
| 6 |
using System.Net;
|
| 7 |
using System.Net.Sockets;
|
| 8 |
using System.Text;
|
| 9 |
+
using System.Text.RegularExpressions;
|
| 10 |
using System.Threading;
|
| 11 |
using System.Threading.Tasks;
|
| 12 |
using System.Windows.Forms;
|
|
|
|
| 41 |
private Thread? _serverThread;
|
| 42 |
private CancellationTokenSource? _cts;
|
| 43 |
private SshClient? _sshClient;
|
| 44 |
+
private ShellStream? _shellStream;
|
| 45 |
private bool _isConnected;
|
| 46 |
private int _commandCount;
|
| 47 |
private string _currentHost = "";
|
| 48 |
private string _currentUser = "";
|
| 49 |
+
private string _promptPattern = "";
|
| 50 |
+
private readonly object _shellLock = new object();
|
| 51 |
+
|
| 52 |
+
// UI state
|
| 53 |
+
private bool _stayOnTop;
|
| 54 |
+
private bool _penLifted;
|
| 55 |
+
private bool _allowSudo;
|
| 56 |
+
private bool _isRunning;
|
| 57 |
+
private string _storedPassword = "";
|
| 58 |
+
private Button _stayOnTopButton = null!;
|
| 59 |
+
private Button _penButton = null!;
|
| 60 |
+
private Button _sudoButton = null!;
|
| 61 |
+
private System.Windows.Forms.Timer _runningTimer = null!;
|
| 62 |
+
private int _runningDots;
|
| 63 |
|
| 64 |
private const int PORT = 52718;
|
| 65 |
+
private const int MAX_OUTPUT_BYTES = 500 * 1024; // 500KB limit for MCP
|
| 66 |
+
private const int MAX_OUTPUT_LINES = 150; // Keep last N lines for large output
|
| 67 |
+
private const int DEFAULT_TIMEOUT_MS = 30000;
|
| 68 |
+
|
| 69 |
+
private int _nextCommandTimeoutMs = DEFAULT_TIMEOUT_MS; // Configurable per-command
|
| 70 |
+
private Dictionary<string, int> _spawnedProcesses = new(); // Track background processes
|
| 71 |
|
| 72 |
public SshBridgeForm()
|
| 73 |
{
|
|
|
|
| 166 |
Height = 28,
|
| 167 |
Anchor = AnchorStyles.Right,
|
| 168 |
};
|
|
|
|
| 169 |
_disconnectButton.Click += (s, e) => Disconnect();
|
| 170 |
topBar.Controls.Add(_disconnectButton);
|
| 171 |
+
|
| 172 |
+
_penButton = new Button
|
| 173 |
+
{
|
| 174 |
+
Text = "✏️ Lift Pen",
|
| 175 |
+
ForeColor = Color.White,
|
| 176 |
+
BackColor = Color.FromArgb(70, 130, 180),
|
| 177 |
+
FlatStyle = FlatStyle.Flat,
|
| 178 |
+
Width = 100,
|
| 179 |
+
Height = 28,
|
| 180 |
+
};
|
| 181 |
+
_penButton.Click += (s, e) => TogglePen();
|
| 182 |
+
topBar.Controls.Add(_penButton);
|
| 183 |
+
|
| 184 |
+
_stayOnTopButton = new Button
|
| 185 |
+
{
|
| 186 |
+
Text = "📌 Pin",
|
| 187 |
+
ForeColor = Color.White,
|
| 188 |
+
BackColor = Color.FromArgb(80, 80, 85),
|
| 189 |
+
FlatStyle = FlatStyle.Flat,
|
| 190 |
+
Width = 70,
|
| 191 |
+
Height = 28,
|
| 192 |
+
};
|
| 193 |
+
_stayOnTopButton.Click += (s, e) => ToggleStayOnTop();
|
| 194 |
+
topBar.Controls.Add(_stayOnTopButton);
|
| 195 |
+
|
| 196 |
+
_sudoButton = new Button
|
| 197 |
+
{
|
| 198 |
+
Text = "🔓 Sudo",
|
| 199 |
+
ForeColor = Color.White,
|
| 200 |
+
BackColor = Color.FromArgb(80, 80, 85),
|
| 201 |
+
FlatStyle = FlatStyle.Flat,
|
| 202 |
+
Width = 80,
|
| 203 |
+
Height = 28,
|
| 204 |
+
};
|
| 205 |
+
_sudoButton.Click += (s, e) => ToggleSudo();
|
| 206 |
+
topBar.Controls.Add(_sudoButton);
|
| 207 |
+
|
| 208 |
+
// Position buttons from right edge
|
| 209 |
+
topBar.Resize += (s, e) => RepositionTopBarButtons();
|
| 210 |
+
this.Load += (s, e) => RepositionTopBarButtons();
|
| 211 |
|
| 212 |
_outputBox = new RichTextBox
|
| 213 |
{
|
|
|
|
| 219 |
WordWrap = false,
|
| 220 |
};
|
| 221 |
|
| 222 |
+
// Right-click context menu
|
| 223 |
+
var contextMenu = new ContextMenuStrip();
|
| 224 |
+
var copyItem = new ToolStripMenuItem("Copy", null, (s, e) =>
|
| 225 |
+
{
|
| 226 |
+
if (_outputBox.SelectionLength > 0)
|
| 227 |
+
Clipboard.SetText(_outputBox.SelectedText);
|
| 228 |
+
});
|
| 229 |
+
var copyAllItem = new ToolStripMenuItem("Copy All", null, (s, e) =>
|
| 230 |
+
{
|
| 231 |
+
if (!string.IsNullOrEmpty(_outputBox.Text))
|
| 232 |
+
Clipboard.SetText(_outputBox.Text);
|
| 233 |
+
});
|
| 234 |
+
var clearItem = new ToolStripMenuItem("Clear", null, (s, e) =>
|
| 235 |
+
{
|
| 236 |
+
_outputBox.Clear();
|
| 237 |
+
});
|
| 238 |
+
contextMenu.Items.Add(copyItem);
|
| 239 |
+
contextMenu.Items.Add(copyAllItem);
|
| 240 |
+
contextMenu.Items.AddRange(new ToolStripItem[] { new ToolStripSeparator(), clearItem });
|
| 241 |
+
_outputBox.ContextMenuStrip = contextMenu;
|
| 242 |
+
|
| 243 |
_sessionPanel.Controls.Add(_outputBox);
|
| 244 |
_sessionPanel.Controls.Add(topBar);
|
| 245 |
|
| 246 |
this.Controls.Add(_loginPanel);
|
| 247 |
this.Controls.Add(_sessionPanel);
|
| 248 |
+
|
| 249 |
+
// Timer for running indicator
|
| 250 |
+
_runningTimer = new System.Windows.Forms.Timer { Interval = 400 };
|
| 251 |
+
_runningTimer.Tick += (s, e) =>
|
| 252 |
+
{
|
| 253 |
+
if (_isRunning)
|
| 254 |
+
{
|
| 255 |
+
_runningDots = (_runningDots + 1) % 4;
|
| 256 |
+
var dots = new string('.', _runningDots);
|
| 257 |
+
_statusLabel.Text = $"⚡ Running{dots}";
|
| 258 |
+
_statusLabel.ForeColor = Color.Yellow;
|
| 259 |
+
}
|
| 260 |
+
};
|
| 261 |
}
|
| 262 |
|
| 263 |
private void Connect()
|
|
|
|
| 292 |
_currentUser = user;
|
| 293 |
_commandCount = 0;
|
| 294 |
|
| 295 |
+
// Create persistent shell stream
|
| 296 |
+
_shellStream = _sshClient.CreateShellStream("xterm", 200, 50, 800, 600, 65536);
|
| 297 |
+
|
| 298 |
+
// Wait for initial prompt and detect the pattern
|
| 299 |
+
Thread.Sleep(500);
|
| 300 |
+
var initial = ReadAvailable();
|
| 301 |
+
_promptPattern = DetectPrompt(initial, user);
|
| 302 |
+
|
| 303 |
this.Invoke(() =>
|
| 304 |
{
|
| 305 |
+
_storedPassword = _passwordBox.Text;
|
| 306 |
_passwordBox.Clear();
|
| 307 |
OnConnected();
|
| 308 |
+
if (!string.IsNullOrEmpty(initial))
|
| 309 |
+
{
|
| 310 |
+
var cleaned = StripAnsiCodes(initial).Trim();
|
| 311 |
+
if (!string.IsNullOrEmpty(cleaned))
|
| 312 |
+
{
|
| 313 |
+
AppendOutput(cleaned, Color.Gray);
|
| 314 |
+
}
|
| 315 |
+
}
|
| 316 |
});
|
| 317 |
}
|
| 318 |
else
|
|
|
|
| 345 |
private void Disconnect()
|
| 346 |
{
|
| 347 |
_isConnected = false;
|
| 348 |
+
_penLifted = false;
|
| 349 |
+
_allowSudo = false;
|
| 350 |
+
_storedPassword = "";
|
| 351 |
|
| 352 |
try
|
| 353 |
{
|
| 354 |
+
_shellStream?.Dispose();
|
| 355 |
_sshClient?.Disconnect();
|
| 356 |
_sshClient?.Dispose();
|
| 357 |
}
|
| 358 |
catch { }
|
| 359 |
|
| 360 |
+
_shellStream = null;
|
| 361 |
_sshClient = null;
|
| 362 |
_currentHost = "";
|
| 363 |
_currentUser = "";
|
| 364 |
+
_promptPattern = "";
|
| 365 |
|
| 366 |
_loginPanel.Visible = true;
|
| 367 |
_sessionPanel.Visible = false;
|
| 368 |
_connectButton.Enabled = true;
|
| 369 |
_connectButton.Text = "Connect";
|
| 370 |
_statusLabel.Text = "Disconnected";
|
| 371 |
+
UpdatePenButton();
|
| 372 |
+
UpdateSudoButton();
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
private void RepositionTopBarButtons()
|
| 376 |
+
{
|
| 377 |
+
int rightEdge = _sessionPanel.Width - 10;
|
| 378 |
+
_disconnectButton.Location = new Point(rightEdge - _disconnectButton.Width, 6);
|
| 379 |
+
_penButton.Location = new Point(_disconnectButton.Left - _penButton.Width - 5, 6);
|
| 380 |
+
_stayOnTopButton.Location = new Point(_penButton.Left - _stayOnTopButton.Width - 5, 6);
|
| 381 |
+
_sudoButton.Location = new Point(_stayOnTopButton.Left - _sudoButton.Width - 5, 6);
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
private void ToggleStayOnTop()
|
| 385 |
+
{
|
| 386 |
+
_stayOnTop = !_stayOnTop;
|
| 387 |
+
this.TopMost = _stayOnTop;
|
| 388 |
+
_stayOnTopButton.BackColor = _stayOnTop
|
| 389 |
+
? Color.FromArgb(60, 140, 60) // Green when active
|
| 390 |
+
: Color.FromArgb(80, 80, 85); // Gray when inactive
|
| 391 |
+
_stayOnTopButton.Text = _stayOnTop ? "📌 Pinned" : "📌 Pin";
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
private void TogglePen()
|
| 395 |
+
{
|
| 396 |
+
_penLifted = !_penLifted;
|
| 397 |
+
UpdatePenButton();
|
| 398 |
+
|
| 399 |
+
if (_penLifted)
|
| 400 |
+
{
|
| 401 |
+
AppendOutput("=== PEN LIFTED - Claude paused ===", Color.Orange);
|
| 402 |
+
}
|
| 403 |
+
else
|
| 404 |
+
{
|
| 405 |
+
AppendOutput("=== PEN DOWN - Claude resumed ===", Color.LimeGreen);
|
| 406 |
+
}
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
private void UpdatePenButton()
|
| 410 |
+
{
|
| 411 |
+
_penButton.BackColor = _penLifted
|
| 412 |
+
? Color.FromArgb(200, 120, 50) // Orange when lifted
|
| 413 |
+
: Color.FromArgb(70, 130, 180); // Blue when down
|
| 414 |
+
_penButton.Text = _penLifted ? "✏️ Pen Up!" : "✏️ Lift Pen";
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
private void ToggleSudo()
|
| 418 |
+
{
|
| 419 |
+
if (string.IsNullOrEmpty(_storedPassword))
|
| 420 |
+
{
|
| 421 |
+
MessageBox.Show("No password stored. Reconnect to enable sudo.", "Sudo", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
| 422 |
+
return;
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
_allowSudo = !_allowSudo;
|
| 426 |
+
UpdateSudoButton();
|
| 427 |
+
|
| 428 |
+
if (_allowSudo)
|
| 429 |
+
{
|
| 430 |
+
AppendOutput("=== SUDO ENABLED - Password will auto-send ===", Color.FromArgb(180, 100, 255));
|
| 431 |
+
}
|
| 432 |
+
else
|
| 433 |
+
{
|
| 434 |
+
AppendOutput("=== SUDO DISABLED ===", Color.Gray);
|
| 435 |
+
}
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
private void UpdateSudoButton()
|
| 439 |
+
{
|
| 440 |
+
_sudoButton.BackColor = _allowSudo
|
| 441 |
+
? Color.FromArgb(140, 80, 200) // Purple when enabled
|
| 442 |
+
: Color.FromArgb(80, 80, 85); // Gray when disabled
|
| 443 |
+
_sudoButton.Text = _allowSudo ? "🔐 Sudo On" : "🔓 Sudo";
|
| 444 |
}
|
| 445 |
|
| 446 |
private void AppendOutput(string? text, Color? color = null)
|
|
|
|
| 466 |
|
| 467 |
public string ExecuteCommand(string command)
|
| 468 |
{
|
| 469 |
+
if (!_isConnected || _sshClient == null || !_sshClient.IsConnected || _shellStream == null)
|
| 470 |
{
|
| 471 |
return "ERROR: Not connected. Open SSH Bridge and connect first.";
|
| 472 |
}
|
| 473 |
|
| 474 |
+
lock (_shellLock)
|
| 475 |
+
{
|
| 476 |
+
try
|
| 477 |
+
{
|
| 478 |
+
_commandCount++;
|
| 479 |
+
SetRunning(true);
|
| 480 |
+
|
| 481 |
+
// Clear any pending output
|
| 482 |
+
ReadAvailable();
|
| 483 |
+
|
| 484 |
+
// Block interactive commands that would break the shell
|
| 485 |
+
if (IsBlockedCommand(command, out string blockReason))
|
| 486 |
+
{
|
| 487 |
+
AppendOutput($"> {command}", Color.Gray);
|
| 488 |
+
AppendOutput($"[BLOCKED - Interactive command]\n{blockReason}", Color.Red);
|
| 489 |
+
return $"BLOCKED: Interactive command not supported.\n{blockReason}";
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
// Block sudo commands if sudo not enabled - BEFORE sending
|
| 493 |
+
if (command.TrimStart().StartsWith("sudo") && !_allowSudo)
|
| 494 |
+
{
|
| 495 |
+
AppendOutput($"> {command}", Color.White);
|
| 496 |
+
AppendOutput("[SUDO BLOCKED - Command not sent. Enable sudo button to allow.]", Color.Red);
|
| 497 |
+
return "SUDO_BLOCKED: Sudo commands are disabled. Click the Sudo button to enable.";
|
| 498 |
+
}
|
| 499 |
+
|
| 500 |
+
// Send command
|
| 501 |
+
_shellStream.WriteLine(command);
|
| 502 |
+
|
| 503 |
+
// Wait for output and prompt
|
| 504 |
+
var output = WaitForPrompt(_nextCommandTimeoutMs);
|
| 505 |
+
_nextCommandTimeoutMs = DEFAULT_TIMEOUT_MS; // Reset after use
|
| 506 |
+
|
| 507 |
+
// Handle sudo password prompt (only runs if sudo IS enabled)
|
| 508 |
+
if (command.TrimStart().StartsWith("sudo") && IsSudoPrompt(output))
|
| 509 |
+
{
|
| 510 |
+
AppendOutput("[sudo password auto-sent]", Color.FromArgb(180, 100, 255));
|
| 511 |
+
_shellStream.WriteLine(_storedPassword);
|
| 512 |
+
var sudoOutput = WaitForPrompt(30000);
|
| 513 |
+
output += sudoOutput;
|
| 514 |
+
}
|
| 515 |
+
|
| 516 |
+
|
| 517 |
+
// Clean up output - remove the echoed command and prompt
|
| 518 |
+
output = CleanOutput(output, command);
|
| 519 |
+
|
| 520 |
+
if (string.IsNullOrEmpty(output))
|
| 521 |
+
{
|
| 522 |
+
output = "(no output)";
|
| 523 |
+
}
|
| 524 |
+
|
| 525 |
+
// Tail large output - keep last N lines
|
| 526 |
+
var lines = output.Split('\n');
|
| 527 |
+
string returnOutput;
|
| 528 |
+
if (lines.Length > MAX_OUTPUT_LINES)
|
| 529 |
+
{
|
| 530 |
+
var tailLines = lines.Skip(lines.Length - MAX_OUTPUT_LINES).ToArray();
|
| 531 |
+
returnOutput = $"[... {lines.Length - MAX_OUTPUT_LINES} lines truncated ...]\n" + string.Join("\n", tailLines);
|
| 532 |
+
}
|
| 533 |
+
else
|
| 534 |
+
{
|
| 535 |
+
returnOutput = output;
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
+
// Output already displayed in real-time by WaitForPrompt
|
| 539 |
+
if (lines.Length > MAX_OUTPUT_LINES)
|
| 540 |
+
{
|
| 541 |
+
AppendOutput($"[Returned last {MAX_OUTPUT_LINES} of {lines.Length} lines to Claude]", Color.Gray);
|
| 542 |
+
}
|
| 543 |
+
|
| 544 |
+
// Final size check
|
| 545 |
+
if (Encoding.UTF8.GetByteCount(returnOutput) > MAX_OUTPUT_BYTES)
|
| 546 |
+
{
|
| 547 |
+
returnOutput = TruncateToBytes(returnOutput, MAX_OUTPUT_BYTES);
|
| 548 |
+
returnOutput += $"\n\n[OUTPUT TRUNCATED - exceeded 500KB limit]";
|
| 549 |
+
}
|
| 550 |
+
|
| 551 |
+
return returnOutput;
|
| 552 |
+
}
|
| 553 |
+
catch (Exception ex)
|
| 554 |
+
{
|
| 555 |
+
var error = $"ERROR: {ex.Message}";
|
| 556 |
+
AppendOutput(error, Color.Red);
|
| 557 |
+
return error;
|
| 558 |
+
}
|
| 559 |
+
finally
|
| 560 |
+
{
|
| 561 |
+
SetRunning(false);
|
| 562 |
+
}
|
| 563 |
+
}
|
| 564 |
+
}
|
| 565 |
+
|
| 566 |
+
private void SetRunning(bool running)
|
| 567 |
+
{
|
| 568 |
+
_isRunning = running;
|
| 569 |
+
if (this.InvokeRequired)
|
| 570 |
+
{
|
| 571 |
+
this.Invoke(() => SetRunning(running));
|
| 572 |
+
return;
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
if (running)
|
| 576 |
+
{
|
| 577 |
+
_runningDots = 0;
|
| 578 |
+
_runningTimer.Start();
|
| 579 |
+
}
|
| 580 |
+
else
|
| 581 |
+
{
|
| 582 |
+
_runningTimer.Stop();
|
| 583 |
+
_statusLabel.Text = $"Connected to {_currentUser}@{_currentHost}";
|
| 584 |
+
_statusLabel.ForeColor = Color.White;
|
| 585 |
+
}
|
| 586 |
+
}
|
| 587 |
+
|
| 588 |
+
private static string TruncateToBytes(string text, int maxBytes)
|
| 589 |
+
{
|
| 590 |
+
if (string.IsNullOrEmpty(text)) return text;
|
| 591 |
+
var bytes = Encoding.UTF8.GetBytes(text);
|
| 592 |
+
if (bytes.Length <= maxBytes) return text;
|
| 593 |
+
|
| 594 |
+
// Find a safe cut point (don't break UTF-8 sequences)
|
| 595 |
+
int cutPoint = maxBytes;
|
| 596 |
+
while (cutPoint > 0 && (bytes[cutPoint] & 0xC0) == 0x80)
|
| 597 |
+
cutPoint--;
|
| 598 |
+
|
| 599 |
+
return Encoding.UTF8.GetString(bytes, 0, cutPoint);
|
| 600 |
+
}
|
| 601 |
+
|
| 602 |
+
private bool IsProcessRunning(int pid)
|
| 603 |
+
{
|
| 604 |
+
if (pid <= 0) return false;
|
| 605 |
try
|
| 606 |
{
|
| 607 |
+
var proc = System.Diagnostics.Process.GetProcessById(pid);
|
| 608 |
+
return !proc.HasExited;
|
| 609 |
+
}
|
| 610 |
+
catch
|
| 611 |
+
{
|
| 612 |
+
return false;
|
| 613 |
+
}
|
| 614 |
+
}
|
|
|
|
| 615 |
|
| 616 |
+
private void SendAbort()
|
| 617 |
+
{
|
| 618 |
+
if (_shellStream != null && _isConnected)
|
| 619 |
+
{
|
| 620 |
+
try
|
| 621 |
{
|
| 622 |
+
_shellStream.Write("\x03"); // Ctrl+C
|
| 623 |
+
Thread.Sleep(100);
|
| 624 |
+
_shellStream.Write("\x03"); // Send twice for stubborn processes
|
| 625 |
+
AppendOutput("[ABORT SIGNAL SENT - Ctrl+C]", Color.Orange);
|
| 626 |
}
|
| 627 |
+
catch { }
|
| 628 |
+
}
|
| 629 |
+
}
|
| 630 |
|
| 631 |
+
private string ReadAvailable()
|
| 632 |
+
{
|
| 633 |
+
if (_shellStream == null) return "";
|
| 634 |
+
|
| 635 |
+
var sb = new StringBuilder();
|
| 636 |
+
while (_shellStream.DataAvailable)
|
| 637 |
+
{
|
| 638 |
+
var buffer = new byte[4096];
|
| 639 |
+
var read = _shellStream.Read(buffer, 0, buffer.Length);
|
| 640 |
+
if (read > 0)
|
| 641 |
{
|
| 642 |
+
sb.Append(Encoding.UTF8.GetString(buffer, 0, read));
|
| 643 |
}
|
| 644 |
+
}
|
| 645 |
+
return sb.ToString();
|
| 646 |
+
}
|
| 647 |
|
| 648 |
+
private string WaitForPrompt(int timeoutMs)
|
| 649 |
+
{
|
| 650 |
+
if (_shellStream == null) return "";
|
| 651 |
+
|
| 652 |
+
var sb = new StringBuilder();
|
| 653 |
+
var sw = System.Diagnostics.Stopwatch.StartNew();
|
| 654 |
+
var lastDataTime = sw.ElapsedMilliseconds;
|
| 655 |
+
|
| 656 |
+
// Minimum wait time to ensure we get all output
|
| 657 |
+
const int minWaitMs = 300;
|
| 658 |
+
const int quietTimeMs = 150;
|
| 659 |
+
|
| 660 |
+
while (sw.ElapsedMilliseconds < timeoutMs)
|
| 661 |
+
{
|
| 662 |
+
// Check for pen-lift interrupt - abort immediately
|
| 663 |
+
if (_penLifted)
|
| 664 |
+
{
|
| 665 |
+
try { _shellStream.Write("\x03"); } catch { } // Send Ctrl+C
|
| 666 |
+
Thread.Sleep(200);
|
| 667 |
+
sb.Append(ReadAvailable()); // Grab remaining output
|
| 668 |
+
sb.Append("\n[ABORTED BY USER - Pen lifted]");
|
| 669 |
+
AppendOutput("[Command aborted - Pen lifted]", Color.Orange);
|
| 670 |
+
return sb.ToString();
|
| 671 |
+
}
|
| 672 |
|
| 673 |
+
if (_shellStream.DataAvailable)
|
| 674 |
+
{
|
| 675 |
+
var buffer = new byte[4096];
|
| 676 |
+
var read = _shellStream.Read(buffer, 0, buffer.Length);
|
| 677 |
+
if (read > 0)
|
| 678 |
+
{
|
| 679 |
+
var chunk = Encoding.UTF8.GetString(buffer, 0, read);
|
| 680 |
+
sb.Append(chunk);
|
| 681 |
+
lastDataTime = sw.ElapsedMilliseconds;
|
| 682 |
+
|
| 683 |
+
// Real-time display update
|
| 684 |
+
var cleaned = StripAnsiCodes(chunk);
|
| 685 |
+
if (!string.IsNullOrEmpty(cleaned))
|
| 686 |
+
{
|
| 687 |
+
AppendOutput(cleaned, Color.FromArgb(180, 180, 180));
|
| 688 |
+
}
|
| 689 |
+
}
|
| 690 |
+
}
|
| 691 |
+
else
|
| 692 |
+
{
|
| 693 |
+
// Only check for prompt after minimum wait AND data stops flowing
|
| 694 |
+
if (sw.ElapsedMilliseconds > minWaitMs &&
|
| 695 |
+
sw.ElapsedMilliseconds - lastDataTime > quietTimeMs)
|
| 696 |
+
{
|
| 697 |
+
var current = sb.ToString();
|
| 698 |
+
if (LooksLikePrompt(current))
|
| 699 |
+
{
|
| 700 |
+
break;
|
| 701 |
+
}
|
| 702 |
+
}
|
| 703 |
+
Thread.Sleep(5); // Fast polling for responsive terminal
|
| 704 |
+
}
|
| 705 |
+
}
|
| 706 |
+
|
| 707 |
+
return sb.ToString();
|
| 708 |
+
}
|
| 709 |
+
|
| 710 |
+
private bool LooksLikePrompt(string text)
|
| 711 |
+
{
|
| 712 |
+
if (string.IsNullOrEmpty(text)) return false;
|
| 713 |
+
|
| 714 |
+
// Normalize line endings and get last non-empty line
|
| 715 |
+
var normalized = text.Replace("\r\n", "\n").Replace("\r", "\n");
|
| 716 |
+
var lines = normalized.Split('\n');
|
| 717 |
+
|
| 718 |
+
// Find last non-empty line
|
| 719 |
+
string lastLine = "";
|
| 720 |
+
for (int i = lines.Length - 1; i >= 0; i--)
|
| 721 |
+
{
|
| 722 |
+
var trimmed = lines[i].Trim();
|
| 723 |
+
if (!string.IsNullOrEmpty(trimmed))
|
| 724 |
+
{
|
| 725 |
+
lastLine = trimmed;
|
| 726 |
+
break;
|
| 727 |
+
}
|
| 728 |
+
}
|
| 729 |
+
|
| 730 |
+
if (string.IsNullOrEmpty(lastLine)) return false;
|
| 731 |
+
|
| 732 |
+
// Known prompt pattern from connection (most reliable)
|
| 733 |
+
if (!string.IsNullOrEmpty(_promptPattern) && lastLine.Contains(_promptPattern)) return true;
|
| 734 |
+
|
| 735 |
+
// Linux/Unix prompts: user@host patterns ending with $ or #
|
| 736 |
+
if ((lastLine.EndsWith("$") || lastLine.EndsWith("#")) && lastLine.Contains("@")) return true;
|
| 737 |
+
|
| 738 |
+
// Windows cmd prompt: ends with > and contains drive letter or path
|
| 739 |
+
if (lastLine.EndsWith(">") && (lastLine.Contains(":\\") || lastLine.Contains(":/"))) return true;
|
| 740 |
+
|
| 741 |
+
// PowerShell prompt
|
| 742 |
+
if (lastLine.EndsWith("PS>") || lastLine.EndsWith("PS >")) return true;
|
| 743 |
+
|
| 744 |
+
return false;
|
| 745 |
+
}
|
| 746 |
+
|
| 747 |
+
private bool IsSudoPrompt(string text)
|
| 748 |
+
{
|
| 749 |
+
if (string.IsNullOrEmpty(text)) return false;
|
| 750 |
+
|
| 751 |
+
// Only check the last line - prevents false matches on buffer history
|
| 752 |
+
var lines = text.Split('\n');
|
| 753 |
+
var lastLine = lines[lines.Length - 1].Trim().ToLowerInvariant();
|
| 754 |
+
|
| 755 |
+
// Match password prompts
|
| 756 |
+
if (lastLine.EndsWith("password:")) return true;
|
| 757 |
+
if (lastLine.Contains("[sudo]") && lastLine.Contains("password")) return true;
|
| 758 |
+
if (Regex.IsMatch(lastLine, @"password for \w+:")) return true;
|
| 759 |
+
|
| 760 |
+
return false;
|
| 761 |
+
}
|
| 762 |
+
|
| 763 |
+
private bool IsBlockedCommand(string command, out string reason)
|
| 764 |
+
{
|
| 765 |
+
var trimmed = command.Trim();
|
| 766 |
+
var parts = trimmed.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
| 767 |
+
var firstWord = parts.FirstOrDefault()?.ToLowerInvariant() ?? "";
|
| 768 |
+
var fullLower = trimmed.ToLowerInvariant();
|
| 769 |
+
|
| 770 |
+
reason = "";
|
| 771 |
+
|
| 772 |
+
switch (firstWord)
|
| 773 |
+
{
|
| 774 |
+
// Editors - always blocked (no non-interactive mode)
|
| 775 |
+
case "nano":
|
| 776 |
+
case "vim":
|
| 777 |
+
case "vi":
|
| 778 |
+
case "nvim":
|
| 779 |
+
case "emacs":
|
| 780 |
+
case "pico":
|
| 781 |
+
case "joe":
|
| 782 |
+
case "mcedit":
|
| 783 |
+
reason = "Interactive editors not supported. Use:\n" +
|
| 784 |
+
"• echo \"content\" > file.txt (create/overwrite)\n" +
|
| 785 |
+
"• echo \"more\" >> file.txt (append)\n" +
|
| 786 |
+
"• cat << 'EOF' > file.txt (multi-line)\n" +
|
| 787 |
+
"• sed -i 's/old/new/g' file.txt (find/replace)";
|
| 788 |
+
return true;
|
| 789 |
+
|
| 790 |
+
// Pagers - always blocked
|
| 791 |
+
case "less":
|
| 792 |
+
case "more":
|
| 793 |
+
reason = "Use 'cat', 'head -n 100', or 'tail -n 100' instead.";
|
| 794 |
+
return true;
|
| 795 |
+
|
| 796 |
+
// TUI monitors - allow batch mode
|
| 797 |
+
case "top":
|
| 798 |
+
if (!fullLower.Contains("-b"))
|
| 799 |
+
{
|
| 800 |
+
reason = "Use 'top -b -n 1' for batch mode, or 'ps aux' instead.";
|
| 801 |
+
return true;
|
| 802 |
+
}
|
| 803 |
+
break;
|
| 804 |
+
case "htop":
|
| 805 |
+
case "btop":
|
| 806 |
+
case "atop":
|
| 807 |
+
case "nmon":
|
| 808 |
+
case "glances":
|
| 809 |
+
reason = "Use 'ps aux', 'free -h', 'df -h', or 'top -b -n 1' instead.";
|
| 810 |
+
return true;
|
| 811 |
+
|
| 812 |
+
// Databases - allow with query flags
|
| 813 |
+
case "mysql":
|
| 814 |
+
if (!fullLower.Contains("-e"))
|
| 815 |
+
{
|
| 816 |
+
reason = "Use 'mysql -e \"SELECT...\"' for non-interactive query.";
|
| 817 |
+
return true;
|
| 818 |
+
}
|
| 819 |
+
break;
|
| 820 |
+
case "psql":
|
| 821 |
+
if (!fullLower.Contains("-c"))
|
| 822 |
+
{
|
| 823 |
+
reason = "Use 'psql -c \"SELECT...\"' for non-interactive query.";
|
| 824 |
+
return true;
|
| 825 |
+
}
|
| 826 |
+
break;
|
| 827 |
+
case "mongo":
|
| 828 |
+
case "mongosh":
|
| 829 |
+
if (!fullLower.Contains("--eval"))
|
| 830 |
+
{
|
| 831 |
+
reason = "Use 'mongosh --eval \"db.collection.find()\"' for non-interactive.";
|
| 832 |
+
return true;
|
| 833 |
+
}
|
| 834 |
+
break;
|
| 835 |
+
case "redis-cli":
|
| 836 |
+
if (parts.Length == 1)
|
| 837 |
+
{
|
| 838 |
+
reason = "Add command: 'redis-cli GET key' or 'redis-cli INFO'.";
|
| 839 |
+
return true;
|
| 840 |
+
}
|
| 841 |
+
break;
|
| 842 |
+
case "sqlite3":
|
| 843 |
+
if (!fullLower.Contains("-cmd") && !trimmed.Contains("\""))
|
| 844 |
+
{
|
| 845 |
+
reason = "Use 'sqlite3 db.sqlite \"SELECT...\"' for non-interactive.";
|
| 846 |
+
return true;
|
| 847 |
+
}
|
| 848 |
+
break;
|
| 849 |
+
|
| 850 |
+
// Terminal multiplexers - always blocked
|
| 851 |
+
case "tmux":
|
| 852 |
+
case "screen":
|
| 853 |
+
case "byobu":
|
| 854 |
+
reason = "Terminal multiplexers not supported in this shell.";
|
| 855 |
+
return true;
|
| 856 |
+
|
| 857 |
+
// File managers - always blocked
|
| 858 |
+
case "mc":
|
| 859 |
+
case "ranger":
|
| 860 |
+
case "nnn":
|
| 861 |
+
reason = "Use 'ls -la', 'find', or 'tree' instead.";
|
| 862 |
+
return true;
|
| 863 |
+
|
| 864 |
+
// Nested shells - block unless -c flag
|
| 865 |
+
case "bash":
|
| 866 |
+
case "zsh":
|
| 867 |
+
case "fish":
|
| 868 |
+
case "sh":
|
| 869 |
+
case "csh":
|
| 870 |
+
case "tcsh":
|
| 871 |
+
if (!fullLower.Contains("-c"))
|
| 872 |
+
{
|
| 873 |
+
reason = "Nested shells not supported. Use 'bash -c \"command\"' for one-offs.";
|
| 874 |
+
return true;
|
| 875 |
+
}
|
| 876 |
+
break;
|
| 877 |
+
|
| 878 |
+
// SSH/remote - always blocked
|
| 879 |
+
case "ssh":
|
| 880 |
+
case "telnet":
|
| 881 |
+
reason = "Nested SSH not supported. Disconnect and connect to the other server.";
|
| 882 |
+
return true;
|
| 883 |
+
|
| 884 |
+
// Man pages - always blocked
|
| 885 |
+
case "man":
|
| 886 |
+
case "info":
|
| 887 |
+
reason = "Use 'command --help' or search online.";
|
| 888 |
+
return true;
|
| 889 |
+
|
| 890 |
+
// FTP - always blocked
|
| 891 |
+
case "ftp":
|
| 892 |
+
case "sftp":
|
| 893 |
+
reason = "Interactive FTP not supported. Use 'scp' or 'curl' instead.";
|
| 894 |
+
return true;
|
| 895 |
}
|
| 896 |
+
|
| 897 |
+
return false;
|
| 898 |
+
}
|
| 899 |
+
|
| 900 |
+
private string DetectPrompt(string initialOutput, string user)
|
| 901 |
+
{
|
| 902 |
+
if (string.IsNullOrEmpty(initialOutput)) return ">";
|
| 903 |
+
|
| 904 |
+
var lines = initialOutput.Split('\n');
|
| 905 |
+
var lastLine = lines[^1].Trim();
|
| 906 |
+
|
| 907 |
+
// Return the last line as the prompt pattern
|
| 908 |
+
if (lastLine.EndsWith(">") || lastLine.EndsWith("$") || lastLine.EndsWith("#"))
|
| 909 |
{
|
| 910 |
+
return lastLine;
|
|
|
|
|
|
|
| 911 |
}
|
| 912 |
+
|
| 913 |
+
return ">";
|
| 914 |
+
}
|
| 915 |
+
|
| 916 |
+
private string CleanOutput(string output, string command)
|
| 917 |
+
{
|
| 918 |
+
if (string.IsNullOrEmpty(output)) return "";
|
| 919 |
+
|
| 920 |
+
// Strip ANSI escape codes and normalize line endings
|
| 921 |
+
var cleaned = StripAnsiCodes(output);
|
| 922 |
+
// Convert all line endings to \n only
|
| 923 |
+
cleaned = cleaned.Replace("\r\n", "\n").Replace("\r", "\n");
|
| 924 |
+
return cleaned.Trim();
|
| 925 |
+
}
|
| 926 |
+
|
| 927 |
+
private static string StripAnsiCodes(string text)
|
| 928 |
+
{
|
| 929 |
+
if (string.IsNullOrEmpty(text)) return text;
|
| 930 |
+
|
| 931 |
+
// Remove ANSI escape sequences:
|
| 932 |
+
// CSI sequences: ESC [ ... final_byte
|
| 933 |
+
// OSC sequences: ESC ] ... BEL or ESC \
|
| 934 |
+
// Simple escapes: ESC followed by single char
|
| 935 |
+
|
| 936 |
+
var result = text;
|
| 937 |
+
|
| 938 |
+
// CSI sequences (ESC [ or 0x9B followed by parameters and final byte)
|
| 939 |
+
result = Regex.Replace(result, @"\x1B\[[0-9;?]*[A-Za-z]", "");
|
| 940 |
+
result = Regex.Replace(result, @"\x1B\[[0-9;?]*[ -/]*[@-~]", "");
|
| 941 |
+
|
| 942 |
+
// OSC sequences (ESC ] ... BEL or ST)
|
| 943 |
+
result = Regex.Replace(result, @"\x1B\][^\x07\x1B]*(\x07|\x1B\\)?", "");
|
| 944 |
+
|
| 945 |
+
// Other escape sequences
|
| 946 |
+
result = Regex.Replace(result, @"\x1B[()][AB012]", ""); // Character set selection
|
| 947 |
+
result = Regex.Replace(result, @"\x1B[@-_]", ""); // C1 control codes
|
| 948 |
+
|
| 949 |
+
// Clean up any remaining escape chars and control chars (except newline, tab, CR)
|
| 950 |
+
result = Regex.Replace(result, @"[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]", "");
|
| 951 |
+
|
| 952 |
+
return result;
|
| 953 |
}
|
| 954 |
|
| 955 |
private void StartTcpServer()
|
|
|
|
| 991 |
{
|
| 992 |
response = _isConnected ? $"CONNECTED:{_currentUser}@{_currentHost}" : "DISCONNECTED";
|
| 993 |
}
|
| 994 |
+
else if (command == "__PEN_STATUS__")
|
| 995 |
+
{
|
| 996 |
+
response = _penLifted ? "PEN_LIFTED" : "PEN_DOWN";
|
| 997 |
+
}
|
| 998 |
+
else if (command == "__PEN_DOWN__")
|
| 999 |
+
{
|
| 1000 |
+
if (_penLifted)
|
| 1001 |
+
{
|
| 1002 |
+
this.Invoke(() => TogglePen());
|
| 1003 |
+
response = "PEN_LOWERED";
|
| 1004 |
+
}
|
| 1005 |
+
else
|
| 1006 |
+
{
|
| 1007 |
+
response = "PEN_ALREADY_DOWN";
|
| 1008 |
+
}
|
| 1009 |
+
}
|
| 1010 |
+
else if (command == "__ABORT__")
|
| 1011 |
+
{
|
| 1012 |
+
SendAbort();
|
| 1013 |
+
response = _isRunning ? "ABORT_SENT" : "NO_COMMAND_RUNNING";
|
| 1014 |
+
}
|
| 1015 |
+
else if (command == "__IS_RUNNING__")
|
| 1016 |
+
{
|
| 1017 |
+
response = _isRunning ? "RUNNING" : "IDLE";
|
| 1018 |
+
}
|
| 1019 |
+
else if (command.StartsWith("__TIMEOUT__:"))
|
| 1020 |
+
{
|
| 1021 |
+
var secStr = command.Substring(12);
|
| 1022 |
+
if (int.TryParse(secStr, out int seconds) && seconds > 0 && seconds <= 3600)
|
| 1023 |
+
{
|
| 1024 |
+
_nextCommandTimeoutMs = seconds * 1000;
|
| 1025 |
+
response = $"TIMEOUT_SET:{seconds}s";
|
| 1026 |
+
AppendOutput($"[Timeout set to {seconds}s for next command]", Color.Cyan);
|
| 1027 |
+
}
|
| 1028 |
+
else
|
| 1029 |
+
{
|
| 1030 |
+
response = "ERROR: Invalid timeout (1-3600 seconds)";
|
| 1031 |
+
}
|
| 1032 |
+
}
|
| 1033 |
+
else if (command.StartsWith("__KILL_PORT__:"))
|
| 1034 |
+
{
|
| 1035 |
+
var portStr = command.Substring(14);
|
| 1036 |
+
if (int.TryParse(portStr, out int portNum) && portNum > 0 && portNum <= 65535)
|
| 1037 |
+
{
|
| 1038 |
+
// Execute netstat to find PID, then taskkill
|
| 1039 |
+
var killCmd = $"for /f \"tokens=5\" %a in ('netstat -ano ^| findstr :{portNum} ^| findstr LISTENING') do @taskkill /PID %a /F 2>nul & echo Killed PID %a";
|
| 1040 |
+
response = ExecuteCommand(killCmd);
|
| 1041 |
+
}
|
| 1042 |
+
else
|
| 1043 |
+
{
|
| 1044 |
+
response = "ERROR: Invalid port number";
|
| 1045 |
+
}
|
| 1046 |
+
}
|
| 1047 |
+
else if (command.StartsWith("__WRITE_FILE__:"))
|
| 1048 |
+
{
|
| 1049 |
+
// Format: __WRITE_FILE__:C:\path\file.txt|content
|
| 1050 |
+
// Uses | as delimiter since it's not valid in Windows paths
|
| 1051 |
+
// Use <<LF>> for newlines, <<CR>> for carriage returns
|
| 1052 |
+
var rest = command.Substring(15);
|
| 1053 |
+
var pipeIdx = rest.IndexOf('|');
|
| 1054 |
+
if (pipeIdx > 0)
|
| 1055 |
+
{
|
| 1056 |
+
var path = rest.Substring(0, pipeIdx);
|
| 1057 |
+
var content = rest.Substring(pipeIdx + 1)
|
| 1058 |
+
.Replace("<<CRLF>>", "\r\n")
|
| 1059 |
+
.Replace("<<LF>>", "\r\n") // Convert to Windows line endings
|
| 1060 |
+
.Replace("<<CR>>", "\r");
|
| 1061 |
+
try
|
| 1062 |
+
{
|
| 1063 |
+
// Base64 encode to avoid ALL escaping issues
|
| 1064 |
+
var base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(content));
|
| 1065 |
+
var psCmd = $"[System.IO.File]::WriteAllText('{path}', [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('{base64}')))";
|
| 1066 |
+
ExecuteCommand($"powershell -Command \"{psCmd}\"");
|
| 1067 |
+
response = $"WRITTEN:{path} ({content.Length} chars)";
|
| 1068 |
+
AppendOutput($"[Wrote {content.Length} chars to {path}]", Color.Green);
|
| 1069 |
+
}
|
| 1070 |
+
catch (Exception ex)
|
| 1071 |
+
{
|
| 1072 |
+
response = $"ERROR: {ex.Message}";
|
| 1073 |
+
}
|
| 1074 |
+
}
|
| 1075 |
+
else
|
| 1076 |
+
{
|
| 1077 |
+
response = "ERROR: Use __WRITE_FILE__:path|content (use <<LF>> for newlines)";
|
| 1078 |
+
}
|
| 1079 |
+
}
|
| 1080 |
+
else if (command.StartsWith("__APPEND_FILE__:"))
|
| 1081 |
+
{
|
| 1082 |
+
// Format: __APPEND_FILE__:C:\path\file.txt|content
|
| 1083 |
+
var rest = command.Substring(16);
|
| 1084 |
+
var pipeIdx = rest.IndexOf('|');
|
| 1085 |
+
if (pipeIdx > 0)
|
| 1086 |
+
{
|
| 1087 |
+
var path = rest.Substring(0, pipeIdx);
|
| 1088 |
+
var content = rest.Substring(pipeIdx + 1)
|
| 1089 |
+
.Replace("<<CRLF>>", "\r\n")
|
| 1090 |
+
.Replace("<<LF>>", "\r\n")
|
| 1091 |
+
.Replace("<<CR>>", "\r");
|
| 1092 |
+
try
|
| 1093 |
+
{
|
| 1094 |
+
// Base64 encode to avoid ALL escaping issues
|
| 1095 |
+
var base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(content));
|
| 1096 |
+
var psCmd = $"[System.IO.File]::AppendAllText('{path}', [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('{base64}')))";
|
| 1097 |
+
ExecuteCommand($"powershell -Command \"{psCmd}\"");
|
| 1098 |
+
response = $"APPENDED:{path} ({content.Length} chars)";
|
| 1099 |
+
AppendOutput($"[Appended {content.Length} chars to {path}]", Color.Green);
|
| 1100 |
+
}
|
| 1101 |
+
catch (Exception ex)
|
| 1102 |
+
{
|
| 1103 |
+
response = $"ERROR: {ex.Message}";
|
| 1104 |
+
}
|
| 1105 |
+
}
|
| 1106 |
+
else
|
| 1107 |
+
{
|
| 1108 |
+
response = "ERROR: Use __APPEND_FILE__:path|content (use <<LF>> for newlines)";
|
| 1109 |
+
}
|
| 1110 |
+
}
|
| 1111 |
+
else if (command.StartsWith("__SPAWN__:"))
|
| 1112 |
+
{
|
| 1113 |
+
// Format: __SPAWN__:name:command
|
| 1114 |
+
var rest = command.Substring(10);
|
| 1115 |
+
var colonIdx = rest.IndexOf(':');
|
| 1116 |
+
if (colonIdx > 0)
|
| 1117 |
+
{
|
| 1118 |
+
var name = rest.Substring(0, colonIdx);
|
| 1119 |
+
var spawnCmd = rest.Substring(colonIdx + 1);
|
| 1120 |
+
// Run in background, capture PID via PowerShell
|
| 1121 |
+
var psCmd = $"$p = Start-Process -FilePath cmd -ArgumentList '/c {spawnCmd.Replace("'", "''")}' -PassThru -WindowStyle Hidden; $p.Id";
|
| 1122 |
+
var pidResult = ExecuteCommand($"powershell -Command \"{psCmd}\"");
|
| 1123 |
+
if (int.TryParse(pidResult.Trim().Split('\n').Last().Trim(), out int pid))
|
| 1124 |
+
{
|
| 1125 |
+
_spawnedProcesses[name] = pid;
|
| 1126 |
+
response = $"SPAWNED:{name}:PID={pid}";
|
| 1127 |
+
}
|
| 1128 |
+
else
|
| 1129 |
+
{
|
| 1130 |
+
response = $"SPAWN_STARTED:{name} (PID unknown)";
|
| 1131 |
+
}
|
| 1132 |
+
}
|
| 1133 |
+
else
|
| 1134 |
+
{
|
| 1135 |
+
response = "ERROR: Use __SPAWN__:name:command";
|
| 1136 |
+
}
|
| 1137 |
+
}
|
| 1138 |
+
else if (command == "__LIST_SPAWNED__")
|
| 1139 |
+
{
|
| 1140 |
+
if (_spawnedProcesses.Count == 0)
|
| 1141 |
+
{
|
| 1142 |
+
response = "NO_SPAWNED_PROCESSES";
|
| 1143 |
+
}
|
| 1144 |
+
else
|
| 1145 |
+
{
|
| 1146 |
+
var sb = new StringBuilder();
|
| 1147 |
+
foreach (var kvp in _spawnedProcesses)
|
| 1148 |
+
{
|
| 1149 |
+
// Check if still running
|
| 1150 |
+
var checkCmd = $"tasklist /FI \"PID eq {kvp.Value}\" /NH 2>nul | findstr {kvp.Value}";
|
| 1151 |
+
var checkResult = ExecuteCommand(checkCmd);
|
| 1152 |
+
var status = checkResult.Contains(kvp.Value.ToString()) ? "RUNNING" : "STOPPED";
|
| 1153 |
+
sb.AppendLine($"{kvp.Key}: PID={kvp.Value} ({status})");
|
| 1154 |
+
}
|
| 1155 |
+
response = sb.ToString().TrimEnd();
|
| 1156 |
+
}
|
| 1157 |
+
}
|
| 1158 |
+
else if (command == "__TAIL__")
|
| 1159 |
+
{
|
| 1160 |
+
// Get last 50 lines from the output textbox
|
| 1161 |
+
string text = "";
|
| 1162 |
+
this.Invoke(() => text = _outputBox.Text);
|
| 1163 |
+
var lines = text.Split('\n');
|
| 1164 |
+
var tail = lines.Skip(Math.Max(0, lines.Length - 50)).ToArray();
|
| 1165 |
+
response = string.Join("\n", tail);
|
| 1166 |
+
}
|
| 1167 |
+
else if (command.StartsWith("__KILL_SPAWNED__:"))
|
| 1168 |
+
{
|
| 1169 |
+
var name = command.Substring(17);
|
| 1170 |
+
if (_spawnedProcesses.TryGetValue(name, out int pid))
|
| 1171 |
+
{
|
| 1172 |
+
ExecuteCommand($"taskkill /PID {pid} /F /T 2>nul");
|
| 1173 |
+
_spawnedProcesses.Remove(name);
|
| 1174 |
+
response = $"KILLED:{name}:PID={pid}";
|
| 1175 |
+
}
|
| 1176 |
+
else
|
| 1177 |
+
{
|
| 1178 |
+
response = $"ERROR: No spawned process named '{name}'";
|
| 1179 |
+
}
|
| 1180 |
+
}
|
| 1181 |
else if (command.StartsWith("__PREFILL__:"))
|
| 1182 |
{
|
| 1183 |
// Format: __PREFILL__:host:port:user:password
|
|
|
|
| 1211 |
this.Invoke(() => Connect());
|
| 1212 |
response = "CONNECTING";
|
| 1213 |
}
|
| 1214 |
+
else if (_penLifted)
|
| 1215 |
+
{
|
| 1216 |
+
// Block command execution when pen is lifted
|
| 1217 |
+
AppendOutput($"> {command}", Color.Gray);
|
| 1218 |
+
AppendOutput("[BLOCKED - Pen lifted by user]", Color.Orange);
|
| 1219 |
+
response = "PEN_LIFTED: User has paused command execution. Use SshPenDown to resume, or wait for user to click 'Lift Pen' button again.";
|
| 1220 |
+
}
|
| 1221 |
else
|
| 1222 |
{
|
| 1223 |
response = ExecuteCommand(command);
|
| 1224 |
}
|
| 1225 |
// Encode newlines so they survive the single-line protocol
|
| 1226 |
+
response = response.Replace("\r\n", "<<CRLF>>").Replace("\n", "<<LF>>").Replace("\r", "<<CR>>");
|
| 1227 |
writer.WriteLine(response);
|
| 1228 |
}
|
| 1229 |
}
|