ソースに絡まるエスカルゴ

貧弱プログラマの外部記憶装置です。

【iexpress】iexpressでインストーラとアンインストーラを作成する

 インストーラアンインストーラについて今まで作成したことがなかったのですが、Windows標準のアプリである「iexpress」を使えば一応のそれらが作成できることがわかったので、今回はその備忘録になります。


 では、始めます。


1:iexpressとは
 iexpressは簡単に言うと「複数のファイルをまとめてexeに固めてくれる」というようなアプリです。

 過去の記事でpyinstallerについて書きましたが、要はこれと同じでexeを起動するとその中身が展開されて特定ファイルを実行できるという代物です。

 この機能を使って疑似的なインストーラアンインストーラを作っていきます。


2:iexpresでexeを作成する
 まずはiexpressを使ってみます。

 「Windows+R」キーで出てきたダイアログに「iexpress」と入力してエンターキーを押下します。

 すると以下のようにiexpressが起動します。

 初めてのexeを作成する場合は、チェックボックスの上の方を選択した状態で「次へ」ボタンをクリックします。

 Package purposeの画面が表示されます。単にファイルの圧縮・解凍のみを行う(Extract files only)のか、解凍時にファイルを自動実行させるか(Extract files and run an installation command)を選択します。
 今回はインストーラアンインストーラを作成したいので、解凍時にファイルを自動実行する「Extract files and run an installation command」の方を選択して「次へ」ボタンをクリックします。

 Package titleの画面が表示されます。ここではexe実行時に出てくるダイアログのタイトル名を設定します。次の画面でダイアログを表示するかどうか選択できますが、表示しない場合でも何か入力しておく必要があります。値を設定したら「次へ」ボタンをクリックします。

 Confirmation promptの画面が表示されます。先に説明したようにここではexe実行時のダイアログを表示するかどうかを設定します。表示しない場合は「No prompt」、表示する場合は「Prompt user with」を選択して表示させたいメッセージを入力します。
 今回はダイアログを表示させないので「No prompt」を選択して「次へ」ボタンをクリックします。

 License agreementの画面が表示されます。ここではライセンスを表示するかどうかを設定します。表示しない場合は「Do not display a license」、表示する場合は「Display a licence」を選択してライセンスファイルを設定します。
 今回はライセンスは表示しないので「Do not display a license」を選択して「次へ」ボタンをクリックします。

 Package filesの画面が表示されます。ここではexeに含めるファイルを選択します。「add」ボタンをクリックして必要なファイルをすべて追加してから「次へ」ボタンをクリックします。

 Install Program to Launchの画面が表示されます。Package purposeの画面で解凍時にファイル自動実行を選択した場合に表示され、ここで自動実行するファイルを選択します。上にある「Install Program」は必須でこれが解凍時に自動実行されます。そしてその処理が終わった後に、下の方で別のファイルを実行する設定もできます。
 ここで注意なのですが、どうやら設定できるのは「拡張子が.cmdのファイルのみ」のようです。プルダウンではbatファイルなどが出てきますが、batファイルを設定してもインストーラ実行時にはエラーになるので「拡張子がcmdのファイル」を設定するようにしてください。中身はbatでcmdの拡張子に変更するだけで対応できます。
 今回は実行ファイルに「INSTALL_CALL.cmd」とcmdファイルを設定して「次へ」をクリックします。

 Show windowの画面が表示されます。これは先ほど設定したファイル実行中にウインドウを表示するかを設定します。
 今回は表示させたくないので「Hidden」を選択して「次へ」をクリックします。

 Finished messageの画面が表示されます。exeの解凍処理が終了した後にメッセージを表示するかどうかを設定します。
 今回は表示しないので「No message」を選択して「次へ」をクリックします。

 Packege Name and Optionsの画面が表示されます。「Browse」ボタンを押下してexeを保存する階層とファイル名を設定します。Optionsは下の方は「長いファイル名を採用するかどうか」のチェックなので基本的にチェックを入れます。Optionsの上の方は解凍中にダイアログでファイルの展開状況を表示させるかどうかを設定します。
 今回は以下のように設定し、「次へ」をクリックします。

 Configure restartの画面が表示されます。exe実行後に再起動を行うかどうかの設定を行います。
 今回は再起動の必要がないので「No restart」を選択して「次へ」をクリックします。

 Save Self Extractionの画面が表示されます。今までiexpressの設定をしてきた内容をファイルに保存するかを設定します。毎回設定するのは面倒なので上を選択してファイルに保存しておくのが良いです。
 今回はファイルに保存するよう設定して「次へ」をクリックします。

 Create packageの画面が表示されるので「次へ」をクリックします。

 しばらく待つとexeが作成されるので「完了」をクリックします。

 ちゃんと保存フォルダにexeとiexpressの設定ファイルであるsedファイルが作成されているのが確認できると思います。

 ちなみにsedファイルを使いたい場合は、iexpressの最初の画面で下の方を選び「Browse」ボタンから対象のsedファイルを選択します。

 後は出てくる画面全てで「次へ」を選択すればexeが作成されます。


