hawksnowlog
2026年2月22日日曜日
2026年2月16日月曜日
Github actions で kitchen test をする場合は buildkit を無効にしなければ動かなくなった
概要
おそらく本当に最近仕様変更があった模様
環境
- Github actions (2026/02/13時点)
- Ruby 4.0.1
- test-kitchen 3.9.1
- kitchen-docker 3.0.0
Github actions の設定
name: Kitchen test
on:
push:
branches:
- "master"
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: linux/arm64
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Install dependencies
run: bundle install
- name: Run Test Kitchen
run: |
bundle exec kitchen test
env:
CHEF_LICENSE: accept
DOCKER_BUILDKIT: 0
デバッグログを表示する場合
name: Kitchen test
on:
push:
branches:
- "master"
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: linux/arm64
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Install dependencies
run: bundle install
- name: Run Test Kitchen
run: |
bundle exec kitchen test --log-level debug || true
env:
CHEF_LICENSE: accept
DOCKER_BUILDKIT: 0
- name: Show Kitchen logs
if: always()
run: |
echo "===== kitchen.log ====="
cat .kitchen/logs/kitchen.log || true
echo "===== instance log ====="
cat .kitchen/logs/default-ubuntu-2204.log || true
最後に
内部的に buildkit を使うようになっており docker の出力形式が変更されたために起きていたエラーでした
2026年2月14日土曜日
スマホ版の Chrome ではちゃんと DOM を定義してから値を設定したりするほうがいい
概要
動的に appendChild とかするとリソースを消費しすぎと判断して描画を強制的に停止するようです
環境
- iOS 26.2.1
- Chrome 144.07559.95
ダメなコード
function addMessage(msg) {
// avoid duplicates; update indicator if already present
const existing = document.querySelector(`#log li[data-msg-id="${msg.id}"]`);
if (existing) {
const ind = existing.querySelector(".read-indicator");
if (ind) {
updateIndicator(ind, existing, msg.reads || []);
}
return;
}
// 新着メッセージ(相手からのもの)に対する通知・未読処理
if (msg.message && msg.client_id !== CLIENT_ID) {
// ブラウザ通知の呼び出し
showBrowserNotification(msg);
// タブが裏側にある場合、未読カウントを増やす
if (document.visibilityState !== "visible") {
unreadCount++;
updateTabTitle();
}
}
// new message
const li = document.createElement("li");
li.className = msg.client_id === CLIENT_ID ? "me" : "other";
li.dataset.msgId = msg.id;
const textSpan = document.createElement("span");
textSpan.className = "msg-text";
textSpan.textContent = msg.message;
const ind = document.createElement("span");
ind.className = "read-indicator";
if (msg.client_id === CLIENT_ID) {
// 自分のメッセージ: サーバーの reads 情報を使って表示
updateIndicator(ind, li, msg.reads || []);
li.appendChild(textSpan);
li.appendChild(ind);
} else {
// 相手のメッセージ
updateIndicator(ind, li, msg.reads || []);
li.addEventListener("click", () => {
if (isRead(msg.id)) return;
setRead(msg.id);
ind.classList.remove("unread");
ind.classList.add("read");
ind.title = "既読";
// notify server
markRead(msg.id);
});
li.appendChild(textSpan);
li.appendChild(ind);
}
document.getElementById("log").appendChild(li);
}
良いコード
function addMessage(msg) {
// 1. 基本チェック
if (!msg || !msg.id) return;
// read_event は applyReadEvent で処理されるため、ここではメッセージ本体がないものを除外
if (msg.type === "read_event" || !msg.message) return;
// 2. 重複チェック
const log = document.getElementById("log");
if (!log) return;
const existing = document.querySelector(`#log li[data-msg-id="${msg.id}"]`);
if (existing) return;
// 3. 要素の作成
const li = document.createElement("li");
// 安全な ID チェック
const currentId =
typeof CLIENT_ID !== "undefined" ? CLIENT_ID : getClientId();
li.className = msg.client_id === currentId ? "me" : "other";
li.setAttribute("data-msg-id", msg.id); // dataset ではなく setAttribute を使用(古いブラウザ対策)
const textSpan = document.createElement("span");
textSpan.className = "msg-text";
textSpan.textContent = String(msg.message); // 明示的に文字列変換
const ind = document.createElement("span");
ind.className = "read-indicator unread"; // デフォルトは unread
// 4. 要素の組み立て(先に DOM に追加してから細部を調整するのがスマホでは安定します)
li.appendChild(textSpan);
li.appendChild(ind);
log.appendChild(li);
// 5. 既読インジケーターの更新(エラーが起きても表示自体は維持する)
try {
updateIndicator(ind, li, msg.reads || []);
} catch (e) {
console.warn("Indicator error ignored for safety", e);
}
// 6. イベントリスナー(相手のメッセージのみ)
if (msg.client_id !== currentId) {
li.addEventListener("click", function () {
try {
if (isRead(msg.id)) return;
setRead(msg.id);
ind.classList.remove("unread");
ind.classList.add("read");
markRead(msg.id);
} catch (err) {
console.error(err);
}
});
// 通知処理(エラー回避のため try-catch)
try {
showBrowserNotification(msg);
} catch (e) {}
}
// 7. スクロール
log.scrollTop = log.scrollHeight;
}
比較
堅牢性とエラーハンドリング
「良いコード」は、外部要因でプログラムが止まらないよう工夫されています。
| 項目 | ダメなコード | 良いコード |
|---|---|---|
| ガード句 | データの存在チェックが甘く、msg.idが欠落しているとエラーになる可能性がある。 | 冒頭で `!msg |
| 例外処理 | updateIndicator 等でエラーが起きると、後続の処理(DOM追加等)が止まる。 | try-catch を活用し、一部の表示エラーが起きてもアプリ全体が止まらない。 |
| 変数の安全性 | CLIENT_ID がグローバルにある前提。 | typeof チェックやフォールバック(getClientId())を用意している。 |
責務の明確化と可読性
コードが整理されているため、後からの修正が容易です。
- 単一責任の原則:
- ダメ: addMessage 内で通知、未読カウント、DOM操作、重複チェックが混ざり、条件分岐(if (msg.client_id === CLIENT_ID) … else …)で同じような appendChild が重複している。
- 良い: メッセージの型判定(read_event かどうか)を fetchMessages 側で仕分け、addMessage は「表示」に専念している。
- DRY (Don’t Repeat Yourself):
- ダメ: 自分のメッセージと相手のメッセージで、インジケーターの追加処理を2回書いている。
- 良い: 共通の組み立て(li への追加)を先に行い、差分(イベントリスナー)だけを条件分岐で書いている。
ユーザー体験 (UX) と実用性
ブラウザやネットワークの特性を考慮した実装になっています。
- スクロール処理: 「良いコード」では追加後に log.scrollTop = log.scrollHeight を行い、常に最新メッセージが見えるように配慮されています。
- 通信の最適化: fetchMessages でキャッシュ回避のためのタイムスタンプを追加しており、古いデータを掴まされるリスクを減らしています。
- 暗黙の型変換への対処: String(msg.message) とすることで、数値や null が送られてきても textContent で安全に表示できます。
改善のポイント(まとめ)
- ❌ ダメな点
- 重複コード: li.appendChild(textSpan) などが if/else 両方にあり、修正漏れの原因になる。
- 不親切なエラー: ネットワークエラーやデータ不備で JS が停止し、画面が真っ白になるリスクがある。
- 状態管理の欠如: スクロール位置の調整がないため、ユーザーが手動でスクロールする必要がある。
- ✅ 良い点
- 徹底したバリデーション: 「データはあるか?」「型は正しいか?」を常に疑っている。
- 保守性の高い構造: コメントで工程(1〜7)が区切られており、どこで何をしているか一目でわかる。
- API設計への配慮: lastId の更新により、重複してメッセージを取得しない仕組みが整っている。
最後に
スマホ版の Chrome も意識して JavaScript を書かないとダメなようです
2026年2月13日金曜日
サーバから Chrome ブラウザプッシュする実装の流れ
概要
実装の流れを紹介します
環境
- macOS 26.2
- Python 3.12.11
鍵の作成
こちらを参考に作成します
鍵ファイルと JavaScript 用の鍵文字列を作成すれば OK です
サブスクリプション情報の保存 (Python)
JavaScrpt 側で通知の許可をした場合に通知先の情報をサーバ側に保存しておきます
今回は Redis で紹介しますが永続化できるものであれば何でも OK です
@app.route("/subscribe", methods=["POST"])
def subscribe():
"""フロントから送られてくる Subscription 情報を Redis に保存"""
data = request.get_json()
client_id = data.get("client_id")
subscription_info = data.get("subscription")
redis_conn = get_kv()
# client_id ごとに購読情報を保存
redis_conn.set(f"push_sub:{client_id}", json.dumps(subscription_info))
return jsonify({"ok": True})
プッシュ送信処理 (Python)
あとで使いますが実際にプッシュを送信する処理です
メールアドレス部分はダミーでも問題ないですが一応ちゃんと設定した方がいいです (公開アプリの場合)
VAPID_PRIVATE_KEY_PATH = os.getenv("VAPID_PRIVATE_KEY_PATH", "./private_key.pem")
VAPID_CLAIMS = {"sub": "mailto:user@mail"}
def send_web_push(subscription_info, message_body):
try:
webpush(
subscription_info=json.loads(subscription_info),
data=json.dumps(message_body),
vapid_private_key=VAPID_PRIVATE_KEY_PATH,
vapid_claims=VAPID_CLAIMS, # type: ignore
)
except WebPushException as ex:
print("Push failed:", repr(ex))
ServiceWorker (JavaScript)
リモートプッシュはこの ServiceWorker がないと動きません
Python からのプッシュをこの ServiceWorker が受け取り通知を表示する感じです
通知のスタイルなどはシンプルにしているので必要であればアイコンなど設定してください
self.addEventListener("push", function (event) {
console.log("Push received");
let data = { title: "新着", body: "メッセージがあります" };
try {
if (event.data) {
data = event.data.json(); // ここでエラーが起きると通知が出ない
}
} catch (e) {
console.error("JSON parse error:", e);
}
const options = {
body: data.body,
};
event.waitUntil(self.registration.showNotification(data.title, options));
});
ブラウザ側のサブスクリプション登録 (JavaScript)
ブラウザで特定のサイトなどにアクセスすると通知を許可しますかなど出るサイトがありますがそれを実装します
許可するをクリックしたら必要な情報を Python 側に送ります
urlBase64ToUint8Array に鍵文字列は冒頭で作成したものを使ってください
function urlBase64ToUint8Array(base64String) {
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
async function setupPush() {
const register = await navigator.serviceWorker.register("/static/js/sw.js");
// すでに購読済みかチェック
let subscription = await register.pushManager.getSubscription();
// 未購読なら新規作成
if (!subscription) {
subscription = await register.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(
"xxx",
),
});
}
// サーバーへ送信(常に送ることで client_id との紐付けを確実に更新する)
await fetch("/subscribe", {
method: "POST",
body: JSON.stringify({
client_id: CLIENT_ID,
subscription: subscription,
}),
headers: { "Content-Type": "application/json" },
});
}
通知許可ダイアログ表示 (JavaScript)
ページが読み込まれた場合に自動で出してもいいですが嫌がられるケースもあるので特定のボタンを押したら許可する流れにします
document.addEventListener("DOMContentLoaded", () => {
// Click title to set client_id and request notification permission
const title = document.getElementById("notification");
if (title) {
title.addEventListener("click", async () => {
// 通知の許可を求める
if ("Notification" in window) {
const permission = await Notification.requestPermission();
if (permission === "granted") {
// ここで実行!
await setupPush();
}
}
});
}
// 追加: すでに許可済みなら、ページ読み込み時に購読情報を同期する
if ("Notification" in window && Notification.permission === "granted") {
setupPush().catch((err) => console.error("Auto-setup failed:", err));
}
});
プッシュ通知送信タイミング (Python)
あとは任意のタイミングでサーバ側で送信すれば OK です
当然ですが送信しまくると大変なことになるので条件など設けて適切なタイミングで送るようにしましょう
@app.route("/send", methods=["POST"])
def redis_chat_send():
# 1... 既存の処理
# 2. プッシュ通知の送信 (自分以外の購読者へ)
try:
# push_sub: で始まるキーをすべて取得
# ※ ユーザー数が多い場合はメンバーリストを別途管理するのが理想ですが、
# 現状のチャットルーム運用であれば keys で十分です
sub_keys = redis_conn.keys("push_sub:*")
for sub_key in sub_keys: # type: ignore
target_id = sub_key.decode().split(":")[-1]
# 送信者(自分)以外にのみ送る
if target_id != client_id:
sub_info = redis_conn.get(sub_key)
if sub_info:
logger.info(f"Sending push to {target_id}")
# 非同期処理にするのが理想ですが、まずは直列で実装
send_web_push(
sub_info,
{"title": "新着メッセージ", "body": f"{client_id}: {msg}"},
)
except Exception as e:
logger.error(f"Push notification error: {e}")
return jsonify({"ok": True})
サブスクリプション確認
- Redis 側にキーが登録されていること
- 開発者コンソールで ServiceWorker が登録されていること
プッシュが届かない場合
- Mac であれば通知の設定を確認しましょう
- Mac の Chrome はデフォルトでオフになっている
- Chrome で許可していても Mac 側で拒否していることが多い
- サイトが https 化されていない
- localhost ならテストできるようです
- IP ベースのサイトや http のサイトでは動作しません
スマホ or タブレット版でプッシュ通知が届かない場合
- ラップトップ版と違いかなり作りが違う
- 単純に通知を許可することができない
- iOS 側で当然許可する必要があるがそれでもサブスクリプションできないことがある
- サイトを PWA 化して上げる必要などがあるかも
最後に
参考サイト
2026年2月12日木曜日
Windows のルーティング情報を可視化する Powershell
概要
タイトル通り
環境
- Windows 11
スクリプト
# Visualize-Routes.ps1 (vis.jsを使用したHTMLベースの可視化)
param (
[string]$OutputPath = "$($PSScriptRoot)\network_routes.html",
[switch]$IncludeDefaultRoutes = $false,
[string[]]$ExcludeNextHops = @()
)
function Visualize-NetRouteGraph {
[CmdletBinding()]
param (
[string]$OutputPath = "$($PSScriptRoot)\network_routes.html",
[switch]$IncludeDefaultRoutes = $false,
[string[]]$ExcludeNextHops = @()
)
# ルーティング情報を取得
$routes = Get-NetRoute
# デフォルトルートを除外する場合
if (-not $IncludeDefaultRoutes) {
$routes = $routes | Where-Object { $_.NextHop -ne "::" -and $_.NextHop -ne "0.0.0.0" }
}
# 指定されたNextHopを除外する場合
if ($ExcludeNextHops.Count -gt 0) {
foreach ($excludeHop in $ExcludeNextHops) {
$routes = $routes | Where-Object { $_.NextHop -ne $excludeHop }
}
}
if (-not $routes) {
Write-Warning "ルーティング情報が見つかりませんでした。"
return
}
# ノードとエッジの定義
$nodes = @{}
$edges = @()
$nodeId = 0
$nodeMap = @{}
$edgeMap = @{} # エッジの重複を管理
foreach ($route in $routes) {
$destination = $route.DestinationPrefix
$nextHop = $route.NextHop
$ifIndex = $route.IfIndex
$interface = $null
# ネットワークアダプタ名を取得(エラーハンドリング付き)
try {
$interface = (Get-NetAdapter -IfIndex $ifIndex -ErrorAction Stop).Name
}
catch {
$interface = $null
}
# ノードの作成(重複なし)
if (-not $nodeMap.ContainsKey($destination)) {
$nodeMap[$destination] = @{
id = $nodeId
label = $destination
title = "Destination: $destination"
color = "lightblue"
shape = "box"
}
$nodeId++
}
if (-not $nodeMap.ContainsKey($nextHop)) {
$nodeMap[$nextHop] = @{
id = $nodeId
label = $nextHop
title = "NextHop: $nextHop"
color = "lightgreen"
shape = "ellipse"
}
$nodeId++
}
# エッジのキーを作成(重複チェック用)
# NextHop → Destination の向きにすることで、トラフィックフローを表現
$edgeKey = "$($nodeMap[$nextHop].id)->$($nodeMap[$destination].id)"
# エッジラベルの作成
$edgeInfo = "Metric: $($route.RouteMetric)"
if ($interface) {
$edgeInfo += " (via $interface)"
}
# 既存のエッジがあれば統合、なければ新規作成
if ($edgeMap.ContainsKey($edgeKey)) {
# 既存のエッジにラベルを追加
$edgeMap[$edgeKey].labels += $edgeInfo
}
else {
# 新規エッジを作成
$edgeMap[$edgeKey] = @{
from = $nodeMap[$nextHop].id
to = $nodeMap[$destination].id
labels = @($edgeInfo)
}
}
}
# エッジマップからエッジ配列を作成(ラベルを結合)
foreach ($edgeKey in $edgeMap.Keys) {
$edge = $edgeMap[$edgeKey]
$edges += @{
from = $edge.from
to = $edge.to
label = $edge.labels -join "`n"
}
}
# JSON形式でノードとエッジを作成
$nodesJson = $nodeMap.Values | ConvertTo-Json
$edgesJson = $edges | ConvertTo-Json
# HTMLテンプレートを生成
$htmlContent = @"
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ネットワークルーティンググラフ</title>
<script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
font-family: Arial, sans-serif;
}
#network {
width: 100%;
height: 100%;
border: 1px solid lightgray;
}
.controls {
position: absolute;
top: 10px;
left: 10px;
background: white;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
z-index: 100;
}
button {
padding: 5px 10px;
margin: 5px 2px;
cursor: pointer;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
}
button:hover {
background-color: #45a049;
}
</style>
</head>
<body>
<div class="controls">
<button onclick="resetView()">リセット</button>
<button onclick="fitToScreen()">画面に合わせる</button>
</div>
<div id="network"></div>
<script type="text/javascript">
var nodes = new vis.DataSet($nodesJson);
var edges = new vis.DataSet($edgesJson);
var container = document.getElementById('network');
var data = {
nodes: nodes,
edges: edges
};
var options = {
physics: {
enabled: true,
stabilization: {
iterations: 200
},
barnesHut: {
gravitationalConstant: -26000,
centralGravity: 0.3,
springLength: 300,
springConstant: 0.04,
damping: 0.3
}
},
nodes: {
font: {
size: 14,
color: 'black'
},
borderWidth: 2,
shadow: true
},
edges: {
arrows: 'to',
font: {
size: 12,
align: 'middle'
},
shadow: true,
smooth: {
type: 'continuous'
}
},
interaction: {
navigationButtons: true,
keyboard: true,
zoomView: true,
dragView: true
}
};
var network = new vis.Network(container, data, options);
function resetView() {
network.fit();
}
function fitToScreen() {
network.fit({
animation: {
duration: 1000,
easingFunction: 'easeInOutQuad'
}
});
}
window.addEventListener('resize', function() {
network.redraw();
});
network.fit();
</script>
</body>
</html>
"@
# HTMLファイルを保存
$htmlContent | Set-Content $OutputPath -Encoding UTF8
Write-Host "ネットワークルーティンググラフをHTMLで生成しました: $OutputPath"
Write-Host "ブラウザで表示しています..."
# ブラウザで開く
Invoke-Item $OutputPath
}
# 関数を実行(スクリプトの引数を渡す)
$params = @{
OutputPath = $OutputPath
}
if ($IncludeDefaultRoutes) {
$params['IncludeDefaultRoutes'] = $IncludeDefaultRoutes
}
if ($ExcludeNextHops.Count -gt 0) {
$params['ExcludeNextHops'] = $ExcludeNextHops
}
Visualize-NetRouteGraph @params
最後に
作成された HTML をブラウザ開けば OK です
2026年2月11日水曜日
2026年2月10日火曜日
Outlook の受信トレイにあるメールを検索する Powershell
概要
タイトル通り
環境
- Windows 11
スクリプト
param(
[string]$Keyword = "test",
[switch]$SearchBody = $false
)
try {
Write-Host "Connecting to Outlook..."
$ErrorActionPreference = "Stop"
$Outlook = New-Object -ComObject Outlook.Application
$Namespace = $Outlook.GetNamespace("MAPI")
$Namespace.Logon()
$Inbox = $Namespace.GetDefaultFolder(6)
Write-Host "Searching in $($Inbox.Name)"
Write-Host "Keyword: $Keyword"
Write-Host "Search Body: $SearchBody"
$Items = $Inbox.Items
Write-Host "Total items: $($Items.Count)"
$FoundItems = @()
$count = 0
foreach ($Item in $Items) {
$count++
if ($count % 100 -eq 0) {
Write-Host "Processing item $count..."
}
if ($SearchBody) {
if ($Item.Subject -match $Keyword -or $Item.Body -match $Keyword) {
$FoundItems += $Item
}
}
else {
if ($Item.Subject -match $Keyword) {
$FoundItems += $Item
}
}
}
Write-Host "Found: $($FoundItems.Count) emails"
if ($FoundItems.Count -gt 0) {
foreach ($MailItem in $FoundItems) {
Write-Host "-----"
Write-Host "Subject: $($MailItem.Subject)"
Write-Host "Sender: $($MailItem.SenderName)"
Write-Host "Received: $($MailItem.ReceivedTime)"
}
}
else {
Write-Host "No emails found."
}
if ($Inbox) { [System.Runtime.InteropServices.Marshal]::ReleaseComObject($Inbox) | Out-Null }
if ($Namespace) { [System.Runtime.InteropServices.Marshal]::ReleaseComObject($Namespace) | Out-Null }
if ($Outlook) { [System.Runtime.InteropServices.Marshal]::ReleaseComObject($Outlook) | Out-Null }
Write-Host "Done."
}
catch {
Write-Host "Error: $($_.Exception.Message)"
}
最後に
日本語で検索すると文字コードエラーになる場合があるのでその場合は検索キーワードを引数で受け取るようにしましょう
メールの件数が多いとかなり時間がかかるのでタイトルだけで検索するなどの工夫をしましょう
進捗がわかりづらい場合などがあるので 100件ずつ分割して検索してもいいと思います