사용자 도구

사이트 도구


kb:wmiusingcpp

WMI Using C++

WMI를 이용해, 시스템 상의 오브젝트들에 관한 정보를 알아내기

설명

애초에 WMI까지 찾아 들어가게 된 계기는 시스템에 설치되어 있는 비디오 카드의 종류와 비디오 램의 크기를 알기 위해서였다. CPU 종류나 하드 디스크 용량 등의 정보는 비교적 쉽게 접근할 수 있는 데에 비해서, 비디오 카드에 관한 정보를 얻는 것은 딱 떨어지는 API가 없었다. 그래서 이것저것 찾다가, 결국 어떻게 [WMI]라는 것이 하드웨어 정보와 연관되어있다는 것을 알게 되었는데, 막상 자세히 살펴보니, 하드웨어 뿐만 아니라 다른 거의 모든 시스템 오브젝트에 관한 정보를 알 수 있게 되어있었다.

라이브러리

아래의 라이브러리(?)는 WMI SDK 내에 존재하는 샘플 중에서 Simple Client라는 샘플을 참고해서 제작한 것이다. 각각의 정보들은 모두 포맷이 다른데, 자세한 것은 MSDN을 참고하기 바란다.

참고

  • Visual C++ .Net 2003에서는 아무 문제없이 컴파일에 성공했다.
  • CoCreateInstance() 등의 함수를 사용하기 위해서는 windows.h를 포함하기 전에
  • _WIN32_WINNT 상수를 재정의해줘야한다.
  • 소스의 주석에도 나와있지만, 실행 파일 프로젝트에다가 wbemuuid.lib를
  • 추가적으로 링크해줘야한다.
  • MFC를 사용하지 않고 만들어진 라이브러리다. MFC를 사용할 경우, 2바이트
  • 문자열 부분이나, OLE 관련 부분이 상당히 편해지긴 하지만, 독립성이
  • 떨어진다고 생각했기 때문에, 사용하지 않았다.

개선해야할 점

  • COM과 관련된 부분은 잘 알지 못하는 상황에서 코딩했기 때문에, 버그가
  • 존재할 가능성이 높다.
  • 유니코드를 멀티바이트로 변환해서 처리하고 있다.
  • 오브젝트에 대한 정보는 VARIANT 형식으로 넘어오는데, 문자열과 숫자만
  • 처리하고 다른 타입의 VARIANT는 제대로 처리하고 있지 않다.

소스

WMIAccessor.h
//////////////////////////////////////////////////////////////////////////////
/// \file WMIAccessor.h
/// \author excel96
/// \date 2003.12.18
//////////////////////////////////////////////////////////////////////////////
 
#ifndef __WMIACCESSOR_H__
#define __WMIACCESSOR_H__
 
// 플랫폼 SDK에 있는 함수 중에서 _WIN32_WINNT 값이 0x0500이상이어야만 
// include되는 함수들이 존재한다. 그런 함수들을 사용하기 위해서 정의한다.
#ifdef _WIN32_WINNT
    #undef _WIN32_WINNT
#endif
#define _WIN32_WINNT 0x0500
 
#include <string>
using namespace std;
 
//////////////////////////////////////////////////////////////////////////////
/// \class WMIAccessor
/// \brief 시스템에 설치되어 있는 오브젝트에 대한 정보를 알아내기 위한
/// 유틸리티 클래스. 
///
/// 내부적으로 WMI(Windows Management Instrumentation)를 사용한다. 그래서
/// wbemuuid.lib를 링크 탭에다 추가해줘야한다. pragma를 사용하면 깔끔하게
/// 해결될 듯도 하다만...
//////////////////////////////////////////////////////////////////////////////
 
class WMIAccessor
{
private:
    struct IMPL;
    IMPL* m_pImpl;
 
public:
    WMIAccessor(const string& szNameSpace="\\\\.\\root\\cimv2");
    virtual ~WMIAccessor();
 
public:
    /// \brief 주어진 클래스에 속한 오브젝트들에 대한 정보를 조사해서,
    /// 내부 버퍼에다 저장한다.
    void enumerate(const string& szWMIAccessorID);
 
    /// \brief enumerate 함수를 사용해서 생성한 오브젝트의 수를 반환한다.
    size_t size() const;
 
    /// \brief 지정된 오브젝트의 속성값을 반환한다.
    string asString(size_t index, const string& property_name) const;
 
