From f870ed8d97e65bb41ee5aa136066132d2094f6c9 Mon Sep 17 00:00:00 2001 From: nishi_74322014 Date: Sat, 12 Sep 2026 16:19:46 +0900 Subject: [PATCH 1/4] fixed #587 --- .../Authentication/OAuth2AndOIDCConst.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/root/programs/CS/Frameworks/Infrastructure/Framework/Authentication/OAuth2AndOIDCConst.cs b/root/programs/CS/Frameworks/Infrastructure/Framework/Authentication/OAuth2AndOIDCConst.cs index 33e91c5b1..760d3a56b 100644 --- a/root/programs/CS/Frameworks/Infrastructure/Framework/Authentication/OAuth2AndOIDCConst.cs +++ b/root/programs/CS/Frameworks/Infrastructure/Framework/Authentication/OAuth2AndOIDCConst.cs @@ -31,6 +31,7 @@ //* 201X/XX/XX 西野 大介 ... //* 2020/02/27 西野 大介 FAPI CIBAのパラメタを追加 //* 2020/12/18 西野 大介 Device AuthZのパラメタを追加 +//* 2026/09/12 玄人 幸道 RFC 6750 / RFC 7662 / CIBA / OIDCの定数を追加 //********************************************************************************** // urnはClaimのurnで、 @@ -144,6 +145,9 @@ public class OAuth2AndOIDCConst /// token_type_hint public const string token_type_hint = "token_type_hint"; + /// active(イントロスペクションの応答フィールド) + public const string active = "active"; + #endregion #endregion @@ -597,6 +601,14 @@ public class OAuth2AndOIDCConst public const string error = "error"; #region error値 + + // エラー コードは、ここに定数として定義する。 + // 但し、トークン エンドポイントのポーリング状態 + // (authorization_pendingなど)は、状態遷移を伴うため、 + // OAuth2AndOIDCEnumのDeviceAuthZState、CibaStateに定義し、 + // ToStringByEmitで文字列化して使用する(access_deniedのみ両方に在る)。 + + #region RFC 6749 /// invalid_request public const string invalid_request = "invalid_request"; @@ -628,6 +640,32 @@ public class OAuth2AndOIDCConst public const string temporarily_unavailable = "temporarily_unavailable"; #endregion + #region RFC 6750 + /// invalid_token(Bearerトークンが無効) + public const string invalid_token = "invalid_token"; + #endregion + + #region OIDC Core + /// login_required(prompt=noneで、未認証) + public const string login_required = "login_required"; + + /// consent_required(prompt=noneで、同意が無い) + public const string consent_required = "consent_required"; + + /// interaction_required(prompt=noneで、対話が必要) + public const string interaction_required = "interaction_required"; + + /// account_selection_required(prompt=noneで、アカウントの選択が必要) + public const string account_selection_required = "account_selection_required"; + #endregion + + #region CIBA Core + /// unknown_user_id(login_hintなどのユーザが見つからない) + public const string unknown_user_id = "unknown_user_id"; + #endregion + + #endregion + /// error_description public const string error_description = "error_description"; From ce08fc8863d9b6c4470f1180579dbace1253cbbb Mon Sep 17 00:00:00 2001 From: nishi_74322014 Date: Sat, 12 Sep 2026 17:36:17 +0900 Subject: [PATCH 2/4] fixing #588 --- root/programs/1_BuildAll.ps1 | 24 ++++++++++++++ root/programs/3_SmokeTest.ps1 | 53 ++++++++++++++++++++++++++++++ root/programs/BUILDING.md | 41 +++++++++++++++++++++++ root/programs/CHEATSHEET.md | 1 + root/programs/SMOKETEST.md | 35 +++++++++++++++++++- root/programs/st_Targets.ps1 | 26 +++++++++++++++ root/programs/st_Utility.ps1 | 61 +++++++++++++++++++++++++++++++++++ 7 files changed, 240 insertions(+), 1 deletion(-) diff --git a/root/programs/1_BuildAll.ps1 b/root/programs/1_BuildAll.ps1 index 890777763..ca0b220ea 100644 --- a/root/programs/1_BuildAll.ps1 +++ b/root/programs/1_BuildAll.ps1 @@ -497,6 +497,30 @@ if ($allErrors.Count -gt 0) } } +# --- ロックのヒント(#588)--- +# +# **MSB3021 / MSB3027 が出たら、一律でヒントを出す。** +# 出力ファイルがロックされているとき、コンパイル エラーは 0 件のまま NG になるため、 +# **コード側の誤りと見分けが付かない。** +# プロジェクト名や件数で条件を分けない。分けるほどの精度は要らず、 +# 条件が増えるほど「出るはずの場面で出ない」が起きる。 +# +# **表示だけで、判定は変えない。** 終了コードは従来どおりエラー件数で決まる。 +# ここでプロセスを探索もしない。コマンドを文字列として示すに留めるので、 +# CI に権限と実行時間の影響を持ち込まない。 +if (@($allErrors | Where-Object { $_ -match 'MSB3021|MSB3027' }).Count -gt 0) +{ + Write-Host "" + @( + '【ヒント】出力ファイルがロックされています。前回の疎通テストの残骸を疑ってください。' + ' 中断された 3_SmokeTest.ps1 が起動したプロセスが残っていることが多い。' + ' Get-CimInstance Win32_Process -Filter "Name=''dotnet.exe''" |' + ' Where-Object { $_.CommandLine -like ''*OpenTouryo*'' } |' + ' Select-Object ProcessId, CreationDate, CommandLine' + ' 詳細は BUILDING.md 4 節「既知の環境依存」。' + ) | ForEach-Object { Write-Host $_ -ForegroundColor Yellow } +} + # 除外したものは必ず表示する。黙って消すと、-IgnoreErrors が広すぎたときに気付けない。 if ($allKnown.Count -gt 0) { diff --git a/root/programs/3_SmokeTest.ps1 b/root/programs/3_SmokeTest.ps1 index ecde145b0..6fa005c6e 100644 --- a/root/programs/3_SmokeTest.ps1 +++ b/root/programs/3_SmokeTest.ps1 @@ -307,6 +307,59 @@ if ($Only) Write-Host (" -Only '$Only' : {0} 件に絞りました" -f $selected.Count) -ForegroundColor Yellow } +# ------------------------------------------------------------------ +# 前提の確認(#588) +# ------------------------------------------------------------------ +# **冒頭で見る。** 対象ごとの確認だけだと、aspnet_state のように +# 終盤でしか使わないものは、気付くまでに数分かかる。 +# +# **表示だけで、判定は変えない。** 足りなくてもここでは止めない +# (対象ごとの「前提未達」は従来どおり出る)。 +# **開始も停止もしない。** システムの状態を変える操作だからで、 +# CI は専用のステップで開始している(SMOKETEST.md 4 節・AGENTS.md の線引き)。 +# +# **選んだ対象が要るものだけを見る。** -Only で絞ったときに、 +# 無関係なサービスの不足を報告しない。 +$needSvc = @($selected | ForEach-Object { $_.Need } | Where-Object { $_ } | Select-Object -Unique) +$needDb = @($selected | Where-Object { $_.NeedDb }).Count -gt 0 + +if ($needSvc.Count -gt 0 -or $needDb) +{ + Write-Host "" + Write-Host "=== 前提の確認 ===" -ForegroundColor Cyan + + $missing = @() + + foreach ($name in $needSvc) + { + $svc = Get-Service $name -EA SilentlyContinue + $ok = ($svc -and $svc.Status -eq "Running") + $detail = if ($ok) { "Running" } elseif ($svc) { [string]$svc.Status } else { "未導入" } + + Write-Host (" {0,-12} {1}" -f $name, $detail) ` + -ForegroundColor $(if ($ok) { "Green" } else { "Yellow" }) + + if (-not $ok) { $missing += ("Start-Service {0} # 管理者権限が必要" -f $name) } + } + + if ($needDb) + { + # 接続で見る(サービス名では見ない)。理由は st_Utility.ps1 の Test-SqlServer。 + $db = Test-SqlServer 5 + Write-Host (" {0,-12} {1}" -f "SQL Server", $db.Detail) ` + -ForegroundColor $(if ($db.Ok) { "Green" } else { "Yellow" }) + + if (-not $db.Ok) { $missing += "SQL Server の Northwind に接続できること(SMOKETEST.md 4 節)" } + } + + if ($missing.Count -gt 0) + { + Write-Host "" + Write-Host " **前提が足りません。このまま進めますが、該当の対象は NG になります。**" -ForegroundColor Yellow + $missing | ForEach-Object { Write-Host (" " + $_) } + } +} + if (-not $SkipBuild) { # ビルドする単位は「フォルダ + バッチ」。同じバッチ名が CS と VB の diff --git a/root/programs/BUILDING.md b/root/programs/BUILDING.md index 708fd480a..f6f4770dc 100644 --- a/root/programs/BUILDING.md +++ b/root/programs/BUILDING.md @@ -167,6 +167,47 @@ error MSB3321: Importing key file "WSClientWinCone_sample_TemporaryKey.pfx" was プロジェクト名まで含めているのは、同じコードが他のプロジェクトで出たときに 見逃さないため。コードだけで除外すると範囲が広すぎる。 +### 残存プロセスによるファイル ロック(#588) + +``` +error MSB3027: "...\OpenTouryo.Framework.dll" を "bin\Debug\net10.0\..." にコピーできませんでした。 + 10 回の再試行回数を超えたため、失敗しました。 + このファイルは ".NET Host (24216)" によってロックされています。 +error MSB3021: ... The process cannot access the file ... because it is being used by another process. +``` + +**前回の疎通テストが起動したサンプル Web サーバが残っている。** +`3_SmokeTest.ps1` は `finally` で停止するため、**正常に終われば残らない。** +残るのは中断されたときで、**次回の実行はそれを検知しない。** + +**コード側の不具合と見分けにくい。** `error CS` は **0 件**のまま NG になる。 +`1_BuildAll.ps1` はこの 2 つのコードを見つけるとヒントを出す(表示だけで、判定は変えない)。 + +```powershell +Get-CimInstance Win32_Process -Filter "Name='dotnet.exe'" | + Where-Object { $_.CommandLine -like '*OpenTouryo*' } | + Select-Object ProcessId, CreationDate, CommandLine +``` + +**起動時刻が今回の実行より前なら、残存である。** 停止してから絞って回し直せば足りる。 + +```powershell +.\1_BuildAll.ps1 -Only "WSSrvCore_sample" -SkipClean +.\3_SmokeTest.ps1 -Only "OpenAPI" -SkipBuild +``` + +> **疎通まで連鎖する。** Clean で出力を消した後にコピーが失敗するため、 +> `runtimeconfig.json` が揃わず、続く `3_SmokeTest.ps1` で +> **「ポートが開かない」**(実体は起動失敗)になる。 +> **1 つの残存プロセスが、無関係に見える 2 つの NG を生む。** + +**プロセスの停止は、スクリプトは行わない。** システムの状態を変える操作であり、 +利用者が意図して動かしているものを黙って落とすべきではないため +([`SMOKETEST.md`](SMOKETEST.md) 4 節と同じ線引き)。 + +**CI では起きない。** `windows-latest` は使い捨てで回をまたげず、 +回の中でも疎通はビルドの後なので、出力をロックしようがない。 + --- ## 5. 修正の経緯 : nuget.exe の MSBuild 誤検出 diff --git a/root/programs/CHEATSHEET.md b/root/programs/CHEATSHEET.md index afed4c7b1..5341576c8 100644 --- a/root/programs/CHEATSHEET.md +++ b/root/programs/CHEATSHEET.md @@ -281,6 +281,7 @@ powershell.exe -NoProfile -Command "Set-Location 'root\programs'; .\3_SmokeTest. | ビルドは通るのに `DllNotFoundException`(`...SNI...`) | `nuget restore` を呼んでおらず、ネイティブ DLL が出力に入らない | 該当 sln に restore を足す。[`BUILDING.md`](BUILDING.md) 10 節 | | `packages.config` の id が `csproj` に無い=不要に見える | サテライト(`.ja`)とコンテンツ パッケージは**出なくて正常**(48 件中 44 件) | 消す前に [`BUILDING.md`](BUILDING.md) 11 節 | | ビルドは通るのに実行時 `FileNotFoundException` | **版は 4 か所に散らばる。** `` の `Version=` がずれると、警告だけ出て参照が落ちる | `.\ComparePackage.ps1 -Check`。[`BUILDING.md`](BUILDING.md) 12 節 | +| `MSB3021` / `MSB3027`(コピーできない・ロック)。`error CS` は 0 件 | **前回の疎通テストの残骸**(中断されたときに残る)が出力を掴んでいる。**疎通の「ポートが開かない」まで連鎖する** | 起動時刻で残存を見分けて停止。[`BUILDING.md`](BUILDING.md) 4 節 | **NuGet パッケージ作成の落とし穴は [`CS/NuGet/README.md`](CS/NuGet/README.md) 9 節**にまとめてある。 diff --git a/root/programs/SMOKETEST.md b/root/programs/SMOKETEST.md index 912f68731..3dd2cb010 100644 --- a/root/programs/SMOKETEST.md +++ b/root/programs/SMOKETEST.md @@ -567,6 +567,37 @@ Start-Service aspnet_state # 管理者権限が必要 **スクリプトはサービスを自動起動しない。** システムの状態を変える操作であり、 リリース判定のために黙って環境を書き換えるべきではないため。 +### 冒頭の「前提の確認」(#588) + +**対象ごとの確認だけだと、気付くのが遅い。** +`aspnet_state` を使うのは終盤の Web アプリだけで、そこまで約 7 分かかる。 +そこで、**実行の冒頭で一覧にする。** + +``` +=== 前提の確認 === + aspnet_state Running + SQL Server localhost / Northwind +``` + +| 前提 | 確認の方法 | +|---|---| +| `aspnet_state` | `Get-Service`。**名前が固定**なので、これでよい | +| SQL Server | **`App.config` の接続文字列で、接続を試す** | + +**SQL Server をサービス名で確認してはならない。** +既定インスタンス(`MSSQLSERVER`)と名前付き(`MSSQL$SQLEXPRESS`)で名前が変わり、 +リモートなら手元にサービスが無い。**決め打つと、CI を誤って「前提未達」にする。** +確認の接続は `Connection Timeout` を **5 秒**に上書きする +(既定の 15 秒は長く、`App.config` 側の値に左右されると確認時間が環境で変わる)。 +**上書きするのはこの確認の中だけ**で、実際にデータを読む側は従来どおり。 + +- **表示だけで、判定は変えない。** 足りなくてもここでは止めない + (対象ごとの「前提未達」は従来どおり出る) +- **選んだ対象が要るものだけを見る。** `-Only` で絞ったときに、 + 無関係なサービスの不足を報告しない +- **対象側で宣言する。** `st_Targets.ps1` の `Need`(サービス名)と + `NeedDb`(SQL Server を使うか)。**対象を追加するときは、ここも見る**(9 節) + --- ## 5. 判定基準 @@ -780,7 +811,9 @@ MVC_Sample (net10.0) OK ログイン後 /Crud1/Index = 200 | `Verify` | 追加の検証(スクリプト ブロック) | | `Kind` | `Web` を指定すると Web アプリ扱い | | `WebHost` | `IISExpress` または `Kestrel` | -| `Site` / `Port` / `Flow` / `Need` | Web アプリ用 | +| `Site` / `Port` / `Flow` | Web アプリ用 | +| `Need` | 必要な Windows サービス名(`aspnet_state`。**名前が固定のものだけ**) | +| `NeedDb` | SQL Server の Northwind を使うなら `$true`(#588) | **判定条件は「動いていれば必ず満たす」ものにする。** 実行のたびに変わる値(件数以外の可変値、日時など)を条件に入れると、 diff --git a/root/programs/st_Targets.ps1 b/root/programs/st_Targets.ps1 index fbfaf126c..3f837154f 100644 --- a/root/programs/st_Targets.ps1 +++ b/root/programs/st_Targets.ps1 @@ -11,6 +11,11 @@ **3_SmokeTest.ps1 からドット ソースで読まれる。** 単体では動かない(#571 で分割)。 + + <前提の宣言(#588)> + Need … 必要な Windows サービス名(名前が固定のものだけ) + NeedDb … SQL Server の Northwind を使うか + 3_SmokeTest.ps1 が冒頭で、**選ばれた対象が要るものだけ**を確認する。 #> $targetsCS = @( @@ -26,6 +31,7 @@ $targetsCS = @( Name = "DaoGen_Tool DAODEFGEN (net48)"; Bat = "4_Build_Framework_Tool.bat" Exe = "Frameworks\Tools\DaoGen_Tool\bin\Debug\OpenTouryo.DaoGen_Tool.exe" Args = $daoDefArgs48; Expect = '生成が完了しました。' + NeedDb = $true Pre = $prepareDaoGen48; Verify = $verifyDaoDef48 } @{ @@ -43,6 +49,7 @@ $targetsCS = @( Name = "DaoGen_Tool DAODEFGEN (net10.0)"; Bat = "4_Build_Framework_ToolCore.bat" Exe = "Frameworks\Tools\DaoGen_Tool\bin\Debug\net10.0-windows7.0\OpenTouryo.DaoGen_Tool.exe" Args = $daoDefArgsCore; Expect = '生成が完了しました。' + NeedDb = $true Pre = $prepareDaoGenCore; Verify = $verifyDaoDefCore } @{ @@ -135,21 +142,25 @@ $targetsCS = @( Name = "SimpleBatch_sample (net48)"; Bat = "5_Build_Bat_sample.bat" Exe = "Samples\Bat_sample\SimpleBatch_sample\bin\Debug\SimpleBatch_sample.exe" Args = $batchArgs; Expect = '\d+件のデータがあります' + NeedDb = $true } @{ Name = "RerunnableBatch_sample (net48)"; Bat = "5_Build_Bat_sample.bat" Exe = "Samples\Bat_sample\RerunnableBatch_sample\bin\Debug\RerunnableBatch_sample.exe" Args = $batchArgs; Pre = $clearOrders2; Verify = $verifyOrders2 + NeedDb = $true } @{ Name = "RerunnableBatch_sample2 (net48)"; Bat = "5_Build_Bat_sample.bat" Exe = "Samples\Bat_sample\RerunnableBatch_sample2\bin\Debug\RerunnableBatch_sample2.exe" Args = $batchArgs; Pre = $clearOrders2; Verify = $verifyOrders2 + NeedDb = $true } @{ Name = "RerunnableBatch_sample3 (net48)"; Bat = "5_Build_Bat_sample.bat" Exe = "Samples\Bat_sample\RerunnableBatch_sample3\bin\Debug\RerunnableBatch_sample3.exe" Args = $batchArgs; Pre = $clearOrders2; Verify = $verifyOrders2 + NeedDb = $true } # --- バッチ (net10.0) --- @@ -157,21 +168,25 @@ $targetsCS = @( Name = "SimpleBatch_sample (net10.0)"; Bat = "5_Build_BatCore_sample.bat" Exe = "Samples4NetCore\Legacy\Bat_sample\SimpleBatch_sample\bin\Debug\net10.0\SimpleBatch_sample.dll" Args = $batchArgs; Expect = '\d+件のデータがあります' + NeedDb = $true } @{ Name = "RerunnableBatch_sample (net10.0)"; Bat = "5_Build_BatCore_sample.bat" Exe = "Samples4NetCore\Legacy\Bat_sample\RerunnableBatch_sample\bin\Debug\net10.0\RerunnableBatch_sample.dll" Args = $batchArgs; Pre = $clearOrders2; Verify = $verifyOrders2 + NeedDb = $true } @{ Name = "RerunnableBatch_sample2 (net10.0)"; Bat = "5_Build_BatCore_sample.bat" Exe = "Samples4NetCore\Legacy\Bat_sample\RerunnableBatch_sample2\bin\Debug\net10.0\RerunnableBatch_sample2.dll" Args = $batchArgs; Pre = $clearOrders2; Verify = $verifyOrders2 + NeedDb = $true } @{ Name = "RerunnableBatch_sample3 (net10.0)"; Bat = "5_Build_BatCore_sample.bat" Exe = "Samples4NetCore\Legacy\Bat_sample\RerunnableBatch_sample3\bin\Debug\net10.0\RerunnableBatch_sample3.dll" Args = $batchArgs; Pre = $clearOrders2; Verify = $verifyOrders2 + NeedDb = $true } # --- CLI (net10.0) --- @@ -201,6 +216,7 @@ $targetsCS = @( Name = "TestWebAPIClient (net48)"; Bat = "y_Build_TestWebAPIClient.bat" Exe = "Frameworks\Tests\TestWebAPIClient\net48\bin\Debug\TestWebAPIClientFx.exe" Args = @("http://localhost:51087") + NeedDb = $true Expect = 'NG : 0 件' Pre = { if (-not (Start-ApiWeb "net48" 51087)) { throw "WebAPI のホストを起動できません(port 51087)" } } Verify = { Stop-ApiWeb; return $true } @@ -209,6 +225,7 @@ $targetsCS = @( Name = "TestWebAPIClient (net10.0)"; Bat = "y_Build_TestWebAPIClient.bat" Exe = "Frameworks\Tests\TestWebAPIClient\net48\bin\Debug\TestWebAPIClientFx.exe" Args = @("http://localhost:51088") + NeedDb = $true Expect = 'NG : 0 件' Pre = { if (-not (Start-ApiWeb "net10.0" 51088)) { throw "WebAPI のホストを起動できません(port 51088)" } } Verify = { Stop-ApiWeb; return $true } @@ -232,6 +249,7 @@ $targetsCS = @( Site = "Samples\WebApp_sample\WebForms_Sample\WebForms_Sample" Need = "aspnet_state" Flow = $webFormsFlow + NeedDb = $true } @{ Name = "MVC_Sample (net48)"; Bat = "10_Build_WebApp_sample.bat" @@ -239,12 +257,14 @@ $targetsCS = @( Site = "Samples\WebApp_sample\MVC_Sample\MVC_Sample" Need = "aspnet_state" Flow = $mvcLoginFlow + NeedDb = $true } @{ Name = "MVC_Sample (net10.0)"; Bat = "10_Build_WebAppCore_sample.bat" Kind = "Web"; WebHost = "Kestrel"; Port = 51083 Exe = "Samples4NetCore\Backend\MVC_Sample\MVC_Sample\bin\Debug\net10.0\MVC_Sample.dll" Flow = $mvcLoginFlow + NeedDb = $true } ) @@ -265,21 +285,25 @@ $targetsVB = @( Name = "SimpleBatch_sample (VB net48)"; Bat = "5_Build_Bat_sample.bat" Exe = "Samples\Bat_sample\SimpleBatch_sample\bin\Debug\SimpleBatch_sample.exe" Args = $batchArgs; Expect = '\d+件のデータがあります' + NeedDb = $true } @{ Name = "RerunnableBatch_sample (VB net48)"; Bat = "5_Build_Bat_sample.bat" Exe = "Samples\Bat_sample\RerunnableBatch_sample\bin\Debug\RerunnableBatch_sample.exe" Args = $batchArgs; Pre = $clearOrders2; Verify = $verifyOrders2 + NeedDb = $true } @{ Name = "RerunnableBatch_sample2 (VB net48)"; Bat = "5_Build_Bat_sample.bat" Exe = "Samples\Bat_sample\RerunnableBatch_sample2\bin\Debug\RerunnableBatch_sample2.exe" Args = $batchArgs; Pre = $clearOrders2; Verify = $verifyOrders2 + NeedDb = $true } @{ Name = "RerunnableBatch_sample3 (VB net48)"; Bat = "5_Build_Bat_sample.bat" Exe = "Samples\Bat_sample\RerunnableBatch_sample3\bin\Debug\RerunnableBatch_sample3.exe" Args = $batchArgs; Pre = $clearOrders2; Verify = $verifyOrders2 + NeedDb = $true } # --- Web アプリ --- @@ -289,6 +313,7 @@ $targetsVB = @( Site = "Samples\WebApp_sample\MVC_Sample\MVC_Sample" Need = "aspnet_state" Flow = $mvcLoginFlow + NeedDb = $true } @{ Name = "WebForms_Sample (VB net48)"; Bat = "10_Build_WebApp_sample.bat" @@ -296,5 +321,6 @@ $targetsVB = @( Site = "Samples\WebApp_sample\WebForms_Sample\WebForms_Sample" Need = "aspnet_state" Flow = $webFormsFlow + NeedDb = $true } ) diff --git a/root/programs/st_Utility.ps1 b/root/programs/st_Utility.ps1 index 1359ce208..a3b844ce7 100644 --- a/root/programs/st_Utility.ps1 +++ b/root/programs/st_Utility.ps1 @@ -44,6 +44,67 @@ function Invoke-Sql([string]$sql) finally { $c.Close() } } +# ------------------------------------------------------------------ +# SQL Server に繋がるかの確認(#588) +# ------------------------------------------------------------------ +# **サービス名で確認してはならない。** +# 既定インスタンス(MSSQLSERVER)と名前付き(MSSQL$SQLEXPRESS)で名前が変わり、 +# リモートなら手元にサービスが無い。 +# **決め打つと、CI を誤って「前提未達」にする。** +# サンプルが実際に使う接続文字列で、接続そのものを試す。 +# +# **Connection Timeout を明示する。** +# 既定は 15 秒で、確認のためだけに待つには長い。 +# App.config 側の値に左右されると、**確認に掛かる時間が環境で変わる。** +# 上書きするのはこの確認の中だけで、Invoke-Sql は従来どおりにする。 +function Test-SqlServer([int]$TimeoutSec = 5) +{ + if (-not $connString) { return @{ Ok = $false; Detail = "接続文字列を読めない(App.config)" } } + + $c = $null + try + { + $b = New-Object System.Data.SqlClient.SqlConnectionStringBuilder $connString + + # **$b.ConnectTimeout = ... は動かない。** + # SqlConnectionStringBuilder は IDictionary で、PowerShell の代入が + # プロパティではなくキーへ回るため、 + # Keyword not supported: 'ConnectTimeout'. になる。 + # **接続文字列のキー名で書くこと。** + $b['Connection Timeout'] = $TimeoutSec + + $c = New-Object System.Data.SqlClient.SqlConnection $b.ConnectionString + + # **待ち時間は、こちら側で断ち切る。** + # Connection Timeout だけでは上限にならない。実測では、 + # 到達しないホスト … 約 21 秒(OS の TCP 再送が先に立つ) + # 閉じたポート … 約 12 秒(IPv6/IPv4 と再試行) + # 無いインスタンス … 約 5 秒 + # **確認のために待つ時間を環境任せにしないため**、Wait で打ち切る。 + $task = $c.OpenAsync() + if (-not $task.Wait($TimeoutSec * 1000)) + { + return @{ Ok = $false; Detail = ("応答がありません({0} 秒で打ち切り)" -f $TimeoutSec) } + } + + return @{ Ok = $true; Detail = ("{0} / {1}" -f $c.DataSource, $c.Database) } + } + catch + { + # GetBaseException で SqlException 本体を取る。 + # そのままだと AggregateException や PowerShell の包みが表に出る。 + # 1 行に畳む。例外の本文は改行を含むことがあり、表示が崩れる。 + $msg = ($_.Exception.GetBaseException().Message -replace '\s+', ' ') + if ($msg.Length -gt 72) { $msg = $msg.Substring(0, 72) + " …" } + return @{ Ok = $false; Detail = $msg } + } + finally + { + # 打ち切った場合、接続はまだ進行中のことがある。ここでは黙って閉じる。 + if ($c) { try { $c.Close() } catch { } } + } +} + # ------------------------------------------------------------------ # ログの初期化(#571) # ------------------------------------------------------------------ From c1cf8e020b49c82bb626d53045467b1d96b1ef66 Mon Sep 17 00:00:00 2001 From: nishi_74322014 Date: Sat, 12 Sep 2026 18:18:30 +0900 Subject: [PATCH 3/4] fixed #588 --- .github/workflows/build-windows-vb.yml | 6 +- .github/workflows/build-windows.yml | 6 +- AGENTS.md | 53 +++++++++ root/programs/2_RunAllTests.ps1 | 35 +++++- root/programs/3_SmokeTest.ps1 | 85 ++++++++------- root/programs/CHEATSHEET.md | 8 +- root/programs/Prerequisite.ps1 | 142 +++++++++++++++++++++++++ root/programs/SMOKETEST.md | 24 +++++ root/programs/TESTING.md | 24 ++++- root/programs/st_Utility.ps1 | 79 ++------------ 10 files changed, 341 insertions(+), 121 deletions(-) create mode 100644 root/programs/Prerequisite.ps1 diff --git a/.github/workflows/build-windows-vb.yml b/.github/workflows/build-windows-vb.yml index fdc46a6a1..1067d16a6 100644 --- a/.github/workflows/build-windows-vb.yml +++ b/.github/workflows/build-windows-vb.yml @@ -199,10 +199,14 @@ jobs: # IIS Express は windows-latest に同梱されている(%ProgramFiles% 側)。 # 3_SmokeTest.ps1 が見るのも同じ場所なので、追加の導入は要らない。 + # + # -IncludeUIA : UI を実際に操作する対象も回す(#588)。 + # 手元ではデスクトップを占有して人の作業と衝突するため、既定で外れている。 + # ランナーは使い捨てで衝突する相手が居ないので、ここでは回す。 - name: Smoke test (VB) shell: pwsh working-directory: root/programs - run: .\3_SmokeTest.ps1 -Lang VB -OutputDir "${{ runner.temp }}\SmokeTest" + run: .\3_SmokeTest.ps1 -Lang VB -IncludeUIA -OutputDir "${{ runner.temp }}\SmokeTest" # 失敗時こそ中身を見たいので、常に採取する。 - name: Upload build logs diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 07fe844e0..a23e4237f 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -242,10 +242,14 @@ jobs: # IIS Express は windows-latest(win25-vs2026)に同梱されている。 # C:\Program Files\IIS Express\iisexpress.exe v10.0.26013.1000 # 3_SmokeTest.ps1 が見るのも %ProgramFiles% 側なので、そのまま使える。 + # + # -IncludeUIA : UI を実際に操作する対象も回す(#588)。 + # 手元ではデスクトップを占有して人の作業と衝突するため、既定で外れている。 + # ランナーは使い捨てで衝突する相手が居ないので、ここでは回す。 - name: Smoke test shell: pwsh working-directory: root/programs - run: .\3_SmokeTest.ps1 -OutputDir "${{ runner.temp }}\SmokeTest" + run: .\3_SmokeTest.ps1 -IncludeUIA -OutputDir "${{ runner.temp }}\SmokeTest" # 失敗時こそ中身を見たいので、常に採取する。 - name: Upload build logs diff --git a/AGENTS.md b/AGENTS.md index 414964f7d..c4288c8b6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -92,6 +92,33 @@ gh issue create --repo OpenTouryoProject/OpenTouryo --title --body-file **「利用者への影響」は、無いなら「無し」と明記する。** 空欄だと、確認したのか未確認なのかが読み手に分からない。 +### セキュリティの指摘は、公開される前に確認を取る + +[`SECURITY.md`](SECURITY.md) / [`Security.ja.md`](Security.ja.md) のとおり、 +**セキュリティの問題は公開の Issue に書かない。** +報告は **Private vulnerability reporting**(非公開の security advisory)で行う。 + +https://github.com/OpenTouryoProject/OpenTouryo/security/advisories/new + +**見落としやすいのは、Issue 以外の経路で公開されることである。** +ワーキング ツリーに書いたものは、**コミットした時点で公開になる。** +`ANALYSIS.md`・README・コード中のコメント・コミット メッセージ・PR の本文も同じで、 +未修正の弱点をそこに書くことは、公開の Issue に書くのと同じ効果を持つ。 +**「記録として書いただけ」は、公開しない理由にならない。** + +したがって、**未修正**のセキュリティ上の弱点を見つけたら、次の順で進める。 + +1. **まず人に報告する。** どこに書くか(公開の文書 / 非公開の advisory)を決めるのは人である +2. 合意を得るまで、**攻撃の手順や再現の具体を、ワーキング ツリーのファイルに書かない。** + 書いてよいのは「どの関数で、どの確認が抜けているか」まで +3. 非公開の advisory の番号(`GHSA-…`)は、**公開ファイルに書かない。** 存在そのものが分かるため +4. 公開の Issue として起票してよいかは、**文面を見せて承認を得る**(この節の上の規約と同じ) + +**修正と同じコミットに入るなら、文書に書いてよい。** その時点で攻撃は成立しない。 + +**利用側のリポジトリで見つけた問題でも、原因が Open棟梁 側なら、報告先は Open棟梁 の advisory である。** +サンプル アプリケーション側の文書に書くと、そこで公開されてしまう。 + ### GitHub Actions を書き足すとき **ワークフローはエージェントが書き足すことが多い。厳し目に倒すこと。** @@ -208,6 +235,32 @@ Copyright ブロックの扱い、`ArgumentException` 系の引数の順、`.bat **判断に迷ったら通しでよい。** ただし**迷っていないのに通すのは、ただの浪費である。** +#### 簡易な変更なら、毎回は回さないという選択もある + +**変更が実行されるコードに届いていないなら、通しを回しても何も分からない。** +文書のみ、コメントのみ、表示文言のみといった変更がこれに当たる。 +**検証は目的ではなく、壊していないことを確かめる手段である。** + +| 変更 | 目安 | +|---|---| +| 文書(`.md`)のみ | **回さなくてよい** | +| コメント・表示文言のみ | 変えたスクリプトを `-Only` で 1 件だけ回し、表示を見る | +| ツール・個別サンプル | その対象だけ(依存の末端なので、他は変わらない) | +| 基盤(`Infrastructure/`) | **通し**(上のとおり) | + +**ただし、`.ps1` を変えたなら、表示だけの変更でも実行すること。** +構文エラーは読んでも分からず、**5.1 と 7 で振る舞いが違う** +([`CODING.md`](root/programs/CODING.md) 5 節)。 + +**回さなかったなら、そう報告する。** 「検証済み」と書かない。 +**回していないのに回したように読める報告が、一番危い。** +検収する人は、何が確かめられていないかを知っている必要がある。 + +> **CI(GitHub Actions)はこれとは別である。** +> push すれば回るので、手元で省いた分はそこで拾われる。 +> ただし**結果が出るのはコミットの後**なので、 +> 実行されるコードを変えたなら、手元で見てから渡すこと。 + **上記の既定は C# 側である。VB 側に手を入れたときは `-Lang` で回す。** 理由と対象は [`BUILDING.md`](root/programs/BUILDING.md) 10 節・ [`SMOKETEST.md`](root/programs/SMOKETEST.md) 10 節。 diff --git a/root/programs/2_RunAllTests.ps1 b/root/programs/2_RunAllTests.ps1 index fa018d64f..5daa9a426 100644 --- a/root/programs/2_RunAllTests.ps1 +++ b/root/programs/2_RunAllTests.ps1 @@ -29,7 +29,14 @@ <前提> ・Frameworks をビルド済み(Build_net48 / Build_netcore100 が存在すること) - ・SQL Server の Northwind に接続できること(TestBatch が使用) + ・SQL Server の Northwind に接続できること + (TestDataAccess と TestBatch が使用。冒頭で確認する) + + **ASP.NET 状態サービス(aspnet_state)は要らない。** + System.Web / System.Web.Extensions は csproj の参照だけで、 + HttpContext も sessionState も使っていない。 + CI も、このスクリプトの**後**で aspnet_state を開始している + (build-windows.yml)。要るのは 3_SmokeTest.ps1 の Web アプリだけ。 <副作用> ワーキング ツリーの Result*.txt が書き換わる。これは従来のバッチ運用と同じで、 @@ -96,6 +103,7 @@ New-Item -ItemType Directory -Force $OutputDir | Out-Null # サマリの整形。Format-Table は 5.1 で全角の桁を数えないため、自前で揃える。 . (Join-Path $PSScriptRoot "SummaryTable.ps1") +. (Join-Path $PSScriptRoot "Prerequisite.ps1") # ------------------------------------------------------------------ # コンソールのコード ページを先に UTF-8 にしておく @@ -181,6 +189,8 @@ function Copy-TestCertificates # **伏せると、その範囲の変化は差分に出ない。** # 識別子(IsHankaku/IsZenkaku など)にも当たるため、 # **実行のたびに値が変わるテストだけ $true にする。** +# NeedDb : SQL Server の Northwind を使うか(#588) +# **単体テストも DB を使う。** 冒頭でまとめて確認する。 $tests = @( @{ Name = "TestCode (net48)"; Bat = "y_Build_TestCode_Public.bat" @@ -193,18 +203,22 @@ $tests = @( @{ Name = "TestDataAccess (net48)"; Bat = "y_Build_TestCode_DataAccess.bat" Result = "TestDataAccess\Result48.txt"; SkipLog4net = $false; NormBase64 = $false + NeedDb = $true } @{ Name = "TestDataAccess (net10.0)"; Bat = "y_Build_TestCode_DataAccess.bat" Result = "TestDataAccess\ResultCore100.txt"; SkipLog4net = $false; NormBase64 = $false + NeedDb = $true } @{ Name = "TestBatch (net48)"; Bat = "y_Build_TestCode_Batch.bat" Result = "TestBatch\ResultSimpleBatch48.txt"; SkipLog4net = $true; NormBase64 = $false + NeedDb = $true } @{ Name = "TestBatch (net10.0)"; Bat = "y_Build_TestCode_Batch.bat" Result = "TestBatch\ResultSimpleBatchCore100.txt"; SkipLog4net = $true; NormBase64 = $false + NeedDb = $true } @{ Name = "EncAndDecUtilCUI (net48)"; Bat = "y_Build_TestCode_SecCUI.bat" @@ -247,6 +261,25 @@ if ($Only) Write-Host (" -Only '$Only' : {0} 件に絞りました" -f $tests.Count) -ForegroundColor Yellow } +# ------------------------------------------------------------------ +# 前提の確認(#588) +# ------------------------------------------------------------------ +# **単体テストも DB を使う。** TestDataAccess と TestBatch が Northwind に繋ぐ。 +# 落ちるまで分からないと、差分を見て初めて気付くことになる。 +# +# **接続文字列は TestDataAccess の App.config から読む。** +# 対象が実際に使う設定を見る(3_SmokeTest.ps1 がサンプルの +# App.config を見るのと同じ理屈)。 +# +# **aspnet_state は見ない。** 単体テストは ASP.NET を使わない +# (上の<前提>を参照)。 +$needDb = @($tests | Where-Object { $_.NeedDb }).Count -gt 0 + +if ($needDb) +{ + Show-Prerequisites -DbConfig (Join-Path $testsRoot "TestDataAccess\App.config") +} + # ------------------------------------------------------------------ # 期待値(HEAD 版)の取り出し # ------------------------------------------------------------------ diff --git a/root/programs/3_SmokeTest.ps1 b/root/programs/3_SmokeTest.ps1 index 6fa005c6e..a849068b1 100644 --- a/root/programs/3_SmokeTest.ps1 +++ b/root/programs/3_SmokeTest.ps1 @@ -55,6 +55,13 @@ .PARAMETER OutputDir ログの保存先。既定は %TEMP%\OpenTouryoSmokeTest。 +.PARAMETER IncludeUIA + UI を実際に操作する対象(UIA 系)も回す。**既定では回さない。**(#588) + + **デスクトップを占有するため。** キー入力やフォーカスを奪うので、 + 人が作業中の端末で回すと、その作業と衝突して両方が壊れる。 + **CI(GitHub Actions)で回すのが本筋で、手元では明示的に指定したときだけ。** + .EXAMPLE .\3_SmokeTest.ps1 @@ -82,6 +89,7 @@ param( [string]$Only, [switch]$List, [switch]$SkipBuild, + [switch]$IncludeUIA, [string]$OutputDir = (Join-Path $env:TEMP "OpenTouryoSmokeTest") ) @@ -152,6 +160,9 @@ if ([Console]::OutputEncoding.CodePage -ne 65001) # **ドット ソースで読む。** 関数と変数を、この実行スコープへ入れるため。 # # **順序が要る。** st_Targets.ps1 は他の 3 つが定義したものを参照する。 +# **st_Utility.ps1 より先に読む。**(#588) +# st_Utility.ps1 が読み込み時に Get-ConnectionStringFromConfig を使う。 +. (Join-Path $PSScriptRoot "Prerequisite.ps1") . (Join-Path $PSScriptRoot "st_Utility.ps1") . (Join-Path $PSScriptRoot "st_Server.ps1") . (Join-Path $PSScriptRoot "st_Flow.ps1") @@ -307,58 +318,46 @@ if ($Only) Write-Host (" -Only '$Only' : {0} 件に絞りました" -f $selected.Count) -ForegroundColor Yellow } -# ------------------------------------------------------------------ -# 前提の確認(#588) -# ------------------------------------------------------------------ -# **冒頭で見る。** 対象ごとの確認だけだと、aspnet_state のように -# 終盤でしか使わないものは、気付くまでに数分かかる。 +# --- UIA 系は既定で外す(#588)--- # -# **表示だけで、判定は変えない。** 足りなくてもここでは止めない -# (対象ごとの「前提未達」は従来どおり出る)。 -# **開始も停止もしない。** システムの状態を変える操作だからで、 -# CI は専用のステップで開始している(SMOKETEST.md 4 節・AGENTS.md の線引き)。 +# **UI を実際に操作する対象は、デスクトップを占有する。** +# キー入力やフォーカスを奪うため、人が作業中の端末で回すと +# **その作業と衝突して、作業もテストも壊れる。** +# 使い捨てのランナー(CI)にはこの問題が無いので、そちらで回す。 # -# **選んだ対象が要るものだけを見る。** -Only で絞ったときに、 -# 無関係なサービスの不足を報告しない。 -$needSvc = @($selected | ForEach-Object { $_.Need } | Where-Object { $_ } | Select-Object -Unique) -$needDb = @($selected | Where-Object { $_.NeedDb }).Count -gt 0 - -if ($needSvc.Count -gt 0 -or $needDb) +# **-Only の空振り判定より後ろに置く。** +# 先に外すと、一致していたのに「一致する対象がありません」と出てしまう。 +# +# **黙って減らさない。** 外した件数を出す。 +# 出さないと、外れていることに気付けないまま「全対象 OK」になる。 +if (-not $IncludeUIA) { - Write-Host "" - Write-Host "=== 前提の確認 ===" -ForegroundColor Cyan + $uia = @($selected | Where-Object { $_.UIA }) - $missing = @() - - foreach ($name in $needSvc) + if ($uia.Count -gt 0) { - $svc = Get-Service $name -EA SilentlyContinue - $ok = ($svc -and $svc.Status -eq "Running") - $detail = if ($ok) { "Running" } elseif ($svc) { [string]$svc.Status } else { "未導入" } - - Write-Host (" {0,-12} {1}" -f $name, $detail) ` - -ForegroundColor $(if ($ok) { "Green" } else { "Yellow" }) + $selected = @($selected | Where-Object { -not $_.UIA }) + Write-Host (" UIA 系 {0} 件を外しました(回すなら -IncludeUIA)" -f $uia.Count) -ForegroundColor Yellow - if (-not $ok) { $missing += ("Start-Service {0} # 管理者権限が必要" -f $name) } + if ($selected.Count -eq 0) + { + Write-Host " **残った対象がありません。**" -ForegroundColor Red + Write-Host " UIA 系だけを選んでいます。-IncludeUIA を付けてください。" -ForegroundColor Yellow + exit 1 + } } +} - if ($needDb) - { - # 接続で見る(サービス名では見ない)。理由は st_Utility.ps1 の Test-SqlServer。 - $db = Test-SqlServer 5 - Write-Host (" {0,-12} {1}" -f "SQL Server", $db.Detail) ` - -ForegroundColor $(if ($db.Ok) { "Green" } else { "Yellow" }) - - if (-not $db.Ok) { $missing += "SQL Server の Northwind に接続できること(SMOKETEST.md 4 節)" } - } +# ------------------------------------------------------------------ +# 前提の確認(#588) +# ------------------------------------------------------------------ +# **選んだ対象が要るものだけを見る。** -Only で絞ったときに、 +# 無関係なサービスの不足を報告しない。 +# 表示だけで判定を変えないことは Prerequisite.ps1 側の責任。 +$needSvc = @($selected | ForEach-Object { $_.Need } | Where-Object { $_ } | Select-Object -Unique) +$needDb = @($selected | Where-Object { $_.NeedDb }).Count -gt 0 - if ($missing.Count -gt 0) - { - Write-Host "" - Write-Host " **前提が足りません。このまま進めますが、該当の対象は NG になります。**" -ForegroundColor Yellow - $missing | ForEach-Object { Write-Host (" " + $_) } - } -} +Show-Prerequisites -Services $needSvc -DbConfig $(if ($needDb) { $sampleConfig }) if (-not $SkipBuild) { diff --git a/root/programs/CHEATSHEET.md b/root/programs/CHEATSHEET.md index 5341576c8..81197dee1 100644 --- a/root/programs/CHEATSHEET.md +++ b/root/programs/CHEATSHEET.md @@ -81,7 +81,13 @@ cd root\programs | `Tools/`(ツール) | `-Only Framework_Tool` | **不要** | `-Only <そのツール>` | | 個別サンプル | `-Only <サンプル>` | **不要** | `-Only <サンプル>` | | `Tests/` | `-Only <対象>` | `-Only <対象>` | **不要** | -| `.ps1` / `.md` のみ | **不要** | **不要** | **不要** | +| `.md` のみ | **不要** | **不要** | **不要** | +| `.ps1` のみ | 変えた本を `-Only` で 1 件 | 同左 | 同左 | + +**`.ps1` は、表示だけの変更でも実行する**(#588)。 +構文エラーは読んでも分からず、**5.1 と 7 で振る舞いが違う** +([`CODING.md`](CODING.md) 5 節)。 +**回さなかったなら、そう報告する(「検証済み」と書かない)。** **`2_RunAllTests.ps1` の対象はフレームワークのテストだけ**で、 ツールやサンプルを変えても動かない。**回す理由が無い。** diff --git a/root/programs/Prerequisite.ps1 b/root/programs/Prerequisite.ps1 new file mode 100644 index 000000000..1df47e236 --- /dev/null +++ b/root/programs/Prerequisite.ps1 @@ -0,0 +1,142 @@ +<# +.SYNOPSIS + 検証の前提(サービス・DB)の確認(#588) + +.DESCRIPTION + **2_RunAllTests.ps1 と 3_SmokeTest.ps1 の両方からドット ソースで読まれる。** + スモークだけでなく、**単体テストも DB を使う**ため + (TestDataAccess と TestBatch)、共有の場所に置いている。 + 片方だけに置くと、もう片方で複写することになる。 + + **表示だけで、判定は変えない。** + 足りなくても止めず、終了コードにも影響しない。 + **開始も停止もしない。** システムの状態を変える操作だからで、 + CI は専用のステップで開始している + (SMOKETEST.md 4 節・AGENTS.md の線引き)。 +#> + +# ------------------------------------------------------------------ +# 接続文字列を設定ファイルから読む +# ------------------------------------------------------------------ +# **対象が実際に使う設定から読む。** +# ここで別途ハードコードすると、対象側の変更に追随できなくなる。 +function Get-ConnectionStringFromConfig([string]$ConfigPath) +{ + if (-not $ConfigPath -or -not (Test-Path $ConfigPath)) { return $null } + + $xml = [xml](Get-Content $ConfigPath -Raw -Encoding UTF8) + $node = $xml.configuration.connectionStrings.add | + Where-Object { $_.name -eq "ConnectionString_SQL" } + + return $node.connectionString +} + +# ------------------------------------------------------------------ +# SQL Server に繋がるかの確認 +# ------------------------------------------------------------------ +# **サービス名で確認してはならない。** +# 既定インスタンス(MSSQLSERVER)と名前付き(MSSQL$SQLEXPRESS)で名前が変わり、 +# リモートなら手元にサービスが無い。 +# **決め打つと、CI を誤って「前提未達」にする。** +# 接続文字列で、接続そのものを試す。 +function Test-SqlServer([string]$ConnString, [int]$TimeoutSec = 5) +{ + if (-not $ConnString) { return @{ Ok = $false; Detail = "接続文字列を読めない" } } + + $c = $null + try + { + $b = New-Object System.Data.SqlClient.SqlConnectionStringBuilder $ConnString + + # **$b.ConnectTimeout = ... は動かない。** + # SqlConnectionStringBuilder は IDictionary で、PowerShell の代入が + # プロパティではなくキーへ回るため、 + # Keyword not supported: 'ConnectTimeout'. になる。 + # **接続文字列のキー名で書くこと。** + $b['Connection Timeout'] = $TimeoutSec + + $c = New-Object System.Data.SqlClient.SqlConnection $b.ConnectionString + + # **待ち時間は、こちら側で断ち切る。** + # Connection Timeout だけでは上限にならない。実測では、 + # 到達しないホスト … 約 21 秒(OS の TCP 再送が先に立つ) + # 閉じたポート … 約 12 秒(IPv6/IPv4 と再試行) + # 無いインスタンス … 約 5 秒 + # **確認のために待つ時間を環境任せにしないため**、Wait で打ち切る。 + $task = $c.OpenAsync() + if (-not $task.Wait($TimeoutSec * 1000)) + { + return @{ Ok = $false; Detail = ("応答がありません({0} 秒で打ち切り)" -f $TimeoutSec) } + } + + return @{ Ok = $true; Detail = ("{0} / {1}" -f $c.DataSource, $c.Database) } + } + catch + { + # GetBaseException で SqlException 本体を取る。 + # そのままだと AggregateException や PowerShell の包みが表に出る。 + # 1 行に畳む。例外の本文は改行を含むことがあり、表示が崩れる。 + $msg = ($_.Exception.GetBaseException().Message -replace '\s+', ' ') + if ($msg.Length -gt 72) { $msg = $msg.Substring(0, 72) + " …" } + return @{ Ok = $false; Detail = $msg } + } + finally + { + # 打ち切った場合、接続はまだ進行中のことがある。ここでは黙って閉じる。 + if ($c) { try { $c.Close() } catch { } } + } +} + +# ------------------------------------------------------------------ +# 前提の一覧を出す +# ------------------------------------------------------------------ +# **冒頭で見る。** 対象ごとに見るだけだと、終盤でしか使わないものは +# 気付くまでに数分かかる(aspnet_state は約 7 分後)。 +# +# **引数で渡されたものだけを見る。** 呼ぶ側が「選ばれた対象が要るもの」を +# 絞って渡すこと。-Only で絞ったときに、無関係な不足を報告しないため。 +function Show-Prerequisites +{ + param( + [string[]]$Services = @(), + [string]$DbConfig, + [int]$TimeoutSec = 5 + ) + + if ($Services.Count -eq 0 -and -not $DbConfig) { return } + + Write-Host "" + Write-Host "=== 前提の確認 ===" -ForegroundColor Cyan + + $missing = @() + + foreach ($name in $Services) + { + $svc = Get-Service $name -EA SilentlyContinue + $ok = ($svc -and $svc.Status -eq "Running") + $detail = if ($ok) { "Running" } elseif ($svc) { [string]$svc.Status } else { "未導入" } + + Write-Host (" {0,-12} {1}" -f $name, $detail) ` + -ForegroundColor $(if ($ok) { "Green" } else { "Yellow" }) + + if (-not $ok) { $missing += ("Start-Service {0} # 管理者権限が必要" -f $name) } + } + + if ($DbConfig) + { + $cs = Get-ConnectionStringFromConfig $DbConfig + $db = Test-SqlServer $cs $TimeoutSec + + Write-Host (" {0,-12} {1}" -f "SQL Server", $db.Detail) ` + -ForegroundColor $(if ($db.Ok) { "Green" } else { "Yellow" }) + + if (-not $db.Ok) { $missing += "SQL Server の Northwind に接続できること(SMOKETEST.md 4 節)" } + } + + if ($missing.Count -gt 0) + { + Write-Host "" + Write-Host " **前提が足りません。このまま進めますが、該当の対象は NG になります。**" -ForegroundColor Yellow + $missing | ForEach-Object { Write-Host (" " + $_) } + } +} diff --git a/root/programs/SMOKETEST.md b/root/programs/SMOKETEST.md index 3dd2cb010..1717a9eb2 100644 --- a/root/programs/SMOKETEST.md +++ b/root/programs/SMOKETEST.md @@ -598,6 +598,29 @@ Start-Service aspnet_state # 管理者権限が必要 - **対象側で宣言する。** `st_Targets.ps1` の `Need`(サービス名)と `NeedDb`(SQL Server を使うか)。**対象を追加するときは、ここも見る**(9 節) +### UI を操作する対象(UIA 系)は、既定で回さない(#588) + +**現時点で該当する対象は無い。** 30 件すべてコンソールか HTTP で、 +画面を操作するものは無い。**先に仕組みだけ入れてある。** + +対象側で `UIA = $true` と宣言すると、**既定では外れる。** + +```powershell +.\3_SmokeTest.ps1 # UIA 系は外れる(外した件数を表示) +.\3_SmokeTest.ps1 -IncludeUIA # 回す +``` + +**デスクトップを占有するからである。** +UI を実際に操作するテストはキー入力やフォーカスを奪うので、 +**人が作業中の端末で回すと、その作業と衝突して両方が壊れる。** +テストが誰かのクリックを拾って失敗し、人の方は勝手にキーを持っていかれる。 + +**CI で回すのが本筋。** ランナーは使い捨てで衝突する相手が居ない。 +`build-windows.yml` / `build-windows-vb.yml` は `-IncludeUIA` を渡している。 + +> **黙って減らさない。** 外した件数を表示する。 +> 出さないと、外れていることに気付けないまま「全対象 OK」になる。 + --- ## 5. 判定基準 @@ -814,6 +837,7 @@ MVC_Sample (net10.0) OK ログイン後 /Crud1/Index = 200 | `Site` / `Port` / `Flow` | Web アプリ用 | | `Need` | 必要な Windows サービス名(`aspnet_state`。**名前が固定のものだけ**) | | `NeedDb` | SQL Server の Northwind を使うなら `$true`(#588) | +| `UIA` | UI を実際に操作するなら `$true`。**既定では回さない**(#588。後述) | **判定条件は「動いていれば必ず満たす」ものにする。** 実行のたびに変わる値(件数以外の可変値、日時など)を条件に入れると、 diff --git a/root/programs/TESTING.md b/root/programs/TESTING.md index dfaa61532..d437df299 100644 --- a/root/programs/TESTING.md +++ b/root/programs/TESTING.md @@ -143,9 +143,31 @@ SimpleBatch.exe /Dap SQL ... > ..\..\..\ResultSimpleBatch48.txt ## 3. 前提条件 - **Frameworks をビルド済み**であること(`Build_net48` / `Build_netcore100` が存在) -- **SQL Server の Northwind に接続できる**こと(SimpleBatch が使用) +- **SQL Server の Northwind に接続できる**こと + (`TestDataAccess` と `TestBatch` が使用。**8 本中 4 本**) - **Northwind のテスト データが標準状態**であること(後述の「4. テスト データ」) +### 冒頭の「前提の確認」(#588) + +**落ちるまで分からないと、差分を見て初めて気付くことになる。** +`2_RunAllTests.ps1` は、DB を使う対象が選ばれているときだけ、実行の冒頭で確認する。 + +``` +=== 前提の確認 === + SQL Server localhost / Northwind +``` + +- **接続文字列は `TestDataAccess\App.config` から読む**(対象が実際に使う設定) +- **サービス名では見ない。** 理由とタイムアウトの扱いは + [`SMOKETEST.md`](SMOKETEST.md) 4 節と共通(実体も `Prerequisite.ps1` で共有) +- **表示だけで、判定は変えない。** 足りなくても止めない + +> **ASP.NET 状態サービス(`aspnet_state`)は要らない。** +> `System.Web` / `System.Web.Extensions` は csproj の**参照だけ**で、 +> `HttpContext` も `sessionState` も使っていない。 +> CI も、単体テストの**後**で `aspnet_state` を開始している +> (`build-windows.yml`)。要るのは `3_SmokeTest.ps1` の Web アプリだけ。 + > **GitHub Actions でも実行している。** 前提の揃え方(SQL Server の導入、Northwind の > ロード、照合順序)は [`BUILDING.md`](BUILDING.md) 9 節が一次情報。 > **期待値は開発環境で生成されたもの**なので、CI 側の環境がずれると差分が出る。 diff --git a/root/programs/st_Utility.ps1 b/root/programs/st_Utility.ps1 index a3b844ce7..50fcae30b 100644 --- a/root/programs/st_Utility.ps1 +++ b/root/programs/st_Utility.ps1 @@ -18,18 +18,12 @@ # ------------------------------------------------------------------ # サンプルが実際に使う App.config から読む。 # ここで別途ハードコードすると、サンプル側の変更に追随できなくなる。 -function Get-SampleConnectionString -{ - $config = Join-Path $configRoot "Samples\Bat_sample\SimpleBatch_sample\App.config" - if (-not (Test-Path $config)) { return $null } - - $xml = [xml](Get-Content $config -Raw) - $node = $xml.configuration.connectionStrings.add | - Where-Object { $_.name -eq "ConnectionString_SQL" } - return $node.connectionString -} - -$connString = Get-SampleConnectionString +# +# **読み出し自体は Prerequisite.ps1 にある。**(#588) +# 2_RunAllTests.ps1 も同じことをする(単体テストも DB を使う)ため、 +# 片方に置くと複写になる。 +$sampleConfig = Join-Path $configRoot "Samples\Bat_sample\SimpleBatch_sample\App.config" +$connString = Get-ConnectionStringFromConfig $sampleConfig function Invoke-Sql([string]$sql) { @@ -44,67 +38,6 @@ function Invoke-Sql([string]$sql) finally { $c.Close() } } -# ------------------------------------------------------------------ -# SQL Server に繋がるかの確認(#588) -# ------------------------------------------------------------------ -# **サービス名で確認してはならない。** -# 既定インスタンス(MSSQLSERVER)と名前付き(MSSQL$SQLEXPRESS)で名前が変わり、 -# リモートなら手元にサービスが無い。 -# **決め打つと、CI を誤って「前提未達」にする。** -# サンプルが実際に使う接続文字列で、接続そのものを試す。 -# -# **Connection Timeout を明示する。** -# 既定は 15 秒で、確認のためだけに待つには長い。 -# App.config 側の値に左右されると、**確認に掛かる時間が環境で変わる。** -# 上書きするのはこの確認の中だけで、Invoke-Sql は従来どおりにする。 -function Test-SqlServer([int]$TimeoutSec = 5) -{ - if (-not $connString) { return @{ Ok = $false; Detail = "接続文字列を読めない(App.config)" } } - - $c = $null - try - { - $b = New-Object System.Data.SqlClient.SqlConnectionStringBuilder $connString - - # **$b.ConnectTimeout = ... は動かない。** - # SqlConnectionStringBuilder は IDictionary で、PowerShell の代入が - # プロパティではなくキーへ回るため、 - # Keyword not supported: 'ConnectTimeout'. になる。 - # **接続文字列のキー名で書くこと。** - $b['Connection Timeout'] = $TimeoutSec - - $c = New-Object System.Data.SqlClient.SqlConnection $b.ConnectionString - - # **待ち時間は、こちら側で断ち切る。** - # Connection Timeout だけでは上限にならない。実測では、 - # 到達しないホスト … 約 21 秒(OS の TCP 再送が先に立つ) - # 閉じたポート … 約 12 秒(IPv6/IPv4 と再試行) - # 無いインスタンス … 約 5 秒 - # **確認のために待つ時間を環境任せにしないため**、Wait で打ち切る。 - $task = $c.OpenAsync() - if (-not $task.Wait($TimeoutSec * 1000)) - { - return @{ Ok = $false; Detail = ("応答がありません({0} 秒で打ち切り)" -f $TimeoutSec) } - } - - return @{ Ok = $true; Detail = ("{0} / {1}" -f $c.DataSource, $c.Database) } - } - catch - { - # GetBaseException で SqlException 本体を取る。 - # そのままだと AggregateException や PowerShell の包みが表に出る。 - # 1 行に畳む。例外の本文は改行を含むことがあり、表示が崩れる。 - $msg = ($_.Exception.GetBaseException().Message -replace '\s+', ' ') - if ($msg.Length -gt 72) { $msg = $msg.Substring(0, 72) + " …" } - return @{ Ok = $false; Detail = $msg } - } - finally - { - # 打ち切った場合、接続はまだ進行中のことがある。ここでは黙って閉じる。 - if ($c) { try { $c.Close() } catch { } } - } -} - # ------------------------------------------------------------------ # ログの初期化(#571) # ------------------------------------------------------------------ From 9379239c01c2fdfef7fccde8b8022cf1aec64f29 Mon Sep 17 00:00:00 2001 From: nishi_74322014 <nishi_74322014@ksj.biglobe.ne.jp> Date: Sat, 12 Sep 2026 20:10:58 +0900 Subject: [PATCH 4/4] fixed #586 --- .../Authentication/AuthenticationHeader.cs | 47 ++++++++++++++----- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/root/programs/CS/Frameworks/Infrastructure/Framework/Authentication/AuthenticationHeader.cs b/root/programs/CS/Frameworks/Infrastructure/Framework/Authentication/AuthenticationHeader.cs index 52197f400..eef4a5a58 100644 --- a/root/programs/CS/Frameworks/Infrastructure/Framework/Authentication/AuthenticationHeader.cs +++ b/root/programs/CS/Frameworks/Infrastructure/Framework/Authentication/AuthenticationHeader.cs @@ -28,6 +28,8 @@ //* 日時 更新者 内容 //* ---------- ---------------- ------------------------------------------------- //* 2018/12/26 西野 大介 新規作成 +//* 2026/09/12 玄人 幸道 方式の後ろに値の無い Authorization ヘッダで +//* 例外になっていたのを修正 //********************************************************************************** using System; @@ -94,24 +96,47 @@ public static bool GetCredentials(string authHeader, out string bearerToken) /// <param name="authHeader">string</param> /// <param name="credentials">string[]</param> /// <returns>AuthenticationScheme</returns> + /// <remarks> + /// 資格情報を取り出せない場合は、空の配列と "" を返す。 + /// 呼び出し側は、それを受けて 401 を返せる。 + /// </remarks> public static string GetCredentials(string authHeader, out string[] credentials) { if (!string.IsNullOrEmpty(authHeader)) { string[] temp = authHeader.Split(' '); - if (temp[0] == OAuth2AndOIDCConst.Basic) + // 方式の後ろに値が無い("Bearer" や "Bearer " など)場合は、 + // 資格情報の無い要求として扱う。 + // 以前は temp[1] を確かめずに読み、IndexOutOfRangeException になっていた。 + if (temp.Length >= 2 && !string.IsNullOrEmpty(temp[1])) { - credentials = CustomEncode.ByteToString( - CustomEncode.FromBase64String(temp[1]), CustomEncode.us_ascii).Split(':'); - - return OAuth2AndOIDCConst.Basic; - } - else if (temp[0] == OAuth2AndOIDCConst.Bearer) - { - credentials = new string[] { temp[1] }; - - return OAuth2AndOIDCConst.Bearer; + if (temp[0] == OAuth2AndOIDCConst.Basic) + { + try + { + credentials = CustomEncode.ByteToString( + CustomEncode.FromBase64String(temp[1]), CustomEncode.us_ascii).Split(':'); + + return OAuth2AndOIDCConst.Basic; + } + catch (FormatException) + { + // Base64 として読めない。資格情報が無いものとして扱う。 + // + // **ここで throw すると、呼び出し側は 401 ではなく 500 を返すことになり、 + // クライアントは「サーバの不具合」と「認証の失敗」を区別できない。** + // + // Convert.TryFromBase64String は net48 に無いため、ここは try/catch で受ける + // (このファイルは net48 / net10.0 の共通コード)。 + } + } + else if (temp[0] == OAuth2AndOIDCConst.Bearer) + { + credentials = new string[] { temp[1] }; + + return OAuth2AndOIDCConst.Bearer; + } } }