From c4bf2a2c9e63db10b748ac10be0b4bf5c44389c4 Mon Sep 17 00:00:00 2001 From: Vladimir Sitnikov Date: Tue, 17 Feb 2026 12:21:09 +0300 Subject: [PATCH] feat: allow custom file descriptors for propagating output from the JVM Previously all the output was printed to stdout which made it hard to intercept just the needed bits (e.g. threaddump). See https://github.com/jattach/jattach/issues/88 --- .github/workflows/test.yml | 52 +++++++++++++++++++ jattach.go | 40 ++++++--------- jattach_posix_impl.go | 4 +- jattach_test.go | 94 +++++++++++++++++++++++++--------- jattach_windows_impl.go | 4 +- src/posix/jattach.c | 18 ++++--- src/posix/jattach_hotspot.c | 33 ++++++------ src/posix/jattach_openj9.c | 56 ++++++++++---------- src/posix/jattach_posix.go | 12 ++--- src/windows/jattach.c | 65 +++++++++++++---------- src/windows/jattach_windows.go | 26 +++++++--- testdata/SleepLoop.java | 6 +++ 12 files changed, 263 insertions(+), 147 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 testdata/SleepLoop.java diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..75b0f4d --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,52 @@ +name: Test + +on: + push: + branches: + - master + - 'rel/**' + pull_request: + branches: + - master + - 'rel/**' + +jobs: + test: + name: Test ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ ubuntu-latest, macos-latest, windows-latest ] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout code + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + + - name: Setup Go + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 + with: + go-version-file: go.mod + + # Build test binary once, reuse across Java versions + - name: Build test binary + run: go test -c -o jattach-test${{ runner.os == 'Windows' && '.exe' || '' }} + + - name: Setup Java 17 + uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4.8.0 + with: + distribution: temurin + java-version: '17' + + - name: Test with Java 17 + # -count=1 disables test caching to ensure tests actually run with each Java version + run: go test -v -count=1 + + - name: Setup Java 21 + uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4.8.0 + with: + distribution: temurin + java-version: '21' + + - name: Test with Java 21 + run: go test -v -count=1 + diff --git a/jattach.go b/jattach.go index 09c7175..59225d7 100644 --- a/jattach.go +++ b/jattach.go @@ -34,6 +34,7 @@ import ( "fmt" "io" "os" + "sync" ) // Command represents a JVM attach command type. @@ -73,52 +74,43 @@ const ( ) // Attach sends a command to a JVM process. -// The command output is printed to stdout. +// The command output is printed to stdout, errors to stderr. // Returns the exit code from the JVM command. func Attach(pid int, cmd Command, args ...string) (int, error) { cmdArgs := append([]string{string(cmd)}, args...) - return callJattach(pid, cmdArgs, true) + return callJattach(pid, cmdArgs, int(os.Stdout.Fd()), int(os.Stderr.Fd())) } // AttachWithOutput sends a command to a JVM process and captures the output. -// Unlike Attach, this function captures stdout instead of printing it. +// Unlike Attach, this function captures both stdout and stderr from the C code +// via a pipe passed directly as file descriptors. // Returns the captured output, exit code, and any error. func AttachWithOutput(pid int, cmd Command, args ...string) (string, int, error) { - // Create a pipe to capture stdout r, w, err := os.Pipe() if err != nil { return "", 1, fmt.Errorf("failed to create pipe: %w", err) } - // Save original stdout and restore it when done - oldStdout := os.Stdout - defer func() { - os.Stdout = oldStdout - }() - - // Redirect stdout to our pipe - os.Stdout = w - - // Capture output in a goroutine - outputChan := make(chan string, 1) + // Read from pipe in a goroutine + var buf bytes.Buffer + var wg sync.WaitGroup + wg.Add(1) go func() { - var buf bytes.Buffer + defer wg.Done() io.Copy(&buf, r) - outputChan <- buf.String() }() - // Execute the command + // Execute the command with pipe write-end as both out_fd and err_fd cmdArgs := append([]string{string(cmd)}, args...) - exitCode, err := callJattach(pid, cmdArgs, true) + writeFd := int(w.Fd()) + exitCode, callErr := callJattach(pid, cmdArgs, writeFd, writeFd) - // Close the write end of the pipe + // Close the write end so the reader goroutine finishes w.Close() - - // Read the captured output - output := <-outputChan + wg.Wait() r.Close() - return output, exitCode, err + return buf.String(), exitCode, callErr } // GetThreadDump retrieves a thread dump from the target JVM. diff --git a/jattach_posix_impl.go b/jattach_posix_impl.go index ffa5470..fdeb791 100644 --- a/jattach_posix_impl.go +++ b/jattach_posix_impl.go @@ -7,6 +7,6 @@ package jattach import "github.com/vlsi/jattach/v2/src/posix" // callJattach delegates to the POSIX-specific implementation -func callJattach(pid int, args []string, printOutput bool) (int, error) { - return posix.CallJattach(pid, args, printOutput) +func callJattach(pid int, args []string, outFd int, errFd int) (int, error) { + return posix.CallJattach(pid, args, outFd, errFd) } diff --git a/jattach_test.go b/jattach_test.go index 556de32..cd0b09c 100644 --- a/jattach_test.go +++ b/jattach_test.go @@ -5,7 +5,12 @@ package jattach import ( + "bufio" + "os" + "os/exec" + "strings" "testing" + "time" ) func TestCommandConstants(t *testing.T) { @@ -36,12 +41,12 @@ func TestCommandConstants(t *testing.T) { func TestAttach_InvalidPID(t *testing.T) { // Test with invalid PID - _, err := callJattach(0, []string{"properties"}, true) + _, err := callJattach(0, []string{"properties"}, 1, 2) if err == nil { t.Error("Expected error for invalid PID, got nil") } - _, err = callJattach(-1, []string{"properties"}, true) + _, err = callJattach(-1, []string{"properties"}, 1, 2) if err == nil { t.Error("Expected error for negative PID, got nil") } @@ -49,34 +54,77 @@ func TestAttach_InvalidPID(t *testing.T) { func TestAttach_NoCommand(t *testing.T) { // Test with no command - _, err := callJattach(1, []string{}, true) + _, err := callJattach(1, []string{}, 1, 2) if err == nil { t.Error("Expected error for empty command, got nil") } } -// Integration tests require a running JVM process -// Run with: go test -tags=integration -// These tests are skipped by default -func TestIntegration_Attach(t *testing.T) { +func TestIntegration_ThreadDump(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } - // Note: You need to set JATTACH_TEST_PID environment variable - // to the PID of a running JVM process for these tests to work - t.Skip("Integration tests require a running JVM - set JATTACH_TEST_PID environment variable") - - // Example of how integration tests would work: - // pid := getTestJVMPID(t) - // - // t.Run("Properties", func(t *testing.T) { - // output, err := GetSystemProperties(pid) - // if err != nil { - // t.Fatalf("Failed to get properties: %v", err) - // } - // if len(output) == 0 { - // t.Error("Expected non-empty properties output") - // } - // }) + javaPath, err := exec.LookPath("java") + if err != nil { + t.Skip("java not found in PATH") + } + + javacPath, err := exec.LookPath("javac") + if err != nil { + t.Skip("javac not found in PATH") + } + + // Compile the test fixture + compileCmd := exec.Command(javacPath, "-d", "testdata", "testdata/SleepLoop.java") + compileCmd.Dir = "." + if out, err := compileCmd.CombinedOutput(); err != nil { + t.Fatalf("Failed to compile SleepLoop.java: %v\n%s", err, out) + } + t.Cleanup(func() { + os.Remove("testdata/SleepLoop.class") + }) + + // Launch the Java process + cmd := exec.Command(javaPath, "-cp", "testdata", "SleepLoop") + stdout, err := cmd.StdoutPipe() + if err != nil { + t.Fatalf("Failed to create stdout pipe: %v", err) + } + if err := cmd.Start(); err != nil { + t.Fatalf("Failed to start java process: %v", err) + } + t.Cleanup(func() { + cmd.Process.Kill() + cmd.Wait() + }) + + // Wait for "READY" signal with timeout + ready := make(chan struct{}) + go func() { + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + if strings.TrimSpace(scanner.Text()) == "READY" { + close(ready) + return + } + } + }() + + select { + case <-ready: + case <-time.After(30 * time.Second): + t.Fatal("Timed out waiting for Java process to start") + } + + // Take thread dump + output, err := GetThreadDump(cmd.Process.Pid) + if err != nil { + t.Fatalf("GetThreadDump failed: %v", err) + } + + lower := strings.ToLower(output) + if !strings.Contains(lower, "sleeploop") && !strings.Contains(lower, "sleep") { + t.Errorf("Thread dump does not mention SleepLoop or sleep.\nOutput:\n%s", output) + } } diff --git a/jattach_windows_impl.go b/jattach_windows_impl.go index 89d96d0..f4d7073 100644 --- a/jattach_windows_impl.go +++ b/jattach_windows_impl.go @@ -7,6 +7,6 @@ package jattach import "github.com/vlsi/jattach/v2/src/windows" // callJattach delegates to the Windows-specific implementation -func callJattach(pid int, args []string, printOutput bool) (int, error) { - return windows.CallJattach(pid, args, printOutput) +func callJattach(pid int, args []string, outFd int, errFd int) (int, error) { + return windows.CallJattach(pid, args, outFd, errFd) } diff --git a/src/posix/jattach.c b/src/posix/jattach.c index a8a851e..7ef206d 100644 --- a/src/posix/jattach.c +++ b/src/posix/jattach.c @@ -14,29 +14,31 @@ * limitations under the License. */ +#include #include #include +#include #include #include #include "psutil.h" extern int is_openj9_process(int pid); -extern int jattach_openj9(int pid, int nspid, int argc, char** argv, int print_output); -extern int jattach_hotspot(int pid, int nspid, int argc, char** argv, int print_output); +extern int jattach_openj9(int pid, int nspid, int argc, char** argv, int out_fd, int err_fd); +extern int jattach_hotspot(int pid, int nspid, int argc, char** argv, int out_fd, int err_fd); int mnt_changed = 0; __attribute__((visibility("default"))) -int jattach(int pid, int argc, char** argv, int print_output) { +int jattach(int pid, int argc, char** argv, int out_fd, int err_fd) { uid_t my_uid = geteuid(); gid_t my_gid = getegid(); uid_t target_uid = my_uid; gid_t target_gid = my_gid; int nspid; if (get_process_info(pid, &target_uid, &target_gid, &nspid) < 0) { - fprintf(stderr, "Process %d not found\n", pid); + dprintf(err_fd, "Process %d not found\n", pid); return 1; } @@ -50,7 +52,7 @@ int jattach(int pid, int argc, char** argv, int print_output) { // If we are running under root, switch to the required euid/egid automatically. if ((my_gid != target_gid && setegid(target_gid) != 0) || (my_uid != target_uid && seteuid(target_uid) != 0)) { - perror("Failed to change credentials to match the target process"); + dprintf(err_fd, "Failed to change credentials to match the target process: %s\n", strerror(errno)); return 1; } @@ -60,9 +62,9 @@ int jattach(int pid, int argc, char** argv, int print_output) { signal(SIGPIPE, SIG_IGN); if (is_openj9_process(nspid)) { - return jattach_openj9(pid, nspid, argc, argv, print_output); + return jattach_openj9(pid, nspid, argc, argv, out_fd, err_fd); } else { - return jattach_hotspot(pid, nspid, argc, argv, print_output); + return jattach_hotspot(pid, nspid, argc, argv, out_fd, err_fd); } } @@ -87,7 +89,7 @@ int main(int argc, char** argv) { return 1; } - return jattach(pid, argc - 2, argv + 2, 1); + return jattach(pid, argc - 2, argv + 2, STDOUT_FILENO, STDERR_FILENO); } #endif // JATTACH_VERSION diff --git a/src/posix/jattach_hotspot.c b/src/posix/jattach_hotspot.c index 68d8805..0ec39f9 100644 --- a/src/posix/jattach_hotspot.c +++ b/src/posix/jattach_hotspot.c @@ -14,6 +14,7 @@ * limitations under the License. */ +#include #include #include #include @@ -133,15 +134,15 @@ static int write_command(int fd, int argc, char** argv) { return 0; } -// Mirror response from remote JVM to stdout -static int read_response(int fd, int argc, char** argv, int print_output) { +// Mirror response from remote JVM to out_fd +static int read_response(int fd, int argc, char** argv, int out_fd, int err_fd) { char buf[8192]; ssize_t bytes = read(fd, buf, sizeof(buf) - 1); if (bytes == 0) { - fprintf(stderr, "Unexpected EOF reading response\n"); + dprintf(err_fd, "Unexpected EOF reading response\n"); return 1; } else if (bytes < 0) { - perror("Error reading response"); + dprintf(err_fd, "Error reading response: %s\n", strerror(errno)); return 1; } @@ -162,42 +163,42 @@ static int read_response(int fd, int argc, char** argv, int print_output) { result = atoi(strncmp(buf + 2, "return code: ", 13) == 0 ? buf + 15 : buf + 2); } - if (print_output) { - // Mirror JVM response to stdout - printf("JVM response code = "); + if (out_fd >= 0) { + // Mirror JVM response to out_fd + dprintf(out_fd, "JVM response code = "); do { - fwrite(buf, 1, bytes, stdout); + write(out_fd, buf, bytes); bytes = read(fd, buf, sizeof(buf)); } while (bytes > 0); - printf("\n"); + write(out_fd, "\n", 1); } return result; } -int jattach_hotspot(int pid, int nspid, int argc, char** argv, int print_output) { +int jattach_hotspot(int pid, int nspid, int argc, char** argv, int out_fd, int err_fd) { if (check_socket(nspid) != 0 && start_attach_mechanism(pid, nspid) != 0) { - perror("Could not start attach mechanism"); + dprintf(err_fd, "Could not start attach mechanism: %s\n", strerror(errno)); return 1; } int fd = connect_socket(nspid); if (fd == -1) { - perror("Could not connect to socket"); + dprintf(err_fd, "Could not connect to socket: %s\n", strerror(errno)); return 1; } - if (print_output) { - printf("Connected to remote JVM\n"); + if (out_fd >= 0) { + dprintf(out_fd, "Connected to remote JVM\n"); } if (write_command(fd, argc, argv) != 0) { - perror("Error writing to socket"); + dprintf(err_fd, "Error writing to socket: %s\n", strerror(errno)); close(fd); return 1; } - int result = read_response(fd, argc, argv, print_output); + int result = read_response(fd, argc, argv, out_fd, err_fd); close(fd); return result; diff --git a/src/posix/jattach_openj9.c b/src/posix/jattach_openj9.c index 90683c5..ae1d71d 100644 --- a/src/posix/jattach_openj9.c +++ b/src/posix/jattach_openj9.c @@ -78,8 +78,8 @@ static void translate_command(char* buf, size_t bufsize, int argc, char** argv) buf[bufsize - 1] = 0; } -// Unescape a string and print it on stdout -static void print_unescaped(char* str) { +// Unescape a string and print it to out_fd +static void print_unescaped(char* str, int out_fd) { char* p = strchr(str, '\n'); if (p != NULL) { *p = 0; @@ -104,12 +104,12 @@ static void print_unescaped(char* str) { default: *p = p[1]; } - fwrite(str, 1, p - str + 1, stdout); + write(out_fd, str, p - str + 1); str = p + 2; } - fwrite(str, 1, strlen(str), stdout); - printf("\n"); + write(out_fd, str, strlen(str)); + write(out_fd, "\n", 1); } // Send command with arguments to socket @@ -126,8 +126,8 @@ static int write_command(int fd, const char* cmd) { return 0; } -// Mirror response from remote JVM to stdout -static int read_response(int fd, const char* cmd, int print_output) { +// Mirror response from remote JVM to out_fd +static int read_response(int fd, const char* cmd, int out_fd, int err_fd) { size_t size = 8192; char* buf = malloc(size); @@ -135,10 +135,10 @@ static int read_response(int fd, const char* cmd, int print_output) { while (buf != NULL) { ssize_t bytes = read(fd, buf + off, size - off); if (bytes == 0) { - fprintf(stderr, "Unexpected EOF reading response\n"); + dprintf(err_fd, "Unexpected EOF reading response\n"); return 1; } else if (bytes < 0) { - perror("Error reading response"); + dprintf(err_fd, "Error reading response: %s\n", strerror(errno)); return 1; } @@ -153,7 +153,7 @@ static int read_response(int fd, const char* cmd, int print_output) { } if (buf == NULL) { - fprintf(stderr, "Failed to allocate memory for response\n"); + dprintf(err_fd, "Failed to allocate memory for response\n"); return 1; } @@ -164,19 +164,19 @@ static int read_response(int fd, const char* cmd, int print_output) { // AgentOnLoad error code comes right after AgentInitializationException result = strncmp(buf, "ATTACH_ERR AgentInitializationException", 39) == 0 ? atoi(buf + 39) : -1; } - } else if (strncmp(cmd, "ATTACH_DIAGNOSTICS:", 19) == 0 && print_output) { + } else if (strncmp(cmd, "ATTACH_DIAGNOSTICS:", 19) == 0 && out_fd >= 0) { char* p = strstr(buf, "openj9_diagnostics.string_result="); if (p != NULL) { // The result of a diagnostic command is encoded in Java Properties format - print_unescaped(p + 33); + print_unescaped(p + 33, out_fd); free(buf); return result; } } - if (print_output) { + if (out_fd >= 0) { buf[off - 1] = '\n'; - fwrite(buf, 1, off, stdout); + write(out_fd, buf, off); } free(buf); @@ -306,13 +306,13 @@ static int notify_semaphore(int value, int notif_count) { return 0; } -static int accept_client(int s, unsigned long long key) { +static int accept_client(int s, unsigned long long key, int err_fd) { struct timeval tv = {5, 0}; setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); int client = accept(s, NULL, NULL); if (client < 0) { - perror("JVM did not respond"); + dprintf(err_fd, "JVM did not respond: %s\n", strerror(errno)); return -1; } @@ -321,7 +321,7 @@ static int accept_client(int s, unsigned long long key) { while (off < sizeof(buf)) { ssize_t bytes = recv(client, buf + off, sizeof(buf) - off, 0); if (bytes <= 0) { - fprintf(stderr, "The JVM connection was prematurely closed\n"); + dprintf(err_fd, "The JVM connection was prematurely closed\n"); close(client); return -1; } @@ -331,7 +331,7 @@ static int accept_client(int s, unsigned long long key) { char expected[35]; snprintf(expected, sizeof(expected), "ATTACH_CONNECTED %016llx ", key); if (memcmp(buf, expected, sizeof(expected) - 1) != 0) { - fprintf(stderr, "Unexpected JVM response\n"); + dprintf(err_fd, "Unexpected JVM response\n"); close(client); return -1; } @@ -381,10 +381,10 @@ int is_openj9_process(int pid) { return stat(path, &stats) == 0; } -int jattach_openj9(int pid, int nspid, int argc, char** argv, int print_output) { +int jattach_openj9(int pid, int nspid, int argc, char** argv, int out_fd, int err_fd) { int attach_lock = acquire_lock("", "_attachlock"); if (attach_lock < 0) { - perror("Could not acquire attach lock"); + dprintf(err_fd, "Could not acquire attach lock: %s\n", strerror(errno)); return 1; } @@ -392,23 +392,23 @@ int jattach_openj9(int pid, int nspid, int argc, char** argv, int print_output) int port; int s = create_attach_socket(&port); if (s < 0) { - perror("Failed to listen to attach socket"); + dprintf(err_fd, "Failed to listen to attach socket: %s\n", strerror(errno)); goto error; } unsigned long long key = random_key(); if (write_reply_info(nspid, port, key) != 0) { - perror("Could not write replyInfo"); + dprintf(err_fd, "Could not write replyInfo: %s\n", strerror(errno)); goto error; } notif_count = lock_notification_files(); if (notify_semaphore(1, notif_count) != 0) { - perror("Could not notify semaphore"); + dprintf(err_fd, "Could not notify semaphore: %s\n", strerror(errno)); goto error; } - int fd = accept_client(s, key); + int fd = accept_client(s, key, err_fd); if (fd < 0) { // The error message has been already printed goto error; @@ -419,20 +419,20 @@ int jattach_openj9(int pid, int nspid, int argc, char** argv, int print_output) notify_semaphore(-1, notif_count); release_lock(attach_lock); - if (print_output) { - printf("Connected to remote JVM\n"); + if (out_fd >= 0) { + dprintf(out_fd, "Connected to remote JVM\n"); } char cmd[8192]; translate_command(cmd, sizeof(cmd), argc, argv); if (write_command(fd, cmd) != 0) { - perror("Error writing to socket"); + dprintf(err_fd, "Error writing to socket: %s\n", strerror(errno)); close(fd); return 1; } - int result = read_response(fd, cmd, print_output); + int result = read_response(fd, cmd, out_fd, err_fd); if (result != 1) { detach(fd); } diff --git a/src/posix/jattach_posix.go b/src/posix/jattach_posix.go index bb4cd75..1ae8e5b 100644 --- a/src/posix/jattach_posix.go +++ b/src/posix/jattach_posix.go @@ -12,7 +12,7 @@ package posix #include "psutil.h" // Forward declaration of the jattach function -extern int jattach(int pid, int argc, char** argv, int print_output); +extern int jattach(int pid, int argc, char** argv, int out_fd, int err_fd); */ import "C" import ( @@ -23,7 +23,7 @@ import ( // CallJattach is the low-level CGo wrapper for the jattach C function. // It handles C string conversion and memory management. // Returns the exit code from the jattach function. -func CallJattach(pid int, args []string, printOutput bool) (int, error) { +func CallJattach(pid int, args []string, outFd int, errFd int) (int, error) { if pid <= 0 { return 1, fmt.Errorf("invalid PID: %d", pid) } @@ -41,14 +41,8 @@ func CallJattach(pid int, args []string, printOutput bool) (int, error) { defer C.free(unsafe.Pointer(argv[i])) } - // Determine print_output flag - printOutputInt := C.int(0) - if printOutput { - printOutputInt = C.int(1) - } - // Call the C function - ret := C.jattach(C.int(pid), argc, &argv[0], printOutputInt) + ret := C.jattach(C.int(pid), argc, &argv[0], C.int(outFd), C.int(errFd)) return int(ret), nil } diff --git a/src/windows/jattach.c b/src/windows/jattach.c index f25f1d6..a377c1f 100644 --- a/src/windows/jattach.c +++ b/src/windows/jattach.c @@ -14,11 +14,23 @@ * limitations under the License. */ +#include +#include #include #include #include #include +static int dprintf(int fd, const char* fmt, ...) { + char buf[4096]; + va_list ap; + va_start(ap, fmt); + int n = vsnprintf(buf, sizeof(buf), fmt, ap); + va_end(ap); + if (n > 0) _write(fd, buf, n); + return n; +} + typedef HMODULE (WINAPI *GetModuleHandle_t)(LPCTSTR lpModuleName); typedef FARPROC (WINAPI *GetProcAddress_t)(HMODULE hModule, LPCSTR lpProcName); typedef int (__stdcall *JVM_EnqueueOperation_t)(char* cmd, char* arg0, char* arg1, char* arg2, char* pipename); @@ -107,8 +119,8 @@ static LPVOID allocate_data(HANDLE hProcess, char* pipeName, int argc, char** ar return remoteData; } -static void print_error(const char* msg, DWORD code) { - printf("%s (error code = %d)\n", msg, code); +static void print_error(int err_fd, const char* msg, DWORD code) { + dprintf(err_fd, "%s (error code = %d)\n", msg, code); } // If the process is owned by another user, request SeDebugPrivilege to open it. @@ -138,11 +150,11 @@ static int enable_debug_privileges() { } // Fail if attaching 64-bit jattach to 32-bit JVM or vice versa -static int check_bitness(HANDLE hProcess) { +static int check_bitness(HANDLE hProcess, int err_fd) { #ifdef _WIN64 BOOL targetWow64 = FALSE; if (IsWow64Process(hProcess, &targetWow64) && targetWow64) { - printf("Cannot attach 64-bit process to 32-bit JVM\n"); + dprintf(err_fd, "Cannot attach 64-bit process to 32-bit JVM\n"); return 0; } #else @@ -150,7 +162,7 @@ static int check_bitness(HANDLE hProcess) { BOOL targetWow64 = FALSE; if (IsWow64Process(GetCurrentProcess(), &thisWow64) && IsWow64Process(hProcess, &targetWow64)) { if (thisWow64 != targetWow64) { - printf("Cannot attach 32-bit process to 64-bit JVM\n"); + dprintf(err_fd, "Cannot attach 32-bit process to 64-bit JVM\n"); return 0; } } @@ -160,21 +172,21 @@ static int check_bitness(HANDLE hProcess) { // The idea of Dynamic Attach on Windows is to inject a thread into remote JVM // that calls JVM_EnqueueOperation() function exported by HotSpot DLL -static int inject_thread(int pid, char* pipeName, int argc, char** argv) { +static int inject_thread(int pid, char* pipeName, int argc, char** argv, int out_fd, int err_fd) { HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, (DWORD)pid); if (hProcess == NULL && GetLastError() == ERROR_ACCESS_DENIED) { if (!enable_debug_privileges()) { - print_error("Not enough privileges", GetLastError()); + print_error(err_fd, "Not enough privileges", GetLastError()); return 0; } hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, (DWORD)pid); } if (hProcess == NULL) { - print_error("Could not open process", GetLastError()); + print_error(err_fd, "Could not open process", GetLastError()); return 0; } - if (!check_bitness(hProcess)) { + if (!check_bitness(hProcess, err_fd)) { CloseHandle(hProcess); return 0; } @@ -182,7 +194,7 @@ static int inject_thread(int pid, char* pipeName, int argc, char** argv) { LPTHREAD_START_ROUTINE code = allocate_code(hProcess); LPVOID data = code != NULL ? allocate_data(hProcess, pipeName, argc, argv) : NULL; if (data == NULL) { - print_error("Could not allocate memory in target process", GetLastError()); + print_error(err_fd, "Could not allocate memory in target process", GetLastError()); CloseHandle(hProcess); return 0; } @@ -190,15 +202,14 @@ static int inject_thread(int pid, char* pipeName, int argc, char** argv) { int success = 1; HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, code, data, 0, NULL); if (hThread == NULL) { - print_error("Could not create remote thread", GetLastError()); + print_error(err_fd, "Could not create remote thread", GetLastError()); success = 0; } else { - printf("Connected to remote process\n"); WaitForSingleObject(hThread, INFINITE); DWORD exitCode; GetExitCodeThread(hThread, &exitCode); if (exitCode != 0) { - print_error("Attach is not supported by the target process", exitCode); + print_error(err_fd, "Attach is not supported by the target process", exitCode); success = 0; } CloseHandle(hThread); @@ -211,14 +222,14 @@ static int inject_thread(int pid, char* pipeName, int argc, char** argv) { return success; } -// JVM response is read from the pipe and mirrored to stdout -static int read_response(HANDLE hPipe, int print_output) { +// JVM response is read from the pipe and mirrored to out_fd +static int read_response(HANDLE hPipe, int out_fd, int err_fd) { ConnectNamedPipe(hPipe, NULL); char buf[8192]; DWORD bytesRead; if (!ReadFile(hPipe, buf, sizeof(buf) - 1, &bytesRead, NULL)) { - print_error("Error reading response", GetLastError()); + print_error(err_fd, "Error reading response", GetLastError()); return 1; } @@ -226,19 +237,19 @@ static int read_response(HANDLE hPipe, int print_output) { buf[bytesRead] = 0; int result = atoi(buf); - if (print_output) { - // Mirror JVM response to stdout - printf("JVM response code = "); + if (out_fd >= 0) { + // Mirror JVM response to out_fd + dprintf(out_fd, "JVM response code = "); do { - fwrite(buf, 1, bytesRead, stdout); + _write(out_fd, buf, bytesRead); } while (ReadFile(hPipe, buf, sizeof(buf), &bytesRead, NULL)); - printf("\n"); - } + _write(out_fd, "\n", 1); + } return result; } -int jattach(int pid, int argc, char** argv, int print_output) { +int jattach(int pid, int argc, char** argv, int out_fd, int err_fd) { // When attaching as an Administrator, make sure the target process can connect to our pipe, // i.e. allow read-write access to everyone. For the complete format description, see // https://docs.microsoft.com/en-us/windows/win32/secauthz/security-descriptor-string-format @@ -251,19 +262,19 @@ int jattach(int pid, int argc, char** argv, int print_output) { HANDLE hPipe = CreateNamedPipe(pipeName, PIPE_ACCESS_INBOUND, PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, 1, 4096, 8192, NMPWAIT_USE_DEFAULT_WAIT, &sec); if (hPipe == INVALID_HANDLE_VALUE) { - print_error("Could not create pipe", GetLastError()); + print_error(err_fd, "Could not create pipe", GetLastError()); LocalFree(sec.lpSecurityDescriptor); return 1; } LocalFree(sec.lpSecurityDescriptor); - if (!inject_thread(pid, pipeName, argc, argv)) { + if (!inject_thread(pid, pipeName, argc, argv, out_fd, err_fd)) { CloseHandle(hPipe); return 1; } - int result = read_response(hPipe, print_output); + int result = read_response(hPipe, out_fd, err_fd); CloseHandle(hPipe); return result; @@ -290,7 +301,7 @@ int main(int argc, char** argv) { return 1; } - return jattach(pid, argc - 2, argv + 2, 1); + return jattach(pid, argc - 2, argv + 2, _fileno(stdout), _fileno(stderr)); } #endif // JATTACH_VERSION diff --git a/src/windows/jattach_windows.go b/src/windows/jattach_windows.go index bf398dd..aaef1a6 100644 --- a/src/windows/jattach_windows.go +++ b/src/windows/jattach_windows.go @@ -9,9 +9,19 @@ package windows #cgo LDFLAGS: -ladvapi32 #include +#include +#include // Forward declaration of the jattach function -extern int jattach(int pid, int argc, char** argv, int print_output); +extern int jattach(int pid, int argc, char** argv, int out_fd, int err_fd); + +// Convert a Windows HANDLE (from Go's os.File.Fd()) to a C runtime file descriptor. +// Go's os.File.Fd() returns a Windows HANDLE, but jattach's C code uses _write() +// which requires a C runtime file descriptor. +static int handle_to_cfd(intptr_t handle) { + if (handle < 0) return -1; + return _open_osfhandle(handle, _O_WRONLY); +} */ import "C" import ( @@ -22,7 +32,7 @@ import ( // CallJattach is the low-level CGo wrapper for the jattach C function (Windows implementation). // It handles C string conversion and memory management. // Returns the exit code from the jattach function. -func CallJattach(pid int, args []string, printOutput bool) (int, error) { +func CallJattach(pid int, args []string, outFd int, errFd int) (int, error) { if pid <= 0 { return 1, fmt.Errorf("invalid PID: %d", pid) } @@ -40,14 +50,14 @@ func CallJattach(pid int, args []string, printOutput bool) (int, error) { defer C.free(unsafe.Pointer(argv[i])) } - // Determine print_output flag - printOutputInt := C.int(0) - if printOutput { - printOutputInt = C.int(1) - } + // Convert Windows HANDLEs to C runtime file descriptors. + // Go's os.File.Fd() returns Windows HANDLEs, but the C code uses _write() + // which requires C runtime file descriptors. + cOutFd := C.handle_to_cfd(C.intptr_t(outFd)) + cErrFd := C.handle_to_cfd(C.intptr_t(errFd)) // Call the C function - ret := C.jattach(C.int(pid), argc, &argv[0], printOutputInt) + ret := C.jattach(C.int(pid), argc, &argv[0], C.int(cOutFd), C.int(cErrFd)) return int(ret), nil } diff --git a/testdata/SleepLoop.java b/testdata/SleepLoop.java new file mode 100644 index 0000000..2559fb6 --- /dev/null +++ b/testdata/SleepLoop.java @@ -0,0 +1,6 @@ +public class SleepLoop { + public static void main(String[] args) throws Exception { + System.out.println("READY"); + Thread.sleep(60000); + } +}