    /// \brief 지정된 오브젝트의 속성값을 반환한다.
    int asInt(size_t index, const string& property_name) const;
 
    /// \brief 내부 데이터를 문자열로 변환해서 반환한다.
    string toString() const;
 
public:
    /// \brief enumerate 함수 내부에서 사용하는 함수로서, 해당하는 인덱스의
    /// 오브젝트에 대한 상세한 정보를 맵에다 기록한다.
    void query(size_t index);
 
public:
    /// \brief WMIAccessor 클래스를 사용하기 전에 불러줘야하는 초기화 함수
    static void initialize();
 
    /// \brief WMIAccessor 클래스를 사용한 후에 불러줘야하는 초기화 함수
    static void finalize();
};
 
#endif //__WMIACCESSOR_H__
WMIAccessor.cpp
//////////////////////////////////////////////////////////////////////////////
/// \file WMIAccessor.cpp
/// \author excel96
/// \date 2003.12.18
//////////////////////////////////////////////////////////////////////////////
 
#include "WMIAccessor.h"
#include <windows.h>
#include <wbemcli.h>
#include <assert.h>
#include <stdio.h>
#include <stdarg.h>
#include <time.h>
#include <fstream>
#include <vector>
#include <hash_map>
 
#define BUF_SIZE 1024 
 
//////////////////////////////////////////////////////////////////////////////
/// \brief 파일에다 로그를 남긴다.
/// \param fmt 포맷
/// \param ... 파라미터들...
//////////////////////////////////////////////////////////////////////////////
static void filelog(char* fmt, ...)
{
    ofstream file;
    file.open("WMIAccessorError.log", ios::out | ios::app); 
    if (file.is_open())
    {
        va_list valist;
        va_start(valist, fmt);
        char message_buffer[30000] = {0, };
        int nchars = _vsnprintf(message_buffer, 30000, fmt, valist);
        if (nchars == -1 || nchars > 30000)
        {
            filelog("filelog buffer overflow!");
            throw ("filelog() : more buffer size needed for log");
        }
        va_end(valist);
 
        time_t now = time(0);
        char time_buffer[256] = {0, };
        sprintf(time_buffer, "%s : ", ctime(&now));
 
        file.write(time_buffer, (streamsize)strlen(time_buffer));
        file.write(message_buffer, (streamsize)strlen(message_buffer));
        file.write("\n", (streamsize)strlen("\n"));
    }
}
 
//////////////////////////////////////////////////////////////////////////////
/// \class WideString
/// \brief
//////////////////////////////////////////////////////////////////////////////
 
class WideString
{
private:
    BSTR m_pSTR;
 
public:
    WideString() 
    {
        m_pSTR = ::SysAllocString(NULL);
        if (m_pSTR == NULL) { filelog("WideString() : Cannot allocate."); }
    }
 
    WideString(const char* szContent)
    {
        wchar_t buf[BUF_SIZE] = {0, };
        mbstowcs(buf, szContent, BUF_SIZE);
        m_pSTR = ::SysAllocString(buf);
        if (m_pSTR == NULL) { filelog("WideString() : Cannot allocate."); }
    }
 
    WideString(const string& szContent)
    {
        wchar_t buf[BUF_SIZE] = {0, };
        mbstowcs(buf, szContent.c_str(), BUF_SIZE);
        m_pSTR = ::SysAllocString(buf);
        if (m_pSTR == NULL) { filelog("WideString() : Cannot allocate."); }
    }
 
    WideString(const wchar_t* szContent)
    {
        m_pSTR = ::SysAllocString(szContent);
        if (m_pSTR == NULL) { filelog("WideString() : Cannot allocate."); }
    }
 
    ~WideString() { clear(); }
 
    string toString() const
    {
        char buf[BUF_SIZE] = {0, };
        wcstombs(buf, m_pSTR, BUF_SIZE);
        return string(buf);
    }
 
    void clear()
    {
        if (m_pSTR)
        {
            ::SysFreeString(m_pSTR);
            m_pSTR = NULL;
        }
    }
 
    operator BSTR() { return m_pSTR; }
    operator string() { return toString(); }
 
 
private:
    WideString(const WideString&) {}
    WideString& operator = (const WideString&) { return *this; }
};
 
