<commit_before>// Copyright (c) 2015, <NAME>. All rights reserved. Use of this source code
// is governed by a BSD-style license that can be found in the LICENSE file.

/// Exposes the node process APIs.
library node.process;

import 'dart:async';

import 'package:logging/logging.dart';

import '../atom.dart';
import '../src/js.dart';
import 'node.dart';

final Process process = new Process._();

final bool isWindows = process.platform.startsWith('win');
final bool isMac = process.platform == 'darwin';
final bool isLinux = !isWindows && !isMac;

final Logger _logger = new Logger('process');

class Process extends ProxyHolder {
  Process._() : super(require('process'));

  /// 'darwin', 'freebsd', 'linux', 'sunos' or 'win32'.
  String get platform => obj['platform'];

  String get chromeVersion => obj['versions']['chrome'];

  /// Get the value of an environment variable. This is often not accurate on
  /// the mac since mac apps are launched in a different shell then the terminal
  /// default.
  String env(String key) {
    try {
      return obj['env'][key];
    } catch (err) {
      return null;
    }
  }
}

Future<String> exec(String cmd, [List<String> args]) {
  ProcessRunner runner = new ProcessRunner(cmd, args: args);
  return runner.execSimple().then((ProcessResult result) {
    if (result.exit == 0) return result.stdout;
    throw result.exit;
  }) as Future<String>;
}

class ProcessRunner {
  final String command;
  final List<String> args;
  final String cwd;
  final Map<String, String> env;

  BufferedProcess _process;
  Completer<int> _exitCompleter = new Completer();
  int _exit;

  StreamController<String> _stdoutController = new StreamController();
  StreamController<String> _stderrController = new StreamController();

  ProcessRunner(this.command, {this.args, this.cwd, this.env});

  /// Execute the command under the user's preferred shell. On the Mac, this
  /// will determine the shell from the `$SHELL` env variable. On other platforms,
  /// this will call through to the normal [ProcessRunner] constructor.
  factory ProcessRunner.underShell(String command, {
    List<String> args, String cwd, Map<String, String> env
  }) {
    if (isMac) {
      // This shouldn't be trusted for security.
      final RegExp shellEscape = new RegExp('(["\'| \\\$!\\(\\)\\[\\]])');

      String shell = process.env('SHELL');

      if (shell == null) {
        _logger.warning("Couldn't identify the user's shell");
        shell = '/bin/bash';
      }

      if (args != null) {
        // Escape the arguments.
        Iterable escaped = args.map((String arg) {
          return "'${arg.replaceAllMapped(shellEscape, (Match m) => '\\' + m.group(0))}'";
        });
        command += ' ' + (escaped.join(' '));
      }

      args = [];

      // bash, zsh, tcsh, csh
      if (shell != 'csh' && shell != 'tcsh') args.add('-l');
      args.addAll(['-c', command]);

      return new ProcessRunner(shell, args: args, cwd: cwd, env: env);
    } else {
      return new ProcessRunner(command, args: args, cwd: cwd, env: env);
    }
  }

  bool get started => _process != null;
  bool get finished => _exit != null;

  int get exit => _exit;

  Future<int> get onExit => _exitCompleter.future;

  Stream<String> get onStdout => _stdoutController.stream;
  Stream<String> get onStderr => _stderrController.stream;

  Future<ProcessResult> execSimple() {
    if (_process != null) throw new StateError('exec can only be called once');

    StringBuffer stdout = new StringBuffer();
    StringBuffer stderr = new StringBuffer();

    onStdout.listen((str) => stdout.write(str));
    onStderr.listen((str) => stderr.write(str));

    return execStreaming().then((code) {
      return new ProcessResult(code, stdout.toString(), stderr.toString());
    }) as Future<ProcessResult>;
  }

