Support cancelling device functions
Recently came across a scenario where it would be nice to cancel device functions. For most device funcs this lib seems to fire and forget. For instance Params(). There isn't a way to cancel the C func itself. The best the caller can do here is "cancel" it with go semantics which turns into more of a fire and forget. So like:
func GetParams(fridaDevice frida.DeviceInt, timeoutSecs int) (error, bool) {
done := make(chan map[string]any, 1)
errC := make(chan error, 1)
go func() {
params, err := fridaDevice.Params()
if err != nil {
errC <- err
return
}
done <- params
}()
select {
case <-done:
fmt.Println("has frida trust")
return nil, true
case <-time.After(time.Duration(timeoutSecs) * time.Second):
return errors.New("timed out waiting for frida params"), false
case err := <-errC:
return err, false
}
}
Setting up everything to support GCancellable is a quite the lift. Same with wrapping every function and with our own CGO and cancelling from within this lib. 🤷
I'm currently trying to figure out a workaround. Just some thoughts while struggling with something. [
Hey, this is great idea, but like you said going for GCancellable would be huge thing. We can always start providing functions with context version and over time to cover them all. Besides, every Frida major or minor versions should not have too much functions to implement.
I am thinking whether passing parameter to the functions such as some bool cancellable is easier than creating context versions of functions.
You mind showing a little example of each?
context would look something like:
func (d *Device) ParamsCtx(ctx context.Context) (map[string]any, error) {
cancel := C.g_cancellable_new()
done := make(chan map[string]any, 1)
errC := make(chan error, 1)
go func() {
params, err := d.params(cancel)
if err != nil {
errC <- err
return
}
done <- params
}()
select {
case result := <-done:
return result, nil
case <-ctx.Done():
// cancel code goes here
return nil, ctx.Err()
case err := <-errC:
return nil, err
}
}
// Params returns system parameters of the device
func (d *Device) Params() (map[string]any, error) {
return d.params(nil)
}
// Params returns system parameters of the device
func (d *Device) params(cancellable *C.GCancellable) (map[string]any, error) {
if d.device != nil {
var err *C.GError
ht := C.frida_device_query_system_parameters_sync(d.device, cancellable, &err)
if err != nil {
return nil, &FError{err}
}
params := gHashTableToMap(ht)
return params, nil
}
return nil, errors.New("could not obtain params for nil device")
}
Which is meh. Maybe there's some way that's totally obvious to me where we can make the context timeout code a bit more reusable.
I'm not really following the idea about using a bool.
To be honest, I am not quite sure how the GCancellable works so I cannot help that much to choose which one is better.
I'm also learning on the fly here. :swe
My other thought is we support something along these lines.
misc.go Cancellable
type Cancellable struct {
cancellable *C.GCancellable
}
func NewCancellable() *Cancellable {
return &Cancellable{
cancellable: C.g_cancellable_new(),
}
}
func (c *Cancellable) Cancel() {
C.g_cancellable_cancel(c.cancellable)
}
device.go d.Params()
// Params returns system parameters of the device
func (d *Device) Params(cancellable *Cancellable) (map[string]any, error) {
if d.device == nil {
return nil, errors.New("could not obtain params for nil device")
}
var cancel *C.GCancellable
if cancellable != nil {
cancel = cancellable.cancellable
}
var err *C.GError
ht := C.frida_device_query_system_parameters_sync(d.device, cancel, &err)
if err != nil {
return nil, &FError{err}
}
return gHashTableToMap(ht), nil
}
The caller would do something like:
func main() {
d := frida.NewDeviceManager()
d.EnumerateDevices()
fridaDevice, err := d.FindDeviceByID("my device")
if err != nil {
log.Fatal(err)
}
params, err := fridaDevice.Params(nil)
if err != nil {
log.Fatal(err)
}
fmt.Println(params)
params, err = fridaDevice.Params(frida.NewCancellable())
if err != nil {
log.Fatal(err)
}
fmt.Println(params)
}
We could either break the API with this or make a function called ParamsCancellable. In this case since we're really only asking callers to pass nil when they bump versions as we work through functions where this is needed.
Trying this out now. It's actually feels fairly nice. Here's an example of a successful cancel.
func main() {
d := frida.NewDeviceManager()
d.EnumerateDevices()
fridaDevice, err := d.FindDeviceByID("my-device")
if err != nil {
log.Fatal(err)
}
c := frida.NewCancellable()
done := make(chan map[string]any, 1)
errC := make(chan error, 1)
go func() {
params, err := fridaDevice.Params(c)
if err != nil {
errC <- err
return
}
done <- params
}()
c.Cancel()
select {
case result := <-done:
fmt.Println(result)
return
case err := <-errC:
fmt.Println(err)
return
}
}
logs
frida-go git:(chore/params-cancel) ✗ go run main.go
FError: Operation was cancelled
Hmm this does change users API alot, I think it would be better to provide WithCancel versions of the functions.
What are you thinking exactly. Like:
Func().withcancel(cancellable) Func(withcancel(cancellable)) FuncWithCancel(cancellable)
I believe with versions 1/2 we’d end up needing to support function names with different signatures anyway from what is already there. To be extendable in the future we’d probably want to name functions with this ability
FuncWithOpts
What are your thoughts on the api here?
Actually with version 2 using a varadic input we wouldn’t need to change the current signature
Through together an example of this:
misc.go
type Cancellable struct {
cancellable *C.GCancellable
}
func NewCancellable() *Cancellable {
return &Cancellable{
cancellable: C.g_cancellable_new(),
}
}
func (c *Cancellable) Cancel() {
C.g_cancellable_cancel(c.cancellable)
}
type options struct {
cancellable *C.GCancellable
}
type OptFunc func(o *options)
func WithCancel(cancel *Cancellable) OptFunc {
return func(o *options) {
o.cancellable = cancel.cancellable
}
}
device.go
// Params returns system parameters of the device
func (d *Device) Params(opts ...OptFunc) (map[string]any, error) {
o := &options{}
for _, opt := range opts { // <--- this all can become a helper func
opt(o)
}
return d.params(o)
}
func (d *Device) params(opts *options) (map[string]any, error) {
if d.device == nil {
return nil, errors.New("could not obtain params for nil device")
}
var err *C.GError
ht := C.frida_device_query_system_parameters_sync(d.device, opts.cancellable, &err)
if err != nil {
return nil, &FError{err}
}
return gHashTableToMap(ht), nil
}
implemantion
func main() {
d := frida.NewDeviceManager()
d.EnumerateDevices()
fridaDevice, err := d.FindDeviceByID("my-device")
if err != nil {
log.Fatal(err)
}
done := make(chan map[string]any, 1)
errC := make(chan error, 1)
go func() {
params, err := fridaDevice.Params()
if err != nil {
errC <- err
return
}
done <- params
}()
select {
case result := <-done:
fmt.Println(result)
return
case err := <-errC:
fmt.Println(err)
return
}
}
func main() {
d := frida.NewDeviceManager()
d.EnumerateDevices()
fridaDevice, err := d.FindDeviceByID("my-device")
if err != nil {
log.Fatal(err)
}
c := frida.NewCancellable()
done := make(chan map[string]any, 1)
errC := make(chan error, 1)
go func() {
params, err := fridaDevice.Params(frida.WithCancel(c))
if err != nil {
errC <- err
return
}
done <- params
}()
c.Cancel()
select {
case result := <-done:
fmt.Println(result)
case err := <-errC:
fmt.Println(err)
}
}
we end up sticking with params, err := fridaDevice.Params(). This would also provide plenty of flexibility for any other options moving forward making us resistant to breaking changes.
Gonna close. Adding support for context asynchronously. Thanks