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); + } +}