前些天有 AgileConfig 的用户反映,如果把 AgileConfig 部署成 Windows 服务程序会启动失败。我看了一下日志,发现根目录被定位到了 C:/Windows/System32
下,那么读取 appsettings.json 配置文件自然就失败了。
var builder = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory());
以上是我构造 ConfigurationBuilder 的代码。使用 Directory.GetCurrentDirectory()
获取程序根目录然后设置 SetBasePath
。以上代码在99%的情况是不会有问题的,那么为什么会在做为服务部署的时候会有问题呢?让我们往下看。
Directory.GetCurrentDirectory()
Directory.GetCurrentDirectory()
获取根目录是我们很常见的一个操作。先让我们对其进行一些简单的测试。新建一个控制台程序,编写以下代码进行:
var dirpath = Directory.GetCurrentDirectory(); Console.WriteLine("Directory.GetCurrentDirectory = " + dirpath);
直接运行
代码很简单,就是读取根目录,然后输出一下。
Directory.GetCurrentDirectory = C:/00WORKSPACE/basedir/basedir/bin/Debug/net6.0
编译完成后双击 exe 文件,可以看到获取到的目录是正确的。
使用 cmd 运行
下面让我们试一下在 cmd 下运行这个 exe 。
C:/>cd C:/00WORKSPACE/basedir/basedir/bin/Debug/net6.0 C:/00WORKSPACE/basedir/basedir/bin/Debug/net6.0>basedir.exe Directory.GetCurrentDirectory = C:/00WORKSPACE/basedir/basedir/bin/Debug/net6.0
我们把路径切换到 exe 所在的目录,然后输入 basedir.exe 直接运行它,可以看到输出的目录也是正确的。
切换工作目录
这次我们把工作目录切换到 C 盘的 apps 目录,然后使用完全路径去执行 exe 程序。
C:/>cd apps C:/APPS>C:/00WORKSPACE/basedir/basedir/bin/Debug/net6.0/basedir.exe Directory.GetCurrentDirectory = C:/APPS
怎么样?是不是跟预期的不一样了?这次输出的根目录不是 exe 所在的目录,而是 c:/APPS
,也就是我们的工作目录。
使用另外一个 exe 程序启动测试程序
在我们日常场景中有很多时候需要通过一个程序去运行另外一个程序,那么这个时候 Directory.GetCurrentDirectory
获取的根目录是怎么样的呢?
首先我们编写另外一个 WPF 的程序,使用这个程序来启动我们的 basedir.exe 测试程序。
我们把这个 WPF 程序放在 c:/APPS
目录下,然后运行它:
其中按钮的事件代码如下:
private void Button_Click(object sender, RoutedEventArgs e) { var process = new Process(); process.StartInfo.FileName = this.path.Text; process.Start(); }
点击这个按钮,它会把我们的测试程序 basedir.exe 给运行起来:
我们可以看到,当 WPF 程序把我们的测试程序运行起来的时候,测试程序输出的目录为 c:/APPS
,也就是 WPF 程序所在的目录。
为什么做为服务运行的时候获取程序根目录为 System32
通过以上的测试,AgileConfig 做为服务运行的时候获取根目录为 C:/Windows/System32
的原因已经很明显了。我们的 windows 服务的启动一般来说有2个途径,一个是通过 sc.exe 工具另外一个是通过 services.exe 也就是 SCM (service control manager) 来启动。那么这2个可执行程序在哪里呢?答案就是 C:/Windows/System32
。我们的 windows 服务的启动其实是通过这2个工具来运行的,所以根据上面的测试,很明显通过Directory.GetCurrentDirectory
来获取根目录的话会是这2个工具所在的目录。
其它读取程序根目录的方式
通过以上我们知道通过Directory.GetCurrentDirectory
读取根目录会有一点小坑。在我们的 .NET 世界里还有很多办法能获取根目录,那么他们会不会也有坑呢?
以下列几个常见的获取根目录的方法:
var dirpath = Directory.GetCurrentDirectory(); Console.WriteLine("Directory.GetCurrentDirectory = " + dirpath); // 通过 AppDomain.CurrentDomain.BaseDirectory 读取根目录 var dirpath1 = AppDomain.CurrentDomain.BaseDirectory; Console.WriteLine("AppDomain.CurrentDomain.BaseDirectory = " + dirpath1); // 通过 Environment.CurrentDirectory 来读取根目录 var dirpath2 = Environment.CurrentDirectory; Console.WriteLine("Environment.CurrentDirectory = " + dirpath2); // 通过 Assembly.GetExecutingAssembly().Location 来获取运行程序集所在的位置,从而判断根目录 var dirpath3 = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); Console.WriteLine("Path.GetDirectoryName Assembly.GetExecutingAssembly().Location = " + dirpath3); // 通过 AppContext.BaseDirectory 获取根目录 var dirpath4 = AppContext.BaseDirectory; Console.WriteLine("AppContext.BaseDirectory = " + dirpath4);
直接运行
把以上获取根目录的代码补充进我们的测试程序,编译成功后直接运行:
Directory.GetCurrentDirectory = C:/00WORKSPACE/basedir/basedir/bin/Debug/net6.0 AppDomain.CurrentDomain.BaseDirectory = C:/00WORKSPACE/basedir/basedir/bin/Debug/net6.0/ Environment.CurrentDirectory = C:/00WORKSPACE/basedir/basedir/bin/Debug/net6.0 Path.GetDirectoryName Assembly.GetExecutingAssembly().Location = C:/00WORKSPACE/basedir/basedir/bin/Debug/net6.0 AppContext.BaseDirectory = C:/00WORKSPACE/basedir/basedir/bin/Debug/net6.0/
以上是输出结果。可以看到所有的方法都准确的获取到了 exe 程序所在的根目录。有一点要注意的是
AppDomain.CurrentDomain.BaseDirectory
跟 AppContext.BaseDirectory
方法获取的路径最后带有一个 /
其它则没有。
使用 cmd 运行
同样让我们在 cmd 下运行一下:
C:/00WORKSPACE/basedir/basedir/bin/Debug/net6.0>basedir.exe Directory.GetCurrentDirectory = C:/00WORKSPACE/basedir/basedir/bin/Debug/net6.0 AppDomain.CurrentDomain.BaseDirectory = C:/00WORKSPACE/basedir/basedir/bin/Debug/net6.0/ Environment.CurrentDirectory = C:/00WORKSPACE/basedir/basedir/bin/Debug/net6.0 Path.GetDirectoryName Assembly.GetExecutingAssembly().Location = C:/00WORKSPACE/basedir/basedir/bin/Debug/net6.0 AppContext.BaseDirectory = C:/00WORKSPACE/basedir/basedir/bin/Debug/net6.0/
我们把路径切换到 exe 所在的目录,然后输入 basedir.exe 直接运行它,可以看到所有的方法输出的目录都是正确的。
切换工作目录
同样我们在 cmd 下把工作目录切换到 c:/APPS
,然后运行 exe 。
C:/>cd APPS C:/APPS>C:/00WORKSPACE/basedir/basedir/bin/Debug/net6.0/basedir.exe Directory.GetCurrentDirectory = C:/APPS AppDomain.CurrentDomain.BaseDirectory = C:/00WORKSPACE/basedir/basedir/bin/Debug/net6.0/ Environment.CurrentDirectory = C:/APPS Path.GetDirectoryName Assembly.GetExecutingAssembly().Location = C:/00WORKSPACE/basedir/basedir/bin/Debug/net6.0 AppContext.BaseDirectory = C:/00WORKSPACE/basedir/basedir/bin/Debug/net6.0/
可以看到 Directory.GetCurrentDirectory
和 Environment.CurrentDirectory
方法输出均为 c:/APPS
而其它方法则都输出了 exe 所在目录。
使用另外一个 exe 程序启动测试程序
同样我们再次使用另外一个 WPF 程序来运行 basedir.exe 测试程序:
可以看到 Directory.GetCurrentDirectory
和 Environment.CurrentDirectory
方法输出均为 c:/APPS
,也就是 WPF 所在的目录, 而其它方法则都输出了 exe 所在目录。
总结
以上常见的 5 种读取程序当前目录的办法在绝大多数情况下都可以正确的获取到预期的结果。其中需要注意的是Directory.GetCurrentDirectory
和 Environment.CurrentDirectory
。这2个方法在 cmd 或者 bash 环境下返回的是工作目录;使用 A 程序启动另外一个 B 程序的时候,B 程序获取到的根目录是 A 程序所在的目录。所以使用 Directory.GetCurrentDirectory
和 Environment.CurrentDirectory
的时候一定要格外注意,避免引入 BUG 。