From 989eb69f1fb4e2b6eb5398dcb232026c82c4bfc9 Mon Sep 17 00:00:00 2001 From: Yannick Mauray Date: Thu, 28 Oct 2021 01:52:14 +0200 Subject: [PATCH] =?UTF-8?q?The=20Big=20Merge=C2=A9=C2=AE=EF=B8=8F=E2=84=A2?= =?UTF-8?q?!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integrated Mark's files, and it all works ! --- lib/src/model/vminfo.dart | 5 + lib/src/pages/manager.dart | 237 +++++++++++++++++++++++ lib/src/widgets/home_page/main_menu.dart | 11 +- pubspec.yaml | 1 + 4 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 lib/src/model/vminfo.dart create mode 100644 lib/src/pages/manager.dart diff --git a/lib/src/model/vminfo.dart b/lib/src/model/vminfo.dart new file mode 100644 index 0000000..0d3bbcd --- /dev/null +++ b/lib/src/model/vminfo.dart @@ -0,0 +1,5 @@ +/// Store info about a running vm, such as connection ports. +class VmInfo { + String? sshPort; + String? spicePort; +} diff --git a/lib/src/pages/manager.dart b/lib/src/pages/manager.dart new file mode 100644 index 0000000..e96ebb1 --- /dev/null +++ b/lib/src/pages/manager.dart @@ -0,0 +1,237 @@ +import 'dart:async'; +import 'dart:core'; +import 'package:flutter/material.dart'; +import 'package:path/path.dart' as path; +import 'package:file_picker/file_picker.dart'; +import 'dart:io'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:quickgui/src/model/vminfo.dart'; + +/// VM manager page. +/// Displays a list of available VMs, running state and connection info, +/// with buttons to start and stop VMs. +class Manager extends StatefulWidget { + const Manager({Key? key}) : super(key: key); + + @override + State createState() => _ManagerState(); +} + +class _ManagerState extends State { + List _currentVms = []; + Map _activeVms = {}; + final List _spicyVms = []; + Timer? refreshTimer; + static const String prefsWorkingDirectory = 'workingDirectory'; + + @override + void initState() { + super.initState(); + _getCurrentDirectory(); + 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. + } + + @override + void dispose() { + refreshTimer?.cancel(); + super.dispose(); + } + + void _saveCurrentDirectory() async { + final prefs = await SharedPreferences.getInstance(); + prefs.setString(prefsWorkingDirectory, Directory.current.path); + } + + void _getCurrentDirectory() async { + final prefs = await SharedPreferences.getInstance(); + if (prefs.containsKey(prefsWorkingDirectory)) { + setState(() { + final directory = prefs.getString(prefsWorkingDirectory); + if (directory != null) { + Directory.current = directory; + } + }); + } + } + + VmInfo _parseVmInfo(name) { + String shellScript = File(name + '/' + name + '.sh').readAsStringSync(); + RegExpMatch? sshMatch = RegExp('hostfwd=tcp::(\\d+?)-:22').firstMatch(shellScript); + RegExpMatch? spiceMatch = RegExp('-spice.+?port=(\\d+)').firstMatch(shellScript); + VmInfo info = VmInfo(); + if (sshMatch != null) { + info.sshPort = sshMatch.group(1); + } + if (spiceMatch != null) { + info.spicePort = spiceMatch.group(1); + } + return info; + } + + void _getVms(context) async { + List currentVms = []; + Map activeVms = {}; + + await for (var entity in Directory.current.list(recursive: false, followLinks: true)) { + if (entity.path.endsWith('.conf')) { + String name = path.basenameWithoutExtension(entity.path); + currentVms.add(name); + File pidFile = File(name + '/' + name + '.pid'); + if (pidFile.existsSync()) { + String pid = pidFile.readAsStringSync().trim(); + Directory procDir = Directory('/proc/' + pid); + if (procDir.existsSync()) { + if (_activeVms.containsKey(name)) { + activeVms[name] = _activeVms[name]!; + } else { + activeVms[name] = _parseVmInfo(name); + } + } + } + } + } + currentVms.sort(); + setState(() { + _currentVms = currentVms; + _activeVms = activeVms; + }); + } + + Widget _buildVmList() { + List _widgetList = []; + _widgetList.add(TextButton( + onPressed: () async { + String? result = await FilePicker.platform.getDirectoryPath(); + if (result != null) { + Directory.current = result; + _saveCurrentDirectory(); + _getVms(context); + } + }, + child: Text(Directory.current.path))); + List> rows = _currentVms.map((vm) { + return _buildRow(vm); + }).toList(); + for (var row in rows) { + _widgetList.addAll(row); + } + + return ListView( + padding: const EdgeInsets.all(16.0), + children: _widgetList, + ); + } + + List _buildRow(String currentVm) { + final bool active = _activeVms.containsKey(currentVm); + final bool spicy = _spicyVms.contains(currentVm); + String connectInfo = ''; + if (active) { + VmInfo vmInfo = _activeVms[currentVm]!; + if (vmInfo.sshPort != null) { + connectInfo += 'SSH port: ' + vmInfo.sshPort! + ' '; + } + if (vmInfo.spicePort != null) { + connectInfo += 'SPICE port: ' + vmInfo.spicePort! + ' '; + } + } + return [ + ListTile( + title: Text(currentVm), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: Icon(Icons.monitor, color: spicy ? Colors.red : null, semanticLabel: spicy ? 'Using SPICE display' : 'Click to use SPICE display'), + tooltip: spicy ? 'Using SPICE display' : 'Use SPICE display', + onPressed: () { + if (spicy) { + setState(() { + _spicyVms.remove(currentVm); + }); + } else { + setState(() { + _spicyVms.add(currentVm); + }); + } + }), + IconButton( + icon: Icon( + active ? Icons.play_arrow : Icons.play_arrow_outlined, + color: active ? Colors.green : null, + semanticLabel: active ? 'Running' : 'Run', + ), + onPressed: () async { + if (!active) { + Map activeVms = _activeVms; + List args = ['--vm', currentVm + '.conf']; + if (spicy) { + args.addAll(['--display', 'spice']); + } + await Process.start('quickemu', args); + VmInfo info = _parseVmInfo(currentVm); + activeVms[currentVm] = info; + setState(() { + _activeVms = activeVms; + }); + } + }), + IconButton( + icon: Icon( + active ? Icons.stop : Icons.stop_outlined, + color: active ? Colors.red : null, + semanticLabel: active ? 'Stop' : 'Not running', + ), + onPressed: () { + if (active) { + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: const Text('Stop The Virtual Machine?'), + content: Text('You are about to terminate the virtual machine $currentVm'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: const Text('OK'), + ), + ], + ), + ).then((result) { + result = result ?? false; + if (result) { + Process.run('killall', [currentVm]); + setState(() { + _activeVms.remove(currentVm); + }); + } + }); + } + }, + ), + ], + )), + if (connectInfo.isNotEmpty) + ListTile( + title: Text(connectInfo), + ), + const Divider() + ]; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Manager'), + ), + body: _buildVmList(), + ); + } +} diff --git a/lib/src/widgets/home_page/main_menu.dart b/lib/src/widgets/home_page/main_menu.dart index 1a80c09..27b8020 100644 --- a/lib/src/widgets/home_page/main_menu.dart +++ b/lib/src/widgets/home_page/main_menu.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:quickgui/src/pages/downloader_page.dart'; +import 'package:quickgui/src/pages/manager.dart'; import 'package:quickgui/src/widgets/home_page/home_page_button.dart'; class MainMenu extends StatelessWidget { @@ -16,7 +17,15 @@ class MainMenu extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ HomePageButton( - onPressed: () {}, + onPressed: () { + Navigator.of(context).push( + PageRouteBuilder( + fullscreenDialog: true, + pageBuilder: (context, animation1, animation2) => const Manager(), + transitionDuration: Duration.zero, + ), + ); + }, text: 'Manage existing machines', ), HomePageButton( diff --git a/pubspec.yaml b/pubspec.yaml index a462eb6..b4c43e5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,6 +41,7 @@ dependencies: quiver: ^3.0.1+1 tuple: ^2.0.0 file_picker: ^4.1.6 + shared_preferences: ^2.0.8 dev_dependencies: flutter_test: