2026年1月24日土曜日

RDP で毎回認証を求められるがそれを回避して認証情報なしで自動で接続する方法

RDP で毎回認証を求められるがそれを回避して認証情報なしで自動で接続する方法

概要

ダウンロードした RDP ファイルをコピーして書き換えて実行します 前回の続きです

環境

  • Windows11

monitory_rdp_file.ps1

param(
    [Parameter(Mandatory=$false)]
    [string]$RDPServer = "your.rdp.server.or.ip",
    
    [Parameter(Mandatory=$false)]
    [string]$RDPUsername = "domain\username",
    
    [Parameter(Mandatory=$false)]
    [string]$RDPPassword = "xxxx",
    
    [Parameter(Mandatory=$false)]
    [switch]$TestMode = $false
)

# Basic file logging to help diagnose scheduled task behavior
$logDir = Join-Path $env:LOCALAPPDATA "RDPFileMonitor"
if (-not (Test-Path $logDir)) {
    New-Item -Path $logDir -ItemType Directory -Force | Out-Null
}
$logPath = Join-Path $logDir "monitor.log"

# Emit a simple startup marker
("Started: " + (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')) | Out-File -FilePath (Join-Path $logDir 'started.txt') -Append -Encoding utf8

try {
    Start-Transcript -Path $logPath -Append -ErrorAction SilentlyContinue | Out-Null
} catch {}

# Get user profile path - handle both system and user context execution
$userProfile = [Environment]::GetFolderPath("UserProfile")
if (-not (Test-Path $userProfile)) {
    $userProfile = "C:\Users\username"
}
$downloadFolder = Join-Path $userProfile "Downloads"
$targetRDPFileName = "rdgateway101_vdgate_nifcloud_net.rdp"
$processedFiles = @()

Write-Host "Starting RDP file monitoring (polling method)"
Write-Host "Executed by: $env:USERNAME from host: $env:COMPUTERNAME"
Write-Host "User Profile: $userProfile"
Write-Host "Log: $logPath"
Write-Host "Target folder: $downloadFolder"
Write-Host "Target file name: $targetRDPFileName"

if ($TestMode) {
    Write-Host "Test Mode: Running one iteration only"
} else {
    Write-Host "Checking every 2 seconds..."
}

$iterationCount = 0
$maxIterations = if ($TestMode) { 1 } else { [int]::MaxValue }

while ($iterationCount -lt $maxIterations) {
    $iterationCount++
    Write-Host "Iteration $iterationCount at $(Get-Date -Format 'HH:mm:ss')" -ForegroundColor Gray
    
    try {
        if (Test-Path $downloadFolder) {
            # Clean up processed files list - remove entries for files that no longer exist
            $processedFiles = @($processedFiles | Where-Object { Test-Path $_ })
            
            $files = Get-ChildItem -Path $downloadFolder -Filter "*.rdp" -File
            
            foreach ($file in $files) {
                if ($file.Name -eq $targetRDPFileName -and $file.FullName -notin $processedFiles) {
                    Write-Host "Target file detected: $($file.Name) at $(Get-Date -Format 'HH:mm:ss')"
                    Write-Host "Full path: $($file.FullName)"
                    
                    Start-Sleep -Seconds 2
                    
                    try {
                        Write-Host "Executing RDP file: $($file.FullName)"
                        
                        # If credentials provided, cache them and modify RDP file
                        if ($RDPUsername -and $RDPPassword) {
                            Write-Host "Caching credentials for: $RDPServer"
                            
                            # Cache credentials using cmdkey (more reliable than embedding in RDP)
                            $cmdkeyCmd = "cmdkey.exe /generic:$RDPServer /user:$RDPUsername /pass:$RDPPassword"
                            Invoke-Expression $cmdkeyCmd | Out-Null
                            Write-Host "Credentials cached successfully"
                            
                            # Read original RDP file
                            $rdpContent = Get-Content -Path $file.FullName -Encoding ASCII
                            
                            # Remove or disable credential prompting settings
                            $rdpContent = $rdpContent -replace 'prompt for credentials:i:1', 'prompt for credentials:i:0'
                            $rdpContent = $rdpContent -replace 'promptcredentialonce:i:1', 'promptcredentialonce:i:0'
                            $rdpContent = $rdpContent -replace 'enablecredsspsupport:i:1', 'enablecredsspsupport:i:0'
                            
                            # Ensure username is set in RDP file
                            if ($rdpContent -notmatch 'username:s:') {
                                $rdpContent += "`r`nusername:s:$RDPUsername"
                            } else {
                                $rdpContent = $rdpContent -replace 'username:s:.*', "username:s:$RDPUsername"
                            }
                            
                            # Create temporary RDP file
                            $tempRDPPath = [System.IO.Path]::GetTempFileName() -replace '\.tmp$', '.rdp'
                            Set-Content -Path $tempRDPPath -Value $rdpContent -Encoding ASCII
                            Write-Host "Temporary RDP file created: $tempRDPPath"
                            
                            Start-Sleep -Seconds 1
                            
                            # Execute temporary RDP file with cached credentials
                            Start-Process -FilePath $tempRDPPath
                            $processedFiles += $file.FullName
                            Write-Host "Executed successfully with cached credentials"
                            
                            # Clean up temporary file after a delay
                            Start-Sleep -Seconds 3
                            Remove-Item -Path $tempRDPPath -Force -ErrorAction SilentlyContinue
                            Write-Host "Temporary RDP file cleaned up"
                        }
                        else {
                            # Execute without credentials modification
                            Write-Host "No credentials provided, executing original RDP file"
                            Start-Process -FilePath $file.FullName
                            $processedFiles += $file.FullName
                            Write-Host "Executed successfully"
                        }
                        
                        Start-Sleep -Seconds 1
                        Remove-Item -Path $file.FullName -Force
                        Write-Host "File deleted: $($file.FullName)"
                    }
                    catch {
                        Write-Error "Error executing RDP file: $($_.Exception.Message)"
                    }
                }
            }
        }
        else {
            Write-Host "Download folder not found: $downloadFolder"
        }
    }
    catch {
        Write-Error "Error during monitoring: $($_.Exception.Message)"
    }
    
    # For test mode, exit after one iteration
    if ($TestMode) {
        Write-Host "Test mode iteration complete. Exiting."
        break
    }
    
    # Normal mode: sleep before next iteration
    Start-Sleep -Seconds 2
}

Write-Host "RDP file monitor stopped at $(Get-Date -Format 'HH:mm:ss')"
Stop-Transcript -ErrorAction SilentlyContinue

最後に

RDP ファイルは中身はテキストなのでいろいろハックできます

2026年1月23日金曜日

ダウンロードフォルダを監視し RDP ファイルを自動で開く Powershell スクリプト

ダウンロードフォルダを監視し RDP ファイルを自動で開く Powershell スクリプト

概要

今の chrome ではできないので自作する必要があります

環境

  • Windows 11

ダウンロードフォルダを監視し特定の rdp ファイルが出現したら自動で実行する Powershell

  • vim monitor_rdp_file.ps1
param()

# Basic file logging to help diagnose scheduled task behavior
$logDir = Join-Path $env:LOCALAPPDATA "RDPFileMonitor"
if (-not (Test-Path $logDir)) {
    New-Item -Path $logDir -ItemType Directory -Force | Out-Null
}
$logPath = Join-Path $logDir "monitor.log"
# Emit a simple startup marker regardless of transcript support
("Started: " + (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')) | Out-File -FilePath (Join-Path $logDir 'started.txt') -Append -Encoding utf8
try {
    Start-Transcript -Path $logPath -Append -ErrorAction SilentlyContinue | Out-Null
} catch {}

$downloadFolder = [Environment]::GetFolderPath("UserProfile") + "\Downloads"
$targetRDPFileName = "your-rdp-filename.rdp"
$processedFiles = @()

Write-Host "Starting RDP file monitoring (polling method)"
Write-Host "Log: $logPath"
Write-Host "Target folder: $downloadFolder"
Write-Host "Target file name: $targetRDPFileName"
Write-Host "Checking every 2 seconds..."

while ($true) {
    try {
        if (Test-Path $downloadFolder) {
            $files = Get-ChildItem -Path $downloadFolder -Filter "*.rdp" -File
            
            foreach ($file in $files) {
                if ($file.Name -eq $targetRDPFileName -and $file.FullName -notin $processedFiles) {
                    Write-Host "Target file detected: $($file.Name) at $(Get-Date -Format 'HH:mm:ss')"
                    Write-Host "Full path: $($file.FullName)"
                    
                    Start-Sleep -Seconds 2
                    
                    try {
                        Write-Host "Executing RDP file: $($file.FullName)"
                        # Use Start-Process for reliability in scheduled tasks
                        Start-Process -FilePath $file.FullName
                        $processedFiles += $file.FullName
                        Write-Host "Executed successfully"
                        
                        Start-Sleep -Seconds 1
                        Remove-Item -Path $file.FullName -Force
                        Write-Host "File deleted: $($file.FullName)"
                    }
                    catch {
                        Write-Error "Error executing RDP file: $($_.Exception.Message)"
                    }
                }
            }
        }
    }
    catch {
        Write-Error "Error during monitoring: $($_.Exception.Message)"
    }
    
    Start-Sleep -Seconds 2
}
finally {
    try { Stop-Transcript | Out-Null } catch {}
}

上記ファイルを実行する bat ファイル

権限を付与して実行する必要があるのでラッパー用のバッチファイルを作成します

  • vim monitor_rdp_file.bat
@echo off
REM This batch file launches the RDP file monitor PowerShell script
REM It will be called by Task Scheduler at user login

REM Ensure we start in the script directory (useful for relative paths)
pushd "C:\Users\username\path\to\powershell_script\"

REM Prepare simple logs for troubleshooting
set "LOGDIR=%LOCALAPPDATA%\RDPFileMonitor"
if not exist "%LOGDIR%" mkdir "%LOGDIR%" >nul 2>&1

REM Launch the PowerShell monitor hidden with execution policy bypass and redirect output to logs
powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "C:\Users\username\path\to\powershell_script\monitor_rdp_file.ps1" 1>>"%LOGDIR%\ps_out.log" 2>>"%LOGDIR%\ps_err.log"

popd
exit /b 0

バッチファイルをタスクスケジューラに登録する Powershell

タスクスケジューラで動かす場合はバッチファイルを登録します

  • vim register_scheduled_task.ps1
# Create scheduled task for RDP file monitor at user login
# This script should be run with administrator privileges

$taskName = "RDP File Monitor"
$taskPath = "\"
$scriptPath = "C:\Users\username\path\to\powershell_script\monitor_rdp_file.bat"

Write-Host "Creating scheduled task for RDP file monitor..."

# Check if task already exists
$existingTask = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue

if ($existingTask) {
    Write-Host "Task already exists. Removing..."
    Unregister-ScheduledTask -TaskName $taskName -Confirm:$false
}

# Create task trigger (at user login)
$trigger = New-ScheduledTaskTrigger -AtLogOn

# Create task action
$workingDir = Split-Path -Path $scriptPath -Parent
$action = New-ScheduledTaskAction -Execute "cmd.exe" -Argument "/c `"$scriptPath`"" -WorkingDirectory $workingDir

# Create task settings
$settings = New-ScheduledTaskSettingsSet -MultipleInstances IgnoreNew -StartWhenAvailable

# Register the task with highest privilege
$principal = New-ScheduledTaskPrincipal -UserId "$env:USERNAME" -RunLevel Highest -LogonType Interactive

Register-ScheduledTask -TaskName $taskName -Trigger $trigger -Action $action -Settings $settings -Principal $principal -Force

Write-Host "Task created successfully!"
Write-Host "Task name: $taskName"
Write-Host "Batch file: $scriptPath"
Write-Host "Trigger: At user login"
Write-Host ""
Write-Host "The task will now run automatically when you log in to Windows."

タスクスケジューラ登録

Start-Process powershell.exe -ArgumentList "-ExecutionPolicy Bypass -Command `"& 'C:\Users\username\path\to\powershell_script\register_scheduled_task.ps1'`"" -Verb RunAs -Wait

最後に

これでログインすると自動で起動し監視を開始します
cmdkey などと組み合わせると自動でログインまでしてくれます
ただ rdp ファイルの prompt for credentials:i:1 が 0 になっていないとダメです
(1 でも自動入力する方法はないのだろうか)

2026年1月20日火曜日

Ubuntu24.04 に MySQL8.4 をインストールする方法

Ubuntu24.04 に MySQL8.4 をインストールする方法

概要

apt でインストールできます
ついでに unattended-upgrade にも対応しておきます

環境

  • Ubuntu 24.04
  • MySQL 8.4.7

インストール方法

curl -LO https://dev.mysql.com/get/mysql-apt-config_0.8.36-1_all.deb
sudo dpkg -i mysql-apt-config_0.8.36-1_all.deb
sudo apt -y update
sudo apt -y install mysql-server

確認項目

  • root パスワードの設定
  • 既存の設定ファイル (my.cnf など) がある場合は上書きするか

動作確認

  • dpkg -l | grep 'mysql'
ii  default-libmysqlclient-dev:amd64       1.1.0build1                             amd64        MySQL database development files (metapackage)
ii  libmysqlclient-dev                     8.4.7-1ubuntu24.04                      amd64        MySQL development headers
ii  libmysqlclient24:amd64                 8.4.7-1ubuntu24.04                      amd64        MySQL shared client libraries
ii  mysql-apt-config                       0.8.36-1                                all          Auto configuration for MySQL APT Repo.
ii  mysql-client                           8.4.7-1ubuntu24.04                      amd64        MySQL Client meta package depending on latest version
ii  mysql-common                           8.4.7-1ubuntu24.04                      amd64        Common files shared between packages
ii  mysql-community-client                 8.4.7-1ubuntu24.04                      amd64        MySQL Client
ii  mysql-community-client-core            8.4.7-1ubuntu24.04                      amd64        MySQL Client Core Binaries
ii  mysql-community-client-plugins         8.4.7-1ubuntu24.04                      amd64        MySQL Client plugin
ii  mysql-community-server                 8.4.7-1ubuntu24.04                      amd64        MySQL Server
ii  mysql-community-server-core            8.4.7-1ubuntu24.04                      amd64        MySQL Server Core Binaries
ii  mysql-server                           8.4.7-1ubuntu24.04                      amd64        MySQL Server meta package depending on latest version

unattended-upgrade

  • grep -e 'Origin:' -e 'Suite:' /var/lib/apt/lists/repo.mysql.com_apt_ubuntu_dists_noble_*
/var/lib/apt/lists/repo.mysql.com_apt_ubuntu_dists_noble_InRelease:Origin: MySQL
  • sudo vim /etc/apt/apt.conf.d/58unattended-upgrades-mysql-packages
Unattended-Upgrade::Allowed-Origins {
        "MySQL:noble";
};

最後に

8.0 は 2026/04 で EOL なのでアップグレードしましょう

参考サイト

2026年1月14日水曜日

bpftrace で実行されたコマンドの親プロセスを特定する

bpftrace で実行されたコマンドの親プロセスを特定する

概要

例えば内部で curl を実行している親プロセスを特定する場合を紹介します

環境

  • Ubuntu 24.04
  • bpftrace 0.20.2

コマンド

sudo bpftrace -e '
tracepoint:syscalls:sys_enter_execve
/str(args->filename) == "/usr/bin/curl"/
{
    printf("curl executed! pid=%d comm=%s filename=%s\n",
        pid, comm, str(args->filename));
}
'

最後に

なぞのエージェントやサービスをインストールすると意図しないコマンドなどを実行することがあるので親プロセスを特定して問題ないことを確認しましょう

2026年1月13日火曜日

Tailwind CSS 超入門

Tailwind CSS 超入門

概要

試しました
nodejs だけ必要になります

環境

  • macOS 15.7.3
  • nodejs 22.15.1
  • tailwindcss 4.1.18

インストール

  • npm init -y
  • npm install tailwindcss @tailwindcss/cli

インポート

  • vim src/input.css
@import "tailwindcss";

CSS 生成

  • npx @tailwindcss/cli -i ./src/input.css -o ./src/output.css

index.html

<!doctype html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link href="./src/output.css" rel="stylesheet">
</head>
<body>
  <h1 class="text-3xl font-bold underline">
    Hello world!
  </h1>
</body>
</html>

動作確認

  • open index.html

最後に

これだけで「text-3xl」「font-bold」「underline」などのすでに tailwindcss に定義されているクラスを使えるようになります

Tailwind CSS はすでに定義されているクラスを組み合わせるだけで簡単にモダンなデザインができるという CSS フレームワークになります

基本は vite のプラグインとして使うようですが vite なしでも使えます

参考サイト

2026年1月12日月曜日

BasedPyright を pyright の設定に合わせる方法

BasedPyright を pyright の設定に合わせる方法

概要

pyrightconfig.json があるとそれを優先してしまうので basedpyright 用の設定ファイルを作成してそっちに記載します

環境

  • Ubuntu 24.04
  • code-server 4.107.0
  • basedpyright 1.37.1

basedpyrightconfig.json

{
  "typeCheckingMode": "basic",
  "reportUnknownParameterType": "none",
  "reportMissingTypeArgument": "none"
}

vscode 設定

  • settings.json
{
  "basedpyright.analysis.configFilePath": "./basedpyrightconfig.json"
}

最後に

pyrightconfig.json がある場合は注意しましょう

デフォルトは recommended なのですがそれだと pyright よりも厳しいようです

2026年1月11日日曜日

code-server と copilot chat プラグインを自動更新する方法

code-server と copilot chat プラグインを自動更新する方法

概要

code-server は apt 管理ではないので手動で更新する必要があります
更新するシェルスクリプトを作成しそれを cron などで回せば自動更新できるかなと思います

環境

  • Ubuntu 24.04
  • code-server 4.107.0
  • copilot chat plugin 0.35.3

スクリプト

#!/bin/bash

echo "=== Code Server をインストール/更新中..."
curl -fsSL https://code-server.dev/install.sh | sh

echo "=== systemd デーモンを再読み込み中..."
sudo systemctl daemon-reload

echo "=== Code Server を再起動中..."
sudo systemctl restart code-server@$USER
echo "✓ Code Server の準備完了"

# https://github.com/microsoft/vscode-copilot-chat/tags から最新の安定版を自動取得
# 安定板のタグ形式は x.y.z(パッチバージョンが1-3桁、日付のような8桁以上は除外)
# 検証版(0.37.2026010802など)は自動的に除外される
# ただし code-server の最新版でも copilot chat プラグインの最新版がインストールできるわけではないので注意
echo "=== Copilot Chat最新安定版を取得中..."
VERSION=$(curl -s https://api.github.com/repos/microsoft/vscode-copilot-chat/tags \
  | grep -oP '"name":\s*"\K[^"]+' \
  | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]{1,3}$' \
  | head -1 \
  | sed 's/^v//')

if [ -z "$VERSION" ]; then
  echo "❌ エラー: バージョン取得に失敗しました"
  exit 1
fi

echo "✓ 取得したバージョン: $VERSION"

echo "=== vsixファイルをダウンロード中..."
curl -o "copilot-chat-${VERSION}.vsix" --compressed "https://marketplace.visualstudio.com/_apis/public/gallery/publishers/GitHub/vsextensions/copilot-chat/${VERSION}/vspackage"

echo "=== 拡張機能をインストール中..."
code-server --install-extension "copilot-chat-${VERSION}.vsix"

echo "=== クリーンアップ中..."
rm "copilot-chat-${VERSION}.vsix"

echo "✓ インストール完了! (バージョン: $VERSION)"%   

最後に

これを cron とかで回せばとりあえず最新版は常に手に入ります

2026年1月10日土曜日

vscode pylance で reportMissingImports の対策

vscode pylance で reportMissingImports の対策

概要

プロジェクトを切り替えてもなぜか virtualenv が切り替わらずライブラリが参照できないケースがあるようです

環境

  • macOS 15.7.2
  • vscode 1.108.0
  • pylance 2025.10.4

対策

settings.json でもできますがもっと簡単なのは右下の virtualenv を現在のプロジェクトに変更してあげることです

以下のように表示されているのが現在使用している virtualenv です

3.13.11 (blogbot-6iuakeu5)

ここをクリックすると他の virtualenv に切り替えできるので現在のプロジェクトと同じ virtualenv にすれば参照できるようになります

その他

ちゃんとプロジェクトで pipenv install や poetry install はしておきましょう

最後に

バグなのかもしれません
面倒ですが手動で変更してあげましょう

参考サイト

2026年1月9日金曜日

script タグで管理されている JavaScript ライブラリを nodejs で管理する方法

script タグで管理されている JavaScript ライブラリを nodejs で管理する方法

概要

esbuild を使います

環境

  • macOS 15.7.2
  • Ruby 4.0.0
  • nodejs 22.21.0
    • esbuild 0.27.2

erb の script タグ

これを nodejs で管理します

<script src="https://cdnjs.cloudflare.com/ajax/libs/plyr/3.7.8/plyr.min.js" integrity="sha512-vONptKEoKbP1gaC5UkbYDa9OPr04ur4bxaaqT7DAJxGHB2oogtseCPrl5e5hPFokGYotlGNV4d+GM593ka7iNA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

esbuild のインストール

  • npm install --save-dev esbuild

plyr のインストール

  • npm install plyr

src/podcast.js の作成

esbuild が自動で作成する plyr の JavaScript ファイルを作成するための設定的なものを書きます
また plyr に関する処理も書きます
このファイルを元に自動で配信用の JavaScript ファイルを esbuild が作成してくれます

import Plyr from "plyr";

document.addEventListener("DOMContentLoaded", () => {
  const player = new Plyr("#player", {
    speed: { selected: 1, options: [1, 1.5, 2] },
  });
});

package.json に追記

scripts に追記します
esbuild を使ってビルドし自動で JavaScript ファイルを作成する処理を記載します
実際にアプリで使用する public/js 配下になります

"scripts": {
  "build:podcast": "esbuild src/podcast.js --bundle --minify --outfile=public/js/podcast.js"
}

ビルド

  • npm run build:podcast

これで public/js/podcast.js ができます
冒頭の erb はこのファイルだけを参照すれば OK です
erb コード内で直接参照していた script タグは削除して OK です

おまけ: .gitignore

自動生成される public/js/podcast.js は .gitignore に入れたほうがいいです
理由は git grep などで検索するときに大変になるからです
なので Dockerfile などでビルドする設定を入れるようにしましょう

###############################
# Stage 1: JS/CSS Build
###############################
FROM node:22.21-slim AS build

WORKDIR /app

# package.json だけコピーして npm install を早くする
COPY package.json package-lock.json ./
RUN npm install

# ソースコードコピー
COPY src ./src
COPY public ./public

# CSS minify tool
RUN npm install -g clean-css-cli

# CSS minify
RUN find public/css -name "*.css" -exec sh -c 'cleancss -o "$1" "$1"' _ {} \;

# JS build
RUN npm run build:podcast

ただビルドする環境がなくリポジトリの内容をそのままデプロイするような環境では .gitignore せずに git 管理に含めるようにしましょう

最後に

今後は npm update すれば自動で更新してくれます
esbuild はミニファイもしてくれるのでそのまま配信用として使えます

2026年1月8日木曜日

超簡単 websocket お試しコード

超簡単 websocket お試しコード

概要

二人専用チャットルームを簡単に作成してみます

環境

  • macOS 15.7.2
  • Python 3.12.11
    • Flask==3.1.2
    • Flask-SocketIO==5.6.0
    • eventlet==0.40.4

インストール

  • pyenv local 3.12.11
  • pipenv install flask flask-socketio eventlet

アプリ

  • vim app.py
from typing import cast

from flask import Flask, render_template, request
from flask_socketio import SocketIO, emit, join_room, leave_room

app = Flask(__name__)

socketio = SocketIO(app, cors_allowed_origins="*")

CHAT_ROOM_ID = "hoge"

# 接続管理(sid を保持)
chat_clients: set[str] = set()


@app.route("/<room_id>")
def chat(room_id: str):
    if room_id != CHAT_ROOM_ID:
        return "Not Found", 404
    return render_template("index.html", room_id=CHAT_ROOM_ID)


@socketio.on("connect")
def on_connect():
    sid = cast(str, request.sid)  # type: ignore

    if len(chat_clients) >= 2:
        emit("full", to=sid)
        return False

    chat_clients.add(sid)
    join_room(CHAT_ROOM_ID)

    if len(chat_clients) == 1:
        # 1人目
        emit("status", "待っています…", to=sid)
    else:
        # 2人目が入った瞬間に「全員」に通知
        emit("status", "準備OK!", to=CHAT_ROOM_ID)


@socketio.on("message")
def on_message(data):
    msg = data.get("message")
    if msg:
        emit("message", msg, to=CHAT_ROOM_ID, include_self=False)


@socketio.on("disconnect")
def on_disconnect():
    sid = cast(str, request.sid)  # type: ignore
    chat_clients.discard(sid)
    leave_room(CHAT_ROOM_ID)

    if len(chat_clients) == 1:
        # 残った1人に通知
        emit("status", "終わり", to=CHAT_ROOM_ID)


if __name__ == "__main__":
    socketio.run(app, host="0.0.0.0", port=5000, debug=False)

テンプレート

  • vim templates/index.html
<!doctype html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title>Private Chat</title>
    <script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
  </head>
  <body>
    <h3>Private Chat</h3>
    <div id="status"></div>
    <ul id="log"></ul>

    <input id="msg" placeholder="message" />
    <button onclick="send()">Send</button>

    <script>
      const socket = io();

      socket.on("status", (msg) => {
        document.getElementById("status").innerText = msg;
      });

      socket.on("message", (msg) => {
        const li = document.createElement("li");
        li.textContent = msg;
        log.appendChild(li);
      });

      socket.on("full", () => {
        alert("このチャットは満員です");
      });

      function send() {
        const input = document.getElementById("msg");
        socket.emit("message", { message: input.value });
        input.value = "";
      }
    </script>
  </body>
</html>

動作確認

localhost:5000/hoge に複数のブラウザでアクセスしてメッセージのやり取りができることを確認しましょう

できない場合はファイアウォールや NetFilter、リバースプロキシなどの設定を確認しましょう
WebSocket は Upgrade: example/1, foo/2 のような Upgrade ヘッダの許可が必須なので前段でヘッダが弾かれている可能性があります

最後に

5000 番ポートがすでに使われている場合はポートを変更するか AirPlay レシーバをオフにしましょう

lsof -i :5000

COMMAND    PID         USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
ControlCe 1129       user01   10u  IPv4 0xfb24a12bbd08b2c2      0t0  TCP *:commplex-main (LISTEN)
ControlCe 1129       user01   11u  IPv6 0x9dc35b82883e083d      0t0  TCP *:commplex-main (LISTEN)

2026年1月7日水曜日

pywebpush で鍵を作成する方法

pywebpush で鍵を作成する方法

概要

pywebpush は Python からブラウザプッシュを実現する仕組みです
プッシュ通知するには秘密鍵と公開鍵の作成が必要です
今回はそれらの鍵の作成方法を紹介します

環境

  • macOS 15.7.2
  • Python 3.12.11
    • pywebpush 2.2.0
    • py-vapid 1.9.4

インストール

  • pipenv install py-vapid

鍵ファイル作成

  • pipenv run vapid --gen

これで private_key.pem と public_key.pem が作成できます
private_key.pem は Python 側のアプリで設定します
ファイルのパスをそのまま指定すれば OK です

webpush(
    subscription_info=json.loads(sub),
    data=json.dumps(
        {"title": "新着メッセージ", "body": "相手からメッセージが届きました"}
    ),
    vapid_private_key=VAPID_PRIVATE_KEY_PATH,
    vapid_claims={"sub": "user01@mail"},
)

公開鍵の文字列化

  • pipenv run vapid --applicationServerKey

ここで文字列化した情報は JavaScript 側に設定します

const sub = await reg.pushManager.subscribe({
  userVisibleOnly: true,
  applicationServerKey:
    "xxx",
});

最後に

ブラウザプッシュは https が必須なので注意しましょう
localhost はテスト用で動作しますが IP や http していないサイトでは動作しません

2026年1月5日月曜日

Github copilot Pro のトライアルを解約する方法

Github copilot Pro のトライアルを解約する方法

概要

手順がコロコロ変わるらしいです

環境

  • Github copilot (2026/01/05時点)

解約手順

最後に

トライアル期間の30日を過ぎると自動で10ドル課金になるのでトライアル中にキャンセルするようにしましょう