3:iexpressでインストーラを作成
 iexpressの使い方がわかったので、Program Files(x86)内に「TEST_IEXPRESS」というフォルダを作成しその中に「test.bat」ファイルを配置するインストーラ、を作成してみます。

 前提としてCドライブ直下に「iexpress_test」というフォルダを作成し、その中にさらに「INSTALLER」フォルダを作成してその中に必要なファイルを作っていく形にします。

 まずは適当なtest.batを作成します。

・test.bat

echo "テストです"
pause

 次にインストール処理のメインとなるpowershellのファイルを作成します。

・INSTALL_MAIN.ps1

# アセンブリの読み込み
Add-Type -Assembly System.Windows.Forms

# メッセージボックスを表示する関数
function Show-Message([string]$msg, [string]$title, [string]$type="OK", [string]$icon="None") {
  $showMessageRes = [System.Windows.Forms.MessageBox]::Show($msg, $title, [System.Windows.Forms.MessageBoxButtons]::$type, [System.Windows.Forms.MessageBoxIcon]::$icon)
  return $showMessageRes
}

# エラーが出たらストップする
$ErrorActionPreference = "Stop"

# スクリプト実行場所のディレクトリ取得
$currentDir = Split-Path $MyInvocation.MyCommand.Path

# インストール先ファイルパスを設定
$programFilesDir = "C:\Program Files (x86)"
$saveDir = "${programFilesDir}\TEST_IEXPRESS"

# ファイルパスを作成
$batFile = "${currentDir}\test.bat"
$toBatFile = "${saveDir}\test.bat"

# 既にインストールされているかチェック
if((Test-Path $saveDir)) {
  # 既にインストールされていたの場合は中断
  Show-Message "${saveDir} と同名のフォルダが既に存在したためインストールを中断します。" "確認" "OK" "Warning"
  exit 200
}

# フォルダ作成
try {
  New-Item $saveDir -ItemType Directory -Force > $null
} catch {
  Show-Message "${saveDir} フォルダの作成に失敗しました。" "エラー" "OK" "Error"
  exit 200
}

# ファイルコピー処理
try {
  Copy-Item $batFile $toBatFile > $null
} catch {
  Show-Message "${saveDir} 配下にファイルをコピーできませんでした。" "エラー" "OK" "Error"
  exit 200
}

# インストール完了
Show-Message "インストール処理が正常に終了しました。" "完了"
exit

 次にメイン処理のpowershellを管理者権限で呼び出すバッチファイルを作成します。

・INSTALL_RUN.bat

@echo off

@REM このファイルがある階層へ移動
cd /d %~dp0

@REM 管理者権限、ポリシー無制限で実行
powershell -NoProfile -ExecutionPolicy Unrestricted ".\INSTALL_MAIN.ps1" -verb runas

@REM 処理が終わったら処理が終わったファイルを作成
echo end > end.txt

 最後にexeの解凍時に実行されるcmdファイルを作成します。

・INSTALL_CALL.cmd

@echo off

@REM ウインドウ非表示、管理者権限で実行
powershell -Command Start-Process "INSTALL_RUN.bat" -WindowStyle Hidden -verb runas

@REM エラー起きたら終了
if %errorlevel% neq 0 (
    goto :END
)

@REM endファイルが作成されるまでこの処理を終了させない
:LOOP
if exist end.txt (
    del /f end.txt
    goto :END
)
goto :LOOP

:END

 LOOPで処理待ちをしているのはINSTALL_RUN.batを呼び出した瞬間に「INSTALL_CALL.cmd」の処理が終了し、呼び出し先も一緒に終了してしまうためです。
 それを防ぐために「INSTALL_RUN.bat」の処理が終了した時にファイルを作成し、そのファイルが作成されるまで終了せずに待つという形になっています。

 色々とファイルが出てきて少しややこしくなったので、一度整理します。以下のような順番で処理が進んでいきます。
1:exe解凍後、INSTALL_CALL.cmdが自動実行
2:INSTALL_CALL.cmdからINSTALL_RUN.batを呼び出し
3:INSTALL_RUN.batからINSTALL_MAIN.ps1を管理者権限で実行
4:INSTALL_MAIN.ps1内の処理でProgram Files(x86)にフォルダを作成してtest.batをコピーして配置
5:INSTALL_MAIN.ps1の処理終了後、INSTALL_RUN.batで終了したとわかるファイルを作成
6:処理終了ファイルが存在すればINSTALL_CALL.cmdの処理を終了

 これらのファイルを「C:\iexpress_test\INSTALLER」配下に以下のように配置します。

 あとは2の手順と同じようにiexpressでexeを作成します。exeとsedの保存先は「C:\iexpress_test」直下にしておくと色々と楽です。

 実際にiexpressで作成すると以下のようになります。

 これでexeを実行すると環境によっては管理者権限の許可が求められ、許可をするとインストール処理が実行されます。


4:iexpressでアンインストーラを作成
 次はアンインストーラを作成していきます。インストーラと違って削除するだけなのと呼び出し部分はほぼ同じでいけるので比較的楽に作成できます。

 前提としてCドライブ直下に「iexpress_test」というフォルダを作成し、その中にさらに「UNINSTALLER」フォルダを作成してその中に必要なファイルを作っていく形にします。

 まずはアンインストールのメイン処理になるpowershellファイルを作成します。

・UNINSTALL_MAIN.ps1

# アセンブリの読み込み
Add-Type -Assembly System.Windows.Forms

