vscode 源码学习4-vscode 启动前准备工作

1,394 阅读7分钟

set Current Working Directory

前面我们学习到,设置Crash目录,接着我们继续,开始设置:工作目录,即process.cwd

// src/utils/workingDirectory.ts
import { app } from 'electron';
import path from 'path';
import { PROCESS_ENV } from './constant';


const setCurrentWorkingDirectory = () => {
	try {
		if (process.platform === 'win32') {
			process.env[PROCESS_ENV.MY_VSCODE_CWD] = process.cwd(); // remember as environment variable
			process.chdir(path.dirname(app.getPath('exe'))); // always set application folder as cwd
		} else if (process.env[PROCESS_ENV.MY_VSCODE_CWD]) {
			process.chdir(process.env[PROCESS_ENV.MY_VSCODE_CWD] as string);
		}
	} catch (err) {
		console.error(err);
	}
}

export {
	setCurrentWorkingDirectory
}

// src/main.ts
...
setCurrentWorkingDirectory();

Protocol

接下来,vscode 设置了一些协议,我们去Electron官网看看,这个是什么东西!

protocol.registerSchemesAsPrivileged([
	{
		scheme: 'vscode-webview',
		privileges: {
			standard: true,
			secure: true,
			supportFetchAPI: true,
			corsEnabled: true,
		}
	}, {
		scheme: 'vscode-webview-resource',
		privileges: {
			secure: true,
			standard: true,
			supportFetchAPI: true,
			corsEnabled: true,
		}
	},
]);

这里有点不理解其作用,先不加,后续有需求在增加:TODO

全局监听

到现在,需要注册一些应用上的监听,这些监听好像与拖动文件到应用并打开有关,在这之前,监听文件拖动并存储,后续应用ready后进行打开,是一个小的优化点。

// src/main.ts
...
registerListeners();
// src/utils/globalListeners.ts

import { app } from 'electron';
import { GLOBAL_NAMES } from './constant';

const registerListeners = () => {
  /**
	 * macOS: when someone drops a file to the not-yet running VSCode, the open-file event fires even before
	 * the app-ready event. We listen very early for open-file and remember this upon startup as path to open.
	 *
	 * @type {string[]}
	 */
	const macOpenFiles: string[] = [];
	global['macOpenFiles'] = macOpenFiles;
	app.on('open-file', function (event: any, path: string) {
		macOpenFiles.push(path);
	});

	/**
	 * macOS: react to open-url requests.
	 *
	 * @type {string[]}
	 */
	const openUrls: string[] = [];
	const onOpenUrl = function (event, url: string) {
		event.preventDefault();
		openUrls.push(url);
	};

	app.on('will-finish-launching', function () {
		app.on('open-url', onOpenUrl);
	});

	global[GLOBAL_NAMES.getOpenUrls] = function () {
		app.removeListener('open-url', onOpenUrl);
		return openUrls;
	};

}

export {
  registerListeners
}


nodeCachedDataDir

这个东东比较复杂,暂时没理解其作用,记个 TODO

locale

/**
 * Support user defined locale: load it early before app('ready')
 * to have more things running in parallel.
 *
 * @type {Promise<import('./vs/base/node/languagePacks').NLSConfiguration> | undefined}
 */
let nlsConfigurationPromise = undefined;

const metaDataFile = path.join(__dirname, 'nls.metadata.json');
const locale = getUserDefinedLocale(argvConfig);
if (locale) {
	nlsConfigurationPromise = lp.getNLSConfiguration(product.commit, userDataPath, metaDataFile, locale);
}

暂时不增加,记个 TODO

contentTracing

终于到 Ready 环节了, 这里首先使用了 ElectroncontentTracing,我们来了解下是什么,有啥用,以及怎么用。

好像是个内容追踪的模块,我也不懂,看是 dev下调试使用的,那就先记个 TODO,后续搞懂。

startup

最终,我们跟踪到这里,要进行Electron启动了:

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

这是vscode使用自己的loader进行的加载,我们不懂作用是什么,记个 TODO 后续搞懂。

我们进入vs/code/electron-main/main.ts 文件,终于可以开始Electron启动了。

// vs/code/electron-main/main.ts
// Main Startup
const code = new CodeMain();
code.main();

首先,我们来解读main 函数,它有一个异常处理的统一的工具类, 并提供注册统一错误处理方法,这个机制,我们可以先统一集成,后续感受其带来的好处。

setUnexpectedErrorHandler(err => console.error(err));

先将工具类复制到我们的项目中src/vs/base/common/errors.ts

我们新建一个src/main-process.ts

// src/main-process.ts
import { setUnexpectedErrorHandler } from "./base/common/errors";

class CodeMain {
  main(): void {
    // Set the error handler early enough so that we are not getting the
		// default electron error dialog popping up
		setUnexpectedErrorHandler(err => console.error(err));
  }
}

export const startCodeMain = () => {
  const codeMain = new CodeMain();
  codeMain.main();
}
// src/main.ts
import { startCodeMain } from './main-process';
import { configureCommandlineSwitchesSync } from './utils/commandLineSwitches';
import { startCrash } from './utils/crash';
import { registerListeners } from './utils/globalListeners';
import { parseCLIArgs } from './utils/utils';
import { setCurrentWorkingDirectory } from './utils/workingDirectory';

