<commit_before>/*    Copyright 2016-2021 Firewalla Inc.
 *
 *    This program is free software: you can redistribute it and/or  modify
 *    it under the terms of the GNU Affero General Public License, version 3,
 *    as published by the Free Software Foundation.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

'use strict';

const Platform = require('../Platform.js');
const f = require('../../net2/Firewalla.js')
const exec = require('child-process-promise').exec;
const log = require('../../net2/logger.js')(__filename);
const ipset = require('../../net2/Ipset.js');

const fs = require('fs');
const util = require('util');
const readFileAsync = util.promisify(fs.readFile)

const cpuProfilePath = "/etc/default/cpufrequtils";

class PurplePlatform extends Platform {

  getName() {
    return "purple";
  }

  getLicenseTypes() {
    return ["c1"];
  }

  getAllNicNames() {
    // there are two NICs on purple
    return ["eth0", "eth1"];
  }

  getDNSServiceName() {
    return "firerouter_dns";
  }

  getDHCPServiceName() {
    return "firerouter_dhcp";
  }

  getBoardSerial() {
    // use mac address as unique serial number
    return this.getSignatureMac();
  }

  getB4Binary() {
    return `${f.getFirewallaHome()}/bin/real.purple/bitbridge7`;
  }

  getB6Binary() {
    return `${f.getFirewallaHome()}/bin/real.purple/bitbridge6`;
  }

  getGCMemoryForMain() {
    return 200;
  }

  getDHKeySize() {
    return 2048;
  }

  getLedPaths() {
    return [
      "/sys/devices/platform/leds/leds/blue"
    ];
  }

  async switchQoS(state, qdisc) {
    if (state == false) {
      await exec(`sudo ipset add -! ${ipset.CONSTANTS.IPSET_QOS_OFF} ${ipset.CONSTANTS.IPSET_MATCH_ALL_SET4}`).catch((err) => {
        log.error(`Failed to add ${ipset.CONSTANTS.IPSET_MATCH_ALL_SET4} to ${ipset.CONSTANTS.IPSET_QOS_OFF}`, err.message);
      });
      await exec(`sudo ipset add -! ${ipset.CONSTANTS.IPSET_QOS_OFF} ${ipset.CONSTANTS.IPSET_MATCH_ALL_SET6}`).catch((err) => {
        log.error(`Failed to add ${ipset.CONSTANTS.IPSET_MATCH_ALL_SET6} to ${ipset.CONSTANTS.IPSET_QOS_OFF}`, err.message);
      });
    } else {
      await exec(`sudo ipset del -! ${ipset.CONSTANTS.IPSET_QOS_OFF} ${ipset.CONSTANTS.IPSET_MATCH_ALL_SET4}`).catch((err) => {
        log.error(`Failed to remove ${ipset.CONSTANTS.IPSET_MATCH_ALL_SET4} from ${ipset.CONSTANTS.IPSET_QOS_OFF}`, err.message);
      });
      await exec(`sudo ipset del -! ${ipset.CONSTANTS.IPSET_QOS_OFF} ${ipset.CONSTANTS.IPSET_MATCH_ALL_SET6}`).catch((err) => {
        log.error(`Failed to remove ${ipset.CONSTANTS.IPSET_MATCH_ALL_SET6} from ${ipset.CONSTANTS.IPSET_QOS_OFF}`, err.message);
      });
    }
  }

  getCPUDefaultFile() {
    return `${__dirname}/files/cpu_default.conf`;
  }

  async applyCPUDefaultProfile() {
    log.info("Applying CPU default profile...");
    const cmd = `sudo cp ${this.getCPUDefaultFile()} ${cpuProfilePath}`;
    await exec(cmd);
    return this.reload();
  }

  async reload() {
    return exec("sudo systemctl reload cpufrequtils");
  }

  getCPUBoostFile() {
    return `${__dirname}/files/cpu_boost.conf`;
  }

  async applyCPUBoostProfile() {
    log.info("Applying CPU boost profile...");
    const cmd = `sudo cp ${this.getCPUBoostFile()} ${cpuProfilePath}`;
    await exec(cmd);
    return this.reload();
  }

  getSubnetCapacity() {
    return 19;
  }

  // via /etc/update-motd.d/30-armbian-sysinfo
  async getCpuTemperature() {
    try {
      const source = '/sys/class/thermal/thermal_zone0/temp';
      return Number(await readFileAsync(source)) / 1000;
    } catch(err) {
      log.error("Failed to get cpu temperature, use 0 as default, err:", err);
      return 0;
    }
  }

  getPolicyCapacity() {
    return 3000;
  }

  isFireRouterManaged() {
    return true;
  }

  isWireguardSupported() {
    return true;
  }

  getAllowCustomizedProfiles(){
    return 1;
  }
  getRatelimitConfig(){
    return {
      "appMax": 120,
      "webMax": 240,
      "duration": 60
    }
  }

  isBonjourBroadcastEnabled() {
    return false;
  }

  isOverlayNetworkAvailable() {
    return false;
  }

  isIFBSupported() {
    return true;
  }

  getRetentionTimeMultiplier() {
    return 1;
  }

  getRetentionCountMultiplier() {
    return 1;
  }

  isAccountingSupported() {
    return true;
  }

  
  async applyProfile() {
    try {
      log.info("apply profile to optimize network performance");
      await exec(`sudo ${f.getFirewallaHome()}/scripts/apply_profile.sh`);
    } catch(err) {
      log.error("Error applying profile", err)
    }
  }
  getStatsSpecs() {
    return [{
      granularities: '1hour',
      hits: 72,
      stat: '3d'
    }]
  }

  async configFan(policy) {
    const FAN_MODE_PATH='/sys/devices/platform/pwm-fan/hwmon/hwmon0/pwm1_enable';
    const FAN_SPEED_PATH='/sys/devices/platform/pwm-fan/hwmon/hwmon0/pwm1';
    const FAN_MODE_MANUAL=1;
    const FAN_MODE_AUTO=2;
    const FAN_SPEED_MIN=0;
    const FAN_SPEED_MAX=255;
    try {
      log.info("config fan with policy: ",policy);

      log.info("set fan mode: ",policy.mode);
      switch (policy.mode) {
        case "auto" : {
          await exec(`echo ${FAN_MODE_AUTO} | sudo tee ${FAN_MODE_PATH}`);
          break;
        }
        case "manual" : {
          await exec(`echo ${FAN_MODE_MANUAL} | sudo tee ${FAN_MODE_PATH}`);
          break;
        }
        default: {
          log.error("unsupported fan mode: ",policy.mode)
        }
      }

      if ( 'speed' in policy ) {
        let fanSpeed = policy.speed;
        if ( fanSpeed>=FAN_SPEED_MIN && fanSpeed<=FAN_SPEED_MAX ) {
          log.info("set fan speed:",fanSpeed);
          await exec(`echo ${fanSpeed} | sudo tee ${FAN_SPEED_PATH}`);
        } else {
          log.error("Invalid fan speed value(beyond [0,255]):",fanSpeed);
        }
      }
    } catch(err) {
      log.error("Error config fan", err)
    }
  }

  async setLED(color, state) {
    const LED_PATH = '/sys/devices/platform/leds/leds'
    const LED_TRIGGER_ERROR = `${LED_PATH}/red/trigger`;
    const LED_TRIGGER_STATUS = `${LED_PATH}/blue/trigger`;
    const LED_STATE_ON = 'default-on'
    const LED_STATE_OFF = 'none'
    const LED_STATE_BLINK = 'timer'
    try {
      log.info(`set LED ${color} to ${state}`);
      let triggerPath = null;
      switch (color) {
        case "error": {
          triggerPath = LED_TRIGGER_ERROR;
          break;
        }
        case "status": {
          triggerPath = LED_TRIGGER_STATUS;
          break;
        }
        default: {
          break;
        }
      }
      let triggerState = null;
      switch (state) {
        case "on": {
          triggerState = LED_STATE_ON;
          break;
        }
        case "off": {
          triggerState = LED_STATE_OFF;
          break;
        }
        case "blink": {
          triggerState = LED_STATE_BLINK;
          break;
        }
        default: {
          break;
        }
      }
      if ( triggerPath && triggerState ) {
        log.debug(`set ${triggerPath} to ${triggerState}`);
        await exec(`echo "${triggerState}" | sudo tee ${triggerPath}`);
      }
    } catch (err) {
      log.error(`Failed to set LED ${color} to ${state}:`,err);
    }
  }

  async configLED(policy) {
    log.info("Apply LED configuration: ",policy);
    if ( 'mode' in policy ) {
      switch ( policy.mode ) {
        case "on"  : {
          log.info("Turn ON status LED.");
          await this.setLED("status","off");
          break;
        }
        case "off"  : {
          log.info("Turn OFF status LED.");
          await this.setLED("status","on");
          break;
        }
        case "auto" : {
          log.info("Status LED is automatically controlled by Firewalla");
          break;
        }
      }

    } else {
      log.error("Invalid policy with NO mode defined.");
    }
  }

  async updateLEDDisplay(systemState) {
    log.info("Update LED display based on system state");

    const SYSTEM_CHECKS = [ 'fireboot', 'firereset', 'firerouter' ];
    const NETWORK_CHECKS = [ 'fireboot_network', 'wan', 'firerouter_network'];

    let systemError = false;
    for (const comp of SYSTEM_CHECKS) {
      if (comp in systemState && systemState[comp] === 'fail') {
        systemError = true;
      }
    }
    let networkError = false;
    for (const comp of NETWORK_CHECKS) {
      if (comp in systemState && systemState[comp] === 'fail') {
        networkError = true;
      }
    }

    if (networkError) {
      await this.setLED("error", "blink")
    } else if (systemError) {
      await this.setLED("error", "on")
    } else {
      await this.setLED("error", "off")
    }

    switch (systemState.boot_state) {
      case "booting": {
        await this.setLED("status","blink");
        break;
      }
      case "ready4pairing": {
        await this.setLED("status","on");
        break;
      }
      case "paired": {
        await this.setLED("status","off");
        break;
      }
    }
  }

  async ledReadyForPairing() {
    try {
      this.updateLEDDisplay({boot_state:"ready4pairing"});
    } catch(err) {
      log.error("Error set LED as ready for pairing", err)
    }
  }

  async ledPaired() {
    try {
      this.updateLEDDisplay({boot_state:"paired"});
    } catch(err) {
      log.error("Error set LED as paired", err)
    }
  }

  async ledBooting() {
    try {
      this.updateLEDDisplay({boot_state:"booting"});
    } catch(err) {
      log.error("Error set LED as booting", err)
    }
  }

  getIftopPath() {
    return `${__dirname}/files/iftop`
  }

  getSpeedtestCliBinPath() {
    return `${__dirname}/files/speedtest`
  }

  async getWlanVendor() {
    if ( !this.vendor ) {
      const procCmdline = await fs.readFileAsync("/proc/cmdline", {encoding: 'utf8'});
      this.vendor = procCmdline.match(' wifi_rev=([0-9a-z]*) ')[1];
    }
    return this.vendor;
  }

  /* There are 2 variants for Purple
   *
   * Variant A
   * - Realtek WiFi chip
   * 
   * Variant B
   * - Ampak WiFi chip
   * 
   */
  async getVariant() {
    if ( !this.variant ) {
      switch (await this.getWlanVendor()) {
        case '88x2cs':
          this.variant = 'A';
          break;
        case 'dhd':
          this.variant = 'B';
          break;
        default:
          this.variant = '';
      }
    }
    return this.variant;
  }

  getDefaultWlanIntfName() {
    return 'wlan0'
  }
}