//////////////////////////////////////////////////////////////////////////////
/// \brief 
/// 
/// \param var 
/// \return static string
//////////////////////////////////////////////////////////////////////////////
static string Variant2String(const VARIANT& var)
{
    if (var.vt == VT_NULL)
    {
        return "NULL";
    }
    else if (var.vt == VT_BOOL)
    {
        return var.boolVal ? "true" : "false";
    }
    else if (var.vt == VT_UI1)
    {
        char buf[128] = {0,};
        sprintf(buf, "%c", var.cVal);
        return string(buf);
    }
    else if (var.vt == VT_I2)
    {
        char buf[128] = {0,};
        sprintf(buf, "%d", var.iVal);
        return string(buf);
    }
    else if (var.vt == VT_I4)
    {
        char buf[128] = {0,};
        sprintf(buf, "%d", var.lVal);
        return string(buf);
    }
    else if (var.vt == VT_BSTR)
    {
        return WideString(V_BSTR(&var)).toString();
    }
 
    return "Unknown";
}
 
 
//////////////////////////////////////////////////////////////////////////////
/// \struct WMIAccessor::IMPL
/// \brief WMIAccessor 클래스 내부 데이터 구조체
//////////////////////////////////////////////////////////////////////////////
struct WMIAccessor::IMPL
{
    typedef vector<string> DEVICE_NAMES;
    typedef stdext::hash_map<string, string> DETAIL;
    typedef stdext::hash_map<string, DETAIL> DETAIL_MAP;
 
    IWbemServices* pServices;  ///< IWbemServices 인터페이스
    DEVICE_NAMES   Names;      ///< 임의의 클래스에 속한 오브젝트들의 이름
    DETAIL_MAP     Details;    ///< 각 오브젝트의 상세한 정보들
 
    static bool    s_bOLEInit; ///> OLE DLL이 초기화되었는가?
 
    IMPL()
    : pServices(NULL)
    {
    }
 
    ~IMPL()
    {
        if (pServices) pServices->Release();
        Names.clear();
        Details.clear();
    }
};
 
bool WMIAccessor::IMPL::s_bOLEInit = false;
 
//////////////////////////////////////////////////////////////////////////////
/// \brief 
/// \param szNameSpace 
//////////////////////////////////////////////////////////////////////////////
WMIAccessor::WMIAccessor(const string& szNameSpace)
: m_pImpl(new IMPL)
{
    IWbemLocator* pLocator = NULL;
    HRESULT       result   = S_OK;
 
    // WbemLocator 인터페이스 객체를 생성한다.
    result = ::CoCreateInstance(CLSID_WbemLocator, NULL, 
        CLSCTX_INPROC_SERVER, IID_IWbemLocator, (LPVOID*)&pLocator);
 
    // WBemLocation 인터페이스 생성에 성공했다면...
    if (SUCCEEDED(result))
    {
        // 네임 스페이스 문자열을 BSTR로 변환한다.
        WideString wszNameSpace(szNameSpace.c_str());
 
        // If already connected, release m_pImpl->pServices.
        if (m_pImpl->pServices) m_pImpl->pServices->Release();
 
        // Using the locator, connect to CIMOM in the given namespace.
        result = pLocator->ConnectServer(
            wszNameSpace, // namespace
            NULL,         //using current account for simplicity
            NULL,         //using current password for simplicity
            0L,           // locale
            0L,           // securityFlags
            NULL,         // authority (domain for NTLM)
            NULL,         // context
            &m_pImpl->pServices);
 
        if (FAILED(result)) 
        {   
            filelog("WMIAccessor() : Bad namespace!");
        }
    }
    // WBemLocation 인터페이스 생성에 실패했다!
    else
    {
        filelog("Failed to create IWbemLocator object");
    }
 
    // Done with pLocator. 
    pLocator->Release(); 
}
 
//////////////////////////////////////////////////////////////////////////////
/// \brief 소멸자
//////////////////////////////////////////////////////////////////////////////
WMIAccessor::~WMIAccessor()
{
    delete m_pImpl;
}
 