# メッセージボックスを表示する関数
function Show-Message([string]$msg, [string]$title, [string]$type="OK", [string]$icon="None") {
  $showMessageRes = [System.Windows.Forms.MessageBox]::Show($msg, $title, [System.Windows.Forms.MessageBoxButtons]::$type, [System.Windows.Forms.MessageBoxIcon]::$icon)
  return $showMessageRes
}

# エラーが出たらストップする
$ErrorActionPreference = "Stop"

# インストール先ファイルパスを設定
$programFilesDir = "C:\Program Files (x86)"
$saveDir = "${programFilesDir}\TEST_IEXPRESS"

# 既存のインストール先フォルダがある場合は強制削除
if ((Test-Path $saveDir)) {
  try {
    Remove-Item $saveDir -Recurse -Force
  } catch {
    Show-Message "${saveDir} のフォルダの削除に失敗しました。" "エラー" "OK" "Error"
    exit 200
  }
}

Show-Message "アンインストール処理が正常に終了しました。" "完了"

exit

 次にメイン処理のpowershellを管理者権限で呼び出すバッチファイルを作成します。

・UNINSTALL_RUN.bat

@echo off

@REM このファイルがある階層へ移動
cd /d %~dp0

@REM 管理者権限、ポリシー無制限で実行
powershell -NoProfile -ExecutionPolicy Unrestricted ".\UNINSTALL_MAIN.ps1" -verb runas

@REM 処理が終わったら処理が終わったファイルを作成
echo end > end.txt

 最後にexeの解凍時に実行されるcmdファイルを作成します。

・UNINSTALL_CALL.cmd

@echo off

@REM ウインドウ非表示、管理者権限で実行
powershell -Command Start-Process "UNINSTALL_RUN.bat" -WindowStyle Hidden -verb runas

@REM エラー起きたら終了
if %errorlevel% neq 0 (
    goto :END
)

@REM endファイルが作成されるまでこの処理を終了させない
:LOOP
if exist end.txt (
    del /f end.txt
    goto :END
)
goto :LOOP

:END

 整理すると、アンインストーラは以下のような順番で処理が進んでいきます。
1:exe解凍後、UNINSTALL_CALL.cmdが自動実行
2:UNINSTALL_CALL.cmdからUNINSTALL_RUN.batを呼び出し
3:UNINSTALL_RUN.batからUNINSTALL_MAIN.ps1を管理者権限で実行
4:UNINSTALL_MAIN.ps1内の処理でProgram Files(x86)に「TEST_IEXPRESS」があれば削除
5:UNINSTALL_MAIN.ps1の処理終了後、UNINSTALL_RUN.batで終了したとわかるファイルを作成
6:処理終了ファイルが存在すればUNINSTALL_CALL.cmdの処理を終了

 これらのファイルを「C:\iexpress_test\UNINSTALLER」配下に以下のように配置します。

 あとはiexpressでexeとsedを作成します。

 この作成したアンインストーラを実行すると、Program Files(x86)に「TEST_IEXPRESS」フォルダがあれば削除されます。


 以上がiexpressでインストーラアンインストーラを作成する方法になります。

 exe解凍時の自動実行ファイルからメイン処理まで何度も呼び出しを繰り返すので少しややこしいですし、ここまで呼び出すファイルが必要ではないかもしれませんが、一応この方法でインストーラアンインストーラを作成できます。
 iexpressはWindowsの標準ソフトとして入っているので、開発環境の制限が厳しいところでもインストーラアンインストーラを作成できるというのを知っておけばどこかで役に立つかもしれません。


・参考資料

【PowerShell】PowerShellでタスクスケジューラの登録/削除を行う

 以前タスクスケジューラの記事を書きました。

 その中でxmlファイルでタスク情報を保存できることを書きましたが、今回はそのxmlファイルを使用してPowerShellでタスク登録を行う方法、そして削除する方法になります。
 厳密にはWindowsコマンドを使っていますが、ps1ファイル内で記述しても使えるのでまとめてPowerShellということで話を進めていきます。


 では、始めます。


1:PowerShellでのタスクスケジューラへのタスク登録
 PowerShell、というかWindowsコマンドでのタスク登録、削除方法は以下の通りです。

・タスク登録

schtasks /create /tn [タスク名] /xml [タスクxmlのファイルパス]

・タスク削除(強制)

schtasks /delete /tn [タスク名] /f

 これらタスクスケジューラに変更を加えるコマンドは、実行時に管理者権限が必要らしいので注意してください。


2:サンプルコード
 書き方がわかったので実際に動かしてみます。「テストタスク」というタスク名でそのタスク情報が保存されているxmlファイル名が「テストタスク.xml」、そしてそのxmlファイルがps1ファイルと同階層にあるという前提です。

・task_test.ps1

# スクリプト実行場所のフォルダ取得
$currentDir = Split-Path $MyInvocation.MyCommand.Path

# タスク名を設定
$taskname = "テストタスク"

# タスク登録
schtasks /create /tn $taskname /xml "${currentDir}\テストタスク.xml"

# タスクが存在すれば強制削除
if ((Get-ScheduledTask -TaskName $taskname)) {
  schtasks /delete /tn $taskName /f
}


 このコマンドを実行すると一度タスク登録をして削除をするという流れになります。
 タスク削除部分をコメントアウトするとタスクが登録された状態のままになります。


 以上がPowerShellでタスクスケジューラの登録/削除を行う方法になります。

 別のPCにあるオリジナルのタスクをまとめて登録したい場合などは、このようにPowerShellから登録する形にすると楽かもしれません。


