Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -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

40 changes: 16 additions & 24 deletions jattach.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
"fmt"
"io"
"os"
"sync"
)

// Command represents a JVM attach command type.
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions jattach_posix_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
94 changes: 71 additions & 23 deletions jattach_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
package jattach

import (
"bufio"
"os"
"os/exec"
"strings"
"testing"
"time"
)

func TestCommandConstants(t *testing.T) {
Expand Down Expand Up @@ -36,47 +41,90 @@ 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")
}
}

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)
}
}
4 changes: 2 additions & 2 deletions jattach_windows_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
18 changes: 10 additions & 8 deletions src/posix/jattach.c
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,31 @@
* limitations under the License.
*/

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#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;
}

Expand All @@ -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;
}

Expand All @@ -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);
}
}

Expand All @@ -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
Loading
Loading