package cast import ( "context" "sync" "time" "forge.cadoles.com/arcad/edge/pkg/app" "forge.cadoles.com/arcad/edge/pkg/module" "github.com/dop251/goja" "github.com/pkg/errors" "gitlab.com/wpetit/goweb/logger" ) const ( defaultTimeout = 30 * time.Second ) type Module struct { ctx context.Context server *app.Server mutex struct { devices sync.RWMutex refreshDevices sync.Mutex loadURL sync.Mutex quitApp sync.Mutex getStatus sync.Mutex } devices []*Device } func (m *Module) Name() string { return "cast" } func (m *Module) Export(export *goja.Object) { if err := export.Set("refreshDevices", m.refreshDevices); err != nil { panic(errors.Wrap(err, "could not set 'refreshDevices' function")) } if err := export.Set("getDevices", m.getDevices); err != nil { panic(errors.Wrap(err, "could not set 'getDevices' function")) } if err := export.Set("loadUrl", m.loadUrl); err != nil { panic(errors.Wrap(err, "could not set 'loadUrl' function")) } if err := export.Set("stopCast", m.stopCast); err != nil { panic(errors.Wrap(err, "could not set 'stopCast' function")) } if err := export.Set("getStatus", m.getStatus); err != nil { panic(errors.Wrap(err, "could not set 'getStatus' function")) } } func (m *Module) refreshDevices(call goja.FunctionCall, rt *goja.Runtime) goja.Value { rawTimeout := call.Argument(0).String() timeout, err := m.parseTimeout(rawTimeout) if err != nil { panic(errors.WithStack(err)) } promise := m.server.NewPromise() go func() { m.mutex.refreshDevices.Lock() defer m.mutex.refreshDevices.Unlock() ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() devices, err := findDevices(ctx) if err != nil && !errors.Is(err, context.DeadlineExceeded) { err = errors.WithStack(err) logger.Error(ctx, "error refreshing casting devices list", logger.E(errors.WithStack(err))) promise.Reject(err) return } if err == nil { m.mutex.devices.Lock() m.devices = devices m.mutex.devices.Unlock() } devicesCopy := m.getDevicesCopy(devices) promise.Resolve(devicesCopy) }() return rt.ToValue(promise) } func (m *Module) getDevices(call goja.FunctionCall, rt *goja.Runtime) goja.Value { m.mutex.devices.RLock() defer m.mutex.devices.RUnlock() devices := m.getDevicesCopy(m.devices) return rt.ToValue(devices) } func (m *Module) loadUrl(call goja.FunctionCall, rt *goja.Runtime) goja.Value { if len(call.Arguments) < 2 { panic(errors.WithStack(module.ErrUnexpectedArgumentsNumber)) } deviceUUID := call.Argument(0).String() url := call.Argument(1).String() rawTimeout := call.Argument(2).String() timeout, err := m.parseTimeout(rawTimeout) if err != nil { panic(errors.WithStack(err)) } promise := m.server.NewPromise() go func() { m.mutex.loadURL.Lock() defer m.mutex.loadURL.Unlock() ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() err := loadURL(ctx, deviceUUID, url) if err != nil { err = errors.WithStack(err) logger.Error(ctx, "error while casting url", logger.E(err)) promise.Reject(err) return } promise.Resolve(nil) }() return m.server.ToValue(promise) } func (m *Module) stopCast(call goja.FunctionCall) goja.Value { if len(call.Arguments) < 1 { panic(errors.WithStack(module.ErrUnexpectedArgumentsNumber)) } deviceUUID := call.Argument(0).String() rawTimeout := call.Argument(1).String() timeout, err := m.parseTimeout(rawTimeout) if err != nil { panic(errors.WithStack(err)) } promise := m.server.NewPromise() go func() { m.mutex.quitApp.Lock() defer m.mutex.quitApp.Unlock() ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() err := stopCast(ctx, deviceUUID) if err != nil { err = errors.WithStack(err) logger.Error(ctx, "error while quitting casting device app", logger.E(errors.WithStack(err))) promise.Reject(err) return } promise.Resolve(nil) }() return m.server.ToValue(promise) } func (m *Module) getStatus(call goja.FunctionCall) goja.Value { if len(call.Arguments) < 1 { panic(errors.WithStack(module.ErrUnexpectedArgumentsNumber)) } deviceUUID := call.Argument(0).String() rawTimeout := call.Argument(1).String() timeout, err := m.parseTimeout(rawTimeout) if err != nil { panic(errors.WithStack(err)) } promise := m.server.NewPromise() go func() { m.mutex.getStatus.Lock() defer m.mutex.getStatus.Unlock() ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() status, err := getStatus(ctx, deviceUUID) if err != nil { err = errors.WithStack(err) logger.Error(ctx, "error while getting casting device status", logger.E(err)) promise.Reject(err) return } promise.Resolve(status) }() return m.server.ToValue(promise) } func (m *Module) getDevicesCopy(devices []*Device) []Device { devicesCopy := make([]Device, 0, len(m.devices)) for _, d := range devices { devicesCopy = append(devicesCopy, Device{ UUID: d.UUID, Name: d.Name, Host: d.Host, Port: d.Port, }) } return devicesCopy } func (m *Module) parseTimeout(rawTimeout string) (time.Duration, error) { var ( timeout time.Duration err error ) if rawTimeout == "undefined" { timeout = defaultTimeout } else { timeout, err = time.ParseDuration(rawTimeout) if err != nil { return defaultTimeout, errors.Wrapf(err, "invalid duration format '%s'", rawTimeout) } } return timeout, nil } func CastModuleFactory() app.ServerModuleFactory { return func(server *app.Server) app.ServerModule { return &Module{ server: server, devices: make([]*Device, 0), } } }