//////////////////////////////////////////////////////////////////////////////
/// \brief 주어진 클래스에 속한 오브젝트들에 대한 정보를 조사해서,
/// 내부 버퍼에다 저장한다.
/// \param szWMIAccessorID 열거할 클래스
//////////////////////////////////////////////////////////////////////////////
void WMIAccessor::enumerate(const string& szWMIAccessorID)
{
    // 기존의 데이터를 삭제한다.
    m_pImpl->Names.clear();
    m_pImpl->Details.clear();
 
    HRESULT               result        = S_OK;
    IEnumWbemClassObject* pClassObjects = NULL;
 
    // 해당하는 클래스에 속하는 오브젝트들을 목록을 얻어온다.
    result = m_pImpl->pServices->CreateInstanceEnum(
        WideString(szWMIAccessorID), 0, NULL, &pClassObjects);
 
    // 오브젝트의 목록을 얻어오는데 실패했다면 리턴한다.
    if (FAILED(result))
    {
        filelog("enumerate() : CreateInstanceEnum() failed:");
        return;
    }
 
    ULONG uReturned = 1;
    while (uReturned == 1)
    {
        IWbemClassObject* pObject = NULL;
 
        // 결과셋을 횡단하면서...
        result = pClassObjects->Next(
            2000,         // 응답이 올 때까지 2초간 기다린다.
            1,            // 결과셋 중에 하나만 리턴한다.
            &pObject,     // 오브젝트의 위치를 저장할 곳
            &uReturned);  // 결과셋에서 꺼낸 오브젝트의 수, 1 또는 0
 
        if (SUCCEEDED(result) && (uReturned == 1))
        {
            VARIANT pObjectName;
            ::VariantClear(&pObjectName);
 
            // Get the "__RELPATH" property.
            result = pObject->Get(WideString(L"__RELPATH"), 
                0L, &pObjectName, NULL, NULL);
 
            // 얻어온 클래스 오브젝트의 이름을 저장해둔다.
            if (SUCCEEDED(result)) 
            {
                m_pImpl->Names.push_back(WideString(V_BSTR(&pObjectName)));
            }
 
            // 이 오브젝트는 더 이상 사용할 일이 없다.
            pObject->Release();
        }
    } // end of while (uReturned == 1)
 
    // 더 이상 사용할 이리 없으니, 오브젝트 목록을 삭제한다.
    if (pClassObjects) pClassObjects->Release();
 
    // 각각의 클래스 오브젝트에 대한 상세 내역을 얻어온다.
    for (size_t i=0; i<m_pImpl->Names.size(); i++) { query(i); }
}
 
