動かざることバグの如し

近づきたいよ 君の理想に

Lubuntu26.04でxrdpリモートデスクトップ

環境

  • Lubuntu 26.04

やりたいこと

Lubuntuのマシンへリモートデスクトップをしたい。Ubuntu 26.04からX11が廃止されてWaylandになったためxrdpではなくGNOME Remote Desktopを使用したが、 LubuntuはGNOMEではないため逆にGNOME Remote Desktopが使えない。そこで今までどおりのxrdpを使用する。

blog.turai.work

インストール

apt install xrdp

そのあとにサーバー再起動する。

接続できない

インストール自体はこれで終わるのだが、そのままではRDP接続できなかった。Windowsのリモートデスクトップクライアントから接続してログインまでは通るものの、デスクトップが表示される前に即切断される。

systemctl status xrdp xrdp-sesman でサービス状態を見ると両方とも起動している。つまり xrdp 自体が死んでいるわけではない。

その状態でログを見ると、以下のようなエラーが出ていた。

[ERROR] Xorg server closed connection

さらに /var/log/xrdp-sesman.log を確認すると、LXQtのセッションが起動直後に終了していることが分かる。

[INFO ] Window manager (pid 2290, display 10) finished normally in 0 secs
[WARN ] Window manager (pid 2290, display 10) exited quickly (0 secs).
        This could indicate a window manager config problem
[INFO ] Session on display 10 has finished.

最初は Xorg 側の問題かと思ったが、~/.xorgxrdp.10.log では正常終了扱いだった。つまり落ちていたのは Xorg ではなく、その上で起動する LXQt セッション側である。

直接の症状は「認証後すぐ切断される」である。画面が真っ黒なまま落ちるので分かりにくいが、実際には lxqt-session が起動してすぐ死んでいる。

決定打になったのは ~/.xsession-errors の中身だった。

isPrimaryInstance
XIO: fatal IO error 17 (File exists) on X server ":0"

isPrimaryInstancelxqt-session のシングルインスタンス判定まわりの出力で、すでに同じユーザーで LXQt セッションが動いていることを示している。

実際に確認すると、サーバー側ではローカルログイン済みの LXQt セッションが存在していた。

pgrep -a lxqt-session
loginctl list-sessions

Lubuntuをデスクトップ用途でも使っていると、物理コンソール側でログインしたままRDP接続したくなることがある。今回はまさにその構成でハマった。

原因

原因は、ローカルの LXQt セッションと xrdp 経由の LXQt セッションが同じ D-Bus セッションバスを見に行っていたことだった。

/etc/xrdp/startwm.sh はデフォルトだと /etc/X11/Xsession を実行するだけなので、環境によっては既存の DBUS_SESSION_BUS_ADDRESS をそのまま引き継ぐ。 その状態で startlxqt が起動すると、すでにローカル側で動いている lxqt-session を検出して「自分は2個目のインスタンスだ」と判断し、即終了する。

要するに xrdp が悪いというより、LXQt のシングルインスタンス前提と D-Bus 共有が噛み合っていなかった。

対処法

対処は単純で、RDPセッション専用の D-Bus セッションを作ってから LXQt を起動すればよい。

/etc/xrdp/startwm.sh を以下のように修正する。

変更前

test -x /etc/X11/Xsession && exec /etc/X11/Xsession
exec /bin/sh /etc/X11/Xsession

変更後

unset DBUS_SESSION_BUS_ADDRESS
exec dbus-launch --exit-with-session startlxqt

やっていることは3つだけである。

  • unset DBUS_SESSION_BUS_ADDRESS で既存のユーザー D-Bus バス参照を消す
  • dbus-launch --exit-with-session でRDPセッション専用の D-Bus を新しく作る
  • startlxqt をその独立したセッション上で起動する

これでローカルログイン中の LXQt セッションとは別物として扱われるため、lxqt-session の競合が起きなくなる。

修正後は次回接続からそのまま反映された。少なくとも手元の環境では systemctl restart xrdp は不要だった。

Lubuntuをヘッドレスで使うなら遭遇しないかもしれないが、ローカル画面とRDPを併用するならかなり踏みやすい罠だと思う。

Ubuntuで最新版curlを使いたいときはDockerが便利

環境

  • Ubuntu 22.04

やりたいこと

Ubuntu上で curl --json を使おうとしたら、手元の環境では普通に怒られた。

