ethereum/mist

View on GitHub
modules/settings.js

Summary

Maintainability
D
1 day
Test Coverage
const { app } = require('electron');
const path = require('path');
const fs = require('fs');
const packageJson = require('../package.json');
const _ = require('./utils/underscore');
const lodash = require('lodash');

import {
  syncBuildConfig,
  syncFlags,
  setSwarmEnableOnStart
} from './core/settings/actions';
import logger from './utils/logger';

const settingsLog = logger.create('Settings');

let instance = null;

class Settings {
  constructor() {
    if (!instance) {
      instance = this;
    }

    return instance;
  }

  init() {
    const logLevel = { logLevel: argv.loglevel };
    const logFolder = { logFolder: path.join(this.userDataPath, 'logs') };
    const loggerOptions = Object.assign(argv, logLevel, logFolder);
    logger.setup(loggerOptions);

    store.dispatch(syncFlags(argv));

    // If -v flag provided, log the Mist version and exit
    if (argv.version) {
      settingsLog.info(`Mist v${this.appVersion}`);
      process.exit(0);
    }

    // Some Linux installations require this setting:
    if (argv.ignoreGpuBlacklist) {
      app.commandLine.appendSwitch('ignore-gpu-blacklist', 'true');
      store.dispatch({ type: '[MAIN]:IGNORE_GPU_BLACKLIST:SET' });
    }

    if (this.inAutoTestMode) {
      settingsLog.info('AUTOMATED TESTING');
      store.dispatch({ type: '[MAIN]:TEST_MODE:SET' });
    }

    settingsLog.info(`Running in production mode: ${this.inProductionMode}`);

    if (this.rpcMode === 'http') {
      settingsLog.warn(
        'Connecting to a node via HTTP instead of ipcMain. This is less secure!!!!'.toUpperCase()
      );
    }

    store.dispatch(syncBuildConfig('appVersion', packageJson.version));
    store.dispatch(syncBuildConfig('rpcMode', this.rpcMode));
    store.dispatch(syncBuildConfig('productionMode', this.inProductionMode));
    store.dispatch(syncBuildConfig('uiMode', this.uiMode));
  }

  // @returns "Application Support/Mist" in production mode
  // @returns "Application Support/Electron" in development mode
  get userDataPath() {
    return app.getPath('userData');
  }

  get dbFilePath() {
    const dbFileName = this.inAutoTestMode ? 'mist.test.lokidb' : 'mist.lokidb';
    return path.join(this.userDataPath, dbFileName);
  }

  get appDataPath() {
    // Application Support/
    return app.getPath('appData');
  }

  get userHomePath() {
    return app.getPath('home');
  }

  get cli() {
    return argv;
  }

  get appVersion() {
    return packageJson.version;
  }

  get appName() {
    return this.uiMode === 'mist' ? 'Mist' : 'Ethereum Wallet';
  }

  get appLicense() {
    return packageJson.license;
  }

  get uiMode() {
    return _.isString(argv.mode) ? argv.mode.toLowerCase() : argv.mode;
  }

  get inProductionMode() {
    return defaultConfig.production;
  }

  get inAutoTestMode() {
    return !!process.env.TEST_MODE;
  }

  get swarmURL() {
    return argv.swarmurl;
  }

  get gethPath() {
    return argv.gethpath;
  }

  get ethPath() {
    return argv.ethpath;
  }

  get rpcMode() {
    if (argv.rpc && argv.rpc.indexOf('http') === 0) return 'http';
    if (argv.rpc && argv.rpc.indexOf('ws:') === 0) {
      settingsLog.warn(
        'Websockets are not yet supported by Mist, using default IPC connection'
      );
      argv.rpc = null;
      return 'ipc';
    } else return 'ipc';
  }

  get rpcConnectConfig() {
    if (this.rpcMode === 'ipc') {
      return {
        path: this.rpcIpcPath
      };
    }

    return {
      hostPort: this.rpcHttpPath
    };
  }

  get rpcHttpPath() {
    return this.rpcMode === 'http' ? argv.rpc : null;
  }

