Skip to content
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

feat(libooniengine): add support for running tasks #1112

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
30 changes: 0 additions & 30 deletions internal/libooniengine/engine.go

This file was deleted.

93 changes: 92 additions & 1 deletion internal/libooniengine/engine.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@
/// C API for using the OONI engine.
///

#include <stdint.h>

/// OONITask is an asynchronous thread of execution managed by the OONI
/// engine that performs a background operation and emits interim outpus
/// like logs and progress and results of the operation with meaningful
/// events such as, for example, the results of measurements.
typedef intptr_t OONITask;

#ifdef __cplusplus
extern "C" {
#endif
Expand All @@ -19,7 +27,90 @@ char *OONIEngineVersion(void);
/// OONIEngineFreeMemory frees the memory allocated by the engine.
///
/// @param ptr a void pointer refering to the memory to be freed.
void OONIENgineFreeMemory(void *ptr);
void OONIEngineFreeMemory(void *ptr);

/// OONIEngineCall starts a new OONITask using the given @p req.
///
/// @param req A JSON string, owned by the caller, that contains
/// the configuration for the task to start.
///
/// @return Zero on failure, nonzero on success. If the return value
/// is nonzero, a task is running. In such a case, the caller is
/// responsible to eventually dispose of the task using OONIEngineFreeTask.
OONITask OONIEngineCall(char *req);

/// OONIEngineWaitForNextEvent awaits on the @p task event queue until
/// a new event is available or the given @p timeout expires.
///
/// @param task Task handle returned by OONIEngineCall.
///
/// @param timeout Timeout in milliseconds. If the timeout is zero
/// or negative, this function would potentially block forever.
///
/// @return A NULL pointer on failure, non-NULL JSON string otherwise.
/// If the return value is non-NULL, the caller takes ownership of the
/// char pointer and MUST free it using OONIEngineFreeMemory when done
/// using it.
///
/// This function will return a NULL pointer:
///
/// 1. when the timeout expires;
///
/// 2. if @p task is done;
///
/// 3. if @p task is zero or does not refer to a valid task;
///
/// 4. if we cannot JSON serialize the message;
///
/// 5. possibly because of other unknown internal errors.
///
/// In short, you cannot reliably determine whether a task is done by
/// checking whether this function has returned an empty string.
char *OONIEngineWaitForNextEvent(OONITask task, int32_t timeout);

/// OONIEngineTaskGetResult awaits on the result queue until the final
/// result is available.
///
/// @param task Task handle returned by OONIEngineCall.
///
/// @return A NULL pointer on failure, non-NULL JSON string otherwise.
/// If the return value is non-NULL, the caller takes ownership of the
/// char pointer and MUST free it using OONIEngineFreeMemory when done
/// using it.
///
/// This function will return a NULL pointer:
///
/// 1. if @p task is zero or does not refer to a valid task;
///
/// 2. if we cannot JSON serialize the message;
///
/// 3. possibly because of other unknown internal errors.
///
/// In short, you cannot reliably determine whether a task is done by
/// checking whether this function has returned an empty string.
char *OONIEngineTaskGetResult(OONITask task);

/// OONIEngineTaskIsDone returns whether @p task is done. A task is
/// done when it has finished running _and_ its events queue has been drained.
///
/// @param task Task handle returned by OONIEngineCall.
///
/// @return Nonzero if the task exists and either is still running or has some
/// unread events inside its events queue, zero otherwise.
uint8_t OONIEngineTaskIsDone(OONITask task);

/// OONIEngineInterruptTask tells @p task to stop ASAP.
///
/// @param task Task handle returned by OONIEngineCall. If task is zero
/// or does not refer to a valid task, this function will just do nothing.
void OONIEngineInterruptTask(OONITask task);

/// OONIEngineFreeTask frees the memory associated with @p task. If the task is
/// still running, this function will also interrupt it.
///
/// @param task Task handle returned by OONIEngineCall. If task is zero
/// or does not refer to a valid task, this function will just do nothing.
void OONIEngineFreeTask(OONITask task);

#ifdef __cplusplus
}
Expand Down
90 changes: 90 additions & 0 deletions internal/libooniengine/handle.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package main

//
// Handle mimics cgo.Handle but uses a intptr
//

//#include <stdlib.h>
//
//#include "engine.h"
import "C"

import (
"errors"
"log"
"sync"

"github.com/ooni/probe-cli/v3/internal/motor"
)

var (
handler Handler

// errHandleMisuse indicates that an invalid handle was misused
errHandleMisuse = errors.New("misuse of a invalid handle")

// errHandleSpaceExceeded
errHandleSpaceExceeded = errors.New("ran out of handle space")
)

func init() {
handler = Handler{
handles: make(map[Handle]interface{}),
}
}

type Handle C.intptr_t

// Handler handles the entirety of handle operations.
type Handler struct {
handles map[Handle]interface{}
handleIdx Handle
mu sync.Mutex
}

