diff --git a/lib/src/pages/manager.dart b/lib/src/pages/manager.dart index a9af8e0..6486c9b 100644 --- a/lib/src/pages/manager.dart +++ b/lib/src/pages/manager.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:core'; import 'package:flutter/material.dart'; import 'package:path/path.dart' as path; @@ -24,11 +25,14 @@ class _ManagerState extends State with PreferencesMixin { List _currentVms = []; Map _activeVms = {}; final List _spicyVms = []; + final List _sshVms = []; + String? _terminalEmulator; Timer? refreshTimer; @override void initState() { super.initState(); + _getTerminalEmulator(); getPreference(prefWorkingDirectory).then((pref) { setState(() { if (pref == null) { @@ -38,7 +42,6 @@ class _ManagerState extends State with PreferencesMixin { }); Future.delayed(Duration.zero, () => _getVms(context)); // Reload VM list when we enter the page. }); - refreshTimer = Timer.periodic(const Duration(seconds: 5), (Timer t) { _getVms(context); }); // Reload VM list every 5 seconds. @@ -50,6 +53,18 @@ class _ManagerState extends State with PreferencesMixin { super.dispose(); } + + void _getTerminalEmulator() async { + ProcessResult result = Process.runSync('x-terminal-emulator', ['-h']); + RegExp pattern = RegExp(r"usage:\s+([^\s]+)", multiLine: true, caseSensitive: false); + RegExpMatch? match = pattern.firstMatch(result.stdout); + if (match != null) { + setState(() { + _terminalEmulator = match.group(1); + }); + } + } + VmInfo _parseVmInfo(name) { VmInfo info = VmInfo(); List lines = File(name + '/' + name + '.ports').readAsLinesSync(); @@ -107,6 +122,18 @@ class _ManagerState extends State with PreferencesMixin { }); } + Future _detectSsh(int port) async { + bool isSSH = false; + try { + Socket socket = await Socket.connect('localhost', port); + isSSH = await socket.any((event) => utf8.decode(event).contains('SSH')); + socket.close(); + return isSSH; + } catch (exception) { + return false; + } + } + Widget _buildVmList() { List _widgetList = []; _widgetList.add( @@ -156,12 +183,24 @@ class _ManagerState extends State with PreferencesMixin { List _buildRow(String currentVm) { final bool active = _activeVms.containsKey(currentVm); final bool spicy = _spicyVms.contains(currentVm); + final bool sshy = _sshVms.contains(currentVm); VmInfo vmInfo = VmInfo(); String connectInfo = ''; if (active) { vmInfo = _activeVms[currentVm]!; - if (vmInfo.sshPort != null) { + if (vmInfo.sshPort != null && _terminalEmulator != null) { connectInfo += context.t('SSH port') + ': ' + vmInfo.sshPort! + ' '; + _detectSsh(int.parse(vmInfo.sshPort!)).then((sshRunning) { + if (sshRunning && !sshy) { + setState(() { + _sshVms.add(currentVm); + }); + } else if (!sshRunning && sshy) { + setState(() { + _sshVms.remove(currentVm); + }); + } + }); } if (vmInfo.spicePort != null) { connectInfo += context.t('SPICE port') + ': ' + vmInfo.spicePort! + ' '; @@ -259,6 +298,56 @@ class _ManagerState extends State with PreferencesMixin { Process.start('spicy', ['-p', vmInfo.spicePort!]); }, ), + TextButton( + child: Text('SSH'), + onPressed: !sshy ? null : () { + TextEditingController _usernameController = TextEditingController(); + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: Text('Launch SSH connection to $currentVm'), + content: TextField( + controller: _usernameController, + decoration: const InputDecoration(hintText: "SSH username"), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: const Text('Connect'), + ), + ], + ), + ).then((result) { + result = result ?? false; + if (result) { + List sshArgs = ['ssh', '-p', vmInfo.sshPort!, _usernameController.text + '@localhost']; + switch(_terminalEmulator) { + case 'gnome-terminal': + case 'mate-terminal': + sshArgs.insert(0, '--'); + break; + case 'xterm': + case 'konsole': + sshArgs.insert(0, '-e'); + break; + case 'terminator': + case 'xfce4-terminal': + sshArgs.insert(0, '-x'); + break; + case 'guake': + String command = sshArgs.join(' '); + sshArgs = ['-e', command]; + break; + } + Process.start(_terminalEmulator!, sshArgs); + } + }); + }, + ), ] ) ),