From 11a7342e9f3d681ffbd22f1838dcf9eb4e74747d Mon Sep 17 00:00:00 2001 From: dantmnf Date: Mon, 5 Sep 2022 00:57:44 +0800 Subject: [PATCH] chore: split platform utils --- src/MeoAssistant/AsstPlatformPosix.cpp | 67 +++++ src/MeoAssistant/AsstPlatformWin32.cpp | 197 ++++++++++++++ src/MeoAssistant/AsstPlatformWin32.h | 13 + src/MeoAssistant/AsstUtils.cpp | 253 ------------------ src/MeoAssistant/MeoAssistant.vcxproj | 4 +- src/MeoAssistant/MeoAssistant.vcxproj.filters | 8 +- 6 files changed, 287 insertions(+), 255 deletions(-) create mode 100644 src/MeoAssistant/AsstPlatformPosix.cpp create mode 100644 src/MeoAssistant/AsstPlatformWin32.cpp create mode 100644 src/MeoAssistant/AsstPlatformWin32.h delete mode 100644 src/MeoAssistant/AsstUtils.cpp diff --git a/src/MeoAssistant/AsstPlatformPosix.cpp b/src/MeoAssistant/AsstPlatformPosix.cpp new file mode 100644 index 0000000000..8a187e35e7 --- /dev/null +++ b/src/MeoAssistant/AsstPlatformPosix.cpp @@ -0,0 +1,67 @@ +#if __has_include() +#include "AsstPlatformPosix.h" +#include "AsstUtils.hpp" +std::string asst::utils::callcmd(const std::string& cmdline) +{ + constexpr int PipeBuffSize = 4096; + std::string pipe_str; + auto pipe_buffer = std::make_unique(PipeBuffSize); + + constexpr static int PIPE_READ = 0; + constexpr static int PIPE_WRITE = 1; + int pipe_in[2] = { 0 }; + int pipe_out[2] = { 0 }; + int pipe_in_ret = pipe(pipe_in); + int pipe_out_ret = pipe(pipe_out); + if (pipe_in_ret != 0 || pipe_out_ret != 0) { + return {}; + } + fcntl(pipe_out[PIPE_READ], F_SETFL, O_NONBLOCK); + int exit_ret = 0; + int child = fork(); + if (child == 0) { + // child process + dup2(pipe_in[PIPE_READ], STDIN_FILENO); + dup2(pipe_out[PIPE_WRITE], STDOUT_FILENO); + dup2(pipe_out[PIPE_WRITE], STDERR_FILENO); + + // all these are for use by parent only + close(pipe_in[PIPE_READ]); + close(pipe_in[PIPE_WRITE]); + close(pipe_out[PIPE_READ]); + close(pipe_out[PIPE_WRITE]); + + exit_ret = execlp("sh", "sh", "-c", cmdline.c_str(), nullptr); + exit(exit_ret); + } + else if (child > 0) { + // parent process + + // close unused file descriptors, these are for child only + close(pipe_in[PIPE_READ]); + close(pipe_out[PIPE_WRITE]); + + do { + ssize_t read_num = read(pipe_out[PIPE_READ], pipe_buffer.get(), PipeBuffSize); + + while (read_num > 0) { + pipe_str.append(pipe_buffer.get(), pipe_buffer.get() + read_num); + read_num = read(pipe_out[PIPE_READ], pipe_buffer.get(), PipeBuffSize); + }; + } while (::waitpid(child, &exit_ret, WNOHANG) == 0); + + close(pipe_in[PIPE_WRITE]); + close(pipe_out[PIPE_READ]); + } + else { + // failed to create child process + close(pipe_in[PIPE_READ]); + close(pipe_in[PIPE_WRITE]); + close(pipe_out[PIPE_READ]); + close(pipe_out[PIPE_WRITE]); + } + return pipe_str; +} + + +#endif diff --git a/src/MeoAssistant/AsstPlatformWin32.cpp b/src/MeoAssistant/AsstPlatformWin32.cpp new file mode 100644 index 0000000000..8cd4f81979 --- /dev/null +++ b/src/MeoAssistant/AsstPlatformWin32.cpp @@ -0,0 +1,197 @@ +#ifdef _WIN32 +#include "AsstPlatformWin32.h" + +#include +#include +#include + +#include "AsstUtils.hpp" +#include "Logger.hpp" + +bool asst::win32::CreateOverlappablePipe(HANDLE* read, HANDLE* write, SECURITY_ATTRIBUTES* secattr_read, + SECURITY_ATTRIBUTES* secattr_write, DWORD bufsize, bool overlapped_read, + bool overlapped_write) +{ + static std::atomic pipeid {}; + auto pipename = std::format(L"\\\\.\\pipe\\maa-pipe-{}-{}", GetCurrentProcessId(), pipeid++); + DWORD read_flag = PIPE_ACCESS_INBOUND; + if (overlapped_read) read_flag |= FILE_FLAG_OVERLAPPED; + DWORD write_flag = GENERIC_WRITE; + if (overlapped_write) write_flag |= FILE_FLAG_OVERLAPPED; + auto pipe_read = + CreateNamedPipeW(pipename.c_str(), read_flag, PIPE_TYPE_BYTE | PIPE_WAIT, 1, bufsize, bufsize, 0, secattr_read); + if (pipe_read == INVALID_HANDLE_VALUE) return false; + auto pipe_write = + CreateFileW(pipename.c_str(), write_flag, 0, secattr_write, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (pipe_write == INVALID_HANDLE_VALUE) { + CloseHandle(pipe_read); + return false; + } + + *read = pipe_read; + *write = pipe_write; + return true; +} + + +std::string asst::utils::path_to_crt_string(const std::filesystem::path& path) +{ + // UCRT may use UTF-8 encoding while ANSI code page is still some other MBCS encoding + // so we use CRT wcstombs instead of WideCharToMultiByte + size_t mbsize; + auto osstr = path.native(); + auto err = wcstombs_s(&mbsize, nullptr, 0, osstr.c_str(), osstr.size()); + if (err == 0) { + std::string result(mbsize, 0); + err = wcstombs_s(&mbsize, &result[0], mbsize, osstr.c_str(), osstr.size()); + if (err != 0) return {}; + return result.substr(0, mbsize - 1); + } + else { + // cannot convert (CRT is not using UTF-8), fallback to short path name in ACP + wchar_t short_path[MAX_PATH]; + auto shortlen = GetShortPathNameW(osstr.c_str(), short_path, MAX_PATH); + if (shortlen == 0) return {}; + BOOL failed = FALSE; + auto ansilen = WideCharToMultiByte(CP_ACP, 0, short_path, shortlen, nullptr, 0, nullptr, &failed); + if (failed) return {}; + std::string result(ansilen, 0); + WideCharToMultiByte(CP_ACP, 0, short_path, shortlen, &result[0], ansilen, nullptr, nullptr); + return result; + } +} + +std::string asst::utils::path_to_ansi_string(const std::filesystem::path& path) +{ + // UCRT may use UTF-8 encoding while ANSI code page is still some other MBCS encoding + // so we use CRT wcstombs instead of WideCharToMultiByte + BOOL failed = FALSE; + auto osstr = path.native(); + auto ansilen = WideCharToMultiByte(CP_ACP, 0, osstr.c_str(), (int)osstr.size(), nullptr, 0, nullptr, &failed); + if (!failed) { + std::string result(ansilen, 0); + WideCharToMultiByte(CP_ACP, 0, osstr.c_str(), (int)osstr.size(), &result[0], ansilen, nullptr, &failed); + return result; + } + else { + // contains character that cannot be converted, fallback to short path name in ACP + wchar_t short_path[MAX_PATH]; + auto shortlen = GetShortPathNameW(osstr.c_str(), short_path, MAX_PATH); + if (shortlen == 0) return {}; + ansilen = WideCharToMultiByte(CP_ACP, 0, short_path, shortlen, nullptr, 0, nullptr, &failed); + if (failed) return {}; + std::string result(ansilen, 0); + WideCharToMultiByte(CP_ACP, 0, short_path, shortlen, &result[0], ansilen, nullptr, nullptr); + return result; + } +} + +asst::utils::os_string asst::utils::to_osstring(const std::string& utf8_str) +{ + int len = MultiByteToWideChar(CP_UTF8, 0, utf8_str.c_str(), (int)utf8_str.size(), nullptr, 0); + asst::utils::os_string result(len, 0); + MultiByteToWideChar(CP_UTF8, 0, utf8_str.c_str(), (int)utf8_str.size(), &result[0], len); + return result; +} + +std::string asst::utils::from_osstring(const asst::utils::os_string& os_str) +{ + int len = WideCharToMultiByte(CP_UTF8, 0, os_str.c_str(), (int)os_str.size(), nullptr, 0, nullptr, nullptr); + std::string result(len, 0); + WideCharToMultiByte(CP_UTF8, 0, os_str.c_str(), (int)os_str.size(), &result[0], len, nullptr, nullptr); + return result; +} + +std::string asst::utils::callcmd(const std::string& cmdline) +{ + constexpr int PipeBuffSize = 4096; + auto pipe_buffer = std::make_unique(PipeBuffSize); + std::string pipe_str; + + DWORD err; + HANDLE pipe_parent_read, pipe_child_write; + SECURITY_ATTRIBUTES sa_inherit { .nLength = sizeof(SECURITY_ATTRIBUTES), .bInheritHandle = TRUE }; + if (!asst::win32::CreateOverlappablePipe(&pipe_parent_read, &pipe_child_write, nullptr, &sa_inherit, PipeBuffSize, + true, false)) { + err = GetLastError(); + asst::Log.error("CreateOverlappablePipe failed, err", err); + return {}; + } + + STARTUPINFOW si {}; + si.cb = sizeof(STARTUPINFOW); + si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW; + si.wShowWindow = SW_HIDE; + si.hStdOutput = pipe_child_write; + si.hStdError = pipe_child_write; + ASST_AUTO_DEDUCED_ZERO_INIT_START + PROCESS_INFORMATION process_info = { nullptr }; + ASST_AUTO_DEDUCED_ZERO_INIT_END + + auto cmdline_osstr = asst::utils::to_osstring(cmdline); + BOOL create_ret = + CreateProcessW(nullptr, &cmdline_osstr[0], nullptr, nullptr, TRUE, 0, nullptr, nullptr, &si, &process_info); + if (!create_ret) { + Log.error("Call `", cmdline, "` create process failed, ret", create_ret); + return {}; + } + + CloseHandle(pipe_child_write); + + std::vector wait_handles; + wait_handles.reserve(2); + bool process_running = true; + bool pipe_eof = false; + + OVERLAPPED pipeov { .hEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr) }; + (void)ReadFile(pipe_parent_read, pipe_buffer.get(), PipeBuffSize, nullptr, &pipeov); + + while (1) { + wait_handles.clear(); + if (process_running) wait_handles.push_back(process_info.hProcess); + if (!pipe_eof) wait_handles.push_back(pipeov.hEvent); + if (wait_handles.empty()) break; + auto wait_result = + WaitForMultipleObjectsEx((DWORD)wait_handles.size(), &wait_handles[0], FALSE, INFINITE, TRUE); + HANDLE signaled_object = INVALID_HANDLE_VALUE; + if (wait_result >= WAIT_OBJECT_0 && wait_result < WAIT_OBJECT_0 + wait_handles.size()) { + signaled_object = wait_handles[(size_t)wait_result - WAIT_OBJECT_0]; + } + else if (wait_result == WAIT_TIMEOUT) { + continue; + } + else { + // something bad happened + err = GetLastError(); + throw std::system_error(std::error_code(err, std::system_category())); + } + + if (signaled_object == process_info.hProcess) { + process_running = false; + } + else if (signaled_object == pipeov.hEvent) { + // pipe read + DWORD len = 0; + if (GetOverlappedResult(pipe_parent_read, &pipeov, &len, FALSE)) { + pipe_str.insert(pipe_str.end(), pipe_buffer.get(), pipe_buffer.get() + len); + (void)ReadFile(pipe_parent_read, pipe_buffer.get(), PipeBuffSize, nullptr, &pipeov); + } + else { + err = GetLastError(); + if (err == ERROR_HANDLE_EOF || err == ERROR_BROKEN_PIPE) { + pipe_eof = true; + } + } + } + } + + DWORD exit_ret = 0; + GetExitCodeProcess(process_info.hProcess, &exit_ret); + CloseHandle(process_info.hProcess); + CloseHandle(process_info.hThread); + CloseHandle(pipe_parent_read); + CloseHandle(pipeov.hEvent); + return pipe_str; +} + +#endif diff --git a/src/MeoAssistant/AsstPlatformWin32.h b/src/MeoAssistant/AsstPlatformWin32.h new file mode 100644 index 0000000000..ccf171090b --- /dev/null +++ b/src/MeoAssistant/AsstPlatformWin32.h @@ -0,0 +1,13 @@ +#pragma once +#ifdef _WIN32 + +#include "SafeWindows.h" + +namespace asst::win32 +{ + bool CreateOverlappablePipe(HANDLE* read, HANDLE* write, SECURITY_ATTRIBUTES* secattr_read, + SECURITY_ATTRIBUTES* secattr_write, DWORD bufsize, bool overlapped_read, + bool overlapped_write); +} + +#endif diff --git a/src/MeoAssistant/AsstUtils.cpp b/src/MeoAssistant/AsstUtils.cpp deleted file mode 100644 index 1a3d536644..0000000000 --- a/src/MeoAssistant/AsstUtils.cpp +++ /dev/null @@ -1,253 +0,0 @@ -#include "AsstUtils.hpp" - -#ifdef _WIN32 - -#include - -std::string asst::utils::path_to_crt_string(const std::filesystem::path& path) -{ - // UCRT may use UTF-8 encoding while ANSI code page is still some other MBCS encoding - // so we use CRT wcstombs instead of WideCharToMultiByte - size_t mbsize; - auto osstr = path.native(); - auto err = wcstombs_s(&mbsize, nullptr, 0, osstr.c_str(), osstr.size()); - if (err == 0) { - std::string result(mbsize, 0); - err = wcstombs_s(&mbsize, &result[0], mbsize, osstr.c_str(), osstr.size()); - if (err != 0) return {}; - return result.substr(0, mbsize - 1); - } - else { - // cannot convert (CRT is not using UTF-8), fallback to short path name in ACP - wchar_t short_path[MAX_PATH]; - auto shortlen = GetShortPathNameW(osstr.c_str(), short_path, MAX_PATH); - if (shortlen == 0) return {}; - BOOL failed = FALSE; - auto ansilen = WideCharToMultiByte(CP_ACP, 0, short_path, shortlen, nullptr, 0, nullptr, &failed); - if (failed) return {}; - std::string result(ansilen, 0); - WideCharToMultiByte(CP_ACP, 0, short_path, shortlen, &result[0], ansilen, nullptr, nullptr); - return result; - } -} - -std::string asst::utils::path_to_ansi_string(const std::filesystem::path& path) -{ - // UCRT may use UTF-8 encoding while ANSI code page is still some other MBCS encoding - // so we use CRT wcstombs instead of WideCharToMultiByte - BOOL failed = FALSE; - auto osstr = path.native(); - auto ansilen = WideCharToMultiByte(CP_ACP, 0, osstr.c_str(), (int)osstr.size(), nullptr, 0, nullptr, &failed); - if (!failed) { - std::string result(ansilen, 0); - WideCharToMultiByte(CP_ACP, 0, osstr.c_str(), (int)osstr.size(), &result[0], ansilen, nullptr, &failed); - return result; - } - else { - // contains character that cannot be converted, fallback to short path name in ACP - wchar_t short_path[MAX_PATH]; - auto shortlen = GetShortPathNameW(osstr.c_str(), short_path, MAX_PATH); - if (shortlen == 0) return {}; - ansilen = WideCharToMultiByte(CP_ACP, 0, short_path, shortlen, nullptr, 0, nullptr, &failed); - if (failed) return {}; - std::string result(ansilen, 0); - WideCharToMultiByte(CP_ACP, 0, short_path, shortlen, &result[0], ansilen, nullptr, nullptr); - return result; - } -} - -asst::utils::os_string asst::utils::to_osstring(const std::string& utf8_str) -{ - int len = MultiByteToWideChar(CP_UTF8, 0, utf8_str.c_str(), (int)utf8_str.size(), nullptr, 0); - asst::utils::os_string result(len, 0); - MultiByteToWideChar(CP_UTF8, 0, utf8_str.c_str(), (int)utf8_str.size(), &result[0], len); - return result; -} - -std::string asst::utils::from_osstring(const asst::utils::os_string& os_str) -{ - int len = WideCharToMultiByte(CP_UTF8, 0, os_str.c_str(), (int)os_str.size(), nullptr, 0, nullptr, nullptr); - std::string result(len, 0); - WideCharToMultiByte(CP_UTF8, 0, os_str.c_str(), (int)os_str.size(), &result[0], len, nullptr, nullptr); - return result; -} - - -std::string asst::utils::callcmd(const std::string& cmdline) -{ - static std::atomic pipeid {}; - constexpr int PipeBuffSize = 4096; - std::string pipe_str; - auto pipe_buffer = std::make_unique(PipeBuffSize); - ASST_AUTO_DEDUCED_ZERO_INIT_START - SECURITY_ATTRIBUTES pipe_sec_attr = { 0 }; - ASST_AUTO_DEDUCED_ZERO_INIT_END - pipe_sec_attr.nLength = sizeof(SECURITY_ATTRIBUTES); - pipe_sec_attr.lpSecurityDescriptor = nullptr; - pipe_sec_attr.bInheritHandle = TRUE; - auto pipename = std::format(L"\\\\.\\pipe\\maa-callcmd-{}-{}", GetCurrentProcessId(), pipeid++); - auto pipe_read = - CreateNamedPipeW(pipename.c_str(), PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED, PIPE_TYPE_BYTE | PIPE_WAIT, 1, PipeBuffSize, PipeBuffSize, 0, nullptr); - auto pipe_child_write = - CreateFileW(pipename.c_str(), GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); - - ASST_AUTO_DEDUCED_ZERO_INIT_START - STARTUPINFOW si = { 0 }; - ASST_AUTO_DEDUCED_ZERO_INIT_END - si.cb = sizeof(STARTUPINFOW); - si.dwFlags = STARTF_USESTDHANDLES; - si.wShowWindow = SW_HIDE; - si.hStdOutput = pipe_child_write; - si.hStdError = pipe_child_write; - - ASST_AUTO_DEDUCED_ZERO_INIT_START - PROCESS_INFORMATION pi = { nullptr }; - ASST_AUTO_DEDUCED_ZERO_INIT_END - - auto cmdline_osstr = asst::utils::to_osstring(cmdline); - BOOL p_ret = CreateProcessW(nullptr, &cmdline_osstr[0], nullptr, nullptr, TRUE, CREATE_NO_WINDOW, nullptr, nullptr, - &si, &pi); - if (p_ret) { - DWORD read_num = 0; - OVERLAPPED ov { .hEvent = CreateEventW(nullptr, FALSE, FALSE, nullptr) }; - bool reading = false; - bool process_running = true; - bool pipe_eof = false; - bool process_signaled = false, pipe_signaled = false; - HANDLE wait_handles[] { pi.hProcess, ov.hEvent }; - while (process_running || !pipe_eof) { - if (!pipe_eof && !reading) { - (void)ReadFile(pipe_read, pipe_buffer.get(), PipeBuffSize, nullptr, &ov); - reading = true; - } - DWORD wait_result = WAIT_FAILED; - if (process_running && !pipe_eof) { - wait_result = WaitForMultipleObjects(2, wait_handles, FALSE, INFINITE); - process_signaled = wait_result == WAIT_OBJECT_0; - pipe_signaled = wait_result == WAIT_OBJECT_0 + 1; - } - else if (!pipe_eof) { // process exited, pipe not eof yet - wait_result = WaitForMultipleObjects(1, wait_handles + 1, FALSE, INFINITE); - pipe_signaled = wait_result == WAIT_OBJECT_0; - } - else if (process_running) { // pipe eof, process not exit yet - wait_result = WaitForMultipleObjects(1, wait_handles, FALSE, INFINITE); - process_signaled = wait_result == WAIT_OBJECT_0; - } - if (wait_result == WAIT_FAILED) { - // something bad happened - abort(); - } - if (process_signaled) { - // process died, wait for data remaining in pipe - process_running = false; - } - if (pipe_signaled) { - reading = false; - // pipe read - if (GetOverlappedResult(pipe_read, &ov, &read_num, TRUE)) { - pipe_str.append(pipe_buffer.get(), pipe_buffer.get() + read_num); - } - else { - if (GetLastError() == ERROR_HANDLE_EOF) { - pipe_eof = true; - } - } - } - }; - - // fetch data in pipe buffer (if any) - //if (reading) { - // if (GetOverlappedResult(pipe_read, &ov, &read_num, FALSE)) { - // pipe_str.append(pipe_buffer.get(), pipe_buffer.get() + read_num); - // } - // else { - // CancelIo(pipe_read); - // } - //} - - //while (PeekNamedPipe(pipe_read, nullptr, 0, nullptr, &peek_num, nullptr) && peek_num > 0) { - // ReadFile(pipe_read, pipe_buffer.get(), PipeBuffSize, nullptr, &ov); - // if (GetOverlappedResult(pipe_read, &ov, &read_num, TRUE)) { - // pipe_str.append(pipe_buffer.get(), pipe_buffer.get() + read_num); - // } - //} - - DWORD exit_ret = 255; - GetExitCodeProcess(pi.hProcess, &exit_ret); - CloseHandle(pi.hProcess); - CloseHandle(pi.hThread); - CloseHandle(ov.hEvent); - } - - CloseHandle(pipe_read); - CloseHandle(pipe_child_write); - - return pipe_str; -} - -#else -std::string asst::utils::callcmd(const std::string& cmdline) -{ - constexpr int PipeBuffSize = 4096; - std::string pipe_str; - auto pipe_buffer = std::make_unique(PipeBuffSize); - - constexpr static int PIPE_READ = 0; - constexpr static int PIPE_WRITE = 1; - int pipe_in[2] = { 0 }; - int pipe_out[2] = { 0 }; - int pipe_in_ret = pipe(pipe_in); - int pipe_out_ret = pipe(pipe_out); - if (pipe_in_ret != 0 || pipe_out_ret != 0) { - return {}; - } - fcntl(pipe_out[PIPE_READ], F_SETFL, O_NONBLOCK); - int exit_ret = 0; - int child = fork(); - if (child == 0) { - // child process - dup2(pipe_in[PIPE_READ], STDIN_FILENO); - dup2(pipe_out[PIPE_WRITE], STDOUT_FILENO); - dup2(pipe_out[PIPE_WRITE], STDERR_FILENO); - - // all these are for use by parent only - close(pipe_in[PIPE_READ]); - close(pipe_in[PIPE_WRITE]); - close(pipe_out[PIPE_READ]); - close(pipe_out[PIPE_WRITE]); - - exit_ret = execlp("sh", "sh", "-c", cmdline.c_str(), nullptr); - exit(exit_ret); - } - else if (child > 0) { - // parent process - - // close unused file descriptors, these are for child only - close(pipe_in[PIPE_READ]); - close(pipe_out[PIPE_WRITE]); - - do { - ssize_t read_num = read(pipe_out[PIPE_READ], pipe_buffer.get(), PipeBuffSize); - - while (read_num > 0) { - pipe_str.append(pipe_buffer.get(), pipe_buffer.get() + read_num); - read_num = read(pipe_out[PIPE_READ], pipe_buffer.get(), PipeBuffSize); - }; - } while (::waitpid(child, &exit_ret, WNOHANG) == 0); - - close(pipe_in[PIPE_WRITE]); - close(pipe_out[PIPE_READ]); - } - else { - // failed to create child process - close(pipe_in[PIPE_READ]); - close(pipe_in[PIPE_WRITE]); - close(pipe_out[PIPE_READ]); - close(pipe_out[PIPE_WRITE]); - } - return pipe_str; -} - - -#endif diff --git a/src/MeoAssistant/MeoAssistant.vcxproj b/src/MeoAssistant/MeoAssistant.vcxproj index 9bc11be127..c2c6112a7d 100644 --- a/src/MeoAssistant/MeoAssistant.vcxproj +++ b/src/MeoAssistant/MeoAssistant.vcxproj @@ -23,6 +23,7 @@ + @@ -123,7 +124,8 @@ - + + diff --git a/src/MeoAssistant/MeoAssistant.vcxproj.filters b/src/MeoAssistant/MeoAssistant.vcxproj.filters index faa3bb0b93..9a7fb630ee 100644 --- a/src/MeoAssistant/MeoAssistant.vcxproj.filters +++ b/src/MeoAssistant/MeoAssistant.vcxproj.filters @@ -402,6 +402,9 @@ 头文件\Utils + + 头文件\Utils + @@ -674,7 +677,10 @@ 源文件\Task\Sub - + + 源文件 + + 源文件