Apache Guacamole の Web 画面から Windows に RDP でつなぐと、半角/全角キーを押してもリモートの IME が切り替わらない、iPad から日本語を打っても届かない、といった不具合に当たります。これらは Guacamole 本体の不具合で、本体で直るまでには年単位の時間がかかっています。
本体に手を入れずに回避する拡張機能(JS のみ)を作って公開したので、症状、原因、使い方をまとめます。
- リポジトリ: https://github.com/millibird/guacamole-ja-input-workaround
- 対象: Guacamole 1.6.0
- ライセンス: Apache License 2.0
この記事は 2026 年 9 月時点、Guacamole 1.6.0 での内容です。拡張機能は Guacamole の内部の作りに依存しているので、版が変わると動かなくなる可能性があります。最新の状態はリポジトリの README を見てください。
また、この拡張機能は現状のまま(AS IS)で提供し、サポートはしません。
回避する症状
回避する症状は次の 4 つです。画面の例は後の症状の例に、原因と拡張機能の対処は原因と対処にまとめています。
| # | 症状 | 起きる場面 |
|---|---|---|
| 1 | 半角/全角キーを押しても、リモートの IME が切り替わらない。代わりに手元の IME が切り替わる | RDP、Windows の Chrome |
| 2 | (1 を回避すると)ときどき IME が高速に切り替わり続ける。その後は 2 回押さないと切り替わらない | RDP、Windows の Chrome |
| 3 | テキストインプットで、手元の IME で確定した日本語が送られない(英字は届く) | すべてのプロトコル、Windows の Chrome と iPad の Safari |
| 4 | (3 を回避すると)RDP で「ああああ」「神神」のような同じ文字の連続がまちまちに欠ける | RDP |
4 件のうち、Apache の Jira に登録されているのは 1 の半角/全角キーの件(GUACAMOLE-1503)だけです。残りの 3 件は拡張機能を作る中で見つけたもので、本体に報告されているかは確かめていません。
テキストインプットとは
Guacamole のメニューの「インプットメソッド」で選べる入力方式です。この方式でiPad等のスマートデバイスからソフトキーボードを使用して文字入力をすることができます。メニューは、キーボードのある端末では Ctrl+Alt+Shift で、iPad などのタッチ端末では画面の左端から右へスワイプすると開きます。
症状の例
症状 1
半角/全角キーを押した後、リモートのタスクバーの IME 表示は「A」のままで、手元 PC の IME 表示が「あ」に切り替わっています。
症状 2
IME が切り替わり続ける動き。静止画は伝わりづらいため省略。半角/全角キー押しっぱなしの症状です。
症状 3
テキストインプットで日本語を確定すると、欄に確定した「テスト」が残ったままで、リモートのメモ帳には何も届いていません。
症状 4
テキストインプットで「ああああ神神」の 6 文字を送って、届いたのは「あ神」の 2 文字だけです。
確認した環境
Guacamole 1.6.0(公式 Docker イメージ guacamole/guacamole と guacamole/guacd)で、接続先は Windows 11(RDP)と Ubuntu(SSH)です。
| 手元の環境 | 回避を確認した症状 |
|---|---|
| Windows 11、Google Chrome、日本語キーボード、Microsoft IME | 1〜4 すべて(RDP・SSH)。SSH の通常モードで手元の IME が使えることも確認 |
| iPad、Safari、ソフトキーボード | 3、4(RDP・SSH) |
Edge、Firefox、macOS、Android、iPad の通常モードなどは確かめていません。キーイベントの届き方はブラウザと OS で違うので、動かない、あるいは逆効果になる可能性があります。
使い方
-
Releases から、使っている Guacamole の版に合う jar を取得します(例:
guacamole-ja-input-workaround-1.0.0-for-1.6.0.jar)。python3 build.pyでdist/に作ることもできます(標準ライブラリだけで動きます) -
GUACAMOLE_HOME/extensions/に置きます - Guacamole(Tomcat)を再起動します
- ブラウザでページを読み込み直します(Windows なら Ctrl+Shift+R)。開いたままのタブは古い JS で動き続けます
起動ログに次の行が出れば読み込まれています。
Extension "Japanese input workarounds 1.0.0" (ja-input-workaround) loaded.
公式 Docker イメージなら、GUACAMOLE_HOME(既定は /etc/guacamole)を読み取り専用でマウントするだけです。
services:
guacamole:
image: guacamole/guacamole:1.6.0
volumes:
- ./guacamole-home:/etc/guacamole:ro # ./guacamole-home/extensions/ に jar を置く
外すときは jar を消して Guacamole を再起動し、ブラウザでページを読み込み直します。
適用範囲と制限
- 半角/全角キーの回避(1、2)は RDP の接続だけに適用します。 SSH などではリモートに IME が無く、手元の IME だけが日本語を入力する手段なので、適用しません。
- RDP の通常モード(インプットメソッド「なし」)では、手元の IME は使えなくなります。 日本語はリモートの Windows の IME で入力してください。手元の IME で入力したいときはテキストインプットに切り替えます。iPad など物理キーボードの無い端末もテキストインプットを使ってください。
- テキストインプットの回避(3、4)はすべてのプロトコルに適用します。 1 文字ずつ 50 ミリ秒の間隔を空けて送るので、長い文を一度に確定すると 20 文字で約 1 秒かかります。
原因と対処
以下は、拡張機能の 160 行ほどの JS(src/ja-input-workaround.js)で何をしているかの説明です。
1. 半角/全角キーが手元の IME に握りつぶされる
Guacamole はキー入力を受け取るために、見えない <textarea>(Guacamole.InputSink)にフォーカスを置き続けます。編集できる入力欄にフォーカスがあると、ブラウザは手元の IME を有効にします。すると半角/全角キーは手元の IME が消費してしまい、Guacamole まで届きません。手元の IME が切り替わるのはこのためです。
対処は、この <textarea> を 読み取り専用にすることです。読み取り専用の入力欄では手元の IME が働かないので、半角/全角キーがそのままキーイベントとして Guacamole に届きます。
InputSink には id も class も無いので、生成時のインラインスタイル(position: fixed、幅と高さが 0)で見分けています。メニューのクリップボード欄など、他の <textarea> は編集できるままにする必要があります。
function isInputSink(el) {
return el.tagName === 'TEXTAREA'
&& el.style.position === 'fixed'
&& parseFloat(el.style.width) === 0
&& parseFloat(el.style.height) === 0;
}
RDP かどうかは、URL(#/client/<id>)の id と、AngularJS の guacClientManager サービスが持つ接続の protocol で判定します。接続直後は protocol がまだ null のことがあるので、その間は RDP でない扱い(=素の Guacamole の動き)にし、hashchange、focusin、1 秒ごとのポーリングで追いかけています。
2. keyup が「次に押したとき」まで届かない
1 を回避すると、今度は IME が高速に切り替わり続けることがあります。
Windows の Chrome では、半角/全角キーを離しても、その時点では keyup が届きません。次にキーを押したときにまとめて届きます。さらに、押すたびにキーの名前が Zenkaku(keyCode 244)と Hankaku(243)で入れ替わります。
Guacamole は keyup が来ないので「キーが押されたまま」と判断し、押しっぱなしのときに自動で送る連続入力(500 ミリ秒後から 50 ミリ秒ごと)を送り続けます。これが高速切り替えの正体です。2 回押さないと切り替わらなくなるのは、名前が入れ替わることで「押されたまま」の扱いが残るためと推定しています。
対処は、本物のイベントを Guacamole に見せず、1 回押すごとに同じキーの keydown と keyup を 1 組だけ合成して送ることです。Guacamole は document でイベントを聞いているので、その手前の window のキャプチャ段階で止めます。
window.addEventListener(type, function (e) {
if (!e.isTrusted || ZK.indexOf(e.key) === -1) return; // 合成イベントは通す
if (!rdpActive()) return; // RDP 以外は手元の IME に任せる
e.stopImmediatePropagation();
e.preventDefault();
if (type === 'keydown') {
fire(e.target, 'keydown');
fire(e.target, 'keyup');
}
}, true);
3. 確定した日本語が送られない
テキストインプットの入力欄(guacTextInput、class target の <textarea>)は、変換中の入力を送らないように、compositionstart から compositionend の間は input イベントを無視します。
ところが Chrome では、確定した文字を含む最後の input イベントが compositionend より先に発火します。つまり確定した文字は「変換中」の扱いで届き、無視されます。Guacamole には確定後に送り直す処理が無いので、確定した文字は入力欄に残ったまま送られません。英字は変換を経ないので届きます。
対処は、compositionend の直後に、入力欄に残っている確定文字を input イベントで送らせることです。Guacamole 自身の compositionend ハンドラが「変換中」フラグを下ろした後に動くよう、setTimeout(…, 0) で一拍置いています。
ここで、入力欄の中身の作りを押さえておく必要があります。テキストインプットの入力欄は、空に見えるときでも、実際には幅ゼロの空白(U+200B)が左右 4 文字ずつ、合わせて 8 文字入っています(resetTextInputTarget)。カーソルはその真ん中にあります。
□□□□|□□□□ □ = U+200B(幅ゼロの空白、画面には見えない) | = カーソル
これは Backspace と Delete を検知するための仕掛けです。何も無い欄では Backspace を押しても何も起きず、押されたことが分かりません。見えない文字を置いておけば、Backspace で左の □ が減り、Delete で右の □ が減るので、Guacamole は input イベントのたびに「文字が減ったなら Backspace か Delete、増えたなら入力」と判断できます。増えた場合は欄の中身を丸ごと送り(U+200B は読み飛ばします)、欄を上の初期状態に戻します。
英字を 1 文字打ったときの通常の流れは次の通りです。
□□□□a□□□□ → input → Guacamole が「a」を送り、欄を □□□□□□□□ に戻す
日本語の場合、上で説明した通り、確定文字を含む input は「変換中」として無視されます。そのため確定後の欄は、確定文字が □ の間に残ったまま、何も送られていない状態になります。
□□□□あいう□□□□ ← 確定直後の欄。Guacamole は何も送っていない
拡張機能は compositionend の後にこの欄を見て、□ を除いた「あいう」を取り出します。次に欄を初期状態に戻し、通常の英字入力と同じ形になるよう 1 文字ずつ □ の間に置いて input イベントを起こします。Guacamole から見ると英字を 1 文字打ったときと区別が付かないので、通常の経路で送られます。(1 文字ずつに分けている理由は次の節で説明します。)
□□□□あ□□□□ → input → Guacamole が「あ」を送り、欄を □□□□□□□□ に戻す
□□□□い□□□□ → input → 「い」を送る
□□□□う□□□□ → input → 「う」を送る
// setTimeout: guacTextInput 自身の compositionend ハンドラが
// composingText = false にした後に動かす
document.addEventListener('compositionend', function (e) {
var t = e.target;
if (!(t.tagName === 'TEXTAREA' && t.classList.contains('target'))) return;
window.setTimeout(function () {
var v = t.value, pad = v.charAt(0); // pad = □(U+200B)
var text = v.split(pad).join(''); // □ を除いた確定文字
if (!text) return;
var half = (v.length - text.length) / 2; // 片側の □ の数(4)
var chars = Array.from(text);
var i = 0;
t.value = pad.repeat(2 * half); // 欄を初期状態 □□□□□□□□ に戻す
// ... 1 文字ずつ送る(次の節)
}, 0);
}, true);
拡張機能を入れた状態で症状 3 と同じ操作をすると、「テスト」がリモートに届き、欄は空になります。
4. 同じ文字の連続が欠ける
3 を回避すると、RDP で「ああああ」が「あ」や「ああ」になる、といった欠けがまちまちに起きました(症状 4 の画像)。確定した文字をまとめて一気に送ると起きる症状なので、テキストインプット欄に日本語を貼り付けたときも同じことが起きます。
これは推定ですが、guacd はキーボード配列に無い文字(日本語など)を RDP の Unicode キーボードイベントで送るとき、「押した」だけを送って「離した」を送りません(guacamole-server の src/protocols/rdp/keyboard.c。送信処理では flags を 0 にして UnicodeKeyboardEvent を呼んでいます)。同じ文字が短い間隔で続くと、リモートの Windows が 2 回目以降を押しっぱなしの繰り返しと見なして捨てているのだと考えています。
対処は、1 文字ずつ 50 ミリ秒の間隔を空けて送ることです。間隔を空けると欠けなくなりました。この間隔は SSH にも同じように掛かります。
前の節のハンドラの続きで、確定文字を 1 文字ずつ □ の間に置いて input を発火させ、次の文字は SEND_INTERVAL 後に送ります。
var SEND_INTERVAL = 50; // ms
// ... compositionend ハンドラの続き
(function step() {
if (i >= chars.length) return;
t.value = pad.repeat(half) + chars[i++] + pad.repeat(half);
t.dispatchEvent(new Event('input')); // guacTextInput に 1 文字送らせる
window.setTimeout(step, SEND_INTERVAL);
})();
拡張機能を入れた状態で「ああああ神神」を確定すると、6 文字すべて届きます。
なお、拡張機能が 1 文字ずつ送るのは compositionend の後の送り直しだけです。貼り付けは対象外なので、貼り付けた日本語は拡張機能を入れても一気に送られ、欠ける可能性があります。
なぜ本体の修正ではなく拡張機能にしたか
Guacamole の日本語入力の不具合は、本体で直るまでに時間がかかっています。
| 不具合 | 経過 |
|---|---|
| GUACAMOLE-520(RDP の日本語キーボード配列が不正確) | 2018-03 に登録、2024-06 に解決、2025-06 の 1.6.0 でリリース。登録から使えるまで 7 年あまり |
| GUACAMOLE-1503(半角/全角キーで日本語入力に切り替えられない) | 2022-01 に登録。修正すべきファイルまで挙がっているが未解決 |
本体で直るのを待つ以外の手として、手元に配備した Guacamole の JS(war の中の guacamole.<ハッシュ>.js)を直接パッチする方法があります。ただし、これには次の難点があります。
- Guacamole を上げるたびに消えます。 新しい war に毎回パッチを当て直すことになります。
- 公式 Docker イメージに適用しづらいです。 JS はイメージ内の war に入っているので、パッチを当てるには自前でイメージを作り直すか、起動時に war を展開して書き換える仕掛けが必要です。
- 本体を書き換えたことが外から分かりにくいです。 不具合が出たとき、素の Guacamole の問題かパッチの問題かの切り分けが難しくなります。
一方、Guacamole には拡張機能の仕組みがあり、guac-manifest.json で JS を指定すると、すべてのページにその JS が読み込まれます。この仕組みを使えば、本体に手を入れずに回避できます。jar を GUACAMOLE_HOME/extensions/ に置くだけなので、公式 Docker イメージでも読み取り専用のボリュームマウントで済み、Guacamole を上げても jar は残ります(動作の確認は必要です)。外すのも jar を消すだけです。
拡張機能は zip にマニフェストと JS を入れただけのものです。
{
"guacamoleVersion" : "1.6.0",
"name" : "Japanese input workarounds",
"namespace" : "ja-input-workaround",
"js" : [ "ja-input-workaround.js" ]
}
ただし、この拡張機能は Guacamole の内部の作りに依存しています。InputSink の見分け方(インラインスタイル)、テキストインプット入力欄の class 名、AngularJS のサービス名(guacClientManager)などです。Guacamole を上げると、これらが変わって動かなくなる可能性があります。そのため guacamoleVersion は 1.6.0 に固定してあります。
なお、拡張機能の JS はログイン画面を含むすべてのページで読み込まれます。つまりこの拡張機能は、ログイン時のパスワードを含むキー入力を横取りできる位置で動きます。導入する管理者から見れば、他人が書いたそのようなコードをサーバーに置くことになるので、入力を別の場所へ送ったり外部と通信したりしていないことを、自分でコードを読んで確かめたいはずです。そのために、依存ライブラリを使わず、ひと通り読み切れる 160 行ほどに収めています。
この拡張機能では直せないもの
SSH で、全角文字がところどころ欠けて見える症状は直せません。guacd の表示の不具合(GUACAMOLE-2091)です。
SSH の画面はブラウザではなく guacd が描いて送っています。guacd はカーソルが動くたびにカーソルがあったマスを描き直しますが、全角文字は 2 マスを使うのに 1 マスしか描き直しません。そのため、カーソルが通った位置の全角文字が欠けます。入力のたびにカーソルが動くので、どの文字が欠けるかはタイミング次第になります。
画面が欠けるだけで、入力は正しく届いています(od -An -tx1 で受け取ったバイト列を見て確認しました)。
下の画像では、打っている途中の入力行には全角文字が 1 文字も表示されず、実行後のエラーメッセージでは先頭の「日」が欠けて見えます。一方、history には全文が残っており、入力自体は届いていることが分かります。
描いているのは guacd なので、ブラウザ側で動く拡張機能では直せません。フォントの設定や送る間隔を変えても直りませんでした。
修正(apache/guacamole-server#602)は 1.6.1 に入れる前提で patch ブランチ宛てになり、メンテナ 1 人が承認していますが、2026 年 9 月時点では取り込まれていません。
まとめ
- Guacamole 1.6.0 の日本語入力の不具合 4 件を、本体に手を入れない拡張機能(JS のみ)で回避しました
- 原因は、見えない入力欄で手元の IME が動くこと、Chrome の半角/全角キーの keyup の届き方、
inputとcompositionendの順序、guacd の Unicode 入力の送り方(推定)です - 本体で直ったら外す前提の回避策です
- SSH の全角文字の表示欠けは guacd 側の不具合で、拡張機能では直せません
同じ症状で困っている方の参考になれば幸いです。動作しない環境があれば、リポジトリの Issue で報告してもらえると助かります(対応は約束できません)。