・参考資料

【PowerShell】PowerShellを記述したファイルをコマンドで実行する方法

 過去の記事でPowerShellを右クリックから実行するような内容を書きました。

 しかし、コマンドで実行できた方がが何かと便利なので、今回はPowerShellを記述したファイルをコマンドで実行する方法になります。


 では、始めます。


・コマンドからの実行方法
 powershellでファイルを実行するための基本的なコマンドは以下の通りです。

powershell [ファイルパス]

// 例
powershell .\test1.ps1

 しかし処理内容などによっては権限やポリシーで実行できない場合があります。その場合はオプションを付けて実行します。

オプション 内容
-NoProfile プロファイル無しで実行
-ExecutionPolicy Unrestricted ポリシーの制限なしで実行
-verb runas この記述の前にあるコマンドを管理者権限で実行

 この他にもオプションは色々ありますが、オプションについての詳しい説明は参考資料に挙げているページ様を参照してください。

 ひとまず「プロファイル無し、ポリシー無制限、管理者権限で実行」するには以下のように記述します。

powershell -NoProfile -ExecutionPolicy Unrestricted [ファイルパス] -verb runas

 例えば実行させたいPowerShellのファイル(test1.ps1)を管理者権限で実行するためのバッチファイル(start.bat)を同階層に作る場合は以下のようになります。

・start.bat

@echo off

@REM 現在のフォルダに移動
cd /d %~dp0

@REM 同階層のtest1.ps1をプロファイル無し、ポリシー無制限、管理者権限で実行
powershell -NoProfile -ExecutionPolicy Unrestricted ".\test1.ps1" -verb runas

 このようにバッチファイルからPowerShellファイルを呼び出すことで、管理者権限を付与した状態で実行することができます。


 以上がPowerShellを記述したファイルをコマンドで実行する方法になります。

 バッチファイルから管理者権限でPowerShellが実行できてしまうため、「-verb runas」オプションの扱いには注意が必要です。
 本当に必要な処理だけにつけるようにしましょう。


・参考資料

【Windows】タスクスケジューラでプログラムを自動起動させる

 過去にRaspberry Piでプログラムを自動実行する記事を書きました。

 これと同じようなことがWindowsでもできる仕組みがあります。それがタスクスケジューラです。

 今回はそのタスクスケジューラについての記事になります。


 では、始めます。


1:タスクスケジューラとは
 Windowsのタスクバーにある検索欄に「タスクスケジューラ」と入力すると、以下のようにアプリが出てきます。

 これをクリックして起動すると、以下のようにタスクスケジューラが立ち上がります。

 このタスクスケジューラにタスクを作成して登録することで、決まった時間やログインなどをトリガーとしてプログラムを自動実行することができます。


2:タスクを作成してみる
 では実際にタスクを作成してみます。

 自動実行させたいバッチファイルの内容とパスは以下の通りであるという前提で進めていきます。

・start.bat

echo "test"
pause

・実行したいバッチファイルのパス

C:\test\start.bat

 タスクの作成は、タスクスケジューラの右側のメニューにある「基本タスクの作成」をクリックします。

 以下のようなタスク作成画面が出てくるので、ここではタスクの名前を「テストタスク」、説明には「テストタスクです」と入力して「次へ」ボタンをクリックします。

 トリガー(どのタイミングで実行するか)の設定画面になるので、ここでは「ログオン時」を選択して「次へ」ボタンをクリックします。

 トリガー時にどのような処理を実行するかの画面になるので「プログラムの開始」を選択して「次へ」ボタンをクリックします。

 起動するプログラムの設定画面になるので「参照」ボタンをクリックして対象のプログラムを選択します。ここでは先に挙げたstart.batを選択しています。選択したら「次へ」ボタンをクリックします。

 確認画面が出てくるので、内容を確認して「完了」ボタンをクリックします。あとでまた細かい設定を変更することもできるので、今回はこのまま登録します。

 タスクの作成終了すると、一覧の中に新しく追加したタスクができています。「トリガー」タブや「操作」タブを開くとちゃんと設定したトリガーとプログラムが設定されていることがわかります。


3:タスクの実行確認
 タスクが作成できたので、ちゃんとトリガーのタイミングで実行するかどうかを確認してみます。

 2で作成したのは「ログオン時」をトリガーとするタスクなので、一度ユーザをサインアウトしてからもう一度サインインをするか、PC自体を再起動させて同じユーザでログインすれば確認できます。

 実際にサインアウトしてからもう一度サインインすると、以下のようにコンソールが自動的に立ち上がります。

 この状態でタスクスケジューラを立ち上げて該当タスクを確認してみると、実行結果のところが「現在タスクを実行中です」になっていることがわかります。

 コンソール上で何かキーを入力してタスクを終了させた後、タスクスケジューラの右側メニューにある「最新の情報に更新」をクリックすると、以下のように実行結果が「この操作を正しく終了しました」に変化します。

 今回は正常終了したので実行結果の表示がこのようになりましたが、途中でエラーなどで終了した場合はエラーコードだったり、エラーで終了した旨のメッセージなどが表示されます。


