Skip to content

Add ProcessInfo2 call #7

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Supported platforms:
Implemented commands:
- [x] StopTracing
- [x] CollectTracing
- [x] ProcessInfo2
- [ ] CollectTracing2
- [ ] CreateCoreDump
- [ ] AttachProfiler
Expand Down
59 changes: 57 additions & 2 deletions client.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package dotnetdiag

import (
"bytes"
"encoding/binary"
"fmt"
"io"
"net"

"github.com/pyroscope-io/dotnetdiag/nettrace"
)

// Client implement Diagnostic IPC Protocol client.
Expand Down Expand Up @@ -58,8 +63,8 @@ type CollectTracingConfig struct {
// NewClient creates a new Diagnostic IPC Protocol client for the transport
// specified - on Unix/Linux based platforms, a Unix Domain Socket will be used, and
// on Windows, a Named Pipe will be used:
// - /tmp/dotnet-diagnostic-{%d:PID}-{%llu:disambiguation key}-socket (Linux/MacOS)
// - \\.\pipe\dotnet-diagnostic-{%d:PID} (Windows)
// - /tmp/dotnet-diagnostic-{%d:PID}-{%llu:disambiguation key}-socket (Linux/MacOS)
// - \\.\pipe\dotnet-diagnostic-{%d:PID} (Windows)
//
// Refer to documentation for details:
// https://github.com/dotnet/diagnostics/blob/main/documentation/design-docs/ipc-protocol.md#transport
Expand All @@ -74,6 +79,56 @@ func NewClient(addr string, options ...Option) *Client {
return c
}

func (c *Client) ProcessInfo2() (*ProcessInfo2Response, error) {
conn, err := c.dial(c.addr)
if err != nil {
return nil, err
}
defer conn.Close()

if err = writeMessage(conn, CommandSetProcess, ProcessProcessInfo2, nil); err != nil {
return nil, err
}

var resp ProcessInfo2Response
header, err := readResponseHeader(conn)
if err != nil {
return nil, err
}

if header.CommandSet != CommandSetServer || header.CommandID != 00 {
return nil, fmt.Errorf("unexpected response header: commandSet=%v (expected 0xff) commandID=%v (expected 0x00)", header.CommandSet, header.CommandID)
}
Comment on lines +99 to +101
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great if we could get the error code when CommandID is 0xFF

In readResponse we have quite weak check, and a TODO note – I'd fix this:

func verifyResponseHeader(h Header) error {
	if h.CommandSet != CommandSetServer {
		return  fmt.Errorf("%w: received response with unknown command set %#x", ErrDiagnosticServer, h.CommandSet)
	}
	switch h.CommandID {
	default:
		return fmt.Errorf("%w: received response with unknown command ID %#x", ErrDiagnosticServer, h.CommandID)
	case 0x00:
		return nil
	case 0xFF:
		var er ErrorResponse
		if err := binary.Read(r, binary.LittleEndian, &er); err != nil {
			return err
		}
		return fmt.Errorf("%w: error code %#x", ErrDiagnosticServer, er.Code)
	}
}


buf := bytes.NewBuffer(nil)
if _, err := io.CopyN(buf, conn, int64(header.Size-headerSize)); err != nil {
return nil, err
}

if err := binary.Read(buf, binary.LittleEndian, &resp.ProcessID); err != nil {
return nil, fmt.Errorf("unable to read process ID: %w", err)
}

if err := binary.Read(buf, binary.LittleEndian, &resp.GUID); err != nil {
return nil, fmt.Errorf("unable to read process ID: %w", err)
}

// now parse the strings out
p := &nettrace.Parser{Buffer: buf}
p.UTF16NTS()
resp.CommandLine = p.UTF16NTS()
p.UTF16NTS()
resp.OS = p.UTF16NTS()
p.UTF16NTS()
resp.Arch = p.UTF16NTS()
p.UTF16NTS()
resp.AssemblyName = p.UTF16NTS()
p.UTF16NTS()
resp.RuntimeVersion = p.UTF16NTS()
Comment on lines +103 to +127
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to check the parser error with p.Err() in the end

nit:

It looks like the buffer size is known in advance, probably we should pre-alloc it. Also, for small payloads of known size io.ReadAtLeast might be a better alternative to io.Copy. Provided that we will be calling ProcessInfo2 relatively frequently, this micro-optimisation might make sense. Also, we could parse all fields with parser, like:

	md := Metadata{p: &Parser{Buffer: blob.Payload}}
	md.p.Read(&md.Header.MetaDataID)
	md.Header.ProviderName = md.p.UTF16NTS()
	md.p.Read(&md.Header.EventID)
	md.Header.EventName = md.p.UTF16NTS()
	md.p.Read(&md.Header.Keywords)
	md.p.Read(&md.Header.Version)
	md.p.Read(&md.Header.Level)

There's probably a bug in the UTF16NTS() function, as it should be called once per a string:

  • It looks like the fallback path (decodeUTF16NTS) only works as intended if the very first char is a non-ASCII one
  • the function is quite wasteful: we probably should reuse bytes buffer / use strings.Buidler


return &resp, nil
}

// CollectTracing creates a new EventPipe session stream of NetTrace data.
func (c *Client) CollectTracing(config CollectTracingConfig) (s *Session, err error) {
// Every session has its own IPC connection which cannot be reused for any
Expand Down
39 changes: 36 additions & 3 deletions dotnetdiag.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@ const (
CommandSetServer = 0xFF
)

const (
ProcessProcessInfo uint8 = iota
ProcessResumeRuntime
ProcessProcessEnvironment
_ // 0x03 not used
ProcessProcessInfo2
ProcessEnablePerfMap
ProcessDisablePerfMap
ProcessApplyStartupHook
ProcessProcessInfo3
)

const (
_ = iota
EventPipeStopTracing
Expand Down Expand Up @@ -82,6 +94,16 @@ type StopTracingResponse struct {
SessionID uint64
}

type ProcessInfo2Response struct {
ProcessID uint64
GUID [16]byte
CommandLine string
OS string
Arch string
AssemblyName string
RuntimeVersion string
}

func writeMessage(w io.Writer, commandSet, commandID uint8, payload []byte) error {
bw := bufio.NewWriter(w)
err := binary.Write(bw, binary.LittleEndian, Header{
Expand All @@ -100,14 +122,25 @@ func writeMessage(w io.Writer, commandSet, commandID uint8, payload []byte) erro
return bw.Flush()
}

func readResponse(r io.Reader, v interface{}) error {
func readResponseHeader(r io.Reader) (*Header, error) {
var h Header
if err := binary.Read(r, binary.LittleEndian, &h); err != nil {
return err
return nil, err
}

if h.Magic != magic {
return ErrHeaderMalformed
return nil, ErrHeaderMalformed
}

return &h, nil
}

func readResponse(r io.Reader, v interface{}) error {
h, err := readResponseHeader(r)
if err != nil {
return err
}

if !(h.CommandSet == CommandSetServer && h.CommandID == 0xFF) {
return binary.Read(r, binary.LittleEndian, v)
}
Expand Down