vscode源码学习3:从零到一实现 Vscode-开启`CrashReporter`

985 阅读5分钟

我们在学习 vscode 的源码中,也是在探寻,如何去写一个非常复杂且性能极佳的应用,也是在探究 vscode 非常优秀的原因。

现在,我们要基于自己的学习,来实现一个自己的 vscode。

首先,来搭建一个空的项目,并开始第一步。

我们可能不会每一步都按照 vscode 去实现,那就是抄了,我们是需要根据自己的理解去实现,懂的地方,按照自己的思路去写,不懂的地方,则标记出来,不知道其作用,或许在后续的研究中,你可能就知道是因为什么了。

搭建完项目后,我们在进行集成时候,发现 vscode 的入口文件是 js 的,并用了一种很奇怪的方式进行加载。

/**
 * Main startup routine
 *
 * @param {string | undefined} cachedDataDir
 * @param {import('./vs/base/node/languagePacks').NLSConfiguration} nlsConfig
 */
function startup(cachedDataDir, nlsConfig) {
	nlsConfig._languagePackSupport = true;

	process.env['VSCODE_NLS_CONFIG'] = JSON.stringify(nlsConfig);
	process.env['VSCODE_NODE_CACHED_DATA_DIR'] = cachedDataDir || '';

	// Load main in AMD
	perf.mark('willLoadMainBundle');
	require('./bootstrap-amd').load('vs/code/electron-main/main', () => {
		perf.mark('didLoadMainBundle');
	});
}

所以到这里,我们发现这些问题:

  • 为什么它的源码都是 ts 的而入口文件却是 js。
  • 为什么载入文件需要如此异常, 似乎用自己的 loader 加载了页面:
require('./bootstrap-amd').load('vs/code/electron-main/main', () => {
perf.mark('didLoadMainBundle');
});

基于以上疑问,我们不懂,先记录。

我会首先在我的项目里,把 ts 集成进来, 以前写过,就照抄先集成一波

接下来,我们安装 electron。

yarn add electron@latest -D --registry=https://registry.npm.taobao.org/

到此,基本的项目搭建完毕了,我们就从主进程入口来继续学习吧。

禁用渲染进程重复使用

// Disable render process reuse, we still have
// non-context aware native modules in the renderer.
app.allowRendererProcessReuse = false;

这里这个点不是很明白在 Electron 中的作用,先记录,后续读到后再加上。

异步启动 bootstrap

接着 vscode 使用了 bootstrap , 这个模块跟vscode 自身的异步启动相关。

const portable = bootstrap.configurePortable(product);

这块,我们不管他,因为不明白它异步加载的优势,我们记录下这个问题,后续回过头来在看。

设置一些用户数据

同上,记录问题,不作处理。

const args = parseCLIArgs();
const userDataPath = getUserDataPath(args);
app.setPath('userData', userDataPath);

// Configure static command line arguments
const argvConfig = configureCommandlineSwitchesSync(args);

存储 crashed 目录设置

这个看起来是应用需要的,我们在自己应用中实现此功能。这里使用了minimist这个包用于解析参数,我们来操作一波。

yarn add minimist

我们来写个简单测试,测试下这块功能:

// a.js
function parseCLIArgs() {
	const minimist = require('minimist');

	return minimist(process.argv, {
		string: [
			'user-data-dir',
			'locale',
			'js-flags',
			'max-memory',
			'crash-reporter-directory'
		]
	});
}
console.log('parseCLIArgs:', parseCLIArgs()['crash-reporter-directory']);

运行下这个脚本:

my-vscode git:(master) ✗ node a.js --crash-reporter-directory=ccc
parseCLIArgs: ccc

可以看到,其实就是辅助获取一些运行参数,比如输入了:--crash-reporter-directory=ccc即可拿到参数:parseCLIArgs()['crash-reporter-directory']

Ok,我们集成进去,这个 crash 冲突文件夹设置,看起来是辅助性的东西,我们放到utils下去,保持入口文件的干净整洁。

