dotnetでUnix、Windows両方の環境で管理者権限かどうかのチェック

とりま結論。
細かい文章校正は後でします。

ifディレクティブでUNIXかどうかの判定はPowershellのソースコードでも全体で使われている。
プロジェクトにUNIXとWindowsのソースコードがどうしてもかなり混ざる、大規模なソースコードだとほぼ必須。

ifディレクティブでUnixとわけないと視覚的にUnixのソースコードかどうかを見分けるのが大変（ディレクティブでなくてifという形で散らばるため。）。
ちゃんとやるとifディレクティブでソースコード折り畳めるようになるし、目にも優しくなる。

プロジェクトファイルに書いたが、各プロジェクトでなくてソリューション直下にpropsとして持っておくべき。

```xml:cli.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  <!-- #if UNIX, #if !UNIX とifディレクティブでソースコードを分けれるようにする。 -->
  <PropertyGroup>
    <DefineConstants>$(DefineConstants);CORECLR</DefineConstants>
    <IsWindows Condition="'$(IsWindows)' =='true' or ( '$(IsWindows)' == '' and '$(OS)' == 'Windows_NT')">true</IsWindows>
  </PropertyGroup>

  <!-- Define non-windows, all configuration properties -->
  <PropertyGroup Condition=" '$(IsWindows)' != 'true' ">
    <DefineConstants>$(DefineConstants);UNIX</DefineConstants>
  </PropertyGroup>

</Project>
```

```csharp
using System.Diagnostics;
using System;
using System.Threading;
using System.Security.Permissions;
using System.Security.Principal;
using Microsoft.CSharp.RuntimeBinder;

internal class Program
{
    /// <summary>
    /// check Administrator priviledge.
    /// </summary>
    /// <exception cref="RuntimeBinderException">unix環境下においてuidの取得失敗</exception>
    /// <exception cref="IOException">unix環境下においてuid取得時のIOError</exception>
    /// <exception cref="NullReferenceException">windows環境下においてCurrentPrincipalの取得失敗</exception>
    /// <returns>If Administrator or root user, return true.</returns>
    public static bool IsAdministrator()
    {

        bool judge = false;
#if UNIX
        ProcessStartInfo processStartInfo = new() {
            FileName = "id",
            Arguments = "-u",
            CreateNoWindow = true,
            RedirectStandardOutput = true,
            UseShellExecute = false,
        };
        string uidln = string.Empty;
        try
        {
            using Process process = new();
            process.StartInfo = processStartInfo;
            process.Start();
            process.WaitForExit();
            uidln = process.StandardOutput.ReadToEnd();

            if (uidln.Length < 1 || process.ExitCode != 0) 
            {
                throw new RuntimeBinderException($"External process exited with non-zero exit code: {process.ExitCode}");
            }
        }
        catch (Exception)
        {
            throw;
        }

        // ReadToEndは最後の改行文字まで取得するので末尾を削除
        string uid = uidln[..^1];

        // uid が0ということはrootユーザー。
        if ("0".Equals(uid))
        {
            judge = true;
        }
#else
        AppDomain domain = Thread.GetDomain();

        domain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);
        WindowsPrincipal principal = (WindowsPrincipal?)Thread.CurrentPrincipal ?? throw new NullReferenceException();

        judge = principal.IsInRole(WindowsBuiltInRole.Administrator);
#endif
        return judge;
    }

    private static void Main(string[] args)
    {
        bool judge = false;
        try
        {
           judge = IsAdministrator(); 
        } catch (Exception e)
        {
            Console.WriteLine(e.ToString());
            return;
        }

        if (judge)
        {
            Console.WriteLine("You are Administrator user!");
        } else {
            Console.WriteLine("You are not Administrator user!");
        }
    }
}
```


## まとめ

windowsとunix環境両方でも動くものが必要ならひたすらこのようにifディレクティブでUnix, windows
と環境を分けて書くことになる。意外とメンテナンス性は良い。

必要とあらばここからruntimeごとにさらにifディレクティブで分けることになる。

.netframework系とdotnet core系をruntimeで分ける場合はほぼ、ソースコードが倍になることは覚悟しよう。

## 参考

[WindowsPrincipal class](https://learn.microsoft.com/ja-jp/dotnet/api/system.security.principal.windowsprincipal?view=net-7.0)

[WindowsBuiltinRole Enum](https://learn.microsoft.com/ja-jp/dotnet/api/system.security.principal.windowsbuiltinrole?view=net-7.0)