  get rpcIpcPath() {
    let ipcPath = this.rpcMode === 'ipc' ? argv.rpc : null;

    if (ipcPath) {
      return ipcPath;
    }

    ipcPath = this.userHomePath;

    if (process.platform === 'darwin') {
      ipcPath += '/Library/Ethereum/geth.ipc';
    } else if (
      process.platform === 'freebsd' ||
      process.platform === 'linux' ||
      process.platform === 'sunos'
    ) {
      ipcPath += '/.ethereum/geth.ipc';
    } else if (process.platform === 'win32') {
      ipcPath = '\\\\.\\pipe\\geth.ipc';
    }

    settingsLog.debug(`IPC path: ${ipcPath}`);

    return ipcPath;
  }

  get nodeType() {
    return argv.node;
  }

  get network() {
    return argv.network;
  }

  get syncmode() {
    return argv.syncmode;
  }

  get nodeOptions() {
    return argv.nodeOptions;
  }

  get language() {
    return this.loadConfig('ui.i18n');
  }

  set language(langCode) {
    this.saveConfig('ui.i18n', langCode);
  }

  get enableSwarmOnStart() {
    if (global.mode === 'wallet') {
      return false;
    }

    if (argv.swarm) {
      return true;
    }

    const enableOnStart = this.loadConfig('swarm.enableOnStart');

    // Sync to redux
    if (enableOnStart) {
      store.dispatch(setSwarmEnableOnStart());
    }

    return enableOnStart;
  }

  set enableSwarmOnStart(bool) {
    this.saveConfig('swarm.enableOnStart', bool);
  }

  get skiptimesynccheck() {
    return argv.skiptimesynccheck;
  }

  initConfig() {
    global.config.insert({
      ui: {
        i18n: i18n.getBestMatchedLangCode(app.getLocale())
      },
      swarm: {
        enableOnStart: argv.swarm
      }
    });
  }

  saveConfig(key, value) {
    let obj = global.config.get(1);

    if (!obj) {
      this.initConfig();
      obj = global.config.get(1);
    }

    if (lodash.get(obj, key) !== value) {
      lodash.set(obj, key, value);
      global.config.update(obj);

      settingsLog.debug(`Settings: saveConfig('${key}', '${value}')`);
      settingsLog.trace(global.config.data);
    }
  }

  loadConfig(key) {
    const obj = global.config.get(1);

    if (!obj) {
      this.initConfig();
      return this.loadConfig(key);
    }

    settingsLog.trace(
      `Settings: loadConfig('${key}') = '${lodash.get(obj, key)}'`
    );

    return lodash.get(obj, key);
  }

  loadUserData(thisPath) {
    const fullPath = this.constructUserDataPath(thisPath);

    settingsLog.trace('Load user data', fullPath);

    // check if the file exists
    try {
      fs.accessSync(fullPath, fs.R_OK);
    } catch (err) {
      return null;
    }

    // try to read it
    try {
      const data = fs.readFileSync(fullPath, { encoding: 'utf8' });
      settingsLog.debug(`Reading "${data}" from ${fullPath}`);
      return data;
    } catch (err) {
      settingsLog.warn(`File not readable: ${fullPath}`, err);
    }

    return null;
  }

  saveUserData(thisPath, data) {
    if (!data) {
      // return so we dont write null, or other invalid data
      return;
    }

    const fullPath = this.constructUserDataPath(thisPath);

    try {
      settingsLog.debug(`Saving "${data}" to ${fullPath}`);
      fs.writeFileSync(fullPath, data, { encoding: 'utf8' });
    } catch (err) {
      settingsLog.warn(`Unable to write to ${fullPath}`, err);
    }
  }

  constructUserDataPath(filePath) {
    return path.join(this.userDataPath, filePath);
  }
}

module.exports = new Settings();

/* ==========================
Command line argument parsing
============================= */

// Load config
const defaultConfig = {
  mode: 'mist',
  production: false
};

try {
  _.extend(defaultConfig, require('../config.json'));
} catch (error) {
  settingsLog.error(error);
}

