zoukankan      html  css  js  c++  java
  • vista开发注意事项及解决方案

    1.判断UAC是否启动
    BOOL _stdcall IsRunUAC()//判断是否启动UAC
    {
        BOOL bRet = FALSE;
        LONG lErr;
        HKEY hKEY;
        DWORD dwEnableLUA;
        DWORD dwType = REG_DWORD;
        DWORD dwSize = sizeof( DWORD );

        if( IsVISTA() )
        {
            lErr = RegOpenKeyEx( HKEY_LOCAL_MACHINE,
                                _T("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\"),
                                0,
                                KEY_READ,
                                &hKEY );
            if( lErr == ERROR_SUCCESS )
            {
                lErr = RegQueryValueEx( hKEY,
                                        _T( "EnableLUA" ),
                                        NULL,
                                        &dwType,
                                        (BYTE*)&dwEnableLUA,
                                        &dwSize );
                if( lErr == ERROR_SUCCESS )
                {
                    if( 0 == dwEnableLUA )
                    {
                        bRet = FALSE;
                    }
                    else
                    {
                        bRet = TRUE;
                    }
                }
                else;

                RegCloseKey( hKEY );
            }
            else;
        }
        else;

        return bRet;
    }

    以上代码是判断一个注册表键值,不是正规方式。UAC启动要通过重起系统完成,这个标志位不表示当前UAC状态,所以此代码要在系统启动时执行才有效。

    2.解决UAC打开时,不同权限之间的应用程序间不能广播消息
        Vista UAC打开时,不同权限的应用程序广播消息是收不到的。

        UINT UIBroadcastCommand = ::RegisterWindowMessage( SNA_MESSAGE );
        ON_REGISTERED_MESSAGE( UIBroadcastCommand, OnFromMessage )

        将以下代码加入程序启始位置   
    BOOL AllowMeesageForVista( UINT uMessageID, BOOL bAllow )//注册Vista全局消息
    {
        BOOL bResult = FALSE;
        HMODULE hUserMod = NULL;

        do
        {
            //vista and later
            hUserMod = LoadLibrary( "User32.dll" );
            if( NULL == hUserMod ) break;
               
            _ChangeWindowMessageFilter pChangeWindowMessageFilter = (_ChangeWindowMessageFilter)GetProcAddress( hUserMod, "ChangeWindowMessageFilter" );
            if( NULL == pChangeWindowMessageFilter )break;

            bResult = pChangeWindowMessageFilter( uMessageID, bAllow ? 1 : 2 );//MSGFLT_ADD: 1, MSGFLT_REMOVE: 2
        }
        while( 0 );

        if( NULL != hUserMod )
        {
            FreeLibrary( hUserMod );
        }
        else;

        return bResult;
    }

        这里使用Vista提供的标准函数ChangeWindowMessageFilter注册一个全局消息。但是由于系统服务与应用程序间的session不同,所以应用程序无法响应系统服务的广播消息。

    3. 系统服务与应用程序的事件通讯
       
        在Vista中高权限进程创建的事件使用低权限进程是无法open的(其它windows也一样)。在创建事件时使用以下代码
    DWORD _stdcall MyCreateEvent( HANDLE* phEvent, BOOL bManualReset, BOOL bInitialState, LPCTSTR lpName )
    {
        DWORD dwRet = 0;
        PSID pEveryoneSID = NULL, pAdminSID = NULL;
        SID_IDENTIFIER_AUTHORITY SIDAuthWorld = SECURITY_WORLD_SID_AUTHORITY;
        EXPLICIT_ACCESS ea[1];
        PSECURITY_DESCRIPTOR pSD = NULL;
        PACL pACL = NULL;
        SECURITY_ATTRIBUTES sa;

        *phEvent = NULL;

        ////////////////////////
        do
        {
            // Create a well-known SID for the Everyone group.
            if( !AllocateAndInitializeSid(  &SIDAuthWorld,
                                            1,
                                            SECURITY_WORLD_RID,
                                            0, 0, 0, 0, 0, 0, 0,
                                            &pEveryoneSID ) )
            {
                dwRet = GetLastError();
                break;
            }
            else;

            // Initialize an EXPLICIT_ACCESS structure for an ACE.
            ZeroMemory( &ea, sizeof(EXPLICIT_ACCESS) );
            ea[0].grfAccessPermissions = EVENT_ALL_ACCESS;
            ea[0].grfAccessMode = SET_ACCESS;
            ea[0].grfInheritance= NO_INHERITANCE;
            ea[0].Trustee.TrusteeForm = TRUSTEE_IS_SID;
            ea[0].Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP;
            ea[0].Trustee.ptstrName  = (LPTSTR) pEveryoneSID;

            // Create a new ACL that contains the new ACEs.
            dwRet = SetEntriesInAcl( 1, ea, NULL, &pACL );
            if( ERROR_SUCCESS != dwRet )
            {
                break;
            }
            else;

            // Initialize a security descriptor. 
            pSD = (PSECURITY_DESCRIPTOR)LocalAlloc( LPTR,
                                                    SECURITY_DESCRIPTOR_MIN_LENGTH );
            if( NULL == pSD )
            {
                dwRet = GetLastError();
                break;
            }
            else;

            if( !InitializeSecurityDescriptor( pSD, SECURITY_DESCRIPTOR_REVISION ) )
            { 
                dwRet = GetLastError();
                break;
            }
            else;

            // Add the ACL to the security descriptor.
            if (!SetSecurityDescriptorDacl( pSD,
                                            TRUE,    // bDaclPresent flag 
                                            pACL,
                                            FALSE ) )  // not a default DACL
            { 
                dwRet = GetLastError();
                break;
            }
            else;

            // Initialize a security attributes structure.
            sa.nLength = sizeof( SECURITY_ATTRIBUTES );
            sa.lpSecurityDescriptor = pSD;
            sa.bInheritHandle = FALSE;

            ////////////////
            *phEvent = CreateEvent( &sa, bManualReset, bInitialState, lpName );
            if( NULL == *phEvent )
            {
                dwRet = GetLastError();
                break;
            }
            else;
        }
        while( 0 );

        //////////
        if( pEveryoneSID )
        {
            FreeSid( pEveryoneSID );
        }
        else;

        if( pACL )
        {
            LocalFree( pACL );
        }
        else;

        if( pSD )
        {
            LocalFree(pSD);
        }
        else;

        return dwRet;
    }
        为CreateEvent注册一个Everyone的事件,这段代码也可以注册Everyone的文件和filemap的属性。


    4.全局名称
        在VISTA下所有跨进程事件,互斥量、名称都必须加“Global\”标识。否则无法和其他用户环境下通讯。


    5.服务启动进程
        VISTA下服务为session0,所以用服务直接启动进程,会出现非交互试对话框的问题。以下代码解决此问题。
    DWORD _stdcall LaunchAppIntoDifferentSession( LPTSTR lpCommand )
    {
        DWORD dwRet = 0;
        PROCESS_INFORMATION pi;
        STARTUPINFO si;
        DWORD dwSessionId;
        HANDLE hUserToken = NULL;
        HANDLE hUserTokenDup = NULL;
        HANDLE hPToken = NULL;
        HANDLE hProcess = NULL;
        DWORD dwCreationFlags;

        // Log the client on to the local computer.
        dwSessionId = WTSGetActiveConsoleSessionId();

        do
        {
            WTSQueryUserToken( dwSessionId,&hUserToken );
            dwCreationFlags = NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE;
            ZeroMemory( &si, sizeof( STARTUPINFO ) );
            si.cb= sizeof( STARTUPINFO );
            si.lpDesktop = "winsta0\\default";
            ZeroMemory( &pi, sizeof(pi) );
            TOKEN_PRIVILEGES tp;
            LUID luid;

            if( !::OpenProcessToken( GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY
                                                            | TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY | TOKEN_ADJUST_SESSIONID
                                                            | TOKEN_READ | TOKEN_WRITE, &hPToken ) )
            {
                dwRet = GetLastError();
                break;
            }
            else;

            if ( !LookupPrivilegeValue( NULL, SE_DEBUG_NAME, &luid ) )
            {
                dwRet = GetLastError();
                break;
            }
            else;
            tp.PrivilegeCount =1;
            tp.Privileges[0].Luid =luid;
            tp.Privileges[0].Attributes =SE_PRIVILEGE_ENABLED;

            if( !DuplicateTokenEx( hPToken, MAXIMUM_ALLOWED, NULL, SecurityIdentification, TokenPrimary, &hUserTokenDup ) )
            {
                dwRet = GetLastError();
                break;
            }
            else;

            //Adjust Token privilege
            if( !SetTokenInformation( hUserTokenDup,TokenSessionId,(void*)&dwSessionId,sizeof(DWORD) ) )
            {
                dwRet = GetLastError();
                break;
            }
            else;

            if( !AdjustTokenPrivileges( hUserTokenDup, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), (PTOKEN_PRIVILEGES)NULL, NULL ) )
            {
                dwRet = GetLastError();
                break;
            }
            else;

            LPVOID pEnv =NULL;
            if( CreateEnvironmentBlock( &pEnv, hUserTokenDup, TRUE ) )
            {
                dwCreationFlags|=CREATE_UNICODE_ENVIRONMENT;
            }
            else pEnv=NULL;

            // Launch the process in the client's logon session.
            if( CreateProcessAsUser(    hUserTokenDup,    // client's access token
                                        NULL,        // file to execute
                                        lpCommand,        // command line
                                        NULL,            // pointer to process SECURITY_ATTRIBUTES
                                        NULL,            // pointer to thread SECURITY_ATTRIBUTES
                                        FALSE,            // handles are not inheritable
                                        dwCreationFlags,// creation flags
                                        pEnv,          // pointer to new environment block
                                        NULL,          // name of current directory
                                        &si,            // pointer to STARTUPINFO structure
                                        &pi            // receives information about new process
                                        ) )
            {
            }
            else
            {
                dwRet = GetLastError();
                break;
            }
        }
        while( 0 );

        //Perform All the Close Handles task
        if( NULL != hUserToken )
        {
            CloseHandle( hUserToken );
        }
        else;

        if( NULL != hUserTokenDup)
        {
            CloseHandle( hUserTokenDup );
        }
        else;

        if( NULL != hPToken )
        {
            CloseHandle( hPToken );
        }
        else;
       
        return dwRet;
    }   

        以上代码取得服务Token,通过SetTokenInformation将进程调到当前session,这样创建的进程具有system权限又不会出现非交互试对话框。
        以下程序是降权限运行进程,同样也能解决非交互试对话框的问题。

    BOOL PrivDown_Execute( LPTSTR lpFilePath )
    {
        BOOL bRet = FALSE;
        HANDLE hToken = NULL;

        EnablePrivilege( SE_DEBUG_NAME );
        hToken = GetCurrentUserToken();
        if( NULL != hToken )
        {
            bRet = StartInteractiveClientProcess( NULL, NULL, NULL, lpFilePath, hToken, NULL );
        }
        else;

        return bRet;
    }

    ///////////////////////////////////////////

    HANDLE GetCurrentUserToken()
    {
        HANDLE hProc  = NULL;
        HANDLE hToken = NULL;
        BOOL bSuccess = FALSE;
        BOOL fResult;

        __try
        {
            // Enable the SE_DEBUG_NAME privilege in our process token
            if (!EnablePrivilege(SE_DEBUG_NAME))
            {
                printf("GetLSAToken EnablePrivilege Failed");
                __leave;
            }

            // Retrieve a handle to the "System" process
            hProc = OpenExplorerProcess();
            if(hProc == NULL)
            {
                printf("GetLSAToken OpenSystemProcess Failed");
                __leave;
            }

            // Open the process token with READ_CONTROL and WRITE_DAC access.  We
            // will use this access to modify the security of the token so that we
            // retrieve it again with a more complete set of rights.
            fResult = OpenProcessToken(hProc, READ_CONTROL | WRITE_DAC,
                &hToken);
            if(FALSE == fResult) 
            {
                printf("GetLSAToken OpenProcessToken Failed");
                __leave;
            }

            // Add an ace for the current user for the token.  This ace will add
            // TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY | TOKEN_QUERY rights.
            if (!ModifySecurity(hToken, TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY
                | TOKEN_QUERY | TOKEN_ADJUST_SESSIONID))
            {
                printf("GetLSAToken ModifySecurity Failed");
                __leave;
            }
           

            // Reopen the process token now that we have added the rights to
            // query the token, duplicate it, and assign it.
            fResult = OpenProcessToken(hProc, TOKEN_QUERY | TOKEN_DUPLICATE
                | TOKEN_ASSIGN_PRIMARY | READ_CONTROL | WRITE_DAC, &hToken);
            if (FALSE == fResult) 
            {
                printf("GetLSAToken OpenProcessToken Failed");
                __leave;
            }
            bSuccess = TRUE;
        }
        __finally
        {
            // Close the System process handle
            if (hProc != NULL)    CloseHandle(hProc);
            if(bSuccess)
                return hToken;
            else
            {
                CloseHandle(hToken);
                return NULL;
            }
        }
    }

    #define DESKTOP_ALL (DESKTOP_READOBJECTS | DESKTOP_CREATEWINDOW | DESKTOP_CREATEMENU | DESKTOP_HOOKCONTROL | \
            DESKTOP_JOURNALRECORD | DESKTOP_JOURNALPLAYBACK | \
            DESKTOP_ENUMERATE | DESKTOP_WRITEOBJECTS | \
            DESKTOP_SWITCHDESKTOP | STANDARD_RIGHTS_REQUIRED)

    #define WINSTA_ALL (WINSTA_ENUMDESKTOPS | WINSTA_READATTRIBUTES |  \
        WINSTA_ACCESSCLIPBOARD | WINSTA_CREATEDESKTOP | \
            WINSTA_WRITEATTRIBUTES | WINSTA_ACCESSGLOBALATOMS | \
            WINSTA_EXITWINDOWS | WINSTA_ENUMERATE | \
            WINSTA_READSCREEN | \
            STANDARD_RIGHTS_REQUIRED)

    #define GENERIC_ACCESS (GENERIC_READ | GENERIC_WRITE | GENERIC_EXECUTE | GENERIC_ALL)

    BOOL AddAceToWindowStation(HWINSTA hwinsta, PSID psid);

    BOOL AddAceToDesktop(HDESK hdesk, PSID psid);

    BOOL GetLogonSID(HANDLE hToken, PSID *ppsid)
    {
        PWTS_PROCESS_INFO pProcessInfo = NULL;
        DWORD            ProcessCount = 0;
        BOOL                ret=FALSE;
        DWORD CurrentProcess;
        if (WTSEnumerateProcesses(WTS_CURRENT_SERVER_HANDLE, 0, 1, &pProcessInfo, &ProcessCount))
        {
            // dump each process description
            for (CurrentProcess = 0; CurrentProcess < ProcessCount; CurrentProcess++)
            {

                if( strcmp(pProcessInfo[CurrentProcess].pProcessName, "System") == 0 )
                {
                    //*ppsid = pProcessInfo[CurrentProcess].pUserSid;
                    DWORD dwLength = GetLengthSid(pProcessInfo[CurrentProcess].pUserSid);
                    *ppsid = (PSID) HeapAlloc(GetProcessHeap(),
                                HEAP_ZERO_MEMORY, dwLength);
                    if (*ppsid == NULL)
                        break;
                    if (!CopySid(dwLength, *ppsid, pProcessInfo[CurrentProcess].pUserSid))
                    {
                        HeapFree(GetProcessHeap(), 0, (LPVOID)*ppsid);
                        break;
                    }
                    ret=TRUE;
                    break;
                }
            }

            WTSFreeMemory(pProcessInfo);
        }

        return ret;
    }

    BOOL GetLogonSID_1 (HANDLE hToken, PSID *ppsid)
    {
      BOOL bSuccess = FALSE;
      DWORD dwIndex;
      DWORD dwLength = 0;
      PTOKEN_GROUPS ptg = NULL;

    // Verify the parameter passed in is not NULL.
        if (NULL == ppsid)
            goto Cleanup;

    // Get required buffer size and allocate the TOKEN_GROUPS buffer.

      if (!GetTokenInformation(
            hToken,        // handle to the access token
            TokenGroups,    // get information about the token's groups
            (LPVOID) ptg,  // pointer to TOKEN_GROUPS buffer
            0,              // size of buffer
            &dwLength      // receives required buffer size
          ))
      {
          if (GetLastError() != ERROR_INSUFFICIENT_BUFFER)
            goto Cleanup;

          ptg = (PTOKEN_GROUPS)HeapAlloc(GetProcessHeap(),
            HEAP_ZERO_MEMORY, dwLength);

          if (ptg == NULL)
            goto Cleanup;
      }


    // Get the token group information from the access token.

      if (!GetTokenInformation(
            hToken,        // handle to the access token
            TokenGroups,    // get information about the token's groups
            (LPVOID) ptg,  // pointer to TOKEN_GROUPS buffer
            dwLength,      // size of buffer
            &dwLength      // receives required buffer size
            ))
      {
          goto Cleanup;
      }

    // Loop through the groups to find the logon SID.

      for (dwIndex = 0; dwIndex < ptg->GroupCount; dwIndex++)
          if ((ptg->Groups[dwIndex].Attributes & 0xE)
                ==  0xE)
          {
          // Found the logon SID; make a copy of it.

            dwLength = GetLengthSid(ptg->Groups[dwIndex].Sid);
            *ppsid = (PSID) HeapAlloc(GetProcessHeap(),
                        HEAP_ZERO_MEMORY, dwLength);
            if (*ppsid == NULL)
                goto Cleanup;
            if (!CopySid(dwLength, *ppsid, ptg->Groups[dwIndex].Sid))
            {
                HeapFree(GetProcessHeap(), 0, (LPVOID)*ppsid);
                goto Cleanup;
            }
            bSuccess = TRUE;
            break;
          }

    Cleanup:

    // Free the buffer for the token groups.

      if (ptg != NULL)
          HeapFree(GetProcessHeap(), 0, (LPVOID)ptg);

      return bSuccess;
    }




    VOID FreeLogonSID (PSID *ppsid)
    {
        HeapFree(GetProcessHeap(), 0, (LPVOID)*ppsid);
    }


    BOOL StartInteractiveClientProcess( LPTSTR lpszUsername,    // client to log on
                                        LPTSTR lpszDomain,      // domain of client's account
                                        LPTSTR lpszPassword,    // client's password
                                        LPTSTR lpCommandLine,    // command line to execute
                                        HANDLE Token,
                                        PROCESS_INFORMATION* ppi ) //返回PROCESS_INFORMATION
    {
      HANDLE      hToken;
      HDESK      hdesk = NULL;
      HWINSTA    hwinsta = NULL, hwinstaSave = NULL;
      PROCESS_INFORMATION pi;
      PSID pSid = NULL;
      STARTUPINFO si;
      BOOL bResult = FALSE;

    // Log the client on to the local computer.

      if(Token!=NULL)
      {
          printf("%08x\n", Token);
          hToken = Token;
      }
      else if (!LogonUser(
              lpszUsername,
              lpszDomain,
              lpszPassword,
              LOGON32_LOGON_INTERACTIVE,
              LOGON32_PROVIDER_DEFAULT,
              &hToken) )
      {
          goto Cleanup;
      }

    // Save a handle to the caller's current window station.
      if ( (hwinstaSave = GetProcessWindowStation() ) == NULL)
          goto Cleanup;

    // Get a handle to the interactive window station.
      hwinsta = OpenWindowStation(
          "winsta0",                  // the interactive window station
          FALSE,                      // handle is not inheritable
          READ_CONTROL | WRITE_DAC);  // rights to read/write the DACL

      if (hwinsta == NULL)
          goto Cleanup;

    // To get the correct default desktop, set the caller's
    // window station to the interactive window station.

      if (!SetProcessWindowStation(hwinsta))
          goto Cleanup;

    // Get a handle to the interactive desktop.
      hdesk = OpenDesktop(
          "default",    // the interactive window station
          0,            // no interaction with other desktop processes
          FALSE,        // handle is not inheritable
          READ_CONTROL | // request the rights to read and write the DACL
          WRITE_DAC |
          DESKTOP_WRITEOBJECTS |
          DESKTOP_READOBJECTS);

    // Restore the caller's window station.
      if (!SetProcessWindowStation(hwinstaSave))
          goto Cleanup;

      if (hdesk == NULL)
          goto Cleanup;

    // Get the SID for the client's logon session.
      if (!GetLogonSID(hToken, &pSid))
      {
          if (!GetLogonSID_1(hToken, &pSid))
          {
              goto Cleanup;
          }
      }

    // Allow logon SID full access to interactive window station.
      if (! AddAceToWindowStation(hwinsta, pSid) )
          goto Cleanup;

    // Allow logon SID full access to interactive desktop.
      if (! AddAceToDesktop(hdesk, pSid) )
          goto Cleanup;

    // Impersonate client to ensure access to executable file.
      if (! ImpersonateLoggedOnUser(hToken) )
          goto Cleanup;

    // Initialize the STARTUPINFO structure.
    // Specify that the process runs in the interactive desktop.
      ZeroMemory(&si, sizeof(STARTUPINFO));
      si.cb= sizeof(STARTUPINFO);
      si.lpDesktop = TEXT("winsta0\\default");  //You can use EnumWindowStations to enum desktop

    // Launch the process in the client's logon session.
      bResult = CreateProcessAsUser(
          hToken,            // client's access token
          NULL,              // file to execute
          lpCommandLine,    // command line
          NULL,              // pointer to process SECURITY_ATTRIBUTES
          NULL,              // pointer to thread SECURITY_ATTRIBUTES
          FALSE,            // handles are not inheritable
          NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE,  // creation flags
          NULL,              // pointer to new environment block
          NULL,              // name of current directory
          &si,              // pointer to STARTUPINFO structure
          &pi                // receives information about new process
      );

      ////
      if( NULL != ppi )
      {
            *ppi = pi;
      }
      else;

    // End impersonation of client.
      RevertToSelf();

      goto Cleanup;
      //return bResult; <------------------------------------------------------------------------

      if (bResult && pi.hProcess != INVALID_HANDLE_VALUE)
      {
          WaitForSingleObject(pi.hProcess, INFINITE);
          CloseHandle(pi.hProcess);
      }

      if (pi.hThread != INVALID_HANDLE_VALUE)
          CloseHandle(pi.hThread); 

    Cleanup:

      if (hwinstaSave != NULL)
          SetProcessWindowStation (hwinstaSave);

    // Free the buffer for the logon SID.

      if (pSid)
          FreeLogonSID(&pSid);

    // Close the handles to the interactive window station and desktop.

      if (hwinsta)
          CloseWindowStation(hwinsta);

      if (hdesk)
          CloseDesktop(hdesk);

    // Close the handle to the client's access token.

      if (hToken != INVALID_HANDLE_VALUE)
          CloseHandle(hToken); 

      return bResult;
    }

    6.system权限进程的一些问题。
        为了解决UAC启动给程序造成的权限问题,我们采用了将进程提升到system权限的方法。但是出现以下一系列问题。
        1)使用SHBrowseForFolder无法显示盘目录。
          解决方案:重新做了个目录选择的模块替代SHBrowseForFolder。

        2)使用CFileDialog 会指向{sys}\config\systemprofile\desktop空目录,且无法保存到用户桌面。
          解决方案:创建一个{sys}\config\systemprofile\desktop目录。但无法解决保存到用户桌面的问题。

        3)无法打开帮助文件。
          解决方案:开始使用降权限运行hh.exe但是无效。重新做了个exe,用HtmlHelp调用帮助,降权限运行就可以了。

        4)继承CHtmlView内至的IE浏览器无法打开。
          解决方案:目前只能从外部打开IE浏览器。


  • 相关阅读:
    001-nginx基础配置-location、Rewrite、return 直接返回码、全局变量
    001-Spring Cloud Edgware.SR3 升级最新 Finchley.SR1,spring boot 1.5.9.RELEASE 升级2.0.4.RELEASE注意问题点
    004-读书记录
    007-ant design 对象属性赋值,双向绑定
    006-ant design -结合echart-地址map市
    005-ant design -结合echart
    002-Spring Framework-Core-01-IoC容器
    003-驾照考试,总结
    006-markdown基础语法
    java-mybaits-010-mybatis-spring-使用 SqlSession、注入映射器
  • 原文地址:https://www.cnblogs.com/lzjsky/p/1857750.html
Copyright © 2011-2022 走看看