const args = parseCLIArgs();

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

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

// 设置当前工作目录
setCurrentWorkingDirectory();

// 注册全局监听
registerListeners();

// 启动主进程
startCodeMain();

Parse arguments-01

在运行之前,统一处理一下启动时,带来的一些运行参数,这里我们需要一些辅助的工具类,就不再去解释了,主要是做一些参数的接收处理,便于我们使用,这里我就统一的直接拷贝过来。

  • src/platform/environment/common/argv.ts
  • src/platform/environment/node/argvHelper.ts

在拷贝argvHelper的时候,遇到了一些需要集成的东西:

  • localize 这个是处理语言的多种翻译。具体的细节后续详解,先集成进来,记个TODO

  • platform工具类,用于判断操作系统相关的东西,集成进来,记个 TODO

  • fies工具类,处理文件相关内容, 但此事一个大模块,此处先不详解也不集成,记个 TODO

    • 依赖的MIN_MAX_MEMORY_SIZE_MB常量先定义到constants.ts中。

我们在做参数准备工作的时候,却遇到了validatePaths 而这个看起来会是一个比较复杂的模块,咱们一点点来理解。

parse arguments 的事情暂时搁置,后续启动。

Paths

首先,我们在处理环境变量的时候,需要去根据变量的值去验证一些路径的合法性:

args = this.validatePaths(args);
	private validatePaths(args: NativeParsedArgs): NativeParsedArgs {
		// Track URLs if they're going to be used
    // TODO:这个逻辑不理解先放着
		if (args['open-url']) {
			args._urls = args._;
			args._ = [];
		}

		// Normalize paths and watch out for goto line mode
    // 这个是因为操作系统差异性,因此会需要去 normalize 路径,从而确保参数上路径的正确性。
		if (!args['remote']) {
      // TODO goto 是个 boolean 值,啥意思?
			const paths = this.doValidatePaths(args._, args.goto);
			args._ = paths;
		}

		return args;
	}

再来看doValidatePaths

export interface IPathWithLineAndColumn { 
	path: string;
	line?: number;
	column?: number;
}

private doValidatePaths(args: string[], gotoLineMode?: boolean): string[] {
		// 获取到所有参数
    const cwd = process.env['VSCODE_CWD'] || process.cwd();
		const result = args.map(arg => {
      // 候选路径
			let pathCandidate = String(arg);

      // 路径包含行号和列号
      // 是这个意思: C:\file.txt:<line>:<column> 某个文件的某行某列
			let parsedPath: IPathWithLineAndColumn | undefined = undefined;
			if (gotoLineMode) {
				parsedPath = parseLineAndColumnAware(pathCandidate);
				pathCandidate = parsedPath.path;
			}

			if (pathCandidate) {
				pathCandidate = this.preparePath(cwd, pathCandidate);
			}

			const sanitizedFilePath = sanitizeFilePath(pathCandidate, cwd);

			const filePathBasename = basename(sanitizedFilePath);
			if (filePathBasename /* can be empty if code is opened on root */ && !isValidBasename(filePathBasename)) {
				return null; // do not allow invalid file names
			}

			if (gotoLineMode && parsedPath) {
				parsedPath.path = sanitizedFilePath;

				return this.toPath(parsedPath);
			}

			return sanitizedFilePath;
		});

		const caseInsensitive = isWindows || isMacintosh;
		const distinctPaths = distinct(result, path => path && caseInsensitive ? path.toLowerCase() : (path || ''));

		return coalesce(distinctPaths);
	}

解读到这个函数parseLineAndColumnAware:我们来写个例子跑下,转换成了啥。

定义parseLineAndColumnAware

function parseLineAndColumnAware(rawPath) {
	const segments = rawPath.split(':'); // C:\file.txt:<line>:<column>

	let path = undefined;
	let line = undefined;
	let column = undefined;

	segments.forEach(segment => {
		const segmentAsNumber = Number(segment);
		if (!isNumber(segmentAsNumber)) {
      // 处理路径中带“:”的情况,防止错误识别。比如: "c:\",从而正确的去读取行号。
			path = !!path ? [path, segment].join(':') : segment; // a colon can well be part of a path (e.g. C:\...)
		} else if (line === undefined) {
			line = segmentAsNumber;
		} else if (column === undefined) {
			column = segmentAsNumber;
		}
	});

	if (!path) {
		throw new Error('Format for `--goto` should be: `FILE:LINE(:COLUMN)`');
	}

	return {
		path,
		line: line !== undefined ? line : undefined,
		column: column !== undefined ? column : line !== undefined ? 1 : undefined // if we have a line, make sure column is also set
	};
}

定义isNumber

function isNumber(obj) {
	return (typeof obj === 'number' && !isNaN(obj));
}

运行:

parseLineAndColumnAware('C:\file.txt:3:3');

result:
{
  column: 3
  line: 3
  path: "C:\file.txt"
}

所以大概理解其作用了。