// src/utils/utils.ts
import minimist from 'minimist';

const parseCLIArgs = () => {
  return minimist(process.argv, {
		string: [
			'user-data-dir',
			'locale',
			'js-flags',
			'max-memory',
			'crash-reporter-directory'
		]
	});
}
export {
  parseCLIArgs
}

为了方便存在的变量管理,我们新建一个常量

// src/utils/constants.ts
const ARGS_NAME = {
  crashReporterDirectory: 'crash-reporter-directory'
}

export {
  ARGS_NAME
}

继续完成 crash目录设置

import path from 'path';
import fs from 'fs';
import { app } from 'electron';
import { ARGS_NAME } from './constant';

const setCrashFolder = (args: any) => {
  let crashReporterDirectory = args[ARGS_NAME.crashReporterDirectory];
  if (crashReporterDirectory) {
    // 存在变量,则进行设置
    crashReporterDirectory = path.normalize(crashReporterDirectory);

    if (!path.isAbsolute(crashReporterDirectory)) {
      console.error(`The path '${crashReporterDirectory}' specified for --crash-reporter-directory must be absolute.`);
      app.exit(1);
    }

    if (!fs.existsSync(crashReporterDirectory)) {
      try {
        fs.mkdirSync(crashReporterDirectory);
      } catch (error) {
        console.error(`The path '${crashReporterDirectory}' specified for --crash-reporter-directory does not seem to exist or cannot be created.`);
        app.exit(1);
      }
    }

    // Crashes are stored in the crashDumps directory by default, so we
    // need to change that directory to the provided one
    console.log(`Found --crash-reporter-directory argument. Setting crashDumps directory to be '${crashReporterDirectory}'`);
    app.setPath('crashDumps', crashReporterDirectory);
  } else {
		// TODO
  }
}

到这里,主要就是如果在运行时设置了运行变量,则设置自己的 crash存放位置,如果不存在,则需要做一些事情:

这里,就遇到了我们原先跳过的内容:configureCommandlineSwitchesSync

既然遇到了,那我们就看看做了哪些事情,对我们来说有没有用:

首先是一些应用能力的支持的开关,这个我们倒是可以先设置着:

const getSwitches = () => {
  const SUPPORTED_ELECTRON_SWITCHES = [
		// alias from us for --disable-gpu
		'disable-hardware-acceleration',

		// provided by Electron
		'disable-color-correct-rendering',

		// override for the color profile to use
		'force-color-profile'
	];

	if (process.platform === 'linux') {

		// Force enable screen readers on Linux via this flag
		SUPPORTED_ELECTRON_SWITCHES.push('force-renderer-accessibility');
	}

	const SUPPORTED_MAIN_PROCESS_SWITCHES = [

		// Persistently enable proposed api via argv.json: https://github.com/microsoft/vscode/issues/99775
		'enable-proposed-api'
  ];
  return {
    electronSwitches: SUPPORTED_ELECTRON_SWITCHES,
    mainSwitches: SUPPORTED_MAIN_PROCESS_SWITCHES
  }
}
const configureCommandlineSwitchesSync = (cliArgs: NativeParsedArgs) => {
  const switches = getSwitches();
  ...
}

前面,我们有解读到,增加运行参数:

node a.js --crash-reporter-directory=ccc

而在这里呢,他们是将运行参数放到:argv.json 中的,并且在刚才上述过程中会去读配置文件,因此,我们现在需要设置下argv.json的文件路径。

import path from 'path';
import product from '../../production.json';
import os from 'os';
const getArgvConfigPath = () => {
  let dataFolderName = product.dataFolderName;
  // dev 的时候我们增加一个 dev 后缀
	if (process.env['VSCODE_DEV']) {
		dataFolderName = `${dataFolderName}-dev`;
	}
	return path.join(os.homedir(), dataFolderName, 'argv.json');
}
export {
  getArgvConfigPath
}

上面例子里涉及到了环境变量,因此,我们也定义一个常量去做环境变量的管理。

