From 46aae6e5416f69f588d401efbb8477a37d130562 Mon Sep 17 00:00:00 2001 From: sam80180 Date: Sat, 18 Nov 2023 02:29:47 +0800 Subject: [PATCH] feat: add USB and dev mode commands --- cmd/devmode.go | 20 ++++ cmd/devmode/arm.go | 51 ++++++++ cmd/devmode/confirm.go | 53 +++++++++ cmd/devmode/devmodeinit.go | 66 +++++++++++ cmd/devmode/enable.go | 89 ++++++++++++++ cmd/devmode/list.go | 143 ++++++++++++++++++++++ cmd/devmode/reveal.go | 56 +++++++++ cmd/root.go | 6 +- cmd/usb.go | 20 ++++ cmd/usb/list.go | 158 +++++++++++++++++++++++++ cmd/usb/power.go | 102 ++++++++++++++++ cmd/usb/reconnect.go | 55 +++++++++ cmd/usb/usbinit.go | 189 ++++++++++++++++++++++++++++++ go.mod | 17 ++- go.sum | 51 ++++++-- src/entity/devices.go | 15 +++ src/errorcodes/zerrors_windows.go | 14 +++ src/util/log.go | 16 +++ src/util/slice.go | 5 + src/util/usbmux.go | 51 ++++++++ 20 files changed, 1158 insertions(+), 19 deletions(-) create mode 100644 cmd/devmode.go create mode 100644 cmd/devmode/arm.go create mode 100644 cmd/devmode/confirm.go create mode 100644 cmd/devmode/devmodeinit.go create mode 100644 cmd/devmode/enable.go create mode 100644 cmd/devmode/list.go create mode 100644 cmd/devmode/reveal.go create mode 100644 cmd/usb.go create mode 100644 cmd/usb/list.go create mode 100644 cmd/usb/power.go create mode 100644 cmd/usb/reconnect.go create mode 100644 cmd/usb/usbinit.go create mode 100644 src/errorcodes/zerrors_windows.go create mode 100644 src/util/log.go create mode 100644 src/util/slice.go create mode 100644 src/util/usbmux.go diff --git a/cmd/devmode.go b/cmd/devmode.go new file mode 100644 index 0000000..566d435 --- /dev/null +++ b/cmd/devmode.go @@ -0,0 +1,20 @@ +package cmd + +import ( + "github.com/SonicCloudOrg/sonic-ios-bridge/cmd/devmode" + "github.com/spf13/cobra" +) + +var devmodeCmd = &cobra.Command{ + Use: "devmode", + Short: "Enable Developer Mode on iOS 16+ devices or print the current status.", + Long: "Enable Developer Mode on iOS 16+ devices or print the current status.", + Run: func(cmd *cobra.Command, args []string) { + cmd.Help() + }, +} + +func init() { + rootCmd.AddCommand(devmodeCmd) + devmode.InitDevmode(devmodeCmd) +} diff --git a/cmd/devmode/arm.go b/cmd/devmode/arm.go new file mode 100644 index 0000000..5519713 --- /dev/null +++ b/cmd/devmode/arm.go @@ -0,0 +1,51 @@ +package devmode + +import ( + "fmt" + "net/http" + + "github.com/SonicCloudOrg/sonic-ios-bridge/src/util" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "golang.org/x/xerrors" +) + +var devmodeArmCmd = &cobra.Command{ + Use: "arm", + Short: "Arm the Developer Mode (device will reboot)", + Long: "Arm the Developer Mode (device will reboot)", + RunE: func(cmd *cobra.Command, args []string) error { + util.InitLogger() + if bCan, eCan := canToggleDevMode(udid); eCan != nil { + strErrMsg := fmt.Sprintf("Failed to check device %s iOS version", udid) + logrus.Warn(strErrMsg) + return xerrors.New(strErrMsg) + } else if !bCan { + strErrMsg := fmt.Sprintf("Device %s iOS version below 16", udid) + logrus.Warn(strErrMsg) + return xerrors.New(strErrMsg) + } + amfi, errAmfi := getAmfiServer() + if errAmfi != nil { + return errAmfi + } + res, errArm := amfi.DevModeArm() + if errArm != nil { + return errArm + } + if res == http.StatusOK { + logrus.Infof("Developer Mode armed.") + return nil + } else { + strErrMsg := fmt.Sprintf("Failed to arm Developer Mode (%d).", res) + logrus.Warn(strErrMsg) + return xerrors.New(strErrMsg) + } + }, +} + +func initDevModeArmCmd() { + devmodeRootCMD.AddCommand(devmodeArmCmd) + devmodeArmCmd.Flags().StringVarP(&udid, "udid", "u", "", "target specific device by UDID") + devmodeArmCmd.MarkFlagRequired("udid") +} diff --git a/cmd/devmode/confirm.go b/cmd/devmode/confirm.go new file mode 100644 index 0000000..e0d40ba --- /dev/null +++ b/cmd/devmode/confirm.go @@ -0,0 +1,53 @@ +package devmode + +import ( + "fmt" + "net/http" + + "github.com/SonicCloudOrg/sonic-ios-bridge/src/util" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "golang.org/x/xerrors" +) + +var devmodeConfirmCmd = &cobra.Command{ + Use: "confirm", + Short: "Confirm enabling of Developer Mode", + Long: "Confirm enabling of Developer Mode", + RunE: func(cmd *cobra.Command, args []string) error { + util.InitLogger() + if bPreCheckIOSVer { + if bCan, eCan := canToggleDevMode(udid); eCan != nil { + strErrMsg := fmt.Sprintf("Failed to check device %s iOS version", udid) + logrus.Warn(strErrMsg) + return xerrors.New(strErrMsg) + } else if !bCan { + strErrMsg := fmt.Sprintf("Device %s iOS version below 16", udid) + logrus.Warn(strErrMsg) + return xerrors.New(strErrMsg) + } + } + amfi, errAmfi := getAmfiServer() + if errAmfi != nil { + return errAmfi + } + res, errReveal := amfi.DevModeEnable() + if errReveal != nil { + return errReveal + } + if res == http.StatusOK { + logrus.Infof("Developer Mode menu enabled successfully.") + return nil + } else { + strErrMsg := fmt.Sprintf("Failed to enable Developer Mode menu (%d).", res) + logrus.Warn(strErrMsg) + return xerrors.New(strErrMsg) + } + }, +} + +func initDevModeConfirmCmd() { + devmodeRootCMD.AddCommand(devmodeConfirmCmd) + devmodeConfirmCmd.Flags().StringVarP(&udid, "udid", "u", "", "target specific device by UDID") + devmodeConfirmCmd.MarkFlagRequired("udid") +} diff --git a/cmd/devmode/devmodeinit.go b/cmd/devmode/devmodeinit.go new file mode 100644 index 0000000..3bc8d07 --- /dev/null +++ b/cmd/devmode/devmodeinit.go @@ -0,0 +1,66 @@ +package devmode + +import ( + "encoding/json" + "os" + "reflect" + + "github.com/SonicCloudOrg/sonic-ios-bridge/src/errorcodes" + + giDevice "github.com/SonicCloudOrg/sonic-gidevice" + "github.com/SonicCloudOrg/sonic-ios-bridge/src/entity" + "github.com/SonicCloudOrg/sonic-ios-bridge/src/util" + "github.com/spf13/cobra" + "golang.org/x/xerrors" +) + +var devmodeRootCMD *cobra.Command +var bPreCheckIOSVer bool = true + +// option bindings +var udid string +var bIsOutputJson bool + +func InitDevmode(devmodeCMD *cobra.Command) { + devmodeRootCMD = devmodeCMD + initDevModeListCmd() + initDevModeArmCmd() + initDevModeRevealCmd() + initDevModeEnableCmd() + initDevModeConfirmCmd() +} + +func getAmfiServer() (giDevice.Amfi, error) { + device := util.GetDeviceByUdId(udid) + if device == nil { + os.Exit(errorcodes.ERROR_DEV_NOT_EXIST) + } + return device.AmfiService() +} + +func canToggleDevMode(udid string) (bool, error) { + gidevice := util.GetDeviceByUdId(udid) + if gidevice == nil { + return false, xerrors.Errorf("Device %s not found", udid) + } + device := entity.Device{} + deviceByte, _ := json.Marshal(gidevice.Properties()) + json.Unmarshal(deviceByte, &device) + detail, err2 := entity.GetDetail(gidevice) + if err2 != nil { + return false, err2 + } else { + device.DeviceDetail = *detail + } + devmode := entity.DevMode{Device: device} + b, e := devmode.CanCheck() + if e != nil { + return false, e + } + return b, nil +} + +func __PACKAGE__() string { // https://www.appsloveworld.com/go/3/how-to-get-name-of-current-package-in-go?expand_article=1 + type dummy struct{} + return reflect.TypeOf(dummy{}).PkgPath() +} diff --git a/cmd/devmode/enable.go b/cmd/devmode/enable.go new file mode 100644 index 0000000..a1060e2 --- /dev/null +++ b/cmd/devmode/enable.go @@ -0,0 +1,89 @@ +package devmode + +import ( + "os" + "strings" + "sync" + "time" + + giDevice "github.com/SonicCloudOrg/sonic-gidevice" + "github.com/SonicCloudOrg/sonic-ios-bridge/src/entity" + "github.com/SonicCloudOrg/sonic-ios-bridge/src/util" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +// option bindings +var bWaitReboot, bAutoConfirm bool +var intEnableWaitTimeout int + +var devmodeEnableCmd = &cobra.Command{ + Use: "enable", + Short: "Enable Developer Mode (device will reboot)", + Long: "Enable Developer Mode (device will reboot)", + RunE: func(cmd *cobra.Command, args []string) error { + //util.InitLogger() + errArm := devmodeArmCmd.RunE(cmd, args) + if errArm != nil { + return errArm + } + if bWaitReboot { + bIsDeviceOnline := true + wg := new(sync.WaitGroup) + wg.Add(1) + shutDownFun, errListen := util.UsbmuxListen(func(gidevice *giDevice.Device, device *entity.Device, e error, cancelFunc func()) { + if device == nil { + return + } + funcDone := func() { + cancelFunc() + bIsDeviceOnline = true + logrus.Infof("Device %s is online.", udid) + wg.Done() + } + if device.Status == "offline" { + bIsDeviceOnline = false + logrus.Infof("Device %s is offline.", udid) + } else if !bIsDeviceOnline && device.Status == "online" { + if device.SerialNumber == udid { + funcDone() + return + } + detail, _ := entity.GetDetail(*gidevice) + if detail != nil && detail.UniqueDeviceID == udid { + funcDone() + return + } + } + }) + if errListen != nil { + return errListen + } + go func() { + time.Sleep(time.Duration(intEnableWaitTimeout) * time.Second) + logrus.Warnf("Timeout waiting for device %s to reboot.", udid) + shutDownFun() + wg.Done() + }() + wg.Wait() + if bIsDeviceOnline && bAutoConfirm { + bPreCheckIOSVer = false + devmodeConfirmCmd.RunE(cmd, args) + } else { + executable, _ := os.Executable() + pkgPath := strings.Split(__PACKAGE__(), "/") + logrus.Infof("Please check the device %s is online and then run '%s %s %s -u %s'.", udid, executable, pkgPath[len(pkgPath)-1], devmodeConfirmCmd.Use, udid) + } + } + return nil + }, +} + +func initDevModeEnableCmd() { + devmodeRootCMD.AddCommand(devmodeEnableCmd) + devmodeEnableCmd.Flags().StringVarP(&udid, "udid", "u", "", "target specific device by UDID") + devmodeEnableCmd.MarkFlagRequired("udid") + devmodeEnableCmd.Flags().BoolVar(&bWaitReboot, "wait", false, "wait for reboot to complete") + devmodeEnableCmd.Flags().IntVar(&intEnableWaitTimeout, "wait-timeout", 60, "wait timeout in seconds") + devmodeEnableCmd.Flags().BoolVarP(&bAutoConfirm, "confirm", "y", false, "automatically confirm after reboot") +} diff --git a/cmd/devmode/list.go b/cmd/devmode/list.go new file mode 100644 index 0000000..9702ab7 --- /dev/null +++ b/cmd/devmode/list.go @@ -0,0 +1,143 @@ +package devmode + +import ( + "encoding/json" + "fmt" + + giDevice "github.com/SonicCloudOrg/sonic-gidevice" + "github.com/SonicCloudOrg/sonic-ios-bridge/src/entity" + "github.com/SonicCloudOrg/sonic-ios-bridge/src/util" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var devmodeListCmd = &cobra.Command{ + Use: "list", + Short: "Print the Developer Mode status of connected devices", + Long: "Print the Developer Mode status of connected devices", + RunE: func(cmd *cobra.Command, args []string) error { + util.InitLogger() + usbMuxClient, err := giDevice.NewUsbmux() + if err != nil { + return util.NewErrorPrint(util.ErrConnect, "usbMux", err) + } + allErrors := []error{} + localList, errReadLocal := usbMuxClient.Devices() + if errReadLocal != nil { + allErrors = append(allErrors, errReadLocal) + } + remoteList, _ := util.ReadRemote() + devices := []entity.DevMode{} + if len(localList) != 0 { + for _, d := range localList { + deviceByte, _ := json.Marshal(d.Properties()) + device := &entity.Device{} + json.Unmarshal(deviceByte, device) + if len(udid) > 0 && device.SerialNumber != udid { + continue + } + detail, err2 := entity.GetDetail(d) + if err2 != nil { + allErrors = append(allErrors, err2) + } else { + device.DeviceDetail = *detail + } + devmode := entity.DevMode{Device: *device} + devices = append(devices, devmode) + } + } + if len(remoteList) != 0 { + for _, d := range remoteList { + deviceByte, _ := json.Marshal(d.Properties()) + device := &entity.Device{} + json.Unmarshal(deviceByte, device) + if len(udid) > 0 && device.SerialNumber != udid { + continue + } + detail, err2 := entity.GetDetail(d) + if err2 != nil { + allErrors = append(allErrors, err2) + } else { + device.DeviceDetail = *detail + } + devmode := entity.DevMode{Device: *device} + devices = append(devices, devmode) + } + } + jsonResults := []map[string]any{} + for _, devmode := range devices { + jsonObj := map[string]any{"udid": devmode.SerialNumber, "status": "N/A"} + bCanCheck, errChk := devmode.CanCheck() + if errChk != nil { + if bIsOutputJson { + jsonObj["error"] = errChk.Error() + } else { + fmt.Printf("%s\t%s\n", devmode.SerialNumber, "Error: "+errChk.Error()) + } + } else if !bCanCheck { + if bIsOutputJson { + jsonObj["error"] = "iOS version below 16" + } else { + fmt.Printf("%s\tN/A\n", devmode.SerialNumber) + } + } else { + device := util.GetDeviceByUdId(devmode.SerialNumber) + if device == nil { + continue + } + interResult, errInfo := device.GetValue("com.apple.security.mac.amfi", "DeveloperModeStatus") + if errInfo != nil { + if bIsOutputJson { + jsonObj["error"] = errInfo.Error() + } else { + fmt.Printf("%s\t%s\n", devmode.SerialNumber, "Error: "+errInfo.Error()) + } + } else { + strDevModeStatus := "N/A" + switch interResult := interResult.(type) { + case bool: + if interResult { + strDevModeStatus = "enabled" + } else { + strDevModeStatus = "disabled" + } + } + if bIsOutputJson { + jsonObj["status"] = strDevModeStatus + } else { + fmt.Printf("%s\t%s\n", devmode.SerialNumber, strDevModeStatus) + } + } + } + if bIsOutputJson { + jsonResults = append(jsonResults, jsonObj) + } + } + if len(jsonResults) > 0 && bIsOutputJson { + if len(udid) > 0 { + b, _ := json.Marshal(jsonResults[0]) + fmt.Println(string(b)) + } else { + b, _ := json.Marshal(jsonResults) + fmt.Println(string(b)) + } + } + if len(allErrors) > 0 { + for _, e := range allErrors { + logrus.Warnf("%+v\n", e) + } + } + return nil + }, +} + +func initDevModeListCmd() { + devmodeRootCMD.AddCommand(devmodeListCmd) + devmodeListCmd.Flags().StringVarP(&udid, "udid", "u", "", "target specific device by UDID") + devmodeListCmd.Flags().BoolVarP(&bIsOutputJson, "json", "j", false, "output in JSON format") +} + +/* +References: +https://github.com/libimobiledevice/libimobiledevice/blob/master/tools/idevicedevmodectl.c#L99 +*/ diff --git a/cmd/devmode/reveal.go b/cmd/devmode/reveal.go new file mode 100644 index 0000000..dd4ed87 --- /dev/null +++ b/cmd/devmode/reveal.go @@ -0,0 +1,56 @@ +package devmode + +import ( + "fmt" + "net/http" + + "github.com/SonicCloudOrg/sonic-ios-bridge/src/util" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "golang.org/x/xerrors" +) + +var devmodeRevealCmd = &cobra.Command{ + Use: "reveal", + Short: "Reveal the Developer Mode menu on the device", + Long: "Reveal the Developer Mode menu on the device", + RunE: func(cmd *cobra.Command, args []string) error { + util.InitLogger() + if bCan, eCan := canToggleDevMode(udid); eCan != nil { + strErrMsg := fmt.Sprintf("Failed to check device %s iOS version", udid) + logrus.Warn(strErrMsg) + return xerrors.New(strErrMsg) + } else if !bCan { + strErrMsg := fmt.Sprintf("Device %s iOS version below 16", udid) + logrus.Warn(strErrMsg) + return xerrors.New(strErrMsg) + } + amfi, errAmfi := getAmfiServer() + if errAmfi != nil { + return errAmfi + } + res, errReveal := amfi.DevModeReveal() + if errReveal != nil { + return errReveal + } + if res == http.StatusOK { + logrus.Infof("Developer Mode menu revealed successfully.") + return nil + } else { + strErrMsg := fmt.Sprintf("Failed to reveal Developer Mode menu (%d).", res) + logrus.Warn(strErrMsg) + return xerrors.New(strErrMsg) + } + }, +} + +func initDevModeRevealCmd() { + devmodeRootCMD.AddCommand(devmodeRevealCmd) + devmodeRevealCmd.Flags().StringVarP(&udid, "udid", "u", "", "target specific device by UDID") + devmodeRevealCmd.MarkFlagRequired("udid") +} + +/* +References: +https://github.com/libimobiledevice/libimobiledevice/blob/master/tools/idevicedevmodectl.c#L440 +*/ diff --git a/cmd/root.go b/cmd/root.go index 2d0710f..be9ca2c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -26,8 +26,12 @@ import ( var isJson, isDetail, isFormat bool var udid string +var strExeccutable string = (func() string { + exe, _ := os.Executable() + return exe +})() var rootCmd = &cobra.Command{ - Use: "sib", + Use: strExeccutable, Short: "Bridge of iOS Devices", Long: ` ▄▄▄▄ ▄▄▄▄ ▄▄▄ ▄▄ ▄▄▄▄▄▄ ▄▄▄▄ diff --git a/cmd/usb.go b/cmd/usb.go new file mode 100644 index 0000000..6ba7513 --- /dev/null +++ b/cmd/usb.go @@ -0,0 +1,20 @@ +package cmd + +import ( + "github.com/SonicCloudOrg/sonic-ios-bridge/cmd/usb" + "github.com/spf13/cobra" +) + +var usbCmd = &cobra.Command{ + Use: "usb", + Short: "Control USB hub", + Long: "Control USB hub", + Run: func(cmd *cobra.Command, args []string) { + cmd.Help() + }, +} + +func init() { + rootCmd.AddCommand(usbCmd) + usb.InitUsb(usbCmd) +} diff --git a/cmd/usb/list.go b/cmd/usb/list.go new file mode 100644 index 0000000..57291f3 --- /dev/null +++ b/cmd/usb/list.go @@ -0,0 +1,158 @@ +package usb + +import ( + "C" + "encoding/json" + "fmt" + + "github.com/SonicCloudOrg/sonic-ios-bridge/src/util" + "github.com/google/gousb" + "github.com/spf13/cobra" + asciitree "github.com/thediveo/go-asciitree" + "golang.org/x/exp/maps" + "golang.org/x/exp/slices" +) + +// option bindings +var bIsOutputTree bool + +type UsbDevice struct { + Bus int `json:"bus"` + Port int `json:"port"` + Address int `json:"address"` + Class gousb.Class `json:"class"` + VendorID gousb.ID `json:"idVendor"` + Manufacturer string `json:"manufacturer"` + SerialNumber string `json:"serialNumber"` + Product string `json:"product"` + ProductID gousb.ID `json:"idProduct"` +} + +func gousbDevice2MyUsbDeviceStruct(device *gousb.Device) UsbDevice { + strSerialNumber, errSerialNumber := device.SerialNumber() + if errSerialNumber != nil { + strSerialNumber = "N/A" + } + strMfr, errMfr := device.Manufacturer() + if errMfr != nil { + strMfr = "N/A" + } + strProduct, errProduct := device.Product() + if errProduct != nil { + strProduct = "N/A" + } + return UsbDevice{ + Bus: device.Desc.Bus, + Port: device.Desc.Port, + Address: device.Desc.Address, + Class: device.Desc.Class, + VendorID: device.Desc.Vendor, + Manufacturer: trimStringDescriptorValue(strMfr), + SerialNumber: trimStringDescriptorValue(strSerialNumber), + Product: trimStringDescriptorValue(strProduct), + ProductID: device.Desc.Product, + } +} + +func (device UsbDevice) ToString() string { + return fmt.Sprintf("Bus=%d Port=%d Dev#=%d\nCls=%d\nManufacturer=%s (%s)\nProduct=%s (%s)\nSerialNumber=%s\n", device.Bus, device.Port, device.Address, device.Class, device.Manufacturer, device.VendorID.String(), device.Product, device.ProductID.String(), device.SerialNumber) +} + +type asciiTreeNode struct { + ID string `json:"-"` + Label string `asciitree:"label" json:"-"` + Props []string `asciitree:"properties" json:"-"` + Children []*asciiTreeNode `asciitree:"children" json:"children"` + UsbDevice +} + +func (node asciiTreeNode) Equals(other asciiTreeNode) bool { + return (node.ID == other.ID) +} + +func gousbDevice2AsciiTreeNode(device *gousb.Device) asciiTreeNode { + structDev := gousbDevice2MyUsbDeviceStruct(device) + return asciiTreeNode{ + ID: usbDeviceID(device), + Label: fmt.Sprintf("%s (%s)", structDev.Product, structDev.ProductID.String()), + Props: []string{ + fmt.Sprintf("Bus=%d Port=%d Addr=%d", structDev.Bus, structDev.Port, structDev.Address), + fmt.Sprintf("Cls=%d", structDev.Class), + fmt.Sprintf("Manufacturer=%s (%s)", structDev.Manufacturer, structDev.VendorID.String()), + fmt.Sprintf("SerialNumber=%s", structDev.SerialNumber), + }, + UsbDevice: structDev, + } +} + +var usbListCmd = &cobra.Command{ + Use: "list", + Short: "List all connected iOS devices", + Long: "List all connected iOS devices", + RunE: func(cmd *cobra.Command, args []string) error { + util.InitLogger() + usbdevices := []UsbDevice{} + d := findUsbDevice(isAppleDevice, func(usbdevice *gousb.Device) bool { return (udid == "" || isMySerial(udid)(usbdevice)) }) + rootHubs := map[string]*asciiTreeNode{} + for _, device := range d { + defer device.Close() + d := gousbDevice2MyUsbDeviceStruct(device) + if bIsOutputTree { + var currentNode *asciiTreeNode + for _, parent := range getUsbDeviceLineage(device, -1) { + nodeID := usbDeviceID(parent) + if isRootHub(parent) { + if v, bFound := rootHubs[nodeID]; bFound { + currentNode = v + } else { + newNode := gousbDevice2AsciiTreeNode(parent) + currentNode = &newNode + rootHubs[nodeID] = currentNode + } + } else { + newNode := gousbDevice2AsciiTreeNode(parent) + if p := slices.IndexFunc(currentNode.Children, func(child *asciiTreeNode) bool { return child.Equals(newNode) }); p >= 0 { + currentNode = currentNode.Children[p] + } else { + currentNode.Children = append(currentNode.Children, &newNode) + currentNode = &newNode + } + } + } + newNode := gousbDevice2AsciiTreeNode(device) + currentNode.Children = append(currentNode.Children, &newNode) + } + if bIsOutputJson { + usbdevices = append(usbdevices, d) + } else if !bIsOutputTree { + fmt.Println(d.ToString()) + } + } + if bIsOutputJson { + var b []byte + if bIsOutputTree { + b, _ = json.Marshal(rootHubs) + } else { + b, _ = json.Marshal(usbdevices) + } + fmt.Println(string(b)) + } else { + if bIsOutputTree { + fmt.Println(asciitree.RenderFancy(maps.Values(rootHubs))) + } + } + return nil + }, +} + +func initUsbListCmd() { + usbRootCMD.AddCommand(usbListCmd) + usbListCmd.Flags().StringVarP(&udid, "udid", "u", "", "target specific device by UDID") + usbListCmd.Flags().BoolVarP(&bIsOutputJson, "json", "j", false, "output in JSON format") + usbListCmd.Flags().BoolVarP(&bIsOutputTree, "tree", "t", false, "output in tree format") +} + +/* +References: +https://pkg.go.dev/github.com/thediveo/go-asciitree +*/ diff --git a/cmd/usb/power.go b/cmd/usb/power.go new file mode 100644 index 0000000..e995bcd --- /dev/null +++ b/cmd/usb/power.go @@ -0,0 +1,102 @@ +package usb + +import ( + "encoding/json" + "fmt" + "os" + "strings" + + "github.com/SonicCloudOrg/sonic-ios-bridge/src/errorcodes" + "github.com/SonicCloudOrg/sonic-ios-bridge/src/util" + "github.com/google/gousb" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +// option bindings +var intBus, intDevAddr int +var intHubPortIndex uint16 +var bPowerOn, bPowerOff bool + +var usbHubPortPowerCmd = &cobra.Command{ + Use: "power", + Short: "Turn on/off power to a USB hub port", + Long: "Turn on/off power to a USB hub port", + RunE: func(cmd *cobra.Command, args []string) error { + util.InitLogger() + devices := findUsbDevice(func(desc *gousb.DeviceDesc) bool { + return (desc.Class == gousb.Class(gousb.ClassHub) && desc.Address == intDevAddr && desc.Bus == intBus) + }, isMySerial(udid)) + if len(devices) <= 0 { + os.Exit(errorcodes.ERROR_DEV_NOT_EXIST) + } + hub := devices[0] + defer hub.Close() + if bPowerOn == bPowerOff { // show status + descHub, errDescHub := getHubDescriptor(hub) + if errDescHub != nil { + logrus.Warn(errDescHub) + os.Exit(errorcodes.ERROR_READ_FAULT) + } + if len(descHub) < 3 { + logrus.Warnf("Unable to determine number of ports: descriptor= %+v", descHub) + os.Exit(errorcodes.ERROR_BAD_LENGTH) + } + numberOfPorts := uint16(descHub[2]) + if intHubPortIndex > numberOfPorts { + logrus.Warnf("Port index out of range (must be 1 ~ %d)", numberOfPorts) + os.Exit(errorcodes.ERROR_INVALID_PARAMETER) + } + results := []map[string]any{} + for i := uint16(1); i <= numberOfPorts; i++ { + if intHubPortIndex > 0 && i != intHubPortIndex { + continue + } + descHubPort, errDescPort := getHubPortDescriptor(hub, uint16(i)) + resultItem := map[string]any{"index": i} + if errDescPort != nil { + if bIsOutputJson { + resultItem["error"] = errDescPort.Error() + } else { + fmt.Printf("Port #%d: %+v\n", i, errDescPort) + } + } else { + state := getHubPortStatus(descHubPort) + if bIsOutputJson { + resultItem["status"] = state + } else { + fmt.Printf("Port #%d: %+v\n", i, strings.Join(state, ", ")) + } + } + results = append(results, resultItem) + } + if bIsOutputJson { + b, _ := json.Marshal(results) + fmt.Println(string(b)) + } + } else { + if intHubPortIndex <= 0 { + logrus.Debug("Port index ('--port') must be greater than 0") + os.Exit(errorcodes.ERROR_INVALID_PARAMETER) + } + s := true + if bPowerOff { + s = false + } + toggleUsbHubPortPower(hub, intHubPortIndex, s) + } + return nil + }, +} + +func initUsbHubPortPowerCmd() { + usbRootCMD.AddCommand(usbHubPortPowerCmd) + usbHubPortPowerCmd.Flags().IntVarP(&intBus, "bus", "b", 0, "bus") + usbHubPortPowerCmd.MarkFlagRequired("bus") + usbHubPortPowerCmd.Flags().IntVarP(&intDevAddr, "device", "d", 0, "device address") + usbHubPortPowerCmd.MarkFlagRequired("device") + usbHubPortPowerCmd.Flags().Uint16VarP(&intHubPortIndex, "port", "p", 0, "port index") + usbHubPortPowerCmd.Flags().BoolVarP(&bPowerOn, "on", "1", false, "power ON") + usbHubPortPowerCmd.Flags().BoolVarP(&bPowerOff, "off", "0", false, "power OFF") + usbHubPortPowerCmd.Flags().BoolVarP(&bIsOutputJson, "json", "j", false, "output in JSON format") +} diff --git a/cmd/usb/reconnect.go b/cmd/usb/reconnect.go new file mode 100644 index 0000000..622210c --- /dev/null +++ b/cmd/usb/reconnect.go @@ -0,0 +1,55 @@ +package usb + +import ( + "os" + "time" + + "github.com/SonicCloudOrg/sonic-ios-bridge/src/errorcodes" + "github.com/SonicCloudOrg/sonic-ios-bridge/src/util" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +// option bindings +var intReconnectWaitTime int + +var usbReconnectCmd = &cobra.Command{ + Use: "reconnect", + Short: "Reconnct a USB device", + Long: "Reconnct a USB device", + RunE: func(cmd *cobra.Command, args []string) error { + util.InitLogger() + devices := findUsbDevice(isAppleDevice, isMySerial(udid)) + if len(devices) <= 0 { + os.Exit(errorcodes.ERROR_DEV_NOT_EXIST) + } + defer devices[0].Close() + lineage := getUsbDeviceLineage(devices[0], 1) + if len(lineage) <= 0 { + os.Exit(errorcodes.ERROR_BAD_UNIT) + } + parent := lineage[0] + defer parent.Close() + logrus.Infof("Connected to hub: %+v,port=%d", parent, devices[0].Desc.Port) + portIndex := uint16(devices[0].Desc.Port) + toggleUsbHubPortPower(parent, portIndex, false) + if intReconnectWaitTime > 0 { + time.Sleep(time.Duration(intReconnectWaitTime) * time.Second) + } + toggleUsbHubPortPower(parent, portIndex, true) + return nil + }, +} + +func initUsbReconnectCmd() { + usbRootCMD.AddCommand(usbReconnectCmd) + usbReconnectCmd.Flags().StringVarP(&udid, "udid", "u", "", "target specific device by UDID") + usbReconnectCmd.MarkFlagRequired("udid") + usbReconnectCmd.Flags().IntVar(&intReconnectWaitTime, "wait", 3, "wait time in seconds") +} + +/* +References: +https://www.gniibe.org/development/ac-power-control-by-USB-hub/ +https://git.gniibe.org/cgit/gnuk/gnuk.git/tree/tool/hub_ctrl.py +*/ diff --git a/cmd/usb/usbinit.go b/cmd/usb/usbinit.go new file mode 100644 index 0000000..0a5e679 --- /dev/null +++ b/cmd/usb/usbinit.go @@ -0,0 +1,189 @@ +package usb + +import ( + "fmt" + "reflect" + "strings" + + "github.com/SonicCloudOrg/sonic-ios-bridge/src/util" + "github.com/google/gousb" + "github.com/spf13/cobra" + "golang.org/x/exp/slices" + "golang.org/x/xerrors" +) + +const ( + APPLE_VENDOR_ID = gousb.ID(0x5ac) + + USB_RT_HUB = (gousb.ControlClass | gousb.ControlDevice) + USB_RT_PORT = (gousb.ControlClass | gousb.ControlOther) + + REQUEST_GET_STATUS = 0x00 + REQUEST_CLEAR_FEATURE = 0x01 + REQUEST_SET_FEATURE = 0x03 + REQUEST_GET_DESCRIPTOR = 0x06 + + DESCRIPTOR_TYPE_HUB = 41 // https://github.com/pyusb/pyusb/blob/73e87f68fc8ece558057113690f9d2391441b58f/usb/legacy.py#L59 + + USB_PORT_FEAT_POWER = 8 +) + +var usbRootCMD *cobra.Command +var gousbContext *gousb.Context = gousb.NewContext() +var all_devices_opener = func(desc *gousb.DeviceDesc) bool { return true } + +// option bindings +var udid string +var bIsOutputJson bool + +func InitUsb(usbCMD *cobra.Command) { + usbRootCMD = usbCMD + initUsbListCmd() + initUsbReconnectCmd() + initUsbHubPortPowerCmd() +} + +func __PACKAGE__() string { // https://www.appsloveworld.com/go/3/how-to-get-name-of-current-package-in-go?expand_article=1 + type dummy struct{} + return reflect.TypeOf(dummy{}).PkgPath() +} + +type myUsbDevicePrefilter func(desc *gousb.DeviceDesc) bool +type myUsbDevicePostfilter func(usbdevice *gousb.Device) bool + +func isAppleDevice(desc *gousb.DeviceDesc) bool { + return desc.Vendor == APPLE_VENDOR_ID +} + +func trimStringDescriptorValue(s string) string { + return strings.Trim(s, "\x00") +} + +func isMySerial(myUdid string) myUsbDevicePostfilter { + return func(usbdevice *gousb.Device) bool { + strSerialNumber, errSerialNumber := usbdevice.SerialNumber() + if errSerialNumber != nil { + return false + } + return (myUdid == trimStringDescriptorValue(strSerialNumber)) + } +} + +func isRootHub(d *gousb.Device) bool { + return (d.Desc.Class == gousb.ClassHub && len(d.Desc.Path) <= 0) +} + +func usbDeviceID(d *gousb.Device) string { + return fmt.Sprintf("%d-%d", d.Desc.Bus, d.Desc.Address) +} + +func findUsbDevice(prefilter myUsbDevicePrefilter, postfilter myUsbDevicePostfilter) []*gousb.Device { + if prefilter == nil { + prefilter = all_devices_opener + } + d, _ := gousbContext.OpenDevices(prefilter) + devices := []*gousb.Device{} + for _, device := range d { + if postfilter != nil && !postfilter(device) { + defer device.Close() + continue + } + devices = append(devices, device) + } + return devices +} + +func getUsbDeviceLineage(device *gousb.Device, levels int) []*gousb.Device { + lineage := []*gousb.Device{} + currentDevice := device + for { + if len(currentDevice.Desc.Path) <= 0 /* root hub */ || (levels > 0 && len(lineage) >= levels) { + break + } + parentPath := currentDevice.Desc.Path[0 : len(currentDevice.Desc.Path)-1] + parents := findUsbDevice(func(desc *gousb.DeviceDesc) bool { + if desc.Bus != currentDevice.Desc.Bus || desc.Class != gousb.ClassHub { + return false + } + return (slices.Compare(parentPath, desc.Path) == 0) + }, nil) + if len(parents) <= 0 { + break + } + currentDevice = parents[0] + lineage = util.Prepend(lineage, currentDevice) + } + return lineage +} + +func getHubDescriptor(hub *gousb.Device) ([]byte, error) { + desc := make([]byte, 1024) + descLength, errDesc := hub.Control((USB_RT_HUB | gousb.ControlIn), REQUEST_GET_DESCRIPTOR, (DESCRIPTOR_TYPE_HUB << 8), 0, desc) + if errDesc != nil { + return nil, errDesc + } + if descLength <= 0 { + return nil, xerrors.New("Invalid hub descriptor") + } + return desc[0:descLength], nil +} + +func getHubPortDescriptor(hub *gousb.Device, index uint16) ([]byte, error) { + desc := make([]byte, 4) + descLength, errDesc := hub.Control((USB_RT_PORT | gousb.ControlIn), REQUEST_GET_STATUS, 0, index, desc) + if errDesc != nil { + return nil, errDesc + } + if descLength <= 0 { + return nil, xerrors.New("Invalid hub descriptor") + } + return desc, nil +} + +func getHubPortStatus(desc []byte) []string { + state := []string{} + if (desc[1] & 0x10) != 0 { + state = append(state, "indicator") + } + if (desc[1] & 0x08) != 0 { + state = append(state, "test") + } + if (desc[1] & 0x04) != 0 { + state = append(state, "highspeed") + } + if (desc[1] & 0x02) != 0 { + state = append(state, "lowspeed") + } + if (desc[1] & 0x01) != 0 { + state = append(state, "power") + } + if (desc[0] & 0x10) != 0 { + state = append(state, "RESET") + } + if (desc[0] & 0x08) != 0 { + state = append(state, "oc") + } + if (desc[0] & 0x04) != 0 { + state = append(state, "suspend") + } + if (desc[0] & 0x02) != 0 { + state = append(state, "enable") + } + if (desc[0] & 0x01) != 0 { + state = append(state, "connect") + } + return state +} + +func toggleUsbHubPortPower(hub *gousb.Device, portIndex uint16, s bool) { + req := REQUEST_CLEAR_FEATURE + if s { + req = REQUEST_SET_FEATURE + } + hub.Control(USB_RT_PORT, uint8(req), USB_PORT_FEAT_POWER, portIndex, nil) +} + +/* +References: +https://github.com/google/gousb/issues/87 +*/ diff --git a/go.mod b/go.mod index 415f009..98cce63 100644 --- a/go.mod +++ b/go.mod @@ -3,15 +3,22 @@ module github.com/SonicCloudOrg/sonic-ios-bridge go 1.18 require ( - github.com/SonicCloudOrg/sonic-gidevice v0.7.5 + github.com/Masterminds/semver v1.5.0 + github.com/SonicCloudOrg/sonic-gidevice v0.7.6 github.com/SonicCloudOrg/sonic-ios-webkit-adapter v0.0.7 github.com/gin-gonic/gin v1.8.1 github.com/google/gopacket v1.1.19 + github.com/google/gousb v1.1.2 github.com/google/uuid v1.3.0 github.com/gorilla/websocket v1.5.0 github.com/mitchellh/mapstructure v1.5.0 github.com/satori/go.uuid v1.2.0 + github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.4.0 + github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816 + github.com/thediveo/go-asciitree v1.0.1 + golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 howett.net/plist v1.0.0 ) @@ -37,10 +44,10 @@ require ( github.com/tidwall/sjson v1.2.5 // indirect github.com/ugorji/go/codec v1.2.7 // indirect github.com/yezihack/e v1.0.0 // indirect - golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect - golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect - golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 // indirect - golang.org/x/text v0.3.6 // indirect + golang.org/x/crypto v0.14.0 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/sys v0.14.0 // indirect + golang.org/x/text v0.13.0 // indirect google.golang.org/protobuf v1.28.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 037f43d..86fbe7e 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,7 @@ -github.com/SonicCloudOrg/sonic-gidevice v0.7.3 h1:1+p2vE+mr79ZOhhtSnQDls1bYk5o8mWlfgEI1IiU7w4= -github.com/SonicCloudOrg/sonic-gidevice v0.7.3/go.mod h1:sww8Tk3iZ/FMhCJtZT+gQgWg7F4u5M2858OARwjC3Yc= -github.com/SonicCloudOrg/sonic-gidevice v0.7.4 h1:Yt9lCzZdziQr4efBWV+fUWakqvxqb/ksT9T6LULrHDY= -github.com/SonicCloudOrg/sonic-gidevice v0.7.4/go.mod h1:sww8Tk3iZ/FMhCJtZT+gQgWg7F4u5M2858OARwjC3Yc= -github.com/SonicCloudOrg/sonic-gidevice v0.7.5 h1:8liJHks2EQoA7INfHeid5+/9GgGV3/KAnyPEPQw8XKE= -github.com/SonicCloudOrg/sonic-gidevice v0.7.5/go.mod h1:sww8Tk3iZ/FMhCJtZT+gQgWg7F4u5M2858OARwjC3Yc= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/SonicCloudOrg/sonic-gidevice v0.7.6 h1:zSHY1aCrMEQNtnsKsfsOUp7erxf5ocl/eoxjfvKSpNg= +github.com/SonicCloudOrg/sonic-gidevice v0.7.6/go.mod h1:SEquP5doc1Xa9Y0556SJBpFg7Pex+jXk+y4XQ4i0yxo= github.com/SonicCloudOrg/sonic-ios-webkit-adapter v0.0.7 h1:s4OTcJ0VG4mg3501Ec+aSR6gQ8eTWz26fdgCUjxlmJQ= github.com/SonicCloudOrg/sonic-ios-webkit-adapter v0.0.7/go.mod h1:q5dSyO/kX8Mkypy6DkFuqgWnBmHZE7oU7m+pW7kdT+8= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -16,6 +14,7 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= @@ -27,15 +26,19 @@ github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0= github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM= github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/google/gousb v1.1.2 h1:1BwarNB3inFTFhPgUEfah4hwOPuDz/49I0uX8XNginU= +github.com/google/gousb v1.1.2/go.mod h1:GGWUkK0gAXDzxhwrzetW592aOmkkqSGcj5KLEgmCVUg= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= @@ -50,6 +53,7 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= @@ -73,6 +77,8 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= +github.com/onsi/gomega v1.28.1 h1:MijcGUbfYuznzK/5R4CPNoUP/9Xvuo20sXfEm6XxoTA= github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -86,6 +92,9 @@ github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6po github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= @@ -95,12 +104,18 @@ github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816 h1:J6v8awz+me+xeb/cUTotKgceAYouhIB3pjzgRd6IlGk= +github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816/go.mod h1:tzym/CEb5jnFI+Q0k4Qq3+LvRF4gO3E2pxS8fHP8jcA= +github.com/thediveo/go-asciitree v1.0.1 h1:GvKV0JtP9PhVhOXjhdE6Gnw5WlDyRK9s1x0kNvBdOco= +github.com/thediveo/go-asciitree v1.0.1/go.mod h1:OoZbd7y9qy8qizoDaYZ1qxlO+Ks4Nd1+BC28wUaBmFg= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw= github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= @@ -119,36 +134,46 @@ github.com/yezihack/e v1.0.0 h1:7VlBECYRR9AdgHOl4V5D8llUsebRXp/LEcyAQzNLX08= github.com/yezihack/e v1.0.0/go.mod h1:oeAOeWSXinqRjLpELZwcmvBwqSbXjzHKT7inUH0emdk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 h1:siQdpVirKtzPhKl3lZWozZraCFObP8S1v6PRp0bLrtU= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= @@ -164,8 +189,8 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= howett.net/plist v0.0.0-20201203080718-1454fab16a06/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= diff --git a/src/entity/devices.go b/src/entity/devices.go index 7f14d30..4ef3d2f 100644 --- a/src/entity/devices.go +++ b/src/entity/devices.go @@ -21,6 +21,8 @@ import ( "encoding/json" "fmt" "strings" + + "github.com/Masterminds/semver" ) type DeviceList struct { @@ -79,3 +81,16 @@ func (deviceList DeviceList) ToFormat() string { result, _ := json.MarshalIndent(deviceList, "", "\t") return string(result) } + +type DevMode struct { + Device + DevModeStatus string `json:"status,omitempty"` +} + +func (devmode DevMode) CanCheck() (bool, error) { + v, err := semver.NewVersion(devmode.DeviceDetail.ProductVersion) + if err != nil { + return false, err + } + return v.Major() >= 16, nil +} diff --git a/src/errorcodes/zerrors_windows.go b/src/errorcodes/zerrors_windows.go new file mode 100644 index 0000000..c1ef554 --- /dev/null +++ b/src/errorcodes/zerrors_windows.go @@ -0,0 +1,14 @@ +package errorcodes + +const ( + ERROR_BAD_UNIT = 20 + ERROR_BAD_LENGTH = 24 + ERROR_READ_FAULT = 30 + ERROR_DEV_NOT_EXIST = 55 + ERROR_INVALID_PARAMETER = 87 +) + +/* +References: +https://tip.golang.org/src/cmd/vendor/golang.org/x/sys/windows/zerrors_windows.go +*/ diff --git a/src/util/log.go b/src/util/log.go new file mode 100644 index 0000000..f2da35c --- /dev/null +++ b/src/util/log.go @@ -0,0 +1,16 @@ +package util + +import ( + "os" + + "github.com/sirupsen/logrus" + easy "github.com/t-tomalak/logrus-easy-formatter" +) + +func InitLogger() { + logrus.SetOutput(os.Stderr) + logrus.SetFormatter(&easy.Formatter{ + TimestampFormat: "2006-01-02 15:04:05", + LogFormat: "[%lvl%]: %time% - %msg%\n", + }) +} diff --git a/src/util/slice.go b/src/util/slice.go new file mode 100644 index 0000000..d1a2fda --- /dev/null +++ b/src/util/slice.go @@ -0,0 +1,5 @@ +package util + +func Prepend[E any](arr []E, el ...E) []E { // https://stackoverflow.com/a/27169176/12857692 + return append(el, arr...) +} diff --git a/src/util/usbmux.go b/src/util/usbmux.go new file mode 100644 index 0000000..85c30e1 --- /dev/null +++ b/src/util/usbmux.go @@ -0,0 +1,51 @@ +package util + +import ( + "encoding/json" + "os" + "os/signal" + + giDevice "github.com/SonicCloudOrg/sonic-gidevice" + "github.com/SonicCloudOrg/sonic-ios-bridge/src/entity" +) + +func UsbmuxListen(cb func(gidevice *giDevice.Device, device *entity.Device, e error, cancelFunc func())) (func(), error) { + usbMuxClient, err := giDevice.NewUsbmux() + if err != nil { + return nil, NewErrorPrint(ErrConnect, "usbMux", err) + } + usbmuxInput := make(chan giDevice.Device) + shutdownUsbmuxFun, err2 := usbMuxClient.Listen(usbmuxInput) + if err2 != nil { + return nil, NewErrorPrint(ErrSendCommand, "listen", err2) + } + shutDown := make(chan os.Signal, 1) + signal.Notify(shutDown, os.Interrupt, os.Kill) + go func() { + for { + select { + case d, ok := <-usbmuxInput: + if !ok { // usbmux channel closed + close(shutDown) + return + } + if d == nil { + continue + } + deviceByte, _ := json.Marshal(d.Properties()) + device := &entity.Device{} + errDecode := json.Unmarshal(deviceByte, device) + if errDecode != nil { + cb(nil, nil, errDecode, shutdownUsbmuxFun) + continue + } + device.Status = device.GetStatus() + cb(&d, device, nil, shutdownUsbmuxFun) + case <-shutDown: + shutdownUsbmuxFun() + return + } + } + }() + return shutdownUsbmuxFun, nil +}