From c28100863229bc58443e4d352418b517798cf7fb Mon Sep 17 00:00:00 2001 From: Jihyeon Gim Date: Sun, 30 Aug 2026 00:00:48 +0900 Subject: [PATCH] rdma: add the Go binding for the RC session server Bind the RC session server archive through cgo: Init opens the verbs device with the resource limits, and the RCSvc wrapper carries the admission gate handlers use around every session call (TryEnter/Leave), a service-lifetime context that Close cancels so in-flight handler I/O unblocks during shutdown, and an idempotent Close that marks every session for reaping, waits for admitted calls to drain, and destroys the server. The rest of the surface maps the C ABI one-to-one: prepare, ready (with the transfer outcome returned atomically in the reply), staging borrow/finish, the put-view handoff, session introspection, and cancel. Non-linux or non-cgo builds compile against a stub so the package is portable. Nothing imports the package yet; the gateway integration that links and exercises it follows. Signed-off-by: Jihyeon Gim --- rdma/rcserver/rcserver_linux.go | 564 ++++++++++++++++++++++++++++++++ rdma/rcserver/rcserver_stub.go | 187 +++++++++++ 2 files changed, 751 insertions(+) create mode 100644 rdma/rcserver/rcserver_linux.go create mode 100644 rdma/rcserver/rcserver_stub.go diff --git a/rdma/rcserver/rcserver_linux.go b/rdma/rcserver/rcserver_linux.go new file mode 100644 index 00000000..17a8064f --- /dev/null +++ b/rdma/rcserver/rcserver_linux.go @@ -0,0 +1,564 @@ +// Copyright 2026 Versity Software +// This file is licensed under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT ANY KIND, either express or implied. +// See the License for the specific language governing permissions +// and limitations under the License. + +//go:build linux && amd64 && cgo + +// Package rcserver binds the RC session server (rdma/librcserver.a) +// that implements the hipobj-rc-v2 two-phase transfer protocol. +// +// The gateway owns routing, authentication, and the object backend; +// the C side owns the session state machine, QP/CQ/MR lifetimes, the +// data phase, and session reaping. Every cgo call is synchronous +// from the request goroutine. +package rcserver + +/* +#cgo CFLAGS: -I${SRCDIR}/../../cuwrapper/rc +#cgo LDFLAGS: -L${SRCDIR}/.. -l:librcserver.a -lstdc++ -ldl -lpthread +#include "rc_server_abi.h" +#include +*/ +import "C" + +import ( + "context" + "errors" + "fmt" + "runtime" + "sync" + "sync/atomic" + "unsafe" +) + +// Status codes mirrored from rc_server_abi.h. +const ( + StatusOK = C.RC_OK + StatusErrArg = C.RC_E_ARG + StatusErrState = C.RC_E_STATE + StatusErrSession = C.RC_E_SESSION + StatusErrStale = C.RC_E_STALE + StatusErrNoSession = C.RC_E_NO_SESSION + StatusErrDouble = C.RC_E_DOUBLE + StatusErrWire = C.RC_E_WIRE + StatusErrShort = C.RC_E_SHORT + StatusErrTrunc = C.RC_E_TRUNC + StatusErrLimit = C.RC_E_LIMIT + StatusErrInternal = C.RC_E_INTERNAL +) + +// DeviceOpts configures device selection and resource limits. +type DeviceOpts struct { + // GidHint selects the device whose GID starts with this dotted + // prefix; empty picks the first verbs device. + GidHint string + Port uint8 + GidIdx int + // Global limits. + MaxSessions uint32 + MaxStagingBytes uint64 + MaxQPs uint32 + MaxUserSessions uint32 + MaxUserStagingBytes uint64 + MaxUserQPs uint32 + // Timeouts in milliseconds. + TPrepMs uint64 + TExecMs uint64 + // Concurrency slots (0 picks the C-side default 64/32). + MaxReadySlots uint32 + MaxStageSlots uint32 +} + +// PrincipalID is the SHA-256 digest identifying the requester. +type PrincipalID [32]byte + +// PrepareRequest carries the PREPARE wire parameters. +type PrepareRequest struct { + Principal PrincipalID + Op uint8 // 0 = GET, 1 = PUT + Target string + Offset uint64 + Size uint64 + ClientPsn uint32 + Cookie uint32 + ClientToken string // 88-hex, optional +} + +// PrepareResponse is the PREPARE reply. +type PrepareResponse struct { + SessionID string + ServerQpn uint32 + ServerPsn uint32 + StagingAddr uint64 + StagingRkey uint32 + ReplyToken string +} + +// Handle is an opaque consume-once lease identifier. +type Handle struct { + Epoch uint64 + Nonce uint64 +} + +// StagingLease is a borrowed GET staging buffer. The caller must +// return it with FinishStaging exactly once. +type StagingLease struct { + Buf []byte + Capacity int + Handle Handle +} + +// PutView is a borrowed PUT data view. The caller must return it +// with FinishPut exactly once. +type PutView struct { + Buf []byte + Len int + Handle Handle +} + +// ReadyRequest carries the READY wire parameters. +type ReadyRequest struct { + Principal PrincipalID + SessionID string + Cookie uint32 + ClientQpn uint32 + ClientMrAddr uint64 + ClientMrRkey uint32 +} + +// ReadyResponse is the READY reply. +type ReadyResponse struct { + BytesTransferred uint64 + CookieEcho uint32 + // Outcome mirrors the RC_READY_* enum: returned atomically + // with the transfer result so a concurrent READY cannot + // rewrite it between the transfer and the read. + Outcome int + Etag string + VersionID string +} + +// SessionInfo describes a session for READY re-authorization. +type SessionInfo struct { + Op uint8 // 0 = GET, 1 = PUT + Target string +} + +// RCSvc owns the RC session server. Handlers must call TryEnter +// before any session call and Leave when the request completes +// (defer). Close stops admissions, cancels the service context +// (bound to every handler's backend I/O so in-flight requests +// unblock), waits for every entered call to leave, then tears the +// server down; it is idempotent and safe from any goroutine. +type RCSvc struct { + srv *C.rc_server + closing atomic.Bool + ops atomic.Int64 + once sync.Once + ctx context.Context + cancel context.CancelFunc +} + +// Context returns the service-lifetime context. Handlers bind +// their backend calls to it so Close unblocks in-flight I/O. +func (s *RCSvc) Context() context.Context { + return s.ctx +} + +// Init opens the verbs device and returns a service. +func Init(opts DeviceOpts) (*RCSvc, error) { + copts := C.rc_device_opts{ + port: C.uint8_t(opts.Port), + gid_index: C.int(opts.GidIdx), + max_sessions: C.uint32_t(opts.MaxSessions), + max_user_sessions: C.uint32_t(opts.MaxUserSessions), + max_staging_bytes: C.uint64_t(opts.MaxStagingBytes), + max_user_staging_bytes: C.uint64_t(opts.MaxUserStagingBytes), + max_qps: C.uint32_t(opts.MaxQPs), + max_user_qps: C.uint32_t(opts.MaxUserQPs), + t_prep_ms: C.uint64_t(opts.TPrepMs), + t_exec_ms: C.uint64_t(opts.TExecMs), + max_ready_slots: C.uint32_t(opts.MaxReadySlots), + max_stage_slots: C.uint32_t(opts.MaxStageSlots), + } + var gidHint *C.char + if opts.GidHint != "" { + gidHint = C.CString(opts.GidHint) + defer C.free(unsafe.Pointer(gidHint)) + } + copts.gid_hint = gidHint + + var srv *C.rc_server + if rc := C.rc_server_init(&copts, &srv); rc != C.RC_OK { + return nil, fmt.Errorf("rcserver: init failed: status %d", int(rc)) + } + ctx, cancel := context.WithCancel(context.Background()) + return &RCSvc{srv: srv, ctx: ctx, cancel: cancel}, nil +} + +// TryEnter admits a request into the service. It returns false once +// Close has begun; the caller must not touch the service then. +func (s *RCSvc) TryEnter() bool { + if s.closing.Load() { + return false + } + s.ops.Add(1) + if s.closing.Load() { + s.ops.Add(-1) + return false + } + return true +} + +// Leave releases a TryEnter admission. +func (s *RCSvc) Leave() { + s.ops.Add(-1) +} + +// Close shuts the service down: it stops admissions, marks every +// session for reaping, waits for all in-flight handlers to +// finish, and destroys the server. Safe to call multiple times. +// +// Convergence: every blocking call an RC handler makes after +// TryEnter is bounded - GET staging, PUT commit, and the +// bucket-ACL lookup run under the service context and unblock on +// the cancel below; the fresh IAM revalidation selects on the +// same context and its lookup goroutines are capped (a stalled +// context-less IAM backend can strand at most a fixed number of +// them; further requests fail fast); and the remaining +// authorization helpers (VerifyAccess, CheckObjectAccess) +// consume the fiber request context, which the gateway shuts +// down before RunVersityGW returns - and Close runs after +// RunVersityGW returns in every current caller - so those calls +// are cancelled by the fiber shutdown that precedes Close. +func (s *RCSvc) Close() { + s.once.Do(func() { + s.closing.Store(true) + // Cancel the service context first: handlers bind their + // backend I/O to it, so in-flight GET staging and PUT + // commits unblock instead of stretching the ops wait + // below into an unbounded spin. + s.cancel() + if s.srv != nil { + // Mark every session reap-pending first so the + // destroy path below can actually collect them; + // sessions with drained refs are reaped here. + C.rc_cancel_all(s.srv) + } + for s.ops.Load() > 0 { + runtime.Gosched() + } + if s.srv != nil { + C.rc_server_destroy(s.srv) + s.srv = nil + } + }) +} + +func statusErr(rc C.int, op string) error { + if rc == C.RC_OK { + return nil + } + return fmt.Errorf("rcserver: %s: status %d", op, int(rc)) +} + +// Sentinel errors matched by errors.Is for each ABI status. +var ( + ErrArg = errors.New("rcserver: invalid argument") + ErrState = errors.New("rcserver: wrong session state") + ErrSession = errors.New("rcserver: owner mismatch") + ErrStale = errors.New("rcserver: stale handle or session") + ErrNoSession = errors.New("rcserver: no such session") + ErrDouble = errors.New("rcserver: duplicate borrow") + ErrWire = errors.New("rcserver: verbs transfer failure") + ErrShort = errors.New("rcserver: short transfer") + ErrTrunc = errors.New("rcserver: value too long") + ErrLimit = errors.New("rcserver: resource limit") + ErrInternal = errors.New("rcserver: internal error") +) + +// statusError maps an ABI status to a descriptive error value. +func statusError(rc C.int) error { + switch rc { + case C.RC_OK: + return nil + case C.RC_E_ARG: + return ErrArg + case C.RC_E_STATE: + return ErrState + case C.RC_E_SESSION: + return ErrSession + case C.RC_E_STALE: + return ErrStale + case C.RC_E_NO_SESSION: + return ErrNoSession + case C.RC_E_DOUBLE: + return ErrDouble + case C.RC_E_WIRE: + return ErrWire + case C.RC_E_SHORT: + return ErrShort + case C.RC_E_TRUNC: + return ErrTrunc + case C.RC_E_LIMIT: + return ErrLimit + default: + return ErrInternal + } +} + +func strIn(s string) C.rc_str_in { + if s == "" { + return C.rc_str_in{ptr: nil, len: 0} + } + // The pointer must stay alive across the cgo call; every + // wrapper keeps the string referenced until after the call. + return C.rc_str_in{ + ptr: (*C.char)(unsafe.Pointer(unsafe.StringData(s))), + len: C.uint32_t(len(s)), + } +} + +// cStr reads a fixed C char array of length n into a Go string. +func cStr(p *C.char, n C.uint32_t) string { + if n == 0 { + return "" + } + return C.GoStringN(p, C.int(n)) +} + +// Prepare creates a session (PREPARE). +func (s *RCSvc) Prepare(req PrepareRequest) (*PrepareResponse, error) { + if s.srv == nil { + return nil, errors.New("rcserver: service closed") + } + var principal C.rc_principal_id + copy((*[32]byte)(unsafe.Pointer(&principal.id[0]))[:], req.Principal[:]) + + var token C.rc_str_in + if req.ClientToken != "" { + token = strIn(req.ClientToken) + } + target := strIn(req.Target) + creq := C.rc_prepare_req{ + principal: principal, + op: C.uint8_t(req.Op), + target: target, + offset: C.uint64_t(req.Offset), + size: C.uint64_t(req.Size), + client_psn: C.uint32_t(req.ClientPsn), + cookie: C.uint32_t(req.Cookie), + client_token: token, + } + var cresp C.rc_prepare_resp + runtime.KeepAlive(req.ClientToken) + runtime.KeepAlive(req.Target) + rc := C.rc_prepare(s.srv, &creq, &cresp) + if rc != C.RC_OK { + return nil, statusError(rc) + } + return &PrepareResponse{ + SessionID: cStr(&cresp.session_id[0], cresp.session_len), + ServerQpn: uint32(cresp.server_qpn), + ServerPsn: uint32(cresp.server_psn), + StagingAddr: uint64(cresp.staging_addr), + StagingRkey: uint32(cresp.staging_rkey), + ReplyToken: cStr(&cresp.reply_token[0], cresp.reply_len), + }, nil +} + +// FinishPrepare commits or aborts a PREPARE. +func (s *RCSvc) FinishPrepare(sessionID string, committed bool) error { + if s.srv == nil { + return errors.New("rcserver: service closed") + } + in := strIn(sessionID) + rc := C.rc_finish_prepare(s.srv, in, C.int(btoi(committed))) + runtime.KeepAlive(sessionID) + return statusError(rc) +} + +// BorrowStaging borrows the GET staging buffer. +func (s *RCSvc) BorrowStaging(sessionID string) (*StagingLease, error) { + if s.srv == nil { + return nil, errors.New("rcserver: service closed") + } + in := strIn(sessionID) + var lease C.rc_staging_lease + rc := C.rc_borrow_staging(s.srv, in, &lease) + runtime.KeepAlive(sessionID) + if rc != C.RC_OK { + return nil, statusError(rc) + } + return &StagingLease{ + Buf: unsafe.Slice((*byte)(unsafe.Pointer(lease.buf)), + lease.capacity), + Capacity: int(lease.capacity), + Handle: Handle{ + Epoch: uint64(lease.handle.session_epoch), + Nonce: uint64(lease.handle.nonce), + }, + }, nil +} + +// FinishStaging returns a staging lease (GET). +func (s *RCSvc) FinishStaging(lease StagingLease, ok bool, + written int, etag, versionID string) error { + if s.srv == nil { + return errors.New("rcserver: service closed") + } + h := C.rc_handle{ + session_epoch: C.uint64_t(lease.Handle.Epoch), + nonce: C.uint64_t(lease.Handle.Nonce), + } + clease := C.rc_staging_lease{handle: h} + inEtag := strIn(etag) + inVer := strIn(versionID) + rc := C.rc_finish_staging(s.srv, clease, C.int(btoi(ok)), + C.size_t(written), inEtag, inVer) + runtime.KeepAlive(etag) + runtime.KeepAlive(versionID) + return statusError(rc) +} + +// SessionInfo looks up a session's op and target (READY auth). +func (s *RCSvc) SessionInfo(sessionID string, who PrincipalID) (*SessionInfo, error) { + if s.srv == nil { + return nil, errors.New("rcserver: service closed") + } + var principal C.rc_principal_id + copy((*[32]byte)(unsafe.Pointer(&principal.id[0]))[:], who[:]) + in := strIn(sessionID) + var out C.rc_session_info_resp + rc := C.rc_session_info(s.srv, in, principal, &out) + runtime.KeepAlive(sessionID) + if rc != C.RC_OK { + return nil, statusError(rc) + } + return &SessionInfo{ + Op: uint8(out.op), + Target: cStr(&out.target[0], out.target_len), + }, nil +} + +// ReadyTransfer runs the data phase (READY). +func (s *RCSvc) ReadyTransfer(req ReadyRequest) (*ReadyResponse, error) { + if s.srv == nil { + return nil, errors.New("rcserver: service closed") + } + var principal C.rc_principal_id + copy((*[32]byte)(unsafe.Pointer(&principal.id[0]))[:], req.Principal[:]) + in := strIn(req.SessionID) + creq := C.rc_ready_req{ + principal: principal, + session_id: in, + cookie: C.uint32_t(req.Cookie), + client_qpn: C.uint32_t(req.ClientQpn), + client_mr_addr: C.uint64_t(req.ClientMrAddr), + client_mr_rkey: C.uint32_t(req.ClientMrRkey), + } + var cresp C.rc_ready_resp + rc := C.rc_ready_transfer(s.srv, &creq, &cresp) + runtime.KeepAlive(req.SessionID) + if rc != C.RC_OK { + return nil, statusError(rc) + } + return &ReadyResponse{ + BytesTransferred: uint64(cresp.bytes_transferred), + CookieEcho: uint32(cresp.cookie_echo), + Outcome: int(cresp.outcome), + Etag: cStr(&cresp.etag[0], cresp.etag_len), + VersionID: cStr(&cresp.version_id[0], cresp.version_len), + }, nil +} + +// Ready outcome detail codes (rc_ready_resp.outcome). +const ( + ReadyOK = C.RC_READY_OK + ReadyBusy = C.RC_READY_BUSY + ReadyTimeout = C.RC_READY_TIMEOUT + ReadyVerifyFail = C.RC_READY_VERIFY_FAIL + ReadyWireFail = C.RC_READY_WIRE_FAIL +) + +// GetPutData borrows the PUT data view. +func (s *RCSvc) GetPutData(sessionID string) (*PutView, error) { + if s.srv == nil { + return nil, errors.New("rcserver: service closed") + } + in := strIn(sessionID) + var view C.rc_put_view + rc := C.rc_get_put_data(s.srv, in, &view) + runtime.KeepAlive(sessionID) + if rc != C.RC_OK { + return nil, statusError(rc) + } + return &PutView{ + Buf: unsafe.Slice((*byte)(unsafe.Pointer(view.buf)), view.len), + Len: int(view.len), + Handle: Handle{ + Epoch: uint64(view.handle.session_epoch), + Nonce: uint64(view.handle.nonce), + }, + }, nil +} + +// FinishPut returns a PUT data view with the commit outcome. +func (s *RCSvc) FinishPut(view PutView, committed bool, + etag, versionID string) error { + if s.srv == nil { + return errors.New("rcserver: service closed") + } + h := C.rc_handle{ + session_epoch: C.uint64_t(view.Handle.Epoch), + nonce: C.uint64_t(view.Handle.Nonce), + } + cview := C.rc_put_view{handle: h} + inEtag := strIn(etag) + inVer := strIn(versionID) + rc := C.rc_finish_put(s.srv, cview, C.int(btoi(committed)), + inEtag, inVer) + runtime.KeepAlive(etag) + runtime.KeepAlive(versionID) + return statusError(rc) +} + +// FinishFinal marks the FINAL response committed. +func (s *RCSvc) FinishFinal(sessionID string) error { + if s.srv == nil { + return errors.New("rcserver: service closed") + } + in := strIn(sessionID) + rc := C.rc_finish_final(s.srv, in) + runtime.KeepAlive(sessionID) + return statusError(rc) +} + +// Cancel cancels a session (owner-checked). +func (s *RCSvc) Cancel(sessionID string, who PrincipalID) error { + if s.srv == nil { + return errors.New("rcserver: service closed") + } + var principal C.rc_principal_id + copy((*[32]byte)(unsafe.Pointer(&principal.id[0]))[:], who[:]) + in := strIn(sessionID) + rc := C.rc_cancel(s.srv, in, principal) + runtime.KeepAlive(sessionID) + return statusError(rc) +} + +func btoi(b bool) int { + if b { + return 1 + } + return 0 +} diff --git a/rdma/rcserver/rcserver_stub.go b/rdma/rcserver/rcserver_stub.go new file mode 100644 index 00000000..6bb051e8 --- /dev/null +++ b/rdma/rcserver/rcserver_stub.go @@ -0,0 +1,187 @@ +// Copyright 2026 Versity Software +// This file is licensed under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT ANY KIND, either express or implied. +// See the License for the specific language governing permissions +// and limitations under the License. + +//go:build !(linux && amd64 && cgo) + +// Package rcserver binds the RC session server implementing the +// hipobj-rc-v2 two-phase transfer protocol. This file is a stub for +// platforms without RDMA support. +package rcserver + +import "errors" + +var errNotSupported = errors.New("rcserver: not supported on this platform") + +// DeviceOpts configures device selection and resource limits. +type DeviceOpts struct { + GidHint string + Port uint8 + GidIdx int + MaxSessions uint32 + MaxStagingBytes uint64 + MaxQPs uint32 + MaxUserSessions uint32 + MaxUserStagingBytes uint64 + MaxUserQPs uint32 + TPrepMs uint64 + TExecMs uint64 + MaxReadySlots uint32 + MaxStageSlots uint32 +} + +// PrincipalID is the SHA-256 digest identifying the requester. +type PrincipalID [32]byte + +// PrepareRequest carries the PREPARE wire parameters. +type PrepareRequest struct { + Principal PrincipalID + Op uint8 + Target string + Offset uint64 + Size uint64 + ClientPsn uint32 + Cookie uint32 + ClientToken string +} + +// PrepareResponse is the PREPARE reply. +type PrepareResponse struct { + SessionID string + ServerQpn uint32 + ServerPsn uint32 + StagingAddr uint64 + StagingRkey uint32 + ReplyToken string +} + +// Handle is an opaque consume-once lease identifier. +type Handle struct { + Epoch uint64 + Nonce uint64 +} + +// StagingLease is a borrowed GET staging buffer. +type StagingLease struct { + Buf []byte + Capacity int + Handle Handle +} + +// PutView is a borrowed PUT data view. +type PutView struct { + Buf []byte + Len int + Handle Handle +} + +// ReadyRequest carries the READY wire parameters. +type ReadyRequest struct { + Principal PrincipalID + SessionID string + Cookie uint32 + ClientQpn uint32 + ClientMrAddr uint64 + ClientMrRkey uint32 +} + +// ReadyResponse is the READY reply. +type ReadyResponse struct { + BytesTransferred uint64 + CookieEcho uint32 + // Outcome mirrors the RC_READY_* enum. + Outcome int + Etag string + VersionID string +} + +// SessionInfo describes a session for READY re-authorization. +type SessionInfo struct { + Op uint8 + Target string +} + +// RCSvc owns the RC session server (stub). +type RCSvc struct{} + +// Init is a stub that always fails. +func Init(opts DeviceOpts) (*RCSvc, error) { return nil, errNotSupported } + +// TryEnter is a stub. +func (s *RCSvc) TryEnter() bool { return false } + +// Leave is a stub. +func (s *RCSvc) Leave() {} + +// Close is a stub. +func (s *RCSvc) Close() {} + +// Prepare is a stub. +func (s *RCSvc) Prepare(req PrepareRequest) (*PrepareResponse, error) { + return nil, errNotSupported +} + +// FinishPrepare is a stub. +func (s *RCSvc) FinishPrepare(sessionID string, committed bool) error { + return errNotSupported +} + +// BorrowStaging is a stub. +func (s *RCSvc) BorrowStaging(sessionID string) (*StagingLease, error) { + return nil, errNotSupported +} + +// FinishStaging is a stub. +func (s *RCSvc) FinishStaging(lease StagingLease, ok bool, + written int, etag, versionID string) error { + return errNotSupported +} + +// SessionInfo is a stub. +func (s *RCSvc) SessionInfo(sessionID string, who PrincipalID) (*SessionInfo, error) { + return nil, errNotSupported +} + +// ReadyTransfer is a stub. +func (s *RCSvc) ReadyTransfer(req ReadyRequest) (*ReadyResponse, error) { + return nil, errNotSupported +} + +// GetPutData is a stub. +func (s *RCSvc) GetPutData(sessionID string) (*PutView, error) { + return nil, errNotSupported +} + +// FinishPut is a stub. +func (s *RCSvc) FinishPut(view PutView, committed bool, + etag, versionID string) error { + return errNotSupported +} + +// FinishFinal is a stub. +func (s *RCSvc) FinishFinal(sessionID string) error { + return errNotSupported +} + +// Cancel is a stub. +func (s *RCSvc) Cancel(sessionID string, who PrincipalID) error { + return errNotSupported +} + +// Ready outcome detail codes (see the linux build). +const ( + ReadyOK = 0 + ReadyBusy = 1 + ReadyTimeout = 2 + ReadyVerifyFail = 3 + ReadyWireFail = 4 +)