からめもぶろぐ。

俺たちは雰囲気で OAuth をやっている

Binary Module (C#) でいい感じに Write-Verbose したい

この記事は「PowerShell Advent Calendar 2018」の参加記事です。

qiita.com

ちなみに投稿が遅れたのではなく 16 日が埋まっていなかったので穴埋めしています。ここ重要!

Write-Verbose について

PowerShell では Verbose パラメーターを付けることでトレース情報を出力させることができます。モジュールの内部では Write-Verbose を呼び出すことで出力内容を制御します。Script Module の場合、PowerShell のスクリプトからコマンドレットを呼び出すだけなので、特に何も考える必要はないのですが、Binary Module の場合は勝手が違います。なぜなら、Cmdlet.WriteVerbose メソッドは static ではないからです。単純なコマンドレットであれば、ProcessRecord メソッドで全部書けば済むかもしれません。ある程度の規模になってくるとそうもいきません。かといって Cmdlet のインスタンスを持ち回すのも美しいとは思えません。

Trace.WriteLine があるじゃないか

C# には Trace.WriteLine メソッドがあります。こいつを使えばいけるのでは…と思いましたが、標準では Trace.WriteLine をホストに流し込んでくれるような仕組みにはなっていないようです。*1 仕方がないので TraceLister を自作します。

CmdletTraceListener.cs

public class CmdletTraceListener : TraceListener
{
    private readonly Cmdlet cmdlet;

    public CmdletTraceListener(Cmdlet cmdlet)
    {
        if (cmdlet == null)
        {
            throw new ArgumentNullException(nameof(cmdlet));
        }
        this.cmdlet = cmdlet;
    }

    public override void Write(string message)
    {
        this.cmdlet.WriteVerbose(message);
    }

    public override void WriteLine(string message)
    {
        this.cmdlet.WriteVerbose(message);
    }
}

TraceableCmdlet.cs

public abstract class TraceableCmdlet : PSCmdlet
{
    private readonly TraceListener traceListener;

    protected TraceableCmdlet()
    {
        this.traceListener = new CmdletTraceListener(this);
    }

    protected override void BeginProcessing()
    {
        Trace.Listeners.Add(this.traceListener);
        base.BeginProcessing();
    }

    protected override void EndProcessing()
    {
        base.EndProcessing();
        Trace.Listeners.Remove(this.traceListener);
    }
}

こうしてあげることで、どこからでも Trace.WriteLine を呼び出すだけで (Verbose パラメーターを指定していれば) ホストにメッセージが出力されますし、疎結合になるのでテストも容易になるのではないかと思います。

(2018/12/20 追記) Twitter でご指摘いただきました。ありがとうございます。

確かに例外が発生すると EndProcessing が呼ばれないので、TraceListener が解除されずにメモリ リークになってしまっているようです。IDisposable を実装してあげるのがよいようですね。

tech.blog.aerie.jp

(2019/04/19 追記) パイプライン使ったときに駄目みたいですねorz

stackoverflow.com

*1:Console.WriteLine は Write-Host と同様に動作します。

PowerShell Core で Binary Module (C#) を開発するときの注意点について

この記事は「PowerShell Advent Calendar 2018」の参加記事です。

qiita.com

サンプルとして簡単な JSON を返すコマンドレットを持つモジュールを作成します。対象の PowerShell Core のバージョンは 6.1.0 です。

サンプル コード

SampleModule.csproj

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netcoreapp2.1</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
    <PackageReference Include="System.Management.Automation" Version="6.1.0" />
  </ItemGroup>
  <ItemGroup>
    <None Update="SampleModule.psd1">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
  </ItemGroup>
</Project>

SampleModule.psd1

@{
    RootModule = 'SampleModule.dll'
    ModuleVersion = '1.0.0'
    CmdletsToExport = "*"
}

WriteHelloWorldCommand.cs

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Management.Automation;
using System.Text;

namespace SampleModule
{
    [Cmdlet("Write", "HelloWorld")]
    public class WriteHelloWorldCommand : PSCmdlet
    {
        protected override void ProcessRecord()
        {
            this.WriteObject(JsonConvert.SerializeObject(new { Message = "Hello World" }));
        }
    }
}

実行結果

dotnet publish してから Import-Module でモジュールを読み込んで実行してみますがエラーになります。

C:\SampleModule> dotnet publish
C:\SampleModule> Import-Module "C:\SampleModule\bin\Debug\netcoreapp2.1\publish\SampleModule.psd1"
C:\SampleModule> Write-HelloWorld
Write-HelloWorld : Could not load file or assembly 'Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed'. Could not find or load a specific file. (Exception from HRESULT: 0x80131621)
At line:1 char:2
+  Write-HelloWorld
+  ~~~~~~~~~~~~~~~~
+ CategoryInfo          : NotSpecified: (:) [Write-HelloWorld], FileLoadException
+ FullyQualifiedErrorId : System.IO.FileLoadException,SampleModule.WriteHelloWorldCommand

何が起こっているのか?

PowerShell Core は自身が Json.NET をライブラリとして使っています。PowerShell Core 6.1 系では Json.NET 11.0.2 が同梱されています。PowerShell Core を起動した時点で 11.0.2 がロードされてしまっているので、インポートしたモジュールで異なるバージョンのアセンブリを読もうとするとエラーになってしまいます。この問題を解決するには、モジュールが使う Json.NET のバージョンを常に PowerShell Core が使っている Json.NET のバージョンと合わせる必要があります。

根底としては PowerShell Core の問題ではなく .NET Core の問題 (AppDomain をサポートしていないことによる) なのですが、こんな時代になっても DLL 地獄に悩まされるのはどうなのかなあと思ってしまいます。

なお GitHub でもだいぶ前から Issue は上がっていますが Open のままとなっています。ちなみにこちらでは、異なるモジュールをインポートしたときにそれぞれが異なるバージョンのアセンブリを使っているとエラーになるよという内容ですが、どちらかというとこちらの問題のほうがエグいですね。

github.com

Microsoft Graph の変更通知を Microsoft Flow で受け取る

Microsoft Graph は Webhook により変更通知を受け取ることができます。アクセス許可の内容から察するに、個人で使うためではなく、組織の管理のためのようなのですが *1、こういうものはだいたい Flow で受け取れるような気がするのでやってみました。今回はユーザーの変更を受け取ります。

変更通知 - ドキュメント - Microsoft Graph

変更通知を受け取る Flow

変更通知を受け取るためのサブスクリプションを作成するときに、URL を検証するために、Microsoft Graph から送られる validationToken を返す必要があります。

  • Microsoft Graph sends a POST request to the notification URL:
    • POST https://{notificationUrl}?validationToken={TokenDefinedByMicrosoftGraph}
  • The client must provide a response with the following characteristics within 10 seconds:
    • A 200 (OK) status code.
    • The content type must be text/plain.
    • The body must include the validation token provided by Microsoft Graph.

ということでクエリ文字列に validationToken を含む場合と含まない場合で処理を分岐します。validationToken を含む場合は上記の通りのレスポンスを返却します。含まない場合は実際の変更通知なので、ID を受け取ってユーザー情報を取得し、その情報をメールで送ります。


サブスクリプションを作成する Flow

サブスクリプションのエンドポイントは「/subscriptions」なのですが、組織全体で共通ではなくユーザー/アプリケーション単位のようです。後述するサブスクリプションの更新を自動化するためには、アプリケーションのアクセス許可 (Client Credentials Grant) を使用する必要があるため *2、サブスクリプションの作成も同様に行う必要があります。notificationUrl には 変更通知を受け取る Flow の URL を指定します。


サブスクリプションを更新する Flow

サブスクリプションには有効期限があり (最大 3 日間)、更新しないと自動的に削除されてしまいます。永続的に変更通知を受け取るためには、スケジュールで 1 日 1 回 Flow を呼び出して有効期限を更新する必要があります。


まとめ

Flow でできるので LogicApps でもできますし、機能的に LogicApps で実装したほうがよいと思いました。変更通知を受け取るところは、カスタム処理を入れたいのであれば Functions に変更してください。

*1:いくつかのリソースは管理者の同意が必要なアクセス許可を必要とします。

*2:Microsoft Flow の HTTP アクションにある Active Directory OAuth 認証で Microsoft Graph API を利用する | idea.toString(); を参照のこと。

Power BI でマトリックスを使わないでマトリックスを頑張ってみる

Power BI を使ってザ・ジャパニーズな帳票を実現するためのバッドノウハウです。

問題点

例として東京の平均気温をマトリックスにしてみます。行に年月、列に平均気温を 5 ℃刻みで指定します。

はい、できました。しかし問題があります。「5 ℃未満」の列が左端にきてほしいのですが右端にきてしまっています。Power BI のマトリックスでは列の順序は昇順または降順でしか並べ替えるしかできないため、辞書順に並べ替えた結果、このような順序になってしまいます。

次に特定の年月でフィルターしてみます。

今後は「5 ℃未満」の列が表示されません。データのない列は表示されません。しかし列ヘッダーは固定でほしいという要望もあるでしょう。

解決方法

Power Query で頑張ります。

クエリの参照を作り、行列 (年月、分類) でグループ化します。列ヘッダーの中身に合うようにフィルターします。これを列の数だけ作成します。

もうひとつクエリの参照を作ります。行 (年月) でグループ化します。

先ほど作った列のクエリをひたすらマージします。列の順番も任意に並べ替えられます。

マトリックスではなくテーブルでデータを表示します。「5 ℃未満」の列が左端にきています。

フィルターをかけてもいい感じです。


まとめ

あくまで「頑張ればできるよ」という感じ。Excel のピボット テーブルだともうちょっといろいろできるので、そっちでやったほうがいいと思います。

Microsoft Graph data connect を実行してみる

以下の記事の続きです。

blog.karamem0.jp

日本リージョンではできないことがわかったので North America で Office 365 と Azure のテナントを作りました。途中ストレージ アカウントの種類を間違えて (V1じゃないと駄目っぽい?) 試行錯誤したものの、頑張ってなんとか Succeeded までこぎつけました。しかし注釈で判明しているとはいえデータ 2 件なのに時間がかかりすぎる…。

Blob Storage には JSON 形式でファイルが落ちてきます。ここに例えば Power BI から接続して可視化するなんてこともできそうですね。