自從用 dotnet run 成功運行第一個 "Hello world" .NET Core 應用程序后,一直有個好奇心:dotnet run 究竟是如何運行一個 .NET Core 應用程序的?
在 從 ASP.NET 5 RC1 升級至 ASP.NET Core 1.0 與 在Linux上以本地機器碼運行 ASP.NET Core 站點 之后,這個好奇心被進一步激發,于是“探秘 dotnet run”順理成章地成為.NET跨平臺之旅的下一站。
首先我們了解一下 dotnet 命令是什么東東?dotnet 命令實際就是一個C#寫的簡單的.NET控制臺程序(詳見Program.cs),但為什么在不同操作系統平臺上安裝 dotnet cli 后,dotnet 命令是一個本地可執行文件?功臣依然是前一篇博文中見識過其威力的 .NET Native,dotnet 命令背后的.NET控制臺程序被編譯為針對不同操作系統的本地機器碼,dotnet 命令本身就是 .NET Native 的一個實際應用。
接下來,我們沿著 dotnet 命令的 Program.cs 探尋 dotnet run 運行 .NET Core 應用程序的秘密。
dotnet Program.cs 的C#代碼不超過200行,而與我們的探秘之旅最相關的是下面這一段代碼:
var builtIns = new Dictionary<string, Func<string[], int>> { //... ["run"] = RunCommand.Run, //... }; Func<string[], int> builtIn; if (builtIns.TryGetValue(command, out builtIn)) { return builtIn(appArgs.ToArray()); }
從上面的代碼可以看出,dotnet run 命令實際執行的是 RunCommand 的 Run() 方法,沿著 Run() 方法往前走,從 Start() 方法 來到 RunExecutable() 方法,此處的風景吸引了我們。
在 RunExecutable() 方法中,先執行了 BuildCommand.Run() 方法 —— 對 .NET Core 應用程序進行 build :
var result = Build.BuildCommand.Run(new[] { $"--framework", $"{_context.TargetFramework}", $"--configuration", Configuration, $"{_context.ProjectFile.ProjectDirectory}" });
如果 build 成功,會在 .NET Core 應用程序的bin文件夾中生成相應的程序集(.dll文件)。如何 build,不是我們這次旅程所關心的,我們關心的是 build 出來的程序集是如何被運行的。
所以略過此處風景,繼續向前,發現了下面的代碼:
result = Command.Create(outputName, _args)
.ForwardStdOut()
.ForwardStdErr()
.Execute()
.ExitCode;
從上面的代碼可以分析出,dotnet run 最終執行的是一個命令行,而這個命令行是由 Command.Create() 根據 outputName 生成的,outputName 就是 BuildCommand 生成的應用程序的程序集名稱。顯然,秘密一定藏在 Command.Create() 中。
目標 Command.Create() ,跑步前進 。。。在 Command.cs 中看到了 Command.Create() 的廬山真面目:
public static Command Create( string commandName, IEnumerable<string> args, NuGetFramework framework = null, string configuration = Constants.DefaultConfiguration) { var commandSpec = CommandResolver.TryResolveCommandSpec(commandName, args, framework, configuration: configuration); if (commandSpec == null) { throw new CommandUnknownException(commandName); } var command = new Command(commandSpec); return command; }
發現 CommandResolver,快步邁入 CommandResolver.TryResolveCommandSpec() ,看看其中又是怎樣的風景:
public static CommandSpec TryResolveCommandSpec( string commandName, IEnumerable<string> args, NuGetFramework framework = null, string configuration=Constants.DefaultConfiguration, string outputPath=null) { var commandResolverArgs = new CommandResolverArguments { CommandName = commandName, CommandArguments = args, Framework = framework, ProjectDirectory = Directory.GetCurrentDirectory(), Configuration = configuration, OutputPath = outputPath }; var defaultCommandResolver = DefaultCommandResolverPolicy.Create(); return defaultCommandResolver.Resolve(commandResolverArgs); }
發現 DefaultCommandResolverPolicy ,一個箭步,置身其 Create() 方法中:
public static CompositeCommandResolver Create() { var environment = new EnvironmentProvider(); var packagedCommandSpecFactory = new PackagedCommandSpecFactory(); var platformCommandSpecFactory = default(IPlatformCommandSpecFactory); if (PlatformServices.Default.Runtime.OperatingSystemPlatform == Platform.Windows) { platformCommandSpecFactory = new WindowsExePreferredCommandSpecFactory(); } else { platformCommandSpecFactory = new GenericPlatformCommandSpecFactory(); } return CreateDefaultCommandResolver(environment, packagedCommandSpecFactory, platformCommandSpecFactory); }
出現兩道風景 —— packagedCommandSpecFactory 與 platformCommandSpecFactory,它們都被作為參數傳給了 CreateDefaultCommandResolver() 方法。
一心不可二用,先看其中一道風景 —— packagedCommandSpecFactory ,急不可待地奔向 CreateDefaultCommandResolver() 方法 。
public static CompositeCommandResolver CreateDefaultCommandResolver( IEnvironmentProvider environment, IPackagedCommandSpecFactory packagedCommandSpecFactory, IPlatformCommandSpecFactory platformCommandSpecFactory) { var compositeCommandResolver = new CompositeCommandResolver(); //.. compositeCommandResolver.AddCommandResolver( new ProjectToolsCommandResolver(packagedCommandSpecFactory)); //.. return compositeCommandResolver; }
packagedCommandSpecFactory 將我們引向新的風景 —— ProjectToolsCommandResolver 。飛奔過去之后,立即被 Resolve() 方法吸引(在之前的 DefaultCommandResolverPolicy.Create() 執行之后,執行 defaultCommandResolver.Resolve(commandResolverArgs) 時,該方法被調用)。
這里的風景十八彎。在 ProjectToolsCommandResolver 中七繞八繞,從 ResolveFromProjectTools() -> ResolveCommandSpecFromAllToolLibraries() -> ResolveCommandSpecFromToolLibrary() 。。。又回到了 PackagedCommandSpecFactory ,進入 CreateCommandSpecFromLibrary() 方法。
在 PackagedCommandSpecFactory 中繼續轉悠,在從 CreateCommandSpecWrappingWithCorehostfDll() 到 CreatePackageCommandSpecUsingCorehost() 時,發現一個新東東從天而降 —— corehost :
private CommandSpec CreatePackageCommandSpecUsingCorehost( string commandPath, IEnumerable<string> commandArguments, string depsFilePath, CommandResolutionStrategy commandResolutionStrategy) { var corehost = CoreHost.HostExePath; var arguments = new List<string>(); arguments.Add(commandPath); if (depsFilePath != null) { arguments.Add($"--depsfile:{depsFilePath}"); } arguments.AddRange(commandArguments); return CreateCommandSpec(corehost, arguments, commandResolutionStrategy); }
這里的 corehost 變量是干嘛的?心中產生了一個大大的問號。
遙望 corehost 的身后,發現 CreateCommandSpec() 方法(corehost 是它的一個參數),一路狂奔過去:
private CommandSpec CreateCommandSpec( string commandPath, IEnumerable<string> commandArguments, CommandResolutionStrategy commandResolutionStrategy) { var escapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(commandArguments); return new CommandSpec(commandPath, escapedArgs, commandResolutionStrategy); }
原來 corehost 是作為 commandPath 的值,也就是說 Command.Create() 創建的 dotnet run 所對應的命令行是以 corehost 開頭的,秘密一定就在 corehost 的前方不遠處。
corehost 的值來自 CoreHost.HostExePath ,HostExePath 的值來自 Constants.HostExecutableName ,HostExecutableName 的值是:
public static readonly string HostExecutableName = "corehost" + ExeSuffix;
corehost 命令!原來 dotnet run 命令最終執行的 corehost 命令,corehost 才是背后真正的主角,.NET Core 應用程序是由它運行的。
去 dotnet cli 的安裝目錄一看,果然有一個 corehost 可執行文件。
-rwxr-xr-x 1 root root 31208 Mar 2 03:59 /usr/share/dotnet-nightly/bin/corehost
既然 corehost 是主角,那么不通過 dotnet run ,直接用 corehost 應該也可以運行 .NET Core 程序,我們來試一試。
進入示例站點 about.cnblogs.com 的 build 輸出文件夾:
cd /git/AboutUs/bin/Debug/netstandardapp1.3/ubuntu.14.04-x64
然后直接用 corehost 命令運行程序集:
/usr/share/dotnet-nightly/bin/corehost AboutUs.dll
運行成功!事實證明 corehost 是運行 .NET Core 程序的主角。
dbug: Microsoft.AspNetCore.Hosting.Internal.WebHost[3] Hosting starting dbug: Microsoft.AspNetCore.Hosting.Internal.WebHost[4] Hosting started Hosting environment: Production Application base path: /git/AboutUs/bin/Debug/netstandardapp1.3/ubuntu.14.04-x64 Now listening on: http://*:8001 Application started. Press Ctrl+C to shut down.
去 dotnet cli 的源代碼中看一下 corehost 的實現代碼,是用 C++ 寫的,這是 dotnet cli 中唯一用 C++ 實現的部分,它也不得不用 C++ ,因為它有一個重要職責 —— 加載 coreclr ,再次證實 corehost 是主角。
探秘 dotnet run , 踏破鐵鞋,走過千山萬水,終于找到了你 —— corehost,終于滿足了那顆不安分的好奇心。
文章列表
留言列表