$ curl https://httpbin.org/post --json '{"name": "コナン", "age": 8}'
curl: option --json: is unknown
curl: try 'curl --help' or 'curl --manual' for more information

--json は便利で、JSONを投げるだけなら Content-TypeAccept を毎回手で付けなくてよい。ところがこのオプションは curl 7.82.0 で追加された機能なので、それより古い環境では使えない。

curl.se curl.se

手元のサーバーでバージョンを確認するとこうだった。

$ curl -V
curl 7.81.0 (x86_64-pc-linux-gnu)

惜しい。0.1足りない。

Ubuntuではディストリビューション側のパッケージに乗るバージョンに引っ張られるので、「ちょっと新しい機能を使いたい」みたいなケースだと地味に詰まる。今回はまさにそれだった。

対策

apt で入る curl では足りなかったが、このためだけにソースからビルドするのも面倒である。そこで公式のコンテナイメージ curlimages/curl を使うことにした。

$ docker run --rm curlimages/curl:latest https://httpbin.org/post --json '{"name":"コナン","age":8}'

これならホスト側の curl を汚さずに、新しい curl だけをその場で使える。Dockerさえ入っていればよいので、検証用サーバーや一時的な作業でも雑に扱えて楽である。

手元のファイルを入力にしたい場合は、普通のコンテナと同じくボリュームマウントすればよい。

$ docker run --rm -v "./:/app" -w /work curlimages/curl:latest https://httpbin.org/post --json @payload.json

「curl本体の更新はしたくないが、新しめのオプションだけ使いたい」という用途にはかなりちょうどよい。

curlimages/curlって安全なの?

Dockerイメージは基本的に野良ビルドを避けたい。特に curl みたいなネットワーク境界に立つツールは、どこの誰が作ったものか分からないイメージを気軽に使う気にはなれない。

その点 curlimages/curl は curl プロジェクトが公開している公式イメージで、配布用のリポジトリも curl/curl-container として公開されている。

github.com

公式リポジトリの説明を見ると、安定版の配布先として docker.io/curlimages/curlquay.io/curl/curl があり、ghcr.io/curl/curl-container/curl:master は master ブランチ追従の開発用イメージという扱いである。 つまり全部が同じ用途というわけではない。

自分の用途が「新しい curl を安定して使いたい」であれば、素直に docker.io/curlimages/curl:latestquay.io/curl/curl:latest を使うのがよい。 少なくとも、よく分からない個人ビルドを引くよりはだいぶ安心して使える。

Claude Codeで.agents/skillを使うためにシンボリックリンクを張る

環境

  • Claude Code 2.1.140
  • codex 0.130.0

やりたいこと

codex 向けに .agents/skills 以下で管理している skill を、Claude Code からもそのまま使いたい。 ただしClaude Codeは .agents を直接見てくれず、.claude/skills 以下に置いた skill しか認識しないようだった。

同じ内容を二重管理したくないので、.claude/skills 側にはシンボリックリンクだけ作って済ませたい。

手順

やりたいことだけ見ると、次のコマンドでいけそうに見える。

ln -s .agents/skills/drizzle-sqlite-schema .claude/skills/drizzle-sqlite-schema

しかしこれは期待通りに動かない。正しくはこうである。

ln -s ../../.agents/skills/drizzle-sqlite-schema .claude/skills/drizzle-sqlite-schema

なぜ俺のシンボリックリンクは動かないのか

理由は、ln -s の第1引数に渡した相対パスが「コマンドを実行したカレントディレクトリ基準」ではなく、「作られるシンボリックリンク自身が置かれる場所基準」で解決されるからだ。

今回作りたいリンクは .claude/skills/drizzle-sqlite-schema なので、リンクの実体は .claude/skills ディレクトリの中に置かれる。そのため、リンク先も .claude/skills から見た相対パスで書かないといけない。

ディレクトリ構造で書くとこうなる。

project-root/
├── .agents/skills/drizzle-sqlite-schema
└── .claude/skills/

.claude/skills から .agents/skills/drizzle-sqlite-schema へ行くには、いったん ../../ でプロジェクトルートまで戻る必要がある。 だからリンク先は ../../.agents/skills/drizzle-sqlite-schema になる。

逆に最初のように .agents/skills/drizzle-sqlite-schema とだけ書くと、.claude/skills から見て .claude/skills/.agents/skills/drizzle-sqlite-schema を探しにいくことになる。

