qshinoの日記

Powershell関係と徒然なこと

SynchronizingObject

SynchronizingObject on powetshell

Start-ProcessのExitedイベントの終了処理で、wpfのLabel.Contentを変更したい。

それだけ。

$proc = Start-Process "poi.bat" -passThru
$ev = Register-ObjectEvent $proc -EventName Exited -Action { AddText("hoge") }

function AddText($moji){
  $label.Content+=$moji}

$label = New-Object System.windows.controls.label

だがしかし、火の粉が降りかかる。

イベントが遅い

イベントが出るが、プロセスExit時にlabelの内容が変わらず、unregister-event時にlabelが変わっている様な。

また、ISEでステップ実行すると、プロセス終了時にlabelの内容が変わる場合がある。

イベントハンドラの実行時刻とイベントの発生時刻$e.TimeGeneratedを比べると大きくズレていた。

スレッドが違う?

非同期実行が問題の可能性があるので、$procのSynchronizingObjectにwpfのコントロールを設定しようとするが。

型変換に失敗

Windows.FormsではformなどをSynchronizingObjectに設定できるが、wpfでは型変換に失敗する。

IsynchronizeInvokeが必要だが、WPFでこれを実装しているクラスはない。

次…

と言う事で次の策を検討。

  1. 無理無理ISync…を実装
  2. ハンドラでDispatcher経由で実行

ここでは、2. を採用するとして、

  1. uiスレッドからDispatcherをハンドラに渡す。
  2. Debug用にDispatcher の前後にWrite-Host
  3. 渡されたDispatcherを使って、ハンドラがUIを操作。

Dispatcherを試す前に

$proc.EnableRaisingEvent について。

Start-Processの直後はfalse.但し、Register-ObjectEvent後はTrueに変化。此奴が原因ではない模様。

また、wpfとは無関係な下記の手順ではプロセス終了に合わせてイベント発生。

  1. $proc=Start-Proces cmd -PassThru
  2. Register-ObjectEvent $proc -EventName Exited -action { write-host “hoa” }
  3. cmd終了
  4. “hoa” がコンソールに表示

この場合はうまくいったがwpfだと如何に。

さて、ここでDispatcher登場

結果は少し後で。

とは言え、どのスレッドのDispatcherを使うのかが問題。UIスレッドのDispatcherを別スレッドに渡すとするなら、Register-ObjectEventの-MessageDataで渡すか、グローバルを使うか。

https://msdn.microsoft.com/ja-jp/library/cc647500(v=vs.110).aspx

によると、

In WPF, only the thread that created a DispatcherObject may access that object. For example, a background thread that is spun off from the main UI thread cannot update the contents of a Button that was created on the UI thread. In order for the background thread to access the Content property of the Button, the background thread must delegate the work to the Dispatcher associated with the UI thread. This is accomplished by using either Invoke or BeginInvoke.Invoke is synchronous and BeginInvoke is asynchronous. The operation is added to the event queue of the Dispatcher at the specified DispatcherPriority.

この中の一節。"Dispatcher associated with the UI thread.“を使えとあるので、UIスレッドのDispatcherを使う必要があると読める。どうやって渡すかが課題。

In order for the background thread to access the Content property of the Button, the background thread must delegate the work to the Dispatcher associated with the UI thread.

まずはコード、その一。クラスのDispatcher

$proc = Start-Process "poi.bat" -passThru
$ev = Register-ObjectEvent $proc -EventName Exited -Action { AddText("hoge") } 

function AddText($moji){
  [System.windows.Threading.Dispatcher]::BeginInvoke({
  $label.Content+=$moji})
}


$label = New-Object System.windows.controls.label

その二

UIスレッドのDispatcher をMessageDataを使ってハンドラに引き渡し、ハンドラが本Dispatcher を使用。

$proc = Start-Process "poi.bat" -passThru
$ev = Register-ObjectEvent $proc -EventName Exited -Action { AddText("hoge", $event.MessageData) } -MessageData [system.windows.threading.Dispatcher]::CurrentDispatcher

function AddText($moji, $dp){
  $dp.BeginInvoke({
  $label.Content+=$moji})
}

$label = New-Object System.windows.controls.label

結局、PowerShellでは

Dispatcherにスクリプトブロックを渡すと、delegate型への変換不可の例外発生。スクリプトブロックは動的メソッドらしく変換できないらしい。C#で書けば問題ないのだが、残念ながら今はPowershellがターゲット。つまり、再びの失敗。