const argv = require('yargs')
  .usage('Usage: $0 [Mist options] [Node options]')
  .option({
    mode: {
      alias: 'm',
      demand: false,
      default: defaultConfig.mode,
      describe: 'App UI mode: wallet, mist.',
      requiresArg: true,
      nargs: 1,
      type: 'string',
      group: 'Mist options:'
    },
    node: {
      demand: false,
      default: null,
      describe: 'Node to use: geth, eth',
      requiresArg: true,
      nargs: 1,
      type: 'string',
      group: 'Mist options:'
    },
    network: {
      demand: false,
      default: null,
      describe: 'Network to connect to: main, test',
      requiresArg: true,
      nargs: 1,
      type: 'string',
      group: 'Mist options:'
    },
    rpc: {
      demand: false,
      describe:
        'Path to node IPC socket file OR HTTP RPC hostport (if IPC socket file then --node-ipcpath will be set with this value).',
      requiresArg: true,
      nargs: 1,
      type: 'string',
      group: 'Mist options:'
    },
    swarm: {
      describe: 'Enable Swarm on start.',
      requiresArg: false,
      nargs: 0,
      type: 'boolean',
      group: 'Mist options:'
    },
    swarmurl: {
      demand: false,
      default: 'http://localhost:8500',
      describe:
        'URL serving the Swarm HTTP API. If null, Mist will open a local node.',
      requiresArg: true,
      nargs: 1,
      type: 'string',
      group: 'Mist options:'
    },
    gethpath: {
      demand: false,
      describe: 'Path to Geth executable to use instead of default.',
      requiresArg: true,
      nargs: 1,
      type: 'string',
      group: 'Mist options:'
    },
    ethpath: {
      demand: false,
      describe: 'Path to Eth executable to use instead of default.',
      requiresArg: true,
      nargs: 1,
      type: 'string',
      group: 'Mist options:'
    },
    'ignore-gpu-blacklist': {
      demand: false,
      describe: 'Ignores GPU blacklist (needed for some Linux installations).',
      requiresArg: false,
      nargs: 0,
      type: 'boolean',
      group: 'Mist options:'
    },
    'reset-tabs': {
      demand: false,
      describe: 'Reset Mist tabs to their default settings.',
      requiresArg: false,
      nargs: 0,
      type: 'boolean',
      group: 'Mist options:'
    },
    loglevel: {
      demand: false,
      default: 'info',
      describe:
        'Minimum logging threshold: info, debug, error, trace (shows all logs, including possible passwords over IPC!).',
      requiresArg: true,
      nargs: 1,
      type: 'string',
      group: 'Mist options:'
    },
    syncmode: {
      demand: false,
      requiresArg: true,
      describe: 'Geth synchronization mode: [fast|light|full|nosync]',
      nargs: 1,
      type: 'string',
      group: 'Mist options:'
    },
    version: {
      alias: 'v',
      demand: false,
      requiresArg: false,
      nargs: 0,
      describe: 'Display Mist version.',
      group: 'Mist options:',
      type: 'boolean'
    },
    skiptimesynccheck: {
      demand: false,
      requiresArg: false,
      nargs: 0,
      describe:
        'Disable checks for the presence of automatic time sync on your OS.',
      group: 'Mist options:',
      type: 'boolean'
    },
    '': {
      describe:
        'To pass options to the underlying node (e.g. Geth) use the --node- prefix, e.g. --node-datadir',
      group: 'Node options:'
    }
  })
  .help('h')
  .alias('h', 'help')
  .parse(process.argv.slice(1));

argv.nodeOptions = [];

for (const optIdx in argv) {
  if (optIdx.indexOf('node-') === 0) {
    argv.nodeOptions.push(`--${optIdx.substr(5)}`);

    if (argv[optIdx] !== true) {
      argv.nodeOptions.push(argv[optIdx]);
    }
  }
}

// some options are shared
if (argv.ipcpath) {
  argv.nodeOptions.push('--ipcpath', argv.ipcpath);
}

if (argv.nodeOptions && argv.nodeOptions.syncmode) {
  argv.push('--syncmode', argv.nodeOptions.syncmode);
}