From 694854a89f2a8e7a2599c39a7adacc91596a97c1 Mon Sep 17 00:00:00 2001 From: PaperCube Date: Tue, 13 Feb 2024 20:39:22 +0000 Subject: [PATCH 1/3] Rework shell installer script writer s.t. fish shell doesn't rely on sftp --- lib/core/extension/ssh_client.dart | 39 ++++++++++++++++++++++++++++++ lib/data/model/app/shell_func.dart | 18 ++++++++------ lib/data/provider/server.dart | 32 ++++++++++++++++++++---- 3 files changed, 77 insertions(+), 12 deletions(-) diff --git a/lib/core/extension/ssh_client.dart b/lib/core/extension/ssh_client.dart index 703cc8b0..c4959fad 100644 --- a/lib/core/extension/ssh_client.dart +++ b/lib/core/extension/ssh_client.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:typed_data'; import 'package:dartssh2/dartssh2.dart'; import 'package:flutter/foundation.dart'; @@ -85,4 +86,42 @@ extension SSHClientX on SSHClient { stdin: stdin, ); } + + Future runWithSessionAction( + String command, { + bool runInPty = false, + bool stdout = true, + bool stderr = true, + Map? environment, + Future Function(SSHSession)? action, + }) async { + final session = await execute( + command, + pty: runInPty ? const SSHPtyConfig() : null, + environment: environment, + ); + + final result = BytesBuilder(copy: false); + final stdoutDone = Completer(); + final stderrDone = Completer(); + + session.stdout.listen( + stdout ? result.add : (_) {}, + onDone: stdoutDone.complete, + onError: stderrDone.completeError, + ); + + session.stderr.listen( + stderr ? result.add : (_) {}, + onDone: stderrDone.complete, + onError: stderrDone.completeError, + ); + + if (action != null) await action(session); + + await stdoutDone.future; + await stderrDone.future; + + return result.takeBytes(); + } } diff --git a/lib/data/model/app/shell_func.dart b/lib/data/model/app/shell_func.dart index b4636e1e..297a1f68 100644 --- a/lib/data/model/app/shell_func.dart +++ b/lib/data/model/app/shell_func.dart @@ -33,13 +33,17 @@ enum ShellFunc { /// Issue #168 /// Use `sh` for compatibility - static final installShellCmd = """ -mkdir -p $_homeVar/$_srvBoxDir -cat << 'EOF' > $_installShellPath -${ShellFunc.allScript} -EOF -chmod +x $_installShellPath -"""; + // static final installShellCmd = """ +// mkdir -p $_homeVar/$_srvBoxDir +// cat << 'EOF' > $_installShellPath +// ${ShellFunc.allScript} +// EOF +// chmod +x $_installShellPath +// """; + + static const installerMkdirs = "mkdir -p $_homeVar/$_srvBoxDir"; + static const installerShellWriter = "cat > $_installShellPath"; + static const installerPermissionModifier = "chmod +x $_installShellPath"; String get flag { switch (this) { diff --git a/lib/data/provider/server.dart b/lib/data/provider/server.dart index 8f195304..61edfba0 100644 --- a/lib/data/provider/server.dart +++ b/lib/data/provider/server.dart @@ -1,9 +1,12 @@ import 'dart:async'; import 'dart:io'; +import 'dart:typed_data'; +import 'dart:convert'; import 'package:computer/computer.dart'; import 'package:dartssh2/dartssh2.dart'; import 'package:flutter/material.dart'; +import 'package:toolbox/core/extension/ssh_client.dart'; import 'package:toolbox/core/utils/platform/path.dart'; import 'package:toolbox/data/model/app/shell_func.dart'; import 'package:toolbox/data/model/server/system.dart'; @@ -256,6 +259,29 @@ class ServerProvider extends ChangeNotifier { notifyListeners(); } + Future _writeInstallerScript(Server s) async { + void ensure(String? writeResult) { + if (writeResult == null || writeResult.isNotEmpty) { + throw Exception("Failed to write installer script: $writeResult"); + } + } + + final client = s.client; + if (client == null) { + throw Exception("Invalid state: s.client cannot be null"); + } + + ensure(await client.run(ShellFunc.installerMkdirs).string); + + ensure(await client.runWithSessionAction(ShellFunc.installerShellWriter, + action: (session) async { + session.stdin.add(utf8.encode(ShellFunc.allScript)); + }) + .string); + + ensure(await client.run(ShellFunc.installerPermissionModifier).string); + } + Future _getData(ServerPrivateInfo spi) async { final sid = spi.id; final s = _servers[sid]; @@ -304,11 +330,7 @@ class ServerProvider extends ChangeNotifier { // Write script to server // by ssh try { - final writeResult = - await s.client?.run(ShellFunc.installShellCmd).string; - if (writeResult == null || writeResult.isNotEmpty) { - throw Exception('$writeResult'); - } + await _writeInstallerScript(s); } on SSHAuthAbortError catch (e) { TryLimiter.inc(sid); s.status.err = e.toString(); From 71e6e18e0982a48643832f897b92bb8f7ba8f0a6 Mon Sep 17 00:00:00 2001 From: PaperCube Date: Tue, 13 Feb 2024 22:15:00 +0000 Subject: [PATCH 2/3] Fix: bad detection of docker installation on fish --- lib/data/model/container/version.dart | 2 +- lib/data/provider/container.dart | 35 +++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/lib/data/model/container/version.dart b/lib/data/model/container/version.dart index 5407e343..98328a66 100644 --- a/lib/data/model/container/version.dart +++ b/lib/data/model/container/version.dart @@ -49,7 +49,7 @@ class ContainerdClient { factory ContainerdClient.fromJson(Map json) => ContainerdClient( - apiVersion: json["APIVersion"], + apiVersion: json["ApiVersion"], // should be ApiVersion? version: json["Version"], goVersion: json["GoVersion"], gitCommit: json["GitCommit"], diff --git a/lib/data/provider/container.dart b/lib/data/provider/container.dart index 8cceae0f..f6b1a251 100644 --- a/lib/data/provider/container.dart +++ b/lib/data/provider/container.dart @@ -12,7 +12,8 @@ import 'package:toolbox/data/model/container/version.dart'; import 'package:toolbox/data/res/logger.dart'; import 'package:toolbox/data/res/store.dart'; -final _dockerNotFound = RegExp(r'command not found|Unknown command'); +final _dockerNotFound = + RegExp(r"command not found|Unknown command|Command '\w+' not found"); class ContainerProvider extends ChangeNotifier { SSHClient? client; @@ -43,15 +44,44 @@ class ContainerProvider extends ChangeNotifier { await refresh(); } + Future _checkDockerInstalled(SSHClient client) async { + final session = await client.execute("docker"); + await session.done; + // print('docker code: ${session.exitCode}'); + return session.exitCode == 0; + } + + String _removeSudoPrompts(String value) { + final regex = RegExp(r"\[sudo\] password for \w+:"); + if (value.startsWith(regex)){ + return value.replaceFirstMapped(regex, (match) => ""); + } + return value; + } + Future refresh() async { var raw = ''; + var rawErr = ''; + print('exec: ${_wrap(ContainerCmdType.execAll(type))}'); + await client?.execWithPwd( _wrap(ContainerCmdType.execAll(type)), context: context, onStdout: (data, _) => raw = '$raw$data', + onStderr: (data, _) => raw = '$rawErr$data', ); - if (raw.contains(_dockerNotFound)) { + raw = _removeSudoPrompts(raw); + rawErr = _removeSudoPrompts(rawErr); + + print('result raw [$raw, $rawErr]'); + + final dockerInstalled = await _checkDockerInstalled(client!); + // print("docker installed = $dockerInstalled"); + + if (!dockerInstalled || + raw.contains(_dockerNotFound) || + rawErr.contains(_dockerNotFound)) { error = ContainerErr(type: ContainerErrType.notInstalled); notifyListeners(); return; @@ -71,6 +101,7 @@ class ContainerProvider extends ChangeNotifier { // Parse docker version final verRaw = ContainerCmdType.version.find(segments); + print('version raw = $verRaw\n'); try { final containerVersion = Containerd.fromRawJson(verRaw); version = containerVersion.client.version; From 1f69f88b817422a1b9661367e1ce73d407753dbd Mon Sep 17 00:00:00 2001 From: PaperCube Date: Wed, 14 Feb 2024 15:02:59 +0000 Subject: [PATCH 3/3] Docker: Adaptive sudo --- lib/core/extension/ssh_client.dart | 1 - lib/data/model/container/version.dart | 16 ++++++------ lib/data/provider/container.dart | 35 ++++++++++++++++++--------- lib/data/provider/server.dart | 1 - 4 files changed, 32 insertions(+), 21 deletions(-) diff --git a/lib/core/extension/ssh_client.dart b/lib/core/extension/ssh_client.dart index c4959fad..d963096d 100644 --- a/lib/core/extension/ssh_client.dart +++ b/lib/core/extension/ssh_client.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:dartssh2/dartssh2.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:toolbox/core/extension/context/dialog.dart'; import 'package:toolbox/core/extension/stringx.dart'; diff --git a/lib/data/model/container/version.dart b/lib/data/model/container/version.dart index 98328a66..d199d913 100644 --- a/lib/data/model/container/version.dart +++ b/lib/data/model/container/version.dart @@ -26,9 +26,9 @@ class ContainerdClient { final String version; final String goVersion; final String gitCommit; - final String builtTime; - final int built; - final String osArch; + final String? builtTime; + final int? built; // more fields should be marked nullable + final String? osArch; final String os; ContainerdClient({ @@ -49,22 +49,22 @@ class ContainerdClient { factory ContainerdClient.fromJson(Map json) => ContainerdClient( - apiVersion: json["ApiVersion"], // should be ApiVersion? + apiVersion: json["ApiVersion"], version: json["Version"], goVersion: json["GoVersion"], gitCommit: json["GitCommit"], - builtTime: json["BuiltTime"], - built: json["Built"], + builtTime: json["BuildTime"], + built: json["Built"], // should be Build? osArch: json["OsArch"], os: json["Os"], ); Map toJson() => { - "APIVersion": apiVersion, + "ApiVersion": apiVersion, "Version": version, "GoVersion": goVersion, "GitCommit": gitCommit, - "BuiltTime": builtTime, + "BuildTime": builtTime, "Built": built, "OsArch": osArch, "Os": os, diff --git a/lib/data/provider/container.dart b/lib/data/provider/container.dart index f6b1a251..7514aa38 100644 --- a/lib/data/provider/container.dart +++ b/lib/data/provider/container.dart @@ -11,6 +11,7 @@ import 'package:toolbox/data/model/container/type.dart'; import 'package:toolbox/data/model/container/version.dart'; import 'package:toolbox/data/res/logger.dart'; import 'package:toolbox/data/res/store.dart'; +import 'package:toolbox/core/extension/uint8list.dart'; final _dockerNotFound = RegExp(r"command not found|Unknown command|Command '\w+' not found"); @@ -47,25 +48,36 @@ class ContainerProvider extends ChangeNotifier { Future _checkDockerInstalled(SSHClient client) async { final session = await client.execute("docker"); await session.done; - // print('docker code: ${session.exitCode}'); + // debugPrint('docker code: ${session.exitCode}'); return session.exitCode == 0; } String _removeSudoPrompts(String value) { final regex = RegExp(r"\[sudo\] password for \w+:"); - if (value.startsWith(regex)){ + if (value.startsWith(regex)) { return value.replaceFirstMapped(regex, (match) => ""); } return value; } + Future _requiresSudo() async { + final psResult = await client?.run(_wrap(ContainerCmdType.ps.exec(type))); + if (psResult == null) return true; + if (psResult.string.toLowerCase().contains("permission denied")) { + return true; + } + return false; + } + Future refresh() async { var raw = ''; var rawErr = ''; - print('exec: ${_wrap(ContainerCmdType.execAll(type))}'); + debugPrint('exec: ${_wrap(ContainerCmdType.execAll(type))}'); + + final sudo = await _requiresSudo(); await client?.execWithPwd( - _wrap(ContainerCmdType.execAll(type)), + _wrap(ContainerCmdType.execAll(type, sudo: sudo)), context: context, onStdout: (data, _) => raw = '$raw$data', onStderr: (data, _) => raw = '$rawErr$data', @@ -74,10 +86,10 @@ class ContainerProvider extends ChangeNotifier { raw = _removeSudoPrompts(raw); rawErr = _removeSudoPrompts(rawErr); - print('result raw [$raw, $rawErr]'); + debugPrint('result raw [$raw, $rawErr]'); final dockerInstalled = await _checkDockerInstalled(client!); - // print("docker installed = $dockerInstalled"); + // debugPrint("docker installed = $dockerInstalled"); if (!dockerInstalled || raw.contains(_dockerNotFound) || @@ -101,7 +113,7 @@ class ContainerProvider extends ChangeNotifier { // Parse docker version final verRaw = ContainerCmdType.version.find(segments); - print('version raw = $verRaw\n'); + debugPrint('version raw = $verRaw\n'); try { final containerVersion = Containerd.fromRawJson(verRaw); version = containerVersion.client.version; @@ -228,8 +240,8 @@ enum ContainerCmdType { images, ; - String exec(ContainerType type) { - final prefix = type.name; + String exec(ContainerType type, {bool sudo = false}) { + final prefix = sudo ? 'sudo -S ${type.name}' : type.name; return switch (this) { ContainerCmdType.version => '$prefix version $_jsonFmt', ContainerCmdType.ps => '$prefix ps -a $_jsonFmt', @@ -238,6 +250,7 @@ enum ContainerCmdType { }; } - static String execAll(ContainerType type) => - values.map((e) => e.exec(type)).join(' && echo $seperator && '); + static String execAll(ContainerType type, {bool sudo = false}) => values + .map((e) => e.exec(type, sudo: sudo)) + .join(' && echo $seperator && '); } diff --git a/lib/data/provider/server.dart b/lib/data/provider/server.dart index 61edfba0..ff5c55f4 100644 --- a/lib/data/provider/server.dart +++ b/lib/data/provider/server.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:io'; -import 'dart:typed_data'; import 'dart:convert'; import 'package:computer/computer.dart';