From 545cc66096fb118cef61cf045849d39800589aa4 Mon Sep 17 00:00:00 2001 From: horizonlin Date: Tue, 30 Jul 2019 17:02:05 +0800 Subject: [PATCH 01/44] =?UTF-8?q?=E4=BF=AE=E5=A4=8Durlprotocol?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复urlprotocol调用问题 --- server/www/teleport/static/js/tp-assist.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/server/www/teleport/static/js/tp-assist.js b/server/www/teleport/static/js/tp-assist.js index cc983db..ec4a174 100644 --- a/server/www/teleport/static/js/tp-assist.js +++ b/server/www/teleport/static/js/tp-assist.js @@ -134,14 +134,16 @@ $assist._make_message_box = function () { }; $assist.do_teleport = function (args, func_success, func_error) { - if(!$assist.running) { - $assist.errcode = TPE_NO_ASSIST; - func_error(TPE_NO_ASSIST, ''); - return; - } else if(!$assist._version_compare()) { - $assist.errcode = TPE_OLD_ASSIST; - func_error(TPE_NO_ASSIST, ''); - return; + if(!$app.options.url_proto){ + if(!$assist.running) { + $assist.errcode = TPE_NO_ASSIST; + func_error(TPE_NO_ASSIST, ''); + return; + } else if(!$assist._version_compare()) { + $assist.errcode = TPE_OLD_ASSIST; + func_error(TPE_NO_ASSIST, ''); + return; + } } // 第一步:将参数传递给web服务,准备获取一个远程连接会话ID From 4fec254455198c6488c7c34ee8d2bb592e5f5ccf Mon Sep 17 00:00:00 2001 From: horizonlin Date: Wed, 31 Jul 2019 17:04:04 +0800 Subject: [PATCH 02/44] Update tp-assist.js --- server/www/teleport/static/js/tp-assist.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/www/teleport/static/js/tp-assist.js b/server/www/teleport/static/js/tp-assist.js index ec4a174..b8cf11a 100644 --- a/server/www/teleport/static/js/tp-assist.js +++ b/server/www/teleport/static/js/tp-assist.js @@ -71,11 +71,11 @@ $assist.alert_assist_not_found = function () { if($assist.errcode === TPE_NO_ASSIST) { $assist.dom.msg_box_title.html('未检测到TELEPORT助手'); $assist.dom.msg_box_info.html('需要TELEPORT助手来辅助远程连接,请确认本机运行了TELEPORT助手!'); - $assist.dom.msg_box_desc.html('如果您尚未运行TELEPORT助手,请 下载最新版TELEPORT助手安装包 并安装。一旦运行了TELEPORT助手,即可重新进行远程连接。'); + $assist.dom.msg_box_desc.html('如果您尚未运行TELEPORT助手,请 下载最新版TELEPORT助手安装包 并安装。一旦运行了TELEPORT助手,即可重新进行远程连接。'); } else if($assist.errcode === TPE_OLD_ASSIST) { $assist.dom.msg_box_title.html('TELEPORT助手需要升级'); $assist.dom.msg_box_info.html('检测到TELEPORT助手版本 v'+ $assist.version +',但需要最低版本 v'+ $assist.ver_require+'。'); - $assist.dom.msg_box_desc.html('请 下载最新版TELEPORT助手安装包 并安装。一旦升级了TELEPORT助手,即可重新进行远程连接。'); + $assist.dom.msg_box_desc.html('请 下载最新版TELEPORT助手安装包 并安装。一旦升级了TELEPORT助手,即可重新进行远程连接。'); } $('#dialog-need-assist').modal(); From 6b409a998d92fc4a0cec3bfde8df6e2885c07662 Mon Sep 17 00:00:00 2001 From: ApexLiu Date: Thu, 1 Aug 2019 17:43:46 +0800 Subject: [PATCH 03/44] can build on Ubuntu now. --- build/build-py-static.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build/build-py-static.sh b/build/build-py-static.sh index b501055..296612b 100755 --- a/build/build-py-static.sh +++ b/build/build-py-static.sh @@ -3,10 +3,10 @@ ################################################################ # Basic settings. ################################################################ -VER_PYTHON="3.7.0" +VER_PYTHON="3.7.4" VER_PYTHON_SHORT="3.7" -VER_OPENSSL="1.0.2p" -VER_SQLITE="3250000" +VER_OPENSSL="1.0.2s" +VER_SQLITE="3290000" VER_ZLIB="1.2.11" VER_PYTHON_LIB="${VER_PYTHON_SHORT}m" @@ -77,7 +77,7 @@ function step_download_files() dlfile "python source tarball" "https://www.python.org/ftp/python/${VER_PYTHON}/" "Python-${VER_PYTHON}.tgz" ${PATH_DOWNLOAD} dlfile "openssl source tarball" "https://www.openssl.org/source/" "openssl-${VER_OPENSSL}.tar.gz" ${PATH_DOWNLOAD} - dlfile "sqlite source tarball" "http://sqlite.org/2018/" "sqlite-autoconf-${VER_SQLITE}.tar.gz" ${PATH_DOWNLOAD} + dlfile "sqlite source tarball" "http://sqlite.org/2019/" "sqlite-autoconf-${VER_SQLITE}.tar.gz" ${PATH_DOWNLOAD} dlfile "zlib source tarball" "https://www.zlib.net/" "zlib-${VER_ZLIB}.tar.gz" ${PATH_DOWNLOAD} } From c2f8b89de0e14be88c32535fef17ad12f0aa2599 Mon Sep 17 00:00:00 2001 From: Apex Liu Date: Mon, 5 Aug 2019 01:22:03 +0800 Subject: [PATCH 04/44] =?UTF-8?q?=E5=8A=A9=E6=89=8B=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E4=B8=AD=E9=BB=98=E8=AE=A4RDP=E4=BD=BF=E7=94=A8mstsc.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/cfg/tp-assist.windows.json | 8 +++++++- dist/client/windows/assist/installer.nsi | Bin 2938 -> 3052 bytes 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/client/cfg/tp-assist.windows.json b/client/cfg/tp-assist.windows.json index f4fc876..0f9baea 100755 --- a/client/cfg/tp-assist.windows.json +++ b/client/cfg/tp-assist.windows.json @@ -71,12 +71,18 @@ "rdp" : { "available" : [ { + "app" : "mstsc.exe", + "cmdline" : "\"{tmp_rdp_file}\"", + "display" : "微软RDP客户端(系统自带)", + "name" : "mstsc" + }, + { "app" : "{assist_tools_path}\\tprdp\\tprdp-client.exe", "cmdline" : "/v:{host_ip}:{host_port} /u:{user_name} /t:\"TP#{real_ip}\"", "display" : "FreeRDP(内置)", "name" : "freerdp" } ], - "selected" : "freerdp" + "selected" : "mstsc" } } diff --git a/dist/client/windows/assist/installer.nsi b/dist/client/windows/assist/installer.nsi index 91f46d4adc7b0635a21bde5580c5627a09714827..f6c82e0fe99033ccf2b21690452c59ef912e1cd7 100644 GIT binary patch delta 480 zcmew*_C|a{89Ofn7lSne=j69c@)KvdG4f8lUoHU=gOCagv!XryEaJ{j|LVA5cP@qB@*L1GHbc2HIUvza_t7Hq8o!*aia zA)V1jJ6MC=8A2I68C;;IC@>35e#Be^*Do=-jztHFcZJ0aZVSkKfysKTLQwf2Ruw3# zgH;ZyYB{SuRO|&SOs71XDpV|t%?he+B3nF^^^r{+Dl5g_1{GV#ZV43Y=Tx7>AqHfK za;P%_L&b)}3LcCXIfW_i<>6LjqHgA(bJIA%!86Aq^U0KRJS-w#0E7fz7rD ylZKQ3af)%mLW*~?J(u`oJuZ+B?YXp}uF2*)2;^&XJBq;6Su?mY_%pbH%?1F#Xl0N9 delta 343 zcmaDO{!45^*~E%0W-bQn$&5_JLO_}UgcTTOMSJ>L#GRiwxrotk@*ze8cBm9k*~Ezo zEX+W4lOHk*PhP{M!3bf!VX~V1h*@c}7qcFip2Msr$ptlSx!=K%&gi2ZtikRKp$wi3 zEu|XN j1Bz>MHJ9e(Wn5Z7*{fU!C(qz^;DcE1%HYr72J|ohK^$Zi From 051ddaca6af9faa719d1ca7d19a61c604cddaa37 Mon Sep 17 00:00:00 2001 From: Apex Liu Date: Thu, 15 Aug 2019 19:22:41 +0800 Subject: [PATCH 05/44] typo fix. --- client/cfg/tp-assist.windows.json | 8 +- client/tp_assist_win/tp_assist.vs2017.vcxproj | 4 +- dist/client/windows/assist/installer.nsi | Bin 2938 -> 3052 bytes server/tp_core/core/tp_core.vs2017.sln | 3 + server/tp_core/core/tp_core.vs2017.vcxproj | 77 +----------------- server/tp_core/testssh/testssh.sln | 4 - server/tp_core/testssh/testssh.vcxproj | 4 +- 7 files changed, 16 insertions(+), 84 deletions(-) diff --git a/client/cfg/tp-assist.windows.json b/client/cfg/tp-assist.windows.json index f4fc876..0f9baea 100755 --- a/client/cfg/tp-assist.windows.json +++ b/client/cfg/tp-assist.windows.json @@ -71,12 +71,18 @@ "rdp" : { "available" : [ { + "app" : "mstsc.exe", + "cmdline" : "\"{tmp_rdp_file}\"", + "display" : "微软RDP客户端(系统自带)", + "name" : "mstsc" + }, + { "app" : "{assist_tools_path}\\tprdp\\tprdp-client.exe", "cmdline" : "/v:{host_ip}:{host_port} /u:{user_name} /t:\"TP#{real_ip}\"", "display" : "FreeRDP(内置)", "name" : "freerdp" } ], - "selected" : "freerdp" + "selected" : "mstsc" } } diff --git a/client/tp_assist_win/tp_assist.vs2017.vcxproj b/client/tp_assist_win/tp_assist.vs2017.vcxproj index 0ebbf9c..d2ab99a 100644 --- a/client/tp_assist_win/tp_assist.vs2017.vcxproj +++ b/client/tp_assist_win/tp_assist.vs2017.vcxproj @@ -46,8 +46,8 @@ true ..\..\out\client\$(PlatformTarget)\$(Configuration)\ ..\..\out\_tmp_\$(ProjectName)\$(PlatformTarget)\$(Configuration)\ - C:\Program Files %28x86%29\Visual Leak Detector\include;$(IncludePath) - C:\Program Files %28x86%29\Visual Leak Detector\lib\Win32;$(LibraryPath) + C:\apps\vld\include;$(IncludePath) + C:\apps\vld\lib\Win32;$(LibraryPath) false diff --git a/dist/client/windows/assist/installer.nsi b/dist/client/windows/assist/installer.nsi index 91f46d4adc7b0635a21bde5580c5627a09714827..f6c82e0fe99033ccf2b21690452c59ef912e1cd7 100644 GIT binary patch delta 480 zcmew*_C|a{89Ofn7lSne=j69c@)KvdG4f8lUoHU=gOCagv!XryEaJ{j|LVA5cP@qB@*L1GHbc2HIUvza_t7Hq8o!*aia zA)V1jJ6MC=8A2I68C;;IC@>35e#Be^*Do=-jztHFcZJ0aZVSkKfysKTLQwf2Ruw3# zgH;ZyYB{SuRO|&SOs71XDpV|t%?he+B3nF^^^r{+Dl5g_1{GV#ZV43Y=Tx7>AqHfK za;P%_L&b)}3LcCXIfW_i<>6LjqHgA(bJIA%!86Aq^U0KRJS-w#0E7fz7rD ylZKQ3af)%mLW*~?J(u`oJuZ+B?YXp}uF2*)2;^&XJBq;6Su?mY_%pbH%?1F#Xl0N9 delta 343 zcmaDO{!45^*~E%0W-bQn$&5_JLO_}UgcTTOMSJ>L#GRiwxrotk@*ze8cBm9k*~Ezo zEX+W4lOHk*PhP{M!3bf!VX~V1h*@c}7qcFip2Msr$ptlSx!=K%&gi2ZtikRKp$wi3 zEu|XN j1Bz>MHJ9e(Wn5Z7*{fU!C(qz^;DcE1%HYr72J|ohK^$Zi diff --git a/server/tp_core/core/tp_core.vs2017.sln b/server/tp_core/core/tp_core.vs2017.sln index d059e1d..3c23a0b 100644 --- a/server/tp_core/core/tp_core.vs2017.sln +++ b/server/tp_core/core/tp_core.vs2017.sln @@ -19,4 +19,7 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {787A1953-2C25-4859-B81F-7F63A94B8EE3} + EndGlobalSection EndGlobal diff --git a/server/tp_core/core/tp_core.vs2017.vcxproj b/server/tp_core/core/tp_core.vs2017.vcxproj index 8f3236a..ee0e4d1 100644 --- a/server/tp_core/core/tp_core.vs2017.vcxproj +++ b/server/tp_core/core/tp_core.vs2017.vcxproj @@ -9,14 +9,6 @@ Release Win32 - - Debug - x64 - - - Release - x64 - {6548CB1D-A7BA-4A68-9B3F-A5129F77868B} @@ -39,19 +31,6 @@ Unicode v141 - - Application - true - v140 - Unicode - - - Application - false - v140 - true - Unicode - @@ -63,35 +42,19 @@ - - - - - - true ..\..\..\out\server\$(PlatformTarget)\$(Configuration)\ ..\..\..\out\_tmp_\$(ProjectName)\$(PlatformTarget)\$(Configuration)\ - C:\Program Files %28x86%29\Visual Leak Detector\include;$(IncludePath) - C:\Program Files %28x86%29\Visual Leak Detector\lib\Win32;$(LibraryPath) - - - true - ..\..\out\$(ProjectName)\$(PlatformTarget)\$(Configuration)\ - ..\..\out\_tmp_\$(ProjectName)\$(PlatformTarget)\$(Configuration)\ + c:\apps\vld\include;$(IncludePath) + C:\apps\vld\lib\Win32;$(LibraryPath) false ..\..\..\out\server\$(PlatformTarget)\$(Configuration)\ ..\..\..\out\_tmp_\$(ProjectName)\$(PlatformTarget)\$(Configuration)\ - - false - ..\..\out\$(ProjectName)\$(PlatformTarget)\$(Configuration)\ - ..\..\out\_tmp_\$(ProjectName)\$(PlatformTarget)\$(Configuration)\ - @@ -111,22 +74,6 @@ Debug - - - - - Level3 - Disabled - _DEBUG;_WINDOWS;%(PreprocessorDefinitions) - true - ../../external/windows/libuv/include;../../external/windows/openssl/include;../../external/windows/zlib/include;../../external/windows/mbedtls/include;../../external/windows/libssh/include;../../external/common/jsoncpp/include;../../external/common/sqlite;d:/apps/vld/include - - - Windows - true - ../../external/windows/openssl/lib;../../external/windows/zlib/lib;../../external/windows/libssh/lib - - Level3 @@ -148,26 +95,6 @@ - - - Level3 - - - MaxSpeed - true - true - NDEBUG;_WINDOWS;%(PreprocessorDefinitions) - true - ../../external/windows/libuv/include;../../external/windows/openssl/include;../../external/windows/zlib/include;../../external/windows/mbedtls/include;../../external/windows/libssh/include;../../external/common/jsoncpp/include;../../external/common/sqlite - - - Windows - true - true - true - ../../external/windows/openssl/lib;../../external/windows/zlib/lib;../../external/windows/libssh/lib - - diff --git a/server/tp_core/testssh/testssh.sln b/server/tp_core/testssh/testssh.sln index 63aeb3c..7819b63 100644 --- a/server/tp_core/testssh/testssh.sln +++ b/server/tp_core/testssh/testssh.sln @@ -7,16 +7,12 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "testssh", "testssh.vcxproj" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 - Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {27998EAA-69B4-4AA5-9D91-54CE740181A9}.Debug|x64.ActiveCfg = Debug|Win32 {27998EAA-69B4-4AA5-9D91-54CE740181A9}.Debug|x86.ActiveCfg = Debug|Win32 {27998EAA-69B4-4AA5-9D91-54CE740181A9}.Debug|x86.Build.0 = Debug|Win32 - {27998EAA-69B4-4AA5-9D91-54CE740181A9}.Release|x64.ActiveCfg = Release|Win32 {27998EAA-69B4-4AA5-9D91-54CE740181A9}.Release|x86.ActiveCfg = Release|Win32 {27998EAA-69B4-4AA5-9D91-54CE740181A9}.Release|x86.Build.0 = Release|Win32 EndGlobalSection diff --git a/server/tp_core/testssh/testssh.vcxproj b/server/tp_core/testssh/testssh.vcxproj index e1dbf3a..7e764b2 100644 --- a/server/tp_core/testssh/testssh.vcxproj +++ b/server/tp_core/testssh/testssh.vcxproj @@ -55,13 +55,13 @@ Disabled WIN32;_DEBUG;_CONSOLE;LIBSSH_STATIC;%(PreprocessorDefinitions) true - ..\..\..\..\external\libssh\include;%(AdditionalIncludeDirectories) + ..\..\..\external\libssh\include;%(AdditionalIncludeDirectories) MultiThreadedDebug Console true - ..\..\..\..\external\libssh\build\src\static;..\..\..\..\external\openssl\out32;%(AdditionalLibraryDirectories) + ..\..\..\external\libssh\build\src\static;..\..\..\external\openssl\out32;%(AdditionalLibraryDirectories) From 03a21be954ecf4140aafffab881ec7ec8c55cdb6 Mon Sep 17 00:00:00 2001 From: Apex Liu Date: Thu, 15 Aug 2019 19:26:35 +0800 Subject: [PATCH 06/44] .temp. --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index d6ddb82..bb2164f 100644 --- a/.gitignore +++ b/.gitignore @@ -28,8 +28,10 @@ __pycache__ **/.idea/misc.xml **/.idea/dictionaries **/.idea/watcherTasks.xml +**/.idea/dataSources.* **/.idea/inspectionProfiles **/.idea/codeStyles +**/.idea/dataSources **/.idea/inspectionProfiles **/.idea/vcs.xml **/.idea/modules.xml From db0990d8c297b95d5bc45ba3a1179345748d67a1 Mon Sep 17 00:00:00 2001 From: Apex Liu Date: Sun, 1 Sep 2019 02:54:22 +0800 Subject: [PATCH 07/44] convert source files to UTF-8 with BOM. --- .gitignore | 2 + common/libex/include/ex.h | 2 +- common/libex/include/ex/ex_const.h | 6 +- common/libex/include/ex/ex_ini.h | 2 +- common/libex/include/ex/ex_log.h | 6 +- common/libex/include/ex/ex_path.h | 4 +- common/libex/include/ex/ex_platform.h | 2 +- common/libex/include/ex/ex_str.h | 172 +- common/libex/include/ex/ex_thread.h | 16 +- common/libex/include/ex/ex_types.h | 88 +- common/libex/include/ex/ex_util.h | 110 +- common/libex/include/ex/ex_winsrv.h | 2 +- common/libex/src/ex_ini.cpp | 30 +- common/libex/src/ex_log.cpp | 1034 ++++++------ common/libex/src/ex_path.cpp | 2 +- common/libex/src/ex_str.cpp | 1710 ++++++++++---------- common/libex/src/ex_thread.cpp | 448 ++--- common/libex/src/ex_util.cpp | 2 +- common/libex/src/ex_winsrv.cpp | 10 +- common/teleport/teleport_const.h | 302 ++-- server/tp_core/common/base_env.cpp | 96 +- server/tp_core/common/base_env.h | 58 +- server/tp_core/common/base_record.cpp | 2 +- server/tp_core/common/base_record.h | 202 +-- server/tp_core/common/protocol_interface.h | 200 +-- server/tp_core/common/ts_const.h | 52 +- server/tp_core/common/ts_membuf.cpp | 4 +- server/tp_core/common/ts_membuf.h | 12 +- server/tp_core/common/ts_memstream.cpp | 444 ++--- server/tp_core/common/ts_memstream.h | 90 +- 30 files changed, 2556 insertions(+), 2554 deletions(-) diff --git a/.gitignore b/.gitignore index bb2164f..919bcb9 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,5 @@ profile *.moved-aside /server/share/tmp +/server/tp_core/testssh/Debug +/server/tp_core/testssh/Release diff --git a/common/libex/include/ex.h b/common/libex/include/ex.h index 68166fb..2c668d3 100644 --- a/common/libex/include/ex.h +++ b/common/libex/include/ex.h @@ -1,4 +1,4 @@ -#ifndef __LIB_EX_H__ +#ifndef __LIB_EX_H__ #define __LIB_EX_H__ #ifdef EX_HAVE_CONFIG diff --git a/common/libex/include/ex/ex_const.h b/common/libex/include/ex/ex_const.h index 30604a7..699bf10 100644 --- a/common/libex/include/ex/ex_const.h +++ b/common/libex/include/ex/ex_const.h @@ -1,4 +1,4 @@ -#ifndef __LIB_EX_CONST_H__ +#ifndef __LIB_EX_CONST_H__ #define __LIB_EX_CONST_H__ #include "ex_platform.h" @@ -43,8 +43,8 @@ // error code. //==================================================== #define EXRV_OK 0 -#define EXRV_SYS_ERR 1 // ϵͳ󣬿ʹGetLastErrorerrnoȡֵ -#define EXRV_FAILED 2 // ʧ +#define EXRV_SYS_ERR 1 // 系统错误,可以使用GetLastError或者errno来获取具体错误值 +#define EXRV_FAILED 2 // 操作失败 //#define EXRV_CANNOT_FOUND 9 #define EXRV_CANNOT_CREATE 10 diff --git a/common/libex/include/ex/ex_ini.h b/common/libex/include/ex/ex_ini.h index 38749f5..681f05f 100644 --- a/common/libex/include/ex/ex_ini.h +++ b/common/libex/include/ex/ex_ini.h @@ -1,4 +1,4 @@ -#ifndef __EX_INI_H__ +#ifndef __EX_INI_H__ #define __EX_INI_H__ /* diff --git a/common/libex/include/ex/ex_log.h b/common/libex/include/ex/ex_log.h index 29a1b41..1cd89ab 100644 --- a/common/libex/include/ex/ex_log.h +++ b/common/libex/include/ex/ex_log.h @@ -1,4 +1,4 @@ -#ifndef __EX_LOG_H__ +#ifndef __EX_LOG_H__ #define __EX_LOG_H__ #include "ex_types.h" @@ -27,7 +27,7 @@ public: protected: bool _open_file(); - bool _rotate_file(void); // ־ļݣȻ¿һ־ļ + bool _rotate_file(void); // 将现有日志文件改名备份,然后新开一个日志文件 public: ExThreadLock lock; @@ -63,7 +63,7 @@ void EXLOG_USE_LOGGER(ExLogger* logger); void EXLOG_LEVEL(int min_level); void EXLOG_DEBUG(bool debug_mode); -// 趨־ļ·δָ·ΪִгĿ¼µlogĿ¼ +// 设定日志文件名及路径,如未指定路径,则为可执行程序所在目录下的log目录。 void EXLOG_FILE(const wchar_t* log_file, const wchar_t* log_path = NULL, ex_u32 max_filesize = EX_LOG_FILE_MAX_SIZE, ex_u8 max_filecount = EX_LOG_FILE_MAX_COUNT); void EXLOG_CONSOLE(bool output_to_console); diff --git a/common/libex/include/ex/ex_path.h b/common/libex/include/ex/ex_path.h index 2f1fa19..20ac79e 100644 --- a/common/libex/include/ex/ex_path.h +++ b/common/libex/include/ex/ex_path.h @@ -1,4 +1,4 @@ -#ifndef __LIB_EX_PATH_H__ +#ifndef __LIB_EX_PATH_H__ #define __LIB_EX_PATH_H__ #include "ex_platform.h" @@ -39,7 +39,7 @@ bool ex_path_join(ex_wstr& inout_path, EX_BOOL auto_abspath, ...); bool ex_abspath_to(const ex_wstr& base_abs_path, const ex_wstr& relate_path, ex_wstr& out_path); bool ex_mkdirs(const ex_wstr& in_path); -// ȡļеչ֣.abc.py py +// 获取文件名中的扩展名部分(不包括.,例如abc.py,返回 py) bool ex_path_ext_name(const ex_wstr& in_filename, ex_wstr& out_ext); #endif diff --git a/common/libex/include/ex/ex_platform.h b/common/libex/include/ex/ex_platform.h index 3000efe..6a23715 100644 --- a/common/libex/include/ex/ex_platform.h +++ b/common/libex/include/ex/ex_platform.h @@ -1,4 +1,4 @@ -#ifndef __LIB_EX_PLATFORM_H__ +#ifndef __LIB_EX_PLATFORM_H__ #define __LIB_EX_PLATFORM_H__ #if defined(_WIN32) || defined(WIN32) diff --git a/common/libex/include/ex/ex_str.h b/common/libex/include/ex/ex_str.h index b5c4a43..070b999 100644 --- a/common/libex/include/ex/ex_str.h +++ b/common/libex/include/ex/ex_str.h @@ -1,86 +1,86 @@ -#ifndef __LIB_EX_STR_H__ -#define __LIB_EX_STR_H__ - -#include "ex_types.h" - -#define EX_CODEPAGE_ACP 0 -#define EX_CODEPAGE_UTF8 1 -#ifdef EX_OS_WIN32 -# define EX_CODEPAGE_DEFAULT EX_CODEPAGE_ACP -#else -# define EX_CODEPAGE_DEFAULT EX_CODEPAGE_UTF8 -#endif - -#define EX_RSC_BEGIN 0x01 -#define EX_RSC_END 0x02 -#define EX_RSC_ALL EX_RSC_BEGIN | EX_RSC_END - -//================================================= -// C Interface -//================================================= - -// copy a string from `source` to `target`. -// `size` is size of target buffer. -// if buffer is to small, NULL will return, but `size-1` characters have been copied. -char* ex_strcpy(char* target, size_t size, const char* source); -wchar_t* ex_wcscpy(wchar_t* target, size_t size, const wchar_t* source); - - -// dupilicate a string. -// must use ex_free() to release the returned value. -char* ex_strdup(const char* src); -wchar_t* ex_wcsdup(const wchar_t* src); - -// convert between mutli-bytes and wide char string. -// must use ex_free() to release the returned value. -wchar_t* ex_str2wcs_alloc(const char* in_buffer, int code_page); -char* ex_wcs2str_alloc(const wchar_t* in_buffer, int code_page); - -// convert char** argv to wchar_t** argv. -// must use ex_free_argv() to release the returned value. -wchar_t** ex_make_wargv(int argc, char** argv); -void ex_free_wargv(int argc, wchar_t** argv); - -EX_BOOL ex_str_only_white_space(const wchar_t* src); -EX_BOOL ex_wcs_only_white_space(const char* src); - - -int ex_strformat(char* out_buf, size_t buf_size, const char* fmt, ...); -int ex_wcsformat(wchar_t* out_buf, size_t buf_size, const wchar_t* fmt, ...); - -//================================================= -// C++ Interface -//================================================= -#ifdef __cplusplus - -#include -#include - -typedef std::string ex_astr; -typedef std::wstring ex_wstr; - -typedef std::vector ex_astrs; -typedef std::vector ex_wstrs; -typedef std::vector ex_str_utf16le; - -bool ex_wstr2astr(const ex_wstr& in_str, ex_astr& out_str, int code_page = EX_CODEPAGE_DEFAULT); -bool ex_wstr2astr(const wchar_t* in_str, ex_astr& out_str, int code_page = EX_CODEPAGE_DEFAULT); -bool ex_astr2wstr(const ex_astr& in_str, ex_wstr& out_str, int code_page = EX_CODEPAGE_DEFAULT); -bool ex_astr2wstr(const char* in_str, ex_wstr& out_str, int code_page = EX_CODEPAGE_DEFAULT); - -bool ex_only_white_space(const ex_astr& str_check); -bool ex_only_white_space(const ex_wstr& str_check); - -void ex_remove_white_space(ex_astr& str_fix, int ulFlag = EX_RSC_ALL); -void ex_remove_white_space(ex_wstr& str_fix, int ulFlag = EX_RSC_ALL); - -ex_astr& ex_replace_all(ex_astr& str, const ex_astr& old_value, const ex_astr& new_value); -ex_wstr& ex_replace_all(ex_wstr& str, const ex_wstr& old_value, const ex_wstr& new_value); - -// UTF8ַתΪUTF16-LEַ\0 -bool ex_utf8_to_utf16le(const std::string& from, ex_str_utf16le& to); - -#endif - - -#endif // __LIB_EX_STR_H__ +#ifndef __LIB_EX_STR_H__ +#define __LIB_EX_STR_H__ + +#include "ex_types.h" + +#define EX_CODEPAGE_ACP 0 +#define EX_CODEPAGE_UTF8 1 +#ifdef EX_OS_WIN32 +# define EX_CODEPAGE_DEFAULT EX_CODEPAGE_ACP +#else +# define EX_CODEPAGE_DEFAULT EX_CODEPAGE_UTF8 +#endif + +#define EX_RSC_BEGIN 0x01 +#define EX_RSC_END 0x02 +#define EX_RSC_ALL EX_RSC_BEGIN | EX_RSC_END + +//================================================= +// C Interface +//================================================= + +// copy a string from `source` to `target`. +// `size` is size of target buffer. +// if buffer is to small, NULL will return, but `size-1` characters have been copied. +char* ex_strcpy(char* target, size_t size, const char* source); +wchar_t* ex_wcscpy(wchar_t* target, size_t size, const wchar_t* source); + + +// dupilicate a string. +// must use ex_free() to release the returned value. +char* ex_strdup(const char* src); +wchar_t* ex_wcsdup(const wchar_t* src); + +// convert between mutli-bytes and wide char string. +// must use ex_free() to release the returned value. +wchar_t* ex_str2wcs_alloc(const char* in_buffer, int code_page); +char* ex_wcs2str_alloc(const wchar_t* in_buffer, int code_page); + +// convert char** argv to wchar_t** argv. +// must use ex_free_argv() to release the returned value. +wchar_t** ex_make_wargv(int argc, char** argv); +void ex_free_wargv(int argc, wchar_t** argv); + +EX_BOOL ex_str_only_white_space(const wchar_t* src); +EX_BOOL ex_wcs_only_white_space(const char* src); + + +int ex_strformat(char* out_buf, size_t buf_size, const char* fmt, ...); +int ex_wcsformat(wchar_t* out_buf, size_t buf_size, const wchar_t* fmt, ...); + +//================================================= +// C++ Interface +//================================================= +#ifdef __cplusplus + +#include +#include + +typedef std::string ex_astr; +typedef std::wstring ex_wstr; + +typedef std::vector ex_astrs; +typedef std::vector ex_wstrs; +typedef std::vector ex_str_utf16le; + +bool ex_wstr2astr(const ex_wstr& in_str, ex_astr& out_str, int code_page = EX_CODEPAGE_DEFAULT); +bool ex_wstr2astr(const wchar_t* in_str, ex_astr& out_str, int code_page = EX_CODEPAGE_DEFAULT); +bool ex_astr2wstr(const ex_astr& in_str, ex_wstr& out_str, int code_page = EX_CODEPAGE_DEFAULT); +bool ex_astr2wstr(const char* in_str, ex_wstr& out_str, int code_page = EX_CODEPAGE_DEFAULT); + +bool ex_only_white_space(const ex_astr& str_check); +bool ex_only_white_space(const ex_wstr& str_check); + +void ex_remove_white_space(ex_astr& str_fix, int ulFlag = EX_RSC_ALL); +void ex_remove_white_space(ex_wstr& str_fix, int ulFlag = EX_RSC_ALL); + +ex_astr& ex_replace_all(ex_astr& str, const ex_astr& old_value, const ex_astr& new_value); +ex_wstr& ex_replace_all(ex_wstr& str, const ex_wstr& old_value, const ex_wstr& new_value); + +// 将UTF8字符串转换为UTF16-LE字符串(输出结果包含\0结束符) +bool ex_utf8_to_utf16le(const std::string& from, ex_str_utf16le& to); + +#endif + + +#endif // __LIB_EX_STR_H__ diff --git a/common/libex/include/ex/ex_thread.h b/common/libex/include/ex/ex_thread.h index f3d4170..3948749 100644 --- a/common/libex/include/ex/ex_thread.h +++ b/common/libex/include/ex/ex_thread.h @@ -1,4 +1,4 @@ -#ifndef __EX_THREAD_H__ +#ifndef __EX_THREAD_H__ #define __EX_THREAD_H__ #include "ex_str.h" @@ -23,11 +23,11 @@ public: bool is_running(void) { return m_is_running; } - // ִ̣߳б˵run() + // 创建并启动线程(执行被重载了的run()函数) bool start(void); - // ̣߳ȴwait_timeout_ms룬wait_timeout_msΪ0޵ȴ + // 结束线程(等待wait_timeout_ms毫秒,如果wait_timeout_ms为0,则无限等待) bool stop(void); - // ֱӽ̣߳ǿɱʹã + // 直接结束线程(强杀,不建议使用) bool terminate(void); protected: @@ -52,7 +52,7 @@ protected: }; -// ߳ʹã +// 线程锁(进程内使用) class ExThreadLock { public: @@ -70,7 +70,7 @@ private: #endif }; -// ߳ +// 线程锁辅助类 class ExThreadSmartLock { public: @@ -109,12 +109,12 @@ private: }; -// ԭӲ +// 原子操作 int ex_atomic_add(volatile int* pt, int t); int ex_atomic_inc(volatile int* pt); int ex_atomic_dec(volatile int* pt); -// ߳ز +// 线程相关操作 ex_u64 ex_get_thread_id(void); #endif // __EX_THREAD_H__ diff --git a/common/libex/include/ex/ex_types.h b/common/libex/include/ex/ex_types.h index 0187b0a..3ad1a01 100644 --- a/common/libex/include/ex/ex_types.h +++ b/common/libex/include/ex/ex_types.h @@ -1,44 +1,44 @@ -#ifndef __LIB_EX_TYPE_H__ -#define __LIB_EX_TYPE_H__ - -#include "ex_platform.h" - -#include - -typedef signed char ex_i8; -typedef signed short ex_i16; - -typedef unsigned char ex_u8; -typedef unsigned short ex_u16; -typedef unsigned int ex_u32; -typedef unsigned long ex_ulong; - -#if defined(EX_OS_WIN32) -typedef unsigned __int64 ex_u64; -typedef signed __int64 ex_i64; -typedef wchar_t ex_utf16; -#else -typedef unsigned long long ex_u64; -typedef signed long long ex_i64; -typedef ex_i16 ex_utf16; -#endif - -typedef int EX_BOOL; -#define EX_TRUE 1 -#define EX_FALSE 0 - - -typedef std::vector ex_bin; -typedef std::vector ex_chars; - -typedef ex_u32 ex_rv; - - -#if defined(EX_OS_WIN32) -# define EX_DYLIB_HANDLE HINSTANCE -#else -# define EX_DYLIB_HANDLE void* -#endif - - -#endif // __LIB_EX_TYPE_H__ +#ifndef __LIB_EX_TYPE_H__ +#define __LIB_EX_TYPE_H__ + +#include "ex_platform.h" + +#include + +typedef signed char ex_i8; +typedef signed short ex_i16; + +typedef unsigned char ex_u8; +typedef unsigned short ex_u16; +typedef unsigned int ex_u32; +typedef unsigned long ex_ulong; + +#if defined(EX_OS_WIN32) +typedef unsigned __int64 ex_u64; +typedef signed __int64 ex_i64; +typedef wchar_t ex_utf16; +#else +typedef unsigned long long ex_u64; +typedef signed long long ex_i64; +typedef ex_i16 ex_utf16; +#endif + +typedef int EX_BOOL; +#define EX_TRUE 1 +#define EX_FALSE 0 + + +typedef std::vector ex_bin; +typedef std::vector ex_chars; + +typedef ex_u32 ex_rv; + + +#if defined(EX_OS_WIN32) +# define EX_DYLIB_HANDLE HINSTANCE +#else +# define EX_DYLIB_HANDLE void* +#endif + + +#endif // __LIB_EX_TYPE_H__ diff --git a/common/libex/include/ex/ex_util.h b/common/libex/include/ex/ex_util.h index 06b78ec..17b2556 100644 --- a/common/libex/include/ex/ex_util.h +++ b/common/libex/include/ex/ex_util.h @@ -1,55 +1,55 @@ -#ifndef __LIB_EX_UTIL_H__ -#define __LIB_EX_UTIL_H__ - -#include "ex_types.h" -#include "ex_str.h" - -#ifdef EX_OS_WIN32 -# include -//# include -//# include -// #include -#pragma comment(lib, "ws2_32.lib") -#else -// #include -# include -# include -#endif - -EX_BOOL ex_initialize(const char* lc_ctype); - -void ex_free(void* buffer); - -// haystackΪhaystacklenֽڣвneedleΪneedlelenʼַNULLʾûҵ -const ex_u8* ex_memmem(const ex_u8* haystack, size_t haystacklen, const ex_u8* needle, size_t needlelen); -void ex_mem_reverse(ex_u8* p, size_t l); - -void ex_printf(const char* fmt, ...); -void ex_wprintf(const wchar_t* fmt, ...); - -ex_u64 ex_get_tick_count(void); -void ex_sleep_ms(int ms); - -EX_BOOL ex_localtime_now(int* t, struct tm* dt); - - -FILE* ex_fopen(const ex_wstr& filename, const wchar_t* mode); -FILE* ex_fopen(const ex_astr& filename, const char* mode); - -// open a text file and read all content. -bool ex_read_text_file(const ex_wstr& file_name, ex_astr& file_content); -// open a file and write content. -bool ex_write_text_file(const ex_wstr& file_name, const ex_astr& file_content); - -EX_DYLIB_HANDLE ex_dlopen(const wchar_t* dylib_path); -void ex_dlclose(EX_DYLIB_HANDLE dylib); - - -// inet... -int ex_ip4_name(const struct sockaddr_in* src, char* dst, size_t size); - -#define EX_IPV4_NAME_LEN 16 -#define EX_IPV6_NAME_LEN 46 -const char* ex_inet_ntop(int af, const void *src, char *dst, size_t size); - -#endif // __LIB_EX_UTIL_H__ +#ifndef __LIB_EX_UTIL_H__ +#define __LIB_EX_UTIL_H__ + +#include "ex_types.h" +#include "ex_str.h" + +#ifdef EX_OS_WIN32 +# include +//# include +//# include +// #include +#pragma comment(lib, "ws2_32.lib") +#else +// #include +# include +# include +#endif + +EX_BOOL ex_initialize(const char* lc_ctype); + +void ex_free(void* buffer); + +// 在haystack(长度为haystacklen字节)中查找needle(长度为needlelen)的起始地址,返回NULL表示没有找到 +const ex_u8* ex_memmem(const ex_u8* haystack, size_t haystacklen, const ex_u8* needle, size_t needlelen); +void ex_mem_reverse(ex_u8* p, size_t l); + +void ex_printf(const char* fmt, ...); +void ex_wprintf(const wchar_t* fmt, ...); + +ex_u64 ex_get_tick_count(void); +void ex_sleep_ms(int ms); + +EX_BOOL ex_localtime_now(int* t, struct tm* dt); + + +FILE* ex_fopen(const ex_wstr& filename, const wchar_t* mode); +FILE* ex_fopen(const ex_astr& filename, const char* mode); + +// open a text file and read all content. +bool ex_read_text_file(const ex_wstr& file_name, ex_astr& file_content); +// open a file and write content. +bool ex_write_text_file(const ex_wstr& file_name, const ex_astr& file_content); + +EX_DYLIB_HANDLE ex_dlopen(const wchar_t* dylib_path); +void ex_dlclose(EX_DYLIB_HANDLE dylib); + + +// inet... +int ex_ip4_name(const struct sockaddr_in* src, char* dst, size_t size); + +#define EX_IPV4_NAME_LEN 16 +#define EX_IPV6_NAME_LEN 46 +const char* ex_inet_ntop(int af, const void *src, char *dst, size_t size); + +#endif // __LIB_EX_UTIL_H__ diff --git a/common/libex/include/ex/ex_winsrv.h b/common/libex/include/ex/ex_winsrv.h index 91938ec..15ac6ee 100644 --- a/common/libex/include/ex/ex_winsrv.h +++ b/common/libex/include/ex/ex_winsrv.h @@ -1,4 +1,4 @@ -#ifndef __EX_WINSRV_H__ +#ifndef __EX_WINSRV_H__ #define __EX_WINSRV_H__ #include "ex_str.h" diff --git a/common/libex/src/ex_ini.cpp b/common/libex/src/ex_ini.cpp index 669b890..e471752 100644 --- a/common/libex/src/ex_ini.cpp +++ b/common/libex/src/ex_ini.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include @@ -241,7 +241,7 @@ bool ExIniFile::LoadFromFile(const ex_wstr& strFileName, bool bClearOld) { pOffset += 3; } - // ļʹUTF8 + // 配置文件均使用UTF8编码 ex_wstr fileData; if (!ex_astr2wstr(pOffset, fileData, EX_CODEPAGE_UTF8)) return false; @@ -357,7 +357,7 @@ void ExIniFile::Save(int codepage/* = EX_CODEPAGE_UTF8*/) return; } - // вκСڵֵԣȱ֮ + // 如果有不属于任何小节的值对,先保存之 if (m_dumy_sec.Count() > 0) m_dumy_sec.Save(file, codepage); @@ -415,16 +415,16 @@ ExIniSection* ExIniFile::GetSection(const ex_wstr& strName, bool bCreateIfNotExi } // static function. -// һУֵΪ [/ֵ/ע/ʲôҲ/] -// => strKey = [section_name] -// ֵ => strKey = strValue +// 解析一行,返回值为 [节名/值对/注释/什么也不是/出错了] +// 节名 => strKey = [section_name] +// 值对 => strKey = strValue ExIniFile::PARSE_RV ExIniFile::_ParseLine(const ex_wstr& strOrigLine, ex_wstr& strKey, ex_wstr& strValue) { - // ȥ׵Ŀո TAB + // 首先去掉行首的空格或者 TAB 控制 ex_wstr strLine(strOrigLine); ex_remove_white_space(strLine, EX_RSC_BEGIN); - // жǷΪע͡ .ini ļ ֺ';'/'#' Ϊעеĵһַ + // 判断是否为注释。 .ini 文件以 分号';'/'#' 作为注释行的第一个字符 if (';' == strLine[0] || '#' == strLine[0]) { return PARSE_COMMENT; @@ -432,7 +432,7 @@ ExIniFile::PARSE_RV ExIniFile::_ParseLine(const ex_wstr& strOrigLine, ex_wstr& s if ('[' == strLine[0]) { - // һ(section) + // 这是一个节(section) ex_wstr::size_type startPos = strLine.find('['); ex_wstr::size_type endPos = strLine.rfind(']'); strLine.erase(endPos); @@ -443,23 +443,23 @@ ExIniFile::PARSE_RV ExIniFile::_ParseLine(const ex_wstr& strOrigLine, ex_wstr& s } else { - // ܷҵȺ(=) key=value б𷽷 + // 看看能否找到等号(=),这是 key=value 的判别方法 ex_wstr::size_type pos = strLine.find('='); if (ex_wstr::npos == pos) { - //return PARSE_OTHER; // ûеȺ + //return PARSE_OTHER; // 没有等号 ex_remove_white_space(strLine); strKey = strLine; strValue.clear(); return PARSE_KEYVALUE; } - // ȺǰȺźķָ + // 将等号前面的与等号后面的分割 strKey.assign(strLine, 0, pos); strValue.assign(strLine, pos + 1, strLine.length() - pos); ex_remove_white_space(strKey); - // ȺźӦԭⲻӦƳհַ + // 等号后面的应该原封不动,不应该移除空白字符 ex_remove_white_space(strValue, EX_RSC_BEGIN); return PARSE_KEYVALUE; @@ -489,7 +489,7 @@ bool ExIniFile::_ProcessLine(const ex_wstr strLine, ExIniSection** pCurSection) break; case PARSE_SECTION: { - // һ + // 创建一个节 ExIniSection* pSection = GetSection(strKey, true); if (NULL == pSection) { @@ -508,7 +508,7 @@ bool ExIniFile::_ProcessLine(const ex_wstr strLine, ExIniSection** pCurSection) *pCurSection = &m_dumy_sec; } - // һֵ + // 创建一个值对 if (!(*pCurSection)->SetValue(strKey, strValue, true)) { bError = true; diff --git a/common/libex/src/ex_log.cpp b/common/libex/src/ex_log.cpp index 7a2f9fa..4810a37 100644 --- a/common/libex/src/ex_log.cpp +++ b/common/libex/src/ex_log.cpp @@ -1,517 +1,517 @@ -#include -#include -//#include -//#include -//#include -//#include - -#ifdef EX_OS_WIN32 -# include -# include -# include -#else -//# include -//# include -#endif - -#define EX_LOG_CONTENT_MAX_LEN 2048 - -//typedef std::deque log_file_deque; - -static ExLogger* g_exlog = NULL; - -void EXLOG_USE_LOGGER(ExLogger* logger) -{ - g_exlog = logger; -} - -void EXLOG_LEVEL(int min_level) -{ - if(NULL != g_exlog) - g_exlog->min_level = min_level; -} - -void EXLOG_DEBUG(bool debug_mode) -{ - if (NULL != g_exlog) - g_exlog->debug_mode = debug_mode; -} - -void EXLOG_CONSOLE(bool output_to_console) -{ - if(NULL != g_exlog) - g_exlog->to_console = output_to_console; -} - -void EXLOG_FILE(const wchar_t* log_file, const wchar_t* log_path /*= NULL*/, ex_u32 max_filesize /*= EX_LOG_FILE_MAX_SIZE*/, ex_u8 max_filecount /*= EX_LOG_FILE_MAX_COUNT*/) -{ - if(NULL == g_exlog) - return; - - ex_wstr _path; - if (NULL == log_path) - { - ex_exec_file(_path); - ex_dirname(_path); - ex_path_join(_path, false, L"log", NULL); - } - else - { - _path = log_path; - } - - g_exlog->set_log_file(_path, log_file, max_filesize, max_filecount); -} - -ExLogger::ExLogger() -{ -#ifdef EX_OS_WIN32 - console_handle = GetStdHandle(STD_OUTPUT_HANDLE); -#endif - - min_level = EX_LOG_LEVEL_INFO; - debug_mode = false; - to_console = true; - - m_file = NULL; - m_filesize = 0; -} - -ExLogger::~ExLogger() -{ - if (NULL != m_file) - { -#ifdef EX_OS_WIN32 - CloseHandle(m_file); -#else - fclose(m_file); -#endif - m_file = NULL; - } -} - -void ExLogger::log_a(int level, const char* fmt, va_list valist) -{ - if (NULL == fmt) - return; - - if (0 == strlen(fmt)) - return; - - char szTmp[4096] = { 0 }; - size_t offset = 0; - - if (level == EX_LOG_LEVEL_ERROR) - { - szTmp[0] = '['; - szTmp[1] = 'E'; - szTmp[2] = ']'; - szTmp[3] = ' '; - offset = 4; - } - -#ifdef EX_OS_WIN32 - vsnprintf_s(szTmp+offset, 4096-offset, 4095-offset, fmt, valist); - if(to_console) - { - if (NULL != console_handle) - { - printf_s("%s", szTmp); - fflush(stdout); - } - else - { - if(debug_mode) - OutputDebugStringA(szTmp); - } - } -#else - vsnprintf(szTmp+offset, 4095-offset, fmt, valist); - if(to_console) - { - // On linux, the stdout only output the first time output format (char or wchar_t). - // e.g.: first time you use printf(), then after that, every wprintf() not work, and vice versa. - // so we always use wprintf() to fix that. - - ex_astr tmp(szTmp); - ex_wstr _tmp; - ex_astr2wstr(tmp, _tmp); - wprintf(L"%ls", _tmp.c_str()); - fflush(stdout); - -// printf("%s", szTmp); -// fflush(stdout); - } -#endif - - write_a(szTmp); -} - -void ExLogger::log_w(int level, const wchar_t* fmt, va_list valist) -{ - if (NULL == fmt || 0 == wcslen(fmt)) - return; - - wchar_t szTmp[4096] = { 0 }; - size_t offset = 0; - - if (level == EX_LOG_LEVEL_ERROR) - { - szTmp[0] = L'['; - szTmp[1] = L'E'; - szTmp[2] = L']'; - szTmp[3] = L' '; - offset = 4; - } - -#ifdef EX_OS_WIN32 - _vsnwprintf_s(szTmp+offset, 4096-offset, 4095-offset, fmt, valist); - if(to_console) - { - if (NULL != console_handle) - { - wprintf_s(_T("%s"), szTmp); - fflush(stdout); - } - else - { - if(debug_mode) - OutputDebugStringW(szTmp); - } - } -#else - vswprintf(szTmp+offset, 4095-offset, fmt, valist); - if(to_console) - { - wprintf(L"%ls", szTmp); - fflush(stdout); - } -#endif - - write_w(szTmp); -} - -#define EX_PRINTF_XA(fn, level) \ -void fn(const char* fmt, ...) \ -{ \ - if(NULL == g_exlog) \ - return; \ - if (g_exlog->min_level > level) \ - return; \ - ExThreadSmartLock locker(g_exlog->lock); \ - va_list valist; \ - va_start(valist, fmt); \ - g_exlog->log_a(level, fmt, valist); \ - va_end(valist); \ -} - -#define EX_PRINTF_XW(fn, level) \ -void fn(const wchar_t* fmt, ...) \ -{ \ - if(NULL == g_exlog) \ - return; \ - if (g_exlog->min_level > level) \ - return; \ - ExThreadSmartLock locker(g_exlog->lock); \ - va_list valist; \ - va_start(valist, fmt); \ - g_exlog->log_w(level, fmt, valist); \ - va_end(valist); \ -} - -EX_PRINTF_XA(ex_printf_d, EX_LOG_LEVEL_DEBUG) -EX_PRINTF_XA(ex_printf_v, EX_LOG_LEVEL_VERBOSE) -EX_PRINTF_XA(ex_printf_i, EX_LOG_LEVEL_INFO) -EX_PRINTF_XA(ex_printf_w, EX_LOG_LEVEL_WARN) -EX_PRINTF_XA(ex_printf_e, EX_LOG_LEVEL_ERROR) - -EX_PRINTF_XW(ex_printf_d, EX_LOG_LEVEL_DEBUG) -EX_PRINTF_XW(ex_printf_v, EX_LOG_LEVEL_VERBOSE) -EX_PRINTF_XW(ex_printf_i, EX_LOG_LEVEL_INFO) -EX_PRINTF_XW(ex_printf_w, EX_LOG_LEVEL_WARN) -EX_PRINTF_XW(ex_printf_e, EX_LOG_LEVEL_ERROR) - - -#ifdef EX_OS_WIN32 -void ex_printf_e_lasterror(const char* fmt, ...) -{ - ExThreadSmartLock locker(g_exlog->lock); - - va_list valist; - va_start(valist, fmt); - g_exlog->log_a(EX_LOG_LEVEL_ERROR, fmt, valist); - va_end(valist); - - //========================================= - - LPVOID lpMsgBuf; - DWORD dw = GetLastError(); - - FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, dw, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), - (LPSTR)&lpMsgBuf, 0, NULL); - - ex_printf_e(" - WinErr(%d): %s\n", dw, (LPSTR)lpMsgBuf); - LocalFree(lpMsgBuf); -} - -void ex_printf_e_lasterror(const wchar_t* fmt, ...) -{ - ExThreadSmartLock locker(g_exlog->lock); - - va_list valist; - va_start(valist, fmt); - g_exlog->log_w(EX_LOG_LEVEL_ERROR, fmt, valist); - va_end(valist); - - //========================================= - - LPVOID lpMsgBuf; - DWORD dw = GetLastError(); - - FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, dw, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), - (LPSTR)&lpMsgBuf, 0, NULL); - - ex_printf_e(" - WinErr(%d): %s\n", dw, (LPSTR)lpMsgBuf); - LocalFree(lpMsgBuf); -} -#endif - -void ex_printf_bin(const ex_u8* bin_data, size_t bin_size, const char* fmt, ...) -{ - if(NULL == g_exlog) - return; - if (!g_exlog->debug_mode) - return; - - ExThreadSmartLock locker(g_exlog->lock); - - va_list valist; - va_start(valist, fmt); - g_exlog->log_a(EX_LOG_LEVEL_DEBUG, fmt, valist); - va_end(valist); - - ex_printf_d(" (%d/0x%02x Bytes)\n", bin_size, bin_size); - - const ex_u8* line = bin_data; - size_t thisline = 0; - size_t offset = 0; - unsigned int i = 0; - - char szTmp[128] = { 0 }; - size_t _offset = 0; - - while (offset < bin_size) - { - memset(szTmp, 0, 128); - _offset = 0; - - snprintf(szTmp + _offset, 128 - _offset, "%06x ", (int)offset); - _offset += 8; - - thisline = bin_size - offset; - if (thisline > 16) - thisline = 16; - - for (i = 0; i < thisline; i++) - { - snprintf(szTmp + _offset, 128 - _offset, "%02x ", line[i]); - _offset += 3; - } - - snprintf(szTmp + _offset, 128 - _offset, " "); - _offset += 2; - - for (; i < 16; i++) - { - snprintf(szTmp + _offset, 128 - _offset, " "); - _offset += 3; - } - - for (i = 0; i < thisline; i++) - { - snprintf(szTmp + _offset, 128 - _offset, "%c", (line[i] >= 0x20 && line[i] < 0x7f) ? line[i] : '.'); - _offset += 1; - } - - snprintf(szTmp + _offset, 128 - _offset, "\n"); - _offset += 1; - - ex_printf_d("%s", szTmp); - - offset += thisline; - line += thisline; - } - - fflush(stdout); -} - -bool ExLogger::set_log_file(const ex_wstr& log_path, const ex_wstr& log_name, ex_u32 max_filesize, ex_u8 max_count) -{ - m_max_filesize = max_filesize; - m_max_count = max_count; - - m_filename = log_name; - - m_path = log_path; - ex_abspath(m_path); - - ex_mkdirs(m_path); - - m_fullname = m_path; - ex_path_join(m_fullname, false, log_name.c_str(), NULL); - - return _open_file(); -} - - -bool ExLogger::_open_file() -{ - if (m_file) - { -#ifdef EX_OS_WIN32 - CloseHandle(m_file); -#else - fclose(m_file); -#endif - m_file = NULL; - } - -#ifdef EX_OS_WIN32 - // ע⣺ʹ CreateFile() ־ļʹFILEָ޷ݸ̬в - m_file = CreateFileW(m_fullname.c_str(), GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); - if (INVALID_HANDLE_VALUE == m_file) - { - m_file = NULL; - return false; - } - - SetFilePointer(m_file, 0, NULL, FILE_END); - m_filesize = GetFileSize(m_file, NULL); -#else - ex_astr _fullname; - ex_wstr2astr(m_fullname, _fullname); - m_file = fopen(_fullname.c_str(), "a"); - - if (NULL == m_file) - { - return false; - } - - fseek(m_file, 0, SEEK_END); - m_filesize = (ex_u32)ftell(m_file); -#endif - - return _rotate_file(); -} - -bool ExLogger::_rotate_file(void) -{ - if (m_filesize < m_max_filesize) - return true; - - if (m_file) - { -#ifdef EX_OS_WIN32 - CloseHandle(m_file); -#else - fclose(m_file); -#endif - m_file = NULL; - } - - // make a name for backup file. - wchar_t _tmpname[64] = { 0 }; -#ifdef EX_OS_WIN32 - SYSTEMTIME st; - GetLocalTime(&st); - swprintf_s(_tmpname, 64, L"%s.%04d%02d%02d%02d%02d%02d.bak", m_filename.c_str(), st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond); -#else - time_t timep; - time(&timep); - struct tm *p = localtime(&timep); - if (p == NULL) - return false; - - ex_wcsformat(_tmpname, 64, L"%ls.%04d%02d%02d%02d%02d%02d.bak", m_filename.c_str(), p->tm_year + 1900, p->tm_mon + 1, p->tm_mday, p->tm_hour, p->tm_min, p->tm_sec); -#endif - - ex_wstr _new_fullname(m_path); - ex_path_join(_new_fullname, false, _tmpname, NULL); - -#ifdef EX_OS_WIN32 - if (!MoveFileW(m_fullname.c_str(), _new_fullname.c_str())) - { - EXLOGE_WIN("can not rename log file, remove old one and try again."); - DeleteFileW(_new_fullname.c_str()); - if (!MoveFileW(m_fullname.c_str(), _new_fullname.c_str())) - return false; - } -#else - ex_astr _a_fullname; - ex_astr _a_new_fullname; - ex_wstr2astr(m_fullname, _a_fullname); - ex_wstr2astr(_new_fullname, _a_new_fullname); - - if (rename(_a_fullname.c_str(), _a_new_fullname.c_str()) != 0) - { - remove(_a_new_fullname.c_str()); - if (0 != (rename(_a_fullname.c_str(), _a_new_fullname.c_str()))) - return false; - } -#endif - - return _open_file(); -} - -bool ExLogger::write_a(const char* buf) -{ - if (NULL == m_file) - return false; - - size_t len = strlen(buf); - - if (len > EX_LOG_CONTENT_MAX_LEN) - return false; - - char szTime[100] = { 0 }; -#ifdef EX_OS_WIN32 - SYSTEMTIME st; - GetLocalTime(&st); - sprintf_s(szTime, 100, "[%04d-%02d-%02d %02d:%02d:%02d] ", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond); - - int lenTime = strlen(szTime); - DWORD dwWritten = 0; - WriteFile(m_file, szTime, lenTime, &dwWritten, NULL); - m_filesize += lenTime; - WriteFile(m_file, buf, len, &dwWritten, NULL); - m_filesize += len; - FlushFileBuffers(m_file); -#else - time_t timep; - struct tm *p; - time(&timep); - p = localtime(&timep); - if (p == NULL) - return false; - sprintf(szTime, "[%04d-%02d-%02d %02d:%02d:%02d] ", p->tm_year + 1900, p->tm_mon + 1, p->tm_mday, p->tm_hour, p->tm_min, p->tm_sec); - - size_t lenTime = strlen(szTime); - fwrite(szTime, lenTime, 1, m_file); - m_filesize += lenTime; - fwrite(buf, len, 1, m_file); - m_filesize += len; - fflush(m_file); -#endif - - - return _rotate_file(); -} - -bool ExLogger::write_w(const wchar_t* buf) -{ - ex_astr _buf; - ex_wstr2astr(buf, _buf, EX_CODEPAGE_UTF8); - return write_a(_buf.c_str()); -} +#include +#include +//#include +//#include +//#include +//#include + +#ifdef EX_OS_WIN32 +# include +# include +# include +#else +//# include +//# include +#endif + +#define EX_LOG_CONTENT_MAX_LEN 2048 + +//typedef std::deque log_file_deque; + +static ExLogger* g_exlog = NULL; + +void EXLOG_USE_LOGGER(ExLogger* logger) +{ + g_exlog = logger; +} + +void EXLOG_LEVEL(int min_level) +{ + if(NULL != g_exlog) + g_exlog->min_level = min_level; +} + +void EXLOG_DEBUG(bool debug_mode) +{ + if (NULL != g_exlog) + g_exlog->debug_mode = debug_mode; +} + +void EXLOG_CONSOLE(bool output_to_console) +{ + if(NULL != g_exlog) + g_exlog->to_console = output_to_console; +} + +void EXLOG_FILE(const wchar_t* log_file, const wchar_t* log_path /*= NULL*/, ex_u32 max_filesize /*= EX_LOG_FILE_MAX_SIZE*/, ex_u8 max_filecount /*= EX_LOG_FILE_MAX_COUNT*/) +{ + if(NULL == g_exlog) + return; + + ex_wstr _path; + if (NULL == log_path) + { + ex_exec_file(_path); + ex_dirname(_path); + ex_path_join(_path, false, L"log", NULL); + } + else + { + _path = log_path; + } + + g_exlog->set_log_file(_path, log_file, max_filesize, max_filecount); +} + +ExLogger::ExLogger() +{ +#ifdef EX_OS_WIN32 + console_handle = GetStdHandle(STD_OUTPUT_HANDLE); +#endif + + min_level = EX_LOG_LEVEL_INFO; + debug_mode = false; + to_console = true; + + m_file = NULL; + m_filesize = 0; +} + +ExLogger::~ExLogger() +{ + if (NULL != m_file) + { +#ifdef EX_OS_WIN32 + CloseHandle(m_file); +#else + fclose(m_file); +#endif + m_file = NULL; + } +} + +void ExLogger::log_a(int level, const char* fmt, va_list valist) +{ + if (NULL == fmt) + return; + + if (0 == strlen(fmt)) + return; + + char szTmp[4096] = { 0 }; + size_t offset = 0; + + if (level == EX_LOG_LEVEL_ERROR) + { + szTmp[0] = '['; + szTmp[1] = 'E'; + szTmp[2] = ']'; + szTmp[3] = ' '; + offset = 4; + } + +#ifdef EX_OS_WIN32 + vsnprintf_s(szTmp+offset, 4096-offset, 4095-offset, fmt, valist); + if(to_console) + { + if (NULL != console_handle) + { + printf_s("%s", szTmp); + fflush(stdout); + } + else + { + if(debug_mode) + OutputDebugStringA(szTmp); + } + } +#else + vsnprintf(szTmp+offset, 4095-offset, fmt, valist); + if(to_console) + { + // On linux, the stdout only output the first time output format (char or wchar_t). + // e.g.: first time you use printf(), then after that, every wprintf() not work, and vice versa. + // so we always use wprintf() to fix that. + + ex_astr tmp(szTmp); + ex_wstr _tmp; + ex_astr2wstr(tmp, _tmp); + wprintf(L"%ls", _tmp.c_str()); + fflush(stdout); + +// printf("%s", szTmp); +// fflush(stdout); + } +#endif + + write_a(szTmp); +} + +void ExLogger::log_w(int level, const wchar_t* fmt, va_list valist) +{ + if (NULL == fmt || 0 == wcslen(fmt)) + return; + + wchar_t szTmp[4096] = { 0 }; + size_t offset = 0; + + if (level == EX_LOG_LEVEL_ERROR) + { + szTmp[0] = L'['; + szTmp[1] = L'E'; + szTmp[2] = L']'; + szTmp[3] = L' '; + offset = 4; + } + +#ifdef EX_OS_WIN32 + _vsnwprintf_s(szTmp+offset, 4096-offset, 4095-offset, fmt, valist); + if(to_console) + { + if (NULL != console_handle) + { + wprintf_s(_T("%s"), szTmp); + fflush(stdout); + } + else + { + if(debug_mode) + OutputDebugStringW(szTmp); + } + } +#else + vswprintf(szTmp+offset, 4095-offset, fmt, valist); + if(to_console) + { + wprintf(L"%ls", szTmp); + fflush(stdout); + } +#endif + + write_w(szTmp); +} + +#define EX_PRINTF_XA(fn, level) \ +void fn(const char* fmt, ...) \ +{ \ + if(NULL == g_exlog) \ + return; \ + if (g_exlog->min_level > level) \ + return; \ + ExThreadSmartLock locker(g_exlog->lock); \ + va_list valist; \ + va_start(valist, fmt); \ + g_exlog->log_a(level, fmt, valist); \ + va_end(valist); \ +} + +#define EX_PRINTF_XW(fn, level) \ +void fn(const wchar_t* fmt, ...) \ +{ \ + if(NULL == g_exlog) \ + return; \ + if (g_exlog->min_level > level) \ + return; \ + ExThreadSmartLock locker(g_exlog->lock); \ + va_list valist; \ + va_start(valist, fmt); \ + g_exlog->log_w(level, fmt, valist); \ + va_end(valist); \ +} + +EX_PRINTF_XA(ex_printf_d, EX_LOG_LEVEL_DEBUG) +EX_PRINTF_XA(ex_printf_v, EX_LOG_LEVEL_VERBOSE) +EX_PRINTF_XA(ex_printf_i, EX_LOG_LEVEL_INFO) +EX_PRINTF_XA(ex_printf_w, EX_LOG_LEVEL_WARN) +EX_PRINTF_XA(ex_printf_e, EX_LOG_LEVEL_ERROR) + +EX_PRINTF_XW(ex_printf_d, EX_LOG_LEVEL_DEBUG) +EX_PRINTF_XW(ex_printf_v, EX_LOG_LEVEL_VERBOSE) +EX_PRINTF_XW(ex_printf_i, EX_LOG_LEVEL_INFO) +EX_PRINTF_XW(ex_printf_w, EX_LOG_LEVEL_WARN) +EX_PRINTF_XW(ex_printf_e, EX_LOG_LEVEL_ERROR) + + +#ifdef EX_OS_WIN32 +void ex_printf_e_lasterror(const char* fmt, ...) +{ + ExThreadSmartLock locker(g_exlog->lock); + + va_list valist; + va_start(valist, fmt); + g_exlog->log_a(EX_LOG_LEVEL_ERROR, fmt, valist); + va_end(valist); + + //========================================= + + LPVOID lpMsgBuf; + DWORD dw = GetLastError(); + + FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, dw, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPSTR)&lpMsgBuf, 0, NULL); + + ex_printf_e(" - WinErr(%d): %s\n", dw, (LPSTR)lpMsgBuf); + LocalFree(lpMsgBuf); +} + +void ex_printf_e_lasterror(const wchar_t* fmt, ...) +{ + ExThreadSmartLock locker(g_exlog->lock); + + va_list valist; + va_start(valist, fmt); + g_exlog->log_w(EX_LOG_LEVEL_ERROR, fmt, valist); + va_end(valist); + + //========================================= + + LPVOID lpMsgBuf; + DWORD dw = GetLastError(); + + FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, dw, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPSTR)&lpMsgBuf, 0, NULL); + + ex_printf_e(" - WinErr(%d): %s\n", dw, (LPSTR)lpMsgBuf); + LocalFree(lpMsgBuf); +} +#endif + +void ex_printf_bin(const ex_u8* bin_data, size_t bin_size, const char* fmt, ...) +{ + if(NULL == g_exlog) + return; + if (!g_exlog->debug_mode) + return; + + ExThreadSmartLock locker(g_exlog->lock); + + va_list valist; + va_start(valist, fmt); + g_exlog->log_a(EX_LOG_LEVEL_DEBUG, fmt, valist); + va_end(valist); + + ex_printf_d(" (%d/0x%02x Bytes)\n", bin_size, bin_size); + + const ex_u8* line = bin_data; + size_t thisline = 0; + size_t offset = 0; + unsigned int i = 0; + + char szTmp[128] = { 0 }; + size_t _offset = 0; + + while (offset < bin_size) + { + memset(szTmp, 0, 128); + _offset = 0; + + snprintf(szTmp + _offset, 128 - _offset, "%06x ", (int)offset); + _offset += 8; + + thisline = bin_size - offset; + if (thisline > 16) + thisline = 16; + + for (i = 0; i < thisline; i++) + { + snprintf(szTmp + _offset, 128 - _offset, "%02x ", line[i]); + _offset += 3; + } + + snprintf(szTmp + _offset, 128 - _offset, " "); + _offset += 2; + + for (; i < 16; i++) + { + snprintf(szTmp + _offset, 128 - _offset, " "); + _offset += 3; + } + + for (i = 0; i < thisline; i++) + { + snprintf(szTmp + _offset, 128 - _offset, "%c", (line[i] >= 0x20 && line[i] < 0x7f) ? line[i] : '.'); + _offset += 1; + } + + snprintf(szTmp + _offset, 128 - _offset, "\n"); + _offset += 1; + + ex_printf_d("%s", szTmp); + + offset += thisline; + line += thisline; + } + + fflush(stdout); +} + +bool ExLogger::set_log_file(const ex_wstr& log_path, const ex_wstr& log_name, ex_u32 max_filesize, ex_u8 max_count) +{ + m_max_filesize = max_filesize; + m_max_count = max_count; + + m_filename = log_name; + + m_path = log_path; + ex_abspath(m_path); + + ex_mkdirs(m_path); + + m_fullname = m_path; + ex_path_join(m_fullname, false, log_name.c_str(), NULL); + + return _open_file(); +} + + +bool ExLogger::_open_file() +{ + if (m_file) + { +#ifdef EX_OS_WIN32 + CloseHandle(m_file); +#else + fclose(m_file); +#endif + m_file = NULL; + } + +#ifdef EX_OS_WIN32 + // 注意:这里必须使用 CreateFile() 来打开日志文件,使用FILE指针无法传递给动态库进行操作。 + m_file = CreateFileW(m_fullname.c_str(), GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if (INVALID_HANDLE_VALUE == m_file) + { + m_file = NULL; + return false; + } + + SetFilePointer(m_file, 0, NULL, FILE_END); + m_filesize = GetFileSize(m_file, NULL); +#else + ex_astr _fullname; + ex_wstr2astr(m_fullname, _fullname); + m_file = fopen(_fullname.c_str(), "a"); + + if (NULL == m_file) + { + return false; + } + + fseek(m_file, 0, SEEK_END); + m_filesize = (ex_u32)ftell(m_file); +#endif + + return _rotate_file(); +} + +bool ExLogger::_rotate_file(void) +{ + if (m_filesize < m_max_filesize) + return true; + + if (m_file) + { +#ifdef EX_OS_WIN32 + CloseHandle(m_file); +#else + fclose(m_file); +#endif + m_file = NULL; + } + + // make a name for backup file. + wchar_t _tmpname[64] = { 0 }; +#ifdef EX_OS_WIN32 + SYSTEMTIME st; + GetLocalTime(&st); + swprintf_s(_tmpname, 64, L"%s.%04d%02d%02d%02d%02d%02d.bak", m_filename.c_str(), st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond); +#else + time_t timep; + time(&timep); + struct tm *p = localtime(&timep); + if (p == NULL) + return false; + + ex_wcsformat(_tmpname, 64, L"%ls.%04d%02d%02d%02d%02d%02d.bak", m_filename.c_str(), p->tm_year + 1900, p->tm_mon + 1, p->tm_mday, p->tm_hour, p->tm_min, p->tm_sec); +#endif + + ex_wstr _new_fullname(m_path); + ex_path_join(_new_fullname, false, _tmpname, NULL); + +#ifdef EX_OS_WIN32 + if (!MoveFileW(m_fullname.c_str(), _new_fullname.c_str())) + { + EXLOGE_WIN("can not rename log file, remove old one and try again."); + DeleteFileW(_new_fullname.c_str()); + if (!MoveFileW(m_fullname.c_str(), _new_fullname.c_str())) + return false; + } +#else + ex_astr _a_fullname; + ex_astr _a_new_fullname; + ex_wstr2astr(m_fullname, _a_fullname); + ex_wstr2astr(_new_fullname, _a_new_fullname); + + if (rename(_a_fullname.c_str(), _a_new_fullname.c_str()) != 0) + { + remove(_a_new_fullname.c_str()); + if (0 != (rename(_a_fullname.c_str(), _a_new_fullname.c_str()))) + return false; + } +#endif + + return _open_file(); +} + +bool ExLogger::write_a(const char* buf) +{ + if (NULL == m_file) + return false; + + size_t len = strlen(buf); + + if (len > EX_LOG_CONTENT_MAX_LEN) + return false; + + char szTime[100] = { 0 }; +#ifdef EX_OS_WIN32 + SYSTEMTIME st; + GetLocalTime(&st); + sprintf_s(szTime, 100, "[%04d-%02d-%02d %02d:%02d:%02d] ", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond); + + int lenTime = strlen(szTime); + DWORD dwWritten = 0; + WriteFile(m_file, szTime, lenTime, &dwWritten, NULL); + m_filesize += lenTime; + WriteFile(m_file, buf, len, &dwWritten, NULL); + m_filesize += len; + FlushFileBuffers(m_file); +#else + time_t timep; + struct tm *p; + time(&timep); + p = localtime(&timep); + if (p == NULL) + return false; + sprintf(szTime, "[%04d-%02d-%02d %02d:%02d:%02d] ", p->tm_year + 1900, p->tm_mon + 1, p->tm_mday, p->tm_hour, p->tm_min, p->tm_sec); + + size_t lenTime = strlen(szTime); + fwrite(szTime, lenTime, 1, m_file); + m_filesize += lenTime; + fwrite(buf, len, 1, m_file); + m_filesize += len; + fflush(m_file); +#endif + + + return _rotate_file(); +} + +bool ExLogger::write_w(const wchar_t* buf) +{ + ex_astr _buf; + ex_wstr2astr(buf, _buf, EX_CODEPAGE_UTF8); + return write_a(_buf.c_str()); +} diff --git a/common/libex/src/ex_path.cpp b/common/libex/src/ex_path.cpp index fd3e7f4..4ac23d5 100644 --- a/common/libex/src/ex_path.cpp +++ b/common/libex/src/ex_path.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include diff --git a/common/libex/src/ex_str.cpp b/common/libex/src/ex_str.cpp index a3fac76..1aa6046 100644 --- a/common/libex/src/ex_str.cpp +++ b/common/libex/src/ex_str.cpp @@ -1,855 +1,855 @@ -#include -#include -#include - -char* ex_strcpy(char* target, size_t size, const char* source) -{ - if (target == source) - return target; - -#ifdef EX_OS_WIN32 - if (SUCCEEDED(StringCchCopyA(target, size, source))) - return target; - else - return NULL; -#else - size_t len = strlen(source); - if (size > len) - { - return strcpy(target, source); - } - else - { - memmove(target, source, size - 1); - return NULL; - } -#endif -} - -wchar_t* ex_wcscpy(wchar_t* target, size_t size, const wchar_t* source) -{ - if (target == source) - return target; - -#ifdef EX_OS_WIN32 - if (SUCCEEDED(StringCchCopyW(target, size, source))) - return target; - else - return NULL; -#else - size_t len = wcslen(source); - if (size > len) - { - return wcscpy(target, source); - } - else - { - memmove(target, source, (size - 1)*sizeof(wchar_t)); - return NULL; - } -#endif -} - -char* ex_strdup(const char* src) -{ - if (NULL == src) - return NULL; - size_t len = strlen(src) + 1; - char* ret = (char*)calloc(1, len); - memcpy(ret, src, len); - return ret; -} - -wchar_t* ex_wcsdup(const wchar_t* src) -{ - if (NULL == src) - return NULL; - size_t len = wcslen(src) + 1; - wchar_t* ret = (wchar_t*)calloc(sizeof(wchar_t), len); - memcpy(ret, src, sizeof(wchar_t)*len); - return ret; -} - -wchar_t* ex_str2wcs_alloc(const char* in_buffer, int code_page) -{ - wchar_t* out_buffer = NULL; -#ifdef EX_OS_WIN32 - int wlen = 0; - UINT _cp = 0; - if (code_page == EX_CODEPAGE_ACP) - _cp = CP_ACP; - else if (code_page == EX_CODEPAGE_UTF8) - _cp = CP_UTF8; - - wlen = MultiByteToWideChar(_cp, 0, in_buffer, -1, NULL, 0); - if (0 == wlen) - return NULL; - - out_buffer = (wchar_t*)calloc(wlen + 1, sizeof(wchar_t)); - if (NULL == out_buffer) - return NULL; - - wlen = MultiByteToWideChar(_cp, 0, in_buffer, -1, out_buffer, wlen); - if (0 == wlen) - { - free(out_buffer); - return NULL; - } - -#else - size_t wlen = 0; - wlen = mbstowcs(NULL, in_buffer, 0); - if (wlen <= 0) - return NULL; - - out_buffer = (wchar_t*)calloc(wlen + 1, sizeof(wchar_t)); - if (NULL == out_buffer) - return NULL; - - wlen = mbstowcs(out_buffer, in_buffer, wlen); - if (wlen <= 0) - { - free(out_buffer); - return NULL; - } - -#endif - - return out_buffer; -} - - -char* ex_wcs2str_alloc(const wchar_t* in_buffer, int code_page) -{ - char* out_buffer = NULL; - - if(NULL == in_buffer) - return NULL; - -#ifdef EX_OS_WIN32 - int len = 0; - UINT _cp = 0; - if (code_page == EX_CODEPAGE_ACP) - _cp = CP_ACP; - else if (code_page == EX_CODEPAGE_UTF8) - _cp = CP_UTF8; - - len = WideCharToMultiByte(_cp, 0, in_buffer, -1, NULL, 0, NULL, NULL); - if (0 == len) - return NULL; - - out_buffer = (char*)calloc(len + 1, sizeof(char)); - if (NULL == out_buffer) - return NULL; - - len = WideCharToMultiByte(_cp, 0, in_buffer, -1, out_buffer, len, NULL, NULL); - if (0 == len) - { - free(out_buffer); - return NULL; - } - -#else - size_t len = 0; - len = wcstombs(NULL, in_buffer, 0); - if (len <= 0) - return NULL; - - out_buffer = (char*)calloc(len + 1, sizeof(char)); - if (NULL == out_buffer) - return NULL; - - len = wcstombs(out_buffer, in_buffer, len); - if (len <= 0) - { - free(out_buffer); - return NULL; - } - -#endif - - return out_buffer; -} - -wchar_t** ex_make_wargv(int argc, char** argv) -{ - int i = 0; - wchar_t** ret = NULL; - - ret = (wchar_t**)calloc(argc + 1, sizeof(wchar_t*)); - if (!ret) - { - return NULL; - } - - for (i = 0; i < argc; ++i) - { - ret[i] = ex_str2wcs_alloc(argv[i], EX_CODEPAGE_DEFAULT); - if (NULL == ret[i]) - goto err; - } - - return ret; - -err: - ex_free_wargv(argc, ret); - return NULL; -} - -void ex_free_wargv(int argc, wchar_t** argv) -{ - int i = 0; - for (i = 0; i < argc; ++i) - free(argv[i]); - - free(argv); -} - -EX_BOOL ex_str_only_white_space(const wchar_t* src) -{ - if (ex_only_white_space(src)) - return EX_TRUE; - else - return EX_FALSE; -} - -EX_BOOL ex_wcs_only_white_space(const char* src) -{ - if (ex_only_white_space(src)) - return EX_TRUE; - else - return EX_FALSE; -} - -int ex_strformat(char* out_buf, size_t buf_size, const char* fmt, ...) -{ - int ret = 0; - va_list valist; - va_start(valist, fmt); - //_ts_printf_a(level, EX_COLOR_BLACK, fmt, valist); -#ifdef EX_OS_WIN32 - ret = vsnprintf(out_buf, buf_size, fmt, valist); -#else - ret = vsprintf(out_buf, fmt, valist); -#endif - va_end(valist); - return ret; -} - -int ex_wcsformat(wchar_t* out_buf, size_t buf_size, const wchar_t* fmt, ...) -{ - int ret = 0; - va_list valist; - va_start(valist, fmt); - //_ts_printf_a(level, EX_COLOR_BLACK, fmt, valist); -#ifdef EX_OS_WIN32 - //ret = vsnprintf(out_buf, buf_size, fmt, valist); - ret = _vsnwprintf_s(out_buf, buf_size, buf_size, fmt, valist); -#else - //ret = vsprintf(out_buf, fmt, valist); - ret = vswprintf(out_buf, buf_size, fmt, valist); -#endif - va_end(valist); - return ret; -} - - -#ifdef __cplusplus -bool ex_wstr2astr(const ex_wstr& in_str, ex_astr& out_str, int code_page/* = EX_CODEPAGE_DEFAULT*/) -{ - return ex_wstr2astr(in_str.c_str(), out_str, code_page); -} - -bool ex_wstr2astr(const wchar_t* in_str, ex_astr& out_str, int code_page/* = EX_CODEPAGE_DEFAULT*/) -{ - char* astr = ex_wcs2str_alloc(in_str, code_page); - if (NULL == astr) - return false; - - out_str = astr; - ex_free(astr); - return true; -} - -bool ex_astr2wstr(const ex_astr& in_str, ex_wstr& out_str, int code_page/* = EX_CODEPAGE_DEFAULT*/) -{ - return ex_astr2wstr(in_str.c_str(), out_str, code_page); -} - -bool ex_astr2wstr(const char* in_str, ex_wstr& out_str, int code_page/* = EX_CODEPAGE_DEFAULT*/) -{ - wchar_t* wstr = ex_str2wcs_alloc(in_str, code_page); - if (NULL == wstr) - return false; - - out_str = wstr; - ex_free(wstr); - return true; -} - -bool ex_only_white_space(const ex_astr& str_check) -{ - ex_astr::size_type pos = 0; - ex_astr strFilter(" \t\r\n"); - pos = str_check.find_first_not_of(strFilter); - if (ex_astr::npos == pos) - return true; - else - return false; -} - -bool ex_only_white_space(const ex_wstr& str_check) -{ - ex_wstr::size_type pos = 0; - ex_wstr strFilter(L" \t\r\n"); - pos = str_check.find_first_not_of(strFilter); - if (ex_wstr::npos == pos) - return true; - else - return false; -} - -void ex_remove_white_space(ex_astr& str_fix, int ulFlag /*= EX_RSC_ALL*/) -{ - ex_astr::size_type pos = 0; - ex_astr strFilter(" \t\r\n"); - - if (ulFlag & EX_RSC_BEGIN) - { - pos = str_fix.find_first_not_of(strFilter); - if (ex_astr::npos != pos) - str_fix.erase(0, pos); - // FIXME - } - if (ulFlag & EX_RSC_END) - { - pos = str_fix.find_last_not_of(strFilter); - if (ex_astr::npos != pos) - str_fix.erase(pos + 1); - // FIXME - } -} - -void ex_remove_white_space(ex_wstr& str_fix, int ulFlag /*= EX_RSC_ALL*/) -{ - ex_wstr::size_type pos = 0; - ex_wstr strFilter(L" \t\r\n"); - - if (ulFlag & EX_RSC_BEGIN) - { - pos = str_fix.find_first_not_of(strFilter); - if (ex_wstr::npos != pos) - str_fix.erase(0, pos); - // FIXME - } - if (ulFlag & EX_RSC_END) - { - pos = str_fix.find_last_not_of(strFilter); - if (ex_wstr::npos != pos) - str_fix.erase(pos + 1); - // FIXME - } -} - -ex_astr& ex_replace_all(ex_astr& str, const ex_astr& old_value, const ex_astr& new_value) -{ - for (ex_astr::size_type pos(0); pos != ex_astr::npos; pos += new_value.length()) - { - if ((pos = str.find(old_value, pos)) != ex_astr::npos) - str.replace(pos, old_value.length(), new_value); - else - break; - } - - return str; -} - -ex_wstr& ex_replace_all(ex_wstr& str, const ex_wstr& old_value, const ex_wstr& new_value) -{ - for (ex_wstr::size_type pos(0); pos != ex_wstr::npos; pos += new_value.length()) - { - if ((pos = str.find(old_value, pos)) != ex_wstr::npos) - str.replace(pos, old_value.length(), new_value); - else - break; - } - - return str; -} - - - -#ifndef EX_OS_WIN32 - -#define BYTE ex_u8 -#define DWORD ex_u32 -#define WCHAR ex_i16 -#define LPWSTR WCHAR* -#define BOOL int -#define TRUE 1 -#define FALSE 0 -#define UINT unsigned int -#define LPCSTR const char* -#define CP_UTF8 1 - -typedef enum -{ - conversionOK, /* conversion successful */ - sourceExhausted, /* partial character in source, but hit end */ - targetExhausted, /* insuff. room in target for conversion */ - sourceIllegal /* source sequence is illegal/malformed */ -} ConversionResult; - -typedef enum -{ - strictConversion = 0, - lenientConversion -} ConversionFlags; - -static const char trailingBytesForUTF8[256] = -{ - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, - 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, 3,3,3,3,3,3,3,3,4,4,4,4,5,5,5,5 -}; - -static const DWORD offsetsFromUTF8[6] = { 0x00000000UL, 0x00003080UL, 0x000E2080UL, 0x03C82080UL, 0xFA082080UL, 0x82082080UL -}; - -static const BYTE firstByteMark[7] = { 0x00, 0x00, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC }; - -static const int halfShift = 10; /* used for shifting by 10 bits */ - -static const DWORD halfBase = 0x0010000UL; -static const DWORD halfMask = 0x3FFUL; - -#define UNI_SUR_HIGH_START (DWORD)0xD800 -#define UNI_SUR_HIGH_END (DWORD)0xDBFF -#define UNI_SUR_LOW_START (DWORD)0xDC00 -#define UNI_SUR_LOW_END (DWORD)0xDFFF - -#define UNI_REPLACEMENT_CHAR (DWORD)0x0000FFFD -#define UNI_MAX_BMP (DWORD)0x0000FFFF -#define UNI_MAX_UTF16 (DWORD)0x0010FFFF -#define UNI_MAX_UTF32 (DWORD)0x7FFFFFFF -#define UNI_MAX_LEGAL_UTF32 (DWORD)0x0010FFFF - - -static ConversionResult ConvertUTF16toUTF8(const WCHAR** sourceStart, const WCHAR* sourceEnd, BYTE** targetStart, BYTE* targetEnd, ConversionFlags flags) -{ - BYTE* target; - const WCHAR* source; - BOOL computeLength; - ConversionResult result; - computeLength = (!targetEnd) ? TRUE : FALSE; - source = *sourceStart; - target = *targetStart; - result = conversionOK; - - while (source < sourceEnd) - { - DWORD ch; - unsigned short bytesToWrite = 0; - const DWORD byteMask = 0xBF; - const DWORD byteMark = 0x80; - const WCHAR* oldSource = source; /* In case we have to back up because of target overflow. */ - ch = *source++; - - /* If we have a surrogate pair, convert to UTF32 first. */ - if (ch >= UNI_SUR_HIGH_START && ch <= UNI_SUR_HIGH_END) - { - /* If the 16 bits following the high surrogate are in the source buffer... */ - if (source < sourceEnd) - { - DWORD ch2 = *source; - - /* If it's a low surrogate, convert to UTF32. */ - if (ch2 >= UNI_SUR_LOW_START && ch2 <= UNI_SUR_LOW_END) - { - ch = ((ch - UNI_SUR_HIGH_START) << halfShift) - + (ch2 - UNI_SUR_LOW_START) + halfBase; - ++source; - } - else if (flags == strictConversion) - { - /* it's an unpaired high surrogate */ - --source; /* return to the illegal value itself */ - result = sourceIllegal; - break; - } - } - else - { - /* We don't have the 16 bits following the high surrogate. */ - --source; /* return to the high surrogate */ - result = sourceExhausted; - break; - } - } - else if (flags == strictConversion) - { - /* UTF-16 surrogate values are illegal in UTF-32 */ - if (ch >= UNI_SUR_LOW_START && ch <= UNI_SUR_LOW_END) - { - --source; /* return to the illegal value itself */ - result = sourceIllegal; - break; - } - } - - /* Figure out how many bytes the result will require */ - if (ch < (DWORD)0x80) - { - bytesToWrite = 1; - } - else if (ch < (DWORD)0x800) - { - bytesToWrite = 2; - } - else if (ch < (DWORD)0x10000) - { - bytesToWrite = 3; - } - else if (ch < (DWORD)0x110000) - { - bytesToWrite = 4; - } - else - { - bytesToWrite = 3; - ch = UNI_REPLACEMENT_CHAR; - } - - target += bytesToWrite; - - if ((target > targetEnd) && (!computeLength)) - { - source = oldSource; /* Back up source pointer! */ - target -= bytesToWrite; - result = targetExhausted; - break; - } - - if (!computeLength) - { - switch (bytesToWrite) - { - /* note: everything falls through. */ - case 4: - *--target = (BYTE)((ch | byteMark) & byteMask); - ch >>= 6; - - case 3: - *--target = (BYTE)((ch | byteMark) & byteMask); - ch >>= 6; - - case 2: - *--target = (BYTE)((ch | byteMark) & byteMask); - ch >>= 6; - - case 1: - *--target = (BYTE)(ch | firstByteMark[bytesToWrite]); - } - } - else - { - switch (bytesToWrite) - { - /* note: everything falls through. */ - case 4: - --target; - ch >>= 6; - - case 3: - --target; - ch >>= 6; - - case 2: - --target; - ch >>= 6; - - case 1: - --target; - } - } - - target += bytesToWrite; - } - - *sourceStart = source; - *targetStart = target; - return result; -} - - -static BOOL isLegalUTF8(const BYTE* source, int length) -{ - BYTE a; - const BYTE* srcptr = source + length; - - switch (length) - { - default: - return FALSE; - - /* Everything else falls through when "TRUE"... */ - case 4: - if ((a = (*--srcptr)) < 0x80 || a > 0xBF) return FALSE; - - case 3: - if ((a = (*--srcptr)) < 0x80 || a > 0xBF) return FALSE; - - case 2: - if ((a = (*--srcptr)) > 0xBF) return FALSE; - - switch (*source) - { - /* no fall-through in this inner switch */ - case 0xE0: - if (a < 0xA0) return FALSE; - - break; - - case 0xED: - if (a > 0x9F) return FALSE; - - break; - - case 0xF0: - if (a < 0x90) return FALSE; - - break; - - case 0xF4: - if (a > 0x8F) return FALSE; - - break; - - default: - if (a < 0x80) return FALSE; - } - - case 1: - if (*source >= 0x80 && *source < 0xC2) return FALSE; - } - - if (*source > 0xF4) - return FALSE; - - return TRUE; -} - -static ConversionResult _ConvertUTF8toUTF16(const BYTE** sourceStart, const BYTE* sourceEnd, WCHAR** targetStart, WCHAR* targetEnd, ConversionFlags flags) -{ - WCHAR* target; - const BYTE* source; - BOOL computeLength; - ConversionResult result; - computeLength = (!targetEnd) ? TRUE : FALSE; - result = conversionOK; - source = *sourceStart; - target = *targetStart; - - while (source < sourceEnd) - { - DWORD ch = 0; - unsigned short extraBytesToRead = trailingBytesForUTF8[*source]; - - if ((source + extraBytesToRead) >= sourceEnd) - { - result = sourceExhausted; - break; - } - - /* Do this check whether lenient or strict */ - if (!isLegalUTF8(source, extraBytesToRead + 1)) - { - result = sourceIllegal; - break; - } - - /* - * The cases all fall through. See "Note A" below. - */ - switch (extraBytesToRead) - { - case 5: - ch += *source++; - ch <<= 6; /* remember, illegal UTF-8 */ - - case 4: - ch += *source++; - ch <<= 6; /* remember, illegal UTF-8 */ - - case 3: - ch += *source++; - ch <<= 6; - - case 2: - ch += *source++; - ch <<= 6; - - case 1: - ch += *source++; - ch <<= 6; - - case 0: - ch += *source++; - } - - ch -= offsetsFromUTF8[extraBytesToRead]; - - if ((target >= targetEnd) && (!computeLength)) - { - source -= (extraBytesToRead + 1); /* Back up source pointer! */ - result = targetExhausted; - break; - } - - if (ch <= UNI_MAX_BMP) - { - /* Target is a character <= 0xFFFF */ - /* UTF-16 surrogate values are illegal in UTF-32 */ - if (ch >= UNI_SUR_HIGH_START && ch <= UNI_SUR_LOW_END) - { - if (flags == strictConversion) - { - source -= (extraBytesToRead + 1); /* return to the illegal value itself */ - result = sourceIllegal; - break; - } - else - { - if (!computeLength) - *target++ = UNI_REPLACEMENT_CHAR; - else - target++; - } - } - else - { - if (!computeLength) - *target++ = (WCHAR)ch; /* normal case */ - else - target++; - } - } - else if (ch > UNI_MAX_UTF16) - { - if (flags == strictConversion) - { - result = sourceIllegal; - source -= (extraBytesToRead + 1); /* return to the start */ - break; /* Bail out; shouldn't continue */ - } - else - { - if (!computeLength) - *target++ = UNI_REPLACEMENT_CHAR; - else - target++; - } - } - else - { - /* target is a character in range 0xFFFF - 0x10FFFF. */ - if ((target + 1 >= targetEnd) && (!computeLength)) - { - source -= (extraBytesToRead + 1); /* Back up source pointer! */ - result = targetExhausted; - break; - } - - ch -= halfBase; - - if (!computeLength) - { - *target++ = (WCHAR)((ch >> halfShift) + UNI_SUR_HIGH_START); - *target++ = (WCHAR)((ch & halfMask) + UNI_SUR_LOW_START); - } - else - { - target++; - target++; - } - } - } - - *sourceStart = source; - *targetStart = target; - return result; -} - -static int MultiByteToWideChar(UINT CodePage, DWORD dwFlags, LPCSTR lpMultiByteStr, int cbMultiByte, LPWSTR lpWideCharStr, int cchWideChar) -{ - int length; - LPWSTR targetStart; - const BYTE* sourceStart; - ConversionResult result; - - /* If cbMultiByte is 0, the function fails */ - - if (cbMultiByte == 0) - return 0; - - /* If cbMultiByte is -1, the string is null-terminated */ - - if (cbMultiByte == -1) - cbMultiByte = (int)strlen((char*)lpMultiByteStr) + 1; - - /* - * if cchWideChar is 0, the function returns the required buffer size - * in characters for lpWideCharStr and makes no use of the output parameter itself. - */ - - if (cchWideChar == 0) - { - sourceStart = (const BYTE*)lpMultiByteStr; - targetStart = (WCHAR*)NULL; - - result = _ConvertUTF8toUTF16(&sourceStart, &sourceStart[cbMultiByte], - &targetStart, NULL, strictConversion); - - length = (int)(targetStart - ((WCHAR*)NULL)); - cchWideChar = length; - } - else - { - sourceStart = (const BYTE*)lpMultiByteStr; - targetStart = lpWideCharStr; - - result = _ConvertUTF8toUTF16(&sourceStart, &sourceStart[cbMultiByte], - &targetStart, &targetStart[cchWideChar], strictConversion); - - length = (int)(targetStart - ((WCHAR*)lpWideCharStr)); - cchWideChar = length; - } - - return cchWideChar; -} - -#endif - - - -bool ex_utf8_to_utf16le(const std::string& from, ex_str_utf16le& to) -{ - int iSize = MultiByteToWideChar(CP_UTF8, 0, from.c_str(), -1, NULL, 0); - if (iSize <= 0) - return false; - - //++iSize; - to.resize(iSize); - memset(&to[0], 0, sizeof(ex_utf16)); - - MultiByteToWideChar(CP_UTF8, 0, from.c_str(), -1, &to[0], iSize); - - return true; -} - -#endif +#include +#include +#include + +char* ex_strcpy(char* target, size_t size, const char* source) +{ + if (target == source) + return target; + +#ifdef EX_OS_WIN32 + if (SUCCEEDED(StringCchCopyA(target, size, source))) + return target; + else + return NULL; +#else + size_t len = strlen(source); + if (size > len) + { + return strcpy(target, source); + } + else + { + memmove(target, source, size - 1); + return NULL; + } +#endif +} + +wchar_t* ex_wcscpy(wchar_t* target, size_t size, const wchar_t* source) +{ + if (target == source) + return target; + +#ifdef EX_OS_WIN32 + if (SUCCEEDED(StringCchCopyW(target, size, source))) + return target; + else + return NULL; +#else + size_t len = wcslen(source); + if (size > len) + { + return wcscpy(target, source); + } + else + { + memmove(target, source, (size - 1)*sizeof(wchar_t)); + return NULL; + } +#endif +} + +char* ex_strdup(const char* src) +{ + if (NULL == src) + return NULL; + size_t len = strlen(src) + 1; + char* ret = (char*)calloc(1, len); + memcpy(ret, src, len); + return ret; +} + +wchar_t* ex_wcsdup(const wchar_t* src) +{ + if (NULL == src) + return NULL; + size_t len = wcslen(src) + 1; + wchar_t* ret = (wchar_t*)calloc(sizeof(wchar_t), len); + memcpy(ret, src, sizeof(wchar_t)*len); + return ret; +} + +wchar_t* ex_str2wcs_alloc(const char* in_buffer, int code_page) +{ + wchar_t* out_buffer = NULL; +#ifdef EX_OS_WIN32 + int wlen = 0; + UINT _cp = 0; + if (code_page == EX_CODEPAGE_ACP) + _cp = CP_ACP; + else if (code_page == EX_CODEPAGE_UTF8) + _cp = CP_UTF8; + + wlen = MultiByteToWideChar(_cp, 0, in_buffer, -1, NULL, 0); + if (0 == wlen) + return NULL; + + out_buffer = (wchar_t*)calloc(wlen + 1, sizeof(wchar_t)); + if (NULL == out_buffer) + return NULL; + + wlen = MultiByteToWideChar(_cp, 0, in_buffer, -1, out_buffer, wlen); + if (0 == wlen) + { + free(out_buffer); + return NULL; + } + +#else + size_t wlen = 0; + wlen = mbstowcs(NULL, in_buffer, 0); + if (wlen <= 0) + return NULL; + + out_buffer = (wchar_t*)calloc(wlen + 1, sizeof(wchar_t)); + if (NULL == out_buffer) + return NULL; + + wlen = mbstowcs(out_buffer, in_buffer, wlen); + if (wlen <= 0) + { + free(out_buffer); + return NULL; + } + +#endif + + return out_buffer; +} + + +char* ex_wcs2str_alloc(const wchar_t* in_buffer, int code_page) +{ + char* out_buffer = NULL; + + if(NULL == in_buffer) + return NULL; + +#ifdef EX_OS_WIN32 + int len = 0; + UINT _cp = 0; + if (code_page == EX_CODEPAGE_ACP) + _cp = CP_ACP; + else if (code_page == EX_CODEPAGE_UTF8) + _cp = CP_UTF8; + + len = WideCharToMultiByte(_cp, 0, in_buffer, -1, NULL, 0, NULL, NULL); + if (0 == len) + return NULL; + + out_buffer = (char*)calloc(len + 1, sizeof(char)); + if (NULL == out_buffer) + return NULL; + + len = WideCharToMultiByte(_cp, 0, in_buffer, -1, out_buffer, len, NULL, NULL); + if (0 == len) + { + free(out_buffer); + return NULL; + } + +#else + size_t len = 0; + len = wcstombs(NULL, in_buffer, 0); + if (len <= 0) + return NULL; + + out_buffer = (char*)calloc(len + 1, sizeof(char)); + if (NULL == out_buffer) + return NULL; + + len = wcstombs(out_buffer, in_buffer, len); + if (len <= 0) + { + free(out_buffer); + return NULL; + } + +#endif + + return out_buffer; +} + +wchar_t** ex_make_wargv(int argc, char** argv) +{ + int i = 0; + wchar_t** ret = NULL; + + ret = (wchar_t**)calloc(argc + 1, sizeof(wchar_t*)); + if (!ret) + { + return NULL; + } + + for (i = 0; i < argc; ++i) + { + ret[i] = ex_str2wcs_alloc(argv[i], EX_CODEPAGE_DEFAULT); + if (NULL == ret[i]) + goto err; + } + + return ret; + +err: + ex_free_wargv(argc, ret); + return NULL; +} + +void ex_free_wargv(int argc, wchar_t** argv) +{ + int i = 0; + for (i = 0; i < argc; ++i) + free(argv[i]); + + free(argv); +} + +EX_BOOL ex_str_only_white_space(const wchar_t* src) +{ + if (ex_only_white_space(src)) + return EX_TRUE; + else + return EX_FALSE; +} + +EX_BOOL ex_wcs_only_white_space(const char* src) +{ + if (ex_only_white_space(src)) + return EX_TRUE; + else + return EX_FALSE; +} + +int ex_strformat(char* out_buf, size_t buf_size, const char* fmt, ...) +{ + int ret = 0; + va_list valist; + va_start(valist, fmt); + //_ts_printf_a(level, EX_COLOR_BLACK, fmt, valist); +#ifdef EX_OS_WIN32 + ret = vsnprintf(out_buf, buf_size, fmt, valist); +#else + ret = vsprintf(out_buf, fmt, valist); +#endif + va_end(valist); + return ret; +} + +int ex_wcsformat(wchar_t* out_buf, size_t buf_size, const wchar_t* fmt, ...) +{ + int ret = 0; + va_list valist; + va_start(valist, fmt); + //_ts_printf_a(level, EX_COLOR_BLACK, fmt, valist); +#ifdef EX_OS_WIN32 + //ret = vsnprintf(out_buf, buf_size, fmt, valist); + ret = _vsnwprintf_s(out_buf, buf_size, buf_size, fmt, valist); +#else + //ret = vsprintf(out_buf, fmt, valist); + ret = vswprintf(out_buf, buf_size, fmt, valist); +#endif + va_end(valist); + return ret; +} + + +#ifdef __cplusplus +bool ex_wstr2astr(const ex_wstr& in_str, ex_astr& out_str, int code_page/* = EX_CODEPAGE_DEFAULT*/) +{ + return ex_wstr2astr(in_str.c_str(), out_str, code_page); +} + +bool ex_wstr2astr(const wchar_t* in_str, ex_astr& out_str, int code_page/* = EX_CODEPAGE_DEFAULT*/) +{ + char* astr = ex_wcs2str_alloc(in_str, code_page); + if (NULL == astr) + return false; + + out_str = astr; + ex_free(astr); + return true; +} + +bool ex_astr2wstr(const ex_astr& in_str, ex_wstr& out_str, int code_page/* = EX_CODEPAGE_DEFAULT*/) +{ + return ex_astr2wstr(in_str.c_str(), out_str, code_page); +} + +bool ex_astr2wstr(const char* in_str, ex_wstr& out_str, int code_page/* = EX_CODEPAGE_DEFAULT*/) +{ + wchar_t* wstr = ex_str2wcs_alloc(in_str, code_page); + if (NULL == wstr) + return false; + + out_str = wstr; + ex_free(wstr); + return true; +} + +bool ex_only_white_space(const ex_astr& str_check) +{ + ex_astr::size_type pos = 0; + ex_astr strFilter(" \t\r\n"); + pos = str_check.find_first_not_of(strFilter); + if (ex_astr::npos == pos) + return true; + else + return false; +} + +bool ex_only_white_space(const ex_wstr& str_check) +{ + ex_wstr::size_type pos = 0; + ex_wstr strFilter(L" \t\r\n"); + pos = str_check.find_first_not_of(strFilter); + if (ex_wstr::npos == pos) + return true; + else + return false; +} + +void ex_remove_white_space(ex_astr& str_fix, int ulFlag /*= EX_RSC_ALL*/) +{ + ex_astr::size_type pos = 0; + ex_astr strFilter(" \t\r\n"); + + if (ulFlag & EX_RSC_BEGIN) + { + pos = str_fix.find_first_not_of(strFilter); + if (ex_astr::npos != pos) + str_fix.erase(0, pos); + // FIXME + } + if (ulFlag & EX_RSC_END) + { + pos = str_fix.find_last_not_of(strFilter); + if (ex_astr::npos != pos) + str_fix.erase(pos + 1); + // FIXME + } +} + +void ex_remove_white_space(ex_wstr& str_fix, int ulFlag /*= EX_RSC_ALL*/) +{ + ex_wstr::size_type pos = 0; + ex_wstr strFilter(L" \t\r\n"); + + if (ulFlag & EX_RSC_BEGIN) + { + pos = str_fix.find_first_not_of(strFilter); + if (ex_wstr::npos != pos) + str_fix.erase(0, pos); + // FIXME + } + if (ulFlag & EX_RSC_END) + { + pos = str_fix.find_last_not_of(strFilter); + if (ex_wstr::npos != pos) + str_fix.erase(pos + 1); + // FIXME + } +} + +ex_astr& ex_replace_all(ex_astr& str, const ex_astr& old_value, const ex_astr& new_value) +{ + for (ex_astr::size_type pos(0); pos != ex_astr::npos; pos += new_value.length()) + { + if ((pos = str.find(old_value, pos)) != ex_astr::npos) + str.replace(pos, old_value.length(), new_value); + else + break; + } + + return str; +} + +ex_wstr& ex_replace_all(ex_wstr& str, const ex_wstr& old_value, const ex_wstr& new_value) +{ + for (ex_wstr::size_type pos(0); pos != ex_wstr::npos; pos += new_value.length()) + { + if ((pos = str.find(old_value, pos)) != ex_wstr::npos) + str.replace(pos, old_value.length(), new_value); + else + break; + } + + return str; +} + + + +#ifndef EX_OS_WIN32 + +#define BYTE ex_u8 +#define DWORD ex_u32 +#define WCHAR ex_i16 +#define LPWSTR WCHAR* +#define BOOL int +#define TRUE 1 +#define FALSE 0 +#define UINT unsigned int +#define LPCSTR const char* +#define CP_UTF8 1 + +typedef enum +{ + conversionOK, /* conversion successful */ + sourceExhausted, /* partial character in source, but hit end */ + targetExhausted, /* insuff. room in target for conversion */ + sourceIllegal /* source sequence is illegal/malformed */ +} ConversionResult; + +typedef enum +{ + strictConversion = 0, + lenientConversion +} ConversionFlags; + +static const char trailingBytesForUTF8[256] = +{ + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, 3,3,3,3,3,3,3,3,4,4,4,4,5,5,5,5 +}; + +static const DWORD offsetsFromUTF8[6] = { 0x00000000UL, 0x00003080UL, 0x000E2080UL, 0x03C82080UL, 0xFA082080UL, 0x82082080UL +}; + +static const BYTE firstByteMark[7] = { 0x00, 0x00, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC }; + +static const int halfShift = 10; /* used for shifting by 10 bits */ + +static const DWORD halfBase = 0x0010000UL; +static const DWORD halfMask = 0x3FFUL; + +#define UNI_SUR_HIGH_START (DWORD)0xD800 +#define UNI_SUR_HIGH_END (DWORD)0xDBFF +#define UNI_SUR_LOW_START (DWORD)0xDC00 +#define UNI_SUR_LOW_END (DWORD)0xDFFF + +#define UNI_REPLACEMENT_CHAR (DWORD)0x0000FFFD +#define UNI_MAX_BMP (DWORD)0x0000FFFF +#define UNI_MAX_UTF16 (DWORD)0x0010FFFF +#define UNI_MAX_UTF32 (DWORD)0x7FFFFFFF +#define UNI_MAX_LEGAL_UTF32 (DWORD)0x0010FFFF + + +static ConversionResult ConvertUTF16toUTF8(const WCHAR** sourceStart, const WCHAR* sourceEnd, BYTE** targetStart, BYTE* targetEnd, ConversionFlags flags) +{ + BYTE* target; + const WCHAR* source; + BOOL computeLength; + ConversionResult result; + computeLength = (!targetEnd) ? TRUE : FALSE; + source = *sourceStart; + target = *targetStart; + result = conversionOK; + + while (source < sourceEnd) + { + DWORD ch; + unsigned short bytesToWrite = 0; + const DWORD byteMask = 0xBF; + const DWORD byteMark = 0x80; + const WCHAR* oldSource = source; /* In case we have to back up because of target overflow. */ + ch = *source++; + + /* If we have a surrogate pair, convert to UTF32 first. */ + if (ch >= UNI_SUR_HIGH_START && ch <= UNI_SUR_HIGH_END) + { + /* If the 16 bits following the high surrogate are in the source buffer... */ + if (source < sourceEnd) + { + DWORD ch2 = *source; + + /* If it's a low surrogate, convert to UTF32. */ + if (ch2 >= UNI_SUR_LOW_START && ch2 <= UNI_SUR_LOW_END) + { + ch = ((ch - UNI_SUR_HIGH_START) << halfShift) + + (ch2 - UNI_SUR_LOW_START) + halfBase; + ++source; + } + else if (flags == strictConversion) + { + /* it's an unpaired high surrogate */ + --source; /* return to the illegal value itself */ + result = sourceIllegal; + break; + } + } + else + { + /* We don't have the 16 bits following the high surrogate. */ + --source; /* return to the high surrogate */ + result = sourceExhausted; + break; + } + } + else if (flags == strictConversion) + { + /* UTF-16 surrogate values are illegal in UTF-32 */ + if (ch >= UNI_SUR_LOW_START && ch <= UNI_SUR_LOW_END) + { + --source; /* return to the illegal value itself */ + result = sourceIllegal; + break; + } + } + + /* Figure out how many bytes the result will require */ + if (ch < (DWORD)0x80) + { + bytesToWrite = 1; + } + else if (ch < (DWORD)0x800) + { + bytesToWrite = 2; + } + else if (ch < (DWORD)0x10000) + { + bytesToWrite = 3; + } + else if (ch < (DWORD)0x110000) + { + bytesToWrite = 4; + } + else + { + bytesToWrite = 3; + ch = UNI_REPLACEMENT_CHAR; + } + + target += bytesToWrite; + + if ((target > targetEnd) && (!computeLength)) + { + source = oldSource; /* Back up source pointer! */ + target -= bytesToWrite; + result = targetExhausted; + break; + } + + if (!computeLength) + { + switch (bytesToWrite) + { + /* note: everything falls through. */ + case 4: + *--target = (BYTE)((ch | byteMark) & byteMask); + ch >>= 6; + + case 3: + *--target = (BYTE)((ch | byteMark) & byteMask); + ch >>= 6; + + case 2: + *--target = (BYTE)((ch | byteMark) & byteMask); + ch >>= 6; + + case 1: + *--target = (BYTE)(ch | firstByteMark[bytesToWrite]); + } + } + else + { + switch (bytesToWrite) + { + /* note: everything falls through. */ + case 4: + --target; + ch >>= 6; + + case 3: + --target; + ch >>= 6; + + case 2: + --target; + ch >>= 6; + + case 1: + --target; + } + } + + target += bytesToWrite; + } + + *sourceStart = source; + *targetStart = target; + return result; +} + + +static BOOL isLegalUTF8(const BYTE* source, int length) +{ + BYTE a; + const BYTE* srcptr = source + length; + + switch (length) + { + default: + return FALSE; + + /* Everything else falls through when "TRUE"... */ + case 4: + if ((a = (*--srcptr)) < 0x80 || a > 0xBF) return FALSE; + + case 3: + if ((a = (*--srcptr)) < 0x80 || a > 0xBF) return FALSE; + + case 2: + if ((a = (*--srcptr)) > 0xBF) return FALSE; + + switch (*source) + { + /* no fall-through in this inner switch */ + case 0xE0: + if (a < 0xA0) return FALSE; + + break; + + case 0xED: + if (a > 0x9F) return FALSE; + + break; + + case 0xF0: + if (a < 0x90) return FALSE; + + break; + + case 0xF4: + if (a > 0x8F) return FALSE; + + break; + + default: + if (a < 0x80) return FALSE; + } + + case 1: + if (*source >= 0x80 && *source < 0xC2) return FALSE; + } + + if (*source > 0xF4) + return FALSE; + + return TRUE; +} + +static ConversionResult _ConvertUTF8toUTF16(const BYTE** sourceStart, const BYTE* sourceEnd, WCHAR** targetStart, WCHAR* targetEnd, ConversionFlags flags) +{ + WCHAR* target; + const BYTE* source; + BOOL computeLength; + ConversionResult result; + computeLength = (!targetEnd) ? TRUE : FALSE; + result = conversionOK; + source = *sourceStart; + target = *targetStart; + + while (source < sourceEnd) + { + DWORD ch = 0; + unsigned short extraBytesToRead = trailingBytesForUTF8[*source]; + + if ((source + extraBytesToRead) >= sourceEnd) + { + result = sourceExhausted; + break; + } + + /* Do this check whether lenient or strict */ + if (!isLegalUTF8(source, extraBytesToRead + 1)) + { + result = sourceIllegal; + break; + } + + /* + * The cases all fall through. See "Note A" below. + */ + switch (extraBytesToRead) + { + case 5: + ch += *source++; + ch <<= 6; /* remember, illegal UTF-8 */ + + case 4: + ch += *source++; + ch <<= 6; /* remember, illegal UTF-8 */ + + case 3: + ch += *source++; + ch <<= 6; + + case 2: + ch += *source++; + ch <<= 6; + + case 1: + ch += *source++; + ch <<= 6; + + case 0: + ch += *source++; + } + + ch -= offsetsFromUTF8[extraBytesToRead]; + + if ((target >= targetEnd) && (!computeLength)) + { + source -= (extraBytesToRead + 1); /* Back up source pointer! */ + result = targetExhausted; + break; + } + + if (ch <= UNI_MAX_BMP) + { + /* Target is a character <= 0xFFFF */ + /* UTF-16 surrogate values are illegal in UTF-32 */ + if (ch >= UNI_SUR_HIGH_START && ch <= UNI_SUR_LOW_END) + { + if (flags == strictConversion) + { + source -= (extraBytesToRead + 1); /* return to the illegal value itself */ + result = sourceIllegal; + break; + } + else + { + if (!computeLength) + *target++ = UNI_REPLACEMENT_CHAR; + else + target++; + } + } + else + { + if (!computeLength) + *target++ = (WCHAR)ch; /* normal case */ + else + target++; + } + } + else if (ch > UNI_MAX_UTF16) + { + if (flags == strictConversion) + { + result = sourceIllegal; + source -= (extraBytesToRead + 1); /* return to the start */ + break; /* Bail out; shouldn't continue */ + } + else + { + if (!computeLength) + *target++ = UNI_REPLACEMENT_CHAR; + else + target++; + } + } + else + { + /* target is a character in range 0xFFFF - 0x10FFFF. */ + if ((target + 1 >= targetEnd) && (!computeLength)) + { + source -= (extraBytesToRead + 1); /* Back up source pointer! */ + result = targetExhausted; + break; + } + + ch -= halfBase; + + if (!computeLength) + { + *target++ = (WCHAR)((ch >> halfShift) + UNI_SUR_HIGH_START); + *target++ = (WCHAR)((ch & halfMask) + UNI_SUR_LOW_START); + } + else + { + target++; + target++; + } + } + } + + *sourceStart = source; + *targetStart = target; + return result; +} + +static int MultiByteToWideChar(UINT CodePage, DWORD dwFlags, LPCSTR lpMultiByteStr, int cbMultiByte, LPWSTR lpWideCharStr, int cchWideChar) +{ + int length; + LPWSTR targetStart; + const BYTE* sourceStart; + ConversionResult result; + + /* If cbMultiByte is 0, the function fails */ + + if (cbMultiByte == 0) + return 0; + + /* If cbMultiByte is -1, the string is null-terminated */ + + if (cbMultiByte == -1) + cbMultiByte = (int)strlen((char*)lpMultiByteStr) + 1; + + /* + * if cchWideChar is 0, the function returns the required buffer size + * in characters for lpWideCharStr and makes no use of the output parameter itself. + */ + + if (cchWideChar == 0) + { + sourceStart = (const BYTE*)lpMultiByteStr; + targetStart = (WCHAR*)NULL; + + result = _ConvertUTF8toUTF16(&sourceStart, &sourceStart[cbMultiByte], + &targetStart, NULL, strictConversion); + + length = (int)(targetStart - ((WCHAR*)NULL)); + cchWideChar = length; + } + else + { + sourceStart = (const BYTE*)lpMultiByteStr; + targetStart = lpWideCharStr; + + result = _ConvertUTF8toUTF16(&sourceStart, &sourceStart[cbMultiByte], + &targetStart, &targetStart[cchWideChar], strictConversion); + + length = (int)(targetStart - ((WCHAR*)lpWideCharStr)); + cchWideChar = length; + } + + return cchWideChar; +} + +#endif + + + +bool ex_utf8_to_utf16le(const std::string& from, ex_str_utf16le& to) +{ + int iSize = MultiByteToWideChar(CP_UTF8, 0, from.c_str(), -1, NULL, 0); + if (iSize <= 0) + return false; + + //++iSize; + to.resize(iSize); + memset(&to[0], 0, sizeof(ex_utf16)); + + MultiByteToWideChar(CP_UTF8, 0, from.c_str(), -1, &to[0], iSize); + + return true; +} + +#endif diff --git a/common/libex/src/ex_thread.cpp b/common/libex/src/ex_thread.cpp index acdcdf8..9358a77 100644 --- a/common/libex/src/ex_thread.cpp +++ b/common/libex/src/ex_thread.cpp @@ -1,224 +1,224 @@ -#include -#include - -//========================================================= -// -//========================================================= - - -#ifdef EX_OS_WIN32 -unsigned int WINAPI ExThreadBase::_thread_func(LPVOID pParam) -#else - -void *ExThreadBase::_thread_func(void *pParam) -#endif -{ - ExThreadBase *_this = (ExThreadBase *) pParam; - - _this->m_is_running = true; - _this->_thread_loop(); - _this->m_is_running = false; - _this->m_handle = 0; - - EXLOGV(" # thread [%s] exit.\n", _this->m_thread_name.c_str()); - _this->_on_stopped(); - return 0; -} - -ExThreadBase::ExThreadBase(const char *thread_name) : - m_handle(0), - m_is_running(false), - m_need_stop(false) { - m_thread_name = thread_name; -} - -ExThreadBase::~ExThreadBase() { - if(m_is_running) { - EXLOGE(" # thread [%s] not stop before destroy.\n", m_thread_name.c_str()); - } -} - -bool ExThreadBase::start(void) { - m_need_stop = false; - EXLOGV(" . thread [%s] starting.\n", m_thread_name.c_str()); -#ifdef WIN32 - HANDLE h = (HANDLE)_beginthreadex(NULL, 0, _thread_func, (void*)this, 0, NULL); - - if (NULL == h) - { - return false; - } - m_handle = h; -#else - pthread_t ptid = 0; - int ret = pthread_create(&ptid, NULL, _thread_func, (void *) this); - if (ret != 0) { - return false; - } - m_handle = ptid; - -#endif - - return true; -} - -bool ExThreadBase::stop(void) { - if (m_handle == 0) { - EXLOGW("[thread] thread [%s] already stopped.\n", m_thread_name.c_str()); - return true; - } - - EXLOGV("[thread] try to stop thread [%s].\n", m_thread_name.c_str()); - m_need_stop = true; - _on_stop(); - - EXLOGV("[thread] wait thread [%s] exit.\n", m_thread_name.c_str()); - -#ifdef EX_OS_WIN32 - if (WaitForSingleObject(m_handle, INFINITE) != WAIT_OBJECT_0) - { - return false; - } -#else - if (pthread_join(m_handle, NULL) != 0) { - return false; - } -#endif - - return true; -} - -bool ExThreadBase::terminate(void) { -#ifdef EX_OS_WIN32 - return (TerminateThread(m_handle, 1) == TRUE); -#else - return (pthread_cancel(m_handle) == 0); -#endif -} - -//========================================================= -// -//========================================================= - -ExThreadManager::ExThreadManager() {} - -ExThreadManager::~ExThreadManager() { - if (!m_threads.empty()) { - EXLOGE("when destroy thread manager, there are %d thread not exit.\n", m_threads.size()); - stop_all(); - } -} - -void ExThreadManager::stop_all(void) { - ExThreadSmartLock locker(m_lock); - - ex_threads::iterator it = m_threads.begin(); - for (; it != m_threads.end(); ++it) { - (*it)->stop(); - } - m_threads.clear(); -} - -void ExThreadManager::add(ExThreadBase *tb) { - ExThreadSmartLock locker(m_lock); - - ex_threads::iterator it = m_threads.begin(); - for (; it != m_threads.end(); ++it) { - if ((*it) == tb) { - EXLOGE("when add thread to manager, it already exist.\n"); - return; - } - } - - m_threads.push_back(tb); -} - -void ExThreadManager::remove(ExThreadBase *tb) { - ExThreadSmartLock locker(m_lock); - - ex_threads::iterator it = m_threads.begin(); - for (; it != m_threads.end(); ++it) { - if ((*it) == tb) { - m_threads.erase(it); - return; - } - } - EXLOGE("thread not hold by thread-manager while remove it.\n"); -} - -//========================================================= -// -//========================================================= - -ExThreadLock::ExThreadLock() { -#ifdef EX_OS_WIN32 - InitializeCriticalSection(&m_locker); -#else - pthread_mutexattr_t attr; - pthread_mutexattr_init(&attr); - pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); - pthread_mutex_init(&m_locker, &attr); - pthread_mutexattr_destroy(&attr); -#endif -} - -ExThreadLock::~ExThreadLock() { -#ifdef EX_OS_WIN32 - DeleteCriticalSection(&m_locker); -#else - pthread_mutex_destroy(&m_locker); -#endif -} - -void ExThreadLock::lock(void) { -#ifdef EX_OS_WIN32 - EnterCriticalSection(&m_locker); -#else - pthread_mutex_lock(&m_locker); -#endif -} - -void ExThreadLock::unlock(void) { -#ifdef EX_OS_WIN32 - LeaveCriticalSection(&m_locker); -#else - pthread_mutex_unlock(&m_locker); -#endif -} - -//========================================================= -// -//========================================================= - -int ex_atomic_add(volatile int *pt, int t) { -#ifdef EX_OS_WIN32 - return (int)InterlockedExchangeAdd((long*)pt, (long)t); -#else - return __sync_add_and_fetch(pt, t); -#endif -} - -int ex_atomic_inc(volatile int *pt) { -#ifdef EX_OS_WIN32 - return (int)InterlockedIncrement((long*)pt); -#else - return __sync_add_and_fetch(pt, 1); -#endif -} - -int ex_atomic_dec(volatile int *pt) { -#ifdef EX_OS_WIN32 - return (int)InterlockedDecrement((long*)pt); -#else - return __sync_add_and_fetch(pt, -1); -#endif -} - - -ex_u64 ex_get_thread_id(void) { -#ifdef EX_OS_WIN32 - return GetCurrentThreadId(); -#else - return (ex_u64) pthread_self(); -#endif -} +#include +#include + +//========================================================= +// +//========================================================= + + +#ifdef EX_OS_WIN32 +unsigned int WINAPI ExThreadBase::_thread_func(LPVOID pParam) +#else + +void *ExThreadBase::_thread_func(void *pParam) +#endif +{ + ExThreadBase *_this = (ExThreadBase *) pParam; + + _this->m_is_running = true; + _this->_thread_loop(); + _this->m_is_running = false; + _this->m_handle = 0; + + EXLOGV(" # thread [%s] exit.\n", _this->m_thread_name.c_str()); + _this->_on_stopped(); + return 0; +} + +ExThreadBase::ExThreadBase(const char *thread_name) : + m_handle(0), + m_is_running(false), + m_need_stop(false) { + m_thread_name = thread_name; +} + +ExThreadBase::~ExThreadBase() { + if(m_is_running) { + EXLOGE(" # thread [%s] not stop before destroy.\n", m_thread_name.c_str()); + } +} + +bool ExThreadBase::start(void) { + m_need_stop = false; + EXLOGV(" . thread [%s] starting.\n", m_thread_name.c_str()); +#ifdef WIN32 + HANDLE h = (HANDLE)_beginthreadex(NULL, 0, _thread_func, (void*)this, 0, NULL); + + if (NULL == h) + { + return false; + } + m_handle = h; +#else + pthread_t ptid = 0; + int ret = pthread_create(&ptid, NULL, _thread_func, (void *) this); + if (ret != 0) { + return false; + } + m_handle = ptid; + +#endif + + return true; +} + +bool ExThreadBase::stop(void) { + if (m_handle == 0) { + EXLOGW("[thread] thread [%s] already stopped.\n", m_thread_name.c_str()); + return true; + } + + EXLOGV("[thread] try to stop thread [%s].\n", m_thread_name.c_str()); + m_need_stop = true; + _on_stop(); + + EXLOGV("[thread] wait thread [%s] exit.\n", m_thread_name.c_str()); + +#ifdef EX_OS_WIN32 + if (WaitForSingleObject(m_handle, INFINITE) != WAIT_OBJECT_0) + { + return false; + } +#else + if (pthread_join(m_handle, NULL) != 0) { + return false; + } +#endif + + return true; +} + +bool ExThreadBase::terminate(void) { +#ifdef EX_OS_WIN32 + return (TerminateThread(m_handle, 1) == TRUE); +#else + return (pthread_cancel(m_handle) == 0); +#endif +} + +//========================================================= +// +//========================================================= + +ExThreadManager::ExThreadManager() {} + +ExThreadManager::~ExThreadManager() { + if (!m_threads.empty()) { + EXLOGE("when destroy thread manager, there are %d thread not exit.\n", m_threads.size()); + stop_all(); + } +} + +void ExThreadManager::stop_all(void) { + ExThreadSmartLock locker(m_lock); + + ex_threads::iterator it = m_threads.begin(); + for (; it != m_threads.end(); ++it) { + (*it)->stop(); + } + m_threads.clear(); +} + +void ExThreadManager::add(ExThreadBase *tb) { + ExThreadSmartLock locker(m_lock); + + ex_threads::iterator it = m_threads.begin(); + for (; it != m_threads.end(); ++it) { + if ((*it) == tb) { + EXLOGE("when add thread to manager, it already exist.\n"); + return; + } + } + + m_threads.push_back(tb); +} + +void ExThreadManager::remove(ExThreadBase *tb) { + ExThreadSmartLock locker(m_lock); + + ex_threads::iterator it = m_threads.begin(); + for (; it != m_threads.end(); ++it) { + if ((*it) == tb) { + m_threads.erase(it); + return; + } + } + EXLOGE("thread not hold by thread-manager while remove it.\n"); +} + +//========================================================= +// +//========================================================= + +ExThreadLock::ExThreadLock() { +#ifdef EX_OS_WIN32 + InitializeCriticalSection(&m_locker); +#else + pthread_mutexattr_t attr; + pthread_mutexattr_init(&attr); + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); + pthread_mutex_init(&m_locker, &attr); + pthread_mutexattr_destroy(&attr); +#endif +} + +ExThreadLock::~ExThreadLock() { +#ifdef EX_OS_WIN32 + DeleteCriticalSection(&m_locker); +#else + pthread_mutex_destroy(&m_locker); +#endif +} + +void ExThreadLock::lock(void) { +#ifdef EX_OS_WIN32 + EnterCriticalSection(&m_locker); +#else + pthread_mutex_lock(&m_locker); +#endif +} + +void ExThreadLock::unlock(void) { +#ifdef EX_OS_WIN32 + LeaveCriticalSection(&m_locker); +#else + pthread_mutex_unlock(&m_locker); +#endif +} + +//========================================================= +// +//========================================================= + +int ex_atomic_add(volatile int *pt, int t) { +#ifdef EX_OS_WIN32 + return (int)InterlockedExchangeAdd((long*)pt, (long)t); +#else + return __sync_add_and_fetch(pt, t); +#endif +} + +int ex_atomic_inc(volatile int *pt) { +#ifdef EX_OS_WIN32 + return (int)InterlockedIncrement((long*)pt); +#else + return __sync_add_and_fetch(pt, 1); +#endif +} + +int ex_atomic_dec(volatile int *pt) { +#ifdef EX_OS_WIN32 + return (int)InterlockedDecrement((long*)pt); +#else + return __sync_add_and_fetch(pt, -1); +#endif +} + + +ex_u64 ex_get_thread_id(void) { +#ifdef EX_OS_WIN32 + return GetCurrentThreadId(); +#else + return (ex_u64) pthread_self(); +#endif +} diff --git a/common/libex/src/ex_util.cpp b/common/libex/src/ex_util.cpp index a84a001..c4daeba 100644 --- a/common/libex/src/ex_util.cpp +++ b/common/libex/src/ex_util.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include #include diff --git a/common/libex/src/ex_winsrv.cpp b/common/libex/src/ex_winsrv.cpp index 4c909f4..0b5087c 100644 --- a/common/libex/src/ex_winsrv.cpp +++ b/common/libex/src/ex_winsrv.cpp @@ -1,4 +1,4 @@ -#include +#include #ifdef EX_OS_WIN32 @@ -44,14 +44,14 @@ ex_rv ex_winsrv_install(const ex_wstr& srv_name, const ex_wstr& disp_name, const } SERVICE_FAILURE_ACTIONS failure_action; - failure_action.dwResetPeriod = 0; // reset failure count to zero ʱ䣬λΪ + failure_action.dwResetPeriod = 0; // reset failure count to zero 的时间,单位为秒 failure_action.lpRebootMsg = NULL; // Message to broadcast to server users before rebooting failure_action.lpCommand = NULL; // Command line of the process for the CreateProcess function to execute in response - failure_action.cActions = 3; // actionĸ + failure_action.cActions = 3; // action数组的个数 SC_ACTION actionarray[3]; - actionarray[0].Type = SC_ACTION_RESTART; // - actionarray[0].Delay = 60000; // λΪ + actionarray[0].Type = SC_ACTION_RESTART; // 重新启动服务 + actionarray[0].Delay = 60000; // 单位为毫秒 actionarray[1].Type = SC_ACTION_RESTART; actionarray[1].Delay = 60000; actionarray[2].Type = SC_ACTION_RESTART; diff --git a/common/teleport/teleport_const.h b/common/teleport/teleport_const.h index 8660e18..1f33d72 100644 --- a/common/teleport/teleport_const.h +++ b/common/teleport/teleport_const.h @@ -1,151 +1,151 @@ -#ifndef __TELEPORT_CONST_H__ -#define __TELEPORT_CONST_H__ - -// עͬͬԵconstļ - -// ļ趨teleportģ֮ͨѶʱĴֵJSONݣ -// - WEB -// - WEBWEB̨ -// - WEB̨COREķ - -//======================================================= -// Urlprotocol -//======================================================= -#define TP_URLPROTO_APP_NAME "teleport" - -//======================================================= -// Զ֤ʽ -//======================================================= -#define TP_AUTH_TYPE_NONE 0 -#define TP_AUTH_TYPE_PASSWORD 1 -#define TP_AUTH_TYPE_PRIVATE_KEY 2 - -//======================================================= -// ԶЭ -//======================================================= -#define TP_PROTOCOL_TYPE_RDP 1 -#define TP_PROTOCOL_TYPE_SSH 2 -#define TP_PROTOCOL_TYPE_TELNET 3 - -//======================================================= -// ԶЭ -//======================================================= -#define TP_PROTOCOL_TYPE_RDP_DESKTOP 100 -#define TP_PROTOCOL_TYPE_SSH_SHELL 200 -#define TP_PROTOCOL_TYPE_SSH_SFTP 201 -#define TP_PROTOCOL_TYPE_TELNET_SHELL 300 - - -//======================================================= -// Զϵͳ -//======================================================= -#define TP_OS_TYPE_WINDOWS 1 -#define TP_OS_TYPE_LINUX 2 - -//======================================================= -// ԶӻỰ״̬ -//======================================================= -#define TP_SESS_STAT_RUNNING 0 // Ựʼˣ -#define TP_SESS_STAT_END 9999 // Ựɹ -#define TP_SESS_STAT_ERR_AUTH_DENIED 1 // ỰΪ֤ʧ -#define TP_SESS_STAT_ERR_CONNECT 2 // ỰΪ޷ӵԶ -#define TP_SESS_STAT_ERR_BAD_SSH_KEY 3 // ỰΪ޷ʶSSH˽Կ -#define TP_SESS_STAT_ERR_INTERNAL 4 // ỰΪڲ -#define TP_SESS_STAT_ERR_UNSUPPORT_PROTOCOL 5 // ỰΪЭ鲻֧(RDP) -#define TP_SESS_STAT_ERR_BAD_PKG 6 // ỰΪյı -#define TP_SESS_STAT_ERR_RESET 7 // ỰΪteleportķ -#define TP_SESS_STAT_ERR_IO 8 // ỰΪж -#define TP_SESS_STAT_ERR_SESSION 9 // ỰΪЧĻỰID -#define TP_SESS_STAT_ERR_AUTH_TYPE 10 // ỰΪ֤ʽ -#define TP_SESS_STAT_STARTED 100 // Ѿӳɹˣʼ¼¼ -#define TP_SESS_STAT_ERR_START_INTERNAL 104 // ỰΪڲ -#define TP_SESS_STAT_ERR_START_BAD_PKG 106 // ỰΪյı -#define TP_SESS_STAT_ERR_START_RESET 107 // ỰΪteleportķ -#define TP_SESS_STAT_ERR_START_IO 108 // ỰΪж - - -//======================================================= -// Ȩ -//======================================================= -#define TP_FLAG_ALL 0xFFFFFFFF -// Ự¼ -#define TP_FLAG_RECORD_REPLAY 0x00000001 // ¼ʷ¼طţ -#define TP_FLAG_RECORD_REAL_TIME 0x00000002 // ʵʱ -// RDP -#define TP_FLAG_RDP_DESKTOP 0x00000001 // Զ -#define TP_FLAG_RDP_CLIPBOARD 0x00000002 // -#define TP_FLAG_RDP_DISK 0x00000004 // ӳ -#define TP_FLAG_RDP_APP 0x00000008 // ԶAPPδʵ֣ -#define TP_FLAG_RDP_CONSOLE 0x00001000 //ӵԱỰRDPconsoleѡ -// SSH -#define TP_FLAG_SSH_SHELL 0x00000001 // SHELL -#define TP_FLAG_SSH_SFTP 0x00000002 // SFTP -#define TP_FLAG_SSH_X11 0x00000004 // X11תδʵ֣ -#define TP_FLAG_SSH_EXEC 0x00000008 // execִԶδʵ֣ -#define TP_FLAG_SSH_TUNNEL 0x00000010 // allow ssh tunnel. (not impl.) - - -//======================================================= -// ֵ -//======================================================= -#define TPE_OK 0 // ɹ -//------------------------------------------------------- -// ͨôֵ -//------------------------------------------------------- -#define TPE_NEED_MORE_DATA 1 // ҪݣһǴ -#define TPE_NEED_LOGIN 2 // Ҫ¼ -#define TPE_PRIVILEGE 3 // ûвȨ -#define TPE_NOT_IMPLEMENT 7 // δʵ -#define TPE_EXISTS 8 // ĿѾ -#define TPE_NOT_EXISTS 9 // Ŀ겻 - -// 100~299ͨôֵ - -#define TPE_FAILED 100 // ڲ -#define TPE_NETWORK 101 // -#define TPE_DATABASE 102 // ݿʧ - -// HTTPش -#define TPE_HTTP_METHOD 120 // Ч󷽷GET/POSTȣߴ󷽷ҪPOSTȴʹGETʽ -#define TPE_HTTP_URL_ENCODE 121 // URL޷룩 -//#define TPE_HTTP_URI 122 // ЧURI - -#define TPE_UNKNOWN_CMD 124 // δ֪ -#define TPE_JSON_FORMAT 125 // JSONʽҪJSONʽݣȴ޷JSONʽ룩 -#define TPE_PARAM 126 // -#define TPE_DATA 127 // ݴ - -// #define TPE_OPENFILE_ERROR 0x1007 // ޷ļ -// #define TPE_GETTEMPPATH_ERROR 0x1007 -#define TPE_OPENFILE 300 - - -//------------------------------------------------------- -// WEBרôֵ -//------------------------------------------------------- -#define TPE_CAPTCHA_EXPIRED 10000 // ֤ѹ -#define TPE_CAPTCHA_MISMATCH 10001 // ֤ -#define TPE_OATH_MISMATCH 10002 // ֤̬֤ -#define TPE_SYS_MAINTENANCE 10003 // ϵͳά - -#define TPE_USER_LOCKED 10100 // ûѾδ룩 -#define TPE_USER_DISABLED 10101 // ûѾ -#define TPE_USER_AUTH 10102 // ֤ʧ - -//------------------------------------------------------- -// ֳרôֵ -//------------------------------------------------------- -#define TPE_NO_ASSIST 100000 // δܼ⵽ֳ -#define TPE_OLD_ASSIST 100001 // ֳ汾̫ -#define TPE_START_CLIENT 100002 // ޷ͻ˳޷̣ - - - -//------------------------------------------------------- -// ķרôֵ -//------------------------------------------------------- -#define TPE_NO_CORE_SERVER 200000 // δܼ⵽ķ - - - -#endif // __TELEPORT_CONST_H__ +#ifndef __TELEPORT_CONST_H__ +#define __TELEPORT_CONST_H__ + +// 注意同步更新三个不同语言的const文件 + +// 本文件设定teleport各个模块之间通讯时的错误值(JSON数据),包括: +// - WEB界面与助手 +// - WEB界面与WEB后台 +// - WEB后台与CORE核心服务 + +//======================================================= +// Urlprotocol相关 +//======================================================= +#define TP_URLPROTO_APP_NAME "teleport" + +//======================================================= +// 远程连接认证方式 +//======================================================= +#define TP_AUTH_TYPE_NONE 0 +#define TP_AUTH_TYPE_PASSWORD 1 +#define TP_AUTH_TYPE_PRIVATE_KEY 2 + +//======================================================= +// 远程连接协议 +//======================================================= +#define TP_PROTOCOL_TYPE_RDP 1 +#define TP_PROTOCOL_TYPE_SSH 2 +#define TP_PROTOCOL_TYPE_TELNET 3 + +//======================================================= +// 远程连接子协议 +//======================================================= +#define TP_PROTOCOL_TYPE_RDP_DESKTOP 100 +#define TP_PROTOCOL_TYPE_SSH_SHELL 200 +#define TP_PROTOCOL_TYPE_SSH_SFTP 201 +#define TP_PROTOCOL_TYPE_TELNET_SHELL 300 + + +//======================================================= +// 远程主机操作系统 +//======================================================= +#define TP_OS_TYPE_WINDOWS 1 +#define TP_OS_TYPE_LINUX 2 + +//======================================================= +// 远程连接会话状态 +//======================================================= +#define TP_SESS_STAT_RUNNING 0 // 会话开始了,正在连接 +#define TP_SESS_STAT_END 9999 // 会话成功结束 +#define TP_SESS_STAT_ERR_AUTH_DENIED 1 // 会话结束,因为认证失败 +#define TP_SESS_STAT_ERR_CONNECT 2 // 会话结束,因为无法连接到远程主机 +#define TP_SESS_STAT_ERR_BAD_SSH_KEY 3 // 会话结束,因为无法识别SSH私钥 +#define TP_SESS_STAT_ERR_INTERNAL 4 // 会话结束,因为内部错误 +#define TP_SESS_STAT_ERR_UNSUPPORT_PROTOCOL 5 // 会话结束,因为协议不支持(RDP) +#define TP_SESS_STAT_ERR_BAD_PKG 6 // 会话结束,因为收到错误的报文 +#define TP_SESS_STAT_ERR_RESET 7 // 会话结束,因为teleport核心服务重置了 +#define TP_SESS_STAT_ERR_IO 8 // 会话结束,因为网络中断 +#define TP_SESS_STAT_ERR_SESSION 9 // 会话结束,因为无效的会话ID +#define TP_SESS_STAT_ERR_AUTH_TYPE 10 // 会话结束,因为不被允许的认证方式 +#define TP_SESS_STAT_STARTED 100 // 已经连接成功了,开始记录录像了 +#define TP_SESS_STAT_ERR_START_INTERNAL 104 // 会话结束,因为内部错误 +#define TP_SESS_STAT_ERR_START_BAD_PKG 106 // 会话结束,因为收到错误的报文 +#define TP_SESS_STAT_ERR_START_RESET 107 // 会话结束,因为teleport核心服务重置了 +#define TP_SESS_STAT_ERR_START_IO 108 // 会话结束,因为网络中断 + + +//======================================================= +// 授权标记 +//======================================================= +#define TP_FLAG_ALL 0xFFFFFFFF +// 会话记录相关 +#define TP_FLAG_RECORD_REPLAY 0x00000001 // 允许记录历史(录像回放) +#define TP_FLAG_RECORD_REAL_TIME 0x00000002 // 允许实时监控 +// RDP相关 +#define TP_FLAG_RDP_DESKTOP 0x00000001 // 允许远程桌面 +#define TP_FLAG_RDP_CLIPBOARD 0x00000002 // 允许剪贴板 +#define TP_FLAG_RDP_DISK 0x00000004 // 允许磁盘映射 +#define TP_FLAG_RDP_APP 0x00000008 // 允许远程APP(尚未实现) +#define TP_FLAG_RDP_CONSOLE 0x00001000 //允许连接到管理员会话(RDP的console选项) +// SSH相关 +#define TP_FLAG_SSH_SHELL 0x00000001 // 允许SHELL +#define TP_FLAG_SSH_SFTP 0x00000002 // 允许SFTP +#define TP_FLAG_SSH_X11 0x00000004 // 允许X11转发(尚未实现) +#define TP_FLAG_SSH_EXEC 0x00000008 // 允许exec执行远程命令(尚未实现) +#define TP_FLAG_SSH_TUNNEL 0x00000010 // allow ssh tunnel. (not impl.) + + +//======================================================= +// 错误值 +//======================================================= +#define TPE_OK 0 // 成功 +//------------------------------------------------------- +// 通用错误值 +//------------------------------------------------------- +#define TPE_NEED_MORE_DATA 1 // 需要更多数据(不一定是错误) +#define TPE_NEED_LOGIN 2 // 需要登录 +#define TPE_PRIVILEGE 3 // 没有操作权限 +#define TPE_NOT_IMPLEMENT 7 // 功能尚未实现 +#define TPE_EXISTS 8 // 目标已经存在 +#define TPE_NOT_EXISTS 9 // 目标不存在 + +// 100~299是通用错误值 + +#define TPE_FAILED 100 // 内部错误 +#define TPE_NETWORK 101 // 网络错误 +#define TPE_DATABASE 102 // 数据库操作失败 + +// HTTP请求相关错误 +#define TPE_HTTP_METHOD 120 // 无效的请求方法(不是GET/POST等),或者错误的请求方法(例如需要POST,却使用GET方式请求) +#define TPE_HTTP_URL_ENCODE 121 // URL编码错误(无法解码) +//#define TPE_HTTP_URI 122 // 无效的URI + +#define TPE_UNKNOWN_CMD 124 // 未知的命令 +#define TPE_JSON_FORMAT 125 // 错误的JSON格式(需要JSON格式数据,但是却无法按JSON格式解码) +#define TPE_PARAM 126 // 参数错误 +#define TPE_DATA 127 // 数据错误 + +// #define TPE_OPENFILE_ERROR 0x1007 // 无法打开文件 +// #define TPE_GETTEMPPATH_ERROR 0x1007 +#define TPE_OPENFILE 300 + + +//------------------------------------------------------- +// WEB服务专用错误值 +//------------------------------------------------------- +#define TPE_CAPTCHA_EXPIRED 10000 // 验证码已过期 +#define TPE_CAPTCHA_MISMATCH 10001 // 验证码错误 +#define TPE_OATH_MISMATCH 10002 // 身份验证器动态验证码错误 +#define TPE_SYS_MAINTENANCE 10003 // 系统维护中 + +#define TPE_USER_LOCKED 10100 // 用户已经被锁定(连续多次错误密码) +#define TPE_USER_DISABLED 10101 // 用户已经被禁用 +#define TPE_USER_AUTH 10102 // 身份验证失败 + +//------------------------------------------------------- +// 助手程序专用错误值 +//------------------------------------------------------- +#define TPE_NO_ASSIST 100000 // 未能检测到助手程序 +#define TPE_OLD_ASSIST 100001 // 助手程序版本太低 +#define TPE_START_CLIENT 100002 // 无法启动客户端程序(无法创建进程) + + + +//------------------------------------------------------- +// 核心服务专用错误值 +//------------------------------------------------------- +#define TPE_NO_CORE_SERVER 200000 // 未能检测到核心服务 + + + +#endif // __TELEPORT_CONST_H__ diff --git a/server/tp_core/common/base_env.cpp b/server/tp_core/common/base_env.cpp index 0975cfd..34f8c07 100644 --- a/server/tp_core/common/base_env.cpp +++ b/server/tp_core/common/base_env.cpp @@ -1,48 +1,48 @@ -#include "base_env.h" - -TppEnvBase::TppEnvBase() -{} - -TppEnvBase::~TppEnvBase() -{} - -bool TppEnvBase::init(TPP_INIT_ARGS* args) -{ - if (NULL == args) - { - EXLOGE("invalid init args(1).\n"); - return false; - } - - EXLOG_USE_LOGGER(args->logger); - - exec_path = args->exec_path; - etc_path = args->etc_path; - replay_path = args->replay_path; - - get_connect_info = args->func_get_connect_info; - free_connect_info = args->func_free_connect_info; - session_begin = args->func_session_begin; - session_update = args->func_session_update; - session_end = args->func_session_end; - - if (NULL == get_connect_info || NULL == free_connect_info || NULL == session_begin || NULL == session_update || NULL == session_end) - { - EXLOGE("invalid init args(2).\n"); - return false; - } - - if (NULL == args->cfg) - { - EXLOGE("invalid init args(3).\n"); - return false; - } - - if (!_on_init(args)) - { - EXLOGE("invalid init args(4).\n"); - return false; - } - - return true; -} +#include "base_env.h" + +TppEnvBase::TppEnvBase() +{} + +TppEnvBase::~TppEnvBase() +{} + +bool TppEnvBase::init(TPP_INIT_ARGS* args) +{ + if (NULL == args) + { + EXLOGE("invalid init args(1).\n"); + return false; + } + + EXLOG_USE_LOGGER(args->logger); + + exec_path = args->exec_path; + etc_path = args->etc_path; + replay_path = args->replay_path; + + get_connect_info = args->func_get_connect_info; + free_connect_info = args->func_free_connect_info; + session_begin = args->func_session_begin; + session_update = args->func_session_update; + session_end = args->func_session_end; + + if (NULL == get_connect_info || NULL == free_connect_info || NULL == session_begin || NULL == session_update || NULL == session_end) + { + EXLOGE("invalid init args(2).\n"); + return false; + } + + if (NULL == args->cfg) + { + EXLOGE("invalid init args(3).\n"); + return false; + } + + if (!_on_init(args)) + { + EXLOGE("invalid init args(4).\n"); + return false; + } + + return true; +} diff --git a/server/tp_core/common/base_env.h b/server/tp_core/common/base_env.h index c57fd41..316216d 100644 --- a/server/tp_core/common/base_env.h +++ b/server/tp_core/common/base_env.h @@ -1,29 +1,29 @@ -#ifndef __TS_BASE_ENV_H__ -#define __TS_BASE_ENV_H__ - -#include "protocol_interface.h" - -class TppEnvBase -{ -public: - TppEnvBase(); - virtual ~TppEnvBase(); - - bool init(TPP_INIT_ARGS* args); - -public: - ex_wstr exec_path; - ex_wstr etc_path; // ļSSH˽ԿļĴ· - ex_wstr replay_path; - - TPP_GET_CONNNECT_INFO_FUNC get_connect_info; - TPP_FREE_CONNECT_INFO_FUNC free_connect_info; - TPP_SESSION_BEGIN_FUNC session_begin; - TPP_SESSION_UPDATE_FUNC session_update; - TPP_SESSION_END_FUNC session_end; - -protected: - virtual bool _on_init(TPP_INIT_ARGS* args) = 0; -}; - -#endif // __TS_BASE_ENV_H__ +#ifndef __TS_BASE_ENV_H__ +#define __TS_BASE_ENV_H__ + +#include "protocol_interface.h" + +class TppEnvBase +{ +public: + TppEnvBase(); + virtual ~TppEnvBase(); + + bool init(TPP_INIT_ARGS* args); + +public: + ex_wstr exec_path; + ex_wstr etc_path; // 配置文件、SSH服务器的私钥文件的存放路径 + ex_wstr replay_path; + + TPP_GET_CONNNECT_INFO_FUNC get_connect_info; + TPP_FREE_CONNECT_INFO_FUNC free_connect_info; + TPP_SESSION_BEGIN_FUNC session_begin; + TPP_SESSION_UPDATE_FUNC session_update; + TPP_SESSION_END_FUNC session_end; + +protected: + virtual bool _on_init(TPP_INIT_ARGS* args) = 0; +}; + +#endif // __TS_BASE_ENV_H__ diff --git a/server/tp_core/common/base_record.cpp b/server/tp_core/common/base_record.cpp index 10849db..ae0d19f 100644 --- a/server/tp_core/common/base_record.cpp +++ b/server/tp_core/common/base_record.cpp @@ -1,4 +1,4 @@ -#include +#include #include "base_record.h" diff --git a/server/tp_core/common/base_record.h b/server/tp_core/common/base_record.h index cdcabb3..a78d1b0 100644 --- a/server/tp_core/common/base_record.h +++ b/server/tp_core/common/base_record.h @@ -1,101 +1,101 @@ -#ifndef __TS_BASE_RECORD_H__ -#define __TS_BASE_RECORD_H__ - -#include "base_env.h" -#include "ts_membuf.h" -#include "protocol_interface.h" - -#include - -#define MAX_SIZE_PER_FILE 4194304 // 4M = 1024*1024*4 - -#pragma pack(push,1) - -/* - * ¼ - * - * һ¼ΪļһϢļһļ - * ڲ4M5룬ͽдļУͬʱϢļ - * - */ - - -// ¼ļͷ(¼д룬ıIJ) -typedef struct TS_RECORD_HEADER_INFO -{ - ex_u32 magic; // "TPPR" ־ TelePort Protocol Record - ex_u16 ver; // ¼ļ汾ĿǰΪ3 - ex_u32 packages; // ܰ - ex_u32 time_ms; // ܺʱ룩 - //ex_u32 file_size; // ļС -}TS_RECORD_HEADER_INFO; -#define ts_record_header_info_size sizeof(TS_RECORD_HEADER_INFO) - -// ¼ļͷ(̶䲿) -typedef struct TS_RECORD_HEADER_BASIC -{ - ex_u16 protocol_type; // Э飺1=RDP, 2=SSH, 3=Telnet - ex_u16 protocol_sub_type; // Э飺100=RDP-DESKTOP, 200=SSH-SHELL, 201=SSH-SFTP, 300=Telnet - ex_u64 timestamp; // ¼ʼʱ䣨UTCʱ - ex_u16 width; // ʼĻߴ磺 - ex_u16 height; // ʼĻߴ磺 - char user_username[64]; // teleport˺ - char acc_username[64]; // Զû - - char host_ip[40]; // ԶIP - char conn_ip[40]; // ԶIP - ex_u16 conn_port; // Զ˿ - - char client_ip[40]; // ͻIP - - // RDPר - ex_u8 rdp_security; // 0 = RDP, 1 = TLS - - ex_u8 _reserve[512 - 2 - 2 - 8 - 2 - 2 - 64 - 64 - 40 - 40 - 2 - 40 - 1 - ts_record_header_info_size]; -}TS_RECORD_HEADER_BASIC; -#define ts_record_header_basic_size sizeof(TS_RECORD_HEADER_BASIC) - -typedef struct TS_RECORD_HEADER -{ - TS_RECORD_HEADER_INFO info; - TS_RECORD_HEADER_BASIC basic; -}TS_RECORD_HEADER; - -// header֣header-info + header-basic = 512B -#define ts_record_header_size sizeof(TS_RECORD_HEADER) - - -// һݰͷ -typedef struct TS_RECORD_PKG -{ - ex_u8 type; // - ex_u32 size; // ܴСͷ - ex_u32 time_ms; // ʼʱʱ룬ζһӲܳ49죩 - ex_u8 _reserve[3]; // -}TS_RECORD_PKG; - -#pragma pack(pop) - -class TppRecBase -{ -public: - TppRecBase(); - virtual ~TppRecBase(); - - bool begin(const wchar_t* base_path, const wchar_t* base_fname, int record_id, const TPP_CONNECT_INFO* info); - bool end(); - -protected: - virtual bool _on_begin(const TPP_CONNECT_INFO* info) = 0; - virtual bool _on_end() = 0; - -protected: - ex_wstr m_base_path; // ¼ļ· /usr/local/teleport/data/replay/ssh/123ֱڲӵģΪλỰ¼ļĿ¼ - ex_wstr m_base_fname; // ¼ļļչ֣ڲԴΪϳļȫ¼ļ m_base_path ָĿ¼ - - ex_u64 m_start_time; - - MemBuffer m_cache; -}; - -#endif // __TS_BASE_RECORD_H__ +#ifndef __TS_BASE_RECORD_H__ +#define __TS_BASE_RECORD_H__ + +#include "base_env.h" +#include "ts_membuf.h" +#include "protocol_interface.h" + +#include + +#define MAX_SIZE_PER_FILE 4194304 // 4M = 1024*1024*4 + +#pragma pack(push,1) + +/* + * 录像 + * + * 一个录像分为两个文件,一个信息文件,一个数据文件。 + * 服务内部缓存最大4M,或者5秒,就将数据写入数据文件中,并同时更新信息文件。 + * + */ + + +// 录像文件头(随着录像数据写入,会改变的部分) +typedef struct TS_RECORD_HEADER_INFO +{ + ex_u32 magic; // "TPPR" 标志 TelePort Protocol Record + ex_u16 ver; // 录像文件版本,目前为3 + ex_u32 packages; // 总包数 + ex_u32 time_ms; // 总耗时(毫秒) + //ex_u32 file_size; // 数据文件大小 +}TS_RECORD_HEADER_INFO; +#define ts_record_header_info_size sizeof(TS_RECORD_HEADER_INFO) + +// 录像文件头(固定不变部分) +typedef struct TS_RECORD_HEADER_BASIC +{ + ex_u16 protocol_type; // 协议:1=RDP, 2=SSH, 3=Telnet + ex_u16 protocol_sub_type; // 子协议:100=RDP-DESKTOP, 200=SSH-SHELL, 201=SSH-SFTP, 300=Telnet + ex_u64 timestamp; // 本次录像的起始时间(UTC时间戳) + ex_u16 width; // 初始屏幕尺寸:宽 + ex_u16 height; // 初始屏幕尺寸:高 + char user_username[64]; // teleport账号 + char acc_username[64]; // 远程主机用户名 + + char host_ip[40]; // 远程主机IP + char conn_ip[40]; // 远程主机IP + ex_u16 conn_port; // 远程主机端口 + + char client_ip[40]; // 客户端IP + + // RDP专有 + ex_u8 rdp_security; // 0 = RDP, 1 = TLS + + ex_u8 _reserve[512 - 2 - 2 - 8 - 2 - 2 - 64 - 64 - 40 - 40 - 2 - 40 - 1 - ts_record_header_info_size]; +}TS_RECORD_HEADER_BASIC; +#define ts_record_header_basic_size sizeof(TS_RECORD_HEADER_BASIC) + +typedef struct TS_RECORD_HEADER +{ + TS_RECORD_HEADER_INFO info; + TS_RECORD_HEADER_BASIC basic; +}TS_RECORD_HEADER; + +// header部分(header-info + header-basic) = 512B +#define ts_record_header_size sizeof(TS_RECORD_HEADER) + + +// 一个数据包的头 +typedef struct TS_RECORD_PKG +{ + ex_u8 type; // 包的数据类型 + ex_u32 size; // 这个包的总大小(不含包头) + ex_u32 time_ms; // 这个包距起始时间的时间差(毫秒,意味着一个连接不能持续超过49天) + ex_u8 _reserve[3]; // 保留 +}TS_RECORD_PKG; + +#pragma pack(pop) + +class TppRecBase +{ +public: + TppRecBase(); + virtual ~TppRecBase(); + + bool begin(const wchar_t* base_path, const wchar_t* base_fname, int record_id, const TPP_CONNECT_INFO* info); + bool end(); + +protected: + virtual bool _on_begin(const TPP_CONNECT_INFO* info) = 0; + virtual bool _on_end() = 0; + +protected: + ex_wstr m_base_path; // 录像文件基础路径,例如 /usr/local/teleport/data/replay/ssh/123,数字编号是内部附加的,作为本次会话录像文件的目录名称 + ex_wstr m_base_fname; // 录像文件的文件名,不含扩展名部分,内部会以此为基础合成文件全名,并将录像文件存放在 m_base_path 指向的目录中 + + ex_u64 m_start_time; + + MemBuffer m_cache; +}; + +#endif // __TS_BASE_RECORD_H__ diff --git a/server/tp_core/common/protocol_interface.h b/server/tp_core/common/protocol_interface.h index 50051c6..bc201ec 100644 --- a/server/tp_core/common/protocol_interface.h +++ b/server/tp_core/common/protocol_interface.h @@ -1,100 +1,100 @@ -#ifndef __TP_PROTOCOL_INTERFACE_H__ -#define __TP_PROTOCOL_INTERFACE_H__ - -#include "ts_const.h" -#include - -#ifdef EX_OS_WIN32 -# ifdef TPP_EXPORTS -# define TPP_API __declspec(dllexport) -# else -# define TPP_API __declspec(dllimport) -# endif -#else -# define TPP_API -#endif - -#define TPP_CMD_INIT 0x00000000 -#define TPP_CMD_SET_RUNTIME_CFG 0x00000005 -#define TPP_CMD_KILL_SESSIONS 0x00000006 - -typedef struct TPP_CONNECT_INFO -{ - char* sid; - - // ϢصҪصID - int user_id; - int host_id; - int acc_id; - - char* user_username; // 뱾ӵû - - char* host_ip; // ԶIPֱģʽremote_host_ipͬ - char* conn_ip; // ҪӵԶIPǶ˿ӳģʽΪ·IP - int conn_port; // ҪӵԶĶ˿ڣǶ˿ӳģʽΪ·Ķ˿ڣ - char* client_ip; - - char* acc_username; // Զ˺ - char* acc_secret; // Զ˺ŵ루˽Կ - char* username_prompt; // for telnet - char* password_prompt; // for telnet - - int protocol_type; - int protocol_sub_type; - int protocol_flag; - int record_flag; - int auth_type; -}TPP_CONNECT_INFO; - -typedef TPP_CONNECT_INFO* (*TPP_GET_CONNNECT_INFO_FUNC)(const char* sid); -typedef void(*TPP_FREE_CONNECT_INFO_FUNC)(TPP_CONNECT_INFO* info); -typedef bool(*TPP_SESSION_BEGIN_FUNC)(const TPP_CONNECT_INFO* info, int* db_id); -typedef bool(*TPP_SESSION_UPDATE_FUNC)(int db_id, int protocol_sub_type, int state); -typedef bool(*TPP_SESSION_END_FUNC)(const char* sid, int db_id, int ret); - - -typedef struct TPP_INIT_ARGS -{ - ExLogger* logger; - ex_wstr exec_path; - ex_wstr etc_path; - ex_wstr replay_path; - ExIniFile* cfg; - - TPP_GET_CONNNECT_INFO_FUNC func_get_connect_info; - TPP_FREE_CONNECT_INFO_FUNC func_free_connect_info; - TPP_SESSION_BEGIN_FUNC func_session_begin; - TPP_SESSION_UPDATE_FUNC func_session_update; - TPP_SESSION_END_FUNC func_session_end; -}TPP_INIT_ARGS; - -// typedef struct TPP_SET_CFG_ARGS { -// ex_u32 noop_timeout; // as second. -// }TPP_SET_CFG_ARGS; - -#ifdef __cplusplus -extern "C" -{ -#endif - - TPP_API ex_rv tpp_init(TPP_INIT_ARGS* init_args); - TPP_API ex_rv tpp_start(void); - TPP_API ex_rv tpp_stop(void); - TPP_API void tpp_timer(void); -// TPP_API void tpp_set_cfg(TPP_SET_CFG_ARGS* cfg_args); - - TPP_API ex_rv tpp_command(ex_u32 cmd, const char* param); - -#ifdef __cplusplus -} -#endif - -typedef ex_rv (*TPP_INIT_FUNC)(TPP_INIT_ARGS* init_args); -typedef ex_rv (*TPP_START_FUNC)(void); -typedef ex_rv(*TPP_STOP_FUNC)(void); -typedef void(*TPP_TIMER_FUNC)(void); -// typedef void(*TPP_SET_CFG_FUNC)(TPP_SET_CFG_ARGS* cfg_args); - -typedef ex_rv(*TPP_COMMAND_FUNC)(ex_u32 cmd, const char* param); // param is a JSON formatted string. - -#endif // __TP_PROTOCOL_INTERFACE_H__ +#ifndef __TP_PROTOCOL_INTERFACE_H__ +#define __TP_PROTOCOL_INTERFACE_H__ + +#include "ts_const.h" +#include + +#ifdef EX_OS_WIN32 +# ifdef TPP_EXPORTS +# define TPP_API __declspec(dllexport) +# else +# define TPP_API __declspec(dllimport) +# endif +#else +# define TPP_API +#endif + +#define TPP_CMD_INIT 0x00000000 +#define TPP_CMD_SET_RUNTIME_CFG 0x00000005 +#define TPP_CMD_KILL_SESSIONS 0x00000006 + +typedef struct TPP_CONNECT_INFO +{ + char* sid; + + // 与此连接信息相关的三个要素的ID + int user_id; + int host_id; + int acc_id; + + char* user_username; // 申请本次连接的用户名 + + char* host_ip; // 真正的远程主机IP(如果是直接连接模式,则与remote_host_ip相同) + char* conn_ip; // 要连接的远程主机的IP(如果是端口映射模式,则为路由主机的IP) + int conn_port; // 要连接的远程主机的端口(如果是端口映射模式,则为路由主机的端口) + char* client_ip; + + char* acc_username; // 远程主机的账号 + char* acc_secret; // 远程主机账号的密码(或者私钥) + char* username_prompt; // for telnet + char* password_prompt; // for telnet + + int protocol_type; + int protocol_sub_type; + int protocol_flag; + int record_flag; + int auth_type; +}TPP_CONNECT_INFO; + +typedef TPP_CONNECT_INFO* (*TPP_GET_CONNNECT_INFO_FUNC)(const char* sid); +typedef void(*TPP_FREE_CONNECT_INFO_FUNC)(TPP_CONNECT_INFO* info); +typedef bool(*TPP_SESSION_BEGIN_FUNC)(const TPP_CONNECT_INFO* info, int* db_id); +typedef bool(*TPP_SESSION_UPDATE_FUNC)(int db_id, int protocol_sub_type, int state); +typedef bool(*TPP_SESSION_END_FUNC)(const char* sid, int db_id, int ret); + + +typedef struct TPP_INIT_ARGS +{ + ExLogger* logger; + ex_wstr exec_path; + ex_wstr etc_path; + ex_wstr replay_path; + ExIniFile* cfg; + + TPP_GET_CONNNECT_INFO_FUNC func_get_connect_info; + TPP_FREE_CONNECT_INFO_FUNC func_free_connect_info; + TPP_SESSION_BEGIN_FUNC func_session_begin; + TPP_SESSION_UPDATE_FUNC func_session_update; + TPP_SESSION_END_FUNC func_session_end; +}TPP_INIT_ARGS; + +// typedef struct TPP_SET_CFG_ARGS { +// ex_u32 noop_timeout; // as second. +// }TPP_SET_CFG_ARGS; + +#ifdef __cplusplus +extern "C" +{ +#endif + + TPP_API ex_rv tpp_init(TPP_INIT_ARGS* init_args); + TPP_API ex_rv tpp_start(void); + TPP_API ex_rv tpp_stop(void); + TPP_API void tpp_timer(void); +// TPP_API void tpp_set_cfg(TPP_SET_CFG_ARGS* cfg_args); + + TPP_API ex_rv tpp_command(ex_u32 cmd, const char* param); + +#ifdef __cplusplus +} +#endif + +typedef ex_rv (*TPP_INIT_FUNC)(TPP_INIT_ARGS* init_args); +typedef ex_rv (*TPP_START_FUNC)(void); +typedef ex_rv(*TPP_STOP_FUNC)(void); +typedef void(*TPP_TIMER_FUNC)(void); +// typedef void(*TPP_SET_CFG_FUNC)(TPP_SET_CFG_ARGS* cfg_args); + +typedef ex_rv(*TPP_COMMAND_FUNC)(ex_u32 cmd, const char* param); // param is a JSON formatted string. + +#endif // __TP_PROTOCOL_INTERFACE_H__ diff --git a/server/tp_core/common/ts_const.h b/server/tp_core/common/ts_const.h index da1fc45..be55510 100644 --- a/server/tp_core/common/ts_const.h +++ b/server/tp_core/common/ts_const.h @@ -1,26 +1,26 @@ -#ifndef __TS_ERRNO_H__ -#define __TS_ERRNO_H__ - -//#include "ts_types.h" - -#define TS_RDP_PROXY_PORT 52089 -#define TS_RDP_PROXY_HOST "0.0.0.0" - -#define TS_SSH_PROXY_PORT 52189 -#define TS_SSH_PROXY_HOST "0.0.0.0" - -#define TS_TELNET_PROXY_PORT 52389 -#define TS_TELNET_PROXY_HOST "0.0.0.0" - -#define TS_HTTP_RPC_PORT 52080 -//#define TS_HTTP_RPC_HOST "127.0.0.1" -#define TS_HTTP_RPC_HOST "localhost" - - -#define TS_RDP_PROTOCOL_RDP 0 -#define TS_RDP_PROTOCOL_TLS 1 -#define TS_RDP_PROTOCOL_HYBRID 2 -#define TS_RDP_PROTOCOL_RDSTLS 4 -#define TS_RDP_PROTOCOL_HYBRID_EX 8 - -#endif // __TS_ERRNO_H__ +#ifndef __TS_ERRNO_H__ +#define __TS_ERRNO_H__ + +//#include "ts_types.h" + +#define TS_RDP_PROXY_PORT 52089 +#define TS_RDP_PROXY_HOST "0.0.0.0" + +#define TS_SSH_PROXY_PORT 52189 +#define TS_SSH_PROXY_HOST "0.0.0.0" + +#define TS_TELNET_PROXY_PORT 52389 +#define TS_TELNET_PROXY_HOST "0.0.0.0" + +#define TS_HTTP_RPC_PORT 52080 +//#define TS_HTTP_RPC_HOST "127.0.0.1" +#define TS_HTTP_RPC_HOST "localhost" + + +#define TS_RDP_PROTOCOL_RDP 0 +#define TS_RDP_PROTOCOL_TLS 1 +#define TS_RDP_PROTOCOL_HYBRID 2 +#define TS_RDP_PROTOCOL_RDSTLS 4 +#define TS_RDP_PROTOCOL_HYBRID_EX 8 + +#endif // __TS_ERRNO_H__ diff --git a/server/tp_core/common/ts_membuf.cpp b/server/tp_core/common/ts_membuf.cpp index c5f063f..f601f6a 100644 --- a/server/tp_core/common/ts_membuf.cpp +++ b/server/tp_core/common/ts_membuf.cpp @@ -1,4 +1,4 @@ -#include "ts_membuf.h" +#include "ts_membuf.h" #include MemBuffer::MemBuffer()// : m_buffer(NULL), m_data_size(0), m_buffer_size(0) @@ -45,7 +45,7 @@ void MemBuffer::reserve(size_t size) return; } - // µĻСȡ MEMBUF_BLOCK_SIZE + // 将新的缓冲区大小取整到 MEMBUF_BLOCK_SIZE 的整数倍 size_t new_size = (size + MEMBUF_BLOCK_SIZE - 1) & ~(MEMBUF_BLOCK_SIZE - 1); //EXLOGD("[mbuf:%p] reserve(2): m_buf: %p, m_buf_size: %d, size: %d, new size: %d.\n", this, m_buffer, m_buffer_size, size, new_size); diff --git a/server/tp_core/common/ts_membuf.h b/server/tp_core/common/ts_membuf.h index 0d7d283..95fa44c 100644 --- a/server/tp_core/common/ts_membuf.h +++ b/server/tp_core/common/ts_membuf.h @@ -1,4 +1,4 @@ -#ifndef __TS_MEMBUF_H__ +#ifndef __TS_MEMBUF_H__ #define __TS_MEMBUF_H__ #include @@ -11,15 +11,15 @@ public: MemBuffer(); virtual ~MemBuffer(); - // sizeֽڵݵĩβܻᵼ» + // 附加size字节的数据到缓冲区末尾(可能会导致缓冲区扩大) void append(const ex_u8* data, size_t size); - // Ϊָֽܻ󻺳С֤Чݲᱻı䣩 + // 缓冲区至少为指定字节数(可能会扩大缓冲区,但不会缩小缓冲区,保证有效数据不会被改变) void reserve(size_t size); - // mЧݸӵԼЧĩβܻ󻺳mݲ + // 将m的有效数据附加到自己的有效数据末尾,可能会扩大缓冲区,m内容不变 void concat(const MemBuffer& m); - // ӻͷƳsizeֽڣСܲʣµЧǰơ + // 从缓冲区头部移除size字节(缓冲区大小可能并不会收缩),剩下的有效数据前移。 void pop(size_t size); - // ջЧΪ0ֽڣ䣩 + // 清空缓冲区(有效数据为0字节,缓冲区不变) void empty(void) { m_data_size = 0; } bool is_empty(void) { return m_data_size == 0; } diff --git a/server/tp_core/common/ts_memstream.cpp b/server/tp_core/common/ts_memstream.cpp index e51c142..64388cb 100644 --- a/server/tp_core/common/ts_memstream.cpp +++ b/server/tp_core/common/ts_memstream.cpp @@ -1,222 +1,222 @@ -#include "ts_memstream.h" - -MemStream::MemStream(MemBuffer& mbuf) : m_mbuf(mbuf) -{ - m_offset = 0; -} - -MemStream::~MemStream() -{} - -void MemStream::reset(void) -{ - m_mbuf.empty(); - rewind(); -} - - -bool MemStream::seek(size_t offset) -{ - if (offset > m_mbuf.size()) - return false; - - m_offset = offset; - return true; -} - -bool MemStream::skip(size_t n) -{ - if (0 == n) - return true; - - if (m_offset + n > m_mbuf.size()) - return false; - m_offset += n; - return true; -} - -bool MemStream::rewind(size_t n) -{ - if (m_offset < n) - return false; - - if (0 == n) - m_offset = 0; - else - m_offset -= n; - return true; -} - -ex_u8 MemStream::get_u8(void) -{ - ASSERT(m_offset + 1 <= m_mbuf.size()); - - ex_u8 v = (m_mbuf.data() + m_offset)[0]; - m_offset++; - return v; -} - -ex_u16 MemStream::get_u16_le(void) -{ - ASSERT(m_offset + 2 <= m_mbuf.size()); - - ex_u8* p = m_mbuf.data() + m_offset; -#if defined(B_ENDIAN) - ex_u16 v = (ex_u16)(p[0] | (p[1] << 8)); -#else - ex_u16 v = ((ex_u16*)p)[0]; -#endif - m_offset += 2; - return v; -} - -ex_u16 MemStream::get_u16_be(void) -{ - ASSERT(m_offset + 2 <= m_mbuf.size()); - - ex_u8* p = m_mbuf.data() + m_offset; -#if defined(B_ENDIAN) - ex_u16 v = ((ex_u16*)p)[0]; -#else - ex_u16 v = (ex_u16)((p[0] << 8) | p[1]); -#endif - m_offset += 2; - return v; -} - - -ex_u32 MemStream::get_u32_le(void) -{ - ASSERT(m_offset + 4 <= m_mbuf.size()); - - ex_u8* p = m_mbuf.data() + m_offset; -#if defined(B_ENDIAN) - ex_u32 v = (ex_u32)(p[0] | (p[1] << 8) | (p[2] << 16) | (p[3] << 24)); -#else - ex_u32 v = ((ex_u32*)p)[0]; -#endif - m_offset += 4; - return v; -} - -ex_u32 MemStream::get_u32_be(void) -{ - ASSERT(m_offset + 4 <= m_mbuf.size()); - - ex_u8* p = m_mbuf.data() + m_offset; -#if defined(B_ENDIAN) - ex_u32 v = ((ex_u32*)p)[0]; -#else - ex_u32 v = (ex_u32)((p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3]); -#endif - m_offset += 4; - return v; -} - -ex_u8* MemStream::get_bin(size_t n) -{ - ASSERT(m_offset + 4 <= m_mbuf.size()); - ex_u8* p = m_mbuf.data() + m_offset; - m_offset += n; - return p; -} - - -void MemStream::put_zero(size_t n) -{ - m_mbuf.reserve(m_mbuf.size() + n); - memset(m_mbuf.data() + m_offset, 0, n); - m_offset += n; - if (m_mbuf.size() < m_offset) - m_mbuf.size(m_offset); -} - -void MemStream::put_u8(ex_u8 v) -{ - m_mbuf.reserve(m_mbuf.size() + 1); - - (m_mbuf.data() + m_offset)[0] = v; - m_offset++; - if (m_mbuf.size() < m_offset) - m_mbuf.size(m_offset); -} - -void MemStream::put_u16_le(ex_u16 v) -{ - m_mbuf.reserve(m_mbuf.size() + 2); - - ex_u8* p = m_mbuf.data() + m_offset; -#if defined(B_ENDIAN) - p[0] = (ex_u8)v; - p[1] = (ex_u8)(v >> 8); -#else - ((ex_u16*)p)[0] = v; -#endif - m_offset += 2; - if (m_mbuf.size() < m_offset) - m_mbuf.size(m_offset); -} - -void MemStream::put_u16_be(ex_u16 v) -{ - m_mbuf.reserve(m_mbuf.size() + 2); - - ex_u8* p = m_mbuf.data() + m_offset; -#if defined(B_ENDIAN) - ((ex_u16*)p)[0] = v; -#else - ex_u8* _v = (ex_u8*)&v; - p[0] = _v[1]; - p[1] = _v[0]; -#endif - m_offset += 2; - if (m_mbuf.size() < m_offset) - m_mbuf.size(m_offset); -} - -void MemStream::put_u32_le(ex_u32 v) -{ - m_mbuf.reserve(m_mbuf.size() + 4); - - ex_u8* p = m_mbuf.data() + m_offset; -#if defined(B_ENDIAN) - p[0] = (ex_u8)v; - p[1] = (ex_u8)(v >> 8); - p[2] = (ex_u8)(v >> 16); - p[3] = (ex_u8)(v >> 24); -#else - ((ex_u32*)p)[0] = v; -#endif - m_offset += 4; - if (m_mbuf.size() < m_offset) - m_mbuf.size(m_offset); -} - -void MemStream::put_u32_be(ex_u32 v) -{ - m_mbuf.reserve(m_mbuf.size() + 4); - - ex_u8* p = m_mbuf.data() + m_offset; -#if defined(B_ENDIAN) - ((ex_u32*)p)[0] = v; -#else - ex_u8* _v = (ex_u8*)&v; - p[0] = _v[3]; - p[1] = _v[2]; - p[2] = _v[1]; - p[3] = _v[0]; -#endif - m_offset += 4; - if (m_mbuf.size() < m_offset) - m_mbuf.size(m_offset); -} - -void MemStream::put_bin(const ex_u8* p, size_t n) -{ - m_mbuf.reserve(m_mbuf.size() + n); - memcpy(m_mbuf.data() + m_offset, p, n); - m_offset += n; - if (m_mbuf.size() < m_offset) - m_mbuf.size(m_offset); -} - +#include "ts_memstream.h" + +MemStream::MemStream(MemBuffer& mbuf) : m_mbuf(mbuf) +{ + m_offset = 0; +} + +MemStream::~MemStream() +{} + +void MemStream::reset(void) +{ + m_mbuf.empty(); + rewind(); +} + + +bool MemStream::seek(size_t offset) +{ + if (offset > m_mbuf.size()) + return false; + + m_offset = offset; + return true; +} + +bool MemStream::skip(size_t n) +{ + if (0 == n) + return true; + + if (m_offset + n > m_mbuf.size()) + return false; + m_offset += n; + return true; +} + +bool MemStream::rewind(size_t n) +{ + if (m_offset < n) + return false; + + if (0 == n) + m_offset = 0; + else + m_offset -= n; + return true; +} + +ex_u8 MemStream::get_u8(void) +{ + ASSERT(m_offset + 1 <= m_mbuf.size()); + + ex_u8 v = (m_mbuf.data() + m_offset)[0]; + m_offset++; + return v; +} + +ex_u16 MemStream::get_u16_le(void) +{ + ASSERT(m_offset + 2 <= m_mbuf.size()); + + ex_u8* p = m_mbuf.data() + m_offset; +#if defined(B_ENDIAN) + ex_u16 v = (ex_u16)(p[0] | (p[1] << 8)); +#else + ex_u16 v = ((ex_u16*)p)[0]; +#endif + m_offset += 2; + return v; +} + +ex_u16 MemStream::get_u16_be(void) +{ + ASSERT(m_offset + 2 <= m_mbuf.size()); + + ex_u8* p = m_mbuf.data() + m_offset; +#if defined(B_ENDIAN) + ex_u16 v = ((ex_u16*)p)[0]; +#else + ex_u16 v = (ex_u16)((p[0] << 8) | p[1]); +#endif + m_offset += 2; + return v; +} + + +ex_u32 MemStream::get_u32_le(void) +{ + ASSERT(m_offset + 4 <= m_mbuf.size()); + + ex_u8* p = m_mbuf.data() + m_offset; +#if defined(B_ENDIAN) + ex_u32 v = (ex_u32)(p[0] | (p[1] << 8) | (p[2] << 16) | (p[3] << 24)); +#else + ex_u32 v = ((ex_u32*)p)[0]; +#endif + m_offset += 4; + return v; +} + +ex_u32 MemStream::get_u32_be(void) +{ + ASSERT(m_offset + 4 <= m_mbuf.size()); + + ex_u8* p = m_mbuf.data() + m_offset; +#if defined(B_ENDIAN) + ex_u32 v = ((ex_u32*)p)[0]; +#else + ex_u32 v = (ex_u32)((p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3]); +#endif + m_offset += 4; + return v; +} + +ex_u8* MemStream::get_bin(size_t n) +{ + ASSERT(m_offset + 4 <= m_mbuf.size()); + ex_u8* p = m_mbuf.data() + m_offset; + m_offset += n; + return p; +} + + +void MemStream::put_zero(size_t n) +{ + m_mbuf.reserve(m_mbuf.size() + n); + memset(m_mbuf.data() + m_offset, 0, n); + m_offset += n; + if (m_mbuf.size() < m_offset) + m_mbuf.size(m_offset); +} + +void MemStream::put_u8(ex_u8 v) +{ + m_mbuf.reserve(m_mbuf.size() + 1); + + (m_mbuf.data() + m_offset)[0] = v; + m_offset++; + if (m_mbuf.size() < m_offset) + m_mbuf.size(m_offset); +} + +void MemStream::put_u16_le(ex_u16 v) +{ + m_mbuf.reserve(m_mbuf.size() + 2); + + ex_u8* p = m_mbuf.data() + m_offset; +#if defined(B_ENDIAN) + p[0] = (ex_u8)v; + p[1] = (ex_u8)(v >> 8); +#else + ((ex_u16*)p)[0] = v; +#endif + m_offset += 2; + if (m_mbuf.size() < m_offset) + m_mbuf.size(m_offset); +} + +void MemStream::put_u16_be(ex_u16 v) +{ + m_mbuf.reserve(m_mbuf.size() + 2); + + ex_u8* p = m_mbuf.data() + m_offset; +#if defined(B_ENDIAN) + ((ex_u16*)p)[0] = v; +#else + ex_u8* _v = (ex_u8*)&v; + p[0] = _v[1]; + p[1] = _v[0]; +#endif + m_offset += 2; + if (m_mbuf.size() < m_offset) + m_mbuf.size(m_offset); +} + +void MemStream::put_u32_le(ex_u32 v) +{ + m_mbuf.reserve(m_mbuf.size() + 4); + + ex_u8* p = m_mbuf.data() + m_offset; +#if defined(B_ENDIAN) + p[0] = (ex_u8)v; + p[1] = (ex_u8)(v >> 8); + p[2] = (ex_u8)(v >> 16); + p[3] = (ex_u8)(v >> 24); +#else + ((ex_u32*)p)[0] = v; +#endif + m_offset += 4; + if (m_mbuf.size() < m_offset) + m_mbuf.size(m_offset); +} + +void MemStream::put_u32_be(ex_u32 v) +{ + m_mbuf.reserve(m_mbuf.size() + 4); + + ex_u8* p = m_mbuf.data() + m_offset; +#if defined(B_ENDIAN) + ((ex_u32*)p)[0] = v; +#else + ex_u8* _v = (ex_u8*)&v; + p[0] = _v[3]; + p[1] = _v[2]; + p[2] = _v[1]; + p[3] = _v[0]; +#endif + m_offset += 4; + if (m_mbuf.size() < m_offset) + m_mbuf.size(m_offset); +} + +void MemStream::put_bin(const ex_u8* p, size_t n) +{ + m_mbuf.reserve(m_mbuf.size() + n); + memcpy(m_mbuf.data() + m_offset, p, n); + m_offset += n; + if (m_mbuf.size() < m_offset) + m_mbuf.size(m_offset); +} + diff --git a/server/tp_core/common/ts_memstream.h b/server/tp_core/common/ts_memstream.h index 9ab425c..ddbb45f 100644 --- a/server/tp_core/common/ts_memstream.h +++ b/server/tp_core/common/ts_memstream.h @@ -1,45 +1,45 @@ -#ifndef __TS_MEMSTREAM_H__ -#define __TS_MEMSTREAM_H__ - -#include "ts_membuf.h" - -class MemStream -{ -public: - MemStream(MemBuffer& mbuf); - ~MemStream(); - - void reset(void); // ջݣͷڴ棩ָƶͷ - - bool seek(size_t offset); // ƶָ뵽ָƫƣԽ磬򷵻ش - bool rewind(size_t n = 0); // nֽڣԽ磬شnΪ0˵ʼ - bool skip(size_t n); // nֽڣԽ磬򷵻ش - - ex_u8* ptr(void) { return m_mbuf.data() + m_offset; } // صǰָ - size_t offset(void) { return m_offset; } // صǰָʼƫ - - size_t left(void) { return m_mbuf.size() - m_offset; } // ʣݵĴСӵǰָ뵽β - - ex_u8 get_u8(void); - ex_u16 get_u16_le(void); - ex_u16 get_u16_be(void); - ex_u32 get_u32_le(void); - ex_u32 get_u32_be(void); - ex_u8* get_bin(size_t n); // صǰָݵָ룬ڲƫƻƶnֽ - - void put_zero(size_t n); // nֽڵ0 - void put_u8(ex_u8 v); - void put_u16_le(ex_u16 v); - void put_u16_be(ex_u16 v); - void put_u32_le(ex_u32 v); - void put_u32_be(ex_u32 v); - void put_bin(const ex_u8* p, size_t n); // pָnֽ - - size_t size(void) { return m_mbuf.size(); } - -private: - MemBuffer& m_mbuf; - size_t m_offset; -}; - -#endif // __TS_MEMSTREAM_H__ +#ifndef __TS_MEMSTREAM_H__ +#define __TS_MEMSTREAM_H__ + +#include "ts_membuf.h" + +class MemStream +{ +public: + MemStream(MemBuffer& mbuf); + ~MemStream(); + + void reset(void); // 清空缓冲区数据(但不释放内存),指针移动到头部 + + bool seek(size_t offset); // 移动指针到指定偏移,如果越界,则返回错误 + bool rewind(size_t n = 0); // 回退n字节,如果越界,返回错误,如果n为0,则回退到最开始处 + bool skip(size_t n); // 跳过n字节,如果越界,则返回错误 + + ex_u8* ptr(void) { return m_mbuf.data() + m_offset; } // 返回当前数据指针 + size_t offset(void) { return m_offset; } // 返回当前指针相对数据起始的偏移 + + size_t left(void) { return m_mbuf.size() - m_offset; } // 返回剩余数据的大小(从当前数据指针到缓冲区结尾) + + ex_u8 get_u8(void); + ex_u16 get_u16_le(void); + ex_u16 get_u16_be(void); + ex_u32 get_u32_le(void); + ex_u32 get_u32_be(void); + ex_u8* get_bin(size_t n); // 返回当前指向的数据的指针,内部偏移会向后移动n字节 + + void put_zero(size_t n); // 填充n字节的0 + void put_u8(ex_u8 v); + void put_u16_le(ex_u16 v); + void put_u16_be(ex_u16 v); + void put_u32_le(ex_u32 v); + void put_u32_be(ex_u32 v); + void put_bin(const ex_u8* p, size_t n); // 填充p指向的n字节数据 + + size_t size(void) { return m_mbuf.size(); } + +private: + MemBuffer& m_mbuf; + size_t m_offset; +}; + +#endif // __TS_MEMSTREAM_H__ From f5b00ca02ab6b0743988c42637303051e68d1b9b Mon Sep 17 00:00:00 2001 From: Apex Liu Date: Mon, 2 Sep 2019 03:44:08 +0800 Subject: [PATCH 08/44] =?UTF-8?q?RDP=E5=BD=95=E5=83=8F=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E5=AE=89=E5=85=A8=E5=8D=8F=E8=AE=AE=E8=A7=84?= =?UTF-8?q?=E6=A0=BC=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/tp_core/common/base_record.h | 88 ++++++++++++++--------------- 1 file changed, 42 insertions(+), 46 deletions(-) diff --git a/server/tp_core/common/base_record.h b/server/tp_core/common/base_record.h index a78d1b0..5615895 100644 --- a/server/tp_core/common/base_record.h +++ b/server/tp_core/common/base_record.h @@ -20,45 +20,43 @@ */ -// 录像文件头(随着录像数据写入,会改变的部分) -typedef struct TS_RECORD_HEADER_INFO -{ - ex_u32 magic; // "TPPR" 标志 TelePort Protocol Record - ex_u16 ver; // 录像文件版本,目前为3 - ex_u32 packages; // 总包数 - ex_u32 time_ms; // 总耗时(毫秒) - //ex_u32 file_size; // 数据文件大小 + // 录像文件头(随着录像数据写入,会改变的部分) +typedef struct TS_RECORD_HEADER_INFO { + ex_u32 magic; // "TPPR" 标志 TelePort Protocol Record + ex_u16 ver; // 录像文件版本,目前为3 + ex_u32 packages; // 总包数 + ex_u32 time_ms; // 总耗时(毫秒) + //ex_u32 file_size; // 数据文件大小 }TS_RECORD_HEADER_INFO; #define ts_record_header_info_size sizeof(TS_RECORD_HEADER_INFO) // 录像文件头(固定不变部分) -typedef struct TS_RECORD_HEADER_BASIC -{ - ex_u16 protocol_type; // 协议:1=RDP, 2=SSH, 3=Telnet - ex_u16 protocol_sub_type; // 子协议:100=RDP-DESKTOP, 200=SSH-SHELL, 201=SSH-SFTP, 300=Telnet - ex_u64 timestamp; // 本次录像的起始时间(UTC时间戳) - ex_u16 width; // 初始屏幕尺寸:宽 - ex_u16 height; // 初始屏幕尺寸:高 - char user_username[64]; // teleport账号 - char acc_username[64]; // 远程主机用户名 +typedef struct TS_RECORD_HEADER_BASIC { + ex_u16 protocol_type; // 协议:1=RDP, 2=SSH, 3=Telnet + ex_u16 protocol_sub_type; // 子协议:100=RDP-DESKTOP, 200=SSH-SHELL, 201=SSH-SFTP, 300=Telnet + ex_u64 timestamp; // 本次录像的起始时间(UTC时间戳) + ex_u16 width; // 初始屏幕尺寸:宽 + ex_u16 height; // 初始屏幕尺寸:高 + char user_username[64]; // teleport账号 + char acc_username[64]; // 远程主机用户名 - char host_ip[40]; // 远程主机IP - char conn_ip[40]; // 远程主机IP - ex_u16 conn_port; // 远程主机端口 + char host_ip[40]; // 远程主机IP + char conn_ip[40]; // 远程主机IP + ex_u16 conn_port; // 远程主机端口 - char client_ip[40]; // 客户端IP + char client_ip[40]; // 客户端IP - // RDP专有 - ex_u8 rdp_security; // 0 = RDP, 1 = TLS +// // RDP专有 +// ex_u8 rdp_security; // 0 = RDP, 1 = TLS - ex_u8 _reserve[512 - 2 - 2 - 8 - 2 - 2 - 64 - 64 - 40 - 40 - 2 - 40 - 1 - ts_record_header_info_size]; +// ex_u8 _reserve[512 - 2 - 2 - 8 - 2 - 2 - 64 - 64 - 40 - 40 - 2 - 40 - 1 - ts_record_header_info_size]; + ex_u8 _reserve[512 - 2 - 2 - 8 - 2 - 2 - 64 - 64 - 40 - 40 - 2 - 40 - ts_record_header_info_size]; }TS_RECORD_HEADER_BASIC; #define ts_record_header_basic_size sizeof(TS_RECORD_HEADER_BASIC) -typedef struct TS_RECORD_HEADER -{ - TS_RECORD_HEADER_INFO info; - TS_RECORD_HEADER_BASIC basic; +typedef struct TS_RECORD_HEADER { + TS_RECORD_HEADER_INFO info; + TS_RECORD_HEADER_BASIC basic; }TS_RECORD_HEADER; // header部分(header-info + header-basic) = 512B @@ -66,36 +64,34 @@ typedef struct TS_RECORD_HEADER // 一个数据包的头 -typedef struct TS_RECORD_PKG -{ - ex_u8 type; // 包的数据类型 - ex_u32 size; // 这个包的总大小(不含包头) - ex_u32 time_ms; // 这个包距起始时间的时间差(毫秒,意味着一个连接不能持续超过49天) - ex_u8 _reserve[3]; // 保留 +typedef struct TS_RECORD_PKG { + ex_u8 type; // 包的数据类型 + ex_u32 size; // 这个包的总大小(不含包头) + ex_u32 time_ms; // 这个包距起始时间的时间差(毫秒,意味着一个连接不能持续超过49天) + ex_u8 _reserve[3]; // 保留 }TS_RECORD_PKG; #pragma pack(pop) -class TppRecBase -{ +class TppRecBase { public: - TppRecBase(); - virtual ~TppRecBase(); + TppRecBase(); + virtual ~TppRecBase(); - bool begin(const wchar_t* base_path, const wchar_t* base_fname, int record_id, const TPP_CONNECT_INFO* info); - bool end(); + bool begin(const wchar_t* base_path, const wchar_t* base_fname, int record_id, const TPP_CONNECT_INFO* info); + bool end(); protected: - virtual bool _on_begin(const TPP_CONNECT_INFO* info) = 0; - virtual bool _on_end() = 0; + virtual bool _on_begin(const TPP_CONNECT_INFO* info) = 0; + virtual bool _on_end() = 0; protected: - ex_wstr m_base_path; // 录像文件基础路径,例如 /usr/local/teleport/data/replay/ssh/123,数字编号是内部附加的,作为本次会话录像文件的目录名称 - ex_wstr m_base_fname; // 录像文件的文件名,不含扩展名部分,内部会以此为基础合成文件全名,并将录像文件存放在 m_base_path 指向的目录中 + ex_wstr m_base_path; // 录像文件基础路径,例如 /usr/local/teleport/data/replay/ssh/123,数字编号是内部附加的,作为本次会话录像文件的目录名称 + ex_wstr m_base_fname; // 录像文件的文件名,不含扩展名部分,内部会以此为基础合成文件全名,并将录像文件存放在 m_base_path 指向的目录中 - ex_u64 m_start_time; + ex_u64 m_start_time; - MemBuffer m_cache; + MemBuffer m_cache; }; #endif // __TS_BASE_RECORD_H__ From 0b020810b2029dad50b00e59c516f5d6dc0ffee7 Mon Sep 17 00:00:00 2001 From: Apex Liu Date: Tue, 3 Sep 2019 05:40:52 +0800 Subject: [PATCH 09/44] .temp. new rdp-player. --- client/tp-player/main.cpp | 15 + client/tp-player/mainwindow.cpp | 199 ++++++ client/tp-player/mainwindow.h | 48 ++ client/tp-player/mainwindow.ui | 36 ++ client/tp-player/record_format.h | 96 +++ client/tp-player/res/bg.png | Bin 0 -> 64835 bytes client/tp-player/res/cursor.png | Bin 0 -> 1225 bytes client/tp-player/res/tp-player.ico | Bin 0 -> 16958 bytes client/tp-player/rle.c | 974 +++++++++++++++++++++++++++++ client/tp-player/rle.h | 31 + client/tp-player/thr_play.cpp | 87 +++ client/tp-player/thr_play.h | 25 + client/tp-player/tp-player.pro | 26 + client/tp-player/tp-player.qrc | 7 + client/tp-player/tp-player.rc | 2 + client/tp-player/update_data.cpp | 30 + client/tp-player/update_data.h | 33 + resource/icon-tp-player.psd | Bin 0 -> 58282 bytes 18 files changed, 1609 insertions(+) create mode 100644 client/tp-player/main.cpp create mode 100644 client/tp-player/mainwindow.cpp create mode 100644 client/tp-player/mainwindow.h create mode 100644 client/tp-player/mainwindow.ui create mode 100644 client/tp-player/record_format.h create mode 100644 client/tp-player/res/bg.png create mode 100644 client/tp-player/res/cursor.png create mode 100644 client/tp-player/res/tp-player.ico create mode 100644 client/tp-player/rle.c create mode 100644 client/tp-player/rle.h create mode 100644 client/tp-player/thr_play.cpp create mode 100644 client/tp-player/thr_play.h create mode 100644 client/tp-player/tp-player.pro create mode 100644 client/tp-player/tp-player.qrc create mode 100644 client/tp-player/tp-player.rc create mode 100644 client/tp-player/update_data.cpp create mode 100644 client/tp-player/update_data.h create mode 100644 resource/icon-tp-player.psd diff --git a/client/tp-player/main.cpp b/client/tp-player/main.cpp new file mode 100644 index 0000000..23849ce --- /dev/null +++ b/client/tp-player/main.cpp @@ -0,0 +1,15 @@ +#include "mainwindow.h" +#include + +int main(int argc, char *argv[]) +{ +//#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) +// QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); +//#endif + + QApplication a(argc, argv); + MainWindow w; + w.show(); + + return a.exec(); +} diff --git a/client/tp-player/mainwindow.cpp b/client/tp-player/mainwindow.cpp new file mode 100644 index 0000000..2259562 --- /dev/null +++ b/client/tp-player/mainwindow.cpp @@ -0,0 +1,199 @@ +#include "mainwindow.h" +#include "ui_mainwindow.h" +#include "rle.h" + +#include +#include +#include + +bool rdpimg2QImage(QImage& out, int w, int h, int bitsPerPixel, bool isCompressed, uint8_t* dat, uint32_t len) { + switch(bitsPerPixel) { + case 15: + if(isCompressed) { + uint8_t* _dat = (uint8_t*)calloc(1, w*h*2); + if(!bitmap_decompress1(_dat, w, h, dat, len)) { + free(_dat); + return false; + } + out = QImage(_dat, w, h, QImage::Format_RGB555); + free(_dat); + } + else { + out = QImage(dat, w, h, QImage::Format_RGB555).transformed(QMatrix(1.0, 0.0, 0.0, -1.0, 0.0, 0.0)) ; + } + break; + case 16: + if(isCompressed) { + uint8_t* _dat = (uint8_t*)calloc(1, w*h*2); + if(!bitmap_decompress2(_dat, w, h, dat, len)) { + free(_dat); + return false; + } + out = QImage(_dat, w, h, QImage::Format_RGB16); + free(_dat); + } + else { + out = QImage(dat, w, h, QImage::Format_RGB16).transformed(QMatrix(1.0, 0.0, 0.0, -1.0, 0.0, 0.0)) ; + } + break; + case 24: + qDebug() << "--------NOT support 24"; + break; + case 32: + qDebug() << "--------NOT support 32"; + break; + } + + return true; +} + + +MainWindow::MainWindow(QWidget *parent) : + QMainWindow(parent), + ui(new Ui::MainWindow) +{ + m_shown = false; + m_show_bg = true; + m_bg = QImage(":/tp-player/res/bg"); + m_pt_normal = QImage(":/tp-player/res/cursor.png"); + + qDebug() << m_pt_normal.width() << "x" << m_pt_normal.height(); + + ui->setupUi(this); + + //qRegisterMetaType("update_data"); + + // frame-less window. +//#ifdef __APPLE__ +// setWindowFlags(Qt::FramelessWindowHint | Qt::MSWindowsFixedSizeDialogHint | Qt::Window); +// OSXCode::fixWin(winId()); +//#else +// setWindowFlags(Qt::FramelessWindowHint | Qt::MSWindowsFixedSizeDialogHint | windowFlags()); +//#endif //__APPLE__ + + setWindowFlags(windowFlags()&~Qt::WindowMaximizeButtonHint); // 禁止最大化按钮 + //setFixedSize(this->width(),this->height()); // 禁止拖动窗口大小 + + resize(m_bg.width(), m_bg.height()); + + connect(&m_thr_play, SIGNAL(signal_update_data(update_data*)), this, SLOT(on_update_data(update_data*))); +} + +MainWindow::~MainWindow() +{ + m_thr_play.stop(); + m_thr_play.wait(); + delete ui; +} + +void MainWindow::paintEvent(QPaintEvent *) +{ + QPainter painter(this); + + if(m_show_bg) { + //qDebug() << "draw bg."; + painter.setBrush(Qt::black); + painter.drawRect(this->rect()); + + int x = (rect().width() - m_bg.width()) / 2; + int y = (rect().height() - m_bg.height()) / 2; + painter.drawImage(x, y, m_bg); + //painter.drawPixmap(rect(), m_bg1); + } + + else { + painter.drawImage(m_img_update_x, m_img_update_y, m_img_update, 0, 0, m_img_update_w, m_img_update_h, Qt::AutoColor); + //qDebug() << "draw pt (" << m_pt.x << "," << m_pt.y << ")"; + painter.drawImage(m_pt.x, m_pt.y, m_pt_normal); + } + + + if(!m_shown) { + m_shown = true; + m_thr_play.start(); + } +} + +void MainWindow::on_update_data(update_data* dat) { + if(!dat) + return; +// qDebug() << "slot-event: " << dat->data_type(); + + if(dat->data_type() == TYPE_DATA) { + m_show_bg = false; + + if(dat->data_len() <= sizeof(TS_RECORD_PKG)) { + qDebug() << "invalid record package(1)."; + delete dat; + return; + } + + TS_RECORD_PKG* pkg = (TS_RECORD_PKG*)dat->data_buf(); + + if(pkg->type == TS_RECORD_TYPE_RDP_POINTER) { + if(dat->data_len() != sizeof(TS_RECORD_PKG) + sizeof(TS_RECORD_RDP_POINTER)) { + qDebug() << "invalid record package(2)."; + delete dat; + return; + } + + memcpy(&m_pt, dat->data_buf() + sizeof(TS_RECORD_PKG), sizeof(TS_RECORD_RDP_POINTER)); + update(); + //update(m_pt.x - 8, m_pt.y - 8, 32, 32); + } + else if(pkg->type == TS_RECORD_TYPE_RDP_IMAGE) { + if(dat->data_len() <= sizeof(TS_RECORD_PKG) + sizeof(TS_RECORD_RDP_IMAGE_INFO)) { + qDebug() << "invalid record package(3)."; + delete dat; + return; + } + + TS_RECORD_RDP_IMAGE_INFO* info = (TS_RECORD_RDP_IMAGE_INFO*)(dat->data_buf() + sizeof(TS_RECORD_PKG)); + uint8_t* img_dat = dat->data_buf() + sizeof(TS_RECORD_PKG) + sizeof(TS_RECORD_RDP_IMAGE_INFO); + uint32_t img_len = dat->data_len() - sizeof(TS_RECORD_PKG) - sizeof(TS_RECORD_RDP_IMAGE_INFO); + + rdpimg2QImage(m_img_update, info->width, info->height, info->bitsPerPixel, (info->format == TS_RDP_IMG_BMP) ? true : false, img_dat, img_len); + m_img_update_x = info->destLeft; + m_img_update_y = info->destTop; + m_img_update_w = info->destRight - info->destLeft + 1; + m_img_update_h = info->destBottom - info->destTop + 1; + + qDebug() << "img " << ((info->format == TS_RDP_IMG_BMP) ? "+" : " ") << " (" << m_img_update_x << "," << m_img_update_y << "), [" << m_img_update.width() << "x" << m_img_update.height() << "]"; + + + update(m_img_update_x, m_img_update_y, m_img_update_w, m_img_update_h); + } + + delete dat; + return; + } + + + if(dat->data_type() == TYPE_HEADER_INFO) { + if(dat->data_len() != sizeof(TS_RECORD_HEADER)) { + qDebug() << "invalid record header."; + delete dat; + return; + } + memcpy(&m_rec_hdr, dat->data_buf(), sizeof(TS_RECORD_HEADER)); + delete dat; + + qDebug() << "resize (" << m_rec_hdr.basic.width << "," << m_rec_hdr.basic.height << ")"; + if(m_rec_hdr.basic.width > 0 && m_rec_hdr.basic.height > 0) { + resize(m_rec_hdr.basic.width, m_rec_hdr.basic.height); + } + + QString title; + if (m_rec_hdr.basic.conn_port == 3389) + title.sprintf("[%s] %s@%s [Teleport-RDP录像回放]", m_rec_hdr.basic.acc_username, m_rec_hdr.basic.user_username, m_rec_hdr.basic.conn_ip); + else + title.sprintf("[%s] %s@%s:%d [Teleport-RDP录像回放]", m_rec_hdr.basic.acc_username, m_rec_hdr.basic.user_username, m_rec_hdr.basic.conn_ip, m_rec_hdr.basic.conn_port); + + setWindowTitle(title); + + return; + } + + + delete dat; +} diff --git a/client/tp-player/mainwindow.h b/client/tp-player/mainwindow.h new file mode 100644 index 0000000..cd593c0 --- /dev/null +++ b/client/tp-player/mainwindow.h @@ -0,0 +1,48 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#include "thr_play.h" +#include "update_data.h" +#include "record_format.h" + +namespace Ui { +class MainWindow; +} + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = nullptr); + ~MainWindow(); + +private: + void paintEvent(QPaintEvent *); + + +private slots: + void on_update_data(update_data*); + +private: + Ui::MainWindow *ui; + QImage m_bg; + QImage m_pt_normal; + QImage m_img_update; + //QPixmap m_bg1; + bool m_shown; + + ThreadPlay m_thr_play; + + bool m_show_bg; + TS_RECORD_HEADER m_rec_hdr; + TS_RECORD_RDP_POINTER m_pt; + + int m_img_update_x; + int m_img_update_y; + int m_img_update_w; + int m_img_update_h; +}; + +#endif // MAINWINDOW_H diff --git a/client/tp-player/mainwindow.ui b/client/tp-player/mainwindow.ui new file mode 100644 index 0000000..db843d5 --- /dev/null +++ b/client/tp-player/mainwindow.ui @@ -0,0 +1,36 @@ + + + MainWindow + + + + 0 + 0 + 500 + 360 + + + + Teleport Replayer + + + + + + 0 + 0 + 500 + 17 + + + + + + false + + + + + + + diff --git a/client/tp-player/record_format.h b/client/tp-player/record_format.h new file mode 100644 index 0000000..09a1c66 --- /dev/null +++ b/client/tp-player/record_format.h @@ -0,0 +1,96 @@ +#ifndef RECORD_FORMAT_H +#define RECORD_FORMAT_H + +#include + + +#define TYPE_HEADER_INFO 0 +#define TYPE_DATA 1 + + +#define TS_RECORD_TYPE_RDP_POINTER 0x12 // 鼠标坐标位置改变,用于绘制虚拟鼠标 +#define TS_RECORD_TYPE_RDP_IMAGE 0x13 // 服务端返回的图像,用于展示 + +#define TS_RDP_BTN_FREE 0 +#define TS_RDP_BTN_PRESSED 1 +#define TS_RDP_IMG_RAW 0 // 未压缩,原始数据(根据bitsPerPixel,多个字节对应一个点的颜色) +#define TS_RDP_IMG_BMP 1 // 压缩的BMP数据 + +#pragma pack(push,1) + +// 录像文件头(随着录像数据写入,会改变的部分) +typedef struct TS_RECORD_HEADER_INFO { + uint32_t magic; // "TPPR" 标志 TelePort Protocol Record + uint16_t ver; // 录像文件版本,目前为3 + uint32_t packages; // 总包数 + uint32_t time_ms; // 总耗时(毫秒) + //uint32_t file_size; // 数据文件大小 +}TS_RECORD_HEADER_INFO; +#define ts_record_header_info_size sizeof(TS_RECORD_HEADER_INFO) + +// 录像文件头(固定不变部分) +typedef struct TS_RECORD_HEADER_BASIC { + uint16_t protocol_type; // 协议:1=RDP, 2=SSH, 3=Telnet + uint16_t protocol_sub_type; // 子协议:100=RDP-DESKTOP, 200=SSH-SHELL, 201=SSH-SFTP, 300=Telnet + uint64_t timestamp; // 本次录像的起始时间(UTC时间戳) + uint16_t width; // 初始屏幕尺寸:宽 + uint16_t height; // 初始屏幕尺寸:高 + char user_username[64]; // teleport账号 + char acc_username[64]; // 远程主机用户名 + + char host_ip[40]; // 远程主机IP + char conn_ip[40]; // 远程主机IP + uint16_t conn_port; // 远程主机端口 + + char client_ip[40]; // 客户端IP + +// // RDP专有 +// uint8_t rdp_security; // 0 = RDP, 1 = TLS + +// uint8_t _reserve[512 - 2 - 2 - 8 - 2 - 2 - 64 - 64 - 40 - 40 - 2 - 40 - 1 - ts_record_header_info_size]; + uint8_t _reserve[512 - 2 - 2 - 8 - 2 - 2 - 64 - 64 - 40 - 40 - 2 - 40 - ts_record_header_info_size]; +}TS_RECORD_HEADER_BASIC; +#define ts_record_header_basic_size sizeof(TS_RECORD_HEADER_BASIC) + +typedef struct TS_RECORD_HEADER { + TS_RECORD_HEADER_INFO info; + TS_RECORD_HEADER_BASIC basic; +}TS_RECORD_HEADER; + +// header部分(header-info + header-basic) = 512B +#define ts_record_header_size sizeof(TS_RECORD_HEADER) + +// 一个数据包的头 +typedef struct TS_RECORD_PKG { + uint8_t type; // 包的数据类型 + uint32_t size; // 这个包的总大小(不含包头) + uint32_t time_ms; // 这个包距起始时间的时间差(毫秒,意味着一个连接不能持续超过49天) + uint8_t _reserve[3]; // 保留 +}TS_RECORD_PKG; + + +typedef struct TS_RECORD_RDP_POINTER { + uint16_t x; + uint16_t y; + uint8_t button; + uint8_t pressed; +}TS_RECORD_RDP_POINTER; + +// RDP图像更新 +typedef struct TS_RECORD_RDP_IMAGE_INFO { + uint16_t destLeft; + uint16_t destTop; + uint16_t destRight; + uint16_t destBottom; + uint16_t width; + uint16_t height; + uint16_t bitsPerPixel; + uint8_t format; + uint8_t _reserved; +}TS_RECORD_RDP_IMAGE_INFO; + + +#pragma pack(pop) + + +#endif // RECORD_FORMAT_H diff --git a/client/tp-player/res/bg.png b/client/tp-player/res/bg.png new file mode 100644 index 0000000000000000000000000000000000000000..e35d6711e2651f8378e29ecdd7cafb6d4fd40a8f GIT binary patch literal 64835 zcmeFYWl&sEv^5wA!6mp$Ah^3jaCdi?#-(u!!GZ^OcXt|hcX!v|?lzrg-~5<2|EFrG zrn>vqv3t(iYwvw~uTVvK2_$$t_)nicAxTM!Du4O}aSQ&pgoOlOVfdQ44}QZ1ifIB> z?9G90Mowm*giP&?%}AtdjV#QR&5TSv9EZ*LK7IO33Q*MqYRbv-nAqDg82xL*;BM;x z?)~W#pP;*gk%_e#ki^)`0$|5adePcRN&+zDC)Hq=W0G?aF|!0ndODe@c*?7qcv_oq zo019&knp+lfCsQO0~(RI+uGPU^SJYq{>QjH;LraqGm?`0M-!kmKk0uiN>fgeM8w|7 zjD($mmEMGjnVE!xgMpcior8m!j)aAYnT3%F{NkWzX5(S!;9=n;`L91x@N7<|<~+)x z;{P=l_!~c|B@pPq!^r67=EmU0%3$wg!N|&=|E%}_abRav4+k?wWiw}c7bg?&!I_i&o63Pl#L3JEXz!$IZ*TM8 zyQpYs543l-w09s8QQ;t=(g4_*+PgVZ|3`Z{IUXrHXP}XtiJ6orKPh+w1^~d6hm%`a zSd5FCRg9BOoS9jSQ%F>lnOj&yjEz}Dl!=`~?7#bp+MBr8n%M#WyRYg0>C5)N_WcJ1 zTL7E!C(Fd_-1xsv^#-eJyjb~`03LVNJ>;l)qUl(9WI$*-i1RKlvsIuew>mi z*Bbae@T)K#G%g$yj?PaA6RbfT!~ywqZ?5-UuILAM8A-|4FE2NCl=9WYRow$$@tJTC z(Tr<)NFl`0gz+J5Eye5ZWbM`TRzEhp%37_lDkLHEZUk~mjw&lFTU<`~FSsApoH2ub zkbH%Og7p9Y-~K3+2yJnWiVqfqD-`lX+}c=D3{?#?rZ+7s|CrZu!r++1TohdV?d=IP6z$ za}CPG=Uo4z^CwMA2JHc!|iQTB|`T^y{fc^Gk0rV9$AJwWoLuy z*$QQ*yYXlu{oK$0>>32*aNB6M*Q&QfwIo#SFdt7nTx_3eZ7pI_g@47{YeVeB3Fh{B zR$cn}1p!MonJMS$&Z^;gN(7}sv&PuZ*ZMsaL0iYkWOK8|WbpoMb+ALw=lK>eTNvr- zyx!vCKsa!|wz2~M!p5en`|!HrlNq|^AsB6wKU0+Mooj@1tuT4RVY|Y1o@p{kJf1MR z@9E{0<#CYG?)RmL&1L0Q{AEk{{v0O*d}+6%v#5-u%|Ay4{j0d9SK(MH`%7*(1EjyW z5^&S!!FH_)^ z)4y0JG=bYe*uRpxSEBjvaooL!W64>~$9~S3+SF&c^}yXs@SKz%X894366zvC`>Q3h zTVq{lw!Alj+Ua3`?T;p1ykRd2bjrsz&D9}wM@6=Kmw`YR+aYNZ=jTVg!-TOvKKzMr zRl^r&6$A;1#>6g~91f$-ymFOJNmAaXC5X?Cj<_${KrSvW`i}jWpf#t__jI)L{$-2K zF9yxWrDb+c-O&T{7flFKiGJX9!Uz2#6}FlKg|5L#RG4c_a_x>M>AB+KJaxkmLp3vM z*TWJ(#B3}sf_&ee-{0SVWmm}Z@OxaF{&CmPz^>=RIJp`mMu}RjH5<)v+kiWcktP_} zs<8Jtsnlz|TyKB16FHX85+K||Hn(S*)8A^DXli+%-~ACy=QeP1x9In2%xeWubT*vn zpGy8wsoek%y_*DDe+Lo2!7_M&v36hiuur{ByXtn@qd_;CDqwrvl!uo$M96u?)PS~q zhmw%`lf?1)sK|d=XdkrcI0$A^fyZtX2K_eon}hL>n-c>@y_Pwbb&s2aj84;GZhH?I zFRvHy&b_T)_-)6SN+D5nx& zFB0d^q>J_k{om*P;aEZuB1ODWtBZ@#50l)7>CxVMWJw116PVG0p>O1!tBpRxF^sMisfkcgr7!hlgi^|3UW;D%1gORT~QzSGDybEy;Q7d5g87;a2^u{7X2B zey8)`{mXeP3NT<-HiczBUPWF>?W`nW66of}8TE0%*||Jox_LRCJb6g&6J50?HZWT3 z;H{v5qVL?i=KCHPBdw6e5y{->a-x0=`iFdh>c@znI0`w>?V!6!z8l13{zHcW60YyL9uyq}}RTL`LsvYC_FglD2S@kQz z7&)S%113{F#&Y-%7Zw&+Pm=w1e)R(P_{p;(zRZCL=+;}#s{5K!4DYQgJET!X#=WGf zzkU5~`Qwi2iwVtKy+N57%Yx3vHJ0RE0&N!0dDCv!A`1WQ)Y|UBk6Y~q`2j{ z+9)@xd({~Y+Sl29ns9r+ZZx3RX*{hRW%!&X__!5tl4-zu@z@-@OQCS;c~o!>raYfb z`!UN5zn-=?De*PGA7>oNZi1|YC_6*a#=o5ux+bPhStKm8ESwzj*Or$(=Djv)LrSjD zJDYDnAvEm2f9%P-n=rTbbe(3LJ)Nz(r8`f2IRZWxB75HJ{e^plMMiA>Q$k>S`U~V zOJS{H*2GC=F@t}Hatr9oup-BmDu#=|f+f92gA#hXbSx@WPlHrZFY*vEZv6>jG*5b> zF9fiU)2W!<{d&9Qb0Ev~Jf;qsBurCiPX_Nl4*jY>H)|2A+S%Dz8nX#B8++il!23Wr zbZBVk%Mvp!5fPCsOIYVWwS~Bm7hpF_d?B047V1}^9>=#aI~%~6^NbH|h%t&D?`bTn zp-~hgJ-Yho;~?22DyGL<5QQ|()U<=o|KB{_7e~N@rshGBM%IC?qkvx_!H;Xels_;f zM8QT95)w6Ll&%Y`k15vUUO5QKo;vo2I82zq{c ztD1oK3y*t&O3jg8f&p7%ao8}En3~08a%r)6vnnL&NejgeQ=viG*+#opPavF1k-X1# zkjo&k;6sF1_(k(!W}H0F(e13fvmVSb+8XevAjvA#^`rtc0lvwNwp~XkN%z$WHX6cT zw=dA}N*LOZk(&%jqapXkg>q^4p$gu)SBJZh626C@t=VD@af9`^bpcHAfx9n4r@O3}dr@c36WkVthKBuN zn9k8@&R`nlTb)c0&p*6#VE{e;;l`Q(-Lb-l9rADh{a7Fj;${xC#MU|O*l zPjxwKSY!aR;&w=qMV*$~Y;V_~UG@uYWs|V-eK=!>`yink(6w=waQM^+N8tHy9*f8; zm`4;?cp0ZVOt9rjzkmI`1{Xr87iUg2B3}=R(pa9qWbiEB>V9j2~dU z69}ADzI-cM1_z-_6}CP;Z(yG-a5txNH4#fLQ#yYb>6v%H*|OsaN+O(6~A( zRV&+jMnS-4XuIq|z;8Zlb-U=enc&^;+KMH&4=9oJwSGHD(!@Y(Mu!zW5A22geHDt% zL#c3@&Tev@I{!AB#F)iwBJSnId;0=rZup@0K9w>K+vn+9KpdE}>5o=j)(Lst?0m1t zH*YBAPTTHRtPjW27OCJln3xL3R)XNJ{Tx_1VY|*B@b(5~ zf8E6tZfA-i!V54OV4a&;j;SfLZVOcuw15L?{{m+f1@3~1iV8>3%?FJNZI{CgxBG_& zTH|eyb!D^pPChkrbQzxL^R*d@%}Oo*!Bw$qpBL{_)RUYF+VBR_Qe_ zqvrA0R3K+p*DN{F>}teqr0!Ft^v4#jmxauAA9PA!q3q3dIOF5{{kc`MUw_zONO(WF zy`NVeM-_qD`Yb9Z7$nC>MT?dC^!pFDN3)Ebuh&v>MNaZbumS&ed{}LgCMSE2D!wgG zlr9X!)hU5G`7Usn?wm-k!{K(mwl|ag*it?iYsz!P$p?lZlh|-HIQ)pJp$zR6c-o8O zI{5Vk0q+jQ;UF(j1ab@mhU)#Xxs8V37t&yP*2sX|^iEE9)NzpTaKY;FNJXA|A54t+ z4}DwE+ppFQsj`CopGonlC6VAJ#L>LuxI%(F8OpIT=bxJ|CRh4{)1-PfQdQ$4*m+O8JLX0$l%kGQ}^ zVAgSQw)u@Q5B}^qn9TAE(Hz3>YSeY{oSR6Ig@%EM+nL#l2S=5=lik9O@qsY*neIEG zDuuFIt>$tVVqB4euiGf#e757kt#dPcNzn4=h!2E_>h96SNv}fheV38veu9QL^)1*w z?N{mng@Bzs$N45#-8=Ax50qZ7^YerKI2W@1m6$ty=3#dr!5 z1Af7F$@^f36ST+jXx%MESs?eeuzKz~^49M2hQfDAGG`|t0pku;l|o0XSfM@=5D>uY zdiwVQ99;$TGnyw>Rx(si4i}}D;+h96H1ljUy>oUxHC&u&Xnyj!P>}8WbiPTc^u2@2xMn~o=)KP|f!bbQIVc{j|bV@GWK!4v; zCEAX}`2$fGex)!FSi3+!P#!1Gdwx91V9n%YZ$D`gE%1C=H~FPTeLwNXY{URmfw--t z$5LhGM3BAyKQnD~u8``EO2S^?_XATA5)x9l=SvNf}vmbj#$l z-;kor03PZ+{Pd6r3Kg$m8j`Q1bO9%gJ7l<1OERBeNFTuH?7iRV56^NZ)mJQOEwUFx z=sX>siC|Z9vS@>&t#w)Dz)f5Rr)8<^*5W}qdv}?xD*%$e&CZ8AG!C)VA`RNMHe>Ju zHV0!V*T>b78t|ikoG9D;oAq!mxJKxyH0Qr;dzNKsaTb{La9c29*<0!9p;xtCOJ7n@ zL5pgYEO=MJ3!|h4WS80{|K|gj!HGBQ@F!aI*Kd0#WV-6B|pu z6i6PEN?7#|l=$W*5w)RvLYH8F zQ`!Hn*fG_%Wcm9!GxLq~;VpV1z@vMb#9E)GxlD!uK-&A)WPAf`+757v&rY!r9pMLf z8WFp*16)uX`Afvs^#2@*Ai$oEI>`glIBr>OUHWhz21W2bJqHku-O2r)T1rf*$dClG zUF`rTYlZ|GQ+5;?cJ}sJYyh+-_6+$&KMSb3hr2ue=b>x0vP`?zXa7Q;U$sWsf!~Zs zJpV2$fuBsS)_J+<=&X`OS8BFd=0pfel5U`bC}vbo>%ic`~*BCk%y7hg-Eg{Uwog#++t@b%)H0GMx$K z%}$k%YYawFYZ!93<1T z`BLTT3-X@ycn)jZF#Q27_dP*?A5P-oM!{WVW6iTGgVs%L zk$tqb7M0ao5Voz`cJ!e>@sAE4O4m+Jn{UGArWH=wXE~MMJY_VMHZBP!M(_=%( zTusDK7$95(s;~c!Z5gy#4Lq6wd!3&R@dZdWOHGdB$MeH(6M`S!j}Tyk!fp6VTVL$} zE4q8>w<1N^L!SK6w@dq@mL(oS$Y(~1vRka_rqT@@VNK zArF-cms>duA%)kC;k~aRU|)jTP4@zf)yrw3%A|jpB-#2h^Pb%*y&9)+%t?#y$WBlf zC)58!r93}d$rGC<5UHFoI{{_I#HM+%OQ|r zAviKO+qwse{npddwKqtT2$#-#NW~0~Ng1$NCe9}FZ!WIlpJT92gMzIUDzo%@Dz87_ zUZ+ZsPh{OnzX;4z-MXj^gI4~fpXFM0Bs6lfb1r%mV{_SKdR0tpZPtg4^s8U%?x>1@{taB$DVyy~^)#U38|C@0c3|W!$JRquCHKs=CHF3cVXQIOj~swH8+~_>D?SbBOOOEYTo_UJ5!?`Lgql9#C`Zl0 zVFcQ+DEVYsyE5?_aDB%(l>LSa3HJ0^!h1r0T@sjasWeXdu^XQIR5M^QFni@!{Uzrv zGe$JSr^=6l1claPVAmv4?edX&%en204;2ILA3GNdUVf>llGpVfZKV<>(h>09%XCXY zQa`+D$Qtiw{J#Dgl`d?{l!}%FXJQq%{H0mOTmkopN|7xNB{Z>3+L_AbR!NI)DJ6Kd z*b2ZKSLSX;z54X7bn6)f_$|q!!bUiTT!uwu{K#1vO+{t067hp%zk?VBiyKNJby+Je zN^5L)#~^kYGm@2emTeQIwnZI}e3jdvj-o=Qo53qIal+ICZaDRX(u8XowrKlqo+|e` zr4oEqEbx87_bAA#FL&9Ep;`r1tg`Ap?w5s01%Qt)YXqMjCt1V;OAg zNlQ)*aJmp&BI@rol;`eU>yz)XY24 znR}TUQqhmK)@yJ5{hw9G#G${d93yZ0~4ydkPp;D8T35C6p z*=-|o|3q9Sx<-iTYe4Nr|GoCG`e6IMY_^3V+l0$`%wts3i+qif!Riz;;Q_}JrQLK zPqYf=i!SEen3R{+sfEWarK`A`cWJnc!#Q&?V+@`l0O5Ka9!7Qsx41E)VA<~(1l;EK z)y<_G)Tu#NBoO+zS(Y@1dekf;;;f`8(ZmaAuD^`~1M|MDeDgxSQoOpXPKnfo$J_#a9iKxKmyHb+rCtG4^n|M@J)~lN-k=>#VF03s!*D5^mbQK_`F#@>% z*^E+>V%hPIHQU6TXF^(KsUW$fwIjF2$(*c{%bhAa*)Nvr4k?L5!yau%=R#kT0KCrT zFV4iDG1uX@B*Hf-J|uKu7gCW<%aiQ;N*0HO{M7nR&ddB#yr8_h-Ki-jzDv6RRaN)= zk=G7m^vDt2%SJ&(-H~URN?csYYO_+ov|sQpB(|W6JLqdU%&a+Ux$W=iU7%S@KU$LL z@ms`~Ikv`wohM?M;@Wt(CJ=QoZA}VGS`m}kyvrC+b=>iH*_RJDVNDz|<^|$;Oo@dg zE*Wk)89^k~r~FfNGA!C@omuJtnn9?zsNq0?%aB^(NlSr$z#EBet@1LQ_o7wW-uWiXG7QCIhpxbh6C4UeYF^JcvGpwUdiNPOWFL0T$h?0D>@?9 zuYT8;va>bY6!5&4Vg??1Ot?u3Jh*e;;3TBHw-hCnYTljrLHrEdJ!t)3;G|g-BxiOW zn?=aPk+s_76@Se>NS`Qu{TcJQYUd?q!g<+5c#eLw@k}@;`cQzw(kI@Xc=)ssb`d5= zg@IRVwNmw&JLOqk_WZ>pU7x=>+*;uoy~T`H8#yhw)Z^PL%Hjjf8gEHRvava51e2=P zUNF0X>O_vbB=|&_R`{bJZ#*Jm7y6ZL`%X=I)zeG33t+rf=}{MVSbyNx$zm3869PoP zQj~?-H}7xTaM0TZUnJf}6cqbsjyNrVvX@`D;G#xP$tr>>i|A#NIu#js3eBivh&}a% z8yCkXx6vhPCSx^p0l3ljhrsmkYYmYd~w3QZ=vXXavWoj)A_O|@_ zh3%&_iIX3DeE~CvlVM^JMo1nyd30?^xjs-ML9UNpA16|`FDeo}utZ|MKb%8dT}J+p z*)d^~9TPNlP0lYZOb_wo^17dDI!O!!q9bdU&wFDE8CO0j^B(%pPEf}HQM{2B}N@Z z-%*eyB12LcCom6lrOXSFe7g8fH;U>?lrZ>lGAq*%n|*3NzPJ`WQY$=)eV4sPyQ(P2 z!K-BCl&rAC`-;TwyXZzsA-wKThB?DM;lwE|0-1O6ylWy4mz ze6DlLjbuPDCq+a*SO$>Q~}*$s_-ZBiRQLjTqi=m%LmO#a}D#chIm<%I7af z0?`R)S8;-kzX$Pk3TaJ>fPhjgmUf~dWUV;7G?QRQLp@(68v5@H`^J9xc5$)6AD0osxsiih~|)JW)%TZzr0nD8lIzbn_Xqt{c{n~CVcc- zQZMxpM`^GLijEyX!JTo})^;}}R}L=4ZBfP1kq>S@v71AO(p(Pij0N9IFR#Lly^nO% zu@&x0c|!f4_n1jZJxLz|PRn)^d-U=x!dD?pX!9E_}=9BHdc=eWw4uvbGa%Y2QfKP+*V@k^W zkoveZIlPJuVd63suecmmL zU3!>vi?^Z6)^-#H35&GYmA(4r=~FQM@3!m#HJ?3v?UAH$Dt^S~M9KU%`8A0|T?q2- zUL^u_nKHbpTN0U1LRhdg(LjE30!#(E%G_%teZijM`BGF}bm9%E&?2TQ^3Ohr4n=@$ z|LnF>#U@yI&d?Geh!{Ur(DzKW@joZ;ftk93JXU#-y>8X?6|S zmkAWYdYs;hZP7?}Do>D;iff6XoO(s@`a=h{qqQDXg__EHJJQqc0B#rUq+k45AP)dU zkOcR02*PW4&w6#a?!X*0{2LkeguIoJ8pD1!+OhlOY%OvGOkUtsoNN)!(d%AGs?o3y z_-j^jhyIcE?IEG4MWAFB`)@+q!W*KVnbes5&s5atMJdu2 zR`X$PsDF7Y4<&@YipBu`UXCKp2Er*6v0knl-h#tyJtQ&WF8M9bO-3<0>afz-eKKh@ z(l&UfBsBpvz!3A7M^d=!V(WpHjfj*C1XD>GrTM_tSy?&NALd>uQQzlz@_C4MBi4J| zhErRNA8BUie@7F5*>3rYaIle8M5Mrm;XG3_LqV-`!y$%-GjH#yT- zys{x-Qr5{wio9UP&?(EC;X*BZe?+lP6hh-%seQ{Ue(v~gQN(SrK!uBDQjc}q2AQsS zuGFr5Au?e{i>=x(btkFx3-!$tceKd41PMh_wSpW&RcD@dDFy8M5KDp+~)R8<=NU6+LO@v6O8{mN<{qG%1< zLv4CrysrqHp~GBPKsQq+cx^?4X2$W`AWp~%NDMo2@gMnb+ma399r|MPbJqt_V*2XK z0Z&Ho^_7-ki8T@R!}Fd)d|$OH@E&UfO|RI1WmAiTnEaLoi*;HESy=FL>x8Uczh-k5 zb`8V3DcMR&)zML{{)!Trx=9bi4g5J)Y@Fm38hW0c#G{&+ z+(yWG(@I-nY%NCnA=eI731U6{9JiBR(ewO|VZxAz-G4wY7$4jL7X34-kKE|t^{Sh{~TUpTp z@vIBmII$*%r};vx`XqSVz=c+t`lxHJO~0StGG0ni?QiiK*6voQF?#p_uoj^mHLC5j>GuimsI3sP1siQplu^K zh(0nNvhnSqEzAiH+x_OZnINaC+nc&rXeowq$M43IWCH**n~aYfZSfK1dT$EaYma#C zE)}CU=}-$|GZtvk7PPBr!?+OL5#uGk>(A@8vOw51>VCE<6loE$A~u|6hoST(Zi0{4 zEh(L&h>WxJcD4WDll1nlYQ5t-g>l zPaPW>p`{_^yj4T@a(_?u-fiFX-S+}<8X^z!ED*aMBSi0hU@B3_?DnjV7U8c9#>Vbh zILyzD-6eNO50rl79TonCX72PUkJV%+lWdGM-KQf)xe8i;V?q;h9<`-&kB<9I-A#gX zs3S;;-S_&Z(>)2qaX0Y@2U<5xJvtrr9Ej`^Xq{&;h}3unfQy#pE2w;ubfl0N{by|UCNdLnP6aBD@yE#*t_J0ST-N4 zQW3vGF)`N5w>YBTFyuZv9Vb2V*430DmM5HBR}jq0|x-s0uG*qbzA`Ft>)F#eCUQJjNM8!dbkIQD)4=k5sA( znIksNyYp&?{wzN~GEfUnw2T2uZo%#k)*SEOaN(9FWmGWd*~a)}+0#pb!4FWH`o#{L z6``N!E(NLr$=VP(a_3T`fk@?t#cH&?Z46Ge!*k=d$`l(;KhH_;Vk zzfjdA6=xTwX2hBAjK6mflD-p7ZP8Fae2%9LmEBv+>kf;WVjNzR00)SF1#mckC+x1K zD?RSN)~neP7d+66rBzc`=wPMBAcJpB`gP`CG{XcZf8_{>7t9C2`f2N!_>H`!KT=Y6OM4 z0uKgfX>v*@S&>EeNa89@diF?(SO|&tN~#GnG5?Flb4~N4!u{Xz;ud+4;(7V4E9pyS z$*u&(${3SXS4Sn0$%wpxn4#!eL3fCEp6>{g3NY-np3rB(AYDlpel&9K-t2>}*B|+< z^4vT7#IpJagCkBE=kYqLl!(~RwEOU^w(ApzbtTHpn#b#?PYmHnpVwxcRf)R+JQPgp z)Jo080)ELQmH1mpK(aEbfLwRQYyy25S@+K|FOPZ(g;s6IJkbP~kx@fam6{I{H#^$N zF2}Norhkl}^xxnWnksatSEa84Z#O#jkjWY);)!u;utK&lm?_aOPH2e9#|wcZO*{NrMJYm_}FN?8Akp?soV=a$px>mOL z%^G4z{aqXWSz5*Q2)ml#q^5M1h@~@mIRhVbPDqN3{(C8$#`H;`Tt(FlPqr07UBpOS z@x*PUmnEEHh7{+H>ys!PE7(P1Sor$r_$l#7Cr0wG@#~E3g~i8DU?ve$ILU~AS!O4* z#=~N5?jRys!<0D+mRbr}$C3K`sz^O=YGt^h&X6$>jXPEqpB{VomydR4CV6*3TE5|w z6Iw<@kfNC9YM7Nk5A`==d!Gl)CSizlU!O^iPV`uYMY$u4L`7d8+q&#-zhg%FbZ!dG z>@V?^*0!y_lN=ZF$C68uhy+f0fy9$$hFMa*k_7lDfK}zmadixC#*Mp+W>gYSf)Sb7 z4&f_6+j}AgkdhF#$dj`!y{g|l*KSgWaXR8Ve+qC%lT~sYN{mlHAiJfQWYeytFXxLu z@zD<+svi~cam41&B^wJAG;*?!a11K1Nl){>{I$g%IaUpgDMqJ+Xh@*;Xhl{P>0YKg zC#S?Nh`2+gmhUKzr{%8)ve92DoxkTVvVV{l_`0&SP-C_z9;n;&JOh~ zc1ot7hD+3Bi)U6<7c_7Z*gxtRfz%l>{4F%8v3QeL0NGHhZAfUmYIFNpvrO`ZD>i(- zMqGsQGs~~#>9AnCmrDEN#6M?YyK{H|K_%yJF(Md#UvX1Nb@+%_`YZY(VM$@mG|I5p z=&!AP`-%dtAT44C_cin~>nmxEgDdPzAu~ZeA!?t^fwE^kKJ@qJG*^NBic$+?I7PEj zu#jQfbCRchgX#qK$tXeYnfQN~ZK42}g*eN)`f9kRBgrGHP~aa$u!hL?6S1hPZ1ru8 z=cx1v$oTRb?3OHl9VQU(a_)ZmUD_8Ee*$4VYSqgcby%%Q-TYOBk4zsQbtpCu+eLU0 zgCE|bOBq&PdhA$CH)x^MwvT}NPA{Fo)_lxa zpJQBbh6T>KlGN} zeUdVl{=J)Z-=&(g=8t{b0$LnxB9Rk*&z>R;(Wc^buOeC*XHT@!-m?%l-*$?uYcXHn zD4W&5->&dNDdCW>pZmoxYxpVC)NIleIFp_n&MGBG9Dy zI2*V>ts34JpDPWEws6(iK>^HX)hiC1wkKc3%d>7;b%Sqz7tFFBFpz5~B$UPfT=pnl zukz66NKj8sk}4uB>Y;QX&xrl|ryR9ERRY}+hj;Txl~6ysfW5&o@84h*$B6nw z7Ml)+u$a5{+LIOzM$yxrjM({Pk1@ir%A)FU+tthypj#(F(7tE#$)^cWCK(yLwy0Y2 zy|+Y1mxMvz-_{s_y^L7QX6uPr)trpfx#)S-uU+B(iIj%z9}K;&#gN2R+Kn<#*g-T| zO+&rpSGh>1iu?Y*RuB! z8GQwQ>MoQ#L~ObRbD;+SRlu!8*ClrGFhVjKbw0ZJZU^E0i}d(@L(~(WHf;cCG@uuWxS+<}4TBJ9^CxoY zz!67B#swB)QbeP&*W|>vE5PjUmH5b>cnCFL9*r&=G27<}pE)a6>4Cj^;1hT2O+7Fl2XE=bK_S0IQ|B0mI>m<%Md&W$QBez(GB{O}ecCABaai%==ymxy=5K#05UjtwLYPDjk9wB8q|lA3#PA){VRGeMl*SpCkk z6>I}Vma38%8@IdDazAxiqF$f8tF989Fj1&CX~74r9QkUcy9M&7fnjx&l3o6dwm7oO zbZG%BV=u5~C%!HiKp;zcTv6-bJ%w9lKrGX7;qUOKmEle>qBYS@!IC2EmRj>l-Yq%{`zC^NNptgOb;#a@=}-{knFT7#nBe5tl%!$L5m3yhtUv0bSykHs*0 zl$hhV#`@7x79v};VDY$?M3Ai}{LPlE z+m0rOmT&f-(C#=as)J^P!ls_u*w2CBu2c1%y!*_$8B3c)L!z#fu@oN~z2zC#Y7|U- z<;m=xXOE3iTSl727W;PC1p}?QPF+&Rfv%;kHd#e6MprBtpTmh2BzgvR=lw2tjI0>| zHPv28QS&*^uUqW94_xPrY=p$~JF^bJ-{r+x(20bI=#c~(eY;0>S6Dgo&5A0bR?a0X z@rXT_JQey{bz@PXwtqrIz6{>ba&@Ne=HE_DfF%44%6S~^vi>|#3#`Ud@`UX$VaDvC zr34o^kB^rjJ`+h2(ky^xU0W*h*y6`hqzb(dmNV1)CPjs6bg|lw=4kGg+R`VUf#rT*JlCP8gJ`<>9GF_El5t(1EL>h#3%C(YnMyy5w+9w-- zgoIZYm%@dXDbX%HP}k&J|L#8CdqIbbuYr_a4o{hiYfRl*>;dQXHi(1oBiRAv%zvJL z6lTs|^t46R&C+%d3J`LzU@6H_mYQjqP)?1VabZuewFNYc+Z2}76==H9s!g&)#@m_N zB3N+g5;17d*0wOJsJQaP)v{<~_@7!Qq=rWu3b7}$sAjImHk|76ET)b(95^-8xz}*9 zA!_tYEGJks)cml16E8M7@c1R}NQ%p9f)ygeKDn>aC;IZEC&UQD(@EZ(KbAbcp_F+e z(;Ep1dKJy1&Vq=Qi)*YaG}dA#C-SZ4B!ZodH~dCgLOq%}ZS0(vn$-l0KWLtGRJ$ycMo`NP?|}r#7bts8RV+bY1uYIb=}T>Kh|{cB7jt&@iUC z0CibYfDsNWSeqh^S$X->J@lDz1D-m~;t1<_TC&(D=<;SZiyQ!o#cV3e-M~Ka6&jYB=ACcqmY9pZgHNdkwirEutpDE?7!AAiA`A zw>IL?e5x8wuJ+|K9>na*YVD;=Fmy1hE1rf<88=HLbBqn8hO1ovHW|?|^UV_l|EDAE(EMR|58G9XP-7Zd~gb zJ!A7)9e1OhsAWfOtX$<3R&D%3B`Lp|B)Zq|&w3YAB$!E>Pi{~K7#cRIFrBIB9ZvA_b1c_9yQk}?-~_92OsnZu6@26p{>$w_q=<3 zy2S2^oBlQ~I^uVyURYHNa3=}d6 zwbBR=p}lUbAiTARHoFyv*>quplBsdtOx;_CiAQdGq7O# zwRJqz$m@YMuIMM*;SbYm2wF)rx=Ijuw0{me|rlX!FHqV2wVv{__CHD&} z8CB5}qs{Rigso470pC)&>9*+ut{qP2bvq9jB|ckk*hLc2{OjWPe&`n!Y4Gv92-@&G z$#i|WiuSWvy?^Lw_{3SNlaK9!EQQDOGzbQxT634=k z!yKqZM2+|5*<7-E7fq;-KfR&WCs>Kgj<}L#9;U!HUIHJ zwfARa7zSp2N2rT{E-rFu9B2DU2Jdr40(X6-cYmBoA`sKPMJ5k*_%=0itjLNKakgfq z$xX!lQ^p(iZkWS{@HuR{SiQlbB-sp}ySl7a zN5gEvxJf+Y9QBMNlNCc#Ojb<;x#7d>j$NpnS;^Xns>+(xrg{ePe6lKUaJ{(jw_iiq zUme@|@Iy7Q%24ttbDeg@Mol~Nct-YyT9||FyoBT&tz(zj%Np1%G_2kgy8(cj7Z@1# ze%C5K;fiR%Kwf#n-|XbArN{Ioz>Rt*cEQVDsNpc)ub$!Lb+%vCcV3Usc7D8FT#Xs{+|4WSJsqifLccz2Z2anU zE&}970Fj;2i>9dVRz9a19iDP@8}tj?*KydJ{EFp++4R?ZAJOQDl`dGQ(0S-XSAY^? z0*g1%<++SD)=W6;aBs^pW9GlD>d>)DUN@B9VB+`179n;Z5MT(*&(_Wy^=xI6!PuWX zU4wtsyGcG?|9r2oD@6H|5;7qK^!bv=p@u8+{{H@nlA%j$8)!8De$x4VVj=i~>xv^) zJB~>yUe1-AOkP&x6r2ceU8(8Mv2glV8&lzmG|}Gr=wkUe!IRPFP^=3q_FzdFTWZ}{ zUVgglMfn(f=CcVrF9-19Z8)xf~8qtFn|sJmF&Lx;P~OaSCF7!Zvy#%_AW-e zFsz1s{4|-XnNM}3aYf|Kr|n>%Sn}^Ty1jaZJl5my5A|4R7U&(2?7BW3P=p|-mZIn2 z)YvP#6!CNSC^SaK`H0c*gsQjYI`fVTp9`>>?_|oq>j*5N61LdEON?ve0nB_?9HSR} zCKjA84I!#B=HmNfJKW{oH^!jKB>xF(dcq7LC`Z}ILDVOCAo_Wt95+tzDpWz&WnS&8 z@ncR=mM>4z=>CnzSAKzylW1?%hL%FPlzhvPdyN*#hK38GB}}f~w&i{%xGU*gHqH_ZjJy?Gh*>4~Y_{K}=7KBx$SC+u)CJ@(~TqodYj z5Sh;gTxiax`LE>n5iQwnwb3>~dLKRObxyLsFTMuT1aHEhik>7y;R;>oYmb%F(YIO@ z8Du+&3fyz7M=?@2r!2pA!#N$fe2GVn!KSPp1R`29G`1 zTSXIToG8%xe2fB>s1@JZe>^fPj%N-<4y17re?w>zVXYKe@$-J&C5|S|L=NN_G;-YB zpM5w|@Z%W<>u^EquB$!|>zs}|qCl4>>I*%znKd1m%5}o|YxMe` zPGJX32bf-9i(3*VtYu-+evYUn&IE}g&~}u~<#tUs{L%1)yfVM$iy??OaGVK!8ln|! zw<32Z;K$)V1uKKUq;1(&<3AIy@%F8M1{c0R?nb9Q!O3?k-A=Kn)+H$DM$B$}iIg^< zG%N4kcvmi&Qau@#xgH^YN$Z5J+uhdc@czFzx(2q&8ZDe{H+d%8c9ZRz>@VB4n{3Zy z+qON~wynFnf1u|)r?dB3>!rItHtE%uZRbdnmrjAQ>usMQt%s=EVxj~qvi0(B^hwI+ zc3m_`90KYV=C6aBF^0=@#_Ph*SJh`h?(YQr-x@+{!}^X&siablF^5=~)#xqNEcTW3 zh^BI}v20{N)wHR`apVx9{miT$FV;B^2BYz=BW$jEkhK|#L)lW^qR`32Sv-FX6SA~j z-Y}s}Fjmytm~<;NoNSp>jJG}?5KX4D!S~mgPJDwg;B9(xt_Sz|h7qDSblqsR{5{Kc zEe!bo-ChsnD5NV}El31Q*^VsH<(1-Lb=@aha~1T=D)@HHM$W$89oi@8-e#YBv`tsW&ROsX~oR{sl)2env z+bYrA+u#Eno!8NW=Gs2bCQr^(+f^M8D}&`UE={#XyL#M4a9{2XDu4FweMqS!j~>+8|FWnm{wxx&K*6#hwNso4hU z)+Vd61M|4rK05t9ma!w@*Q?xh$QXmnJZ^dmcj?&KtgeJk(Aj%`CHjAVjCH+@0TSgO zrlr5qYoZFFBIUT$ZobiivgZ=%xormn&N4L*G`HZ%fg*UhOyB!9hPGXfN2+69PT|Ve znLSsb({C(p*=48m-vu(7WRw{&S`*+wBNEuQO8_4zv=rQ8lR+QU6FRY;9WDgzB+9uO zvfW~ujxk?MF^vLpEk&q2S)JJ}L5K-9y!WNsDb;@#ZjZAL-9)`T*XU1*R(=Ee+k$sn z%(yr`&w>kvy9}Q<4E7`5*AbM_16MccNwAif$leRH(t!>Qvy8f*#hNCS*(2!ZLcJ{a z*A!|yIsd_dKZMt?=1AlfY;gVW5s?1ZpcR{8x8@Vx0X|5m^`I;hra{?Xj}@Zhor z<^-j&wz08Mv*`gy*gromKMCa!I-A|Dchp5LBzGgejB=woZ7xE;vV2~SN94CvbHfW| z(!e%SeuG5y1VB;*Muq#>%~K^?B4nh+_zw+uu7nV!j=A1*%$oL_ zUAxUc-BuurYjiU0GQ|h_ufdoXW}R1`Wg42|k%1Ficz^TfM`LaJwaBQxvA1=8R?IwAtzyAJwnGr$e`$|5@cK)XsOFDhr^s%EJk~S9| zm7bL^gueLFksB*JHSW4ZOg}|spO#sC1!+AN34)B|XevM3n$|drHRY}HmUtnb!Hr3P z`WunZyc%lW)zXsY=yfNOVdLpW4$JfeY!ei`^Zndb+;)Q$M0m(WT>LAAVkU1pGvM+H zMOfX%slJa-@w83@j|rDh<51O>cCrphulA3#Fpuu5r%Uv&b5M4)!Q+ymZ0_^=xhtz( z;qD*ODSYLWfElnt(f4~c8p*OkdXO8A;0yHk;fpr@_5!H7Tfa1$^bj@`fG4<0oAQEU z2Y-VRiBum+WhM;fA)7k%+aVgQnu}99(*duTu&WEMXw=vHr)m!9`V@|fDg9N zWGq?I2wBt@Sf%_ft`($tLe&B=IP3juIT9VJ4qNLuEo04N3$hx|fASORtxl$dyq>_@ z)=H40iS1&Mh9S*Yp1smo1zD+or@Yth?C7grZ69{cj8~`hT*#A1ls7L`oKRT%u07mf zxfp=IzebPvas>mgM3Oo`y<>*qOWzrRbnzHj=Cu7N&b0Wb1MTtn3MsQh#sH4p^SVQwyLF{NRr+vl>jVB1 zE=Jjo0qKO*YGJNG(+hz2-$Kp;>B2(G2dqjSTXV~52FY631 z>5EjZ+L`f^&M1pg{WSCSfhnI0Cc@A#d<>p zvTFxOoZP5%OFUl^8jpEpjBD|v$N`s#B;a!f?V8Huy7k=rBteEL=FaBhiVr_L zjMRJEd6Cs}3kKoqc$z5M@k(aULn4cuj2?TwmiM_Rx{=}&mq1E`>KuY>u|`6`%C)}D zN^l#gGt^7?HvpAWZk*bh>}UxdHyS!DSh#cx#9ai2qzm}+XK#mWI(|GK2724PEHVs% zU)n2zO0(Q^V)TvCaYw+^4$+z)So45)fH+4Yil4r{GORs)JnA$UkE|M)2vHJK;Z$eX zeniMPKrZJI==W{i7(r7m0|#`<4awnLys8#`_+j5F^Rr~P)~->J+z$+8^)dT?9SF1e z`nXz*9}HOCJ0v`eZRyN1W7=IUuZ^oV8Jza{_!9U8g(|A7Fy#bt&hH%*0scbEC#2jdh@V9f3w5B8;HtSTa!NPpY{=b zFlvKxt;FHWit>2v)97h7+mW~-z9)-At5*i;4{cvle1qr!JNR64oU*pG#N$C|6A;Aq zc-js%Xk3MS$XR}d0lZzz4X`dR(>K^wpNpLj4F%FEqZvpBpts1&G5q0^cF%3}LZP93 zq1oUAP@6V6K7%td7c&x*h(-+BN9f@8u|kN0N^JNs!6%Ju|wr91>zRvpD@ zHYA!nx>9uCIOZn}2l_rYXV`wsB@+>zQDMO;G?g=@FQyj$9qmz}(r@^(A&U^bptKHr zonJal8!yRo0$2XXVav}XzcWj@5AV|RO=!n#>`VKmyW;es17=I$vAmx8K4G>QhU{`H zt}ukOaHoq(Ar1b8S#7z4A5#;vPs!4c828buWsK9`xVzPE_dYmaG5tT4yQ~5);@8x<0G`l?l5!%*JdC zs8n!~l1q|8Jfc-7jzShmyPe6?bp8tHcw7g6S!O?O1wI`oGk58`-k5nso5hN`>=5T! zD_?-=Y#|fX&#=RJ6<5`D(VFwQ^RSeDbDz!3yoq{1iW{+g7;9Z~i#{528C-42FKw?3 zT^3UHd3Gp}K{1i3+(LBHKOgxE{nCKqti3F=$7jZ--cmkwULcpeF z;*q*_P=vM<->B-5Y;0cZZ2aqmp2g$Zd$DsPRGUzgQ=H2dI%+3R>`Se?Jk82Bx3a+2 zauN>vop*eoNT|E5ED>L}i8-y3819Dvk(=r!-8Ea)DU-*N^nYfIXNUZcTyO5z1K526 zf$n`eVmy9c1%?CcsRXkSiQKe)^mIQ&DKhQA63=>2@qOfw67pZ!V(i}UiDFT&yiuYB z&HKlXML;yzvTlbbpN-yQ~AWT8=7x%rKSd>>h4<-ksH|wj;HaAwBYx$zBoq@ZuiA zE06ChU{WsqHpdcSQuVS6KbG>jFYvLOvUTaZi?>BDxT*_xuGf+PiA6tNNgP^RKiO#>EvO2gldHB zvL$v6EFTZRE6ZH-EfXxDPcq=KNIr*r(J~m%|JFEeV?4qjyPO%irPRWlWqs$Vp;US7 zP-}Yt0l{ggj5a(Cy;|dx!%FVSZic{h4Yc9~xW7PYT`oJi$^<&%VA|T+gfY-a#h{Z4nLp11^Y|`@Xuef`fojWEVG(b&udh(!2*;1;Wp>GHHmm}>GDqp4rG9wepMpnD1`U_#F9`M*-ilbukZ5&QD` zeaiMyo+myfYTX8}_xsCGvE)9X+*AOz`*y(|2F`~gb~M>`A!)XHqci7nxj|ip=lx9{ zHJg=@RxTt*M32ZNq+)y_Ev}yVWMjUzE%O%tY?ac zJOI)Bsw)a#?mvryyx$@^MKzT^ zLc@J~gp@)@nwI5QzuQ}}iPnrD!BSJ|^$N8%VHwEVX6#N&JxX9xe@bdLiF16vU$ot5 z$8~zRuks~G0f`YCYC9=?>~7z-*}?h*4un|AG-Up#t?B9M{%~}L_A7#%mwAdDz(S9k z*!TW2BJaao8M02X@$t~qr`?kusARH3We4yxTbR%(-@l9Zh(_PNOSg@RZANDII?GZ5Cd(!0;xBD!hy-v4#260ji9>~djNC_;0g&A=eP zq)ovxC-KTxVfec#_N^;A(cHj&C)=&s3nFmZ`8qWv^XF+q@*HP)Mgxl2|ZQWXSqgRbz z{^NFrnP?9T!hmHVGEU;XqQBulELz4?H^u-lKtYggX=h#mdp_AZt>>L24G(PolB+wb zrW{{%^G?z;No$hz$ic5Q2?Bg4R_Up~=Q|2H%HN{WIuvJ8M5FESRY>t+ct@&B{&VMD z%~ece_Ak;gJ?@E)WrDa`m-l^9UL)s3zv|}y04M@r?5U{GWLYmpA|5S}Z3oq(dg$?wqMs&9+5T1m4^}eiIqu-SNHm+@==#-PfDtdC`)=ZXMvA1SK|k z+7m63@JBj#EYQwg+|Kn1mD6J;U-VS>B`0zHSDBk`1sHz~E!mt6HpY6e5(8r2L%k_W z>4?-()8sUez0@S7-*-@fy8DN=xf@&84KxaQ4w9oLZwLr13grp-@T&qG{ZO(&%D(_g z90)#{2sCn`G|}E#X|ObCnqHY^y7A@|JIaZ~W*8($Nn@(yr<5y&9i<3xVj>o6W2BK}E>~go;=-On zbiyE$&fCJb$;3E~9_iYWS)wf!SQAK_G(@vp8wPm|x=SpEQFiNw%1qWTk}+0zS6_52 zRt$mKr8IHXfbe!<2oh3?i9QBP741y-9k;t=E`kUWv(IAU@Kw+K*;e2M+m2n&<9Pq_ z<8#d$+K*JV!DWA1HOtham%hypCKVEM;0ggku{#VE0zID?UV;%tMFj)XOTvp^G#n1+ zDoWyrJUJnoKDf0E^8{hR)h7KCRi$e%Kz>mD*5lwR?`-(KjeYQGX5Np`U^3o9J)m>_+Wz` z6}5XX;g$_7E{YKM0$1IExe_4u+MrF~P8YYGkAckwzx?zBYqV@TFo$ihlkd(hAnnH4 zDW5qKbC#!T*?j78*ZkK97bisL4?|q7pVdulrPtQ{bHO1Zy~Q1`oC_x2V|_9AO&i1` z|HZzo5&yr*7iZI9+~P<=wJOi^vd!DABWM^J>MER_ohxsv!n@WyMg+{q+;x*CmVb*UOr z7{oUFO>zO;PIz~eVom_P%=gp{E*vIGhb@D|iZ6zkIS@?@OZ$<10W8YbS!@7TAZy`1syIQ<@HGfx5%&YiUL;v`?zpfNPu07XKYgF2m znP|4DG9*!uO%(sLNRlc+FBGyY^=zgNFl()u!gW=yDWbG-;NX)|a~6$Zfy=?(px1ON z(3feFB3_wC3~hTUzT{<>OAltUl`UF!Vajs)R^TuDgJ}R|Dsh-PiTy7ekG2jIGvZyJ*K=r9{|c}@G&6L(_C2sP?OhHPpBL)+8?K+I`O%+_47fr&yYiWd3 zd_>!N>Fk+3I8$FhCP!o;`~_FVxL8l=yepWdp?xq(rq^sWE~{t+E5pozMxzoNanX*D zjRO5+Pi;40|3;fK&%t?$cP_WF}Gk; zbu6PU39K6<@3p4Ig(3FA$Ki&m>-*Hey1TwHgj=0*`tdLNmTF^J4n%hc z^BSf_bLXcK1q4tNbG5IH2Zoh`Zb$k8ht=QZqD8=G^K~Qiw;-+WZN_HUPiSI>E#xhD z#UKZQ!drQ=Ew}At46IVloS=6SesIx%)pcOVUx2@tw(!|M^Zvwn|vQ79#@9k`q`cq(mM(X4J#M-!e>YDwT z#aHl(_Ci@&PHjxZ^3M{Q7PWR&)M6x0xu5mnzX(M9(Dd&H=%5WJy23vf=$8YQosms7 zt0D<~WRRA8eXWwbAC|@U*5^-En;35zs=N_{bWn`n&@yoG&@^t3{`>~xCyseK1ZZVX z316(2`kQYNVdYctl(ECJ*e2L>%`>SG_u%h6kV0)gyKzx&fncu(BlHZo zHtb}#Pq$L280fQIwo!1*<0Xxmw28$@A3^NO*1wRPeYB{_tuDv+6u5cc_fJ zwyXV7BQq}A8R-aG(&Z$MeP7+=e+D4y-U0u=f+#XPTj2eOdl-#K(?uGz0l#+Ar`u>P zB9mKe-N)#wO}oiN?_hHC6lg7yv5cjJD{%6TieYfzSCtOt3?jErCP@uE(1Nql{owT>*l25)xnCFTJ8WmSJx2ojA83UX>PG+~Mga9hGpxww(7FkHRmn&k5XF zmX+qpNUGsMoCyzvp%_b15vdM>k0Rbf5J_L%+RA7(JEIOy@n=5La-i`Jy&Kd7|L{+e@t*itjLNsrUM8o1*_VY%lP6corJ2R-C|qr-m}(3Ut|?nD zGa$pkr%YF;0nXFHW};rOapG^sHo4!c{yDC|LX>796?f8LUxsxRmxU^RlR||mC`ZAl zvA1k`P=)Ru0&+IVS+}<7twU!H%#Ntdto}nujO-e^cv21`cRG{|hePAOtLX`i=}vPb z#uA@gjgo_Oh1!qYhg3+oAee9OuMZ-Af`a;xeS}_D+e628Y;m+@Z*w`H-_DM|62SZW zAzUz0dx0&%{eHps3D|fM_+bCo9sdrA#~Vei|pq@!!kDiMk^k zrlz_JA{XPcg)&djPS$5ig$QqHwOiKhYZMw)fG>x(Gl{GQTzCRGf&_NmpsD_Vjuiq= z>!)u3S@8h|?03F6v4{ft+S}Tv zDJ|xQmMQOxs~`M}(O&yVxHx1$Z3FT}Ex@N+t#ppv%i4ONvkP1ifMZaF3Az)zE%xqa zqxdoZ$CHhFdj*`(E>30YhW7>ie|+kEvdDxO+WoOBWW1SKPqzU$;Qr&0h~e$n00)fr z=Q}=**>q2;##&{LA|6K@$> zRpzAU49z;e;=A`;t(HE5PKCO9`M@R{h-~1@g%DdK5{JzxOpf3Hm@Ud z#Y36Y?iZEG1OP{s#xz*A{gIFtzqV3p^d97-@#tOfcTAG$5*Rg*#d2i>b zwt~?Vh%9owX|(Cl51e#C0}`@k5u4ujL-1@p_GxsHSp6H_yavvWb}ClSiSJ6qiM-+H z;lBr3L(zbuI3DGN1|;G8^rJrVT@_m;YiO7$Np0WsO^Udf6V{H6ZY63?R_Er|!?Om#EObvjeJuC!7afX~U=-{Wnuki> z>)Lp`JZ+Ep4Sz{=M%IlJ={WQ#ngYuyB9PD!J-9h!oju)*E;!n^H~H`K{tugczPD}O z-sid%0eV2tWYAa)8t3jI{?5g0TWk|+n~r<6?aZ{Y1=sa`y6$FxiY|*hV?@MwCS!09 zx1)lgH$M;y$R(UFZMX5{t|n}8MDnm4qoWemE*6vd0rQ8m5!2b*AUVtaK(h~$0g}x9 zI#Le-ERsmSzdw#>YVCLEifKlL!MTB{9~+>#^i8kH(Q#-g#~hI)6K($?O+47?8ZFl>Z@wm$C^5Hel+Y9`>c86Z7YKd3d}4jS$OoU) zq`UJ^7Az89ZW-?WI-YTqfiHY4r(_z`jRv%bW}9)@@avT<6gJ;>aHdPuIhH^$Cbg{U zT+Dpxrj2aW^OFQ)BgWPG7*i)G=dYDkT42299(fe4`_%+-So4aQ^fKQHEm^1*z00jW zmQstBaSFu;j9I25y?PF}{M#=w+C{(g-Q8>eqv z&9xRc!}GZ*(7HQ&_IE~6!?S9A^KFo$Q_(n$Jdv@)uf#x^En{Me$Z=d;;F^j{5{c&t z9S{SyZz+6H#-ObFs!O*hcef19OHL`nO4H`9DoG`+8KbD2Z5JB%Cm8n#$0V<@b5RwR zL$}l!GCguytxhEpQD_y^V&dZVLnfhKFK0x$g8RG6-%^RU?5o*9;=U8&UcvR=PXLkT zi(MZ8fZ^zppQJrhqfH~(>tQXGiWK!2-Fhun>I=&nM1E9jPCfgsf4RNavP?~mkA={g zV?T&%V-8#1D}wxiGZ;AnTF<1paF@9+=XNiCRm^yPO+eHMET{pL`ejUgnzjmoYWQv) zcLfZf*n?poKvKAfWY`;sAdac-e^SN8j=pyyyO==*CSs9zVeJw z3Qu4ixcjvv3c)yL|8Xz;WRCVt!1Nf<3E!i8s0IV|Ztfc}g;o*(>+Ms&b%hC0a zH*6C+FUA$(J9JtwZMerv^V!Kl8K`eHcFl{%x2?0bjeB|-ekTD^$K_fRv`d*Fdm-m_ zr@0CF4X!zH5C3?at+h3L$|`{-VWlGoOmfI_i1DeKAwu{_$1%Dlh*}Y}eid8R_-^4~ z`pf-tzG{~}1PIuN9sofNlK%jZ(euUipxIyoaKun?OxQke4kWxmwc6+Jg;X695ui!} zNz_Loca38{wUgQn+&bf65@{4Q&uMa}m0dRoo!Nt8k!hUJV@izXiWoc(-(xA%Ax6%ql zWy|IsY}(qNcZohZi)n)J@?9Qth`I}bG7t=QeXi2KTm`6}e{lvHklRlud@P};4V(<) zzPImryT=-I-ct6n!lO%R%$Mhzt`sJ?t_2iO#VrCV&`yw1*mm?cR1~OozZUBDsUh!c z9IVBvjtdWcZhrDfJOJhI2kJP`a@=Pkbs@eVPrU2Ifs+qGxbH=b7<{Ul9gy%SID5ZK zXg0|<2Dpbny$?!K==nTXG0i$o@wqOeiW~gi7Hhr)$Y1-O7PC#qM*?Ddkj~%UXGYUr zf<)EQya=hB<)*wOS4pL(5M1DhWY9Jzan;sgU&jhaO-G1D5U z5IT#HM?vSfn;DR0m zf{+o5;G@A|A+#NK03u}d&Kpb=cU4vYEu49A{u|U{Ef7RP7-U7Aj(~g_6M#!GNzuvu zkD+A`jS@EMTPl2J?_2(3^mz^R5oG~Y248k#8;mkaE6wUxE4VcfEx+L>S16KEXQ8r4if7B z+dIC3!3tXV7zsz~X2281s0T{OnBY9~yy;}(IEnVU zt%W%wtR*wgaI|_cdDC}uRGes#%$lMKk3FeJzjpjz5@W+M%cHIg1-cFQ2AkG-_6?cP z4_JiK^sK#=d#<^1jY^(gvBQ%(B{Y=X10Wr5zp9oZk4Y07>1Dr|S$9|TU#^Z47wJ-b zQk=s;V-#tOnW>g!Y9jSOTupcq0dhVrRNT)N-V!k<^DImnD(gdVqNnaEpZhYlhM1q2 ztp$l>)K%b(PLlg=oA31h)k?(v>D4uVDpl`*6ND0B)}y9UgRO&q<+dt+ep!?QbPz6Y z8;_k$4yBb^a*51fa~pd@`6*>m_V>DdyBl`WhLUR!efkHho4lx6zi^=atOOBaAsqn9 zZpZt&(RS1JZ-m9kkvpts%CQU*pBTpXP?Z4(!wVowDvod zpWI=n78CKqrV`;Q@RqOynu(Pz5~~5&r*TsMa`;a(dTO=uwVcSwSrEq6)R#Z z;%8=hIv7s)>;}AUqS9W>zP3+*ltFFUc(40J7Zku7snIRg)DQWOqEP-Ev{C7>Qlm$-gK3EFAIT*t$xsI;e7n z%clJH5ux4=hK+$43X+w-PJBvuqChtYbnBqyfVTI#p()bz)(|&3ul>@ol(h`WV2DW) zkJMEAj~&mTP2`p6f#gyto>@ogyXehtfd}b)i|K~Nb(rH7o3?XC88%s|U6rWY7d+dp zgO`kC+R`&XLkWvy2MbgI0%WLRn(hZ;Q)JR9Ao z(Et4QHk)(aaYzWV{5kYWV-Uhb@+Ce1y@(BJxYft}Sh?GhKym!fvOKQ2lTvHb*I%q1 zp#QPNrKVlBpqChUb<~1JDPxKJy4S{8krPZZ6h`fOKp`2mj|(n`2f{_A8`oNwOX+-% zNRr8KdVf)!ae-b~=|XVJBkkB@720LYYu!h%9XGeQpiRiCi&IcUd$o|B8izZ2rTnWD zsLOjXylIb>F!{KpS$Ew{4JWrwJ5nt=k@;tc<>vFt#Zmu-HYOAS)biXB&7O<$6Tn8G zZ>|S+J84fFM+eBxRO*&^4l^T(ufGeQ0+O&2li%$IhuGUXIAe?m8P3R_{uD<$OvYAn zv4sPX*k8Ahqps<33o+qe`#z*Qei1*3yA!A0Ad?z7##ep{nnl~|gkDRCFbyqkFB=7Y;sQZY-e^^#& zU$>EcVMGHu)+?|CL(#d`8C55&LP1q$Mljb$4x3J*ZRUT_hVa)$vcgEVKD3?+CSPCa zJ-sotdm~#*uTz(K5a!~r9*cmj`C~6wsAih(<|vq^YbLsRn5~vwRQDl8i-C7T^dCP+ zU8s^?9BNimxW;b|xl)=(Aw33sLzjZw^BzHq_>`W*=o(58j1$ED1ZH#xz?pNEk9FKR z+2_!P|7u7})~;KdB~60~HpJe6-89M`$EPSqT%j)&I5FMWW{86E)77kRu-4KKjm6-d z1-6MAh;B9wX%MyC^DhW`h8T8Jp6?!Gi@+(0(TP13YwAFp-s##KIM74+`$$FRs!8`8 zix`(7RDFS}lpt5p7aC)bTV#73GZ)GWB-N&HJIAvcz23_Hh{sbn89BKK8?N_7bzs4S zr*8&fM>Hj*L@w+CET%>aYP5PWdE!3aHnLmL{K3?GI)vut)0Hh#7;HKhS*`bDIDBO_ zaq>k|Sw!LrBY%9@z+#r{%T%XQMw9nFd^)&URMWH}K6K;BJ62TUA;7!bu{T>7e0&uB zA%{06a1R!tKabTB&fjU6U$;QPCpyi7I~HgP1fMRH=nn3 z?}~kxC~`us9y?dLFP!`O2+mIQS9MuGa(JCv>VZe1q9G`$S#SSa8Pk1W?nxzdJGzXB z`_szJ*M;8Ly7T}|=(aUdlx>a~=@1`o0g*q(PQbrf3Dfl_#+UkIE}dlzFqS#`%eqB< zMb5i^q}|YgD$UK!5r;g!b&31A^h&y$2rlnD#wj<@Dfuts`C- z2fbt|+72g=oeV&RYt)fV%K|K$pyN!9b(R_RMES(>>%H#9c`!RT8)M%l=dtnnHpM z0H;epLye{F`A!GS8gPZs@NmiTrSXU^W#~%UWmmljk>KS!U~_w7$$3T~6b#xhV*lAq z-zfk{%G!Y6&b6DXA_Z=SGmm&g#M1Yld{_R8 z0QX~iMdOOp4kU7jW5YiV&kUFgV4h`OzT&I{2SdgOLl0xCMcZQl90YX01vD4Rs#%70 zBPjSQ@ML+f9Gl@J??aQ23GjD7r2AEQhQM4~RcKx}wNPU|jfWsHF4c{$<%s3*oW>K2 zNDS3Oaxwn!G?@L;|GU`uTs1ptMLZx_OeqdP%lh5zwqG-sA zqGFXXtD{2Qizodx^-hMQ8&krTjcJP^DOpa({B77eFhj!{n?E}L`#;8Wt1X8_)!{O` zNk5dDW;)0XMRBih^Qx?Wdc^YoyHHcY)e5lwMV}(NHry^L*=ODO>n44%9V(1A-4rF& zLglo9N5E_TsW0Skz@DNImL-5OE$f&4l|K=rv7dJ!I$*67U_PgNL4aXcNYn>k9thvT z36>)#X~+iE8Upw0c?WzZAkO?87Q$zq_0q(W? zz`*c|YvrZDGX337`%fo%q`iszJ)Mk()-4ecWY8v3ur4HNP=3>fAr4&fixP=&S+j>O zB`G<4*w~$zT6LHK9zz5RWJ}mV2sO|)xSMeKot~et5nujMKvy3GHaImmr_77`xBSTD z1d<_@6n#NWbmpJ4-=q*ULA}?rLpfWahm+G^EpXv~?vN9s*LoiZDKP@qE2lPITmGTh zj2MRKbLkJ8zP>mj8ap3M$J?Pq^brprk6)!v*JA)g(boM~@YVo`sd{Vsu?AiI=-f^x zIL;rpk>gLBZS(jm7|0{Zulm<_ht*Y$tY*+w2@;@+!l$yQyp=;Js>Zkt)={`)z9nxD zujQOI9DrbwEBqTJla$#*{j3+-qDfD?3=A6wU*KwXl4mP$#;+M&@6XvzKSNV33?j&t z!q-wZ-9J*HK*FoDRBJR<<0vX>s8`iNN@o4>oqP^M81A`V7^NT&wf&-jhp77V!wDrt zarM+NF5O5!(&wUj>f$Z}TR4@NhFA)0*}F6#+mdZ%X?6)D_i#!7X_fZBn&SI*GNU2i zAGSNt157|PN(GsQX2S%V$3GI^I>?6C2pX%&$jKnad}y@ zhU^AX=b|NQ7`w)zlD`2?t%#oNUQKm23lQ_ysFi`Pxi9!j+w&xy7%QZ8%NX>WMJ!_? z6FnS*KYUJm>b*u__j(;?=OX=8!bInxj2hQr!Wl^06xgp3Cvpe$!{IeXda#gCb^^QO zj;5v~+brztA)edNC}tOn@dhGWW*YrDFZ~}2X{ymJa{PJb|GiUw$`XCogE`-iJJ! zurJ7&m|ZDk?sHB5cs8z)?2Hv11n2MHpQn5$(SPVtCL$(7vE0u5_PIMLtHMe$227&# z-KT(y*&$XOwXHT516G;MVnR-Uz3pV16v%2Vmf&wFy@~^H*F=es= zP5KS`v4h>!zCD;hOxL^!yfTqQklIy$`U*Q-xl{f^m3Np|6xaEP%<*>Wa%DOI-*GOZ zq+&nKsjqgHN3sD0#hQuz&ZmW;olu8+%l2ikFZOEf^Z7@H|%X zj1~rbp74RZ@D!*Z0UhX>JP#EH+sxa}-C%@^g8X@YGv?bcCs$?IG2dP^WIYoNdWb}G zqZ#*OhDt5DU4K*&AY;Al*A9tE4Q7zlDcZq!q{-mN{uID3s^v>)DCrc5+}oCngh zx?#oGf4;ozblgsnE7pGOnxhYg*B{+#iZIv$wEz98pBbj7E&>L37zd$XwM4O!?y=U% zklJTdS8no`ZVn!rFMAKLvT%@JN$YVpE_T;>8ZN&=xtIHHA5GZ+YO*IB5O_iIG4`<) z4bCMQl%g-Sgy&uphkLYLd*1~pttxx8f2@H3hG0QwBmc)uGQ zdCj_M9pE8`TaqnXOr)}zztQ|tCA zV%SnbbDn1}Zr`k|F!cdJTNxF$>?tJ6O7ScgF%vl;9SbKpN!lo1)!VB{TV06?cinR8 zL5nDCHVQoKvPi|wf)u#{=nXLONHSlIJ~`@F+L#hkXB(liMs+boKlv5@E=b)|D}JP_ zIMm=UP}}h~gZZvG$PZEb6>Pz=;r7D?8$>j9>AMK@&P~>(m-w9=_=U(PgC3XjG8y)1 zYss+bw*>-4v;^WTMTw&6ZDEoN^PRYN9|k<39f9)yIJ>s z$H^BZL zaIxdUS>KD%t%qAQhUNp}#rzAOkn1y}tp44V`}|^A12EZeeMdtAT$h_&t|xFGV1fLX z0r00KykYHU>y_BW5tX3R9 zUr^UY!iQKfYzIkUnc^gf4OI*^2rFoR-0-}1>!qlpVwgFBaSVX$04kULFuEh-;^MwA znjP@Owt1d{%N)Twl8@X#B#+hA4JcF#_6)bj3fuJ#qF9!t!w$}XFHwYsJpxM3Cy+kK zv@R-C2l?Ni>~)4uQY+7e&((#1K<+ma$!wl2KX69OTyjT8XFuXWA7HuO^f;wsWUyL= zT6h8~e5s=77?TbKKA(3;jh44_QqD$fZW2YQYR%UQO+)7iNl(pcbnLc8HKewChWdff zl8I(85DXvz@F^?pG87s9L9h}$n;*er zaAvu~=!013X5hUo46OLNU->5_)F@~MTD#o6tpTKc-F!P?KeA(5`z=FXu7yA2r>FO9Ro7j=5ki)-EUAvRt`rp#3je3rvrgs0#lZX0ia8*b6dj73tz`$Y-q?o=gH; zc2HdgKe_jGz7dWOD6R9cH1|IXsHS;GIr^*T%^5Q%X6<3@Rb~oLT-s-+4xgZ6&!MWq z_)8P~TZj;hF;T|&)Wo5pykNalHrd2*yDg(CGmSg-aXn#f&y*ebr{AeiLh ztjc>2vX9-sWmO-k-?Oln>!$T0JVPrC%8xTc3x{>{-P%y^7Z5*4n-1Xo0K@nxO9ONI z)f)P>(qQVay7wdqR=Y9b+aH8X{Z<`aNawh!7~1}-FSbIkO8O$GtL+AaObV2a)$?B7 zFMZ{8ba}EYt2zh|xI!JY+$4e|h3*?~c0aYb5ceU-LTU8LfAj8KUFJN4!#;RU%}h}` zCf#Q;_H8c+x@gr&#hez7Lo#N0-HhPCu%83o@(;jKg{~l1eF0kN$8zR!rol_Sg$G4g zjO&KKKyxK{*e&NRX28#<8)GLy(O_OL+k9`;{}83a!Cwz_KR?e5>2{ zD^Ow!xu(Uw5tJn>hXbtjOQ0(zlvp1z$d7P(OD1@w6uFuyKy_e#j_Yom>4Iu9N{ z;U@TDa&7s&lOTUx!N}Z4%7&tX4`!wvz}fZzRO|Y$!5Ivu?^IyhbS;Q{Y9tKuy2D}p zw!m%AFd8)`>_{>8D!A~yB8nw@t0><6Jwke!Z=i2zKoj=U*I*J37u`be4gux1?sl>& zF63J;{{psC=~cb9*Y!Yir=R&7;UKOU;;F*gJ^*kJ0F8G++mW$es7(>rM_l6=(*)s4 zl*!79rL4g_(5U`sUJi^#?E4+Aq4D)x_SD{@Z{u!LY}g77?_%&k*l!S5I%lk zfmq?hh$KCX*ZtY3(Xl}+;@y0|k2@WEI4v5uB3f0gmaAo&RGcC-$)TB43^O4ZQ3L+! zxQZA1CYUB#lzkus1w-tL1rSG45>)@5n|fv{e#xXJr=b?a2hivL0k%L%zkPSjfBxrx-get#k50>q^gA`!tfK z#db$?Zn0UP{qk`~0=Xb&?|tukNq!*GO0xgHA2{{YQ=mC;>+hU$>OFVeIXXJ}t6%-< z-~R320A!E{Jk|LZ|5uWdg?H>1js%;=6{xw&B|qnT)aLYwC#J~M0T5f2@%u zI1kr{w!m(|C&kae+%Fw}B8c#zpZW;6wkY<3n}Xf~vatz;$t>~SOn2OQPj~kYJ3~aU zWB=$vvnU!}%c)Wlv^^{iRz}F1ze9s1_&~5KN#sZjBuasQS~N z{sd}CTjgV)%vE0G6rEOm`c1FgZrQSB&@Px6KoZ5_aby=skE~m&slNnWWOqv=)bNO5 zO%t!aGKoCyH2bjV@wV4qd*Lpk84#2fG%=tJIu(j}_uY5L?iGg$;)TUJ`|Puay|f$m z`qsC;h0$T*FrUz}xH6sr-~!DDxD-87fO-&V{0x};_P4*i3Hy-(Ik<~f1OzLx;gpKw zn$*M?0g?Z8#ea*u)_3f++i}Mohv|L%@yDTGMIb!pM7X>~dB~sqj&)+i6F~4j{t2=P_S^S^5Por&e%qFQoF9-> zWY|GyF&p?TmI(xCJ&JZi4rpBJbDuln(MKOW;e-?R+;dOp1{kK$Pf!?uX~=x9ckYI# zmDt+>2OO~X-g^oEB;-eAAme99@-qsux^8lFE|#PX=I}f>z@5%Odlh{p^0nzsr8rue zo2`qCf2TBJS-HUT2;Y;Ipa)>|cBvf4psq2w6FQ9~9VFWHk6RaV_F-`EcFXFf;!w&w zFw(NzoF!?dlEvJg5C~XbYxU(`e9lm;h^)F!p4#yQRJYbN`K?(*jrNizkD@8O^{sD( zl`V`x(WOW*O6)h4zxr8u!Y>IZ#BYEUC!c)s?fo(`ifNdd4$>qBmq;;7$c5RdgGz($ zz53O!#zuuSi`p_okdOMJm0yxZGshh9*RP&8mWMTyQo9ZHiRPTF434&tDC))}XWEe*(kJ&T`(`x18-{ROd^KeO!UMbD=) z+>P04)~>#8=N<9HJQX(W9Di}k>5eDUZtcmuzW(*E!=S-sMF_dbfdnmuhFiUQ zHTa|GrH5w#OBH2jo{qiQt3s}r`H@MMcwtCBMxgE2Pdrv6G~iyC|I<%Do#ds{0c?SA z2&)AP^x+Th>y|23t`c-wopz@@I?`-3pgYO8E+reHXPL0)lHSoT_E&<@{o}Fek%_IE zvnzY$veyAuvaEdSQwP5Do$nO7Rv3|@dpqxxB+K*02}ULw)MdlHGz3e#^wLXV8Q}ZR ze)h8$zvQJK`{)6nP0-l@_pS-@WV)ns-35@v-mg?fx7lVJAr%K5bPzPq({k9pUZp~i z!fe}3;)Dnj)xeV3y7J+lGvMB>OWP;!n&uWKHO*?&yQw|k@=Q{RrpLUO5XW~DoNa`H zr8>Ts2Et?9T3=zsc|Msmr)gS0;ZqK6)KCVNw?Pv z{#oGp9R#yexDha2QuDgZRL!(pi|H~pyx*oUo-_#=0E+ESKqyq*N&#%uHNH1ZtIQAFWw!P@KC>@a1|q8cl1nau zQr~BveMAJ2sK<*%!RmucVsW1)yyz-0Dw#kauh^xfrj9ueqoDo5?Z)2l`qx2`+;+!b zU|xOa@s3Ue+DQ&J{|X zX1!HkF_ikZZY~OA;>~Yjp{ z9o0lkveC+9kVJ9YPboNzdO%moE{tCQ!LaWhqz?oZKybqie^hdnV!o28n&0&ulQF=6 z@@s1{evC|`PJ&8agJCWl5f47J(sBxtW;jKva*PLP&o)>VuH(=9QAUbMM`wzuE2`)> zWjU5@>wZKXWD@H0maN0;rCGA9IitnAU6@_{NZ43IZ|k{t?6oUiq5}rzfyeMr5!##X zzAp-sh3)#d$E=%w#{KL(IF<482Da&r**sj2oIna|&dmVCW%4#pI{6Y0mh~EQk5S@# zuU-J_r)WGYBxs#4(AZ_mmbtDgTD}2Qg#&uiUHrq_Q|>i`6h@V9je&{t(#;-Zf{GP{ zt#|DG_Ah_=%hS`-2OfAJs3?F>{5>Si&X;t(;I-!5{0KLcGs|_`jV2{JJQ28yR*1q; zqgqZeUs82daCVuO0GiK0pGueD(cPvD;P&*+AxOk@Gi^8hRmEO~(^shfGvJ<&$a85vs| zw3EQ+;LFP-0N(*~*j9je?Bwd)4F4t3CEc{Vrl%PiOH(jiYN|`ICoQlAJiNk^Oy-q{ z&Af_h=w%+0kf^$1X-bl6F*v>=#hfyyk_aYAa4+m;mAgJu#vI$iWrKE;QPI~DwX}9z z&C**t8Q^hLT&m`1q!K zX8P5MMU9<&X^EMy1YR4Qz^gxU$8EPlsem-xb=O`0e%cv{Vl7^@MH&a~R4!QNs+EtI zisRty!u$e3!7_$^JxPOp%fk(^bx5G0InJoPt23f=%i zgc@biF%XRSm?iBjX%5w6k)C4hpA9HUN-VPN9EJ5q!INh0@*s@ssXv1(^(_)6_c$fn z#KHWl!F`y&_Yjl(t-$k&>y~Eg^)6hxZn<1(HGL@!+krR0cY3RYx4=o`AZFr}m3W80 z5aRoxbzB<{wP>0@f2^1CI*`aApJvY|(+78gzg($OsU(yy!@%N|G-$Lua{MP*p-41y zZp}0asH}*Y)V>(MNM`fm(!T6o${zfT|-%l3N^g%44{jSFd}u z6@svDK4`x7YXdCTV4wT;<3jbJNoF)YgQ$QJ4?6R+K*v( zdgZ*k>al<1i(|YKdY77jd<4`s)LS2OC&I2_nRHcmEQikfK|30?Y8>#M!%qU9+D;IF3XxCG;oHrO7TW`KIjTF!CoAWUV|GA)f8CN=P4%eDxZRWr=yDKOpC z18U30%c3Q5?1yQ`>y~vCr?tE{1=w`VExmw`%DAhT3i zqUsui%5O}`nXkYM)ZId96y(})i<6a6AC}{617c7pQY58ilZgpfS9LoI+H1RI`c$1e zVT!O6VI9H(@al8os6+U~*!%;}Cv|4fLv~@PfjwxdoTN-vC}5K zL3PArMS3o~Wfq=N4#+!Ul`}fBG|VEeH79cfC{!wzZnZn}@x8~;iNcpkmXixQkV#;g zhj&2a@dG6~Df7Bc4t4?dyDHgH8N^UNS!%iFIBmuIwvBxyaDY?4o03TcR z8Lw6yc)4k#a`Dn^L7%f!2Ji1T+8!LY_8!l&O5|{%ZXb!1igI9y=uAC-q_|bCMtpLZ&kEn~iOu>10{NZJ&Cd4wr@> z6z}a-hwr-Dc|HMG@S0VEf)PfKVW=HUXk*qX6y1?U>mcmH1pL?e);A|`m!)3VXuh;0 zS13hMTAzMwWPBo`91(wfJfEgPb%q*cC@cZoSH>2%T8%J{++5xd8I|Fb%{2D0*F+|i zq$DERUBtOVXbs0Tbm}$MZcd4i7SFRkQP@u;Wq4`TD_nVAN9! zAd!C?Z>-aOFloFxTd%Iza}(U>GvlEU!#;FsT4`i#wABv7Hra+uNy{T!Kzp~nK$Vh# zf2I%3DmY28R4S&DK?PX+KpaAnqbBy&5fMM1%gg;7*9E7P>oB8+(_bl4P&_sjQLu6> zjBJPAtQFA*Dmz7H+6Mm~kJ{m2jS%&52 zC_kj#iZjWSgC@0Tq(=U!vPA2*5 z`;R@ZTuhJTb%WW3E2igUcOBWZ0zWpD6uJRG1A7j;jy&vr!Y*a~8uLRNy6boDM`4RY zzX1tl@o?(=`p)v?0aR@y8T?nld(WRh)=y<}Yb;fLdWDsv&rC8aL_}X zJr!&bM3sOWj|jPe!77Rzkncx>j(0(rS`AnRC{iGM zDkrz=A(||+oxGc~W=Q|B^_c=<$Zn~UuPm`GlC;_CYHwyL6|`xs7~CgtPs!@|mRp0Z zt$chXm9%@d4vd~HaPN_K4lz7u-KjYF(MG$IcBn=mQ>99IQZmhUqs;>Vdhzc=)LQ_G zVN(@JUSo?deoP0RNz|sG_yt6MEv$cnS06gX4}!MyM%b2J07b2gje^KD+ilm%DL_Qi z?L@I=8n$h>{ATR6L7YY9k7Ciq)7Gn1a+s%CE~f*(O6Bo-y)JQwIJh%q>3W`OQn+-A z!Gzv|Bh4p%Q~~aaX_+pjgNjG@>DUn7QD&)>D5}jU)TivaR<+h`if!2?kY_+GW8u`w z$B5S#iaEE8O)jU2-)aS&CQPhCWs#FF3-#mqjrD2MAy>?>@Me535Eud3b8Z2|Fis*% zGr-Q~s%v2V6vq}rU$kmfPS}GX`Qc5VbX!v`QlFXy6NRS^kRV|RsHdZ*3k;$&1xh4H z>5L)iP^FY4q6D1O=g=^Sk!7NPZ|@{PX3}%a|B;BRz%3t7?It!5Ha zkpS10{8*wr0y#C!e7;z(t%3CFR=HTN$hjh9F6DYroYkashEosxcI>x?nDR!0>98_Y zM57hUa^h5h>45Dj0*IkiVCxc*_v^x;FCan~C5>8B)671;%ULjFG_Z{XB~Ib3oMUK8 zqtO)FlE-e%JMyQ;&G)8>Pvwg+@G%yc>#*hLA6L)wun#*c`)7cfRRVd0Ruft|4!n-nER;ra zr5<5#S2@~t3BKw$mhLJ#=}~bhtTq*CNOeLEI0HtK>rXPa&1VB&~>#b{+r9tX| z?15M9Imkd)^5F8RtY^&6;w%YU{b`YUDMli)e$!y6SHYY=r>f^dYAYY`uwObtmt-Rh z6EGoAAQ3pvnZ62=0i%TCCqRuD!zjXzuq3kiT4y64NmVu1%|+x&?Vi~V9Uyc&t%7Tp zM#kezuFuZ-L6Fav@{YdZ(TAv@^dLY$exifrF1uww25L=1LGIbJ<#X4bQ4#E_ZYF|t zGbYAKeO9UAQoBA2E@l_p)Qi)g18|~QV3DZ;qKX6c zVO_*{fQ0Fex%ncNBJ=~pP(jMTtVg$?gof7K6uyXDE2eJeRok(3HRkD^7HIBe)pxoHX?q9$9Go&%szT&qnr$f&t)7*jEH?r|OpxqI#%t9DNp zSq8nEbL^4vE#{^lhlaLucCkERo4NjneZSslR4F%-*V&f^CkkrBl-J?(QTUF{x?Pz# zz&|ZXH!44IqclLQ0F_vw@<)k8exZRQvOBPhvAEr=I)zHD7U;SN$1GE}pX5LFh?ND= zEA$Ow)$sfxtQ*G(I&Gox*sv?F=d;xn#`8RqRv5a4T+Vi)c4L^;Y#84~sdAW`+ibUO zt6rN~y-FI?1b)U}>a!(~{it(sKE@4XMFN`HxxAs82^9#YjCnukfwgVp3&D%0cCr zqo_>LDFt6M6vI+9!vGeju4UpY$228psyWxNEIbS$jcgdqqHfw0Krma4TFq|#KSDR9 zi*B-r_aermNJiLhQt{bND@y`c8yOh8rpMF_JgtyRPVrm3#3o8*wmA_-r_Ym~Lz%~_YZD`aPY*mgo7V5UKQRhpw92xAED zXhB!a>6H&hfp0r`r#Pk=l*@-_(~NvBr&DpsFvz(eSyY)C7E*V-!|wHY60h_dpW69~ znJXBUmQsFi)|jiAcCI`zo~FUv%13;!4xVlm#%#Bu=>?YQA!T5ONfgV}*vO!QY`Gkk z!Lkh9vhmCso&4#77x8}fIw=Otgc$=Rs+k2v%PTAch`=O>+BHAyfICv_5&%B5HdzB8 zw607o6quA{>qk*}a|8C_mLno~CWykklh-YWPN2eeyE)szklVG6H`i)R`E5vP+FKXa zJtpj4XWltRpRzx#hY;f0Je(_E%IKUF(ywBx<(vX-2g9^23wA9@)KIhosF|H}z@tJ+ zwoR~hx|UIj#uMhNKf&zvbOR^==paEyfu&J4h)?TIubmqMLFjZMYyvz?&N4?!c5P-l z3flb)+b0wfDl<;^#0CvoO^*qN_-qN4`fN+4Kp&2*91v5`RVQD>k7=rBav-M}j38)8 zY~YN#Wdf76q+j*BJDmxytVG3lqM*}h)?+HU0+G>Wlj;X3ifn-0g5DdluG`qj2C&~K zDhlAi#jaMg9KNdxK)it8sQ9?z;6}QhKW0n_zj!Ik1> zt5%ie`CChDLt*`hN$WXeOp9FY8Rb~{!qvV$=B}uwq(P1?OnyXd8JM9O8ZWKY@oK7W z6?#zDK&ERIg(@(xDYtj;NlZUWN{hh=IMrdgU`?JUQ&i?&g; ztzu3#)l`?5MqimG=V^JUCyJ)oOnKRF%A9Xl<1!E7RtTZx?trA8m%P72+>>li& z&E)YZ=20#b3Yn~Cabj7yFieBQPc!Ib%W-rnBGga9=t4ZUvjslF6q)y2 zepqUOSGE5>KOdQPcnBy$AHAOYdv9V$h_>OI*zBex0c8(uB!S`%ny zf+}&h%(F)r)Mr&L2W?TaKJSoN6vwIKl(6%_)ntHTLj2vZEihZWMGa3igPgcozdflU zx-ZGXegpvsJ5j4awZ`I9mD0=rX(ndJ41zQj|FSJjP7Ff-2& zS3B5gmPVq<^?Gtrh`9snc#x8jDN@3#7nwRW?ZbOvyJI^JIBcs)Ub4w0i1qITe_XoEe#|oOCl;np86E_BQsSsRTVf+XYX;Rm9PYhs;E@0MWM6; zn^8mZf~;DM0*#X4+C-bHS7xm>CatE~`69>!v|LKn*x6=cc=R5DG*Dr5#DGnVJ!0C} zeEEFAcC6aWY8V`SS?aazRxki7m-E`bDr-FVJHl^Z0n1OH!&>HbUa?DMI(N1K<-{3f zU6pB@q-16ruohqiCV^j_`e&BJPwC`RMBNk|t1vPWc&(Ha*gTu*x#)9WuLDZz_E=IV zqh9<=(UP#ZtTJ&c8)wAU1yCTma*1$#Mc zR>LO!3<#GsTX&s=;9ZBXOR6PP>NK^)<~|%P&M2T950WWqKYEVWc}Ww$BZXn)w?vr~ zu1mB9u{VR3@dI7S4`3Co?Nb;pi~x{#AV-!kAUnmw&{07ljH#wzy;XM;S1LJqz?mPd zS^l{1&GLFUl03-R;2lZULUzk`=~#gxWGEnHov#=CwP}L3Vyjnx5sg5G++|rTp_ek9~L!n8*O^CIR!za?8cQT;BuE#?TOcCw7aj zZpyWpDN4GLG{r1|bt|T0LfM)aR~iRFy*kZELwANNjSNFA6XnJdK(vovz(i&>DUdaa zsL7R}OWaC{0!$lz(DpDnD)g&FBE|E?T$VyRciL@FbzIjjV8n48S&o$us|Sg2(x?%K zbtVGu*yc#y&)L7OT%Qik*W-VU=e>!j^Uyefs zI-jCJX-|M}_-vwmF9Z?8gQ2e&RLn$(hQ7kfbXqY zJCDn`bu0$bFdglZqkQpDmAckLP5JXosh8&ITv^nD-MbysFu9C}@3Ur&0) zdXvjDm3pL*Dv}vzB1s~f7H`rVXCU$|m4XyiHM1-=rDT&>oHJuP>P95V##)?(^{P0i ziav@>D9%oSz^bW4?HB}kWTNbM>T{3J;cB`)3EgYhIfHbG3f2ZpPdare^4M@V`+}6j z9@MTQIGH6ZYQ<5jR+nYNr6}8?YZbQ$8nd47$q^(h;??0b{%8(`sQPnW(1ZrEENGy% zrb)JIMIC6AB8;LmZuL^_**qudW)L;W2B#XsCma;uKU-ZfIkBZiF$EZo#KXe_p?bST zzJ_CMp2Z#CwM4-YqGoM{2a*gy0bH^q911&G_)w{6uqJD&O7X!fKT7$O+l7|zu|8Kcv7C7ID?J1&lHGaq_W=$#- z@-QnI)0s>JEeJdKq~qkcgiygoRX6AiOfWn8^ZC__=p1%_q!FD>D`^y_VJ8i|2xPL= zps#j2@>=xOtOf06L`81q^7yH$FpPKd#_(t627{`brkfj8$rY1C zVJ8Ya+fwqS5lTI6d0wl90S7^Zrw4+^nQTKzSw^W?P;{f+sHRbzrExc%yLY7DJ=jy`ijtAPD8k-rC2{K7 zGVEncS{8fNRjXj{7fTk@ZQ!>xBd4n5xehu_ncC9wqMwwep@Fd5?x<;(3L_<`=LGYF z?Hh$|!7)s=wq~Z&XsTw;ut#&nf|bvydK$Ev_3HAVwlWLqQW|Z7Cq(W>oROIwB|)>( zY_hOzmr4b91ZtMnR0M__3I<$RJeIR_olZS$2MUYx#XvZz@oGhs+mA!CuP0yId zxMCBIvUzUq&F;nZt8l)TN5B+h*;G}9@R225g4(sLlz;uBQhY+lx7gZM{00>VN&1ak zk}}OQG=<3{8uvN?BE}&;I zcbKFQH_ObKmZ7SyVrrH-YFhWf}DM$)4NAex<=sjt z=eUZkdCgk0x&r)!PsXqXt%w*9$Tpb{qVpNq7bYQlq9s7*%wxPiNjJ=K-PU*igpfQA zjPW{Tnic^@sX~C{$u$_`!C|j-}gHz5- zH>=A-Z$?T2S=i%p|J%k#W%N%$$s~z$~GqY6TRqWgO zQUTLlpIu`+IbB!7AYziPTflmq)}O_jH?j|M1+jy&3``@8NXZxl8>()*l#F_MiBdN_1^=)wr zg&w|3GEbfh{5a>@nqd$pWU`^?0}3zP7Xqf&hVr%H{z-hX!D%7qK6FY*u?MoI=L)Xr zR*ccHe11eRZSwS}M0qYs_}M7XF1RJC5F} z&yuOp^}`f3vs5UPH->^DG8sCTl5!mrW^QY?0dvgES@~jKcg-|uw`Nv&)#XXp;0@3R zwJC-Mk}i3Xoi(>d>?|v%@9*$KAFT5p;oD#uVwnw&)U?XjIochV8OZfW{c@P zEw94(FdPS%9{O#ThOV1e6)NwYC7pTBoXvVLWZHJx9k$VFl~0eW{@1TC));1Z+!2MV<`rhyV0jtw&vn$C>Hh6H z>q;uU8XFY5qUaRFMTxSe2@J(o*TvSq1uv8#^XiPS%#yZ8K7CQmk)P1cE5dIBGZU);v1xRjF0I(|5$&jYVj#jP#F5)~0tRrZ>*wyljX+A>od z>IRnJV~J(kvJ^Mx*3_%3ZFe-utU>_*?Ki5chTA8Jq*LfzOhXbUx~(fI^BP_p$A)E7 z-FXA+)9_|R&Ddn2`*M@mN`Cm=y<-tSn$auRj3QiS79qe*gt|mY0?41>}%{EZAUmlq1 z*c6I0Q>z4e=$ZkGkoKm|U`blK6oiQ;XED}L5k;QzK(}i_uP+dUO^39^KUo2(A z$j)o2YGCeeaa6PNup*i*I`OB_aNc*R<9m&n737&olJe+cireA%-u7m3L2kNS&ZZ$@osePY4uCqs^UGNizK%8T`RhvSPi7*Dnp z91W4(o2N(axB3r8MM?15CB9|TWXr=&%8-F;KO}K_XqrutT;5c#F3!tx*9kd$3Mq&m|%tC$i*6~rFx}J zk@ftrTJy6vF~{+jClL0XvhiOebZWZ9F{uES7dAubNc4?HxkorDYLpp3TEFM`0m6-v zBFtZ;NhW;1SZ(t2e7~zw566iXtnUF3XD` zO8c%g4d>4?=1xxU=bij2&>U*~iKgqpvB9pNPEqQc5IpuQbydUsfTbbBY0+S*VuLed z;J5kGj!vBe@wX`6gs~T>P(HHw0DW{T9kx?swW^CUk5%T&`)XrGW!Y8guRQGI#9yCpfWCx;E1mmE;YDXbR8Y2|6z7MWpQn_tH$Poc7AD0-x?T^G z48ZGtL>bpuCR|s$_rRFNdYzoDhzGR(Q+@dt-Pd!=pC-R>sm7FC9S1MqdjNh9{1_v9 zU5D9uIbglNCQ;-An+M=$m{47iuG(tfbqDa9(j0c$*!U{NlHx-Rrj;4)NS2lXp=p|h zX+eFjX~XBg|mgGF%lT za2E!O-9_NUmQSzw;Amp`0m_3j&f`3{fx+c1Z=I^2Mf zzBzv-De2qZH#+jxFnMkX)%&gxnf#gAeg~(x>6#=-1Mn-chR-*OTDYUyy3~U}dom3a zCD`C@e&Pp-#}h**a#DIxqj;=Ud!TGez3NF(gL=0cLq1OKbku&CDgI}f{X>;GOKTnD zFdX}@f&$zTi4}znmOQF{N}nq4q{fkRj>Bn@bK}fUR&6Kuhl3I*$%AwxRG6hoi`~_` z>gK%?-JD^z8DlHtR+qcJtyX7ed6oom#0d6%2XGy;aFs_vNb0iLZufC|malQb2AZb~ zoA?!KK$Z2)ThZ>rWW9ND!F^s|{bT#?@2cAx)+388`>ou6ZKHqoZM1EZWNTOgz;))9 zDots?IW<*7)YuA9vXYtsnfFZz_IsM=co?AI#Gt|0$4VPG6cXQ$Nl<_ci z$m#o8HYgCwg({Q}E)T+ImKK;3z-JKNc&qYGn38CNm zdZEmYd$Kd*@l=cBa>!tfbv-IBVBaZ6!Gt`F3os&Ep>)@uU7Ue6c6oX2$skI===UR^ z4_X}dTb#2h+T#belafy4d9@8MupnNqGVmK1i@;r=YS>p`RPZof`%wh05oU)O19HG| zhuuMzH%d0(G^}2{@;OK09gO)j(>Y0|0hCt2`Y8l?$6)Toi|;S5_Bb_y48%*Y`;qq5 ze&@$bdjSv9G|u*iR&;wU>TGin2GQ|DZFg6oec{=@z-=?vYlhro6q1};Duab-cmINhFH3`k%4`2(UIqV(l`t{e5*quZ)J(D91 zi682|8L&}?u8Op7`k`+=^*Y!bB#r|-Kfk)!G1*HjZ&Ti;fFXss{4_z7zQ1#RWjnL9#+uMl-!T91d6SWLtS^m?J_)g+@>FG^SoT z>nULyfv5rHzTDr`_0aSokIs{P4ZZ|iWy(=@vn_Ah{Z+evE4v;1aklyW386p0f9{eH!IExa6wRINKFEtWcd&IrSb%1a@fJdNs?>@7Z1aRxOX&az0p8P z4;*;#*VWwpL{Rvj2Spe#<%NnUSZ7$8%!44zrO^)>Y4Iyu|B=}--Gl?n;DrVp>Mc!? zOe$faU^tF^`jybcNSamU-lO67|N6%;$*aTePVnH_kUN<--u?CONQGdo*Zm-iBBzWG z{XmT`OeBuTPcI#$pX|4BW9Qj^;n3}VVnbCrvZ?W-05iDEQ2=qn7Mv_ zSi^$S=?gZW5aVNU^(~>>$AZTfUt4RPha3#>AZIinSiREeai&DSh_Ivbmk-M|#9)b1 zEJ+g*=kW2buXm`13Ea-GQk!C7VxXw3cURlXOIaTbt?Mz+k1n}w*id z3NOO!^=geHnQ+!+;1f!l?Yr{orvcSA;8zMBZv1Ek7FE{`;D^FZh|)9Sr?AxhXcLAj z>SsJW!x06?7t&8)so|{`!;s`>7)-ZXuM~I~2o9SNC?{~f5>F8WBZb;h49|nm z$1Vn2zkn73GgT0n>yxOb4|{u1x;61tq?Ra7n(79%uD%($+wynW)ODQXK;>n1z;IgM zb3DNWYK--Y`8noVX=H|Ai*MoClX2})92iJ>Qa2@B_Mz`7>NKaYYEhq>)9SFDoD4K? z{V))H-NRi02VHcY=;Z+Zt{{2{f;5P+fLP0~lJKze4q;0&#>LR16t-=HW;~99sw^LK zsE|b!FS3M;;;gTl7DeRs>HYZmbN4B>V=N9RdRsHg%#2>GU!l6#0Vlf)3>vmZ>B;^z z3yZTnDSV#}gQ|8Fg#(>p?+Va7V+`j%h#HIn6i3AF6gj=>o#CY)=sQ@BNxFYz(&?PM zu@UL`|4o6T8AIbU)M*)dxQQ(WVa)i`Ta}LBj`Ao=^K*q#FdE#HF!El!ID3EDm*tJm zlQ2%fYOyhsW`Bc07&Ai8yF1#{GsU1|yk1SEKy&8NjIPELVjaa%Cv_Nbtl^WQEUUh) z7z2AY(|8?C7)g0uTLQJop|LwN3Rw#*I88V8QNss5xM0L9w|~DS2e$0P{sl`Ur-COF ztN_Qqsv0btfkP>c#ZWcaCO)jsUr8_Ee$-SQd^!|>tC}E6!FXpx?15iQ_Ja9ow#vqt zA#(h_(Wdk56T+1;Vz$$y?{BuH%oI}|p1=IP&*&^sH04{{ zL1Kq^41GHmu<4^1{ro1uYfp>KZhMWU($ZD^oQi%j$8QO~2XJ~Ym@tsBTbsmj0%s?j zOXN{ijK|F5qPi?wolD_0)zwrS`JlKNdveTvYa7mmiF6#C22|B{6=x9`A}H@F1Sb)W z-jKHQ2<~GIb_$YO=;v8XP>D>!^0k2iiu103r6SLdsh=Y4(kU9VFvC&!lhWC!Kl#zZ z5?_9OV!|)K?~Sn+j|CWh2{)l1DH6iRQGE*!8-y9Hb*4Bb_t0L^Qembv$%FVzb6sB7 zJ^1?yLp3umXc)-cfTVsPUhMt_wprkWq^P zOe#(#%0&=i=9!84)xT-y_*AnT= zYTswD(t-e$xjYsZtJEyNVZdQb)|}KH$hs%zm$(I{KnmFEsS|`n!dYNKG@MZmYtbWP^`E5d9br_024eT3&9PTN!L%C*-A}0x&SuO>jHzDmINz(9eblDZJbd=Va(Uh295G&*;o`_FL?AaD_$l^$isLLIO)-2tm}ui5pa-TtpX-{ zk)z=Wfe@7f^BgH%D>znAl8kW_k)zvD&QLKI#@O=%>20U7?|^+)>bTUF=U)Ps$b!DEMFT9rPPmK1W5r-@X&#w z)7h#dup0azge3}l9~LLPcy)MZSQV+@l-WPvAP{QhB+t)^8ud-}L&hilT%3K3#+ZzB zuk|#g2alW(%(AX)Y>$lhSwczkAETIj*EGZ7ro~oj{qVO=o-n!V@q^f03k{aDLnn7L zT(2)Y&RZ)~fcoSuc$S(@Uc~{luz2O2q-{jI`+nX|sLY9pY4|vZ*by!AVj&Y=A5N1rAjz2!iikJutY?&}E zIFF);V$C66vX z%TmLo5ki)`EA3%7Z`-!Q1AnQ&Il9J-`0n-U=FpbA!^Oqw&6^weu#ulJuiah089Kx4 z82)-1ZT)6L*$;mH^7pXJ>ixFuV5eZ=8BR16W(P?UMzKKo@-T#z!MS8W`?PCYElQj{ zD@`El^TyBO6`?U@F+-L12+Xo5#Fh>t&e0Ejhoh0eE3zYkg}S)dT)zK@oskVc8n9J+ z)__(YikpnVYsRwttgB0$-Ac5+Ef?`=K|L#6XioaUGtH8=#Kp%kXWr3}_a5^K%c#*5 zu^&@b;G~CfvD;qT$~XOU<6FI_t-P}g2csR0p-di)1CM5YS72de+3LKnhq|oI#ehYM z<351maX>bXz(%6y0XU=yiQ<@Z?Bd^HTnzkBGjPox#CVx-dZ&;fc;H~j-J<)C!lw*= zlk0Izw$TRejFl{K{vVK{`7l6gutQ+~@FQhj3ha7dfWDg1Lhc}guyXeFw^?+;5jzAX$Ix@a+QB>GM;)rt9;Dazzytce5+910q6jL>uVC>;KzZQ-IDDx{3~ z5=WtO(c_?NxY%G0!$AyAH#j71wZ}3+Q*VA-;PXo?AUy{Iv^v@PCc>L-F4|h4r~{*` zEU_Wov3nDX@A5DLqcY!Jz6C}j9^brtRn-b^B`S-xEgXawuUBOwo2GoRS#2+OVH$P2 zLwR_sg*mirFU7a-xVa^LgW@yiFMo*Bv)%3zyH^E<1kp@3@)_m6jIn_TEck%=Itmit zT;?aJ0s!|>Vl4+6D`w8P){VJf521SsuZuP=H^Ytn)Ot|xlxB@$ zjD+gBEU%zG^{2wlhIlzl#pN~=&i^>vhq2G^HS)+WKrG^rd;k#MuXO*IZs#4*oLrW7my5d$| zz?aal63?J8m48y0vPII!#e|9rJ)*!_#qWKy7689v6&ISE#4_(?ZnAoZT0XYuUCJmJ zNe_XBd>905El2PZAulWNU^^-0yt2@Lxs#`txbO~-Xj0IuSxf{DYjlRZv5wu#&E>}< zsx3^5lu>Jcwtgj-jeVx9cFlD3&BdGJ(|w(Ef19=AC47erR&MuOEpsnVFYm&}f~)eS z$xWTrYTimJBv^U5B2J-INz(+64V@9iOhUC0lG$uLV(z`7@p+|sN4m*V^CZLyt54zk zRQ3e3)&rb?tAY%}0WKodA1d6nBp?N08pcp+Xua=0M52B14g48kd%%#D_9dg;Mv;V? zW+Im8%Lr_=FbtT*_x-+_mts1`E$O8vAOnL1@v)p)E+&=sd{ok>&kQIWEW~L^)XP^G z&-)lR6{3P-m(J#Sf`mIUC_{Ye29c0`wlE(Sbef!)``m#xwH;$V-Cq2W$c{2jEgIQ3 zsYkzl+zI3^?v}U2r%oUXHzG7LAkF#ttq^gg)Fu1&><0;>M_l^(4t(Js06I4FZP$>A zG5a^lFWwx+$TiQ7Wl&mmL&!soRk}6aK=Bh;VjlI-brDM~)M~>+yXnnte?)KQTfh|q zDQ$&P-8$isIk(ydY4WLvQSnx6PmFL}o!eJ};q79O>uwkE{VCT)kT}baS z!~gzKWJ-f%$b!beAgKyTyG^Eh}W{0?Iq z;tl=y+Vc{A!uP##v)1|IW(k$B*s0CfkK&YG6v5zq6P50`0x2v%_^3z{qT92QIrMTp znQLbjt?#h?6*{tV+AuDG;FkTedJ#`V%MX#s*!`7{BiUT*+_6|HS};-6Pto2?^5;%pJr#vqyAQ7xYW8p9`w^xmH zg|WRSS;1Bs0v0c))tO66n)J$-u&&9dipoiXU|o!tFls}ywQ=`)DR_D746i-9N<{L9 z5B-bRgGmAIClOfGL(1PV^UGWIIq4=0rhH-EF`$x8Z%UCpuGLaon6J9v-(R(&fY(55 zb`M^ph4zlFTcMJx8axRGV8`0yh9o0WG5nKXA`LwsZ_g7(>gN0`y{}22wcU_4Y^b|k zBUy&Qw!9P!E4iGH)vbOSS(>aT2CH$Km{4$kRnQ?ZH$rMX&eVP7SP}D1rq)rP*5&mb zGI8KW7xeEML_I@UHdYf4SV&nRvN%EDG(GLP1QHb;zXdxZuQaYx!^H9!`32$8 zXC7AMqOR+$+>$*@N}leMHZM^E@NTwpsM2#K4y;-6QPy5^D}h|MmC8|?p^|YiamSUU zAf@0e#`GfL-6|kGAgfZ$a#?F!%Uy1lupdh?iA=QTNokYLDdI@^A%?YXq{IC>FO+;j zm$a;%DtpzqHSd_GMl{Uoyanq8>R$}2%uDud#;l#L-rU?24Idd8H8EKY6)%@z?M_Z& zDy1!Umi~ix>w#7*oHU2G5IKd8vtls`Vp+rp5u&NkL0m@I&fR5q-_3I>fWJfF!Y(XO z%U75l`Kw_RZG?emrA$^>Dn?dZ6w!s`JHZv$N)?WNkHaZ&cq#bvhbTThv{HnOo)!gM zOfhu%Y7joI;_TACF~psdkp<|Yda^R={V#k0pdHvR$5haD2}r_?W{cTfK&l5uAfQyy zx{AH-1PAKMy(iFdfIliTTcO&1&aV8ERUShe58fE;@`!=3?iIw%PYWyTdXvC}tG_A8!u`HkL~p|ARZD&{k^i~TBpBtB3yT8|Z5A#)*OXXf zVBbM($EFY43wfRFeTlo6^J^oM{6~U<-6Iv>Nyyc_tBBSUPW>hBdhpotYd^<9zi0wx zPVdYPqShue0`~$9CrHgPNT(x6)U>%*^7Eh)E$jb`n?Pl80>g+?7lr|-|X0@{_UZsc_>+u zitw%ROsD6**vc-;gtv`-Ja}w(|k0 zUv6fi+GlK;L_;GjBg_cACh(C#RIc{sn~k-R<3bo_yzt*nsalySkD}b|s|o5jz9G01 zWteZIe|LWMpHXJDXZbm(`BwSOH0>-7FQZ^+*984SKF>hv?mQu<#y)v47eiE*-`9(QMwF1&(HiJst31EIJnVaBAkq z7L;`O_%FYqA!QU9-^279is5LZk}gBK-;4jrO)sLa<(qv;k%?ANFXc9Dwp;pj(O5R* zk~Jfu4J1ki5RVB?Z>uc1@~D-Fz!oR(e{z_;GuT^*HXPy`LV$N!e5t>=xXE88 zWB(4D3&WcIPX~m2ZgJ@pP+C9@gPc;rICceDCfpBR!#CdPu6a54%uepb_e62ZZ161) zO#?v#C=Ocq0B*d_Hmo4HpVhtP{aE|lu^ikkcLxpt@9xgcUjrIYtyZFSxQNaI6AledyCc=3nMP1*X5uY+49i=SUa~ z(7h5#1$H#XPstP~v`#j|OzlRgP)e|c!TqD%otzDuO6I>K`I$GOpDbJNe!2r*2XvYn zhrl(M)GibM^20G%Fxbb**X%koRr4cPS#92OJXF?xnC4nf$1`U)KEllAC5PC%C6*sS zygwy1%w2g|72o3jpp;@I-<-7gM}-VLm7WBCkJtJF^<>)1B3mZ84`{U4x4X-tEXB~@WXZS_-=ed0$S&CKZpBX?mJ^A z2a(_`w8v{7l4IYC;`|uWOO~d7 zAifPCaQ}sqi9){+#v*%|D?6J-RVJMJH0JkKMPAwikB=_K0H*FVF`3gxT z{36s>BQ)?xeMmd#fGIda7=)o(UfXA^@3@7IYJUY40fM zSmgp?Qq2m@eZF(NzugUoHv|oUPx!yOC$K+}I&Uo%47J&5X5Q{zadeW{g}UA`RBj>n zMyWPq#pFZfA^Don7n3>r%Z(s0?t*Wz{IEu5 zznHhRyEJk{8X#@!!+Ci9@5#sPeJIJ8syXc4zuz)KiN;hx;B||9aV{1~-$%fUPFsw7 z?u?txbJiE7#f7F)CP(=>9};t!e~1G>UyK*hh&-@pevhyi!)nfU8ZirX*5upx65Gxv zpZD|blN0y5^SVko+r5bVHiEFOLChEcYB_v8Nqt=y^Nz03lZ%jqLF^Cyuc(g;5Pxn+ z^TF2>?|Kn0ZaLDSNK(?-Oid9D2vHJBqkQ1CsiV(>2|LgcA^C}WhR{PvnOHrXWuJSz zzDV*+o{I6G>m=H#uuH!b#a8(3@3?L#C6k+;y}4DdL*-A@igRS|XYaP-*-nY1d$U*< zPta$a6#(NIdIkGRow#86U-`69l#24(o{S87W*LQtHn~!Ey6!YnMHEB8ugLMf(h9cd z&@8!gf_Zp{31nIhGssCYH8VmS2&U%_JCm!T&L@q%ug&$x?YUc*FFN4L-O)JkAVEpvC!nSIR(wXN8` z>J56mQ>XfmtvjU7q3tGBq{m@Xj|~6J=^!4z$2RqV%E%18SM|#q?t9kF^+DZwSM96e zB8S)fq0rIKhgZOgUh>7(wylb@ZtseiIo-~32v1z^RfV-6{zVOW`3CD`z$^TElksxD zGFnsK1)k9_D7gZx74aMO0wtvBV4URov6o{1&yI6!th`HnkR!_{p2T)cL0Z(bWC?6H zcNEHOJbs>`{9^GNTUK_ueAEi%fxXolRV_3;{eU zq+m>QR1O5Hpw9~q3x5PEc@T(w(95ZGvw`X?z;AZ==>%Yw;vMpd?gOi<42bet14;aL zz}2oxBXWP^w+PXP?~KRW z^YC#Eo>*@<=jo{9D3LMjH+&06?Xk0__0Abzp4pG=s#dDnZ+%8M2aQqj=E4sG>3$o2 zx5&`pmjt4I{Cd57x-fAHE+P^T^U$Ph1sbwa)i7(~W%i%{u;+;W9JU#>PBvB>oj)3W zF)+xiLaN^Z*fP-1I|wA;e*Y{JeQ4S0-E=%x{N2*=7^19*!5GeW6Sp%~9`#I;^EG-6l zHEWVPsS)d6nrO#?xR`pYCr&QM)DrdDjk(27tKV)@!F7h|}SoCoQ4+e{D z?);+1ksv?fAiOelLBBo3jSA}Bj3b)F?l#_LFC(4{v1*Fc|I({l%K8qSITD%DX#Ejp z2TP=x3E$^MyI(TsBJ1VkO^?=$1M|6V(&=>{*YkF^HeYYIN*ph@6Xah!6WfPU zXm)J`n5N2#N-%u3IqVA>ih!8)lXWW9LV7_gDOGG5cqe7H?LW)S3K~{`)J6c19sEh% ziG#gNL*~mgp+-rMlWX5kL=Bm1RCJ_$9#Spqo*`5BiAKdblLNfc8BDMditMQt9-PpE zF*<$vm*{o8G z!%T9o4%~X$rX3d{YcBa|K#C&Bq7~l=vO-YCd$)vQdk-Ti{xyCv|IV}Lj5PPlJPMw; zXPMCYi~HJX9T)o!xGAt(+N^tAV~Y%-BU7-?V*K%>>3bvv!mw^mx+5ZfA5PdxSO(ZY z_@>{{^-A_w)6R2&lwB*2F4ctcx!rzJnaohc%R<453420)tPU%d69ErsnUN{N$2Z_c zeq#!M$no+ijdbKA+~_D+X11^u=&qyJu_m5vHyGx&WMrZVCCyqpiOb3h2i_aRhFZIx zEs}vFGWR1IzmJyT8iE#)EzW2i9dc6NG(!&@n2QYd?g&;0-1TxB0E}r50=bE=6_{nQU zT<7Gc`;!6pxKH=&sZSW%FIaAy&Z*xN$8JCpS2C zzkWA{*B&Y9)FyK?Np1-uxu?`O)8p3E6kwS67fBCem?>M^-ij)Y(3JNoK>mC-DZ+hU z7n|qffu)tVf1mBS;YLVSZ9!)DRk+c>`pmOlGR1EXsq^K#dAkG!B3|^U-?uYK9?0J; z2`#wqgHNFMq;wR@5ApfO1l^S&mbpjdlz$>u;;uh;fYgxH$(@40bEoHwOZMeG9+>NNFa*0F6~O0 za0gF6()^M2yJtoy1eeBYX?`pjxXCl^pj1;Ug+n2#(&6EGb?)Flm=;rvrx3K~iFWb` zcZewmY|yzUi1)P3(NG=l68t&|Zk4th7fy$rgR@QUlH3R)XVDM_ZYz1Gup(K*8(9dF zqORB9bFQY%qrkA6$y-G#k?{it@jYmyB7H(|9B4v0#tj%a$h{kIBF2^@S7Y5dv;?JM~*2!QFXWu zUq2qho}LjaPqdiK{Oako8W9aal<9dTU1)IRwEk?gR2?*KkB8N|l`x=TE6gr+9D4k7}jCD<02@Uo98yMa+{fL^Dto#u+_bJso&cF~1ynB8GfQNss?N z)KfV~u)&8YN7GOUZ)Ute!u+}&ET4=V# z7UbF7JpJ=G+V-Hk$amw*gKGakO={n|dcOjc|ESe6G<+qKvc5pwz=>{| zQz{vo^Gy8hn-$@VyQ_$caa?Sr3a3?WhNFy;M4gj)b7bvj+$X{;>a4JeVOCX-R%m%R z1%yzzTzZGzi(#mQrav7X|3>#R!W&OGlms-?kFAi_jO`9&=C?;l$G^q)DmFadKdEH) zXDQ*obOXXErVJj9p_?TcIdLOFZQmXvU&srVU!!5vo$%+wp_!S@V;YZGo)aby4Ju>Td^<-^~GD}V=Kph0|yfDTr`Dk0w zY}@p81=m?Teb!WdkrlyBpcDh>)v-_-CcOWL;ZisYK4j&2@UN@JeppQ76d1c!Vw74U zxMz`+R2v+fyob(DGFtqadp-E<9L>O zbeBA$))2awy(zh;^!G`bDm7X^sz&800@sVah)?$FC`E}czQi&|(XH>nxDy^kR4=R3 z_Hv*)L?{2GkbgVM3H(dfh@R zVN@7tRfa}>|Czga#S5yraUyG}GuGqkf&~m^7tvR)$Bb*Ztv`u-P5J$Y01Gdy2fJVY z=%ICKgOs4@QZ_WUgL*(k=v1BCE&_%2^43W*|L*4Pvw4;d91?Udyqs8}CWoG)F9O;O z{T`0@LEXNPrS}e>hoUm?$5Z3yDEI_y!1)ZJ@lDGUd|p9b7|`*Bc~Vn3V|b8J(HYQ0 zkBn<7s`3fvu9FSvKxbzInV~n^A_m#k>(R7SpyH|-94ZsxH%+3s_KB*_);F$_nPO|B z19Ne<+acu)*ilnVmeiVEh7V%QeFnc!#exIv(LR$GyS-CW*&=>rE4WN!o2m=A~X!!!*imz03?^~r-K zJ*}}EKF45aFzt75hUcHjQ)JFf&$l{R2ZC0%Fjs@*L2WC+=}^4piAHFG_>amlX7d`6 z#?4G;a*k6yix@k{1ID`NWoJaB=6HCSXKps?j#3gVT$4i}gNi9u!9F{kwo=0F?RX3Y zS38wdxrcK(1Ks(YLCL^?N8@eCuriTH7{eT4QRke2<#nJWLfs=sn8ZvWwkxVAP($d) z^YiZR@%7j7xvlqcvbUS}r>#k26kI1&J|pS)OX6aI(WwK)NmZVbaNt8xYj6x=+Fm5U zT=-M{vt0j=q^e;CwVw9m@AHSsyHW&TJr|g&Vq!)s#$>80DIS<Gcw~`flJx}r_hY=p# z92UX5%b-6a(YH93>37Xr=UiRu_-Tr#LLdpiP|q8SX`%C55vg2UnOnn#Y|vn$gqs@hZxf$jgQx|v=Hv^dDSQ6ebL?m;tI{Fe)3B!A zS`Z?abPM{W#PU4ORFj!8KFxBcSTdY=XVq>)z{5;3pZWrHH>~&_Jx_&Xx-sYvflrcM z_1h-#X26*qww3-8)}j8RI9u_e;VaI03&R;_lOQ_o64uKqJF*cK%?a9Rrmb^eACFZW z$r5z{8Cs%oV-RP$&R8QaC5a?H)Ngr0WHgw|WUZFXj$KhWo5R76Eq2#`@nDvHu;SqN zzSh5>-riniUxgl?eeY8!p3`w#00RkwXp|W!wZXtakSNc?{n85P8ia%S#9=8RU98n> z8=2OShqp_`c|IfKlDyd(*>G$2m7t<^bUTtzYIziLQpY^`#TW!(E=~3I#USsVb1O?n)Aw@ubg*e&F_7TzLrX%PD8Yl@$IHp?kv|8n_h%ms!fw)hJ@ zXxiHxzG|5&6U%YK>7Wi;Mn1eLUfP=!0(}rr3Tg==nW~%jqrP;{F)(^FQMo@{b-b|D z#Y~4NL#xri#l?nQ^o0$?e^sm=J_!&%O>88Cy9873EcgkyKpf5z;spX}^`sT_VY?4c zxbg(~mupLm%1z`r?mOU+Ql_oPUO!r`(+qDYDh3Cpoi&RhndbQFlSF7Z)kV!!kc1o) z>0!4CpC+8pxYa)A4BYqwP+t`Xo2=15Y1hr+4;)ECio^{mx!GQT{XT zbAwhkpV-SK*x^LcBC~#S5aXM9_tQrAKXUAhw{wuc8Y&8b|NF%|V%8T0k(UF*x9s7! z^JC6PF~YjzSE8CZcTG)&)+9nj01T_#=kA_XUqYU&M%yb^{9ZMLLpm(4jKgwz5paoF z#q+CrC!}=qZC+EeOvFeO4jEZN_GKAEdgNt?eSDJ_XZc7y!^|lw7$Y3K^ZHjlw1fAwJ36YQ^)Z_%QNfuub+Jvn`vNmj7OJgMiNfV zK6YH%%&O`e6B2V}-(}wLhx@v1qlsIZ^@^%r640Bsd8@DeFL^#uOlb5)t6$gvYaQPX z1}mU>c^GV)q250Ad8q4kL}}vX+bA?nXX|{Ddl+wz(s;?Ma@$5Ll9BEWK9caX?6H-v zbH$f1dUI14O~;nx)klXp4e7G$r&cX`J$3~2cp`Pj^y_gLg!JpqQU<^CPEJX__)H*E z7VO5@oW9TR$q^wg+VZ(Y!O?})QA;C}xyDaLlELsX0yNqXv}!6buylQP^wua7Tosm1 z6CNN{a# ze$K8lw#8=YxhA6Gf)Wf7c>FbcT_f=-Vz*i?GKpiPcn? zB1P|G$+SgK5f2q3zL~lP{`vj(pG zm6b(93?7T?et=hncCc;JVN&hfJSWl;=TMoVlT>mh=OBtWwB;7BQ+QL1VUFmK`I$hE zp0ujQ@ketT>{{ntm(JO$5 znH8P>P=b_9Q)_aimjjP-M*CME5K*hR6`O0~0R2BYUV{|b3Kc3gM>rIcoY@&xo4y_8 zuk5DL&19G5PYL}7U4TgrvDOaicFojNiS1*ut0Ar8=ESZ3d#dt6vRn6YzLbsS-Hm}>&jrex*3w#8dI#_=>25kN60*=QY;^wED z=_li8ij(nsCsZyPWI5aUQZeXqaIgLJw`#~&%*RnWn@gm6vYys;lk{qC6IBQf6>f|s z`%5Xj?~BSpSSo#B@l?*4 zQo0b2zp)!7?biy+%=@pA*0v)DCHGEbnV^0}q?zGX!sj$>ulc{I;+PXJWI0yR2$ z7_Q%mTSaa^>p#kZJ!mWVPXm}noe)~z_Lw-!oL177Ilgnfp=|A3dHLq0FX>(M@N|t| z$cb;>bA#M)4>HilBPpeDQX;oT=|k?-rrw9w+P$8jOR_;DKi0G|rwm&E&UqjxiwGqR zlGt~@ZYcpWQv;bV{wN1XY6MrD5B^0mYT-VJa$;8uuc}9Af0Ec5di`xPpZ!8|GmG7K$~z zpN3t>aq$UVa&FHvnd2_~gd{|^;N=bv_^Q5=bYpaypAfK()OoJ>IoN1H*Ib}gpB5s` z=bTKOhGX?l`Cuu`GGn8O^?|G;rh_Ew3WjlwFxGt~_C*-w6DWx~_Ep$T#1sA96o5QX zAWZ-gL>9(C-a4cS{2YAyWT@qh-WS#&5wap(;f>9_0&WK6iO% z-W1n&1ZXdLe2t)I4WvnYRO0=YB7n`~`jYAN_d_4y)E{JbVC2OyHt^u`UJS2I=Z^S?N7;0j?Tm%HH93t2rEI zr4!bXn{K_W)$n>E-6KGxarQo$?)w+%SUc^`7f6uQ~?rtocuzGiLpe(kD@ z?Kt*GS<|(YK2m(PEes;6QLz`Gqx^8)j+50-<}?=^4u5xCVsNBzFOW_pwyJ&c?z1P% zQ2m2=jFB5tpYG4Zwfa_*uVyx{{?>5-53o0wnPmzl%lkOGi(l`(p9o%^SQtWz8P~5_ zuPLhKkppVS?r==vj_+$S*gBSTNr4nJ^-(j7TLu(#7X`#T>+GFp-+%R_^$dbBwnm@n zNzbWTlFhr%{OG4E7nN&@Bxq}6ib_?eqi8X-n|(6qh%$WkUeSXcME@= zEKE-*7w!;b0Qi(}t!r49oNTl|U*dY}ZE!xnEcaN}BRaJm_$|A3;^G_^r7P0srel%X z8mR8Y`GitED0LnW9B1UPx3k!G3juCX#FBu*AKF@Scih&&CB~mSKMy|3f1yP*KD#wT zIazOt;@T+T*~A;WuGd*)zI568=N|5lY~+8OXN7LsQueUq>UnhZRuJt~(IzS`?j~-? zw5D?LArYGd;m%IL#FCT=QKk?&?)zw$z zrX6i&Vn|?$yl$wSPI5v8#Xc z^NzIQ!H*lgy}b#Lr0pkQ0MyrFE=(VNPC5dvI_t)q|l( zcP3`3n=S1=oJD{WOR^EQ+>)KH!l-&OmiUv=(7{Md_M8y>I!U(x008*uPdI-bd&f0^ zeoUgyFoCVNFiIXNhU=m)5*1-=8pe45b|@!B5h@OHcW3wy;#Lt3zueddiTAPud2(O6 zx(cYp=3OowbhzQNoqw8z3gK(g=c_oa|8pILs$~A8!O1xN!)EGXR<6F|3=W-n2;Cgv zom2L}zR2?}3+#@F7%mmuCU?2;`&b?R_YY_ ze%5ltsWm*0G@aLVdp)$^Qk+xF+MEW`ay2fC<7sgf%Y$xIh z7wS0pX)NG;_-HeS0O17iKc>G7FbFuGbA+W5mIzM3Y`cJD7?Rn2rqV&EY>29R=L#h9 z=Zhcgeb5(#r=g%bHSk0-)J4zA6R)j|qb>5r7)AMZza)y;DPzhWT6pZcv2@<>TI z9aFdEf@4VJO*2|0Iz>I4QbJ`k+|PbwI{8rNhoG>`Qs2XNS5#Ftp(;{|rqF#fzw7PY zxk($@88=k39y2Rp2sLfOPK+;UX}Rogs7B1nXQ2VcASOt)Z!8qz#?rMrF`g1TFU9@m zW&ep3@GEb30n)T*GR>$Qj+J43ptL9cs#|oanp4&QH%EJUu#2Ok3yAEsFzU zBAZT<(ofJoEN(rQaF9qWm);M(mu!^eldok_z(nE-N#*~du`Cr2%m_XeQgxMOFs8Wk zmFW9Yua7=_jh`yz`d=J^tQQj(19Vxit*V5`c7&;>#R}Omd3U-pYILYa|Ala5U?9XH zkSKuGV$RVV%pWERgs2=^13zZ_G2Jye1$^VKVX7$~Q#bZCyKh7HMPGlq|JO5WV2Z#M zFlN+4&r=L7n3PaJZCan?zi~Q!vZ|Ag&zyQ?DGfWNCCJ%++T%e7bc!jr{NKoz$85_l zNqNSo@BSukJ3~(YBfU#=S<%d-=d|ZS2fYk0=a|;qo%es6-@+>)c101uXD+aMI>6qJ zGV;Fxad;XaLogsN-rFR8RvI$Z#!;2{zesa#uao?n?L|r>NgGpQ0A1LAjsHJjPhBJ-eFLoHvIQBOtD%opgv4SLh*Z*m{HLG0ko_L A?*IS* literal 0 HcmV?d00001 diff --git a/client/tp-player/res/cursor.png b/client/tp-player/res/cursor.png new file mode 100644 index 0000000000000000000000000000000000000000..5728f4f4ae6dce7fd6f91a5873b42fd43aae5496 GIT binary patch literal 1225 zcmbVMTWs4@7&dGjBcjznXbjd0%i}5*+sA3_+G-e%9j{53wn~DUah+qIrf%vBwzH%~ zWg=XpsS_JiY)A-H(V*bJXCwYvVG3^ z{eRzo_x^#tovoqULKud%CVS)zS~msH#trCw`|np&wA|_^@_yDH@++zfF;TY*5Ko$F z5oVyOk3Df3c4OGOAtRUf^XXlJW}BoM=#W*@L1+x?-cfZ_tpt6%0E>ndBYyqhBLX+{ z7%>=4(`iS7!$!}z3$x>WIc>b8@j9{Ne!ROXAOaKmDqb~5El;S%h-F>@?E^7I;L9q0 zDMqX~l}``glI=n~N=CvO4FJxvBw z*g~fmG3@(}Kv9)Sg{(wK+bvRn=lOtxVZulw?2TEzS`Ay?wkCrNJ@w_U{nis z*^d#()0GfRC!Jm;w!B87P|B#P>QI2BDboz%TGsab4E)!PHMPCmm;$p?VB$ zYl2bku5Kt$MBWJfu7Qf8j>@)HHlgJw0Dweud;)EFB`!({g^{Ci zfFx3rV=b)2$P(MhMG{QeQu}RDGRZD|Sy9Tn`z%p0G3X%&| z-*$7hJ=*MmfnnRXy%Ci!!3|(U{)D_n@@#Xvq#v1lzF2W|b z2+hcx7{o2c6+YfYGaxR?QiP+423G$+Iirv>RL~s%X_lr4bzrbvZGCiD9Uf?*iE+_r z>?`fwi^ggxDT}%4xua9{(5LjK=f8gFc*i@vlT)wG)Sl^|K5;R-G4$cn(=$`=UwETq zM4Y{H`b0Ze+`R8k{Jry!zI0~sHR17*+Ln3ofrs0^_;&r=xwC`XEx!2uS5u)5XZ>}* z9ct-sxtyw{@BE`&zVh5y~k{45jla(7UpGrOW=7FX8lkLSb2QT&R{A=@1_RXKY&<3_{pN{=J fclTpUGsm!X*!dJg{&?xaWbkuICi>*L_@3H7ta+gB literal 0 HcmV?d00001 diff --git a/client/tp-player/res/tp-player.ico b/client/tp-player/res/tp-player.ico new file mode 100644 index 0000000000000000000000000000000000000000..d3fce7a44aff8e01e3bd5b95363c6fe11fdab784 GIT binary patch literal 16958 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 1999-2008 + + This program 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 + (at your option) any later version. + + This program 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 . +*/ + +/* three seperate function for speed when decompressing the bitmaps + when modifing one function make the change in the others + jay.sorg@gmail.com */ + +/* indent is confused by this file */ +/* *INDENT-OFF* */ + + +#include +#include "rle.h" + +/* Specific rename for RDPY integration */ +#define unimpl(str, code) + +//#define RD_BOOL int +//#define False 0 +//#define True 1 +/* end specific rename */ + +#define CVAL(p) (*(p++)) +//#ifdef NEED_ALIGN +//#ifdef L_ENDIAN +#define CVAL2(p, v) { v = (*(p++)); v |= (*(p++)) << 8; } +//#else +//#define CVAL2(p, v) { v = (*(p++)) << 8; v |= (*(p++)); } +//#endif /* L_ENDIAN */ +//#else +//#define CVAL2(p, v) { v = (*((uint16*)p)); p += 2; } +//#endif /* NEED_ALIGN */ + +#define UNROLL8(exp) { exp exp exp exp exp exp exp exp } + +#define REPEAT(statement) \ +{ \ + while((count & ~0x7) && ((x+8) < width)) \ + UNROLL8( statement; count--; x++; ); \ + \ + while((count > 0) && (x < width)) \ + { \ + statement; \ + count--; \ + x++; \ + } \ +} + +#define MASK_UPDATE() \ +{ \ + mixmask <<= 1; \ + if (mixmask == 0) \ + { \ + mask = fom_mask ? fom_mask : CVAL(input); \ + mixmask = 1; \ + } \ +} + +/* 1 byte bitmap decompress */ +RD_BOOL +bitmap_decompress1(uint8 * output, int width, int height, uint8 * input, int size) +{ + uint8 *end = input + size; + uint8 *prevline = NULL, *line = NULL; + int opcode, count, offset, isfillormix, x = width; + int lastopcode = -1, insertmix = False, bicolour = False; + uint8 code; + uint8 colour1 = 0, colour2 = 0; + uint8 mixmask, mask = 0; + uint8 mix = 0xff; + int fom_mask = 0; + + while (input < end) + { + fom_mask = 0; + code = CVAL(input); + opcode = code >> 4; + /* Handle different opcode forms */ + switch (opcode) + { + case 0xc: + case 0xd: + case 0xe: + opcode -= 6; + count = code & 0xf; + offset = 16; + break; + case 0xf: + opcode = code & 0xf; + if (opcode < 9) + { + count = CVAL(input); + count |= CVAL(input) << 8; + } + else + { + count = (opcode < 0xb) ? 8 : 1; + } + offset = 0; + break; + default: + opcode >>= 1; + count = code & 0x1f; + offset = 32; + break; + } + /* Handle strange cases for counts */ + if (offset != 0) + { + isfillormix = ((opcode == 2) || (opcode == 7)); + if (count == 0) + { + if (isfillormix) + count = CVAL(input) + 1; + else + count = CVAL(input) + offset; + } + else if (isfillormix) + { + count <<= 3; + } + } + /* Read preliminary data */ + switch (opcode) + { + case 0: /* Fill */ + if ((lastopcode == opcode) && !((x == width) && (prevline == NULL))) + insertmix = True; + break; + case 8: /* Bicolour */ + colour1 = CVAL(input); + case 3: /* Colour */ + colour2 = CVAL(input); + break; + case 6: /* SetMix/Mix */ + case 7: /* SetMix/FillOrMix */ + mix = CVAL(input); + opcode -= 5; + break; + case 9: /* FillOrMix_1 */ + mask = 0x03; + opcode = 0x02; + fom_mask = 3; + break; + case 0x0a: /* FillOrMix_2 */ + mask = 0x05; + opcode = 0x02; + fom_mask = 5; + break; + } + lastopcode = opcode; + mixmask = 0; + /* Output body */ + while (count > 0) + { + if (x >= width) + { + if (height <= 0) + return False; + x = 0; + height--; + prevline = line; + line = output + height * width; + } + switch (opcode) + { + case 0: /* Fill */ + if (insertmix) + { + if (prevline == NULL) + line[x] = mix; + else + line[x] = prevline[x] ^ mix; + insertmix = False; + count--; + x++; + } + if (prevline == NULL) + { + REPEAT(line[x] = 0) + } + else + { + REPEAT(line[x] = prevline[x]) + } + break; + case 1: /* Mix */ + if (prevline == NULL) + { + REPEAT(line[x] = mix) + } + else + { + REPEAT(line[x] = prevline[x] ^ mix) + } + break; + case 2: /* Fill or Mix */ + if (prevline == NULL) + { + REPEAT + ( + MASK_UPDATE(); + if (mask & mixmask) + line[x] = mix; + else + line[x] = 0; + ) + } + else + { + REPEAT + ( + MASK_UPDATE(); + if (mask & mixmask) + line[x] = prevline[x] ^ mix; + else + line[x] = prevline[x]; + ) + } + break; + case 3: /* Colour */ + REPEAT(line[x] = colour2) + break; + case 4: /* Copy */ + REPEAT(line[x] = CVAL(input)) + break; + case 8: /* Bicolour */ + REPEAT + ( + if (bicolour) + { + line[x] = colour2; + bicolour = False; + } + else + { + line[x] = colour1; + bicolour = True; count++; + } + ) + break; + case 0xd: /* White */ + REPEAT(line[x] = 0xff) + break; + case 0xe: /* Black */ + REPEAT(line[x] = 0) + break; + default: + unimpl("bitmap opcode 0x%x\n", opcode); + return False; + } + } + } + return True; +} + +/* 2 byte bitmap decompress */ +RD_BOOL +bitmap_decompress2(uint8 * output, int width, int height, uint8 * input, int size) +{ + uint8 *end = input + size; + uint16 *prevline = NULL, *line = NULL; + int opcode, count, offset, isfillormix, x = width; + int lastopcode = -1, insertmix = False, bicolour = False; + uint8 code; + uint16 colour1 = 0, colour2 = 0; + uint8 mixmask, mask = 0; + uint16 mix = 0xffff; + int fom_mask = 0; + + while (input < end) + { + fom_mask = 0; + code = CVAL(input); + opcode = code >> 4; + /* Handle different opcode forms */ + switch (opcode) + { + case 0xc: + case 0xd: + case 0xe: + opcode -= 6; + count = code & 0xf; + offset = 16; + break; + case 0xf: + opcode = code & 0xf; + if (opcode < 9) + { + count = CVAL(input); + count |= CVAL(input) << 8; + } + else + { + count = (opcode < 0xb) ? 8 : 1; + } + offset = 0; + break; + default: + opcode >>= 1; + count = code & 0x1f; + offset = 32; + break; + } + /* Handle strange cases for counts */ + if (offset != 0) + { + isfillormix = ((opcode == 2) || (opcode == 7)); + if (count == 0) + { + if (isfillormix) + count = CVAL(input) + 1; + else + count = CVAL(input) + offset; + } + else if (isfillormix) + { + count <<= 3; + } + } + /* Read preliminary data */ + switch (opcode) + { + case 0: /* Fill */ + if ((lastopcode == opcode) && !((x == width) && (prevline == NULL))) + insertmix = True; + break; + case 8: /* Bicolour */ + CVAL2(input, colour1); + case 3: /* Colour */ + CVAL2(input, colour2); + break; + case 6: /* SetMix/Mix */ + case 7: /* SetMix/FillOrMix */ + CVAL2(input, mix); + opcode -= 5; + break; + case 9: /* FillOrMix_1 */ + mask = 0x03; + opcode = 0x02; + fom_mask = 3; + break; + case 0x0a: /* FillOrMix_2 */ + mask = 0x05; + opcode = 0x02; + fom_mask = 5; + break; + } + lastopcode = opcode; + mixmask = 0; + /* Output body */ + while (count > 0) + { + if (x >= width) + { + if (height <= 0) + return False; + x = 0; + height--; + prevline = line; + line = ((uint16 *) output) + height * width; + } + switch (opcode) + { + case 0: /* Fill */ + if (insertmix) + { + if (prevline == NULL) + line[x] = mix; + else + line[x] = prevline[x] ^ mix; + insertmix = False; + count--; + x++; + } + if (prevline == NULL) + { + REPEAT(line[x] = 0) + } + else + { + REPEAT(line[x] = prevline[x]) + } + break; + case 1: /* Mix */ + if (prevline == NULL) + { + REPEAT(line[x] = mix) + } + else + { + REPEAT(line[x] = prevline[x] ^ mix) + } + break; + case 2: /* Fill or Mix */ + if (prevline == NULL) + { + REPEAT + ( + MASK_UPDATE(); + if (mask & mixmask) + line[x] = mix; + else + line[x] = 0; + ) + } + else + { + REPEAT + ( + MASK_UPDATE(); + if (mask & mixmask) + line[x] = prevline[x] ^ mix; + else + line[x] = prevline[x]; + ) + } + break; + case 3: /* Colour */ + REPEAT(line[x] = colour2) + break; + case 4: /* Copy */ + REPEAT(CVAL2(input, line[x])) + break; + case 8: /* Bicolour */ + REPEAT + ( + if (bicolour) + { + line[x] = colour2; + bicolour = False; + } + else + { + line[x] = colour1; + bicolour = True; + count++; + } + ) + break; + case 0xd: /* White */ + REPEAT(line[x] = 0xffff) + break; + case 0xe: /* Black */ + REPEAT(line[x] = 0) + break; + default: + unimpl("bitmap opcode 0x%x\n", opcode); + return False; + } + } + } + return True; +} + +/* 3 byte bitmap decompress */ +RD_BOOL +bitmap_decompress3(uint8 * output, int width, int height, uint8 * input, int size) +{ + uint8 *end = input + size; + uint8 *prevline = NULL, *line = NULL; + int opcode, count, offset, isfillormix, x = width; + int lastopcode = -1, insertmix = False, bicolour = False; + uint8 code; + uint8 colour1[3] = {0, 0, 0}, colour2[3] = {0, 0, 0}; + uint8 mixmask, mask = 0; + uint8 mix[3] = {0xff, 0xff, 0xff}; + int fom_mask = 0; + + while (input < end) + { + fom_mask = 0; + code = CVAL(input); + opcode = code >> 4; + /* Handle different opcode forms */ + switch (opcode) + { + case 0xc: + case 0xd: + case 0xe: + opcode -= 6; + count = code & 0xf; + offset = 16; + break; + case 0xf: + opcode = code & 0xf; + if (opcode < 9) + { + count = CVAL(input); + count |= CVAL(input) << 8; + } + else + { + count = (opcode < + 0xb) ? 8 : 1; + } + offset = 0; + break; + default: + opcode >>= 1; + count = code & 0x1f; + offset = 32; + break; + } + /* Handle strange cases for counts */ + if (offset != 0) + { + isfillormix = ((opcode == 2) || (opcode == 7)); + if (count == 0) + { + if (isfillormix) + count = CVAL(input) + 1; + else + count = CVAL(input) + offset; + } + else if (isfillormix) + { + count <<= 3; + } + } + /* Read preliminary data */ + switch (opcode) + { + case 0: /* Fill */ + if ((lastopcode == opcode) && !((x == width) && (prevline == NULL))) + insertmix = True; + break; + case 8: /* Bicolour */ + colour1[0] = CVAL(input); + colour1[1] = CVAL(input); + colour1[2] = CVAL(input); + case 3: /* Colour */ + colour2[0] = CVAL(input); + colour2[1] = CVAL(input); + colour2[2] = CVAL(input); + break; + case 6: /* SetMix/Mix */ + case 7: /* SetMix/FillOrMix */ + mix[0] = CVAL(input); + mix[1] = CVAL(input); + mix[2] = CVAL(input); + opcode -= 5; + break; + case 9: /* FillOrMix_1 */ + mask = 0x03; + opcode = 0x02; + fom_mask = 3; + break; + case 0x0a: /* FillOrMix_2 */ + mask = 0x05; + opcode = 0x02; + fom_mask = 5; + break; + } + lastopcode = opcode; + mixmask = 0; + /* Output body */ + while (count > 0) + { + if (x >= width) + { + if (height <= 0) + return False; + x = 0; + height--; + prevline = line; + line = output + height * (width * 3); + } + switch (opcode) + { + case 0: /* Fill */ + if (insertmix) + { + if (prevline == NULL) + { + line[x * 3] = mix[0]; + line[x * 3 + 1] = mix[1]; + line[x * 3 + 2] = mix[2]; + } + else + { + line[x * 3] = + prevline[x * 3] ^ mix[0]; + line[x * 3 + 1] = + prevline[x * 3 + 1] ^ mix[1]; + line[x * 3 + 2] = + prevline[x * 3 + 2] ^ mix[2]; + } + insertmix = False; + count--; + x++; + } + if (prevline == NULL) + { + REPEAT + ( + line[x * 3] = 0; + line[x * 3 + 1] = 0; + line[x * 3 + 2] = 0; + ) + } + else + { + REPEAT + ( + line[x * 3] = prevline[x * 3]; + line[x * 3 + 1] = prevline[x * 3 + 1]; + line[x * 3 + 2] = prevline[x * 3 + 2]; + ) + } + break; + case 1: /* Mix */ + if (prevline == NULL) + { + REPEAT + ( + line[x * 3] = mix[0]; + line[x * 3 + 1] = mix[1]; + line[x * 3 + 2] = mix[2]; + ) + } + else + { + REPEAT + ( + line[x * 3] = + prevline[x * 3] ^ mix[0]; + line[x * 3 + 1] = + prevline[x * 3 + 1] ^ mix[1]; + line[x * 3 + 2] = + prevline[x * 3 + 2] ^ mix[2]; + ) + } + break; + case 2: /* Fill or Mix */ + if (prevline == NULL) + { + REPEAT + ( + MASK_UPDATE(); + if (mask & mixmask) + { + line[x * 3] = mix[0]; + line[x * 3 + 1] = mix[1]; + line[x * 3 + 2] = mix[2]; + } + else + { + line[x * 3] = 0; + line[x * 3 + 1] = 0; + line[x * 3 + 2] = 0; + } + ) + } + else + { + REPEAT + ( + MASK_UPDATE(); + if (mask & mixmask) + { + line[x * 3] = + prevline[x * 3] ^ mix [0]; + line[x * 3 + 1] = + prevline[x * 3 + 1] ^ mix [1]; + line[x * 3 + 2] = + prevline[x * 3 + 2] ^ mix [2]; + } + else + { + line[x * 3] = + prevline[x * 3]; + line[x * 3 + 1] = + prevline[x * 3 + 1]; + line[x * 3 + 2] = + prevline[x * 3 + 2]; + } + ) + } + break; + case 3: /* Colour */ + REPEAT + ( + line[x * 3] = colour2 [0]; + line[x * 3 + 1] = colour2 [1]; + line[x * 3 + 2] = colour2 [2]; + ) + break; + case 4: /* Copy */ + REPEAT + ( + line[x * 3] = CVAL(input); + line[x * 3 + 1] = CVAL(input); + line[x * 3 + 2] = CVAL(input); + ) + break; + case 8: /* Bicolour */ + REPEAT + ( + if (bicolour) + { + line[x * 3] = colour2[0]; + line[x * 3 + 1] = colour2[1]; + line[x * 3 + 2] = colour2[2]; + bicolour = False; + } + else + { + line[x * 3] = colour1[0]; + line[x * 3 + 1] = colour1[1]; + line[x * 3 + 2] = colour1[2]; + bicolour = True; + count++; + } + ) + break; + case 0xd: /* White */ + REPEAT + ( + line[x * 3] = 0xff; + line[x * 3 + 1] = 0xff; + line[x * 3 + 2] = 0xff; + ) + break; + case 0xe: /* Black */ + REPEAT + ( + line[x * 3] = 0; + line[x * 3 + 1] = 0; + line[x * 3 + 2] = 0; + ) + break; + default: + unimpl("bitmap opcode 0x%x\n", opcode); + return False; + } + } + } + return True; +} + +/* decompress a colour plane */ +static int +process_plane(uint8 * in, int width, int height, uint8 * out, int size) +{ + int indexw; + int indexh; + int code; + int collen; + int replen; + int color; + int x; + int revcode; + uint8 * last_line; + uint8 * this_line; + uint8 * org_in; + uint8 * org_out; + + org_in = in; + org_out = out; + last_line = 0; + indexh = 0; + while (indexh < height) + { + out = (org_out + width * height * 4) - ((indexh + 1) * width * 4); + color = 0; + this_line = out; + indexw = 0; + if (last_line == 0) + { + while (indexw < width) + { + code = CVAL(in); + replen = code & 0xf; + collen = (code >> 4) & 0xf; + revcode = (replen << 4) | collen; + if ((revcode <= 47) && (revcode >= 16)) + { + replen = revcode; + collen = 0; + } + while (collen > 0) + { + color = CVAL(in); + *out = color; + out += 4; + indexw++; + collen--; + } + while (replen > 0) + { + *out = color; + out += 4; + indexw++; + replen--; + } + } + } + else + { + while (indexw < width) + { + code = CVAL(in); + replen = code & 0xf; + collen = (code >> 4) & 0xf; + revcode = (replen << 4) | collen; + if ((revcode <= 47) && (revcode >= 16)) + { + replen = revcode; + collen = 0; + } + while (collen > 0) + { + x = CVAL(in); + if (x & 1) + { + x = x >> 1; + x = x + 1; + color = -x; + } + else + { + x = x >> 1; + color = x; + } + x = last_line[indexw * 4] + color; + *out = x; + out += 4; + indexw++; + collen--; + } + while (replen > 0) + { + x = last_line[indexw * 4] + color; + *out = x; + out += 4; + indexw++; + replen--; + } + } + } + indexh++; + last_line = this_line; + } + return (int) (in - org_in); +} + +/* 4 byte bitmap decompress */ +RD_BOOL +bitmap_decompress4(uint8 * output, int width, int height, uint8 * input, int size) +{ + int code; + int bytes_pro; + int total_pro; + + code = CVAL(input); + if (code != 0x10) + { + return False; + } + total_pro = 1; + bytes_pro = process_plane(input, width, height, output + 3, size - total_pro); + total_pro += bytes_pro; + input += bytes_pro; + bytes_pro = process_plane(input, width, height, output + 2, size - total_pro); + total_pro += bytes_pro; + input += bytes_pro; + bytes_pro = process_plane(input, width, height, output + 1, size - total_pro); + total_pro += bytes_pro; + input += bytes_pro; + bytes_pro = process_plane(input, width, height, output + 0, size - total_pro); + total_pro += bytes_pro; + return size == total_pro; +} + +int +bitmap_decompress_15(uint8 * output, int output_width, int output_height, int input_width, int input_height, uint8* input, int size) { + uint8 * temp = (uint8*)malloc(input_width * input_height * 2); + RD_BOOL rv = bitmap_decompress2(temp, input_width, input_height, input, size); + + // convert to rgba + for (int y = 0; y < output_height; y++) { + for (int x = 0; x < output_width; x++) { + uint16 a = ((uint16*)temp)[y * input_width + x]; + uint8 r = (a & 0x7c00) >> 10; + uint8 g = (a & 0x03e0) >> 5; + uint8 b = (a & 0x001f); + r = r * 255 / 31; + g = g * 255 / 31; + b = b * 255 / 31; + ((uint32*)output)[y * output_width + x] = 0xff << 24 | b << 16 | g << 8 | r; + } + } + + free(temp); + return rv; +} + +int +bitmap_decompress_16(uint8 * output, int output_width, int output_height, int input_width, int input_height, uint8* input, int size) { + uint8 * temp = (uint8*)malloc(input_width * input_height * 2); + RD_BOOL rv = bitmap_decompress2(temp, input_width, input_height, input, size); + + // convert to rgba + for (int y = 0; y < output_height; y++) { + for (int x = 0; x < output_width; x++) { + uint16 a = ((uint16*)temp)[y * input_width + x]; + uint8 r = (a & 0xf800) >> 11; + uint8 g = (a & 0x07e0) >> 5; + uint8 b = (a & 0x001f); + r = r * 255 / 31; + g = g * 255 / 63; + b = b * 255 / 31; + ((uint32*)output)[y * output_width + x] = 0xff << 24 | b << 16 | g << 8 | r; + } + } + free(temp); + return rv; +} + +int +bitmap_decompress_24(uint8 * output, int output_width, int output_height, int input_width, int input_height, uint8* input, int size) { + uint8 * temp = (uint8*)malloc(input_width * input_height * 3); + RD_BOOL rv = bitmap_decompress3(temp, input_width, input_height, input, size); + + // convert to rgba + for (int y = 0; y < output_height; y++) { + for (int x = 0; x < output_width; x++) { + uint8 r = temp[(y * input_width + x) * 3]; + uint8 g = temp[(y * input_width + x) * 3 + 1]; + uint8 b = temp[(y * input_width + x) * 3 + 2]; + ((uint32*)output)[y * output_width + x] = 0xff << 24 | b << 16 | g << 8 | r; + } + } + free(temp); + + return rv; +} + +int +bitmap_decompress_32(uint8 * output, int output_width, int output_height, int input_width, int input_height, uint8* input, int size) { + uint8 * temp = (uint8*)malloc(input_width * input_height * 4); + RD_BOOL rv = bitmap_decompress4(temp, input_width, input_height, input, size); + + // convert to rgba + for (int y = 0; y < output_height; y++) { + for (int x = 0; x < output_width; x++) { + uint8 r = temp[(y * input_width + x) * 4]; + uint8 g = temp[(y * input_width + x) * 4 + 1]; + uint8 b = temp[(y * input_width + x) * 4 + 2]; + uint8 a = temp[(y * input_width + x) * 4 + 3]; + ((uint32*)output)[y * output_width + x] = 0xff << 24 | r << 16 | g << 8 | b; + } + } + free(temp); + + return rv; +} diff --git a/client/tp-player/rle.h b/client/tp-player/rle.h new file mode 100644 index 0000000..3257d8f --- /dev/null +++ b/client/tp-player/rle.h @@ -0,0 +1,31 @@ +#ifndef RLE_H +#define RLE_H + +#define RD_BOOL int +#define False 0 +#define True 1 + +#define uint8 unsigned char +#define uint16 unsigned short +#define uint32 unsigned int + +#ifdef __cplusplus +extern "C" { +#endif + +RD_BOOL bitmap_decompress1(uint8 * output, int width, int height, uint8 * input, int size); +RD_BOOL bitmap_decompress2(uint8 * output, int width, int height, uint8 * input, int size); +RD_BOOL bitmap_decompress3(uint8 * output, int width, int height, uint8 * input, int size); +RD_BOOL bitmap_decompress4(uint8 * output, int width, int height, uint8 * input, int size); + +//int bitmap_decompress_15(uint8 * output, int output_width, int output_height, int input_width, int input_height, uint8* input, int size); +//int bitmap_decompress_16(uint8 * output, int output_width, int output_height, int input_width, int input_height, uint8* input, int size); +//int bitmap_decompress_24(uint8 * output, int output_width, int output_height, int input_width, int input_height, uint8* input, int size); +//int bitmap_decompress_32(uint8 * output, int output_width, int output_height, int input_width, int input_height, uint8* input, int size); + + +#ifdef __cplusplus +} +#endif + +#endif // RLE_H diff --git a/client/tp-player/thr_play.cpp b/client/tp-player/thr_play.cpp new file mode 100644 index 0000000..6f5b64f --- /dev/null +++ b/client/tp-player/thr_play.cpp @@ -0,0 +1,87 @@ +#include +#include + +#include "thr_play.h" +#include "record_format.h" + +static QString REPLAY_PATH = "E:\\work\\tp4a\\teleport\\server\\share\\replay\\rdp\\000000197\\"; + + +ThreadPlay::ThreadPlay() +{ + m_need_stop = false; +} + +void ThreadPlay::stop() { + m_need_stop = true; +} + +void ThreadPlay::run() { + qint64 read_len = 0; + uint32_t total_pkg = 0; + + QString hdr_filename(REPLAY_PATH); + hdr_filename += "tp-rdp.tpr"; + + QFile f_hdr(hdr_filename); + if(!f_hdr.open(QFile::ReadOnly)) { + qDebug() << "Can not open " << hdr_filename << " for read."; + return; + } + else { + update_data* dat = new update_data; + dat->data_type(TYPE_HEADER_INFO); + dat->alloc_data(sizeof(TS_RECORD_HEADER)); + + read_len = f_hdr.read((char*)(dat->data_buf()), dat->data_len()); + if(read_len != sizeof(TS_RECORD_HEADER)) { + delete dat; + qDebug() << "invaid .tpr file."; + return; + } + + TS_RECORD_HEADER* hdr = (TS_RECORD_HEADER*)dat->data_buf(); + total_pkg = hdr->info.packages; + + emit signal_update_data(dat); + } + + + + QString dat_filename(REPLAY_PATH); + dat_filename += "tp-rdp.dat"; + + QFile f_dat(dat_filename); + if(!f_dat.open(QFile::ReadOnly)) { + qDebug() << "Can not open " << dat_filename << " for read."; + return; + } + + for(uint32_t i = 0; i < total_pkg; ++i) { + if(m_need_stop) { + qDebug() << "stop, user cancel."; + break; + } + + TS_RECORD_PKG pkg; + read_len = f_dat.read((char*)(&pkg), sizeof(pkg)); + if(read_len != sizeof(TS_RECORD_PKG)) { + qDebug() << "invaid .dat file."; + return; + } + + update_data* dat = new update_data; + dat->data_type(TYPE_DATA); + dat->alloc_data(sizeof(TS_RECORD_PKG) + pkg.size); + memcpy(dat->data_buf(), &pkg, sizeof(TS_RECORD_PKG)); + read_len = f_dat.read((char*)(dat->data_buf()+sizeof(TS_RECORD_PKG)), pkg.size); + if(read_len != pkg.size) { + delete dat; + qDebug() << "invaid .dat file."; + return; + } + + emit signal_update_data(dat); + msleep(5); + } +} diff --git a/client/tp-player/thr_play.h b/client/tp-player/thr_play.h new file mode 100644 index 0000000..3c3bb2d --- /dev/null +++ b/client/tp-player/thr_play.h @@ -0,0 +1,25 @@ +#ifndef THR_PLAY_H +#define THR_PLAY_H + +#include +#include "update_data.h" + + +class ThreadPlay : public QThread +{ + Q_OBJECT +public: + ThreadPlay(); + + virtual void run(); + void stop(); + + +signals: + void signal_update_data(update_data*); + +private: + bool m_need_stop; +}; + +#endif // THR_PLAY_H diff --git a/client/tp-player/tp-player.pro b/client/tp-player/tp-player.pro new file mode 100644 index 0000000..cb73260 --- /dev/null +++ b/client/tp-player/tp-player.pro @@ -0,0 +1,26 @@ +TEMPLATE = app +TARGET = tp-player + +QT += core gui widgets + +HEADERS += \ + mainwindow.h \ + thr_play.h \ + update_data.h \ + record_format.h \ + rle.h + +SOURCES += main.cpp \ + mainwindow.cpp \ + thr_play.cpp \ + update_data.cpp \ + rle.c + +RESOURCES += \ + tp-player.qrc + +RC_FILE += \ + tp-player.rc + +FORMS += \ + mainwindow.ui diff --git a/client/tp-player/tp-player.qrc b/client/tp-player/tp-player.qrc new file mode 100644 index 0000000..7b8956f --- /dev/null +++ b/client/tp-player/tp-player.qrc @@ -0,0 +1,7 @@ + + + res/logo.png + res/bg.png + res/cursor.png + + diff --git a/client/tp-player/tp-player.rc b/client/tp-player/tp-player.rc new file mode 100644 index 0000000..df678b3 --- /dev/null +++ b/client/tp-player/tp-player.rc @@ -0,0 +1,2 @@ +IDI_ICON1 ICON DISCARDABLE "res\\tp-player.ico" + diff --git a/client/tp-player/update_data.cpp b/client/tp-player/update_data.cpp new file mode 100644 index 0000000..d84f364 --- /dev/null +++ b/client/tp-player/update_data.cpp @@ -0,0 +1,30 @@ +#include "update_data.h" + +update_data::update_data(QObject *parent) : QObject(parent) +{ + m_data_type = 0xff; + m_data_buf = nullptr; + m_data_len = 0; +} + +update_data::~update_data() { + if(m_data_buf) + delete m_data_buf; +} + +void update_data::alloc_data(uint32_t len) { + if(m_data_buf) + delete m_data_buf; + + m_data_buf = new uint8_t[len]; + memset(m_data_buf, 0, len); + m_data_len = len; +} + +void update_data::attach_data(const uint8_t* dat, uint32_t len) { + if(m_data_buf) + delete m_data_buf; + m_data_buf = new uint8_t[len]; + memcpy(m_data_buf, dat, len); + m_data_len = len; +} diff --git a/client/tp-player/update_data.h b/client/tp-player/update_data.h new file mode 100644 index 0000000..505c040 --- /dev/null +++ b/client/tp-player/update_data.h @@ -0,0 +1,33 @@ +#ifndef UPDATE_DATA_H +#define UPDATE_DATA_H + +#include + +class update_data : public QObject +{ + Q_OBJECT +public: + explicit update_data(QObject *parent = nullptr); + virtual ~update_data(); + + void alloc_data(uint32_t len); + void attach_data(const uint8_t* dat, uint32_t len); + + void data_type(int dt) {m_data_type = dt;} + int data_type() const {return m_data_type;} + + uint8_t* data_buf() {return m_data_buf;} + uint32_t data_len() const {return m_data_len;} + +signals: + +public slots: + + +private: + int m_data_type; + uint8_t* m_data_buf; + uint32_t m_data_len; +}; + +#endif // UPDATE_DATA_H diff --git a/resource/icon-tp-player.psd b/resource/icon-tp-player.psd new file mode 100644 index 0000000000000000000000000000000000000000..3ae900860f106fbbee070c6c3eaed47f69d23f8a GIT binary patch literal 58282 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 Bj4J>D literal 0 HcmV?d00001 From bee41150afc46f32e7bdfb8605900e754add1e1d Mon Sep 17 00:00:00 2001 From: Apex Liu Date: Tue, 3 Sep 2019 20:19:09 +0800 Subject: [PATCH 10/44] .temp.: parse compressed image. --- client/tp-player/mainwindow.cpp | 46 ++++++++++++++++++++++++++------- client/tp-player/mainwindow.h | 9 +++++-- client/tp-player/mainwindow.ui | 15 ----------- client/tp-player/tp-player.qrc | 1 - 4 files changed, 44 insertions(+), 27 deletions(-) diff --git a/client/tp-player/mainwindow.cpp b/client/tp-player/mainwindow.cpp index 2259562..1410c6f 100644 --- a/client/tp-player/mainwindow.cpp +++ b/client/tp-player/mainwindow.cpp @@ -5,6 +5,7 @@ #include #include #include +#include bool rdpimg2QImage(QImage& out, int w, int h, int bitsPerPixel, bool isCompressed, uint8_t* dat, uint32_t len) { switch(bitsPerPixel) { @@ -29,6 +30,19 @@ bool rdpimg2QImage(QImage& out, int w, int h, int bitsPerPixel, bool isCompresse free(_dat); return false; } + +// static bool first = true; +// if(first) { +// first = false; +// int total_bytes = w*h*2; +// for(int i = 0; i < total_bytes; i++) { +// printf("%02x ", _dat[i]); +// if(i != 0 && i % 16 == 0) +// printf("\n"); +// } +// fflush(stdout); +// } + out = QImage(_dat, w, h, QImage::Format_RGB16); free(_dat); } @@ -56,6 +70,7 @@ MainWindow::MainWindow(QWidget *parent) : m_show_bg = true; m_bg = QImage(":/tp-player/res/bg"); m_pt_normal = QImage(":/tp-player/res/cursor.png"); + m_update_img = false; qDebug() << m_pt_normal.width() << "x" << m_pt_normal.height(); @@ -71,11 +86,11 @@ MainWindow::MainWindow(QWidget *parent) : // setWindowFlags(Qt::FramelessWindowHint | Qt::MSWindowsFixedSizeDialogHint | windowFlags()); //#endif //__APPLE__ - setWindowFlags(windowFlags()&~Qt::WindowMaximizeButtonHint); // 禁止最大化按钮 - //setFixedSize(this->width(),this->height()); // 禁止拖动窗口大小 - resize(m_bg.width(), m_bg.height()); + setWindowFlags(windowFlags()&~Qt::WindowMaximizeButtonHint); // 禁止最大化按钮 + setFixedSize(m_bg.width(), m_bg.height()); // 禁止拖动窗口大小 + connect(&m_thr_play, SIGNAL(signal_update_data(update_data*)), this, SLOT(on_update_data(update_data*))); } @@ -102,9 +117,11 @@ void MainWindow::paintEvent(QPaintEvent *) } else { - painter.drawImage(m_img_update_x, m_img_update_y, m_img_update, 0, 0, m_img_update_w, m_img_update_h, Qt::AutoColor); - //qDebug() << "draw pt (" << m_pt.x << "," << m_pt.y << ")"; - painter.drawImage(m_pt.x, m_pt.y, m_pt_normal); + if(m_update_img) + painter.drawImage(m_img_update_x, m_img_update_y, m_img_update, 0, 0, m_img_update_w, m_img_update_h, Qt::AutoColor); + else + //qDebug() << "draw pt (" << m_pt.x << "," << m_pt.y << ")"; + painter.drawImage(m_pt.x, m_pt.y, m_pt_normal); } @@ -138,6 +155,7 @@ void MainWindow::on_update_data(update_data* dat) { } memcpy(&m_pt, dat->data_buf() + sizeof(TS_RECORD_PKG), sizeof(TS_RECORD_RDP_POINTER)); + m_update_img = false; update(); //update(m_pt.x - 8, m_pt.y - 8, 32, 32); } @@ -158,9 +176,11 @@ void MainWindow::on_update_data(update_data* dat) { m_img_update_w = info->destRight - info->destLeft + 1; m_img_update_h = info->destBottom - info->destTop + 1; - qDebug() << "img " << ((info->format == TS_RDP_IMG_BMP) ? "+" : " ") << " (" << m_img_update_x << "," << m_img_update_y << "), [" << m_img_update.width() << "x" << m_img_update.height() << "]"; - + static int count = 0; + qDebug() << count << "img " << ((info->format == TS_RDP_IMG_BMP) ? "+" : " ") << " (" << m_img_update_x << "," << m_img_update_y << "), [" << m_img_update.width() << "x" << m_img_update.height() << "]"; + count++; + m_update_img = true; update(m_img_update_x, m_img_update_y, m_img_update_w, m_img_update_h); } @@ -180,7 +200,15 @@ void MainWindow::on_update_data(update_data* dat) { qDebug() << "resize (" << m_rec_hdr.basic.width << "," << m_rec_hdr.basic.height << ")"; if(m_rec_hdr.basic.width > 0 && m_rec_hdr.basic.height > 0) { - resize(m_rec_hdr.basic.width, m_rec_hdr.basic.height); + m_win_board_w = frameGeometry().width() - geometry().width(); + m_win_board_h = frameGeometry().height() - geometry().height(); + + setFixedSize(m_rec_hdr.basic.width + m_win_board_w, m_rec_hdr.basic.height + m_win_board_h); + resize(m_rec_hdr.basic.width + m_win_board_w, m_rec_hdr.basic.height + m_win_board_h); + +// QDesktopWidget *desktop = QApplication::desktop(); // =qApp->desktop();也可以 +// //move((desktop->width() - this->width())/2, (desktop->height() - this->height())/2); +// move(10, (desktop->height() - this->height())/2); } QString title; diff --git a/client/tp-player/mainwindow.h b/client/tp-player/mainwindow.h index cd593c0..ffde816 100644 --- a/client/tp-player/mainwindow.h +++ b/client/tp-player/mainwindow.h @@ -28,8 +28,6 @@ private slots: private: Ui::MainWindow *ui; QImage m_bg; - QImage m_pt_normal; - QImage m_img_update; //QPixmap m_bg1; bool m_shown; @@ -37,8 +35,15 @@ private: bool m_show_bg; TS_RECORD_HEADER m_rec_hdr; + + bool m_update_img; + + QImage m_pt_normal; TS_RECORD_RDP_POINTER m_pt; + QImage m_img_update; + int m_win_board_w; + int m_win_board_h; int m_img_update_x; int m_img_update_y; int m_img_update_w; diff --git a/client/tp-player/mainwindow.ui b/client/tp-player/mainwindow.ui index db843d5..ecdf9ac 100644 --- a/client/tp-player/mainwindow.ui +++ b/client/tp-player/mainwindow.ui @@ -14,21 +14,6 @@ Teleport Replayer - - - - 0 - 0 - 500 - 17 - - - - - - false - - diff --git a/client/tp-player/tp-player.qrc b/client/tp-player/tp-player.qrc index 7b8956f..cccf89b 100644 --- a/client/tp-player/tp-player.qrc +++ b/client/tp-player/tp-player.qrc @@ -1,6 +1,5 @@ - res/logo.png res/bg.png res/cursor.png From cc9552c7431c89879edce144c423595095868e8c Mon Sep 17 00:00:00 2001 From: Apex Liu Date: Wed, 4 Sep 2019 20:28:16 +0800 Subject: [PATCH 11/44] .temp. --- client/tp-player/mainwindow.cpp | 79 ++++++++++++++++++++++++++++----- client/tp-player/mainwindow.h | 3 +- client/tp-player/thr_play.cpp | 2 +- 3 files changed, 72 insertions(+), 12 deletions(-) diff --git a/client/tp-player/mainwindow.cpp b/client/tp-player/mainwindow.cpp index 1410c6f..e04bf28 100644 --- a/client/tp-player/mainwindow.cpp +++ b/client/tp-player/mainwindow.cpp @@ -6,6 +6,7 @@ #include #include #include +#include bool rdpimg2QImage(QImage& out, int w, int h, int bitsPerPixel, bool isCompressed, uint8_t* dat, uint32_t len) { switch(bitsPerPixel) { @@ -43,8 +44,24 @@ bool rdpimg2QImage(QImage& out, int w, int h, int bitsPerPixel, bool isCompresse // fflush(stdout); // } + //out = QImage(_dat, w, h, QImage::Format_RGB16); + + static bool bf = true; + if(bf) { + bf = false; + int total_bytes = w*h*2; + if(total_bytes == 32) { + uchar aaa[32] = {0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff}; + memcpy(_dat, aaa, 32); + } + } + out = QImage(_dat, w, h, QImage::Format_RGB16); free(_dat); + +// QPixmap x(w, h); +// x.fill(QColor(0,0,0)); +// out = x.toImage(); } else { out = QImage(dat, w, h, QImage::Format_RGB16).transformed(QMatrix(1.0, 0.0, 0.0, -1.0, 0.0, 0.0)) ; @@ -71,6 +88,7 @@ MainWindow::MainWindow(QWidget *parent) : m_bg = QImage(":/tp-player/res/bg"); m_pt_normal = QImage(":/tp-player/res/cursor.png"); m_update_img = false; + memset(&m_pt, 0, sizeof(TS_RECORD_RDP_POINTER)); qDebug() << m_pt_normal.width() << "x" << m_pt_normal.height(); @@ -86,6 +104,9 @@ MainWindow::MainWindow(QWidget *parent) : // setWindowFlags(Qt::FramelessWindowHint | Qt::MSWindowsFixedSizeDialogHint | windowFlags()); //#endif //__APPLE__ + //m_canvas = QPixmap(m_bg.width(), m_bg.height()); + m_canvas.load(":/tp-player/res/bg"); + resize(m_bg.width(), m_bg.height()); setWindowFlags(windowFlags()&~Qt::WindowMaximizeButtonHint); // 禁止最大化按钮 @@ -101,26 +122,36 @@ MainWindow::~MainWindow() delete ui; } -void MainWindow::paintEvent(QPaintEvent *) +void MainWindow::paintEvent(QPaintEvent *pe) { QPainter painter(this); if(m_show_bg) { //qDebug() << "draw bg."; - painter.setBrush(Qt::black); - painter.drawRect(this->rect()); +// painter.setBrush(Qt::black); +// painter.drawRect(this->rect()); - int x = (rect().width() - m_bg.width()) / 2; - int y = (rect().height() - m_bg.height()) / 2; - painter.drawImage(x, y, m_bg); - //painter.drawPixmap(rect(), m_bg1); +// int x = (rect().width() - m_bg.width()) / 2; +// int y = (rect().height() - m_bg.height()) / 2; +// painter.drawImage(x, y, m_bg); + + painter.drawPixmap(rect(), m_canvas); } else { - if(m_update_img) - painter.drawImage(m_img_update_x, m_img_update_y, m_img_update, 0, 0, m_img_update_w, m_img_update_h, Qt::AutoColor); - else + painter.drawPixmap(rect(), m_canvas); + +// if(m_update_img) +// painter.drawImage(m_img_update_x, m_img_update_y, m_img_update, 0, 0, m_img_update_w, m_img_update_h, Qt::AutoColor); +// else //qDebug() << "draw pt (" << m_pt.x << "," << m_pt.y << ")"; +// painter.drawImage(m_pt.x, m_pt.y, m_pt_normal); + + QRect rcpt(m_pt_normal.rect()); + rcpt.moveTo(m_pt.x - m_pt_normal.width()/2, m_pt.y-m_pt_normal.height()/2); + QRect rcpe(pe->rect()); + + if(pe->rect().intersects(rcpt)) painter.drawImage(m_pt.x, m_pt.y, m_pt_normal); } @@ -171,6 +202,30 @@ void MainWindow::on_update_data(update_data* dat) { uint32_t img_len = dat->data_len() - sizeof(TS_RECORD_PKG) - sizeof(TS_RECORD_RDP_IMAGE_INFO); rdpimg2QImage(m_img_update, info->width, info->height, info->bitsPerPixel, (info->format == TS_RDP_IMG_BMP) ? true : false, img_dat, img_len); + + static bool need_save = true; + if(need_save) { + need_save = false; + m_img_update.save("E:\\work\\tp4a\\teleport\\server\\share\\replay\\rdp\\000000197\\test.bmp", "BMP"); + + uchar* xx = m_img_update.bits(); + int total_bytes = m_img_update.width()*m_img_update.height()*2; + for(int i = 0; i < total_bytes; i++) { + printf("%02x ", xx[i]); + if(i != 0 && i % 16 == 0) + printf("\n"); + } + fflush(stdout); + + } + + + + QPainter pp(&m_canvas); + pp.drawImage(m_img_update_x, m_img_update_y, m_img_update, 0, 0, m_img_update_w, m_img_update_h, Qt::AutoColor); + + + m_img_update_x = info->destLeft; m_img_update_y = info->destTop; m_img_update_w = info->destRight - info->destLeft + 1; @@ -200,6 +255,10 @@ void MainWindow::on_update_data(update_data* dat) { qDebug() << "resize (" << m_rec_hdr.basic.width << "," << m_rec_hdr.basic.height << ")"; if(m_rec_hdr.basic.width > 0 && m_rec_hdr.basic.height > 0) { + + m_canvas = QPixmap(m_rec_hdr.basic.width, m_rec_hdr.basic.height); + + m_win_board_w = frameGeometry().width() - geometry().width(); m_win_board_h = frameGeometry().height() - geometry().height(); diff --git a/client/tp-player/mainwindow.h b/client/tp-player/mainwindow.h index ffde816..d1bcc28 100644 --- a/client/tp-player/mainwindow.h +++ b/client/tp-player/mainwindow.h @@ -28,11 +28,12 @@ private slots: private: Ui::MainWindow *ui; QImage m_bg; - //QPixmap m_bg1; bool m_shown; ThreadPlay m_thr_play; + QPixmap m_canvas; + bool m_show_bg; TS_RECORD_HEADER m_rec_hdr; diff --git a/client/tp-player/thr_play.cpp b/client/tp-player/thr_play.cpp index 6f5b64f..4c0ce8c 100644 --- a/client/tp-player/thr_play.cpp +++ b/client/tp-player/thr_play.cpp @@ -82,6 +82,6 @@ void ThreadPlay::run() { } emit signal_update_data(dat); - msleep(5); + msleep(10); } } From 51932cb092d83902864b5eadca62606d485b6990 Mon Sep 17 00:00:00 2001 From: Apex Liu Date: Thu, 5 Sep 2019 04:21:29 +0800 Subject: [PATCH 12/44] =?UTF-8?q?=E6=96=B0=E7=9A=84RDP=E5=BD=95=E5=83=8F?= =?UTF-8?q?=E6=92=AD=E6=94=BE=E5=B7=A5=E5=85=B7=E5=8F=AF=E4=BB=A5=E6=92=AD?= =?UTF-8?q?=E6=94=BE=E5=BD=95=E5=83=8F=E4=BA=86=EF=BC=8C=E4=B8=8B=E4=B8=80?= =?UTF-8?q?=E6=AD=A5=E7=BB=A7=E7=BB=AD=E5=AE=8C=E5=96=84=EF=BC=8C=E5=8C=85?= =?UTF-8?q?=E6=8B=AC=E6=92=AD=E6=94=BE=E5=99=A8=E7=9A=84=E6=8E=A7=E5=88=B6?= =?UTF-8?q?=E9=83=A8=E5=88=86=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/tp-player/mainwindow.cpp | 123 +++++++++++--------------------- client/tp-player/mainwindow.h | 5 +- client/tp-player/rle.h | 8 +-- client/tp-player/thr_play.cpp | 46 ++++++++++-- 4 files changed, 91 insertions(+), 91 deletions(-) diff --git a/client/tp-player/mainwindow.cpp b/client/tp-player/mainwindow.cpp index e04bf28..9748b2c 100644 --- a/client/tp-player/mainwindow.cpp +++ b/client/tp-player/mainwindow.cpp @@ -32,36 +32,24 @@ bool rdpimg2QImage(QImage& out, int w, int h, int bitsPerPixel, bool isCompresse return false; } -// static bool first = true; -// if(first) { -// first = false; -// int total_bytes = w*h*2; -// for(int i = 0; i < total_bytes; i++) { -// printf("%02x ", _dat[i]); -// if(i != 0 && i % 16 == 0) -// printf("\n"); -// } -// fflush(stdout); -// } + // TODO: 这里需要进一步优化,直接操作QImage的buffer。 - //out = QImage(_dat, w, h, QImage::Format_RGB16); + out = QImage(w, h, QImage::Format_RGB16); + for(int y = 0; y < h; y++) { + for(int x = 0; x < w; x++) { + uint16 a = ((uint16*)_dat)[y * w + x]; + uint8 r = ((a & 0xf800) >> 11) * 255 / 31; + uint8 g = ((a & 0x07e0) >> 5) * 255 / 63; + uint8 b = (a & 0x001f) * 255 / 31; +// r = r * 255 / 31; +// g = g * 255 / 63; +// b = b * 255 / 31; - static bool bf = true; - if(bf) { - bf = false; - int total_bytes = w*h*2; - if(total_bytes == 32) { - uchar aaa[32] = {0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff}; - memcpy(_dat, aaa, 32); + out.setPixelColor(x, y, QColor(r,g,b)); } } - out = QImage(_dat, w, h, QImage::Format_RGB16); free(_dat); - -// QPixmap x(w, h); -// x.fill(QColor(0,0,0)); -// out = x.toImage(); } else { out = QImage(dat, w, h, QImage::Format_RGB16).transformed(QMatrix(1.0, 0.0, 0.0, -1.0, 0.0, 0.0)) ; @@ -87,7 +75,6 @@ MainWindow::MainWindow(QWidget *parent) : m_show_bg = true; m_bg = QImage(":/tp-player/res/bg"); m_pt_normal = QImage(":/tp-player/res/cursor.png"); - m_update_img = false; memset(&m_pt, 0, sizeof(TS_RECORD_RDP_POINTER)); qDebug() << m_pt_normal.width() << "x" << m_pt_normal.height(); @@ -126,33 +113,23 @@ void MainWindow::paintEvent(QPaintEvent *pe) { QPainter painter(this); - if(m_show_bg) { - //qDebug() << "draw bg."; -// painter.setBrush(Qt::black); -// painter.drawRect(this->rect()); + painter.drawPixmap(pe->rect(), m_canvas, pe->rect()); -// int x = (rect().width() - m_bg.width()) / 2; -// int y = (rect().height() - m_bg.height()) / 2; -// painter.drawImage(x, y, m_bg); - - painter.drawPixmap(rect(), m_canvas); + if(!m_pt_history.empty()) { + for(int i = 0; i < m_pt_history.count(); i++) { + qDebug("pt clean %d,%d", m_pt_history[i].x, m_pt_history[i].y); + QRect rcpt(m_pt_normal.rect()); + rcpt.moveTo(m_pt_history[i].x - m_pt_normal.width()/2, m_pt_history[i].y-m_pt_normal.height()/2); + painter.drawPixmap(rcpt, m_canvas, rcpt); + } + m_pt_history.clear(); } - else { - painter.drawPixmap(rect(), m_canvas); + QRect rcpt(m_pt_normal.rect()); + rcpt.moveTo(m_pt.x - m_pt_normal.width()/2, m_pt.y-m_pt_normal.height()/2); -// if(m_update_img) -// painter.drawImage(m_img_update_x, m_img_update_y, m_img_update, 0, 0, m_img_update_w, m_img_update_h, Qt::AutoColor); -// else - //qDebug() << "draw pt (" << m_pt.x << "," << m_pt.y << ")"; -// painter.drawImage(m_pt.x, m_pt.y, m_pt_normal); - - QRect rcpt(m_pt_normal.rect()); - rcpt.moveTo(m_pt.x - m_pt_normal.width()/2, m_pt.y-m_pt_normal.height()/2); - QRect rcpe(pe->rect()); - - if(pe->rect().intersects(rcpt)) - painter.drawImage(m_pt.x, m_pt.y, m_pt_normal); + if(pe->rect().intersects(rcpt)) { + painter.drawImage(m_pt.x-m_pt_normal.width()/2, m_pt.y-m_pt_normal.height()/2, m_pt_normal); } @@ -165,7 +142,6 @@ void MainWindow::paintEvent(QPaintEvent *pe) void MainWindow::on_update_data(update_data* dat) { if(!dat) return; -// qDebug() << "slot-event: " << dat->data_type(); if(dat->data_type() == TYPE_DATA) { m_show_bg = false; @@ -185,10 +161,16 @@ void MainWindow::on_update_data(update_data* dat) { return; } + // 将现有虚拟鼠标信息放入历史队列,这样下一次绘制界面时就会将其清除掉 + m_pt_history.push_back(m_pt); + + // 更新虚拟鼠标信息,这样下一次绘制界面时就会在新的位置绘制出虚拟鼠标 memcpy(&m_pt, dat->data_buf() + sizeof(TS_RECORD_PKG), sizeof(TS_RECORD_RDP_POINTER)); - m_update_img = false; - update(); - //update(m_pt.x - 8, m_pt.y - 8, 32, 32); + qDebug("pt new position %d,%d", m_pt.x, m_pt.y); + + //setUpdatesEnabled(false); + update(m_pt.x - m_pt_normal.width()/2, m_pt.y - m_pt_normal.width()/2, m_pt_normal.width(), m_pt_normal.height()); + //setUpdatesEnabled(true); } else if(pkg->type == TS_RECORD_TYPE_RDP_IMAGE) { if(dat->data_len() <= sizeof(TS_RECORD_PKG) + sizeof(TS_RECORD_RDP_IMAGE_INFO)) { @@ -203,40 +185,17 @@ void MainWindow::on_update_data(update_data* dat) { rdpimg2QImage(m_img_update, info->width, info->height, info->bitsPerPixel, (info->format == TS_RDP_IMG_BMP) ? true : false, img_dat, img_len); - static bool need_save = true; - if(need_save) { - need_save = false; - m_img_update.save("E:\\work\\tp4a\\teleport\\server\\share\\replay\\rdp\\000000197\\test.bmp", "BMP"); - - uchar* xx = m_img_update.bits(); - int total_bytes = m_img_update.width()*m_img_update.height()*2; - for(int i = 0; i < total_bytes; i++) { - printf("%02x ", xx[i]); - if(i != 0 && i % 16 == 0) - printf("\n"); - } - fflush(stdout); - - } - - - - QPainter pp(&m_canvas); - pp.drawImage(m_img_update_x, m_img_update_y, m_img_update, 0, 0, m_img_update_w, m_img_update_h, Qt::AutoColor); - - - m_img_update_x = info->destLeft; m_img_update_y = info->destTop; m_img_update_w = info->destRight - info->destLeft + 1; m_img_update_h = info->destBottom - info->destTop + 1; - static int count = 0; - qDebug() << count << "img " << ((info->format == TS_RDP_IMG_BMP) ? "+" : " ") << " (" << m_img_update_x << "," << m_img_update_y << "), [" << m_img_update.width() << "x" << m_img_update.height() << "]"; - count++; + setUpdatesEnabled(false); + QPainter pp(&m_canvas); + pp.drawImage(m_img_update_x, m_img_update_y, m_img_update, 0, 0, m_img_update_w, m_img_update_h, Qt::AutoColor); - m_update_img = true; update(m_img_update_x, m_img_update_y, m_img_update_w, m_img_update_h); + setUpdatesEnabled(true); } delete dat; @@ -262,8 +221,10 @@ void MainWindow::on_update_data(update_data* dat) { m_win_board_w = frameGeometry().width() - geometry().width(); m_win_board_h = frameGeometry().height() - geometry().height(); - setFixedSize(m_rec_hdr.basic.width + m_win_board_w, m_rec_hdr.basic.height + m_win_board_h); - resize(m_rec_hdr.basic.width + m_win_board_w, m_rec_hdr.basic.height + m_win_board_h); + //setFixedSize(m_rec_hdr.basic.width + m_win_board_w, m_rec_hdr.basic.height + m_win_board_h); + //resize(m_rec_hdr.basic.width + m_win_board_w, m_rec_hdr.basic.height + m_win_board_h); + setFixedSize(m_rec_hdr.basic.width, m_rec_hdr.basic.height); + resize(m_rec_hdr.basic.width, m_rec_hdr.basic.height); // QDesktopWidget *desktop = QApplication::desktop(); // =qApp->desktop();也可以 // //move((desktop->width() - this->width())/2, (desktop->height() - this->height())/2); diff --git a/client/tp-player/mainwindow.h b/client/tp-player/mainwindow.h index d1bcc28..386010b 100644 --- a/client/tp-player/mainwindow.h +++ b/client/tp-player/mainwindow.h @@ -2,6 +2,7 @@ #define MAINWINDOW_H #include +#include #include "thr_play.h" #include "update_data.h" #include "record_format.h" @@ -37,10 +38,10 @@ private: bool m_show_bg; TS_RECORD_HEADER m_rec_hdr; - bool m_update_img; - QImage m_pt_normal; TS_RECORD_RDP_POINTER m_pt; + QVector m_pt_history; + QImage m_img_update; int m_win_board_w; diff --git a/client/tp-player/rle.h b/client/tp-player/rle.h index 3257d8f..9ed737a 100644 --- a/client/tp-player/rle.h +++ b/client/tp-player/rle.h @@ -18,10 +18,10 @@ RD_BOOL bitmap_decompress2(uint8 * output, int width, int height, uint8 * input, RD_BOOL bitmap_decompress3(uint8 * output, int width, int height, uint8 * input, int size); RD_BOOL bitmap_decompress4(uint8 * output, int width, int height, uint8 * input, int size); -//int bitmap_decompress_15(uint8 * output, int output_width, int output_height, int input_width, int input_height, uint8* input, int size); -//int bitmap_decompress_16(uint8 * output, int output_width, int output_height, int input_width, int input_height, uint8* input, int size); -//int bitmap_decompress_24(uint8 * output, int output_width, int output_height, int input_width, int input_height, uint8* input, int size); -//int bitmap_decompress_32(uint8 * output, int output_width, int output_height, int input_width, int input_height, uint8* input, int size); +int bitmap_decompress_15(uint8 * output, int output_width, int output_height, int input_width, int input_height, uint8* input, int size); +int bitmap_decompress_16(uint8 * output, int output_width, int output_height, int input_width, int input_height, uint8* input, int size); +int bitmap_decompress_24(uint8 * output, int output_width, int output_height, int input_width, int input_height, uint8* input, int size); +int bitmap_decompress_32(uint8 * output, int output_width, int output_height, int input_width, int input_height, uint8* input, int size); #ifdef __cplusplus diff --git a/client/tp-player/thr_play.cpp b/client/tp-player/thr_play.cpp index 4c0ce8c..2094e4f 100644 --- a/client/tp-player/thr_play.cpp +++ b/client/tp-player/thr_play.cpp @@ -1,4 +1,5 @@ -#include +#include +#include #include #include "thr_play.h" @@ -57,6 +58,10 @@ void ThreadPlay::run() { return; } + uint32_t time_pass = 0; + + qint64 time_begin = QDateTime::currentMSecsSinceEpoch(); + for(uint32_t i = 0; i < total_pkg; ++i) { if(m_need_stop) { qDebug() << "stop, user cancel."; @@ -66,7 +71,7 @@ void ThreadPlay::run() { TS_RECORD_PKG pkg; read_len = f_dat.read((char*)(&pkg), sizeof(pkg)); if(read_len != sizeof(TS_RECORD_PKG)) { - qDebug() << "invaid .dat file."; + qDebug() << "invaid .dat file (1)."; return; } @@ -81,7 +86,40 @@ void ThreadPlay::run() { return; } - emit signal_update_data(dat); - msleep(10); + time_pass = (uint32_t)(QDateTime::currentMSecsSinceEpoch() - time_begin); + + if(time_pass >= pkg.time_ms) { + //time_pass = pkg.time_ms; + emit signal_update_data(dat); + continue; + } + + // 需要等待 + uint32_t time_wait = pkg.time_ms - time_pass; + uint32_t wait_this_time = 0; + for(;;) { + wait_this_time = time_wait; + if(wait_this_time > 5) + wait_this_time = 5; + + if(m_need_stop) { + qDebug() << "stop, user cancel (2)."; + break; + } + + msleep(wait_this_time); + + //time_pass += wait_this_time; + //time_pass = pkg.time_ms; + + time_wait -= wait_this_time; + if(time_wait == 0) { + emit signal_update_data(dat); + break; + } + } + +// emit signal_update_data(dat); +// msleep(15); } } From f17c1cf59a587ee5ca5831c982b12a814561bdbe Mon Sep 17 00:00:00 2001 From: Apex Liu Date: Fri, 6 Sep 2019 20:07:13 +0800 Subject: [PATCH 13/44] add player resource. --- client/tp-player/bar.cpp | 57 ++++++++++++++++++++++ client/tp-player/bar.h | 53 ++++++++++++++++++++ client/tp-player/mainwindow.cpp | 29 ++++++----- client/tp-player/mainwindow.h | 3 ++ client/tp-player/res/bar.psd | Bin 0 -> 717957 bytes client/tp-player/res/bar/bg-left.png | Bin 0 -> 1099 bytes client/tp-player/res/bar/bg-mid.png | Bin 0 -> 1015 bytes client/tp-player/res/bar/bg-right.png | Bin 0 -> 1103 bytes client/tp-player/res/bar/btn-left.png | Bin 0 -> 1059 bytes client/tp-player/res/bar/btn-mid.png | Bin 0 -> 1004 bytes client/tp-player/res/bar/btn-right.png | Bin 0 -> 1064 bytes client/tp-player/res/bar/btnsel-left.png | Bin 0 -> 1044 bytes client/tp-player/res/bar/btnsel-mid.png | Bin 0 -> 1011 bytes client/tp-player/res/bar/btnsel-right.png | Bin 0 -> 1047 bytes client/tp-player/res/bar/play-hover.png | Bin 0 -> 2234 bytes client/tp-player/res/bar/play.png | Bin 0 -> 2295 bytes client/tp-player/res/bar/prgbar-left.png | Bin 0 -> 1043 bytes client/tp-player/res/bar/prgbar-mid.png | Bin 0 -> 1007 bytes client/tp-player/res/bar/prgbar-right.png | Bin 0 -> 1049 bytes client/tp-player/res/bar/prgbarh-left.png | Bin 0 -> 1044 bytes client/tp-player/res/bar/prgbarh-mid.png | Bin 0 -> 1007 bytes client/tp-player/res/bar/prgpt-hover.png | Bin 0 -> 1126 bytes client/tp-player/res/bar/prgpt.png | Bin 0 -> 1151 bytes client/tp-player/res/bar/select.png | Bin 0 -> 1084 bytes client/tp-player/res/bar/selected.png | Bin 0 -> 1159 bytes client/tp-player/tp-player.pro | 7 ++- client/tp-player/tp-player.qrc | 20 ++++++++ 27 files changed, 156 insertions(+), 13 deletions(-) create mode 100644 client/tp-player/bar.cpp create mode 100644 client/tp-player/bar.h create mode 100644 client/tp-player/res/bar.psd create mode 100644 client/tp-player/res/bar/bg-left.png create mode 100644 client/tp-player/res/bar/bg-mid.png create mode 100644 client/tp-player/res/bar/bg-right.png create mode 100644 client/tp-player/res/bar/btn-left.png create mode 100644 client/tp-player/res/bar/btn-mid.png create mode 100644 client/tp-player/res/bar/btn-right.png create mode 100644 client/tp-player/res/bar/btnsel-left.png create mode 100644 client/tp-player/res/bar/btnsel-mid.png create mode 100644 client/tp-player/res/bar/btnsel-right.png create mode 100644 client/tp-player/res/bar/play-hover.png create mode 100644 client/tp-player/res/bar/play.png create mode 100644 client/tp-player/res/bar/prgbar-left.png create mode 100644 client/tp-player/res/bar/prgbar-mid.png create mode 100644 client/tp-player/res/bar/prgbar-right.png create mode 100644 client/tp-player/res/bar/prgbarh-left.png create mode 100644 client/tp-player/res/bar/prgbarh-mid.png create mode 100644 client/tp-player/res/bar/prgpt-hover.png create mode 100644 client/tp-player/res/bar/prgpt.png create mode 100644 client/tp-player/res/bar/select.png create mode 100644 client/tp-player/res/bar/selected.png diff --git a/client/tp-player/bar.cpp b/client/tp-player/bar.cpp new file mode 100644 index 0000000..cb034a8 --- /dev/null +++ b/client/tp-player/bar.cpp @@ -0,0 +1,57 @@ +#include "bar.h" + +#include + +Bar::Bar() { + +} + +Bar::~Bar() { + +} + +bool Bar::init(QWidget* owner, int width) { + m_owner = owner; + + // 加载所需的图像资源 + if(!m_bg_left.load(":/tp-player/res/bar/bg-left.png") + || !m_bg_mid.load(":/tp-player/res/bar/bg-mid.png") + || !m_bg_right.load(":/tp-player/res/bar/bg-right.png") + || !m_btn_left.load(":/tp-player/res/bar/btn-left.png") + || !m_btn_mid.load(":/tp-player/res/bar/btn-mid.png") + || !m_btn_right.load(":/tp-player/res/bar/btn-right.png") + || !m_btnsel_left.load(":/tp-player/res/bar/btnsel-left.png") + || !m_btnsel_mid.load(":/tp-player/res/bar/btnsel-mid.png") + || !m_btnsel_right.load(":/tp-player/res/bar/btnsel-right.png") + || !m_prgbarh_left.load(":/tp-player/res/bar/prgbarh-left.png") + || !m_prgbarh_mid.load(":/tp-player/res/bar/prgbarh-mid.png") + || !m_prgbar_left.load(":/tp-player/res/bar/prgbar-left.png") + || !m_prgbar_mid.load(":/tp-player/res/bar/prgbar-mid.png") + || !m_prgbar_right.load(":/tp-player/res/bar/prgbar-right.png") + || !m_prgpt.load(":/tp-player/res/bar/prgpt.png") + || !m_prgpt_hover.load(":/tp-player/res/bar/prgpt-hover.png") + || !m_select.load(":/tp-player/res/bar/select.png") + || !m_selected.load(":/tp-player/res/bar/selected.png") + || !m_play.load(":/tp-player/res/bar/play.png") + || !m_play_hover.load(":/tp-player/res/bar/play-hover.png") + ) + return false; + + // 创建背景图像 + m_bg = QPixmap(width, m_bg_left.height()); + m_bg.fill(Qt::transparent);//用透明色填充 + QPainter pp(&m_bg); + pp.drawPixmap(0, 0, m_bg_left, 0, 0, m_bg_left.width(), m_bg_left.height()); + pp.drawPixmap(m_bg_left.width(), 0, m_bg.width() - m_bg_left.width() - m_bg_right.width(), m_bg_left.height(), m_bg_mid); + pp.drawPixmap(m_bg.width()-m_bg_right.width(), 0, m_bg_right, 0, 0, m_bg_right.width(), m_bg_right.height()); + + //pp.drawPixmap(10, 10, m_prgpt, 0, 0, m_prgpt.width(), m_prgpt.height()); + pp.drawPixmap(10, 10, m_play.width(), m_play.height(), m_play); + + + return true; +} + +void Bar::draw(QPainter& painter, const QRect& rc){ + painter.drawPixmap(10, 150, m_bg, 0, 0, m_bg.width(), m_bg.height()); +} diff --git a/client/tp-player/bar.h b/client/tp-player/bar.h new file mode 100644 index 0000000..5000069 --- /dev/null +++ b/client/tp-player/bar.h @@ -0,0 +1,53 @@ +#ifndef BAR_H +#define BAR_H + +#include +#include +#include + + +class Bar { +public: + Bar(); + ~Bar(); + + bool init(QWidget* owner, int width); + void draw(QPainter& painter, const QRect& rc); + +private: + QWidget* m_owner; + int m_width; + + + QPixmap m_bg; + + QPixmap m_bg_left; + QPixmap m_bg_mid; + QPixmap m_bg_right; + + QPixmap m_btn_left; + QPixmap m_btn_mid; + QPixmap m_btn_right; + + QPixmap m_btnsel_left; + QPixmap m_btnsel_mid; + QPixmap m_btnsel_right; + + QPixmap m_prgbarh_left; + QPixmap m_prgbarh_mid; + + QPixmap m_prgbar_left; + QPixmap m_prgbar_mid; + QPixmap m_prgbar_right; + + QPixmap m_prgpt; + QPixmap m_prgpt_hover; + + QPixmap m_select; + QPixmap m_selected; + + QPixmap m_play; + QPixmap m_play_hover; +}; + +#endif // BAR_H diff --git a/client/tp-player/mainwindow.cpp b/client/tp-player/mainwindow.cpp index 9748b2c..b2467d0 100644 --- a/client/tp-player/mainwindow.cpp +++ b/client/tp-player/mainwindow.cpp @@ -99,6 +99,9 @@ MainWindow::MainWindow(QWidget *parent) : setWindowFlags(windowFlags()&~Qt::WindowMaximizeButtonHint); // 禁止最大化按钮 setFixedSize(m_bg.width(), m_bg.height()); // 禁止拖动窗口大小 + if(!m_bar.init(this, 480)) + return; + connect(&m_thr_play, SIGNAL(signal_update_data(update_data*)), this, SLOT(on_update_data(update_data*))); } @@ -117,26 +120,29 @@ void MainWindow::paintEvent(QPaintEvent *pe) if(!m_pt_history.empty()) { for(int i = 0; i < m_pt_history.count(); i++) { - qDebug("pt clean %d,%d", m_pt_history[i].x, m_pt_history[i].y); + //qDebug("pt clean %d,%d", m_pt_history[i].x, m_pt_history[i].y); QRect rcpt(m_pt_normal.rect()); rcpt.moveTo(m_pt_history[i].x - m_pt_normal.width()/2, m_pt_history[i].y-m_pt_normal.height()/2); - painter.drawPixmap(rcpt, m_canvas, rcpt); + //painter.drawPixmap(rcpt, m_canvas, rcpt); + qDebug("pt ---- (%d,%d), (%d,%d)", rcpt.x(), rcpt.y(), rcpt.width(), rcpt.height()); + painter.fillRect(rcpt, QColor(255, 255, 0, 128)); } m_pt_history.clear(); } QRect rcpt(m_pt_normal.rect()); rcpt.moveTo(m_pt.x - m_pt_normal.width()/2, m_pt.y-m_pt_normal.height()/2); - if(pe->rect().intersects(rcpt)) { + qDebug("pt draw (%d,%d), (%d,%d)", m_pt.x-m_pt_normal.width()/2, m_pt.y-m_pt_normal.height()/2, m_pt_normal.width(), m_pt_normal.height()); painter.drawImage(m_pt.x-m_pt_normal.width()/2, m_pt.y-m_pt_normal.height()/2, m_pt_normal); } + m_bar.draw(painter, pe->rect()); - if(!m_shown) { - m_shown = true; - m_thr_play.start(); - } +// if(!m_shown) { +// m_shown = true; +// m_thr_play.start(); +// } } void MainWindow::on_update_data(update_data* dat) { @@ -166,7 +172,7 @@ void MainWindow::on_update_data(update_data* dat) { // 更新虚拟鼠标信息,这样下一次绘制界面时就会在新的位置绘制出虚拟鼠标 memcpy(&m_pt, dat->data_buf() + sizeof(TS_RECORD_PKG), sizeof(TS_RECORD_RDP_POINTER)); - qDebug("pt new position %d,%d", m_pt.x, m_pt.y); + //qDebug("pt new position %d,%d", m_pt.x, m_pt.y); //setUpdatesEnabled(false); update(m_pt.x - m_pt_normal.width()/2, m_pt.y - m_pt_normal.width()/2, m_pt_normal.width(), m_pt_normal.height()); @@ -226,9 +232,10 @@ void MainWindow::on_update_data(update_data* dat) { setFixedSize(m_rec_hdr.basic.width, m_rec_hdr.basic.height); resize(m_rec_hdr.basic.width, m_rec_hdr.basic.height); -// QDesktopWidget *desktop = QApplication::desktop(); // =qApp->desktop();也可以 -// //move((desktop->width() - this->width())/2, (desktop->height() - this->height())/2); -// move(10, (desktop->height() - this->height())/2); + QDesktopWidget *desktop = QApplication::desktop(); // =qApp->desktop();也可以 + qDebug("desktop width: %d", desktop->width()); + //move((desktop->width() - this->width())/2, (desktop->height() - this->height())/2); + move(10, (desktop->height() - this->height())/2); } QString title; diff --git a/client/tp-player/mainwindow.h b/client/tp-player/mainwindow.h index 386010b..f53d309 100644 --- a/client/tp-player/mainwindow.h +++ b/client/tp-player/mainwindow.h @@ -3,6 +3,7 @@ #include #include +#include "bar.h" #include "thr_play.h" #include "update_data.h" #include "record_format.h" @@ -35,6 +36,8 @@ private: QPixmap m_canvas; + Bar m_bar; + bool m_show_bg; TS_RECORD_HEADER m_rec_hdr; diff --git a/client/tp-player/res/bar.psd b/client/tp-player/res/bar.psd new file mode 100644 index 0000000000000000000000000000000000000000..8348da111a1162ccaaab3df8f6b894bf9cc141ed GIT binary patch literal 717957 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

BOLJp&ZCZF&Ux>7bq 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 OC|a@OcAby*!Ry~IeKuAA literal 0 HcmV?d00001 diff --git a/client/tp-player/res/bar/bg-right.png b/client/tp-player/res/bar/bg-right.png new file mode 100644 index 0000000000000000000000000000000000000000..8cbb474038031ddc5f3cf26beb0c09ada9f91be3 GIT binary patch literal 1103 zcmbVLPiWIn7>}FGQJ6T!go%fksmx)UKW&pHtgcPdZWS|US6vw*OOw|%Y{`qso9#A1 zQ70RUSC1lIL`3kcpw3gjxM@jVsR|5 zFV5S1OfIB^-k8 z49$w7=yPyf5L*Pj1siGApzZay7!=^?uIV5X+N95@72!OJQ#jH_3YMd)+r+llEEFyo zUDX_#4KcK3`FYi?J(L4~!r0N;%P%;9&H)e3yE^VislNrrwcFj1Z-}Fj&blUUiZ-V} zecl2#$|`XR{|OnUA@QQX$eD0DswhlaWO4d(Mih7^BB#^55RqFPJ7Hr27fXf(L1AJF z%cjK`FJ$C2mx+i;EK;!+HfwuGvvtt&Yhu4mEWayOO1VHo(9J_Q*P4LwGDOfTLx)UF z@Z`XxX&bQOo$%Y!$W{Tac?B357g}V!zLL3v|D?dlOhjP#=)dxAV2%H&8I6OX{o(j) zSXx(j0)2IR^zmVPd4P>K#>K1AckI$FyjIt;N-|&lRQ$QMHQdf#?_Rn8aP=4QZ7K1x zcjd#)y@}hb_VN9m+En6Vpr$d&<86Eg@w}T2BwybtET66ou9FvzynVcOZuwDQr1#$7 z;q$R4PX4QZadhBk^6A=AZSYIx?pjyk>i*jF8)~y>WkXh_eH(2DK2H*z48ipr{Ju#{ Qt@|sUO^+$h??6A zBnnf3sas)VKw>ICfPn&KK!OFS5@JN`s0$NPIp+(8(gDl9d++={&-?vwcINUx|JiQ7=Jg}`aKc6Z{1uu*1_SCA6ORFFEPnhF7zu_^L0u+RTLYIhR_%hh`%60 zMNuM-L?X^K;=zhbjWF&8gB=D92c~a()Fv*77>z1frfHse+6uw(3WYAQ8?+O}QYMCm zCqh9I9Vd#bsU6TF{^Q1h+Ch26!(tH!WZ5@aJ!*p;Fw5QHh9X7gje5nmSy7B7jhM?0 zc4=Nq^X!jc*_MhFS<2)_^pvJax&m4FnVcdcDVfoABquW+j)Sl=%IQf6bDA`&L8vRE zNQQbcB_S=RGr5b+y8$&^6L;*|%x)X2ABt78J~k-v%Y-a-I$*X=C<*Gs1KAP+!}GRl zkw$PKDo-m~4g2;TY~_67fM$MG`vCisav~!oWeKJJ6?Y43{ZGzB=8PCM$6w9Txndm{ ziQU#`hwkuTmrab%M&r}f+kPxo<6tTyZfwX z=f=d{*!sOU<;29?+oKypl?}E?+b?zqsJ%PCcVc~aW60P$@_4wf@*RGAbbXDBap2?0 Ur!Q|^`x%XGUZ2snGB+Rm1{3p4-T(jq literal 0 HcmV?d00001 diff --git a/client/tp-player/res/bar/btn-mid.png b/client/tp-player/res/bar/btn-mid.png new file mode 100644 index 0000000000000000000000000000000000000000..07c0acc55ba55ce55c53a6a5b1ceed7765cd031f GIT binary patch literal 1004 zcmbVLy>HV%6t`Nbsw&FFQp9j_mm(4S&auz66;m~_LnDz=r4bDqj(usY);?ojiQAEl zoq-XtupxE^Kuj<&FtV~D@ed%C=X}9XI$+tpcX#jid*4s@cJHjNTwjqSX|=WEc3@qA z@7nSb{9jo=Uc#~_yq?(S12OV5CT)kj&rmD&4_Sx#;pwAKtR_i|gQ(jRz4l!@;IZPD zI!Y0zfR?1%R+0L_5fiA-4x_}8f4+SuqbPLb1EY=GX@d=;owJPXpY3*ov!lQY<*nPO zR@gugGvT8mK2CDGaO5$s4RU#`%4n=2jvRU7RIj~<8a!jjP^y&x6M`sJh;C3yZXpd5 zO~vri3ejzY+M0=`j||qbaA0@b=F}GK9C;{2YOCsKG*U)Yg=dEc;s4}J1!vTN7?hf=1_cIJL zpwFl!y7toVN?-KfIh_4Om+M5UkOf>L4Z}xF*1}Z;bjMgmC1hCZk3OI&hUux<`lTR+?Q*7KDN~jq3KyhL09D=E~;A ziYZ%c<~EoL6-wYBVt~+Dbps_#vrS%w%F!{;f~E>tNwY1dDup7L#y$c`ZZc*Hq6i@5 z#6%K8@fL^+Vw@M~3u9tJNkSzqf%d~vYra)eN@~7si|*2Foe)pq`9`C`H6}UiFY}@- z%MnLB9-|tuVBIA~7;}Ts4ugsU)3-fhV;4k>MisA-G)p~gh2VIFLYLSL+KHkm<3q#a zMNZ%yCyJ}79gq_G?Z&CvL3!Ond_7HRmoyevT>k<&CNC2}2()3A~xW)q?$t3pZ@MNLjY zNz@Wa0jhb8%3ZAP2E=ep)Uj(*yKQXhpIBwuM+U)u8ROMX2Ndg=;Gm8@Fuef5#G>t5 zxDnip%F~KgMZWzQS$Q8jpqXFAKE-}kisyucB*5g~;%;HB|H&Coo#CVA_@h}mN3;VY zvD^A|(;Xh<(uwiuXbhx|pVP6b=xVkcey;vJImz_E{j&#y#qT5F$(e&6n@1y~F9sp_ z$b992K0fwkZ}X^k=-yUpww`}F_Fz}vfBWj3-*>Y8?);_KJ-fq0nfF)cFWi_} ZXo=ZAyf%J(Vd7;pySg@~?&OxXegV3FO6&jt literal 0 HcmV?d00001 diff --git a/client/tp-player/res/bar/btnsel-left.png b/client/tp-player/res/bar/btnsel-left.png new file mode 100644 index 0000000000000000000000000000000000000000..a84e74b282b20a94618536fc5e34ee2f8c633a6f GIT binary patch literal 1044 zcmbVL%TLrm7_ShGkPtP|MCCFmnt*ZpVmqY`71?c97FZ<9Caj4Ey6r66Ks%+Kbqgnu zi^ih|ABpi`xEYRKJO~LKym~VJ6UKu#3{Ll@2jpPWcD|YUe!uVibbjvMK>ziAhG7Qs zQ$~@l=jeC&eDdG@GW(S-*N9mr3%E)eR*0BM2Uiftd)6{4BFkA{`;5jJrnl;r%A{PF z)oko>R-(hjUO>?dGd>Xqmc4=qsGw!n*V*qMKe51dbaqiL2!$YvYVOoVh!!^HO7_Nz ztvc+)9WWkil)yv80Yf=T-z4i>1>UVK;!vFqro*sIUFwYqN=J1 zha{z_Mk-qOi4~{(XsE+rpvVs0fVkKP38PiPb)vJ>(^d#xP$+bX{ivNNnle7N0$$_< z-t&^Un%WU5qCak&s2!Eo1H>0mgzKSA>rox*fNAbdH+xQ+%@MabkvP1!I6Qx$3WnVhOXA)PTzsH8I;j+3woG_#77Qw(9u5JgiRgNkUT zWdRyFlgeFe-j9go+o)sLrFPp`cq&%QhR7l~EMdIb>45nfCOE3$0Av>+7+!RJ2REXT zq&%%?4HUYMkdq6s2b%fS+!O3iDpE#BD*}}N6?Y5k{7=q!>I|PW$6w9TIiekyh~3tw zo9^%+pH56jN8`=ThY>nfM&6h##osDN$Hx=BaIa_Q)n>Wx$>G+^D-U{Z3~#^R?wfx0 zZt(2$rNf*1!mTat$KZ>DnTMC^7p7(^b literal 0 HcmV?d00001 diff --git a/client/tp-player/res/bar/btnsel-mid.png b/client/tp-player/res/bar/btnsel-mid.png new file mode 100644 index 0000000000000000000000000000000000000000..03c01ea4ecd3c78e50653c0f4e1ca637ed20a4ee GIT binary patch literal 1011 zcmbVLJ#W)M7`BL16+|T@IsntjSwJH8*^SS&Ra3>WLnDz=rIDJkj(usY#6EMr61O88 z5`Tb|g^h&~Ay)VcEG&#j>@4MVzF;UFux#IZcklDO-!BjLA8*{acSDk-jrN|`h4mVI zo9k=ve{21C1Iu0E_r)O_ifO>9)QH%CqIMFT&@K(4vu9uEjwDrvaj!4>ohNq45+x{g zlp@IhElE4OMHYl(D$szQ#Hl0y{P0OeapcHHMhADYCLP6l=bRp%@AtyXN~udC22T#4JT6V=!Q+!jZw#c?e0?>8yy;e32+fnOX#yswr4ak}|HDb}qW~ zj~kb2=e@Ixs$H71QyxM+hSen)a(A_%QW3ndpYj-r5==Z6o+dOEZO@V6Mv3CcCYFia z)|PL09`-E_!gpJiNpRiuePY(#CC6o0llVAchMr@itD9baC)}uUrPNNoQ z37X~CjxVv_Fm)H#O-zh`#XZMH|C2KnoKef>_^Vl#N6>+#xN3datcHiCFfklPV{bz5 zz*tq=UZYoh9sIhu*sZL;#~5!*TIF_S^CnrVetwOlTgTG))%C#q`0aZ+hHZb}d*>d% F_zkCWGiv|< literal 0 HcmV?d00001 diff --git a/client/tp-player/res/bar/btnsel-right.png b/client/tp-player/res/bar/btnsel-right.png new file mode 100644 index 0000000000000000000000000000000000000000..cfa0078fe2a7735e24e6d82cf88d66222cd6bba7 GIT binary patch literal 1047 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 BGlT#D literal 0 HcmV?d00001 diff --git a/client/tp-player/res/bar/prgbar-right.png b/client/tp-player/res/bar/prgbar-right.png new file mode 100644 index 0000000000000000000000000000000000000000..d7d3f3f90b7fe7254023ee5bdc7ef47034675055 GIT binary patch literal 1049 zcmbVLJ#W)M7h*S-?B zQ(G|59ioG%zk2Q1t7-rf5=@Au>R%I%5qOXCc~ zOqg?em98W78#|f&C!gJaPM3?ssFMZUByB51Oy0o_1WeCbLRDlr>#Ofkj$wwIZmmx0 zoHqf)as zmTkphXKsO9tWp9G5evlLiXW-5#&&pBDksN03py%fS!26S)ywmsh(iPEM9p!cxUTIe*@s$S~ZqC1Uk5fZ38-)^_L_B4mXC0H!&6#5)TVP&X``Mjj)LRJ?=L&-u} zG%}I^^^!s50oL>*V)-`e+jXhk9yWU{RxO6eA~>vJywdN0`4%QPYT*DB7a+K{==u(B zN7s|`bfeW#=srYFDa0P=h7 zTAywP!-IS}F(Dm|FK<5W)3Gv5JztAIHhvx+&J4r-69;EzPe1)Na&UI*$A!7=v6q{Z zJ9}I8Q#Bh;`bN$G literal 0 HcmV?d00001 diff --git a/client/tp-player/res/bar/prgbarh-left.png b/client/tp-player/res/bar/prgbarh-left.png new file mode 100644 index 0000000000000000000000000000000000000000..4f936ff94b33cc147d59f9bc6962eb4ed225d16d GIT binary patch literal 1044 zcmbVLJ#5oJ6gH{^ZKX=UMyHd@03xx^iDTDR(>BgIG*S|xG*Tl0QXTu!Sgn1=z7V%8 z2nmVKhy{s-fdz?y1qlW^bO0oF#KzDW*bqW0=lm3g(t(rh`|8-PqTMwGWni+eDevLE>N>YS4oHVtq^kshqN&$d)5YSV9VLQ^&T&9+(gH1 zwrHzb(QV=hR$?Q>Uck^Cx3Clkmc5B7XyXmn&+}j3yyb!G|ik?xyXz)xebUJzil}1`@m*=~b2D&Kr`+cFG6-c-t zN}8r69GOg-S)`+FpIUL+kLJb<2uF742Gk`!NEod)>CrsTJROGM1=Z>iu^){R#Zo55 zRv=0O6g@AAYhWGG2L9v5vDQ&@JHTQCN2C|p>^?ekV=&9z$wG-C^G3fGy6jS{Ekx{| zhkaT`d7k|d9M{nmO@&4&YvvGwrY5oQjgqD+P&Q0cQDtMyaU9lAa%>nHg7Zj{Os$~F zP(#R=hgnILHF=CJ`w_K#8;^BeraQvUpNQ3qA+{(9n}lqQE1=#bltf(;fa0nGF0Hw~ zL;BI>fQND{pZx{1Gszt$Bl!@-lNpZ=a5^zdy1>QnpK~E_@*R%S~hFwg|U9` EHyWfxr2qf` literal 0 HcmV?d00001 diff --git a/client/tp-player/res/bar/prgbarh-mid.png b/client/tp-player/res/bar/prgbarh-mid.png new file mode 100644 index 0000000000000000000000000000000000000000..17705f7835875d24035945b2841fdb06f7ba4e0f GIT binary patch literal 1007 zcmbVLO>fgc5Ot|kN~B60feS150ur&;i9c$qrix>SMk1w3BQ-}Hd(&90z3zHL+#Eon z2h=}W1c;0(6vomMAJI~fC4=REnthKk@F3h*! zy|cOk-+RZuzQJ^#dwsq~hkW8QA~ZreAgCSt2c%2<@Z|Ul*%XB3VbtsMe&@Lr&{*;d z8!3-dKnud=R-XF7A>n914x+>szkmEBqA0Y*eXWB#X_Jhi?I|OB)16*0Jq*lH+ zN=?S_=v76vG~KEhXfZ@kW8u*1x~+v4>}+wwd1}e>WHON^b&0YASusts;HcHAU{TFZ z67J{KBwH^TT#^MWN_j*RR51DjI_9>a4G(gOM`Jt65e)g5&lSF6k57z zV5e30G}pzRsX+Kn%QSSXI-aK+s#9`Yhjnxfh6BhAS5Z8(VXD}4U1tN=6+<=E65CEP z?k52$btBMSV4F8$ttKNrr>sZmQCR`IBg$zuqA6FCjZl!SDWJua?i9<57Q^ooQn zMq@O~uN7UR-!N(pRt>Cc|B8E#4gV)+GB_g_#qn3Mlt)m3#qz53VRO|yB!P}$&>HcF z^9yLJhi$jf%fAkOUS4i3ubvA(Uf(@ETdHgi@aK!ARV*0Om5bBcZ{8Ix*!FhZ_s+|= FzX8ZDH5LE> literal 0 HcmV?d00001 diff --git a/client/tp-player/res/bar/prgpt-hover.png b/client/tp-player/res/bar/prgpt-hover.png new file mode 100644 index 0000000000000000000000000000000000000000..51cb23232c33e3bdceed7225c8963e73befc6bc8 GIT binary patch literal 1126 zcmbVL-D}fO6wfAXS{P3Eq=H^DhYGeyx;8O!buLY+9d;{ialmlk-_{oLKl=V=KN)smbr@$+9+xy1Qhf#*fgovV1(<=FF**JU z_E6N8yqO!u!>Ln}j;xU8+l0!NL(mk}(_3~leH3Ca0t==cqrW|SP6N}3(L>P`n{wi? zXr7#MVRmXDr%#RQqCxi_13hJl5LggvplprVo>Y#}HC~C#{cVN@H4{7F`0O}7-)LbJkWt2o+i>zXF5o1SU zm{O?}Ds_jDTVOa*6n&0xI7lpl-lUDSa?tiV8w?8cbk}sSiEQ9AY9nX@$7tf|S_qbt zO05&yUOiDHWlULf7%s#zmgUD)v-WTX{&izRYcDtHKqdn{G~w#xKJuLnFv;EKLcSsK zMjCWYaw*!Fg7gUs+Bm7iXmS!VOhe*Dft3^8YE)5JRpdzcazYe%HX^GkFGS=9$41yN zCvZoi5nf@Bs2rz?a(9AD2&&wdU?aT1MH<+o?P1Nx^--HQ>+wsp@xy0Lujl~ z0qG*b$SWcT#Irm&IAq!eDtU+e>sgCdfv$ND8VMI!pq5|B+(5ri2+M3lV0q%f#&NG< zjsM9RL!4p!;`pan8e60S{pot?lf`=TK$~=oOIl-Y$$3NCYA&ht<;ovNepRczTNci5 zyENa{&a{3xaQs(CKQnXjUh}o(^xNi2w)(XdOrDrmBOB#@lP$WI}h3) zjt8D9i~Y$!{O6(zrURAUyFc9d>~*fQYasw$P-o;DR|_BB$NkPvssqX+`Rs*1NJMQn literal 0 HcmV?d00001 diff --git a/client/tp-player/res/bar/prgpt.png b/client/tp-player/res/bar/prgpt.png new file mode 100644 index 0000000000000000000000000000000000000000..0973ffd75e33c4f585d067b9ea37f64fb0ae916c GIT binary patch literal 1151 zcmbVMO=#0l9M7l@oen<_ejI$H^JB6$Nk7shuFkeewPNe+3RXlkOJ3KwB`-_f)@%oH z195`lE<;cd6`3GRcan(*J$O)Yr^y7psCX1aL=Y78Wm~6*xq~6e`^fM2`+vOuKNs3^ z`|Inr)KL^wpE)4q$+*UQR@9RBo|(~uWY~=5B5p?|Tv2RDB{b9lLB>!zVIC@4f6sH+ zLQ%ewUMS*X_K>I|BdBuH%`y$ie&C>Gc=fm;I245=TtG<22#j| zAQ}t@RF>la&j-0kl;^o75MsFy!;+g1a1k-ei=h~pzi493)=FYtO3&LOt2kZ8*b*70 zQmF(h;UKa*8BP!cPa_lx5JbS~H?iUdOvhhUkf5X5x`lOQ0#8xtK)pCl6Hn(tFsy8L zLD+QW6Gc+SxQfMaL6$KLFRod%gY)o@8;hcyLcax>Jaka6t&;O7`Kx4-yQK|zK;n&f z*w)FRDBTiLdktvfj1;HIm!PI=A}_?)WI8NIC5e>{~8-vA-!zX=T6mb3S*jV4PV*&rm(v2M>i4B9zt1iRovy)W$8N%1@_7y25%Av*fi6Yw}|9X1iTB zWtZVaya?jJg9i^X6ptSCIK`_MFM@dAK@f$(eCd}S<_?A=@BiNW|9=1PoyCReaQ}sV zilV}~8MQ#xNbsHRBmd#=M><)CuvWrLsEq5n2dR{aRv^gPdIc7sZm!>c4<{%pRJMvG zT*}YO2C^AF&|w<3OVAWGG1+i+V-;es0xOoI&_dV4@)t0vlo-H0+w=%MFEY^2$UGj#(NsRq(1px11{F7eNMj5G0v+)Zlm? z2m-?=l7hfr1u>3~u^f4YD4&p%f*cb;`=N<7&n(LYHQTmDb_!j^*p*qfUavFtID@y80Cb}eGJjm?~hl`|gHG4hHC)jA!pSVb85Rpf%qk^n9( zTaJn9{^g)Nt!P!~S$CkB^^gsk`IW6>?5D(7noEeBko;HNEv)%JIb(@4Y|tEkHB0A+ zbYLKMTc2#Y!vh^MF&-I>YtyGj$yn8LYO2`yymEMSG#MJ*>Ya`EygD1XAL6g{D4Sb9 zdm~>$UpKkX)MGgFa`4%Y;r+Ae15*~a4d6x%+Sq; v`aA#IM?5e)2X@YUm>-zBn4NtcpW5o7KFtlD+ueThE12h;wxI5%Z{7P1ApcN} literal 0 HcmV?d00001 diff --git a/client/tp-player/res/bar/selected.png b/client/tp-player/res/bar/selected.png new file mode 100644 index 0000000000000000000000000000000000000000..c0067f5ca8a2d5c66d20a3b0658fc10305cad2e0 GIT binary patch literal 1159 zcmbVMO=#0l9M7n8!w;w;VinvYbAF(0k~U42xVo)NYb$n+uCRh2rpfCTx8%j-&35y! zIj7+CF!7)W17UM`8VZ8=0V0Y_cJbuFiy}A?4kR$mwP{Ns>H`a5zX1L3hByYB^}R^%VsLx|(Cy*gzKW6x9Oi$1$3Cx)g$G zXEH0omb;uNk}{^O+6)(B8PoLQT12}z3;(#WD%#Br*pSIW7xg{NFp0gMiNm)VG|NZ!jC5^8e(@Z4Oj1a&idu zRVtvP2Vvy)ARDxG3Se)SVd<#k?(>dkDOv?O#u=z59b|&V{L02E_FKhpoaIGUAl|PY z_YznCpPVto8OAG)zlx=@MJmvnu9QAmtTYd_NXIy&H4feX93^e_Ag#3K%5Mw578aU) z4a3z(B31V``!D;r{Z+A{;h)w1cfJoptgq!JOx@iw^`maqzx&Ru#=y+=QEB2_ZTItG z-v5REqSDWj#ziBdXy$U3*rpB&Do2x&}jc#apGc__ju+u-ZN2g{l zmhycsUtgJ{#^*n##!jDl`KkKG^ueF}d8(dg^4G?n=Qe-w@dMO>&BlYHl}- kzo>=NHM1wc4riClblV=MP$Gl%dI?=Ao#7~a=20U res/bg.png res/cursor.png + res/bar/bg-left.png + res/bar/bg-mid.png + res/bar/bg-right.png + res/bar/btn-left.png + res/bar/btn-mid.png + res/bar/btn-right.png + res/bar/btnsel-left.png + res/bar/btnsel-mid.png + res/bar/btnsel-right.png + res/bar/play-hover.png + res/bar/play.png + res/bar/prgbar-left.png + res/bar/prgbar-mid.png + res/bar/prgbar-right.png + res/bar/prgbarh-left.png + res/bar/prgbarh-mid.png + res/bar/prgpt-hover.png + res/bar/prgpt.png + res/bar/select.png + res/bar/selected.png From 00c79c47e258c525b8679704bd849d8202c48904 Mon Sep 17 00:00:00 2001 From: Apex Liu Date: Mon, 9 Sep 2019 03:30:31 +0800 Subject: [PATCH 14/44] =?UTF-8?q?=E6=92=AD=E6=94=BE=E5=99=A8=E6=B5=AE?= =?UTF-8?q?=E5=8A=A8=E6=8E=A7=E5=88=B6=E7=AA=97=E7=BB=98=E5=88=B6=E6=AD=A3?= =?UTF-8?q?=E5=B8=B8=E4=BA=86=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/tp-player/bar.cpp | 566 +++++++++++++++++++++-- client/tp-player/bar.h | 135 ++++-- client/tp-player/mainwindow.cpp | 149 +++--- client/tp-player/mainwindow.h | 23 +- client/tp-player/record_format.h | 2 + client/tp-player/res/bar/pause-hover.png | Bin 0 -> 2234 bytes client/tp-player/res/bar/pause.png | Bin 0 -> 2295 bytes client/tp-player/res/bar/prgbar-left.png | Bin 1043 -> 0 bytes client/tp-player/thr_play.cpp | 34 +- client/tp-player/tp-player.qrc | 3 +- client/tp-player/update_data.h | 19 + 11 files changed, 776 insertions(+), 155 deletions(-) create mode 100644 client/tp-player/res/bar/pause-hover.png create mode 100644 client/tp-player/res/bar/pause.png delete mode 100644 client/tp-player/res/bar/prgbar-left.png diff --git a/client/tp-player/bar.cpp b/client/tp-player/bar.cpp index cb034a8..ed28a98 100644 --- a/client/tp-player/bar.cpp +++ b/client/tp-player/bar.cpp @@ -1,57 +1,551 @@ #include "bar.h" - #include +#include + + +#define FONT_SIZE_DEFAULT 12 +#define TIME_STR_PIXEL_SIZE 16 +#define TEXT_COLOR QColor(255,255,255,153) + +typedef struct RES_MAP { + RES_ID id; + const char* name; +}RES_MAP; + +static RES_MAP img_res[res__max] = { + {res_bg_left, "bg-left"}, + {res_bg_mid, "bg-mid"}, + {res_bg_right, "bg-right"}, + {res_bs_left, "btn-left"}, + {res_bs_mid, "btn-mid"}, + {res_bs_right, "btn-right"}, + {res_bsh_left, "btnsel-left"}, + {res_bsh_mid, "btnsel-mid"}, + {res_bsh_right, "btnsel-right"}, + {res_pbh_left, "prgbarh-left"}, + {res_pbh_mid, "prgbarh-mid"}, + {res_pb_mid, "prgbar-mid"}, + {res_pb_right, "prgbar-right"}, +// {res_pp, "prgpt"}, +// {res_pph, "prgpt-hover"}, + {res_cb, "select"}, + {res_cbh, "selected"}, +// {res_play, "play"}, +// {res_play_hover, "play-hover"}, +// {res_pause, "pause"}, +// {res_pause_hover, "pause-hover"} +}; + +typedef struct SPEED_MAP { + int id; + const char* title; +}SPEED_MAP; + +static SPEED_MAP speed[speed_count] = { + {speed_1x, "1x"}, + {speed_2x, "2x"}, + {speed_4x, "4x"}, + {speed_8x, "8x"}, + {speed_16x, "16x"} +}; + +static inline int min(int a, int b){ + return a < b ? a : b; +} + +static inline int max(int a, int b){ + return a > b ? a : b; +} Bar::Bar() { + m_img_ready = false; + m_width = 0; + m_height = 0; + m_str_total_time = "00:00"; + m_str_passed_time = "00:00"; + m_str_passed_time_last_draw = "--:--"; + m_percent = 0; + m_percent_last_draw = -1; + + m_play_hover = false; + m_playing = true; // 0=play, 2=pause + m_speed_selected = speed_1x; + m_speed_hover = speed_count; // speed_count=no-hover + m_skip_selected = true; } Bar::~Bar() { } -bool Bar::init(QWidget* owner, int width) { +bool Bar::init(QWidget* owner) { m_owner = owner; // 加载所需的图像资源 - if(!m_bg_left.load(":/tp-player/res/bar/bg-left.png") - || !m_bg_mid.load(":/tp-player/res/bar/bg-mid.png") - || !m_bg_right.load(":/tp-player/res/bar/bg-right.png") - || !m_btn_left.load(":/tp-player/res/bar/btn-left.png") - || !m_btn_mid.load(":/tp-player/res/bar/btn-mid.png") - || !m_btn_right.load(":/tp-player/res/bar/btn-right.png") - || !m_btnsel_left.load(":/tp-player/res/bar/btnsel-left.png") - || !m_btnsel_mid.load(":/tp-player/res/bar/btnsel-mid.png") - || !m_btnsel_right.load(":/tp-player/res/bar/btnsel-right.png") - || !m_prgbarh_left.load(":/tp-player/res/bar/prgbarh-left.png") - || !m_prgbarh_mid.load(":/tp-player/res/bar/prgbarh-mid.png") - || !m_prgbar_left.load(":/tp-player/res/bar/prgbar-left.png") - || !m_prgbar_mid.load(":/tp-player/res/bar/prgbar-mid.png") - || !m_prgbar_right.load(":/tp-player/res/bar/prgbar-right.png") - || !m_prgpt.load(":/tp-player/res/bar/prgpt.png") - || !m_prgpt_hover.load(":/tp-player/res/bar/prgpt-hover.png") - || !m_select.load(":/tp-player/res/bar/select.png") - || !m_selected.load(":/tp-player/res/bar/selected.png") - || !m_play.load(":/tp-player/res/bar/play.png") - || !m_play_hover.load(":/tp-player/res/bar/play-hover.png") - ) + int i = 0; + for(i = 0; i < res__max; ++i) { + QString name; + name.sprintf(":/tp-player/res/bar/%s.png", img_res[i].name); + if(!m_res[i].load(name)) + return false; + } + + // 无需合成的图像 + if(!m_img_btn_play[play_running][widget_normal].load(":/tp-player/res/bar/play.png") + || !m_img_btn_play[play_running][widget_hover].load(":/tp-player/res/bar/play-hover.png") + || !m_img_btn_play[play_paused][widget_normal].load(":/tp-player/res/bar/pause.png") + || !m_img_btn_play[play_paused][widget_hover].load(":/tp-player/res/bar/pause-hover.png") + || !m_img_progress_pointer[widget_normal].load(":/tp-player/res/bar/prgpt.png") + || !m_img_progress_pointer[widget_hover].load(":/tp-player/res/bar/prgpt-hover.png") + ) { return false; + } - // 创建背景图像 - m_bg = QPixmap(width, m_bg_left.height()); - m_bg.fill(Qt::transparent);//用透明色填充 - QPainter pp(&m_bg); - pp.drawPixmap(0, 0, m_bg_left, 0, 0, m_bg_left.width(), m_bg_left.height()); - pp.drawPixmap(m_bg_left.width(), 0, m_bg.width() - m_bg_left.width() - m_bg_right.width(), m_bg_left.height(), m_bg_mid); - pp.drawPixmap(m_bg.width()-m_bg_right.width(), 0, m_bg_right, 0, 0, m_bg_right.width(), m_bg_right.height()); - - //pp.drawPixmap(10, 10, m_prgpt, 0, 0, m_prgpt.width(), m_prgpt.height()); - pp.drawPixmap(10, 10, m_play.width(), m_play.height(), m_play); - + m_height = m_res[res_bg_left].height(); return true; } -void Bar::draw(QPainter& painter, const QRect& rc){ - painter.drawPixmap(10, 150, m_bg, 0, 0, m_bg.width(), m_bg.height()); +void Bar::start(uint32_t total_ms, int width) { + bool is_first_start = (m_width == 0); + m_width = width; + + m_total_ms = total_ms; + _ms_to_str(total_ms, m_str_total_time); + + + // 首次播放时,调整位置左右居中,距窗口顶部10点处。 + if(is_first_start) { + _init_imgages(); + QRect rc = m_owner->rect(); + m_rc = QRect(0, 0, m_width, m_height); + m_rc.moveTo((rc.width() - m_width)/2, 10); + //m_rc.moveTo(10, 600); + qDebug("m_rc (%d,%d)-(%d,%d)", m_rc.left(), m_rc.top(), m_rc.right(), m_rc.bottom()); + } } + +void Bar::end() { + if(m_passed_ms != m_total_ms) + update_passed_time(m_total_ms); +} + +void Bar::_init_imgages() { + m_img_bg = QPixmap(m_width, m_height); + m_img_bg.fill(Qt::transparent);//用透明色填充 + QPainter pp(&m_img_bg); + QFont font = pp.font(); + + // 合成背景图像 + { + + pp.drawPixmap(0, 0, m_res[res_bg_left].width(), m_res[res_bg_left].height(), m_res[res_bg_left]); + pp.drawPixmap(m_res[res_bg_left].width(), 0, m_width - m_res[res_bg_left].width() - m_res[res_bg_right].width(), m_height, m_res[res_bg_mid]); + pp.drawPixmap(m_width-m_res[res_bg_right].width(), 0, m_res[res_bg_right].width(), m_height, m_res[res_bg_right]); + } + + { + m_rc_btn_play = QRect(15, (m_height - m_img_btn_play[play_running][widget_normal].height())/2 , m_img_btn_play[play_running][widget_normal].width(), m_img_btn_play[play_running][widget_normal].height()); + } + + // 合成速度按钮 + { + int w = 42, h = m_res[res_bs_left].height(); + QRect rc(0, 0, w, h); + QPixmap btn[widget__max]; + + // 未选中状态 + btn[widget_normal] = QPixmap(w, h); + btn[widget_normal].fill(Qt::transparent);//用透明色填充 + QPainter pn(&btn[widget_normal]); + pn.drawPixmap(0, 0, m_res[res_bs_left].width(), m_res[res_bs_left].height(), m_res[res_bs_left]); + pn.drawPixmap(m_res[res_bs_left].width(), 0, w - m_res[res_bs_left].width() - m_res[res_bs_right].width(), h, m_res[res_bs_mid]); + pn.drawPixmap(w-m_res[res_bs_right].width(), 0, m_res[res_bs_right].width(), h, m_res[res_bs_right]); + // 选中状态 + btn[widget_hover] = QPixmap(w, h); + btn[widget_hover].fill(Qt::transparent);//用透明色填充 + QPainter ph(&btn[widget_hover]); + ph.drawPixmap(0, 0, m_res[res_bsh_left].width(), m_res[res_bsh_left].height(), m_res[res_bsh_left]); + ph.drawPixmap(m_res[res_bsh_left].width(), 0, w - m_res[res_bsh_left].width() - m_res[res_bsh_right].width(), h, m_res[res_bsh_mid]); + ph.drawPixmap(w-m_res[res_bsh_right].width(), 0, m_res[res_bsh_right].width(), h, m_res[res_bsh_right]); + + for(int i = 0; i < widget__max; ++i) { + for(int j = 0; j < speed_count; ++j) { + m_img_btn_speed[j][i] = QPixmap(w, h); + m_img_btn_speed[j][i].fill(Qt::transparent); + QPainter ps(&m_img_btn_speed[j][i]); + ps.setPen(TEXT_COLOR); + QFont font = ps.font(); + font.setFamily("consolas"); + font.setPixelSize(FONT_SIZE_DEFAULT); + ps.setFont(font); + ps.drawPixmap(0, 0, w, h, btn[i]); + ps.drawText(rc, Qt::AlignCenter, speed[j].title); + } + } + } + + // 合成跳过无操作选项 + { + // 计算显示跳过无操作选项字符串的宽高 + font.setFamily("微软雅黑"); + font.setBold(false); + font.setPixelSize(FONT_SIZE_DEFAULT); + pp.setFont(font); + QFontMetrics fm = pp.fontMetrics(); + + { + int h = fm.height(); + if(h < m_res[res_cb].height()) + h = m_res[res_cb].height(); + m_rc_skip = QRect(0, 0, fm.width("无操作则跳过")+8+m_res[res_cb].width(), h); + } + + int w = m_rc_skip.width(); + int h = m_rc_skip.height(); + int chkbox_top = (m_rc_skip.height() - m_res[res_cb].height()) / 2; + int text_left = m_res[res_cb].width() + 8; + int text_top = (m_rc_skip.height() - fm.height()) / 2; + + { + m_img_skip[widget_normal] = QPixmap(w,h); + m_img_skip[widget_normal].fill(Qt::transparent); + QPainter ps(&m_img_skip[widget_normal]); + ps.setPen(TEXT_COLOR); + QFont font = ps.font(); + font.setFamily("微软雅黑"); + font.setPixelSize(FONT_SIZE_DEFAULT); + ps.setFont(font); + ps.drawPixmap(0, chkbox_top, m_res[res_cb].width(), m_res[res_cb].height(), m_res[res_cb]); + ps.drawText(QRect(text_left, text_top, w-text_left, h-text_top), Qt::AlignCenter, "无操作则跳过"); + } + + { + m_img_skip[widget_hover] = QPixmap(w,h); + m_img_skip[widget_hover].fill(Qt::transparent); + QPainter ps(&m_img_skip[widget_hover]); + ps.setPen(TEXT_COLOR); + QFont font = ps.font(); + font.setFamily("微软雅黑"); + font.setPixelSize(FONT_SIZE_DEFAULT); + ps.setFont(font); + ps.drawPixmap(0, chkbox_top, m_res[res_cbh].width(), m_res[res_cbh].height(), m_res[res_cbh]); + ps.drawText(QRect(text_left, text_top, w-text_left, h-text_top), Qt::AlignCenter, "无操作则跳过"); + } + } + + { + // 计算显示时间所需的宽高 + font.setFamily("consolas"); + font.setBold(true); + font.setPixelSize(TIME_STR_PIXEL_SIZE); + pp.setFont(font); + { + QFontMetrics fm = pp.fontMetrics(); + m_rc_time_passed = QRect(0, 0, fm.width("00:00:00"), fm.height()); + m_rc_time_total = m_rc_time_passed; + } + + m_img_time_total = QPixmap(m_rc_time_total.width(), m_rc_time_total.height()); + m_img_time_total.fill(Qt::transparent); + QPainter pp(&m_img_time_total); + pp.setPen(TEXT_COLOR); + QFont font = pp.font(); + font.setFamily("consolas"); + font.setBold(true); + font.setPixelSize(TIME_STR_PIXEL_SIZE); + pp.setFont(font); + pp.drawText(m_rc_time_total, Qt::AlignLeft, m_str_total_time); + + // 定位时间字符串的位置 + m_rc_time_passed.moveTo(15+m_img_btn_play[play_running][widget_normal].width()+10, 18); + m_rc_time_total.moveTo(m_width - 15 - m_rc_time_total.width(), 18); + + int prog_width = m_rc_time_total.left() - 10 - 10 - m_rc_time_passed.right();// - m_img_progress_pointer[widget_normal].width(); + int prog_height = max(m_res[res_pbh_left].height(), m_img_progress_pointer->height()); + m_rc_progress = QRect(0, 0, prog_width, prog_height); + m_rc_progress.moveTo(m_rc_time_passed.right() + 10, m_rc_time_passed.height() + (m_rc_time_passed.height() - prog_height)/2); + + qDebug("prog: %d,%d w:%d,h:%d", m_rc_progress.left(), m_rc_progress.top(), prog_width, prog_height); + } + + + // 定位速度按钮 + { + int left = m_rc_time_passed.right() + 10; + int top = m_rc_time_passed.bottom() + 8; + for(int i = 0; i < speed_count; i++) { + m_rc_btn_speed[i] = QRect(left, top, m_img_btn_speed[i][widget_normal].width(), m_img_btn_speed[i][widget_normal].height()); + left += m_img_btn_speed[i][widget_normal].width() + 8; + } + } + + // 定位跳过选项 + { + int left = m_rc_time_total.left() - m_rc_skip.width() - 10; + int top = m_rc_time_passed.bottom() + 10; + m_rc_skip.moveTo(left, top); + } + + m_img_ready = true; +} + +void Bar::_ms_to_str(uint32_t ms, QString& str) { + int h = 0, m = 0, s = 0; + s = ms / 1000; + if(ms % 1000 > 500) + s += 1; + + h = s / 3600; + s = s % 3600; + m = s / 60; + s = s % 60; + + if(h > 0) + str.sprintf("%02d:%02d:%02d", h, m, s); + else + str.sprintf("%02d:%02d", m, s); +} + +void Bar::update_passed_time(uint32_t ms) { + QString str_passed; + _ms_to_str(ms, str_passed); + + if(m_str_passed_time != str_passed) + { + m_str_passed_time = str_passed; + m_owner->update(m_rc.left()+m_rc_time_passed.left(), m_rc.top()+m_rc_time_passed.top(), m_rc_time_passed.width(), m_rc_time_passed.height()); + } + + int percent = 0; + if(ms > m_total_ms) { + percent = 100; + m_passed_ms = m_total_ms; + } + else { + m_passed_ms = ms; + percent = (int)(((double)m_passed_ms / (double)m_total_ms) * 100); + } + + if(percent != m_percent) { + m_percent = percent; + m_owner->update(m_rc.left()+m_rc_progress.left(), m_rc.top()+m_rc_progress.top(), m_rc_progress.width(), m_rc_progress.height()); + } +} + +void Bar::onMouseMove(int x, int y) { + // 映射鼠标坐标点到本浮动窗内部的相对位置 + QPoint pt(x-m_rc.left(), y-m_rc.top()); + + bool play_hover = m_rc_btn_play.contains(pt); + if(play_hover != m_play_hover) { + m_play_hover = play_hover; + m_owner->update(m_rc.left()+m_rc_btn_play.left(), m_rc.top()+m_rc_btn_play.top(), m_rc_btn_play.width(), m_rc_btn_play.height()); + } + if(play_hover) + return; + + int speed_hover = speed_count; + for(int i = 0; i < speed_count; ++i) { + if(m_rc_btn_speed[i].contains(pt)) { + speed_hover = i; + break; + } + } + if(m_speed_hover != speed_hover) { + if(m_speed_hover != speed_count) { + m_owner->update(m_rc.left()+m_rc_btn_speed[m_speed_hover].left(), m_rc.top()+m_rc_btn_speed[m_speed_hover].top(), m_rc_btn_speed[m_speed_hover].width(), m_rc_btn_speed[m_speed_hover].height()); + } + m_speed_hover = speed_hover; + if(m_speed_hover != speed_count) { + m_owner->update(m_rc.left()+m_rc_btn_speed[m_speed_hover].left(), m_rc.top()+m_rc_btn_speed[m_speed_hover].top(), m_rc_btn_speed[m_speed_hover].width(), m_rc_btn_speed[m_speed_hover].height()); + } + } + + // TODO: more hover detect. +} + +void Bar::draw(QPainter& painter, const QRect& rc_draw){ + if(!m_width) + return; + if(!rc_draw.intersects(m_rc)) + return; + + // 绘制背景 + { + QRect rc(m_rc); + //rc.moveTo(m_rc.left()+rc.left(), m_rc.top() + rc.top()); + + int from_x = max(rc_draw.left(), m_rc.left()) - m_rc.left(); + int from_y = max(rc_draw.top(), m_rc.top()) - m_rc.top(); + int w = min(m_rc.right(), rc_draw.right()) - rc.left() - from_x + 1; + int h = min(m_rc.bottom(), rc_draw.bottom()) - rc.top() - from_y + 1; + int to_x = m_rc.left() + from_x; + int to_y = m_rc.top() + from_y; + painter.drawPixmap(to_x, to_y, m_img_bg, from_x, from_y, w, h); + } + + // 绘制播放按钮 + { + QRect rc(m_rc_btn_play); + rc.moveTo(m_rc.left()+rc.left(), m_rc.top() + rc.top()); + if(rc_draw.intersects(rc)) { + int from_x = max(rc_draw.left(), rc.left()) - rc.left(); + int from_y = max(rc_draw.top(), rc.top()) - rc.top(); + int w = min(rc.right(), rc_draw.right()) - rc.left() - from_x + 1; + int h = min(rc.bottom(), rc_draw.bottom()) - rc.top() - from_y + 1; + int to_x = rc.left() + from_x; + int to_y = rc.top() + from_y; + if(m_playing){ + if(m_play_hover) + painter.drawPixmap(to_x, to_y, m_img_btn_play[play_running][widget_hover], from_x, from_y, w, h); + else + painter.drawPixmap(to_x, to_y, m_img_btn_play[play_running][widget_normal], from_x, from_y, w, h); + } else { + if(m_play_hover) + painter.drawPixmap(to_x, to_y, m_img_btn_play[play_paused][widget_hover], from_x, from_y, w, h); + else + painter.drawPixmap(to_x, to_y, m_img_btn_play[play_paused][widget_normal], from_x, from_y, w, h); + } + } + } + + // 绘制已播放时间 + { + QRect rc(m_rc_time_passed); + rc.moveTo(m_rc.left()+rc.left(), m_rc.top() + rc.top()); + if(rc_draw.intersects(rc)) { + if(m_str_passed_time != m_str_passed_time_last_draw) { + m_img_time_passed = QPixmap(m_rc_time_passed.width(), m_rc_time_passed.height()); + m_img_time_passed.fill(Qt::transparent); + QPainter pp(&m_img_time_passed); + pp.setPen(TEXT_COLOR); + QFont font = pp.font(); + font.setFamily("consolas"); + font.setBold(true); + font.setPixelSize(TIME_STR_PIXEL_SIZE); + pp.setFont(font); + pp.drawText(QRect(0,0,m_rc_time_passed.width(), m_rc_time_passed.height()), Qt::AlignRight, m_str_passed_time); + + m_str_passed_time_last_draw = m_str_passed_time; + } + + int from_x = max(rc_draw.left(), rc.left()) - rc.left(); + int from_y = max(rc_draw.top(), rc.top()) - rc.top(); + int w = min(rc.right(), rc_draw.right()) - rc.left() - from_x + 1; + int h = min(rc.bottom(), rc_draw.bottom()) - rc.top() - from_y + 1; + int to_x = rc.left() + from_x; + int to_y = rc.top() + from_y; + painter.drawPixmap(to_x, to_y, m_img_time_passed, from_x, from_y, w, h); + } + } + + // 绘制总时间 + { + QRect rc(m_rc_time_total); + rc.moveTo(m_rc.left()+rc.left(), m_rc.top() + rc.top()); + if(rc_draw.intersects(rc)) { + int from_x = max(rc_draw.left(), rc.left()) - rc.left(); + int from_y = max(rc_draw.top(), rc.top()) - rc.top(); + int w = min(rc.right(), rc_draw.right()) - rc.left() - from_x + 1; + int h = min(rc.bottom(), rc_draw.bottom()) - rc.top() - from_y + 1; + int to_x = rc.left() + from_x; + int to_y = rc.top() + from_y; + painter.drawPixmap(to_x, to_y, m_img_time_total, from_x, from_y, w, h); + } + } + + // 绘制进度条 + { + QRect rc(m_rc_progress); + rc.moveTo(m_rc.left()+rc.left(), m_rc.top() + rc.top()); + + if(rc_draw.intersects(rc)) { + if(m_percent_last_draw != m_percent) { + m_img_progress = QPixmap(m_rc_progress.width(), m_rc_progress.height()); + m_img_progress.fill(Qt::transparent); + QPainter pp(&m_img_progress); + + // 进度条 + int top = (rc.height() - m_res[res_pbh_left].height())/2; + int passed_width = rc.width() * m_percent / 100; // 已经播放的进度条宽度 + int remain_width = rc.width() - passed_width; // 剩下未播放的进度条宽度 + + if(passed_width >= m_res[res_pbh_left].width()) + pp.drawPixmap(0, top , m_res[res_pbh_left].width(), m_res[res_pbh_left].height(), m_res[res_pbh_left]); + if(passed_width > 0) { + //pp.drawPixmap(m_res[res_pbh_left].width(), top, passed_width - m_res[res_pbh_left].width(), m_res[res_pbh_mid].height(), m_res[res_pbh_mid]); + if(remain_width > m_res[res_pb_right].width()) + pp.drawPixmap(m_res[res_pbh_left].width(), top, passed_width - m_res[res_pbh_left].width(), m_res[res_pbh_mid].height(), m_res[res_pbh_mid]); + else + pp.drawPixmap(m_res[res_pbh_left].width(), top, passed_width - m_res[res_pbh_left].width() - m_res[res_pb_right].width(), m_res[res_pbh_mid].height(), m_res[res_pbh_mid]); + } + if(remain_width > 0) + pp.drawPixmap(passed_width, top, remain_width - m_res[res_pb_right].width(), m_res[res_pb_mid].height(), m_res[res_pb_mid]); + if(remain_width >= m_res[res_pb_right].width()) + pp.drawPixmap(rc.width() - m_res[res_pb_right].width(), top , m_res[res_pb_right].width(), m_res[res_pb_right].height(), m_res[res_pb_right]); + + // 进度位置指示 + int left = passed_width - m_img_progress_pointer->width() / 2; + if(left < 0) + left = 0; + if(left + m_img_progress_pointer->width() > rc.width()) + left = rc.width() - m_img_progress_pointer->width(); + top = (rc.height() - m_img_progress_pointer[widget_normal].height())/2; + pp.drawPixmap(left, top , m_img_progress_pointer[widget_normal].width(), m_img_progress_pointer[widget_normal].height(), m_img_progress_pointer[widget_normal]); + + m_percent_last_draw = m_percent; + } + + int from_x = max(rc_draw.left(), rc.left()) - rc.left(); + int from_y = max(rc_draw.top(), rc.top()) - rc.top(); + int w = min(rc.right(), rc_draw.right()) - rc.left() - from_x + 1; + int h = min(rc.bottom(), rc_draw.bottom()) - rc.top() - from_y + 1; + int to_x = rc.left() + from_x; + int to_y = rc.top() + from_y; + painter.drawPixmap(to_x, to_y, m_img_progress, from_x, from_y, w, h); + } + } + + // 绘制速度按钮 + { + for(int i = 0; i < speed_count; i++) { + QRect rc(m_rc_btn_speed[i]); + rc.moveTo(m_rc.left()+rc.left(), m_rc.top() + rc.top()); + if(rc_draw.intersects(rc)) { + int from_x = max(rc_draw.left(), rc.left()) - rc.left(); + int from_y = max(rc_draw.top(), rc.top()) - rc.top(); + int w = min(rc.right(), rc_draw.right()) - rc.left() - from_x + 1; + int h = min(rc.bottom(), rc_draw.bottom()) - rc.top() - from_y + 1; + int to_x = rc.left() + from_x; + int to_y = rc.top() + from_y; + if(m_speed_selected == i || m_speed_hover == i) + painter.drawPixmap(to_x, to_y, m_img_btn_speed[i][widget_hover], from_x, from_y, w, h); + else + painter.drawPixmap(to_x, to_y, m_img_btn_speed[i][widget_normal], from_x, from_y, w, h); + } + } + } + + // 绘制跳过选项 + { + QRect rc(m_rc_skip); + rc.moveTo(m_rc.left()+rc.left(), m_rc.top() + rc.top()); + if(rc_draw.intersects(rc)) { + int from_x = max(rc_draw.left(), rc.left()) - rc.left(); + int from_y = max(rc_draw.top(), rc.top()) - rc.top(); + int w = min(rc.right(), rc_draw.right()) - rc.left() - from_x + 1; + int h = min(rc.bottom(), rc_draw.bottom()) - rc.top() - from_y + 1; + int to_x = rc.left() + from_x; + int to_y = rc.top() + from_y; + //qDebug("skip (%d,%d), (%d,%d)/(%d,%d)", to_x, to_y, from_x, from_y, w, h); + if(m_skip_selected) + painter.drawPixmap(to_x, to_y, m_img_skip[widget_hover], from_x, from_y, w, h); + else + painter.drawPixmap(to_x, to_y, m_img_skip[widget_normal], from_x, from_y, w, h); + } + } +} + + diff --git a/client/tp-player/bar.h b/client/tp-player/bar.h index 5000069..3cb8144 100644 --- a/client/tp-player/bar.h +++ b/client/tp-player/bar.h @@ -5,49 +5,128 @@ #include #include +typedef enum { + res_bg_left = 0, // 背景左侧 + res_bg_mid, // 背景中间,拉伸填充 + res_bg_right, // 背景右侧 + res_bs_left, // 速度按钮(未选中)左侧 + res_bs_mid, // 速度按钮(未选中)中间,拉伸填充 + res_bs_right, // 速度按钮(未选中)右侧 + res_bsh_left, // 速度按钮(选中)左侧 + res_bsh_mid, // 速度按钮(选中)中间,拉伸填充 + res_bsh_right, // 速度按钮(选中)右侧 + res_pbh_left, // 进度条(已经过)左侧 + res_pbh_mid, // 进度条(已经过)中间,拉伸填充 + res_pb_mid, // 进度条(未到达)中间,拉伸填充 + res_pb_right, // 进度条(未到达)右侧 +// res_pp, // 进度条上的指示点,未选中 +// res_pph, // 进度条上的指示点,选中高亮 + res_cb, // 复选框,未选中 + res_cbh, // 复选框,已勾选 +// res_play, +// res_play_hover, +// res_pause, +// res_pause_hover, + + res__max +}RES_ID; + +typedef enum { + widget_normal = 0, + widget_hover, + widget__max +}WIDGET_STAT; + +typedef enum { + play_running = 0, + play_paused, + play__max +}PLAY_STAT; + +//typedef enum { +// speed_1x = 0, +// speed_2x, +// speed_4x, +// speed_8x, +// speed_16x, +// speed__max, +//}SPEED; + +#define speed_1x 0 +#define speed_2x 1 +#define speed_4x 2 +#define speed_8x 3 +#define speed_16x 4 +#define speed_count 5 class Bar { public: Bar(); ~Bar(); - bool init(QWidget* owner, int width); + bool init(QWidget* owner); + void start(uint32_t total_ms, int width); + void end(); void draw(QPainter& painter, const QRect& rc); + void update_passed_time(uint32_t ms); + + QRect rc(){return m_rc;} + + void onMouseMove(int x, int y); + +private: + void _init_imgages(); + void _ms_to_str(uint32_t ms, QString& str); private: QWidget* m_owner; + + uint32_t m_total_ms; // 录像的总时长 + uint32_t m_passed_ms; // 已经播放了的时长 + int m_percent; // 已经播放了的百分比(0~100) + int m_percent_last_draw; + QString m_str_total_time; + QString m_str_passed_time; + QString m_str_passed_time_last_draw; + + bool m_img_ready; + + // 从资源中加载的原始图像 + QPixmap m_res[res__max]; + QPixmap m_img_progress_pointer[widget__max]; + int m_width; + int m_height; + // 此浮动窗相对于父窗口的坐标和大小 + QRect m_rc; + // 尺寸和定位(此浮动窗内部的相对坐标) + QRect m_rc_btn_play; + QRect m_rc_btn_speed[speed_count]; + QRect m_rc_time_passed; + QRect m_rc_time_total; + QRect m_rc_progress; + QRect m_rc_skip; - QPixmap m_bg; + // 画布,最终输出的图像 + //QPixmap m_canvas; - QPixmap m_bg_left; - QPixmap m_bg_mid; - QPixmap m_bg_right; + // 合成的图像 + QPixmap m_img_bg; + QPixmap m_img_btn_play[play__max][widget__max]; + QPixmap m_img_btn_speed[speed_count][widget__max]; + QPixmap m_img_progress; + QPixmap m_img_skip[widget__max]; + QPixmap m_img_time_passed; + QPixmap m_img_time_total; - QPixmap m_btn_left; - QPixmap m_btn_mid; - QPixmap m_btn_right; - - QPixmap m_btnsel_left; - QPixmap m_btnsel_mid; - QPixmap m_btnsel_right; - - QPixmap m_prgbarh_left; - QPixmap m_prgbarh_mid; - - QPixmap m_prgbar_left; - QPixmap m_prgbar_mid; - QPixmap m_prgbar_right; - - QPixmap m_prgpt; - QPixmap m_prgpt_hover; - - QPixmap m_select; - QPixmap m_selected; - - QPixmap m_play; - QPixmap m_play_hover; + // 各种状态 + bool m_play_hover; + bool m_playing; // 0=play, 2=pause + int m_speed_selected; + int m_speed_hover; // speed__max=no-hover + bool m_skip_selected; + //bool m_skip_hover; }; #endif // BAR_H diff --git a/client/tp-player/mainwindow.cpp b/client/tp-player/mainwindow.cpp index b2467d0..a56c53d 100644 --- a/client/tp-player/mainwindow.cpp +++ b/client/tp-player/mainwindow.cpp @@ -41,10 +41,6 @@ bool rdpimg2QImage(QImage& out, int w, int h, int bitsPerPixel, bool isCompresse uint8 r = ((a & 0xf800) >> 11) * 255 / 31; uint8 g = ((a & 0x07e0) >> 5) * 255 / 63; uint8 b = (a & 0x001f) * 255 / 31; -// r = r * 255 / 31; -// g = g * 255 / 63; -// b = b * 255 / 31; - out.setPixelColor(x, y, QColor(r,g,b)); } } @@ -72,15 +68,17 @@ MainWindow::MainWindow(QWidget *parent) : ui(new Ui::MainWindow) { m_shown = false; - m_show_bg = true; - m_bg = QImage(":/tp-player/res/bg"); - m_pt_normal = QImage(":/tp-player/res/cursor.png"); + m_show_default = true; + m_bar_shown = false; memset(&m_pt, 0, sizeof(TS_RECORD_RDP_POINTER)); qDebug() << m_pt_normal.width() << "x" << m_pt_normal.height(); ui->setupUi(this); + ui->centralWidget->setMouseTracking(true); + setMouseTracking(true); + //qRegisterMetaType("update_data"); // frame-less window. @@ -91,15 +89,13 @@ MainWindow::MainWindow(QWidget *parent) : // setWindowFlags(Qt::FramelessWindowHint | Qt::MSWindowsFixedSizeDialogHint | windowFlags()); //#endif //__APPLE__ - //m_canvas = QPixmap(m_bg.width(), m_bg.height()); - m_canvas.load(":/tp-player/res/bg"); - - resize(m_bg.width(), m_bg.height()); + m_pt_normal.load(":/tp-player/res/cursor.png"); + m_default_bg.load(":/tp-player/res/bg.png"); setWindowFlags(windowFlags()&~Qt::WindowMaximizeButtonHint); // 禁止最大化按钮 - setFixedSize(m_bg.width(), m_bg.height()); // 禁止拖动窗口大小 + setFixedSize(m_default_bg.width(), m_default_bg.height()); // 禁止拖动窗口大小 - if(!m_bar.init(this, 480)) + if(!m_bar.init(this)) return; connect(&m_thr_play, SIGNAL(signal_update_data(update_data*)), this, SLOT(on_update_data(update_data*))); @@ -112,49 +108,43 @@ MainWindow::~MainWindow() delete ui; } -void MainWindow::paintEvent(QPaintEvent *pe) +void MainWindow::paintEvent(QPaintEvent *e) { QPainter painter(this); - painter.drawPixmap(pe->rect(), m_canvas, pe->rect()); + if(m_show_default) { + painter.drawPixmap(e->rect(), m_default_bg, e->rect()); + } + else { + painter.drawPixmap(e->rect(), m_canvas, e->rect()); - if(!m_pt_history.empty()) { - for(int i = 0; i < m_pt_history.count(); i++) { - //qDebug("pt clean %d,%d", m_pt_history[i].x, m_pt_history[i].y); - QRect rcpt(m_pt_normal.rect()); - rcpt.moveTo(m_pt_history[i].x - m_pt_normal.width()/2, m_pt_history[i].y-m_pt_normal.height()/2); - //painter.drawPixmap(rcpt, m_canvas, rcpt); - qDebug("pt ---- (%d,%d), (%d,%d)", rcpt.x(), rcpt.y(), rcpt.width(), rcpt.height()); - painter.fillRect(rcpt, QColor(255, 255, 0, 128)); + QRect rcpt(m_pt_normal.rect()); + rcpt.moveTo(m_pt.x - m_pt_normal.width()/2, m_pt.y-m_pt_normal.height()/2); + if(e->rect().intersects(rcpt)) { + painter.drawPixmap(m_pt.x-m_pt_normal.width()/2, m_pt.y-m_pt_normal.height()/2, m_pt_normal); } - m_pt_history.clear(); + + // 绘制浮动控制窗 + if(m_bar_shown) + m_bar.draw(painter, e->rect()); } - QRect rcpt(m_pt_normal.rect()); - rcpt.moveTo(m_pt.x - m_pt_normal.width()/2, m_pt.y-m_pt_normal.height()/2); - if(pe->rect().intersects(rcpt)) { - qDebug("pt draw (%d,%d), (%d,%d)", m_pt.x-m_pt_normal.width()/2, m_pt.y-m_pt_normal.height()/2, m_pt_normal.width(), m_pt_normal.height()); - painter.drawImage(m_pt.x-m_pt_normal.width()/2, m_pt.y-m_pt_normal.height()/2, m_pt_normal); + if(!m_shown) { + m_shown = true; + m_thr_play.start(); } - - m_bar.draw(painter, pe->rect()); - -// if(!m_shown) { -// m_shown = true; -// m_thr_play.start(); -// } } void MainWindow::on_update_data(update_data* dat) { if(!dat) return; + UpdateDataHelper data_helper(dat); + if(dat->data_type() == TYPE_DATA) { - m_show_bg = false; if(dat->data_len() <= sizeof(TS_RECORD_PKG)) { qDebug() << "invalid record package(1)."; - delete dat; return; } @@ -163,25 +153,21 @@ void MainWindow::on_update_data(update_data* dat) { if(pkg->type == TS_RECORD_TYPE_RDP_POINTER) { if(dat->data_len() != sizeof(TS_RECORD_PKG) + sizeof(TS_RECORD_RDP_POINTER)) { qDebug() << "invalid record package(2)."; - delete dat; return; } - // 将现有虚拟鼠标信息放入历史队列,这样下一次绘制界面时就会将其清除掉 - m_pt_history.push_back(m_pt); + TS_RECORD_RDP_POINTER pt; + memcpy(&pt, &m_pt, sizeof(TS_RECORD_RDP_POINTER)); // 更新虚拟鼠标信息,这样下一次绘制界面时就会在新的位置绘制出虚拟鼠标 memcpy(&m_pt, dat->data_buf() + sizeof(TS_RECORD_PKG), sizeof(TS_RECORD_RDP_POINTER)); - //qDebug("pt new position %d,%d", m_pt.x, m_pt.y); - - //setUpdatesEnabled(false); update(m_pt.x - m_pt_normal.width()/2, m_pt.y - m_pt_normal.width()/2, m_pt_normal.width(), m_pt_normal.height()); - //setUpdatesEnabled(true); + + update(pt.x - m_pt_normal.width()/2, pt.y - m_pt_normal.width()/2, m_pt_normal.width(), m_pt_normal.height()); } else if(pkg->type == TS_RECORD_TYPE_RDP_IMAGE) { if(dat->data_len() <= sizeof(TS_RECORD_PKG) + sizeof(TS_RECORD_RDP_IMAGE_INFO)) { qDebug() << "invalid record package(3)."; - delete dat; return; } @@ -189,53 +175,57 @@ void MainWindow::on_update_data(update_data* dat) { uint8_t* img_dat = dat->data_buf() + sizeof(TS_RECORD_PKG) + sizeof(TS_RECORD_RDP_IMAGE_INFO); uint32_t img_len = dat->data_len() - sizeof(TS_RECORD_PKG) - sizeof(TS_RECORD_RDP_IMAGE_INFO); - rdpimg2QImage(m_img_update, info->width, info->height, info->bitsPerPixel, (info->format == TS_RDP_IMG_BMP) ? true : false, img_dat, img_len); + QImage img_update; + rdpimg2QImage(img_update, info->width, info->height, info->bitsPerPixel, (info->format == TS_RDP_IMG_BMP) ? true : false, img_dat, img_len); - m_img_update_x = info->destLeft; - m_img_update_y = info->destTop; - m_img_update_w = info->destRight - info->destLeft + 1; - m_img_update_h = info->destBottom - info->destTop + 1; + int x = info->destLeft; + int y = info->destTop; + int w = info->destRight - info->destLeft + 1; + int h = info->destBottom - info->destTop + 1; - setUpdatesEnabled(false); QPainter pp(&m_canvas); - pp.drawImage(m_img_update_x, m_img_update_y, m_img_update, 0, 0, m_img_update_w, m_img_update_h, Qt::AutoColor); + pp.drawImage(x, y, img_update, 0, 0, w, h, Qt::AutoColor); - update(m_img_update_x, m_img_update_y, m_img_update_w, m_img_update_h); - setUpdatesEnabled(true); + update(x, y, w, h); } - delete dat; return; } + if(dat->data_type() == TYPE_TIMER) { + m_bar.update_passed_time(dat->passed_ms()); + return; + } if(dat->data_type() == TYPE_HEADER_INFO) { if(dat->data_len() != sizeof(TS_RECORD_HEADER)) { qDebug() << "invalid record header."; - delete dat; return; } memcpy(&m_rec_hdr, dat->data_buf(), sizeof(TS_RECORD_HEADER)); - delete dat; qDebug() << "resize (" << m_rec_hdr.basic.width << "," << m_rec_hdr.basic.height << ")"; if(m_rec_hdr.basic.width > 0 && m_rec_hdr.basic.height > 0) { - m_canvas = QPixmap(m_rec_hdr.basic.width, m_rec_hdr.basic.height); + m_canvas.fill(QColor(38, 73, 111)); + //m_win_board_w = frameGeometry().width() - geometry().width(); + //m_win_board_h = frameGeometry().height() - geometry().height(); - m_win_board_w = frameGeometry().width() - geometry().width(); - m_win_board_h = frameGeometry().height() - geometry().height(); + QDesktopWidget *desktop = QApplication::desktop(); // =qApp->desktop();也可以 + qDebug("desktop w:%d,h:%d, this w:%d,h:%d", desktop->width(), desktop->height(), width(), height()); + //move((desktop->width() - this->width())/2, (desktop->height() - this->height())/2); + move(10, (desktop->height() - m_rec_hdr.basic.height)/2); //setFixedSize(m_rec_hdr.basic.width + m_win_board_w, m_rec_hdr.basic.height + m_win_board_h); //resize(m_rec_hdr.basic.width + m_win_board_w, m_rec_hdr.basic.height + m_win_board_h); + //resize(m_rec_hdr.basic.width, m_rec_hdr.basic.height); setFixedSize(m_rec_hdr.basic.width, m_rec_hdr.basic.height); - resize(m_rec_hdr.basic.width, m_rec_hdr.basic.height); - QDesktopWidget *desktop = QApplication::desktop(); // =qApp->desktop();也可以 - qDebug("desktop width: %d", desktop->width()); - //move((desktop->width() - this->width())/2, (desktop->height() - this->height())/2); - move(10, (desktop->height() - this->height())/2); + m_show_default = false; + repaint(); + + m_bar.start(m_rec_hdr.info.time_ms, 640); } QString title; @@ -250,5 +240,30 @@ void MainWindow::on_update_data(update_data* dat) { } - delete dat; + if(dat->data_type() == TYPE_END) { + m_bar.end(); + return; + } } + +void MainWindow::mouseMoveEvent(QMouseEvent *e) { + if(!m_show_default) { + QRect rc = m_bar.rc(); + if(e->y() > rc.top() - 20 && e->y() < rc.bottom() + 20) { + if(!m_bar_shown) { + m_bar_shown = true; + update(rc); + } + + if(rc.contains(QPoint(e->x(), e->y()))) + m_bar.onMouseMove(e->x(), e->y()); + } + else { + if(m_bar_shown) { + m_bar_shown = false; + update(rc); + } + } + } +} + diff --git a/client/tp-player/mainwindow.h b/client/tp-player/mainwindow.h index f53d309..1f3bc6d 100644 --- a/client/tp-player/mainwindow.h +++ b/client/tp-player/mainwindow.h @@ -2,7 +2,6 @@ #define MAINWINDOW_H #include -#include #include "bar.h" #include "thr_play.h" #include "update_data.h" @@ -21,16 +20,19 @@ public: ~MainWindow(); private: - void paintEvent(QPaintEvent *); - + void paintEvent(QPaintEvent *e); + void mouseMoveEvent(QMouseEvent *e); private slots: void on_update_data(update_data*); private: Ui::MainWindow *ui; - QImage m_bg; + //QImage m_bg; bool m_shown; + bool m_show_default; + bool m_bar_shown; + QPixmap m_default_bg; ThreadPlay m_thr_play; @@ -38,21 +40,10 @@ private: Bar m_bar; - bool m_show_bg; TS_RECORD_HEADER m_rec_hdr; - QImage m_pt_normal; + QPixmap m_pt_normal; TS_RECORD_RDP_POINTER m_pt; - QVector m_pt_history; - - - QImage m_img_update; - int m_win_board_w; - int m_win_board_h; - int m_img_update_x; - int m_img_update_y; - int m_img_update_w; - int m_img_update_h; }; #endif // MAINWINDOW_H diff --git a/client/tp-player/record_format.h b/client/tp-player/record_format.h index 09a1c66..2a09635 100644 --- a/client/tp-player/record_format.h +++ b/client/tp-player/record_format.h @@ -6,6 +6,8 @@ #define TYPE_HEADER_INFO 0 #define TYPE_DATA 1 +#define TYPE_TIMER 2 +#define TYPE_END 3 #define TS_RECORD_TYPE_RDP_POINTER 0x12 // 鼠标坐标位置改变,用于绘制虚拟鼠标 diff --git a/client/tp-player/res/bar/pause-hover.png b/client/tp-player/res/bar/pause-hover.png new file mode 100644 index 0000000000000000000000000000000000000000..a4f6c991752c02cf8f1243a497f62195737a7ed0 GIT binary patch literal 2234 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 xI=^Gxdw1;zx6zwEc 1000) { + update_data* _passed_ms = new update_data; + _passed_ms->data_type(TYPE_TIMER); + _passed_ms->passed_ms(time_pass); +// qDebug("--- 1 %d", time_pass); + emit signal_update_data(_passed_ms); + time_last_pass = time_pass; + } if(time_pass >= pkg.time_ms) { //time_pass = pkg.time_ms; @@ -99,8 +111,8 @@ void ThreadPlay::run() { uint32_t wait_this_time = 0; for(;;) { wait_this_time = time_wait; - if(wait_this_time > 5) - wait_this_time = 5; + if(wait_this_time > 10) + wait_this_time = 10; if(m_need_stop) { qDebug() << "stop, user cancel (2)."; @@ -109,8 +121,15 @@ void ThreadPlay::run() { msleep(wait_this_time); - //time_pass += wait_this_time; - //time_pass = pkg.time_ms; + uint32_t _time_pass = (uint32_t)(QDateTime::currentMSecsSinceEpoch() - time_begin); + if(_time_pass - time_last_pass > 1000) { + update_data* _passed_ms = new update_data; + _passed_ms->data_type(TYPE_TIMER); + _passed_ms->passed_ms(_time_pass); +// qDebug("--- 2 %d", _time_pass); + emit signal_update_data(_passed_ms); + time_last_pass = _time_pass; + } time_wait -= wait_this_time; if(time_wait == 0) { @@ -118,8 +137,9 @@ void ThreadPlay::run() { break; } } - -// emit signal_update_data(dat); -// msleep(15); } + + update_data* _end = new update_data; + _end->data_type(TYPE_END); + emit signal_update_data(_end); } diff --git a/client/tp-player/tp-player.qrc b/client/tp-player/tp-player.qrc index 0322973..4175b85 100644 --- a/client/tp-player/tp-player.qrc +++ b/client/tp-player/tp-player.qrc @@ -13,7 +13,8 @@ res/bar/btnsel-right.png res/bar/play-hover.png res/bar/play.png - res/bar/prgbar-left.png + res/bar/pause-hover.png + res/bar/pause.png res/bar/prgbar-mid.png res/bar/prgbar-right.png res/bar/prgbarh-left.png diff --git a/client/tp-player/update_data.h b/client/tp-player/update_data.h index 505c040..e701c95 100644 --- a/client/tp-player/update_data.h +++ b/client/tp-player/update_data.h @@ -19,6 +19,9 @@ public: uint8_t* data_buf() {return m_data_buf;} uint32_t data_len() const {return m_data_len;} + void passed_ms(uint32_t ms) {m_passed_ms = ms;} + uint32_t passed_ms() {return m_passed_ms;} + signals: public slots: @@ -28,6 +31,22 @@ private: int m_data_type; uint8_t* m_data_buf; uint32_t m_data_len; + uint32_t m_passed_ms; }; +class UpdateDataHelper { +public: + UpdateDataHelper(update_data* data) { + m_data = data; + } + ~UpdateDataHelper() { + if(m_data) + delete m_data; + } + +private: + update_data* m_data; +}; + + #endif // UPDATE_DATA_H From b70593a65ee9d59f1bc187924bcbbfc561ba43d6 Mon Sep 17 00:00:00 2001 From: Apex Liu Date: Mon, 9 Sep 2019 17:17:56 +0800 Subject: [PATCH 15/44] update ui-design. --- .gitignore | 1 + client/tp-player/res/bar.psd | Bin 717957 -> 771656 bytes client/tp-player/res/bar/bg-left.png | Bin 1099 -> 1098 bytes client/tp-player/res/bar/bg-mid.png | Bin 1015 -> 1016 bytes client/tp-player/res/bar/bg-right.png | Bin 1103 -> 1105 bytes client/tp-player/res/bar/btn-hover-left.png | Bin 0 -> 1053 bytes .../bar/{btnsel-mid.png => btn-hover-mid.png} | Bin 1011 -> 1016 bytes client/tp-player/res/bar/btn-hover-right.png | Bin 0 -> 1058 bytes client/tp-player/res/bar/btn-left.png | Bin 1059 -> 0 bytes client/tp-player/res/bar/btn-normal-left.png | Bin 0 -> 1063 bytes .../bar/{btn-mid.png => btn-normal-mid.png} | Bin 1004 -> 1008 bytes client/tp-player/res/bar/btn-normal-right.png | Bin 0 -> 1068 bytes client/tp-player/res/bar/btn-right.png | Bin 1064 -> 0 bytes client/tp-player/res/bar/btn-sel-left.png | Bin 0 -> 1049 bytes client/tp-player/res/bar/btn-sel-mid.png | Bin 0 -> 1016 bytes client/tp-player/res/bar/btn-sel-right.png | Bin 0 -> 1053 bytes client/tp-player/res/bar/btnsel-left.png | Bin 1044 -> 0 bytes client/tp-player/res/bar/btnsel-right.png | Bin 1047 -> 0 bytes client/tp-player/res/bar/chkbox-hover.png | Bin 0 -> 1121 bytes client/tp-player/res/bar/chkbox-normal.png | Bin 0 -> 1104 bytes client/tp-player/res/bar/chkbox-sel-hover.png | Bin 0 -> 1226 bytes .../tp-player/res/bar/chkbox-sel-normal.png | Bin 0 -> 1166 bytes client/tp-player/res/bar/pause-hover.png | Bin 2234 -> 2592 bytes client/tp-player/res/bar/pause-normal.png | Bin 0 -> 2497 bytes client/tp-player/res/bar/pause.png | Bin 2295 -> 0 bytes client/tp-player/res/bar/play-hover.png | Bin 2234 -> 3066 bytes client/tp-player/res/bar/play-normal.png | Bin 0 -> 2898 bytes client/tp-player/res/bar/play.png | Bin 2295 -> 0 bytes client/tp-player/res/bar/prgbar-mid.png | Bin 1007 -> 1003 bytes client/tp-player/res/bar/prgbar-right.png | Bin 1049 -> 1037 bytes client/tp-player/res/bar/prgbarh-left.png | Bin 1044 -> 1041 bytes client/tp-player/res/bar/prgbarh-mid.png | Bin 1007 -> 1004 bytes client/tp-player/res/bar/prgpt-hover.png | Bin 1126 -> 1148 bytes client/tp-player/res/bar/prgpt-normal.png | Bin 0 -> 1108 bytes client/tp-player/res/bar/prgpt.png | Bin 1151 -> 0 bytes client/tp-player/tp-player.qrc | 33 ++++++++++++------ 36 files changed, 23 insertions(+), 11 deletions(-) create mode 100644 client/tp-player/res/bar/btn-hover-left.png rename client/tp-player/res/bar/{btnsel-mid.png => btn-hover-mid.png} (51%) create mode 100644 client/tp-player/res/bar/btn-hover-right.png delete mode 100644 client/tp-player/res/bar/btn-left.png create mode 100644 client/tp-player/res/bar/btn-normal-left.png rename client/tp-player/res/bar/{btn-mid.png => btn-normal-mid.png} (51%) create mode 100644 client/tp-player/res/bar/btn-normal-right.png delete mode 100644 client/tp-player/res/bar/btn-right.png create mode 100644 client/tp-player/res/bar/btn-sel-left.png create mode 100644 client/tp-player/res/bar/btn-sel-mid.png create mode 100644 client/tp-player/res/bar/btn-sel-right.png delete mode 100644 client/tp-player/res/bar/btnsel-left.png delete mode 100644 client/tp-player/res/bar/btnsel-right.png create mode 100644 client/tp-player/res/bar/chkbox-hover.png create mode 100644 client/tp-player/res/bar/chkbox-normal.png create mode 100644 client/tp-player/res/bar/chkbox-sel-hover.png create mode 100644 client/tp-player/res/bar/chkbox-sel-normal.png create mode 100644 client/tp-player/res/bar/pause-normal.png delete mode 100644 client/tp-player/res/bar/pause.png create mode 100644 client/tp-player/res/bar/play-normal.png delete mode 100644 client/tp-player/res/bar/play.png create mode 100644 client/tp-player/res/bar/prgpt-normal.png delete mode 100644 client/tp-player/res/bar/prgpt.png diff --git a/.gitignore b/.gitignore index 919bcb9..a0677f0 100644 --- a/.gitignore +++ b/.gitignore @@ -104,3 +104,4 @@ profile /server/tp_core/testssh/Debug /server/tp_core/testssh/Release +/client/build-tp-player-* diff --git a/client/tp-player/res/bar.psd b/client/tp-player/res/bar.psd index 8348da111a1162ccaaab3df8f6b894bf9cc141ed..a2defc84f9548349399b0f6e47292286b82c9a4a 100644 GIT binary patch literal 771656 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

Wd3{kXY&6x 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 literal 717957 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

BOLJp&ZCZF&Ux>7bq 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 delta 260 zcmX@bahhX-Cu7<~uRc{{-DCqpLj^N4Jwp>yGc!XS1tSAPBYguPGSf9Qu`)HYGBV%z zHO=GRCQB8&d+8qq&ij zfr+_+nd#&NX7S0^%(Wc74NuA#7#ON2A7B<$adUXXdc@1&OyfcUu0stL4AVP(0&WS+ zS4wHU*K&s=c!kRvC%(x)nWe4ek7(_2Uvnt;h?Wie5$O+_DJ-Ro6QaHyb9lpf!3k7paJwsDV3nLR91tSAPGkpU~eM1vn14AnVODhAz zjelbq70fM+jNQyk-CT@Z4Gj%lEiIiK&D_jf4c*L)3@scT4JTJI$-or3;#9PaDM2O0 z$lS=(Br!!d)iNhBh!TGnfakXD^(R_6?|>!PC{xWt~$( F69ASxLskF) delta 214 zcmeyt{+)e-Cu7<~uRc{{-DCqpLj^N4Jwp>yGc!XS1tSAPBYguPGSf9Qu`)HYGBV%z zHO=GRLWC8&d+8qq&ij zfr+_+nd#&NX7S0^%(c#Z3%15EFfgclx;TbZ+)DcO|Gzymhp~}?fev#LTbcr&TxyRI jn{2b-6hB7>g#!%cAxd>0-#<(MYGLqn^>bP0l+XkKNXa?x diff --git a/client/tp-player/res/bar/bg-right.png b/client/tp-player/res/bar/bg-right.png index 8cbb474038031ddc5f3cf26beb0c09ada9f91be3..c6b31a01889e8f0a980b8b410174fcdbb4a30a37 100644 GIT binary patch delta 323 zcmX@lagk$!Cu7z`uRc{X-DE>!3k7paJwsDV3nLR91tSAPGkpU~eM1vn14AnVODhAz zjelbq70fM+4UCM9U0jS@4Gj%lEiIiK&D_jf4c*L)3@scT4JTJI$-opj<5aYbDM2O0 z$lS=(Br!!d)iNCu7<~uRc{{-DCqpLj^N4Jwp>yGc!XS1tSAPBYguPGSf9Qu`)HYGBV%z zHO=GQ+888&d+8qq&ij zfr+_+nd#&NX7S0^%(eBBs&Oq03=H+2E{-7;x03$+|8LK1*dW+3ePQQchDX&7dqg|V zH?ugj875vl!+0dhA%^QnqJg6tlNlSsMnOX^#{=CFtzJj8o+yMV9$s-Muyrw)yJ(-5 zOsK`Gf{=$G0Tqouj%yB8AJMww)^mv0p@#2B!a=2Iu*-44$rjF6*2UngAdSVQl~a diff --git a/client/tp-player/res/bar/btn-hover-left.png b/client/tp-player/res/bar/btn-hover-left.png new file mode 100644 index 0000000000000000000000000000000000000000..7914efbfbe6cf1adde24fb0ab0558fc7f1176214 GIT binary patch literal 1053 zcmbVL&ui0A9FJ}enQRQ1h@Ku9=me9zr1{YoR>#_;uCUJ071o1@`SI4qE_qq^Vjxq6Xr|TP7h-T1Ihc5&*%I3{(QW&czbO0>L|x? zW0iTU#?}$`PM*)cd!H6Qv*il48+4hh(vFj0Zq6qz29?lh;u?1Rt@RIhhU11F1oZ}O zR2NK-grbvS#BLZfHpk7(c4NnDV+vf{459-6WB)A=0>8koXjNE^OSl!xZzp(pd$I0q zw>{+Zv$w!Z*JK7lOdZe-*P_(y7WkfBlkKx>i3dFhZ5Q|>ryA8IP$CHiny3q&oXZ0P ziL!?Bih2_$P}U`gBv}=pY(iv08T22XsU`lZS+mN0Ep{sKElOikk~*D^*vW|`X-YCe zNP>!_C;~$W=~hIYt`MbD0}B>Ty(EZfKq8P?bX>AQ3q14mD1ZgVM4NrdNW< zC!O?0R-U71Et~|K*e@p}1ik#4!3p}lVknyG_yR@_7F49Vg6nuW!NlQ1+aO}z4v-}!spf}UXEXz{{AqUT-dq1JN*9X@SB6?-HZD#%99fxIk)`vk$Z6I b6?bNYduCr~|J1Jk$_BP#FIs!W`#XOCwYf)+ literal 0 HcmV?d00001 diff --git a/client/tp-player/res/bar/btnsel-mid.png b/client/tp-player/res/bar/btn-hover-mid.png similarity index 51% rename from client/tp-player/res/bar/btnsel-mid.png rename to client/tp-player/res/bar/btn-hover-mid.png index 03c01ea4ecd3c78e50653c0f4e1ca637ed20a4ee..a1db3498901e3bdbc19b0bb9a8bc48d9a047d4b4 100644 GIT binary patch delta 233 zcmey&{)2sjCu7z`uRc{X-DE>!3k7paJwsDV3nLR91tSAPGkpU~eM1vn14AnVODhAz zjelbq6)eqN%#B@5oL!7u4Gj%lEiEj~3|!64om>nI+$>B?jVD(z$-opj;Z(GZDM2O0 z$lS=(Br!!d)iNzopr E0Qn9-?*IS* delta 210 zcmeyt{+WG(Cu7<~uRc{{-DCqpLj^N4Jwp>yGc!XS1tSAPBYguPGSf9Qu`)HYGBV%z zHbP0l+XkKFxNL- diff --git a/client/tp-player/res/bar/btn-hover-right.png b/client/tp-player/res/bar/btn-hover-right.png new file mode 100644 index 0000000000000000000000000000000000000000..cfd1348dc0931041cb5936b48c4c4ae93d53c651 GIT binary patch literal 1058 zcmbVL&ui0A9FH3e84fnskcmRb82-RCKbj_eVYMtx>WXchU12@?lDw^POJ0_|*=`83 zofI!31HH=b0}q}=bT9<*q^IG@|3C&F)%nu3)5F-oK=OX%`}usnKOfIlZw?J!7-Se` zs5E2N=sG~((X+{S=Sz8?F5|>%kU6|SS};P)w2N)TmVCH~Y6!Y(D<9Dm!}QJ8GU zlywLDDVW%#+I~pU3^SE)htOFPBeNa-xA zfs~X1Sy{Qx%A%wSA`m2n7bRT;x+t;ThofqdyP((1Vpofva$J*;P#1(&tCeb{Q#e`_ zBmjUQ%7QHO)Pj%K0s`B75Kr_NOcXnj7ZMK#Y{Cd_yh?H$_4FtNKde-aiG#SCD4H^% z4MRaniGuGZadoU?QbT{;IMF(;uZ2jcp%|}54z0(+L=Q}Jce+qwNWIZ-M;Z5>^%p6Dmq+HL{RiKGRF>Pf`Q?!6Y!#9dRQ$@wFELBsC9>+;opjn1y7{HV*Ns=s( z1*&Linqi7!F{@&`d3DDaBK?ZRYx-2U`%JU}R*QxnngnE$rFK}V}vGNa??6A zBnnf3sas)VKw>ICfPn&KK!OFS5@JN`s0$NPIp+(8(gDl9d++={&-?vwcINUx|JiQ7=Jg}`aKc6Z{1uu*1_SCA6ORFFEPnhF7zu_^L0u+RTLYIhR_%hh`%60 zMNuM-L?X^K;=zhbjWF&8gB=D92c~a()Fv*77>z1frfHse+6uw(3WYAQ8?+O}QYMCm zCqh9I9Vd#bsU6TF{^Q1h+Ch26!(tH!WZ5@aJ!*p;Fw5QHh9X7gje5nmSy7B7jhM?0 zc4=Nq^X!jc*_MhFS<2)_^pvJax&m4FnVcdcDVfoABquW+j)Sl=%IQf6bDA`&L8vRE zNQQbcB_S=RGr5b+y8$&^6L;*|%x)X2ABt78J~k-v%Y-a-I$*X=C<*Gs1KAP+!}GRl zkw$PKDo-m~4g2;TY~_67fM$MG`vCisav~!oWeKJJ6?Y43{ZGzB=8PCM$6w9Txndm{ ziQU#`hwkuTmrab%M&r}f+kPxo<6tTyZfwX z=f=d{*!sOU<;29?+oKypl?}E?+b?zqsJ%PCcVc~aW60P$@_4wf@*RGAbbXDBap2?0 Ur!Q|^`x%XGUZ2snGB+Rm1{3p4-T(jq diff --git a/client/tp-player/res/bar/btn-normal-left.png b/client/tp-player/res/bar/btn-normal-left.png new file mode 100644 index 0000000000000000000000000000000000000000..eb9b22ab347d837a3e5a359b9469e0c92608a414 GIT binary patch literal 1063 zcmbVLzi-n(6gI6uZKW>!7|L{VDJl}S&rV|JtcJFU9U7@ART5}~7;x-MIQ5KNKEAqV4y%5keEP~5F=tol@JqBIj2css5)TD_w#+<_ujkr9?Z^M9vnD3 zz;WDQVah16`2_n<_r>Gpr|BK@T_@U!j`jg>jNI+xZVY~ zQl-`6v~Ckmv|<~vB%AA%PYiwDGh)JYUe znbfiZNfx2xd2w8A>xh={UpEf5jw&kwmP$Ax%c0HQqdwdPv)mmm6dN*c^edsu9>rQR zh`sD#pB9V^&u&D=b#xVJFq==9DZ_v!l3DoKJknH{%$laEC9_?Q!>~xp8)-R*48@dX z*+gliLQ~VS288)^GL`6J3w}f`-^N{Cm+5w}=_9dvF2oikVTF*TZUxLX2qjU21Rz&d z!RVapJER$1h@WRaS_6mfZS3Sj;(>O4b@vc`N70m2(sBfhEG#5ZvL@6lJ0Un&tzoFC zHcTPDgLnQXZxVAziVNhQg6STyDvXy0<UsWTt=hl-{@&v==eGKGp7m_sn7Grse)mm9nYjA)_{K!3k7paJwsDV3nLR91tSAPGkpU~eM1vn14AnVODhAz zjelbq6)er&Tr3QoU0jS@4Gj%lEiEj~3|!64om>nI+$>B?jVD(z$-opj<5aYbDM2O0 z$lS=(Br!!d)iNyGc!XS1tSAPBYguPGSf9Qu`)HYGBV%z zH4gdfE diff --git a/client/tp-player/res/bar/btn-normal-right.png b/client/tp-player/res/bar/btn-normal-right.png new file mode 100644 index 0000000000000000000000000000000000000000..53f68d8e57b9462e4153779612cf283603d0f25b GIT binary patch literal 1068 zcmbVL&rj1(9Is*!QILbiNKAMsgg|1~*KX_f8s=oQlSPJOEMX=d=-M|{p?$S|xWd5@ zOh~*MPyPj-h@3ru3C4KkV2p_u{{e5x!HBP8Ll4A*P1^UPpU?O6{rRjkd#A5=u$SYw zzQVLtW@|Tluk=LU?Sq+5Y`ISLDxD{F+BAI3Ou1AhMr+AV+v|`$#ygRw>P^yu&oTgkSxNYH-#Ja^tzAd*Jmr{ z`m%{EK6?vHgeo)OU}}KSS#bk3%>H5r0XP z5JDo9MOhXYLI~DeYJ`Ft40kMOI52(Nqc(9tWYMURRhr?Mr^g{UUa@#$*bUl=Vkr|t z!xN<#6dfmutA!3|8UJzPRCG{T^RQUP0a^7;R*(8{hs<*KZ=ndtyixD@HYw$Hr+~l$IpMMVhQjlBAN7?hf=1_cIJL zpwFl!y7toVN?-KfIh_4Om+M5UkOf>L4Z}xF*1}Z;bjMgmC1hCZk3OI&hUux<`lTR+?Q*7KDN~jq3KyhL09D=E~;A ziYZ%c<~EoL6-wYBVt~+Dbps_#vrS%w%F!{;f~E>tNwY1dDup7L#y$c`ZZc*Hq6i@5 z#6%K8@fL^+Vw@M~3u9tJNkSzqf%d~vYra)eN@~7si|*2Foe)pq`9`C`H6}UiFY}@- z%MnLB9-|tuVBIA~7;}Ts4ugsU)3-fhV;4k>MisA-G)p~gh2VIFLYLSL+KHkm<3q#a zMNZ%yCyJ}79gq_G?Z&CvL3!Ond_7HRmoyevT>k<&CNC2}2()3A~xW)q?$t3pZ@MNLjY zNz@Wa0jhb8%3ZAP2E=ep)Uj(*yKQXhpIBwuM+U)u8ROMX2Ndg=;Gm8@Fuef5#G>t5 zxDnip%F~KgMZWzQS$Q8jpqXFAKE-}kisyucB*5g~;%;HB|H&Coo#CVA_@h}mN3;VY zvD^A|(;Xh<(uwiuXbhx|pVP6b=xVkcey;vJImz_E{j&#y#qT5F$(e&6n@1y~F9sp_ z$b992K0fwkZ}X^k=-yUpww`}F_Fz}vfBWj3-*>Y8?);_KJ-fq0nfF)cFWi_} ZXo=ZAyf%J(Vd7;pySg@~?&OxXegV3FO6&jt diff --git a/client/tp-player/res/bar/btn-sel-left.png b/client/tp-player/res/bar/btn-sel-left.png new file mode 100644 index 0000000000000000000000000000000000000000..e26b79e1cb4f34b9dd953b1beb893e91e895b6d2 GIT binary patch literal 1049 zcmbVLJ#W)M7`BLnpr{K_379OGN+7U(b{sorHB@!Zp^=(WrIDJ#Qh!_<)7oe33voMC z6;nYhFwqsnhAOcD5(BJkhzT*m4`5B z^Pn$G-QXuPlNksxb@(h?k5V(M2z|RI+vnG^!1p0^ts)#a)o#r5Rgz%7B%_`%T1h;ZsM_etc(xIEO*BX_+4Q(97g@$bzmW7aND~e*nGSq=> z7#0GcR@O?zA-5i-)QLPi)D4*KfLlG5YgQBNP?EF=Ss!-5e1}kyc1X-u7j*v0Vi5VH zmtM`wa}X`UNpK(gwSUh{8#Y>Y9Zk2gZ|?8?{i%Zf@x;sB z?e>`+y!~u!$vU?)_M{7@gC+W6`RuEAmoL;NK2L1@c$z)F_I$6Pj3-F>{^<5LSK!dt U>9@aL-xFABH4HaR2}S literal 0 HcmV?d00001 diff --git a/client/tp-player/res/bar/btn-sel-mid.png b/client/tp-player/res/bar/btn-sel-mid.png new file mode 100644 index 0000000000000000000000000000000000000000..f69acf27a9726f35ed4e5734d543e01a3f76874f GIT binary patch literal 1016 zcmbVLJ#W)M7`7UzRH&Wk0@KMw0wiqTIgXvPYN|SRXe3gqG*UD6Y+oDG+Gp%5al3$! z7}(g^SlC#QSQuF0FYp_XkeDi#a!%vIP<6nPeII_F_jx~GZ*4wYx_;}rBuPuX4R0XU zIq@zp%!}`P{NtTiZgPLbx7i+_keo{E5eq2jC1jrtD2a}rexs|B)OZpPM|{+OWQQzK zNrh3%Bok~&T3stM5*|GsLyFg!U3aU`!j z0IQ`f3?!5jP$q|IVV90PwQGxgb*;%@3gHKieD2h!zXduhr=X>pN{E_mU||)ta9cO- z105n$gIGg`0+9`|4H1|8rt6A8 zD8+HgNvWj8N^QZTMVQAKk68*TizHx2+>wQ+=OH9nzkgvkEoO-lDbq@lX-I`yl2ma` z(FGsSziwQLE{4Y$)dsX+M|mjfvA0r_MeeQ^s(`{9`*9wNqL4$6g+~cZdCzlX@kfo~ z$TqPB-EPxwc^>pJ65+dDY?;t-ec!YUx3+N^7hAq-xgF%`K0?UHZEQl{vRn_sZrf-z zYi=(sI7vfV>&8NN#)VgM?M_Y!XZet^!@2{u#*DLK%rel~Ho@JUIE~n(xL1|uJX(+D z@iQ8AbC!T++G^~W(c4ZBVn`Hxgv>yXop`Fssx(g=8 z;K8Vz`~w<(0B53!5W~f*7mXKwf{6#?!33wvE_xswY|_q0pXYtv&llC%yCcKbhB=NK zDNmauonNAQfHpfj&b|Twd#uPYs$q)1V_xHOz@Vz|0pjKcdD&nR;y&mJa_1U_+ zzU(58pS%MmItDWkU}}R-uo5OlC(n278f>3li#+H;=yIMva;j0Of+C4AP&1m~O4%IH zQAScxPF8LM8A_T6ktitwlnjUrD1qL?Gqu=TG-_t4r^QZrzDa3hh+@0l&a|@`5-*7o zLP&(ND9Zvv2+3MV?T!#8qkRh|PTbg!s82$WTC^R~qIsTqdK5wsRVv4Z!=#remNKzp zN1~L0Vi2TpbZLv!d85CUIK< z4r$rU^XyN?^F2dDIxLj3mTH>NLJ|wVP(r!}m4anyx>D%dIEjn&l9`i=swrENBv~kj zG-&C1!Gy4sQ`BsqTMiRyhc52x`b@XSRZis^#TeU^#C1Ye`W;Yh5=xRLi9m5q12-4^ z&?D_+JT1>rv?h-IN7yUHBmmv~8vY6To~+BNVtWEcHWn15ID%ulS;51agQ2duP(^%? z@BL5SMCOo~Hpo8>)4yU}m~M~TpB;_|2#0KHVm2O+2j67bXqC(6RK4@f`F(geIi&3k z?tiHMB;d*5{+Fk}Zd`iJ&x83hPV3_H^RLE#?2I*5%W(7F+h+$mW7j|OF9(FJa3i<5 bvvmdRaRbE*x2o?BcGH0^TeIeN;laiqh`35i literal 0 HcmV?d00001 diff --git a/client/tp-player/res/bar/btnsel-left.png b/client/tp-player/res/bar/btnsel-left.png deleted file mode 100644 index a84e74b282b20a94618536fc5e34ee2f8c633a6f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1044 zcmbVL%TLrm7_ShGkPtP|MCCFmnt*ZpVmqY`71?c97FZ<9Caj4Ey6r66Ks%+Kbqgnu zi^ih|ABpi`xEYRKJO~LKym~VJ6UKu#3{Ll@2jpPWcD|YUe!uVibbjvMK>ziAhG7Qs zQ$~@l=jeC&eDdG@GW(S-*N9mr3%E)eR*0BM2Uiftd)6{4BFkA{`;5jJrnl;r%A{PF z)oko>R-(hjUO>?dGd>Xqmc4=qsGw!n*V*qMKe51dbaqiL2!$YvYVOoVh!!^HO7_Nz ztvc+)9WWkil)yv80Yf=T-z4i>1>UVK;!vFqro*sIUFwYqN=J1 zha{z_Mk-qOi4~{(XsE+rpvVs0fVkKP38PiPb)vJ>(^d#xP$+bX{ivNNnle7N0$$_< z-t&^Un%WU5qCak&s2!Eo1H>0mgzKSA>rox*fNAbdH+xQ+%@MabkvP1!I6Qx$3WnVhOXA)PTzsH8I;j+3woG_#77Qw(9u5JgiRgNkUT zWdRyFlgeFe-j9go+o)sLrFPp`cq&%QhR7l~EMdIb>45nfCOE3$0Av>+7+!RJ2REXT zq&%%?4HUYMkdq6s2b%fS+!O3iDpE#BD*}}N6?Y5k{7=q!>I|PW$6w9TIiekyh~3tw zo9^%+pH56jN8`=ThY>nfM&6h##osDN$Hx=BaIa_Q)n>Wx$>G+^D-U{Z3~#^R?wfx0 zZt(2$rNf*1!mTat$KZ>DnTMC^7p7(^b diff --git a/client/tp-player/res/bar/btnsel-right.png b/client/tp-player/res/bar/btnsel-right.png deleted file mode 100644 index cfa0078fe2a7735e24e6d82cf88d66222cd6bba7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1047 zcmbVLzfaUq94|j&3=tgEL|k|&i$t!!xW3YclhA7m7dX5e7rD`1`;IHnzS2It!e9&& z#5ha*3!I3%gC?*T7vo@ziHmG5ZpvVSuXo((fDATm-}k-u{d~T^_ezVig9En)7={@v z%o%06UZUUi%jy5%!@?)J3=^|TmT;Xk?Fcb5F0LU^@a+{;Mz*`P@fJ-nOkdrrR7thC zpgGv*>{N$K{E(sS;R!KP(oz#6f(PD4H@pu|r7#%Y zj2ugEIM;JEs49YPO_;J_2&O91@O4X7ppex~6DnD~!*L!~ftHySMcoi44N)}JNvMcs zRu-UPnN;p#3qefmz(F0m9<{4LGp|UNc_CKIN602Ps$jg{>3~uL6C5{i2=Yr1++Owq z7dPXvv^?!-4HS6~k!wZR2d(^S-Z}PX6iFAdiU8$*#oflb|C2MGI>V>U@mI5Sj%Wv_ zVz>3_raL?+pc50((YW!X_lS0T@#@%@)py6~s20pc J<3N9~^BXn+Mz8hREs9Uc2R z7>3!AA5e>QjfU^GcKSW^ZPlR5UZR!B2p%If!$(Y?g)4~7JH|LFBEy=UUPe6((>7t3 z%A{NvR7~u|j1Uv6J04{-Oiyp!Gt5au*a{lA-3<5j$r8ufR)#y1EP#U7kE-^-oR3E4 zhDzq#qzNsq_c+^ASEvC85reHeQ*NNtGu)b8h3>;^fn(PoWHQ68J5??Wv;EjdY%(VE zrkF^xDHs!zFddh=**Fko0YE{NcpxeODnMkL4@cE}YfLGsdQ*#@GF+7qPZ5M#trn{# zV%Q%SL3Z5Yuj(WNtg5wnm8;0GW znJAhvp>BAB7z2XigmJB*15!kP+}IQylx9676j6X@e3RB=>~M=rbN6qd5JfGPePj^qmoT1cb--{H6C6~r z$M%oNY}XmvwQwys7M5o{S{3>BS!C%xcG$K2D)uJ&Ry-9?N`}QFXdqsKQiZP=W`ehn zTtOfun;;3fCg1v>yb07HA#9Mp8m4tcyD;2tv_Cy;3=neZ)cABfcEA7hjgHp!yxLc) zzpMOQUF~f<(%7n&g{^OHWxsShU)=KjT>Hll*`8lj>Di$@3#0APm&`O5T{<$_0MQ$f z>0Qy4%6!M;$aLq<+k5X!WGBvN58R9_suQ|=sq-!K@aaP2;&Nl<^=`d8I{*2?{s-Hy zoVvT~Qlm>chA)pFWp74a4Q?NJF&Ok6&`&-}WE&CYhkg*>yC!vpL!Z}%)cd*7`QIf7 BVs!uj literal 0 HcmV?d00001 diff --git a/client/tp-player/res/bar/chkbox-normal.png b/client/tp-player/res/bar/chkbox-normal.png new file mode 100644 index 0000000000000000000000000000000000000000..1ac352d1f4db926d8db14a5621ed3d7792615ef4 GIT binary patch literal 1104 zcmbVLNodqi7*1Q4Qd^lKJ*c41xYR9^msv9NLdQBY$=Cs>mg#`KxV%YT+q6wyOkSO7 z6|9~Vym=A4coacFy$FH}cu)}ZC|*2@c#v9f0kOWcGu1=!U?6!*zVHA3^~%uTk*XYIhJ7MrEm|d@=-@F@CfJ%e^OE!lLfeF0 z8DS%(qnb&bC=M~vy5n)SAoTUuJ#0=A2C8Jtb~EDVM^8jxTN&|qvII+Bo{ZZAvpyM~ z9jutMlP0pn{zIUzu5kknVHnh%DL2sS8F9(3#`od1EP^Eno6LyIPK}g?K%V*pB%`Wi zD)BT(p{SBX=~&_*h(Se_A(EAZ1QiV;4Jx4Vh+NILMzykDXlU_MMjU6%(`30;t3_+^ zDD}r=1tBEEm>i2q93cfWF2i-n4R$sy=p-&-M@uGAooT)=G(j| zcuJ?{v_o80)H5Rg6SZthQ&9@$3UMQ;>(D?74?kBxDHSGihM}esxu%WPxG1eCoU7|G zLs1k1rI8AaR4S)KSV$+5@g}$E1`N9hb~%c literal 0 HcmV?d00001 diff --git a/client/tp-player/res/bar/chkbox-sel-hover.png b/client/tp-player/res/bar/chkbox-sel-hover.png new file mode 100644 index 0000000000000000000000000000000000000000..47970ca0a1672543c25d5ad9a34688e49d3f5561 GIT binary patch literal 1226 zcmbVMU2NM_6!z4T!qlR4?4d<4t~_n(G#p>si4&WcwyB*p8tG4(ZQ+I0v2RY9p&c3&y^L-vSYQC&oI>vkD| zoT-kY0#fzqm#?8TK{SjT#gbpjzaVP18B;?{EHE9+CWv%r;HcUJ@Lk!0QA@{0*M$;X$+SFNg>80gcKWp7O;@vX(-T4oPvx91rai!_K;Z3)knmF zoULi$l|+vDz9Z6frBaDixR~vZ(u^PoG-PR(r7(i>rY&C$D9hVjw;&@=a}CEgYzu@I z)v`V1OC_cn zK1O!e$vAfp7Yc#c8?oOt@TI7evaL;-$ntZtMBfJ5SLfy|&sQxC)pZT5TjO>= zlq>eQNcC;EXxo$Z3K$r(ecKze9ndqxgN`GHrP~#6UwAzm(aOj*UO{@+wN0>|U(wh? zUuTnSBChHb5>!ORg?O1Ns~Sh?h%Y0U^2s zvDrY#!d>Iy_GsL9?-D9J!+#%&eEQgx_NPw&23sed?p?inuvvQV zIC~e&Yi(73_Trsi8s=83iM0n;mgg6Dt<2gd+Jfi$x%kZ!O@pV-zxd9Zw+^4*wbK1X zWOn)cqs`-gzS=%>`y7JVZ_-~cye|*V-Qr$qICiM*@w*?xbW=O~N96dIrIrs54+rlq zu2h$=Hx8ca=lbZzaqi~z8(PO(KkKRae$Fj?wwI^CYd>}!+j;xs+xiEW?|jRiX^bwN fKC^H<rl=(W<`ZuZ15*!@o;T>5~^T!zcd%SAUt~ literal 0 HcmV?d00001 diff --git a/client/tp-player/res/bar/chkbox-sel-normal.png b/client/tp-player/res/bar/chkbox-sel-normal.png new file mode 100644 index 0000000000000000000000000000000000000000..872fe0c0a94365b34b519449dccd25542456a310 GIT binary patch literal 1166 zcmbVM&u`mQ95+;3N7qW(5Yjphyty!n!1lA9*m+%|p$S+<>wfW@0J{u(eELV3I}0E3i*D@3X+M3+#NZ z4(magw4LgDNaogOn%4TVg=}_e5=v$6LE^4>k(LzLeO-<2)7JtE_95|dfjw|) zp*{=Ben>!$S2#XIRfqJi>AfrhG`Lj{Q#8b6fi#Tc(&h(-b>4K5Uox^=QCngLf-}Z`PH0b?Awwm<+9l3 z2*QNRqO8f8*pfM$C?%DRFZ+PqKSasvg%*KV&rd%B9tzB!krsu!jW^S^d zzx`nA_7mn=xpcDIJ(bxk4Gev{2yeWj11EFgG@1Ff}=oHUoZtX6GdF000I|Nkl6xT%ou?< zIzSu-LZQ%>b}W5)4t!rd+$j}GTY5RkFTMBl-rL`==eytQ99bw75-ziUDHHHaIp8V4 zvw$Z7PXO`(IzT47<`KXN*aG|wxDWUZ@FyS`PckwdL77Yz^F!bnc=K}r3*aSyHpWM} z06znMK*7K%bOsLb7YO$%nWz?kG0rXFA)pV%0ylqB1+s{tuy#P6_j@JG*Ve#C2SCkzdwY9|(a}-e?Ch-e;NUsuBfPg*tb|L4u~?1st3>o zT&LnHkTuD70dH|%AP^A7#>R9L6BAeBB}{DT>FL3`y1Kpk`g*5Qd91_#7Vs5qvM~#U z?~(jRfLFM0b8}PK-QArJO))(gV_88#!9i`c zUftK%m+SR< z(9n<}Rpyt-1vwGd4M5?k1hNR-;6uUs`g&S#Z*T5JMlKS6N92Uum`lw#%T5!)IzSm7 z`2BvNr>7?`QNzG_3^^e;{3SX>3@1&BNKwN`j$H-3gOjB53 z8NYCGaWO-RJ8j5$d3ia5xXP%&YuvlKx+=Noaa&t|Tcg~sQGwxk9PXh=RiMpgqudLr z!0^VA-ELP(Rp8Ff4&|Op1%?Mox7)3dsz4EU4Hd|DoP}hs)Z=L)?&_!uUMZSfE)Siw zns~$=DwQfIRe>Vz9xBigZnzpvK&k>WGc!*VXyaak!Qhvwz?_^M%H2i0grc#nIFi;KNd6;UYPnAZ;`lKg-Ul(8-BJ@+US3W)Pcu!&2KrHcP*qj6uT&}}vt~lBm{W|T zg$leY9Hor~Og=&sT~ZN_bW&X$azw7gb&EI8NORF) zTy@|9zOa~lOm%_4k!FFB`}fCsS8@6b&gCQoByqdipwA{XRDTo{lC5!Z-O{-vMb<0e0^ zt*v!6H8t&AWF+yokppreZcJfMo_NnGiV(2EqMrM#R;#+dzdu!q5b&MrjJXJbiIlTm zwT}BPO&N(OlJ5cDS{if1VoG{wVGE`!04_);#>gQ6DWVU`DzH?b7s;gSbe1s7I~Cu*`-PWiUL zc95w#9FD_sO3ZU$DgbxOJsyt&A}nJaMo6tz2XOxfw@7do0T-*d7LD+8+kkI$gk+Ud ziXMu&08BO}q9AsI4O64Q&36he(FLA&h!T#{#u(KXh4Ym({~{cfG~bOhGdVM{1mps- zqj?(ejBvat9Yte`0JrvVqj{gr{;??iFTn4BKZHaTrC(P2SAYQkN*NKynK5n600000 LNkvXXu0mjf7Zp0t delta 1443 zcmV;U1zh@|6uJ?RNdab&N{%HnEn_e-F(5WJE-^GUHa0OVATls9GA}SPFEBPOF*G_g zHaaplvHe{EAVM}XIYBcuF+?ypL@_Z%IYBuzFgZj-LO3upF*h_hG?RJ)93VnAGdV#s zH84alI7BfqML9t^G%z_tL_#<)Gch+bIW&{D0$>V3H!?ynG&e9dHIrZi7n3^!eSh=y zz#{+v1nNmdK~#9!+*;pn6GsqUJBHAtI4Eg=CMG~gi$YtHNJxEX1yUa>A%VmLh=SUG zkUxTlN=<2t08`PlYJQ10fLhrip)m;wF*C}y=`gvo@9f>XgONUEy^ihqc4xnt+udBF z(J=3`6+C=rj!-1@G?Q>yO-`nrrCdyRFtsMMAFW}+x=6oO1-iZCqtdN7kk zlZT-4_(kZCVdP_L{1OTv|yygZ2mwd5d64<-ojSCs@|65H5KTqP)t5r67Vw6+vHi zoN%3RA>>)IT^kDGVhI-UKx!XWeHYREyPsoyx)P$I5Sv{Nd9nViC6&jlBP>KW37j5nU{5dLnlZ({_HT_UeS+J9NFjTmciGmPN6P@sB7 zl$6Qv-sdkdRJ1PI>yqG_r~6XbmL2Y=yLvvWr1!k#KFdoq7T5&YCsA@CUMtmb&YmKB#h1glV(>T6CA@b$CN-?2@HHXg_9yNHu&LLDg z5sX@Olv&IB^80f!VsFDt`-K_35H`J)_IHd`LW3g|p=t z63(M{i*P#ua_rMX5%$%f^&NJ?%R+Nr%*LW z=m}#+kTYiQ`F%@*HW$u{*Gt$VX{0=JkKl{7*=DtCKEg;<)cSN$R<)ZB%F3VRR+06t zTqAh_a(`Z~kyw&J7lBTq&)440HuBtpA~KI!*2zU6X-Qw@T2N2(-mY@#6!P9)#*^X6 zGZyzcrQErTd!2Hr9R<(wh1fb$Kx!9a^^Ep|>`~L%mF8z4RdbPZa~^KpGub}#|D|*r xqV`hyHbi6-;kSmhSHnh8W-p~@(%uL#06EQCKM+xV0k!}D002ovPDHLkV1izwkY@k@ diff --git a/client/tp-player/res/bar/pause-normal.png b/client/tp-player/res/bar/pause-normal.png new file mode 100644 index 0000000000000000000000000000000000000000..455497e54f4d13c95942dd8ace38295926307896 GIT binary patch literal 2497 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 literal 0 HcmV?d00001 diff --git a/client/tp-player/res/bar/pause.png b/client/tp-player/res/bar/pause.png deleted file mode 100644 index 66b4bb398e78a7fb0d63efc070a7659552b83b57..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2295 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 V2Pa46dAI-o002ovPDHLkV1l2nIL-h7 delta 1443 zcmV;U1zh_27rGIUNdab&N{%HnEn_e-F(5WJE-^GUHa0OVATls9GA}SPFEBPOF*G_g zHaaplvHe{EAVM}XIYBcuF+?ypL@_Z%IYBuzFgZj-LO3upF*h_hG?RJ)93VnAGdV#s zH84alI7BfqML9t^G%z_tL_#<)Gch+bIW&{D0$>V3H!?ynG&e9dHIrZi7n3^!eSh=y zz#{+v1nNmdK~#9!+*;pn6GsqUJBHAtI4Eg=CMG~gi$YtHNJxEX1yUa>A%VmLh=SUG zkUxTlN=<2t08`PlYJQ10fLhrip)m;wF*C}y=`gvo@9f>XgONUEy^ihqc4xnt+udBF z(J=3`6+C=rj!-1@G?Q>yO-`nrrCdyRFtsMMAFW}+x=6oO1-iZCqtdN7kk zlZT-4_(kZCVdP_L{1OTv|yygZ2mwd5d64<-ojSCs@|65H5KTqP)t5r67Vw6+vHi zoN%3RA>>)IT^kDGVhI-UKx!XWeHYREyPsoyx)P$I5Sv{Nd9nViC6&jlBP>KW37j5nU{5dLnlZ({_HT_UeS+J9NFjTmciGmPN6P@sB7 zl$6Qv-sdkdRJ1PI>yqG_r~6XbmL2Y=yLvvWr1!k#KFdoq7T5&YCsA@CUMtmb&YmKB#h1glV(>T6CA@b$CN-?2@HHXg_9yNHu&LLDg z5sX@Olv&IB^80f!VsFDt`-K_35H`J)_IHd`LW3g|p=t z63(M{i*P#ua_rMX5%$%f^&NJ?%R+Nr%*LW z=m}#+kTYiQ`F%@*HW$u{*Gt$VX{0=JkKl{7*=DtCKEg;<)cSN$R<)ZB%F3VRR+06t zTqAh_a(`Z~kyw&J7lBTq&)440HuBtpA~KI!*2zU6X-Qw@T2N2(-mY@#6!P9)#*^X6 zGZyzcrQErTd!2Hr9R<(wh1fb$Kx!9a^^Ep|>`~L%mF8z4RdbPZa~^KpGub}#|D|*r xqV`hyHbi6-;kSmhSHnh8W-p~@(%uL#06EQCKM+xV0k!}D002ovPDHLkV1jD(ki-B0 diff --git a/client/tp-player/res/bar/play-normal.png b/client/tp-player/res/bar/play-normal.png new file mode 100644 index 0000000000000000000000000000000000000000..430f6f4c0de9bae75682bc5fc61d70077fd5b2f0 GIT binary patch literal 2898 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 diff --git a/client/tp-player/res/bar/prgbar-right.png b/client/tp-player/res/bar/prgbar-right.png index d7d3f3f90b7fe7254023ee5bdc7ef47034675055..8897c17e75145d3dab8eca38ca8daca50a9b7546 100644 GIT binary patch delta 268 zcmbQq(aSMGoQsWvje&vT!I}3S6BRWWvnG1=sha5~8yZ_Em|N-@np#>Indm4O85o-B z8(8Wan&=uBS{Ybc85nN-8_TF*Y3^j<>}qP_V&rOQXy|HbVQFUIYHsf2VqoBAVPa}L zxr#{!rpOqlqHRnGDk(q2cVMt=f2*wWL*F{I*F(x3nT_cJpyAFfyU$9TlQ;fJPxv4NB7goCvTbsfHs%mw6A plMXO)NU?7Iqj`mqhk=oeL1VAXPmMn-4*(5e@O1TaS?83{1ORC^P4WN$ delta 262 zcmeC>n8`6goQso#je&tda8~TxiHaJGX%oHrRE>3$4Gawx%*^x*O-#+q40RNY3=EC* z4S>i@*U-eu)Xd7reB<9(Mg?;-7ehx!Q%e^E3l~E}S4#^sb3<1XQv)*>H&-CrX>t{l z3`~&)PDR_861W`AjhqZj%ni&;CnqqAPqt>RO?|ZI&H}1{rUgjp4sdG z(-A%go=Jb%j_^1B5cO!{T-6}hv9mFPyW{8t)d`I@dYpbSLiYHkF*3xS3*c)I$ztaD0e0swGQO<@24 diff --git a/client/tp-player/res/bar/prgbarh-left.png b/client/tp-player/res/bar/prgbarh-left.png index 4f936ff94b33cc147d59f9bc6962eb4ed225d16d..9a6ba68c5f60b5c31754b818c8ff174a61ac7f87 100644 GIT binary patch delta 272 zcmbQjF_B|}I2RiS8v_HwgEQ|vCMs$$W=-_!Q#I2~HZ-}K9*(Q*C|10$n@%&7*J84I?kGfO2T t9billx_wbqVxmrYyA?x@r9%TF!;_^PKknD95Ca;-;OXk;vd$@?2>{&lO=|!E delta 257 zcmbQpF@ra<6i>0)5vVrb}U>1b(UVCmxGWMN=zXl`O@GP#OL z2BydYr=o3430#imMotDM<_2b_lM|T5CtEYuCPbh0Nd`L2#nZ(xq~cc6pa1{unavJ# zs61pi^3g$u{fLhNXSG7!fn~{d0@?|aZ}1+`IU(c|@QZWTQ-jXdA2ClkN~h{_+mtqd%!3=B8^jb&7@G*CDG<3DJurxDpH8*#1F)(nmFflcr zT*V{ra<82;$mRoVrb}U>1b(UVCmxGWMN=zXl`O@GP#OL z2Bydvr=o3430#imMotDM<_2b_lM|T5CtEYuTIX)sa0ci!c~2L|kcwMLfByfsXXe<8Ac8R2Jzopr0CA^0HUIzs diff --git a/client/tp-player/res/bar/prgpt-hover.png b/client/tp-player/res/bar/prgpt-hover.png index 51cb23232c33e3bdceed7225c8963e73befc6bc8..e98d208403759276391a3c18eec9f150465a1b8f 100644 GIT binary patch delta 401 zcmaFH@rOgPGr-TCmrII^fq{Y7)59eQNb`U&2OE$KD~)+RQBi|2Yob@5s+n%Gp|OR6 zxuu?=silRHiH?GifuWhcfu+8oiLQa6m4T&|f#Jr#v5X2pSqn=eH)j_kS3^TXS4#^^ zGXqz1b0-%A12+p3Q{%~1OfoP|2R9O&Q`o-U3d6}OW9{Qqyy%n_lVn!p?+=^(>-#EM6t zfVE>f^T(dW7kTzOF!d<#Ivl;^@J&p?Ea3p39>YfdfXl&dt5yFys?9&LSb|NDX``j^ z1P^s5Lyi~Bg;E~QBFrBZxRxC3VBu}^;aSpnUxD|z$AyAcPN{Q8nw~1S@)|F6bmNF& zTA9k^cFbSlu(Xurk4Fc%mrN7X%}ik`BZ3`njxgN@xNABnovj delta 361 zcmeyv@r*;UGr-TCmrII^fq{Y7)59eQNb`d*2OE&IzBbisqM`<4+C;BDRb$;`14Bav zGc!Fy6H_xYLmdSp14AQy10XWfH8im@HM24@-}pC{QNhU2+|a_*#LUIO!qw2w)zZn> z&Ct!<)ydJ#z{Jel&}4EIlMGCeDNaS(m=d@g&5fK4Ow0|;OeZHWi%+&@uC1STtl%`z zY12Gi978H@CH?vT-=2AA184UPUIBgfN9qoLguV3JyB0G|I2&+==}7SZ$LtD~VKd}X z&TjrM%$~M5d)q&MDTbBpzb$7U*>H+cu;;>sI>sZ9R5I$=j$AT0yX4q)eTB4k%ipS< zv+gS#|6C_zD(}>nE-mDIo6UCKa)Fh`gzopr0FMuMCIA2c diff --git a/client/tp-player/res/bar/prgpt-normal.png b/client/tp-player/res/bar/prgpt-normal.png new file mode 100644 index 0000000000000000000000000000000000000000..59e1faa48a573c8c92a59c78ebd254ee8ed40be7 GIT binary patch literal 1108 zcmbVLOK8+k6b*jsr`0aBDt^eYy95V{I$V4I~So;AK(8 z-aGE2zVV)tIbJoP#qHk3c7-Z6;2>hKp)=wJYMAC`?W%MiT?-sL3nA4sHy6}kp_k2K zAF;`p%$s67#VRl+CSgiS>|`Y%$^w9bnBak^0;mF!tv?*~=37H*QOnhR(NmhM5aOwV zP^;BqwRjBs!-5DQ6o4d15>F9)Fy;~?DwN$vCBpljWQl3X^uuZmxALJ z3iF2Dpe_`ZOb8865Mw}aoG7nZbU=#ePZ$fLgVLCXgdz&?sBhAG47E4NRJ(sWihwj4 z^?+~Fq8K9@Hb))glDwAY=%1KnTdE8dkjcgMq^1EKiZuO94k|K8WOQ9t5}AgLg}7Ap z6qIr)P0~eC)L{zBKv$HE20$*ANX8r7yc-b1HBrN_P5sun%0Ibk)<*`xehK4|MhEm( zFu_3udu+B(W;+LL*TS`6dsLpeY&GQD$B~uuvBS>ltJ(|rwU`^ebQ5SmLewCCHB94*c44%gZ-082A0Xt?sqyJ}G;O)OjgHnt zUh6J}?=D>wTPE1m`hym)>BX6?=dOVZogMGBwvWC0UTs>x?&P-(SAMOyv$VAvJbS!k zsBLok{Lkqwupj}7%#q3820&qG literal 0 HcmV?d00001 diff --git a/client/tp-player/res/bar/prgpt.png b/client/tp-player/res/bar/prgpt.png deleted file mode 100644 index 0973ffd75e33c4f585d067b9ea37f64fb0ae916c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1151 zcmbVMO=#0l9M7l@oen<_ejI$H^JB6$Nk7shuFkeewPNe+3RXlkOJ3KwB`-_f)@%oH z195`lE<;cd6`3GRcan(*J$O)Yr^y7psCX1aL=Y78Wm~6*xq~6e`^fM2`+vOuKNs3^ z`|Inr)KL^wpE)4q$+*UQR@9RBo|(~uWY~=5B5p?|Tv2RDB{b9lLB>!zVIC@4f6sH+ zLQ%ewUMS*X_K>I|BdBuH%`y$ie&C>Gc=fm;I245=TtG<22#j| zAQ}t@RF>la&j-0kl;^o75MsFy!;+g1a1k-ei=h~pzi493)=FYtO3&LOt2kZ8*b*70 zQmF(h;UKa*8BP!cPa_lx5JbS~H?iUdOvhhUkf5X5x`lOQ0#8xtK)pCl6Hn(tFsy8L zLD+QW6Gc+SxQfMaL6$KLFRod%gY)o@8;hcyLcax>Jaka6t&;O7`Kx4-yQK|zK;n&f z*w)FRDBTiLdktvfj1;HIm!PI=A}_?)WI8NIC5e>{~8-vA-!zX=T6mb3S*jV4PV*&rm(v2M>i4B9zt1iRovy)W$8N%1@_ res/bg.png res/cursor.png + res/bar/bg-left.png res/bar/bg-mid.png res/bar/bg-right.png - res/bar/btn-left.png - res/bar/btn-mid.png - res/bar/btn-right.png - res/bar/btnsel-left.png - res/bar/btnsel-mid.png - res/bar/btnsel-right.png + + res/bar/btn-normal-left.png + res/bar/btn-normal-mid.png + res/bar/btn-normal-right.png + res/bar/btn-sel-left.png + res/bar/btn-sel-mid.png + res/bar/btn-sel-right.png + res/bar/btn-hover-left.png + res/bar/btn-hover-mid.png + res/bar/btn-hover-right.png + res/bar/play-hover.png - res/bar/play.png + res/bar/play-normal.png res/bar/pause-hover.png - res/bar/pause.png + res/bar/pause-normal.png + res/bar/prgbar-mid.png res/bar/prgbar-right.png res/bar/prgbarh-left.png res/bar/prgbarh-mid.png + + res/bar/prgpt-normal.png res/bar/prgpt-hover.png - res/bar/prgpt.png - res/bar/select.png - res/bar/selected.png + + res/bar/chkbox-normal.png + res/bar/chkbox-hover.png + res/bar/chkbox-sel-normal.png + res/bar/chkbox-sel-hover.png From 4897a6e483bce47eae72b29e40616d001041b82b Mon Sep 17 00:00:00 2001 From: Apex Liu Date: Mon, 9 Sep 2019 20:15:53 +0800 Subject: [PATCH 16/44] .temp. --- client/tp-player/bar.cpp | 240 +++++++++++++++++++------------- client/tp-player/bar.h | 96 +++++++------ client/tp-player/mainwindow.cpp | 6 +- 3 files changed, 196 insertions(+), 146 deletions(-) diff --git a/client/tp-player/bar.cpp b/client/tp-player/bar.cpp index ed28a98..6d77bf4 100644 --- a/client/tp-player/bar.cpp +++ b/client/tp-player/bar.cpp @@ -6,6 +6,17 @@ #define FONT_SIZE_DEFAULT 12 #define TIME_STR_PIXEL_SIZE 16 #define TEXT_COLOR QColor(255,255,255,153) +#define SPEED_BTN_WIDTH 42 +#define CHKBOX_RIGHT_PADDING 6 +#define PADDING_TIME_PROGRESS_BAR 10 +#define SPEED_BTN_PADDING_TOP 8 +#define SPEED_BTN_PADDING_RIGHT 8 +#define SKIP_PADDING_TOP 10 + +#define BAR_ALIGN_TOP 10 +#define BAR_PADDING_TOP 18 +#define BAR_PADDING_LEFT 15 +#define BAR_PADDING_RIGHT 15 typedef struct RES_MAP { RES_ID id; @@ -16,24 +27,25 @@ static RES_MAP img_res[res__max] = { {res_bg_left, "bg-left"}, {res_bg_mid, "bg-mid"}, {res_bg_right, "bg-right"}, - {res_bs_left, "btn-left"}, - {res_bs_mid, "btn-mid"}, - {res_bs_right, "btn-right"}, - {res_bsh_left, "btnsel-left"}, - {res_bsh_mid, "btnsel-mid"}, - {res_bsh_right, "btnsel-right"}, - {res_pbh_left, "prgbarh-left"}, - {res_pbh_mid, "prgbarh-mid"}, - {res_pb_mid, "prgbar-mid"}, - {res_pb_right, "prgbar-right"}, -// {res_pp, "prgpt"}, -// {res_pph, "prgpt-hover"}, - {res_cb, "select"}, - {res_cbh, "selected"}, -// {res_play, "play"}, -// {res_play_hover, "play-hover"}, -// {res_pause, "pause"}, -// {res_pause_hover, "pause-hover"} + {res_btn_normal_left, "btn-normal-left"}, + {res_btn_normal_mid, "btn-normal-mid"}, + {res_btn_normal_right, "btn-normal-right"}, + {res_btn_sel_left, "btn-sel-left"}, + {res_btn_sel_mid, "btn-sel-mid"}, + {res_btn_sel_right, "btn-sel-right"}, + {res_btn_hover_left, "btn-hover-left"}, + {res_btn_hover_mid, "btn-hover-mid"}, + {res_btn_hover_right, "btn-hover-right"}, + + {res_prgbarh_left, "prgbarh-left"}, + {res_prgbarh_mid, "prgbarh-mid"}, + {res_prgbar_mid, "prgbar-mid"}, + {res_prgbar_right, "prgbar-right"}, + + {res_chkbox_normal, "chkbox-normal"}, + {res_chkbox_hover, "chkbox-hover"}, + {res_chkbox_sel_normal, "chkbox-sel-normal"}, + {res_chkbox_sel_hover, "chkbox-sel-hover"}, }; typedef struct SPEED_MAP { @@ -69,10 +81,11 @@ Bar::Bar() { m_percent_last_draw = -1; m_play_hover = false; - m_playing = true; // 0=play, 2=pause + m_playing = true; // false=paused m_speed_selected = speed_1x; m_speed_hover = speed_count; // speed_count=no-hover - m_skip_selected = true; + m_skip_selected = false; + m_skip_hover = false; } Bar::~Bar() { @@ -92,11 +105,11 @@ bool Bar::init(QWidget* owner) { } // 无需合成的图像 - if(!m_img_btn_play[play_running][widget_normal].load(":/tp-player/res/bar/play.png") + if(!m_img_btn_play[play_running][widget_normal].load(":/tp-player/res/bar/play-normal.png") || !m_img_btn_play[play_running][widget_hover].load(":/tp-player/res/bar/play-hover.png") - || !m_img_btn_play[play_paused][widget_normal].load(":/tp-player/res/bar/pause.png") + || !m_img_btn_play[play_paused][widget_normal].load(":/tp-player/res/bar/pause-normal.png") || !m_img_btn_play[play_paused][widget_hover].load(":/tp-player/res/bar/pause-hover.png") - || !m_img_progress_pointer[widget_normal].load(":/tp-player/res/bar/prgpt.png") + || !m_img_progress_pointer[widget_normal].load(":/tp-player/res/bar/prgpt-normal.png") || !m_img_progress_pointer[widget_hover].load(":/tp-player/res/bar/prgpt-hover.png") ) { return false; @@ -120,9 +133,7 @@ void Bar::start(uint32_t total_ms, int width) { _init_imgages(); QRect rc = m_owner->rect(); m_rc = QRect(0, 0, m_width, m_height); - m_rc.moveTo((rc.width() - m_width)/2, 10); - //m_rc.moveTo(10, 600); - qDebug("m_rc (%d,%d)-(%d,%d)", m_rc.left(), m_rc.top(), m_rc.right(), m_rc.bottom()); + m_rc.moveTo((rc.width() - m_width)/2, BAR_ALIGN_TOP); } } @@ -139,38 +150,44 @@ void Bar::_init_imgages() { // 合成背景图像 { - pp.drawPixmap(0, 0, m_res[res_bg_left].width(), m_res[res_bg_left].height(), m_res[res_bg_left]); pp.drawPixmap(m_res[res_bg_left].width(), 0, m_width - m_res[res_bg_left].width() - m_res[res_bg_right].width(), m_height, m_res[res_bg_mid]); pp.drawPixmap(m_width-m_res[res_bg_right].width(), 0, m_res[res_bg_right].width(), m_height, m_res[res_bg_right]); } { - m_rc_btn_play = QRect(15, (m_height - m_img_btn_play[play_running][widget_normal].height())/2 , m_img_btn_play[play_running][widget_normal].width(), m_img_btn_play[play_running][widget_normal].height()); + m_rc_btn_play = QRect(BAR_PADDING_LEFT, (m_height - m_img_btn_play[play_running][widget_normal].height())/2 , m_img_btn_play[play_running][widget_normal].width(), m_img_btn_play[play_running][widget_normal].height()); } // 合成速度按钮 { - int w = 42, h = m_res[res_bs_left].height(); + int w = SPEED_BTN_WIDTH, h = m_res[res_btn_normal_left].height(); QRect rc(0, 0, w, h); - QPixmap btn[widget__max]; + QPixmap btn[btnspd_state_count]; // 未选中状态 - btn[widget_normal] = QPixmap(w, h); - btn[widget_normal].fill(Qt::transparent);//用透明色填充 - QPainter pn(&btn[widget_normal]); - pn.drawPixmap(0, 0, m_res[res_bs_left].width(), m_res[res_bs_left].height(), m_res[res_bs_left]); - pn.drawPixmap(m_res[res_bs_left].width(), 0, w - m_res[res_bs_left].width() - m_res[res_bs_right].width(), h, m_res[res_bs_mid]); - pn.drawPixmap(w-m_res[res_bs_right].width(), 0, m_res[res_bs_right].width(), h, m_res[res_bs_right]); + btn[btnspd_normal] = QPixmap(w, h); + btn[btnspd_normal].fill(Qt::transparent);//用透明色填充 + QPainter pn(&btn[btnspd_normal]); + pn.drawPixmap(0, 0, m_res[res_btn_normal_left].width(), m_res[res_btn_normal_left].height(), m_res[res_btn_normal_left]); + pn.drawPixmap(m_res[res_btn_normal_left].width(), 0, w - m_res[res_btn_normal_left].width() - m_res[res_btn_normal_right].width(), h, m_res[res_btn_normal_mid]); + pn.drawPixmap(w-m_res[res_btn_normal_right].width(), 0, m_res[res_btn_normal_right].width(), h, m_res[res_btn_normal_right]); // 选中状态 - btn[widget_hover] = QPixmap(w, h); - btn[widget_hover].fill(Qt::transparent);//用透明色填充 - QPainter ph(&btn[widget_hover]); - ph.drawPixmap(0, 0, m_res[res_bsh_left].width(), m_res[res_bsh_left].height(), m_res[res_bsh_left]); - ph.drawPixmap(m_res[res_bsh_left].width(), 0, w - m_res[res_bsh_left].width() - m_res[res_bsh_right].width(), h, m_res[res_bsh_mid]); - ph.drawPixmap(w-m_res[res_bsh_right].width(), 0, m_res[res_bsh_right].width(), h, m_res[res_bsh_right]); + btn[btnspd_sel] = QPixmap(w, h); + btn[btnspd_sel].fill(Qt::transparent);//用透明色填充 + QPainter ps(&btn[btnspd_sel]); + ps.drawPixmap(0, 0, m_res[res_btn_sel_left].width(), m_res[res_btn_sel_left].height(), m_res[res_btn_sel_left]); + ps.drawPixmap(m_res[res_btn_sel_left].width(), 0, w - m_res[res_btn_sel_left].width() - m_res[res_btn_sel_right].width(), h, m_res[res_btn_sel_mid]); + ps.drawPixmap(w-m_res[res_btn_sel_right].width(), 0, m_res[res_btn_sel_right].width(), h, m_res[res_btn_sel_right]); + // 鼠标滑过状态 + btn[btnspd_hover] = QPixmap(w, h); + btn[btnspd_hover].fill(Qt::transparent);//用透明色填充 + QPainter ph(&btn[btnspd_hover]); + ph.drawPixmap(0, 0, m_res[res_btn_hover_left].width(), m_res[res_btn_hover_left].height(), m_res[res_btn_hover_left]); + ph.drawPixmap(m_res[res_btn_hover_left].width(), 0, w - m_res[res_btn_hover_left].width() - m_res[res_btn_hover_right].width(), h, m_res[res_btn_hover_mid]); + ph.drawPixmap(w-m_res[res_btn_hover_right].width(), 0, m_res[res_btn_hover_right].width(), h, m_res[res_btn_hover_right]); - for(int i = 0; i < widget__max; ++i) { + for(int i = 0; i < btnspd_state_count; ++i) { for(int j = 0; j < speed_count; ++j) { m_img_btn_speed[j][i] = QPixmap(w, h); m_img_btn_speed[j][i].fill(Qt::transparent); @@ -197,44 +214,49 @@ void Bar::_init_imgages() { { int h = fm.height(); - if(h < m_res[res_cb].height()) - h = m_res[res_cb].height(); - m_rc_skip = QRect(0, 0, fm.width("无操作则跳过")+8+m_res[res_cb].width(), h); + if(h < m_res[res_chkbox_normal].height()) + h = m_res[res_chkbox_normal].height(); + m_rc_skip = QRect(0, 0, fm.width("无操作则跳过") + CHKBOX_RIGHT_PADDING + m_res[res_chkbox_normal].width(), h); } int w = m_rc_skip.width(); int h = m_rc_skip.height(); - int chkbox_top = (m_rc_skip.height() - m_res[res_cb].height()) / 2; - int text_left = m_res[res_cb].width() + 8; + int chkbox_top = (m_rc_skip.height() - m_res[res_chkbox_normal].height()) / 2; + int text_left = m_res[res_chkbox_normal].width() + CHKBOX_RIGHT_PADDING; int text_top = (m_rc_skip.height() - fm.height()) / 2; - { - m_img_skip[widget_normal] = QPixmap(w,h); - m_img_skip[widget_normal].fill(Qt::transparent); - QPainter ps(&m_img_skip[widget_normal]); - ps.setPen(TEXT_COLOR); - QFont font = ps.font(); - font.setFamily("微软雅黑"); - font.setPixelSize(FONT_SIZE_DEFAULT); - ps.setFont(font); - ps.drawPixmap(0, chkbox_top, m_res[res_cb].width(), m_res[res_cb].height(), m_res[res_cb]); - ps.drawText(QRect(text_left, text_top, w-text_left, h-text_top), Qt::AlignCenter, "无操作则跳过"); - } + for(int i = 0; i < chkbox_state_count; ++i) { + for(int j = 0; j < widget_state_count; ++j) { + m_img_skip[i][j] = QPixmap(w,h); + m_img_skip[i][j].fill(Qt::transparent); + QPainter ps(&m_img_skip[i][j]); + ps.setPen(TEXT_COLOR); + QFont font = ps.font(); + font.setFamily("微软雅黑"); + font.setPixelSize(FONT_SIZE_DEFAULT); + ps.setFont(font); - { - m_img_skip[widget_hover] = QPixmap(w,h); - m_img_skip[widget_hover].fill(Qt::transparent); - QPainter ps(&m_img_skip[widget_hover]); - ps.setPen(TEXT_COLOR); - QFont font = ps.font(); - font.setFamily("微软雅黑"); - font.setPixelSize(FONT_SIZE_DEFAULT); - ps.setFont(font); - ps.drawPixmap(0, chkbox_top, m_res[res_cbh].width(), m_res[res_cbh].height(), m_res[res_cbh]); - ps.drawText(QRect(text_left, text_top, w-text_left, h-text_top), Qt::AlignCenter, "无操作则跳过"); + QPixmap* img = nullptr; + if(i == chkbox_normal && j == widget_normal) + img = &m_res[res_chkbox_normal]; + else if(i == chkbox_normal && j == widget_hover) + img = &m_res[res_chkbox_hover]; + else if(i == chkbox_selected && j == widget_normal) + img = &m_res[res_chkbox_sel_normal]; + else if(i == chkbox_selected && j == widget_hover) + img = &m_res[res_chkbox_sel_hover]; + + if(img == nullptr) { + qDebug("ERROR: can not load image for check-box."); + img = &m_res[res_chkbox_normal]; + } + ps.drawPixmap(0, chkbox_top, img->width(), img->height(), *img); + ps.drawText(QRect(text_left, text_top, w-text_left, h-text_top), Qt::AlignCenter, "无操作则跳过"); + } } } + // 定位进度条 { // 计算显示时间所需的宽高 font.setFamily("consolas"); @@ -259,32 +281,30 @@ void Bar::_init_imgages() { pp.drawText(m_rc_time_total, Qt::AlignLeft, m_str_total_time); // 定位时间字符串的位置 - m_rc_time_passed.moveTo(15+m_img_btn_play[play_running][widget_normal].width()+10, 18); - m_rc_time_total.moveTo(m_width - 15 - m_rc_time_total.width(), 18); + m_rc_time_passed.moveTo(BAR_PADDING_LEFT+m_img_btn_play[play_running][widget_normal].width()+PADDING_TIME_PROGRESS_BAR, BAR_PADDING_TOP); + m_rc_time_total.moveTo(m_width - BAR_PADDING_RIGHT - m_rc_time_total.width(), BAR_PADDING_TOP); - int prog_width = m_rc_time_total.left() - 10 - 10 - m_rc_time_passed.right();// - m_img_progress_pointer[widget_normal].width(); - int prog_height = max(m_res[res_pbh_left].height(), m_img_progress_pointer->height()); + int prog_width = m_rc_time_total.left() - PADDING_TIME_PROGRESS_BAR - PADDING_TIME_PROGRESS_BAR - m_rc_time_passed.right(); + int prog_height = max(m_res[res_prgbarh_left].height(), m_img_progress_pointer->height()); m_rc_progress = QRect(0, 0, prog_width, prog_height); - m_rc_progress.moveTo(m_rc_time_passed.right() + 10, m_rc_time_passed.height() + (m_rc_time_passed.height() - prog_height)/2); - - qDebug("prog: %d,%d w:%d,h:%d", m_rc_progress.left(), m_rc_progress.top(), prog_width, prog_height); + m_rc_progress.moveTo(m_rc_time_passed.right() + PADDING_TIME_PROGRESS_BAR, m_rc_time_passed.height() + (m_rc_time_passed.height() - prog_height)/2); } // 定位速度按钮 { - int left = m_rc_time_passed.right() + 10; - int top = m_rc_time_passed.bottom() + 8; + int left = m_rc_time_passed.right() + PADDING_TIME_PROGRESS_BAR; + int top = m_rc_time_passed.bottom() + SPEED_BTN_PADDING_TOP; for(int i = 0; i < speed_count; i++) { m_rc_btn_speed[i] = QRect(left, top, m_img_btn_speed[i][widget_normal].width(), m_img_btn_speed[i][widget_normal].height()); - left += m_img_btn_speed[i][widget_normal].width() + 8; + left += m_img_btn_speed[i][widget_normal].width() + SPEED_BTN_PADDING_RIGHT; } } // 定位跳过选项 { - int left = m_rc_time_total.left() - m_rc_skip.width() - 10; - int top = m_rc_time_passed.bottom() + 10; + int left = m_rc_time_total.left() - m_rc_skip.width() - PADDING_TIME_PROGRESS_BAR; + int top = m_rc_time_passed.bottom() + SKIP_PADDING_TOP;//m_rc_btn_speed[0].top() + (m_rc_btn_speed[0].height() - m_rc_skip.height())/2; m_rc_skip.moveTo(left, top); } @@ -363,6 +383,14 @@ void Bar::onMouseMove(int x, int y) { } } + bool skip_hover = m_rc_skip.contains(pt); + if(skip_hover != m_skip_hover) { + m_skip_hover = skip_hover; + m_owner->update(m_rc.left()+m_rc_skip.left(), m_rc.top()+m_rc_skip.top(), m_rc_skip.width(), m_rc_skip.height()); + } + if(skip_hover) + return; + // TODO: more hover detect. } @@ -468,23 +496,23 @@ void Bar::draw(QPainter& painter, const QRect& rc_draw){ QPainter pp(&m_img_progress); // 进度条 - int top = (rc.height() - m_res[res_pbh_left].height())/2; + int top = (rc.height() - m_res[res_prgbarh_left].height())/2; int passed_width = rc.width() * m_percent / 100; // 已经播放的进度条宽度 int remain_width = rc.width() - passed_width; // 剩下未播放的进度条宽度 - if(passed_width >= m_res[res_pbh_left].width()) - pp.drawPixmap(0, top , m_res[res_pbh_left].width(), m_res[res_pbh_left].height(), m_res[res_pbh_left]); + if(passed_width >= m_res[res_prgbarh_left].width()) + pp.drawPixmap(0, top , m_res[res_prgbarh_left].width(), m_res[res_prgbarh_left].height(), m_res[res_prgbarh_left]); if(passed_width > 0) { //pp.drawPixmap(m_res[res_pbh_left].width(), top, passed_width - m_res[res_pbh_left].width(), m_res[res_pbh_mid].height(), m_res[res_pbh_mid]); - if(remain_width > m_res[res_pb_right].width()) - pp.drawPixmap(m_res[res_pbh_left].width(), top, passed_width - m_res[res_pbh_left].width(), m_res[res_pbh_mid].height(), m_res[res_pbh_mid]); + if(remain_width > m_res[res_prgbar_right].width()) + pp.drawPixmap(m_res[res_prgbarh_left].width(), top, passed_width - m_res[res_prgbarh_left].width(), m_res[res_prgbarh_mid].height(), m_res[res_prgbarh_mid]); else - pp.drawPixmap(m_res[res_pbh_left].width(), top, passed_width - m_res[res_pbh_left].width() - m_res[res_pb_right].width(), m_res[res_pbh_mid].height(), m_res[res_pbh_mid]); + pp.drawPixmap(m_res[res_prgbarh_left].width(), top, passed_width - m_res[res_prgbarh_left].width() - m_res[res_prgbar_right].width(), m_res[res_prgbarh_mid].height(), m_res[res_prgbarh_mid]); } if(remain_width > 0) - pp.drawPixmap(passed_width, top, remain_width - m_res[res_pb_right].width(), m_res[res_pb_mid].height(), m_res[res_pb_mid]); - if(remain_width >= m_res[res_pb_right].width()) - pp.drawPixmap(rc.width() - m_res[res_pb_right].width(), top , m_res[res_pb_right].width(), m_res[res_pb_right].height(), m_res[res_pb_right]); + pp.drawPixmap(passed_width, top, remain_width - m_res[res_prgbar_right].width(), m_res[res_prgbar_mid].height(), m_res[res_prgbar_mid]); + if(remain_width >= m_res[res_prgbar_right].width()) + pp.drawPixmap(rc.width() - m_res[res_prgbar_right].width(), top , m_res[res_prgbar_right].width(), m_res[res_prgbar_right].height(), m_res[res_prgbar_right]); // 进度位置指示 int left = passed_width - m_img_progress_pointer->width() / 2; @@ -520,10 +548,13 @@ void Bar::draw(QPainter& painter, const QRect& rc_draw){ int h = min(rc.bottom(), rc_draw.bottom()) - rc.top() - from_y + 1; int to_x = rc.left() + from_x; int to_y = rc.top() + from_y; - if(m_speed_selected == i || m_speed_hover == i) - painter.drawPixmap(to_x, to_y, m_img_btn_speed[i][widget_hover], from_x, from_y, w, h); + + if(m_speed_hover == i) + painter.drawPixmap(to_x, to_y, m_img_btn_speed[i][btnspd_hover], from_x, from_y, w, h); + else if(m_speed_selected == i) + painter.drawPixmap(to_x, to_y, m_img_btn_speed[i][btnspd_sel], from_x, from_y, w, h); else - painter.drawPixmap(to_x, to_y, m_img_btn_speed[i][widget_normal], from_x, from_y, w, h); + painter.drawPixmap(to_x, to_y, m_img_btn_speed[i][btnspd_normal], from_x, from_y, w, h); } } } @@ -532,6 +563,9 @@ void Bar::draw(QPainter& painter, const QRect& rc_draw){ { QRect rc(m_rc_skip); rc.moveTo(m_rc.left()+rc.left(), m_rc.top() + rc.top()); + + // painter.fillRect(rc, QColor(255, 255, 255)); + if(rc_draw.intersects(rc)) { int from_x = max(rc_draw.left(), rc.left()) - rc.left(); int from_y = max(rc_draw.top(), rc.top()) - rc.top(); @@ -540,10 +574,18 @@ void Bar::draw(QPainter& painter, const QRect& rc_draw){ int to_x = rc.left() + from_x; int to_y = rc.top() + from_y; //qDebug("skip (%d,%d), (%d,%d)/(%d,%d)", to_x, to_y, from_x, from_y, w, h); - if(m_skip_selected) - painter.drawPixmap(to_x, to_y, m_img_skip[widget_hover], from_x, from_y, w, h); - else - painter.drawPixmap(to_x, to_y, m_img_skip[widget_normal], from_x, from_y, w, h); + if(m_skip_selected) { + if(m_skip_hover) + painter.drawPixmap(to_x, to_y, m_img_skip[chkbox_selected][widget_hover], from_x, from_y, w, h); + else + painter.drawPixmap(to_x, to_y, m_img_skip[chkbox_selected][widget_normal], from_x, from_y, w, h); + } + else { + if(m_skip_hover) + painter.drawPixmap(to_x, to_y, m_img_skip[chkbox_normal][widget_hover], from_x, from_y, w, h); + else + painter.drawPixmap(to_x, to_y, m_img_skip[chkbox_normal][widget_normal], from_x, from_y, w, h); + } } } } diff --git a/client/tp-player/bar.h b/client/tp-player/bar.h index 3cb8144..a87a388 100644 --- a/client/tp-player/bar.h +++ b/client/tp-player/bar.h @@ -6,51 +6,50 @@ #include typedef enum { - res_bg_left = 0, // 背景左侧 - res_bg_mid, // 背景中间,拉伸填充 - res_bg_right, // 背景右侧 - res_bs_left, // 速度按钮(未选中)左侧 - res_bs_mid, // 速度按钮(未选中)中间,拉伸填充 - res_bs_right, // 速度按钮(未选中)右侧 - res_bsh_left, // 速度按钮(选中)左侧 - res_bsh_mid, // 速度按钮(选中)中间,拉伸填充 - res_bsh_right, // 速度按钮(选中)右侧 - res_pbh_left, // 进度条(已经过)左侧 - res_pbh_mid, // 进度条(已经过)中间,拉伸填充 - res_pb_mid, // 进度条(未到达)中间,拉伸填充 - res_pb_right, // 进度条(未到达)右侧 -// res_pp, // 进度条上的指示点,未选中 -// res_pph, // 进度条上的指示点,选中高亮 - res_cb, // 复选框,未选中 - res_cbh, // 复选框,已勾选 -// res_play, -// res_play_hover, -// res_pause, -// res_pause_hover, + res_bg_left = 0, // 背景 + res_bg_mid, + res_bg_right, + res_btn_normal_left, // 按钮(速度选择),普通状态 + res_btn_normal_mid, + res_btn_normal_right, + res_btn_sel_left, // 按钮(速度选择),已选中 + res_btn_sel_mid, + res_btn_sel_right, + res_btn_hover_left, // 按钮(速度选择),鼠标滑过 + res_btn_hover_mid, + res_btn_hover_right, + + res_prgbarh_left, // 进度条(已经过)左侧 + res_prgbarh_mid, // 进度条(已经过)中间,拉伸填充 + res_prgbar_mid, // 进度条(未到达)中间,拉伸填充 + res_prgbar_right, // 进度条(未到达)右侧 + res_chkbox_normal, // 复选框 + res_chkbox_hover, + res_chkbox_sel_normal, + res_chkbox_sel_hover, res__max }RES_ID; -typedef enum { - widget_normal = 0, - widget_hover, - widget__max -}WIDGET_STAT; +//typedef enum { +// widget_normal = 0, +// widget_hover, +// widget__max +//}WIDGET_STAT; -typedef enum { - play_running = 0, - play_paused, - play__max -}PLAY_STAT; +#define widget_normal 0 +#define widget_hover 1 +#define widget_state_count 2 //typedef enum { -// speed_1x = 0, -// speed_2x, -// speed_4x, -// speed_8x, -// speed_16x, -// speed__max, -//}SPEED; +// play_running = 0, +// play_paused, +// play__max +//}PLAY_STAT; + +#define play_running 0 +#define play_paused 1 +#define play_state_count 2 #define speed_1x 0 #define speed_2x 1 @@ -59,6 +58,15 @@ typedef enum { #define speed_16x 4 #define speed_count 5 +#define btnspd_normal 0 +#define btnspd_sel 1 +#define btnspd_hover 2 +#define btnspd_state_count 3 + +#define chkbox_normal 0 +#define chkbox_selected 1 +#define chkbox_state_count 2 + class Bar { public: Bar(); @@ -93,7 +101,7 @@ private: // 从资源中加载的原始图像 QPixmap m_res[res__max]; - QPixmap m_img_progress_pointer[widget__max]; + QPixmap m_img_progress_pointer[widget_state_count]; int m_width; int m_height; @@ -113,20 +121,20 @@ private: // 合成的图像 QPixmap m_img_bg; - QPixmap m_img_btn_play[play__max][widget__max]; - QPixmap m_img_btn_speed[speed_count][widget__max]; + QPixmap m_img_btn_play[play_state_count][widget_state_count]; + QPixmap m_img_btn_speed[speed_count][btnspd_state_count]; QPixmap m_img_progress; - QPixmap m_img_skip[widget__max]; + QPixmap m_img_skip[chkbox_state_count][widget_state_count]; QPixmap m_img_time_passed; QPixmap m_img_time_total; // 各种状态 - bool m_play_hover; bool m_playing; // 0=play, 2=pause + bool m_play_hover; int m_speed_selected; int m_speed_hover; // speed__max=no-hover bool m_skip_selected; - //bool m_skip_hover; + bool m_skip_hover; }; #endif // BAR_H diff --git a/client/tp-player/mainwindow.cpp b/client/tp-player/mainwindow.cpp index a56c53d..ae16cdb 100644 --- a/client/tp-player/mainwindow.cpp +++ b/client/tp-player/mainwindow.cpp @@ -72,8 +72,6 @@ MainWindow::MainWindow(QWidget *parent) : m_bar_shown = false; memset(&m_pt, 0, sizeof(TS_RECORD_RDP_POINTER)); - qDebug() << m_pt_normal.width() << "x" << m_pt_normal.height(); - ui->setupUi(this); ui->centralWidget->setMouseTracking(true); @@ -95,8 +93,10 @@ MainWindow::MainWindow(QWidget *parent) : setWindowFlags(windowFlags()&~Qt::WindowMaximizeButtonHint); // 禁止最大化按钮 setFixedSize(m_default_bg.width(), m_default_bg.height()); // 禁止拖动窗口大小 - if(!m_bar.init(this)) + if(!m_bar.init(this)) { + qDebug("bar init failed."); return; + } connect(&m_thr_play, SIGNAL(signal_update_data(update_data*)), this, SLOT(on_update_data(update_data*))); } From 9bc0fcb22560223d1ee9a811e3e1f025a2626e1f Mon Sep 17 00:00:00 2001 From: Apex Liu Date: Tue, 10 Sep 2019 02:04:23 +0800 Subject: [PATCH 17/44] =?UTF-8?q?=E6=B5=AE=E5=8A=A8=E7=AA=97=E6=B7=A1?= =?UTF-8?q?=E5=85=A5=E6=B7=A1=E5=87=BA=E5=8A=9F=E8=83=BD=E5=AE=8C=E6=88=90?= =?UTF-8?q?=EF=BC=9B=E6=92=AD=E6=94=BE=E3=80=81=E6=9A=82=E5=81=9C=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=AE=8C=E6=88=90=EF=BC=9B=E4=BB=8E=E5=A4=B4=E6=92=AD?= =?UTF-8?q?=E6=94=BE=E5=8A=9F=E8=83=BD=E5=AE=8C=E6=88=90=EF=BC=9B=E5=80=8D?= =?UTF-8?q?=E9=80=9F=E6=92=AD=E6=94=BE=E5=8A=9F=E8=83=BD=E5=AE=8C=E6=88=90?= =?UTF-8?q?=EF=BC=9B=E5=8E=BB=E9=99=A416=E5=80=8D=E9=80=9F=EF=BC=8C?= =?UTF-8?q?=E6=B2=A1=E6=9C=89=E5=A4=AA=E5=A4=A7=E6=84=8F=E4=B9=89=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/tp-player/bar.cpp | 81 +++++++++++--- client/tp-player/bar.h | 12 ++- client/tp-player/mainwindow.cpp | 181 +++++++++++++++++++++++++++----- client/tp-player/mainwindow.h | 29 ++++- client/tp-player/thr_play.cpp | 31 ++++-- client/tp-player/thr_play.h | 6 +- 6 files changed, 284 insertions(+), 56 deletions(-) diff --git a/client/tp-player/bar.cpp b/client/tp-player/bar.cpp index 6d77bf4..39b052f 100644 --- a/client/tp-player/bar.cpp +++ b/client/tp-player/bar.cpp @@ -1,10 +1,11 @@ #include "bar.h" #include #include +#include "mainwindow.h" #define FONT_SIZE_DEFAULT 12 -#define TIME_STR_PIXEL_SIZE 16 +#define FONT_SIZE_TIME 14 #define TEXT_COLOR QColor(255,255,255,153) #define SPEED_BTN_WIDTH 42 #define CHKBOX_RIGHT_PADDING 6 @@ -58,7 +59,6 @@ static SPEED_MAP speed[speed_count] = { {speed_2x, "2x"}, {speed_4x, "4x"}, {speed_8x, "8x"}, - {speed_16x, "16x"} }; static inline int min(int a, int b){ @@ -92,7 +92,7 @@ Bar::~Bar() { } -bool Bar::init(QWidget* owner) { +bool Bar::init(MainWindow* owner) { m_owner = owner; // 加载所需的图像资源 @@ -140,6 +140,9 @@ void Bar::start(uint32_t total_ms, int width) { void Bar::end() { if(m_passed_ms != m_total_ms) update_passed_time(m_total_ms); + + m_playing = false; + m_owner->update(m_rc.left()+m_rc_btn_play.left(), m_rc.top()+m_rc_btn_play.top(), m_rc_btn_play.width(), m_rc_btn_play.height()); } void Bar::_init_imgages() { @@ -261,7 +264,7 @@ void Bar::_init_imgages() { // 计算显示时间所需的宽高 font.setFamily("consolas"); font.setBold(true); - font.setPixelSize(TIME_STR_PIXEL_SIZE); + font.setPixelSize(FONT_SIZE_TIME); pp.setFont(font); { QFontMetrics fm = pp.fontMetrics(); @@ -276,7 +279,7 @@ void Bar::_init_imgages() { QFont font = pp.font(); font.setFamily("consolas"); font.setBold(true); - font.setPixelSize(TIME_STR_PIXEL_SIZE); + font.setPixelSize(FONT_SIZE_TIME); pp.setFont(font); pp.drawText(m_rc_time_total, Qt::AlignLeft, m_str_total_time); @@ -287,7 +290,7 @@ void Bar::_init_imgages() { int prog_width = m_rc_time_total.left() - PADDING_TIME_PROGRESS_BAR - PADDING_TIME_PROGRESS_BAR - m_rc_time_passed.right(); int prog_height = max(m_res[res_prgbarh_left].height(), m_img_progress_pointer->height()); m_rc_progress = QRect(0, 0, prog_width, prog_height); - m_rc_progress.moveTo(m_rc_time_passed.right() + PADDING_TIME_PROGRESS_BAR, m_rc_time_passed.height() + (m_rc_time_passed.height() - prog_height)/2); + m_rc_progress.moveTo(m_rc_time_passed.right() + PADDING_TIME_PROGRESS_BAR, m_rc_time_passed.top() + (m_rc_time_passed.height() - prog_height)/2); } @@ -394,6 +397,60 @@ void Bar::onMouseMove(int x, int y) { // TODO: more hover detect. } +void Bar::onMousePress(int x, int y) { + // 映射鼠标坐标点到本浮动窗内部的相对位置 + QPoint pt(x-m_rc.left(), y-m_rc.top()); + + if(m_rc_btn_play.contains(pt)) { + if(m_playing) + m_owner->pause(); + else + m_owner->resume(); + + m_playing = !m_playing; + m_owner->update(m_rc.left()+m_rc_btn_play.left(), m_rc.top()+m_rc_btn_play.top(), m_rc_btn_play.width(), m_rc_btn_play.height()); + + return; + } + + int speed_sel = speed_count; + for(int i = 0; i < speed_count; ++i) { + if(m_rc_btn_speed[i].contains(pt)) { + speed_sel = i; + break; + } + } + if(m_speed_selected != speed_sel && speed_sel != speed_count) { + int old_sel = m_speed_selected; + m_speed_selected = speed_sel; + m_owner->speed(get_speed()); + m_owner->update(m_rc.left()+m_rc_btn_speed[old_sel].left(), m_rc.top()+m_rc_btn_speed[old_sel].top(), m_rc_btn_speed[old_sel].width(), m_rc_btn_speed[old_sel].height()); + m_owner->update(m_rc.left()+m_rc_btn_speed[m_speed_hover].left(), m_rc.top()+m_rc_btn_speed[m_speed_hover].top(), m_rc_btn_speed[m_speed_hover].width(), m_rc_btn_speed[m_speed_hover].height()); + return; + } + + if(m_rc_skip.contains(pt)) { + m_skip_selected = !m_skip_selected; + m_owner->update(m_rc.left()+m_rc_skip.left(), m_rc.top()+m_rc_skip.top(), m_rc_skip.width(), m_rc_skip.height()); + return; + } +} + +int Bar::get_speed() { + switch (m_speed_selected) { + case speed_1x: + return 1; + case speed_2x: + return 2; + case speed_4x: + return 4; + case speed_8x: + return 8; + default: + return 1; + } +} + void Bar::draw(QPainter& painter, const QRect& rc_draw){ if(!m_width) return; @@ -426,15 +483,15 @@ void Bar::draw(QPainter& painter, const QRect& rc_draw){ int to_x = rc.left() + from_x; int to_y = rc.top() + from_y; if(m_playing){ - if(m_play_hover) - painter.drawPixmap(to_x, to_y, m_img_btn_play[play_running][widget_hover], from_x, from_y, w, h); - else - painter.drawPixmap(to_x, to_y, m_img_btn_play[play_running][widget_normal], from_x, from_y, w, h); - } else { if(m_play_hover) painter.drawPixmap(to_x, to_y, m_img_btn_play[play_paused][widget_hover], from_x, from_y, w, h); else painter.drawPixmap(to_x, to_y, m_img_btn_play[play_paused][widget_normal], from_x, from_y, w, h); + } else { + if(m_play_hover) + painter.drawPixmap(to_x, to_y, m_img_btn_play[play_running][widget_hover], from_x, from_y, w, h); + else + painter.drawPixmap(to_x, to_y, m_img_btn_play[play_running][widget_normal], from_x, from_y, w, h); } } } @@ -452,7 +509,7 @@ void Bar::draw(QPainter& painter, const QRect& rc_draw){ QFont font = pp.font(); font.setFamily("consolas"); font.setBold(true); - font.setPixelSize(TIME_STR_PIXEL_SIZE); + font.setPixelSize(FONT_SIZE_TIME); pp.setFont(font); pp.drawText(QRect(0,0,m_rc_time_passed.width(), m_rc_time_passed.height()), Qt::AlignRight, m_str_passed_time); diff --git a/client/tp-player/bar.h b/client/tp-player/bar.h index a87a388..8ff8804 100644 --- a/client/tp-player/bar.h +++ b/client/tp-player/bar.h @@ -55,8 +55,7 @@ typedef enum { #define speed_2x 1 #define speed_4x 2 #define speed_8x 3 -#define speed_16x 4 -#define speed_count 5 +#define speed_count 4 #define btnspd_normal 0 #define btnspd_sel 1 @@ -67,27 +66,32 @@ typedef enum { #define chkbox_selected 1 #define chkbox_state_count 2 +class MainWindow; + class Bar { public: Bar(); ~Bar(); - bool init(QWidget* owner); + bool init(MainWindow* owner); void start(uint32_t total_ms, int width); void end(); void draw(QPainter& painter, const QRect& rc); void update_passed_time(uint32_t ms); + int get_speed(); + QRect rc(){return m_rc;} void onMouseMove(int x, int y); + void onMousePress(int x, int y); private: void _init_imgages(); void _ms_to_str(uint32_t ms, QString& str); private: - QWidget* m_owner; + MainWindow* m_owner; uint32_t m_total_ms; // 录像的总时长 uint32_t m_passed_ms; // 已经播放了的时长 diff --git a/client/tp-player/mainwindow.cpp b/client/tp-player/mainwindow.cpp index ae16cdb..0dbd413 100644 --- a/client/tp-player/mainwindow.cpp +++ b/client/tp-player/mainwindow.cpp @@ -70,15 +70,19 @@ MainWindow::MainWindow(QWidget *parent) : m_shown = false; m_show_default = true; m_bar_shown = false; + m_bar_fade_in = false; + m_bar_fading = false; + m_bar_opacity = 1.0; memset(&m_pt, 0, sizeof(TS_RECORD_RDP_POINTER)); + m_thr_play = nullptr; + m_play_state = PLAY_STATE_UNKNOWN; + ui->setupUi(this); ui->centralWidget->setMouseTracking(true); setMouseTracking(true); - //qRegisterMetaType("update_data"); - // frame-less window. //#ifdef __APPLE__ // setWindowFlags(Qt::FramelessWindowHint | Qt::MSWindowsFixedSizeDialogHint | Qt::Window); @@ -98,16 +102,47 @@ MainWindow::MainWindow(QWidget *parent) : return; } - connect(&m_thr_play, SIGNAL(signal_update_data(update_data*)), this, SLOT(on_update_data(update_data*))); +// connect(&m_thr_play, SIGNAL(signal_update_data(update_data*)), this, SLOT(_do_update_data(update_data*))); + connect(&m_timer_bar_fade, SIGNAL(timeout()), this, SLOT(_do_bar_fade())); + connect(&m_timer_bar_delay_hide, SIGNAL(timeout()), this, SLOT(_do_bar_delay_hide())); } MainWindow::~MainWindow() { - m_thr_play.stop(); - m_thr_play.wait(); + if(m_thr_play) { + m_thr_play->stop(); + m_thr_play->wait(); + + disconnect(m_thr_play, SIGNAL(signal_update_data(update_data*)), this, SLOT(_do_update_data(update_data*))); + + delete m_thr_play; + m_thr_play = nullptr; + } delete ui; } +void MainWindow::_start_play_thread() { + if(m_thr_play) { + m_thr_play->stop(); + m_thr_play->wait(); + + disconnect(m_thr_play, SIGNAL(signal_update_data(update_data*)), this, SLOT(_do_update_data(update_data*))); + + delete m_thr_play; + m_thr_play = nullptr; + } + + m_thr_play = new ThreadPlay; + connect(m_thr_play, SIGNAL(signal_update_data(update_data*)), this, SLOT(_do_update_data(update_data*))); + m_thr_play->speed(m_bar.get_speed()); + m_thr_play->start(); +} + +void MainWindow::speed(int s) { + if(m_thr_play) + m_thr_play->speed(s); +} + void MainWindow::paintEvent(QPaintEvent *e) { QPainter painter(this); @@ -125,17 +160,40 @@ void MainWindow::paintEvent(QPaintEvent *e) } // 绘制浮动控制窗 - if(m_bar_shown) + if(m_bar_fading) { + painter.setOpacity(m_bar_opacity); m_bar.draw(painter, e->rect()); + } + else if(m_bar_shown) { + m_bar.draw(painter, e->rect()); + } } if(!m_shown) { m_shown = true; - m_thr_play.start(); + //m_thr_play.start(); + _start_play_thread(); } } -void MainWindow::on_update_data(update_data* dat) { +void MainWindow::pause() { + if(m_play_state != PLAY_STATE_RUNNING) + return; + m_thr_play->pause(); + m_play_state = PLAY_STATE_PAUSE; +} + +void MainWindow::resume() { + if(m_play_state == PLAY_STATE_PAUSE) + m_thr_play->resume(); + else if(m_play_state == PLAY_STATE_STOP) + //m_thr_play->start(); + _start_play_thread(); + + m_play_state = PLAY_STATE_RUNNING; +} + +void MainWindow::_do_update_data(update_data* dat) { if(!dat) return; @@ -197,6 +255,7 @@ void MainWindow::on_update_data(update_data* dat) { return; } + // 这是播放开始时收到的第一个数据包 if(dat->data_type() == TYPE_HEADER_INFO) { if(dat->data_len() != sizeof(TS_RECORD_HEADER)) { qDebug() << "invalid record header."; @@ -206,26 +265,38 @@ void MainWindow::on_update_data(update_data* dat) { qDebug() << "resize (" << m_rec_hdr.basic.width << "," << m_rec_hdr.basic.height << ")"; if(m_rec_hdr.basic.width > 0 && m_rec_hdr.basic.height > 0) { - m_canvas = QPixmap(m_rec_hdr.basic.width, m_rec_hdr.basic.height); + + if(m_canvas.width() != m_rec_hdr.basic.width && m_canvas.height() != m_rec_hdr.basic.height) { + m_canvas = QPixmap(m_rec_hdr.basic.width, m_rec_hdr.basic.height); + + //m_win_board_w = frameGeometry().width() - geometry().width(); + //m_win_board_h = frameGeometry().height() - geometry().height(); + + QDesktopWidget *desktop = QApplication::desktop(); // =qApp->desktop();也可以 + qDebug("desktop w:%d,h:%d, this w:%d,h:%d", desktop->width(), desktop->height(), width(), height()); + //move((desktop->width() - this->width())/2, (desktop->height() - this->height())/2); + move(10, (desktop->height() - m_rec_hdr.basic.height)/2); + + //setFixedSize(m_rec_hdr.basic.width + m_win_board_w, m_rec_hdr.basic.height + m_win_board_h); + //resize(m_rec_hdr.basic.width + m_win_board_w, m_rec_hdr.basic.height + m_win_board_h); + //resize(m_rec_hdr.basic.width, m_rec_hdr.basic.height); + setFixedSize(m_rec_hdr.basic.width, m_rec_hdr.basic.height); + } + m_canvas.fill(QColor(38, 73, 111)); - //m_win_board_w = frameGeometry().width() - geometry().width(); - //m_win_board_h = frameGeometry().height() - geometry().height(); - - QDesktopWidget *desktop = QApplication::desktop(); // =qApp->desktop();也可以 - qDebug("desktop w:%d,h:%d, this w:%d,h:%d", desktop->width(), desktop->height(), width(), height()); - //move((desktop->width() - this->width())/2, (desktop->height() - this->height())/2); - move(10, (desktop->height() - m_rec_hdr.basic.height)/2); - - //setFixedSize(m_rec_hdr.basic.width + m_win_board_w, m_rec_hdr.basic.height + m_win_board_h); - //resize(m_rec_hdr.basic.width + m_win_board_w, m_rec_hdr.basic.height + m_win_board_h); - //resize(m_rec_hdr.basic.width, m_rec_hdr.basic.height); - setFixedSize(m_rec_hdr.basic.width, m_rec_hdr.basic.height); - m_show_default = false; repaint(); m_bar.start(m_rec_hdr.info.time_ms, 640); + m_bar_shown = true; + m_play_state = PLAY_STATE_RUNNING; + + update(m_bar.rc()); + + m_bar_fade_in = false; + m_bar_fading = true; + m_timer_bar_delay_hide.start(2000); } QString title; @@ -242,28 +313,80 @@ void MainWindow::on_update_data(update_data* dat) { if(dat->data_type() == TYPE_END) { m_bar.end(); + m_play_state = PLAY_STATE_STOP; return; } } +void MainWindow::_do_bar_delay_hide() { + m_bar_fading = true; + m_timer_bar_delay_hide.stop(); + m_timer_bar_fade.stop(); + m_timer_bar_fade.start(50); +} + +void MainWindow::_do_bar_fade() { + if(m_bar_fade_in) { + if(m_bar_opacity < 1.0) + m_bar_opacity += 0.3; + if(m_bar_opacity >= 1.0) { + m_bar_opacity = 1.0; + m_bar_shown = true; + m_bar_fading = false; + m_timer_bar_fade.stop(); + } + } + else { + if(m_bar_opacity > 0.0) + m_bar_opacity -= 0.2; + if(m_bar_opacity <= 0.0) { + m_bar_opacity = 0.0; + m_bar_shown = false; + m_bar_fading = false; + m_timer_bar_fade.stop(); + } + } + + update(m_bar.rc()); +} + void MainWindow::mouseMoveEvent(QMouseEvent *e) { if(!m_show_default) { QRect rc = m_bar.rc(); if(e->y() > rc.top() - 20 && e->y() < rc.bottom() + 20) { - if(!m_bar_shown) { - m_bar_shown = true; - update(rc); + if((!m_bar_shown && !m_bar_fading) || (m_bar_fading && !m_bar_fade_in)) { + m_bar_fade_in = true; + m_bar_fading = true; + + m_timer_bar_delay_hide.stop(); + m_timer_bar_fade.stop(); + m_timer_bar_fade.start(50); } - if(rc.contains(QPoint(e->x(), e->y()))) + if(rc.contains(e->pos())) m_bar.onMouseMove(e->x(), e->y()); } else { - if(m_bar_shown) { - m_bar_shown = false; - update(rc); + if((m_bar_shown && !m_bar_fading) || (m_bar_fading && m_bar_fade_in)) { + m_bar_fade_in = false; + m_bar_fading = true; + m_timer_bar_fade.stop(); + m_timer_bar_delay_hide.stop(); + + if(m_bar_opacity != 1.0) + m_timer_bar_fade.start(50); + else + m_timer_bar_delay_hide.start(1000); } } } } +void MainWindow::mousePressEvent(QMouseEvent *e) { + if(!m_show_default) { + QRect rc = m_bar.rc(); + if(rc.contains(e->pos())) { + m_bar.onMousePress(e->x(), e->y()); + } + } +} diff --git a/client/tp-player/mainwindow.h b/client/tp-player/mainwindow.h index 1f3bc6d..03aac59 100644 --- a/client/tp-player/mainwindow.h +++ b/client/tp-player/mainwindow.h @@ -2,11 +2,17 @@ #define MAINWINDOW_H #include +#include #include "bar.h" #include "thr_play.h" #include "update_data.h" #include "record_format.h" +#define PLAY_STATE_UNKNOWN 0 +#define PLAY_STATE_RUNNING 1 +#define PLAY_STATE_PAUSE 2 +#define PLAY_STATE_STOP 3 + namespace Ui { class MainWindow; } @@ -19,22 +25,31 @@ public: explicit MainWindow(QWidget *parent = nullptr); ~MainWindow(); + void pause(); + void resume(); + void restart(); + void speed(int s); + private: void paintEvent(QPaintEvent *e); void mouseMoveEvent(QMouseEvent *e); + void mousePressEvent(QMouseEvent *e); + + void _start_play_thread(); private slots: - void on_update_data(update_data*); + void _do_update_data(update_data*); + void _do_bar_fade(); + void _do_bar_delay_hide(); private: Ui::MainWindow *ui; - //QImage m_bg; bool m_shown; bool m_show_default; bool m_bar_shown; QPixmap m_default_bg; - ThreadPlay m_thr_play; + ThreadPlay* m_thr_play; QPixmap m_canvas; @@ -44,6 +59,14 @@ private: QPixmap m_pt_normal; TS_RECORD_RDP_POINTER m_pt; + + QTimer m_timer_bar_fade; + QTimer m_timer_bar_delay_hide; + bool m_bar_fade_in; + bool m_bar_fading; + qreal m_bar_opacity; + + int m_play_state; }; #endif // MAINWINDOW_H diff --git a/client/tp-player/thr_play.cpp b/client/tp-player/thr_play.cpp index c083e8b..9cc820f 100644 --- a/client/tp-player/thr_play.cpp +++ b/client/tp-player/thr_play.cpp @@ -11,6 +11,8 @@ static QString REPLAY_PATH = "E:\\work\\tp4a\\teleport\\server\\share\\replay\\r ThreadPlay::ThreadPlay() { m_need_stop = false; + m_need_pause = false; + m_speed = 2; } void ThreadPlay::stop() { @@ -23,6 +25,7 @@ void ThreadPlay::run() { qint64 read_len = 0; uint32_t total_pkg = 0; + uint32_t total_ms = 0; QString hdr_filename(REPLAY_PATH); hdr_filename += "tp-rdp.tpr"; @@ -46,6 +49,7 @@ void ThreadPlay::run() { TS_RECORD_HEADER* hdr = (TS_RECORD_HEADER*)dat->data_buf(); total_pkg = hdr->info.packages; + total_ms = hdr->info.time_ms; emit signal_update_data(dat); } @@ -72,6 +76,12 @@ void ThreadPlay::run() { break; } + if(m_need_pause) { + msleep(50); + time_begin += 50; + continue; + } + TS_RECORD_PKG pkg; read_len = f_dat.read((char*)(&pkg), sizeof(pkg)); if(read_len != sizeof(TS_RECORD_PKG)) { @@ -90,18 +100,18 @@ void ThreadPlay::run() { return; } - time_pass = (uint32_t)(QDateTime::currentMSecsSinceEpoch() - time_begin); - if(time_pass - time_last_pass > 1000) { + time_pass = (uint32_t)(QDateTime::currentMSecsSinceEpoch() - time_begin) * m_speed; + if(time_pass > total_ms) + time_pass = total_ms; + if(time_pass - time_last_pass > 200) { update_data* _passed_ms = new update_data; _passed_ms->data_type(TYPE_TIMER); _passed_ms->passed_ms(time_pass); -// qDebug("--- 1 %d", time_pass); emit signal_update_data(_passed_ms); time_last_pass = time_pass; } if(time_pass >= pkg.time_ms) { - //time_pass = pkg.time_ms; emit signal_update_data(dat); continue; } @@ -110,6 +120,12 @@ void ThreadPlay::run() { uint32_t time_wait = pkg.time_ms - time_pass; uint32_t wait_this_time = 0; for(;;) { + if(m_need_pause) { + msleep(50); + time_begin += 50; + continue; + } + wait_this_time = time_wait; if(wait_this_time > 10) wait_this_time = 10; @@ -121,12 +137,13 @@ void ThreadPlay::run() { msleep(wait_this_time); - uint32_t _time_pass = (uint32_t)(QDateTime::currentMSecsSinceEpoch() - time_begin); - if(_time_pass - time_last_pass > 1000) { + uint32_t _time_pass = (uint32_t)(QDateTime::currentMSecsSinceEpoch() - time_begin) * m_speed; + if(_time_pass > total_ms) + _time_pass = total_ms; + if(_time_pass - time_last_pass > 200) { update_data* _passed_ms = new update_data; _passed_ms->data_type(TYPE_TIMER); _passed_ms->passed_ms(_time_pass); -// qDebug("--- 2 %d", _time_pass); emit signal_update_data(_passed_ms); time_last_pass = _time_pass; } diff --git a/client/tp-player/thr_play.h b/client/tp-player/thr_play.h index 3c3bb2d..c875762 100644 --- a/client/tp-player/thr_play.h +++ b/client/tp-player/thr_play.h @@ -13,13 +13,17 @@ public: virtual void run(); void stop(); - + void pause() {m_need_pause = true;} + void resume() {m_need_pause = false;} + void speed(int s) {if(s >= 1 && s <= 16) m_speed = s;} signals: void signal_update_data(update_data*); private: bool m_need_stop; + bool m_need_pause; + int m_speed; }; #endif // THR_PLAY_H From 6f95664ca1921730474a6c8ec26b4e98bbce72fc Mon Sep 17 00:00:00 2001 From: Apex Liu Date: Tue, 10 Sep 2019 02:39:08 +0800 Subject: [PATCH 18/44] .temp. --- client/tp-player/main.cpp | 3 +++ client/tp-player/thr_play.cpp | 18 +++++++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/client/tp-player/main.cpp b/client/tp-player/main.cpp index 23849ce..b7b993e 100644 --- a/client/tp-player/main.cpp +++ b/client/tp-player/main.cpp @@ -1,6 +1,9 @@ #include "mainwindow.h" #include +// 编译出来的可执行程序复制到单独目录,然后执行 windeployqt 应用程序文件名 +// 即可自动将依赖的动态库等复制到此目录中。有些文件是多余的,可以酌情删除。 + int main(int argc, char *argv[]) { //#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) diff --git a/client/tp-player/thr_play.cpp b/client/tp-player/thr_play.cpp index 9cc820f..c40e7c6 100644 --- a/client/tp-player/thr_play.cpp +++ b/client/tp-player/thr_play.cpp @@ -1,13 +1,12 @@ -#include +#include +#include #include +#include #include #include "thr_play.h" #include "record_format.h" -static QString REPLAY_PATH = "E:\\work\\tp4a\\teleport\\server\\share\\replay\\rdp\\000000197\\"; - - ThreadPlay::ThreadPlay() { m_need_stop = false; @@ -23,11 +22,16 @@ void ThreadPlay::run() { sleep(1); + + QString currentPath = QCoreApplication::applicationDirPath() + "/testdata/"; + qint64 read_len = 0; uint32_t total_pkg = 0; uint32_t total_ms = 0; - QString hdr_filename(REPLAY_PATH); + // 加载录像基本信息数据 + + QString hdr_filename(currentPath); hdr_filename += "tp-rdp.tpr"; QFile f_hdr(hdr_filename); @@ -54,9 +58,9 @@ void ThreadPlay::run() { emit signal_update_data(dat); } + // 加载录像文件数据 - - QString dat_filename(REPLAY_PATH); + QString dat_filename(currentPath); dat_filename += "tp-rdp.dat"; QFile f_dat(dat_filename); From e4b30658afbd68a3e1a24f5e4e94020bb369138a Mon Sep 17 00:00:00 2001 From: horizon Date: Thu, 12 Sep 2019 20:08:35 +0800 Subject: [PATCH 19/44] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E6=9C=89=E6=95=88=E6=9C=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../www/teleport/static/js/user/user-list.js | 24 ++++++++++++++- server/www/teleport/view/user/user-list.mako | 20 +++++++++++++ .../teleport/webroot/app/controller/user.py | 11 ++++++- server/www/teleport/webroot/app/model/user.py | 29 ++++++++++++------- 4 files changed, 71 insertions(+), 13 deletions(-) diff --git a/server/www/teleport/static/js/user/user-list.js b/server/www/teleport/static/js/user/user-list.js index cd8d6ad..2208c9e 100755 --- a/server/www/teleport/static/js/user/user-list.js +++ b/server/www/teleport/static/js/user/user-list.js @@ -744,6 +744,8 @@ $app.create_dlg_edit_user = function () { dlg.field_mobile = ''; dlg.field_qq = ''; dlg.field_wechat = ''; + dlg.field_vaild_from = ''; + dlg.field_vaild_to = ''; dlg.field_desc = ''; dlg.dom = { @@ -756,6 +758,8 @@ $app.create_dlg_edit_user = function () { , edit_mobile: $('#edit-user-mobile') , edit_qq: $('#edit-user-qq') , edit_wechat: $('#edit-user-wechat') + , edit_valid_from: $('#edit-user-valid-from') + , edit_valid_to: $('#edit-user-valid-to') , edit_desc: $('#edit-user-desc') , msg: $('#edit-user-message') , btn_save: $('#btn-edit-user-save') @@ -778,6 +782,8 @@ $app.create_dlg_edit_user = function () { _ret.push('

  • ' + role.name + '
  • '); }); _ret.push(''); + dlg.dom.edit_valid_from.datetimepicker({format: "yyyy-mm-dd h:ii", autoclose: 1, todayHighlight: 1}); + dlg.dom.edit_valid_to.datetimepicker({format: "yyyy-mm-dd h:ii", autoclose: 1, todayHighlight: 1}); dlg.dom.select_role.after($(_ret.join(''))); dlg.dom.selected_role = $('#' + dlg.dom_id + ' span[data-selected-role]'); @@ -869,7 +875,7 @@ $app.create_dlg_edit_user = function () { var role_name = '选择角色'; dlg.field_role = -1; dlg.field_auth_type = 0; - + // dlg.dom.btn_auth_use_sys_config.removeClass('tp-selected'); // dlg.dom.btn_auth_username_password.removeClass('tp-selected'); // dlg.dom.btn_auth_username_password_captcha.removeClass('tp-selected'); @@ -887,6 +893,8 @@ $app.create_dlg_edit_user = function () { dlg.dom.edit_qq.val(''); dlg.dom.edit_wechat.val(''); dlg.dom.edit_desc.val(''); + dlg.dom.edit_valid_from.find('input').val(''); + dlg.dom.edit_valid_to.find('input').val(''); } else { dlg.field_id = user.id; dlg.field_auth_type = user.auth_type; @@ -905,6 +913,16 @@ $app.create_dlg_edit_user = function () { dlg.dom.edit_qq.val(user.qq); dlg.dom.edit_wechat.val(user.wechat); dlg.dom.edit_desc.val(user.desc); + if (user.valid_from == 0 ) { + dlg.dom.edit_valid_from.find('input').val(''); + }else{ + dlg.dom.edit_valid_from.find('input').val(tp_format_datetime(tp_utc2local(user.valid_from), 'yyyy-MM-dd HH:mm')); + } + if (user.valid_to == 0 ) { + dlg.dom.edit_valid_to.find('input').val(''); + }else{ + dlg.dom.edit_valid_to.find('input').val(tp_format_datetime(tp_utc2local(user.valid_to), 'yyyy-MM-dd HH:mm')); + } } dlg.dom.selected_role.text(role_name); @@ -943,6 +961,8 @@ $app.create_dlg_edit_user = function () { dlg.field_mobile = dlg.dom.edit_mobile.val(); dlg.field_qq = dlg.dom.edit_qq.val(); dlg.field_wechat = dlg.dom.edit_wechat.val(); + dlg.field_valid_from = dlg.dom.edit_valid_from.find('input').val(); + dlg.field_valid_to = dlg.dom.edit_valid_to.find('input').val(); dlg.field_desc = dlg.dom.edit_desc.val(); if (dlg.field_role === -1) { @@ -1002,6 +1022,8 @@ $app.create_dlg_edit_user = function () { , mobile: dlg.field_mobile , qq: dlg.field_qq , wechat: dlg.field_wechat + , valid_from: dlg.field_valid_from + , valid_to: dlg.field_valid_to , desc: dlg.field_desc }, function (ret) { diff --git a/server/www/teleport/view/user/user-list.mako b/server/www/teleport/view/user/user-list.mako index 8a5a02b..40ab30f 100644 --- a/server/www/teleport/view/user/user-list.mako +++ b/server/www/teleport/view/user/user-list.mako @@ -8,6 +8,7 @@ <%block name="extend_js_file"> + <%block name="embed_js"> @@ -230,6 +231,25 @@ + +
    + +
    +
    + + + + +
    +
    + + + + +
    +
    +
    +
    diff --git a/server/www/teleport/webroot/app/controller/user.py b/server/www/teleport/webroot/app/controller/user.py index 6200873..5965293 100755 --- a/server/www/teleport/webroot/app/controller/user.py +++ b/server/www/teleport/webroot/app/controller/user.py @@ -588,13 +588,22 @@ class DoUpdateUserHandler(TPBaseJsonHandler): args['mobile'] = args['mobile'].strip() args['qq'] = args['qq'].strip() args['wechat'] = args['wechat'].strip() + + if args['valid_from'] == '': + args['valid_from'] = '1970-01-01' + else: + args['valid_from'] = args['valid_from'].strip() + if args['valid_to'] == '': + args['valid_to'] = '1970-01-01' + else: + args['valid_to'] = args['valid_to'].strip() args['desc'] = args['desc'].strip() except: return self.write_json(TPE_PARAM) if len(args['username']) == 0: return self.write_json(TPE_PARAM) - + if args['id'] == -1: args['password'] = tp_gen_password(8) err, _ = user.create_user(self, args) diff --git a/server/www/teleport/webroot/app/model/user.py b/server/www/teleport/webroot/app/model/user.py index b3688ce..9bc45e1 100755 --- a/server/www/teleport/webroot/app/model/user.py +++ b/server/www/teleport/webroot/app/model/user.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- - +import time,datetime from app.base.configs import tp_cfg from app.base.db import get_db, SQL from app.base.logger import log @@ -38,7 +38,7 @@ def get_by_username(username): s.select_from('user', ['id', 'type', 'auth_type', 'username', 'surname', 'ldap_dn', 'password', 'oath_secret', 'role_id', 'state', 'fail_count', 'lock_time', 'email', 'create_time', 'last_login', 'last_ip', 'last_chpass', - 'mobile', 'qq', 'wechat', 'desc'], alt_name='u') + 'mobile', 'qq', 'wechat', 'valid_from', 'valid_to', 'desc'], alt_name='u') s.left_join('role', ['name', 'privilege'], join_on='r.id=u.role_id', alt_name='r', out_map={'name': 'role'}) s.where('u.username="{}"'.format(username)) err = s.query() @@ -57,7 +57,9 @@ def get_by_username(username): def login(handler, username, password=None, oath_code=None, check_bind_oath=False): sys_cfg = tp_cfg().sys msg = '' - + current_unix_time = int(time.mktime(datetime.datetime.now().timetuple())) +# log.e('current:',current_unix_time,'validfrom:', user_info['valid_from']) + err, user_info = get_by_username(username) if err != TPE_OK: return err, None, msg @@ -88,6 +90,10 @@ def login(handler, username, password=None, oath_code=None, check_bind_oath=Fals msg = '登录失败,用户状态异常' syslog.sys_log(user_info, handler.request.remote_ip, TPE_FAILED, msg) return TPE_FAILED, None, msg + elif current_unix_time < user_info['valid_from'] or (current_unix_time > user_info['valid_to'] and user_info['valid_to'] != 0): + msg = '登录失败,用户已过期' + syslog.sys_log(user_info, handler.request.remote_ip, TPE_FAILED, msg) + return TPE_FAILED, None, msg err_msg = '' if password is not None: @@ -172,7 +178,7 @@ def login(handler, username, password=None, oath_code=None, check_bind_oath=Fals def get_users(sql_filter, sql_order, sql_limit, sql_restrict, sql_exclude): dbtp = get_db().table_prefix s = SQL(get_db()) - s.select_from('user', ['id', 'type', 'auth_type', 'username', 'surname', 'role_id', 'state', 'email', 'last_login'], + s.select_from('user', ['id', 'type', 'auth_type', 'username', 'surname', 'role_id', 'state', 'email', 'last_login', 'valid_from', 'valid_to'], alt_name='u') s.left_join('role', ['name', 'privilege'], join_on='r.id=u.role_id', alt_name='r', out_map={'name': 'role'}) @@ -353,14 +359,15 @@ def create_user(handler, user): sql = 'INSERT INTO `{}user` (' \ '`role_id`, `username`, `surname`, `type`, `ldap_dn`, `auth_type`, `password`, `state`, ' \ - '`email`, `creator_id`, `create_time`, `last_login`, `last_chpass`, `desc`' \ + '`email`, `creator_id`, `create_time`, `last_login`, `last_chpass`, `valid_from`, `valid_to`, `desc`' \ ') VALUES (' \ '{role}, "{username}", "{surname}", {user_type}, "{ldap_dn}", {auth_type}, "{password}", {state}, ' \ - '"{email}", {creator_id}, {create_time}, {last_login}, {last_chpass}, "{desc}");' \ + '"{email}", {creator_id}, {create_time}, {last_login}, {last_chpass}, unix_timestamp("{valid_from}"), '\ + 'unix_timestamp("{valid_to}"), "{desc}");' \ ''.format(db.table_prefix, role=user['role'], username=user['username'], surname=user['surname'], user_type=user['type'], ldap_dn=user['ldap_dn'], auth_type=user['auth_type'], password=_password, state=TP_STATE_NORMAL, email=user['email'], creator_id=operator['id'], create_time=_time_now, - last_login=0, last_chpass=_time_now, desc=user['desc']) + last_login=0, last_chpass=_time_now, valid_from=user['valid_from'], valid_to=user['valid_to'], desc=user['desc']) db_ret = db.exec(sql) if not db_ret: return TPE_DATABASE, 0 @@ -400,12 +407,12 @@ def update_user(handler, args): sql = 'UPDATE `{}user` SET ' \ '`username`="{username}", `surname`="{surname}", `auth_type`={auth_type}, ' \ '`role_id`={role}, `email`="{email}", `mobile`="{mobile}", `qq`="{qq}", ' \ - '`wechat`="{wechat}", `desc`="{desc}" WHERE `id`={user_id};' \ + '`wechat`="{wechat}", `valid_from`=unix_timestamp("{valid_from}"), `valid_to`=unix_timestamp("{valid_to}"), '\ + '`desc`="{desc}" WHERE `id`={user_id};' \ ''.format(db.table_prefix, username=args['username'], surname=args['surname'], auth_type=args['auth_type'], role=args['role'], - email=args['email'], - mobile=args['mobile'], qq=args['qq'], wechat=args['wechat'], desc=args['desc'], - user_id=args['id'] + email=args['email'], mobile=args['mobile'], qq=args['qq'], wechat=args['wechat'], + valid_from=args['valid_from'], valid_to=args['valid_to'], desc=args['desc'], user_id=args['id'] ) db_ret = db.exec(sql) if not db_ret: From 99087cddc32decdb51e4c29fb98533734bfcc292 Mon Sep 17 00:00:00 2001 From: horizon Date: Thu, 12 Sep 2019 20:09:44 +0800 Subject: [PATCH 20/44] =?UTF-8?q?=E8=BF=90=E7=BB=B4=E6=8E=88=E6=9D=83?= =?UTF-8?q?=E4=B8=AD=EF=BC=8C=E5=B8=90=E5=8F=B7=E6=8E=88=E6=9D=83=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E4=B8=BB=E6=9C=BA=E5=90=8D=E6=88=96=E4=B8=BB=E6=9C=BA?= =?UTF-8?q?=E6=8F=8F=E8=BF=B0=E8=BF=87=E6=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/www/teleport/webroot/app/model/account.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/www/teleport/webroot/app/model/account.py b/server/www/teleport/webroot/app/model/account.py index 3cb22b5..be11035 100644 --- a/server/www/teleport/webroot/app/model/account.py +++ b/server/www/teleport/webroot/app/model/account.py @@ -166,6 +166,7 @@ def get_accounts(sql_filter, sql_order, sql_limit, sql_restrict, sql_exclude): s = SQL(db) # s.select_from('acc', ['id', 'host_id', 'host_ip', 'router_ip', 'router_port', 'username', 'protocol_type', 'auth_type', 'state'], alt_name='a') s.select_from('acc', ['id', 'host_id', 'username', 'protocol_type', 'auth_type', 'state', 'username_prompt', 'password_prompt'], alt_name='a') + s.left_join('host', ['name', 'desc'], join_on='h.id=a.host_id', alt_name='h', out_map={'name': 'host_name'}) str_where = '' _where = list() @@ -189,7 +190,7 @@ def get_accounts(sql_filter, sql_order, sql_limit, sql_restrict, sql_exclude): if len(sql_filter) > 0: for k in sql_filter: if k == 'search': - _where.append('(a.username LIKE "%{filter}%" OR a.host_ip LIKE "%{filter}%" OR a.router_ip LIKE "%{filter}%")'.format(filter=sql_filter[k])) + _where.append('(a.username LIKE "%{filter}%" OR a.host_ip LIKE "%{filter}%" OR a.router_ip LIKE "%{filter}%" OR h.name LIKE "%{filter}%" OR h.desc LIKE "%{filter}%")'.format(filter=sql_filter[k])) # _where.append('(a.username LIKE "%{filter}%")'.format(filter=sql_filter[k])) if len(_where) > 0: From 354ec7e4c420d3d486c916fb21fdd91811754ecc Mon Sep 17 00:00:00 2001 From: horizon Date: Thu, 12 Sep 2019 20:20:38 +0800 Subject: [PATCH 21/44] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E4=B8=8B=E8=BD=BD?= =?UTF-8?q?=E9=93=BE=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/www/teleport/static/js/tp-assist.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/www/teleport/static/js/tp-assist.js b/server/www/teleport/static/js/tp-assist.js index b8cf11a..62bd5a2 100644 --- a/server/www/teleport/static/js/tp-assist.js +++ b/server/www/teleport/static/js/tp-assist.js @@ -71,11 +71,13 @@ $assist.alert_assist_not_found = function () { if($assist.errcode === TPE_NO_ASSIST) { $assist.dom.msg_box_title.html('未检测到TELEPORT助手'); $assist.dom.msg_box_info.html('需要TELEPORT助手来辅助远程连接,请确认本机运行了TELEPORT助手!'); - $assist.dom.msg_box_desc.html('如果您尚未运行TELEPORT助手,请 下载最新版TELEPORT助手安装包 并安装。一旦运行了TELEPORT助手,即可重新进行远程连接。'); + $assist.dom.msg_box_desc.html('如果您尚未运行TELEPORT助手,请 下载最新版TELEPORT助手安装包 并安装。一旦运行了TELEPORT助手,即可重新进行远程连接。'); + //$assist.dom.msg_box_desc.html('如果您尚未运行TELEPORT助手,请 下载最新版TELEPORT助手安装包 并安装。一旦运行了TELEPORT助手,即可重新进行远程连接。'); } else if($assist.errcode === TPE_OLD_ASSIST) { $assist.dom.msg_box_title.html('TELEPORT助手需要升级'); $assist.dom.msg_box_info.html('检测到TELEPORT助手版本 v'+ $assist.version +',但需要最低版本 v'+ $assist.ver_require+'。'); - $assist.dom.msg_box_desc.html('请 下载最新版TELEPORT助手安装包 并安装。一旦升级了TELEPORT助手,即可重新进行远程连接。'); + $assist.dom.msg_box_desc.html('请 下载最新版TELEPORT助手安装包 并安装。一旦升级了TELEPORT助手,即可重新进行远程连接。'); + //$assist.dom.msg_box_desc.html('请 下载最新版TELEPORT助手安装包 并安装。一旦升级了TELEPORT助手,即可重新进行远程连接。'); } $('#dialog-need-assist').modal(); From 8e24e8622bc5111a53af4fef87fffcab12a80407 Mon Sep 17 00:00:00 2001 From: horizon Date: Thu, 12 Sep 2019 20:21:39 +0800 Subject: [PATCH 22/44] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E4=B8=8B=E8=BF=9E?= =?UTF-8?q?=E9=93=BE=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/www/teleport/static/js/tp-assist.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/www/teleport/static/js/tp-assist.js b/server/www/teleport/static/js/tp-assist.js index 62bd5a2..bd051b6 100644 --- a/server/www/teleport/static/js/tp-assist.js +++ b/server/www/teleport/static/js/tp-assist.js @@ -76,7 +76,7 @@ $assist.alert_assist_not_found = function () { } else if($assist.errcode === TPE_OLD_ASSIST) { $assist.dom.msg_box_title.html('TELEPORT助手需要升级'); $assist.dom.msg_box_info.html('检测到TELEPORT助手版本 v'+ $assist.version +',但需要最低版本 v'+ $assist.ver_require+'。'); - $assist.dom.msg_box_desc.html('请 下载最新版TELEPORT助手安装包 并安装。一旦升级了TELEPORT助手,即可重新进行远程连接。'); + $assist.dom.msg_box_desc.html('请 下载最新版TELEPORT助手安装包 并安装。一旦升级了TELEPORT助手,即可重新进行远程连接。'); //$assist.dom.msg_box_desc.html('请 下载最新版TELEPORT助手安装包 并安装。一旦升级了TELEPORT助手,即可重新进行远程连接。'); } From e5610b17a2510e1673766314477052181eff2104 Mon Sep 17 00:00:00 2001 From: Apex Liu Date: Mon, 16 Sep 2019 13:53:27 +0800 Subject: [PATCH 23/44] .temp. --- client/tp-player/dlgmessage.cpp | 21 ++++ client/tp-player/dlgmessage.h | 24 ++++ client/tp-player/dlgmessage.ui | 32 +++++ client/tp-player/main.cpp | 42 ++++++- client/tp-player/mainwindow.cpp | 131 +++++++++++++------- client/tp-player/mainwindow.h | 13 +- client/tp-player/record_format.h | 18 +-- client/tp-player/thr_download.cpp | 34 +++++ client/tp-player/thr_download.h | 47 +++++++ client/tp-player/thr_play.cpp | 184 ++++++++++++++++++++++++---- client/tp-player/thr_play.h | 14 ++- client/tp-player/tp-player.pro | 63 +++++----- client/tp-player/update_data.cpp | 4 +- client/tp-player/update_data.h | 21 +++- external/version.ini | 2 +- server/tp_core/common/base_record.h | 36 +++--- 16 files changed, 546 insertions(+), 140 deletions(-) create mode 100644 client/tp-player/dlgmessage.cpp create mode 100644 client/tp-player/dlgmessage.h create mode 100644 client/tp-player/dlgmessage.ui create mode 100644 client/tp-player/thr_download.cpp create mode 100644 client/tp-player/thr_download.h diff --git a/client/tp-player/dlgmessage.cpp b/client/tp-player/dlgmessage.cpp new file mode 100644 index 0000000..5df1bb5 --- /dev/null +++ b/client/tp-player/dlgmessage.cpp @@ -0,0 +1,21 @@ +#include "dlgmessage.h" +#include "ui_dlgmessage.h" + +DlgMessage::DlgMessage(QWidget *parent) : + QDialog(parent), + ui(new Ui::DlgMessage) +{ + ui->setupUi(this); +} + +DlgMessage::~DlgMessage() +{ + delete ui; +} + +void DlgMessage::set_text(const QString& text) { + // TODO: 根据文字长度,父窗口宽度,调节对话框宽度,最大不超过父窗口宽度的 2/3。 + // 调节label的宽度和高度,并调节对话框高度,最后将对话框调整到父窗口居中的位置。 + + ui->label->setText(text); +} diff --git a/client/tp-player/dlgmessage.h b/client/tp-player/dlgmessage.h new file mode 100644 index 0000000..8dbbe7d --- /dev/null +++ b/client/tp-player/dlgmessage.h @@ -0,0 +1,24 @@ +#ifndef DLGMESSAGE_H +#define DLGMESSAGE_H + +#include + +namespace Ui { +class DlgMessage; +} + +class DlgMessage : public QDialog +{ + Q_OBJECT + +public: + explicit DlgMessage(QWidget *parent = nullptr); + ~DlgMessage(); + + void set_text(const QString& text); + +private: + Ui::DlgMessage *ui; +}; + +#endif // DLGMESSAGE_H diff --git a/client/tp-player/dlgmessage.ui b/client/tp-player/dlgmessage.ui new file mode 100644 index 0000000..71a5631 --- /dev/null +++ b/client/tp-player/dlgmessage.ui @@ -0,0 +1,32 @@ + + + DlgMessage + + + + 0 + 0 + 400 + 120 + + + + + + + + + 10 + 10 + 59 + 16 + + + + + + + + + + diff --git a/client/tp-player/main.cpp b/client/tp-player/main.cpp index b7b993e..bbf4c1a 100644 --- a/client/tp-player/main.cpp +++ b/client/tp-player/main.cpp @@ -1,5 +1,8 @@ -#include "mainwindow.h" +#include "mainwindow.h" #include +#include +#include +#include // 编译出来的可执行程序复制到单独目录,然后执行 windeployqt 应用程序文件名 // 即可自动将依赖的动态库等复制到此目录中。有些文件是多余的,可以酌情删除。 @@ -11,8 +14,41 @@ int main(int argc, char *argv[]) //#endif QApplication a(argc, argv); - MainWindow w; - w.show(); + QGuiApplication::setApplicationDisplayName("TP-Player"); + + QCommandLineParser parser; + const QCommandLineOption opt_help = parser.addHelpOption(); + + parser.addPositionalArgument("RESOURCE", "teleport record resource to be play."); + + if(!parser.parse(QCoreApplication::arguments())) { + QMessageBox::warning(nullptr, QGuiApplication::applicationDisplayName(), + //"

    " + parser.errorText() + "

    "
    +                             "

    " + parser.errorText() + "

    "
    +                             + parser.helpText() + "
    "); + return 1; + } + + if(parser.isSet(opt_help)) { + QMessageBox::warning(nullptr, QGuiApplication::applicationDisplayName(), + "
    "
    +                             + parser.helpText()
    +                             + "\n\n"
    +                             + "RESOURCE could be:\n"
    +                             + "    teleport record file (.tpr).\n"
    +                             + "    a directory contains .tpr file.\n"
    +                             + "    an URL for download teleport record file."
    +                             + "
    "); + return 2; + } + + const QStringList args = parser.positionalArguments(); + QString resource = args.at(0); + qDebug() << resource; + + MainWindow w; + w.set_resource(resource); + w.show(); return a.exec(); } diff --git a/client/tp-player/mainwindow.cpp b/client/tp-player/mainwindow.cpp index 0dbd413..3b0b323 100644 --- a/client/tp-player/mainwindow.cpp +++ b/client/tp-player/mainwindow.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include bool rdpimg2QImage(QImage& out, int w, int h, int bitsPerPixel, bool isCompressed, uint8_t* dat, uint32_t len) { switch(bitsPerPixel) { @@ -67,7 +69,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { - m_shown = false; + //m_shown = false; m_show_default = true; m_bar_shown = false; m_bar_fade_in = false; @@ -78,6 +80,8 @@ MainWindow::MainWindow(QWidget *parent) : m_thr_play = nullptr; m_play_state = PLAY_STATE_UNKNOWN; + m_msg_box = nullptr; + ui->setupUi(this); ui->centralWidget->setMouseTracking(true); @@ -103,28 +107,46 @@ MainWindow::MainWindow(QWidget *parent) : } // connect(&m_thr_play, SIGNAL(signal_update_data(update_data*)), this, SLOT(_do_update_data(update_data*))); + connect(&m_timer_first_run, SIGNAL(timeout()), this, SLOT(_do_first_run())); connect(&m_timer_bar_fade, SIGNAL(timeout()), this, SLOT(_do_bar_fade())); connect(&m_timer_bar_delay_hide, SIGNAL(timeout()), this, SLOT(_do_bar_delay_hide())); + + m_timer_first_run.setSingleShot(true); + m_timer_first_run.start(500); } MainWindow::~MainWindow() { if(m_thr_play) { m_thr_play->stop(); - m_thr_play->wait(); + //m_thr_play->wait(); + //qDebug() << "play thread stoped."; disconnect(m_thr_play, SIGNAL(signal_update_data(update_data*)), this, SLOT(_do_update_data(update_data*))); delete m_thr_play; m_thr_play = nullptr; } + + if(m_msg_box) { + delete m_msg_box; + } + delete ui; } +void MainWindow::set_resource(const QString &res) { + m_res = res; +} + +void MainWindow::_do_first_run() { + _start_play_thread(); +} + void MainWindow::_start_play_thread() { if(m_thr_play) { m_thr_play->stop(); - m_thr_play->wait(); + //m_thr_play->wait(); disconnect(m_thr_play, SIGNAL(signal_update_data(update_data*)), this, SLOT(_do_update_data(update_data*))); @@ -132,7 +154,7 @@ void MainWindow::_start_play_thread() { m_thr_play = nullptr; } - m_thr_play = new ThreadPlay; + m_thr_play = new ThreadPlay(m_res); connect(m_thr_play, SIGNAL(signal_update_data(update_data*)), this, SLOT(_do_update_data(update_data*))); m_thr_play->speed(m_bar.get_speed()); m_thr_play->start(); @@ -169,11 +191,11 @@ void MainWindow::paintEvent(QPaintEvent *e) } } - if(!m_shown) { - m_shown = true; - //m_thr_play.start(); - _start_play_thread(); - } +// if(!m_shown) { +// m_shown = true; +// //m_thr_play.start(); +// _start_play_thread(); +// } } void MainWindow::pause() { @@ -200,6 +222,9 @@ void MainWindow::_do_update_data(update_data* dat) { UpdateDataHelper data_helper(dat); if(dat->data_type() == TYPE_DATA) { + if(m_msg_box) { + m_msg_box->hide(); + } if(dat->data_len() <= sizeof(TS_RECORD_PKG)) { qDebug() << "invalid record package(1)."; @@ -250,13 +275,35 @@ void MainWindow::_do_update_data(update_data* dat) { return; } - if(dat->data_type() == TYPE_TIMER) { - m_bar.update_passed_time(dat->passed_ms()); + else if(dat->data_type() == TYPE_PLAYED_MS) { + m_bar.update_passed_time(dat->played_ms()); + return; + } + + else if(dat->data_type() == TYPE_MESSAGE) { + //QMessageBox::warning(nullptr, QGuiApplication::applicationDisplayName(), dat->message()); + if(!m_msg_box) { + m_msg_box = new DlgMessage(this); + // 无窗口标题栏,无边框 + m_msg_box->setWindowFlags(Qt::Dialog | Qt::MSWindowsFixedSizeDialogHint | Qt::ToolTip | Qt::FramelessWindowHint); + // 设置成非模态 + m_msg_box->setModal(false); + } + + m_msg_box->set_text(dat->message()); + // 显示对话框 + m_msg_box->show(); + return; + } + + else if(dat->data_type() == TYPE_ERROR) { + QMessageBox::warning(nullptr, QGuiApplication::applicationDisplayName(), dat->message()); + QApplication::instance()->exit(0); return; } // 这是播放开始时收到的第一个数据包 - if(dat->data_type() == TYPE_HEADER_INFO) { + else if(dat->data_type() == TYPE_HEADER_INFO) { if(dat->data_len() != sizeof(TS_RECORD_HEADER)) { qDebug() << "invalid record header."; return; @@ -264,41 +311,39 @@ void MainWindow::_do_update_data(update_data* dat) { memcpy(&m_rec_hdr, dat->data_buf(), sizeof(TS_RECORD_HEADER)); qDebug() << "resize (" << m_rec_hdr.basic.width << "," << m_rec_hdr.basic.height << ")"; - if(m_rec_hdr.basic.width > 0 && m_rec_hdr.basic.height > 0) { - if(m_canvas.width() != m_rec_hdr.basic.width && m_canvas.height() != m_rec_hdr.basic.height) { - m_canvas = QPixmap(m_rec_hdr.basic.width, m_rec_hdr.basic.height); + if(m_canvas.width() != m_rec_hdr.basic.width && m_canvas.height() != m_rec_hdr.basic.height) { + m_canvas = QPixmap(m_rec_hdr.basic.width, m_rec_hdr.basic.height); - //m_win_board_w = frameGeometry().width() - geometry().width(); - //m_win_board_h = frameGeometry().height() - geometry().height(); + //m_win_board_w = frameGeometry().width() - geometry().width(); + //m_win_board_h = frameGeometry().height() - geometry().height(); - QDesktopWidget *desktop = QApplication::desktop(); // =qApp->desktop();也可以 - qDebug("desktop w:%d,h:%d, this w:%d,h:%d", desktop->width(), desktop->height(), width(), height()); - //move((desktop->width() - this->width())/2, (desktop->height() - this->height())/2); - move(10, (desktop->height() - m_rec_hdr.basic.height)/2); + QDesktopWidget *desktop = QApplication::desktop(); // =qApp->desktop();也可以 + qDebug("desktop w:%d,h:%d, this w:%d,h:%d", desktop->width(), desktop->height(), width(), height()); + //move((desktop->width() - this->width())/2, (desktop->height() - this->height())/2); + move(10, (desktop->height() - m_rec_hdr.basic.height)/2); - //setFixedSize(m_rec_hdr.basic.width + m_win_board_w, m_rec_hdr.basic.height + m_win_board_h); - //resize(m_rec_hdr.basic.width + m_win_board_w, m_rec_hdr.basic.height + m_win_board_h); - //resize(m_rec_hdr.basic.width, m_rec_hdr.basic.height); - setFixedSize(m_rec_hdr.basic.width, m_rec_hdr.basic.height); - } - - m_canvas.fill(QColor(38, 73, 111)); - - m_show_default = false; - repaint(); - - m_bar.start(m_rec_hdr.info.time_ms, 640); - m_bar_shown = true; - m_play_state = PLAY_STATE_RUNNING; - - update(m_bar.rc()); - - m_bar_fade_in = false; - m_bar_fading = true; - m_timer_bar_delay_hide.start(2000); + //setFixedSize(m_rec_hdr.basic.width + m_win_board_w, m_rec_hdr.basic.height + m_win_board_h); + //resize(m_rec_hdr.basic.width + m_win_board_w, m_rec_hdr.basic.height + m_win_board_h); + //resize(m_rec_hdr.basic.width, m_rec_hdr.basic.height); + setFixedSize(m_rec_hdr.basic.width, m_rec_hdr.basic.height); } + m_canvas.fill(QColor(38, 73, 111)); + + m_show_default = false; + repaint(); + + m_bar.start(m_rec_hdr.info.time_ms, 640); + m_bar_shown = true; + m_play_state = PLAY_STATE_RUNNING; + + update(m_bar.rc()); + + m_bar_fade_in = false; + m_bar_fading = true; + m_timer_bar_delay_hide.start(2000); + QString title; if (m_rec_hdr.basic.conn_port == 3389) title.sprintf("[%s] %s@%s [Teleport-RDP录像回放]", m_rec_hdr.basic.acc_username, m_rec_hdr.basic.user_username, m_rec_hdr.basic.conn_ip); @@ -311,7 +356,7 @@ void MainWindow::_do_update_data(update_data* dat) { } - if(dat->data_type() == TYPE_END) { + else if(dat->data_type() == TYPE_END) { m_bar.end(); m_play_state = PLAY_STATE_STOP; return; @@ -383,6 +428,8 @@ void MainWindow::mouseMoveEvent(QMouseEvent *e) { } void MainWindow::mousePressEvent(QMouseEvent *e) { +// QApplication::instance()->exit(0); +// return; if(!m_show_default) { QRect rc = m_bar.rc(); if(rc.contains(e->pos())) { diff --git a/client/tp-player/mainwindow.h b/client/tp-player/mainwindow.h index 03aac59..1e14620 100644 --- a/client/tp-player/mainwindow.h +++ b/client/tp-player/mainwindow.h @@ -2,11 +2,13 @@ #define MAINWINDOW_H #include +#include #include #include "bar.h" #include "thr_play.h" #include "update_data.h" #include "record_format.h" +#include "dlgmessage.h" #define PLAY_STATE_UNKNOWN 0 #define PLAY_STATE_RUNNING 1 @@ -25,6 +27,8 @@ public: explicit MainWindow(QWidget *parent = nullptr); ~MainWindow(); + void set_resource(const QString& res); + void pause(); void resume(); void restart(); @@ -38,17 +42,20 @@ private: void _start_play_thread(); private slots: + void _do_first_run(); // 默认界面加载完成后,开始播放操作(可能会进行数据下载) void _do_update_data(update_data*); void _do_bar_fade(); void _do_bar_delay_hide(); private: Ui::MainWindow *ui; - bool m_shown; + + //bool m_shown; bool m_show_default; bool m_bar_shown; QPixmap m_default_bg; + QString m_res; ThreadPlay* m_thr_play; QPixmap m_canvas; @@ -60,6 +67,7 @@ private: QPixmap m_pt_normal; TS_RECORD_RDP_POINTER m_pt; + QTimer m_timer_first_run; QTimer m_timer_bar_fade; QTimer m_timer_bar_delay_hide; bool m_bar_fade_in; @@ -67,6 +75,9 @@ private: qreal m_bar_opacity; int m_play_state; + + //QMessageBox* m_msg_box; + DlgMessage* m_msg_box; }; #endif // MAINWINDOW_H diff --git a/client/tp-player/record_format.h b/client/tp-player/record_format.h index 2a09635..cbb122c 100644 --- a/client/tp-player/record_format.h +++ b/client/tp-player/record_format.h @@ -3,13 +3,6 @@ #include - -#define TYPE_HEADER_INFO 0 -#define TYPE_DATA 1 -#define TYPE_TIMER 2 -#define TYPE_END 3 - - #define TS_RECORD_TYPE_RDP_POINTER 0x12 // 鼠标坐标位置改变,用于绘制虚拟鼠标 #define TS_RECORD_TYPE_RDP_IMAGE 0x13 // 服务端返回的图像,用于展示 @@ -22,11 +15,12 @@ // 录像文件头(随着录像数据写入,会改变的部分) typedef struct TS_RECORD_HEADER_INFO { - uint32_t magic; // "TPPR" 标志 TelePort Protocol Record - uint16_t ver; // 录像文件版本,目前为3 - uint32_t packages; // 总包数 - uint32_t time_ms; // 总耗时(毫秒) - //uint32_t file_size; // 数据文件大小 + uint32_t magic; // "TPPR" 标志 TelePort Protocol Record + uint16_t ver; // 录像文件版本,从3.5.0开始,为4 + uint32_t packages; // 总包数 + uint32_t time_ms; // 总耗时(毫秒) + uint32_t dat_file_count; // 数据文件数量 + uint8_t _reserve[64-4-2-4-4-4]; }TS_RECORD_HEADER_INFO; #define ts_record_header_info_size sizeof(TS_RECORD_HEADER_INFO) diff --git a/client/tp-player/thr_download.cpp b/client/tp-player/thr_download.cpp new file mode 100644 index 0000000..c338932 --- /dev/null +++ b/client/tp-player/thr_download.cpp @@ -0,0 +1,34 @@ +#include "thr_download.h" +#include + +ThreadDownload::ThreadDownload(const QString& url) +{ + m_url = url; + m_need_stop = false; +} + +void ThreadDownload::stop() { + if(!isRunning()) + return; + m_need_stop = true; + wait(); + qDebug() << "download thread end."; +} + +bool ThreadDownload::prepare(QString& path_base, QString& msg) { + path_base = m_path_base; + return true; +} + + +void ThreadDownload::run() { + for(int i = 0; i < 500; i++) { + if(m_need_stop) + break; + msleep(100); + + if(i == 50) { + m_path_base = "/Users/apex/Desktop/tp-testdata/"; + } + } +} diff --git a/client/tp-player/thr_download.h b/client/tp-player/thr_download.h new file mode 100644 index 0000000..88be069 --- /dev/null +++ b/client/tp-player/thr_download.h @@ -0,0 +1,47 @@ +#ifndef THREADDOWNLOAD_H +#define THREADDOWNLOAD_H + +#include + +/* +为支持“边下载,边播放”、“可拖动进度条”等功能,录像数据会分为多个文件存放,目前每个文件约4MB。 +例如: + tp-rdp.tpr + tp-rdp.tpk (关键帧信息文件,v3.5.0开始引入) + tp-rdp-1.tpd, tp-rdp-2.tpd, tp-rdp-3.tpd, ... +这样,下载完一个数据文件,即可播放此数据文件中的内容,同时下载线程可以下载后续数据文件。 + +为支持“拖动进度条”,可以在数据文件中插入关键帧的方式,这就要求记录录像数据的同时对图像数据进行解码, +并同步合成全屏数据(关键帧),每经过一段时间(或者一定数量的图像数据包)之后,就在录像数据文件中增加一个关键帧。 +正常播放时,跳过此关键帧。 +当进度条拖放发生时,找到目标时间点之前的最后一个关键帧,从此处开始无延时播放到目标时间点,然后正常播放。 +因此,需要能够快速定位到各个关键帧,因为有可能此时尚未下载这个关键帧所在的数据文件。定位到此关键帧 +所在的数据文件后,下载线程要放弃当前下载任务(如果不是当前正在下载的数据文件),并开始下载新的数据文件。 +因此,需要引入新的关键帧信息文件(.tpk),记录各个关键帧数据所在的数据文件序号、偏移、时间点等信息。 + +另外,为保证数据文件、关键帧信息文件等下载正确,下载时保存到对应的临时文件中,并记录已下载字节数,下载完成后再改名,如: + tp-rdp.tpk.tmp, tp-rdp.tpk.len + tp-rdp-1.tpd.tmp, tp-rdp-1.tpd.len, ... +这样,下次需要下载指定文件时,如果发现对应的临时文件存在,可以根据已下载字节数,继续下载。 +*/ + + +class ThreadDownload : public QThread +{ +public: + ThreadDownload(const QString& url); + + virtual void run(); + void stop(); + + // 下载 .tpr 和 .tpf 文件,出错返回false,正在下载或已经下载完成则返回true. + bool prepare(QString& path_base, QString& msg); + +private: + bool m_need_stop; + QString m_url; + + QString m_path_base; +}; + +#endif // THREADDOWNLOAD_H diff --git a/client/tp-player/thr_play.cpp b/client/tp-player/thr_play.cpp index c40e7c6..8aaa311 100644 --- a/client/tp-player/thr_play.cpp +++ b/client/tp-player/thr_play.cpp @@ -3,27 +3,136 @@ #include #include #include +#include #include "thr_play.h" #include "record_format.h" -ThreadPlay::ThreadPlay() +ThreadPlay::ThreadPlay(const QString& res) { m_need_stop = false; m_need_pause = false; m_speed = 2; + m_res = res; + m_thr_download = nullptr; +} + +ThreadPlay::~ThreadPlay() { + stop(); } void ThreadPlay::stop() { + if(!isRunning()) + return; + + // warning: never call stop() inside thread::run() loop. + m_need_stop = true; + wait(); + qDebug() << "play-thread end."; + + if(m_thr_download) { + m_thr_download->stop(); + //m_thr_download->wait(); + delete m_thr_download; + m_thr_download = nullptr; + } } +void ThreadPlay::_notify_message(const QString& msg) { + update_data* _msg = new update_data(TYPE_MESSAGE); + _msg->message(msg); + emit signal_update_data(_msg); +} + +void ThreadPlay::_notify_error(const QString& err_msg) { + update_data* _err = new update_data(TYPE_ERROR); + _err->message(err_msg); + emit signal_update_data(_err); +} + + void ThreadPlay::run() { - sleep(1); +//#ifdef __APPLE__ +// QString currentPath = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); +// currentPath += "/tp-testdata/"; +//#else +// QString currentPath = QCoreApplication::applicationDirPath() + "/testdata/"; +//#endif + // /Users/apex/Library/Preferences/tp-player + //qDebug() << "appdata:" << QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation); + + // /private/var/folders/_3/zggrxjdx1lxcdqnfsbgpcwzh0000gn/T + //qDebug() << "tmp:" << QStandardPaths::writableLocation(QStandardPaths::TempLocation); + + // base of data path (include the .tpr file) + QString path_base; + + QString _tmp_res = m_res.toLower(); + if(_tmp_res.startsWith("http")) { + qDebug() << "DOWNLOAD"; + m_need_download = true; + +// path_base = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation); +// path_base += "/tprdp/"; + + _notify_message("正在缓存录像数据,请稍候..."); + + m_thr_download = new ThreadDownload(m_res); + m_thr_download->start(); + + QString msg; + for(;;) { + msleep(500); + + if(m_need_stop) { +// m_thr_download->stop(); +// m_thr_download->wait(); +// delete m_thr_download; +// m_thr_download = nullptr; + + return; + } + + if(!m_thr_download->prepare(path_base, msg)) { + msg.sprintf("指定的文件或目录不存在!\n\n%s", _tmp_res.toStdString().c_str()); + _notify_error(msg); + return; + } + + if(path_base.length()) + break; + } + } + else { + { + QFileInfo fi(m_res); + if(fi.isSymLink()) + _tmp_res = fi.symLinkTarget(); + else + _tmp_res = m_res; + } + + QFileInfo fi(_tmp_res); + if(!fi.exists()) { + QString msg; + msg.sprintf("指定的文件或目录不存在!\n\n%s", _tmp_res.toStdString().c_str()); + _notify_error(msg); + return; + } + + if(fi.isFile()) { + path_base = fi.path(); + } + else if(fi.isDir()) { + path_base = m_res; + } + + path_base += "/"; + } - QString currentPath = QCoreApplication::applicationDirPath() + "/testdata/"; qint64 read_len = 0; uint32_t total_pkg = 0; @@ -31,27 +140,47 @@ void ThreadPlay::run() { // 加载录像基本信息数据 - QString hdr_filename(currentPath); - hdr_filename += "tp-rdp.tpr"; + QString tpr_filename(path_base); + tpr_filename += "tp-rdp.tpr"; - QFile f_hdr(hdr_filename); + QFile f_hdr(tpr_filename); if(!f_hdr.open(QFile::ReadOnly)) { - qDebug() << "Can not open " << hdr_filename << " for read."; + qDebug() << "Can not open " << tpr_filename << " for read."; + QString msg; + msg.sprintf("无法打开录像信息文件!\n\n%s", tpr_filename.toStdString().c_str()); + _notify_error(msg); return; } else { - update_data* dat = new update_data; - dat->data_type(TYPE_HEADER_INFO); + update_data* dat = new update_data(TYPE_HEADER_INFO); dat->alloc_data(sizeof(TS_RECORD_HEADER)); read_len = f_hdr.read((char*)(dat->data_buf()), dat->data_len()); if(read_len != sizeof(TS_RECORD_HEADER)) { delete dat; qDebug() << "invaid .tpr file."; + QString msg; + msg.sprintf("错误的录像信息文件!\n\n%s", tpr_filename.toStdString().c_str()); + _notify_error(msg); return; } TS_RECORD_HEADER* hdr = (TS_RECORD_HEADER*)dat->data_buf(); + + if(hdr->info.ver != 4) { + delete dat; + qDebug() << "invaid .tpr file."; + QString msg; + msg.sprintf("不支持的录像文件版本 %d!\n\n此播放器支持录像文件版本 4。", hdr->info.ver); + _notify_error(msg); + return; + } + +// if(hdr->basic.width == 0 || hdr->basic.height == 0) { +// _notify_error("错误的录像信息,未记录窗口尺寸!"); +// return; +// } + total_pkg = hdr->info.packages; total_ms = hdr->info.time_ms; @@ -60,12 +189,15 @@ void ThreadPlay::run() { // 加载录像文件数据 - QString dat_filename(currentPath); - dat_filename += "tp-rdp.dat"; + QString tpd_filename(path_base); + tpd_filename += "tp-rdp.tpd"; - QFile f_dat(dat_filename); + QFile f_dat(tpd_filename); if(!f_dat.open(QFile::ReadOnly)) { - qDebug() << "Can not open " << dat_filename << " for read."; + qDebug() << "Can not open " << tpd_filename << " for read."; + QString msg; + msg.sprintf("无法打开录像数据文件!\n\n%s", tpd_filename.toStdString().c_str()); + _notify_error(msg); return; } @@ -89,18 +221,23 @@ void ThreadPlay::run() { TS_RECORD_PKG pkg; read_len = f_dat.read((char*)(&pkg), sizeof(pkg)); if(read_len != sizeof(TS_RECORD_PKG)) { - qDebug() << "invaid .dat file (1)."; + qDebug() << "invaid .tpd file (1)."; + QString msg; + msg.sprintf("错误的录像数据文件!\n\n%s", tpd_filename.toStdString().c_str()); + _notify_error(msg); return; } - update_data* dat = new update_data; - dat->data_type(TYPE_DATA); + update_data* dat = new update_data(TYPE_DATA); dat->alloc_data(sizeof(TS_RECORD_PKG) + pkg.size); memcpy(dat->data_buf(), &pkg, sizeof(TS_RECORD_PKG)); read_len = f_dat.read((char*)(dat->data_buf()+sizeof(TS_RECORD_PKG)), pkg.size); if(read_len != pkg.size) { delete dat; - qDebug() << "invaid .dat file."; + qDebug() << "invaid .tpd file."; + QString msg; + msg.sprintf("错误的录像数据文件!\n\n%s", tpd_filename.toStdString().c_str()); + _notify_error(msg); return; } @@ -108,9 +245,8 @@ void ThreadPlay::run() { if(time_pass > total_ms) time_pass = total_ms; if(time_pass - time_last_pass > 200) { - update_data* _passed_ms = new update_data; - _passed_ms->data_type(TYPE_TIMER); - _passed_ms->passed_ms(time_pass); + update_data* _passed_ms = new update_data(TYPE_PLAYED_MS); + _passed_ms->played_ms(time_pass); emit signal_update_data(_passed_ms); time_last_pass = time_pass; } @@ -145,9 +281,8 @@ void ThreadPlay::run() { if(_time_pass > total_ms) _time_pass = total_ms; if(_time_pass - time_last_pass > 200) { - update_data* _passed_ms = new update_data; - _passed_ms->data_type(TYPE_TIMER); - _passed_ms->passed_ms(_time_pass); + update_data* _passed_ms = new update_data(TYPE_PLAYED_MS); + _passed_ms->played_ms(_time_pass); emit signal_update_data(_passed_ms); time_last_pass = _time_pass; } @@ -160,7 +295,6 @@ void ThreadPlay::run() { } } - update_data* _end = new update_data; - _end->data_type(TYPE_END); + update_data* _end = new update_data(TYPE_END); emit signal_update_data(_end); } diff --git a/client/tp-player/thr_play.h b/client/tp-player/thr_play.h index c875762..84f23c0 100644 --- a/client/tp-player/thr_play.h +++ b/client/tp-player/thr_play.h @@ -3,13 +3,14 @@ #include #include "update_data.h" - +#include "thr_download.h" class ThreadPlay : public QThread { Q_OBJECT public: - ThreadPlay(); + ThreadPlay(const QString& res); + ~ThreadPlay(); virtual void run(); void stop(); @@ -17,6 +18,10 @@ public: void resume() {m_need_pause = false;} void speed(int s) {if(s >= 1 && s <= 16) m_speed = s;} +private: + void _notify_message(const QString& msg); + void _notify_error(const QString& err_msg); + signals: void signal_update_data(update_data*); @@ -24,6 +29,11 @@ private: bool m_need_stop; bool m_need_pause; int m_speed; + + QString m_res; + bool m_need_download; + + ThreadDownload* m_thr_download; }; #endif // THR_PLAY_H diff --git a/client/tp-player/tp-player.pro b/client/tp-player/tp-player.pro index 11b66b3..a7ca8e7 100644 --- a/client/tp-player/tp-player.pro +++ b/client/tp-player/tp-player.pro @@ -1,29 +1,34 @@ -TEMPLATE = app -TARGET = tp-player - -QT += core gui widgets - -HEADERS += \ - mainwindow.h \ - bar.h \ - thr_play.h \ - update_data.h \ - record_format.h \ - rle.h - -SOURCES += \ - main.cpp \ - mainwindow.cpp \ - bar.cpp \ - thr_play.cpp \ - update_data.cpp \ - rle.c - -RESOURCES += \ - tp-player.qrc - -RC_FILE += \ - tp-player.rc - -FORMS += \ - mainwindow.ui +TEMPLATE = app +TARGET = tp-player + +QT += core gui widgets + +HEADERS += \ + dlgmessage.h \ + mainwindow.h \ + bar.h \ + thr_download.h \ + thr_play.h \ + update_data.h \ + record_format.h \ + rle.h + +SOURCES += \ + dlgmessage.cpp \ + main.cpp \ + mainwindow.cpp \ + bar.cpp \ + thr_download.cpp \ + thr_play.cpp \ + update_data.cpp \ + rle.c + +RESOURCES += \ + tp-player.qrc + +RC_FILE += \ + tp-player.rc + +FORMS += \ + dlgmessage.ui \ + mainwindow.ui diff --git a/client/tp-player/update_data.cpp b/client/tp-player/update_data.cpp index d84f364..c57c2a1 100644 --- a/client/tp-player/update_data.cpp +++ b/client/tp-player/update_data.cpp @@ -1,8 +1,8 @@ #include "update_data.h" -update_data::update_data(QObject *parent) : QObject(parent) +update_data::update_data(int data_type, QObject *parent) : QObject(parent) { - m_data_type = 0xff; + m_data_type = data_type; m_data_buf = nullptr; m_data_len = 0; } diff --git a/client/tp-player/update_data.h b/client/tp-player/update_data.h index e701c95..97b420b 100644 --- a/client/tp-player/update_data.h +++ b/client/tp-player/update_data.h @@ -3,24 +3,34 @@ #include +#define TYPE_HEADER_INFO 0 +#define TYPE_DATA 1 +#define TYPE_PLAYED_MS 2 +#define TYPE_DOWNLOAD_PERCENT 3 +#define TYPE_END 4 +#define TYPE_MESSAGE 5 +#define TYPE_ERROR 6 + class update_data : public QObject { Q_OBJECT public: - explicit update_data(QObject *parent = nullptr); + explicit update_data(int data_type, QObject *parent = nullptr); virtual ~update_data(); void alloc_data(uint32_t len); void attach_data(const uint8_t* dat, uint32_t len); - void data_type(int dt) {m_data_type = dt;} int data_type() const {return m_data_type;} uint8_t* data_buf() {return m_data_buf;} uint32_t data_len() const {return m_data_len;} - void passed_ms(uint32_t ms) {m_passed_ms = ms;} - uint32_t passed_ms() {return m_passed_ms;} + void played_ms(uint32_t ms) {m_played_ms = ms;} + uint32_t played_ms() {return m_played_ms;} + + void message(const QString& msg) {m_msg = msg;} + const QString message(){return m_msg;} signals: @@ -31,7 +41,8 @@ private: int m_data_type; uint8_t* m_data_buf; uint32_t m_data_len; - uint32_t m_passed_ms; + uint32_t m_played_ms; + QString m_msg; }; class UpdateDataHelper { diff --git a/external/version.ini b/external/version.ini index 61456f5..78dacf0 100644 --- a/external/version.ini +++ b/external/version.ini @@ -1,5 +1,5 @@ [external_ver] -openssl = 1.0.2p,1000210f +openssl = 1.0.2s,1000210f libuv = 1.28.0 mbedtls = 2.12.0 libssh = 0.9.0 diff --git a/server/tp_core/common/base_record.h b/server/tp_core/common/base_record.h index 5615895..ddd0345 100644 --- a/server/tp_core/common/base_record.h +++ b/server/tp_core/common/base_record.h @@ -22,34 +22,34 @@ // 录像文件头(随着录像数据写入,会改变的部分) typedef struct TS_RECORD_HEADER_INFO { - ex_u32 magic; // "TPPR" 标志 TelePort Protocol Record - ex_u16 ver; // 录像文件版本,目前为3 - ex_u32 packages; // 总包数 - ex_u32 time_ms; // 总耗时(毫秒) - //ex_u32 file_size; // 数据文件大小 + ex_u32 magic; // "TPPR" 标志 TelePort Protocol Record + ex_u16 ver; // 录像文件版本,v3.5.0开始为4 + ex_u32 packages; // 总包数 + ex_u32 time_ms; // 总耗时(毫秒) + uint32_t dat_file_count; // 数据文件数量 + uint8_t _reserve[64-4-2-4-4-4]; }TS_RECORD_HEADER_INFO; #define ts_record_header_info_size sizeof(TS_RECORD_HEADER_INFO) // 录像文件头(固定不变部分) typedef struct TS_RECORD_HEADER_BASIC { - ex_u16 protocol_type; // 协议:1=RDP, 2=SSH, 3=Telnet - ex_u16 protocol_sub_type; // 子协议:100=RDP-DESKTOP, 200=SSH-SHELL, 201=SSH-SFTP, 300=Telnet - ex_u64 timestamp; // 本次录像的起始时间(UTC时间戳) - ex_u16 width; // 初始屏幕尺寸:宽 - ex_u16 height; // 初始屏幕尺寸:高 - char user_username[64]; // teleport账号 - char acc_username[64]; // 远程主机用户名 + ex_u16 protocol_type; // 协议:1=RDP, 2=SSH, 3=Telnet + ex_u16 protocol_sub_type; // 子协议:100=RDP-DESKTOP, 200=SSH-SHELL, 201=SSH-SFTP, 300=Telnet + ex_u64 timestamp; // 本次录像的起始时间(UTC时间戳) + ex_u16 width; // 初始屏幕尺寸:宽 + ex_u16 height; // 初始屏幕尺寸:高 + char user_username[64]; // teleport账号 + char acc_username[64]; // 远程主机用户名 - char host_ip[40]; // 远程主机IP - char conn_ip[40]; // 远程主机IP - ex_u16 conn_port; // 远程主机端口 + char host_ip[40]; // 远程主机IP + char conn_ip[40]; // 远程主机IP + ex_u16 conn_port; // 远程主机端口 - char client_ip[40]; // 客户端IP + char client_ip[40]; // 客户端IP -// // RDP专有 +// // RDP专有 - v3.5.0废弃并移除 // ex_u8 rdp_security; // 0 = RDP, 1 = TLS -// ex_u8 _reserve[512 - 2 - 2 - 8 - 2 - 2 - 64 - 64 - 40 - 40 - 2 - 40 - 1 - ts_record_header_info_size]; ex_u8 _reserve[512 - 2 - 2 - 8 - 2 - 2 - 64 - 64 - 40 - 40 - 2 - 40 - ts_record_header_info_size]; }TS_RECORD_HEADER_BASIC; #define ts_record_header_basic_size sizeof(TS_RECORD_HEADER_BASIC) From 6aa5c046df502be6c089125dfceae683cd05780a Mon Sep 17 00:00:00 2001 From: Apex Liu Date: Wed, 18 Sep 2019 00:51:22 +0800 Subject: [PATCH 24/44] .temp. --- client/tp-player/mainwindow.cpp | 62 +++++++-- client/tp-player/mainwindow.h | 2 + client/tp-player/thr_play.cpp | 198 +++++++++++++++------------- server/tp_core/common/base_record.h | 13 +- 4 files changed, 172 insertions(+), 103 deletions(-) diff --git a/client/tp-player/mainwindow.cpp b/client/tp-player/mainwindow.cpp index 3b0b323..f4acfbb 100644 --- a/client/tp-player/mainwindow.cpp +++ b/client/tp-player/mainwindow.cpp @@ -64,6 +64,13 @@ bool rdpimg2QImage(QImage& out, int w, int h, int bitsPerPixel, bool isCompresse return true; } +static inline int min(int a, int b){ + return a < b ? a : b; +} + +static inline int max(int a, int b){ + return a > b ? a : b; +} MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), @@ -181,6 +188,20 @@ void MainWindow::paintEvent(QPaintEvent *e) painter.drawPixmap(m_pt.x-m_pt_normal.width()/2, m_pt.y-m_pt_normal.height()/2, m_pt_normal); } + { + QRect rc_draw = e->rect(); + QRect rc(100, 100, m_img_message.width(), m_img_message.height()); + //rc.moveTo(m_rc.left()+rc.left(), m_rc.top() + rc.top()); + + int from_x = max(rc_draw.left(), rc.left()) - rc.left(); + int from_y = max(rc_draw.top(), rc.top()) - rc.top(); + int w = min(rc.right(), rc_draw.right()) - rc.left() - from_x + 1; + int h = min(rc.bottom(), rc_draw.bottom()) - rc.top() - from_y + 1; + int to_x = rc.left() + from_x; + int to_y = rc.top() + from_y; + painter.drawPixmap(to_x, to_y, m_img_message, from_x, from_y, w, h); + } + // 绘制浮动控制窗 if(m_bar_fading) { painter.setOpacity(m_bar_opacity); @@ -281,18 +302,37 @@ void MainWindow::_do_update_data(update_data* dat) { } else if(dat->data_type() == TYPE_MESSAGE) { - //QMessageBox::warning(nullptr, QGuiApplication::applicationDisplayName(), dat->message()); - if(!m_msg_box) { - m_msg_box = new DlgMessage(this); - // 无窗口标题栏,无边框 - m_msg_box->setWindowFlags(Qt::Dialog | Qt::MSWindowsFixedSizeDialogHint | Qt::ToolTip | Qt::FramelessWindowHint); - // 设置成非模态 - m_msg_box->setModal(false); - } + QPainter pp(&m_canvas); + QFontMetrics fm = pp.fontMetrics(); + QRect rcWin(0, 0, m_canvas.width(), m_canvas.height()); + QRect rc = fm.boundingRect(rcWin, Qt::AlignLeft|Qt::TextWordWrap, dat->message()); + qDebug("message, w=%d, h=%d", rc.width(), rc.height()); +// int w = fm.width(dat->message()); +// int h = fm.height(); +// qDebug("message, w=%d, h=%d", w, h); - m_msg_box->set_text(dat->message()); - // 显示对话框 - m_msg_box->show(); + m_img_message = QPixmap(rc.width() + 30, rc.height() + 30); + m_img_message.fill(Qt::transparent); + QPainter pm(&m_img_message); + pm.setPen(QColor(255,255,255,153)); + pm.fillRect(rc, QColor(0,0,0,190)); + pm.drawText(rc, Qt::AlignLeft|Qt::TextWordWrap, dat->message()); + + + + +// //QMessageBox::warning(nullptr, QGuiApplication::applicationDisplayName(), dat->message()); +// if(!m_msg_box) { +// m_msg_box = new DlgMessage(this); +// // 无窗口标题栏,无边框 +// m_msg_box->setWindowFlags(Qt::Dialog | Qt::MSWindowsFixedSizeDialogHint | Qt::ToolTip | Qt::FramelessWindowHint); +// // 设置成非模态 +// m_msg_box->setModal(false); +// } + +// m_msg_box->set_text(dat->message()); +// // 显示对话框 +// m_msg_box->show(); return; } diff --git a/client/tp-player/mainwindow.h b/client/tp-player/mainwindow.h index 1e14620..dbcdb09 100644 --- a/client/tp-player/mainwindow.h +++ b/client/tp-player/mainwindow.h @@ -78,6 +78,8 @@ private: //QMessageBox* m_msg_box; DlgMessage* m_msg_box; + + QPixmap m_img_message; }; #endif // MAINWINDOW_H diff --git a/client/tp-player/thr_play.cpp b/client/tp-player/thr_play.cpp index 8aaa311..9d133d0 100644 --- a/client/tp-player/thr_play.cpp +++ b/client/tp-player/thr_play.cpp @@ -75,9 +75,6 @@ void ThreadPlay::run() { qDebug() << "DOWNLOAD"; m_need_download = true; -// path_base = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation); -// path_base += "/tprdp/"; - _notify_message("正在缓存录像数据,请稍候..."); m_thr_download = new ThreadDownload(m_res); @@ -87,14 +84,8 @@ void ThreadPlay::run() { for(;;) { msleep(500); - if(m_need_stop) { -// m_thr_download->stop(); -// m_thr_download->wait(); -// delete m_thr_download; -// m_thr_download = nullptr; - + if(m_need_stop) return; - } if(!m_thr_download->prepare(path_base, msg)) { msg.sprintf("指定的文件或目录不存在!\n\n%s", _tmp_res.toStdString().c_str()); @@ -137,8 +128,11 @@ void ThreadPlay::run() { qint64 read_len = 0; uint32_t total_pkg = 0; uint32_t total_ms = 0; + uint32_t file_count = 0; + //====================================== // 加载录像基本信息数据 + //====================================== QString tpr_filename(path_base); tpr_filename += "tp-rdp.tpr"; @@ -176,125 +170,153 @@ void ThreadPlay::run() { return; } -// if(hdr->basic.width == 0 || hdr->basic.height == 0) { -// _notify_error("错误的录像信息,未记录窗口尺寸!"); -// return; -// } + if(hdr->basic.width == 0 || hdr->basic.height == 0) { + _notify_error("错误的录像信息,未记录窗口尺寸!"); + return; + } + + if(hdr->info.dat_file_count == 0) { + _notify_error("错误的录像信息,未记录数据文件数量!"); + return; + } total_pkg = hdr->info.packages; total_ms = hdr->info.time_ms; + file_count = hdr->info.dat_file_count; emit signal_update_data(dat); } - // 加载录像文件数据 - - QString tpd_filename(path_base); - tpd_filename += "tp-rdp.tpd"; - - QFile f_dat(tpd_filename); - if(!f_dat.open(QFile::ReadOnly)) { - qDebug() << "Can not open " << tpd_filename << " for read."; - QString msg; - msg.sprintf("无法打开录像数据文件!\n\n%s", tpd_filename.toStdString().c_str()); - _notify_error(msg); - return; - } + //====================================== + // 加载录像文件数据并播放 + //====================================== + uint32_t pkg_count = 0; uint32_t time_pass = 0; uint32_t time_last_pass = 0; - qint64 time_begin = QDateTime::currentMSecsSinceEpoch(); + QString msg; - for(uint32_t i = 0; i < total_pkg; ++i) { + for(uint32_t fidx = 0; fidx < file_count; ++fidx) { if(m_need_stop) { - qDebug() << "stop, user cancel."; + qDebug() << "stop, user cancel 1."; break; } - if(m_need_pause) { - msleep(50); - time_begin += 50; - continue; - } + QString tpd_filename; + tpd_filename.sprintf("%stp-rdp-%d.tpd", path_base.toStdString().c_str(), fidx+1); - TS_RECORD_PKG pkg; - read_len = f_dat.read((char*)(&pkg), sizeof(pkg)); - if(read_len != sizeof(TS_RECORD_PKG)) { - qDebug() << "invaid .tpd file (1)."; - QString msg; - msg.sprintf("错误的录像数据文件!\n\n%s", tpd_filename.toStdString().c_str()); + // for test. + msg.sprintf("无法打开录像数据文件!\n\n%s", tpd_filename.toStdString().c_str()); + _notify_message(msg); + + QFile f_dat(tpd_filename); + if(!f_dat.open(QFile::ReadOnly)) { + qDebug() << "Can not open " << tpd_filename << " for read."; + msg.sprintf("无法打开录像数据文件!\n\n%s", tpd_filename.toStdString().c_str()); _notify_error(msg); return; } - update_data* dat = new update_data(TYPE_DATA); - dat->alloc_data(sizeof(TS_RECORD_PKG) + pkg.size); - memcpy(dat->data_buf(), &pkg, sizeof(TS_RECORD_PKG)); - read_len = f_dat.read((char*)(dat->data_buf()+sizeof(TS_RECORD_PKG)), pkg.size); - if(read_len != pkg.size) { - delete dat; - qDebug() << "invaid .tpd file."; - QString msg; - msg.sprintf("错误的录像数据文件!\n\n%s", tpd_filename.toStdString().c_str()); - _notify_error(msg); - return; - } - - time_pass = (uint32_t)(QDateTime::currentMSecsSinceEpoch() - time_begin) * m_speed; - if(time_pass > total_ms) - time_pass = total_ms; - if(time_pass - time_last_pass > 200) { - update_data* _passed_ms = new update_data(TYPE_PLAYED_MS); - _passed_ms->played_ms(time_pass); - emit signal_update_data(_passed_ms); - time_last_pass = time_pass; - } - - if(time_pass >= pkg.time_ms) { - emit signal_update_data(dat); - continue; - } - - // 需要等待 - uint32_t time_wait = pkg.time_ms - time_pass; - uint32_t wait_this_time = 0; for(;;) { + if(m_need_stop) { + qDebug() << "stop, user cancel 2."; + break; + } + if(m_need_pause) { msleep(50); time_begin += 50; continue; } - wait_this_time = time_wait; - if(wait_this_time > 10) - wait_this_time = 10; - - if(m_need_stop) { - qDebug() << "stop, user cancel (2)."; + TS_RECORD_PKG pkg; + read_len = f_dat.read((char*)(&pkg), sizeof(pkg)); + if(read_len == 0) break; + if(read_len != sizeof(TS_RECORD_PKG)) { + qDebug() << "invaid .tpd file (1)."; + msg.sprintf("错误的录像数据文件!\n\n%s", tpd_filename.toStdString().c_str()); + _notify_error(msg); + return; } - msleep(wait_this_time); + update_data* dat = new update_data(TYPE_DATA); + dat->alloc_data(sizeof(TS_RECORD_PKG) + pkg.size); + memcpy(dat->data_buf(), &pkg, sizeof(TS_RECORD_PKG)); + read_len = f_dat.read((char*)(dat->data_buf()+sizeof(TS_RECORD_PKG)), pkg.size); + if(read_len != pkg.size) { + delete dat; + qDebug() << "invaid .tpd file."; + msg.sprintf("错误的录像数据文件!\n\n%s", tpd_filename.toStdString().c_str()); + _notify_error(msg); + return; + } - uint32_t _time_pass = (uint32_t)(QDateTime::currentMSecsSinceEpoch() - time_begin) * m_speed; - if(_time_pass > total_ms) - _time_pass = total_ms; - if(_time_pass - time_last_pass > 200) { + pkg_count++; + + time_pass = (uint32_t)(QDateTime::currentMSecsSinceEpoch() - time_begin) * m_speed; + if(time_pass > total_ms) + time_pass = total_ms; + if(time_pass - time_last_pass > 200) { update_data* _passed_ms = new update_data(TYPE_PLAYED_MS); - _passed_ms->played_ms(_time_pass); + _passed_ms->played_ms(time_pass); emit signal_update_data(_passed_ms); - time_last_pass = _time_pass; + time_last_pass = time_pass; } - time_wait -= wait_this_time; - if(time_wait == 0) { + if(time_pass >= pkg.time_ms) { emit signal_update_data(dat); - break; + continue; } + + // 需要等待 + uint32_t time_wait = pkg.time_ms - time_pass; + uint32_t wait_this_time = 0; + for(;;) { + if(m_need_pause) { + msleep(50); + time_begin += 50; + continue; + } + + wait_this_time = time_wait; + if(wait_this_time > 10) + wait_this_time = 10; + + if(m_need_stop) { + qDebug() << "stop, user cancel (2)."; + break; + } + + msleep(wait_this_time); + + uint32_t _time_pass = (uint32_t)(QDateTime::currentMSecsSinceEpoch() - time_begin) * m_speed; + if(_time_pass > total_ms) + _time_pass = total_ms; + if(_time_pass - time_last_pass > 200) { + update_data* _passed_ms = new update_data(TYPE_PLAYED_MS); + _passed_ms->played_ms(_time_pass); + emit signal_update_data(_passed_ms); + time_last_pass = _time_pass; + } + + time_wait -= wait_this_time; + if(time_wait == 0) { + emit signal_update_data(dat); + break; + } + } + } } + if(pkg_count < total_pkg) { + qDebug() << "total-pkg:" << total_pkg << ", played:" << pkg_count; + msg.sprintf("录像数据文件有误!\n\n部分录像数据缺失!"); + _notify_message(msg); + } + update_data* _end = new update_data(TYPE_END); emit signal_update_data(_end); } diff --git a/server/tp_core/common/base_record.h b/server/tp_core/common/base_record.h index ddd0345..33bf2a9 100644 --- a/server/tp_core/common/base_record.h +++ b/server/tp_core/common/base_record.h @@ -7,14 +7,19 @@ #include -#define MAX_SIZE_PER_FILE 4194304 // 4M = 1024*1024*4 +#define MAX_CACHE_SIZE 1048576 // 1M = 1024*1024*1 +#define MAX_SIZE_PER_FILE 4194304 // 4M = 1024*1024*4 #pragma pack(push,1) /* * 录像 * - * 一个录像分为两个文件,一个信息文件,一个数据文件。 + * 一个录像分为多个文件: + * *.tpr,录像信息文件,一个,固定大小(512字节) + * *.tpd,数据文件,n个,例如 tp-rdp-1.tpd,tp-rdp-2.tpd等等,每个数据文件约4MB + * *.tpk,关键帧信息文件,一个,仅RDP录像,记录各个关键帧数据所在的数据文件序号、偏移、时间点等信息。 + * *-cmd.txt,ssh命令记录文件,仅SSH。 * 服务内部缓存最大4M,或者5秒,就将数据写入数据文件中,并同时更新信息文件。 * */ @@ -26,8 +31,8 @@ typedef struct TS_RECORD_HEADER_INFO { ex_u16 ver; // 录像文件版本,v3.5.0开始为4 ex_u32 packages; // 总包数 ex_u32 time_ms; // 总耗时(毫秒) - uint32_t dat_file_count; // 数据文件数量 - uint8_t _reserve[64-4-2-4-4-4]; + ex_u32 dat_file_count; // 数据文件数量 + ex_u8 _reserve[64-4-2-4-4-4]; }TS_RECORD_HEADER_INFO; #define ts_record_header_info_size sizeof(TS_RECORD_HEADER_INFO) From 28909f376ee4eb2e58af110e6618baecdcfa744f Mon Sep 17 00:00:00 2001 From: Apex Liu Date: Fri, 20 Sep 2019 00:42:37 +0800 Subject: [PATCH 25/44] =?UTF-8?q?=E9=83=A8=E5=88=86=E8=A7=A3=E5=86=B3win?= =?UTF-8?q?=E4=B8=8B=E6=92=AD=E6=94=BE=E5=99=A8=E4=B8=AD=E6=96=87=E4=B9=B1?= =?UTF-8?q?=E7=A0=81=E9=97=AE=E9=A2=98=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/tp-player/dlgmessage.cpp | 21 ----------- client/tp-player/dlgmessage.h | 24 ------------ client/tp-player/dlgmessage.ui | 32 ---------------- client/tp-player/main.cpp | 6 +++ client/tp-player/mainwindow.cpp | 37 +++++++++---------- client/tp-player/mainwindow.h | 5 +-- client/tp-player/mainwindow.ui | 5 +++ client/tp-player/thr_play.cpp | 5 ++- client/tp-player/tp-player.pro | 65 ++++++++++++++++----------------- 9 files changed, 65 insertions(+), 135 deletions(-) delete mode 100644 client/tp-player/dlgmessage.cpp delete mode 100644 client/tp-player/dlgmessage.h delete mode 100644 client/tp-player/dlgmessage.ui diff --git a/client/tp-player/dlgmessage.cpp b/client/tp-player/dlgmessage.cpp deleted file mode 100644 index 5df1bb5..0000000 --- a/client/tp-player/dlgmessage.cpp +++ /dev/null @@ -1,21 +0,0 @@ -#include "dlgmessage.h" -#include "ui_dlgmessage.h" - -DlgMessage::DlgMessage(QWidget *parent) : - QDialog(parent), - ui(new Ui::DlgMessage) -{ - ui->setupUi(this); -} - -DlgMessage::~DlgMessage() -{ - delete ui; -} - -void DlgMessage::set_text(const QString& text) { - // TODO: 根据文字长度,父窗口宽度,调节对话框宽度,最大不超过父窗口宽度的 2/3。 - // 调节label的宽度和高度,并调节对话框高度,最后将对话框调整到父窗口居中的位置。 - - ui->label->setText(text); -} diff --git a/client/tp-player/dlgmessage.h b/client/tp-player/dlgmessage.h deleted file mode 100644 index 8dbbe7d..0000000 --- a/client/tp-player/dlgmessage.h +++ /dev/null @@ -1,24 +0,0 @@ -#ifndef DLGMESSAGE_H -#define DLGMESSAGE_H - -#include - -namespace Ui { -class DlgMessage; -} - -class DlgMessage : public QDialog -{ - Q_OBJECT - -public: - explicit DlgMessage(QWidget *parent = nullptr); - ~DlgMessage(); - - void set_text(const QString& text); - -private: - Ui::DlgMessage *ui; -}; - -#endif // DLGMESSAGE_H diff --git a/client/tp-player/dlgmessage.ui b/client/tp-player/dlgmessage.ui deleted file mode 100644 index 71a5631..0000000 --- a/client/tp-player/dlgmessage.ui +++ /dev/null @@ -1,32 +0,0 @@ - - - DlgMessage - - - - 0 - 0 - 400 - 120 - - - - - - - - - 10 - 10 - 59 - 16 - - - - - - - - - - diff --git a/client/tp-player/main.cpp b/client/tp-player/main.cpp index bbf4c1a..2131644 100644 --- a/client/tp-player/main.cpp +++ b/client/tp-player/main.cpp @@ -3,6 +3,7 @@ #include #include #include +#include // 编译出来的可执行程序复制到单独目录,然后执行 windeployqt 应用程序文件名 // 即可自动将依赖的动态库等复制到此目录中。有些文件是多余的,可以酌情删除。 @@ -47,6 +48,11 @@ int main(int argc, char *argv[]) QString resource = args.at(0); qDebug() << resource; + +// QTextCodec::setCodecForTr(QTextCodec::codecForName("GB2312")); +// QTextCodec::setCodecForLocale(QTextCodec::codecForName("GBK")); +// QTextCodec::setCodecForCStrings(QTextCodec::codecForName("GB2312")); + MainWindow w; w.set_resource(resource); w.show(); diff --git a/client/tp-player/mainwindow.cpp b/client/tp-player/mainwindow.cpp index f4acfbb..9c56f4f 100644 --- a/client/tp-player/mainwindow.cpp +++ b/client/tp-player/mainwindow.cpp @@ -87,8 +87,6 @@ MainWindow::MainWindow(QWidget *parent) : m_thr_play = nullptr; m_play_state = PLAY_STATE_UNKNOWN; - m_msg_box = nullptr; - ui->setupUi(this); ui->centralWidget->setMouseTracking(true); @@ -135,10 +133,6 @@ MainWindow::~MainWindow() m_thr_play = nullptr; } - if(m_msg_box) { - delete m_msg_box; - } - delete ui; } @@ -190,7 +184,7 @@ void MainWindow::paintEvent(QPaintEvent *e) { QRect rc_draw = e->rect(); - QRect rc(100, 100, m_img_message.width(), m_img_message.height()); + QRect rc(m_rc_message); //rc.moveTo(m_rc.left()+rc.left(), m_rc.top() + rc.top()); int from_x = max(rc_draw.left(), rc.left()) - rc.left(); @@ -243,9 +237,6 @@ void MainWindow::_do_update_data(update_data* dat) { UpdateDataHelper data_helper(dat); if(dat->data_type() == TYPE_DATA) { - if(m_msg_box) { - m_msg_box->hide(); - } if(dat->data_len() <= sizeof(TS_RECORD_PKG)) { qDebug() << "invalid record package(1)."; @@ -303,20 +294,28 @@ void MainWindow::_do_update_data(update_data* dat) { else if(dat->data_type() == TYPE_MESSAGE) { QPainter pp(&m_canvas); - QFontMetrics fm = pp.fontMetrics(); QRect rcWin(0, 0, m_canvas.width(), m_canvas.height()); - QRect rc = fm.boundingRect(rcWin, Qt::AlignLeft|Qt::TextWordWrap, dat->message()); - qDebug("message, w=%d, h=%d", rc.width(), rc.height()); -// int w = fm.width(dat->message()); -// int h = fm.height(); -// qDebug("message, w=%d, h=%d", w, h); + pp.drawText(rcWin, Qt::AlignLeft|Qt::TextDontPrint, dat->message(), &m_rc_message); - m_img_message = QPixmap(rc.width() + 30, rc.height() + 30); + qDebug("message, w=%d, h=%d", m_rc_message.width(), m_rc_message.height()); + m_rc_message.setWidth(m_rc_message.width()+60); + m_rc_message.setHeight(m_rc_message.height()+60); + + m_img_message = QPixmap(m_rc_message.width(), m_rc_message.height()); m_img_message.fill(Qt::transparent); QPainter pm(&m_img_message); pm.setPen(QColor(255,255,255,153)); - pm.fillRect(rc, QColor(0,0,0,190)); - pm.drawText(rc, Qt::AlignLeft|Qt::TextWordWrap, dat->message()); + pm.fillRect(m_rc_message, QColor(0,0,0,190)); + + QRect rcText(m_rc_message); + rcText.setLeft(30); + rcText.setTop(30); + pm.drawText(rcText, Qt::AlignLeft, dat->message()); + m_rc_message.moveTo( + (m_canvas.width() - m_rc_message.width())/2, + (m_canvas.height() - m_rc_message.height())/2 + ); + diff --git a/client/tp-player/mainwindow.h b/client/tp-player/mainwindow.h index dbcdb09..ca7fe78 100644 --- a/client/tp-player/mainwindow.h +++ b/client/tp-player/mainwindow.h @@ -8,7 +8,6 @@ #include "thr_play.h" #include "update_data.h" #include "record_format.h" -#include "dlgmessage.h" #define PLAY_STATE_UNKNOWN 0 #define PLAY_STATE_RUNNING 1 @@ -76,10 +75,8 @@ private: int m_play_state; - //QMessageBox* m_msg_box; - DlgMessage* m_msg_box; - QPixmap m_img_message; + QRect m_rc_message; }; #endif // MAINWINDOW_H diff --git a/client/tp-player/mainwindow.ui b/client/tp-player/mainwindow.ui index ecdf9ac..b296c52 100644 --- a/client/tp-player/mainwindow.ui +++ b/client/tp-player/mainwindow.ui @@ -10,6 +10,11 @@ 360 + + + 微软雅黑 Light + + Teleport Replayer diff --git a/client/tp-player/thr_play.cpp b/client/tp-player/thr_play.cpp index 9d133d0..1000653 100644 --- a/client/tp-player/thr_play.cpp +++ b/client/tp-player/thr_play.cpp @@ -75,6 +75,7 @@ void ThreadPlay::run() { qDebug() << "DOWNLOAD"; m_need_download = true; + // "正在缓存录像数据,请稍候..." _notify_message("正在缓存录像数据,请稍候..."); m_thr_download = new ThreadDownload(m_res); @@ -207,7 +208,9 @@ void ThreadPlay::run() { tpd_filename.sprintf("%stp-rdp-%d.tpd", path_base.toStdString().c_str(), fidx+1); // for test. - msg.sprintf("无法打开录像数据文件!\n\n%s", tpd_filename.toStdString().c_str()); + msg = QString::fromLocal8Bit("无法打开录像数据文件!\n\n"); + //msg.sprintf("无法打开录像数据文件!\n\n%s", tpd_filename.toStdString().c_str()); + msg += tpd_filename.toStdString().c_str(); _notify_message(msg); QFile f_dat(tpd_filename); diff --git a/client/tp-player/tp-player.pro b/client/tp-player/tp-player.pro index a7ca8e7..cffb1d5 100644 --- a/client/tp-player/tp-player.pro +++ b/client/tp-player/tp-player.pro @@ -1,34 +1,31 @@ -TEMPLATE = app -TARGET = tp-player - -QT += core gui widgets - -HEADERS += \ - dlgmessage.h \ - mainwindow.h \ - bar.h \ - thr_download.h \ - thr_play.h \ - update_data.h \ - record_format.h \ - rle.h - -SOURCES += \ - dlgmessage.cpp \ - main.cpp \ - mainwindow.cpp \ - bar.cpp \ - thr_download.cpp \ - thr_play.cpp \ - update_data.cpp \ - rle.c - -RESOURCES += \ - tp-player.qrc - -RC_FILE += \ - tp-player.rc - -FORMS += \ - dlgmessage.ui \ - mainwindow.ui +TEMPLATE = app +TARGET = tp-player + +QT += core gui widgets + +HEADERS += \ + mainwindow.h \ + bar.h \ + thr_download.h \ + thr_play.h \ + update_data.h \ + record_format.h \ + rle.h + +SOURCES += \ + main.cpp \ + mainwindow.cpp \ + bar.cpp \ + thr_download.cpp \ + thr_play.cpp \ + update_data.cpp \ + rle.c + +RESOURCES += \ + tp-player.qrc + +RC_FILE += \ + tp-player.rc + +FORMS += \ + mainwindow.ui From a4b0fec814e97e66e5ed1e4157d6a92265cea70d Mon Sep 17 00:00:00 2001 From: Apex Liu Date: Sat, 21 Sep 2019 01:11:02 +0800 Subject: [PATCH 26/44] =?UTF-8?q?=E5=A4=9A=E4=B8=AA=E5=BD=95=E5=83=8F?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E6=96=87=E4=BB=B6=E6=9E=84=E6=88=90=E7=9A=84?= =?UTF-8?q?=E5=BD=95=E5=83=8F=E5=8F=AF=E4=BB=A5=E6=92=AD=E6=94=BE=E4=BA=86?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/tp-player/mainwindow.cpp | 19 +------------------ client/tp-player/thr_play.cpp | 10 +++++----- server/tp_core/common/base_record.h | 7 +++++-- 3 files changed, 11 insertions(+), 25 deletions(-) diff --git a/client/tp-player/mainwindow.cpp b/client/tp-player/mainwindow.cpp index 9c56f4f..cc032a2 100644 --- a/client/tp-player/mainwindow.cpp +++ b/client/tp-player/mainwindow.cpp @@ -313,25 +313,8 @@ void MainWindow::_do_update_data(update_data* dat) { pm.drawText(rcText, Qt::AlignLeft, dat->message()); m_rc_message.moveTo( (m_canvas.width() - m_rc_message.width())/2, - (m_canvas.height() - m_rc_message.height())/2 + (m_canvas.height() - m_rc_message.height())/3 ); - - - - - -// //QMessageBox::warning(nullptr, QGuiApplication::applicationDisplayName(), dat->message()); -// if(!m_msg_box) { -// m_msg_box = new DlgMessage(this); -// // 无窗口标题栏,无边框 -// m_msg_box->setWindowFlags(Qt::Dialog | Qt::MSWindowsFixedSizeDialogHint | Qt::ToolTip | Qt::FramelessWindowHint); -// // 设置成非模态 -// m_msg_box->setModal(false); -// } - -// m_msg_box->set_text(dat->message()); -// // 显示对话框 -// m_msg_box->show(); return; } diff --git a/client/tp-player/thr_play.cpp b/client/tp-player/thr_play.cpp index 1000653..06c97ed 100644 --- a/client/tp-player/thr_play.cpp +++ b/client/tp-player/thr_play.cpp @@ -207,11 +207,11 @@ void ThreadPlay::run() { QString tpd_filename; tpd_filename.sprintf("%stp-rdp-%d.tpd", path_base.toStdString().c_str(), fidx+1); - // for test. - msg = QString::fromLocal8Bit("无法打开录像数据文件!\n\n"); - //msg.sprintf("无法打开录像数据文件!\n\n%s", tpd_filename.toStdString().c_str()); - msg += tpd_filename.toStdString().c_str(); - _notify_message(msg); +// // for test. +// msg = QString::fromLocal8Bit("无法打开录像数据文件!\n\n"); +// //msg.sprintf("无法打开录像数据文件!\n\n%s", tpd_filename.toStdString().c_str()); +// msg += tpd_filename.toStdString().c_str(); +// _notify_message(msg); QFile f_dat(tpd_filename); if(!f_dat.open(QFile::ReadOnly)) { diff --git a/server/tp_core/common/base_record.h b/server/tp_core/common/base_record.h index 33bf2a9..d3303ea 100644 --- a/server/tp_core/common/base_record.h +++ b/server/tp_core/common/base_record.h @@ -7,8 +7,11 @@ #include -#define MAX_CACHE_SIZE 1048576 // 1M = 1024*1024*1 -#define MAX_SIZE_PER_FILE 4194304 // 4M = 1024*1024*4 +// #define MAX_CACHE_SIZE 1048576 // 1M = 1024*1024*1 +// #define MAX_SIZE_PER_FILE 4194304 // 4M = 1024*1024*4 +// for test. +#define MAX_CACHE_SIZE 524288 // 512KB = 512*1024 +#define MAX_SIZE_PER_FILE 1048576 // 4M = 1024*1024*1 #pragma pack(push,1) From ad5534fd9461f76baf1f77159c9a57886d824319 Mon Sep 17 00:00:00 2001 From: Apex Liu Date: Mon, 14 Oct 2019 03:41:35 +0800 Subject: [PATCH 27/44] =?UTF-8?q?=E5=A4=84=E7=90=86=E5=85=B3=E9=94=AE?= =?UTF-8?q?=E5=B8=A7=EF=BC=8C=E6=9C=AA=E5=AE=8C=E6=88=90.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/tp-player/mainwindow.cpp | 5 +++++ client/tp-player/mainwindow.h | 6 ++++++ client/tp-player/record_format.h | 4 +++- client/tp-player/thr_play.cpp | 18 ++++++++++++++---- client/tp-player/tp-player.pro | 6 ++++-- server/tp_core/common/base_record.h | 11 +++++++---- 6 files changed, 39 insertions(+), 11 deletions(-) diff --git a/client/tp-player/mainwindow.cpp b/client/tp-player/mainwindow.cpp index cc032a2..6c4f01d 100644 --- a/client/tp-player/mainwindow.cpp +++ b/client/tp-player/mainwindow.cpp @@ -28,6 +28,7 @@ bool rdpimg2QImage(QImage& out, int w, int h, int bitsPerPixel, bool isCompresse break; case 16: if(isCompressed) { + uint8_t* _dat = (uint8_t*)calloc(1, w*h*2); if(!bitmap_decompress2(_dat, w, h, dat, len)) { free(_dat); @@ -35,6 +36,8 @@ bool rdpimg2QImage(QImage& out, int w, int h, int bitsPerPixel, bool isCompresse } // TODO: 这里需要进一步优化,直接操作QImage的buffer。 +// QTime t1; +// t1.start(); out = QImage(w, h, QImage::Format_RGB16); for(int y = 0; y < h; y++) { @@ -46,6 +49,7 @@ bool rdpimg2QImage(QImage& out, int w, int h, int bitsPerPixel, bool isCompresse out.setPixelColor(x, y, QColor(r,g,b)); } } +// qDebug("parse: %dB, %dms", len, t1.elapsed()); free(_dat); } @@ -381,6 +385,7 @@ void MainWindow::_do_update_data(update_data* dat) { else if(dat->data_type() == TYPE_END) { m_bar.end(); m_play_state = PLAY_STATE_STOP; + return; } } diff --git a/client/tp-player/mainwindow.h b/client/tp-player/mainwindow.h index ca7fe78..b043f19 100644 --- a/client/tp-player/mainwindow.h +++ b/client/tp-player/mainwindow.h @@ -8,6 +8,7 @@ #include "thr_play.h" #include "update_data.h" #include "record_format.h" +#include "util.h" #define PLAY_STATE_UNKNOWN 0 #define PLAY_STATE_RUNNING 1 @@ -77,6 +78,11 @@ private: QPixmap m_img_message; QRect m_rc_message; + + + // for test + TimeUseTest m_time_imgconvert_normal; + TimeUseTest m_time_imgconvert_compressed; }; #endif // MAINWINDOW_H diff --git a/client/tp-player/record_format.h b/client/tp-player/record_format.h index cbb122c..0bfa0d8 100644 --- a/client/tp-player/record_format.h +++ b/client/tp-player/record_format.h @@ -5,6 +5,7 @@ #define TS_RECORD_TYPE_RDP_POINTER 0x12 // 鼠标坐标位置改变,用于绘制虚拟鼠标 #define TS_RECORD_TYPE_RDP_IMAGE 0x13 // 服务端返回的图像,用于展示 +#define TS_RECORD_TYPE_RDP_KEYFRAME 0x14 // #define TS_RDP_BTN_FREE 0 #define TS_RDP_BTN_PRESSED 1 @@ -17,10 +18,11 @@ typedef struct TS_RECORD_HEADER_INFO { uint32_t magic; // "TPPR" 标志 TelePort Protocol Record uint16_t ver; // 录像文件版本,从3.5.0开始,为4 + uint16_t type; // uint32_t packages; // 总包数 uint32_t time_ms; // 总耗时(毫秒) uint32_t dat_file_count; // 数据文件数量 - uint8_t _reserve[64-4-2-4-4-4]; + uint8_t _reserve[64-4-2-2-4-4-4]; }TS_RECORD_HEADER_INFO; #define ts_record_header_info_size sizeof(TS_RECORD_HEADER_INFO) diff --git a/client/tp-player/thr_play.cpp b/client/tp-player/thr_play.cpp index 06c97ed..e058419 100644 --- a/client/tp-player/thr_play.cpp +++ b/client/tp-player/thr_play.cpp @@ -216,7 +216,9 @@ void ThreadPlay::run() { QFile f_dat(tpd_filename); if(!f_dat.open(QFile::ReadOnly)) { qDebug() << "Can not open " << tpd_filename << " for read."; - msg.sprintf("无法打开录像数据文件!\n\n%s", tpd_filename.toStdString().c_str()); + // msg.sprintf("无法打开录像数据文件!\n\n%s", tpd_filename.toStdString().c_str()); + msg = QString::fromLocal8Bit("无法打开录像数据文件!\n\n"); + msg += tpd_filename.toStdString().c_str(); _notify_error(msg); return; } @@ -239,10 +241,15 @@ void ThreadPlay::run() { break; if(read_len != sizeof(TS_RECORD_PKG)) { qDebug() << "invaid .tpd file (1)."; - msg.sprintf("错误的录像数据文件!\n\n%s", tpd_filename.toStdString().c_str()); + // msg.sprintf("错误的录像数据文件!\n\n%s", tpd_filename.toStdString().c_str()); + msg = QString::fromLocal8Bit("错误的录像数据文件!\n\n"); + msg += tpd_filename.toStdString().c_str(); _notify_error(msg); return; } + if(pkg.type == TS_RECORD_TYPE_RDP_KEYFRAME) { + qDebug("----key frame: %d", pkg.time_ms); + } update_data* dat = new update_data(TYPE_DATA); dat->alloc_data(sizeof(TS_RECORD_PKG) + pkg.size); @@ -251,7 +258,9 @@ void ThreadPlay::run() { if(read_len != pkg.size) { delete dat; qDebug() << "invaid .tpd file."; - msg.sprintf("错误的录像数据文件!\n\n%s", tpd_filename.toStdString().c_str()); + // msg.sprintf("错误的录像数据文件!\n\n%s", tpd_filename.toStdString().c_str()); + msg = QString::fromLocal8Bit("错误的录像数据文件!\n\n"); + msg += tpd_filename.toStdString().c_str(); _notify_error(msg); return; } @@ -316,7 +325,8 @@ void ThreadPlay::run() { if(pkg_count < total_pkg) { qDebug() << "total-pkg:" << total_pkg << ", played:" << pkg_count; - msg.sprintf("录像数据文件有误!\n\n部分录像数据缺失!"); + // msg.sprintf("录像数据文件有误!\n\n部分录像数据缺失!"); + msg = QString::fromLocal8Bit("录像数据文件有误!\n\n部分录像数据缺失!"); _notify_message(msg); } diff --git a/client/tp-player/tp-player.pro b/client/tp-player/tp-player.pro index cffb1d5..68abd41 100644 --- a/client/tp-player/tp-player.pro +++ b/client/tp-player/tp-player.pro @@ -10,7 +10,8 @@ HEADERS += \ thr_play.h \ update_data.h \ record_format.h \ - rle.h + rle.h \ + util.h SOURCES += \ main.cpp \ @@ -19,7 +20,8 @@ SOURCES += \ thr_download.cpp \ thr_play.cpp \ update_data.cpp \ - rle.c + rle.c \ + util.cpp RESOURCES += \ tp-player.qrc diff --git a/server/tp_core/common/base_record.h b/server/tp_core/common/base_record.h index d3303ea..63b1815 100644 --- a/server/tp_core/common/base_record.h +++ b/server/tp_core/common/base_record.h @@ -11,7 +11,7 @@ // #define MAX_SIZE_PER_FILE 4194304 // 4M = 1024*1024*4 // for test. #define MAX_CACHE_SIZE 524288 // 512KB = 512*1024 -#define MAX_SIZE_PER_FILE 1048576 // 4M = 1024*1024*1 +#define MAX_SIZE_PER_FILE 1048576 // 1M = 1024*1024*1 #pragma pack(push,1) @@ -27,15 +27,19 @@ * */ +#define TS_TPPR_TYPE_UNKNOWN 0x0000 +#define TS_TPPR_TYPE_SSH 0x0001 +#define TS_TPPR_TYPE_RDP 0x0101 // 录像文件头(随着录像数据写入,会改变的部分) typedef struct TS_RECORD_HEADER_INFO { ex_u32 magic; // "TPPR" 标志 TelePort Protocol Record ex_u16 ver; // 录像文件版本,v3.5.0开始为4 + ex_u16 type; // 录像内容,SSH or RDP ex_u32 packages; // 总包数 ex_u32 time_ms; // 总耗时(毫秒) ex_u32 dat_file_count; // 数据文件数量 - ex_u8 _reserve[64-4-2-4-4-4]; + ex_u8 _reserve[64-4-2-2-4-4-4]; }TS_RECORD_HEADER_INFO; #define ts_record_header_info_size sizeof(TS_RECORD_HEADER_INFO) @@ -58,7 +62,7 @@ typedef struct TS_RECORD_HEADER_BASIC { // // RDP专有 - v3.5.0废弃并移除 // ex_u8 rdp_security; // 0 = RDP, 1 = TLS - ex_u8 _reserve[512 - 2 - 2 - 8 - 2 - 2 - 64 - 64 - 40 - 40 - 2 - 40 - ts_record_header_info_size]; + ex_u8 _reserve[512 - ts_record_header_info_size - 2 - 2 - 8 - 2 - 2 - 64 - 64 - 40 - 40 - 2 - 40]; }TS_RECORD_HEADER_BASIC; #define ts_record_header_basic_size sizeof(TS_RECORD_HEADER_BASIC) @@ -70,7 +74,6 @@ typedef struct TS_RECORD_HEADER { // header部分(header-info + header-basic) = 512B #define ts_record_header_size sizeof(TS_RECORD_HEADER) - // 一个数据包的头 typedef struct TS_RECORD_PKG { ex_u8 type; // 包的数据类型 From 2f7fc1623527adf782429ee06126a18b47c966b4 Mon Sep 17 00:00:00 2001 From: Apex Liu Date: Fri, 1 Nov 2019 01:45:17 +0800 Subject: [PATCH 28/44] .temp. --- client/tp-player/bar.cpp | 2 +- client/tp-player/bar.h | 2 +- client/tp-player/downloader.cpp | 106 +++ client/tp-player/downloader.h | 50 ++ client/tp-player/main.cpp | 73 +- client/tp-player/mainwindow.cpp | 117 ++- client/tp-player/mainwindow.h | 19 +- client/tp-player/record_format.h | 9 +- client/tp-player/rle.c | 2 +- client/tp-player/thr_data.cpp | 665 ++++++++++++++++++ .../tp-player/{thr_download.h => thr_data.h} | 72 +- client/tp-player/thr_download.cpp | 34 - client/tp-player/thr_play.cpp | 85 ++- client/tp-player/thr_play.h | 20 +- client/tp-player/tp-player.pro | 12 +- client/tp-player/update_data.cpp | 10 +- client/tp-player/update_data.h | 10 +- client/tp-player/util.cpp | 0 client/tp-player/util.h | 33 + server/tp_core/common/base_record.h | 9 +- .../teleport/webroot/app/controller/audit.py | 13 +- 21 files changed, 1189 insertions(+), 154 deletions(-) create mode 100644 client/tp-player/downloader.cpp create mode 100644 client/tp-player/downloader.h create mode 100644 client/tp-player/thr_data.cpp rename client/tp-player/{thr_download.h => thr_data.h} (51%) delete mode 100644 client/tp-player/thr_download.cpp create mode 100644 client/tp-player/util.cpp create mode 100644 client/tp-player/util.h diff --git a/client/tp-player/bar.cpp b/client/tp-player/bar.cpp index 39b052f..14044a3 100644 --- a/client/tp-player/bar.cpp +++ b/client/tp-player/bar.cpp @@ -1,4 +1,4 @@ -#include "bar.h" +#include "bar.h" #include #include #include "mainwindow.h" diff --git a/client/tp-player/bar.h b/client/tp-player/bar.h index 8ff8804..8d61a2f 100644 --- a/client/tp-player/bar.h +++ b/client/tp-player/bar.h @@ -1,4 +1,4 @@ -#ifndef BAR_H +#ifndef BAR_H #define BAR_H #include diff --git a/client/tp-player/downloader.cpp b/client/tp-player/downloader.cpp new file mode 100644 index 0000000..bee544d --- /dev/null +++ b/client/tp-player/downloader.cpp @@ -0,0 +1,106 @@ +#include "downloader.h" +#include "record_format.h" + +#include +#include + + +// TODO: 将Downloader的实现代码迁移到ThrData线程中 +// 使用局部event循环的方式进行下载 +/* +QEventLoop eventLoop; +connect(netWorker, &NetWorker::finished, + &eventLoop, &QEventLoop::quit); +QNetworkReply *reply = netWorker->get(url); +replyMap.insert(reply, FetchWeatherInfo); +eventLoop.exec(); +*/ + + +//================================================================= +// Downloader +//================================================================= +Downloader::Downloader() { + m_reply = nullptr; + m_code = codeUnknown; +} + +Downloader::~Downloader() { +} + +void Downloader::run(QNetworkAccessManager* nam, QString& url, QString& sid, QString& filename) { + m_filename = filename; + + if(!m_filename.isEmpty()) { + m_file.setFileName(m_filename); + if(!m_file.open(QIODevice::WriteOnly | QFile::Truncate)){ + qDebug("open file for write failed."); + return; + } + } + + m_code = codeDownloading; + QString cookie = QString("_sid=%1\r\n").arg(sid); + + QNetworkRequest req; + req.setUrl(QUrl(url)); + req.setRawHeader("Cookie", cookie.toLatin1()); + + m_reply = nam->get(req); + connect(m_reply, &QNetworkReply::finished, this, &Downloader::_on_finished); + connect(m_reply, &QIODevice::readyRead, this, &Downloader::_on_data_ready); +} + +void Downloader::_on_data_ready() { + QNetworkReply *reply = reinterpret_cast(sender()); + + if(m_filename.isEmpty()) { + m_data += reply->readAll(); + } + else { + m_file.write(reply->readAll()); + } +} + +void Downloader::reset() { + m_code = codeUnknown; +} + +void Downloader::abort() { + if(m_reply) + m_reply->abort(); +} + +void Downloader::_on_finished() { + qDebug("download finished"); + QNetworkReply *reply = reinterpret_cast(sender()); + + QVariant statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); + + if (reply->error() != QNetworkReply::NoError) { + // reply->abort() got "Operation canceled" + //QString strError = reply->errorString(); + //qDebug() << strError; + m_file.flush(); + m_file.close(); + m_code = codeFailed; + return; + } + + disconnect(m_reply, &QNetworkReply::finished, this, &Downloader::_on_finished); + disconnect(m_reply, &QIODevice::readyRead, this, &Downloader::_on_data_ready); +// reply->deleteLater(); + + if(m_filename.isEmpty()) { + m_data += reply->readAll(); + } + else { + m_file.write(reply->readAll()); + m_file.flush(); + m_file.close(); + } + + reply->deleteLater(); + + m_code = codeSuccess; +} diff --git a/client/tp-player/downloader.h b/client/tp-player/downloader.h new file mode 100644 index 0000000..38dcbdf --- /dev/null +++ b/client/tp-player/downloader.h @@ -0,0 +1,50 @@ +#ifndef DOWNLOADER_H +#define DOWNLOADER_H + +#include +#include + +class Downloader : public QObject { + Q_OBJECT + +public: + enum EndCode{ + codeUnknown = 0, + codeSuccess, + codeDownloading, + codeFailed + }; + +public: + // 从url下载数据,写入到filename文件中,如果filename为空字符串,则保存在内存中,可通过 data() 获取。 + Downloader(); + ~Downloader(); + + void run(QNetworkAccessManager* nam, QString& url, QString& sid, QString& filename); + void abort(); + void reset(); + QByteArray& data(){return m_data;} + + EndCode code() {return m_code;} + +private slots: + void _on_data_ready(); // 有数据可读了,读取并写入文件 + void _on_finished(); // 下载结束了 + +private: + QString m_filename; + QFile m_file; + QByteArray m_data; + + QNetworkReply* m_reply; + + EndCode m_code; +}; + +typedef struct DownloadParam { + QString url; + QString sid; + QString fname; +}DownloadParam; + +#endif diff --git a/client/tp-player/main.cpp b/client/tp-player/main.cpp index 2131644..486d0c5 100644 --- a/client/tp-player/main.cpp +++ b/client/tp-player/main.cpp @@ -8,14 +8,63 @@ // 编译出来的可执行程序复制到单独目录,然后执行 windeployqt 应用程序文件名 // 即可自动将依赖的动态库等复制到此目录中。有些文件是多余的,可以酌情删除。 +// 命令行参数格式: +// ## 本地文件或目录 +// tp-player.exe path/of/tp-rdp.tpr 一个 .tpr 文件的文件名 +// tp-player.exe path/contains/tp-rdp.tpr 包含 .tpr 文件的路径 +// +// ## 从TP服务器上下载 +// (废弃) tp-player.exe "http://127.0.0.1:7190" 1234 "tp_1491560510_ca67fceb75a78c9d" "000000256-admin-administrator-218.244.140.14-20171209-020047" +// (废弃) TP服务器地址 记录编号 session-id(仅授权用户可下载) 合成的名称,用于本地生成路径来存放下载的文件 +// +// ## 从TP服务器上下载 +// tp-player.exe http://teleport.domain.com:7190/{sub/path/}tp_1491560510_ca67fceb75a78c9d/1234 (注意,并不直接访问此URI,实际上其并不存在) +// TP服务器地址(可能包含子路径哦,例如上例中的{sub/path}部分)/session-id(用于判断当前授权用户)/录像会话编号 +// 按 “/” 进行分割后,去掉最后两个项,剩下部分是TP服务器的WEB地址,用于合成后续的文件下载URL。 +// 根据下载的.tpr文件内容,本地合成类似于 "000000256-admin-administrator-218.244.140.14-20171209-020047" 的路径来存放下载的文件 +// 特别注意,如果账号是 domain\user 这种形式,需要将 "\" 替换为下划线,否则此符号作为路径分隔符,会导致路径不存在而无法保存下载的文件。 +// - 获取文件大小: http://127.0.0.1:7190/audit/get-file?act=size&type=rdp&rid=yyyyy&f=file-name +// - 'act'为`size`表示获取文件大小(返回一个数字字符串,就是指定的文件大小) +// - 'type'可以是`rdp`或`ssh`,目前仅用了`rdp` +// - 'rid'是录像会话编号(在服务端,一个会话的录像文件均放在录像会话编号命名的目录下) +// - 'f' 是文件名,即会话编号目录下的指定文件,例如 'tp-rdp.tpr' +// - 读取文件内容: http://127.0.0.1:7190/audit/get-file?act=read&type=rdp&rid=yyyyy&f=file-name&offset=1234&length=1024 +// - 'act'为`read`表示获取文件内容 +// - 'offset'表示要读取的偏移,如果未指定,则表示从文件起始处开始读取,即默认为 offset=0 +// - 'length'表示要读取的大小,如果未指定,表示读取整个文件,即默认为 length=-1(服务端对length=-1做完全读取处理) +// - 搭配使用 offst 和 length 可以做到分块下载、断点续传。 + + +void show_usage(QCommandLineParser& parser) { + QMessageBox::warning(nullptr, QGuiApplication::applicationDisplayName(), + "
    "
    +                         + parser.helpText()
    +                         + "\n\n"
    +                         + "RESOURCE could be:\n"
    +                         + "    teleport record file (.tpr).\n"
    +                         + "    a directory contains .tpr file.\n"
    +                         + "    an URL to download teleport record file."
    +                         + "
    "); +} + int main(int argc, char *argv[]) { //#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) // QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); //#endif + QApplication a(argc, argv); +//#ifdef __APPLE__ +// QString data_path_base = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); +// data_path_base += "/tp-testdata/"; +//#else +// QString data_path_base = QCoreApplication::applicationDirPath() + "/record"; +//#endif +// qDebug("data-path-base: %s", data_path_base.toStdString().c_str()); +// return 0; + QGuiApplication::setApplicationDisplayName("TP-Player"); QCommandLineParser parser; @@ -32,19 +81,25 @@ int main(int argc, char *argv[]) } if(parser.isSet(opt_help)) { - QMessageBox::warning(nullptr, QGuiApplication::applicationDisplayName(), - "
    "
    -                             + parser.helpText()
    -                             + "\n\n"
    -                             + "RESOURCE could be:\n"
    -                             + "    teleport record file (.tpr).\n"
    -                             + "    a directory contains .tpr file.\n"
    -                             + "    an URL for download teleport record file."
    -                             + "
    "); + show_usage(parser); +// QMessageBox::warning(nullptr, QGuiApplication::applicationDisplayName(), +// "
    "
    +//                             + parser.helpText()
    +//                             + "\n\n"
    +//                             + "RESOURCE could be:\n"
    +//                             + "    teleport record file (.tpr).\n"
    +//                             + "    a directory contains .tpr file.\n"
    +//                             + "    an URL for download teleport record file."
    +//                             + "
    "); return 2; } const QStringList args = parser.positionalArguments(); + if(0 == args.size()) { + show_usage(parser); + return 2; + } + QString resource = args.at(0); qDebug() << resource; diff --git a/client/tp-player/mainwindow.cpp b/client/tp-player/mainwindow.cpp index 6c4f01d..7440262 100644 --- a/client/tp-player/mainwindow.cpp +++ b/client/tp-player/mainwindow.cpp @@ -1,4 +1,4 @@ -#include "mainwindow.h" +#include "mainwindow.h" #include "ui_mainwindow.h" #include "rle.h" @@ -86,10 +86,14 @@ MainWindow::MainWindow(QWidget *parent) : m_bar_fade_in = false; m_bar_fading = false; m_bar_opacity = 1.0; + m_show_message = false; memset(&m_pt, 0, sizeof(TS_RECORD_RDP_POINTER)); m_thr_play = nullptr; m_play_state = PLAY_STATE_UNKNOWN; + m_thr_data = nullptr; + + m_dl = nullptr; ui->setupUi(this); @@ -107,14 +111,20 @@ MainWindow::MainWindow(QWidget *parent) : m_pt_normal.load(":/tp-player/res/cursor.png"); m_default_bg.load(":/tp-player/res/bg.png"); + m_canvas = QPixmap(m_default_bg.width(), m_default_bg.height()); + QPainter pp(&m_canvas); + pp.drawPixmap(0, 0, m_default_bg, 0, 0, m_default_bg.width(), m_default_bg.height()); + + setWindowFlags(windowFlags()&~Qt::WindowMaximizeButtonHint); // 禁止最大化按钮 - setFixedSize(m_default_bg.width(), m_default_bg.height()); // 禁止拖动窗口大小 + setFixedSize(m_default_bg.width(), m_default_bg.height()); // 禁止拖动窗口大小 if(!m_bar.init(this)) { qDebug("bar init failed."); return; } + // connect(&m_thr_play, SIGNAL(signal_update_data(update_data*)), this, SLOT(_do_update_data(update_data*))); connect(&m_timer_first_run, SIGNAL(timeout()), this, SLOT(_do_first_run())); connect(&m_timer_bar_fade, SIGNAL(timeout()), this, SLOT(_do_bar_fade())); @@ -131,12 +141,25 @@ MainWindow::~MainWindow() //m_thr_play->wait(); //qDebug() << "play thread stoped."; - disconnect(m_thr_play, SIGNAL(signal_update_data(update_data*)), this, SLOT(_do_update_data(update_data*))); + disconnect(m_thr_play, SIGNAL(signal_update_data(UpdateData*)), this, SLOT(_do_update_data(UpdateData*))); delete m_thr_play; m_thr_play = nullptr; } + if(m_thr_data) { + m_thr_data->stop(); + disconnect(m_thr_data, SIGNAL(signal_update_data(UpdateData*)), this, SLOT(_do_update_data(UpdateData*))); + disconnect(m_thr_data, SIGNAL(signal_download(DownloadParam*)), this, SLOT(_do_download(DownloadParam*))); + delete m_thr_data; + m_thr_data = nullptr; + } + + if(m_dl) { + delete m_dl; + m_dl = nullptr; + } + delete ui; } @@ -145,6 +168,11 @@ void MainWindow::set_resource(const QString &res) { } void MainWindow::_do_first_run() { + m_thr_data = new ThrData(this, m_res); + connect(m_thr_data, SIGNAL(signal_update_data(UpdateData*)), this, SLOT(_do_update_data(UpdateData*))); + connect(m_thr_data, SIGNAL(signal_download(DownloadParam*)), this, SLOT(_do_download(DownloadParam*))); + m_thr_data->start(); + _start_play_thread(); } @@ -153,14 +181,15 @@ void MainWindow::_start_play_thread() { m_thr_play->stop(); //m_thr_play->wait(); - disconnect(m_thr_play, SIGNAL(signal_update_data(update_data*)), this, SLOT(_do_update_data(update_data*))); + disconnect(m_thr_play, SIGNAL(signal_update_data(UpdateData*)), this, SLOT(_do_update_data(UpdateData*))); delete m_thr_play; m_thr_play = nullptr; } - m_thr_play = new ThreadPlay(m_res); - connect(m_thr_play, SIGNAL(signal_update_data(update_data*)), this, SLOT(_do_update_data(update_data*))); + m_thr_play = new ThrPlay(); + connect(m_thr_play, SIGNAL(signal_update_data(UpdateData*)), this, SLOT(_do_update_data(UpdateData*))); + m_thr_play->speed(m_bar.get_speed()); m_thr_play->start(); } @@ -186,19 +215,19 @@ void MainWindow::paintEvent(QPaintEvent *e) painter.drawPixmap(m_pt.x-m_pt_normal.width()/2, m_pt.y-m_pt_normal.height()/2, m_pt_normal); } - { - QRect rc_draw = e->rect(); - QRect rc(m_rc_message); - //rc.moveTo(m_rc.left()+rc.left(), m_rc.top() + rc.top()); +// { +// QRect rc_draw = e->rect(); +// QRect rc(m_rc_message); +// //rc.moveTo(m_rc.left()+rc.left(), m_rc.top() + rc.top()); - int from_x = max(rc_draw.left(), rc.left()) - rc.left(); - int from_y = max(rc_draw.top(), rc.top()) - rc.top(); - int w = min(rc.right(), rc_draw.right()) - rc.left() - from_x + 1; - int h = min(rc.bottom(), rc_draw.bottom()) - rc.top() - from_y + 1; - int to_x = rc.left() + from_x; - int to_y = rc.top() + from_y; - painter.drawPixmap(to_x, to_y, m_img_message, from_x, from_y, w, h); - } +// int from_x = max(rc_draw.left(), rc.left()) - rc.left(); +// int from_y = max(rc_draw.top(), rc.top()) - rc.top(); +// int w = min(rc.right(), rc_draw.right()) - rc.left() - from_x + 1; +// int h = min(rc.bottom(), rc_draw.bottom()) - rc.top() - from_y + 1; +// int to_x = rc.left() + from_x; +// int to_y = rc.top() + from_y; +// painter.drawPixmap(to_x, to_y, m_img_message, from_x, from_y, w, h); +// } // 绘制浮动控制窗 if(m_bar_fading) { @@ -210,6 +239,20 @@ void MainWindow::paintEvent(QPaintEvent *e) } } + if(m_show_message) { + QRect rc_draw = e->rect(); + QRect rc(m_rc_message); + //rc.moveTo(m_rc.left()+rc.left(), m_rc.top() + rc.top()); + + int from_x = max(rc_draw.left(), rc.left()) - rc.left(); + int from_y = max(rc_draw.top(), rc.top()) - rc.top(); + int w = min(rc.right(), rc_draw.right()) - rc.left() - from_x + 1; + int h = min(rc.bottom(), rc_draw.bottom()) - rc.top() - from_y + 1; + int to_x = rc.left() + from_x; + int to_y = rc.top() + from_y; + painter.drawPixmap(to_x, to_y, m_img_message, from_x, from_y, w, h); + } + // if(!m_shown) { // m_shown = true; // //m_thr_play.start(); @@ -234,7 +277,7 @@ void MainWindow::resume() { m_play_state = PLAY_STATE_RUNNING; } -void MainWindow::_do_update_data(update_data* dat) { +void MainWindow::_do_update_data(UpdateData* dat) { if(!dat) return; @@ -297,11 +340,19 @@ void MainWindow::_do_update_data(update_data* dat) { } else if(dat->data_type() == TYPE_MESSAGE) { + m_show_message = true; + + qDebug("1message, w=%d, h=%d", m_canvas.width(), m_canvas.height()); +// if(0 == m_canvas.width()) { +// QMessageBox::warning(nullptr, QGuiApplication::applicationDisplayName(), dat->message()); +// return; +// } + QPainter pp(&m_canvas); QRect rcWin(0, 0, m_canvas.width(), m_canvas.height()); pp.drawText(rcWin, Qt::AlignLeft|Qt::TextDontPrint, dat->message(), &m_rc_message); - qDebug("message, w=%d, h=%d", m_rc_message.width(), m_rc_message.height()); + qDebug("2message, w=%d, h=%d", m_rc_message.width(), m_rc_message.height()); m_rc_message.setWidth(m_rc_message.width()+60); m_rc_message.setHeight(m_rc_message.height()+60); @@ -311,6 +362,11 @@ void MainWindow::_do_update_data(update_data* dat) { pm.setPen(QColor(255,255,255,153)); pm.fillRect(m_rc_message, QColor(0,0,0,190)); + QRect rcRect(m_rc_message); + rcRect.setWidth(rcRect.width()-1); + rcRect.setHeight(rcRect.height()-1); + pm.drawRect(rcRect); + QRect rcText(m_rc_message); rcText.setLeft(30); rcText.setTop(30); @@ -319,11 +375,14 @@ void MainWindow::_do_update_data(update_data* dat) { (m_canvas.width() - m_rc_message.width())/2, (m_canvas.height() - m_rc_message.height())/3 ); + + update(m_rc_message.x(), m_rc_message.y(), m_rc_message.width(), m_rc_message.height()); + return; } else if(dat->data_type() == TYPE_ERROR) { - QMessageBox::warning(nullptr, QGuiApplication::applicationDisplayName(), dat->message()); + QMessageBox::critical(this, QGuiApplication::applicationDisplayName(), dat->message()); QApplication::instance()->exit(0); return; } @@ -338,7 +397,7 @@ void MainWindow::_do_update_data(update_data* dat) { qDebug() << "resize (" << m_rec_hdr.basic.width << "," << m_rec_hdr.basic.height << ")"; - if(m_canvas.width() != m_rec_hdr.basic.width && m_canvas.height() != m_rec_hdr.basic.height) { + //if(m_canvas.width() != m_rec_hdr.basic.width && m_canvas.height() != m_rec_hdr.basic.height) { m_canvas = QPixmap(m_rec_hdr.basic.width, m_rec_hdr.basic.height); //m_win_board_w = frameGeometry().width() - geometry().width(); @@ -353,7 +412,7 @@ void MainWindow::_do_update_data(update_data* dat) { //resize(m_rec_hdr.basic.width + m_win_board_w, m_rec_hdr.basic.height + m_win_board_h); //resize(m_rec_hdr.basic.width, m_rec_hdr.basic.height); setFixedSize(m_rec_hdr.basic.width, m_rec_hdr.basic.height); - } + //} m_canvas.fill(QColor(38, 73, 111)); @@ -422,6 +481,18 @@ void MainWindow::_do_bar_fade() { update(m_bar.rc()); } +void MainWindow::_do_download(DownloadParam* param) { + qDebug("MainWindow::_do_download(). %s %s %s", param->url.toStdString().c_str(), param->sid.toStdString().c_str(), param->fname.toStdString().c_str()); + + if(m_dl) { + delete m_dl; + m_dl = nullptr; + } + + m_dl = new Downloader(); + m_dl->run(&m_nam, param->url, param->sid, param->fname); +} + void MainWindow::mouseMoveEvent(QMouseEvent *e) { if(!m_show_default) { QRect rc = m_bar.rc(); diff --git a/client/tp-player/mainwindow.h b/client/tp-player/mainwindow.h index b043f19..031fc2b 100644 --- a/client/tp-player/mainwindow.h +++ b/client/tp-player/mainwindow.h @@ -1,4 +1,4 @@ -#ifndef MAINWINDOW_H +#ifndef MAINWINDOW_H #define MAINWINDOW_H #include @@ -6,9 +6,11 @@ #include #include "bar.h" #include "thr_play.h" +#include "thr_data.h" #include "update_data.h" #include "record_format.h" #include "util.h" +#include "downloader.h" #define PLAY_STATE_UNKNOWN 0 #define PLAY_STATE_RUNNING 1 @@ -34,6 +36,9 @@ public: void restart(); void speed(int s); + Downloader* downloader() {return m_dl;} + void reset_downloader() {if(m_dl){delete m_dl;m_dl= nullptr;}} + private: void paintEvent(QPaintEvent *e); void mouseMoveEvent(QMouseEvent *e); @@ -43,10 +48,13 @@ private: private slots: void _do_first_run(); // 默认界面加载完成后,开始播放操作(可能会进行数据下载) - void _do_update_data(update_data*); + void _do_update_data(UpdateData*); void _do_bar_fade(); void _do_bar_delay_hide(); +// void _do_download(Downloader*); + void _do_download(DownloadParam*); + private: Ui::MainWindow *ui; @@ -56,7 +64,8 @@ private: QPixmap m_default_bg; QString m_res; - ThreadPlay* m_thr_play; + ThrPlay* m_thr_play; + ThrData* m_thr_data; QPixmap m_canvas; @@ -76,10 +85,14 @@ private: int m_play_state; + bool m_show_message; QPixmap m_img_message; QRect m_rc_message; + QNetworkAccessManager m_nam; + Downloader* m_dl; + // for test TimeUseTest m_time_imgconvert_normal; TimeUseTest m_time_imgconvert_compressed; diff --git a/client/tp-player/record_format.h b/client/tp-player/record_format.h index 0bfa0d8..68d7ef9 100644 --- a/client/tp-player/record_format.h +++ b/client/tp-player/record_format.h @@ -60,10 +60,11 @@ typedef struct TS_RECORD_HEADER { // 一个数据包的头 typedef struct TS_RECORD_PKG { - uint8_t type; // 包的数据类型 - uint32_t size; // 这个包的总大小(不含包头) - uint32_t time_ms; // 这个包距起始时间的时间差(毫秒,意味着一个连接不能持续超过49天) - uint8_t _reserve[3]; // 保留 + uint8_t type; // 包的数据类型 + uint8_t _reserve[3]; // 保留 + uint32_t size; // 这个包的总大小(不含包头) + uint32_t time_ms; // 这个包距起始时间的时间差(毫秒,意味着一个连接不能持续超过49天) + uint32_t index; // 这个包的序号(最后一个包的序号与TS_RECORD_HEADER_INFO::packages数量匹配) }TS_RECORD_PKG; diff --git a/client/tp-player/rle.c b/client/tp-player/rle.c index 6906e4e..ce9fd03 100644 --- a/client/tp-player/rle.c +++ b/client/tp-player/rle.c @@ -1,4 +1,4 @@ -/* -*- c-basic-offset: 8 -*- +/* -*- c-basic-offset: 8 -*- rdesktop: A Remote Desktop Protocol client. Bitmap decompression routines Copyright (C) Matthew Chapman 1999-2008 diff --git a/client/tp-player/thr_data.cpp b/client/tp-player/thr_data.cpp new file mode 100644 index 0000000..f0b0987 --- /dev/null +++ b/client/tp-player/thr_data.cpp @@ -0,0 +1,665 @@ +#include +#include +#include +#include +#include +#include + +#include "thr_play.h" +#include "thr_data.h" +#include "util.h" +#include "record_format.h" +#include "mainwindow.h" + + +//================================================================= +// ThrData +//================================================================= + +ThrData::ThrData(MainWindow* mainwin, const QString& res) { + m_mainwin = mainwin; + m_res = res; + m_need_download = false; + m_need_stop = false; + +#ifdef __APPLE__ + QString data_path_base = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); + data_path_base += "/tp-testdata/"; +#else + m_local_data_path_base = QCoreApplication::applicationDirPath() + "/record"; +#endif + qDebug("data-path-base: %s", m_local_data_path_base.toStdString().c_str()); + + // qDebug() << "AppConfigLocation:" << QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation); + // qDebug() << "AppDataLocation:" << QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + // qDebug() << "AppLocalDataLocation:" << QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation); + // qDebug() << "ConfigLocation:" << QStandardPaths::writableLocation(QStandardPaths::ConfigLocation); + // qDebug() << "CacheLocation:" << QStandardPaths::writableLocation(QStandardPaths::CacheLocation); + // qDebug() << "GenericCacheLocation:" << QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation); + + /* +AppConfigLocation: "C:/Users/apex/AppData/Local/tp-player" +AppDataLocation: "C:/Users/apex/AppData/Roaming/tp-player" +AppLocalDataLocation: "C:/Users/apex/AppData/Local/tp-player" +ConfigLocation: "C:/Users/apex/AppData/Local/tp-player" +CacheLocation: "C:/Users/apex/AppData/Local/tp-player/cache" +GenericCacheLocation: "C:/Users/apex/AppData/Local/cache" + */ +} + +ThrData::~ThrData() { +} + +void ThrData::stop() { + if(!isRunning()) + return; + m_need_stop = true; + wait(); + qDebug("data thread stop() end."); +} + +void ThrData::_notify_message(const QString& msg) { + UpdateData* _msg = new UpdateData(TYPE_MESSAGE); + _msg->message(msg); + emit signal_update_data(_msg); +} + +void ThrData::_notify_error(const QString& msg) { + UpdateData* _msg = new UpdateData(TYPE_ERROR); + _msg->message(msg); + emit signal_update_data(_msg); +} + +void ThrData::_notify_download(DownloadParam* param) { + emit signal_download(param); +} + +// tp-player.exe http://teleport.domain.com:7190/{sub/path/}tp_1491560510_ca67fceb75a78c9d/1234 (注意,并不直接访问此URI,实际上其并不存在) +// TP服务器地址(可能包含子路径哦,例如上例中的{sub/path/}部分)/session-id(用于判断当前授权用户)/录像会话编号 + +void ThrData::run() { + if(!_load_header()) + return; + + if(!_load_keyframe()) + return; + + for(;;) { + if(m_need_stop) + break; + msleep(500); + } + + qDebug("ThrData thread run() end."); +} + +bool ThrData::_load_header() { + QString msg; + QString _tmp_res = m_res.toLower(); + + if(_tmp_res.startsWith("http")) { + m_need_download = true; + } + + if(m_need_download) { + _notify_message(LOCAL8BIT("正在准备录像数据,请稍候...")); + + QStringList _uris = m_res.split('/'); + if(_uris.size() < 3) { + qDebug() << "invalid param: " << m_res; + return false; + } + + m_sid = _uris[_uris.size()-2]; + m_rid = _uris[_uris.size()-1]; + m_url_base = m_res.left(m_res.length() - m_sid.length() - m_rid.length() - 2); + + qDebug() << "url-base:[" << m_url_base << "], sid:[" << m_sid << "], rid:[" << m_rid << "]"; + + // download .tpr + QString url(m_url_base); + url += "/audit/get-file?act=read&type=rdp&rid="; + url += m_rid; + url += "&f=tp-rdp.tpr"; + + QString fname; + if(!_download_file(url, fname)) + return false; + +// Downloader& dl = m_mainwin->downloader(); +// dl.reset(); + +// DownloadParam param; +// param.url = url; +// param.sid = m_sid; +// param.fname = fname; +// _notify_download(¶m); + +// for(;;) { +// if(dl.code() == Downloader::codeUnknown || dl.code() == Downloader::codeDownloading) { +// msleep(100); +// continue; +// } + +// break; +// } + +// if(dl.code() != Downloader::codeSuccess) { +// _notify_error(QString("%1").arg(LOCAL8BIT("下载文件失败!"))); +// return false; +// } + + Downloader* dl = m_mainwin->downloader(); + QByteArray& data = dl->data(); + if(data.size() != sizeof(TS_RECORD_HEADER)) { + qDebug("invalid header file. %d", data.size()); + _notify_error(QString("%1\n\n%2").arg(LOCAL8BIT("指定的文件或目录不存在!"), _tmp_res)); + return false; + } + + memcpy(&m_hdr, data.data(), sizeof(TS_RECORD_HEADER)); + } + else { + QFileInfo fi_chk_link(m_res); + if(fi_chk_link.isSymLink()) + _tmp_res = fi_chk_link.symLinkTarget(); + else + _tmp_res = m_res; + + QFileInfo fi(_tmp_res); + if(!fi.exists()) { + _notify_error(QString("%1\n\n%2").arg(LOCAL8BIT("指定的文件或目录不存在!"), _tmp_res)); + return false; + } + + if(fi.isFile()) { + m_path_base = fi.path(); + } + else if(fi.isDir()) { + m_path_base = _tmp_res; + } + + m_path_base = QDir::toNativeSeparators(m_path_base); + + qDebug() << "PATH_BASE: " << m_path_base; + + QString filename = QString("%1/tp-rdp.tpr").arg(m_path_base); + filename = QDir::toNativeSeparators(filename); + qDebug() << "TPR: " << filename; + + QFile f(filename); + if(!f.open(QFile::ReadOnly)) { + qDebug() << "Can not open " << filename << " for read."; + _notify_error(QString("%1\n\n%2").arg(LOCAL8BIT("无法打开录像信息文件!"), filename)); + return false; + } + + memset(&m_hdr, 0, sizeof(TS_RECORD_HEADER)); + + qint64 read_len = 0; + read_len = f.read(reinterpret_cast(&m_hdr), sizeof(TS_RECORD_HEADER)); + if(read_len != sizeof(TS_RECORD_HEADER)) { + qDebug() << "invaid .tpr file."; + _notify_error(QString("%1\n\n%2").arg(LOCAL8BIT("错误的录像信息文件!"), filename)); + return false; + } + } + + if(m_hdr.info.ver != 4) { + qDebug() << "invaid .tpr file."; + _notify_error(QString("%1 %2%3").arg(LOCAL8BIT("不支持的录像文件版本 "), QString(m_hdr.info.ver), LOCAL8BIT("!\n\n此播放器支持录像文件版本 4。"))); + return false; + } + + if(m_hdr.basic.width == 0 || m_hdr.basic.height == 0) { + _notify_error(LOCAL8BIT("错误的录像信息,未记录窗口尺寸!")); + return false; + } + + if(m_hdr.info.dat_file_count == 0) { + _notify_error(LOCAL8BIT("错误的录像信息,未记录数据文件数量!")); + return false; + } + + + // 下载得到的数据应该是一个TS_RECORD_HEADER,解析此数据,生成本地文件路径,并保存之。 + if(m_need_download) { + QDateTime timeUTC; + // timeUTC.setTimeSpec(Qt::UTC); + // timeUTC.setTime_t(m_hdr.basic.timestamp); + timeUTC.setSecsSinceEpoch(m_hdr.basic.timestamp); + QString strUTC = timeUTC.toString("yyyyMMdd-hhmmss"); + + QString strAcc(m_hdr.basic.acc_username); + int idx = strAcc.indexOf('\\'); + if(-1 != idx) { + QString _domain = strAcc.left(idx); + QString _user = strAcc.right(strAcc.length() - idx - 1); + strAcc = _user + "@" + _domain; + } + + // .../record/RDP-211-admin-user@domain-192.168.0.68-20191015-020243 + m_path_base = QString("%1/RDP-%2-%3-%4-%5-%6").arg(m_local_data_path_base, + m_rid, + m_hdr.basic.user_username, + strAcc, + m_hdr.basic.host_ip, + strUTC + ); + + m_path_base = QDir::toNativeSeparators(m_path_base); + + qDebug() << "PATH_BASE: " << m_path_base; + + QDir dir; + dir.mkpath(m_path_base); + QFileInfo fi; + fi.setFile(m_path_base); + if(!fi.isDir()) { + qDebug("can not create folder to save downloaded file."); + return false; + } + + QString filename = QString("%1/tp-rdp.tpr").arg(m_path_base); + filename = QDir::toNativeSeparators(filename); + qDebug() << "TPR: " << filename; + + QFile f; + f.setFileName(filename); + if(!f.open(QIODevice::WriteOnly | QFile::Truncate)){ + qDebug("open file for write failed."); + return false; + } + + qint64 written = f.write(reinterpret_cast(&m_hdr), sizeof(TS_RECORD_HEADER)); + f.flush(); + f.close(); + + if(written != sizeof(TS_RECORD_HEADER)) { + qDebug("save header file failed."); + return false; + } + } + + return true; +} + +bool ThrData::_load_keyframe() { + // _notify_error(QString("%1").arg(LOCAL8BIT("测试!"))); + + QString tpk_fname = QString("%1/tp-rdp.tpk").arg(m_path_base); + tpk_fname = QDir::toNativeSeparators(tpk_fname); + + if(m_need_download) { + // download .tpr + QString url(m_url_base); + url += "/audit/get-file?act=read&type=rdp&rid="; + url += m_rid; + url += "&f=tp-rdp.tpk"; + + QString tmp_fname = QString("%1/tp-rdp.tpk.downloading").arg(m_path_base); + tmp_fname = QDir::toNativeSeparators(tmp_fname); + qDebug() << "TPK(tmp): " << tmp_fname; + qDebug() << "TPK(out): " << tpk_fname; + + if(!_download_file(url, tmp_fname)) + return false; + + QFile::rename(tmp_fname, tpk_fname); + } + + QFile f_kf(tpk_fname); + if(!f_kf.open(QFile::ReadOnly)) { + qDebug() << "Can not open " << tpk_fname << " for read."; + _notify_error(QString("%1\n\n%3").arg(LOCAL8BIT("无法打开关键帧信息文件!"), tpk_fname)); + return false; + } + + qint64 fsize = f_kf.size(); + if(!fsize || fsize % sizeof(KEYFRAME_INFO) != 0) { + qDebug() << "Can not open " << tpk_fname << " for read."; + _notify_error(LOCAL8BIT("关键帧信息文件格式错误!")); + return false; + } + + qint64 read_len = 0; + int kf_count = fsize / sizeof(KEYFRAME_INFO); + for(int i = 0; i < kf_count; ++i) { + KEYFRAME_INFO kf; + memset(&kf, 0, sizeof(KEYFRAME_INFO)); + read_len = f_kf.read(reinterpret_cast(&kf), sizeof(KEYFRAME_INFO)); + if(read_len != sizeof(KEYFRAME_INFO)) { + qDebug() << "invaid .tpk file."; + _notify_error(LOCAL8BIT("关键帧信息文件格式错误!")); + return false; + } + + m_kf.push_back(kf); + } + + return true; +} + +bool ThrData::_download_file(const QString& url, const QString filename) { + if(!m_need_download) { + qDebug() << "download not necessary."; + return false; + } + + m_mainwin->reset_downloader(); + msleep(100); + + DownloadParam param; + param.url = url; + param.sid = m_sid; + param.fname = filename; + _notify_download(¶m); + + for(;;) { + Downloader* dl = m_mainwin->downloader(); + if(!dl || dl->code() == Downloader::codeUnknown || dl->code() == Downloader::codeDownloading) { + msleep(100); + continue; + } + + if(dl->code() != Downloader::codeSuccess) { + qDebug() << "download failed."; + _notify_error(QString("%1").arg(LOCAL8BIT("下载文件失败!"))); + return false; + } + else { + qDebug() << "download ok."; + return true; + } + } +} + +#if 0 +void ThrData::run() { + QString msg; + QString path_base; + + QString _tmp_res = m_res.toLower(); + + if(_tmp_res.startsWith("http")) { + qDebug() << "DOWNLOAD"; + m_need_download = true; + + // "正在缓存录像数据,请稍候..." + m_thr_play->_notify_message(LOCAL8BIT("正在下载录像数据,请稍候...")); + + // QString msg; + // for(;;) { + // msleep(500); + + // if(m_need_stop) + // return; + + // if(!m_thr_data->prepare(path_base, msg)) { + // msg.sprintf("指定的文件或目录不存在!\n\n%s", _tmp_res.toStdString().c_str()); + // _notify_error(msg); + // return; + // } + + // if(path_base.length()) + // break; + // } + } + else { + QFileInfo fi_chk_link(m_res); + if(fi_chk_link.isSymLink()) + _tmp_res = fi_chk_link.symLinkTarget(); + else + _tmp_res = m_res; + + QFileInfo fi(_tmp_res); + if(!fi.exists()) { + msg.sprintf(LOCAL8BIT("指定的文件或目录不存在!\n\n%s").toStdString().c_str(), _tmp_res.toStdString().c_str()); + m_thr_play->_notify_error(msg); + return; + } + + if(fi.isFile()) { + path_base = fi.path(); + } + else if(fi.isDir()) { + path_base = m_res; + } + + path_base += "/"; + } + + //====================================== + // 加载录像基本信息数据 + //====================================== + + QString tpr_filename(path_base); + tpr_filename += "tp-rdp.tpr"; + + QFile f_hdr(tpr_filename); + if(!f_hdr.open(QFile::ReadOnly)) { + qDebug() << "Can not open " << tpr_filename << " for read."; + msg.sprintf(LOCAL8BIT("无法打开录像信息文件!\n\n%s").toStdString().c_str(), tpr_filename.toStdString().c_str()); + m_thr_play->_notify_error(msg); + return; + } + + TS_RECORD_HEADER hdr; + memset(&hdr, 0, sizeof(TS_RECORD_HEADER)); + + qint64 read_len = 0; + read_len = f_hdr.read((char*)(&hdr), sizeof(TS_RECORD_HEADER)); + if(read_len != sizeof(TS_RECORD_HEADER)) { + qDebug() << "invaid .tpr file."; + msg.sprintf(LOCAL8BIT("错误的录像信息文件!\n\n%s").toStdString().c_str(), tpr_filename.toStdString().c_str()); + m_thr_play->_notify_error(msg); + return; + } + + if(hdr.info.ver != 4) { + qDebug() << "invaid .tpr file."; + msg.sprintf(LOCAL8BIT("不支持的录像文件版本 %d!\n\n此播放器支持录像文件版本 4。").toStdString().c_str(), hdr.info.ver); + m_thr_play->_notify_error(msg); + return; + } + + if(hdr.basic.width == 0 || hdr.basic.height == 0) { + m_thr_play->_notify_error(LOCAL8BIT("错误的录像信息,未记录窗口尺寸!")); + return; + } + + if(hdr.info.dat_file_count == 0) { + m_thr_play->_notify_error(LOCAL8BIT("错误的录像信息,未记录数据文件数量!")); + return; + } + + //====================================== + // 加载关键帧数据 + //====================================== + QString tpk_filename(path_base); + tpk_filename += "tp-rdp.tpk"; + + QFile f_kf(tpk_filename); + if(!f_kf.open(QFile::ReadOnly)) { + qDebug() << "Can not open " << tpk_filename << " for read."; + msg.sprintf(LOCAL8BIT("无法打开关键帧信息文件!\n\n%s").toStdString().c_str(), tpk_filename.toStdString().c_str()); + m_thr_play->_notify_error(msg); + return; + } + + qint64 fsize = f_kf.size(); + if(!fsize || fsize % sizeof(KEYFRAME_INFO) != 0) { + qDebug() << "Can not open " << tpk_filename << " for read."; + msg.sprintf(LOCAL8BIT("关键帧信息文件格式错误!\n\n").toStdString().c_str()); + m_thr_play->_notify_error(msg); + return; + } + + int kf_count = fsize / sizeof(KEYFRAME_INFO); + for(int i = 0; i < kf_count; ++i) { + KEYFRAME_INFO kf; + memset(&kf, 0, sizeof(KEYFRAME_INFO)); + read_len = f_kf.read((char*)(&kf), sizeof(KEYFRAME_INFO)); + if(read_len != sizeof(KEYFRAME_INFO)) { + qDebug() << "invaid .tpk file."; + msg.sprintf(LOCAL8BIT("关键帧信息文件格式错误!\n\n").toStdString().c_str()); + m_thr_play->_notify_error(msg); + return; + } + + m_kf.push_back(kf); + } + + //====================================== + // 读取并解析录像数据文件 + //====================================== + uint32_t fidx = 0; + while(!m_need_stop) { + + for(fidx = 0; fidx < hdr.info.dat_file_count; ++fidx) { + QString tpd_filename(path_base); + QString str_tmp; + + str_tmp.sprintf("tp-rdp-%d.tpd", fidx+1); + tpd_filename += str_tmp; + + QFileInfo fi(tpd_filename); + if(!fi.isFile()) { + // 文件不存在,如需下载,则启动下载函数并等待下载结束。(下载是异步的吗?) + } + + QFile f_dat(tpd_filename); + if(!f_dat.open(QFile::ReadOnly)) { + qDebug() << "Can not open " << tpd_filename << " for read."; + // msg.sprintf("无法打开录像数据文件!\n\n%s", tpd_filename.toStdString().c_str()); + msg = QString::fromLocal8Bit("无法打开录像数据文件!\n\n"); + msg += tpd_filename; + m_thr_play->_notify_error(msg); + return; + } + + + + for(;;) { + if(m_need_stop) { + qDebug() << "stop, user cancel 2."; + break; + } + + if(m_need_pause) { + msleep(50); + time_begin += 50; + continue; + } + + TS_RECORD_PKG pkg; + read_len = f_dat.read((char*)(&pkg), sizeof(pkg)); + if(read_len == 0) + break; + if(read_len != sizeof(TS_RECORD_PKG)) { + qDebug() << "invaid .tpd file (1)."; + // msg.sprintf("错误的录像数据文件!\n\n%s", tpd_filename.toStdString().c_str()); + msg = QString::fromLocal8Bit("错误的录像数据文件!\n\n"); + msg += tpd_filename.toStdString().c_str(); + _notify_error(msg); + return; + } + if(pkg.type == TS_RECORD_TYPE_RDP_KEYFRAME) { + qDebug("----key frame: %d", pkg.time_ms); + } + + UpdateData* dat = new UpdateData(TYPE_DATA); + dat->alloc_data(sizeof(TS_RECORD_PKG) + pkg.size); + memcpy(dat->data_buf(), &pkg, sizeof(TS_RECORD_PKG)); + read_len = f_dat.read((char*)(dat->data_buf()+sizeof(TS_RECORD_PKG)), pkg.size); + if(read_len != pkg.size) { + delete dat; + qDebug() << "invaid .tpd file."; + // msg.sprintf("错误的录像数据文件!\n\n%s", tpd_filename.toStdString().c_str()); + msg = QString::fromLocal8Bit("错误的录像数据文件!\n\n"); + msg += tpd_filename.toStdString().c_str(); + _notify_error(msg); + return; + } + + pkg_count++; + + time_pass = (uint32_t)(QDateTime::currentMSecsSinceEpoch() - time_begin) * m_speed; + if(time_pass > total_ms) + time_pass = total_ms; + if(time_pass - time_last_pass > 200) { + UpdateData* _passed_ms = new UpdateData(TYPE_PLAYED_MS); + _passed_ms->played_ms(time_pass); + emit signal_update_data(_passed_ms); + time_last_pass = time_pass; + } + + if(time_pass >= pkg.time_ms) { + emit signal_update_data(dat); + continue; + } + + // 需要等待 + uint32_t time_wait = pkg.time_ms - time_pass; + uint32_t wait_this_time = 0; + for(;;) { + if(m_need_pause) { + msleep(50); + time_begin += 50; + continue; + } + + wait_this_time = time_wait; + if(wait_this_time > 10) + wait_this_time = 10; + + if(m_need_stop) { + qDebug() << "stop, user cancel (2)."; + break; + } + + msleep(wait_this_time); + + uint32_t _time_pass = (uint32_t)(QDateTime::currentMSecsSinceEpoch() - time_begin) * m_speed; + if(_time_pass > total_ms) + _time_pass = total_ms; + if(_time_pass - time_last_pass > 200) { + UpdateData* _passed_ms = new UpdateData(TYPE_PLAYED_MS); + _passed_ms->played_ms(_time_pass); + emit signal_update_data(_passed_ms); + time_last_pass = _time_pass; + } + + time_wait -= wait_this_time; + if(time_wait == 0) { + emit signal_update_data(dat); + break; + } + } + + } + } + } + + + // msg = LOCAL8BIT("开始播放..."); + // m_thr_play->_notify_error(msg); +} +#endif + +void ThrData::_prepare() { + UpdateData* d = new UpdateData(TYPE_HEADER_INFO); + + m_locker.lock(); + m_data.enqueue(d); + m_locker.unlock(); +} + +UpdateData* ThrData::get_data() { + + m_locker.lock(); + UpdateData* d = m_data.dequeue(); + m_locker.unlock(); + + return d; +} diff --git a/client/tp-player/thr_download.h b/client/tp-player/thr_data.h similarity index 51% rename from client/tp-player/thr_download.h rename to client/tp-player/thr_data.h index 88be069..dcb40f8 100644 --- a/client/tp-player/thr_download.h +++ b/client/tp-player/thr_data.h @@ -1,13 +1,20 @@ -#ifndef THREADDOWNLOAD_H -#define THREADDOWNLOAD_H +#ifndef THR_DATA_H +#define THR_DATA_H #include +#include +#include +#include +#include +#include "downloader.h" +#include "update_data.h" +#include "record_format.h" /* 为支持“边下载,边播放”、“可拖动进度条”等功能,录像数据会分为多个文件存放,目前每个文件约4MB。 例如: tp-rdp.tpr - tp-rdp.tpk (关键帧信息文件,v3.5.0开始引入) + tp-rdp.tpk (关键帧信息文件,v3.5.1开始引入) tp-rdp-1.tpd, tp-rdp-2.tpd, tp-rdp-3.tpd, ... 这样,下载完一个数据文件,即可播放此数据文件中的内容,同时下载线程可以下载后续数据文件。 @@ -25,23 +32,66 @@ 这样,下次需要下载指定文件时,如果发现对应的临时文件存在,可以根据已下载字节数,继续下载。 */ +typedef struct KEYFRAME_INFO { + uint32_t time_ms; // 此关键帧的时间点 + uint32_t file_index; // 此关键帧图像数据位于哪一个数据文件中 + uint32_t offset; // 此关键帧图像数据在数据文件中的偏移 +}KEYFRAME_INFO; -class ThreadDownload : public QThread -{ +typedef std::vector KeyFrames; + +class MainWindow; + +// 下载必要的文件,解析文件数据,生成图像数据(QImage*),将数据包放入待显示队列中,等待 ThrPlay 线程使用 +// 注意,无需将所有数据解析并放入待显示队列,此队列有数量限制(例如1000个),避免过多占用内存 +class ThrData : public QThread { + Q_OBJECT public: - ThreadDownload(const QString& url); + ThrData(MainWindow* mainwin, const QString& url); + ~ThrData(); virtual void run(); void stop(); - // 下载 .tpr 和 .tpf 文件,出错返回false,正在下载或已经下载完成则返回true. - bool prepare(QString& path_base, QString& msg); + + bool have_more_data(); + + UpdateData* get_data(); private: - bool m_need_stop; - QString m_url; + bool _load_header(); + bool _load_keyframe(); + bool _download_file(const QString& url, const QString filename); + + void _prepare(); + + void _notify_message(const QString& msg); + void _notify_error(const QString& err_msg); + void _notify_download(DownloadParam* param); + +signals: + void signal_update_data(UpdateData*); + void signal_download(DownloadParam*); + +private: + MainWindow* m_mainwin; + QQueue m_data; + QMutex m_locker; + + bool m_need_stop; + + bool m_need_download; + QString m_res; + QString m_local_data_path_base; + + QString m_url_base; + QString m_sid; + QString m_rid; QString m_path_base; + + TS_RECORD_HEADER m_hdr; + KeyFrames m_kf; }; -#endif // THREADDOWNLOAD_H +#endif // THR_DATA_H diff --git a/client/tp-player/thr_download.cpp b/client/tp-player/thr_download.cpp deleted file mode 100644 index c338932..0000000 --- a/client/tp-player/thr_download.cpp +++ /dev/null @@ -1,34 +0,0 @@ -#include "thr_download.h" -#include - -ThreadDownload::ThreadDownload(const QString& url) -{ - m_url = url; - m_need_stop = false; -} - -void ThreadDownload::stop() { - if(!isRunning()) - return; - m_need_stop = true; - wait(); - qDebug() << "download thread end."; -} - -bool ThreadDownload::prepare(QString& path_base, QString& msg) { - path_base = m_path_base; - return true; -} - - -void ThreadDownload::run() { - for(int i = 0; i < 500; i++) { - if(m_need_stop) - break; - msleep(100); - - if(i == 50) { - m_path_base = "/Users/apex/Desktop/tp-testdata/"; - } - } -} diff --git a/client/tp-player/thr_play.cpp b/client/tp-player/thr_play.cpp index e058419..a5a1f3f 100644 --- a/client/tp-player/thr_play.cpp +++ b/client/tp-player/thr_play.cpp @@ -1,27 +1,36 @@ #include #include #include -#include -#include -#include #include "thr_play.h" +#include "thr_data.h" #include "record_format.h" +#include "util.h" -ThreadPlay::ThreadPlay(const QString& res) -{ + +/* + * 录像播放流程: + * - 数据处理线程,该线程负责(下载)文件、解析文件,将数据准备成待播放队列; + * + 数据处理线程维护待播放队列,少于500个则填充至1000个,每20ms检查一次队列是否少于500个。 + * - 播放线程从队列中取出一个数据,判断当前时间是否应该播放此数据,如果应该,则将此数据发送给主UI + * + if( 播放速率 * (当前时间 - 播放时间) >= (当前数据包偏移时间 - 上个数据包偏移时间)) 则 播放 + * + 如选择“跳过无操作时间”,则数据包偏移时间差超过3秒的,视为3秒。 + */ + + +ThrPlay::ThrPlay() { m_need_stop = false; m_need_pause = false; m_speed = 2; - m_res = res; - m_thr_download = nullptr; +// m_res = res; +// m_thr_data = nullptr; } -ThreadPlay::~ThreadPlay() { +ThrPlay::~ThrPlay() { stop(); } -void ThreadPlay::stop() { +void ThrPlay::stop() { if(!isRunning()) return; @@ -31,28 +40,31 @@ void ThreadPlay::stop() { wait(); qDebug() << "play-thread end."; - if(m_thr_download) { - m_thr_download->stop(); - //m_thr_download->wait(); - delete m_thr_download; - m_thr_download = nullptr; - } +// if(m_thr_data) { +// m_thr_data->stop(); +// qDebug("delete thrData."); +// //m_thr_download->wait(); +// delete m_thr_data; +// m_thr_data = nullptr; +// } } -void ThreadPlay::_notify_message(const QString& msg) { - update_data* _msg = new update_data(TYPE_MESSAGE); +void ThrPlay::_notify_message(const QString& msg) { + UpdateData* _msg = new UpdateData(TYPE_MESSAGE); _msg->message(msg); emit signal_update_data(_msg); } -void ThreadPlay::_notify_error(const QString& err_msg) { - update_data* _err = new update_data(TYPE_ERROR); - _err->message(err_msg); - emit signal_update_data(_err); +void ThrPlay::_notify_error(const QString& msg) { + UpdateData* _msg = new UpdateData(TYPE_ERROR); + _msg->message(msg); + emit signal_update_data(_msg); } +void ThrPlay::run() { -void ThreadPlay::run() { + // http://127.0.0.1:7190/tp_1491560510_ca67fceb75a78c9d/211 + // E:\work\tp4a\teleport\server\share\replay\rdp\000000211 //#ifdef __APPLE__ // QString currentPath = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); @@ -62,11 +74,19 @@ void ThreadPlay::run() { //#endif // /Users/apex/Library/Preferences/tp-player - //qDebug() << "appdata:" << QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation); +// qDebug() << "appdata:" << QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation); // /private/var/folders/_3/zggrxjdx1lxcdqnfsbgpcwzh0000gn/T //qDebug() << "tmp:" << QStandardPaths::writableLocation(QStandardPaths::TempLocation); +// m_thr_data = new ThrData(this, m_res); +// m_thr_data->start(); + + // "正在准备录像数据,请稍候..." +// _notify_message(LOCAL8BIT("正在准备录像数据,请稍候...")); + + +#if 0 // base of data path (include the .tpr file) QString path_base; @@ -78,8 +98,8 @@ void ThreadPlay::run() { // "正在缓存录像数据,请稍候..." _notify_message("正在缓存录像数据,请稍候..."); - m_thr_download = new ThreadDownload(m_res); - m_thr_download->start(); + m_thr_data = new ThreadDownload(m_res); + m_thr_data->start(); QString msg; for(;;) { @@ -88,7 +108,7 @@ void ThreadPlay::run() { if(m_need_stop) return; - if(!m_thr_download->prepare(path_base, msg)) { + if(!m_thr_data->prepare(path_base, msg)) { msg.sprintf("指定的文件或目录不存在!\n\n%s", _tmp_res.toStdString().c_str()); _notify_error(msg); return; @@ -147,7 +167,7 @@ void ThreadPlay::run() { return; } else { - update_data* dat = new update_data(TYPE_HEADER_INFO); + UpdateData* dat = new UpdateData(TYPE_HEADER_INFO); dat->alloc_data(sizeof(TS_RECORD_HEADER)); read_len = f_hdr.read((char*)(dat->data_buf()), dat->data_len()); @@ -251,7 +271,7 @@ void ThreadPlay::run() { qDebug("----key frame: %d", pkg.time_ms); } - update_data* dat = new update_data(TYPE_DATA); + UpdateData* dat = new UpdateData(TYPE_DATA); dat->alloc_data(sizeof(TS_RECORD_PKG) + pkg.size); memcpy(dat->data_buf(), &pkg, sizeof(TS_RECORD_PKG)); read_len = f_dat.read((char*)(dat->data_buf()+sizeof(TS_RECORD_PKG)), pkg.size); @@ -271,7 +291,7 @@ void ThreadPlay::run() { if(time_pass > total_ms) time_pass = total_ms; if(time_pass - time_last_pass > 200) { - update_data* _passed_ms = new update_data(TYPE_PLAYED_MS); + UpdateData* _passed_ms = new UpdateData(TYPE_PLAYED_MS); _passed_ms->played_ms(time_pass); emit signal_update_data(_passed_ms); time_last_pass = time_pass; @@ -307,7 +327,7 @@ void ThreadPlay::run() { if(_time_pass > total_ms) _time_pass = total_ms; if(_time_pass - time_last_pass > 200) { - update_data* _passed_ms = new update_data(TYPE_PLAYED_MS); + UpdateData* _passed_ms = new UpdateData(TYPE_PLAYED_MS); _passed_ms->played_ms(_time_pass); emit signal_update_data(_passed_ms); time_last_pass = _time_pass; @@ -330,6 +350,9 @@ void ThreadPlay::run() { _notify_message(msg); } - update_data* _end = new update_data(TYPE_END); +#endif + + qDebug("play end."); + UpdateData* _end = new UpdateData(TYPE_END); emit signal_update_data(_end); } diff --git a/client/tp-player/thr_play.h b/client/tp-player/thr_play.h index 84f23c0..dcfb190 100644 --- a/client/tp-player/thr_play.h +++ b/client/tp-player/thr_play.h @@ -1,16 +1,19 @@ -#ifndef THR_PLAY_H +#ifndef THR_PLAY_H #define THR_PLAY_H #include #include "update_data.h" -#include "thr_download.h" +#include "downloader.h" -class ThreadPlay : public QThread +// 根据播放规则,将要播放的图像发送给主UI线程进行显示 +class ThrPlay : public QThread { Q_OBJECT + +friend class ThrData; public: - ThreadPlay(const QString& res); - ~ThreadPlay(); + ThrPlay(); + ~ThrPlay(); virtual void run(); void stop(); @@ -23,17 +26,12 @@ private: void _notify_error(const QString& err_msg); signals: - void signal_update_data(update_data*); + void signal_update_data(UpdateData*); private: bool m_need_stop; bool m_need_pause; int m_speed; - - QString m_res; - bool m_need_download; - - ThreadDownload* m_thr_download; }; #endif // THR_PLAY_H diff --git a/client/tp-player/tp-player.pro b/client/tp-player/tp-player.pro index 68abd41..43f18c6 100644 --- a/client/tp-player/tp-player.pro +++ b/client/tp-player/tp-player.pro @@ -1,27 +1,29 @@ TEMPLATE = app TARGET = tp-player -QT += core gui widgets +QT += core gui widgets network HEADERS += \ mainwindow.h \ bar.h \ - thr_download.h \ thr_play.h \ + thr_data.h \ update_data.h \ record_format.h \ rle.h \ - util.h + util.h \ + downloader.h SOURCES += \ main.cpp \ mainwindow.cpp \ bar.cpp \ - thr_download.cpp \ thr_play.cpp \ + thr_data.cpp \ update_data.cpp \ rle.c \ - util.cpp + util.cpp \ + downloader.cpp RESOURCES += \ tp-player.qrc diff --git a/client/tp-player/update_data.cpp b/client/tp-player/update_data.cpp index c57c2a1..31f2625 100644 --- a/client/tp-player/update_data.cpp +++ b/client/tp-player/update_data.cpp @@ -1,18 +1,18 @@ -#include "update_data.h" +#include "update_data.h" -update_data::update_data(int data_type, QObject *parent) : QObject(parent) +UpdateData::UpdateData(int data_type, QObject *parent) : QObject(parent) { m_data_type = data_type; m_data_buf = nullptr; m_data_len = 0; } -update_data::~update_data() { +UpdateData::~UpdateData() { if(m_data_buf) delete m_data_buf; } -void update_data::alloc_data(uint32_t len) { +void UpdateData::alloc_data(uint32_t len) { if(m_data_buf) delete m_data_buf; @@ -21,7 +21,7 @@ void update_data::alloc_data(uint32_t len) { m_data_len = len; } -void update_data::attach_data(const uint8_t* dat, uint32_t len) { +void UpdateData::attach_data(const uint8_t* dat, uint32_t len) { if(m_data_buf) delete m_data_buf; m_data_buf = new uint8_t[len]; diff --git a/client/tp-player/update_data.h b/client/tp-player/update_data.h index 97b420b..c76c299 100644 --- a/client/tp-player/update_data.h +++ b/client/tp-player/update_data.h @@ -11,12 +11,12 @@ #define TYPE_MESSAGE 5 #define TYPE_ERROR 6 -class update_data : public QObject +class UpdateData : public QObject { Q_OBJECT public: - explicit update_data(int data_type, QObject *parent = nullptr); - virtual ~update_data(); + explicit UpdateData(int data_type, QObject *parent = nullptr); + virtual ~UpdateData(); void alloc_data(uint32_t len); void attach_data(const uint8_t* dat, uint32_t len); @@ -47,7 +47,7 @@ private: class UpdateDataHelper { public: - UpdateDataHelper(update_data* data) { + UpdateDataHelper(UpdateData* data) { m_data = data; } ~UpdateDataHelper() { @@ -56,7 +56,7 @@ public: } private: - update_data* m_data; + UpdateData* m_data; }; diff --git a/client/tp-player/util.cpp b/client/tp-player/util.cpp new file mode 100644 index 0000000..e69de29 diff --git a/client/tp-player/util.h b/client/tp-player/util.h new file mode 100644 index 0000000..8ff938b --- /dev/null +++ b/client/tp-player/util.h @@ -0,0 +1,33 @@ +#ifndef TP_PLAYER_UTIL_H +#define TP_PLAYER_UTIL_H + +#include + +class TimeUseTest { +public: + TimeUseTest() { + m_used_ms = 0; + m_count = 0; + } + ~TimeUseTest() {} + + void begin() { + m_time.start(); + } + void end() { + m_count++; + m_used_ms += m_time.elapsed(); + } + + uint32_t used() const {return m_used_ms;} + uint32_t count() const {return m_count;} + +private: + QTime m_time; + uint32_t m_used_ms; + uint32_t m_count; +}; + +#define LOCAL8BIT(x) QString::fromLocal8Bit(x) + +#endif // TP_PLAYER_UTIL_H diff --git a/server/tp_core/common/base_record.h b/server/tp_core/common/base_record.h index 63b1815..c3fb219 100644 --- a/server/tp_core/common/base_record.h +++ b/server/tp_core/common/base_record.h @@ -76,10 +76,11 @@ typedef struct TS_RECORD_HEADER { // 一个数据包的头 typedef struct TS_RECORD_PKG { - ex_u8 type; // 包的数据类型 - ex_u32 size; // 这个包的总大小(不含包头) - ex_u32 time_ms; // 这个包距起始时间的时间差(毫秒,意味着一个连接不能持续超过49天) - ex_u8 _reserve[3]; // 保留 + ex_u8 type; // 包的数据类型 + ex_u8 _reserve[3]; // 保留 + ex_u32 size; // 这个包的总大小(不含包头) + ex_u32 time_ms; // 这个包距起始时间的时间差(毫秒,意味着一个连接不能持续超过49天) + ex_u32 index; // 这个包的序号(最后一个包的序号与TS_RECORD_HEADER_INFO::packages数量匹配) }TS_RECORD_PKG; #pragma pack(pop) diff --git a/server/www/teleport/webroot/app/controller/audit.py b/server/www/teleport/webroot/app/controller/audit.py index 9e2b3d7..41a1cb2 100644 --- a/server/www/teleport/webroot/app/controller/audit.py +++ b/server/www/teleport/webroot/app/controller/audit.py @@ -671,12 +671,13 @@ class DoGetFileHandler(TPBaseHandler): # return self.write('need login first.') # self._user = _user - if not self._user['_is_login']: - self.set_status(401) # 401=未授权, 要求身份验证 - return self.write('need login first.') - if (self._user['privilege'] & require_privilege) == 0: - self.set_status(403) # 403=禁止 - return self.write('you have no such privilege.') + # when test, disable auth. + # if not self._user['_is_login']: + # self.set_status(401) # 401=未授权, 要求身份验证 + # return self.write('need login first.') + # if (self._user['privilege'] & require_privilege) == 0: + # self.set_status(403) # 403=禁止 + # return self.write('you have no such privilege.') act = self.get_argument('act', None) _type = self.get_argument('type', None) From a7b8b68ae6c207e4646f4356311a87a8ed7469e2 Mon Sep 17 00:00:00 2001 From: Apex Liu Date: Fri, 1 Nov 2019 03:33:18 +0800 Subject: [PATCH 29/44] downloader works. --- client/tp-player/downloader.cpp | 39 +++++++----- client/tp-player/downloader.h | 6 +- client/tp-player/mainwindow.cpp | 32 +++++----- client/tp-player/mainwindow.h | 10 ++-- client/tp-player/thr_data.cpp | 103 ++++++++++++++++++++++++-------- client/tp-player/thr_data.h | 3 +- client/tp-player/tp-player.pro | 2 + 7 files changed, 132 insertions(+), 63 deletions(-) diff --git a/client/tp-player/downloader.cpp b/client/tp-player/downloader.cpp index bee544d..637fbd4 100644 --- a/client/tp-player/downloader.cpp +++ b/client/tp-player/downloader.cpp @@ -1,6 +1,7 @@ #include "downloader.h" #include "record_format.h" +#include #include #include @@ -22,36 +23,50 @@ eventLoop.exec(); //================================================================= Downloader::Downloader() { m_reply = nullptr; - m_code = codeUnknown; + m_code = codeDownloading; } Downloader::~Downloader() { +// qDebug("Downloader destroied."); } -void Downloader::run(QNetworkAccessManager* nam, QString& url, QString& sid, QString& filename) { +void Downloader::run(QNetworkAccessManager* nam, const QString& url, const QString& sid, const QString& filename) { + m_code = codeDownloading; m_filename = filename; if(!m_filename.isEmpty()) { m_file.setFileName(m_filename); if(!m_file.open(QIODevice::WriteOnly | QFile::Truncate)){ qDebug("open file for write failed."); + m_code = codeFailed; return; } } - m_code = codeDownloading; QString cookie = QString("_sid=%1\r\n").arg(sid); QNetworkRequest req; req.setUrl(QUrl(url)); req.setRawHeader("Cookie", cookie.toLatin1()); + QEventLoop eventLoop; m_reply = nam->get(req); + connect(m_reply, &QNetworkReply::finished, &eventLoop, &QEventLoop::quit); connect(m_reply, &QNetworkReply::finished, this, &Downloader::_on_finished); connect(m_reply, &QIODevice::readyRead, this, &Downloader::_on_data_ready); + + eventLoop.exec(); + + disconnect(m_reply, &QNetworkReply::finished, &eventLoop, &QEventLoop::quit); + disconnect(m_reply, &QNetworkReply::finished, this, &Downloader::_on_finished); + disconnect(m_reply, &QIODevice::readyRead, this, &Downloader::_on_data_ready); + delete m_reply; + m_reply = nullptr; + qDebug("Downloader::run(%p) end.", this); } void Downloader::_on_data_ready() { +// qDebug("Downloader::_on_data_ready(%p).", this); QNetworkReply *reply = reinterpret_cast(sender()); if(m_filename.isEmpty()) { @@ -62,17 +77,16 @@ void Downloader::_on_data_ready() { } } -void Downloader::reset() { - m_code = codeUnknown; -} - void Downloader::abort() { - if(m_reply) + if(m_reply) { + qDebug("Downloader::abort(%p);", this); m_reply->abort(); + m_code = codeAbort; + } } void Downloader::_on_finished() { - qDebug("download finished"); +// qDebug("Downloader::_on_finished(%p).", this); QNetworkReply *reply = reinterpret_cast(sender()); QVariant statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); @@ -83,14 +97,11 @@ void Downloader::_on_finished() { //qDebug() << strError; m_file.flush(); m_file.close(); - m_code = codeFailed; + if(m_code != codeDownloading) + m_code = codeFailed; return; } - disconnect(m_reply, &QNetworkReply::finished, this, &Downloader::_on_finished); - disconnect(m_reply, &QIODevice::readyRead, this, &Downloader::_on_data_ready); -// reply->deleteLater(); - if(m_filename.isEmpty()) { m_data += reply->readAll(); } diff --git a/client/tp-player/downloader.h b/client/tp-player/downloader.h index 38dcbdf..c2a3df9 100644 --- a/client/tp-player/downloader.h +++ b/client/tp-player/downloader.h @@ -9,9 +9,9 @@ class Downloader : public QObject { public: enum EndCode{ - codeUnknown = 0, codeSuccess, codeDownloading, + codeAbort, codeFailed }; @@ -20,9 +20,9 @@ public: Downloader(); ~Downloader(); - void run(QNetworkAccessManager* nam, QString& url, QString& sid, QString& filename); + void run(QNetworkAccessManager* nam, const QString& url, const QString& sid, const QString& filename); void abort(); - void reset(); +// void reset(); QByteArray& data(){return m_data;} EndCode code() {return m_code;} diff --git a/client/tp-player/mainwindow.cpp b/client/tp-player/mainwindow.cpp index 7440262..7999b7e 100644 --- a/client/tp-player/mainwindow.cpp +++ b/client/tp-player/mainwindow.cpp @@ -93,7 +93,7 @@ MainWindow::MainWindow(QWidget *parent) : m_play_state = PLAY_STATE_UNKNOWN; m_thr_data = nullptr; - m_dl = nullptr; +// m_dl = nullptr; ui->setupUi(this); @@ -150,15 +150,15 @@ MainWindow::~MainWindow() if(m_thr_data) { m_thr_data->stop(); disconnect(m_thr_data, SIGNAL(signal_update_data(UpdateData*)), this, SLOT(_do_update_data(UpdateData*))); - disconnect(m_thr_data, SIGNAL(signal_download(DownloadParam*)), this, SLOT(_do_download(DownloadParam*))); +// disconnect(m_thr_data, SIGNAL(signal_download(DownloadParam*)), this, SLOT(_do_download(DownloadParam*))); delete m_thr_data; m_thr_data = nullptr; } - if(m_dl) { - delete m_dl; - m_dl = nullptr; - } +// if(m_dl) { +// delete m_dl; +// m_dl = nullptr; +// } delete ui; } @@ -170,7 +170,7 @@ void MainWindow::set_resource(const QString &res) { void MainWindow::_do_first_run() { m_thr_data = new ThrData(this, m_res); connect(m_thr_data, SIGNAL(signal_update_data(UpdateData*)), this, SLOT(_do_update_data(UpdateData*))); - connect(m_thr_data, SIGNAL(signal_download(DownloadParam*)), this, SLOT(_do_download(DownloadParam*))); +// connect(m_thr_data, SIGNAL(signal_download(DownloadParam*)), this, SLOT(_do_download(DownloadParam*))); m_thr_data->start(); _start_play_thread(); @@ -481,17 +481,17 @@ void MainWindow::_do_bar_fade() { update(m_bar.rc()); } -void MainWindow::_do_download(DownloadParam* param) { - qDebug("MainWindow::_do_download(). %s %s %s", param->url.toStdString().c_str(), param->sid.toStdString().c_str(), param->fname.toStdString().c_str()); +//void MainWindow::_do_download(DownloadParam* param) { +// qDebug("MainWindow::_do_download(). %s %s %s", param->url.toStdString().c_str(), param->sid.toStdString().c_str(), param->fname.toStdString().c_str()); - if(m_dl) { - delete m_dl; - m_dl = nullptr; - } +// if(m_dl) { +// delete m_dl; +// m_dl = nullptr; +// } - m_dl = new Downloader(); - m_dl->run(&m_nam, param->url, param->sid, param->fname); -} +// m_dl = new Downloader(); +// m_dl->run(&m_nam, param->url, param->sid, param->fname); +//} void MainWindow::mouseMoveEvent(QMouseEvent *e) { if(!m_show_default) { diff --git a/client/tp-player/mainwindow.h b/client/tp-player/mainwindow.h index 031fc2b..feabbf4 100644 --- a/client/tp-player/mainwindow.h +++ b/client/tp-player/mainwindow.h @@ -36,8 +36,8 @@ public: void restart(); void speed(int s); - Downloader* downloader() {return m_dl;} - void reset_downloader() {if(m_dl){delete m_dl;m_dl= nullptr;}} +// Downloader* downloader() {return m_dl;} +// void reset_downloader() {if(m_dl){delete m_dl;m_dl= nullptr;}} private: void paintEvent(QPaintEvent *e); @@ -53,7 +53,7 @@ private slots: void _do_bar_delay_hide(); // void _do_download(Downloader*); - void _do_download(DownloadParam*); +// void _do_download(DownloadParam*); private: Ui::MainWindow *ui; @@ -90,8 +90,8 @@ private: QRect m_rc_message; - QNetworkAccessManager m_nam; - Downloader* m_dl; +// QNetworkAccessManager m_nam; +// Downloader* m_dl; // for test TimeUseTest m_time_imgconvert_normal; diff --git a/client/tp-player/thr_data.cpp b/client/tp-player/thr_data.cpp index f0b0987..5e0b166 100644 --- a/client/tp-player/thr_data.cpp +++ b/client/tp-player/thr_data.cpp @@ -21,6 +21,7 @@ ThrData::ThrData(MainWindow* mainwin, const QString& res) { m_res = res; m_need_download = false; m_need_stop = false; + m_dl = nullptr; #ifdef __APPLE__ QString data_path_base = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); @@ -48,12 +49,19 @@ GenericCacheLocation: "C:/Users/apex/AppData/Local/cache" } ThrData::~ThrData() { + if(m_dl) + delete m_dl; } void ThrData::stop() { if(!isRunning()) return; m_need_stop = true; + + if(m_dl) { + m_dl->abort(); + } + wait(); qDebug("data thread stop() end."); } @@ -70,10 +78,6 @@ void ThrData::_notify_error(const QString& msg) { emit signal_update_data(_msg); } -void ThrData::_notify_download(DownloadParam* param) { - emit signal_download(param); -} - // tp-player.exe http://teleport.domain.com:7190/{sub/path/}tp_1491560510_ca67fceb75a78c9d/1234 (注意,并不直接访问此URI,实际上其并不存在) // TP服务器地址(可能包含子路径哦,例如上例中的{sub/path/}部分)/session-id(用于判断当前授权用户)/录像会话编号 @@ -84,11 +88,11 @@ void ThrData::run() { if(!_load_keyframe()) return; - for(;;) { - if(m_need_stop) - break; - msleep(500); - } +// for(;;) { +// if(m_need_stop) +// break; +// msleep(500); +// } qDebug("ThrData thread run() end."); } @@ -149,8 +153,10 @@ bool ThrData::_load_header() { // return false; // } - Downloader* dl = m_mainwin->downloader(); - QByteArray& data = dl->data(); + //Downloader* dl = m_mainwin->downloader(); + if(!m_dl) + return false; + QByteArray& data = m_dl->data(); if(data.size() != sizeof(TS_RECORD_HEADER)) { qDebug("invalid header file. %d", data.size()); _notify_error(QString("%1\n\n%2").arg(LOCAL8BIT("指定的文件或目录不存在!"), _tmp_res)); @@ -285,29 +291,36 @@ bool ThrData::_load_header() { } bool ThrData::_load_keyframe() { - // _notify_error(QString("%1").arg(LOCAL8BIT("测试!"))); - QString tpk_fname = QString("%1/tp-rdp.tpk").arg(m_path_base); tpk_fname = QDir::toNativeSeparators(tpk_fname); if(m_need_download) { - // download .tpr - QString url(m_url_base); - url += "/audit/get-file?act=read&type=rdp&rid="; - url += m_rid; - url += "&f=tp-rdp.tpk"; - QString tmp_fname = QString("%1/tp-rdp.tpk.downloading").arg(m_path_base); tmp_fname = QDir::toNativeSeparators(tmp_fname); - qDebug() << "TPK(tmp): " << tmp_fname; - qDebug() << "TPK(out): " << tpk_fname; - if(!_download_file(url, tmp_fname)) - return false; + QFileInfo fi_tmp(tmp_fname); + if(fi_tmp.isFile()) { + QFile::remove(tmp_fname); + } - QFile::rename(tmp_fname, tpk_fname); + QFileInfo fi_tpk(tpk_fname); + if(!fi_tpk.exists()) { + QString url(m_url_base); + url += "/audit/get-file?act=read&type=rdp&rid="; + url += m_rid; + url += "&f=tp-rdp.tpk"; + + qDebug() << "TPK(tmp): " << tmp_fname; + if(!_download_file(url, tmp_fname)) + return false; + + if(!QFile::rename(tmp_fname, tpk_fname)) + return false; + } } + qDebug() << "TPK: " << tpk_fname; + QFile f_kf(tpk_fname); if(!f_kf.open(QFile::ReadOnly)) { qDebug() << "Can not open " << tpk_fname << " for read."; @@ -340,6 +353,46 @@ bool ThrData::_load_keyframe() { return true; } +bool ThrData::_download_file(const QString& url, const QString filename) { + if(!m_need_download) { + qDebug() << "download not necessary."; + return false; + } + + if(m_dl) { + delete m_dl; + m_dl = nullptr; + } + + m_dl = new Downloader(); + + QNetworkAccessManager* nam = new QNetworkAccessManager; + + m_dl->run(nam, url, m_sid, filename); +// qDebug("m_dl.run(%p) end.", m_dl); + + for(;;) { + if(m_dl->code() == Downloader::codeDownloading) { + msleep(100); + continue; + } + + if(m_dl->code() != Downloader::codeSuccess) { + qDebug() << "download failed."; + _notify_error(QString("%1").arg(LOCAL8BIT("下载文件失败!"))); + delete nam; + return false; + } + else { + qDebug() << "download ok."; + delete nam; + return true; + } + } + +} + +#if 0 bool ThrData::_download_file(const QString& url, const QString filename) { if(!m_need_download) { qDebug() << "download not necessary."; @@ -373,6 +426,8 @@ bool ThrData::_download_file(const QString& url, const QString filename) { } } } +#endif + #if 0 void ThrData::run() { diff --git a/client/tp-player/thr_data.h b/client/tp-player/thr_data.h index dcb40f8..e4adcbe 100644 --- a/client/tp-player/thr_data.h +++ b/client/tp-player/thr_data.h @@ -72,7 +72,6 @@ private: signals: void signal_update_data(UpdateData*); - void signal_download(DownloadParam*); private: MainWindow* m_mainwin; @@ -90,6 +89,8 @@ private: QString m_rid; QString m_path_base; + Downloader* m_dl; + TS_RECORD_HEADER m_hdr; KeyFrames m_kf; }; diff --git a/client/tp-player/tp-player.pro b/client/tp-player/tp-player.pro index 43f18c6..27a2794 100644 --- a/client/tp-player/tp-player.pro +++ b/client/tp-player/tp-player.pro @@ -3,6 +3,8 @@ TARGET = tp-player QT += core gui widgets network +#DEFINES += QT_NO_DEBUG_OUTPUT + HEADERS += \ mainwindow.h \ bar.h \ From 5754f49f30d4144bef4b371c73f96ad382c97a6d Mon Sep 17 00:00:00 2001 From: Apex Liu Date: Sat, 2 Nov 2019 02:12:48 +0800 Subject: [PATCH 30/44] downloader works. --- client/tp-player/downloader.cpp | 89 +-- client/tp-player/downloader.h | 24 +- client/tp-player/thr_data.cpp | 528 +++++------------- client/tp-player/thr_data.h | 7 +- .../teleport/webroot/app/controller/audit.py | 5 +- 5 files changed, 204 insertions(+), 449 deletions(-) diff --git a/client/tp-player/downloader.cpp b/client/tp-player/downloader.cpp index 637fbd4..0f5a10a 100644 --- a/client/tp-player/downloader.cpp +++ b/client/tp-player/downloader.cpp @@ -5,41 +5,36 @@ #include #include - -// TODO: 将Downloader的实现代码迁移到ThrData线程中 -// 使用局部event循环的方式进行下载 -/* -QEventLoop eventLoop; -connect(netWorker, &NetWorker::finished, - &eventLoop, &QEventLoop::quit); -QNetworkReply *reply = netWorker->get(url); -replyMap.insert(reply, FetchWeatherInfo); -eventLoop.exec(); -*/ - - -//================================================================= -// Downloader -//================================================================= -Downloader::Downloader() { +Downloader::Downloader() : QObject () { + m_data = nullptr; m_reply = nullptr; - m_code = codeDownloading; + m_result = false; } Downloader::~Downloader() { -// qDebug("Downloader destroied."); } -void Downloader::run(QNetworkAccessManager* nam, const QString& url, const QString& sid, const QString& filename) { - m_code = codeDownloading; - m_filename = filename; +bool Downloader::request(const QString& url, const QString& sid, const QString& filename) { + return _request(url, sid, filename, nullptr); +} - if(!m_filename.isEmpty()) { - m_file.setFileName(m_filename); +bool Downloader::request(const QString& url, const QString& sid, QByteArray* data) { + QString fname; + return _request(url, sid, fname, data); +} + +bool Downloader::_request(const QString& url, const QString& sid, const QString& filename, QByteArray* data) { + if(filename.isEmpty() && data == nullptr) + return false; + if(!filename.isEmpty() && data != nullptr) + return false; + m_data = data; + + if(!filename.isEmpty()) { + m_file.setFileName(filename); if(!m_file.open(QIODevice::WriteOnly | QFile::Truncate)){ qDebug("open file for write failed."); - m_code = codeFailed; - return; + return false; } } @@ -49,28 +44,36 @@ void Downloader::run(QNetworkAccessManager* nam, const QString& url, const QStri req.setUrl(QUrl(url)); req.setRawHeader("Cookie", cookie.toLatin1()); - QEventLoop eventLoop; + QNetworkAccessManager* nam = new QNetworkAccessManager(); + QEventLoop eloop; m_reply = nam->get(req); - connect(m_reply, &QNetworkReply::finished, &eventLoop, &QEventLoop::quit); + + connect(m_reply, &QNetworkReply::finished, &eloop, &QEventLoop::quit); connect(m_reply, &QNetworkReply::finished, this, &Downloader::_on_finished); connect(m_reply, &QIODevice::readyRead, this, &Downloader::_on_data_ready); - eventLoop.exec(); +// qDebug("before eventLoop.exec(%p)", &eloop); + eloop.exec(); +// qDebug("after eventLoop.exec()"); - disconnect(m_reply, &QNetworkReply::finished, &eventLoop, &QEventLoop::quit); + disconnect(m_reply, &QNetworkReply::finished, &eloop, &QEventLoop::quit); disconnect(m_reply, &QNetworkReply::finished, this, &Downloader::_on_finished); disconnect(m_reply, &QIODevice::readyRead, this, &Downloader::_on_data_ready); + delete m_reply; m_reply = nullptr; - qDebug("Downloader::run(%p) end.", this); + delete nam; + + qDebug("Downloader::_request() end."); + return m_result; } void Downloader::_on_data_ready() { // qDebug("Downloader::_on_data_ready(%p).", this); QNetworkReply *reply = reinterpret_cast(sender()); - if(m_filename.isEmpty()) { - m_data += reply->readAll(); + if(m_data != nullptr) { + m_data->push_back(reply->readAll()); } else { m_file.write(reply->readAll()); @@ -79,9 +82,8 @@ void Downloader::_on_data_ready() { void Downloader::abort() { if(m_reply) { - qDebug("Downloader::abort(%p);", this); + qDebug("Downloader::abort()."); m_reply->abort(); - m_code = codeAbort; } } @@ -94,16 +96,17 @@ void Downloader::_on_finished() { if (reply->error() != QNetworkReply::NoError) { // reply->abort() got "Operation canceled" //QString strError = reply->errorString(); - //qDebug() << strError; - m_file.flush(); - m_file.close(); - if(m_code != codeDownloading) - m_code = codeFailed; + qDebug() << "ERROR:" << reply->errorString(); + if(m_data == nullptr) { + m_file.flush(); + m_file.close(); + } + m_result = false; return; } - if(m_filename.isEmpty()) { - m_data += reply->readAll(); + if(m_data != nullptr) { + m_data->push_back(reply->readAll()); } else { m_file.write(reply->readAll()); @@ -113,5 +116,5 @@ void Downloader::_on_finished() { reply->deleteLater(); - m_code = codeSuccess; + m_result = true; } diff --git a/client/tp-player/downloader.h b/client/tp-player/downloader.h index c2a3df9..5f5a123 100644 --- a/client/tp-player/downloader.h +++ b/client/tp-player/downloader.h @@ -8,37 +8,27 @@ class Downloader : public QObject { Q_OBJECT public: - enum EndCode{ - codeSuccess, - codeDownloading, - codeAbort, - codeFailed - }; - -public: - // 从url下载数据,写入到filename文件中,如果filename为空字符串,则保存在内存中,可通过 data() 获取。 + // 从url下载数据,写入到filename文件中,或放入data中。 Downloader(); ~Downloader(); - void run(QNetworkAccessManager* nam, const QString& url, const QString& sid, const QString& filename); + bool request(const QString& url, const QString& sid, const QString& filename); + bool request(const QString& url, const QString& sid, QByteArray* data); void abort(); -// void reset(); - QByteArray& data(){return m_data;} - EndCode code() {return m_code;} +private: + bool _request(const QString& url, const QString& sid, const QString& filename, QByteArray* data); private slots: void _on_data_ready(); // 有数据可读了,读取并写入文件 void _on_finished(); // 下载结束了 private: - QString m_filename; QFile m_file; - QByteArray m_data; + QByteArray* m_data; + bool m_result; QNetworkReply* m_reply; - - EndCode m_code; }; typedef struct DownloadParam { diff --git a/client/tp-player/thr_data.cpp b/client/tp-player/thr_data.cpp index 5e0b166..25a5f06 100644 --- a/client/tp-player/thr_data.cpp +++ b/client/tp-player/thr_data.cpp @@ -8,6 +8,7 @@ #include "thr_play.h" #include "thr_data.h" #include "util.h" +#include "downloader.h" #include "record_format.h" #include "mainwindow.h" @@ -21,7 +22,7 @@ ThrData::ThrData(MainWindow* mainwin, const QString& res) { m_res = res; m_need_download = false; m_need_stop = false; - m_dl = nullptr; +// m_dl = nullptr; #ifdef __APPLE__ QString data_path_base = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); @@ -49,19 +50,12 @@ GenericCacheLocation: "C:/Users/apex/AppData/Local/cache" } ThrData::~ThrData() { - if(m_dl) - delete m_dl; } void ThrData::stop() { if(!isRunning()) return; m_need_stop = true; - - if(m_dl) { - m_dl->abort(); - } - wait(); qDebug("data thread stop() end."); } @@ -82,19 +76,137 @@ void ThrData::_notify_error(const QString& msg) { // TP服务器地址(可能包含子路径哦,例如上例中的{sub/path/}部分)/session-id(用于判断当前授权用户)/录像会话编号 void ThrData::run() { + _run(); + qDebug("ThrData thread run() end."); +} + +void ThrData::_run() { if(!_load_header()) return; if(!_load_keyframe()) return; -// for(;;) { -// if(m_need_stop) -// break; -// msleep(500); -// } + uint32_t file_idx = 0; + for(;;) { + if(m_need_stop) + break; + QString str_fidx; + str_fidx.sprintf("%d", file_idx+1); - qDebug("ThrData thread run() end."); + QString tpd_fname = QString("%1/tp-rdp-%2.tpd").arg(m_path_base, str_fidx); + tpd_fname = QDir::toNativeSeparators(tpd_fname); + + if(m_need_download) { + QString tmp_fname = QString("%1/tp-rdp-%2.tpd.downloading").arg(m_path_base, str_fidx); + tmp_fname = QDir::toNativeSeparators(tmp_fname); + + QFileInfo fi_tmp(tmp_fname); + if(fi_tmp.isFile()) { + QFile::remove(tmp_fname); + } + + QFileInfo fi_tpd(tpd_fname); + if(!fi_tpd.exists()) { + QString url = QString("%1/audit/get-file?act=read&type=rdp&rid=%2&f=tp-rdp-%3.tpd").arg(m_url_base, m_rid, str_fidx); + + qDebug() << "URL : " << url; + qDebug() << "TPD : " << tmp_fname; + if(!_download_file(url, tmp_fname)) + return; + + if(!QFile::rename(tmp_fname, tpd_fname)) + return; + } + } + + qDebug() << "TPD: " << tpd_fname; + + QFile f_tpd(tpd_fname); + if(!f_tpd.open(QFile::ReadOnly)) { + qDebug() << "Can not open " << tpd_fname << " for read."; + _notify_error(QString("%1\n\n%3").arg(LOCAL8BIT("无法打开录像数据文件!"), tpd_fname)); + return; + } + + qint64 filesize = f_tpd.size(); + qint64 processed = 0; + + // 加载并解析到待播放队列 + qint64 read_len = 0; + for(;;) { + { +// if(m_need_stop) +// break; + +// m_locker.lock(); + +// if(m_data.size() > 500) { +// msleep(1000); +// continue; +// } + +// m_locker.unlock(); + } + + if(filesize - processed < sizeof(TS_RECORD_PKG)) { + qDebug("invaid tp-rdp-%d.tpd file, filesize=%d, processed=%d, need=%d.", file_idx+1, filesize, processed, sizeof(TS_RECORD_PKG)); + _notify_error(QString("%1\n\n%3").arg(LOCAL8BIT("错误的录像数据文件!"), tpd_fname)); + return; + } + + TS_RECORD_PKG pkg; + read_len = f_tpd.read(reinterpret_cast(&pkg), sizeof(pkg)); + if(read_len == 0) + break; + if(read_len != sizeof(TS_RECORD_PKG)) { + qDebug("invaid tp-rdp-%d.tpd file, read_len=%d (1).", file_idx+1, read_len); + _notify_error(QString("%1\n\n%3").arg(LOCAL8BIT("错误的录像数据文件!"), tpd_fname)); + return; + } + processed += sizeof(TS_RECORD_PKG); + + if(filesize - processed < pkg.size) { + qDebug("invaid tp-rdp-%d.tpd file (2).", file_idx+1); + _notify_error(QString("%1\n\n%3").arg(LOCAL8BIT("错误的录像数据文件!"), tpd_fname)); + return; + } + + if(pkg.type == TS_RECORD_TYPE_RDP_KEYFRAME) { + qDebug("----key frame: %d, pkg.size=%d", pkg.time_ms, pkg.size); + if(pkg.size > 0) { + f_tpd.read(pkg.size); + } + continue; + } + + UpdateData* dat = new UpdateData(TYPE_DATA); + dat->alloc_data(sizeof(TS_RECORD_PKG) + pkg.size); + memcpy(dat->data_buf(), &pkg, sizeof(TS_RECORD_PKG)); + read_len = f_tpd.read(reinterpret_cast(dat->data_buf()+sizeof(TS_RECORD_PKG)), pkg.size); + if(read_len != pkg.size) { + delete dat; + qDebug("invaid tp-rdp-%d.tpd file (3).", file_idx+1); + _notify_error(QString("%1\n\n%3").arg(LOCAL8BIT("错误的录像数据文件!"), tpd_fname)); + return; + } + + + { + m_locker.lock(); + m_data.enqueue(dat); +// qDebug("data count: %d", m_data.size()); + m_locker.unlock(); + } + } + + + + + file_idx += 1; + if(file_idx >= m_hdr.info.dat_file_count) + break; + } } bool ThrData::_load_header() { @@ -121,42 +233,11 @@ bool ThrData::_load_header() { qDebug() << "url-base:[" << m_url_base << "], sid:[" << m_sid << "], rid:[" << m_rid << "]"; // download .tpr - QString url(m_url_base); - url += "/audit/get-file?act=read&type=rdp&rid="; - url += m_rid; - url += "&f=tp-rdp.tpr"; - - QString fname; - if(!_download_file(url, fname)) + QString url = QString("%1/audit/get-file?act=read&type=rdp&rid=%2&f=tp-rdp.tpr").arg(m_url_base, m_rid); + QByteArray data; + if(!_download_file(url, data)) return false; -// Downloader& dl = m_mainwin->downloader(); -// dl.reset(); - -// DownloadParam param; -// param.url = url; -// param.sid = m_sid; -// param.fname = fname; -// _notify_download(¶m); - -// for(;;) { -// if(dl.code() == Downloader::codeUnknown || dl.code() == Downloader::codeDownloading) { -// msleep(100); -// continue; -// } - -// break; -// } - -// if(dl.code() != Downloader::codeSuccess) { -// _notify_error(QString("%1").arg(LOCAL8BIT("下载文件失败!"))); -// return false; -// } - - //Downloader* dl = m_mainwin->downloader(); - if(!m_dl) - return false; - QByteArray& data = m_dl->data(); if(data.size() != sizeof(TS_RECORD_HEADER)) { qDebug("invalid header file. %d", data.size()); _notify_error(QString("%1\n\n%2").arg(LOCAL8BIT("指定的文件或目录不存在!"), _tmp_res)); @@ -305,12 +386,8 @@ bool ThrData::_load_keyframe() { QFileInfo fi_tpk(tpk_fname); if(!fi_tpk.exists()) { - QString url(m_url_base); - url += "/audit/get-file?act=read&type=rdp&rid="; - url += m_rid; - url += "&f=tp-rdp.tpk"; - - qDebug() << "TPK(tmp): " << tmp_fname; + QString url = QString("%1/audit/get-file?act=read&type=rdp&rid=%2&f=tp-rdp.tpk").arg(m_url_base, m_rid); + qDebug() << "TPK: " << tmp_fname; if(!_download_file(url, tmp_fname)) return false; @@ -359,349 +436,34 @@ bool ThrData::_download_file(const QString& url, const QString filename) { return false; } - if(m_dl) { - delete m_dl; - m_dl = nullptr; - } - - m_dl = new Downloader(); - - QNetworkAccessManager* nam = new QNetworkAccessManager; - - m_dl->run(nam, url, m_sid, filename); -// qDebug("m_dl.run(%p) end.", m_dl); - - for(;;) { - if(m_dl->code() == Downloader::codeDownloading) { - msleep(100); - continue; - } - - if(m_dl->code() != Downloader::codeSuccess) { - qDebug() << "download failed."; - _notify_error(QString("%1").arg(LOCAL8BIT("下载文件失败!"))); - delete nam; - return false; - } - else { - qDebug() << "download ok."; - delete nam; - return true; - } + Downloader dl; + if(!dl.request(url, m_sid, filename)) { + qDebug() << "download failed."; + _notify_error(QString("%1").arg(LOCAL8BIT("下载文件失败!"))); + return false; } + return true; } -#if 0 -bool ThrData::_download_file(const QString& url, const QString filename) { +bool ThrData::_download_file(const QString& url, QByteArray& data) { if(!m_need_download) { qDebug() << "download not necessary."; return false; } - m_mainwin->reset_downloader(); - msleep(100); + Downloader dl; - DownloadParam param; - param.url = url; - param.sid = m_sid; - param.fname = filename; - _notify_download(¶m); - - for(;;) { - Downloader* dl = m_mainwin->downloader(); - if(!dl || dl->code() == Downloader::codeUnknown || dl->code() == Downloader::codeDownloading) { - msleep(100); - continue; - } - - if(dl->code() != Downloader::codeSuccess) { - qDebug() << "download failed."; - _notify_error(QString("%1").arg(LOCAL8BIT("下载文件失败!"))); - return false; - } - else { - qDebug() << "download ok."; - return true; - } + if(!dl.request(url, m_sid, &data)) { + qDebug() << "download failed."; + _notify_error(QString("%1").arg(LOCAL8BIT("下载文件失败!"))); + return false; } + + return true; } -#endif -#if 0 -void ThrData::run() { - QString msg; - QString path_base; - - QString _tmp_res = m_res.toLower(); - - if(_tmp_res.startsWith("http")) { - qDebug() << "DOWNLOAD"; - m_need_download = true; - - // "正在缓存录像数据,请稍候..." - m_thr_play->_notify_message(LOCAL8BIT("正在下载录像数据,请稍候...")); - - // QString msg; - // for(;;) { - // msleep(500); - - // if(m_need_stop) - // return; - - // if(!m_thr_data->prepare(path_base, msg)) { - // msg.sprintf("指定的文件或目录不存在!\n\n%s", _tmp_res.toStdString().c_str()); - // _notify_error(msg); - // return; - // } - - // if(path_base.length()) - // break; - // } - } - else { - QFileInfo fi_chk_link(m_res); - if(fi_chk_link.isSymLink()) - _tmp_res = fi_chk_link.symLinkTarget(); - else - _tmp_res = m_res; - - QFileInfo fi(_tmp_res); - if(!fi.exists()) { - msg.sprintf(LOCAL8BIT("指定的文件或目录不存在!\n\n%s").toStdString().c_str(), _tmp_res.toStdString().c_str()); - m_thr_play->_notify_error(msg); - return; - } - - if(fi.isFile()) { - path_base = fi.path(); - } - else if(fi.isDir()) { - path_base = m_res; - } - - path_base += "/"; - } - - //====================================== - // 加载录像基本信息数据 - //====================================== - - QString tpr_filename(path_base); - tpr_filename += "tp-rdp.tpr"; - - QFile f_hdr(tpr_filename); - if(!f_hdr.open(QFile::ReadOnly)) { - qDebug() << "Can not open " << tpr_filename << " for read."; - msg.sprintf(LOCAL8BIT("无法打开录像信息文件!\n\n%s").toStdString().c_str(), tpr_filename.toStdString().c_str()); - m_thr_play->_notify_error(msg); - return; - } - - TS_RECORD_HEADER hdr; - memset(&hdr, 0, sizeof(TS_RECORD_HEADER)); - - qint64 read_len = 0; - read_len = f_hdr.read((char*)(&hdr), sizeof(TS_RECORD_HEADER)); - if(read_len != sizeof(TS_RECORD_HEADER)) { - qDebug() << "invaid .tpr file."; - msg.sprintf(LOCAL8BIT("错误的录像信息文件!\n\n%s").toStdString().c_str(), tpr_filename.toStdString().c_str()); - m_thr_play->_notify_error(msg); - return; - } - - if(hdr.info.ver != 4) { - qDebug() << "invaid .tpr file."; - msg.sprintf(LOCAL8BIT("不支持的录像文件版本 %d!\n\n此播放器支持录像文件版本 4。").toStdString().c_str(), hdr.info.ver); - m_thr_play->_notify_error(msg); - return; - } - - if(hdr.basic.width == 0 || hdr.basic.height == 0) { - m_thr_play->_notify_error(LOCAL8BIT("错误的录像信息,未记录窗口尺寸!")); - return; - } - - if(hdr.info.dat_file_count == 0) { - m_thr_play->_notify_error(LOCAL8BIT("错误的录像信息,未记录数据文件数量!")); - return; - } - - //====================================== - // 加载关键帧数据 - //====================================== - QString tpk_filename(path_base); - tpk_filename += "tp-rdp.tpk"; - - QFile f_kf(tpk_filename); - if(!f_kf.open(QFile::ReadOnly)) { - qDebug() << "Can not open " << tpk_filename << " for read."; - msg.sprintf(LOCAL8BIT("无法打开关键帧信息文件!\n\n%s").toStdString().c_str(), tpk_filename.toStdString().c_str()); - m_thr_play->_notify_error(msg); - return; - } - - qint64 fsize = f_kf.size(); - if(!fsize || fsize % sizeof(KEYFRAME_INFO) != 0) { - qDebug() << "Can not open " << tpk_filename << " for read."; - msg.sprintf(LOCAL8BIT("关键帧信息文件格式错误!\n\n").toStdString().c_str()); - m_thr_play->_notify_error(msg); - return; - } - - int kf_count = fsize / sizeof(KEYFRAME_INFO); - for(int i = 0; i < kf_count; ++i) { - KEYFRAME_INFO kf; - memset(&kf, 0, sizeof(KEYFRAME_INFO)); - read_len = f_kf.read((char*)(&kf), sizeof(KEYFRAME_INFO)); - if(read_len != sizeof(KEYFRAME_INFO)) { - qDebug() << "invaid .tpk file."; - msg.sprintf(LOCAL8BIT("关键帧信息文件格式错误!\n\n").toStdString().c_str()); - m_thr_play->_notify_error(msg); - return; - } - - m_kf.push_back(kf); - } - - //====================================== - // 读取并解析录像数据文件 - //====================================== - uint32_t fidx = 0; - while(!m_need_stop) { - - for(fidx = 0; fidx < hdr.info.dat_file_count; ++fidx) { - QString tpd_filename(path_base); - QString str_tmp; - - str_tmp.sprintf("tp-rdp-%d.tpd", fidx+1); - tpd_filename += str_tmp; - - QFileInfo fi(tpd_filename); - if(!fi.isFile()) { - // 文件不存在,如需下载,则启动下载函数并等待下载结束。(下载是异步的吗?) - } - - QFile f_dat(tpd_filename); - if(!f_dat.open(QFile::ReadOnly)) { - qDebug() << "Can not open " << tpd_filename << " for read."; - // msg.sprintf("无法打开录像数据文件!\n\n%s", tpd_filename.toStdString().c_str()); - msg = QString::fromLocal8Bit("无法打开录像数据文件!\n\n"); - msg += tpd_filename; - m_thr_play->_notify_error(msg); - return; - } - - - - for(;;) { - if(m_need_stop) { - qDebug() << "stop, user cancel 2."; - break; - } - - if(m_need_pause) { - msleep(50); - time_begin += 50; - continue; - } - - TS_RECORD_PKG pkg; - read_len = f_dat.read((char*)(&pkg), sizeof(pkg)); - if(read_len == 0) - break; - if(read_len != sizeof(TS_RECORD_PKG)) { - qDebug() << "invaid .tpd file (1)."; - // msg.sprintf("错误的录像数据文件!\n\n%s", tpd_filename.toStdString().c_str()); - msg = QString::fromLocal8Bit("错误的录像数据文件!\n\n"); - msg += tpd_filename.toStdString().c_str(); - _notify_error(msg); - return; - } - if(pkg.type == TS_RECORD_TYPE_RDP_KEYFRAME) { - qDebug("----key frame: %d", pkg.time_ms); - } - - UpdateData* dat = new UpdateData(TYPE_DATA); - dat->alloc_data(sizeof(TS_RECORD_PKG) + pkg.size); - memcpy(dat->data_buf(), &pkg, sizeof(TS_RECORD_PKG)); - read_len = f_dat.read((char*)(dat->data_buf()+sizeof(TS_RECORD_PKG)), pkg.size); - if(read_len != pkg.size) { - delete dat; - qDebug() << "invaid .tpd file."; - // msg.sprintf("错误的录像数据文件!\n\n%s", tpd_filename.toStdString().c_str()); - msg = QString::fromLocal8Bit("错误的录像数据文件!\n\n"); - msg += tpd_filename.toStdString().c_str(); - _notify_error(msg); - return; - } - - pkg_count++; - - time_pass = (uint32_t)(QDateTime::currentMSecsSinceEpoch() - time_begin) * m_speed; - if(time_pass > total_ms) - time_pass = total_ms; - if(time_pass - time_last_pass > 200) { - UpdateData* _passed_ms = new UpdateData(TYPE_PLAYED_MS); - _passed_ms->played_ms(time_pass); - emit signal_update_data(_passed_ms); - time_last_pass = time_pass; - } - - if(time_pass >= pkg.time_ms) { - emit signal_update_data(dat); - continue; - } - - // 需要等待 - uint32_t time_wait = pkg.time_ms - time_pass; - uint32_t wait_this_time = 0; - for(;;) { - if(m_need_pause) { - msleep(50); - time_begin += 50; - continue; - } - - wait_this_time = time_wait; - if(wait_this_time > 10) - wait_this_time = 10; - - if(m_need_stop) { - qDebug() << "stop, user cancel (2)."; - break; - } - - msleep(wait_this_time); - - uint32_t _time_pass = (uint32_t)(QDateTime::currentMSecsSinceEpoch() - time_begin) * m_speed; - if(_time_pass > total_ms) - _time_pass = total_ms; - if(_time_pass - time_last_pass > 200) { - UpdateData* _passed_ms = new UpdateData(TYPE_PLAYED_MS); - _passed_ms->played_ms(_time_pass); - emit signal_update_data(_passed_ms); - time_last_pass = _time_pass; - } - - time_wait -= wait_this_time; - if(time_wait == 0) { - emit signal_update_data(dat); - break; - } - } - - } - } - } - - - // msg = LOCAL8BIT("开始播放..."); - // m_thr_play->_notify_error(msg); -} -#endif - void ThrData::_prepare() { UpdateData* d = new UpdateData(TYPE_HEADER_INFO); diff --git a/client/tp-player/thr_data.h b/client/tp-player/thr_data.h index e4adcbe..963203e 100644 --- a/client/tp-player/thr_data.h +++ b/client/tp-player/thr_data.h @@ -6,7 +6,7 @@ #include #include #include -#include "downloader.h" +#include #include "update_data.h" #include "record_format.h" @@ -59,16 +59,17 @@ public: UpdateData* get_data(); private: + void _run(); bool _load_header(); bool _load_keyframe(); bool _download_file(const QString& url, const QString filename); + bool _download_file(const QString& url, QByteArray& data); void _prepare(); void _notify_message(const QString& msg); void _notify_error(const QString& err_msg); - void _notify_download(DownloadParam* param); signals: void signal_update_data(UpdateData*); @@ -89,8 +90,6 @@ private: QString m_rid; QString m_path_base; - Downloader* m_dl; - TS_RECORD_HEADER m_hdr; KeyFrames m_kf; }; diff --git a/server/www/teleport/webroot/app/controller/audit.py b/server/www/teleport/webroot/app/controller/audit.py index 41a1cb2..b49466a 100644 --- a/server/www/teleport/webroot/app/controller/audit.py +++ b/server/www/teleport/webroot/app/controller/audit.py @@ -711,8 +711,8 @@ class DoGetFileHandler(TPBaseHandler): self.set_status(416) # 416=请求范围不符合要求 return self.write('no more data.') - # we read most 4096 bytes one time. - BULK_SIZE = 4096 + # we read most 8192 bytes one time. + BULK_SIZE = 8192 total_need = file_size - offset if length != -1 and length < total_need: total_need = length @@ -722,6 +722,7 @@ class DoGetFileHandler(TPBaseHandler): read_this_time = BULK_SIZE if total_need > BULK_SIZE else total_need while read_this_time > 0: self.write(f.read(read_this_time)) + self.flush() total_read += read_this_time if total_read >= total_need: break From 3aa475587698ea8b8c76c2de790b9707c93206d1 Mon Sep 17 00:00:00 2001 From: Apex Liu Date: Mon, 4 Nov 2019 03:34:11 +0800 Subject: [PATCH 31/44] .tmp. --- client/tp-player/bar.cpp | 4 +- client/tp-player/mainwindow.cpp | 19 +- client/tp-player/mainwindow.h | 3 + client/tp-player/record_format.h | 5 + client/tp-player/thr_data.cpp | 526 ++++++++++++++---------------- client/tp-player/thr_data.h | 10 +- client/tp-player/thr_download.cpp | 292 +++++++++++++++++ client/tp-player/thr_download.h | 72 ++++ client/tp-player/thr_play.cpp | 167 ++-------- client/tp-player/thr_play.h | 4 +- client/tp-player/tp-player.pro | 6 +- 11 files changed, 675 insertions(+), 433 deletions(-) create mode 100644 client/tp-player/thr_download.cpp create mode 100644 client/tp-player/thr_download.h diff --git a/client/tp-player/bar.cpp b/client/tp-player/bar.cpp index 14044a3..c1cac0f 100644 --- a/client/tp-player/bar.cpp +++ b/client/tp-player/bar.cpp @@ -219,7 +219,7 @@ void Bar::_init_imgages() { int h = fm.height(); if(h < m_res[res_chkbox_normal].height()) h = m_res[res_chkbox_normal].height(); - m_rc_skip = QRect(0, 0, fm.width("无操作则跳过") + CHKBOX_RIGHT_PADDING + m_res[res_chkbox_normal].width(), h); + m_rc_skip = QRect(0, 0, fm.width(LOCAL8BIT("无操作则跳过")) + CHKBOX_RIGHT_PADDING + m_res[res_chkbox_normal].width(), h); } int w = m_rc_skip.width(); @@ -254,7 +254,7 @@ void Bar::_init_imgages() { img = &m_res[res_chkbox_normal]; } ps.drawPixmap(0, chkbox_top, img->width(), img->height(), *img); - ps.drawText(QRect(text_left, text_top, w-text_left, h-text_top), Qt::AlignCenter, "无操作则跳过"); + ps.drawText(QRect(text_left, text_top, w-text_left, h-text_top), Qt::AlignCenter, LOCAL8BIT("无操作则跳过")); } } } diff --git a/client/tp-player/mainwindow.cpp b/client/tp-player/mainwindow.cpp index 7999b7e..db7407f 100644 --- a/client/tp-player/mainwindow.cpp +++ b/client/tp-player/mainwindow.cpp @@ -187,7 +187,7 @@ void MainWindow::_start_play_thread() { m_thr_play = nullptr; } - m_thr_play = new ThrPlay(); + m_thr_play = new ThrPlay(this); connect(m_thr_play, SIGNAL(signal_update_data(UpdateData*)), this, SLOT(_do_update_data(UpdateData*))); m_thr_play->speed(m_bar.get_speed()); @@ -340,6 +340,11 @@ void MainWindow::_do_update_data(UpdateData* dat) { } else if(dat->data_type() == TYPE_MESSAGE) { + if(dat->message().isEmpty()) { + m_show_message = false; + return; + } + m_show_message = true; qDebug("1message, w=%d, h=%d", m_canvas.width(), m_canvas.height()); @@ -430,10 +435,14 @@ void MainWindow::_do_update_data(UpdateData* dat) { m_timer_bar_delay_hide.start(2000); QString title; - if (m_rec_hdr.basic.conn_port == 3389) - title.sprintf("[%s] %s@%s [Teleport-RDP录像回放]", m_rec_hdr.basic.acc_username, m_rec_hdr.basic.user_username, m_rec_hdr.basic.conn_ip); - else - title.sprintf("[%s] %s@%s:%d [Teleport-RDP录像回放]", m_rec_hdr.basic.acc_username, m_rec_hdr.basic.user_username, m_rec_hdr.basic.conn_ip, m_rec_hdr.basic.conn_port); + if (m_rec_hdr.basic.conn_port == 3389) { + title = QString(LOCAL8BIT("[%1] %2@%3 [Teleport-RDP录像回放]").arg(m_rec_hdr.basic.acc_username, m_rec_hdr.basic.user_username, m_rec_hdr.basic.conn_ip)); + } + else { + QString _port; + _port.sprintf("%d", m_rec_hdr.basic.conn_port); + title = QString(LOCAL8BIT("[%1] %2@%3:%4 [Teleport-RDP录像回放]").arg(m_rec_hdr.basic.acc_username, m_rec_hdr.basic.user_username, m_rec_hdr.basic.conn_ip, _port)); + } setWindowTitle(title); diff --git a/client/tp-player/mainwindow.h b/client/tp-player/mainwindow.h index feabbf4..9c5eed6 100644 --- a/client/tp-player/mainwindow.h +++ b/client/tp-player/mainwindow.h @@ -39,6 +39,9 @@ public: // Downloader* downloader() {return m_dl;} // void reset_downloader() {if(m_dl){delete m_dl;m_dl= nullptr;}} + // TODO: 将thr_data移动到thr_play线程,由play线程进行管理 + ThrData* get_thr_data() {return m_thr_data;} + private: void paintEvent(QPaintEvent *e); void mouseMoveEvent(QMouseEvent *e); diff --git a/client/tp-player/record_format.h b/client/tp-player/record_format.h index 68d7ef9..2f95c7b 100644 --- a/client/tp-player/record_format.h +++ b/client/tp-player/record_format.h @@ -3,6 +3,11 @@ #include +#define TS_TPPR_TYPE_UNKNOWN 0x0000 +#define TS_TPPR_TYPE_SSH 0x0001 +#define TS_TPPR_TYPE_RDP 0x0101 + + #define TS_RECORD_TYPE_RDP_POINTER 0x12 // 鼠标坐标位置改变,用于绘制虚拟鼠标 #define TS_RECORD_TYPE_RDP_IMAGE 0x13 // 服务端返回的图像,用于展示 #define TS_RECORD_TYPE_RDP_KEYFRAME 0x14 // diff --git a/client/tp-player/thr_data.cpp b/client/tp-player/thr_data.cpp index 25a5f06..846ac39 100644 --- a/client/tp-player/thr_data.cpp +++ b/client/tp-player/thr_data.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "thr_play.h" #include "thr_data.h" @@ -25,12 +26,12 @@ ThrData::ThrData(MainWindow* mainwin, const QString& res) { // m_dl = nullptr; #ifdef __APPLE__ - QString data_path_base = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); - data_path_base += "/tp-testdata/"; + m_data_path_base = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); + m_data_path_base += "/tp-testdata/"; #else - m_local_data_path_base = QCoreApplication::applicationDirPath() + "/record"; + m_data_path_base = QCoreApplication::applicationDirPath() + "/record"; #endif - qDebug("data-path-base: %s", m_local_data_path_base.toStdString().c_str()); + qDebug("data-path-base: %s", m_data_path_base.toStdString().c_str()); // qDebug() << "AppConfigLocation:" << QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation); // qDebug() << "AppDataLocation:" << QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); @@ -50,6 +51,7 @@ GenericCacheLocation: "C:/Users/apex/AppData/Local/cache" } ThrData::~ThrData() { + _clear_data(); } void ThrData::stop() { @@ -72,224 +74,294 @@ void ThrData::_notify_error(const QString& msg) { emit signal_update_data(_msg); } -// tp-player.exe http://teleport.domain.com:7190/{sub/path/}tp_1491560510_ca67fceb75a78c9d/1234 (注意,并不直接访问此URI,实际上其并不存在) -// TP服务器地址(可能包含子路径哦,例如上例中的{sub/path/}部分)/session-id(用于判断当前授权用户)/录像会话编号 - void ThrData::run() { _run(); qDebug("ThrData thread run() end."); } void ThrData::_run() { + + QString _tmp_res = m_res.toLower(); + if(_tmp_res.startsWith("http")) { + m_need_download = true; + _notify_message(LOCAL8BIT("正在准备录像数据,请稍候...")); + + if(!m_thr_download.init(m_data_path_base, m_res)) { + _notify_error(QString("%1\n\n%2").arg(LOCAL8BIT("无法下载录像文件!\n\n"), m_res)); + return; + } + + m_thr_download.start(); + msleep(100); + + for(;;) { + if(m_need_stop) + return; + if(!m_thr_download.is_running() || m_thr_download.is_tpk_downloaded()) + break; + msleep(100); + } + + if(!m_thr_download.is_tpk_downloaded()) + return; + + m_thr_download.get_data_path(m_data_path); + } + else { + QFileInfo fi_chk_link(m_res); + if(fi_chk_link.isSymLink()) + m_res = fi_chk_link.symLinkTarget(); + + QFileInfo fi(m_res); + if(!fi.exists()) { + _notify_error(QString("%1\n\n%2").arg(LOCAL8BIT("指定的文件或目录不存在!"), m_res)); + return; + } + + if(fi.isFile()) { + m_data_path = fi.path(); + } + else if(fi.isDir()) { + m_data_path = m_res; + } + + m_data_path = QDir::toNativeSeparators(m_data_path); + } + + // 到这里,.tpr和.tpk文件均已经下载完成了。 + if(!_load_header()) return; if(!_load_keyframe()) return; - uint32_t file_idx = 0; + + UpdateData* dat = new UpdateData(TYPE_HEADER_INFO); + dat->alloc_data(sizeof(TS_RECORD_HEADER)); + memcpy(dat->data_buf(), &m_hdr, sizeof(TS_RECORD_HEADER)); + emit signal_update_data(dat); + + + /* + // fake-code: + file_index = 0; + for(;;) { + if(file_index >= file_count) { + msleep(500); + continue; + } + + + if(queue.size < 500) { + need_pkg = 1000 - queue.size; + + for(i = 0; i < need_pkg; i++) { + if(f.not_open) { + f.open(file_index); + } + + pkg = read_pkg() + if(f.to_end) { + f.close(); + file_index += 1; + if(file_index >= file_count) + break; + } + + queue.add(pkg) + } + + if(file_index >= file_count) + break; + } + else { + msleep(100); + } + } + + */ + + QFile* fdata = nullptr; + uint32_t file_idx = 0; + uint32_t start_offset = 0; + qint64 file_size = 0; + qint64 file_processed = 0; + qint64 read_len = 0; + QString str_fidx; + + for(;;) { + // 任何时候确保第一时间响应退出操作 if(m_need_stop) - break; - QString str_fidx; - str_fidx.sprintf("%d", file_idx+1); - - QString tpd_fname = QString("%1/tp-rdp-%2.tpd").arg(m_path_base, str_fidx); - tpd_fname = QDir::toNativeSeparators(tpd_fname); - - if(m_need_download) { - QString tmp_fname = QString("%1/tp-rdp-%2.tpd.downloading").arg(m_path_base, str_fidx); - tmp_fname = QDir::toNativeSeparators(tmp_fname); - - QFileInfo fi_tmp(tmp_fname); - if(fi_tmp.isFile()) { - QFile::remove(tmp_fname); - } - - QFileInfo fi_tpd(tpd_fname); - if(!fi_tpd.exists()) { - QString url = QString("%1/audit/get-file?act=read&type=rdp&rid=%2&f=tp-rdp-%3.tpd").arg(m_url_base, m_rid, str_fidx); - - qDebug() << "URL : " << url; - qDebug() << "TPD : " << tmp_fname; - if(!_download_file(url, tmp_fname)) - return; - - if(!QFile::rename(tmp_fname, tpd_fname)) - return; - } - } - - qDebug() << "TPD: " << tpd_fname; - - QFile f_tpd(tpd_fname); - if(!f_tpd.open(QFile::ReadOnly)) { - qDebug() << "Can not open " << tpd_fname << " for read."; - _notify_error(QString("%1\n\n%3").arg(LOCAL8BIT("无法打开录像数据文件!"), tpd_fname)); return; + + // 如果所有文件都已经处理完了,则等待(可能用户会拖动滚动条,或者重新播放) + if(file_idx >= m_hdr.info.dat_file_count) { + msleep(500); + continue; } - qint64 filesize = f_tpd.size(); - qint64 processed = 0; + // 看看待播放队列中还有多少个数据包 + int pkg_count_in_queue = 0; + int pkg_need_add = 0; - // 加载并解析到待播放队列 - qint64 read_len = 0; - for(;;) { - { -// if(m_need_stop) -// break; + m_locker.lock(); + pkg_count_in_queue = m_data.size(); + m_locker.unlock(); -// m_locker.lock(); + // 少于500个的话,补足到1000个 + if(m_data.size() < 500) + pkg_need_add = 1000 - pkg_count_in_queue; -// if(m_data.size() > 500) { -// msleep(1000); -// continue; -// } + if(pkg_need_add == 0) { + msleep(100); + continue; + } -// m_locker.unlock(); + for(int i = 0; i < pkg_need_add; ++i) { + if(m_need_stop) + return; + + // 如果数据文件尚未打开,则打开它 + if(fdata == nullptr) { + str_fidx.sprintf("%d", file_idx+1); + QString tpd_fname = QString("%1/tp-rdp-%2.tpd").arg(m_data_path, str_fidx); + tpd_fname = QDir::toNativeSeparators(tpd_fname); + + QFileInfo fi_tpd(tpd_fname); + if(!fi_tpd.exists()) { + if(m_need_download) { + // 此文件尚未下载完成,等待 + for(;;) { + if(m_need_stop) + return; + if(!m_thr_download.is_running() || m_thr_download.is_tpd_downloaded(file_idx)) + break; + msleep(100); + } + + // 下载失败了 + if(!m_thr_download.is_tpd_downloaded(file_idx)) + return; + } + } + + fdata = new QFile(tpd_fname); + if(!fdata->open(QFile::ReadOnly)) { + qDebug() << "Can not open " << tpd_fname << " for read."; + _notify_error(QString("%1\n\n%2").arg(LOCAL8BIT("无法打开录像数据文件!"), tpd_fname)); + return; + } + + file_size = fdata->size(); + file_processed = 0; + qDebug("Open file, processed: %" PRId64 ", size: %" PRId64, file_processed, file_size); } +// qDebug("B processed: %" PRId64 ", size: %" PRId64, file_processed, file_size); - if(filesize - processed < sizeof(TS_RECORD_PKG)) { - qDebug("invaid tp-rdp-%d.tpd file, filesize=%d, processed=%d, need=%d.", file_idx+1, filesize, processed, sizeof(TS_RECORD_PKG)); - _notify_error(QString("%1\n\n%3").arg(LOCAL8BIT("错误的录像数据文件!"), tpd_fname)); + //---------------------------------- + // 读取一个数据包 + //---------------------------------- + if(file_size - file_processed < sizeof(TS_RECORD_PKG)) { + qDebug("invaid tp-rdp-%d.tpd file, filesize=%" PRId64 ", processed=%" PRId64 ", need=%d.", file_idx+1, file_size, file_processed, sizeof(TS_RECORD_PKG)); + _notify_error(QString("%1\ntp-rdp-%2.tpd").arg(LOCAL8BIT("错误的录像数据文件!"), str_fidx)); return; } TS_RECORD_PKG pkg; - read_len = f_tpd.read(reinterpret_cast(&pkg), sizeof(pkg)); - if(read_len == 0) - break; + read_len = fdata->read(reinterpret_cast(&pkg), sizeof(TS_RECORD_PKG)); + // if(read_len == 0) + // break; if(read_len != sizeof(TS_RECORD_PKG)) { - qDebug("invaid tp-rdp-%d.tpd file, read_len=%d (1).", file_idx+1, read_len); - _notify_error(QString("%1\n\n%3").arg(LOCAL8BIT("错误的录像数据文件!"), tpd_fname)); + qDebug("invaid tp-rdp-%d.tpd file, read_len=%" PRId64 " (1).", file_idx+1, read_len); + _notify_error(QString("%1\ntp-rdp-%2.tpd").arg(LOCAL8BIT("错误的录像数据文件!"), str_fidx)); return; } - processed += sizeof(TS_RECORD_PKG); + file_processed += sizeof(TS_RECORD_PKG); - if(filesize - processed < pkg.size) { + if(file_size - file_processed < pkg.size) { qDebug("invaid tp-rdp-%d.tpd file (2).", file_idx+1); - _notify_error(QString("%1\n\n%3").arg(LOCAL8BIT("错误的录像数据文件!"), tpd_fname)); + _notify_error(QString("%1\ntp-rdp-%2.tpd").arg(LOCAL8BIT("错误的录像数据文件!"), str_fidx)); return; } - if(pkg.type == TS_RECORD_TYPE_RDP_KEYFRAME) { - qDebug("----key frame: %d, pkg.size=%d", pkg.time_ms, pkg.size); - if(pkg.size > 0) { - f_tpd.read(pkg.size); - } - continue; + if(pkg.size == 0) { + qDebug("################## too bad."); } UpdateData* dat = new UpdateData(TYPE_DATA); dat->alloc_data(sizeof(TS_RECORD_PKG) + pkg.size); memcpy(dat->data_buf(), &pkg, sizeof(TS_RECORD_PKG)); - read_len = f_tpd.read(reinterpret_cast(dat->data_buf()+sizeof(TS_RECORD_PKG)), pkg.size); + read_len = fdata->read(reinterpret_cast(dat->data_buf()+sizeof(TS_RECORD_PKG)), pkg.size); if(read_len != pkg.size) { delete dat; - qDebug("invaid tp-rdp-%d.tpd file (3).", file_idx+1); - _notify_error(QString("%1\n\n%3").arg(LOCAL8BIT("错误的录像数据文件!"), tpd_fname)); + qDebug("invaid tp-rdp-%d.tpd file, read_len=%" PRId64 " (3).", file_idx+1, read_len); + _notify_error(QString("%1\ntp-rdp-%2.tpd").arg(LOCAL8BIT("错误的录像数据文件!"), str_fidx)); return; } + file_processed += pkg.size; + + // 跳过关键帧 + // TODO: 拖动滚动条后,需要显示一次关键帧数据,然后跳过后续关键帧。 + if(pkg.type == TS_RECORD_TYPE_RDP_KEYFRAME) { + qDebug("----key frame: %ld, processed=%" PRId64 ", pkg.size=%d", pkg.time_ms, file_processed, pkg.size); + delete dat; + dat = nullptr; + } - { + // 数据放到待播放列表中 + if(dat) { m_locker.lock(); m_data.enqueue(dat); -// qDebug("data count: %d", m_data.size()); + qDebug("queue data count: %d", m_data.size()); + m_locker.unlock(); + } + + // 如果此文件已经处理完毕,则关闭文件,这样下次处理一个新的文件 +// qDebug("C processed: %" PRId64 ", size: %" PRId64, file_processed, file_size); + if(file_processed >= file_size) { + fdata->close(); + delete fdata; + fdata = nullptr; + file_idx++; + } + + if(file_idx >= m_hdr.info.dat_file_count) { + UpdateData* dat = new UpdateData(TYPE_END); + m_locker.lock(); + m_data.enqueue(dat); + qDebug("queue data count: %d", m_data.size()); m_locker.unlock(); } } - - - - - file_idx += 1; - if(file_idx >= m_hdr.info.dat_file_count) - break; } } bool ThrData::_load_header() { QString msg; - QString _tmp_res = m_res.toLower(); + qDebug() << "PATH_BASE: " << m_data_path; - if(_tmp_res.startsWith("http")) { - m_need_download = true; + QString filename = QString("%1/tp-rdp.tpr").arg(m_data_path); + filename = QDir::toNativeSeparators(filename); + qDebug() << "TPR: " << filename; + + QFile f(filename); + if(!f.open(QFile::ReadOnly)) { + qDebug() << "Can not open " << filename << " for read."; + _notify_error(QString("%1\n\n%2").arg(LOCAL8BIT("无法打开录像信息文件!"), filename)); + return false; } - if(m_need_download) { - _notify_message(LOCAL8BIT("正在准备录像数据,请稍候...")); + memset(&m_hdr, 0, sizeof(TS_RECORD_HEADER)); - QStringList _uris = m_res.split('/'); - if(_uris.size() < 3) { - qDebug() << "invalid param: " << m_res; - return false; - } - - m_sid = _uris[_uris.size()-2]; - m_rid = _uris[_uris.size()-1]; - m_url_base = m_res.left(m_res.length() - m_sid.length() - m_rid.length() - 2); - - qDebug() << "url-base:[" << m_url_base << "], sid:[" << m_sid << "], rid:[" << m_rid << "]"; - - // download .tpr - QString url = QString("%1/audit/get-file?act=read&type=rdp&rid=%2&f=tp-rdp.tpr").arg(m_url_base, m_rid); - QByteArray data; - if(!_download_file(url, data)) - return false; - - if(data.size() != sizeof(TS_RECORD_HEADER)) { - qDebug("invalid header file. %d", data.size()); - _notify_error(QString("%1\n\n%2").arg(LOCAL8BIT("指定的文件或目录不存在!"), _tmp_res)); - return false; - } - - memcpy(&m_hdr, data.data(), sizeof(TS_RECORD_HEADER)); - } - else { - QFileInfo fi_chk_link(m_res); - if(fi_chk_link.isSymLink()) - _tmp_res = fi_chk_link.symLinkTarget(); - else - _tmp_res = m_res; - - QFileInfo fi(_tmp_res); - if(!fi.exists()) { - _notify_error(QString("%1\n\n%2").arg(LOCAL8BIT("指定的文件或目录不存在!"), _tmp_res)); - return false; - } - - if(fi.isFile()) { - m_path_base = fi.path(); - } - else if(fi.isDir()) { - m_path_base = _tmp_res; - } - - m_path_base = QDir::toNativeSeparators(m_path_base); - - qDebug() << "PATH_BASE: " << m_path_base; - - QString filename = QString("%1/tp-rdp.tpr").arg(m_path_base); - filename = QDir::toNativeSeparators(filename); - qDebug() << "TPR: " << filename; - - QFile f(filename); - if(!f.open(QFile::ReadOnly)) { - qDebug() << "Can not open " << filename << " for read."; - _notify_error(QString("%1\n\n%2").arg(LOCAL8BIT("无法打开录像信息文件!"), filename)); - return false; - } - - memset(&m_hdr, 0, sizeof(TS_RECORD_HEADER)); - - qint64 read_len = 0; - read_len = f.read(reinterpret_cast(&m_hdr), sizeof(TS_RECORD_HEADER)); - if(read_len != sizeof(TS_RECORD_HEADER)) { - qDebug() << "invaid .tpr file."; - _notify_error(QString("%1\n\n%2").arg(LOCAL8BIT("错误的录像信息文件!"), filename)); - return false; - } + qint64 read_len = 0; + read_len = f.read(reinterpret_cast(&m_hdr), sizeof(TS_RECORD_HEADER)); + if(read_len != sizeof(TS_RECORD_HEADER)) { + qDebug() << "invaid .tpr file."; + _notify_error(QString("%1\n\n%2").arg(LOCAL8BIT("错误的录像信息文件!"), filename)); + return false; } if(m_hdr.info.ver != 4) { @@ -308,94 +380,13 @@ bool ThrData::_load_header() { return false; } - - // 下载得到的数据应该是一个TS_RECORD_HEADER,解析此数据,生成本地文件路径,并保存之。 - if(m_need_download) { - QDateTime timeUTC; - // timeUTC.setTimeSpec(Qt::UTC); - // timeUTC.setTime_t(m_hdr.basic.timestamp); - timeUTC.setSecsSinceEpoch(m_hdr.basic.timestamp); - QString strUTC = timeUTC.toString("yyyyMMdd-hhmmss"); - - QString strAcc(m_hdr.basic.acc_username); - int idx = strAcc.indexOf('\\'); - if(-1 != idx) { - QString _domain = strAcc.left(idx); - QString _user = strAcc.right(strAcc.length() - idx - 1); - strAcc = _user + "@" + _domain; - } - - // .../record/RDP-211-admin-user@domain-192.168.0.68-20191015-020243 - m_path_base = QString("%1/RDP-%2-%3-%4-%5-%6").arg(m_local_data_path_base, - m_rid, - m_hdr.basic.user_username, - strAcc, - m_hdr.basic.host_ip, - strUTC - ); - - m_path_base = QDir::toNativeSeparators(m_path_base); - - qDebug() << "PATH_BASE: " << m_path_base; - - QDir dir; - dir.mkpath(m_path_base); - QFileInfo fi; - fi.setFile(m_path_base); - if(!fi.isDir()) { - qDebug("can not create folder to save downloaded file."); - return false; - } - - QString filename = QString("%1/tp-rdp.tpr").arg(m_path_base); - filename = QDir::toNativeSeparators(filename); - qDebug() << "TPR: " << filename; - - QFile f; - f.setFileName(filename); - if(!f.open(QIODevice::WriteOnly | QFile::Truncate)){ - qDebug("open file for write failed."); - return false; - } - - qint64 written = f.write(reinterpret_cast(&m_hdr), sizeof(TS_RECORD_HEADER)); - f.flush(); - f.close(); - - if(written != sizeof(TS_RECORD_HEADER)) { - qDebug("save header file failed."); - return false; - } - } - return true; } bool ThrData::_load_keyframe() { - QString tpk_fname = QString("%1/tp-rdp.tpk").arg(m_path_base); + QString tpk_fname = QString("%1/tp-rdp.tpk").arg(m_data_path); tpk_fname = QDir::toNativeSeparators(tpk_fname); - if(m_need_download) { - QString tmp_fname = QString("%1/tp-rdp.tpk.downloading").arg(m_path_base); - tmp_fname = QDir::toNativeSeparators(tmp_fname); - - QFileInfo fi_tmp(tmp_fname); - if(fi_tmp.isFile()) { - QFile::remove(tmp_fname); - } - - QFileInfo fi_tpk(tpk_fname); - if(!fi_tpk.exists()) { - QString url = QString("%1/audit/get-file?act=read&type=rdp&rid=%2&f=tp-rdp.tpk").arg(m_url_base, m_rid); - qDebug() << "TPK: " << tmp_fname; - if(!_download_file(url, tmp_fname)) - return false; - - if(!QFile::rename(tmp_fname, tpk_fname)) - return false; - } - } - qDebug() << "TPK: " << tpk_fname; QFile f_kf(tpk_fname); @@ -430,40 +421,6 @@ bool ThrData::_load_keyframe() { return true; } -bool ThrData::_download_file(const QString& url, const QString filename) { - if(!m_need_download) { - qDebug() << "download not necessary."; - return false; - } - - Downloader dl; - if(!dl.request(url, m_sid, filename)) { - qDebug() << "download failed."; - _notify_error(QString("%1").arg(LOCAL8BIT("下载文件失败!"))); - return false; - } - - return true; -} - -bool ThrData::_download_file(const QString& url, QByteArray& data) { - if(!m_need_download) { - qDebug() << "download not necessary."; - return false; - } - - Downloader dl; - - if(!dl.request(url, m_sid, &data)) { - qDebug() << "download failed."; - _notify_error(QString("%1").arg(LOCAL8BIT("下载文件失败!"))); - return false; - } - - return true; -} - - void ThrData::_prepare() { UpdateData* d = new UpdateData(TYPE_HEADER_INFO); @@ -473,10 +430,21 @@ void ThrData::_prepare() { } UpdateData* ThrData::get_data() { + UpdateData* d = nullptr; m_locker.lock(); - UpdateData* d = m_data.dequeue(); + if(m_data.size() > 0) + d = m_data.dequeue(); m_locker.unlock(); return d; } + +void ThrData::_clear_data() { + m_locker.lock(); + while(m_data.size() > 0) { + UpdateData* d = m_data.dequeue(); + delete d; + } + m_locker.unlock(); +} diff --git a/client/tp-player/thr_data.h b/client/tp-player/thr_data.h index 963203e..dab105c 100644 --- a/client/tp-player/thr_data.h +++ b/client/tp-player/thr_data.h @@ -9,6 +9,7 @@ #include #include "update_data.h" #include "record_format.h" +#include "thr_download.h" /* 为支持“边下载,边播放”、“可拖动进度条”等功能,录像数据会分为多个文件存放,目前每个文件约4MB。 @@ -63,9 +64,8 @@ private: bool _load_header(); bool _load_keyframe(); - bool _download_file(const QString& url, const QString filename); - bool _download_file(const QString& url, QByteArray& data); + void _clear_data(); void _prepare(); void _notify_message(const QString& msg); @@ -79,16 +79,18 @@ private: QQueue m_data; QMutex m_locker; + ThrDownload m_thr_download; + bool m_need_stop; bool m_need_download; QString m_res; - QString m_local_data_path_base; + QString m_data_path_base; QString m_url_base; QString m_sid; QString m_rid; - QString m_path_base; + QString m_data_path; TS_RECORD_HEADER m_hdr; KeyFrames m_kf; diff --git a/client/tp-player/thr_download.cpp b/client/tp-player/thr_download.cpp new file mode 100644 index 0000000..cbfe0f7 --- /dev/null +++ b/client/tp-player/thr_download.cpp @@ -0,0 +1,292 @@ +#include +#include +#include +#include + +#include "thr_download.h" +#include "util.h" +#include "downloader.h" +#include "record_format.h" + +//================================================================= +// ThrDownload +//================================================================= + +ThrDownload::ThrDownload() { + m_need_stop = false; + m_have_tpr = false; + m_have_tpk = false; + m_have_tpd = nullptr; + m_need_tpk = false; + m_running = true; +} + +ThrDownload::~ThrDownload() { + if(m_have_tpd) + delete[] m_have_tpd; +} + +// tp-player.exe http://teleport.domain.com:7190/{sub/path/}tp_1491560510_ca67fceb75a78c9d/1234 (注意,并不直接访问此URI,实际上其并不存在) +// TP服务器地址(可能包含子路径哦,例如上例中的{sub/path/}部分)/session-id(用于判断当前授权用户)/录像会话编号 + +bool ThrDownload::init(const QString& local_data_path_base, const QString &res) { + m_data_path_base = local_data_path_base; + + QString _tmp_res = res.toLower(); + if(!_tmp_res.startsWith("http")) { + return false; + } + + QStringList _uris = res.split('/'); + if(_uris.size() < 3) { + return false; + } + + m_sid = _uris[_uris.size()-2]; + m_rid = _uris[_uris.size()-1]; + m_url_base = res.left(res.length() - m_sid.length() - m_rid.length() - 2); + + if(m_sid.length() == 0 || m_rid.length() == 0 || m_url_base.length() == 0) + return false; + + return true; +} + +void ThrDownload::stop() { + if(!m_running) + return; +// if(!isRunning()) +// return; + m_need_stop = true; + wait(); + qDebug("data thread stop() end."); +} + +// tp-player.exe http://teleport.domain.com:7190/{sub/path/}tp_1491560510_ca67fceb75a78c9d/1234 (注意,并不直接访问此URI,实际上其并不存在) +// TP服务器地址(可能包含子路径哦,例如上例中的{sub/path/}部分)/session-id(用于判断当前授权用户)/录像会话编号 + +void ThrDownload::run() { + _run(); + m_running = false; + qDebug("ThrDownload thread run() end."); +} + +void ThrDownload::_run() { +// m_state = statDownloading; + + if(!_download_tpr()) { +// m_state = statFailDone; + return; + } + m_have_tpr = true; + + m_have_tpd = new bool[m_tpd_count]; + for(uint32_t i = 0; i < m_tpd_count; ++i) { + m_have_tpd[i] = false; + } + + if(m_need_tpk) { + if(!_download_tpk()) { +// m_state = statFailDone; + return; + } + m_have_tpk = true; + } + + uint32_t file_idx = 0; + for(;;) { + if(m_need_stop) + break; + QString str_fidx; + str_fidx.sprintf("%d", file_idx+1); + + QString tpd_fname = QString("%1/tp-rdp-%2.tpd").arg(m_data_path, str_fidx); + tpd_fname = QDir::toNativeSeparators(tpd_fname); + + QString tmp_fname = QString("%1/tp-rdp-%2.tpd.downloading").arg(m_data_path, str_fidx); + tmp_fname = QDir::toNativeSeparators(tmp_fname); + + QFileInfo fi_tmp(tmp_fname); + if(fi_tmp.isFile()) { + QFile::remove(tmp_fname); + } + + QFileInfo fi_tpd(tpd_fname); + if(!fi_tpd.exists()) { + QString url = QString("%1/audit/get-file?act=read&type=rdp&rid=%2&f=tp-rdp-%3.tpd").arg(m_url_base, m_rid, str_fidx); + + qDebug() << "URL : " << url; + qDebug() << "TPD : " << tmp_fname; + if(!_download_file(url, tmp_fname)) { +// m_state = statFailDone; + return; + } + + if(!QFile::rename(tmp_fname, tpd_fname)) { +// m_state = statFailDone; + return; + } + } + + m_have_tpd[file_idx] = true; + + file_idx += 1; + if(file_idx >= m_tpd_count) + break; + } + +// m_state = statSuccessDone; +} + +bool ThrDownload::_download_tpr() { + QString url = QString("%1/audit/get-file?act=read&type=rdp&rid=%2&f=tp-rdp.tpr").arg(m_url_base, m_rid); + QByteArray data; + if(!_download_file(url, data)) + return false; + + if(data.size() != sizeof(TS_RECORD_HEADER)) { + qDebug("invalid header data. %d", data.size()); + m_error = QString(LOCAL8BIT("录像信息文件数据错误!")); + return false; + } + + TS_RECORD_HEADER* hdr = reinterpret_cast(data.data()); +// if(hdr->info.ver != 4) { +// qDebug() << "invaid .tpr file."; +// m_last_error = QString("%1 %2%3").arg(LOCAL8BIT("不支持的录像文件版本 "), QString(hdr->info.ver), LOCAL8BIT("!\n\n此播放器支持录像文件版本 4。")); +// return false; +// } + +// if(m_hdr.basic.width == 0 || m_hdr.basic.height == 0) { +// _notify_error(LOCAL8BIT("错误的录像信息,未记录窗口尺寸!")); +// return false; +// } + +// if(m_hdr.info.dat_file_count == 0) { +// _notify_error(LOCAL8BIT("错误的录像信息,未记录数据文件数量!")); +// return false; +// } + + + // 下载得到的数据应该是一个TS_RECORD_HEADER,解析此数据,生成本地文件路径,并保存之。 + QDateTime timeUTC; + // timeUTC.setTimeSpec(Qt::UTC); + // timeUTC.setTime_t(m_hdr.basic.timestamp); + timeUTC.setSecsSinceEpoch(hdr->basic.timestamp); + QString strUTC = timeUTC.toString("yyyyMMdd-hhmmss"); + + QString strAcc(hdr->basic.acc_username); + int idx = strAcc.indexOf('\\'); + if(-1 != idx) { + QString _domain = strAcc.left(idx); + QString _user = strAcc.right(strAcc.length() - idx - 1); + strAcc = _user + "@" + _domain; + } + + QString strType; + if(hdr->info.type == TS_TPPR_TYPE_SSH) { + strType = "SSH"; + } + else if(hdr->info.type == TS_TPPR_TYPE_RDP) { + strType = "RDP"; + m_need_tpk = true; + } + else { + strType = "UNKNOWN"; + } + + // .../record/RDP-211-admin-user@domain-192.168.0.68-20191015-020243 + m_data_path = QString("%1/%2-%3-%4-%5-%6-%7").arg(m_data_path_base, strType, m_rid, hdr->basic.user_username, strAcc, hdr->basic.host_ip, strUTC); + m_data_path = QDir::toNativeSeparators(m_data_path); + qDebug() << "PATH_BASE: " << m_data_path; + + QDir dir; + dir.mkpath(m_data_path); + QFileInfo fi; + fi.setFile(m_data_path); + if(!fi.isDir()) { + qDebug("can not create folder to save downloaded file."); + return false; + } + + QString filename = QString("%1/tp-rdp.tpr").arg(m_data_path); + filename = QDir::toNativeSeparators(filename); + qDebug() << "TPR: " << filename; + + QFile f; + f.setFileName(filename); + if(!f.open(QIODevice::WriteOnly | QFile::Truncate)){ + qDebug("open file for write failed."); + return false; + } + + qint64 written = f.write(reinterpret_cast(hdr), sizeof(TS_RECORD_HEADER)); + f.flush(); + f.close(); + + if(written != sizeof(TS_RECORD_HEADER)) { + qDebug("save header file failed."); + return false; + } + + m_tpd_count = hdr->info.dat_file_count; + + return true; +} + +bool ThrDownload::_download_tpk() { + QString tpk_fname = QString("%1/tp-rdp.tpk").arg(m_data_path); + tpk_fname = QDir::toNativeSeparators(tpk_fname); + + QString tmp_fname = QString("%1/tp-rdp.tpk.downloading").arg(m_data_path); + tmp_fname = QDir::toNativeSeparators(tmp_fname); + + QFileInfo fi_tmp(tmp_fname); + if(fi_tmp.isFile()) { + QFile::remove(tmp_fname); + } + + QFileInfo fi_tpk(tpk_fname); + if(!fi_tpk.exists()) { + QString url = QString("%1/audit/get-file?act=read&type=rdp&rid=%2&f=tp-rdp.tpk").arg(m_url_base, m_rid); + qDebug() << "TPK: " << tmp_fname; + if(!_download_file(url, tmp_fname)) + return false; + + if(!QFile::rename(tmp_fname, tpk_fname)) + return false; + } + + return true; +} + +bool ThrDownload::_download_file(const QString& url, const QString filename) { + Downloader dl; + if(!dl.request(url, m_sid, filename)) { + qDebug() << "download failed."; + m_error = QString("%1").arg(LOCAL8BIT("下载文件失败!")); + return false; + } + + return true; +} + +bool ThrDownload::_download_file(const QString& url, QByteArray& data) { + Downloader dl; + if(!dl.request(url, m_sid, &data)) { + qDebug() << "download failed."; + m_error = QString("%1").arg(LOCAL8BIT("下载文件失败!")); + return false; + } + + return true; +} + +bool ThrDownload::is_tpd_downloaded(uint32_t file_idx) const { + if(!m_have_tpd) + return false; + if(file_idx >= m_tpd_count) + return false; + return m_have_tpd[file_idx]; +} + diff --git a/client/tp-player/thr_download.h b/client/tp-player/thr_download.h new file mode 100644 index 0000000..e5478e0 --- /dev/null +++ b/client/tp-player/thr_download.h @@ -0,0 +1,72 @@ +#ifndef THR_DOWNLOAD_H +#define THR_DOWNLOAD_H + +#include +#include +#include +#include + +class ThrDownload : public QThread { + Q_OBJECT + +//public: +// enum State { +// statStarting, +// statDownloading, +// statInvalidParam, +// statFailDone, +// statSuccessDone +// }; + +public: + ThrDownload(); + ~ThrDownload(); + + bool init(const QString& local_data_path_base, const QString& res); + + virtual void run(); + void stop(); + + bool is_running() const {return m_running;} + + bool is_tpr_downloaded() const {return m_have_tpr;} + bool is_tpk_downloaded() const {return m_have_tpk;} + bool is_tpd_downloaded(uint32_t file_idx) const; + bool get_data_path(QString& path) const { + if(m_data_path.isEmpty()) + return false; + path = m_data_path; + return true; + } + +private: + void _run(); + + bool _download_tpr(); + bool _download_tpk(); + + bool _download_file(const QString& url, const QString filename); + bool _download_file(const QString& url, QByteArray& data); + +private: + bool m_need_stop; + + QString m_data_path_base; + + QString m_url_base; + QString m_sid; + QString m_rid; + QString m_data_path; + + bool m_running; + bool m_have_tpr; + bool m_have_tpk; + bool m_need_tpk; + + uint32_t m_tpd_count; + bool* m_have_tpd; + + QString m_error; +}; + +#endif // THR_DOWNLOAD_H diff --git a/client/tp-player/thr_play.cpp b/client/tp-player/thr_play.cpp index a5a1f3f..d366efb 100644 --- a/client/tp-player/thr_play.cpp +++ b/client/tp-player/thr_play.cpp @@ -4,6 +4,7 @@ #include "thr_play.h" #include "thr_data.h" +#include "mainwindow.h" #include "record_format.h" #include "util.h" @@ -18,7 +19,8 @@ */ -ThrPlay::ThrPlay() { +ThrPlay::ThrPlay(MainWindow* mainwnd) { + m_mainwnd = mainwnd; m_need_stop = false; m_need_pause = false; m_speed = 2; @@ -63,151 +65,37 @@ void ThrPlay::_notify_error(const QString& msg) { void ThrPlay::run() { - // http://127.0.0.1:7190/tp_1491560510_ca67fceb75a78c9d/211 - // E:\work\tp4a\teleport\server\share\replay\rdp\000000211 + ThrData* thr_data = m_mainwnd->get_thr_data(); + bool first_run = true; -//#ifdef __APPLE__ -// QString currentPath = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); -// currentPath += "/tp-testdata/"; -//#else -// QString currentPath = QCoreApplication::applicationDirPath() + "/testdata/"; -//#endif + for(;;) { + if(m_need_stop) + break; - // /Users/apex/Library/Preferences/tp-player -// qDebug() << "appdata:" << QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation); - - // /private/var/folders/_3/zggrxjdx1lxcdqnfsbgpcwzh0000gn/T - //qDebug() << "tmp:" << QStandardPaths::writableLocation(QStandardPaths::TempLocation); - -// m_thr_data = new ThrData(this, m_res); -// m_thr_data->start(); - - // "正在准备录像数据,请稍候..." -// _notify_message(LOCAL8BIT("正在准备录像数据,请稍候...")); - - -#if 0 - // base of data path (include the .tpr file) - QString path_base; - - QString _tmp_res = m_res.toLower(); - if(_tmp_res.startsWith("http")) { - qDebug() << "DOWNLOAD"; - m_need_download = true; - - // "正在缓存录像数据,请稍候..." - _notify_message("正在缓存录像数据,请稍候..."); - - m_thr_data = new ThreadDownload(m_res); - m_thr_data->start(); - - QString msg; - for(;;) { - msleep(500); - - if(m_need_stop) - return; - - if(!m_thr_data->prepare(path_base, msg)) { - msg.sprintf("指定的文件或目录不存在!\n\n%s", _tmp_res.toStdString().c_str()); - _notify_error(msg); - return; - } - - if(path_base.length()) - break; - } - } - else { - { - QFileInfo fi(m_res); - if(fi.isSymLink()) - _tmp_res = fi.symLinkTarget(); - else - _tmp_res = m_res; + // 1. 从ThrData的待播放队列中取出一个数据 + UpdateData* dat = thr_data->get_data(); + if(dat == nullptr) { + msleep(20); + continue; } - QFileInfo fi(_tmp_res); - if(!fi.exists()) { - QString msg; - msg.sprintf("指定的文件或目录不存在!\n\n%s", _tmp_res.toStdString().c_str()); - _notify_error(msg); - return; + if(first_run) { + first_run = false; + _notify_message(""); } - if(fi.isFile()) { - path_base = fi.path(); + // 2. 根据数据包的信息,等待到播放时间点 + // 3. 将数据包发送给主UI界面进行显示 +// qDebug("emit one package."); + if(dat->data_type() == TYPE_END) { + _notify_message(LOCAL8BIT("播放结束")); } - else if(fi.isDir()) { - path_base = m_res; - } - - path_base += "/"; - } - - - qint64 read_len = 0; - uint32_t total_pkg = 0; - uint32_t total_ms = 0; - uint32_t file_count = 0; - - //====================================== - // 加载录像基本信息数据 - //====================================== - - QString tpr_filename(path_base); - tpr_filename += "tp-rdp.tpr"; - - QFile f_hdr(tpr_filename); - if(!f_hdr.open(QFile::ReadOnly)) { - qDebug() << "Can not open " << tpr_filename << " for read."; - QString msg; - msg.sprintf("无法打开录像信息文件!\n\n%s", tpr_filename.toStdString().c_str()); - _notify_error(msg); - return; - } - else { - UpdateData* dat = new UpdateData(TYPE_HEADER_INFO); - dat->alloc_data(sizeof(TS_RECORD_HEADER)); - - read_len = f_hdr.read((char*)(dat->data_buf()), dat->data_len()); - if(read_len != sizeof(TS_RECORD_HEADER)) { - delete dat; - qDebug() << "invaid .tpr file."; - QString msg; - msg.sprintf("错误的录像信息文件!\n\n%s", tpr_filename.toStdString().c_str()); - _notify_error(msg); - return; - } - - TS_RECORD_HEADER* hdr = (TS_RECORD_HEADER*)dat->data_buf(); - - if(hdr->info.ver != 4) { - delete dat; - qDebug() << "invaid .tpr file."; - QString msg; - msg.sprintf("不支持的录像文件版本 %d!\n\n此播放器支持录像文件版本 4。", hdr->info.ver); - _notify_error(msg); - return; - } - - if(hdr->basic.width == 0 || hdr->basic.height == 0) { - _notify_error("错误的录像信息,未记录窗口尺寸!"); - return; - } - - if(hdr->info.dat_file_count == 0) { - _notify_error("错误的录像信息,未记录数据文件数量!"); - return; - } - - total_pkg = hdr->info.packages; - total_ms = hdr->info.time_ms; - file_count = hdr->info.dat_file_count; emit signal_update_data(dat); - } + msleep(5); +} +#if 0 //====================================== // 加载录像文件数据并播放 //====================================== @@ -351,8 +239,7 @@ void ThrPlay::run() { } #endif - - qDebug("play end."); - UpdateData* _end = new UpdateData(TYPE_END); - emit signal_update_data(_end); +// qDebug("play end."); +// UpdateData* _end = new UpdateData(TYPE_END); +// emit signal_update_data(_end); } diff --git a/client/tp-player/thr_play.h b/client/tp-player/thr_play.h index dcfb190..abb142d 100644 --- a/client/tp-player/thr_play.h +++ b/client/tp-player/thr_play.h @@ -5,6 +5,7 @@ #include "update_data.h" #include "downloader.h" +class MainWindow; // 根据播放规则,将要播放的图像发送给主UI线程进行显示 class ThrPlay : public QThread { @@ -12,7 +13,7 @@ class ThrPlay : public QThread friend class ThrData; public: - ThrPlay(); + ThrPlay(MainWindow* mainwnd); ~ThrPlay(); virtual void run(); @@ -29,6 +30,7 @@ signals: void signal_update_data(UpdateData*); private: + MainWindow* m_mainwnd; bool m_need_stop; bool m_need_pause; int m_speed; diff --git a/client/tp-player/tp-player.pro b/client/tp-player/tp-player.pro index 27a2794..0a8af1d 100644 --- a/client/tp-player/tp-player.pro +++ b/client/tp-player/tp-player.pro @@ -14,7 +14,8 @@ HEADERS += \ record_format.h \ rle.h \ util.h \ - downloader.h + downloader.h \ + thr_download.h SOURCES += \ main.cpp \ @@ -25,7 +26,8 @@ SOURCES += \ update_data.cpp \ rle.c \ util.cpp \ - downloader.cpp + downloader.cpp \ + thr_download.cpp RESOURCES += \ tp-player.qrc From 1bbc109ae93cf09deb2968c2172c0755b80959f1 Mon Sep 17 00:00:00 2001 From: Apex Liu Date: Tue, 5 Nov 2019 03:05:29 +0800 Subject: [PATCH 32/44] =?UTF-8?q?=E6=92=AD=E6=94=BE=E5=99=A8=E6=B5=81?= =?UTF-8?q?=E7=95=85=E5=B7=A5=E4=BD=9C=EF=BC=8C=E8=BF=98=E7=BC=BA=E9=87=8D?= =?UTF-8?q?=E6=92=AD=E5=92=8C=E8=BF=9B=E5=BA=A6=E6=9D=A1=E6=8B=96=E5=8A=A8?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/tp-player/bar.cpp | 2 +- client/tp-player/mainwindow.cpp | 157 ++++------------------ client/tp-player/mainwindow.h | 18 +-- client/tp-player/rle.c | 8 +- client/tp-player/rle.h | 10 +- client/tp-player/thr_data.cpp | 44 +++++-- client/tp-player/thr_play.cpp | 218 ++++++++++--------------------- client/tp-player/thr_play.h | 2 + client/tp-player/update_data.cpp | 142 +++++++++++++++++++- client/tp-player/update_data.h | 57 ++++++-- 10 files changed, 329 insertions(+), 329 deletions(-) diff --git a/client/tp-player/bar.cpp b/client/tp-player/bar.cpp index c1cac0f..f5a39d5 100644 --- a/client/tp-player/bar.cpp +++ b/client/tp-player/bar.cpp @@ -423,7 +423,7 @@ void Bar::onMousePress(int x, int y) { if(m_speed_selected != speed_sel && speed_sel != speed_count) { int old_sel = m_speed_selected; m_speed_selected = speed_sel; - m_owner->speed(get_speed()); + m_owner->set_speed(get_speed()); m_owner->update(m_rc.left()+m_rc_btn_speed[old_sel].left(), m_rc.top()+m_rc_btn_speed[old_sel].top(), m_rc_btn_speed[old_sel].width(), m_rc_btn_speed[old_sel].height()); m_owner->update(m_rc.left()+m_rc_btn_speed[m_speed_hover].left(), m_rc.top()+m_rc_btn_speed[m_speed_hover].top(), m_rc_btn_speed[m_speed_hover].width(), m_rc_btn_speed[m_speed_hover].height()); return; diff --git a/client/tp-player/mainwindow.cpp b/client/tp-player/mainwindow.cpp index db7407f..0d09810 100644 --- a/client/tp-player/mainwindow.cpp +++ b/client/tp-player/mainwindow.cpp @@ -1,6 +1,5 @@ #include "mainwindow.h" #include "ui_mainwindow.h" -#include "rle.h" #include #include @@ -10,64 +9,6 @@ #include #include -bool rdpimg2QImage(QImage& out, int w, int h, int bitsPerPixel, bool isCompressed, uint8_t* dat, uint32_t len) { - switch(bitsPerPixel) { - case 15: - if(isCompressed) { - uint8_t* _dat = (uint8_t*)calloc(1, w*h*2); - if(!bitmap_decompress1(_dat, w, h, dat, len)) { - free(_dat); - return false; - } - out = QImage(_dat, w, h, QImage::Format_RGB555); - free(_dat); - } - else { - out = QImage(dat, w, h, QImage::Format_RGB555).transformed(QMatrix(1.0, 0.0, 0.0, -1.0, 0.0, 0.0)) ; - } - break; - case 16: - if(isCompressed) { - - uint8_t* _dat = (uint8_t*)calloc(1, w*h*2); - if(!bitmap_decompress2(_dat, w, h, dat, len)) { - free(_dat); - return false; - } - - // TODO: 这里需要进一步优化,直接操作QImage的buffer。 -// QTime t1; -// t1.start(); - - out = QImage(w, h, QImage::Format_RGB16); - for(int y = 0; y < h; y++) { - for(int x = 0; x < w; x++) { - uint16 a = ((uint16*)_dat)[y * w + x]; - uint8 r = ((a & 0xf800) >> 11) * 255 / 31; - uint8 g = ((a & 0x07e0) >> 5) * 255 / 63; - uint8 b = (a & 0x001f) * 255 / 31; - out.setPixelColor(x, y, QColor(r,g,b)); - } - } -// qDebug("parse: %dB, %dms", len, t1.elapsed()); - - free(_dat); - } - else { - out = QImage(dat, w, h, QImage::Format_RGB16).transformed(QMatrix(1.0, 0.0, 0.0, -1.0, 0.0, 0.0)) ; - } - break; - case 24: - qDebug() << "--------NOT support 24"; - break; - case 32: - qDebug() << "--------NOT support 32"; - break; - } - - return true; -} - static inline int min(int a, int b){ return a < b ? a : b; } @@ -80,7 +21,6 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { - //m_shown = false; m_show_default = true; m_bar_shown = false; m_bar_fade_in = false; @@ -93,8 +33,6 @@ MainWindow::MainWindow(QWidget *parent) : m_play_state = PLAY_STATE_UNKNOWN; m_thr_data = nullptr; -// m_dl = nullptr; - ui->setupUi(this); ui->centralWidget->setMouseTracking(true); @@ -125,7 +63,6 @@ MainWindow::MainWindow(QWidget *parent) : } -// connect(&m_thr_play, SIGNAL(signal_update_data(update_data*)), this, SLOT(_do_update_data(update_data*))); connect(&m_timer_first_run, SIGNAL(timeout()), this, SLOT(_do_first_run())); connect(&m_timer_bar_fade, SIGNAL(timeout()), this, SLOT(_do_bar_fade())); connect(&m_timer_bar_delay_hide, SIGNAL(timeout()), this, SLOT(_do_bar_delay_hide())); @@ -194,11 +131,16 @@ void MainWindow::_start_play_thread() { m_thr_play->start(); } -void MainWindow::speed(int s) { +void MainWindow::set_speed(int s) { if(m_thr_play) m_thr_play->speed(s); } +void MainWindow::set_skip(bool s) { + if(m_thr_play) + m_thr_play->skip(s); +} + void MainWindow::paintEvent(QPaintEvent *e) { QPainter painter(this); @@ -252,12 +194,6 @@ void MainWindow::paintEvent(QPaintEvent *e) int to_y = rc.top() + from_y; painter.drawPixmap(to_x, to_y, m_img_message, from_x, from_y, w, h); } - -// if(!m_shown) { -// m_shown = true; -// //m_thr_play.start(); -// _start_play_thread(); -// } } void MainWindow::pause() { @@ -271,7 +207,6 @@ void MainWindow::resume() { if(m_play_state == PLAY_STATE_PAUSE) m_thr_play->resume(); else if(m_play_state == PLAY_STATE_STOP) - //m_thr_play->start(); _start_play_thread(); m_play_state = PLAY_STATE_RUNNING; @@ -283,53 +218,28 @@ void MainWindow::_do_update_data(UpdateData* dat) { UpdateDataHelper data_helper(dat); - if(dat->data_type() == TYPE_DATA) { + if(dat->data_type() == TYPE_POINTER) { + TS_RECORD_RDP_POINTER pt; + memcpy(&pt, &m_pt, sizeof(TS_RECORD_RDP_POINTER)); - if(dat->data_len() <= sizeof(TS_RECORD_PKG)) { - qDebug() << "invalid record package(1)."; + // 更新虚拟鼠标信息,这样下一次绘制界面时就会在新的位置绘制出虚拟鼠标 + memcpy(&m_pt, dat->get_pointer(), sizeof(TS_RECORD_RDP_POINTER)); + update(m_pt.x - m_pt_normal.width()/2, m_pt.y - m_pt_normal.width()/2, m_pt_normal.width(), m_pt_normal.height()); + + update(pt.x - m_pt_normal.width()/2, pt.y - m_pt_normal.width()/2, m_pt_normal.width(), m_pt_normal.height()); + + return; + } + else if(dat->data_type() == TYPE_IMAGE) { + QImage* img_update = nullptr; + int x, y, w, h; + if(!dat->get_image(&img_update, x, y, w, h)) return; - } - TS_RECORD_PKG* pkg = (TS_RECORD_PKG*)dat->data_buf(); + QPainter pp(&m_canvas); + pp.drawImage(x, y, *img_update, 0, 0, w, h, Qt::AutoColor); - if(pkg->type == TS_RECORD_TYPE_RDP_POINTER) { - if(dat->data_len() != sizeof(TS_RECORD_PKG) + sizeof(TS_RECORD_RDP_POINTER)) { - qDebug() << "invalid record package(2)."; - return; - } - - TS_RECORD_RDP_POINTER pt; - memcpy(&pt, &m_pt, sizeof(TS_RECORD_RDP_POINTER)); - - // 更新虚拟鼠标信息,这样下一次绘制界面时就会在新的位置绘制出虚拟鼠标 - memcpy(&m_pt, dat->data_buf() + sizeof(TS_RECORD_PKG), sizeof(TS_RECORD_RDP_POINTER)); - update(m_pt.x - m_pt_normal.width()/2, m_pt.y - m_pt_normal.width()/2, m_pt_normal.width(), m_pt_normal.height()); - - update(pt.x - m_pt_normal.width()/2, pt.y - m_pt_normal.width()/2, m_pt_normal.width(), m_pt_normal.height()); - } - else if(pkg->type == TS_RECORD_TYPE_RDP_IMAGE) { - if(dat->data_len() <= sizeof(TS_RECORD_PKG) + sizeof(TS_RECORD_RDP_IMAGE_INFO)) { - qDebug() << "invalid record package(3)."; - return; - } - - TS_RECORD_RDP_IMAGE_INFO* info = (TS_RECORD_RDP_IMAGE_INFO*)(dat->data_buf() + sizeof(TS_RECORD_PKG)); - uint8_t* img_dat = dat->data_buf() + sizeof(TS_RECORD_PKG) + sizeof(TS_RECORD_RDP_IMAGE_INFO); - uint32_t img_len = dat->data_len() - sizeof(TS_RECORD_PKG) - sizeof(TS_RECORD_RDP_IMAGE_INFO); - - QImage img_update; - rdpimg2QImage(img_update, info->width, info->height, info->bitsPerPixel, (info->format == TS_RDP_IMG_BMP) ? true : false, img_dat, img_len); - - int x = info->destLeft; - int y = info->destTop; - int w = info->destRight - info->destLeft + 1; - int h = info->destBottom - info->destTop + 1; - - QPainter pp(&m_canvas); - pp.drawImage(x, y, img_update, 0, 0, w, h, Qt::AutoColor); - - update(x, y, w, h); - } + update(x, y, w, h); return; } @@ -394,11 +304,10 @@ void MainWindow::_do_update_data(UpdateData* dat) { // 这是播放开始时收到的第一个数据包 else if(dat->data_type() == TYPE_HEADER_INFO) { - if(dat->data_len() != sizeof(TS_RECORD_HEADER)) { - qDebug() << "invalid record header."; + TS_RECORD_HEADER* hdr = dat->get_header(); + if(hdr == nullptr) return; - } - memcpy(&m_rec_hdr, dat->data_buf(), sizeof(TS_RECORD_HEADER)); + memcpy(&m_rec_hdr, hdr, sizeof(TS_RECORD_HEADER)); qDebug() << "resize (" << m_rec_hdr.basic.width << "," << m_rec_hdr.basic.height << ")"; @@ -490,18 +399,6 @@ void MainWindow::_do_bar_fade() { update(m_bar.rc()); } -//void MainWindow::_do_download(DownloadParam* param) { -// qDebug("MainWindow::_do_download(). %s %s %s", param->url.toStdString().c_str(), param->sid.toStdString().c_str(), param->fname.toStdString().c_str()); - -// if(m_dl) { -// delete m_dl; -// m_dl = nullptr; -// } - -// m_dl = new Downloader(); -// m_dl->run(&m_nam, param->url, param->sid, param->fname); -//} - void MainWindow::mouseMoveEvent(QMouseEvent *e) { if(!m_show_default) { QRect rc = m_bar.rc(); diff --git a/client/tp-player/mainwindow.h b/client/tp-player/mainwindow.h index 9c5eed6..240ad41 100644 --- a/client/tp-player/mainwindow.h +++ b/client/tp-player/mainwindow.h @@ -34,10 +34,8 @@ public: void pause(); void resume(); void restart(); - void speed(int s); - -// Downloader* downloader() {return m_dl;} -// void reset_downloader() {if(m_dl){delete m_dl;m_dl= nullptr;}} + void set_speed(int s); + void set_skip(bool s); // TODO: 将thr_data移动到thr_play线程,由play线程进行管理 ThrData* get_thr_data() {return m_thr_data;} @@ -55,13 +53,9 @@ private slots: void _do_bar_fade(); void _do_bar_delay_hide(); -// void _do_download(Downloader*); -// void _do_download(DownloadParam*); - private: Ui::MainWindow *ui; - //bool m_shown; bool m_show_default; bool m_bar_shown; QPixmap m_default_bg; @@ -91,14 +85,6 @@ private: bool m_show_message; QPixmap m_img_message; QRect m_rc_message; - - -// QNetworkAccessManager m_nam; -// Downloader* m_dl; - - // for test - TimeUseTest m_time_imgconvert_normal; - TimeUseTest m_time_imgconvert_compressed; }; #endif // MAINWINDOW_H diff --git a/client/tp-player/rle.c b/client/tp-player/rle.c index ce9fd03..393e333 100644 --- a/client/tp-player/rle.c +++ b/client/tp-player/rle.c @@ -74,7 +74,7 @@ /* 1 byte bitmap decompress */ RD_BOOL -bitmap_decompress1(uint8 * output, int width, int height, uint8 * input, int size) +bitmap_decompress1(uint8 * output, int width, int height, const uint8 * input, int size) { uint8 *end = input + size; uint8 *prevline = NULL, *line = NULL; @@ -272,7 +272,7 @@ bitmap_decompress1(uint8 * output, int width, int height, uint8 * input, int siz /* 2 byte bitmap decompress */ RD_BOOL -bitmap_decompress2(uint8 * output, int width, int height, uint8 * input, int size) +bitmap_decompress2(uint8 * output, int width, int height, const uint8 * input, int size) { uint8 *end = input + size; uint16 *prevline = NULL, *line = NULL; @@ -471,7 +471,7 @@ bitmap_decompress2(uint8 * output, int width, int height, uint8 * input, int siz /* 3 byte bitmap decompress */ RD_BOOL -bitmap_decompress3(uint8 * output, int width, int height, uint8 * input, int size) +bitmap_decompress3(uint8 * output, int width, int height, const uint8 * input, int size) { uint8 *end = input + size; uint8 *prevline = NULL, *line = NULL; @@ -863,7 +863,7 @@ process_plane(uint8 * in, int width, int height, uint8 * out, int size) /* 4 byte bitmap decompress */ RD_BOOL -bitmap_decompress4(uint8 * output, int width, int height, uint8 * input, int size) +bitmap_decompress4(uint8 * output, int width, int height, const uint8 * input, int size) { int code; int bytes_pro; diff --git a/client/tp-player/rle.h b/client/tp-player/rle.h index 9ed737a..6de7624 100644 --- a/client/tp-player/rle.h +++ b/client/tp-player/rle.h @@ -1,4 +1,4 @@ -#ifndef RLE_H +#ifndef RLE_H #define RLE_H #define RD_BOOL int @@ -13,10 +13,10 @@ extern "C" { #endif -RD_BOOL bitmap_decompress1(uint8 * output, int width, int height, uint8 * input, int size); -RD_BOOL bitmap_decompress2(uint8 * output, int width, int height, uint8 * input, int size); -RD_BOOL bitmap_decompress3(uint8 * output, int width, int height, uint8 * input, int size); -RD_BOOL bitmap_decompress4(uint8 * output, int width, int height, uint8 * input, int size); +RD_BOOL bitmap_decompress1(uint8 * output, int width, int height, const uint8 * input, int size); +RD_BOOL bitmap_decompress2(uint8 * output, int width, int height, const uint8 * input, int size); +RD_BOOL bitmap_decompress3(uint8 * output, int width, int height, const uint8 * input, int size); +RD_BOOL bitmap_decompress4(uint8 * output, int width, int height, const uint8 * input, int size); int bitmap_decompress_15(uint8 * output, int output_width, int output_height, int input_width, int input_height, uint8* input, int size); int bitmap_decompress_16(uint8 * output, int output_width, int output_height, int input_width, int input_height, uint8* input, int size); diff --git a/client/tp-player/thr_data.cpp b/client/tp-player/thr_data.cpp index 846ac39..a3070c7 100644 --- a/client/tp-player/thr_data.cpp +++ b/client/tp-player/thr_data.cpp @@ -137,9 +137,9 @@ void ThrData::_run() { return; - UpdateData* dat = new UpdateData(TYPE_HEADER_INFO); - dat->alloc_data(sizeof(TS_RECORD_HEADER)); - memcpy(dat->data_buf(), &m_hdr, sizeof(TS_RECORD_HEADER)); + UpdateData* dat = new UpdateData(m_hdr); +// dat->alloc_data(sizeof(TS_RECORD_HEADER)); +// memcpy(dat->data_buf(), &m_hdr, sizeof(TS_RECORD_HEADER)); emit signal_update_data(dat); @@ -290,18 +290,34 @@ void ThrData::_run() { qDebug("################## too bad."); } - UpdateData* dat = new UpdateData(TYPE_DATA); - dat->alloc_data(sizeof(TS_RECORD_PKG) + pkg.size); - memcpy(dat->data_buf(), &pkg, sizeof(TS_RECORD_PKG)); - read_len = fdata->read(reinterpret_cast(dat->data_buf()+sizeof(TS_RECORD_PKG)), pkg.size); - if(read_len != pkg.size) { - delete dat; + QByteArray pkg_data = fdata->read(pkg.size); + if(pkg_data.size() != pkg.size) { qDebug("invaid tp-rdp-%d.tpd file, read_len=%" PRId64 " (3).", file_idx+1, read_len); _notify_error(QString("%1\ntp-rdp-%2.tpd").arg(LOCAL8BIT("错误的录像数据文件!"), str_fidx)); return; } file_processed += pkg.size; + UpdateData* dat = new UpdateData(); + if(!dat->parse(pkg, pkg_data)) { + qDebug("invaid tp-rdp-%d.tpd file (4).", file_idx+1); + _notify_error(QString("%1\ntp-rdp-%2.tpd").arg(LOCAL8BIT("错误的录像数据文件!"), str_fidx)); + return; + } + + +// UpdateData* dat = new UpdateData(TYPE_DATA); +// dat->alloc_data(sizeof(TS_RECORD_PKG) + pkg.size); +// memcpy(dat->data_buf(), &pkg, sizeof(TS_RECORD_PKG)); +// read_len = fdata->read(reinterpret_cast(dat->data_buf()+sizeof(TS_RECORD_PKG)), pkg.size); +// if(read_len != pkg.size) { +// delete dat; +// qDebug("invaid tp-rdp-%d.tpd file, read_len=%" PRId64 " (3).", file_idx+1, read_len); +// _notify_error(QString("%1\ntp-rdp-%2.tpd").arg(LOCAL8BIT("错误的录像数据文件!"), str_fidx)); +// return; +// } +// file_processed += pkg.size; + // 跳过关键帧 // TODO: 拖动滚动条后,需要显示一次关键帧数据,然后跳过后续关键帧。 if(pkg.type == TS_RECORD_TYPE_RDP_KEYFRAME) { @@ -315,10 +331,12 @@ void ThrData::_run() { if(dat) { m_locker.lock(); m_data.enqueue(dat); - qDebug("queue data count: %d", m_data.size()); +// qDebug("queue data count: %d", m_data.size()); m_locker.unlock(); } + msleep(1); + // 如果此文件已经处理完毕,则关闭文件,这样下次处理一个新的文件 // qDebug("C processed: %" PRId64 ", size: %" PRId64, file_processed, file_size); if(file_processed >= file_size) { @@ -332,7 +350,7 @@ void ThrData::_run() { UpdateData* dat = new UpdateData(TYPE_END); m_locker.lock(); m_data.enqueue(dat); - qDebug("queue data count: %d", m_data.size()); +// qDebug("queue data count: %d", m_data.size()); m_locker.unlock(); } } @@ -433,8 +451,10 @@ UpdateData* ThrData::get_data() { UpdateData* d = nullptr; m_locker.lock(); - if(m_data.size() > 0) + if(m_data.size() > 0) { +// qDebug("get_data(), left: %d", m_data.size()); d = m_data.dequeue(); + } m_locker.unlock(); return d; diff --git a/client/tp-player/thr_play.cpp b/client/tp-player/thr_play.cpp index d366efb..cdab18e 100644 --- a/client/tp-player/thr_play.cpp +++ b/client/tp-player/thr_play.cpp @@ -23,7 +23,8 @@ ThrPlay::ThrPlay(MainWindow* mainwnd) { m_mainwnd = mainwnd; m_need_stop = false; m_need_pause = false; - m_speed = 2; + m_speed = 1; + m_skip = false; // m_res = res; // m_thr_data = nullptr; } @@ -67,13 +68,16 @@ void ThrPlay::run() { ThrData* thr_data = m_mainwnd->get_thr_data(); bool first_run = true; + uint32_t last_time_ms = 0; + uint32_t last_pass_ms = 0; + UpdateData* dat = nullptr; for(;;) { if(m_need_stop) break; // 1. 从ThrData的待播放队列中取出一个数据 - UpdateData* dat = thr_data->get_data(); + dat = thr_data->get_data(); if(dat == nullptr) { msleep(20); continue; @@ -85,161 +89,73 @@ void ThrPlay::run() { } // 2. 根据数据包的信息,等待到播放时间点 + uint32_t need_wait_ms = 0; + uint32_t this_time_ms = dat->get_time(); + uint32_t this_pass_ms = last_time_ms; + if(this_time_ms > 0) { + need_wait_ms = this_time_ms - last_time_ms; + + if(need_wait_ms > 0) { + uint32_t time_wait = 0; + + // 如果设置了跳过无操作区间,将超过1秒的等待时间压缩至1秒。 + if(m_skip) { + if(need_wait_ms > 1000) + need_wait_ms = 1000; + } + + for(;;) { + time_wait = need_wait_ms > 10 ? 10 : need_wait_ms; + msleep(time_wait); + + if(m_need_pause) { + while(m_need_pause) { + msleep(50); + if(m_need_stop) + break; + } + } + + if(m_need_stop) + break; + + time_wait *= m_speed; + + // 如果已经在等待长时间无操作区间内,用户设置了跳过无操作区间,则将超过0.5秒的等待时间压缩至0.5秒。 + if(m_skip) { + if(need_wait_ms > 500) + need_wait_ms = 500; + } + + this_pass_ms += time_wait; + if(this_pass_ms - last_pass_ms > 100) { + UpdateData* _passed_ms = new UpdateData(TYPE_PLAYED_MS); + _passed_ms->played_ms(this_pass_ms); + emit signal_update_data(_passed_ms); + last_pass_ms = this_pass_ms; + } + + if(need_wait_ms <= time_wait) + break; + else + need_wait_ms -= time_wait; + } + + if(m_need_stop) + break; + } + + last_time_ms = this_time_ms; + } + // 3. 将数据包发送给主UI界面进行显示 -// qDebug("emit one package."); if(dat->data_type() == TYPE_END) { _notify_message(LOCAL8BIT("播放结束")); } emit signal_update_data(dat); - msleep(5); -} - -#if 0 - //====================================== - // 加载录像文件数据并播放 - //====================================== - - uint32_t pkg_count = 0; - uint32_t time_pass = 0; - uint32_t time_last_pass = 0; - qint64 time_begin = QDateTime::currentMSecsSinceEpoch(); - QString msg; - - for(uint32_t fidx = 0; fidx < file_count; ++fidx) { - if(m_need_stop) { - qDebug() << "stop, user cancel 1."; - break; - } - - QString tpd_filename; - tpd_filename.sprintf("%stp-rdp-%d.tpd", path_base.toStdString().c_str(), fidx+1); - -// // for test. -// msg = QString::fromLocal8Bit("无法打开录像数据文件!\n\n"); -// //msg.sprintf("无法打开录像数据文件!\n\n%s", tpd_filename.toStdString().c_str()); -// msg += tpd_filename.toStdString().c_str(); -// _notify_message(msg); - - QFile f_dat(tpd_filename); - if(!f_dat.open(QFile::ReadOnly)) { - qDebug() << "Can not open " << tpd_filename << " for read."; - // msg.sprintf("无法打开录像数据文件!\n\n%s", tpd_filename.toStdString().c_str()); - msg = QString::fromLocal8Bit("无法打开录像数据文件!\n\n"); - msg += tpd_filename.toStdString().c_str(); - _notify_error(msg); - return; - } - - for(;;) { - if(m_need_stop) { - qDebug() << "stop, user cancel 2."; - break; - } - - if(m_need_pause) { - msleep(50); - time_begin += 50; - continue; - } - - TS_RECORD_PKG pkg; - read_len = f_dat.read((char*)(&pkg), sizeof(pkg)); - if(read_len == 0) - break; - if(read_len != sizeof(TS_RECORD_PKG)) { - qDebug() << "invaid .tpd file (1)."; - // msg.sprintf("错误的录像数据文件!\n\n%s", tpd_filename.toStdString().c_str()); - msg = QString::fromLocal8Bit("错误的录像数据文件!\n\n"); - msg += tpd_filename.toStdString().c_str(); - _notify_error(msg); - return; - } - if(pkg.type == TS_RECORD_TYPE_RDP_KEYFRAME) { - qDebug("----key frame: %d", pkg.time_ms); - } - - UpdateData* dat = new UpdateData(TYPE_DATA); - dat->alloc_data(sizeof(TS_RECORD_PKG) + pkg.size); - memcpy(dat->data_buf(), &pkg, sizeof(TS_RECORD_PKG)); - read_len = f_dat.read((char*)(dat->data_buf()+sizeof(TS_RECORD_PKG)), pkg.size); - if(read_len != pkg.size) { - delete dat; - qDebug() << "invaid .tpd file."; - // msg.sprintf("错误的录像数据文件!\n\n%s", tpd_filename.toStdString().c_str()); - msg = QString::fromLocal8Bit("错误的录像数据文件!\n\n"); - msg += tpd_filename.toStdString().c_str(); - _notify_error(msg); - return; - } - - pkg_count++; - - time_pass = (uint32_t)(QDateTime::currentMSecsSinceEpoch() - time_begin) * m_speed; - if(time_pass > total_ms) - time_pass = total_ms; - if(time_pass - time_last_pass > 200) { - UpdateData* _passed_ms = new UpdateData(TYPE_PLAYED_MS); - _passed_ms->played_ms(time_pass); - emit signal_update_data(_passed_ms); - time_last_pass = time_pass; - } - - if(time_pass >= pkg.time_ms) { - emit signal_update_data(dat); - continue; - } - - // 需要等待 - uint32_t time_wait = pkg.time_ms - time_pass; - uint32_t wait_this_time = 0; - for(;;) { - if(m_need_pause) { - msleep(50); - time_begin += 50; - continue; - } - - wait_this_time = time_wait; - if(wait_this_time > 10) - wait_this_time = 10; - - if(m_need_stop) { - qDebug() << "stop, user cancel (2)."; - break; - } - - msleep(wait_this_time); - - uint32_t _time_pass = (uint32_t)(QDateTime::currentMSecsSinceEpoch() - time_begin) * m_speed; - if(_time_pass > total_ms) - _time_pass = total_ms; - if(_time_pass - time_last_pass > 200) { - UpdateData* _passed_ms = new UpdateData(TYPE_PLAYED_MS); - _passed_ms->played_ms(_time_pass); - emit signal_update_data(_passed_ms); - time_last_pass = _time_pass; - } - - time_wait -= wait_this_time; - if(time_wait == 0) { - emit signal_update_data(dat); - break; - } - } - - } } - if(pkg_count < total_pkg) { - qDebug() << "total-pkg:" << total_pkg << ", played:" << pkg_count; - // msg.sprintf("录像数据文件有误!\n\n部分录像数据缺失!"); - msg = QString::fromLocal8Bit("录像数据文件有误!\n\n部分录像数据缺失!"); - _notify_message(msg); - } - -#endif -// qDebug("play end."); -// UpdateData* _end = new UpdateData(TYPE_END); -// emit signal_update_data(_end); + if(dat != nullptr) + delete dat; } diff --git a/client/tp-player/thr_play.h b/client/tp-player/thr_play.h index abb142d..35a1e40 100644 --- a/client/tp-player/thr_play.h +++ b/client/tp-player/thr_play.h @@ -21,6 +21,7 @@ public: void pause() {m_need_pause = true;} void resume() {m_need_pause = false;} void speed(int s) {if(s >= 1 && s <= 16) m_speed = s;} + void skip(bool s) {m_skip = s;} private: void _notify_message(const QString& msg); @@ -34,6 +35,7 @@ private: bool m_need_stop; bool m_need_pause; int m_speed; + bool m_skip; }; #endif // THR_PLAY_H diff --git a/client/tp-player/update_data.cpp b/client/tp-player/update_data.cpp index 31f2625..7faca7a 100644 --- a/client/tp-player/update_data.cpp +++ b/client/tp-player/update_data.cpp @@ -1,17 +1,157 @@ #include "update_data.h" +#include "rle.h" -UpdateData::UpdateData(int data_type, QObject *parent) : QObject(parent) +#include +#include + + +static QImage* _rdpimg2QImage(int w, int h, int bitsPerPixel, bool isCompressed, const uint8_t* dat, uint32_t len) { + QImage* out; + switch(bitsPerPixel) { + case 15: + if(isCompressed) { + uint8_t* _dat = reinterpret_cast(calloc(1, w*h*2)); + if(!bitmap_decompress1(_dat, w, h, dat, len)) { + free(_dat); + return nullptr; + } + out = new QImage(_dat, w, h, QImage::Format_RGB555); + free(_dat); + } + else { + out = new QImage(QImage(dat, w, h, QImage::Format_RGB555).transformed(QMatrix(1.0, 0.0, 0.0, -1.0, 0.0, 0.0))); + } + return out; + + case 16: + if(isCompressed) { + uint8_t* _dat = reinterpret_cast(calloc(1, w*h*2)); + if(!bitmap_decompress2(_dat, w, h, dat, len)) { + free(_dat); + return nullptr; + } + + // TODO: 这里需要进一步优化,直接操作QImage的buffer。 + out = new QImage(w, h, QImage::Format_RGB16); + for(int y = 0; y < h; y++) { + for(int x = 0; x < w; x++) { + uint16 a = ((uint16*)_dat)[y * w + x]; + uint8 r = ((a & 0xf800) >> 11) * 255 / 31; + uint8 g = ((a & 0x07e0) >> 5) * 255 / 63; + uint8 b = (a & 0x001f) * 255 / 31; + out->setPixelColor(x, y, QColor(r,g,b)); + } + } + free(_dat); + return out; + } + else { + out = new QImage(QImage(dat, w, h, QImage::Format_RGB16).transformed(QMatrix(1.0, 0.0, 0.0, -1.0, 0.0, 0.0))); + } + return out; + + case 24: + case 32: + default: + qDebug() << "--------NOT support UNKNOWN bitsPerPix" << bitsPerPixel; + return nullptr; + } +} + + +UpdateData::UpdateData() : QObject(nullptr) { + _init(); +} + +UpdateData::UpdateData(int data_type) : QObject(nullptr) +{ + _init(); m_data_type = data_type; +} + +UpdateData::UpdateData(const TS_RECORD_HEADER& hdr) : QObject(nullptr) +{ + _init(); + m_data_type = TYPE_HEADER_INFO; + m_hdr = new TS_RECORD_HEADER; + memcpy(m_hdr, &hdr, sizeof(TS_RECORD_HEADER)); +} + +void UpdateData::_init() { + m_data_type = TYPE_UNKNOWN; + m_hdr = nullptr; + m_pointer = nullptr; + m_img = nullptr; +// m_img_info = nullptr; + m_data_buf = nullptr; m_data_len = 0; + m_time_ms = 0; } UpdateData::~UpdateData() { + if(m_hdr) + delete m_hdr; + if(m_pointer) + delete m_pointer; + if(m_img) + delete m_img; +// if(m_img_info) +// delete m_img_info; + if(m_data_buf) delete m_data_buf; } +bool UpdateData::parse(const TS_RECORD_PKG& pkg, const QByteArray& data) { + m_time_ms = pkg.time_ms; + + if(pkg.type == TS_RECORD_TYPE_RDP_POINTER) { + m_data_type = TYPE_POINTER; + if(data.size() != sizeof(TS_RECORD_RDP_POINTER)) + return false; + m_pointer = new TS_RECORD_RDP_POINTER; + memcpy(m_pointer, data.data(), sizeof(TS_RECORD_RDP_POINTER)); + return true; + } + else if(pkg.type == TS_RECORD_TYPE_RDP_IMAGE) { + m_data_type = TYPE_IMAGE; + if(data.size() <= sizeof(TS_RECORD_RDP_IMAGE_INFO)) + return false; + const TS_RECORD_RDP_IMAGE_INFO* info = reinterpret_cast(data.data()); + const uint8_t* img_dat = reinterpret_cast(data.data() + sizeof(TS_RECORD_RDP_IMAGE_INFO)); + uint32_t img_len = data.size() - sizeof(TS_RECORD_RDP_IMAGE_INFO); + + QImage* img = _rdpimg2QImage(info->width, info->height, info->bitsPerPixel, (info->format == TS_RDP_IMG_BMP) ? true : false, img_dat, img_len); + if(img == nullptr) + return false; + + m_img = img; + m_img_x = info->destLeft; + m_img_y = info->destTop; + m_img_w = info->destRight - info->destLeft + 1; + m_img_h = info->destBottom - info->destTop + 1; + + + +// m_img_info = new TS_RECORD_RDP_IMAGE_INFO; +// memcpy(m_img_info, data.data(), sizeof(TS_RECORD_RDP_IMAGE_INFO)); +// m_data_buf = new uint8_t[img_len]; +// memcpy(m_data_buf, img_dat, img_len); +// m_data_len = img_len; + + + return true; + } + else if(pkg.type == TS_RECORD_TYPE_RDP_KEYFRAME) { + return true; + } + + return false; +} + + void UpdateData::alloc_data(uint32_t len) { if(m_data_buf) delete m_data_buf; diff --git a/client/tp-player/update_data.h b/client/tp-player/update_data.h index c76c299..c2dea27 100644 --- a/client/tp-player/update_data.h +++ b/client/tp-player/update_data.h @@ -1,23 +1,45 @@ -#ifndef UPDATE_DATA_H +#ifndef UPDATE_DATA_H #define UPDATE_DATA_H #include +#include "record_format.h" -#define TYPE_HEADER_INFO 0 -#define TYPE_DATA 1 -#define TYPE_PLAYED_MS 2 -#define TYPE_DOWNLOAD_PERCENT 3 -#define TYPE_END 4 -#define TYPE_MESSAGE 5 -#define TYPE_ERROR 6 +#define TYPE_UNKNOWN 0 +#define TYPE_HEADER_INFO 1 +#define TYPE_POINTER 10 +#define TYPE_IMAGE 11 +#define TYPE_KEYFRAME 12 +#define TYPE_PLAYED_MS 20 +#define TYPE_DOWNLOAD_PERCENT 21 +#define TYPE_END 50 +#define TYPE_MESSAGE 90 +#define TYPE_ERROR 91 class UpdateData : public QObject { Q_OBJECT public: - explicit UpdateData(int data_type, QObject *parent = nullptr); + explicit UpdateData(); + explicit UpdateData(int data_type); + explicit UpdateData(const TS_RECORD_HEADER& hdr); virtual ~UpdateData(); + bool parse(const TS_RECORD_PKG& pkg, const QByteArray& data); + TS_RECORD_HEADER* get_header() {return m_hdr;} + TS_RECORD_RDP_POINTER* get_pointer() {return m_pointer;} + bool get_image(QImage** img, int& x, int& y, int& w, int& h) { + if(m_img == nullptr) + return false; + *img = m_img; + x = m_img_x; + y = m_img_y; + w = m_img_w; + h = m_img_h; + return true; + } + + uint32_t get_time() {return m_time_ms;} + void alloc_data(uint32_t len); void attach_data(const uint8_t* dat, uint32_t len); @@ -32,6 +54,9 @@ public: void message(const QString& msg) {m_msg = msg;} const QString message(){return m_msg;} +private: + void _init(void); + signals: public slots: @@ -39,10 +64,24 @@ public slots: private: int m_data_type; + uint32_t m_time_ms; uint8_t* m_data_buf; uint32_t m_data_len; uint32_t m_played_ms; QString m_msg; + + // for HEADER + TS_RECORD_HEADER* m_hdr; + // for POINTER + TS_RECORD_RDP_POINTER* m_pointer; + // for IMAGE + QImage* m_img; + int m_img_x; + int m_img_y; + int m_img_w; + int m_img_h; + +// TS_RECORD_RDP_IMAGE_INFO* m_img_info; }; class UpdateDataHelper { From bde794d253d4a18ca157ab3718a0e3c3e545e8ce Mon Sep 17 00:00:00 2001 From: Apex Liu Date: Wed, 6 Nov 2019 02:49:30 +0800 Subject: [PATCH 33/44] =?UTF-8?q?=E6=92=AD=E6=94=BE=E5=99=A8=E8=BF=9B?= =?UTF-8?q?=E5=BA=A6=E6=9D=A1=E4=BB=BB=E6=84=8F=E6=8B=96=E5=8A=A8=E5=8F=AF?= =?UTF-8?q?=E4=BB=A5=E5=B7=A5=E4=BD=9C=E4=BA=86=E3=80=82=E9=9C=80=E8=A6=81?= =?UTF-8?q?=E6=94=B9=E8=BF=9B=E5=BD=95=E5=83=8F=E7=9A=84=E5=85=B3=E9=94=AE?= =?UTF-8?q?=E5=B8=A7=E5=A4=84=E7=90=86=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/tp-player/bar.cpp | 66 ++++++++++-- client/tp-player/bar.h | 9 +- client/tp-player/mainwindow.cpp | 65 ++++++++---- client/tp-player/mainwindow.h | 5 +- client/tp-player/thr_data.cpp | 174 ++++++++++++++++++-------------- client/tp-player/thr_data.h | 7 ++ client/tp-player/thr_play.cpp | 42 +++++++- client/tp-player/thr_play.h | 4 +- 8 files changed, 254 insertions(+), 118 deletions(-) diff --git a/client/tp-player/bar.cpp b/client/tp-player/bar.cpp index f5a39d5..32b4a23 100644 --- a/client/tp-player/bar.cpp +++ b/client/tp-player/bar.cpp @@ -86,6 +86,10 @@ Bar::Bar() { m_speed_hover = speed_count; // speed_count=no-hover m_skip_selected = false; m_skip_hover = false; + m_progress_hover = false; + m_progress_pressed = false; + + m_resume_ms = 0; } Bar::~Bar() { @@ -138,7 +142,7 @@ void Bar::start(uint32_t total_ms, int width) { } void Bar::end() { - if(m_passed_ms != m_total_ms) + if(m_played_ms != m_total_ms) update_passed_time(m_total_ms); m_playing = false; @@ -342,13 +346,14 @@ void Bar::update_passed_time(uint32_t ms) { } int percent = 0; - if(ms > m_total_ms) { + if(ms >= m_total_ms) { percent = 100; - m_passed_ms = m_total_ms; + m_played_ms = m_total_ms; } else { - m_passed_ms = ms; - percent = (int)(((double)m_passed_ms / (double)m_total_ms) * 100); + m_played_ms = ms; + //percent = (int)(((double)m_played_ms / (double)m_total_ms) * 100); + percent = m_played_ms * 100 / m_total_ms; } if(percent != m_percent) { @@ -361,6 +366,27 @@ void Bar::onMouseMove(int x, int y) { // 映射鼠标坐标点到本浮动窗内部的相对位置 QPoint pt(x-m_rc.left(), y-m_rc.top()); + if(m_progress_pressed) { + // 重新设置进度条指示器位置 + int percent = 0; + + if(pt.x() < m_rc_progress.left()) { + percent = 0; + m_resume_ms = 0; + } + else if(pt.x() > m_rc_progress.right()) { + percent = 100; + m_resume_ms = m_total_ms; + } + else { + percent = (pt.x() + m_img_progress_pointer[widget_normal].width()/2 - m_rc_progress.left()) * 100 / m_rc_progress.width(); + m_resume_ms = m_total_ms * percent / 100; + } + update_passed_time(m_resume_ms); + return; + } + + bool play_hover = m_rc_btn_play.contains(pt); if(play_hover != m_play_hover) { m_play_hover = play_hover; @@ -393,11 +419,13 @@ void Bar::onMouseMove(int x, int y) { } if(skip_hover) return; - - // TODO: more hover detect. } -void Bar::onMousePress(int x, int y) { +void Bar::onMousePress(int x, int y, Qt::MouseButton button) { + // 我们只关心左键按下 + if(button != Qt::LeftButton) + return; + // 映射鼠标坐标点到本浮动窗内部的相对位置 QPoint pt(x-m_rc.left(), y-m_rc.top()); @@ -405,7 +433,7 @@ void Bar::onMousePress(int x, int y) { if(m_playing) m_owner->pause(); else - m_owner->resume(); + m_owner->resume(false, 0); m_playing = !m_playing; m_owner->update(m_rc.left()+m_rc_btn_play.left(), m_rc.top()+m_rc_btn_play.top(), m_rc_btn_play.width(), m_rc_btn_play.height()); @@ -431,9 +459,29 @@ void Bar::onMousePress(int x, int y) { if(m_rc_skip.contains(pt)) { m_skip_selected = !m_skip_selected; + m_owner->set_skip(m_skip_selected); m_owner->update(m_rc.left()+m_rc_skip.left(), m_rc.top()+m_rc_skip.top(), m_rc_skip.width(), m_rc_skip.height()); return; } + + // + if(m_rc_progress.contains(pt)) { + m_progress_pressed = true; + // TODO: 暂停播放,按比例计算出点击位置占整个录像时长的百分比,定位到此位置准备播放。 + // TODO: 如果点击的位置是进度条指示标志,则仅暂停播放 + m_owner->pause(); + } +} + +void Bar::onMouseRelease(int x, int y, Qt::MouseButton button) { + // 我们只关心左键释放 + if(button != Qt::LeftButton) + return; + if(m_progress_pressed) { + m_progress_pressed = false; + qDebug("resume at %dms.", m_resume_ms); + m_owner->resume(true, m_resume_ms); + } } int Bar::get_speed() { diff --git a/client/tp-player/bar.h b/client/tp-player/bar.h index 8d61a2f..7141cb3 100644 --- a/client/tp-player/bar.h +++ b/client/tp-player/bar.h @@ -84,7 +84,8 @@ public: QRect rc(){return m_rc;} void onMouseMove(int x, int y); - void onMousePress(int x, int y); + void onMousePress(int x, int y, Qt::MouseButton button); + void onMouseRelease(int x, int y, Qt::MouseButton button); private: void _init_imgages(); @@ -94,7 +95,7 @@ private: MainWindow* m_owner; uint32_t m_total_ms; // 录像的总时长 - uint32_t m_passed_ms; // 已经播放了的时长 + uint32_t m_played_ms; // 已经播放了的时长 int m_percent; // 已经播放了的百分比(0~100) int m_percent_last_draw; QString m_str_total_time; @@ -139,6 +140,10 @@ private: int m_speed_hover; // speed__max=no-hover bool m_skip_selected; bool m_skip_hover; + bool m_progress_hover; + bool m_progress_pressed; + + uint32_t m_resume_ms; // after drag progress-pointer, resume play from here. }; #endif // BAR_H diff --git a/client/tp-player/mainwindow.cpp b/client/tp-player/mainwindow.cpp index 0d09810..a914926 100644 --- a/client/tp-player/mainwindow.cpp +++ b/client/tp-player/mainwindow.cpp @@ -110,20 +110,7 @@ void MainWindow::_do_first_run() { // connect(m_thr_data, SIGNAL(signal_download(DownloadParam*)), this, SLOT(_do_download(DownloadParam*))); m_thr_data->start(); - _start_play_thread(); -} - -void MainWindow::_start_play_thread() { - if(m_thr_play) { - m_thr_play->stop(); - //m_thr_play->wait(); - - disconnect(m_thr_play, SIGNAL(signal_update_data(UpdateData*)), this, SLOT(_do_update_data(UpdateData*))); - - delete m_thr_play; - m_thr_play = nullptr; - } - + //_start_play_thread(); m_thr_play = new ThrPlay(this); connect(m_thr_play, SIGNAL(signal_update_data(UpdateData*)), this, SLOT(_do_update_data(UpdateData*))); @@ -131,6 +118,24 @@ void MainWindow::_start_play_thread() { m_thr_play->start(); } +//void MainWindow::_start_play_thread() { +// if(m_thr_play) { +// m_thr_play->stop(); +// //m_thr_play->wait(); + +// disconnect(m_thr_play, SIGNAL(signal_update_data(UpdateData*)), this, SLOT(_do_update_data(UpdateData*))); + +// delete m_thr_play; +// m_thr_play = nullptr; +// } + +// m_thr_play = new ThrPlay(this); +// connect(m_thr_play, SIGNAL(signal_update_data(UpdateData*)), this, SLOT(_do_update_data(UpdateData*))); + +// m_thr_play->speed(m_bar.get_speed()); +// m_thr_play->start(); +//} + void MainWindow::set_speed(int s) { if(m_thr_play) m_thr_play->speed(s); @@ -203,11 +208,17 @@ void MainWindow::pause() { m_play_state = PLAY_STATE_PAUSE; } -void MainWindow::resume() { - if(m_play_state == PLAY_STATE_PAUSE) - m_thr_play->resume(); - else if(m_play_state == PLAY_STATE_STOP) - _start_play_thread(); +void MainWindow::resume(bool relocate, uint32_t ms) { + if(m_play_state == PLAY_STATE_PAUSE) { + if(relocate) + m_thr_data->restart(ms); + m_thr_play->resume(relocate, ms); + } + else if(m_play_state == PLAY_STATE_STOP) { +// _start_play_thread(); + m_thr_data->restart(0); + m_thr_play->resume(true, 0); + } m_play_state = PLAY_STATE_RUNNING; } @@ -432,12 +443,22 @@ void MainWindow::mouseMoveEvent(QMouseEvent *e) { } void MainWindow::mousePressEvent(QMouseEvent *e) { -// QApplication::instance()->exit(0); -// return; if(!m_show_default) { QRect rc = m_bar.rc(); if(rc.contains(e->pos())) { - m_bar.onMousePress(e->x(), e->y()); + m_bar.onMousePress(e->x(), e->y(), e->button()); } } } + +void MainWindow::mouseReleaseEvent(QMouseEvent *e) { + qDebug("mouse release."); +// if(!m_show_default) { +// QRect rc = m_bar.rc(); +// if(rc.contains(e->pos())) { +// m_bar.onMouseRelease(e->x(), e->y(), e->button()); +// } +// } + m_bar.onMouseRelease(e->x(), e->y(), e->button()); +} + diff --git a/client/tp-player/mainwindow.h b/client/tp-player/mainwindow.h index 240ad41..511c864 100644 --- a/client/tp-player/mainwindow.h +++ b/client/tp-player/mainwindow.h @@ -32,7 +32,7 @@ public: void set_resource(const QString& res); void pause(); - void resume(); + void resume(bool relocate, uint32_t ms); void restart(); void set_speed(int s); void set_skip(bool s); @@ -44,8 +44,9 @@ private: void paintEvent(QPaintEvent *e); void mouseMoveEvent(QMouseEvent *e); void mousePressEvent(QMouseEvent *e); + void mouseReleaseEvent(QMouseEvent *e); - void _start_play_thread(); +// void _start_play_thread(); private slots: void _do_first_run(); // 默认界面加载完成后,开始播放操作(可能会进行数据下载) diff --git a/client/tp-player/thr_data.cpp b/client/tp-player/thr_data.cpp index a3070c7..b029439 100644 --- a/client/tp-player/thr_data.cpp +++ b/client/tp-player/thr_data.cpp @@ -23,7 +23,12 @@ ThrData::ThrData(MainWindow* mainwin, const QString& res) { m_res = res; m_need_download = false; m_need_stop = false; -// m_dl = nullptr; + m_need_restart = false; + m_wait_restart = false; + m_need_show_kf = false; + + m_file_idx = 0; + m_offset = 0; #ifdef __APPLE__ m_data_path_base = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); @@ -138,54 +143,12 @@ void ThrData::_run() { UpdateData* dat = new UpdateData(m_hdr); -// dat->alloc_data(sizeof(TS_RECORD_HEADER)); -// memcpy(dat->data_buf(), &m_hdr, sizeof(TS_RECORD_HEADER)); emit signal_update_data(dat); - /* - // fake-code: - file_index = 0; - - for(;;) { - if(file_index >= file_count) { - msleep(500); - continue; - } - - - if(queue.size < 500) { - need_pkg = 1000 - queue.size; - - for(i = 0; i < need_pkg; i++) { - if(f.not_open) { - f.open(file_index); - } - - pkg = read_pkg() - if(f.to_end) { - f.close(); - file_index += 1; - if(file_index >= file_count) - break; - } - - queue.add(pkg) - } - - if(file_index >= file_count) - break; - } - else { - msleep(100); - } - } - - */ - QFile* fdata = nullptr; - uint32_t file_idx = 0; - uint32_t start_offset = 0; + //uint32_t file_idx = 0; + //uint32_t start_offset = 0; qint64 file_size = 0; qint64 file_processed = 0; qint64 read_len = 0; @@ -196,8 +159,20 @@ void ThrData::_run() { if(m_need_stop) return; + if(m_need_restart) { + if(fdata) { + fdata->close(); + delete fdata; + fdata = nullptr; + } + + m_wait_restart = true; + msleep(50); + continue; + } + // 如果所有文件都已经处理完了,则等待(可能用户会拖动滚动条,或者重新播放) - if(file_idx >= m_hdr.info.dat_file_count) { + if(m_file_idx >= m_hdr.info.dat_file_count) { msleep(500); continue; } @@ -222,10 +197,12 @@ void ThrData::_run() { for(int i = 0; i < pkg_need_add; ++i) { if(m_need_stop) return; + if(m_need_restart) + break; // 如果数据文件尚未打开,则打开它 if(fdata == nullptr) { - str_fidx.sprintf("%d", file_idx+1); + str_fidx.sprintf("%d", m_file_idx+1); QString tpd_fname = QString("%1/tp-rdp-%2.tpd").arg(m_data_path, str_fidx); tpd_fname = QDir::toNativeSeparators(tpd_fname); @@ -236,13 +213,13 @@ void ThrData::_run() { for(;;) { if(m_need_stop) return; - if(!m_thr_download.is_running() || m_thr_download.is_tpd_downloaded(file_idx)) + if(!m_thr_download.is_running() || m_thr_download.is_tpd_downloaded(m_file_idx)) break; msleep(100); } // 下载失败了 - if(!m_thr_download.is_tpd_downloaded(file_idx)) + if(!m_thr_download.is_tpd_downloaded(m_file_idx)) return; } } @@ -256,15 +233,22 @@ void ThrData::_run() { file_size = fdata->size(); file_processed = 0; - qDebug("Open file, processed: %" PRId64 ", size: %" PRId64, file_processed, file_size); + qDebug("Open file tp-rdp-%d.tpd, processed: %" PRId64 ", size: %" PRId64, m_file_idx+1, file_processed, file_size); } // qDebug("B processed: %" PRId64 ", size: %" PRId64, file_processed, file_size); + // 如果指定了起始偏移,则跳过这部分数据 + if(m_offset > 0) { + fdata->seek(m_offset); + file_processed = m_offset; + m_offset = 0; + } + //---------------------------------- // 读取一个数据包 //---------------------------------- if(file_size - file_processed < sizeof(TS_RECORD_PKG)) { - qDebug("invaid tp-rdp-%d.tpd file, filesize=%" PRId64 ", processed=%" PRId64 ", need=%d.", file_idx+1, file_size, file_processed, sizeof(TS_RECORD_PKG)); + qDebug("invaid tp-rdp-%d.tpd file, filesize=%" PRId64 ", processed=%" PRId64 ", need=%d.", m_file_idx+1, file_size, file_processed, sizeof(TS_RECORD_PKG)); _notify_error(QString("%1\ntp-rdp-%2.tpd").arg(LOCAL8BIT("错误的录像数据文件!"), str_fidx)); return; } @@ -274,14 +258,14 @@ void ThrData::_run() { // if(read_len == 0) // break; if(read_len != sizeof(TS_RECORD_PKG)) { - qDebug("invaid tp-rdp-%d.tpd file, read_len=%" PRId64 " (1).", file_idx+1, read_len); + qDebug("invaid tp-rdp-%d.tpd file, read_len=%" PRId64 " (1).", m_file_idx+1, read_len); _notify_error(QString("%1\ntp-rdp-%2.tpd").arg(LOCAL8BIT("错误的录像数据文件!"), str_fidx)); return; } file_processed += sizeof(TS_RECORD_PKG); if(file_size - file_processed < pkg.size) { - qDebug("invaid tp-rdp-%d.tpd file (2).", file_idx+1); + qDebug("invaid tp-rdp-%d.tpd file (2).", m_file_idx+1); _notify_error(QString("%1\ntp-rdp-%2.tpd").arg(LOCAL8BIT("错误的录像数据文件!"), str_fidx)); return; } @@ -292,7 +276,7 @@ void ThrData::_run() { QByteArray pkg_data = fdata->read(pkg.size); if(pkg_data.size() != pkg.size) { - qDebug("invaid tp-rdp-%d.tpd file, read_len=%" PRId64 " (3).", file_idx+1, read_len); + qDebug("invaid tp-rdp-%d.tpd file, read_len=%" PRId64 " (3).", m_file_idx+1, read_len); _notify_error(QString("%1\ntp-rdp-%2.tpd").arg(LOCAL8BIT("错误的录像数据文件!"), str_fidx)); return; } @@ -300,30 +284,23 @@ void ThrData::_run() { UpdateData* dat = new UpdateData(); if(!dat->parse(pkg, pkg_data)) { - qDebug("invaid tp-rdp-%d.tpd file (4).", file_idx+1); + qDebug("invaid tp-rdp-%d.tpd file (4).", m_file_idx+1); _notify_error(QString("%1\ntp-rdp-%2.tpd").arg(LOCAL8BIT("错误的录像数据文件!"), str_fidx)); return; } - -// UpdateData* dat = new UpdateData(TYPE_DATA); -// dat->alloc_data(sizeof(TS_RECORD_PKG) + pkg.size); -// memcpy(dat->data_buf(), &pkg, sizeof(TS_RECORD_PKG)); -// read_len = fdata->read(reinterpret_cast(dat->data_buf()+sizeof(TS_RECORD_PKG)), pkg.size); -// if(read_len != pkg.size) { -// delete dat; -// qDebug("invaid tp-rdp-%d.tpd file, read_len=%" PRId64 " (3).", file_idx+1, read_len); -// _notify_error(QString("%1\ntp-rdp-%2.tpd").arg(LOCAL8BIT("错误的录像数据文件!"), str_fidx)); -// return; -// } -// file_processed += pkg.size; - - // 跳过关键帧 - // TODO: 拖动滚动条后,需要显示一次关键帧数据,然后跳过后续关键帧。 + // 拖动滚动条后,需要显示一次关键帧数据,然后跳过后续关键帧。 if(pkg.type == TS_RECORD_TYPE_RDP_KEYFRAME) { qDebug("----key frame: %ld, processed=%" PRId64 ", pkg.size=%d", pkg.time_ms, file_processed, pkg.size); - delete dat; - dat = nullptr; + if(m_need_show_kf) { + m_need_show_kf = false; + qDebug("++ show keyframe."); + } + else { + qDebug("-- skip keyframe."); + delete dat; + dat = nullptr; + } } @@ -331,32 +308,75 @@ void ThrData::_run() { if(dat) { m_locker.lock(); m_data.enqueue(dat); -// qDebug("queue data count: %d", m_data.size()); m_locker.unlock(); } + // 让线程调度器让播放线程有机会执行 msleep(1); // 如果此文件已经处理完毕,则关闭文件,这样下次处理一个新的文件 -// qDebug("C processed: %" PRId64 ", size: %" PRId64, file_processed, file_size); if(file_processed >= file_size) { fdata->close(); delete fdata; fdata = nullptr; - file_idx++; + m_file_idx++; } - if(file_idx >= m_hdr.info.dat_file_count) { + if(m_file_idx >= m_hdr.info.dat_file_count) { UpdateData* dat = new UpdateData(TYPE_END); m_locker.lock(); m_data.enqueue(dat); -// qDebug("queue data count: %d", m_data.size()); m_locker.unlock(); + break; } } } } +void ThrData::restart(uint32_t start_ms) { + // 让处理线程处理完当前循环,然后等待 + m_need_restart = true; + + // 确保处理线程已经处理完当前循环 + for(;;) { + msleep(50); + if(m_need_stop) + return; + if(m_wait_restart) + break; + } + + // 清空待播放队列 + _clear_data(); + + if(start_ms == 0) { + m_offset = 0; + m_file_idx = 0; + m_need_show_kf = false; + } + else { + // 找到最接近 start_ms 但小于它的关键帧 + size_t i = 0; + for(i = 0; i < m_kf.size(); ++i) { + if(m_kf[i].time_ms > start_ms) + break; + } + if(i > 0) + i--; + + // 指定要播放的数据的开始位置 + m_offset = m_kf[i].offset; + m_file_idx = m_kf[i].file_index; + m_need_show_kf = true; + } + + qDebug("RESTART: offset=%d, file_idx=%d", m_offset, m_file_idx); + + // 让处理线程继续 + m_wait_restart = false; + m_need_restart = false; +} + bool ThrData::_load_header() { QString msg; qDebug() << "PATH_BASE: " << m_data_path; diff --git a/client/tp-player/thr_data.h b/client/tp-player/thr_data.h index dab105c..ee3861f 100644 --- a/client/tp-player/thr_data.h +++ b/client/tp-player/thr_data.h @@ -54,6 +54,7 @@ public: virtual void run(); void stop(); + void restart(uint32_t start_ms); // 重新从指定时间开始播放 bool have_more_data(); @@ -94,6 +95,12 @@ private: TS_RECORD_HEADER m_hdr; KeyFrames m_kf; + + bool m_need_restart; + bool m_wait_restart; + bool m_need_show_kf; + uint32_t m_file_idx; + uint32_t m_offset; }; #endif // THR_DATA_H diff --git a/client/tp-player/thr_play.cpp b/client/tp-player/thr_play.cpp index cdab18e..254e617 100644 --- a/client/tp-player/thr_play.cpp +++ b/client/tp-player/thr_play.cpp @@ -25,8 +25,7 @@ ThrPlay::ThrPlay(MainWindow* mainwnd) { m_need_pause = false; m_speed = 1; m_skip = false; -// m_res = res; -// m_thr_data = nullptr; + m_start_ms = 0; } ThrPlay::~ThrPlay() { @@ -64,10 +63,18 @@ void ThrPlay::_notify_error(const QString& msg) { emit signal_update_data(_msg); } +void ThrPlay::resume(bool relocate, uint32_t start_ms) { + if(relocate) { + m_start_ms = start_ms; + m_first_run = true; + } + m_need_pause = false; +} + void ThrPlay::run() { ThrData* thr_data = m_mainwnd->get_thr_data(); - bool first_run = true; + m_first_run = true; uint32_t last_time_ms = 0; uint32_t last_pass_ms = 0; @@ -83,11 +90,20 @@ void ThrPlay::run() { continue; } - if(first_run) { - first_run = false; + if(m_first_run) { + m_first_run = false; _notify_message(""); } + if(m_start_ms > 0) { + if(dat->get_time() < m_start_ms) { + emit signal_update_data(dat); + continue; + } + last_time_ms = m_start_ms; + m_start_ms = 0; + } + // 2. 根据数据包的信息,等待到播放时间点 uint32_t need_wait_ms = 0; uint32_t this_time_ms = dat->get_time(); @@ -119,6 +135,14 @@ void ThrPlay::run() { if(m_need_stop) break; + if(m_start_ms > 0) { +// if(dat) { +// delete dat; +// dat = nullptr; +// } + break; + } + time_wait *= m_speed; // 如果已经在等待长时间无操作区间内,用户设置了跳过无操作区间,则将超过0.5秒的等待时间压缩至0.5秒。 @@ -143,6 +167,14 @@ void ThrPlay::run() { if(m_need_stop) break; + +// if(m_start_ms > 0) { +// if(dat) { +// delete dat; +// dat = nullptr; +// } +// break; +// } } last_time_ms = this_time_ms; diff --git a/client/tp-player/thr_play.h b/client/tp-player/thr_play.h index 35a1e40..e8ea3d8 100644 --- a/client/tp-player/thr_play.h +++ b/client/tp-player/thr_play.h @@ -19,7 +19,7 @@ public: virtual void run(); void stop(); void pause() {m_need_pause = true;} - void resume() {m_need_pause = false;} + void resume(bool relocate, uint32_t start_ms); void speed(int s) {if(s >= 1 && s <= 16) m_speed = s;} void skip(bool s) {m_skip = s;} @@ -36,6 +36,8 @@ private: bool m_need_pause; int m_speed; bool m_skip; + bool m_first_run; + uint32_t m_start_ms; }; #endif // THR_PLAY_H From 2d5c9eeae0f0f823d41ef2aadf52e7380fa5ae1e Mon Sep 17 00:00:00 2001 From: Apex Liu Date: Wed, 6 Nov 2019 03:15:23 +0800 Subject: [PATCH 34/44] .tmp. --- client/tp-player/bar.cpp | 4 ++++ client/tp-player/thr_play.cpp | 7 +++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/client/tp-player/bar.cpp b/client/tp-player/bar.cpp index 32b4a23..ff034ff 100644 --- a/client/tp-player/bar.cpp +++ b/client/tp-player/bar.cpp @@ -470,6 +470,8 @@ void Bar::onMousePress(int x, int y, Qt::MouseButton button) { // TODO: 暂停播放,按比例计算出点击位置占整个录像时长的百分比,定位到此位置准备播放。 // TODO: 如果点击的位置是进度条指示标志,则仅暂停播放 m_owner->pause(); + m_playing = false; + m_owner->update(m_rc.left()+m_rc_btn_play.left(), m_rc.top()+m_rc_btn_play.top(), m_rc_btn_play.width(), m_rc_btn_play.height()); } } @@ -481,6 +483,8 @@ void Bar::onMouseRelease(int x, int y, Qt::MouseButton button) { m_progress_pressed = false; qDebug("resume at %dms.", m_resume_ms); m_owner->resume(true, m_resume_ms); + m_playing = true; + m_owner->update(m_rc.left()+m_rc_btn_play.left(), m_rc.top()+m_rc_btn_play.top(), m_rc_btn_play.width(), m_rc_btn_play.height()); } } diff --git a/client/tp-player/thr_play.cpp b/client/tp-player/thr_play.cpp index 254e617..f1d4d22 100644 --- a/client/tp-player/thr_play.cpp +++ b/client/tp-player/thr_play.cpp @@ -109,7 +109,10 @@ void ThrPlay::run() { uint32_t this_time_ms = dat->get_time(); uint32_t this_pass_ms = last_time_ms; if(this_time_ms > 0) { - need_wait_ms = this_time_ms - last_time_ms; + if(this_time_ms >= last_time_ms) + need_wait_ms = this_time_ms - last_time_ms; + else + need_wait_ms = 0; if(need_wait_ms > 0) { uint32_t time_wait = 0; @@ -177,8 +180,8 @@ void ThrPlay::run() { // } } - last_time_ms = this_time_ms; } + last_time_ms = this_time_ms; // 3. 将数据包发送给主UI界面进行显示 if(dat->data_type() == TYPE_END) { From 23c24ceb69f71d6966398e69d5971ab487f25e65 Mon Sep 17 00:00:00 2001 From: Apex Liu Date: Sun, 10 Nov 2019 05:25:54 +0800 Subject: [PATCH 35/44] =?UTF-8?q?=E6=9E=84=E5=BB=BA=E5=85=B3=E9=94=AE?= =?UTF-8?q?=E5=B8=A7=E7=9A=84=E6=B5=8B=E8=AF=95=E4=BB=A3=E7=A0=81=E5=AE=8C?= =?UTF-8?q?=E6=88=90=EF=BC=8C=E5=87=86=E5=A4=87=E7=A7=BB=E6=A4=8D=E5=88=B0?= =?UTF-8?q?rdp=E6=A0=B8=E5=BF=83=E6=A8=A1=E5=9D=97=E4=B8=AD=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/tp-player/bar.cpp | 16 ++++ client/tp-player/mainwindow.cpp | 9 +- client/tp-player/record_format.h | 6 ++ client/tp-player/rle.c | 4 +- client/tp-player/thr_data.cpp | 160 ++++++++++++++++++++++++++++++- client/tp-player/thr_data.h | 3 + client/tp-player/update_data.cpp | 48 ++++++++-- client/tp-player/update_data.h | 4 + 8 files changed, 234 insertions(+), 16 deletions(-) diff --git a/client/tp-player/bar.cpp b/client/tp-player/bar.cpp index ff034ff..61ebbcc 100644 --- a/client/tp-player/bar.cpp +++ b/client/tp-player/bar.cpp @@ -472,6 +472,22 @@ void Bar::onMousePress(int x, int y, Qt::MouseButton button) { m_owner->pause(); m_playing = false; m_owner->update(m_rc.left()+m_rc_btn_play.left(), m_rc.top()+m_rc_btn_play.top(), m_rc_btn_play.width(), m_rc_btn_play.height()); + + int percent = 0; + + if(pt.x() < m_rc_progress.left()) { + percent = 0; + m_resume_ms = 0; + } + else if(pt.x() > m_rc_progress.right()) { + percent = 100; + m_resume_ms = m_total_ms; + } + else { + percent = (pt.x() + m_img_progress_pointer[widget_normal].width()/2 - m_rc_progress.left()) * 100 / m_rc_progress.width(); + m_resume_ms = m_total_ms * percent / 100; + } + update_passed_time(m_resume_ms); } } diff --git a/client/tp-player/mainwindow.cpp b/client/tp-player/mainwindow.cpp index a914926..28d93d5 100644 --- a/client/tp-player/mainwindow.cpp +++ b/client/tp-player/mainwindow.cpp @@ -216,8 +216,8 @@ void MainWindow::resume(bool relocate, uint32_t ms) { } else if(m_play_state == PLAY_STATE_STOP) { // _start_play_thread(); - m_thr_data->restart(0); - m_thr_play->resume(true, 0); + m_thr_data->restart(ms); + m_thr_play->resume(true, ms); } m_play_state = PLAY_STATE_RUNNING; @@ -247,6 +247,11 @@ void MainWindow::_do_update_data(UpdateData* dat) { if(!dat->get_image(&img_update, x, y, w, h)) return; + static int img_idx = 0; + img_idx++; + qDebug("draw img: %d (%d,%d)-(%d,%d)", img_idx, x, y, w, h); + + QPainter pp(&m_canvas); pp.drawImage(x, y, *img_update, 0, 0, w, h, Qt::AutoColor); diff --git a/client/tp-player/record_format.h b/client/tp-player/record_format.h index 2f95c7b..3fe863c 100644 --- a/client/tp-player/record_format.h +++ b/client/tp-player/record_format.h @@ -93,6 +93,12 @@ typedef struct TS_RECORD_RDP_IMAGE_INFO { uint8_t _reserved; }TS_RECORD_RDP_IMAGE_INFO; +// 关键帧索引 +typedef struct TS_RECORD_RDP_KEYFRAME_INFO { + uint32_t time_ms; // 此关键帧的时间点 + uint32_t file_index; // 此关键帧图像数据位于哪一个数据文件中 + uint32_t offset; // 此关键帧图像数据在数据文件中的偏移 +}TS_RECORD_RDP_KEYFRAME_INFO; #pragma pack(pop) diff --git a/client/tp-player/rle.c b/client/tp-player/rle.c index 393e333..7aa0b49 100644 --- a/client/tp-player/rle.c +++ b/client/tp-player/rle.c @@ -76,7 +76,7 @@ RD_BOOL bitmap_decompress1(uint8 * output, int width, int height, const uint8 * input, int size) { - uint8 *end = input + size; + const uint8 *end = input + size; uint8 *prevline = NULL, *line = NULL; int opcode, count, offset, isfillormix, x = width; int lastopcode = -1, insertmix = False, bicolour = False; @@ -274,7 +274,7 @@ bitmap_decompress1(uint8 * output, int width, int height, const uint8 * input, i RD_BOOL bitmap_decompress2(uint8 * output, int width, int height, const uint8 * input, int size) { - uint8 *end = input + size; + const uint8 *end = input + size; uint16 *prevline = NULL, *line = NULL; int opcode, count, offset, isfillormix, x = width; int lastopcode = -1, insertmix = False, bicolour = False; diff --git a/client/tp-player/thr_data.cpp b/client/tp-player/thr_data.cpp index b029439..6c56156 100644 --- a/client/tp-player/thr_data.cpp +++ b/client/tp-player/thr_data.cpp @@ -13,6 +13,78 @@ #include "record_format.h" #include "mainwindow.h" +#include "rle.h" + +// for test only +int g_kf_idx = 0; +QByteArray g_kfdata[10]; +QByteArray* g_kf = nullptr; + +int g_img_idx = 0; + +void _update_key_frame(QByteArray* kf, uint16_t screen_w, uint16_t screen_h, uint16_t destLeft, uint16_t destTop, uint16_t w, uint16_t h, uint16_t wr, uint16_t hr, uint16_t bitsPerPixel, bool isCompressed, const uint8_t* dat, size_t len) { + switch(bitsPerPixel) { +// case 15: +// if(isCompressed) { +// uint8_t* _dat = reinterpret_cast(calloc(1, w*h*2)); +// if(!bitmap_decompress1(_dat, w, h, dat, len)) { +// free(_dat); +// qDebug("bitmap_decompress1() failed."); +// return; +// } +// out = new QImage(_dat, w, h, QImage::Format_RGB555); +// free(_dat); +// } +// else { +// out = new QImage(QImage(dat, w, h, QImage::Format_RGB555).transformed(QMatrix(1.0, 0.0, 0.0, -1.0, 0.0, 0.0))); +// } +// return out; + + case 16: + { + g_img_idx++; + uint8_t* kfd = reinterpret_cast(kf->data()); + if(isCompressed) { + uint8_t* _dat = reinterpret_cast(calloc(1, w*h*2)); + if(!bitmap_decompress2(_dat, w, h, dat, static_cast(len))) { + free(_dat); + qDebug() << "------------------DECOMPRESS2 failed."; + return; + } + +// out = new QImage(w, h, QImage::Format_RGB16); + + qDebug("c: %ld, img: %d (%d,%d)-(%d,%d) (%d,%d)", ((destTop+hr-1)*screen_w)+destLeft+wr-1, g_img_idx, destLeft, destTop, w, h, wr, hr); + for(int y = 0; y < hr; y++) { +// if((destTop+y)*screen_w+destLeft > 6) +// memcpy(kfd+((destTop+y)*screen_w+destLeft - 6)*2, _dat+((y*w)*2), wr*2); +// else + memcpy(kfd+((destTop+y)*screen_w+destLeft)*2, _dat+((y*w)*2), wr*2); + } + + free(_dat); + return; + } + else { +// out = new QImage(QImage(dat, w, h, QImage::Format_RGB16).transformed(QMatrix(1.0, 0.0, 0.0, -1.0, 0.0, 0.0))); + + qDebug("nc: %ld, img: %d (%d,%d)-(%d,%d) (%d,%d)", ((destTop+hr-1)*screen_w)+destLeft+wr-1, g_img_idx, destLeft, destTop, w, h, wr, hr); + for(int y = 0; y < hr; y++) { + memcpy(kfd+((destTop+h-y)*screen_w+destLeft)*2, dat+(y*w*2), wr*2); + } + } + + return; + } + + case 24: + case 32: + default: + qDebug() << "------------------NOT support UNKNOWN bitsPerPix" << bitsPerPixel; + return; + } +} + //================================================================= // ThrData @@ -30,6 +102,8 @@ ThrData::ThrData(MainWindow* mainwin, const QString& res) { m_file_idx = 0; m_offset = 0; + m_xxx = false; + #ifdef __APPLE__ m_data_path_base = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); m_data_path_base += "/tp-testdata/"; @@ -107,8 +181,10 @@ void ThrData::_run() { msleep(100); } - if(!m_thr_download.is_tpk_downloaded()) + if(!m_thr_download.is_tpk_downloaded()) { + _notify_error(QString("%1\n%2").arg(LOCAL8BIT("无法下载录像文件!"), m_res)); return; + } m_thr_download.get_data_path(m_data_path); } @@ -154,6 +230,11 @@ void ThrData::_run() { qint64 read_len = 0; QString str_fidx; + g_kf_idx = 0; + g_kf = &(g_kfdata[g_kf_idx]); + g_kf->resize(m_hdr.basic.width*m_hdr.basic.height*2); + memset(g_kf->data(), 0, m_hdr.basic.width*m_hdr.basic.height*2); + for(;;) { // 任何时候确保第一时间响应退出操作 if(m_need_stop) @@ -282,21 +363,80 @@ void ThrData::_run() { } file_processed += pkg.size; - UpdateData* dat = new UpdateData(); + + + + + // for test only + if(!m_xxx && pkg.type == TS_RECORD_TYPE_RDP_IMAGE) { + const TS_RECORD_RDP_IMAGE_INFO* info = reinterpret_cast(pkg_data.data()); + uint8_t* img_dat = reinterpret_cast(pkg_data.data() + sizeof(TS_RECORD_RDP_IMAGE_INFO)); + size_t img_len = pkg_data.size() - sizeof(TS_RECORD_RDP_IMAGE_INFO); + + bool isCompress = (info->format == TS_RDP_IMG_BMP) ? true : false; +// _update_key_frame(&g_kf, m_hdr.basic.width, m_hdr.basic.height, info->destLeft, info->destTop, (info->destRight-info->destLeft+1), (info->destBottom-info->destTop+1), info->bitsPerPixel, isCompress, img_dat, img_len); + _update_key_frame(g_kf, m_hdr.basic.width, m_hdr.basic.height, + info->destLeft, info->destTop, + info->width, info->height, + info->destRight - info->destLeft + 1, info->destBottom - info->destTop + 1, + info->bitsPerPixel, isCompress, img_dat, img_len); + } + if(pkg.type == TS_RECORD_TYPE_RDP_KEYFRAME) { +// const TS_RECORD_RDP_KEYFRAME_INFO* info = reinterpret_cast(pkg_data.data()); + uint8_t* img_dat = reinterpret_cast(pkg_data.data() + sizeof(TS_RECORD_RDP_KEYFRAME_INFO)); + uint32_t img_len = pkg_data.size() - sizeof(TS_RECORD_RDP_KEYFRAME_INFO); + + if(m_xxx) { + qDebug("use kf: %d", m_restart_kf_idx); + memcpy(img_dat, g_kfdata[m_restart_kf_idx].data(), img_len); + } + else { + memcpy(img_dat, g_kf->data(), img_len); + } + } + + + + + + UpdateData* dat = new UpdateData(m_hdr.basic.width, m_hdr.basic.height); if(!dat->parse(pkg, pkg_data)) { qDebug("invaid tp-rdp-%d.tpd file (4).", m_file_idx+1); _notify_error(QString("%1\ntp-rdp-%2.tpd").arg(LOCAL8BIT("错误的录像数据文件!"), str_fidx)); return; } + + + + // 拖动滚动条后,需要显示一次关键帧数据,然后跳过后续关键帧。 if(pkg.type == TS_RECORD_TYPE_RDP_KEYFRAME) { + g_kf_idx++; + g_kf = &(g_kfdata[g_kf_idx]); + if(!m_xxx) { + g_kf->resize(m_hdr.basic.width*m_hdr.basic.height*2); + memcpy(g_kf->data(), g_kfdata[g_kf_idx-1].data(), m_hdr.basic.width*m_hdr.basic.height*2); + } + qDebug("----key frame: %ld, processed=%" PRId64 ", pkg.size=%d", pkg.time_ms, file_processed, pkg.size); if(m_need_show_kf) { m_need_show_kf = false; qDebug("++ show keyframe."); } else { + //m_restart_kf_idx + + + QString tmp; + tmp.sprintf("%d", g_kf_idx); + QString img_fname = QString("%1/img-%2.png").arg(m_data_path, tmp); + QImage* img = nullptr; + int x = 0, y = 0, w = 0, h = 0; + dat->get_image(&img, x, y, w, h); + if(img != nullptr) + img->save(img_fname, "png"); + qDebug("-- skip keyframe."); delete dat; dat = nullptr; @@ -304,6 +444,9 @@ void ThrData::_run() { } + + + // 数据放到待播放列表中 if(dat) { m_locker.lock(); @@ -334,6 +477,7 @@ void ThrData::_run() { } void ThrData::restart(uint32_t start_ms) { + qDebug("restart at %ld ms", start_ms); // 让处理线程处理完当前循环,然后等待 m_need_restart = true; @@ -353,16 +497,26 @@ void ThrData::restart(uint32_t start_ms) { m_offset = 0; m_file_idx = 0; m_need_show_kf = false; + + g_kf_idx = 0; + m_restart_kf_idx = 0; + m_xxx = true; } else { // 找到最接近 start_ms 但小于它的关键帧 size_t i = 0; for(i = 0; i < m_kf.size(); ++i) { - if(m_kf[i].time_ms > start_ms) + if(m_kf[i].time_ms > start_ms) { break; + } } if(i > 0) i--; + g_kf_idx = i; + m_restart_kf_idx = i; + m_xxx = true; + + qDebug("restart acturelly at %ld ms, kf: %d", m_kf[i].time_ms, i); // 指定要播放的数据的开始位置 m_offset = m_kf[i].offset; diff --git a/client/tp-player/thr_data.h b/client/tp-player/thr_data.h index ee3861f..18c07c6 100644 --- a/client/tp-player/thr_data.h +++ b/client/tp-player/thr_data.h @@ -101,6 +101,9 @@ private: bool m_need_show_kf; uint32_t m_file_idx; uint32_t m_offset; + + bool m_xxx; + int m_restart_kf_idx; }; #endif // THR_DATA_H diff --git a/client/tp-player/update_data.cpp b/client/tp-player/update_data.cpp index 7faca7a..ac25d01 100644 --- a/client/tp-player/update_data.cpp +++ b/client/tp-player/update_data.cpp @@ -28,6 +28,7 @@ static QImage* _rdpimg2QImage(int w, int h, int bitsPerPixel, bool isCompressed, uint8_t* _dat = reinterpret_cast(calloc(1, w*h*2)); if(!bitmap_decompress2(_dat, w, h, dat, len)) { free(_dat); + qDebug() << "22------------------DECOMPRESS2 failed."; return nullptr; } @@ -58,6 +59,22 @@ static QImage* _rdpimg2QImage(int w, int h, int bitsPerPixel, bool isCompressed, } } +static QImage* _raw2QImage(int w, int h, const uint8_t* dat, uint32_t len) { + QImage* out; + + // TODO: 这里需要进一步优化,直接操作QImage的buffer。 + out = new QImage(w, h, QImage::Format_RGB16); + for(int y = 0; y < h; y++) { + for(int x = 0; x < w; x++) { + uint16 a = ((uint16*)dat)[y * w + x]; + uint8 r = ((a & 0xf800) >> 11) * 255 / 31; + uint8 g = ((a & 0x07e0) >> 5) * 255 / 63; + uint8 b = (a & 0x001f) * 255 / 31; + out->setPixelColor(x, y, QColor(r,g,b)); + } + } + return out; +} UpdateData::UpdateData() : QObject(nullptr) { @@ -78,6 +95,12 @@ UpdateData::UpdateData(const TS_RECORD_HEADER& hdr) : QObject(nullptr) memcpy(m_hdr, &hdr, sizeof(TS_RECORD_HEADER)); } +UpdateData::UpdateData(uint16_t screen_w, uint16_t screen_h) { + _init(); + m_screen_w = screen_w; + m_screen_h = screen_h; +} + void UpdateData::_init() { m_data_type = TYPE_UNKNOWN; m_hdr = nullptr; @@ -88,6 +111,9 @@ void UpdateData::_init() { m_data_buf = nullptr; m_data_len = 0; m_time_ms = 0; + + m_screen_w = 0; + m_screen_h = 0; } UpdateData::~UpdateData() { @@ -133,18 +159,22 @@ bool UpdateData::parse(const TS_RECORD_PKG& pkg, const QByteArray& data) { m_img_w = info->destRight - info->destLeft + 1; m_img_h = info->destBottom - info->destTop + 1; - - -// m_img_info = new TS_RECORD_RDP_IMAGE_INFO; -// memcpy(m_img_info, data.data(), sizeof(TS_RECORD_RDP_IMAGE_INFO)); -// m_data_buf = new uint8_t[img_len]; -// memcpy(m_data_buf, img_dat, img_len); -// m_data_len = img_len; - - return true; } else if(pkg.type == TS_RECORD_TYPE_RDP_KEYFRAME) { + m_data_type = TYPE_IMAGE; +// const TS_RECORD_RDP_KEYFRAME_INFO* info = reinterpret_cast(data.data()); + const uint8_t* img_dat = reinterpret_cast(data.data() + sizeof(TS_RECORD_RDP_KEYFRAME_INFO)); + uint32_t img_len = data.size() - sizeof(TS_RECORD_RDP_KEYFRAME_INFO); + + QImage* img = _raw2QImage((int)m_screen_w, (int)m_screen_h, img_dat, img_len); + if(img == nullptr) + return false; + m_img = img; + m_img_x = 0; + m_img_y = 0; + m_img_w = m_screen_w; + m_img_h = m_screen_h; return true; } diff --git a/client/tp-player/update_data.h b/client/tp-player/update_data.h index c2dea27..e037588 100644 --- a/client/tp-player/update_data.h +++ b/client/tp-player/update_data.h @@ -22,6 +22,7 @@ public: explicit UpdateData(); explicit UpdateData(int data_type); explicit UpdateData(const TS_RECORD_HEADER& hdr); + explicit UpdateData(uint16_t screen_w, uint16_t screen_h); virtual ~UpdateData(); bool parse(const TS_RECORD_PKG& pkg, const QByteArray& data); @@ -82,6 +83,9 @@ private: int m_img_h; // TS_RECORD_RDP_IMAGE_INFO* m_img_info; + + uint16_t m_screen_w; + uint16_t m_screen_h; }; class UpdateDataHelper { From dd115c8af61c2ff391c0e40ab8daaba5b75c6352 Mon Sep 17 00:00:00 2001 From: Apex Liu Date: Sun, 10 Nov 2019 20:37:09 +0800 Subject: [PATCH 36/44] =?UTF-8?q?RDP=E6=92=AD=E6=94=BE=E5=99=A8=E5=AE=8C?= =?UTF-8?q?=E5=B7=A5=EF=BC=8C=E5=85=81=E8=AE=B8=E6=8B=96=E5=8A=A8=E8=BF=9B?= =?UTF-8?q?=E5=BA=A6=E6=9D=A1=E4=BA=86=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/tp-player/bar.cpp | 2 +- client/tp-player/main.cpp | 2 +- client/tp-player/mainwindow.cpp | 76 ++++++-------- client/tp-player/record_format.h | 7 +- client/tp-player/thr_data.cpp | 156 +--------------------------- client/tp-player/thr_data.h | 4 +- client/tp-player/thr_play.cpp | 27 ++--- client/tp-player/update_data.cpp | 75 +++++++++---- client/tp-player/update_data.h | 47 ++++++--- server/tp_core/common/base_record.h | 6 +- 10 files changed, 146 insertions(+), 256 deletions(-) diff --git a/client/tp-player/bar.cpp b/client/tp-player/bar.cpp index 61ebbcc..0c9bea5 100644 --- a/client/tp-player/bar.cpp +++ b/client/tp-player/bar.cpp @@ -372,7 +372,7 @@ void Bar::onMouseMove(int x, int y) { if(pt.x() < m_rc_progress.left()) { percent = 0; - m_resume_ms = 0; + m_resume_ms = 1; } else if(pt.x() > m_rc_progress.right()) { percent = 100; diff --git a/client/tp-player/main.cpp b/client/tp-player/main.cpp index 486d0c5..b4a133e 100644 --- a/client/tp-player/main.cpp +++ b/client/tp-player/main.cpp @@ -65,7 +65,7 @@ int main(int argc, char *argv[]) // qDebug("data-path-base: %s", data_path_base.toStdString().c_str()); // return 0; - QGuiApplication::setApplicationDisplayName("TP-Player"); + QGuiApplication::setApplicationDisplayName(LOCAL8BIT("[Teleport播放器]")); QCommandLineParser parser; const QCommandLineOption opt_help = parser.addHelpOption(); diff --git a/client/tp-player/mainwindow.cpp b/client/tp-player/mainwindow.cpp index 28d93d5..c9d9adf 100644 --- a/client/tp-player/mainwindow.cpp +++ b/client/tp-player/mainwindow.cpp @@ -75,8 +75,6 @@ MainWindow::~MainWindow() { if(m_thr_play) { m_thr_play->stop(); - //m_thr_play->wait(); - //qDebug() << "play thread stoped."; disconnect(m_thr_play, SIGNAL(signal_update_data(UpdateData*)), this, SLOT(_do_update_data(UpdateData*))); @@ -87,16 +85,10 @@ MainWindow::~MainWindow() if(m_thr_data) { m_thr_data->stop(); disconnect(m_thr_data, SIGNAL(signal_update_data(UpdateData*)), this, SLOT(_do_update_data(UpdateData*))); -// disconnect(m_thr_data, SIGNAL(signal_download(DownloadParam*)), this, SLOT(_do_download(DownloadParam*))); delete m_thr_data; m_thr_data = nullptr; } -// if(m_dl) { -// delete m_dl; -// m_dl = nullptr; -// } - delete ui; } @@ -107,10 +99,8 @@ void MainWindow::set_resource(const QString &res) { void MainWindow::_do_first_run() { m_thr_data = new ThrData(this, m_res); connect(m_thr_data, SIGNAL(signal_update_data(UpdateData*)), this, SLOT(_do_update_data(UpdateData*))); -// connect(m_thr_data, SIGNAL(signal_download(DownloadParam*)), this, SLOT(_do_download(DownloadParam*))); m_thr_data->start(); - //_start_play_thread(); m_thr_play = new ThrPlay(this); connect(m_thr_play, SIGNAL(signal_update_data(UpdateData*)), this, SLOT(_do_update_data(UpdateData*))); @@ -118,24 +108,6 @@ void MainWindow::_do_first_run() { m_thr_play->start(); } -//void MainWindow::_start_play_thread() { -// if(m_thr_play) { -// m_thr_play->stop(); -// //m_thr_play->wait(); - -// disconnect(m_thr_play, SIGNAL(signal_update_data(UpdateData*)), this, SLOT(_do_update_data(UpdateData*))); - -// delete m_thr_play; -// m_thr_play = nullptr; -// } - -// m_thr_play = new ThrPlay(this); -// connect(m_thr_play, SIGNAL(signal_update_data(UpdateData*)), this, SLOT(_do_update_data(UpdateData*))); - -// m_thr_play->speed(m_bar.get_speed()); -// m_thr_play->start(); -//} - void MainWindow::set_speed(int s) { if(m_thr_play) m_thr_play->speed(s); @@ -242,20 +214,27 @@ void MainWindow::_do_update_data(UpdateData* dat) { return; } else if(dat->data_type() == TYPE_IMAGE) { - QImage* img_update = nullptr; - int x, y, w, h; - if(!dat->get_image(&img_update, x, y, w, h)) + UpdateImages uimgs; + if(!dat->get_images(uimgs)) return; - static int img_idx = 0; - img_idx++; - qDebug("draw img: %d (%d,%d)-(%d,%d)", img_idx, x, y, w, h); + if(uimgs.size() > 1) { + // 禁止界面更新 + setUpdatesEnabled(false); + } QPainter pp(&m_canvas); - pp.drawImage(x, y, *img_update, 0, 0, w, h, Qt::AutoColor); + for(int i = 0; i < uimgs.size(); ++i) { + pp.drawImage(uimgs[i].x, uimgs[i].y, *(uimgs[i].img), 0, 0, uimgs[i].w, uimgs[i].h, Qt::AutoColor); + update(uimgs[i].x, uimgs[i].y, uimgs[i].w, uimgs[i].h); + } - update(x, y, w, h); + + if(uimgs.size() > 1) { + // 允许界面更新 + setUpdatesEnabled(true); + } return; } @@ -265,6 +244,18 @@ void MainWindow::_do_update_data(UpdateData* dat) { return; } + else if(dat->data_type() == TYPE_DISABLE_DRAW) { + // 禁止界面更新 + setUpdatesEnabled(false); + return; + } + + else if(dat->data_type() == TYPE_ENABLE_DRAW) { + // 允许界面更新 + setUpdatesEnabled(true); + return; + } + else if(dat->data_type() == TYPE_MESSAGE) { if(dat->message().isEmpty()) { m_show_message = false; @@ -361,12 +352,14 @@ void MainWindow::_do_update_data(UpdateData* dat) { QString title; if (m_rec_hdr.basic.conn_port == 3389) { - title = QString(LOCAL8BIT("[%1] %2@%3 [Teleport-RDP录像回放]").arg(m_rec_hdr.basic.acc_username, m_rec_hdr.basic.user_username, m_rec_hdr.basic.conn_ip)); +// title = QString(LOCAL8BIT("[%1] %2@%3 [Teleport-RDP录像回放]").arg(m_rec_hdr.basic.acc_username, m_rec_hdr.basic.user_username, m_rec_hdr.basic.conn_ip)); + title = QString(LOCAL8BIT("用户 %1 访问 %2 的 %3 账号").arg(m_rec_hdr.basic.user_username, m_rec_hdr.basic.conn_ip, m_rec_hdr.basic.acc_username)); } else { QString _port; _port.sprintf("%d", m_rec_hdr.basic.conn_port); - title = QString(LOCAL8BIT("[%1] %2@%3:%4 [Teleport-RDP录像回放]").arg(m_rec_hdr.basic.acc_username, m_rec_hdr.basic.user_username, m_rec_hdr.basic.conn_ip, _port)); + //title = QString(LOCAL8BIT("[%1] %2@%3:%4 [Teleport-RDP录像回放]").arg(m_rec_hdr.basic.acc_username, m_rec_hdr.basic.user_username, m_rec_hdr.basic.conn_ip, _port)); + title = QString(LOCAL8BIT("用户 %1 访问 %2:%3 的 %4 账号").arg(m_rec_hdr.basic.user_username, m_rec_hdr.basic.conn_ip, _port, m_rec_hdr.basic.acc_username)); } setWindowTitle(title); @@ -457,13 +450,6 @@ void MainWindow::mousePressEvent(QMouseEvent *e) { } void MainWindow::mouseReleaseEvent(QMouseEvent *e) { - qDebug("mouse release."); -// if(!m_show_default) { -// QRect rc = m_bar.rc(); -// if(rc.contains(e->pos())) { -// m_bar.onMouseRelease(e->x(), e->y(), e->button()); -// } -// } m_bar.onMouseRelease(e->x(), e->y(), e->button()); } diff --git a/client/tp-player/record_format.h b/client/tp-player/record_format.h index 3fe863c..ebc6041 100644 --- a/client/tp-player/record_format.h +++ b/client/tp-player/record_format.h @@ -24,10 +24,10 @@ typedef struct TS_RECORD_HEADER_INFO { uint32_t magic; // "TPPR" 标志 TelePort Protocol Record uint16_t ver; // 录像文件版本,从3.5.0开始,为4 uint16_t type; // - uint32_t packages; // 总包数 + // uint32_t packages; // 总包数 uint32_t time_ms; // 总耗时(毫秒) uint32_t dat_file_count; // 数据文件数量 - uint8_t _reserve[64-4-2-2-4-4-4]; + uint8_t _reserve[64-4-2-2-4-4]; }TS_RECORD_HEADER_INFO; #define ts_record_header_info_size sizeof(TS_RECORD_HEADER_INFO) @@ -69,7 +69,7 @@ typedef struct TS_RECORD_PKG { uint8_t _reserve[3]; // 保留 uint32_t size; // 这个包的总大小(不含包头) uint32_t time_ms; // 这个包距起始时间的时间差(毫秒,意味着一个连接不能持续超过49天) - uint32_t index; // 这个包的序号(最后一个包的序号与TS_RECORD_HEADER_INFO::packages数量匹配) + // uint32_t index; // 这个包的序号(最后一个包的序号与TS_RECORD_HEADER_INFO::packages数量匹配) }TS_RECORD_PKG; @@ -91,6 +91,7 @@ typedef struct TS_RECORD_RDP_IMAGE_INFO { uint16_t bitsPerPixel; uint8_t format; uint8_t _reserved; + uint32_t dat_len; }TS_RECORD_RDP_IMAGE_INFO; // 关键帧索引 diff --git a/client/tp-player/thr_data.cpp b/client/tp-player/thr_data.cpp index 6c56156..60502a3 100644 --- a/client/tp-player/thr_data.cpp +++ b/client/tp-player/thr_data.cpp @@ -15,77 +15,6 @@ #include "rle.h" -// for test only -int g_kf_idx = 0; -QByteArray g_kfdata[10]; -QByteArray* g_kf = nullptr; - -int g_img_idx = 0; - -void _update_key_frame(QByteArray* kf, uint16_t screen_w, uint16_t screen_h, uint16_t destLeft, uint16_t destTop, uint16_t w, uint16_t h, uint16_t wr, uint16_t hr, uint16_t bitsPerPixel, bool isCompressed, const uint8_t* dat, size_t len) { - switch(bitsPerPixel) { -// case 15: -// if(isCompressed) { -// uint8_t* _dat = reinterpret_cast(calloc(1, w*h*2)); -// if(!bitmap_decompress1(_dat, w, h, dat, len)) { -// free(_dat); -// qDebug("bitmap_decompress1() failed."); -// return; -// } -// out = new QImage(_dat, w, h, QImage::Format_RGB555); -// free(_dat); -// } -// else { -// out = new QImage(QImage(dat, w, h, QImage::Format_RGB555).transformed(QMatrix(1.0, 0.0, 0.0, -1.0, 0.0, 0.0))); -// } -// return out; - - case 16: - { - g_img_idx++; - uint8_t* kfd = reinterpret_cast(kf->data()); - if(isCompressed) { - uint8_t* _dat = reinterpret_cast(calloc(1, w*h*2)); - if(!bitmap_decompress2(_dat, w, h, dat, static_cast(len))) { - free(_dat); - qDebug() << "------------------DECOMPRESS2 failed."; - return; - } - -// out = new QImage(w, h, QImage::Format_RGB16); - - qDebug("c: %ld, img: %d (%d,%d)-(%d,%d) (%d,%d)", ((destTop+hr-1)*screen_w)+destLeft+wr-1, g_img_idx, destLeft, destTop, w, h, wr, hr); - for(int y = 0; y < hr; y++) { -// if((destTop+y)*screen_w+destLeft > 6) -// memcpy(kfd+((destTop+y)*screen_w+destLeft - 6)*2, _dat+((y*w)*2), wr*2); -// else - memcpy(kfd+((destTop+y)*screen_w+destLeft)*2, _dat+((y*w)*2), wr*2); - } - - free(_dat); - return; - } - else { -// out = new QImage(QImage(dat, w, h, QImage::Format_RGB16).transformed(QMatrix(1.0, 0.0, 0.0, -1.0, 0.0, 0.0))); - - qDebug("nc: %ld, img: %d (%d,%d)-(%d,%d) (%d,%d)", ((destTop+hr-1)*screen_w)+destLeft+wr-1, g_img_idx, destLeft, destTop, w, h, wr, hr); - for(int y = 0; y < hr; y++) { - memcpy(kfd+((destTop+h-y)*screen_w+destLeft)*2, dat+(y*w*2), wr*2); - } - } - - return; - } - - case 24: - case 32: - default: - qDebug() << "------------------NOT support UNKNOWN bitsPerPix" << bitsPerPixel; - return; - } -} - - //================================================================= // ThrData //================================================================= @@ -102,8 +31,6 @@ ThrData::ThrData(MainWindow* mainwin, const QString& res) { m_file_idx = 0; m_offset = 0; - m_xxx = false; - #ifdef __APPLE__ m_data_path_base = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); m_data_path_base += "/tp-testdata/"; @@ -223,18 +150,11 @@ void ThrData::_run() { QFile* fdata = nullptr; - //uint32_t file_idx = 0; - //uint32_t start_offset = 0; qint64 file_size = 0; qint64 file_processed = 0; qint64 read_len = 0; QString str_fidx; - g_kf_idx = 0; - g_kf = &(g_kfdata[g_kf_idx]); - g_kf->resize(m_hdr.basic.width*m_hdr.basic.height*2); - memset(g_kf->data(), 0, m_hdr.basic.width*m_hdr.basic.height*2); - for(;;) { // 任何时候确保第一时间响应退出操作 if(m_need_stop) @@ -356,49 +276,13 @@ void ThrData::_run() { } QByteArray pkg_data = fdata->read(pkg.size); - if(pkg_data.size() != pkg.size) { + if(pkg_data.size() != static_cast(pkg.size)) { qDebug("invaid tp-rdp-%d.tpd file, read_len=%" PRId64 " (3).", m_file_idx+1, read_len); _notify_error(QString("%1\ntp-rdp-%2.tpd").arg(LOCAL8BIT("错误的录像数据文件!"), str_fidx)); return; } file_processed += pkg.size; - - - - - // for test only - if(!m_xxx && pkg.type == TS_RECORD_TYPE_RDP_IMAGE) { - const TS_RECORD_RDP_IMAGE_INFO* info = reinterpret_cast(pkg_data.data()); - uint8_t* img_dat = reinterpret_cast(pkg_data.data() + sizeof(TS_RECORD_RDP_IMAGE_INFO)); - size_t img_len = pkg_data.size() - sizeof(TS_RECORD_RDP_IMAGE_INFO); - - bool isCompress = (info->format == TS_RDP_IMG_BMP) ? true : false; -// _update_key_frame(&g_kf, m_hdr.basic.width, m_hdr.basic.height, info->destLeft, info->destTop, (info->destRight-info->destLeft+1), (info->destBottom-info->destTop+1), info->bitsPerPixel, isCompress, img_dat, img_len); - _update_key_frame(g_kf, m_hdr.basic.width, m_hdr.basic.height, - info->destLeft, info->destTop, - info->width, info->height, - info->destRight - info->destLeft + 1, info->destBottom - info->destTop + 1, - info->bitsPerPixel, isCompress, img_dat, img_len); - } - if(pkg.type == TS_RECORD_TYPE_RDP_KEYFRAME) { -// const TS_RECORD_RDP_KEYFRAME_INFO* info = reinterpret_cast(pkg_data.data()); - uint8_t* img_dat = reinterpret_cast(pkg_data.data() + sizeof(TS_RECORD_RDP_KEYFRAME_INFO)); - uint32_t img_len = pkg_data.size() - sizeof(TS_RECORD_RDP_KEYFRAME_INFO); - - if(m_xxx) { - qDebug("use kf: %d", m_restart_kf_idx); - memcpy(img_dat, g_kfdata[m_restart_kf_idx].data(), img_len); - } - else { - memcpy(img_dat, g_kf->data(), img_len); - } - } - - - - - UpdateData* dat = new UpdateData(m_hdr.basic.width, m_hdr.basic.height); if(!dat->parse(pkg, pkg_data)) { qDebug("invaid tp-rdp-%d.tpd file (4).", m_file_idx+1); @@ -406,47 +290,20 @@ void ThrData::_run() { return; } - - - - // 拖动滚动条后,需要显示一次关键帧数据,然后跳过后续关键帧。 if(pkg.type == TS_RECORD_TYPE_RDP_KEYFRAME) { - g_kf_idx++; - g_kf = &(g_kfdata[g_kf_idx]); - if(!m_xxx) { - g_kf->resize(m_hdr.basic.width*m_hdr.basic.height*2); - memcpy(g_kf->data(), g_kfdata[g_kf_idx-1].data(), m_hdr.basic.width*m_hdr.basic.height*2); - } - qDebug("----key frame: %ld, processed=%" PRId64 ", pkg.size=%d", pkg.time_ms, file_processed, pkg.size); if(m_need_show_kf) { m_need_show_kf = false; qDebug("++ show keyframe."); } else { - //m_restart_kf_idx - - - QString tmp; - tmp.sprintf("%d", g_kf_idx); - QString img_fname = QString("%1/img-%2.png").arg(m_data_path, tmp); - QImage* img = nullptr; - int x = 0, y = 0, w = 0, h = 0; - dat->get_image(&img, x, y, w, h); - if(img != nullptr) - img->save(img_fname, "png"); - qDebug("-- skip keyframe."); delete dat; dat = nullptr; } } - - - - // 数据放到待播放列表中 if(dat) { m_locker.lock(); @@ -497,10 +354,6 @@ void ThrData::restart(uint32_t start_ms) { m_offset = 0; m_file_idx = 0; m_need_show_kf = false; - - g_kf_idx = 0; - m_restart_kf_idx = 0; - m_xxx = true; } else { // 找到最接近 start_ms 但小于它的关键帧 @@ -512,15 +365,14 @@ void ThrData::restart(uint32_t start_ms) { } if(i > 0) i--; - g_kf_idx = i; - m_restart_kf_idx = i; - m_xxx = true; qDebug("restart acturelly at %ld ms, kf: %d", m_kf[i].time_ms, i); // 指定要播放的数据的开始位置 m_offset = m_kf[i].offset; m_file_idx = m_kf[i].file_index; + if(m_file_idx == (uint32_t)-1) + m_file_idx = 0; m_need_show_kf = true; } @@ -596,7 +448,7 @@ bool ThrData::_load_keyframe() { } qint64 read_len = 0; - int kf_count = fsize / sizeof(KEYFRAME_INFO); + int kf_count = static_cast(fsize / sizeof(KEYFRAME_INFO)); for(int i = 0; i < kf_count; ++i) { KEYFRAME_INFO kf; memset(&kf, 0, sizeof(KEYFRAME_INFO)); diff --git a/client/tp-player/thr_data.h b/client/tp-player/thr_data.h index 18c07c6..a00c5e0 100644 --- a/client/tp-player/thr_data.h +++ b/client/tp-player/thr_data.h @@ -102,8 +102,8 @@ private: uint32_t m_file_idx; uint32_t m_offset; - bool m_xxx; - int m_restart_kf_idx; +// bool m_xxx; +// int m_restart_kf_idx; }; #endif // THR_DATA_H diff --git a/client/tp-player/thr_play.cpp b/client/tp-player/thr_play.cpp index f1d4d22..b76d017 100644 --- a/client/tp-player/thr_play.cpp +++ b/client/tp-player/thr_play.cpp @@ -102,6 +102,8 @@ void ThrPlay::run() { } last_time_ms = m_start_ms; m_start_ms = 0; + UpdateData* _enable = new UpdateData(TYPE_ENABLE_DRAW); + emit signal_update_data(_enable); } // 2. 根据数据包的信息,等待到播放时间点 @@ -139,10 +141,10 @@ void ThrPlay::run() { break; if(m_start_ms > 0) { -// if(dat) { -// delete dat; -// dat = nullptr; -// } + delete dat; + dat = nullptr; + UpdateData* _disable = new UpdateData(TYPE_DISABLE_DRAW); + emit signal_update_data(_disable); break; } @@ -170,25 +172,18 @@ void ThrPlay::run() { if(m_need_stop) break; - -// if(m_start_ms > 0) { -// if(dat) { -// delete dat; -// dat = nullptr; -// } -// break; -// } } } last_time_ms = this_time_ms; // 3. 将数据包发送给主UI界面进行显示 - if(dat->data_type() == TYPE_END) { - _notify_message(LOCAL8BIT("播放结束")); + if(dat != nullptr) { + if(dat->data_type() == TYPE_END) { + _notify_message(LOCAL8BIT("播放结束")); + } + emit signal_update_data(dat); } - - emit signal_update_data(dat); } if(dat != nullptr) diff --git a/client/tp-player/update_data.cpp b/client/tp-player/update_data.cpp index ac25d01..15e8165 100644 --- a/client/tp-player/update_data.cpp +++ b/client/tp-player/update_data.cpp @@ -105,7 +105,7 @@ void UpdateData::_init() { m_data_type = TYPE_UNKNOWN; m_hdr = nullptr; m_pointer = nullptr; - m_img = nullptr; +// m_img = nullptr; // m_img_info = nullptr; m_data_buf = nullptr; @@ -121,10 +121,14 @@ UpdateData::~UpdateData() { delete m_hdr; if(m_pointer) delete m_pointer; - if(m_img) - delete m_img; +// if(m_img) +// delete m_img; // if(m_img_info) // delete m_img_info; + for(int i = 0; i < m_images.size(); ++i) { + delete m_images[i].img; + } + m_images.clear(); if(m_data_buf) delete m_data_buf; @@ -143,21 +147,41 @@ bool UpdateData::parse(const TS_RECORD_PKG& pkg, const QByteArray& data) { } else if(pkg.type == TS_RECORD_TYPE_RDP_IMAGE) { m_data_type = TYPE_IMAGE; - if(data.size() <= sizeof(TS_RECORD_RDP_IMAGE_INFO)) - return false; - const TS_RECORD_RDP_IMAGE_INFO* info = reinterpret_cast(data.data()); - const uint8_t* img_dat = reinterpret_cast(data.data() + sizeof(TS_RECORD_RDP_IMAGE_INFO)); - uint32_t img_len = data.size() - sizeof(TS_RECORD_RDP_IMAGE_INFO); - - QImage* img = _rdpimg2QImage(info->width, info->height, info->bitsPerPixel, (info->format == TS_RDP_IMG_BMP) ? true : false, img_dat, img_len); - if(img == nullptr) + if(data.size() <= static_cast(sizeof(uint16_t) + sizeof(TS_RECORD_RDP_IMAGE_INFO))) return false; - m_img = img; - m_img_x = info->destLeft; - m_img_y = info->destTop; - m_img_w = info->destRight - info->destLeft + 1; - m_img_h = info->destBottom - info->destTop + 1; + const uint8_t* dat_ptr = reinterpret_cast(data.data()); + + uint16_t count = (reinterpret_cast(dat_ptr))[0]; + uint32_t offset = sizeof(uint16_t); + + for(uint16_t i = 0; i < count; ++i) { + + const TS_RECORD_RDP_IMAGE_INFO* info = reinterpret_cast(dat_ptr+offset); + offset += sizeof(TS_RECORD_RDP_IMAGE_INFO); + //const uint8_t* img_dat = reinterpret_cast(data.data() + sizeof(TS_RECORD_RDP_IMAGE_INFO)); + //uint32_t img_len = data.size() - sizeof(TS_RECORD_RDP_IMAGE_INFO); + const uint8_t* img_dat = dat_ptr + offset; + offset += info->dat_len; + + + QImage* img = _rdpimg2QImage(info->width, info->height, info->bitsPerPixel, (info->format == TS_RDP_IMG_BMP) ? true : false, img_dat, info->dat_len); + if(img == nullptr) + return false; + +// m_img = img; +// m_img_x = info->destLeft; +// m_img_y = info->destTop; +// m_img_w = info->destRight - info->destLeft + 1; +// m_img_h = info->destBottom - info->destTop + 1; + UPDATE_IMAGE uimg; + uimg.x = info->destLeft; + uimg.y = info->destTop; + uimg.w = info->destRight - info->destLeft + 1; + uimg.h = info->destBottom - info->destTop + 1; + uimg.img = img; + m_images.push_back(uimg); + } return true; } @@ -170,11 +194,20 @@ bool UpdateData::parse(const TS_RECORD_PKG& pkg, const QByteArray& data) { QImage* img = _raw2QImage((int)m_screen_w, (int)m_screen_h, img_dat, img_len); if(img == nullptr) return false; - m_img = img; - m_img_x = 0; - m_img_y = 0; - m_img_w = m_screen_w; - m_img_h = m_screen_h; + + UPDATE_IMAGE uimg; + uimg.x = 0; + uimg.y = 0; + uimg.w = m_screen_w; + uimg.h = m_screen_h; + uimg.img = img; + m_images.push_back(uimg); + +// m_img = img; +// m_img_x = 0; +// m_img_y = 0; +// m_img_w = m_screen_w; +// m_img_h = m_screen_h; return true; } diff --git a/client/tp-player/update_data.h b/client/tp-player/update_data.h index e037588..d6e1380 100644 --- a/client/tp-player/update_data.h +++ b/client/tp-player/update_data.h @@ -2,10 +2,15 @@ #define UPDATE_DATA_H #include +#include #include "record_format.h" #define TYPE_UNKNOWN 0 #define TYPE_HEADER_INFO 1 + +#define TYPE_DISABLE_DRAW 5 +#define TYPE_ENABLE_DRAW 6 + #define TYPE_POINTER 10 #define TYPE_IMAGE 11 #define TYPE_KEYFRAME 12 @@ -15,6 +20,17 @@ #define TYPE_MESSAGE 90 #define TYPE_ERROR 91 + +typedef struct UPDATE_IMAGE { + int x; + int y; + int w; + int h; + QImage* img; +}UPDATE_IMAGE; + +typedef QVector UpdateImages; + class UpdateData : public QObject { Q_OBJECT @@ -28,14 +44,20 @@ public: bool parse(const TS_RECORD_PKG& pkg, const QByteArray& data); TS_RECORD_HEADER* get_header() {return m_hdr;} TS_RECORD_RDP_POINTER* get_pointer() {return m_pointer;} - bool get_image(QImage** img, int& x, int& y, int& w, int& h) { - if(m_img == nullptr) +// bool get_image(QImage** img, int& x, int& y, int& w, int& h) { +// if(m_img == nullptr) +// return false; +// *img = m_img; +// x = m_img_x; +// y = m_img_y; +// w = m_img_w; +// h = m_img_h; +// return true; +// } + bool get_images(UpdateImages& uimgs) const { + if(m_images.size() == 0) return false; - *img = m_img; - x = m_img_x; - y = m_img_y; - w = m_img_w; - h = m_img_h; + uimgs = m_images; return true; } @@ -76,11 +98,12 @@ private: // for POINTER TS_RECORD_RDP_POINTER* m_pointer; // for IMAGE - QImage* m_img; - int m_img_x; - int m_img_y; - int m_img_w; - int m_img_h; +// QImage* m_img; +// int m_img_x; +// int m_img_y; +// int m_img_w; +// int m_img_h; + UpdateImages m_images; // TS_RECORD_RDP_IMAGE_INFO* m_img_info; diff --git a/server/tp_core/common/base_record.h b/server/tp_core/common/base_record.h index c3fb219..a7080f3 100644 --- a/server/tp_core/common/base_record.h +++ b/server/tp_core/common/base_record.h @@ -36,10 +36,10 @@ typedef struct TS_RECORD_HEADER_INFO { ex_u32 magic; // "TPPR" 标志 TelePort Protocol Record ex_u16 ver; // 录像文件版本,v3.5.0开始为4 ex_u16 type; // 录像内容,SSH or RDP - ex_u32 packages; // 总包数 + // ex_u32 packages; // 总包数 ex_u32 time_ms; // 总耗时(毫秒) ex_u32 dat_file_count; // 数据文件数量 - ex_u8 _reserve[64-4-2-2-4-4-4]; + ex_u8 _reserve[64-4-2-2-4-4]; }TS_RECORD_HEADER_INFO; #define ts_record_header_info_size sizeof(TS_RECORD_HEADER_INFO) @@ -80,7 +80,7 @@ typedef struct TS_RECORD_PKG { ex_u8 _reserve[3]; // 保留 ex_u32 size; // 这个包的总大小(不含包头) ex_u32 time_ms; // 这个包距起始时间的时间差(毫秒,意味着一个连接不能持续超过49天) - ex_u32 index; // 这个包的序号(最后一个包的序号与TS_RECORD_HEADER_INFO::packages数量匹配) + //ex_u32 index; // 这个包的序号(最后一个包的序号与TS_RECORD_HEADER_INFO::packages数量匹配) }TS_RECORD_PKG; #pragma pack(pop) From 8ef5cfe118375c615289cde87ef7acf58fd0b0bc Mon Sep 17 00:00:00 2001 From: Apex Liu Date: Mon, 11 Nov 2019 00:45:37 +0800 Subject: [PATCH 37/44] =?UTF-8?q?=E5=87=86=E5=A4=87=E5=8A=A0=E5=85=A5zlib?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/tp_core/common/base_record.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/tp_core/common/base_record.h b/server/tp_core/common/base_record.h index a7080f3..f4ac222 100644 --- a/server/tp_core/common/base_record.h +++ b/server/tp_core/common/base_record.h @@ -7,11 +7,11 @@ #include -// #define MAX_CACHE_SIZE 1048576 // 1M = 1024*1024*1 -// #define MAX_SIZE_PER_FILE 4194304 // 4M = 1024*1024*4 +#define MAX_CACHE_SIZE 1048576 // 1M = 1024*1024*1 +#define MAX_SIZE_PER_FILE 4194304 // 4M = 1024*1024*4 // for test. -#define MAX_CACHE_SIZE 524288 // 512KB = 512*1024 -#define MAX_SIZE_PER_FILE 1048576 // 1M = 1024*1024*1 +// #define MAX_CACHE_SIZE 524288 // 512KB = 512*1024 +// #define MAX_SIZE_PER_FILE 1048576 // 1M = 1024*1024*1 #pragma pack(push,1) From 41fa960afcc0b3c1753bf87d51bdb9bc90c04702 Mon Sep 17 00:00:00 2001 From: Apex Liu Date: Wed, 13 Nov 2019 21:34:30 +0800 Subject: [PATCH 38/44] =?UTF-8?q?=E6=8C=89=E4=BC=98=E5=8C=96=E5=90=8E?= =?UTF-8?q?=E7=9A=84RDP=E5=BD=95=E5=83=8F=E6=A0=BC=E5=BC=8F=E8=BF=9B?= =?UTF-8?q?=E8=A1=8C=E6=92=AD=E6=94=BE=EF=BC=8C=E5=8A=9F=E8=83=BD=E5=AE=8C?= =?UTF-8?q?=E6=88=90=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/tp-player/mainwindow.cpp | 4 +- client/tp-player/record_format.h | 2 + client/tp-player/thr_data.cpp | 249 +++++++++++++++++++++++++++++-- client/tp-player/thr_data.h | 18 ++- client/tp-player/tp-player.pro | 13 ++ client/tp-player/update_data.cpp | 159 +++++++++++--------- client/tp-player/update_data.h | 25 +--- external/version.ini | 3 +- 8 files changed, 363 insertions(+), 110 deletions(-) diff --git a/client/tp-player/mainwindow.cpp b/client/tp-player/mainwindow.cpp index c9d9adf..8411b73 100644 --- a/client/tp-player/mainwindow.cpp +++ b/client/tp-player/mainwindow.cpp @@ -214,8 +214,8 @@ void MainWindow::_do_update_data(UpdateData* dat) { return; } else if(dat->data_type() == TYPE_IMAGE) { - UpdateImages uimgs; - if(!dat->get_images(uimgs)) + const UpdateImages uimgs = dat->get_images(); + if(uimgs.size() == 0) return; if(uimgs.size() > 1) { diff --git a/client/tp-player/record_format.h b/client/tp-player/record_format.h index ebc6041..42b8ff3 100644 --- a/client/tp-player/record_format.h +++ b/client/tp-player/record_format.h @@ -16,6 +16,7 @@ #define TS_RDP_BTN_PRESSED 1 #define TS_RDP_IMG_RAW 0 // 未压缩,原始数据(根据bitsPerPixel,多个字节对应一个点的颜色) #define TS_RDP_IMG_BMP 1 // 压缩的BMP数据 +#define TS_RDP_IMG_ALT 2 #pragma pack(push,1) @@ -92,6 +93,7 @@ typedef struct TS_RECORD_RDP_IMAGE_INFO { uint8_t format; uint8_t _reserved; uint32_t dat_len; + uint32_t zip_len; }TS_RECORD_RDP_IMAGE_INFO; // 关键帧索引 diff --git a/client/tp-player/thr_data.cpp b/client/tp-player/thr_data.cpp index 60502a3..0d0e327 100644 --- a/client/tp-player/thr_data.cpp +++ b/client/tp-player/thr_data.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "thr_play.h" #include "thr_data.h" @@ -15,6 +16,80 @@ #include "rle.h" + +static QImage* _rdpimg2QImage(int w, int h, int bitsPerPixel, bool isCompressed, const uint8_t* dat, uint32_t len) { + QImage* out; + switch(bitsPerPixel) { + case 15: + if(isCompressed) { + uint8_t* _dat = reinterpret_cast(calloc(1, w*h*2)); + if(!bitmap_decompress1(_dat, w, h, dat, len)) { + free(_dat); + return nullptr; + } + out = new QImage(_dat, w, h, QImage::Format_RGB555); + free(_dat); + } + else { + out = new QImage(QImage(dat, w, h, QImage::Format_RGB555).transformed(QMatrix(1.0, 0.0, 0.0, -1.0, 0.0, 0.0))); + } + return out; + + case 16: + if(isCompressed) { + uint8_t* _dat = reinterpret_cast(calloc(1, w*h*2)); + if(!bitmap_decompress2(_dat, w, h, dat, len)) { + free(_dat); + qDebug() << "22------------------DECOMPRESS2 failed."; + return nullptr; + } + + // TODO: 这里需要进一步优化,直接操作QImage的buffer。 + out = new QImage(w, h, QImage::Format_RGB16); + for(int y = 0; y < h; y++) { + for(int x = 0; x < w; x++) { + uint16 a = ((uint16*)_dat)[y * w + x]; + uint8 r = ((a & 0xf800) >> 11) * 255 / 31; + uint8 g = ((a & 0x07e0) >> 5) * 255 / 63; + uint8 b = (a & 0x001f) * 255 / 31; + out->setPixelColor(x, y, QColor(r,g,b)); + } + } + free(_dat); + return out; + } + else { + out = new QImage(QImage(dat, w, h, QImage::Format_RGB16).transformed(QMatrix(1.0, 0.0, 0.0, -1.0, 0.0, 0.0))); + } + return out; + + case 24: + case 32: + default: + qDebug() << "--------NOT support UNKNOWN bitsPerPix" << bitsPerPixel; + return nullptr; + } +} + +static QImage* _raw2QImage(int w, int h, const uint8_t* dat, uint32_t len) { + QImage* out; + + // TODO: 这里需要进一步优化,直接操作QImage的buffer。 + out = new QImage(w, h, QImage::Format_RGB16); + for(int y = 0; y < h; y++) { + for(int x = 0; x < w; x++) { + uint16 a = ((uint16*)dat)[y * w + x]; + uint8 r = ((a & 0xf800) >> 11) * 255 / 31; + uint8 g = ((a & 0x07e0) >> 5) * 255 / 63; + uint8 b = (a & 0x001f) * 255 / 31; + out->setPixelColor(x, y, QColor(r,g,b)); + } + } + return out; +} + + + //================================================================= // ThrData //================================================================= @@ -186,9 +261,9 @@ void ThrData::_run() { pkg_count_in_queue = m_data.size(); m_locker.unlock(); - // 少于500个的话,补足到1000个 - if(m_data.size() < 500) - pkg_need_add = 1000 - pkg_count_in_queue; + // 少于1000个的话,补足到2000个 + if(m_data.size() < 1000) + pkg_need_add = 2000 - pkg_count_in_queue; if(pkg_need_add == 0) { msleep(100); @@ -283,13 +358,24 @@ void ThrData::_run() { } file_processed += pkg.size; - UpdateData* dat = new UpdateData(m_hdr.basic.width, m_hdr.basic.height); - if(!dat->parse(pkg, pkg_data)) { + //UpdateData* dat = new UpdateData(m_hdr.basic.width, m_hdr.basic.height); + UpdateData* dat = _parse(pkg, pkg_data); + //if(!dat->parse(pkg, pkg_data)) { + if(dat == nullptr) { qDebug("invaid tp-rdp-%d.tpd file (4).", m_file_idx+1); _notify_error(QString("%1\ntp-rdp-%2.tpd").arg(LOCAL8BIT("错误的录像数据文件!"), str_fidx)); return; } + // 遇到关键帧,需要清除自上一个关键帧以来保存的缓存图像数据 + if(pkg.type == TS_RECORD_TYPE_RDP_KEYFRAME) { + for(size_t ci = 0; ci < m_cache_imgs.size(); ++ci) { + if(m_cache_imgs[ci] != nullptr) + delete m_cache_imgs[ci]; + } + m_cache_imgs.clear(); + } + // 拖动滚动条后,需要显示一次关键帧数据,然后跳过后续关键帧。 if(pkg.type == TS_RECORD_TYPE_RDP_KEYFRAME) { qDebug("----key frame: %ld, processed=%" PRId64 ", pkg.size=%d", pkg.time_ms, file_processed, pkg.size); @@ -333,6 +419,147 @@ void ThrData::_run() { } } +UpdateData* ThrData::_parse(const TS_RECORD_PKG& pkg, const QByteArray& data) { + if(pkg.type == TS_RECORD_TYPE_RDP_POINTER) { + if(data.size() != sizeof(TS_RECORD_RDP_POINTER)) + return nullptr; + + UpdateData* ud = new UpdateData(); + ud->set_pointer(pkg.time_ms, reinterpret_cast(data.data())); + return ud; + } + else if(pkg.type == TS_RECORD_TYPE_RDP_IMAGE) { + UpdateData* ud = new UpdateData(TYPE_IMAGE, pkg.time_ms); + + if(data.size() < static_cast(sizeof(uint16_t) + sizeof(TS_RECORD_RDP_IMAGE_INFO))) { + delete ud; + return nullptr; + } + + const uint8_t* dat_ptr = reinterpret_cast(data.data()); + + uint16_t count = (reinterpret_cast(dat_ptr))[0]; + uint32_t offset = sizeof(uint16_t); + + UpdateImages& imgs = ud->get_images(); + + for(uint16_t i = 0; i < count; ++i) { + + const TS_RECORD_RDP_IMAGE_INFO* info = reinterpret_cast(dat_ptr+offset); + offset += sizeof(TS_RECORD_RDP_IMAGE_INFO); + + if(info->format != TS_RDP_IMG_ALT) { + const uint8_t* img_dat = dat_ptr + offset; + + const uint8_t* real_img_dat = nullptr; + QByteArray unzip_data; + if(info->zip_len > 0) { + // 数据被压缩了,需要解压缩 + unzip_data.resize(static_cast(info->dat_len)); + + uLong u_len = info->dat_len; + int err = uncompress(reinterpret_cast(unzip_data.data()), &u_len, img_dat, info->zip_len); + if(err != Z_OK || u_len != info->dat_len) { + qDebug("image uncompress failed. err=%d.", err); + } + else { + real_img_dat = reinterpret_cast(unzip_data.data()); + } + + offset += info->zip_len; + } + else { + real_img_dat = img_dat; + offset += info->dat_len; + } + + + UPDATE_IMAGE uimg; + uimg.x = info->destLeft; + uimg.y = info->destTop; + uimg.w = info->destRight - info->destLeft + 1; + uimg.h = info->destBottom - info->destTop + 1; + if(real_img_dat) + uimg.img = _rdpimg2QImage(info->width, info->height, info->bitsPerPixel, (info->format == TS_RDP_IMG_BMP) ? true : false, real_img_dat, info->dat_len); + else + uimg.img = nullptr; + imgs.push_back(uimg); + + QImage* cache_img = nullptr; + if(uimg.img != nullptr) + cache_img = new QImage(*uimg.img); + + m_cache_imgs.push_back(cache_img); + } + else { + UPDATE_IMAGE uimg; + uimg.x = info->destLeft; + uimg.y = info->destTop; + uimg.w = info->destRight - info->destLeft + 1; + uimg.h = info->destBottom - info->destTop + 1; + + size_t cache_idx = info->dat_len; + + if(cache_idx >= m_cache_imgs.size() || m_cache_imgs[cache_idx] == nullptr) { + uimg.img = nullptr; + } + else { + uimg.img = new QImage(*m_cache_imgs[cache_idx]); + } + imgs.push_back(uimg); + } + } + + return ud; + } + else if(pkg.type == TS_RECORD_TYPE_RDP_KEYFRAME) { + UpdateData* ud = new UpdateData(TYPE_IMAGE, pkg.time_ms); + const TS_RECORD_RDP_KEYFRAME_INFO* info = reinterpret_cast(data.data()); + const uint8_t* data_buf = reinterpret_cast(data.data() + sizeof(TS_RECORD_RDP_KEYFRAME_INFO)); + uint32_t data_len = data.size() - sizeof(TS_RECORD_RDP_KEYFRAME_INFO); + + UpdateImages& imgs = ud->get_images(); + + UPDATE_IMAGE uimg; + uimg.x = 0; + uimg.y = 0; + uimg.w = m_hdr.basic.width; + uimg.h = m_hdr.basic.height; + + const uint8_t* real_img_dat = nullptr; + uint32_t real_img_len = m_hdr.basic.width * m_hdr.basic.height * 2; + + QByteArray unzip_data; + if(data_len != real_img_len) { + // 数据被压缩了,需要解压缩 + unzip_data.resize(static_cast(real_img_len)); + + uLong u_len = real_img_len; + int err = uncompress(reinterpret_cast(unzip_data.data()), &u_len, data_buf, data_len); + if(err != Z_OK || u_len != real_img_len) { + qDebug("keyframe uncompress failed. err=%d.", err); + } + else { + real_img_dat = reinterpret_cast(unzip_data.data()); + } + } + else { + real_img_dat = data_buf; + } + + if(real_img_dat != nullptr) + uimg.img = _raw2QImage(m_hdr.basic.width, m_hdr.basic.height, real_img_dat, real_img_len); + else + uimg.img = nullptr; + imgs.push_back(uimg); + + return ud; + } + + return nullptr; +} + + void ThrData::restart(uint32_t start_ms) { qDebug("restart at %ld ms", start_ms); // 让处理线程处理完当前循环,然后等待 @@ -441,19 +668,19 @@ bool ThrData::_load_keyframe() { } qint64 fsize = f_kf.size(); - if(!fsize || fsize % sizeof(KEYFRAME_INFO) != 0) { + if(!fsize || fsize % sizeof(TS_RECORD_RDP_KEYFRAME_INFO) != 0) { qDebug() << "Can not open " << tpk_fname << " for read."; _notify_error(LOCAL8BIT("关键帧信息文件格式错误!")); return false; } qint64 read_len = 0; - int kf_count = static_cast(fsize / sizeof(KEYFRAME_INFO)); + int kf_count = static_cast(fsize / sizeof(TS_RECORD_RDP_KEYFRAME_INFO)); for(int i = 0; i < kf_count; ++i) { - KEYFRAME_INFO kf; - memset(&kf, 0, sizeof(KEYFRAME_INFO)); - read_len = f_kf.read(reinterpret_cast(&kf), sizeof(KEYFRAME_INFO)); - if(read_len != sizeof(KEYFRAME_INFO)) { + TS_RECORD_RDP_KEYFRAME_INFO kf; + memset(&kf, 0, sizeof(TS_RECORD_RDP_KEYFRAME_INFO)); + read_len = f_kf.read(reinterpret_cast(&kf), sizeof(TS_RECORD_RDP_KEYFRAME_INFO)); + if(read_len != sizeof(TS_RECORD_RDP_KEYFRAME_INFO)) { qDebug() << "invaid .tpk file."; _notify_error(LOCAL8BIT("关键帧信息文件格式错误!")); return false; diff --git a/client/tp-player/thr_data.h b/client/tp-player/thr_data.h index a00c5e0..5f25ebf 100644 --- a/client/tp-player/thr_data.h +++ b/client/tp-player/thr_data.h @@ -7,6 +7,7 @@ #include #include #include +#include #include "update_data.h" #include "record_format.h" #include "thr_download.h" @@ -33,13 +34,15 @@ 这样,下次需要下载指定文件时,如果发现对应的临时文件存在,可以根据已下载字节数,继续下载。 */ -typedef struct KEYFRAME_INFO { - uint32_t time_ms; // 此关键帧的时间点 - uint32_t file_index; // 此关键帧图像数据位于哪一个数据文件中 - uint32_t offset; // 此关键帧图像数据在数据文件中的偏移 -}KEYFRAME_INFO; +//typedef struct KEYFRAME_INFO { +// uint32_t time_ms; // 此关键帧的时间点 +// uint32_t file_index; // 此关键帧图像数据位于哪一个数据文件中 +// uint32_t offset; // 此关键帧图像数据在数据文件中的偏移 +//}KEYFRAME_INFO; -typedef std::vector KeyFrames; +typedef std::vector KeyFrames; + +typedef std::vector CachedImages; class MainWindow; @@ -69,6 +72,8 @@ private: void _clear_data(); void _prepare(); + UpdateData* _parse(const TS_RECORD_PKG& pkg, const QByteArray& data); + void _notify_message(const QString& msg); void _notify_error(const QString& err_msg); @@ -104,6 +109,7 @@ private: // bool m_xxx; // int m_restart_kf_idx; + CachedImages m_cache_imgs; }; #endif // THR_DATA_H diff --git a/client/tp-player/tp-player.pro b/client/tp-player/tp-player.pro index 0a8af1d..c72fee9 100644 --- a/client/tp-player/tp-player.pro +++ b/client/tp-player/tp-player.pro @@ -37,3 +37,16 @@ RC_FILE += \ FORMS += \ mainwindow.ui + +win32:CONFIG(release, debug|release): LIBS += -L$$PWD/../../external/zlib/build/release/ -lzlibstatic +else:win32:CONFIG(debug, debug|release): LIBS += -L$$PWD/../../external/zlib/build/debug/ -lzlibstaticd + +INCLUDEPATH += $$PWD/../../external/zlib +INCLUDEPATH += $$PWD/../../external/zlib/build +DEPENDPATH += $$PWD/../../external/zlib +DEPENDPATH += $$PWD/../../external/zlib/build + +win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$PWD/../../external/zlib/build/release/libzlibstaticd.a +else:win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$PWD/../../external/zlib/build/debug/libzlibstaticd.a +else:win32:!win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$PWD/../../external/zlib/build/release/zlibstaticd.lib +else:win32:!win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$PWD/../../external/zlib/build/debug/zlibstaticd.lib diff --git a/client/tp-player/update_data.cpp b/client/tp-player/update_data.cpp index 15e8165..3b931c6 100644 --- a/client/tp-player/update_data.cpp +++ b/client/tp-player/update_data.cpp @@ -1,80 +1,80 @@ #include "update_data.h" -#include "rle.h" +//#include "rle.h" #include #include -static QImage* _rdpimg2QImage(int w, int h, int bitsPerPixel, bool isCompressed, const uint8_t* dat, uint32_t len) { - QImage* out; - switch(bitsPerPixel) { - case 15: - if(isCompressed) { - uint8_t* _dat = reinterpret_cast(calloc(1, w*h*2)); - if(!bitmap_decompress1(_dat, w, h, dat, len)) { - free(_dat); - return nullptr; - } - out = new QImage(_dat, w, h, QImage::Format_RGB555); - free(_dat); - } - else { - out = new QImage(QImage(dat, w, h, QImage::Format_RGB555).transformed(QMatrix(1.0, 0.0, 0.0, -1.0, 0.0, 0.0))); - } - return out; +//static QImage* _rdpimg2QImage(int w, int h, int bitsPerPixel, bool isCompressed, const uint8_t* dat, uint32_t len) { +// QImage* out; +// switch(bitsPerPixel) { +// case 15: +// if(isCompressed) { +// uint8_t* _dat = reinterpret_cast(calloc(1, w*h*2)); +// if(!bitmap_decompress1(_dat, w, h, dat, len)) { +// free(_dat); +// return nullptr; +// } +// out = new QImage(_dat, w, h, QImage::Format_RGB555); +// free(_dat); +// } +// else { +// out = new QImage(QImage(dat, w, h, QImage::Format_RGB555).transformed(QMatrix(1.0, 0.0, 0.0, -1.0, 0.0, 0.0))); +// } +// return out; - case 16: - if(isCompressed) { - uint8_t* _dat = reinterpret_cast(calloc(1, w*h*2)); - if(!bitmap_decompress2(_dat, w, h, dat, len)) { - free(_dat); - qDebug() << "22------------------DECOMPRESS2 failed."; - return nullptr; - } +// case 16: +// if(isCompressed) { +// uint8_t* _dat = reinterpret_cast(calloc(1, w*h*2)); +// if(!bitmap_decompress2(_dat, w, h, dat, len)) { +// free(_dat); +// qDebug() << "22------------------DECOMPRESS2 failed."; +// return nullptr; +// } - // TODO: 这里需要进一步优化,直接操作QImage的buffer。 - out = new QImage(w, h, QImage::Format_RGB16); - for(int y = 0; y < h; y++) { - for(int x = 0; x < w; x++) { - uint16 a = ((uint16*)_dat)[y * w + x]; - uint8 r = ((a & 0xf800) >> 11) * 255 / 31; - uint8 g = ((a & 0x07e0) >> 5) * 255 / 63; - uint8 b = (a & 0x001f) * 255 / 31; - out->setPixelColor(x, y, QColor(r,g,b)); - } - } - free(_dat); - return out; - } - else { - out = new QImage(QImage(dat, w, h, QImage::Format_RGB16).transformed(QMatrix(1.0, 0.0, 0.0, -1.0, 0.0, 0.0))); - } - return out; +// // TODO: 这里需要进一步优化,直接操作QImage的buffer。 +// out = new QImage(w, h, QImage::Format_RGB16); +// for(int y = 0; y < h; y++) { +// for(int x = 0; x < w; x++) { +// uint16 a = ((uint16*)_dat)[y * w + x]; +// uint8 r = ((a & 0xf800) >> 11) * 255 / 31; +// uint8 g = ((a & 0x07e0) >> 5) * 255 / 63; +// uint8 b = (a & 0x001f) * 255 / 31; +// out->setPixelColor(x, y, QColor(r,g,b)); +// } +// } +// free(_dat); +// return out; +// } +// else { +// out = new QImage(QImage(dat, w, h, QImage::Format_RGB16).transformed(QMatrix(1.0, 0.0, 0.0, -1.0, 0.0, 0.0))); +// } +// return out; - case 24: - case 32: - default: - qDebug() << "--------NOT support UNKNOWN bitsPerPix" << bitsPerPixel; - return nullptr; - } -} +// case 24: +// case 32: +// default: +// qDebug() << "--------NOT support UNKNOWN bitsPerPix" << bitsPerPixel; +// return nullptr; +// } +//} -static QImage* _raw2QImage(int w, int h, const uint8_t* dat, uint32_t len) { - QImage* out; +//static QImage* _raw2QImage(int w, int h, const uint8_t* dat, uint32_t len) { +// QImage* out; - // TODO: 这里需要进一步优化,直接操作QImage的buffer。 - out = new QImage(w, h, QImage::Format_RGB16); - for(int y = 0; y < h; y++) { - for(int x = 0; x < w; x++) { - uint16 a = ((uint16*)dat)[y * w + x]; - uint8 r = ((a & 0xf800) >> 11) * 255 / 31; - uint8 g = ((a & 0x07e0) >> 5) * 255 / 63; - uint8 b = (a & 0x001f) * 255 / 31; - out->setPixelColor(x, y, QColor(r,g,b)); - } - } - return out; -} +// // TODO: 这里需要进一步优化,直接操作QImage的buffer。 +// out = new QImage(w, h, QImage::Format_RGB16); +// for(int y = 0; y < h; y++) { +// for(int x = 0; x < w; x++) { +// uint16 a = ((uint16*)dat)[y * w + x]; +// uint8 r = ((a & 0xf800) >> 11) * 255 / 31; +// uint8 g = ((a & 0x07e0) >> 5) * 255 / 63; +// uint8 b = (a & 0x001f) * 255 / 31; +// out->setPixelColor(x, y, QColor(r,g,b)); +// } +// } +// return out; +//} UpdateData::UpdateData() : QObject(nullptr) { @@ -87,6 +87,13 @@ UpdateData::UpdateData(int data_type) : QObject(nullptr) m_data_type = data_type; } +UpdateData::UpdateData(int data_type, uint32_t time_ms) : QObject(nullptr) +{ + _init(); + m_data_type = data_type; + m_time_ms = time_ms; +} + UpdateData::UpdateData(const TS_RECORD_HEADER& hdr) : QObject(nullptr) { _init(); @@ -95,11 +102,11 @@ UpdateData::UpdateData(const TS_RECORD_HEADER& hdr) : QObject(nullptr) memcpy(m_hdr, &hdr, sizeof(TS_RECORD_HEADER)); } -UpdateData::UpdateData(uint16_t screen_w, uint16_t screen_h) { - _init(); - m_screen_w = screen_w; - m_screen_h = screen_h; -} +//UpdateData::UpdateData(uint16_t screen_w, uint16_t screen_h) { +// _init(); +// m_screen_w = screen_w; +// m_screen_h = screen_h; +//} void UpdateData::_init() { m_data_type = TYPE_UNKNOWN; @@ -134,6 +141,14 @@ UpdateData::~UpdateData() { delete m_data_buf; } +void UpdateData::set_pointer(uint32_t ts, const TS_RECORD_RDP_POINTER* p) { + m_data_type = TYPE_POINTER; + m_time_ms = ts; + m_pointer = new TS_RECORD_RDP_POINTER; + memcpy(m_pointer, p, sizeof(TS_RECORD_RDP_POINTER)); +} + +#if 0 bool UpdateData::parse(const TS_RECORD_PKG& pkg, const QByteArray& data) { m_time_ms = pkg.time_ms; @@ -213,7 +228,7 @@ bool UpdateData::parse(const TS_RECORD_PKG& pkg, const QByteArray& data) { return false; } - +#endif void UpdateData::alloc_data(uint32_t len) { if(m_data_buf) diff --git a/client/tp-player/update_data.h b/client/tp-player/update_data.h index d6e1380..c84322d 100644 --- a/client/tp-player/update_data.h +++ b/client/tp-player/update_data.h @@ -37,29 +37,18 @@ class UpdateData : public QObject public: explicit UpdateData(); explicit UpdateData(int data_type); + explicit UpdateData(int data_type, uint32_t time_ms); explicit UpdateData(const TS_RECORD_HEADER& hdr); - explicit UpdateData(uint16_t screen_w, uint16_t screen_h); + //explicit UpdateData(uint16_t screen_w, uint16_t screen_h); virtual ~UpdateData(); - bool parse(const TS_RECORD_PKG& pkg, const QByteArray& data); + void set_pointer(uint32_t ts, const TS_RECORD_RDP_POINTER* p); + +// bool parse(const TS_RECORD_PKG& pkg, const QByteArray& data); TS_RECORD_HEADER* get_header() {return m_hdr;} TS_RECORD_RDP_POINTER* get_pointer() {return m_pointer;} -// bool get_image(QImage** img, int& x, int& y, int& w, int& h) { -// if(m_img == nullptr) -// return false; -// *img = m_img; -// x = m_img_x; -// y = m_img_y; -// w = m_img_w; -// h = m_img_h; -// return true; -// } - bool get_images(UpdateImages& uimgs) const { - if(m_images.size() == 0) - return false; - uimgs = m_images; - return true; - } + UpdateImages& get_images() {return m_images;} + const UpdateImages& get_images() const {return m_images;} uint32_t get_time() {return m_time_ms;} diff --git a/external/version.ini b/external/version.ini index 78dacf0..94bb489 100644 --- a/external/version.ini +++ b/external/version.ini @@ -5,4 +5,5 @@ mbedtls = 2.12.0 libssh = 0.9.0 jsoncpp = 0.10.6 mongoose = 6.12 - +; https://www.zlib.net/zlib1211.zip +zlilb = 1211,1.2.11 \ No newline at end of file From 5455c9ab8d660ffa96ca7da1b76b023afb91880e Mon Sep 17 00:00:00 2001 From: Apex Liu Date: Fri, 15 Nov 2019 09:35:00 +0800 Subject: [PATCH 39/44] .temp. --- client/tp-player/bar.cpp | 2 +- client/tp-player/mainwindow.cpp | 67 ++++-------- client/tp-player/mainwindow.h | 3 +- client/tp-player/thr_data.cpp | 16 +-- client/tp-player/thr_data.h | 10 +- client/tp-player/thr_play.cpp | 11 +- client/tp-player/update_data.cpp | 169 ------------------------------- client/tp-player/update_data.h | 12 --- 8 files changed, 26 insertions(+), 264 deletions(-) diff --git a/client/tp-player/bar.cpp b/client/tp-player/bar.cpp index 0c9bea5..5ff3f28 100644 --- a/client/tp-player/bar.cpp +++ b/client/tp-player/bar.cpp @@ -491,7 +491,7 @@ void Bar::onMousePress(int x, int y, Qt::MouseButton button) { } } -void Bar::onMouseRelease(int x, int y, Qt::MouseButton button) { +void Bar::onMouseRelease(int, int, Qt::MouseButton button) { // 我们只关心左键释放 if(button != Qt::LeftButton) return; diff --git a/client/tp-player/mainwindow.cpp b/client/tp-player/mainwindow.cpp index 8411b73..bc3b43c 100644 --- a/client/tp-player/mainwindow.cpp +++ b/client/tp-player/mainwindow.cpp @@ -33,6 +33,8 @@ MainWindow::MainWindow(QWidget *parent) : m_play_state = PLAY_STATE_UNKNOWN; m_thr_data = nullptr; + m_disable_draw = false; + ui->setupUi(this); ui->centralWidget->setMouseTracking(true); @@ -99,7 +101,7 @@ void MainWindow::set_resource(const QString &res) { void MainWindow::_do_first_run() { m_thr_data = new ThrData(this, m_res); connect(m_thr_data, SIGNAL(signal_update_data(UpdateData*)), this, SLOT(_do_update_data(UpdateData*))); - m_thr_data->start(); + m_thr_data->start(QThread::TimeCriticalPriority); m_thr_play = new ThrPlay(this); connect(m_thr_play, SIGNAL(signal_update_data(UpdateData*)), this, SLOT(_do_update_data(UpdateData*))); @@ -134,20 +136,6 @@ void MainWindow::paintEvent(QPaintEvent *e) painter.drawPixmap(m_pt.x-m_pt_normal.width()/2, m_pt.y-m_pt_normal.height()/2, m_pt_normal); } -// { -// QRect rc_draw = e->rect(); -// QRect rc(m_rc_message); -// //rc.moveTo(m_rc.left()+rc.left(), m_rc.top() + rc.top()); - -// int from_x = max(rc_draw.left(), rc.left()) - rc.left(); -// int from_y = max(rc_draw.top(), rc.top()) - rc.top(); -// int w = min(rc.right(), rc_draw.right()) - rc.left() - from_x + 1; -// int h = min(rc.bottom(), rc_draw.bottom()) - rc.top() - from_y + 1; -// int to_x = rc.left() + from_x; -// int to_y = rc.top() + from_y; -// painter.drawPixmap(to_x, to_y, m_img_message, from_x, from_y, w, h); -// } - // 绘制浮动控制窗 if(m_bar_fading) { painter.setOpacity(m_bar_opacity); @@ -161,15 +149,16 @@ void MainWindow::paintEvent(QPaintEvent *e) if(m_show_message) { QRect rc_draw = e->rect(); QRect rc(m_rc_message); - //rc.moveTo(m_rc.left()+rc.left(), m_rc.top() + rc.top()); - int from_x = max(rc_draw.left(), rc.left()) - rc.left(); - int from_y = max(rc_draw.top(), rc.top()) - rc.top(); - int w = min(rc.right(), rc_draw.right()) - rc.left() - from_x + 1; - int h = min(rc.bottom(), rc_draw.bottom()) - rc.top() - from_y + 1; - int to_x = rc.left() + from_x; - int to_y = rc.top() + from_y; - painter.drawPixmap(to_x, to_y, m_img_message, from_x, from_y, w, h); + if(e->rect().intersects(rc)) { + int from_x = max(rc_draw.left(), rc.left()) - rc.left(); + int from_y = max(rc_draw.top(), rc.top()) - rc.top(); + int w = min(rc.right(), rc_draw.right()) - rc.left() - from_x + 1; + int h = min(rc.bottom(), rc_draw.bottom()) - rc.top() - from_y + 1; + int to_x = rc.left() + from_x; + int to_y = rc.top() + from_y; + painter.drawPixmap(to_x, to_y, m_img_message, from_x, from_y, w, h); + } } } @@ -187,7 +176,6 @@ void MainWindow::resume(bool relocate, uint32_t ms) { m_thr_play->resume(relocate, ms); } else if(m_play_state == PLAY_STATE_STOP) { -// _start_play_thread(); m_thr_data->restart(ms); m_thr_play->resume(true, ms); } @@ -218,7 +206,7 @@ void MainWindow::_do_update_data(UpdateData* dat) { if(uimgs.size() == 0) return; - if(uimgs.size() > 1) { + if(uimgs.size() > 1 && !m_disable_draw) { // 禁止界面更新 setUpdatesEnabled(false); } @@ -231,7 +219,7 @@ void MainWindow::_do_update_data(UpdateData* dat) { } - if(uimgs.size() > 1) { + if(uimgs.size() > 1 && !m_disable_draw) { // 允许界面更新 setUpdatesEnabled(true); } @@ -246,12 +234,14 @@ void MainWindow::_do_update_data(UpdateData* dat) { else if(dat->data_type() == TYPE_DISABLE_DRAW) { // 禁止界面更新 + m_disable_draw = true; setUpdatesEnabled(false); return; } else if(dat->data_type() == TYPE_ENABLE_DRAW) { // 允许界面更新 + m_disable_draw = false; setUpdatesEnabled(true); return; } @@ -265,10 +255,6 @@ void MainWindow::_do_update_data(UpdateData* dat) { m_show_message = true; qDebug("1message, w=%d, h=%d", m_canvas.width(), m_canvas.height()); -// if(0 == m_canvas.width()) { -// QMessageBox::warning(nullptr, QGuiApplication::applicationDisplayName(), dat->message()); -// return; -// } QPainter pp(&m_canvas); QRect rcWin(0, 0, m_canvas.width(), m_canvas.height()); @@ -318,22 +304,13 @@ void MainWindow::_do_update_data(UpdateData* dat) { qDebug() << "resize (" << m_rec_hdr.basic.width << "," << m_rec_hdr.basic.height << ")"; - //if(m_canvas.width() != m_rec_hdr.basic.width && m_canvas.height() != m_rec_hdr.basic.height) { - m_canvas = QPixmap(m_rec_hdr.basic.width, m_rec_hdr.basic.height); + m_canvas = QPixmap(m_rec_hdr.basic.width, m_rec_hdr.basic.height); - //m_win_board_w = frameGeometry().width() - geometry().width(); - //m_win_board_h = frameGeometry().height() - geometry().height(); + QDesktopWidget *desktop = QApplication::desktop(); // =qApp->desktop();也可以 + qDebug("desktop w:%d,h:%d, this w:%d,h:%d", desktop->width(), desktop->height(), width(), height()); + move(10, (desktop->height() - m_rec_hdr.basic.height)/2); - QDesktopWidget *desktop = QApplication::desktop(); // =qApp->desktop();也可以 - qDebug("desktop w:%d,h:%d, this w:%d,h:%d", desktop->width(), desktop->height(), width(), height()); - //move((desktop->width() - this->width())/2, (desktop->height() - this->height())/2); - move(10, (desktop->height() - m_rec_hdr.basic.height)/2); - - //setFixedSize(m_rec_hdr.basic.width + m_win_board_w, m_rec_hdr.basic.height + m_win_board_h); - //resize(m_rec_hdr.basic.width + m_win_board_w, m_rec_hdr.basic.height + m_win_board_h); - //resize(m_rec_hdr.basic.width, m_rec_hdr.basic.height); - setFixedSize(m_rec_hdr.basic.width, m_rec_hdr.basic.height); - //} + setFixedSize(m_rec_hdr.basic.width, m_rec_hdr.basic.height); m_canvas.fill(QColor(38, 73, 111)); @@ -352,13 +329,11 @@ void MainWindow::_do_update_data(UpdateData* dat) { QString title; if (m_rec_hdr.basic.conn_port == 3389) { -// title = QString(LOCAL8BIT("[%1] %2@%3 [Teleport-RDP录像回放]").arg(m_rec_hdr.basic.acc_username, m_rec_hdr.basic.user_username, m_rec_hdr.basic.conn_ip)); title = QString(LOCAL8BIT("用户 %1 访问 %2 的 %3 账号").arg(m_rec_hdr.basic.user_username, m_rec_hdr.basic.conn_ip, m_rec_hdr.basic.acc_username)); } else { QString _port; _port.sprintf("%d", m_rec_hdr.basic.conn_port); - //title = QString(LOCAL8BIT("[%1] %2@%3:%4 [Teleport-RDP录像回放]").arg(m_rec_hdr.basic.acc_username, m_rec_hdr.basic.user_username, m_rec_hdr.basic.conn_ip, _port)); title = QString(LOCAL8BIT("用户 %1 访问 %2:%3 的 %4 账号").arg(m_rec_hdr.basic.user_username, m_rec_hdr.basic.conn_ip, _port, m_rec_hdr.basic.acc_username)); } diff --git a/client/tp-player/mainwindow.h b/client/tp-player/mainwindow.h index 511c864..174487f 100644 --- a/client/tp-player/mainwindow.h +++ b/client/tp-player/mainwindow.h @@ -46,8 +46,6 @@ private: void mousePressEvent(QMouseEvent *e); void mouseReleaseEvent(QMouseEvent *e); -// void _start_play_thread(); - private slots: void _do_first_run(); // 默认界面加载完成后,开始播放操作(可能会进行数据下载) void _do_update_data(UpdateData*); @@ -86,6 +84,7 @@ private: bool m_show_message; QPixmap m_img_message; QRect m_rc_message; + bool m_disable_draw; }; #endif // MAINWINDOW_H diff --git a/client/tp-player/thr_data.cpp b/client/tp-player/thr_data.cpp index 0d0e327..0bcc255 100644 --- a/client/tp-player/thr_data.cpp +++ b/client/tp-player/thr_data.cpp @@ -311,7 +311,6 @@ void ThrData::_run() { file_processed = 0; qDebug("Open file tp-rdp-%d.tpd, processed: %" PRId64 ", size: %" PRId64, m_file_idx+1, file_processed, file_size); } -// qDebug("B processed: %" PRId64 ", size: %" PRId64, file_processed, file_size); // 如果指定了起始偏移,则跳过这部分数据 if(m_offset > 0) { @@ -331,8 +330,6 @@ void ThrData::_run() { TS_RECORD_PKG pkg; read_len = fdata->read(reinterpret_cast(&pkg), sizeof(TS_RECORD_PKG)); - // if(read_len == 0) - // break; if(read_len != sizeof(TS_RECORD_PKG)) { qDebug("invaid tp-rdp-%d.tpd file, read_len=%" PRId64 " (1).", m_file_idx+1, read_len); _notify_error(QString("%1\ntp-rdp-%2.tpd").arg(LOCAL8BIT("错误的录像数据文件!"), str_fidx)); @@ -358,9 +355,7 @@ void ThrData::_run() { } file_processed += pkg.size; - //UpdateData* dat = new UpdateData(m_hdr.basic.width, m_hdr.basic.height); UpdateData* dat = _parse(pkg, pkg_data); - //if(!dat->parse(pkg, pkg_data)) { if(dat == nullptr) { qDebug("invaid tp-rdp-%d.tpd file (4).", m_file_idx+1); _notify_error(QString("%1\ntp-rdp-%2.tpd").arg(LOCAL8BIT("错误的录像数据文件!"), str_fidx)); @@ -398,7 +393,7 @@ void ThrData::_run() { } // 让线程调度器让播放线程有机会执行 - msleep(1); +// msleep(1); // 如果此文件已经处理完毕,则关闭文件,这样下次处理一个新的文件 if(file_processed >= file_size) { @@ -692,20 +687,11 @@ bool ThrData::_load_keyframe() { return true; } -void ThrData::_prepare() { - UpdateData* d = new UpdateData(TYPE_HEADER_INFO); - - m_locker.lock(); - m_data.enqueue(d); - m_locker.unlock(); -} - UpdateData* ThrData::get_data() { UpdateData* d = nullptr; m_locker.lock(); if(m_data.size() > 0) { -// qDebug("get_data(), left: %d", m_data.size()); d = m_data.dequeue(); } m_locker.unlock(); diff --git a/client/tp-player/thr_data.h b/client/tp-player/thr_data.h index 5f25ebf..92ad518 100644 --- a/client/tp-player/thr_data.h +++ b/client/tp-player/thr_data.h @@ -34,12 +34,6 @@ 这样,下次需要下载指定文件时,如果发现对应的临时文件存在,可以根据已下载字节数,继续下载。 */ -//typedef struct KEYFRAME_INFO { -// uint32_t time_ms; // 此关键帧的时间点 -// uint32_t file_index; // 此关键帧图像数据位于哪一个数据文件中 -// uint32_t offset; // 此关键帧图像数据在数据文件中的偏移 -//}KEYFRAME_INFO; - typedef std::vector KeyFrames; typedef std::vector CachedImages; @@ -70,7 +64,7 @@ private: bool _load_keyframe(); void _clear_data(); - void _prepare(); +// void _prepare(); UpdateData* _parse(const TS_RECORD_PKG& pkg, const QByteArray& data); @@ -107,8 +101,6 @@ private: uint32_t m_file_idx; uint32_t m_offset; -// bool m_xxx; -// int m_restart_kf_idx; CachedImages m_cache_imgs; }; diff --git a/client/tp-player/thr_play.cpp b/client/tp-player/thr_play.cpp index b76d017..424f7bd 100644 --- a/client/tp-player/thr_play.cpp +++ b/client/tp-player/thr_play.cpp @@ -36,19 +36,9 @@ void ThrPlay::stop() { if(!isRunning()) return; - // warning: never call stop() inside thread::run() loop. - m_need_stop = true; wait(); qDebug() << "play-thread end."; - -// if(m_thr_data) { -// m_thr_data->stop(); -// qDebug("delete thrData."); -// //m_thr_download->wait(); -// delete m_thr_data; -// m_thr_data = nullptr; -// } } void ThrPlay::_notify_message(const QString& msg) { @@ -144,6 +134,7 @@ void ThrPlay::run() { delete dat; dat = nullptr; UpdateData* _disable = new UpdateData(TYPE_DISABLE_DRAW); + msleep(500); emit signal_update_data(_disable); break; } diff --git a/client/tp-player/update_data.cpp b/client/tp-player/update_data.cpp index 3b931c6..b9cfdfd 100644 --- a/client/tp-player/update_data.cpp +++ b/client/tp-player/update_data.cpp @@ -1,81 +1,9 @@ #include "update_data.h" -//#include "rle.h" #include #include -//static QImage* _rdpimg2QImage(int w, int h, int bitsPerPixel, bool isCompressed, const uint8_t* dat, uint32_t len) { -// QImage* out; -// switch(bitsPerPixel) { -// case 15: -// if(isCompressed) { -// uint8_t* _dat = reinterpret_cast(calloc(1, w*h*2)); -// if(!bitmap_decompress1(_dat, w, h, dat, len)) { -// free(_dat); -// return nullptr; -// } -// out = new QImage(_dat, w, h, QImage::Format_RGB555); -// free(_dat); -// } -// else { -// out = new QImage(QImage(dat, w, h, QImage::Format_RGB555).transformed(QMatrix(1.0, 0.0, 0.0, -1.0, 0.0, 0.0))); -// } -// return out; - -// case 16: -// if(isCompressed) { -// uint8_t* _dat = reinterpret_cast(calloc(1, w*h*2)); -// if(!bitmap_decompress2(_dat, w, h, dat, len)) { -// free(_dat); -// qDebug() << "22------------------DECOMPRESS2 failed."; -// return nullptr; -// } - -// // TODO: 这里需要进一步优化,直接操作QImage的buffer。 -// out = new QImage(w, h, QImage::Format_RGB16); -// for(int y = 0; y < h; y++) { -// for(int x = 0; x < w; x++) { -// uint16 a = ((uint16*)_dat)[y * w + x]; -// uint8 r = ((a & 0xf800) >> 11) * 255 / 31; -// uint8 g = ((a & 0x07e0) >> 5) * 255 / 63; -// uint8 b = (a & 0x001f) * 255 / 31; -// out->setPixelColor(x, y, QColor(r,g,b)); -// } -// } -// free(_dat); -// return out; -// } -// else { -// out = new QImage(QImage(dat, w, h, QImage::Format_RGB16).transformed(QMatrix(1.0, 0.0, 0.0, -1.0, 0.0, 0.0))); -// } -// return out; - -// case 24: -// case 32: -// default: -// qDebug() << "--------NOT support UNKNOWN bitsPerPix" << bitsPerPixel; -// return nullptr; -// } -//} - -//static QImage* _raw2QImage(int w, int h, const uint8_t* dat, uint32_t len) { -// QImage* out; - -// // TODO: 这里需要进一步优化,直接操作QImage的buffer。 -// out = new QImage(w, h, QImage::Format_RGB16); -// for(int y = 0; y < h; y++) { -// for(int x = 0; x < w; x++) { -// uint16 a = ((uint16*)dat)[y * w + x]; -// uint8 r = ((a & 0xf800) >> 11) * 255 / 31; -// uint8 g = ((a & 0x07e0) >> 5) * 255 / 63; -// uint8 b = (a & 0x001f) * 255 / 31; -// out->setPixelColor(x, y, QColor(r,g,b)); -// } -// } -// return out; -//} - UpdateData::UpdateData() : QObject(nullptr) { _init(); @@ -102,25 +30,14 @@ UpdateData::UpdateData(const TS_RECORD_HEADER& hdr) : QObject(nullptr) memcpy(m_hdr, &hdr, sizeof(TS_RECORD_HEADER)); } -//UpdateData::UpdateData(uint16_t screen_w, uint16_t screen_h) { -// _init(); -// m_screen_w = screen_w; -// m_screen_h = screen_h; -//} - void UpdateData::_init() { m_data_type = TYPE_UNKNOWN; m_hdr = nullptr; m_pointer = nullptr; -// m_img = nullptr; -// m_img_info = nullptr; m_data_buf = nullptr; m_data_len = 0; m_time_ms = 0; - - m_screen_w = 0; - m_screen_h = 0; } UpdateData::~UpdateData() { @@ -128,10 +45,6 @@ UpdateData::~UpdateData() { delete m_hdr; if(m_pointer) delete m_pointer; -// if(m_img) -// delete m_img; -// if(m_img_info) -// delete m_img_info; for(int i = 0; i < m_images.size(); ++i) { delete m_images[i].img; } @@ -148,88 +61,6 @@ void UpdateData::set_pointer(uint32_t ts, const TS_RECORD_RDP_POINTER* p) { memcpy(m_pointer, p, sizeof(TS_RECORD_RDP_POINTER)); } -#if 0 -bool UpdateData::parse(const TS_RECORD_PKG& pkg, const QByteArray& data) { - m_time_ms = pkg.time_ms; - - if(pkg.type == TS_RECORD_TYPE_RDP_POINTER) { - m_data_type = TYPE_POINTER; - if(data.size() != sizeof(TS_RECORD_RDP_POINTER)) - return false; - m_pointer = new TS_RECORD_RDP_POINTER; - memcpy(m_pointer, data.data(), sizeof(TS_RECORD_RDP_POINTER)); - return true; - } - else if(pkg.type == TS_RECORD_TYPE_RDP_IMAGE) { - m_data_type = TYPE_IMAGE; - if(data.size() <= static_cast(sizeof(uint16_t) + sizeof(TS_RECORD_RDP_IMAGE_INFO))) - return false; - - const uint8_t* dat_ptr = reinterpret_cast(data.data()); - - uint16_t count = (reinterpret_cast(dat_ptr))[0]; - uint32_t offset = sizeof(uint16_t); - - for(uint16_t i = 0; i < count; ++i) { - - const TS_RECORD_RDP_IMAGE_INFO* info = reinterpret_cast(dat_ptr+offset); - offset += sizeof(TS_RECORD_RDP_IMAGE_INFO); - //const uint8_t* img_dat = reinterpret_cast(data.data() + sizeof(TS_RECORD_RDP_IMAGE_INFO)); - //uint32_t img_len = data.size() - sizeof(TS_RECORD_RDP_IMAGE_INFO); - const uint8_t* img_dat = dat_ptr + offset; - offset += info->dat_len; - - - QImage* img = _rdpimg2QImage(info->width, info->height, info->bitsPerPixel, (info->format == TS_RDP_IMG_BMP) ? true : false, img_dat, info->dat_len); - if(img == nullptr) - return false; - -// m_img = img; -// m_img_x = info->destLeft; -// m_img_y = info->destTop; -// m_img_w = info->destRight - info->destLeft + 1; -// m_img_h = info->destBottom - info->destTop + 1; - UPDATE_IMAGE uimg; - uimg.x = info->destLeft; - uimg.y = info->destTop; - uimg.w = info->destRight - info->destLeft + 1; - uimg.h = info->destBottom - info->destTop + 1; - uimg.img = img; - m_images.push_back(uimg); - } - - return true; - } - else if(pkg.type == TS_RECORD_TYPE_RDP_KEYFRAME) { - m_data_type = TYPE_IMAGE; -// const TS_RECORD_RDP_KEYFRAME_INFO* info = reinterpret_cast(data.data()); - const uint8_t* img_dat = reinterpret_cast(data.data() + sizeof(TS_RECORD_RDP_KEYFRAME_INFO)); - uint32_t img_len = data.size() - sizeof(TS_RECORD_RDP_KEYFRAME_INFO); - - QImage* img = _raw2QImage((int)m_screen_w, (int)m_screen_h, img_dat, img_len); - if(img == nullptr) - return false; - - UPDATE_IMAGE uimg; - uimg.x = 0; - uimg.y = 0; - uimg.w = m_screen_w; - uimg.h = m_screen_h; - uimg.img = img; - m_images.push_back(uimg); - -// m_img = img; -// m_img_x = 0; -// m_img_y = 0; -// m_img_w = m_screen_w; -// m_img_h = m_screen_h; - return true; - } - - return false; -} -#endif - void UpdateData::alloc_data(uint32_t len) { if(m_data_buf) delete m_data_buf; diff --git a/client/tp-player/update_data.h b/client/tp-player/update_data.h index c84322d..f142629 100644 --- a/client/tp-player/update_data.h +++ b/client/tp-player/update_data.h @@ -39,12 +39,10 @@ public: explicit UpdateData(int data_type); explicit UpdateData(int data_type, uint32_t time_ms); explicit UpdateData(const TS_RECORD_HEADER& hdr); - //explicit UpdateData(uint16_t screen_w, uint16_t screen_h); virtual ~UpdateData(); void set_pointer(uint32_t ts, const TS_RECORD_RDP_POINTER* p); -// bool parse(const TS_RECORD_PKG& pkg, const QByteArray& data); TS_RECORD_HEADER* get_header() {return m_hdr;} TS_RECORD_RDP_POINTER* get_pointer() {return m_pointer;} UpdateImages& get_images() {return m_images;} @@ -87,17 +85,7 @@ private: // for POINTER TS_RECORD_RDP_POINTER* m_pointer; // for IMAGE -// QImage* m_img; -// int m_img_x; -// int m_img_y; -// int m_img_w; -// int m_img_h; UpdateImages m_images; - -// TS_RECORD_RDP_IMAGE_INFO* m_img_info; - - uint16_t m_screen_w; - uint16_t m_screen_h; }; class UpdateDataHelper { From b8c6bbcbb197c7b494965dc9130628b4fcf73fe1 Mon Sep 17 00:00:00 2001 From: Apex Liu Date: Sat, 16 Nov 2019 00:48:23 +0800 Subject: [PATCH 40/44] add temp file/folder to .gitignore --- .gitignore | 8 ++++++++ client/tp-player/mainwindow.cpp | 5 ++++- client/tp-player/tp-player.pro | 4 ++-- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index a0677f0..badc925 100644 --- a/.gitignore +++ b/.gitignore @@ -105,3 +105,11 @@ profile /server/tp_core/testssh/Debug /server/tp_core/testssh/Release /client/build-tp-player-* +/client/tp-player/debug +/client/tp-player/release +/external/zlib +/client/tp-player/.qmake.stash +/client/tp-player/Makefile +/client/tp-player/Makefile.Debug +/client/tp-player/Makefile.Release +/client/tp-player/ui_mainwindow.h diff --git a/client/tp-player/mainwindow.cpp b/client/tp-player/mainwindow.cpp index bc3b43c..2e2e458 100644 --- a/client/tp-player/mainwindow.cpp +++ b/client/tp-player/mainwindow.cpp @@ -215,7 +215,9 @@ void MainWindow::_do_update_data(UpdateData* dat) { QPainter pp(&m_canvas); for(int i = 0; i < uimgs.size(); ++i) { pp.drawImage(uimgs[i].x, uimgs[i].y, *(uimgs[i].img), 0, 0, uimgs[i].w, uimgs[i].h, Qt::AutoColor); - update(uimgs[i].x, uimgs[i].y, uimgs[i].w, uimgs[i].h); + + if(!m_disable_draw) + update(uimgs[i].x, uimgs[i].y, uimgs[i].w, uimgs[i].h); } @@ -243,6 +245,7 @@ void MainWindow::_do_update_data(UpdateData* dat) { // 允许界面更新 m_disable_draw = false; setUpdatesEnabled(true); + update(); return; } diff --git a/client/tp-player/tp-player.pro b/client/tp-player/tp-player.pro index c72fee9..32e3a82 100644 --- a/client/tp-player/tp-player.pro +++ b/client/tp-player/tp-player.pro @@ -46,7 +46,7 @@ INCLUDEPATH += $$PWD/../../external/zlib/build DEPENDPATH += $$PWD/../../external/zlib DEPENDPATH += $$PWD/../../external/zlib/build -win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$PWD/../../external/zlib/build/release/libzlibstaticd.a +win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$PWD/../../external/zlib/build/release/libzlibstatic.a else:win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$PWD/../../external/zlib/build/debug/libzlibstaticd.a -else:win32:!win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$PWD/../../external/zlib/build/release/zlibstaticd.lib +else:win32:!win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$PWD/../../external/zlib/build/release/zlibstatic.lib else:win32:!win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$PWD/../../external/zlib/build/debug/zlibstaticd.lib From 50b6eddb3620d0d681c9914427a0c5f9ae7ad7d6 Mon Sep 17 00:00:00 2001 From: Apex Liu Date: Sat, 16 Nov 2019 01:27:37 +0800 Subject: [PATCH 41/44] =?UTF-8?q?=E8=B0=83=E6=95=B4Qt=E5=B7=A5=E7=A8=8B?= =?UTF-8?q?=E8=BE=93=E5=87=BA=E8=B7=AF=E5=BE=84=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 8 -------- client/tp-player/tp-player.pro | 19 +++++++++++++------ 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index badc925..9a35bbf 100644 --- a/.gitignore +++ b/.gitignore @@ -104,12 +104,4 @@ profile /server/tp_core/testssh/Debug /server/tp_core/testssh/Release -/client/build-tp-player-* -/client/tp-player/debug -/client/tp-player/release /external/zlib -/client/tp-player/.qmake.stash -/client/tp-player/Makefile -/client/tp-player/Makefile.Debug -/client/tp-player/Makefile.Release -/client/tp-player/ui_mainwindow.h diff --git a/client/tp-player/tp-player.pro b/client/tp-player/tp-player.pro index 32e3a82..6a3f16f 100644 --- a/client/tp-player/tp-player.pro +++ b/client/tp-player/tp-player.pro @@ -38,15 +38,22 @@ RC_FILE += \ FORMS += \ mainwindow.ui -win32:CONFIG(release, debug|release): LIBS += -L$$PWD/../../external/zlib/build/release/ -lzlibstatic -else:win32:CONFIG(debug, debug|release): LIBS += -L$$PWD/../../external/zlib/build/debug/ -lzlibstaticd + +win32:CONFIG(release, debug|release): { + LIBS += -L$$PWD/../../external/zlib/build/release/ -lzlibstatic + DESTDIR = $$PWD/../../out/client/x86/Release/tools/player +} +else:win32:CONFIG(debug, debug|release): { + LIBS += -L$$PWD/../../external/zlib/build/debug/ -lzlibstaticd + DESTDIR = $$PWD/../../out/client/x86/Debug/tools/player +} INCLUDEPATH += $$PWD/../../external/zlib INCLUDEPATH += $$PWD/../../external/zlib/build DEPENDPATH += $$PWD/../../external/zlib DEPENDPATH += $$PWD/../../external/zlib/build -win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$PWD/../../external/zlib/build/release/libzlibstatic.a -else:win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$PWD/../../external/zlib/build/debug/libzlibstaticd.a -else:win32:!win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$PWD/../../external/zlib/build/release/zlibstatic.lib -else:win32:!win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$PWD/../../external/zlib/build/debug/zlibstaticd.lib +#win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$PWD/../../external/zlib/build/release/libzlibstatic.a +#else:win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$PWD/../../external/zlib/build/debug/libzlibstaticd.a +#else:win32:!win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$PWD/../../external/zlib/build/release/zlibstatic.lib +#else:win32:!win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$PWD/../../external/zlib/build/debug/zlibstaticd.lib From eae1db3edc8f571235256c2728dc94cbf3e100c4 Mon Sep 17 00:00:00 2001 From: Apex Liu Date: Sun, 17 Nov 2019 03:32:05 +0800 Subject: [PATCH 42/44] =?UTF-8?q?=E5=85=A8=E9=9D=A2=E5=8D=87=E7=BA=A7?= =?UTF-8?q?=E7=AC=AC=E4=B8=89=E6=96=B9=E5=BA=93=EF=BC=9Aopenssl/libuv/mbed?= =?UTF-8?q?tls/jsoncpp/mongoose/zlib/libssh=EF=BC=9B=E5=8A=A0=E5=85=A5?= =?UTF-8?q?=E6=92=AD=E6=94=BE=E5=99=A8=E7=9A=84=E4=B8=80=E9=94=AE=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=EF=BC=88Qt=E5=91=BD=E4=BB=A4=E8=A1=8C=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=EF=BC=89=EF=BC=9B=E8=B0=83=E6=95=B4=E5=8A=A9=E6=89=8B?= =?UTF-8?q?=E5=AE=89=E8=A3=85=E5=8C=85=E6=9E=84=E5=BB=BA=E8=84=9A=E6=9C=AC?= =?UTF-8?q?=EF=BC=8C=E4=BD=BF=E4=B9=8B=E8=83=BD=E5=A4=9F=E5=8F=91=E5=B8=83?= =?UTF-8?q?=E5=9F=BA=E4=BA=8EQt=E7=9A=84=E8=BD=AF=E4=BB=B6=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + build/builder/build-assist.py | 56 +- build/builder/build-external.py | 230 +- build/builder/core/env.py | 9 + build/builder/core/utils.py | 15 + build/builder/core/ver.py | 4 +- client/tp-player/main.cpp | 27 +- client/tp-player/tp-player.pro | 11 +- .../tp_assist_macos/src/TP-Assist-Info.plist | 4 +- client/tp_assist_macos/src/csrc/ts_ver.h | 2 +- client/tp_assist_win/stdafx.cpp | 15 +- client/tp_assist_win/tp_assist.rc | Bin 8822 -> 8822 bytes client/tp_assist_win/tp_assist.vs2017.vcxproj | 10 +- client/tp_assist_win/ts_cfg.cpp | 540 ++-- client/tp_assist_win/ts_env.cpp | 145 +- client/tp_assist_win/ts_http_rpc.cpp | 2398 ++++++++--------- client/tp_assist_win/ts_ver.h | 2 +- common/libex/include/ex/ex_str.h | 3 + config.ini.in | 2 + dist/client/windows/assist/installer.nsi | Bin 3052 -> 3052 bytes external/version.ini | 21 +- server/tp_core/core/tp_core.rc | Bin 5146 -> 5146 bytes server/tp_core/core/ts_ver.h | 2 +- .../teleport/static/js/audit/record-list.js | 83 +- server/www/teleport/static/js/tp-assist.js | 66 +- server/www/teleport/webroot/app/app_ver.py | 4 +- .../teleport/webroot/app/controller/audit.py | 28 +- version.in | 8 +- 28 files changed, 1889 insertions(+), 1797 deletions(-) diff --git a/.gitignore b/.gitignore index 9a35bbf..8e3a1fd 100644 --- a/.gitignore +++ b/.gitignore @@ -105,3 +105,4 @@ profile /server/tp_core/testssh/Debug /server/tp_core/testssh/Release /external/zlib +/client/tools/qt-redist diff --git a/build/builder/build-assist.py b/build/builder/build-assist.py index da84c60..06cad85 100644 --- a/build/builder/build-assist.py +++ b/build/builder/build-assist.py @@ -13,21 +13,24 @@ class BuilderBase: def __init__(self): self.out_dir = '' - def build_exe(self): - pass + def build_assist(self): + cc.e("this is a pure-virtual function.") + + def build_player(self): + cc.e("this is a pure-virtual function.") def build_rdp(self): - pass + cc.e("this is a pure-virtual function.") def build_installer(self): - pass + cc.e("this is a pure-virtual function.") class BuilderWin(BuilderBase): def __init__(self): super().__init__() - def build_exe(self): + def build_assist(self): cc.i('build tp_assist...') sln_file = os.path.join(env.root_path, 'client', 'tp_assist_win', 'tp_assist.vs2017.sln') out_file = os.path.join(env.root_path, 'out', 'client', ctx.bits_path, ctx.target_path, 'tp_assist.exe') @@ -36,6 +39,15 @@ class BuilderWin(BuilderBase): utils.msvc_build(sln_file, 'tp_assist', ctx.target_path, ctx.bits_path, False) utils.ensure_file_exists(out_file) + def build_player(self): + cc.i('build tp-player...') + prj_path = os.path.join(env.root_path, 'client', 'tp-player') + out_file = os.path.join(env.root_path, 'out', 'client', ctx.bits_path, ctx.target_path, 'tp-player.exe') + if os.path.exists(out_file): + utils.remove(out_file) + utils.qt_build_win(prj_path, 'tp-player', ctx.bits_path, ctx.target_path) + utils.ensure_file_exists(out_file) + # def build_rdp(self): # cc.n('build tp_rdp...') # sln_file = os.path.join(ROOT_PATH, 'client', 'tp_rdp', 'tp_rdp.2015.sln') @@ -74,12 +86,13 @@ class BuilderWin(BuilderBase): utils.makedirs(tmp_cfg_path) utils.copy_file(os.path.join(env.root_path, 'out', 'client', ctx.bits_path, ctx.target_path), tmp_app_path, 'tp_assist.exe') - utils.copy_file(os.path.join(env.root_path, 'client', 'cfg'), tmp_cfg_path, ('tp-assist.windows.json', 'tp-assist.json')) + utils.copy_file(os.path.join(env.root_path, 'client', 'cfg'), tmp_cfg_path, ('tp-assist.windows.json', 'tp-assist.json')) utils.copy_file(os.path.join(env.root_path, 'client', 'cfg'), tmp_cfg_path, 'cacert.cer') utils.copy_file(os.path.join(env.root_path, 'client', 'cfg'), tmp_cfg_path, 'localhost.key') utils.copy_file(os.path.join(env.root_path, 'client', 'cfg'), tmp_cfg_path, 'localhost.pem') + # assist configuration web page utils.copy_ex(os.path.join(env.root_path, 'client', 'tp_assist_win'), tmp_app_path, 'site') utils.makedirs(os.path.join(tmp_app_path, 'tools', 'putty')) @@ -91,13 +104,35 @@ class BuilderWin(BuilderBase): utils.makedirs(os.path.join(tmp_app_path, 'tools', 'tprdp')) utils.copy_file(os.path.join(env.root_path, 'client', 'tools', 'tprdp'), os.path.join(tmp_app_path, 'tools', 'tprdp'), 'tprdp-client.exe') - utils.copy_file(os.path.join(env.root_path, 'client', 'tools', 'tprdp'), os.path.join(tmp_app_path, 'tools', 'tprdp'), 'tprdp-replay.exe') + # utils.copy_file(os.path.join(env.root_path, 'client', 'tools', 'tprdp'), os.path.join(tmp_app_path, 'tools', 'tprdp'), 'tprdp-replay.exe') utils.copy_file(os.path.join(env.root_path, 'client', 'tools', 'tprdp'), os.path.join(tmp_app_path, 'tools', 'tprdp'), 'libeay32.dll') utils.copy_file(os.path.join(env.root_path, 'client', 'tools', 'tprdp'), os.path.join(tmp_app_path, 'tools', 'tprdp'), 'ssleay32.dll') utils.copy_file(os.path.join(env.root_path, 'client', 'tools', 'tprdp'), os.path.join(tmp_app_path, 'tools', 'tprdp'), 'msvcr120.dll') utils.copy_file(os.path.join(env.root_path, 'client', 'tools'), os.path.join(tmp_app_path, 'tools'), 'securecrt-telnet.vbs') + # tp-player + utils.copy_file(os.path.join(env.root_path, 'out', 'client', ctx.bits_path, ctx.target_path), tmp_app_path, 'tp-player.exe') + + # qt-redist + qt_redist_path = os.path.join(env.root_path, 'client', 'tools', 'qt-redist') + utils.copy_file(qt_redist_path, tmp_app_path, 'Qt5Core.dll') + utils.copy_file(qt_redist_path, tmp_app_path, 'Qt5Gui.dll') + utils.copy_file(qt_redist_path, tmp_app_path, 'Qt5Network.dll') + utils.copy_file(qt_redist_path, tmp_app_path, 'Qt5Widgets.dll') + utils.copy_ex(os.path.join(qt_redist_path, 'platforms'), os.path.join(tmp_app_path, 'platforms')) + utils.copy_ex(os.path.join(qt_redist_path, 'styles'), os.path.join(tmp_app_path, 'styles')) + utils.copy_ex(os.path.join(qt_redist_path, 'translations'), os.path.join(tmp_app_path, 'translations')) + + # zlib + suffix = 'd' if ctx.target_path == 'debug' else '' + utils.copy_file(os.path.join(env.root_path, 'external', 'zlib', 'build', ctx.target_path), tmp_app_path, 'zlib{}.dll'.format(suffix)) + + # openssl + utils.copy_file(os.path.join(env.root_path, 'external', 'openssl', 'bin'), tmp_app_path, 'libcrypto-1_1.dll') + utils.copy_file(os.path.join(env.root_path, 'external', 'openssl', 'bin'), tmp_app_path, 'libssl-1_1.dll') + + # final build utils.nsis_build(os.path.join(env.root_path, 'dist', 'client', 'windows', 'assist', 'installer.nsi')) @@ -105,7 +140,7 @@ class BuilderMacOS(BuilderBase): def __init__(self): super().__init__() - def build_exe(self): + def build_assist(self): cc.i('build tp_assist...') configuration = ctx.target_path.capitalize() @@ -169,7 +204,7 @@ class BuilderLinux(BuilderBase): def __init__(self): super().__init__() - def build_exe(self): + def build_assist(self): cc.e('not support linux.') # def build_rdp(self): @@ -215,7 +250,8 @@ def main(): builder = gen_builder(ctx.host_os) if 'exe' in argv: - builder.build_exe() + builder.build_assist() + builder.build_player() # elif 'rdp' in argv: # builder.build_rdp() elif 'installer' in argv: diff --git a/build/builder/build-external.py b/build/builder/build-external.py index e3f9c7a..491e4b0 100644 --- a/build/builder/build-external.py +++ b/build/builder/build-external.py @@ -27,19 +27,25 @@ class BuilderBase: def build_jsoncpp(self): file_name = 'jsoncpp-{}.zip'.format(env.ver_jsoncpp) - if not utils.download_file('jsoncpp source tarball', 'https://github.com/open-source-parsers/jsoncpp/archive/{}.zip'.format(env.ver_jsoncpp), PATH_DOWNLOAD, file_name): - return + # if not utils.download_file('jsoncpp source tarball', 'https://github.com/open-source-parsers/jsoncpp/archive/{}.zip'.format(env.ver_jsoncpp), PATH_DOWNLOAD, file_name): + # return self._build_jsoncpp(file_name) + def _download_jsoncpp(self, file_name): + return utils.download_file('jsoncpp source tarball', 'https://github.com/open-source-parsers/jsoncpp/archive/{}.zip'.format(env.ver_jsoncpp), PATH_DOWNLOAD, file_name) + def _build_jsoncpp(self, file_name): cc.e("this is a pure-virtual function.") def build_mongoose(self): file_name = 'mongoose-{}.zip'.format(env.ver_mongoose) - if not utils.download_file('mongoose source tarball', 'https://github.com/cesanta/mongoose/archive/{}.zip'.format(env.ver_mongoose), PATH_DOWNLOAD, file_name): - return + # if not utils.download_file('mongoose source tarball', 'https://github.com/cesanta/mongoose/archive/{}.zip'.format(env.ver_mongoose), PATH_DOWNLOAD, file_name): + # return self._build_mongoose(file_name) + def _download_mongoose(self, file_name): + return utils.download_file('mongoose source tarball', 'https://github.com/cesanta/mongoose/archive/{}.zip'.format(env.ver_mongoose), PATH_DOWNLOAD, file_name) + def _build_mongoose(self, file_name): cc.e("this is a pure-virtual function.") @@ -47,38 +53,64 @@ class BuilderBase: file_name = 'openssl-{}.zip'.format(env.ver_ossl) self._build_openssl(file_name) - def _build_openssl(self, file_name): + def _download_openssl(self, file_name): _alt_ver = '_'.join(env.ver_ossl.split('.')) - if not utils.download_file('openssl source tarball', 'https://github.com/openssl/openssl/archive/OpenSSL_{}.zip'.format(_alt_ver), PATH_DOWNLOAD, file_name): - cc.e("can not download openssl source tarball.") - return False - else: - return True + return utils.download_file('openssl source tarball', 'https://github.com/openssl/openssl/archive/OpenSSL_{}.zip'.format(_alt_ver), PATH_DOWNLOAD, file_name) + + def _build_openssl(self, file_name): + cc.e("this is a pure-virtual function.") + # _alt_ver = '_'.join(env.ver_ossl.split('.')) + # if not utils.download_file('openssl source tarball', 'https://github.com/openssl/openssl/archive/OpenSSL_{}.zip'.format(_alt_ver), PATH_DOWNLOAD, file_name): + # cc.e("can not download openssl source tarball.") + # return False + # else: + # return True def build_libuv(self): file_name = 'libuv-{}.zip'.format(env.ver_libuv) - if not utils.download_file('libuv source tarball', 'https://github.com/libuv/libuv/archive/v{}.zip'.format(env.ver_libuv), PATH_DOWNLOAD, file_name): - return + # if not utils.download_file('libuv source tarball', 'https://github.com/libuv/libuv/archive/v{}.zip'.format(env.ver_libuv), PATH_DOWNLOAD, file_name): + # return self._build_libuv(file_name) + def _download_libuv(self, file_name): + return utils.download_file('libuv source tarball', 'https://github.com/libuv/libuv/archive/v{}.zip'.format(env.ver_libuv), PATH_DOWNLOAD, file_name) + def _build_libuv(self, file_name): cc.e("this is a pure-virtual function.") def build_mbedtls(self): file_name = 'mbedtls-mbedtls-{}.zip'.format(env.ver_mbedtls) - if not utils.download_file('mbedtls source tarball', 'https://github.com/ARMmbed/mbedtls/archive/mbedtls-{}.zip'.format(env.ver_mbedtls), PATH_DOWNLOAD, file_name): - return + # if not utils.download_file('mbedtls source tarball', 'https://github.com/ARMmbed/mbedtls/archive/mbedtls-{}.zip'.format(env.ver_mbedtls), PATH_DOWNLOAD, file_name): + # return self._build_mbedtls(file_name) + def _download_mbedtls(self, file_name): + return utils.download_file('mbedtls source tarball', 'https://github.com/ARMmbed/mbedtls/archive/mbedtls-{}.zip'.format(env.ver_mbedtls), PATH_DOWNLOAD, file_name) + def _build_mbedtls(self, file_name): cc.e("this is a pure-virtual function.") + def build_zlib(self): + file_name = 'zlilb{}.zip'.format(env.ver_zlib_number) + # if not utils.download_file('mbedtls source tarball', 'https://www.zlib.net/zlib{}.zip'.format(env.ver_zlib_number), PATH_DOWNLOAD, file_name): + # return + self._build_zlib(file_name) + + def _download_zlib(self, file_name): + return utils.download_file('mbedtls source tarball', 'https://www.zlib.net/zlib{}.zip'.format(env.ver_zlib_number), PATH_DOWNLOAD, file_name) + + def _build_zlib(self, file_name): + cc.e("this is a pure-virtual function.") + def build_libssh(self): file_name = 'libssh-{}.zip'.format(env.ver_libssh) - if not utils.download_file('libssh source tarball', 'https://git.libssh.org/projects/libssh.git/snapshot/libssh-{}.zip'.format(env.ver_libssh), PATH_DOWNLOAD, file_name): - return + # if not utils.download_file('libssh source tarball', 'https://git.libssh.org/projects/libssh.git/snapshot/libssh-{}.zip'.format(env.ver_libssh), PATH_DOWNLOAD, file_name): + # return self._build_libssh(file_name) + def _download_libssh(self, file_name): + return utils.download_file('libssh source tarball', 'https://git.libssh.org/projects/libssh.git/snapshot/libssh-{}.zip'.format(env.ver_libssh), PATH_DOWNLOAD, file_name) + def _build_libssh(self, file_name): cc.e("this is a pure-virtual function.") @@ -103,9 +135,10 @@ class BuilderWin(BuilderBase): self.MBEDTLS_PATH_SRC = os.path.join(PATH_EXTERNAL, 'mbedtls') self.LIBUV_PATH_SRC = os.path.join(PATH_EXTERNAL, 'libuv') self.LIBSSH_PATH_SRC = os.path.join(PATH_EXTERNAL, 'libssh') + self.ZLIB_PATH_SRC = os.path.join(PATH_EXTERNAL, 'zlib') def _prepare_python(self): - cc.n('prepare python header files ...', end='') + cc.n('prepare python header files ... ', end='') if os.path.exists(os.path.join(PATH_EXTERNAL, 'python', 'include', 'Python.h')): cc.w('already exists, skip.') @@ -125,53 +158,85 @@ class BuilderWin(BuilderBase): utils.copy_ex(_header_path, os.path.join(PATH_EXTERNAL, 'python', 'include')) def _build_openssl(self, file_name): - cc.n('build openssl static library from source code... ') - - if not super()._build_openssl(file_name): - return - - _chk_output = [ - os.path.join(self.OPENSSL_PATH_SRC, 'out32', 'libeay32.lib'), - os.path.join(self.OPENSSL_PATH_SRC, 'out32', 'ssleay32.lib'), - os.path.join(self.OPENSSL_PATH_SRC, 'inc32', 'openssl', 'opensslconf.h'), - ] - - need_build = False - for f in _chk_output: - if not os.path.exists(f): - need_build = True - break - - if not need_build: - cc.n('build openssl static library from source code... ', end='') + cc.n('prepare OpenSSL pre-built package ... ', end='') + if os.path.exists(self.OPENSSL_PATH_SRC): cc.w('already exists, skip.') return cc.v('') - cc.n('prepare openssl source code...') _alt_ver = '_'.join(env.ver_ossl.split('.')) - if not os.path.exists(self.OPENSSL_PATH_SRC): - utils.unzip(os.path.join(PATH_DOWNLOAD, file_name), PATH_EXTERNAL) - os.rename(os.path.join(PATH_EXTERNAL, 'openssl-OpenSSL_{}'.format(_alt_ver)), self.OPENSSL_PATH_SRC) - if not os.path.exists(self.OPENSSL_PATH_SRC): - raise RuntimeError('can not prepare openssl source code.') - else: - cc.w('already exists, skip.') - os.chdir(self.OPENSSL_PATH_SRC) - os.system('""{}" Configure VC-WIN32"'.format(env.perl)) - os.system(r'ms\do_nasm') - # for vs2015 - # utils.sys_exec(r'"{}\VC\bin\vcvars32.bat" && nmake -f ms\nt.mak'.format(env.visual_studio_path), direct_output=True) - # for vs2017 community - utils.sys_exec(r'"{}VC\Auxiliary\Build\vcvars32.bat" && nmake -f ms\nt.mak'.format(env.visual_studio_path), direct_output=True) + file_name = 'Win32OpenSSL-{}.msi'.format(_alt_ver) + installer = os.path.join(PATH_DOWNLOAD, file_name) - for f in _chk_output: - if not os.path.exists(f): - raise RuntimeError('build openssl static library from source code failed.') + if not os.path.exists(installer): + if not utils.download_file('openssl installer', 'http://slproweb.com/download/{}'.format(filename), PATH_DOWNLOAD, file_name): + cc.e('can not download pre-built installer of OpenSSL.') + return + + utils.ensure_file_exists(installer) + + cc.w('On Windows, we use pre-built package of OpenSSL.') + cc.w('The installer have been downloaded at "{}".'.format(installer)) + cc.w('please install OpenSSL into "{}".'.format(self.OPENSSL_PATH_SRC)) + cc.w('\nOnce the OpenSSL installed, press Enter to continue or Q to quit...', end='') + try: + x = env.input() + except EOFError: + x = 'q' + if x == 'q': + return + + + # cc.n('build openssl static library from source code... ') + + # if not super()._build_openssl(file_name): + # return + + # _chk_output = [ + # os.path.join(self.OPENSSL_PATH_SRC, 'out32', 'libeay32.lib'), + # os.path.join(self.OPENSSL_PATH_SRC, 'out32', 'ssleay32.lib'), + # os.path.join(self.OPENSSL_PATH_SRC, 'inc32', 'openssl', 'opensslconf.h'), + # ] + + # need_build = False + # for f in _chk_output: + # if not os.path.exists(f): + # need_build = True + # break + + # if not need_build: + # cc.n('build openssl static library from source code... ', end='') + # cc.w('already exists, skip.') + # return + # cc.v('') + + # cc.n('prepare openssl source code...') + # _alt_ver = '_'.join(env.ver_ossl.split('.')) + # if not os.path.exists(self.OPENSSL_PATH_SRC): + # utils.unzip(os.path.join(PATH_DOWNLOAD, file_name), PATH_EXTERNAL) + # os.rename(os.path.join(PATH_EXTERNAL, 'openssl-OpenSSL_{}'.format(_alt_ver)), self.OPENSSL_PATH_SRC) + # if not os.path.exists(self.OPENSSL_PATH_SRC): + # raise RuntimeError('can not prepare openssl source code.') + # else: + # cc.w('already exists, skip.') + + # os.chdir(self.OPENSSL_PATH_SRC) + # os.system('""{}" Configure VC-WIN32"'.format(env.perl)) + # os.system(r'ms\do_nasm') + # # for vs2015 + # # utils.sys_exec(r'"{}\VC\bin\vcvars32.bat" && nmake -f ms\nt.mak'.format(env.visual_studio_path), direct_output=True) + # # for vs2017 community + # utils.sys_exec(r'"{}VC\Auxiliary\Build\vcvars32.bat" && nmake -f ms\nt.mak'.format(env.visual_studio_path), direct_output=True) + + # for f in _chk_output: + # if not os.path.exists(f): + # raise RuntimeError('build openssl static library from source code failed.') def _build_libssh(self, file_name): - cc.n('build libssh static library from source code... ', end='') + if not self._download_libssh(file_name): + return + cc.n('build libssh library from source code... ', end='') if not os.path.exists(self.LIBSSH_PATH_SRC): cc.v('') @@ -210,7 +275,7 @@ class BuilderWin(BuilderBase): cc.i('build libssh...') sln_file = os.path.join(self.LIBSSH_PATH_SRC, 'build', 'libssh.sln') - utils.msvc_build(sln_file, 'ssh_shared', ctx.target_path, 'win32', False) + utils.msvc_build(sln_file, 'ssh', ctx.target_path, 'win32', False) utils.ensure_file_exists(os.path.join(self.LIBSSH_PATH_SRC, 'build', 'src', ctx.target_path, 'ssh.lib')) utils.ensure_file_exists(os.path.join(self.LIBSSH_PATH_SRC, 'build', 'src', ctx.target_path, 'ssh.dll')) utils.copy_file(os.path.join(self.LIBSSH_PATH_SRC, 'build', 'src', ctx.target_path), os.path.join(self.LIBSSH_PATH_SRC, 'lib', ctx.target_path), 'ssh.lib') @@ -218,7 +283,53 @@ class BuilderWin(BuilderBase): utils.ensure_file_exists(out_file_lib) utils.ensure_file_exists(out_file_dll) + def _build_zlib(self, file_name): + if not self._download_zlib(file_name): + return + cc.n('build zlib library from source code... ', end='') + + if not os.path.exists(self.ZLIB_PATH_SRC): + cc.v('') + utils.unzip(os.path.join(PATH_DOWNLOAD, file_name), PATH_EXTERNAL) + os.rename(os.path.join(PATH_EXTERNAL, 'zlib-{}'.format(env.ver_zlib)), self.ZLIB_PATH_SRC) + + if ctx.target_path == 'debug': + olib = 'zlibd.lib' + odll = 'zlibd.dll' + else: + olib = 'zlib.lib' + odll = 'zlib.dll' + out_file_lib = os.path.join(self.ZLIB_PATH_SRC, 'build', ctx.target_path, olib) + out_file_dll = os.path.join(self.ZLIB_PATH_SRC, 'build', ctx.target_path, odll) + + if os.path.exists(out_file_lib) and os.path.exists(out_file_dll): + cc.w('already exists, skip.') + return + cc.v('') + + cc.w('On Windows, when build zlib, need you use cmake-gui.exe to generate solution file') + cc.w('for Visual Studio 2017. Visit https://docs.tp4a.com for more details.') + cc.w('\nOnce the zlib.sln generated, press Enter to continue or Q to quit...', end='') + try: + x = env.input() + except EOFError: + x = 'q' + if x == 'q': + return + + cc.i('build zlib...') + sln_file = os.path.join(self.ZLIB_PATH_SRC, 'build', 'zlib.sln') + utils.msvc_build(sln_file, 'zlib', ctx.target_path, 'win32', False) + # utils.ensure_file_exists(os.path.join(self.ZLIB_PATH_SRC, 'build', ctx.target_path, 'zlib.lib')) + # utils.ensure_file_exists(os.path.join(self.ZLIB_PATH_SRC, 'build', ctx.target_path, 'zlib.dll')) + # utils.copy_file(os.path.join(self.ZLIB_PATH_SRC, 'build', ctx.target_path), os.path.join(self.ZLIB_PATH_SRC, 'lib', ctx.target_path), 'zlib.lib') + # utils.copy_file(os.path.join(self.ZLIB_PATH_SRC, 'build', ctx.target_path), os.path.join(self.ZLIB_PATH_SRC, 'lib', ctx.target_path), 'zlib.dll') + utils.ensure_file_exists(out_file_lib) + utils.ensure_file_exists(out_file_dll) + def _build_jsoncpp(self, file_name): + if not self._download_jsoncpp(file_name): + return cc.n('prepare jsoncpp source code... ', end='') if not os.path.exists(self.JSONCPP_PATH_SRC): cc.v('') @@ -228,6 +339,8 @@ class BuilderWin(BuilderBase): cc.w('already exists, skip.') def _build_mongoose(self, file_name): + if not self._download_mongoose(file_name): + return cc.n('prepare mongoose source code... ', end='') if not os.path.exists(self.MONGOOSE_PATH_SRC): cc.v('') @@ -237,6 +350,8 @@ class BuilderWin(BuilderBase): cc.w('already exists, skip.') def _build_mbedtls(self, file_name): + if not self._download_mbedtls(file_name): + return cc.n('prepare mbedtls source code... ', end='') if not os.path.exists(self.MBEDTLS_PATH_SRC): cc.v('') @@ -254,6 +369,8 @@ class BuilderWin(BuilderBase): # utils.copy_file(os.path.join(PATH_EXTERNAL, 'fix-external', 'mbedtls', 'library'), os.path.join(self.MBEDTLS_PATH_SRC, 'library'), 'rsa.c') def _build_libuv(self, file_name): + if not self._download_libuv(file_name): + return cc.n('prepare libuv source code... ', end='') if not os.path.exists(self.LIBUV_PATH_SRC): cc.v('') @@ -674,6 +791,7 @@ def main(): builder.build_openssl() builder.build_libuv() builder.build_mbedtls() + builder.build_zlib() builder.build_libssh() builder.fix_output() diff --git a/build/builder/core/env.py b/build/builder/core/env.py index 82a907e..4a89e73 100644 --- a/build/builder/core/env.py +++ b/build/builder/core/env.py @@ -148,6 +148,11 @@ class Env(object): if warn_miss_tool: cc.w(' - can not locate `nsis`, so I can not make installer.') + if 'qt' in _tmp: + self.qt = _tmp['qt'] + else: + self.qt = None + elif self.is_linux or self.is_macos: if 'cmake' in _tmp: self.cmake = _tmp['cmake'] @@ -178,6 +183,10 @@ class Env(object): self.ver_ossl = _v_openssl[0].strip() self.ver_ossl_number = _v_openssl[1].strip() + _v_zlib = _tmp['zlib'].split(',') + self.ver_zlib = _v_zlib[0].strip() + self.ver_zlib_number = _v_zlib[1].strip() + self.ver_libuv = _tmp['libuv'] self.ver_mbedtls = _tmp['mbedtls'] # self.ver_sqlite = _tmp['sqlite'] diff --git a/build/builder/core/utils.py b/build/builder/core/utils.py index c6aa97f..3aacf62 100644 --- a/build/builder/core/utils.py +++ b/build/builder/core/utils.py @@ -320,6 +320,21 @@ def msvc_build(sln_file, proj_name, target, platform, force_rebuild): raise RuntimeError('build MSVC project `{}` failed.'.format(proj_name)) +def qt_build_win(prj_path, prj_name, bit_path, target_path): + cc.n(env.visual_studio_path) + if env.qt is None: + raise RuntimeError('where is `qt`?') + + if env.is_win: + tmp_path = os.path.join(env.root_path, 'out', '_tmp_', prj_name, bit_path) + # C:\Windows\System32\cmd.exe /A /Q /K C:\Qt\Qt5.12.0\5.12.0\msvc2017\bin\qtenv2.bat + cmd = 'C:\\Windows\\System32\\cmd.exe /A /Q /C ""{}\qt-helper.bat" "{}\\bin\\qtenv2.bat" "{}VC\\Auxiliary\\Build\\vcvarsall.bat" {} "{}" "{}" {}"'.format(env.build_path, env.qt, env.visual_studio_path, bit_path, tmp_path, prj_path, target_path) + ret, _ = sys_exec(cmd, direct_output=True) + if ret != 0: + raise RuntimeError('build XCode project `{}` failed.'.format(proj_name)) + + + def xcode_build(proj_file, proj_name, target, force_rebuild): if force_rebuild: cmd = 'xcodebuild -project "{}" -target {} -configuration {} clean'.format(proj_file, proj_name, target) diff --git a/build/builder/core/ver.py b/build/builder/core/ver.py index 31996dd..30abc5a 100644 --- a/build/builder/core/ver.py +++ b/build/builder/core/ver.py @@ -1,3 +1,3 @@ # -*- coding: utf8 -*- -VER_TP_SERVER = "3.3.1" -VER_TP_ASSIST = "3.3.1" +VER_TP_SERVER = "3.5.1" +VER_TP_ASSIST = "3.5.1" diff --git a/client/tp-player/main.cpp b/client/tp-player/main.cpp index b4a133e..82e2531 100644 --- a/client/tp-player/main.cpp +++ b/client/tp-player/main.cpp @@ -14,14 +14,10 @@ // tp-player.exe path/contains/tp-rdp.tpr 包含 .tpr 文件的路径 // // ## 从TP服务器上下载 -// (废弃) tp-player.exe "http://127.0.0.1:7190" 1234 "tp_1491560510_ca67fceb75a78c9d" "000000256-admin-administrator-218.244.140.14-20171209-020047" -// (废弃) TP服务器地址 记录编号 session-id(仅授权用户可下载) 合成的名称,用于本地生成路径来存放下载的文件 -// -// ## 从TP服务器上下载 // tp-player.exe http://teleport.domain.com:7190/{sub/path/}tp_1491560510_ca67fceb75a78c9d/1234 (注意,并不直接访问此URI,实际上其并不存在) -// TP服务器地址(可能包含子路径哦,例如上例中的{sub/path}部分)/session-id(用于判断当前授权用户)/录像会话编号 +// TP服务器地址(可能包含子路径,例如上例中的{sub/path}部分)/session-id(用于判断当前授权用户)/录像会话编号 // 按 “/” 进行分割后,去掉最后两个项,剩下部分是TP服务器的WEB地址,用于合成后续的文件下载URL。 -// 根据下载的.tpr文件内容,本地合成类似于 "000000256-admin-administrator-218.244.140.14-20171209-020047" 的路径来存放下载的文件 +// 根据下载的.tpr文件内容,本地合成类似于 "000000256-admin-administrator-123.45.77.88-20191109-020047" 的路径来存放下载的文件 // 特别注意,如果账号是 domain\user 这种形式,需要将 "\" 替换为下划线,否则此符号作为路径分隔符,会导致路径不存在而无法保存下载的文件。 // - 获取文件大小: http://127.0.0.1:7190/audit/get-file?act=size&type=rdp&rid=yyyyy&f=file-name // - 'act'为`size`表示获取文件大小(返回一个数字字符串,就是指定的文件大小) @@ -41,9 +37,9 @@ void show_usage(QCommandLineParser& parser) { + parser.helpText() + "\n\n" + "RESOURCE could be:\n" - + " teleport record file (.tpr).\n" - + " a directory contains .tpr file.\n" - + " an URL to download teleport record file." + + " - teleport record file (.tpr).\n" + + " - a directory contains .tpr file.\n" + + " - an URL to download teleport record file." + "
    "); } @@ -82,15 +78,6 @@ int main(int argc, char *argv[]) if(parser.isSet(opt_help)) { show_usage(parser); -// QMessageBox::warning(nullptr, QGuiApplication::applicationDisplayName(), -// "
    "
    -//                             + parser.helpText()
    -//                             + "\n\n"
    -//                             + "RESOURCE could be:\n"
    -//                             + "    teleport record file (.tpr).\n"
    -//                             + "    a directory contains .tpr file.\n"
    -//                             + "    an URL for download teleport record file."
    -//                             + "
    "); return 2; } @@ -104,10 +91,6 @@ int main(int argc, char *argv[]) qDebug() << resource; -// QTextCodec::setCodecForTr(QTextCodec::codecForName("GB2312")); -// QTextCodec::setCodecForLocale(QTextCodec::codecForName("GBK")); -// QTextCodec::setCodecForCStrings(QTextCodec::codecForName("GB2312")); - MainWindow w; w.set_resource(resource); w.show(); diff --git a/client/tp-player/tp-player.pro b/client/tp-player/tp-player.pro index 6a3f16f..3b95bae 100644 --- a/client/tp-player/tp-player.pro +++ b/client/tp-player/tp-player.pro @@ -3,8 +3,6 @@ TARGET = tp-player QT += core gui widgets network -#DEFINES += QT_NO_DEBUG_OUTPUT - HEADERS += \ mainwindow.h \ bar.h \ @@ -40,12 +38,13 @@ FORMS += \ win32:CONFIG(release, debug|release): { - LIBS += -L$$PWD/../../external/zlib/build/release/ -lzlibstatic - DESTDIR = $$PWD/../../out/client/x86/Release/tools/player + DEFINES += QT_NO_DEBUG_OUTPUT + LIBS += -L$$PWD/../../external/zlib/build/release/ -lzlib + DESTDIR = $$PWD/../../out/client/x86/Release } else:win32:CONFIG(debug, debug|release): { - LIBS += -L$$PWD/../../external/zlib/build/debug/ -lzlibstaticd - DESTDIR = $$PWD/../../out/client/x86/Debug/tools/player + LIBS += -L$$PWD/../../external/zlib/build/debug/ -lzlibd + DESTDIR = $$PWD/../../out/client/x86/Debug } INCLUDEPATH += $$PWD/../../external/zlib diff --git a/client/tp_assist_macos/src/TP-Assist-Info.plist b/client/tp_assist_macos/src/TP-Assist-Info.plist index 3231d75..4b86b89 100644 --- a/client/tp_assist_macos/src/TP-Assist-Info.plist +++ b/client/tp_assist_macos/src/TP-Assist-Info.plist @@ -17,11 +17,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 3.3.1 + 3.5.1 CFBundleSignature ???? CFBundleVersion - 3.3.1 + 3.5.1 LSApplicationCategoryType public.app-category.productivity LSMinimumSystemVersion diff --git a/client/tp_assist_macos/src/csrc/ts_ver.h b/client/tp_assist_macos/src/csrc/ts_ver.h index 6aa10dc..9c55de2 100644 --- a/client/tp_assist_macos/src/csrc/ts_ver.h +++ b/client/tp_assist_macos/src/csrc/ts_ver.h @@ -1,6 +1,6 @@ #ifndef __TS_ASSIST_VER_H__ #define __TS_ASSIST_VER_H__ -#define TP_ASSIST_VER L"3.3.1" +#define TP_ASSIST_VER L"3.5.1" #endif // __TS_ASSIST_VER_H__ diff --git a/client/tp_assist_win/stdafx.cpp b/client/tp_assist_win/stdafx.cpp index fd4f341..6c5f035 100644 --- a/client/tp_assist_win/stdafx.cpp +++ b/client/tp_assist_win/stdafx.cpp @@ -1 +1,14 @@ -#include "stdafx.h" +#include "stdafx.h" + +#include + +// #ifdef EX_DEBUG +// // # pragma comment(lib, "libssl32MTd.lib") +// // # pragma comment(lib, "libcrypto32MTd.lib") +// #else +// # pragma comment(lib, "libssl32MT.lib") +// # pragma comment(lib, "libcrypto32MT.lib") +// #endif + +# pragma comment(lib, "libssl.lib") +# pragma comment(lib, "libcrypto.lib") diff --git a/client/tp_assist_win/tp_assist.rc b/client/tp_assist_win/tp_assist.rc index 8dc73d08248949fee5e79bb9480c45838789933c..b1c88326775c6c8b3c884662489de074beaeaeef 100644 GIT binary patch delta 38 rcmez7^37!fmn5UDisabled WIN32;MG_ENABLE_SSL;_DEBUG;_WINDOWS;_WINSOCK_DEPRECATED_NO_WARNINGS;MG_ENABLE_THREADS;MG_DISABLE_HTTP_DIGEST_AUTH;MG_DISABLE_MQTT;MG_DISABLE_SSI;MG_DISABLE_FILESYSTEM;%(PreprocessorDefinitions) true - ..\..\common\teleport;..\..\common\libex\include;..\..\external\jsoncpp\include;..\..\external\openssl\inc32 + ..\..\common\teleport;..\..\common\libex\include;..\..\external\jsoncpp\include;..\..\external\openssl\include MultiThreadedDebug Windows true - ..\..\external\openssl\out32\ssleay32.lib;..\..\external\openssl\out32\libeay32.lib;%(AdditionalDependencies) + %(AdditionalDependencies) + ..\..\external\openssl\lib;%(AdditionalLibraryDirectories) @@ -79,7 +80,7 @@ true WIN32;MG_ENABLE_SSL;NDEBUG;_WINDOWS;_WINSOCK_DEPRECATED_NO_WARNINGS;MG_ENABLE_THREADS;MG_DISABLE_HTTP_DIGEST_AUTH;MG_DISABLE_MQTT;MG_DISABLE_SSI;MG_DISABLE_FILESYSTEM;%(PreprocessorDefinitions) true - ..\..\common\teleport;..\..\common\libex\include;..\..\external\jsoncpp\include;..\..\external\openssl\inc32 + ..\..\common\teleport;..\..\common\libex\include;..\..\external\jsoncpp\include;..\..\external\openssl\include MultiThreaded @@ -87,7 +88,8 @@ true true true - ..\..\external\openssl\out32\ssleay32.lib;..\..\external\openssl\out32\libeay32.lib;%(AdditionalDependencies) + %(AdditionalDependencies) + ..\..\external\openssl\lib;%(AdditionalLibraryDirectories) diff --git a/client/tp_assist_win/ts_cfg.cpp b/client/tp_assist_win/ts_cfg.cpp index 722a7b5..f9214f6 100644 --- a/client/tp_assist_win/ts_cfg.cpp +++ b/client/tp_assist_win/ts_cfg.cpp @@ -1,266 +1,274 @@ -#include "stdafx.h" -#include "ts_cfg.h" -#include "ts_env.h" - - -TsCfg g_cfg; - -TsCfg::TsCfg() -{} - -TsCfg::~TsCfg() -{} - -bool TsCfg::init(void) { - ex_astr file_content; - if (!ex_read_text_file(g_env.m_cfg_file, file_content)) { - EXLOGE("can not load config file.\n"); - return false; - } - - if (!_load(file_content)) - return false; - - return true; -} - -bool TsCfg::save(const ex_astr& new_value) -{ - if (!_load(new_value)) - return false; - - Json::StyledWriter jwriter; - ex_astr val = jwriter.write(m_root); - - if (!ex_write_text_file(g_env.m_cfg_file, val)) { - EXLOGE("can not save config file.\n"); - return false; - } - - return true; -} - -bool TsCfg::_load(const ex_astr& str_json) { - Json::Reader jreader; - - if (!jreader.parse(str_json.c_str(), m_root)) { - EXLOGE("can not parse new config data, not in json format? %s\n", jreader.getFormattedErrorMessages().c_str()); - return false; - } - - ex_astr sel_name; - size_t i = 0; - ex_astr tmp; - - //=================================== - // check ssh config - //=================================== - - if (!m_root["ssh"].isObject()) { - EXLOGE("invalid config, error 1.\n"); - return false; - } - - if (!m_root["ssh"]["selected"].isString()) { - EXLOGE("invalid config, error 2.\n"); - return false; - } - - sel_name = m_root["ssh"]["selected"].asCString(); - - if (!m_root["ssh"]["available"].isArray() || m_root["ssh"]["available"].size() == 0) { - EXLOGE("invalid config, error 3.\n"); - return false; - } - - for (i = 0; i < m_root["ssh"]["available"].size(); ++i) { - - if ( - !m_root["ssh"]["available"][i]["name"].isString() - || !m_root["ssh"]["available"][i]["app"].isString() - || !m_root["ssh"]["available"][i]["cmdline"].isString() - ) { - EXLOGE("invalid config, error 4.\n"); - return false; - } - - if (m_root["ssh"]["available"][i]["display"].isNull()) { - m_root["ssh"]["available"][i]["display"] = m_root["ssh"]["available"][i]["name"]; - } - - if (m_root["ssh"]["available"][i]["name"].asCString() != sel_name) - continue; - - tmp = m_root["ssh"]["available"][i]["app"].asCString(); - ex_astr2wstr(tmp, ssh_app, EX_CODEPAGE_UTF8); - tmp = m_root["ssh"]["available"][i]["cmdline"].asCString(); - ex_astr2wstr(tmp, ssh_cmdline, EX_CODEPAGE_UTF8); - - break; - } - - if (ssh_app.length() == 0 || ssh_cmdline.length() == 0) { - EXLOGE("invalid config, error 6.\n"); - return false; - } - - //=================================== - // check sftp config - //=================================== - - if (!m_root["scp"].isObject()) { - EXLOGE("invalid config, error 1.\n"); - return false; - } - - if (!m_root["scp"]["selected"].isString()) { - EXLOGE("invalid config, error 2.\n"); - return false; - } - - sel_name = m_root["scp"]["selected"].asCString(); - - if (!m_root["scp"]["available"].isArray() || m_root["scp"]["available"].size() == 0) { - EXLOGE("invalid config, error 3.\n"); - return false; - } - - for (i = 0; i < m_root["scp"]["available"].size(); ++i) { - - if ( - !m_root["scp"]["available"][i]["name"].isString() - || !m_root["scp"]["available"][i]["app"].isString() - || !m_root["scp"]["available"][i]["cmdline"].isString() - ) { - EXLOGE("invalid config, error 4.\n"); - return false; - } - - if (m_root["scp"]["available"][i]["display"].isNull()) { - m_root["scp"]["available"][i]["display"] = m_root["scp"]["available"][i]["name"]; - } - - if (m_root["scp"]["available"][i]["name"].asCString() != sel_name) - continue; - - tmp = m_root["scp"]["available"][i]["app"].asCString(); - ex_astr2wstr(tmp, scp_app, EX_CODEPAGE_UTF8); - tmp = m_root["scp"]["available"][i]["cmdline"].asCString(); - ex_astr2wstr(tmp, scp_cmdline, EX_CODEPAGE_UTF8); - - break; - } - - if (scp_app.length() == 0 || scp_cmdline.length() == 0) { - EXLOGE("invalid config, error 6.\n"); - return false; - } - - //=================================== - // check telnet config - //=================================== - - if (!m_root["telnet"].isObject()) { - EXLOGE("invalid config, error 1.\n"); - return false; - } - - if (!m_root["telnet"]["selected"].isString()) { - EXLOGE("invalid config, error 2.\n"); - return false; - } - - sel_name = m_root["telnet"]["selected"].asCString(); - - if (!m_root["telnet"]["available"].isArray() || m_root["telnet"]["available"].size() == 0) { - EXLOGE("invalid config, error 3.\n"); - return false; - } - - for (i = 0; i < m_root["telnet"]["available"].size(); ++i) { - - if ( - !m_root["telnet"]["available"][i]["name"].isString() - || !m_root["telnet"]["available"][i]["app"].isString() - || !m_root["telnet"]["available"][i]["cmdline"].isString() - ) { - EXLOGE("invalid config, error 4.\n"); - return false; - } - - if (m_root["telnet"]["available"][i]["display"].isNull()) { - m_root["telnet"]["available"][i]["display"] = m_root["telnet"]["available"][i]["name"]; - } - - if (m_root["telnet"]["available"][i]["name"].asCString() != sel_name) - continue; - - tmp = m_root["telnet"]["available"][i]["app"].asCString(); - ex_astr2wstr(tmp, telnet_app, EX_CODEPAGE_UTF8); - tmp = m_root["telnet"]["available"][i]["cmdline"].asCString(); - ex_astr2wstr(tmp, telnet_cmdline, EX_CODEPAGE_UTF8); - - break; - } - - if (telnet_app.length() == 0 || telnet_cmdline.length() == 0) { - EXLOGE("invalid config, error 6.\n"); - return false; - } - - //=================================== - // check rdp config - //=================================== - - if (!m_root["rdp"].isObject()) { - EXLOGE("invalid config, error 1.\n"); - return false; - } - - if (!m_root["rdp"]["selected"].isString()) { - EXLOGE("invalid config, error 2.\n"); - return false; - } - - sel_name = m_root["rdp"]["selected"].asCString(); - - if (!m_root["rdp"]["available"].isArray() || m_root["rdp"]["available"].size() == 0) { - EXLOGE("invalid config, error 3.\n"); - return false; - } - - for (i = 0; i < m_root["rdp"]["available"].size(); ++i) { - - if ( - !m_root["rdp"]["available"][i]["name"].isString() - || !m_root["rdp"]["available"][i]["app"].isString() - || !m_root["rdp"]["available"][i]["cmdline"].isString() - ) { - EXLOGE("invalid config, error 4.\n"); - return false; - } - - if (m_root["rdp"]["available"][i]["display"].isNull()) { - m_root["rdp"]["available"][i]["display"] = m_root["rdp"]["available"][i]["name"]; - } - - if (m_root["rdp"]["available"][i]["name"].asCString() != sel_name) - continue; - - tmp = m_root["rdp"]["available"][i]["app"].asCString(); - ex_astr2wstr(tmp, rdp_app, EX_CODEPAGE_UTF8); - tmp = m_root["rdp"]["available"][i]["cmdline"].asCString(); - ex_astr2wstr(tmp, rdp_cmdline, EX_CODEPAGE_UTF8); - tmp = m_root["rdp"]["available"][i]["name"].asCString(); - ex_astr2wstr(tmp, rdp_name, EX_CODEPAGE_UTF8); - - break; - } - - if (rdp_app.length() == 0 || rdp_cmdline.length() == 0 || rdp_name.length() == 0) { - EXLOGE("invalid config, error 6.\n"); - return false; - } - - return true; -} +#include "stdafx.h" +#include "ts_cfg.h" +#include "ts_env.h" + + +TsCfg g_cfg; + +TsCfg::TsCfg() +{} + +TsCfg::~TsCfg() +{} + +bool TsCfg::init(void) { + ex_astr file_content; + if (!ex_read_text_file(g_env.m_cfg_file, file_content)) { + EXLOGE("can not load config file.\n"); + return false; + } + + if (!_load(file_content)) + return false; + + return true; +} + +bool TsCfg::save(const ex_astr& new_value) +{ + if (!_load(new_value)) + return false; + + //Json::StyledWriter jwriter; + Json::StreamWriterBuilder jwb; + std::unique_ptr jwriter(jwb.newStreamWriter()); + ex_aoss os; + jwriter->write(m_root, &os); + ex_astr val = os.str(); + + if (!ex_write_text_file(g_env.m_cfg_file, val)) { + EXLOGE("can not save config file.\n"); + return false; + } + + return true; +} + +bool TsCfg::_load(const ex_astr& str_json) { + //Json::Reader jreader; + Json::CharReaderBuilder jcrb; + std::unique_ptr const jreader(jcrb.newCharReader()); + const char *str_json_begin = str_json.c_str(); + + ex_astr err; + if (!jreader->parse(str_json_begin, str_json_begin + str_json.length(), &m_root, &err)) { + EXLOGE("can not parse new config data, not in json format? %s\n", err.c_str()); + return false; + } + + ex_astr sel_name; + size_t i = 0; + ex_astr tmp; + + //=================================== + // check ssh config + //=================================== + + if (!m_root["ssh"].isObject()) { + EXLOGE("invalid config, error 1.\n"); + return false; + } + + if (!m_root["ssh"]["selected"].isString()) { + EXLOGE("invalid config, error 2.\n"); + return false; + } + + sel_name = m_root["ssh"]["selected"].asCString(); + + if (!m_root["ssh"]["available"].isArray() || m_root["ssh"]["available"].size() == 0) { + EXLOGE("invalid config, error 3.\n"); + return false; + } + + for (i = 0; i < m_root["ssh"]["available"].size(); ++i) { + + if ( + !m_root["ssh"]["available"][i]["name"].isString() + || !m_root["ssh"]["available"][i]["app"].isString() + || !m_root["ssh"]["available"][i]["cmdline"].isString() + ) { + EXLOGE("invalid config, error 4.\n"); + return false; + } + + if (m_root["ssh"]["available"][i]["display"].isNull()) { + m_root["ssh"]["available"][i]["display"] = m_root["ssh"]["available"][i]["name"]; + } + + if (m_root["ssh"]["available"][i]["name"].asCString() != sel_name) + continue; + + tmp = m_root["ssh"]["available"][i]["app"].asCString(); + ex_astr2wstr(tmp, ssh_app, EX_CODEPAGE_UTF8); + tmp = m_root["ssh"]["available"][i]["cmdline"].asCString(); + ex_astr2wstr(tmp, ssh_cmdline, EX_CODEPAGE_UTF8); + + break; + } + + if (ssh_app.length() == 0 || ssh_cmdline.length() == 0) { + EXLOGE("invalid config, error 6.\n"); + return false; + } + + //=================================== + // check sftp config + //=================================== + + if (!m_root["scp"].isObject()) { + EXLOGE("invalid config, error 1.\n"); + return false; + } + + if (!m_root["scp"]["selected"].isString()) { + EXLOGE("invalid config, error 2.\n"); + return false; + } + + sel_name = m_root["scp"]["selected"].asCString(); + + if (!m_root["scp"]["available"].isArray() || m_root["scp"]["available"].size() == 0) { + EXLOGE("invalid config, error 3.\n"); + return false; + } + + for (i = 0; i < m_root["scp"]["available"].size(); ++i) { + + if ( + !m_root["scp"]["available"][i]["name"].isString() + || !m_root["scp"]["available"][i]["app"].isString() + || !m_root["scp"]["available"][i]["cmdline"].isString() + ) { + EXLOGE("invalid config, error 4.\n"); + return false; + } + + if (m_root["scp"]["available"][i]["display"].isNull()) { + m_root["scp"]["available"][i]["display"] = m_root["scp"]["available"][i]["name"]; + } + + if (m_root["scp"]["available"][i]["name"].asCString() != sel_name) + continue; + + tmp = m_root["scp"]["available"][i]["app"].asCString(); + ex_astr2wstr(tmp, scp_app, EX_CODEPAGE_UTF8); + tmp = m_root["scp"]["available"][i]["cmdline"].asCString(); + ex_astr2wstr(tmp, scp_cmdline, EX_CODEPAGE_UTF8); + + break; + } + + if (scp_app.length() == 0 || scp_cmdline.length() == 0) { + EXLOGE("invalid config, error 6.\n"); + return false; + } + + //=================================== + // check telnet config + //=================================== + + if (!m_root["telnet"].isObject()) { + EXLOGE("invalid config, error 1.\n"); + return false; + } + + if (!m_root["telnet"]["selected"].isString()) { + EXLOGE("invalid config, error 2.\n"); + return false; + } + + sel_name = m_root["telnet"]["selected"].asCString(); + + if (!m_root["telnet"]["available"].isArray() || m_root["telnet"]["available"].size() == 0) { + EXLOGE("invalid config, error 3.\n"); + return false; + } + + for (i = 0; i < m_root["telnet"]["available"].size(); ++i) { + + if ( + !m_root["telnet"]["available"][i]["name"].isString() + || !m_root["telnet"]["available"][i]["app"].isString() + || !m_root["telnet"]["available"][i]["cmdline"].isString() + ) { + EXLOGE("invalid config, error 4.\n"); + return false; + } + + if (m_root["telnet"]["available"][i]["display"].isNull()) { + m_root["telnet"]["available"][i]["display"] = m_root["telnet"]["available"][i]["name"]; + } + + if (m_root["telnet"]["available"][i]["name"].asCString() != sel_name) + continue; + + tmp = m_root["telnet"]["available"][i]["app"].asCString(); + ex_astr2wstr(tmp, telnet_app, EX_CODEPAGE_UTF8); + tmp = m_root["telnet"]["available"][i]["cmdline"].asCString(); + ex_astr2wstr(tmp, telnet_cmdline, EX_CODEPAGE_UTF8); + + break; + } + + if (telnet_app.length() == 0 || telnet_cmdline.length() == 0) { + EXLOGE("invalid config, error 6.\n"); + return false; + } + + //=================================== + // check rdp config + //=================================== + + if (!m_root["rdp"].isObject()) { + EXLOGE("invalid config, error 1.\n"); + return false; + } + + if (!m_root["rdp"]["selected"].isString()) { + EXLOGE("invalid config, error 2.\n"); + return false; + } + + sel_name = m_root["rdp"]["selected"].asCString(); + + if (!m_root["rdp"]["available"].isArray() || m_root["rdp"]["available"].size() == 0) { + EXLOGE("invalid config, error 3.\n"); + return false; + } + + for (i = 0; i < m_root["rdp"]["available"].size(); ++i) { + + if ( + !m_root["rdp"]["available"][i]["name"].isString() + || !m_root["rdp"]["available"][i]["app"].isString() + || !m_root["rdp"]["available"][i]["cmdline"].isString() + ) { + EXLOGE("invalid config, error 4.\n"); + return false; + } + + if (m_root["rdp"]["available"][i]["display"].isNull()) { + m_root["rdp"]["available"][i]["display"] = m_root["rdp"]["available"][i]["name"]; + } + + if (m_root["rdp"]["available"][i]["name"].asCString() != sel_name) + continue; + + tmp = m_root["rdp"]["available"][i]["app"].asCString(); + ex_astr2wstr(tmp, rdp_app, EX_CODEPAGE_UTF8); + tmp = m_root["rdp"]["available"][i]["cmdline"].asCString(); + ex_astr2wstr(tmp, rdp_cmdline, EX_CODEPAGE_UTF8); + tmp = m_root["rdp"]["available"][i]["name"].asCString(); + ex_astr2wstr(tmp, rdp_name, EX_CODEPAGE_UTF8); + + break; + } + + if (rdp_app.length() == 0 || rdp_cmdline.length() == 0 || rdp_name.length() == 0) { + EXLOGE("invalid config, error 6.\n"); + return false; + } + + return true; +} diff --git a/client/tp_assist_win/ts_env.cpp b/client/tp_assist_win/ts_env.cpp index db08129..767ef7b 100644 --- a/client/tp_assist_win/ts_env.cpp +++ b/client/tp_assist_win/ts_env.cpp @@ -1,71 +1,74 @@ -#include "stdafx.h" -#include "ts_env.h" - -#include -#ifdef EX_OS_WIN32 -# include -//# include -#endif - -TsEnv g_env; - -//======================================================= -// -//======================================================= - -TsEnv::TsEnv() -{} - -TsEnv::~TsEnv() -{} - -bool TsEnv::init(void) -{ - if (!ex_exec_file(m_exec_file)) - return false; - - m_exec_path = m_exec_file; - if (!ex_dirname(m_exec_path)) - return false; - - m_cfg_file = m_exec_path; - ex_path_join(m_cfg_file, false, L"cfg", L"tp-assist.json", NULL); - - m_log_path = m_exec_path; - ex_path_join(m_log_path, false, L"log", NULL); - - ex_wstr cfg_default; - -#ifdef _DEBUG - m_site_path = m_exec_path; - ex_path_join(m_site_path, true, L"..", L"..", L"..", L"..", L"client", L"tp_assist_win", L"site", NULL); - - m_tools_path = m_exec_path; - ex_path_join(m_tools_path, true, L"..", L"..", L"..", L"..", L"client", L"tools", NULL); - - cfg_default = m_exec_path; - ex_path_join(cfg_default, true, L"..", L"..", L"..", L"..", L"client", L"tp_assist_win", L"cfg", L"tp-assist.default.json", NULL); - -#else - m_site_path = m_exec_path; - ex_path_join(m_site_path, false, L"site", NULL); - - m_tools_path = m_exec_path; - ex_path_join(m_tools_path, false, L"tools", NULL); - - cfg_default = m_exec_path; - ex_path_join(cfg_default, false, L"tp-assist.default.json", NULL); -#endif - - if (!ex_is_file_exists(m_cfg_file.c_str())) { - ex_wstr cfg_path = m_exec_path; - ex_path_join(cfg_path, false, L"cfg", NULL); - - ex_mkdirs(cfg_path); - - if (!ex_copy_file(cfg_default.c_str(), m_cfg_file.c_str())) - return false; -} - - return true; -} +#include "stdafx.h" +#include "ts_env.h" + +#include +#ifdef EX_OS_WIN32 +# include +//# include +#endif + +TsEnv g_env; + +//======================================================= +// +//======================================================= + +TsEnv::TsEnv() +{} + +TsEnv::~TsEnv() +{} + +bool TsEnv::init(void) +{ + if (!ex_exec_file(m_exec_file)) + return false; + + m_exec_path = m_exec_file; + if (!ex_dirname(m_exec_path)) + return false; + + m_cfg_file = m_exec_path; + ex_path_join(m_cfg_file, false, L"cfg", L"tp-assist.json", NULL); + + m_log_path = m_exec_path; + ex_path_join(m_log_path, false, L"log", NULL); + + ex_wstr cfg_default; + +#ifdef _DEBUG + m_site_path = m_exec_path; + ex_path_join(m_site_path, true, L"..", L"..", L"..", L"..", L"client", L"tp_assist_win", L"site", NULL); + +// m_tools_path = m_exec_path; +// ex_path_join(m_tools_path, true, L"..", L"..", L"..", L"..", L"client", L"tools", NULL); + + cfg_default = m_exec_path; + ex_path_join(cfg_default, true, L"..", L"..", L"..", L"..", L"client", L"tp_assist_win", L"cfg", L"tp-assist.default.json", NULL); + +#else + m_site_path = m_exec_path; + ex_path_join(m_site_path, false, L"site", NULL); + +// m_tools_path = m_exec_path; +// ex_path_join(m_tools_path, false, L"tools", NULL); + + cfg_default = m_exec_path; + ex_path_join(cfg_default, false, L"tp-assist.default.json", NULL); +#endif + + m_tools_path = m_exec_path; + ex_path_join(m_tools_path, false, L"tools", NULL); + + if (!ex_is_file_exists(m_cfg_file.c_str())) { + ex_wstr cfg_path = m_exec_path; + ex_path_join(cfg_path, false, L"cfg", NULL); + + ex_mkdirs(cfg_path); + + if (!ex_copy_file(cfg_default.c_str(), m_cfg_file.c_str())) + return false; +} + + return true; +} diff --git a/client/tp_assist_win/ts_http_rpc.cpp b/client/tp_assist_win/ts_http_rpc.cpp index 8c57c25..3cebf9c 100644 --- a/client/tp_assist_win/ts_http_rpc.cpp +++ b/client/tp_assist_win/ts_http_rpc.cpp @@ -1,1207 +1,1191 @@ -#include "stdafx.h" - -#pragma warning(disable:4091) - -#include -#include -#include - -#pragma comment(lib, "Crypt32.lib") - -#include - -#include "ts_http_rpc.h" -#include "dlg_main.h" -#include "ts_ver.h" -#include "ts_env.h" - -/* -1. -SecureCRT֧ñǩҳı⣬в /N "tab name"Ϳ -Example: -To launch a new Telnet session, displaying the name "Houston, TX" on the tab, use the following: -/T /N "Houston, TX" /TELNET 192.168.0.6 - -2. -SecureCRTŵһڵIJͬǩҳУʹò /T - SecureCRT.exe /T /N "TP#ssh://192.168.1.3" /SSH2 /L root /PASSWORD 1234 120.26.109.25 - -3. -telnetͻ˵ - putty.exe telnet://administrator@127.0.0.1:52389 -SecureCRTҪ - SecureCRT.exe /T /N "TP#telnet://192.168.1.3" /SCRIPT X:\path\to\startup.vbs /TELNET 127.0.0.1 52389 -Уstartup.vbsΪ ----------ļʼ--------- -#$language = "VBScript" -#$interface = "1.0" -Sub main - crt.Screen.Synchronous = True - crt.Screen.WaitForString "ogin: " - crt.Screen.Send "SESSION-ID" & VbCr - crt.Screen.Synchronous = False -End Sub ----------ļ--------- - -4. ΪputtyĴڱǩʾIPԳӳɹ˷ - PS1="\[\e]0;${debian_chroot:+($debian_chroot)}\u@192.168.1.2: \w\a\]$PS1" -ֹˣubuntuԣ֪Ƿܹ֧еLinuxSecureCRTԴ˱ʾԡ -*/ - -//#define RDP_CLIENT_SYSTEM_BUILTIN -// #define RDP_CLIENT_SYSTEM_ACTIVE_CONTROL -//#define RDP_CLIENT_FREERDP - - -//#ifdef RDP_CLIENT_SYSTEM_BUILTIN - -//connect to console:i:%d -//compression:i:1 -//bitmapcachepersistenable:i:1 - -std::string rdp_content = "\ -administrative session:i:%d\n\ -screen mode id:i:%d\n\ -use multimon:i:0\n\ -desktopwidth:i:%d\n\ -desktopheight:i:%d\n\ -session bpp:i:16\n\ -winposstr:s:0,1,%d,%d,%d,%d\n\ -compression:i:1\n\ -bitmapcachepersistenable:i:1\n\ -keyboardhook:i:2\n\ -audiocapturemode:i:0\n\ -videoplaybackmode:i:1\n\ -connection type:i:7\n\ -networkautodetect:i:1\n\ -bandwidthautodetect:i:1\n\ -displayconnectionbar:i:1\n\ -enableworkspacereconnect:i:0\n\ -disable wallpaper:i:1\n\ -allow font smoothing:i:0\n\ -allow desktop composition:i:0\n\ -disable full window drag:i:1\n\ -disable menu anims:i:1\n\ -disable themes:i:1\n\ -disable cursor setting:i:1\n\ -full address:s:%s:%d\n\ -audiomode:i:0\n\ -redirectprinters:i:0\n\ -redirectcomports:i:0\n\ -redirectsmartcards:i:0\n\ -redirectclipboard:i:%d\n\ -redirectposdevices:i:0\n\ -autoreconnection enabled:i:0\n\ -authentication level:i:2\n\ -prompt for credentials:i:0\n\ -negotiate security layer:i:1\n\ -remoteapplicationmode:i:0\n\ -alternate shell:s:\n\ -shell working directory:s:\n\ -gatewayhostname:s:\n\ -gatewayusagemethod:i:4\n\ -gatewaycredentialssource:i:4\n\ -gatewayprofileusagemethod:i:0\n\ -promptcredentialonce:i:0\n\ -gatewaybrokeringtype:i:0\n\ -use redirection server name:i:0\n\ -rdgiskdcproxy:i:0\n\ -kdcproxyname:s:\n\ -drivestoredirect:s:%s\n\ -username:s:%s\n\ -password 51:b:%s\n\ -"; - -//redirectdirectx:i:0\n\ -//prompt for credentials on client:i:0\n\ - -//#endif - - -TsHttpRpc g_http_interface; -TsHttpRpc g_https_interface; - -void http_rpc_main_loop(bool is_https) { - if (is_https) { - if (!g_https_interface.init_https()) { - EXLOGE("[ERROR] can not start HTTPS-RPC listener, maybe port %d is already in use.\n", TS_HTTPS_RPC_PORT); - return; - } - - EXLOGW("======================================================\n"); - EXLOGW("[rpc] TeleportAssist-HTTPS-RPC ready on 127.0.0.1:%d\n", TS_HTTPS_RPC_PORT); - - g_https_interface.run(); - - EXLOGW("[rpc] HTTPS-Server main loop end.\n"); - } else { - if (!g_http_interface.init_http()) { - EXLOGE("[ERROR] can not start HTTP-RPC listener, maybe port %d is already in use.\n", TS_HTTP_RPC_PORT); - return; - } - - EXLOGW("======================================================\n"); - EXLOGW("[rpc] TeleportAssist-HTTP-RPC ready on 127.0.0.1:%d\n", TS_HTTP_RPC_PORT); - - g_http_interface.run(); - - EXLOGW("[rpc] HTTP-Server main loop end.\n"); - } -} - -void http_rpc_stop(bool is_https) { - if (is_https) - g_https_interface.stop(); - else - g_http_interface.stop(); -} - -#define HEXTOI(x) (isdigit(x) ? x - '0' : x - 'W') - -int ts_url_decode(const char *src, int src_len, char *dst, int dst_len, int is_form_url_encoded) { - int i, j, a, b; - - for (i = j = 0; i < src_len && j < dst_len - 1; i++, j++) { - if (src[i] == '%') { - if (i < src_len - 2 && isxdigit(*(const unsigned char *)(src + i + 1)) && - isxdigit(*(const unsigned char *)(src + i + 2))) { - a = tolower(*(const unsigned char *)(src + i + 1)); - b = tolower(*(const unsigned char *)(src + i + 2)); - dst[j] = (char)((HEXTOI(a) << 4) | HEXTOI(b)); - i += 2; - } else { - return -1; - } - } else if (is_form_url_encoded && src[i] == '+') { - dst[j] = ' '; - } else { - dst[j] = src[i]; - } - } - - dst[j] = '\0'; /* Null-terminate the destination */ - - return i >= src_len ? j : -1; -} - -bool calc_psw51b(const char* password, std::string& ret) { - DATA_BLOB DataIn; - DATA_BLOB DataOut; - - ex_wstr w_pswd; - ex_astr2wstr(password, w_pswd, EX_CODEPAGE_ACP); - - DataIn.cbData = w_pswd.length() * sizeof(wchar_t); - DataIn.pbData = (BYTE*)w_pswd.c_str(); - - - if (!CryptProtectData(&DataIn, L"psw", nullptr, nullptr, nullptr, 0, &DataOut)) - return false; - - char szRet[5] = { 0 }; - for (DWORD i = 0; i < DataOut.cbData; ++i) { - sprintf_s(szRet, 5, "%02X", DataOut.pbData[i]); - ret += szRet; - } - - LocalFree(DataOut.pbData); - return true; -} - -bool isDegital(std::string str) { - for (int i = 0; i < str.size(); i++) { - if (str.at(i) == '-' && str.size() > 1) // пָܳ - continue; - if (str.at(i) > '9' || str.at(i) < '0') - return false; - } - return true; -} - -std::string strtolower(std::string str) { - for (int i = 0; i < str.size(); i++) - { - str[i] = tolower(str[i]); - } - return str; -} - -void SplitString(const std::string& s, std::vector& v, const std::string& c) -{ - std::string::size_type pos1, pos2; - pos2 = s.find(c); - pos1 = 0; - while (std::string::npos != pos2) - { - v.push_back(s.substr(pos1, pos2 - pos1)); - - pos1 = pos2 + c.size(); - pos2 = s.find(c, pos1); - } - if (pos1 != s.length()) - v.push_back(s.substr(pos1)); -} - -TsHttpRpc::TsHttpRpc() { - m_stop = false; - mg_mgr_init(&m_mg_mgr, nullptr); -} - -TsHttpRpc::~TsHttpRpc() { - mg_mgr_free(&m_mg_mgr); -} - -bool TsHttpRpc::init_http() { - struct mg_connection* nc = nullptr; - - char addr[128] = { 0 }; - ex_strformat(addr, 128, "tcp://127.0.0.1:%d", TS_HTTP_RPC_PORT); - - nc = mg_bind(&m_mg_mgr, addr, _mg_event_handler); - if (!nc) { - EXLOGE("[rpc] TsHttpRpc::init 127.0.0.1:%d\n", TS_HTTP_RPC_PORT); - return false; - } - nc->user_data = this; - - mg_set_protocol_http_websocket(nc); - - return _on_init(); -} - -bool TsHttpRpc::init_https() { - ex_wstr file_ssl_cert = g_env.m_exec_path; - ex_path_join(file_ssl_cert, true, L"cfg", L"localhost.pem", NULL); - ex_wstr file_ssl_key = g_env.m_exec_path; - ex_path_join(file_ssl_key, true, L"cfg", L"localhost.key", NULL); - ex_astr _ssl_cert; - ex_wstr2astr(file_ssl_cert, _ssl_cert); - ex_astr _ssl_key; - ex_wstr2astr(file_ssl_key, _ssl_key); - - const char *err = NULL; - struct mg_bind_opts bind_opts; - memset(&bind_opts, 0, sizeof(bind_opts)); - bind_opts.ssl_cert = _ssl_cert.c_str(); - bind_opts.ssl_key = _ssl_key.c_str(); - bind_opts.error_string = &err; - - - char addr[128] = { 0 }; - ex_strformat(addr, 128, "tcp://127.0.0.1:%d", TS_HTTPS_RPC_PORT); - - struct mg_connection* nc = nullptr; - nc = mg_bind_opt(&m_mg_mgr, addr, _mg_event_handler, bind_opts); - if (!nc) { - EXLOGE("[rpc] TsHttpRpc::init 127.0.0.1:%d\n", TS_HTTPS_RPC_PORT); - return false; - } - nc->user_data = this; - - mg_set_protocol_http_websocket(nc); - - return _on_init(); -} - -bool TsHttpRpc::_on_init() { - char file_name[MAX_PATH] = { 0 }; - if (!GetModuleFileNameA(nullptr, file_name, MAX_PATH)) - return false; - - int len = strlen(file_name); - - if (file_name[len] == '\\') - file_name[len] = '\0'; - - char* match = strrchr(file_name, '\\'); - if (match) - *match = '\0'; - - m_content_type_map[".js"] = "application/javascript"; - m_content_type_map[".png"] = "image/png"; - m_content_type_map[".jpeg"] = "image/jpeg"; - m_content_type_map[".jpg"] = "image/jpeg"; - m_content_type_map[".gif"] = "image/gif"; - m_content_type_map[".ico"] = "image/x-icon"; - m_content_type_map[".json"] = "image/json"; - m_content_type_map[".html"] = "text/html"; - m_content_type_map[".css"] = "text/css"; - m_content_type_map[".tif"] = "image/tiff"; - m_content_type_map[".tiff"] = "image/tiff"; - m_content_type_map[".svg"] = "text/html"; - - return true; -} - -void TsHttpRpc::run(void) { - while (!m_stop) { - mg_mgr_poll(&m_mg_mgr, 500); - } -} - -void TsHttpRpc::stop(void) { - m_stop = true; -} - -void TsHttpRpc::_mg_event_handler(struct mg_connection *nc, int ev, void *ev_data) { - struct http_message *hm = (struct http_message*)ev_data; - - TsHttpRpc* _this = (TsHttpRpc*)nc->user_data; - if (!_this) { - EXLOGE("[ERROR] invalid http request.\n"); - return; - } - - switch (ev) { - case MG_EV_HTTP_REQUEST: - { - ex_astr uri; - ex_chars _uri; - _uri.resize(hm->uri.len + 1); - memset(&_uri[0], 0, hm->uri.len + 1); - memcpy(&_uri[0], hm->uri.p, hm->uri.len); - uri = &_uri[0]; - -#ifdef EX_DEBUG - char* dbg_method = nullptr; - if (hm->method.len == 3 && 0 == memcmp(hm->method.p, "GET", hm->method.len)) - dbg_method = "GET"; - else if (hm->method.len == 4 && 0 == memcmp(hm->method.p, "POST", hm->method.len)) - dbg_method = "POST"; - else - dbg_method = "UNSUPPORTED-HTTP-METHOD"; - - EXLOGV("[rpc] got %s request: %s\n", dbg_method, uri.c_str()); -#endif - ex_astr ret_buf; - bool b_is_html = false; - -// if (uri == "/") { -// ex_wstr page = L"Teleport\n\n
    Teleportֹ
    "; -// ex_wstr2astr(page, ret_buf, EX_CODEPAGE_UTF8); -// -// mg_printf(nc, "HTTP/1.0 200 OK\r\nAccess-Control-Allow-Origin: *\r\nContent-Length: %d\r\nContent-Type: text/html\r\n\r\n%s", ret_buf.size() - 1, &ret_buf[0]); -// nc->flags |= MG_F_SEND_AND_CLOSE; -// return; -// } - - if (uri == "/") { - uri = "/status.html"; - b_is_html = true; - } - else if (uri == "/config") { - uri = "/index.html"; - b_is_html = true; - } - - ex_astr temp; - int offset = uri.find("/", 1); - if (offset > 0) { - temp = uri.substr(1, offset - 1); - - if (temp == "api") { - ex_astr method; - ex_astr json_param; - int rv = _this->_parse_request(hm, method, json_param); - if (0 != rv) { - EXLOGE("[ERROR] http-rpc got invalid request.\n"); - _this->_create_json_ret(ret_buf, rv); - } else { - _this->_process_js_request(method, json_param, ret_buf); - } - - mg_printf(nc, "HTTP/1.0 200 OK\r\nAccess-Control-Allow-Origin: *\r\nContent-Length: %ld\r\nContent-Type: application/json\r\n\r\n%s", ret_buf.size() - 1, &ret_buf[0]); - nc->flags |= MG_F_SEND_AND_CLOSE; - return; - } - } - - - ex_astr file_suffix; - offset = uri.rfind("."); - if (offset > 0) { - file_suffix = uri.substr(offset, uri.length()); - } - - ex_wstr2astr(g_env.m_site_path, temp); - ex_astr index_path = temp + uri; - - - FILE* file = ex_fopen(index_path.c_str(), "rb"); - if (file) { - unsigned long file_size = 0; - char* buf = nullptr; - size_t ret = 0; - - fseek(file, 0, SEEK_END); - file_size = ftell(file); - buf = new char[file_size]; - memset(buf, 0, file_size); - fseek(file, 0, SEEK_SET); - ret = fread(buf, 1, file_size, file); - fclose(file); - - ex_astr content_type = _this->get_content_type(file_suffix); - - mg_printf(nc, "HTTP/1.0 200 OK\r\nAccess-Control-Allow-Origin: *\r\nContent-Length: %ld\r\nContent-Type: %s\r\n\r\n", file_size, content_type.c_str()); - mg_send(nc, buf, (int)file_size); - delete[]buf; - nc->flags |= MG_F_SEND_AND_CLOSE; - return; - } else if (b_is_html) { - ex_wstr page = L"404 Not Found

    404 Not Found


    Teleport Assistor configuration page not found.

    "; - ex_wstr2astr(page, ret_buf, EX_CODEPAGE_UTF8); - - mg_printf(nc, "HTTP/1.0 404 File Not Found\r\nAccess-Control-Allow-Origin: *\r\nContent-Length: %ld\r\nContent-Type: text/html\r\n\r\n%s", ret_buf.size() - 1, &ret_buf[0]); - nc->flags |= MG_F_SEND_AND_CLOSE; - return; - } - - } - break; - default: - break; - } -} - -int TsHttpRpc::_parse_request(struct http_message* req, ex_astr& func_cmd, ex_astr& func_args) { - if (!req) - return TPE_FAILED; - - bool is_get = true; - if (req->method.len == 3 && 0 == memcmp(req->method.p, "GET", req->method.len)) - is_get = true; - else if (req->method.len == 4 && 0 == memcmp(req->method.p, "POST", req->method.len)) - is_get = false; - else - return TPE_HTTP_METHOD; - - ex_astrs strs; - - size_t pos_start = 1; // һֽڣһ '/' - - size_t i = 0; - for (i = pos_start; i < req->uri.len; ++i) { - if (req->uri.p[i] == '/') { - if (i - pos_start > 0) { - ex_astr tmp_uri; - tmp_uri.assign(req->uri.p + pos_start, i - pos_start); - strs.push_back(tmp_uri); - } - pos_start = i + 1; // ǰҵķָ - } - } - if (pos_start < req->uri.len) { - ex_astr tmp_uri; - tmp_uri.assign(req->uri.p + pos_start, req->uri.len - pos_start); - strs.push_back(tmp_uri); - } - - if (strs.empty() || strs[0] != "api") - return TPE_PARAM; - - if (is_get) { - if (2 == strs.size()) { - func_cmd = strs[1]; - } else if (3 == strs.size()) { - func_cmd = strs[1]; - func_args = strs[2]; - } else { - return TPE_PARAM; - } - } else { - if (2 == strs.size()) { - func_cmd = strs[1]; - } else { - return TPE_PARAM; - } - - if (req->body.len > 0) { - func_args.assign(req->body.p, req->body.len); - } - } - - if (func_args.length() > 0) { - // url-decode - int len = func_args.length() * 2; - ex_chars sztmp; - sztmp.resize(len); - memset(&sztmp[0], 0, len); - if (-1 == ts_url_decode(func_args.c_str(), func_args.length(), &sztmp[0], len, 0)) - return TPE_HTTP_URL_ENCODE; - - func_args = &sztmp[0]; - } - - EXLOGV("[rpc] method=%s, json_param=%s\n", func_cmd.c_str(), func_args.c_str()); - - return TPE_OK; -} - -void TsHttpRpc::_process_js_request(const ex_astr& func_cmd, const ex_astr& func_args, ex_astr& buf) { - if (func_cmd == "get_version") { - _rpc_func_get_version(func_args, buf); - } else if (func_cmd == "run") { - _rpc_func_run_client(func_args, buf); - } else if (func_cmd == "rdp_play") { - _rpc_func_rdp_play(func_args, buf); - } else if (func_cmd == "get_config") { - _rpc_func_get_config(func_args, buf); - } else if (func_cmd == "set_config") { - _rpc_func_set_config(func_args, buf); - } else if (func_cmd == "file_action") { - _rpc_func_file_action(func_args, buf); - } else { - EXLOGE("[rpc] got unknown command: %s\n", func_cmd.c_str()); - _create_json_ret(buf, TPE_UNKNOWN_CMD); - } -} - -void TsHttpRpc::_create_json_ret(ex_astr& buf, int errcode) { - // أ {"code":123} - - Json::FastWriter jr_writer; - Json::Value jr_root; - - jr_root["code"] = errcode; - buf = jr_writer.write(jr_root); -} - -void TsHttpRpc::_create_json_ret(ex_astr& buf, Json::Value& jr_root) { - Json::FastWriter jr_writer; - buf = jr_writer.write(jr_root); -} - -void TsHttpRpc::_rpc_func_url_protocol(const ex_astr& args, ex_astr& buf) -{ - //urlprotocol÷ʽ - // url-decode - std::string func_args = args; - if (func_args.length() > 0) - { - int len = func_args.length() * 2; - ex_chars sztmp; - sztmp.resize(len); - memset(&sztmp[0], 0, len); - if (-1 == ts_url_decode(func_args.c_str(), func_args.length(), &sztmp[0], len, 0)) - return ; - - func_args = &sztmp[0]; - } - EXLOGD(("%s\n"), func_args.c_str()); - //ιteleport://{}/,ֻ - std::string urlproto_appname = TP_URLPROTO_APP_NAME; - urlproto_appname += "://{"; - func_args.erase(0, urlproto_appname.length());//ȥһURLPROTO_APP_NAMEԼ://ַ - int pos = func_args.length() - 1; - if (func_args.substr(pos, 1) == "/") - func_args.erase(pos - 1, 2);//ȥһ}/ַ - else - func_args.erase(pos, 1); - - //Сieʱԭjsonṹе"ȥҪ¸ʽΪjsonʽ - if (func_args.find("\"", 0) == std::string::npos) { - std::vector strv; - SplitString(func_args, strv, ","); - func_args = ""; - for (std::vector::size_type i = 0; i < strv.size(); i++) { - std::vector strv1; - SplitString(strv[i], strv1, ":"); - strv1[0] = "\"" + strv1[0] + "\""; - if (!isDegital(strv1[1]) && strtolower(strv1[1]) != "true" && strtolower(strv1[1]) != "false") - strv1[1] = "\"" + strv1[1] + "\""; - - strv[i] = strv1[0] + ":" + strv1[1]; - if (i == 0) - func_args = strv[i]; - else - func_args += "," + strv[i]; - } - } - func_args = "{" + func_args + "}"; - EXLOGD(("%s\n"), func_args.c_str()); - //TsHttpRpc_rpc_func_run_clientͻ - _rpc_func_run_client(func_args, buf); -} - -void TsHttpRpc::_rpc_func_run_client(const ex_astr& func_args, ex_astr& buf) { - // Σ{"ip":"192.168.5.11","port":22,"uname":"root","uauth":"abcdefg","authmode":1,"protocol":2} - // authmode: 1=password, 2=private-key - // protocol: 1=rdp, 2=ssh - // SSHأ {"code":0, "data":{"sid":"0123abcde"}} - // RDPأ {"code":0, "data":{"sid":"0123abcde0A"}} - - Json::Reader jreader; - Json::Value jsRoot; - - if (!jreader.parse(func_args.c_str(), jsRoot)) { - _create_json_ret(buf, TPE_JSON_FORMAT); - return; - } - if (!jsRoot.isObject()) { - _create_json_ret(buf, TPE_PARAM); - return; - } - - // жϲǷȷ - if (!jsRoot["teleport_ip"].isString() - || !jsRoot["teleport_port"].isNumeric() || !jsRoot["remote_host_ip"].isString() - || !jsRoot["session_id"].isString() || !jsRoot["protocol_type"].isNumeric() || !jsRoot["protocol_sub_type"].isNumeric() - || !jsRoot["protocol_flag"].isNumeric() - ) { - _create_json_ret(buf, TPE_PARAM); - return; - } - - int pro_type = jsRoot["protocol_type"].asUInt(); - int pro_sub = jsRoot["protocol_sub_type"].asInt(); - ex_u32 protocol_flag = jsRoot["protocol_flag"].asUInt(); - - ex_astr teleport_ip = jsRoot["teleport_ip"].asCString(); - int teleport_port = jsRoot["teleport_port"].asUInt(); - - ex_astr remote_host_name = jsRoot["remote_host_name"].asCString(); - - ex_astr real_host_ip = jsRoot["remote_host_ip"].asCString(); - ex_astr sid = jsRoot["session_id"].asCString(); - - ex_wstr w_exe_path; - WCHAR w_szCommandLine[MAX_PATH] = { 0 }; - - - ex_wstr w_sid; - ex_astr2wstr(sid, w_sid); - ex_wstr w_teleport_ip; - ex_astr2wstr(teleport_ip, w_teleport_ip); - ex_wstr w_real_host_ip; - ex_astr2wstr(real_host_ip, w_real_host_ip); - ex_wstr w_remote_host_name; - ex_astr2wstr(remote_host_name, w_remote_host_name); - WCHAR w_port[32] = { 0 }; - swprintf_s(w_port, _T("%d"), teleport_port); - - ex_wstr tmp_rdp_file; // for .rdp file - - if (pro_type == TP_PROTOCOL_TYPE_RDP) { - //============================================== - // RDP - //============================================== - - bool flag_clipboard = ((protocol_flag & TP_FLAG_RDP_CLIPBOARD) == TP_FLAG_RDP_CLIPBOARD); - bool flag_disk = ((protocol_flag & TP_FLAG_RDP_DISK) == TP_FLAG_RDP_DISK); - bool flag_console = ((protocol_flag & TP_FLAG_RDP_CONSOLE) == TP_FLAG_RDP_CONSOLE); - - int rdp_w = 800; - int rdp_h = 640; - bool rdp_console = false; - - if (!jsRoot["rdp_width"].isNull()) { - if (jsRoot["rdp_width"].isNumeric()) { - rdp_w = jsRoot["rdp_width"].asUInt(); - } else { - _create_json_ret(buf, TPE_PARAM); - return; - } - } - - if (!jsRoot["rdp_height"].isNull()) { - if (jsRoot["rdp_height"].isNumeric()) { - rdp_h = jsRoot["rdp_height"].asUInt(); - } else { - _create_json_ret(buf, TPE_PARAM); - return; - } - } - - if (!jsRoot["rdp_console"].isNull()) { - if (jsRoot["rdp_console"].isBool()) { - rdp_console = jsRoot["rdp_console"].asBool(); - } else { - _create_json_ret(buf, TPE_PARAM); - return; - } - } - - if (!flag_console) - rdp_console = false; - - - int split_pos = sid.length() - 2; - ex_astr real_sid = sid.substr(0, split_pos); - ex_astr str_pwd_len = sid.substr(split_pos, sid.length()); - int n_pwd_len = strtol(str_pwd_len.c_str(), nullptr, 16); - n_pwd_len -= real_sid.length(); - n_pwd_len -= 2; - char szPwd[256] = { 0 }; - for (int i = 0; i < n_pwd_len; i++) { - szPwd[i] = '*'; - } - - ex_astr2wstr(real_sid, w_sid); - - w_exe_path = _T("\""); - w_exe_path += g_cfg.rdp_app + _T("\" "); - - ex_wstr rdp_name = g_cfg.rdp_name; - if (rdp_name == L"mstsc") { - w_exe_path += g_cfg.rdp_cmdline; - - int width = 0; - int higth = 0; - int cx = 0; - int cy = 0; - - int display = 1; - int iWidth = GetSystemMetrics(SM_CXSCREEN); - int iHeight = GetSystemMetrics(SM_CYSCREEN); - - if (rdp_w == 0 || rdp_h == 0) { - //ȫ - width = iWidth; - higth = iHeight; - display = 2; - } else { - width = rdp_w; - higth = rdp_h; - display = 1; - } - - cx = (iWidth - width) / 2; - cy = (iHeight - higth) / 2; - if (cx < 0) { - cx = 0; - } - if (cy < 0) { - cy = 0; - } - - // int console_mode = 0; - // if (rdp_console) - // console_mode = 1; - - std::string psw51b; - if (!calc_psw51b(szPwd, psw51b)) { - EXLOGE("calc password failed.\n"); - _create_json_ret(buf, TPE_FAILED); - return; - } - - real_sid = "01" + real_sid; - - char sz_rdp_file_content[4096] = { 0 }; - sprintf_s(sz_rdp_file_content, 4096, rdp_content.c_str() - , (flag_console && rdp_console) ? 1 : 0 - , display, width, higth - , cx, cy, cx + width + 100, cy + higth + 100 - , teleport_ip.c_str(), teleport_port - , flag_clipboard ? 1 : 0 - , flag_disk ? "*" : "" - , real_sid.c_str() - , psw51b.c_str() - ); - - char sz_file_name[MAX_PATH] = { 0 }; - char temp_path[MAX_PATH] = { 0 }; - DWORD ret = GetTempPathA(MAX_PATH, temp_path); - if (ret <= 0) { - EXLOGE("fopen failed (%d).\n", GetLastError()); - _create_json_ret(buf, TPE_FAILED); - return; - } - - ex_astr temp_host_ip = real_host_ip; - ex_replace_all(temp_host_ip, ".", "-"); - - sprintf_s(sz_file_name, MAX_PATH, ("%s%s.rdp"), temp_path, temp_host_ip.c_str()); - - FILE* f = NULL; - if (fopen_s(&f, sz_file_name, "wt") != 0) { - EXLOGE("fopen failed (%d).\n", GetLastError()); - _create_json_ret(buf, TPE_OPENFILE); - return; - } - // Write a string into the file. - fwrite(sz_rdp_file_content, strlen(sz_rdp_file_content), 1, f); - fclose(f); - ex_astr2wstr(sz_file_name, tmp_rdp_file); - - // 滻 - ex_replace_all(w_exe_path, _T("{tmp_rdp_file}"), tmp_rdp_file); - } else if (g_cfg.rdp_name == L"freerdp") { - w_exe_path += L"{size} {console} {clipboard} {drives} "; - w_exe_path += g_cfg.rdp_cmdline; - - ex_wstr w_screen; - - if (rdp_w == 0 || rdp_h == 0) { - //ȫ - w_screen = _T("/f"); - } else { - char sz_size[64] = { 0 }; - ex_strformat(sz_size, 63, "/size:%dx%d", rdp_w, rdp_h); - ex_astr2wstr(sz_size, w_screen); - } - - // wchar_t* w_console = NULL; - // - // if (flag_console && rdp_console) - // { - // w_console = L"/admin"; - // } - // else - // { - // w_console = L""; - // } - - ex_wstr w_password; - ex_astr2wstr(szPwd, w_password); - w_exe_path += L" /p:"; - w_exe_path += w_password; - - w_sid = L"02" + w_sid; - - w_exe_path += L" /gdi:sw"; // ʹȾgdi:hwʹӲ٣ǻֺܶڿ飨¼طʱģ - w_exe_path += L" -grab-keyboard"; // [new style] ֹFreeRDPʧȥؼӦСһFreeRDPڣòƲã - - // 滻 - ex_replace_all(w_exe_path, _T("{size}"), w_screen); - - if (flag_console && rdp_console) - ex_replace_all(w_exe_path, _T("{console}"), L"/admin"); - else - ex_replace_all(w_exe_path, _T("{console}"), L""); - - //ex_replace_all(w_exe_path, _T("{clipboard}"), L"+clipboard"); - - if (flag_clipboard) - ex_replace_all(w_exe_path, _T("{clipboard}"), L"/clipboard"); - else - ex_replace_all(w_exe_path, _T("{clipboard}"), L"-clipboard"); - - if (flag_disk) - ex_replace_all(w_exe_path, _T("{drives}"), L"/drives"); - else - ex_replace_all(w_exe_path, _T("{drives}"), L"-drives"); - } else { - _create_json_ret(buf, TPE_FAILED); - return; - } - } else if (pro_type == TP_PROTOCOL_TYPE_SSH) { - //============================================== - // SSH - //============================================== - - if (pro_sub == TP_PROTOCOL_TYPE_SSH_SHELL) { - w_exe_path = _T("\""); - w_exe_path += g_cfg.ssh_app + _T("\" "); - w_exe_path += g_cfg.ssh_cmdline; - } else { - w_exe_path = _T("\""); - w_exe_path += g_cfg.scp_app + _T("\" "); - w_exe_path += g_cfg.scp_cmdline; - } - } else if (pro_type == TP_PROTOCOL_TYPE_TELNET) { - //============================================== - // TELNET - //============================================== - w_exe_path = _T("\""); - w_exe_path += g_cfg.telnet_app + _T("\" "); - w_exe_path += g_cfg.telnet_cmdline; - } - - ex_replace_all(w_exe_path, _T("{host_ip}"), w_teleport_ip.c_str()); - ex_replace_all(w_exe_path, _T("{host_port}"), w_port); - ex_replace_all(w_exe_path, _T("{user_name}"), w_sid.c_str()); - ex_replace_all(w_exe_path, _T("{host_name}"), w_remote_host_name.c_str()); - ex_replace_all(w_exe_path, _T("{real_ip}"), w_real_host_ip.c_str()); - ex_replace_all(w_exe_path, _T("{assist_tools_path}"), g_env.m_tools_path.c_str()); - - - STARTUPINFO si; - PROCESS_INFORMATION pi; - - ZeroMemory(&si, sizeof(si)); - si.cb = sizeof(si); - ZeroMemory(&pi, sizeof(pi)); - - Json::Value root_ret; - ex_astr utf8_path; - ex_wstr2astr(w_exe_path, utf8_path, EX_CODEPAGE_UTF8); - root_ret["path"] = utf8_path; - - if (!CreateProcess(NULL, (wchar_t *)w_exe_path.c_str(), NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) { - EXLOGE(_T("CreateProcess() failed. Error=0x%08X.\n %s\n"), GetLastError(), w_exe_path.c_str()); - root_ret["code"] = TPE_START_CLIENT; - _create_json_ret(buf, root_ret); - return; - } - - root_ret["code"] = TPE_OK; - _create_json_ret(buf, root_ret); -} - -void TsHttpRpc::_rpc_func_rdp_play(const ex_astr& func_args, ex_astr& buf) { - Json::Reader jreader; - Json::Value jsRoot; - - if (!jreader.parse(func_args.c_str(), jsRoot)) { - _create_json_ret(buf, TPE_JSON_FORMAT); - return; - } - - // жϲǷȷ - if (!jsRoot["rid"].isInt() - || !jsRoot["web"].isString() - || !jsRoot["sid"].isString() - || !jsRoot["user"].isString() - || !jsRoot["acc"].isString() - || !jsRoot["host"].isString() - || !jsRoot["start"].isString() - ) { - _create_json_ret(buf, TPE_PARAM); - return; - } - - int rid = jsRoot["rid"].asInt(); - ex_astr a_url_base = jsRoot["web"].asCString(); - ex_astr a_sid = jsRoot["sid"].asCString(); - ex_astr a_user = jsRoot["user"].asCString(); - ex_astr a_acc = jsRoot["acc"].asCString(); - ex_astr a_host = jsRoot["host"].asCString(); - ex_astr a_start = jsRoot["start"].asCString(); - - char cmd_args[1024] = { 0 }; - ex_strformat(cmd_args, 1023, "%d \"%s\" \"%09d-%s-%s-%s-%s\"", rid, a_sid.c_str(), rid, a_user.c_str(), a_acc.c_str(), a_host.c_str(), a_start.c_str()); - - // TODO: ϲӦǰתΪIPIJӦý͸ɲԼȥ - // ڸFreeRDPIJʱΪ˴ӷļʹMongoose⣬⣨ò첽ѯDNS⣩ - // ʱֽIPת - { - unsigned int port_i = 0; - struct mg_str scheme, query, fragment, user_info, host, path; - - if (mg_parse_uri(mg_mk_str(a_url_base.c_str()), &scheme, &user_info, &host, &port_i, &path, &query, &fragment) != 0) { - EXLOGE(_T("parse url failed.\n")); - Json::Value root_ret; - root_ret["code"] = TPE_PARAM; - _create_json_ret(buf, root_ret); - return; - } - - ex_astr _scheme; - _scheme.assign(scheme.p, scheme.len); - - // hostתΪIP - ex_astr str_tp_host; - str_tp_host.assign(host.p, host.len); - struct hostent *tp_host = gethostbyname(str_tp_host.c_str()); - if (NULL == tp_host) { - EXLOGE(_T("resolve host name failed.\n")); - Json::Value root_ret; - root_ret["code"] = TPE_PARAM; - _create_json_ret(buf, root_ret); - return; - } - - int i = 0; - char* _ip = NULL; - if (tp_host->h_addrtype == AF_INET) { - struct in_addr addr; - while (tp_host->h_addr_list[i] != 0) { - addr.s_addr = *(u_long *)tp_host->h_addr_list[i++]; - _ip = inet_ntoa(addr); - break; - } - } - - if (NULL == _ip) { - EXLOGE(_T("resolve host name failed.\n")); - Json::Value root_ret; - root_ret["code"] = TPE_PARAM; - _create_json_ret(buf, root_ret); - return; - } - - char _url_base[256]; - ex_strformat(_url_base, 255, "%s://%s:%d", _scheme.c_str(), _ip, port_i); - a_url_base = _url_base; - } - - ex_wstr w_url_base; - ex_astr2wstr(a_url_base, w_url_base); - ex_wstr w_cmd_args; - ex_astr2wstr(cmd_args, w_cmd_args); - - ex_wstr w_exe_path; - w_exe_path = _T("\""); - w_exe_path += g_env.m_tools_path + _T("\\tprdp\\tprdp-replay.exe\""); - w_exe_path += _T(" \""); - w_exe_path += w_url_base; - w_exe_path += _T("\" "); - w_exe_path += w_cmd_args; - - Json::Value root_ret; - ex_astr utf8_path; - ex_wstr2astr(w_exe_path, utf8_path, EX_CODEPAGE_UTF8); - root_ret["cmdline"] = utf8_path; - - EXLOGD(w_exe_path.c_str()); - - STARTUPINFO si; - PROCESS_INFORMATION pi; - - ZeroMemory(&si, sizeof(si)); - si.cb = sizeof(si); - ZeroMemory(&pi, sizeof(pi)); - if (!CreateProcess(NULL, (wchar_t *)w_exe_path.c_str(), NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) { - EXLOGE(_T("CreateProcess() failed. Error=0x%08X.\n %s\n"), GetLastError(), w_exe_path.c_str()); - root_ret["code"] = TPE_START_CLIENT; - _create_json_ret(buf, root_ret); - return; - } - - root_ret["code"] = TPE_OK; - _create_json_ret(buf, root_ret); - return; -} - -void TsHttpRpc::_rpc_func_get_config(const ex_astr& func_args, ex_astr& buf) { - Json::Value jr_root; - jr_root["code"] = 0; - jr_root["data"] = g_cfg.get_root(); - _create_json_ret(buf, jr_root); -} - -void TsHttpRpc::_rpc_func_set_config(const ex_astr& func_args, ex_astr& buf) { - Json::Reader jreader; - Json::Value jsRoot; - if (!jreader.parse(func_args.c_str(), jsRoot)) { - _create_json_ret(buf, TPE_JSON_FORMAT); - return; - } - - if (!g_cfg.save(func_args)) - _create_json_ret(buf, TPE_FAILED); - else - _create_json_ret(buf, TPE_OK); -} - -void TsHttpRpc::_rpc_func_file_action(const ex_astr& func_args, ex_astr& buf) { - - Json::Reader jreader; - Json::Value jsRoot; - - if (!jreader.parse(func_args.c_str(), jsRoot)) { - _create_json_ret(buf, TPE_JSON_FORMAT); - return; - } - // жϲǷȷ - if (!jsRoot["action"].isNumeric()) { - _create_json_ret(buf, TPE_PARAM); - return; - } - int action = jsRoot["action"].asUInt(); - - HWND hParent = GetForegroundWindow(); - if (NULL == hParent) - hParent = g_hDlgMain; - - BOOL ret = FALSE; - wchar_t wszReturnPath[MAX_PATH] = _T(""); - - if (action == 1 || action == 2) { - OPENFILENAME ofn; - ex_wstr wsDefaultName; - ex_wstr wsDefaultPath; - StringCchCopy(wszReturnPath, MAX_PATH, wsDefaultName.c_str()); - - ZeroMemory(&ofn, sizeof(ofn)); - - ofn.lStructSize = sizeof(ofn); - ofn.lpstrTitle = _T("ѡļ"); - ofn.hwndOwner = hParent; - ofn.lpstrFilter = _T("ִг (*.exe)\0*.exe\0"); - ofn.lpstrFile = wszReturnPath; - ofn.nMaxFile = MAX_PATH; - ofn.lpstrInitialDir = wsDefaultPath.c_str(); - ofn.Flags = OFN_EXPLORER | OFN_PATHMUSTEXIST; - - if (action == 1) { - ofn.Flags |= OFN_FILEMUSTEXIST; - ret = GetOpenFileName(&ofn); - } else { - ofn.Flags |= OFN_OVERWRITEPROMPT; - ret = GetSaveFileName(&ofn); - } - } else if (action == 3) { - BROWSEINFO bi; - ZeroMemory(&bi, sizeof(BROWSEINFO)); - bi.hwndOwner = NULL; - bi.pidlRoot = NULL; - bi.pszDisplayName = wszReturnPath; //˲ΪNULLʾԻ - bi.lpszTitle = _T("ѡĿ¼"); - bi.ulFlags = BIF_RETURNONLYFSDIRS; - bi.lpfn = NULL; - bi.iImage = 0; //ʼڲbi - LPITEMIDLIST pIDList = SHBrowseForFolder(&bi);//ʾѡԻ - if (pIDList) { - ret = true; - SHGetPathFromIDList(pIDList, wszReturnPath); - } else { - ret = false; - } - } else if (action == 4) { - ex_wstr wsDefaultName; - ex_wstr wsDefaultPath; - - if (wsDefaultPath.length() == 0) { - _create_json_ret(buf, TPE_PARAM); - return; - } - - ex_wstr::size_type pos = 0; - - while (ex_wstr::npos != (pos = wsDefaultPath.find(L"/", pos))) { - wsDefaultPath.replace(pos, 1, L"\\"); - pos += 1; - } - - ex_wstr wArg = L"/select, \""; - wArg += wsDefaultPath; - wArg += L"\""; - if ((int)ShellExecute(hParent, _T("open"), _T("explorer"), wArg.c_str(), NULL, SW_SHOW) > 32) - ret = true; - else - ret = false; - } - - if (ret) { - if (action == 1 || action == 2 || action == 3) { - ex_astr utf8_path; - ex_wstr2astr(wszReturnPath, utf8_path, EX_CODEPAGE_UTF8); - Json::Value root; - root["code"] = TPE_OK; - root["path"] = utf8_path; - _create_json_ret(buf, root); - - return; - } else { - _create_json_ret(buf, TPE_OK); - return; - } - } else { - _create_json_ret(buf, TPE_DATA); - return; - } -} - -void TsHttpRpc::_rpc_func_get_version(const ex_astr& func_args, ex_astr& buf) { - Json::Value root_ret; - ex_wstr w_version = TP_ASSIST_VER; - ex_astr version; - ex_wstr2astr(w_version, version, EX_CODEPAGE_UTF8); - root_ret["version"] = version; - root_ret["code"] = TPE_OK; - _create_json_ret(buf, root_ret); - return; -} +#include "stdafx.h" + +#pragma warning(disable:4091) + +#include +#include +#include + +#pragma comment(lib, "Crypt32.lib") + +#include + +#include "ts_http_rpc.h" +#include "dlg_main.h" +#include "ts_ver.h" +#include "ts_env.h" + +/* +1. +SecureCRT支持设置标签页的标题,命令行参数 /N "tab name"就可以 +Example: +To launch a new Telnet session, displaying the name "Houston, TX" on the tab, use the following: +/T /N "Houston, TX" /TELNET 192.168.0.6 + +2. +多次启动的SecureCRT放到一个窗口的不同标签页中,使用参数: /T + SecureCRT.exe /T /N "TP#ssh://192.168.1.3" /SSH2 /L root /PASSWORD 1234 120.26.109.25 + +3. +telnet客户端的启动: + putty.exe telnet://administrator@127.0.0.1:52389 +如果是SecureCRT,则需要 + SecureCRT.exe /T /N "TP#telnet://192.168.1.3" /SCRIPT X:\path\to\startup.vbs /TELNET 127.0.0.1 52389 +其中,startup.vbs的内容为: +---------文件开始--------- +#$language = "VBScript" +#$interface = "1.0" +Sub main + crt.Screen.Synchronous = True + crt.Screen.WaitForString "ogin: " + crt.Screen.Send "SESSION-ID" & VbCr + crt.Screen.Synchronous = False +End Sub +---------文件结束--------- + +4. 为了让putty的窗口标签显示正常的IP,可以尝试在连接成功后,主动向服务端发送下列命令: + PS1="\[\e]0;${debian_chroot:+($debian_chroot)}\u@192.168.1.2: \w\a\]$PS1" +手工测试了,ubuntu服务器可以,不知道是否能够支持所有的Linux。SecureCRT对此表示忽略。 +*/ + +//#define RDP_CLIENT_SYSTEM_BUILTIN +// #define RDP_CLIENT_SYSTEM_ACTIVE_CONTROL +//#define RDP_CLIENT_FREERDP + + +//#ifdef RDP_CLIENT_SYSTEM_BUILTIN + +//connect to console:i:%d +//compression:i:1 +//bitmapcachepersistenable:i:1 + +std::string rdp_content = "\ +administrative session:i:%d\n\ +screen mode id:i:%d\n\ +use multimon:i:0\n\ +desktopwidth:i:%d\n\ +desktopheight:i:%d\n\ +session bpp:i:16\n\ +winposstr:s:0,1,%d,%d,%d,%d\n\ +bitmapcachepersistenable:i:1\n\ +bitmapcachesize:i:32000\n\ +compression:i:1\n\ +keyboardhook:i:2\n\ +audiocapturemode:i:0\n\ +videoplaybackmode:i:1\n\ +connection type:i:7\n\ +networkautodetect:i:1\n\ +bandwidthautodetect:i:1\n\ +disableclipboardredirection:i:0\n\ +displayconnectionbar:i:1\n\ +enableworkspacereconnect:i:0\n\ +disable wallpaper:i:1\n\ +allow font smoothing:i:0\n\ +allow desktop composition:i:0\n\ +disable full window drag:i:1\n\ +disable menu anims:i:1\n\ +disable themes:i:1\n\ +disable cursor setting:i:1\n\ +full address:s:%s:%d\n\ +audiomode:i:0\n\ +redirectprinters:i:0\n\ +redirectcomports:i:0\n\ +redirectsmartcards:i:0\n\ +redirectclipboard:i:%d\n\ +redirectposdevices:i:0\n\ +autoreconnection enabled:i:0\n\ +authentication level:i:2\n\ +prompt for credentials:i:0\n\ +negotiate security layer:i:1\n\ +remoteapplicationmode:i:0\n\ +alternate shell:s:\n\ +shell working directory:s:\n\ +gatewayhostname:s:\n\ +gatewayusagemethod:i:4\n\ +gatewaycredentialssource:i:4\n\ +gatewayprofileusagemethod:i:0\n\ +promptcredentialonce:i:0\n\ +gatewaybrokeringtype:i:0\n\ +use redirection server name:i:0\n\ +rdgiskdcproxy:i:0\n\ +kdcproxyname:s:\n\ +drivestoredirect:s:%s\n\ +username:s:%s\n\ +password 51:b:%s\n\ +"; + +// https://www.donkz.nl/overview-rdp-file-settings/ +// +// authentication level:i:2\n +// +// +// negotiate security layer:i:1\n +// 0 = negotiation is not enabled and the session is started by using Secure Sockets Layer (SSL). +// 1 = negotiation is enabled and the session is started by using x.224 encryption. + + + +//redirectdirectx:i:0\n\ +//prompt for credentials on client:i:0\n\ + +//#endif + + +TsHttpRpc g_http_interface; +TsHttpRpc g_https_interface; + +void http_rpc_main_loop(bool is_https) { + if (is_https) { + if (!g_https_interface.init_https()) { + EXLOGE("[ERROR] can not start HTTPS-RPC listener, maybe port %d is already in use.\n", TS_HTTPS_RPC_PORT); + return; + } + + EXLOGW("======================================================\n"); + EXLOGW("[rpc] TeleportAssist-HTTPS-RPC ready on 127.0.0.1:%d\n", TS_HTTPS_RPC_PORT); + + g_https_interface.run(); + + EXLOGW("[rpc] HTTPS-Server main loop end.\n"); + } else { + if (!g_http_interface.init_http()) { + EXLOGE("[ERROR] can not start HTTP-RPC listener, maybe port %d is already in use.\n", TS_HTTP_RPC_PORT); + return; + } + + EXLOGW("======================================================\n"); + EXLOGW("[rpc] TeleportAssist-HTTP-RPC ready on 127.0.0.1:%d\n", TS_HTTP_RPC_PORT); + + g_http_interface.run(); + + EXLOGW("[rpc] HTTP-Server main loop end.\n"); + } +} + +void http_rpc_stop(bool is_https) { + if (is_https) + g_https_interface.stop(); + else + g_http_interface.stop(); +} + +#define HEXTOI(x) (isdigit(x) ? x - '0' : x - 'W') + +int ts_url_decode(const char *src, int src_len, char *dst, int dst_len, int is_form_url_encoded) { + int i, j, a, b; + + for (i = j = 0; i < src_len && j < dst_len - 1; i++, j++) { + if (src[i] == '%') { + if (i < src_len - 2 && isxdigit(*(const unsigned char *)(src + i + 1)) && + isxdigit(*(const unsigned char *)(src + i + 2))) { + a = tolower(*(const unsigned char *)(src + i + 1)); + b = tolower(*(const unsigned char *)(src + i + 2)); + dst[j] = (char)((HEXTOI(a) << 4) | HEXTOI(b)); + i += 2; + } else { + return -1; + } + } else if (is_form_url_encoded && src[i] == '+') { + dst[j] = ' '; + } else { + dst[j] = src[i]; + } + } + + dst[j] = '\0'; /* Null-terminate the destination */ + + return i >= src_len ? j : -1; +} + +bool calc_psw51b(const char* password, std::string& ret) { + DATA_BLOB DataIn; + DATA_BLOB DataOut; + + ex_wstr w_pswd; + ex_astr2wstr(password, w_pswd, EX_CODEPAGE_ACP); + + DataIn.cbData = w_pswd.length() * sizeof(wchar_t); + DataIn.pbData = (BYTE*)w_pswd.c_str(); + + + if (!CryptProtectData(&DataIn, L"psw", nullptr, nullptr, nullptr, 0, &DataOut)) + return false; + + char szRet[5] = { 0 }; + for (DWORD i = 0; i < DataOut.cbData; ++i) { + sprintf_s(szRet, 5, "%02X", DataOut.pbData[i]); + ret += szRet; + } + + LocalFree(DataOut.pbData); + return true; +} + +bool isDegital(std::string str) { + for (int i = 0; i < str.size(); i++) { + if (str.at(i) == '-' && str.size() > 1) // 有可能出现负数 + continue; + if (str.at(i) > '9' || str.at(i) < '0') + return false; + } + return true; +} + +std::string strtolower(std::string str) { + for (int i = 0; i < str.size(); i++) + { + str[i] = tolower(str[i]); + } + return str; +} + +void SplitString(const std::string& s, std::vector& v, const std::string& c) +{ + std::string::size_type pos1, pos2; + pos2 = s.find(c); + pos1 = 0; + while (std::string::npos != pos2) + { + v.push_back(s.substr(pos1, pos2 - pos1)); + + pos1 = pos2 + c.size(); + pos2 = s.find(c, pos1); + } + if (pos1 != s.length()) + v.push_back(s.substr(pos1)); +} + +TsHttpRpc::TsHttpRpc() { + m_stop = false; + mg_mgr_init(&m_mg_mgr, nullptr); +} + +TsHttpRpc::~TsHttpRpc() { + mg_mgr_free(&m_mg_mgr); +} + +bool TsHttpRpc::init_http() { + struct mg_connection* nc = nullptr; + + char addr[128] = { 0 }; + ex_strformat(addr, 128, "tcp://127.0.0.1:%d", TS_HTTP_RPC_PORT); + + nc = mg_bind(&m_mg_mgr, addr, _mg_event_handler); + if (!nc) { + EXLOGE("[rpc] TsHttpRpc::init 127.0.0.1:%d\n", TS_HTTP_RPC_PORT); + return false; + } + nc->user_data = this; + + mg_set_protocol_http_websocket(nc); + + return _on_init(); +} + +bool TsHttpRpc::init_https() { + ex_wstr file_ssl_cert = g_env.m_exec_path; + ex_path_join(file_ssl_cert, true, L"cfg", L"localhost.pem", NULL); + ex_wstr file_ssl_key = g_env.m_exec_path; + ex_path_join(file_ssl_key, true, L"cfg", L"localhost.key", NULL); + ex_astr _ssl_cert; + ex_wstr2astr(file_ssl_cert, _ssl_cert); + ex_astr _ssl_key; + ex_wstr2astr(file_ssl_key, _ssl_key); + + const char *err = NULL; + struct mg_bind_opts bind_opts; + memset(&bind_opts, 0, sizeof(bind_opts)); + bind_opts.ssl_cert = _ssl_cert.c_str(); + bind_opts.ssl_key = _ssl_key.c_str(); + bind_opts.error_string = &err; + + + char addr[128] = { 0 }; + ex_strformat(addr, 128, "tcp://127.0.0.1:%d", TS_HTTPS_RPC_PORT); + + struct mg_connection* nc = nullptr; + nc = mg_bind_opt(&m_mg_mgr, addr, _mg_event_handler, bind_opts); + if (!nc) { + EXLOGE("[rpc] TsHttpRpc::init 127.0.0.1:%d\n", TS_HTTPS_RPC_PORT); + return false; + } + nc->user_data = this; + + mg_set_protocol_http_websocket(nc); + + return _on_init(); +} + +bool TsHttpRpc::_on_init() { + char file_name[MAX_PATH] = { 0 }; + if (!GetModuleFileNameA(nullptr, file_name, MAX_PATH)) + return false; + + int len = strlen(file_name); + + if (file_name[len] == '\\') + file_name[len] = '\0'; + + char* match = strrchr(file_name, '\\'); + if (match) + *match = '\0'; + + m_content_type_map[".js"] = "application/javascript"; + m_content_type_map[".png"] = "image/png"; + m_content_type_map[".jpeg"] = "image/jpeg"; + m_content_type_map[".jpg"] = "image/jpeg"; + m_content_type_map[".gif"] = "image/gif"; + m_content_type_map[".ico"] = "image/x-icon"; + m_content_type_map[".json"] = "image/json"; + m_content_type_map[".html"] = "text/html"; + m_content_type_map[".css"] = "text/css"; + m_content_type_map[".tif"] = "image/tiff"; + m_content_type_map[".tiff"] = "image/tiff"; + m_content_type_map[".svg"] = "text/html"; + + return true; +} + +void TsHttpRpc::run(void) { + while (!m_stop) { + mg_mgr_poll(&m_mg_mgr, 500); + } +} + +void TsHttpRpc::stop(void) { + m_stop = true; +} + +void TsHttpRpc::_mg_event_handler(struct mg_connection *nc, int ev, void *ev_data) { + struct http_message *hm = (struct http_message*)ev_data; + + TsHttpRpc* _this = (TsHttpRpc*)nc->user_data; + if (!_this) { + EXLOGE("[ERROR] invalid http request.\n"); + return; + } + + switch (ev) { + case MG_EV_HTTP_REQUEST: + { + ex_astr uri; + ex_chars _uri; + _uri.resize(hm->uri.len + 1); + memset(&_uri[0], 0, hm->uri.len + 1); + memcpy(&_uri[0], hm->uri.p, hm->uri.len); + uri = &_uri[0]; + +#ifdef EX_DEBUG + char* dbg_method = nullptr; + if (hm->method.len == 3 && 0 == memcmp(hm->method.p, "GET", hm->method.len)) + dbg_method = "GET"; + else if (hm->method.len == 4 && 0 == memcmp(hm->method.p, "POST", hm->method.len)) + dbg_method = "POST"; + else + dbg_method = "UNSUPPORTED-HTTP-METHOD"; + + EXLOGV("[rpc] got %s request: %s\n", dbg_method, uri.c_str()); +#endif + ex_astr ret_buf; + bool b_is_html = false; + +// if (uri == "/") { +// ex_wstr page = L"Teleport助手\n\n
    Teleport助手工作正常!
    "; +// ex_wstr2astr(page, ret_buf, EX_CODEPAGE_UTF8); +// +// mg_printf(nc, "HTTP/1.0 200 OK\r\nAccess-Control-Allow-Origin: *\r\nContent-Length: %d\r\nContent-Type: text/html\r\n\r\n%s", ret_buf.size() - 1, &ret_buf[0]); +// nc->flags |= MG_F_SEND_AND_CLOSE; +// return; +// } + + if (uri == "/") { + uri = "/status.html"; + b_is_html = true; + } + else if (uri == "/config") { + uri = "/index.html"; + b_is_html = true; + } + + ex_astr temp; + int offset = uri.find("/", 1); + if (offset > 0) { + temp = uri.substr(1, offset - 1); + + if (temp == "api") { + ex_astr method; + ex_astr json_param; + int rv = _this->_parse_request(hm, method, json_param); + if (0 != rv) { + EXLOGE("[ERROR] http-rpc got invalid request.\n"); + _this->_create_json_ret(ret_buf, rv); + } else { + _this->_process_js_request(method, json_param, ret_buf); + } + + mg_printf(nc, "HTTP/1.0 200 OK\r\nAccess-Control-Allow-Origin: *\r\nContent-Length: %ld\r\nContent-Type: application/json\r\n\r\n%s", ret_buf.length(), &ret_buf[0]); + nc->flags |= MG_F_SEND_AND_CLOSE; + return; + } + } + + + ex_astr file_suffix; + offset = uri.rfind("."); + if (offset > 0) { + file_suffix = uri.substr(offset, uri.length()); + } + + ex_wstr2astr(g_env.m_site_path, temp); + ex_astr index_path = temp + uri; + + + FILE* file = ex_fopen(index_path.c_str(), "rb"); + if (file) { + unsigned long file_size = 0; + char* buf = nullptr; + size_t ret = 0; + + fseek(file, 0, SEEK_END); + file_size = ftell(file); + buf = new char[file_size]; + memset(buf, 0, file_size); + fseek(file, 0, SEEK_SET); + ret = fread(buf, 1, file_size, file); + fclose(file); + + ex_astr content_type = _this->get_content_type(file_suffix); + + mg_printf(nc, "HTTP/1.0 200 OK\r\nAccess-Control-Allow-Origin: *\r\nContent-Length: %ld\r\nContent-Type: %s\r\n\r\n", file_size, content_type.c_str()); + mg_send(nc, buf, (int)file_size); + delete[]buf; + nc->flags |= MG_F_SEND_AND_CLOSE; + return; + } else if (b_is_html) { + ex_wstr page = L"404 Not Found

    404 Not Found


    Teleport Assistor configuration page not found.

    "; + ex_wstr2astr(page, ret_buf, EX_CODEPAGE_UTF8); + + mg_printf(nc, "HTTP/1.0 404 File Not Found\r\nAccess-Control-Allow-Origin: *\r\nContent-Length: %ld\r\nContent-Type: text/html\r\n\r\n%s", ret_buf.size() - 1, &ret_buf[0]); + nc->flags |= MG_F_SEND_AND_CLOSE; + return; + } + + } + break; + default: + break; + } +} + +int TsHttpRpc::_parse_request(struct http_message* req, ex_astr& func_cmd, ex_astr& func_args) { + if (!req) + return TPE_FAILED; + + bool is_get = true; + if (req->method.len == 3 && 0 == memcmp(req->method.p, "GET", req->method.len)) + is_get = true; + else if (req->method.len == 4 && 0 == memcmp(req->method.p, "POST", req->method.len)) + is_get = false; + else + return TPE_HTTP_METHOD; + + ex_astrs strs; + + size_t pos_start = 1; // 跳过第一个字节,一定是 '/' + + size_t i = 0; + for (i = pos_start; i < req->uri.len; ++i) { + if (req->uri.p[i] == '/') { + if (i - pos_start > 0) { + ex_astr tmp_uri; + tmp_uri.assign(req->uri.p + pos_start, i - pos_start); + strs.push_back(tmp_uri); + } + pos_start = i + 1; // 跳过当前找到的分隔符 + } + } + if (pos_start < req->uri.len) { + ex_astr tmp_uri; + tmp_uri.assign(req->uri.p + pos_start, req->uri.len - pos_start); + strs.push_back(tmp_uri); + } + + if (strs.empty() || strs[0] != "api") + return TPE_PARAM; + + if (is_get) { + if (2 == strs.size()) { + func_cmd = strs[1]; + } else if (3 == strs.size()) { + func_cmd = strs[1]; + func_args = strs[2]; + } else { + return TPE_PARAM; + } + } else { + if (2 == strs.size()) { + func_cmd = strs[1]; + } else { + return TPE_PARAM; + } + + if (req->body.len > 0) { + func_args.assign(req->body.p, req->body.len); + } + } + + if (func_args.length() > 0) { + // 将参数进行 url-decode 解码 + int len = func_args.length() * 2; + ex_chars sztmp; + sztmp.resize(len); + memset(&sztmp[0], 0, len); + if (-1 == ts_url_decode(func_args.c_str(), func_args.length(), &sztmp[0], len, 0)) + return TPE_HTTP_URL_ENCODE; + + func_args = &sztmp[0]; + } + + EXLOGV("[rpc] method=%s, json_param=%s\n", func_cmd.c_str(), func_args.c_str()); + + return TPE_OK; +} + +void TsHttpRpc::_process_js_request(const ex_astr& func_cmd, const ex_astr& func_args, ex_astr& buf) { + if (func_cmd == "get_version") { + _rpc_func_get_version(func_args, buf); + } else if (func_cmd == "run") { + _rpc_func_run_client(func_args, buf); + } else if (func_cmd == "rdp_play") { + _rpc_func_rdp_play(func_args, buf); + } else if (func_cmd == "get_config") { + _rpc_func_get_config(func_args, buf); + } else if (func_cmd == "set_config") { + _rpc_func_set_config(func_args, buf); + } else if (func_cmd == "file_action") { + _rpc_func_file_action(func_args, buf); + } else { + EXLOGE("[rpc] got unknown command: %s\n", func_cmd.c_str()); + _create_json_ret(buf, TPE_UNKNOWN_CMD); + } +} + +void TsHttpRpc::_create_json_ret(ex_astr& buf, int errcode) { + // 返回: {"code":123} + + Json::Value jr_root; + jr_root["code"] = errcode; + + // buf = jr_writer.write(jr_root); + Json::StreamWriterBuilder jwb; + std::unique_ptr jwriter(jwb.newStreamWriter()); + ex_aoss os; + jwriter->write(jr_root, &os); + buf = os.str(); +} + +void TsHttpRpc::_create_json_ret(ex_astr& buf, Json::Value& jr_root) { +// Json::FastWriter jr_writer; +// buf = jr_writer.write(jr_root); + Json::StreamWriterBuilder jwb; + std::unique_ptr jwriter(jwb.newStreamWriter()); + ex_aoss os; + jwriter->write(jr_root, &os); + buf = os.str(); +} + +void TsHttpRpc::_rpc_func_url_protocol(const ex_astr& args, ex_astr& buf) +{ + //处理urlprotocol调用访式 + // 将参数进行 url-decode 解码 + std::string func_args = args; + if (func_args.length() > 0) + { + int len = func_args.length() * 2; + ex_chars sztmp; + sztmp.resize(len); + memset(&sztmp[0], 0, len); + if (-1 == ts_url_decode(func_args.c_str(), func_args.length(), &sztmp[0], len, 0)) + return ; + + func_args = &sztmp[0]; + } + EXLOGD(("%s\n"), func_args.c_str()); + //处理传参过来的teleport://{}/,只保留参数部份 + std::string urlproto_appname = TP_URLPROTO_APP_NAME; + urlproto_appname += "://{"; + func_args.erase(0, urlproto_appname.length());//去除第一个URLPROTO_APP_NAME以及://字符 + int pos = func_args.length() - 1; + if (func_args.substr(pos, 1) == "/") + func_args.erase(pos - 1, 2);//去除最后一个}/字符 + else + func_args.erase(pos, 1); + + //由于命令行、ie浏览器参数传递时会把原来json结构中的"号去掉,需要重新格式化参数为json格式 + if (func_args.find("\"", 0) == std::string::npos) { + std::vector strv; + SplitString(func_args, strv, ","); + func_args = ""; + for (std::vector::size_type i = 0; i < strv.size(); i++) { + std::vector strv1; + SplitString(strv[i], strv1, ":"); + strv1[0] = "\"" + strv1[0] + "\""; + if (!isDegital(strv1[1]) && strtolower(strv1[1]) != "true" && strtolower(strv1[1]) != "false") + strv1[1] = "\"" + strv1[1] + "\""; + + strv[i] = strv1[0] + ":" + strv1[1]; + if (i == 0) + func_args = strv[i]; + else + func_args += "," + strv[i]; + } + } + func_args = "{" + func_args + "}"; + EXLOGD(("%s\n"), func_args.c_str()); + //调用TsHttpRpc类里的_rpc_func_run_client启动客户端 + _rpc_func_run_client(func_args, buf); +} + +void TsHttpRpc::_rpc_func_run_client(const ex_astr& func_args, ex_astr& buf) { + // 入参:{"ip":"192.168.5.11","port":22,"uname":"root","uauth":"abcdefg","authmode":1,"protocol":2} + // authmode: 1=password, 2=private-key + // protocol: 1=rdp, 2=ssh + // SSH返回: {"code":0, "data":{"sid":"0123abcde"}} + // RDP返回: {"code":0, "data":{"sid":"0123abcde0A"}} + + //Json::Reader jreader; + Json::Value jsRoot; + + Json::CharReaderBuilder jcrb; + std::unique_ptr const jreader(jcrb.newCharReader()); + const char *str_json_begin = func_args.c_str(); + ex_astr err; + + //if (!jreader.parse(func_args.c_str(), jsRoot)) { + if (!jreader->parse(str_json_begin, str_json_begin + func_args.length(), &jsRoot, &err)) { + _create_json_ret(buf, TPE_JSON_FORMAT); + return; + } + if (!jsRoot.isObject()) { + _create_json_ret(buf, TPE_PARAM); + return; + } + + // 判断参数是否正确 + if (!jsRoot["teleport_ip"].isString() + || !jsRoot["teleport_port"].isNumeric() || !jsRoot["remote_host_ip"].isString() + || !jsRoot["session_id"].isString() || !jsRoot["protocol_type"].isNumeric() || !jsRoot["protocol_sub_type"].isNumeric() + || !jsRoot["protocol_flag"].isNumeric() + ) { + _create_json_ret(buf, TPE_PARAM); + return; + } + + int pro_type = jsRoot["protocol_type"].asUInt(); + int pro_sub = jsRoot["protocol_sub_type"].asInt(); + ex_u32 protocol_flag = jsRoot["protocol_flag"].asUInt(); + + ex_astr teleport_ip = jsRoot["teleport_ip"].asCString(); + int teleport_port = jsRoot["teleport_port"].asUInt(); + + ex_astr remote_host_name = jsRoot["remote_host_name"].asCString(); + + ex_astr real_host_ip = jsRoot["remote_host_ip"].asCString(); + ex_astr sid = jsRoot["session_id"].asCString(); + + ex_wstr w_exe_path; + WCHAR w_szCommandLine[MAX_PATH] = { 0 }; + + + ex_wstr w_sid; + ex_astr2wstr(sid, w_sid); + ex_wstr w_teleport_ip; + ex_astr2wstr(teleport_ip, w_teleport_ip); + ex_wstr w_real_host_ip; + ex_astr2wstr(real_host_ip, w_real_host_ip); + ex_wstr w_remote_host_name; + ex_astr2wstr(remote_host_name, w_remote_host_name); + WCHAR w_port[32] = { 0 }; + swprintf_s(w_port, _T("%d"), teleport_port); + + ex_wstr tmp_rdp_file; // for .rdp file + + if (pro_type == TP_PROTOCOL_TYPE_RDP) { + //============================================== + // RDP + //============================================== + + bool flag_clipboard = ((protocol_flag & TP_FLAG_RDP_CLIPBOARD) == TP_FLAG_RDP_CLIPBOARD); + bool flag_disk = ((protocol_flag & TP_FLAG_RDP_DISK) == TP_FLAG_RDP_DISK); + bool flag_console = ((protocol_flag & TP_FLAG_RDP_CONSOLE) == TP_FLAG_RDP_CONSOLE); + + int rdp_w = 800; + int rdp_h = 640; + bool rdp_console = false; + + if (!jsRoot["rdp_width"].isNull()) { + if (jsRoot["rdp_width"].isNumeric()) { + rdp_w = jsRoot["rdp_width"].asUInt(); + } else { + _create_json_ret(buf, TPE_PARAM); + return; + } + } + + if (!jsRoot["rdp_height"].isNull()) { + if (jsRoot["rdp_height"].isNumeric()) { + rdp_h = jsRoot["rdp_height"].asUInt(); + } else { + _create_json_ret(buf, TPE_PARAM); + return; + } + } + + if (!jsRoot["rdp_console"].isNull()) { + if (jsRoot["rdp_console"].isBool()) { + rdp_console = jsRoot["rdp_console"].asBool(); + } else { + _create_json_ret(buf, TPE_PARAM); + return; + } + } + + if (!flag_console) + rdp_console = false; + + + int split_pos = sid.length() - 2; + ex_astr real_sid = sid.substr(0, split_pos); + ex_astr str_pwd_len = sid.substr(split_pos, sid.length()); + int n_pwd_len = strtol(str_pwd_len.c_str(), nullptr, 16); + n_pwd_len -= real_sid.length(); + n_pwd_len -= 2; + char szPwd[256] = { 0 }; + for (int i = 0; i < n_pwd_len; i++) { + szPwd[i] = '*'; + } + + ex_astr2wstr(real_sid, w_sid); + + w_exe_path = _T("\""); + w_exe_path += g_cfg.rdp_app + _T("\" "); + + ex_wstr rdp_name = g_cfg.rdp_name; + if (rdp_name == L"mstsc") { + w_exe_path += g_cfg.rdp_cmdline; + + int width = 0; + int higth = 0; + int cx = 0; + int cy = 0; + + int display = 1; + int iWidth = GetSystemMetrics(SM_CXSCREEN); + int iHeight = GetSystemMetrics(SM_CYSCREEN); + + if (rdp_w == 0 || rdp_h == 0) { + //全屏 + width = iWidth; + higth = iHeight; + display = 2; + } else { + width = rdp_w; + higth = rdp_h; + display = 1; + } + + cx = (iWidth - width) / 2; + cy = (iHeight - higth) / 2; + if (cx < 0) { + cx = 0; + } + if (cy < 0) { + cy = 0; + } + + // int console_mode = 0; + // if (rdp_console) + // console_mode = 1; + + std::string psw51b; + if (!calc_psw51b(szPwd, psw51b)) { + EXLOGE("calc password failed.\n"); + _create_json_ret(buf, TPE_FAILED); + return; + } + + real_sid = "01" + real_sid; + + char sz_rdp_file_content[4096] = { 0 }; + sprintf_s(sz_rdp_file_content, 4096, rdp_content.c_str() + , (flag_console && rdp_console) ? 1 : 0 + , display, width, higth + , cx, cy, cx + width + 100, cy + higth + 100 + , teleport_ip.c_str(), teleport_port + , flag_clipboard ? 1 : 0 + , flag_disk ? "*" : "" + , real_sid.c_str() + , psw51b.c_str() + ); + + char sz_file_name[MAX_PATH] = { 0 }; + char temp_path[MAX_PATH] = { 0 }; + DWORD ret = GetTempPathA(MAX_PATH, temp_path); + if (ret <= 0) { + EXLOGE("fopen failed (%d).\n", GetLastError()); + _create_json_ret(buf, TPE_FAILED); + return; + } + + ex_astr temp_host_ip = real_host_ip; + ex_replace_all(temp_host_ip, ".", "-"); + + sprintf_s(sz_file_name, MAX_PATH, ("%s%s.rdp"), temp_path, temp_host_ip.c_str()); + + FILE* f = NULL; + if (fopen_s(&f, sz_file_name, "wt") != 0) { + EXLOGE("fopen failed (%d).\n", GetLastError()); + _create_json_ret(buf, TPE_OPENFILE); + return; + } + // Write a string into the file. + fwrite(sz_rdp_file_content, strlen(sz_rdp_file_content), 1, f); + fclose(f); + ex_astr2wstr(sz_file_name, tmp_rdp_file); + + // 变量替换 + ex_replace_all(w_exe_path, _T("{tmp_rdp_file}"), tmp_rdp_file); + } else if (g_cfg.rdp_name == L"freerdp") { + w_exe_path += L"{size} {console} {clipboard} {drives} "; + w_exe_path += g_cfg.rdp_cmdline; + + ex_wstr w_screen; + + if (rdp_w == 0 || rdp_h == 0) { + //全屏 + w_screen = _T("/f"); + } else { + char sz_size[64] = { 0 }; + ex_strformat(sz_size, 63, "/size:%dx%d", rdp_w, rdp_h); + ex_astr2wstr(sz_size, w_screen); + } + + // wchar_t* w_console = NULL; + // + // if (flag_console && rdp_console) + // { + // w_console = L"/admin"; + // } + // else + // { + // w_console = L""; + // } + + ex_wstr w_password; + ex_astr2wstr(szPwd, w_password); + w_exe_path += L" /p:"; + w_exe_path += w_password; + + w_sid = L"02" + w_sid; + + w_exe_path += L" /gdi:sw"; // 使用软件渲染,gdi:hw使用硬件加速,但是会出现很多黑块(录像回放时又是正常的!) + w_exe_path += L" -grab-keyboard"; // [new style] 防止启动FreeRDP后,失去本地键盘响应,必须得先最小化一下FreeRDP窗口(不过貌似不起作用) + + // 变量替换 + ex_replace_all(w_exe_path, _T("{size}"), w_screen); + + if (flag_console && rdp_console) + ex_replace_all(w_exe_path, _T("{console}"), L"/admin"); + else + ex_replace_all(w_exe_path, _T("{console}"), L""); + + //ex_replace_all(w_exe_path, _T("{clipboard}"), L"+clipboard"); + + if (flag_clipboard) + ex_replace_all(w_exe_path, _T("{clipboard}"), L"/clipboard"); + else + ex_replace_all(w_exe_path, _T("{clipboard}"), L"-clipboard"); + + if (flag_disk) + ex_replace_all(w_exe_path, _T("{drives}"), L"/drives"); + else + ex_replace_all(w_exe_path, _T("{drives}"), L"-drives"); + } else { + _create_json_ret(buf, TPE_FAILED); + return; + } + } else if (pro_type == TP_PROTOCOL_TYPE_SSH) { + //============================================== + // SSH + //============================================== + + if (pro_sub == TP_PROTOCOL_TYPE_SSH_SHELL) { + w_exe_path = _T("\""); + w_exe_path += g_cfg.ssh_app + _T("\" "); + w_exe_path += g_cfg.ssh_cmdline; + } else { + w_exe_path = _T("\""); + w_exe_path += g_cfg.scp_app + _T("\" "); + w_exe_path += g_cfg.scp_cmdline; + } + } else if (pro_type == TP_PROTOCOL_TYPE_TELNET) { + //============================================== + // TELNET + //============================================== + w_exe_path = _T("\""); + w_exe_path += g_cfg.telnet_app + _T("\" "); + w_exe_path += g_cfg.telnet_cmdline; + } + + ex_replace_all(w_exe_path, _T("{host_ip}"), w_teleport_ip.c_str()); + ex_replace_all(w_exe_path, _T("{host_port}"), w_port); + ex_replace_all(w_exe_path, _T("{user_name}"), w_sid.c_str()); + ex_replace_all(w_exe_path, _T("{host_name}"), w_remote_host_name.c_str()); + ex_replace_all(w_exe_path, _T("{real_ip}"), w_real_host_ip.c_str()); + ex_replace_all(w_exe_path, _T("{assist_tools_path}"), g_env.m_tools_path.c_str()); + + + STARTUPINFO si; + PROCESS_INFORMATION pi; + + ZeroMemory(&si, sizeof(si)); + si.cb = sizeof(si); + ZeroMemory(&pi, sizeof(pi)); + + Json::Value root_ret; + ex_astr utf8_path; + ex_wstr2astr(w_exe_path, utf8_path, EX_CODEPAGE_UTF8); + root_ret["path"] = utf8_path; + + if (!CreateProcess(NULL, (wchar_t *)w_exe_path.c_str(), NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) { + EXLOGE(_T("CreateProcess() failed. Error=0x%08X.\n %s\n"), GetLastError(), w_exe_path.c_str()); + root_ret["code"] = TPE_START_CLIENT; + _create_json_ret(buf, root_ret); + return; + } + + root_ret["code"] = TPE_OK; + _create_json_ret(buf, root_ret); +} + +void TsHttpRpc::_rpc_func_rdp_play(const ex_astr& func_args, ex_astr& buf) { + //Json::Reader jreader; + Json::Value jsRoot; + + Json::CharReaderBuilder jcrb; + std::unique_ptr const jreader(jcrb.newCharReader()); + const char *str_json_begin = func_args.c_str(); + ex_astr err; + + //if (!jreader.parse(func_args.c_str(), jsRoot)) { + if (!jreader->parse(str_json_begin, str_json_begin + func_args.length(), &jsRoot, &err)) { + _create_json_ret(buf, TPE_JSON_FORMAT); + return; + } + + // 判断参数是否正确 + if (!jsRoot["rid"].isInt() + || !jsRoot["web"].isString() + || !jsRoot["sid"].isString() + ) { + _create_json_ret(buf, TPE_PARAM); + return; + } + + int rid = jsRoot["rid"].asInt(); + ex_astr a_url_base = jsRoot["web"].asCString(); + ex_astr a_sid = jsRoot["sid"].asCString(); + + char cmd_args[1024] = { 0 }; + ex_strformat(cmd_args, 1023, "%s/%d", a_sid.c_str(), rid); + + ex_wstr w_url_base; + ex_astr2wstr(a_url_base, w_url_base); + ex_wstr w_cmd_args; + ex_astr2wstr(cmd_args, w_cmd_args); + + ex_wstr w_exe_path; + w_exe_path = _T("\""); + w_exe_path += g_env.m_exec_path + _T("\\tp-player.exe\""); + w_exe_path += _T(" \""); + w_exe_path += w_url_base; + w_exe_path += _T("/"); + w_exe_path += w_cmd_args; + + Json::Value root_ret; + ex_astr utf8_path; + ex_wstr2astr(w_exe_path, utf8_path, EX_CODEPAGE_UTF8); + root_ret["cmdline"] = utf8_path; + + EXLOGD(w_exe_path.c_str()); + + STARTUPINFO si; + PROCESS_INFORMATION pi; + + ZeroMemory(&si, sizeof(si)); + si.cb = sizeof(si); + ZeroMemory(&pi, sizeof(pi)); + if (!CreateProcess(NULL, (wchar_t *)w_exe_path.c_str(), NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) { + EXLOGE(_T("CreateProcess() failed. Error=0x%08X.\n %s\n"), GetLastError(), w_exe_path.c_str()); + root_ret["code"] = TPE_START_CLIENT; + _create_json_ret(buf, root_ret); + return; + } + + root_ret["code"] = TPE_OK; + _create_json_ret(buf, root_ret); + return; +} + +void TsHttpRpc::_rpc_func_get_config(const ex_astr& func_args, ex_astr& buf) { + Json::Value jr_root; + jr_root["code"] = 0; + jr_root["data"] = g_cfg.get_root(); + _create_json_ret(buf, jr_root); +} + +void TsHttpRpc::_rpc_func_set_config(const ex_astr& func_args, ex_astr& buf) { + //Json::Reader jreader; + Json::Value jsRoot; + Json::CharReaderBuilder jcrb; + std::unique_ptr const jreader(jcrb.newCharReader()); + const char *str_json_begin = func_args.c_str(); + ex_astr err; + + //if (!jreader.parse(func_args.c_str(), jsRoot)) { + if (!jreader->parse(str_json_begin, str_json_begin + func_args.length(), &jsRoot, &err)) { + _create_json_ret(buf, TPE_JSON_FORMAT); + return; + } + + if (!g_cfg.save(func_args)) + _create_json_ret(buf, TPE_FAILED); + else + _create_json_ret(buf, TPE_OK); +} + +void TsHttpRpc::_rpc_func_file_action(const ex_astr& func_args, ex_astr& buf) { + + //Json::Reader jreader; + Json::Value jsRoot; + + Json::CharReaderBuilder jcrb; + std::unique_ptr const jreader(jcrb.newCharReader()); + const char *str_json_begin = func_args.c_str(); + ex_astr err; + + //if (!jreader.parse(func_args.c_str(), jsRoot)) { + if (!jreader->parse(str_json_begin, str_json_begin + func_args.length(), &jsRoot, &err)) { + _create_json_ret(buf, TPE_JSON_FORMAT); + return; + } + // 判断参数是否正确 + if (!jsRoot["action"].isNumeric()) { + _create_json_ret(buf, TPE_PARAM); + return; + } + int action = jsRoot["action"].asUInt(); + + HWND hParent = GetForegroundWindow(); + if (NULL == hParent) + hParent = g_hDlgMain; + + BOOL ret = FALSE; + wchar_t wszReturnPath[MAX_PATH] = _T(""); + + if (action == 1 || action == 2) { + OPENFILENAME ofn; + ex_wstr wsDefaultName; + ex_wstr wsDefaultPath; + StringCchCopy(wszReturnPath, MAX_PATH, wsDefaultName.c_str()); + + ZeroMemory(&ofn, sizeof(ofn)); + + ofn.lStructSize = sizeof(ofn); + ofn.lpstrTitle = _T("选择文件"); + ofn.hwndOwner = hParent; + ofn.lpstrFilter = _T("可执行程序 (*.exe)\0*.exe\0"); + ofn.lpstrFile = wszReturnPath; + ofn.nMaxFile = MAX_PATH; + ofn.lpstrInitialDir = wsDefaultPath.c_str(); + ofn.Flags = OFN_EXPLORER | OFN_PATHMUSTEXIST; + + if (action == 1) { + ofn.Flags |= OFN_FILEMUSTEXIST; + ret = GetOpenFileName(&ofn); + } else { + ofn.Flags |= OFN_OVERWRITEPROMPT; + ret = GetSaveFileName(&ofn); + } + } else if (action == 3) { + BROWSEINFO bi; + ZeroMemory(&bi, sizeof(BROWSEINFO)); + bi.hwndOwner = NULL; + bi.pidlRoot = NULL; + bi.pszDisplayName = wszReturnPath; //此参数如为NULL则不能显示对话框 + bi.lpszTitle = _T("选择目录"); + bi.ulFlags = BIF_RETURNONLYFSDIRS; + bi.lpfn = NULL; + bi.iImage = 0; //初始化入口参数bi结束 + LPITEMIDLIST pIDList = SHBrowseForFolder(&bi);//调用显示选择对话框 + if (pIDList) { + ret = true; + SHGetPathFromIDList(pIDList, wszReturnPath); + } else { + ret = false; + } + } else if (action == 4) { + ex_wstr wsDefaultName; + ex_wstr wsDefaultPath; + + if (wsDefaultPath.length() == 0) { + _create_json_ret(buf, TPE_PARAM); + return; + } + + ex_wstr::size_type pos = 0; + + while (ex_wstr::npos != (pos = wsDefaultPath.find(L"/", pos))) { + wsDefaultPath.replace(pos, 1, L"\\"); + pos += 1; + } + + ex_wstr wArg = L"/select, \""; + wArg += wsDefaultPath; + wArg += L"\""; + if ((int)ShellExecute(hParent, _T("open"), _T("explorer"), wArg.c_str(), NULL, SW_SHOW) > 32) + ret = true; + else + ret = false; + } + + if (ret) { + if (action == 1 || action == 2 || action == 3) { + ex_astr utf8_path; + ex_wstr2astr(wszReturnPath, utf8_path, EX_CODEPAGE_UTF8); + Json::Value root; + root["code"] = TPE_OK; + root["path"] = utf8_path; + _create_json_ret(buf, root); + + return; + } else { + _create_json_ret(buf, TPE_OK); + return; + } + } else { + _create_json_ret(buf, TPE_DATA); + return; + } +} + +void TsHttpRpc::_rpc_func_get_version(const ex_astr& func_args, ex_astr& buf) { + Json::Value root_ret; + ex_wstr w_version = TP_ASSIST_VER; + ex_astr version; + ex_wstr2astr(w_version, version, EX_CODEPAGE_UTF8); + root_ret["version"] = version; + root_ret["code"] = TPE_OK; + _create_json_ret(buf, root_ret); + return; +} diff --git a/client/tp_assist_win/ts_ver.h b/client/tp_assist_win/ts_ver.h index 6aa10dc..9c55de2 100644 --- a/client/tp_assist_win/ts_ver.h +++ b/client/tp_assist_win/ts_ver.h @@ -1,6 +1,6 @@ #ifndef __TS_ASSIST_VER_H__ #define __TS_ASSIST_VER_H__ -#define TP_ASSIST_VER L"3.3.1" +#define TP_ASSIST_VER L"3.5.1" #endif // __TS_ASSIST_VER_H__ diff --git a/common/libex/include/ex/ex_str.h b/common/libex/include/ex/ex_str.h index 070b999..330d8e8 100644 --- a/common/libex/include/ex/ex_str.h +++ b/common/libex/include/ex/ex_str.h @@ -55,9 +55,12 @@ int ex_wcsformat(wchar_t* out_buf, size_t buf_size, const wchar_t* fmt, ...); #include #include +#include typedef std::string ex_astr; typedef std::wstring ex_wstr; +typedef std::ostringstream ex_aoss; +typedef std::wostringstream ex_woss; typedef std::vector ex_astrs; typedef std::vector ex_wstrs; diff --git a/config.ini.in b/config.ini.in index fbc4f5e..27cb9fa 100644 --- a/config.ini.in +++ b/config.ini.in @@ -22,6 +22,8 @@ wget = C:\Program Files (x86)\wget\wget.exe # if not set msbuild path, default to get it by register. #msbuild = C:\Program Files (x86)\MSBuild\14.0\bin\MSBuild.exe +# need qt to build tp-player. +qt = C:\Qt\Qt5.12.0\5.12.0\msvc2017 # ============================================ # for Linux and macOS diff --git a/dist/client/windows/assist/installer.nsi b/dist/client/windows/assist/installer.nsi index f6c82e0fe99033ccf2b21690452c59ef912e1cd7..f6fc853c9e2169d09e3925df4329357da2e4b60b 100644 GIT binary patch delta 30 lcmaDO{ziO58Y`peZ|PToW|05?YtA^-pY delta 42 wcmbQGF-v2^91cd~$#XgM8I3oe<+#ZNZ|PToW|05+8l8UO$Q diff --git a/server/tp_core/core/ts_ver.h b/server/tp_core/core/ts_ver.h index 4da7b50..dda936f 100644 --- a/server/tp_core/core/ts_ver.h +++ b/server/tp_core/core/ts_ver.h @@ -1,6 +1,6 @@ #ifndef __TS_SERVER_VER_H__ #define __TS_SERVER_VER_H__ -#define TP_SERVER_VER L"3.3.0" +#define TP_SERVER_VER L"3.5.0" #endif // __TS_SERVER_VER_H__ diff --git a/server/www/teleport/static/js/audit/record-list.js b/server/www/teleport/static/js/audit/record-list.js index 46eabca..e6612f0 100644 --- a/server/www/teleport/static/js/audit/record-list.js +++ b/server/www/teleport/static/js/audit/record-list.js @@ -157,7 +157,7 @@ $app.on_table_host_cell_created = function (tbl, row_id, col_key, cell_obj) { cell_obj.find('[data-action]').click(function () { var row_data = tbl.get_row(row_id); - console.log('---', row_data); + // console.log('---', row_data); var action = $(this).attr('data-action'); if (action === 'replay') { @@ -363,83 +363,26 @@ $app.on_table_host_render_created = function (render) { }; $app.do_replay_rdp = function (record_id, user_username, acc_username, host_ip, time_begin) { + if(!$app.options.core_running) { + $tp.notify_error(tp_error_msg(TPE_NO_CORE_SERVER), '无法播放。'); + return; + } + + if(!$assist.check()) + return; + $assist.do_rdp_replay( - { - rid: record_id - // , web: $tp.web_server // + '/audit/get_rdp_record/' + record_id // 'http://' + ip + ':' + port + '/log/replay/rdp/' + record_id; - // , sid: Cookies.get('_sid') - , user: user_username - , acc: acc_username - , host: host_ip - , start: time_begin//tp_format_datetime(tp_utc2local(time_begin), 'yyyyMMdd-HHmmss') - } + record_id , function () { // func_success } , function (code, message) { - if (code === TPE_NO_ASSIST) + if (code === TPE_NO_ASSIST) { + $assist.errcode = TPE_NO_ASSIST; $assist.alert_assist_not_found(); + } else $tp.notify_error('播放RDP操作录像失败:' + tp_error_msg(code, message)); } ); }; - - -// $app.on_table_host_header_created = function (header) { -// $('#' + header._table_ctrl.dom_id + ' a[data-reset-filter]').click(function () { -// CALLBACK_STACK.create() -// .add(header._table_ctrl.load_data) -// .add(header._table_ctrl.reset_filters) -// .exec(); -// }); -// -// // 表格内嵌过滤器的事件绑定在这时进行(也可以延期到整个表格创建完成时进行) -// header._table_ctrl.get_filter_ctrl('search').on_created(); -// }; - -// $app.get_selected_record = function (tbl) { -// var records = []; -// var _objs = $('#' + $app.table_record.dom_id + ' tbody tr td input[data-check-box]'); -// $.each(_objs, function (i, _obj) { -// if ($(_obj).is(':checked')) { -// var _row_data = tbl.get_row(_obj); -// records.push(_row_data.id); -// } -// }); -// return records; -// }; - -// $app.on_btn_remove_record_click = function () { -// var records = $app.get_selected_record($app.table_record); -// if (records.length === 0) { -// $tp.notify_error('请选择要删除的会话记录!'); -// return; -// } -// -// var _fn_sure = function (cb_stack, cb_args) { -// $tp.ajax_post_json('/user/remove-user', {users: users}, -// function (ret) { -// if (ret.code === TPE_OK) { -// cb_stack.add($app.check_host_all_selected); -// cb_stack.add($app.table_record.load_data); -// $tp.notify_success('删除用户账号操作成功!'); -// } else { -// $tp.notify_error('删除用户账号操作失败:' + tp_error_msg(ret.code, ret.message)); -// } -// -// cb_stack.exec(); -// }, -// function () { -// $tp.notify_error('网络故障,删除用户账号操作失败!'); -// cb_stack.exec(); -// } -// ); -// }; -// -// var cb_stack = CALLBACK_STACK.create(); -// $tp.dlg_confirm(cb_stack, { -// msg: '

    注意:删除操作不可恢复!!

    删除用户账号将同时将其从所在用户组中移除,并且删除所有分配给此用户的授权!

    如果您希望禁止某个用户登录本系统,可对其进行“禁用”操作!

    您确定要移除所有选定的 ' + user_list.length + '个 用户账号吗?

    ', -// fn_yes: _fn_sure -// }); -// }; diff --git a/server/www/teleport/static/js/tp-assist.js b/server/www/teleport/static/js/tp-assist.js index cc983db..a38a674 100644 --- a/server/www/teleport/static/js/tp-assist.js +++ b/server/www/teleport/static/js/tp-assist.js @@ -66,16 +66,29 @@ $assist.init = function (cb_stack) { cb_stack.exec(); }; +$assist.check = function() { + if (!$assist.running) { + $assist.errcode = TPE_NO_ASSIST; + $assist.alert_assist_not_found(); + return false; + } else if (!$assist._version_compare()) { + $assist.errcode = TPE_OLD_ASSIST; + $assist.alert_assist_not_found(); + return false; + } + return true; +}; + + $assist.alert_assist_not_found = function () { - console.log($assist.errcode); if($assist.errcode === TPE_NO_ASSIST) { $assist.dom.msg_box_title.html('未检测到TELEPORT助手'); $assist.dom.msg_box_info.html('需要TELEPORT助手来辅助远程连接,请确认本机运行了TELEPORT助手!'); - $assist.dom.msg_box_desc.html('如果您尚未运行TELEPORT助手,请 下载最新版TELEPORT助手安装包 并安装。一旦运行了TELEPORT助手,即可重新进行远程连接。'); + $assist.dom.msg_box_desc.html('如果您尚未运行TELEPORT助手,请 下载最新版TELEPORT助手安装包 并安装。一旦运行了TELEPORT助手,即可刷新页面,重新进行远程连接。'); } else if($assist.errcode === TPE_OLD_ASSIST) { $assist.dom.msg_box_title.html('TELEPORT助手需要升级'); $assist.dom.msg_box_info.html('检测到TELEPORT助手版本 v'+ $assist.version +',但需要最低版本 v'+ $assist.ver_require+'。'); - $assist.dom.msg_box_desc.html('请 下载最新版TELEPORT助手安装包 并安装。一旦升级了TELEPORT助手,即可重新进行远程连接。'); + $assist.dom.msg_box_desc.html('请 下载最新版TELEPORT助手安装包 并安装。一旦升级了TELEPORT助手,即可刷新页面,重新进行远程连接。'); } $('#dialog-need-assist').modal(); @@ -134,15 +147,8 @@ $assist._make_message_box = function () { }; $assist.do_teleport = function (args, func_success, func_error) { - if(!$assist.running) { - $assist.errcode = TPE_NO_ASSIST; - func_error(TPE_NO_ASSIST, ''); + if(!$assist.check()) return; - } else if(!$assist._version_compare()) { - $assist.errcode = TPE_OLD_ASSIST; - func_error(TPE_NO_ASSIST, ''); - return; - } // 第一步:将参数传递给web服务,准备获取一个远程连接会话ID var args_ = JSON.stringify(args); @@ -226,7 +232,7 @@ $assist.do_teleport = function (args, func_success, func_error) { }); }; -$assist.do_rdp_replay = function (args, func_success, func_error) { +$assist.do_rdp_replay = function (rid, func_success, func_error) { // ================================================== // args is dict with fields shown below: // rid: (int) - record-id in database. @@ -236,10 +242,11 @@ $assist.do_rdp_replay = function (args, func_success, func_error) { // start: (string) - when start the RDP connection, should be a UTC timestamp. // ================================================== - // now fix the args. + // now make the args. + var args = {rid: rid}; args.web = $tp.web_server; // (string) - teleport server base address, like "http://127.0.0.1:7190", without end-slash. args.sid = Cookies.get('_sid'); // (string) - current login user's session-id. - args.start = tp_format_datetime(tp_utc2local(args.start), 'yyyyMMdd-HHmmss'); // (string) - convert UTC timestamp to local human-readable string. + // args.start = tp_format_datetime(tp_utc2local(args.start), 'yyyyMMdd-HHmmss'); // (string) - convert UTC timestamp to local human-readable string. console.log('do-rdp-replay:', args); @@ -264,34 +271,3 @@ $assist.do_rdp_replay = function (args, func_success, func_error) { } }); }; - -/* - -var version_compare = function () { - var cur_version = parseInt(g_current_version.split(".")[2]); - var req_version = parseInt(g_req_version.split(".")[2]); - return cur_version >= req_version; -}; - -var start_rdp_replay = function (args, func_success, func_error) { - var args_ = encodeURIComponent(JSON.stringify(args)); - $.ajax({ - type: 'GET', - timeout: 6000, - url: $assist.api_url + '/rdp_play/' + args_, - jsonp: 'callback', - dataType: 'json', - success: function (ret) { - if (ret.code === TPE_OK) { - error_process(ret, func_success, func_error); - } else { - func_error(ret.code, '查看录像失败!'); - } - console.log('ret', ret); - }, - error: function () { - func_error(TPE_NETWORK, '与助手的络通讯失败!'); - } - }); -}; -*/ diff --git a/server/www/teleport/webroot/app/app_ver.py b/server/www/teleport/webroot/app/app_ver.py index 1c328c3..ada39de 100644 --- a/server/www/teleport/webroot/app/app_ver.py +++ b/server/www/teleport/webroot/app/app_ver.py @@ -1,3 +1,3 @@ # -*- coding: utf8 -*- -TP_SERVER_VER = "3.3.1" -TP_ASSIST_REQUIRE_VER = "3.3.1" +TP_SERVER_VER = "3.5.1" +TP_ASSIST_REQUIRE_VER = "3.5.1" diff --git a/server/www/teleport/webroot/app/controller/audit.py b/server/www/teleport/webroot/app/controller/audit.py index b49466a..529b03a 100644 --- a/server/www/teleport/webroot/app/controller/audit.py +++ b/server/www/teleport/webroot/app/controller/audit.py @@ -411,12 +411,15 @@ class RecordHandler(TPBaseHandler): return if not tp_cfg().core.detected: + core_running = False total_size = 0 free_size = 0 else: + core_running = True total_size, free_size = get_free_space_bytes(tp_cfg().core.replay_path) param = { + 'core_running': core_running, 'total_size': total_size, 'free_size': free_size, } @@ -659,25 +662,12 @@ class DoGetFileHandler(TPBaseHandler): require_privilege = TP_PRIVILEGE_OPS_AUZ | TP_PRIVILEGE_AUDIT_AUZ | TP_PRIVILEGE_AUDIT - # sid = self.get_argument('sid', None) - # if sid is None: - # self.set_status(403) - # return self.write('need login first.') - # - # self._s_id = sid - # _user = self.get_session('user') - # if _user is None: - # self.set_status(403) - # return self.write('need login first.') - # self._user = _user - - # when test, disable auth. - # if not self._user['_is_login']: - # self.set_status(401) # 401=未授权, 要求身份验证 - # return self.write('need login first.') - # if (self._user['privilege'] & require_privilege) == 0: - # self.set_status(403) # 403=禁止 - # return self.write('you have no such privilege.') + if not self._user['_is_login']: + self.set_status(401) # 401=未授权, 要求身份验证 + return self.write('need login first.') + if (self._user['privilege'] & require_privilege) == 0: + self.set_status(403) # 403=禁止 + return self.write('you have no such privilege.') act = self.get_argument('act', None) _type = self.get_argument('type', None) diff --git a/version.in b/version.in index 8405318..4de5209 100644 --- a/version.in +++ b/version.in @@ -10,8 +10,8 @@ Minor: 次版本号。如果两个程序集的名称和主版本号相同,而 Revision: 修订号。主版本号和次版本号都相同但修订号不同的程序集应是完全可互换的。 这适用于修复以前发布的程序集中的错误或安全漏洞。 -TP_SERVER 3.3.1 # 整个服务端打包的版本 -TP_TPCORE 3.3.0 # 核心服务 tp_core 的版本 +TP_SERVER 3.5.1 # 整个服务端打包的版本 +TP_TPCORE 3.5.0 # 核心服务 tp_core 的版本 TP_TPWEB 3.1.0 # web服务 tp_web 的版本(一般除非升级Python,否则不会变化) -TP_ASSIST 3.3.1 # 助手版本 -TP_ASSIST_REQUIRE 3.3.1 # 适配的助手最低版本 +TP_ASSIST 3.5.1 # 助手版本 +TP_ASSIST_REQUIRE 3.5.1 # 适配的助手最低版本 From 483aa810eea9b9a19e7e97c64618501160a6d82f Mon Sep 17 00:00:00 2001 From: Apex Liu Date: Sun, 17 Nov 2019 04:31:11 +0800 Subject: [PATCH 43/44] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BA=93=E6=93=8D=E4=BD=9C=E4=BD=BF=E7=94=A8unix=5Ftimestamp()?= =?UTF-8?q?=E5=AF=BC=E8=87=B4sqlite=E6=97=A0=E6=B3=95=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/www/teleport/static/js/tp-assist.js | 10 +--------- server/www/teleport/webroot/app/base/utils.py | 6 ++++++ server/www/teleport/webroot/app/controller/user.py | 10 +++++----- server/www/teleport/webroot/app/model/user.py | 8 ++++---- 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/server/www/teleport/static/js/tp-assist.js b/server/www/teleport/static/js/tp-assist.js index ce9410a..98c0e74 100644 --- a/server/www/teleport/static/js/tp-assist.js +++ b/server/www/teleport/static/js/tp-assist.js @@ -235,20 +235,12 @@ $assist.do_teleport = function (args, func_success, func_error) { }; $assist.do_rdp_replay = function (rid, func_success, func_error) { - // ================================================== - // args is dict with fields shown below: - // rid: (int) - record-id in database. - // user: (string) - who did the RDP connection. - // acc: (string) - account to login to remote RDP server. - // host: (string) - IP of the remote RDP server. - // start: (string) - when start the RDP connection, should be a UTC timestamp. - // ================================================== + // rid: (int) - record-id in database. // now make the args. var args = {rid: rid}; args.web = $tp.web_server; // (string) - teleport server base address, like "http://127.0.0.1:7190", without end-slash. args.sid = Cookies.get('_sid'); // (string) - current login user's session-id. - // args.start = tp_format_datetime(tp_utc2local(args.start), 'yyyyMMdd-HHmmss'); // (string) - convert UTC timestamp to local human-readable string. console.log('do-rdp-replay:', args); diff --git a/server/www/teleport/webroot/app/base/utils.py b/server/www/teleport/webroot/app/base/utils.py index 426cebd..6c7855e 100644 --- a/server/www/teleport/webroot/app/base/utils.py +++ b/server/www/teleport/webroot/app/base/utils.py @@ -160,6 +160,12 @@ def tp_second2human(n): return ret +def tp_timestamp_from_str(t, fmt='%Y-%m-%d %H:%M:%S'): + _fmt = '%Y-%m-%d %H:%M:%S' if fmt is None else fmt + d = datetime.datetime.strptime(t, _fmt) + return int(d.timestamp()) + + def tp_timestamp_local_to_utc(t): return int(datetime.datetime.utcfromtimestamp(time.mktime(time.localtime(t))).timestamp()) diff --git a/server/www/teleport/webroot/app/controller/user.py b/server/www/teleport/webroot/app/controller/user.py index 5965293..d232529 100755 --- a/server/www/teleport/webroot/app/controller/user.py +++ b/server/www/teleport/webroot/app/controller/user.py @@ -11,7 +11,7 @@ from app.base.configs import tp_cfg from app.base.controller import TPBaseHandler, TPBaseJsonHandler from app.base.logger import * from app.base.session import tp_session -from app.base.utils import tp_check_strong_password, tp_gen_password +from app.base.utils import tp_check_strong_password, tp_gen_password, tp_timestamp_from_str from app.logic.auth.oath import tp_oath_verify_code from app.const import * from app.logic.auth.oath import tp_oath_generate_secret, tp_oath_generate_qrcode @@ -590,13 +590,13 @@ class DoUpdateUserHandler(TPBaseJsonHandler): args['wechat'] = args['wechat'].strip() if args['valid_from'] == '': - args['valid_from'] = '1970-01-01' + args['valid_from'] = 0 else: - args['valid_from'] = args['valid_from'].strip() + args['valid_from'] = tp_timestamp_from_str(args['valid_from'].strip(), '%Y-%m-%d %H:%M') if args['valid_to'] == '': - args['valid_to'] = '1970-01-01' + args['valid_to'] = 0 else: - args['valid_to'] = args['valid_to'].strip() + args['valid_to'] = tp_timestamp_from_str(args['valid_to'].strip(), '%Y-%m-%d %H:%M') args['desc'] = args['desc'].strip() except: return self.write_json(TPE_PARAM) diff --git a/server/www/teleport/webroot/app/model/user.py b/server/www/teleport/webroot/app/model/user.py index 9bc45e1..844c341 100755 --- a/server/www/teleport/webroot/app/model/user.py +++ b/server/www/teleport/webroot/app/model/user.py @@ -90,7 +90,7 @@ def login(handler, username, password=None, oath_code=None, check_bind_oath=Fals msg = '登录失败,用户状态异常' syslog.sys_log(user_info, handler.request.remote_ip, TPE_FAILED, msg) return TPE_FAILED, None, msg - elif current_unix_time < user_info['valid_from'] or (current_unix_time > user_info['valid_to'] and user_info['valid_to'] != 0): + elif current_unix_time < user_info['valid_from'] or (current_unix_time > user_info['valid_to'] and user_info['valid_to'] != 0): msg = '登录失败,用户已过期' syslog.sys_log(user_info, handler.request.remote_ip, TPE_FAILED, msg) return TPE_FAILED, None, msg @@ -362,8 +362,8 @@ def create_user(handler, user): '`email`, `creator_id`, `create_time`, `last_login`, `last_chpass`, `valid_from`, `valid_to`, `desc`' \ ') VALUES (' \ '{role}, "{username}", "{surname}", {user_type}, "{ldap_dn}", {auth_type}, "{password}", {state}, ' \ - '"{email}", {creator_id}, {create_time}, {last_login}, {last_chpass}, unix_timestamp("{valid_from}"), '\ - 'unix_timestamp("{valid_to}"), "{desc}");' \ + '"{email}", {creator_id}, {create_time}, {last_login}, {last_chpass}, {valid_from}, '\ + '{valid_to}, "{desc}");' \ ''.format(db.table_prefix, role=user['role'], username=user['username'], surname=user['surname'], user_type=user['type'], ldap_dn=user['ldap_dn'], auth_type=user['auth_type'], password=_password, state=TP_STATE_NORMAL, email=user['email'], creator_id=operator['id'], create_time=_time_now, @@ -407,7 +407,7 @@ def update_user(handler, args): sql = 'UPDATE `{}user` SET ' \ '`username`="{username}", `surname`="{surname}", `auth_type`={auth_type}, ' \ '`role_id`={role}, `email`="{email}", `mobile`="{mobile}", `qq`="{qq}", ' \ - '`wechat`="{wechat}", `valid_from`=unix_timestamp("{valid_from}"), `valid_to`=unix_timestamp("{valid_to}"), '\ + '`wechat`="{wechat}", `valid_from`={valid_from}, `valid_to`={valid_to}, '\ '`desc`="{desc}" WHERE `id`={user_id};' \ ''.format(db.table_prefix, username=args['username'], surname=args['surname'], auth_type=args['auth_type'], role=args['role'], From f99af6a2dfd326f390ea6dda34c276b650c86b6a Mon Sep 17 00:00:00 2001 From: Apex Liu Date: Sun, 17 Nov 2019 04:43:33 +0800 Subject: [PATCH 44/44] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E5=8D=87=E7=BA=A7?= =?UTF-8?q?=E4=BA=86jsoncpp=E5=BA=93=E4=B9=8B=E5=90=8Etp=5Fcore=E6=97=A0?= =?UTF-8?q?=E6=B3=95=E7=BC=96=E8=AF=91=E7=9A=84=E9=97=AE=E9=A2=98=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/tp_core/core/ts_http_rpc.cpp | 1123 ++++++++++++++------------- server/tp_core/core/ts_web_rpc.cpp | 679 ++++++++-------- 2 files changed, 929 insertions(+), 873 deletions(-) diff --git a/server/tp_core/core/ts_http_rpc.cpp b/server/tp_core/core/ts_http_rpc.cpp index e8bd3f6..d9530ab 100644 --- a/server/tp_core/core/ts_http_rpc.cpp +++ b/server/tp_core/core/ts_http_rpc.cpp @@ -1,553 +1,570 @@ -#include "ts_http_rpc.h" -#include "ts_ver.h" -#include "ts_env.h" -#include "ts_session.h" -#include "ts_crypto.h" -#include "ts_web_rpc.h" -#include "tp_tpp_mgr.h" - -extern TppManager g_tpp_mgr; - -#include - - -#define HEXTOI(x) (isdigit(x) ? x - '0' : x - 'W') -int ts_url_decode(const char *src, int src_len, char *dst, int dst_len, int is_form_url_encoded) -{ - int i, j, a, b; - - for (i = j = 0; i < src_len && j < dst_len - 1; i++, j++) - { - if (src[i] == '%') - { - if (i < src_len - 2 && isxdigit(*(const unsigned char *)(src + i + 1)) && - isxdigit(*(const unsigned char *)(src + i + 2))) { - a = tolower(*(const unsigned char *)(src + i + 1)); - b = tolower(*(const unsigned char *)(src + i + 2)); - dst[j] = (char)((HEXTOI(a) << 4) | HEXTOI(b)); - i += 2; - } - else - { - return -1; - } - } - else if (is_form_url_encoded && src[i] == '+') - { - dst[j] = ' '; - } - else - { - dst[j] = src[i]; - } - } - - dst[j] = '\0'; /* Null-terminate the destination */ - - return i >= src_len ? j : -1; -} - -TsHttpRpc::TsHttpRpc() : - ExThreadBase("http-rpc-thread") -{ - mg_mgr_init(&m_mg_mgr, NULL); -} - -TsHttpRpc::~TsHttpRpc() -{ - mg_mgr_free(&m_mg_mgr); -} - -void TsHttpRpc::_thread_loop(void) -{ - EXLOGI("[core] TeleportServer-RPC ready on %s:%d\n", m_host_ip.c_str(), m_host_port); - - while (!m_need_stop) - { - mg_mgr_poll(&m_mg_mgr, 500); - } - - EXLOGV("[core] rpc main loop end.\n"); -} - - -bool TsHttpRpc::init(void) -{ - struct mg_connection* nc = NULL; - - m_host_ip = g_env.rpc_bind_ip; - m_host_port = g_env.rpc_bind_port; - - char addr[128] = { 0 }; - // if (0 == strcmp(m_host_ip.c_str(), "127.0.0.1") || 0 == strcmp(m_host_ip.c_str(), "localhost")) - // ex_strformat(addr, 128, ":%d", m_host_port); - // else - // ex_strformat(addr, 128, "%s:%d", m_host_ip.c_str(), m_host_port); - if (0 == strcmp(m_host_ip.c_str(), "0.0.0.0")) - ex_strformat(addr, 128, ":%d", m_host_port); - else - ex_strformat(addr, 128, "%s:%d", m_host_ip.c_str(), m_host_port); - - nc = mg_bind(&m_mg_mgr, addr, _mg_event_handler); - if (NULL == nc) - { - EXLOGE("[core] rpc listener failed to bind at %s.\n", addr); - return false; - } - - nc->user_data = this; - - mg_set_protocol_http_websocket(nc); - - // ڴй¶ĵطÿԼ1KBڴ棩 - // DO NOT USE MULTITHREADING OF MG. - // cpq (one of the authors of MG) commented on 3 Feb: Multithreading support has been removed. - // https://github.com/cesanta/mongoose/commit/707b9ed2d6f177b3ad8787cb16a1bff90ddad992 - //mg_enable_multithreading(nc); - - return true; -} - -void TsHttpRpc::_mg_event_handler(struct mg_connection *nc, int ev, void *ev_data) -{ - struct http_message *hm = (struct http_message*)ev_data; - - TsHttpRpc* _this = (TsHttpRpc*)nc->user_data; - if (NULL == _this) - { - EXLOGE("[core] rpc invalid http request.\n"); - return; - } - - switch (ev) - { - case MG_EV_HTTP_REQUEST: - { - ex_astr ret_buf; - - ex_astr uri; - uri.assign(hm->uri.p, hm->uri.len); - - //EXLOGD("[core] rpc got request: %s\n", uri.c_str()); - - if (uri == "/rpc") - { - ex_astr method; - Json::Value json_param; - - ex_rv rv = _this->_parse_request(hm, method, json_param); - if (TPE_OK != rv) - { - EXLOGE("[core] rpc got invalid request.\n"); - _this->_create_json_ret(ret_buf, rv); - } - else - { - EXLOGD("[core] rpc got request method `%s`\n", method.c_str()); - _this->_process_request(method, json_param, ret_buf); - } - } - else - { - EXLOGE("[core] rpc got invalid request: not `rpc` uri.\n"); - _this->_create_json_ret(ret_buf, TPE_PARAM, "not a `rpc` request."); - } - - mg_printf(nc, "HTTP/1.0 200 OK\r\nAccess-Control-Allow-Origin: *\r\nContent-Length: %d\r\nContent-Type: application/json\r\n\r\n%s", (int)ret_buf.size() - 1, &ret_buf[0]); - nc->flags |= MG_F_SEND_AND_CLOSE; - } - break; - default: - break; - } -} - -ex_rv TsHttpRpc::_parse_request(struct http_message* req, ex_astr& func_cmd, Json::Value& json_param) -{ - if (NULL == req) - return TPE_PARAM; - - bool is_get = true; - if (req->method.len == 3 && 0 == memcmp(req->method.p, "GET", req->method.len)) - is_get = true; - else if (req->method.len == 4 && 0 == memcmp(req->method.p, "POST", req->method.len)) - is_get = false; - else - return TPE_HTTP_METHOD; - - ex_astr json_str; - bool need_decode = false; - if (is_get) { - json_str.assign(req->query_string.p, req->query_string.len); - need_decode = true; - } - else { - json_str.assign(req->body.p, req->body.len); - if (json_str.length() > 0 && json_str[0] == '%') - need_decode = true; - } - - if (need_decode) { - // url-decode - int len = json_str.length() * 2; - ex_chars sztmp; - sztmp.resize(len); - memset(&sztmp[0], 0, len); - if (-1 == ts_url_decode(json_str.c_str(), json_str.length(), &sztmp[0], len, 0)) - return TPE_HTTP_URL_ENCODE; - - json_str = &sztmp[0]; - } - - if (0 == json_str.length()) - return TPE_PARAM; - - Json::Reader jreader; - - if (!jreader.parse(json_str.c_str(), json_param)) - return TPE_JSON_FORMAT; - - if (json_param.isArray()) - return TPE_PARAM; - - if (json_param["method"].isNull() || !json_param["method"].isString()) - return TPE_PARAM; - - func_cmd = json_param["method"].asCString(); - json_param = json_param["param"]; - - return TPE_OK; -} - -void TsHttpRpc::_create_json_ret(ex_astr& buf, int errcode, const Json::Value& jr_data) -{ - // أ {"code":errcode, "data":{jr_data}} - - Json::FastWriter jr_writer; - Json::Value jr_root; - - jr_root["code"] = errcode; - jr_root["data"] = jr_data; - buf = jr_writer.write(jr_root); -} - -void TsHttpRpc::_create_json_ret(ex_astr& buf, int errcode) -{ - // أ {"code":errcode} - - Json::FastWriter jr_writer; - Json::Value jr_root; - - jr_root["code"] = errcode; - buf = jr_writer.write(jr_root); -} - -void TsHttpRpc::_create_json_ret(ex_astr& buf, int errcode, const char* message) -{ - // أ {"code":errcode, "message":message} - - Json::FastWriter jr_writer; - Json::Value jr_root; - - jr_root["code"] = errcode; - jr_root["message"] = message; - buf = jr_writer.write(jr_root); -} - -void TsHttpRpc::_process_request(const ex_astr& func_cmd, const Json::Value& json_param, ex_astr& buf) -{ - if (func_cmd == "request_session") { - _rpc_func_request_session(json_param, buf); - } - else if (func_cmd == "kill_sessions") { - _rpc_func_kill_sessions(json_param, buf); - } - else if (func_cmd == "get_config") { - _rpc_func_get_config(json_param, buf); - } - else if (func_cmd == "set_config") { - _rpc_func_set_config(json_param, buf); - } - else if (func_cmd == "enc") { - _rpc_func_enc(json_param, buf); - } - else if (func_cmd == "exit") { - _rpc_func_exit(json_param, buf); - } - else { - EXLOGE("[core] rpc got unknown command: %s\n", func_cmd.c_str()); - _create_json_ret(buf, TPE_UNKNOWN_CMD); - } -} - -extern bool g_exit_flag; // ҪTS˳ı־ֹ̣ͣ߳ -void TsHttpRpc::_rpc_func_exit(const Json::Value& json_param, ex_astr& buf) -{ - // һȫ˳־ - g_exit_flag = true; - _create_json_ret(buf, TPE_OK); -} - -void TsHttpRpc::_rpc_func_get_config(const Json::Value& json_param, ex_astr& buf) -{ - Json::Value jr_data; - - ex_astr _replay_name; - ex_wstr2astr(g_env.m_replay_path, _replay_name); - jr_data["replay-path"] = _replay_name; - - jr_data["web-server-rpc"] = g_env.web_server_rpc; - - ex_astr _version; - ex_wstr2astr(TP_SERVER_VER, _version); - jr_data["version"] = _version; - - ExIniFile& ini = g_env.get_ini(); - ex_ini_sections& secs = ini.GetAllSections(); - ex_ini_sections::iterator it = secs.begin(); - for (; it != secs.end(); ++it) - { - if (it->first.length() > 9 && 0 == wcsncmp(it->first.c_str(), L"protocol-", 9)) - { - ex_wstr name; - name.assign(it->first, 9, it->first.length() - 9); - ex_astr _name; - ex_wstr2astr(name, _name); - - bool enabled = false; - it->second->GetBool(L"enabled", enabled, false); - - ex_wstr ip; - if (!it->second->GetStr(L"bind-ip", ip)) - continue; - ex_astr _ip; - ex_wstr2astr(ip, _ip); - - int port; - it->second->GetInt(L"bind-port", port, 52189); - - jr_data[_name.c_str()]["enable"] = enabled; - jr_data[_name.c_str()]["ip"] = _ip; - jr_data[_name.c_str()]["port"] = port; - } - } - - _create_json_ret(buf, TPE_OK, jr_data); -} - -void TsHttpRpc::_rpc_func_request_session(const Json::Value& json_param, ex_astr& buf) -{ - // https://github.com/tp4a/teleport/wiki/TELEPORT-CORE-JSON-RPC#request_session - - int conn_id = 0; - ex_rv rv = TPE_OK; - - if (json_param["conn_id"].isNull()) - { - _create_json_ret(buf, TPE_PARAM); - return; - } - if (!json_param["conn_id"].isInt()) - { - _create_json_ret(buf, TPE_PARAM); - return; - } - - conn_id = json_param["conn_id"].asInt(); - if (0 == conn_id) - { - _create_json_ret(buf, TPE_PARAM); - return; - } - - TS_CONNECT_INFO* info = new TS_CONNECT_INFO; - if ((rv = ts_web_rpc_get_conn_info(conn_id, *info)) != TPE_OK) - { - _create_json_ret(buf, rv); - return; - } - -// info->ref_count = 0; -// info->ticket_start = ex_get_tick_count(); -// - // һsession-idڲظ - ex_astr sid; - if (!g_session_mgr.request_session(sid, info)) { - _create_json_ret(buf, TPE_FAILED); - return; - } - - EXLOGD("[core] rpc new session-id: %s\n", sid.c_str()); - - Json::Value jr_data; - jr_data["sid"] = sid; - - _create_json_ret(buf, TPE_OK, jr_data); -} - -void TsHttpRpc::_rpc_func_kill_sessions(const Json::Value& json_param, ex_astr& buf) { - /* - { - "sessions": ["0123456", "ABCDEF", ...] - } - */ - - if (json_param.isArray()) { - _create_json_ret(buf, TPE_PARAM); - return; - } - - if (json_param["sessions"].isNull() || !json_param["sessions"].isArray()) { - _create_json_ret(buf, TPE_PARAM); - return; - } - - Json::Value s = json_param["sessions"]; - int cnt = s.size(); - for (int i = 0; i < cnt; ++i) { - if (!s[i].isString()) { - _create_json_ret(buf, TPE_PARAM); - return; - } - } - - EXLOGV("[core] try to kill %d sessions.\n", cnt); - ex_astr sp = s.toStyledString(); - g_tpp_mgr.kill_sessions(sp); - - _create_json_ret(buf, TPE_OK); -} - -void TsHttpRpc::_rpc_func_enc(const Json::Value& json_param, ex_astr& buf) -{ - // https://github.com/tp4a/teleport/wiki/TELEPORT-CORE-JSON-RPC#enc - // һַ [ p=plain-text, c=cipher-text ] - // : {"p":"need be encrypt"} - // ʾ: {"p":"this-is-a-password"} - // p: ַܵ - // أ - // dataе"c"Ǽܺĵbase64 - // ʾ: {"code":0, "data":{"c":"Mxs340a9r3fs+3sdf=="}} - // 󷵻أ {"code":1234} - - if (json_param.isArray()) - { - _create_json_ret(buf, TPE_PARAM); - return; - } - - ex_astr plain_text; - - if (json_param["p"].isNull() || !json_param["p"].isString()) - { - _create_json_ret(buf, TPE_PARAM); - return; - } - - plain_text = json_param["p"].asCString(); - if (plain_text.length() == 0) - { - _create_json_ret(buf, TPE_PARAM); - return; - } - ex_astr cipher_text; - - if (!ts_db_field_encrypt(plain_text, cipher_text)) - { - _create_json_ret(buf, TPE_FAILED); - return; - } - - Json::Value jr_data; - jr_data["c"] = cipher_text; - _create_json_ret(buf, TPE_OK, jr_data); -} - -void TsHttpRpc::_rpc_func_set_config(const Json::Value& json_param, ex_astr& buf) -{ - // https://github.com/tp4a/teleport/wiki/TELEPORT-CORE-JSON-RPC#set_config - /* - { - "noop-timeout": 15 # Ӽ - } - */ - - if (json_param.isArray()) { - _create_json_ret(buf, TPE_PARAM); - return; - } - - if (json_param["noop_timeout"].isNull() || !json_param["noop_timeout"].isUInt()) { - _create_json_ret(buf, TPE_PARAM); - return; - } - - int noop_timeout = json_param["noop_timeout"].asUInt(); - EXLOGV("[core] set run-time config:\n"); - EXLOGV("[core] noop_timeout = %dm\n", noop_timeout); - - ex_astr sp = json_param.toStyledString(); - g_tpp_mgr.set_runtime_config(sp); - - _create_json_ret(buf, TPE_OK); -} - - -/* -void TsHttpRpc::_rpc_func_enc(const Json::Value& json_param, ex_astr& buf) -{ - // https://github.com/tp4a/teleport/wiki/TELEPORT-CORE-JSON-RPC#enc - // ַܶ [ p=plain-text, c=cipher-text ] - // : {"p":["need be encrypt", "plain to cipher"]} - // ʾ: {"p":["password-for-A"]} - // p: ַܵ - // أ - // dataе"c"Ǽܺĵbase64 - // ʾ: {"code":0, "data":{"c":["Mxs340a9r3fs+3sdf=="]}} - // 󷵻أ {"code":1234} - - if (json_param.isArray()) - { - _create_json_ret(buf, TPE_PARAM); - return; - } - - ex_astr plain_text; - - if (json_param["p"].isNull() || !json_param["p"].isArray()) - { - _create_json_ret(buf, TPE_PARAM); - return; - } - - Json::Value c; - - Json::Value p = json_param["p"]; - int cnt = p.size(); - for (int i = 0; i < cnt; ++i) - { - if (!p[i].isString()) { - _create_json_ret(buf, TPE_PARAM); - return; - } - - ex_astr p_txt = p[i].asCString(); - if (p_txt.length() == 0) { - c["c"].append(""); - } - - ex_astr c_txt; - if (!ts_db_field_encrypt(p_txt, c_txt)) - { - _create_json_ret(buf, TPE_FAILED); - return; - } - - c["c"].append(c_txt); - } - - Json::Value jr_data; - jr_data["c"] = c; - _create_json_ret(buf, TPE_OK, jr_data); -} -*/ +#include "ts_http_rpc.h" +#include "ts_ver.h" +#include "ts_env.h" +#include "ts_session.h" +#include "ts_crypto.h" +#include "ts_web_rpc.h" +#include "tp_tpp_mgr.h" + +extern TppManager g_tpp_mgr; + +#include + + +#define HEXTOI(x) (isdigit(x) ? x - '0' : x - 'W') +int ts_url_decode(const char *src, int src_len, char *dst, int dst_len, int is_form_url_encoded) +{ + int i, j, a, b; + + for (i = j = 0; i < src_len && j < dst_len - 1; i++, j++) + { + if (src[i] == '%') + { + if (i < src_len - 2 && isxdigit(*(const unsigned char *)(src + i + 1)) && + isxdigit(*(const unsigned char *)(src + i + 2))) { + a = tolower(*(const unsigned char *)(src + i + 1)); + b = tolower(*(const unsigned char *)(src + i + 2)); + dst[j] = (char)((HEXTOI(a) << 4) | HEXTOI(b)); + i += 2; + } + else + { + return -1; + } + } + else if (is_form_url_encoded && src[i] == '+') + { + dst[j] = ' '; + } + else + { + dst[j] = src[i]; + } + } + + dst[j] = '\0'; /* Null-terminate the destination */ + + return i >= src_len ? j : -1; +} + +TsHttpRpc::TsHttpRpc() : + ExThreadBase("http-rpc-thread") +{ + mg_mgr_init(&m_mg_mgr, NULL); +} + +TsHttpRpc::~TsHttpRpc() +{ + mg_mgr_free(&m_mg_mgr); +} + +void TsHttpRpc::_thread_loop(void) +{ + EXLOGI("[core] TeleportServer-RPC ready on %s:%d\n", m_host_ip.c_str(), m_host_port); + + while (!m_need_stop) + { + mg_mgr_poll(&m_mg_mgr, 500); + } + + EXLOGV("[core] rpc main loop end.\n"); +} + + +bool TsHttpRpc::init(void) +{ + struct mg_connection* nc = NULL; + + m_host_ip = g_env.rpc_bind_ip; + m_host_port = g_env.rpc_bind_port; + + char addr[128] = { 0 }; + // if (0 == strcmp(m_host_ip.c_str(), "127.0.0.1") || 0 == strcmp(m_host_ip.c_str(), "localhost")) + // ex_strformat(addr, 128, ":%d", m_host_port); + // else + // ex_strformat(addr, 128, "%s:%d", m_host_ip.c_str(), m_host_port); + if (0 == strcmp(m_host_ip.c_str(), "0.0.0.0")) + ex_strformat(addr, 128, ":%d", m_host_port); + else + ex_strformat(addr, 128, "%s:%d", m_host_ip.c_str(), m_host_port); + + nc = mg_bind(&m_mg_mgr, addr, _mg_event_handler); + if (NULL == nc) + { + EXLOGE("[core] rpc listener failed to bind at %s.\n", addr); + return false; + } + + nc->user_data = this; + + mg_set_protocol_http_websocket(nc); + + // 导致内存泄露的地方(每次请求约消耗1KB内存) + // DO NOT USE MULTITHREADING OF MG. + // cpq (one of the authors of MG) commented on 3 Feb: Multithreading support has been removed. + // https://github.com/cesanta/mongoose/commit/707b9ed2d6f177b3ad8787cb16a1bff90ddad992 + //mg_enable_multithreading(nc); + + return true; +} + +void TsHttpRpc::_mg_event_handler(struct mg_connection *nc, int ev, void *ev_data) +{ + struct http_message *hm = (struct http_message*)ev_data; + + TsHttpRpc* _this = (TsHttpRpc*)nc->user_data; + if (NULL == _this) + { + EXLOGE("[core] rpc invalid http request.\n"); + return; + } + + switch (ev) + { + case MG_EV_HTTP_REQUEST: + { + ex_astr ret_buf; + + ex_astr uri; + uri.assign(hm->uri.p, hm->uri.len); + + //EXLOGD("[core] rpc got request: %s\n", uri.c_str()); + + if (uri == "/rpc") + { + ex_astr method; + Json::Value json_param; + + ex_rv rv = _this->_parse_request(hm, method, json_param); + if (TPE_OK != rv) + { + EXLOGE("[core] rpc got invalid request.\n"); + _this->_create_json_ret(ret_buf, rv); + } + else + { + EXLOGD("[core] rpc got request method `%s`\n", method.c_str()); + _this->_process_request(method, json_param, ret_buf); + } + } + else + { + EXLOGE("[core] rpc got invalid request: not `rpc` uri.\n"); + _this->_create_json_ret(ret_buf, TPE_PARAM, "not a `rpc` request."); + } + + mg_printf(nc, "HTTP/1.0 200 OK\r\nAccess-Control-Allow-Origin: *\r\nContent-Length: %d\r\nContent-Type: application/json\r\n\r\n%s", (int)ret_buf.length(), &ret_buf[0]); + nc->flags |= MG_F_SEND_AND_CLOSE; + } + break; + default: + break; + } +} + +ex_rv TsHttpRpc::_parse_request(struct http_message* req, ex_astr& func_cmd, Json::Value& json_param) +{ + if (NULL == req) + return TPE_PARAM; + + bool is_get = true; + if (req->method.len == 3 && 0 == memcmp(req->method.p, "GET", req->method.len)) + is_get = true; + else if (req->method.len == 4 && 0 == memcmp(req->method.p, "POST", req->method.len)) + is_get = false; + else + return TPE_HTTP_METHOD; + + ex_astr json_str; + bool need_decode = false; + if (is_get) { + json_str.assign(req->query_string.p, req->query_string.len); + need_decode = true; + } + else { + json_str.assign(req->body.p, req->body.len); + if (json_str.length() > 0 && json_str[0] == '%') + need_decode = true; + } + + if (need_decode) { + // 将参数进行 url-decode 解码 + int len = json_str.length() * 2; + ex_chars sztmp; + sztmp.resize(len); + memset(&sztmp[0], 0, len); + if (-1 == ts_url_decode(json_str.c_str(), json_str.length(), &sztmp[0], len, 0)) + return TPE_HTTP_URL_ENCODE; + + json_str = &sztmp[0]; + } + + if (0 == json_str.length()) + return TPE_PARAM; + + //Json::Reader jreader; + Json::CharReaderBuilder jcrb; + std::unique_ptr const jreader(jcrb.newCharReader()); + const char *str_json_begin = json_str.c_str(); + ex_astr err; + + //if (!jreader.parse(json_str.c_str(), json_param)) + if (!jreader->parse(str_json_begin, str_json_begin + json_str.length(), &json_param, &err)) + return TPE_JSON_FORMAT; + + if (json_param.isArray()) + return TPE_PARAM; + + if (json_param["method"].isNull() || !json_param["method"].isString()) + return TPE_PARAM; + + func_cmd = json_param["method"].asCString(); + json_param = json_param["param"]; + + return TPE_OK; +} + +void TsHttpRpc::_create_json_ret(ex_astr& buf, int errcode, const Json::Value& jr_data) +{ + // 返回: {"code":errcode, "data":{jr_data}} + + //Json::FastWriter jr_writer; + Json::Value jr_root; + jr_root["code"] = errcode; + jr_root["data"] = jr_data; + //buf = jr_writer.write(jr_root); + Json::StreamWriterBuilder jwb; + std::unique_ptr jwriter(jwb.newStreamWriter()); + ex_aoss os; + jwriter->write(jr_root, &os); + buf = os.str(); +} + +void TsHttpRpc::_create_json_ret(ex_astr& buf, int errcode) +{ + // 返回: {"code":errcode} + + //Json::FastWriter jr_writer; + Json::Value jr_root; + jr_root["code"] = errcode; + //buf = jr_writer.write(jr_root); + Json::StreamWriterBuilder jwb; + std::unique_ptr jwriter(jwb.newStreamWriter()); + ex_aoss os; + jwriter->write(jr_root, &os); + buf = os.str(); +} + +void TsHttpRpc::_create_json_ret(ex_astr& buf, int errcode, const char* message) +{ + // 返回: {"code":errcode, "message":message} + + //Json::FastWriter jr_writer; + Json::Value jr_root; + jr_root["code"] = errcode; + jr_root["message"] = message; + //buf = jr_writer.write(jr_root); + Json::StreamWriterBuilder jwb; + std::unique_ptr jwriter(jwb.newStreamWriter()); + ex_aoss os; + jwriter->write(jr_root, &os); + buf = os.str(); +} + +void TsHttpRpc::_process_request(const ex_astr& func_cmd, const Json::Value& json_param, ex_astr& buf) +{ + if (func_cmd == "request_session") { + _rpc_func_request_session(json_param, buf); + } + else if (func_cmd == "kill_sessions") { + _rpc_func_kill_sessions(json_param, buf); + } + else if (func_cmd == "get_config") { + _rpc_func_get_config(json_param, buf); + } + else if (func_cmd == "set_config") { + _rpc_func_set_config(json_param, buf); + } + else if (func_cmd == "enc") { + _rpc_func_enc(json_param, buf); + } + else if (func_cmd == "exit") { + _rpc_func_exit(json_param, buf); + } + else { + EXLOGE("[core] rpc got unknown command: %s\n", func_cmd.c_str()); + _create_json_ret(buf, TPE_UNKNOWN_CMD); + } +} + +extern bool g_exit_flag; // 要求整个TS退出的标志(用于停止各个工作线程) +void TsHttpRpc::_rpc_func_exit(const Json::Value& json_param, ex_astr& buf) +{ + // 设置一个全局退出标志 + g_exit_flag = true; + _create_json_ret(buf, TPE_OK); +} + +void TsHttpRpc::_rpc_func_get_config(const Json::Value& json_param, ex_astr& buf) +{ + Json::Value jr_data; + + ex_astr _replay_name; + ex_wstr2astr(g_env.m_replay_path, _replay_name); + jr_data["replay-path"] = _replay_name; + + jr_data["web-server-rpc"] = g_env.web_server_rpc; + + ex_astr _version; + ex_wstr2astr(TP_SERVER_VER, _version); + jr_data["version"] = _version; + + ExIniFile& ini = g_env.get_ini(); + ex_ini_sections& secs = ini.GetAllSections(); + ex_ini_sections::iterator it = secs.begin(); + for (; it != secs.end(); ++it) + { + if (it->first.length() > 9 && 0 == wcsncmp(it->first.c_str(), L"protocol-", 9)) + { + ex_wstr name; + name.assign(it->first, 9, it->first.length() - 9); + ex_astr _name; + ex_wstr2astr(name, _name); + + bool enabled = false; + it->second->GetBool(L"enabled", enabled, false); + + ex_wstr ip; + if (!it->second->GetStr(L"bind-ip", ip)) + continue; + ex_astr _ip; + ex_wstr2astr(ip, _ip); + + int port; + it->second->GetInt(L"bind-port", port, 52189); + + jr_data[_name.c_str()]["enable"] = enabled; + jr_data[_name.c_str()]["ip"] = _ip; + jr_data[_name.c_str()]["port"] = port; + } + } + + _create_json_ret(buf, TPE_OK, jr_data); +} + +void TsHttpRpc::_rpc_func_request_session(const Json::Value& json_param, ex_astr& buf) +{ + // https://github.com/tp4a/teleport/wiki/TELEPORT-CORE-JSON-RPC#request_session + + int conn_id = 0; + ex_rv rv = TPE_OK; + + if (json_param["conn_id"].isNull()) + { + _create_json_ret(buf, TPE_PARAM); + return; + } + if (!json_param["conn_id"].isInt()) + { + _create_json_ret(buf, TPE_PARAM); + return; + } + + conn_id = json_param["conn_id"].asInt(); + if (0 == conn_id) + { + _create_json_ret(buf, TPE_PARAM); + return; + } + + TS_CONNECT_INFO* info = new TS_CONNECT_INFO; + if ((rv = ts_web_rpc_get_conn_info(conn_id, *info)) != TPE_OK) + { + _create_json_ret(buf, rv); + return; + } + +// info->ref_count = 0; +// info->ticket_start = ex_get_tick_count(); +// + // 生成一个session-id(内部会避免重复) + ex_astr sid; + if (!g_session_mgr.request_session(sid, info)) { + _create_json_ret(buf, TPE_FAILED); + return; + } + + EXLOGD("[core] rpc new session-id: %s\n", sid.c_str()); + + Json::Value jr_data; + jr_data["sid"] = sid; + + _create_json_ret(buf, TPE_OK, jr_data); +} + +void TsHttpRpc::_rpc_func_kill_sessions(const Json::Value& json_param, ex_astr& buf) { + /* + { + "sessions": ["0123456", "ABCDEF", ...] + } + */ + + if (json_param.isArray()) { + _create_json_ret(buf, TPE_PARAM); + return; + } + + if (json_param["sessions"].isNull() || !json_param["sessions"].isArray()) { + _create_json_ret(buf, TPE_PARAM); + return; + } + + Json::Value s = json_param["sessions"]; + int cnt = s.size(); + for (int i = 0; i < cnt; ++i) { + if (!s[i].isString()) { + _create_json_ret(buf, TPE_PARAM); + return; + } + } + + EXLOGV("[core] try to kill %d sessions.\n", cnt); + ex_astr sp = s.toStyledString(); + g_tpp_mgr.kill_sessions(sp); + + _create_json_ret(buf, TPE_OK); +} + +void TsHttpRpc::_rpc_func_enc(const Json::Value& json_param, ex_astr& buf) +{ + // https://github.com/tp4a/teleport/wiki/TELEPORT-CORE-JSON-RPC#enc + // 加密一个字符串 [ p=plain-text, c=cipher-text ] + // 入参: {"p":"need be encrypt"} + // 示例: {"p":"this-is-a-password"} + // p: 被加密的字符串 + // 返回: + // data域中的"c"的内容是加密后密文的base64编码结果 + // 示例: {"code":0, "data":{"c":"Mxs340a9r3fs+3sdf=="}} + // 错误返回: {"code":1234} + + if (json_param.isArray()) + { + _create_json_ret(buf, TPE_PARAM); + return; + } + + ex_astr plain_text; + + if (json_param["p"].isNull() || !json_param["p"].isString()) + { + _create_json_ret(buf, TPE_PARAM); + return; + } + + plain_text = json_param["p"].asCString(); + if (plain_text.length() == 0) + { + _create_json_ret(buf, TPE_PARAM); + return; + } + ex_astr cipher_text; + + if (!ts_db_field_encrypt(plain_text, cipher_text)) + { + _create_json_ret(buf, TPE_FAILED); + return; + } + + Json::Value jr_data; + jr_data["c"] = cipher_text; + _create_json_ret(buf, TPE_OK, jr_data); +} + +void TsHttpRpc::_rpc_func_set_config(const Json::Value& json_param, ex_astr& buf) +{ + // https://github.com/tp4a/teleport/wiki/TELEPORT-CORE-JSON-RPC#set_config + /* + { + "noop-timeout": 15 # 按分钟计 + } + */ + + if (json_param.isArray()) { + _create_json_ret(buf, TPE_PARAM); + return; + } + + if (json_param["noop_timeout"].isNull() || !json_param["noop_timeout"].isUInt()) { + _create_json_ret(buf, TPE_PARAM); + return; + } + + int noop_timeout = json_param["noop_timeout"].asUInt(); + EXLOGV("[core] set run-time config:\n"); + EXLOGV("[core] noop_timeout = %dm\n", noop_timeout); + + ex_astr sp = json_param.toStyledString(); + g_tpp_mgr.set_runtime_config(sp); + + _create_json_ret(buf, TPE_OK); +} + + +/* +void TsHttpRpc::_rpc_func_enc(const Json::Value& json_param, ex_astr& buf) +{ + // https://github.com/tp4a/teleport/wiki/TELEPORT-CORE-JSON-RPC#enc + // 加密多个个字符串 [ p=plain-text, c=cipher-text ] + // 入参: {"p":["need be encrypt", "plain to cipher"]} + // 示例: {"p":["password-for-A"]} + // p: 被加密的字符串 + // 返回: + // data域中的"c"的内容是加密后密文的base64编码结果 + // 示例: {"code":0, "data":{"c":["Mxs340a9r3fs+3sdf=="]}} + // 错误返回: {"code":1234} + + if (json_param.isArray()) + { + _create_json_ret(buf, TPE_PARAM); + return; + } + + ex_astr plain_text; + + if (json_param["p"].isNull() || !json_param["p"].isArray()) + { + _create_json_ret(buf, TPE_PARAM); + return; + } + + Json::Value c; + + Json::Value p = json_param["p"]; + int cnt = p.size(); + for (int i = 0; i < cnt; ++i) + { + if (!p[i].isString()) { + _create_json_ret(buf, TPE_PARAM); + return; + } + + ex_astr p_txt = p[i].asCString(); + if (p_txt.length() == 0) { + c["c"].append(""); + } + + ex_astr c_txt; + if (!ts_db_field_encrypt(p_txt, c_txt)) + { + _create_json_ret(buf, TPE_FAILED); + return; + } + + c["c"].append(c_txt); + } + + Json::Value jr_data; + jr_data["c"] = c; + _create_json_ret(buf, TPE_OK, jr_data); +} +*/ diff --git a/server/tp_core/core/ts_web_rpc.cpp b/server/tp_core/core/ts_web_rpc.cpp index cf5b899..1ec813a 100644 --- a/server/tp_core/core/ts_web_rpc.cpp +++ b/server/tp_core/core/ts_web_rpc.cpp @@ -1,320 +1,359 @@ -#include "ts_web_rpc.h" -#include "ts_env.h" -#include "ts_crypto.h" -#include "ts_http_client.h" - -#include "../common/ts_const.h" - -#include -#include - -bool ts_web_rpc_register_core() -{ - Json::FastWriter json_writer; - Json::Value jreq; - jreq["method"] = "register_core"; - jreq["param"]["rpc"] = g_env.core_server_rpc; - - ex_astr json_param; - json_param = json_writer.write(jreq); - - ex_astr param; - ts_url_encode(json_param.c_str(), param); - - ex_astr url = g_env.web_server_rpc; - url += "?"; - url += param; - - ex_astr body; - return ts_http_get(url, body); -} - -int ts_web_rpc_get_conn_info(int conn_id, TS_CONNECT_INFO& info) -{ - Json::FastWriter json_writer; - Json::Value jreq; - jreq["method"] = "get_conn_info"; - jreq["param"]["conn_id"] = conn_id; - - ex_astr json_param; - json_param = json_writer.write(jreq); - - ex_astr param; - ts_url_encode(json_param.c_str(), param); - - ex_astr url = g_env.web_server_rpc; - url += "?"; - url += param; - - ex_astr body; - if (!ts_http_get(url, body)) - { - EXLOGE("[core] get conn info from web-server failed: can not connect to web-server.\n"); - return TPE_NETWORK; - } - if (body.length() == 0) { - EXLOGE("[core] get conn info from web-server failed: got nothing.\n"); - return TPE_NETWORK; - } - - Json::Reader jreader; - Json::Value jret; - - if (!jreader.parse(body.c_str(), jret)) - return TPE_PARAM; - if (!jret.isObject()) - return TPE_PARAM; - if (!jret["data"].isObject()) - return TPE_PARAM; - - Json::Value& _jret = jret["data"]; - - if(!_jret["user_id"].isInt()) - EXLOGE("connection info: need `user_id`.\n"); - if(!_jret["host_id"].isInt()) - EXLOGE("connection info: need `host_id`.\n"); - if(!_jret["acc_id"].isInt()) - EXLOGE("connection info: need `acc_id`.\n"); - if(!_jret["conn_port"].isInt()) - EXLOGE("connection info: need `conn_port`.\n"); - if(!_jret["protocol_type"].isInt()) - EXLOGE("connection info: need `protocol_type`.\n"); - if(!_jret["protocol_sub_type"].isInt()) - EXLOGE("connection info: need `protocol_sub_type`.\n"); - if(!_jret["auth_type"].isInt()) - EXLOGE("connection info: need `auth_type`.\n"); - if (!_jret["protocol_flag"].isUInt()) - EXLOGE("connection info: need `protocol_flag`.\n"); - if (!_jret["record_flag"].isUInt()) - EXLOGE("connection info: need `record_flag`.\n"); - if (!_jret["_enc"].isInt()) - EXLOGE("connection info: need `_enc`.\n"); - if(!_jret["user_username"].isString()) - EXLOGE("connection info: need `user_username`.\n"); - if(!_jret["host_ip"].isString()) - EXLOGE("connection info: need `host_ip`.\n"); - if(!_jret["conn_ip"].isString()) - EXLOGE("connection info: need `conn_ip`.\n"); - if(!_jret["client_ip"].isString()) - EXLOGE("connection info: need `client_ip`.\n"); - if(!_jret["acc_username"].isString()) - EXLOGE("connection info: need `acc_username`.\n"); - if(!_jret["acc_secret"].isString()) - EXLOGE("connection info: need `acc_secret`.\n"); - if(!_jret["username_prompt"].isString()) - EXLOGE("connection info: need `username_prompt`.\n"); - if(!_jret["password_prompt"].isString()) - EXLOGE("connection info: need `password_prompt`.\n"); - - if ( - !_jret["user_id"].isInt() - || !_jret["host_id"].isInt() - || !_jret["acc_id"].isInt() - || !_jret["conn_port"].isInt() - || !_jret["protocol_type"].isInt() - || !_jret["protocol_sub_type"].isInt() - || !_jret["auth_type"].isInt() - || !_jret["protocol_flag"].isUInt() - || !_jret["record_flag"].isUInt() - || !_jret["_enc"].isInt() - - || !_jret["user_username"].isString() - || !_jret["host_ip"].isString() - || !_jret["conn_ip"].isString() - || !_jret["client_ip"].isString() - || !_jret["acc_username"].isString() - || !_jret["acc_secret"].isString() - || !_jret["username_prompt"].isString() - || !_jret["password_prompt"].isString() - ) - { - EXLOGE("got connection info from web-server, but not all info valid.\n"); - return TPE_PARAM; - } - - int user_id; - int host_id; - int acc_id; - ex_astr user_username;// 뱾ӵû - ex_astr host_ip;// ԶIPֱģʽremote_host_ipͬ - ex_astr conn_ip;// ҪӵԶIPǶ˿ӳģʽΪ·IP - int conn_port;// ҪӵԶĶ˿ڣǶ˿ӳģʽΪ·Ķ˿ڣ - ex_astr client_ip; - ex_astr acc_username; // Զ˺ - ex_astr acc_secret;// Զ˺ŵ루˽Կ - ex_astr username_prompt; - ex_astr password_prompt; - int protocol_type = 0; - int protocol_sub_type = 0; - int auth_type = 0; - int protocol_flag = 0; - int record_flag = 0; - bool _enc; - - user_id = _jret["user_id"].asInt(); - host_id = _jret["host_id"].asInt(); - acc_id = _jret["acc_id"].asInt(); - user_username = _jret["user_username"].asString(); - host_ip = _jret["host_ip"].asString(); - conn_ip = _jret["conn_ip"].asString(); - conn_port = _jret["conn_port"].asInt(); - client_ip = _jret["client_ip"].asString(); - acc_username = _jret["acc_username"].asString(); - acc_secret = _jret["acc_secret"].asString(); - username_prompt = _jret["username_prompt"].asString(); - password_prompt = _jret["password_prompt"].asString(); - protocol_type = _jret["protocol_type"].asInt(); - protocol_sub_type = _jret["protocol_sub_type"].asInt(); - protocol_flag = _jret["protocol_flag"].asUInt(); - record_flag = _jret["record_flag"].asUInt(); - auth_type = _jret["auth_type"].asInt(); - _enc = _jret["_enc"].asBool(); - - - // һжϲǷϷ - // ע⣬account_idΪ-1ʾһβӡ - if (user_id <= 0 || host_id <= 0 - || user_username.length() == 0 - || host_ip.length() == 0 || conn_ip.length() == 0 || client_ip.length() == 0 - || conn_port <= 0 || conn_port >= 65535 - || acc_username.length() == 0 || acc_secret.length() == 0 - || !(protocol_type == TP_PROTOCOL_TYPE_RDP || protocol_type == TP_PROTOCOL_TYPE_SSH || protocol_type == TP_PROTOCOL_TYPE_TELNET) - || !(auth_type == TP_AUTH_TYPE_NONE || auth_type == TP_AUTH_TYPE_PASSWORD || auth_type == TP_AUTH_TYPE_PRIVATE_KEY) - ) - { - return TPE_PARAM; - } - - if (_enc) { - ex_astr _auth; - if (!ts_db_field_decrypt(acc_secret, _auth)) - return TPE_FAILED; - - acc_secret = _auth; - } - - info.user_id = user_id; - info.host_id = host_id; - info.acc_id = acc_id; - info.user_username = user_username; - info.host_ip = host_ip; - info.conn_ip = conn_ip; - info.conn_port = conn_port; - info.client_ip = client_ip; - info.acc_username = acc_username; - info.acc_secret = acc_secret; - info.username_prompt = username_prompt; - info.password_prompt = password_prompt; - info.protocol_type = protocol_type; - info.protocol_sub_type = protocol_sub_type; - info.auth_type = auth_type; - info.protocol_flag = protocol_flag; - info.record_flag = record_flag; - - return TPE_OK; -} - -bool ts_web_rpc_session_begin(TS_CONNECT_INFO& info, int& record_id) -{ - Json::FastWriter json_writer; - Json::Value jreq; - - jreq["method"] = "session_begin"; - jreq["param"]["sid"] = info.sid.c_str(); - jreq["param"]["user_id"] = info.user_id; - jreq["param"]["host_id"] = info.host_id; - jreq["param"]["acc_id"] = info.acc_id; - jreq["param"]["user_username"] = info.user_username.c_str(); - jreq["param"]["acc_username"] = info.acc_username.c_str(); - jreq["param"]["host_ip"] = info.host_ip.c_str(); - jreq["param"]["conn_ip"] = info.conn_ip.c_str(); - jreq["param"]["client_ip"] = info.client_ip.c_str(); - //jreq["param"]["sys_type"] = info.sys_type; - jreq["param"]["conn_port"] = info.conn_port; - jreq["param"]["auth_type"] = info.auth_type; - jreq["param"]["protocol_type"] = info.protocol_type; - jreq["param"]["protocol_sub_type"] = info.protocol_sub_type; - - ex_astr json_param; - json_param = json_writer.write(jreq); - - ex_astr param; - ts_url_encode(json_param.c_str(), param); - - ex_astr url = g_env.web_server_rpc; - url += "?"; - url += param; - - ex_astr body; - if (!ts_http_get(url, body)) - { - // EXLOGV("request `rpc::session_begin` from web return: "); - // EXLOGV(body.c_str()); - // EXLOGV("\n"); - return false; - } - - Json::Reader jreader; - Json::Value jret; - - if (!jreader.parse(body.c_str(), jret)) - return false; - if (!jret.isObject()) - return false; - if (!jret["data"].isObject()) - return false; - if (!jret["data"]["rid"].isUInt()) - return false; - - record_id = jret["data"]["rid"].asUInt(); - - return true; -} - -bool ts_web_rpc_session_update(int record_id, int protocol_sub_type, int state) { - Json::FastWriter json_writer; - Json::Value jreq; - jreq["method"] = "session_update"; - jreq["param"]["rid"] = record_id; - jreq["param"]["protocol_sub_type"] = protocol_sub_type; - jreq["param"]["code"] = state; - - ex_astr json_param; - json_param = json_writer.write(jreq); - - ex_astr param; - ts_url_encode(json_param.c_str(), param); - - ex_astr url = g_env.web_server_rpc; - url += "?"; - url += param; - - ex_astr body; - return ts_http_get(url, body); -} - - -//session -bool ts_web_rpc_session_end(const char* sid, int record_id, int ret_code) -{ - // TODO: ָsidصĻỰüһ0ʱ٣ - - Json::FastWriter json_writer; - Json::Value jreq; - jreq["method"] = "session_end"; - jreq["param"]["rid"] = record_id; - jreq["param"]["code"] = ret_code; - - ex_astr json_param; - json_param = json_writer.write(jreq); - - ex_astr param; - ts_url_encode(json_param.c_str(), param); - - ex_astr url = g_env.web_server_rpc; - url += "?"; - url += param; - - ex_astr body; - return ts_http_get(url, body); -} +#include "ts_web_rpc.h" +#include "ts_env.h" +#include "ts_crypto.h" +#include "ts_http_client.h" + +#include "../common/ts_const.h" + +#include +#include + +bool ts_web_rpc_register_core() +{ + //Json::FastWriter json_writer; + Json::Value jreq; + jreq["method"] = "register_core"; + jreq["param"]["rpc"] = g_env.core_server_rpc; + + ex_astr json_param; + //json_param = json_writer.write(jreq); + Json::StreamWriterBuilder jwb; + std::unique_ptr jwriter(jwb.newStreamWriter()); + ex_aoss os; + jwriter->write(jreq, &os); + json_param = os.str(); + + ex_astr param; + ts_url_encode(json_param.c_str(), param); + + ex_astr url = g_env.web_server_rpc; + url += "?"; + url += param; + + ex_astr body; + return ts_http_get(url, body); +} + +int ts_web_rpc_get_conn_info(int conn_id, TS_CONNECT_INFO& info) +{ + //Json::FastWriter json_writer; + Json::Value jreq; + jreq["method"] = "get_conn_info"; + jreq["param"]["conn_id"] = conn_id; + + ex_astr json_param; + //json_param = json_writer.write(jreq); + Json::StreamWriterBuilder jwb; + std::unique_ptr jwriter(jwb.newStreamWriter()); + ex_aoss os; + jwriter->write(jreq, &os); + json_param = os.str(); + + ex_astr param; + ts_url_encode(json_param.c_str(), param); + + ex_astr url = g_env.web_server_rpc; + url += "?"; + url += param; + + ex_astr body; + if (!ts_http_get(url, body)) + { + EXLOGE("[core] get conn info from web-server failed: can not connect to web-server.\n"); + return TPE_NETWORK; + } + if (body.length() == 0) { + EXLOGE("[core] get conn info from web-server failed: got nothing.\n"); + return TPE_NETWORK; + } + + //Json::Reader jreader; + Json::Value jret; + + //if (!jreader.parse(body.c_str(), jret)) + Json::CharReaderBuilder jcrb; + std::unique_ptr const jreader(jcrb.newCharReader()); + const char *str_json_begin = body.c_str(); + ex_astr err; + + //if (!jreader.parse(func_args.c_str(), jsRoot)) { + if (!jreader->parse(str_json_begin, str_json_begin + body.length(), &jret, &err)) + return TPE_PARAM; + if (!jret.isObject()) + return TPE_PARAM; + if (!jret["data"].isObject()) + return TPE_PARAM; + + Json::Value& _jret = jret["data"]; + + if(!_jret["user_id"].isInt()) + EXLOGE("connection info: need `user_id`.\n"); + if(!_jret["host_id"].isInt()) + EXLOGE("connection info: need `host_id`.\n"); + if(!_jret["acc_id"].isInt()) + EXLOGE("connection info: need `acc_id`.\n"); + if(!_jret["conn_port"].isInt()) + EXLOGE("connection info: need `conn_port`.\n"); + if(!_jret["protocol_type"].isInt()) + EXLOGE("connection info: need `protocol_type`.\n"); + if(!_jret["protocol_sub_type"].isInt()) + EXLOGE("connection info: need `protocol_sub_type`.\n"); + if(!_jret["auth_type"].isInt()) + EXLOGE("connection info: need `auth_type`.\n"); + if (!_jret["protocol_flag"].isUInt()) + EXLOGE("connection info: need `protocol_flag`.\n"); + if (!_jret["record_flag"].isUInt()) + EXLOGE("connection info: need `record_flag`.\n"); + if (!_jret["_enc"].isInt()) + EXLOGE("connection info: need `_enc`.\n"); + if(!_jret["user_username"].isString()) + EXLOGE("connection info: need `user_username`.\n"); + if(!_jret["host_ip"].isString()) + EXLOGE("connection info: need `host_ip`.\n"); + if(!_jret["conn_ip"].isString()) + EXLOGE("connection info: need `conn_ip`.\n"); + if(!_jret["client_ip"].isString()) + EXLOGE("connection info: need `client_ip`.\n"); + if(!_jret["acc_username"].isString()) + EXLOGE("connection info: need `acc_username`.\n"); + if(!_jret["acc_secret"].isString()) + EXLOGE("connection info: need `acc_secret`.\n"); + if(!_jret["username_prompt"].isString()) + EXLOGE("connection info: need `username_prompt`.\n"); + if(!_jret["password_prompt"].isString()) + EXLOGE("connection info: need `password_prompt`.\n"); + + if ( + !_jret["user_id"].isInt() + || !_jret["host_id"].isInt() + || !_jret["acc_id"].isInt() + || !_jret["conn_port"].isInt() + || !_jret["protocol_type"].isInt() + || !_jret["protocol_sub_type"].isInt() + || !_jret["auth_type"].isInt() + || !_jret["protocol_flag"].isUInt() + || !_jret["record_flag"].isUInt() + || !_jret["_enc"].isInt() + + || !_jret["user_username"].isString() + || !_jret["host_ip"].isString() + || !_jret["conn_ip"].isString() + || !_jret["client_ip"].isString() + || !_jret["acc_username"].isString() + || !_jret["acc_secret"].isString() + || !_jret["username_prompt"].isString() + || !_jret["password_prompt"].isString() + ) + { + EXLOGE("got connection info from web-server, but not all info valid.\n"); + return TPE_PARAM; + } + + int user_id; + int host_id; + int acc_id; + ex_astr user_username;// 申请本次连接的用户名 + ex_astr host_ip;// 真正的远程主机IP(如果是直接连接模式,则与remote_host_ip相同) + ex_astr conn_ip;// 要连接的远程主机的IP(如果是端口映射模式,则为路由主机的IP) + int conn_port;// 要连接的远程主机的端口(如果是端口映射模式,则为路由主机的端口) + ex_astr client_ip; + ex_astr acc_username; // 远程主机的账号 + ex_astr acc_secret;// 远程主机账号的密码(或者私钥) + ex_astr username_prompt; + ex_astr password_prompt; + int protocol_type = 0; + int protocol_sub_type = 0; + int auth_type = 0; + int protocol_flag = 0; + int record_flag = 0; + bool _enc; + + user_id = _jret["user_id"].asInt(); + host_id = _jret["host_id"].asInt(); + acc_id = _jret["acc_id"].asInt(); + user_username = _jret["user_username"].asString(); + host_ip = _jret["host_ip"].asString(); + conn_ip = _jret["conn_ip"].asString(); + conn_port = _jret["conn_port"].asInt(); + client_ip = _jret["client_ip"].asString(); + acc_username = _jret["acc_username"].asString(); + acc_secret = _jret["acc_secret"].asString(); + username_prompt = _jret["username_prompt"].asString(); + password_prompt = _jret["password_prompt"].asString(); + protocol_type = _jret["protocol_type"].asInt(); + protocol_sub_type = _jret["protocol_sub_type"].asInt(); + protocol_flag = _jret["protocol_flag"].asUInt(); + record_flag = _jret["record_flag"].asUInt(); + auth_type = _jret["auth_type"].asInt(); + _enc = _jret["_enc"].asBool(); + + + // 进一步判断参数是否合法 + // 注意,account_id可以为-1,表示这是一次测试连接。 + if (user_id <= 0 || host_id <= 0 + || user_username.length() == 0 + || host_ip.length() == 0 || conn_ip.length() == 0 || client_ip.length() == 0 + || conn_port <= 0 || conn_port >= 65535 + || acc_username.length() == 0 || acc_secret.length() == 0 + || !(protocol_type == TP_PROTOCOL_TYPE_RDP || protocol_type == TP_PROTOCOL_TYPE_SSH || protocol_type == TP_PROTOCOL_TYPE_TELNET) + || !(auth_type == TP_AUTH_TYPE_NONE || auth_type == TP_AUTH_TYPE_PASSWORD || auth_type == TP_AUTH_TYPE_PRIVATE_KEY) + ) + { + return TPE_PARAM; + } + + if (_enc) { + ex_astr _auth; + if (!ts_db_field_decrypt(acc_secret, _auth)) + return TPE_FAILED; + + acc_secret = _auth; + } + + info.user_id = user_id; + info.host_id = host_id; + info.acc_id = acc_id; + info.user_username = user_username; + info.host_ip = host_ip; + info.conn_ip = conn_ip; + info.conn_port = conn_port; + info.client_ip = client_ip; + info.acc_username = acc_username; + info.acc_secret = acc_secret; + info.username_prompt = username_prompt; + info.password_prompt = password_prompt; + info.protocol_type = protocol_type; + info.protocol_sub_type = protocol_sub_type; + info.auth_type = auth_type; + info.protocol_flag = protocol_flag; + info.record_flag = record_flag; + + return TPE_OK; +} + +bool ts_web_rpc_session_begin(TS_CONNECT_INFO& info, int& record_id) +{ + //Json::FastWriter json_writer; + Json::Value jreq; + + jreq["method"] = "session_begin"; + jreq["param"]["sid"] = info.sid.c_str(); + jreq["param"]["user_id"] = info.user_id; + jreq["param"]["host_id"] = info.host_id; + jreq["param"]["acc_id"] = info.acc_id; + jreq["param"]["user_username"] = info.user_username.c_str(); + jreq["param"]["acc_username"] = info.acc_username.c_str(); + jreq["param"]["host_ip"] = info.host_ip.c_str(); + jreq["param"]["conn_ip"] = info.conn_ip.c_str(); + jreq["param"]["client_ip"] = info.client_ip.c_str(); + //jreq["param"]["sys_type"] = info.sys_type; + jreq["param"]["conn_port"] = info.conn_port; + jreq["param"]["auth_type"] = info.auth_type; + jreq["param"]["protocol_type"] = info.protocol_type; + jreq["param"]["protocol_sub_type"] = info.protocol_sub_type; + + ex_astr json_param; + //json_param = json_writer.write(jreq); + Json::StreamWriterBuilder jwb; + std::unique_ptr jwriter(jwb.newStreamWriter()); + ex_aoss os; + jwriter->write(jreq, &os); + json_param = os.str(); + + ex_astr param; + ts_url_encode(json_param.c_str(), param); + + ex_astr url = g_env.web_server_rpc; + url += "?"; + url += param; + + ex_astr body; + if (!ts_http_get(url, body)) + { + // EXLOGV("request `rpc::session_begin` from web return: "); + // EXLOGV(body.c_str()); + // EXLOGV("\n"); + return false; + } + + //Json::Reader jreader; + Json::Value jret; + + //if (!jreader.parse(body.c_str(), jret)) + Json::CharReaderBuilder jcrb; + std::unique_ptr const jreader(jcrb.newCharReader()); + const char *str_json_begin = body.c_str(); + ex_astr err; + + //if (!jreader.parse(func_args.c_str(), jsRoot)) { + if (!jreader->parse(str_json_begin, str_json_begin + body.length(), &jret, &err)) + return false; + if (!jret.isObject()) + return false; + if (!jret["data"].isObject()) + return false; + if (!jret["data"]["rid"].isUInt()) + return false; + + record_id = jret["data"]["rid"].asUInt(); + + return true; +} + +bool ts_web_rpc_session_update(int record_id, int protocol_sub_type, int state) { + //Json::FastWriter json_writer; + Json::Value jreq; + jreq["method"] = "session_update"; + jreq["param"]["rid"] = record_id; + jreq["param"]["protocol_sub_type"] = protocol_sub_type; + jreq["param"]["code"] = state; + + ex_astr json_param; + //json_param = json_writer.write(jreq); + Json::StreamWriterBuilder jwb; + std::unique_ptr jwriter(jwb.newStreamWriter()); + ex_aoss os; + jwriter->write(jreq, &os); + json_param = os.str(); + + ex_astr param; + ts_url_encode(json_param.c_str(), param); + + ex_astr url = g_env.web_server_rpc; + url += "?"; + url += param; + + ex_astr body; + return ts_http_get(url, body); +} + + +//session 结束 +bool ts_web_rpc_session_end(const char* sid, int record_id, int ret_code) +{ + // TODO: 对指定的sid相关的会话的引用计数减一(但减到0时销毁) + + //Json::FastWriter json_writer; + Json::Value jreq; + jreq["method"] = "session_end"; + jreq["param"]["rid"] = record_id; + jreq["param"]["code"] = ret_code; + + ex_astr json_param; + //json_param = json_writer.write(jreq); + Json::StreamWriterBuilder jwb; + std::unique_ptr jwriter(jwb.newStreamWriter()); + ex_aoss os; + jwriter->write(jreq, &os); + json_param = os.str(); + + ex_astr param; + ts_url_encode(json_param.c_str(), param); + + ex_astr url = g_env.web_server_rpc; + url += "?"; + url += param; + + ex_astr body; + return ts_http_get(url, body); +}