// Package hydrant runs hydrant inside a go program through its C api. // // hydrant keeps serving its usual http api, so after Start you talk to it over the bind you // gave it, ideally a unix socket. link against libhydrant_ffi.a by putting its directory in // CGO_LDFLAGS, eg. CGO_LDFLAGS=-L/path/to/target/release. package hydrant /* #cgo CFLAGS: -I${SRCDIR}/../include #cgo LDFLAGS: -lhydrant_ffi #cgo darwin LDFLAGS: -framework SystemConfiguration -framework Security -framework CoreFoundation -liconv -lm // no -lgcc_s because musl has no shared one, and gcc already pulls in an unwinder // either way (libgcc_s when dynamic, libgcc_eh when static) #cgo linux LDFLAGS: -lutil -lrt -lpthread -lm -ldl #include #include "hydrant.h" */ import "C" import ( "encoding/json" "errors" "fmt" "os/signal" "syscall" "time" "unsafe" ) // ErrShutdown is what Err reports after Shutdown stopped hydrant. var ErrShutdown = errors.New("hydrant was shut down") // Hydrant is a hydrant running in this process, until Shutdown. type Hydrant struct { handle *C.hydrant_t done chan struct{} err error } // Start runs hydrant with the settings its binary reads from the environment, like // HYDRANT_DATABASE_PATH and RUST_LOG, given as a map instead. the process environment is // ignored. HYDRANT_API_BIND is required, and "none" runs without an api. hydrant binds it in // the background, so the api may need a moment before it answers. it also ignores SIGPIPE // for the whole process, so writing to a closed pipe returns EPIPE instead of exiting. func Start(settings map[string]string) (*Hydrant, error) { raw, err := json.Marshal(settings) if err != nil { return nil, fmt.Errorf("encode hydrant settings: %w", err) } cSettings := C.CString(string(raw)) defer C.free(unsafe.Pointer(cSettings)) // hydrant writes to sockets whose peer may be gone, and on linux that raises // SIGPIPE on a rust thread, which go doesn't handle and so the default kills // the whole process. rust binaries ignore it at startup, and this does the same signal.Ignore(syscall.SIGPIPE) var cErr *C.char handle := C.hydrant_start(cSettings, &cErr) if handle == nil { return nil, takeErr(cErr) } h := &Hydrant{handle: handle, done: make(chan struct{})} go func() { var cErr *C.char if C.hydrant_wait(handle, &cErr) != 0 { h.err = takeErr(cErr) } else { h.err = ErrShutdown } close(h.done) }() return h, nil } // Done is closed once hydrant stops, because it failed or Shutdown stopped it. a failed // hydrant still holds its database until Shutdown. func (h *Hydrant) Done() <-chan struct{} { return h.done } // Shutdown stops hydrant and waits up to timeout for its database to close, so its memory // is given back and Start can open the same database again. whatever hydrant was in the // middle of is cut off like a crash, which it recovers from on the next start. call it // once, from one goroutine, also after a failure. it errors if the database is still open // after timeout, and the Hydrant can't be used again either way. func (h *Hydrant) Shutdown(timeout time.Duration) error { if h.handle == nil { return errors.New("hydrant was already shut down") } var cErr *C.char var err error if C.hydrant_shutdown(h.handle, C.uint64_t(timeout.Milliseconds()), &cErr) != 0 { err = takeErr(cErr) } // the wait goroutine still uses the handle until it sees the stop <-h.done C.hydrant_free(h.handle) h.handle = nil return err } // Err says why hydrant stopped. it's nil while hydrant is running. func (h *Hydrant) Err() error { select { case <-h.done: return h.err default: return nil } } func takeErr(cErr *C.char) error { if cErr == nil { return errors.New("hydrant failed without saying why") } defer C.hydrant_free_string(cErr) return errors.New(C.GoString(cErr)) }