//src/utils/constant.ts

const PROCESS_ENV = {
  MY_VSCODE_DEV: 'MY_VSCODE_DEV'
}
...
...
if (process.env[PROCESS_ENV.MY_VSCODE_DEV]) {
  dataFolderName = `${dataFolderName}-dev`;
}

过程中,他也定义了一个 prodction.json 文件来描述产品的一些信息,其中就包含dataFolderName这个也是在这里需要的,我们也新建一个prodction.json

// production.json
{
  "dataFolderName": ".my-vscode"
}

接下来,我们来写读取变量配置文件:

const readArgvConfigSync = () => {
	// Read or create the argv.json config file sync before app('ready')
	const argvConfigPath = getArgvConfigPath();
	let argvConfig;
	try {
		argvConfig = JSON.parse(stripComments(fs.readFileSync(argvConfigPath).toString()));
	} catch (error) {
    console.warn(`
			Unable to read argv.json configuration file in ${argvConfigPath}, falling back to defaults (${error})`
    );
	}
	// Fallback to default
	if (!argvConfig) {
		argvConfig = {
      // Force pre-Chrome-60 color profile handling (for https://github.com/microsoft/vscode/issues/51791)
			'disable-color-correct-rendering': true 
		};
	}
	return argvConfig;
}
  • 配置文件存在则读取,否则就创建一默认的配置,但我们这里可以先简单处理,没有就没有: TODO

这里,有个这个函数:stripComments,看起来是在文件的读取后,做一些格式的处理,我们就加上吧。

为什么 json 文件不直接去读取,而是用读文件的形式呢?留个疑问🤔️

const stripComments = (content) => {
	const regexp = /("(?:[^\\"]*(?:\\.)?)*")|('(?:[^\\']*(?:\\.)?)*')|(\/\*(?:\r?\n|.)*?\*\/)|(\/{2,}.*?(?:(?:\r?\n)|$))/g;
	return content.replace(regexp, function (match, m1, m2, m3, m4) {
		// Only one of m1, m2, m3, m4 matches
		if (m3) {
			// A block comment. Replace with nothing
			return '';
		} else if (m4) {
			// A line comment. If it ends in \r?\n then keep it.
			const length_1 = m4.length;
			if (length_1 > 2 && m4[length_1 - 1] === '\n') {
				return m4[length_1 - 2] === '\r' ? '\r\n' : '\n';
			}
			else {
				return '';
			}
		} else {
			// We match a string
			return match;
		}
	});
}

绕了一圈,我们又回来啦,继续配置: commandline switches 吧。

import { readArgvConfigSync } from './argv';
...
const configureCommandlineSwitchesSync = (cliArgs: NativeParsedArgs) => {
  const switches = getSwitches();
  const argvConfig = readArgvConfigSync();
}

接下来,就是根据argv.json中配置的开关选项,来决定是否打开这些能力。为了结构清晰,我们简单调整下:

import { app } from 'electron';
const setSwitches = (supportElectronSwitches: any) => {
  const argvConfig = readArgvConfigSync();
  Object.keys(argvConfig).forEach(argvKey => {
		const argvValue = argvConfig[argvKey];

		// Append Electron flags to Electron
		if (supportElectronSwitches.indexOf(argvKey) !== -1) {

			// Color profile
			if (argvKey === 'force-color-profile') {
				if (argvValue) {
					app.commandLine.appendSwitch(argvKey, argvValue);
				}
			}

			// Others
			else if (argvValue === true || argvValue === 'true') {
				if (argvKey === 'disable-hardware-acceleration') {
					app.disableHardwareAcceleration(); // needs to be called explicitly
				} else {
					app.commandLine.appendSwitch(argvKey);
				}
			}
		}

		// Append main process flags to process.argv
		else if (supportElectronSwitches.indexOf(argvKey) !== -1) {
			if (argvKey === 'enable-proposed-api') {
				if (Array.isArray(argvValue)) {
					argvValue.forEach(id => id && typeof id === 'string' && process.argv.push('--enable-proposed-api', id));
				} else {
					console.error(`Unexpected value for \`enable-proposed-api\` in argv.json. Expected array of extension ids.`);
				}
			}
		}
	});
}