//////////////////////////////////////////////////////////////////////////////
/// \brief enumerate 함수 내부에서 사용하는 함수로서, 해당하는 인덱스의
/// 오브젝트에 대한 상세한 정보를 맵에다 기록한다.
///
/// 인덱스가 범위를 벗어나는 경우 어서트!
/// 
/// \param index 오브젝트의 인덱스
//////////////////////////////////////////////////////////////////////////////
void WMIAccessor::query(size_t index)
{
    assert(index < m_pImpl->Names.size());
 
    IWbemClassObject* pClassObject = NULL;
    HRESULT           result       = S_OK;
    SAFEARRAY*        psaNames     = NULL;
 
    // 오브젝트의 이름을 이용해, 오브젝트 객체를 얻어온다.
    result = m_pImpl->pServices->GetObject(
        WideString(m_pImpl->Names[index]), 0L, NULL, &pClassObject, NULL);
 
    // 오브젝트 객체를 얻어오지 못했다면 그냥 리턴한다.
    if (FAILED(result))
    {
        filelog("IWbemServices::GetObject() failed!");
        return;
    }
 
    // 상세 사항 맵에서 해당하는 오브젝트에 대한 엔트리가 있는지 조사한다.
    IMPL::DETAIL_MAP::iterator itr = 
        m_pImpl->Details.find(m_pImpl->Names[index]);
 
    // 상세 사항 맵에 해당하는 엔트리가 존재할 경우, 기존값들을 삭제하고,
    // 엔트리가 존재하지 않는다면, 새로 만들어준다.
    if (itr != m_pImpl->Details.end())
    {
        itr->second.clear();
    }
    else
    {
        itr = m_pImpl->Details.insert(IMPL::DETAIL_MAP::value_type(
            m_pImpl->Names[index], IMPL::DETAIL())).first;
    }
 
    IMPL::DETAIL& Detail = itr->second;
 
    // 오브젝트 내에 존재하는 속성 이름들을 얻어온다.
    result = pClassObject->GetNames(
        NULL, WBEM_FLAG_ALWAYS | WBEM_FLAG_NONSYSTEM_ONLY, NULL, &psaNames);
 
    // 속성 이름들을 얻어오지 못했다면 리턴한다.
    if (FAILED(result))
    {
        filelog("IWbemClassObject::GetNames() failed!");
        return;
    }
 
    // 배열의 최소 인덱스와 최대 인덱스를 얻어온다.
    long lbound = 0, ubound = 0;
    ::SafeArrayGetLBound(psaNames, 1, &lbound);
    ::SafeArrayGetUBound(psaNames, 1, &ubound);
 
    // 배열을 횡단하면서 각각의 속성에 대한 값을 얻어온다.
    for (long i = lbound; i <= ubound; i++) 
    {
        WideString wszObjectPropName;
 
        // 현재 인덱스의 속성 이름을 배열에서 얻어온다.
        result = ::SafeArrayGetElement(psaNames, &i, &wszObjectPropName);
        if (FAILED(result)) { continue; }
 
        VARIANT pObjectPropType;
        VARIANT pObjectPropValue;
        VariantClear(&pObjectPropType);
        VariantClear(&pObjectPropValue);
 
        // 현재 속성이 속해있는 타입들을 읽어온다. 시스템 속성인 경우, 타입 
        // 셋이 존재하지 않는다는 것을 참고하기 바란다.
        IWbemQualifierSet* pObjectProperties = NULL;
        if (FAILED(pClassObject->GetPropertyQualifierSet(
                wszObjectPropName, &pObjectProperties)))
        {
            continue;
        }
 
        // 속성 타입들을 이용해 속성 문자열을 읽어온다.
        if (FAILED(pObjectProperties->Get(
            L"CIMTYPE", 0L, &pObjectPropType, NULL)))
        {
            continue;
        }
 
        // 속성 문자열을 일반 문자열로 변환해둔다.
        string property_type_string = WideString(V_BSTR(&pObjectPropType));
 
        // 속성의 값을 읽어온다.
        if (FAILED(pClassObject->Get(
            wszObjectPropName, 0, &pObjectPropValue, NULL, NULL)))
        {
            continue;
        }
 
        // 속성의 값 타입에 따라, 적당히 문자열로 변환해준 뒤,
        // 그 값을 상세 사항 맵에다가 집어넣는다.
        Detail[wszObjectPropName.toString()] =
Variant2String(pObjectPropValue);
    }
 
    // 속성 이름 배열은 사용이 끝났으니 삭제해준다.
    ::SafeArrayDestroy(psaNames);
 
    // 클래스 오브젝트도 마찬가지...
    pClassObject->Release();
}
 
//////////////////////////////////////////////////////////////////////////////
/// \brief enumerate 함수를 사용해서 생성한 오브젝트의 수를 반환한다.
//////////////////////////////////////////////////////////////////////////////
size_t WMIAccessor::size() const
{
    return m_pImpl->Names.size();
}
 
//////////////////////////////////////////////////////////////////////////////
/// \brief 지정된 오브젝트의 속성값을 반환한다.
///
/// 인덱스가 범위를 벗어나거나, 해당하는 속성이 존재하지 않는 경우에는
/// 빈 문자열을 반환한다.
/// 
/// \param index 오브젝트 인덱스
/// \param property_name 속성 이름
/// \return string 속성의 값
//////////////////////////////////////////////////////////////////////////////
string WMIAccessor::asString(size_t index, const string& property_name) const
{
    if (index < m_pImpl->Names.size())
    {
        IMPL::DETAIL_MAP::const_iterator i = 
            m_pImpl->Details.find(m_pImpl->Names[index]);
 
        if (i == m_pImpl->Details.end()) return "";
 
        const IMPL::DETAIL& detail = i->second;
 
        IMPL::DETAIL::const_iterator j = detail.find(property_name);
        return j != detail.end() ? j->second : "";
    }
 
    return "";
}
 
