天天看點

.NET跨平台之旅:探秘 dotnet run 如何運作 .NET Core 應用程式

自從用 dotnet run 成功運作第一個 "Hello world" .NET Core 應用程式後,一直有個好奇心:dotnet run 究竟是如何運作一個 .NET Core 應用程式的?在前兩篇博文之後,這個好奇心被進一步激發,于是“探秘 dotnet run”順理成章地成為.NET跨平台之旅的下一站。

自從用 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,終于滿足了那顆不安分的好奇心。

繼續閱讀