CH-J ToolsSSH によるサーバー管理
メニュー

リモートエディターの保存とレイテンシーの監視

ファイルをダウンロード

この翻訳は現在、言語面での校閲中です。不明な点がある場合は、英語の原文と照らし合わせてください。

根本的な原因

以前のエディターは、作業用ファイルを対象ファイルの親ディレクトリにアップロードしていましたが、/etc や /var の配下では、SSH アカウントがそのディレクトリに書き込めないことがよくありました。SFTP の名前変更には、認可された sudo の経路も、ベースラインとの競合チェックも、セキュリティメタデータの検証も、確実な上書きの仕組みもありませんでした。また、エラー処理で一時コピーを削除していたため、完了したかどうかが不明な場合でも、復旧に使える材料が失われていました。

以前のセッションの状態表示は、接続状態だけでした。ターミナルのインジケーターに反映される ICMP や認証済み SSH の応答時間の計測はありませんでした。

保存のプロトコル

  1. 固定の SSH ヘルパーが、ディスクリプター基準の相対操作と、パスの各要素に対する O_NOFOLLOW を使って、リンクが 1 つだけの通常ファイルを開きます。内容、SHA-256 ハッシュ、デバイス/inode、UID、GID、すべてのパーミッションビット、ナノ秒単位の変更/状態変更時刻、読み取り可能なすべての拡張属性を記録します。Core はハッシュを検証し、BOM を保持したまま厳密な UTF-8 としてデコードします。各ドキュメントには、そのプラグイン、ターミナルセッション、サーバーの識別情報に限定された、不透明な editId が割り当てられます。
  2. 特権のない準備処理が、pwd.getpwuid(os.getuid()) によって認証済みアカウントのホームディレクトリを特定し、モード 0700 で $HOME/ch-j-sm/ を作成して、非公開かつ排他的な復旧用ジャーナルを作成します。安全でないホームディレクトリ、シンボリックリンクの作業領域、ほかのアカウントが所有する作業領域は拒否されます。
  3. Core は、排他的な作成(wx)、モード 0600、60 秒のチャネル/アップロードのタイムアウトを指定して、隠しファイルとなるランダムな 128 ビット名の .tmp 作業用ファイルを SFTP でアップロードします。ジャーナルには、対象、元のベースライン、想定される作業用ファイルのハッシュが含まれますが、認証情報は含まれません。
  4. 1 回の SSH exec で最終処理全体を実行します。必要に応じて、既存の sudo 実行機構を介して実行されます。ヘルパーは非公開の作業用コピーを安全に開いてハッシュを計算し、ディスクに書き出します。元の inode をロックしてベースラインを確認し、対象の隣に排他的な保存先ファイルを作成して内容をすべて書き込み、メタデータを適用・検証したうえで、元のファイルをもう一度確認し、置き換え用のファイルをディスクに書き出します。
  5. 同じディレクトリ内での os.replace によって、対象をアトミックに置き換えます。親ディレクトリをディスクに書き出し、最終的なパス、ハッシュ、inode、メタデータを検証します。置き換えが完了するまで、元のパスは利用可能なままです。作業用ファイルがファイルシステムをまたいで移動されることはなく、最終処理で削除されることもありません。
  6. Core が検証済みの成功を受け取った後、別のクリーンアップ処理が最終的な内容とセキュリティメタデータを再確認してから、作業用ファイルとジャーナルを削除します。クリーンアップに失敗した場合も、recoveryAvailable: true 付きで保存の確定が報告されます。

アップロードの失敗、sudo の失敗、競合、メタデータのエラー、置き換えの失敗が起きた場合は、元のファイルと、準備済みのすべての材料が保持されます。未完了のアップロードはそのように識別され、完全な編集済みドキュメントとして復旧することはできません。SSH 通信の途絶、終了ステータスの欠落、タイムアウト、想定外の最終処理結果は、保存済みではなく 不明 として報告されます。結果が不確かなドキュメントについては、Core が次の保存をブロックします。置き換えが自動で再試行されることはありません。

メタデータ、権限、安全上の境界

