mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 07:14:28 +01:00
refactor: SSHClientX.exec
This commit is contained in:
@@ -13,65 +13,66 @@ typedef OnStdin = void Function(SSHSession session);
|
||||
typedef PwdRequestFunc = Future<String?> Function(String? user);
|
||||
|
||||
extension SSHClientX on SSHClient {
|
||||
/// TODO: delete [exec]
|
||||
Future<SSHSession> exec(
|
||||
String cmd, {
|
||||
OnStdout? onStderr,
|
||||
Future<(SSHSession, String)> exec(
|
||||
OnStdin onStdin, {
|
||||
String? entry,
|
||||
SSHPtyConfig? pty,
|
||||
OnStdout? onStdout,
|
||||
OnStdin? stdin,
|
||||
bool redirectToBash = false, // not working yet. do not use
|
||||
OnStdout? onStderr,
|
||||
bool stdout = true,
|
||||
bool stderr = true,
|
||||
Map<String, String>? env,
|
||||
}) async {
|
||||
final session = await execute(redirectToBash ? 'head -1 | bash' : cmd);
|
||||
|
||||
if (redirectToBash) {
|
||||
session.stdin.add('$cmd\n'.uint8List);
|
||||
}
|
||||
final session = await execute(
|
||||
entry ?? 'cat | sh',
|
||||
pty: pty,
|
||||
environment: env,
|
||||
);
|
||||
|
||||
final result = BytesBuilder(copy: false);
|
||||
final stdoutDone = Completer<void>();
|
||||
final stderrDone = Completer<void>();
|
||||
|
||||
if (onStdout != null) {
|
||||
session.stdout.listen(
|
||||
(e) => onStdout(e.string, session),
|
||||
onDone: stdoutDone.complete,
|
||||
);
|
||||
} else {
|
||||
stdoutDone.complete();
|
||||
}
|
||||
session.stdout.listen(
|
||||
(e) {
|
||||
onStdout?.call(e.string, session);
|
||||
if (stdout) result.add(e);
|
||||
},
|
||||
onDone: stdoutDone.complete,
|
||||
onError: stderrDone.completeError,
|
||||
);
|
||||
|
||||
if (onStderr != null) {
|
||||
session.stderr.listen(
|
||||
(e) => onStderr(e.string, session),
|
||||
onDone: stderrDone.complete,
|
||||
);
|
||||
} else {
|
||||
stderrDone.complete();
|
||||
}
|
||||
session.stderr.listen(
|
||||
(e) {
|
||||
onStderr?.call(e.string, session);
|
||||
if (stderr) result.add(e);
|
||||
},
|
||||
onDone: stderrDone.complete,
|
||||
onError: stderrDone.completeError,
|
||||
);
|
||||
|
||||
if (stdin != null) {
|
||||
stdin(session);
|
||||
}
|
||||
onStdin(session);
|
||||
|
||||
await stdoutDone.future;
|
||||
await stderrDone.future;
|
||||
|
||||
session.close();
|
||||
return session;
|
||||
return (session, result.takeBytes().string);
|
||||
}
|
||||
|
||||
Future<int?> execWithPwd(
|
||||
String cmd, {
|
||||
String script, {
|
||||
String? entry,
|
||||
BuildContext? context,
|
||||
OnStdout? onStdout,
|
||||
OnStdout? onStderr,
|
||||
OnStdin? stdin,
|
||||
bool redirectToBash = false, // not working yet. do not use
|
||||
required String id,
|
||||
}) async {
|
||||
var isRequestingPwd = false;
|
||||
final session = await exec(
|
||||
cmd,
|
||||
redirectToBash: redirectToBash,
|
||||
final (session, _) = await exec(
|
||||
(sess) {
|
||||
sess.stdin.add('$script\n'.uint8List);
|
||||
sess.stdin.close();
|
||||
},
|
||||
onStderr: (data, session) async {
|
||||
onStderr?.call(data, session);
|
||||
if (isRequestingPwd) return;
|
||||
@@ -84,88 +85,38 @@ extension SSHClientX on SSHClient {
|
||||
? await context.showPwdDialog(title: user, id: id)
|
||||
: null;
|
||||
if (pwd == null || pwd.isEmpty) {
|
||||
session.kill(SSHSignal.TERM);
|
||||
session.stdin.close();
|
||||
} else {
|
||||
session.stdin.add('$pwd\n'.uint8List);
|
||||
}
|
||||
isRequestingPwd = false;
|
||||
}
|
||||
},
|
||||
onStdout: (data, sink) async {
|
||||
onStdout?.call(data, sink);
|
||||
},
|
||||
stdin: stdin,
|
||||
onStdout: onStdout,
|
||||
entry: entry,
|
||||
);
|
||||
return session.exitCode;
|
||||
}
|
||||
|
||||
Future<Uint8List> runForOutput(
|
||||
String command, {
|
||||
bool runInPty = false,
|
||||
Future<String> execForOutput(
|
||||
String script, {
|
||||
SSHPtyConfig? pty,
|
||||
bool stdout = true,
|
||||
bool stderr = true,
|
||||
Map<String, String>? environment,
|
||||
Future<void> Function(SSHSession)? action,
|
||||
String? entry,
|
||||
Map<String, String>? env,
|
||||
}) async {
|
||||
final session = await execute(
|
||||
command,
|
||||
pty: runInPty ? const SSHPtyConfig() : null,
|
||||
environment: environment,
|
||||
final ret = await exec(
|
||||
(session) {
|
||||
session.stdin.add('$script\n'.uint8List);
|
||||
session.stdin.close();
|
||||
},
|
||||
pty: pty,
|
||||
env: env,
|
||||
stdout: stdout,
|
||||
stderr: stderr,
|
||||
entry: entry,
|
||||
);
|
||||
|
||||
final result = BytesBuilder(copy: false);
|
||||
final stdoutDone = Completer<void>();
|
||||
final stderrDone = Completer<void>();
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
Future<String> runScriptIn(
|
||||
String cmd, {
|
||||
String shell = '/bin/sh',
|
||||
bool stdout = true,
|
||||
bool stderr = true,
|
||||
}) async {
|
||||
final session = await execute('cat | $shell');
|
||||
|
||||
final result = BytesBuilder(copy: false);
|
||||
final stdoutDone = Completer<void>();
|
||||
final stderrDone = Completer<void>();
|
||||
|
||||
session.stdout.listen(
|
||||
stdout ? result.add : (_) {},
|
||||
onDone: stdoutDone.complete,
|
||||
onError: stderrDone.completeError,
|
||||
);
|
||||
session.stderr.listen(
|
||||
stderr ? result.add : (_) {},
|
||||
onDone: stderrDone.complete,
|
||||
onError: stderrDone.completeError,
|
||||
);
|
||||
|
||||
session.stdin.add('$cmd\n'.uint8List);
|
||||
session.stdin.close();
|
||||
|
||||
await stdoutDone.future;
|
||||
await stderrDone.future;
|
||||
|
||||
return result.takeBytes().string;
|
||||
return ret.$2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,8 +95,8 @@ Future<SSHClient> genClient(
|
||||
try {
|
||||
final ipPort = spi.fromStringUrl();
|
||||
return await SSHSocket.connect(
|
||||
ipPort.ip,
|
||||
ipPort.port,
|
||||
ipPort.$1,
|
||||
ipPort.$2,
|
||||
timeout: timeout,
|
||||
);
|
||||
} catch (e) {
|
||||
|
||||
@@ -31,8 +31,8 @@ class ServerPrivateInfo {
|
||||
final List<String>? tags;
|
||||
@HiveField(7)
|
||||
final String? alterUrl;
|
||||
@HiveField(8)
|
||||
final bool? autoConnect;
|
||||
@HiveField(8, defaultValue: true)
|
||||
final bool autoConnect;
|
||||
|
||||
/// [id] of the jump server
|
||||
@HiveField(9)
|
||||
@@ -59,7 +59,7 @@ class ServerPrivateInfo {
|
||||
this.keyId,
|
||||
this.tags,
|
||||
this.alterUrl,
|
||||
this.autoConnect,
|
||||
this.autoConnect = true,
|
||||
this.jumpId,
|
||||
this.custom,
|
||||
this.wolCfg,
|
||||
@@ -75,7 +75,7 @@ class ServerPrivateInfo {
|
||||
final keyId = json['pubKeyId'] as String?;
|
||||
final tags = (json['tags'] as List?)?.cast<String>();
|
||||
final alterUrl = json['alterUrl'] as String?;
|
||||
final autoConnect = json['autoConnect'] as bool?;
|
||||
final autoConnect = json['autoConnect'] as bool? ?? true;
|
||||
final jumpId = json['jumpId'] as String?;
|
||||
final custom = json['customCmd'] == null
|
||||
? null
|
||||
@@ -128,9 +128,7 @@ class ServerPrivateInfo {
|
||||
if (alterUrl != null) {
|
||||
data['alterUrl'] = alterUrl;
|
||||
}
|
||||
if (autoConnect != null) {
|
||||
data['autoConnect'] = autoConnect;
|
||||
}
|
||||
data['autoConnect'] = autoConnect;
|
||||
if (jumpId != null) {
|
||||
data['jumpId'] = jumpId;
|
||||
}
|
||||
@@ -160,7 +158,7 @@ class ServerPrivateInfo {
|
||||
custom?.cmds != old.custom?.cmds;
|
||||
}
|
||||
|
||||
IpPort fromStringUrl() {
|
||||
(String, int) fromStringUrl() {
|
||||
if (alterUrl == null) {
|
||||
throw SSHErr(type: SSHErrType.connect, message: 'alterUrl is null');
|
||||
}
|
||||
@@ -177,7 +175,7 @@ class ServerPrivateInfo {
|
||||
if (port <= 0 || port > 65535) {
|
||||
throw SSHErr(type: SSHErrType.connect, message: 'alterUrl port error');
|
||||
}
|
||||
return IpPort(ip_, port_);
|
||||
return (ip_, port_);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -206,11 +204,6 @@ class ServerPrivateInfo {
|
||||
logoUrl: 'https://example.com/logo.png',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class IpPort {
|
||||
final String ip;
|
||||
final int port;
|
||||
|
||||
IpPort(this.ip, this.port);
|
||||
bool get isRoot => user == 'root';
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ class ServerPrivateInfoAdapter extends TypeAdapter<ServerPrivateInfo> {
|
||||
keyId: fields[5] as String?,
|
||||
tags: (fields[6] as List?)?.cast<String>(),
|
||||
alterUrl: fields[7] as String?,
|
||||
autoConnect: fields[8] as bool?,
|
||||
autoConnect: fields[8] == null ? true : fields[8] as bool,
|
||||
jumpId: fields[9] as String?,
|
||||
custom: fields[10] as ServerCustom?,
|
||||
wolCfg: fields[11] as WakeOnLanCfg?,
|
||||
|
||||
@@ -34,7 +34,7 @@ class ServerProvider extends ChangeNotifier {
|
||||
final _manualDisconnectedIds = <String>{};
|
||||
|
||||
Future<void> load() async {
|
||||
// Issue #147
|
||||
// #147
|
||||
// Clear all servers because of restarting app will cause duplicate servers
|
||||
final oldServers = Map<String, Server>.from(_servers);
|
||||
_servers.clear();
|
||||
@@ -46,7 +46,7 @@ class ServerProvider extends ChangeNotifier {
|
||||
final originServer = oldServers[spi.id];
|
||||
final newServer = genServer(spi);
|
||||
|
||||
/// Issues #258
|
||||
/// #258
|
||||
/// If not [shouldReconnect], then keep the old state.
|
||||
if (originServer != null && !originServer.spi.shouldReconnect(spi)) {
|
||||
newServer.conn = originServer.conn;
|
||||
@@ -112,20 +112,20 @@ class ServerProvider extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
|
||||
await Future.wait(_servers.values.map((s) => _connectFn(s, onlyFailed)));
|
||||
}
|
||||
await Future.wait(_servers.values.map((s) async {
|
||||
if (onlyFailed) {
|
||||
if (s.conn != ServerConn.failed) return;
|
||||
TryLimiter.reset(s.spi.id);
|
||||
}
|
||||
|
||||
Future<void> _connectFn(Server s, bool onlyFailed) async {
|
||||
if (onlyFailed) {
|
||||
if (s.conn != ServerConn.failed) return;
|
||||
TryLimiter.reset(s.spi.id);
|
||||
}
|
||||
if (_manualDisconnectedIds.contains(s.spi.id)) return;
|
||||
|
||||
if (!(s.spi.autoConnect ?? true) && s.conn == ServerConn.disconnected ||
|
||||
_manualDisconnectedIds.contains(s.spi.id)) {
|
||||
return;
|
||||
}
|
||||
return await _getData(s.spi);
|
||||
if (s.conn == ServerConn.disconnected && !s.spi.autoConnect) {
|
||||
return;
|
||||
}
|
||||
|
||||
return await _getData(s.spi);
|
||||
}));
|
||||
}
|
||||
|
||||
Future<void> startAutoRefresh() async {
|
||||
@@ -301,16 +301,16 @@ class ServerProvider extends ChangeNotifier {
|
||||
final scriptRaw = ShellFunc.allScript(spi.custom?.cmds).uint8List;
|
||||
|
||||
try {
|
||||
final writeScriptResult = await s.client!.runForOutput(
|
||||
ShellFunc.getInstallShellCmd(spi.id),
|
||||
action: (session) async {
|
||||
final (_, writeScriptResult) = await s.client!.exec(
|
||||
(session) async {
|
||||
session.stdin.add(scriptRaw);
|
||||
session.stdin.close();
|
||||
},
|
||||
entry: ShellFunc.getInstallShellCmd(spi.id),
|
||||
);
|
||||
if (writeScriptResult.isNotEmpty) {
|
||||
ShellFunc.switchScriptDir(spi.id);
|
||||
throw String.fromCharCodes(writeScriptResult);
|
||||
throw writeScriptResult;
|
||||
}
|
||||
} on SSHAuthAbortError catch (e) {
|
||||
TryLimiter.inc(sid);
|
||||
|
||||
@@ -8,28 +8,27 @@ import 'package:server_box/data/res/provider.dart';
|
||||
|
||||
final class SystemdProvider {
|
||||
late final SSHClient _client;
|
||||
late final bool isRoot;
|
||||
|
||||
SystemdProvider.init(ServerPrivateInfo spi) {
|
||||
isRoot = spi.isRoot;
|
||||
_client = Pros.server.pick(spi: spi)!.client!;
|
||||
getUnits();
|
||||
}
|
||||
|
||||
final isBusy = false.vn;
|
||||
final isRoot = false.vn;
|
||||
final units = <SystemdUnit>[].vn;
|
||||
|
||||
Future<void> getUnits() async {
|
||||
isBusy.value = true;
|
||||
|
||||
try {
|
||||
final result = await _client.runScriptIn(_getUnitsCmd);
|
||||
final result = await _client.execForOutput(_getUnitsCmd);
|
||||
final units = result.split('\n');
|
||||
final isRootRaw = units.firstOrNull;
|
||||
isRoot.value = isRootRaw == '0';
|
||||
|
||||
final userUnits = <String>[];
|
||||
final systemUnits = <String>[];
|
||||
for (final unit in units.skip(1)) {
|
||||
for (final unit in units) {
|
||||
if (unit.startsWith('/etc/systemd/system')) {
|
||||
systemUnits.add(unit);
|
||||
} else if (unit.startsWith('~/.config/systemd/user')) {
|
||||
@@ -64,7 +63,7 @@ for unit in ${unitNames_.join(' ')}; do
|
||||
echo -n "${ShellFunc.seperator}\n\$state"
|
||||
done
|
||||
''';
|
||||
final result = await _client.runScriptIn(script);
|
||||
final result = await _client.execForOutput(script);
|
||||
final units = result.split(ShellFunc.seperator);
|
||||
|
||||
final parsedUnits = <SystemdUnit>[];
|
||||
@@ -124,17 +123,8 @@ done
|
||||
});
|
||||
return parsedUnits;
|
||||
}
|
||||
}
|
||||
|
||||
String _getIniVal(String line) {
|
||||
return line.split('=').last;
|
||||
}
|
||||
|
||||
const _getUnitsCmd = '''
|
||||
# If root, get system & user units, otherwise get user units
|
||||
uid=\$(id -u)
|
||||
echo \$uid
|
||||
|
||||
late final _getUnitsCmd = '''
|
||||
get_files() {
|
||||
unit_type=\$1
|
||||
base_dir=\$2
|
||||
@@ -149,14 +139,12 @@ get_files() {
|
||||
|
||||
get_type_files() {
|
||||
unit_type=\$1
|
||||
|
||||
base_dir=""
|
||||
if [ "\$uid" -eq 0 ]; then
|
||||
get_files \$unit_type /etc/systemd/system
|
||||
get_files \$unit_type ~/.config/systemd/user
|
||||
else
|
||||
get_files \$unit_type ~/.config/systemd/user
|
||||
fi
|
||||
|
||||
${isRoot ? """
|
||||
get_files \$unit_type /etc/systemd/system
|
||||
get_files \$unit_type ~/.config/systemd/user""" : """
|
||||
get_files \$unit_type ~/.config/systemd/user"""}
|
||||
}
|
||||
|
||||
types="service socket mount timer"
|
||||
@@ -165,3 +153,8 @@ for type in \$types; do
|
||||
get_type_files \$type
|
||||
done
|
||||
''';
|
||||
}
|
||||
|
||||
String _getIniVal(String line) {
|
||||
return line.split('=').last;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// This file is generated by fl_build. Do not edit.
|
||||
// ignore_for_file: prefer_single_quotes
|
||||
|
||||
class BuildData {
|
||||
static const String name = 'ServerBox';
|
||||
static const int build = 1058;
|
||||
static const int script = 56;
|
||||
static const String name = "ServerBox";
|
||||
static const int build = 1060;
|
||||
static const int script = 57;
|
||||
}
|
||||
|
||||
@@ -156,9 +156,8 @@ class BackupPage extends StatelessWidget {
|
||||
trailing: ListenableBuilder(
|
||||
listenable: webdavLoading,
|
||||
builder: (_, __) {
|
||||
if (webdavLoading.value) {
|
||||
return UIs.centerSizedLoadingSmall;
|
||||
}
|
||||
if (webdavLoading.value) return SizedLoading.centerSmall;
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
||||
@@ -199,7 +199,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
|
||||
return;
|
||||
}
|
||||
FocusScope.of(context).unfocus();
|
||||
_loading.value = UIs.centerSizedLoading;
|
||||
_loading.value = SizedLoading.centerMedium;
|
||||
try {
|
||||
final decrypted = await Computer.shared.start(decyptPem, [key, pwd]);
|
||||
final pki = PrivateKeyInfo(id: name, key: decrypted);
|
||||
|
||||
@@ -606,7 +606,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||
_tags.value = spi.tags?.toSet() ?? {};
|
||||
|
||||
_altUrlController.text = spi.alterUrl ?? '';
|
||||
_autoConnect.value = spi.autoConnect ?? true;
|
||||
_autoConnect.value = spi.autoConnect;
|
||||
_jumpServer.value = spi.jumpId;
|
||||
|
||||
final custom = spi.custom;
|
||||
|
||||
@@ -64,24 +64,22 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
||||
}
|
||||
|
||||
Widget _buildSortMenu() {
|
||||
final options = [
|
||||
(_SortType.name, libL10n.name),
|
||||
(_SortType.size, l10n.size),
|
||||
(_SortType.time, l10n.time),
|
||||
];
|
||||
return ValBuilder(
|
||||
listenable: _sortOption,
|
||||
builder: (value) {
|
||||
return PopupMenuButton<_SortType>(
|
||||
icon: const Icon(Icons.sort),
|
||||
itemBuilder: (context) {
|
||||
final currentSelectedOption = _sortOption.value;
|
||||
final options = [
|
||||
(_SortType.name, libL10n.name),
|
||||
(_SortType.size, l10n.size),
|
||||
(_SortType.time, l10n.time),
|
||||
];
|
||||
return options.map((r) {
|
||||
final (type, name) = r;
|
||||
final selected = type == currentSelectedOption.sortBy;
|
||||
final title = selected
|
||||
? "$name (${currentSelectedOption.reversed ? '-' : '+'})"
|
||||
: name;
|
||||
final selected = type == value.sortBy;
|
||||
final title =
|
||||
selected ? "$name (${value.reversed ? '-' : '+'})" : name;
|
||||
return PopupMenuItem(
|
||||
value: type,
|
||||
child: Text(
|
||||
@@ -607,7 +605,7 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
||||
/// Issue #97
|
||||
/// In order to compatible with the Synology NAS
|
||||
/// which not has '.' and '..' in listdir
|
||||
if (fs.isNotEmpty && fs.firstOrNull?.filename == '.') {
|
||||
if (fs.firstOrNull?.filename == '.') {
|
||||
fs.removeAt(0);
|
||||
}
|
||||
|
||||
|
||||
@@ -54,9 +54,9 @@ final class _SystemdPageState extends State<SystemdPage> {
|
||||
(isBusy) => AnimatedContainer(
|
||||
duration: Durations.medium1,
|
||||
curve: Curves.fastEaseInToSlowEaseOut,
|
||||
height: isBusy ? 50 : 0,
|
||||
height: isBusy ? 30 : 0,
|
||||
child: isBusy
|
||||
? UIs.centerSizedLoadingSmall.paddingOnly(bottom: 7)
|
||||
? SizedLoading.centerSmall.paddingOnly(bottom: 7)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
@@ -71,8 +71,7 @@ final class _SystemdPageState extends State<SystemdPage> {
|
||||
(units) {
|
||||
if (units.isEmpty) {
|
||||
return SliverToBoxAdapter(
|
||||
child: ListTile(title: Text(libL10n.empty))
|
||||
.cardx
|
||||
child: CenterGreyTitle(libL10n.empty)
|
||||
.paddingSymmetric(horizontal: 13),
|
||||
);
|
||||
}
|
||||
@@ -103,7 +102,7 @@ final class _SystemdPageState extends State<SystemdPage> {
|
||||
return PopupMenu(
|
||||
items: unit.availableFuncs.map(_buildUnitFuncBtn).toList(),
|
||||
onSelected: (val) async {
|
||||
final cmd = unit.getCmd(func: val, isRoot: _pro.isRoot.value);
|
||||
final cmd = unit.getCmd(func: val, isRoot: _pro.isRoot);
|
||||
final sure = await context.showRoundDialog(
|
||||
title: libL10n.attention,
|
||||
child: SimpleMarkdown(data: '```shell\n$cmd\n```'),
|
||||
|
||||
Reference in New Issue
Block a user