• 在服务中以当前用户身份启动一个程序


    在CodeProject上看到一个Demo, 在服务中以当前用户身份启动一个程序.

    跟帖的人指出了一些bug, 我整理了一下, 将跟帖人指出的bug在工程中修正.

    他提供的类, 也有一个小bug, 没有被跟帖的人指出, 被我发现并修正.

    这个Demo整理后, 被我用在项目中, 用起来效果还不错.

    以当前登录用户的身份运行一个程序的好处: 

    * 可以避免权限问题. e.g. 文件建立后, 当前用户打不开.

    * 有些程序或部件运行, 是需要Windows窗口消息的, 不能直接在服务中运行.

    工程下载点: src_bk_2015_0722_1601_prj_run_cur_user_prog_on_service.zip

    编译环境: vs2010 vc++

    备注 :  

     /// @todo ls 服务启动停止时, 检测服务是否已经在运行或停止的处理要加上, 提高效率.

    /// 如果硬生生的启动停止服务时, 还要启动停止桌面上的程序, 在没有检测服务状态时, 要花费的时间多些.

    效果图:

    工程预览:

    [cpp] view plaincopy
     
    1. // lsServiceForTest.cpp : Defines the entry point for the application.  
    2. //  
    3.   
    4. #include "stdafx.h"  
    5. #include <process.h>  
    6. #include "lsServiceForTest.h"  
    7. #include "ProcessStarter.h"  
    8.   
    9. #define SERVICE_VER_W L"1, 0, 0, 1"  
    10. #define PROJECT_MODIFY_TIME L"2015-0722-1426"  
    11.   
    12. VOID WINAPI ServiceMain(DWORD dwArgc, LPWSTR *lpszArgv);  
    13. SERVICE_TABLE_ENTRYW lpServiceStartTable[] =   
    14. {  
    15.     {SERVICE_NAME_W, ServiceMain},  
    16.     {NULL, NULL}  
    17. };  
    18.   
    19. SERVICE_STATUS_HANDLE g_hServiceCtrlHandler = NULL;   
    20. SERVICE_STATUS g_ServiceStatus;  
    21. std::wstring g_strPathNameMe = L"";  
    22. std::wstring g_strCmdLine = L"";  
    23. ns_base::CThreadManager g_ThreadManager;  
    24.   
    25. VOID ServiceMainProc();  
    26. static UINT WINAPI ThreadProcWorker(void* pParam);  
    27. BOOL ThreadProcStart_Worker();  
    28. BOOL ThreadProcStop_Worker();  
    29.   
    30. BOOL GetObjProgInfo(DWORD dwSessionId, OUT std::wstring& strObjPathName, OUT std::wstring& strCmdLine);  
    31.   
    32. VOID ExecuteAsService();  
    33. VOID WINAPI ServiceHandler(DWORD fdwControl);  
    34.   
    35. int APIENTRY _tWinMain(HINSTANCE hInstance,  
    36.     HINSTANCE hPrevInstance,  
    37.     LPTSTR    lpCmdLine,  
    38.     int       nCmdShow)  
    39. {  
    40.     std::wstring strLogFilePathName = L"";  
    41.   
    42.     ns_base::GetFilePathName_Me(g_strPathNameMe);  
    43.     g_strCmdLine = (NULL != lpCmdLine) ? lpCmdLine : L"";  
    44.   
    45.     strLogFilePathName = ns_business::GetLogPathName_lsServiceForTest().c_str();  
    46.     SetLogFilePathName(strLogFilePathName.c_str());  
    47.   
    48.     ServiceMainProc();  
    49.     return 0;  
    50. }  
    51.   
    52. VOID ServiceMainProc()  
    53. {  
    54.     WriteLogEx(L">> ServiceMainProc() [%s][%s][%s]", SERVICE_NAME_W, SERVICE_VER_W, PROJECT_MODIFY_TIME);  
    55.   
    56.     if (ns_base::StringCompare_equ(g_strCmdLine.c_str(), L"-i")  
    57.         || ns_base::StringCompare_equ(g_strCmdLine.c_str(), L"-I"))  
    58.     {  
    59.         ns_base::ServiceInstall(g_strPathNameMe.c_str(), SERVICE_NAME_W);  
    60.     }  
    61.     else if (ns_base::StringCompare_equ(g_strCmdLine.c_str(), L"-s")  
    62.         || ns_base::StringCompare_equ(g_strCmdLine.c_str(), L"-S"))  
    63.     {  
    64.         ns_base::ServiceStart(SERVICE_NAME_W);  
    65.     }  
    66.     else if (ns_base::StringCompare_equ(g_strCmdLine.c_str(), L"-k")  
    67.         || ns_base::StringCompare_equ(g_strCmdLine.c_str(), L"-K"))  
    68.     {  
    69.         ns_base::ServiceStop(SERVICE_NAME_W);  
    70.     }  
    71.     else if (ns_base::StringCompare_equ(g_strCmdLine.c_str(), L"-u")  
    72.         || ns_base::StringCompare_equ(g_strCmdLine.c_str(), L"-U"))  
    73.     {  
    74.         ns_base::ServiceUnInstall(SERVICE_NAME_W);  
    75.     }  
    76.     else  
    77.         ExecuteAsService();  
    78.   
    79.     WriteLogEx(L"<< ServiceMainProc() [%s][%s][%s]", SERVICE_NAME_W, SERVICE_VER_W, PROJECT_MODIFY_TIME);  
    80. }  
    81.   
    82. VOID ExecuteAsService()  
    83. {  
    84.     WriteLogEx(L">> ExecuteAsService");  
    85.   
    86.     if(!ThreadProcStart_Worker())  
    87.     {  
    88.         WriteLogEx(L"ThreadProcStart_Worker failed[%d]", GetLastError());  
    89.     }  
    90.   
    91.     if(!StartServiceCtrlDispatcherW(lpServiceStartTable))  
    92.     {  
    93.         WriteLogEx(L"StartServiceCtrlDispatcher failed[%d]", GetLastError());  
    94.     }  
    95.   
    96.     WriteLogEx(L"<< ExecuteAsService");  
    97. }  
    98.   
    99. VOID WINAPI ServiceMain(DWORD dwArgc, LPWSTR* lpszArgv)  
    100. {  
    101.     WriteLogEx(L">> ServiceMain(%d, lpszArgv)", dwArgc);  
    102.     do   
    103.     {  
    104.         g_ServiceStatus.dwServiceType = SERVICE_WIN32;   
    105.         g_ServiceStatus.dwCurrentState = SERVICE_START_PENDING;   
    106.         g_ServiceStatus.dwControlsAccepted =   
    107.             SERVICE_ACCEPT_STOP  
    108.             | SERVICE_ACCEPT_PAUSE_CONTINUE  
    109.             | SERVICE_ACCEPT_SHUTDOWN  
    110.             | SERVICE_ACCEPT_PARAMCHANGE  
    111.             | SERVICE_ACCEPT_NETBINDCHANGE  
    112.             | SERVICE_ACCEPT_HARDWAREPROFILECHANGE  
    113.             | SERVICE_ACCEPT_POWEREVENT  
    114.             | SERVICE_ACCEPT_SESSIONCHANGE  
    115.             | SERVICE_ACCEPT_PRESHUTDOWN  
    116.             | SERVICE_ACCEPT_TIMECHANGE  
    117.             | SERVICE_ACCEPT_TRIGGEREVENT;  
    118.   
    119.         g_ServiceStatus.dwWin32ExitCode = 0;   
    120.         g_ServiceStatus.dwServiceSpecificExitCode = 0;  
    121.         g_ServiceStatus.dwCheckPoint = 0;   
    122.         g_ServiceStatus.dwWaitHint = 0;   
    123.   
    124.         g_hServiceCtrlHandler = RegisterServiceCtrlHandlerW(SERVICE_NAME_W, ServiceHandler);   
    125.         if (NULL == g_hServiceCtrlHandler)   
    126.         {  
    127.             ns_base::NotifyFailed_RegisterServiceCtrlHandler(GetLastError(), SERVICE_NAME_W);  
    128.             break;  
    129.         }   
    130.   
    131.         // Initialization complete - report running status   
    132.         g_ServiceStatus.dwCurrentState = SERVICE_RUNNING;   
    133.         g_ServiceStatus.dwCheckPoint = 0;   
    134.         g_ServiceStatus.dwWaitHint = 0;    
    135.         if (!SetServiceStatus(g_hServiceCtrlHandler, &g_ServiceStatus))   
    136.         {   
    137.             ns_base::NotifyFailed_SetServiceStatus(GetLastError(), SERVICE_NAME_W);  
    138.         }   
    139.     } while (0);  
    140.   
    141.     WriteLogEx(L"<< ServiceMain(%d, lpszArgv)", dwArgc);  
    142. }  
    143.   
    144. VOID WINAPI ServiceHandler(DWORD fdwControl)  
    145. {  
    146.     int iIndex = 0;  
    147.   
    148.     WriteLogEx(L">> ServiceHandler(%d)", fdwControl);  
    149.     switch(fdwControl)   
    150.     {  
    151.     case SERVICE_CONTROL_STOP:  
    152.     case SERVICE_CONTROL_SHUTDOWN:  
    153.         g_ThreadManager.StopThread(TRUE, L"g_ThreadManager");  
    154.         g_ServiceStatus.dwWin32ExitCode = 0;   
    155.         g_ServiceStatus.dwCurrentState  = SERVICE_STOPPED;   
    156.         g_ServiceStatus.dwCheckPoint    = 0;   
    157.         g_ServiceStatus.dwWaitHint      = 0;  
    158.   
    159.         // terminate all processes started by this service before shutdown  
    160.         ns_business::StopAndKill_dlgNotify();  
    161.         break;   
    162.     case SERVICE_CONTROL_PAUSE:  
    163.         g_ServiceStatus.dwCurrentState = SERVICE_PAUSED;   
    164.         break;  
    165.     case SERVICE_CONTROL_CONTINUE:  
    166.         g_ServiceStatus.dwCurrentState = SERVICE_RUNNING;   
    167.         break;  
    168.     default:  
    169.         WriteLogEx(L"Unrecognized opcode %d ", fdwControl);  
    170.     };  
    171.   
    172.     if (!SetServiceStatus(g_hServiceCtrlHandler,  &g_ServiceStatus))   
    173.     {   
    174.         ns_base::NotifyFailed_SetServiceStatus(GetLastError(), SERVICE_NAME_W);  
    175.     }  
    176.   
    177.     WriteLogEx(L"<< ServiceHandler(%d)", fdwControl);  
    178. }  
    179.   
    180. static UINT WINAPI ThreadProcWorker(void* pParam)  
    181. {  
    182.     BOOL bLog = TRUE;  
    183.     DWORD dwSessionIdPrev = -1;  
    184.     DWORD dwSessionId = -1;  
    185.     size_t nSleepTotal = 0;  
    186.     UINT uRc = S_FALSE;  
    187.   
    188.     std::wstring strObjPathName = L"";  
    189.     std::wstring strCmdLine = L"";  
    190.   
    191.     ns_base::TAG_THREAD_MANAGER_PARAM ThreadManagerParam;  
    192.     ns_base::TAG_THREAD_MANAGER_PARAM* pThreadManagerParam = NULL;  
    193.     CProcessStarter ProcessStarter;  
    194.   
    195.     WriteLogEx(L">> lsServiceForTest ThreadProcWorker");  
    196.     do   
    197.     {  
    198.         if (NULL == pParam)  
    199.             break;  
    200.   
    201.         pThreadManagerParam = (ns_base::TAG_THREAD_MANAGER_PARAM*)pParam;  
    202.         ThreadManagerParam.copy((ns_base::TAG_THREAD_MANAGER_PARAM*)pParam);  
    203.         SAFE_DELETE(pThreadManagerParam);  
    204.         if (NULL == ThreadManagerParam.pThreadManager)  
    205.             break;  
    206.   
    207.         while(!ThreadManagerParam.pThreadManager->IsNeedQuitThread())  
    208.         {  
    209.             if (!ns_base::SleepContinueEx(2000, 100, nSleepTotal))  
    210.                 continue;  
    211.   
    212.             /// 保证 只有在SessionId变化的时候, 才打印 FindActiveSessionId 的日志  
    213.             if (!ProcessStarter.FindActiveSessionId(dwSessionId, bLog)  
    214.                 || (dwSessionIdPrev == dwSessionId))  
    215.             {  
    216.                 if (bLog)  
    217.                     bLog = FALSE;  
    218.   
    219.                 continue;  
    220.             }  
    221.   
    222.             if (!bLog)  
    223.                 bLog = TRUE;  
    224.   
    225.             /// 用户每次切换一次桌面, 我们就启动一次子程序  
    226.             do   
    227.             {  
    228.                 dwSessionIdPrev = dwSessionId;  
    229.                 if (GetObjProgInfo(dwSessionId, strObjPathName, strCmdLine))  
    230.                 {  
    231.                     if (!ns_base::IsFileExist(strObjPathName.c_str()))  
    232.                     {  
    233.                         WriteLogEx(L"[error] !ns_base::IsFileExist(%s)", strObjPathName.c_str());  
    234.                         break;  
    235.                     }  
    236.   
    237.                     /// 确保在多个SessionId的环境下, 也只有当前SessionId上运行唯一一个dlgNotify  
    238.                     ns_business::StopAndKill_dlgNotify();  
    239.   
    240.                     ProcessStarter.Run(  
    241.                         strObjPathName.c_str(),  
    242.                         strCmdLine.c_str());  
    243.                 }  
    244.             } while (0);  
    245.   
    246.             continue;  
    247.         }  
    248.   
    249.         uRc = S_OK;  
    250.     } while (0);  
    251.     WriteLogEx(L"<< FzAppService ThreadProcWorker");  
    252.   
    253.     return uRc;  
    254. }  
    255.   
    256. BOOL ThreadProcStart_Worker()  
    257. {  
    258.     BOOL bRc = FALSE;  
    259.     ns_base::TAG_THREAD_MANAGER_PARAM* pThreadManagerParam = NULL;  
    260.   
    261.     if (!g_ThreadManager.IsNeedQuitThread()  
    262.         && !g_ThreadManager.IsThreadRunning())  
    263.     {  
    264.         do   
    265.         {  
    266.             pThreadManagerParam = new ns_base::TAG_THREAD_MANAGER_PARAM;  
    267.             if (NULL == pThreadManagerParam)  
    268.                 break;  
    269.   
    270.             pThreadManagerParam->pThreadManager = &g_ThreadManager;  
    271.             g_ThreadManager.SetThreadHandle(  
    272.                 (HANDLE)_beginthreadex(  
    273.                 NULL,   
    274.                 0,   
    275.                 &ThreadProcWorker,   
    276.                 (void*)pThreadManagerParam,   
    277.                 0,   
    278.                 NULL));  
    279.   
    280.             bRc = TRUE;  
    281.         } while (0);  
    282.     }  
    283.   
    284.     return bRc;  
    285. }  
    286.   
    287. BOOL ThreadProcStop_Worker()  
    288. {  
    289.     g_ThreadManager.StopThread(TRUE, L"g_ThreadManager");  
    290.     return TRUE;  
    291. }  
    292.   
    293. BOOL GetObjProgInfo(DWORD dwSessionId, OUT std::wstring& strObjPathName, OUT std::wstring& strCmdLine)  
    294. {  
    295.     ns_base::GetPathName_Me(strObjPathName);  
    296.     strObjPathName += FILE_NAME_ObjProgInfo;  
    297.   
    298.     strCmdLine = ns_base::StringFormatV(L"sessionId-%d", dwSessionId);  
    299.   
    300.     return TRUE;  
    301. }  


    CProcessStarter 实现, 负责以当前用户身份启动一个程序.

    [cpp] view plaincopy
     
    1. #ifndef _PROCESS_STARTER_H_  
    2. #define _PROCESS_STARTER_H_  
    3.   
    4. #include "stdafx.h"  
    5.   
    6. class CProcessStarter  
    7. {  
    8. public:  
    9.     CProcessStarter();  
    10.   
    11.     /// 如果没有找到"已经激活的SessionId", 说明还没有进入桌面  
    12.     BOOL FindActiveSessionId(OUT DWORD& dwSessionId, BOOL bNeedLog);  
    13.     BOOL Run(LPCWSTR pcProcessPathName, LPCWSTR pcCmdLine);  
    14.       
    15. private:  
    16.     HANDLE GetCurrentUserToken();  
    17.   
    18. private:  
    19.     std::wstring m_strProcessPathName;  
    20.     std::wstring m_strCmdLine;  
    21. };  
    22.   
    23. #endif //_PROCESS_STARTER_H_  
    [cpp] view plaincopy
     
      1. #include "stdafx.h"  
      2. #include "ProcessStarter.h"  
      3.   
      4. #include <userenv.h>  
      5. #pragma comment(lib, "Userenv.lib")  
      6.   
      7. #include <wtsapi32.h>  
      8. #pragma comment(lib, "Wtsapi32.lib")  
      9.   
      10. CProcessStarter::CProcessStarter()  
      11.     : m_strProcessPathName(L""),   
      12.     m_strCmdLine(L"")  
      13. {  
      14. }  
      15.   
      16. BOOL CProcessStarter::FindActiveSessionId(OUT DWORD& dwSessionId, BOOL bNeedLog)  
      17. {  
      18.     BOOL bFindActiveSession = FALSE;  
      19.     DWORD dwIndex = 0;  
      20.   
      21.     PWTS_SESSION_INFO pWtsSessionInfo = NULL;  
      22.     DWORD dwCntWtsSessionInfo = 0;  
      23.   
      24.     if (bNeedLog)  
      25.         WriteLogEx(L">> CProcessStarter::FindActiveSessionId()");  
      26.     do   
      27.     {  
      28.         dwSessionId = (DWORD)(-1);  
      29.   
      30.         if ((!WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 0, 1, &pWtsSessionInfo, &dwCntWtsSessionInfo))  
      31.             || (NULL == pWtsSessionInfo))  
      32.         {  
      33.             if (bNeedLog)  
      34.                 WriteLogEx(L"break 0 CProcessStarter::FindActiveSessionId()");  
      35.   
      36.             break;  
      37.         }  
      38.   
      39.         for (dwIndex = 0; dwIndex < dwCntWtsSessionInfo; dwIndex++)  
      40.         {  
      41.             if (WTSActive == pWtsSessionInfo[dwIndex].State)  
      42.             {  
      43.                 dwSessionId = pWtsSessionInfo[dwIndex].SessionId;  
      44.                 bFindActiveSession = TRUE;  
      45.                 break;  
      46.             }  
      47.         }  
      48.   
      49.         WTSFreeMemory(pWtsSessionInfo);  
      50.   
      51.         if (!bFindActiveSession)  
      52.         {  
      53.             if (bNeedLog)  
      54.                 WriteLogEx(L"break 1 CProcessStarter::FindActiveSessionId()");  
      55.   
      56.             break;  
      57.         }  
      58.     } while (0);  
      59.   
      60.     if (bNeedLog)  
      61.     {  
      62.         WriteLogEx(L"<< CProcessStarter::FindActiveSessionId(), bFindActiveSession = [%s], dwSessionId = %d",   
      63.             bFindActiveSession ? L"TRUE" : L"FALSE",   
      64.             dwSessionId);  
      65.     }  
      66.   
      67.     return bFindActiveSession;  
      68. }  
      69.   
      70. HANDLE CProcessStarter::GetCurrentUserToken()  
      71. {  
      72.     DWORD dwSessionId = 0;  
      73.     HANDLE hCurrentToken = NULL;  
      74.     HANDLE hPrimaryToken = NULL;  
      75.   
      76.     WriteLogEx(L">> CProcessStarter::GetCurrentUserToken()");  
      77.     do   
      78.     {  
      79.         if (!FindActiveSessionId(dwSessionId, TRUE))  
      80.         {  
      81.             WriteLogEx(L"break 0 CProcessStarter::GetCurrentUserToken()");  
      82.             break;  
      83.         }  
      84.   
      85.         if (!WTSQueryUserToken(dwSessionId, &hCurrentToken)  
      86.             || (NULL == hCurrentToken))  
      87.         {  
      88.             WriteLogEx(L"break 2 CProcessStarter::GetCurrentUserToken()");  
      89.             break;  
      90.         }  
      91.   
      92.         if (!DuplicateTokenEx(hCurrentToken, TOKEN_ASSIGN_PRIMARY | TOKEN_ALL_ACCESS, 0, SecurityImpersonation, TokenPrimary, &hPrimaryToken))  
      93.         {  
      94.             WriteLogEx(L"break 3 CProcessStarter::GetCurrentUserToken()");  
      95.             break;  
      96.         }  
      97.   
      98.     } while (0);  
      99.   
      100.     WriteLogEx(L"<< CProcessStarter::GetCurrentUserToken(), hCurrentToken = 0x%p, hPrimaryToken = 0x%p",   
      101.         hCurrentToken,  
      102.         hPrimaryToken);  
      103.   
      104.     SAFE_CLOSE_HANDLE(hCurrentToken);  
      105.     return hPrimaryToken;  
      106. }  
      107.   
      108. BOOL CProcessStarter::Run(LPCWSTR pcProcessPathName, LPCWSTR pcCmdLine)  
      109. {  
      110.     BOOL bRc = FALSE;  
      111.     BOOL bTmp = FALSE;  
      112.     HANDLE hPrimaryToken = NULL;  
      113.     STARTUPINFOA StartupInfo = {0};  
      114.     PROCESS_INFORMATION processInfo = {0};  
      115.     std::wstring command = L"";  
      116.     LPVOID lpEnvironment = NULL;  
      117.   
      118.     WriteLogEx(L">> CProcessStarter::Run");  
      119.   
      120.     do   
      121.     {  
      122.         if ((NULL == pcProcessPathName) || (!ns_base::IsFileExist(pcProcessPathName)))  
      123.         {  
      124.             WriteLogEx(L"break 0 CProcessStarter::Run");  
      125.             break;  
      126.         }  
      127.   
      128.         this->m_strProcessPathName = pcProcessPathName;  
      129.         this->m_strCmdLine = (NULL != pcCmdLine) ? pcCmdLine : L"";  
      130.   
      131.         hPrimaryToken = GetCurrentUserToken();  
      132.         if (NULL == hPrimaryToken)  
      133.         {  
      134.             WriteLogEx(L"break 1 CProcessStarter::Run");  
      135.             break;  
      136.         }  
      137.   
      138.         StartupInfo.cb = sizeof(STARTUPINFO);  
      139.         command = L""";  
      140.         command += m_strProcessPathName.c_str();  
      141.         command += L""";  
      142.         if (m_strCmdLine.length() != 0)  
      143.         {  
      144.             command += L" ";  
      145.             command += m_strCmdLine.c_str();  
      146.         }  
      147.   
      148.         WriteLogEx(L"command = [%s]", command.c_str());  
      149.   
      150.         if (!CreateEnvironmentBlock(&lpEnvironment, hPrimaryToken, TRUE))  
      151.         {  
      152.             WriteLogEx(L"!CreateEnvironmentBlock by hPrimaryToken");  
      153.         }  
      154.   
      155.         bTmp = CreateProcessAsUserA(  
      156.             hPrimaryToken,   
      157.             0,   
      158.             (LPSTR)ns_base::W2Aex(command.c_str()).c_str(),   
      159.             NULL,   
      160.             NULL,   
      161.             FALSE,   
      162.             NORMAL_PRIORITY_CLASS | CREATE_UNICODE_ENVIRONMENT,  
      163.             lpEnvironment, // __in_opt    LPVOID lpEnvironment,  
      164.             0,   
      165.             &StartupInfo,   
      166.             &processInfo);  
      167.   
      168.         if (NULL != lpEnvironment)  
      169.             DestroyEnvironmentBlock(lpEnvironment);  
      170.   
      171.         WriteLogEx(L"CreateProcessAsUserA = %s", bTmp ? L"TRUE" : L"FALSE");  
      172.         if (!bTmp)  
      173.             break;  
      174.   
      175.         bRc = TRUE;  
      176.     } while (0);  
      177.   
      178.     SAFE_CLOSE_HANDLE(hPrimaryToken);  
      179.     WriteLogEx(L"<< CProcessStarter::Run, bRc = [%s]", bRc ? L"TRUE" : L"FALSE");  
      180.   
      181.     return bRc;  
      182. }  

    http://blog.csdn.net/lostspeed/article/details/47018925

    http://download.csdn.net/detail/lostspeed/8925599

  • 相关阅读:
    C#8.0——异步流(AsyncStream)
    递归,循环,尾递归
    C#7.2——编写安全高效的C#代码
    Ocelot中文文档-Route
    Ocelot中文文档-Configuration
    Ocelot中文文档-Not Supported
    Ocelot中文文档-Getting Started
    .NET 单元测试的利剑——模拟框架Moq(简述篇)
    输入五个数字,按从大到小的顺序输出
    函数和方法的区别
  • 原文地址:https://www.cnblogs.com/findumars/p/5174324.html
Copyright © 2020-2023  润新知