4:タスクの細かい部分の設定変更
 ログオンしてから1分遅れで実行させたいなど、細かい設定変更を行いたい場合があります。

 その場合は、タスクスケジューラでタスクを選択した後右クリックをし、出てきたメニューから「プロパティ」をクリックします。

 タスクのプロパティ画面が開くので、ここで細かい設定変更が行えます。

 今回はログオンから1分遅れで実行させたいので「トリガー」タブを開いて対象のトリガーを選択した状態で「編集」ボタンをクリックします。

 トリガー編集画面が開きます。1分遅れで実行させたいので「遅延時間を設定する」にチェックを入れてプルダウンから遅延させたい時間を設定して「OK」ボタンをクリックします。

 これでログオンしてから1分後に起動させる設定に変更できました。

 その他にもタスク名や実行させたいプログラムの変更などもこのプロパティ画面から行えます。


5:その他タスクに関する情報
 タスクの情報をファイルとして保存しておきたい場合は、タスクを右クリックして出てきた中にある「エクスポート」を選択します。

 これでタスクの情報をxmlファイルとして保存することができます。このxmlファイルをタスクスケジューラに読み込ませることで、同じタスクを再度登録することもできます。

 タスクを削除したい場合は、タスクを右クリックして出てきた中にある「削除」を選択すると削除できます。

 その他、無効にしたり実行したりタスクを右クリックすることで色々と操作ができます。


 以上がタスクスケジューラでプログラムを自動起動させる方法になります。

 色々と細かい設定もできるので、定期的に必ず行うようなプログラムについてはタスクスケジューラで自動起動させると便利かもしれません。

【PowerShell】PowerShellでメッセージボックスを表示させる

 たまにPowerShellでちょっとした処理を実行したい場合があります。
 その際にコンソールではなくメッセージボックスでメッセージを表示させたり、確認メッセージを表示してYes, Noを選択させたい場合があります。

 今回はPowerShellでそんなメッセージボックスを表示させる方法になります。

 2つの方法を紹介しますが、詳しくは参考資料に挙げているページ様の方を参照してください。


 では、始めます。


1:.NETのMessageBoxを使う方法
 1つ目の方法は.NETの機能を使うことです。

 具体的には以下のようにします。

 以下の内容をコピペしてSJIS形式で保存してください。
 PowerShellSJISで動くため、UTF-8などの形式だと文字化けしたり動かない場合があるので注意してください。

・test1.ps1

# アセンブリの読み込み(必須)
Add-Type -Assembly System.Windows.Forms

# メッセージボックスを表示する関数
function Show-Message([string]$msg, [string]$title, [string]$type="OK", [string]$icon="None") {
  $showMessageRes = [System.Windows.Forms.MessageBox]::Show($msg, $title, [System.Windows.Forms.MessageBoxButtons]::$type, [System.Windows.Forms.MessageBoxIcon]::$icon)
  return $showMessageRes
}

# メッセージボックス表示
$res = Show-Message "YesかNoかを選択してください。" "選択" "YesNo" "Warning"
Show-Message "${res}を選択しました。" "確認" "OK" "Information"
Show-Message "エラー発生。" "エラー" "OK" "Error"
Show-Message "デフォルト表示"

 少し解説すると最初のアセンブリ読み込みをした後、メッセージボックスを表示させる関数を定義しています。
 第1引数にメッセージ内容、第2引数にタイトル、第3引数にボタンのタイプ、第4引数にアイコンのタイプを指定します。設定されていない場合はデフォルト値で実行されます。
 戻り値も取得できるようにしているため、選択肢がある場合はその結果で処理を分けることもできるようにしています。
 ボタンやアイコンに設定できる値については、以下を参照してください。


 PowerShellを記述したファイルを実行するには、ファイルを右クリックして出てきた中から「PowerShellで実行」を選択して実行します。

 実際に実行すると以下のように順番にメッセージボックスが表示されます。





2:その他の方法
 1の方法は自由度は高いですが、関数にまとめないと少し面倒な書き方が必要でした。

 少し自由度は落ちますが、より簡単な書き方もできます。

 以下の内容をコピペして、こちらもSJIS形式で保存してください。

・test2.ps1

# オブジェクト作成
$msgbox = new-object -comobject wscript.shell

# メッセージボックス表示
$res = $msgbox.popup("YesかNoかを選択してください。", 0, "選択", 4)
$msgbox.popup("${res}を選択しました。", 0, "確認", 0)
$msgbox.popup("エラー発生。", 0, "エラー", 16)
$msgbox.popup("デフォルト表示")

 こちらの方はシンプルですが、引数に設定する値が固定されています。
 第1引数はメッセージ内容、第2引数は表示秒数(省略時は0)、第3引数はタイトル、そして第4引数はメッセージボックスのタイプを以下の表の番号から指定します(参考ページ様から引用して加工)。