  Future<int> execStreaming() {
    if (_process != null) throw new StateError('exec can only be called once');

    _logger.fine('exec: ${command} ${args == null ? "" : args.join(" ")}'
        '${cwd == null ? "" : " (cwd=${cwd})"}');

    try {
      _process = BufferedProcess.create(command, args: args, cwd: cwd, env: env,
          stdout: (s) => _stdoutController.add(s),
          stderr: (s) => _stderrController.add(s),
          exit: (code) {
            _logger.fine('exit code: ${code} (${command})');
            _exit = code;
            if (!_exitCompleter.isCompleted) _exitCompleter.complete(code);
          },
          onWillThrowError: (e) {
            if (!_exitCompleter.isCompleted) _exitCompleter.completeError(e);
          }
      );
    } catch (e) {
      // TODO: We don't seem to be able to catch some JS exceptions.
      return new Future.error(e);
    }

    return _exitCompleter.future;
  }

  void write(String str) => _process.write(str);

  Future<int> kill() {
    // _logger.fine('kill: ${command} ');
    if (_process != null) _process.kill();
    new Future.delayed(new Duration(milliseconds: 50), () {
      if (!_exitCompleter.isCompleted) _exitCompleter.complete(0);
    });
    return _exitCompleter.future;
  }

  String getDescription() {
    if (args != null) {
      return '${command} ${args.join(' ')}';
    } else {
      return command;
    }
  }
}

class ProcessResult {
  final int exit;
  final String stdout;
  final String stderr;

  ProcessResult(this.exit, this.stdout, this.stderr);

  String toString() => '${exit}';
}<commit_msg>improve our handling of situations where we don't have full env variables<commit_after>// Copyright (c) 2015, <NAME>. All rights reserved. Use of this source code
// is governed by a BSD-style license that can be found in the LICENSE file.

/// Exposes the node process APIs.
library node.process;

import 'dart:async';

import 'package:logging/logging.dart';

import '../atom.dart';
import '../src/js.dart';
import 'node.dart';

final Process process = new Process._();

final bool isWindows = process.platform.startsWith('win');
final bool isMac = process.platform == 'darwin';
final bool isLinux = !isWindows && !isMac;

final Logger _logger = new Logger('process');

class Process extends ProxyHolder {
  Process._() : super(require('process'));

  /// 'darwin', 'freebsd', 'linux', 'sunos' or 'win32'.
  String get platform => obj['platform'];

  String get chromeVersion => obj['versions']['chrome'];

  /// Get the value of an environment variable. This is often not accurate on
  /// the mac since mac apps are launched in a different shell then the terminal
  /// default.
  String env(String key) {
    try {
      return obj['env'][key];
    } catch (err) {
      return null;
    }
  }
}

Future<String> exec(String command, [List<String> args]) {
  ProcessRunner runner = new ProcessRunner(command, args: args);
  return runner.execSimple().then((ProcessResult result) {
    if (result.exit == 0) return result.stdout;
    throw result.exit;
  }) as Future<String>;
}

/// Execute the given command synchronously and return the stdout. If the
/// process has a non-zero exit code, this method will throw.
String execSync(String command) {
  try {
    String result = require('child_process').callMethod('execSync', [command]);
    if (result == null) return null;
    result = '$result'.trim();
    return result.isEmpty ? null : result;
  } catch (error) {
    // https://nodejs.org/api/child_process.html#child_process_child_process_spawnsync_command_args_options
    throw '$error';
  }
}

class ProcessRunner {
  static _MacShellWrangler _shellWrangler;

  final String command;
  final List<String> args;
  final String cwd;
  final Map<String, String> env;

  BufferedProcess _process;
  Completer<int> _exitCompleter = new Completer();
  int _exit;

  StreamController<String> _stdoutController = new StreamController();
  StreamController<String> _stderrController = new StreamController();

  ProcessRunner(this.command, {this.args, this.cwd, this.env});

  /// Execute the command using the environment of the user's preferred shell.
  /// On the Mac, this will determine the shell from the `$SHELL` env variable.
  /// On other platforms, this will call through to the normal [ProcessRunner]
  /// constructor.
  factory ProcessRunner.underShell(String command, {
    List<String> args, String cwd, Map<String, String> env
  }) {
    if (isMac && _shellWrangler == null) {
      _shellWrangler = new _MacShellWrangler();
    }

    if (isMac && _shellWrangler.isNecessary) {
      return new ProcessRunner(command, args: args, cwd: cwd, env: _shellWrangler.env);
    }

    return new ProcessRunner(command, args: args, cwd: cwd, env: env);
  }

