diff --git a/lib/core/extension/ssh_client.dart b/lib/core/extension/ssh_client.dart index 703cc8b0..d963096d 100644 --- a/lib/core/extension/ssh_client.dart +++ b/lib/core/extension/ssh_client.dart @@ -1,7 +1,7 @@ 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'; @@ -85,4 +85,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/container.dart b/lib/data/provider/container.dart index 2cd3de27..0ee4c30b 100644 --- a/lib/data/provider/container.dart +++ b/lib/data/provider/container.dart @@ -11,8 +11,10 @@ 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'); +final _dockerNotFound = + RegExp(r"command not found|Unknown command|Command '\w+' not found"); class ContainerProvider extends ChangeNotifier { SSHClient? client; @@ -43,15 +45,55 @@ class ContainerProvider extends ChangeNotifier { await refresh(); } + Future _checkDockerInstalled(SSHClient client) async { + final session = await client.execute("docker"); + await session.done; + // 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)) { + 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 = ''; + 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', ); - if (raw.contains(_dockerNotFound)) { + raw = _removeSudoPrompts(raw); + rawErr = _removeSudoPrompts(rawErr); + + debugPrint('result raw [$raw, $rawErr]'); + + final dockerInstalled = await _checkDockerInstalled(client!); + // debugPrint("docker installed = $dockerInstalled"); + + if (!dockerInstalled || + raw.contains(_dockerNotFound) || + rawErr.contains(_dockerNotFound)) { error = ContainerErr(type: ContainerErrType.notInstalled); notifyListeners(); return; @@ -71,6 +113,7 @@ class ContainerProvider extends ChangeNotifier { // Parse docker version final verRaw = ContainerCmdType.version.find(segments); + debugPrint('version raw = $verRaw\n'); try { final containerVersion = Containerd.fromRawJson(verRaw); version = containerVersion.client.version; @@ -196,8 +239,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', @@ -206,6 +249,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 8f195304..ff5c55f4 100644 --- a/lib/data/provider/server.dart +++ b/lib/data/provider/server.dart @@ -1,9 +1,11 @@ import 'dart:async'; import 'dart:io'; +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 +258,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 +329,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();