diff --git a/README.md b/README.md
index f2c9eadd..e9735b24 100644
--- a/README.md
+++ b/README.md
@@ -32,10 +32,10 @@ Supports Linux, macOS and Windows - notes below
- Add users (for admins)
- Devices control (most of interaction is wrapped around Appium APIs)
- Live video
- - Basic remote control - tap, swipe, touch&hold
- - Basic functionalities - Home, Lock, Unlock, Type text
- - Take high quality screenshots
+ - **NB** Videos are essentially MJPEG streams so they are very bandwidth hungry
+ - Basic remote control - tap, swipe, touch&hold, home, lock, unlock, type text to active element, get clipboard
- Install/Uninstall apps
+ - Take high quality screenshots
- Reservation - loading a device sets it `In use` and can't be used by another person until it is released
- Backend
- Serving the web interface
@@ -45,15 +45,16 @@ Supports Linux, macOS and Windows - notes below
### Provider features
- Straightforward dependencies setup
-- Automatic provisioning when devices are connected
+- Devices administration via the hub UI
+- Automatic provisioning when registered devices are connected
- Dependencies automatically installed on devices
- - Appium server set up and started for each device
- - Optionally Selenium Grid 4 node can be registered for each device Appium server
+ - Appium server set up and started for each device
- Remote control APIs for the hub
- - iOS video stream using [WebDriverAgent](https://github.com/appium/WebDriverAgent)
- - Android video stream using [GADS-Android-stream](https://github.com/shamanec/GADS-Android-stream)
- - Limited interaction wrapped around Appium - tap, swipe, touch&hold, type text, lock and unlock device
+ - iOS MJPEG video stream using [WebDriverAgent](https://github.com/appium/WebDriverAgent)
+ - Android MJPEG video stream using [GADS-Android-stream](https://github.com/shamanec/GADS-Android-stream)
+ - Interaction wrapped around Appium - tap, swipe, touch&hold, type text, lock and unlock device, get clipboard
- Appium test execution - each device has its Appium server proxied on a provider endpoint for easier access
+- Optionally Selenium Grid 4 nodes can be registered for each device Appium server
- macOS
- Supports both Android and iOS
- Linux
@@ -101,8 +102,21 @@ or
|[go-ios](https://github.com/danielpaulus/go-ios)| Many thanks for creating this CLI tool to communicate with iOS devices, perfect for installing/reinstalling and running WebDriverAgentRunner without Xcode |
|[Appium](https://github.com/appium)| It would be impossible to control the devices remotely without Appium for the control and WebDriverAgent for the iOS screen stream, kudos! |
-### Demo video
-https://github.com/shamanec/GADS/assets/60219580/271c5025-7f2c-4d7c-a276-eb0e9cceb787
+### Videos
+#### Start hub
+https://github.com/user-attachments/assets/7a6dab5a-52d1-4c48-882d-48b67e180c89
+
+#### Add provider configuration
+https://github.com/user-attachments/assets/07c94ecf-217e-4185-9465-8b8054ddef7e
+
+#### Add devices and start provider
+https://github.com/user-attachments/assets/a1b323da-0169-463e-9a37-b0364fc52480
+
+#### Run Appium tests in parallel with TestNG
+https://github.com/user-attachments/assets/cb2da413-6a72-4ead-9433-c4d2b41d5f4b
+
+#### Remote control
+https://github.com/user-attachments/assets/2d6b29fc-3e83-46be-88c4-d7a563205975
diff --git a/common/constants/constants.go b/common/constants/constants.go
index 35a4c9c0..1769a288 100644
--- a/common/constants/constants.go
+++ b/common/constants/constants.go
@@ -1,7 +1,5 @@
package constants
-import "GADS/common/models"
-
type IndexSort int
const (
@@ -23,654 +21,3 @@ var AndroidVersionToSDK = map[string]string{
"33": "13",
"34": "14",
}
-
-var IOSDeviceInfoMap = map[string]models.IOSModelData{
- "iPhone12,8": {
- Width: "375",
- Height: "667",
- Model: "iPhone SE (2nd gen)",
- },
- // iPhone 5
- "iPhone5,1": {
- Width: "320",
- Height: "568",
- Model: "iPhone 5",
- },
- // iPhone 5
- "iPhone5,2": {
- Width: "320",
- Height: "568",
- Model: "iPhone 5",
- },
- // iPhone 5c
- "iPhone5,3": {
- Width: "320",
- Height: "568",
- Model: "iPhone 5c",
- },
- // iPhone 5c
- "iPhone5,4": {
- Width: "320",
- Height: "568",
- Model: "iPhone 5c",
- },
- // iPhone 5s
- "iPhone6,1": {
- Width: "320",
- Height: "568",
- Model: "iPhone 5S",
- },
- // iPhone 5s
- "iPhone6,2": {
- Width: "320",
- Height: "568",
- Model: "iPhone 5S",
- },
- // iPhone 6
- "iPhone7,2": {
- Width: "375",
- Height: "667",
- Model: "iPhone 6",
- },
- // iPhone 6 Plus
- "iPhone7,1": {
- Width: "414",
- Height: "736",
- Model: "iPhone 6 Plus",
- },
- // iPhone 6s
- "iPhone8,1": {
- Width: "375",
- Height: "667",
- Model: "iPhone 6S",
- },
- // iPhone 6s Plus
- "iPhone8,2": {
- Width: "414",
- Height: "736",
- Model: "iPhone 6S Plus",
- },
- // iPhone SE (1st gen)
- "iPhone8,4": {
- Width: "320",
- Height: "568",
- Model: "iPhone SE (1st gen)",
- },
- // iPhone 7
- "iPhone9,1": {
- Width: "375",
- Height: "667",
- Model: "iPhone 7",
- },
- // iPhone 7
- "iPhone9,3": {
- Width: "375",
- Height: "667",
- Model: "iPhone 7",
- },
- // iPhone 7 Plus
- "iPhone9,2": {
- Width: "414",
- Height: "736",
- Model: "iPhone 7 Plus",
- },
- // iPhone 7 Plus
- "iPhone9,4": {
- Width: "414",
- Height: "736",
- Model: "iPhone 7 Plus",
- },
- // iPhone 8
- "iPhone10,1": {
- Width: "375",
- Height: "667",
- Model: "iPhone 8",
- },
- // iPhone 8
- "iPhone10,4": {
- Width: "375",
- Height: "667",
- Model: "iPhone 8",
- },
- // iPhone 8 plus
- "iPhone10,2": {
- Width: "414",
- Height: "736",
- Model: "iPhone 8 Plus",
- },
- // iPhone 8 plus
- "iPhone10,5": {
- Width: "414",
- Height: "736",
- Model: "iPhone 8 Plus",
- },
- // iPhone X
- "iPhone10,3": {
- Width: "375",
- Height: "812",
- Model: "iPhone X",
- },
- // iPhone X
- "iPhone10,6": {
- Width: "375",
- Height: "812",
- Model: "iPhone X",
- },
- // iPhone XR
- "iPhone11,8": {
- Width: "414",
- Height: "896",
- Model: "iPhone XR",
- },
- // iPhone XS
- "iPhone11,2": {
- Width: "375",
- Height: "812",
- Model: "iPhone XS",
- },
- // iPhone XS Max
- "iPhone11,4": {
- Width: "414",
- Height: "896",
- Model: "iPhone XS Max",
- },
- // iPhone XS Max
- "iPhone11,6": {
- Width: "414",
- Height: "896",
- Model: "iPhone XS Max",
- },
- // iPhone 11
- "iPhone12,1": {
- Width: "414",
- Height: "896",
- Model: "iPhone 11",
- },
- // iPhone 11 Pro
- "iPhone12,3": {
- Width: "375",
- Height: "812",
- Model: "iPhone 11 Pro",
- },
- // iPhone 11 Pro Max
- "iPhone12,5": {
- Width: "414",
- Height: "896",
- Model: "iPhone 11 Pro Max",
- },
- // iPhone 12 Mini
- "iPhone13,1": {
- Width: "360",
- Height: "780",
- Model: "iPhone 12 Mini",
- },
- // iPhone 12
- "iPhone13,2": {
- Width: "390",
- Height: "844",
- Model: "iPhone 12",
- },
- // iPhone 12 Pro
- "iPhone13,3": {
- Width: "390",
- Height: "844",
- Model: "iPhone 12 Pro",
- },
- // iPhone 12 Pro Max
- "iPhone13,4": {
- Width: "428",
- Height: "926",
- Model: "iPhone 12 Pro Max",
- },
- // iPhone 13 Mini
- "iPhone14,4": {
- Width: "360",
- Height: "780",
- Model: "iPhone 13 Mini",
- },
- // iPhone 13
- "iPhone14,5": {
- Width: "390",
- Height: "844",
- Model: "iPhone 13",
- },
- // iPhone 13 Pro
- "iPhone14,2": {
- Width: "390",
- Height: "844",
- Model: "iPhone 13 Pro",
- },
- // iPhone 13 Pro Max
- "iPhone14,3": {
- Width: "428",
- Height: "926",
- Model: "iPhone 13 Pro Max",
- },
- // iPhone SE (3rd gen)
- "iPhone14,6": {
- Width: "375",
- Height: "667",
- Model: "iPhone SE (3rd gen)",
- },
- // iPhone 14
- "iPhone14,7": {
- Width: "390",
- Height: "844",
- Model: "iPhone 14",
- },
- // iPhone 14 Plus
- "iPhone14,8": {
- Width: "428",
- Height: "926",
- Model: "iPhone 14 Plus",
- },
- // iPhone 14 Pro
- "iPhone15,2": {
- Width: "393",
- Height: "852",
- Model: "iPhone 14 Pro",
- },
- // iPhone 14 Pro Max
- "iPhone15,3": {
- Width: "430",
- Height: "932",
- Model: "iPhone 14 Pro Max",
- },
- // iPhone 15
- "iPhone15,4": {
- Width: "393",
- Height: "852",
- Model: "iPhone 15",
- },
- // iPhone 15 Plus
- "iPhone15,5": {
- Width: "430",
- Height: "932",
- Model: "iPhone 15 Plus",
- },
- // iPhone 15 Pro
- "iPhone16,1": {
- Width: "393",
- Height: "852",
- Model: "iPhone 15 Pro",
- },
- // iPhone 15 Pro Max
- "iPhone16,2": {
- Width: "430",
- Height: "932",
- Model: "iPhone 15 Pro Max",
- },
- // iPads
- // iPad Air
- "iPad4,1": {
- Width: "768",
- Height: "1024",
- Model: "iPad Air",
- },
- // iPad Air
- "iPad4,2": {
- Width: "768",
- Height: "1024",
- Model: "iPad Air",
- },
- // iPad Air
- "iPad4,3": {
- Width: "768",
- Height: "1024",
- Model: "iPad Air",
- },
- // iPad Mini 3
- "iPad4,7": {
- Width: "768",
- Height: "1024",
- Model: "iPad Mini 3",
- },
- // iPad Mini 3
- "iPad4,8": {
- Width: "768",
- Height: "1024",
- Model: "iPad Mini 3",
- },
- // iPad Mini 3
- "iPad4,9": {
- Width: "768",
- Height: "1024",
- Model: "iPad Mini 3",
- },
- // iPad Air 2
- "iPad5,3": {
- Width: "768",
- Height: "1024",
- Model: "iPad Air 2",
- },
- // iPad Air 2
- "iPad5,4": {
- Width: "768",
- Height: "1024",
- Model: "iPad Air 2",
- },
- // iPad Mini 4
- "iPad5,1": {
- Width: "768",
- Height: "1024",
- Model: "iPad Mini 4",
- },
- // iPad Mini 4
- "iPad5,2": {
- Width: "768",
- Height: "1024",
- Model: "iPad Mini 4",
- },
- // iPad Pro (9.7)
- "iPad6,3": {
- Width: "768",
- Height: "1024",
- Model: "iPad Pro (9.7)",
- },
- // iPad Pro (9.7)
- "iPad6,4": {
- Width: "768",
- Height: "1024",
- Model: "iPad Pro (9.7)",
- },
- // iPad Pro (12.9 2nd Gen)
- "iPad7,1": {
- Width: "1024",
- Height: "1366",
- Model: "iPad Pro (12.9 2nd Gen)",
- },
- // iPad Pro (12.9 2nd Gen)
- "iPad7,2": {
- Width: "1024",
- Height: "1366",
- Model: "iPad Pro (12.9 2nd Gen)",
- },
- // iPad Pro (10.5)
- "iPad7,3": {
- Width: "834",
- Height: "1112",
- Model: "iPad Pro (10.5)",
- },
- // iPad Pro (10.5)
- "iPad7,4": {
- Width: "834",
- Height: "1112",
- Model: "iPad Pro (10.5)",
- },
- // iPad (5th gen)
- "iPad6,11": {
- Width: "768",
- Height: "1024",
- Model: "iPad (5th gen)",
- },
- // iPad (5th gen)
- "iPad6,12": {
- Width: "768",
- Height: "1024",
- Model: "iPad (5th gen)",
- },
- // iPad Pro (12.9 3rd Gen)
- "iPad8,5": {
- Width: "1024",
- Height: "1366",
- Model: "iPad Pro (12.9 3rd Gen)",
- },
- // iPad Pro (12.9 3rd Gen)
- "iPad8,6": {
- Width: "1024",
- Height: "1366",
- Model: "iPad Pro (12.9 3rd Gen)",
- },
- // iPad Pro (12.9 3rd Gen)
- "iPad8,7": {
- Width: "1024",
- Height: "1366",
- Model: "iPad Pro (12.9 3rd Gen)",
- },
- // iPad Pro (12.9 3rd Gen)
- "iPad8,8": {
- Width: "1024",
- Height: "1366",
- Model: "iPad Pro (12.9 3rd Gen)",
- },
- // iPad Pro (11)
- "iPad8,1": {
- Width: "834",
- Height: "1194",
- Model: "iPad Pro (11)",
- },
- // iPad Pro (11)
- "iPad8,2": {
- Width: "834",
- Height: "1194",
- Model: "iPad Pro (11)",
- },
- // iPad Pro (11)
- "iPad8,3": {
- Width: "834",
- Height: "1194",
- Model: "iPad Pro (11)",
- },
- // iPad Pro (11)
- "iPad8,4": {
- Width: "834",
- Height: "1194",
- Model: "iPad Pro (11)",
- },
- // iPad (6th Gen)
- "iPad7,5": {
- Width: "768",
- Height: "1024",
- Model: "iPad (6th Gen)",
- },
- // iPad (6th Gen)
- "iPad7,6": {
- Width: "768",
- Height: "1024",
- Model: "iPad (6th Gen)",
- },
- // iPad Mini 5
- "iPad11,1": {
- Width: "768",
- Height: "1024",
- Model: "iPad Mini 5",
- },
- // iPad Mini 5
- "iPad11,2": {
- Width: "768",
- Height: "1024",
- Model: "iPad Mini 5",
- },
- // iPad Air 3
- "iPad11,3": {
- Width: "834",
- Height: "1112",
- Model: "iPad Air 3",
- },
- // iPad Air 3
- "iPad11,4": {
- Width: "834",
- Height: "1112",
- Model: "iPad Air 3",
- },
- // iPad (7th Gen)
- "iPad7,11": {
- Width: "810",
- Height: "1080",
- Model: "iPad (7th Gen)",
- },
- // iPad (7th Gen)
- "iPad7,12": {
- Width: "810",
- Height: "1080",
- Model: "iPad (7th Gen)",
- },
- // iPad Pro (12.9 4th Gen)
- "iPad8,11": {
- Width: "1024",
- Height: "1366",
- Model: "iPad Pro (12.9 4th Gen)",
- },
- // iPad Pro (12.9 4th Gen)
- "iPad8,12": {
- Width: "1024",
- Height: "1366",
- Model: "iPad Pro (12.9 4th Gen)",
- },
- // iPad Pro (11 2nd Gen)
- "iPad8,9": {
- Width: "",
- Height: "",
- Model: "iPad Pro (11 2nd Gen)",
- },
- // iPad Pro (11 2nd Gen)
- "iPad8,10": {
- Width: "",
- Height: "",
- Model: "iPad Pro (11 2nd Gen)",
- },
- // iPad Air 4
- "iPad13,1": {
- Width: "820",
- Height: "1180",
- Model: "iPad Air 4",
- },
- // iPad Air 4
- "iPad13,2": {
- Width: "820",
- Height: "1180",
- Model: "iPad Air 4",
- },
- // iPad (8th Gen)
- "iPad11,6": {
- Width: "810",
- Height: "1180",
- Model: "iPad (8th Gen)",
- },
- // iPad (8th Gen)
- "iPad11,7": {
- Width: "810",
- Height: "1180",
- Model: "iPad (8th Gen)",
- },
- // iPad Pro (12.9 5th Gen)
- "iPad13,8": {
- Width: "1024",
- Height: "1366",
- Model: "iPad Pro (12.9 5th Gen)",
- },
- // iPad Pro (12.9 5th Gen)
- "iPad13,9": {
- Width: "1024",
- Height: "1366",
- Model: "iPad Pro (12.9 5th Gen)",
- },
- // iPad Pro (12.9 5th Gen)
- "iPad13,10": {
- Width: "1024",
- Height: "1366",
- Model: "iPad Pro (12.9 5th Gen)",
- },
- // iPad Pro (12.9 5th Gen)
- "iPad13,11": {
- Width: "1024",
- Height: "1366",
- Model: "iPad Pro (12.9 5th Gen)",
- },
- // iPad Pro (11 3rd Gen)
- "iPad13,4": {
- Width: "834",
- Height: "1194",
- Model: "iPad Pro (11 3rd Gen)",
- },
- // iPad Pro (11 3rd Gen)
- "iPad13,5": {
- Width: "834",
- Height: "1194",
- Model: "iPad Pro (11 3rd Gen)",
- },
- // iPad Pro (11 3rd Gen)
- "iPad13,6": {
- Width: "834",
- Height: "1194",
- Model: "iPad Pro (11 3rd Gen)",
- },
- // iPad Pro (11 3rd Gen)
- "iPad13,7": {
- Width: "834",
- Height: "1194",
- Model: "iPad Pro (11 3rd Gen)",
- },
- // iPad mini 6
- "iPad14,1": {
- Width: "",
- Height: "",
- Model: "iPad mini 6",
- },
- // iPad mini 6
- "iPad14,2": {
- Width: "744",
- Height: "1133",
- Model: "iPad mini 6",
- },
- // iPad (9th Gen)
- "iPad12,1": {
- Width: "810",
- Height: "1080",
- Model: "iPad (9th Gen)",
- },
- // iPad (9th Gen)
- "iPad12,2": {
- Width: "810",
- Height: "1080",
- Model: "iPad (9th Gen)",
- },
- // iPad Pro (12.9 6th Gen)
- "iPad14,5": {
- Width: "1024",
- Height: "1366",
- Model: "iPad Pro (12.9 6th Gen)",
- },
- // iPad Pro (12.9 6th Gen)
- "iPad14,6": {
- Width: "1024",
- Height: "1366",
- Model: "iPad Pro (12.9 6th Gen)",
- },
- // iPad Pro (11 4th Gen)
- "iPad14,3": {
- Width: "834",
- Height: "1194",
- Model: "iPad Pro (11 4th Gen)",
- },
- // iPad Pro (11 4th Gen)
- "iPad14,4": {
- Width: "834",
- Height: "1194",
- Model: "iPad Pro (11 4th Gen)",
- },
- // iPad Air 5
- "iPad13,16": {
- Width: "820",
- Height: "1180",
- Model: "iPad Air 5",
- },
- // iPad Air 5
- "iPad13,17": {
- Width: "820",
- Height: "1180",
- Model: "iPad Air 5",
- },
- // iPad (10th Gen)
- "iPad13,18": {
- Width: "820",
- Height: "1180",
- Model: "iPad (10th Gen)",
- },
- // iPad (10th Gen)
- "iPad13,19": {
- Width: "820",
- Height: "1180",
- Model: "iPad (10th Gen)",
- },
-}
diff --git a/common/db/db.go b/common/db/db.go
index 63c19508..7459e067 100644
--- a/common/db/db.go
+++ b/common/db/db.go
@@ -5,11 +5,12 @@ import (
"GADS/common/models"
"context"
"fmt"
- "go.mongodb.org/mongo-driver/bson/primitive"
- "go.mongodb.org/mongo-driver/mongo/gridfs"
"io"
"time"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+ "go.mongodb.org/mongo-driver/mongo/gridfs"
+
"slices"
"go.mongodb.org/mongo-driver/bson"
@@ -79,20 +80,20 @@ func checkDBConnection() {
}
}
-func GetProviderFromDB(nickname string) (models.ProviderDB, error) {
- var provider models.ProviderDB
+func GetProviderFromDB(nickname string) (models.Provider, error) {
+ var provider models.Provider
coll := mongoClient.Database("gads").Collection("providers")
filter := bson.D{{Key: "nickname", Value: nickname}}
err := coll.FindOne(context.TODO(), filter).Decode(&provider)
if err != nil {
- return models.ProviderDB{}, err
+ return models.Provider{}, err
}
return provider, nil
}
-func GetProvidersFromDB() []models.ProviderDB {
- var providers []models.ProviderDB
+func GetProvidersFromDB() []models.Provider {
+ var providers []models.Provider
ctx, cancel := context.WithTimeout(mongoClientCtx, 10*time.Second)
defer cancel()
@@ -201,7 +202,7 @@ func GetUserFromDB(username string) (models.User, error) {
return user, nil
}
-func AddOrUpdateProvider(provider models.ProviderDB) error {
+func AddOrUpdateProvider(provider models.Provider) error {
update := bson.M{
"$set": provider,
}
@@ -244,64 +245,121 @@ func GetDBDevices() []models.Device {
return dbDevices
}
-func GetDBDevicesUDIDs() []string {
- dbDevices := GetDBDevices()
- var udids []string
+func GetUsers() []models.User {
+ var users []models.User
+ collection := mongoClient.Database("gads").Collection("users")
- for _, dbDevice := range dbDevices {
- udids = append(udids, dbDevice.UDID)
+ cursor, err := collection.Find(mongoClientCtx, bson.D{{}}, nil)
+ if err != nil {
+ log.WithFields(log.Fields{
+ "event": "get_db_users",
+ }).Error(fmt.Sprintf("Could not get db cursor when trying to get latest user info from db - %s", err))
+ return users
}
- return udids
-}
+ if err := cursor.All(mongoClientCtx, &users); err != nil {
+ log.WithFields(log.Fields{
+ "event": "get_db_users",
+ }).Error(fmt.Sprintf("Could not get users latest info from db cursor - %s", err))
+ return users
+ }
-func UpsertDeviceDB(device models.Device) error {
- update := bson.M{
- "$set": device,
+ if err := cursor.Err(); err != nil {
+ log.WithFields(log.Fields{
+ "event": "get_db_devices",
+ }).Error(fmt.Sprintf("Encountered db cursor error - %s", err))
+ return users
}
- coll := mongoClient.Database("gads").Collection("devices")
- filter := bson.D{{Key: "udid", Value: device.UDID}}
- opts := options.Update().SetUpsert(true)
- _, err := coll.UpdateOne(mongoClientCtx, filter, update, opts)
- if err != nil {
- return err
+
+ if err := cursor.Err(); err != nil {
+ log.WithFields(log.Fields{
+ "event": "get_db_users",
+ }).Error(fmt.Sprintf("Encountered db cursor error - %s", err))
+ return users
}
- return nil
+
+ cursor.Close(mongoClientCtx)
+
+ return users
}
-func GetDevices() []models.Device {
+func GetDBDeviceNew() []models.Device {
+ var dbDevices []models.Device
// Access the database and collection
- collection := MongoClient().Database("gads").Collection("devices")
- latestDevices := []models.Device{}
+ collection := MongoClient().Database("gads").Collection("new_devices")
- cursor, err := collection.Find(context.Background(), bson.D{{}}, options.Find())
+ cursor, err := collection.Find(context.Background(), bson.D{{}}, nil)
if err != nil {
log.WithFields(log.Fields{
"event": "get_db_devices",
}).Error(fmt.Sprintf("Could not get db cursor when trying to get latest device info from db - %s", err))
- return latestDevices
}
- if err := cursor.All(context.Background(), &latestDevices); err != nil {
+ if err := cursor.All(context.Background(), &dbDevices); err != nil {
log.WithFields(log.Fields{
"event": "get_db_devices",
}).Error(fmt.Sprintf("Could not get devices latest info from db cursor - %s", err))
- return latestDevices
}
if err := cursor.Err(); err != nil {
log.WithFields(log.Fields{
"event": "get_db_devices",
}).Error(fmt.Sprintf("Encountered db cursor error - %s", err))
- return latestDevices
}
- err = cursor.Close(context.TODO())
+ cursor.Close(context.TODO())
+
+ return dbDevices
+}
+
+func UpsertDeviceDB(device models.Device) error {
+ update := bson.M{
+ "$set": device,
+ }
+ coll := mongoClient.Database("gads").Collection("new_devices")
+ filter := bson.D{{Key: "udid", Value: device.UDID}}
+ opts := options.Update().SetUpsert(true)
+ _, err := coll.UpdateOne(mongoClientCtx, filter, update, opts)
if err != nil {
- //stuff
+ return err
}
+ return nil
+}
- return latestDevices
+func DeleteDeviceDB(udid string) error {
+ coll := mongoClient.Database("gads").Collection("new_devices")
+ filter := bson.M{"udid": udid}
+
+ _, err := coll.DeleteOne(mongoClientCtx, filter)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func DeleteUserDB(nickname string) error {
+ coll := mongoClient.Database("gads").Collection("users")
+ filter := bson.M{"username": nickname}
+
+ _, err := coll.DeleteOne(mongoClientCtx, filter)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func DeleteProviderDB(nickname string) error {
+ coll := mongoClient.Database("gads").Collection("providers")
+ filter := bson.M{"nickname": nickname}
+
+ _, err := coll.DeleteOne(mongoClientCtx, filter)
+ if err != nil {
+ return err
+ }
+
+ return nil
}
func AddAdminUserIfMissing() error {
@@ -375,7 +433,3 @@ func UploadFileGridFS(file io.Reader, fileName string, force bool) error {
return nil
}
}
-
-func DownloadFileGridFS(fileName string, filePath string) {
-
-}
diff --git a/common/models/config.go b/common/models/config.go
index 1b49d46f..f7c69757 100644
--- a/common/models/config.go
+++ b/common/models/config.go
@@ -1,33 +1,29 @@
package models
-type ConfigJsonData struct {
- EnvConfig ProviderDB `json:"env-config" bson:"env-config"`
-}
-
-type ProviderDB struct {
- OS string `json:"os" bson:"os"`
- Nickname string `json:"nickname" bson:"nickname"`
- HostAddress string `json:"host_address" bson:"host_address"`
- Port int `json:"port" bson:"port"`
- UseSeleniumGrid bool `json:"use_selenium_grid" bson:"use_selenium_grid"`
- SeleniumGrid string `json:"selenium_grid" bson:"selenium_grid"`
- ProvideAndroid bool `json:"provide_android" bson:"provide_android"`
- ProvideIOS bool `json:"provide_ios" bson:"provide_ios"`
- WdaBundleID string `json:"wda_bundle_id" bson:"wda_bundle_id"`
- WdaRepoPath string `json:"wda_repo_path" bson:"wda_repo_path"`
- SupervisionPassword string `json:"supervision_password" bson:"supervision_password"`
- ProviderFolder string `json:"-" bson:"-"`
- LastUpdatedTimestamp int64 `json:"last_updated" bson:"last_updated"`
- ProvidedDevices []Device `json:"provided_devices" bson:"provided_devices"`
- ConnectedDevices []ConnectedDevice `json:"connected_devices" bson:"connected_devices"`
- WebDriverBinary string `json:"-" bson:"-"`
- UseGadsIosStream bool `json:"use_gads_ios_stream" bson:"use_gads_ios_stream"`
- UseCustomWDA bool `json:"use_custom_wda" bson:"use_custom_wda"`
+type Provider struct {
+ OS string `json:"os" bson:"os"`
+ Nickname string `json:"nickname" bson:"nickname"`
+ HostAddress string `json:"host_address" bson:"host_address"`
+ Port int `json:"port" bson:"port"`
+ UseSeleniumGrid bool `json:"use_selenium_grid" bson:"use_selenium_grid"`
+ SeleniumGrid string `json:"selenium_grid" bson:"selenium_grid"`
+ ProvideAndroid bool `json:"provide_android" bson:"provide_android"`
+ ProvideIOS bool `json:"provide_ios" bson:"provide_ios"`
+ WdaBundleID string `json:"wda_bundle_id" bson:"wda_bundle_id"`
+ WdaRepoPath string `json:"wda_repo_path" bson:"wda_repo_path"`
+ SupervisionPassword string `json:"supervision_password" bson:"supervision_password"`
+ ProviderFolder string `json:"-" bson:"-"`
+ LastUpdatedTimestamp int64 `json:"last_updated" bson:"last_updated"`
+ ProvidedDevices []Device `json:"provided_devices" bson:"provided_devices"`
+ WebDriverBinary string `json:"-" bson:"-"`
+ UseGadsIosStream bool `json:"use_gads_ios_stream" bson:"use_gads_ios_stream"`
+ UseCustomWDA bool `json:"use_custom_wda" bson:"use_custom_wda"`
+ HubAddress string `json:"hub_address" bson:"-"`
}
type ProviderData struct {
- ProviderData ProviderDB `json:"provider"`
- DeviceData []*Device `json:"device_data"`
+ ProviderData Provider `json:"provider"`
+ DeviceData []Device `json:"device_data"`
}
type HubConfig struct {
diff --git a/common/models/models.go b/common/models/models.go
index b5fee136..e605fd68 100644
--- a/common/models/models.go
+++ b/common/models/models.go
@@ -2,6 +2,8 @@ package models
import (
"context"
+ "sync"
+
"github.com/danielpaulus/go-ios/ios"
)
@@ -24,12 +26,6 @@ func (a ByUDID) Len() int { return len(a) }
func (a ByUDID) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByUDID) Less(i, j int) bool { return a[i].UDID < a[j].UDID }
-type IOSModelData struct {
- Width string
- Height string
- Model string
-}
-
type User struct {
Username string `json:"username" bson:"username"`
Password string `json:"password" bson:"password"`
@@ -38,41 +34,41 @@ type User struct {
}
type Device struct {
- Connected bool `json:"connected" bson:"connected"` // common value - if device is currently connected
- UDID string `json:"udid" bson:"udid"` // common value - device UDID
- OS string `json:"os" bson:"os"` // common value - device OS
- Name string `json:"name" bson:"name"` // common value - name of the device
- OSVersion string `json:"os_version" bson:"os_version"` // common value - OS version of the device
- Model string `json:"model" bson:"model"` // common value - device model
- Host string `json:"host" bson:"host"` // common value - IP address of the device host(provider)
- Provider string `json:"provider" bson:"provider"` // common value - nickname of the device host(provider)
- ScreenWidth string `json:"screen_width" bson:"screen_width"` // common value - screen width of device
- ScreenHeight string `json:"screen_height" bson:"screen_height"` // common value - screen height of device
- HardwareModel string `json:"hardware_model,omitempty" bson:"hardware_model,omitempty"` // common value - hardware model of device
- InstalledApps []string `json:"installed_apps" bson:"-"` // provider value - list of installed apps on device
- IOSProductType string `json:"ios_product_type,omitempty" bson:"ios_product_type,omitempty"` // provider value - product type of iOS devices
- LastUpdatedTimestamp int64 `json:"last_updated_timestamp" bson:"last_updated_timestamp"` // common value - last time the device data was updated
- WdaReadyChan chan bool `json:"-" bson:"-"` // provider value - channel for checking that WebDriverAgent is up after start
- Context context.Context `json:"-" bson:"-"` // provider value - context used to control the device set up since we have multiple goroutines
- CtxCancel context.CancelFunc `json:"-" bson:"-"` // provider value - cancel func for the context above, can be used to stop all running device goroutines
- GoIOSDeviceEntry ios.DeviceEntry `json:"-" bson:"-"` // provider value - `go-ios` device entry object used for `go-ios` library interactions
- IsResetting bool `json:"is_resetting" bson:"is_resetting"` // common value - if device setup is currently being reset
- Logger CustomLogger `json:"-" bson:"-"` // provider value - CustomLogger object for the device
- AppiumSessionID string `json:"appiumSessionID" bson:"-"` // provider value - current Appium session ID
- WDASessionID string `json:"wdaSessionID" bson:"-"` // provider value - current WebDriverAgent session ID
- AppiumPort string `json:"appium_port" bson:"-"` // provider value - port assigned to the device for the Appium server
- StreamPort string `json:"stream_port" bson:"-"` // provider value - port assigned to the device for the video stream
- WDAStreamPort string `json:"wda_stream_port" bson:"-"` // provider value - port assigned to iOS devices for the WebDriverAgent stream
- WDAPort string `json:"wda_port" bson:"-"` // provider value - port assigned to iOS devices for the WebDriverAgent instance
- AppiumLogger AppiumLogger `json:"-" bson:"-"` // provider value - AppiumLogger object for logging appium actions
- Available bool `json:"available" bson:"-"` // provider value - if device is currently available - not only connected, but setup completed
- ProviderState string `json:"provider_state" bson:"provider_state"` // common value - current state of the device on the provider - init, preparing, live
-}
-
-type ConnectedDevice struct {
- OS string `json:"os" bson:"os"`
- UDID string `json:"udid" bson:"udid"`
- IsConfigured bool `json:"is_configured" bson:"-"`
+ // DB DATA
+ UDID string `json:"udid" bson:"udid"` // device UDID
+ OS string `json:"os" bson:"os"` // device OS
+ Name string `json:"name" bson:"name"` // name of the device
+ OSVersion string `json:"os_version" bson:"os_version"` // OS version of the device
+ Provider string `json:"provider" bson:"provider"` // nickname of the device host(provider)
+ Usage string `json:"usage" bson:"usage"` // what is the device used for: enabled(automation and remote control), automation(only Appium testing), remote(only remote control), disabled
+ ScreenWidth string `json:"screen_width" bson:"screen_width"` // screen width of device
+ ScreenHeight string `json:"screen_height" bson:"screen_height"` // screen height of device
+ DeviceType string `json:"device_type" bson:"device_type"` // The type of device - `real` or `emulator`
+ // NON-DB DATA
+ /// COMMON VALUES
+ Host string `json:"host" bson:"-"` // IP address of the device host(provider)
+ HardwareModel string `json:"hardware_model" bson:"-"` // hardware model of device
+ LastUpdatedTimestamp int64 `json:"last_updated_timestamp" bson:"-"` // last time the device data was updated
+ Connected bool `json:"connected" bson:"-"` // if device is currently connected
+ IsResetting bool `json:"is_resetting" bson:"-"` // if device setup is currently being reset
+ ProviderState string `json:"provider_state" bson:"-"` // current state of the device on the provider - init, preparing, live
+ /// PROVIDER ONLY VALUES
+ //// RETURNABLE VALUES
+ InstalledApps []string `json:"installed_apps" bson:"-"` // list of installed apps on device
+ ///// NON-RETURNABLE VALUES
+ AppiumSessionID string `json:"-" bson:"-"` // current Appium session ID
+ WDASessionID string `json:"-" bson:"-"` // current WebDriverAgent session ID
+ AppiumPort string `json:"-" bson:"-"` // port assigned to the device for the Appium server
+ StreamPort string `json:"-" bson:"-"` // port assigned to the device for the video stream
+ WDAStreamPort string `json:"-" bson:"-"` // port assigned to iOS devices for the WebDriverAgent stream
+ WDAPort string `json:"-" bson:"-"` // port assigned to iOS devices for the WebDriverAgent instance
+ WdaReadyChan chan bool `json:"-" bson:"-"` // channel for checking that WebDriverAgent is up after start
+ Context context.Context `json:"-" bson:"-"` // context used to control the device set up since we have multiple goroutines
+ CtxCancel context.CancelFunc `json:"-" bson:"-"` // cancel func for the context above, can be used to stop all running device goroutines
+ GoIOSDeviceEntry ios.DeviceEntry `json:"-" bson:"-"` // `go-ios` device entry object used for `go-ios` library interactions
+ Logger CustomLogger `json:"-" bson:"-"` // CustomLogger object for the device
+ AppiumLogger AppiumLogger `json:"-" bson:"-"` // AppiumLogger object for logging appium actions
+ Mutex sync.Mutex `json:"-" bson:"-"` // Mutex to lock resources - especially on device reset
}
type LocalHubDevice struct {
@@ -85,4 +81,5 @@ type LocalHubDevice struct {
InUseTS int64 `json:"in_use_ts"`
AppiumNewCommandTimeout int64 `json:"appium_new_command_timeout"`
IsAvailableForAutomation bool `json:"is_available_for_automation"`
+ Available bool `json:"available" bson:"-"` // if device is currently available - not only connected, but setup completed
}
diff --git a/docs/hub.md b/docs/hub.md
index db126ac0..749841ce 100644
--- a/docs/hub.md
+++ b/docs/hub.md
@@ -7,8 +7,7 @@ Follow the setup steps to create and run a provider instance.
You can have multiple provider instances on different hosts providing devices.
### Starting hub instance
-Run `./GADS hub` with the following flags:
-- `--auth=` - `true/false` to enable actual user authentication (default is `false`)
+Run `./GADS hub` with the following flags:
- `--host-address=` - local IP address of the host machine, e.g. `192.168.1.6` (default is `localhost`, I would advise against using the default value)
- `--port=` - port on which the UI and backend service will run
- `--mongo-db=` - IP address and port of the MongoDB instance, e.g `192.168.1.6:27017` (default is `localhost:27017`) - tested only on local network
@@ -24,8 +23,25 @@ If you want to work on the React UI with hot reload you need to add a proxy in `
4. Run `npm start`
### Additional notes
+#### Users administration
+You can add/delete users and change their roles/passwords via the `Admin` panel.
+There are no limitations on usernames and passwords - only the default `admin` user cannot be deleted and its role changed(you can change its password though)
+
+#### Providers administration
+For each provider instance you need to create a provider configuration via the `Admin` panel.
+All fields have tooltips to help you with the required information.
+
+#### Devices administration
+Device configurations are added via the `Admin` panel.
+You have to provide all the required information and assign each device to a provider.
+Changes to the device configuration require the respective provider instance restarted.
+All fields have tooltips to help you with the required information.
+
#### Experimental Appium grid
-Using Selenium Grid 4 is a bit of a hassle and some versions do not work properly with Appium relay nodes. For this reason I created an experimental grid implementation into the hub itself. I haven't even read the Selenium Grid implementation and made up something myself - it might not work properly but could be the better alternative if it does work properly. The experimental grid was tested only using latest Appium and Selenium Java client versions and with TestNG. Tests can be executed sequentially or in parallel using TestNG with `methods` or `classes` with multiple threads. I assume it should support any type of session creation with any Appium language client
+Using Selenium Grid 4 is a bit of a hassle and some versions do not work properly with Appium relay nodes.
+For this reason I created an experimental grid implementation into the hub itself.
+I haven't even read the Selenium Grid implementation and made up something myself - it might not work properly but could be the better alternative if it does work properly.
+The experimental grid was tested only using latest Appium and Selenium Java client versions and with TestNG. Tests can be executed sequentially or in parallel using TestNG with `methods` or `classes` with multiple threads. I assume it should support any type of session creation with any Appium language client
* The grid is accessible on your hub instance e.g. `http://192.168.1.6:10000/grid` and should be used as Appium/Selenium driver URL target. You just try to start a session as you usually do with Selenium Grid
* The grid allows targeting devices by UDID
@@ -33,11 +49,12 @@ Using Selenium Grid 4 is a bit of a hassle and some versions do not work properl
* Additionally the grid allows filtering by `appium:platformVersion` capability which supports exact version e.g. `17.5.1` or a major version e.g. `17`, `11` etc
#### Selenium Grid
-Devices can be automatically connected to Selenium Grid 4 instance. You need to create the Selenium Grid hub instance yourself and then set it up in the provider configuration to connect to it.
+Devices can be automatically connected to Selenium Grid 4 instance.
+You need to create the Selenium Grid hub instance yourself and then set it up in the provider configuration to connect to it.
* Start your Selenium hub instance, e.g. `java -jar selenium.jar --host 192.168.1.6 --port 4444`
-* When adding/updating provider configuration from `Admin > Provider administration` you need to supply the Selenium hub address, e.g. `http://192.168.1.6:4444`
+* When adding/updating provider configuration from `Admin > Provider` you need to supply the Selenium hub address, e.g. `http://192.168.1.6:4444`
* You also need to upload the respective Selenium jar file so the provider instances have access to it
- * Log in to the hub with admin user, go to `Admin > Files administration` and upload the Selenium jar file - v4.13 is recommended.
+ * Log in to the hub with admin user, go to `Admin > Files` and upload the Selenium jar file - v4.13 is recommended.
* The file will be stored in Mongo and providers will download it on start automatically.
**NB** At the time support for Selenium Grid was implemented latest Selenium version was 4.15. The latest version that actually worked with Appium relay nodes was 4.13. I haven't tested with lower versions. Use lower versions at your own risk. Versions > 4.15 might also work but it wasn't tested as well.
\ No newline at end of file
diff --git a/docs/provider.md b/docs/provider.md
index 4ed9546f..01366ddc 100644
--- a/docs/provider.md
+++ b/docs/provider.md
@@ -17,9 +17,9 @@ The provider component is what actually sets up the Appium servers and all other
Provider configuration is added through the GADS UI
- Log in the hub UI with an admin user.
- Go to the `Admin` section.
-- Open `Providers administration`
+- Open `Providers`
- On the `New provider` tab fill in all needed data and save.
-- You should see a new provider tab with the nickname you provided. You can now start up a provider instance using the newly added configuration.
+- You should see a new provider component with the configuration you provided. You can now start up a provider instance using the newly added configuration.
## Provider data folder - optional
The provider needs a persistent folder where logs, apps and other files might be stored.
@@ -89,6 +89,7 @@ Refer to the `--provider-folder` flag in [Running a provider instance](#running-
- `--mongo-db=` - optional, IP address and port of the MongoDB instance (default is `localhost:27017`)
- `--provider-folder=` - optional, folder where provider should store logs and apps and other needed files. Can be relative path to the folder where provider binary is located or full path on the host - `./test`, `.`, `./test/test1`, `/Users/shamanec/Desktop/test` are all valid. Default is the folder where the binary is currently located - `.`
- `--log-level=` - optional, how verbose should the provider logs be (default is `info`, use `debug` for more log output)
+ - `--hub=` - mandatory, the address of the hub instance so the provider can push data to it automatically, e.g `http://192.168.68.109:10000`
### Dependencies notes
#### Appium
diff --git a/go.mod b/go.mod
index 888be64b..8a707d28 100644
--- a/go.mod
+++ b/go.mod
@@ -3,41 +3,48 @@ module GADS
go 1.21
require (
- github.com/danielpaulus/go-ios v1.0.121
+ github.com/Masterminds/semver v1.5.0
+ github.com/danielpaulus/go-ios v1.0.123
github.com/gin-contrib/static v0.0.1
github.com/gin-gonic/gin v1.9.0
github.com/gobwas/ws v1.4.0
- github.com/sirupsen/logrus v1.8.1
+ github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.8.0
github.com/spf13/pflag v1.0.5
)
require (
- github.com/Masterminds/semver v1.5.0 // indirect
+ github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
- github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/golang/snappy v0.0.1 // indirect
+ github.com/grandcat/zeroconf v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.13.6 // indirect
+ github.com/miekg/dns v1.1.61 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
+ github.com/stretchr/testify v1.8.4 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
- golang.org/x/net v0.9.0 // indirect
- golang.org/x/sync v0.1.0 // indirect
- golang.org/x/sys v0.7.0 // indirect
- golang.org/x/text v0.9.0 // indirect
+ go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
+ golang.org/x/mod v0.18.0 // indirect
+ golang.org/x/net v0.26.0 // indirect
+ golang.org/x/sync v0.7.0 // indirect
+ golang.org/x/sys v0.21.0 // indirect
+ golang.org/x/text v0.16.0 // indirect
+ golang.org/x/tools v0.22.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
- howett.net/plist v0.0.0-20200419221736-3b63eb3a43b5 // indirect
+ howett.net/plist v1.0.1 // indirect
+ software.sslmate.com/src/go-pkcs12 v0.4.0 // indirect
)
require (
@@ -45,7 +52,7 @@ require (
github.com/gin-contrib/cors v1.4.0
github.com/go-playground/validator/v10 v10.12.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
- github.com/google/uuid v1.4.0
+ github.com/google/uuid v1.6.0
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.3 // indirect
github.com/mattn/go-isatty v0.0.18 // indirect
@@ -54,7 +61,6 @@ require (
github.com/ugorji/go/codec v1.2.11 // indirect
go.mongodb.org/mongo-driver v1.12.1
golang.org/x/arch v0.3.0 // indirect
- golang.org/x/crypto v0.8.0 // indirect
- golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
- google.golang.org/protobuf v1.30.0 // indirect
+ golang.org/x/crypto v0.24.0 // indirect
+ google.golang.org/protobuf v1.33.0 // indirect
)
diff --git a/go.sum b/go.sum
index 47a57320..d12b7d7e 100644
--- a/go.sum
+++ b/go.sum
@@ -3,18 +3,18 @@ github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF0
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.8.7 h1:d3sry5vGgVq/OpgozRUNP6xBsSo0mtNdwliApw+SAMQ=
github.com/bytedance/sonic v1.8.7/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
+github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
+github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
-github.com/danielpaulus/go-ios v1.0.121 h1:PvaYooOlhl3t6H5JGYkdTbNcz0iWBkyywk5fOLuPK+k=
-github.com/danielpaulus/go-ios v1.0.121/go.mod h1:Rtn5ICcWo8dhRyhmX/O++hgmiaqrXMwuWR/QykADB6Q=
+github.com/danielpaulus/go-ios v1.0.123 h1:Pyv+9xdFIaaGXJFQHL11l1rBde1pJhSA+N0Id1M9x+A=
+github.com/danielpaulus/go-ios v1.0.123/go.mod h1:tCOjUoiimx1wL8WJ7GoTF1bT9o4xAN1Fpif+vzSHXkc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa h1:RDBNVkRviHZtvDvId8XSGPu3rmpmSe+wKRcEWNgsfWU=
-github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA=
github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g=
github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
@@ -54,11 +54,14 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-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/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
-github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE=
+github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
@@ -86,6 +89,9 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
+github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs=
+github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -98,14 +104,16 @@ github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZO
github.com/pelletier/go-toml/v2 v2.0.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9CjgDb8Us=
github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
-github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@@ -114,7 +122,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
-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=
@@ -122,8 +129,9 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
@@ -143,28 +151,38 @@ github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7Jul
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.12.1 h1:nLkghSU8fQNaK7oUmDhQFsnrtcoNy7Z6LVFKsEecqgE=
go.mongodb.org/mongo-driver v1.12.1/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ=
+go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 h1:CCriYyAfq1Br1aIYettdHZTy8mBTIPo7We18TuO/bak=
+go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
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/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
-golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
+golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
+golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
+golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+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-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
-golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
+golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
+golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
-golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
+golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -173,10 +191,11 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
-golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
+golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -186,25 +205,27 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
-golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
+golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
+golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
-google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
-google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
+google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
-gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
@@ -212,6 +233,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
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=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-howett.net/plist v0.0.0-20200419221736-3b63eb3a43b5 h1:AQkaJpH+/FmqRjmXZPELom5zIERYZfwTjnHpfoVMQEc=
-howett.net/plist v0.0.0-20200419221736-3b63eb3a43b5/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
+howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
+howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
+software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k=
+software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
diff --git a/hub/devices/devices.go b/hub/devices/devices.go
index 1fd5dee5..31366b51 100644
--- a/hub/devices/devices.go
+++ b/hub/devices/devices.go
@@ -28,37 +28,84 @@ func CalculateCanvasDimensions(device *models.Device) (canvasWidth string, canva
return
}
-var HubDevicesMap = make(map[string]*models.LocalHubDevice)
+type HubDevices struct {
+ Mu sync.Mutex
+ Devices map[string]*models.LocalHubDevice
+}
+
+var HubDevicesData HubDevices
+
+func InitHubDevicesData() {
+ HubDevicesData = HubDevices{
+ Devices: make(map[string]*models.LocalHubDevice),
+ }
+}
// Get the latest devices information from MongoDB each second
func GetLatestDBDevices() {
var latestDBDevices []models.Device
for {
- latestDBDevices = db.GetDevices()
+ latestDBDevices = db.GetDBDeviceNew()
+
+ HubDevicesData.Mu.Lock()
+ for udid, _ := range HubDevicesData.Devices {
+ found := false
+ for _, dbDevice := range latestDBDevices {
+ if dbDevice.UDID == udid {
+ found = true
+ break
+ }
+ }
+ if !found {
+ delete(HubDevicesData.Devices, udid)
+ }
+ }
+ HubDevicesData.Mu.Unlock()
+
for _, dbDevice := range latestDBDevices {
- hubDevice, ok := HubDevicesMap[dbDevice.UDID]
+ HubDevicesData.Mu.Lock()
+ hubDevice, ok := HubDevicesData.Devices[dbDevice.UDID]
if ok {
- hubDevice.Device = dbDevice
+ // Update data only if needed
+ if hubDevice.Device.OSVersion != dbDevice.OSVersion {
+ hubDevice.Device.OSVersion = dbDevice.OSVersion
+ }
+ if hubDevice.Device.Name != dbDevice.Name {
+ hubDevice.Device.Name = dbDevice.Name
+ }
+ if hubDevice.Device.ScreenWidth != dbDevice.ScreenWidth {
+ hubDevice.Device.ScreenWidth = dbDevice.ScreenWidth
+ }
+ if hubDevice.Device.ScreenHeight != dbDevice.ScreenHeight {
+ hubDevice.Device.ScreenHeight = dbDevice.ScreenHeight
+ }
+ if hubDevice.Device.Usage != dbDevice.Usage {
+ hubDevice.Device.Usage = dbDevice.Usage
+ }
+ if hubDevice.Device.Provider != dbDevice.Provider {
+ hubDevice.Device.Provider = dbDevice.Provider
+ }
} else {
- HubDevicesMap[dbDevice.UDID] = &models.LocalHubDevice{
+ HubDevicesData.Devices[dbDevice.UDID] = &models.LocalHubDevice{
Device: dbDevice,
IsRunningAutomation: false,
IsAvailableForAutomation: true,
LastAutomationActionTS: 0,
}
}
+ HubDevicesData.Mu.Unlock()
}
time.Sleep(1 * time.Second)
}
}
-var getDeviceMu sync.Mutex
+var getDeviceMu sync.RWMutex
func GetHubDeviceByUDID(udid string) *models.LocalHubDevice {
getDeviceMu.Lock()
defer getDeviceMu.Unlock()
- for _, hubDevice := range HubDevicesMap {
+ for _, hubDevice := range HubDevicesData.Devices {
if hubDevice.Device.UDID == udid {
return hubDevice
}
diff --git a/hub/gads-ui/src/Gads.js b/hub/gads-ui/src/Gads.js
index 830fec59..e1063d4f 100644
--- a/hub/gads-ui/src/Gads.js
+++ b/hub/gads-ui/src/Gads.js
@@ -1,16 +1,16 @@
import './Gads.css'
import DeviceSelection from './components/DeviceSelection/DeviceSelection'
-import {Routes, Route, Navigate} from 'react-router-dom'
+import { Routes, Route, Navigate } from 'react-router-dom'
import NavBar from './components/TopNavigationBar/TopNavigationBar'
import DeviceControl from './components/DeviceControl/DeviceControl'
import Login from './components/Login/Login'
-import {useContext, useEffect} from 'react'
+import { useContext, useEffect } from 'react'
import { Auth } from './contexts/Auth'
import AdminDashboard from './components/Admin/AdminDashboard'
import axiosInterceptor from './services/axiosInterceptor'
function Gads() {
- const {authToken, logout} = useContext(Auth)
+ const { authToken, logout } = useContext(Auth)
// Set the logout function from the Auth context on the axiosInterceptor to automatically logout on each 401
axiosInterceptor(logout)
@@ -21,17 +21,17 @@ function Gads() {
}, [])
if (!authToken) {
- return
+ return
}
return (
-
-
+
+
- }/>
- }/>
- }/>
- }/>
+ } />
+ } />
+ } />
+ } />
)
diff --git a/hub/gads-ui/src/components/Admin/AdminDashboard.css b/hub/gads-ui/src/components/Admin/AdminDashboard.css
deleted file mode 100644
index e69de29b..00000000
diff --git a/hub/gads-ui/src/components/Admin/AdminDashboard.js b/hub/gads-ui/src/components/Admin/AdminDashboard.js
index 02f55e33..ed595086 100644
--- a/hub/gads-ui/src/components/Admin/AdminDashboard.js
+++ b/hub/gads-ui/src/components/Admin/AdminDashboard.js
@@ -3,8 +3,9 @@ import Tab from '@mui/material/Tab';
import { useState } from "react";
import { Box } from "@mui/material";
import UsersAdministration from "./Users/UsersAdministration";
-import ProvidersAdministration from "./Providers/ProvidersAdministration";
import FilesAdministration from "./Files/FilesAdministration";
+import DevicesAdministration from './Devices/DevicesAdministration';
+import ProvidersAdministration from './Providers/ProvidersAdministration';
export default function AdminDashboard() {
@@ -38,7 +39,7 @@ export default function AdminDashboard() {
}}
>
+
- {currentTabIndex === 0 &&
}
- {currentTabIndex === 1 &&
}
- {currentTabIndex === 2 &&
}
+ {currentTabIndex === 0 &&
}
+ {currentTabIndex === 1 &&
}
+ {currentTabIndex === 2 &&
}
+ {currentTabIndex === 3 &&
}
)
}
\ No newline at end of file
diff --git a/hub/gads-ui/src/components/Admin/Devices/DevicesAdministration.js b/hub/gads-ui/src/components/Admin/Devices/DevicesAdministration.js
new file mode 100644
index 00000000..cffbcb3f
--- /dev/null
+++ b/hub/gads-ui/src/components/Admin/Devices/DevicesAdministration.js
@@ -0,0 +1,639 @@
+import { useContext, useState, useEffect } from "react"
+import { api } from "../../../services/api"
+import { Box, Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, FormControl, Grid, MenuItem, Stack, TextField, Tooltip } from "@mui/material"
+import { Auth } from "../../../contexts/Auth"
+import CircularProgress from "@mui/material/CircularProgress";
+import CheckIcon from "@mui/icons-material/Check";
+import CloseIcon from "@mui/icons-material/Close";
+
+export default function DevicesAdministration() {
+ const [devices, setDevices] = useState([])
+ const [providers, setProviders] = useState([])
+ const { logout } = useContext(Auth)
+
+ function handleGetDeviceData() {
+ let url = `/admin/devices`
+
+ api.get(url)
+ .then(response => {
+ setDevices(response.data.devices)
+ setProviders(response.data.providers)
+ })
+ .catch(error => {
+ if (error.response) {
+ if (error.response.status === 401) {
+ logout()
+ }
+ }
+ })
+ }
+
+ useEffect(() => {
+ handleGetDeviceData()
+ }, [])
+
+ return (
+
+
+
+
+
+
+
+ {devices.map((device) => {
+ return (
+
+
+
+
+ )
+ })
+ }
+
+
+
+ )
+}
+
+function NewDevice({ providers, handleGetDeviceData }) {
+ const [udid, setUdid] = useState('')
+ const [provider, setProvider] = useState('')
+ const [os, setOS] = useState('')
+ const [name, setName] = useState('')
+ const [osVersion, setOSVersion] = useState('')
+ const [screenHeight, setScreenHeight] = useState('')
+ const [screenWidth, setScreenWidth] = useState('')
+ const [usage, setUsage] = useState('enabled')
+ const [type, setType] = useState('real')
+
+ const [loading, setLoading] = useState(false);
+ const [addDeviceStatus, setAddDeviceStatus] = useState(null)
+
+ function handleAddDevice(event) {
+ setLoading(true)
+ setAddDeviceStatus(null)
+ event.preventDefault()
+
+ let url = `/admin/device`
+
+ const deviceData = {
+ udid: udid,
+ name: name,
+ os_version: osVersion,
+ provider: provider,
+ screen_height: screenHeight,
+ screen_width: screenWidth,
+ os: os,
+ usage: usage,
+ device_type: type
+ }
+
+ api.post(url, deviceData)
+ .then(() => {
+ setAddDeviceStatus('success')
+ setUdid('')
+ setProvider('')
+ setOS('')
+ setName('')
+ setOSVersion('')
+ setScreenHeight('')
+ setScreenWidth('')
+ setUsage('enabled')
+ })
+ .catch(() => {
+ setAddDeviceStatus('error')
+ })
+ .finally(() => {
+ setTimeout(() => {
+ setLoading(false)
+ handleGetDeviceData()
+ setTimeout(() => {
+ setAddDeviceStatus(null)
+ }, 2000)
+ }, 1000)
+ })
+ }
+
+ return (
+
+ }
+ arrow
+ placement='top'
+ >
+ setUdid(event.target.value)}
+ />
+
+
+ setName(event.target.value)}
+ />
+
+
+ setOSVersion(event.target.value)}
+ />
+
+ Device screen width For Android - go to `https://whatismyandroidversion.com` and use the displayed `Screen size`, not `Viewport size` For iOS - you can get it on https://whatismyviewport.com (ScreenSize: at the bottom)}
+ arrow
+ placement='top'
+ >
+ setScreenWidth(event.target.value)}
+ />
+
+ Device screen height For Android - go to `https://whatismyandroidversion.com` and use the displayed `Screen size`, not `Viewport size` For iOS - you can get it on https://whatismyviewport.com (ScreenSize: at the bottom)}
+ arrow
+ placement='top'
+ >
+ setScreenHeight(event.target.value)}
+ />
+
+ Intended usage of the device Enabled: Can be used for automation and remote control Automation: Can be used only as automation target Remote control: Can be used only for remote control testing Disabled: Device will not be provided}
+ arrow
+ placement='top'
+ >
+
+ setUsage(e.target.value)}
+ select
+ label="Device usage"
+ required
+ >
+ Enabled
+ Automation
+ Remote control
+ Disabled
+
+
+
+
+
+ setProvider(e.target.value)}
+ select
+ label="Provider"
+ required
+ >
+ {providers.map((providerName) => {
+ return (
+ {providerName}
+ )
+ })
+ }
+
+
+
+
+ {loading ? (
+
+ ) : addDeviceStatus === 'success' ? (
+
+ ) : addDeviceStatus === 'error' ? (
+
+ ) : (
+ 'Add device'
+ )}
+
+ All updates to existing devices require respective provider restart
+
+
+
+ )
+}
+
+function ExistingDevice({ deviceData, providersData, handleGetDeviceData }) {
+ const [provider, setProvider] = useState(deviceData.provider)
+ const [os, setOS] = useState(deviceData.os)
+ const [name, setName] = useState(deviceData.name)
+ const [osVersion, setOSVersion] = useState(deviceData.os_version)
+ const [screenHeight, setScreenHeight] = useState(deviceData.screen_height)
+ const [screenWidth, setScreenWidth] = useState(deviceData.screen_width)
+ const [usage, setUsage] = useState(deviceData.usage)
+ const [type, setType] = useState(deviceData.device_type)
+ const udid = deviceData.udid
+
+ const [loading, setLoading] = useState(false);
+ const [updateDeviceStatus, setUpdateDeviceStatus] = useState(null)
+
+ useEffect(() => {
+ setProvider(deviceData.provider)
+ setOS(deviceData.os)
+ setName(deviceData.name)
+ setOSVersion(deviceData.os_version)
+ setScreenHeight(deviceData.screen_height)
+ setScreenWidth(deviceData.screen_width)
+ }, [deviceData])
+
+ function handleUpdateDevice(event) {
+ setLoading(true)
+ setUpdateDeviceStatus(null)
+ event.preventDefault()
+
+ let url = `/admin/device`
+
+ const reqData = {
+ udid: udid,
+ name: name,
+ os_version: osVersion,
+ provider: provider,
+ screen_height: screenHeight,
+ screen_width: screenWidth,
+ os: os,
+ usage: usage,
+ device_type: type
+ }
+
+ api.put(url, reqData)
+ .then(() => {
+ setUpdateDeviceStatus('success')
+ })
+ .catch(() => {
+ setUpdateDeviceStatus('error')
+ })
+ .finally(() => {
+ setTimeout(() => {
+ setLoading(false)
+ handleGetDeviceData()
+ setTimeout(() => {
+ setUpdateDeviceStatus(null)
+ }, 2000)
+ }, 1000)
+ })
+ }
+
+ function handleDeleteDevice(event) {
+ event.preventDefault()
+
+ let url = `/admin/device/${udid}`
+
+ api.delete(url)
+ .catch(e => {
+ })
+ .finally(() => {
+ handleGetDeviceData()
+ setOpenAlert(false)
+ })
+ }
+
+ const [openAlert, setOpenAlert] = useState(false)
+
+ return (
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/hub/gads-ui/src/components/Admin/Files/UploadSeleniumJar.js b/hub/gads-ui/src/components/Admin/Files/UploadSeleniumJar.js
index a593b8fa..2a797743 100644
--- a/hub/gads-ui/src/components/Admin/Files/UploadSeleniumJar.js
+++ b/hub/gads-ui/src/components/Admin/Files/UploadSeleniumJar.js
@@ -58,7 +58,12 @@ export default function UploadSeleniumJar() {
setAlertSeverity('error')
setAlertText('Failed uploading Selenium jar file')
setShowAlert(true)
- });
+ })
+ .finally(() => {
+ setTimeout(() => {
+ setShowAlert(false)
+ }, 5000)
+ })
}
}
@@ -66,7 +71,12 @@ export default function UploadSeleniumJar() {
Upload Selenium jar
@@ -78,7 +88,7 @@ export default function UploadSeleniumJar() {
}
+ startIcon={isUploading ? null : }
style={{
backgroundColor: "#2f3b26",
color: "#9ba984",
@@ -90,12 +100,13 @@ export default function UploadSeleniumJar() {
type="file"
onChange={(event) => handleUpload(event)}
/>
- Select and upload
+ {isUploading ? (
+
+ ) : (
+ 'Select and upload'
+ )}
- {isUploading &&
-
- }
- {showAlert && {alertText} }
+ {showAlert && {alertText} }
)
}
\ No newline at end of file
diff --git a/hub/gads-ui/src/components/Admin/Providers/Provider/Provider.js b/hub/gads-ui/src/components/Admin/Providers/Provider/Provider.js
deleted file mode 100644
index 28585d2d..00000000
--- a/hub/gads-ui/src/components/Admin/Providers/Provider/Provider.js
+++ /dev/null
@@ -1,156 +0,0 @@
-import { Box, Skeleton, Stack } from "@mui/material";
-import ProviderConfig from "./ProviderConfig";
-import { useEffect, useState } from "react";
-import ProviderInfo from "./ProviderInfo";
-import ProviderDevice from "./ProviderDevice"
-import ProviderLogsTable from "./ProviderLogsTable/ProviderLogsTable";
-
-export default function Provider({ info }) {
- return (
-
-
-
-
-
-
-
-
-
- )
-}
-
-function InfoBox({ os, isOnline }) {
- return (
-
- )
-}
-
-
-function LiveProviderBox({ nickname, os }) {
- let infoSocket = null;
- let [devicesData, setDevicesData] = useState(null)
- const [isLoading, setIsLoading] = useState(true)
- const [isOnline, setIsOnline] = useState(false)
- const [providerData, setProviderData] = useState(null)
-
- useEffect(() => {
- // Use specific full address for local development, proxy does not seem to work okay
- // const evtSource = new EventSource(`http://192.168.1.6:10000/admin/provider/${nickname}/info`);
- const evtSource = new EventSource(`/admin/provider/${nickname}/info`);
-
- evtSource.onmessage = (event) => {
- let providerJSON = JSON.parse(event.data)
- setProviderData(providerJSON)
- setDevicesData(providerJSON.provided_devices)
-
- let unixTimestamp = new Date().getTime();
- let diff = unixTimestamp - providerJSON.last_updated
- if (diff > 4000) {
- setIsOnline(false)
- } else {
- setIsOnline(true)
- }
-
- if (isLoading) {
- setIsLoading(false)
- }
- }
-
- return () => {
- evtSource.close()
- }
- }, [])
-
- if (isLoading) {
- return (
-
- )
- } else {
- return (
-
-
-
-
- )
- }
-}
-
-function ProviderDevices({ devicesData, isOnline }) {
- if (!isOnline || devicesData === null) {
- return (
- No device data or provider offline
- )
- } else {
- return (
- <>
-
- {devicesData.map((device) => {
- return (
-
-
- )
- })
- }
-
- >
- )
- }
-}
\ No newline at end of file
diff --git a/hub/gads-ui/src/components/Admin/Providers/Provider/ProviderConfig.js b/hub/gads-ui/src/components/Admin/Providers/Provider/ProviderConfig.js
deleted file mode 100644
index 933d11b0..00000000
--- a/hub/gads-ui/src/components/Admin/Providers/Provider/ProviderConfig.js
+++ /dev/null
@@ -1,336 +0,0 @@
-import { Alert, Button, MenuItem, Select, Stack, TextField } from '@mui/material'
-import { useContext, useEffect, useState } from 'react'
-import { Auth } from '../../../../contexts/Auth'
-import { api } from '../../../../services/api.js'
-
-export default function ProviderConfig({ isNew, data, setProviders }) {
- useEffect(() => {
- if (data) {
- setOS(data.os)
- setHostAddress(data.host_address)
- setNickname(data.nickname)
- setPort(data.port)
- setAndroid(data.provide_android)
- setIos(data.provide_ios)
- setUseSeleniumGrid(data.use_selenium_grid)
- setSeleniumGrid(data.selenium_grid)
- setWdaBundleId(data.wda_bundle_id)
- setWdaRepoPath(data.wda_repo_path)
- setSupervisionPassword(data.supervision_password)
- setButtonText('Update')
- setUrlPath('update')
- setUseCustomWda(data.use_custom_wda)
- }
- }, [data])
- // Main
- const {logout} = useContext(Auth)
- // OS
- const [os, setOS] = useState('windows')
- // Host address
- const [hostAddress, setHostAddress] = useState('')
- // Nickname
- const [nickname, setNickname] = useState('')
- // Port
- const [port, setPort] = useState(0)
- // Provide Android
- const [android, setAndroid] = useState(false)
- // Provide iOS
- const [ios, setIos] = useState(false)
- // Use Selenium Grid
- const [useSeleniumGrid, setUseSeleniumGrid] = useState(false)
- // Selenium Grid
- const [seleniumGrid, setSeleniumGrid] = useState('')
- // Supervision password
- const [supervisionPassword, setSupervisionPassword] = useState('')
- // Custom WebDriverAgent
- const [useCustomWda, setUseCustomWda] = useState(false)
- // WebDriverAgent bundle id
- const [wdaBundleId, setWdaBundleId] = useState('')
- // WebDriverAgent repo path - MacOS
- const [wdaRepoPath, setWdaRepoPath] = useState('')
- // Error
- const [showError, setShowError] = useState(false)
- const [errorText, setErrorText] = useState('')
- // Button
- const [buttonText, setButtonText] = useState('Add')
- // URL path
- const [urlPath, setUrlPath] = useState('add')
-
- // On successful provider creation reset the form data
- function resetForm() {
- setOS('windows')
- setHostAddress('')
- setNickname('')
- setPort(0)
- setAndroid(false)
- setIos(false)
- setUseSeleniumGrid(false)
- setSeleniumGrid('')
- setSupervisionPassword('')
- setWdaBundleId('')
- setWdaRepoPath('')
- setUseCustomWda(false)
- }
-
- // On pressing Add/Update
- function handleAddClick() {
- setShowError(false)
- let url = `/admin/providers/${urlPath}`
- let bodyString = buildPayload()
-
- api.post(url, bodyString, {})
- .then(response => {
- if (isNew) {
- resetForm()
- }
- if (urlPath === 'add') {
- setProviders(response.data)
- }
- })
- .catch(error => {
- if (error.response) {
- if (error.response.status === 401) {
- logout()
- return
- }
- handleError(error.response.data.error)
- return
- }
- handleError('Failure')
- })
- }
-
- // Create the payload for adding/updating provider request
- function buildPayload() {
- let body = {}
- body.os = os
- body.host_address = hostAddress
- body.nickname = nickname
- body.port = port
- body.provide_android = android
- body.provide_ios = ios
- if (ios) {
- body.wda_bundle_id = wdaBundleId
- body.wda_repo_path = wdaRepoPath
- body.supervision_password = supervisionPassword
- body.use_custom_wda = useCustomWda
- }
- body.use_selenium_grid = useSeleniumGrid
- if (useSeleniumGrid) {
- body.selenium_grid = seleniumGrid
- }
-
- let bodyString = JSON.stringify(body)
- return bodyString
- }
-
- function handleError(msg) {
- setErrorText(msg)
- setShowError(true)
- }
-
- return (
-
-
-
- setOS(event.target.value)}
- style={{ width: '100%', height: '40px' }}
- disabled={!isNew}
- >
- Windows
- Linux
- MacOS
-
- setNickname(e.target.value)}
- label='Nickname'
- required
- id='outlined-required'
- autoComplete='off'
- helperText='Unique nickname for the provider'
- fullWidth
- size='small'
- value={nickname}
- disabled={!isNew}
- />
- setHostAddress(e.target.value)}
- label='Host address'
- required
- id='outlined-required'
- autoComplete='off'
- helperText='Local IP address of the provider host without scheme, e.g. 192.168.1.10'
- fullWidth
- size='small'
- value={hostAddress}
- InputLabelProps={{ style: { fontSize: 14 } }}
- />
- setPort(Number(e.target.value))}
- label='Port'
- required
- id='outlined-required'
- autoComplete='off'
- helperText='The port on which you want the provider instance to run'
- fullWidth
- size='small'
- value={port}
- />
- Provide Android?
- setAndroid(event.target.value)}
- style={{
- width: '100%',
- height: '40px'
- }}
- >
- Yes
- No
-
- Provide iOS?
- setIos(event.target.value)}
- disabled={os === 'windows'}
- style={{
- width: '100%',
- height: '40px'
- }}
- >
- Yes
- No
-
-
-
- setWdaBundleId(e.target.value)}
- label='WebDriverAgent bundle ID'
- required
- id='outlined-required'
- autoComplete='off'
- disabled={!ios}
- helperText='Bundle ID of the prebuilt WebDriverAgent.ipa, used by `go-ios` to start it'
- value={wdaBundleId}
- size='small'
- fullWidth
- />
- setWdaRepoPath(e.target.value)}
- label='WebDriverAgent repo path'
- required
- id='outlined-required'
- autoComplete='off'
- helperText='Path on the host to the WebDriverAgent repo to build from, e.g. /Users/shamanec/WebDriverAgent-5.8.3'
- disabled={!ios || (ios && os !== 'darwin')}
- value={wdaRepoPath}
- size='small'
- fullWidth
- />
- setSupervisionPassword(e.target.value)}
- label='Supervision password'
- id='outlined-required'
- autoComplete='off'
- helperText='Password for the supervision profile for iOS devices(leave empty if devices not supervised)'
- disabled={!ios}
- value={supervisionPassword}
- size='small'
- fullWidth
- />
- Use custom WebDriverAgent?
- setUseCustomWda(event.target.value)}
- style={{
- width: '100%',
- height: '40px'
- }}
- >
- Yes
- No
-
- Use Selenium Grid?
- setUseSeleniumGrid(event.target.value)}
- style={{
- width: '100%',
- height: '40px'
- }}
- >
- Yes
- No
-
- setSeleniumGrid(e.target.value)}
- label='Selenium Grid'
- required
- id='outlined-required'
- autoComplete='off'
- helperText='Address of the Selenium Grid instance, e.g. http://192.168.1.28:4444'
- disabled={!useSeleniumGrid}
- value={seleniumGrid}
- size='small'
- fullWidth
- />
-
-
-
- {buttonText}
- {showError &&
- {errorText}
- }
-
-
- )
-}
\ No newline at end of file
diff --git a/hub/gads-ui/src/components/Admin/Providers/Provider/ProviderDevice.js b/hub/gads-ui/src/components/Admin/Providers/Provider/ProviderDevice.js
deleted file mode 100644
index b30812e5..00000000
--- a/hub/gads-ui/src/components/Admin/Providers/Provider/ProviderDevice.js
+++ /dev/null
@@ -1,97 +0,0 @@
-import { Box, Button, Stack } from "@mui/material";
-import { useContext, useEffect, useState } from "react";
-import { Auth } from "../../../../contexts/Auth";
-import { api } from '../../../../services/api.js'
-
-export default function ProviderDevice({ deviceInfo }) {
- let img_src = deviceInfo.os === 'android' ? './images/android-logo.png' : './images/apple-logo.png'
- const [statusColor, setStatusColor] = useState('red')
- const [buttonDisabled, setButtonDisabled] = useState(false)
- const { logout } = useContext(Auth)
-
- useEffect(() => {
- if (deviceInfo.connected && deviceInfo.provider_state === 'live') {
- setStatusColor('green')
- } else if (deviceInfo.connected && deviceInfo.provider_state === 'preparing') {
- setStatusColor('orange')
- } else {
- setStatusColor('red')
- }
- if (deviceInfo.provider_state !== 'init') {
- setButtonDisabled(false)
- } else {
- setButtonDisabled(true)
- }
- })
-
- function handleResetClick() {
- let url = `/device/${deviceInfo.udid}/reset`
-
- api.post(url)
- .catch(error => {
- if (error.response) {
- if (error.response.status === 401) {
- logout()
- }
- }
- })
- }
-
- return (
-
-
-
-
- UDID
- {deviceInfo.udid}
- Last provider state: {deviceInfo.provider_state}
- Name: {deviceInfo.name}
- Width: {deviceInfo.screen_width}
- Height: {deviceInfo.screen_height}
- Reset
-
- )
-}
-
-function OSImage({ img_src }) {
- return (
-
- )
-}
\ No newline at end of file
diff --git a/hub/gads-ui/src/components/Admin/Providers/Provider/ProviderInfo.js b/hub/gads-ui/src/components/Admin/Providers/Provider/ProviderInfo.js
deleted file mode 100644
index 50be7016..00000000
--- a/hub/gads-ui/src/components/Admin/Providers/Provider/ProviderInfo.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import { Box, Stack } from "@mui/material"
-import { useEffect, useState } from "react"
-
-export default function ProviderInfo({ os, isOnline }) {
- const [statusColor, setStatusColor] = useState('')
- const [status, setStatus] = useState('')
- const logoPath = `./images/${os}-logo.png`
-
- useEffect(() => {
- if (isOnline) {
- setStatus('Online')
- setStatusColor('green')
- } else {
- setStatus('Offline')
- setStatusColor('red')
- }
- }, [isOnline])
-
- return (
-
-
-
- )
-}
-
-function Status({ logoPath, statusColor }) {
- return (
-
-
- Status
-
-
- )
-}
\ No newline at end of file
diff --git a/hub/gads-ui/src/components/Admin/Providers/Provider/ProviderLogsTable/ProviderLogsTable.js b/hub/gads-ui/src/components/Admin/Providers/ProviderLogsTable.js
similarity index 82%
rename from hub/gads-ui/src/components/Admin/Providers/Provider/ProviderLogsTable/ProviderLogsTable.js
rename to hub/gads-ui/src/components/Admin/Providers/ProviderLogsTable.js
index 1c66369a..1432238b 100644
--- a/hub/gads-ui/src/components/Admin/Providers/Provider/ProviderLogsTable/ProviderLogsTable.js
+++ b/hub/gads-ui/src/components/Admin/Providers/ProviderLogsTable.js
@@ -1,5 +1,5 @@
import { useContext, useState } from "react";
-import {Auth} from "../../../../../contexts/Auth";
+import { Auth } from "../../../contexts/Auth.js";
import {
Box,
Button,
@@ -10,13 +10,14 @@ import {
TableBody, TableCell,
TableContainer, TableFooter, TablePagination,
TableRow,
+ Tooltip,
useTheme
} from "@mui/material";
import FirstPageIcon from '@mui/icons-material/FirstPage';
import KeyboardArrowLeft from '@mui/icons-material/KeyboardArrowLeft';
import KeyboardArrowRight from '@mui/icons-material/KeyboardArrowRight';
import LastPageIcon from '@mui/icons-material/LastPage';
-import { api } from '../../../../../services/api.js'
+import { api } from '../../../services/api.js'
function TablePaginationActions(props) {
const theme = useTheme();
@@ -92,6 +93,7 @@ export default function ProviderLogsTable({ nickname }) {
api.get(url)
.then(response => {
setLogData(response.data)
+ setPage(0)
})
.catch(error => {
if (error.response) {
@@ -104,7 +106,7 @@ export default function ProviderLogsTable({ nickname }) {
}
return (
-
+
-
-
+
+
{(rowsPerPage > 0
- ? logData.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
- : logData
+ ? logData.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
+ : logData
).map((logEntry, index) => (
-
-
+
+
{logEntry.eventname}
-
- {logEntry.message}
-
+
+
+ {logEntry.message}
+
+
))}
{emptyRows > 0 && (
-
+
)}
diff --git a/hub/gads-ui/src/components/Admin/Providers/Providers.js b/hub/gads-ui/src/components/Admin/Providers/Providers.js
deleted file mode 100644
index 45e5c1fa..00000000
--- a/hub/gads-ui/src/components/Admin/Providers/Providers.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import { Box, Tab, Tabs } from "@mui/material"
-import { useState } from "react"
-import Provider from "./Provider/Provider"
-import ProviderConfig from "./Provider/ProviderConfig"
-
-export default function Providers({ providers, setProviders }) {
- const [currentTabIndex, setCurrentTabIndex] = useState(0)
-
- const handleTabChange = (e, tabIndex) => {
- setCurrentTabIndex(tabIndex)
- }
-
- return (
-
-
-
- {providers.map((provider) => {
- return (
-
- )
- })
- }
-
- {currentTabIndex === 0 && }
- {currentTabIndex !== 0 && }
-
-
- )
-}
\ No newline at end of file
diff --git a/hub/gads-ui/src/components/Admin/Providers/ProvidersAdministration.css b/hub/gads-ui/src/components/Admin/Providers/ProvidersAdministration.css
new file mode 100644
index 00000000..1416bc1c
--- /dev/null
+++ b/hub/gads-ui/src/components/Admin/Providers/ProvidersAdministration.css
@@ -0,0 +1,31 @@
+#outer-stack {
+ width: 100%;
+ margin-left: 10px;
+ margin-top: 10px;
+}
+
+#outer-box {
+ margin-bottom: 10px;
+ height: 80vh;
+ overflow-y: scroll;
+ border: 2px solid black;
+ border-radius: 10px;
+ box-shadow: inset 0 -10px 10px -10px #000000;
+ scrollbar-width: none;
+ margin-right: 10px;
+ width: 100%;
+}
+
+.provider-box {
+ border: 1px solid black;
+ width: 400px;
+ min-width: 400px;
+ max-width: 400px;
+ height: 790px;
+ border-radius: 5px;
+ background-color: #9ba984
+}
+
+.provider-box-stack {
+ padding: 10px;
+}
\ No newline at end of file
diff --git a/hub/gads-ui/src/components/Admin/Providers/ProvidersAdministration.js b/hub/gads-ui/src/components/Admin/Providers/ProvidersAdministration.js
index 735f7e2b..151c28b5 100644
--- a/hub/gads-ui/src/components/Admin/Providers/ProvidersAdministration.js
+++ b/hub/gads-ui/src/components/Admin/Providers/ProvidersAdministration.js
@@ -1,17 +1,20 @@
+import { Box, Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, FormControl, Grid, MenuItem, Stack, TextField, Tooltip } from "@mui/material"
+import { useContext, useEffect, useState } from "react"
+import { api } from "../../../services/api"
import { Auth } from "../../../contexts/Auth"
-import { useContext, useState, useEffect } from "react"
-import Providers from "./Providers";
-import Skeleton from '@mui/material/Skeleton';
-import { Box, Stack } from "@mui/material";
-import { api } from '../../../services/api.js'
+import ProviderLogsTable from "./ProviderLogsTable"
+import CircularProgress from "@mui/material/CircularProgress";
+import CheckIcon from "@mui/icons-material/Check";
+import CloseIcon from "@mui/icons-material/Close";
+import './ProvidersAdministration.css'
export default function ProvidersAdministration() {
- const { logout } = useContext(Auth)
const [providers, setProviders] = useState([])
- const [isLoading, setIsLoading] = useState(true)
- let url = `/admin/providers`
+ const { logout } = useContext(Auth)
+
+ function handleGetProvidersData() {
+ let url = `/admin/providers`
- useEffect(() => {
api.get(url)
.then(response => {
setProviders(response.data)
@@ -22,45 +25,686 @@ export default function ProvidersAdministration() {
logout()
}
}
- });
-
- setTimeout(() => {
- setIsLoading(false)
- }, 1500);
+ })
+ }
+ useEffect(() => {
+ handleGetProvidersData()
}, [])
return (
-
-
- {
- isLoading ? (
-
- ) : (
-
- )
- }
-
+
+
+
+
+
+
+
+ {providers.map((provider) => {
+ return (
+
+
+
+
+ )
+ })
+ }
+
+
+
+ )
+}
+
+function NewProvider({ handleGetProvidersData }) {
+ const [os, setOS] = useState('windows')
+ const [nickname, setNickname] = useState('')
+ const [hostAddress, setHostAddress] = useState('')
+ const [port, setPort] = useState(0)
+ const [ios, setIos] = useState(false)
+ const [android, setAndroid] = useState(false)
+ const [wdaRepoPath, setWdaRepoPath] = useState('')
+ const [wdaBundleId, setWdaBundleId] = useState('')
+ const [useCustomWda, setUseCustomWda] = useState(false)
+ const [useSeleniumGrid, setUseSeleniumGrid] = useState(false)
+ const [seleniumGridInstance, setSeleniumGridInstance] = useState('')
+ const [loading, setLoading] = useState(false);
+ const [addProviderStatus, setAddProviderStatus] = useState(null)
+ function buildPayload() {
+ let body = {}
+ body.os = os
+ body.host_address = hostAddress
+ body.nickname = nickname
+ body.port = port
+ body.provide_android = android
+ body.provide_ios = ios
+ if (ios) {
+ body.wda_bundle_id = wdaBundleId
+ body.wda_repo_path = wdaRepoPath
+ body.use_custom_wda = useCustomWda
+ }
+ body.use_selenium_grid = useSeleniumGrid
+ if (useSeleniumGrid) {
+ body.selenium_grid = seleniumGridInstance
+ }
+
+ let bodyString = JSON.stringify(body)
+ return bodyString
+ }
+
+ function handleAddProvider(event) {
+ setLoading(true)
+ setAddProviderStatus(null)
+ event.preventDefault()
+
+ let url = `/admin/providers/add`
+ let bodyString = buildPayload()
+
+ api.post(url, bodyString, {})
+ .then(() => {
+ setAddProviderStatus('success')
+ setOS('windows')
+ setNickname('')
+ setHostAddress('')
+ setPort(0)
+ setIos(false)
+ setAndroid(false)
+ setWdaRepoPath('')
+ setWdaBundleId('')
+ setUseCustomWda(false)
+ setUseSeleniumGrid(false)
+ setSeleniumGridInstance('')
+ })
+ .catch(() => {
+ setAddProviderStatus('error')
+ })
+ .finally(() => {
+ setTimeout(() => {
+ setLoading(false)
+ handleGetProvidersData()
+ setTimeout(() => {
+ setAddProviderStatus(null)
+ }, 2000)
+ }, 1000)
+ })
+ }
+
+ return (
+
+
+ )
+}
+function ExistingProvider({ providerData, handleGetProvidersData }) {
+ const [os, setOS] = useState(providerData.os)
+ const [nickname, setNickname] = useState(providerData.nickname)
+ const [hostAddress, setHostAddress] = useState(providerData.host_address)
+ const [port, setPort] = useState(providerData.port)
+ const [ios, setIos] = useState(providerData.provide_ios)
+ const [android, setAndroid] = useState(providerData.provide_android)
+ const [wdaRepoPath, setWdaRepoPath] = useState(providerData.wda_repo_path)
+ const [wdaBundleId, setWdaBundleId] = useState(providerData.wda_bundle_id)
+ const [useCustomWda, setUseCustomWda] = useState(providerData.use_custom_wda)
+ const [useSeleniumGrid, setUseSeleniumGrid] = useState(providerData.use_selenium_grid)
+ const [seleniumGridInstance, setSeleniumGridInstance] = useState(providerData.selenium_grid)
+
+ const [openAlert, setOpenAlert] = useState(false)
+ const [openLogsDialog, setOpenLogsDialog] = useState(false)
+
+ const [loading, setLoading] = useState(false);
+ const [updateProviderStatus, setUpdateProviderStatus] = useState(null)
+
+ function handleDeleteProvider(event) {
+ event.preventDefault()
+
+ let url = `/admin/providers/${nickname}`
+
+ api.delete(url)
+ .catch(e => {
+ })
+ .finally(() => {
+ handleGetProvidersData()
+ setOpenAlert(false)
+ })
+ }
+
+ function buildPayload() {
+ let body = {}
+ body.os = os
+ body.host_address = hostAddress
+ body.nickname = nickname
+ body.port = port
+ body.provide_android = android
+ body.provide_ios = ios
+ if (ios) {
+ body.wda_bundle_id = wdaBundleId
+ body.wda_repo_path = wdaRepoPath
+ body.use_custom_wda = useCustomWda
+ }
+ body.use_selenium_grid = useSeleniumGrid
+ if (useSeleniumGrid) {
+ body.selenium_grid = seleniumGridInstance
+ }
+
+ let bodyString = JSON.stringify(body)
+ return bodyString
+ }
+
+ function handleUpdateProvider(event) {
+ setLoading(true)
+ setUpdateProviderStatus(null)
+ event.preventDefault()
+
+ let url = `/admin/providers/update`
+ let bodyString = buildPayload()
+
+ api.post(url, bodyString, {})
+ .then(() => {
+ setUpdateProviderStatus('success')
+ })
+ .catch(() => {
+ setUpdateProviderStatus('error')
+ })
+ .finally(() => {
+ setTimeout(() => {
+ setLoading(false)
+ handleGetProvidersData()
+ setTimeout(() => {
+ setUpdateProviderStatus(null)
+ }, 2000)
+ }, 1000)
+ })
+ }
+
+ return (
+
+
+
)
-}
\ No newline at end of file
+
+}
+
diff --git a/hub/gads-ui/src/components/Admin/Users/AddUser.css b/hub/gads-ui/src/components/Admin/Users/AddUser.css
deleted file mode 100644
index 9f9d9f58..00000000
--- a/hub/gads-ui/src/components/Admin/Users/AddUser.css
+++ /dev/null
@@ -1,8 +0,0 @@
-.add-user {
- background-color: #9ba984;
- width: 300px;
- margin-left: 20px;
- margin-top: 20px;
- border-radius: 10px;
- height: 500px;
-}
\ No newline at end of file
diff --git a/hub/gads-ui/src/components/Admin/Users/AddUser.js b/hub/gads-ui/src/components/Admin/Users/AddUser.js
deleted file mode 100644
index 54239626..00000000
--- a/hub/gads-ui/src/components/Admin/Users/AddUser.js
+++ /dev/null
@@ -1,164 +0,0 @@
-import Stack from '@mui/material/Stack';
-import TextField from '@mui/material/TextField'
-import { useState, useContext } from 'react';
-import { Button } from '@mui/material';
-import { Alert } from '@mui/material';
-import MenuItem from '@mui/material/MenuItem';
-import './AddUser.css'
-import Select from '@mui/material/Select';
-import { Auth } from '../../../contexts/Auth';
-import { api } from '../../../services/api.js'
-
-
-export default function AddUser() {
- // Inputs
- const [username, setUsername] = useState()
- const [password, setPassword] = useState()
- const [role, setRole] = useState('user')
-
- // Submission button
- const [buttonDisabled, setButtonDisabled] = useState(true)
-
- // Alert
- const [showAlert, setShowAlert] = useState(false)
- const [alertText, setAlertText] = useState()
- const [alertSeverity, setAlertSeverity] = useState('error')
-
- // Validations
- const [passwordValid, setPasswordValid] = useState(false)
- const [usernameValid, setUsernameValid] = useState(false)
-
- // Form styles
- const [usernameColor, setUsernameColor] = useState('')
- const [passwordColor, setPasswordColor] = useState('')
-
- function showAlertWithTimeout(alertText, severity) {
- setAlertText(alertText)
- setShowAlert(true)
- setAlertSeverity(severity)
-
- setTimeout(() => {
- setShowAlert(false);
- }, 3000);
- }
-
- function validatePassword(password) {
- if (/^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{6,}$/.test(password)) {
- setPasswordColor('success')
- setPasswordValid(true)
- setButtonDisabled(!usernameValid)
-
- } else {
- setPasswordColor('error')
- setPasswordValid(false)
- setButtonDisabled(true)
- }
- }
-
- function validateUsername(username) {
- if (/^[a-zA-Z0-9._-]{4,}$/.test(username)) {
- setUsernameColor('success')
- setUsernameValid(true)
- setButtonDisabled(!passwordValid)
-
- } else {
- setUsernameColor('error')
- setUsernameValid(false)
- setButtonDisabled(true)
- }
- }
-
- function handleAddUser(event) {
- event.preventDefault()
-
- if (!usernameValid || !passwordValid) {
- showAlertWithTimeout('Invalid input', 'error')
- return
- }
-
- let url = `/admin/user`
-
- const loginData = {
- username: username,
- password: password,
- role: role
- };
-
- api.post(url, loginData)
- .then(response => {
- if (response.status !== 200) {
- return response.data.then(json => {
- showAlertWithTimeout(json.error, 'error')
- });
- }
- showAlertWithTimeout('Successfully added user', 'success')
- })
- .catch(e => {
- console.log(e)
- })
- }
-
- return (
-
- )
-}
\ No newline at end of file
diff --git a/hub/gads-ui/src/components/Admin/Users/UsersAdministration.css b/hub/gads-ui/src/components/Admin/Users/UsersAdministration.css
new file mode 100644
index 00000000..7f0d670e
--- /dev/null
+++ b/hub/gads-ui/src/components/Admin/Users/UsersAdministration.css
@@ -0,0 +1,35 @@
+#outer-stack {
+ width: 100%;
+ margin-left: 10px;
+ margin-top: 10px;
+}
+
+#outer-box {
+ margin-bottom: 10px;
+ height: 80vh;
+ overflow-y: scroll;
+ border: 2px solid black;
+ border-radius: 10px;
+ box-shadow: inset 0 -10px 10px -10px #000000;
+ scrollbar-width: none;
+ margin-right: 10px;
+ width: 100%;
+}
+
+#user-grid {
+ margin: 10px;
+}
+
+.user-box {
+ border: 1px solid black;
+ width: 400px;
+ min-width: 400px;
+ max-width: 400px;
+ height: 300px;
+ border-radius: 5px;
+ background-color: #9ba984;
+}
+
+#user-box-stack {
+ padding: 10px;
+}
\ No newline at end of file
diff --git a/hub/gads-ui/src/components/Admin/Users/UsersAdministration.js b/hub/gads-ui/src/components/Admin/Users/UsersAdministration.js
index 17de0abf..66162186 100644
--- a/hub/gads-ui/src/components/Admin/Users/UsersAdministration.js
+++ b/hub/gads-ui/src/components/Admin/Users/UsersAdministration.js
@@ -1,13 +1,319 @@
-import AddUser from "./AddUser"
+import {
+ Box,
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent, DialogContentText,
+ DialogTitle,
+ FormControl,
+ Grid,
+ MenuItem,
+ TextField, Tooltip
+} from "@mui/material";
import Stack from '@mui/material/Stack';
+import { api } from "../../../services/api";
+import { useContext, useEffect, useState } from "react";
+import { Auth } from "../../../contexts/Auth";
+import './UsersAdministration.css'
+import CircularProgress from '@mui/material/CircularProgress';
+import CheckIcon from '@mui/icons-material/Check';
+import CloseIcon from '@mui/icons-material/Close';
export default function UsersAdministration() {
+ const [userData, setUserData] = useState([])
+ const { logout } = useContext(Auth)
+
+ function handleGetUserData() {
+ let url = `/admin/users`
+ api.get(url)
+ .then(response => {
+ setUserData(response.data)
+ })
+ .catch(error => {
+ if (error.response) {
+ if (error.response.status === 401) {
+ logout()
+ }
+ }
+ })
+
+ }
+
+ useEffect(() => {
+ handleGetUserData()
+ }, [])
+
return (
-
-
+
+
+
+
+
+
+ {userData.map((user) => {
+ return (
+
+
+
+ )
+ })
+ }
+
+
)
+}
+
+function NewUser({ handleGetUserData }) {
+ const [username, setUsername] = useState('')
+ const [password, setPassword] = useState('')
+ const [role, setRole] = useState('user')
+ const [loading, setLoading] = useState(false);
+ const [addUserStatus, setAddUserStatus] = useState(null)
+
+ function handleAddUser(event) {
+ setLoading(true)
+ setAddUserStatus(null)
+ event.preventDefault()
+
+ let url = `/admin/user`
+
+ const loginData = {
+ username: username,
+ password: password,
+ role: role
+ };
+
+ api.post(url, loginData)
+ .then(() => {
+ setAddUserStatus('success')
+ setUsername('')
+ setPassword('')
+ setRole('user')
+ })
+ .catch(e => {
+ setAddUserStatus('error')
+ })
+ .finally(() => {
+ setTimeout(() => {
+ setLoading(false)
+ handleGetUserData()
+ setTimeout(() => {
+ setAddUserStatus(null)
+ }, 2000)
+ }, 1000)
+ })
+ }
+
+ return (
+
+
+
+ )
+}
+
+function ExistingUser({ user, handleGetUserData }) {
+ const [username, setUsername] = useState(user.username)
+ const [password, setPassword] = useState('')
+ const [role, setRole] = useState(user.role)
+ const [openAlert, setOpenAlert] = useState(false)
+ const [updateLoading, setUpdateLoading] = useState(false);
+ const [updateUserStatus, setUpdateUserStatus] = useState(null)
+
+ function handleUpdateUser(event) {
+ setUpdateLoading(true)
+ setUpdateUserStatus(null)
+ event.preventDefault()
+
+ let url = `/admin/user`
+
+ const loginData = {
+ username: username,
+ password: password,
+ role: role
+ };
+
+ api.put(url, loginData)
+ .then(() => {
+ setUpdateUserStatus('success')
+ setPassword('')
+ })
+ .catch(() => {
+ setUpdateUserStatus('error')
+ })
+ .finally(() => {
+ setTimeout(() => {
+ setUpdateLoading(false)
+ handleGetUserData()
+ setTimeout(() => {
+ setUpdateUserStatus(null)
+ }, 2000)
+ }, 1000)
+ })
+ }
+
+ function handleDeleteUser() {
+ let url = `/admin/user/${username}`
+
+ api.delete(url)
+ .then(() =>
+ handleGetUserData()
+ )
+ .catch()
+ .finally(() => {
+ setOpenAlert(false)
+ })
+ }
+
+ return (
+
+
+
+ )
}
\ No newline at end of file
diff --git a/hub/gads-ui/src/components/DeviceControl/DeviceControl.js b/hub/gads-ui/src/components/DeviceControl/DeviceControl.js
index fea7d29c..5ca54690 100644
--- a/hub/gads-ui/src/components/DeviceControl/DeviceControl.js
+++ b/hub/gads-ui/src/components/DeviceControl/DeviceControl.js
@@ -1,27 +1,36 @@
-import { useParams } from 'react-router-dom';
-import { useNavigate } from 'react-router-dom';
+import { useParams } from 'react-router-dom'
+import { useNavigate } from 'react-router-dom'
import StreamCanvas from './StreamCanvas'
-import { Skeleton, Stack } from '@mui/material';
-import { Button } from '@mui/material';
-import TabularControl from './Tabs/TabularControl';
-import { useContext, useEffect, useState } from 'react';
-import { Auth } from '../../contexts/Auth';
-import { DialogProvider } from './SessionDialogContext';
+import { Skeleton, Stack } from '@mui/material'
+import { Button } from '@mui/material'
+import TabularControl from './Tabs/TabularControl'
+import { useContext, useEffect, useState } from 'react'
+import { Auth } from '../../contexts/Auth'
+import { DialogProvider } from './SessionDialogContext'
import { api } from '../../services/api.js'
export default function DeviceControl() {
const { logout, userName } = useContext(Auth)
- const { id } = useParams();
- const navigate = useNavigate();
+ const { udid } = useParams()
+ const navigate = useNavigate()
const [deviceData, setDeviceData] = useState(null)
const [isLoading, setIsLoading] = useState(true)
+ let screenRatio = window.innerHeight / window.innerWidth
+
+ const healthUrl = `/device/${udid}/health`
+ let infoUrl = `/device/${udid}/info`
- let url = `/device/${id}/info`
let in_use_socket = null
useEffect(() => {
- api.get(url)
+ api.get(healthUrl)
+ .then((response) => {
+ return api.get(infoUrl)
+ })
.then(response => {
setDeviceData(response.data)
+ setInterval(() => {
+ setIsLoading(false)
+ }, 1000);
})
.catch(error => {
if (error.response) {
@@ -30,9 +39,8 @@ export default function DeviceControl() {
return
}
}
- console.log('Failed getting providers data' + error)
- navigate('/devices');
- });
+ // navigate('/devices')
+ })
if (in_use_socket) {
in_use_socket.close()
@@ -42,26 +50,29 @@ export default function DeviceControl() {
if (protocol === "https") {
wsType = "wss"
}
- let socketUrl = `${wsType}://${window.location.host}/devices/control/${id}/in-use`
- // let socketUrl = `${wsType}://192.168.1.6:10000/devices/control/${id}/in-use`
- in_use_socket = new WebSocket(socketUrl);
- if (in_use_socket.readyState === WebSocket.OPEN) {
- in_use_socket.send('ping');
- }
- const pingInterval = setInterval(() => {
+ let socketUrl = `${wsType}://${window.location.host}/devices/control/${udid}/in-use`
+ // let socketUrl = `${wsType}://192.168.68.109:10000/devices/control/${udid}/in-use`
+ in_use_socket = new WebSocket(socketUrl)
+ in_use_socket.onopen = () => {
+ console.log('In Use WebSocket connection opened');
+ };
+
+ in_use_socket.onclose = () => {
+ console.log('In Use WebSocket connection closed');
+ };
+
+ in_use_socket.onerror = (error) => {
+ console.error('In Use WebSocket error:', error);
+ };
+
+ in_use_socket.onmessage = (message) => {
if (in_use_socket.readyState === WebSocket.OPEN) {
- in_use_socket.send(userName);
+ in_use_socket.send(userName)
}
- }, 1000);
-
- setInterval(() => {
- setIsLoading(false)
- }, 2000);
+ }
return () => {
if (in_use_socket) {
- console.log('component unmounted, clearing itnerval and closing socket')
- clearInterval(pingInterval)
in_use_socket.close()
}
}
@@ -69,7 +80,7 @@ export default function DeviceControl() {
}, [])
const handleBackClick = () => {
- navigate('/devices');
+ navigate('/devices')
};
return (
@@ -104,8 +115,8 @@ export default function DeviceControl() {
style={{
backgroundColor: 'gray',
animationDuration: '1s',
- height: '950px',
- width: '500px',
+ height: (window.innerHeight * 0.7),
+ width: (window.innerHeight * 0.7) * screenRatio,
borderRadius: '30px'
}}
/>
@@ -114,7 +125,7 @@ export default function DeviceControl() {
style={{
backgroundColor: 'gray',
animationDuration: '1s',
- height: '850px',
+ height: (window.innerHeight * 0.7),
width: '100%',
marginRight: '10px'
}}
diff --git a/hub/gads-ui/src/components/DeviceControl/StreamCanvas.js b/hub/gads-ui/src/components/DeviceControl/StreamCanvas.js
index 20255c12..6640829e 100644
--- a/hub/gads-ui/src/components/DeviceControl/StreamCanvas.js
+++ b/hub/gads-ui/src/components/DeviceControl/StreamCanvas.js
@@ -1,5 +1,5 @@
import { Auth } from "../../contexts/Auth"
-import {useContext, useEffect} from "react"
+import { useContext, useEffect, useState } from "react"
import './StreamCanvas.css'
import { Button, Divider, Grid, Stack } from "@mui/material"
import HomeIcon from '@mui/icons-material/Home';
@@ -11,34 +11,52 @@ import { api } from '../../services/api.js'
export default function StreamCanvas({ deviceData }) {
const { authToken, logout } = useContext(Auth)
const { setDialog } = useDialog()
+ const [canvasSize, setCanvasSize] = useState({
+ width: 0,
+ height: 0
+ });
let deviceX = parseInt(deviceData.screen_width, 10)
let deviceY = parseInt(deviceData.screen_height, 10)
let screen_ratio = deviceX / deviceY
- let canvasHeight = (window.innerHeight * 0.7)
- let canvasWidth = (window.innerHeight * 0.7) * screen_ratio
const streamData = {
udid: deviceData.udid,
deviceX: deviceX,
deviceY: deviceY,
screen_ratio: screen_ratio,
- canvasHeight: canvasHeight,
- canvasWidth: canvasWidth
+ canvasHeight: canvasSize.height,
+ canvasWidth: canvasSize.width
}
let streamUrl = ""
if (deviceData.os === 'ios') {
- // streamUrl = `http://192.168.1.6:10000/device/${deviceData.udid}/ios-stream-mjpeg`
+ // streamUrl = `http://192.168.68.109:10000/device/${deviceData.udid}/ios-stream-mjpeg`
streamUrl = `/device/${deviceData.udid}/ios-stream-mjpeg`
} else {
- // streamUrl = `http://192.168.1.6:10000/device/${deviceData.udid}/android-stream-mjpeg`
+ // streamUrl = `http://192.168.68.109:10000/device/${deviceData.udid}/android-stream-mjpeg`
streamUrl = `/device/${deviceData.udid}/android-stream-mjpeg`
}
useEffect(() => {
+ const updateCanvasSize = () => {
+ let canvasHeight = window.innerHeight * 0.7
+ let canvasWidth = canvasHeight * screen_ratio
+
+ setCanvasSize({
+ width: canvasWidth,
+ height: canvasHeight
+ })
+ }
+
+ updateCanvasSize()
+
+ // Set resize listener
+ window.addEventListener('resize', updateCanvasSize);
+
return () => {
window.stop()
+ window.removeEventListener('resize', updateCanvasSize);
}
}, []);
@@ -56,18 +74,22 @@ export default function StreamCanvas({ deviceData }) {
>{deviceData.model}
diff --git a/hub/gads-ui/src/components/DeviceControl/Tabs/Actions/Apps/UninstallApp.js b/hub/gads-ui/src/components/DeviceControl/Tabs/Actions/Apps/UninstallApp.js
index 0d99c4de..785e90fb 100644
--- a/hub/gads-ui/src/components/DeviceControl/Tabs/Actions/Apps/UninstallApp.js
+++ b/hub/gads-ui/src/components/DeviceControl/Tabs/Actions/Apps/UninstallApp.js
@@ -1,11 +1,11 @@
-import { Box, Button, CircularProgress, FormControl, MenuItem, Select, Stack } from "@mui/material";
+import {Box, Button, CircularProgress, FormControl, MenuItem, Select, Stack} from "@mui/material";
import InstallMobileIcon from '@mui/icons-material/InstallMobile';
import './InstallApp.css'
-import { useContext, useState } from "react";
-import { Auth } from "../../../../../contexts/Auth";
-import { api } from '../../../../../services/api.js'
+import {useContext, useState} from "react";
+import {Auth} from "../../../../../contexts/Auth";
+import {api} from '../../../../../services/api.js'
-export default function UninstallApp({ udid, installedApps }) {
+export default function UninstallApp({udid, installedApps}) {
const [selectedAppUninstall, setSelectedAppUninstall] = useState('no-app')
const [uninstallButtonDisabled, setUninstallButtonDisabled] = useState(true)
const [isUninstalling, setIsUninstalling] = useState(false)
@@ -46,7 +46,7 @@ export default function UninstallApp({ udid, installedApps }) {
}
return (
-
+
@@ -84,21 +84,26 @@ export default function UninstallApp({ udid, installedApps }) {
}
+ startIcon={ }
id='install-button'
variant='contained'
disabled={uninstallButtonDisabled}
style={{
backgroundColor: "#2f3b26",
color: "#9ba984",
- fontWeight: "bold"
+ fontWeight: "bold",
+ width: '260px'
}}
- >Uninstall
- {isUninstalling &&
-
- }
+ >
+ {isUninstalling ? (
+
+ ) : (
+ 'Uninstall'
+ )}
+
+
-
+
)
}
\ No newline at end of file
diff --git a/hub/gads-ui/src/components/DeviceControl/Tabs/Actions/Apps/UploadAppFile.css b/hub/gads-ui/src/components/DeviceControl/Tabs/Actions/Apps/UploadAppFile.css
index dc9b21d2..bb48c966 100644
--- a/hub/gads-ui/src/components/DeviceControl/Tabs/Actions/Apps/UploadAppFile.css
+++ b/hub/gads-ui/src/components/DeviceControl/Tabs/Actions/Apps/UploadAppFile.css
@@ -1,19 +1,12 @@
#upload-wrapper {
display: flex;
flex-direction: column;
- /* border-radius: 10px; */
- /* border: 1px solid black; */
- align-items: left;
justify-content: center;
width: 300px;
padding: 10px;
background-color: #9ba984;
}
-#upload-button {
- width: 50%;
-}
-
#progress-indicator {
margin-left: 5px;
}
diff --git a/hub/gads-ui/src/components/DeviceControl/Tabs/Actions/Clipboard.js b/hub/gads-ui/src/components/DeviceControl/Tabs/Actions/Clipboard.js
index 55adac96..5eff66ad 100644
--- a/hub/gads-ui/src/components/DeviceControl/Tabs/Actions/Clipboard.js
+++ b/hub/gads-ui/src/components/DeviceControl/Tabs/Actions/Clipboard.js
@@ -51,10 +51,13 @@ export default function Clipboard({ deviceData }) {
color: "#9ba984",
fontWeight: "bold"
}}
- >Get clipboard
- {isGettingCb &&
-
- }
+ >
+ {isGettingCb ? (
+
+ ) : (
+ 'Get clipboard'
+ )}
+
{
if (response.status === 404) {
@@ -22,58 +29,146 @@ export default function Screenshot({ udid }) {
return response.data
})
.then(screenshotJson => {
- var imageBase64String = screenshotJson.value
- let image = document.getElementById('screenshot-image')
- image.src = "data:image/png;base64," + imageBase64String
- image = document.getElementById('screenshot-image')
- setWidth(image.width)
- setHeight(image.height)
+ imageBase64String = screenshotJson.value
+ })
+ .catch(() => {
+ setTakeScreenshotStatus('error')
})
- .catch(error => {
- console.log('could not take screenshot - ' + error)
+ .finally(() => {
+ setTimeout(() => {
+ setIsTakingScreenshot(false)
+ if (imageBase64String) {
+ createThumbnail(imageBase64String, (thumbnailBase64) => {
+ setScreenshots(prevScreenshots => [...prevScreenshots, { full: imageBase64String, thumbnail: thumbnailBase64 }])
+ })
+ }
+ setTimeout(() => {
+ setTakeScreenshotStatus(null)
+ }, 1000)
+ }, 500)
})
}
+ function createThumbnail(base64Image, callback) {
+ const img = new Image()
+ img.src = `data:image/png;base64,${base64Image}`
+ img.onload = () => {
+ const canvas = document.createElement('canvas')
+ const ctx = canvas.getContext('2d')
+
+ const height = 400
+ const width = (img.width * height) / img.height
+
+ canvas.width = width
+ canvas.height = height
+ ctx.drawImage(img, 0, 0, width, height)
+ const thumbnailBase64 = canvas.toDataURL('image/png').split(',')[1]
+ callback(thumbnailBase64)
+ }
+ }
+
+ const handlShowImageDialog = (image) => {
+ setSelectedImage(image)
+ setOpen(true)
+ }
+
+ const handleCloseImageDialog = () => {
+ setOpen(false)
+ }
+
+ const handleDeleteImage = (index) => {
+ setScreenshots(prevScreenshots => prevScreenshots.filter((_, i) => i !== index));
+ }
+
return (
takeScreenshot(udid)}
+ onClick={() => takeScreenshot()}
variant="contained"
style={{
- marginBottom: "10px",
- backgroundColor: "#2f3b26",
- color: "#9ba984",
- fontWeight: "bold"
+ backgroundColor: '#2f3b26',
+ color: '#9ba984',
+ fontWeight: 'bold',
+ width: '200px',
+ height: '40px',
+ boxShadow: 'none'
}}
- >Screenshot
-
+ {isTakingScreenshot ? (
+
+ ) : takeScreenshotStatus === 'error' ? (
+
+ ) : (
+ 'Take screenshot'
+ )}
+
+
-
+ {screenshots.map((screenshot, index) => (
+
+
+ handlShowImageDialog(`data:image/png;base64,${screenshot.full}`)}
+ />
+ handleDeleteImage(index)}
+ style={{
+ backgroundColor: '#2f3b26',
+ color: '#9ba984',
+ }}
+ >
+ Delete
+
+
+
+ ))}
+
+
+
+
+
+
-
-
-
+
+
- )
+ );
}
+export default React.memo(Screenshot)
diff --git a/hub/gads-ui/src/components/DeviceControl/Tabs/TabularControl.js b/hub/gads-ui/src/components/DeviceControl/Tabs/TabularControl.js
index cb645510..1d8bae48 100644
--- a/hub/gads-ui/src/components/DeviceControl/Tabs/TabularControl.js
+++ b/hub/gads-ui/src/components/DeviceControl/Tabs/TabularControl.js
@@ -9,6 +9,7 @@ export default function TabularControl({ deviceData }) {
const udid = deviceData.udid
const [currentTabIndex, setCurrentTabIndex] = useState(0)
+ const [screenshots, setScreenshots] = useState([]);
const handleTabChange = (e, tabIndex) => {
setCurrentTabIndex(tabIndex)
@@ -38,20 +39,20 @@ export default function TabularControl({ deviceData }) {
- {currentTabIndex === 1 && }
+ {currentTabIndex === 1 && }
{currentTabIndex === 0 && }
{currentTabIndex === 2 && }
diff --git a/hub/gads-ui/src/components/DeviceSelection/NewDeviceBox.css b/hub/gads-ui/src/components/DeviceSelection/DeviceBox.css
similarity index 100%
rename from hub/gads-ui/src/components/DeviceSelection/NewDeviceBox.css
rename to hub/gads-ui/src/components/DeviceSelection/DeviceBox.css
diff --git a/hub/gads-ui/src/components/DeviceSelection/NewDeviceBox.js b/hub/gads-ui/src/components/DeviceSelection/DeviceBox.js
similarity index 56%
rename from hub/gads-ui/src/components/DeviceSelection/NewDeviceBox.js
rename to hub/gads-ui/src/components/DeviceSelection/DeviceBox.js
index 09cee825..29890d7c 100644
--- a/hub/gads-ui/src/components/DeviceSelection/NewDeviceBox.js
+++ b/hub/gads-ui/src/components/DeviceSelection/DeviceBox.js
@@ -1,15 +1,15 @@
-import { Box, Stack, List, ListItemIcon, ListItem, ListItemText, Divider, Button } from "@mui/material";
-import HomeIcon from '@mui/icons-material/Home';
-import InfoIcon from '@mui/icons-material/Info';
-import AspectRatioIcon from '@mui/icons-material/AspectRatio';
-import PhoneAndroidIcon from '@mui/icons-material/PhoneAndroid';
-import PhoneIphoneIcon from '@mui/icons-material/PhoneIphone';
+import { Box, Stack, List, ListItemIcon, ListItem, ListItemText, Divider, Button } from "@mui/material"
+import HomeIcon from '@mui/icons-material/Home'
+import InfoIcon from '@mui/icons-material/Info'
+import AspectRatioIcon from '@mui/icons-material/AspectRatio'
+import PhoneAndroidIcon from '@mui/icons-material/PhoneAndroid'
+import PhoneIphoneIcon from '@mui/icons-material/PhoneIphone'
import { api } from '../../services/api.js'
-import { useNavigate } from 'react-router-dom';
+import { useNavigate } from 'react-router-dom'
import React, { useState } from 'react'
-import './NewDeviceBox.css'
+import './DeviceBox.css'
-export default function NewDeviceBox({ device, handleAlert }) {
+export default function DeviceBox({ device }) {
let img_src = device.info.os === 'android' ? './images/android-logo.png' : './images/apple-logo.png'
return (
@@ -48,42 +48,42 @@ export default function NewDeviceBox({ device, handleAlert }) {
>
-
+ {device.info.os === 'ios' ? (
+
+ ) : (
+
+ )}
-
+
-
+
- {device.info.os === 'ios' ? (
-
- ) : (
-
- )}
+
@@ -103,81 +103,119 @@ export default function NewDeviceBox({ device, handleAlert }) {
}
function DeviceStatus({ device }) {
- if (device.is_running_automation) {
- return (
-
- )
- } else if (device.in_use === true) {
- return (
-
-
Currently in use
-
{device.in_use_by}
-
- )
- } else if (device.info.available === true) {
+ if (device.info.usage === "disabled") {
return (
Available
- );
+ className='offline-status'
+ >Disabled
+ )
+ }
+
+ if (device.available) {
+ if (device.info.usage === "automation") {
+ if (device.is_running_automation) {
+ return (
+
+ )
+ } else {
+ return (
+ Automation only
+ )
+ }
+ }
+
+ if (device.info.usage === "enabled" || device.info.usage === "control") {
+ if (device.is_running_automation) {
+ return (
+
+ )
+ }
+ if (device.in_use === true) {
+ return (
+
+
Currently in use
+
{device.in_use_by}
+
+ )
+ } else {
+ return (
+ Available
+ )
+ }
+ }
} else {
return (
Offline
- );
+ )
}
}
-function UseButton({ device, handleAlert }) {
- // Difference between current time and last time the device was reported as healthy
- // let healthyDiff = (Date.now() - device.last_healthy_timestamp)
+function UseButton({ device }) {
const [loading, setLoading] = useState(false)
-
- const navigate = useNavigate();
+ const navigate = useNavigate()
function handleUseButtonClick() {
- setLoading(true);
- const url = `/device/${device.info.udid}/health`;
- api.get(url)
- .then(response => {
- if (response.status === 200) {
- navigate('/devices/control/' + device.info.udid, device);
- }
- })
- .catch(() => {
- handleAlert()
- })
- .finally(() => {
- setTimeout(() => {
- setLoading(false);
- }, 2000);
- });
+ setLoading(true)
+ setTimeout(() => {
+ navigate('/devices/control/' + device.info.udid, device)
+ }, 1000)
}
- const buttonDisabled = loading || !device.info.connected;
+ const buttonDisabled = loading || !device.info.connected
- if (device.is_running_automation || device.in_use) {
+ if (device.info.usage === "disabled") {
return (
In Use
+ >N/A
)
- } else if (device.info.available === true) {
- return (
-
- {loading ? : 'Use'}
-
- );
+ }
+
+ if (device.available) {
+ if (device.info.usage === "automation") {
+ return (
+ N/A
+ )
+ }
+ if (device.is_running_automation || device.in_use) {
+ return (
+ In Use
+ )
+ } else {
+ return (
+
+ {loading ? : 'Use'}
+
+ )
+ }
+
} else {
return (
N/A
- );
+ )
}
}
\ No newline at end of file
diff --git a/hub/gads-ui/src/components/DeviceSelection/DeviceSelection.js b/hub/gads-ui/src/components/DeviceSelection/DeviceSelection.js
index 15e7e107..9a4fca47 100644
--- a/hub/gads-ui/src/components/DeviceSelection/DeviceSelection.js
+++ b/hub/gads-ui/src/components/DeviceSelection/DeviceSelection.js
@@ -1,7 +1,5 @@
import React, { useEffect, useState, useContext } from 'react'
import './DeviceSelection.css'
-import Alert from '@mui/material/Alert';
-import Snackbar from '@mui/material/Snackbar';
import Box from '@mui/material/Box';
import TabPanel from '@mui/lab/TabPanel';
import TabContext from '@mui/lab/TabContext';
@@ -11,21 +9,11 @@ import Divider from '@mui/material/Divider';
import { OSFilterTabs, DeviceSearch } from './Filters'
import { Auth } from '../../contexts/Auth';
import { api } from '../../services/api.js'
-import NewDeviceBox from "./NewDeviceBox";
+import DeviceBox from "./DeviceBox";
export default function DeviceSelection() {
- // States
- const [devices, setDevices] = useState([]);
- const [showAlert, setShowAlert] = useState(false);
- const [timeoutId, setTimeoutId] = useState(null);
-
- let devicesSocket = null;
- let vertical = 'bottom'
- let horizontal = 'center'
-
const open = true
-
- // Authentication and session control
+ const [devices, setDevices] = useState([]);
const { logout } = useContext(Auth)
function CheckServerHealth() {
@@ -45,30 +33,15 @@ export default function DeviceSelection() {
})
}
- // Show a snackbar alert if device is unavailable
- function presentDeviceUnavailableAlert() {
- // Present the alert
- setShowAlert(true);
- // Clear the previous timeout if it exists
- clearTimeout(timeoutId);
- // Set a new timeout for the alert
- setTimeoutId(
- setTimeout(() => {
- setShowAlert(false);
- }, 3000)
- );
- }
-
useEffect(() => {
CheckServerHealth()
// Use specific full address for local development, proxy does not seem to work okay
- // const evtSource = new EventSource(`http://192.168.1.6:10000/available-devices`);
+ // const evtSource = new EventSource(`http://192.168.68.109:10000/available-devices`);
const evtSource = new EventSource(`/available-devices`);
evtSource.onmessage = (message) => {
let devicesJson = JSON.parse(message.data)
- console.log(devicesJson)
setDevices(devicesJson);
}
@@ -89,25 +62,13 @@ export default function DeviceSelection() {
>
- {showAlert && (
-
-
- Device is unavailable
-
-
- )}
)
}
-function OSSelection({ devices, handleAlert }) {
+function OSSelection({ devices }) {
const [currentTabIndex, setCurrentTabIndex] = useState(0);
const handleTabChange = (e, tabIndex) => {
@@ -131,7 +92,7 @@ function OSSelection({ devices, handleAlert }) {
alignItems='center'
className='filters-stack'
sx={{
- height: '500px',
+ height: '200px',
backgroundColor: '#9ba984',
borderRadius: '10px'
}}
@@ -171,7 +132,8 @@ function OSSelection({ devices, handleAlert }) {
>
-
)
@@ -191,9 +152,8 @@ function OSSelection({ devices, handleAlert }) {
} else if (currentTabIndex === 1 && device.info.os === 'android') {
return (
-
)
@@ -201,9 +161,8 @@ function OSSelection({ devices, handleAlert }) {
} else if (currentTabIndex === 2 && device.info.os === 'ios') {
return (
-
)
diff --git a/hub/gads-ui/src/components/DeviceSelection/Filters.js b/hub/gads-ui/src/components/DeviceSelection/Filters.js
index 27671ea8..601cb02b 100644
--- a/hub/gads-ui/src/components/DeviceSelection/Filters.js
+++ b/hub/gads-ui/src/components/DeviceSelection/Filters.js
@@ -12,18 +12,18 @@ export function OSFilterTabs({ currentTabIndex, handleTabChange }) {
TabIndicatorProps={{
style: {
background: "#2f3b26",
- height: "5px"
+ height: '5px'
}
}}
textColor='#f4e6cd'
sx={{
color: "#2f3b26",
- fontFamily: "Verdana"
+ fontFamily: 'Verdana'
}}
>
-
-
-
+
+
+
)
}
@@ -35,11 +35,12 @@ export function DeviceSearch({ keyUpFilterFunc }) {
keyUpFilterFunc()}
- placeholder="Search devices"
- className="custom-placeholder"
+ placeholder='Search devices'
+ className='custom-placeholder'
+ autoComplete='off'
>
)
diff --git a/hub/gads-ui/src/components/Login/Login.css b/hub/gads-ui/src/components/Login/Login.css
deleted file mode 100644
index b740bf58..00000000
--- a/hub/gads-ui/src/components/Login/Login.css
+++ /dev/null
@@ -1,34 +0,0 @@
-.top-wrapper {
- justify-content: center;
- align-items: center;
- display: flex;
- background-color: #f4e6cd;
- width: 100vw;
- /* 100% of the viewport width */
- height: 100vh;
-}
-
-.login-wrapper {
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- width: 50%;
-}
-
-.fancy-wrapper {
- width: 30%;
- height: 500px;
- background-color: #9ba984;
- display: flex;
- flex-direction: row;
- border-radius: 10px;
-}
-
-#funky-div {
- background: #9ba984;
- background: linear-gradient(62deg, rgba(38,199,127,1) 0%, rgba(200,137,0,1) 100%);
- width: 50%;
- border-top-left-radius: 10px;
- border-bottom-left-radius: 10px;
-}
\ No newline at end of file
diff --git a/hub/gads-ui/src/components/Login/Login.js b/hub/gads-ui/src/components/Login/Login.js
index 7f8888dc..f273db29 100644
--- a/hub/gads-ui/src/components/Login/Login.js
+++ b/hub/gads-ui/src/components/Login/Login.js
@@ -1,18 +1,18 @@
import { useState, useContext } from "react"
import { useNavigate } from "react-router-dom"
import { Auth } from "../../contexts/Auth"
-import './Login.css'
import TextField from '@mui/material/TextField'
import Button from '@mui/material/Button'
import Alert from '@mui/material/Alert'
import { api } from '../../services/api.js'
+import {Box, Stack} from "@mui/material";
export default function Login() {
- const [username, setUsername] = useState()
- const [password, setPassword] = useState()
+ const [username, setUsername] = useState('')
+ const [password, setPassword] = useState('')
const { login } = useContext(Auth)
const [showAlert, setShowAlert] = useState(false)
- const [alertText, setAlertText] = useState()
+ const [alertText, setAlertText] = useState('')
const navigate = useNavigate()
function toggleAlert(message) {
@@ -23,110 +23,119 @@ export default function Login() {
function handleLogin(event) {
event.preventDefault()
- let url = `/authenticate`
-
const loginData = {
username: username,
password: password,
}
+ let url = `/authenticate`
api.post(url, loginData)
.then(response => {
- if (response.status !== 200) {
- toggleAlert(response.data.error);
- throw new Error(response.data.error)
- } else {
- return response.data
- }
+ return response.data
})
.then(json => {
const sessionID = json.sessionID
login(sessionID, json.username, json.role)
navigate("/devices")
})
- .catch(e => {
- console.log("Login failed")
- console.log(e)
+ .catch((e) => {
+ if (e.response) {
+ if (e.response.status === 401) {
+ toggleAlert('Invalid credentials')
+ }
+ } else {
+ toggleAlert('Something went wrong')
+ }
+ })
+ .finally(() => {
+ setTimeout(() => {
+ setShowAlert(false)
+ }, 3000)
})
}
let gadsVersion = localStorage.getItem('gadsVersion') || 'unknown'
return (
-
-
-
-
-
+
+
+
+
- Please log in
-
- {gadsVersion.startsWith('v') ? gadsVersion : "DEV"}
-
-
-
-
+
+
+
+
)
}
\ No newline at end of file
diff --git a/hub/gads-ui/src/components/TopNavigationBar/TopNavigationBar.js b/hub/gads-ui/src/components/TopNavigationBar/TopNavigationBar.js
index df8f41a6..854e79f0 100644
--- a/hub/gads-ui/src/components/TopNavigationBar/TopNavigationBar.js
+++ b/hub/gads-ui/src/components/TopNavigationBar/TopNavigationBar.js
@@ -1,4 +1,4 @@
-import {useContext, useState} from 'react'
+import {useContext, useEffect, useState} from 'react'
import './TopNavigationBar.css'
import {NavLink} from 'react-router-dom'
import {Auth} from '../../contexts/Auth'
@@ -11,13 +11,12 @@ export default function NavBar() {
const [showAdmin, setShowAdmin] = useState(false)
- const roleFromStorage = localStorage.getItem('userRole')
-
- if (roleFromStorage === 'admin') {
- if (!showAdmin) {
+ useEffect(() => {
+ const roleFromStorage = localStorage.getItem('userRole')
+ if (roleFromStorage === 'admin') {
setShowAdmin(true)
}
- }
+ }, [])
let appVersion = localStorage.getItem('gadsVersion') || 'unknown'
diff --git a/hub/gads-ui/src/services/axios.js b/hub/gads-ui/src/services/axios.js
index fbb1494f..37d48520 100644
--- a/hub/gads-ui/src/services/axios.js
+++ b/hub/gads-ui/src/services/axios.js
@@ -2,7 +2,7 @@ import axios from 'axios'
export function GetAPIClient() {
const api = axios.create({
- // baseURL: `http://192.168.1.6:10000`
+ // baseURL: `http://192.168.68.109:10000`
baseURL: ``
})
diff --git a/hub/hub.go b/hub/hub.go
index e7084b45..69bd811c 100644
--- a/hub/hub.go
+++ b/hub/hub.go
@@ -7,11 +7,12 @@ import (
"GADS/hub/router"
"embed"
"fmt"
- log "github.com/sirupsen/logrus"
- "github.com/spf13/pflag"
"io/fs"
"os"
"path/filepath"
+
+ log "github.com/sirupsen/logrus"
+ "github.com/spf13/pflag"
)
//go:embed gads-ui/build
@@ -64,6 +65,7 @@ func StartHub(flags *pflag.FlagSet, appVersion string) {
// Create a new connection to MongoDB
db.InitMongoClient(mongoDB)
+ devices.InitHubDevicesData()
// Start a goroutine that continuously gets the latest devices data from MongoDB
go devices.GetLatestDBDevices()
// Start a goroutine to clean hanging grid sessions
diff --git a/hub/router/appiumgrid.go b/hub/router/appiumgrid.go
index fc6fe2e3..90ae1880 100644
--- a/hub/router/appiumgrid.go
+++ b/hub/router/appiumgrid.go
@@ -9,7 +9,6 @@ import (
"io"
"net/http"
"strings"
- "sync"
"time"
"github.com/Masterminds/semver"
@@ -53,8 +52,6 @@ type AppiumSessionResponse struct {
Value AppiumSessionValue `json:"value"`
}
-var devicesMapMu sync.Mutex
-
type SeleniumSessionErrorResponse struct {
Value SeleniumSessionErrorResponseValue `json:"value"`
}
@@ -69,8 +66,8 @@ type SeleniumSessionErrorResponseValue struct {
// And clean the automation session if no action was taken in the timeout limit
func UpdateExpiredGridSessions() {
for {
- devicesMapMu.Lock()
- for _, hubDevice := range devices.HubDevicesMap {
+ devices.HubDevicesData.Mu.Lock()
+ for _, hubDevice := range devices.HubDevicesData.Devices {
if hubDevice.LastAutomationActionTS <= (time.Now().UnixMilli()-hubDevice.AppiumNewCommandTimeout) && hubDevice.IsRunningAutomation {
hubDevice.IsRunningAutomation = false
hubDevice.IsAvailableForAutomation = true
@@ -80,7 +77,7 @@ func UpdateExpiredGridSessions() {
}
}
}
- devicesMapMu.Unlock()
+ devices.HubDevicesData.Mu.Unlock()
time.Sleep(3 * time.Second)
}
}
@@ -107,7 +104,7 @@ func AppiumGridMiddleware() gin.HandlerFunc {
// Check for available device
var foundDevice *models.LocalHubDevice
- foundDevice, err = findAvailableDevice(appiumSessionBody)
+ foundDevice, _ = findAvailableDevice(appiumSessionBody)
// If no device is available start checking each second for 60 seconds
// If no device is available after 60 seconds - return error
if foundDevice == nil {
@@ -118,7 +115,7 @@ func AppiumGridMiddleware() gin.HandlerFunc {
for {
select {
case <-ticker.C:
- foundDevice, err = findAvailableDevice(appiumSessionBody)
+ foundDevice, _ = findAvailableDevice(appiumSessionBody)
if foundDevice != nil {
break FOR_LOOP
}
@@ -137,7 +134,13 @@ func AppiumGridMiddleware() gin.HandlerFunc {
return
}
- devicesMapMu.Lock()
+ devices.HubDevicesData.Mu.Lock()
+ // Set device found as running automation and is not available for automation
+ // Before even starting the Appium session creation request
+ // Also set an automation action timestamp so that the goroutine does not reset it while session is being created
+ foundDevice.IsRunningAutomation = true
+ foundDevice.IsAvailableForAutomation = false
+ foundDevice.LastAutomationActionTS = time.Now().UnixMilli()
// Update the session timeout values if none were provided
if appiumSessionBody.Capabilities.FirstMatch[0].NewCommandTimeout != 0 {
foundDevice.AppiumNewCommandTimeout = appiumSessionBody.Capabilities.FirstMatch[0].NewCommandTimeout * 1000
@@ -146,14 +149,15 @@ func AppiumGridMiddleware() gin.HandlerFunc {
} else {
foundDevice.AppiumNewCommandTimeout = 60000
}
- devicesMapMu.Unlock()
+ devices.HubDevicesData.Mu.Unlock()
// Create a new request to the device target URL on its provider instance
proxyReq, err := http.NewRequest(c.Request.Method, fmt.Sprintf("http://%s/device/%s/appium%s", foundDevice.Device.Host, foundDevice.Device.UDID, strings.Replace(c.Request.URL.Path, "/grid", "", -1)), bytes.NewBuffer(sessionRequestBody))
if err != nil {
- devicesMapMu.Lock()
+ devices.HubDevicesData.Mu.Lock()
foundDevice.IsAvailableForAutomation = true
- devicesMapMu.Unlock()
+ foundDevice.IsRunningAutomation = false
+ devices.HubDevicesData.Mu.Unlock()
c.JSON(http.StatusInternalServerError, createErrorResponse("GADS failed to create http request to proxy the call to the device respective provider Appium session endpoint", "", err.Error()))
return
}
@@ -167,9 +171,10 @@ func AppiumGridMiddleware() gin.HandlerFunc {
client := &http.Client{}
resp, err := client.Do(proxyReq)
if err != nil {
- devicesMapMu.Lock()
+ devices.HubDevicesData.Mu.Lock()
foundDevice.IsAvailableForAutomation = true
- devicesMapMu.Unlock()
+ foundDevice.IsRunningAutomation = false
+ devices.HubDevicesData.Mu.Unlock()
c.JSON(http.StatusInternalServerError, createErrorResponse("GADS failed to failed to execute the proxy request to the device respective provider Appium session endpoint", "", err.Error()))
return
}
@@ -178,9 +183,10 @@ func AppiumGridMiddleware() gin.HandlerFunc {
// Read the response sessionRequestBody from the proxied request
proxiedSessionResponseBody, err := readBody(resp.Body)
if err != nil {
- devicesMapMu.Lock()
+ devices.HubDevicesData.Mu.Lock()
foundDevice.IsAvailableForAutomation = true
- devicesMapMu.Unlock()
+ foundDevice.IsRunningAutomation = false
+ devices.HubDevicesData.Mu.Unlock()
c.JSON(http.StatusInternalServerError, createErrorResponse("GADS failed to read the response sessionRequestBody of the proxied Appium session request", "", err.Error()))
return
}
@@ -189,16 +195,17 @@ func AppiumGridMiddleware() gin.HandlerFunc {
var proxySessionResponse AppiumSessionResponse
err = json.Unmarshal(proxiedSessionResponseBody, &proxySessionResponse)
if err != nil {
- devicesMapMu.Lock()
+ devices.HubDevicesData.Mu.Lock()
foundDevice.IsAvailableForAutomation = true
- devicesMapMu.Unlock()
+ foundDevice.IsRunningAutomation = false
+ devices.HubDevicesData.Mu.Unlock()
c.JSON(http.StatusInternalServerError, createErrorResponse("GADS failed to unmarshal the response sessionRequestBody of the proxied Appium session request", "", err.Error()))
return
}
- devicesMapMu.Lock()
+ devices.HubDevicesData.Mu.Lock()
foundDevice.SessionID = proxySessionResponse.Value.SessionID
- devicesMapMu.Unlock()
+ devices.HubDevicesData.Mu.Unlock()
// Copy the response back to the original client
for k, v := range resp.Header {
@@ -206,12 +213,10 @@ func AppiumGridMiddleware() gin.HandlerFunc {
}
c.Writer.WriteHeader(resp.StatusCode)
c.Writer.Write(proxiedSessionResponseBody)
- devicesMapMu.Lock()
- foundDevice.IsRunningAutomation = true
- foundDevice.IsAvailableForAutomation = false
+ devices.HubDevicesData.Mu.Lock()
foundDevice.LastAutomationActionTS = time.Now().UnixMilli()
foundDevice.InUseBy = "automation"
- devicesMapMu.Unlock()
+ devices.HubDevicesData.Mu.Unlock()
} else {
// If this is not a request for a new session
var sessionID = ""
@@ -255,9 +260,9 @@ func AppiumGridMiddleware() gin.HandlerFunc {
defer c.Request.Body.Close()
// Check if there is a device in the local session map for that session ID
- devicesMapMu.Lock()
+ devices.HubDevicesData.Mu.Lock()
foundDevice, err := getDeviceBySessionID(sessionID)
- devicesMapMu.Unlock()
+ devices.HubDevicesData.Mu.Unlock()
if err != nil {
c.JSON(http.StatusNotFound, createErrorResponse(fmt.Sprintf("No session ID `%s` is available to GADS, it timed out or something unexpected occurred", sessionID), "", ""))
return
@@ -291,19 +296,19 @@ func AppiumGridMiddleware() gin.HandlerFunc {
// If the request succeeded and was a delete request, remove the session ID from the map
if c.Request.Method == http.MethodDelete {
- devicesMapMu.Lock()
+ devices.HubDevicesData.Mu.Lock()
foundDevice.IsAvailableForAutomation = true
- devicesMapMu.Unlock()
+ devices.HubDevicesData.Mu.Unlock()
// Start a goroutine that will release the device after 10 seconds if no other actions were taken
go func() {
time.Sleep(10 * time.Second)
- devicesMapMu.Lock()
+ devices.HubDevicesData.Mu.Lock()
if foundDevice.LastAutomationActionTS <= (time.Now().UnixMilli() - 10000) {
foundDevice.SessionID = ""
foundDevice.IsRunningAutomation = false
foundDevice.InUseBy = ""
}
- devicesMapMu.Unlock()
+ devices.HubDevicesData.Mu.Unlock()
}()
}
@@ -335,7 +340,7 @@ func readBody(r io.Reader) ([]byte, error) {
}
func getDeviceBySessionID(sessionID string) (*models.LocalHubDevice, error) {
- for _, localDevice := range devices.HubDevicesMap {
+ for _, localDevice := range devices.HubDevicesData.Devices {
if localDevice.SessionID == sessionID {
return localDevice, nil
}
@@ -344,7 +349,7 @@ func getDeviceBySessionID(sessionID string) (*models.LocalHubDevice, error) {
}
func getDeviceByUDID(udid string) (*models.LocalHubDevice, error) {
- for _, localDevice := range devices.HubDevicesMap {
+ for _, localDevice := range devices.HubDevicesData.Devices {
if strings.EqualFold(localDevice.Device.UDID, udid) {
return localDevice, nil
}
@@ -353,8 +358,8 @@ func getDeviceByUDID(udid string) (*models.LocalHubDevice, error) {
}
func findAvailableDevice(appiumSessionBody AppiumSession) (*models.LocalHubDevice, error) {
- devicesMapMu.Lock()
- defer devicesMapMu.Unlock()
+ devices.HubDevicesData.Mu.Lock()
+ defer devices.HubDevicesData.Mu.Unlock()
var foundDevice *models.LocalHubDevice
@@ -384,10 +389,14 @@ func findAvailableDevice(appiumSessionBody AppiumSession) (*models.LocalHubDevic
strings.EqualFold(appiumSessionBody.DesiredCapabilities.AutomationName, "XCUITest") {
// Loop through all latest devices looking for an iOS device that is not currently `being prepared` for automation and the last time it was updated from provider was less than 3 seconds ago
- for _, localDevice := range devices.HubDevicesMap {
+ // Also device should not be disabled or for remote control only
+ for _, localDevice := range devices.HubDevicesData.Devices {
if strings.EqualFold(localDevice.Device.OS, "ios") &&
+ !localDevice.InUse &&
localDevice.Device.LastUpdatedTimestamp >= (time.Now().UnixMilli()-3000) &&
- localDevice.IsAvailableForAutomation {
+ localDevice.IsAvailableForAutomation &&
+ localDevice.Device.Usage != "control" &&
+ localDevice.Device.Usage != "disabled" {
availableDevices = append(availableDevices, localDevice)
}
}
@@ -397,11 +406,14 @@ func findAvailableDevice(appiumSessionBody AppiumSession) (*models.LocalHubDevic
strings.EqualFold(appiumSessionBody.DesiredCapabilities.AutomationName, "UiAutomator2") {
// Loop through all latest devices looking for an Android device that is not currently `being prepared` for automation and the last time it was updated from provider was less than 3 seconds ago
- for _, localDevice := range devices.HubDevicesMap {
+ // Also device should not be disabled or for remote control only
+ for _, localDevice := range devices.HubDevicesData.Devices {
if strings.EqualFold(localDevice.Device.OS, "android") &&
- !localDevice.IsRunningAutomation &&
+ !localDevice.InUse &&
localDevice.Device.LastUpdatedTimestamp >= (time.Now().UnixMilli()-3000) &&
- localDevice.IsAvailableForAutomation {
+ localDevice.IsAvailableForAutomation &&
+ localDevice.Device.Usage != "control" &&
+ localDevice.Device.Usage != "disabled" {
availableDevices = append(availableDevices, localDevice)
}
}
diff --git a/hub/router/handler.go b/hub/router/handler.go
index bbf8ea41..2c292f81 100644
--- a/hub/router/handler.go
+++ b/hub/router/handler.go
@@ -3,10 +3,11 @@ package router
import (
"GADS/hub/auth"
"GADS/hub/devices"
+ "path/filepath"
+
"github.com/gin-contrib/cors"
"github.com/gin-contrib/static"
"github.com/gin-gonic/gin"
- "path/filepath"
)
func HandleRequests() *gin.Engine {
@@ -35,6 +36,7 @@ func HandleRequests() *gin.Engine {
authGroup.GET("/available-devices", AvailableDevicesSSE)
authGroup.GET("/admin/provider/:nickname/info", ProviderInfoSSE)
authGroup.GET("/devices/control/:udid/in-use", DeviceInUseWS)
+ authGroup.POST("/provider-update", ProviderUpdate)
// Enable authentication on the endpoints below
authGroup.Use(auth.AuthMiddleware())
authGroup.GET("/appium-logs", GetAppiumLogs)
@@ -46,12 +48,17 @@ func HandleRequests() *gin.Engine {
authGroup.GET("/admin/providers", GetProviders)
authGroup.POST("/admin/providers/add", AddProvider)
authGroup.POST("/admin/providers/update", UpdateProvider)
+ authGroup.DELETE("/admin/providers/:nickname", DeleteProvider)
authGroup.GET("/admin/providers/logs", GetProviderLogs)
- authGroup.POST("/admin/devices/add", AddNewDevice)
+ authGroup.POST("/admin/device", AddDevice)
+ authGroup.PUT("/admin/device", UpdateDevice)
+ authGroup.DELETE("/admin/device/:udid", DeleteDevice)
+ authGroup.GET("/admin/devices", GetDevices)
authGroup.POST("/admin/user", AddUser)
+ authGroup.GET("/admin/users", GetUsers)
authGroup.POST("/admin/upload-selenium-jar", UploadSeleniumJar)
- authGroup.PUT("/admin/user") // TODO Update user
- authGroup.DELETE("/admin/user") // TODO Delete user
+ authGroup.PUT("/admin/user", UpdateUser)
+ authGroup.DELETE("/admin/user/:nickname", DeleteUser)
appiumGroup := r.Group("/grid")
appiumGroup.Use(AppiumGridMiddleware())
appiumGroup.Any("/*path")
diff --git a/hub/router/proxy.go b/hub/router/proxy.go
index b7803d06..4abc4170 100644
--- a/hub/router/proxy.go
+++ b/hub/router/proxy.go
@@ -34,7 +34,9 @@ func DeviceProxyHandler(c *gin.Context) {
Director: func(req *http.Request) {
udid := c.Param("udid")
req.URL.Scheme = "http"
- req.URL.Host = devices.GetHubDeviceByUDID(udid).Device.Host
+ devices.HubDevicesData.Mu.Lock()
+ req.URL.Host = devices.HubDevicesData.Devices[udid].Device.Host
+ devices.HubDevicesData.Mu.Unlock()
req.URL.Path = "/device/" + udid + path
},
Transport: proxyTransport,
diff --git a/hub/router/routes.go b/hub/router/routes.go
index 01ec0113..e47eac1b 100644
--- a/hub/router/routes.go
+++ b/hub/router/routes.go
@@ -7,20 +7,19 @@ import (
"GADS/provider/logger"
"encoding/json"
"fmt"
- "github.com/gobwas/ws"
- "github.com/gobwas/ws/wsutil"
- "go.mongodb.org/mongo-driver/bson"
- "go.mongodb.org/mongo-driver/mongo/options"
"io"
"net/http"
"path/filepath"
- "slices"
"sort"
"strconv"
"strings"
- "sync"
"time"
+ "github.com/gobwas/ws"
+ "github.com/gobwas/ws/wsutil"
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+
"github.com/gin-gonic/gin"
"go.mongodb.org/mongo-driver/mongo"
)
@@ -181,16 +180,17 @@ func AddUser(c *gin.Context) {
}
if user.Username == "" {
- user.Username = "New user"
+ BadRequest(c, "Empty username provided")
+ }
+
+ if user.Password == "" {
+ BadRequest(c, "Empty password provided")
}
dbUser, err := db.GetUserFromDB(user.Username)
if err != nil && err != mongo.ErrNoDocuments {
InternalServerError(c, "Failed checking for user in db - "+err.Error())
return
- } else {
- fmt.Println("User does not exist, creating")
- // ADD LOGGER HERE
}
if dbUser != (models.User{}) {
@@ -207,6 +207,60 @@ func AddUser(c *gin.Context) {
OK(c, "Successfully added user")
}
+func UpdateUser(c *gin.Context) {
+ var user models.User
+
+ body, err := io.ReadAll(c.Request.Body)
+ if err != nil {
+ InternalServerError(c, fmt.Sprintf("%s", err))
+ return
+ }
+
+ err = json.Unmarshal(body, &user)
+ if err != nil {
+ BadRequest(c, fmt.Sprintf("%s", err))
+ return
+ }
+
+ if user == (models.User{}) {
+ BadRequest(c, "Empty or invalid body")
+ return
+ }
+
+ dbUser, err := db.GetUserFromDB(user.Username)
+ if err != nil && err != mongo.ErrNoDocuments {
+ InternalServerError(c, "Failed checking for user in db - "+err.Error())
+ return
+ }
+
+ if dbUser == (models.User{}) {
+ BadRequest(c, "Cannot update non-existing user")
+ return
+ }
+
+ if user.Password == "" {
+ user.Password = dbUser.Password
+ }
+
+ err = db.AddOrUpdateUser(user)
+ if err != nil {
+ InternalServerError(c, fmt.Sprintf("Failed adding/updating user - %s", err))
+ return
+ }
+}
+
+func DeleteUser(c *gin.Context) {
+ nickname := c.Param("nickname")
+
+ err := db.DeleteUserDB(nickname)
+ if err != nil {
+ InternalServerError(c, "Failed to delete user - "+err.Error())
+ return
+ }
+
+ OK(c, "Successfully deleted user")
+}
+
func GetProviders(c *gin.Context) {
providers := db.GetProvidersFromDB()
if len(providers) == 0 {
@@ -229,7 +283,7 @@ func GetProviderInfo(c *gin.Context) {
}
func AddProvider(c *gin.Context) {
- var provider models.ProviderDB
+ var provider models.Provider
body, err := io.ReadAll(c.Request.Body)
if err != nil {
InternalServerError(c, fmt.Sprintf("%s", err))
@@ -291,7 +345,7 @@ func AddProvider(c *gin.Context) {
}
func UpdateProvider(c *gin.Context) {
- var provider models.ProviderDB
+ var provider models.Provider
body, err := io.ReadAll(c.Request.Body)
if err != nil {
InternalServerError(c, fmt.Sprintf("%s", err))
@@ -344,18 +398,23 @@ func UpdateProvider(c *gin.Context) {
OK(c, "Provider updated successfully")
}
+func DeleteProvider(c *gin.Context) {
+ nickname := c.Param("nickname")
+
+ err := db.DeleteProviderDB(nickname)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to delete provider from DB - %s", err)})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("Successfully deleted provider with nickname `%s` from DB", nickname)})
+}
+
func ProviderInfoSSE(c *gin.Context) {
nickname := c.Param("nickname")
c.Stream(func(w io.Writer) bool {
providerData, _ := db.GetProviderFromDB(nickname)
- dbDevices := db.GetDBDevicesUDIDs()
-
- for i, connectedDevice := range providerData.ConnectedDevices {
- if slices.Contains(dbDevices, connectedDevice.UDID) {
- providerData.ConnectedDevices[i].IsConfigured = true
- }
- }
jsonData, _ := json.Marshal(&providerData)
@@ -366,43 +425,9 @@ func ProviderInfoSSE(c *gin.Context) {
})
}
-func AddNewDevice(c *gin.Context) {
- var device models.Device
-
- payload, err := io.ReadAll(c.Request.Body)
- if err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid payload"})
- return
- }
-
- err = json.Unmarshal(payload, &device)
- if err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid payload"})
- return
- }
-
- err = db.UpsertDeviceDB(device)
- if err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to upsert device in DB"})
- return
- }
-
- c.JSON(http.StatusOK, gin.H{"message": "Added device in DB for the current provider"})
-}
-
-func getDBDevice(udid string) *models.Device {
- for _, dbDevice := range devices.HubDevicesMap {
- if dbDevice.Device.UDID == udid {
- return &dbDevice.Device
- }
- }
- return nil
-}
-
func DeviceInUseWS(c *gin.Context) {
udid := c.Param("udid")
- var mu sync.Mutex
conn, _, _, err := ws.UpgradeHTTP(c.Request, c.Writer)
if err != nil {
logger.ProviderLogger.LogError("device_in_use_ws", fmt.Sprintf("Failed upgrading device in-use websocket - %s", err))
@@ -413,16 +438,16 @@ func DeviceInUseWS(c *gin.Context) {
messageReceived := make(chan string)
defer close(messageReceived)
+ // Loop getting messages from the client
+ // To keep device in use
go func() {
for {
data, code, err := wsutil.ReadClientData(conn)
if err != nil {
- fmt.Println(err)
return
}
if code == 8 {
- close(messageReceived)
return
}
@@ -432,60 +457,74 @@ func DeviceInUseWS(c *gin.Context) {
}
}()
- //var timeout = time.After(2 * time.Second)
+ // Loop sending messages to client to keep the connection and avoid using setInterval in the UI
+ go func() {
+ for {
+ err := wsutil.WriteServerText(conn, []byte("ping"))
+ if err != nil {
+ fmt.Println("Write error " + err.Error())
+ return
+ }
+ time.Sleep(1 * time.Second)
+ }
+ }()
+
+ timer := time.NewTimer(2 * time.Second)
for {
select {
case userName := <-messageReceived:
- mu.Lock()
- devices.HubDevicesMap[udid].InUseTS = time.Now().UnixMilli()
- devices.HubDevicesMap[udid].InUseBy = userName
- mu.Unlock()
- case <-time.After(2 * time.Second):
- mu.Lock()
- devices.HubDevicesMap[udid].InUseTS = 0
- if devices.HubDevicesMap[udid].InUseBy != "automation" {
- devices.HubDevicesMap[udid].InUseBy = ""
+ devices.HubDevicesData.Mu.Lock()
+ devices.HubDevicesData.Devices[udid].InUseTS = time.Now().UnixMilli()
+ devices.HubDevicesData.Devices[udid].InUseBy = userName
+ devices.HubDevicesData.Mu.Unlock()
+ if !timer.Stop() {
+ <-timer.C
}
- mu.Unlock()
+ timer.Reset(2 * time.Second)
+ case <-timer.C:
+ devices.HubDevicesData.Mu.Lock()
+ devices.HubDevicesData.Devices[udid].InUseTS = 0
+ if devices.HubDevicesData.Devices[udid].InUseBy != "automation" {
+ devices.HubDevicesData.Devices[udid].InUseBy = ""
+ }
+ devices.HubDevicesData.Mu.Unlock()
return
}
}
}
-var availableMu sync.Mutex
-
func AvailableDevicesSSE(c *gin.Context) {
c.Stream(func(w io.Writer) bool {
- availableMu.Lock()
- for _, device := range devices.HubDevicesMap {
-
- if device.Device.Connected && device.Device.LastUpdatedTimestamp >= (time.Now().UnixMilli()-3000) {
- device.Device.Available = true
-
- if device.InUseTS >= (time.Now().UnixMilli() - 3000) {
- device.InUse = true
- } else {
- device.InUse = false
- }
- continue
- }
- device.InUse = false
- device.Device.Available = false
- }
-
+ devices.HubDevicesData.Mu.Lock()
// Extract the keys from the map and order them
var hubDeviceMapKeys []string
- for key := range devices.HubDevicesMap {
+ for key := range devices.HubDevicesData.Devices {
hubDeviceMapKeys = append(hubDeviceMapKeys, key)
}
sort.Strings(hubDeviceMapKeys)
var deviceList = []*models.LocalHubDevice{}
for _, key := range hubDeviceMapKeys {
- deviceList = append(deviceList, devices.HubDevicesMap[key])
+ if devices.HubDevicesData.Devices[key].Device.LastUpdatedTimestamp < (time.Now().UnixMilli()-3000) && devices.HubDevicesData.Devices[key].Device.Connected {
+ devices.HubDevicesData.Devices[key].Available = false
+ } else if devices.HubDevicesData.Devices[key].Device.ProviderState != "live" {
+ devices.HubDevicesData.Devices[key].Available = false
+ } else {
+ devices.HubDevicesData.Devices[key].Available = true
+ }
+ if devices.HubDevicesData.Devices[key].InUseTS > (time.Now().UnixMilli() - 3000) {
+ if !devices.HubDevicesData.Devices[key].InUse {
+ devices.HubDevicesData.Devices[key].InUse = true
+ }
+ } else {
+ if devices.HubDevicesData.Devices[key].InUse {
+ devices.HubDevicesData.Devices[key].InUse = false
+ }
+ }
+ deviceList = append(deviceList, devices.HubDevicesData.Devices[key])
}
- availableMu.Unlock()
+ devices.HubDevicesData.Mu.Unlock()
jsonData, _ := json.Marshal(deviceList)
c.SSEvent("", string(jsonData))
@@ -524,3 +563,188 @@ func UploadSeleniumJar(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Selenium jar uploaded successfully"})
}
+
+func AddDevice(c *gin.Context) {
+ reqBody, err := io.ReadAll(c.Request.Body)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to read request body - %s", err)})
+ return
+ }
+ defer c.Request.Body.Close()
+
+ var device models.Device
+ err = json.Unmarshal(reqBody, &device)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to unmarshal request body to struct - %s", err)})
+ return
+ }
+
+ dbDevices := db.GetDBDeviceNew()
+ for _, dbDevice := range dbDevices {
+ if dbDevice.UDID == device.UDID {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Device already exists in the DB"})
+ return
+ }
+ }
+
+ err = db.UpsertDeviceDB(device)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to upsert device in DB"})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"message": "Added device in DB"})
+}
+
+func UpdateDevice(c *gin.Context) {
+ reqBody, err := io.ReadAll(c.Request.Body)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to read request body - %s", err)})
+ return
+ }
+ defer c.Request.Body.Close()
+
+ var reqDevice models.Device
+ err = json.Unmarshal(reqBody, &reqDevice)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to unmarshal request body to struct - %s", err)})
+ return
+ }
+
+ dbDevices := db.GetDBDeviceNew()
+ for _, dbDevice := range dbDevices {
+ if dbDevice.UDID == reqDevice.UDID {
+ // Update only the relevant data and only if something has changed
+ if dbDevice.Provider != reqDevice.Provider {
+ dbDevice.Provider = reqDevice.Provider
+ }
+ if reqDevice.OS != "" && dbDevice.OS != reqDevice.OS {
+ dbDevice.OS = reqDevice.OS
+ }
+ if reqDevice.ScreenHeight != "" && dbDevice.ScreenHeight != reqDevice.ScreenHeight {
+ dbDevice.ScreenHeight = reqDevice.ScreenHeight
+ }
+ if reqDevice.ScreenWidth != "" && dbDevice.ScreenWidth != reqDevice.ScreenWidth {
+ dbDevice.ScreenWidth = reqDevice.ScreenWidth
+ }
+ if reqDevice.OSVersion != "" && dbDevice.OSVersion != reqDevice.OSVersion {
+ dbDevice.OSVersion = reqDevice.OSVersion
+ }
+ if reqDevice.Name != "" && reqDevice.Name != dbDevice.Name {
+ dbDevice.Name = reqDevice.Name
+ }
+
+ if reqDevice.Usage != "" && reqDevice.Usage != dbDevice.Usage {
+ dbDevice.Usage = reqDevice.Usage
+ }
+ err = db.UpsertDeviceDB(dbDevice)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to upsert device in DB"})
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{"message": "Successfully updated device in DB"})
+ return
+ }
+ }
+
+ c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("Device with udid `%s` does not exist in the DB", reqDevice.UDID)})
+}
+
+func DeleteDevice(c *gin.Context) {
+ udid := c.Param("udid")
+
+ err := db.DeleteDeviceDB(udid)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to delete device from DB - %s", err)})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("Successfully deleted device with udid `%s` from DB", udid)})
+}
+
+type AdminDeviceData struct {
+ Devices []models.Device `json:"devices"`
+ Providers []string `json:"providers"`
+}
+
+func GetDevices(c *gin.Context) {
+ dbDevices := db.GetDBDeviceNew()
+ providers := db.GetProvidersFromDB()
+
+ var providerNames []string
+ for _, provider := range providers {
+ providerNames = append(providerNames, provider.Nickname)
+ }
+
+ if len(dbDevices) == 0 {
+ dbDevices = []models.Device{}
+ }
+
+ var adminDeviceData = AdminDeviceData{
+ Devices: dbDevices,
+ Providers: providerNames,
+ }
+
+ c.JSON(http.StatusOK, adminDeviceData)
+}
+
+func ProviderUpdate(c *gin.Context) {
+ bodyBytes, err := io.ReadAll(c.Request.Body)
+ defer c.Request.Body.Close()
+ if err != nil {
+ // handle error if needed
+ }
+
+ var providerDeviceData models.ProviderData
+
+ err = json.Unmarshal(bodyBytes, &providerDeviceData)
+ if err != nil {
+ // handle error if needed
+ }
+
+ for _, providerDevice := range providerDeviceData.DeviceData {
+ devices.HubDevicesData.Mu.Lock()
+ hubDevice, ok := devices.HubDevicesData.Devices[providerDevice.UDID]
+ if ok {
+ // Set a timestamp to indicate last time info about the device was updated from the provider
+ providerDevice.LastUpdatedTimestamp = time.Now().UnixMilli()
+
+ // Check all DB related values so if you make a change in the DB for a device
+ // The provider pushing updates will not overwrite with something wrong
+ if providerDevice.Usage != hubDevice.Device.Usage {
+ providerDevice.Usage = hubDevice.Device.Usage
+ }
+ if providerDevice.Name != hubDevice.Device.Name {
+ providerDevice.Name = hubDevice.Device.Name
+ }
+ if providerDevice.OSVersion != hubDevice.Device.OSVersion {
+ providerDevice.OSVersion = hubDevice.Device.OSVersion
+ }
+ if providerDevice.ScreenWidth != hubDevice.Device.ScreenWidth {
+ providerDevice.ScreenWidth = hubDevice.Device.ScreenWidth
+ }
+ if providerDevice.ScreenHeight != hubDevice.Device.ScreenHeight {
+ providerDevice.ScreenHeight = hubDevice.Device.ScreenHeight
+ }
+ if providerDevice.Provider != hubDevice.Device.Provider {
+ providerDevice.Provider = hubDevice.Device.Provider
+ }
+
+ hubDevice.Device = providerDevice
+ }
+ devices.HubDevicesData.Mu.Unlock()
+ }
+
+ c.JSON(http.StatusOK, gin.H{})
+}
+
+func GetUsers(c *gin.Context) {
+ users := db.GetUsers()
+ // Clean up the passwords, not that the project is very secure but let's not send them
+ for i := range users {
+ users[i].Password = ""
+ }
+ fmt.Println(users)
+
+ c.JSON(http.StatusOK, users)
+}
diff --git a/main.go b/main.go
index 5b8df4ba..6006d57f 100644
--- a/main.go
+++ b/main.go
@@ -4,16 +4,15 @@ import (
"GADS/hub"
"GADS/provider"
"fmt"
- "github.com/spf13/cobra"
"os"
+
+ "github.com/spf13/cobra"
)
var AppVersion = "development"
func main() {
var rootCmd = &cobra.Command{Use: "GADS"}
- rootCmd.PersistentFlags().String("host-address", "localhost", "The IP address of the host machine")
- rootCmd.PersistentFlags().String("port", "", "The port on which the component should run")
rootCmd.PersistentFlags().String("mongo-db", "localhost:27017", "The address of the MongoDB instance")
// Hub Command
@@ -24,6 +23,8 @@ func main() {
hub.StartHub(cmd.Flags(), AppVersion)
},
}
+ hubCmd.Flags().String("host-address", "localhost", "The IP address of the host machine")
+ hubCmd.Flags().String("port", "", "The port on which the component should run")
hubCmd.Flags().String("ui-files-dir", "", "Directory where the UI static files will be unpacked and served from."+
"\nBy default app will try to use a temp dir on the host, use this flag only if you encounter issues with the temp folder."+
"\nAlso you need to have created the folder in advance!")
@@ -40,6 +41,7 @@ func main() {
providerCmd.Flags().String("nickname", "", "Nickname of the provider")
providerCmd.Flags().String("provider-folder", ".", "The folder where logs and other data will be stored")
providerCmd.Flags().String("log-level", "info", "The verbosity of the logs of the provider instance")
+ providerCmd.Flags().String("hub", "", "The address of the GADS hub instance")
rootCmd.AddCommand(providerCmd)
var versionCmd = &cobra.Command{
diff --git a/provider/config/config.go b/provider/config/config.go
index 41685f3a..83b244ff 100644
--- a/provider/config/config.go
+++ b/provider/config/config.go
@@ -5,26 +5,29 @@ import (
"GADS/common/models"
"bytes"
"fmt"
- "go.mongodb.org/mongo-driver/bson"
- "go.mongodb.org/mongo-driver/bson/primitive"
- "go.mongodb.org/mongo-driver/mongo/gridfs"
"io"
"log"
"os"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+ "go.mongodb.org/mongo-driver/mongo/gridfs"
)
-var Config = &models.ConfigJsonData{}
+var ProviderConfig = &models.Provider{}
-func SetupConfig(nickname, folder string) {
+func SetupConfig(nickname, folder, hubAddress string) {
provider, err := db.GetProviderFromDB(nickname)
if err != nil {
- log.Fatalf("Failed to gte provider data from DB - %s", err)
+ log.Fatalf("Failed to get provider data from DB - %s", err)
}
if provider.Nickname == "" {
log.Fatal("Provider with this nickname is not registered in the DB")
}
provider.ProviderFolder = folder
- Config.EnvConfig = provider
+ provider.HubAddress = hubAddress
+
+ ProviderConfig = &provider
}
func SetupSeleniumJar() error {
@@ -60,7 +63,7 @@ func SetupSeleniumJar() error {
}
// Create the filepath and remove the selenium jar if present
- filePath := fmt.Sprintf("%s/%s", Config.EnvConfig.ProviderFolder, "selenium.jar")
+ filePath := fmt.Sprintf("%s/%s", ProviderConfig.ProviderFolder, "selenium.jar")
err = os.Remove(filePath)
if err != nil {
fmt.Printf("There is no Selenium jar file located at `%s`, nothing to remove\n", filePath)
diff --git a/provider/devices/android.go b/provider/devices/android.go
index 2bf31a30..fe5530e9 100644
--- a/provider/devices/android.go
+++ b/provider/devices/android.go
@@ -42,7 +42,7 @@ func stopGadsStreamService(device *models.Device) {
func installGadsStream(device *models.Device) error {
logger.ProviderLogger.LogInfo("android_device_setup", fmt.Sprintf("Installing GADS-stream apk on device `%v`", device.UDID))
- cmd := exec.CommandContext(device.Context, "adb", "-s", device.UDID, "install", "-r", fmt.Sprintf("%s/gads-stream.apk", config.Config.EnvConfig.ProviderFolder))
+ cmd := exec.CommandContext(device.Context, "adb", "-s", device.UDID, "install", "-r", fmt.Sprintf("%s/gads-stream.apk", config.ProviderConfig.ProviderFolder))
err := cmd.Run()
if err != nil {
return fmt.Errorf("installGadsStream: Error executing `%s` - %s", cmd.Args, err)
@@ -150,14 +150,14 @@ func updateAndroidScreenSizeADB(device *models.Device) error {
}
// Get all installed apps on an Android device
-func getInstalledAppsAndroid(device *models.Device) []string {
+func GetInstalledAppsAndroid(device *models.Device) []string {
var installedApps []string
cmd := exec.CommandContext(device.Context, "adb", "-s", device.UDID, "shell", "cmd", "package", "list", "packages", "-3")
var outBuffer bytes.Buffer
cmd.Stdout = &outBuffer
if err := cmd.Run(); err != nil {
- device.Logger.LogError("get_installed_apps", fmt.Sprintf("getInstalledAppsAndroid: Error executing `%s` trying to get installed apps - %v", cmd.Args, err))
+ device.Logger.LogError("get_installed_apps", fmt.Sprintf("GetInstalledAppsAndroid: Error executing `%s` trying to get installed apps - %v", cmd.Args, err))
return installedApps
}
@@ -194,7 +194,7 @@ func uninstallAppAndroid(device *models.Device, packageName string) error {
// Install app on Android device by apk name
func installAppAndroid(device *models.Device, appName string) error {
- cmd := exec.CommandContext(device.Context, "adb", "-s", device.UDID, "install", "-r", fmt.Sprintf("%s/%s", config.Config.EnvConfig.ProviderFolder, appName))
+ cmd := exec.CommandContext(device.Context, "adb", "-s", device.UDID, "install", "-r", fmt.Sprintf("%s/%s", config.ProviderConfig.ProviderFolder, appName))
if err := cmd.Run(); err != nil {
device.Logger.LogError("install_app", fmt.Sprintf("installAppAndroid: Error executing `%s` trying to install app - %v", cmd.Args, err))
diff --git a/provider/devices/common.go b/provider/devices/common.go
index 6a4670c7..3a145511 100644
--- a/provider/devices/common.go
+++ b/provider/devices/common.go
@@ -6,8 +6,6 @@ import (
"context"
"encoding/json"
"fmt"
- "github.com/danielpaulus/go-ios/ios"
- "github.com/pelletier/go-toml/v2"
"io"
"log"
"net/http"
@@ -16,14 +14,19 @@ import (
"slices"
"strconv"
"strings"
+ "sync"
"time"
+ "github.com/danielpaulus/go-ios/ios"
+ "github.com/pelletier/go-toml/v2"
+
"GADS/common/constants"
"GADS/common/db"
"GADS/common/models"
"GADS/provider/config"
"GADS/provider/logger"
"GADS/provider/providerutil"
+
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
)
@@ -31,144 +34,168 @@ import (
var netClient = &http.Client{
Timeout: time.Second * 120,
}
-var DeviceMap = make(map[string]*models.Device)
+var DBDeviceMap = make(map[string]*models.Device)
func Listener() {
Setup()
+ DBDeviceMap = getDBProviderDevices()
+ setupDevices()
// Start updating devices each 10 seconds in a goroutine
go updateDevices()
- // Start updating the local devices data to Mongo in a goroutine
- go updateDevicesMongo()
+ // Start updating the local devices data to the hub in a goroutine
+ go updateProviderHub()
}
-func updateDevices() {
- ticker := time.NewTicker(5 * time.Second)
- defer ticker.Stop()
+func updateProviderHub() {
+ client := &http.Client{
+ Timeout: 5 * time.Second,
+ }
+ var updateFailureCounter = 1
+ var mu sync.Mutex
- for range ticker.C {
- connectedDevices := GetConnectedDevicesCommon()
+ for {
+ if updateFailureCounter >= 10 {
+ log.Fatalf("Unsuccessfully attempted to update device data in hub for 10 times, killing provider")
+ }
+ time.Sleep(1 * time.Second)
- // Loop through the connected devices
- for _, connectedDevice := range connectedDevices {
- // If a connected device is not already in the local devices map
- // Do the initial set up and add it
- if _, ok := DeviceMap[connectedDevice.UDID]; !ok {
- newDevice := &models.Device{}
- newDevice.UDID = connectedDevice.UDID
- newDevice.OS = connectedDevice.OS
- newDevice.ProviderState = "init"
- newDevice.IsResetting = false
- newDevice.Connected = true
-
- // Add default name for the device
- if connectedDevice.OS == "ios" {
- newDevice.Name = "iPhone"
- } else {
- newDevice.Name = "Android"
- }
+ mu.Lock()
- newDevice.Host = fmt.Sprintf("%s:%v", config.Config.EnvConfig.HostAddress, config.Config.EnvConfig.Port)
- newDevice.Provider = config.Config.EnvConfig.Nickname
- // Set N/A for model and OS version because we will set those during the device set up
- newDevice.Model = "N/A"
- newDevice.OSVersion = "N/A"
+ var properJson models.ProviderData
+ for _, dbDevice := range DBDeviceMap {
+ properJson.DeviceData = append(properJson.DeviceData, *dbDevice)
+ properJson.ProviderData = *config.ProviderConfig
+ }
+ mu.Unlock()
+ jsonData, err := json.Marshal(properJson)
+ if err != nil {
+ updateFailureCounter++
+ logger.ProviderLogger.LogError("update_provider_hub", "Failed marshaling provider data to json - "+err.Error())
+ continue
+ }
+ req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/provider-update", config.ProviderConfig.HubAddress), bytes.NewBuffer(jsonData))
+ if err != nil {
+ updateFailureCounter++
+ logger.ProviderLogger.LogError("update_provider_hub", "Failed to create request to update provider data in hub - "+err.Error())
+ continue
+ }
- // Check if a capped Appium logs collection already exists for the current device
- exists, err := db.CollectionExists("appium_logs", newDevice.UDID)
- if err != nil {
- logger.ProviderLogger.Warnf("Could not check if device collection exists in `appium_logs` db, will attempt to create it either way - %s", err)
- }
+ resp, err := client.Do(req)
+ if err != nil {
+ updateFailureCounter++
+ logger.ProviderLogger.LogError("update_provider_hub", fmt.Sprintf("Failed to execute request to update provider data in hub, hub is probably down, current retry counter is `%v` - %s", updateFailureCounter, err))
+ continue
+ }
- // If it doesn't exist - attempt to create it
- if !exists {
- err = db.CreateCappedCollection("appium_logs", newDevice.UDID, 30000, 30)
- if err != nil {
- logger.ProviderLogger.Errorf("updateDevices: Failed to create capped collection for device `%s` - %s", connectedDevice.UDID, err)
- continue
- }
- }
+ if resp.StatusCode != 200 {
+ updateFailureCounter++
+ logger.ProviderLogger.LogError("update_provider_hub", fmt.Sprintf("Executed request to update provider data in hub but it was not successful, current retry counter is `%v` - %s", updateFailureCounter, err))
+ continue
+ }
+ // Reset the counter if update went well
+ updateFailureCounter = 1
+ }
+}
- // Create an index model and add it to the respective device Appium log collection
- appiumCollectionIndexModel := mongo.IndexModel{
- Keys: bson.D{
- {
- Key: "ts", Value: constants.SortAscending},
- {
- Key: "session_id", Value: constants.SortAscending,
- },
- },
- }
- db.AddCollectionIndex("appium_logs", newDevice.UDID, appiumCollectionIndexModel)
-
- // Create logs directory for the device if it doesn't already exist
- if _, err := os.Stat(fmt.Sprintf("%s/device_%s", config.Config.EnvConfig.ProviderFolder, newDevice.UDID)); os.IsNotExist(err) {
- err = os.Mkdir(fmt.Sprintf("%s/device_%s", config.Config.EnvConfig.ProviderFolder, newDevice.UDID), os.ModePerm)
- if err != nil {
- logger.ProviderLogger.Errorf("updateDevices: Could not create logs folder for device `%s` - %s\n", newDevice.UDID, err)
- continue
- }
- }
+func setupDevices() {
+ for _, dbDevice := range DBDeviceMap {
+ dbDevice.ProviderState = "init"
+ dbDevice.Connected = false
+ dbDevice.LastUpdatedTimestamp = 0
+ dbDevice.IsResetting = false
- // Create a custom logger and attach it to the local device
- deviceLogger, err := logger.CreateCustomLogger(fmt.Sprintf("%s/device_%s/device.log", config.Config.EnvConfig.ProviderFolder, newDevice.UDID), newDevice.UDID)
- if err != nil {
- logger.ProviderLogger.Errorf("updateDevices: Could not create custom logger for device `%s` - %s\n", newDevice.UDID, err)
- continue
- }
- newDevice.Logger = *deviceLogger
+ dbDevice.Host = fmt.Sprintf("%s:%v", config.ProviderConfig.HostAddress, config.ProviderConfig.Port)
- appiumLogger, err := logger.NewAppiumLogger(fmt.Sprintf("%s/device_%s/appium.log", config.Config.EnvConfig.ProviderFolder, newDevice.UDID), newDevice.UDID)
- if err != nil {
- logger.ProviderLogger.Errorf("updateDevices: Could not create Appium logger for device `%s` - %s\n", newDevice.UDID, err)
- continue
- }
- newDevice.AppiumLogger = appiumLogger
+ // Check if a capped Appium logs collection already exists for the current device
+ exists, err := db.CollectionExists("appium_logs", dbDevice.UDID)
+ if err != nil {
+ logger.ProviderLogger.Warnf("Could not check if device collection exists in `appium_logs` db, will attempt to create it either way - %s", err)
+ }
- // Add the new local device to the map
- DeviceMap[connectedDevice.UDID] = newDevice
+ // If it doesn't exist - attempt to create it
+ if !exists {
+ err = db.CreateCappedCollection("appium_logs", dbDevice.UDID, 30000, 30)
+ if err != nil {
+ logger.ProviderLogger.Errorf("updateDevices: Failed to create capped collection for device `%s` - %s", dbDevice, err)
+ continue
}
}
- // Loop through the local devices map to remove any no longer connected devices
- for _, localDevice := range DeviceMap {
- isConnected := false
- for _, connectedDevice := range connectedDevices {
- if connectedDevice.UDID == localDevice.UDID {
- isConnected = true
- }
+ // Create an index model and add it to the respective device Appium log collection
+ appiumCollectionIndexModel := mongo.IndexModel{
+ Keys: bson.D{
+ {
+ Key: "ts", Value: constants.SortAscending},
+ {
+ Key: "session_id", Value: constants.SortAscending,
+ },
+ },
+ }
+ db.AddCollectionIndex("appium_logs", dbDevice.UDID, appiumCollectionIndexModel)
+
+ // Create logs directory for the device if it doesn't already exist
+ if _, err := os.Stat(fmt.Sprintf("%s/device_%s", config.ProviderConfig.ProviderFolder, dbDevice.UDID)); os.IsNotExist(err) {
+ err = os.Mkdir(fmt.Sprintf("%s/device_%s", config.ProviderConfig.ProviderFolder, dbDevice.UDID), os.ModePerm)
+ if err != nil {
+ logger.ProviderLogger.Errorf("updateDevices: Could not create logs folder for device `%s` - %s\n", dbDevice.UDID, err)
+ continue
}
+ }
- // If the device is no longer connected
- // Reset its set up in case something is lingering and delete it from the map
- if !isConnected {
- resetLocalDevice(localDevice)
- delete(DeviceMap, localDevice.UDID)
- }
+ // Create a custom logger and attach it to the local device
+ deviceLogger, err := logger.CreateCustomLogger(fmt.Sprintf("%s/device_%s/device.log", config.ProviderConfig.ProviderFolder, dbDevice.UDID), dbDevice.UDID)
+ if err != nil {
+ logger.ProviderLogger.Errorf("updateDevices: Could not create custom logger for device `%s` - %s\n", dbDevice.UDID, err)
+ continue
}
+ dbDevice.Logger = *deviceLogger
- // Loop through the final local device map and set up the devices if they are not already being set up or live
- for _, device := range DeviceMap {
- // If we are not already preparing the device, or it's not already prepared
- if device.ProviderState != "preparing" && device.ProviderState != "live" {
- setContext(device)
- if device.OS == "ios" {
- device.WdaReadyChan = make(chan bool, 1)
- go setupIOSDevice(device)
- }
+ appiumLogger, err := logger.NewAppiumLogger(fmt.Sprintf("%s/device_%s/appium.log", config.ProviderConfig.ProviderFolder, dbDevice.UDID), dbDevice.UDID)
+ if err != nil {
+ logger.ProviderLogger.Errorf("updateDevices: Could not create Appium logger for device `%s` - %s\n", dbDevice.UDID, err)
+ continue
+ }
+ dbDevice.AppiumLogger = appiumLogger
+ }
+}
- if device.OS == "android" {
- go setupAndroidDevice(device)
+func updateDevices() {
+ ticker := time.NewTicker(5 * time.Second)
+ defer ticker.Stop()
+
+ for range ticker.C {
+ connectedDevices := GetConnectedDevicesCommon()
+
+ DEVICE_MAP_LOOP:
+ for dbDeviceUDID, dbDevice := range DBDeviceMap {
+ if dbDevice.Usage == "disabled" {
+ continue DEVICE_MAP_LOOP
+ }
+ if slices.Contains(connectedDevices, dbDeviceUDID) {
+ dbDevice.Connected = true
+ if dbDevice.ProviderState != "preparing" && dbDevice.ProviderState != "live" {
+ setContext(dbDevice)
+ if dbDevice.OS == "ios" {
+ dbDevice.WdaReadyChan = make(chan bool, 1)
+ go setupIOSDevice(dbDevice)
+ }
+
+ if dbDevice.OS == "android" {
+ go setupAndroidDevice(dbDevice)
+ }
}
+ } else {
+ dbDevice.ProviderState = "init"
+ dbDevice.IsResetting = false
+ dbDevice.Connected = false
}
}
}
}
-// Create Mongo collections for all devices for logging
-// Create a map of *device.LocalDevice for easier access across the code
func Setup() {
- if config.Config.EnvConfig.ProvideAndroid {
+ if config.ProviderConfig.ProvideAndroid {
err := providerutil.CheckGadsStreamAndDownload()
if err != nil {
log.Fatalf("Setup: Could not check availability of and download GADS-stream latest release - %s", err)
@@ -181,17 +208,8 @@ func setupAndroidDevice(device *models.Device) {
logger.ProviderLogger.LogInfo("android_device_setup", fmt.Sprintf("Running setup for device `%v`", device.UDID))
- err := updateScreenSize(device)
- if err != nil {
- logger.ProviderLogger.LogError("android_device_setup", fmt.Sprintf("Could not update screen dimensions with adb for device `%v` - %v", device.UDID, err))
- resetLocalDevice(device)
- return
- }
- getModel(device)
- getAndroidOSVersion(device)
-
// If Selenium Grid is used attempt to create a TOML file for the grid connection
- if config.Config.EnvConfig.UseSeleniumGrid {
+ if config.ProviderConfig.UseSeleniumGrid {
err := createGridTOML(device)
if err != nil {
logger.ProviderLogger.LogError("ios_device_setup", fmt.Sprintf("Selenium Grid use is enabled but couldn't create TOML for device `%s` - %s", device.UDID, err))
@@ -199,6 +217,7 @@ func setupAndroidDevice(device *models.Device) {
return
}
}
+ getAndroidDeviceHardwareModel(device)
streamPort, err := providerutil.GetFreePort()
if err != nil {
@@ -208,7 +227,7 @@ func setupAndroidDevice(device *models.Device) {
}
device.StreamPort = streamPort
- apps := getInstalledAppsAndroid(device)
+ apps := GetInstalledAppsAndroid(device)
if slices.Contains(apps, "com.shamanec.stream") {
stopGadsStreamService(device)
time.Sleep(3 * time.Second)
@@ -254,7 +273,7 @@ func setupAndroidDevice(device *models.Device) {
return
}
- device.InstalledApps = getInstalledAppsAndroid(device)
+ device.InstalledApps = GetInstalledAppsAndroid(device)
if slices.Contains(device.InstalledApps, "io.appium.settings") {
logger.ProviderLogger.LogInfo("android_device_setup", "Appium settings found on device, attempting to uninstall")
@@ -281,7 +300,7 @@ func setupAndroidDevice(device *models.Device) {
}
go startAppium(device)
- if config.Config.EnvConfig.UseSeleniumGrid {
+ if config.ProviderConfig.UseSeleniumGrid {
go startGridNode(device)
}
@@ -309,26 +328,32 @@ func setupIOSDevice(device *models.Device) {
resetLocalDevice(device)
return
}
- // Update hardware model got from plist, os version and product type
+ // Update hardware model got from plist
device.HardwareModel = plistValues["HardwareModel"].(string)
- device.OSVersion = plistValues["ProductVersion"].(string)
- device.IOSProductType = plistValues["ProductType"].(string)
- isAboveIOS17, err := isAboveIOS17(device)
+ // Mount the DDI on the device
+ err = mountDeveloperImageIOS(device)
+ if err != nil {
+ logger.ProviderLogger.LogError("ios_device_setup", fmt.Sprintf("Could not mount DDI on device `%s` - %v", device.UDID, err))
+ resetLocalDevice(device)
+ return
+ }
+
+ isAboveIOS17 := isAboveIOS17(device)
if err != nil {
- device.Logger.LogError("ios_device_setup", fmt.Sprintf("Could not determine if device `%v` is above iOS 17 - %v", device.UDID, err))
+ logger.ProviderLogger.LogError("ios_device_setup", fmt.Sprintf("Could not determine if device `%s` is above iOS 17 - %v", device.UDID, err))
resetLocalDevice(device)
return
}
- if isAboveIOS17 && config.Config.EnvConfig.OS != "darwin" {
+ if isAboveIOS17 && config.ProviderConfig.OS != "darwin" {
logger.ProviderLogger.LogInfo("ios_device_setup", "Device `%s` is iOS 17+ which is not supported on Windows/Linux, setup will be skipped")
device.ProviderState = "init"
return
}
// If Selenium Grid is used attempt to create a TOML file for the grid connection
- if config.Config.EnvConfig.UseSeleniumGrid {
+ if config.ProviderConfig.UseSeleniumGrid {
err := createGridTOML(device)
if err != nil {
logger.ProviderLogger.LogError("ios_device_setup", fmt.Sprintf("Selenium Grid use is enabled but couldn't create TOML for device `%s` - %s", device.UDID, err))
@@ -337,15 +362,6 @@ func setupIOSDevice(device *models.Device) {
}
}
- // Update the screen dimensions of the device using data from the IOSDeviceDimensions map
- err = updateScreenSize(device)
- if err != nil {
- logger.ProviderLogger.LogError("ios_device_setup", fmt.Sprintf("Could not update screen dimensions for device `%v` - %v", device.UDID, err))
- resetLocalDevice(device)
- return
- }
- getModel(device)
-
wdaPort, err := providerutil.GetFreePort()
if err != nil {
logger.ProviderLogger.LogError("ios_device_setup", fmt.Sprintf("Could not allocate free WebDriverAgent port for device `%v` - %v", device.UDID, err))
@@ -375,35 +391,40 @@ func setupIOSDevice(device *models.Device) {
go goIOSForward(device, device.StreamPort, "9500")
go goIOSForward(device, device.WDAStreamPort, "9100")
- // TODO - finalize this when we can use go-ios to start tests anywhere
- //if config.Config.EnvConfig.UseGadsIosStream {
- // err = startGadsIosBroadcastViaXCTestGoIOS(device)
- // if err != nil {
- // logger.ProviderLogger.LogError("ios_device_setup", fmt.Sprintf("Could not start GADS broadcast with XCTest on device `%s` - %s", device.UDID, err))
- // resetLocalDevice(device)
- // return
- // }
- //}
-
// If on Linux or Windows use the prebuilt and provided WebDriverAgent.ipa/app file
- if config.Config.EnvConfig.OS != "darwin" {
- wdaPath := fmt.Sprintf("%s/%s", config.Config.EnvConfig.ProviderFolder, config.Config.EnvConfig.WebDriverBinary)
- err = installAppWithPathIOS(device, wdaPath)
+ if config.ProviderConfig.OS != "darwin" {
+ wdaPath := fmt.Sprintf("%s/%s", config.ProviderConfig.ProviderFolder, config.ProviderConfig.WebDriverBinary)
+ err = installAppIOS(device, wdaPath)
if err != nil {
logger.ProviderLogger.LogError("ios_device_setup", fmt.Sprintf("Could not install WebDriverAgent on device `%s` - %s", device.UDID, err))
resetLocalDevice(device)
return
}
- go startWdaWithGoIOS(device)
+ go startXCTestWithGoIOS(device, config.ProviderConfig.WdaBundleID, "WebDriverAgentRunner.xctest")
} else {
- go startWdaWithXcodebuild(device)
+ if !isAboveIOS17 {
+ wdaRepoPath := strings.TrimSuffix(config.ProviderConfig.WdaRepoPath, "/")
+ wdaPath := fmt.Sprintf("%s/build/Build/Products/Debug-iphoneos/WebDriverAgentRunner-Runner.app", wdaRepoPath)
+ err = installAppIOS(device, wdaPath)
+ if err != nil {
+ logger.ProviderLogger.LogError("ios_device_setup", fmt.Sprintf("Could not install WebDriverAgent on device `%s` - %s", device.UDID, err))
+ resetLocalDevice(device)
+ return
+ }
+ go startXCTestWithGoIOS(device, config.ProviderConfig.WdaBundleID, "WebDriverAgentRunner.xctest")
+ } else {
+ go startWdaWithXcodebuild(device)
+ }
}
+
+ go checkWebDriverAgentUp(device)
+
// Wait until WebDriverAgent successfully starts
select {
case <-device.WdaReadyChan:
logger.ProviderLogger.LogInfo("ios_device_setup", fmt.Sprintf("Successfully started WebDriverAgent for device `%v` forwarded on port %v", device.UDID, device.WDAPort))
break
- case <-time.After(30 * time.Second):
+ case <-time.After(60 * time.Second):
logger.ProviderLogger.LogError("ios_device_setup", fmt.Sprintf("Did not successfully start WebDriverAgent on device `%v` in 30 seconds", device.UDID))
resetLocalDevice(device)
return
@@ -418,28 +439,28 @@ func setupIOSDevice(device *models.Device) {
}
go startAppium(device)
- if config.Config.EnvConfig.UseSeleniumGrid {
+ if config.ProviderConfig.UseSeleniumGrid {
go startGridNode(device)
}
- device.InstalledApps = getInstalledAppsIOS(device)
+ device.InstalledApps = GetInstalledAppsIOS(device)
// Mark the device as 'live'
device.ProviderState = "live"
}
// Gets all connected iOS and Android devices to the host
-func GetConnectedDevicesCommon() []models.ConnectedDevice {
- var connectedDevices []models.ConnectedDevice
+func GetConnectedDevicesCommon() []string {
+ var connectedDevices []string
- var androidDevices []models.ConnectedDevice
- var iosDevices []models.ConnectedDevice
+ var androidDevices []string
+ var iosDevices []string
- if config.Config.EnvConfig.ProvideAndroid {
+ if config.ProviderConfig.ProvideAndroid {
androidDevices = getConnectedDevicesAndroid()
}
- if config.Config.EnvConfig.ProvideIOS {
+ if config.ProviderConfig.ProvideIOS {
iosDevices = getConnectedDevicesIOS()
}
@@ -450,8 +471,8 @@ func GetConnectedDevicesCommon() []models.ConnectedDevice {
}
// Gets the connected iOS devices using the `go-ios` library
-func getConnectedDevicesIOS() []models.ConnectedDevice {
- var connectedDevices []models.ConnectedDevice
+func getConnectedDevicesIOS() []string {
+ var connectedDevices []string
deviceList, err := ios.ListDevices()
if err != nil {
@@ -460,14 +481,14 @@ func getConnectedDevicesIOS() []models.ConnectedDevice {
}
for _, connDevice := range deviceList.DeviceList {
- connectedDevices = append(connectedDevices, models.ConnectedDevice{OS: "ios", UDID: connDevice.Properties.SerialNumber})
+ connectedDevices = append(connectedDevices, connDevice.Properties.SerialNumber)
}
return connectedDevices
}
// Gets the connected android devices using `adb`
-func getConnectedDevicesAndroid() []models.ConnectedDevice {
- var connectedDevices []models.ConnectedDevice
+func getConnectedDevicesAndroid() []string {
+ var connectedDevices []string
cmd := exec.Command("adb", "devices")
// Create a pipe to capture the command's output
@@ -489,20 +510,22 @@ func getConnectedDevicesAndroid() []models.ConnectedDevice {
for scanner.Scan() {
line := scanner.Text()
if !strings.Contains(line, "List of devices") && line != "" && strings.Contains(line, "device") && !strings.Contains(line, "emulator") {
- connectedDevices = append(connectedDevices, models.ConnectedDevice{OS: "android", UDID: strings.Fields(line)[0]})
+ connectedDevices = append(connectedDevices, strings.Fields(line)[0])
}
}
err = cmd.Wait()
if err != nil {
logger.ProviderLogger.LogDebug("provider", fmt.Sprintf("getConnectedDevicesAndroid: Waiting for `%s` command to finish failed, returning empty slice - %s", cmd.Args, err))
- return []models.ConnectedDevice{}
+ return []string{}
}
return connectedDevices
}
func resetLocalDevice(device *models.Device) {
+ device.Mutex.Lock()
+ defer device.Mutex.Unlock()
if !device.IsResetting && device.ProviderState != "init" {
logger.ProviderLogger.LogInfo("provider", fmt.Sprintf("Resetting LocalDevice for device `%v` after error. Cancelling context, setting ProviderState to `init`, Healthy to `false` and updating the DB", device.UDID))
@@ -605,7 +628,7 @@ func createGridTOML(device *models.Device) error {
automationName = "UiAutomator2"
}
- url := fmt.Sprintf("http://%s:%v/device/%s/appium", config.Config.EnvConfig.HostAddress, config.Config.EnvConfig.Port, device.UDID)
+ url := fmt.Sprintf("http://%s:%v/device/%s/appium", config.ProviderConfig.HostAddress, config.ProviderConfig.Port, device.UDID)
configs := fmt.Sprintf(`{"appium:deviceName": "%s", "platformName": "%s", "appium:platformVersion": "%s", "appium:automationName": "%s", "appium:udid": "%s"}`, device.Name, device.OS, device.OSVersion, automationName, device.UDID)
port, _ := providerutil.GetFreePort()
@@ -632,7 +655,7 @@ func createGridTOML(device *models.Device) error {
return fmt.Errorf("Failed marshalling TOML Appium config - %s", err)
}
- file, err := os.Create(fmt.Sprintf("%s/%s.toml", config.Config.EnvConfig.ProviderFolder, device.UDID))
+ file, err := os.Create(fmt.Sprintf("%s/%s.toml", config.ProviderConfig.ProviderFolder, device.UDID))
if err != nil {
return fmt.Errorf("Failed creating TOML Appium config file - %s", err)
}
@@ -651,14 +674,14 @@ func startGridNode(device *models.Device) {
cmd := exec.CommandContext(device.Context,
"java",
"-jar",
- fmt.Sprintf("%s/selenium.jar", config.Config.EnvConfig.ProviderFolder),
+ fmt.Sprintf("%s/selenium.jar", config.ProviderConfig.ProviderFolder),
"node",
"--host",
- config.Config.EnvConfig.HostAddress,
+ config.ProviderConfig.HostAddress,
"--config",
- fmt.Sprintf("%s/%s.toml", config.Config.EnvConfig.ProviderFolder, device.UDID),
+ fmt.Sprintf("%s/%s.toml", config.ProviderConfig.ProviderFolder, device.UDID),
"--grid-url",
- config.Config.EnvConfig.SeleniumGrid,
+ config.ProviderConfig.SeleniumGrid,
)
logger.ProviderLogger.LogInfo("device_setup", fmt.Sprintf("Starting Selenium grid node for device `%s` with command `%s`", device.UDID, cmd.Args))
@@ -689,77 +712,11 @@ func startGridNode(device *models.Device) {
}
}
-func updateScreenSize(device *models.Device) error {
- if device.OS == "ios" {
- if dimensions, ok := constants.IOSDeviceInfoMap[device.IOSProductType]; ok {
- device.ScreenHeight = dimensions.Height
- device.ScreenWidth = dimensions.Width
- } else {
- return fmt.Errorf("could not find `%s` hardware model in the IOSDeviceDimensions map, please update the map", device.HardwareModel)
- }
- } else {
- err := updateAndroidScreenSizeADB(device)
- if err != nil {
- return err
- }
- }
-
- return nil
-}
-
-func getModel(device *models.Device) {
- if device.OS == "ios" {
- if info, ok := constants.IOSDeviceInfoMap[device.IOSProductType]; ok {
- device.Model = info.Model
- } else {
- device.Model = "Unknown iOS device"
- }
- } else {
- brandCmd := exec.CommandContext(device.Context, "adb", "-s", device.UDID, "shell", "getprop", "ro.product.brand")
- var outBuffer bytes.Buffer
- brandCmd.Stdout = &outBuffer
- if err := brandCmd.Run(); err != nil {
- device.Model = "Unknown brand and model"
- }
- brand := outBuffer.String()
- outBuffer.Reset()
-
- modelCmd := exec.CommandContext(device.Context, "adb", "-s", device.UDID, "shell", "getprop", "ro.product.model")
- modelCmd.Stdout = &outBuffer
- if err := modelCmd.Run(); err != nil {
- device.Model = "Unknown brand/model"
- return
- }
- model := outBuffer.String()
-
- device.Model = fmt.Sprintf("%s %s", strings.TrimSpace(brand), strings.TrimSpace(model))
- }
-}
-
-func getAndroidOSVersion(device *models.Device) {
- if device.OS == "ios" {
-
- } else {
- sdkCmd := exec.CommandContext(device.Context, "adb", "-s", device.UDID, "shell", "getprop", "ro.build.version.sdk")
- var outBuffer bytes.Buffer
- sdkCmd.Stdout = &outBuffer
- if err := sdkCmd.Run(); err != nil {
- device.OSVersion = "N/A"
- }
- sdkVersion := strings.TrimSpace(outBuffer.String())
- if osVersion, ok := constants.AndroidVersionToSDK[sdkVersion]; ok {
- device.OSVersion = osVersion
- } else {
- device.OSVersion = "N/A"
- }
- }
-}
-
func UpdateInstalledApps(device *models.Device) {
if device.OS == "ios" {
- device.InstalledApps = getInstalledAppsIOS(device)
+ device.InstalledApps = GetInstalledAppsIOS(device)
} else {
- device.InstalledApps = getInstalledAppsAndroid(device)
+ device.InstalledApps = GetInstalledAppsAndroid(device)
}
}
@@ -781,7 +738,7 @@ func UninstallApp(device *models.Device, app string) error {
func InstallApp(device *models.Device, app string) error {
if device.OS == "ios" {
- err := installAppIOS(device, app)
+ err := installAppDefaultPath(device, app)
if err != nil {
device.Logger.LogError("install_app_ios", fmt.Sprintf("Failed installing app on device `%s` - %s", device.UDID, err))
return err
@@ -796,3 +753,24 @@ func InstallApp(device *models.Device, app string) error {
return nil
}
+
+func getAndroidDeviceHardwareModel(device *models.Device) {
+ brandCmd := exec.CommandContext(device.Context, "adb", "-s", device.UDID, "shell", "getprop", "ro.product.brand")
+ var outBuffer bytes.Buffer
+ brandCmd.Stdout = &outBuffer
+ if err := brandCmd.Run(); err != nil {
+ device.HardwareModel = "Unknown"
+ }
+ brand := outBuffer.String()
+ outBuffer.Reset()
+
+ modelCmd := exec.CommandContext(device.Context, "adb", "-s", device.UDID, "shell", "getprop", "ro.product.model")
+ modelCmd.Stdout = &outBuffer
+ if err := modelCmd.Run(); err != nil {
+ device.HardwareModel = "Unknown"
+ return
+ }
+ model := outBuffer.String()
+
+ device.HardwareModel = fmt.Sprintf("%s %s", strings.TrimSpace(brand), strings.TrimSpace(model))
+}
diff --git a/provider/devices/db.go b/provider/devices/db.go
index ee6c323f..97cad518 100644
--- a/provider/devices/db.go
+++ b/provider/devices/db.go
@@ -2,46 +2,44 @@ package devices
import (
"context"
- "fmt"
- "time"
"GADS/common/db"
- "GADS/provider/logger"
+ "GADS/common/models"
+ "GADS/provider/config"
+
"go.mongodb.org/mongo-driver/bson"
- "go.mongodb.org/mongo-driver/mongo/options"
)
-// Update all devices data in Mongo each second
-func updateDevicesMongo() {
- ticker := time.NewTicker(1 * time.Second)
- defer ticker.Stop()
+func getDBProviderDevices() map[string]*models.Device {
+ ctx, cancel := context.WithCancel(db.MongoCtx())
+ defer cancel()
+
+ var deviceDataMap = make(map[string]*models.Device)
+
+ filter := bson.M{"provider": config.ProviderConfig.Nickname}
+
+ collection := db.MongoClient().Database("gads").Collection("new_devices")
- for {
- <-ticker.C
- upsertDevicesMongo()
+ cursor, err := collection.Find(ctx, filter, nil)
+ if err != nil {
+ return nil
}
-}
-// Upsert all devices data in Mongo
-func upsertDevicesMongo() {
- ctx, cancel := context.WithCancel(db.MongoCtx())
- defer cancel()
+ var deviceData []*models.Device
- for _, device := range DeviceMap {
- filter := bson.M{"udid": device.UDID}
- if device.Connected {
- device.LastUpdatedTimestamp = time.Now().UnixMilli()
- }
+ if err := cursor.All(context.Background(), &deviceData); err != nil {
+ return nil
+ }
- update := bson.M{
- "$set": device,
- }
- opts := options.Update().SetUpsert(true)
+ if err := cursor.Err(); err != nil {
+ return nil
+ }
- _, err := db.MongoClient().Database("gads").Collection("devices").UpdateOne(ctx, filter, update, opts)
+ cursor.Close(context.TODO())
- if err != nil {
- logger.ProviderLogger.LogError("provider", fmt.Sprintf("upsertDevicesMongo: Failed upserting device data in Mongo - %s", err))
- }
+ for _, dbDevice := range deviceData {
+ deviceDataMap[dbDevice.UDID] = dbDevice
}
+
+ return deviceDataMap
}
diff --git a/provider/devices/go-ios.go b/provider/devices/go-ios.go
deleted file mode 100644
index e8423206..00000000
--- a/provider/devices/go-ios.go
+++ /dev/null
@@ -1,23 +0,0 @@
-package devices
-
-import (
- "fmt"
- "github.com/danielpaulus/go-ios/ios/zipconduit"
-
- "GADS/common/models"
- "GADS/provider/logger"
-)
-
-func InstallAppWithDevice(device *models.Device, filePath string) error {
- logger.ProviderLogger.LogInfo("ios_device", fmt.Sprintf("Installing app `%s` on iOS device `%s`", filePath, device.UDID))
- conn, err := zipconduit.New(device.GoIOSDeviceEntry)
- if err != nil {
- return fmt.Errorf("InstallAppWithDevice: Failed creating zip conduit with go-ios - %s", err)
- }
-
- err = conn.SendFile(filePath)
- if err != nil {
- return fmt.Errorf("InstallAppWithDevice: Failed installing application with go-ios - %s", err)
- }
- return nil
-}
diff --git a/provider/devices/ios.go b/provider/devices/ios.go
index ad964137..cf5acc73 100644
--- a/provider/devices/ios.go
+++ b/provider/devices/ios.go
@@ -6,19 +6,19 @@ import (
"context"
"encoding/json"
"fmt"
- "github.com/danielpaulus/go-ios/ios"
- "github.com/danielpaulus/go-ios/ios/imagemounter"
"io"
"net/http"
"os"
"os/exec"
- "strconv"
"strings"
"sync"
+ "time"
"GADS/common/models"
"GADS/provider/config"
"GADS/provider/logger"
+ "github.com/Masterminds/semver"
+ "github.com/danielpaulus/go-ios/ios"
)
// Forward iOS device ports using `go-ios` CLI, for some reason using the library doesn't work properly
@@ -53,7 +53,7 @@ func startWdaWithXcodebuild(device *models.Device) {
"-destination", "platform=iOS,id="+device.UDID,
"-derivedDataPath", "./build",
"test-without-building")
- cmd.Dir = config.Config.EnvConfig.WdaRepoPath
+ cmd.Dir = config.ProviderConfig.WdaRepoPath
logger.ProviderLogger.LogDebug("webdriveragent_xcodebuild", fmt.Sprintf("startWdaWithXcodebuild: Starting WebDriverAgent with command `%v`", cmd.Args))
stdout, err := cmd.StdoutPipe()
@@ -80,11 +80,6 @@ func startWdaWithXcodebuild(device *models.Device) {
resetLocalDevice(device)
return
}
-
- if strings.Contains(line, "ServerURLHere") {
- // device.DeviceIP = strings.Split(strings.Split(line, "//")[1], ":")[0]
- device.WdaReadyChan <- true
- }
}
if err := cmd.Wait(); err != nil {
@@ -172,9 +167,14 @@ func createWebDriverAgentSession(device *models.Device) error {
return nil
}
-// Start WebDriverAgent with the go-ios binary
-func startWdaWithGoIOS(device *models.Device) {
- cmd := exec.CommandContext(context.Background(), "ios", "runwda", "--bundleid="+config.Config.EnvConfig.WdaBundleID, "--testrunnerbundleid="+config.Config.EnvConfig.WdaBundleID, "--xctestconfig=WebDriverAgentRunner.xctest", "--udid="+device.UDID)
+func startXCTestWithGoIOS(device *models.Device, bundleId string, xctestConfig string) {
+ cmd := exec.CommandContext(context.Background(),
+ "ios",
+ "runtest",
+ fmt.Sprintf("--bundle-id=%s", bundleId),
+ fmt.Sprintf("--test-runner-bundle-id=%s", bundleId),
+ fmt.Sprintf("--xctest-config=%s", xctestConfig),
+ fmt.Sprintf("--udid=%s", device.UDID))
logger.ProviderLogger.LogDebug("device_setup", fmt.Sprintf("startWdaWithGoIOS: Starting with command `%v`", cmd.Args))
// Create a pipe to capture the command's output
stdout, err := cmd.StdoutPipe()
@@ -209,10 +209,10 @@ func startWdaWithGoIOS(device *models.Device) {
device.Logger.LogDebug("webdriveragent", strings.TrimSpace(line))
- if strings.Contains(line, "ServerURLHere") {
- // device.DeviceIP = strings.Split(strings.Split(line, "//")[1], ":")[0]
- device.WdaReadyChan <- true
- }
+ //if strings.Contains(line, "ServerURLHere") {
+ // // device.DeviceIP = strings.Split(strings.Split(line, "//")[1], ":")[0]
+ // device.WdaReadyChan <- true
+ //}
}
err = cmd.Wait()
@@ -222,30 +222,30 @@ func startWdaWithGoIOS(device *models.Device) {
}
}
-// Start an XCUITest(similar to WebDriverAgent) that will enable the broadcast stream if the GADS app is used
-func startGadsIosBroadcastViaXCTestGoIOS(device *models.Device) error {
- cmd := exec.CommandContext(context.Background(), "ios", "runwda", "--bundleid=com.shamanec.iosstreamUITests.xctrunner", "--testrunnerbundleid=com.shamanec.iosstreamUITests.xctrunner", "--xctestconfig=iosstreamUITests.xctest", "--udid="+device.UDID)
+// cmd := exec.CommandContext(context.Background(), "ios", "runwda", "--bundleid=com.shamanec.iosstreamUITests.xctrunner", "--testrunnerbundleid=com.shamanec.iosstreamUITests.xctrunner", "--xctestconfig=iosstreamUITests.xctest", "--udid="+device.UDID)
+
+// Mount a developer disk image on an iOS device with the go-ios library
+func mountDeveloperImageIOS(device *models.Device) error {
+ basedir := fmt.Sprintf("%s/devimages", config.ProviderConfig.ProviderFolder)
+
+ cmd := exec.CommandContext(device.Context, "ios", "image", "auto", fmt.Sprintf("--basedir=%s", basedir))
+ logger.ProviderLogger.LogInfo("ios_device_setup", fmt.Sprintf("Mounting DDI on device `%s` with command `%s`, image will be stored/found in `%s`", device.UDID, cmd.Args, basedir))
+
// Create a pipe to capture the command's output
stdout, err := cmd.StdoutPipe()
if err != nil {
- logger.ProviderLogger.LogError("device_setup", fmt.Sprintf("startGadsIosBroadcastViaXCTestGoIOS: Error creating stdoutpipe while starting GADS broadcast with XCUITest, xcodebuild and go-ios for device `%v` - %v", device.UDID, err))
- resetLocalDevice(device)
- return err
+ return fmt.Errorf("mountDeveloperImageIOS: Failed creating stdout pipe - %s", err)
}
// Create a pipe to capture the command's error output
stderr, err := cmd.StderrPipe()
if err != nil {
- logger.ProviderLogger.LogError("device_setup", fmt.Sprintf("startGadsIosBroadcastViaXCTestGoIOS: Error creating stderrpipe while starting GADS broadcast with XCUITest, xcodebuild and go-ios for device `%v` - %v", device.UDID, err))
- resetLocalDevice(device)
- return err
+ return fmt.Errorf("mountDeveloperImageIOS: Failed creating stderr pipe - %s", err)
}
err = cmd.Start()
if err != nil {
- logger.ProviderLogger.LogError("device_setup", fmt.Sprintf("startGadsIosBroadcastViaXCTestGoIOS: Failed executing `%s` - %v", cmd.Args, err))
- resetLocalDevice(device)
- return err
+ return fmt.Errorf("mountDeveloperImageIOS: Failed starting command `%s` - %s", cmd.Args, err)
}
// Create a combined reader from stdout and stderr
@@ -254,38 +254,13 @@ func startGadsIosBroadcastViaXCTestGoIOS(device *models.Device) error {
scanner := bufio.NewScanner(combinedReader)
for scanner.Scan() {
- line := scanner.Text()
- if strings.Contains(line, "didFinishExecutingTestPlan received. Closing test.") {
- if killErr := cmd.Process.Kill(); killErr != nil {
- return killErr
- }
- return nil
- }
+ //line := scanner.Text()
+ //fmt.Println(line)
}
err = cmd.Wait()
if err != nil {
- device.Logger.LogError("gads_broadcast_startup", fmt.Sprintf("startGadsIosBroadcastViaXCTestGoIOS: Error waiting for `%s` to finish, it errored out or device `%v` was disconnected - %v", cmd.Args, device.UDID, err))
- resetLocalDevice(device)
- return err
- }
-
- return nil
-}
-
-// Mount a developer disk image on an iOS device with the go-ios library
-func mountDeveloperImageIOS(device *models.Device) error {
- basedir := fmt.Sprintf("%s/devimages", config.Config.EnvConfig.ProviderFolder)
-
- var err error
- path, err := imagemounter.DownloadImageFor(device.GoIOSDeviceEntry, basedir)
- if err != nil {
- return fmt.Errorf("Could not download developer disk image with go-ios - %s", err)
- }
-
- err = imagemounter.MountImage(device.GoIOSDeviceEntry, path)
- if err != nil {
- return fmt.Errorf("Could not mount developer disk image with go-ios - %s", err)
+ return fmt.Errorf("mountDeveloperImageIOS: Failed to run command to mount DDI - %s", err)
}
return nil
@@ -295,7 +270,7 @@ func mountDeveloperImageIOS(device *models.Device) error {
func pairIOS(device *models.Device) error {
logger.ProviderLogger.LogInfo("ios_device_setup", fmt.Sprintf("Pairing device `%s`", device.UDID))
- p12, err := os.ReadFile(fmt.Sprintf("%s/supervision.p12", config.Config.EnvConfig.ProviderFolder))
+ p12, err := os.ReadFile(fmt.Sprintf("%s/supervision.p12", config.ProviderConfig.ProviderFolder))
if err != nil {
logger.ProviderLogger.LogWarn("ios_device_setup", fmt.Sprintf("Could not read supervision.p12 file when pairing device with UDID: %s, falling back to unsupervised pairing - %s", device.UDID, err))
err = ios.Pair(device.GoIOSDeviceEntry)
@@ -305,7 +280,7 @@ func pairIOS(device *models.Device) error {
return nil
}
- err = ios.PairSupervised(device.GoIOSDeviceEntry, p12, config.Config.EnvConfig.SupervisionPassword)
+ err = ios.PairSupervised(device.GoIOSDeviceEntry, p12, config.ProviderConfig.SupervisionPassword)
if err != nil {
return fmt.Errorf("Could not perform supervised pairing successfully - %s", err)
}
@@ -314,7 +289,7 @@ func pairIOS(device *models.Device) error {
}
// Get all installed apps on an iOS device
-func getInstalledAppsIOS(device *models.Device) []string {
+func GetInstalledAppsIOS(device *models.Device) []string {
var installedApps []string
cmd := exec.CommandContext(device.Context, "ios", "apps", "--udid="+device.UDID)
@@ -323,7 +298,7 @@ func getInstalledAppsIOS(device *models.Device) []string {
var outBuffer bytes.Buffer
cmd.Stdout = &outBuffer
if err := cmd.Run(); err != nil {
- device.Logger.LogError("get_installed_apps", fmt.Sprintf("getInstalledAppsIOS: Failed executing `%s` to get installed apps - %v", cmd.Args, err))
+ device.Logger.LogError("get_installed_apps", fmt.Sprintf("GetInstalledAppsIOS: Failed executing `%s` to get installed apps - %v", cmd.Args, err))
return installedApps
}
@@ -336,11 +311,11 @@ func getInstalledAppsIOS(device *models.Device) []string {
err := json.Unmarshal([]byte(jsonString), &appsData)
if err != nil {
- device.Logger.LogError("get_installed_apps", fmt.Sprintf("getInstalledAppsIOS: Error unmarshalling `%s` output json - %v", cmd.Args, err))
+ device.Logger.LogError("get_installed_apps", fmt.Sprintf("GetInstalledAppsIOS: Error unmarshalling `%s` output json - %v", cmd.Args, err))
return installedApps
}
- var mu sync.Mutex
+ var mu sync.RWMutex
mu.Lock()
defer mu.Unlock()
for _, appData := range appsData {
@@ -350,6 +325,35 @@ func getInstalledAppsIOS(device *models.Device) []string {
return installedApps
}
+// To use for iOS 17+ when stable
+func StartIOSTunnel() {
+ cmd := exec.CommandContext(context.Background(), "ios", "tunnel", "start")
+
+ // Create a pipe to capture the command's output
+ stdout, err := cmd.StdoutPipe()
+ if err != nil {
+ }
+
+ // Create a pipe to capture the command's error output
+ stderr, err := cmd.StderrPipe()
+ if err != nil {
+ }
+
+ err = cmd.Start()
+ if err != nil {
+ }
+
+ // Create a combined reader from stdout and stderr
+ combinedReader := io.MultiReader(stderr, stdout)
+ // Create a scanner to read the command's output line by line
+ scanner := bufio.NewScanner(combinedReader)
+
+ for scanner.Scan() {
+ line := scanner.Text()
+ fmt.Println(line)
+ }
+}
+
// Uninstall an app on an iOS device by bundle identifier
func uninstallAppIOS(device *models.Device, bundleID string) error {
cmd := exec.CommandContext(device.Context, "ios", "uninstall", bundleID, "--udid="+device.UDID)
@@ -362,27 +366,18 @@ func uninstallAppIOS(device *models.Device, bundleID string) error {
return nil
}
-// Install app with the go-ios binary from provided path
-func installAppWithPathIOS(device *models.Device, path string) error {
- if config.Config.EnvConfig.OS == "windows" {
- if strings.HasPrefix(path, "./") {
- path = strings.TrimPrefix(path, "./")
- }
- }
+func installAppDefaultPath(device *models.Device, appName string) error {
+ appPath := fmt.Sprintf("%s/%s", config.ProviderConfig.ProviderFolder, appName)
- cmd := exec.CommandContext(device.Context, "ios", "install", fmt.Sprintf("--path=%s", path), "--udid="+device.UDID)
- logger.ProviderLogger.LogDebug("install_app", fmt.Sprintf("installAppWithPathIOS: Installing with command `%s`", cmd.Args))
- if err := cmd.Run(); err != nil {
- device.Logger.LogError("install_app", fmt.Sprintf("Failed executing `%s` - %v", cmd.Args, err))
- return err
- }
-
- return nil
+ return installAppIOS(device, appPath)
}
-func installAppIOS(device *models.Device, appName string) error {
- appPath := fmt.Sprintf("%s/%s", config.Config.EnvConfig.ProviderFolder, appName)
- if config.Config.EnvConfig.OS == "darwin" {
+func installAppIOS(device *models.Device, appPath string) error {
+ if config.ProviderConfig.OS == "windows" {
+ appPath = strings.TrimPrefix(appPath, "./")
+ }
+
+ if config.ProviderConfig.OS == "darwin" && isAboveIOS16(device) {
cmd := exec.CommandContext(device.Context,
"xcrun",
"devicectl",
@@ -414,14 +409,39 @@ func installAppIOS(device *models.Device, appName string) error {
}
// Check if a device is above iOS 17
-func isAboveIOS17(device *models.Device) (bool, error) {
- majorVersion := strings.Split(device.OSVersion, ".")[0]
- convertedVersion, err := strconv.Atoi(majorVersion)
- if err != nil {
- return false, fmt.Errorf("isAboveIOS17: Failed converting `%s` to int - %s", majorVersion, err)
+func isAboveIOS17(device *models.Device) bool {
+ deviceOSVersion, _ := semver.NewVersion(device.OSVersion)
+
+ return deviceOSVersion.Major() >= 17
+}
+
+func isAboveIOS16(device *models.Device) bool {
+ deviceOSVersion, _ := semver.NewVersion(device.OSVersion)
+
+ return deviceOSVersion.Major() >= 16
+}
+
+func checkWebDriverAgentUp(device *models.Device) {
+ var netClient = &http.Client{
+ Timeout: time.Second * 120,
}
- if convertedVersion >= 17 {
- return true, nil
+
+ req, _ := http.NewRequest(http.MethodGet, fmt.Sprintf("http://localhost:%v/status", device.WDAPort), nil)
+
+ loops := 0
+ for {
+ if loops >= 30 {
+ return
+ }
+ resp, err := netClient.Do(req)
+ if err != nil {
+ time.Sleep(1 * time.Second)
+ } else {
+ if resp.StatusCode == http.StatusOK {
+ device.WdaReadyChan <- true
+ return
+ }
+ }
+ loops++
}
- return false, nil
}
diff --git a/provider/logger/logger.go b/provider/logger/logger.go
index 353723b8..82ec0c9b 100644
--- a/provider/logger/logger.go
+++ b/provider/logger/logger.go
@@ -31,8 +31,8 @@ func SetupLogging(level string) {
logLevel = level
var err error
- fmt.Println(fmt.Sprintf("Provider will be logging to `%s/provider.log`", config.Config.EnvConfig.ProviderFolder))
- ProviderLogger, err = CreateCustomLogger(fmt.Sprintf("%s/provider.log", config.Config.EnvConfig.ProviderFolder), config.Config.EnvConfig.Nickname)
+ fmt.Println(fmt.Sprintf("Provider will be logging to `%s/provider.log`", config.ProviderConfig.ProviderFolder))
+ ProviderLogger, err = CreateCustomLogger(fmt.Sprintf("%s/provider.log", config.ProviderConfig.ProviderFolder), config.ProviderConfig.Nickname)
if err != nil {
log.Fatalf("Failed to create custom logger for the provider instance - %s", err)
}
@@ -124,7 +124,7 @@ func (hook *MongoDBHook) Fire(entry *log.Entry) error {
Level: entry.Level.String(),
Message: entry.Message,
Timestamp: time.Now().UnixMilli(),
- Host: config.Config.EnvConfig.Nickname,
+ Host: config.ProviderConfig.Nickname,
EventName: fields["event"].(string),
}
diff --git a/provider/provider.go b/provider/provider.go
index bfb74f66..2a08cdab 100644
--- a/provider/provider.go
+++ b/provider/provider.go
@@ -10,15 +10,16 @@ import (
"GADS/provider/router"
"context"
"fmt"
- "github.com/spf13/pflag"
- "go.mongodb.org/mongo-driver/bson"
- "go.mongodb.org/mongo-driver/mongo/options"
"log"
"os"
"runtime"
"sort"
"strings"
"time"
+
+ "github.com/spf13/pflag"
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
)
func StartProvider(flags *pflag.FlagSet) {
@@ -26,11 +27,16 @@ func StartProvider(flags *pflag.FlagSet) {
nickname, _ := flags.GetString("nickname")
mongoDb, _ := flags.GetString("mongo-db")
providerFolder, _ := flags.GetString("provider-folder")
+ hubAddress, _ := flags.GetString("hub")
if nickname == "" {
log.Fatalf("Please provide valid provider instance nickname via the --nickname flag, e.g. --nickname=Provider1")
}
+ if hubAddress == "" {
+ log.Fatalf("Please provide valid GADS hub instance address via the --hub flag, e.g. --hub=http://192.168.1.6:10000")
+ }
+
if providerFolder == "." {
providerFolder = fmt.Sprintf("./%s", nickname)
}
@@ -47,14 +53,14 @@ func StartProvider(flags *pflag.FlagSet) {
db.InitMongoClient(mongoDb)
defer db.MongoCtxCancel()
// Set up the provider configuration
- config.SetupConfig(nickname, providerFolder)
- config.Config.EnvConfig.OS = runtime.GOOS
+ config.SetupConfig(nickname, providerFolder, hubAddress)
+ config.ProviderConfig.OS = runtime.GOOS
// Defer closing the Mongo connection on provider stopped
defer db.CloseMongoConn()
// Setup logging for the provider itself
logger.SetupLogging(logLevel)
- logger.ProviderLogger.LogInfo("provider_setup", fmt.Sprintf("Starting provider on port `%v`", config.Config.EnvConfig.Port))
+ logger.ProviderLogger.LogInfo("provider_setup", fmt.Sprintf("Starting provider on port `%v`", config.ProviderConfig.Port))
logger.ProviderLogger.LogInfo("provider_setup", "Checking if Appium is installed and available on the host")
if !providerutil.AppiumAvailable() {
@@ -62,7 +68,7 @@ func StartProvider(flags *pflag.FlagSet) {
}
// Finalize grid configuration if Selenium Grid usage enabled
- if config.Config.EnvConfig.UseSeleniumGrid {
+ if config.ProviderConfig.UseSeleniumGrid {
err = config.SetupSeleniumJar()
if err != nil {
log.Fatalf("Selenium Grid connection is enabled but there is something wrong with providing the selenium jar file from MongoDB - %s", err)
@@ -70,21 +76,21 @@ func StartProvider(flags *pflag.FlagSet) {
}
// If running on macOS and iOS device provisioning is enabled
- if config.Config.EnvConfig.OS == "darwin" && config.Config.EnvConfig.ProvideIOS {
+ if config.ProviderConfig.OS == "darwin" && config.ProviderConfig.ProvideIOS {
logger.ProviderLogger.LogInfo("provider_setup", "Provider runs on macOS and is set up to provide iOS devices")
// Add a trailing slash to WDA repo folder if its missing
// To avoid issues with the configuration
logger.ProviderLogger.LogDebug("provider_setup", "Handling trailing slash of provided WebDriverAgent repo path if needed")
- if !strings.HasSuffix(config.Config.EnvConfig.WdaRepoPath, "/") {
+ if !strings.HasSuffix(config.ProviderConfig.WdaRepoPath, "/") {
logger.ProviderLogger.LogDebug("provider_setup", "Provided WebDriverAgent repo path has no trailing slash, adding it")
- config.Config.EnvConfig.WdaRepoPath = fmt.Sprintf("%s/", config.Config.EnvConfig.WdaRepoPath)
+ config.ProviderConfig.WdaRepoPath = fmt.Sprintf("%s/", config.ProviderConfig.WdaRepoPath)
}
// Check if the provided WebDriverAgent repo path exists
logger.ProviderLogger.LogDebug("provider_setup", "Checking if provided WebDriverAgent repo path exists on the host")
- _, err := os.Stat(config.Config.EnvConfig.WdaRepoPath)
+ _, err := os.Stat(config.ProviderConfig.WdaRepoPath)
if err != nil {
- log.Fatalf("`%s` does not exist, you need to provide valid path to the WebDriverAgent repo in the provider configuration", config.Config.EnvConfig.WdaRepoPath)
+ log.Fatalf("`%s` does not exist, you need to provide valid path to the WebDriverAgent repo in the provider configuration", config.ProviderConfig.WdaRepoPath)
}
// Check if xcodebuild is available - Xcode and command line tools should be installed
@@ -99,14 +105,14 @@ func StartProvider(flags *pflag.FlagSet) {
}
}
- if config.Config.EnvConfig.ProvideIOS {
+ if config.ProviderConfig.ProvideIOS {
// Check if the `go-ios` binary is available on PATH as explained in the setup readme
if !providerutil.GoIOSAvailable() {
log.Fatal("`go-ios` is not available, you need to set it up on the host as explained in the readme")
}
// If on Linux or Windows and iOS devices provision enabled check for WebDriverAgent.ipa/app
- if config.Config.EnvConfig.OS != "darwin" {
+ if config.ProviderConfig.OS != "darwin" {
logger.ProviderLogger.LogInfo(
"provider_setup",
"Provider runs on Linux/Windows and is set up to provide iOS devices, checking if prepared WebDriverAgent binary exists in the provider folder as explained in the readme")
@@ -118,7 +124,7 @@ func StartProvider(flags *pflag.FlagSet) {
}
// If we want to provide Android devices check if adb is available on PATH
- if config.Config.EnvConfig.ProvideAndroid {
+ if config.ProviderConfig.ProvideAndroid {
if !providerutil.AdbAvailable() {
logger.ProviderLogger.LogError("provider", "adb is not available, you need to set up the host as explained in the readme")
fmt.Println("adb is not available, you need to set up the host as explained in the readme")
@@ -145,7 +151,7 @@ func startHTTPServer() error {
// Start periodically updating the provider data in the DB
go updateProviderInDB()
// Start the provider
- address := fmt.Sprintf("%s:%v", config.Config.EnvConfig.HostAddress, config.Config.EnvConfig.Port)
+ address := fmt.Sprintf("%s:%v", config.ProviderConfig.HostAddress, config.ProviderConfig.Port)
err := r.Run(address)
if err != nil {
return err
@@ -164,9 +170,9 @@ func configureWebDriverBinary(providerFolder string) error {
if os.IsNotExist(err) {
return err
}
- config.Config.EnvConfig.WebDriverBinary = "WebDriverAgent.app"
+ config.ProviderConfig.WebDriverBinary = "WebDriverAgent.app"
} else {
- config.Config.EnvConfig.WebDriverBinary = "WebDriverAgent.ipa"
+ config.ProviderConfig.WebDriverBinary = "WebDriverAgent.ipa"
}
return nil
}
@@ -178,10 +184,10 @@ func updateProviderInDB() {
for {
coll := db.MongoClient().Database("gads").Collection("providers")
- filter := bson.D{{Key: "nickname", Value: config.Config.EnvConfig.Nickname}}
+ filter := bson.D{{Key: "nickname", Value: config.ProviderConfig.Nickname}}
var providedDevices []models.Device
- for _, mapDevice := range devices.DeviceMap {
+ for _, mapDevice := range devices.DBDeviceMap {
providedDevices = append(providedDevices, *mapDevice)
}
sort.Sort(models.ByUDID(providedDevices))
diff --git a/provider/providerutil/providerutil.go b/provider/providerutil/providerutil.go
index 568caf8f..7f94b01d 100644
--- a/provider/providerutil/providerutil.go
+++ b/provider/providerutil/providerutil.go
@@ -16,7 +16,7 @@ import (
"GADS/provider/logger"
)
-var mu sync.Mutex
+var mu sync.RWMutex
var UsedPorts = make(map[string]bool)
var gadsStreamURL = "https://github.com/shamanec/GADS-Android-stream/releases/latest/download/gads-stream.apk"
@@ -103,14 +103,14 @@ func GoIOSAvailable() bool {
// Build WebDriverAgent for testing with `xcodebuild`
func BuildWebDriverAgent() error {
cmd := exec.Command("xcodebuild", "-project", "WebDriverAgent.xcodeproj", "-scheme", "WebDriverAgentRunner", "-destination", "generic/platform=iOS", "build-for-testing", "-derivedDataPath", "./build")
- cmd.Dir = config.Config.EnvConfig.WdaRepoPath
+ cmd.Dir = config.ProviderConfig.WdaRepoPath
stdout, err := cmd.StdoutPipe()
if err != nil {
return err
}
- logger.ProviderLogger.LogInfo("provider_setup", fmt.Sprintf("Starting WebDriverAgent using xcodebuild in path `%s` with command `%s` ", config.Config.EnvConfig.WdaRepoPath, cmd.String()))
+ logger.ProviderLogger.LogInfo("provider_setup", fmt.Sprintf("Building WebDriverAgent for testing using xcodebuild in path `%s` with command `%s` ", config.ProviderConfig.WdaRepoPath, cmd.String()))
if err := cmd.Start(); err != nil {
return err
}
@@ -165,7 +165,7 @@ func CheckGadsStreamAndDownload() error {
// Check if the gads-stream.apk file is located in the provider folder
func isGadsStreamApkAvailable() bool {
- _, err := os.Stat(fmt.Sprintf("%s/gads-stream.apk", config.Config.EnvConfig.ProviderFolder))
+ _, err := os.Stat(fmt.Sprintf("%s/gads-stream.apk", config.ProviderConfig.ProviderFolder))
if os.IsNotExist(err) {
return false
}
@@ -175,9 +175,9 @@ func isGadsStreamApkAvailable() bool {
// Download the latest release of GADS-Android-stream and put the apk in the provider folder
func downloadGadsStreamApk() error {
logger.ProviderLogger.LogInfo("provider", "Downloading latest GADS-stream release apk file")
- outFile, err := os.Create(fmt.Sprintf("%s/gads-stream.apk", config.Config.EnvConfig.ProviderFolder))
+ outFile, err := os.Create(fmt.Sprintf("%s/gads-stream.apk", config.ProviderConfig.ProviderFolder))
if err != nil {
- return fmt.Errorf("Could not create file at %s/gads-stream.apk - %s", config.Config.EnvConfig.ProviderFolder, err)
+ return fmt.Errorf("Could not create file at %s/gads-stream.apk - %s", config.ProviderConfig.ProviderFolder, err)
}
defer outFile.Close()
diff --git a/provider/router/appium.go b/provider/router/appium.go
index 69c98848..c4a1df89 100644
--- a/provider/router/appium.go
+++ b/provider/router/appium.go
@@ -52,7 +52,7 @@ func appiumLockUnlock(device *models.Device, lock string) (*http.Response, error
}
func appiumTap(device *models.Device, x float64, y float64) (*http.Response, error) {
- if config.Config.EnvConfig.UseCustomWDA && device.OS == "ios" {
+ if config.ProviderConfig.UseCustomWDA && device.OS == "ios" {
requestBody := struct {
X float64 `json:"x"`
Y float64 `json:"y"`
@@ -150,7 +150,7 @@ func appiumTouchAndHold(device *models.Device, x float64, y float64) (*http.Resp
}
func appiumSwipe(device *models.Device, x, y, endX, endY float64) (*http.Response, error) {
- if config.Config.EnvConfig.UseCustomWDA && device.OS == "ios" {
+ if config.ProviderConfig.UseCustomWDA && device.OS == "ios" {
requestBody := struct {
X float64 `json:"startX"`
Y float64 `json:"startY"`
@@ -344,7 +344,7 @@ func appiumGetClipboard(device *models.Device) (*http.Response, error) {
switch device.OS {
case "ios":
- activateAppResp, err := appiumActivateApp(device, config.Config.EnvConfig.WdaBundleID)
+ activateAppResp, err := appiumActivateApp(device, config.ProviderConfig.WdaBundleID)
if err != nil {
return activateAppResp, fmt.Errorf("appiumGetClipboard: Failed to activate app - %s", err)
}
diff --git a/provider/router/device_routes.go b/provider/router/device_routes.go
index 9ed006ae..203a32b4 100644
--- a/provider/router/device_routes.go
+++ b/provider/router/device_routes.go
@@ -9,6 +9,7 @@ import (
"GADS/common/models"
"GADS/provider/devices"
+
"github.com/gin-gonic/gin"
)
@@ -24,7 +25,7 @@ func copyHeaders(destination, source http.Header) {
// Check the device health by checking Appium and WDA(for iOS)
func DeviceHealth(c *gin.Context) {
udid := c.Param("udid")
- dev := devices.DeviceMap[udid]
+ dev := devices.DBDeviceMap[udid]
bool, err := devices.GetDeviceHealth(dev)
if err != nil {
dev.Logger.LogInfo("device", fmt.Sprintf("Could not check device health - %s", err))
@@ -45,7 +46,7 @@ func DeviceHealth(c *gin.Context) {
// Call the respective Appium/WDA endpoint to go to Homescreen
func DeviceHome(c *gin.Context) {
udid := c.Param("udid")
- device := devices.DeviceMap[udid]
+ device := devices.DBDeviceMap[udid]
device.Logger.LogInfo("appium_interact", "Navigating to Home/Springboard")
// Send the request
@@ -72,7 +73,7 @@ func DeviceHome(c *gin.Context) {
func DeviceGetClipboard(c *gin.Context) {
udid := c.Param("udid")
- device := devices.DeviceMap[udid]
+ device := devices.DBDeviceMap[udid]
device.Logger.LogInfo("appium_interact", "Getting device clipboard value")
// Send the request
@@ -110,7 +111,7 @@ func DeviceGetClipboard(c *gin.Context) {
// Call respective Appium/WDA endpoint to lock the device
func DeviceLock(c *gin.Context) {
udid := c.Param("udid")
- device := devices.DeviceMap[udid]
+ device := devices.DBDeviceMap[udid]
device.Logger.LogInfo("appium_interact", "Locking device")
lockResponse, err := appiumLockUnlock(device, "lock")
@@ -137,7 +138,7 @@ func DeviceLock(c *gin.Context) {
// Call the respective Appium/WDA endpoint to unlock the device
func DeviceUnlock(c *gin.Context) {
udid := c.Param("udid")
- device := devices.DeviceMap[udid]
+ device := devices.DBDeviceMap[udid]
device.Logger.LogInfo("appium_interact", "Unlocking device")
lockResponse, err := appiumLockUnlock(device, "unlock")
@@ -164,7 +165,7 @@ func DeviceUnlock(c *gin.Context) {
// Call the respective Appium/WDA endpoint to take a screenshot of the device screen
func DeviceScreenshot(c *gin.Context) {
udid := c.Param("udid")
- device := devices.DeviceMap[udid]
+ device := devices.DBDeviceMap[udid]
device.Logger.LogInfo("appium_interact", "Getting screenshot from device")
screenshotResp, err := appiumScreenshot(device)
@@ -188,7 +189,7 @@ func DeviceScreenshot(c *gin.Context) {
func DeviceAppiumSource(c *gin.Context) {
udid := c.Param("udid")
- device := devices.DeviceMap[udid]
+ device := devices.DBDeviceMap[udid]
device.Logger.LogInfo("appium_interact", "Getting Appium source from device")
sourceResp, err := appiumSource(device)
@@ -217,7 +218,7 @@ func DeviceAppiumSource(c *gin.Context) {
func DeviceTypeText(c *gin.Context) {
udid := c.Param("udid")
- device := devices.DeviceMap[udid]
+ device := devices.DBDeviceMap[udid]
var requestBody models.ActionData
if err := json.NewDecoder(c.Request.Body).Decode(&requestBody); err != nil {
@@ -250,7 +251,7 @@ func DeviceTypeText(c *gin.Context) {
func DeviceClearText(c *gin.Context) {
udid := c.Param("udid")
- device := devices.DeviceMap[udid]
+ device := devices.DBDeviceMap[udid]
device.Logger.LogInfo("appium_interact", "Clearing text from active element")
clearResp, err := appiumClearText(device)
@@ -275,7 +276,7 @@ func DeviceClearText(c *gin.Context) {
func DeviceTap(c *gin.Context) {
udid := c.Param("udid")
- device := devices.DeviceMap[udid]
+ device := devices.DBDeviceMap[udid]
var requestBody models.ActionData
if err := json.NewDecoder(c.Request.Body).Decode(&requestBody); err != nil {
@@ -308,7 +309,7 @@ func DeviceTap(c *gin.Context) {
func DeviceTouchAndHold(c *gin.Context) {
udid := c.Param("udid")
- device := devices.DeviceMap[udid]
+ device := devices.DBDeviceMap[udid]
var requestBody models.ActionData
if err := json.NewDecoder(c.Request.Body).Decode(&requestBody); err != nil {
@@ -341,7 +342,7 @@ func DeviceTouchAndHold(c *gin.Context) {
func DeviceSwipe(c *gin.Context) {
udid := c.Param("udid")
- device := devices.DeviceMap[udid]
+ device := devices.DBDeviceMap[udid]
var requestBody models.ActionData
if err := json.NewDecoder(c.Request.Body).Decode(&requestBody); err != nil {
diff --git a/provider/router/handler.go b/provider/router/handler.go
index 7df5c27a..f0a03265 100644
--- a/provider/router/handler.go
+++ b/provider/router/handler.go
@@ -36,6 +36,7 @@ func HandleRequests() *gin.Engine {
deviceGroup := r.Group("/device/:udid")
deviceGroup.GET("/info", DeviceInfo)
+ deviceGroup.GET("/apps", DeviceInstalledApps)
deviceGroup.GET("/health", DeviceHealth)
deviceGroup.POST("/tap", DeviceTap)
deviceGroup.POST("/touchAndHold", DeviceTouchAndHold)
@@ -51,7 +52,7 @@ func HandleRequests() *gin.Engine {
deviceGroup.Any("/appium/*proxyPath", AppiumReverseProxy)
deviceGroup.GET("/android-stream", AndroidStreamProxy)
deviceGroup.GET("/android-stream-mjpeg", AndroidStreamMJPEG)
- if config.Config.EnvConfig.UseGadsIosStream {
+ if config.ProviderConfig.UseGadsIosStream {
deviceGroup.GET("/ios-stream", IosStreamProxyGADS)
deviceGroup.GET("/ios-stream-mjpeg", IOSStreamMJPEG)
} else {
diff --git a/provider/router/routes.go b/provider/router/routes.go
index 52b59723..c59c671d 100644
--- a/provider/router/routes.go
+++ b/provider/router/routes.go
@@ -9,7 +9,6 @@ import (
"bytes"
"encoding/json"
"fmt"
- "github.com/gin-gonic/gin"
"io"
"net/http"
"net/http/httputil"
@@ -18,6 +17,8 @@ import (
"path/filepath"
"slices"
"strings"
+
+ "github.com/gin-gonic/gin"
)
type JsonErrorResponse struct {
@@ -45,7 +46,7 @@ func AppiumReverseProxy(c *gin.Context) {
}()
udid := c.Param("udid")
- device := devices.DeviceMap[udid]
+ device := devices.DBDeviceMap[udid]
target := "http://localhost:" + device.AppiumPort
path := c.Param("proxyPath")
@@ -70,7 +71,7 @@ func newAppiumProxy(target string, path string) *httputil.ReverseProxy {
func UploadAndInstallApp(c *gin.Context) {
// Specify the upload directory
- uploadDir := fmt.Sprintf("%s/", config.Config.EnvConfig.ProviderFolder)
+ uploadDir := fmt.Sprintf("%s/", config.ProviderConfig.ProviderFolder)
// Read the file from the form data
file, err := c.FormFile("file")
@@ -97,7 +98,7 @@ func UploadAndInstallApp(c *gin.Context) {
udid := c.Param("udid")
// Check if the target device is currently provisioned
- if dev, ok := devices.DeviceMap[udid]; ok {
+ if dev, ok := devices.DBDeviceMap[udid]; ok {
// If the uploaded file is not a zip archive
if ext != ".zip" {
// Create file destination based on the provider dir and file name
@@ -221,12 +222,12 @@ func UploadAndInstallApp(c *gin.Context) {
func GetProviderData(c *gin.Context) {
var providerData models.ProviderData
- deviceData := []*models.Device{}
- for _, device := range devices.DeviceMap {
- deviceData = append(deviceData, device)
+ deviceData := []models.Device{}
+ for _, device := range devices.DBDeviceMap {
+ deviceData = append(deviceData, *device)
}
- providerData.ProviderData = config.Config.EnvConfig
+ providerData.ProviderData = *config.ProviderConfig
providerData.DeviceData = deviceData
c.JSON(http.StatusOK, providerData)
@@ -235,7 +236,7 @@ func GetProviderData(c *gin.Context) {
func DeviceInfo(c *gin.Context) {
udid := c.Param("udid")
- if dev, ok := devices.DeviceMap[udid]; ok {
+ if dev, ok := devices.DBDeviceMap[udid]; ok {
devices.UpdateInstalledApps(dev)
c.JSON(http.StatusOK, dev)
return
@@ -244,10 +245,26 @@ func DeviceInfo(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("Did not find device with udid `%s`", udid)})
}
+func DeviceInstalledApps(c *gin.Context) {
+ udid := c.Param("udid")
+ var installedApps []string
+
+ if dev, ok := devices.DBDeviceMap[udid]; ok {
+ if dev.OS == "ios" {
+ installedApps = devices.GetInstalledAppsIOS(dev)
+ } else {
+ installedApps = devices.GetInstalledAppsAndroid(dev)
+ }
+ c.JSON(http.StatusOK, installedApps)
+ return
+ }
+ c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("Did not find device with udid `%s`", udid)})
+}
+
func DevicesInfo(c *gin.Context) {
deviceList := []*models.Device{}
- for _, device := range devices.DeviceMap {
+ for _, device := range devices.DBDeviceMap {
deviceList = append(deviceList, device)
}
@@ -261,7 +278,7 @@ type ProcessApp struct {
func UninstallApp(c *gin.Context) {
udid := c.Param("udid")
- if dev, ok := devices.DeviceMap[udid]; ok {
+ if dev, ok := devices.DBDeviceMap[udid]; ok {
payload, err := io.ReadAll(c.Request.Body)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid payload"})
@@ -275,7 +292,14 @@ func UninstallApp(c *gin.Context) {
return
}
- if slices.Contains(dev.InstalledApps, payloadJson.App) {
+ var installedApps []string
+ if dev.OS == "ios" {
+ installedApps = devices.GetInstalledAppsIOS(dev)
+ } else {
+ installedApps = devices.GetInstalledAppsAndroid(dev)
+ }
+
+ if slices.Contains(installedApps, payloadJson.App) {
err = devices.UninstallApp(dev, payloadJson.App)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to uninstall app `%s`", payloadJson.App)})
@@ -294,7 +318,7 @@ func UninstallApp(c *gin.Context) {
func ResetDevice(c *gin.Context) {
udid := c.Param("udid")
- if device, ok := devices.DeviceMap[udid]; ok {
+ if device, ok := devices.DBDeviceMap[udid]; ok {
device.IsResetting = true
device.CtxCancel()
device.ProviderState = "init"
diff --git a/provider/router/stream.go b/provider/router/stream.go
index f7a527c8..0e362fa2 100644
--- a/provider/router/stream.go
+++ b/provider/router/stream.go
@@ -15,6 +15,7 @@ import (
"strings"
"GADS/provider/devices"
+
"github.com/gin-gonic/gin"
"github.com/gobwas/ws"
"github.com/gobwas/ws/wsutil"
@@ -22,7 +23,7 @@ import (
func AndroidStreamProxy(c *gin.Context) {
udid := c.Param("udid")
- device := devices.DeviceMap[udid]
+ device := devices.DBDeviceMap[udid]
conn, _, _, err := ws.UpgradeHTTP(c.Request, c.Writer)
if err != nil {
@@ -62,7 +63,7 @@ func AndroidStreamMJPEG(c *gin.Context) {
c.Deadline()
udid := c.Param("udid")
- device := devices.DeviceMap[udid]
+ device := devices.DBDeviceMap[udid]
u := url.URL{Scheme: "ws", Host: "localhost:" + device.StreamPort, Path: ""}
conn, _, _, err := ws.DefaultDialer.Dial(context.Background(), u.String())
@@ -112,7 +113,7 @@ func IOSStreamMJPEG(c *gin.Context) {
c.Deadline()
udid := c.Param("udid")
- device := devices.DeviceMap[udid]
+ device := devices.DBDeviceMap[udid]
// Read data from device
server := "localhost:" + device.StreamPort
@@ -167,7 +168,7 @@ func IOSStreamMJPEG(c *gin.Context) {
func IOSStreamMJPEGWda(c *gin.Context) {
udid := c.Param("udid")
- device := devices.DeviceMap[udid]
+ device := devices.DBDeviceMap[udid]
// Set the necessary headers for MJPEG streaming
// Note: The "boundary" is arbitrary but must be unique and consistent.
@@ -239,7 +240,7 @@ func IOSStreamMJPEGWda(c *gin.Context) {
func IosStreamProxyGADS(c *gin.Context) {
udid := c.Param("udid")
- device := devices.DeviceMap[udid]
+ device := devices.DBDeviceMap[udid]
jpegChannel := make(chan []byte, 15)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
@@ -326,7 +327,7 @@ func IosStreamProxyGADS(c *gin.Context) {
func IosStreamProxyWDA(c *gin.Context) {
udid := c.Param("udid")
- device := devices.DeviceMap[udid]
+ device := devices.DBDeviceMap[udid]
conn, _, _, err := ws.UpgradeHTTP(c.Request, c.Writer)
if err != nil {