// workflow.dart -- This file is part of tiny_computer. // Copyright (C) 2023 Caten Hu // Tiny Computer is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published // by the Free Software Foundation, either version 3 of the License, // or any later version. // Tiny Computer is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty // of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // See the GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with this program. If not, see http://www.gnu.org/licenses/. import 'dart:io'; import 'dart:async'; import 'dart:convert'; import 'dart:math'; import 'package:intl/intl.dart'; import 'package:http/http.dart' as http; import 'package:retry/retry.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:webview_flutter/webview_flutter.dart'; import 'package:xterm/xterm.dart'; import 'package:flutter_pty/flutter_pty.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:unity_ads_plugin/unity_ads_plugin.dart'; class Util { static Future copyAsset(String src, String dst) async { await File(dst).writeAsBytes((await rootBundle.load(src)).buffer.asUint8List()); } static Future copyAsset2(String src, String dst) async { ByteData data = await rootBundle.load(src); await File(dst).writeAsBytes(data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes)); } static void createDirFromString(String dir) { Directory.fromRawPath(const Utf8Encoder().convert(dir)).createSync(recursive: true); } static Future execute(String str) async { Pty pty = Pty.start( "/system/bin/sh" ); pty.write(const Utf8Encoder().convert("$str\nexit\n")); await pty.exitCode; } static void termWrite(String str) { G.termPtys[G.currentContainer]!.pty.write(const Utf8Encoder().convert("$str\n")); } static dynamic getCurrentProp(String key) { return jsonDecode(G.prefs.getStringList("containersInfo")![G.currentContainer])[key]; } //用来设置name, boot, vnc, vncUrl等 static Future setCurrentProp(String key, dynamic value) async { await G.prefs.setStringList("containersInfo", G.prefs.getStringList("containersInfo")!..setAll(G.currentContainer, [jsonEncode((jsonDecode( G.prefs.getStringList("containersInfo")![G.currentContainer] ))..update(key, (v) => value))] ) ); } //返回单个G.bonusTable定义的item static Map getRandomBonus() { final random = Random(); final totalWeight = G.bonusTable.fold(0.0, (sum, item) => sum + item['weight']); final randomIndex = random.nextDouble() * totalWeight; var cumulativeWeight = 0.0; for (final item in G.bonusTable) { cumulativeWeight += item['weight']; if (randomIndex <= cumulativeWeight) { return item; } } return G.bonusTable[0]; } //由getRandomBonus返回的数据 static Future applyBonus(Map bonus) async { bool flag = false; List ret = G.prefs.getStringList("adsBonus")!.map((e) { Map item = jsonDecode(e); return (item["name"] == bonus["name"])? jsonEncode(item..update("amount", (v) { flag = true; return v + bonus["amount"]; })):e; }).toList(); if (!flag) { ret.add("""{"name": "${bonus["name"]}", "amount": ${bonus["amount"]}}"""); } await G.prefs.setStringList("adsBonus", ret); print(G.prefs.getStringList("adsBonus")!); } } //一个结合terminal和pty的类 class TermPty { late final Terminal terminal; late final Pty pty; TermPty() { terminal = Terminal(); pty = Pty.start( "/system/bin/sh", workingDirectory: G.dataPath, columns: terminal.viewWidth, rows: terminal.viewHeight, ); pty.output .cast>() .transform(const Utf8Decoder()) .listen(terminal.write); pty.exitCode.then((code) { terminal.write('the process exited with exit code $code'); if (code == 0) { SystemChannels.platform.invokeMethod("SystemNavigator.pop"); } //TODO: Singal 9 hint, 改成对话框 if (code == -9) { Navigator.push(G.homePageStateContext, MaterialPageRoute(builder: (context) { const TextStyle ts = TextStyle(fontSize: 16, color: Colors.white, fontWeight: FontWeight.normal); return const Scaffold(backgroundColor: Colors.deepPurple, body: Center( child: Scrollbar(child: SingleChildScrollView( child: Column(children: [ Text("发生了什么?", textScaleFactor: 2, style: ts, textAlign: TextAlign.center,), Text("终端异常退出, 返回错误码9\n此错误通常是高版本安卓系统(12+)限制进程造成的, \n可以使用以下工具修复:", style: ts, textAlign: TextAlign.center), SelectableText("https://www.vmos.cn/zhushou.htm", style: ts, textAlign: TextAlign.center), Text("(复制链接到浏览器查看)", style: ts, textAlign: TextAlign.center), ]), ) ) )); })); } }); terminal.onOutput = (data) { if (G.prefs.getBool("isTerminalWriteEnabled")!) { pty.write(const Utf8Encoder().convert(data)); } }; terminal.onResize = (w, h, pw, ph) { pty.resize(h, w); }; } } // Global variables class G { static late final String dataPath; static Pty? audioPty; static late WebViewController controller; static late BuildContext homePageStateContext; static late int currentContainer; //目前运行第几个容器 static late Map termPtys; //为容器存放TermPty数据 static late AdManager ads;//广告实例 //看广告可以获得的奖励。 //weight抽奖权重,singleUse使用一次花费的数量,amount抽中可以获得的数量 static const List> bonusTable = [ {"name": "开发者的祝福", "subtitle": "支持开发者的证明", "description": "(*'v'*)\n开发者由衷地感谢你!", "weight": 10000, "amount": 1, "singleUse": 0}, {"name": "记忆晶片", "subtitle": "看上去像平行四边形", "description": "组成记忆空间的基本元素。\n是从哪里掉下来的呢?", "weight": 50, "amount": 1, "singleUse": 0}, {"name": "Wishes Flower Part", "subtitle": "为1个人献上祝福", "description": "希望之花的花瓣。在想好为谁祝福后, 点击使用", "weight": 500, "amount": 1, "singleUse": 1}, {"name": "Wishes Flower Part", "subtitle": "为1个人献上祝福", "description": "希望之花的花瓣。在想好为谁祝福后, 点击使用", "weight": 100, "amount": 3, "singleUse": 1}, {"name": "Wishes Flower", "subtitle": "为3个人献上祝福", "description": "希望之花。在想好为谁祝福后, 点击使用", "weight": 50, "amount": 1, "singleUse": 1}, {"name": "Bonus Reward", "subtitle": "会有极好的事情发生", "description": "来自记忆空间的传说。\n使用后一天内必有极好的事情...\n就是你想象的那种事情...\n就会发生。\n不过, 大概只是个传说吧。", "weight": 10, "amount": 0.01, "singleUse": 1}, {"name": "Bonus Reward", "subtitle": "会有极好的事情发生", "description": "来自记忆空间的传说。\n使用后一天内必有极好的事情...\n就是你想象的那种事情...\n就会发生。\n不过, 大概只是个传说吧。", "weight": 1, "amount": 0.1, "singleUse": 1}, {"name": "Bonus Reward", "subtitle": "会有极好的事情发生", "description": "来自记忆空间的传说。\n使用后一天内必有极好的事情...\n就是你想象的那种事情...\n就会发生。\n不过, 大概只是个传说吧。", "weight": 1, "amount": 1, "singleUse": 1}, ]; //所有key //int defaultContainer = 0: 默认启动第0个容器 //String defaultAudioPort = 4713: 默认pulseaudio端口(为了避免和其它软件冲突改成4718了) !!!注意!这个值是String类型 //bool autoLaunchVnc = true: 是否自动启动VNC并跳转 //String lastDate: 上次启动软件的日期,yyyy-MM-dd //int adsWatchedToday: 今日视频广告观看数量 //int adsWatchedTotal: 视频广告观看数量 //bool isBannerAdsClosed = false //bool bannerAdsCanBeClosed = false 看一次视频广告永久开启,历史遗留 //bool isTerminalWriteEnabled = false //bool terminalWriteCanBeEnabled = false 看一次视频广告永久开启,历史遗留 //? int bootstrapVersion: 启动包版本 //String[] containersInfo: 所有容器信息(json) //{name, boot:"\$DATA_DIR/bin/proot ...", vnc:"startnovnc", vncUrl:"...", commands:[{name:"更新和升级", command:"apt update -y && apt upgrade -y"}, ...]} //String[] adsBonus: 观看广告获取的奖励(json) //{name: "xxx", amount: xxx} static late SharedPreferences prefs; } class AdManager { static Map placements = { interstitialVideoAdPlacementId: false, rewardedVideoAdPlacementId: false, }; static void loadAds() { for (var placementId in placements.keys) { loadAd(placementId); } } static void loadAd(String placementId) { UnityAds.load( placementId: placementId, onComplete: (placementId) { print('Load Complete $placementId'); placements[placementId] = true; }, onFailed: (placementId, error, message) => print('Load Failed $placementId: $error $message'), ); } static void showAd(String placementId, Function completeExtra, Function full) { if (G.prefs.getInt("adsWatchedToday")!>=5) { full(); return; } placements[placementId] = false; UnityAds.showVideoAd( placementId: placementId, onComplete: (placementId) async { print('Video Ad $placementId completed'); loadAd(placementId); await G.prefs.setInt("adsWatchedTotal", G.prefs.getInt("adsWatchedTotal")!+1); await G.prefs.setInt("adsWatchedToday", G.prefs.getInt("adsWatchedToday")!+1); completeExtra(); }, onFailed: (placementId, error, message) { print('Video Ad $placementId failed: $error $message'); loadAd(placementId); }, onStart: (placementId) => print('Video Ad $placementId started'), onClick: (placementId) => print('Video Ad $placementId click'), onSkipped: (placementId) { print('Video Ad $placementId skipped'); loadAd(placementId); }, ); } static String get gameId { if (defaultTargetPlatform == TargetPlatform.android) { return '5403132'; } if (defaultTargetPlatform == TargetPlatform.iOS) { return '5403133'; } return ''; } static String bannerAdPlacementId = 'Banner_Android'; static String interstitialVideoAdPlacementId = 'Interstitial_Android'; static String rewardedVideoAdPlacementId = 'Rewarded_Android'; } class Workflow { static Future grantPermissions() async { Permission.storage.request(); Permission.manageExternalStorage.request(); } static Future setupBootstrap() async { //用来共享数据文件的文件夹 Util.createDirFromString("${G.dataPath}/share"); //挂载到/dev/shm的文件夹 Util.createDirFromString("${G.dataPath}/tmp"); //给proot的tmp文件夹,虽然我不知道为什么proot要这个 Util.createDirFromString("${G.dataPath}/proot_tmp"); //解压后得到bin文件夹和libexec文件夹 //bin存放了proot和pulseaudio //libexec存放了proot loader await Util.copyAsset( "assets/assets.zip", "${G.dataPath}/assets.zip", ); //dddd await Util.copyAsset( "assets/busybox", "${G.dataPath}/busybox", ); await Util.execute( """ export DATA_DIR=${G.dataPath} cd \$DATA_DIR chmod +x busybox \$DATA_DIR/busybox unzip assets.zip chmod -R +x bin/* chmod -R +x libexec/proot/* chmod 1777 tmp ln -s \$DATA_DIR/busybox \$DATA_DIR/bin/xz \$DATA_DIR/busybox rm -rf assets.zip """); } //初次启动要做的事情 static Future initForFirstTime() async { //首先设置bootstrap await setupBootstrap(); //存放容器的文件夹0和存放硬链接的文件夹.l2s Util.createDirFromString("${G.dataPath}/containers/0/.l2s"); //这个是容器rootfs,被split命令分成了xa* //首次启动,就用这个,别让用户另选了 //TODO: 这个字符串列表太丑陋了 //for (String name in ["xaa", "xab", "xac", "xad", "xae", "xaf", "xag", "xah", "xai", "xaj", "xak", "xal", "xam", "xan"]) { for (String name in ["xaa", "xab", "xac", "xad", "xae", "xaf", "xag", "xah", "xai", "xaj", "xak", "xal", "xam"]) { await Util.copyAsset("assets/$name", "${G.dataPath}/$name"); } //-J await Util.execute( """ export DATA_DIR=${G.dataPath} export CONTAINER_DIR=\$DATA_DIR/containers/0 cd \$DATA_DIR export PATH=\$DATA_DIR/bin:\$PATH export PROOT_TMP_DIR=\$DATA_DIR/proot_tmp export PROOT_LOADER=\$DATA_DIR/libexec/proot/loader export PROOT_LOADER_32=\$DATA_DIR/libexec/proot/loader32 export PROOT_L2S_DIR=\$CONTAINER_DIR/.l2s \$DATA_DIR/bin/proot --link2symlink -H sh -c "cat xa* | \$DATA_DIR/bin/tar x -J --delay-directory-restore --preserve-permissions -v -C containers/0" #Script from proot-distro chmod u+rw "\$CONTAINER_DIR/etc/passwd" "\$CONTAINER_DIR/etc/shadow" "\$CONTAINER_DIR/etc/group" "\$CONTAINER_DIR/etc/gshadow" echo "aid_\$(id -un):x:\$(id -u):\$(id -g):Termux:/:/sbin/nologin" >> "\$CONTAINER_DIR/etc/passwd" echo "aid_\$(id -un):*:18446:0:99999:7:::" >> "\$CONTAINER_DIR/etc/shadow" id -Gn | tr ' ' '\\n' > tmp1 id -G | tr ' ' '\\n' > tmp2 \$DATA_DIR/busybox paste tmp1 tmp2 > tmp3 local group_name group_id cat tmp3 | while read -r group_name group_id; do echo "aid_\${group_name}:x:\${group_id}:root,aid_\$(id -un)" >> "\$CONTAINER_DIR/etc/group" if [ -f "\$CONTAINER_DIR/etc/gshadow" ]; then echo "aid_\${group_name}:*::root,aid_\$(id -un)" >> "\$CONTAINER_DIR/etc/gshadow" fi done \$DATA_DIR/busybox rm -rf xa* tmp1 tmp2 tmp3 """); //一些数据初始化 //$DATA_DIR是数据文件夹, $CONTAINER_DIR是容器根目录 //容器根目录会有一个fake-proc文件夹存放一些假的proc文件供挂载 //"boot":"\$DATA_DIR/bin/proot --link2symlink -H --kill-on-exit --tcsetsf2tcsetsw --root-id --pwd=/root --rootfs=\$CONTAINER_DIR -L --kernel-release=6.2.1-PRoot-Distro --bind=\$DATA_DIR/tmp:/dev/shm --bind=/sys --bind=/proc/self/fd/2:/dev/stderr --bind=/proc/self/fd/1:/dev/stdout --bind=/proc/self/fd/0:/dev/stdin --bind=/proc/self/fd:/dev/fd --bind=/proc --bind=/dev/urandom:/dev/random --bind=/dev --bind=\$CONTAINER_DIR/fake-proc/.loadavg:/proc/loadavg --bind=\$CONTAINER_DIR/fake-proc/.stat:/proc/stat --bind=\$CONTAINER_DIR/fake-proc/.uptime:/proc/uptime --bind=\$CONTAINER_DIR/fake-proc/.version:/proc/version --bind=\$CONTAINER_DIR/fake-proc/.vmstat:/proc/vmstat --bind=\$CONTAINER_DIR/fake-proc/.sysctl_entry_cap_last_cap:/proc/sys/kernel/cap_last_cap /usr/bin/env -i HOME=/root USER=root TERM=xterm-256color SDL_IM_MODULE=fcitx XMODIFIERS=\\\\@im=fcitx QT_IM_MODULE=fcitx GTK_IM_MODULE=fcitx TMPDIR=/tmp DISPLAY=:4 PULSE_SERVER=tcp:127.0.0.1:4718 LANG=zh_CN.UTF-8 SHELL=/bin/bash PATH=/usr/local/sbin:/usr/local/bin:/bin:/usr/bin:/sbin:/usr/sbin:/usr/games:/usr/local/games /bin/bash -l", await G.prefs.setStringList("containersInfo", ["""{ "name":"Ubuntu Jammy", "boot":"\$DATA_DIR/bin/proot --link2symlink --ashmem-memfd --sysvipc -H --kill-on-exit --root-id --pwd=/root --rootfs=\$CONTAINER_DIR -L --kernel-release=6.2.1-PRoot-Distro --mount=\$DATA_DIR/share:/media/share --mount=/storage/self/primary/Fonts:/usr/share/fonts/wpsm --mount=/storage/self/primary/AppFiles/Fonts:/usr/share/fonts/yozom --mount=/system/fonts:/usr/share/fonts/androidm --mount=/storage/self/primary:/media/storage/shared --mount=/storage/self/primary/Pictures:/media/storage/Pictures --mount=/storage/self/primary/Music:/media/storage/Music --mount=/storage/self/primary/Movies:/media/storage/Movies --mount=/storage/self/primary/Download:/media/storage/Download --mount=/storage/self/primary/DCIM:/media/storage/DCIM --mount=/storage/self/primary/Documents:/media/storage/Documents --bind=\$DATA_DIR/tmp:/dev/shm --bind=/sys --bind=/proc/self/fd/2:/dev/stderr --bind=/proc/self/fd/1:/dev/stdout --bind=/proc/self/fd/0:/dev/stdin --bind=/proc/self/fd:/dev/fd --bind=/proc --bind=/dev/urandom:/dev/random --bind=/dev --bind=\$CONTAINER_DIR/fake-proc/.loadavg:/proc/loadavg --bind=\$CONTAINER_DIR/fake-proc/.stat:/proc/stat --bind=\$CONTAINER_DIR/fake-proc/.uptime:/proc/uptime --bind=\$CONTAINER_DIR/fake-proc/.version:/proc/version --bind=\$CONTAINER_DIR/fake-proc/.vmstat:/proc/vmstat --bind=\$CONTAINER_DIR/fake-proc/.sysctl_entry_cap_last_cap:/proc/sys/kernel/cap_last_cap /usr/bin/env -i HOME=/root USER=root TERM=xterm-256color SDL_IM_MODULE=fcitx XMODIFIERS=\\\\@im=fcitx QT_IM_MODULE=fcitx GTK_IM_MODULE=fcitx TMPDIR=/tmp DISPLAY=:4 PULSE_SERVER=tcp:127.0.0.1:4718 LANG=zh_CN.UTF-8 SHELL=/bin/bash PATH=/usr/local/sbin:/usr/local/bin:/bin:/usr/bin:/sbin:/usr/sbin:/usr/games:/usr/local/games /bin/bash -l", "vnc":"tigervncserver :4 -SecurityTypes none && .local/share/noVNC/utils/novnc_proxy --vnc localhost:5904 --listen localhost:36082 &", "vncUrl":"http://localhost:36082/vnc.html?host=localhost&port=36082&autoconnect=true&resize=remote", "commands":[{"name":"检查更新并升级", "command":"apt update && apt upgrade -y"}, {"name":"查看系统信息", "command":"uname -a"}, {"name":"清屏", "command":"clear"}, {"name":"安装图形处理软件Krita", "command":"apt update && apt install -y krita krita-l10n"}, {"name":"卸载图形处理软件Krita", "command":"apt autoremove --purge -y krita krita-l10n"}, {"name":"安装视频剪辑软件Kdenlive", "command":"apt update && apt install -y kdenlive"}, {"name":"卸载视频剪辑软件Kdenlive", "command":"apt autoremove --purge -y kdenlive"}, {"name":"安装科学计算软件Octave", "command":"apt update && apt install -y octave"}, {"name":"卸载科学计算软件Octave", "command":"apt autoremove --purge -y octave"}, {"name":"???", "command":"timeout 8 /root/.local/bin/cmatrix"}] }"""]); await G.prefs.setStringList("adsBonus", []); await G.prefs.setInt("adsWatchedTotal", 0); //await G.prefs.setBool("terminalWriteCanBeEnabled", false); //G.prefs.setBool("isTerminalWriteEnabled", false); await G.prefs.setBool("isTerminalWriteEnabled", false); //await G.prefs.setBool("bannerAdsCanBeClosed", false); await G.prefs.setBool("isBannerAdsClosed", false); //G.prefs.setBool("autoLaunchVnc", true); await G.prefs.setBool("autoLaunchVnc", true); await G.prefs.setString("defaultAudioPort", "4718"); await G.prefs.setInt("defaultContainer", 0); } static Future initData() async { G.dataPath = (await getApplicationSupportDirectory()).path; G.termPtys = {}; G.prefs = await SharedPreferences.getInstance(); //限制一天内观看视频广告不超过5次 final String currentDate = DateFormat("yyyy-MM-dd").format(DateTime.now()); if (currentDate != G.prefs.getString("lastDate")) { await G.prefs.setString("lastDate", currentDate); await G.prefs.setInt("adsWatchedToday", 0); } //如果没有这个key,说明是初次启动 if (!G.prefs.containsKey("defaultContainer")) { await initForFirstTime(); } G.currentContainer = G.prefs.getInt("defaultContainer")!; G.controller = WebViewController()..setJavaScriptMode(JavaScriptMode.unrestricted); } static Future initTerminalForCurrent() async { if (!G.termPtys.containsKey(G.currentContainer)) { G.termPtys[G.currentContainer] = TermPty(); } } static Future initAds() async { UnityAds.init( gameId: AdManager.gameId, testMode: true, onComplete: () { print('Initialization Complete'); AdManager.loadAds(); }, onFailed: (error, message) => print('Initialization Failed: $error $message'), ); } static Future setupAudio() async { G.audioPty?.kill(); G.audioPty = Pty.start( "/system/bin/sh" ); //pulseaudio也需要一个tmp文件夹,这里选择前面的cache,没有什么特别的原因,不行再换 //pulseaudio还需要一个文件夹放配置,这里用share G.audioPty!.write(const Utf8Encoder().convert(""" export DATA_DIR=${G.dataPath} cd \$DATA_DIR/.. export TMPDIR=\$PWD/cache cd \$DATA_DIR export HOME=\$DATA_DIR/share export LD_LIBRARY_PATH=\$DATA_DIR/bin \$DATA_DIR/busybox sed "s/4713/${G.prefs.getString("defaultAudioPort")!}/g" \$DATA_DIR/bin/pulseaudio.conf > \$DATA_DIR/bin/pulseaudio.conf.tmp \$DATA_DIR/bin/pulseaudio -F \$DATA_DIR/bin/pulseaudio.conf.tmp exit """)); await G.audioPty?.exitCode; } static Future launchCurrentContainer() async { Util.termWrite( """ export DATA_DIR=${G.dataPath} export CONTAINER_DIR=\$DATA_DIR/containers/${G.currentContainer} export PROOT_L2S_DIR=\$DATA_DIR/containers/0/.l2s cd \$DATA_DIR export PROOT_TMP_DIR=\$DATA_DIR/proot_tmp export PROOT_LOADER=\$DATA_DIR/libexec/proot/loader export PROOT_LOADER_32=\$DATA_DIR/libexec/proot/loader32 ${Util.getCurrentProp("boot")} ${G.prefs.getBool("autoLaunchVnc")!?Util.getCurrentProp("vnc"):""} clear"""); } static Future waitForConnection() async { await retry( // Make a GET request () => http.get(Uri.parse(Util.getCurrentProp("vncUrl"))).timeout(const Duration(milliseconds: 250)), // Retry on SocketException or TimeoutException retryIf: (e) => e is SocketException || e is TimeoutException, ); } static Future launchBrowser() async { G.controller.loadRequest(Uri.parse(Util.getCurrentProp("vncUrl"))); Navigator.push(G.homePageStateContext, MaterialPageRoute(builder: (context) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky,overlays: []); SystemChrome.setSystemUIChangeCallback((systemOverlaysAreVisible) async { await Future.delayed(const Duration(seconds: 1)); SystemChrome.restoreSystemUIOverlays(); }); return Focus( onKey: (node, event) { // Allow webview to handle cursor keys. Without this, the // arrow keys seem to get "eaten" by Flutter and therefore // never reach the webview. // (https://github.com/flutter/flutter/issues/102505). if (!kIsWeb) { if ({ LogicalKeyboardKey.arrowLeft, LogicalKeyboardKey.arrowRight, LogicalKeyboardKey.arrowUp, LogicalKeyboardKey.arrowDown }.contains(event.logicalKey)) { return KeyEventResult.skipRemainingHandlers; } } return KeyEventResult.ignored; }, child: WebViewWidget(controller: G.controller), ); })); } static Future workflow() async { grantPermissions(); await initData(); await initAds(); await initTerminalForCurrent(); setupAudio(); launchCurrentContainer(); if (G.prefs.getBool("autoLaunchVnc")!) { waitForConnection().then((value) => launchBrowser()); } } }