ヘルパーは、UID、GID、および実行、setuid、setgid、sticky の各ビットを含むすべての POSIX パーミッションビットを保持し、検証します。POSIX ACL、capabilities、SELinux ラベルを含む Linux の拡張属性を、1 バイトずつそのまま再現します。書き込みや chown によって capabilities や特殊なパーミッションビットがクリアされることがあるため、所有者とファイルの内容をセキュリティ属性より先に設定します。元のファイルにはないのに保存先ファイルが継承した属性は削除されます。変更時刻は新しい書き込みを反映するもので、元のファイルからはコピーされません。

読み取れない属性や表現できない属性があると、処理は中止されます。メタデータが知らないうちに失われることはありません。IMA/EVM 署名は明示的に拒否されます。内容/inode に結び付いた完全性の署名をコピーすると、署名が無効になってしまうためです。対象パスやホームディレクトリのパスにあるシンボリックリンク、ハードリンクされたファイル、通常ファイル以外のファイルは拒否されます。その場での上書きや、より弱い SFTP での上書きへのフォールバックは使用しません。

書き込み可能な通常の保存は、SSH アカウントとして実行されます。保護された保存には、明示的な { sudo: true } が必要です。パスワードなしの sudo の場合、sudo パスワードは任意です。パスワードは検証されたうえでチャネルの stdin でのみ渡され、コマンド、ジャーナル、作業用ファイル、ロガー、リモートのエラーに含まれることはありません。root の SSH セッションでは、既存の root 直接実行機構を使用します。認可に失敗した場合、元のファイルと作業用コピーはそのまま残ります。root として実行されるすべての保存(root での直接 SSH または sudo)では、さらに、ほかのアカウントが所有または書き込み可能な、あるいはアクセス ACL が設定された対象ディレクトリ/上位ディレクトリを拒否し、ほかのアカウントが制御する特権的なパス名を信頼しません。

固定のヘルパーは python3 -I -B で実行されます。ユーザーの Python パス、作業ディレクトリ、ユーザーの site-packages を無視し、バイトコードを書き出しません。パスと JSON の引数は、Core 内でシェル用にクォートされます。プラグインが任意の SSH exec や SFTP のハンドルを受け取ることはありません。既存のサンドボックス、CSP、マニフェストの権限、ホスト鍵の検証、アップデートの検証は引き続き有効です。

Plugin API と残っている Monaco との統合

別途配布される Monaco/File Manager の UI は、このチェックアウトには含まれていません。その UI との統合、sudo のプロンプト、未保存のバッファーの扱い、翻訳は、ここでは実装もテストもできませんでした。同梱のプラグインのソースは Hash & Checksum プラグインです。代わりとなる File Manager のソースを作り出すことはしていません。

既存のメソッドは引き続き利用できます。追加のメソッド/オプションは、サンドボックス化された preload と、権限をチェックするランタイムによって公開されます:

const opened = await chjPlugin.files.readText(sessionId, path);
// For a protected read, explicitly pass { sudo: true, sudoPassword }.

const result = await chjPlugin.files.saveText(sessionId, path, editor.getValue(), {
  editId: opened.editId,
  sudo: true,           // Include only after the user authorizes elevation.
  sudoPassword         // Omit for passwordless sudo; never persist this value.
});

if (result.ok && result.value.status === "saved") {
  // Mark this exact submitted editor version as saved. Later edits stay dirty.
} else {
  // Keep the Monaco model dirty. Translate result.error.code/status.
  // result.error.recoveryId/recoveryPath identify retained working material.
}

await chjPlugin.files.closeText(opened.editId);
const recovery = await chjPlugin.files.listRecovery(sessionId);
const copy = await chjPlugin.files.readRecovery(sessionId, recoveryId);
// Review copy.status and copy.text; open a fresh document before another save.
await chjPlugin.files.cleanupRecovery(sessionId, recoveryId);
// Cleanup rejects anything whose target is not currently confirmed.

対象の確認に権限の昇格が必要な場合、復旧用のメソッドは最後の引数として任意の { sudo: true, sudoPassword } を受け付けます。sudo を介して実行した場合でも、認証済みユーザーの作業領域が保持されます。ジャーナルはアプリの再起動や SSH の再接続後も残ります。復旧によって最終処理が自動で繰り返されることはありません。復旧したバッファーを明示的に保存する前に、開き直した対象と比較して確認してください。

