From 07b310ff3876564b19128bc0a5ae889c2ff57d27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Roland?= Date: Wed, 4 Dec 2024 22:27:44 +0100 Subject: [PATCH] fix: terminal corruption when running in iEX (#91) --- .gitignore | 1 + lib/nodejs/worker.ex | 41 ++++++++++++++++++++++++++-------------- test/js/terminal-test.js | 33 ++++++++++++++++++++++++++++++++ test/nodejs_test.exs | 20 ++++++++++++++++++++ 4 files changed, 81 insertions(+), 14 deletions(-) create mode 100644 test/js/terminal-test.js diff --git a/.gitignore b/.gitignore index 7cd85c2..a02e490 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ node-*.tar /node_modules/ package-lock.json +app \ No newline at end of file diff --git a/lib/nodejs/worker.ex b/lib/nodejs/worker.ex index 2370f80..14474af 100644 --- a/lib/nodejs/worker.ex +++ b/lib/nodejs/worker.ex @@ -50,20 +50,28 @@ defmodule NodeJS.Worker do port = Port.open( {:spawn_executable, node}, - line: @read_chunk_size, - env: [ - {~c"NODE_PATH", node_path(module_path)}, - {~c"WRITE_CHUNK_SIZE", String.to_charlist("#{@read_chunk_size}")} - ], - args: [node_service_path()] + [ + {:line, @read_chunk_size}, + {:env, get_env_vars(module_path)}, + {:args, [node_service_path()]}, + :exit_status, + :stderr_to_stdout + ] ) {:ok, [node_service_path(), port]} end + defp get_env_vars(module_path) do + [ + {~c"NODE_PATH", node_path(module_path)}, + {~c"WRITE_CHUNK_SIZE", String.to_charlist("#{@read_chunk_size}")} + ] + end + defp get_response(data, timeout) do receive do - {_, {:data, {flag, chunk}}} -> + {_port, {:data, {flag, chunk}}} -> data = data ++ chunk case flag do @@ -72,16 +80,15 @@ defmodule NodeJS.Worker do :eol -> case data do - @prefix ++ protocol_data -> - {:ok, protocol_data} - - _ -> - get_response(~c"", timeout) + @prefix ++ protocol_data -> {:ok, protocol_data} + _ -> get_response(~c"", timeout) end end + + {_port, {:exit_status, status}} when status != 0 -> + {:error, {:exit, status}} after - timeout -> - {:error, :timeout} + timeout -> {:error, :timeout} end end @@ -126,8 +133,14 @@ defmodule NodeJS.Worker do end end + defp reset_terminal(port) do + Port.command(port, "\x1b[0m\x1b[?7h\x1b[?25h\x1b[H\x1b[2J") + Port.command(port, "\x1b[!p\x1b[?47l") + end + @doc false def terminate(_reason, [_, port]) do + reset_terminal(port) send(port, {self(), :close}) end end diff --git a/test/js/terminal-test.js b/test/js/terminal-test.js new file mode 100644 index 0000000..2309494 --- /dev/null +++ b/test/js/terminal-test.js @@ -0,0 +1,33 @@ +// Test various ANSI sequences and terminal control characters +module.exports = { + outputWithANSI: () => { + // Color and formatting + process.stdout.write('\u001b[31mred text\u001b[0m\n'); + process.stdout.write('\u001b[1mbold text\u001b[0m\n'); + + // Cursor movement + process.stdout.write('\u001b[2Amove up\n'); + process.stdout.write('\u001b[2Bmove down\n'); + + // Screen control + process.stdout.write('\u001b[2Jclear screen\n'); + process.stdout.write('\u001b[?25linvisible cursor\n'); + + // Return a clean string to verify protocol handling + return "clean output"; + }, + + // Test function that outputs complex ANSI sequences + complexOutput: () => { + // Nested and compound sequences + process.stdout.write('\u001b[1m\u001b[31m\u001b[4mcomplex formatting\u001b[0m\n'); + + // OSC sequences (window title, etc) + process.stdout.write('\u001b]0;Window Title\u0007'); + + // Alternative screen buffer + process.stdout.write('\u001b[?1049h\u001b[Halternate screen\u001b[?1049l'); + + return "complex test passed"; + } +} diff --git a/test/nodejs_test.exs b/test/nodejs_test.exs index cabd3f1..5f5c2e1 100644 --- a/test/nodejs_test.exs +++ b/test/nodejs_test.exs @@ -256,4 +256,24 @@ defmodule NodeJS.Test do assert js_error_message(msg) =~ "ReferenceError: require is not defined in ES module scope" end end + + describe "terminal handling" do + test "handles ANSI sequences without corrupting protocol" do + # Test basic ANSI handling - protocol messages should work + assert {:ok, "clean output"} = NodeJS.call({"terminal-test", "outputWithANSI"}) + + # Test complex ANSI sequences - protocol messages should work + assert {:ok, "complex test passed"} = NodeJS.call({"terminal-test", "complexOutput"}) + + # Test multiple processes don't interfere with each other + tasks = for _ <- 1..4 do + Task.async(fn -> + NodeJS.call({"terminal-test", "outputWithANSI"}) + end) + end + + results = Task.await_many(tasks) + assert Enum.all?(results, &match?({:ok, "clean output"}, &1)) + end + end end