WMI.js

/* globals Wsh: false */

(function () {
  if (Wsh && Wsh.OS) return;

  /**
   * Shorthand of WScript.CreateObject('Shell.Application')
   *
   * @namespace ShellApplication
   * @memberof Wsh
   */
  if (!Wsh.ShellApplication) {
    Wsh.ShellApplication = WScript.CreateObject('Shell.Application');
  }

  /**
   * This module takes charge of basis handling Windows OS (Partly similar to Node.js-OS).
   *
   * @namespace OS
   * @memberof Wsh
   * @requires {@link https://github.com/tuckn/WshPath|tuckn/WshPath}
   */
  Wsh.OS = {};

  // Shorthands
  var CD = Wsh.Constants;
  var util = Wsh.Util;
  var fso = Wsh.FileSystemObject;
  var path = Wsh.Path;

  var insp = util.inspect;
  var isArray = util.isArray;
  var isPureNumber = util.isPureNumber;
  var isString = util.isString;
  var isSolidArray = util.isSolidArray;
  var isSolidString = util.isSolidString;
  var isSameMeaning = util.isSameMeaning;
  var obtain = util.obtainPropVal;
  var includes = util.includes;

  var os = Wsh.OS;

  /** @constant {string} */
  var MODULE_TITLE = 'WshOS/WMI.js';

  var throwErrNonStr = function (functionName, typeErrVal) {
    util.throwTypeError('string', MODULE_TITLE, functionName, typeErrVal);
  };

  /**
   * The wrapper object for functions to handle {@link https://docs.microsoft.com/en-us/windows/win32/wmisdk/wmi-start-page|WMI} (Windows Management Instrumentation. WBEM for Windows).
   *
   * @namespace WMI
   * @memberof Wsh.OS
   */
  os.WMI = {};

  /**
   * An SWbemObjectSet object is a collection of {@link https://docs.microsoft.com/en-us/windows/win32/wmisdk/swbemobjectset|SWbemObjectSet object}
   *
   * @name sWbemObjectSet
   */

  // os.WMI.execQuery {{{
  /**
   * @typedef {object} typeExecWmiQueryOptions
   * @property {string} [compName=null] - Ex1. server1.network.fabrikam Ex2. 111.222.333.444
   * @property {string} [domainName="."]
   * @property {string} [namespace="."]
   */

  /**
   * Executes the query to WMI (Windows Management Instrumentation. WBEM for Windows). {@link https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/scripting-articles/ms974592(v=msdn.10)?redirectedfrom=MSDN|Microsoft Docs} {@link http://www.wmifun.net/library/|Ref.1}
   *
   * @example
   * var wmi = Wsh.OS.WMI; // Shorthand
   *
   * // Ex.1 Gets all processes
   * var query = 'SELECT * FROM Win32_Process';
   * var sWbemObjSets = wmi.execQuery(query);
   *
   * // Ex.2 Add WHERE phrase (Do not omit the extension)
   * var query = 'SELECT * FROM Win32_Process WHERE Caption = "notepad.exe"';
   * var sWbemObjSets = wmi.execQuery(query);
   *
   * sWbemObjSets.forEach(function (sWbemObjSet) {
   *   console.log('ProcessID: ' + sWbemObjSet.ProcessId);
   *   console.log('ExecutablePath: ' + sWbemObjSet.ExecutablePath);
   *   var iRetVal = sWbemObjSet.Terminate();
   *   console.log('Terminated it with returning value ' + iRetVal);
   * });
   * // Outputs:
   * //   ProcessID: 8356
   * //   ExecutablePath: C:\WINDOWS\system32\notepad.exe
   * //   Terminated it with returning value 0
   * //   ProcessID: 26712
   * //   ExecutablePath: C:\WINDOWS\system32\notepad.exe
   * //   Terminated it with returning value 0
   * //   ....
   *
   * // The below code does not work. Because the return object is not JS objects.
   * // Use Wsh.OS.WMI.toJsObjects to convert to JS Objects
   * Object.keys(sWbemObjSets[0]).forEach(function (propName) {
   *   console.log(propName + ': ' + sWbemObjSets[0][propName]);
   * });
   * // No display
   *
   * // Ex.3 Specifing options
   * var query = 'SELECT * FROM CIM_BIOSElement';
   * var sWbemObjSets = wmi.execQuery(query, {
   *   compName: 'otherComp.office.local',
   *   domainName: 'office.local'
   * });
   *
   * // Require converting to read the array property in SWbemObjectSet
   * console.log(new VBArray(sWbemObjSets[0].BiosCharacteristics).toArray());
   *
   * // Ex.4 Specifing the other namespace
   * var query = 'SELECT * FROM CIM_LogicalElement';
   * var sWbemObjSets = wmi.execQuery(query, {
   *   namespace: '\\\\.\\root\\Hardware'
   * });
   * @function execQuery
   * @memberof Wsh.OS.WMI
   * @param {string} query - The query to execute.
   * @param {typeExecWmiQueryOptions} [options] - Optional parameters.
   * @returns {sWbemObjectSet[]} - The array of enumerated SWbem-Object-Sets.
   */
  os.WMI.execQuery = function (query, options) {
    var FN = 'os.WMI.execQuery';
    if (!isSolidString(query)) throwErrNonStr(FN, query);

    var compName = obtain(options, 'compName', null);
    var domainName = obtain(options, 'domainName', '.');
    var namespace = obtain(options, 'namespace', '\\\\' + domainName + '\\root\\CIMV2');

    /** WBEM:Web-Based Enterprise Management */
    var wbem = WScript.CreateObject('WbemScripting.SWbemLocator');
    // var swbemServices = GetObject(namespace);
    /** @note The Above code equal WScript.CreateObject('WbemScripting.SWbemLocator').ConnectServer(null, namespace) */

    try {
      // Gets SWbemServices object
      /**
       * root\CIMV2 is the local computer namespace. CIM = Common Information Model. {@link https://docs.microsoft.com/en-us/windows/win32/wmisdk/swbemlocator-connectserver|Microsoft Docs}
       */
      var swbemServices = wbem.ConnectServer(compName, namespace);
      // Execute a query. Gets SWbemObjectSet object.
      var swbemSet = swbemServices.ExecQuery(query);
      // Convert SWbemObjectSet object to Enumerator object for JScript.
      var enumeSet = new Enumerator(swbemSet);
      var sWbemObjSets = [];
      if (!sWbemObjSets) return [];

      while (!enumeSet.atEnd()) {
        sWbemObjSets.push(enumeSet.item());
        enumeSet.moveNext();
      }

      return sWbemObjSets;
    } catch (e) {
      throw new Error(insp(e) + '\n'
        + '  at ' + FN + ' (' + MODULE_TITLE + ')\n'
        + '  query: ' + query + '\n  options: ' + insp(options));
    }
  }; // }}}

  // os.WMI.toJsObject {{{
  /**
   * Converts Enumerated SWbemObjectSets to JScript Object. {@link https://qiita.com/tnakagawa/items/ae579f19d74dd86e40c6|Ref.2}
   *
   * @example
   * var wmi = Wsh.OS.WMI;
   *
   * var sWbemObjSets = wmi.execQuery('SELECT * FROM CIM_BIOSElement');
   * var biosElements = wmi.toJsObject(sWbemObjSets[0]);
   * console.dir(biosElements.ListOfLanguages);
   * // Outputs: [
   * //   "en|US|iso8859-1",
   * //   "fr|FR|iso8859-1",
   * //   "zh|TW|unicode",
   * //   "zh|CN|unicode",
   * //   "ja|JP|unicode",
   * //   "de|DE|iso8859-1",
   * //   "es|ES|iso8859-1",
   * //   "ru|RU|iso8859-5",
   * //   "ko|KR|unicode" ],
   * @function toJsObject
   * @memberof Wsh.OS.WMI
   * @param {sWbemObjectSet} sWbemObjSet - The SWbemObjectSet.
   * @returns {object} - The converted object.
   */
  os.WMI.toJsObject = function (sWbemObjSet) {
    // var FN = 'os.WMI.toJsObject';
    var wmiObj = {};

    // Store Properties
    var enumedProps = new Enumerator(sWbemObjSet.Properties_);

    while (!enumedProps.atEnd()) {
      var propItem = enumedProps.item();

      var val = propItem.Value;
      if (propItem.IsArray) {
        /**
         * @function toArray
         * @memberof VBArray
         * Returns the standard JavaScript array converted from a VBArray. {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Microsoft_Extensions/VBArray/toArray|MDN}
         * @param {VBArray} safeArray
         * @returns {Array} - The standard JavaScript array
         */
        if (val !== null) val = new VBArray(val).toArray();
      }

      wmiObj[propItem.Name] = val;
      enumedProps.moveNext();
    }

    // Store Methods
    /**
     * @todo Not work.
    var enumedMethods = new Enumerator(sWbemObjSet.Methods_);

    while (!enumedMethods.atEnd()) {
      var methodItem = enumedMethods.item();
      var methdName = methodItem.Name;
      // Property name and method name do not overlap. maybe...
      wmiObj[methdName] = sWbemObjSet[methdName];
      enumedMethods.moveNext();
    }
    */

    return wmiObj;
  }; // }}}

  // os.WMI.toJsObjects {{{
  /**
   * Converts Enumerated SWbemObjectSets to JScript Objects. {@link https://qiita.com/tnakagawa/items/ae579f19d74dd86e40c6|Ref.2}
   *
   * @example
   * var wmi = Wsh.OS.WMI;
   *
   * var sWbemObjSets = wmi.execQuery('SELECT * FROM CIM_BIOSElement');
   * var wmiObjs = wmi.toJsObjects(sWbemObjSets);
   * console.dir(wmiObjs[0].ListOfLanguages);
   * // Outputs: [
   * //   "en|US|iso8859-1",
   * //   "fr|FR|iso8859-1",
   * //   "zh|TW|unicode",
   * //   "zh|CN|unicode",
   * //   "ja|JP|unicode",
   * //   "de|DE|iso8859-1",
   * //   "es|ES|iso8859-1",
   * //   "ru|RU|iso8859-5",
   * //   "ko|KR|unicode" ],
   * @function toJsObjects
   * @memberof Wsh.OS.WMI
   * @param {sWbemObjectSet[]} sWbemObjSets - The SWbemObjectSets.
   * @returns {object[]} - The array of converted objects.
   */
  os.WMI.toJsObjects = function (sWbemObjSets) {
    // var FN = 'os.WMI.toJsObjects';

    var wmiObjs = sWbemObjSets.map(function (sWbemObjSet) {
      return os.WMI.toJsObject(sWbemObjSet);
    });

    return wmiObjs;
  }; // }}}

  // os.WMI.getProcesses {{{
  /**
   * @typedef {object} typeGetProcessesOptions
   * @property {string[]} [selects=[]] - The field names to output. 'Caption', 'CommandLine', 'ExecutablePath', 'ProcessID'. default '*'.
   * @property {string[]} [matchWords=[]] - The matching filter words.
   * @property {string[]} [excludingWords=[]] - The excluding filter words.
   */

  /**
   * Returns Enumerated SWbemObjectSets of the specified process. {@link https://docs.microsoft.com/en-us/windows/win32/cimwin32prov/win32-process|Microsoft Docs}
   *
   * @example
   * var wmi = Wsh.OS.WMI;
   *
   * // Ex.1 Specifing a process name (Do not omit the extension)
   * var sWbemObjSets = wmi.getProcesses('chrome.exe');
   *
   * sWbemObjSets.forEach(function (sWbemObjSet) {
   *   console.log('ProcessID: ' + sWbemObjSet.ProcessId);
   *   console.log('Handle: ' + sWbemObjSet.Handle);
   *   console.log('CreationDate: ' + sWbemObjSet.CreationDate);
   *
   *   try {
   *     var iRetVal = sWbemObjSet.Terminate();
   *     console.log('Terminated it with returning value ' + iRetVal);
   *   } catch (e) {
   *     console.error('Failed to terminate. Already terminated?');
   *   }
   * });
   * // Outputs:
   * //   ProcessID: 8356
   * //   Handle: 18012
   * //   CreationDate: 20200217042344.517814+540
   * //   Terminated it with returning value 0
   * //   ....
   *
   * // Ex.2 Specifing a process ID
   * var sWbemObjSets = wmi.getProcesses(8356);
   *
   * // Ex.3 Specifing a full path
   * var sWbemObjSets = wmi.getProcesses('D:\\Apps\\Firefox\\firefox.exe');
   *
   * // Ex.4 Specifing options
   * var sWbemObjSets = wmi.getProcesses('node.exe', {
   *   matchWords: ['nodemon'], // Compare with CommandLine
   *   excludingWords: ['80', '8080'] // Compare with CommandLine
   * });
   * @function getProcesses
   * @memberof Wsh.OS.WMI
   * @param {(number|string)} [processName] - The PID or the process name or the full path. If empty to specify all processes.
   * @param {typeGetProcessesOptions} [options] - Optional parameters.
   * @returns {sWbemObjectSet[]} - The array of enumerated SWbem-Object-Sets.
   */
  os.WMI.getProcesses = function (processName, options) {
    var FN = 'os.WMI.getProcesses';

    var query = 'SELECT';

    // SELECT phrase
    var selects = obtain(options, 'selects', []);
    var selectQuery = '';
    if (isSolidArray(selects)) {
      // The essential selectors
      if (!includes(selects, 'Caption')) selects.push('Caption');
      if (!includes(selects, 'CommandLine')) selects.push('CommandLine');
      if (!includes(selects, 'ExecutablePath')) selects.push('ExecutablePath');
      if (!includes(selects, 'ProcessID')) selects.push('ProcessID');

      // Join
      for (var i = 0, len = selects.length - 1; i < len; i++) {
        selectQuery += selects[i] + ', ';
      }
      selectQuery += util.last(selects);
    } else {
      selectQuery = '*';
    }

    query += ' ' + selectQuery + ' FROM Win32_Process';

    // WHERE phrase
    if (!isPureNumber(processName) && !isString(processName)) {
      throwErrNonStr(FN, processName);
    }
    var comparesFullPath = false;

    if (isPureNumber(processName)) { // PID
      query += ' WHERE ProcessID=' + String(processName);
    } else if (isSolidString(processName)) {
      query += ' WHERE Caption="' + path.basename(processName) + '"';
      comparesFullPath = path.isAbsolute(processName);
    }

    // Execute
    var sWbemObjSets = os.WMI.execQuery(query);
    if (sWbemObjSets.length === 0) return [];

    // Filter
    var matchWords = obtain(options, 'matchWords', []);
    // Convert matchWords to Array, if it is string
    if (isSolidString(matchWords)) {
      matchWords = [matchWords];
    } else if (!isArray(matchWords)) {
      matchWords = [];
    }

    var excludingWords = obtain(options, 'excludingWords', []);
    // Convert excludedWords to Array, if it is string
    if (isSolidString(excludingWords)) {
      excludingWords = [excludingWords];
    } else if (!isArray(excludingWords)) {
      excludingWords = [];
    }

    return sWbemObjSets.filter(function (sWbemObjSet) {
      // Filter with ExecutablePath
      if (comparesFullPath
          && !isSameMeaning(sWbemObjSet.ExecutablePath, processName)) {
        return false;
      }

      // Filter with CommandLine
      /** CommandLine sometimes does not contain a full path */
      var notMatched = matchWords.some(function (word) {
        if (!includes(sWbemObjSet.CommandLine, word, 'i')) return true;
      });
      if (notMatched) return false;

      var matchedExcluding = excludingWords.some(function (word) {
        if (includes(sWbemObjSet.CommandLine, word, 'i')) return true;
      });
      if (matchedExcluding) return false;

      return true;
    });
  }; // }}}

  // os.WMI.getProcess {{{
  /**
   * Returns the SWbemObjectSet of the specified process
   *
   * @example
   * var wmi = Wsh.OS.WMI;
   *
   * var sWbemObjSet = wmi.getProcess('excel.exe');
   *
   * console.log('ProcessID: ' + sWbemObjSet.ProcessId);
   * console.log('ParentProcessId: ' + sWbemObjSet.ParentProcessId);
   * var iRetVal = sWbemObjSet.Terminate();
   * console.log('Terminated it with returning value ' + iRetVal);
   * // Outputs:
   * //   ProcessID: 19220
   * //   ParentProcessId: 160624
   * //   Terminated it with returning value 0
   * @function getProcess
   * @memberof Wsh.OS.WMI
   * @param {(number|string)} [processName] - The PID or the process name or the full path. If empty to specify all processes.
   * @param {typeGetProcessesOptions} [options] - Optional parameters.
   * @returns {sWbemObjectSet|null} - The enumerated SWbem-Object-Set.
   */
  os.WMI.getProcess = function (processName, options) {
    var FN = 'os.WMI.getProcess';
    if (!isPureNumber(processName) && !isString(processName)) {
      throwErrNonStr(FN, processName);
    }

    var sWbemObjSets = os.WMI.getProcesses(processName, options);

    if (sWbemObjSets.length === 0) return null;
    return sWbemObjSets[0];
  }; // }}}

  // os.WMI.getWithSWbemPath {{{
  /**
   * Returns the Enumerated SWbem-Object-Sets from the SWbem path (e.g. 'Win32_LogicalDisk.DeviceID="C:"').
   *
   * @example
   * var wmi = Wsh.OS.WMI;
   *
   * var sWbemPath = 'Win32_LogicalDisk.DeviceID="C:"';
   * var sWbemObjSet = wmi.getWithSWbemPath(sWbemPath);
   *
   * console.log('DeviceID: ' + sWbemObjSet.DeviceID);
   * console.log('Caption: ' + sWbemObjSet.Caption);
   * console.log('FileSystem: ' + sWbemObjSet.FileSystem);
   * console.log('FreeSpace: ' + sWbemObjSet.FreeSpace);
   * // Outputs:
   * //   DeviceID: C:
   * //   Caption: C:
   * //   FileSystem: NTFS
   * //   FreeSpace: 193899817343
   *
   * // The below code does not work. Because the return object is not JS objects.
   * // Use Wsh.OS.WMI.toJsObject to convert to A JS Object
   * Object.keys(sWbemObjSet).forEach(function (propName) {
   *   console.log(propName + ': ' + sWbemObjSet[propName]);
   * });
   * // No display
   *
   * // Ex.2 @todo Not work. Please tell me why
   * var sWbemPath = 'Win32_Process.ProcessId=11888';
   * var sWbemObjSet = os.WMI.getWithSWbemPath(sWbemPath); // Error [-2147217350]
   * @function getWithSWbemPath
   * @memberof Wsh.OS.WMI
   * @param {string} sWbemPath - The SWbem path.
   * @param {typeExecWmiQueryOptions} [options] - Optional parameters.
   * @returns {sWbemObjectSet|null} - The enumerated SWbem-Object-Set.
   */
  os.WMI.getWithSWbemPath = function (sWbemPath, options) {
    var FN = 'os.WMI.getWithSWbemPath';
    if (!isSolidString(sWbemPath)) throwErrNonStr(FN, sWbemPath);

    var compName = obtain(options, 'compName', null);
    var domainName = obtain(options, 'domainName', '.');
    var namespace = obtain(options, 'namespace', '\\\\' + domainName + '\\root\\CIMV2');
    var wbem = WScript.CreateObject('WbemScripting.SWbemLocator');

    try {
      var swbemServices = wbem.ConnectServer(compName, namespace);
      var sWbemObjSet = swbemServices.Get(sWbemPath);
      if (!sWbemObjSet) return null;
      return sWbemObjSet;
    } catch (e) {
      throw new Error(insp(e) + '\n'
        + '  at ' + FN + ' (' + MODULE_TITLE + ')\n'
        + '  query: ' + sWbemPath + '\n  options: ' + insp(options));
    }
  }; // }}}

  // os.WMI.getWindowsUserAccounts {{{
  /**
   * Returns Enumerated SWbemObjectSets of Windows user accounts. {@link https://docs.microsoft.com/en-us/windows/win32/cimwin32prov/win32-useraccount|Microsoft Docs}
   *
   * @example
   * var sWbemObjSets = os.WMI.getWindowsUserAccounts();
   *
   * sWbemObjSets.forEach(function (sWbemObjSet) {
   *   console.log('AccountType: ' + sWbemObjSet.AccountType);
   *   console.log('Caption: ' + sWbemObjSet.Caption);
   *   console.log('Name: ' + sWbemObjSet.Name);
   *   console.log('Domain: ' + sWbemObjSet.Domain);
   *   console.log('Status: ' + sWbemObjSet.Status);
   * });
   * // Outputs:
   * //   AccountType: 512
   * //   Caption: COMPNAME\Administrator
   * //   Name: Administrator
   * //   Domain: COMPNAME
   * //   Status: Degraded
   * //   AccountType: 512
   * //   Caption: COMPNAME\DefaultAccount
   * //   ....
   * @function getWindowsUserAccounts
   * @memberof Wsh.OS.WMI
   * @returns {sWbemObjectSet[]} - The array of enumerated SWbem-Object-Sets.
   */
  os.WMI.getWindowsUserAccounts = function () {
    // var FN = 'os.WMI.getWindowsUserAccounts';
    var sWbemObjSets = os.WMI.execQuery('SELECT * FROM Win32_UserAccount');
    return sWbemObjSets;
  }; // }}}

  os._thisProcessID = undefined;
  os._thisProcessSWbemObjSet = undefined;
  os._thisParentProcessID = undefined;

  // os.WMI.getThisProcess {{{
  /**
   * Returns the enumerated SWbem-Object-Set of the self process.
   *
   * @example
   * var wmi = Wsh.OS.WMI;
   *
   * var thisProcess = wmi.getThisProcess();
   *
   * console.log('ProcessID: ' + thisProcess.ProcessId);
   * console.log('Caption: ' + thisProcess.Caption);
   * console.log('Name: ' + thisProcess.Name);
   * console.log('CommandLine: ' + thisProcess.CommandLine);
   * console.log('ParentProcessId: ' + thisProcess.ParentProcessId);
   * // Outputs:
   * //   ProcessID: 23576
   * //   Caption: cscript.exe
   * //   Name: cscript.exe
   * //   CommandLine: cscript.exe  //nologo OS.test.wsf -t WMI_getThisProcess
   * //   ParentProcessId: 300
   * @function getThisProcess
   * @memberof Wsh.OS.WMI
   * @returns {sWbemObjectSet} - The enumerated SWbem-Object-Set.
   */
  os.WMI.getThisProcess = function () {
    var FN = 'os.WMI.getThisProcess';

    if (os._thisProcessSWbemObjSet !== undefined) {
      return os._thisProcessSWbemObjSet;
    }

    // var FN = 'os.WMI.getThisProcess';
    var tmpJsCode = 'while(true) WScript.Sleep(1000);';
    var tmpJsPath = os.writeTempText(tmpJsCode, '.js');

    os.shRun(os.exefiles.wscript, [tmpJsPath]);

    var sWbemObjSet = os.WMI.getProcess(os.exefiles.wscript, {
      matchWords: [tmpJsPath] // tmpJsPath is the unique
    });

    // @FIXME sometime, ParentProcessId is null?
		try {
			os._thisProcessID = Number(sWbemObjSet.ParentProcessId);
		} catch (e) {
      throw new Error(insp(e) + '\n'
        + '  at ' + FN + ' (' + MODULE_TITLE + ')');
		}

    sWbemObjSet.Terminate();
    fso.DeleteFile(tmpJsPath, CD.fso.force.yes);

    os._thisProcessSWbemObjSet = os.WMI.getProcess(os._thisProcessID);
    os._thisParentProcessID = os._thisProcessSWbemObjSet.ParentProcessId;

    return os._thisProcessSWbemObjSet;
  }; // }}}
})();

// vim:set foldmethod=marker commentstring=//%s :