module.exports = PurplePlatform;
<commit_msg>add error handling in PurplePlatform.getWlanVendor<commit_after>/*    Copyright 2016-2021 Firewalla Inc.
 *
 *    This program is free software: you can redistribute it and/or  modify
 *    it under the terms of the GNU Affero General Public License, version 3,
 *    as published by the Free Software Foundation.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

'use strict';

const Platform = require('../Platform.js');
const f = require('../../net2/Firewalla.js')
const exec = require('child-process-promise').exec;
const log = require('../../net2/logger.js')(__filename);
const ipset = require('../../net2/Ipset.js');

const fs = require('fs');
const util = require('util');
const readFileAsync = util.promisify(fs.readFile)

const cpuProfilePath = "/etc/default/cpufrequtils";

class PurplePlatform extends Platform {

  getName() {
    return "purple";
  }

  getLicenseTypes() {
    return ["c1"];
  }

  getAllNicNames() {
    // there are two NICs on purple
    return ["eth0", "eth1"];
  }

  getDNSServiceName() {
    return "firerouter_dns";
  }

  getDHCPServiceName() {
    return "firerouter_dhcp";
  }

  getBoardSerial() {
    // use mac address as unique serial number
    return this.getSignatureMac();
  }

  getB4Binary() {
    return `${f.getFirewallaHome()}/bin/real.purple/bitbridge7`;
  }

  getB6Binary() {
    return `${f.getFirewallaHome()}/bin/real.purple/bitbridge6`;
  }

  getGCMemoryForMain() {
    return 200;
  }

  getDHKeySize() {
    return 2048;
  }

  getLedPaths() {
    return [
      "/sys/devices/platform/leds/leds/blue"
    ];
  }

  async switchQoS(state, qdisc) {
    if (state == false) {
      await exec(`sudo ipset add -! ${ipset.CONSTANTS.IPSET_QOS_OFF} ${ipset.CONSTANTS.IPSET_MATCH_ALL_SET4}`).catch((err) => {
        log.error(`Failed to add ${ipset.CONSTANTS.IPSET_MATCH_ALL_SET4} to ${ipset.CONSTANTS.IPSET_QOS_OFF}`, err.message);
      });
      await exec(`sudo ipset add -! ${ipset.CONSTANTS.IPSET_QOS_OFF} ${ipset.CONSTANTS.IPSET_MATCH_ALL_SET6}`).catch((err) => {
        log.error(`Failed to add ${ipset.CONSTANTS.IPSET_MATCH_ALL_SET6} to ${ipset.CONSTANTS.IPSET_QOS_OFF}`, err.message);
      });
    } else {
      await exec(`sudo ipset del -! ${ipset.CONSTANTS.IPSET_QOS_OFF} ${ipset.CONSTANTS.IPSET_MATCH_ALL_SET4}`).catch((err) => {
        log.error(`Failed to remove ${ipset.CONSTANTS.IPSET_MATCH_ALL_SET4} from ${ipset.CONSTANTS.IPSET_QOS_OFF}`, err.message);
      });
      await exec(`sudo ipset del -! ${ipset.CONSTANTS.IPSET_QOS_OFF} ${ipset.CONSTANTS.IPSET_MATCH_ALL_SET6}`).catch((err) => {
        log.error(`Failed to remove ${ipset.CONSTANTS.IPSET_MATCH_ALL_SET6} from ${ipset.CONSTANTS.IPSET_QOS_OFF}`, err.message);
      });
    }
  }

  getCPUDefaultFile() {
    return `${__dirname}/files/cpu_default.conf`;
  }

  async applyCPUDefaultProfile() {
    log.info("Applying CPU default profile...");
    const cmd = `sudo cp ${this.getCPUDefaultFile()} ${cpuProfilePath}`;
    await exec(cmd);
    return this.reload();
  }

  async reload() {
    return exec("sudo systemctl reload cpufrequtils");
  }

  getCPUBoostFile() {
    return `${__dirname}/files/cpu_boost.conf`;
  }

  async applyCPUBoostProfile() {
    log.info("Applying CPU boost profile...");
    const cmd = `sudo cp ${this.getCPUBoostFile()} ${cpuProfilePath}`;
    await exec(cmd);
    return this.reload();
  }

  getSubnetCapacity() {
    return 19;
  }

  // via /etc/update-motd.d/30-armbian-sysinfo
  async getCpuTemperature() {
    try {
      const source = '/sys/class/thermal/thermal_zone0/temp';
      return Number(await readFileAsync(source)) / 1000;
    } catch(err) {
      log.error("Failed to get cpu temperature, use 0 as default, err:", err);
      return 0;
    }
  }

  getPolicyCapacity() {
    return 3000;
  }

  isFireRouterManaged() {
    return true;
  }

  isWireguardSupported() {
    return true;
  }

  getAllowCustomizedProfiles(){
    return 1;
  }
  getRatelimitConfig(){
    return {
      "appMax": 120,
      "webMax": 240,
      "duration": 60
    }
  }

  isBonjourBroadcastEnabled() {
    return false;
  }

  isOverlayNetworkAvailable() {
    return false;
  }

  isIFBSupported() {
    return true;
  }

  getRetentionTimeMultiplier() {
    return 1;
  }

  getRetentionCountMultiplier() {
    return 1;
  }

  isAccountingSupported() {
    return true;
  }

  
  async applyProfile() {
    try {
      log.info("apply profile to optimize network performance");
      await exec(`sudo ${f.getFirewallaHome()}/scripts/apply_profile.sh`);
    } catch(err) {
      log.error("Error applying profile", err)
    }
  }
  getStatsSpecs() {
    return [{
      granularities: '1hour',
      hits: 72,
      stat: '3d'
    }]
  }

  async configFan(policy) {
    const FAN_MODE_PATH='/sys/devices/platform/pwm-fan/hwmon/hwmon0/pwm1_enable';
    const FAN_SPEED_PATH='/sys/devices/platform/pwm-fan/hwmon/hwmon0/pwm1';
    const FAN_MODE_MANUAL=1;
    const FAN_MODE_AUTO=2;
    const FAN_SPEED_MIN=0;
    const FAN_SPEED_MAX=255;
    try {
      log.info("config fan with policy: ",policy);

      log.info("set fan mode: ",policy.mode);
      switch (policy.mode) {
        case "auto" : {
          await exec(`echo ${FAN_MODE_AUTO} | sudo tee ${FAN_MODE_PATH}`);
          break;
        }
        case "manual" : {
          await exec(`echo ${FAN_MODE_MANUAL} | sudo tee ${FAN_MODE_PATH}`);
          break;
        }
        default: {
          log.error("unsupported fan mode: ",policy.mode)
        }
      }

      if ( 'speed' in policy ) {
        let fanSpeed = policy.speed;
        if ( fanSpeed>=FAN_SPEED_MIN && fanSpeed<=FAN_SPEED_MAX ) {
          log.info("set fan speed:",fanSpeed);
          await exec(`echo ${fanSpeed} | sudo tee ${FAN_SPEED_PATH}`);
        } else {
          log.error("Invalid fan speed value(beyond [0,255]):",fanSpeed);
        }
      }
    } catch(err) {
      log.error("Error config fan", err)
    }
  }

  async setLED(color, state) {
    const LED_PATH = '/sys/devices/platform/leds/leds'
    const LED_TRIGGER_ERROR = `${LED_PATH}/red/trigger`;
    const LED_TRIGGER_STATUS = `${LED_PATH}/blue/trigger`;
    const LED_STATE_ON = 'default-on'
    const LED_STATE_OFF = 'none'
    const LED_STATE_BLINK = 'timer'
    try {
      log.info(`set LED ${color} to ${state}`);
      let triggerPath = null;
      switch (color) {
        case "error": {
          triggerPath = LED_TRIGGER_ERROR;
          break;
        }
        case "status": {
          triggerPath = LED_TRIGGER_STATUS;
          break;
        }
        default: {
          break;
        }
      }
      let triggerState = null;
      switch (state) {
        case "on": {
          triggerState = LED_STATE_ON;
          break;
        }
        case "off": {
          triggerState = LED_STATE_OFF;
          break;
        }
        case "blink": {
          triggerState = LED_STATE_BLINK;
          break;
        }
        default: {
          break;
        }
      }
      if ( triggerPath && triggerState ) {
        log.debug(`set ${triggerPath} to ${triggerState}`);
        await exec(`echo "${triggerState}" | sudo tee ${triggerPath}`);
      }
    } catch (err) {
      log.error(`Failed to set LED ${color} to ${state}:`,err);
    }
  }

  async configLED(policy) {
    log.info("Apply LED configuration: ",policy);
    if ( 'mode' in policy ) {
      switch ( policy.mode ) {
        case "on"  : {
          log.info("Turn ON status LED.");
          await this.setLED("status","off");
          break;
        }
        case "off"  : {
          log.info("Turn OFF status LED.");
          await this.setLED("status","on");
          break;
        }
        case "auto" : {
          log.info("Status LED is automatically controlled by Firewalla");
          break;
        }
      }

    } else {
      log.error("Invalid policy with NO mode defined.");
    }
  }

  async updateLEDDisplay(systemState) {
    log.info("Update LED display based on system state");

    const SYSTEM_CHECKS = [ 'fireboot', 'firereset', 'firerouter' ];
    const NETWORK_CHECKS = [ 'fireboot_network', 'wan', 'firerouter_network'];

    let systemError = false;
    for (const comp of SYSTEM_CHECKS) {
      if (comp in systemState && systemState[comp] === 'fail') {
        systemError = true;
      }
    }
    let networkError = false;
    for (const comp of NETWORK_CHECKS) {
      if (comp in systemState && systemState[comp] === 'fail') {
        networkError = true;
      }
    }

    if (networkError) {
      await this.setLED("error", "blink")
    } else if (systemError) {
      await this.setLED("error", "on")
    } else {
      await this.setLED("error", "off")
    }

    switch (systemState.boot_state) {
      case "booting": {
        await this.setLED("status","blink");
        break;
      }
      case "ready4pairing": {
        await this.setLED("status","on");
        break;
      }
      case "paired": {
        await this.setLED("status","off");
        break;
      }
    }
  }

  async ledReadyForPairing() {
    try {
      this.updateLEDDisplay({boot_state:"ready4pairing"});
    } catch(err) {
      log.error("Error set LED as ready for pairing", err)
    }
  }

  async ledPaired() {
    try {
      this.updateLEDDisplay({boot_state:"paired"});
    } catch(err) {
      log.error("Error set LED as paired", err)
    }
  }

  async ledBooting() {
    try {
      this.updateLEDDisplay({boot_state:"booting"});
    } catch(err) {
      log.error("Error set LED as booting", err)
    }
  }

  getIftopPath() {
    return `${__dirname}/files/iftop`
  }

  getSpeedtestCliBinPath() {
    return `${__dirname}/files/speedtest`
  }

  async getWlanVendor() {
    if ( !this.vendor ) {
      this.vendor = await fs.readFileAsync("/proc/cmdline", {encoding: 'utf8'}).then(cmdline => cmdline.match(' wifi_rev=([0-9a-z]*) ')[1]).catch(err => {
        log.error("Failed to parse wifi_rev from /proc/cmdline", err.message);
        return "unknown";
      });
    }
    return this.vendor;
  }

  /* There are 2 variants for Purple
   *
   * Variant A
   * - Realtek WiFi chip
   * 
   * Variant B
   * - Ampak WiFi chip
   * 
   */
  async getVariant() {
    if ( !this.variant ) {
      switch (await this.getWlanVendor()) {
        case '88x2cs':
          this.variant = 'A';
          break;
        case 'dhd':
          this.variant = 'B';
          break;
        default:
          this.variant = '';
      }
    }
    return this.variant;
  }

  getDefaultWlanIntfName() {
    return 'wlan0'
  }
}

module.exports = PurplePlatform;

{'fraction_non_alphanumeric': 0.1386821064240419, 'fraction_numerical': 0.00817939527616947, 'mean_word_length': 2.4261649874055418, 'pattern_counts': {'":': 22, '<': 7, '<?xml version=': 0, '>': 17, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2994317', 'n_tokens_mistral': 7460, 'n_tokens_neox': 7239, 'n_words': 1547}