番号 内容
0 [OK] ボタンのみ表示。
1 [OK] ボタンと [キャンセル] ボタンを表示。
2 [中止] ボタン、[再試行] ボタン、 [無視] ボタンを表示。
3 [はい] ボタン、[いいえ] ボタン、[キャンセル] ボタンを表示。
4 [はい] ボタンと [いいえ] ボタンを表示。
5 [再試行] ボタンと [キャンセル] ボタンを表示。
16 [Stop] アイコンで[OK]ボタンを表示。
32 [?] アイコンで[OK]ボタンを表示。
48 [!] アイコンで[OK]ボタンを表示。
64 [i] アイコンで[OK]ボタンを表示。

 保存できたら先ほどと同様に右クリックから「PowerShellで実行」を選択して実行します。

 実行すると以下のように順番にメッセージボックスが表示されます。





 以上がPowerShellでメッセージボックスを表示させる方法になります。

 ちょっとした処理でもメッセージボックスを表示させるようにすれば、使いやすくなるかと思います。


・参考資料

【C#】プレーンなWindows環境でローカルHTTPサーバを作成する

 某大企業ではUSBフラッシュメモリが全面禁止になったりなど、プログラムを開発する環境は何かと制限されたりすることが多いです。
 インストールするアプリに制限があるような環境での開発で「HTTPリクエストを送ってその結果を受け取って処理をする」ようなプログラムを書いたとしても、テスト用のローカルサーバを作るのも何かと大変だったりします。

 そこで今回はプレーンなWindows環境でローカルHTTPサーバを作成できることがわかったので、それについての記事になります。

 参考資料に挙げているページ様の内容を自分なりに改造してみた形となるので、元の情報を知りたい方はそちらを参照してください。


 では、始めます。


1:プレーンなWindows環境でもC#は使える
 C#の開発にはVisual Studioのインストールが必要だという認識の方も多いと思いますが、実は真っ新なWindows環境でもC#の開発ができます。

 Visual Studioをインストールしていない状態でも、以下のNET Frameworkのフォルダが存在していると思います。

C:\Windows\Microsoft.NET\Framework64

 C#.NET Frameworkの一部なので、.NET Frameworkがあれば(バージョンの違いやメモ帳などエディタは限られますが)C#の開発が可能なのです。

 では、実際にC#のプログラムを書いて実行してみます。

 メモ帳を開いて以下の内容をコピペして「hello.cs」という名前で保存します。

・hello.cs

using System;

class Hello{
  public static void Main(){
    Console.WriteLine("Hello World!");
  }
}

 次にコマンドプロンプトPowerShellを立ち上げ、hello.csを保存したフォルダへ移動します。

cd [hello.csを保存しているフォルダ]

 移動できたら以下のコマンドを実行してexe化します。

C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe hello.cs

 実行すると、以下のようなメッセージが表示されます。

> C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe hello.cs
Microsoft (R) Visual C# Compiler version 4.8.9032.0
for C# 5
Copyright (C) Microsoft Corporation. All rights reserved.

This compiler is provided as part of the Microsoft (R) .NET Framework, but only supports language versions up to C# 5, which is no longer the latest version. For compilers that support newer versions of the C# programming language, see http://go.microsoft.com/fwlink/?LinkID=533240

 エラーが出たり実行できなかった場合は、対象ファイルと同じ階層にちゃんと移動しているか、コマンドのバージョン部分が合っているかなどを確認してみてください。

 コマンド実行後は、同じ階層に「hello.exe」が作成されているはずです。

 あとはコマンドを実行したコンソールで「hello.exe」と叩くだけでexeを実行できます。

 実際に実行すると以下のようになります。

>hello.exe
Hello World!

 このようにプレーンなWindows環境でもC#のプログラムを書いて動かせます。


2:C#でローカルHTTPサーバを作成する
 では本題のローカルHTTPサーバを作成していきます。と言っても、hello worldの時と同じようにコピペしてexe化するだけです。

 メモ帳を開いて以下の内容をコピペし「HttpLocalServer.cs」という名前で保存します。

・HttpLocalServer.cs

using System;
using System.IO;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;

public class HttpLocalServer
{
    private readonly static string DirSep = Path.DirectorySeparatorChar.ToString();
    private readonly static string ParentMid = DirSep + ".." + DirSep;
    private readonly static string ParentLast = DirSep + "..";

    private static string s_root = "./";
    private static string s_prefix = null;
    private static string PORT = "8001";

    public static void Main(string[] args)
    {
        try
        {
            ParseOptions(args);
            string prefixPath = WebUtility.UrlDecode(
                Regex.Replace(s_prefix, "https?://[^/]*", ""));

            using (HttpListener listener = new HttpListener())
            {
                listener.Prefixes.Add(s_prefix);
                listener.Start();
                Console.WriteLine("--- Start ---");
                Console.WriteLine("http://localhost:" + PORT + "/normal");
                Console.WriteLine("http://localhost:" + PORT + "/empty");
                Console.WriteLine("http://localhost:" + PORT + "/error");

                while (true)
                {
                    HttpListenerContext context = listener.GetContext();
                    HttpListenerRequest request = context.Request;

                    using (HttpListenerResponse response = context.Response)
                    {

                        Console.WriteLine("-------------------------");

                        ShowRequestData(request);

                        string rawPath = WebUtility.UrlDecode(
                            Regex.Replace(request.RawUrl, "[?;].*$", ""))
                            .Substring(prefixPath.Length-1);

                        Console.WriteLine("called " + rawPath);

                        string responseData = "";
                        response.StatusCode = 200;

                        if (rawPath == "/normal")
                        {
                            responseData = "normal";
                        }
                        else if (rawPath == "/empty")
                        {
                            responseData = "";
                        }
                        else if (rawPath == "/error")
                        {
                            responseData = "NG error\n";
                        }
                        else
                        {
                            response.StatusCode = 502; // BadGateway
                        }

                        byte[] content = Encoding.UTF8.GetBytes(responseData);
                        response.ContentType = "text/plain";
                        response.ContentLength64 = content.Length;
                        response.OutputStream.Write(content, 0, content.Length);

                        Console.WriteLine("[{0}] \"{1} {2} HTTP/{3}\" {4} {5}",
                            DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss K"),
                            request.HttpMethod,
                            request.RawUrl,
                            request.ProtocolVersion,
                            response.StatusCode,
                            response.ContentLength64);
                    }
                }
            }
        }
        catch (Exception e)
        {
            Console.Error.WriteLine(e);
        }
    }