接下来是preparePath 就是对字符串做一些处理。这里涉及到一个字符串处理工具类,直接复制过来好了。

  • string.ts 字符串处理工具类。
  • charCode.ts又被string.ts依赖,处理一些字符相关的。

后续有工具类的时候,不再解释,直接复制即可。

相关的工具类都复制完毕,这里主要是对参数中的路径做了一些处理,不再详细阐述了,可自行去看。

我们先把所有的路径都处理好。

这里,我们将主进程中,获取运行时的参数,并对其做统一的处理比如:路径处理等,封装成argsPathsUtil.ts

// src/utils/argsPathsUtil.ts
import { coalesce, distinct } from "../base/common/arrays";
import { IPathWithLineAndColumn, isValidBasename, parseLineAndColumnAware, sanitizeFilePath } from "../base/common/extpath";
import { basename, resolve } from "../base/common/path";
import { isMacintosh, isWindows } from "../base/common/platform";
import { rtrim, trim } from "../base/common/strings";
import { isNumber } from "../base/common/types";
import { NativeParsedArgs } from "../platform/environment/common/argv";
import { PROCESS_ENV } from "./constant";

const preparePath = (cwd: string, path: string): string => {

  // Trim trailing quotes
  if (isWindows) {
    path = rtrim(path, '"'); // https://github.com/microsoft/vscode/issues/1498
  }

  // Trim whitespaces
  path = trim(trim(path, ' '), '\t');

  if (isWindows) {

    // Resolve the path against cwd if it is relative
    path = resolve(cwd, path);

    // Trim trailing '.' chars on Windows to prevent invalid file names
    path = rtrim(path, '.');
  }

  return path;
}

}

const toPath = (pathWithLineAndCol: IPathWithLineAndColumn): string => {
  const segments = [pathWithLineAndCol.path];

  if (isNumber(pathWithLineAndCol.line)) {
    segments.push(String(pathWithLineAndCol.line));
  }

  if (isNumber(pathWithLineAndCol.column)) {
    segments.push(String(pathWithLineAndCol.column));
  }

  return segments.join(':');
}

const doValidatePaths = (args: string[], gotoLineMode?: boolean): string[] => {
  const cwd = process.env[PROCESS_ENV.MY_VSCODE_CWD] || process.cwd();
  const result = args.map(arg => {
    let pathCandidate = String(arg);

    let parsedPath: IPathWithLineAndColumn | undefined = undefined;
    if (gotoLineMode) {
      parsedPath = parseLineAndColumnAware(pathCandidate);
      pathCandidate = parsedPath.path;
    }

    if (pathCandidate) {
      pathCandidate = preparePath(cwd, pathCandidate);
    }

    const sanitizedFilePath = sanitizeFilePath(pathCandidate, cwd);

    const filePathBasename = basename(sanitizedFilePath);
    if (filePathBasename /* can be empty if code is opened on root */ && !isValidBasename(filePathBasename)) {
      return null; // do not allow invalid file names
    }

    if (gotoLineMode && parsedPath) {
      parsedPath.path = sanitizedFilePath;

      return toPath(parsedPath);
    }

    return sanitizedFilePath;
  });

  const caseInsensitive = isWindows || isMacintosh;
  const distinctPaths = distinct(result, path => path && caseInsensitive ? path.toLowerCase() : (path || ''));

  return coalesce(distinctPaths);
}

export const validatePaths = (args: NativeParsedArgs): NativeParsedArgs => {
  	// Track URLs if they're going to be used
		if (args['open-url']) {
			args._urls = args._;
			args._ = [];
		}

		// Normalize paths and watch out for goto line mode
		if (!args['remote']) {
			const paths = doValidatePaths(args._, args.goto);
			args._ = paths;
		}

		return args;
}

最后回到main.ts函数:

class CodeMain {
  main(): void {
    // Set the error handler early enough so that we are not getting the
		// default electron error dialog popping up
    setUnexpectedErrorHandler(err => console.error(err));
    // 处理主进程运行时参数 包含路径处理
    const args: NativeParsedArgs = parseMainAgrs();
  }
}

这样处理后,就清爽很多了,确保做到:每段代码只展示当时读者需要看的内容,不展示细节!

waitMarkerFile

// If we are started with --wait create a random temporary file
// and pass it over to the starting instance. We can use this file
// to wait for it to be deleted to monitor that the edited file
// is closed and then exit the waiting process.
//
// Note: we are not doing this if the wait marker has been already
// added as argument. This can happen if Code was started from CLI.
if (args.wait && !args.waitMarkerFilePath) {
  const waitMarkerFilePath = createWaitMarkerFile(args.verbose);
  if (waitMarkerFilePath) {
    addArg(process.argv, '--waitMarkerFilePath', waitMarkerFilePath);
    args.waitMarkerFilePath = waitMarkerFilePath;
  }
}

这里在说等待waitMarkerFilePath ,先记个TODO 不影响主流程。

当下的解读,很多比较细节的地方会记下TODO, 后续回顾的时候一一了解。

StartUp

接着,我们进入最重要的环节: start up 。需要看懂这里,就需要明白 IOC 机制,这也是我们的重点。