そんなパスは存在しないので、壊れたリンクになるわけである。

この仕様を知らないと「プロジェクトルートで実行したのになぜ .agents/... でダメなのか」で毎回ハマる。ln -s は実行場所よりも、リンクファイルが最終的にどこへ置かれるかを意識したほうが理解しやすい。

もちろん絶対パスで書いても動く。

ln -s /absolute/path/to/project/.agents/skills/drizzle-sqlite-schema .claude/skills/drizzle-sqlite-schema

ただ、リポジトリを別の場所に clone したときに壊れやすいので、こういう用途なら相対パスで張っておくほうが扱いやすい。

まとめ

  • ln -s <リンク先> <リンク名><リンク先> が相対パスなら、基準は「リンク名が置かれるディレクトリ
  • 迷ったら「リンクが置かれるディレクトリに cd したつもり」で相対パスを書く

自分はファイルのシンボリックリンクではあまり意識していなかったが、ディレクトリ同士をつなぐとこの仕様がかなり露骨に効いてくる。Claude Code用の skill を既存管理ディレクトリから流用したいときも、このルールを知っていれば素直に解決できる。

irodori-ttsをOpenAI Text-to-Speech互換APIで使う

環境

  • irodori-tts-500m-v3
  • Python

やりたいこと

Irodori-TTSはAratako氏が開発した日本語特化のローカル音声合成AIモデルである。

  • 日本語に最強クラスの対応力: やっぱり日本人が作ったTTSしか勝たん
  • 絵文字で感情をコントロールできる: テキスト中に絵文字を挿入するだけで声のトーンや感情表現が変わる
  • 参考音声クローニング機能: 好みの参考音声を与えれば、その声質に近い音声を生成できる。たった10秒程度の音声データでも機能する

で、自分はOpen-LLM-VTuberを使って嫁を召喚したかった。

github.com

Open-LLM-VTuberにはOpenAI互換のTTS設定項目がある。ここにIrodori-TTSの互換APIを指定すれば、Open-LLM-VTuberのバックエンドとしてIrodori-TTSが使えるはずだ。

openai_tts: # Configuration for OpenAI-compatible TTS endpoints
  # These settings override the defaults in the openai_tts.py file if provided
  model: 'kokoro' # Model name expected by the server (e.g., 'tts-1', 'kokoro')
  voice: 'af_sky+af_bella' # Voice name(s) expected by the server (e.g., 'alloy', 'af_sky+af_bella')
  api_key: 'not-needed' # API key if required by the server
  base_url: 'http://localhost:8880/v1' # Base URL of the TTS server
  file_extension: 'mp3' # Audio file format ('mp3' or 'wav')

手順

探してみると有志の方がすでに互換APIを作ってくださっていたので、ありがたく使わせてもらう。

github.com

Dockerで起動したら、まずリファレンス音声を登録する。リファレンス音声は10秒程度でも十分だが長いほうが品質が良くなる傾向はあった。最大30秒なので注意。

curl -XPOST http://localhost:8880/v1/audio/voice_contents -F voice_id=myvoice  -F "file=@ref.wav" 

続いて音声を生成する。

curl http://localhost:8880/v1/audio/speech \
  -H "Content-Type: application/json" \
  -d '{
    "model": "irodori-tts-500m-v3",
    "input": "おはようございます。",
    "voice": "myvoice"
  }' \
  --output output.mp3

設定変更

.envでIrodori-TTSのデフォルトパラメーターを変更できる。自分の場合は多少時間かかっても品質向上したかったのでNUM_STEPSを増やした。

# Diffusionサンプリングのステップ数(デフォルト: 40、範囲: 1〜120)
NUM_STEPS=60
CFG_SCALE_TEXT=4

V3を使う場合

V2とV3ではAPIに渡すパラメーターが異なり、元のリポジトリのままではV3が動作しない。自分でフォークしてV3対応させたので公開しておく。

github.com

パナソニックの食洗機壊れたので修理してもらった話 Episode1

パナソニックの食洗機壊れた

2年近く使っているパナソニックの食洗機「NP-TSP1」が、ある日いきなり壊れた。 毎日使っているので、これが止まると生活へのダメージがかなり大きい。手洗いに戻るのはもう無理である。

