mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2026-02-15 04:34:34 +01:00
Compare commits
4 Commits
lollipopki
...
v1.0.1297
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5e22cbc65 | ||
|
|
7926a4d4a6 | ||
|
|
39a3e0800b | ||
|
|
cd3c094af0 |
149
.github/workflows/release.yml
vendored
149
.github/workflows/release.yml
vendored
@@ -9,10 +9,9 @@ on:
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
# Set by fl_build
|
||||
# env:
|
||||
# APP_NAME: ServerBox
|
||||
# BUILD_NUMBER: ${{ github.ref_name }}
|
||||
env:
|
||||
APP_NAME: ServerBox
|
||||
RELEASE_TAG: ${{ github.ref_name }}
|
||||
|
||||
jobs:
|
||||
releaseAndroid:
|
||||
@@ -21,11 +20,13 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: "3.38.0"
|
||||
flutter-version: "3.38.7"
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: "zulu"
|
||||
@@ -37,17 +38,28 @@ jobs:
|
||||
- name: Build
|
||||
run: dart run fl_build -p android
|
||||
- name: Rename for fdroid
|
||||
shell: bash
|
||||
run: |
|
||||
mv build/app/outputs/flutter-apk/${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_arm64.apk build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_arm64.apk
|
||||
mv build/app/outputs/flutter-apk/${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_arm.apk build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_arm.apk
|
||||
mv build/app/outputs/flutter-apk/${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_amd64.apk build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_amd64.apk
|
||||
APK_DIR="build/app/outputs/flutter-apk"
|
||||
shopt -s nullglob
|
||||
|
||||
for arch in arm64 arm amd64; do
|
||||
matches=("$APK_DIR"/"${APP_NAME}"_*_"${arch}".apk)
|
||||
if [ ${#matches[@]} -ne 1 ]; then
|
||||
echo "Error: expected 1 APK for ${arch}, found ${#matches[@]}"
|
||||
echo "APK_DIR: $APK_DIR"
|
||||
ls -la "$APK_DIR" || true
|
||||
exit 1
|
||||
fi
|
||||
mv "${matches[0]}" "$APK_DIR/${APP_NAME}_${RELEASE_TAG}_${arch}.apk"
|
||||
done
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_arm64.apk
|
||||
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_arm.apk
|
||||
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_amd64.apk
|
||||
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_${{ env.RELEASE_TAG }}_arm64.apk
|
||||
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_${{ env.RELEASE_TAG }}_arm.apk
|
||||
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_${{ env.RELEASE_TAG }}_amd64.apk
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -57,6 +69,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
- name: Install dependencies
|
||||
@@ -69,11 +83,22 @@ jobs:
|
||||
- name: Build
|
||||
run: |
|
||||
dart run fl_build -p linux
|
||||
- name: Rename for release
|
||||
shell: bash
|
||||
run: |
|
||||
shopt -s nullglob
|
||||
matches=("${APP_NAME}"_*_amd64.AppImage)
|
||||
if [ ${#matches[@]} -ne 1 ]; then
|
||||
echo "Error: expected 1 AppImage, found ${#matches[@]}"
|
||||
ls -la || true
|
||||
exit 1
|
||||
fi
|
||||
mv "${matches[0]}" "${APP_NAME}_${RELEASE_TAG}_amd64.AppImage"
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_amd64.AppImage
|
||||
${{ env.APP_NAME }}_${{ env.RELEASE_TAG }}_amd64.AppImage
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -83,32 +108,96 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
- name: Build
|
||||
run: dart run fl_build -p windows
|
||||
- name: Rename for release
|
||||
shell: bash
|
||||
run: |
|
||||
shopt -s nullglob
|
||||
matches=("${APP_NAME}"_*_windows_amd64.zip)
|
||||
if [ ${#matches[@]} -ne 1 ]; then
|
||||
echo "Error: expected 1 zip, found ${#matches[@]}"
|
||||
ls -la || true
|
||||
exit 1
|
||||
fi
|
||||
mv "${matches[0]}" "${APP_NAME}_${RELEASE_TAG}_windows_amd64.zip"
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_windows_amd64.zip
|
||||
${{ env.APP_NAME }}_${{ env.RELEASE_TAG }}_windows_amd64.zip
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# releaseApple:
|
||||
# name: Release ios and macos
|
||||
# runs-on: macos-latest
|
||||
# steps:
|
||||
# - name: Checkout
|
||||
# uses: actions/checkout@v6
|
||||
# - name: Install Flutter
|
||||
# uses: subosito/flutter-action@v2
|
||||
# - name: Build
|
||||
# run: dart run fl_build -p ios
|
||||
# - name: Create Release
|
||||
# uses: softprops/action-gh-release@v2
|
||||
# with:
|
||||
# files: |
|
||||
# ${{ env.APP_NAME }}_universal.ipa
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
releaseIOS:
|
||||
name: Release iOS
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
- name: Build
|
||||
run: |
|
||||
dart run fl_build -p ios -- --no-codesign
|
||||
shopt -s nullglob
|
||||
IPA_FILES=(build/ios/ipa/*.ipa)
|
||||
if [ ${#IPA_FILES[@]} -ne 1 ]; then
|
||||
echo "Error: expected 1 IPA, found ${#IPA_FILES[@]}"
|
||||
ls -la build/ios/ipa || true
|
||||
exit 1
|
||||
fi
|
||||
IPA_FILE="${IPA_FILES[0]}"
|
||||
echo "Found IPA: $IPA_FILE"
|
||||
cp "$IPA_FILE" "${APP_NAME}_${RELEASE_TAG}_ios.ipa"
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
${{ env.APP_NAME }}_${{ env.RELEASE_TAG }}_ios.ipa
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
releaseMacOS:
|
||||
name: Release macOS
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
- name: Build
|
||||
run: |
|
||||
dart run fl_build -p macos -- --no-codesign
|
||||
- name: Package
|
||||
run: |
|
||||
RELEASE_DIR="$GITHUB_WORKSPACE/build/macos/Build/Products/Release"
|
||||
APP_DIR="$RELEASE_DIR/$APP_NAME.app"
|
||||
OUT_ZIP="$GITHUB_WORKSPACE/${APP_NAME}_${RELEASE_TAG}_macos.zip"
|
||||
|
||||
if [ ! -d "$RELEASE_DIR" ]; then
|
||||
echo "Error: macOS release directory not found: $RELEASE_DIR"
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -d "$APP_DIR" ]; then
|
||||
echo "Error: macOS app bundle not found: $APP_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$RELEASE_DIR"
|
||||
zip -ry "$OUT_ZIP" "$APP_NAME.app"
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
${{ env.APP_NAME }}_${{ env.RELEASE_TAG }}_macos.zip
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -748,7 +748,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 1291;
|
||||
CURRENT_PROJECT_VERSION = 1297;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
||||
@@ -758,7 +758,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.1291;
|
||||
MARKETING_VERSION = 1.0.1297;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
@@ -884,7 +884,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 1291;
|
||||
CURRENT_PROJECT_VERSION = 1297;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
||||
@@ -894,7 +894,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.1291;
|
||||
MARKETING_VERSION = 1.0.1297;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
@@ -912,7 +912,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 1291;
|
||||
CURRENT_PROJECT_VERSION = 1297;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
||||
@@ -922,7 +922,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.1291;
|
||||
MARKETING_VERSION = 1.0.1297;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
@@ -943,7 +943,7 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1291;
|
||||
CURRENT_PROJECT_VERSION = 1297;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -956,7 +956,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.1291;
|
||||
MARKETING_VERSION = 1.0.1297;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
|
||||
@@ -982,7 +982,7 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1291;
|
||||
CURRENT_PROJECT_VERSION = 1297;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -995,7 +995,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.1291;
|
||||
MARKETING_VERSION = 1.0.1297;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -1018,7 +1018,7 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1291;
|
||||
CURRENT_PROJECT_VERSION = 1297;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -1031,7 +1031,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.1291;
|
||||
MARKETING_VERSION = 1.0.1297;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -1054,7 +1054,7 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1291;
|
||||
CURRENT_PROJECT_VERSION = 1297;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1066,7 +1066,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.1291;
|
||||
MARKETING_VERSION = 1.0.1297;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
|
||||
@@ -1095,7 +1095,7 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1291;
|
||||
CURRENT_PROJECT_VERSION = 1297;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1107,7 +1107,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.1291;
|
||||
MARKETING_VERSION = 1.0.1297;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
|
||||
PRODUCT_NAME = ServerBox;
|
||||
@@ -1133,7 +1133,7 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1291;
|
||||
CURRENT_PROJECT_VERSION = 1297;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1145,7 +1145,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.1291;
|
||||
MARKETING_VERSION = 1.0.1297;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
|
||||
PRODUCT_NAME = ServerBox;
|
||||
|
||||
@@ -112,7 +112,7 @@ extension SSHClientX on SSHClient {
|
||||
return (session, result.takeBytes().string);
|
||||
}
|
||||
|
||||
Future<int?> execWithPwd(
|
||||
Future<(int?, String)> execWithPwd(
|
||||
String script, {
|
||||
String? entry,
|
||||
BuildContext? context,
|
||||
@@ -121,7 +121,7 @@ extension SSHClientX on SSHClient {
|
||||
required String id,
|
||||
}) async {
|
||||
var isRequestingPwd = false;
|
||||
final (session, _) = await exec(
|
||||
final (session, output) = await exec(
|
||||
(sess) {
|
||||
sess.stdin.add('$script\n'.uint8List);
|
||||
sess.stdin.close();
|
||||
@@ -147,7 +147,7 @@ extension SSHClientX on SSHClient {
|
||||
onStdout: onStdout,
|
||||
entry: entry,
|
||||
);
|
||||
return session.exitCode;
|
||||
return (session.exitCode, output);
|
||||
}
|
||||
|
||||
Future<String> execForOutput(
|
||||
|
||||
@@ -38,77 +38,6 @@ String getPrivateKey(String id) {
|
||||
return pki.key;
|
||||
}
|
||||
|
||||
List<Spi> resolveMergedJumpChain(
|
||||
Spi target, {
|
||||
List<Spi>? jumpChain,
|
||||
}) {
|
||||
final injectedSpiMap = <String, Spi>{};
|
||||
if (jumpChain != null) {
|
||||
for (final s in jumpChain) {
|
||||
injectedSpiMap[s.id] = s;
|
||||
if (s.oldId.isNotEmpty) {
|
||||
injectedSpiMap[s.oldId] = s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spi resolveSpi(String id) {
|
||||
final injected = injectedSpiMap[id];
|
||||
if (injected != null) return injected;
|
||||
if (jumpChain != null) {
|
||||
throw SSHErr(type: SSHErrType.connect, message: 'Jump server not found in provided chain: $id');
|
||||
}
|
||||
final fromStore = Stores.server.box.get(id);
|
||||
if (fromStore == null) {
|
||||
throw SSHErr(type: SSHErrType.connect, message: 'Jump server not found: $id');
|
||||
}
|
||||
return fromStore;
|
||||
}
|
||||
|
||||
return _resolveMergedJumpChainInternal(target, resolveSpi: resolveSpi);
|
||||
}
|
||||
|
||||
List<Spi> _resolveMergedJumpChainInternal(
|
||||
Spi target, {
|
||||
required Spi Function(String id) resolveSpi,
|
||||
}) {
|
||||
final roots = target.jumpChainIds ?? (target.jumpId == null ? const <String>[] : [target.jumpId!]);
|
||||
if (roots.isEmpty) return const <Spi>[];
|
||||
|
||||
final seen = <String>{};
|
||||
final stack = <String>{};
|
||||
final out = <Spi>[];
|
||||
|
||||
String normId(Spi spi) => spi.id.isNotEmpty ? spi.id : spi.oldId;
|
||||
|
||||
void dfs(String id) {
|
||||
final hop = resolveSpi(id);
|
||||
final norm = normId(hop);
|
||||
|
||||
if (stack.contains(norm)) {
|
||||
throw SSHErr(type: SSHErrType.connect, message: 'Jump loop detected at $norm');
|
||||
}
|
||||
if (seen.contains(norm)) return;
|
||||
|
||||
stack.add(norm);
|
||||
final deps = hop.jumpChainIds ?? (hop.jumpId == null ? const <String>[] : [hop.jumpId!]);
|
||||
for (final dep in deps) {
|
||||
dfs(dep);
|
||||
}
|
||||
stack.remove(norm);
|
||||
|
||||
if (seen.add(norm)) {
|
||||
out.add(hop);
|
||||
}
|
||||
}
|
||||
|
||||
for (final r in roots) {
|
||||
dfs(r);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
Future<SSHClient> genClient(
|
||||
Spi spi, {
|
||||
void Function(GenSSHClientStatus)? onStatus,
|
||||
@@ -116,59 +45,21 @@ Future<SSHClient> genClient(
|
||||
/// Only pass this param if using multi-threading and key login
|
||||
String? privateKey,
|
||||
|
||||
/// Pre-resolved jump chain (in `spi.jumpId` order: immediate -> farthest).
|
||||
///
|
||||
/// This is mainly used when `Stores` is unavailable (e.g. in an isolate).
|
||||
List<Spi>? jumpChain,
|
||||
|
||||
/// Private keys for [jumpChain], aligned by index.
|
||||
///
|
||||
/// If a jump server uses key auth (`keyId != null`), you must provide the
|
||||
/// decrypted key pem here (or `genClient` will try to read from `Stores`).
|
||||
List<String?>? jumpPrivateKeys,
|
||||
/// Only pass this param if using multi-threading and key login
|
||||
String? jumpPrivateKey,
|
||||
Duration timeout = const Duration(seconds: 5),
|
||||
|
||||
/// [Spi] of the jump server
|
||||
///
|
||||
/// Must pass this param if using multi-threading and key login
|
||||
Spi? jumpSpi,
|
||||
|
||||
/// Handle keyboard-interactive authentication
|
||||
SSHUserInfoRequestHandler? onKeyboardInteractive,
|
||||
Map<String, String>? knownHostFingerprints,
|
||||
void Function(String storageKey, String fingerprintHex)? onHostKeyAccepted,
|
||||
Future<bool> Function(HostKeyPromptInfo info)? onHostKeyPrompt,
|
||||
}) async {
|
||||
return _genClientInternal(
|
||||
spi,
|
||||
onStatus: onStatus,
|
||||
privateKey: privateKey,
|
||||
jumpChain: jumpChain,
|
||||
jumpPrivateKeys: jumpPrivateKeys,
|
||||
timeout: timeout,
|
||||
onKeyboardInteractive: onKeyboardInteractive,
|
||||
knownHostFingerprints: knownHostFingerprints,
|
||||
onHostKeyAccepted: onHostKeyAccepted,
|
||||
onHostKeyPrompt: onHostKeyPrompt,
|
||||
visited: <String>{},
|
||||
);
|
||||
}
|
||||
|
||||
Future<SSHClient> _genClientInternal(
|
||||
Spi spi, {
|
||||
void Function(GenSSHClientStatus)? onStatus,
|
||||
String? privateKey,
|
||||
List<Spi>? jumpChain,
|
||||
List<String?>? jumpPrivateKeys,
|
||||
Duration timeout = const Duration(seconds: 5),
|
||||
SSHUserInfoRequestHandler? onKeyboardInteractive,
|
||||
Map<String, String>? knownHostFingerprints,
|
||||
void Function(String storageKey, String fingerprintHex)? onHostKeyAccepted,
|
||||
Future<bool> Function(HostKeyPromptInfo info)? onHostKeyPrompt,
|
||||
required Set<String> visited,
|
||||
SSHSocket? socketOverride,
|
||||
bool followJumpConfig = true,
|
||||
}) async {
|
||||
final identifier = _hostIdentifier(spi);
|
||||
if (!visited.add(identifier)) {
|
||||
throw SSHErr(type: SSHErrType.connect, message: 'Jump loop detected at ${spi.name} ($identifier)');
|
||||
}
|
||||
|
||||
onStatus?.call(GenSSHClientStatus.socket);
|
||||
|
||||
final hostKeyCache = Map<String, String>.from(knownHostFingerprints ?? _loadKnownHostFingerprints());
|
||||
@@ -177,126 +68,37 @@ Future<SSHClient> _genClientInternal(
|
||||
|
||||
String? alterUser;
|
||||
|
||||
final (socket, hopClients) = await () async {
|
||||
if (socketOverride != null) return (socketOverride, <SSHClient>[]);
|
||||
final socket = await () async {
|
||||
// Proxy
|
||||
final jumpSpi_ = () {
|
||||
// Multi-thread or key login
|
||||
if (jumpSpi != null) return jumpSpi;
|
||||
// Main thread
|
||||
if (spi.jumpId != null) return Stores.server.box.get(spi.jumpId);
|
||||
}();
|
||||
if (jumpSpi_ != null) {
|
||||
final jumpClient = await genClient(
|
||||
jumpSpi_,
|
||||
privateKey: jumpPrivateKey,
|
||||
timeout: timeout,
|
||||
knownHostFingerprints: hostKeyCache,
|
||||
onHostKeyAccepted: hostKeyPersist,
|
||||
onHostKeyPrompt: onHostKeyPrompt,
|
||||
);
|
||||
|
||||
if (followJumpConfig) {
|
||||
final injectedSpiMap = <String, Spi>{};
|
||||
final injectedKeyMap = <String, String?>{};
|
||||
|
||||
if (jumpChain != null) {
|
||||
for (var i = 0; i < jumpChain.length; i++) {
|
||||
final s = jumpChain[i];
|
||||
injectedSpiMap[s.id] = s;
|
||||
if (s.oldId.isNotEmpty) injectedSpiMap[s.oldId] = s;
|
||||
if (jumpPrivateKeys != null && i < jumpPrivateKeys.length) {
|
||||
injectedKeyMap[s.id] = jumpPrivateKeys[i];
|
||||
if (s.oldId.isNotEmpty) injectedKeyMap[s.oldId] = jumpPrivateKeys[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spi resolveSpi(String id) {
|
||||
final injected = injectedSpiMap[id];
|
||||
if (injected != null) return injected;
|
||||
if (jumpChain != null) {
|
||||
throw SSHErr(type: SSHErrType.connect, message: 'Jump server not found in provided chain: $id');
|
||||
}
|
||||
final fromStore = Stores.server.box.get(id);
|
||||
if (fromStore == null) {
|
||||
throw SSHErr(type: SSHErrType.connect, message: 'Jump server not found: $id');
|
||||
}
|
||||
return fromStore;
|
||||
}
|
||||
|
||||
String? resolveHopPrivateKey(Spi hop) {
|
||||
final keyId = hop.keyId;
|
||||
if (keyId == null) return null;
|
||||
final injected = injectedKeyMap[hop.id] ?? injectedKeyMap[hop.oldId];
|
||||
return injected ?? getPrivateKey(keyId);
|
||||
}
|
||||
|
||||
final hops = _resolveMergedJumpChainInternal(spi, resolveSpi: resolveSpi);
|
||||
if (hops.isNotEmpty) {
|
||||
// Build multi-hop forward chain with dedup/merge.
|
||||
final createdClients = <SSHClient>[];
|
||||
SSHClient? currentClient;
|
||||
|
||||
try {
|
||||
final firstHop = hops.first;
|
||||
final firstKey = resolveHopPrivateKey(firstHop);
|
||||
if (firstHop.keyId != null && firstKey == null) {
|
||||
throw SSHErr(type: SSHErrType.noPrivateKey, message: l10n.privateKeyNotFoundFmt(firstHop.keyId ?? ''));
|
||||
}
|
||||
|
||||
currentClient = await _genClientInternal(
|
||||
firstHop,
|
||||
privateKey: firstKey,
|
||||
jumpChain: jumpChain,
|
||||
jumpPrivateKeys: jumpPrivateKeys,
|
||||
timeout: timeout,
|
||||
onKeyboardInteractive: onKeyboardInteractive,
|
||||
knownHostFingerprints: hostKeyCache,
|
||||
onHostKeyAccepted: hostKeyPersist,
|
||||
onHostKeyPrompt: hostKeyPrompt,
|
||||
visited: visited,
|
||||
followJumpConfig: false,
|
||||
);
|
||||
createdClients.add(currentClient);
|
||||
|
||||
for (var i = 1; i < hops.length; i++) {
|
||||
final hop = hops[i];
|
||||
final forwarded = await currentClient!.forwardLocal(hop.ip, hop.port);
|
||||
final hopKey = resolveHopPrivateKey(hop);
|
||||
if (hop.keyId != null && hopKey == null) {
|
||||
throw SSHErr(type: SSHErrType.noPrivateKey, message: l10n.privateKeyNotFoundFmt(hop.keyId ?? ''));
|
||||
}
|
||||
|
||||
currentClient = await _genClientInternal(
|
||||
hop,
|
||||
privateKey: hopKey,
|
||||
jumpChain: jumpChain,
|
||||
jumpPrivateKeys: jumpPrivateKeys,
|
||||
timeout: timeout,
|
||||
onKeyboardInteractive: onKeyboardInteractive,
|
||||
knownHostFingerprints: hostKeyCache,
|
||||
onHostKeyAccepted: hostKeyPersist,
|
||||
onHostKeyPrompt: hostKeyPrompt,
|
||||
visited: visited,
|
||||
socketOverride: forwarded,
|
||||
followJumpConfig: false,
|
||||
);
|
||||
createdClients.add(currentClient);
|
||||
}
|
||||
|
||||
final forwardedSocket = await currentClient!.forwardLocal(spi.ip, spi.port);
|
||||
return (forwardedSocket, createdClients);
|
||||
} catch (e) {
|
||||
// Close all created clients on error to avoid leaks
|
||||
for (final client in createdClients) {
|
||||
try {
|
||||
client.close();
|
||||
} catch (_) {
|
||||
// Ignore close errors during cleanup
|
||||
}
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
// Note: On success, all intermediate clients must remain open
|
||||
// because the returned socket tunnels through them.
|
||||
}
|
||||
return await jumpClient.forwardLocal(spi.ip, spi.port);
|
||||
}
|
||||
|
||||
// Direct
|
||||
try {
|
||||
return (await SSHSocket.connect(spi.ip, spi.port, timeout: timeout), <SSHClient>[]);
|
||||
return await SSHSocket.connect(spi.ip, spi.port, timeout: timeout);
|
||||
} catch (e) {
|
||||
Loggers.app.warning('genClient', e);
|
||||
if (spi.alterUrl == null) rethrow;
|
||||
try {
|
||||
final res = spi.parseAlterUrl();
|
||||
alterUser = res.$2;
|
||||
return (await SSHSocket.connect(res.$1, res.$3, timeout: timeout), <SSHClient>[]);
|
||||
return await SSHSocket.connect(res.$1, res.$3, timeout: timeout);
|
||||
} catch (e) {
|
||||
Loggers.app.warning('genClient alterUrl', e);
|
||||
rethrow;
|
||||
@@ -311,52 +113,32 @@ Future<SSHClient> _genClientInternal(
|
||||
prompt: hostKeyPrompt,
|
||||
);
|
||||
|
||||
Future<SSHClient> buildClient(SSHSocket socket) async {
|
||||
final keyId = spi.keyId;
|
||||
if (keyId == null) {
|
||||
onStatus?.call(GenSSHClientStatus.pwd);
|
||||
return SSHClient(
|
||||
socket,
|
||||
username: alterUser ?? spi.user,
|
||||
onPasswordRequest: () => spi.pwd,
|
||||
onUserInfoRequest: onKeyboardInteractive,
|
||||
onVerifyHostKey: hostKeyVerifier.call,
|
||||
// printDebug: debugPrint,
|
||||
// printTrace: debugPrint,
|
||||
);
|
||||
}
|
||||
privateKey ??= getPrivateKey(keyId);
|
||||
|
||||
onStatus?.call(GenSSHClientStatus.key);
|
||||
final keyId = spi.keyId;
|
||||
if (keyId == null) {
|
||||
onStatus?.call(GenSSHClientStatus.pwd);
|
||||
return SSHClient(
|
||||
socket,
|
||||
username: spi.user,
|
||||
// Must use [compute] here, instead of [Computer.shared.start]
|
||||
identities: await compute(loadIndentity, privateKey!),
|
||||
username: alterUser ?? spi.user,
|
||||
onPasswordRequest: () => spi.pwd,
|
||||
onUserInfoRequest: onKeyboardInteractive,
|
||||
onVerifyHostKey: hostKeyVerifier.call,
|
||||
// printDebug: debugPrint,
|
||||
// printTrace: debugPrint,
|
||||
);
|
||||
}
|
||||
privateKey ??= getPrivateKey(keyId);
|
||||
|
||||
final client = await buildClient(socket);
|
||||
|
||||
// Tie hop clients' lifetime to the final client: close all hop clients
|
||||
// when the target client disconnects to avoid leaking SSH connections.
|
||||
if (hopClients.isNotEmpty) {
|
||||
client.done.whenComplete(() {
|
||||
for (final hopClient in hopClients) {
|
||||
try {
|
||||
hopClient.close();
|
||||
} catch (_) {
|
||||
// Ignore close errors during cleanup
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return client;
|
||||
onStatus?.call(GenSSHClientStatus.key);
|
||||
return SSHClient(
|
||||
socket,
|
||||
username: spi.user,
|
||||
// Must use [compute] here, instead of [Computer.shared.start]
|
||||
identities: await compute(loadIndentity, privateKey),
|
||||
onUserInfoRequest: onKeyboardInteractive,
|
||||
onVerifyHostKey: hostKeyVerifier.call,
|
||||
// printDebug: debugPrint,
|
||||
// printTrace: debugPrint,
|
||||
);
|
||||
}
|
||||
|
||||
typedef _HostKeyPersistCallback = void Function(String storageKey, String fingerprintHex);
|
||||
@@ -518,53 +300,20 @@ Future<void> ensureKnownHostKey(
|
||||
Duration timeout = const Duration(seconds: 5),
|
||||
SSHUserInfoRequestHandler? onKeyboardInteractive,
|
||||
}) async {
|
||||
var cache = _loadKnownHostFingerprints();
|
||||
|
||||
final hops = resolveMergedJumpChain(spi);
|
||||
|
||||
// Check each hop's host key, routing through preceding hops
|
||||
for (var i = 0; i < hops.length; i++) {
|
||||
final hop = hops[i];
|
||||
// Preceding hops needed to reach this hop
|
||||
final precedingHops = i > 0 ? hops.sublist(0, i) : null;
|
||||
final precedingKeys = precedingHops?.map((h) =>
|
||||
h.keyId != null ? getPrivateKey(h.keyId!) : null
|
||||
).toList();
|
||||
|
||||
cache = await _ensureKnownHostKeyForSingle(
|
||||
hop,
|
||||
cache: cache,
|
||||
timeout: timeout,
|
||||
onKeyboardInteractive: onKeyboardInteractive,
|
||||
jumpChain: precedingHops,
|
||||
jumpPrivateKeys: precedingKeys,
|
||||
);
|
||||
final cache = _loadKnownHostFingerprints();
|
||||
if (_hasKnownHostFingerprintForSpi(spi, cache)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check the target's host key, routing through all hops
|
||||
final allKeys = hops.isNotEmpty
|
||||
? hops.map((h) => h.keyId != null ? getPrivateKey(h.keyId!) : null).toList()
|
||||
: null;
|
||||
await _ensureKnownHostKeyForSingle(
|
||||
spi,
|
||||
cache: cache,
|
||||
timeout: timeout,
|
||||
onKeyboardInteractive: onKeyboardInteractive,
|
||||
jumpChain: hops.isNotEmpty ? hops : null,
|
||||
jumpPrivateKeys: allKeys,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, String>> _ensureKnownHostKeyForSingle(
|
||||
Spi spi, {
|
||||
required Map<String, String> cache,
|
||||
Duration timeout = const Duration(seconds: 5),
|
||||
SSHUserInfoRequestHandler? onKeyboardInteractive,
|
||||
List<Spi>? jumpChain,
|
||||
List<String?>? jumpPrivateKeys,
|
||||
}) async {
|
||||
if (_hasKnownHostFingerprintForSpi(spi, cache)) {
|
||||
return cache;
|
||||
final jumpSpi = spi.jumpId != null ? Stores.server.box.get(spi.jumpId) : null;
|
||||
if (jumpSpi != null && !_hasKnownHostFingerprintForSpi(jumpSpi, cache)) {
|
||||
await ensureKnownHostKey(
|
||||
jumpSpi,
|
||||
timeout: timeout,
|
||||
onKeyboardInteractive: onKeyboardInteractive,
|
||||
);
|
||||
cache.addAll(_loadKnownHostFingerprints());
|
||||
if (_hasKnownHostFingerprintForSpi(spi, cache)) return;
|
||||
}
|
||||
|
||||
final client = await genClient(
|
||||
@@ -572,8 +321,6 @@ Future<Map<String, String>> _ensureKnownHostKeyForSingle(
|
||||
timeout: timeout,
|
||||
onKeyboardInteractive: onKeyboardInteractive,
|
||||
knownHostFingerprints: cache,
|
||||
jumpChain: jumpChain,
|
||||
jumpPrivateKeys: jumpPrivateKeys,
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -581,9 +328,6 @@ Future<Map<String, String>> _ensureKnownHostKeyForSingle(
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
|
||||
cache.addAll(_loadKnownHostFingerprints());
|
||||
return cache;
|
||||
}
|
||||
|
||||
bool _hasKnownHostFingerprintForSpi(Spi spi, Map<String, String> cache) {
|
||||
|
||||
@@ -26,6 +26,7 @@ enum ContainerErrType {
|
||||
parsePs,
|
||||
parseImages,
|
||||
parseStats,
|
||||
podmanDetected,
|
||||
}
|
||||
|
||||
class ContainerErr extends Err<ContainerErrType> {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:server_box/data/model/app/error.dart';
|
||||
import 'package:server_box/data/model/server/custom.dart';
|
||||
@@ -36,15 +35,8 @@ abstract class Spi with _$Spi {
|
||||
String? alterUrl,
|
||||
@Default(true) bool autoConnect,
|
||||
|
||||
/// [id] of the jump server (legacy, single hop)
|
||||
///
|
||||
/// Migrated to [jumpChainIds].
|
||||
/// [id] of the jump server
|
||||
String? jumpId,
|
||||
|
||||
/// Jump chain hop ids (nearest -> farthest)
|
||||
///
|
||||
/// Preferred over [jumpId].
|
||||
@JsonKey(includeIfNull: false) List<String>? jumpChainIds,
|
||||
ServerCustom? custom,
|
||||
WakeOnLanCfg? wolCfg,
|
||||
|
||||
@@ -87,10 +79,7 @@ extension Spix on Spi {
|
||||
String? migrateId() {
|
||||
if (id.isNotEmpty) return null;
|
||||
ServerStore.instance.delete(oldId);
|
||||
final newSpi = copyWith(
|
||||
id: ShortId.generate(),
|
||||
jumpChainIds: jumpChainIds ?? (jumpId == null ? null : [jumpId!]),
|
||||
);
|
||||
final newSpi = copyWith(id: ShortId.generate());
|
||||
newSpi.save();
|
||||
return newSpi.id;
|
||||
}
|
||||
@@ -105,8 +94,7 @@ extension Spix on Spi {
|
||||
port == other.port &&
|
||||
pwd == other.pwd &&
|
||||
keyId == other.keyId &&
|
||||
jumpId == other.jumpId &&
|
||||
listEquals(jumpChainIds, other.jumpChainIds);
|
||||
jumpId == other.jumpId;
|
||||
}
|
||||
|
||||
/// Returns true if the connection should be re-established.
|
||||
@@ -149,7 +137,7 @@ extension Spix on Spi {
|
||||
tags: ['tag1', 'tag2'],
|
||||
alterUrl: 'user@ip:port',
|
||||
autoConnect: true,
|
||||
jumpChainIds: ['jump_server_id'],
|
||||
jumpId: 'jump_server_id',
|
||||
custom: ServerCustom(
|
||||
pveAddr: 'http://localhost:8006',
|
||||
pveIgnoreCert: false,
|
||||
|
||||
@@ -16,13 +16,8 @@ T _$identity<T>(T value) => value;
|
||||
mixin _$Spi {
|
||||
|
||||
String get name; String get ip; int get port; String get user; String? get pwd;/// [id] of private key
|
||||
@JsonKey(name: 'pubKeyId') String? get keyId; List<String>? get tags; String? get alterUrl; bool get autoConnect;/// [id] of the jump server (legacy, single hop)
|
||||
///
|
||||
/// Migrated to [jumpChainIds].
|
||||
String? get jumpId;/// Jump chain hop ids (nearest -> farthest)
|
||||
///
|
||||
/// Preferred over [jumpId].
|
||||
@JsonKey(includeIfNull: false) List<String>? get jumpChainIds; ServerCustom? get custom; WakeOnLanCfg? get wolCfg;/// It only applies to SSH terminal.
|
||||
@JsonKey(name: 'pubKeyId') String? get keyId; List<String>? get tags; String? get alterUrl; bool get autoConnect;/// [id] of the jump server
|
||||
String? get jumpId; ServerCustom? get custom; WakeOnLanCfg? get wolCfg;/// It only applies to SSH terminal.
|
||||
Map<String, String>? get envs;@JsonKey(fromJson: Spi.parseId) String get id;/// Custom system type (unix or windows). If set, skip auto-detection.
|
||||
@JsonKey(includeIfNull: false) SystemType? get customSystemType;/// Disabled command types for this server
|
||||
@JsonKey(includeIfNull: false) List<String>? get disabledCmdTypes;
|
||||
@@ -38,12 +33,12 @@ $SpiCopyWith<Spi> get copyWith => _$SpiCopyWithImpl<Spi>(this as Spi, _$identity
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other.tags, tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&const DeepCollectionEquality().equals(other.jumpChainIds, jumpChainIds)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other.envs, envs)&&(identical(other.id, id) || other.id == id)&&(identical(other.customSystemType, customSystemType) || other.customSystemType == customSystemType)&&const DeepCollectionEquality().equals(other.disabledCmdTypes, disabledCmdTypes));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other.tags, tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other.envs, envs)&&(identical(other.id, id) || other.id == id)&&(identical(other.customSystemType, customSystemType) || other.customSystemType == customSystemType)&&const DeepCollectionEquality().equals(other.disabledCmdTypes, disabledCmdTypes));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(tags),alterUrl,autoConnect,jumpId,const DeepCollectionEquality().hash(jumpChainIds),custom,wolCfg,const DeepCollectionEquality().hash(envs),id,customSystemType,const DeepCollectionEquality().hash(disabledCmdTypes));
|
||||
int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(tags),alterUrl,autoConnect,jumpId,custom,wolCfg,const DeepCollectionEquality().hash(envs),id,customSystemType,const DeepCollectionEquality().hash(disabledCmdTypes));
|
||||
|
||||
|
||||
|
||||
@@ -54,7 +49,7 @@ abstract mixin class $SpiCopyWith<$Res> {
|
||||
factory $SpiCopyWith(Spi value, $Res Function(Spi) _then) = _$SpiCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId,@JsonKey(includeIfNull: false) List<String>? jumpChainIds, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id,@JsonKey(includeIfNull: false) SystemType? customSystemType,@JsonKey(includeIfNull: false) List<String>? disabledCmdTypes
|
||||
String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id,@JsonKey(includeIfNull: false) SystemType? customSystemType,@JsonKey(includeIfNull: false) List<String>? disabledCmdTypes
|
||||
});
|
||||
|
||||
|
||||
@@ -71,7 +66,7 @@ class _$SpiCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of Spi
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? jumpChainIds = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,Object? customSystemType = freezed,Object? disabledCmdTypes = freezed,}) {
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,Object? customSystemType = freezed,Object? disabledCmdTypes = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable
|
||||
@@ -83,8 +78,7 @@ as String?,tags: freezed == tags ? _self.tags : tags // ignore: cast_nullable_to
|
||||
as List<String>?,alterUrl: freezed == alterUrl ? _self.alterUrl : alterUrl // ignore: cast_nullable_to_non_nullable
|
||||
as String?,autoConnect: null == autoConnect ? _self.autoConnect : autoConnect // ignore: cast_nullable_to_non_nullable
|
||||
as bool,jumpId: freezed == jumpId ? _self.jumpId : jumpId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,jumpChainIds: freezed == jumpChainIds ? _self.jumpChainIds : jumpChainIds // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>?,custom: freezed == custom ? _self.custom : custom // ignore: cast_nullable_to_non_nullable
|
||||
as String?,custom: freezed == custom ? _self.custom : custom // ignore: cast_nullable_to_non_nullable
|
||||
as ServerCustom?,wolCfg: freezed == wolCfg ? _self.wolCfg : wolCfg // ignore: cast_nullable_to_non_nullable
|
||||
as WakeOnLanCfg?,envs: freezed == envs ? _self.envs : envs // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, String>?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
@@ -175,10 +169,10 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String name, String ip, int port, String user, String? pwd, @JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, @JsonKey(includeIfNull: false) List<String>? jumpChainIds, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) String id, @JsonKey(includeIfNull: false) SystemType? customSystemType, @JsonKey(includeIfNull: false) List<String>? disabledCmdTypes)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String name, String ip, int port, String user, String? pwd, @JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) String id, @JsonKey(includeIfNull: false) SystemType? customSystemType, @JsonKey(includeIfNull: false) List<String>? disabledCmdTypes)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _Spi() when $default != null:
|
||||
return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.jumpChainIds,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes);case _:
|
||||
return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
@@ -196,10 +190,10 @@ return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String name, String ip, int port, String user, String? pwd, @JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, @JsonKey(includeIfNull: false) List<String>? jumpChainIds, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) String id, @JsonKey(includeIfNull: false) SystemType? customSystemType, @JsonKey(includeIfNull: false) List<String>? disabledCmdTypes) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String name, String ip, int port, String user, String? pwd, @JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) String id, @JsonKey(includeIfNull: false) SystemType? customSystemType, @JsonKey(includeIfNull: false) List<String>? disabledCmdTypes) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _Spi():
|
||||
return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.jumpChainIds,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes);case _:
|
||||
return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
@@ -216,10 +210,10 @@ return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String name, String ip, int port, String user, String? pwd, @JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, @JsonKey(includeIfNull: false) List<String>? jumpChainIds, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) String id, @JsonKey(includeIfNull: false) SystemType? customSystemType, @JsonKey(includeIfNull: false) List<String>? disabledCmdTypes)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String name, String ip, int port, String user, String? pwd, @JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) String id, @JsonKey(includeIfNull: false) SystemType? customSystemType, @JsonKey(includeIfNull: false) List<String>? disabledCmdTypes)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _Spi() when $default != null:
|
||||
return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.jumpChainIds,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes);case _:
|
||||
return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
@@ -231,7 +225,7 @@ return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,
|
||||
|
||||
@JsonSerializable(includeIfNull: false)
|
||||
class _Spi extends Spi {
|
||||
const _Spi({required this.name, required this.ip, required this.port, required this.user, this.pwd, @JsonKey(name: 'pubKeyId') this.keyId, final List<String>? tags, this.alterUrl, this.autoConnect = true, this.jumpId, @JsonKey(includeIfNull: false) final List<String>? jumpChainIds, this.custom, this.wolCfg, final Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) this.id = '', @JsonKey(includeIfNull: false) this.customSystemType, @JsonKey(includeIfNull: false) final List<String>? disabledCmdTypes}): _tags = tags,_jumpChainIds = jumpChainIds,_envs = envs,_disabledCmdTypes = disabledCmdTypes,super._();
|
||||
const _Spi({required this.name, required this.ip, required this.port, required this.user, this.pwd, @JsonKey(name: 'pubKeyId') this.keyId, final List<String>? tags, this.alterUrl, this.autoConnect = true, this.jumpId, this.custom, this.wolCfg, final Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) this.id = '', @JsonKey(includeIfNull: false) this.customSystemType, @JsonKey(includeIfNull: false) final List<String>? disabledCmdTypes}): _tags = tags,_envs = envs,_disabledCmdTypes = disabledCmdTypes,super._();
|
||||
factory _Spi.fromJson(Map<String, dynamic> json) => _$SpiFromJson(json);
|
||||
|
||||
@override final String name;
|
||||
@@ -252,25 +246,8 @@ class _Spi extends Spi {
|
||||
|
||||
@override final String? alterUrl;
|
||||
@override@JsonKey() final bool autoConnect;
|
||||
/// [id] of the jump server (legacy, single hop)
|
||||
///
|
||||
/// Migrated to [jumpChainIds].
|
||||
/// [id] of the jump server
|
||||
@override final String? jumpId;
|
||||
/// Jump chain hop ids (nearest -> farthest)
|
||||
///
|
||||
/// Preferred over [jumpId].
|
||||
final List<String>? _jumpChainIds;
|
||||
/// Jump chain hop ids (nearest -> farthest)
|
||||
///
|
||||
/// Preferred over [jumpId].
|
||||
@override@JsonKey(includeIfNull: false) List<String>? get jumpChainIds {
|
||||
final value = _jumpChainIds;
|
||||
if (value == null) return null;
|
||||
if (_jumpChainIds is EqualUnmodifiableListView) return _jumpChainIds;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(value);
|
||||
}
|
||||
|
||||
@override final ServerCustom? custom;
|
||||
@override final WakeOnLanCfg? wolCfg;
|
||||
/// It only applies to SSH terminal.
|
||||
@@ -312,12 +289,12 @@ Map<String, dynamic> toJson() {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other._tags, _tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&const DeepCollectionEquality().equals(other._jumpChainIds, _jumpChainIds)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other._envs, _envs)&&(identical(other.id, id) || other.id == id)&&(identical(other.customSystemType, customSystemType) || other.customSystemType == customSystemType)&&const DeepCollectionEquality().equals(other._disabledCmdTypes, _disabledCmdTypes));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other._tags, _tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other._envs, _envs)&&(identical(other.id, id) || other.id == id)&&(identical(other.customSystemType, customSystemType) || other.customSystemType == customSystemType)&&const DeepCollectionEquality().equals(other._disabledCmdTypes, _disabledCmdTypes));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(_tags),alterUrl,autoConnect,jumpId,const DeepCollectionEquality().hash(_jumpChainIds),custom,wolCfg,const DeepCollectionEquality().hash(_envs),id,customSystemType,const DeepCollectionEquality().hash(_disabledCmdTypes));
|
||||
int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(_tags),alterUrl,autoConnect,jumpId,custom,wolCfg,const DeepCollectionEquality().hash(_envs),id,customSystemType,const DeepCollectionEquality().hash(_disabledCmdTypes));
|
||||
|
||||
|
||||
|
||||
@@ -328,7 +305,7 @@ abstract mixin class _$SpiCopyWith<$Res> implements $SpiCopyWith<$Res> {
|
||||
factory _$SpiCopyWith(_Spi value, $Res Function(_Spi) _then) = __$SpiCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId,@JsonKey(includeIfNull: false) List<String>? jumpChainIds, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id,@JsonKey(includeIfNull: false) SystemType? customSystemType,@JsonKey(includeIfNull: false) List<String>? disabledCmdTypes
|
||||
String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id,@JsonKey(includeIfNull: false) SystemType? customSystemType,@JsonKey(includeIfNull: false) List<String>? disabledCmdTypes
|
||||
});
|
||||
|
||||
|
||||
@@ -345,7 +322,7 @@ class __$SpiCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of Spi
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? jumpChainIds = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,Object? customSystemType = freezed,Object? disabledCmdTypes = freezed,}) {
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,Object? customSystemType = freezed,Object? disabledCmdTypes = freezed,}) {
|
||||
return _then(_Spi(
|
||||
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable
|
||||
@@ -357,8 +334,7 @@ as String?,tags: freezed == tags ? _self._tags : tags // ignore: cast_nullable_t
|
||||
as List<String>?,alterUrl: freezed == alterUrl ? _self.alterUrl : alterUrl // ignore: cast_nullable_to_non_nullable
|
||||
as String?,autoConnect: null == autoConnect ? _self.autoConnect : autoConnect // ignore: cast_nullable_to_non_nullable
|
||||
as bool,jumpId: freezed == jumpId ? _self.jumpId : jumpId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,jumpChainIds: freezed == jumpChainIds ? _self._jumpChainIds : jumpChainIds // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>?,custom: freezed == custom ? _self.custom : custom // ignore: cast_nullable_to_non_nullable
|
||||
as String?,custom: freezed == custom ? _self.custom : custom // ignore: cast_nullable_to_non_nullable
|
||||
as ServerCustom?,wolCfg: freezed == wolCfg ? _self.wolCfg : wolCfg // ignore: cast_nullable_to_non_nullable
|
||||
as WakeOnLanCfg?,envs: freezed == envs ? _self._envs : envs // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, String>?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
|
||||
@@ -17,9 +17,6 @@ _Spi _$SpiFromJson(Map<String, dynamic> json) => _Spi(
|
||||
alterUrl: json['alterUrl'] as String?,
|
||||
autoConnect: json['autoConnect'] as bool? ?? true,
|
||||
jumpId: json['jumpId'] as String?,
|
||||
jumpChainIds: (json['jumpChainIds'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList(),
|
||||
custom: json['custom'] == null
|
||||
? null
|
||||
: ServerCustom.fromJson(json['custom'] as Map<String, dynamic>),
|
||||
@@ -50,7 +47,6 @@ Map<String, dynamic> _$SpiToJson(_Spi instance) => <String, dynamic>{
|
||||
'alterUrl': ?instance.alterUrl,
|
||||
'autoConnect': instance.autoConnect,
|
||||
'jumpId': ?instance.jumpId,
|
||||
'jumpChainIds': ?instance.jumpChainIds,
|
||||
'custom': ?instance.custom,
|
||||
'wolCfg': ?instance.wolCfg,
|
||||
'envs': ?instance.envs,
|
||||
|
||||
@@ -6,8 +6,8 @@ class SftpReq {
|
||||
final String localPath;
|
||||
final SftpReqType type;
|
||||
String? privateKey;
|
||||
List<Spi>? jumpChain;
|
||||
List<String?>? jumpPrivateKeys;
|
||||
Spi? jumpSpi;
|
||||
String? jumpPrivateKey;
|
||||
Map<String, String>? knownHostFingerprints;
|
||||
|
||||
SftpReq(this.spi, this.remotePath, this.localPath, this.type) {
|
||||
@@ -15,17 +15,9 @@ class SftpReq {
|
||||
if (keyId != null) {
|
||||
privateKey = getPrivateKey(keyId);
|
||||
}
|
||||
if (spi.jumpChainIds != null || spi.jumpId != null) {
|
||||
// Use resolveMergedJumpChain to recursively expand nested hop chains
|
||||
final chain = resolveMergedJumpChain(spi);
|
||||
final keys = <String?>[];
|
||||
for (final hop in chain) {
|
||||
keys.add(hop.keyId != null ? getPrivateKey(hop.keyId!) : null);
|
||||
}
|
||||
|
||||
// Always set when a jump is configured so the isolate won't fallback to Stores.
|
||||
jumpChain = chain;
|
||||
jumpPrivateKeys = keys;
|
||||
if (spi.jumpId != null) {
|
||||
jumpSpi = Stores.server.box.get(spi.jumpId);
|
||||
jumpPrivateKey = Stores.key.fetchOne(jumpSpi?.keyId)?.key;
|
||||
}
|
||||
try {
|
||||
knownHostFingerprints = Map<String, String>.from(Stores.setting.sshKnownHostFingerprints.get());
|
||||
@@ -98,4 +90,4 @@ class SftpReqStatus {
|
||||
}
|
||||
}
|
||||
|
||||
enum SftpWorkerStatus { preparing, sshConnected, loading, finished }
|
||||
enum SftpWorkerStatus { preparing, sshConnectted, loading, finished }
|
||||
|
||||
@@ -63,11 +63,11 @@ Future<void> _download(SftpReq req, SendPort mainSendPort, SendErrorFunction sen
|
||||
final client = await genClient(
|
||||
req.spi,
|
||||
privateKey: req.privateKey,
|
||||
jumpChain: req.jumpChain,
|
||||
jumpPrivateKeys: req.jumpPrivateKeys,
|
||||
jumpSpi: req.jumpSpi,
|
||||
jumpPrivateKey: req.jumpPrivateKey,
|
||||
knownHostFingerprints: req.knownHostFingerprints,
|
||||
);
|
||||
mainSendPort.send(SftpWorkerStatus.sshConnected);
|
||||
mainSendPort.send(SftpWorkerStatus.sshConnectted);
|
||||
|
||||
/// Create the directory if not exists
|
||||
final dirPath = req.localPath.substring(0, req.localPath.lastIndexOf(Pfs.seperator));
|
||||
@@ -120,11 +120,11 @@ Future<void> _upload(SftpReq req, SendPort mainSendPort, SendErrorFunction sendE
|
||||
final client = await genClient(
|
||||
req.spi,
|
||||
privateKey: req.privateKey,
|
||||
jumpChain: req.jumpChain,
|
||||
jumpPrivateKeys: req.jumpPrivateKeys,
|
||||
jumpSpi: req.jumpSpi,
|
||||
jumpPrivateKey: req.jumpPrivateKey,
|
||||
knownHostFingerprints: req.knownHostFingerprints,
|
||||
);
|
||||
mainSendPort.send(SftpWorkerStatus.sshConnected);
|
||||
mainSendPort.send(SftpWorkerStatus.sshConnectted);
|
||||
|
||||
final local = File(req.localPath);
|
||||
if (!await local.exists()) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/core/extension/ssh_client.dart';
|
||||
import 'package:server_box/data/model/app/error.dart';
|
||||
import 'package:server_box/data/model/app/scripts/script_consts.dart';
|
||||
@@ -18,6 +19,7 @@ part 'container.freezed.dart';
|
||||
part 'container.g.dart';
|
||||
|
||||
final _dockerNotFound = RegExp(r"command not found|Unknown command|Command '\w+' not found");
|
||||
final _podmanEmulationMsg = 'Emulate Docker CLI using podman';
|
||||
|
||||
@freezed
|
||||
abstract class ContainerState with _$ContainerState {
|
||||
@@ -84,21 +86,51 @@ class ContainerNotifier extends _$ContainerNotifier {
|
||||
}
|
||||
final includeStats = Stores.setting.containerParseStat.fetch();
|
||||
|
||||
var raw = '';
|
||||
final cmd = _wrap(ContainerCmdType.execAll(state.type, sudo: sudo, includeStats: includeStats));
|
||||
final code = await client?.execWithPwd(
|
||||
cmd,
|
||||
context: context,
|
||||
onStdout: (data, _) => raw = '$raw$data',
|
||||
id: hostId,
|
||||
);
|
||||
int? code;
|
||||
String raw = '';
|
||||
final errs = <String>[];
|
||||
if (client != null) {
|
||||
(code, raw) = await client!.execWithPwd(cmd, context: context, id: hostId);
|
||||
} else {
|
||||
state = state.copyWith(
|
||||
isBusy: false,
|
||||
error: ContainerErr(type: ContainerErrType.noClient),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(isBusy: false);
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
/// Code 127 means command not found
|
||||
if (code == 127 || raw.contains(_dockerNotFound)) {
|
||||
if (code == 127 || raw.contains(_dockerNotFound) || errs.join().contains(_dockerNotFound)) {
|
||||
state = state.copyWith(error: ContainerErr(type: ContainerErrType.notInstalled));
|
||||
return;
|
||||
}
|
||||
|
||||
/// Pre-parse Podman detection
|
||||
if (raw.contains(_podmanEmulationMsg)) {
|
||||
state = state.copyWith(
|
||||
error: ContainerErr(
|
||||
type: ContainerErrType.podmanDetected,
|
||||
message: l10n.podmanDockerEmulationDetected,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
/// Filter out sudo password prompt from output
|
||||
if (errs.any((e) => e.contains('[sudo] password'))) {
|
||||
raw = raw.split('\n').where((line) => !line.contains('[sudo] password')).join('\n');
|
||||
}
|
||||
|
||||
/// Detect Podman not installed when using Podman mode
|
||||
if (state.type == ContainerType.podman &&
|
||||
(errs.any((e) => e.contains('podman: not found')) ||
|
||||
raw.contains('podman: not found'))) {
|
||||
state = state.copyWith(error: ContainerErr(type: ContainerErrType.notInstalled));
|
||||
return;
|
||||
}
|
||||
@@ -122,9 +154,11 @@ class ContainerNotifier extends _$ContainerNotifier {
|
||||
final version = json.decode(verRaw)['Client']['Version'];
|
||||
state = state.copyWith(version: version, error: null);
|
||||
} catch (e, trace) {
|
||||
state = state.copyWith(
|
||||
error: ContainerErr(type: ContainerErrType.invalidVersion, message: '$e'),
|
||||
);
|
||||
if (state.error == null) {
|
||||
state = state.copyWith(
|
||||
error: ContainerErr(type: ContainerErrType.invalidVersion, message: '$e'),
|
||||
);
|
||||
}
|
||||
Loggers.app.warning('Container version failed', e, trace);
|
||||
}
|
||||
|
||||
@@ -140,9 +174,11 @@ class ContainerNotifier extends _$ContainerNotifier {
|
||||
final items = lines.map((e) => ContainerPs.fromRaw(e, state.type)).toList();
|
||||
state = state.copyWith(items: items);
|
||||
} catch (e, trace) {
|
||||
state = state.copyWith(
|
||||
error: ContainerErr(type: ContainerErrType.parsePs, message: '$e'),
|
||||
);
|
||||
if (state.error == null) {
|
||||
state = state.copyWith(
|
||||
error: ContainerErr(type: ContainerErrType.parsePs, message: '$e'),
|
||||
);
|
||||
}
|
||||
Loggers.app.warning('Container ps failed', e, trace);
|
||||
}
|
||||
|
||||
@@ -162,9 +198,11 @@ class ContainerNotifier extends _$ContainerNotifier {
|
||||
}
|
||||
state = state.copyWith(images: images);
|
||||
} catch (e, trace) {
|
||||
state = state.copyWith(
|
||||
error: ContainerErr(type: ContainerErrType.parseImages, message: '$e'),
|
||||
);
|
||||
if (state.error == null) {
|
||||
state = state.copyWith(
|
||||
error: ContainerErr(type: ContainerErrType.parseImages, message: '$e'),
|
||||
);
|
||||
}
|
||||
Loggers.app.warning('Container images failed', e, trace);
|
||||
}
|
||||
|
||||
@@ -189,9 +227,11 @@ class ContainerNotifier extends _$ContainerNotifier {
|
||||
item.parseStats(statsLine, state.version);
|
||||
}
|
||||
} catch (e, trace) {
|
||||
state = state.copyWith(
|
||||
error: ContainerErr(type: ContainerErrType.parseStats, message: '$e'),
|
||||
);
|
||||
if (state.error == null) {
|
||||
state = state.copyWith(
|
||||
error: ContainerErr(type: ContainerErrType.parseStats, message: '$e'),
|
||||
);
|
||||
}
|
||||
Loggers.app.warning('Parse docker stats: $statsRaw', e, trace);
|
||||
}
|
||||
}
|
||||
@@ -227,6 +267,10 @@ class ContainerNotifier extends _$ContainerNotifier {
|
||||
}
|
||||
|
||||
Future<ContainerErr?> run(String cmd, {bool autoRefresh = true}) async {
|
||||
if (client == null) {
|
||||
return ContainerErr(type: ContainerErrType.noClient);
|
||||
}
|
||||
|
||||
cmd = switch (state.type) {
|
||||
ContainerType.docker => 'docker $cmd',
|
||||
ContainerType.podman => 'podman $cmd',
|
||||
@@ -234,7 +278,7 @@ class ContainerNotifier extends _$ContainerNotifier {
|
||||
|
||||
state = state.copyWith(runLog: '');
|
||||
final errs = <String>[];
|
||||
final code = await client?.execWithPwd(
|
||||
final (code, _) = await client?.execWithPwd(
|
||||
_wrap((await sudoCompleter.future) ? 'sudo -S $cmd' : cmd),
|
||||
context: context,
|
||||
onStdout: (data, _) {
|
||||
@@ -242,7 +286,7 @@ class ContainerNotifier extends _$ContainerNotifier {
|
||||
},
|
||||
onStderr: (data, _) => errs.add(data),
|
||||
id: hostId,
|
||||
);
|
||||
) ?? (null, null);
|
||||
state = state.copyWith(runLog: null);
|
||||
|
||||
if (code != 0) {
|
||||
|
||||
@@ -41,7 +41,7 @@ final class ServersNotifierProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$serversNotifierHash() => r'277d1b219235f14bcc1b82a1e16260c2f28decdb';
|
||||
String _$serversNotifierHash() => r'dc5da44f9bd8d8dcfba3e6e932cca3e2f379e582';
|
||||
|
||||
abstract class _$ServersNotifier extends $Notifier<ServersState> {
|
||||
ServersState build();
|
||||
|
||||
@@ -135,7 +135,7 @@ class ServerNotifier extends _$ServerNotifier {
|
||||
|
||||
final time2 = DateTime.now();
|
||||
final spentTime = time2.difference(time1).inMilliseconds;
|
||||
if ((spi.jumpChainIds?.isNotEmpty != true) && spi.jumpId == null) {
|
||||
if (spi.jumpId == null) {
|
||||
Loggers.app.info('Connected to ${spi.name} in $spentTime ms.');
|
||||
} else {
|
||||
Loggers.app.info('Jump to ${spi.name} in $spentTime ms.');
|
||||
|
||||
@@ -58,7 +58,7 @@ final class ServerNotifierProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$serverNotifierHash() => r'52e806bcc32a7818d1ec2b07a3c683b06885c9f8';
|
||||
String _$serverNotifierHash() => r'04b1beef4d96242fd10d5b523c6f5f17eb774bae';
|
||||
|
||||
final class ServerNotifierFamily extends $Family
|
||||
with
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
|
||||
abstract class BuildData {
|
||||
static const String name = "ServerBox";
|
||||
static const int build = 1291;
|
||||
static const int build = 1297;
|
||||
static const int script = 70;
|
||||
}
|
||||
|
||||
@@ -89,12 +89,15 @@ class ServerStore extends HiveStore {
|
||||
// Replace ids in jump server settings.
|
||||
final spi = get<Spi>(newId);
|
||||
if (spi != null) {
|
||||
final jumpChainIds = spi.jumpChainIds ?? (spi.jumpId == null ? null : [spi.jumpId!]);
|
||||
if (jumpChainIds == null || jumpChainIds.isEmpty) continue;
|
||||
|
||||
final newChain = jumpChainIds.map((e) => idMap[e] ?? e).toList();
|
||||
final newSpi = spi.copyWith(jumpId: null, jumpChainIds: newChain);
|
||||
update(spi, newSpi);
|
||||
final jumpId = spi.jumpId; // This could be an oldId.
|
||||
// Check if this jumpId corresponds to a server that was also migrated.
|
||||
if (jumpId != null && idMap.containsKey(jumpId)) {
|
||||
final newJumpId = idMap[jumpId];
|
||||
if (spi.jumpId != newJumpId) {
|
||||
final newSpi = spi.copyWith(jumpId: newJumpId);
|
||||
update(spi, newSpi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Replace ids in [Snippet]
|
||||
|
||||
@@ -1933,6 +1933,12 @@ abstract class AppLocalizations {
|
||||
/// In en, this message translates to:
|
||||
/// **'Logs'**
|
||||
String get logs;
|
||||
|
||||
/// No description provided for @podmanDockerEmulationDetected.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Podman Docker emulation detected. Please switch to Podman in settings.'**
|
||||
String get podmanDockerEmulationDetected;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
||||
@@ -1031,4 +1031,8 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get logs => 'Protokolle';
|
||||
|
||||
@override
|
||||
String get podmanDockerEmulationDetected =>
|
||||
'Podman Docker-Emulation erkannt. Bitte wechseln Sie in den Einstellungen zu Podman.';
|
||||
}
|
||||
|
||||
@@ -1022,4 +1022,8 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get logs => 'Logs';
|
||||
|
||||
@override
|
||||
String get podmanDockerEmulationDetected =>
|
||||
'Podman Docker emulation detected. Please switch to Podman in settings.';
|
||||
}
|
||||
|
||||
@@ -1033,4 +1033,8 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get logs => 'Registros';
|
||||
|
||||
@override
|
||||
String get podmanDockerEmulationDetected =>
|
||||
'Detectada emulación de Podman Docker. Por favor, cambie a Podman en la configuración.';
|
||||
}
|
||||
|
||||
@@ -1036,4 +1036,8 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get logs => 'Journaux';
|
||||
|
||||
@override
|
||||
String get podmanDockerEmulationDetected =>
|
||||
'Émulation Podman Docker détectée. Veuillez passer à Podman dans les paramètres.';
|
||||
}
|
||||
|
||||
@@ -1022,4 +1022,8 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get logs => 'Log';
|
||||
|
||||
@override
|
||||
String get podmanDockerEmulationDetected =>
|
||||
'Emulasi Podman Docker terdeteksi. Silakan beralih ke Podman di pengaturan.';
|
||||
}
|
||||
|
||||
@@ -992,4 +992,8 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get logs => 'ログ';
|
||||
|
||||
@override
|
||||
String get podmanDockerEmulationDetected =>
|
||||
'Podman Docker エミュレーションが検出されました。設定で Podman に切り替えてください。';
|
||||
}
|
||||
|
||||
@@ -1029,4 +1029,8 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get logs => 'Logboeken';
|
||||
|
||||
@override
|
||||
String get podmanDockerEmulationDetected =>
|
||||
'Podman Docker-emulatie gedetecteerd. Schakel over naar Podman in de instellingen.';
|
||||
}
|
||||
|
||||
@@ -1024,4 +1024,8 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get logs => 'Logs';
|
||||
|
||||
@override
|
||||
String get podmanDockerEmulationDetected =>
|
||||
'Emulação Podman Docker detectada. Por favor, alterne para Podman nas configurações.';
|
||||
}
|
||||
|
||||
@@ -1028,4 +1028,8 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get logs => 'Журналы';
|
||||
|
||||
@override
|
||||
String get podmanDockerEmulationDetected =>
|
||||
'Обнаружена эмуляция Podman Docker. Пожалуйста, переключитесь на Podman в настройках.';
|
||||
}
|
||||
|
||||
@@ -1023,4 +1023,8 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get logs => 'Günlükler';
|
||||
|
||||
@override
|
||||
String get podmanDockerEmulationDetected =>
|
||||
'Podman Docker emülasyonu tespit edildi. Lütfen ayarlarda Podman\'a geçin.';
|
||||
}
|
||||
|
||||
@@ -1028,4 +1028,8 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get logs => 'Журнали';
|
||||
|
||||
@override
|
||||
String get podmanDockerEmulationDetected =>
|
||||
'Виявлено емуляцію Podman Docker. Будь ласка, переключіться на Podman у налаштуваннях.';
|
||||
}
|
||||
|
||||
@@ -977,6 +977,10 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get logs => '日志';
|
||||
|
||||
@override
|
||||
String get podmanDockerEmulationDetected =>
|
||||
'检测到 Podman Docker 仿真。请在设置中切换到 Podman。';
|
||||
}
|
||||
|
||||
/// The translations for Chinese, as used in Taiwan (`zh_TW`).
|
||||
@@ -1931,4 +1935,8 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
|
||||
@override
|
||||
String get logs => '日誌';
|
||||
|
||||
@override
|
||||
String get podmanDockerEmulationDetected =>
|
||||
'檢測到 Podman Docker 仿真。請在設定中切換到 Podman。';
|
||||
}
|
||||
|
||||
@@ -107,7 +107,6 @@ class SpiAdapter extends TypeAdapter<Spi> {
|
||||
alterUrl: fields[7] as String?,
|
||||
autoConnect: fields[8] == null ? true : fields[8] as bool,
|
||||
jumpId: fields[9] as String?,
|
||||
jumpChainIds: (fields[16] as List?)?.cast<String>(),
|
||||
custom: fields[10] as ServerCustom?,
|
||||
wolCfg: fields[11] as WakeOnLanCfg?,
|
||||
envs: (fields[12] as Map?)?.cast<String, String>(),
|
||||
@@ -120,7 +119,7 @@ class SpiAdapter extends TypeAdapter<Spi> {
|
||||
@override
|
||||
void write(BinaryWriter writer, Spi obj) {
|
||||
writer
|
||||
..writeByte(17)
|
||||
..writeByte(16)
|
||||
..writeByte(0)
|
||||
..write(obj.name)
|
||||
..writeByte(1)
|
||||
@@ -152,9 +151,7 @@ class SpiAdapter extends TypeAdapter<Spi> {
|
||||
..writeByte(14)
|
||||
..write(obj.customSystemType)
|
||||
..writeByte(15)
|
||||
..write(obj.disabledCmdTypes)
|
||||
..writeByte(16)
|
||||
..write(obj.jumpChainIds);
|
||||
..write(obj.disabledCmdTypes);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -27,7 +27,7 @@ types:
|
||||
index: 4
|
||||
Spi:
|
||||
typeId: 3
|
||||
nextIndex: 17
|
||||
nextIndex: 16
|
||||
fields:
|
||||
name:
|
||||
index: 0
|
||||
@@ -61,8 +61,6 @@ types:
|
||||
index: 14
|
||||
disabledCmdTypes:
|
||||
index: 15
|
||||
jumpChainIds:
|
||||
index: 16
|
||||
VirtKey:
|
||||
typeId: 4
|
||||
nextIndex: 45
|
||||
|
||||
@@ -294,5 +294,6 @@
|
||||
"write": "Schreiben",
|
||||
"writeScriptFailTip": "Das Schreiben des Skripts ist fehlgeschlagen, möglicherweise aufgrund fehlender Berechtigungen oder das Verzeichnis existiert nicht.",
|
||||
"writeScriptTip": "Nach der Verbindung mit dem Server wird ein Skript in `~/.config/server_box` \n | `/tmp/server_box` geschrieben, um den Systemstatus zu überwachen. Sie können den Skriptinhalt überprüfen.",
|
||||
"logs": "Protokolle"
|
||||
"logs": "Protokolle",
|
||||
"podmanDockerEmulationDetected": "Podman Docker-Emulation erkannt. Bitte wechseln Sie in den Einstellungen zu Podman."
|
||||
}
|
||||
|
||||
@@ -304,5 +304,6 @@
|
||||
"menuGitHubRepository": "GitHub Repository",
|
||||
"menuWiki": "Wiki",
|
||||
"menuHelp": "Help",
|
||||
"logs": "Logs"
|
||||
"logs": "Logs",
|
||||
"podmanDockerEmulationDetected": "Podman Docker emulation detected. Please switch to Podman in settings."
|
||||
}
|
||||
|
||||
@@ -294,5 +294,6 @@
|
||||
"write": "Escribir",
|
||||
"writeScriptFailTip": "La escritura en el script falló, posiblemente por falta de permisos o porque el directorio no existe.",
|
||||
"writeScriptTip": "Después de conectarse al servidor, se escribirá un script en `~/.config/server_box` \n | `/tmp/server_box` para monitorear el estado del sistema. Puedes revisar el contenido del script.",
|
||||
"logs": "Registros"
|
||||
"logs": "Registros",
|
||||
"podmanDockerEmulationDetected": "Detectada emulación de Podman Docker. Por favor, cambie a Podman en la configuración."
|
||||
}
|
||||
|
||||
@@ -294,5 +294,6 @@
|
||||
"write": "Écrire",
|
||||
"writeScriptFailTip": "Échec de l'écriture dans le script, probablement en raison d'un manque de permissions ou que le répertoire n'existe pas.",
|
||||
"writeScriptTip": "Après la connexion au serveur, un script sera écrit dans `~/.config/server_box` \n | `/tmp/server_box` pour surveiller l'état du système. Vous pouvez examiner le contenu du script.",
|
||||
"logs": "Journaux"
|
||||
"logs": "Journaux",
|
||||
"podmanDockerEmulationDetected": "Émulation Podman Docker détectée. Veuillez passer à Podman dans les paramètres."
|
||||
}
|
||||
|
||||
@@ -294,5 +294,6 @@
|
||||
"write": "Tulis",
|
||||
"writeScriptFailTip": "Penulisan ke skrip gagal, mungkin karena tidak ada izin atau direktori tidak ada.",
|
||||
"writeScriptTip": "Setelah terhubung ke server, sebuah skrip akan ditulis ke `~/.config/server_box` \n | `/tmp/server_box` untuk memantau status sistem. Anda dapat meninjau konten skrip tersebut.",
|
||||
"logs": "Log"
|
||||
"logs": "Log",
|
||||
"podmanDockerEmulationDetected": "Emulasi Podman Docker terdeteksi. Silakan beralih ke Podman di pengaturan."
|
||||
}
|
||||
|
||||
@@ -294,5 +294,6 @@
|
||||
"write": "書き込み",
|
||||
"writeScriptFailTip": "スクリプトの書き込みに失敗しました。権限がないかディレクトリが存在しない可能性があります。",
|
||||
"writeScriptTip": "サーバーへの接続後、システムステータスを監視するスクリプトが `~/.config/server_box` \n | `/tmp/server_box` に書き込まれます。スクリプトの内容を確認できます。",
|
||||
"logs": "ログ"
|
||||
"logs": "ログ",
|
||||
"podmanDockerEmulationDetected": "Podman Docker エミュレーションが検出されました。設定で Podman に切り替えてください。"
|
||||
}
|
||||
|
||||
@@ -294,5 +294,6 @@
|
||||
"write": "Schrijven",
|
||||
"writeScriptFailTip": "Het schrijven naar het script is mislukt, mogelijk door gebrek aan rechten of omdat de map niet bestaat.",
|
||||
"writeScriptTip": "Na het verbinden met de server wordt een script geschreven naar `~/.config/server_box` \n | `/tmp/server_box` om de systeemstatus te monitoren. U kunt de inhoud van het script controleren.",
|
||||
"logs": "Logboeken"
|
||||
"logs": "Logboeken",
|
||||
"podmanDockerEmulationDetected": "Podman Docker-emulatie gedetecteerd. Schakel over naar Podman in de instellingen."
|
||||
}
|
||||
|
||||
@@ -294,5 +294,6 @@
|
||||
"write": "Escrita",
|
||||
"writeScriptFailTip": "Falha ao escrever no script, possivelmente devido à falta de permissões ou o diretório não existe.",
|
||||
"writeScriptTip": "Após conectar ao servidor, um script será escrito em `~/.config/server_box` \n | `/tmp/server_box` para monitorar o status do sistema. Você pode revisar o conteúdo do script.",
|
||||
"logs": "Logs"
|
||||
"logs": "Logs",
|
||||
"podmanDockerEmulationDetected": "Emulação Podman Docker detectada. Por favor, alterne para Podman nas configurações."
|
||||
}
|
||||
|
||||
@@ -294,5 +294,6 @@
|
||||
"write": "Запись",
|
||||
"writeScriptFailTip": "Запись скрипта не удалась, возможно, из-за отсутствия прав или потому что, директории не существует.",
|
||||
"writeScriptTip": "После подключения к серверу скрипт будет записан в `~/.config/server_box` \n | `/tmp/server_box` для мониторинга состояния системы. Вы можете проверить содержимое скрипта.",
|
||||
"logs": "Журналы"
|
||||
"logs": "Журналы",
|
||||
"podmanDockerEmulationDetected": "Обнаружена эмуляция Podman Docker. Пожалуйста, переключитесь на Podman в настройках."
|
||||
}
|
||||
|
||||
@@ -294,5 +294,6 @@
|
||||
"write": "Yaz",
|
||||
"writeScriptFailTip": "Betik yazma başarısız oldu, muhtemelen izin eksikliği veya dizin mevcut değil.",
|
||||
"writeScriptTip": "Sunucuya bağlandıktan sonra, sistem durumunu izlemek için `~/.config/server_box` \n | `/tmp/server_box` dizinine bir betik yazılacak. Betik içeriğini inceleyebilirsiniz.",
|
||||
"logs": "Günlükler"
|
||||
"logs": "Günlükler",
|
||||
"podmanDockerEmulationDetected": "Podman Docker emülasyonu tespit edildi. Lütfen ayarlarda Podman'a geçin."
|
||||
}
|
||||
|
||||
@@ -294,5 +294,6 @@
|
||||
"write": "Записати",
|
||||
"writeScriptFailTip": "Запис у скрипт не вдався, можливо, через брак дозволів або каталог не існує.",
|
||||
"writeScriptTip": "Після підключення до сервера скрипт буде записано у `~/.config/server_box` \n | `/tmp/server_box` для моніторингу стану системи. Ви можете переглянути вміст скрипта.",
|
||||
"logs": "Журнали"
|
||||
"logs": "Журнали",
|
||||
"podmanDockerEmulationDetected": "Виявлено емуляцію Podman Docker. Будь ласка, переключіться на Podman у налаштуваннях."
|
||||
}
|
||||
|
||||
@@ -301,5 +301,6 @@
|
||||
"menuGitHubRepository": "GitHub 仓库",
|
||||
"menuWiki": "Wiki",
|
||||
"menuHelp": "帮助",
|
||||
"logs": "日志"
|
||||
"logs": "日志",
|
||||
"podmanDockerEmulationDetected": "检测到 Podman Docker 仿真。请在设置中切换到 Podman。"
|
||||
}
|
||||
|
||||
@@ -294,5 +294,6 @@
|
||||
"write": "寫入",
|
||||
"writeScriptFailTip": "寫入腳本失敗,可能是沒有權限/目錄不存在等。",
|
||||
"writeScriptTip": "連線到伺服器後,將會在 `~/.config/server_box` \n | `/tmp/server_box` 中寫入一個腳本來監測系統狀態。你可以審查腳本內容。",
|
||||
"logs": "日誌"
|
||||
"logs": "日誌",
|
||||
"podmanDockerEmulationDetected": "檢測到 Podman Docker 仿真。請在設定中切換到 Podman。"
|
||||
}
|
||||
|
||||
@@ -234,7 +234,7 @@ class _ContainerPageState extends ConsumerState<ContainerPage> {
|
||||
if (item.cpu == null || item.mem == null) return UIs.placeholder;
|
||||
return LayoutBuilder(
|
||||
builder: (_, cons) {
|
||||
final width = cons.maxWidth / 2 - 41;
|
||||
final width = cons.maxWidth / 2 - 6.5;
|
||||
return Column(
|
||||
children: [
|
||||
UIs.height13,
|
||||
@@ -264,10 +264,17 @@ class _ContainerPageState extends ConsumerState<ContainerPage> {
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 12, color: Colors.grey),
|
||||
UIs.width7,
|
||||
Text(value ?? l10n.unknown, style: UIs.text11Grey),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value ?? l10n.unknown,
|
||||
style: UIs.text11Grey,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -222,30 +222,6 @@ extension _Actions on _ServerEditPageState {
|
||||
return;
|
||||
}
|
||||
|
||||
final oldSpi = this.spi;
|
||||
if (oldSpi != null) {
|
||||
final originalJumpChain = oldSpi.jumpChainIds ?? (oldSpi.jumpId == null ? const <String>[] : [oldSpi.jumpId!]);
|
||||
final currentJumpChain = _jumpChain.value;
|
||||
|
||||
final jumpChainChanged = () {
|
||||
if (originalJumpChain.isEmpty && currentJumpChain.isEmpty) return false;
|
||||
if (originalJumpChain.length != currentJumpChain.length) return true;
|
||||
for (var i = 0; i < originalJumpChain.length; i++) {
|
||||
if (originalJumpChain[i] != currentJumpChain[i]) return true;
|
||||
}
|
||||
return false;
|
||||
}();
|
||||
|
||||
if (jumpChainChanged) {
|
||||
final ok = await context.showRoundDialog<bool>(
|
||||
title: libL10n.attention,
|
||||
child: Text(libL10n.askContinue('${l10n.jumpServer} ${libL10n.setting}')),
|
||||
actions: Btnx.cancelOk,
|
||||
);
|
||||
if (ok != true) return;
|
||||
}
|
||||
}
|
||||
|
||||
if (_keyIdx.value == null && _passwordController.text.isEmpty) {
|
||||
final ok = await context.showRoundDialog<bool>(
|
||||
title: libL10n.attention,
|
||||
@@ -301,8 +277,7 @@ extension _Actions on _ServerEditPageState {
|
||||
tags: _tags.value.isEmpty ? null : _tags.value.toList(),
|
||||
alterUrl: _altUrlController.text.selfNotEmptyOrNull,
|
||||
autoConnect: _autoConnect.value,
|
||||
jumpId: null,
|
||||
jumpChainIds: _jumpChain.value.isEmpty ? null : _jumpChain.value,
|
||||
jumpId: _jumpServer.value,
|
||||
custom: custom,
|
||||
wolCfg: wol,
|
||||
envs: _env.value.isEmpty ? null : _env.value,
|
||||
@@ -446,7 +421,7 @@ extension _Utils on _ServerEditPageState {
|
||||
|
||||
_altUrlController.text = spi.alterUrl ?? '';
|
||||
_autoConnect.value = spi.autoConnect;
|
||||
_jumpChain.value = spi.jumpChainIds ?? (spi.jumpId == null ? const <String>[] : [spi.jumpId!]);
|
||||
_jumpServer.value = spi.jumpId;
|
||||
|
||||
final custom = spi.custom;
|
||||
if (custom != null) {
|
||||
|
||||
@@ -25,7 +25,6 @@ import 'package:server_box/view/page/private_key/edit.dart';
|
||||
import 'package:server_box/view/page/server/discovery/discovery.dart';
|
||||
|
||||
part 'actions.dart';
|
||||
part 'jump_chain.dart';
|
||||
part 'widget.dart';
|
||||
|
||||
class ServerEditPage extends ConsumerStatefulWidget {
|
||||
@@ -67,7 +66,7 @@ class _ServerEditPageState extends ConsumerState<ServerEditPage> with AfterLayou
|
||||
/// -1: non selected, null: password, others: index of private key
|
||||
final _keyIdx = ValueNotifier<int?>(null);
|
||||
final _autoConnect = ValueNotifier(true);
|
||||
final _jumpChain = <String>[].vn;
|
||||
final _jumpServer = nvn<String?>();
|
||||
final _pveIgnoreCert = ValueNotifier(false);
|
||||
final _env = <String, String>{}.vn;
|
||||
final _customCmds = <String, String>{}.vn;
|
||||
@@ -101,7 +100,7 @@ class _ServerEditPageState extends ConsumerState<ServerEditPage> with AfterLayou
|
||||
|
||||
_keyIdx.dispose();
|
||||
_autoConnect.dispose();
|
||||
_jumpChain.dispose();
|
||||
_jumpServer.dispose();
|
||||
_pveIgnoreCert.dispose();
|
||||
_env.dispose();
|
||||
_customCmds.dispose();
|
||||
@@ -200,6 +199,7 @@ class _ServerEditPageState extends ConsumerState<ServerEditPage> with AfterLayou
|
||||
),
|
||||
_buildAuth(),
|
||||
_buildSystemType(),
|
||||
_buildJumpServer(),
|
||||
_buildMore(),
|
||||
];
|
||||
return AutoMultiList(children: children);
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
part of 'edit.dart';
|
||||
|
||||
extension _JumpChain on _ServerEditPageState {
|
||||
Widget _buildJumpChain() {
|
||||
final serversState = ref.watch(serversProvider);
|
||||
final servers = serversState.servers;
|
||||
final selfId = spi?.id;
|
||||
|
||||
if (selfId == null) {
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.map),
|
||||
title: Text(l10n.jumpServer),
|
||||
subtitle: Text(libL10n.empty, style: UIs.textGrey),
|
||||
).cardx;
|
||||
}
|
||||
|
||||
String serverNameOrId(String id) {
|
||||
return servers[id]?.name ?? id;
|
||||
}
|
||||
|
||||
List<String> flattenHopIds(String id, {required Set<String> visited}) {
|
||||
if (!visited.add(id)) return const <String>[];
|
||||
final spi = servers[id];
|
||||
if (spi == null) return const <String>[];
|
||||
|
||||
final hops = spi.jumpChainIds;
|
||||
if (hops == null || hops.isEmpty) return const <String>[];
|
||||
|
||||
final flat = <String>[];
|
||||
for (final hopId in hops) {
|
||||
flat.add(hopId);
|
||||
flat.addAll(flattenHopIds(hopId, visited: visited));
|
||||
}
|
||||
return flat;
|
||||
}
|
||||
|
||||
bool containsCycleWithCandidate(String candidateId) {
|
||||
final queue = [..._jumpChain.value, candidateId];
|
||||
|
||||
final directVisited = <String>{selfId};
|
||||
for (final hopId in queue) {
|
||||
if (hopId == selfId) return true;
|
||||
if (!directVisited.add(hopId)) return true;
|
||||
}
|
||||
|
||||
for (final hopId in queue) {
|
||||
final extra = flattenHopIds(hopId, visited: <String>{selfId});
|
||||
for (final id in extra) {
|
||||
if (id == selfId) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
String? buildTextNearToFar() {
|
||||
if (_jumpChain.value.isEmpty) return null;
|
||||
final flat = <String>[];
|
||||
final visited = <String>{selfId};
|
||||
for (final hopId in _jumpChain.value) {
|
||||
flat.add(hopId);
|
||||
flat.addAll(flattenHopIds(hopId, visited: visited));
|
||||
}
|
||||
final names = flat.map(serverNameOrId).toList();
|
||||
if (names.isEmpty) return null;
|
||||
return names.join(' → ');
|
||||
}
|
||||
|
||||
String? buildTextFarToNear() {
|
||||
final text = buildTextNearToFar();
|
||||
if (text == null) return null;
|
||||
return text.split(' → ').reversed.join(' → ');
|
||||
}
|
||||
|
||||
return _jumpChain.listenVal((_) {
|
||||
final nearToFar2 = buildTextNearToFar();
|
||||
final farToNear2 = buildTextFarToNear();
|
||||
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.map),
|
||||
title: Text(l10n.jumpServer),
|
||||
subtitle: (nearToFar2 == null)
|
||||
? Text(libL10n.empty, style: UIs.textGrey)
|
||||
: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('${l10n.route}: $nearToFar2', style: UIs.textGrey),
|
||||
Text('${libL10n.path}: $farToNear2', style: UIs.textGrey),
|
||||
],
|
||||
),
|
||||
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||
onTap: () async {
|
||||
if (serversState.serverOrder.isEmpty) {
|
||||
context.showSnackBar(libL10n.empty);
|
||||
return;
|
||||
}
|
||||
|
||||
final candidates = serversState.serverOrder.where((e) => e != selfId).toList();
|
||||
if (candidates.isEmpty) {
|
||||
context.showSnackBar(libL10n.empty);
|
||||
return;
|
||||
}
|
||||
|
||||
// Add a hop
|
||||
final nextHop = await context.showPickSingleDialog<String>(
|
||||
title: '${l10n.jumpServer} (+1)',
|
||||
items: candidates.where((id) => !containsCycleWithCandidate(id)).toList(),
|
||||
display: serverNameOrId,
|
||||
clearable: true,
|
||||
);
|
||||
if (nextHop == null) return;
|
||||
|
||||
_jumpChain.value = [..._jumpChain.value, nextHop];
|
||||
|
||||
// If user wants to manage order/remove, offer a simple editor dialog
|
||||
await context.showRoundDialog<void>(
|
||||
title: l10n.jumpServer,
|
||||
child: SizedBox(
|
||||
width: 320,
|
||||
child: _jumpChain.listenVal((hops) {
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: hops.length,
|
||||
itemBuilder: (context, index) {
|
||||
final id = hops[index];
|
||||
return ListTile(
|
||||
title: Text(serverNameOrId(id)),
|
||||
subtitle: Text(id, style: UIs.textGrey),
|
||||
trailing: Wrap(
|
||||
spacing: 4,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_upward, size: 18),
|
||||
onPressed: index == 0
|
||||
? null
|
||||
: () {
|
||||
final list = [..._jumpChain.value];
|
||||
final tmp = list[index - 1];
|
||||
list[index - 1] = list[index];
|
||||
list[index] = tmp;
|
||||
_jumpChain.value = list;
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_downward, size: 18),
|
||||
onPressed: index == hops.length - 1
|
||||
? null
|
||||
: () {
|
||||
final list = [..._jumpChain.value];
|
||||
final tmp = list[index + 1];
|
||||
list[index + 1] = list[index];
|
||||
list[index] = tmp;
|
||||
_jumpChain.value = list;
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete, size: 18),
|
||||
onPressed: () {
|
||||
final list = [..._jumpChain.value]..removeAt(index);
|
||||
_jumpChain.value = list;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
actions: Btnx.oks,
|
||||
);
|
||||
},
|
||||
).cardx;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -132,7 +132,6 @@ extension _Widgets on _ServerEditPageState {
|
||||
return ExpandTile(
|
||||
title: Text(l10n.more),
|
||||
children: [
|
||||
_buildJumpChain(),
|
||||
Input(
|
||||
controller: _logoUrlCtrl,
|
||||
type: TextInputType.url,
|
||||
@@ -348,6 +347,48 @@ extension _Widgets on _ServerEditPageState {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildJumpServer() {
|
||||
const padding = EdgeInsets.only(left: 13, right: 13, bottom: 7);
|
||||
final srvs = ref
|
||||
.watch(serversProvider)
|
||||
.servers
|
||||
.values
|
||||
.where((e) => e.jumpId == null)
|
||||
.where((e) => e.id != spi?.id)
|
||||
.toList();
|
||||
final choice = _jumpServer.listenVal((val) {
|
||||
final srv = srvs.firstWhereOrNull((e) => e.id == _jumpServer.value);
|
||||
return Choice<Spi>(
|
||||
multiple: false,
|
||||
clearable: true,
|
||||
value: srv != null ? [srv] : [],
|
||||
builder: (state, _) => Wrap(
|
||||
children: List<Widget>.generate(srvs.length, (index) {
|
||||
final item = srvs[index];
|
||||
return ChoiceChipX<Spi>(
|
||||
label: item.name,
|
||||
state: state,
|
||||
value: item,
|
||||
onSelected: (srv, on) {
|
||||
if (on) {
|
||||
_jumpServer.value = srv.id;
|
||||
} else {
|
||||
_jumpServer.value = null;
|
||||
}
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
return ExpandTile(
|
||||
leading: const Icon(Icons.map),
|
||||
initiallyExpanded: _jumpServer.value != null,
|
||||
childrenPadding: padding,
|
||||
title: Text(l10n.jumpServer),
|
||||
children: [choice],
|
||||
).cardx;
|
||||
}
|
||||
|
||||
Widget _buildWriteScriptTip() {
|
||||
return Btn.tile(
|
||||
|
||||
@@ -49,11 +49,12 @@ extension _Operation on _ServerPageState {
|
||||
await context.showRoundDialog(title: libL10n.attention, child: Text(l10n.suspendTip));
|
||||
Stores.setting.showSuspendTip.put(false);
|
||||
}
|
||||
srv.client?.execWithPwd(
|
||||
await srv.client?.execWithPwd(
|
||||
ShellFunc.suspend.exec(srv.spi.id, systemType: srv.status.system, customDir: null),
|
||||
context: context,
|
||||
id: srv.id,
|
||||
);
|
||||
) ??
|
||||
(null, '');
|
||||
},
|
||||
typ: l10n.suspend,
|
||||
name: srv.spi.name,
|
||||
@@ -62,11 +63,13 @@ extension _Operation on _ServerPageState {
|
||||
|
||||
void _onTapShutdown(ServerState srv) {
|
||||
_askFor(
|
||||
func: () => srv.client?.execWithPwd(
|
||||
ShellFunc.shutdown.exec(srv.spi.id, systemType: srv.status.system, customDir: null),
|
||||
context: context,
|
||||
id: srv.id,
|
||||
),
|
||||
func: () async {
|
||||
await srv.client?.execWithPwd(
|
||||
ShellFunc.shutdown.exec(srv.spi.id, systemType: srv.status.system, customDir: null),
|
||||
context: context,
|
||||
id: srv.id,
|
||||
);
|
||||
},
|
||||
typ: l10n.shutdown,
|
||||
name: srv.spi.name,
|
||||
);
|
||||
@@ -74,11 +77,14 @@ extension _Operation on _ServerPageState {
|
||||
|
||||
void _onTapReboot(ServerState srv) {
|
||||
_askFor(
|
||||
func: () => srv.client?.execWithPwd(
|
||||
ShellFunc.reboot.exec(srv.spi.id, systemType: srv.status.system, customDir: null),
|
||||
context: context,
|
||||
id: srv.id,
|
||||
),
|
||||
func: () async {
|
||||
await srv.client?.execWithPwd(
|
||||
ShellFunc.reboot.exec(srv.spi.id, systemType: srv.status.system, customDir: null),
|
||||
context: context,
|
||||
id: srv.id,
|
||||
) ??
|
||||
(null, '');
|
||||
},
|
||||
typ: l10n.reboot,
|
||||
name: srv.spi.name,
|
||||
);
|
||||
|
||||
@@ -355,7 +355,7 @@ class SSHPageState extends ConsumerState<SSHPage>
|
||||
onTapUp: (_) => _virtKeyLongPressTimer?.cancel(),
|
||||
child: SizedBox(
|
||||
width: virtKeyWidth,
|
||||
height: _virtKeysHeight / _virtKeysList.length,
|
||||
height: _horizonVirtKeys ? _virtKeysHeight : _virtKeysHeight / _virtKeysList.length,
|
||||
child: Center(child: child),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -53,7 +53,7 @@ class _SftpMissionPageState extends ConsumerState<SftpMissionPage> {
|
||||
return switch (status.status) {
|
||||
const (SftpWorkerStatus.finished) => _buildFinished(status),
|
||||
const (SftpWorkerStatus.loading) => _buildLoading(status),
|
||||
const (SftpWorkerStatus.sshConnected) => _buildConnected(status),
|
||||
const (SftpWorkerStatus.sshConnectted) => _buildConnected(status),
|
||||
const (SftpWorkerStatus.preparing) => _buildPreparing(status),
|
||||
_ => _buildDefault(status),
|
||||
};
|
||||
|
||||
@@ -471,7 +471,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1291;
|
||||
CURRENT_PROJECT_VERSION = 1297;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Server Box";
|
||||
@@ -481,7 +481,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||
MARKETING_VERSION = 1.0.1291;
|
||||
MARKETING_VERSION = 1.0.1297;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||
PRODUCT_NAME = "Server Box";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -608,7 +608,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1291;
|
||||
CURRENT_PROJECT_VERSION = 1297;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Server Box";
|
||||
@@ -618,7 +618,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||
MARKETING_VERSION = 1.0.1291;
|
||||
MARKETING_VERSION = 1.0.1297;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||
PRODUCT_NAME = "Server Box";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -638,7 +638,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "3rd Party Mac Developer Application";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1291;
|
||||
CURRENT_PROJECT_VERSION = 1297;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = BA88US33G6;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -649,7 +649,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||
MARKETING_VERSION = 1.0.1291;
|
||||
MARKETING_VERSION = 1.0.1297;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||
PRODUCT_NAME = "Server Box";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: server_box
|
||||
description: server status & toolbox app.
|
||||
publish_to: "none"
|
||||
version: 1.0.1291+1291
|
||||
version: 1.0.1297+1297
|
||||
|
||||
environment:
|
||||
sdk: ">=3.9.0"
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:server_box/core/utils/server.dart';
|
||||
import 'package:server_box/data/model/app/error.dart';
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
|
||||
void main() {
|
||||
group('Jump server', () {
|
||||
test('resolveMergedJumpChain throws when injected chain misses jump server', () {
|
||||
const spi = Spi(
|
||||
name: 'target',
|
||||
ip: '10.0.0.10',
|
||||
port: 22,
|
||||
user: 'root',
|
||||
id: 't',
|
||||
jumpId: 'missing',
|
||||
);
|
||||
|
||||
expect(
|
||||
() => resolveMergedJumpChain(spi, jumpChain: const <Spi>[]),
|
||||
throwsA(
|
||||
isA<SSHErr>().having(
|
||||
(e) => e.type,
|
||||
'type',
|
||||
SSHErrType.connect,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('resolveMergedJumpChain merges and dedups', () {
|
||||
const c = Spi(name: 'c', ip: '10.0.0.30', port: 22, user: 'root', id: 'c');
|
||||
const d = Spi(name: 'd', ip: '10.0.0.40', port: 22, user: 'root', id: 'd');
|
||||
const b = Spi(
|
||||
name: 'b',
|
||||
ip: '10.0.0.20',
|
||||
port: 22,
|
||||
user: 'root',
|
||||
id: 'b',
|
||||
jumpChainIds: ['c', 'd'],
|
||||
);
|
||||
const target = Spi(
|
||||
name: 'target',
|
||||
ip: '10.0.0.10',
|
||||
port: 22,
|
||||
user: 'root',
|
||||
id: 't',
|
||||
jumpChainIds: ['b', 'c'],
|
||||
);
|
||||
|
||||
final chain = resolveMergedJumpChain(target, jumpChain: const <Spi>[b, c, d]);
|
||||
expect(chain.map((e) => e.id).toList(), ['c', 'd', 'b']);
|
||||
});
|
||||
|
||||
test('resolveMergedJumpChain detects jump loop', () {
|
||||
const b = Spi(
|
||||
name: 'b',
|
||||
ip: '10.0.0.20',
|
||||
port: 22,
|
||||
user: 'root',
|
||||
id: 'b',
|
||||
jumpChainIds: ['c'],
|
||||
);
|
||||
const c = Spi(
|
||||
name: 'c',
|
||||
ip: '10.0.0.30',
|
||||
port: 22,
|
||||
user: 'root',
|
||||
id: 'c',
|
||||
jumpChainIds: ['b'],
|
||||
);
|
||||
const target = Spi(
|
||||
name: 'target',
|
||||
ip: '10.0.0.10',
|
||||
port: 22,
|
||||
user: 'root',
|
||||
id: 't',
|
||||
jumpChainIds: ['b'],
|
||||
);
|
||||
|
||||
expect(
|
||||
() => resolveMergedJumpChain(target, jumpChain: const <Spi>[b, c]),
|
||||
throwsA(
|
||||
isA<SSHErr>().having(
|
||||
(e) => e.type,
|
||||
'type',
|
||||
SSHErrType.connect,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user