From 3d8587c2b04f9c04acb37169b14fcf6060ce2106 Mon Sep 17 00:00:00 2001 From: "ryosuke.a.tanaka" Date: Tue, 3 Feb 2026 14:05:14 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20GUI/CLI=E3=81=AE=E5=88=9D=E6=9C=9F?= =?UTF-8?q?=E5=8C=96=E5=87=A6=E7=90=86=E3=82=92=E4=B8=80=E9=83=A8=E5=85=B1?= =?UTF-8?q?=E9=80=9A=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/log.pp | 1 + .../java/core/packetproxy/PacketProxy.java | 21 ++- .../kotlin/core/packetproxy/AppInitializer.kt | 177 ++++++++++++++++++ .../core/packetproxy/gulp/GulpTerminal.kt | 3 - 4 files changed, 191 insertions(+), 11 deletions(-) create mode 100644 src/main/kotlin/core/packetproxy/AppInitializer.kt diff --git a/scripts/log.pp b/scripts/log.pp index 3996317b..17a4c6e3 100755 --- a/scripts/log.pp +++ b/scripts/log.pp @@ -1,3 +1,4 @@ #!/usr/bin/env packetproxy-cli log +exit diff --git a/src/main/java/core/packetproxy/PacketProxy.java b/src/main/java/core/packetproxy/PacketProxy.java index 13d04971..d9b2ca61 100644 --- a/src/main/java/core/packetproxy/PacketProxy.java +++ b/src/main/java/core/packetproxy/PacketProxy.java @@ -20,7 +20,6 @@ import java.sql.SQLException; import javax.swing.*; import org.apache.commons.io.IOUtils; -import packetproxy.common.ClientKeyManager; import packetproxy.common.I18nString; import packetproxy.common.Utils; import packetproxy.gui.GUIMain; @@ -37,11 +36,21 @@ public PacketProxy() throws Exception { } public static void main(String[] args) throws Exception { + // バイナリへの引数の解釈とセット String gulpMode = getOption("--gulp", args); - Logging.init(gulpMode != null); + String settingsJson = getOption("--settings-json", args); + AppInitializer.setArgs(gulpMode != null, settingsJson); + AppInitializer.initCore(); if (gulpMode != null) { - String settingsJson = getOption("--settings-json", args); + try { + AppInitializer.initGulp(); + AppInitializer.initComponents(); + } catch (Exception e) { + Logging.errWithStackTrace(e); + System.exit(1); + } + Logging.log("Gulp Mode: " + settingsJson); GulpTerminal.run(settingsJson, gulpMode); System.exit(0); @@ -106,11 +115,7 @@ private static String getOption(String option, String[] args) { public void start() throws Exception { startGUI(); - ClientKeyManager.initialize(); - listenPortManager = ListenPortManager.getInstance(); - // encoderのロードに1,2秒かかるのでここでロードをしておく(ここでしておかないと通信がacceptされたタイミングでロードする) - EncoderManager.getInstance(); - VulCheckerManager.getInstance(); + AppInitializer.initComponents(); } private void startGUI() throws Exception { diff --git a/src/main/kotlin/core/packetproxy/AppInitializer.kt b/src/main/kotlin/core/packetproxy/AppInitializer.kt new file mode 100644 index 00000000..d08aceed --- /dev/null +++ b/src/main/kotlin/core/packetproxy/AppInitializer.kt @@ -0,0 +1,177 @@ +package packetproxy + +import java.nio.file.Paths +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ExecutionException +import kotlin.system.exitProcess +import packetproxy.common.ClientKeyManager +import packetproxy.common.ConfigIO +import packetproxy.common.Utils +import packetproxy.model.Database +import packetproxy.model.Packets +import packetproxy.util.Logging + +object AppInitializer { + private var isGulp = false // Gulp modeか否か + private var settingsPath = "" // 設定用JSONのファイルpath + + private var isCoreNotReady = true + private var isGulpNotReady = true + private var isComponentsNotReady = true + + @JvmStatic + fun setArgs(isGulp: Boolean, settingsPath: String?) { + this.isGulp = isGulp + this.settingsPath = settingsPath ?: "" + } + + /** GUI / CLI(Gulp) に関連なく最初に実行するべき初期化を一度のみ実行する */ + @JvmStatic + fun initCore() { + check(isCoreNotReady) { "initCore() has already been done !" } + + // ログ機能のエラーについては標準エラー出力への出力を行い終了する + try { + Logging.init(isGulp) + } catch (e: Exception) { + System.err.println("[FATAL ERROR]: Logging.init(), exit 1") + System.err.println(e.message) + e.printStackTrace(System.err) + + exitProcess(1) + } + + Logging.log("Launching PacketProxy !") + + isCoreNotReady = false + } + + /** CLI(Gulp) 専用の初期化を実行 GUI ではGUIMainなどで実行されている処理 */ + @JvmStatic + fun initGulp() { + check(isGulp) { "initGulp() is for gulp mode only !" } + check(isGulpNotReady) { "initGulp() has already been done !" } + + initDatabase() + initPackets() + + isGulpNotReady = false + } + + private fun initDatabase() { + val dbPath = + Paths.get(System.getProperty("user.home"), ".packetproxy", "db", "resources.sqlite3") + Database.getInstance().openAt(dbPath.toString()) + Logging.log("Databaseを初期化しました: $dbPath") + } + + private fun initPackets() { + Packets.getInstance(false) // CLIモードでは履歴を復元しない + Logging.log("Packetsを初期化しました") + } + + /** + * GUI / CLI(Gulp) に共通の初期化を GUI の表示よりも後回しして良い初期化を一度のみ実行する + * + * 並列処理による高速化: + * - EncoderManagerとVulCheckerManagerは完全に独立しているため、並列実行可能 + * - ClientKeyManagerとListenPortManagerはDatabaseに依存しているが、 + * Databaseは既に初期化済み(GUIモードではstartGUI()で、CLIモードではinitGulp()で初期化) + * かつ、それぞれ異なるテーブル(ClientCertificates/Servers/ListenPorts)にアクセスするため、 読み取り操作のみであれば並列実行可能 + * + * 依存関係の整理: + * 1. ClientKeyManager: ClientCertificates → Database (読み取りのみ) + * 2. ListenPortManager: ListenPorts + Servers → Database (読み取りのみ) + * 3. EncoderManager: クラスパス/JARファイルのスキャン(Database非依存) + * 4. VulCheckerManager: クラスパスのスキャン(Database非依存) + */ + @JvmStatic + fun initComponents() { + check(isComponentsNotReady) { "initComponents() has already been done !" } + + // Database依存のコンポーネントを並列実行 + // 注意: Databaseは既に初期化済みであることを前提とする + val dbDependentFuture1 = CompletableFuture.runAsync { initClientKeyManager() } + + val dbDependentFuture2 = CompletableFuture.runAsync { initListenPortManager() } + + // Database非依存のコンポーネントを並列実行 + val independentFuture1 = + CompletableFuture.runAsync { + // encoderのロードに1,2秒かかるのでここでロードをしておく(ここでしておかないと通信がacceptされたタイミングでロードする) + initEncoderManager() + } + + val independentFuture2 = CompletableFuture.runAsync { initVulCheckerManager() } + + // 全ての初期化が完了するまで待機 + try { + CompletableFuture.allOf( + dbDependentFuture1, + dbDependentFuture2, + independentFuture1, + independentFuture2, + ) + .get() + + Logging.log("全てのコンポーネントの初期化が完了しました") + } catch (e: ExecutionException) { + // ExecutionExceptionは、CompletableFuture内で発生した例外をラップした例外 + // e.causeで実際の例外を取得できる + val cause = e.cause + if (cause is Exception) { + Logging.errWithStackTrace(cause) + throw cause + } else { + Logging.errWithStackTrace(e) + throw e + } + } catch (e: InterruptedException) { + Logging.errWithStackTrace(e) + Thread.currentThread().interrupt() + throw RuntimeException("初期化が中断されました", e) + } + + loadSettingsFromJson() + + isComponentsNotReady = false + } + + private fun initClientKeyManager() { + ClientKeyManager.initialize() + Logging.log("ClientKeyManagerを初期化しました") + } + + private fun initListenPortManager() { + ListenPortManager.getInstance() + Logging.log("ListenPortManagerを初期化しました") + } + + private fun initEncoderManager() { + EncoderManager.getInstance() + Logging.log("EncoderManagerを初期化しました") + } + + private fun initVulCheckerManager() { + VulCheckerManager.getInstance() + Logging.log("VulCheckerManagerを初期化しました") + } + + /** JSON設定ファイルを読み込んで適用 ListenPortManager初期化後に呼び出すことで、設定ファイル内の有効なプロキシが自動的に開始される */ + private fun loadSettingsFromJson() { + if (settingsPath.isEmpty()) return + + try { + val jsonBytes = Utils.readfile(settingsPath) + val json = String(jsonBytes, Charsets.UTF_8) + + val configIO = ConfigIO() + configIO.setOptions(json) + + Logging.log("設定ファイルを正常に読み込みました: $settingsPath") + } catch (e: Exception) { + Logging.err("設定ファイルの読み込みに失敗しました: ${e.message}", e) + Logging.errWithStackTrace(e) + } + } +} diff --git a/src/main/kotlin/core/packetproxy/gulp/GulpTerminal.kt b/src/main/kotlin/core/packetproxy/gulp/GulpTerminal.kt index 3c5ec4d9..ab75fd3b 100644 --- a/src/main/kotlin/core/packetproxy/gulp/GulpTerminal.kt +++ b/src/main/kotlin/core/packetproxy/gulp/GulpTerminal.kt @@ -30,9 +30,6 @@ import packetproxy.util.Logging object GulpTerminal { @JvmStatic fun run(settingJsonPath: String?, scriptFilePath: String) { - // 設定ファイルを読み込む(ListenPortManager初期化後) - loadSettingsFromJson(settingJsonPath) - val cmdCtx = CommandContext() val terminal = TerminalFactory.create(cmdCtx) From 4ead2fa35c5ead89d13e9d17d4fe540ed97af233 Mon Sep 17 00:00:00 2001 From: "ryosuke.a.tanaka" Date: Tue, 3 Feb 2026 17:01:30 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20CommandOutput=E6=8A=BD=E8=B1=A1?= =?UTF-8?q?=E5=8C=96=E3=82=A4=E3=83=B3=E3=82=BF=E3=83=BC=E3=83=95=E3=82=A7?= =?UTF-8?q?=E3=83=BC=E3=82=B9=E3=81=A8=E5=AE=9F=E8=A3=85=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLI出力の抽象化のための基盤コンポーネントを追加: - OutputStyle: 色付けスタイルインターフェース - AnsiStyle: ANSIエスケープシーケンス実装(標準出力用) - PlainStyle: 空文字列実装(バッファ出力用) - CommandOutput: 出力抽象化インターフェース - ConsoleOutput: 標準出力実装 - BufferedOutput: バッファ蓄積実装(テスト・内部連携用) --- .../packetproxy/gulp/output/BufferedOutput.kt | 41 ++++++++ .../packetproxy/gulp/output/CommandOutput.kt | 44 +++++++++ .../packetproxy/gulp/output/ConsoleOutput.kt | 35 +++++++ .../packetproxy/gulp/output/OutputStyle.kt | 93 +++++++++++++++++++ 4 files changed, 213 insertions(+) create mode 100644 src/main/kotlin/core/packetproxy/gulp/output/BufferedOutput.kt create mode 100644 src/main/kotlin/core/packetproxy/gulp/output/CommandOutput.kt create mode 100644 src/main/kotlin/core/packetproxy/gulp/output/ConsoleOutput.kt create mode 100644 src/main/kotlin/core/packetproxy/gulp/output/OutputStyle.kt diff --git a/src/main/kotlin/core/packetproxy/gulp/output/BufferedOutput.kt b/src/main/kotlin/core/packetproxy/gulp/output/BufferedOutput.kt new file mode 100644 index 00000000..9446256b --- /dev/null +++ b/src/main/kotlin/core/packetproxy/gulp/output/BufferedOutput.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2025 DeNA Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package packetproxy.gulp.output + +/** + * バッファへの出力実装(テスト・内部連携用) + * + * 出力内容を蓄積し、後から取得可能にする。 色付けは無効(PlainStyle)のため、ANSIエスケープシーケンスは含まれない。 + */ +class BufferedOutput : CommandOutput { + override val style: OutputStyle = PlainStyle + + private val buffer = StringBuilder() + + override fun println(text: String) { + buffer.appendLine(text) + } + + override fun print(text: String) { + buffer.append(text) + } + + override fun getOutput(): String = buffer.toString() + + override fun clear() { + buffer.clear() + } +} diff --git a/src/main/kotlin/core/packetproxy/gulp/output/CommandOutput.kt b/src/main/kotlin/core/packetproxy/gulp/output/CommandOutput.kt new file mode 100644 index 00000000..22c13001 --- /dev/null +++ b/src/main/kotlin/core/packetproxy/gulp/output/CommandOutput.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2025 DeNA Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package packetproxy.gulp.output + +/** + * コマンド出力の抽象化インターフェース + * + * 標準出力への直接呼び出しを避け、テスト容易性と出力先の柔軟性を実現する。 + * + * 主な実装: + * - ConsoleOutput: 標準出力(本番用、色付きサポート) + * - BufferedOutput: バッファ蓄積(テスト・内部連携用、色なし) + */ +interface CommandOutput { + /** + * 色付きテキストを生成するためのスタイルオブジェクト ConsoleOutput: ANSIエスケープシーケンスを含むスタイル BufferedOutput: 空文字列を返すスタイル(色なし) + */ + val style: OutputStyle + + /** テキストを出力する(改行付き) */ + fun println(text: String = "") + + /** テキストを出力する(改行なし) */ + fun print(text: String) + + /** 出力済みの内容を取得する ConsoleOutputでは空文字列を返す(キャプチャしない) BufferedOutputでは蓄積された内容を返す */ + fun getOutput(): String + + /** 出力バッファをクリアする */ + fun clear() +} diff --git a/src/main/kotlin/core/packetproxy/gulp/output/ConsoleOutput.kt b/src/main/kotlin/core/packetproxy/gulp/output/ConsoleOutput.kt new file mode 100644 index 00000000..1381f8f0 --- /dev/null +++ b/src/main/kotlin/core/packetproxy/gulp/output/ConsoleOutput.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2025 DeNA Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package packetproxy.gulp.output + +/** 標準出力への出力実装(本番用) ANSIエスケープシーケンスによる色付けをサポート */ +object ConsoleOutput : CommandOutput { + override val style: OutputStyle = AnsiStyle + + override fun println(text: String) { + kotlin.io.println(text) + } + + override fun print(text: String) { + kotlin.io.print(text) + } + + override fun getOutput(): String = "" + + override fun clear() { + // 標準出力はクリアできない + } +} diff --git a/src/main/kotlin/core/packetproxy/gulp/output/OutputStyle.kt b/src/main/kotlin/core/packetproxy/gulp/output/OutputStyle.kt new file mode 100644 index 00000000..c7bca119 --- /dev/null +++ b/src/main/kotlin/core/packetproxy/gulp/output/OutputStyle.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2025 DeNA Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package packetproxy.gulp.output + +/** + * 出力スタイル(色付け)を提供するインターフェース + * + * ConsoleOutputではANSIエスケープシーケンスを含むスタイル、 BufferedOutputでは空文字列を返すスタイルを使用することで、 + * 標準出力では色付き、内部連携では色なしのテキストを生成できる。 + * + * 使用例: + * ``` + * ctx.println("${ctx.style.green}Success${ctx.style.reset}") + * ctx.println(ctx.style.success("Operation completed")) + * ``` + */ +interface OutputStyle { + val reset: String + val bold: String + + // 前景色 + val black: String + val red: String + val green: String + val yellow: String + val blue: String + val magenta: String + val cyan: String + val white: String + + /** 指定した色でテキストを装飾する */ + fun colored(text: String, color: String): String = "$color$text$reset" + + /** 成功メッセージ用(緑色) */ + fun success(text: String): String = colored(text, green) + + /** エラーメッセージ用(赤色) */ + fun error(text: String): String = colored(text, red) + + /** 警告メッセージ用(黄色) */ + fun warning(text: String): String = colored(text, yellow) + + /** 情報メッセージ用(シアン) */ + fun info(text: String): String = colored(text, cyan) +} + +/** + * ANSIエスケープシーケンスを使用した色付きスタイル 標準出力(ConsoleOutput)で使用 + * + * Note: Logging.ktではorg.jline.jansi.Ansiのビルダーパターンを使用しているが、 + * OutputStyleインターフェース(文字列プロパティベース)とは設計が異なる。 ANSIエスケープコード自体は標準仕様のため、独自に定数を定義する。 + */ +object AnsiStyle : OutputStyle { + override val reset = "\u001B[0m" + override val bold = "\u001B[1m" + + override val black = "\u001B[30m" + override val red = "\u001B[31m" + override val green = "\u001B[32m" + override val yellow = "\u001B[33m" + override val blue = "\u001B[34m" + override val magenta = "\u001B[35m" + override val cyan = "\u001B[36m" + override val white = "\u001B[37m" +} + +/** 色なしスタイル(全て空文字列) バッファ出力(BufferedOutput)や内部連携で使用 */ +object PlainStyle : OutputStyle { + override val reset = "" + override val bold = "" + + override val black = "" + override val red = "" + override val green = "" + override val yellow = "" + override val blue = "" + override val magenta = "" + override val cyan = "" + override val white = "" +} From 6457993ca30c902c8b1842e439b1d76f4572fc36 Mon Sep 17 00:00:00 2001 From: "ryosuke.a.tanaka" Date: Tue, 3 Feb 2026 17:01:38 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20CommandContext=E3=81=ABoutput?= =?UTF-8?q?=E3=83=95=E3=82=A3=E3=83=BC=E3=83=AB=E3=83=89=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CommandContextにCommandOutput型のoutputフィールドを追加 - println/print便利メソッドとstyleショートカットを追加 - Commandインターフェースのシグネチャにctxパラメータを追加 --- .../core/packetproxy/gulp/CommandContext.kt | 19 ++++++++++++++++++- .../core/packetproxy/gulp/command/Command.kt | 3 ++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/core/packetproxy/gulp/CommandContext.kt b/src/main/kotlin/core/packetproxy/gulp/CommandContext.kt index 78e630a3..951773c7 100644 --- a/src/main/kotlin/core/packetproxy/gulp/CommandContext.kt +++ b/src/main/kotlin/core/packetproxy/gulp/CommandContext.kt @@ -18,12 +18,29 @@ package packetproxy.gulp import kotlinx.coroutines.Job import packetproxy.cli.CLIModeHandler import packetproxy.cli.EncodeModeHandler +import packetproxy.gulp.output.CommandOutput +import packetproxy.gulp.output.ConsoleOutput +import packetproxy.gulp.output.OutputStyle -class CommandContext { +/** + * コマンド実行コンテキスト + * + * @param output 出力先(デフォルト: 標準出力) + */ +class CommandContext(val output: CommandOutput = ConsoleOutput) { var currentHandler: CLIModeHandler = EncodeModeHandler var executionJob: Job? = null fun cancelJob() { executionJob?.cancel() } + + // 便利メソッド: 出力 + fun println(text: String = "") = output.println(text) + + fun print(text: String) = output.print(text) + + // スタイル(色付け)へのショートカット + val style: OutputStyle + get() = output.style } diff --git a/src/main/kotlin/core/packetproxy/gulp/command/Command.kt b/src/main/kotlin/core/packetproxy/gulp/command/Command.kt index 146b2ffb..de8b196b 100644 --- a/src/main/kotlin/core/packetproxy/gulp/command/Command.kt +++ b/src/main/kotlin/core/packetproxy/gulp/command/Command.kt @@ -15,8 +15,9 @@ */ package core.packetproxy.gulp.command +import packetproxy.gulp.CommandContext import packetproxy.gulp.ParsedCommand interface Command { - suspend operator fun invoke(parsed: ParsedCommand) + suspend operator fun invoke(parsed: ParsedCommand, ctx: CommandContext) } From 20537373a904106ab041e957fda5ba5a5116126d Mon Sep 17 00:00:00 2001 From: "ryosuke.a.tanaka" Date: Tue, 3 Feb 2026 17:01:50 +0900 Subject: [PATCH 4/6] =?UTF-8?q?refactor:=20=E7=9B=B4=E6=8E=A5println?= =?UTF-8?q?=E3=82=92ctx.println=E3=81=AB=E7=BD=AE=E3=81=8D=E6=8F=9B?= =?UTF-8?q?=E3=81=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLIコマンド実行時の出力をCommandOutput経由に変更: - CLIModeHandler: handleCommandにctx引数追加、出力をctx経由に - EncodeModeHandler/DecodeModeHandler: extensionCommandにctx引数追加 - GulpTerminal: 出力をctx経由に - LogCommand/SourceCommand: シグネチャ更新 - TerminalSource/FallBackTerminalSource: 出力をctx経由に --- .../core/packetproxy/gulp/CLIModeHandler.kt | 18 ++++++++++-------- .../core/packetproxy/gulp/DecodeModeHandler.kt | 7 ++++--- .../core/packetproxy/gulp/EncodeModeHandler.kt | 7 ++++--- .../core/packetproxy/gulp/GulpTerminal.kt | 7 ++++--- .../packetproxy/gulp/command/LogCommand.kt | 3 ++- .../packetproxy/gulp/command/SourceCommand.kt | 3 ++- .../gulp/input/FallBackTerminalSource.kt | 6 +++--- .../packetproxy/gulp/input/TerminalSource.kt | 2 +- 8 files changed, 30 insertions(+), 23 deletions(-) diff --git a/src/main/kotlin/core/packetproxy/gulp/CLIModeHandler.kt b/src/main/kotlin/core/packetproxy/gulp/CLIModeHandler.kt index 9d3901ef..910fec5d 100644 --- a/src/main/kotlin/core/packetproxy/gulp/CLIModeHandler.kt +++ b/src/main/kotlin/core/packetproxy/gulp/CLIModeHandler.kt @@ -20,6 +20,7 @@ import core.packetproxy.gulp.command.SourceCommand import org.jline.builtins.Completers.TreeCompleter import org.jline.builtins.Completers.TreeCompleter.node import packetproxy.common.I18nString +import packetproxy.gulp.CommandContext import packetproxy.gulp.ParsedCommand /** CLIモードのハンドラーインターフェース 各モード(encode/decode)で異なるコマンド処理と補完を提供 */ @@ -49,28 +50,29 @@ abstract class CLIModeHandler { * コマンドを処理 * * @param parsed コマンド + * @param ctx コマンドコンテキスト */ - suspend fun handleCommand(parsed: ParsedCommand) { + suspend fun handleCommand(parsed: ParsedCommand, ctx: CommandContext) { when (parsed.cmd) { - "help" -> println(getHelpMessage()) + "help" -> ctx.println(getHelpMessage()) ".", - "source" -> SourceCommand(parsed) + "source" -> SourceCommand(parsed, ctx) "l", - "log" -> LogCommand(parsed) + "log" -> LogCommand(parsed, ctx) - else -> extensionCommand(parsed) + else -> extensionCommand(parsed, ctx) } } - protected fun commandNotDefined(parsed: ParsedCommand) { - println(I18nString.get("command not defined: %s", parsed.raw)) + protected fun commandNotDefined(parsed: ParsedCommand, ctx: CommandContext) { + ctx.println(I18nString.get("command not defined: %s", parsed.raw)) } abstract fun getOppositeMode(): CLIModeHandler - protected abstract fun extensionCommand(parsed: ParsedCommand) + protected abstract fun extensionCommand(parsed: ParsedCommand, ctx: CommandContext) fun getHelpMessage(): String { return """共通コマンド: diff --git a/src/main/kotlin/core/packetproxy/gulp/DecodeModeHandler.kt b/src/main/kotlin/core/packetproxy/gulp/DecodeModeHandler.kt index 60788c5f..250f26ca 100644 --- a/src/main/kotlin/core/packetproxy/gulp/DecodeModeHandler.kt +++ b/src/main/kotlin/core/packetproxy/gulp/DecodeModeHandler.kt @@ -19,6 +19,7 @@ import org.fusesource.jansi.Ansi import org.fusesource.jansi.Ansi.Color.CYAN import org.jline.builtins.Completers.TreeCompleter import org.jline.builtins.Completers.TreeCompleter.node +import packetproxy.gulp.CommandContext import packetproxy.gulp.ParsedCommand /** Decode Modeのハンドラー */ @@ -35,11 +36,11 @@ object DecodeModeHandler : CLIModeHandler() { return EncodeModeHandler } - override fun extensionCommand(parsed: ParsedCommand) { + override fun extensionCommand(parsed: ParsedCommand, ctx: CommandContext) { when (parsed.cmd) { - "status" -> println("Decode Mode !") + "status" -> ctx.println("Decode Mode !") - else -> commandNotDefined(parsed) + else -> commandNotDefined(parsed, ctx) } } diff --git a/src/main/kotlin/core/packetproxy/gulp/EncodeModeHandler.kt b/src/main/kotlin/core/packetproxy/gulp/EncodeModeHandler.kt index bc2a6577..ee8201a9 100644 --- a/src/main/kotlin/core/packetproxy/gulp/EncodeModeHandler.kt +++ b/src/main/kotlin/core/packetproxy/gulp/EncodeModeHandler.kt @@ -20,6 +20,7 @@ import org.fusesource.jansi.Ansi.Color.GREEN import org.jline.builtins.Completers.TreeCompleter import org.jline.builtins.Completers.TreeCompleter.node import org.jline.reader.impl.completer.StringsCompleter +import packetproxy.gulp.CommandContext import packetproxy.gulp.ParsedCommand /** Encode Modeのハンドラー */ @@ -39,11 +40,11 @@ object EncodeModeHandler : CLIModeHandler() { return DecodeModeHandler } - override fun extensionCommand(parsed: ParsedCommand) { + override fun extensionCommand(parsed: ParsedCommand, ctx: CommandContext) { when (parsed.cmd) { - "status" -> println("Encode Mode !") + "status" -> ctx.println("Encode Mode !") - else -> commandNotDefined(parsed) + else -> commandNotDefined(parsed, ctx) } } diff --git a/src/main/kotlin/core/packetproxy/gulp/GulpTerminal.kt b/src/main/kotlin/core/packetproxy/gulp/GulpTerminal.kt index ab75fd3b..42fecafa 100644 --- a/src/main/kotlin/core/packetproxy/gulp/GulpTerminal.kt +++ b/src/main/kotlin/core/packetproxy/gulp/GulpTerminal.kt @@ -47,7 +47,7 @@ object GulpTerminal { when (e) { is UserInterruptException -> {} // Ctrl+C is EndOfFileException -> { - println("${cmdCtx.currentHandler.prompts}exit") + cmdCtx.println("${cmdCtx.currentHandler.prompts}exit") break } // Ctrl+D else -> Logging.errWithStackTrace(e) @@ -74,10 +74,11 @@ object GulpTerminal { else -> { cmdCtx.executionJob = launch { try { - cmdCtx.currentHandler.handleCommand(parsed) + cmdCtx.currentHandler.handleCommand(parsed, cmdCtx) } catch (e: Exception) { when (e) { - is CancellationException -> println() // Ctrl+Cによってcancel()が実行された結果throwされるもの + is CancellationException -> + cmdCtx.println() // Ctrl+Cによってcancel()が実行された結果throwされるもの else -> Logging.errWithStackTrace(e) } } diff --git a/src/main/kotlin/core/packetproxy/gulp/command/LogCommand.kt b/src/main/kotlin/core/packetproxy/gulp/command/LogCommand.kt index 331ea804..daae2789 100644 --- a/src/main/kotlin/core/packetproxy/gulp/command/LogCommand.kt +++ b/src/main/kotlin/core/packetproxy/gulp/command/LogCommand.kt @@ -15,11 +15,12 @@ */ package core.packetproxy.gulp.command +import packetproxy.gulp.CommandContext import packetproxy.gulp.ParsedCommand import packetproxy.util.Logging object LogCommand : Command { - override suspend fun invoke(parsed: ParsedCommand) { + override suspend fun invoke(parsed: ParsedCommand, ctx: CommandContext) { Logging.tailLog() } } diff --git a/src/main/kotlin/core/packetproxy/gulp/command/SourceCommand.kt b/src/main/kotlin/core/packetproxy/gulp/command/SourceCommand.kt index 4e54871a..ed8c3f82 100644 --- a/src/main/kotlin/core/packetproxy/gulp/command/SourceCommand.kt +++ b/src/main/kotlin/core/packetproxy/gulp/command/SourceCommand.kt @@ -15,12 +15,13 @@ */ package core.packetproxy.gulp.command +import packetproxy.gulp.CommandContext import packetproxy.gulp.ParsedCommand import packetproxy.gulp.input.ChainedSource import packetproxy.gulp.input.ScriptSource object SourceCommand : Command { - override suspend fun invoke(parsed: ParsedCommand) { + override suspend fun invoke(parsed: ParsedCommand, ctx: CommandContext) { ChainedSource.push(ScriptSource(parsed.args.firstOrNull() ?: "")) } } diff --git a/src/main/kotlin/core/packetproxy/gulp/input/FallBackTerminalSource.kt b/src/main/kotlin/core/packetproxy/gulp/input/FallBackTerminalSource.kt index 46232ce0..d620cacd 100644 --- a/src/main/kotlin/core/packetproxy/gulp/input/FallBackTerminalSource.kt +++ b/src/main/kotlin/core/packetproxy/gulp/input/FallBackTerminalSource.kt @@ -26,17 +26,17 @@ class FallBackTerminalSource( private val scanner: java.util.Scanner, ) : LineSource() { override fun execOpen() { - println("=== Fallback CLI Mode ===") + cmdCtx.println("=== Fallback CLI Mode ===") } override fun readLine(): String { try { - print(cmdCtx.currentHandler.prompts) + cmdCtx.print(cmdCtx.currentHandler.prompts) return scanner.nextLine() } catch (e: NoSuchElementException) { // FallbackTerminalでCtrl+Dが押下された場合に投げられる例外 // DefaultTerminalでCtrl+Dが押下された場合と同じ例外に変換している - println() // 表記をDefaultTerminalと揃える + cmdCtx.println() // 表記をDefaultTerminalと揃える throw org.jline.reader.EndOfFileException("EOF from Standard Input") } } diff --git a/src/main/kotlin/core/packetproxy/gulp/input/TerminalSource.kt b/src/main/kotlin/core/packetproxy/gulp/input/TerminalSource.kt index 39f35858..6af50289 100644 --- a/src/main/kotlin/core/packetproxy/gulp/input/TerminalSource.kt +++ b/src/main/kotlin/core/packetproxy/gulp/input/TerminalSource.kt @@ -26,7 +26,7 @@ class TerminalSource( private val reader: LineReader, ) : LineSource() { override fun execOpen() { - println("=== CLI Mode ===") + cmdCtx.println("=== CLI Mode ===") } override fun readLine(): String { From 12f3d090b63e9c9684e6485324ec2786be500e23 Mon Sep 17 00:00:00 2001 From: "ryosuke.a.tanaka" Date: Tue, 3 Feb 2026 17:01:59 +0900 Subject: [PATCH 5/6] =?UTF-8?q?test:=20=E5=87=BA=E5=8A=9B=E6=8A=BD?= =?UTF-8?q?=E8=B1=A1=E5=8C=96=E3=81=AE=E3=83=86=E3=82=B9=E3=83=88=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=E3=83=BB=E6=97=A2=E5=AD=98=E3=83=86=E3=82=B9?= =?UTF-8?q?=E3=83=88=E6=94=B9=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新規テスト: - BufferedOutputTest: バッファ出力のユニットテスト - OutputStyleTest: AnsiStyle/PlainStyleのテスト - CommandOutputIntegrationTest: コマンド出力の統合テスト 既存テスト改善: - SourceCommandTest: BufferedOutput使用に更新 - TerminalCoroutinesTest: BufferedOutput使用に更新 --- .../gulp/CommandOutputIntegrationTest.kt | 115 ++++++++++++++++++ .../gulp/TerminalCoroutinesTest.kt | 3 +- .../gulp/command/SourceCommandTest.kt | 7 +- .../gulp/output/BufferedOutputTest.kt | 83 +++++++++++++ .../gulp/output/OutputStyleTest.kt | 97 +++++++++++++++ 5 files changed, 302 insertions(+), 3 deletions(-) create mode 100644 src/test/kotlin/packetproxy/gulp/CommandOutputIntegrationTest.kt create mode 100644 src/test/kotlin/packetproxy/gulp/output/BufferedOutputTest.kt create mode 100644 src/test/kotlin/packetproxy/gulp/output/OutputStyleTest.kt diff --git a/src/test/kotlin/packetproxy/gulp/CommandOutputIntegrationTest.kt b/src/test/kotlin/packetproxy/gulp/CommandOutputIntegrationTest.kt new file mode 100644 index 00000000..9482be6c --- /dev/null +++ b/src/test/kotlin/packetproxy/gulp/CommandOutputIntegrationTest.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2025 DeNA Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package packetproxy.gulp + +import kotlinx.coroutines.runBlocking +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import packetproxy.cli.DecodeModeHandler +import packetproxy.cli.EncodeModeHandler +import packetproxy.gulp.output.BufferedOutput + +class CommandOutputIntegrationTest { + + private lateinit var output: BufferedOutput + private lateinit var ctx: CommandContext + + @BeforeEach + fun setUp() { + output = BufferedOutput() + ctx = CommandContext(output) + } + + @Test + fun EncodeModeでstatusコマンドがモード名を出力すること() = runBlocking { + ctx.currentHandler = EncodeModeHandler + val parsed = CommandParser.parse("status")!! + + ctx.currentHandler.handleCommand(parsed, ctx) + + assertThat(output.getOutput()).contains("Encode Mode") + } + + @Test + fun DecodeModeでstatusコマンドがモード名を出力すること() = runBlocking { + ctx.currentHandler = DecodeModeHandler + val parsed = CommandParser.parse("status")!! + + ctx.currentHandler.handleCommand(parsed, ctx) + + assertThat(output.getOutput()).contains("Decode Mode") + } + + @Test + fun helpコマンドがヘルプメッセージを出力すること() = runBlocking { + ctx.currentHandler = EncodeModeHandler + val parsed = CommandParser.parse("help")!! + + ctx.currentHandler.handleCommand(parsed, ctx) + + val result = output.getOutput() + assertThat(result).contains("共通コマンド") + assertThat(result).contains("exit") + assertThat(result).contains("help") + assertThat(result).contains("log") + } + + @Test + fun 未定義コマンドでエラーメッセージが出力されること() = runBlocking { + ctx.currentHandler = EncodeModeHandler + val parsed = CommandParser.parse("undefined_command_xyz")!! + + ctx.currentHandler.handleCommand(parsed, ctx) + + assertThat(output.getOutput()).contains("command not defined") + } + + @Test + fun 出力にANSIエスケープシーケンスが含まれないこと() = runBlocking { + ctx.currentHandler = EncodeModeHandler + val parsed = CommandParser.parse("status")!! + + ctx.currentHandler.handleCommand(parsed, ctx) + + val result = output.getOutput() + // ANSIエスケープシーケンス(\u001B[)が含まれていないことを確認 + assertThat(result).doesNotContain("\u001B[") + } + + @Test + fun clearで出力がリセットされること() = runBlocking { + ctx.currentHandler = EncodeModeHandler + val parsed = CommandParser.parse("status")!! + + ctx.currentHandler.handleCommand(parsed, ctx) + assertThat(output.getOutput()).isNotEmpty() + + output.clear() + assertThat(output.getOutput()).isEmpty() + } + + @Test + fun styleを使用して色付きテキストを生成できること() { + // ConsoleOutputではANSIコード付き、BufferedOutputでは色なし + val successText = ctx.style.success("OK") + val errorText = ctx.style.error("Error") + + // BufferedOutputなので色コードなし + assertThat(successText).isEqualTo("OK") + assertThat(errorText).isEqualTo("Error") + } +} diff --git a/src/test/kotlin/packetproxy/gulp/TerminalCoroutinesTest.kt b/src/test/kotlin/packetproxy/gulp/TerminalCoroutinesTest.kt index d0c7acee..a3064f2b 100644 --- a/src/test/kotlin/packetproxy/gulp/TerminalCoroutinesTest.kt +++ b/src/test/kotlin/packetproxy/gulp/TerminalCoroutinesTest.kt @@ -27,6 +27,7 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir import packetproxy.gulp.input.ChainedSource import packetproxy.gulp.input.LineSource +import packetproxy.gulp.output.BufferedOutput class TerminalCoroutinesTest { @TempDir lateinit var tempDir: Path @@ -58,7 +59,7 @@ class TerminalCoroutinesTest { ChainedSource.push(mockTerminal) ChainedSource.open() - val cmdCtx = CommandContext() + val cmdCtx = CommandContext(BufferedOutput()) // 長時間実行されるコマンドをシミュレート var commandStarted = false diff --git a/src/test/kotlin/packetproxy/gulp/command/SourceCommandTest.kt b/src/test/kotlin/packetproxy/gulp/command/SourceCommandTest.kt index 64445c49..9ef3f144 100644 --- a/src/test/kotlin/packetproxy/gulp/command/SourceCommandTest.kt +++ b/src/test/kotlin/packetproxy/gulp/command/SourceCommandTest.kt @@ -25,6 +25,7 @@ import org.junit.jupiter.api.io.TempDir import packetproxy.gulp.input.ChainedSource import packetproxy.gulp.input.LineSource import packetproxy.gulp.input.ScriptSource +import packetproxy.gulp.output.BufferedOutput class SourceCommandTest { @TempDir lateinit var tempDir: Path @@ -175,6 +176,7 @@ class SourceCommandTest { ChainedSource.open() val commands = mutableListOf() + val ctx = CommandContext(BufferedOutput()) while (true) { val line = ChainedSource.readLine() ?: break @@ -183,7 +185,7 @@ class SourceCommandTest { when (parsed.cmd) { "" -> continue ".", - "source" -> SourceCommand(parsed) + "source" -> SourceCommand(parsed, ctx) else -> commands.add(parsed.cmd) } @@ -242,6 +244,7 @@ class SourceCommandTest { ChainedSource.open() val commands = mutableListOf() + val ctx = CommandContext(BufferedOutput()) while (true) { val line = ChainedSource.readLine() ?: break @@ -251,7 +254,7 @@ class SourceCommandTest { "" -> continue "exit" -> break ".", - "source" -> SourceCommand(parsed) + "source" -> SourceCommand(parsed, ctx) else -> commands.add(parsed.cmd) } diff --git a/src/test/kotlin/packetproxy/gulp/output/BufferedOutputTest.kt b/src/test/kotlin/packetproxy/gulp/output/BufferedOutputTest.kt new file mode 100644 index 00000000..06c42d47 --- /dev/null +++ b/src/test/kotlin/packetproxy/gulp/output/BufferedOutputTest.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2025 DeNA Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package packetproxy.gulp.output + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class BufferedOutputTest { + + private lateinit var output: BufferedOutput + + @BeforeEach + fun setUp() { + output = BufferedOutput() + } + + @Test + fun printlnで改行付きで出力されること() { + output.println("Hello") + output.println("World") + + assertThat(output.getOutput()).isEqualTo("Hello\nWorld\n") + } + + @Test + fun printで改行なしで出力されること() { + output.print("Hello") + output.print("World") + + assertThat(output.getOutput()).isEqualTo("HelloWorld") + } + + @Test + fun printとprintlnを混在させられること() { + output.print("Hello ") + output.println("World") + output.print("!") + + assertThat(output.getOutput()).isEqualTo("Hello World\n!") + } + + @Test + fun 空文字列のprintlnで改行のみ出力されること() { + output.println() + output.println() + + assertThat(output.getOutput()).isEqualTo("\n\n") + } + + @Test + fun clearでバッファがクリアされること() { + output.println("Hello") + output.clear() + + assertThat(output.getOutput()).isEmpty() + } + + @Test + fun styleがPlainStyleであること() { + assertThat(output.style).isEqualTo(PlainStyle) + } + + @Test + fun styleの色コードが空文字列であること() { + assertThat(output.style.red).isEmpty() + assertThat(output.style.green).isEmpty() + assertThat(output.style.reset).isEmpty() + } +} diff --git a/src/test/kotlin/packetproxy/gulp/output/OutputStyleTest.kt b/src/test/kotlin/packetproxy/gulp/output/OutputStyleTest.kt new file mode 100644 index 00000000..19907fb2 --- /dev/null +++ b/src/test/kotlin/packetproxy/gulp/output/OutputStyleTest.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2025 DeNA Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package packetproxy.gulp.output + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class OutputStyleTest { + + @Test + fun AnsiStyleのresetがANSIエスケープシーケンスであること() { + assertThat(AnsiStyle.reset).isEqualTo("\u001B[0m") + } + + @Test + fun AnsiStyleの各色がANSIエスケープシーケンスであること() { + assertThat(AnsiStyle.red).isEqualTo("\u001B[31m") + assertThat(AnsiStyle.green).isEqualTo("\u001B[32m") + assertThat(AnsiStyle.yellow).isEqualTo("\u001B[33m") + assertThat(AnsiStyle.blue).isEqualTo("\u001B[34m") + assertThat(AnsiStyle.cyan).isEqualTo("\u001B[36m") + } + + @Test + fun PlainStyleの全ての値が空文字列であること() { + assertThat(PlainStyle.reset).isEmpty() + assertThat(PlainStyle.bold).isEmpty() + assertThat(PlainStyle.red).isEmpty() + assertThat(PlainStyle.green).isEmpty() + assertThat(PlainStyle.yellow).isEmpty() + assertThat(PlainStyle.blue).isEmpty() + assertThat(PlainStyle.cyan).isEmpty() + } + + @Test + fun AnsiStyleのcoloredメソッドが正しく色付けすること() { + val result = AnsiStyle.colored("Hello", AnsiStyle.red) + + assertThat(result).isEqualTo("\u001B[31mHello\u001B[0m") + } + + @Test + fun PlainStyleのcoloredメソッドが色なしで返すこと() { + val result = PlainStyle.colored("Hello", PlainStyle.red) + + assertThat(result).isEqualTo("Hello") + } + + @Test + fun AnsiStyleのsuccessメソッドが緑色で返すこと() { + val result = AnsiStyle.success("OK") + + assertThat(result).isEqualTo("\u001B[32mOK\u001B[0m") + } + + @Test + fun AnsiStyleのerrorメソッドが赤色で返すこと() { + val result = AnsiStyle.error("Error") + + assertThat(result).isEqualTo("\u001B[31mError\u001B[0m") + } + + @Test + fun AnsiStyleのwarningメソッドが黄色で返すこと() { + val result = AnsiStyle.warning("Warning") + + assertThat(result).isEqualTo("\u001B[33mWarning\u001B[0m") + } + + @Test + fun AnsiStyleのinfoメソッドがシアンで返すこと() { + val result = AnsiStyle.info("Info") + + assertThat(result).isEqualTo("\u001B[36mInfo\u001B[0m") + } + + @Test + fun PlainStyleの便利メソッドが全て色なしで返すこと() { + assertThat(PlainStyle.success("OK")).isEqualTo("OK") + assertThat(PlainStyle.error("Error")).isEqualTo("Error") + assertThat(PlainStyle.warning("Warning")).isEqualTo("Warning") + assertThat(PlainStyle.info("Info")).isEqualTo("Info") + } +} From 03b28a35d6d6b30563bd9d124ff47b24900829cc Mon Sep 17 00:00:00 2001 From: "ryosuke.a.tanaka" Date: Tue, 3 Feb 2026 17:16:56 +0900 Subject: [PATCH 6/6] =?UTF-8?q?feat:=20echo=E3=82=B3=E3=83=9E=E3=83=B3?= =?UTF-8?q?=E3=83=89=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 引数をそのまま出力するechoコマンドを実装 BufferedOutputを使用した内部連携のデモとして 標準出力を汚さずに結果を取得できることを検証 --- .../core/packetproxy/gulp/CLIModeHandler.kt | 5 + .../packetproxy/gulp/command/EchoCommand.kt | 35 +++++ .../gulp/command/EchoCommandTest.kt | 136 ++++++++++++++++++ 3 files changed, 176 insertions(+) create mode 100644 src/main/kotlin/core/packetproxy/gulp/command/EchoCommand.kt create mode 100644 src/test/kotlin/packetproxy/gulp/command/EchoCommandTest.kt diff --git a/src/main/kotlin/core/packetproxy/gulp/CLIModeHandler.kt b/src/main/kotlin/core/packetproxy/gulp/CLIModeHandler.kt index 910fec5d..71f964a8 100644 --- a/src/main/kotlin/core/packetproxy/gulp/CLIModeHandler.kt +++ b/src/main/kotlin/core/packetproxy/gulp/CLIModeHandler.kt @@ -15,6 +15,7 @@ */ package packetproxy.cli +import core.packetproxy.gulp.command.EchoCommand import core.packetproxy.gulp.command.LogCommand import core.packetproxy.gulp.command.SourceCommand import org.jline.builtins.Completers.TreeCompleter @@ -33,6 +34,7 @@ abstract class CLIModeHandler { node("switch"), node("decode"), node("encode"), + node("echo"), node("log"), node("source"), node("help"), @@ -62,6 +64,8 @@ abstract class CLIModeHandler { "l", "log" -> LogCommand(parsed, ctx) + "echo" -> EchoCommand(parsed, ctx) + else -> extensionCommand(parsed, ctx) } } @@ -78,6 +82,7 @@ abstract class CLIModeHandler { return """共通コマンド: exit - 終了 help - ヘルプ + echo - 引数を出力 l, log - ログ継続出力 s, switch - Mode切り替え diff --git a/src/main/kotlin/core/packetproxy/gulp/command/EchoCommand.kt b/src/main/kotlin/core/packetproxy/gulp/command/EchoCommand.kt new file mode 100644 index 00000000..60603611 --- /dev/null +++ b/src/main/kotlin/core/packetproxy/gulp/command/EchoCommand.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2025 DeNA Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package core.packetproxy.gulp.command + +import packetproxy.gulp.CommandContext +import packetproxy.gulp.ParsedCommand + +/** + * echoコマンド: 引数をそのまま出力する + * + * 使用例: + * - echo Hello World -> "Hello World" + * - echo foo bar baz -> "foo bar baz" + * + * 内部連携の際はBufferedOutputを使用することで、 標準出力を汚さずに結果を取得できる。 + */ +object EchoCommand : Command { + override suspend fun invoke(parsed: ParsedCommand, ctx: CommandContext) { + val message = parsed.args.joinToString(" ") + ctx.println(message) + } +} diff --git a/src/test/kotlin/packetproxy/gulp/command/EchoCommandTest.kt b/src/test/kotlin/packetproxy/gulp/command/EchoCommandTest.kt new file mode 100644 index 00000000..196e6401 --- /dev/null +++ b/src/test/kotlin/packetproxy/gulp/command/EchoCommandTest.kt @@ -0,0 +1,136 @@ +/* + * Copyright 2025 DeNA Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package packetproxy.gulp.command + +import core.packetproxy.gulp.command.EchoCommand +import kotlinx.coroutines.runBlocking +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import packetproxy.gulp.CommandContext +import packetproxy.gulp.CommandParser +import packetproxy.gulp.output.BufferedOutput + +/** + * EchoCommandのテスト + * + * 内部連携のユースケースを検証: BufferedOutputを使用することで標準出力を汚さずに結果を取得できる + */ +class EchoCommandTest { + private lateinit var output: BufferedOutput + private lateinit var ctx: CommandContext + + @BeforeEach + fun setUp() { + output = BufferedOutput() + ctx = CommandContext(output) + } + + @Test + fun 単一の引数を出力できること() = runBlocking { + val parsed = CommandParser.parse("echo Hello")!! + + EchoCommand(parsed, ctx) + + assertThat(output.getOutput()).isEqualTo("Hello\n") + } + + @Test + fun 複数の引数をスペース区切りで出力できること() = runBlocking { + val parsed = CommandParser.parse("echo Hello World")!! + + EchoCommand(parsed, ctx) + + assertThat(output.getOutput()).isEqualTo("Hello World\n") + } + + @Test + fun 引数なしの場合は空行が出力されること() = runBlocking { + val parsed = CommandParser.parse("echo")!! + + EchoCommand(parsed, ctx) + + assertThat(output.getOutput()).isEqualTo("\n") + } + + @Test + fun 多数の引数を連結して出力できること() = runBlocking { + val parsed = CommandParser.parse("echo foo bar baz qux")!! + + EchoCommand(parsed, ctx) + + assertThat(output.getOutput()).isEqualTo("foo bar baz qux\n") + } + + @Test + fun 複数回のコマンド実行結果を蓄積できること() = runBlocking { + val parsed1 = CommandParser.parse("echo first")!! + val parsed2 = CommandParser.parse("echo second")!! + val parsed3 = CommandParser.parse("echo third")!! + + EchoCommand(parsed1, ctx) + EchoCommand(parsed2, ctx) + EchoCommand(parsed3, ctx) + + assertThat(output.getOutput()).isEqualTo("first\nsecond\nthird\n") + } + + @Test + fun clearで出力をリセットしてから再度取得できること() = runBlocking { + val parsed1 = CommandParser.parse("echo before clear")!! + EchoCommand(parsed1, ctx) + + output.clear() + + val parsed2 = CommandParser.parse("echo after clear")!! + EchoCommand(parsed2, ctx) + + assertThat(output.getOutput()).isEqualTo("after clear\n") + } + + @Test + fun 出力にANSIエスケープシーケンスが含まれないこと() = runBlocking { + val parsed = CommandParser.parse("echo test message")!! + + EchoCommand(parsed, ctx) + + val result = output.getOutput() + // BufferedOutputはPlainStyleを使用するため、ANSIコードは含まれない + assertThat(result).doesNotContain("\u001B[") + } + + // 内部連携: MCPサーバとの連携など + @Test + fun 内部連携で標準出力を汚さずに結果を取得できること() = runBlocking { + // BufferedOutputを使用した内部連携のデモ + val internalOutput = BufferedOutput() + val internalCtx = CommandContext(internalOutput) + + // コマンドを実行 + val parsed = CommandParser.parse("echo internal message")!! + EchoCommand(parsed, internalCtx) + + // 結果をKotlinコードで取得(標準出力には出力されない) + val result = internalOutput.getOutput().trim() + + // 取得した結果を検証 + assertThat(result).isEqualTo("internal message") + + // 結果を別の処理に渡す例 + val processedResult = result.uppercase() + assertThat(processedResult).isEqualTo("INTERNAL MESSAGE") + } +}