const configureCommandlineSwitchesSync = (cliArgs: NativeParsedArgs) => {
  const switches = getSwitches();
  setSwitches(switches.electronSwitches);
  ...
}

接下来,就是配置了一些 js flags,比如: max_old_space_size啥的,感觉这个东西还有点用,咱们也搞进来吧。

// src/utils/commandLineSwitches.ts
const getJSFlags = (cliArgs: NativeParsedArgs) => {
	const jsFlags = [];

	// Add any existing JS flags we already got from the command line
	if (cliArgs['js-flags']) {
		jsFlags.push(cliArgs['js-flags']);
	}

	// Support max-memory flag
	if (cliArgs['max-memory'] && !/max_old_space_size=(\d+)/g.exec(cliArgs['js-flags'])) {
		jsFlags.push(`--max_old_space_size=${cliArgs['max-memory']}`);
	}

	return jsFlags.length > 0 ? jsFlags.join(' ') : null;
}

const configureCommandlineSwitchesSync = (cliArgs: NativeParsedArgs) => {
  const switches = getSwitches();
  setSwitches(switches.electronSwitches);
  // Support JS Flags
	const jsFlags = getJSFlags(cliArgs);
	if (jsFlags) {
		app.commandLine.appendSwitch('js-flags', jsFlags);
	}

	return argvConfig;
}

到此,我们就完成了configureCommandlineSwitchesSync ,唉!没想到要搞好一个 Electron要考虑这么多事情,到现在都还没开始写内容,还在做准备工作。

还记得在configureCommandlineSwitchesSync在之前,我们其实是在设置crash目录,我们继续工作吧,

不过看起来,下面的逻辑都是基于存在appCenter这个东西的,我看vscode并没有配置,我们也没有使用到,那算了吧,记个TODO。

const appCenter = product.appCenter;
	// Disable Appcenter crash reporting if
	// * --crash-reporter-directory is specified
	// * enable-crash-reporter runtime argument is set to 'false'
	// * --disable-crash-reporter command line parameter is set
if (appCenter && argvConfig['enable-crash-reporter'] && !args['disable-crash-reporter']) {
  ...
}

好,设置冲突文件夹的逻辑,我们完成了!

import { configureCommandlineSwitchesSync } from './utils/commandLineSwitches';
import { setCrashFolder } from './utils/crash';
import { parseCLIArgs } from './utils/utils';

const args = parseCLIArgs();
// argv 参数配置
const argvConfig = configureCommandlineSwitchesSync(args);
setCrashFolder(args, argvConfig);

接下来,好像设置了crashRepoter相关信息,这个值得学习和使用, 我们在production.json中增加一些产品信息吧:

{
  "nameShort": "my-vscode",
  "companyName": "pk"
}

我们读取production.json需要开启下json支持。

{
  "compilerOptions": {
     "strictNullChecks": true,
     "module": "commonjs",
     "esModuleInterop": true,
     "importHelpers": true,
     "allowSyntheticDefaultImports": true,
     "target": "es5",
     "resolveJsonModule": true,
     "lib": [
      "es2015"
     ]
  },
  "files": [
    "**/*.json"
  ],
  "include": [
      "src" // write ts in src, you can define your own dir
  ]
}

这里,我们启动一下crashReporter, 在这里,我们好像遇到了上报crashReporter路径设置问题,好像跟刚才提及的appCenter相关,我们是想要这样的配置的,那我们来添加一下这样的功能吧。

// production.json
{
  "dataFolderName": ".my-vscode",
  "nameShort": "my-vscode",
  "companyName": "pk",
  "appCenter": {
    "win32-ia32": "",
    "win32-x64": "",
    "linux-x64": "",
    "drawn": ""
  }
}

我们先配上,然后空着,后续使用的时候增加对应的Crash报警URL。

