Compare commits

...

4 Commits

Author SHA1 Message Date
Aveline 255f18a2ad
Merge dafebacdfd into 36f06a064a 2025-11-06 21:55:06 +01:00
tadic-luka 36f06a064a
feat: add web route for sending WOL package to given mac addr (#945)
* feat: add web route for sending WOL package to given mac addr

```

adds a new route /device/send-wol/:mac-addr to send the magic WOL package
to the specified mac-addr.
Method is POST and is protected.

Useful for custom wake up scripts: example is sending HTTP request through iOS shortcut

Test plan:
calling the API with curl
```
$ curl -X POST   http://<jetkvm-ip>/device/send-wol/xx:xx:xx:xx:xx:xx
WOL sent to xx:xx:xx:xx:xx:xx
```

and observing the magic packet on my laptop/PC:
```
$ ncat -u -l 9 -k | xxd
00000000: ffff ffff ffff d050 9978 a620 d050 9978  .......P.x. .P.x
00000010: a620 d050 9978 a620 d050 9978 a620 d050  . .P.x. .P.x. .P
00000020: 9978 a620 d050 9978 a620 d050 9978 a620  .x. .P.x. .P.x.
00000030: d050 9978 a620 d050 9978 a620 d050 9978  .P.x. .P.x. .P.x
00000040: a620 d050 9978 a620 d050 9978 a620 d050  . .P.x. .P.x. .P
00000050: 9978 a620 d050 9978 a620 d050 9978 a620  .x. .P.x. .P.x.
```

calling the api with invalid mac addr returns HTTP 400 error
```
$ curl -X POST -v http://<jetkvm-ip>/device/send-wol/abcd
...
* Request completely sent off
< HTTP/1.1 400 Bad Request
...
...
Invalid mac address provided

* Resolve golint complaint

---------

Co-authored-by: Marc Brooks <IDisposable@gmail.com>
2025-11-06 21:39:22 +01:00
Siyuan dafebacdfd feat: reset USB gadget if needed 2025-11-05 15:47:31 +00:00
Siyuan f0595fff40 fix: await sleep needs to be called inside async function 2025-11-05 15:45:34 +00:00
4 changed files with 55 additions and 19 deletions

View File

@ -177,7 +177,7 @@ func (u *UsbGadget) Init() error {
u.udc = udcs[0] u.udc = udcs[0]
err := u.configureUsbGadget(false) err := u.configureUsbGadget(false, true)
if err != nil { if err != nil {
return u.logError("unable to initialize USB stack", err) return u.logError("unable to initialize USB stack", err)
} }
@ -185,13 +185,13 @@ func (u *UsbGadget) Init() error {
return nil return nil
} }
func (u *UsbGadget) UpdateGadgetConfig() error { func (u *UsbGadget) UpdateGadgetConfig(resetUsbIfNeeded bool) error {
u.configLock.Lock() u.configLock.Lock()
defer u.configLock.Unlock() defer u.configLock.Unlock()
u.loadGadgetConfig() u.loadGadgetConfig()
err := u.configureUsbGadget(true) err := u.configureUsbGadget(true, resetUsbIfNeeded)
if err != nil { if err != nil {
return u.logError("unable to update gadget config", err) return u.logError("unable to update gadget config", err)
} }
@ -199,14 +199,28 @@ func (u *UsbGadget) UpdateGadgetConfig() error {
return nil return nil
} }
func (u *UsbGadget) configureUsbGadget(resetUsb bool) error { func (u *UsbGadget) configureUsbGadget(resetUsb bool, resetUsbIfNeeded bool) error {
return u.WithTransaction(func() error { f := func(resetUsbBefore bool, resetUsbAfter bool) func() error {
return func() error {
if resetUsbBefore {
u.tx.RebindUsb(true)
}
u.tx.MountConfigFS() u.tx.MountConfigFS()
u.tx.CreateConfigPath() u.tx.CreateConfigPath()
u.tx.WriteGadgetConfig() u.tx.WriteGadgetConfig()
if resetUsb { if resetUsbAfter {
u.tx.RebindUsb(true) u.tx.RebindUsb(true)
} }
return nil return nil
}) }
}
// initial attempt to configure the gadget
err := u.WithTransaction(f(false, resetUsb))
if err != nil && !resetUsbIfNeeded {
return err
}
// if the initial attempt failed, try to configure the gadget again with the resetUsb flag
return u.WithTransaction(f(true, resetUsb))
} }

View File

@ -678,7 +678,7 @@ func rpcSetUsbConfig(usbConfig usbgadget.Config) error {
LoadConfig() LoadConfig()
config.UsbConfig = &usbConfig config.UsbConfig = &usbConfig
gadget.SetGadgetConfig(config.UsbConfig) gadget.SetGadgetConfig(config.UsbConfig)
return updateUsbRelatedConfig() return updateUsbRelatedConfig(false)
} }
func rpcGetWakeOnLanDevices() ([]WakeOnLanDevice, error) { func rpcGetWakeOnLanDevices() ([]WakeOnLanDevice, error) {
@ -890,8 +890,8 @@ func rpcGetUsbDevices() (usbgadget.Devices, error) {
return *config.UsbDevices, nil return *config.UsbDevices, nil
} }
func updateUsbRelatedConfig() error { func updateUsbRelatedConfig(resetUsbIfNeeded bool) error {
if err := gadget.UpdateGadgetConfig(); err != nil { if err := gadget.UpdateGadgetConfig(resetUsbIfNeeded); err != nil {
return fmt.Errorf("failed to write gadget config: %w", err) return fmt.Errorf("failed to write gadget config: %w", err)
} }
if err := SaveConfig(); err != nil { if err := SaveConfig(); err != nil {
@ -903,7 +903,7 @@ func updateUsbRelatedConfig() error {
func rpcSetUsbDevices(usbDevices usbgadget.Devices) error { func rpcSetUsbDevices(usbDevices usbgadget.Devices) error {
config.UsbDevices = &usbDevices config.UsbDevices = &usbDevices
gadget.SetGadgetDevices(config.UsbDevices) gadget.SetGadgetDevices(config.UsbDevices)
return updateUsbRelatedConfig() return updateUsbRelatedConfig(false)
} }
func rpcSetUsbDeviceState(device string, enabled bool) error { func rpcSetUsbDeviceState(device string, enabled bool) error {
@ -920,7 +920,7 @@ func rpcSetUsbDeviceState(device string, enabled bool) error {
return fmt.Errorf("invalid device: %s", device) return fmt.Errorf("invalid device: %s", device)
} }
gadget.SetGadgetDevices(config.UsbDevices) gadget.SetGadgetDevices(config.UsbDevices)
return updateUsbRelatedConfig() return updateUsbRelatedConfig(false)
} }
func rpcSetCloudUrl(apiUrl string, appUrl string) error { func rpcSetCloudUrl(apiUrl string, appUrl string) error {

View File

@ -66,7 +66,7 @@ func setMassStorageMode(cdrom bool) error {
return nil return nil
} }
return gadget.UpdateGadgetConfig() return gadget.UpdateGadgetConfig(true)
} }
func mountImage(imagePath string) error { func mountImage(imagePath string) error {

24
web.go
View File

@ -8,6 +8,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io/fs" "io/fs"
"net"
"net/http" "net/http"
"net/http/pprof" "net/http/pprof"
"path/filepath" "path/filepath"
@ -184,6 +185,8 @@ func setupRouter() *gin.Engine {
protected.PUT("/auth/password-local", handleUpdatePassword) protected.PUT("/auth/password-local", handleUpdatePassword)
protected.DELETE("/auth/local-password", handleDeletePassword) protected.DELETE("/auth/local-password", handleDeletePassword)
protected.POST("/storage/upload", handleUploadHttp) protected.POST("/storage/upload", handleUploadHttp)
protected.POST("/device/send-wol/:mac-addr", handleSendWOLMagicPacket)
} }
// Catch-all route for SPA // Catch-all route for SPA
@ -341,7 +344,6 @@ func handleWebRTCSignalWsMessages(
l.Trace().Msg("sending ping frame") l.Trace().Msg("sending ping frame")
err := wsCon.Ping(runCtx) err := wsCon.Ping(runCtx)
if err != nil { if err != nil {
l.Warn().Str("error", err.Error()).Msg("websocket ping error") l.Warn().Str("error", err.Error()).Msg("websocket ping error")
cancelRun() cancelRun()
@ -807,3 +809,23 @@ func handleSetup(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Device setup completed successfully"}) c.JSON(http.StatusOK, gin.H{"message": "Device setup completed successfully"})
} }
func handleSendWOLMagicPacket(c *gin.Context) {
inputMacAddr := c.Param("mac-addr")
macAddr, err := net.ParseMAC(inputMacAddr)
if err != nil {
logger.Warn().Err(err).Str("sendWol", inputMacAddr).Msg("Invalid mac address provided")
c.String(http.StatusBadRequest, "Invalid mac address provided")
return
}
macAddrString := macAddr.String()
err = rpcSendWOLMagicPacket(macAddrString)
if err != nil {
logger.Warn().Err(err).Str("sendWOL", macAddrString).Msg("Failed to send WOL magic packet")
c.String(http.StatusInternalServerError, "Failed to send WOL to %s: %v", macAddrString, err)
return
}
c.String(http.StatusOK, "WOL sent to %s ", macAddr)
}