そう言えば、$e.TimeGenerated

の発生時刻とハンドラ実行時刻がズレていた、という事は、真の問題はイベント発生後、直ぐにはハンドラが実行されないこと。ならば、UIスレッドでGet-Eventすれば良い?

当たり!

イベント発生後、ハンドラ実行前にPowershell のGet-Eventを実行するとヒョコッとハンドラが実行され、label.Contentにメッセージが追加された。だがしかし、ハンドラでlabelを書き換えており、ハンドラが非同期実行であれば、上手く行ったのは偶然の可能性がある。運が悪ければ非同期実行の罠にはまる。

仕方ない、インチキするしか

まさにインチキなのだが、

ここでは結果をWpfのタイマーDispatcherTimerを使う。完全修飾名はSystem.Windows.Threading.DispatcherTimer。引き渡すデータはDispatcherTimer のTagを使う。非同期のExitedハンドラでTagにデータを登録し、UIスレッドのTimerハンドラで引き取りlabelを更新。

非同期スレッド用のFIFO/queueなどがあればそれでも良いが、上記で動いたので、一旦締め。

暇があればWPFで使えるqueueを探すか、Dispatcher をもう少し深掘りしてみるともっと良い解決策が見つかるかもしれない。

以降は少し前の日記/流れが悪いので、お蔵入り。

Start-Processで生成した[System.Diagnostics.Process]のインスタンスのSynchronizingObjectプロパティにWpfコンポーネントを設定できない。設定しようとすると、IsynchronizeInvokeに変換できないとエラーになる。WPFには実装しているクラスがないらしい。むむ。

  • $menuitem
  • $window
  • Dispatcher

のどれもダメ。

下記のソースでは、Windows.Forms.Formは大丈夫な模様。

そもそもの問題はprocessのexited eventで、Labelの文字を変更するアクションを設定したものの、process終了時すぐには書き換えが発生せず、イベントハンドラクリア時にLabelが書き換わる。

WPFのスレッド処理が今一理解できていない。もっとシンプルなカーネルとかセマフォとかを見つけられない所が、まだ浅いと言うことか?

#FileWatchSample.ps1
param
( [string]$Directory = "D:\", [switch]$IncludeSubDirectory = $true )

#制御用フォーム準備
$label = New-Object -TypeName "System.Windows.Forms.Label"
$label.Text = "監視終了時は、このフォームを閉じてください。"
$label.Size = New-Object -TypeName "System.Drawing.Size" `
                         -ArgumentList @( $label.PreferredWidth, $label.PreferredHeight )
                         
$form = New-Object -TypeName "System.Windows.Forms.Form"
$form.Text = "ファイル監視中"
$form.AutoSize = $true
$form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::Fixed3D
[void]$form.Controls.Add( $label )

#FileSystemWatcher準備
$fileSystemWatcher = New-Object -TypeName "System.IO.FileSystemWatcher" `
                -ArgumentList @( $Directory )
$fileSystemWatcher.IncludeSubdirectories = $IncludeSubDirectory
$fileSystemWatcher.NotifyFilter = [System.IO.NotifyFilters]::CreationTime.value__ `
                    -bor [System.IO.NotifyFilters]::FileName.value__
$fileSystemWatcher.Filter = "*.*"

$fileSystemWatcher.add_Changed( `
    { Write-Host ( "{0} changed: {1}" -f [DateTime]::Now.ToString(), $_.FullPath ) } )
$fileSystemWatcher.add_Created( `
    { Write-Host ( "{0} created: {1}" -f [DateTime]::Now.ToString(), $_.FullPath ) } )
$fileSystemWatcher.add_Deleted( `
    { Write-Host ( "{0} deleted: {1}" -f [DateTime]::Now.ToString(), $_.FullPath ) } )
$fileSystemWatcher.add_Renamed( `
    { Write-Host ( "{0} renamed: {1} => {2}" `
                        -f [DateTime]::Now.ToString(), $_.OldFullPath, $_.FullPath ) } )
$fileSystemWatcher.SynchronizingObject = $form

#ファイル監視開始
$fileSystemWatcher.EnableRaisingEvents = $true
#終了時はフォームを閉じること。
$form.ShowDialog() | Out-Null

参考

http://dobon.net/vb/dotnet/process/openfile.html

http://cyberboy6.blog.fc2.com/blog-entry-445.html

WPF eventargs

https://msdn.microsoft.com/ja-jp/library/system.eventargs(v=vs.110).aspx