//////////////////////////////////////////////////////////////////////////////
/// \brief 지정된 오브젝트의 속성값을 반환한다.
///
/// 인덱스가 범위를 벗어나거나, 해당하는 속성이 존재하지 않는 경우에는
/// -1을 반환한다.
/// 
/// \param index 오브젝트 인덱스
/// \param property_name 속성 이름
/// \return string 속성의 값
//////////////////////////////////////////////////////////////////////////////
int WMIAccessor::asInt(size_t index, const string& property_name) const
{
    if (index < m_pImpl->Names.size())
    {
        IMPL::DETAIL_MAP::const_iterator i = 
            m_pImpl->Details.find(m_pImpl->Names[index]);
 
        if (i == m_pImpl->Details.end()) return -1;
 
        const IMPL::DETAIL& detail = i->second;
 
        IMPL::DETAIL::const_iterator j = detail.find(property_name);
        return j != detail.end() ? atoi((j->second).c_str()) : -1;
    }
 
    return -1;
}
 
//////////////////////////////////////////////////////////////////////////////
/// \brief 내부 데이터를 문자열로 변환해서 반환한다.
/// \return string 변환한 데이터
//////////////////////////////////////////////////////////////////////////////
string WMIAccessor::toString() const
{
    string msg;
 
    for (IMPL::DETAIL_MAP::const_iterator i = m_pImpl->Details.begin();
        i != m_pImpl->Details.end(); i++)
    {
        const IMPL::DETAIL& detail = i->second;
        msg += string("DEVICE ") + i->first + "\n";
        for (IMPL::DETAIL::const_iterator j=detail.begin();
            j != detail.end(); j++)
        {
            msg += string("\t") + j->first + string(" = ") + j->second + "\n";
        }
    }
 
    return msg;
}
 
//////////////////////////////////////////////////////////////////////////////
/// \brief WMIAccessor 클래스를 사용하기 전에 불러줘야하는 초기화 함수.
///
/// OLE DLL을 로드하고, 보안 권한 수정을 행한다.
//////////////////////////////////////////////////////////////////////////////
void WMIAccessor::initialize()
{
    IMPL::s_bOLEInit = (::CoInitialize(NULL) == S_OK);
    if (!IMPL::s_bOLEInit) 
    {
        filelog("Failed to initialize the OLE libraries");
        assert(false);
    }
 
    // Adjust the security to allow client impersonation.
    // NOTE:
    // When using asynchronous WMI API's remotely in an environment where 
    // the "Local System" account has no network identity 
    // (such as non-Kerberos domains), the authentication level of 
    // RPC_C_AUTHN_LEVEL_NONE is needed. However, lowering the authentication 
    // level to RPC_C_AUTHN_LEVEL_NONE makes your application less secure. 
    // It is wise to use semi-synchronous API's for accessing WMI data and 
    // events instead of the asynchronous ones.
    HRESULT hres = ::CoInitializeSecurity( 
        NULL, -1, NULL, NULL, 
        RPC_C_AUTHN_LEVEL_PKT_PRIVACY, 
        RPC_C_IMP_LEVEL_IMPERSONATE, 
        NULL, 
        EOAC_SECURE_REFS, //change to EOAC_NONE if you change dwAuthnLevel to
RPC_C_AUTHN_LEVEL_NONE
        NULL);
 
    if (FAILED(hres))
    {
        filelog("Failed to fix security!");
        assert(false);
    }
}
 
//////////////////////////////////////////////////////////////////////////////
/// \brief WMIAccessor 클래스를 사용한 후에 불러줘야하는 초기화 함수.
///
/// OLE DLL의 정리를 담당한다.
//////////////////////////////////////////////////////////////////////////////
void WMIAccessor::finalize()
{
    if (IMPL::s_bOLEInit) { ::CoUninitialize(); }
}

사용법

int main()
{
    WMIAccessor::initialize();
 
    WMIAccessor video;
    video.enumerate("Win32_VideoController");
    cout << video.toString() << endl;
 
    WMIAccessor::finalize();
 
    return 0;
}

출력 결과

DEVICE Win32_VideoController.DeviceID="VideoController1"
    AdapterRAM = 33554432
    MaxRefreshRate = 150
    Name = NVIDIA GeForce2 MX/MX 400
    ReservedSystemPaletteEntries = NULL
    VideoModeDescription = 1280 x 1024 x 4294967296
    DitherType = NULL
    DriverVersion = 6.14.10.4403
    InfSection = nv4
    VideoMemoryType = 2
    CreationClassName = Win32_VideoController
    ProtocolSupported = NULL
    Description = NVIDIA GeForce2 MX/MX 400
    ...이하 생략...

kb/wmiusingcpp.txt · 마지막으로 수정됨: 2014/11/10 16:40 (바깥 편집)