From b7f3fff18c8eb90e0038bb73374a74cc2d3df7a4 Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 13 Apr 2022 23:26:17 +0200 Subject: [PATCH 1/7] Added LXC repo, stdin & stdout listener --- lib/components/api.dart | 91 +++++++++++++++++++++++++++++++--- lib/components/constants.dart | 2 +- lib/dialogs/create_dialog.dart | 4 +- 3 files changed, 86 insertions(+), 11 deletions(-) diff --git a/lib/components/api.dart b/lib/components/api.dart index 49f7cfe..cc87134 100644 --- a/lib/components/api.dart +++ b/lib/components/api.dart @@ -1,5 +1,6 @@ +import 'dart:async'; import 'dart:io'; -import 'dart:convert' show utf8, json; +import 'dart:convert' show Utf8Decoder, json, utf8; import 'package:dio/dio.dart'; import 'constants.dart'; import 'helpers.dart'; @@ -247,6 +248,50 @@ class WSLApi { return results.stdout; } + List resultQueue = []; + + /// Get the current cached output + /// @return String + String getCurrentOutput() { + String tmp = resultQueue.join('\n'); + resultQueue = []; + return tmp; + } + + /// Executes a command list in a WSL distro + /// @param distribution: String + /// @param cmd: List + /// @return Future> + Future> execCmds( + String distribution, List cmds, Function(String) callback) async { + List processes = []; + Process result = await Process.start( + 'wsl', ['-d', distribution, '-u', 'root'], + mode: ProcessStartMode.detachedWithStdio); + + Timer currentWaiter = Timer(const Duration(seconds: 30), () { + result.kill(); + }); + + result.stdout + .cast>() + .transform(const Utf8Decoder()) + .listen((String line) { + resultQueue.add(line); + callback(line); + currentWaiter.cancel(); + // No new output within the last 30 seconds + currentWaiter = Timer(const Duration(seconds: 30), () { + result.kill(); + }); + }); + + for (var cmd in cmds) { + result.stdin.writeln(cmd); + } + return processes; + } + /// Executes a command in a WSL distro. passwd will open a shell /// @param distribution: String /// @param cmd: List @@ -307,8 +352,8 @@ class WSLApi { /// @param installPath: String distro name or tar file /// @param filename: String /// @return Future - Future create( - String distribution, String filename, String installPath) async { + Future create(String distribution, String filename, + String installPath, Function(String) status) async { if (installPath == '') { installPath = defaultPath + distribution; } @@ -320,10 +365,15 @@ class WSLApi { !(await File(downloadPath).exists())) { String url = distroRootfsLinks[filename]!; // Download file - Dio dio = Dio(); - Response response = await dio.download(url, downloadPath); - if (response.statusCode != 200) { - return response; + try { + Dio dio = Dio(); + Response response = await dio.download(url, downloadPath, + onReceiveProgress: (int count, int total) { + status( + 'Step 1: Downloading distro: ${(count / total * 100).toStringAsFixed(0)}%'); + }); + } catch (error) { + status('Error downloading: $error'); } } @@ -401,7 +451,32 @@ class WSLApi { nameStarted = true; } });*/ - List list = distroRootfsLinks.keys.toList(); + // Get even more distros + String repoLink = + "http://ftp.halifax.rwth-aachen.de/turnkeylinux/images/proxmox/"; + await Dio().get(repoLink).then((value) => { + value.data.split('\n').forEach((line) { + if (line.contains('tar.gz"') && + line.contains('href=') && + (line.contains('debian-10') || line.contains('debian-11'))) { + String name = line + .split('href="')[1] + .split('"')[0] + .toString() + .replaceAll('.tar.gz', '') + .replaceAll('1_amd64', '') + .replaceAll(RegExp(r'-|_'), ' ') + .replaceAllMapped(RegExp(r' .|^.'), + (Match m) => m[0].toString().toUpperCase()); + distroRootfsLinks.addAll({ + name: + repoLink + line.split('href="')[1].split('"')[0].toString() + }); + } + }) + }); + List list = []; + list.addAll(distroRootfsLinks.keys); return list; } diff --git a/lib/components/constants.dart b/lib/components/constants.dart index c1d1e0f..291aa02 100644 --- a/lib/components/constants.dart +++ b/lib/components/constants.dart @@ -11,7 +11,7 @@ const String motdUrl = 'https://raw.githubusercontent.com/bostrot/wsl2-distro-manager/main/motd.json'; // https://docs.microsoft.com/en-us/windows/wsl/install-on-server -const Map distroRootfsLinks = { +Map distroRootfsLinks = { 'Ubuntu 21.04': 'https://cloud-images.ubuntu.com/releases/hirsute/release/ubuntu-21.04-server-cloudimg-amd64-wsl.rootfs.tar.gz', 'Ubuntu 20.04': diff --git a/lib/dialogs/create_dialog.dart b/lib/dialogs/create_dialog.dart index 93ffbc2..3561581 100644 --- a/lib/dialogs/create_dialog.dart +++ b/lib/dialogs/create_dialog.dart @@ -156,8 +156,8 @@ createDialog(context, Function(String, {bool loading}) statusMsg) { location = prefs.getString("SaveLocation") ?? defaultPath; location += '/' + name; } - var result = - await api.create(name, autoSuggestBox.text, location); + var result = await api.create(name, autoSuggestBox.text, + location, (String msg) => statusMsg(msg)); if (result.exitCode != 0) { statusMsg(result.stdout); } else { From 658f4faf055a3777b40e72a6d965fb25d83f2c93 Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 14 Apr 2022 16:10:26 +0200 Subject: [PATCH 2/7] Updated package version --- pubspec.lock | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 4fb5534..4876419 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -182,7 +182,7 @@ packages: name: fluent_ui url: "https://pub.dartlang.org" source: hosted - version: "3.10.0" + version: "3.10.2" flutter: dependency: "direct main" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index c9a32ee..752d6b0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,7 +34,7 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 desktop_window: ^0.4.0 - fluent_ui: ^3.10.0 + fluent_ui: ^3.10.2 system_theme: ^1.0.1 file_picker: ^4.5.1 url_launcher: ^6.0.20 From cab905edcccaecc830c5252d69d541ad84334a86 Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 14 Apr 2022 16:11:51 +0200 Subject: [PATCH 3/7] Updated version --- lib/components/constants.dart | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/components/constants.dart b/lib/components/constants.dart index 291aa02..98a31f7 100644 --- a/lib/components/constants.dart +++ b/lib/components/constants.dart @@ -1,5 +1,5 @@ // TODO: Update on release -const String currentVersion = "v0.9.1"; +const String currentVersion = "v1.0.0"; const String windowsStoreUrl = "https://www.microsoft.com/store/" "productId/9NWS9K95NMJB"; const String defaultPath = 'C:\\WSL2-Distros\\'; diff --git a/pubspec.yaml b/pubspec.yaml index 752d6b0..5e4c0da 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.9.1 # Current version +version: 1.0.0 # Current version environment: sdk: ">=2.12.0 <3.0.0" From 52e990b9cae8b3039f921d717b196dc728b3aad4 Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 14 Apr 2022 16:12:18 +0200 Subject: [PATCH 4/7] Adjusted box size --- lib/dialogs/base_dialog.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/dialogs/base_dialog.dart b/lib/dialogs/base_dialog.dart index a120217..b5d7195 100644 --- a/lib/dialogs/base_dialog.dart +++ b/lib/dialogs/base_dialog.dart @@ -21,6 +21,7 @@ dialog({ context: context, builder: (context) { return ContentDialog( + constraints: const BoxConstraints(maxHeight: 300.0, maxWidth: 400.0), title: centerText ? Center(child: Text(title)) : Text(title), content: Column( children: [ From 2d18d844f4eff6093311e6210c5f4a3be4e28c49 Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 14 Apr 2022 16:13:06 +0200 Subject: [PATCH 5/7] Added onDone and onMsg to execCmds function --- lib/components/api.dart | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/components/api.dart b/lib/components/api.dart index cc87134..639d08e 100644 --- a/lib/components/api.dart +++ b/lib/components/api.dart @@ -263,14 +263,19 @@ class WSLApi { /// @param cmd: List /// @return Future> Future> execCmds( - String distribution, List cmds, Function(String) callback) async { + String distribution, + List cmds, { + required Function(String) onMsg, + required Function onDone, + }) async { List processes = []; Process result = await Process.start( 'wsl', ['-d', distribution, '-u', 'root'], mode: ProcessStartMode.detachedWithStdio); - Timer currentWaiter = Timer(const Duration(seconds: 30), () { + Timer currentWaiter = Timer(const Duration(seconds: 15), () { result.kill(); + onDone(); }); result.stdout @@ -278,11 +283,12 @@ class WSLApi { .transform(const Utf8Decoder()) .listen((String line) { resultQueue.add(line); - callback(line); + onMsg(line); currentWaiter.cancel(); // No new output within the last 30 seconds - currentWaiter = Timer(const Duration(seconds: 30), () { + currentWaiter = Timer(const Duration(seconds: 15), () { result.kill(); + onDone(); }); }); @@ -367,10 +373,10 @@ class WSLApi { // Download file try { Dio dio = Dio(); - Response response = await dio.download(url, downloadPath, + await dio.download(url, downloadPath, onReceiveProgress: (int count, int total) { - status( - 'Step 1: Downloading distro: ${(count / total * 100).toStringAsFixed(0)}%'); + status('Step 1: Downloading distro: ' + '${(count / total * 100).toStringAsFixed(0)}%'); }); } catch (error) { status('Error downloading: $error'); From a02a24d2474d542d80fa6f5470933e6023a1e398 Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 14 Apr 2022 16:18:07 +0200 Subject: [PATCH 6/7] Stateful widget, turnkey detection, fake systemctl --- lib/dialogs/create_dialog.dart | 314 +++++++++++++++++++++------------ 1 file changed, 199 insertions(+), 115 deletions(-) diff --git a/lib/dialogs/create_dialog.dart b/lib/dialogs/create_dialog.dart index 3561581..73511a8 100644 --- a/lib/dialogs/create_dialog.dart +++ b/lib/dialogs/create_dialog.dart @@ -21,120 +21,15 @@ createDialog(context, Function(String, {bool loading}) statusMsg) { context: context, builder: (context) { return ContentDialog( + constraints: const BoxConstraints(maxHeight: 450.0, maxWidth: 400.0), title: const Text('Create a new distro'), - content: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - height: 10.0, - ), - const Text( - 'Name:', - ), - Container( - height: 5.0, - ), - Tooltip( - message: 'The name of your new WSL instance', - child: TextBox( - controller: nameController, - placeholder: 'Name', - suffix: IconButton( - icon: const Icon(FluentIcons.chrome_close, size: 15.0), - onPressed: () { - nameController.clear(); - }, - ), - ), - ), - Container( - height: 10.0, - ), - const Text( - 'Path to rootfs or distro name:', - ), - Container( - height: 5.0, - ), - Tooltip( - message: - 'Either use one of the pre-defined Distros or a file path to a rootfs', - child: FutureBuilder>( - future: api.getDownloadable(), - builder: (context, snapshot) { - List list = []; - if (snapshot.hasData) { - list = snapshot.data ?? []; - } else if (snapshot.hasError) {} - return AutoSuggestBox( - placeholder: 'Distro name or path to rootfs', - controller: autoSuggestBox, - items: list, - trailingIcon: IconButton( - icon: const Icon(FluentIcons.open_folder_horizontal, - size: 15.0), - onPressed: () async { - FilePickerResult? result = - await FilePicker.platform.pickFiles( - type: FileType.custom, - allowedExtensions: ['*'], - ); - - if (result != null) { - autoSuggestBox.text = result.files.single.path!; - } else { - // User canceled the picker - } - }, - ), - ); - }), - ), - Container( - height: 10.0, - ), - const Text( - 'Save location:', - ), - Container( - height: 5.0, - ), - Tooltip( - message: '(Optional) Path where to save the new instance', - child: TextBox( - controller: locationController, - placeholder: 'Save location (optional)', - suffix: IconButton( - icon: const Icon(FluentIcons.open_folder_horizontal, - size: 15.0), - onPressed: () async { - String? path = await FilePicker.platform.getDirectoryPath(); - if (path != null) { - locationController.text = path; - } else { - // User canceled the picker - } - }, - ), - ), - ), - Container( - height: 10.0, - ), - const Text( - 'Create default user (only on Debian/Ubuntu):', - ), - Container( - height: 5.0, - ), - Tooltip( - message: '(Optional) Username', - child: TextBox( - controller: userController, - placeholder: '(Optional) User', - ), - ), - ], + content: SingleChildScrollView( + child: CreateWidget( + nameController: nameController, + api: api, + autoSuggestBox: autoSuggestBox, + locationController: locationController, + userController: userController), ), actions: [ Button( @@ -189,8 +84,23 @@ createDialog(context, Function(String, {bool loading}) statusMsg) { 'WARNING: Created instance but failed to create user'); } } else { - Navigator.pop(context); - statusMsg('DONE: creating instance'); + if (Navigator.canPop(context)) { + Navigator.pop(context); + } + statusMsg('Installing fake systemd ...'); + // Install fake systemctl + if (autoSuggestBox.text.contains('Turnkey')) { + WSLApi().execCmds( + name, + [ + 'wget https://raw.githubusercontent.com/bostrot/' + 'fake-systemd/master/systemctl -O /usr/bin/systemctl', + 'chmod +x /usr/bin/systemctl', + '/usr/bin/systemctl', + ], + onMsg: (output) => print(output), + onDone: () => statusMsg('DONE: creating instance')); + } } // Save distro label prefs.setString('DistroName_' + name, label); @@ -209,3 +119,177 @@ createDialog(context, Function(String, {bool loading}) statusMsg) { }, ); } + +class CreateWidget extends StatefulWidget { + const CreateWidget({ + Key? key, + required this.nameController, + required this.api, + required this.autoSuggestBox, + required this.locationController, + required this.userController, + }) : super(key: key); + + final TextEditingController nameController; + final WSLApi api; + final TextEditingController autoSuggestBox; + final TextEditingController locationController; + final TextEditingController userController; + + @override + State createState() => _CreateWidgetState(); +} + +class _CreateWidgetState extends State { + bool turnkey = false; + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 10.0, + ), + const Text( + 'Name:', + ), + Container( + height: 5.0, + ), + Tooltip( + message: 'The name of your new WSL instance', + child: TextBox( + controller: widget.nameController, + placeholder: 'Name', + suffix: IconButton( + icon: const Icon(FluentIcons.chrome_close, size: 15.0), + onPressed: () { + widget.nameController.clear(); + }, + ), + ), + ), + Container( + height: 10.0, + ), + const Text( + 'Path to rootfs or distro name:', + ), + Container( + height: 5.0, + ), + Tooltip( + message: + 'Either use one of the pre-defined Distros or a file path to a ' + 'rootfs', + child: FutureBuilder>( + future: widget.api.getDownloadable(), + builder: (context, snapshot) { + List list = []; + if (snapshot.hasData) { + list = snapshot.data ?? []; + } else if (snapshot.hasError) {} + return AutoSuggestBox( + placeholder: 'Distro name or path to rootfs', + controller: widget.autoSuggestBox, + items: list, + onChanged: (String value, TextChangedReason reason) { + if (value.contains('Turnkey')) { + if (!turnkey) { + setState(() { + turnkey = true; + }); + } + } else { + if (turnkey) { + setState(() { + turnkey = false; + }); + } + } + }, + trailingIcon: IconButton( + icon: const Icon(FluentIcons.open_folder_horizontal, + size: 15.0), + onPressed: () async { + FilePickerResult? result = + await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['*'], + ); + + if (result != null) { + widget.autoSuggestBox.text = result.files.single.path!; + } else { + // User canceled the picker + } + }, + ), + ); + }), + ), + Container( + height: 10.0, + ), + const Text( + 'Save location:', + ), + Container( + height: 5.0, + ), + Tooltip( + message: '(Optional) Path where to save the new instance', + child: TextBox( + controller: widget.locationController, + placeholder: 'Save location (optional)', + suffix: IconButton( + icon: const Icon(FluentIcons.open_folder_horizontal, size: 15.0), + onPressed: () async { + String? path = await FilePicker.platform.getDirectoryPath(); + if (path != null) { + widget.locationController.text = path; + } else { + // User canceled the picker + } + }, + ), + ), + ), + Container( + height: 10.0, + ), + turnkey + ? const Text( + 'Warning: You selected a turnkey container. [Experimental]\n' + 'As most of them use systemd and WSL currently does not ' + 'support systemd out of the box it will ' + 'be replaced with a fork of fake_systemd. This will start the ' + 'applications not on init but on console openings for more info ' + 'check the GitHub project\'s README.\n' + 'To access the service you can use "ip a | grep inet" to find ' + 'the ip and then navigate to WSL-IP:PORT e.g. in your browser.', + style: TextStyle(fontStyle: FontStyle.italic)) + : Container(), + !turnkey + ? const Text( + 'Create default user (only on Debian/Ubuntu):', + ) + : Container(), + !turnkey + ? Container( + height: 5.0, + ) + : Container(), + !turnkey + ? Tooltip( + message: '(Optional) Username', + child: TextBox( + controller: widget.userController, + placeholder: '(Optional) User', + ), + ) + : Container(), + ], + ); + } +} From b3ee45ee87a9f6e3819b8cce312a4bc6d25028d1 Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 14 Apr 2022 16:33:36 +0200 Subject: [PATCH 7/7] Hotfix for #22 --- lib/components/list.dart | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/components/list.dart b/lib/components/list.dart index 541950d..dfb585c 100644 --- a/lib/components/list.dart +++ b/lib/components/list.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:wsl2distromanager/components/api.dart'; import 'package:fluent_ui/fluent_ui.dart'; import 'package:wsl2distromanager/dialogs/dialogs.dart'; @@ -28,9 +30,17 @@ class _DistroListState extends State { @override void initState() { initPrefs(); + reloadEvery5Seconds(); super.initState(); } + void reloadEvery5Seconds() async { + for (;;) { + await Future.delayed(const Duration(seconds: 5)); + setState(() {}); + } + } + @override Widget build(BuildContext context) { return distroList(widget.api, widget.statusMsg, hover); @@ -43,6 +53,7 @@ FutureBuilder distroList(WSLApi api, return FutureBuilder( future: api.list(), builder: (context, snapshot) { + // Update every 20 seconds if (snapshot.hasData) { List newList = []; List list = snapshot.data?.all ?? [];