From 089cf41a2ff86b71696e2591ac74daf0cfef17eb Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 9 Oct 2022 20:32:28 +0800 Subject: [PATCH 1/3] add install page Signed-off-by: 21pages --- flutter/lib/consts.dart | 2 +- flutter/lib/desktop/pages/install_page.dart | 198 ++++++++++++++++++++ flutter/lib/main.dart | 27 +++ src/flutter_ffi.rs | 16 ++ 4 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 flutter/lib/desktop/pages/install_page.dart diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index d31f77111..ce6c93c00 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -3,7 +3,7 @@ import 'dart:io'; const double kDesktopRemoteTabBarHeight = 28.0; -/// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)' , 'Desktop CM Page' +/// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)' , 'Desktop CM Page', "Install Page" const String kAppTypeMain = "main"; const String kAppTypeDesktopRemote = "remote"; const String kAppTypeDesktopFileTransfer = "file transfer"; diff --git a/flutter/lib/desktop/pages/install_page.dart b/flutter/lib/desktop/pages/install_page.dart new file mode 100644 index 000000000..b7b3f982d --- /dev/null +++ b/flutter/lib/desktop/pages/install_page.dart @@ -0,0 +1,198 @@ +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get.dart'; +import 'package:url_launcher/url_launcher_string.dart'; +import 'package:window_manager/window_manager.dart'; + +class InstallPage extends StatefulWidget { + const InstallPage({Key? key}) : super(key: key); + + @override + State createState() => _InstallPageState(); +} + +class _InstallPageState extends State with WindowListener { + late final TextEditingController controller; + final RxBool startmenu = true.obs; + final RxBool desktopicon = true.obs; + final RxBool showProgress = false.obs; + final RxBool btnEnabled = true.obs; + + @override + void initState() { + windowManager.addListener(this); + controller = TextEditingController(text: bind.installInstallPath()); + super.initState(); + } + + @override + void dispose() { + windowManager.removeListener(this); + super.dispose(); + } + + @override + void onWindowClose() { + gFFI.close(); + super.onWindowClose(); + windowManager.setPreventClose(false); + windowManager.close(); + } + + @override + Widget build(BuildContext context) { + final double em = 13; + final btnFontSize = 0.9 * em; + final double button_radius = 6; + final buttonStyle = OutlinedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(button_radius)), + )); + final inputBorder = OutlineInputBorder( + borderRadius: BorderRadius.zero, + borderSide: BorderSide(color: Colors.black12)); + return Scaffold( + backgroundColor: Colors.white, + body: SingleChildScrollView( + child: Column( + children: [ + Row( + children: [ + Text( + translate('Installation'), + style: TextStyle( + fontSize: 2 * em, fontWeight: FontWeight.w500), + ), + ], + ), + Row( + children: [ + Text('${translate('Installation Path')}: '), + Expanded( + child: TextField( + controller: controller, + readOnly: true, + style: TextStyle( + fontSize: 1.5 * em, fontWeight: FontWeight.w400), + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.all(0.75 * em), + enabledBorder: inputBorder, + border: inputBorder, + focusedBorder: inputBorder, + constraints: BoxConstraints(maxHeight: 3 * em), + ), + )), + Obx(() => OutlinedButton( + onPressed: + btnEnabled.value ? selectInstallPath : null, + style: buttonStyle, + child: Text(translate('Change Path'), + style: TextStyle( + color: Colors.black87, + fontSize: btnFontSize))) + .marginOnly(left: em)) + ], + ).marginSymmetric(vertical: 2 * em), + Row( + children: [ + Obx(() => Checkbox( + value: startmenu.value, + onChanged: (b) { + if (b != null) startmenu.value = b; + })), + Text(translate('Create start menu shortcuts')) + ], + ), + Row( + children: [ + Obx(() => Checkbox( + value: desktopicon.value, + onChanged: (b) { + if (b != null) desktopicon.value = b; + })), + Text(translate('Create desktop icon')) + ], + ), + GestureDetector( + onTap: () => launchUrlString('http://rustdesk.com/privacy'), + child: Row( + children: [ + Text(translate('End-user license agreement'), + style: const TextStyle( + decoration: TextDecoration.underline)) + ], + )).marginOnly(top: 2 * em), + Row(children: [Text(translate('agreement_tip'))]) + .marginOnly(top: em), + Divider(color: Colors.black87) + .marginSymmetric(vertical: 0.5 * em), + Row( + children: [ + Expanded( + child: Obx(() => Offstage( + offstage: !showProgress.value, + child: LinearProgressIndicator(), + ))), + Obx(() => OutlinedButton( + onPressed: btnEnabled.value + ? () => windowManager.close() + : null, + style: buttonStyle, + child: Text(translate('Cancel'), + style: TextStyle( + color: Colors.black87, + fontSize: btnFontSize))) + .marginOnly(right: 2 * em)), + Obx(() => ElevatedButton( + onPressed: btnEnabled.value ? install : null, + style: ElevatedButton.styleFrom( + primary: MyTheme.button, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(button_radius)), + )), + child: Text( + translate('Accept and Install'), + style: TextStyle(fontSize: btnFontSize), + ))), + Offstage( + offstage: bind.installShowRunWithoutInstall(), + child: Obx(() => OutlinedButton( + onPressed: btnEnabled.value + ? () => bind.installRunWithoutInstall() + : null, + style: buttonStyle, + child: Text(translate('Run without install'), + style: TextStyle( + color: Colors.black87, + fontSize: btnFontSize))) + .marginOnly(left: 2 * em)), + ), + ], + ) + ], + ).paddingSymmetric(horizontal: 8 * em, vertical: 2 * em), + )); + } + + void install() { + btnEnabled.value = false; + showProgress.value = true; + String args = ''; + if (startmenu.value) args += 'startmenu '; + if (desktopicon.value) args += 'desktopicon '; + bind.installInstallMe(options: args, path: controller.text); + } + + void selectInstallPath() async { + String? install_path = await FilePicker.platform + .getDirectoryPath(initialDirectory: controller.text); + if (install_path != null) { + install_path = '$install_path\\${await bind.mainGetAppName()}'; + controller.text = install_path; + } + } +} diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 0d1123e05..27b14be09 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -4,6 +4,7 @@ import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; import 'package:flutter_hbb/desktop/pages/server_page.dart'; +import 'package:flutter_hbb/desktop/pages/install_page.dart'; import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart'; import 'package:flutter_hbb/desktop/screen/desktop_port_forward_screen.dart'; import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart'; @@ -64,6 +65,8 @@ Future main(List args) async { desktopType = DesktopType.cm; await windowManager.ensureInitialized(); runConnectionManagerScreen(); + } else if (args.contains('--install')) { + runInstallPage(); } else { desktopType = DesktopType.main; await windowManager.ensureInitialized(); @@ -215,6 +218,30 @@ void runConnectionManagerScreen() async { }); } +void runInstallPage() async { + await windowManager.ensureInitialized(); + await initEnv(kAppTypeMain); + runApp(GetMaterialApp( + debugShowCheckedModeBanner: false, + theme: MyTheme.lightTheme, + themeMode: ThemeMode.light, + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: supportedLocales, + home: const InstallPage(), + builder: _keepScaleBuilder())); + windowManager.waitUntilReadyToShow( + WindowOptions(size: Size(800, 600), center: true), () async { + windowManager.show(); + windowManager.focus(); + windowManager.setOpacity(1); + windowManager.setAlignment(Alignment.center); // ensure + }); +} + WindowOptions getHiddenTitleBarWindowOptions({Size? size}) { return WindowOptions( size: size, diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 9ce92aeb2..b33c9490f 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1041,6 +1041,22 @@ pub fn main_update_me() -> SyncReturn { SyncReturn(true) } +pub fn install_show_run_without_install() -> SyncReturn { + SyncReturn(show_run_without_install()) +} + +pub fn install_run_without_install() { + run_without_install(); +} + +pub fn install_install_me(options: String, path: String) { + install_me(options, path, false, false); +} + +pub fn install_install_path() -> SyncReturn { + SyncReturn(install_path()) +} + #[cfg(target_os = "android")] pub mod server_side { use jni::{ From 2ced73cddaf16da44389de01a0d51a953b789cd7 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 9 Oct 2022 21:10:41 +0800 Subject: [PATCH 2/3] pass rust args to flutter Signed-off-by: 21pages --- flutter/windows/runner/main.cpp | 18 +++++++- src/flutter.rs | 74 +++++++++++++++++++++++++++++++++ src/flutter_ffi.rs | 10 ----- 3 files changed, 90 insertions(+), 12 deletions(-) diff --git a/flutter/windows/runner/main.cpp b/flutter/windows/runner/main.cpp index efa26314e..3921e03dd 100644 --- a/flutter/windows/runner/main.cpp +++ b/flutter/windows/runner/main.cpp @@ -7,7 +7,8 @@ #include "utils.h" // #include -typedef bool (*FUNC_RUSTDESK_CORE_MAIN)(void); +typedef char** (*FUNC_RUSTDESK_CORE_MAIN)(int*); +typedef void (*FUNC_RUSTDESK_FREE_ARGS)( char**, int); // auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP); int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, @@ -26,11 +27,23 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, std::cout << "Failed to get rustdesk_core_main" << std::endl; return EXIT_FAILURE; } - if (!rustdesk_core_main()) + FUNC_RUSTDESK_FREE_ARGS free_c_args = + (FUNC_RUSTDESK_FREE_ARGS)GetProcAddress(hInstance, "free_c_args"); + if (!free_c_args) + { + std::cout << "Failed to get free_c_args" << std::endl; + return EXIT_FAILURE; + } + + int args_len = 0; + char** c_args = rustdesk_core_main(&args_len); + if (!c_args) { std::cout << "Rustdesk core returns false, exiting without launching Flutter app" << std::endl; return EXIT_SUCCESS; } + std::vector rust_args(c_args, c_args + args_len); + free_c_args(c_args, args_len); // Attach to console when present (e.g., 'flutter run') or create a // new console when running with a debugger. @@ -48,6 +61,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, std::vector command_line_arguments = GetCommandLineArguments(); + command_line_arguments.insert(command_line_arguments.end(), rust_args.begin(), rust_args.end()); project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); FlutterWindow window(project); diff --git a/src/flutter.rs b/src/flutter.rs index bf758e31c..b65ce4412 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1,5 +1,7 @@ use std::{ collections::HashMap, + ffi::CString, + os::raw::{c_char, c_int}, sync::{Arc, RwLock}, }; @@ -24,6 +26,78 @@ lazy_static::lazy_static! { pub static ref GLOBAL_EVENT_STREAM: RwLock>> = Default::default(); // rust to dart event channel } +/// FFI for rustdesk core's main entry. +/// Return true if the app should continue running with UI(possibly Flutter), false if the app should exit. +#[cfg(not(windows))] +#[no_mangle] +pub extern "C" fn rustdesk_core_main() -> bool { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return crate::core_main::core_main().is_some(); + #[cfg(any(target_os = "android", target_os = "ios"))] + false +} + +#[cfg(windows)] +#[no_mangle] +pub extern "C" fn rustdesk_core_main(args_len: *mut c_int) -> *mut *mut c_char { + unsafe { std::ptr::write(args_len, 0) }; + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + if let Some(args) = crate::core_main::core_main() { + return rust_args_to_c_args(args, args_len); + } + return std::ptr::null_mut() as _; + } + #[cfg(any(target_os = "android", target_os = "ios"))] + return std::ptr::null_mut() as _; +} + +// https://gist.github.com/iskakaushik/1c5b8aa75c77479c33c4320913eebef6 +fn rust_args_to_c_args(args: Vec, outlen: *mut c_int) -> *mut *mut c_char { + let mut v = vec![]; + + // Let's fill a vector with null-terminated strings + for s in args { + v.push(CString::new(s).unwrap()); + } + + // Turning each null-terminated string into a pointer. + // `into_raw` takes ownershop, gives us the pointer and does NOT drop the data. + let mut out = v.into_iter().map(|s| s.into_raw()).collect::>(); + + // Make sure we're not wasting space. + out.shrink_to_fit(); + assert!(out.len() == out.capacity()); + + // Get the pointer to our vector. + let len = out.len(); + let ptr = out.as_mut_ptr(); + std::mem::forget(out); + + // Let's write back the length the caller can expect + unsafe { std::ptr::write(outlen, len as c_int) }; + + // Finally return the data + ptr +} + +#[no_mangle] +pub unsafe extern "C" fn free_c_args(ptr: *mut *mut c_char, len: c_int) { + let len = len as usize; + + // Get back our vector. + // Previously we shrank to fit, so capacity == length. + let v = Vec::from_raw_parts(ptr, len, len); + + // Now drop one string at a time. + for elem in v { + let s = CString::from_raw(elem); + std::mem::drop(s); + } + + // Afterwards the vector will be dropped and thus freed. +} + #[derive(Default, Clone)] pub struct FlutterHandler { pub event_stream: Arc>>>, diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index b33c9490f..f788c679d 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -43,16 +43,6 @@ fn initialize(app_dir: &str) { } } -/// FFI for rustdesk core's main entry. -/// Return true if the app should continue running with UI(possibly Flutter), false if the app should exit. -#[no_mangle] -pub extern "C" fn rustdesk_core_main() -> bool { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - return crate::core_main::core_main().is_some(); - #[cfg(any(target_os = "android", target_os = "ios"))] - false -} - pub enum EventToUI { Event(String), Rgba(ZeroCopyBuffer>), From e2924f0d4152649025bd1c0fbe9e39bcd7a761a3 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 11 Oct 2022 14:52:46 +0800 Subject: [PATCH 3/3] build windows install Signed-off-by: 21pages --- build.py | 10 ++-- flutter/lib/desktop/pages/install_page.dart | 6 +-- libs/portable/src/main.rs | 53 +++++++++++++++------ src/common.rs | 2 +- src/core_main.rs | 15 ++++-- src/platform/windows.rs | 14 ++++++ 6 files changed, 74 insertions(+), 26 deletions(-) diff --git a/build.py b/build.py index 54f50a0b8..85dab7090 100755 --- a/build.py +++ b/build.py @@ -189,7 +189,7 @@ def build_flutter_arch_manjaro(): os.chdir('..') os.system('HBB=`pwd` FLUTTER=1 makepkg -f') -def build_flutter_windows_portable(): +def build_flutter_windows(version): os.system("cargo build --lib --features flutter --release") os.chdir('flutter') os.system("flutter build windows --release") @@ -203,6 +203,8 @@ def build_flutter_windows_portable(): else: os.rename("./target/release/rustdesk-portable-packer.exe", "./rustdesk_portable.exe") print(f"output location: {os.path.abspath(os.curdir)}/rustdesk_portable.exe") + os.system(f"cp -rf ./rustdesk_portable.exe ./rustdesk-{version}-install.exe") + print(f"output location: {os.path.abspath(os.curdir)}/rustdesk-{version}-install.exe") def main(): parser = make_parser() @@ -227,8 +229,8 @@ def main(): os.system('python3 res/inline-sciter.py') portable = args.portable if windows: - if portable: - build_flutter_windows_portable() + if flutter: + build_flutter_windows(version) return os.system('cargo build --release --features ' + features) # os.system('upx.exe target/release/rustdesk.exe') @@ -239,7 +241,7 @@ def main(): 'target\\release\\rustdesk.exe') else: print('Not signed') - os.system(f'cp -rf target/release/RustDesk.exe rustdesk-{version}-setdown.exe') + os.system(f'cp -rf target/release/RustDesk.exe rustdesk-{version}-win7-install.exe') elif os.path.isfile('/usr/bin/pacman'): # pacman -S -needed base-devel os.system("sed -i 's/pkgver=.*/pkgver=%s/g' PKGBUILD" % version) diff --git a/flutter/lib/desktop/pages/install_page.dart b/flutter/lib/desktop/pages/install_page.dart index b7b3f982d..73ad8769d 100644 --- a/flutter/lib/desktop/pages/install_page.dart +++ b/flutter/lib/desktop/pages/install_page.dart @@ -181,9 +181,9 @@ class _InstallPageState extends State with WindowListener { void install() { btnEnabled.value = false; showProgress.value = true; - String args = ''; - if (startmenu.value) args += 'startmenu '; - if (desktopicon.value) args += 'desktopicon '; + String args = '--flutter'; + if (startmenu.value) args += ' startmenu'; + if (desktopicon.value) args += ' desktopicon'; bind.installInstallMe(options: args, path: controller.text); } diff --git a/libs/portable/src/main.rs b/libs/portable/src/main.rs index 614c4c17c..1c9dc11ef 100644 --- a/libs/portable/src/main.rs +++ b/libs/portable/src/main.rs @@ -12,29 +12,37 @@ pub mod bin_reader; const APP_PREFIX: &str = "rustdesk"; const APPNAME_RUNTIME_ENV_KEY: &str = "RUSTDESK_APPNAME"; -fn setup(reader: BinaryReader) -> Option { - // home dir - if let Some(dir) = dirs::data_local_dir() { - let dir = dir.join(APP_PREFIX); - for file in reader.files.iter() { - file.write_to_file(&dir); - } - #[cfg(unix)] - reader.configure_permission(&dir); - Some(dir.join(&reader.exe)) +fn setup(reader: BinaryReader, dir: Option, clear: bool) -> Option { + let dir = if let Some(dir) = dir { + dir } else { - eprintln!("not found data local dir"); - None + // home dir + if let Some(dir) = dirs::data_local_dir() { + dir.join(APP_PREFIX) + } else { + eprintln!("not found data local dir"); + return None; + } + }; + if clear { + std::fs::remove_dir_all(&dir).ok(); } + for file in reader.files.iter() { + file.write_to_file(&dir); + } + #[cfg(unix)] + reader.configure_permission(&dir); + Some(dir.join(&reader.exe)) } -fn execute(path: PathBuf) { +fn execute(path: PathBuf, args: Vec) { println!("executing {}", path.display()); // setup env let exe = std::env::current_exe().unwrap(); let exe_name = exe.file_name().unwrap(); // run executable Command::new(path) + .args(args) .env(APPNAME_RUNTIME_ENV_KEY, exe_name) .stdin(Stdio::inherit()) .stdout(Stdio::inherit()) @@ -43,9 +51,24 @@ fn execute(path: PathBuf) { .expect(&format!("failed to execute {:?}", exe_name)); } +fn is_setup(name: &str) -> bool { + name.to_lowercase().ends_with("install.exe") || name.to_lowercase().ends_with("安装.exe") +} + fn main() { + let is_setup = is_setup( + &std::env::current_exe() + .unwrap() + .to_string_lossy() + .to_string(), + ); let reader = BinaryReader::default(); - if let Some(exe) = setup(reader) { - execute(exe); + if let Some(exe) = setup(reader, None, is_setup) { + let args = if is_setup { + vec!["--install".to_owned()] + } else { + vec![] + }; + execute(exe, args); } } diff --git a/src/common.rs b/src/common.rs index dd792362a..129e948cf 100644 --- a/src/common.rs +++ b/src/common.rs @@ -544,7 +544,7 @@ pub fn is_ip(id: &str) -> bool { } pub fn is_setup(name: &str) -> bool { - name.to_lowercase().ends_with("setdown.exe") || name.to_lowercase().ends_with("安装.exe") + name.to_lowercase().ends_with("install.exe") || name.to_lowercase().ends_with("安装.exe") } pub fn get_custom_rendezvous_server(custom: String) -> String { diff --git a/src/core_main.rs b/src/core_main.rs index d159e115e..ac05b20bf 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -6,6 +6,7 @@ pub fn core_main() -> Option> { // though async logger more efficient, but it also causes more problems, disable it for now // let mut _async_logger_holder: Option = None; let mut args = Vec::new(); + let mut flutter_args = Vec::new(); let mut i = 0; let mut is_setup = false; let mut _is_elevate = false; @@ -25,13 +26,18 @@ pub fn core_main() -> Option> { } i += 1; } + if args.contains(&"--install".to_string()) { + is_setup = true; + } if is_setup { if args.is_empty() { args.push("--install".to_owned()); - } else if args[0] == "--noinstall" { - args.clear(); + flutter_args.push("--install".to_string()); } } + if args.contains(&"--noinstall".to_string()) { + args.clear(); + } if args.len() > 0 && args[0] == "--version" { println!("{}", crate::VERSION); return None; @@ -171,7 +177,10 @@ pub fn core_main() -> Option> { } } //_async_logger_holder.map(|x| x.flush()); - Some(args) + #[cfg(feature = "flutter")] + return Some(flutter_args); + #[cfg(not(feature = "flutter"))] + return Some(args); } fn import_config(path: &str) { diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 10b0dcee6..6732f2c36 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -1025,6 +1025,18 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{start_menu}\\\" app_name = crate::get_app_name(), ); } + let mut flutter_copy = Default::default(); + if options.contains("--flutter") { + flutter_copy = format!( + "XCOPY \"{}\" \"{}\" /Y /E /H /C /I /K /R /Z", + std::env::current_exe()? + .parent() + .unwrap() + .to_string_lossy() + .to_string(), + path + ); + } let meta = std::fs::symlink_metadata(std::env::current_exe()?)?; let size = meta.len() / 1024; @@ -1052,6 +1064,7 @@ if exist \"{tmp_path}\\{app_name} Tray.lnk\" del /f /q \"{tmp_path}\\{app_name} {uninstall_str} chcp 65001 md \"{path}\" +{flutter_copy} copy /Y \"{src_exe}\" \"{exe}\" copy /Y \"{ORIGIN_PROCESS_EXE}\" \"{path}\\{broker_exe}\" \"{src_exe}\" --extract \"{path}\" @@ -1114,6 +1127,7 @@ sc delete {app_name} } else { &dels }, + flutter_copy = flutter_copy, ); run_cmds(cmds, debug, "install")?; std::thread::sleep(std::time::Duration::from_millis(2000));