这样,我们在设置Crash文件夹那加上这些逻辑,最后主进程逻辑:

// src/main.ts
import { configureCommandlineSwitchesSync } from './utils/commandLineSwitches';
import { startCrash } from './utils/crash';
import { parseCLIArgs } from './utils/utils';

const args = parseCLIArgs();

// argv 参数配置
const argvConfig = configureCommandlineSwitchesSync(args);

// 设置 crasher
startCrash(args, argvConfig);

// src/utils/crash.ts
import path from 'path';
import fs from 'fs';
import { app, crashReporter } from 'electron';
import product from '../../production.json';
import { ARGS_NAME } from './constant';

const setCrashFolder = (args: any, argvConfig: any): {
  submitURL: string;
  crashReporterDirectory: string
} => {
  let submitURL = '';
  let crashReporterDirectory = args[ARGS_NAME.crashReporterDirectory];
  if (crashReporterDirectory) {
    // 存在变量,则进行设置
    crashReporterDirectory = path.normalize(crashReporterDirectory);

    if (!path.isAbsolute(crashReporterDirectory)) {
      console.error(`The path '${crashReporterDirectory}' specified for --crash-reporter-directory must be absolute.`);
      app.exit(1);
    }

    if (!fs.existsSync(crashReporterDirectory)) {
      try {
        fs.mkdirSync(crashReporterDirectory);
      } catch (error) {
        console.error(`The path '${crashReporterDirectory}' specified for --crash-reporter-directory does not seem to exist or cannot be created.`);
        app.exit(1);
      }
    }

    // Crashes are stored in the crashDumps directory by default, so we
    // need to change that directory to the provided one
    console.log(`Found --crash-reporter-directory argument. Setting crashDumps directory to be '${crashReporterDirectory}'`);
    app.setPath('crashDumps', crashReporterDirectory);
  } else {
	  const appCenter = product.appCenter;
	// Disable Appcenter crash reporting if
	// * --crash-reporter-directory is specified
	// * enable-crash-reporter runtime argument is set to 'false'
	// * --disable-crash-reporter command line parameter is set
	if (appCenter && argvConfig[ARGS_NAME.enableCrashReporter] && !args[ARGS_NAME.disableCrashReporter]) {
		const isWindows = (process.platform === 'win32');
		const isLinux = (process.platform === 'linux');
		const crashReporterId = argvConfig[ARGS_NAME.crashReporterId];
		const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
		if (uuidPattern.test(crashReporterId)) {
			submitURL = isWindows ? appCenter[process.arch === 'ia32' ? 'win32-ia32' : 'win32-x64'] : isLinux ? appCenter[`linux-x64`] : appCenter.darwin;
			submitURL = submitURL.concat('&uid=', crashReporterId, '&iid=', crashReporterId, '&sid=', crashReporterId);
			// Send the id for child node process that are explicitly starting crash reporter.
			// For vscode this is ExtensionHost process currently.
			const argv = process.argv;
			const endOfArgsMarkerIndex = argv.indexOf('--');
			if (endOfArgsMarkerIndex === -1) {
				argv.push('--crash-reporter-id', crashReporterId);
			} else {
				// if the we have an argument "--" (end of argument marker)
				// we cannot add arguments at the end. rather, we add
				// arguments before the "--" marker.
				argv.splice(endOfArgsMarkerIndex, 0, '--crash-reporter-id', crashReporterId);
			}
		}
	}
  }
  return {
    submitURL,
    crashReporterDirectory
  }
}

const startCrash = (args: any, argvConfig: any) => {
  const productName = product.nameShort;
  const companyName = product.companyName;
  const result = setCrashFolder(args, argvConfig);
  crashReporter.start({
    companyName: companyName,
    productName: process.env['VSCODE_DEV'] ? `${productName} Dev` : productName,
    submitURL: result.submitURL,
    uploadToServer: !result.crashReporterDirectory
  });
}

export {
  startCrash
}

今天先到这里吧,明天继续加油!!