電源を切ると直るバグを追う — ThinkPadキーボードのスクロールだけが死ぬ理由
電源を切ると直るバグを追う — ThinkPadキーボードのスクロールだけが死ぬ理由
症状は、条件のほうが奇妙だった
Lenovo の外付けキーボード ThinkPad TrackPoint Keyboard II を使っていると、ときどき中ボタンを押しながら赤ポチを倒すスクロールが効かなくなる。キー入力は打てる。左クリックも右クリックもポインタ移動も正常。スクロールだけが死ぬ。
発生条件として申告されたのは次の3つだった。
- 席を離れて Windows が自動スリープし、電源ボタンで復帰させたときに起きやすい
- Windows の「再起動」では直らないことがある
- 電源をオフにしてから入れ直すと直る
最後の2つが噛み合わない。ふつう、再起動のほうが電源オフ→オンより「深い」初期化に見える。実際には逆で、再起動では USB ポートへの給電が維持され、完全な電源断ならデバイス側がリセットされる、という説明は成り立つ。だからこの時点での見立ては「USB デバイスかハブが、スリープ復帰時のリセットに失敗して変な状態で固まっている」だった。
結論から言うと、この見立ては外れだった。そして最終的な犯人は、USB でもドライバでもなかった。
調べる対象を先に押さえる
デバイスを列挙すると VID_17EF(Lenovo)PID_60EE が 2台分出てきた。左手用と右手用に、同じ型を2台つないでいるためだ。あとで「2台あることが競合の原因では」という疑いが出てくるので、この時点で頭に入れておく。
あわせて、効いてきそうな条件を先に並べておく。
| 項目 | 値 |
|---|---|
| OS | Windows 11 Pro 10.0.26200 |
| キーボード | ThinkPad TrackPoint Keyboard II × 2台(USB接続) |
| 接続経路 | Genesys Logic の USB ハブを2段カスケード |
| スリープ | S3 のみ(Modern Standby はファームウェア非対応) |
| 休止状態・高速スタートアップ | 無効 |
| USB セレクティブサスペンド | AC 側で無効 |
外付けキーボードなので、疑う先は大きく2系統になる。USB の接続経路か、キーボードに付属する Lenovo のソフトか。まず前者から見た。
空振り その1 — 死んだUSBポートは死体ですらなかった
USB のツリーをたどると、それらしいものが見つかった。
USB Root Hub 3.0
└ Generic USB Hub (05E3:0610) ← Genesys Logic 製
└ Generic USB Hub (05E3:0610) ← 2段目
├ port1: ThinkPad Keyboard II
├ port2: ThinkPad Keyboard II
└ port3: Unknown USB Device (Port Reset Failed) ← ?
キーボードと同じハブの隣のポートに、ポートリセットに失敗したデバイスがある。安価な USB ハブがスリープ復帰時のリセットに失敗するのはよくある話で、筋書きとしては悪くない。
詳細を見て、この線は消えた。
Problem : CM_PROB_PHANTOM
ProblemDescription : Currently, this hardware device is not connected to the computer. (Code 45).
Present : False
CM_PROB_PHANTOM(コード45)は「今は接続されていない」という意味で、デバイスマネージャーで「非表示のデバイスを表示」にしたときだけ見える幽霊エントリだ。過去に挿していた何かの登録が残っているだけで、現在の不具合とは関係がない。
USB の電源管理も見たが、セレクティブサスペンドは AC 側で無効になっていた。デスクトップなので常時 AC であり、実効しない。
空振り1つ目。 ただし後述するとおり、この2つの否定は後で外部レビューに突き返されることになる。
手がかりは「壊れ方の非対称性」だった
行き詰まったので、症状そのものを見直した。
キー入力は生きている。左右クリックも生きている。スクロールだけが死ぬ。
これは強い手がかりだ。もしキーボードのUSB接続そのものが壊れているなら、キー入力も一緒に死ぬはずで、そうなっていない。つまりキーボードから Windows までの標準的な経路は無事で、スクロールだけが別の仕組みに依存していることになる。
そこで、その「別の仕組み」がどこにあるかを探した。まずカーネル空間を疑う。
Win32_PnPSignedDriver に DriverProviderName = Lenovo のもの → 0 件
HIDClass UpperFilters: (空)
Mouse UpperFilters: mouclass ← Windows標準のみ
Keyboard UpperFilters: kbdclass ← Windows標準のみ
Lenovo のカーネルドライバは1本も入っていなかった。 マウスやキーボードのクラスに独自のフィルタドライバを挟んでもいない。すべて Windows 標準のままだ。
ここで方向が定まる。カーネルに手が入っていないのなら、中ボタンスクロールはユーザーモードの常駐プログラムが、独自の HID インターフェースを読んでホイール操作を合成しているしかない。
そして、その構造なら壊れ方の非対称性が説明できる。キー入力と左右クリックは OS 標準の HID 経路をそのまま流れるので無傷。常駐プログラムに依存するスクロールだけが、単独で落ちる。
空振り その2 — 二世代のドライバが相打ちしている説
インストール済みソフトを見ると、思わぬものが出てきた。
| ソフト名 | バージョン | サービス |
|---|---|---|
| ThinkPad TrackPoint Keyboard II Software | 1.0.8.06241 | ExternalTPKBSvc |
| ThinkPad Compact Keyboard with TrackPoint driver | 1.5.6.0 | ThinkPadKBSvc |
第2世代用と第1世代用のドライバソフトが両方入っていて、両方のサービスが同時に走っていた。 つないでいるキーボードは2台とも第2世代なので、第1世代用は本来不要だ。
さらにインストール先を覗くと、両方のフォルダに同名の実行ファイルが揃っている。
HScrollFun.exe ← HScroll = スクロール機能の実体
MainCpl.exe ← 設定パネル/トレイ常駐
osd.exe ← 画面表示(Caps Lock 等)
SetSpeed.exe ← ポインタ速度
そしてスタートアップ登録が、2世代分そろって並んでいた。aHScrollutility(第1世代)と anHScrollutility(第2世代)という具合に、接頭辞だけ違う同じ顔ぶれが二重に登録されている。
ここで「同じ名前・同じ機能のプロセスが2セット同時に起動しようとして、ミューテックスの取り合いで相打ちになるレースコンディションだ」と考えた。起動タイミングで勝敗が変わるなら、「電源オフ→オンだと直る」という確率的な挙動も説明がつく。
もっともらしい。しかしこれも外れだった。
全滅していた
症状が出ている最中に、常駐プロセスの生死を数えた。ここが分岐点になった。
| プロセス | セッション | 状態 |
|---|---|---|
ExternalTPKBSvc | 0 | 動いている |
ThinkPadKBSvc | 0 | 動いている |
logonsetsvc | 0 | 動いている |
logonset | 1 | 動いている |
HScrollFun | — | いない |
MainCpl / osd / SetSpeed | — | いない |
スクロールの実体である HScrollFun.exe が、そもそも起動していなかった。
レースコンディションどころではない。相打ちする以前に、どちらも土俵に上がっていなかった。
理由はレジストリにあった。Windows はスタートアップの有効・無効を StartupApproved というキーで管理していて、先頭バイトが 2 なら有効、3 なら無効を意味する。
anHScrollutility 3 ← 第2世代スクロール:無効
anRunMaincpl 3 anOSD 3 anSetSpeed 3
aHScrollutility 3 ← 第1世代スクロール:無効
aRunMaincpl 3 aOSD 3 aSetSpeed 3
8本すべてが無効。あとで 32bit 側のビュー(WOW6432Node)にも旧世代の3本が登録されていて、そちらも同じく無効だと分かった。合計11本、全滅である。
おそらく過去のどこかで、起動を軽くする目的でタスクマネージャーのスタートアップタブからまとめて無効化されたのだろう。誰がいつ切ったかまでは追えなかった。
壊れていたときの流れを追うと、こうなる。
キー入力と左右クリックはこの図に登場しない。それらはキーボードから Windows へまっすぐ流れるので、右端の常駐が不在でも何の影響も受けない。この図に描かれている経路だけが、スクロールの生死を握っている。
犯人を1本起動して確定させる
仮説が正しいなら、その1本を手で起動すれば直るはずだ。削除でもインストールでもなく、落とせば元に戻る可逆な操作でもある。
Start-Process "C:\Program Files (x86)\Lenovo\External TrackPoint Keyboard driver\HScrollFun.exe"
3秒後も生きていた(PID 36644, セッション1)。すぐ落ちるわけではない。
そして実機で確認してもらったところ、中ボタンスクロールが復活した。
HScrollFun.exe の不在が直接原因である、と確定した。
外部レビューで、否定した仮説が3つ突き返される
ここで OpenAI の Codex(gpt-5.6-sol)に調査記録を渡し、「やったこと・やっていないこと・仮説」をレビューしてもらった。自分で2回も仮説を外している以上、第三者の目を入れる価値がある。
返ってきた指摘は容赦がなかった。
1つめ。「スタートアップが無効だから二世代競合は否定できる」は成立しない。 StartupApproved が止めるのは Explorer 経由の起動だけで、サービスや独自ランチャーからの起動までは止めない。実際、Codex が独自に調べたところ次の混在が見つかった。
- サービスとして登録されている
logonsetsvcの実体は第1世代側のフォルダにある - しかし
HKLM\Software\ServiceAppPathは第2世代側のlogonset.exeを指している - 両世代が
HKLM\Software\Lenovo\autolaunchという同じキーを共有している
自分でも確認したところ、そのとおりだった。autolaunch に書かれた installed_version は 1.5.6.0、つまり第1世代のバージョンで上書きされている。「旧世代のサービスが新世代の実行ファイルを起動する」というねじれた状態になっていた。
2つめ。「プロセスが生きているか」を見るだけでは自己復旧の判定として足りない。 手動起動で直ったという事実は、次の2つのどちらでも説明できてしまう。
- 常駐プログラムが動いている間じゅう、ずっとスクロールを合成している
- 起動した瞬間にデバイスと結び直しただけで、あとは別の何かが担っている
もし後者なら、プロセスは生きているのに機能だけ死んでいる状態がありえて、生存確認では救えない。
3つめ。否定の根拠が弱いものが他にもある。
- 「2台とも同じ世代だから競合しない」— 単一インスタンスのユーティリティが最初の1台だけを掴む実装は普通にありうる
- 「セレクティブサスペンドが無効だからUSB電源管理は無関係」— 否定できるのは動作中のアイドルサスペンドだけで、S3スリープへの遷移に伴うデバイス電源状態の変化は別の話。これは明確にこちらの誤りだった
- 「幽霊デバイスだから無関係」— 現在いないことしか示さない。過去の復帰時にリセット失敗が起き、それが引き金になった可能性までは消せない
決定打は、人間の目だった
指摘の2つめは対策の有効性に直結する。ところがこれは、意図せず解決した。
検証のために HScrollFun.exe を一度落とし、タスクで復旧させる実験をしていたとき、画面の前にいた本人からこう報告が来た。
「落ちました」と打った時、確かにスクロールができてたのができなくなった。「タスクを発火させます」と言ったその後に、ターミナルが一瞬ちらついてスクロールできるようになった
プロセスを落とした瞬間にスクロールが死に、起こし直した瞬間に生き返った。
これで前者が確定する。HScrollFun.exe は常駐している間ずっとスクロールを合成しており、プロセスの生存がそのまま機能の生存を意味する。ログや API では取れなかった因果が、目の前の人間の観察一発で埋まった。
なお、このプロセスは通常権限からは終了できない。
Stop-Process : アクセスが拒否されました
入力を扱うソフトとして高い整合性レベル(uiAccess)で動いているためで、落とすには昇格が必要だった。副作用として、実行ファイルのパスも通常権限では読み取れない。
対策 — 落ちていたら起こし直す
原因が「起動していないこと」なので、対策は「確実に起動させること」になる。スタートアップを有効に戻す手もあるが、それだけでは復帰中に落ちた場合を拾えない。そこでタスクスケジューラに見張り役を置いた。
- ログオン時(20秒遅延)
- スリープ復帰時 —
Microsoft-Windows-Power-TroubleshooterのイベントID 1 を購読(15秒遅延) - ロック解除時(5秒遅延)
動作は「同じセッション内に HScrollFun がいなければ起動する」。いれば何もしないので二重起動しない。遅延を入れているのは、復帰直後は USB の再列挙が終わっていない可能性があるためだ。管理者権限は不要で、外部ファイルも置いていない。
図1で誰もいなかった真ん中の位置に、見張りが入る。
2枚を見比べると、変えたのは1点だけだと分かる。起動の合図を「ログオン時に1回」から「復帰・ログオン・ロック解除のたび」に増やし、その合図を受け取る役を常駐の手前に置いた。 HScrollFun.exe 自身にも、Lenovo のドライバにも手を入れていない。
Codex の指摘を受けて直した点が2つある。多重起動ポリシーを IgnoreNew から Parallel に変えた(前者だと実行中に届いた復帰トリガーが捨てられる)。それと、次に症状が出たとき「プロセスがいたのか、いなかったのか」を証拠で言えるよう、実行結果を %LOCALAPPDATA%\ThinkPadScrollWatchdog\watchdog.log に追記するようにした。
「登録できた」で終わらせない
タスクを登録した直後、LastTaskResult: 0 が返ってきた。これは「タスクが正常終了した」以上のことを何も意味しない。復旧したかどうかは別の話だ。
そこで実際に壊して試した。最初の試行はこう出た。
BEFORE PID : 36644
AFTER KILL : 1 process ← 落ちていない
AFTER TASK : 36644 ← 同じPIDのまま
Stop-Process が失敗していたのに、-ErrorAction SilentlyContinue でエラーを握り潰していた。検証手順そのものが壊れていたわけで、危うく「動きました」と報告するところだった。
昇格して落とし直し、あらためて通した結果がこれになる。
AFTER KILL count : 0 ← 確かに落ちた(この間スクロールは死んでいた)
REVIVED PID : 38376 ← 別のPIDで復活
そして在中時にタスクを走らせると、二重起動せずログにこう残る。
2026-08-11 13:13:35 sid=1 ALIVE pid=38376
不在なら起こす、在中なら手を出さない。両側とも実機で確認できた。
残っている謎
正直に書くと、当初の症状すべてを説明しきれてはいない。
最大の穴は「電源オフ→オンなら直る」という体験だ。スタートアップが11本とも無効なら、コールドブートでも HScrollFun.exe は起動しないはずで、辻褄が合わない。
別の起動経路を探して、サービスのバイナリを文字列走査した。
| バイナリ | HScrollFun | CreateProcessAsUser | WTSGetActiveConsoleSessionId |
|---|---|---|---|
ExternalTPKBSvc.exe | 無し | 無し | 無し |
logonsetsvc.exe | 無し | 有り | 有り |
logonsetsvc.exe は「サービスからユーザーセッションにプロセスを起こす」ときの定番の組み合わせを持っている。起こす能力はある。ただし起動対象の名前が文字列として現れないため、これが HScrollFun.exe を起こしているという確証は取れなかった。
もう一つ、スリープ復帰との因果も実は未検証のままだ。今回症状を見つけたのは再起動から約10分後であって、スリープ復帰の直後ではない。つまり今回観測したのは「復帰でクラッシュした状態」ではなく「最初から起動していない状態」だった可能性が高い。両者を切り分けるには、実際にスリープ復帰させて観測するしかない。ログを残すようにしたのはそのためでもある。
この件から持ち帰れること
- 申告された条件が矛盾していたら、そこが手がかりになる。 「再起動で直らないのに電源オフで直る」という噛み合わなさが、最初に疑う対象を決めてくれた。結果として外れたが、外れ方が次の一手を教えてくれた
- 壊れ方の非対称性を見る。 全部が死ぬのか、一部だけが死ぬのか。今回は「キーは効くのにスクロールだけ死ぬ」が、カーネルではなくユーザーモードを見ろという道標になった
- もっともらしい仮説ほど、証拠で潰す。 死んだUSBポートも二世代競合も、話としてはよくできていた。前者は
Present: Falseの一行で、後者はプロセス一覧で消えた LastTaskResult: 0は「直った」ではない。 壊して直ることを見るまで、対策が効いた証拠は何もない- 外部レビューは、自分の否定を疑うために使う。 今回 Codex が刺したのは新しい発見ではなく、こちらが「無関係」と切り捨てた根拠の甘さだった。実際1つは明確な誤りだった
調査記録の一次資料は memo/2026-08-11/thinkpad-trackpoint-scroll-investigation.md に置いてある。