// newHandle returns a handle for a given value
// if we run out of handle space, a zero handle is returned.
func (h *Handler) newHandle(v any) (Handle, error) {
h.mu.Lock()
defer h.mu.Unlock()
ptr := C.intptr_t(h.handleIdx)
newId := ptr + 1
if newId < 0 {
return Handle(0), errHandleSpaceExceeded
}
h.handleIdx = Handle(newId)
h.handles[h.handleIdx] = v
return h.handleIdx, nil
}

// delete invalidates a handle
func (h *Handler) delete(hd Handle) {
h.mu.Lock()
defer h.mu.Unlock()
delete(h.handles, hd)
}

// value returns the associated go value for a valid handle
func (h *Handler) value(hd Handle) (any, error) {
v, ok := h.handles[hd]
if !ok {
return nil, errHandleMisuse
}
return v, nil
}

// getTaskHandle checks if the task handle is valid and returns the corresponding TaskAPI.
func (h *Handler) getTaskHandle(task C.OONITask) (tp motor.TaskAPI) {
hd := Handle(task)
val, err := h.value(hd)
if err != nil {
log.Printf("getTaskHandle: %s", err.Error())
return
}
tp, ok := val.(motor.TaskAPI)
if !ok {
log.Printf("getTaskHandle: invalid type assertion")
return
}
return
}
136 changes: 136 additions & 0 deletions internal/libooniengine/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package main

//
// C API
//

//#include <stdlib.h>
//
//#include "engine.h"
import "C"

import (
"encoding/json"
"log"
"time"
"unsafe"

"github.com/ooni/probe-cli/v3/internal/motor"
"github.com/ooni/probe-cli/v3/internal/version"
)

const (
// invalidTaskHandle represents the invalid task handle.
invalidTaskHandle = 0
)

// parse converts a JSON request string to the concrete Go type.
func parse(req *C.char) (*motor.Request, error) {
out := &motor.Request{}
if err := json.Unmarshal([]byte(C.GoString(req)), &out); err != nil {
return nil, err
}
return out, nil
}

// serialize serializes a OONI response to a JSON string accessible to C code.
func serialize(resp *motor.Response) *C.char {
if resp == nil {
return nil
}
out, err := json.Marshal(resp)
if err != nil {
log.Printf("serializeMessage: cannot serialize message: %s", err.Error())
return nil
}
return C.CString(string(out))
}

//export OONIEngineVersion
func OONIEngineVersion() *C.char {
return C.CString(version.Version)
}

//export OONIEngineFreeMemory
func OONIEngineFreeMemory(ptr *C.void) {
C.free(unsafe.Pointer(ptr))
}

//export OONIEngineCall
func OONIEngineCall(req *C.char) C.OONITask {
r, err := parse(req)
Copy link
Contributor

Choose a reason for hiding this comment

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

I think parse should also handle the case where req is nil.

if err != nil {
log.Printf("OONIEngineCall: %s", err.Error())
return invalidTaskHandle
}
tp := motor.StartTask(r)
if tp == nil {
log.Printf("OONITaskStart: startTask returned NULL")
return invalidTaskHandle
}
handle, err := handler.newHandle(tp)
if err != nil {
log.Printf("OONITaskStart: %s", err.Error())
return invalidTaskHandle
}
return C.OONITask(handle)
}

//export OONIEngineWaitForNextEvent
func OONIEngineWaitForNextEvent(task C.OONITask, timeout C.int32_t) *C.char {
tp := handler.getTaskHandle(task)
if tp == nil {
return nil
}
var ev *motor.Response
if timeout <= 0 {
ev = tp.WaitForNextEvent(time.Duration(timeout))
} else {
ev = tp.WaitForNextEvent(time.Duration(timeout) * time.Millisecond)
}
return serialize(ev)
}

//export OONIEngineTaskGetResult
func OONIEngineTaskGetResult(task C.OONITask) *C.char {
tp := handler.getTaskHandle(task)
if tp == nil {
return nil
}
result := tp.Result()
return serialize(result)
}

//export OONIEngineTaskIsDone
func OONIEngineTaskIsDone(task C.OONITask) (out C.uint8_t) {
tp := handler.getTaskHandle(task)
if tp == nil {
return
}
if !tp.IsDone() {
out++
}
return
}

//export OONIEngineInterruptTask
func OONIEngineInterruptTask(task C.OONITask) {
tp := handler.getTaskHandle(task)
if tp == nil {
return
}
tp.Interrupt()
}

//export OONIEngineFreeTask
func OONIEngineFreeTask(task C.OONITask) {
tp := handler.getTaskHandle(task)
if tp != nil {
tp.Interrupt()
}
handler.delete(Handle(task))
}

func main() {
// do nothing
}
21 changes: 21 additions & 0 deletions internal/motor/emitter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package motor

//
// Emitter
//

// taskChanEmitter implements taskMaybeEmitter.
type taskChanEmitter struct {
// out is the channel where we emit events.
out chan *Response
}

var _ taskMaybeEmitter = &taskChanEmitter{}

// maybeEmitEvent implements taskMaybeEmitter.maybeEmitEvent.
func (e *taskChanEmitter) maybeEmitEvent(resp *Response) {
select {
case e.out <- resp:
default: // buffer full, discard this event
}
}
Loading