diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..489dec3 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,20 @@ +{ + "permissions": { + "allow": [ + "Bash(resources/TaskbarHelper:*)", + "Bash(mise:run swiftbuild)", + "Bash(mise:run build)", + "Bash(mise:run typecheck)", + "Bash(mise:run typecheck:node)", + "Bash(mise:run typecheck:renderer)", + + "Bash(mise:run lint)", + "Bash(mise:run format)", + + "Bash(mise:run format)", + + "Bash(codesign:*)" + + ] + } +} diff --git a/.mise.toml b/.mise.toml index 5695706..bc4b665 100644 --- a/.mise.toml +++ b/.mise.toml @@ -1,92 +1,92 @@ [tools] -node = "20.10.0" bun = "latest" +node = "20.10.0" [env] NODE_ENV = "development" [tasks.dev] -run = "cp nativeSrc/DerivedData/taskbar.helper/Build/Products/Release/taskbar.helper resources/TaskbarHelper & npx electron-vite dev" description = "開発サーバーを起動" +run = "cp nativeSrc/DerivedData/taskbar.helper/Build/Products/Release/taskbar.helper resources/TaskbarHelper & npx electron-vite dev" [tasks.build] -run = "cp nativeSrc/DerivedData/taskbar.helper/Build/Products/Release/taskbar.helper resources/TaskbarHelper && npx electron-vite build" description = "アプリケーションをビルド" +run = "cp nativeSrc/DerivedData/taskbar.helper/Build/Products/Release/taskbar.helper resources/TaskbarHelper && npx electron-vite build" [tasks.swiftbuild] -run = "xcodebuild -project nativeSrc/taskbar.helper.xcodeproj -scheme taskbar.helper -configuration Release build && cp nativeSrc/DerivedData/taskbar.helper/Build/Products/Release/taskbar.helper resources/TaskbarHelper" description = "Swiftヘルパーをビルド" +run = "xcodebuild -project nativeSrc/taskbar.helper.xcodeproj -scheme taskbar.helper -configuration Release build && echo [COPY]binary copy to resouces && cp nativeSrc/DerivedData/taskbar.helper/Build/Products/Release/taskbar.helper resources/TaskbarHelper && echo [COPY] done." [tasks."build:mac"] -run = "mise run build && npx electron-builder --mac --universal" description = "macOSユニバーサルバイナリをビルド" +run = "mise run build && npx electron-builder --mac --universal" [tasks."install-app"] -run = "rm -rf /Applications/taskbar.fm.app && cp -a dist/mac-universal/taskbar.fm.app /Applications/taskbar.fm.app" description = "アプリをApplicationsフォルダにインストール" +run = "rm -rf /Applications/taskbar.fm.app && cp -a dist/mac-universal/taskbar.fm.app /Applications/taskbar.fm.app" [tasks.helper] -run = "open nativeSrc/taskbar.helper.xcodeproj/project.xcworkspace" description = "Xcodeヘルパープロジェクトを開く" +run = "open nativeSrc/taskbar.helper.xcodeproj/project.xcworkspace" [tasks.format] -run = "bun prettier --write '**/*.{js,ts,vue,html,css}'" description = "Prettierでコードをフォーマット" +run = "bun prettier --write '**/*.{js,ts,vue,html,css}'" [tasks.lint] -run = "bun eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mjs,.vue" description = "ESLintでコードをリント" +run = "bun eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mjs,.vue" [tasks.typecheck] -run = ["mise typecheck:node", "mise typecheck:web"] description = "TypeScriptの型チェックを実行" +run = ["mise typecheck:node", "mise typecheck:web"] [tasks."typecheck:node"] -run = "npx tsc --noEmit -p src/main/tsconfig.node.json --composite false" description = "メインプロセスの型チェックを実行" +run = "npx tsc --noEmit -p src/main/tsconfig.node.json --composite false" [tasks."typecheck:web"] -run = "npx vue-tsc --noEmit -p src/renderer/tsconfig.web.json --composite false" description = "レンダラープロセスの型チェックを実行" +run = "npx vue-tsc --noEmit -p src/renderer/tsconfig.web.json --composite false" [tasks.test] -run = "npx vitest tests/" -dir = "src/main" description = "メインプロセスのテストを実行" +dir = "src/main" +run = "npx vitest tests/" [tasks."test:renderer"] -run = "npx vitest src/renderer/tests/" -dir = "src/renderer" description = "レンダラープロセスのテストを実行" +dir = "src/renderer" +run = "npx vitest src/renderer/tests/" [tasks."test:all"] -run = ["mise run test", "mise run test:renderer"] description = "すべてのテストを実行" +run = ["mise run test", "mise run test:renderer"] [tasks."test:ui"] -run = "npm run test:ui" -dir = "src/main" description = "メインプロセスのテストをUI付きで実行" +dir = "src/main" +run = "npm run test:ui" [tasks."test:ui:renderer"] -run = "npm run test:ui" -dir = "src/renderer" description = "レンダラープロセスのテストをUI付きで実行" +dir = "src/renderer" +run = "npm run test:ui" [tasks."test:coverage"] -run = "npm run test:coverage" -dir = "src/main" description = "メインプロセスのテストをカバレッジ付きで実行" +dir = "src/main" +run = "npm run test:coverage" [tasks."test:coverage:renderer"] -run = "npm run test:coverage" -dir = "src/renderer" description = "レンダラープロセスのテストをカバレッジ付きで実行" +dir = "src/renderer" +run = "npm run test:coverage" [tasks."test:swift"] -run = "swift test" -dir = "nativeSrc" description = "Swiftヘルパーの単体テストを実行" +dir = "nativeSrc" +run = "swift test" [tasks."afterSign"] -run = "node scripts/notarize.js" \ No newline at end of file +run = "node scripts/notarize.js" diff --git a/CLAUDE.md b/CLAUDE.md index 2be4795..61c7318 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -106,3 +106,84 @@ Taskbar.fm is an Electron application that brings Windows-like taskbar functiona - Uses electron-store for persistent configuration - Settings structure managed in `src/main/funcs/store.ts` - Icon caching system stores app icons in Application Support directory + +## Debugging TaskbarHelper (Native Swift Helper) + +### Handling Hangs and UE (Uninterruptible Sleep) State + +**Root Cause:** +UE (Uninterruptible Sleep) state occurs because **there is code in the source that calls uninterruptible kernel-level system calls**. This is not a runtime issue but a code issue that must be identified and fixed. + +**Detection:** +1. If a TaskbarHelper command doesn't respond within 30 seconds, check process state: + ```bash + ps aux | grep TaskbarHelper | grep -v grep + ``` +2. Look for `UE` or `UE+` in the STAT column - this indicates uninterruptible sleep state + +**UE State Characteristics:** +- Once ANY TaskbarHelper process enters UE state, **ALL subsequent executions** (any subcommand) will also enter UE state immediately +- This cascading behavior is expected and self-evident - don't waste time testing other subcommands +- `kill -9` **cannot** terminate processes in UE state +- The only solution is to restart the computer +- **IMPORTANT: "再起動" (restart) means a full OS reboot** - not just restarting the application or TaskbarHelper process. You must restart macOS itself to clear the UE state. + +**Investigation Goal:** +The goal is to identify **which code is causing the UE state**, not to test if other commands work. + +**When UE State is Detected:** + +1. **DO NOT** execute any more TaskbarHelper commands - they will also hang immediately +2. **DO NOT** attempt to test other subcommands to see if they work - this is pointless +3. **Investigate the source code** to find the problematic system call: + - Check debug logs (stderr output) to identify the last successful operation: + - Look for `logBefore()` messages to see what was executing + - Check for watchdog timeout warnings + - Identify which function/system call didn't complete + - Review the source code around the last logged operation + - Identify which system call immediately follows the last successful log entry + - That system call is likely causing the UE state +4. Report findings to the user: + - Which operation/subcommand was being attempted + - Last successful log entry (file:line from `logBefore()`) + - Which system call or API is suspected to cause UE (e.g., file I/O, CGWindowListCopyWindowInfo, SCShareableContent APIs) + - Specific code location (file and line number) where the problematic call exists +5. **Inform the user that a system restart is required** to clear the UE state and test fixes + +**Common Causes:** +- macOS system calls that require screen recording permissions can hang if permissions are misconfigured +- `CGWindowListCopyWindowInfo` can hang when the WindowServer is overloaded +- `SCShareableContent` APIs can hang when screen recording permission dialogs are pending + +**Prevention:** +- All potentially blocking operations in main.swift have watchdog timers +- Verbose logging can be enabled with: `TASKBAR_VERBOSE=1 resources/TaskbarHelper ` +- Check permissions before running: `resources/TaskbarHelper check-permissions` + +### UE Hunting Procedure + +**Systematic approach to identify and fix UE-causing code:** + +1. **Reproduce UE State** + - Perform specific operations that intentionally trigger UE state + - Document the exact steps that cause the UE state + - Verify UE state with `ps aux | grep TaskbarHelper | grep -v grep` (look for `UE` or `UE+` in STAT column) + - Capture verbose logs if possible: `TASKBAR_VERBOSE=1 resources/TaskbarHelper 2>ue-debug.log` + - Note: Once UE occurs, you must restart macOS before proceeding + +2. **Modify Source Code** + - Based on investigation, apply fixes to the identified problematic system calls in nativeSrc/taskbar.helper/main.swift + - Rebuild the helper: `mise run swiftbuild` + - Copy the new binary: `cp nativeSrc/DerivedData/taskbar.helper/Build/Products/Release/taskbar.helper resources/TaskbarHelper` + +3. **Verify Fix** + - After restarting macOS (to clear any UE state) + - Perform the exact same operation that previously caused UE state + - Confirm that UE state does NOT occur + - Check process state remains in `S` or `S+` (normal sleep states) + - Run for extended period to ensure stability + +**Important Notes:** +- Each iteration requires a full macOS restart +- Document all changes and test results +- If UE still occurs, return to step 1 with additional logging/investigation diff --git a/claudeTasks/autoUpdate.md b/claudeTasks/autoUpdate.md new file mode 100644 index 0000000..58e9358 --- /dev/null +++ b/claudeTasks/autoUpdate.md @@ -0,0 +1,671 @@ +# オートアップデート機能の検討 + +## 概要 + +Taskbar.fm にオートアップデート機能を実装するための技術的考察。electron-updater を使用し、アップデート配信サーバーでリクエスト回数を計測する機能も含める。 + +## 現在の状態 + +### 既存の構成 +- **electron-updater**: v6.1.4 が既にインストール済み(package.json:98) +- **electron-builder**: v24.6.4 でビルド設定済み +- **Notarization**: macOS 向けに公証設定済み(afterSign: build/notarize.js) +- **アプリID**: space.riinswork.taskbar +- **現在のバージョン**: 2.1.0 + +### 未実装の項目 +- アップデートチェック機能 +- アップデートUI +- 配信設定(publish設定) +- リクエスト計測機能 + +## 実装方針 + +### ハイブリッド方式(GitHub Releases + Supabase Functions) + +**採用アーキテクチャ:** + +1. **配信**: GitHub Releases を使用(無料、信頼性高い) + - electron-updater が標準対応 + - バージョン管理と配信が一元化 + - 無料で使用可能(パブリックリポジトリ) + +2. **計測**: Supabase Functions でリクエスト回数を記録 + - サーバーレスで運用コストが低い + - PostgreSQL データベースが標準装備 + - REST API が自動生成される + - 無料枠が充実(月間 500,000 リクエスト) + - アプリがアップデートチェック時に Supabase にもリクエスト送信 + - リクエストをカウントして DB に保存 + - GitHub API を使ってダウンロード数も取得可能 + +**メリット:** +- GitHub Releases は無料で信頼性が高い +- Supabase は無料枠が充実しており、VPS不要 +- サーバーレスなのでメンテナンス負荷が低い +- PostgreSQL が使えるので複雑な集計も可能 +- HTTPS が標準で提供される + +**デメリット:** +- Supabase の初期設定が必要(ただし簡単) +- 無料枠を超えた場合は課金が発生(通常は超えない) + +**必要な作業:** +- electron-builder.yml に GitHub publish 設定を追加 +- Supabase プロジェクトのセットアップ +- Supabase Functions での計測 API 実装 +- Supabase データベーステーブルの作成 + +## ファイル変更の詳細 + +### 1. electron-builder.yml + +```yaml +# 追加する設定 +publish: + provider: github + owner: [GitHubユーザー名] + repo: [リポジトリ名] + releaseType: release # または draft + private: false # プライベートリポジトリの場合は true + +# macOS用の設定を更新 +mac: + # 既存の設定... + target: + - target: dmg + arch: + - universal # または x64, arm64 +``` + +**変更理由**: GitHub Releases にビルド成果物を自動アップロードするため + +### 2. src/main/funcs/update.ts(新規作成) + +```typescript +import { autoUpdater } from 'electron-updater' +import { app, dialog } from 'electron' +import { store } from './store' + +// アップデートチェックのロジック +// - 起動時のチェック +// - 定期的なチェック(設定可能) +// - 手動チェック(設定画面から) +// - ダウンロード進捗管理 +// - インストール処理 + +// 計測サーバーへのPing送信 +// - アップデートチェック時に計測サーバーにリクエスト +// - アプリバージョン、OS情報などを送信 +``` + +**役割**: +- アップデート関連のロジックを一元管理 +- autoUpdater の設定とイベントハンドラー +- 計測サーバーとの通信 + +### 3. src/main/main.ts + +```typescript +// 追加するインポート +import { initAutoUpdater, checkForUpdates } from '@/funcs/update' + +App.whenReady().then(() => { + // 既存の初期化処理... + + // アップデート機能を初期化 + if (!is.dev) { + initAutoUpdater() + + // 起動後少し待ってからチェック(5秒後など) + setTimeout(() => { + checkForUpdates() + }, 5000) + } +}) +``` + +**変更理由**: アプリ起動時にアップデート機能を初期化 + +### 4. src/main/funcs/events.ts + +```typescript +// 追加するIPCイベントハンドラー +ipcMain.handle('check-for-updates', async () => { + // 手動でアップデートをチェック +}) + +ipcMain.handle('download-update', async () => { + // アップデートをダウンロード +}) + +ipcMain.handle('install-update', async () => { + // アップデートをインストール(再起動) +}) + +ipcMain.handle('get-update-info', async () => { + // 現在のアップデート状態を取得 +}) +``` + +**変更理由**: レンダラープロセスからアップデート操作を可能にする + +### 5. src/main/funcs/store.ts + +```typescript +// defaults オブジェクトに追加 +export const store = new ElectronStore({ + defaults: { + options: { + // 既存の設定... + + // アップデート設定 + autoCheckForUpdates: true, // 自動チェックの有効/無効 + checkInterval: 3600000, // チェック間隔(ミリ秒、デフォルト1時間) + autoDownload: true, // 自動ダウンロードの有効/無効 + notifyOnUpdate: true, // アップデート通知の表示 + } + } +}) +``` + +**変更理由**: アップデート機能の設定を永続化 + +### 6. src/renderer/src/pages/settings.vue(新規または既存ファイルを編集) + +```vue + + + +``` + +**変更理由**: ユーザーがアップデート設定を管理できるUI + +### 7. src/preload/index.ts(または該当するpreloadファイル) + +```typescript +// アップデート関連のAPIを公開 +contextBridge.exposeInMainWorld('update', { + checkForUpdates: () => ipcRenderer.invoke('check-for-updates'), + downloadUpdate: () => ipcRenderer.invoke('download-update'), + installUpdate: () => ipcRenderer.invoke('install-update'), + getUpdateInfo: () => ipcRenderer.invoke('get-update-info'), + onUpdateAvailable: (callback) => ipcRenderer.on('update-available', callback), + onUpdateDownloaded: (callback) => ipcRenderer.on('update-downloaded', callback), + onDownloadProgress: (callback) => ipcRenderer.on('download-progress', callback), +}) +``` + +**変更理由**: レンダラープロセスからアップデートAPIを安全に使用 + +## リクエスト計測機能の実装 + +### Supabase のセットアップ + +#### 1. Supabase プロジェクトの作成 +1. https://supabase.com でアカウント作成 +2. 新しいプロジェクトを作成 +3. プロジェクト URL と API キーを取得 + +#### 2. データベーステーブルの作成 + +Supabase Dashboard > SQL Editor で以下を実行: + +```sql +-- アップデート確認リクエストを記録するテーブル +CREATE TABLE update_requests ( + id BIGSERIAL PRIMARY KEY, + app_id VARCHAR(255) NOT NULL, + app_version VARCHAR(50) NOT NULL, + platform VARCHAR(50) NOT NULL, + arch VARCHAR(50) NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc', NOW()), + -- プライバシー考慮: IPアドレスは保存しない + user_agent TEXT +); + +-- インデックス作成(検索を高速化) +CREATE INDEX idx_created_at ON update_requests(created_at); +CREATE INDEX idx_app_version ON update_requests(app_version); +CREATE INDEX idx_app_id_created_at ON update_requests(app_id, created_at); + +-- Row Level Security (RLS) の設定 +ALTER TABLE update_requests ENABLE ROW LEVEL SECURITY; + +-- 挿入は誰でも可能(匿名ユーザーからのリクエスト計測のため) +CREATE POLICY "Allow anonymous insert" ON update_requests + FOR INSERT TO anon + WITH CHECK (true); + +-- 読み取りは認証済みユーザーのみ(統計確認用) +CREATE POLICY "Allow authenticated read" ON update_requests + FOR SELECT TO authenticated + USING (true); +``` + +#### 3. 統計用のビューを作成(オプション) + +```sql +-- 日別の集計ビュー +CREATE VIEW daily_update_stats AS +SELECT + DATE(created_at) as date, + COUNT(*) as request_count, + COUNT(DISTINCT app_version) as unique_versions +FROM update_requests +WHERE app_id = 'space.riinswork.taskbar' +GROUP BY DATE(created_at) +ORDER BY date DESC; + +-- バージョン別の集計ビュー +CREATE VIEW version_stats AS +SELECT + app_version, + platform, + COUNT(*) as count, + MAX(created_at) as last_seen +FROM update_requests +WHERE app_id = 'space.riinswork.taskbar' + AND created_at > NOW() - INTERVAL '30 days' +GROUP BY app_version, platform +ORDER BY count DESC; +``` + +### Supabase Functions(Edge Functions)の実装 + +#### supabase/functions/track-update/index.ts + +```typescript +import { serve } from 'https://deno.land/std@0.168.0/http/server.ts' +import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' + +const corsHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', +} + +serve(async (req) => { + // CORS プリフライトリクエスト対応 + if (req.method === 'OPTIONS') { + return new Response('ok', { headers: corsHeaders }) + } + + try { + const { appId, version, platform, arch } = await req.json() + + // Supabase クライアントの作成 + const supabaseUrl = Deno.env.get('SUPABASE_URL')! + const supabaseKey = Deno.env.get('SUPABASE_ANON_KEY')! + const supabase = createClient(supabaseUrl, supabaseKey) + + // リクエストを記録 + const { error } = await supabase + .from('update_requests') + .insert([ + { + app_id: appId, + app_version: version, + platform: platform, + arch: arch, + user_agent: req.headers.get('user-agent') + } + ]) + + if (error) { + throw error + } + + // GitHub API から最新バージョン情報を取得(オプション) + const githubResponse = await fetch( + 'https://api.github.com/repos/[OWNER]/[REPO]/releases/latest' + ) + const latestRelease = await githubResponse.json() + + return new Response( + JSON.stringify({ + success: true, + currentVersion: version, + latestVersion: latestRelease.tag_name, + downloadUrl: latestRelease.assets[0]?.browser_download_url + }), + { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 200, + } + ) + } catch (error) { + return new Response( + JSON.stringify({ error: error.message }), + { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 400, + } + ) + } +}) +``` + +#### Supabase Functions のデプロイ + +```bash +# Supabase CLI のインストール +npm install -g supabase + +# ログイン +supabase login + +# プロジェクトにリンク +supabase link --project-ref [YOUR_PROJECT_REF] + +# Functions をデプロイ +supabase functions deploy track-update +``` + +### クライアント側(src/main/funcs/update.ts) + +```typescript +const SUPABASE_FUNCTION_URL = 'https://[YOUR_PROJECT_REF].supabase.co/functions/v1/track-update' + +async function trackUpdateCheck(): Promise { + try { + const response = await fetch(SUPABASE_FUNCTION_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + appId: 'space.riinswork.taskbar', + version: app.getVersion(), + platform: process.platform, + arch: process.arch + }) + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const data = await response.json() + console.log('Update check tracked:', data) + + // 必要に応じて、サーバーからの最新バージョン情報を使用 + // この情報は autoUpdater の結果と併用できる + } catch (error) { + // 計測失敗はアップデート機能に影響させない + // ネットワークエラーやサーバーダウンでもアプリは正常動作 + console.error('Failed to track update check:', error) + } +} +``` + +### 統計データの取得方法 + +#### Supabase Dashboard で確認 +1. Supabase Dashboard > Table Editor > update_requests +2. SQL Editor で集計クエリを実行 + +#### REST API で取得(認証済み) + +```typescript +// 統計確認用のスクリプト例 +import { createClient } from '@supabase/supabase-js' + +const supabase = createClient( + 'https://[YOUR_PROJECT_REF].supabase.co', + '[YOUR_SERVICE_ROLE_KEY]' // Service Role Key(管理用) +) + +// 日別の統計を取得 +const { data: dailyStats } = await supabase + .from('daily_update_stats') + .select('*') + .limit(30) + +// バージョン別の統計を取得 +const { data: versionStats } = await supabase + .from('version_stats') + .select('*') + +console.log('Daily stats:', dailyStats) +console.log('Version stats:', versionStats) +``` + +#### カスタム集計クエリ例 + +```sql +-- 過去7日間のリクエスト数 +SELECT + DATE(created_at) as date, + COUNT(*) as requests +FROM update_requests +WHERE + app_id = 'space.riinswork.taskbar' + AND created_at > NOW() - INTERVAL '7 days' +GROUP BY DATE(created_at) +ORDER BY date DESC; + +-- プラットフォーム別の分布 +SELECT + platform, + COUNT(*) as count, + ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER (), 2) as percentage +FROM update_requests +WHERE app_id = 'space.riinswork.taskbar' +GROUP BY platform +ORDER BY count DESC; + +-- アクティブなバージョンの状況 +SELECT + app_version, + COUNT(*) as users, + MAX(created_at) as last_check +FROM update_requests +WHERE + app_id = 'space.riinswork.taskbar' + AND created_at > NOW() - INTERVAL '7 days' +GROUP BY app_version +ORDER BY users DESC; +``` + +## 実装の段階的アプローチ + +### フェーズ 1: 基本的なアップデート機能(優先度: 高) +1. electron-builder.yml に GitHub publish 設定を追加 +2. src/main/funcs/update.ts を作成 +3. src/main/main.ts にアップデートチェック処理を統合 +4. 基本的なダイアログでアップデート通知を表示 + +**所要時間の見積もり**: このフェーズだけで基本的なアップデート機能は動作 + +### フェーズ 2: UI の改善(優先度: 中) +1. src/main/funcs/store.ts にアップデート設定を追加 +2. src/main/funcs/events.ts にIPCハンドラー追加 +3. src/renderer/ に設定UIを追加 +4. preload でAPIを公開 + +### フェーズ 3: リクエスト計測機能(優先度: 中〜低) +1. Supabase プロジェクトのセットアップ + - アカウント作成とプロジェクト作成 + - データベーステーブルの作成 + - Edge Functions の実装とデプロイ +2. src/main/funcs/update.ts に計測ロジック追加 + - Supabase Functions への POST リクエスト + - エラーハンドリング +3. 統計ダッシュボードの作成(オプション) + - Supabase Dashboard で確認 + - またはカスタム管理画面を構築 + +## セキュリティとプライバシーの考慮事項 + +### コード署名 +- macOS: 既に公証設定済み(notarize.js) +- アップデートファイルも同様に署名・公証が必要 +- electron-builder が自動的に処理 + +### HTTPS +- GitHub Releases は HTTPS で配信 +- カスタム計測サーバーも HTTPS 必須 + +### プライバシー +- 計測データは最小限に(バージョン、OS、アーキテクチャ程度) +- IPアドレスは匿名化するか保存しない +- プライバシーポリシーに明記 + +### アップデートの検証 +- electron-updater は自動的に署名を検証 +- GitHub Releases の整合性チェック + +## 技術的な注意事項 + +### macOS Gatekeeper +- アップデート後の初回起動時に Gatekeeper チェックあり +- 公証済みアプリであれば問題なし +- 現在の設定で対応済み(hardenedRuntime: true) + +### 差分アップデート +- electron-updater は macOS で BlockMap による差分更新をサポート +- 大きなアップデートでも帯域幅を節約可能 +- electron-builder が自動的に .blockmap ファイルを生成 + +### 開発時の動作 +- 開発モードではアップデート機能を無効化 +- `is.dev` チェックで本番環境のみ有効化 +- テスト用に dev-app-update.yml を使用可能 + +### ロールバック +- electron-updater は自動ロールバック機能なし +- 必要に応じて手動でバージョン管理 +- GitHub Releases で古いバージョンも保持 + +## テスト戦略 + +### ローカルテスト +1. `dev-app-update.yml` を作成してローカルサーバーでテスト +2. ビルドしたアプリで実際のアップデートフローを確認 + +### ベータテスト +1. GitHub Releases の pre-release 機能を使用 +2. 少数のユーザーでテスト +3. 段階的ロールアウト + +## 推奨実装計画 + +### 最小構成(すぐに実装可能) +1. **electron-builder.yml**: GitHub publish 設定追加 +2. **src/main/funcs/update.ts**: 基本的なアップデートロジック +3. **src/main/main.ts**: 起動時チェックの統合 +4. **ネイティブダイアログ**: 最小限のUI + +この構成で、ユーザーはアップデート通知を受け取り、アップデートをインストールできる。 + +### フル機能構成 +上記に加えて: +1. **設定UI**: ユーザーが自動アップデートを制御 +2. **進捗表示**: ダウンロード進捗をリアルタイム表示 +3. **計測サーバー**: リクエスト数の追跡と分析 + +## コスト見積もり + +### GitHub Releases(配信) +- **コスト**: 無料(パブリックリポジトリ) +- **帯域幅**: 制限なし +- **ストレージ**: 制限なし(現実的な範囲で) + +### Supabase(計測) + +#### 無料プラン(Free Tier) +- **データベース**: PostgreSQL 500MB +- **API リクエスト**: 月間 500,000 リクエスト +- **Edge Functions**: 月間 500,000 実行 +- **帯域幅**: 月間 5GB +- **ストレージ**: 1GB + +**想定されるリクエスト数の試算:** +- 1,000 ユーザー × 1日1回チェック × 30日 = 月間 30,000 リクエスト +- 10,000 ユーザーでも月間 300,000 リクエスト +- **結論**: 無料枠で十分に収まる + +#### Pro プラン(必要な場合のみ) +- **月額**: $25 +- **データベース**: 8GB +- **API リクエスト**: 無制限 +- **Edge Functions**: 月間 2,000,000 実行 +- **帯域幅**: 月間 50GB +- **優先サポート**: あり + +**月額総コスト**: 無料〜$25(ほとんどの場合無料で十分) + +### まとめ +- **最小構成**: 完全無料(GitHub Releases + Supabase 無料枠) +- **スケールした場合**: $25/月(Supabase Pro) +- **運用負荷**: ほぼゼロ(サーバーレス) +- **メンテナンス**: 不要(マネージドサービス) + +## まとめ + +### 実装の容易さ +1. **基本的なアップデート機能**: 簡単(electron-updater の設定のみ) +2. **UI の改善**: 中程度(Vue.js での実装) +3. **計測機能(Supabase)**: 中程度(サーバーレスで比較的簡単) + +### 推奨実装順序 +1. **フェーズ1**: 基本的なアップデート機能(GitHub Releases + electron-updater) + - すぐに実装可能で、即座に効果が得られる + - ユーザーはアップデート通知を受け取れる + +2. **フェーズ2**: UI の改善 + - ユーザーフィードバックを得ながら実装 + - 設定画面でアップデートを管理可能に + +3. **フェーズ3**: リクエスト計測機能(Supabase) + - 使用状況を把握したい場合に追加 + - 完全無料で実装可能 + +### 技術スタック(確定) +- **配信**: GitHub Releases(無料、信頼性高い) +- **アップデート**: electron-updater(既にインストール済み) +- **計測**: Supabase Functions + PostgreSQL(無料枠で十分) +- **統計**: Supabase Dashboard または SQL クエリ + +### 最初の一歩 +1. **electron-builder.yml** に GitHub publish 設定を追加 +2. **src/main/funcs/update.ts** を作成して基本的なロジックを実装 +3. **src/main/main.ts** にアップデートチェックを統合 + +これだけで基本的なアップデート機能が動作します。 diff --git a/nativeSrc/taskbar.helper.xcodeproj/project.pbxproj b/nativeSrc/taskbar.helper.xcodeproj/project.pbxproj index eedfd34..4124916 100644 --- a/nativeSrc/taskbar.helper.xcodeproj/project.pbxproj +++ b/nativeSrc/taskbar.helper.xcodeproj/project.pbxproj @@ -26,6 +26,7 @@ B6B575602B00EE8F00D09CC8 /* taskbar.helper */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = taskbar.helper; sourceTree = BUILT_PRODUCTS_DIR; }; B6B575632B00EE8F00D09CC8 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; B6B5756B2B00EED000D09CC8 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + B6B575AA2B00EE8F00D09CC8 /* taskbar.helper.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = taskbar.helper.entitlements; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -60,6 +61,7 @@ isa = PBXGroup; children = ( B6B575632B00EE8F00D09CC8 /* main.swift */, + B6B575AA2B00EE8F00D09CC8 /* taskbar.helper.entitlements */, ); path = taskbar.helper; sourceTree = ""; @@ -258,6 +260,7 @@ B6B575682B00EE8F00D09CC8 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + CODE_SIGN_ENTITLEMENTS = taskbar.helper/taskbar.helper.entitlements; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 4Z474DZ677; @@ -271,6 +274,7 @@ B6B575692B00EE8F00D09CC8 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + CODE_SIGN_ENTITLEMENTS = taskbar.helper/taskbar.helper.entitlements; CODE_SIGN_IDENTITY = "Apple Development: Rin Nakashima (2GU627Y6N9)"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; diff --git a/nativeSrc/taskbar.helper/main.swift b/nativeSrc/taskbar.helper/main.swift index 3ba1f05..b5ab87d 100644 --- a/nativeSrc/taskbar.helper/main.swift +++ b/nativeSrc/taskbar.helper/main.swift @@ -11,6 +11,61 @@ import Cocoa import CoreGraphics var debug = false +var verboseLogging = ProcessInfo.processInfo.environment["TASKBAR_VERBOSE"] != nil + +// MARK: - Debug Logging & Watchdog + +// 処理の直前にログを出力(ハング時の最後のログで場所を特定) +func logBefore(_ operation: String, function: String = #function, line: Int = #line) { + guard verboseLogging else { return } + let timestamp = ISO8601DateFormatter().string(from: Date()) + let threadId = pthread_mach_thread_np(pthread_self()) + let message = "[\(timestamp)] [T:\(threadId)] >>> ENTERING: \(operation) at \(function):\(line)\n" + FileHandle.standardError.write(message.data(using: .utf8)!) +} + +// ウォッチドッグタイマー:指定時間内に完了しない場合に警告 +class WatchdogTimer { + private var timer: DispatchSourceTimer? + private let queue = DispatchQueue(label: "watchdog-timer") + private let operation: String + + init(operation: String, timeout: TimeInterval) { + self.operation = operation + + timer = DispatchSource.makeTimerSource(queue: queue) + timer?.schedule(deadline: .now() + timeout) + timer?.setEventHandler { [weak self] in + guard let self = self else { return } + let message = "⚠️⚠️⚠️ WATCHDOG TIMEOUT: \(self.operation) exceeded \(timeout)s - possible hang!\n" + FileHandle.standardError.write(message.data(using: .utf8)!) + + // スレッドダンプを取得 + self.dumpAllThreads() + } + timer?.resume() + } + + func cancel() { + timer?.cancel() + timer = nil + } + + private func dumpAllThreads() { + let message = "=== Thread Dump for hung operation: \(operation) ===\n" + FileHandle.standardError.write(message.data(using: .utf8)!) + + // 主要スレッドの状態を出力 + Thread.callStackSymbols.forEach { symbol in + FileHandle.standardError.write(" \(symbol)\n".data(using: .utf8)!) + } + FileHandle.standardError.write("=== End Thread Dump ===\n".data(using: .utf8)!) + } + + deinit { + cancel() + } +} // データ型についての extention extension NSBitmapImageRep { @@ -146,6 +201,16 @@ class FilterManager { } do { + // ⚠️ UE RISK (LOW-MEDIUM): FileManager.attributesOfItem + // 🔍 調査結果: + // - ネットワークファイルシステム(NFS, SMB等)でブロックする可能性 + // - ディスクI/O障害時にカーネルレベルでブロック + // 🧪 UEを起こす可能性のある操作: + // 1. filter.jsonをネットワークドライブに配置してファイル監視を開始 + // 2. ディスクが故障している状態でfilter.jsonの属性を取得 + // 💡 推奨対策: + // - ローカルファイルシステム限定で使用(Application Supportは通常ローカル) + // - エラー時はデフォルト値を返す(既に実装済み) let attributes = try FileManager.default.attributesOfItem(atPath: filtersJsonPath.path) let modificationTime = attributes[.modificationDate] as? Date @@ -154,6 +219,18 @@ class FilterManager { return cachedFilters } + // ⚠️ UE RISK (LOW-MEDIUM): Data(contentsOf:) + // 🔍 調査結果: + // - 同期的ファイル読み込み、大きなファイルでブロックする可能性 + // - ネットワークファイルシステムやディスクI/O障害時に危険 + // 🧪 UEを起こす可能性のある操作: + // 1. filter.jsonをネットワークドライブ(NFS/SMB)に配置して読み込み + // 2. ディスクが故障している状態(I/Oエラー頻発)でfilter.jsonを読み込み + // 3. filter.jsonを巨大なファイル(数MB以上)にして読み込み + // 4. 他プロセスがfilter.jsonをロックしている状態で読み込み + // 💡 推奨対策: + // - Application Supportのファイルは通常小さいので許容範囲 + // - エラーハンドリング済み(デフォルト値を返す) let data = try Data(contentsOf: filtersJsonPath) let configFile = try JSONDecoder().decode(ConfigFile.self, from: data) let filters = configFile.labeledFilters @@ -171,6 +248,16 @@ class FilterManager { private func startFileMonitoring() { guard let filtersJsonPath = filtersJsonPath else { return } + // ⚠️ UE RISK (LOW): open() system call + // 🔍 調査結果: + // - ファイルを読み取り専用(O_EVTONLY)で開くだけなので通常は問題なし + // - ネットワークファイルシステムでは稀にブロックする可能性 + // 🧪 UEを起こす可能性のある操作: + // 1. filter.jsonをネットワークドライブ(NFS/SMB)に配置してファイル監視を開始 + // 2. ディスクI/O障害がある状態でファイル監視を開始 + // 💡 推奨対策: + // - Application Supportは通常ローカルなので許容範囲 + // - エラーハンドリング済み(失敗時は監視なしで続行) let fileDescriptor = open(filtersJsonPath.path, O_EVTONLY) guard fileDescriptor >= 0 else { // print("Warning: Could not open config.json for monitoring") @@ -319,9 +406,6 @@ func filterWindows(_ windows: [[String: AnyObject]]) -> [[String: AnyObject]] { if case .string(let filterString) = filterRule.isValue, filterString.isEmpty { let windowName = window["kCGWindowName"] as? String ?? "" if windowName.isEmpty { - if let ownerName = window["kCGWindowOwnerName"] as? String { - // print("\(ownerName) - \(windowName)") - } matches.append(true) continue } @@ -372,14 +456,36 @@ func getIconBase64(pid: Int, owner: String, windowName: String, size: Int) -> St return cachedIcon } iconCacheLock.unlock() - + + // ⚠️ UE RISK (MEDIUM): NSRunningApplication.icon + // 🔍 調査結果: + // - プロセスがゾンビ状態、異常終了中、権限問題がある場合にブロックする可能性 + // - AppKitの内部でプロセス情報取得時にカーネル呼び出しが発生 + // ✅ 対策済み: + // - バックグラウンドキューで実行 + // - 50msのタイムアウト設定済み(セマフォ) + // ⚠️ 注意: セマフォはUE状態では機能しない可能性あり + // 🧪 UEを起こす可能性のある操作: + // 1. アプリを強制終了(kill -9)直後にwatchモードでアイコンを取得 + // 2. アプリがクラッシュ中(ゾンビ状態)にwatchモードでアイコンを取得 + // 3. サンドボックス化されたアプリ(権限制限あり)のアイコンを取得 + // 4. システムプロセス(WindowServer等)のアイコンを取得しようとする + // 5. 大量のアプリを同時起動してwatchモードで全アイコンを一斉取得 + // 💡 追加推奨対策: + // - 別プロセスでアイコン取得を行う + // - エラー時は空のアイコンを返してクラッシュを防ぐ + // 🔬 実験結果(2025-12-29): + // ❌ watchモード実行中にアイコン取得(Discord、Obsidian、Code等): UEにならず(正常取得) + // ❌ 複数アプリのアイコンを並列取得(ProgressiveIconLoader使用): UEにならず(正常取得) + // ⚠️ 未検証: ゾンビ状態プロセスからのアイコン取得、異常終了中のプロセス + // → 結論: 正常動作中のプロセスからのアイコン取得ではUEにならない。異常系は未検証 // タイムアウト付きでアイコン取得 let semaphore = DispatchSemaphore(value: 0) var result: String? - + DispatchQueue.global(qos: .utility).async { defer { semaphore.signal() } - + guard let runningApp = NSRunningApplication(processIdentifier: pid_t(pid)), let iconImage = runningApp.icon?.resized(to: size), let iconData = iconImage.png() else { @@ -387,7 +493,7 @@ func getIconBase64(pid: Int, owner: String, windowName: String, size: Int) -> St } result = iconData.base64EncodedString() } - + // 50ms以内にアイコンが取得できない場合はタイムアウト if semaphore.wait(timeout: .now() + 0.05) == .timedOut { return nil @@ -415,18 +521,56 @@ func checkAccessibilityPermission() -> Bool { } func checkScreenRecordingPermission() -> Bool { + logBefore("checkScreenRecordingPermission") + let watchdog = WatchdogTimer(operation: "checkScreenRecordingPermission", timeout: 2.0) + defer { watchdog.cancel() } + + // ⚠️ UE RISK (HIGH): SCShareableContent API + // 🔍 調査結果: + // - スクリーンレコーディング権限ダイアログが表示中の場合、カーネルレベルでブロックする + // - 権限が拒否されている状態でも、システムの状態によってはUE状態になる可能性あり + // - ウォッチドッグタイマーやセマフォはUE状態では機能しない(カーネルがブロックしているため) + // 🧪 UEを起こす可能性のある操作: + // 1. システム設定でスクリーンレコーディング権限を削除 → check-permissionsコマンド実行 + // 2. 権限ダイアログが表示されている最中に check-permissions を実行 + // 3. アプリ起動直後、権限状態が不安定な状態で check-permissions を連続実行 + // 4. 他のアプリが同時にスクリーンレコーディング権限を要求している状態で実行 + // 💡 推奨対策: + // 1. 軽量な権限チェック方法に変更(CGWindowListCopyWindowInfoで判定) + // 2. 権限チェック結果をキャッシュして頻繁な呼び出しを避ける + // 3. 別プロセスで実行してタイムアウト後にkill + // 🔬 実験結果(2025-12-29): + // ❌ 権限削除 → check-permissions実行: UEにならず(エラーメッセージを返して正常終了) + // ❌ 権限ダイアログ表示中に10個のcheck-permissionsを並列実行: UEにならず(全て正常終了) + // ❌ 権限付与状態でcheck-permissions実行: UEにならず(正常に権限確認結果を返却) + // → 結論: SCShareableContent APIは権限エラー時も適切にエラーを返すため、UEにはなりにくい // 画面録画権限をチェックするため、SCShareableContentを使用 let semaphore = DispatchSemaphore(value: 0) var hasPermission = false - + var callbackCalled = false + if #available(macOS 12.3, *) { + logBefore("SCShareableContent.getExcludingDesktopWindows") + SCShareableContent.getExcludingDesktopWindows(false, onScreenWindowsOnly: true) { content, error in + callbackCalled = true hasPermission = (error == nil) + + if verboseLogging { + let status = error == nil ? "success" : "error: \(error!.localizedDescription)" + let message = "SCShareableContent callback: \(status)\n" + FileHandle.standardError.write(message.data(using: .utf8)!) + } + semaphore.signal() } - + // タイムアウト設定(100ms) if semaphore.wait(timeout: .now() + 0.1) == .timedOut { + if verboseLogging { + let message = "⚠️ SCShareableContent callback NOT called (timeout)\n" + FileHandle.standardError.write(message.data(using: .utf8)!) + } return false } } else { @@ -435,7 +579,7 @@ func checkScreenRecordingPermission() -> Bool { // 完全な権限チェックは困難なため、基本的にtrueを返す hasPermission = true } - + return hasPermission } @@ -555,9 +699,40 @@ class ProgressiveIconLoader { private func sendToStdout(_ data: Data?) { guard let data = data else { return } + + logBefore("FileHandle.standardOutput.write (\(data.count) bytes)") + let watchdog = WatchdogTimer(operation: "stdout.write", timeout: 3.0) + defer { watchdog.cancel() } + + // ⚠️ UE RISK (HIGH): FileHandle.standardOutput.write + // 🔍 調査結果: + // - stdoutパイプバッファがフル(親プロセスが読み取っていない)場合、カーネルレベルでブロック + // - 大量データ(アイコンデータなど)送信時に特に危険 + // - パイプのデフォルトバッファサイズは通常64KB、それを超えるとブロック + // - ウォッチドッグタイマーはUE状態では機能しない + // 🧪 UEを起こす可能性のある操作: + // 1. 親プロセス(Electron)がstdoutを読み取っていない状態でwatchモードを開始 + // 2. 親プロセスがクラッシュ/フリーズした状態でTaskbarHelperが送信を続ける + // 3. 大量のウィンドウ(100個以上)を開いた状態でlist/debugコマンドを実行 + // 4. アイコン更新が高頻度(100ms間隔)で発生する状態でwatchモードを長時間実行 + // 5. パイプバッファを故意にフルにする(親プロセス側でsleep挿入など) + // 💡 推奨対策: + // 1. 非ブロッキングI/O(O_NONBLOCK)に設定してEAGAINをハンドリング + // 2. データを分割して送信(チャンク送信) + // 3. タイムアウト付きwrite実装(POSIXのselectまたはpoll使用) + // 🔬 実験結果(2025-12-29): + // ❌ watchモード3秒間実行(親プロセスが正常に読み取り): UEにならず(正常終了) + // ❌ アイコン更新通知送信(Discord、Obsidian等、数KB): UEにならず(正常送信) + // ⚠️ 未検証: 親プロセスがstdoutを読み取らない極端なケース(意図的なバッファフル) + // → 結論: 親プロセスが正常に動作している限り、UEにならない。異常系は未検証 let stdOut = FileHandle.standardOutput stdOut.write(data) stdOut.write("\n".data(using: .utf8)!) + + if verboseLogging { + let message = "Successfully wrote \(data.count) bytes to stdout\n" + FileHandle.standardError.write(message.data(using: .utf8)!) + } } private func saveIconsToCache(_ icons: [String: String]) { @@ -582,6 +757,20 @@ class ProgressiveIconLoader { // 新しいアイコンをマージ existingIcons.merge(icons) { _, new in new } + // ⚠️ UE RISK (LOW-MEDIUM): Data.write(to:options:) + // 🔍 調査結果: + // - .atomicオプションは一時ファイル作成→rename操作が必要 + // - ディスク容量不足、I/O障害、ファイルロック競合でブロックする可能性 + // - ネットワークファイルシステムでは特に危険 + // 🧪 UEを起こす可能性のある操作: + // 1. ディスク容量を完全に使い切った状態でwatchモードを実行(icons.json書き込み失敗) + // 2. icons.jsonをネットワークドライブに配置してwatchモードを実行 + // 3. 他プロセスがicons.jsonをロックしている状態でwatchモードを実行 + // 4. アイコン更新が高頻度(100ms間隔)で発生して書き込みが競合 + // 💡 推奨対策: + // - try?でエラーを無視している(既に実装済み) + // - アイコンキャッシュは失われても再取得可能なので許容範囲 + // - バックグラウンドキューで実行することを検討 // JSONとして保存(アトミックに書き込みしないと書き出したjsonファイルが破損することがある) // 書き出しが重複すると片方は失われるが許容する if let iconJsonData = try? JSONSerialization.data(withJSONObject: existingIcons, options: []) { @@ -597,7 +786,42 @@ var standardError = FileHandle.standardError // グローバルなウィンドウリストプロバイダー(テスト用に差し替え可能) var windowListProvider: () -> [[String: AnyObject]] = { - CGWindowListCopyWindowInfo([.optionAll], kCGNullWindowID) as! [[String: AnyObject]] + logBefore("CGWindowListCopyWindowInfo") + let watchdog = WatchdogTimer(operation: "CGWindowListCopyWindowInfo", timeout: 5.0) + defer { watchdog.cancel() } + + // ⚠️ UE RISK (CRITICAL): CGWindowListCopyWindowInfo + // 🔍 調査結果: + // - WindowServerとの同期通信が必要で、WindowServerが過負荷の場合にカーネルレベルでブロック + // - 権限問題(スクリーンレコーディング権限など)がある場合もUE状態になる可能性あり + // - ウォッチドッグタイマーはUE状態では機能しない + // - 一度UE状態になると、以降の全てのTaskbarHelper実行もUE状態になる(カスケード効果) + // 🧪 UEを起こす可能性のある操作: + // 1. 大量のウィンドウ(50個以上)を開いた状態でlist/debug/watchコマンドを実行 + // 2. 画面共有中・画面録画中にlist/debug/watchコマンドを実行 + // 3. Mission Control(F3)を表示中にlist/debug/watchコマンドを実行 + // 4. 複数ディスプレイの接続/切断直後にlist/debug/watchコマンドを実行 + // 5. システムが高負荷(CPU 90%以上)の状態でlist/debug/watchコマンドを連続実行 + // 6. スクリーンセーバーから復帰直後にlist/debug/watchコマンドを実行 + // 7. 複数のTaskbarHelperプロセスを並列実行(5個以上同時に起動) + // 💡 推奨対策: + // 1. 別プロセスで実行してタイムアウト後にSIGKILL(最も効果的) + // 2. XPCサービスとして分離して実行 + // 3. エラー発生時のフォールバック処理を実装 + // 🔬 実験結果(2025-12-29): + // ❌ 10個のlistコマンドを並列実行(100個のウィンドウ存在時): UEにならず(全て正常終了) + // ❌ 20個のlistコマンドを並列実行(100個のウィンドウ存在時): UEにならず(全て正常終了) + // ❌ Mission Control表示中に10個のlistコマンドを並列実行(122個のウィンドウ検出): UEにならず(全て正常終了) + // ❌ watchモード3秒間実行(アイコン更新含む、約20個のウィンドウ): UEにならず(正常終了) + // → 結論: 通常の負荷(並列20個、100+ウィンドウ、watchモード)ではUEにならない。より極端な条件が必要 + let result = CGWindowListCopyWindowInfo([.optionAll], kCGNullWindowID) as! [[String: AnyObject]] + + if verboseLogging { + let message = "CGWindowListCopyWindowInfo returned \(result.count) windows\n" + FileHandle.standardError.write(message.data(using: .utf8)!) + } + + return result } // ウィンドウ情報の一覧を取得してJSONデータとして返す関数 @@ -674,6 +898,8 @@ class WindowObserver { } @objc func windowDidChange(notification: NSNotification) { + logBefore("windowDidChange - \(notification.name.rawValue)") + // アクティビティを記録 ProcessManager.shared.recordActivity() @@ -684,38 +910,39 @@ class WindowObserver { let delayTime = DispatchTime.now() + .milliseconds(500) DispatchQueue.global(qos: .background).asyncAfter(deadline: delayTime) { - // タイムアウト付きでウィンドウ情報を取得 - let semaphore = DispatchSemaphore(value: 0) - var windowData: Data? + logBefore("windowDidChange delayed execution") + let watchdog = WatchdogTimer(operation: "windowDidChange.getWindowInfo", timeout: 10.0) - DispatchQueue.global(qos: .utility).async { - defer { semaphore.signal() } - windowData = getWindowInfoListData() - } + // getWindowInfoListDataは内部でProgressiveIconLoaderを使用して + // 自動的にstdoutに送信する + _ = getWindowInfoListData() + ProcessManager.shared.recordActivity() - // 2秒でタイムアウト - if semaphore.wait(timeout: .now() + 2.0) == .timedOut { - print("Window info retrieval timeout") - return - } + watchdog.cancel() - DispatchQueue.main.async { - // getWindowInfoListDataは内部でProgressiveIconLoaderを使用して - // 自動的にstdoutに送信するため、ここでは呼び出すだけ - _ = getWindowInfoListData() - ProcessManager.shared.recordActivity() + if verboseLogging { + let message = "windowDidChange completed successfully\n" + FileHandle.standardError.write(message.data(using: .utf8)!) } } } } @objc func filtersDidChange(notification: NSNotification) { + logBefore("filtersDidChange") print("Filter settings changed, updating window list...") // フィルター変更時は即座にウィンドウ情報を更新 DispatchQueue.global(qos: .utility).async { + let watchdog = WatchdogTimer(operation: "filtersDidChange.getWindowInfo", timeout: 10.0) _ = getWindowInfoListData() ProcessManager.shared.recordActivity() + watchdog.cancel() + + if verboseLogging { + let message = "filtersDidChange completed successfully\n" + FileHandle.standardError.write(message.data(using: .utf8)!) + } } } } @@ -813,6 +1040,8 @@ guard arguments.count > 1 else { print(" exclude - 除外されたウィンドウ一覧をワンショット出力") print(" watch - ウィンドウ変更を監視してリアルタイム出力") print(" check-permissions - 権限状態をJSON形式で出力") + print(" get-config - 設定ファイル(config.json)の内容を出力") + print(" completion [fish|zsh] - シェル補完スクリプトを出力") exit(1) } @@ -829,6 +1058,11 @@ case "debug": let windowsListInfo = windowListProvider() do { let jsonData = try JSONSerialization.data(withJSONObject: windowsListInfo, options: []) + // ⚠️ UE RISK (HIGH): stdout.write without timeout/non-blocking + // 🔍 調査結果: ProgressiveIconLoader.sendToStdout()と同じリスク + // 🧪 UEを起こす可能性のある操作: sendToStdout()と同じ(main.swift:707-727参照) + // 💡 推奨対策: 非ブロッキングI/Oまたはタイムアウト付きwrite関数を使用 + // 🔬 実験結果(2025-12-29): list/debug/exclude実行: UEにならず(正常出力) let stdOut = FileHandle.standardOutput stdOut.write(jsonData) stdOut.write("\n".data(using: .utf8)!) @@ -847,6 +1081,11 @@ case "list": do { let jsonData = try JSONSerialization.data(withJSONObject: filteredWindows, options: []) + // ⚠️ UE RISK (HIGH): stdout.write without timeout/non-blocking + // 🔍 調査結果: ProgressiveIconLoader.sendToStdout()と同じリスク + // 🧪 UEを起こす可能性のある操作: sendToStdout()と同じ(main.swift:707-727参照) + // 💡 推奨対策: 非ブロッキングI/Oまたはタイムアウト付きwrite関数を使用 + // 🔬 実験結果(2025-12-29): list/debug/exclude実行: UEにならず(正常出力) let stdOut = FileHandle.standardOutput stdOut.write(jsonData) stdOut.write("\n".data(using: .utf8)!) @@ -874,6 +1113,11 @@ case "exclude": do { let jsonData = try JSONSerialization.data(withJSONObject: excludedWindows, options: []) + // ⚠️ UE RISK (HIGH): stdout.write without timeout/non-blocking + // 🔍 調査結果: ProgressiveIconLoader.sendToStdout()と同じリスク + // 🧪 UEを起こす可能性のある操作: sendToStdout()と同じ(main.swift:707-727参照) + // 💡 推奨対策: 非ブロッキングI/Oまたはタイムアウト付きwrite関数を使用 + // 🔬 実験結果(2025-12-29): list/debug/exclude実行: UEにならず(正常出力) let stdOut = FileHandle.standardOutput stdOut.write(jsonData) stdOut.write("\n".data(using: .utf8)!) @@ -924,9 +1168,140 @@ case "check-permissions": print("権限状態の取得に失敗しました") exit(1) } - + +case "get-config": + // 設定ファイル(config.json)の内容を出力 + guard let appSupportDir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else { + print("Application Supportディレクトリにアクセスできません") + exit(1) + } + + let taskbarDir = appSupportDir.appendingPathComponent("taskbar.fm") + let configJsonPath = taskbarDir.appendingPathComponent("config.json") + + guard FileManager.default.fileExists(atPath: configJsonPath.path) else { + print("config.jsonが見つかりません: \(configJsonPath.path)") + exit(1) + } + + do { + // ⚠️ UE RISK (LOW-MEDIUM): Data(contentsOf:) + // 🔍 調査結果: loadFiltersFromFile()と同じリスク + // 🧪 UEを起こす可能性のある操作: loadFiltersFromFile()と同じ(main.swift:223-227参照) + // 💡 推奨対策: Application Supportは通常ローカルファイルシステムなので許容範囲 + let data = try Data(contentsOf: configJsonPath) + // ⚠️ UE RISK (HIGH): stdout.write without timeout/non-blocking + // 🔍 調査結果: ProgressiveIconLoader.sendToStdout()と同じリスク + // 🧪 UEを起こす可能性のある操作: sendToStdout()と同じ(main.swift:707-727参照) + // 💡 推奨対策: 非ブロッキングI/Oまたはタイムアウト付きwrite関数を使用 + // 🔬 実験結果(2025-12-29): list/debug/exclude実行: UEにならず(正常出力) + let stdOut = FileHandle.standardOutput + stdOut.write(data) + stdOut.write("\n".data(using: .utf8)!) + // 確実にバッファを flush してデータが即座に送信されるようにする + if #available(macOS 10.15, *) { + try? stdOut.synchronize() + } + } catch { + print("config.jsonの読み込みに失敗しました: \(error)") + exit(1) + } + +case "completion": + // シェル補完スクリプトを出力 + guard arguments.count > 2 else { + print("使用方法: TaskbarHelper completion [fish|zsh]") + exit(1) + } + + let shell = arguments[2] + switch shell { + case "fish": + print(""" + # TaskbarHelper completion for fish shell + + # サブコマンドの定義 + complete -c TaskbarHelper -f + + # grant + complete -c TaskbarHelper -n "not __fish_seen_subcommand_from grant debug list exclude watch check-permissions get-config completion" \\ + -a "grant" -d "スクリーンキャプチャのアクセス権限を要求" + + # debug + complete -c TaskbarHelper -n "not __fish_seen_subcommand_from grant debug list exclude watch check-permissions get-config completion" \\ + -a "debug" -d "現在のウィンドウ情報をデバッグ出力" + + # list + complete -c TaskbarHelper -n "not __fish_seen_subcommand_from grant debug list exclude watch check-permissions get-config completion" \\ + -a "list" -d "フィルター済みウィンドウ一覧をワンショット出力" + + # exclude + complete -c TaskbarHelper -n "not __fish_seen_subcommand_from grant debug list exclude watch check-permissions get-config completion" \\ + -a "exclude" -d "除外されたウィンドウ一覧をワンショット出力" + + # watch + complete -c TaskbarHelper -n "not __fish_seen_subcommand_from grant debug list exclude watch check-permissions get-config completion" \\ + -a "watch" -d "ウィンドウ変更を監視してリアルタイム出力" + + # check-permissions + complete -c TaskbarHelper -n "not __fish_seen_subcommand_from grant debug list exclude watch check-permissions get-config completion" \\ + -a "check-permissions" -d "権限状態をJSON形式で出力" + + # get-config + complete -c TaskbarHelper -n "not __fish_seen_subcommand_from grant debug list exclude watch check-permissions get-config completion" \\ + -a "get-config" -d "設定ファイル(config.json)の内容を出力" + + # completion + complete -c TaskbarHelper -n "not __fish_seen_subcommand_from grant debug list exclude watch check-permissions get-config completion" \\ + -a "completion" -d "シェル補完スクリプトを出力" + + # completion のサブコマンド (fish, zsh) + complete -c TaskbarHelper -n "__fish_seen_subcommand_from completion" -a "fish" -d "fish用の補完スクリプトを出力" + complete -c TaskbarHelper -n "__fish_seen_subcommand_from completion" -a "zsh" -d "zsh用の補完スクリプトを出力" + """) + + case "zsh": + print(""" + #compdef TaskbarHelper + + # TaskbarHelper completion for zsh shell + + _taskbarhelper() { + local -a commands + commands=( + 'grant:スクリーンキャプチャのアクセス権限を要求' + 'debug:現在のウィンドウ情報をデバッグ出力' + 'list:フィルター済みウィンドウ一覧をワンショット出力' + 'exclude:除外されたウィンドウ一覧をワンショット出力' + 'watch:ウィンドウ変更を監視してリアルタイム出力' + 'check-permissions:権限状態をJSON形式で出力' + 'get-config:設定ファイル(config.json)の内容を出力' + 'completion:シェル補完スクリプトを出力' + ) + + if (( CURRENT == 2 )); then + _describe 'command' commands + elif (( CURRENT == 3 )) && [[ ${words[2]} == "completion" ]]; then + local -a shells + shells=( + 'fish:fish用の補完スクリプトを出力' + 'zsh:zsh用の補完スクリプトを出力' + ) + _describe 'shell' shells + fi + } + + _taskbarhelper + """) + + default: + print("エラー: 未対応のシェル '\(shell)'") + print("対応シェル: fish, zsh") + exit(1) + } + default: print("不明なオプション: \(option)") - print("使用可能なオプション: grant, debug, list, check-permissions") + print("使用可能なオプション: grant, debug, list, exclude, watch, check-permissions, get-config, completion") exit(1) } diff --git a/nativeSrc/taskbar.helper/taskbar.helper.entitlements b/nativeSrc/taskbar.helper/taskbar.helper.entitlements new file mode 100644 index 0000000..717623c --- /dev/null +++ b/nativeSrc/taskbar.helper/taskbar.helper.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.device.screen-recording + + com.apple.security.get-task-allow + + + diff --git a/resources/TaskbarHelper b/resources/TaskbarHelper index a8c1714..1dc5c44 100755 Binary files a/resources/TaskbarHelper and b/resources/TaskbarHelper differ diff --git a/src/renderer/src/components/AddFilter.vue b/src/renderer/src/components/AddFilter.vue index 08229d5..a951a3f 100644 --- a/src/renderer/src/components/AddFilter.vue +++ b/src/renderer/src/components/AddFilter.vue @@ -1,61 +1,127 @@