  bool get started => _process != null;
  bool get finished => _exit != null;

  int get exit => _exit;

  Future<int> get onExit => _exitCompleter.future;

  Stream<String> get onStdout => _stdoutController.stream;
  Stream<String> get onStderr => _stderrController.stream;

  Future<ProcessResult> execSimple() {
    if (_process != null) throw new StateError('exec can only be called once');

    StringBuffer stdout = new StringBuffer();
    StringBuffer stderr = new StringBuffer();

    onStdout.listen((str) => stdout.write(str));
    onStderr.listen((str) => stderr.write(str));

    return execStreaming().then((code) {
      return new ProcessResult(code, stdout.toString(), stderr.toString());
    }) as Future<ProcessResult>;
  }

  Future<int> execStreaming() {
    if (_process != null) throw new StateError('exec can only be called once');

    _logger.fine('exec: ${command} ${args == null ? "" : args.join(" ")}'
        '${cwd == null ? "" : " (cwd=${cwd})"}');

    try {
      _process = BufferedProcess.create(command, args: args, cwd: cwd, env: env,
          stdout: (s) => _stdoutController.add(s),
          stderr: (s) => _stderrController.add(s),
          exit: (code) {
            _logger.fine('exit code: ${code} (${command})');
            _exit = code;
            if (!_exitCompleter.isCompleted) _exitCompleter.complete(code);
          },
          onWillThrowError: (e) {
            if (!_exitCompleter.isCompleted) _exitCompleter.completeError(e);
          }
      );
    } catch (e) {
      // TODO: We don't seem to be able to catch some JS exceptions.
      return new Future.error(e);
    }

    return _exitCompleter.future;
  }

  void write(String str) => _process.write(str);

  Future<int> kill() {
    if (_process != null) _process.kill();
    new Future.delayed(new Duration(milliseconds: 50), () {
      if (!_exitCompleter.isCompleted) _exitCompleter.complete(0);
    });
    return _exitCompleter.future;
  }

  String getDescription() {
    if (args != null) {
      return '${command} ${args.join(' ')}';
    } else {
      return command;
    }
  }
}

class ProcessResult {
  final int exit;
  final String stdout;
  final String stderr;

  ProcessResult(this.exit, this.stdout, this.stderr);

  String toString() => '${exit}';
}

/// Shell out to query the given environment variable. This is slower but more
/// reliable than the node env map.
String queryEnv(String variable) {
  try {
    return execSync('echo \$$variable');
  } catch (e) {
    return null;
  }
}

/// This class exists to help manage situations where Atom is running in an
/// environment without a properly set up environment (env and PATH variables).
class _MacShellWrangler {
  String _currentShell;
  String _targetShell;
  Map<String, String> _env = {};

  _MacShellWrangler() {
    _currentShell = execSync(r'echo $0');
    _targetShell = execSync(r'echo $SHELL');

    if (isNecessary) {
      String result;
      if (_targetShell.endsWith('/csh') || _targetShell.endsWith('/tcsh')) {
        // csh and tcsh don't support -l
        result = execSync("$_targetShell -c 'printenv'");
      } else {
        result = execSync("$_targetShell -l -c 'printenv'");
      }

      for (String line in result.split('\n')) {
        int index = line.indexOf('=');
        if (index != -1) {
          _env[line.substring(0, index)] = line.substring(index + 1);
        }
      }
    }
  }

  bool get isNecessary => _currentShell == '/bin/sh';

  String get targetShell => _targetShell;

  String getEnv(String variable) => _env[variable];

  Map<String, String> get env => _env;

  String toString() => '$_currentShell $_targetShell $_env';
}
{'fraction_non_alphanumeric': 0.14059696623715545, 'fraction_numerical': 0.0020388191159680315, 'mean_word_length': 3.2010962658444675, 'pattern_counts': {'":': 0, '<': 41, '<?xml version=': 0, '>': 75, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, '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': '76307', 'n_tokens_mistral': 3817, 'n_tokens_neox': 3706, 'n_words': 1108}