files.read は読み取り、確認、ハンドルの解放を保護します。files.write は保存と復旧のクリーンアップを保護します。既存のプラグイン向けに、writeText は失敗時に引き続き reject します。一方 saveText は、Electron が reject された Error オブジェクトのカスタムプロパティを削除してしまうため、構造化された { ok, value/error } のエンベロープを返します。従来の 3 引数の writeText は、開いているドキュメントが 1 つだけの場合、そのベースラインを使用できます。同じパスで複数のドキュメントが開いている場合は editId が必要です。あいまいな従来の呼び出しは、別のドキュメントのベースラインを選ぶことなく、安全に失敗します。プラグインは使わなくなったハンドルを解放してください。

外部の UI では、保存中であることをすぐに表示し、失敗時にはバッファーを未保存のまま保ち、権限/認証に失敗した場合にだけ sudo を提案し、上書きせずに競合を表示し、接続が切れた場合や結果が不明な場合には復旧を提案する必要があります。これらの状態は、チェコ語、ドイツ語、英語に翻訳しなければなりません。推奨する状態の文言:

状態 チェコ語 ドイツ語 英語
保存中 Ukládání… Wird gespeichert… Saving…
確定 Uloženo Gespeichert Saved
失敗 Uložení selhalo Speichern fehlgeschlagen Save failed
sudo が必要 Vyžadováno oprávnění sudo sudo-Autorisierung erforderlich Sudo authorization required
競合 Původní soubor byl změněn Originaldatei wurde geändert Original file changed
切断 Spojení bylo přerušeno Verbindung unterbrochen Connection lost
不明 Výsledek uložení není znám Speicherergebnis unbekannt Save result unknown
復旧 Obnova je dostupná Wiederherstellung verfügbar Recovery available

レイテンシーの定義とライフサイクル

Ping は、SSH が使用する解決済みの IP に対する ICMP echo に、クライアント側で実際に応答が返ってきたときに報告されるレイテンシーです。Core は、シェルを使わず、管理者としての起動も必要とせずに、固定の引数配列でプラットフォームの ping 実行ファイルを非同期に呼び出します。Linux、macOS(ping6 を含む)、Windows の引数は、それぞれ個別に選択されます。ICMP のブロック、実行ファイルの欠落、タイムアウト、無効な出力、<1ms のように上限値しか示さない Windows の応答では、数値をでっち上げることなく「利用不可」の値になります。

SSH RTT は、既存の認証済み SSH クライアント上で固定の true exec リクエストを実行するのにかかった、単調な performance.now() による時間です。チャネルの確立、サーバーでの処理、応答の到着が含まれます。純粋なネットワークの RTT でも、SSH のログインにかかる時間でもありません。翻訳されたインジケーターのツールチップで、この違いを説明しています。

監視は、ターミナルが接続されるとすぐに開始されます。プローブは並行して非同期に実行され、リクエスト/プロセスのタイムアウトは 5 秒です。重複を防ぐため、次のサンプルは前のサンプルが完了してから 12 秒後に予定されます。切断すると、ICMP が中止され、タイマーがクリアされ、保留中の SSH の処理がキャンセルされ、値がクリアされます。レコードの識別情報のチェックにより、再接続後に以前の接続が値を発行することはありません。ICMP の失敗によって SSH の接続状態が変わることはありません。

レイテンシーは、既存の state イベント / ssh:state IPC のペイロードに含まれて送られます:

latency: { pingMs: 24.1, sshRttMs: 31.25, measuredAt: "…" }

利用できない数値は null です。コンパクトなターミナルのインジケーターは、ミリ秒、ローカライズされた「利用不可」の文言、チェコ語/ドイツ語/英語のラベルを使用します。切断すると非表示になります。レンダラーに追加の実行機能や IPC リクエストは必要ありませんでした。

検証と運用上の制限

ユーザーの要望により、変更は既存の作業ツリー(現在は feature/hash-checksum-plugin)に残しています。ブランチの切り替えや、関係のない整理は行っていません。

この作業で変更したファイル