使っているのは「NP-TSP1」。分岐水栓を付けられない賃貸でも、自分で給水すれば使えるタイプの食洗機である。

panasonic.jp

症状

通常は「洗い」から「乾燥」までそのまま進んで終了するのだが、問題が起きると洗い工程のあと、排水タイミングで止まる。 その際、けたたましい警告音とともに「U11」エラーが表示され、そこで運転終了になる。

とりあえず思いつくことは一通り試した。庫内の掃除、クリーン洗浄、コンセントの抜き差しあたりである。しかし何をやっても改善しなかった。

奇妙なのはU11エラーが出たあとに一度電源を落とし、再度電源を入れて「乾燥のみ」でスタートすると、何事もなかったかのように排水できて、そのまま乾燥まで正常終了する。

つまり、排水経路が完全に詰まっているわけではなさそうである。単純なゴミ詰まりというより、センサーか関連部品の故障ではないかと考えた。

U11表示時の様子は以下。液晶には U11 が交互に表示される。

picture 0

picture 1

修理依頼

この食洗機は買い切りではなく、パナソニック公式の定額利用サービスで使っている。月額2,570円。 ちゃんと確認できていなかったのだが、故意ではない故障であれば無償修理の対象だった。

定額利用中に加えて、商品進呈後の1年間も保証期間らしい。はえー知らなかった。

picture 2

修理の受付先は以下

パナソニック 定額利用サービス カスタマーセンター
TEL:0120-878-131
受付時間:10:00~18:00(土日祝日・年末年始を除く)

10時ちょうどくらいに電話したところ、すぐにオペレーターにつながった。U11エラー自体はそこまで珍しくないのか、対応はかなり手慣れた印象だった。

10分ほど症状の説明と質疑応答をしたところで、訪問修理の手配が確定した。 日程調整は別部署から改めて連絡する方式で、その時点では「1週間後なのか1か月後なのかも分からない」とのことだった。ただ、土日対応が可能なのは助かる。

その後、別部署から電話がかかってきて訪問日を調整した。修理依頼を出したのはGW直前だったが、祝日でも対応可能とのことで、このあたりはかなり柔軟だった。 一方で、訪問時間の細かい指定はできず、何時ごろ来るかは当日の朝に修理担当者から電話があって初めて分かる仕組みだった。

少し雑だと感じたのは情報連携で、2回目の電話では最初に説明した内容がほとんど引き継がれておらず、「U11エラーが出る」程度しか共有されていなかった。せっかく最初に細かく説明したのだから、そこはもう少しちゃんとしてほしい。

修理当日

修理時間は1時間ほどだった。技術担当者にもあらためて症状を説明したが、どうも過去にあまり見ないケースらしく、「この部品の故障が原因です」と即断できる状況ではなかったようだ。

とはいえ、事前に伝えていた情報からある程度アタリは付けていたようで、持参した部品を順番に交換して対応してくれた。

修理した分は以下

  • 水位スイッチ
  • 排水ポンプ
  • フローメータ
    • 給水が適当に(ある一定の勢いで)行われているかをチェックするセンサーらしい。
    • ここの故障がダントツらしい
    • ここが故障すると給水が満タンになっても音が鳴らず、となってしまう

当然無償だった。

結果

修理後は今のところ正常に動作している。2戦2勝。 また壊れる可能性はゼロではないが、保証期間中は無償で修理対応してもらえるので、そのまま使い続けるつもりだ。

頑張ってくれ、食洗機くん。

考察

食洗機はドラム式洗濯機に次いで壊れやすい家電と聞く。[要出典] 買い切りのほうが理論値は最安だがメンテナンス費用のことを考えるとレンタルの方がありなんじゃないかと思った。 特に今回のような微妙な故障だと、「まだ使えなくはないが正常でもない」みたいな状態になりがちで、こういうケースほど無償修理が通しやすいかどうかが効いてくる。

もっとも、利用規約の免責事項には

本商品の不具合等に起因して生じた本商品その他の財物の使用の阻害

とある。つまり、故意ではない故障でしばらく使えなくなったとしても、その不便さ自体まで補償してくれるわけではないと読める。

少なくとも今回に関しては、修理の要否で変に揉めることなく、そのまま訪問修理まで進んだ。この安心感込みで見るなら、食洗機みたいな毎日使う壊れやすい家電はレンタルも十分ありだと思う。

参考リンク