    private static void ParseOptions(string[] args)
    {
        string port = PORT;
        string host = "+";

        for (int i = 0; i < args.Length; i++)
        {
            if (args[i].Equals("-t"))
            {
                s_prefix = "http://+:80/Temporary_Listen_Addresses/";
            }
            else if (args[i].Equals("-p") && i+1 < args.Length)
            {
                port = args[++i];
            }
            else if (args[i].Equals("-b") && i+1 < args.Length)
            {
                host = args[++i];
            }
            else if (args[i].Equals("-r") && i+1 < args.Length)
            {
                s_root = args[++i];
            }
            else if (args[i].Equals("-P") && i+1 < args.Length)
            {
                s_prefix = args[++i];
            }
            else
            {
                Console.Error.WriteLine(
                    "usage: {0} [-r DIR] [-p PORT] [-b ADDR]\n" +
                    "    or {0} [-r DIR] [-t]\n" +
                    "    or {0} [-r DIR] [-P PREFIX]",
                    AppDomain.CurrentDomain.FriendlyName);
                Environment.Exit(0);
            }
        }

        if (s_prefix == null)
        {
            s_prefix = string.Format("http://{0}:{1}/", host, port);
        }
    }

    private static void ShowRequestData (HttpListenerRequest request)
    {
        if (!request.HasEntityBody)
        {
            Console.WriteLine("No client data was sent with the request.");
            return;
        }
        System.IO.Stream body = request.InputStream;
        System.Text.Encoding encoding = request.ContentEncoding;
        System.IO.StreamReader reader = new System.IO.StreamReader(body, encoding);
        string s = reader.ReadToEnd();
        Console.WriteLine(s);
        body.Close();
        reader.Close();
    }
}

 少し解説するとクラスの最初の方にある「PORT」の定数でポート番号を指定しています。
 ShowRequestData関数でrequest bodyの中身を取得してコンソール上に表示させています。
 rawPath変数にはURLのAPI部分(http://localhost:8001/xxxのxxx部分)を取得しています。
 その後API部分毎の処理を記述し、responseの返す値を変更しています。
 処理の最後にresponseを作成し、処理が終了した日時を表示しています。

 ファイルの作成できたら、先ほどと同じようにコマンドプロンプトPowerShellを立ち上げ、HttpLocalServer.csを保存したフォルダへ移動します。

cd [HttpLocalServer.csを保存しているフォルダ]

 移動できたら以下のコマンドを実行してexe化します。

C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe HttpLocalServer.cs

 コマンドを実行すると同階層に「HttpLocalServer.exe」ができていると思います。

 このローカルHTTPサーバを実行したい場合は「HttpLocalServer.exe」を右クリックし「管理者として実行」をクリックして実行します。

 許可して実行すると、コンソールが立ち上がって以下の表示が出てきます。

--- Start ---
http://localhost:8001/normal
http://localhost:8001/empty
http://localhost:8001/error

 このコンソールを立ち上げた状態でブラウザなどから表示されているURLにアクセスしてみてください。

 例えば「http://localhost:8001/normal」にアクセスすると、レスポンスとして「normal」の文字列が返ってきます。
 そしてコンソール上では以下のようにアクセスした際のrequest bodyの中身(ブラウザからなので今回は何もない)と呼ばれたAPI名、呼び出された時間が表示されます。

-------------------------
No client data was sent with the request.
called /normal
[2023-11-09 12:47:17 +09:00] "GET /normal HTTP/1.1" 200 6
-------------------------
No client data was sent with the request.
called /favicon.ico
[2023-11-09 12:47:17 +09:00] "GET /favicon.ico HTTP/1.1" 502 0

 サーバを終了させたい場合は、コンソール自体を閉じるか、Ctrl + Cキーで終了させます。


 以上がプレーンなWindows環境でローカルHTTPサーバを作成する方法になります。

 正直C#に慣れていないので中身をちゃんと理解しているわけではないですが、制限された環境でも簡単なテスト用ローカルHTTPサーバであれば作成できることがわかったので、そのような環境で苦しんでいる方々の助けになれば幸いです。


・参考資料

【C#】システムログの取得とそこに含まれるEventDataの値を取得する

 過去の記事でシステムログ関連のことをやってきました。

 今回は実際にシステムログを取得してみた記事になります。


 では、始めます。


1:システムログの取得方法
 やり方は色々とありますが、今回は「EventLogReader」を使ったやり方を紹介します。

 やり方は簡単で以下のような方法で取得できます。

// イベントログ取得(EventLogQueryの第一引数にはログ名を設定する。今回はSystemログのため"System"を設定)
EventLogQuery eventLogQuery = new EventLogQuery("System", PathType.LogName);
EventLogReader eventLogReader = new EventLogReader(eventLogQuery);

// 取得したログ全てについての処理
for (EventRecord eventRecord = eventLogReader.ReadEvent(); eventRecord != null; eventRecord = eventLogReader.ReadEvent())
{
    // 1つのログごとに何らかの処理をここに書く
}

 最初の「EventLogQuery eventLogQuery = new EventLogQuery("System", PathType.LogName);」で対象のログを読み込んでいます。
 今回はシステムログを読み込みたかったので第一引数に「"System"」を設定しています。第二引数に「PathType.LogName」を設定することでイベントログの名前が格納されます。
 それをEventLogReaderで読み込み、for文を使ってログを1つずつ抽出している形になります。


2:システムログのEventDataの値を取得する
 前に記事にも書きましたが、システムログの「詳細」タブのEventDataの内容はアプリ毎に自由に設定できるものです。

 そのEventDataの内容へアクセスする場合は、以下のように「EventRecordのProperties」を使います。

// 1の方法でEventRecordを取得してきたという前提
for (EventRecord eventRecord = eventLogReader.ReadEvent(); eventRecord != null; eventRecord = eventLogReader.ReadEvent())
{
    eventRecord.Properties[0].Value; // EventDataの1番目、つまり画像での「TSId」を取得(文字列にするならさらにtoStringが必要)
    eventRecord.Properties[1].Value; // EventDataの2番目、つまり画像での「UserSid」を取得(文字列にするならさらにtoStringが必要)
}

 EventDataの中身はアプリ毎によって全く違うため「存在するか、Propertiesの長さがちゃんとあるか」などチェックしてから取得するようにするのが良さそうです。


3:システムログのログインとログアウトをUserSidから取得するサンプル
 1と2でシステムログの取得とそこに含まれるEventDataの取得方法がわかったので、今までの記事の内容と合わせて「システムログのログインとログアウトをUserSidから取得」してみようと思います。

 前回と同じようにC#のコンソールプロジェクトを作成し、その名前を「コンソールプロジェクト」として以下のプログラムをコピペしてください。

・Program.cs

using System;
using System.Collections.Generic;
using System.Diagnostics.Eventing.Reader;
using System.Linq;
using System.Security.Principal;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace コンソールプロジェクト
{
    internal class Program
    {
        static void Main(string[] args)
        {
            // SIDを取得
            string myUser = Environment.UserName;
            string userSid = GetUserSid(myUser);
            Console.WriteLine(userSid);

            // SIDのログを取得
            string loginLogout = GetLog(userSid);
            Console.WriteLine(loginLogout);

            Console.ReadKey();
        }

        // ユーザ名からSIDに変換する
        private static string GetUserSid(string username)
        {
            NTAccount nTAccount = new NTAccount(username);
            SecurityIdentifier s = (SecurityIdentifier)nTAccount.Translate(typeof(SecurityIdentifier)); // ユーザが存在しなかった場合ここでエラーになる
            string userSid = s.ToString();
            return userSid;
        }

        // ログを取得する
        private static string GetLog(string userSid)
        {
            // Systemのイベントログ取得
            EventLogQuery eventLogQuery = new EventLogQuery("System", PathType.LogName);
            EventLogReader eventLogReader = new EventLogReader(eventLogQuery);

            StringBuilder sb = new StringBuilder();

            // 取得したログ全てについての処理
            for (EventRecord eventRecord = eventLogReader.ReadEvent(); eventRecord != null; eventRecord = eventLogReader.ReadEvent())
            {
                // ログインとログアウトのイベントIDのみ実行
                if (eventRecord.Id == 7001 || eventRecord.Id == 7002)
                {
                    // ログからSIDを取得して一致判定
                    string logUserSid = GetLogUserSid(eventRecord);
                    if (!userSid.Equals(logUserSid)) continue;

                    // SIDが一致していたら結果に追加
                    sb.Append("Time = ");
                    sb.Append(eventRecord.TimeCreated);
                    sb.Append(", EventID = ");
                    sb.Append(eventRecord.Id);
                    sb.Append(", usersid = ");
                    sb.Append(logUserSid);
                    sb.Append(Environment.NewLine);
                }
            }
            return sb.ToString();
        }

        // ログのUserSIDを取得する
        private static string GetLogUserSid(EventRecord eventRecord)
        {
            string userId = null;
            if (eventRecord.UserId != null && eventRecord.Properties.Count >= 2)
            {
                userId = eventRecord.Properties[1].Value.ToString();
            }
            return userId;
        }
    }
}

 特にプログラムの解説はする必要ないかと思いますが、エラー処理などは不十分だと思うのでしっかりやりたい場合は色々と改造してみてください。

 実行すると以下のように実行したユーザのSIDと一致しているログインとログアウトのログがあれば、そのログの時刻とイベントID、SIDを表示しています。


 以上がC#でシステムログの取得とそこに含まれるEventDataの値を取得する方法になります。

 イベントビューアーにあるEventDataの取得方法がわからなくてかなり長時間悩んだりしたので、同じように困っている人の助けになればと思います。


・参考資料