From 3919d4d429f23c00eec24c05ba72ea10a1d1e36a Mon Sep 17 00:00:00 2001 From: Maxim Date: Wed, 10 Sep 2025 22:00:58 +0300 Subject: [PATCH 01/23] Wire initial typescript (#338) * Wire initial typescript * Fix types * Something works * Finishing touches * Linter fixes * Build problems --- eslint.config.js | 18 +- google/protobuf/any.ts | 326 + lib/cli/please.js | 2 +- lib/db/handlers/group/index.js | 68 +- lib/db/index.ts | 20 +- lib/units/api/controllers/autotests.js | 3 +- lib/units/api/controllers/devices.js | 1 + lib/units/api/controllers/user.js | 20 +- lib/units/api/helpers/test.js | 11 + lib/units/api/helpers/useDevice.js | 5 +- lib/units/base-device/plugins/group.js | 7 +- lib/units/base-device/support/connector.js | 7 +- lib/units/base-device/support/push.ts | 2 +- lib/units/base-device/support/router.js | 2 +- lib/units/device/plugins/account.js | 11 +- lib/units/device/plugins/airplane.js | 3 +- lib/units/device/plugins/bluetooth.js | 7 +- lib/units/device/plugins/browser.js | 5 +- lib/units/device/plugins/clipboard.js | 5 +- lib/units/device/plugins/connect.js | 3 +- lib/units/device/plugins/filesystem.js | 5 +- lib/units/device/plugins/forward/index.js | 7 +- lib/units/device/plugins/group.js | 7 +- lib/units/device/plugins/install.js | 5 +- lib/units/device/plugins/logcat.js | 7 +- lib/units/device/plugins/reboot.js | 3 +- lib/units/device/plugins/ringer.js | 5 +- lib/units/device/plugins/screen/capture.js | 3 +- lib/units/device/plugins/screen/stream.js | 3 +- lib/units/device/plugins/sd.js | 3 +- lib/units/device/plugins/service.ts | 4 +- lib/units/device/plugins/shell.js | 5 +- lib/units/device/plugins/solo.js | 22 +- lib/units/device/plugins/store.js | 3 +- lib/units/device/plugins/touch/index.js | 15 +- lib/units/device/plugins/vnc/index.js | 3 +- lib/units/device/plugins/wifi.js | 5 +- lib/units/device/resources/service.js | 3 +- lib/units/groups-engine/watchers/devices.js | 3 +- lib/units/ios-device/plugins/clipboard.js | 3 +- lib/units/ios-device/plugins/devicelog.js | 7 +- lib/units/ios-device/plugins/filesystem.js | 5 +- lib/units/ios-device/plugins/install.js | 5 +- lib/units/ios-device/plugins/reboot.js | 3 +- lib/units/ios-device/plugins/wda/index.js | 29 +- lib/units/log/mongodb.js | 3 +- lib/units/processor/index.js | 95 +- lib/units/provider/ADBObserver.ts | 14 +- lib/units/provider/index.ts | 10 +- lib/units/reaper/index.js | 7 +- lib/units/tizen-device/index.js | 3 +- lib/units/tizen-device/plugins/filesystem.js | 5 +- lib/units/tizen-device/plugins/identity.js | 3 +- lib/units/tizen-device/plugins/install.js | 5 +- lib/units/tizen-device/plugins/launcher.js | 9 +- lib/units/vnc-device/plugins/group.js | 7 +- lib/units/vnc-device/plugins/screen/stream.js | 21 +- lib/units/websocket/index.js | 67 +- lib/util/apiutil.js | 4 +- lib/util/devutil.js | 2 +- lib/util/grouputil.js | 7 +- lib/util/lifecycle.js | 68 - lib/util/lifecycle.ts | 66 + lib/util/logger.ts | 15 +- lib/util/srv.ts | 3 +- lib/util/zmqutil.js | 8 +- lib/wire/google/protobuf/any.ts | 326 + lib/wire/index.js | 14 - lib/wire/index.ts | 35 + lib/wire/messagestream.ts | 2 +- lib/wire/router.ts | 154 +- lib/wire/transmanager.js | 3 +- lib/wire/util.js | 63 - lib/wire/util.ts | 108 + lib/wire/wire.proto | 143 +- lib/wire/wire.ts | 11545 ++++++++++++++++ package-lock.json | 524 +- package.json | 11 +- ui/src/store/device-connection.ts | 4 +- 79 files changed, 12985 insertions(+), 1058 deletions(-) create mode 100644 google/protobuf/any.ts create mode 100644 lib/units/api/helpers/test.js delete mode 100644 lib/util/lifecycle.js create mode 100644 lib/util/lifecycle.ts create mode 100644 lib/wire/google/protobuf/any.ts delete mode 100644 lib/wire/index.js create mode 100644 lib/wire/index.ts delete mode 100644 lib/wire/util.js create mode 100644 lib/wire/util.ts create mode 100644 lib/wire/wire.ts diff --git a/eslint.config.js b/eslint.config.js index 99f97b2655..02c704c90e 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -51,7 +51,8 @@ export default tseslint.config( }, }, { - ignores: ['dist'], + files: ['lib/**/*.js'], + // ignores: ['dist'], languageOptions: { ecmaVersion: 2025, sourceType: 'module', @@ -213,10 +214,17 @@ export default tseslint.config( 'no-restricted-modules': 0, 'no-sync': 0, 'no-async-promise-executor': 0, + + "no-restricted-imports": ["error", { + "patterns": [{ + "regex": ".*\\.ts", + "message": "Do not import typescript files. Import them with *.js" + }] + }] }, }, { - languageOptions: { + languageOptions: { parser: tseslint.parser, parserOptions: { projectService: true, @@ -224,4 +232,10 @@ export default tseslint.config( }, }, }, + tseslint.configs.eslintRecommended, + { + rules: { + "prefer-const": "off" + } + } ) diff --git a/google/protobuf/any.ts b/google/protobuf/any.ts new file mode 100644 index 0000000000..465a591802 --- /dev/null +++ b/google/protobuf/any.ts @@ -0,0 +1,326 @@ +// @generated by protobuf-ts 2.11.1 +// @generated from protobuf file "google/protobuf/any.proto" (package "google.protobuf", syntax proto3) +// tslint:disable +// +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +import type { BinaryWriteOptions } from "@protobuf-ts/runtime"; +import type { IBinaryWriter } from "@protobuf-ts/runtime"; +import { WireType } from "@protobuf-ts/runtime"; +import type { IBinaryReader } from "@protobuf-ts/runtime"; +import { UnknownFieldHandler } from "@protobuf-ts/runtime"; +import type { PartialMessage } from "@protobuf-ts/runtime"; +import { reflectionMergePartial } from "@protobuf-ts/runtime"; +import { isJsonObject } from "@protobuf-ts/runtime"; +import { typeofJsonValue } from "@protobuf-ts/runtime"; +import type { JsonValue } from "@protobuf-ts/runtime"; +import { jsonWriteOptions } from "@protobuf-ts/runtime"; +import type { JsonReadOptions } from "@protobuf-ts/runtime"; +import type { JsonWriteOptions } from "@protobuf-ts/runtime"; +import type { BinaryReadOptions } from "@protobuf-ts/runtime"; +import type { IMessageType } from "@protobuf-ts/runtime"; +import { MessageType } from "@protobuf-ts/runtime"; +/** + * `Any` contains an arbitrary serialized protocol buffer message along with a + * URL that describes the type of the serialized message. + * + * Protobuf library provides support to pack/unpack Any values in the form + * of utility functions or additional generated methods of the Any type. + * + * Example 1: Pack and unpack a message in C++. + * + * Foo foo = ...; + * Any any; + * any.PackFrom(foo); + * ... + * if (any.UnpackTo(&foo)) { + * ... + * } + * + * Example 2: Pack and unpack a message in Java. + * + * Foo foo = ...; + * Any any = Any.pack(foo); + * ... + * if (any.is(Foo.class)) { + * foo = any.unpack(Foo.class); + * } + * // or ... + * if (any.isSameTypeAs(Foo.getDefaultInstance())) { + * foo = any.unpack(Foo.getDefaultInstance()); + * } + * + * Example 3: Pack and unpack a message in Python. + * + * foo = Foo(...) + * any = Any() + * any.Pack(foo) + * ... + * if any.Is(Foo.DESCRIPTOR): + * any.Unpack(foo) + * ... + * + * Example 4: Pack and unpack a message in Go + * + * foo := &pb.Foo{...} + * any, err := anypb.New(foo) + * if err != nil { + * ... + * } + * ... + * foo := &pb.Foo{} + * if err := any.UnmarshalTo(foo); err != nil { + * ... + * } + * + * The pack methods provided by protobuf library will by default use + * 'type.googleapis.com/full.type.name' as the type URL and the unpack + * methods only use the fully qualified type name after the last '/' + * in the type URL, for example "foo.bar.com/x/y.z" will yield type + * name "y.z". + * + * JSON + * ==== + * The JSON representation of an `Any` value uses the regular + * representation of the deserialized, embedded message, with an + * additional field `@type` which contains the type URL. Example: + * + * package google.profile; + * message Person { + * string first_name = 1; + * string last_name = 2; + * } + * + * { + * "@type": "type.googleapis.com/google.profile.Person", + * "firstName": , + * "lastName": + * } + * + * If the embedded message type is well-known and has a custom JSON + * representation, that representation will be embedded adding a field + * `value` which holds the custom JSON in addition to the `@type` + * field. Example (for message [google.protobuf.Duration][]): + * + * { + * "@type": "type.googleapis.com/google.protobuf.Duration", + * "value": "1.212s" + * } + * + * + * @generated from protobuf message google.protobuf.Any + */ +export interface Any { + /** + * A URL/resource name that uniquely identifies the type of the serialized + * protocol buffer message. This string must contain at least + * one "/" character. The last segment of the URL's path must represent + * the fully qualified name of the type (as in + * `path/google.protobuf.Duration`). The name should be in a canonical form + * (e.g., leading "." is not accepted). + * + * In practice, teams usually precompile into the binary all types that they + * expect it to use in the context of Any. However, for URLs which use the + * scheme `http`, `https`, or no scheme, one can optionally set up a type + * server that maps type URLs to message definitions as follows: + * + * * If no scheme is provided, `https` is assumed. + * * An HTTP GET on the URL must yield a [google.protobuf.Type][] + * value in binary format, or produce an error. + * * Applications are allowed to cache lookup results based on the + * URL, or have them precompiled into a binary to avoid any + * lookup. Therefore, binary compatibility needs to be preserved + * on changes to types. (Use versioned type names to manage + * breaking changes.) + * + * Note: this functionality is not currently available in the official + * protobuf release, and it is not used for type URLs beginning with + * type.googleapis.com. As of May 2023, there are no widely used type server + * implementations and no plans to implement one. + * + * Schemes other than `http`, `https` (or the empty scheme) might be + * used with implementation specific semantics. + * + * + * @generated from protobuf field: string type_url = 1 + */ + typeUrl: string; + /** + * Must be a valid serialized protocol buffer of the above specified type. + * + * @generated from protobuf field: bytes value = 2 + */ + value: Uint8Array; +} +// @generated message type with reflection information, may provide speed optimized methods +class Any$Type extends MessageType { + constructor() { + super("google.protobuf.Any", [ + { no: 1, name: "type_url", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "value", kind: "scalar", T: 12 /*ScalarType.BYTES*/ } + ]); + } + /** + * Pack the message into a new `Any`. + * + * Uses 'type.googleapis.com/full.type.name' as the type URL. + */ + pack(message: T, type: IMessageType): Any { + return { + typeUrl: this.typeNameToUrl(type.typeName), value: type.toBinary(message), + }; + } + /** + * Unpack the message from the `Any`. + */ + unpack(any: Any, type: IMessageType, options?: Partial): T { + if (!this.contains(any, type)) + throw new Error("Cannot unpack google.protobuf.Any with typeUrl '" + any.typeUrl + "' as " + type.typeName + "."); + return type.fromBinary(any.value, options); + } + /** + * Does the given `Any` contain a packed message of the given type? + */ + contains(any: Any, type: IMessageType | string): boolean { + if (!any.typeUrl.length) + return false; + let wants = typeof type == "string" ? type : type.typeName; + let has = this.typeUrlToName(any.typeUrl); + return wants === has; + } + /** + * Convert the message to canonical JSON value. + * + * You have to provide the `typeRegistry` option so that the + * packed message can be converted to JSON. + * + * The `typeRegistry` option is also required to read + * `google.protobuf.Any` from JSON format. + */ + internalJsonWrite(any: Any, options: JsonWriteOptions): JsonValue { + if (any.typeUrl === "") + return {}; + let typeName = this.typeUrlToName(any.typeUrl); + let opt = jsonWriteOptions(options); + let type = opt.typeRegistry?.find(t => t.typeName === typeName); + if (!type) + throw new globalThis.Error("Unable to convert google.protobuf.Any with typeUrl '" + any.typeUrl + "' to JSON. The specified type " + typeName + " is not available in the type registry."); + let value = type.fromBinary(any.value, { readUnknownField: false }); + let json = type.internalJsonWrite(value, opt); + if (typeName.startsWith("google.protobuf.") || !isJsonObject(json)) + json = { value: json }; + json["@type"] = any.typeUrl; + return json; + } + internalJsonRead(json: JsonValue, options: JsonReadOptions, target?: Any): Any { + if (!isJsonObject(json)) + throw new globalThis.Error("Unable to parse google.protobuf.Any from JSON " + typeofJsonValue(json) + "."); + if (typeof json["@type"] != "string" || json["@type"] == "") + return this.create(); + let typeName = this.typeUrlToName(json["@type"]); + let type = options?.typeRegistry?.find(t => t.typeName == typeName); + if (!type) + throw new globalThis.Error("Unable to parse google.protobuf.Any from JSON. The specified type " + typeName + " is not available in the type registry."); + let value; + if (typeName.startsWith("google.protobuf.") && json.hasOwnProperty("value")) + value = type.fromJson(json["value"], options); + else { + let copy = Object.assign({}, json); + delete copy["@type"]; + value = type.fromJson(copy, options); + } + if (target === undefined) + target = this.create(); + target.typeUrl = json["@type"]; + target.value = type.toBinary(value); + return target; + } + typeNameToUrl(name: string): string { + if (!name.length) + throw new Error("invalid type name: " + name); + return "type.googleapis.com/" + name; + } + typeUrlToName(url: string): string { + if (!url.length) + throw new Error("invalid type url: " + url); + let slash = url.lastIndexOf("/"); + let name = slash > 0 ? url.substring(slash + 1) : url; + if (!name.length) + throw new Error("invalid type url: " + url); + return name; + } + create(value?: PartialMessage): Any { + const message = globalThis.Object.create((this.messagePrototype!)); + message.typeUrl = ""; + message.value = new Uint8Array(0); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: Any): Any { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* string type_url */ 1: + message.typeUrl = reader.string(); + break; + case /* bytes value */ 2: + message.value = reader.bytes(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: Any, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* string type_url = 1; */ + if (message.typeUrl !== "") + writer.tag(1, WireType.LengthDelimited).string(message.typeUrl); + /* bytes value = 2; */ + if (message.value.length) + writer.tag(2, WireType.LengthDelimited).bytes(message.value); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message google.protobuf.Any + */ +export const Any = new Any$Type(); diff --git a/lib/cli/please.js b/lib/cli/please.js index 68fcf377f6..1c513326fd 100644 --- a/lib/cli/please.js +++ b/lib/cli/please.js @@ -1 +1 @@ -import './index.ts' +import './index.js' diff --git a/lib/db/handlers/group/index.js b/lib/db/handlers/group/index.js index 7e540ad9be..74e7a8b92a 100644 --- a/lib/db/handlers/group/index.js +++ b/lib/db/handlers/group/index.js @@ -5,6 +5,7 @@ import wire from '../../../wire/index.js' import dbapi from '../../api.js' import GroupsScheduler from './scheduler.js' import {WireRouter} from '../../../wire/router.js' +import {GroupChangeMessage, GroupField, LeaveGroupMessage, UngroupMessage} from '../../../wire/wire.js' class GroupChangeHandler { @@ -31,12 +32,13 @@ class GroupChangeHandler { this.scheduler?.scheduleAllGroupsTasks() this.push.send([ channel, - wireutil.envelope(new wire.UngroupMessage(wireutil.toDeviceRequirements({ - serial: { - value: serial, - match: 'exact' - } - }))) + wireutil.pack(UngroupMessage, { + requirements: wireutil.toDeviceRequirements({ + serial: { + value: serial, + match: 'exact' + } + })}) ]) } @@ -50,32 +52,32 @@ class GroupChangeHandler { this.pushdev.send([ wireutil.global, - wireutil.envelope(new wire.GroupChangeMessage( - new wire.GroupField( - group.id - , group.name - , group.class - , group.privilege - , group.owner - , dates - , group.duration - , group.repetitions - , group.devices - , group.users - , group.state - , group.isActive - , group.moderators - ) - , action - , subscribers - , isChangedDates - , isChangedClass - , isAddedUser - , users - , isAddedDevice - , devices - , timeutil.now('nano') - )) + wireutil.pack(GroupChangeMessage, { + group: GroupField.create({ + id: group.id, + name: group.name, + class: group.class, + privilege: group.privilege, + owner: group.owner, + dates: dates, + duration: group.duration, + repetitions: group.repetitions, + devices: group.devices, + users: group.users, + state: group.state, + isActive: group.isActive, + moderators: group.moderators, + }), + action: action, + subscribers: subscribers, + isChangedDates: isChangedDates, + isChangedClass: isChangedClass, + isAddedUser: isAddedUser, + users: users, + isAddedDevice: isAddedDevice, + devices: devices, + timeStamp: timeutil.now('nano') + }) ]) } @@ -152,7 +154,7 @@ class GroupChangeHandler { } const listener = new WireRouter() - .on(wire.LeaveGroupMessage, (channel, message) => { + .on(LeaveGroupMessage, (channel, message) => { if (message.serial === serial && message.owner.email === email) { clearTimeout(responseTimer) diff --git a/lib/db/index.ts b/lib/db/index.ts index 26b6eec7d5..a4964c96d0 100644 --- a/lib/db/index.ts +++ b/lib/db/index.ts @@ -148,7 +148,7 @@ export default class DbClient { ) } catch (err) { - _log.fatal('Unable to connect to sub endpoint', err) + _log.fatal('Unable to connect to sub endpoint: %s', err) lifecycle.fatal() } }) @@ -173,7 +173,7 @@ export default class DbClient { ) } catch (err) { - _log.fatal('Unable to connect to subdev endpoint', err) + _log.fatal('Unable to connect to subdev endpoint: %s', err) lifecycle.fatal() } }) @@ -195,7 +195,7 @@ export default class DbClient { ) } catch (err) { - _log.fatal('Unable to connect to push endpoint', err) + _log.fatal('Unable to connect to push endpoint: %s', err) lifecycle.fatal() } }) @@ -218,7 +218,7 @@ export default class DbClient { } catch (err) { _log.fatal( - 'Unable to connect to pushdev endpoint', + 'Unable to connect to pushdev endpoint: %s', err ) lifecycle.fatal() @@ -254,13 +254,11 @@ export default class DbClient { // an issue with the processor unit, as it started processing messages before // it was actually truly able to save anything to the database. This lead to // lost messages in certain situations. - static ensureConnectivity = (fn: Function) => - function() { - let args = [].slice.call(arguments) - return DbClient.connect().then(function() { - return fn.apply(null, args) - }) - } + static ensureConnectivity = async (fn: T) => { + await DbClient.connect() + log.info("Db is up") + return fn + } // Sets up the database static setup = () => DbClient.connect().then((conn) => _setup(conn)) diff --git a/lib/units/api/controllers/autotests.js b/lib/units/api/controllers/autotests.js index 4a0423e839..93be14cafd 100644 --- a/lib/units/api/controllers/autotests.js +++ b/lib/units/api/controllers/autotests.js @@ -9,6 +9,7 @@ import logger from '../../../util/logger.js' import * as Sentry from '@sentry/node' import _ from 'lodash' import useDevice, {UseDeviceError} from '../helpers/useDevice.js' +import {InstallResultMessage} from '../../../wire/wire.js' const log = logger.createLogger('api:controllers:autotests') @@ -150,7 +151,7 @@ function installOnDevice(req, res) { return apiutil.respond(res, 504, 'Device is not responding') }, apiutil.INSTALL_APK_WAIT) let messageListener = new WireRouter() - .on(wire.InstallResultMessage, function(channel, message) { + .on(InstallResultMessage, function(channel, message) { if (message.serial === serial) { clearTimeout(timer) req.options.sub.unsubscribe(responseChannel) diff --git a/lib/units/api/controllers/devices.js b/lib/units/api/controllers/devices.js index c5be7246b9..7d9f916420 100644 --- a/lib/units/api/controllers/devices.js +++ b/lib/units/api/controllers/devices.js @@ -15,6 +15,7 @@ import * as jwtutil from '../../../util/jwtutil.js' import useDevice, {UseDeviceError} from '../helpers/useDevice.js' import * as Sentry from '@sentry/node' import {accessTokenAuth} from '../helpers/securityHandlers.js' +import {DeviceOriginGroupMessage} from '../../../wire/wire.js' var log = logger.createLogger('api:controllers:devices') /* ------------------------------------ PRIVATE FUNCTIONS ------------------------------- */ diff --git a/lib/units/api/controllers/user.js b/lib/units/api/controllers/user.js index 2bb043d0c0..6d1c7dd080 100644 --- a/lib/units/api/controllers/user.js +++ b/lib/units/api/controllers/user.js @@ -14,6 +14,7 @@ import * as jwtutil from '../../../util/jwtutil.js' import * as lockutil from '../../../util/lockutil.js' import * as Sentry from '@sentry/node' import generateToken from '../helpers/generateToken.js' +import {ConnectStartedMessage, ConnectStoppedMessage, JoinGroupMessage, LeaveGroupMessage, UngroupMessage} from '../../../wire/wire.js' import {runTransaction} from '../../../wire/transmanager.js' let log = logger.createLogger('api:controllers:user') @@ -132,7 +133,7 @@ function addUserDevice(req, res) { return apiutil.respond(res, 504, 'Device is not responding') }, apiutil.GRPC_WAIT_TIMEOUT) let messageListener = new WireRouter() - .on(wire.JoinGroupMessage, function(channel, message) { + .on(JoinGroupMessage, function(channel, message) { log.info(device.serial + ' added to user group ' + req.user) if (message.serial === serial && message.owner.email === req.user.email) { clearTimeout(responseTimer) @@ -208,12 +209,13 @@ function deleteUserDeviceBySerial(req, res) { } } - await runTransaction(device.channel, new wire.UngroupMessage(wireutil.toDeviceRequirements({ - serial: { - value: serial, - match: 'exact' - } - }))) + await runTransaction(device.channel, UngroupMessage.create({ + requirements: wireutil.toDeviceRequirements({ + serial: { + value: serial, + match: 'exact' + } + })})) }) .catch(function(err) { let errSerial @@ -259,7 +261,7 @@ function remoteConnectUserDeviceBySerial(req, res) { return apiutil.respond(res, 504, 'Device is not responding') }, apiutil.GRPC_WAIT_TIMEOUT) let messageListener = new WireRouter() - .on(wire.ConnectStartedMessage, function(channel, message) { + .on(ConnectStartedMessage, function(channel, message) { if (message.serial === serial) { clearTimeout(timer) req.options.sub.unsubscribe(responseChannel) @@ -332,7 +334,7 @@ function remoteDisconnectUserDeviceBySerial(req, res) { } }, apiutil.GRPC_WAIT_TIMEOUT) var messageListener = new WireRouter() - .on(wire.ConnectStoppedMessage, function(channel, message) { + .on(ConnectStoppedMessage, function(channel, message) { if (message.serial === serial) { clearTimeout(timer) req.options.sub.unsubscribe(responseChannel) diff --git a/lib/units/api/helpers/test.js b/lib/units/api/helpers/test.js new file mode 100644 index 0000000000..6a88e940c4 --- /dev/null +++ b/lib/units/api/helpers/test.js @@ -0,0 +1,11 @@ +import * as jwtutil from '../../../util/jwtutil.js' + +const a = jwtutil.encode({ + payload: { + email: user.email, + name: user.name + }, + secret: secret +}) + +console.log(a) diff --git a/lib/units/api/helpers/useDevice.js b/lib/units/api/helpers/useDevice.js index 7e769d7f3a..41e10af9b5 100644 --- a/lib/units/api/helpers/useDevice.js +++ b/lib/units/api/helpers/useDevice.js @@ -9,6 +9,7 @@ import wire from '../../../wire/index.js' import {v4 as uuidv4} from 'uuid' import {Log} from '../../../util/logger.js' import {runTransaction} from '../../../wire/transmanager.js' +import {ConnectStartedMessage, JoinGroupMessage} from '../../../wire/wire.js' export const UseDeviceError = Object.freeze({ NOT_FOUND: 0, @@ -61,7 +62,7 @@ const useDevice = ({user, device, channelRouter, push, sub, usage = null, log}) }, apiutil.GRPC_WAIT_TIMEOUT) const useDeviceMessageListener = new WireRouter() - .on(wire.JoinGroupMessage, function(channel, message) { + .on(JoinGroupMessage, function(channel, message) { log?.info(device.serial + ' added to user group ' + user) if (message.serial === device.serial && message.owner.email === user.email) { @@ -79,7 +80,7 @@ const useDevice = ({user, device, channelRouter, push, sub, usage = null, log}) }, apiutil.GRPC_WAIT_TIMEOUT) const messageListener = new WireRouter() - .on(wire.ConnectStartedMessage, function(channel, message) { + .on(ConnectStartedMessage, function(channel, message) { if (message.serial === device.serial) { clearTimeout(connectTimeout) sub.unsubscribe(responseChannel) diff --git a/lib/units/base-device/plugins/group.js b/lib/units/base-device/plugins/group.js index fbf8cf3f84..d01d8a6012 100755 --- a/lib/units/base-device/plugins/group.js +++ b/lib/units/base-device/plugins/group.js @@ -14,6 +14,7 @@ import router from '../support/router.js' import push from '../support/push.js' import sub from '../support/sub.js' import channels from '../support/channels.js' +import {AutoGroupMessage, GroupMessage, UngroupMessage} from '../../../wire/wire.js' export default syrup.serial() .dependency(solo) .dependency(router) @@ -91,7 +92,7 @@ export default syrup.serial() }) } router - .on(wire.GroupMessage, (channel, message) => { + .on(GroupMessage, (channel, message) => { let reply = wireutil.reply(options.serial) // grouputil.match(ident, message.requirements) plugin.join(message.owner, message.timeout, message.usage) @@ -114,7 +115,7 @@ export default syrup.serial() ]) }) }) - .on(wire.AutoGroupMessage, (channel, message) => { + .on(AutoGroupMessage, (channel, message) => { return plugin.join(message.owner, message.timeout, message.identifier) .then(() => { plugin.emit('autojoin', message.identifier, true) @@ -123,7 +124,7 @@ export default syrup.serial() plugin.emit('autojoin', message.identifier, false) }) }) - .on(wire.UngroupMessage, (channel, message) => { + .on(UngroupMessage, (channel, message) => { let reply = wireutil.reply(options.serial) Promise.method(() => { return plugin.leave('ungroup_request') diff --git a/lib/units/base-device/support/connector.js b/lib/units/base-device/support/connector.js index c3b5d0e2d3..4e48ca0bd6 100755 --- a/lib/units/base-device/support/connector.js +++ b/lib/units/base-device/support/connector.js @@ -6,6 +6,7 @@ import dbapi from '../../../db/api.js' import wireutil from '../../../wire/util.js' import db from '../../../db/index.js' import push from './push.js' +import {ConnectGetForwardUrlMessage, ConnectStartMessage, ConnectStopMessage} from '../../../wire/wire.js' /** * @typedef {{ @@ -52,13 +53,13 @@ export default syrup.serial() this.urlWithoutAdbPort = urlWithoutAdbPort router - .on(wire.ConnectStartMessage, + .on(ConnectStartMessage, (channel) => this.start(channel) ) - .on(wire.ConnectGetForwardUrlMessage, + .on(ConnectGetForwardUrlMessage, (channel) => this.getUrl(channel) ) - .on(wire.ConnectStopMessage, + .on(ConnectStopMessage, (channel) => this.stop(channel) ) } diff --git a/lib/units/base-device/support/push.ts b/lib/units/base-device/support/push.ts index 0fe7c58d4d..65c066af5a 100755 --- a/lib/units/base-device/support/push.ts +++ b/lib/units/base-device/support/push.ts @@ -32,7 +32,7 @@ export default syrup.serial().define( return push } catch (err) { - log.fatal('Unable to connect to sub endpoint', err) + log.fatal('Unable to connect to sub endpoint: %s', err) return lifecycle.fatal() // kill process } } diff --git a/lib/units/base-device/support/router.js b/lib/units/base-device/support/router.js index 26bf2a7cc6..ee9bb7a68a 100755 --- a/lib/units/base-device/support/router.js +++ b/lib/units/base-device/support/router.js @@ -9,7 +9,7 @@ export default syrup.serial() const router = new WireRouter() sub.on('message', router.handler()) // Special case, we're hooking into a message that's not actually routed. - router.on({$code: 'message'}, channel => { + router.on('message', channel => { channels.keepalive(channel) }) return router diff --git a/lib/units/device/plugins/account.js b/lib/units/device/plugins/account.js index 4323a982d3..52d9951393 100644 --- a/lib/units/device/plugins/account.js +++ b/lib/units/device/plugins/account.js @@ -8,6 +8,7 @@ import touch from './touch/index.js' import router from '../../base-device/support/router.js' import push from '../../base-device/support/push.js' import adb from '../support/adb.js' +import {AccountAddMenuMessage, AccountAddMessage, AccountCheckMessage, AccountGetMessage, AccountRemoveMessage} from '../../../wire/wire.js' export default syrup.serial() .dependency(service) .dependency(identity) @@ -27,7 +28,7 @@ export default syrup.serial() throw new Error('The account is not added') }) } - router.on(wire.AccountCheckMessage, function(channel, message) { + router.on(AccountCheckMessage, function(channel, message) { var reply = wireutil.reply(options.serial) log.info('Checking if account "%s" is added', message.account) checkAccount(message.type, message.account) @@ -45,7 +46,7 @@ export default syrup.serial() ]) }) }) - router.on(wire.AccountGetMessage, function(channel, message) { + router.on(AccountGetMessage, function(channel, message) { var reply = wireutil.reply(options.serial) log.info('Getting account(s)') service.getAccounts(message) @@ -64,7 +65,7 @@ export default syrup.serial() ]) }) }) - router.on(wire.AccountRemoveMessage, function(channel, message) { + router.on(AccountRemoveMessage, function(channel, message) { var reply = wireutil.reply(options.serial) log.info('Removing "%s" account(s)', message.type) service.removeAccount(message) @@ -83,7 +84,7 @@ export default syrup.serial() ]) }) }) - router.on(wire.AccountAddMenuMessage, function(channel) { + router.on(AccountAddMenuMessage, function(channel) { var reply = wireutil.reply(options.serial) log.info('Showing add account menu for Google Account') service.addAccountMenu() @@ -102,7 +103,7 @@ export default syrup.serial() ]) }) }) - router.on(wire.AccountAddMessage, function(channel, message) { + router.on(AccountAddMessage, function(channel, message) { var reply = wireutil.reply(options.serial) var type = 'com.google' var account = message.user + '@gmail.com' diff --git a/lib/units/device/plugins/airplane.js b/lib/units/device/plugins/airplane.js index eef54033a6..9649f57ec4 100644 --- a/lib/units/device/plugins/airplane.js +++ b/lib/units/device/plugins/airplane.js @@ -5,13 +5,14 @@ import wireutil from '../../../wire/util.js' import router from '../../base-device/support/router.js' import push from '../../base-device/support/push.js' import devutil from '../../../util/devutil.js' +import {AirplaneSetMessage} from '../../../wire/wire.js' export default syrup.serial() .dependency(devutil) .dependency(router) .dependency(push) .define(function(options, devutil, router, push) { var log = logger.createLogger('device:plugins:airplane') - router.on(wire.AirplaneSetMessage, async function(channel, message) { + router.on(AirplaneSetMessage, async function(channel, message) { const reply = wireutil.reply(options.serial) const enabled = message.enabled log.info('Setting airplane mode to', enabled) diff --git a/lib/units/device/plugins/bluetooth.js b/lib/units/device/plugins/bluetooth.js index dab8241ecd..b47ac65048 100644 --- a/lib/units/device/plugins/bluetooth.js +++ b/lib/units/device/plugins/bluetooth.js @@ -5,13 +5,14 @@ import wireutil from '../../../wire/util.js' import service from './service.js' import router from '../../base-device/support/router.js' import push from '../../base-device/support/push.js' +import {BluetoothCleanBondedMessage, BluetoothGetStatusMessage, BluetoothSetEnabledMessage} from '../../../wire/wire.js' export default syrup.serial() .dependency(service) .dependency(router) .dependency(push) .define(function(options, service, router, push) { var log = logger.createLogger('device:plugins:bluetooth') - router.on(wire.BluetoothSetEnabledMessage, function(channel, message) { + router.on(BluetoothSetEnabledMessage, function(channel, message) { var reply = wireutil.reply(options.serial) log.info('Setting Bluetooth "%s"', message.enabled) service.setBluetoothEnabled(message.enabled) @@ -30,7 +31,7 @@ export default syrup.serial() ]) }) }) - router.on(wire.BluetoothGetStatusMessage, function(channel) { + router.on(BluetoothGetStatusMessage, function(channel) { var reply = wireutil.reply(options.serial) log.info('Getting Bluetooth status') service.getBluetoothStatus() @@ -49,7 +50,7 @@ export default syrup.serial() ]) }) }) - router.on(wire.BluetoothCleanBondedMessage, function(channel) { + router.on(BluetoothCleanBondedMessage, function(channel) { var reply = wireutil.reply(options.serial) log.info('Clean bonded Bluetooth devices') service.cleanupBondedBluetoothDevices() diff --git a/lib/units/device/plugins/browser.js b/lib/units/device/plugins/browser.js index 86d65031e0..e1425ee2fe 100644 --- a/lib/units/device/plugins/browser.js +++ b/lib/units/device/plugins/browser.js @@ -7,6 +7,7 @@ import router from '../../base-device/support/router.js' import push from '../../base-device/support/push.js' import adb from '../support/adb.js' import service from './service.js' +import {BrowserClearMessage, BrowserOpenMessage} from '../../../wire/wire.js' const mapping = (function() { var list = Object.create(null) Object.keys(browsers).forEach(function(id) { @@ -76,7 +77,7 @@ export default syrup.serial() return (url.indexOf('://') === -1 ? 'http://' : '') + url } service.on('browserPackageChange', updateBrowsers) - router.on(wire.BrowserOpenMessage, function(channel, message) { + router.on(BrowserOpenMessage, function(channel, message) { message.url = ensureHttpProtocol(message.url) if (message.browser) { log.info('Opening "%s" in "%s"', message.url, message.browser) @@ -109,7 +110,7 @@ export default syrup.serial() ]) }) }) - router.on(wire.BrowserClearMessage, function(channel, message) { + router.on(BrowserClearMessage, function(channel, message) { log.info('Clearing "%s"', message.browser) var reply = wireutil.reply(options.serial) adb.getDevice(options.serial).clear(pkg(message.browser)) diff --git a/lib/units/device/plugins/clipboard.js b/lib/units/device/plugins/clipboard.js index b5c0a20582..30d0278673 100644 --- a/lib/units/device/plugins/clipboard.js +++ b/lib/units/device/plugins/clipboard.js @@ -5,13 +5,14 @@ import wireutil from '../../../wire/util.js' import router from '../../base-device/support/router.js' import push from '../../base-device/support/push.js' import service from './service.js' +import {CopyMessage, PasteMessage} from '../../../wire/wire.js' export default syrup.serial() .dependency(router) .dependency(push) .dependency(service) .define(function(options, router, push, service) { var log = logger.createLogger('device:plugins:clipboard') - router.on(wire.PasteMessage, function(channel, message) { + router.on(PasteMessage, function(channel, message) { log.info('Pasting "%s" to clipboard', message.text) var reply = wireutil.reply(options.serial) service.paste(message.text) @@ -29,7 +30,7 @@ export default syrup.serial() ]) }) }) - router.on(wire.CopyMessage, function(channel) { + router.on(CopyMessage, function(channel) { log.info('Copying clipboard contents') var reply = wireutil.reply(options.serial) service.copy() diff --git a/lib/units/device/plugins/connect.js b/lib/units/device/plugins/connect.js index 80b41d6bd1..51be539ca8 100644 --- a/lib/units/device/plugins/connect.js +++ b/lib/units/device/plugins/connect.js @@ -15,6 +15,7 @@ import urlformat from '../../base-device/support/urlformat.js' import identity from './util/identity.js' import data from './util/data.js' import {GRPC_WAIT_TIMEOUT} from '../../../util/apiutil.js' +import {AdbKeysUpdatedMessage} from '../../../wire/wire.js' // The promise passed as an argument will not be cancelled after the time has elapsed, // only the second promise will be rejected. @@ -83,7 +84,7 @@ export default syrup.serial() const auth = key => promiseTimeout(new Promise((resolve, reject) => { plugin.auth(key, resolve, reject) - router.on(wire.AdbKeysUpdatedMessage, () => notify(key)) + router.on(AdbKeysUpdatedMessage, () => notify(key)) notify(key) }), GRPC_WAIT_TIMEOUT) // reject after 2 minutes if autojoin event doesn't fire diff --git a/lib/units/device/plugins/filesystem.js b/lib/units/device/plugins/filesystem.js index 500e593a7f..a24964bb5b 100644 --- a/lib/units/device/plugins/filesystem.js +++ b/lib/units/device/plugins/filesystem.js @@ -7,6 +7,7 @@ import adb from '../support/adb.js' import router from '../../base-device/support/router.js' import push from '../../base-device/support/push.js' import storage from '../../base-device/support/storage.js' +import {FileSystemGetMessage, FileSystemListMessage} from '../../../wire/wire.js' export default syrup.serial() .dependency(adb) .dependency(router) @@ -34,7 +35,7 @@ export default syrup.serial() }) }) } - router.on(wire.FileSystemGetMessage, function(channel, message) { + router.on(FileSystemGetMessage, function(channel, message) { var reply = wireutil.reply(options.serial) plugin.retrieve(message.file, message.jwt) .then(function(file) { @@ -51,7 +52,7 @@ export default syrup.serial() ]) }) }) - router.on(wire.FileSystemListMessage, function(channel, message) { + router.on(FileSystemListMessage, function(channel, message) { var reply = wireutil.reply(options.serial) adb.getDevice(options.serial).readdir(message.dir) .then(function(files) { diff --git a/lib/units/device/plugins/forward/index.js b/lib/units/device/plugins/forward/index.js index bd929415c7..181ca1a560 100644 --- a/lib/units/device/plugins/forward/index.js +++ b/lib/units/device/plugins/forward/index.js @@ -13,6 +13,7 @@ import router from '../../../base-device/support/router.js' import push from '../../../base-device/support/push.js' import minirev from '../../resources/minirev.js' import group from '../group.js' +import {ForwardCreateMessage, ForwardRemoveMessage, ForwardTestMessage} from '../../../../wire/wire.js' export default syrup.serial() .dependency(adb) .dependency(router) @@ -108,7 +109,7 @@ export default syrup.serial() .then(awaitServer) .then(function() { router - .on(wire.ForwardTestMessage, function(channel, message) { + .on(ForwardTestMessage, function(channel, message) { var reply = wireutil.reply(options.serial) plugin.connect(message) .then(function(conn) { @@ -125,7 +126,7 @@ export default syrup.serial() ]) }) }) - .on(wire.ForwardCreateMessage, function(channel, message) { + .on(ForwardCreateMessage, function(channel, message) { var reply = wireutil.reply(options.serial) plugin.createForward(message.id, message) .then(function() { @@ -142,7 +143,7 @@ export default syrup.serial() ]) }) }) - .on(wire.ForwardRemoveMessage, function(channel, message) { + .on(ForwardRemoveMessage, function(channel, message) { var reply = wireutil.reply(options.serial) plugin.removeForward(message.id) .then(function() { diff --git a/lib/units/device/plugins/group.js b/lib/units/device/plugins/group.js index 90da7e25fd..6a31e8a0aa 100644 --- a/lib/units/device/plugins/group.js +++ b/lib/units/device/plugins/group.js @@ -16,6 +16,7 @@ import router from '../../base-device/support/router.js' import push from '../../base-device/support/push.js' import sub from '../../base-device/support/sub.js' import channels from '../../base-device/support/channels.js' +import {AutoGroupMessage, GroupMessage, UngroupMessage} from '../../../wire/wire.js' export default syrup.serial() .dependency(solo) .dependency(identity) @@ -143,7 +144,7 @@ export default syrup.serial() service.releaseWakeLock() }) router - .on(wire.GroupMessage, function(channel, message) { + .on(GroupMessage, function(channel, message) { let reply = wireutil.reply(options.serial) grouputil.match(ident, message.requirements) .then(function() { @@ -168,7 +169,7 @@ export default syrup.serial() ]) }) }) - .on(wire.AutoGroupMessage, function(channel, message) { + .on(AutoGroupMessage, function(channel, message) { return plugin.join(message.owner, message.timeout, message.identifier) .then(function() { plugin.emit('autojoin', message.identifier, true) @@ -177,7 +178,7 @@ export default syrup.serial() plugin.emit('autojoin', message.identifier, false) }) }) - .on(wire.UngroupMessage, function(channel, message) { + .on(UngroupMessage, function(channel, message) { let reply = wireutil.reply(options.serial) grouputil.match(ident, message.requirements) .then(function() { diff --git a/lib/units/device/plugins/install.js b/lib/units/device/plugins/install.js index e2384ef20f..9eae1703d2 100644 --- a/lib/units/device/plugins/install.js +++ b/lib/units/device/plugins/install.js @@ -11,6 +11,7 @@ import adb from '../support/adb.js' import router from '../../base-device/support/router.js' import push from '../../base-device/support/push.js' import storage from '../../base-device/support/storage.js' +import {InstallMessage, UninstallMessage} from '../../../wire/wire.js' // @ts-ignore const readAll = async(stream) => Utils.readAll(stream) @@ -24,7 +25,7 @@ export default syrup.serial() const log = logger.createLogger('device:plugins:install') const reply = wireutil.reply(options.serial) - router.on(wire.InstallMessage, async(channel, message) => { + router.on(InstallMessage, async(channel, message) => { const manifest = JSON.parse(message.manifest) const pkg = manifest.package const installFlags = message.installFlags @@ -197,7 +198,7 @@ export default syrup.serial() } }) - router.on(wire.UninstallMessage, async(channel, message) => { + router.on(UninstallMessage, async(channel, message) => { log.info('Uninstalling "%s"', message.packageName) try { await adb.getDevice(options.serial).uninstall(message.packageName) diff --git a/lib/units/device/plugins/logcat.js b/lib/units/device/plugins/logcat.js index b90f6b3b12..b88c9f2ab4 100644 --- a/lib/units/device/plugins/logcat.js +++ b/lib/units/device/plugins/logcat.js @@ -8,6 +8,7 @@ import adb from '../support/adb.js' import router from '../../base-device/support/router.js' import push from '../../base-device/support/push.js' import group from './group.js' +import {LogcatApplyFiltersMessage, LogcatStartMessage, LogcatStopMessage} from '../../../wire/wire.js' export default syrup.serial() .dependency(adb) .dependency(router) @@ -69,7 +70,7 @@ export default syrup.serial() lifecycle.observe(plugin.stop) group.on('leave', plugin.stop) router - .on(wire.LogcatStartMessage, function(channel, message) { + .on(LogcatStartMessage, function(channel, message) { var reply = wireutil.reply(options.serial) plugin.start(message.filters) .then(function() { @@ -86,7 +87,7 @@ export default syrup.serial() ]) }) }) - .on(wire.LogcatApplyFiltersMessage, function(channel, message) { + .on(LogcatApplyFiltersMessage, function(channel, message) { var reply = wireutil.reply(options.serial) plugin.reset(message.filters) .then(function() { @@ -103,7 +104,7 @@ export default syrup.serial() ]) }) }) - .on(wire.LogcatStopMessage, function(channel) { + .on(LogcatStopMessage, function(channel) { var reply = wireutil.reply(options.serial) plugin.stop() .then(function() { diff --git a/lib/units/device/plugins/reboot.js b/lib/units/device/plugins/reboot.js index 4e95cf4cfc..f872fe8a46 100644 --- a/lib/units/device/plugins/reboot.js +++ b/lib/units/device/plugins/reboot.js @@ -5,13 +5,14 @@ import wireutil from '../../../wire/util.js' import adb from '../support/adb.js' import router from '../../base-device/support/router.js' import push from '../../base-device/support/push.js' +import {RebootMessage} from '../../../wire/wire.js' export default syrup.serial() .dependency(adb) .dependency(router) .dependency(push) .define(function(options, adb, router, push) { const log = logger.createLogger('device:plugins:reboot') - router.on(wire.RebootMessage, function(channel) { + router.on(RebootMessage, function(channel) { let reply = wireutil.reply(options.serial) log.important('Rebooting') adb.getDevice(options.serial).reboot() diff --git a/lib/units/device/plugins/ringer.js b/lib/units/device/plugins/ringer.js index 61d4445fa5..bafbfe9a19 100644 --- a/lib/units/device/plugins/ringer.js +++ b/lib/units/device/plugins/ringer.js @@ -5,13 +5,14 @@ import wireutil from '../../../wire/util.js' import service from './service.js' import router from '../../base-device/support/router.js' import push from '../../base-device/support/push.js' +import {RingerGetMessage, RingerSetMessage} from '../../../wire/wire.js' export default syrup.serial() .dependency(service) .dependency(router) .dependency(push) .define(function(options, service, router, push) { var log = logger.createLogger('device:plugins:ringer') - router.on(wire.RingerSetMessage, function(channel, message) { + router.on(RingerSetMessage, function(channel, message) { var reply = wireutil.reply(options.serial) log.info('Setting ringer mode to mode "%s"', message.mode) service.setRingerMode(message.mode) @@ -30,7 +31,7 @@ export default syrup.serial() ]) }) }) - router.on(wire.RingerGetMessage, function(channel) { + router.on(RingerGetMessage, function(channel) { var reply = wireutil.reply(options.serial) log.info('Getting ringer mode') service.getRingerMode() diff --git a/lib/units/device/plugins/screen/capture.js b/lib/units/device/plugins/screen/capture.js index ed074b5e82..5d689d2be5 100644 --- a/lib/units/device/plugins/screen/capture.js +++ b/lib/units/device/plugins/screen/capture.js @@ -10,6 +10,7 @@ import push from '../../../base-device/support/push.js' import storage from '../../../base-device/support/storage.js' import minicap from '../../resources/minicap.js' import display from '../util/display.js' +import {ScreenCaptureMessage} from '../../../../wire/wire.js' export default syrup.serial() .dependency(adb) .dependency(router) @@ -48,7 +49,7 @@ export default syrup.serial() .then(Adb.util.readAll) }) } - router.on(wire.ScreenCaptureMessage, function(channel) { + router.on(ScreenCaptureMessage, function(channel) { var reply = wireutil.reply(options.serial) plugin.capture() .then(function(file) { diff --git a/lib/units/device/plugins/screen/stream.js b/lib/units/device/plugins/screen/stream.js index 77e2b52183..5305c28868 100644 --- a/lib/units/device/plugins/screen/stream.js +++ b/lib/units/device/plugins/screen/stream.js @@ -25,6 +25,7 @@ import options from './options.js' import group from '../group.js' import * as jwtutil from '../../../../util/jwtutil.js' import {NoGroupError} from '../../../../util/grouputil.js' +import {ChangeQualityMessage} from '../../../../wire/wire.js' export default syrup.serial() .dependency(adb) .dependency(router) @@ -439,7 +440,7 @@ export default syrup.serial() display.on('rotationChange', function(newRotation) { frameProducer.updateRotation(newRotation) }) - router.on(wire.ChangeQualityMessage, function(channel, message) { + router.on(ChangeQualityMessage, function(channel, message) { frameProducer.changeQuality(message.quality) }) frameProducer.on('start', function() { diff --git a/lib/units/device/plugins/sd.js b/lib/units/device/plugins/sd.js index 382315fd32..da4aca405c 100644 --- a/lib/units/device/plugins/sd.js +++ b/lib/units/device/plugins/sd.js @@ -5,13 +5,14 @@ import wireutil from '../../../wire/util.js' import service from './service.js' import router from '../../base-device/support/router.js' import push from '../../base-device/support/push.js' +import {SdStatusMessage} from '../../../wire/wire.js' export default syrup.serial() .dependency(service) .dependency(router) .dependency(push) .define(function(options, service, router, push) { var log = logger.createLogger('device:plugins:sd') - router.on(wire.SdStatusMessage, function(channel, message) { + router.on(SdStatusMessage, function(channel, message) { var reply = wireutil.reply(options.serial) log.info('Getting SD card status') service.getSdStatus(message) diff --git a/lib/units/device/plugins/service.ts b/lib/units/device/plugins/service.ts index 94c077c319..7971808d8c 100644 --- a/lib/units/device/plugins/service.ts +++ b/lib/units/device/plugins/service.ts @@ -560,7 +560,7 @@ export default syrup.serial() log.important('Agent connection ended, attempting to relaunch') try { await openAgent() - log.important('Agent relaunched in %dms', Date.now() - startTime) + log.important('Agent relaunched in %sms', Date.now() - startTime) resolve() } catch (err: any) { @@ -599,7 +599,7 @@ export default syrup.serial() log.important('Service connection ended, attempting to relaunch') await openAgent() // restart agent await openService() - log.important('Service relaunched in %dms', Date.now() - startTime) + log.important('Service relaunched in %sms', Date.now() - startTime) } catch (err: any) { log.fatal('[prepareForServiceDeath] Service connection could not be relaunched: %s', err?.message) diff --git a/lib/units/device/plugins/shell.js b/lib/units/device/plugins/shell.js index 8bbe76ceff..4ce1ef79c4 100644 --- a/lib/units/device/plugins/shell.js +++ b/lib/units/device/plugins/shell.js @@ -7,6 +7,7 @@ import adb from '../support/adb.js' import router from '../../base-device/support/router.js' import push from '../../base-device/support/push.js' import sub from '../../base-device/support/sub.js' +import {ShellCommandMessage, ShellKeepAliveMessage} from '../../../wire/wire.js' export default syrup.serial() .dependency(adb) .dependency(router) @@ -14,7 +15,7 @@ export default syrup.serial() .dependency(sub) .define(function(options, adb, router, push, sub) { var log = logger.createLogger('device:plugins:shell') - router.on(wire.ShellCommandMessage, async function(channel, message) { + router.on(ShellCommandMessage, async function(channel, message) { var reply = wireutil.reply(options.serial) log.info('Running shell command "%s"', message.command) const stream = await adb.getDevice(options.serial).shell(message.command) @@ -52,7 +53,7 @@ export default syrup.serial() stream.on('end', endListener) stream.on('error', errorListener) sub.subscribe(channel) - router.on(wire.ShellKeepAliveMessage, keepAliveListener) + router.on(ShellKeepAliveMessage, keepAliveListener) timer = setTimeout(forceStop, message.timeout) return resolver.promise.finally(function() { stream.removeListener('readable', readableListener) diff --git a/lib/units/device/plugins/solo.js b/lib/units/device/plugins/solo.js index dbc97524a1..e151d124e2 100644 --- a/lib/units/device/plugins/solo.js +++ b/lib/units/device/plugins/solo.js @@ -7,6 +7,7 @@ import sub from '../../base-device/support/sub.js' import push from '../../base-device/support/push.js' import router from '../../base-device/support/router.js' import identity from './util/identity.js' +import {DeviceDisplayMessage, DeviceIdentityMessage, DevicePhoneMessage, ProbeMessage} from '../../../wire/wire.js' export default syrup.serial() .dependency(sub) .dependency(push) @@ -24,10 +25,27 @@ export default syrup.serial() var channel = makeChannelId() log.info('Subscribing to permanent channel "%s"', channel) sub.subscribe(channel) - router.on(wire.ProbeMessage, function() { + router.on(ProbeMessage, function() { push.send([ wireutil.global, - wireutil.envelope(new wire.DeviceIdentityMessage(options.serial, identity.platform, identity.manufacturer, identity.operator, identity.model, identity.version, identity.abi, identity.sdk, new wire.DeviceDisplayMessage(identity.display), new wire.DevicePhoneMessage(identity.phone), identity.product, identity.cpuPlatform, identity.openGLESVersion, identity.marketName, identity.macAddress, identity.ram)) + wireutil.pack(DeviceIdentityMessage, { + serial: options.serial, + platform: identity.platform, + manufacturer: identity.manufacturer, + operator: identity.operator, + model: identity.model, + version: identity.version, + abi: identity.abi, + sdk: identity.sdk, + display: DeviceDisplayMessage.create(identity.display), + phone: DevicePhoneMessage.create(identity.phone), + product: identity.product, + cpuPlatform: identity.cpuPlatform, + openGLESVersion: identity.openGLESVersion, + marketName: identity.marketName, + macAddress: identity.macAddress, + ram: identity.ram + }) ]) }) return { diff --git a/lib/units/device/plugins/store.js b/lib/units/device/plugins/store.js index 8981525856..00d1a657eb 100644 --- a/lib/units/device/plugins/store.js +++ b/lib/units/device/plugins/store.js @@ -5,13 +5,14 @@ import wireutil from '../../../wire/util.js' import router from '../../base-device/support/router.js' import push from '../../base-device/support/push.js' import adb from '../support/adb.js' +import {StoreOpenMessage} from '../../../wire/wire.js' export default syrup.serial() .dependency(router) .dependency(push) .dependency(adb) .define(function(options, router, push, adb) { var log = logger.createLogger('device:plugins:store') - router.on(wire.StoreOpenMessage, function(channel) { + router.on(StoreOpenMessage, function(channel) { log.info('Opening Play Store') var reply = wireutil.reply(options.serial) adb.getDevice(options.serial).startActivity({ diff --git a/lib/units/device/plugins/touch/index.js b/lib/units/device/plugins/touch/index.js index 028b44a8cc..e3256c771b 100644 --- a/lib/units/device/plugins/touch/index.js +++ b/lib/units/device/plugins/touch/index.js @@ -15,6 +15,7 @@ import adb from '../../support/adb.js' import router from '../../../base-device/support/router.js' import minitouch from '../../resources/minitouch.js' import flags from '../util/flags.js' +import {GestureStartMessage, GestureStopMessage, TouchCommitMessage, TouchDownMessage, TouchMoveMessage, TouchResetMessage, TouchUpMessage} from '../../../../wire/wire.js' export default syrup.serial() .dependency(adb) .dependency(router) @@ -434,35 +435,35 @@ export default syrup.serial() lifecycle.fatal() }) router - .on(wire.GestureStartMessage, function(channel, message) { + .on(GestureStartMessage, function(channel, message) { queue.start(message.seq) }) - .on(wire.GestureStopMessage, function(channel, message) { + .on(GestureStopMessage, function(channel, message) { queue.push(message.seq, function() { queue.stop() }) }) - .on(wire.TouchDownMessage, function(channel, message) { + .on(TouchDownMessage, function(channel, message) { queue.push(message.seq, function() { touchConsumer.touchDown(message) }) }) - .on(wire.TouchMoveMessage, function(channel, message) { + .on(TouchMoveMessage, function(channel, message) { queue.push(message.seq, function() { touchConsumer.touchMove(message) }) }) - .on(wire.TouchUpMessage, function(channel, message) { + .on(TouchUpMessage, function(channel, message) { queue.push(message.seq, function() { touchConsumer.touchUp(message) }) }) - .on(wire.TouchCommitMessage, function(channel, message) { + .on(TouchCommitMessage, function(channel, message) { queue.push(message.seq, function() { touchConsumer.touchCommit() }) }) - .on(wire.TouchResetMessage, function(channel, message) { + .on(TouchResetMessage, function(channel, message) { queue.push(message.seq, function() { touchConsumer.touchReset() }) diff --git a/lib/units/device/plugins/vnc/index.js b/lib/units/device/plugins/vnc/index.js index 7fd31c34ff..f980a369df 100644 --- a/lib/units/device/plugins/vnc/index.js +++ b/lib/units/device/plugins/vnc/index.js @@ -19,6 +19,7 @@ import stream from '../screen/stream.js' import touch from '../touch/index.js' import group from '../group.js' import solo from '../solo.js' +import {VncAuthResponsesUpdatedMessage} from '../../../../wire/wire.js' export default syrup.serial() .dependency(router) .dependency(push) @@ -63,7 +64,7 @@ export default syrup.serial() } group.on('join', joinListener) group.on('autojoin', autojoinListener) - router.on(wire.VncAuthResponsesUpdatedMessage, notify) + router.on(VncAuthResponsesUpdatedMessage, notify) notify() return resolver.promise .timeout(5000) diff --git a/lib/units/device/plugins/wifi.js b/lib/units/device/plugins/wifi.js index 742796b927..8aace2359f 100644 --- a/lib/units/device/plugins/wifi.js +++ b/lib/units/device/plugins/wifi.js @@ -5,13 +5,14 @@ import wireutil from '../../../wire/util.js' import service from './service.js' import router from '../../base-device/support/router.js' import push from '../../base-device/support/push.js' +import {WifiGetStatusMessage, WifiSetEnabledMessage} from '../../../wire/wire.js' export default syrup.serial() .dependency(service) .dependency(router) .dependency(push) .define(function(options, service, router, push) { var log = logger.createLogger('device:plugins:wifi') - router.on(wire.WifiSetEnabledMessage, function(channel, message) { + router.on(WifiSetEnabledMessage, function(channel, message) { var reply = wireutil.reply(options.serial) log.info('Setting Wifi "%s"', message.enabled) service.setWifiEnabled(message.enabled) @@ -30,7 +31,7 @@ export default syrup.serial() ]) }) }) - router.on(wire.WifiGetStatusMessage, function(channel) { + router.on(WifiGetStatusMessage, function(channel) { var reply = wireutil.reply(options.serial) log.info('Getting Wifi status') service.getWifiStatus() diff --git a/lib/units/device/resources/service.js b/lib/units/device/resources/service.js index cb62d36c7d..8a10ed4841 100644 --- a/lib/units/device/resources/service.js +++ b/lib/units/device/resources/service.js @@ -22,7 +22,8 @@ export default syrup.serial() startIntent: { action: 'jp.co.cyberagent.stf.ACTION_START', component: 'jp.co.cyberagent.stf/.Service' - } + }, + path: '' } // am startservice -a jp.co.cyberagent.stf.ACTION_START jp.co.cyberagent.stf/.Service function getPath() { diff --git a/lib/units/groups-engine/watchers/devices.js b/lib/units/groups-engine/watchers/devices.js index 0dd21bef2e..d76ce6a9b3 100644 --- a/lib/units/groups-engine/watchers/devices.js +++ b/lib/units/groups-engine/watchers/devices.js @@ -8,6 +8,7 @@ import wireutil from '../../../wire/util.js' import wire from '../../../wire/index.js' import dbapi from '../../../db/api.js' import db from '../../../db/index.js' +import {LeaveGroupMessage} from '../../../wire/wire.js' export default (function(push, pushdev, channelRouter) { const log = logger.createLogger('watcher-devices') function sendReleaseDeviceControl(serial, channel) { @@ -48,7 +49,7 @@ export default (function(push, pushdev, channelRouter) { sendDeviceGroupChangeWrapper() }, 5000) messageListener = new WireRouter() - .on(wire.LeaveGroupMessage, function(channel, message) { + .on(LeaveGroupMessage, function(channel, message) { if (message.serial === device.serial && message.owner.email === device.owner.email) { clearTimeout(responseTimer) diff --git a/lib/units/ios-device/plugins/clipboard.js b/lib/units/ios-device/plugins/clipboard.js index b48c3e0c0e..140f4b3f69 100755 --- a/lib/units/ios-device/plugins/clipboard.js +++ b/lib/units/ios-device/plugins/clipboard.js @@ -5,13 +5,14 @@ import router from '../../base-device/support/router.js' import push from '../../base-device/support/push.js' import wdaClient from './wda/client.js' import Logger from '../../../util/logger.js' +import {CopyMessage} from '../../../wire/wire.js' const log = Logger.createLogger('ios-device:clipboard') export default syrup.serial() .dependency(router) .dependency(push) .dependency(wdaClient) .define(function(options, router, push, wdaClient) { - router.on(wire.CopyMessage, function(channel) { + router.on(CopyMessage, function(channel) { const reply = wireutil.reply(options.serial) wdaClient.getClipBoard() .then(clipboard => { diff --git a/lib/units/ios-device/plugins/devicelog.js b/lib/units/ios-device/plugins/devicelog.js index 2f0cf184df..534c2ec923 100755 --- a/lib/units/ios-device/plugins/devicelog.js +++ b/lib/units/ios-device/plugins/devicelog.js @@ -12,6 +12,7 @@ import sub from '../../base-device/support/sub.js' import group from '../../base-device/plugins/group.js' import nsyslogParser from 'nsyslog-parser' import db from '../../../db/index.js' +import {LogcatStartMessage, LogcatStopMessage, GroupMessage} from '../../../wire/wire.js' export default syrup.serial() .dependency(push) @@ -102,7 +103,7 @@ export default syrup.serial() group.on('leave', deviceLogger.killLoggingProcess) router - .on(wire.LogcatStartMessage, async function(channel, message) { + .on(LogcatStartMessage, async function(channel, message) { const reply = wireutil.reply(options.serial) try { await dbapi.loadDeviceBySerial(options.serial) @@ -114,12 +115,12 @@ export default syrup.serial() deviceLogger.killLoggingProcess() } }) - .on(wire.LogcatStopMessage, function(channel, data) { + .on(LogcatStopMessage, function(channel, data) { const reply = wireutil.reply(options.serial) deviceLogger.killLoggingProcess() push.send([channel, reply.okay('success')]) }) - .on(wire.GroupMessage, function(channel, data) { + .on(GroupMessage, function(channel, data) { deviceLogger.channel = channel }) diff --git a/lib/units/ios-device/plugins/filesystem.js b/lib/units/ios-device/plugins/filesystem.js index 13d5abbbf3..b9fc1b20e2 100644 --- a/lib/units/ios-device/plugins/filesystem.js +++ b/lib/units/ios-device/plugins/filesystem.js @@ -9,6 +9,7 @@ import {execFile} from 'child_process' import path from 'path' import fs from 'fs' import {v4 as uuidv4} from 'uuid' +import {FileSystemGetMessage, FileSystemListMessage} from '../../../wire/wire.js' export default syrup.serial() .dependency(router) .dependency(push) @@ -28,7 +29,7 @@ export default syrup.serial() } } - router.on(wire.FileSystemGetMessage, function(channel, message) { + router.on(FileSystemGetMessage, function(channel, message) { let reply = wireutil.reply(options.serial) let file = message.file let currentPath = file.split('/') @@ -72,7 +73,7 @@ export default syrup.serial() } ) }) - router.on(wire.FileSystemListMessage, function(channel, message) { + router.on(FileSystemListMessage, function(channel, message) { let reply = wireutil.reply(options.serial) let dirs = [] let rootDir = message.dir diff --git a/lib/units/ios-device/plugins/install.js b/lib/units/ios-device/plugins/install.js index 425950e190..0d95efd686 100755 --- a/lib/units/ios-device/plugins/install.js +++ b/lib/units/ios-device/plugins/install.js @@ -10,6 +10,7 @@ import router from '../../base-device/support/router.js' import push from '../../base-device/support/push.js' import storage from '../../base-device/support/storage.js' import deviceutil from '../../../util/deviceutil.js' +import {InstallMessage} from '../../../wire/wire.js' function execShellCommand(cmd) { return new Promise((resolve, reject) => { @@ -49,7 +50,7 @@ export default syrup.serial() const log = logger.createLogger('ios-device:plugins:install') const reply = wireutil.reply(options.serial) - router.on(wire.InstallMessage, async function(channel, message) { + router.on(InstallMessage, async function(channel, message) { log.info('Installing application from "%s"', message.href) const jwt = message.jwt @@ -99,7 +100,7 @@ export default syrup.serial() cleanup() }) }) - router.on(wire.UninstallIosMessage, function(channel, message) { + router.on(UninstallIosMessage, function(channel, message) { uninstallApp(options.serial, message.packageName) }) }) diff --git a/lib/units/ios-device/plugins/reboot.js b/lib/units/ios-device/plugins/reboot.js index 231944e61d..5c4cd53839 100755 --- a/lib/units/ios-device/plugins/reboot.js +++ b/lib/units/ios-device/plugins/reboot.js @@ -7,12 +7,13 @@ import wireutil from '../../../wire/util.js' import {execFileSync} from 'child_process' import router from '../../base-device/support/router.js' import push from '../../base-device/support/push.js' +import {RebootMessage} from '../../../wire/wire.js' export default syrup.serial() .dependency(router) .dependency(push) .define((options, router, push) => { const log = logger.createLogger('device:plugins:reboot') - router.on(wire.RebootMessage, (channel) => { + router.on(RebootMessage, (channel) => { log.important('Rebooting') const reply = wireutil.reply(options.serial) let udid = options.serial diff --git a/lib/units/ios-device/plugins/wda/index.js b/lib/units/ios-device/plugins/wda/index.js index a94d9e1871..acda59c6e4 100755 --- a/lib/units/ios-device/plugins/wda/index.js +++ b/lib/units/ios-device/plugins/wda/index.js @@ -12,6 +12,7 @@ import push from '../../../base-device/support/push.js' import sub from '../../../base-device/support/sub.js' import wdaClient from './client.js' import {Esp32Touch} from '../touch/esp32touch.js' +import {BrowserOpenMessage, DashboardOpenMessage, KeyDownMessage, KeyPressMessage, PhysicalIdentifyMessage, RotateMessage, ScreenCaptureMessage, StoreOpenMessage, TapDeviceTreeElement, TouchDownMessage, TouchMoveIosMessage, TouchMoveMessage, TouchUpMessage, TypeMessage} from '../../../../wire/wire.js' export default syrup.serial() .dependency(push) .dependency(sub) @@ -46,7 +47,7 @@ export default syrup.serial() }) } sub.on('message', new WireRouter() - .on(wire.KeyPressMessage, (channel, message) => { + .on(KeyPressMessage, (channel, message) => { if (wdaClient.orientation === 'LANDSCAPE' && message.key === 'home') { wdaClient.rotation({orientation: 'PORTRAIT'}) .then(() => { @@ -70,16 +71,16 @@ export default syrup.serial() } } }) - .on(wire.StoreOpenMessage, (channel, message) => { + .on(StoreOpenMessage, (channel, message) => { wdaClient.pressButton('store') }) - .on(wire.DashboardOpenMessage, (channel, message) => { + .on(DashboardOpenMessage, (channel, message) => { wdaClient.pressButton('settings') }) - .on(wire.PhysicalIdentifyMessage, (channel, message) => { + .on(PhysicalIdentifyMessage, (channel, message) => { wdaClient.pressButton('finder') }) - .on(wire.TouchDownMessage, (channel, message) => { + .on(TouchDownMessage, (channel, message) => { if(cursorDevice?.state === 'paired') { cursorDevice.press() } @@ -87,17 +88,17 @@ export default syrup.serial() wdaClient.tap(message) } }) - .on(wire.TouchMoveIosMessage, (channel, message) => { + .on(TouchMoveIosMessage, (channel, message) => { if(cursorDevice?.state !== 'paired') { wdaClient.swipe(message) } }) - .on(wire.TouchMoveMessage, (channel, message) => { + .on(TouchMoveMessage, (channel, message) => { if(cursorDevice?.state === 'paired') { cursorDevice.move(message.x, message.y) } }) - .on(wire.TouchUpMessage, (channel, message) => { + .on(TouchUpMessage, (channel, message) => { if(cursorDevice?.state === 'paired') { cursorDevice.release() } @@ -105,14 +106,14 @@ export default syrup.serial() wdaClient.touchUp() } }) - .on(wire.TapDeviceTreeElement, (channel, message) => { + .on(TapDeviceTreeElement, (channel, message) => { wdaClient.tapDeviceTreeElement(message) }) - .on(wire.TypeMessage, (channel, message) => { + .on(TypeMessage, (channel, message) => { log.verbose('wire.TypeMessage: ', message) wdaClient.typeKey({value: [iosutil.asciiparser(message.text)]}) }) - .on(wire.KeyDownMessage, (channel, message) => { + .on(KeyDownMessage, (channel, message) => { log.verbose('wire.KeyDownMessage: ', message) if (message.key === 'home') { wdaClient.homeBtn() @@ -121,10 +122,10 @@ export default syrup.serial() wdaClient.typeKey({value: [iosutil.asciiparser(message.key)]}) } }) - .on(wire.BrowserOpenMessage, (channel, message) => { + .on(BrowserOpenMessage, (channel, message) => { wdaClient.openUrl(message) }) - .on(wire.RotateMessage, (channel, message) => { + .on(RotateMessage, (channel, message) => { if (wdaClient.isRotating) { return } @@ -140,7 +141,7 @@ export default syrup.serial() log.error('Failed to rotate device to : ', rotation, err) }) }) - .on(wire.ScreenCaptureMessage, (channel, message) => { + .on(ScreenCaptureMessage, (channel, message) => { wdaClient.screenshot() .then(response => { let reply = wireutil.reply(options.serial) diff --git a/lib/units/log/mongodb.js b/lib/units/log/mongodb.js index 8691670cda..33a5c78b09 100644 --- a/lib/units/log/mongodb.js +++ b/lib/units/log/mongodb.js @@ -7,6 +7,7 @@ import lifecycle from '../../util/lifecycle.js' import srv from '../../util/srv.js' import dbapi from '../../db/api.js' import * as zmqutil from '../../util/zmqutil.js' +import {DeviceLogMessage} from '../../wire/wire.js' export default (function(options) { var log = logger.createLogger('log-db') // Input @@ -25,7 +26,7 @@ export default (function(options) { sub.subscribe(channel) }) sub.on('message', new WireRouter() - .on(wire.DeviceLogMessage, function(channel, message) { + .on(DeviceLogMessage, function(channel, message) { if (message.priority >= options.priority) { dbapi.saveDeviceLog(message.serial, message) } diff --git a/lib/units/processor/index.js b/lib/units/processor/index.js index 8a9398f7ad..0b714c6be1 100644 --- a/lib/units/processor/index.js +++ b/lib/units/processor/index.js @@ -8,8 +8,9 @@ import dbapi from '../../db/models/all/index.js' import lifecycle from '../../util/lifecycle.js' import srv from '../../util/srv.js' import * as zmqutil from '../../util/zmqutil.js' +import {UpdateAccessTokenMessage, DeleteUserMessage, DeviceChangeMessage, UserChangeMessage, GroupChangeMessage, DeviceGroupChangeMessage, GroupUserChangeMessage, DeviceHeartbeatMessage, DeviceLogMessage, TransactionProgressMessage, TransactionDoneMessage, TransactionTreeMessage, InstallResultMessage, DeviceLogcatEntryMessage, TemporarilyUnavailableMessage, UpdateRemoteConnectUrl, InstalledApplications, DeviceIntroductionMessage, InitializeIosDeviceState, DevicePresentMessage, DeviceAbsentMessage, DeviceStatusMessage, DeviceReadyMessage, JoinGroupByAdbFingerprintMessage, JoinGroupByVncAuthResponseMessage, ConnectStartedMessage, ConnectStoppedMessage, JoinGroupMessage, LeaveGroupMessage, DeviceIdentityMessage, AirplaneModeEvent, BatteryEvent, DeviceBrowserMessage, ConnectivityEvent, PhoneStateEvent, RotationEvent, CapabilitiesMessage, ReverseForwardsEvent, SetDeviceDisplay, UpdateIosDevice, SdkIosVersion, SizeIosDevice, DeviceTypeMessage, DeleteDevice, SetAbsentDisconnectedDevices, GetServicesAvailabilityMessage} from '../../wire/wire.js' -export default db.ensureConnectivity(async function(options) { +export default await db.ensureConnectivity(async function(options) { const log = logger.createLogger('processor') if (options.name) { logger.setGlobalIdentifier(options.name) @@ -66,24 +67,24 @@ export default db.ensureConnectivity(async function(options) { const defaultWireHandler = (channel, _, data) => appDealer.send([channel, data]) const router = new WireRouter() - .on(wire.UpdateAccessTokenMessage, defaultWireHandler) - .on(wire.DeleteUserMessage, defaultWireHandler) - .on(wire.DeviceChangeMessage, defaultWireHandler) - .on(wire.UserChangeMessage, defaultWireHandler) - .on(wire.GroupChangeMessage, defaultWireHandler) - .on(wire.DeviceGroupChangeMessage, defaultWireHandler) - .on(wire.GroupUserChangeMessage, defaultWireHandler) - .on(wire.DeviceHeartbeatMessage, defaultWireHandler) - .on(wire.DeviceLogMessage, defaultWireHandler) - .on(wire.TransactionProgressMessage, defaultWireHandler) - .on(wire.TransactionDoneMessage, defaultWireHandler) - .on(wire.TransactionTreeMessage, defaultWireHandler) - .on(wire.InstallResultMessage, defaultWireHandler) - .on(wire.DeviceLogcatEntryMessage, defaultWireHandler) - .on(wire.TemporarilyUnavailableMessage, defaultWireHandler) - .on(wire.UpdateRemoteConnectUrl, defaultWireHandler) - .on(wire.InstalledApplications, defaultWireHandler) - .on(wire.DeviceIntroductionMessage, async(channel, message, data) => { + .on(UpdateAccessTokenMessage, defaultWireHandler) + .on(DeleteUserMessage, defaultWireHandler) + .on(DeviceChangeMessage, defaultWireHandler) + .on(UserChangeMessage, defaultWireHandler) + .on(GroupChangeMessage, defaultWireHandler) + .on(DeviceGroupChangeMessage, defaultWireHandler) + .on(GroupUserChangeMessage, defaultWireHandler) + .on(DeviceHeartbeatMessage, defaultWireHandler) + .on(DeviceLogMessage, defaultWireHandler) + .on(TransactionProgressMessage, defaultWireHandler) + .on(TransactionDoneMessage, defaultWireHandler) + .on(TransactionTreeMessage, defaultWireHandler) + .on(InstallResultMessage, defaultWireHandler) + .on(DeviceLogcatEntryMessage, defaultWireHandler) + .on(TemporarilyUnavailableMessage, defaultWireHandler) + .on(UpdateRemoteConnectUrl, defaultWireHandler) + .on(InstalledApplications, defaultWireHandler) + .on(DeviceIntroductionMessage, async(channel, message, data) => { await dbapi.saveDeviceInitialState(message.serial, message) devDealer.send([ message.provider.channel, @@ -91,29 +92,29 @@ export default db.ensureConnectivity(async function(options) { ]) appDealer.send([channel, data]) }) - .on(wire.InitializeIosDeviceState, (channel, message, data) => { + .on(InitializeIosDeviceState, (channel, message, data) => { dbapi.initializeIosDeviceState(options.publicIp, message) }) - .on(wire.DevicePresentMessage, async(channel, message, data) => { + .on(DevicePresentMessage, async(channel, message, data) => { await dbapi.setDevicePresent(message.serial) appDealer.send([channel, data]) }) - .on(wire.DeviceAbsentMessage, async(channel, message, data) => { + .on(DeviceAbsentMessage, async(channel, message, data) => { if (!message.applications) { await dbapi.setDeviceAbsent(message.serial) appDealer.send([channel, data]) } }) - .on(wire.DeviceStatusMessage, (channel, message, data) => { + .on(DeviceStatusMessage, (channel, message, data) => { dbapi.saveDeviceStatus(message.serial, message.status) appDealer.send([channel, data]) }) - .on(wire.DeviceReadyMessage, async(channel, message, data) => { + .on(DeviceReadyMessage, async(channel, message, data) => { await dbapi.setDeviceReady(message.serial, message.channel) devDealer.send([message.channel, wireutil.envelope(new wire.ProbeMessage())]) appDealer.send([channel, data]) }) - .on(wire.JoinGroupByAdbFingerprintMessage, async(channel, message, data) => { + .on(JoinGroupByAdbFingerprintMessage, async(channel, message, data) => { try { const user = await dbapi.lookupUserByAdbFingerprint(message.fingerprint) if (user) { @@ -132,7 +133,7 @@ export default db.ensureConnectivity(async function(options) { log.error('Unable to lookup user by ADB fingerprint "%s"', message.fingerprint, err.stack) } }) - .on(wire.JoinGroupByVncAuthResponseMessage, async(channel, message, data) => { + .on(JoinGroupByVncAuthResponseMessage, async(channel, message, data) => { try { const user = await dbapi.lookupUserByVncAuthResponse(message.response, message.serial) if (user) { @@ -152,15 +153,15 @@ export default db.ensureConnectivity(async function(options) { log.error('Unable to lookup user by VNC auth response "%s"', message.response, err.stack) } }) - .on(wire.ConnectStartedMessage, async(channel, message, data) => { + .on(ConnectStartedMessage, async(channel, message, data) => { await dbapi.setDeviceConnectUrl(message.serial, message.url) appDealer.send([channel, data]) }) - .on(wire.ConnectStoppedMessage, async(channel, message, data) => { + .on(ConnectStoppedMessage, async(channel, message, data) => { await dbapi.unsetDeviceConnectUrl(message.serial) appDealer.send([channel, data]) }) - .on(wire.JoinGroupMessage, async(channel, message, data) => { + .on(JoinGroupMessage, async(channel, message, data) => { await Promise.all([ dbapi.setDeviceOwner(message.serial, message.owner), @@ -175,7 +176,7 @@ export default db.ensureConnectivity(async function(options) { ]) appDealer.send([channel, data]) }) - .on(wire.LeaveGroupMessage, async(channel, message, data) => { + .on(LeaveGroupMessage, async(channel, message, data) => { await Promise.all([ dbapi.unsetDeviceOwner(message.serial), dbapi.unsetDeviceUsage(message.serial), @@ -187,43 +188,43 @@ export default db.ensureConnectivity(async function(options) { ]) appDealer.send([channel, data]) }) - .on(wire.DeviceIdentityMessage, (channel, message, data) => { + .on(DeviceIdentityMessage, (channel, message, data) => { dbapi.saveDeviceIdentity(message.serial, message) appDealer.send([channel, data]) }) - .on(wire.AirplaneModeEvent, (channel, message, data) => { + .on(AirplaneModeEvent, (channel, message, data) => { dbapi.setDeviceAirplaneMode(message.serial, message.enabled) appDealer.send([channel, data]) }) - .on(wire.BatteryEvent, (channel, message, data) => { + .on(BatteryEvent, (channel, message, data) => { dbapi.setDeviceBattery(message.serial, message) appDealer.send([channel, data]) }) - .on(wire.DeviceBrowserMessage, (channel, message, data) => { + .on(DeviceBrowserMessage, (channel, message, data) => { dbapi.setDeviceBrowser(message.serial, message) appDealer.send([channel, data]) }) - .on(wire.ConnectivityEvent, (channel, message, data) => { + .on(ConnectivityEvent, (channel, message, data) => { dbapi.setDeviceConnectivity(message.serial, message) appDealer.send([channel, data]) }) - .on(wire.PhoneStateEvent, (channel, message, data) => { + .on(PhoneStateEvent, (channel, message, data) => { dbapi.setDevicePhoneState(message.serial, message) appDealer.send([channel, data]) }) - .on(wire.RotationEvent, (channel, message, data) => { + .on(RotationEvent, (channel, message, data) => { dbapi.setDeviceRotation(message) appDealer.send([channel, data]) }) - .on(wire.CapabilitiesMessage, (channel, message, data) => { + .on(CapabilitiesMessage, (channel, message, data) => { dbapi.setDeviceCapabilities(message) appDealer.send([channel, data]) }) - .on(wire.ReverseForwardsEvent, (channel, message, data) => { + .on(ReverseForwardsEvent, (channel, message, data) => { dbapi.setDeviceReverseForwards(message.serial, message.forwards) appDealer.send([channel, data]) }) - .on(wire.SetDeviceDisplay, (channel, message, data) => { + .on(SetDeviceDisplay, (channel, message, data) => { dbapi .setDeviceSocketDisplay(message) .then(function(response) { @@ -233,7 +234,7 @@ export default db.ensureConnectivity(async function(options) { log.error('setDeviceSocketDisplay', err) }) }) - .on(wire.UpdateIosDevice, (channel, message, data) => { + .on(UpdateIosDevice, (channel, message, data) => { dbapi .updateIosDevice(message) .then(result => { @@ -243,7 +244,7 @@ export default db.ensureConnectivity(async function(options) { log.info('UpdateIosDevice error: %s', err?.message) }) }) - .on(wire.SdkIosVersion, (channel, message, data) => { + .on(SdkIosVersion, (channel, message, data) => { dbapi .setDeviceIosVersion(message) .then(result => { @@ -253,7 +254,7 @@ export default db.ensureConnectivity(async function(options) { log.info('SdkIosVersion error: %s', err?.message) }) }) - .on(wire.SizeIosDevice, (channel, message, data) => { + .on(SizeIosDevice, (channel, message, data) => { dbapi.sizeIosDevice(message.id, message.height, message.width, message.scale).then(result => { log.info('SizeIosDevice: %s', result) }).catch(err => { @@ -261,16 +262,16 @@ export default db.ensureConnectivity(async function(options) { }) appDealer.send([channel, data]) }) - .on(wire.DeviceTypeMessage, (channel, message, data) => { + .on(DeviceTypeMessage, (channel, message, data) => { dbapi.setDeviceType(message.serial, message.type) }) - .on(wire.DeleteDevice, (channel, message, data) => { + .on(DeleteDevice, (channel, message, data) => { dbapi.deleteDevice(message.serial) }) - .on(wire.SetAbsentDisconnectedDevices, (channel, message, data) => { + .on(SetAbsentDisconnectedDevices, (channel, message, data) => { dbapi.setAbsentDisconnectedDevices() }) - .on(wire.GetServicesAvailabilityMessage, (channel, message, data) => { + .on(GetServicesAvailabilityMessage, (channel, message, data) => { dbapi.setDeviceServicesAvailability(message.serial, message) appDealer.send([channel, data]) }) diff --git a/lib/units/provider/ADBObserver.ts b/lib/units/provider/ADBObserver.ts index fcc37487da..33fcfade03 100644 --- a/lib/units/provider/ADBObserver.ts +++ b/lib/units/provider/ADBObserver.ts @@ -1,9 +1,11 @@ import EventEmitter from 'events' import net, {Socket} from 'net' +export type ADBDeviceType = 'unknown' | 'bootloader' | 'device' | 'recovery' | 'sideload' | 'offline' | 'unauthorized' | 'unknown' // https://android.googlesource.com/platform/system/core/+/android-4.4_r1/adb/adb.c#394 + interface ADBDevice { serial: string - type: 'device' | 'unknown' | 'offline' | 'unauthorized' | 'recovery' + type: ADBDeviceType reconnect: () => Promise } @@ -240,7 +242,7 @@ class ADBObserver extends EventEmitter { reject(new Error('Connection closed')) } this.pendingRequests.clear() - + // Auto-reconnect if we should continue polling if (this.shouldContinuePolling && !this.isDestroyed) { this.ensureConnection().catch(err => { @@ -258,7 +260,7 @@ class ADBObserver extends EventEmitter { /** * Process ADB protocol responses and return remaining buffer */ - private processADBResponses(buffer: Buffer): Buffer { + private processADBResponses(buffer: Buffer): Buffer { let offset = 0 while (offset < buffer.length) { @@ -277,7 +279,7 @@ class ADBObserver extends EventEmitter { } const responseData = buffer.subarray(offset + 8, offset + 8 + dataLength).toString('utf-8') - + if (status === 'OKAY') { // Find and resolve the corresponding request const requestId = 'host:devices' // For now, we only handle device listing @@ -308,7 +310,7 @@ class ADBObserver extends EventEmitter { */ private async sendADBCommand(command: string): Promise { const connection = await this.ensureConnection() - + return new Promise((resolve, reject) => { // Store the request for response matching this.pendingRequests.set(command, {resolve, reject}) @@ -337,7 +339,7 @@ class ADBObserver extends EventEmitter { this.connection.destroy() this.connection = null } - + // Reject any pending requests for (const [, {reject}] of this.pendingRequests) { reject(new Error('Connection closed')) diff --git a/lib/units/provider/index.ts b/lib/units/provider/index.ts index 3200c92653..9156a0884f 100644 --- a/lib/units/provider/index.ts +++ b/lib/units/provider/index.ts @@ -10,6 +10,7 @@ import db from '../../db/index.js' import dbapi from '../../db/api.js' import {ChildProcess} from 'node:child_process' import ADBObserver, {ADBDevice} from './ADBObserver.js' +import { DeviceRegisteredMessage } from '../../wire/wire.ts' interface DeviceWorker { state: 'waiting' | 'running' @@ -66,7 +67,7 @@ export default (async function(options: Options) { })) } catch (err) { - log.fatal('Unable to connect to push endpoint', err) + log.fatal('Unable to connect to push endpoint: %s', err) lifecycle.fatal() } @@ -87,7 +88,7 @@ export default (async function(options: Options) { }) sub.on('message', new WireRouter() - .on(wire.DeviceRegisteredMessage, (channel, message) => { + .on(DeviceRegisteredMessage, (channel, message) => { if (workers[message.serial]?.resolveRegister) { workers[message.serial].resolveRegister!() delete workers[message.serial]?.resolveRegister @@ -97,7 +98,7 @@ export default (async function(options: Options) { ) } catch (err) { - log.fatal('Unable to connect to sub endpoint', err) + log.fatal('Unable to connect to sub endpoint: %s', err) lifecycle.fatal() } @@ -160,8 +161,7 @@ export default (async function(options: Options) { proc.removeAllListeners('message') if (signal) { - log.warn('Device worker "%s" was killed with signal %s, assuming ' + - 'deliberate action and not restarting', device.serial, signal) + log.warn('Device worker "%s" was killed with signal %s, assuming deliberate action and not restarting', device.serial, signal) if (workers[device.serial].state === 'running') { workers[device.serial].terminate() diff --git a/lib/units/reaper/index.js b/lib/units/reaper/index.js index 20529d9711..af4a59f442 100644 --- a/lib/units/reaper/index.js +++ b/lib/units/reaper/index.js @@ -9,6 +9,7 @@ import srv from '../../util/srv.js' import TtlSet from '../../util/ttlset.js' import * as zmqutil from '../../util/zmqutil.js' import db from '../../db/index.js' +import {DeviceIntroductionMessage, DeviceHeartbeatMessage, DeviceAbsentMessage} from '../../wire/wire.js' const log = logger.createLogger('reaper') export default (async function(options) { @@ -76,15 +77,15 @@ export default (async function(options) { } function listenToChanges() { sub.on('message', new WireRouter() - .on(wire.DeviceIntroductionMessage, function(channel, message) { + .on(DeviceIntroductionMessage, function(channel, message) { message.status = 3 ttlset.drop(message.serial, TtlSet.SILENT) ttlset.bump(message.serial, Date.now()) }) - .on(wire.DeviceHeartbeatMessage, function(channel, message) { + .on(DeviceHeartbeatMessage, function(channel, message) { ttlset.bump(message.serial, Date.now()) }) - .on(wire.DeviceAbsentMessage, function(channel, message) { + .on(DeviceAbsentMessage, function(channel, message) { ttlset.drop(message.serial, TtlSet.SILENT) }) .handler()) diff --git a/lib/units/tizen-device/index.js b/lib/units/tizen-device/index.js index 0f0c5b6666..b2368f042d 100644 --- a/lib/units/tizen-device/index.js +++ b/lib/units/tizen-device/index.js @@ -16,6 +16,7 @@ import router from '../base-device/support/router.js' import identity from './plugins/identity.js' import launcher from './plugins/launcher.js' import filesystem from './plugins/filesystem.js' +import {DeviceRegisteredMessage} from '../../wire/wire.js' const log = logger.createLogger('tizen-device') const isTcpPortOpen = (host, port, timeout = 2_000) => new Promise((resolve) => { @@ -59,7 +60,7 @@ export default (async(options) => syrup.serial() let listener const waitRegister = Promise.race([ new Promise(resolve => - router.on(wire.DeviceRegisteredMessage, listener = (...args) => resolve(args)) + router.on(DeviceRegisteredMessage, listener = (...args) => resolve(args)) ), new Promise(r => setTimeout(r, 15000)) ]) diff --git a/lib/units/tizen-device/plugins/filesystem.js b/lib/units/tizen-device/plugins/filesystem.js index 3e4c3e1dce..63564c20ef 100644 --- a/lib/units/tizen-device/plugins/filesystem.js +++ b/lib/units/tizen-device/plugins/filesystem.js @@ -9,6 +9,7 @@ import logger from '../../../util/logger.js' import storage from '../../base-device/support/storage.js' import {basename} from 'path' import {unlink} from 'fs' +import {FileSystemGetMessage, FileSystemListMessage} from '../../../wire/wire.js' export default syrup.serial() .dependency(push) @@ -41,7 +42,7 @@ export default syrup.serial() return storedFile } - router.on(wire.FileSystemGetMessage, async(channel, message) => { + router.on(FileSystemGetMessage, async(channel, message) => { try { const file = await uploadFromDevice(message.file, message.jwt) push.send([ @@ -77,7 +78,7 @@ export default syrup.serial() })) } - router.on(wire.FileSystemListMessage, async(channel, message) => { + router.on(FileSystemListMessage, async(channel, message) => { try { push.send([ channel, diff --git a/lib/units/tizen-device/plugins/identity.js b/lib/units/tizen-device/plugins/identity.js index 8a694da8fb..9696f3e19f 100644 --- a/lib/units/tizen-device/plugins/identity.js +++ b/lib/units/tizen-device/plugins/identity.js @@ -5,6 +5,7 @@ import wire from '../../../wire/index.js' import wireutil from '../../../wire/util.js' import push from '../../base-device/support/push.js' import {exec} from 'child_process' +import {ProbeMessage} from '../../../wire/wire.js' export default syrup.serial() .dependency(push) @@ -29,7 +30,7 @@ export default syrup.serial() }) }) - router.on(wire.ProbeMessage, () => { + router.on(ProbeMessage, () => { push.send([ wireutil.global, wireutil.envelope(new wire.DeviceIdentityMessage( diff --git a/lib/units/tizen-device/plugins/install.js b/lib/units/tizen-device/plugins/install.js index e0f8ef1978..ceb3a5de1c 100644 --- a/lib/units/tizen-device/plugins/install.js +++ b/lib/units/tizen-device/plugins/install.js @@ -7,6 +7,7 @@ import storage from '../../base-device/support/storage.js' import sdb from './sdb' import launcher from './launcher' import deviceutil from '../../../util/deviceutil.js' +import {InstallMessage, UninstallIosMessage} from '../../../wire/wire.js' export default syrup.serial() .dependency(router) @@ -18,7 +19,7 @@ export default syrup.serial() const log = logger.createLogger('tizen-device:plugins:install') const reply = wireutil.reply(options.serial) - router.on(wire.InstallMessage, async function(channel, message) { + router.on(InstallMessage, async function(channel, message) { log.info('Installing application from "%s"', message.href, message.launch ? '[ LAUNCH ]' : '') const sendProgress = (data, progress) => @@ -45,7 +46,7 @@ export default syrup.serial() sendProgress('installing_app', 100) }) - router.on(wire.UninstallIosMessage, function(channel, message) { + router.on(UninstallIosMessage, function(channel, message) { uninstallApp(options.serial, message.packageName) }) }) diff --git a/lib/units/tizen-device/plugins/launcher.js b/lib/units/tizen-device/plugins/launcher.js index ee4420c540..ae9839a883 100644 --- a/lib/units/tizen-device/plugins/launcher.js +++ b/lib/units/tizen-device/plugins/launcher.js @@ -6,6 +6,7 @@ import sdb from './sdb/index.js' import wire from '../../../wire/index.js' import wireutil from '../../../wire/util.js' import webinspector from './webinspector.js' +import {GetInstalledApplications, KillDeviceApp, LaunchDeviceApp, TerminateDeviceApp} from '../../../wire/wire.js' export default syrup.serial() .dependency(push) @@ -57,16 +58,16 @@ export default syrup.serial() group.on('join', () => plugin.killApp(true)) router - .on(wire.GetInstalledApplications, async(channel) => + .on(GetInstalledApplications, async(channel) => success(channel, Object.fromEntries(await sdb.getApps())) ) - .on(wire.LaunchDeviceApp, plugin.launchApp) + .on(LaunchDeviceApp, plugin.launchApp) - .on(wire.TerminateDeviceApp, async(channel) => + .on(TerminateDeviceApp, async(channel) => success(channel, await plugin.killApp()) ) - .on(wire.KillDeviceApp, async(channel) => + .on(KillDeviceApp, async(channel) => success(channel, await plugin.killApp(true)) ) } diff --git a/lib/units/vnc-device/plugins/group.js b/lib/units/vnc-device/plugins/group.js index 50fa723816..11dc7d4d52 100755 --- a/lib/units/vnc-device/plugins/group.js +++ b/lib/units/vnc-device/plugins/group.js @@ -14,6 +14,7 @@ import router from '../../base-device/support/router.js' import push from '../../base-device/support/push.js' import sub from '../../base-device/support/sub.js' import channels from '../../base-device/support/channels.js' +import {GroupMessage, AutoGroupMessage, UngroupMessage} from '../../../wire/wire.js' export default syrup.serial() .dependency(solo) @@ -116,7 +117,7 @@ export default syrup.serial() } router - .on(wire.GroupMessage, (channel, message) => { + .on(GroupMessage, (channel, message) => { let reply = wireutil.reply(options.serial) Promise.method(() => { return plugin.join(message.owner, message.timeout, message.usage) @@ -140,7 +141,7 @@ export default syrup.serial() ]) }) }) - .on(wire.AutoGroupMessage, (channel, message) => { + .on(AutoGroupMessage, (channel, message) => { return plugin.join(message.owner, message.timeout, message.identifier) .then(() => { plugin.emit('autojoin', message.identifier, true) @@ -149,7 +150,7 @@ export default syrup.serial() plugin.emit('autojoin', message.identifier, false) }) }) - .on(wire.UngroupMessage, (channel, message) => { + .on(UngroupMessage, (channel, message) => { let reply = wireutil.reply(options.serial) Promise.method(() => { return plugin.leave('ungroup_request') diff --git a/lib/units/vnc-device/plugins/screen/stream.js b/lib/units/vnc-device/plugins/screen/stream.js index 6bc74f1c44..9dc4d22608 100755 --- a/lib/units/vnc-device/plugins/screen/stream.js +++ b/lib/units/vnc-device/plugins/screen/stream.js @@ -15,6 +15,7 @@ import router from '../../../base-device/support/router.js' import group from '../group.js' import {decode} from '../../../../util/jwtutil.js' import {NoGroupError} from '../../../../util/grouputil.js' +import {GestureStartMessage, GestureStopMessage, TouchDownMessage, TouchMoveMessage, TouchUpMessage, TouchCommitMessage, TouchResetMessage, TypeMessage, KeyDownMessage, KeyUpMessage} from '../../../../wire/wire.js' export default syrup.serial() .dependency(solo) @@ -127,34 +128,34 @@ export default syrup.serial() }) router - .on(wire.GestureStartMessage, function(channel, message) { + .on(GestureStartMessage, function(channel, message) { }) - .on(wire.GestureStopMessage, function(channel, message) { + .on(GestureStopMessage, function(channel, message) { }) - .on(wire.TouchDownMessage, function(channel, message) { + .on(TouchDownMessage, function(channel, message) { lastClicked.x = message.x * height lastClicked.y = message.y * width r.pointerEvent(message.x * height, message.y * width, 1) }) - .on(wire.TouchMoveMessage, function(channel, message) { + .on(TouchMoveMessage, function(channel, message) { }) - .on(wire.TouchUpMessage, function(channel, message) { + .on(TouchUpMessage, function(channel, message) { }) - .on(wire.TouchCommitMessage, function(channel, message) { + .on(TouchCommitMessage, function(channel, message) { }) - .on(wire.TouchResetMessage, function(channel, message) { + .on(TouchResetMessage, function(channel, message) { }) - .on(wire.TypeMessage, function(channel, message) { + .on(TypeMessage, function(channel, message) { let keyCode = message.text.charCodeAt(0) r.keyEvent(keyCode, 1) r.keyEvent(keyCode, 0) r.requestUpdate(false, 0, 0, r.width, r.height) }) - .on(wire.KeyDownMessage, function(channel, message) { + .on(KeyDownMessage, function(channel, message) { r.keyEvent(keyNameToX11KeyCode(message.key), 1) r.requestUpdate(false, 0, 0, r.width, r.height) }) - .on(wire.KeyUpMessage, function(channel, message) { + .on(KeyUpMessage, function(channel, message) { r.keyEvent(keyNameToX11KeyCode(message.key), 0) r.requestUpdate(false, 0, 0, r.width, r.height) }) diff --git a/lib/units/websocket/index.js b/lib/units/websocket/index.js index bb0153eedf..70243e570a 100644 --- a/lib/units/websocket/index.js +++ b/lib/units/websocket/index.js @@ -25,6 +25,7 @@ import {Server} from 'socket.io' import db from '../../db/index.js' import EventEmitter from 'events' import generateToken from '../api/helpers/generateToken.js' +import {UpdateAccessTokenMessage, DeleteUserMessage, DeviceChangeMessage, UserChangeMessage, GroupChangeMessage, DeviceGroupChangeMessage, GroupUserChangeMessage, DeviceLogMessage, DeviceIntroductionMessage, DeviceReadyMessage, DevicePresentMessage, DeviceAbsentMessage, InstalledApplications, JoinGroupMessage, JoinGroupByAdbFingerprintMessage, LeaveGroupMessage, DeviceStatusMessage, DeviceIdentityMessage, TransactionProgressMessage, TransactionDoneMessage, TransactionTreeMessage, DeviceLogcatEntryMessage, AirplaneModeEvent, BatteryEvent, GetServicesAvailabilityMessage, DeviceBrowserMessage, ConnectivityEvent, PhoneStateEvent, RotationEvent, CapabilitiesMessage, ReverseForwardsEvent, TemporarilyUnavailableMessage, UpdateRemoteConnectUrl} from '../../wire/wire.js' const request = Promise.promisifyAll(postmanRequest) export default (async function(options) { const log = logger.createLogger('websocket') @@ -103,13 +104,13 @@ export default (async function(options) { } let disconnectSocket var messageListener = new WireRouter() - .on(wire.UpdateAccessTokenMessage, function() { + .on(UpdateAccessTokenMessage, function() { socket.emit('user.keys.accessToken.updated') }) - .on(wire.DeleteUserMessage, function() { + .on(DeleteUserMessage, function() { disconnectSocket(true) }) - .on(wire.DeviceChangeMessage, function(channel, message) { + .on(DeviceChangeMessage, function(channel, message) { if (user.groups.subscribed.indexOf(message.device.group.id) > -1) { socket.emit('device.change', { important: true, @@ -124,12 +125,12 @@ export default (async function(options) { socket.emit('user.settings.devices.' + message.action, message) } }) - .on(wire.UserChangeMessage, function(channel, message) { + .on(UserChangeMessage, function(channel, message) { Promise.map(message.targets, function(target) { socket.emit('user.' + target + '.users.' + message.action, message) }) }) - .on(wire.GroupChangeMessage, function(channel, message) { + .on(GroupChangeMessage, function(channel, message) { if (user.privilege === 'admin' || user.email === message.group.owner.email || !apiutil.isOriginGroup(message.group.class) && @@ -140,7 +141,7 @@ export default (async function(options) { socket.emit('user.view.groups.' + message.action, message) } }) - .on(wire.DeviceGroupChangeMessage, function(channel, message) { + .on(DeviceGroupChangeMessage, function(channel, message) { if (user.groups.subscribed.indexOf(message.id) > -1) { if (user.groups.subscribed.indexOf(message.group.id) > -1) { socket.emit('device.updateGroupDevice', { @@ -159,7 +160,7 @@ export default (async function(options) { socket.emit('device.addGroupDevices', {important: true, devices: [message.serial]}) } }) - .on(wire.GroupUserChangeMessage, function(channel, message) { + .on(GroupUserChangeMessage, function(channel, message) { if (message.users.indexOf(user.email) > -1) { if (message.isAdded) { user.groups.subscribed = _.union(user.groups.subscribed, [message.id]) @@ -182,10 +183,10 @@ export default (async function(options) { } } }) - .on(wire.DeviceLogMessage, function(channel, message) { + .on(DeviceLogMessage, function(channel, message) { io.emit('logcat.log', message) }) - .on(wire.DeviceIntroductionMessage, function(channel, message) { + .on(DeviceIntroductionMessage, function(channel, message) { if (message && message.group && user.groups.subscribed.indexOf(message.group.id) > -1) { io.emit('device.add', { important: true, @@ -202,7 +203,7 @@ export default (async function(options) { }) } }) - .on(wire.DeviceReadyMessage, function(channel, message) { + .on(DeviceReadyMessage, function(channel, message) { io.emit('device.change', { important: true, data: { @@ -214,7 +215,7 @@ export default (async function(options) { } }) }) - .on(wire.DevicePresentMessage, function(channel, message) { + .on(DevicePresentMessage, function(channel, message) { io.emit('device.change', { important: true, data: { @@ -223,7 +224,7 @@ export default (async function(options) { } }) }) - .on(wire.DeviceAbsentMessage, function(channel, message) { + .on(DeviceAbsentMessage, function(channel, message) { io.emit('device.remove', { important: true, data: { @@ -233,7 +234,7 @@ export default (async function(options) { } }) }) - .on(wire.InstalledApplications, function(channel, message, data) { + .on(InstalledApplications, function(channel, message, data) { socket.emit('device.applications', { important: true, data: { @@ -243,7 +244,7 @@ export default (async function(options) { }) }) // @TODO refactore JoimGroupMessage route - .on(wire.JoinGroupMessage, function(channel, message) { + .on(JoinGroupMessage, function(channel, message) { dbapi.getInstalledApplications({serial: message.serial}) .then(applications => { if (!user?.ownedChannels) { @@ -283,13 +284,13 @@ export default (async function(options) { }) }) }) - .on(wire.JoinGroupByAdbFingerprintMessage, function(channel, message) { + .on(JoinGroupByAdbFingerprintMessage, function(channel, message) { socket.emit('user.keys.adb.confirm', { title: message.comment, fingerprint: message.fingerprint }) }) - .on(wire.LeaveGroupMessage, function(channel, message) { + .on(LeaveGroupMessage, function(channel, message) { io.emit('device.change', { important: true, data: datautil.applyOwner({ @@ -299,33 +300,33 @@ export default (async function(options) { }, user) }) }) - .on(wire.DeviceStatusMessage, function(channel, message) { + .on(DeviceStatusMessage, function(channel, message) { message.likelyLeaveReason = 'status_change' io.emit('device.change', { important: true, data: message }) }) - .on(wire.DeviceIdentityMessage, function(channel, message) { + .on(DeviceIdentityMessage, function(channel, message) { datautil.applyData(message) io.emit('device.change', { important: true, data: message }) }) - .on(wire.TransactionProgressMessage, function(channel, message) { + .on(TransactionProgressMessage, function(channel, message) { socket.emit('tx.progress', channel.toString(), message) }) - .on(wire.TransactionDoneMessage, function(channel, message) { + .on(TransactionDoneMessage, function(channel, message) { socket.emit('tx.done', channel.toString(), message) }) - .on(wire.TransactionTreeMessage, function(channel, message) { + .on(TransactionTreeMessage, function(channel, message) { socket.emit('tx.tree', channel.toString(), message) }) - .on(wire.DeviceLogcatEntryMessage, function(channel, message) { + .on(DeviceLogcatEntryMessage, function(channel, message) { socket.emit('logcat.entry', message) }) - .on(wire.AirplaneModeEvent, function(channel, message) { + .on(AirplaneModeEvent, function(channel, message) { io.emit('device.change', { important: true, data: { @@ -334,7 +335,7 @@ export default (async function(options) { } }) }) - .on(wire.BatteryEvent, function(channel, message) { + .on(BatteryEvent, function(channel, message) { var {serial} = message delete message.serial io.emit('device.change', { @@ -345,7 +346,7 @@ export default (async function(options) { } }) }) - .on(wire.GetServicesAvailabilityMessage, function(channel, message) { + .on(GetServicesAvailabilityMessage, function(channel, message) { let serial = message.serial delete message.serial io.emit('device.change', { @@ -356,7 +357,7 @@ export default (async function(options) { } }) }) - .on(wire.DeviceBrowserMessage, function(channel, message) { + .on(DeviceBrowserMessage, function(channel, message) { var {serial} = message delete message.serial io.emit('device.change', { @@ -367,7 +368,7 @@ export default (async function(options) { }) }) }) - .on(wire.ConnectivityEvent, function(channel, message) { + .on(ConnectivityEvent, function(channel, message) { var {serial} = message delete message.serial io.emit('device.change', { @@ -378,7 +379,7 @@ export default (async function(options) { } }) }) - .on(wire.PhoneStateEvent, function(channel, message) { + .on(PhoneStateEvent, function(channel, message) { var {serial} = message delete message.serial io.emit('device.change', { @@ -389,7 +390,7 @@ export default (async function(options) { } }) }) - .on(wire.RotationEvent, function(channel, message) { + .on(RotationEvent, function(channel, message) { socket.emit('device.change', { important: false, data: { @@ -400,7 +401,7 @@ export default (async function(options) { } }) }) - .on(wire.CapabilitiesMessage, function(channel, message) { + .on(CapabilitiesMessage, function(channel, message) { socket.emit('device.change', { important: false, data: { @@ -412,7 +413,7 @@ export default (async function(options) { } }) }) - .on(wire.ReverseForwardsEvent, function(channel, message) { + .on(ReverseForwardsEvent, function(channel, message) { socket.emit('device.change', { important: false, data: { @@ -421,14 +422,14 @@ export default (async function(options) { } }) }) - .on(wire.TemporarilyUnavailableMessage, function(channel, message) { + .on(TemporarilyUnavailableMessage, function(channel, message) { socket.emit('temporarily-unavailable', { data: { removeConnectUrl: message.removeConnectUrl } }) }) - .on(wire.UpdateRemoteConnectUrl, function(channel, message) { + .on(UpdateRemoteConnectUrl, function(channel, message) { socket.emit('device.change', { important: true, data: { diff --git a/lib/util/apiutil.js b/lib/util/apiutil.js index d10c77cf87..7e33ab56bb 100644 --- a/lib/util/apiutil.js +++ b/lib/util/apiutil.js @@ -3,9 +3,9 @@ import _ from 'lodash' import logger from './logger.js' import datautil from './datautil.js' import wireutil from '../wire/util.js' -import wire from '../wire/index.js' import * as Sentry from '@sentry/node' import {v4 as uuidv4} from 'uuid' +import {ConnectGetForwardUrlMessage} from '../wire/wire.js' const log = logger.createLogger('api:controllers:apiutil') export const PENDING = 'pending' export const READY = 'ready' @@ -140,7 +140,7 @@ export const filterDevice = function(req, device) { let responseChannel = 'txn_' + uuidv4() req.options.push.send([ device.channel, - wireutil.transaction(responseChannel, new wire.ConnectGetForwardUrlMessage()) + wireutil.transaction(responseChannel, ConnectGetForwardUrlMessage.create()) ]) } if (fields) { diff --git a/lib/util/devutil.js b/lib/util/devutil.js index 0de51c695f..132663ee93 100644 --- a/lib/util/devutil.js +++ b/lib/util/devutil.js @@ -13,7 +13,7 @@ export default syrup.serial() devutil.executeShellCommand = function(command) { return adb.getDevice(options.serial).execOut(command).then(result => { - log.debug('executing shell command ' + command, result) + log.debug(`executing shell command ${command}, %s`, result) }) } devutil.ensureUnusedLocalSocket = function(sock) { diff --git a/lib/util/grouputil.js b/lib/util/grouputil.js index f8123242f4..caade52508 100644 --- a/lib/util/grouputil.js +++ b/lib/util/grouputil.js @@ -3,6 +3,7 @@ import Promise from 'bluebird' import semver from 'semver' import minimatch from 'minimatch' import wire from '../wire/index.js' +import {RequirementType} from '../wire/wire.js' function RequirementMismatchError(name) { Error.call(this) this.name = 'RequirementMismatchError' @@ -33,17 +34,17 @@ export const match = Promise.method(function(capabilities, requirements) { throw new RequirementMismatchError(req.name) } switch (req.type) { - case wire.RequirementType.SEMVER: + case RequirementType.SEMVER: if (!semver.satisfies(capability, req.value)) { throw new RequirementMismatchError(req.name) } break - case wire.RequirementType.GLOB: + case RequirementType.GLOB: if (!minimatch(capability, req.value)) { throw new RequirementMismatchError(req.name) } break - case wire.RequirementType.EXACT: + case RequirementType.EXACT: if (capability !== req.value) { throw new RequirementMismatchError(req.name) } diff --git a/lib/util/lifecycle.js b/lib/util/lifecycle.js deleted file mode 100644 index ef26d77fbb..0000000000 --- a/lib/util/lifecycle.js +++ /dev/null @@ -1,68 +0,0 @@ -import Promise from 'bluebird' -import logger from './logger.js' - -const log = logger.createLogger('util:lifecycle') - -export default new class Lifecycle { - observers = [] - ending = false - - constructor() { - process.on('SIGINT', this.graceful.bind(this)) - process.on('SIGTERM', this.graceful.bind(this)) - } - - share(name, emitter, options) { - const opts = Object.assign({ - end: true, error: true - }, options) - - if (opts.end) { - emitter.on('end', () => { - if (!this.ending) { - log.fatal(`${name} ended; we shall share its fate`) - this.fatal() - } - }) - } - - if (opts.error) { - emitter.on('error', (err) => { - if (!this.ending) { - log.fatal(`${name} had an error ${err.stack}`) - this.fatal() - } - }) - } - - if (emitter.end) { - this.observe(() => { - emitter.end() - }) - } - return emitter - } - - graceful(err) { - log.info(`Winding down for graceful exit ${err || ''}`) - if (this.ending) { - log.error('Repeated gracefull shutdown request. Exiting immediately.') - process.exit(1) - } - - this.ending = true - return Promise.all(this.observers.map(fn => fn())) - .then(() => process.exit(0)) - } - - /** @returns {any} */ - fatal(/** @type {any} */ err) { - log.fatal(`Shutting down due to fatal error ${err || ''}`) - this.ending = true - process.exit(1) - } - - observe(promise) { - this.observers.push(promise) - } -}() diff --git a/lib/util/lifecycle.ts b/lib/util/lifecycle.ts new file mode 100644 index 0000000000..0948079e8d --- /dev/null +++ b/lib/util/lifecycle.ts @@ -0,0 +1,66 @@ +import EventEmitter from "node:events"; +import logger from "./logger.ts"; + +const log = logger.createLogger("util:lifecycle"); + +type LifecycleObserver = () => Promise | unknown; + +export default new (class Lifecycle { + cleanups: LifecycleObserver[] = []; + ending = false; + + constructor() { + process.on("SIGINT", this.graceful.bind(this)); + process.on("SIGTERM", this.graceful.bind(this)); + } + + share(name: string, emitter: EventEmitter) { + emitter.on("end", () => { + if (!this.ending) { + log.fatal(`${name} ended; we shall share its fate`) + this.fatal(); + } + }); + + emitter.on("error", (err) => { + if (!this.ending) { + log.fatal(`${name} had an error ${err.stack}`) + this.fatal(); + } + }); + + if ('end' in emitter) { + this.observe(() => { + if(typeof emitter.end === 'function') { + emitter.end(); + } + }); + } + return emitter; + } + + graceful(err: Error) { + log.info(`Winding down for graceful exit ${err || ''}`) + if (this.ending) { + log.error( + "Repeated gracefull shutdown request. Exiting immediately." + ); + process.exit(1); + } + + this.ending = true; + return Promise.all(this.cleanups.map((fn) => fn())).then(() => + process.exit(0) + ); + } + + fatal(err?: Error | string): never { + log.fatal(`Shutting down due to fatal error ${err || ''}`) + this.ending = true; + process.exit(1); + } + + observe(cleanupFn: LifecycleObserver) { + this.cleanups.push(cleanupFn); + } +})(); diff --git a/lib/util/logger.ts b/lib/util/logger.ts index dd5faefc39..1c70342bd8 100644 --- a/lib/util/logger.ts +++ b/lib/util/logger.ts @@ -14,7 +14,17 @@ export enum LogLevel { const innerLogger = new EventEmitter() -type LogArguments = [string, ...any[]] +type BuildLog< + N extends number, + S extends any[] = [] // counter +> = + S['length'] extends N + ? [string, []] // base: just string + no args + : [`${BuildLog[0]}%s${string}`, [...BuildLog[1], any]]; + +type NLog = [BuildLog[0], ...BuildLog[1]] + +type LogArguments = NLog<0> | NLog<1> | NLog<2> | NLog<3> | NLog<4> export interface LogEntry { unit: string; @@ -117,7 +127,8 @@ export class Log extends EventEmitter { } private _format(entry: LogEntry): string { - entry.message = printf(...entry.args) + const args = entry.args as [string, ...any[]] + entry.message = printf(...args) const [fg, bg] = Log.unitColors[entry.unit] ?? [chalk.yellow, chalk.bgYellow] return ( `${chalk.grey(entry.timestamp.toJSON())} ${fg(bg(entry.unit))} ${this._name(entry.priority)}/${chalk.bold(entry.tag)} ${entry.pid} [${entry.identifier}] ${entry.message}\n` diff --git a/lib/util/srv.ts b/lib/util/srv.ts index 66179b1f9d..ba2114852d 100644 --- a/lib/util/srv.ts +++ b/lib/util/srv.ts @@ -3,7 +3,6 @@ import util from 'util' import dns from 'dns/promises' import {SrvRecord} from 'dns' import _ from 'lodash' -var srv = Object.create(null) function groupByPriority(records: SrvRecord[]) { return records .sort((a, b) => a.priority - b.priority) @@ -27,7 +26,7 @@ function shuffleWeighted(records: SrvRecord[]) { function pick(records: SrvRecord[], sum: number): SrvRecord[] { const rand = Math.random() * sum let counter = 0 - for (var i = 0, l = records.length; i < l; ++i) { + for (let i = 0, l = records.length; i < l; ++i) { counter += records[i].weight if (rand < counter) { const picked = records.splice(i, 1) diff --git a/lib/util/zmqutil.js b/lib/util/zmqutil.js index 2d1013b248..6098ff06d7 100644 --- a/lib/util/zmqutil.js +++ b/lib/util/zmqutil.js @@ -81,7 +81,7 @@ export class SocketWrapper extends EventEmitter { try { await this.socket.send( (Array.isArray(args) ? args : [args]) - .map(arg => Buffer.isBuffer(arg) ? arg : Buffer.from(String(arg))) + .map(arg => Buffer.isBuffer(arg) || ArrayBuffer.isView(arg) ? arg : Buffer.from(String(arg))) ) } catch (/** @type {any} */ err) { @@ -101,6 +101,10 @@ export class SocketWrapper extends EventEmitter { return this } + /** + * + * @returns {Promise} + */ async startReceiveLoop() { const isValidType = this.type === 'sub' || @@ -130,7 +134,7 @@ export class SocketWrapper extends EventEmitter { } } catch (/** @type {any} */ err) { - log.error('Error in message receive loop: %s', err?.message || err?.toString() || err) + log.error('Error in message receive loop: %s, %s', err?.message || err?.toString() || err, err.stack) return this.startReceiveLoop() } } diff --git a/lib/wire/google/protobuf/any.ts b/lib/wire/google/protobuf/any.ts new file mode 100644 index 0000000000..c13b7a74f2 --- /dev/null +++ b/lib/wire/google/protobuf/any.ts @@ -0,0 +1,326 @@ +// @generated by protobuf-ts 2.11.1 with parameter generate_dependencies +// @generated from protobuf file "google/protobuf/any.proto" (package "google.protobuf", syntax proto3) +// tslint:disable +// +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +import type { BinaryWriteOptions } from "@protobuf-ts/runtime"; +import type { IBinaryWriter } from "@protobuf-ts/runtime"; +import { WireType } from "@protobuf-ts/runtime"; +import type { IBinaryReader } from "@protobuf-ts/runtime"; +import { UnknownFieldHandler } from "@protobuf-ts/runtime"; +import type { PartialMessage } from "@protobuf-ts/runtime"; +import { reflectionMergePartial } from "@protobuf-ts/runtime"; +import { isJsonObject } from "@protobuf-ts/runtime"; +import { typeofJsonValue } from "@protobuf-ts/runtime"; +import type { JsonValue } from "@protobuf-ts/runtime"; +import { jsonWriteOptions } from "@protobuf-ts/runtime"; +import type { JsonReadOptions } from "@protobuf-ts/runtime"; +import type { JsonWriteOptions } from "@protobuf-ts/runtime"; +import type { BinaryReadOptions } from "@protobuf-ts/runtime"; +import type { IMessageType } from "@protobuf-ts/runtime"; +import { MessageType } from "@protobuf-ts/runtime"; +/** + * `Any` contains an arbitrary serialized protocol buffer message along with a + * URL that describes the type of the serialized message. + * + * Protobuf library provides support to pack/unpack Any values in the form + * of utility functions or additional generated methods of the Any type. + * + * Example 1: Pack and unpack a message in C++. + * + * Foo foo = ...; + * Any any; + * any.PackFrom(foo); + * ... + * if (any.UnpackTo(&foo)) { + * ... + * } + * + * Example 2: Pack and unpack a message in Java. + * + * Foo foo = ...; + * Any any = Any.pack(foo); + * ... + * if (any.is(Foo.class)) { + * foo = any.unpack(Foo.class); + * } + * // or ... + * if (any.isSameTypeAs(Foo.getDefaultInstance())) { + * foo = any.unpack(Foo.getDefaultInstance()); + * } + * + * Example 3: Pack and unpack a message in Python. + * + * foo = Foo(...) + * any = Any() + * any.Pack(foo) + * ... + * if any.Is(Foo.DESCRIPTOR): + * any.Unpack(foo) + * ... + * + * Example 4: Pack and unpack a message in Go + * + * foo := &pb.Foo{...} + * any, err := anypb.New(foo) + * if err != nil { + * ... + * } + * ... + * foo := &pb.Foo{} + * if err := any.UnmarshalTo(foo); err != nil { + * ... + * } + * + * The pack methods provided by protobuf library will by default use + * 'type.googleapis.com/full.type.name' as the type URL and the unpack + * methods only use the fully qualified type name after the last '/' + * in the type URL, for example "foo.bar.com/x/y.z" will yield type + * name "y.z". + * + * JSON + * ==== + * The JSON representation of an `Any` value uses the regular + * representation of the deserialized, embedded message, with an + * additional field `@type` which contains the type URL. Example: + * + * package google.profile; + * message Person { + * string first_name = 1; + * string last_name = 2; + * } + * + * { + * "@type": "type.googleapis.com/google.profile.Person", + * "firstName": , + * "lastName": + * } + * + * If the embedded message type is well-known and has a custom JSON + * representation, that representation will be embedded adding a field + * `value` which holds the custom JSON in addition to the `@type` + * field. Example (for message [google.protobuf.Duration][]): + * + * { + * "@type": "type.googleapis.com/google.protobuf.Duration", + * "value": "1.212s" + * } + * + * + * @generated from protobuf message google.protobuf.Any + */ +export interface Any { + /** + * A URL/resource name that uniquely identifies the type of the serialized + * protocol buffer message. This string must contain at least + * one "/" character. The last segment of the URL's path must represent + * the fully qualified name of the type (as in + * `path/google.protobuf.Duration`). The name should be in a canonical form + * (e.g., leading "." is not accepted). + * + * In practice, teams usually precompile into the binary all types that they + * expect it to use in the context of Any. However, for URLs which use the + * scheme `http`, `https`, or no scheme, one can optionally set up a type + * server that maps type URLs to message definitions as follows: + * + * * If no scheme is provided, `https` is assumed. + * * An HTTP GET on the URL must yield a [google.protobuf.Type][] + * value in binary format, or produce an error. + * * Applications are allowed to cache lookup results based on the + * URL, or have them precompiled into a binary to avoid any + * lookup. Therefore, binary compatibility needs to be preserved + * on changes to types. (Use versioned type names to manage + * breaking changes.) + * + * Note: this functionality is not currently available in the official + * protobuf release, and it is not used for type URLs beginning with + * type.googleapis.com. As of May 2023, there are no widely used type server + * implementations and no plans to implement one. + * + * Schemes other than `http`, `https` (or the empty scheme) might be + * used with implementation specific semantics. + * + * + * @generated from protobuf field: string type_url = 1 + */ + typeUrl: string; + /** + * Must be a valid serialized protocol buffer of the above specified type. + * + * @generated from protobuf field: bytes value = 2 + */ + value: Uint8Array; +} +// @generated message type with reflection information, may provide speed optimized methods +class Any$Type extends MessageType { + constructor() { + super("google.protobuf.Any", [ + { no: 1, name: "type_url", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "value", kind: "scalar", T: 12 /*ScalarType.BYTES*/ } + ]); + } + /** + * Pack the message into a new `Any`. + * + * Uses 'type.googleapis.com/full.type.name' as the type URL. + */ + pack(message: T, type: IMessageType): Any { + return { + typeUrl: this.typeNameToUrl(type.typeName), value: type.toBinary(message), + }; + } + /** + * Unpack the message from the `Any`. + */ + unpack(any: Any, type: IMessageType, options?: Partial): T { + if (!this.contains(any, type)) + throw new Error("Cannot unpack google.protobuf.Any with typeUrl '" + any.typeUrl + "' as " + type.typeName + "."); + return type.fromBinary(any.value, options); + } + /** + * Does the given `Any` contain a packed message of the given type? + */ + contains(any: Any, type: IMessageType | string): boolean { + if (!any.typeUrl.length) + return false; + let wants = typeof type == "string" ? type : type.typeName; + let has = this.typeUrlToName(any.typeUrl); + return wants === has; + } + /** + * Convert the message to canonical JSON value. + * + * You have to provide the `typeRegistry` option so that the + * packed message can be converted to JSON. + * + * The `typeRegistry` option is also required to read + * `google.protobuf.Any` from JSON format. + */ + internalJsonWrite(any: Any, options: JsonWriteOptions): JsonValue { + if (any.typeUrl === "") + return {}; + let typeName = this.typeUrlToName(any.typeUrl); + let opt = jsonWriteOptions(options); + let type = opt.typeRegistry?.find(t => t.typeName === typeName); + if (!type) + throw new globalThis.Error("Unable to convert google.protobuf.Any with typeUrl '" + any.typeUrl + "' to JSON. The specified type " + typeName + " is not available in the type registry."); + let value = type.fromBinary(any.value, { readUnknownField: false }); + let json = type.internalJsonWrite(value, opt); + if (typeName.startsWith("google.protobuf.") || !isJsonObject(json)) + json = { value: json }; + json["@type"] = any.typeUrl; + return json; + } + internalJsonRead(json: JsonValue, options: JsonReadOptions, target?: Any): Any { + if (!isJsonObject(json)) + throw new globalThis.Error("Unable to parse google.protobuf.Any from JSON " + typeofJsonValue(json) + "."); + if (typeof json["@type"] != "string" || json["@type"] == "") + return this.create(); + let typeName = this.typeUrlToName(json["@type"]); + let type = options?.typeRegistry?.find(t => t.typeName == typeName); + if (!type) + throw new globalThis.Error("Unable to parse google.protobuf.Any from JSON. The specified type " + typeName + " is not available in the type registry."); + let value; + if (typeName.startsWith("google.protobuf.") && json.hasOwnProperty("value")) + value = type.fromJson(json["value"], options); + else { + let copy = Object.assign({}, json); + delete copy["@type"]; + value = type.fromJson(copy, options); + } + if (target === undefined) + target = this.create(); + target.typeUrl = json["@type"]; + target.value = type.toBinary(value); + return target; + } + typeNameToUrl(name: string): string { + if (!name.length) + throw new Error("invalid type name: " + name); + return "type.googleapis.com/" + name; + } + typeUrlToName(url: string): string { + if (!url.length) + throw new Error("invalid type url: " + url); + let slash = url.lastIndexOf("/"); + let name = slash > 0 ? url.substring(slash + 1) : url; + if (!name.length) + throw new Error("invalid type url: " + url); + return name; + } + create(value?: PartialMessage): Any { + const message = globalThis.Object.create((this.messagePrototype!)); + message.typeUrl = ""; + message.value = new Uint8Array(0); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: Any): Any { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* string type_url */ 1: + message.typeUrl = reader.string(); + break; + case /* bytes value */ 2: + message.value = reader.bytes(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: Any, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* string type_url = 1; */ + if (message.typeUrl !== "") + writer.tag(1, WireType.LengthDelimited).string(message.typeUrl); + /* bytes value = 2; */ + if (message.value.length) + writer.tag(2, WireType.LengthDelimited).bytes(message.value); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message google.protobuf.Any + */ +export const Any = new Any$Type(); diff --git a/lib/wire/index.js b/lib/wire/index.js deleted file mode 100644 index fdfa458b9d..0000000000 --- a/lib/wire/index.js +++ /dev/null @@ -1,14 +0,0 @@ -import path from 'path' -import ProtoBuf from 'protobufjs' -var wire = ProtoBuf.loadProtoFile(path.join(import.meta.dirname, 'wire.proto')).build() -wire.ReverseMessageType = Object.keys(wire.MessageType) - .reduce(function(acc, type) { - var code = wire.MessageType[type] - if (!wire[type]) { - throw new Error('wire.MessageType has unknown value "' + type + '"') - } - wire[type].$code = wire[type].prototype.$code = code - acc[code] = type - return acc - }, Object.create(null)) -export default wire diff --git a/lib/wire/index.ts b/lib/wire/index.ts new file mode 100644 index 0000000000..a852eb53cc --- /dev/null +++ b/lib/wire/index.ts @@ -0,0 +1,35 @@ +import { MessageType } from "@protobuf-ts/runtime"; +import { createLogger } from "../util/logger.ts"; +const allClasses = await import('./wire.ts') + + +const log = createLogger('wire:legacy') + +interface UnknownMessage { + new(...args: unknown[]): T + type: MessageType +} + +const alerted = new Set() + +/** + * @deprecated Do not use the proxy for the constructor. Import the model directly from wire.ts + */ +export default new Proxy({} as Record>, { + get(target, prop, receiver) { + const messageType = (allClasses as any)[prop] as MessageType + if (!messageType) { + throw new Error(`Unknown message type tried constructing: ${prop.toString()}`) + } + if (!alerted.has(messageType.typeName)) { + alerted.add(messageType.typeName) + log.warn('Legacy contstructor lookup for %s', messageType.typeName) + } + const construct = function constructor(this: any, ...args: unknown[]) { + const message = messageType.create(Object.fromEntries(messageType.fields.map((name, index) => ([name.localName, args[index]])))) + Object.assign(this, message) + } + construct.type = messageType + return construct + } +}) diff --git a/lib/wire/messagestream.ts b/lib/wire/messagestream.ts index e0ea0e06ae..003ae9ce59 100644 --- a/lib/wire/messagestream.ts +++ b/lib/wire/messagestream.ts @@ -10,7 +10,7 @@ class DelimitedStream extends stream.Transform { this._buffer = Buffer.concat([this._buffer, chunk]) while (this._buffer.length) { if (this._readingLength) { - var byte = this._buffer[0] + const byte = this._buffer[0] this._length += (byte & 0x7f) << (7 * this._lengthIndex) if (byte & (1 << 7)) { this._lengthIndex += 1 diff --git a/lib/wire/router.ts b/lib/wire/router.ts index f0ab4eb122..eca2d22f87 100644 --- a/lib/wire/router.ts +++ b/lib/wire/router.ts @@ -1,61 +1,53 @@ -import EventEmitter from 'eventemitter3' -import wire from './index.js' -import logger from '../util/logger.js' +import EventEmitter from "eventemitter3"; +import logger from "../util/logger.js"; +import { MessageType } from "@protobuf-ts/runtime"; +import { Envelope } from "./wire.ts"; +import { Any } from "./google/protobuf/any.ts"; -const log = logger.createLogger('wire:router') -export interface WireMessage { - $code: string; -} +const log = logger.createLogger("wire:router"); -type EventType = string | symbol | WireMessage; +type MessageHandler = (channel: string, message: T) => unknown -export class WireRouter extends EventEmitter { - constructor() { - super() - } +export class WireRouter { + emitter = new EventEmitter() + registeredTypes = new Map> - on( - event: string | symbol, - fn: (...args: any[]) => void, - context?: any - ): this; - on(message: WireMessage, fn: (...args: any[]) => void): this; - on( - eventOrMessage: EventType, - fn: (...args: any[]) => void, - context?: any + on(eventName: string | symbol, fn: MessageHandler, context?: object): this; + on(messageType: MessageType, fn: MessageHandler): this; + on( + messageType: MessageType | string | symbol, + fn: MessageHandler, + context?: object ): this { if ( - typeof eventOrMessage !== 'string' && - typeof eventOrMessage !== 'symbol' && - '$code' in eventOrMessage + typeof messageType !== "string" && + typeof messageType !== "symbol" ) { // WireMessage - super.on(eventOrMessage.$code, fn) - } - else { - super.on(eventOrMessage as string | symbol, fn, context) + this.emitter.on(messageType.typeName, fn); + this.registeredTypes.set(Any.typeNameToUrl(messageType.typeName), messageType) + } else { + this.emitter.on(messageType, fn, context); } return this } - removeListener( - event: string | symbol, - fn: (...args: any[]) => void, - context?: any - ): this; - removeListener(message: WireMessage, fn: (...args: any[]) => void): this; - removeListener( - eventOrMessage: EventType, - fn: (...args: any[]) => void, - context?: any + removeAllListeners(messageType: MessageType) { + this.emitter.removeAllListeners(messageType.typeName) + } + + removeListener(eventName: string | symbol, fn: MessageHandler, context?: object): this; + removeListener(messageType: MessageType, fn: MessageHandler): this; + removeListener( + messageType: MessageType | string | symbol, + fn: MessageHandler, + context?: object ): this { - if (typeof eventOrMessage === 'object' && '$code' in eventOrMessage) { - super.removeListener(eventOrMessage.$code, fn) - } - else { - super.removeListener( - eventOrMessage as string | symbol, + if (typeof messageType === "object") { + this.emitter.removeListener(messageType.typeName, fn); + } else { + this.emitter.removeListener( + messageType, fn, context ) @@ -63,44 +55,48 @@ export class WireRouter extends EventEmitter { return this } - handler(): (channel: any, data: Uint8Array) => void { - return (channel: any, data: Uint8Array) => { - const wrapper = wire.Envelope.decode(data) - const type = wire.ReverseMessageType[wrapper.type] - let decodedMessage: any - - try { - decodedMessage = wire[type].decode(wrapper.message) + handler() { + return (channel: string, data: Buffer) => { + const decoded = Envelope.fromBinary(data); + if (!decoded.message) { + log.warn(`Message without message %s`, decoded) + return + } + let target = decoded.message.typeUrl; + if(!target) { + log.warn(`Message without typeUrl %s`, decoded) + return } - catch (e) { - log.error( - 'Received message with type "%s", but cant parse data ' + - wrapper.message - ) - throw e + const messageType = this.registeredTypes.get(target); + if (!messageType) { + // log.warn(`Unknown message type:`, decoded) + // Nobody is expecting such message type.. Ignoring.. + return } + const decodedMessage = Any.unpack(decoded.message, messageType) + // const wrapper = wire.Envelope.decode(data); + // const type = wire.ReverseMessageType[wrapper.type]; + // let decodedMessage: any; - log.info( - 'Received message with type "%s", and data %s', - type || wrapper.type, - JSON.stringify(decodedMessage) - ) + // try { + // decodedMessage = wire[type].decode(wrapper.message) + // } + // catch (e) { + // log.error( + // 'Received message with type "%s", but cant parse data ' + + // wrapper.message + // ); + // throw e; + // } - if (type) { - this.emit( - wrapper.type, - wrapper.channel || channel, - decodedMessage, - data - ) - this.emit('message', channel) - } - else { - log.warn( - 'Unknown message type "%d", perhaps we need an update?', - wrapper.type - ) - } + // log.info( + // 'Received message with type "%s", and data %s', + // messageType.typeName, + // messageType.toJsonString(decodedMessage) + // ) + + this.emitter.emit(messageType.typeName, decoded.channel || channel, decodedMessage, data) + this.emitter.emit('message', channel) } } } diff --git a/lib/wire/transmanager.js b/lib/wire/transmanager.js index 014bfb99e1..926b5ccfbb 100644 --- a/lib/wire/transmanager.js +++ b/lib/wire/transmanager.js @@ -4,6 +4,7 @@ import wire from './index.js' import {WireRouter} from './router.js' import * as Sentry from '@sentry/node' import wireutil from './util.js' +import {TransactionDoneMessage} from './wire.js' export const runTransaction = (channel, message, {sub, push, channelRouter, timeout = apiutil.GRPC_WAIT_TIMEOUT}) => { return Sentry.startSpan({ @@ -20,7 +21,7 @@ export const runTransaction = (channel, message, {sub, push, channelRouter, time sub.subscribe(responseChannel) return new Promise((resolve, reject) => { const messageListener = new WireRouter() - .on(wire.TransactionDoneMessage, function(channel, message) { + .on(TransactionDoneMessage, function(channel, message) { clearTimeout(trTimeout) sub.unsubscribe(responseChannel) channelRouter.removeListener(responseChannel, messageListener) diff --git a/lib/wire/util.js b/lib/wire/util.js deleted file mode 100644 index 8994705efa..0000000000 --- a/lib/wire/util.js +++ /dev/null @@ -1,63 +0,0 @@ -import {v4 as uuidv4} from 'uuid' -import wire from './index.js' -const wireutil = { - global: '*ALL', - makePrivateChannel: function() { - // @ts-ignore - return uuidv4(null, Buffer.alloc(16)).toString('base64') - }, - toDeviceStatus: function(type) { - return wire.DeviceStatus[{ - device: 'ONLINE', - emulator: 'ONLINE', - unauthorized: 'UNAUTHORIZED', - offline: 'OFFLINE', - connecting: 'CONNECTING', - authorizing: 'AUTHORIZING' - }[type]] - }, - toDeviceRequirements: function(/** @type {{ [x: string]: {value: string, match: "semver" | "glob" | "exact"}; }} */ requirements) { - return Object.keys(requirements).map(function(name) { - var item = requirements[name] - return new wire.DeviceRequirement(name, item.value, wire.RequirementType[item.match.toUpperCase()]) - }) - }, - toInstalledApps: function(installedApps) { - if (installedApps.length > 0) { - return installedApps.map(instApp => { - return new wire.Applications(instApp.bundleId, instApp.bundleName) - }) - } - else { - return [] - } - }, - envelope: function(message) { - return new wire.Envelope(message.$code, message.encode()).encodeNB() - }, - transaction: function(channel, message) { - return new wire.Envelope(message.$code, message.encode(), channel) - .encodeNB() - }, - reply: function(source) { - var seq = 0 - return { - okay: function(data, body) { - return wireutil.envelope(new wire.TransactionDoneMessage(source, seq++, true, data === null ? null : (data || 'success'), body ? JSON.stringify(body) : null)) - }, - fail: function(data, body) { - return wireutil.envelope(new wire.TransactionDoneMessage(source, seq++, false, data || 'fail', body ? JSON.stringify(body) : null)) - }, - tree: function(data, body) { - return wireutil.envelope(new wire.TransactionTreeMessage(source, seq++, true, data === null ? null : (data || 'success'), body ? JSON.stringify(body) : null)) - }, - progress: function(data, progress) { - return wireutil.envelope(new wire.TransactionProgressMessage(source, seq++, data, ~~progress)) - }, - device: function(data, body) { - return wireutil.envelope(new wire.TransationGetMessage(source, body ? JSON.stringify(body) : null)) - } - } - } -} -export default wireutil diff --git a/lib/wire/util.ts b/lib/wire/util.ts new file mode 100644 index 0000000000..748c4b04d7 --- /dev/null +++ b/lib/wire/util.ts @@ -0,0 +1,108 @@ + +import crypto from 'node:crypto' +import {DeviceRequirement, DeviceStatus, Envelope, RequirementType, TransactionDoneMessage, TransactionProgressMessage} from './wire.ts' +import { Any } from './google/protobuf/any.ts'; +import { createLogger } from '../util/logger.ts'; +import {MessageType} from "@protobuf-ts/runtime"; +import { ADBDeviceType } from '../units/provider/ADBObserver.js'; + +const DEVICE_STATUS_MAP = { + device: 'ONLINE', + // emulator: 'ONLINE', + unauthorized: 'UNAUTHORIZED', + offline: 'OFFLINE', + // connecting: 'CONNECTING', + // authorizing: 'AUTHORIZING', + unknown: 'OFFLINE', + recovery: 'OFFLINE', + bootloader: 'OFFLINE', + sideload: 'OFFLINE' +} as const satisfies Record // TODO: replace value type with proper type once it's ready + +const log = createLogger('wireutil') + +const wireutil = { + global: '*ALL', + makePrivateChannel() { + return crypto.randomBytes(16).toString('base64') + }, + toDeviceStatus(type: keyof typeof DEVICE_STATUS_MAP) { + return DeviceStatus[DEVICE_STATUS_MAP[type]] + }, + toDeviceRequirements(requirements: Record) { + return Object.keys(requirements).map(function(name) { + let item = requirements[name] + return DeviceRequirement.create({ + name, + value: item.value, + type: RequirementType[item.match.toUpperCase() as 'SEMVER' | 'GLOB' | 'EXACT'] + }) + }) + }, + /** + * @deprecated Do not use raw envelope with a message. Use `pack` for type safety + */ + envelope(message: object) { + return this.oldSend(message) + }, + // envelope(Message.create({...})) // <- forbidden - no way to extract the type + // envelope(new wire.Message(...)) + + pack(messageType: MessageType, message: T, channel: string | undefined = undefined) { + return Envelope.toBinary({ message: Any.pack(message, messageType), channel} ) + }, + + tr(channel: string, messageType: MessageType, message: T) { + return Envelope.toBinary({ message: Any.pack(message, messageType), channel} ) + }, + + /** + * @deprecated Use `tr` for type safety + */ + transaction(channel: string, message: object) { + return this.oldSend(message, channel) + }, + + oldSend(message: object, channel: string | undefined = undefined) { + // @ts-expect-error + const messageType = message.__proto__.constructor.type as MessageType + return this.pack(messageType, message, channel) + }, + + reply(source: string) { + let seq = 0 + return { + okay(data?: string, body?: string | object) { + return wireutil.pack(TransactionDoneMessage, { + source, + seq: seq++, + success: true, + data: data ?? 'success', + body: body ? JSON.stringify(body) : undefined + }) + }, + fail(data?: string, body?:string | object) { + return wireutil.pack(TransactionDoneMessage, { + source, + seq: seq++, + success: false, + data: data ?? 'fail', + body: body ? JSON.stringify(body) : undefined + }) + }, + progress(data: string, progress: number) { + if (!Number.isInteger(progress)) { + log.warn('Somebody is sending non integer as progress: %s', data) + progress = Math.round(progress) + } + return wireutil.pack(TransactionProgressMessage, { + source, + seq: seq++, + data: data, + progress: progress + }) + } + } + } +} +export default wireutil diff --git a/lib/wire/wire.proto b/lib/wire/wire.proto index a0aac52000..2e87a0f41a 100644 --- a/lib/wire/wire.proto +++ b/lib/wire/wire.proto @@ -4,136 +4,13 @@ // Message wrapper -enum MessageType { - CopyMessage = 33; - DeviceIntroductionMessage = 74; - DeviceAbsentMessage = 111; - DeviceIdentityMessage = 2; - DeviceLogcatEntryMessage = 3; - DeviceLogMessage = 4; - DeviceReadyMessage = 5; - DevicePresentMessage = 6; - DevicePropertiesMessage = 7; - DeviceRegisteredMessage = 8; - DeviceStatusMessage = 9; - DeviceTypeMessage = 20; - GroupMessage = 10; - InstallMessage = 30; - PhysicalIdentifyMessage = 29; - JoinGroupMessage = 11; - JoinGroupByAdbFingerprintMessage = 69; - JoinGroupByVncAuthResponseMessage = 90; - VncAuthResponsesUpdatedMessage = 91; - AutoGroupMessage = 70; - AdbKeysUpdatedMessage = 71; - KeyDownMessage = 12; - KeyPressMessage = 13; - KeyUpMessage = 14; - LaunchActivityMessage = 31; - LeaveGroupMessage = 15; - LogcatApplyFiltersMessage = 16; - PasteMessage = 32; - ProbeMessage = 17; - ShellCommandMessage = 18; - ShellKeepAliveMessage = 19; - TouchDownMessage = 21; - TouchMoveMessage = 22; - TouchUpMessage = 23; - TouchCommitMessage = 65; - TouchResetMessage = 66; - GestureStartMessage = 67; - GestureStopMessage = 68; - TransactionDoneMessage = 24; - TransactionProgressMessage = 25; - TypeMessage = 26; - UngroupMessage = 27; - UninstallMessage = 34; - RotateMessage = 35; - ForwardTestMessage = 36; - ForwardCreateMessage = 37; - ForwardRemoveMessage = 38; - LogcatStartMessage = 39; - LogcatStopMessage = 40; - BrowserOpenMessage = 41; - BrowserClearMessage = 42; - AirplaneModeEvent = 43; - BatteryEvent = 44; - DeviceBrowserMessage = 45; - ConnectivityEvent = 46; - PhoneStateEvent = 47; - RotationEvent = 48; - StoreOpenMessage = 49; - ScreenCaptureMessage = 50; - DeviceHeartbeatMessage = 73; - RebootMessage = 52; - ConnectStartMessage = 53; - ConnectStopMessage = 54; - RingerSetMessage = 56; - RingerGetMessage = 64; - WifiSetEnabledMessage = 57; - WifiGetStatusMessage = 58; - AccountAddMenuMessage = 59; - AccountAddMessage = 60; - AccountCheckMessage = 63; - AccountGetMessage = 62; - AccountRemoveMessage = 55; - SdStatusMessage = 61; - ReverseForwardsEvent = 72; - FileSystemListMessage = 81; - FileSystemGetMessage = 82; - InstalledApplications = 83; - GetInstalledApplications = 84; - Applications = 85; - ConnectStartedMessage = 92; - ConnectStoppedMessage = 93; - SetDeviceDisplay = 94; - IosDevicePorts = 95; - StartStreaming = 96; - TouchMoveIosMessage = 99; - DeviceIosIntroductionMessage = 100; - ProviderIosMessage = 101; - DeleteDevice = 104; - SetAbsentDisconnectedDevices = 105; - UninstallIosMessage = 106; - SetDeviceApp = 107; - GetIosDeviceApps = 109; - TransationGetMessage = 110; - UpdateIosDevice = 112; - SdkIosVersion = 114; - SizeIosDevice = 115; - DashboardOpenMessage = 116; - GetIosTreeElements = 117; - TransactionTreeMessage = 118; - TapDeviceTreeElement = 119; - TemporarilyUnavailableMessage = 120; - InitializeIosDeviceState = 121; - UpdateRemoteConnectUrl = 122; - GroupUserChangeMessage = 1200; - DeviceGroupChangeMessage = 1201; - DeviceOriginGroupMessage = 1202; - DeleteUserMessage = 1203; - UpdateAccessTokenMessage = 1204; - GroupChangeMessage = 1205; - UserChangeMessage = 1206; - DeviceChangeMessage = 1207; - IosServiceMessage = 1208; - ChangeQualityMessage = 1209; - InstallResultMessage = 1210; - UnlockDeviceMessage = 1211; - CapabilitiesMessage = 1212; - AirplaneSetMessage = 1213; - ConnectGetForwardUrlMessage = 9000; - GetServicesAvailabilityMessage = 9001; - BluetoothSetEnabledMessage = 9002; - BluetoothGetStatusMessage = 9003; - BluetoothCleanBondedMessage = 9004; - LaunchDeviceApp = 9005; - TerminateDeviceApp = 9006; - KillDeviceApp = 9007; - GetAppAsset = 9008; - GetAppAssetsList = 9009; - GetAppHTML = 9010; - GetAppInspectServerUrl = 9011; +import "google/protobuf/any.proto"; + + +message Envelope { + // required MessageType type = 1; + required google.protobuf.Any message = 2; + optional string channel = 3; } message UpdateAccessTokenMessage { @@ -323,12 +200,6 @@ message FileSystemGetMessage { optional string jwt = 2; } -message Envelope { - required MessageType type = 1; - required bytes message = 2; - optional string channel = 3; -} - message TransactionProgressMessage { required string source = 1; required uint32 seq = 2; diff --git a/lib/wire/wire.ts b/lib/wire/wire.ts new file mode 100644 index 0000000000..a005fb4e3c --- /dev/null +++ b/lib/wire/wire.ts @@ -0,0 +1,11545 @@ +// @generated by protobuf-ts 2.11.1 with parameter generate_dependencies +// @generated from protobuf file "wire.proto" (syntax proto2) +// tslint:disable +import type { BinaryWriteOptions } from "@protobuf-ts/runtime"; +import type { IBinaryWriter } from "@protobuf-ts/runtime"; +import { WireType } from "@protobuf-ts/runtime"; +import type { BinaryReadOptions } from "@protobuf-ts/runtime"; +import type { IBinaryReader } from "@protobuf-ts/runtime"; +import { UnknownFieldHandler } from "@protobuf-ts/runtime"; +import type { PartialMessage } from "@protobuf-ts/runtime"; +import { reflectionMergePartial } from "@protobuf-ts/runtime"; +import { MessageType } from "@protobuf-ts/runtime"; +import { Any } from "./google/protobuf/any.js"; // NOTE: KEEP THIS. ADD .js MANUALLY. +/** + * @generated from protobuf message Envelope + */ +export interface Envelope { + /** + * required MessageType type = 1; + * + * @generated from protobuf field: required google.protobuf.Any message = 2 + */ + message?: Any; + /** + * @generated from protobuf field: optional string channel = 3 + */ + channel?: string; +} +/** + * @generated from protobuf message UpdateAccessTokenMessage + */ +export interface UpdateAccessTokenMessage { +} +/** + * @generated from protobuf message UnlockDeviceMessage + */ +export interface UnlockDeviceMessage { +} +/** + * @generated from protobuf message DeleteUserMessage + */ +export interface DeleteUserMessage { + /** + * @generated from protobuf field: required string email = 1 + */ + email: string; +} +/** + * @generated from protobuf message DeviceOriginGroupMessage + */ +export interface DeviceOriginGroupMessage { + /** + * @generated from protobuf field: required string signature = 1 + */ + signature: string; +} +/** + * @generated from protobuf message UserQuotasDetailField + */ +export interface UserQuotasDetailField { + /** + * @generated from protobuf field: required double duration = 1 + */ + duration: number; + /** + * @generated from protobuf field: required uint32 number = 2 + */ + number: number; +} +/** + * @generated from protobuf message UserQuotasField + */ +export interface UserQuotasField { + /** + * @generated from protobuf field: required UserQuotasDetailField allocated = 1 + */ + allocated?: UserQuotasDetailField; + /** + * @generated from protobuf field: required UserQuotasDetailField consumed = 2 + */ + consumed?: UserQuotasDetailField; + /** + * @generated from protobuf field: required uint32 defaultGroupsDuration = 3 + */ + defaultGroupsDuration: number; + /** + * @generated from protobuf field: required uint32 defaultGroupsNumber = 4 + */ + defaultGroupsNumber: number; + /** + * @generated from protobuf field: required uint32 defaultGroupsRepetitions = 5 + */ + defaultGroupsRepetitions: number; + /** + * @generated from protobuf field: required uint32 repetitions = 6 + */ + repetitions: number; +} +/** + * @generated from protobuf message UserGroupsField + */ +export interface UserGroupsField { + /** + * @generated from protobuf field: required UserQuotasField quotas = 1 + */ + quotas?: UserQuotasField; + /** + * @generated from protobuf field: repeated string subscribed = 2 + */ + subscribed: string[]; +} +/** + * @generated from protobuf message AlertMessageField + */ +export interface AlertMessageField { + /** + * @generated from protobuf field: required string activation = 1 + */ + activation: string; + /** + * @generated from protobuf field: required string data = 2 + */ + data: string; + /** + * @generated from protobuf field: required string level = 3 + */ + level: string; +} +/** + * @generated from protobuf message UserSettingsField + */ +export interface UserSettingsField { + /** + * @generated from protobuf field: optional AlertMessageField alertMessage = 1 + */ + alertMessage?: AlertMessageField; +} +/** + * @generated from protobuf message UserField + */ +export interface UserField { + /** + * @generated from protobuf field: required string email = 1 + */ + email: string; + /** + * @generated from protobuf field: required string name = 2 + */ + name: string; + /** + * @generated from protobuf field: required string privilege = 3 + */ + privilege: string; + /** + * @generated from protobuf field: required UserGroupsField groups = 4 + */ + groups?: UserGroupsField; + /** + * @generated from protobuf field: optional UserSettingsField settings = 5 + */ + settings?: UserSettingsField; +} +/** + * @generated from protobuf message UserChangeMessage + */ +export interface UserChangeMessage { + /** + * @generated from protobuf field: required UserField user = 1 + */ + user?: UserField; + /** + * @generated from protobuf field: required bool isAddedGroup = 2 + */ + isAddedGroup: boolean; + /** + * @generated from protobuf field: repeated string groups = 3 + */ + groups: string[]; + /** + * @generated from protobuf field: required string action = 4 + */ + action: string; + /** + * @generated from protobuf field: repeated string targets = 5 + */ + targets: string[]; + /** + * @generated from protobuf field: required double timeStamp = 6 + */ + timeStamp: number; +} +/** + * @generated from protobuf message DeviceNetworkField + */ +export interface DeviceNetworkField { + /** + * @generated from protobuf field: optional string type = 1 + */ + type?: string; + /** + * @generated from protobuf field: optional string subtype = 2 + */ + subtype?: string; +} +/** + * @generated from protobuf message DeviceDisplayField + */ +export interface DeviceDisplayField { + /** + * @generated from protobuf field: optional uint32 height = 1 + */ + height?: number; + /** + * @generated from protobuf field: optional uint32 width = 2 + */ + width?: number; +} +/** + * @generated from protobuf message DevicePhoneField + */ +export interface DevicePhoneField { + /** + * @generated from protobuf field: optional string imei = 1 + */ + imei?: string; +} +/** + * @generated from protobuf message DeviceProviderField + */ +export interface DeviceProviderField { + /** + * @generated from protobuf field: optional string name = 1 + */ + name?: string; +} +/** + * @generated from protobuf message DeviceGroupField + */ +export interface DeviceGroupField { + /** + * @generated from protobuf field: optional string id = 1 + */ + id?: string; + /** + * @generated from protobuf field: optional string name = 2 + */ + name?: string; + /** + * @generated from protobuf field: optional string origin = 3 + */ + origin?: string; + /** + * @generated from protobuf field: optional string originName = 4 + */ + originName?: string; + /** + * @generated from protobuf field: optional GroupOwnerField owner = 5 + */ + owner?: GroupOwnerField; +} +/** + * @generated from protobuf message DeviceField + */ +export interface DeviceField { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; + /** + * @generated from protobuf field: optional string model = 2 + */ + model?: string; + /** + * @generated from protobuf field: optional string version = 3 + */ + version?: string; + /** + * @generated from protobuf field: optional string operator = 4 + */ + operator?: string; + /** + * @generated from protobuf field: optional DeviceNetworkField network = 5 + */ + network?: DeviceNetworkField; + /** + * @generated from protobuf field: optional DeviceDisplayField display = 6 + */ + display?: DeviceDisplayField; + /** + * @generated from protobuf field: optional string manufacturer = 7 + */ + manufacturer?: string; + /** + * @generated from protobuf field: optional string sdk = 8 + */ + sdk?: string; + /** + * @generated from protobuf field: optional string abi = 9 + */ + abi?: string; + /** + * @generated from protobuf field: optional string cpuPlatform = 10 + */ + cpuPlatform?: string; + /** + * @generated from protobuf field: optional string openGLESVersion = 11 + */ + openGLESVersion?: string; + /** + * @generated from protobuf field: optional DevicePhoneField phone = 12 + */ + phone?: DevicePhoneField; + /** + * @generated from protobuf field: optional DeviceProviderField provider = 13 + */ + provider?: DeviceProviderField; + /** + * @generated from protobuf field: optional DeviceGroupField group = 14 + */ + group?: DeviceGroupField; + /** + * @generated from protobuf field: optional string marketName = 15 + */ + marketName?: string; +} +/** + * @generated from protobuf message DeviceChangeMessage + */ +export interface DeviceChangeMessage { + /** + * @generated from protobuf field: required DeviceField device = 1 + */ + device?: DeviceField; + /** + * @generated from protobuf field: required string action = 2 + */ + action: string; + /** + * @generated from protobuf field: required string oldOriginGroupId = 3 + */ + oldOriginGroupId: string; + /** + * @generated from protobuf field: required double timeStamp = 4 + */ + timeStamp: number; +} +/** + * @generated from protobuf message GroupDateField + */ +export interface GroupDateField { + /** + * @generated from protobuf field: required string start = 1 + */ + start: string; + /** + * @generated from protobuf field: required string stop = 2 + */ + stop: string; +} +/** + * @generated from protobuf message GroupOwnerField + */ +export interface GroupOwnerField { + /** + * @generated from protobuf field: required string email = 1 + */ + email: string; + /** + * @generated from protobuf field: required string name = 2 + */ + name: string; +} +/** + * @generated from protobuf message GroupField + */ +export interface GroupField { + /** + * @generated from protobuf field: required string id = 1 + */ + id: string; + /** + * @generated from protobuf field: required string name = 2 + */ + name: string; + /** + * @generated from protobuf field: required string class = 3 + */ + class: string; + /** + * @generated from protobuf field: required string privilege = 4 + */ + privilege: string; + /** + * @generated from protobuf field: required GroupOwnerField owner = 5 + */ + owner?: GroupOwnerField; + /** + * @generated from protobuf field: repeated GroupDateField dates = 6 + */ + dates: GroupDateField[]; + /** + * @generated from protobuf field: required uint32 duration = 7 + */ + duration: number; + /** + * @generated from protobuf field: required uint32 repetitions = 8 + */ + repetitions: number; + /** + * @generated from protobuf field: repeated string devices = 9 + */ + devices: string[]; + /** + * @generated from protobuf field: repeated string users = 10 + */ + users: string[]; + /** + * @generated from protobuf field: required string state = 11 + */ + state: string; + /** + * @generated from protobuf field: required bool isActive = 12 + */ + isActive: boolean; + /** + * @generated from protobuf field: repeated string moderators = 13 + */ + moderators: string[]; +} +/** + * @generated from protobuf message GroupChangeMessage + */ +export interface GroupChangeMessage { + /** + * @generated from protobuf field: required GroupField group = 1 + */ + group?: GroupField; + /** + * @generated from protobuf field: required string action = 2 + */ + action: string; + /** + * @generated from protobuf field: repeated string subscribers = 3 + */ + subscribers: string[]; + /** + * @generated from protobuf field: required bool isChangedDates = 4 + */ + isChangedDates: boolean; + /** + * @generated from protobuf field: required bool isChangedClass = 5 + */ + isChangedClass: boolean; + /** + * @generated from protobuf field: required bool isAddedUser = 6 + */ + isAddedUser: boolean; + /** + * @generated from protobuf field: repeated string users = 7 + */ + users: string[]; + /** + * @generated from protobuf field: required bool isAddedDevice = 8 + */ + isAddedDevice: boolean; + /** + * @generated from protobuf field: repeated string devices = 9 + */ + devices: string[]; + /** + * @generated from protobuf field: required double timeStamp = 10 + */ + timeStamp: number; +} +/** + * @generated from protobuf message DeviceGroupChangeMessage + */ +export interface DeviceGroupChangeMessage { + /** + * @generated from protobuf field: required string id = 1 + */ + id: string; + /** + * @generated from protobuf field: required DeviceGroupMessage group = 2 + */ + group?: DeviceGroupMessage; + /** + * @generated from protobuf field: required string serial = 3 + */ + serial: string; +} +/** + * @generated from protobuf message GroupUserChangeMessage + */ +export interface GroupUserChangeMessage { + /** + * @generated from protobuf field: repeated string users = 1 + */ + users: string[]; + /** + * @generated from protobuf field: required bool isAdded = 2 + */ + isAdded: boolean; + /** + * @generated from protobuf field: required string id = 3 + */ + id: string; + /** + * @generated from protobuf field: required bool isDeletedLater = 4 + */ + isDeletedLater: boolean; + /** + * @generated from protobuf field: repeated string devices = 5 + */ + devices: string[]; +} +/** + * @generated from protobuf message ConnectStartedMessage + */ +export interface ConnectStartedMessage { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; + /** + * @generated from protobuf field: required string url = 2 + */ + url: string; +} +/** + * @generated from protobuf message InstallResultMessage + */ +export interface InstallResultMessage { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; + /** + * @generated from protobuf field: required string result = 2 + */ + result: string; +} +/** + * @generated from protobuf message ConnectStoppedMessage + */ +export interface ConnectStoppedMessage { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; +} +/** + * @generated from protobuf message FileSystemListMessage + */ +export interface FileSystemListMessage { + /** + * @generated from protobuf field: required string dir = 1 + */ + dir: string; +} +/** + * @generated from protobuf message FileSystemGetMessage + */ +export interface FileSystemGetMessage { + /** + * @generated from protobuf field: required string file = 1 + */ + file: string; + /** + * @generated from protobuf field: optional string jwt = 2 + */ + jwt?: string; +} +/** + * @generated from protobuf message TransactionProgressMessage + */ +export interface TransactionProgressMessage { + /** + * @generated from protobuf field: required string source = 1 + */ + source: string; + /** + * @generated from protobuf field: required uint32 seq = 2 + */ + seq: number; + /** + * @generated from protobuf field: optional string data = 3 + */ + data?: string; + /** + * @generated from protobuf field: optional uint32 progress = 4 [default = 0] + */ + progress?: number; +} +/** + * @generated from protobuf message TransactionDoneMessage + */ +export interface TransactionDoneMessage { + /** + * @generated from protobuf field: required string source = 1 + */ + source: string; + /** + * @generated from protobuf field: required uint32 seq = 2 + */ + seq: number; + /** + * @generated from protobuf field: required bool success = 3 + */ + success: boolean; + /** + * @generated from protobuf field: optional string data = 4 + */ + data?: string; + /** + * @generated from protobuf field: optional string body = 5 + */ + body?: string; +} +/** + * @generated from protobuf message TransactionTreeMessage + */ +export interface TransactionTreeMessage { + /** + * @generated from protobuf field: required string source = 1 + */ + source: string; + /** + * @generated from protobuf field: required uint32 seq = 2 + */ + seq: number; + /** + * @generated from protobuf field: required bool success = 3 + */ + success: boolean; + /** + * @generated from protobuf field: optional string data = 4 + */ + data?: string; + /** + * @generated from protobuf field: optional string body = 5 + */ + body?: string; +} +// Logging + +/** + * @generated from protobuf message DeviceLogMessage + */ +export interface DeviceLogMessage { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; + /** + * @generated from protobuf field: required double timestamp = 2 + */ + timestamp: number; + /** + * @generated from protobuf field: required uint32 priority = 3 + */ + priority: number; + /** + * @generated from protobuf field: required string tag = 4 + */ + tag: string; + /** + * @generated from protobuf field: required uint32 pid = 5 + */ + pid: number; + /** + * @generated from protobuf field: required string message = 6 + */ + message: string; + /** + * @generated from protobuf field: required string identifier = 7 + */ + identifier: string; +} +// Introductions + +/** + * @generated from protobuf message DeviceGroupOwnerMessage + */ +export interface DeviceGroupOwnerMessage { + /** + * @generated from protobuf field: required string email = 1 + */ + email: string; + /** + * @generated from protobuf field: required string name = 2 + */ + name: string; +} +/** + * @generated from protobuf message DeviceGroupLifetimeMessage + */ +export interface DeviceGroupLifetimeMessage { + /** + * @generated from protobuf field: required double start = 1 + */ + start: number; + /** + * @generated from protobuf field: required double stop = 2 + */ + stop: number; +} +/** + * @generated from protobuf message DeviceGroupMessage + */ +export interface DeviceGroupMessage { + /** + * @generated from protobuf field: required string id = 1 + */ + id: string; + /** + * @generated from protobuf field: required string name = 2 + */ + name: string; + /** + * @generated from protobuf field: required DeviceGroupOwnerMessage owner = 3 + */ + owner?: DeviceGroupOwnerMessage; + /** + * @generated from protobuf field: required DeviceGroupLifetimeMessage lifeTime = 4 + */ + lifeTime?: DeviceGroupLifetimeMessage; + /** + * @generated from protobuf field: required string class = 5 + */ + class: string; + /** + * @generated from protobuf field: required uint32 repetitions = 6 + */ + repetitions: number; + /** + * @generated from protobuf field: required string originName = 7 + */ + originName: string; +} +/** + * @generated from protobuf message ProviderMessage + */ +export interface ProviderMessage { + /** + * @generated from protobuf field: required string channel = 1 + */ + channel: string; + /** + * @generated from protobuf field: required string name = 2 + */ + name: string; +} +/** + * @generated from protobuf message ProviderIosMessage + */ +export interface ProviderIosMessage { + /** + * @generated from protobuf field: required string channel = 1 + */ + channel: string; + /** + * @generated from protobuf field: required string name = 2 + */ + name: string; + /** + * @generated from protobuf field: required string screenWsUrlPattern = 3 + */ + screenWsUrlPattern: string; +} +/** + * @generated from protobuf message DeviceHeartbeatMessage + */ +export interface DeviceHeartbeatMessage { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; +} +/** + * @generated from protobuf message DeviceIntroductionMessage + */ +export interface DeviceIntroductionMessage { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; + /** + * @generated from protobuf field: required DeviceStatus status = 2 + */ + status: DeviceStatus; + /** + * @generated from protobuf field: required ProviderMessage provider = 3 + */ + provider?: ProviderMessage; // optional DeviceGroupMessage group = 4; +} +/** + * @generated from protobuf message DeviceIosIntroductionMessage + */ +export interface DeviceIosIntroductionMessage { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; + /** + * @generated from protobuf field: required DeviceStatus status = 2 + */ + status: DeviceStatus; + /** + * @generated from protobuf field: required ProviderMessage provider = 3 + */ + provider?: ProviderMessage; +} +/** + * @generated from protobuf message InitializeIosDeviceState + */ +export interface InitializeIosDeviceState { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; + /** + * @generated from protobuf field: required DeviceStatus status = 2 + */ + status: DeviceStatus; + /** + * @generated from protobuf field: required ProviderIosMessage provider = 3 + */ + provider?: ProviderIosMessage; + /** + * @generated from protobuf field: required IosDevicePorts ports = 4 + */ + ports?: IosDevicePorts; + /** + * @generated from protobuf field: required UpdateIosDevice options = 5 + */ + options?: UpdateIosDevice; +} +/** + * @generated from protobuf message DeviceRegisteredMessage + */ +export interface DeviceRegisteredMessage { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; +} +/** + * @generated from protobuf message DevicePresentMessage + */ +export interface DevicePresentMessage { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; +} +/** + * @generated from protobuf message DeviceAbsentMessage + */ +export interface DeviceAbsentMessage { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; +} +/** + * @generated from protobuf message DeviceReadyMessage + */ +export interface DeviceReadyMessage { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; + /** + * @generated from protobuf field: required string channel = 2 + */ + channel: string; +} +/** + * @generated from protobuf message ProbeMessage + */ +export interface ProbeMessage { +} +/** + * @generated from protobuf message DeviceStatusMessage + */ +export interface DeviceStatusMessage { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; + /** + * @generated from protobuf field: required DeviceStatus status = 2 + */ + status: DeviceStatus; +} +/** + * @generated from protobuf message DeviceTypeMessage + */ +export interface DeviceTypeMessage { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; + /** + * @generated from protobuf field: required string type = 2 + */ + type: string; +} +/** + * @generated from protobuf message DeviceDisplayMessage + */ +export interface DeviceDisplayMessage { + /** + * @generated from protobuf field: required int32 id = 1 + */ + id: number; + /** + * @generated from protobuf field: required int32 width = 2 + */ + width: number; + /** + * @generated from protobuf field: required int32 height = 3 + */ + height: number; + /** + * @generated from protobuf field: required int32 rotation = 4 + */ + rotation: number; + /** + * @generated from protobuf field: required float xdpi = 5 + */ + xdpi: number; + /** + * @generated from protobuf field: required float ydpi = 6 + */ + ydpi: number; + /** + * @generated from protobuf field: required float fps = 7 + */ + fps: number; + /** + * @generated from protobuf field: required float density = 8 + */ + density: number; + /** + * @generated from protobuf field: required bool secure = 9 + */ + secure: boolean; + /** + * @generated from protobuf field: required string url = 10 + */ + url: string; + /** + * @generated from protobuf field: optional float size = 11 + */ + size?: number; +} +/** + * @generated from protobuf message DeviceBrowserAppMessage + */ +export interface DeviceBrowserAppMessage { + /** + * @generated from protobuf field: required string id = 1 + */ + id: string; + /** + * @generated from protobuf field: required string type = 2 + */ + type: string; + /** + * @generated from protobuf field: required string name = 3 + */ + name: string; + /** + * @generated from protobuf field: required bool selected = 4 + */ + selected: boolean; + /** + * @generated from protobuf field: required bool system = 5 + */ + system: boolean; +} +/** + * @generated from protobuf message DeviceBrowserMessage + */ +export interface DeviceBrowserMessage { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; + /** + * @generated from protobuf field: required bool selected = 2 + */ + selected: boolean; + /** + * @generated from protobuf field: repeated DeviceBrowserAppMessage apps = 3 + */ + apps: DeviceBrowserAppMessage[]; +} +/** + * @generated from protobuf message GetServicesAvailabilityMessage + */ +export interface GetServicesAvailabilityMessage { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; + /** + * @generated from protobuf field: required bool hasGMS = 2 + */ + hasGMS: boolean; + /** + * @generated from protobuf field: required bool hasHMS = 3 + */ + hasHMS: boolean; +} +/** + * @generated from protobuf message DevicePhoneMessage + */ +export interface DevicePhoneMessage { + /** + * @generated from protobuf field: optional string imei = 1 + */ + imei?: string; + /** + * @generated from protobuf field: optional string imsi = 5 + */ + imsi?: string; + /** + * @generated from protobuf field: optional string phoneNumber = 2 + */ + phoneNumber?: string; + /** + * @generated from protobuf field: optional string iccid = 3 + */ + iccid?: string; + /** + * @generated from protobuf field: optional string network = 4 + */ + network?: string; +} +/** + * @generated from protobuf message DeviceIdentityMessage + */ +export interface DeviceIdentityMessage { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; + /** + * @generated from protobuf field: required string platform = 2 + */ + platform: string; + /** + * @generated from protobuf field: required string manufacturer = 3 + */ + manufacturer: string; + /** + * @generated from protobuf field: optional string operator = 4 + */ + operator?: string; + /** + * @generated from protobuf field: required string model = 5 + */ + model: string; + /** + * @generated from protobuf field: required string version = 6 + */ + version: string; + /** + * @generated from protobuf field: required string abi = 7 + */ + abi: string; + /** + * @generated from protobuf field: required string sdk = 8 + */ + sdk: string; + /** + * @generated from protobuf field: required DeviceDisplayMessage display = 9 + */ + display?: DeviceDisplayMessage; + /** + * @generated from protobuf field: required DevicePhoneMessage phone = 11 + */ + phone?: DevicePhoneMessage; + /** + * @generated from protobuf field: optional string product = 12 + */ + product?: string; + /** + * @generated from protobuf field: optional string cpuPlatform = 13 + */ + cpuPlatform?: string; + /** + * @generated from protobuf field: optional string openGLESVersion = 14 + */ + openGLESVersion?: string; + /** + * @generated from protobuf field: optional string marketName = 15 + */ + marketName?: string; + /** + * @generated from protobuf field: optional string macAddress = 16 + */ + macAddress?: string; + /** + * @generated from protobuf field: optional string ram = 17 + */ + ram?: string; +} +/** + * @generated from protobuf message DeviceProperty + */ +export interface DeviceProperty { + /** + * @generated from protobuf field: required string name = 1 + */ + name: string; + /** + * @generated from protobuf field: required string value = 2 + */ + value: string; +} +/** + * @generated from protobuf message DevicePropertiesMessage + */ +export interface DevicePropertiesMessage { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; + /** + * @generated from protobuf field: repeated DeviceProperty properties = 2 + */ + properties: DeviceProperty[]; +} +/** + * @generated from protobuf message DeviceRequirement + */ +export interface DeviceRequirement { + /** + * @generated from protobuf field: required string name = 1 + */ + name: string; + /** + * @generated from protobuf field: required string value = 2 + */ + value: string; + /** + * @generated from protobuf field: required RequirementType type = 3 + */ + type: RequirementType; +} +/** + * @generated from protobuf message OwnerMessage + */ +export interface OwnerMessage { + /** + * @generated from protobuf field: required string email = 1 + */ + email: string; + /** + * @generated from protobuf field: required string name = 2 + */ + name: string; + /** + * @generated from protobuf field: required string group = 3 + */ + group: string; +} +/** + * @generated from protobuf message GroupMessage + */ +export interface GroupMessage { + /** + * @generated from protobuf field: required OwnerMessage owner = 1 + */ + owner?: OwnerMessage; + /** + * @generated from protobuf field: optional uint32 timeout = 2 + */ + timeout?: number; + /** + * @generated from protobuf field: repeated DeviceRequirement requirements = 3 + */ + requirements: DeviceRequirement[]; + /** + * @generated from protobuf field: optional string usage = 4 + */ + usage?: string; +} +/** + * @generated from protobuf message AutoGroupMessage + */ +export interface AutoGroupMessage { + /** + * @generated from protobuf field: required OwnerMessage owner = 1 + */ + owner?: OwnerMessage; + /** + * @generated from protobuf field: required string identifier = 2 + */ + identifier: string; +} +/** + * @generated from protobuf message UngroupMessage + */ +export interface UngroupMessage { + /** + * @generated from protobuf field: repeated DeviceRequirement requirements = 2 + */ + requirements: DeviceRequirement[]; +} +/** + * @generated from protobuf message JoinGroupMessage + */ +export interface JoinGroupMessage { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; + /** + * @generated from protobuf field: required OwnerMessage owner = 2 + */ + owner?: OwnerMessage; + /** + * @generated from protobuf field: optional string usage = 3 + */ + usage?: string; +} +/** + * @generated from protobuf message JoinGroupByAdbFingerprintMessage + */ +export interface JoinGroupByAdbFingerprintMessage { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; + /** + * @generated from protobuf field: required string fingerprint = 2 + */ + fingerprint: string; + /** + * @generated from protobuf field: optional string comment = 3 + */ + comment?: string; + /** + * @generated from protobuf field: optional string currentGroup = 4 + */ + currentGroup?: string; +} +/** + * @generated from protobuf message JoinGroupByVncAuthResponseMessage + */ +export interface JoinGroupByVncAuthResponseMessage { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; + /** + * @generated from protobuf field: required string response = 2 + */ + response: string; + /** + * @generated from protobuf field: optional string currentGroup = 4 + */ + currentGroup?: string; +} +/** + * @generated from protobuf message AdbKeysUpdatedMessage + */ +export interface AdbKeysUpdatedMessage { +} +/** + * @generated from protobuf message VncAuthResponsesUpdatedMessage + */ +export interface VncAuthResponsesUpdatedMessage { +} +/** + * @generated from protobuf message LeaveGroupMessage + */ +export interface LeaveGroupMessage { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; + /** + * @generated from protobuf field: required OwnerMessage owner = 2 + */ + owner?: OwnerMessage; + /** + * @generated from protobuf field: required string reason = 3 + */ + reason: string; +} +// Input + +/** + * @generated from protobuf message PhysicalIdentifyMessage + */ +export interface PhysicalIdentifyMessage { +} +/** + * @generated from protobuf message TouchDownMessage + */ +export interface TouchDownMessage { + /** + * @generated from protobuf field: required uint32 seq = 1 + */ + seq: number; + /** + * @generated from protobuf field: required uint32 contact = 2 + */ + contact: number; + /** + * @generated from protobuf field: required float x = 3 + */ + x: number; + /** + * @generated from protobuf field: required float y = 4 + */ + y: number; + /** + * @generated from protobuf field: optional float pressure = 5 + */ + pressure?: number; +} +/** + * @generated from protobuf message TouchMoveMessage + */ +export interface TouchMoveMessage { + /** + * @generated from protobuf field: required uint32 seq = 1 + */ + seq: number; + /** + * @generated from protobuf field: required uint32 contact = 2 + */ + contact: number; + /** + * @generated from protobuf field: required float x = 3 + */ + x: number; + /** + * @generated from protobuf field: required float y = 4 + */ + y: number; + /** + * @generated from protobuf field: optional float pressure = 5 + */ + pressure?: number; +} +/** + * @generated from protobuf message TouchMoveIosMessage + */ +export interface TouchMoveIosMessage { + /** + * @generated from protobuf field: required float toX = 1 + */ + toX: number; + /** + * @generated from protobuf field: required float toY = 2 + */ + toY: number; + /** + * @generated from protobuf field: required float fromX = 3 + */ + fromX: number; + /** + * @generated from protobuf field: required float fromY = 4 + */ + fromY: number; + /** + * @generated from protobuf field: optional float duration = 5 + */ + duration?: number; +} +/** + * @generated from protobuf message TouchUpMessage + */ +export interface TouchUpMessage { + /** + * @generated from protobuf field: required uint32 seq = 1 + */ + seq: number; + /** + * @generated from protobuf field: required uint32 contact = 2 + */ + contact: number; +} +/** + * @generated from protobuf message TouchCommitMessage + */ +export interface TouchCommitMessage { + /** + * @generated from protobuf field: required uint32 seq = 1 + */ + seq: number; +} +/** + * @generated from protobuf message TouchResetMessage + */ +export interface TouchResetMessage { + /** + * @generated from protobuf field: required uint32 seq = 1 + */ + seq: number; +} +/** + * @generated from protobuf message GestureStartMessage + */ +export interface GestureStartMessage { + /** + * @generated from protobuf field: required uint32 seq = 1 + */ + seq: number; +} +/** + * @generated from protobuf message GestureStopMessage + */ +export interface GestureStopMessage { + /** + * @generated from protobuf field: required uint32 seq = 1 + */ + seq: number; +} +/** + * @generated from protobuf message TypeMessage + */ +export interface TypeMessage { + /** + * @generated from protobuf field: required string text = 1 + */ + text: string; +} +/** + * @generated from protobuf message PasteMessage + */ +export interface PasteMessage { + /** + * @generated from protobuf field: required string text = 1 + */ + text: string; +} +/** + * @generated from protobuf message CopyMessage + */ +export interface CopyMessage { +} +/** + * @generated from protobuf message KeyDownMessage + */ +export interface KeyDownMessage { + /** + * @generated from protobuf field: required string key = 1 + */ + key: string; +} +/** + * @generated from protobuf message KeyUpMessage + */ +export interface KeyUpMessage { + /** + * @generated from protobuf field: required string key = 1 + */ + key: string; +} +/** + * @generated from protobuf message KeyPressMessage + */ +export interface KeyPressMessage { + /** + * @generated from protobuf field: required string key = 1 + */ + key: string; +} +/** + * @generated from protobuf message RebootMessage + */ +export interface RebootMessage { +} +// Output + +/** + * @generated from protobuf message DeviceLogcatEntryMessage + */ +export interface DeviceLogcatEntryMessage { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; + /** + * @generated from protobuf field: required double date = 2 + */ + date: number; + /** + * @generated from protobuf field: required uint32 pid = 3 + */ + pid: number; + /** + * @generated from protobuf field: required uint32 tid = 4 + */ + tid: number; + /** + * @generated from protobuf field: required uint32 priority = 5 + */ + priority: number; + /** + * @generated from protobuf field: required string tag = 6 + */ + tag: string; + /** + * @generated from protobuf field: required string message = 7 + */ + message: string; +} +/** + * @generated from protobuf message LogcatFilter + */ +export interface LogcatFilter { + /** + * @generated from protobuf field: required string tag = 1 + */ + tag: string; + /** + * @generated from protobuf field: required uint32 priority = 2 + */ + priority: number; +} +/** + * @generated from protobuf message LogcatStartMessage + */ +export interface LogcatStartMessage { + /** + * @generated from protobuf field: repeated LogcatFilter filters = 1 + */ + filters: LogcatFilter[]; +} +/** + * @generated from protobuf message LogcatStopMessage + */ +export interface LogcatStopMessage { +} +/** + * @generated from protobuf message LogcatApplyFiltersMessage + */ +export interface LogcatApplyFiltersMessage { + /** + * @generated from protobuf field: repeated LogcatFilter filters = 1 + */ + filters: LogcatFilter[]; +} +// Commands + +/** + * @generated from protobuf message ShellCommandMessage + */ +export interface ShellCommandMessage { + /** + * @generated from protobuf field: required string command = 1 + */ + command: string; + /** + * @generated from protobuf field: required uint32 timeout = 2 + */ + timeout: number; +} +/** + * @generated from protobuf message ShellKeepAliveMessage + */ +export interface ShellKeepAliveMessage { + /** + * @generated from protobuf field: required uint32 timeout = 1 + */ + timeout: number; +} +/** + * @generated from protobuf message InstallMessage + */ +export interface InstallMessage { + /** + * @generated from protobuf field: required string href = 1 + */ + href: string; + /** + * @generated from protobuf field: required bool launch = 2 + */ + launch: boolean; + /** + * @generated from protobuf field: required bool isApi = 3 + */ + isApi: boolean; + /** + * @generated from protobuf field: optional string manifest = 4 + */ + manifest?: string; + /** + * @generated from protobuf field: repeated string installFlags = 5 + */ + installFlags: string[]; + /** + * @generated from protobuf field: optional string jwt = 6 + */ + jwt?: string; // used for storage authorization + /** + * @generated from protobuf field: optional string pkg = 7 + */ + pkg?: string; // used for .tpk installation via sdb +} +/** + * @generated from protobuf message UninstallMessage + */ +export interface UninstallMessage { + /** + * @generated from protobuf field: required string packageName = 1 + */ + packageName: string; +} +/** + * @generated from protobuf message UninstallIosMessage + */ +export interface UninstallIosMessage { + /** + * @generated from protobuf field: required string packageName = 1 + */ + packageName: string; +} +/** + * @generated from protobuf message LaunchActivityMessage + */ +export interface LaunchActivityMessage { + /** + * @generated from protobuf field: required string action = 1 + */ + action: string; + /** + * @generated from protobuf field: required string component = 2 + */ + component: string; + /** + * @generated from protobuf field: repeated string category = 3 + */ + category: string[]; + /** + * @generated from protobuf field: optional uint32 flags = 4 + */ + flags?: number; +} +/** + * @generated from protobuf message RotateMessage + */ +export interface RotateMessage { + /** + * @generated from protobuf field: required int32 rotation = 1 + */ + rotation: number; +} +/** + * @generated from protobuf message ChangeQualityMessage + */ +export interface ChangeQualityMessage { + /** + * @generated from protobuf field: required int32 quality = 1 + */ + quality: number; +} +/** + * @generated from protobuf message ForwardTestMessage + */ +export interface ForwardTestMessage { + /** + * @generated from protobuf field: required string targetHost = 1 + */ + targetHost: string; + /** + * @generated from protobuf field: required uint32 targetPort = 2 + */ + targetPort: number; +} +/** + * @generated from protobuf message ForwardCreateMessage + */ +export interface ForwardCreateMessage { + /** + * @generated from protobuf field: required string id = 1 + */ + id: string; + /** + * @generated from protobuf field: required uint32 devicePort = 2 + */ + devicePort: number; + /** + * @generated from protobuf field: required string targetHost = 3 + */ + targetHost: string; + /** + * @generated from protobuf field: required uint32 targetPort = 4 + */ + targetPort: number; +} +/** + * @generated from protobuf message ForwardRemoveMessage + */ +export interface ForwardRemoveMessage { + /** + * @generated from protobuf field: required string id = 1 + */ + id: string; +} +/** + * @generated from protobuf message ReverseForward + */ +export interface ReverseForward { + /** + * @generated from protobuf field: required string id = 1 + */ + id: string; + /** + * @generated from protobuf field: required uint32 devicePort = 2 + */ + devicePort: number; + /** + * @generated from protobuf field: required string targetHost = 3 + */ + targetHost: string; + /** + * @generated from protobuf field: required uint32 targetPort = 4 + */ + targetPort: number; +} +/** + * @generated from protobuf message ReverseForwardsEvent + */ +export interface ReverseForwardsEvent { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; + /** + * @generated from protobuf field: repeated ReverseForward forwards = 2 + */ + forwards: ReverseForward[]; +} +/** + * @generated from protobuf message BrowserOpenMessage + */ +export interface BrowserOpenMessage { + /** + * @generated from protobuf field: required string url = 1 + */ + url: string; + /** + * @generated from protobuf field: optional string browser = 2 + */ + browser?: string; +} +/** + * @generated from protobuf message BrowserClearMessage + */ +export interface BrowserClearMessage { + /** + * @generated from protobuf field: optional string browser = 1 + */ + browser?: string; +} +/** + * @generated from protobuf message StoreOpenMessage + */ +export interface StoreOpenMessage { +} +/** + * @generated from protobuf message ScreenCaptureMessage + */ +export interface ScreenCaptureMessage { +} +/** + * @generated from protobuf message ConnectStartMessage + */ +export interface ConnectStartMessage { +} +/** + * @generated from protobuf message ConnectGetForwardUrlMessage + */ +export interface ConnectGetForwardUrlMessage { +} +/** + * @generated from protobuf message ConnectStopMessage + */ +export interface ConnectStopMessage { +} +/** + * @generated from protobuf message AccountAddMenuMessage + */ +export interface AccountAddMenuMessage { +} +/** + * @generated from protobuf message AccountAddMessage + */ +export interface AccountAddMessage { + /** + * @generated from protobuf field: required string user = 1 + */ + user: string; + /** + * @generated from protobuf field: required string password = 2 + */ + password: string; +} +/** + * @generated from protobuf message AccountCheckMessage + */ +export interface AccountCheckMessage { + /** + * @generated from protobuf field: required string type = 1 + */ + type: string; + /** + * @generated from protobuf field: required string account = 2 + */ + account: string; +} +/** + * @generated from protobuf message AccountGetMessage + */ +export interface AccountGetMessage { + /** + * @generated from protobuf field: optional string type = 1 + */ + type?: string; +} +/** + * @generated from protobuf message AccountRemoveMessage + */ +export interface AccountRemoveMessage { + /** + * @generated from protobuf field: required string type = 1 + */ + type: string; + /** + * @generated from protobuf field: optional string account = 2 + */ + account?: string; +} +/** + * @generated from protobuf message SdStatusMessage + */ +export interface SdStatusMessage { +} +/** + * @generated from protobuf message AirplaneSetMessage + */ +export interface AirplaneSetMessage { + /** + * @generated from protobuf field: required bool enabled = 1 + */ + enabled: boolean; +} +/** + * @generated from protobuf message RingerSetMessage + */ +export interface RingerSetMessage { + /** + * @generated from protobuf field: required RingerMode mode = 1 + */ + mode: RingerMode; +} +/** + * @generated from protobuf message RingerGetMessage + */ +export interface RingerGetMessage { +} +/** + * @generated from protobuf message WifiSetEnabledMessage + */ +export interface WifiSetEnabledMessage { + /** + * @generated from protobuf field: required bool enabled = 1 + */ + enabled: boolean; +} +/** + * @generated from protobuf message WifiGetStatusMessage + */ +export interface WifiGetStatusMessage { +} +/** + * @generated from protobuf message BluetoothSetEnabledMessage + */ +export interface BluetoothSetEnabledMessage { + /** + * @generated from protobuf field: required bool enabled = 1 + */ + enabled: boolean; +} +/** + * @generated from protobuf message BluetoothGetStatusMessage + */ +export interface BluetoothGetStatusMessage { +} +/** + * @generated from protobuf message BluetoothCleanBondedMessage + */ +export interface BluetoothCleanBondedMessage { +} +/** + * @generated from protobuf message CapabilitiesMessage + */ +export interface CapabilitiesMessage { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; + /** + * @generated from protobuf field: required bool hasTouch = 2 + */ + hasTouch: boolean; + /** + * @generated from protobuf field: required bool hasCursor = 3 + */ + hasCursor: boolean; // TODO: + // required bool hasKeyboard = 1; + // hasLogs + // hasMedia + // hasClipboard and stuff like that, but later.. +} +// Events, these must be kept in sync with STFService/wire.proto + +/** + * @generated from protobuf message AirplaneModeEvent + */ +export interface AirplaneModeEvent { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; + /** + * @generated from protobuf field: required bool enabled = 2 + */ + enabled: boolean; +} +/** + * @generated from protobuf message BatteryEvent + */ +export interface BatteryEvent { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; + /** + * @generated from protobuf field: required string status = 2 + */ + status: string; + /** + * @generated from protobuf field: required string health = 3 + */ + health: string; + /** + * @generated from protobuf field: required string source = 4 + */ + source: string; + /** + * @generated from protobuf field: required uint32 level = 5 + */ + level: number; + /** + * @generated from protobuf field: required uint32 scale = 6 + */ + scale: number; + /** + * @generated from protobuf field: required double temp = 7 + */ + temp: number; + /** + * @generated from protobuf field: optional double voltage = 8 + */ + voltage?: number; +} +/** + * @generated from protobuf message ConnectivityEvent + */ +export interface ConnectivityEvent { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; + /** + * @generated from protobuf field: required bool connected = 2 + */ + connected: boolean; + /** + * @generated from protobuf field: optional string type = 3 + */ + type?: string; + /** + * @generated from protobuf field: optional string subtype = 4 + */ + subtype?: string; + /** + * @generated from protobuf field: optional bool failover = 5 + */ + failover?: boolean; + /** + * @generated from protobuf field: optional bool roaming = 6 + */ + roaming?: boolean; +} +/** + * @generated from protobuf message PhoneStateEvent + */ +export interface PhoneStateEvent { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; + /** + * @generated from protobuf field: required string state = 2 + */ + state: string; + /** + * @generated from protobuf field: required bool manual = 3 + */ + manual: boolean; + /** + * @generated from protobuf field: optional string operator = 4 + */ + operator?: string; +} +/** + * @generated from protobuf message RotationEvent + */ +export interface RotationEvent { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; + /** + * @generated from protobuf field: required int32 rotation = 2 + */ + rotation: number; + /** + * @generated from protobuf field: optional int32 width = 3 + */ + width?: number; + /** + * @generated from protobuf field: optional int32 height = 4 + */ + height?: number; +} +/** + * @generated from protobuf message SetDeviceDisplay + */ +export interface SetDeviceDisplay { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; + /** + * @generated from protobuf field: required string channel = 2 + */ + channel: string; + /** + * @generated from protobuf field: required int32 width = 3 + */ + width: number; + /** + * @generated from protobuf field: required int32 height = 4 + */ + height: number; +} +/** + * @generated from protobuf message IosDevicePorts + */ +export interface IosDevicePorts { + /** + * @generated from protobuf field: required int32 screenPort = 2 + */ + screenPort: number; + /** + * @generated from protobuf field: required int32 connectPort = 3 + */ + connectPort: number; +} +/** + * @generated from protobuf message StartStreaming + */ +export interface StartStreaming { + /** + * @generated from protobuf field: required int32 port = 1 + */ + port: number; + /** + * @generated from protobuf field: required string channel = 2 + */ + channel: string; +} +/** + * @generated from protobuf message DeleteDevice + */ +export interface DeleteDevice { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; +} +/** + * @generated from protobuf message SetAbsentDisconnectedDevices + */ +export interface SetAbsentDisconnectedDevices { +} +/** + * @generated from protobuf message Applications + */ +export interface Applications { + /** + * @generated from protobuf field: required string bundleId = 1 + */ + bundleId: string; + /** + * @generated from protobuf field: required string bundleName = 2 + */ + bundleName: string; +} +/** + * @generated from protobuf message InstalledApplications + */ +export interface InstalledApplications { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; + /** + * @generated from protobuf field: repeated Applications applications = 2 + */ + applications: Applications[]; +} +/** + * @generated from protobuf message TransportInstalledApps + */ +export interface TransportInstalledApps { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; +} +/** + * @generated from protobuf message SetDeviceApp + */ +export interface SetDeviceApp { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; + /** + * @generated from protobuf field: required string bundleId = 2 + */ + bundleId: string; + /** + * @generated from protobuf field: required string bundleName = 3 + */ + bundleName: string; + /** + * @generated from protobuf field: required string pathToApp = 4 + */ + pathToApp: string; +} +/** + * @generated from protobuf message GetIosDeviceApps + */ +export interface GetIosDeviceApps { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; + /** + * @generated from protobuf field: required string bundleId = 2 + */ + bundleId: string; + /** + * @generated from protobuf field: required string bundleName = 3 + */ + bundleName: string; + /** + * @generated from protobuf field: required string pathToApp = 4 + */ + pathToApp: string; +} +/** + * @generated from protobuf message TransationGetMessage + */ +export interface TransationGetMessage { + /** + * @generated from protobuf field: required string source = 1 + */ + source: string; + /** + * @generated from protobuf field: required string data = 2 + */ + data: string; +} +/** + * required string serial = 1; + * optional string channel = 2; + * + * @generated from protobuf message GetInstalledApplications + */ +export interface GetInstalledApplications { +} +/** + * @generated from protobuf message UpdateIosDevice + */ +export interface UpdateIosDevice { + /** + * @generated from protobuf field: required string id = 1 + */ + id: string; + /** + * @generated from protobuf field: required string name = 2 + */ + name: string; + /** + * @generated from protobuf field: required string platform = 3 + */ + platform: string; + /** + * @generated from protobuf field: required string architecture = 4 + */ + architecture: string; + /** + * @generated from protobuf field: required string sdk = 5 + */ + sdk: string; + /** + * @generated from protobuf field: required IosServiceMessage service = 6 + */ + service?: IosServiceMessage; +} +/** + * @generated from protobuf message SdkIosVersion + */ +export interface SdkIosVersion { + /** + * @generated from protobuf field: required string id = 1 + */ + id: string; + /** + * @generated from protobuf field: required string sdkVersion = 2 + */ + sdkVersion: string; +} +/** + * @generated from protobuf message SizeIosDevice + */ +export interface SizeIosDevice { + /** + * @generated from protobuf field: required string id = 1 + */ + id: string; + /** + * @generated from protobuf field: required double height = 2 + */ + height: number; + /** + * @generated from protobuf field: required double width = 3 + */ + width: number; + /** + * @generated from protobuf field: required int32 scale = 4 + */ + scale: number; +} +/** + * @generated from protobuf message DashboardOpenMessage + */ +export interface DashboardOpenMessage { +} +/** + * @generated from protobuf message GetIosTreeElements + */ +export interface GetIosTreeElements { +} +/** + * @generated from protobuf message TapDeviceTreeElement + */ +export interface TapDeviceTreeElement { + /** + * @generated from protobuf field: required string label = 1 + */ + label: string; +} +/** + * @generated from protobuf message TemporarilyUnavailableMessage + */ +export interface TemporarilyUnavailableMessage { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; +} +/** + * @generated from protobuf message UpdateRemoteConnectUrl + */ +export interface UpdateRemoteConnectUrl { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; +} +/** + * @generated from protobuf message IosServiceMessage + */ +export interface IosServiceMessage { + /** + * @generated from protobuf field: required bool hasAPNS = 1 + */ + hasAPNS: boolean; +} +/** + * @generated from protobuf message LaunchDeviceApp + */ +export interface LaunchDeviceApp { + /** + * @generated from protobuf field: required string pkg = 1 + */ + pkg: string; +} +/** + * @generated from protobuf message TerminateDeviceApp + */ +export interface TerminateDeviceApp { +} +/** + * @generated from protobuf message KillDeviceApp + */ +export interface KillDeviceApp { +} +/** + * @generated from protobuf message GetAppAsset + */ +export interface GetAppAsset { + /** + * @generated from protobuf field: required string url = 1 + */ + url: string; +} +/** + * @generated from protobuf message GetAppAssetsList + */ +export interface GetAppAssetsList { +} +/** + * @generated from protobuf message GetAppHTML + */ +export interface GetAppHTML { +} +/** + * @generated from protobuf message GetAppInspectServerUrl + */ +export interface GetAppInspectServerUrl { +} +/** + * @generated from protobuf enum DeviceStatus + */ +export enum DeviceStatus { + /** + * @generated synthetic value - protobuf-ts requires all enums to have a 0 value + */ + UNSPECIFIED$ = 0, + /** + * @generated from protobuf enum value: OFFLINE = 1; + */ + OFFLINE = 1, + /** + * @generated from protobuf enum value: UNAUTHORIZED = 2; + */ + UNAUTHORIZED = 2, + /** + * @generated from protobuf enum value: ONLINE = 3; + */ + ONLINE = 3, + /** + * @generated from protobuf enum value: CONNECTING = 4; + */ + CONNECTING = 4, + /** + * @generated from protobuf enum value: AUTHORIZING = 5; + */ + AUTHORIZING = 5, + /** + * @generated from protobuf enum value: PREPARING = 6; + */ + PREPARING = 6, + /** + * @generated from protobuf enum value: UNHEALTHY = 7; + */ + UNHEALTHY = 7 +} +// Grouping + +/** + * @generated from protobuf enum RequirementType + */ +export enum RequirementType { + /** + * @generated synthetic value - protobuf-ts requires all enums to have a 0 value + */ + UNSPECIFIED$ = 0, + /** + * @generated from protobuf enum value: SEMVER = 1; + */ + SEMVER = 1, + /** + * @generated from protobuf enum value: GLOB = 2; + */ + GLOB = 2, + /** + * @generated from protobuf enum value: EXACT = 3; + */ + EXACT = 3 +} +/** + * @generated from protobuf enum RingerMode + */ +export enum RingerMode { + /** + * @generated from protobuf enum value: SILENT = 0; + */ + SILENT = 0, + /** + * @generated from protobuf enum value: VIBRATE = 1; + */ + VIBRATE = 1, + /** + * @generated from protobuf enum value: NORMAL = 2; + */ + NORMAL = 2 +} +// @generated message type with reflection information, may provide speed optimized methods +class Envelope$Type extends MessageType { + constructor() { + super("Envelope", [ + { no: 2, name: "message", kind: "message", T: () => Any }, + { no: 3, name: "channel", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): Envelope { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: Envelope): Envelope { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required google.protobuf.Any message */ 2: + message.message = Any.internalBinaryRead(reader, reader.uint32(), options, message.message); + break; + case /* optional string channel */ 3: + message.channel = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: Envelope, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required google.protobuf.Any message = 2; */ + if (message.message) + Any.internalBinaryWrite(message.message, writer.tag(2, WireType.LengthDelimited).fork(), options).join(); + /* optional string channel = 3; */ + if (message.channel !== undefined) + writer.tag(3, WireType.LengthDelimited).string(message.channel); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message Envelope + */ +export const Envelope = new Envelope$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class UpdateAccessTokenMessage$Type extends MessageType { + constructor() { + super("UpdateAccessTokenMessage", []); + } + create(value?: PartialMessage): UpdateAccessTokenMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: UpdateAccessTokenMessage): UpdateAccessTokenMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: UpdateAccessTokenMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message UpdateAccessTokenMessage + */ +export const UpdateAccessTokenMessage = new UpdateAccessTokenMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class UnlockDeviceMessage$Type extends MessageType { + constructor() { + super("UnlockDeviceMessage", []); + } + create(value?: PartialMessage): UnlockDeviceMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: UnlockDeviceMessage): UnlockDeviceMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: UnlockDeviceMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message UnlockDeviceMessage + */ +export const UnlockDeviceMessage = new UnlockDeviceMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class DeleteUserMessage$Type extends MessageType { + constructor() { + super("DeleteUserMessage", [ + { no: 1, name: "email", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): DeleteUserMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.email = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DeleteUserMessage): DeleteUserMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string email */ 1: + message.email = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: DeleteUserMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string email = 1; */ + if (message.email !== "") + writer.tag(1, WireType.LengthDelimited).string(message.email); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message DeleteUserMessage + */ +export const DeleteUserMessage = new DeleteUserMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class DeviceOriginGroupMessage$Type extends MessageType { + constructor() { + super("DeviceOriginGroupMessage", [ + { no: 1, name: "signature", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): DeviceOriginGroupMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.signature = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DeviceOriginGroupMessage): DeviceOriginGroupMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string signature */ 1: + message.signature = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: DeviceOriginGroupMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string signature = 1; */ + if (message.signature !== "") + writer.tag(1, WireType.LengthDelimited).string(message.signature); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message DeviceOriginGroupMessage + */ +export const DeviceOriginGroupMessage = new DeviceOriginGroupMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class UserQuotasDetailField$Type extends MessageType { + constructor() { + super("UserQuotasDetailField", [ + { no: 1, name: "duration", kind: "scalar", T: 1 /*ScalarType.DOUBLE*/ }, + { no: 2, name: "number", kind: "scalar", T: 13 /*ScalarType.UINT32*/ } + ]); + } + create(value?: PartialMessage): UserQuotasDetailField { + const message = globalThis.Object.create((this.messagePrototype!)); + message.duration = 0; + message.number = 0; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: UserQuotasDetailField): UserQuotasDetailField { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required double duration */ 1: + message.duration = reader.double(); + break; + case /* required uint32 number */ 2: + message.number = reader.uint32(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: UserQuotasDetailField, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required double duration = 1; */ + if (message.duration !== 0) + writer.tag(1, WireType.Bit64).double(message.duration); + /* required uint32 number = 2; */ + if (message.number !== 0) + writer.tag(2, WireType.Varint).uint32(message.number); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message UserQuotasDetailField + */ +export const UserQuotasDetailField = new UserQuotasDetailField$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class UserQuotasField$Type extends MessageType { + constructor() { + super("UserQuotasField", [ + { no: 1, name: "allocated", kind: "message", T: () => UserQuotasDetailField }, + { no: 2, name: "consumed", kind: "message", T: () => UserQuotasDetailField }, + { no: 3, name: "defaultGroupsDuration", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }, + { no: 4, name: "defaultGroupsNumber", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }, + { no: 5, name: "defaultGroupsRepetitions", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }, + { no: 6, name: "repetitions", kind: "scalar", T: 13 /*ScalarType.UINT32*/ } + ]); + } + create(value?: PartialMessage): UserQuotasField { + const message = globalThis.Object.create((this.messagePrototype!)); + message.defaultGroupsDuration = 0; + message.defaultGroupsNumber = 0; + message.defaultGroupsRepetitions = 0; + message.repetitions = 0; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: UserQuotasField): UserQuotasField { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required UserQuotasDetailField allocated */ 1: + message.allocated = UserQuotasDetailField.internalBinaryRead(reader, reader.uint32(), options, message.allocated); + break; + case /* required UserQuotasDetailField consumed */ 2: + message.consumed = UserQuotasDetailField.internalBinaryRead(reader, reader.uint32(), options, message.consumed); + break; + case /* required uint32 defaultGroupsDuration */ 3: + message.defaultGroupsDuration = reader.uint32(); + break; + case /* required uint32 defaultGroupsNumber */ 4: + message.defaultGroupsNumber = reader.uint32(); + break; + case /* required uint32 defaultGroupsRepetitions */ 5: + message.defaultGroupsRepetitions = reader.uint32(); + break; + case /* required uint32 repetitions */ 6: + message.repetitions = reader.uint32(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: UserQuotasField, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required UserQuotasDetailField allocated = 1; */ + if (message.allocated) + UserQuotasDetailField.internalBinaryWrite(message.allocated, writer.tag(1, WireType.LengthDelimited).fork(), options).join(); + /* required UserQuotasDetailField consumed = 2; */ + if (message.consumed) + UserQuotasDetailField.internalBinaryWrite(message.consumed, writer.tag(2, WireType.LengthDelimited).fork(), options).join(); + /* required uint32 defaultGroupsDuration = 3; */ + if (message.defaultGroupsDuration !== 0) + writer.tag(3, WireType.Varint).uint32(message.defaultGroupsDuration); + /* required uint32 defaultGroupsNumber = 4; */ + if (message.defaultGroupsNumber !== 0) + writer.tag(4, WireType.Varint).uint32(message.defaultGroupsNumber); + /* required uint32 defaultGroupsRepetitions = 5; */ + if (message.defaultGroupsRepetitions !== 0) + writer.tag(5, WireType.Varint).uint32(message.defaultGroupsRepetitions); + /* required uint32 repetitions = 6; */ + if (message.repetitions !== 0) + writer.tag(6, WireType.Varint).uint32(message.repetitions); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message UserQuotasField + */ +export const UserQuotasField = new UserQuotasField$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class UserGroupsField$Type extends MessageType { + constructor() { + super("UserGroupsField", [ + { no: 1, name: "quotas", kind: "message", T: () => UserQuotasField }, + { no: 2, name: "subscribed", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): UserGroupsField { + const message = globalThis.Object.create((this.messagePrototype!)); + message.subscribed = []; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: UserGroupsField): UserGroupsField { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required UserQuotasField quotas */ 1: + message.quotas = UserQuotasField.internalBinaryRead(reader, reader.uint32(), options, message.quotas); + break; + case /* repeated string subscribed */ 2: + message.subscribed.push(reader.string()); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: UserGroupsField, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required UserQuotasField quotas = 1; */ + if (message.quotas) + UserQuotasField.internalBinaryWrite(message.quotas, writer.tag(1, WireType.LengthDelimited).fork(), options).join(); + /* repeated string subscribed = 2; */ + for (let i = 0; i < message.subscribed.length; i++) + writer.tag(2, WireType.LengthDelimited).string(message.subscribed[i]); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message UserGroupsField + */ +export const UserGroupsField = new UserGroupsField$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class AlertMessageField$Type extends MessageType { + constructor() { + super("AlertMessageField", [ + { no: 1, name: "activation", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "data", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 3, name: "level", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): AlertMessageField { + const message = globalThis.Object.create((this.messagePrototype!)); + message.activation = ""; + message.data = ""; + message.level = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: AlertMessageField): AlertMessageField { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string activation */ 1: + message.activation = reader.string(); + break; + case /* required string data */ 2: + message.data = reader.string(); + break; + case /* required string level */ 3: + message.level = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: AlertMessageField, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string activation = 1; */ + if (message.activation !== "") + writer.tag(1, WireType.LengthDelimited).string(message.activation); + /* required string data = 2; */ + if (message.data !== "") + writer.tag(2, WireType.LengthDelimited).string(message.data); + /* required string level = 3; */ + if (message.level !== "") + writer.tag(3, WireType.LengthDelimited).string(message.level); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message AlertMessageField + */ +export const AlertMessageField = new AlertMessageField$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class UserSettingsField$Type extends MessageType { + constructor() { + super("UserSettingsField", [ + { no: 1, name: "alertMessage", kind: "message", T: () => AlertMessageField } + ]); + } + create(value?: PartialMessage): UserSettingsField { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: UserSettingsField): UserSettingsField { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* optional AlertMessageField alertMessage */ 1: + message.alertMessage = AlertMessageField.internalBinaryRead(reader, reader.uint32(), options, message.alertMessage); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: UserSettingsField, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* optional AlertMessageField alertMessage = 1; */ + if (message.alertMessage) + AlertMessageField.internalBinaryWrite(message.alertMessage, writer.tag(1, WireType.LengthDelimited).fork(), options).join(); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message UserSettingsField + */ +export const UserSettingsField = new UserSettingsField$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class UserField$Type extends MessageType { + constructor() { + super("UserField", [ + { no: 1, name: "email", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "name", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 3, name: "privilege", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 4, name: "groups", kind: "message", T: () => UserGroupsField }, + { no: 5, name: "settings", kind: "message", T: () => UserSettingsField } + ]); + } + create(value?: PartialMessage): UserField { + const message = globalThis.Object.create((this.messagePrototype!)); + message.email = ""; + message.name = ""; + message.privilege = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: UserField): UserField { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string email */ 1: + message.email = reader.string(); + break; + case /* required string name */ 2: + message.name = reader.string(); + break; + case /* required string privilege */ 3: + message.privilege = reader.string(); + break; + case /* required UserGroupsField groups */ 4: + message.groups = UserGroupsField.internalBinaryRead(reader, reader.uint32(), options, message.groups); + break; + case /* optional UserSettingsField settings */ 5: + message.settings = UserSettingsField.internalBinaryRead(reader, reader.uint32(), options, message.settings); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: UserField, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string email = 1; */ + if (message.email !== "") + writer.tag(1, WireType.LengthDelimited).string(message.email); + /* required string name = 2; */ + if (message.name !== "") + writer.tag(2, WireType.LengthDelimited).string(message.name); + /* required string privilege = 3; */ + if (message.privilege !== "") + writer.tag(3, WireType.LengthDelimited).string(message.privilege); + /* required UserGroupsField groups = 4; */ + if (message.groups) + UserGroupsField.internalBinaryWrite(message.groups, writer.tag(4, WireType.LengthDelimited).fork(), options).join(); + /* optional UserSettingsField settings = 5; */ + if (message.settings) + UserSettingsField.internalBinaryWrite(message.settings, writer.tag(5, WireType.LengthDelimited).fork(), options).join(); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message UserField + */ +export const UserField = new UserField$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class UserChangeMessage$Type extends MessageType { + constructor() { + super("UserChangeMessage", [ + { no: 1, name: "user", kind: "message", T: () => UserField }, + { no: 2, name: "isAddedGroup", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }, + { no: 3, name: "groups", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ }, + { no: 4, name: "action", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 5, name: "targets", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ }, + { no: 6, name: "timeStamp", kind: "scalar", T: 1 /*ScalarType.DOUBLE*/ } + ]); + } + create(value?: PartialMessage): UserChangeMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.isAddedGroup = false; + message.groups = []; + message.action = ""; + message.targets = []; + message.timeStamp = 0; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: UserChangeMessage): UserChangeMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required UserField user */ 1: + message.user = UserField.internalBinaryRead(reader, reader.uint32(), options, message.user); + break; + case /* required bool isAddedGroup */ 2: + message.isAddedGroup = reader.bool(); + break; + case /* repeated string groups */ 3: + message.groups.push(reader.string()); + break; + case /* required string action */ 4: + message.action = reader.string(); + break; + case /* repeated string targets */ 5: + message.targets.push(reader.string()); + break; + case /* required double timeStamp */ 6: + message.timeStamp = reader.double(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: UserChangeMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required UserField user = 1; */ + if (message.user) + UserField.internalBinaryWrite(message.user, writer.tag(1, WireType.LengthDelimited).fork(), options).join(); + /* required bool isAddedGroup = 2; */ + if (message.isAddedGroup !== false) + writer.tag(2, WireType.Varint).bool(message.isAddedGroup); + /* repeated string groups = 3; */ + for (let i = 0; i < message.groups.length; i++) + writer.tag(3, WireType.LengthDelimited).string(message.groups[i]); + /* required string action = 4; */ + if (message.action !== "") + writer.tag(4, WireType.LengthDelimited).string(message.action); + /* repeated string targets = 5; */ + for (let i = 0; i < message.targets.length; i++) + writer.tag(5, WireType.LengthDelimited).string(message.targets[i]); + /* required double timeStamp = 6; */ + if (message.timeStamp !== 0) + writer.tag(6, WireType.Bit64).double(message.timeStamp); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message UserChangeMessage + */ +export const UserChangeMessage = new UserChangeMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class DeviceNetworkField$Type extends MessageType { + constructor() { + super("DeviceNetworkField", [ + { no: 1, name: "type", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "subtype", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): DeviceNetworkField { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DeviceNetworkField): DeviceNetworkField { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* optional string type */ 1: + message.type = reader.string(); + break; + case /* optional string subtype */ 2: + message.subtype = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: DeviceNetworkField, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* optional string type = 1; */ + if (message.type !== undefined) + writer.tag(1, WireType.LengthDelimited).string(message.type); + /* optional string subtype = 2; */ + if (message.subtype !== undefined) + writer.tag(2, WireType.LengthDelimited).string(message.subtype); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message DeviceNetworkField + */ +export const DeviceNetworkField = new DeviceNetworkField$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class DeviceDisplayField$Type extends MessageType { + constructor() { + super("DeviceDisplayField", [ + { no: 1, name: "height", kind: "scalar", opt: true, T: 13 /*ScalarType.UINT32*/ }, + { no: 2, name: "width", kind: "scalar", opt: true, T: 13 /*ScalarType.UINT32*/ } + ]); + } + create(value?: PartialMessage): DeviceDisplayField { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DeviceDisplayField): DeviceDisplayField { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* optional uint32 height */ 1: + message.height = reader.uint32(); + break; + case /* optional uint32 width */ 2: + message.width = reader.uint32(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: DeviceDisplayField, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* optional uint32 height = 1; */ + if (message.height !== undefined) + writer.tag(1, WireType.Varint).uint32(message.height); + /* optional uint32 width = 2; */ + if (message.width !== undefined) + writer.tag(2, WireType.Varint).uint32(message.width); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message DeviceDisplayField + */ +export const DeviceDisplayField = new DeviceDisplayField$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class DevicePhoneField$Type extends MessageType { + constructor() { + super("DevicePhoneField", [ + { no: 1, name: "imei", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): DevicePhoneField { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DevicePhoneField): DevicePhoneField { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* optional string imei */ 1: + message.imei = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: DevicePhoneField, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* optional string imei = 1; */ + if (message.imei !== undefined) + writer.tag(1, WireType.LengthDelimited).string(message.imei); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message DevicePhoneField + */ +export const DevicePhoneField = new DevicePhoneField$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class DeviceProviderField$Type extends MessageType { + constructor() { + super("DeviceProviderField", [ + { no: 1, name: "name", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): DeviceProviderField { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DeviceProviderField): DeviceProviderField { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* optional string name */ 1: + message.name = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: DeviceProviderField, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* optional string name = 1; */ + if (message.name !== undefined) + writer.tag(1, WireType.LengthDelimited).string(message.name); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message DeviceProviderField + */ +export const DeviceProviderField = new DeviceProviderField$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class DeviceGroupField$Type extends MessageType { + constructor() { + super("DeviceGroupField", [ + { no: 1, name: "id", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "name", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }, + { no: 3, name: "origin", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }, + { no: 4, name: "originName", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }, + { no: 5, name: "owner", kind: "message", T: () => GroupOwnerField } + ]); + } + create(value?: PartialMessage): DeviceGroupField { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DeviceGroupField): DeviceGroupField { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* optional string id */ 1: + message.id = reader.string(); + break; + case /* optional string name */ 2: + message.name = reader.string(); + break; + case /* optional string origin */ 3: + message.origin = reader.string(); + break; + case /* optional string originName */ 4: + message.originName = reader.string(); + break; + case /* optional GroupOwnerField owner */ 5: + message.owner = GroupOwnerField.internalBinaryRead(reader, reader.uint32(), options, message.owner); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: DeviceGroupField, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* optional string id = 1; */ + if (message.id !== undefined) + writer.tag(1, WireType.LengthDelimited).string(message.id); + /* optional string name = 2; */ + if (message.name !== undefined) + writer.tag(2, WireType.LengthDelimited).string(message.name); + /* optional string origin = 3; */ + if (message.origin !== undefined) + writer.tag(3, WireType.LengthDelimited).string(message.origin); + /* optional string originName = 4; */ + if (message.originName !== undefined) + writer.tag(4, WireType.LengthDelimited).string(message.originName); + /* optional GroupOwnerField owner = 5; */ + if (message.owner) + GroupOwnerField.internalBinaryWrite(message.owner, writer.tag(5, WireType.LengthDelimited).fork(), options).join(); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message DeviceGroupField + */ +export const DeviceGroupField = new DeviceGroupField$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class DeviceField$Type extends MessageType { + constructor() { + super("DeviceField", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "model", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }, + { no: 3, name: "version", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }, + { no: 4, name: "operator", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }, + { no: 5, name: "network", kind: "message", T: () => DeviceNetworkField }, + { no: 6, name: "display", kind: "message", T: () => DeviceDisplayField }, + { no: 7, name: "manufacturer", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }, + { no: 8, name: "sdk", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }, + { no: 9, name: "abi", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }, + { no: 10, name: "cpuPlatform", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }, + { no: 11, name: "openGLESVersion", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }, + { no: 12, name: "phone", kind: "message", T: () => DevicePhoneField }, + { no: 13, name: "provider", kind: "message", T: () => DeviceProviderField }, + { no: 14, name: "group", kind: "message", T: () => DeviceGroupField }, + { no: 15, name: "marketName", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): DeviceField { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DeviceField): DeviceField { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + case /* optional string model */ 2: + message.model = reader.string(); + break; + case /* optional string version */ 3: + message.version = reader.string(); + break; + case /* optional string operator */ 4: + message.operator = reader.string(); + break; + case /* optional DeviceNetworkField network */ 5: + message.network = DeviceNetworkField.internalBinaryRead(reader, reader.uint32(), options, message.network); + break; + case /* optional DeviceDisplayField display */ 6: + message.display = DeviceDisplayField.internalBinaryRead(reader, reader.uint32(), options, message.display); + break; + case /* optional string manufacturer */ 7: + message.manufacturer = reader.string(); + break; + case /* optional string sdk */ 8: + message.sdk = reader.string(); + break; + case /* optional string abi */ 9: + message.abi = reader.string(); + break; + case /* optional string cpuPlatform */ 10: + message.cpuPlatform = reader.string(); + break; + case /* optional string openGLESVersion */ 11: + message.openGLESVersion = reader.string(); + break; + case /* optional DevicePhoneField phone */ 12: + message.phone = DevicePhoneField.internalBinaryRead(reader, reader.uint32(), options, message.phone); + break; + case /* optional DeviceProviderField provider */ 13: + message.provider = DeviceProviderField.internalBinaryRead(reader, reader.uint32(), options, message.provider); + break; + case /* optional DeviceGroupField group */ 14: + message.group = DeviceGroupField.internalBinaryRead(reader, reader.uint32(), options, message.group); + break; + case /* optional string marketName */ 15: + message.marketName = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: DeviceField, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + /* optional string model = 2; */ + if (message.model !== undefined) + writer.tag(2, WireType.LengthDelimited).string(message.model); + /* optional string version = 3; */ + if (message.version !== undefined) + writer.tag(3, WireType.LengthDelimited).string(message.version); + /* optional string operator = 4; */ + if (message.operator !== undefined) + writer.tag(4, WireType.LengthDelimited).string(message.operator); + /* optional DeviceNetworkField network = 5; */ + if (message.network) + DeviceNetworkField.internalBinaryWrite(message.network, writer.tag(5, WireType.LengthDelimited).fork(), options).join(); + /* optional DeviceDisplayField display = 6; */ + if (message.display) + DeviceDisplayField.internalBinaryWrite(message.display, writer.tag(6, WireType.LengthDelimited).fork(), options).join(); + /* optional string manufacturer = 7; */ + if (message.manufacturer !== undefined) + writer.tag(7, WireType.LengthDelimited).string(message.manufacturer); + /* optional string sdk = 8; */ + if (message.sdk !== undefined) + writer.tag(8, WireType.LengthDelimited).string(message.sdk); + /* optional string abi = 9; */ + if (message.abi !== undefined) + writer.tag(9, WireType.LengthDelimited).string(message.abi); + /* optional string cpuPlatform = 10; */ + if (message.cpuPlatform !== undefined) + writer.tag(10, WireType.LengthDelimited).string(message.cpuPlatform); + /* optional string openGLESVersion = 11; */ + if (message.openGLESVersion !== undefined) + writer.tag(11, WireType.LengthDelimited).string(message.openGLESVersion); + /* optional DevicePhoneField phone = 12; */ + if (message.phone) + DevicePhoneField.internalBinaryWrite(message.phone, writer.tag(12, WireType.LengthDelimited).fork(), options).join(); + /* optional DeviceProviderField provider = 13; */ + if (message.provider) + DeviceProviderField.internalBinaryWrite(message.provider, writer.tag(13, WireType.LengthDelimited).fork(), options).join(); + /* optional DeviceGroupField group = 14; */ + if (message.group) + DeviceGroupField.internalBinaryWrite(message.group, writer.tag(14, WireType.LengthDelimited).fork(), options).join(); + /* optional string marketName = 15; */ + if (message.marketName !== undefined) + writer.tag(15, WireType.LengthDelimited).string(message.marketName); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message DeviceField + */ +export const DeviceField = new DeviceField$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class DeviceChangeMessage$Type extends MessageType { + constructor() { + super("DeviceChangeMessage", [ + { no: 1, name: "device", kind: "message", T: () => DeviceField }, + { no: 2, name: "action", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 3, name: "oldOriginGroupId", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 4, name: "timeStamp", kind: "scalar", T: 1 /*ScalarType.DOUBLE*/ } + ]); + } + create(value?: PartialMessage): DeviceChangeMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.action = ""; + message.oldOriginGroupId = ""; + message.timeStamp = 0; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DeviceChangeMessage): DeviceChangeMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required DeviceField device */ 1: + message.device = DeviceField.internalBinaryRead(reader, reader.uint32(), options, message.device); + break; + case /* required string action */ 2: + message.action = reader.string(); + break; + case /* required string oldOriginGroupId */ 3: + message.oldOriginGroupId = reader.string(); + break; + case /* required double timeStamp */ 4: + message.timeStamp = reader.double(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: DeviceChangeMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required DeviceField device = 1; */ + if (message.device) + DeviceField.internalBinaryWrite(message.device, writer.tag(1, WireType.LengthDelimited).fork(), options).join(); + /* required string action = 2; */ + if (message.action !== "") + writer.tag(2, WireType.LengthDelimited).string(message.action); + /* required string oldOriginGroupId = 3; */ + if (message.oldOriginGroupId !== "") + writer.tag(3, WireType.LengthDelimited).string(message.oldOriginGroupId); + /* required double timeStamp = 4; */ + if (message.timeStamp !== 0) + writer.tag(4, WireType.Bit64).double(message.timeStamp); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message DeviceChangeMessage + */ +export const DeviceChangeMessage = new DeviceChangeMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class GroupDateField$Type extends MessageType { + constructor() { + super("GroupDateField", [ + { no: 1, name: "start", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "stop", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): GroupDateField { + const message = globalThis.Object.create((this.messagePrototype!)); + message.start = ""; + message.stop = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: GroupDateField): GroupDateField { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string start */ 1: + message.start = reader.string(); + break; + case /* required string stop */ 2: + message.stop = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: GroupDateField, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string start = 1; */ + if (message.start !== "") + writer.tag(1, WireType.LengthDelimited).string(message.start); + /* required string stop = 2; */ + if (message.stop !== "") + writer.tag(2, WireType.LengthDelimited).string(message.stop); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message GroupDateField + */ +export const GroupDateField = new GroupDateField$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class GroupOwnerField$Type extends MessageType { + constructor() { + super("GroupOwnerField", [ + { no: 1, name: "email", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "name", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): GroupOwnerField { + const message = globalThis.Object.create((this.messagePrototype!)); + message.email = ""; + message.name = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: GroupOwnerField): GroupOwnerField { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string email */ 1: + message.email = reader.string(); + break; + case /* required string name */ 2: + message.name = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: GroupOwnerField, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string email = 1; */ + if (message.email !== "") + writer.tag(1, WireType.LengthDelimited).string(message.email); + /* required string name = 2; */ + if (message.name !== "") + writer.tag(2, WireType.LengthDelimited).string(message.name); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message GroupOwnerField + */ +export const GroupOwnerField = new GroupOwnerField$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class GroupField$Type extends MessageType { + constructor() { + super("GroupField", [ + { no: 1, name: "id", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "name", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 3, name: "class", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 4, name: "privilege", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 5, name: "owner", kind: "message", T: () => GroupOwnerField }, + { no: 6, name: "dates", kind: "message", repeat: 2 /*RepeatType.UNPACKED*/, T: () => GroupDateField }, + { no: 7, name: "duration", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }, + { no: 8, name: "repetitions", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }, + { no: 9, name: "devices", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ }, + { no: 10, name: "users", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ }, + { no: 11, name: "state", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 12, name: "isActive", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }, + { no: 13, name: "moderators", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): GroupField { + const message = globalThis.Object.create((this.messagePrototype!)); + message.id = ""; + message.name = ""; + message.class = ""; + message.privilege = ""; + message.dates = []; + message.duration = 0; + message.repetitions = 0; + message.devices = []; + message.users = []; + message.state = ""; + message.isActive = false; + message.moderators = []; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: GroupField): GroupField { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string id */ 1: + message.id = reader.string(); + break; + case /* required string name */ 2: + message.name = reader.string(); + break; + case /* required string class */ 3: + message.class = reader.string(); + break; + case /* required string privilege */ 4: + message.privilege = reader.string(); + break; + case /* required GroupOwnerField owner */ 5: + message.owner = GroupOwnerField.internalBinaryRead(reader, reader.uint32(), options, message.owner); + break; + case /* repeated GroupDateField dates */ 6: + message.dates.push(GroupDateField.internalBinaryRead(reader, reader.uint32(), options)); + break; + case /* required uint32 duration */ 7: + message.duration = reader.uint32(); + break; + case /* required uint32 repetitions */ 8: + message.repetitions = reader.uint32(); + break; + case /* repeated string devices */ 9: + message.devices.push(reader.string()); + break; + case /* repeated string users */ 10: + message.users.push(reader.string()); + break; + case /* required string state */ 11: + message.state = reader.string(); + break; + case /* required bool isActive */ 12: + message.isActive = reader.bool(); + break; + case /* repeated string moderators */ 13: + message.moderators.push(reader.string()); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: GroupField, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string id = 1; */ + if (message.id !== "") + writer.tag(1, WireType.LengthDelimited).string(message.id); + /* required string name = 2; */ + if (message.name !== "") + writer.tag(2, WireType.LengthDelimited).string(message.name); + /* required string class = 3; */ + if (message.class !== "") + writer.tag(3, WireType.LengthDelimited).string(message.class); + /* required string privilege = 4; */ + if (message.privilege !== "") + writer.tag(4, WireType.LengthDelimited).string(message.privilege); + /* required GroupOwnerField owner = 5; */ + if (message.owner) + GroupOwnerField.internalBinaryWrite(message.owner, writer.tag(5, WireType.LengthDelimited).fork(), options).join(); + /* repeated GroupDateField dates = 6; */ + for (let i = 0; i < message.dates.length; i++) + GroupDateField.internalBinaryWrite(message.dates[i], writer.tag(6, WireType.LengthDelimited).fork(), options).join(); + /* required uint32 duration = 7; */ + if (message.duration !== 0) + writer.tag(7, WireType.Varint).uint32(message.duration); + /* required uint32 repetitions = 8; */ + if (message.repetitions !== 0) + writer.tag(8, WireType.Varint).uint32(message.repetitions); + /* repeated string devices = 9; */ + for (let i = 0; i < message.devices.length; i++) + writer.tag(9, WireType.LengthDelimited).string(message.devices[i]); + /* repeated string users = 10; */ + for (let i = 0; i < message.users.length; i++) + writer.tag(10, WireType.LengthDelimited).string(message.users[i]); + /* required string state = 11; */ + if (message.state !== "") + writer.tag(11, WireType.LengthDelimited).string(message.state); + /* required bool isActive = 12; */ + if (message.isActive !== false) + writer.tag(12, WireType.Varint).bool(message.isActive); + /* repeated string moderators = 13; */ + for (let i = 0; i < message.moderators.length; i++) + writer.tag(13, WireType.LengthDelimited).string(message.moderators[i]); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message GroupField + */ +export const GroupField = new GroupField$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class GroupChangeMessage$Type extends MessageType { + constructor() { + super("GroupChangeMessage", [ + { no: 1, name: "group", kind: "message", T: () => GroupField }, + { no: 2, name: "action", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 3, name: "subscribers", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ }, + { no: 4, name: "isChangedDates", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }, + { no: 5, name: "isChangedClass", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }, + { no: 6, name: "isAddedUser", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }, + { no: 7, name: "users", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ }, + { no: 8, name: "isAddedDevice", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }, + { no: 9, name: "devices", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ }, + { no: 10, name: "timeStamp", kind: "scalar", T: 1 /*ScalarType.DOUBLE*/ } + ]); + } + create(value?: PartialMessage): GroupChangeMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.action = ""; + message.subscribers = []; + message.isChangedDates = false; + message.isChangedClass = false; + message.isAddedUser = false; + message.users = []; + message.isAddedDevice = false; + message.devices = []; + message.timeStamp = 0; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: GroupChangeMessage): GroupChangeMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required GroupField group */ 1: + message.group = GroupField.internalBinaryRead(reader, reader.uint32(), options, message.group); + break; + case /* required string action */ 2: + message.action = reader.string(); + break; + case /* repeated string subscribers */ 3: + message.subscribers.push(reader.string()); + break; + case /* required bool isChangedDates */ 4: + message.isChangedDates = reader.bool(); + break; + case /* required bool isChangedClass */ 5: + message.isChangedClass = reader.bool(); + break; + case /* required bool isAddedUser */ 6: + message.isAddedUser = reader.bool(); + break; + case /* repeated string users */ 7: + message.users.push(reader.string()); + break; + case /* required bool isAddedDevice */ 8: + message.isAddedDevice = reader.bool(); + break; + case /* repeated string devices */ 9: + message.devices.push(reader.string()); + break; + case /* required double timeStamp */ 10: + message.timeStamp = reader.double(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: GroupChangeMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required GroupField group = 1; */ + if (message.group) + GroupField.internalBinaryWrite(message.group, writer.tag(1, WireType.LengthDelimited).fork(), options).join(); + /* required string action = 2; */ + if (message.action !== "") + writer.tag(2, WireType.LengthDelimited).string(message.action); + /* repeated string subscribers = 3; */ + for (let i = 0; i < message.subscribers.length; i++) + writer.tag(3, WireType.LengthDelimited).string(message.subscribers[i]); + /* required bool isChangedDates = 4; */ + if (message.isChangedDates !== false) + writer.tag(4, WireType.Varint).bool(message.isChangedDates); + /* required bool isChangedClass = 5; */ + if (message.isChangedClass !== false) + writer.tag(5, WireType.Varint).bool(message.isChangedClass); + /* required bool isAddedUser = 6; */ + if (message.isAddedUser !== false) + writer.tag(6, WireType.Varint).bool(message.isAddedUser); + /* repeated string users = 7; */ + for (let i = 0; i < message.users.length; i++) + writer.tag(7, WireType.LengthDelimited).string(message.users[i]); + /* required bool isAddedDevice = 8; */ + if (message.isAddedDevice !== false) + writer.tag(8, WireType.Varint).bool(message.isAddedDevice); + /* repeated string devices = 9; */ + for (let i = 0; i < message.devices.length; i++) + writer.tag(9, WireType.LengthDelimited).string(message.devices[i]); + /* required double timeStamp = 10; */ + if (message.timeStamp !== 0) + writer.tag(10, WireType.Bit64).double(message.timeStamp); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message GroupChangeMessage + */ +export const GroupChangeMessage = new GroupChangeMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class DeviceGroupChangeMessage$Type extends MessageType { + constructor() { + super("DeviceGroupChangeMessage", [ + { no: 1, name: "id", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "group", kind: "message", T: () => DeviceGroupMessage }, + { no: 3, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): DeviceGroupChangeMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.id = ""; + message.serial = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DeviceGroupChangeMessage): DeviceGroupChangeMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string id */ 1: + message.id = reader.string(); + break; + case /* required DeviceGroupMessage group */ 2: + message.group = DeviceGroupMessage.internalBinaryRead(reader, reader.uint32(), options, message.group); + break; + case /* required string serial */ 3: + message.serial = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: DeviceGroupChangeMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string id = 1; */ + if (message.id !== "") + writer.tag(1, WireType.LengthDelimited).string(message.id); + /* required DeviceGroupMessage group = 2; */ + if (message.group) + DeviceGroupMessage.internalBinaryWrite(message.group, writer.tag(2, WireType.LengthDelimited).fork(), options).join(); + /* required string serial = 3; */ + if (message.serial !== "") + writer.tag(3, WireType.LengthDelimited).string(message.serial); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message DeviceGroupChangeMessage + */ +export const DeviceGroupChangeMessage = new DeviceGroupChangeMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class GroupUserChangeMessage$Type extends MessageType { + constructor() { + super("GroupUserChangeMessage", [ + { no: 1, name: "users", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "isAdded", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }, + { no: 3, name: "id", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 4, name: "isDeletedLater", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }, + { no: 5, name: "devices", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): GroupUserChangeMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.users = []; + message.isAdded = false; + message.id = ""; + message.isDeletedLater = false; + message.devices = []; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: GroupUserChangeMessage): GroupUserChangeMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* repeated string users */ 1: + message.users.push(reader.string()); + break; + case /* required bool isAdded */ 2: + message.isAdded = reader.bool(); + break; + case /* required string id */ 3: + message.id = reader.string(); + break; + case /* required bool isDeletedLater */ 4: + message.isDeletedLater = reader.bool(); + break; + case /* repeated string devices */ 5: + message.devices.push(reader.string()); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: GroupUserChangeMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* repeated string users = 1; */ + for (let i = 0; i < message.users.length; i++) + writer.tag(1, WireType.LengthDelimited).string(message.users[i]); + /* required bool isAdded = 2; */ + if (message.isAdded !== false) + writer.tag(2, WireType.Varint).bool(message.isAdded); + /* required string id = 3; */ + if (message.id !== "") + writer.tag(3, WireType.LengthDelimited).string(message.id); + /* required bool isDeletedLater = 4; */ + if (message.isDeletedLater !== false) + writer.tag(4, WireType.Varint).bool(message.isDeletedLater); + /* repeated string devices = 5; */ + for (let i = 0; i < message.devices.length; i++) + writer.tag(5, WireType.LengthDelimited).string(message.devices[i]); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message GroupUserChangeMessage + */ +export const GroupUserChangeMessage = new GroupUserChangeMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class ConnectStartedMessage$Type extends MessageType { + constructor() { + super("ConnectStartedMessage", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "url", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): ConnectStartedMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + message.url = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: ConnectStartedMessage): ConnectStartedMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + case /* required string url */ 2: + message.url = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: ConnectStartedMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + /* required string url = 2; */ + if (message.url !== "") + writer.tag(2, WireType.LengthDelimited).string(message.url); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message ConnectStartedMessage + */ +export const ConnectStartedMessage = new ConnectStartedMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class InstallResultMessage$Type extends MessageType { + constructor() { + super("InstallResultMessage", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "result", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): InstallResultMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + message.result = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: InstallResultMessage): InstallResultMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + case /* required string result */ 2: + message.result = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: InstallResultMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + /* required string result = 2; */ + if (message.result !== "") + writer.tag(2, WireType.LengthDelimited).string(message.result); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message InstallResultMessage + */ +export const InstallResultMessage = new InstallResultMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class ConnectStoppedMessage$Type extends MessageType { + constructor() { + super("ConnectStoppedMessage", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): ConnectStoppedMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: ConnectStoppedMessage): ConnectStoppedMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: ConnectStoppedMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message ConnectStoppedMessage + */ +export const ConnectStoppedMessage = new ConnectStoppedMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class FileSystemListMessage$Type extends MessageType { + constructor() { + super("FileSystemListMessage", [ + { no: 1, name: "dir", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): FileSystemListMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.dir = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: FileSystemListMessage): FileSystemListMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string dir */ 1: + message.dir = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: FileSystemListMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string dir = 1; */ + if (message.dir !== "") + writer.tag(1, WireType.LengthDelimited).string(message.dir); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message FileSystemListMessage + */ +export const FileSystemListMessage = new FileSystemListMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class FileSystemGetMessage$Type extends MessageType { + constructor() { + super("FileSystemGetMessage", [ + { no: 1, name: "file", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "jwt", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): FileSystemGetMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.file = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: FileSystemGetMessage): FileSystemGetMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string file */ 1: + message.file = reader.string(); + break; + case /* optional string jwt */ 2: + message.jwt = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: FileSystemGetMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string file = 1; */ + if (message.file !== "") + writer.tag(1, WireType.LengthDelimited).string(message.file); + /* optional string jwt = 2; */ + if (message.jwt !== undefined) + writer.tag(2, WireType.LengthDelimited).string(message.jwt); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message FileSystemGetMessage + */ +export const FileSystemGetMessage = new FileSystemGetMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class TransactionProgressMessage$Type extends MessageType { + constructor() { + super("TransactionProgressMessage", [ + { no: 1, name: "source", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "seq", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }, + { no: 3, name: "data", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }, + { no: 4, name: "progress", kind: "scalar", opt: true, T: 13 /*ScalarType.UINT32*/ } + ]); + } + create(value?: PartialMessage): TransactionProgressMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.source = ""; + message.seq = 0; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: TransactionProgressMessage): TransactionProgressMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string source */ 1: + message.source = reader.string(); + break; + case /* required uint32 seq */ 2: + message.seq = reader.uint32(); + break; + case /* optional string data */ 3: + message.data = reader.string(); + break; + case /* optional uint32 progress = 4 [default = 0] */ 4: + message.progress = reader.uint32(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: TransactionProgressMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string source = 1; */ + if (message.source !== "") + writer.tag(1, WireType.LengthDelimited).string(message.source); + /* required uint32 seq = 2; */ + if (message.seq !== 0) + writer.tag(2, WireType.Varint).uint32(message.seq); + /* optional string data = 3; */ + if (message.data !== undefined) + writer.tag(3, WireType.LengthDelimited).string(message.data); + /* optional uint32 progress = 4 [default = 0]; */ + if (message.progress !== undefined) + writer.tag(4, WireType.Varint).uint32(message.progress); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message TransactionProgressMessage + */ +export const TransactionProgressMessage = new TransactionProgressMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class TransactionDoneMessage$Type extends MessageType { + constructor() { + super("TransactionDoneMessage", [ + { no: 1, name: "source", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "seq", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }, + { no: 3, name: "success", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }, + { no: 4, name: "data", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }, + { no: 5, name: "body", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): TransactionDoneMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.source = ""; + message.seq = 0; + message.success = false; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: TransactionDoneMessage): TransactionDoneMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string source */ 1: + message.source = reader.string(); + break; + case /* required uint32 seq */ 2: + message.seq = reader.uint32(); + break; + case /* required bool success */ 3: + message.success = reader.bool(); + break; + case /* optional string data */ 4: + message.data = reader.string(); + break; + case /* optional string body */ 5: + message.body = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: TransactionDoneMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string source = 1; */ + if (message.source !== "") + writer.tag(1, WireType.LengthDelimited).string(message.source); + /* required uint32 seq = 2; */ + if (message.seq !== 0) + writer.tag(2, WireType.Varint).uint32(message.seq); + /* required bool success = 3; */ + if (message.success !== false) + writer.tag(3, WireType.Varint).bool(message.success); + /* optional string data = 4; */ + if (message.data !== undefined) + writer.tag(4, WireType.LengthDelimited).string(message.data); + /* optional string body = 5; */ + if (message.body !== undefined) + writer.tag(5, WireType.LengthDelimited).string(message.body); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message TransactionDoneMessage + */ +export const TransactionDoneMessage = new TransactionDoneMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class TransactionTreeMessage$Type extends MessageType { + constructor() { + super("TransactionTreeMessage", [ + { no: 1, name: "source", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "seq", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }, + { no: 3, name: "success", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }, + { no: 4, name: "data", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }, + { no: 5, name: "body", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): TransactionTreeMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.source = ""; + message.seq = 0; + message.success = false; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: TransactionTreeMessage): TransactionTreeMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string source */ 1: + message.source = reader.string(); + break; + case /* required uint32 seq */ 2: + message.seq = reader.uint32(); + break; + case /* required bool success */ 3: + message.success = reader.bool(); + break; + case /* optional string data */ 4: + message.data = reader.string(); + break; + case /* optional string body */ 5: + message.body = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: TransactionTreeMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string source = 1; */ + if (message.source !== "") + writer.tag(1, WireType.LengthDelimited).string(message.source); + /* required uint32 seq = 2; */ + if (message.seq !== 0) + writer.tag(2, WireType.Varint).uint32(message.seq); + /* required bool success = 3; */ + if (message.success !== false) + writer.tag(3, WireType.Varint).bool(message.success); + /* optional string data = 4; */ + if (message.data !== undefined) + writer.tag(4, WireType.LengthDelimited).string(message.data); + /* optional string body = 5; */ + if (message.body !== undefined) + writer.tag(5, WireType.LengthDelimited).string(message.body); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message TransactionTreeMessage + */ +export const TransactionTreeMessage = new TransactionTreeMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class DeviceLogMessage$Type extends MessageType { + constructor() { + super("DeviceLogMessage", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "timestamp", kind: "scalar", T: 1 /*ScalarType.DOUBLE*/ }, + { no: 3, name: "priority", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }, + { no: 4, name: "tag", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 5, name: "pid", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }, + { no: 6, name: "message", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 7, name: "identifier", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): DeviceLogMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + message.timestamp = 0; + message.priority = 0; + message.tag = ""; + message.pid = 0; + message.message = ""; + message.identifier = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DeviceLogMessage): DeviceLogMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + case /* required double timestamp */ 2: + message.timestamp = reader.double(); + break; + case /* required uint32 priority */ 3: + message.priority = reader.uint32(); + break; + case /* required string tag */ 4: + message.tag = reader.string(); + break; + case /* required uint32 pid */ 5: + message.pid = reader.uint32(); + break; + case /* required string message */ 6: + message.message = reader.string(); + break; + case /* required string identifier */ 7: + message.identifier = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: DeviceLogMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + /* required double timestamp = 2; */ + if (message.timestamp !== 0) + writer.tag(2, WireType.Bit64).double(message.timestamp); + /* required uint32 priority = 3; */ + if (message.priority !== 0) + writer.tag(3, WireType.Varint).uint32(message.priority); + /* required string tag = 4; */ + if (message.tag !== "") + writer.tag(4, WireType.LengthDelimited).string(message.tag); + /* required uint32 pid = 5; */ + if (message.pid !== 0) + writer.tag(5, WireType.Varint).uint32(message.pid); + /* required string message = 6; */ + if (message.message !== "") + writer.tag(6, WireType.LengthDelimited).string(message.message); + /* required string identifier = 7; */ + if (message.identifier !== "") + writer.tag(7, WireType.LengthDelimited).string(message.identifier); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message DeviceLogMessage + */ +export const DeviceLogMessage = new DeviceLogMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class DeviceGroupOwnerMessage$Type extends MessageType { + constructor() { + super("DeviceGroupOwnerMessage", [ + { no: 1, name: "email", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "name", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): DeviceGroupOwnerMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.email = ""; + message.name = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DeviceGroupOwnerMessage): DeviceGroupOwnerMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string email */ 1: + message.email = reader.string(); + break; + case /* required string name */ 2: + message.name = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: DeviceGroupOwnerMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string email = 1; */ + if (message.email !== "") + writer.tag(1, WireType.LengthDelimited).string(message.email); + /* required string name = 2; */ + if (message.name !== "") + writer.tag(2, WireType.LengthDelimited).string(message.name); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message DeviceGroupOwnerMessage + */ +export const DeviceGroupOwnerMessage = new DeviceGroupOwnerMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class DeviceGroupLifetimeMessage$Type extends MessageType { + constructor() { + super("DeviceGroupLifetimeMessage", [ + { no: 1, name: "start", kind: "scalar", T: 1 /*ScalarType.DOUBLE*/ }, + { no: 2, name: "stop", kind: "scalar", T: 1 /*ScalarType.DOUBLE*/ } + ]); + } + create(value?: PartialMessage): DeviceGroupLifetimeMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.start = 0; + message.stop = 0; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DeviceGroupLifetimeMessage): DeviceGroupLifetimeMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required double start */ 1: + message.start = reader.double(); + break; + case /* required double stop */ 2: + message.stop = reader.double(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: DeviceGroupLifetimeMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required double start = 1; */ + if (message.start !== 0) + writer.tag(1, WireType.Bit64).double(message.start); + /* required double stop = 2; */ + if (message.stop !== 0) + writer.tag(2, WireType.Bit64).double(message.stop); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message DeviceGroupLifetimeMessage + */ +export const DeviceGroupLifetimeMessage = new DeviceGroupLifetimeMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class DeviceGroupMessage$Type extends MessageType { + constructor() { + super("DeviceGroupMessage", [ + { no: 1, name: "id", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "name", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 3, name: "owner", kind: "message", T: () => DeviceGroupOwnerMessage }, + { no: 4, name: "lifeTime", kind: "message", T: () => DeviceGroupLifetimeMessage }, + { no: 5, name: "class", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 6, name: "repetitions", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }, + { no: 7, name: "originName", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): DeviceGroupMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.id = ""; + message.name = ""; + message.class = ""; + message.repetitions = 0; + message.originName = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DeviceGroupMessage): DeviceGroupMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string id */ 1: + message.id = reader.string(); + break; + case /* required string name */ 2: + message.name = reader.string(); + break; + case /* required DeviceGroupOwnerMessage owner */ 3: + message.owner = DeviceGroupOwnerMessage.internalBinaryRead(reader, reader.uint32(), options, message.owner); + break; + case /* required DeviceGroupLifetimeMessage lifeTime */ 4: + message.lifeTime = DeviceGroupLifetimeMessage.internalBinaryRead(reader, reader.uint32(), options, message.lifeTime); + break; + case /* required string class */ 5: + message.class = reader.string(); + break; + case /* required uint32 repetitions */ 6: + message.repetitions = reader.uint32(); + break; + case /* required string originName */ 7: + message.originName = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: DeviceGroupMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string id = 1; */ + if (message.id !== "") + writer.tag(1, WireType.LengthDelimited).string(message.id); + /* required string name = 2; */ + if (message.name !== "") + writer.tag(2, WireType.LengthDelimited).string(message.name); + /* required DeviceGroupOwnerMessage owner = 3; */ + if (message.owner) + DeviceGroupOwnerMessage.internalBinaryWrite(message.owner, writer.tag(3, WireType.LengthDelimited).fork(), options).join(); + /* required DeviceGroupLifetimeMessage lifeTime = 4; */ + if (message.lifeTime) + DeviceGroupLifetimeMessage.internalBinaryWrite(message.lifeTime, writer.tag(4, WireType.LengthDelimited).fork(), options).join(); + /* required string class = 5; */ + if (message.class !== "") + writer.tag(5, WireType.LengthDelimited).string(message.class); + /* required uint32 repetitions = 6; */ + if (message.repetitions !== 0) + writer.tag(6, WireType.Varint).uint32(message.repetitions); + /* required string originName = 7; */ + if (message.originName !== "") + writer.tag(7, WireType.LengthDelimited).string(message.originName); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message DeviceGroupMessage + */ +export const DeviceGroupMessage = new DeviceGroupMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class ProviderMessage$Type extends MessageType { + constructor() { + super("ProviderMessage", [ + { no: 1, name: "channel", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "name", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): ProviderMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.channel = ""; + message.name = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: ProviderMessage): ProviderMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string channel */ 1: + message.channel = reader.string(); + break; + case /* required string name */ 2: + message.name = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: ProviderMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string channel = 1; */ + if (message.channel !== "") + writer.tag(1, WireType.LengthDelimited).string(message.channel); + /* required string name = 2; */ + if (message.name !== "") + writer.tag(2, WireType.LengthDelimited).string(message.name); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message ProviderMessage + */ +export const ProviderMessage = new ProviderMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class ProviderIosMessage$Type extends MessageType { + constructor() { + super("ProviderIosMessage", [ + { no: 1, name: "channel", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "name", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 3, name: "screenWsUrlPattern", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): ProviderIosMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.channel = ""; + message.name = ""; + message.screenWsUrlPattern = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: ProviderIosMessage): ProviderIosMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string channel */ 1: + message.channel = reader.string(); + break; + case /* required string name */ 2: + message.name = reader.string(); + break; + case /* required string screenWsUrlPattern */ 3: + message.screenWsUrlPattern = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: ProviderIosMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string channel = 1; */ + if (message.channel !== "") + writer.tag(1, WireType.LengthDelimited).string(message.channel); + /* required string name = 2; */ + if (message.name !== "") + writer.tag(2, WireType.LengthDelimited).string(message.name); + /* required string screenWsUrlPattern = 3; */ + if (message.screenWsUrlPattern !== "") + writer.tag(3, WireType.LengthDelimited).string(message.screenWsUrlPattern); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message ProviderIosMessage + */ +export const ProviderIosMessage = new ProviderIosMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class DeviceHeartbeatMessage$Type extends MessageType { + constructor() { + super("DeviceHeartbeatMessage", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): DeviceHeartbeatMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DeviceHeartbeatMessage): DeviceHeartbeatMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: DeviceHeartbeatMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message DeviceHeartbeatMessage + */ +export const DeviceHeartbeatMessage = new DeviceHeartbeatMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class DeviceIntroductionMessage$Type extends MessageType { + constructor() { + super("DeviceIntroductionMessage", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "status", kind: "enum", T: () => ["DeviceStatus", DeviceStatus] }, + { no: 3, name: "provider", kind: "message", T: () => ProviderMessage } + ]); + } + create(value?: PartialMessage): DeviceIntroductionMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + message.status = 0; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DeviceIntroductionMessage): DeviceIntroductionMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + case /* required DeviceStatus status */ 2: + message.status = reader.int32(); + break; + case /* required ProviderMessage provider */ 3: + message.provider = ProviderMessage.internalBinaryRead(reader, reader.uint32(), options, message.provider); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: DeviceIntroductionMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + /* required DeviceStatus status = 2; */ + if (message.status !== 0) + writer.tag(2, WireType.Varint).int32(message.status); + /* required ProviderMessage provider = 3; */ + if (message.provider) + ProviderMessage.internalBinaryWrite(message.provider, writer.tag(3, WireType.LengthDelimited).fork(), options).join(); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message DeviceIntroductionMessage + */ +export const DeviceIntroductionMessage = new DeviceIntroductionMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class DeviceIosIntroductionMessage$Type extends MessageType { + constructor() { + super("DeviceIosIntroductionMessage", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "status", kind: "enum", T: () => ["DeviceStatus", DeviceStatus] }, + { no: 3, name: "provider", kind: "message", T: () => ProviderMessage } + ]); + } + create(value?: PartialMessage): DeviceIosIntroductionMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + message.status = 0; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DeviceIosIntroductionMessage): DeviceIosIntroductionMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + case /* required DeviceStatus status */ 2: + message.status = reader.int32(); + break; + case /* required ProviderMessage provider */ 3: + message.provider = ProviderMessage.internalBinaryRead(reader, reader.uint32(), options, message.provider); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: DeviceIosIntroductionMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + /* required DeviceStatus status = 2; */ + if (message.status !== 0) + writer.tag(2, WireType.Varint).int32(message.status); + /* required ProviderMessage provider = 3; */ + if (message.provider) + ProviderMessage.internalBinaryWrite(message.provider, writer.tag(3, WireType.LengthDelimited).fork(), options).join(); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message DeviceIosIntroductionMessage + */ +export const DeviceIosIntroductionMessage = new DeviceIosIntroductionMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class InitializeIosDeviceState$Type extends MessageType { + constructor() { + super("InitializeIosDeviceState", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "status", kind: "enum", T: () => ["DeviceStatus", DeviceStatus] }, + { no: 3, name: "provider", kind: "message", T: () => ProviderIosMessage }, + { no: 4, name: "ports", kind: "message", T: () => IosDevicePorts }, + { no: 5, name: "options", kind: "message", T: () => UpdateIosDevice } + ]); + } + create(value?: PartialMessage): InitializeIosDeviceState { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + message.status = 0; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: InitializeIosDeviceState): InitializeIosDeviceState { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + case /* required DeviceStatus status */ 2: + message.status = reader.int32(); + break; + case /* required ProviderIosMessage provider */ 3: + message.provider = ProviderIosMessage.internalBinaryRead(reader, reader.uint32(), options, message.provider); + break; + case /* required IosDevicePorts ports */ 4: + message.ports = IosDevicePorts.internalBinaryRead(reader, reader.uint32(), options, message.ports); + break; + case /* required UpdateIosDevice options */ 5: + message.options = UpdateIosDevice.internalBinaryRead(reader, reader.uint32(), options, message.options); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: InitializeIosDeviceState, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + /* required DeviceStatus status = 2; */ + if (message.status !== 0) + writer.tag(2, WireType.Varint).int32(message.status); + /* required ProviderIosMessage provider = 3; */ + if (message.provider) + ProviderIosMessage.internalBinaryWrite(message.provider, writer.tag(3, WireType.LengthDelimited).fork(), options).join(); + /* required IosDevicePorts ports = 4; */ + if (message.ports) + IosDevicePorts.internalBinaryWrite(message.ports, writer.tag(4, WireType.LengthDelimited).fork(), options).join(); + /* required UpdateIosDevice options = 5; */ + if (message.options) + UpdateIosDevice.internalBinaryWrite(message.options, writer.tag(5, WireType.LengthDelimited).fork(), options).join(); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message InitializeIosDeviceState + */ +export const InitializeIosDeviceState = new InitializeIosDeviceState$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class DeviceRegisteredMessage$Type extends MessageType { + constructor() { + super("DeviceRegisteredMessage", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): DeviceRegisteredMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DeviceRegisteredMessage): DeviceRegisteredMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: DeviceRegisteredMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message DeviceRegisteredMessage + */ +export const DeviceRegisteredMessage = new DeviceRegisteredMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class DevicePresentMessage$Type extends MessageType { + constructor() { + super("DevicePresentMessage", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): DevicePresentMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DevicePresentMessage): DevicePresentMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: DevicePresentMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message DevicePresentMessage + */ +export const DevicePresentMessage = new DevicePresentMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class DeviceAbsentMessage$Type extends MessageType { + constructor() { + super("DeviceAbsentMessage", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): DeviceAbsentMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DeviceAbsentMessage): DeviceAbsentMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: DeviceAbsentMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message DeviceAbsentMessage + */ +export const DeviceAbsentMessage = new DeviceAbsentMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class DeviceReadyMessage$Type extends MessageType { + constructor() { + super("DeviceReadyMessage", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "channel", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): DeviceReadyMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + message.channel = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DeviceReadyMessage): DeviceReadyMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + case /* required string channel */ 2: + message.channel = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: DeviceReadyMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + /* required string channel = 2; */ + if (message.channel !== "") + writer.tag(2, WireType.LengthDelimited).string(message.channel); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message DeviceReadyMessage + */ +export const DeviceReadyMessage = new DeviceReadyMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class ProbeMessage$Type extends MessageType { + constructor() { + super("ProbeMessage", []); + } + create(value?: PartialMessage): ProbeMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: ProbeMessage): ProbeMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: ProbeMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message ProbeMessage + */ +export const ProbeMessage = new ProbeMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class DeviceStatusMessage$Type extends MessageType { + constructor() { + super("DeviceStatusMessage", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "status", kind: "enum", T: () => ["DeviceStatus", DeviceStatus] } + ]); + } + create(value?: PartialMessage): DeviceStatusMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + message.status = 0; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DeviceStatusMessage): DeviceStatusMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + case /* required DeviceStatus status */ 2: + message.status = reader.int32(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: DeviceStatusMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + /* required DeviceStatus status = 2; */ + if (message.status !== 0) + writer.tag(2, WireType.Varint).int32(message.status); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message DeviceStatusMessage + */ +export const DeviceStatusMessage = new DeviceStatusMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class DeviceTypeMessage$Type extends MessageType { + constructor() { + super("DeviceTypeMessage", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "type", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): DeviceTypeMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + message.type = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DeviceTypeMessage): DeviceTypeMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + case /* required string type */ 2: + message.type = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: DeviceTypeMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + /* required string type = 2; */ + if (message.type !== "") + writer.tag(2, WireType.LengthDelimited).string(message.type); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message DeviceTypeMessage + */ +export const DeviceTypeMessage = new DeviceTypeMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class DeviceDisplayMessage$Type extends MessageType { + constructor() { + super("DeviceDisplayMessage", [ + { no: 1, name: "id", kind: "scalar", T: 5 /*ScalarType.INT32*/ }, + { no: 2, name: "width", kind: "scalar", T: 5 /*ScalarType.INT32*/ }, + { no: 3, name: "height", kind: "scalar", T: 5 /*ScalarType.INT32*/ }, + { no: 4, name: "rotation", kind: "scalar", T: 5 /*ScalarType.INT32*/ }, + { no: 5, name: "xdpi", kind: "scalar", T: 2 /*ScalarType.FLOAT*/ }, + { no: 6, name: "ydpi", kind: "scalar", T: 2 /*ScalarType.FLOAT*/ }, + { no: 7, name: "fps", kind: "scalar", T: 2 /*ScalarType.FLOAT*/ }, + { no: 8, name: "density", kind: "scalar", T: 2 /*ScalarType.FLOAT*/ }, + { no: 9, name: "secure", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }, + { no: 10, name: "url", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 11, name: "size", kind: "scalar", opt: true, T: 2 /*ScalarType.FLOAT*/ } + ]); + } + create(value?: PartialMessage): DeviceDisplayMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.id = 0; + message.width = 0; + message.height = 0; + message.rotation = 0; + message.xdpi = 0; + message.ydpi = 0; + message.fps = 0; + message.density = 0; + message.secure = false; + message.url = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DeviceDisplayMessage): DeviceDisplayMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required int32 id */ 1: + message.id = reader.int32(); + break; + case /* required int32 width */ 2: + message.width = reader.int32(); + break; + case /* required int32 height */ 3: + message.height = reader.int32(); + break; + case /* required int32 rotation */ 4: + message.rotation = reader.int32(); + break; + case /* required float xdpi */ 5: + message.xdpi = reader.float(); + break; + case /* required float ydpi */ 6: + message.ydpi = reader.float(); + break; + case /* required float fps */ 7: + message.fps = reader.float(); + break; + case /* required float density */ 8: + message.density = reader.float(); + break; + case /* required bool secure */ 9: + message.secure = reader.bool(); + break; + case /* required string url */ 10: + message.url = reader.string(); + break; + case /* optional float size */ 11: + message.size = reader.float(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: DeviceDisplayMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required int32 id = 1; */ + if (message.id !== 0) + writer.tag(1, WireType.Varint).int32(message.id); + /* required int32 width = 2; */ + if (message.width !== 0) + writer.tag(2, WireType.Varint).int32(message.width); + /* required int32 height = 3; */ + if (message.height !== 0) + writer.tag(3, WireType.Varint).int32(message.height); + /* required int32 rotation = 4; */ + if (message.rotation !== 0) + writer.tag(4, WireType.Varint).int32(message.rotation); + /* required float xdpi = 5; */ + if (message.xdpi !== 0) + writer.tag(5, WireType.Bit32).float(message.xdpi); + /* required float ydpi = 6; */ + if (message.ydpi !== 0) + writer.tag(6, WireType.Bit32).float(message.ydpi); + /* required float fps = 7; */ + if (message.fps !== 0) + writer.tag(7, WireType.Bit32).float(message.fps); + /* required float density = 8; */ + if (message.density !== 0) + writer.tag(8, WireType.Bit32).float(message.density); + /* required bool secure = 9; */ + if (message.secure !== false) + writer.tag(9, WireType.Varint).bool(message.secure); + /* required string url = 10; */ + if (message.url !== "") + writer.tag(10, WireType.LengthDelimited).string(message.url); + /* optional float size = 11; */ + if (message.size !== undefined) + writer.tag(11, WireType.Bit32).float(message.size); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message DeviceDisplayMessage + */ +export const DeviceDisplayMessage = new DeviceDisplayMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class DeviceBrowserAppMessage$Type extends MessageType { + constructor() { + super("DeviceBrowserAppMessage", [ + { no: 1, name: "id", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "type", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 3, name: "name", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 4, name: "selected", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }, + { no: 5, name: "system", kind: "scalar", T: 8 /*ScalarType.BOOL*/ } + ]); + } + create(value?: PartialMessage): DeviceBrowserAppMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.id = ""; + message.type = ""; + message.name = ""; + message.selected = false; + message.system = false; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DeviceBrowserAppMessage): DeviceBrowserAppMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string id */ 1: + message.id = reader.string(); + break; + case /* required string type */ 2: + message.type = reader.string(); + break; + case /* required string name */ 3: + message.name = reader.string(); + break; + case /* required bool selected */ 4: + message.selected = reader.bool(); + break; + case /* required bool system */ 5: + message.system = reader.bool(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: DeviceBrowserAppMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string id = 1; */ + if (message.id !== "") + writer.tag(1, WireType.LengthDelimited).string(message.id); + /* required string type = 2; */ + if (message.type !== "") + writer.tag(2, WireType.LengthDelimited).string(message.type); + /* required string name = 3; */ + if (message.name !== "") + writer.tag(3, WireType.LengthDelimited).string(message.name); + /* required bool selected = 4; */ + if (message.selected !== false) + writer.tag(4, WireType.Varint).bool(message.selected); + /* required bool system = 5; */ + if (message.system !== false) + writer.tag(5, WireType.Varint).bool(message.system); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message DeviceBrowserAppMessage + */ +export const DeviceBrowserAppMessage = new DeviceBrowserAppMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class DeviceBrowserMessage$Type extends MessageType { + constructor() { + super("DeviceBrowserMessage", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "selected", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }, + { no: 3, name: "apps", kind: "message", repeat: 2 /*RepeatType.UNPACKED*/, T: () => DeviceBrowserAppMessage } + ]); + } + create(value?: PartialMessage): DeviceBrowserMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + message.selected = false; + message.apps = []; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DeviceBrowserMessage): DeviceBrowserMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + case /* required bool selected */ 2: + message.selected = reader.bool(); + break; + case /* repeated DeviceBrowserAppMessage apps */ 3: + message.apps.push(DeviceBrowserAppMessage.internalBinaryRead(reader, reader.uint32(), options)); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: DeviceBrowserMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + /* required bool selected = 2; */ + if (message.selected !== false) + writer.tag(2, WireType.Varint).bool(message.selected); + /* repeated DeviceBrowserAppMessage apps = 3; */ + for (let i = 0; i < message.apps.length; i++) + DeviceBrowserAppMessage.internalBinaryWrite(message.apps[i], writer.tag(3, WireType.LengthDelimited).fork(), options).join(); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message DeviceBrowserMessage + */ +export const DeviceBrowserMessage = new DeviceBrowserMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class GetServicesAvailabilityMessage$Type extends MessageType { + constructor() { + super("GetServicesAvailabilityMessage", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "hasGMS", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }, + { no: 3, name: "hasHMS", kind: "scalar", T: 8 /*ScalarType.BOOL*/ } + ]); + } + create(value?: PartialMessage): GetServicesAvailabilityMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + message.hasGMS = false; + message.hasHMS = false; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: GetServicesAvailabilityMessage): GetServicesAvailabilityMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + case /* required bool hasGMS */ 2: + message.hasGMS = reader.bool(); + break; + case /* required bool hasHMS */ 3: + message.hasHMS = reader.bool(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: GetServicesAvailabilityMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + /* required bool hasGMS = 2; */ + if (message.hasGMS !== false) + writer.tag(2, WireType.Varint).bool(message.hasGMS); + /* required bool hasHMS = 3; */ + if (message.hasHMS !== false) + writer.tag(3, WireType.Varint).bool(message.hasHMS); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message GetServicesAvailabilityMessage + */ +export const GetServicesAvailabilityMessage = new GetServicesAvailabilityMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class DevicePhoneMessage$Type extends MessageType { + constructor() { + super("DevicePhoneMessage", [ + { no: 1, name: "imei", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }, + { no: 5, name: "imsi", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "phoneNumber", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }, + { no: 3, name: "iccid", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }, + { no: 4, name: "network", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): DevicePhoneMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DevicePhoneMessage): DevicePhoneMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* optional string imei */ 1: + message.imei = reader.string(); + break; + case /* optional string imsi */ 5: + message.imsi = reader.string(); + break; + case /* optional string phoneNumber */ 2: + message.phoneNumber = reader.string(); + break; + case /* optional string iccid */ 3: + message.iccid = reader.string(); + break; + case /* optional string network */ 4: + message.network = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: DevicePhoneMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* optional string imei = 1; */ + if (message.imei !== undefined) + writer.tag(1, WireType.LengthDelimited).string(message.imei); + /* optional string phoneNumber = 2; */ + if (message.phoneNumber !== undefined) + writer.tag(2, WireType.LengthDelimited).string(message.phoneNumber); + /* optional string iccid = 3; */ + if (message.iccid !== undefined) + writer.tag(3, WireType.LengthDelimited).string(message.iccid); + /* optional string network = 4; */ + if (message.network !== undefined) + writer.tag(4, WireType.LengthDelimited).string(message.network); + /* optional string imsi = 5; */ + if (message.imsi !== undefined) + writer.tag(5, WireType.LengthDelimited).string(message.imsi); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message DevicePhoneMessage + */ +export const DevicePhoneMessage = new DevicePhoneMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class DeviceIdentityMessage$Type extends MessageType { + constructor() { + super("DeviceIdentityMessage", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "platform", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 3, name: "manufacturer", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 4, name: "operator", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }, + { no: 5, name: "model", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 6, name: "version", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 7, name: "abi", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 8, name: "sdk", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 9, name: "display", kind: "message", T: () => DeviceDisplayMessage }, + { no: 11, name: "phone", kind: "message", T: () => DevicePhoneMessage }, + { no: 12, name: "product", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }, + { no: 13, name: "cpuPlatform", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }, + { no: 14, name: "openGLESVersion", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }, + { no: 15, name: "marketName", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }, + { no: 16, name: "macAddress", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }, + { no: 17, name: "ram", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): DeviceIdentityMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + message.platform = ""; + message.manufacturer = ""; + message.model = ""; + message.version = ""; + message.abi = ""; + message.sdk = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DeviceIdentityMessage): DeviceIdentityMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + case /* required string platform */ 2: + message.platform = reader.string(); + break; + case /* required string manufacturer */ 3: + message.manufacturer = reader.string(); + break; + case /* optional string operator */ 4: + message.operator = reader.string(); + break; + case /* required string model */ 5: + message.model = reader.string(); + break; + case /* required string version */ 6: + message.version = reader.string(); + break; + case /* required string abi */ 7: + message.abi = reader.string(); + break; + case /* required string sdk */ 8: + message.sdk = reader.string(); + break; + case /* required DeviceDisplayMessage display */ 9: + message.display = DeviceDisplayMessage.internalBinaryRead(reader, reader.uint32(), options, message.display); + break; + case /* required DevicePhoneMessage phone */ 11: + message.phone = DevicePhoneMessage.internalBinaryRead(reader, reader.uint32(), options, message.phone); + break; + case /* optional string product */ 12: + message.product = reader.string(); + break; + case /* optional string cpuPlatform */ 13: + message.cpuPlatform = reader.string(); + break; + case /* optional string openGLESVersion */ 14: + message.openGLESVersion = reader.string(); + break; + case /* optional string marketName */ 15: + message.marketName = reader.string(); + break; + case /* optional string macAddress */ 16: + message.macAddress = reader.string(); + break; + case /* optional string ram */ 17: + message.ram = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: DeviceIdentityMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + /* required string platform = 2; */ + if (message.platform !== "") + writer.tag(2, WireType.LengthDelimited).string(message.platform); + /* required string manufacturer = 3; */ + if (message.manufacturer !== "") + writer.tag(3, WireType.LengthDelimited).string(message.manufacturer); + /* optional string operator = 4; */ + if (message.operator !== undefined) + writer.tag(4, WireType.LengthDelimited).string(message.operator); + /* required string model = 5; */ + if (message.model !== "") + writer.tag(5, WireType.LengthDelimited).string(message.model); + /* required string version = 6; */ + if (message.version !== "") + writer.tag(6, WireType.LengthDelimited).string(message.version); + /* required string abi = 7; */ + if (message.abi !== "") + writer.tag(7, WireType.LengthDelimited).string(message.abi); + /* required string sdk = 8; */ + if (message.sdk !== "") + writer.tag(8, WireType.LengthDelimited).string(message.sdk); + /* required DeviceDisplayMessage display = 9; */ + if (message.display) + DeviceDisplayMessage.internalBinaryWrite(message.display, writer.tag(9, WireType.LengthDelimited).fork(), options).join(); + /* required DevicePhoneMessage phone = 11; */ + if (message.phone) + DevicePhoneMessage.internalBinaryWrite(message.phone, writer.tag(11, WireType.LengthDelimited).fork(), options).join(); + /* optional string product = 12; */ + if (message.product !== undefined) + writer.tag(12, WireType.LengthDelimited).string(message.product); + /* optional string cpuPlatform = 13; */ + if (message.cpuPlatform !== undefined) + writer.tag(13, WireType.LengthDelimited).string(message.cpuPlatform); + /* optional string openGLESVersion = 14; */ + if (message.openGLESVersion !== undefined) + writer.tag(14, WireType.LengthDelimited).string(message.openGLESVersion); + /* optional string marketName = 15; */ + if (message.marketName !== undefined) + writer.tag(15, WireType.LengthDelimited).string(message.marketName); + /* optional string macAddress = 16; */ + if (message.macAddress !== undefined) + writer.tag(16, WireType.LengthDelimited).string(message.macAddress); + /* optional string ram = 17; */ + if (message.ram !== undefined) + writer.tag(17, WireType.LengthDelimited).string(message.ram); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message DeviceIdentityMessage + */ +export const DeviceIdentityMessage = new DeviceIdentityMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class DeviceProperty$Type extends MessageType { + constructor() { + super("DeviceProperty", [ + { no: 1, name: "name", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "value", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): DeviceProperty { + const message = globalThis.Object.create((this.messagePrototype!)); + message.name = ""; + message.value = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DeviceProperty): DeviceProperty { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string name */ 1: + message.name = reader.string(); + break; + case /* required string value */ 2: + message.value = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: DeviceProperty, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string name = 1; */ + if (message.name !== "") + writer.tag(1, WireType.LengthDelimited).string(message.name); + /* required string value = 2; */ + if (message.value !== "") + writer.tag(2, WireType.LengthDelimited).string(message.value); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message DeviceProperty + */ +export const DeviceProperty = new DeviceProperty$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class DevicePropertiesMessage$Type extends MessageType { + constructor() { + super("DevicePropertiesMessage", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "properties", kind: "message", repeat: 2 /*RepeatType.UNPACKED*/, T: () => DeviceProperty } + ]); + } + create(value?: PartialMessage): DevicePropertiesMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + message.properties = []; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DevicePropertiesMessage): DevicePropertiesMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + case /* repeated DeviceProperty properties */ 2: + message.properties.push(DeviceProperty.internalBinaryRead(reader, reader.uint32(), options)); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: DevicePropertiesMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + /* repeated DeviceProperty properties = 2; */ + for (let i = 0; i < message.properties.length; i++) + DeviceProperty.internalBinaryWrite(message.properties[i], writer.tag(2, WireType.LengthDelimited).fork(), options).join(); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message DevicePropertiesMessage + */ +export const DevicePropertiesMessage = new DevicePropertiesMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class DeviceRequirement$Type extends MessageType { + constructor() { + super("DeviceRequirement", [ + { no: 1, name: "name", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "value", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 3, name: "type", kind: "enum", T: () => ["RequirementType", RequirementType] } + ]); + } + create(value?: PartialMessage): DeviceRequirement { + const message = globalThis.Object.create((this.messagePrototype!)); + message.name = ""; + message.value = ""; + message.type = 0; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DeviceRequirement): DeviceRequirement { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string name */ 1: + message.name = reader.string(); + break; + case /* required string value */ 2: + message.value = reader.string(); + break; + case /* required RequirementType type */ 3: + message.type = reader.int32(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: DeviceRequirement, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string name = 1; */ + if (message.name !== "") + writer.tag(1, WireType.LengthDelimited).string(message.name); + /* required string value = 2; */ + if (message.value !== "") + writer.tag(2, WireType.LengthDelimited).string(message.value); + /* required RequirementType type = 3; */ + if (message.type !== 0) + writer.tag(3, WireType.Varint).int32(message.type); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message DeviceRequirement + */ +export const DeviceRequirement = new DeviceRequirement$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class OwnerMessage$Type extends MessageType { + constructor() { + super("OwnerMessage", [ + { no: 1, name: "email", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "name", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 3, name: "group", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): OwnerMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.email = ""; + message.name = ""; + message.group = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: OwnerMessage): OwnerMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string email */ 1: + message.email = reader.string(); + break; + case /* required string name */ 2: + message.name = reader.string(); + break; + case /* required string group */ 3: + message.group = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: OwnerMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string email = 1; */ + if (message.email !== "") + writer.tag(1, WireType.LengthDelimited).string(message.email); + /* required string name = 2; */ + if (message.name !== "") + writer.tag(2, WireType.LengthDelimited).string(message.name); + /* required string group = 3; */ + if (message.group !== "") + writer.tag(3, WireType.LengthDelimited).string(message.group); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message OwnerMessage + */ +export const OwnerMessage = new OwnerMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class GroupMessage$Type extends MessageType { + constructor() { + super("GroupMessage", [ + { no: 1, name: "owner", kind: "message", T: () => OwnerMessage }, + { no: 2, name: "timeout", kind: "scalar", opt: true, T: 13 /*ScalarType.UINT32*/ }, + { no: 3, name: "requirements", kind: "message", repeat: 2 /*RepeatType.UNPACKED*/, T: () => DeviceRequirement }, + { no: 4, name: "usage", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): GroupMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.requirements = []; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: GroupMessage): GroupMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required OwnerMessage owner */ 1: + message.owner = OwnerMessage.internalBinaryRead(reader, reader.uint32(), options, message.owner); + break; + case /* optional uint32 timeout */ 2: + message.timeout = reader.uint32(); + break; + case /* repeated DeviceRequirement requirements */ 3: + message.requirements.push(DeviceRequirement.internalBinaryRead(reader, reader.uint32(), options)); + break; + case /* optional string usage */ 4: + message.usage = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: GroupMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required OwnerMessage owner = 1; */ + if (message.owner) + OwnerMessage.internalBinaryWrite(message.owner, writer.tag(1, WireType.LengthDelimited).fork(), options).join(); + /* optional uint32 timeout = 2; */ + if (message.timeout !== undefined) + writer.tag(2, WireType.Varint).uint32(message.timeout); + /* repeated DeviceRequirement requirements = 3; */ + for (let i = 0; i < message.requirements.length; i++) + DeviceRequirement.internalBinaryWrite(message.requirements[i], writer.tag(3, WireType.LengthDelimited).fork(), options).join(); + /* optional string usage = 4; */ + if (message.usage !== undefined) + writer.tag(4, WireType.LengthDelimited).string(message.usage); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message GroupMessage + */ +export const GroupMessage = new GroupMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class AutoGroupMessage$Type extends MessageType { + constructor() { + super("AutoGroupMessage", [ + { no: 1, name: "owner", kind: "message", T: () => OwnerMessage }, + { no: 2, name: "identifier", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): AutoGroupMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.identifier = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: AutoGroupMessage): AutoGroupMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required OwnerMessage owner */ 1: + message.owner = OwnerMessage.internalBinaryRead(reader, reader.uint32(), options, message.owner); + break; + case /* required string identifier */ 2: + message.identifier = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: AutoGroupMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required OwnerMessage owner = 1; */ + if (message.owner) + OwnerMessage.internalBinaryWrite(message.owner, writer.tag(1, WireType.LengthDelimited).fork(), options).join(); + /* required string identifier = 2; */ + if (message.identifier !== "") + writer.tag(2, WireType.LengthDelimited).string(message.identifier); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message AutoGroupMessage + */ +export const AutoGroupMessage = new AutoGroupMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class UngroupMessage$Type extends MessageType { + constructor() { + super("UngroupMessage", [ + { no: 2, name: "requirements", kind: "message", repeat: 2 /*RepeatType.UNPACKED*/, T: () => DeviceRequirement } + ]); + } + create(value?: PartialMessage): UngroupMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.requirements = []; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: UngroupMessage): UngroupMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* repeated DeviceRequirement requirements */ 2: + message.requirements.push(DeviceRequirement.internalBinaryRead(reader, reader.uint32(), options)); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: UngroupMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* repeated DeviceRequirement requirements = 2; */ + for (let i = 0; i < message.requirements.length; i++) + DeviceRequirement.internalBinaryWrite(message.requirements[i], writer.tag(2, WireType.LengthDelimited).fork(), options).join(); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message UngroupMessage + */ +export const UngroupMessage = new UngroupMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class JoinGroupMessage$Type extends MessageType { + constructor() { + super("JoinGroupMessage", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "owner", kind: "message", T: () => OwnerMessage }, + { no: 3, name: "usage", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): JoinGroupMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: JoinGroupMessage): JoinGroupMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + case /* required OwnerMessage owner */ 2: + message.owner = OwnerMessage.internalBinaryRead(reader, reader.uint32(), options, message.owner); + break; + case /* optional string usage */ 3: + message.usage = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: JoinGroupMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + /* required OwnerMessage owner = 2; */ + if (message.owner) + OwnerMessage.internalBinaryWrite(message.owner, writer.tag(2, WireType.LengthDelimited).fork(), options).join(); + /* optional string usage = 3; */ + if (message.usage !== undefined) + writer.tag(3, WireType.LengthDelimited).string(message.usage); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message JoinGroupMessage + */ +export const JoinGroupMessage = new JoinGroupMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class JoinGroupByAdbFingerprintMessage$Type extends MessageType { + constructor() { + super("JoinGroupByAdbFingerprintMessage", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "fingerprint", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 3, name: "comment", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }, + { no: 4, name: "currentGroup", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): JoinGroupByAdbFingerprintMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + message.fingerprint = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: JoinGroupByAdbFingerprintMessage): JoinGroupByAdbFingerprintMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + case /* required string fingerprint */ 2: + message.fingerprint = reader.string(); + break; + case /* optional string comment */ 3: + message.comment = reader.string(); + break; + case /* optional string currentGroup */ 4: + message.currentGroup = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: JoinGroupByAdbFingerprintMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + /* required string fingerprint = 2; */ + if (message.fingerprint !== "") + writer.tag(2, WireType.LengthDelimited).string(message.fingerprint); + /* optional string comment = 3; */ + if (message.comment !== undefined) + writer.tag(3, WireType.LengthDelimited).string(message.comment); + /* optional string currentGroup = 4; */ + if (message.currentGroup !== undefined) + writer.tag(4, WireType.LengthDelimited).string(message.currentGroup); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message JoinGroupByAdbFingerprintMessage + */ +export const JoinGroupByAdbFingerprintMessage = new JoinGroupByAdbFingerprintMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class JoinGroupByVncAuthResponseMessage$Type extends MessageType { + constructor() { + super("JoinGroupByVncAuthResponseMessage", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "response", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 4, name: "currentGroup", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): JoinGroupByVncAuthResponseMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + message.response = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: JoinGroupByVncAuthResponseMessage): JoinGroupByVncAuthResponseMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + case /* required string response */ 2: + message.response = reader.string(); + break; + case /* optional string currentGroup */ 4: + message.currentGroup = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: JoinGroupByVncAuthResponseMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + /* required string response = 2; */ + if (message.response !== "") + writer.tag(2, WireType.LengthDelimited).string(message.response); + /* optional string currentGroup = 4; */ + if (message.currentGroup !== undefined) + writer.tag(4, WireType.LengthDelimited).string(message.currentGroup); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message JoinGroupByVncAuthResponseMessage + */ +export const JoinGroupByVncAuthResponseMessage = new JoinGroupByVncAuthResponseMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class AdbKeysUpdatedMessage$Type extends MessageType { + constructor() { + super("AdbKeysUpdatedMessage", []); + } + create(value?: PartialMessage): AdbKeysUpdatedMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: AdbKeysUpdatedMessage): AdbKeysUpdatedMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: AdbKeysUpdatedMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message AdbKeysUpdatedMessage + */ +export const AdbKeysUpdatedMessage = new AdbKeysUpdatedMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class VncAuthResponsesUpdatedMessage$Type extends MessageType { + constructor() { + super("VncAuthResponsesUpdatedMessage", []); + } + create(value?: PartialMessage): VncAuthResponsesUpdatedMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: VncAuthResponsesUpdatedMessage): VncAuthResponsesUpdatedMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: VncAuthResponsesUpdatedMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message VncAuthResponsesUpdatedMessage + */ +export const VncAuthResponsesUpdatedMessage = new VncAuthResponsesUpdatedMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class LeaveGroupMessage$Type extends MessageType { + constructor() { + super("LeaveGroupMessage", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "owner", kind: "message", T: () => OwnerMessage }, + { no: 3, name: "reason", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): LeaveGroupMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + message.reason = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: LeaveGroupMessage): LeaveGroupMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + case /* required OwnerMessage owner */ 2: + message.owner = OwnerMessage.internalBinaryRead(reader, reader.uint32(), options, message.owner); + break; + case /* required string reason */ 3: + message.reason = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: LeaveGroupMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + /* required OwnerMessage owner = 2; */ + if (message.owner) + OwnerMessage.internalBinaryWrite(message.owner, writer.tag(2, WireType.LengthDelimited).fork(), options).join(); + /* required string reason = 3; */ + if (message.reason !== "") + writer.tag(3, WireType.LengthDelimited).string(message.reason); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message LeaveGroupMessage + */ +export const LeaveGroupMessage = new LeaveGroupMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class PhysicalIdentifyMessage$Type extends MessageType { + constructor() { + super("PhysicalIdentifyMessage", []); + } + create(value?: PartialMessage): PhysicalIdentifyMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: PhysicalIdentifyMessage): PhysicalIdentifyMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: PhysicalIdentifyMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message PhysicalIdentifyMessage + */ +export const PhysicalIdentifyMessage = new PhysicalIdentifyMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class TouchDownMessage$Type extends MessageType { + constructor() { + super("TouchDownMessage", [ + { no: 1, name: "seq", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }, + { no: 2, name: "contact", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }, + { no: 3, name: "x", kind: "scalar", T: 2 /*ScalarType.FLOAT*/ }, + { no: 4, name: "y", kind: "scalar", T: 2 /*ScalarType.FLOAT*/ }, + { no: 5, name: "pressure", kind: "scalar", opt: true, T: 2 /*ScalarType.FLOAT*/ } + ]); + } + create(value?: PartialMessage): TouchDownMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.seq = 0; + message.contact = 0; + message.x = 0; + message.y = 0; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: TouchDownMessage): TouchDownMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required uint32 seq */ 1: + message.seq = reader.uint32(); + break; + case /* required uint32 contact */ 2: + message.contact = reader.uint32(); + break; + case /* required float x */ 3: + message.x = reader.float(); + break; + case /* required float y */ 4: + message.y = reader.float(); + break; + case /* optional float pressure */ 5: + message.pressure = reader.float(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: TouchDownMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required uint32 seq = 1; */ + if (message.seq !== 0) + writer.tag(1, WireType.Varint).uint32(message.seq); + /* required uint32 contact = 2; */ + if (message.contact !== 0) + writer.tag(2, WireType.Varint).uint32(message.contact); + /* required float x = 3; */ + if (message.x !== 0) + writer.tag(3, WireType.Bit32).float(message.x); + /* required float y = 4; */ + if (message.y !== 0) + writer.tag(4, WireType.Bit32).float(message.y); + /* optional float pressure = 5; */ + if (message.pressure !== undefined) + writer.tag(5, WireType.Bit32).float(message.pressure); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message TouchDownMessage + */ +export const TouchDownMessage = new TouchDownMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class TouchMoveMessage$Type extends MessageType { + constructor() { + super("TouchMoveMessage", [ + { no: 1, name: "seq", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }, + { no: 2, name: "contact", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }, + { no: 3, name: "x", kind: "scalar", T: 2 /*ScalarType.FLOAT*/ }, + { no: 4, name: "y", kind: "scalar", T: 2 /*ScalarType.FLOAT*/ }, + { no: 5, name: "pressure", kind: "scalar", opt: true, T: 2 /*ScalarType.FLOAT*/ } + ]); + } + create(value?: PartialMessage): TouchMoveMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.seq = 0; + message.contact = 0; + message.x = 0; + message.y = 0; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: TouchMoveMessage): TouchMoveMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required uint32 seq */ 1: + message.seq = reader.uint32(); + break; + case /* required uint32 contact */ 2: + message.contact = reader.uint32(); + break; + case /* required float x */ 3: + message.x = reader.float(); + break; + case /* required float y */ 4: + message.y = reader.float(); + break; + case /* optional float pressure */ 5: + message.pressure = reader.float(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: TouchMoveMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required uint32 seq = 1; */ + if (message.seq !== 0) + writer.tag(1, WireType.Varint).uint32(message.seq); + /* required uint32 contact = 2; */ + if (message.contact !== 0) + writer.tag(2, WireType.Varint).uint32(message.contact); + /* required float x = 3; */ + if (message.x !== 0) + writer.tag(3, WireType.Bit32).float(message.x); + /* required float y = 4; */ + if (message.y !== 0) + writer.tag(4, WireType.Bit32).float(message.y); + /* optional float pressure = 5; */ + if (message.pressure !== undefined) + writer.tag(5, WireType.Bit32).float(message.pressure); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message TouchMoveMessage + */ +export const TouchMoveMessage = new TouchMoveMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class TouchMoveIosMessage$Type extends MessageType { + constructor() { + super("TouchMoveIosMessage", [ + { no: 1, name: "toX", kind: "scalar", T: 2 /*ScalarType.FLOAT*/ }, + { no: 2, name: "toY", kind: "scalar", T: 2 /*ScalarType.FLOAT*/ }, + { no: 3, name: "fromX", kind: "scalar", T: 2 /*ScalarType.FLOAT*/ }, + { no: 4, name: "fromY", kind: "scalar", T: 2 /*ScalarType.FLOAT*/ }, + { no: 5, name: "duration", kind: "scalar", opt: true, T: 2 /*ScalarType.FLOAT*/ } + ]); + } + create(value?: PartialMessage): TouchMoveIosMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.toX = 0; + message.toY = 0; + message.fromX = 0; + message.fromY = 0; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: TouchMoveIosMessage): TouchMoveIosMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required float toX */ 1: + message.toX = reader.float(); + break; + case /* required float toY */ 2: + message.toY = reader.float(); + break; + case /* required float fromX */ 3: + message.fromX = reader.float(); + break; + case /* required float fromY */ 4: + message.fromY = reader.float(); + break; + case /* optional float duration */ 5: + message.duration = reader.float(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: TouchMoveIosMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required float toX = 1; */ + if (message.toX !== 0) + writer.tag(1, WireType.Bit32).float(message.toX); + /* required float toY = 2; */ + if (message.toY !== 0) + writer.tag(2, WireType.Bit32).float(message.toY); + /* required float fromX = 3; */ + if (message.fromX !== 0) + writer.tag(3, WireType.Bit32).float(message.fromX); + /* required float fromY = 4; */ + if (message.fromY !== 0) + writer.tag(4, WireType.Bit32).float(message.fromY); + /* optional float duration = 5; */ + if (message.duration !== undefined) + writer.tag(5, WireType.Bit32).float(message.duration); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message TouchMoveIosMessage + */ +export const TouchMoveIosMessage = new TouchMoveIosMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class TouchUpMessage$Type extends MessageType { + constructor() { + super("TouchUpMessage", [ + { no: 1, name: "seq", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }, + { no: 2, name: "contact", kind: "scalar", T: 13 /*ScalarType.UINT32*/ } + ]); + } + create(value?: PartialMessage): TouchUpMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.seq = 0; + message.contact = 0; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: TouchUpMessage): TouchUpMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required uint32 seq */ 1: + message.seq = reader.uint32(); + break; + case /* required uint32 contact */ 2: + message.contact = reader.uint32(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: TouchUpMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required uint32 seq = 1; */ + if (message.seq !== 0) + writer.tag(1, WireType.Varint).uint32(message.seq); + /* required uint32 contact = 2; */ + if (message.contact !== 0) + writer.tag(2, WireType.Varint).uint32(message.contact); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message TouchUpMessage + */ +export const TouchUpMessage = new TouchUpMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class TouchCommitMessage$Type extends MessageType { + constructor() { + super("TouchCommitMessage", [ + { no: 1, name: "seq", kind: "scalar", T: 13 /*ScalarType.UINT32*/ } + ]); + } + create(value?: PartialMessage): TouchCommitMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.seq = 0; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: TouchCommitMessage): TouchCommitMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required uint32 seq */ 1: + message.seq = reader.uint32(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: TouchCommitMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required uint32 seq = 1; */ + if (message.seq !== 0) + writer.tag(1, WireType.Varint).uint32(message.seq); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message TouchCommitMessage + */ +export const TouchCommitMessage = new TouchCommitMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class TouchResetMessage$Type extends MessageType { + constructor() { + super("TouchResetMessage", [ + { no: 1, name: "seq", kind: "scalar", T: 13 /*ScalarType.UINT32*/ } + ]); + } + create(value?: PartialMessage): TouchResetMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.seq = 0; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: TouchResetMessage): TouchResetMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required uint32 seq */ 1: + message.seq = reader.uint32(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: TouchResetMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required uint32 seq = 1; */ + if (message.seq !== 0) + writer.tag(1, WireType.Varint).uint32(message.seq); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message TouchResetMessage + */ +export const TouchResetMessage = new TouchResetMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class GestureStartMessage$Type extends MessageType { + constructor() { + super("GestureStartMessage", [ + { no: 1, name: "seq", kind: "scalar", T: 13 /*ScalarType.UINT32*/ } + ]); + } + create(value?: PartialMessage): GestureStartMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.seq = 0; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: GestureStartMessage): GestureStartMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required uint32 seq */ 1: + message.seq = reader.uint32(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: GestureStartMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required uint32 seq = 1; */ + if (message.seq !== 0) + writer.tag(1, WireType.Varint).uint32(message.seq); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message GestureStartMessage + */ +export const GestureStartMessage = new GestureStartMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class GestureStopMessage$Type extends MessageType { + constructor() { + super("GestureStopMessage", [ + { no: 1, name: "seq", kind: "scalar", T: 13 /*ScalarType.UINT32*/ } + ]); + } + create(value?: PartialMessage): GestureStopMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.seq = 0; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: GestureStopMessage): GestureStopMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required uint32 seq */ 1: + message.seq = reader.uint32(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: GestureStopMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required uint32 seq = 1; */ + if (message.seq !== 0) + writer.tag(1, WireType.Varint).uint32(message.seq); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message GestureStopMessage + */ +export const GestureStopMessage = new GestureStopMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class TypeMessage$Type extends MessageType { + constructor() { + super("TypeMessage", [ + { no: 1, name: "text", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): TypeMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.text = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: TypeMessage): TypeMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string text */ 1: + message.text = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: TypeMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string text = 1; */ + if (message.text !== "") + writer.tag(1, WireType.LengthDelimited).string(message.text); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message TypeMessage + */ +export const TypeMessage = new TypeMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class PasteMessage$Type extends MessageType { + constructor() { + super("PasteMessage", [ + { no: 1, name: "text", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): PasteMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.text = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: PasteMessage): PasteMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string text */ 1: + message.text = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: PasteMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string text = 1; */ + if (message.text !== "") + writer.tag(1, WireType.LengthDelimited).string(message.text); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message PasteMessage + */ +export const PasteMessage = new PasteMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class CopyMessage$Type extends MessageType { + constructor() { + super("CopyMessage", []); + } + create(value?: PartialMessage): CopyMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: CopyMessage): CopyMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: CopyMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message CopyMessage + */ +export const CopyMessage = new CopyMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class KeyDownMessage$Type extends MessageType { + constructor() { + super("KeyDownMessage", [ + { no: 1, name: "key", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): KeyDownMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.key = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: KeyDownMessage): KeyDownMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string key */ 1: + message.key = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: KeyDownMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string key = 1; */ + if (message.key !== "") + writer.tag(1, WireType.LengthDelimited).string(message.key); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message KeyDownMessage + */ +export const KeyDownMessage = new KeyDownMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class KeyUpMessage$Type extends MessageType { + constructor() { + super("KeyUpMessage", [ + { no: 1, name: "key", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): KeyUpMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.key = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: KeyUpMessage): KeyUpMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string key */ 1: + message.key = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: KeyUpMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string key = 1; */ + if (message.key !== "") + writer.tag(1, WireType.LengthDelimited).string(message.key); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message KeyUpMessage + */ +export const KeyUpMessage = new KeyUpMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class KeyPressMessage$Type extends MessageType { + constructor() { + super("KeyPressMessage", [ + { no: 1, name: "key", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): KeyPressMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.key = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: KeyPressMessage): KeyPressMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string key */ 1: + message.key = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: KeyPressMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string key = 1; */ + if (message.key !== "") + writer.tag(1, WireType.LengthDelimited).string(message.key); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message KeyPressMessage + */ +export const KeyPressMessage = new KeyPressMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class RebootMessage$Type extends MessageType { + constructor() { + super("RebootMessage", []); + } + create(value?: PartialMessage): RebootMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: RebootMessage): RebootMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: RebootMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message RebootMessage + */ +export const RebootMessage = new RebootMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class DeviceLogcatEntryMessage$Type extends MessageType { + constructor() { + super("DeviceLogcatEntryMessage", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "date", kind: "scalar", T: 1 /*ScalarType.DOUBLE*/ }, + { no: 3, name: "pid", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }, + { no: 4, name: "tid", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }, + { no: 5, name: "priority", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }, + { no: 6, name: "tag", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 7, name: "message", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): DeviceLogcatEntryMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + message.date = 0; + message.pid = 0; + message.tid = 0; + message.priority = 0; + message.tag = ""; + message.message = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DeviceLogcatEntryMessage): DeviceLogcatEntryMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + case /* required double date */ 2: + message.date = reader.double(); + break; + case /* required uint32 pid */ 3: + message.pid = reader.uint32(); + break; + case /* required uint32 tid */ 4: + message.tid = reader.uint32(); + break; + case /* required uint32 priority */ 5: + message.priority = reader.uint32(); + break; + case /* required string tag */ 6: + message.tag = reader.string(); + break; + case /* required string message */ 7: + message.message = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: DeviceLogcatEntryMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + /* required double date = 2; */ + if (message.date !== 0) + writer.tag(2, WireType.Bit64).double(message.date); + /* required uint32 pid = 3; */ + if (message.pid !== 0) + writer.tag(3, WireType.Varint).uint32(message.pid); + /* required uint32 tid = 4; */ + if (message.tid !== 0) + writer.tag(4, WireType.Varint).uint32(message.tid); + /* required uint32 priority = 5; */ + if (message.priority !== 0) + writer.tag(5, WireType.Varint).uint32(message.priority); + /* required string tag = 6; */ + if (message.tag !== "") + writer.tag(6, WireType.LengthDelimited).string(message.tag); + /* required string message = 7; */ + if (message.message !== "") + writer.tag(7, WireType.LengthDelimited).string(message.message); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message DeviceLogcatEntryMessage + */ +export const DeviceLogcatEntryMessage = new DeviceLogcatEntryMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class LogcatFilter$Type extends MessageType { + constructor() { + super("LogcatFilter", [ + { no: 1, name: "tag", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "priority", kind: "scalar", T: 13 /*ScalarType.UINT32*/ } + ]); + } + create(value?: PartialMessage): LogcatFilter { + const message = globalThis.Object.create((this.messagePrototype!)); + message.tag = ""; + message.priority = 0; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: LogcatFilter): LogcatFilter { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string tag */ 1: + message.tag = reader.string(); + break; + case /* required uint32 priority */ 2: + message.priority = reader.uint32(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: LogcatFilter, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string tag = 1; */ + if (message.tag !== "") + writer.tag(1, WireType.LengthDelimited).string(message.tag); + /* required uint32 priority = 2; */ + if (message.priority !== 0) + writer.tag(2, WireType.Varint).uint32(message.priority); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message LogcatFilter + */ +export const LogcatFilter = new LogcatFilter$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class LogcatStartMessage$Type extends MessageType { + constructor() { + super("LogcatStartMessage", [ + { no: 1, name: "filters", kind: "message", repeat: 2 /*RepeatType.UNPACKED*/, T: () => LogcatFilter } + ]); + } + create(value?: PartialMessage): LogcatStartMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.filters = []; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: LogcatStartMessage): LogcatStartMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* repeated LogcatFilter filters */ 1: + message.filters.push(LogcatFilter.internalBinaryRead(reader, reader.uint32(), options)); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: LogcatStartMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* repeated LogcatFilter filters = 1; */ + for (let i = 0; i < message.filters.length; i++) + LogcatFilter.internalBinaryWrite(message.filters[i], writer.tag(1, WireType.LengthDelimited).fork(), options).join(); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message LogcatStartMessage + */ +export const LogcatStartMessage = new LogcatStartMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class LogcatStopMessage$Type extends MessageType { + constructor() { + super("LogcatStopMessage", []); + } + create(value?: PartialMessage): LogcatStopMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: LogcatStopMessage): LogcatStopMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: LogcatStopMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message LogcatStopMessage + */ +export const LogcatStopMessage = new LogcatStopMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class LogcatApplyFiltersMessage$Type extends MessageType { + constructor() { + super("LogcatApplyFiltersMessage", [ + { no: 1, name: "filters", kind: "message", repeat: 2 /*RepeatType.UNPACKED*/, T: () => LogcatFilter } + ]); + } + create(value?: PartialMessage): LogcatApplyFiltersMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.filters = []; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: LogcatApplyFiltersMessage): LogcatApplyFiltersMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* repeated LogcatFilter filters */ 1: + message.filters.push(LogcatFilter.internalBinaryRead(reader, reader.uint32(), options)); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: LogcatApplyFiltersMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* repeated LogcatFilter filters = 1; */ + for (let i = 0; i < message.filters.length; i++) + LogcatFilter.internalBinaryWrite(message.filters[i], writer.tag(1, WireType.LengthDelimited).fork(), options).join(); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message LogcatApplyFiltersMessage + */ +export const LogcatApplyFiltersMessage = new LogcatApplyFiltersMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class ShellCommandMessage$Type extends MessageType { + constructor() { + super("ShellCommandMessage", [ + { no: 1, name: "command", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "timeout", kind: "scalar", T: 13 /*ScalarType.UINT32*/ } + ]); + } + create(value?: PartialMessage): ShellCommandMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.command = ""; + message.timeout = 0; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: ShellCommandMessage): ShellCommandMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string command */ 1: + message.command = reader.string(); + break; + case /* required uint32 timeout */ 2: + message.timeout = reader.uint32(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: ShellCommandMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string command = 1; */ + if (message.command !== "") + writer.tag(1, WireType.LengthDelimited).string(message.command); + /* required uint32 timeout = 2; */ + if (message.timeout !== 0) + writer.tag(2, WireType.Varint).uint32(message.timeout); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message ShellCommandMessage + */ +export const ShellCommandMessage = new ShellCommandMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class ShellKeepAliveMessage$Type extends MessageType { + constructor() { + super("ShellKeepAliveMessage", [ + { no: 1, name: "timeout", kind: "scalar", T: 13 /*ScalarType.UINT32*/ } + ]); + } + create(value?: PartialMessage): ShellKeepAliveMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.timeout = 0; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: ShellKeepAliveMessage): ShellKeepAliveMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required uint32 timeout */ 1: + message.timeout = reader.uint32(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: ShellKeepAliveMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required uint32 timeout = 1; */ + if (message.timeout !== 0) + writer.tag(1, WireType.Varint).uint32(message.timeout); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message ShellKeepAliveMessage + */ +export const ShellKeepAliveMessage = new ShellKeepAliveMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class InstallMessage$Type extends MessageType { + constructor() { + super("InstallMessage", [ + { no: 1, name: "href", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "launch", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }, + { no: 3, name: "isApi", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }, + { no: 4, name: "manifest", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }, + { no: 5, name: "installFlags", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ }, + { no: 6, name: "jwt", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }, + { no: 7, name: "pkg", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): InstallMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.href = ""; + message.launch = false; + message.isApi = false; + message.installFlags = []; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: InstallMessage): InstallMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string href */ 1: + message.href = reader.string(); + break; + case /* required bool launch */ 2: + message.launch = reader.bool(); + break; + case /* required bool isApi */ 3: + message.isApi = reader.bool(); + break; + case /* optional string manifest */ 4: + message.manifest = reader.string(); + break; + case /* repeated string installFlags */ 5: + message.installFlags.push(reader.string()); + break; + case /* optional string jwt */ 6: + message.jwt = reader.string(); + break; + case /* optional string pkg */ 7: + message.pkg = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: InstallMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string href = 1; */ + if (message.href !== "") + writer.tag(1, WireType.LengthDelimited).string(message.href); + /* required bool launch = 2; */ + if (message.launch !== false) + writer.tag(2, WireType.Varint).bool(message.launch); + /* required bool isApi = 3; */ + if (message.isApi !== false) + writer.tag(3, WireType.Varint).bool(message.isApi); + /* optional string manifest = 4; */ + if (message.manifest !== undefined) + writer.tag(4, WireType.LengthDelimited).string(message.manifest); + /* repeated string installFlags = 5; */ + for (let i = 0; i < message.installFlags.length; i++) + writer.tag(5, WireType.LengthDelimited).string(message.installFlags[i]); + /* optional string jwt = 6; */ + if (message.jwt !== undefined) + writer.tag(6, WireType.LengthDelimited).string(message.jwt); + /* optional string pkg = 7; */ + if (message.pkg !== undefined) + writer.tag(7, WireType.LengthDelimited).string(message.pkg); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message InstallMessage + */ +export const InstallMessage = new InstallMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class UninstallMessage$Type extends MessageType { + constructor() { + super("UninstallMessage", [ + { no: 1, name: "packageName", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): UninstallMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.packageName = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: UninstallMessage): UninstallMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string packageName */ 1: + message.packageName = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: UninstallMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string packageName = 1; */ + if (message.packageName !== "") + writer.tag(1, WireType.LengthDelimited).string(message.packageName); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message UninstallMessage + */ +export const UninstallMessage = new UninstallMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class UninstallIosMessage$Type extends MessageType { + constructor() { + super("UninstallIosMessage", [ + { no: 1, name: "packageName", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): UninstallIosMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.packageName = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: UninstallIosMessage): UninstallIosMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string packageName */ 1: + message.packageName = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: UninstallIosMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string packageName = 1; */ + if (message.packageName !== "") + writer.tag(1, WireType.LengthDelimited).string(message.packageName); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message UninstallIosMessage + */ +export const UninstallIosMessage = new UninstallIosMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class LaunchActivityMessage$Type extends MessageType { + constructor() { + super("LaunchActivityMessage", [ + { no: 1, name: "action", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "component", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 3, name: "category", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ }, + { no: 4, name: "flags", kind: "scalar", opt: true, T: 13 /*ScalarType.UINT32*/ } + ]); + } + create(value?: PartialMessage): LaunchActivityMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.action = ""; + message.component = ""; + message.category = []; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: LaunchActivityMessage): LaunchActivityMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string action */ 1: + message.action = reader.string(); + break; + case /* required string component */ 2: + message.component = reader.string(); + break; + case /* repeated string category */ 3: + message.category.push(reader.string()); + break; + case /* optional uint32 flags */ 4: + message.flags = reader.uint32(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: LaunchActivityMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string action = 1; */ + if (message.action !== "") + writer.tag(1, WireType.LengthDelimited).string(message.action); + /* required string component = 2; */ + if (message.component !== "") + writer.tag(2, WireType.LengthDelimited).string(message.component); + /* repeated string category = 3; */ + for (let i = 0; i < message.category.length; i++) + writer.tag(3, WireType.LengthDelimited).string(message.category[i]); + /* optional uint32 flags = 4; */ + if (message.flags !== undefined) + writer.tag(4, WireType.Varint).uint32(message.flags); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message LaunchActivityMessage + */ +export const LaunchActivityMessage = new LaunchActivityMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class RotateMessage$Type extends MessageType { + constructor() { + super("RotateMessage", [ + { no: 1, name: "rotation", kind: "scalar", T: 5 /*ScalarType.INT32*/ } + ]); + } + create(value?: PartialMessage): RotateMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.rotation = 0; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: RotateMessage): RotateMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required int32 rotation */ 1: + message.rotation = reader.int32(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: RotateMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required int32 rotation = 1; */ + if (message.rotation !== 0) + writer.tag(1, WireType.Varint).int32(message.rotation); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message RotateMessage + */ +export const RotateMessage = new RotateMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class ChangeQualityMessage$Type extends MessageType { + constructor() { + super("ChangeQualityMessage", [ + { no: 1, name: "quality", kind: "scalar", T: 5 /*ScalarType.INT32*/ } + ]); + } + create(value?: PartialMessage): ChangeQualityMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.quality = 0; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: ChangeQualityMessage): ChangeQualityMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required int32 quality */ 1: + message.quality = reader.int32(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: ChangeQualityMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required int32 quality = 1; */ + if (message.quality !== 0) + writer.tag(1, WireType.Varint).int32(message.quality); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message ChangeQualityMessage + */ +export const ChangeQualityMessage = new ChangeQualityMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class ForwardTestMessage$Type extends MessageType { + constructor() { + super("ForwardTestMessage", [ + { no: 1, name: "targetHost", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "targetPort", kind: "scalar", T: 13 /*ScalarType.UINT32*/ } + ]); + } + create(value?: PartialMessage): ForwardTestMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.targetHost = ""; + message.targetPort = 0; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: ForwardTestMessage): ForwardTestMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string targetHost */ 1: + message.targetHost = reader.string(); + break; + case /* required uint32 targetPort */ 2: + message.targetPort = reader.uint32(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: ForwardTestMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string targetHost = 1; */ + if (message.targetHost !== "") + writer.tag(1, WireType.LengthDelimited).string(message.targetHost); + /* required uint32 targetPort = 2; */ + if (message.targetPort !== 0) + writer.tag(2, WireType.Varint).uint32(message.targetPort); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message ForwardTestMessage + */ +export const ForwardTestMessage = new ForwardTestMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class ForwardCreateMessage$Type extends MessageType { + constructor() { + super("ForwardCreateMessage", [ + { no: 1, name: "id", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "devicePort", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }, + { no: 3, name: "targetHost", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 4, name: "targetPort", kind: "scalar", T: 13 /*ScalarType.UINT32*/ } + ]); + } + create(value?: PartialMessage): ForwardCreateMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.id = ""; + message.devicePort = 0; + message.targetHost = ""; + message.targetPort = 0; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: ForwardCreateMessage): ForwardCreateMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string id */ 1: + message.id = reader.string(); + break; + case /* required uint32 devicePort */ 2: + message.devicePort = reader.uint32(); + break; + case /* required string targetHost */ 3: + message.targetHost = reader.string(); + break; + case /* required uint32 targetPort */ 4: + message.targetPort = reader.uint32(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: ForwardCreateMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string id = 1; */ + if (message.id !== "") + writer.tag(1, WireType.LengthDelimited).string(message.id); + /* required uint32 devicePort = 2; */ + if (message.devicePort !== 0) + writer.tag(2, WireType.Varint).uint32(message.devicePort); + /* required string targetHost = 3; */ + if (message.targetHost !== "") + writer.tag(3, WireType.LengthDelimited).string(message.targetHost); + /* required uint32 targetPort = 4; */ + if (message.targetPort !== 0) + writer.tag(4, WireType.Varint).uint32(message.targetPort); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message ForwardCreateMessage + */ +export const ForwardCreateMessage = new ForwardCreateMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class ForwardRemoveMessage$Type extends MessageType { + constructor() { + super("ForwardRemoveMessage", [ + { no: 1, name: "id", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): ForwardRemoveMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.id = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: ForwardRemoveMessage): ForwardRemoveMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string id */ 1: + message.id = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: ForwardRemoveMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string id = 1; */ + if (message.id !== "") + writer.tag(1, WireType.LengthDelimited).string(message.id); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message ForwardRemoveMessage + */ +export const ForwardRemoveMessage = new ForwardRemoveMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class ReverseForward$Type extends MessageType { + constructor() { + super("ReverseForward", [ + { no: 1, name: "id", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "devicePort", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }, + { no: 3, name: "targetHost", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 4, name: "targetPort", kind: "scalar", T: 13 /*ScalarType.UINT32*/ } + ]); + } + create(value?: PartialMessage): ReverseForward { + const message = globalThis.Object.create((this.messagePrototype!)); + message.id = ""; + message.devicePort = 0; + message.targetHost = ""; + message.targetPort = 0; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: ReverseForward): ReverseForward { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string id */ 1: + message.id = reader.string(); + break; + case /* required uint32 devicePort */ 2: + message.devicePort = reader.uint32(); + break; + case /* required string targetHost */ 3: + message.targetHost = reader.string(); + break; + case /* required uint32 targetPort */ 4: + message.targetPort = reader.uint32(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: ReverseForward, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string id = 1; */ + if (message.id !== "") + writer.tag(1, WireType.LengthDelimited).string(message.id); + /* required uint32 devicePort = 2; */ + if (message.devicePort !== 0) + writer.tag(2, WireType.Varint).uint32(message.devicePort); + /* required string targetHost = 3; */ + if (message.targetHost !== "") + writer.tag(3, WireType.LengthDelimited).string(message.targetHost); + /* required uint32 targetPort = 4; */ + if (message.targetPort !== 0) + writer.tag(4, WireType.Varint).uint32(message.targetPort); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message ReverseForward + */ +export const ReverseForward = new ReverseForward$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class ReverseForwardsEvent$Type extends MessageType { + constructor() { + super("ReverseForwardsEvent", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "forwards", kind: "message", repeat: 2 /*RepeatType.UNPACKED*/, T: () => ReverseForward } + ]); + } + create(value?: PartialMessage): ReverseForwardsEvent { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + message.forwards = []; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: ReverseForwardsEvent): ReverseForwardsEvent { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + case /* repeated ReverseForward forwards */ 2: + message.forwards.push(ReverseForward.internalBinaryRead(reader, reader.uint32(), options)); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: ReverseForwardsEvent, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + /* repeated ReverseForward forwards = 2; */ + for (let i = 0; i < message.forwards.length; i++) + ReverseForward.internalBinaryWrite(message.forwards[i], writer.tag(2, WireType.LengthDelimited).fork(), options).join(); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message ReverseForwardsEvent + */ +export const ReverseForwardsEvent = new ReverseForwardsEvent$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class BrowserOpenMessage$Type extends MessageType { + constructor() { + super("BrowserOpenMessage", [ + { no: 1, name: "url", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "browser", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): BrowserOpenMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.url = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: BrowserOpenMessage): BrowserOpenMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string url */ 1: + message.url = reader.string(); + break; + case /* optional string browser */ 2: + message.browser = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: BrowserOpenMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string url = 1; */ + if (message.url !== "") + writer.tag(1, WireType.LengthDelimited).string(message.url); + /* optional string browser = 2; */ + if (message.browser !== undefined) + writer.tag(2, WireType.LengthDelimited).string(message.browser); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message BrowserOpenMessage + */ +export const BrowserOpenMessage = new BrowserOpenMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class BrowserClearMessage$Type extends MessageType { + constructor() { + super("BrowserClearMessage", [ + { no: 1, name: "browser", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): BrowserClearMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: BrowserClearMessage): BrowserClearMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* optional string browser */ 1: + message.browser = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: BrowserClearMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* optional string browser = 1; */ + if (message.browser !== undefined) + writer.tag(1, WireType.LengthDelimited).string(message.browser); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message BrowserClearMessage + */ +export const BrowserClearMessage = new BrowserClearMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class StoreOpenMessage$Type extends MessageType { + constructor() { + super("StoreOpenMessage", []); + } + create(value?: PartialMessage): StoreOpenMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: StoreOpenMessage): StoreOpenMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: StoreOpenMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message StoreOpenMessage + */ +export const StoreOpenMessage = new StoreOpenMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class ScreenCaptureMessage$Type extends MessageType { + constructor() { + super("ScreenCaptureMessage", []); + } + create(value?: PartialMessage): ScreenCaptureMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: ScreenCaptureMessage): ScreenCaptureMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: ScreenCaptureMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message ScreenCaptureMessage + */ +export const ScreenCaptureMessage = new ScreenCaptureMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class ConnectStartMessage$Type extends MessageType { + constructor() { + super("ConnectStartMessage", []); + } + create(value?: PartialMessage): ConnectStartMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: ConnectStartMessage): ConnectStartMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: ConnectStartMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message ConnectStartMessage + */ +export const ConnectStartMessage = new ConnectStartMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class ConnectGetForwardUrlMessage$Type extends MessageType { + constructor() { + super("ConnectGetForwardUrlMessage", []); + } + create(value?: PartialMessage): ConnectGetForwardUrlMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: ConnectGetForwardUrlMessage): ConnectGetForwardUrlMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: ConnectGetForwardUrlMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message ConnectGetForwardUrlMessage + */ +export const ConnectGetForwardUrlMessage = new ConnectGetForwardUrlMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class ConnectStopMessage$Type extends MessageType { + constructor() { + super("ConnectStopMessage", []); + } + create(value?: PartialMessage): ConnectStopMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: ConnectStopMessage): ConnectStopMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: ConnectStopMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message ConnectStopMessage + */ +export const ConnectStopMessage = new ConnectStopMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class AccountAddMenuMessage$Type extends MessageType { + constructor() { + super("AccountAddMenuMessage", []); + } + create(value?: PartialMessage): AccountAddMenuMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: AccountAddMenuMessage): AccountAddMenuMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: AccountAddMenuMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message AccountAddMenuMessage + */ +export const AccountAddMenuMessage = new AccountAddMenuMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class AccountAddMessage$Type extends MessageType { + constructor() { + super("AccountAddMessage", [ + { no: 1, name: "user", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "password", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): AccountAddMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.user = ""; + message.password = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: AccountAddMessage): AccountAddMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string user */ 1: + message.user = reader.string(); + break; + case /* required string password */ 2: + message.password = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: AccountAddMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string user = 1; */ + if (message.user !== "") + writer.tag(1, WireType.LengthDelimited).string(message.user); + /* required string password = 2; */ + if (message.password !== "") + writer.tag(2, WireType.LengthDelimited).string(message.password); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message AccountAddMessage + */ +export const AccountAddMessage = new AccountAddMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class AccountCheckMessage$Type extends MessageType { + constructor() { + super("AccountCheckMessage", [ + { no: 1, name: "type", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "account", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): AccountCheckMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.type = ""; + message.account = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: AccountCheckMessage): AccountCheckMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string type */ 1: + message.type = reader.string(); + break; + case /* required string account */ 2: + message.account = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: AccountCheckMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string type = 1; */ + if (message.type !== "") + writer.tag(1, WireType.LengthDelimited).string(message.type); + /* required string account = 2; */ + if (message.account !== "") + writer.tag(2, WireType.LengthDelimited).string(message.account); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message AccountCheckMessage + */ +export const AccountCheckMessage = new AccountCheckMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class AccountGetMessage$Type extends MessageType { + constructor() { + super("AccountGetMessage", [ + { no: 1, name: "type", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): AccountGetMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: AccountGetMessage): AccountGetMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* optional string type */ 1: + message.type = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: AccountGetMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* optional string type = 1; */ + if (message.type !== undefined) + writer.tag(1, WireType.LengthDelimited).string(message.type); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message AccountGetMessage + */ +export const AccountGetMessage = new AccountGetMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class AccountRemoveMessage$Type extends MessageType { + constructor() { + super("AccountRemoveMessage", [ + { no: 1, name: "type", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "account", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): AccountRemoveMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.type = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: AccountRemoveMessage): AccountRemoveMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string type */ 1: + message.type = reader.string(); + break; + case /* optional string account */ 2: + message.account = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: AccountRemoveMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string type = 1; */ + if (message.type !== "") + writer.tag(1, WireType.LengthDelimited).string(message.type); + /* optional string account = 2; */ + if (message.account !== undefined) + writer.tag(2, WireType.LengthDelimited).string(message.account); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message AccountRemoveMessage + */ +export const AccountRemoveMessage = new AccountRemoveMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class SdStatusMessage$Type extends MessageType { + constructor() { + super("SdStatusMessage", []); + } + create(value?: PartialMessage): SdStatusMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: SdStatusMessage): SdStatusMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: SdStatusMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message SdStatusMessage + */ +export const SdStatusMessage = new SdStatusMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class AirplaneSetMessage$Type extends MessageType { + constructor() { + super("AirplaneSetMessage", [ + { no: 1, name: "enabled", kind: "scalar", T: 8 /*ScalarType.BOOL*/ } + ]); + } + create(value?: PartialMessage): AirplaneSetMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.enabled = false; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: AirplaneSetMessage): AirplaneSetMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required bool enabled */ 1: + message.enabled = reader.bool(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: AirplaneSetMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required bool enabled = 1; */ + if (message.enabled !== false) + writer.tag(1, WireType.Varint).bool(message.enabled); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message AirplaneSetMessage + */ +export const AirplaneSetMessage = new AirplaneSetMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class RingerSetMessage$Type extends MessageType { + constructor() { + super("RingerSetMessage", [ + { no: 1, name: "mode", kind: "enum", T: () => ["RingerMode", RingerMode] } + ]); + } + create(value?: PartialMessage): RingerSetMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.mode = 0; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: RingerSetMessage): RingerSetMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required RingerMode mode */ 1: + message.mode = reader.int32(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: RingerSetMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required RingerMode mode = 1; */ + if (message.mode !== 0) + writer.tag(1, WireType.Varint).int32(message.mode); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message RingerSetMessage + */ +export const RingerSetMessage = new RingerSetMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class RingerGetMessage$Type extends MessageType { + constructor() { + super("RingerGetMessage", []); + } + create(value?: PartialMessage): RingerGetMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: RingerGetMessage): RingerGetMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: RingerGetMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message RingerGetMessage + */ +export const RingerGetMessage = new RingerGetMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class WifiSetEnabledMessage$Type extends MessageType { + constructor() { + super("WifiSetEnabledMessage", [ + { no: 1, name: "enabled", kind: "scalar", T: 8 /*ScalarType.BOOL*/ } + ]); + } + create(value?: PartialMessage): WifiSetEnabledMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.enabled = false; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: WifiSetEnabledMessage): WifiSetEnabledMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required bool enabled */ 1: + message.enabled = reader.bool(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: WifiSetEnabledMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required bool enabled = 1; */ + if (message.enabled !== false) + writer.tag(1, WireType.Varint).bool(message.enabled); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message WifiSetEnabledMessage + */ +export const WifiSetEnabledMessage = new WifiSetEnabledMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class WifiGetStatusMessage$Type extends MessageType { + constructor() { + super("WifiGetStatusMessage", []); + } + create(value?: PartialMessage): WifiGetStatusMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: WifiGetStatusMessage): WifiGetStatusMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: WifiGetStatusMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message WifiGetStatusMessage + */ +export const WifiGetStatusMessage = new WifiGetStatusMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class BluetoothSetEnabledMessage$Type extends MessageType { + constructor() { + super("BluetoothSetEnabledMessage", [ + { no: 1, name: "enabled", kind: "scalar", T: 8 /*ScalarType.BOOL*/ } + ]); + } + create(value?: PartialMessage): BluetoothSetEnabledMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.enabled = false; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: BluetoothSetEnabledMessage): BluetoothSetEnabledMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required bool enabled */ 1: + message.enabled = reader.bool(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: BluetoothSetEnabledMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required bool enabled = 1; */ + if (message.enabled !== false) + writer.tag(1, WireType.Varint).bool(message.enabled); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message BluetoothSetEnabledMessage + */ +export const BluetoothSetEnabledMessage = new BluetoothSetEnabledMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class BluetoothGetStatusMessage$Type extends MessageType { + constructor() { + super("BluetoothGetStatusMessage", []); + } + create(value?: PartialMessage): BluetoothGetStatusMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: BluetoothGetStatusMessage): BluetoothGetStatusMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: BluetoothGetStatusMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message BluetoothGetStatusMessage + */ +export const BluetoothGetStatusMessage = new BluetoothGetStatusMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class BluetoothCleanBondedMessage$Type extends MessageType { + constructor() { + super("BluetoothCleanBondedMessage", []); + } + create(value?: PartialMessage): BluetoothCleanBondedMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: BluetoothCleanBondedMessage): BluetoothCleanBondedMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: BluetoothCleanBondedMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message BluetoothCleanBondedMessage + */ +export const BluetoothCleanBondedMessage = new BluetoothCleanBondedMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class CapabilitiesMessage$Type extends MessageType { + constructor() { + super("CapabilitiesMessage", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "hasTouch", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }, + { no: 3, name: "hasCursor", kind: "scalar", T: 8 /*ScalarType.BOOL*/ } + ]); + } + create(value?: PartialMessage): CapabilitiesMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + message.hasTouch = false; + message.hasCursor = false; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: CapabilitiesMessage): CapabilitiesMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + case /* required bool hasTouch */ 2: + message.hasTouch = reader.bool(); + break; + case /* required bool hasCursor */ 3: + message.hasCursor = reader.bool(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: CapabilitiesMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + /* required bool hasTouch = 2; */ + if (message.hasTouch !== false) + writer.tag(2, WireType.Varint).bool(message.hasTouch); + /* required bool hasCursor = 3; */ + if (message.hasCursor !== false) + writer.tag(3, WireType.Varint).bool(message.hasCursor); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message CapabilitiesMessage + */ +export const CapabilitiesMessage = new CapabilitiesMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class AirplaneModeEvent$Type extends MessageType { + constructor() { + super("AirplaneModeEvent", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "enabled", kind: "scalar", T: 8 /*ScalarType.BOOL*/ } + ]); + } + create(value?: PartialMessage): AirplaneModeEvent { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + message.enabled = false; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: AirplaneModeEvent): AirplaneModeEvent { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + case /* required bool enabled */ 2: + message.enabled = reader.bool(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: AirplaneModeEvent, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + /* required bool enabled = 2; */ + if (message.enabled !== false) + writer.tag(2, WireType.Varint).bool(message.enabled); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message AirplaneModeEvent + */ +export const AirplaneModeEvent = new AirplaneModeEvent$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class BatteryEvent$Type extends MessageType { + constructor() { + super("BatteryEvent", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "status", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 3, name: "health", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 4, name: "source", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 5, name: "level", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }, + { no: 6, name: "scale", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }, + { no: 7, name: "temp", kind: "scalar", T: 1 /*ScalarType.DOUBLE*/ }, + { no: 8, name: "voltage", kind: "scalar", opt: true, T: 1 /*ScalarType.DOUBLE*/ } + ]); + } + create(value?: PartialMessage): BatteryEvent { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + message.status = ""; + message.health = ""; + message.source = ""; + message.level = 0; + message.scale = 0; + message.temp = 0; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: BatteryEvent): BatteryEvent { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + case /* required string status */ 2: + message.status = reader.string(); + break; + case /* required string health */ 3: + message.health = reader.string(); + break; + case /* required string source */ 4: + message.source = reader.string(); + break; + case /* required uint32 level */ 5: + message.level = reader.uint32(); + break; + case /* required uint32 scale */ 6: + message.scale = reader.uint32(); + break; + case /* required double temp */ 7: + message.temp = reader.double(); + break; + case /* optional double voltage */ 8: + message.voltage = reader.double(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: BatteryEvent, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + /* required string status = 2; */ + if (message.status !== "") + writer.tag(2, WireType.LengthDelimited).string(message.status); + /* required string health = 3; */ + if (message.health !== "") + writer.tag(3, WireType.LengthDelimited).string(message.health); + /* required string source = 4; */ + if (message.source !== "") + writer.tag(4, WireType.LengthDelimited).string(message.source); + /* required uint32 level = 5; */ + if (message.level !== 0) + writer.tag(5, WireType.Varint).uint32(message.level); + /* required uint32 scale = 6; */ + if (message.scale !== 0) + writer.tag(6, WireType.Varint).uint32(message.scale); + /* required double temp = 7; */ + if (message.temp !== 0) + writer.tag(7, WireType.Bit64).double(message.temp); + /* optional double voltage = 8; */ + if (message.voltage !== undefined) + writer.tag(8, WireType.Bit64).double(message.voltage); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message BatteryEvent + */ +export const BatteryEvent = new BatteryEvent$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class ConnectivityEvent$Type extends MessageType { + constructor() { + super("ConnectivityEvent", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "connected", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }, + { no: 3, name: "type", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }, + { no: 4, name: "subtype", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }, + { no: 5, name: "failover", kind: "scalar", opt: true, T: 8 /*ScalarType.BOOL*/ }, + { no: 6, name: "roaming", kind: "scalar", opt: true, T: 8 /*ScalarType.BOOL*/ } + ]); + } + create(value?: PartialMessage): ConnectivityEvent { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + message.connected = false; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: ConnectivityEvent): ConnectivityEvent { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + case /* required bool connected */ 2: + message.connected = reader.bool(); + break; + case /* optional string type */ 3: + message.type = reader.string(); + break; + case /* optional string subtype */ 4: + message.subtype = reader.string(); + break; + case /* optional bool failover */ 5: + message.failover = reader.bool(); + break; + case /* optional bool roaming */ 6: + message.roaming = reader.bool(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: ConnectivityEvent, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + /* required bool connected = 2; */ + if (message.connected !== false) + writer.tag(2, WireType.Varint).bool(message.connected); + /* optional string type = 3; */ + if (message.type !== undefined) + writer.tag(3, WireType.LengthDelimited).string(message.type); + /* optional string subtype = 4; */ + if (message.subtype !== undefined) + writer.tag(4, WireType.LengthDelimited).string(message.subtype); + /* optional bool failover = 5; */ + if (message.failover !== undefined) + writer.tag(5, WireType.Varint).bool(message.failover); + /* optional bool roaming = 6; */ + if (message.roaming !== undefined) + writer.tag(6, WireType.Varint).bool(message.roaming); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message ConnectivityEvent + */ +export const ConnectivityEvent = new ConnectivityEvent$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class PhoneStateEvent$Type extends MessageType { + constructor() { + super("PhoneStateEvent", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "state", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 3, name: "manual", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }, + { no: 4, name: "operator", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): PhoneStateEvent { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + message.state = ""; + message.manual = false; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: PhoneStateEvent): PhoneStateEvent { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + case /* required string state */ 2: + message.state = reader.string(); + break; + case /* required bool manual */ 3: + message.manual = reader.bool(); + break; + case /* optional string operator */ 4: + message.operator = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: PhoneStateEvent, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + /* required string state = 2; */ + if (message.state !== "") + writer.tag(2, WireType.LengthDelimited).string(message.state); + /* required bool manual = 3; */ + if (message.manual !== false) + writer.tag(3, WireType.Varint).bool(message.manual); + /* optional string operator = 4; */ + if (message.operator !== undefined) + writer.tag(4, WireType.LengthDelimited).string(message.operator); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message PhoneStateEvent + */ +export const PhoneStateEvent = new PhoneStateEvent$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class RotationEvent$Type extends MessageType { + constructor() { + super("RotationEvent", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "rotation", kind: "scalar", T: 5 /*ScalarType.INT32*/ }, + { no: 3, name: "width", kind: "scalar", opt: true, T: 5 /*ScalarType.INT32*/ }, + { no: 4, name: "height", kind: "scalar", opt: true, T: 5 /*ScalarType.INT32*/ } + ]); + } + create(value?: PartialMessage): RotationEvent { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + message.rotation = 0; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: RotationEvent): RotationEvent { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + case /* required int32 rotation */ 2: + message.rotation = reader.int32(); + break; + case /* optional int32 width */ 3: + message.width = reader.int32(); + break; + case /* optional int32 height */ 4: + message.height = reader.int32(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: RotationEvent, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + /* required int32 rotation = 2; */ + if (message.rotation !== 0) + writer.tag(2, WireType.Varint).int32(message.rotation); + /* optional int32 width = 3; */ + if (message.width !== undefined) + writer.tag(3, WireType.Varint).int32(message.width); + /* optional int32 height = 4; */ + if (message.height !== undefined) + writer.tag(4, WireType.Varint).int32(message.height); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message RotationEvent + */ +export const RotationEvent = new RotationEvent$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class SetDeviceDisplay$Type extends MessageType { + constructor() { + super("SetDeviceDisplay", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "channel", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 3, name: "width", kind: "scalar", T: 5 /*ScalarType.INT32*/ }, + { no: 4, name: "height", kind: "scalar", T: 5 /*ScalarType.INT32*/ } + ]); + } + create(value?: PartialMessage): SetDeviceDisplay { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + message.channel = ""; + message.width = 0; + message.height = 0; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: SetDeviceDisplay): SetDeviceDisplay { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + case /* required string channel */ 2: + message.channel = reader.string(); + break; + case /* required int32 width */ 3: + message.width = reader.int32(); + break; + case /* required int32 height */ 4: + message.height = reader.int32(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: SetDeviceDisplay, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + /* required string channel = 2; */ + if (message.channel !== "") + writer.tag(2, WireType.LengthDelimited).string(message.channel); + /* required int32 width = 3; */ + if (message.width !== 0) + writer.tag(3, WireType.Varint).int32(message.width); + /* required int32 height = 4; */ + if (message.height !== 0) + writer.tag(4, WireType.Varint).int32(message.height); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message SetDeviceDisplay + */ +export const SetDeviceDisplay = new SetDeviceDisplay$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class IosDevicePorts$Type extends MessageType { + constructor() { + super("IosDevicePorts", [ + { no: 2, name: "screenPort", kind: "scalar", T: 5 /*ScalarType.INT32*/ }, + { no: 3, name: "connectPort", kind: "scalar", T: 5 /*ScalarType.INT32*/ } + ]); + } + create(value?: PartialMessage): IosDevicePorts { + const message = globalThis.Object.create((this.messagePrototype!)); + message.screenPort = 0; + message.connectPort = 0; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: IosDevicePorts): IosDevicePorts { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required int32 screenPort */ 2: + message.screenPort = reader.int32(); + break; + case /* required int32 connectPort */ 3: + message.connectPort = reader.int32(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: IosDevicePorts, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required int32 screenPort = 2; */ + if (message.screenPort !== 0) + writer.tag(2, WireType.Varint).int32(message.screenPort); + /* required int32 connectPort = 3; */ + if (message.connectPort !== 0) + writer.tag(3, WireType.Varint).int32(message.connectPort); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message IosDevicePorts + */ +export const IosDevicePorts = new IosDevicePorts$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class StartStreaming$Type extends MessageType { + constructor() { + super("StartStreaming", [ + { no: 1, name: "port", kind: "scalar", T: 5 /*ScalarType.INT32*/ }, + { no: 2, name: "channel", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): StartStreaming { + const message = globalThis.Object.create((this.messagePrototype!)); + message.port = 0; + message.channel = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: StartStreaming): StartStreaming { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required int32 port */ 1: + message.port = reader.int32(); + break; + case /* required string channel */ 2: + message.channel = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: StartStreaming, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required int32 port = 1; */ + if (message.port !== 0) + writer.tag(1, WireType.Varint).int32(message.port); + /* required string channel = 2; */ + if (message.channel !== "") + writer.tag(2, WireType.LengthDelimited).string(message.channel); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message StartStreaming + */ +export const StartStreaming = new StartStreaming$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class DeleteDevice$Type extends MessageType { + constructor() { + super("DeleteDevice", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): DeleteDevice { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DeleteDevice): DeleteDevice { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: DeleteDevice, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message DeleteDevice + */ +export const DeleteDevice = new DeleteDevice$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class SetAbsentDisconnectedDevices$Type extends MessageType { + constructor() { + super("SetAbsentDisconnectedDevices", []); + } + create(value?: PartialMessage): SetAbsentDisconnectedDevices { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: SetAbsentDisconnectedDevices): SetAbsentDisconnectedDevices { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: SetAbsentDisconnectedDevices, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message SetAbsentDisconnectedDevices + */ +export const SetAbsentDisconnectedDevices = new SetAbsentDisconnectedDevices$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class Applications$Type extends MessageType { + constructor() { + super("Applications", [ + { no: 1, name: "bundleId", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "bundleName", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): Applications { + const message = globalThis.Object.create((this.messagePrototype!)); + message.bundleId = ""; + message.bundleName = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: Applications): Applications { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string bundleId */ 1: + message.bundleId = reader.string(); + break; + case /* required string bundleName */ 2: + message.bundleName = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: Applications, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string bundleId = 1; */ + if (message.bundleId !== "") + writer.tag(1, WireType.LengthDelimited).string(message.bundleId); + /* required string bundleName = 2; */ + if (message.bundleName !== "") + writer.tag(2, WireType.LengthDelimited).string(message.bundleName); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message Applications + */ +export const Applications = new Applications$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class InstalledApplications$Type extends MessageType { + constructor() { + super("InstalledApplications", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "applications", kind: "message", repeat: 2 /*RepeatType.UNPACKED*/, T: () => Applications } + ]); + } + create(value?: PartialMessage): InstalledApplications { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + message.applications = []; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: InstalledApplications): InstalledApplications { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + case /* repeated Applications applications */ 2: + message.applications.push(Applications.internalBinaryRead(reader, reader.uint32(), options)); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: InstalledApplications, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + /* repeated Applications applications = 2; */ + for (let i = 0; i < message.applications.length; i++) + Applications.internalBinaryWrite(message.applications[i], writer.tag(2, WireType.LengthDelimited).fork(), options).join(); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message InstalledApplications + */ +export const InstalledApplications = new InstalledApplications$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class TransportInstalledApps$Type extends MessageType { + constructor() { + super("TransportInstalledApps", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): TransportInstalledApps { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: TransportInstalledApps): TransportInstalledApps { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: TransportInstalledApps, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message TransportInstalledApps + */ +export const TransportInstalledApps = new TransportInstalledApps$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class SetDeviceApp$Type extends MessageType { + constructor() { + super("SetDeviceApp", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "bundleId", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 3, name: "bundleName", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 4, name: "pathToApp", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): SetDeviceApp { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + message.bundleId = ""; + message.bundleName = ""; + message.pathToApp = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: SetDeviceApp): SetDeviceApp { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + case /* required string bundleId */ 2: + message.bundleId = reader.string(); + break; + case /* required string bundleName */ 3: + message.bundleName = reader.string(); + break; + case /* required string pathToApp */ 4: + message.pathToApp = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: SetDeviceApp, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + /* required string bundleId = 2; */ + if (message.bundleId !== "") + writer.tag(2, WireType.LengthDelimited).string(message.bundleId); + /* required string bundleName = 3; */ + if (message.bundleName !== "") + writer.tag(3, WireType.LengthDelimited).string(message.bundleName); + /* required string pathToApp = 4; */ + if (message.pathToApp !== "") + writer.tag(4, WireType.LengthDelimited).string(message.pathToApp); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message SetDeviceApp + */ +export const SetDeviceApp = new SetDeviceApp$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class GetIosDeviceApps$Type extends MessageType { + constructor() { + super("GetIosDeviceApps", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "bundleId", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 3, name: "bundleName", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 4, name: "pathToApp", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): GetIosDeviceApps { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + message.bundleId = ""; + message.bundleName = ""; + message.pathToApp = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: GetIosDeviceApps): GetIosDeviceApps { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + case /* required string bundleId */ 2: + message.bundleId = reader.string(); + break; + case /* required string bundleName */ 3: + message.bundleName = reader.string(); + break; + case /* required string pathToApp */ 4: + message.pathToApp = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: GetIosDeviceApps, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + /* required string bundleId = 2; */ + if (message.bundleId !== "") + writer.tag(2, WireType.LengthDelimited).string(message.bundleId); + /* required string bundleName = 3; */ + if (message.bundleName !== "") + writer.tag(3, WireType.LengthDelimited).string(message.bundleName); + /* required string pathToApp = 4; */ + if (message.pathToApp !== "") + writer.tag(4, WireType.LengthDelimited).string(message.pathToApp); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message GetIosDeviceApps + */ +export const GetIosDeviceApps = new GetIosDeviceApps$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class TransationGetMessage$Type extends MessageType { + constructor() { + super("TransationGetMessage", [ + { no: 1, name: "source", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "data", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): TransationGetMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.source = ""; + message.data = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: TransationGetMessage): TransationGetMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string source */ 1: + message.source = reader.string(); + break; + case /* required string data */ 2: + message.data = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: TransationGetMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string source = 1; */ + if (message.source !== "") + writer.tag(1, WireType.LengthDelimited).string(message.source); + /* required string data = 2; */ + if (message.data !== "") + writer.tag(2, WireType.LengthDelimited).string(message.data); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message TransationGetMessage + */ +export const TransationGetMessage = new TransationGetMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class GetInstalledApplications$Type extends MessageType { + constructor() { + super("GetInstalledApplications", []); + } + create(value?: PartialMessage): GetInstalledApplications { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: GetInstalledApplications): GetInstalledApplications { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: GetInstalledApplications, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message GetInstalledApplications + */ +export const GetInstalledApplications = new GetInstalledApplications$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class UpdateIosDevice$Type extends MessageType { + constructor() { + super("UpdateIosDevice", [ + { no: 1, name: "id", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "name", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 3, name: "platform", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 4, name: "architecture", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 5, name: "sdk", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 6, name: "service", kind: "message", T: () => IosServiceMessage } + ]); + } + create(value?: PartialMessage): UpdateIosDevice { + const message = globalThis.Object.create((this.messagePrototype!)); + message.id = ""; + message.name = ""; + message.platform = ""; + message.architecture = ""; + message.sdk = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: UpdateIosDevice): UpdateIosDevice { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string id */ 1: + message.id = reader.string(); + break; + case /* required string name */ 2: + message.name = reader.string(); + break; + case /* required string platform */ 3: + message.platform = reader.string(); + break; + case /* required string architecture */ 4: + message.architecture = reader.string(); + break; + case /* required string sdk */ 5: + message.sdk = reader.string(); + break; + case /* required IosServiceMessage service */ 6: + message.service = IosServiceMessage.internalBinaryRead(reader, reader.uint32(), options, message.service); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: UpdateIosDevice, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string id = 1; */ + if (message.id !== "") + writer.tag(1, WireType.LengthDelimited).string(message.id); + /* required string name = 2; */ + if (message.name !== "") + writer.tag(2, WireType.LengthDelimited).string(message.name); + /* required string platform = 3; */ + if (message.platform !== "") + writer.tag(3, WireType.LengthDelimited).string(message.platform); + /* required string architecture = 4; */ + if (message.architecture !== "") + writer.tag(4, WireType.LengthDelimited).string(message.architecture); + /* required string sdk = 5; */ + if (message.sdk !== "") + writer.tag(5, WireType.LengthDelimited).string(message.sdk); + /* required IosServiceMessage service = 6; */ + if (message.service) + IosServiceMessage.internalBinaryWrite(message.service, writer.tag(6, WireType.LengthDelimited).fork(), options).join(); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message UpdateIosDevice + */ +export const UpdateIosDevice = new UpdateIosDevice$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class SdkIosVersion$Type extends MessageType { + constructor() { + super("SdkIosVersion", [ + { no: 1, name: "id", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "sdkVersion", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): SdkIosVersion { + const message = globalThis.Object.create((this.messagePrototype!)); + message.id = ""; + message.sdkVersion = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: SdkIosVersion): SdkIosVersion { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string id */ 1: + message.id = reader.string(); + break; + case /* required string sdkVersion */ 2: + message.sdkVersion = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: SdkIosVersion, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string id = 1; */ + if (message.id !== "") + writer.tag(1, WireType.LengthDelimited).string(message.id); + /* required string sdkVersion = 2; */ + if (message.sdkVersion !== "") + writer.tag(2, WireType.LengthDelimited).string(message.sdkVersion); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message SdkIosVersion + */ +export const SdkIosVersion = new SdkIosVersion$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class SizeIosDevice$Type extends MessageType { + constructor() { + super("SizeIosDevice", [ + { no: 1, name: "id", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "height", kind: "scalar", T: 1 /*ScalarType.DOUBLE*/ }, + { no: 3, name: "width", kind: "scalar", T: 1 /*ScalarType.DOUBLE*/ }, + { no: 4, name: "scale", kind: "scalar", T: 5 /*ScalarType.INT32*/ } + ]); + } + create(value?: PartialMessage): SizeIosDevice { + const message = globalThis.Object.create((this.messagePrototype!)); + message.id = ""; + message.height = 0; + message.width = 0; + message.scale = 0; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: SizeIosDevice): SizeIosDevice { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string id */ 1: + message.id = reader.string(); + break; + case /* required double height */ 2: + message.height = reader.double(); + break; + case /* required double width */ 3: + message.width = reader.double(); + break; + case /* required int32 scale */ 4: + message.scale = reader.int32(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: SizeIosDevice, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string id = 1; */ + if (message.id !== "") + writer.tag(1, WireType.LengthDelimited).string(message.id); + /* required double height = 2; */ + if (message.height !== 0) + writer.tag(2, WireType.Bit64).double(message.height); + /* required double width = 3; */ + if (message.width !== 0) + writer.tag(3, WireType.Bit64).double(message.width); + /* required int32 scale = 4; */ + if (message.scale !== 0) + writer.tag(4, WireType.Varint).int32(message.scale); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message SizeIosDevice + */ +export const SizeIosDevice = new SizeIosDevice$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class DashboardOpenMessage$Type extends MessageType { + constructor() { + super("DashboardOpenMessage", []); + } + create(value?: PartialMessage): DashboardOpenMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DashboardOpenMessage): DashboardOpenMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: DashboardOpenMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message DashboardOpenMessage + */ +export const DashboardOpenMessage = new DashboardOpenMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class GetIosTreeElements$Type extends MessageType { + constructor() { + super("GetIosTreeElements", []); + } + create(value?: PartialMessage): GetIosTreeElements { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: GetIosTreeElements): GetIosTreeElements { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: GetIosTreeElements, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message GetIosTreeElements + */ +export const GetIosTreeElements = new GetIosTreeElements$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class TapDeviceTreeElement$Type extends MessageType { + constructor() { + super("TapDeviceTreeElement", [ + { no: 1, name: "label", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): TapDeviceTreeElement { + const message = globalThis.Object.create((this.messagePrototype!)); + message.label = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: TapDeviceTreeElement): TapDeviceTreeElement { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string label */ 1: + message.label = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: TapDeviceTreeElement, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string label = 1; */ + if (message.label !== "") + writer.tag(1, WireType.LengthDelimited).string(message.label); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message TapDeviceTreeElement + */ +export const TapDeviceTreeElement = new TapDeviceTreeElement$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class TemporarilyUnavailableMessage$Type extends MessageType { + constructor() { + super("TemporarilyUnavailableMessage", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): TemporarilyUnavailableMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: TemporarilyUnavailableMessage): TemporarilyUnavailableMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: TemporarilyUnavailableMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message TemporarilyUnavailableMessage + */ +export const TemporarilyUnavailableMessage = new TemporarilyUnavailableMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class UpdateRemoteConnectUrl$Type extends MessageType { + constructor() { + super("UpdateRemoteConnectUrl", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): UpdateRemoteConnectUrl { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: UpdateRemoteConnectUrl): UpdateRemoteConnectUrl { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: UpdateRemoteConnectUrl, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message UpdateRemoteConnectUrl + */ +export const UpdateRemoteConnectUrl = new UpdateRemoteConnectUrl$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class IosServiceMessage$Type extends MessageType { + constructor() { + super("IosServiceMessage", [ + { no: 1, name: "hasAPNS", kind: "scalar", T: 8 /*ScalarType.BOOL*/ } + ]); + } + create(value?: PartialMessage): IosServiceMessage { + const message = globalThis.Object.create((this.messagePrototype!)); + message.hasAPNS = false; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: IosServiceMessage): IosServiceMessage { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required bool hasAPNS */ 1: + message.hasAPNS = reader.bool(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: IosServiceMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required bool hasAPNS = 1; */ + if (message.hasAPNS !== false) + writer.tag(1, WireType.Varint).bool(message.hasAPNS); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message IosServiceMessage + */ +export const IosServiceMessage = new IosServiceMessage$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class LaunchDeviceApp$Type extends MessageType { + constructor() { + super("LaunchDeviceApp", [ + { no: 1, name: "pkg", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): LaunchDeviceApp { + const message = globalThis.Object.create((this.messagePrototype!)); + message.pkg = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: LaunchDeviceApp): LaunchDeviceApp { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string pkg */ 1: + message.pkg = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: LaunchDeviceApp, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string pkg = 1; */ + if (message.pkg !== "") + writer.tag(1, WireType.LengthDelimited).string(message.pkg); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message LaunchDeviceApp + */ +export const LaunchDeviceApp = new LaunchDeviceApp$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class TerminateDeviceApp$Type extends MessageType { + constructor() { + super("TerminateDeviceApp", []); + } + create(value?: PartialMessage): TerminateDeviceApp { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: TerminateDeviceApp): TerminateDeviceApp { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: TerminateDeviceApp, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message TerminateDeviceApp + */ +export const TerminateDeviceApp = new TerminateDeviceApp$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class KillDeviceApp$Type extends MessageType { + constructor() { + super("KillDeviceApp", []); + } + create(value?: PartialMessage): KillDeviceApp { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: KillDeviceApp): KillDeviceApp { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: KillDeviceApp, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message KillDeviceApp + */ +export const KillDeviceApp = new KillDeviceApp$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class GetAppAsset$Type extends MessageType { + constructor() { + super("GetAppAsset", [ + { no: 1, name: "url", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): GetAppAsset { + const message = globalThis.Object.create((this.messagePrototype!)); + message.url = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: GetAppAsset): GetAppAsset { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string url */ 1: + message.url = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: GetAppAsset, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string url = 1; */ + if (message.url !== "") + writer.tag(1, WireType.LengthDelimited).string(message.url); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message GetAppAsset + */ +export const GetAppAsset = new GetAppAsset$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class GetAppAssetsList$Type extends MessageType { + constructor() { + super("GetAppAssetsList", []); + } + create(value?: PartialMessage): GetAppAssetsList { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: GetAppAssetsList): GetAppAssetsList { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: GetAppAssetsList, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message GetAppAssetsList + */ +export const GetAppAssetsList = new GetAppAssetsList$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class GetAppHTML$Type extends MessageType { + constructor() { + super("GetAppHTML", []); + } + create(value?: PartialMessage): GetAppHTML { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: GetAppHTML): GetAppHTML { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: GetAppHTML, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message GetAppHTML + */ +export const GetAppHTML = new GetAppHTML$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class GetAppInspectServerUrl$Type extends MessageType { + constructor() { + super("GetAppInspectServerUrl", []); + } + create(value?: PartialMessage): GetAppInspectServerUrl { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: GetAppInspectServerUrl): GetAppInspectServerUrl { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: GetAppInspectServerUrl, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message GetAppInspectServerUrl + */ +export const GetAppInspectServerUrl = new GetAppInspectServerUrl$Type(); diff --git a/package-lock.json b/package-lock.json index 495b9cffc9..18b4acae70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "@irdk/usbmux": "^0.2.2", "@julusian/jpeg-turbo": "2.1.0", "@node-saml/passport-saml": "5.1.0", + "@protobuf-ts/plugin": "^2.11.1", "@sentry/node": "^8.34.0", "@types/chrome-remote-interface": "^0.31.14", "@u4/adbkit": "^5.1.7", @@ -97,7 +98,6 @@ "temp": "0.9.4", "tmp-promise": "^3.0.3", "transliteration": "2.2.0", - "ts-proto": "^2.6.1", "tsx": "4.20.3", "underscore.string": "3.3.6", "url-join": "1.1.0", @@ -117,7 +117,7 @@ }, "devDependencies": { "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "^9.30.1", + "@eslint/js": "^9.33.0", "@playwright/test": "^1.52.0", "@types/bluebird": "^3.5.42", "@types/chalk": "^0.4.31", @@ -133,14 +133,14 @@ "async": "2.6.4", "cli-docs-generator": "1.0.7", "esbuild": "0.25.8", - "eslint": "9.30.1", + "eslint": "^9.30.1", "event-stream": "3.3.5", "exports-loader": "^4.0.0", "fs-extra": "8.1.0", "globals": "^15.11.0", "http-https": "1.0.0", "imports-loader": "^4.0.1", - "typescript": "^5.5.3", + "typescript": "^5.9.2", "typescript-eslint": "^8.39.1" }, "engines": { @@ -1276,11 +1276,35 @@ } }, "node_modules/@bufbuild/protobuf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.6.2.tgz", - "integrity": "sha512-vLu7SRY84CV/Dd+NUdgtidn2hS5hSMUC1vDBY0VcviTdgRYkU43vIz3vIFbmx14cX1r+mM7WjzE5Fl1fGEM0RQ==", + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.6.3.tgz", + "integrity": "sha512-w/gJKME9mYN7ZoUAmSMAWXk4hkVpxRKvEJCb3dV5g9wwWdxTJJ0ayOJAVcNxtdqaxDyFuC0uz4RSGVacJ030PQ==", "license": "(Apache-2.0 AND BSD-3-Clause)" }, + "node_modules/@bufbuild/protoplugin": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/@bufbuild/protoplugin/-/protoplugin-2.6.3.tgz", + "integrity": "sha512-VceMuxeRukxGeABfo34SXq0VqY1MU+mzS+PBf0HAWo97ylFut8F6sQ3mV0tKiM08UQ/xQco7lxCn83BkoxrWrA==", + "license": "Apache-2.0", + "dependencies": { + "@bufbuild/protobuf": "2.6.3", + "@typescript/vfs": "^1.5.2", + "typescript": "5.4.5" + } + }, + "node_modules/@bufbuild/protoplugin/node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/@devicefarmer/adbkit-apkreader": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@devicefarmer/adbkit-apkreader/-/adbkit-apkreader-3.2.4.tgz", @@ -1497,70 +1521,6 @@ "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==", "license": "MIT" }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", - "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", - "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", - "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", - "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@esbuild/darwin-arm64": { "version": "0.25.8", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", @@ -1577,342 +1537,6 @@ "node": ">=18" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", - "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", - "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", - "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", - "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", - "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", - "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", - "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", - "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", - "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", - "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", - "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", - "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", - "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", - "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", - "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", - "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", - "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", - "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", - "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", - "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", - "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", @@ -2031,9 +1655,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.32.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz", - "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==", + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz", + "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", "dev": true, "license": "MIT", "engines": { @@ -3692,6 +3316,61 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@protobuf-ts/plugin": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/plugin/-/plugin-2.11.1.tgz", + "integrity": "sha512-HyuprDcw0bEEJqkOWe1rnXUP0gwYLij8YhPuZyZk6cJbIgc/Q0IFgoHQxOXNIXAcXM4Sbehh6kjVnCzasElw1A==", + "license": "Apache-2.0", + "dependencies": { + "@bufbuild/protobuf": "^2.4.0", + "@bufbuild/protoplugin": "^2.4.0", + "@protobuf-ts/protoc": "^2.11.1", + "@protobuf-ts/runtime": "^2.11.1", + "@protobuf-ts/runtime-rpc": "^2.11.1", + "typescript": "^3.9" + }, + "bin": { + "protoc-gen-dump": "bin/protoc-gen-dump", + "protoc-gen-ts": "bin/protoc-gen-ts" + } + }, + "node_modules/@protobuf-ts/plugin/node_modules/typescript": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz", + "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/@protobuf-ts/protoc": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/protoc/-/protoc-2.11.1.tgz", + "integrity": "sha512-mUZJaV0daGO6HUX90o/atzQ6A7bbN2RSuHtdwo8SSF2Qoe3zHwa4IHyCN1evftTeHfLmdz+45qo47sL+5P8nyg==", + "license": "Apache-2.0", + "bin": { + "protoc": "protoc.js" + } + }, + "node_modules/@protobuf-ts/runtime": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/runtime/-/runtime-2.11.1.tgz", + "integrity": "sha512-KuDaT1IfHkugM2pyz+FwiY80ejWrkH1pAtOBOZFuR6SXEFTsnb/jiQWQ1rCIrcKx2BtyxnxW6BWwsVSA/Ie+WQ==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@protobuf-ts/runtime-rpc": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/runtime-rpc/-/runtime-rpc-2.11.1.tgz", + "integrity": "sha512-4CqqUmNA+/uMz00+d3CYKgElXO9VrEbucjnBFEjqI4GuDrEQ32MaI3q+9qPBvIGOlL4PmHXrzM32vBPWRhQKWQ==", + "license": "Apache-2.0", + "dependencies": { + "@protobuf-ts/runtime": "^2.11.1" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -5466,6 +5145,18 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript/vfs": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@typescript/vfs/-/vfs-1.6.1.tgz", + "integrity": "sha512-JwoxboBh7Oz1v38tPbkrZ62ZXNHAk9bJ7c9x0eI5zBfBnBYGhURdbnh7Z4smN/MV48Y5OCcZb58n972UtbazsA==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1" + }, + "peerDependencies": { + "typescript": "*" + } + }, "node_modules/@u4/adbkit": { "version": "5.1.7", "resolved": "https://registry.npmjs.org/@u4/adbkit/-/adbkit-5.1.7.tgz", @@ -14501,9 +14192,9 @@ } }, "node_modules/ts-proto": { - "version": "2.7.5", - "resolved": "https://registry.npmjs.org/ts-proto/-/ts-proto-2.7.5.tgz", - "integrity": "sha512-FoRxSaNW+P3m+GiXIZjUjhaHXT67Ah4zMGKzn4yklbGRQTS+PqpUhKo5AJnwfUDUByjEUG7ch36byFUYWRH9Nw==", + "version": "2.7.7", + "resolved": "https://registry.npmjs.org/ts-proto/-/ts-proto-2.7.7.tgz", + "integrity": "sha512-/OfN9/Yriji2bbpOysZ/Jzc96isOKz+eBTJEcKaIZ0PR6x1TNgVm4Lz0zfbo+J0jwFO7fJjJyssefBPQ0o1V9A==", "license": "ISC", "dependencies": { "@bufbuild/protobuf": "^2.0.0", @@ -14605,10 +14296,9 @@ } }, "node_modules/typescript": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", - "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", - "dev": true, + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/package.json b/package.json index 44e205fe0b..250090d328 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "build:swagger:tests": "cd ./test/api && poetry run just regen-schema", "build:swagger:ui": "cd ./ui && npm run generate-api", "build:swagger": "cd ./lib/units/api && python3 ./gen_routes.py & npm run build:swagger:ui", + "protoc": "npx protoc --ts_out ./lib/wire --ts_opt generate_dependencies --proto_path ./lib/wire ./lib/wire/wire.proto", "build": "tsc", "dev": "tsx ./bin/devstf.mjs" }, @@ -44,6 +45,7 @@ "@irdk/usbmux": "^0.2.2", "@julusian/jpeg-turbo": "2.1.0", "@node-saml/passport-saml": "5.1.0", + "@protobuf-ts/plugin": "^2.11.1", "@sentry/node": "^8.34.0", "@types/chrome-remote-interface": "^0.31.14", "@u4/adbkit": "^5.1.7", @@ -119,7 +121,6 @@ "temp": "0.9.4", "tmp-promise": "^3.0.3", "transliteration": "2.2.0", - "ts-proto": "^2.6.1", "tsx": "4.20.3", "underscore.string": "3.3.6", "url-join": "1.1.0", @@ -134,7 +135,7 @@ }, "devDependencies": { "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "^9.30.1", + "@eslint/js": "^9.33.0", "@playwright/test": "^1.52.0", "@types/bluebird": "^3.5.42", "@types/chalk": "^0.4.31", @@ -150,14 +151,14 @@ "async": "2.6.4", "cli-docs-generator": "1.0.7", "esbuild": "0.25.8", - "eslint": "9.30.1", + "eslint": "^9.30.1", "event-stream": "3.3.5", "exports-loader": "^4.0.0", "fs-extra": "8.1.0", "globals": "^15.11.0", "http-https": "1.0.0", "imports-loader": "^4.0.1", - "typescript": "^5.5.3", + "typescript": "^5.9.2", "typescript-eslint": "^8.39.1" }, "overrides": { @@ -189,4 +190,4 @@ "pre-commit": "true" } } -} +} \ No newline at end of file diff --git a/ui/src/store/device-connection.ts b/ui/src/store/device-connection.ts index d8060c09be..e14f1fe52c 100644 --- a/ui/src/store/device-connection.ts +++ b/ui/src/store/device-connection.ts @@ -30,9 +30,9 @@ export class DeviceConnection { async useDevice(): Promise { const device = await this.deviceBySerialStore.fetch() - if (!device?.channel) return - try { + if (!device?.channel) throw new Error('Device is not cooperating.') + const startRemoteConnectResult = await this.deviceControlStore.startRemoteConnect() startRemoteConnectResult.donePromise.then(({ data }) => { From 0fa7a73119ade19ebe433da9fdeefada65321306 Mon Sep 17 00:00:00 2001 From: Elmir Khalilov <52529096+e-khalilov@users.noreply.github.com> Date: Fri, 12 Sep 2025 21:19:41 +0300 Subject: [PATCH 02/23] hotfix (#361) Co-authored-by: e.khalilov --- lib/units/device/plugins/service.ts | 30 +++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/lib/units/device/plugins/service.ts b/lib/units/device/plugins/service.ts index 7971808d8c..5a6d5bcafe 100644 --- a/lib/units/device/plugins/service.ts +++ b/lib/units/device/plugins/service.ts @@ -16,6 +16,15 @@ import service from '../resources/service.js' import {Duplex} from 'node:stream' import EventEmitter from 'events' import {GRPC_WAIT_TIMEOUT} from '../../../util/apiutil.js' +import { + PhysicalIdentifyMessage, + KeyDownMessage, + KeyUpMessage, + KeyPressMessage, + TypeMessage, + RotateMessage, + UnlockDeviceMessage +} from "../../../wire/wire.js"; interface Service { socket: Duplex | null @@ -230,7 +239,6 @@ export default syrup.serial() getDisplay = (id: string) => runServiceCommand(apk.wire.MessageType.GET_DISPLAY, new apk.wire.GetDisplayRequest(id)) .then((data) => { - log.info('DISPLAY RESPONSE !') const response = apk.wire.GetDisplayResponse.decode(data) if (response.success) { return { @@ -367,8 +375,10 @@ export default syrup.serial() } const mapped = response.properties.reduce( - (acc: any, property: any) => - acc[property.name] = property.value, {} + (acc: any, property: any) => { + acc[property.name] = property.value + return acc + }, {} ) if (mapped.imei) { return mapped @@ -636,14 +646,14 @@ export default syrup.serial() await openService() router - .on(wire.PhysicalIdentifyMessage, (channel) => { + .on(PhysicalIdentifyMessage, (channel) => { plugin.identity() push.send([ channel, wireutil.reply(options.serial).okay() ]) }) - .on(wire.KeyDownMessage, (channel, message) => { + .on(KeyDownMessage, (channel, message) => { try { keyEvent({ event: apk.wire.KeyEvent.DOWN, @@ -654,7 +664,7 @@ export default syrup.serial() log.warn(e.message) } }) - .on(wire.KeyUpMessage, (channel, message) => { + .on(KeyUpMessage, (channel, message) => { try { keyEvent({ event: apk.wire.KeyEvent.UP, @@ -665,7 +675,7 @@ export default syrup.serial() log.warn(e.message) } }) - .on(wire.KeyPressMessage, (channel, message) => { + .on(KeyPressMessage, (channel, message) => { try { keyEvent({ event: apk.wire.KeyEvent.PRESS, @@ -676,13 +686,13 @@ export default syrup.serial() log.warn(e.message) } }) - .on(wire.TypeMessage, (channel, message) => + .on(TypeMessage, (channel, message) => plugin.type(message.text) ) - .on(wire.RotateMessage, (channel, message) => + .on(RotateMessage, (channel, message) => plugin.rotate(message.rotation) ) - .on(wire.UnlockDeviceMessage, (channel, message) => + .on(UnlockDeviceMessage, (channel, message) => plugin.unlockDevice() ) return plugin From af9350ec83647521cd9f74ced033b0cd1e75a5ad Mon Sep 17 00:00:00 2001 From: Elmir Khalilov <52529096+e-khalilov@users.noreply.github.com> Date: Tue, 23 Sep 2025 16:02:53 +0300 Subject: [PATCH 03/23] Remove all DB connections from device side (#368) * hotfix imei * Decrease apt install list in dockerfile (#365) * remove gm and jdk11 * remove libs * Remove console-feed & react dependencies [backend only] (#366) * remove dep console-feed * minor fix * linter fix --------- Co-authored-by: e.khalilov * fix default quotas hierarchy QA-19255 (#367) * -fix quotas -fix lock -fix test -fix lint * -fix test --------- Co-authored-by: a.chistov * dev units without db conn * fix types * minor fix * minor fix * minor fix * remove useless sockets --------- Co-authored-by: Maksim Alzhanov Co-authored-by: Maxim Co-authored-by: Daniil <8039921+DaniilSmirnov@users.noreply.github.com> Co-authored-by: e.khalilov Co-authored-by: Alexey Chistov <33050834+Alk2017@users.noreply.github.com> Co-authored-by: a.chistov --- Dockerfile | 6 - docker-compose-macos.yaml | 8 +- docker-compose-prod.yaml | 6 +- lib/cli/doctor/index.js | 8 - lib/cli/groups-engine/index.js | 14 - lib/cli/local/index.js | 8 +- lib/cli/processor/index.js | 28 - lib/db/index.ts | 95 ++- lib/db/models/all/model.js | 103 +-- lib/units/api/controllers/user.js | 19 +- lib/units/api/controllers/users.js | 10 +- lib/units/api/helpers/useDevice.js | 5 +- lib/units/base-device/plugins/group.js | 156 ----- lib/units/base-device/plugins/group.ts | 252 ++++++++ .../device/plugins/{connect.js => connect.ts} | 119 ++-- lib/units/device/plugins/group.js | 214 ------- lib/units/device/plugins/group.ts | 96 +++ lib/units/device/plugins/service.ts | 2 +- lib/units/groups-engine/index.js | 6 +- lib/units/groups-engine/watchers/devices.js | 2 +- lib/units/ios-device/plugins/info/index.js | 3 + lib/units/ios-device/plugins/wda/client.js | 49 +- lib/units/processor/{index.js => index.ts} | 227 ++++--- lib/units/provider/ADBObserver.ts | 18 +- lib/units/provider/index.ts | 6 +- lib/units/reaper/index.js | 109 ---- lib/units/reaper/index.ts | 120 ++++ lib/units/tizen-device/plugins/launcher.js | 2 +- .../plugins/webinspector/Replicator.ts | 588 ++++++++++++++++++ .../index.ts} | 111 ++-- .../plugins/webinspector/transform/BigInt.ts | 23 + .../webinspector/transform/Function.ts | 85 +++ .../plugins/webinspector/transform/HTML.ts | 93 +++ .../plugins/webinspector/transform/Map.ts | 59 ++ .../webinspector/transform/arithmetic.ts | 48 ++ .../plugins/webinspector/transform/index.ts | 17 + lib/units/websocket/index.js | 13 +- lib/util/grouputil.js | 60 -- lib/util/grouputil.ts | 67 ++ lib/util/lifecycle.js | 0 lib/util/ttlset.js | 94 --- lib/util/ttlset.ts | 113 ++++ lib/wire/router.ts | 2 +- lib/wire/transmanager.js | 58 -- lib/wire/transmanager.ts | 122 ++++ lib/wire/wire.proto | 16 +- lib/wire/wire.ts | 209 ++++++- package-lock.json | 381 +----------- package.json | 2 +- test/api/conftest.py | 18 + .../users/test_user_lifecycle_management.py | 8 +- tsconfig.json | 5 +- 52 files changed, 2354 insertions(+), 1529 deletions(-) create mode 100755 lib/units/base-device/plugins/group.ts rename lib/units/device/plugins/{connect.js => connect.ts} (54%) create mode 100644 lib/units/device/plugins/group.ts rename lib/units/processor/{index.js => index.ts} (57%) delete mode 100644 lib/units/reaper/index.js create mode 100644 lib/units/reaper/index.ts create mode 100644 lib/units/tizen-device/plugins/webinspector/Replicator.ts rename lib/units/tizen-device/plugins/{webinspector.js => webinspector/index.ts} (67%) create mode 100644 lib/units/tizen-device/plugins/webinspector/transform/BigInt.ts create mode 100644 lib/units/tizen-device/plugins/webinspector/transform/Function.ts create mode 100644 lib/units/tizen-device/plugins/webinspector/transform/HTML.ts create mode 100644 lib/units/tizen-device/plugins/webinspector/transform/Map.ts create mode 100644 lib/units/tizen-device/plugins/webinspector/transform/arithmetic.ts create mode 100644 lib/units/tizen-device/plugins/webinspector/transform/index.ts delete mode 100644 lib/util/grouputil.js create mode 100644 lib/util/grouputil.ts create mode 100644 lib/util/lifecycle.js delete mode 100644 lib/util/ttlset.js create mode 100644 lib/util/ttlset.ts delete mode 100644 lib/wire/transmanager.js create mode 100644 lib/wire/transmanager.ts diff --git a/Dockerfile b/Dockerfile index 468f7ee0b3..eac6f90dca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,13 +5,7 @@ WORKDIR /app RUN apt-get update && apt-get install -y \ python3 \ - build-essential \ git \ - graphicsmagick \ - openjdk-11-jdk \ - yasm \ - libzmq3-dev \ - libprotobuf-dev \ && rm -rf /var/lib/apt/lists/* COPY . . diff --git a/docker-compose-macos.yaml b/docker-compose-macos.yaml index 7bf9ca00b1..f61f37477b 100644 --- a/docker-compose-macos.yaml +++ b/docker-compose-macos.yaml @@ -108,7 +108,7 @@ services: container_name: devicehub-processor env_file: - scripts/variables.env - command: stf processor --name processor --connect-app-dealer tcp://devicehub-triproxy-app:7160 --connect-dev-dealer tcp://devicehub-triproxy-dev:7260 --connect-sub tcp://devicehub-triproxy-app:7150 --connect-push tcp://devicehub-triproxy-app:7170 --connect-sub-dev tcp://devicehub-triproxy-dev:7250 --connect-push-dev tcp://devicehub-triproxy-dev:7270 + command: stf processor --name processor --connect-app-dealer tcp://devicehub-triproxy-app:7160 --connect-dev-dealer tcp://devicehub-triproxy-dev:7260 depends_on: devicehub-migrate: condition: service_completed_successfully @@ -121,7 +121,7 @@ services: container_name: devicehub-reaper env_file: - scripts/variables.env - command: stf reaper --name reaper001 --connect-push tcp://devicehub-triproxy-dev:7270 --connect-sub tcp://devicehub-triproxy-app:7150 --heartbeat-timeout 30000 + command: stf reaper --name reaper001 --connect-push tcp://devicehub-triproxy-dev:7270 --connect-sub tcp://devicehub-triproxy-dev:7250 --heartbeat-timeout 30000 depends_on: devicehub-migrate: condition: service_completed_successfully @@ -233,7 +233,7 @@ services: container_name: devicehub-api-groups-engine env_file: - scripts/variables.env - command: stf groups-engine --connect-sub tcp://devicehub-triproxy-app:7150 --connect-push tcp://devicehub-triproxy-app:7170 --connect-sub-dev tcp://devicehub-triproxy-dev:7250 --connect-push-dev tcp://devicehub-triproxy-dev:7270 + command: stf groups-engine --connect-push tcp://devicehub-triproxy-app:7170 --connect-push-dev tcp://devicehub-triproxy-dev:7270 depends_on: devicehub-migrate: condition: service_completed_successfully @@ -263,4 +263,4 @@ services: - certs:/certs:rw volumes: devicehub-db-volume: - certs: \ No newline at end of file + certs: diff --git a/docker-compose-prod.yaml b/docker-compose-prod.yaml index 0f3dd25b53..acdd35dc65 100644 --- a/docker-compose-prod.yaml +++ b/docker-compose-prod.yaml @@ -124,7 +124,7 @@ services: container_name: devicehub-processor env_file: - scripts/variables.env - command: stf processor --name processor --connect-app-dealer tcp://devicehub-triproxy-app:7160 --connect-dev-dealer tcp://devicehub-triproxy-dev:7260 --connect-sub tcp://devicehub-triproxy-app:7150 --connect-push tcp://devicehub-triproxy-app:7170 --connect-sub-dev tcp://devicehub-triproxy-dev:7250 --connect-push-dev tcp://devicehub-triproxy-dev:7270 + command: stf processor --name processor --connect-app-dealer tcp://devicehub-triproxy-app:7160 --connect-dev-dealer tcp://devicehub-triproxy-dev:7260 depends_on: devicehub-migrate: condition: service_completed_successfully @@ -137,7 +137,7 @@ services: container_name: devicehub-reaper env_file: - scripts/variables.env - command: stf reaper --name reaper001 --connect-push tcp://devicehub-triproxy-dev:7270 --connect-sub tcp://devicehub-triproxy-app:7150 --heartbeat-timeout 30000 + command: stf reaper --name reaper001 --connect-push tcp://devicehub-triproxy-dev:7270 --connect-sub tcp://devicehub-triproxy-dev:7250 --heartbeat-timeout 30000 depends_on: devicehub-migrate: condition: service_completed_successfully @@ -249,7 +249,7 @@ services: container_name: devicehub-api-groups-engine env_file: - scripts/variables.env - command: stf groups-engine --connect-sub tcp://devicehub-triproxy-app:7150 --connect-push tcp://devicehub-triproxy-app:7170 --connect-sub-dev tcp://devicehub-triproxy-dev:7250 --connect-push-dev tcp://devicehub-triproxy-dev:7270 + command: stf groups-engine --connect-push tcp://devicehub-triproxy-app:7170 --connect-push-dev tcp://devicehub-triproxy-dev:7270 depends_on: devicehub-migrate: condition: service_completed_successfully diff --git a/lib/cli/doctor/index.js b/lib/cli/doctor/index.js index 11c35c1412..f698d2bc13 100644 --- a/lib/cli/doctor/index.js +++ b/lib/cli/doctor/index.js @@ -111,13 +111,6 @@ export const handler = function() { return checker.version(pkg.externalDependencies.monogdb)(pkg.dependencies.mongodb) }) } - function checkGraphicsMagick() { - return check('GraphicsMagick', function(checker) { - return checker.call('gm', ['-version']) - .then(checker.extract('version', /GraphicsMagick ([^\s]+)/)) - .then(checker.version(pkg.externalDependencies.gm)) - }) - } function checkZeroMQ() { return check('ZeroMQ', function(checker) { return checker.version(pkg.externalDependencies.zeromq)(zmq.version) @@ -143,7 +136,6 @@ export const handler = function() { checkOSRelease(), checkNodeVersion(), checkLocalMongoDBVersion(), - checkGraphicsMagick(), checkZeroMQ(), checkProtoBuf(), checkADB() diff --git a/lib/cli/groups-engine/index.js b/lib/cli/groups-engine/index.js index 62abfefb87..e523713502 100644 --- a/lib/cli/groups-engine/index.js +++ b/lib/cli/groups-engine/index.js @@ -11,24 +11,12 @@ export const builder = function(yargs) { array: true, demand: true }) - .option('connect-sub', { - alias: 'u', - describe: 'App-side ZeroMQ PUB endpoint to connect to.', - array: true, - demand: true - }) .option('connect-push-dev', { alias: 'pd', describe: 'Device-side ZeroMQ PULL endpoint to connect to.', array: true, demand: true }) - .option('connect-sub-dev', { - alias: 'sd', - describe: 'Device-side ZeroMQ PUB endpoint to connect to.', - array: true, - demand: true - }) .epilog('Each option can be be overwritten with an environment variable ' + 'by converting the option to uppercase, replacing dashes with ' + 'underscores and prefixing it with `STF_GROUPS_ENGINE_` .)') @@ -37,9 +25,7 @@ export const handler = function(argv) { return groupsEngine({ endpoints: { push: argv.connectPush, - sub: argv.connectSub, pushdev: argv.connectPushDev, - subdev: argv.connectSubDev } }) } diff --git a/lib/cli/local/index.js b/lib/cli/local/index.js index 6fb69c93a4..5acc202e36 100644 --- a/lib/cli/local/index.js +++ b/lib/cli/local/index.js @@ -338,16 +338,12 @@ export const handler = function(argv) { 'processor', 'proc001', '--connect-app-dealer', argv.bindAppDealer, '--connect-dev-dealer', argv.bindDevDealer, - '--connect-push', argv.bindAppPull, - '--connect-push-dev', argv.bindDevPull, - '--connect-sub', argv.bindAppPub, - '--connect-sub-dev', argv.bindDevPub, '--public-ip', argv.publicIp ], [ // reaper one 'reaper', 'reaper001', '--connect-push', argv.bindDevPull, - '--connect-sub', argv.bindAppPub + '--connect-sub', argv.bindDevPub ], [ // provider 'provider', @@ -412,9 +408,7 @@ export const handler = function(argv) { [ // groups engine 'groups-engine', '--connect-push', argv.bindAppPull, - '--connect-sub', argv.bindAppPub, '--connect-push-dev', argv.bindDevPull, - '--connect-sub-dev', argv.bindDevPub ], [ // websocket 'websocket', diff --git a/lib/cli/processor/index.js b/lib/cli/processor/index.js index 5092bc1ba5..a420e9e587 100644 --- a/lib/cli/processor/index.js +++ b/lib/cli/processor/index.js @@ -18,30 +18,6 @@ export const builder = function(yargs) { array: true, demand: true }) - .option('connect-push', { - alias: 'c', - describe: 'App-side ZeroMQ PULL endpoint to connect to.', - array: true, - demand: true - }) - .option('connect-push-dev', { - alias: 'pd', - describe: 'Device-side ZeroMQ PULL endpoint to connect to.', - array: true, - demand: true - }) - .option('connect-sub', { - alias: 'u', - describe: 'App-side ZeroMQ PUB endpoint to connect to.', - array: true, - demand: true - }) - .option('connect-sub-dev', { - alias: 'sd', - describe: 'Device-side ZeroMQ PUB endpoint to connect to.', - array: true, - demand: true - }) .option('name', { describe: 'An easily identifiable name for log output.', type: 'string', @@ -64,10 +40,6 @@ export const handler = function(argv) { endpoints: { appDealer: argv.connectAppDealer, devDealer: argv.connectDevDealer, - push: argv.connectPush, - pushdev: argv.connectPushDev, - sub: argv.connectSub, - subdev: argv.connectSubDev }, publicIp: argv.publicIp }) diff --git a/lib/db/index.ts b/lib/db/index.ts index a4964c96d0..2928f5abab 100644 --- a/lib/db/index.ts +++ b/lib/db/index.ts @@ -117,21 +117,18 @@ export default class DbClient { pushdev, channelRouter, }: { - sub: SocketWrapper | string[]; - subdev: SocketWrapper | string[]; + sub?: SocketWrapper | string[]; + subdev?: SocketWrapper | string[]; push: SocketWrapper | string[]; pushdev: SocketWrapper | string[]; channelRouter?: EventEmitter; }, _log: ReturnType | undefined = log - ): Promise<{ - sub: SocketWrapper; - subdev: SocketWrapper; - push: SocketWrapper; - pushdev: SocketWrapper; - channelRouter: EventEmitter; - }> { - if (Array.isArray(sub)) { + ) { + let finalSub: SocketWrapper | undefined + let finalSubdev: SocketWrapper | undefined + + if (sub && Array.isArray(sub)) { const _sub = zmqutil.socket('sub') await Promise.all( sub.map(async(endpoint) => { @@ -153,10 +150,13 @@ export default class DbClient { } }) ) - sub = _sub + finalSub = _sub + } + else if (sub) { + finalSub = sub as SocketWrapper } - if (Array.isArray(subdev)) { + if (subdev && Array.isArray(subdev)) { const _subdev = zmqutil.socket('sub') await Promise.all( subdev.map(async(endpoint) => { @@ -178,12 +178,15 @@ export default class DbClient { } }) ) - subdev = _subdev + finalSubdev = _subdev + } + else if (subdev) { + finalSubdev = subdev as SocketWrapper } if (Array.isArray(push)) { const _push = zmqutil.socket('push') - Promise.all( + await Promise.all( push.map(async(endpoint) => { try { srv.attempt( @@ -205,7 +208,7 @@ export default class DbClient { if (Array.isArray(pushdev)) { const _pushdev = zmqutil.socket('push') - Promise.all( + await Promise.all( pushdev.map(async(endpoint) => { try { srv.attempt( @@ -229,23 +232,48 @@ export default class DbClient { } if (!channelRouter) { - channelRouter = new EventEmitter(); - [wireutil.global].forEach((channel) => { - _log.info('Subscribing to permanent channel "%s"', channel) - sub.subscribe(channel) - subdev.subscribe(channel) - }) + channelRouter = new EventEmitter() - sub.on('message', (channel, data) => { - channelRouter?.emit(channel.toString(), channel, data) - }) + if (finalSub || finalSubdev) { + ;[wireutil.global].forEach((channel) => { + _log.info('Subscribing to permanent channel "%s"', channel) + if (finalSub) { + finalSub.subscribe(channel) + } + if (finalSubdev) { + finalSubdev.subscribe(channel) + } + }) + + if (finalSub) { + finalSub.on('message', (channel, data) => { + channelRouter?.emit(channel.toString(), channel, data) + }) + } - subdev.on('message', (channel, data) => { - channelRouter?.emit(channel.toString(), channel, data) - }) + if (finalSubdev) { + finalSubdev.on('message', (channel, data) => { + channelRouter?.emit(channel.toString(), channel, data) + }) + } + } } - return {sub, subdev, push, pushdev, channelRouter} + const result: { + sub?: SocketWrapper; + subdev?: SocketWrapper; + push: SocketWrapper; + pushdev: SocketWrapper; + channelRouter: EventEmitter; + } = { + push, + pushdev, + channelRouter, + ... !!finalSub && {sub: finalSub}, + ... !!finalSubdev && {subdev: finalSubdev} + } + + return result } // Verifies that we can form a connection. Useful if it's necessary to make @@ -254,11 +282,12 @@ export default class DbClient { // an issue with the processor unit, as it started processing messages before // it was actually truly able to save anything to the database. This lead to // lost messages in certain situations. - static ensureConnectivity = async (fn: T) => { - await DbClient.connect() - log.info("Db is up") - return fn - } + static ensureConnectivity = any>(fn: T) => + async(...args: Parameters): Promise> => { + await DbClient.connect(); + log.info("Db is up"); + return fn(...args); + } // Sets up the database static setup = () => DbClient.connect().then((conn) => _setup(conn)) diff --git a/lib/db/models/all/model.js b/lib/db/models/all/model.js index fb15bb75ce..4cb3b46f63 100644 --- a/lib/db/models/all/model.js +++ b/lib/db/models/all/model.js @@ -84,10 +84,10 @@ export const createBootStrap = function(env) { 'groups.quotas.allocated.duration': group?.envUserGroupsDuration, 'groups.quotas.consumed.duration': 0, 'groups.quotas.consumed.number': 0, - 'groups.defaultGroupsNumber': user?.email !== group?.owner.email ? 0 : group?.envUserGroupsNumber, - 'groups.defaultGroupsDuration': user?.email !== group?.owner.email ? 0 : group?.envUserGroupsDuration, - 'groups.defaultGroupsRepetitions': user?.email !== group?.owner.email ? 0 : group?.envUserGroupsRepetitions, - 'groups.repetitions': group?.envUserGroupsRepetitions + 'groups.quotas.defaultGroupsNumber': user?.email !== group?.owner.email ? 0 : group?.envUserGroupsNumber, + 'groups.quotas.defaultGroupsDuration': user?.email !== group?.owner.email ? 0 : group?.envUserGroupsDuration, + 'groups.quotas.defaultGroupsRepetitions': user?.email !== group?.owner.email ? 0 : group?.envUserGroupsRepetitions, + 'groups.quotas.repetitions': group?.envUserGroupsRepetitions } await db.users.updateOne( @@ -178,11 +178,6 @@ export const createBootStrap = function(env) { }) } -// dbapi.deleteDevice = function(serial) { -export const deleteDevice = function(serial) { - return db.devices.deleteOne({serial: serial}) -} - export const deleteUser = function(email) { return db.users.deleteOne({email: email}) } @@ -425,17 +420,17 @@ export const createUser = function(email, name, ip, privilege) { lock: false, quotas: { allocated: { - number: group?.envUserGroupsNumber, - duration: group?.envUserGroupsDuration + number: adminUser ? adminUser.groups.quotas.defaultGroupsNumber : group?.envUserGroupsNumber, + duration: adminUser ? adminUser.groups.quotas.defaultGroupsDuration : group?.envUserGroupsDuration }, consumed: { number: 0, duration: 0 }, - defaultGroupsNumber: group?.envUserGroupsNumber, - defaultGroupsDuration: group?.envUserGroupsDuration, - defaultGroupsRepetitions: group?.envUserGroupsRepetitions, - repetitions: group?.envUserGroupsRepetitions + defaultGroupsNumber: adminUser ? 0 : group?.envUserGroupsNumber, + defaultGroupsDuration: adminUser ? 0 : group?.envUserGroupsDuration, + defaultGroupsRepetitions: adminUser ? 0 : group?.envUserGroupsRepetitions, + repetitions: adminUser ? adminUser.groups.quotas.defaultGroupsRepetitions : group?.envUserGroupsRepetitions } } } @@ -533,6 +528,11 @@ export const resetUserSettings = function(email) { }) } +export const getUserAdbKeys = function(email) { + return db.users.findOne({email: email}) + .then(user => user?.adbKeys || []) +} + // dbapi.insertUserAdbKey = function(email, key) { export const insertUserAdbKey = function(email, key) { let data = { @@ -824,6 +824,8 @@ export const setDeviceAbsent = function(serial) { { $set: { owner: null, + usage: null, + logs_enabled: false, present: false, presenceChangedAt: getNow() } @@ -832,27 +834,22 @@ export const setDeviceAbsent = function(serial) { } // dbapi.setDeviceUsage = function(serial, usage) { -export const setDeviceUsage = function(serial, usage) { - return db.devices.updateOne( - {serial: serial}, - { - $set: { - usage: usage, - usageChangedAt: getNow() - } - } - ) -} +export const setDeviceState = function(serial, {usage, owner, timeout}) { + const usageSet = typeof usage === 'undefined' ? {} : { + usage, usageChangedAt: getNow(), + ... !usage && {logs_enabled: false} + } -// dbapi.unsetDeviceUsage = function(serial) { -export const unsetDeviceUsage = function(serial) { return db.devices.updateOne( {serial: serial}, { $set: { - usage: null, - usageChangedAt: getNow(), - logs_enabled: false + owner, + ...usageSet, + ... typeof timeout === 'number' && { + statusChangedAt: getNow(), + bookedBefore: timeout + }, } } ) @@ -1449,29 +1446,6 @@ export const generateIndexes = function() { }) } -// dbapi.setDeviceSocketDisplay = function(data) { -export const setDeviceSocketDisplay = function(data) { - return db.devices.updateOne( - {serial: data.serial}, - { - $set: { - 'display.density': 2, - 'display.fps': 60, - 'display.id': 0, - 'display.rotation': 0, - 'display.secure': true, - 'display.size': 4.971253395080566, - 'display.xdpi': 294.9670104980469, - 'display.ydpi': 295.56298828125, - 'display.width': data.width, - 'display.height': data.height - } - } - ).then(() => { - loadDeviceBySerial(data.serial) - }) -} - // dbapi.setDeviceSocketPorts = function(data, publicIp) { export const setDeviceSocketPorts = function(data, publicIp) { return db.devices.updateOne( @@ -1543,21 +1517,6 @@ export const getDeviceDisplaySize = function(serial) { }) } -// TODO Check usage. Probably dead code -export const setAbsentDisconnectedDevices = function() { - return db.devices.updateOne( - { - platform: 'iOS' - }, - { - $set: { - present: false, - ready: false - } - } - ) -} - // dbapi.getInstalledApplications = function(message) { export const getInstalledApplications = function(message) { return loadDeviceBySerial(message.serial) @@ -1742,7 +1701,7 @@ export const updateDefaultUserGroupsQuotas = async(email, duration, number, repe {email: email} , [{ $set: { - defaultGroupsDuration: { + 'groups.quotas.defaultGroupsDuration': { $cond: [ { $ne: [duration, null] @@ -1751,7 +1710,7 @@ export const updateDefaultUserGroupsQuotas = async(email, duration, number, repe '$groups.quotas.defaultGroupsDuration' ] }, - defaultGroupsNumber: { + 'groups.quotas.defaultGroupsNumber': { $cond: [ { $ne: [number, null] @@ -1760,7 +1719,7 @@ export const updateDefaultUserGroupsQuotas = async(email, duration, number, repe '$groups.quotas.defaultGroupsNumber' ] }, - defaultGroupsRepetitions: { + 'groups.quotas.defaultGroupsRepetitions': { $cond: [ { $ne: [repetitions, null] diff --git a/lib/units/api/controllers/user.js b/lib/units/api/controllers/user.js index 6d1c7dd080..8aef72fb95 100644 --- a/lib/units/api/controllers/user.js +++ b/lib/units/api/controllers/user.js @@ -149,12 +149,19 @@ function addUserDevice(req, res) { let usage = 'automation' req.options.push.send([ device.channel, - wireutil.envelope(new wire.GroupMessage(new wire.OwnerMessage(req.user.email, req.user.name, req.user.group), timeout, wireutil.toDeviceRequirements({ - serial: { - value: serial, - match: 'exact' - } - }), usage)) + wireutil.envelope(new wire.GroupMessage( + new wire.OwnerMessage(req.user.email, req.user.name, req.user.group), + timeout, + wireutil.toDeviceRequirements({ + serial: { + value: serial, + match: 'exact' + } + }), + usage, + req.user.adbKeys + .map(key => key.fingerprint) + )) ]) } return false diff --git a/lib/units/api/controllers/users.js b/lib/units/api/controllers/users.js index a0f41a9bf2..7eff6be078 100644 --- a/lib/units/api/controllers/users.js +++ b/lib/units/api/controllers/users.js @@ -142,14 +142,12 @@ function revokeAdmin(req, res) { }) } function lockStfAdminUser(res) { - const lock = {} - dbapi.lockUser(apiutil.STF_ADMIN_EMAIL).then(function(stats) { + return dbapi.lockUser(apiutil.STF_ADMIN_EMAIL).then(function(stats) { if (stats.modifiedCount === 0) { return apiutil.lightComputeStats(res, stats) } - lock.user = stats.changes[0].new_val + return {user: stats.changes[0].new_val} }) - return lock } function updateUsersAlertMessage(req, res) { if (req.user.privilege !== apiutil.ADMIN) { @@ -191,9 +189,9 @@ function getUserInfo(req, email) { } function getUsersAlertMessage(req, res) { const fields = req.query.fields - return dbapi.loadUser(apiutil.STF_ADMIN_EMAIL).then(function(user) { + return dbapi.loadUser(apiutil.STF_ADMIN_EMAIL).then(async function(user) { if (user?.settings?.alertMessage === undefined) { - const lock = lockStfAdminUser(res) + const lock = await lockStfAdminUser(res) const alertMessage = { activation: 'False', data: '*** this site is currently under maintenance, please wait ***', diff --git a/lib/units/api/helpers/useDevice.js b/lib/units/api/helpers/useDevice.js index 41e10af9b5..93c3a267a7 100644 --- a/lib/units/api/helpers/useDevice.js +++ b/lib/units/api/helpers/useDevice.js @@ -52,8 +52,8 @@ const useDevice = ({user, device, channelRouter, push, sub, usage = null, log}) try { await runTransaction(device.channel, new wire.UngroupMessage(deviceRequirements), {sub, push, channelRouter}) } - catch (e) { - log?.info(e) + catch (/** @type {any} */e) { + log?.info('Transaction failed: $s', e?.message) } const responseTimeout = setTimeout(function() { @@ -107,6 +107,7 @@ const useDevice = ({user, device, channelRouter, push, sub, usage = null, log}) , timeout , deviceRequirements , usage + , user.adbKeys.map((/** @type {{ fingerprint: string }} */ k) => k.fingerprint) ), {sub, push, channelRouter}) }) diff --git a/lib/units/base-device/plugins/group.js b/lib/units/base-device/plugins/group.js index d01d8a6012..e69de29bb2 100755 --- a/lib/units/base-device/plugins/group.js +++ b/lib/units/base-device/plugins/group.js @@ -1,156 +0,0 @@ -import events from 'events' -import Promise from 'bluebird' -import syrup from '@devicefarmer/stf-syrup' -import logger from '../../../util/logger.js' -import wire from '../../../wire/index.js' -import wireutil from '../../../wire/util.js' -import * as grouputil from '../../../util/grouputil.js' -import lifecycle from '../../../util/lifecycle.js' -import db from '../../../db/index.js' -import dbapi from '../../../db/api.js' -import * as apiutil from '../../../util/apiutil.js' -import solo from './solo.js' -import router from '../support/router.js' -import push from '../support/push.js' -import sub from '../support/sub.js' -import channels from '../support/channels.js' -import {AutoGroupMessage, GroupMessage, UngroupMessage} from '../../../wire/wire.js' -export default syrup.serial() - .dependency(solo) - .dependency(router) - .dependency(push) - .dependency(sub) - .dependency(channels) - .define(async(options, solo, router, push, sub, channels) => { - const log = logger.createLogger('base-device:plugins:group') - let currentGroup = null - - /** @type {any} */ - let plugin = new events.EventEmitter() - - await db.connect() - - plugin.get = Promise.method(function() { - if (!currentGroup) { - throw new grouputil.NoGroupError() - } - return currentGroup - }) - plugin.join = (newGroup, timeout, usage) => { - return plugin.get() - .then(() => { - if (currentGroup.group !== newGroup.group) { - throw new grouputil.AlreadyGroupedError() - } - log.info('Update timeout for ', apiutil.QUARTER_MINUTES) - channels.updateTimeout(currentGroup.group, apiutil.QUARTER_MINUTES) - let newTimeout = channels.getTimeout(currentGroup.group) - plugin.emit('join', currentGroup) - dbapi.enhanceStatusChangedAt(options.serial, newTimeout).then(() => { - return currentGroup - }) - }) - .catch(grouputil.NoGroupError, () => { - currentGroup = newGroup - log.important('Now owned by "%s"', currentGroup.email) - log.important('Device now in group "%s"', currentGroup.name) - log.info('Rent time is ' + timeout) - log.info('Subscribing to group channel "%s"', currentGroup.group) - channels.register(currentGroup.group, { - timeout: timeout || options.groupTimeout, - alias: solo.channel - }) - dbapi.enhanceStatusChangedAt(options.serial, timeout) - sub.subscribe(currentGroup.group) - push.send([ - wireutil.global, - wireutil.envelope(new wire.JoinGroupMessage(options.serial, currentGroup, usage)) - ]) - plugin.emit('join', currentGroup) - return currentGroup - }) - } - plugin.keepalive = () => { - if (currentGroup) { - channels.keepalive(currentGroup.group) - } - } - plugin.leave = (reason) => { - return plugin.get() - .then(group => { - log.important('No longer owned by "%s"', group.email) - log.info('Unsubscribing from group channel "%s"', group.group) - channels.unregister(group.group) - sub.unsubscribe(group.group) - push.send([ - wireutil.global, - wireutil.envelope(new wire.LeaveGroupMessage(options.serial, group, reason)) - ]) - currentGroup = null - plugin.emit('leave', group) - return group - }) - } - router - .on(GroupMessage, (channel, message) => { - let reply = wireutil.reply(options.serial) - // grouputil.match(ident, message.requirements) - plugin.join(message.owner, message.timeout, message.usage) - .then(() => { - push.send([ - channel, - reply.okay() - ]) - }) - .catch(grouputil.RequirementMismatchError, (err) => { - push.send([ - channel, - reply.fail(err.message) - ]) - }) - .catch(grouputil.AlreadyGroupedError, (err) => { - push.send([ - channel, - reply.fail(err.message) - ]) - }) - }) - .on(AutoGroupMessage, (channel, message) => { - return plugin.join(message.owner, message.timeout, message.identifier) - .then(() => { - plugin.emit('autojoin', message.identifier, true) - }) - .catch(grouputil.AlreadyGroupedError, () => { - plugin.emit('autojoin', message.identifier, false) - }) - }) - .on(UngroupMessage, (channel, message) => { - let reply = wireutil.reply(options.serial) - Promise.method(() => { - return plugin.leave('ungroup_request') - })() - .then(() => { - push.send([ - channel, - reply.okay() - ]) - }) - .catch(grouputil.NoGroupError, err => { - push.send([ - channel, - reply.fail(err.message) - ]) - }) - }) - // @ts-ignore - channels.on('timeout', channel => { - if (currentGroup && channel === currentGroup.group) { - plugin.leave('automatic_timeout') - } - }) - lifecycle.observe(() => { - return plugin.leave('device_absent') - .catch(grouputil.NoGroupError, () => true) - }) - return plugin - }) diff --git a/lib/units/base-device/plugins/group.ts b/lib/units/base-device/plugins/group.ts new file mode 100755 index 0000000000..1ffbd304ed --- /dev/null +++ b/lib/units/base-device/plugins/group.ts @@ -0,0 +1,252 @@ +import syrup from '@devicefarmer/stf-syrup' +import logger from '../../../util/logger.js' +import { + AutoGroupMessage, + DeviceStatusChange, + GroupMessage, + JoinGroupMessage, + LeaveGroupMessage, UngroupMessage +} from '../../../wire/wire.js' +import wireutil from '../../../wire/util.js' +import * as grouputil from '../../../util/grouputil.js' +import lifecycle from '../../../util/lifecycle.js' +import * as apiutil from '../../../util/apiutil.js' +import solo from './solo.js' +import router from '../support/router.js' +import push from '../support/push.js' +import sub from '../support/sub.js' +import channels from '../support/channels.js' +import EventEmitter from 'events' + +interface GroupState { + email: string + name: string + group: string +} + +type ADBKey = string +type Joined = boolean + +interface GroupEvents { + join: [GroupState, ADBKey[]] + leave: [GroupState | null] + autojoin: [ADBKey, Joined] +} + +export default syrup.serial() + .dependency(solo) + .dependency(router) + .dependency(push) + .dependency(sub) + .dependency(channels) + .define(async(options, solo, router, push, sub, channels) => { + const log = logger.createLogger('base-device:plugins:group') + + const plugin = new class GroupManager extends EventEmitter { + private currentGroup: GroupState | null = null + + keepalive = () => { + if (this.currentGroup) { + channels.keepalive(this.currentGroup.group) + } + } + + get = async() => { + if (!this.currentGroup) { + throw new grouputil.NoGroupError() + return + } + + return this.currentGroup + } + + join = async(newGroup: GroupState, timeout: number, usage: string, keys: string[]) => { + try { + if (!newGroup?.group) { + throw new Error(`New group is not valid: ${JSON.stringify(newGroup)}`) + } + + if (!!this.currentGroup?.group) { // if device in group + if (this.currentGroup.group !== newGroup.group) { // and is not same + log.error(`Cannot join group ${JSON.stringify(newGroup)} since this device is in group ${JSON.stringify(this.currentGroup)}`) + throw new grouputil.AlreadyGroupedError() + } + + log.info('Update timeout for %s', apiutil.QUARTER_MINUTES) + channels.updateTimeout(this.currentGroup.group, apiutil.QUARTER_MINUTES) + + const newTimeout = channels.getTimeout(this.currentGroup.group) + push.send([ + wireutil.global, + wireutil.pack(DeviceStatusChange, { + serial: options.serial, + timeout: newTimeout || undefined + }) + ]) + + return this.currentGroup + } + + this.currentGroup = newGroup + + log.important('Now owned by "%s"', this.currentGroup.email) + log.important('Device now in group "%s"', this.currentGroup.name) + log.info(`Rent time is ${timeout}`) + log.info('Subscribing to group channel "%s"', this.currentGroup.group) + + channels.register(this.currentGroup.group, { + timeout: timeout || options.groupTimeout, + alias: solo.channel + }) + + sub.subscribe(this.currentGroup.group) + plugin.emit('join', this.currentGroup, keys) + + push.send([ + wireutil.global, + wireutil.pack(JoinGroupMessage, { + serial: options.serial, + owner: this.currentGroup, + usage, + timeout + }) + ]) + + return this.currentGroup + } + catch (err: any) { + log.error(`Failed to join group ${JSON.stringify(newGroup)}, Error: %s`, err?.message) + return this.currentGroup + } + } + + leave = async(reason: string) => { + if (!this.currentGroup) { + throw new grouputil.NoGroupError() + } + + log.important('No longer owned by "%s"', this.currentGroup.email) + log.info('Unsubscribing from group channel "%s"', this.currentGroup.group) + + push.send([ + wireutil.global, + wireutil.pack(LeaveGroupMessage, { + serial: options.serial, + owner: this.currentGroup, + reason + }) + ]) + + channels.unregister(this.currentGroup.group) + sub.unsubscribe(this.currentGroup.group) + + this.currentGroup = null + plugin.emit('leave', this.currentGroup) + return this.currentGroup + } + + beforeActionCheck = + async(message: any) => true + + // Set that for custom checks before GroupMessage/UngroupMessage processed (optional) + setCheckBeforeAction = + (cb: (message: any) => Promise) => { + this.beforeActionCheck = cb + } + + checkBeforeAction = + (msgName: string, message: any, channel: string, reply: ReturnType) => + this.beforeActionCheck(message) + .catch((err: any) => { + log.error('Error before processing %s: %s', msgName, err?.message) + push.send([ + channel, + reply.fail(err.message) + ]) + + return false + }) + }() + + router + .on(GroupMessage, async(channel, message) => { + const reply = wireutil.reply(options.serial) + try { + if (!await plugin.checkBeforeAction('GroupMessage', message, channel, reply)) { + return + } + + await plugin.join(message.owner!, message.timeout!, message.usage!, message.keys) + push.send([ + channel, + reply.okay() + ]) + } + catch (err: any) { + log.error('Failed processing GroupMessage: %s', err?.message) + if (err instanceof grouputil.AlreadyGroupedError) { + push.send([ + channel, + reply.fail(err.message) + ]) + } + } + }) + .on(AutoGroupMessage, async(channel, message) => { + try { + await plugin.join(message.owner!, 0, message.identifier, []) + plugin.emit('autojoin', message.identifier, true) + } + catch (err: any) { + log.error('Failed processing AutoGroupMessage: %s', err?.message) + if (err instanceof grouputil.AlreadyGroupedError) { + plugin.emit('autojoin', message.identifier, false) + } + } + }) + .on(UngroupMessage, async(channel, message) => { + const reply = wireutil.reply(options.serial) + try { + if (!await plugin.checkBeforeAction('UngroupMessage', message, channel, reply)) { + return + } + + await plugin.leave('ungroup_request') + push.send([ + channel, + reply.okay() + ]) + } + catch (err: any) { + log.error('Failed processing UngroupMessage: %s', err?.message) + if (err instanceof grouputil.NoGroupError) { + push.send([ + channel, + reply.fail(err.message) + ]) + } + } + }) + + // @ts-ignore + channels.on('timeout', async(channel) => { + const currentGroup = await plugin.get() + if (currentGroup && channel === currentGroup.group) { + plugin.leave('automatic_timeout') + } + }) + + lifecycle.observe(async() => { + try { + await plugin.leave('device_absent') + } + catch (err: any) { + log.error('Failed leave from group on process exit: %s', err?.message) + if (err instanceof grouputil.NoGroupError) { + return true + } + } + }) + + return plugin + }) diff --git a/lib/units/device/plugins/connect.js b/lib/units/device/plugins/connect.ts similarity index 54% rename from lib/units/device/plugins/connect.js rename to lib/units/device/plugins/connect.ts index 51be539ca8..8dd727d575 100644 --- a/lib/units/device/plugins/connect.js +++ b/lib/units/device/plugins/connect.ts @@ -1,10 +1,9 @@ import util from 'util' import syrup from '@devicefarmer/stf-syrup' import logger from '../../../util/logger.js' -import wire from '../../../wire/index.js' +import {JoinGroupByAdbFingerprintMessage} from '../../../wire/wire.js' import wireutil from '../../../wire/util.js' import lifecycle from '../../../util/lifecycle.js' -import db from '../../../db/index.js' import adb from '../support/adb.js' import connector, {DEVICE_TYPE} from '../../base-device/support/connector.js' import push from '../../base-device/support/push.js' @@ -14,18 +13,13 @@ import solo from './solo.js' import urlformat from '../../base-device/support/urlformat.js' import identity from './util/identity.js' import data from './util/data.js' -import {GRPC_WAIT_TIMEOUT} from '../../../util/apiutil.js' +import type TcpUsbServer from '@u4/adbkit/dist/adb/tcpusb/server.d.ts' import {AdbKeysUpdatedMessage} from '../../../wire/wire.js' -// The promise passed as an argument will not be cancelled after the time has elapsed, -// only the second promise will be rejected. -const promiseTimeout = (promise, ms, message = 'Timeout exceeded') => Promise.race([ - promise, - new Promise((_, reject) => { - const id = setTimeout(() => reject(new Error(message)), ms) - promise.finally(() => clearTimeout(id)) - }) -]) +interface Key { + fingerprint: string + comment: string +} export default syrup.serial() .dependency(adb) @@ -37,56 +31,58 @@ export default syrup.serial() .dependency(connector) .dependency(identity) .dependency(data) - .define(async function(options, adb, router, push, group, solo, urlformat, connector, identity, data) { + .define(async(options, adb, router, push, group, solo, urlformat, connector, identity, data) => { const log = logger.createLogger('device:plugins:connect') - let activeServer = null - - await db.connect() + let activeServer: TcpUsbServer | null = null - const notify = async(key) => { + const notify = async(key: Key) => { try { - const currentGroup = group.get() + const currentGroup = await group.get() push.send([ solo.channel, - wireutil.envelope(new wire.JoinGroupByAdbFingerprintMessage(options.serial, key.fingerprint, key.comment, currentGroup.group)) + wireutil.pack(JoinGroupByAdbFingerprintMessage, { + serial: options.serial, + fingerprint: key.fingerprint, + comment: key.comment, + currentGroup: currentGroup?.group + }) ]) } catch(e) { push.send([ solo.channel, - wireutil.envelope(new wire.JoinGroupByAdbFingerprintMessage(options.serial, key.fingerprint, key.comment)) + wireutil.pack(JoinGroupByAdbFingerprintMessage, { + serial: options.serial, + fingerprint: key.fingerprint, + comment: key.comment + }) ]) } } - const joinListener = (_, identifier, key, reject) => { - if (identifier !== key.fingerprint) { - reject(new Error('Somebody else took the device')) - } - } - - const autojoinListener = (identifier, joined, key, resolve, reject) => { - if (identifier === key.fingerprint) { - if (joined) { - return resolve() - } - reject(new Error('Device is already in use')) - } - } - const plugin = { serial: options.serial, port: options.connectPort, url: urlformat(options.connectUrlPattern, options.connectPort, identity.model, data ? data.name.id : ''), - auth: (key, resolve, reject) => reject(), + auth: (key: Key): boolean => false, start: () => new Promise((resolve, reject) => { log.info('Starting connect plugin') - const auth = key => promiseTimeout(new Promise((resolve, reject) => { - plugin.auth(key, resolve, reject) + // If Auth failed - the entire unit device will fall + // TODO: fix + const auth = (key: Key) => new Promise(async(resolve, reject) => { + // TODO: Dangerous, discuss and remove router.on(AdbKeysUpdatedMessage, () => notify(key)) - notify(key) - }), GRPC_WAIT_TIMEOUT) // reject after 2 minutes if autojoin event doesn't fire + await notify(key) + + if (plugin.auth(key)) { + resolve() + return + } + + // Connection rejected by user-defined auth handler + reject('Auth failed') + }) activeServer = adb.createTcpUsbBridge(plugin.serial, {auth}) .on('listening', () => resolve(plugin.url)) @@ -97,8 +93,7 @@ export default syrup.serial() conn.on('userActivity', () => group.keepalive()) }) - activeServer.listen(plugin.port) - lifecycle.share('Remote ADB', activeServer) + activeServer!.listen(plugin.port) log.info(util.format('Listening on port %d', plugin.port)) resolve(plugin.url) @@ -109,22 +104,19 @@ export default syrup.serial() } log.info('Stop connect plugin') - router.removeAllListeners(wire.AdbKeysUpdatedMessage) - let resolveClose = () => {} + // TODO: Not ideal way, need WireRouter.once + router.removeAllListeners(AdbKeysUpdatedMessage) - const waitCloseServer = new Promise((resolve) => { - // @ts-ignore - resolveClose = resolve - }) - - activeServer.on('close', () => { - resolveClose() + const waitServerClose = new Promise((resolve) => { + activeServer!.on('close', () => { + resolve() + }) }) activeServer.end() activeServer.close() - await waitCloseServer + await waitServerClose activeServer = null }, @@ -135,13 +127,24 @@ export default syrup.serial() } } - group.on('join', (g, id) => - plugin.auth = (key, resolve, reject) => - joinListener(g, id, key, resolve, reject) + group.on('join', (group, keys) => + plugin.auth = key => { + if (keys?.length && !keys.includes(key.fingerprint)) { + log.error('Invalid RSA key. Somebody else took the device') + return false + } + return true + } ) + group.on('autojoin', (id, joined) => - plugin.auth = (key, resolve, reject) => - autojoinListener(id, joined, key, resolve, reject) + plugin.auth = key => { + if (id === key.fingerprint && joined) { + return true + } + log.error('Device is already in use') + return false + } ) connector.init({ @@ -155,6 +158,6 @@ export default syrup.serial() lifecycle.observe(() => connector.stop()) group.on('leave', () => { connector.stop() - plugin.auth = (key, resolve, reject) => reject() + plugin.auth = (key) => false }) }) diff --git a/lib/units/device/plugins/group.js b/lib/units/device/plugins/group.js index 6a31e8a0aa..e69de29bb2 100644 --- a/lib/units/device/plugins/group.js +++ b/lib/units/device/plugins/group.js @@ -1,214 +0,0 @@ -import events from 'events' -import Promise from 'bluebird' -import syrup from '@devicefarmer/stf-syrup' -import logger from '../../../util/logger.js' -import wire from '../../../wire/index.js' -import wireutil from '../../../wire/util.js' -import * as grouputil from '../../../util/grouputil.js' -import lifecycle from '../../../util/lifecycle.js' -import db from '../../../db/index.js' -import dbapi from '../../../db/api.js' -import * as apiutil from '../../../util/apiutil.js' -import solo from './solo.js' -import identity from './util/identity.js' -import service from './service.js' -import router from '../../base-device/support/router.js' -import push from '../../base-device/support/push.js' -import sub from '../../base-device/support/sub.js' -import channels from '../../base-device/support/channels.js' -import {AutoGroupMessage, GroupMessage, UngroupMessage} from '../../../wire/wire.js' -export default syrup.serial() - .dependency(solo) - .dependency(identity) - .dependency(service) - .dependency(router) - .dependency(push) - .dependency(sub) - .dependency(channels) - .define(async function(options, solo, ident, /** @type {any} */ service, router, push, sub, channels) { - const log = logger.createLogger('device:plugins:group') - let currentGroup = null - - /** @type {any} */ - const plugin = new events.EventEmitter() - - await db.connect() - - plugin.get = Promise.method(function() { - if (!currentGroup) { - throw new grouputil.NoGroupError() - } - return currentGroup - }) - plugin.join = function(newGroup, timeout, usage) { - return plugin.get() - .then(function() { - if (currentGroup.group !== newGroup.group) { - log.error(`Cannot join group ${JSON.stringify(newGroup)} since this device is in group ${JSON.stringify(currentGroup)}`) - throw new grouputil.AlreadyGroupedError() - } - log.info('Update timeout for %s', apiutil.QUARTER_MINUTES) - channels.updateTimeout(currentGroup.group, apiutil.QUARTER_MINUTES) - - const newTimeout = channels.getTimeout(currentGroup.group) - dbapi.enhanceStatusChangedAt(options.serial, newTimeout) - }) - .catch(grouputil.NoGroupError, function() { - currentGroup = newGroup - log.important('Now owned by "%s"', currentGroup.email) - log.important('Device now in group "%s"', currentGroup.name) - log.info(`Rent time is ${timeout}`) - log.info('Subscribing to group channel "%s"', currentGroup.group) - channels.register(currentGroup.group, { - timeout: timeout || options.groupTimeout, - alias: solo.channel - }) - dbapi.enhanceStatusChangedAt(options.serial, timeout).then(() => { - sub.subscribe(currentGroup.group) - plugin.emit('join', currentGroup) - push.send([ - wireutil.global, - wireutil.envelope(new wire.JoinGroupMessage(options.serial, currentGroup, usage)) - ]) - service.freezeRotation(0) - return currentGroup - }) - }) - } - plugin.keepalive = function() { - if (currentGroup) { - channels.keepalive(currentGroup.group) - } - } - plugin.leave = function(reason) { - return plugin.get() - .then(function(group) { - log.important('No longer owned by "%s"', group.email) - log.info('Unsubscribing from group channel "%s"', group.group) - return dbapi.enhanceStatusChangedAt(options.serial, 0).then(() => { - push.send([ - wireutil.global, - wireutil.envelope(new wire.LeaveGroupMessage(options.serial, group, reason)) - ]) - channels.unregister(group.group) - sub.unsubscribe(group.group) - currentGroup = null - plugin.emit('leave', group) - return group - }) - }) - } - plugin.on('join', function() { - service.wake() - service.acquireWakeLock() - }) - plugin.on('leave', function() { - if (options.screenReset) { - service.pressKey('home') - service.thawRotation() - dbapi.loadDeviceBySerial(options.serial).then(device => { - if (device.group.id === device.origin) { - log.warn('Cleaning device') - service.sendCommand('settings put system screen_brightness_mode 0') - service.sendCommand('settings put system screen_brightness 0') - service.setMasterMute(true) - service.sendCommand('input keyevent 26') - service.sendCommand('settings put global http_proxy :0') - service.sendCommand('pm clear com.android.chrome') - service.sendCommand('pm clear com.chrome.beta') - service.sendCommand('pm clear com.sec.android.app.sbrowser') - service.sendCommand('pm uninstall com.vkontakte.android') - service.sendCommand('pm uninstall com.vk.im') - service.sendCommand('pm uninstall com.vk.clips') - service.sendCommand('pm uninstall com.vk.calls') - service.sendCommand('pm uninstall com.vk.admin') - service.sendCommand('pm clear com.mi.globalbrowser') - service.sendCommand('pm clear com.microsoft.emmx') - service.sendCommand('pm clear com.huawei.browser') - service.sendCommand('pm uninstall --user 0 com.samsung.clipboardsaveservice') - service.sendCommand('pm uninstall --user 0 com.samsung.android.clipboarduiservice') - service.sendCommand('rm -rf /sdcard/Downloads') - service.sendCommand('rm -rf /storage/emulated/legacy/Downloads') - service.sendCommand('settings put global always_finish_activities 0') - service.sendCommand('pm enable-user com.google.android.gms') - service.sendCommand('settings put system font_scale 1.0') - service.sendCommand('su') - service.sendCommand('echo "chrome --disable-fre --no-default-browser-check --no-first-run" > /data/local/tmp/chrome-command-line') - service.sendCommand('am set-debug-app --persistent com.android.chrome') - } - else { - log.warn('Device was not cleared because it in custom group') - } - }) - } - service.releaseWakeLock() - }) - router - .on(GroupMessage, function(channel, message) { - let reply = wireutil.reply(options.serial) - grouputil.match(ident, message.requirements) - .then(function() { - return plugin.join(message.owner, message.timeout, message.usage) - }) - .then(function() { - push.send([ - channel, - reply.okay() - ]) - }) - .catch(grouputil.RequirementMismatchError, function(err) { - push.send([ - channel, - reply.fail(err.message) - ]) - }) - .catch(grouputil.AlreadyGroupedError, function(err) { - push.send([ - channel, - reply.fail(err.message) - ]) - }) - }) - .on(AutoGroupMessage, function(channel, message) { - return plugin.join(message.owner, message.timeout, message.identifier) - .then(function() { - plugin.emit('autojoin', message.identifier, true) - }) - .catch(grouputil.AlreadyGroupedError, function() { - plugin.emit('autojoin', message.identifier, false) - }) - }) - .on(UngroupMessage, function(channel, message) { - let reply = wireutil.reply(options.serial) - grouputil.match(ident, message.requirements) - .then(function() { - return plugin.leave('ungroup_request') - }) - .then(function() { - push.send([ - channel, - reply.okay() - ]) - }) - .catch(grouputil.NoGroupError, function(err) { - push.send([ - channel, - reply.fail(err.message) - ]) - }) - }) - - // @ts-ignore - channels.on('timeout', function(channel) { - if (currentGroup && channel === currentGroup.group) { - plugin.leave('automatic_timeout') - } - }) - lifecycle.observe(function() { - return plugin.leave('device_absent') - .catch(grouputil.NoGroupError, function() { - return true - }) - }) - return plugin - }) diff --git a/lib/units/device/plugins/group.ts b/lib/units/device/plugins/group.ts new file mode 100644 index 0000000000..c628918805 --- /dev/null +++ b/lib/units/device/plugins/group.ts @@ -0,0 +1,96 @@ +import syrup from '@devicefarmer/stf-syrup' +import logger from '../../../util/logger.js' +import {DeviceGetIsInOrigin} from '../../../wire/wire.js' +import wireutil from '../../../wire/util.js' +import * as grouputil from '../../../util/grouputil.js' +import solo from './solo.js' +import identity from './util/identity.js' +import service from './service.js' +import router from '../../base-device/support/router.js' +import push from '../../base-device/support/push.js' +import sub from '../../base-device/support/sub.js' +import channels from '../../base-device/support/channels.js' +import group from '../../base-device/plugins/group.js' +import {runTransactionDev} from '../../../wire/transmanager.js' + +export default syrup.serial() + .dependency(solo) + .dependency(identity) + .dependency(service) + .dependency(router) + .dependency(push) + .dependency(sub) + .dependency(channels) + .dependency(group) + .define(async(options, solo, ident, /** @type {any} */ service, router, push, sub, channels, group) => { + const log = logger.createLogger('device:plugins:group') + + group.setCheckBeforeAction(async(message: any) => + !message.requirements || grouputil.match(ident, message.requirements) + ) + + group.on('join', () => { + service.freezeRotation(0) + service.wake() + service.acquireWakeLock() + }) + + group.on('leave', async() => { + try { + if (options.screenReset) { + service.pressKey('home') + service.thawRotation() + + const {isInOrigin} = await runTransactionDev( + wireutil.global, + DeviceGetIsInOrigin, + {serial: options.serial}, + {sub, push, router} + ) as { isInOrigin: boolean } + + if (isInOrigin) { + log.warn('Cleaning device') + await Promise.all([ + service.sendCommand('settings put system screen_brightness_mode 0'), + service.sendCommand('settings put system screen_brightness 0'), + service.setMasterMute(true), + service.sendCommand('input keyevent 26'), + service.sendCommand('settings put global http_proxy :0'), + service.sendCommand('pm clear com.android.chrome'), + service.sendCommand('pm clear com.chrome.beta'), + service.sendCommand('pm clear com.sec.android.app.sbrowser'), + service.sendCommand('pm uninstall com.vkontakte.android'), + service.sendCommand('pm uninstall com.vk.im'), + service.sendCommand('pm uninstall com.vk.clips'), + service.sendCommand('pm uninstall com.vk.calls'), + service.sendCommand('pm uninstall com.vk.admin'), + service.sendCommand('pm clear com.mi.globalbrowser'), + service.sendCommand('pm clear com.microsoft.emmx'), + service.sendCommand('pm clear com.huawei.browser'), + service.sendCommand('pm uninstall --user 0 com.samsung.clipboardsaveservice'), + service.sendCommand('pm uninstall --user 0 com.samsung.android.clipboarduiservice'), + service.sendCommand('rm -rf /sdcard/Downloads'), + service.sendCommand('rm -rf /storage/emulated/legacy/Downloads'), + service.sendCommand('settings put global always_finish_activities 0'), + service.sendCommand('pm enable-user com.google.android.gms'), + service.sendCommand('settings put system font_scale 1.0'), + service.sendCommand('su'), + service.sendCommand('echo "chrome --disable-fre --no-default-browser-check --no-first-run" > /data/local/tmp/chrome-command-line'), + service.sendCommand('am set-debug-app --persistent com.android.chrome') + ]) + } + else { + log.warn('Device was not cleared because it in custom group') + } + } + } + catch (err: any) { + log.error('Clear device on group.leave failed: %s', err?.message) + } + finally { + service.releaseWakeLock() + } + }) + + return group + }) diff --git a/lib/units/device/plugins/service.ts b/lib/units/device/plugins/service.ts index 5a6d5bcafe..4187b9224f 100644 --- a/lib/units/device/plugins/service.ts +++ b/lib/units/device/plugins/service.ts @@ -373,13 +373,13 @@ export default syrup.serial() if (!response.success) { throw new Error('Unable to get properties') } - const mapped = response.properties.reduce( (acc: any, property: any) => { acc[property.name] = property.value return acc }, {} ) + if (mapped.imei) { return mapped } diff --git a/lib/units/groups-engine/index.js b/lib/units/groups-engine/index.js index f2aa35adb3..ee3d1f877c 100644 --- a/lib/units/groups-engine/index.js +++ b/lib/units/groups-engine/index.js @@ -10,17 +10,15 @@ export default (async function(options) { const { push , pushdev - , sub - , subdev , channelRouter } = await db.createZMQSockets(options.endpoints, log) - await db.connect(push, pushdev, channelRouter) + await db.connect() devicesWatcher(push, pushdev, channelRouter) usersWatcher(pushdev) lifecycle.observe(() => - [push, sub, pushdev, subdev].forEach((sock) => sock.close()) + [push, pushdev].forEach((sock) => sock.close()) ) log.info('Groups engine started') }) diff --git a/lib/units/groups-engine/watchers/devices.js b/lib/units/groups-engine/watchers/devices.js index d76ce6a9b3..ee6470403f 100644 --- a/lib/units/groups-engine/watchers/devices.js +++ b/lib/units/groups-engine/watchers/devices.js @@ -37,7 +37,7 @@ export default (function(push, pushdev, channelRouter) { delete device.group.lifeTime return device } - pushdev.send([ + push.send([ wireutil.global, wireutil.envelope(new wire.DeviceChangeMessage(publishDevice(), action, device2.group.origin, timeutil.now('nano'))) ]) diff --git a/lib/units/ios-device/plugins/info/index.js b/lib/units/ios-device/plugins/info/index.js index 6557014b97..d857a01937 100644 --- a/lib/units/ios-device/plugins/info/index.js +++ b/lib/units/ios-device/plugins/info/index.js @@ -120,7 +120,10 @@ export default syrup.serial() let scale = parsedResponse.value.scale height *= scale width *= scale + + Object.assign(extendedInfo, {width, height, scale}) log.info('Storing device size/scale') + push.send([ wireutil.global, wireutil.envelope(new wire.SizeIosDevice(options.serial, height, width, scale)) diff --git a/lib/units/ios-device/plugins/wda/client.js b/lib/units/ios-device/plugins/wda/client.js index c67e829af7..cf1d48c2ce 100644 --- a/lib/units/ios-device/plugins/wda/client.js +++ b/lib/units/ios-device/plugins/wda/client.js @@ -10,12 +10,14 @@ import lifecycle from '../../../../util/lifecycle.js' import db from '../../../../db/index.js' import dbapi from '../../../../db/api.js' import devicenotifier from '../devicenotifier.js' +import info from '../info/index.js' import push from '../../../base-device/support/push.js' const LOG_REQUEST_MSG = 'Request has been sent to WDA with data: ' export default syrup.serial() .dependency(devicenotifier) .dependency(push) - .define(async(options, notifier, push) => { + .dependency(info) + .define(async(options, notifier, push, info) => { const log = logger.createLogger('wdaClient') log.info('WdaClient.js initializing...') await db.connect() @@ -422,34 +424,23 @@ export default syrup.serial() return this.deviceSize } log.info('getting device window size...') - return dbapi.getDeviceDisplaySize(options.serial).then((deviceSize) => { - if (!deviceSize) { - return null - } - let dbHeight = deviceSize.height - let dbWidth = deviceSize.width - let dbScale = deviceSize.scale - if (!dbHeight || !dbWidth || !dbScale) { - return null - } - // Reuse DB values: - log.info('Reusing device size/scale') - // Set device size based on orientation, default is PORTRAIT - if (this.orientation === 'PORTRAIT' || !this.orientation) { - this.deviceSize = {height: dbHeight /= dbScale, width: dbWidth /= dbScale} - } - else if (this.orientation === 'LANDSCAPE') { - this.deviceSize = {height: dbWidth /= dbScale, width: dbHeight /= dbScale} - } - else if (this.deviceType === 'Apple TV') { - this.deviceSize = {height: dbHeight, width: dbWidth} - } - return this.deviceSize - }) - .catch((err) => { - log.error('Error getting device size from DB') - return lifecycle.fatal(err) - }) + const {width, height, scale} = info.extendedInfo + + if (!width || !height || !scale) { + return null + } + + // Set device size based on orientation, default is PORTRAIT + if (this.orientation === 'PORTRAIT' || !this.orientation) { + this.deviceSize = {height: height / scale, width: width / scale} + } + else if (this.orientation === 'LANDSCAPE') { + this.deviceSize = {height: width / scale, width: height / scale} + } + else if (this.deviceType === 'Apple TV') { + this.deviceSize = {height: height, width: width} + } + return this.deviceSize } setVersion(currentSession) { log.info('Setting current device version: ' + currentSession.value.capabilities.sdkVersion) diff --git a/lib/units/processor/index.js b/lib/units/processor/index.ts similarity index 57% rename from lib/units/processor/index.js rename to lib/units/processor/index.ts index 0b714c6be1..266594b347 100644 --- a/lib/units/processor/index.js +++ b/lib/units/processor/index.ts @@ -8,68 +8,116 @@ import dbapi from '../../db/models/all/index.js' import lifecycle from '../../util/lifecycle.js' import srv from '../../util/srv.js' import * as zmqutil from '../../util/zmqutil.js' -import {UpdateAccessTokenMessage, DeleteUserMessage, DeviceChangeMessage, UserChangeMessage, GroupChangeMessage, DeviceGroupChangeMessage, GroupUserChangeMessage, DeviceHeartbeatMessage, DeviceLogMessage, TransactionProgressMessage, TransactionDoneMessage, TransactionTreeMessage, InstallResultMessage, DeviceLogcatEntryMessage, TemporarilyUnavailableMessage, UpdateRemoteConnectUrl, InstalledApplications, DeviceIntroductionMessage, InitializeIosDeviceState, DevicePresentMessage, DeviceAbsentMessage, DeviceStatusMessage, DeviceReadyMessage, JoinGroupByAdbFingerprintMessage, JoinGroupByVncAuthResponseMessage, ConnectStartedMessage, ConnectStoppedMessage, JoinGroupMessage, LeaveGroupMessage, DeviceIdentityMessage, AirplaneModeEvent, BatteryEvent, DeviceBrowserMessage, ConnectivityEvent, PhoneStateEvent, RotationEvent, CapabilitiesMessage, ReverseForwardsEvent, SetDeviceDisplay, UpdateIosDevice, SdkIosVersion, SizeIosDevice, DeviceTypeMessage, DeleteDevice, SetAbsentDisconnectedDevices, GetServicesAvailabilityMessage} from '../../wire/wire.js' +import { + UserChangeMessage, + GroupChangeMessage, + DeviceGroupChangeMessage, + GroupUserChangeMessage, + DeviceHeartbeatMessage, + DeviceLogMessage, + TransactionProgressMessage, + TransactionDoneMessage, + TransactionTreeMessage, + InstallResultMessage, + DeviceLogcatEntryMessage, + TemporarilyUnavailableMessage, + UpdateRemoteConnectUrl, + InstalledApplications, + DeviceIntroductionMessage, + InitializeIosDeviceState, + DevicePresentMessage, + DeviceAbsentMessage, + DeviceStatusMessage, + DeviceReadyMessage, + JoinGroupByAdbFingerprintMessage, + JoinGroupByVncAuthResponseMessage, + ConnectStartedMessage, + ConnectStoppedMessage, + JoinGroupMessage, + LeaveGroupMessage, + DeviceIdentityMessage, + AirplaneModeEvent, + BatteryEvent, + DeviceBrowserMessage, + ConnectivityEvent, + PhoneStateEvent, + RotationEvent, + CapabilitiesMessage, + ReverseForwardsEvent, + SetDeviceDisplay, + UpdateIosDevice, + SdkIosVersion, + SizeIosDevice, + DeviceTypeMessage, + DeleteDevice, + SetAbsentDisconnectedDevices, + GetServicesAvailabilityMessage, + DeviceRegisteredMessage, GetPresentDevices, DeviceGetIsInOrigin +} from '../../wire/wire.js' -export default await db.ensureConnectivity(async function(options) { +interface Options { + name: string + endpoints: { + appDealer: string[] + devDealer: string[] + } + publicIp: string +} + +export default db.ensureConnectivity(async(options: Options) => { const log = logger.createLogger('processor') if (options.name) { logger.setGlobalIdentifier(options.name) } - const { - push - , pushdev - , sub - , subdev - , channelRouter - } = await db.createZMQSockets(options.endpoints, log) - await db.connect({push, pushdev, channelRouter}) + await db.connect() // App side const appDealer = zmqutil.socket('dealer') - Promise.all(options.endpoints.appDealer.map(async(endpoint) => { + await Promise.all(options.endpoints.appDealer.map(async(endpoint: string) => { try { - return srv.resolve(endpoint).then(function(records) { - return srv.attempt(records, async function(record) { + return await srv.resolve(endpoint).then((records) => + srv.attempt(records, (record) => { log.info('App dealer connected to "%s"', record.url) appDealer.connect(record.url) return true }) - }) + ) } - catch (err) { - log.fatal('Unable to connect to app dealer endpoint', err) + catch (err: any) { + log.fatal('Unable to connect to app dealer endpoint %s', err?.message) lifecycle.fatal() } })) // Device side const devDealer = zmqutil.socket('dealer') - appDealer.on('message', function(channel, data) { + appDealer.on('message', (channel, data) => { devDealer.send([channel, data]) }) - Promise.all(options.endpoints.devDealer.map(async(endpoint) => { + + const reply = wireutil.reply(wireutil.global) + await Promise.all(options.endpoints.devDealer.map(async(endpoint: string) => { try { - return srv.resolve(endpoint).then(function(records) { - return srv.attempt(records, async function(record) { + return await srv.resolve(endpoint).then((records) => + srv.attempt(records, (record) => { log.info('Device dealer connected to "%s"', record.url) devDealer.connect(record.url) return true }) - }) + ) } - catch (err) { - log.fatal('Unable to connect to dev dealer endpoint', err) + catch (err: any) { + log.fatal('Unable to connect to dev dealer endpoint %s', err?.message) lifecycle.fatal() } })) - const defaultWireHandler = (channel, _, data) => appDealer.send([channel, data]) + const defaultWireHandler = + (channel: string, _: any, data: any) => + appDealer.send([channel, data]) const router = new WireRouter() - .on(UpdateAccessTokenMessage, defaultWireHandler) - .on(DeleteUserMessage, defaultWireHandler) - .on(DeviceChangeMessage, defaultWireHandler) .on(UserChangeMessage, defaultWireHandler) .on(GroupChangeMessage, defaultWireHandler) .on(DeviceGroupChangeMessage, defaultWireHandler) @@ -84,37 +132,35 @@ export default await db.ensureConnectivity(async function(options) { .on(TemporarilyUnavailableMessage, defaultWireHandler) .on(UpdateRemoteConnectUrl, defaultWireHandler) .on(InstalledApplications, defaultWireHandler) - .on(DeviceIntroductionMessage, async(channel, message, data) => { + .on(DeviceIntroductionMessage, async (channel, message, data) => { await dbapi.saveDeviceInitialState(message.serial, message) devDealer.send([ - message.provider.channel, - wireutil.envelope(new wire.DeviceRegisteredMessage(message.serial)) + message.provider!.channel, + wireutil.pack(DeviceRegisteredMessage, {serial: message.serial}) ]) appDealer.send([channel, data]) }) .on(InitializeIosDeviceState, (channel, message, data) => { dbapi.initializeIosDeviceState(options.publicIp, message) }) - .on(DevicePresentMessage, async(channel, message, data) => { + .on(DevicePresentMessage, async (channel, message, data) => { await dbapi.setDevicePresent(message.serial) appDealer.send([channel, data]) }) - .on(DeviceAbsentMessage, async(channel, message, data) => { - if (!message.applications) { - await dbapi.setDeviceAbsent(message.serial) - appDealer.send([channel, data]) - } + .on(DeviceAbsentMessage, async (channel, message, data) => { + await dbapi.setDeviceAbsent(message.serial) + appDealer.send([channel, data]) }) .on(DeviceStatusMessage, (channel, message, data) => { dbapi.saveDeviceStatus(message.serial, message.status) appDealer.send([channel, data]) }) - .on(DeviceReadyMessage, async(channel, message, data) => { + .on(DeviceReadyMessage, async (channel, message, data) => { await dbapi.setDeviceReady(message.serial, message.channel) devDealer.send([message.channel, wireutil.envelope(new wire.ProbeMessage())]) appDealer.send([channel, data]) }) - .on(JoinGroupByAdbFingerprintMessage, async(channel, message, data) => { + .on(JoinGroupByAdbFingerprintMessage, async (channel, message) => { try { const user = await dbapi.lookupUserByAdbFingerprint(message.fingerprint) if (user) { @@ -128,12 +174,11 @@ export default await db.ensureConnectivity(async function(options) { message.currentGroup, wireutil.envelope(new wire.JoinGroupByAdbFingerprintMessage(message.serial, message.fingerprint, message.comment)) ]) - } - catch (/** @type any */ err) { - log.error('Unable to lookup user by ADB fingerprint "%s"', message.fingerprint, err.stack) + } catch (err: any) { + log.error('Unable to lookup user by ADB fingerprint "%s": %s', message.fingerprint, err?.message) } }) - .on(JoinGroupByVncAuthResponseMessage, async(channel, message, data) => { + .on(JoinGroupByVncAuthResponseMessage, async (channel, message) => { try { const user = await dbapi.lookupUserByVncAuthResponse(message.response, message.serial) if (user) { @@ -148,46 +193,48 @@ export default await db.ensureConnectivity(async function(options) { message.currentGroup, wireutil.envelope(new wire.JoinGroupByVncAuthResponseMessage(message.serial, message.response)) ]) - } - catch (/** @type any */ err) { - log.error('Unable to lookup user by VNC auth response "%s"', message.response, err.stack) + } catch (err: any) { + log.error('Unable to lookup user by VNC auth response "%s": %s', message.response, err?.message) } }) - .on(ConnectStartedMessage, async(channel, message, data) => { + .on(ConnectStartedMessage, async (channel, message, data) => { await dbapi.setDeviceConnectUrl(message.serial, message.url) appDealer.send([channel, data]) }) - .on(ConnectStoppedMessage, async(channel, message, data) => { + .on(ConnectStoppedMessage, async (channel, message, data) => { await dbapi.unsetDeviceConnectUrl(message.serial) appDealer.send([channel, data]) }) - .on(JoinGroupMessage, async(channel, message, data) => { - await Promise.all([ - dbapi.setDeviceOwner(message.serial, message.owner), - - message.usage && - dbapi.setDeviceUsage(message.serial, message.usage), - + .on(JoinGroupMessage, async (channel, message, data) => { + await Promise.all([ // @ts-ignore + dbapi.setDeviceState(message.serial, message), dbapi.sendEvent(`device_${message.usage || 'use'}` , {} - , {deviceSerial: message.serial, userEmail: message.owner.email, groupId: message.owner.group} + , {deviceSerial: message.serial, userEmail: message.owner!.email, groupId: message.owner!.group} , Date.now() ) ]) appDealer.send([channel, data]) }) - .on(LeaveGroupMessage, async(channel, message, data) => { + .on(LeaveGroupMessage, async (channel, message, data) => { await Promise.all([ - dbapi.unsetDeviceOwner(message.serial), - dbapi.unsetDeviceUsage(message.serial), + dbapi.setDeviceState(message.serial, {owner: null, usage: null, timeout: 0}), dbapi.sendEvent('device_leave' , {} - , {deviceSerial: message.serial, userEmail: message.owner.email, groupId: message.owner.group} + , {deviceSerial: message.serial, userEmail: message.owner!.email, groupId: message.owner!.group} , Date.now() ) ]) appDealer.send([channel, data]) }) + .on(DeviceGetIsInOrigin, async (channel, message) => { + const device = await dbapi.loadDeviceBySerial(message.serial) + const isInOrigin = device.group.id === device.group.origin + devDealer.send([ + channel, + reply.okay('success', {isInOrigin}) + ]) + }) .on(DeviceIdentityMessage, (channel, message, data) => { dbapi.saveDeviceIdentity(message.serial, message) appDealer.send([channel, data]) @@ -224,68 +271,42 @@ export default await db.ensureConnectivity(async function(options) { dbapi.setDeviceReverseForwards(message.serial, message.forwards) appDealer.send([channel, data]) }) - .on(SetDeviceDisplay, (channel, message, data) => { - dbapi - .setDeviceSocketDisplay(message) - .then(function(response) { - log.info('setDeviceSocketDisplay response: %s', response) - }) - .catch(function(err) { - log.error('setDeviceSocketDisplay', err) - }) - }) - .on(UpdateIosDevice, (channel, message, data) => { - dbapi - .updateIosDevice(message) - .then(result => { - log.info('UpdateIosDevice: %s', result) - }) - .catch(err => { - log.info('UpdateIosDevice error: %s', err?.message) - }) - }) + .on(UpdateIosDevice, (channel, message, data) => + dbapi.updateIosDevice(message) + ) .on(SdkIosVersion, (channel, message, data) => { - dbapi - .setDeviceIosVersion(message) - .then(result => { - log.info('SdkIosVersion: %s', result) - }) - .catch(err => { - log.info('SdkIosVersion error: %s', err?.message) - }) + dbapi.setDeviceIosVersion(message) }) .on(SizeIosDevice, (channel, message, data) => { - dbapi.sizeIosDevice(message.id, message.height, message.width, message.scale).then(result => { - log.info('SizeIosDevice: %s', result) - }).catch(err => { - log.info('SizeIosDevice: %s', err?.message) - }) + dbapi.sizeIosDevice(message.id, message.height, message.width, message.scale) appDealer.send([channel, data]) }) .on(DeviceTypeMessage, (channel, message, data) => { dbapi.setDeviceType(message.serial, message.type) }) - .on(DeleteDevice, (channel, message, data) => { - dbapi.deleteDevice(message.serial) - }) - .on(SetAbsentDisconnectedDevices, (channel, message, data) => { - dbapi.setAbsentDisconnectedDevices() - }) .on(GetServicesAvailabilityMessage, (channel, message, data) => { dbapi.setDeviceServicesAvailability(message.serial, message) appDealer.send([channel, data]) }) - .handler() + .on(GetPresentDevices, async (channel, message, data) => { + const devices = await dbapi.loadPresentDevices() + .then(devices => devices.map(d => d.serial)) + devDealer.send([ + channel, + reply.okay('success', {devices}) + ]) + }) + .handler(); devDealer.on('message', router) - lifecycle.observe(function() { - [appDealer, devDealer, push, pushdev, sub, subdev].forEach(function(sock) { + lifecycle.observe(() => { + ;[appDealer, devDealer].forEach(function(sock) { try { sock.close() } - catch (err) { - log.error('Error while closing socket "%s"', err.stack) + catch (err: any) { + log.error('Error while closing socket "%s"', err?.message) } }) }) diff --git a/lib/units/provider/ADBObserver.ts b/lib/units/provider/ADBObserver.ts index 33fcfade03..6600448e88 100644 --- a/lib/units/provider/ADBObserver.ts +++ b/lib/units/provider/ADBObserver.ts @@ -14,7 +14,16 @@ interface ADBDeviceEntry { state: ADBDevice['type'] } -class ADBObserver extends EventEmitter { +type PrevADBDeviceType = ADBDevice['type'] + +interface ADBEvents { + connect: [ADBDevice] + update: [ADBDevice, PrevADBDeviceType] + disconnect: [ADBDevice] + error: [Error] +} + +class ADBObserver extends EventEmitter { static instance: ADBObserver | null = null private readonly intervalMs: number = 1000 // Default 1 second polling @@ -28,7 +37,10 @@ class ADBObserver extends EventEmitter { private shouldContinuePolling: boolean = false private connection: Socket | null = null private isConnecting: boolean = false - private pendingRequests: Map void; reject: (error: Error) => void}> = new Map() + private pendingRequests: Map void + reject: (error: Error) => void + }> = new Map() constructor(options?: {intervalMs?: number; host?: string; port?: number}) { if (ADBObserver.instance) { @@ -136,7 +148,7 @@ class ADBObserver extends EventEmitter { } } } - catch (error) { + catch (error: any) { this.emit('error', error) } finally { diff --git a/lib/units/provider/index.ts b/lib/units/provider/index.ts index 9156a0884f..912abc50d3 100644 --- a/lib/units/provider/index.ts +++ b/lib/units/provider/index.ts @@ -6,8 +6,6 @@ import * as procutil from '../../util/procutil.js' import lifecycle from '../../util/lifecycle.js' import srv from '../../util/srv.js' import * as zmqutil from '../../util/zmqutil.js' -import db from '../../db/index.js' -import dbapi from '../../db/api.js' import {ChildProcess} from 'node:child_process' import ADBObserver, {ADBDevice} from './ADBObserver.js' import { DeviceRegisteredMessage } from '../../wire/wire.ts' @@ -39,7 +37,6 @@ export interface Options { export default (async function(options: Options) { const log = logger.createLogger('provider') - await db.connect() // Check whether the ipv4 address contains a port indication if (options.adbHost.includes(':')) { @@ -133,10 +130,9 @@ export default (async function(options: Options) { // Tell others we found a device push.send([ wireutil.global, - wireutil.envelope(new wire.DeviceIntroductionMessage(device.serial, wireutil.toDeviceStatus(device.type), new wire.ProviderMessage(solo, options.name))) + wireutil.envelope(new wire.DeviceIntroductionMessage(device.serial, wireutil.toDeviceStatus(device.type), new wire.ProviderMessage(solo, options.name), options.deviceType)) ]) - dbapi.setDeviceType(device.serial, options.deviceType) process.nextTick(() => { // after creating workers[device.serial] obj if (workers[device.serial]) { workers[device.serial].resolveRegister = () => resolve() diff --git a/lib/units/reaper/index.js b/lib/units/reaper/index.js deleted file mode 100644 index af4a59f442..0000000000 --- a/lib/units/reaper/index.js +++ /dev/null @@ -1,109 +0,0 @@ -import Promise from 'bluebird' -import logger from '../../util/logger.js' -import wire from '../../wire/index.js' -import wireutil from '../../wire/util.js' -import {WireRouter} from '../../wire/router.js' -import dbapi from '../../db/api.js' -import lifecycle from '../../util/lifecycle.js' -import srv from '../../util/srv.js' -import TtlSet from '../../util/ttlset.js' -import * as zmqutil from '../../util/zmqutil.js' -import db from '../../db/index.js' -import {DeviceIntroductionMessage, DeviceHeartbeatMessage, DeviceAbsentMessage} from '../../wire/wire.js' - -const log = logger.createLogger('reaper') -export default (async function(options) { - await db.connect() - - /** @type {any} */ - const ttlset = new TtlSet(options.heartbeatTimeout) - - // Input - const sub = zmqutil.socket('sub') - Promise.map(options.endpoints.sub, function(endpoint) { - return srv.resolve(endpoint).then(function(records) { - return srv.attempt(records, function(record) { - log.info('Receiving input from "%s"', record.url) - sub.connect(record.url) - return Promise.resolve(true) - }) - }) - }) - .catch(function(err) { - log.fatal('Unable to connect to sub endpoint', err) - lifecycle.fatal() - }); - [wireutil.global].forEach(function(channel) { - log.info('Subscribing to permanent channel "%s"', channel) - sub.subscribe(channel) - }) - // Output - const push = zmqutil.socket('push') - Promise.map(options.endpoints.push, function(endpoint) { - return srv.resolve(endpoint).then(function(records) { - return srv.attempt(records, function(record) { - log.info('Sending output to "%s"', record.url) - push.connect(record.url) - return Promise.resolve(true) - }) - }) - }) - .catch(function(err) { - log.fatal('Unable to connect to push endpoint', err) - lifecycle.fatal() - }) - ttlset.on('insert', function(serial) { - log.info('Device "%s" is present', serial) - push.send([ - wireutil.global, - wireutil.envelope(new wire.DevicePresentMessage(serial)) - ]) - }) - ttlset.on('drop', function(serial) { - log.info('Reaping device "%s" due to heartbeat timeout', serial) - push.send([ - wireutil.global, - wireutil.envelope(new wire.DeviceAbsentMessage(serial)) - ]) - }) - function loadInitialState() { - return dbapi.loadPresentDevices() - .then(function(devices) { - let now = Date.now() - devices.forEach(function(device) { - ttlset.bump(device.serial, now, TtlSet.SILENT) - }) - }) - } - function listenToChanges() { - sub.on('message', new WireRouter() - .on(DeviceIntroductionMessage, function(channel, message) { - message.status = 3 - ttlset.drop(message.serial, TtlSet.SILENT) - ttlset.bump(message.serial, Date.now()) - }) - .on(DeviceHeartbeatMessage, function(channel, message) { - ttlset.bump(message.serial, Date.now()) - }) - .on(DeviceAbsentMessage, function(channel, message) { - ttlset.drop(message.serial, TtlSet.SILENT) - }) - .handler()) - } - log.info('Reaping devices with no heartbeat') - lifecycle.observe(function() { - [push, sub].forEach(function(sock) { - try { - sock.close() - } - catch (err) { - log.error(err) - } - }) - ttlset.stop() - }) - loadInitialState().then(listenToChanges).catch(function(err) { - log.fatal('Unable to load initial state', err) - lifecycle.fatal() - }) -}) diff --git a/lib/units/reaper/index.ts b/lib/units/reaper/index.ts new file mode 100644 index 0000000000..0db7898f36 --- /dev/null +++ b/lib/units/reaper/index.ts @@ -0,0 +1,120 @@ +import logger from '../../util/logger.js' +import { + DeviceAbsentMessage, + DeviceHeartbeatMessage, + DeviceIntroductionMessage, DevicePresentMessage, + GetPresentDevices +} from '../../wire/wire.js' +import wireutil from '../../wire/util.js' +import {WireRouter} from '../../wire/router.js' +import lifecycle from '../../util/lifecycle.js' +import srv from '../../util/srv.js' +import TTLSet from '../../util/ttlset.js' +import * as zmqutil from '../../util/zmqutil.js' +import {runTransactionDev} from '../../wire/transmanager.js' + +const log = logger.createLogger('reaper') + +interface Options { + heartbeatTimeout: number + endpoints: { + sub: string[] + push: string[] + } +} + +export default (async(options: Options) => { + const ttlset = new TTLSet(options.heartbeatTimeout) + + // Input + const sub = zmqutil.socket('sub') + await Promise.all(options.endpoints.sub.map((endpoint: string) => + srv.resolve(endpoint).then(records => + srv.attempt(records, (record) => { + log.info('Receiving input from "%s"', record.url) + sub.connect(record.url) + }) + ) + )).catch((err) => { + log.fatal('Unable to connect to sub endpoint %s', err?.message) + lifecycle.fatal() + }) + + ;[wireutil.global].forEach(channel => { + log.info('Subscribing to permanent channel "%s"', channel) + sub.subscribe(channel) + }) + + // Output + const push = zmqutil.socket('push') + await Promise.all(options.endpoints.push.map((endpoint: string) => + srv.resolve(endpoint).then(records => + srv.attempt(records, (record) => { + log.info('Sending output to "%s"', record.url) + push.connect(record.url) + }) + ) + )).catch((err) => { + log.fatal('Unable to connect to push endpoint: %s', err?.message) + lifecycle.fatal() + }) + + ttlset.on('insert', (serial) => { + log.info('Device "%s" is present', serial) + push.send([ + wireutil.global, + wireutil.pack(DevicePresentMessage, {serial}) + ]) + }) + + ttlset.on('drop', (serial) => { + log.info('Reaping device "%s" due to heartbeat timeout', serial) + push.send([ + wireutil.global, + wireutil.pack(DeviceAbsentMessage, {serial}) + ]) + }) + + lifecycle.observe(() => { + [push, sub].forEach(sock => { + try { + sock.close() + } + catch (err: any) { + // no-op + } + }) + ttlset.stop() + }) + + try { + log.info('Reaping devices with no heartbeat') + + const router = new WireRouter() + .on(DeviceIntroductionMessage, (channel, message) => { + ttlset.drop(message.serial, TTLSet.SILENT) + ttlset.bump(message.serial, Date.now()) + }) + .on(DeviceHeartbeatMessage, (channel, message) => { + ttlset.bump(message.serial, Date.now()) + }) + .on(DeviceAbsentMessage, (channel, message) => { + ttlset.drop(message.serial, TTLSet.SILENT) + }) + + // Listen to changes + sub.on('message', router.handler()) + + // Load initial state + const {devices} = await runTransactionDev(wireutil.global, GetPresentDevices, {}, {sub, push, router}) + + const now = Date.now() + devices.forEach((serial: string) => { + ttlset.bump(serial, now, TTLSet.SILENT) + }) + } + catch (err: any) { + log.fatal('Unable to load initial state: %s', err?.message) + lifecycle.fatal() + } +}) diff --git a/lib/units/tizen-device/plugins/launcher.js b/lib/units/tizen-device/plugins/launcher.js index ae9839a883..42339ccf40 100644 --- a/lib/units/tizen-device/plugins/launcher.js +++ b/lib/units/tizen-device/plugins/launcher.js @@ -5,7 +5,7 @@ import group from '../../base-device/plugins/group.js' import sdb from './sdb/index.js' import wire from '../../../wire/index.js' import wireutil from '../../../wire/util.js' -import webinspector from './webinspector.js' +import webinspector from './webinspector/index.js' import {GetInstalledApplications, KillDeviceApp, LaunchDeviceApp, TerminateDeviceApp} from '../../../wire/wire.js' export default syrup.serial() diff --git a/lib/units/tizen-device/plugins/webinspector/Replicator.ts b/lib/units/tizen-device/plugins/webinspector/Replicator.ts new file mode 100644 index 0000000000..4cb8800f01 --- /dev/null +++ b/lib/units/tizen-device/plugins/webinspector/Replicator.ts @@ -0,0 +1,588 @@ +import {Transform} from './transform/index.js' + +// Constants +const TRANSFORMED_TYPE_KEY = '@t' +const CIRCULAR_REF_KEY = '@r' +const KEY_REQUIRE_ESCAPING_RE = /^#*@(t|r)$/ +const REMAINING_KEY = '__console_feed_remaining__' +const GLOBAL = (function getGlobal() { + // NOTE: see http://www.ecma-international.org/ecma-262/6.0/index.html#sec-performeval step 10 + // eslint-disable-next-line no-eval + const savedEval = eval + return savedEval('this') +})() +const ARRAY_BUFFER_SUPPORTED = typeof ArrayBuffer === 'function' +const MAP_SUPPORTED = typeof Map === 'function' +const SET_SUPPORTED = typeof Set === 'function' +const TYPED_ARRAY_CTORS = [ + 'Int8Array', + 'Uint8Array', + 'Uint8ClampedArray', + 'Int16Array', + 'Uint16Array', + 'Int32Array', + 'Uint32Array', + 'Float32Array', + 'Float64Array', +] as const + +const arrSlice = Array.prototype.slice + +interface Serializer { + serialize(val: any): string + deserialize(val: string): any +} + +interface CircularCandidateDescriptor { + parent: any + key: string | number + refIdx: number +} + +interface TransformedObject { + [TRANSFORMED_TYPE_KEY]: string + data: any +} + +interface CircularReference { + [CIRCULAR_REF_KEY]: number +} + +// Default serializer +const JSONSerializer: Serializer = { + serialize: (val: any): string => JSON.stringify(val), + deserialize: (val: string): any => JSON.parse(val) +} + +class EncodingTransformer { + private readonly references: any + private readonly transforms: Transform[] + private readonly transformsMap: Map | undefined + private readonly circularCandidates: any[] = [] + private readonly circularCandidatesDescrs: CircularCandidateDescriptor[] = [] + private circularRefCount: number = 0 + private readonly limit: number + + constructor(val: any, transforms: Transform[], limit?: number) { + this.references = val + this.transforms = transforms + this.transformsMap = this._makeTransformsMap() + this.limit = limit ?? Infinity + } + + private static _createRefMark(idx: number): CircularReference { + const obj = Object.create(null) + obj[CIRCULAR_REF_KEY] = idx + return obj + } + + private _createCircularCandidate(val: any, parent: any, key: string | number): void { + this.circularCandidates.push(val) + this.circularCandidatesDescrs.push({parent, key, refIdx: -1}) + } + + private _applyTransform(val: any, parent: any, key: string | number, transform: Transform): TransformedObject { + const result = Object.create(null) + const serializableVal = transform.toSerializable(val) + + if (typeof serializableVal === 'object' && serializableVal !== null) { + this._createCircularCandidate(val, parent, key) + } + + result[TRANSFORMED_TYPE_KEY] = transform.type + result.data = this._handleValue(() => serializableVal, parent, key) + return result + } + + private _handleArray = (arr: any[]): any[] => { + const result: any[] = [] + const arrayLimit = Math.min(arr.length, this.limit) + const remaining = arr.length - arrayLimit + + for (let i = 0; i < arrayLimit; i++) { + result[i] = this._handleValue(() => arr[i], result, i) + } + + result[arrayLimit] = REMAINING_KEY + remaining + + return result + } + + private _handlePlainObject(obj: Record): Record { + const result = Object.create(null) + let counter = 0 + let total = 0 + + for (const key in obj) { + if (Reflect.has(obj, key)) { + if (counter >= this.limit) { + total++ + continue + } + + const resultKey = KEY_REQUIRE_ESCAPING_RE.test(key) ? '#' + key : key + result[resultKey] = this._handleValue(() => obj[key], result, resultKey) + counter++ + total++ + } + } + + const remaining = total - counter + // eslint-disable-next-line no-proto + const name = obj?.__proto__?.constructor?.name + + if (name && name !== 'Object') { + result.constructor = {name} + } + + if (remaining) { + result[REMAINING_KEY] = remaining + } + + return result + } + + private _handleObject(obj: any, parent: any, key: string | number): any { + this._createCircularCandidate(obj, parent, key) + return Array.isArray(obj) ? this._handleArray(obj) : this._handlePlainObject(obj) + } + + private _ensureCircularReference(obj: any): CircularReference | null { + const circularCandidateIdx = this.circularCandidates.indexOf(obj) + + if (circularCandidateIdx > -1) { + const descr = this.circularCandidatesDescrs[circularCandidateIdx] + + if (descr.refIdx === -1) { + descr.refIdx = descr.parent ? ++this.circularRefCount : 0 + } + + return EncodingTransformer._createRefMark(descr.refIdx) + } + + return null + } + + private _handleValue(getVal: () => any, parent: any, key: string | number): any { + try { + const val = getVal() + const type = typeof val + const isObject = type === 'object' && val !== null + + if (isObject) { + const refMark = this._ensureCircularReference(val) + if (refMark) { + return refMark + } + } + + const transform = this._findTransform(type, val) + if (transform) { + return this._applyTransform(val, parent, key, transform) + } + + if (isObject) { + return this._handleObject(val, parent, key) + } + + return val + } + catch (e) { + try { + return this._handleValue(() => (e instanceof Error ? e : new Error(String(e))), parent, key as string | number) + } + catch { + return null + } + } + } + + private _makeTransformsMap(): Map | undefined { + if (!MAP_SUPPORTED) { + return undefined + } + + const map = new Map() + this.transforms.forEach(transform => { + if (transform.lookup) { + map.set(transform.lookup, transform) + } + }) + + return map + } + + private _findTransform(type: string, val: any): Transform | undefined { + if (MAP_SUPPORTED && this.transformsMap) { + if (val?.constructor) { + const transform = this.transformsMap.get(val.constructor) + if (transform?.shouldTransform(type, val)) { + return transform + } + } + } + + for (const transform of this.transforms) { + if (transform.shouldTransform(type, val)) { + return transform + } + } + + return undefined + } + + transform(): any[] { + const references = [this._handleValue(() => this.references, null, 0)] + + for (const descr of this.circularCandidatesDescrs) { + if (descr.refIdx > 0) { + references[descr.refIdx] = descr.parent[descr.key] + descr.parent[descr.key] = EncodingTransformer._createRefMark(descr.refIdx) + } + } + + return references + } +} + +class DecodingTransformer { + private readonly activeTransformsStack: any[] = [] + private readonly visitedRefs: Record = Object.create(null) + private readonly references: any[] + private readonly transformMap: Record + + constructor(references: any[], transformsMap: Record) { + this.references = references + this.transformMap = transformsMap + } + + private _handlePlainObject(obj: Record): void { + const unescaped: Record = Object.create(null) + + if ('constructor' in obj) { + if (!obj.constructor || typeof obj.constructor.name !== 'string') { + // @ts-ignore + obj.constructor = {name: 'Object'} + } + } + + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + this._handleValue(obj[key], obj, key) + + if (KEY_REQUIRE_ESCAPING_RE.test(key)) { + // NOTE: use intermediate object to avoid unescaped and escaped keys interference + // E.g. unescaped "##@t" will be "#@t" which can overwrite escaped "#@t". + unescaped[key.substring(1)] = obj[key] + delete obj[key] + } + } + } + + for (const unescapedKey in unescaped) { + obj[unescapedKey] = unescaped[unescapedKey] + } + } + + private _handleTransformedObject(obj: TransformedObject, parent: any, key: string | number): void { + const transformType = obj[TRANSFORMED_TYPE_KEY] + const transform = this.transformMap[transformType] + + if (!transform) { + throw new Error(`Can't find transform for "${transformType}" type.`) + } + + this.activeTransformsStack.push(obj) + this._handleValue(obj.data, obj, 'data') + this.activeTransformsStack.pop() + parent[key] = transform.fromSerializable(obj.data) + } + + private _handleCircularSelfRefDuringTransform(refIdx: number, parent: any, key: string | number): void { + // NOTE: we've hit a hard case: object reference itself during transformation. + // We can't dereference it since we don't have resulting object yet. And we'll + // not be able to restore reference lately because we will need to traverse + // transformed object again and reference might be unreachable or new object contain + // new circular references. As a workaround we create getter, so once transformation + // complete, dereferenced property will point to correct transformed object. + const references = this.references + + Object.defineProperty(parent, key, { // @ts-ignore + val: undefined, + configurable: true, + enumerable: true, + get(this: { val: any }) { + if (this.val === undefined) { + this.val = references[refIdx] + } + return this.val + }, + set(this: { val: any }, value: any) { + this.val = value + } + }) + } + + private _handleCircularRef(refIdx: number, parent: any, key: string | number): void { + if (this.activeTransformsStack.includes(this.references[refIdx])) { + this._handleCircularSelfRefDuringTransform(refIdx, parent, key) + } + else { + if (!this.visitedRefs[refIdx]) { + this.visitedRefs[refIdx] = true + this._handleValue(this.references[refIdx], this.references, refIdx) + } + parent[key] = this.references[refIdx] + } + } + + private _handleValue(val: any, parent: any, key: string | number): void { + if (typeof val !== 'object' || val === null) { + return + } + + const refIdx = (val as CircularReference)[CIRCULAR_REF_KEY] + + if (refIdx !== undefined) { + this._handleCircularRef(refIdx, parent, key) + } + else if ((val as TransformedObject)[TRANSFORMED_TYPE_KEY]) { + this._handleTransformedObject(val as TransformedObject, parent, key) + } + else if (Array.isArray(val)) { + for (let i = 0; i < val.length; i++) { + this._handleValue(val[i], val, i) + } + } + else { + this._handlePlainObject(val) + } + } + + transform(): any { + this.visitedRefs[0] = true + this._handleValue(this.references[0], this.references, 0) + return this.references[0] + } +} + +// Built-in transforms with optimized implementations +const builtInTransforms: Transform[] = [ + { + type: '[[NaN]]', + shouldTransform: (type: string, val: any): boolean => type === 'number' && isNaN(val), + toSerializable: (): string => '', + fromSerializable: (): number => NaN + }, + { + type: '[[undefined]]', + shouldTransform: (type: string): boolean => type === 'undefined', + toSerializable: (): string => '', + fromSerializable: (): undefined => undefined + }, + { + type: '[[Date]]', + lookup: Date, + shouldTransform: (type: string, val: any): boolean => val instanceof Date, + toSerializable: (date: Date): number => date.getTime(), + fromSerializable: (val: number): Date => { + const date = new Date() + date.setTime(val) + return date + } + }, + { + type: '[[RegExp]]', + lookup: RegExp, + shouldTransform: (type: string, val: any): boolean => val instanceof RegExp, + toSerializable: (re: RegExp): { src: string; flags: string } => { + const result = { + src: re.source, + flags: '' + } + + if (re.global) { + result.flags += 'g' + } + if (re.ignoreCase) { + result.flags += 'i' + } + if (re.multiline) { + result.flags += 'm' + } + + return result + }, + fromSerializable: (val: { src: string; flags: string }): RegExp => new RegExp(val.src, val.flags) + }, + { + type: '[[Error]]', + lookup: Error, + shouldTransform: (type: string, val: any): boolean => val instanceof Error, + toSerializable: (err: Error): { name: string; message: string; stack?: string } => { + if (!err.stack) { + (Error as any).captureStackTrace?.(err) + } + + return { + name: err.name, + message: err.message, + stack: err.stack + } + }, + fromSerializable: (val: { name: string; message: string; stack?: string }): Error => { + const Ctor = (GLOBAL as any)[val.name] || Error + const err = new Ctor(val.message) + err.stack = val.stack + return err + } + }, + { + type: '[[ArrayBuffer]]', + lookup: ARRAY_BUFFER_SUPPORTED && ArrayBuffer, + shouldTransform: (type: string, val: any): boolean => ARRAY_BUFFER_SUPPORTED && val instanceof ArrayBuffer, + toSerializable: (buffer: ArrayBuffer): number[] => { + const view = new Int8Array(buffer) + return arrSlice.call(view) + }, + fromSerializable: (val: number[]): ArrayBuffer | number[] => { + if (ARRAY_BUFFER_SUPPORTED) { + const buffer = new ArrayBuffer(val.length) + const view = new Int8Array(buffer) + view.set(val) + return buffer + } + return val + } + }, + { + type: '[[TypedArray]]', + shouldTransform: (type: string, val: any): boolean => { + if (ARRAY_BUFFER_SUPPORTED) { + return ArrayBuffer.isView(val) && !(val instanceof DataView) + } + + for (const ctorName of TYPED_ARRAY_CTORS) { + if (typeof (GLOBAL as any)[ctorName] === 'function' && val instanceof (GLOBAL as any)[ctorName]) { + return true + } + } + + return false + }, + toSerializable: (arr: any): { ctorName: string; arr: any[] } => ({ + ctorName: arr.constructor.name, + arr: arrSlice.call(arr) + }), + fromSerializable: (val: { ctorName: string; arr: any[] }): any => { + return typeof (GLOBAL as any)[val.ctorName] === 'function' ? + new (GLOBAL as any)[val.ctorName](val.arr) : + val.arr + } + }, + { + type: '[[Map]]', + lookup: MAP_SUPPORTED && Map, + shouldTransform: (type: string, val: any): boolean => MAP_SUPPORTED && val instanceof Map, + toSerializable: (map: Map): any[] => { + const flattenedKVArr: any[] = [] + map.forEach((val, key) => { + flattenedKVArr.push(key, val) + }) + return flattenedKVArr + }, + fromSerializable: (val: any[]): Map | any[][] => { + if (MAP_SUPPORTED) { + // NOTE: new Map(iterable) is not supported by all browsers + const map = new Map() + for (let i = 0; i < val.length; i += 2) { + map.set(val[i], val[i + 1]) + } + return map + } + + const kvArr: any[][] = [] + for (let j = 0; j < val.length; j += 2) { + kvArr.push([val[j], val[j + 1]]) + } + return kvArr + } + }, + { + type: '[[Set]]', + lookup: SET_SUPPORTED && Set, + shouldTransform: (type: string, val: any): boolean => SET_SUPPORTED && val instanceof Set, + toSerializable: (set: Set): any[] => { + const arr: any[] = [] + set.forEach(val => arr.push(val)) + return arr + }, + fromSerializable: (val: any[]): Set | any[] => { + if (SET_SUPPORTED) { + // NOTE: new Set(iterable) is not supported by all browsers + const set = new Set() + for (const item of val) { + set.add(item) + } + return set + } + return val + } + } +] + +class Replicator { + private readonly transforms: Transform[] = [] + private readonly transformsMap: Record = Object.create(null) + private readonly serializer: Serializer + + constructor(serializer?: Serializer) { + this.serializer = serializer || JSONSerializer + this.addTransforms(builtInTransforms) + } + + addTransforms(transforms: Transform | Transform[]): this { + const transformsArray = Array.isArray(transforms) ? transforms : [transforms] + + for (const transform of transformsArray) { + if (this.transformsMap[transform.type]) { + throw new Error(`Transform with type "${transform.type}" was already added.`) + } + + this.transforms.push(transform) + this.transformsMap[transform.type] = transform + } + + return this + } + + removeTransforms(transforms: Transform | Transform[]): this { + const transformsArray = Array.isArray(transforms) ? transforms : [transforms] + + for (const transform of transformsArray) { + const idx = this.transforms.indexOf(transform) + + if (idx > -1) { + this.transforms.splice(idx, 1) + } + + delete this.transformsMap[transform.type] + } + + return this + } + + encode(val: any, limit?: number): string { + const transformer = new EncodingTransformer(val, this.transforms, limit) + const references = transformer.transform() + return this.serializer.serialize(references) + } + + decode(val: string): any { + const references = this.serializer.deserialize(val) + const transformer = new DecodingTransformer(references, this.transformsMap) + return transformer.transform() + } +} + +export default Replicator diff --git a/lib/units/tizen-device/plugins/webinspector.js b/lib/units/tizen-device/plugins/webinspector/index.ts similarity index 67% rename from lib/units/tizen-device/plugins/webinspector.js rename to lib/units/tizen-device/plugins/webinspector/index.ts index 0d3c4944e2..29d029187b 100644 --- a/lib/units/tizen-device/plugins/webinspector.js +++ b/lib/units/tizen-device/plugins/webinspector/index.ts @@ -1,20 +1,23 @@ -import push from '../../base-device/support/push.js' -import router from '../../base-device/support/router.js' -import group from '../../base-device/plugins/group.js' -import cdp from './cdp/index.js' -import wire from '../../../wire/index.js' -import wireutil from '../../../wire/util.js' -import logger from '../../../util/logger.js' - -import {Encode} from 'console-feed' +import push from '../../../base-device/support/push.js' +import router from '../../../base-device/support/router.js' +import group from '../../../base-device/plugins/group.js' +import cdp, {CDPClient} from '../cdp/index.js' +import wireutil from '../../../../wire/util.js' +import logger from '../../../../util/logger.js' + import syrup from '@devicefarmer/stf-syrup' import webSocketServer from 'ws' import _ from 'lodash' -import urlformat from '../../base-device/support/urlformat.js' +import urlformat from '../../../base-device/support/urlformat.js' +import MyReplicator from './Replicator.js' +import * as transform from './transform/index.js' +import {GetAppAsset, GetAppAssetsList, GetAppHTML, GetAppInspectServerUrl} from "../../../../wire/wire.js"; const consoleListeners = new Map() +const replicator = new MyReplicator() +replicator.addTransforms(Object.values(transform)) -const inspectServer = (port, cdp, log) => +const inspectServer = (port: number, cdp: CDPClient, log: any) => new webSocketServer.Server({port}) .on('connection', async(ws, req) => { try { @@ -45,12 +48,11 @@ const inspectServer = (port, cdp, log) => const out = Object.keys(result)?.length === 1 && 'type' in result ? result.type : (result?.value || result) - // eslint-disable-next-line new-cap - ws.send(JSON.stringify(Encode({ + ws.send(replicator.encode({ method: 'log', timestamp: new Date().toISOString(), data: [out] - })), () => {}) + }), () => {}) }) ws.on('close', () => { @@ -59,20 +61,20 @@ const inspectServer = (port, cdp, log) => }) if (!consoleListeners.has(remoteAddress)) { - consoleListeners.set(remoteAddress, (arg, method, timestamp) => { - if (arg.value === '--mut--') { + consoleListeners.set(remoteAddress, (arg: any, method: string, timestamp: number) => { + if (arg?.value === '--mut--') { sendHTML() return } - // eslint-disable-next-line new-cap - ws.send(JSON.stringify(Encode({ + + ws.send(replicator.encode({ method, timestamp, data: [arg], - }))) + })) }) } } - catch (/** @type {any} */ err) { + catch (err: any) { log.error('Updates server :', err) ws.send(JSON.stringify({error: err?.message || err})) } @@ -87,11 +89,11 @@ export default syrup.serial() .define((options, push, router, cdp, group, urlformat) => { const log = logger.createLogger('tizen-device:plugins:webinspector') const reply = wireutil.reply(options.serial) - let frameId + let frameId: string | null = null - const success = (channel, body) => + const success = (channel: string, body: any) => push.send([channel, reply.okay('success', body)]) - const fail = (channel, err) => + const fail = (channel: string, err: any) => push.send([channel, reply.fail('fail', err?.message || err)]) const getAssetsList = async() => { @@ -102,7 +104,7 @@ export default syrup.serial() return result } - const getAsset = async(url) => { + const getAsset = async(url: string) => { if (!frameId) { await getAssetsList() } @@ -111,36 +113,39 @@ export default syrup.serial() } const wsUrl = urlformat(options.updWsUrlPattern, options.publicPort) + let inspServer: webSocketServer.Server | null = null - const handlers = { - + router // TODO: Create download endpoint - [wire.GetAppAsset .$code]: (channel, message) => getAsset(message.url).then(asset => - success(channel, asset) - ).catch(err => - fail(channel, err) - ), - - [wire.GetAppAssetsList .$code]: (channel) => getAssetsList().then(list => - success(channel, list) - ).catch(err => - fail(channel, err) - ), - - [wire.GetAppHTML .$code]: (channel) => cdp.getHTML().then(content => - success(channel, {content, base64Encoded: false}) - ).catch(err => - fail(channel, err) - ), - - [wire.GetAppInspectServerUrl .$code]: (channel) => - success(channel, wsUrl) - } - let inspServer = null + .on(GetAppAsset, (channel: string, message: any) => !!inspServer && + getAsset(message.url).then(asset => + success(channel, asset) + ).catch(err => + fail(channel, err) + )) + + .on(GetAppAssetsList, (channel: string) => !!inspServer && + getAssetsList().then(list => + success(channel, list) + ).catch(err => + fail(channel, err) + )) + + .on(GetAppHTML, (channel: string) => !!inspServer && + cdp.getHTML().then(content => + success(channel, {content, base64Encoded: false}) + ).catch(err => + fail(channel, err) + )) + + .on(GetAppInspectServerUrl, (channel: string) => + !!inspServer && success(channel, wsUrl) + ) + const plugin = { host: '', port: 0, - start: async(port, host = options.host) => { + start: async(port: number, host = options.host) => { plugin.host = host plugin.port = port @@ -149,15 +154,9 @@ export default syrup.serial() if (!inspServer) { inspServer = inspectServer(options.publicPort, cdp, log) } - - Object.entries(handlers) - .map(([event, handler]) => router.on(event, handler)) }, stop: async() => { - Object.entries(handlers) - .map(([event, handler]) => router.removeListener(event, handler)) - frameId = null await new Promise(r => { inspServer?.close(() => r) @@ -174,7 +173,7 @@ export default syrup.serial() const {Runtime} = cdp.client Runtime.on('consoleAPICalled', _.debounce((event) => { log.info('Send console output to listeners [ %s ]', event.type) - event.args.forEach((arg) => + event.args.forEach((arg: any) => consoleListeners.forEach(fn => fn(arg, event.type, event.timestamp) ) diff --git a/lib/units/tizen-device/plugins/webinspector/transform/BigInt.ts b/lib/units/tizen-device/plugins/webinspector/transform/BigInt.ts new file mode 100644 index 0000000000..cd7db82677 --- /dev/null +++ b/lib/units/tizen-device/plugins/webinspector/transform/BigInt.ts @@ -0,0 +1,23 @@ +import {Transform} from './index.js' + +/** + * Optimized BigInt transform for serialization/deserialization + * Serialize a `bigint` to a string with 'n' suffix for efficient parsing + */ +const BigIntTransform: Transform = { + type: 'BigInt', + + shouldTransform: (_type: string, obj: any): obj is bigint => { + return typeof obj === 'bigint' + }, + + toSerializable: (value: bigint): string => { + return `${value}n` + }, + + fromSerializable: (data: string): bigint => { + return BigInt(data.slice(0, -1)) + } +} + +export default BigIntTransform diff --git a/lib/units/tizen-device/plugins/webinspector/transform/Function.ts b/lib/units/tizen-device/plugins/webinspector/transform/Function.ts new file mode 100644 index 0000000000..1b2f80adb9 --- /dev/null +++ b/lib/units/tizen-device/plugins/webinspector/transform/Function.ts @@ -0,0 +1,85 @@ +import {Transform} from './index.js' + +/** + * Serialized function structure + */ +interface SerializedFunction { + name?: string + body?: string + proto?: string +} + +/** + * Transform for serializing functions into JSON-compatible format + * Note: This creates placeholder functions, not executable ones + */ +const FunctionTransform: Transform = { + type: 'Function', + lookup: Function, + + // Simple and fast function type check + shouldTransform: (type: string, obj: any): obj is Function => { + return typeof obj === 'function' + }, + + // Extract function metadata for serialization + toSerializable: (func: Function): SerializedFunction => { + let body = '' + + try { + // Extract function body between first { and last } + const funcString = func.toString() + const startIndex = funcString.indexOf('{') + 1 + const endIndex = funcString.lastIndexOf('}') + + if (startIndex > 0 && endIndex > startIndex) { + body = funcString.substring(startIndex, endIndex) + } + } + catch { + // Ignore errors in function stringification + } + + return { + name: func.name, + body, + proto: Object.getPrototypeOf(func).constructor.name + } + }, + + // Create placeholder function with metadata + fromSerializable: (data: SerializedFunction): Function | SerializedFunction => { + try { + const tempFunc = function() {} + + if (typeof data.name === 'string') { + Object.defineProperty(tempFunc, 'name', { + value: data.name, + writable: false, + configurable: true + }) + } + + if (typeof data.body === 'string') { + Object.defineProperty(tempFunc, 'body', { + value: data.body, + writable: false, + configurable: true + }) + } + + if (typeof data.proto === 'string') { // @ts-ignore + tempFunc.constructor = { + name: data.proto + } + } + + return tempFunc + } + catch { + return data + } + } +} + +export default FunctionTransform diff --git a/lib/units/tizen-device/plugins/webinspector/transform/HTML.ts b/lib/units/tizen-device/plugins/webinspector/transform/HTML.ts new file mode 100644 index 0000000000..f10911f9aa --- /dev/null +++ b/lib/units/tizen-device/plugins/webinspector/transform/HTML.ts @@ -0,0 +1,93 @@ +import {Transform} from './index.js' + +/** + * Serialized HTML element structure + */ +interface SerializedHTMLElement { + tagName: string + attributes: Record + innerHTML: string +} + +/** + * HTML element-like object for type checking + */ +interface HTMLElementLike { + children: any + innerHTML: string + tagName: string + attributes: ArrayLike<{ name: string; value: string }> & Iterable<{ name: string; value: string }> +} + +// Cached sandbox document for performance +let sandbox: Document | undefined + +/** + * Get or create sandbox document for safe HTML parsing + */ +const getSandbox = (): Document => { + return sandbox ??= document.implementation.createHTMLDocument('sandbox') +} + +/** + * Efficiently convert element attributes to plain object + */ +const objectifyAttributes = (element: HTMLElementLike): Record => { + const data: Record = {} + + // Use for...of for better performance with NamedNodeMap + for (const attribute of element.attributes) { + data[attribute.name] = attribute.value + } + + return data +} + +/** + * Transform for serializing HTML elements into JSON-compatible format + * Uses sandboxed document for safe deserialization + */ +const HTMLTransform: Transform = { + type: 'HTMLElement', + + // Optimized HTML element detection + shouldTransform: (type: string, obj: any): obj is HTMLElementLike => { + return obj && + obj.children && + typeof obj.innerHTML === 'string' && + typeof obj.tagName === 'string' + }, + + // Serialize HTML element to plain object + toSerializable: (element: HTMLElementLike): SerializedHTMLElement => { + return { + tagName: element.tagName.toLowerCase(), + attributes: objectifyAttributes(element), + innerHTML: element.innerHTML + } + }, + + // Deserialize to actual HTML element using sandbox + fromSerializable: (data: SerializedHTMLElement): HTMLElement | SerializedHTMLElement => { + try { + const element = getSandbox().createElement(data.tagName) + element.innerHTML = data.innerHTML + + for (const attributeName of Object.keys(data.attributes)) { + try { + element.setAttribute(attributeName, data.attributes[attributeName]) + } + catch { + // no-op + } + } + + return element + } + catch { + return data + } + } +} + +export default HTMLTransform diff --git a/lib/units/tizen-device/plugins/webinspector/transform/Map.ts b/lib/units/tizen-device/plugins/webinspector/transform/Map.ts new file mode 100644 index 0000000000..ffd5ffbcc8 --- /dev/null +++ b/lib/units/tizen-device/plugins/webinspector/transform/Map.ts @@ -0,0 +1,59 @@ +import {Transform} from './index.js' + +/** + * Serialized Map data structure + */ +interface SerializedMap { + name: 'Map' + body: Record + proto: string +} + +/** + * Transform for serializing Map objects into JSON-compatible format + * Handles object keys by JSON stringifying them + */ +const MapTransform: Transform = { + type: 'Map', + lookup: Map, + + // Optimized Map detection using constructor name + shouldTransform: (type: string, obj: any): obj is Map => { + return obj?.constructor?.name === 'Map' + }, + + // Convert Map to serializable object with stringified object keys + toSerializable: (map: Map): SerializedMap => { + const body: Record = {} + + // Optimize iteration with forEach for better performance + map.forEach((value, key) => { + // Stringify object keys, keep primitive keys as-is for efficiency + const serializedKey = typeof key === 'object' ? JSON.stringify(key) : key + body[serializedKey] = value + }) + + return { + name: 'Map', + body, + proto: Object.getPrototypeOf(map).constructor.name + } + }, + + // Convert serialized data back to object (not actual Map for compatibility) + fromSerializable: (data: SerializedMap): Record => { + const obj = {...data.body} + + // Restore constructor information if available + if (typeof data.proto === 'string') { + // @ts-ignore - Intentional constructor override for compatibility + obj.constructor = { + name: data.proto + } + } + + return obj + } +} + +export default MapTransform diff --git a/lib/units/tizen-device/plugins/webinspector/transform/arithmetic.ts b/lib/units/tizen-device/plugins/webinspector/transform/arithmetic.ts new file mode 100644 index 0000000000..60b4701621 --- /dev/null +++ b/lib/units/tizen-device/plugins/webinspector/transform/arithmetic.ts @@ -0,0 +1,48 @@ +import {Transform} from './index.js' + +const enum ArithmeticType { + infinity = 0, + minusInfinity = 1, + minusZero = 2 +} + +const isMinusZero = (value: number): boolean => 1 / value === -Infinity + +/** + * Transform for handling special arithmetic values: Infinity, -Infinity, and -0 + * These values need special handling as they don't serialize properly in JSON + */ +const ArithmeticTransform: Transform = { + type: 'Arithmetic', + lookup: Number, + + shouldTransform: (type: string, value: any): value is number => { + return type === 'number' && + (value === Infinity || value === -Infinity || isMinusZero(value)) + }, + + toSerializable: (value: number): ArithmeticType => { + if (value === Infinity) { + return ArithmeticType.infinity + } + if (value === -Infinity) { + return ArithmeticType.minusInfinity + } + return ArithmeticType.minusZero + }, + + fromSerializable: (data: ArithmeticType): number => { + switch (data) { + case ArithmeticType.infinity: + return Infinity + case ArithmeticType.minusInfinity: + return -Infinity + case ArithmeticType.minusZero: + return -0 + default: + return data as number + } + } +} + +export default ArithmeticTransform diff --git a/lib/units/tizen-device/plugins/webinspector/transform/index.ts b/lib/units/tizen-device/plugins/webinspector/transform/index.ts new file mode 100644 index 0000000000..81dd2dc6bc --- /dev/null +++ b/lib/units/tizen-device/plugins/webinspector/transform/index.ts @@ -0,0 +1,17 @@ +// Transform exports +export {default as ArithmeticTransform} from './arithmetic.js' +export {default as BigIntTransform} from './BigInt.js' +export {default as FunctionTransform} from './Function.js' +export {default as HTMLTransform} from './HTML.js' +export {default as MapTransform} from './Map.js' + +/** + * Transform interface matching the main Replicator requirements + */ +export interface Transform { + type: string + lookup?: any + shouldTransform(type: string, val: any): boolean + toSerializable(val: any): any + fromSerializable(val: any): any +} diff --git a/lib/units/websocket/index.js b/lib/units/websocket/index.js index 70243e570a..07e08e8c22 100644 --- a/lib/units/websocket/index.js +++ b/lib/units/websocket/index.js @@ -45,7 +45,7 @@ export default (async function(options) { , push , pushdev } = await db.createZMQSockets({...options.endpoints}, log) - await db.connect(push, pushdev, channelRouter) + await db.connect({push, pushdev, channelRouter}) ;[wireutil.global].forEach(function(channel) { log.info('Subscribing to permanent webosocket channel "%s"', channel) @@ -899,11 +899,18 @@ export default (async function(options) { wireutil.transaction(responseChannel, new wire.BluetoothCleanBondedMessage()) ]) }) - .on('group.invite', function(channel, responseChannel, data) { + .on('group.invite', async(channel, responseChannel, data) => { joinChannel(responseChannel) + const keys = await dbapi.getUserAdbKeys(user.email) push.send([ channel, - wireutil.transaction(responseChannel, new wire.GroupMessage(new wire.OwnerMessage(user.email, user.name, user.group), data.timeout || null, wireutil.toDeviceRequirements(data.requirements))) + wireutil.transaction(responseChannel, new wire.GroupMessage( + new wire.OwnerMessage(user.email, user.name, user.group), + data.timeout || null, + wireutil.toDeviceRequirements(data.requirements), + null, + keys.map(key => key.fingerprint) + )) ]) }) .on('group.kick', function(channel, responseChannel, data) { diff --git a/lib/util/grouputil.js b/lib/util/grouputil.js deleted file mode 100644 index caade52508..0000000000 --- a/lib/util/grouputil.js +++ /dev/null @@ -1,60 +0,0 @@ -import util from 'util' -import Promise from 'bluebird' -import semver from 'semver' -import minimatch from 'minimatch' -import wire from '../wire/index.js' -import {RequirementType} from '../wire/wire.js' -function RequirementMismatchError(name) { - Error.call(this) - this.name = 'RequirementMismatchError' - this.message = util.format('Requirement mismatch for "%s"', name) - Error.captureStackTrace(this, RequirementMismatchError) -} -util.inherits(RequirementMismatchError, Error) -function AlreadyGroupedError() { - Error.call(this) - this.name = 'AlreadyGroupedError' - this.message = 'Already a member of another group' - Error.captureStackTrace(this, AlreadyGroupedError) -} -util.inherits(AlreadyGroupedError, Error) -function NoGroupError() { - Error.call(this) - this.name = 'NoGroupError' - this.message = 'Not a member of any group' - Error.captureStackTrace(this, NoGroupError) -} -util.inherits(NoGroupError, Error) - -/** @type {(capabilities: any, requirements: any) => Promise} */ -export const match = Promise.method(function(capabilities, requirements) { - return requirements.every(function(req) { - var capability = capabilities[req.name] - if (!capability) { - throw new RequirementMismatchError(req.name) - } - switch (req.type) { - case RequirementType.SEMVER: - if (!semver.satisfies(capability, req.value)) { - throw new RequirementMismatchError(req.name) - } - break - case RequirementType.GLOB: - if (!minimatch(capability, req.value)) { - throw new RequirementMismatchError(req.name) - } - break - case RequirementType.EXACT: - if (capability !== req.value) { - throw new RequirementMismatchError(req.name) - } - break - default: - throw new RequirementMismatchError(req.name) - } - return true - }) -}) -export {RequirementMismatchError} -export {AlreadyGroupedError} -export {NoGroupError} diff --git a/lib/util/grouputil.ts b/lib/util/grouputil.ts new file mode 100644 index 0000000000..7c457eb1ec --- /dev/null +++ b/lib/util/grouputil.ts @@ -0,0 +1,67 @@ +import util from 'util' +import semver from 'semver' // @ts-ignore +import minimatch from 'minimatch' // TODO: update +import {RequirementType} from '../wire/wire.js' + +export class RequirementMismatchError extends Error { + constructor(name: string) { + super() + this.name = 'RequirementMismatchError' + this.message = util.format('Requirement mismatch for "%s"', name) + Error.captureStackTrace(this, RequirementMismatchError) + } +} + +export class AlreadyGroupedError extends Error { + constructor() { + super() + this.name = 'AlreadyGroupedError' + this.message = 'Already a member of another group' + Error.captureStackTrace(this, AlreadyGroupedError) + } +} + +export class NoGroupError extends Error { + constructor() { + super() + this.name = 'NoGroupError' + this.message = 'Not a member of any group' + Error.captureStackTrace(this, NoGroupError) + } +} + +interface Requirements { + name: string + value: string + type: RequirementType +} + +export const match = async(capabilities: any, requirements: Requirements[]) => { + return requirements.every((req) => { + const capability = capabilities[req.name] + if (!capability) { + throw new RequirementMismatchError(req.name) + } + + switch (req.type) { + case RequirementType.SEMVER: + if (!semver.satisfies(capability, req.value)) { + throw new RequirementMismatchError(req.name) + } + break + case RequirementType.GLOB: + if (!minimatch(capability, req.value)) { + throw new RequirementMismatchError(req.name) + } + break + case RequirementType.EXACT: + if (capability !== req.value) { + throw new RequirementMismatchError(req.name) + } + break + default: + throw new RequirementMismatchError(req.name) + } + return true + }) +} diff --git a/lib/util/lifecycle.js b/lib/util/lifecycle.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/util/ttlset.js b/lib/util/ttlset.js deleted file mode 100644 index 8643f10e1b..0000000000 --- a/lib/util/ttlset.js +++ /dev/null @@ -1,94 +0,0 @@ -import util from 'util' -import EventEmitter from 'eventemitter3' -function TtlItem(value) { - this.next = null - this.prev = null - this.time = null - this.value = value -} -function TtlSet(ttl) { - EventEmitter.call(this) - this.head = null - this.tail = null - this.mapping = Object.create(null) - this.ttl = ttl - this.timer = null -} -util.inherits(TtlSet, EventEmitter) -TtlSet.SILENT = 1 -TtlSet.prototype.bump = function(value, time, flags) { - var item = this._remove(this.mapping[value]) || this._create(value, flags) - item.time = time || Date.now() - item.prev = this.tail - this.tail = item - if (item.prev) { - item.prev.next = item - } - else { - this.head = item - this._scheduleCheck() - } -} -TtlSet.prototype.drop = function(value, flags) { - this._drop(this.mapping[value], flags) -} -TtlSet.prototype.stop = function() { - clearTimeout(this.timer) -} -TtlSet.prototype._scheduleCheck = function() { - clearTimeout(this.timer) - if (this.head) { - var delay = Math.max(0, this.ttl - (Date.now() - this.head.time)) - this.timer = setTimeout(this._check.bind(this), delay) - } -} -TtlSet.prototype._check = function() { - var now = Date.now() - var item - while ((item = this.head)) { - if (now - item.time > this.ttl) { - this._drop(item, 0) - } - else { - break - } - } - this._scheduleCheck() -} -TtlSet.prototype._create = function(value, flags) { - var item = new TtlItem(value) - this.mapping[value] = item - if ((flags & TtlSet.SILENT) !== TtlSet.SILENT) { - this.emit('insert', value) - } - return item -} -TtlSet.prototype._drop = function(item, flags) { - if (item) { - this._remove(item) - delete this.mapping[item.value] - if ((flags & TtlSet.SILENT) !== TtlSet.SILENT) { - this.emit('drop', item.value) - } - } -} -TtlSet.prototype._remove = function(item) { - if (!item) { - return null - } - if (item.prev) { - item.prev.next = item.next - } - if (item.next) { - item.next.prev = item.prev - } - if (item === this.head) { - this.head = item.next - } - if (item === this.tail) { - this.tail = item.prev - } - item.next = item.prev = null - return item -} -export default TtlSet diff --git a/lib/util/ttlset.ts b/lib/util/ttlset.ts new file mode 100644 index 0000000000..0c85c36b59 --- /dev/null +++ b/lib/util/ttlset.ts @@ -0,0 +1,113 @@ +import EventEmitter from 'events' + +class TTLItem { + constructor( + public value: string, + public time: number, + public prev: TTLItem | null, + public next: TTLItem | null + ) {} +} + +class TTLSet extends EventEmitter { + private head: TTLItem | null = null + private tail: TTLItem | null = null + private mapping: Record = {} + private timer: NodeJS.Timeout | null = null + + static SILENT = 1 + + constructor( + private ttl = 30_000 + ) { + super() + } + + bump(value: string, time: number, flags?: any) { + const item = + this.remove(this.mapping[value]) || + this.create(value, this.tail, flags) + + item.time = time || Date.now() + this.tail = item + if (item.prev) { + item.prev.next = item + } + else { + this.head = item + } + + this.scheduleCheck() + } + + drop(value: string, flags?: any) { + this._drop(this.mapping[value], flags) + } + + stop() { + clearTimeout(this.timer!) + } + + private scheduleCheck() { + clearTimeout(this.timer!) + if (this.head) { + const delay = Math.max(0, this.ttl - (Date.now() - this.head.time)) + this.timer = setTimeout(this.check.bind(this), delay) + } + } + + private check() { + const now = Date.now() + let item: TTLItem | null + while ((item = this.head)) { + if (now - item.time > this.ttl) { + this._drop(item, 0) + } + else { + break + } + } + this.scheduleCheck() + } + + private create(value: string, prev: TTLItem | null, flags: any) { + const item = new TTLItem(value, 0, prev, null) + this.mapping[value] = item + if ((flags & TTLSet.SILENT) !== TTLSet.SILENT) { + this.emit('insert', value) + } + return item + } + + private _drop(item: TTLItem, flags?: any) { + if (item) { + this.remove(item) + delete this.mapping[item.value] + if ((flags & TTLSet.SILENT) !== TTLSet.SILENT) { + this.emit('drop', item.value) + } + } + } + + private remove(item: TTLItem) { + if (!item) { + return null + } + if (item.prev) { + item.prev.next = item.next + } + if (item.next) { + item.next.prev = item.prev + } + if (item === this.head) { + this.head = item.next + } + if (item === this.tail) { + this.tail = item.prev + } + item.next = item.prev = null + return item + } +} + +export default TTLSet diff --git a/lib/wire/router.ts b/lib/wire/router.ts index eca2d22f87..10331747cb 100644 --- a/lib/wire/router.ts +++ b/lib/wire/router.ts @@ -6,7 +6,7 @@ import { Any } from "./google/protobuf/any.ts"; const log = logger.createLogger("wire:router"); -type MessageHandler = (channel: string, message: T) => unknown +type MessageHandler = (channel: string, message: T, data: Buffer) => unknown export class WireRouter { emitter = new EventEmitter() diff --git a/lib/wire/transmanager.js b/lib/wire/transmanager.js deleted file mode 100644 index 926b5ccfbb..0000000000 --- a/lib/wire/transmanager.js +++ /dev/null @@ -1,58 +0,0 @@ -import {v4 as uuidv4} from 'uuid' -import apiutil from '../util/apiutil.js' -import wire from './index.js' -import {WireRouter} from './router.js' -import * as Sentry from '@sentry/node' -import wireutil from './util.js' -import {TransactionDoneMessage} from './wire.js' - -export const runTransaction = (channel, message, {sub, push, channelRouter, timeout = apiutil.GRPC_WAIT_TIMEOUT}) => { - return Sentry.startSpan({ - op: 'wireTransaction', - name: message.$code, - attributes: { - message, - channel, - timeout - }, - forceTransaction: true, - }, () => { - const responseChannel = 'txn_' + uuidv4() - sub.subscribe(responseChannel) - return new Promise((resolve, reject) => { - const messageListener = new WireRouter() - .on(TransactionDoneMessage, function(channel, message) { - clearTimeout(trTimeout) - sub.unsubscribe(responseChannel) - channelRouter.removeListener(responseChannel, messageListener) - if (message.success) { - resolve(message) - } - else { - reject(message) - } - }) - .handler() - - const trTimeout = setTimeout(function() { - channelRouter.removeListener(responseChannel, messageListener) - sub.unsubscribe(responseChannel) - - Sentry.addBreadcrumb({ - data: {channel, message, timeout}, - message: 'Transaction context', - level: 'warning', - type: 'default' - }) - Sentry.captureMessage('Timeout when running transaction') - reject(new Error('Timeout when running transaction')) - }, timeout) - - channelRouter.on(responseChannel, messageListener) - push.send([ - channel, - wireutil.transaction(responseChannel, message) - ]) - }) - }) -} diff --git a/lib/wire/transmanager.ts b/lib/wire/transmanager.ts new file mode 100644 index 0000000000..27dc0eb88a --- /dev/null +++ b/lib/wire/transmanager.ts @@ -0,0 +1,122 @@ +import {v4 as uuidv4} from 'uuid' +import apiutil from '../util/apiutil.js' +import {TransactionDoneMessage} from './wire.js' +import {WireRouter} from './router.js' +import * as Sentry from '@sentry/node' +import wireutil from './util.js' +import type {SocketWrapper} from '../util/zmqutil.js' +import EventEmitter from 'events' +import {MessageType} from '@protobuf-ts/runtime' + +const sentryTransactionSpan = >(channel: string, message: any, timeout: number, cb: () => T): T => + Sentry.startSpan({ + op: 'wireTransaction', + name: message.$code, + attributes: { + message, + channel, + timeout + }, + forceTransaction: true, + }, cb) + +const sentryCaptureTimeout = (channel: string, message: any, timeout: number) => { + Sentry.addBreadcrumb({ + data: {channel, message, timeout}, + message: 'Transaction context', + level: 'warning', + type: 'default' + }) + Sentry.captureMessage('Timeout when running transaction') +} + +interface TransactionTransportOptions { + sub: SocketWrapper + push: SocketWrapper + channelRouter: EventEmitter + timeout?: number +} + +export const runTransaction = (channel: string, messageType: MessageType, message: T, {sub, push, channelRouter, timeout = apiutil.GRPC_WAIT_TIMEOUT}: TransactionTransportOptions) => + sentryTransactionSpan( + channel, + message, + timeout, + () => { + const responseChannel = 'txn_' + uuidv4() + sub.subscribe(responseChannel) + return new Promise((resolve, reject) => { + const messageListener = new WireRouter() + .on(TransactionDoneMessage, (channel: string, message: any) => { + clearTimeout(trTimeout) + sub.unsubscribe(responseChannel) + channelRouter.removeListener(responseChannel, messageListener) + if (message.success) { + resolve(message) + } + else { + reject(message) + } + }) + .handler() + + const trTimeout = setTimeout(function() { + channelRouter.removeListener(responseChannel, messageListener) + sub.unsubscribe(responseChannel) + + sentryCaptureTimeout(channel, message, timeout) + reject(new Error('Timeout when running transaction')) + }, timeout) + + channelRouter.on(responseChannel, messageListener) + push.send([ + channel, + wireutil.tr(responseChannel, messageType, message) + ]) + }) + } + ) + +type TransactionDevTransportOptions = Omit & { + router: WireRouter +} + +export const runTransactionDev = (channel: string, messageType: MessageType, message: T, {sub, push, router, timeout = apiutil.GRPC_WAIT_TIMEOUT}: TransactionDevTransportOptions): Promise => + sentryTransactionSpan( + channel, + message, + timeout, + () => { + const responseChannel = 'txn_' + uuidv4() + sub.subscribe(responseChannel) + return new Promise((resolve, reject) => { + const messageListener = (channel: string, message: any) => { + clearTimeout(trTimeout) + sub.unsubscribe(responseChannel) + router.removeListener(TransactionDoneMessage, messageListener) + + const body = message.body ? JSON.parse(message.body) : {} + if (message.success) { + resolve(body) + } + else { + reject(body) + } + } + router.on(TransactionDoneMessage, messageListener) + + const trTimeout = setTimeout(() => { + router.removeListener(TransactionDoneMessage, messageListener) + sub.unsubscribe(responseChannel) + + sentryCaptureTimeout(channel, message, timeout) + reject(new Error('Timeout when running transaction')) + }, timeout) + + push.send([ + channel, + wireutil.tr(responseChannel, messageType, message) + ]) + }) + } + ) diff --git a/lib/wire/wire.proto b/lib/wire/wire.proto index 2e87a0f41a..53be0505e9 100644 --- a/lib/wire/wire.proto +++ b/lib/wire/wire.proto @@ -277,7 +277,7 @@ message DeviceIntroductionMessage { required string serial = 1; required DeviceStatus status = 2; required ProviderMessage provider = 3; -// optional DeviceGroupMessage group = 4; + optional string deviceType = 4; } message DeviceIosIntroductionMessage { @@ -430,6 +430,7 @@ message GroupMessage { optional uint32 timeout = 2; repeated DeviceRequirement requirements = 3; optional string usage = 4; + repeated string keys = 5; } message AutoGroupMessage { @@ -445,6 +446,7 @@ message JoinGroupMessage { required string serial = 1; required OwnerMessage owner = 2; optional string usage = 3; + optional uint32 timeout = 4; } message JoinGroupByAdbFingerprintMessage { @@ -895,3 +897,15 @@ message GetAppHTML { message GetAppInspectServerUrl { } + +message DeviceStatusChange { + required string serial = 1; + required uint32 timeout = 2; +} + +message DeviceGetIsInOrigin { + required string serial = 1; +} + +message GetPresentDevices { +} diff --git a/lib/wire/wire.ts b/lib/wire/wire.ts index a005fb4e3c..85edad04eb 100644 --- a/lib/wire/wire.ts +++ b/lib/wire/wire.ts @@ -792,7 +792,11 @@ export interface DeviceIntroductionMessage { /** * @generated from protobuf field: required ProviderMessage provider = 3 */ - provider?: ProviderMessage; // optional DeviceGroupMessage group = 4; + provider?: ProviderMessage; + /** + * @generated from protobuf field: optional string deviceType = 4 + */ + deviceType?: string; } /** * @generated from protobuf message DeviceIosIntroductionMessage @@ -1189,6 +1193,10 @@ export interface GroupMessage { * @generated from protobuf field: optional string usage = 4 */ usage?: string; + /** + * @generated from protobuf field: repeated string keys = 5 + */ + keys: string[]; } /** * @generated from protobuf message AutoGroupMessage @@ -1228,6 +1236,10 @@ export interface JoinGroupMessage { * @generated from protobuf field: optional string usage = 3 */ usage?: string; + /** + * @generated from protobuf field: optional uint32 timeout = 4 + */ + timeout?: number; } /** * @generated from protobuf message JoinGroupByAdbFingerprintMessage @@ -2358,6 +2370,33 @@ export interface GetAppHTML { */ export interface GetAppInspectServerUrl { } +/** + * @generated from protobuf message DeviceStatusChange + */ +export interface DeviceStatusChange { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; + /** + * @generated from protobuf field: required uint32 timeout = 2 + */ + timeout: number; +} +/** + * @generated from protobuf message DeviceGetIsInOrigin + */ +export interface DeviceGetIsInOrigin { + /** + * @generated from protobuf field: required string serial = 1 + */ + serial: string; +} +/** + * @generated from protobuf message GetPresentDevices + */ +export interface GetPresentDevices { +} /** * @generated from protobuf enum DeviceStatus */ @@ -5072,7 +5111,8 @@ class DeviceIntroductionMessage$Type extends MessageType ["DeviceStatus", DeviceStatus] }, - { no: 3, name: "provider", kind: "message", T: () => ProviderMessage } + { no: 3, name: "provider", kind: "message", T: () => ProviderMessage }, + { no: 4, name: "deviceType", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ } ]); } create(value?: PartialMessage): DeviceIntroductionMessage { @@ -5097,6 +5137,9 @@ class DeviceIntroductionMessage$Type extends MessageType { { no: 1, name: "owner", kind: "message", T: () => OwnerMessage }, { no: 2, name: "timeout", kind: "scalar", opt: true, T: 13 /*ScalarType.UINT32*/ }, { no: 3, name: "requirements", kind: "message", repeat: 2 /*RepeatType.UNPACKED*/, T: () => DeviceRequirement }, - { no: 4, name: "usage", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ } + { no: 4, name: "usage", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }, + { no: 5, name: "keys", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ } ]); } create(value?: PartialMessage): GroupMessage { const message = globalThis.Object.create((this.messagePrototype!)); message.requirements = []; + message.keys = []; if (value !== undefined) reflectionMergePartial(this, message, value); return message; @@ -6443,6 +6491,9 @@ class GroupMessage$Type extends MessageType { case /* optional string usage */ 4: message.usage = reader.string(); break; + case /* repeated string keys */ 5: + message.keys.push(reader.string()); + break; default: let u = options.readUnknownField; if (u === "throw") @@ -6467,6 +6518,9 @@ class GroupMessage$Type extends MessageType { /* optional string usage = 4; */ if (message.usage !== undefined) writer.tag(4, WireType.LengthDelimited).string(message.usage); + /* repeated string keys = 5; */ + for (let i = 0; i < message.keys.length; i++) + writer.tag(5, WireType.LengthDelimited).string(message.keys[i]); let u = options.writeUnknownFields; if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); @@ -6584,7 +6638,8 @@ class JoinGroupMessage$Type extends MessageType { super("JoinGroupMessage", [ { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, { no: 2, name: "owner", kind: "message", T: () => OwnerMessage }, - { no: 3, name: "usage", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ } + { no: 3, name: "usage", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }, + { no: 4, name: "timeout", kind: "scalar", opt: true, T: 13 /*ScalarType.UINT32*/ } ]); } create(value?: PartialMessage): JoinGroupMessage { @@ -6608,6 +6663,9 @@ class JoinGroupMessage$Type extends MessageType { case /* optional string usage */ 3: message.usage = reader.string(); break; + case /* optional uint32 timeout */ 4: + message.timeout = reader.uint32(); + break; default: let u = options.readUnknownField; if (u === "throw") @@ -6629,6 +6687,9 @@ class JoinGroupMessage$Type extends MessageType { /* optional string usage = 3; */ if (message.usage !== undefined) writer.tag(3, WireType.LengthDelimited).string(message.usage); + /* optional uint32 timeout = 4; */ + if (message.timeout !== undefined) + writer.tag(4, WireType.Varint).uint32(message.timeout); let u = options.writeUnknownFields; if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); @@ -11543,3 +11604,143 @@ class GetAppInspectServerUrl$Type extends MessageType { * @generated MessageType for protobuf message GetAppInspectServerUrl */ export const GetAppInspectServerUrl = new GetAppInspectServerUrl$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class DeviceStatusChange$Type extends MessageType { + constructor() { + super("DeviceStatusChange", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "timeout", kind: "scalar", T: 13 /*ScalarType.UINT32*/ } + ]); + } + create(value?: PartialMessage): DeviceStatusChange { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + message.timeout = 0; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DeviceStatusChange): DeviceStatusChange { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + case /* required uint32 timeout */ 2: + message.timeout = reader.uint32(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: DeviceStatusChange, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + /* required uint32 timeout = 2; */ + if (message.timeout !== 0) + writer.tag(2, WireType.Varint).uint32(message.timeout); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message DeviceStatusChange + */ +export const DeviceStatusChange = new DeviceStatusChange$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class DeviceGetIsInOrigin$Type extends MessageType { + constructor() { + super("DeviceGetIsInOrigin", [ + { no: 1, name: "serial", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): DeviceGetIsInOrigin { + const message = globalThis.Object.create((this.messagePrototype!)); + message.serial = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DeviceGetIsInOrigin): DeviceGetIsInOrigin { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required string serial */ 1: + message.serial = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: DeviceGetIsInOrigin, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string serial = 1; */ + if (message.serial !== "") + writer.tag(1, WireType.LengthDelimited).string(message.serial); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message DeviceGetIsInOrigin + */ +export const DeviceGetIsInOrigin = new DeviceGetIsInOrigin$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class GetPresentDevices$Type extends MessageType { + constructor() { + super("GetPresentDevices", []); + } + create(value?: PartialMessage): GetPresentDevices { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: GetPresentDevices): GetPresentDevices { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: GetPresentDevices, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message GetPresentDevices + */ +export const GetPresentDevices = new GetPresentDevices$Type(); diff --git a/package-lock.json b/package-lock.json index 18b4acae70..7d4e593f22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,6 @@ "chalk": "1.1.3", "chrome-remote-interface": "^0.33.3", "compression": "1.7.4", - "console-feed": "^3.8.0", "cookie": "^1.0.1", "cookie-parser": "^1.4.6", "cookie-session": "2.0.0", @@ -126,6 +125,7 @@ "@types/http-proxy": "^1.17.16", "@types/lodash": "^4.17.16", "@types/node": "^22.15.17", + "@types/semver": "^7.7.1", "@types/split": "^1.0.5", "@types/temp": "^0.9.4", "@types/tmp": "^0.2.6", @@ -1150,53 +1150,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/generator": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", - "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.0", - "@babel/types": "^7.28.0", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-validator-identifier": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", @@ -1206,21 +1159,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/parser": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", - "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/runtime": { "version": "7.28.2", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz", @@ -1230,51 +1168,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", - "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.0", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.0", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", - "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@bufbuild/protobuf": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.6.3.tgz", @@ -4751,6 +4644,13 @@ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "license": "MIT" }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/send": { "version": "0.17.5", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", @@ -5979,41 +5879,6 @@ "proxy-from-env": "^1.1.0" } }, - "node_modules/babel-plugin-emotion": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/babel-plugin-emotion/-/babel-plugin-emotion-10.2.2.tgz", - "integrity": "sha512-SMSkGoqTbTyUTDeuVuPIWifPdUGkTk1Kf9BWRiXIOIcuyMfsdp2EjeiiFvOzX8NOBvEh/ypKYvUh2rkgAJMCLA==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.0.0", - "@emotion/hash": "0.8.0", - "@emotion/memoize": "0.7.4", - "@emotion/serialize": "^0.11.16", - "babel-plugin-macros": "^2.0.0", - "babel-plugin-syntax-jsx": "^6.18.0", - "convert-source-map": "^1.5.0", - "escape-string-regexp": "^1.0.5", - "find-root": "^1.1.0", - "source-map": "^0.5.7" - } - }, - "node_modules/babel-plugin-macros": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz", - "integrity": "sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.7.2", - "cosmiconfig": "^6.0.0", - "resolve": "^1.12.0" - } - }, - "node_modules/babel-plugin-syntax-jsx": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", - "integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==", - "license": "MIT" - }, "node_modules/backoff": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz", @@ -7081,37 +6946,6 @@ "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", "license": "ISC" }, - "node_modules/console-feed": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/console-feed/-/console-feed-3.8.0.tgz", - "integrity": "sha512-H7d5dbVOmsujpMdI252RGHKxGwTybRI9o1aDUe2xDeVC63Koqh/QC7Rvo48EYcU3aMCM4OgWHKKISsZ2dMLX7w==", - "license": "MIT", - "dependencies": { - "@emotion/core": "^10.0.10", - "@emotion/styled": "^10.0.12", - "emotion-theming": "^10.0.10", - "linkifyjs": "^2.1.6", - "react-inline-center": "1.0.1", - "react-inspector": "^5.1.0" - }, - "peerDependencies": { - "react": "^15.x || ^16.x || ^17.x || ^18.x || ^19.x" - } - }, - "node_modules/console-feed/node_modules/react-inspector": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/react-inspector/-/react-inspector-5.1.1.tgz", - "integrity": "sha512-GURDaYzoLbW8pMGXwYPDBIv6nqei4kK7LPRZ9q9HCZF54wqXz/dnylBp/kfE9XmekBhHvLDdcYeyIwSrvtOiWg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.0.0", - "is-dom": "^1.0.0", - "prop-types": "^15.0.0" - }, - "peerDependencies": { - "react": "^16.8.4 || ^17.0.0" - } - }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -7153,12 +6987,6 @@ "node": ">= 0.6" } }, - "node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "license": "MIT" - }, "node_modules/cookie": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", @@ -7283,31 +7111,6 @@ "node": ">= 0.10" } }, - "node_modules/cosmiconfig": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", - "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", - "license": "MIT", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.1.0", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.7.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cosmiconfig/node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, "node_modules/crc-32": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", @@ -7361,12 +7164,6 @@ "node": ">= 0.8" } }, - "node_modules/csstype": { - "version": "2.6.21", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", - "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==", - "license": "MIT" - }, "node_modules/csurf": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/csurf/-/csurf-1.11.0.tgz", @@ -7707,21 +7504,6 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, - "node_modules/emotion-theming": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/emotion-theming/-/emotion-theming-10.3.0.tgz", - "integrity": "sha512-mXiD2Oj7N9b6+h/dC6oLf9hwxbtKHQjoIqtodEyL8CpkN4F3V4IK/BT4D0C7zSs4BBFOu4UlPJbvvBLa88SGEA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.5.5", - "@emotion/weak-memoize": "0.2.5", - "hoist-non-react-statics": "^3.3.0" - }, - "peerDependencies": { - "@emotion/core": "^10.0.27", - "react": ">=16.3.0" - } - }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -8674,12 +8456,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, - "node_modules/find-root": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", - "license": "MIT" - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -9325,15 +9101,6 @@ "node": "*" } }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "license": "BSD-3-Clause", - "dependencies": { - "react-is": "^16.7.0" - } - }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -9725,16 +9492,6 @@ "integrity": "sha512-vLwCNpTNkFC5k7SBRxPubhOCryeulkOsSkjbGyZ8eOzZmzMS+hSEO/Kn9ZOVhFNAlRZTFc4ZKql48hESuYUPIQ==", "license": "MIT" }, - "node_modules/is-dom": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-dom/-/is-dom-1.1.0.tgz", - "integrity": "sha512-u82f6mvhYxRPKpw8V1N0W8ce1xXwOrQtgGcxl6UCL5zBmZu3is/18K0rR7uFCnMDuAsS/3W54mGL4vsaFUQlEQ==", - "license": "MIT", - "dependencies": { - "is-object": "^1.0.1", - "is-window": "^1.0.2" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -9783,27 +9540,12 @@ "node": ">=0.12.0" } }, - "node_modules/is-object": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz", - "integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", "license": "MIT" }, - "node_modules/is-window": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-window/-/is-window-1.0.2.tgz", - "integrity": "sha512-uj00kdXyZb9t9RcAUAwMZAnkBUwdYGhYlt7djMXhfyhUCzwNba50tIiBKR7q0l7tdoBtFVw/3JmLY6fI3rmZmg==", - "license": "MIT" - }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -9903,13 +9645,6 @@ "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", "license": "BSD-3-Clause" }, - "node_modules/jquery": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", - "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", - "license": "MIT", - "peer": true - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -9934,18 +9669,6 @@ "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", "license": "MIT" }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/jsftp": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/jsftp/-/jsftp-2.1.3.tgz", @@ -10220,17 +9943,6 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, - "node_modules/linkifyjs": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-2.1.9.tgz", - "integrity": "sha512-74ivurkK6WHvHFozVaGtQWV38FzBwSTGNmJolEgFp7QgR2bl6ArUWlvT4GcHKbPe1z3nWYi+VUdDZk16zDOVug==", - "license": "MIT", - "peerDependencies": { - "jquery": ">= 1.11.0", - "react": ">= 0.14.0", - "react-dom": ">= 0.14.0" - } - }, "node_modules/load-bmfont": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/load-bmfont/-/load-bmfont-1.4.2.tgz", @@ -10331,18 +10043,6 @@ "node": ">=0.6" } }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -12073,17 +11773,6 @@ "node": ">=10.0.0" } }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, "node_modules/proper-lockfile": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", @@ -12359,44 +12048,6 @@ "node": ">=0.10.0" } }, - "node_modules/react": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", - "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", - "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", - "license": "MIT", - "peer": true, - "dependencies": { - "scheduler": "^0.26.0" - }, - "peerDependencies": { - "react": "^19.1.1" - } - }, - "node_modules/react-inline-center": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/react-inline-center/-/react-inline-center-1.0.1.tgz", - "integrity": "sha512-nMxG933OWuZET/CkvQTPBVEPNRI2zhfeeeiRqXKKzSdvIPHnHnDXZmh9KkRO2DDCjJFvZQ3KIe50lXaaxvnoNw==", - "license": "MIT", - "peerDependencies": { - "react": "*" - } - }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -12898,13 +12549,6 @@ "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", "license": "ISC" }, - "node_modules/scheduler": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", - "license": "MIT", - "peer": true - }, "node_modules/schema-utils": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", @@ -13452,15 +13096,6 @@ "node": ">= 14" } }, - "node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", diff --git a/package.json b/package.json index 250090d328..257e9e1c0f 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,6 @@ "chalk": "1.1.3", "chrome-remote-interface": "^0.33.3", "compression": "1.7.4", - "console-feed": "^3.8.0", "cookie": "^1.0.1", "cookie-parser": "^1.4.6", "cookie-session": "2.0.0", @@ -144,6 +143,7 @@ "@types/http-proxy": "^1.17.16", "@types/lodash": "^4.17.16", "@types/node": "^22.15.17", + "@types/semver": "^7.7.1", "@types/split": "^1.0.5", "@types/temp": "^0.9.4", "@types/tmp": "^0.2.6", diff --git a/test/api/conftest.py b/test/api/conftest.py index d468c5816a..a9b3de0023 100644 --- a/test/api/conftest.py +++ b/test/api/conftest.py @@ -22,6 +22,10 @@ STF_SECRET = 'kute kittykat' +DEFAULT_GROUPS_NUMBER = 10 +DEFAULT_GROUPS_DURATION = 1296000000 +DEFAULT_GROUPS_REPETITIONS = 10 + def pytest_addoption(parser): parser.addoption("--token", action="store") @@ -440,3 +444,17 @@ def failure_response_check_func(response, status_code=401, message=None): return response_content return failure_response_check_func + +class Quotas: + def __init__(self, number, duration, repetitions): + self.number = number + self.duration = duration + self.repetitions = repetitions + +@pytest.fixture() +def default_quotas(): + return Quotas( + DEFAULT_GROUPS_NUMBER, + DEFAULT_GROUPS_DURATION, + DEFAULT_GROUPS_REPETITIONS + ) \ No newline at end of file diff --git a/test/api/users/test_user_lifecycle_management.py b/test/api/users/test_user_lifecycle_management.py index 7749cccb60..01a9ff8887 100644 --- a/test/api/users/test_user_lifecycle_management.py +++ b/test/api/users/test_user_lifecycle_management.py @@ -12,7 +12,7 @@ class TestUserLifecycleManagement: """Test suite for user creation, modification, and deletion""" def test_create_and_delete_user_complete_flow(self, api_client, random_user, successful_response_check, - common_group_id): + common_group_id, default_quotas): """Test complete user lifecycle from creation to deletion""" user = random_user() @@ -46,11 +46,11 @@ def test_create_and_delete_user_complete_flow(self, api_client, random_user, suc # Validate user quotas quotas = user_dict.get('groups').get('quotas') - equal(quotas.get('allocated').get('number'), quotas.get('defaultGroupsNumber')) - equal(quotas.get('allocated').get('duration'), quotas.get('defaultGroupsDuration')) + equal(quotas.get('allocated').get('number'), default_quotas.number) + equal(quotas.get('allocated').get('duration'), default_quotas.duration) equal(quotas.get('consumed').get('number'), 0) equal(quotas.get('consumed').get('duration'), 0) - equal(quotas.get('repetitions'), quotas.get('defaultGroupsRepetitions')) + equal(quotas.get('repetitions'), default_quotas.repetitions) # Delete created user response = delete_user.sync_detailed(client=api_client, email=user.email) diff --git a/tsconfig.json b/tsconfig.json index b52d9744e9..77913205eb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,7 +18,10 @@ "moduleResolution": "nodenext", "target": "es2022", "rewriteRelativeImportExtensions": true, - "lib": ["ES2022"], + "lib": ["ES2022", "DOM"], + "paths": { + "@u4/adbkit/*": ["./node_modules/@u4/adbkit/*"] + }, "types": ["node"] }, "include": [ From 6178fbf9ad8a75ac2c1ec582bfc5bf01d88b0922 Mon Sep 17 00:00:00 2001 From: Elmir Khalilov <52529096+e-khalilov@users.noreply.github.com> Date: Mon, 29 Sep 2025 16:41:54 +0300 Subject: [PATCH 04/23] fix device type & types (#374) Co-authored-by: e.khalilov --- lib/units/provider/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/units/provider/index.ts b/lib/units/provider/index.ts index 912abc50d3..69d1fe5bf7 100644 --- a/lib/units/provider/index.ts +++ b/lib/units/provider/index.ts @@ -85,7 +85,7 @@ export default (async function(options: Options) { }) sub.on('message', new WireRouter() - .on(DeviceRegisteredMessage, (channel, message) => { + .on(DeviceRegisteredMessage, (channel, message: {serial: string}) => { if (workers[message.serial]?.resolveRegister) { workers[message.serial].resolveRegister!() delete workers[message.serial]?.resolveRegister @@ -130,7 +130,7 @@ export default (async function(options: Options) { // Tell others we found a device push.send([ wireutil.global, - wireutil.envelope(new wire.DeviceIntroductionMessage(device.serial, wireutil.toDeviceStatus(device.type), new wire.ProviderMessage(solo, options.name), options.deviceType)) + wireutil.envelope(new wire.DeviceIntroductionMessage(device.serial, wireutil.toDeviceStatus(device.type) || 1, new wire.ProviderMessage(solo, options.name), options.deviceType)) ]) process.nextTick(() => { // after creating workers[device.serial] obj From 7cc21d641fd43201f6cecb841c3d9ea1d5303875 Mon Sep 17 00:00:00 2001 From: Elmir Khalilov <52529096+e-khalilov@users.noreply.github.com> Date: Wed, 1 Oct 2025 16:44:34 +0300 Subject: [PATCH 05/23] reconnect in ADBObserver (#376) Co-authored-by: e.khalilov --- lib/units/provider/ADBObserver.ts | 232 +++++++++++++++++++++++------- lib/units/provider/index.ts | 2 +- 2 files changed, 180 insertions(+), 54 deletions(-) diff --git a/lib/units/provider/ADBObserver.ts b/lib/units/provider/ADBObserver.ts index 6600448e88..dfa1fbab77 100644 --- a/lib/units/provider/ADBObserver.ts +++ b/lib/units/provider/ADBObserver.ts @@ -37,10 +37,17 @@ class ADBObserver extends EventEmitter { private shouldContinuePolling: boolean = false private connection: Socket | null = null private isConnecting: boolean = false - private pendingRequests: Map void reject: (error: Error) => void - }> = new Map() + timer?: NodeJS.Timeout // Set when request is in-flight + }> = [] + private readonly requestTimeoutMs: number = 5000 // 5 second timeout per request + private readonly maxReconnectAttempts: number = 8 + private readonly initialReconnectDelayMs: number = 100 + private reconnectAttempt: number = 0 + private isReconnecting: boolean = false constructor(options?: {intervalMs?: number; host?: string; port?: number}) { if (ADBObserver.instance) { @@ -70,9 +77,7 @@ class ADBObserver extends EventEmitter { this.shouldContinuePolling = true // Initial poll - this.pollDevices().catch(err => { - this.emit('error', err) - }) + this.pollDevices() this.scheduleNextPoll() } @@ -165,9 +170,7 @@ class ADBObserver extends EventEmitter { } this.pollTimeout = setTimeout(async() => { - await this.pollDevices().catch(err => { - this.emit('error', err) - }) + await this.pollDevices() if (this.shouldContinuePolling && !this.isDestroyed) { this.scheduleNextPoll() @@ -193,14 +196,14 @@ class ADBObserver extends EventEmitter { return this.connection } - if (this.isConnecting) { - // Wait for ongoing connection attempt + if (this.isConnecting || this.isReconnecting) { + // Wait for ongoing connection or reconnection attempt return new Promise((resolve, reject) => { const checkConnection = () => { if (this.connection && !this.connection.destroyed) { resolve(this.connection) } - else if (!this.isConnecting) { + else if (!this.isConnecting && !this.isReconnecting) { reject(new Error('Connection failed')) } else { @@ -224,6 +227,7 @@ class ADBObserver extends EventEmitter { const client = net.createConnection(this.port, this.host, () => { this.connection = client this.isConnecting = false + this.reconnectAttempt = 0 // Reset reconnection counter on successful connection this.setupConnectionHandlers(client) resolve(client) }) @@ -240,7 +244,7 @@ class ADBObserver extends EventEmitter { * Setup event handlers for persistent connection */ private setupConnectionHandlers(client: Socket): void { - let responseBuffer = Buffer.alloc(0) + let responseBuffer = Buffer.alloc(0) as Buffer client.on('data', (data) => { responseBuffer = Buffer.concat([responseBuffer, data]) @@ -249,17 +253,23 @@ class ADBObserver extends EventEmitter { client.on('close', () => { this.connection = null - // Reject any pending requests - for (const [, {reject}] of this.pendingRequests) { - reject(new Error('Connection closed')) + + // Clear the timeout of in-flight request but keep it for potential retry + if (this.requestQueue.length > 0 && this.requestQueue[0].timer) { + clearTimeout(this.requestQueue[0].timer) + delete this.requestQueue[0].timer } - this.pendingRequests.clear() - // Auto-reconnect if we should continue polling + // Attempt to reconnect if we should continue polling if (this.shouldContinuePolling && !this.isDestroyed) { - this.ensureConnection().catch(err => { - this.emit('error', err) - }) + this.attemptReconnect() + } + else { + // Reject all queued requests (including in-flight one) + for (const {reject} of this.requestQueue) { + reject(new Error('Connection closed')) + } + this.requestQueue = [] } }) @@ -272,7 +282,7 @@ class ADBObserver extends EventEmitter { /** * Process ADB protocol responses and return remaining buffer */ - private processADBResponses(buffer: Buffer): Buffer { + private processADBResponses(buffer: Buffer): Buffer { let offset = 0 while (offset < buffer.length) { @@ -293,20 +303,27 @@ class ADBObserver extends EventEmitter { const responseData = buffer.subarray(offset + 8, offset + 8 + dataLength).toString('utf-8') if (status === 'OKAY') { - // Find and resolve the corresponding request - const requestId = 'host:devices' // For now, we only handle device listing - const pending = this.pendingRequests.get(requestId) - if (pending) { - this.pendingRequests.delete(requestId) - pending.resolve(responseData) + // Resolve the in-flight request (first in queue) + if (this.requestQueue.length > 0) { + const request = this.requestQueue.shift()! + if (request.timer) { + clearTimeout(request.timer) + } + request.resolve(responseData) + // Process next request in queue + this.processNextRequest() } } else if (status === 'FAIL') { - const requestId = 'host:devices' - const pending = this.pendingRequests.get(requestId) - if (pending) { - this.pendingRequests.delete(requestId) - pending.reject(new Error(responseData || 'ADB command failed')) + // Reject the in-flight request (first in queue) + if (this.requestQueue.length > 0) { + const request = this.requestQueue.shift()! + if (request.timer) { + clearTimeout(request.timer) + } + request.reject(new Error(responseData || 'ADB command failed')) + // Process next request in queue + this.processNextRequest() } } @@ -319,30 +336,132 @@ class ADBObserver extends EventEmitter { /** * Send command to ADB server using persistent connection + * Requests are queued and processed sequentially */ private async sendADBCommand(command: string): Promise { - const connection = await this.ensureConnection() + await this.ensureConnection() return new Promise((resolve, reject) => { - // Store the request for response matching - this.pendingRequests.set(command, {resolve, reject}) - - const commandBuffer = Buffer.from(command, 'utf-8') - const lengthHex = commandBuffer.length.toString(16).padStart(4, '0') - const message = Buffer.concat([ - Buffer.from(lengthHex, 'ascii'), - commandBuffer - ]) - - connection.write(message, (err) => { - if (err) { - this.pendingRequests.delete(command) - reject(err) - } - }) + // Add request to the queue + this.requestQueue.push({command, resolve, reject}) + + // Try to process the queue if no request is currently in-flight + this.processNextRequest() + }) + } + + /** + * Process the next request in the queue if no request is currently in-flight + */ + private processNextRequest(): void { + // Don't process if queue is empty or first request already in-flight + if (this.requestQueue.length === 0 || this.requestQueue[0].timer) { + return + } + + // Don't process if connection is not available + if (!this.connection || this.connection.destroyed) { + return + } + + // Get the first request in queue (don't shift yet - only shift on response) + const request = this.requestQueue[0] + const {command, reject} = request + + // Set up timeout for this request + const timer = setTimeout(() => { + if (this.requestQueue.length > 0 && this.requestQueue[0] === request) { + this.requestQueue.shift() // Remove the timed-out request + reject(new Error(`Request timeout after ${this.requestTimeoutMs}ms: ${command}`)) + // Process next request in queue + this.processNextRequest() + } + }, this.requestTimeoutMs) + + // Mark request as in-flight by setting its timer + request.timer = timer + + // Send the command + const commandBuffer = Buffer.from(command, 'utf-8') + const lengthHex = commandBuffer.length.toString(16).padStart(4, '0') + const message = Buffer.concat([ + Buffer.from(lengthHex, 'ascii'), + commandBuffer + ]) + + this.connection.write(message, (err) => { + if (err && this.requestQueue.length > 0 && this.requestQueue[0] === request) { + clearTimeout(request.timer!) + this.requestQueue.shift() // Remove the failed request + reject(err) + // Process next request in queue + this.processNextRequest() + } }) } + /** + * Attempt to reconnect with exponential backoff + */ + private async attemptReconnect(): Promise { + if (this.isReconnecting || this.isDestroyed) { + return + } + + this.isReconnecting = true + + for (let attempt = 0; attempt < this.maxReconnectAttempts; attempt++) { + this.reconnectAttempt = attempt + 1 + + // Calculate exponential backoff delay + const delay = this.initialReconnectDelayMs * Math.pow(2, attempt) + + // Wait before attempting reconnection + await new Promise(resolve => setTimeout(resolve, delay)) + + if (!this.shouldContinuePolling || this.isDestroyed) { + this.isReconnecting = false + return + } + + try { + // Attempt to create a new connection + await this.createConnection() + this.reconnectAttempt = 0 + this.isReconnecting = false + + // Resend the in-flight request if it exists + if (this.requestQueue.length > 0 && !this.requestQueue[0].timer) { + // The first request was in-flight but timer was cleared on disconnect + // Resend it by calling processNextRequest + this.processNextRequest() + } + + return // Successfully reconnected + } + catch { + // Continue to next attempt + continue + } + } + + // All reconnection attempts failed + this.isReconnecting = false + this.reconnectAttempt = 0 + + const error = new Error(`Failed to reconnect to ADB server after ${this.maxReconnectAttempts} attempts`) + this.emit('error', error) + + // Reject all queued requests (including in-flight one) + for (const request of this.requestQueue) { + if (request.timer) { + clearTimeout(request.timer) + } + request.reject(error) + } + this.requestQueue = [] + } + /** * Close the persistent connection */ @@ -352,11 +471,18 @@ class ADBObserver extends EventEmitter { this.connection = null } - // Reject any pending requests - for (const [, {reject}] of this.pendingRequests) { - reject(new Error('Connection closed')) + // Reset reconnection state + this.isReconnecting = false + this.reconnectAttempt = 0 + + // Reject all queued requests (including in-flight one) + for (const request of this.requestQueue) { + if (request.timer) { + clearTimeout(request.timer) + } + request.reject(new Error('Connection closed')) } - this.pendingRequests.clear() + this.requestQueue = [] } /** diff --git a/lib/units/provider/index.ts b/lib/units/provider/index.ts index 69d1fe5bf7..86f55abb54 100644 --- a/lib/units/provider/index.ts +++ b/lib/units/provider/index.ts @@ -299,7 +299,7 @@ export default (async function(options: Options) { // Track and manage devices const tracker = new ADBObserver({ - intervalMs: 2000, + intervalMs: 3000, port: options.adbPort, host: options.adbHost }) From edd413f631d0736410ab28bb9049ab71d328d693 Mon Sep 17 00:00:00 2001 From: Alexey Chistov <33050834+Alk2017@users.noreply.github.com> Date: Wed, 1 Oct 2025 16:55:44 +0300 Subject: [PATCH 06/23] QA-10057 Remove users watcher groups-engine (#369) * -fix after rebase -remove user watcher -user handler for alert message default quotas handler -fix merge del handler update quoats handler user handler method some fix -move without change method of devices to separate class -fix after move users method -move db.users method to separate class * add missing db method --------- Co-authored-by: a.chistov Co-authored-by: e.khalilov --- lib/db/api.ts | 8 +- lib/db/handlers/group/scheduler.js | 3 +- lib/db/handlers/user/index.js | 103 +++++ lib/db/index.ts | 3 +- lib/db/models/all/model.js | 505 +--------------------- lib/db/models/device/index.js | 4 + lib/db/models/device/model.js | 127 ++++++ lib/db/models/group/model.js | 71 +-- lib/db/models/user/index.js | 4 + lib/db/models/user/model.js | 446 +++++++++++++++++++ lib/units/api/controllers/groups.js | 10 +- lib/units/api/controllers/users.js | 64 +-- lib/units/groups-engine/index.js | 2 - lib/units/groups-engine/watchers/users.js | 105 ----- lib/units/processor/index.ts | 3 +- lib/units/websocket/index.js | 41 +- 16 files changed, 819 insertions(+), 680 deletions(-) create mode 100644 lib/db/handlers/user/index.js create mode 100644 lib/db/models/device/index.js create mode 100644 lib/db/models/device/model.js create mode 100644 lib/db/models/user/index.js create mode 100644 lib/db/models/user/model.js delete mode 100644 lib/units/groups-engine/watchers/users.js diff --git a/lib/db/api.ts b/lib/db/api.ts index 1e2e3acaab..5200126262 100644 --- a/lib/db/api.ts +++ b/lib/db/api.ts @@ -1,6 +1,8 @@ import AllModel from './models/all/index.js' import GroupModel from './models/group/index.js' import TeamModel from './models/team/index.js' +import UserModel from './models/user/index.js' +import DeviceModel from './models/device/index.js' const concatModels = (...models: T) => Object.assign({}, ...models) @@ -12,5 +14,7 @@ const concatModels = (...models: T) => export default concatModels( AllModel, GroupModel, - TeamModel -) as typeof AllModel & typeof GroupModel & typeof TeamModel + TeamModel, + UserModel, + DeviceModel, +) as typeof AllModel & typeof GroupModel & typeof TeamModel & typeof UserModel & typeof DeviceModel diff --git a/lib/db/handlers/group/scheduler.js b/lib/db/handlers/group/scheduler.js index 3ac71e199b..ddfbca40f6 100644 --- a/lib/db/handlers/group/scheduler.js +++ b/lib/db/handlers/group/scheduler.js @@ -5,6 +5,7 @@ import db from '../../index.js' import logger from '../../../util/logger.js' import mongo from 'mongodb' +import UserModel from '../../models/user/index.js' const log = logger.createLogger('groups-scheduler') @@ -211,7 +212,7 @@ export default class GroupsScheduler { await dbapi.updateDevicesCurrentGroupFromOrigin(group.devices) } - await dbapi.updateUserGroupDuration(group.owner.email, group.duration, duration) + await UserModel.updateUserGroupDuration(group.owner.email, group.duration, duration) await this.scheduleAllGroupsTasks() } catch (err) { diff --git a/lib/db/handlers/user/index.js b/lib/db/handlers/user/index.js new file mode 100644 index 0000000000..132d9a35a0 --- /dev/null +++ b/lib/db/handlers/user/index.js @@ -0,0 +1,103 @@ +import _ from 'lodash' +import logger from '../../../util/logger.js' +import wireutil from '../../../wire/util.js' +import wire from '../../../wire/index.js' +import timeutil from '../../../util/timeutil.js' + +class UserChangeHandler { + + isPrepared = false + log = logger.createLogger('change-handler-users') + + init(push, pushdev, channelRouter) { + this.pushdev = pushdev + this.isPrepared = !!this.pushdev + } + + sendUserChange = (user, isAddedGroup, groups, action, targets) => { + function wireUserField() { + const wireUser = _.cloneDeep(user) + delete wireUser._id + delete wireUser.ip + delete wireUser.group + delete wireUser.lastLoggedInAt + delete wireUser.createdAt + delete wireUser.forwards + delete wireUser.acceptedPolicy + delete wireUser.groups.lock + delete wireUser.groups.defaultGroupsDuration + delete wireUser.groups.defaultGroupsNumber + delete wireUser.groups.defaultGroupsRepetitions + delete wireUser.groups.repetitions + return wireUser + } + const userField = wireUserField() + this.pushdev.send([ + wireutil.global, + wireutil.envelope(new wire.UserChangeMessage( + userField + , isAddedGroup + , groups + , action + , targets + , timeutil.now('nano') + )) + ]) + } + + updateUserHandler = (oldUser, newUser) => { + if (newUser === null && oldUser === null) { + this.log.info('New user doc and old user doc is NULL') + return false + } + const targets = [] + if (newUser?.groups && oldUser?.groups) { + if (newUser.groups.quotas && oldUser.groups.quotas) { + if (!_.isEqual(newUser.groups.quotas.allocated, oldUser.groups.quotas.allocated)) { + targets.push('settings') + targets.push('view') + } + else if (!_.isEqual(newUser.groups.quotas.consumed, oldUser.groups.quotas.consumed)) { + targets.push('view') + } + else if (newUser.groups.quotas.defaultGroupsNumber !== + oldUser.groups.quotas.defaultGroupsNumber || + newUser.groups.quotas.defaultGroupsDuration !== + oldUser.groups.quotas.defaultGroupsDuration || + newUser.groups.quotas.defaultGroupsRepetitions !== + oldUser.groups.quotas.defaultGroupsRepetitions || + newUser.groups.quotas.repetitions !== + oldUser.groups.quotas.repetitions || + !_.isEqual(newUser.groups.subscribed, oldUser.groups.subscribed)) { + targets.push('settings') + } + } + } + if (!_.isEqual(newUser?.settings.alertMessage, oldUser?.settings.alertMessage)) { + targets.push('menu') + } + if (targets.length) { + this.sendUserChange(newUser, newUser.groups.subscribed.length > oldUser.groups.subscribed.length + , _.xor(newUser.groups.subscribed, oldUser.groups.subscribed), 'updated', targets) + } + return !_.isEqual(newUser, oldUser) + } + +} + +// Temporary solution needed to avoid situations +// where a unit may not initialize the change handler, +// but use the db module. In this case, any methods of this handler +// do nothing and will not cause an error. +/** @type {UserChangeHandler} */ +export default new Proxy(new UserChangeHandler(), { + + /** @param {string} prop */ + get(target, prop) { + if (target.isPrepared || prop === 'init' || typeof target[prop] !== 'function') { + return target[prop] + } + + return () => {} + } +}) diff --git a/lib/db/index.ts b/lib/db/index.ts index 2928f5abab..8e3ab3b1bf 100644 --- a/lib/db/index.ts +++ b/lib/db/index.ts @@ -3,6 +3,7 @@ import _setup from './setup.js' import srv from '../util/srv.js' import EventEmitter from 'events' import GroupChangeHandler from './handlers/group/index.js' +import UserChangeHandler from './handlers/user/index.js' import * as zmqutil from '../util/zmqutil.js' import lifecycle from '../util/lifecycle.js' import logger from '../util/logger.js' @@ -27,7 +28,7 @@ const handlers: { channelRouter?: EventEmitter ) => Promise | void; isPrepared: boolean; -}[] = [GroupChangeHandler] +}[] = [GroupChangeHandler, UserChangeHandler] export default class DbClient { static connection: mongo.Db diff --git a/lib/db/models/all/model.js b/lib/db/models/all/model.js index 4cb3b46f63..1082680ac9 100644 --- a/lib/db/models/all/model.js +++ b/lib/db/models/all/model.js @@ -8,6 +8,7 @@ import wireutil from '../../../wire/util.js' import {v4 as uuidv4} from 'uuid' import * as apiutil from '../../../util/apiutil.js' import GroupModel from '../group/index.js' +import UserModel from '../user/index.js' import logger from '../../../util/logger.js' import {getRootGroup, getGroup} from '../group/model.js' @@ -51,7 +52,7 @@ export const unlockBookingObjects = function() { $set: {'group.lock': false} } ), - db.collection('groups').updateMany( + db.groups.updateMany( {}, { $set: { @@ -74,7 +75,7 @@ export const createBootStrap = function(env) { const now = Date.now() function updateUsersForMigration(group) { - return getUsers().then(function(users) { + return UserModel.getUsers().then(function(users) { return Promise.all(users.map(async(user) => { const data = { privilege: user?.email !== group?.owner.email ? apiutil.USER : apiutil.ADMIN, @@ -161,7 +162,7 @@ export const createBootStrap = function(env) { envUserGroupsRepetitions: apiutil.MAX_USER_GROUPS_REPETITIONS }) .then(function(group) { - return saveUserAfterLogin({ + return UserModel.saveUserAfterLogin({ name: group?.owner.name, email: group?.owner.email, ip: '127.0.0.1' @@ -173,42 +174,11 @@ export const createBootStrap = function(env) { return updateDevicesForMigration(group) }) .then(function() { - return reserveUserGroupInstance(group?.owner?.email) + return UserModel.reserveUserGroupInstance(group?.owner?.email) }) }) } -export const deleteUser = function(email) { - return db.users.deleteOne({email: email}) -} - -// dbapi.getUsers = function() { -export const getUsers = function() { - return db.users.find().toArray() -} - -// dbapi.getEmails = function() { -export const getEmails = function() { - return db.users - .find({ - privilege: { - $ne: apiutil.ADMIN - } - }) - .project({email: 1, _id: 0}) - .toArray() -} - -// dbapi.getAdmins = function() { -export const getAdmins = function() { - return db.users - .find({ - privilege: apiutil.ADMIN - }) - .project({email: 1, _id: 0}) - .toArray() -} - export const lockDeviceByCurrent = function(groups, serial) { function wrappedlockDeviceByCurrent() { return db.devices.findOne({serial: serial}).then(oldDoc => { @@ -345,39 +315,10 @@ export const setLockOnDevices = function(serials, lock) { ) } -/** - * @deprecated Do not use locks in database. - */ -function setLockOnUser(email, state) { - return db.users.findOne({email: email}).then(oldDoc => { - if (!oldDoc || !oldDoc.groups) { - throw new Error(`User with email ${email} not found or groups field is missing.`) - } - return db.users.updateOne( - {email: email}, - { - $set: { - 'groups.lock': oldDoc.groups.lock !== state ? state : oldDoc.groups.lock - } - } - ) - .then(updateStats => { - return db.users.findOne({email: email}).then(newDoc => { - // @ts-ignore - updateStats.changes = [ - {new_val: {...newDoc}, old_val: {...oldDoc}} - ] - return updateStats - }) - }) - }) -} - - // dbapi.lockUser = function(email) { export const lockUser = function(email) { function wrappedlockUser() { - return setLockOnUser(email, true) + return UserModel.setLockOnUser(email, true) .then(function(stats) { return apiutil.lockResult(stats) }) @@ -391,7 +332,7 @@ export const lockUser = function(email) { // dbapi.unlockUser = function(email) { export const unlockUser = function(email) { - return setLockOnUser(email, false) + return UserModel.setLockOnUser(email, false) } // dbapi.isDeviceBooked = function(serial) { @@ -400,197 +341,6 @@ export const isDeviceBooked = function(serial) { .then(groups => !!groups?.length) } -// dbapi.createUser = function(email, name, ip) { -export const createUser = function(email, name, ip, privilege) { - return GroupModel.getRootGroup().then(function(group) { - return loadUser(group?.owner.email).then(function(adminUser) { - let userObj = { - email: email, - name: name, - ip: ip, - group: wireutil.makePrivateChannel(), - lastLoggedInAt: getNow(), - createdAt: getNow(), - forwards: [], - settings: {}, - acceptedPolicy: false, - privilege: privilege || (adminUser ? apiutil.USER : apiutil.ADMIN), - groups: { - subscribed: [], - lock: false, - quotas: { - allocated: { - number: adminUser ? adminUser.groups.quotas.defaultGroupsNumber : group?.envUserGroupsNumber, - duration: adminUser ? adminUser.groups.quotas.defaultGroupsDuration : group?.envUserGroupsDuration - }, - consumed: { - number: 0, - duration: 0 - }, - defaultGroupsNumber: adminUser ? 0 : group?.envUserGroupsNumber, - defaultGroupsDuration: adminUser ? 0 : group?.envUserGroupsDuration, - defaultGroupsRepetitions: adminUser ? 0 : group?.envUserGroupsRepetitions, - repetitions: adminUser ? adminUser.groups.quotas.defaultGroupsRepetitions : group?.envUserGroupsRepetitions - } - } - } - return db.users.insertOne(userObj) - .then(function(stats) { - if (stats.insertedId) { - return GroupModel.addGroupUser(group?.id, email).then(function() { - return loadUser(email).then(function(user) { - // @ts-ignore - stats.changes = [ - {new_val: {...user}} - ] - return stats - }) - }) - } - return stats - }) - }) - }) -} - -// dbapi.saveUserAfterLogin = function(user) { -export const saveUserAfterLogin = function(user) { - const updateData = { - name: user?.name, - ip: user?.ip, - lastLoggedInAt: getNow() - } - - if (user?.privilege) { - updateData.privilege = user?.privilege - } - - return db.users.updateOne({email: user?.email}, {$set: updateData}) - // @ts-ignore - .then(stats => { - if (stats.modifiedCount === 0) { - return createUser(user?.email, user?.name, user?.ip, user?.privilege) - } - return stats - }) -} - -// dbapi.loadUser = function(email) { -export const loadUser = function(email) { - return db.users.findOne({email: email}) -} - -// dbapi.updateUsersAlertMessage = function(alertMessage) { -export const updateUsersAlertMessage = function(alertMessage) { - return db.users.updateOne( - { - email: apiutil.STF_ADMIN_EMAIL - } - , { - $set: Object.fromEntries(Object.entries(alertMessage).map(([key, value]) => - ['settings.alertMessage.' + key, value] - )), - } - ).then(updateStats => { - return db.users.findOne({email: apiutil.STF_ADMIN_EMAIL}).then(updatedMainAdmin => { - // @ts-ignore - updateStats.changes = [ - {new_val: {...updatedMainAdmin}} - ] - return updateStats - }) - }) -} - -// dbapi.updateUserSettings = function(email, changes) { -export const updateUserSettings = function(email, changes) { - return db.users.findOne({email: email}).then(user => { - return db.users.updateOne( - { - email: email - } - , { - $set: { - settings: {...user?.settings, ...changes} - } - } - ) - }) -} - -// dbapi.resetUserSettings = function(email) { -export const resetUserSettings = function(email) { - return db.users.updateOne({email: email}, - { - $set: { - settings: {} - } - }) -} - -export const getUserAdbKeys = function(email) { - return db.users.findOne({email: email}) - .then(user => user?.adbKeys || []) -} - -// dbapi.insertUserAdbKey = function(email, key) { -export const insertUserAdbKey = function(email, key) { - let data = { - title: key.title, - fingerprint: key.fingerprint - } - return db.users.findOne({email: email}).then(user => { - let adbKeys = user?.adbKeys ? user?.adbKeys : [] - adbKeys.push(data) - return db.users.updateOne( - {email: email} - , {$set: {adbKeys: user?.adbKeys ? adbKeys : [data]}} - ) - }) -} - -// dbapi.deleteUserAdbKey = function(email, fingerprint) { -export const deleteUserAdbKey = function(email, fingerprint) { - return db.users.findOne({email: email}).then(user => { - return db.users.updateOne( - {email: email} - , { - $set: { - adbKeys: user?.adbKeys ? user?.adbKeys.filter(key => { - return key.fingerprint !== fingerprint - }) : [] - } - } - ) - }) -} - -// dbapi.lookupUsersByAdbKey = function(fingerprint) { -export const lookupUsersByAdbKey = function(fingerprint) { - return db.users.find({ - adbKeys: fingerprint - }).toArray() -} - -// dbapi.lookupUserByAdbFingerprint = function(fingerprint) { -export const lookupUserByAdbFingerprint = function(fingerprint) { - return db.users.find( - {adbKeys: {$elemMatch: {fingerprint: fingerprint}}} - // @ts-ignore - , {email: 1, name: 1, group: 1, _id: 0} - ).toArray() - .then(function(users) { - switch (users.length) { - case 1: - return users[0] - case 0: - return null - default: - throw new Error('Found multiple users for same ADB fingerprint') - } - }) -} - // dbapi.lookupUserByVncAuthResponse = function(response, serial) { export const lookupUserByVncAuthResponse = function(response, serial) { return db.collection('vncauth').aggregate([ @@ -628,20 +378,6 @@ export const lookupUserByVncAuthResponse = function(response, serial) { }) } -// dbapi.loadUserDevices = function(email) { -export const loadUserDevices = function(email) { - return db.users.findOne({email: email}).then(user => { - let userGroups = user?.groups.subscribed - return db.devices.find( - { - 'owner.email': email, - present: true, - 'group.id': {$in: userGroups} - } - ).toArray() - }) -} - // dbapi.saveDeviceLog = function(serial, entry) { export const saveDeviceLog = function(serial, entry) { return db.connect().then(() => @@ -1161,11 +897,6 @@ export const loadDeviceBySerial = function(serial) { return findDevice({serial: serial}) } -// dbapi.loadDevicesBySerials = function(serials) { -export const loadDevicesBySerials = function(serials) { - return db.devices.find({serial: {$in: serials}}).toArray() -} - // dbapi.loadDevice = function(groups, serial) { export const loadDevice = function(groups, serial) { return findDevice({ @@ -1272,33 +1003,6 @@ export const loadAccessTokenByTitle = function(email, title) { return db.collection('accessTokens').findOne({email: email, title: title}) } -// dbapi.grantAdmin = function(email) { -export const grantAdmin = function(email) { - return db.users.findOneAndUpdate({email: email}, { - $set: { - privilege: apiutil.ADMIN - } - }, {returnDocument: 'after'}) -} - -// dbapi.revokeAdmin = function(email) { -export const revokeAdmin = function(email) { - return db.users.findOneAndUpdate({email: email}, { - $set: { - privilege: apiutil.USER - } - }, {returnDocument: 'after'}) -} - -// dbapi.acceptPolicy = function(email) { -export const acceptPolicy = function(email) { - return db.users.updateOne({email: email}, { - $set: { - acceptedPolicy: true - } - }) -} - // dbapi.writeStats = function(user, serial, action) { // { // event_type: string, @@ -1331,32 +1035,9 @@ export const sendEvent = function(eventType, eventDetails, linkedEntities, times }) } -// dbapi.getDevicesCount = function() { -export const getDevicesCount = function() { - return db.devices.find().count() -} - -// dbapi.getOfflineDevicesCount = function() { -export const getOfflineDevicesCount = function() { - return db.devices.find( - { - present: false - } - ).count() -} - -// dbapi.getOfflineDevices = function() { -export const getOfflineDevices = function() { - return db.devices.find( - {present: false}, - // @ts-ignore - {_id: 0, 'provider.name': 1} - ).toArray() -} - // dbapi.isPortExclusive = function(newPort) { export const isPortExclusive = function(newPort) { - return getAllocatedAdbPorts().then((ports) => { + return DeviceModel.getAllocatedAdbPorts().then((ports) => { let result = !!ports.find(port => port === newPort) return !result }) @@ -1364,7 +1045,7 @@ export const isPortExclusive = function(newPort) { // dbapi.getLastAdbPort = function() { export const getLastAdbPort = function() { - return getAllocatedAdbPorts().then((ports) => { + return DeviceModel.getAllocatedAdbPorts().then((ports) => { if (ports.length === 0) { return 0 } @@ -1372,27 +1053,6 @@ export const getLastAdbPort = function() { }) } -// dbapi.getAllocatedAdbPorts = function() { -export const getAllocatedAdbPorts = function() { - // @ts-ignore - return db.devices.find({}, {adbPort: 1, _id: 0}).toArray().then(ports => { - let result = [] - ports.forEach((port) => { - if (port.adbPort) { - let portNum - if (typeof port.adbPort === 'string') { - portNum = parseInt(port.adbPort.replace(/["']/g, ''), 10) - } - else { - portNum = port.adbPort - } - result.push(portNum) - } - }) - return result.sort((a, b) => a - b) - }) -} - // dbapi.initiallySetAdbPort = function(serial) { export const initiallySetAdbPort = function(serial) { return getFreeAdbPort() @@ -1509,12 +1169,19 @@ export const sizeIosDevice = function(serial, height, width, scale) { ) } -// dbapi.getDeviceDisplaySize = function(serial) { -export const getDeviceDisplaySize = function(serial) { - return db.devices.findOne({serial: serial}) - .then(result => { - return result?.display - }) +// TODO Check usage. Probably dead code +export const setAbsentDisconnectedDevices = function() { + return db.devices.updateOne( + { + platform: 'iOS' + }, + { + $set: { + present: false, + ready: false + } + } + ) } // dbapi.getInstalledApplications = function(message) { @@ -1536,14 +1203,6 @@ export const setDeviceType = function(serial, type) { ) } -// dbapi.getDeviceType = function(serial) { -export const getDeviceType = function(serial) { - return db.devices.findOne({serial: serial}) - .then(result => { - return result?.deviceType - }) -} - // dbapi.initializeIosDeviceState = function(publicIp, message) { export const initializeIosDeviceState = function(publicIp, message) { const screenWsUrlPattern = @@ -1621,126 +1280,6 @@ export const initializeIosDeviceState = function(publicIp, message) { }) } -export const reserveUserGroupInstance = async(email) => { - return db.users.updateMany( - {email} - , [{ - $set: {'groups.quotas.consumed.number': { - $min: [{ - $sum: ['$groups.quotas.consumed.number', 1] - }, '$groups.quotas.allocated.number']} - } - }] - ) -} - -export const releaseUserGroupInstance = async(email) => { - return db.users.updateMany( - { - email - } - , [{ - $set: {'groups.quotas.consumed.number': { - $max: [{ - $sum: ['$groups.quotas.consumed.number', -1] - }, 0]} - } - }] - ) -} - -export const updateUserGroupDuration = async(email, oldDuration, newDuration) => { - return db.users.updateOne( - {email: email} - , [{ - $set: { - 'groups.quotas.consumed.duration': { - $cond: [ - {$lte: [{$sum: ['$groups.quotas.consumed.duration', newDuration, -oldDuration]}, '$groups.quotas.allocated.duration']}, - {$sum: ['$groups.quotas.consumed.duration', newDuration, -oldDuration]}, - '$groups.quotas.consumed.duration' - ] - } - } - }] - ) -} - -export const updateUserGroupsQuotas = async(email, duration, number, repetitions) => { - const oldDoc = await db.users.findOne({email: email}) - - const consumed = oldDoc?.groups.quotas.consumed.duration - const allocated = oldDoc?.groups.quotas.allocated.duration - const consumedNumber = oldDoc?.groups.quotas.consumed.number - const allocatedNumber = oldDoc?.groups.quotas.allocated.number - - const updateStats = await db.users.updateOne( - {email: email} - , { - $set: { - 'groups.quotas.allocated.duration': duration && consumed <= duration && - (!number || consumedNumber <= number) ? duration : allocated, - 'groups.quotas.allocated.number': number && consumedNumber <= number && - (!duration || consumed <= duration) ? number : allocatedNumber, - 'groups.quotas.repetitions': repetitions || oldDoc?.groups.quotas.repetitions - } - } - ) - - const newDoc = await db.users.findOne({email: email}) - // @ts-ignore - updateStats.changes = [ - {new_val: {...newDoc}, old_val: {...oldDoc}} - ] - - return updateStats -} - -export const updateDefaultUserGroupsQuotas = async(email, duration, number, repetitions) => { - const updateStats = await db.users.updateOne( - {email: email} - , [{ - $set: { - 'groups.quotas.defaultGroupsDuration': { - $cond: [ - { - $ne: [duration, null] - }, - duration, - '$groups.quotas.defaultGroupsDuration' - ] - }, - 'groups.quotas.defaultGroupsNumber': { - $cond: [ - { - $ne: [number, null] - }, - number, - '$groups.quotas.defaultGroupsNumber' - ] - }, - 'groups.quotas.defaultGroupsRepetitions': { - $cond: [ - { - $ne: [repetitions, null] - }, - repetitions, - '$groups.quotas.defaultGroupsRepetitions' - ] - } - } - }] - ) - - const newDoc = await db.users.findOne({email: email}) - // @ts-ignore - updateStats.changes = [ - {new_val: {...newDoc}} - ] - - return updateStats -} - export const updateDeviceGroupName = async(serial, group) => { return db.devices.updateOne( {serial: serial} diff --git a/lib/db/models/device/index.js b/lib/db/models/device/index.js new file mode 100644 index 0000000000..61ca31630b --- /dev/null +++ b/lib/db/models/device/index.js @@ -0,0 +1,4 @@ +import proxiedModel from '../../proxiedModel.js' +import * as model from './model.js' + +export default proxiedModel(model) diff --git a/lib/db/models/device/model.js b/lib/db/models/device/model.js new file mode 100644 index 0000000000..ef29f27e49 --- /dev/null +++ b/lib/db/models/device/model.js @@ -0,0 +1,127 @@ +/* * + * Copyright 2025 contains code contributed by V Kontakte LLC - Licensed under the Apache license 2.0 + * */ +// @ts-nocheck +import logger from '../../../util/logger.js' +import db from '../../index.js' + +const log = logger.createLogger('dbapi:device') + +/* +=========================================================== +==================== without change DB ==================== +=========================================================== +*/ + +// dbapi.loadUserDevices = function(email) { +export const loadUserDevices = function(email) { + return db.users.findOne({email: email}).then(user => { + let userGroups = user?.groups.subscribed + return db.devices.find( + { + 'owner.email': email, + present: true, + 'group.id': {$in: userGroups} + } + ).toArray() + }) +} + +// dbapi.loadPresentDevices = function() { +export const loadPresentDevices = function() { + return db.devices.find({present: true}).toArray() +} + +// dbapi.loadDevicesBySerials = function(serials) { +export const loadDevicesBySerials = function(serials) { + return db.devices.find({serial: {$in: serials}}).toArray() +} + +// dbapi.getDevicesCount = function() { +export const getDevicesCount = function() { + return db.devices.countDocuments() +} + +// dbapi.getOfflineDevicesCount = function() { +export const getOfflineDevicesCount = function() { + return db.devices.countDocuments( + { + present: false + } + ) +} + +// dbapi.getOfflineDevices = function() { +export const getOfflineDevices = function() { + return db.devices.find( + {present: false}, + // @ts-ignore + {_id: 0, 'provider.name': 1} + ).toArray() +} + +// dbapi.getAllocatedAdbPorts = function() { +export const getAllocatedAdbPorts = function() { + // @ts-ignore + return db.devices.find({}, {adbPort: 1, _id: 0}).toArray().then(ports => { + let result = [] + ports.forEach((port) => { + if (port.adbPort) { + let portNum + if (typeof port.adbPort === 'string') { + portNum = parseInt(port.adbPort.replace(/["']/g, ''), 10) + } + else { + portNum = port.adbPort + } + result.push(portNum) + } + }) + return result.sort((a, b) => a - b) + }) +} + +// dbapi.getDeviceDisplaySize = function(serial) { +export const getDeviceDisplaySize = function(serial) { + return db.devices.findOne({serial: serial}) + .then(result => { + return result?.display + }) +} + +// dbapi.getDeviceType = function(serial) { +export const getDeviceType = function(serial) { + return db.devices.findOne({serial: serial}) + .then(result => { + return result?.deviceType + }) +} + +// dbapi.generateIndexes = function() { +export const generateIndexes = function() { + db.devices.createIndex({serial: -1}).then((result) => { + log.info('Created indexes with result - ' + result) + }) +} + +/* +==================================================================== +==================== changing DB - use handlers ==================== +==================================================================== +*/ + + + + +// dbapi.deleteDevice = function(serial) { +export const deleteDevice = function(serial) { + return db.devices.deleteOne({serial: serial}) +} + +/* +==================================================== +==================== deprecated ==================== +==================================================== +*/ + + diff --git a/lib/db/models/group/model.js b/lib/db/models/group/model.js index dbc836f85b..a68fd84eb8 100644 --- a/lib/db/models/group/model.js +++ b/lib/db/models/group/model.js @@ -2,14 +2,17 @@ * Copyright 2025 contains code contributed by V Kontakte LLC - Licensed under the Apache license 2.0 * */ + import {v4 as uuidv4} from 'uuid' import db from '../../index.js' import * as apiutil from '../../../util/apiutil.js' import logger from '../../../util/logger.js' import AllModel from '../all/index.js' +import UserModel from '../user/index.js' import _ from 'lodash' import util from 'util' import GroupChangeHandler from '../../handlers/group/index.js' +import UserChangeHandler from '../../handlers/user/index.js' import {isOriginGroup} from '../../../util/apiutil.js' import mongo from 'mongodb' @@ -54,7 +57,7 @@ export const getGroups = async(filter) => { export const addGroupUser = async(id, email) => { try { - const stats = await Promise.all([ + const [group, user] = await Promise.all([ db.groups.findOneAndUpdate( { id: id @@ -66,7 +69,7 @@ export const addGroupUser = async(id, email) => { }, {returnDocument: 'before'} ), - db.users.updateOne( + db.users.findOneAndUpdate( { email: email }, @@ -78,12 +81,17 @@ export const addGroupUser = async(id, email) => { ) ]) - if (stats[0]?.id) { - GroupChangeHandler.sendGroupChange(stats[0], stats[0].users.concat([email]), false, false, true, [email], false, [], 'updated') - GroupChangeHandler.treatGroupUsersChange(stats[0], [email], stats[0].isActive, true) + if (group?.id) { + GroupChangeHandler.sendGroupChange(group, group.users.concat([email]), false, false, true, [email], false, [], 'updated') + GroupChangeHandler.treatGroupUsersChange(group, [email], group.isActive, true) } - return stats[0] && stats[1].modifiedCount === 0 ? 'unchanged ' + email : 'added ' + email + var userModifided = false + if (user) { + const newUser = await UserModel.loadUser(email) + userModifided = UserChangeHandler.updateUserHandler(user, newUser) + } + return group && !userModifided ? 'unchanged ' + email : 'added ' + email } catch (err) { if (err instanceof TypeError) { @@ -96,7 +104,7 @@ export const addGroupUser = async(id, email) => { } export const addAdminsToGroup = async(id) => { - const admins = await AllModel.getAdmins() + const admins = await UserModel.getAdmins() const group = await db.groups.findOne({id: id}) const adminsEmails = Array.from( @@ -106,19 +114,19 @@ export const addAdminsToGroup = async(id) => { const newUsers = (group?.users || []).concat(adminsEmails) - await Promise.all( - adminsEmails.map( - email => db.users.findOne({email}) - .then(user => db.users.updateOne( - {email}, - { - $set: { - 'groups.subscribed': (user?.groups?.subscribed || []).concat([id]) - } - } - )) + adminsEmails.map(async email => { + const oldUser = await UserModel.loadUser(email) + const newUser = await db.users.findOneAndUpdate( + {email}, + { + $set: { + 'groups.subscribed': (oldUser?.groups?.subscribed || []).concat([id]) + } + }, + {returnDocument: 'after'} ) - ) + UserChangeHandler.updateUserHandler(oldUser, newUser) + }) const stats = await db.groups.updateOne( @@ -152,7 +160,7 @@ export const removeGroupUser = async(id, email) => { return "Can't remove owner of group." } - const [group] = await Promise.all([ + const [group, user] = await Promise.all([ db.groups.findOneAndUpdate( {id: id} , { @@ -163,7 +171,7 @@ export const removeGroupUser = async(id, email) => { } , {returnDocument: 'after'} ), - db.users.updateOne( + db.users.findOneAndUpdate( {email: email} , { $pull: {'groups.subscribed': id} @@ -174,6 +182,11 @@ export const removeGroupUser = async(id, email) => { GroupChangeHandler.sendGroupChange(group, group?.users, false, false, false, [email], false, [], 'updated') GroupChangeHandler.treatGroupUsersChange(group, [email], group?.isActive, false) + if (user) { + const newUser = await UserModel.loadUser(email) + UserChangeHandler.updateUserHandler(user, newUser) + } + return 'deleted' } @@ -239,7 +252,7 @@ export const addGroupDevices = async(group, serials) => { } const duration = apiutil.computeDuration(group, serials.length) - const stats = await AllModel.updateUserGroupDuration(group.owner.email, group.duration, duration) + const stats = await UserModel.updateUserGroupDuration(group.owner.email, group.duration, duration) if (!stats.modifiedCount) { throw 'quota is reached' } @@ -280,7 +293,7 @@ export const addGroupDevices = async(group, serials) => { /** @returns {Promise | null>} */ export const removeGroupDevices = async(group, serials) => { const duration = apiutil.computeDuration(group, -serials.length) - await AllModel.updateUserGroupDuration(group.owner.email, group.duration, duration) + await UserModel.updateUserGroupDuration(group.owner.email, group.duration, duration) const newGroup = await db.groups.findOneAndUpdate( {id: group.id} @@ -326,7 +339,7 @@ export const getUserGroup = async(email, id) => { export const getUserGroups = async(email) => { const pipeline = {users: {$in: [email]}} - const admins = await AllModel.getAdmins() + const admins = await UserModel.getAdmins() const adminEmails = admins.map(admin => admin.email) if (!adminEmails.includes(email)) { @@ -419,7 +432,7 @@ export const getGroupAsOwnerOrAdmin = async(email, id) => { return group } - const user = await AllModel.loadUser(email) + const user = await UserModel.loadUser(email) if (user && user.privilege === apiutil.ADMIN) { return group } @@ -462,7 +475,7 @@ export const createGroup = async(data) => { /** @returns {Promise | boolean | null>} */ export const createUserGroup = async(data) => { - const stats = await AllModel.reserveUserGroupInstance(data.owner.email) + const stats = await UserModel.reserveUserGroupInstance(data.owner.email) if (!stats.modifiedCount) { log.info(`Could not reserve group for user ${data.owner.email}`) return false @@ -501,7 +514,7 @@ export const updateDeviceOriginGroup = (serial, group, signature) => /** @returns {Promise | boolean | null>} */ export const updateUserGroup = async(group, data) => { - const stats = await AllModel.updateUserGroupDuration(group.owner.email, group.duration, data.duration) + const stats = await UserModel.updateUserGroupDuration(group.owner.email, group.duration, data.duration) if (!stats.modifiedCount && !stats.matchedCount || group.duration !== data.duration) { return false } @@ -563,8 +576,8 @@ export const deleteUserGroup = async(id) => { )) ) - await AllModel.releaseUserGroupInstance(group?.owner.email) - await AllModel.updateUserGroupDuration(group?.owner.email, group?.duration, 0) + await UserModel.releaseUserGroupInstance(group?.owner.email) + await UserModel.updateUserGroupDuration(group?.owner.email, group?.duration, 0) if (apiutil.isOriginGroup(group?.class)) { await AllModel.returnDevicesToRoot(group?.devices) diff --git a/lib/db/models/user/index.js b/lib/db/models/user/index.js new file mode 100644 index 0000000000..61ca31630b --- /dev/null +++ b/lib/db/models/user/index.js @@ -0,0 +1,4 @@ +import proxiedModel from '../../proxiedModel.js' +import * as model from './model.js' + +export default proxiedModel(model) diff --git a/lib/db/models/user/model.js b/lib/db/models/user/model.js new file mode 100644 index 0000000000..6513dc154c --- /dev/null +++ b/lib/db/models/user/model.js @@ -0,0 +1,446 @@ +/* * + * Copyright 2025 contains code contributed by V Kontakte LLC - Licensed under the Apache license 2.0 + * */ +import db from '../../index.js' +import * as apiutil from '../../../util/apiutil.js' +import logger from '../../../util/logger.js' + +import AllModel from '../all/index.js' +import GroupModel from '../group/index.js' +import wireutil from '../../../wire/util.js' +import UserChangeHandler from '../../handlers/user/index.js' + +const log = logger.createLogger('dbapi:user') + +/* +=========================================================== +==================== without change DB ==================== +=========================================================== +*/ + +// dbapi.getUsers = function() { +export const getUsers = function() { + return db.users.find().toArray() +} + +// dbapi.loadUser = function(email) { +export const loadUser = function(email) { + return db.users.findOne({email: email}) +} + +// dbapi.lookupUsersByAdbKey = function(fingerprint) { +export const lookupUsersByAdbKey = function(fingerprint) { + return db.users.find({ + adbKeys: fingerprint + }).toArray() +} + +// dbapi.lookupUserByAdbFingerprint = function(fingerprint) { +export const lookupUserByAdbFingerprint = function(fingerprint) { + return db.users.find( + {adbKeys: {$elemMatch: {fingerprint: fingerprint}}} + // @ts-ignore + , {email: 1, name: 1, group: 1, _id: 0} + ).toArray() + .then(function(users) { + switch (users.length) { + case 1: + return users[0] + case 0: + return null + default: + throw new Error('Found multiple users for same ADB fingerprint') + } + }) +} + +// dbapi.getEmails = function() { +export const getEmails = function() { + return db.users + .find({ + privilege: { + $ne: apiutil.ADMIN + } + }) + .project({email: 1, _id: 0}) + .toArray() +} + +// dbapi.getAdmins = function() { +export const getAdmins = function() { + return db.users + .find({ + privilege: apiutil.ADMIN + }) + .project({email: 1, _id: 0}) + .toArray() +} + +/* +==================================================================== +==================== changing DB - use handlers ==================== +==================================================================== +*/ + +// dbapi.createUser = function(email, name, ip) { +export const createUser = async(email, name, ip, privilege) => { + const rootGroup = await GroupModel.getRootGroup() + const adminUser = await loadUser(rootGroup?.owner.email) + let userObj = { + email: email, + name: name, + ip: ip, + group: wireutil.makePrivateChannel(), + lastLoggedInAt: AllModel.getNow(), + createdAt: AllModel.getNow(), + forwards: [], + settings: {}, + acceptedPolicy: false, + privilege: privilege || (adminUser ? apiutil.USER : apiutil.ADMIN), + groups: { + subscribed: [], + lock: false, + quotas: { + allocated: { + number: adminUser ? adminUser.groups.quotas.defaultGroupsNumber : rootGroup?.envUserGroupsNumber, + duration: adminUser ? adminUser.groups.quotas.defaultGroupsDuration : rootGroup?.envUserGroupsDuration + }, + consumed: { + number: 0, + duration: 0 + }, + defaultGroupsNumber: adminUser ? 0 : rootGroup?.envUserGroupsNumber, + defaultGroupsDuration: adminUser ? 0 : rootGroup?.envUserGroupsDuration, + defaultGroupsRepetitions: adminUser ? 0 : rootGroup?.envUserGroupsRepetitions, + repetitions: adminUser ? adminUser.groups.quotas.defaultGroupsRepetitions : rootGroup?.envUserGroupsRepetitions + } + } + } + const stats = await db.users.insertOne(userObj) + if (stats.insertedId) { + UserChangeHandler.sendUserChange(userObj, false, [], 'created', ['settings']) + + await GroupModel.addGroupUser(rootGroup?.id, email) + const newUser = await loadUser(email) + + // @ts-ignore + stats.changes = [ + {new_val: {...newUser}} + ] + } + return stats +} + + +// dbapi.saveUserAfterLogin = function(user) { +export const saveUserAfterLogin = function(user) { + const updateData = { + name: user?.name, + ip: user?.ip, + lastLoggedInAt: AllModel.getNow() + } + + if (user?.privilege) { + updateData.privilege = user?.privilege + } + + return db.users.updateOne({email: user?.email}, {$set: updateData}) + // @ts-ignore + .then(stats => { + if (stats.modifiedCount === 0) { + return createUser(user?.email, user?.name, user?.ip, user?.privilege) + } + return stats + }) +} + +// dbapi.updateUsersAlertMessage = function(alertMessage) { +export const updateUsersAlertMessage = async function(alertMessage) { + const oldUser = await db.users.findOne({email: apiutil.STF_ADMIN_EMAIL}) + const newUser = await db.users.findOneAndUpdate( + { + email: apiutil.STF_ADMIN_EMAIL + } + , { + $set: Object.fromEntries(Object.entries(alertMessage).map(([key, value]) => + ['settings.alertMessage.' + key, value] + )), + }, + {returnDocument: 'after'} + ) + + var userModifided = false + const userMatching = oldUser !== null + if (newUser) { + userModifided = UserChangeHandler.updateUserHandler(oldUser, newUser) + } + return {modifiedCount: userModifided ? 1 : 0, matchedCount: userMatching ? 1 : 0, newUser: newUser} +} + + +// dbapi.updateUserSettings = function(email, changes) { +export const updateUserSettings = async function(email, changes) { + const oldUser = await db.users.findOne({email: email}) + const newUser = await db.users.findOneAndUpdate( + { + email: email + } + , { + $set: { + settings: {...oldUser?.settings, ...changes} + } + }, + {returnDocument: 'after'} + ) + UserChangeHandler.updateUserHandler(oldUser, newUser) +} + +// dbapi.insertUserAdbKey = function(email, key) { +export const insertUserAdbKey = function(email, key) { + let data = { + title: key.title, + fingerprint: key.fingerprint + } + return db.users.findOne({email: email}).then(user => { + let adbKeys = user?.adbKeys ? user?.adbKeys : [] + adbKeys.push(data) + return db.users.updateOne( + {email: email} + , {$set: {adbKeys: user?.adbKeys ? adbKeys : [data]}} + ) + }) +} + +// dbapi.grantAdmin = function(email) { +export const grantAdmin = function(email) { + return db.users.findOneAndUpdate({email: email}, { + $set: { + privilege: apiutil.ADMIN + } + }, {returnDocument: 'after'}) +} + +// dbapi.revokeAdmin = function(email) { +export const revokeAdmin = function(email) { + return db.users.findOneAndUpdate({email: email}, { + $set: { + privilege: apiutil.USER + } + }, {returnDocument: 'after'}) +} + +// dbapi.acceptPolicy = function(email) { +export const acceptPolicy = function(email) { + return db.users.updateOne({email: email}, { + $set: { + acceptedPolicy: true + } + }) +} + +export const reserveUserGroupInstance = async(email) => { + const oldUser = await loadUser(email) + const newUser = await db.users.findOneAndUpdate( + {email} + , [{ + $set: {'groups.quotas.consumed.number': { + $min: [{ + $sum: ['$groups.quotas.consumed.number', 1] + }, '$groups.quotas.allocated.number']} + } + }], + {returnDocument: 'after'} + ) + const userModifided = UserChangeHandler.updateUserHandler(oldUser, newUser) + return {modifiedCount: userModifided ? 1 : 0} +} + +export const releaseUserGroupInstance = async(email) => { + const oldUser = await loadUser(email) + const newUser = await db.users.findOneAndUpdate( + { + email + } + , [{ + $set: {'groups.quotas.consumed.number': { + $max: [{ + $sum: ['$groups.quotas.consumed.number', -1] + }, 0]} + } + }], + {returnDocument: 'after'} + ) + const userModifided = UserChangeHandler.updateUserHandler(oldUser, newUser) + return {modifiedCount: userModifided ? 1 : 0} +} + +export const updateUserGroupDuration = async(email, oldDuration, newDuration) => { + const oldUser = await loadUser(email) + const newUser = await db.users.findOneAndUpdate( + {email: email} + , [{ + $set: { + 'groups.quotas.consumed.duration': { + $cond: [ + {$lte: [{$sum: ['$groups.quotas.consumed.duration', newDuration, -oldDuration]}, '$groups.quotas.allocated.duration']}, + {$sum: ['$groups.quotas.consumed.duration', newDuration, -oldDuration]}, + '$groups.quotas.consumed.duration' + ] + } + } + }], + {returnDocument: 'after'} + ) + var userModifided = false + const userMatching = oldUser !== null + if (newUser) { + userModifided = UserChangeHandler.updateUserHandler(oldUser, newUser) + } + return {modifiedCount: userModifided ? 1 : 0, matchedCount: userMatching ? 1 : 0} +} + +export const updateUserGroupsQuotas = async(email, duration, number, repetitions) => { + const oldUser = await db.users.findOne({email: email}) + + const consumed = oldUser?.groups.quotas.consumed.duration + const allocated = oldUser?.groups.quotas.allocated.duration + const consumedNumber = oldUser?.groups.quotas.consumed.number + const allocatedNumber = oldUser?.groups.quotas.allocated.number + + const newUser = await db.users.findOneAndUpdate( + {email: email} + , { + $set: { + 'groups.quotas.allocated.duration': duration && consumed <= duration && + (!number || consumedNumber <= number) ? duration : allocated, + 'groups.quotas.allocated.number': number && consumedNumber <= number && + (!duration || consumed <= duration) ? number : allocatedNumber, + 'groups.quotas.repetitions': repetitions || oldUser?.groups.quotas.repetitions + } + }, + {returnDocument: 'after'} + ) + + var userModifided = false + if (newUser) { + userModifided = UserChangeHandler.updateUserHandler(oldUser, newUser) + } + + return {modifiedCount: userModifided ? 1 : 0, newUser: newUser} +} + +export const updateDefaultUserGroupsQuotas = async(email, duration, number, repetitions) => { + const oldUser = await db.users.findOne({email: email}) + const newUser = await db.users.findOneAndUpdate( + {email: email} + , [{ + $set: { + 'groups.quotas.defaultGroupsDuration': { + $cond: [ + { + $ne: [duration, null] + }, + duration, + '$groups.quotas.defaultGroupsDuration' + ] + }, + 'groups.quotas.defaultGroupsNumber': { + $cond: [ + { + $ne: [number, null] + }, + number, + '$groups.quotas.defaultGroupsNumber' + ] + }, + 'groups.quotas.defaultGroupsRepetitions': { + $cond: [ + { + $ne: [repetitions, null] + }, + repetitions, + '$groups.quotas.defaultGroupsRepetitions' + ] + } + } + }], + {returnDocument: 'after'} + ) + var userModifided = false + if (newUser) { + userModifided = UserChangeHandler.updateUserHandler(oldUser, newUser) + } + return {modifiedCount: userModifided ? 1 : 0, newUser: newUser} +} + +// dbapi.deleteUserAdbKey = function(email, fingerprint) { +export const deleteUserAdbKey = function(email, fingerprint) { + return db.users.findOne({email: email}).then(user => { + return db.users.updateOne( + {email: email} + , { + $set: { + adbKeys: user?.adbKeys ? user?.adbKeys.filter(key => { + return key.fingerprint !== fingerprint + }) : [] + } + } + ) + }) +} + +// dbapi.resetUserSettings = function(email) { +export const resetUserSettings = function(email) { + return db.users.updateOne({email: email}, + { + $set: { + settings: {} + } + }) +} + +export const deleteUser = async function(email) { + const deletedUser = await db.users.findOneAndDelete({email: email}) + UserChangeHandler.sendUserChange(deletedUser, false, [], 'deleted', ['settings']) + return {deletedCount: deletedUser !== null ? 1 : 0} +} + + +/* +==================================================== +==================== deprecated ==================== +==================================================== +*/ + +/** + * @deprecated Do not use locks in database. + */ +export const setLockOnUser = function(email, state) { + return db.users.findOne({email: email}).then(oldDoc => { + if (!oldDoc || !oldDoc.groups) { + throw new Error(`User with email ${email} not found or groups field is missing.`) + } + return db.users.updateOne( + {email: email}, + { + $set: { + 'groups.lock': oldDoc.groups.lock !== state ? state : oldDoc.groups.lock + } + } + ) + .then(updateStats => { + return db.users.findOne({email: email}).then(newDoc => { + // @ts-ignore + updateStats.changes = [ + {new_val: {...newDoc}, old_val: {...oldDoc}} + ] + return updateStats + }) + }) + }) +} + +export const getUserAdbKeys = function(email) { + return db.users.findOne({email: email}) + .then(user => user?.adbKeys || []) +} diff --git a/lib/units/api/controllers/groups.js b/lib/units/api/controllers/groups.js index b39620ac78..c15b25db35 100644 --- a/lib/units/api/controllers/groups.js +++ b/lib/units/api/controllers/groups.js @@ -11,7 +11,7 @@ import {v4 as uuidv4} from 'uuid' import usersapi from './users.js' import lockutil from '../../../util/lockutil.js' import {isOriginGroup} from '../../../util/apiutil.js' -import {loadDevicesBySerials} from '../../../db/models/all/model.js' +import DeviceModel from '../../../db/models/device/index.js' const log = logger.createLogger('groups-controller:') /* ---------------------------------- PRIVATE FUNCTIONS --------------------------------- */ @@ -168,7 +168,7 @@ function addGroupDevices(req, res) { } if (isInternal) { - return loadDevicesBySerials(autotestsGroup?.devices) + return DeviceModel.loadDevicesBySerials(autotestsGroup?.devices) .then(devices => apiutil.respond(res, 200, `Added (group ${target})`, { group: { id: autotestsGroup?.id, @@ -824,7 +824,7 @@ function updateGroup(req, res) { .then(function(group) { if (group && typeof group !== 'boolean') { if (isInternal) { - loadDevicesBySerials(group.devices) + DeviceModel.loadDevicesBySerials(group.devices) .then(devices => apiutil.respond(res, 200, 'Updated (group)', {group: {id: group.id, devices: devices}})) } else { @@ -909,7 +909,7 @@ function updateGroup(req, res) { repetitions === group.repetitions ) { if (isInternal) { - return loadDevicesBySerials(group.devices) + return DeviceModel.loadDevicesBySerials(group.devices) .then(devices => apiutil.respond(res, 200, 'Unchanged (group)', {group: {id: group.id, devices: devices}})) } } @@ -999,7 +999,7 @@ function getGroupDevices(req, res) { }) } else { - loadDevicesBySerials(group.devices) + DeviceModel.loadDevicesBySerials(group.devices) .then(function(devices) { devices = devices.map(device => apiutil.filterDevice(req, device)) apiutil.respond(res, 200, 'Devices Information', {devices: devices}) diff --git a/lib/units/api/controllers/users.js b/lib/units/api/controllers/users.js index 7eff6be078..cbb3cecbc6 100644 --- a/lib/units/api/controllers/users.js +++ b/lib/units/api/controllers/users.js @@ -1,4 +1,3 @@ -import dbapi from '../../../db/api.js' import _ from 'lodash' import * as apiutil from '../../../util/apiutil.js' import * as lockutil from '../../../util/lockutil.js' @@ -8,11 +7,14 @@ import wireutil from '../../../wire/util.js' import userapi from './user.js' import * as service from '../../../util/serviceuser.js' import {MongoServerError} from 'mongodb' +import AllModel from '../../../db/models/all/index.js' +import GroupModel from '../../../db/models/group/index.js' +import UserModel from '../../../db/models/user/index.js' /* --------------------------------- PRIVATE FUNCTIONS --------------------------------------- */ function userApiWrapper(fn, req, res) { const email = req.params.email - dbapi.loadUser(email).then(function(user) { + UserModel.loadUser(email).then(function(user) { if (!user) { apiutil.respond(res, 404, 'Not Found (user)') } @@ -39,24 +41,24 @@ async function removeUser(email, req, res) { const groupOwnerState = req.query.groupOwner const anyGroupOwnerState = typeof groupOwnerState === 'undefined' const lock = {} - const user = await dbapi.loadUser(email) + const user = await UserModel.loadUser(email) if (!user) { return 'not found' } async function removeGroupUser(owner, id) { - const userGroup = await dbapi.getUserGroup(owner, id) + const userGroup = await GroupModel.getUserGroup(owner, id) if (!userGroup) { return 'not found' } return owner === email ? - dbapi.deleteUserGroup(id) : - dbapi.removeGroupUser(id, email) + GroupModel.deleteUserGroup(id) : + GroupModel.removeGroupUser(id, email) } function deleteUserInDatabase(channel) { - return dbapi.removeUserAccessTokens(email).then(function() { - return dbapi.deleteUser(email).then(function() { + return AllModel.removeUserAccessTokens(email).then(function() { + return UserModel.deleteUser(email).then(function() { req.options.pushdev.send([ channel, wireutil.envelope(new wire.DeleteUserMessage(email)) @@ -90,12 +92,12 @@ async function removeUser(email, req, res) { if (req.user.email === email) { return Promise.resolve('forbidden') } - return dbapi.lockUser(email).then(function(stats) { + return AllModel.lockUser(email).then(function(stats) { if (stats.modifiedCount === 0) { return apiutil.lightComputeStats(res, stats) } const user = lock.user = stats.changes[0].new_val - return dbapi.getGroupsByUser(user.email).then(function(groups) { + return GroupModel.getGroupsByUser(user.email).then(function(groups) { return computeUserGroupOwnership(groups).then(function(doContinue) { if (!doContinue) { return 'unchanged' @@ -117,7 +119,7 @@ function grantAdmin(req, res) { if (req.user.privilege !== apiutil.ADMIN) { return apiutil.respond(res, 403, 'Forbidden (user doesnt have admin privilege)') } - dbapi.grantAdmin(req.params.email).then((user) => { + UserModel.grantAdmin(req.params.email).then((user) => { if (user) { // @ts-ignore return apiutil.respond(res, 200, 'Grant admin for user', {user: user}) @@ -131,7 +133,7 @@ function revokeAdmin(req, res) { if (req.user.privilege !== apiutil.ADMIN) { return apiutil.respond(res, 403, 'Forbidden (user doesnt have admin privilege)') } - dbapi.revokeAdmin(req.params.email).then((user) => { + UserModel.revokeAdmin(req.params.email).then((user) => { if (user) { // @ts-ignore return apiutil.respond(res, 200, 'Revoke admin for user', {user: user}) @@ -142,7 +144,7 @@ function revokeAdmin(req, res) { }) } function lockStfAdminUser(res) { - return dbapi.lockUser(apiutil.STF_ADMIN_EMAIL).then(function(stats) { + return AllModel.lockUser(apiutil.STF_ADMIN_EMAIL).then(function(stats) { if (stats.modifiedCount === 0) { return apiutil.lightComputeStats(res, stats) } @@ -154,12 +156,12 @@ function updateUsersAlertMessage(req, res) { return apiutil.respond(res, 403, 'Forbidden (user doesnt have admin privilege)') } const lock = lockStfAdminUser(res) - return dbapi.updateUsersAlertMessage(req.body).then(function(/** @type {any} */ stats) { + return UserModel.updateUsersAlertMessage(req.body).then(function(/** @type {any} */ stats) { if (stats.matchedCount > 0 && stats.modifiedCount === 0) { - apiutil.respond(res, 200, 'Unchanged (users alert message)', {alertMessage: stats.changes[0].new_val.settings.alertMessage}) + apiutil.respond(res, 200, 'Unchanged (users alert message)', {alertMessage: stats.newUser.settings.alertMessage}) } else { - apiutil.respond(res, 200, 'Updated (users alert message)', {alertMessage: stats.changes[0].new_val.settings.alertMessage}) + apiutil.respond(res, 200, 'Updated (users alert message)', {alertMessage: stats.newUser.settings.alertMessage}) } }) .catch(function(err) { @@ -178,9 +180,9 @@ function updateUsersAlertMessage(req, res) { /* --------------------------------- PUBLIC FUNCTIONS --------------------------------------- */ function getUserInfo(req, email) { const fields = req.query.fields - return dbapi.loadUser(email).then(function(user) { + return UserModel.loadUser(email).then(function(user) { if (user) { - return dbapi.getRootGroup().then(function(group) { + return GroupModel.getRootGroup().then(function(group) { return getPublishedUser(user, req.user.email, group?.owner?.email, fields) }) } @@ -189,7 +191,7 @@ function getUserInfo(req, email) { } function getUsersAlertMessage(req, res) { const fields = req.query.fields - return dbapi.loadUser(apiutil.STF_ADMIN_EMAIL).then(async function(user) { + return UserModel.loadUser(apiutil.STF_ADMIN_EMAIL).then(async function(user) { if (user?.settings?.alertMessage === undefined) { const lock = await lockStfAdminUser(res) const alertMessage = { @@ -197,9 +199,9 @@ function getUsersAlertMessage(req, res) { data: '*** this site is currently under maintenance, please wait ***', level: 'Critical' } - return dbapi.updateUsersAlertMessage(alertMessage).then(function(/** @type {any} */ stats) { - if (!stats.errors) { - return stats.changes[0].new_val.settings.alertMessage + return UserModel.updateUsersAlertMessage(alertMessage).then(function(/** @type {any} */ stats) { + if (stats.modifiedCount === 1) { + return stats.newUser.settings.alertMessage } throw new Error('Failed to initialize users alert message') }) @@ -239,15 +241,15 @@ function updateUserGroupsQuotas(req, res) { null const lock = {} - dbapi.loadUser(email).then(function(user) { + UserModel.loadUser(email).then(function(user) { if (user) { lockutil.lockUser(email, res, lock).then(function(lockingSuccessed) { if (lockingSuccessed) { - return dbapi.updateUserGroupsQuotas(email, duration, number, repetitions) + return UserModel.updateUserGroupsQuotas(email, duration, number, repetitions) .then(function(/** @type {any} */ stats) { if (stats.modifiedCount > 0) { return apiutil.respond(res, 200, 'Updated (user quotas)', { - user: apiutil.publishUser(stats.changes[0].new_val) + user: apiutil.publishUser(stats.newUser) }) } if ((duration === null || duration === lock.user.groups.quotas.allocated.duration) && @@ -285,11 +287,11 @@ function updateDefaultUserGroupsQuotas(req, res) { const lock = {} lockutil.lockUser(req.user.email, res, lock).then(function(lockingSuccessed) { if (lockingSuccessed) { - return dbapi.updateDefaultUserGroupsQuotas(req.user.email, duration, number, repetitions) + return UserModel.updateDefaultUserGroupsQuotas(req.user.email, duration, number, repetitions) .then(function(/** @type {any} */ stats) { if (stats.modifiedCount > 0) { return apiutil.respond(res, 200, 'Updated (user default quotas)', { - user: apiutil.publishUser(stats) + user: apiutil.publishUser(stats.newUser) }) } return apiutil.respond(res, 200, 'Unchanged (user default quotas)', {user: {}}) @@ -320,8 +322,8 @@ function getUserByEmail(req, res) { } function getUsers(req, res) { const fields = req.query.fields - dbapi.getUsers().then(function(users) { - return dbapi.getRootGroup().then(function(group) { + UserModel.getUsers().then(function(users) { + return GroupModel.getRootGroup().then(function(group) { apiutil.respond(res, 200, 'Users Information', { users: users.map(function(user) { return getPublishedUser(user, req.user.email, group?.owner?.email, fields) @@ -336,7 +338,7 @@ function getUsers(req, res) { function createUser(req, res) { const email = req.params.email const name = req.query.name - dbapi.createUser(email, name, req.user.ip).then(function(/** @type {any} */ stats) { + UserModel.createUser(email, name, req.user.ip).then(function(/** @type {any} */ stats) { apiutil.respond(res, 201, 'Created (user)', { user: apiutil.publishUser(stats.changes[0].new_val) }) @@ -405,7 +407,7 @@ function deleteUsers(req, res) { } (function() { if (typeof emails === 'undefined') { - return dbapi.getEmails().then(function(emails) { + return UserModel.getEmails().then(function(emails) { return removeUsers(emails) }) } diff --git a/lib/units/groups-engine/index.js b/lib/units/groups-engine/index.js index ee3d1f877c..7ccb596dd6 100644 --- a/lib/units/groups-engine/index.js +++ b/lib/units/groups-engine/index.js @@ -1,6 +1,5 @@ import devicesWatcher from './watchers/devices.js' import lifecycle from '../../util/lifecycle.js' -import usersWatcher from './watchers/users.js' import logger from '../../util/logger.js' import db from '../../db/index.js' @@ -15,7 +14,6 @@ export default (async function(options) { await db.connect() devicesWatcher(push, pushdev, channelRouter) - usersWatcher(pushdev) lifecycle.observe(() => [push, pushdev].forEach((sock) => sock.close()) diff --git a/lib/units/groups-engine/watchers/users.js b/lib/units/groups-engine/watchers/users.js deleted file mode 100644 index 6ae21ffc59..0000000000 --- a/lib/units/groups-engine/watchers/users.js +++ /dev/null @@ -1,105 +0,0 @@ -import timeutil from '../../../util/timeutil.js' -import _ from 'lodash' -import logger from '../../../util/logger.js' -import wireutil from '../../../wire/util.js' -import wire from '../../../wire/index.js' -import db from '../../../db/index.js' -export default (function(pushdev) { - const log = logger.createLogger('watcher-users') - function sendUserChange(user, isAddedGroup, groups, action, targets) { - pushdev.send([ - wireutil.global, - wireutil.envelope(new wire.UserChangeMessage(user, isAddedGroup, groups, action, targets, timeutil.now('nano'))) - ]) - } - let changeStream - db.connect().then(client => { - const users = client.collection('users') - changeStream = users.watch([ - { - $project: { - 'fullDocument.email': 1, - 'fullDocument.name': 1, - 'fullDocument.privilege': 1, - 'fullDocument.groups.quotas': 1, - 'fullDocument.groups.subscribed': 1, - 'fullDocument.settings.alertMessage': 1, - 'fullDocumentBeforeChange.email': 1, - 'fullDocumentBeforeChange.name': 1, - 'fullDocumentBeforeChange.privilege': 1, - 'fullDocumentBeforeChange.groups.quotas': 1, - 'fullDocumentBeforeChange.groups.subscribed': 1, - 'fullDocumentBeforeChange.settings.alertMessage': 1, - operationType: 1 - } - } - ], {fullDocument: 'whenAvailable', fullDocumentBeforeChange: 'whenAvailable'}) - changeStream.on('change', next => { - log.info('Users watcher next: ' + JSON.stringify(next)) - try { - let newDoc, oldDoc - let operationType = next.operationType - // @ts-ignore - if (next.fullDocument) { - // @ts-ignore - newDoc = next.fullDocument - } - else { - newDoc = null - } - // @ts-ignore - if (next.fullDocumentBeforeChange) { - // @ts-ignore - oldDoc = next.fullDocumentBeforeChange - } - else { - oldDoc = null - } - if (newDoc === null && oldDoc === null) { - log.info('New user doc and old user doc is NULL') - return false - } - if (operationType === 'insert') { - sendUserChange(newDoc, false, [], 'created', ['settings']) - } - else if (operationType === 'delete') { - sendUserChange(oldDoc, false, [], 'deleted', ['settings']) - } - else { - const targets = [] - if (newDoc.groups && oldDoc.groups) { - if (newDoc.groups.quotas && oldDoc.groups.quotas) { - if (!_.isEqual(newDoc.groups.quotas.allocated, oldDoc.groups.quotas.allocated)) { - targets.push('settings') - targets.push('view') - } - else if (!_.isEqual(newDoc.groups.quotas.consumed, oldDoc.groups.quotas.consumed)) { - targets.push('view') - } - else if (newDoc.groups.quotas.defaultGroupsNumber !== - oldDoc.groups.quotas.defaultGroupsNumber || - newDoc.groups.defaultGroupsDuration !== - oldDoc.groups.quotas.defaultGroupsDuration || - newDoc.groups.defaultGroupsRepetitions !== - oldDoc.groups.quotas.defaultGroupsRepetitions || - newDoc.groups.repetitions !== - oldDoc.groups.quotas.repetitions || - !_.isEqual(newDoc.groups.subscribed, oldDoc.groups.subscribed)) { - targets.push('settings') - } - } - } - else if (!_.isEqual(newDoc.settings.alertMessage, oldDoc.settings.alertMessage)) { - targets.push('menu') - } - if (targets.length) { - sendUserChange(newDoc, newDoc.groups.subscribed.length > oldDoc.groups.subscribed.length, _.xor(newDoc.groups.subscribed, oldDoc.groups.subscribed), 'updated', targets) - } - } - } - catch (e) { - log.error(e) - } - }) - }) -}) diff --git a/lib/units/processor/index.ts b/lib/units/processor/index.ts index 266594b347..0490196b0d 100644 --- a/lib/units/processor/index.ts +++ b/lib/units/processor/index.ts @@ -8,6 +8,7 @@ import dbapi from '../../db/models/all/index.js' import lifecycle from '../../util/lifecycle.js' import srv from '../../util/srv.js' import * as zmqutil from '../../util/zmqutil.js' +import UserModel from '../../db/models/user/index.js' import { UserChangeMessage, GroupChangeMessage, @@ -162,7 +163,7 @@ export default db.ensureConnectivity(async(options: Options) => { }) .on(JoinGroupByAdbFingerprintMessage, async (channel, message) => { try { - const user = await dbapi.lookupUserByAdbFingerprint(message.fingerprint) + const user = await UserModel.lookupUserByAdbFingerprint(message.fingerprint) if (user) { devDealer.send([ channel, diff --git a/lib/units/websocket/index.js b/lib/units/websocket/index.js index 07e08e8c22..6f59f52e4c 100644 --- a/lib/units/websocket/index.js +++ b/lib/units/websocket/index.js @@ -13,7 +13,6 @@ import logger from '../../util/logger.js' import wire from '../../wire/index.js' import wireutil from '../../wire/util.js' import {WireRouter} from '../../wire/router.js' -import dbapi from '../../db/api.js' import datautil from '../../util/datautil.js' import lifecycle from '../../util/lifecycle.js' import cookieSession from './middleware/cookie-session.js' @@ -26,6 +25,8 @@ import db from '../../db/index.js' import EventEmitter from 'events' import generateToken from '../api/helpers/generateToken.js' import {UpdateAccessTokenMessage, DeleteUserMessage, DeviceChangeMessage, UserChangeMessage, GroupChangeMessage, DeviceGroupChangeMessage, GroupUserChangeMessage, DeviceLogMessage, DeviceIntroductionMessage, DeviceReadyMessage, DevicePresentMessage, DeviceAbsentMessage, InstalledApplications, JoinGroupMessage, JoinGroupByAdbFingerprintMessage, LeaveGroupMessage, DeviceStatusMessage, DeviceIdentityMessage, TransactionProgressMessage, TransactionDoneMessage, TransactionTreeMessage, DeviceLogcatEntryMessage, AirplaneModeEvent, BatteryEvent, GetServicesAvailabilityMessage, DeviceBrowserMessage, ConnectivityEvent, PhoneStateEvent, RotationEvent, CapabilitiesMessage, ReverseForwardsEvent, TemporarilyUnavailableMessage, UpdateRemoteConnectUrl} from '../../wire/wire.js' +import AllModel from '../../db/models/all/index.js' +import UserModel from '../../db/models/user/index.js' const request = Promise.promisifyAll(postmanRequest) export default (async function(options) { const log = logger.createLogger('websocket') @@ -245,7 +246,7 @@ export default (async function(options) { }) // @TODO refactore JoimGroupMessage route .on(JoinGroupMessage, function(channel, message) { - dbapi.getInstalledApplications({serial: message.serial}) + AllModel.getInstalledApplications({serial: message.serial}) .then(applications => { if (!user?.ownedChannels) { user.ownedChannels = new Set() @@ -448,10 +449,10 @@ export default (async function(options) { // // Device note .on('device.note', function(data) { - return dbapi + return AllModel .setDeviceNote(data.serial, data.note) .then(function() { - return dbapi.loadDevice(user.groups.subscribed, data.serial) + return AllModel.loadDevice(user.groups.subscribed, data.serial) }) .then(function(device) { if (device) { @@ -470,19 +471,19 @@ export default (async function(options) { // Settings .on('user.settings.update', function(data) { if (data.alertMessage === undefined) { - dbapi.updateUserSettings(user.email, data) + UserModel.updateUserSettings(user.email, data) } else { - dbapi.updateUserSettings(apiutil.STF_ADMIN_EMAIL, data) + UserModel.updateUserSettings(apiutil.STF_ADMIN_EMAIL, data) } }) .on('user.settings.reset', function() { - dbapi.resetUserSettings(user.email) + UserModel.resetUserSettings(user.email) }) .on('user.keys.accessToken.generate', async(data) => { const {title} = data const token = generateToken(user, options.secret) - await dbapi + await AllModel .saveUserAccessToken(user.email, { title: title, id: token.id, @@ -496,7 +497,7 @@ export default (async function(options) { .on('user.keys.accessToken.remove', function(data) { const isAdmin = user.privilege === apiutil.ADMIN const email = (isAdmin ? data.email : null) || user.email - return dbapi + return AllModel .removeUserAccessToken(email, data.title) .then(function() { socket.emit('user.keys.accessToken.updated') @@ -506,16 +507,16 @@ export default (async function(options) { // @ts-ignore return Adb.util.parsePublicKey(data.key) .then(function(key) { - return dbapi.lookupUsersByAdbKey(key.fingerprint) + return UserModel.lookupUsersByAdbKey(key.fingerprint) .then(function(keys) { return keys }) .then(function(users) { if (users.length) { - throw new dbapi.DuplicateSecondaryIndexError() + throw new AllModel.DuplicateSecondaryIndexError() } else { - return dbapi.insertUserAdbKey(user.email, { + return UserModel.insertUserAdbKey(user.email, { title: data.title, fingerprint: key.fingerprint }) @@ -541,16 +542,16 @@ export default (async function(options) { }) }) .on('user.keys.adb.accept', function(data) { - return dbapi.lookupUsersByAdbKey(data.fingerprint) + return UserModel.lookupUsersByAdbKey(data.fingerprint) .then(function(keys) { return keys }) .then(function(users) { if (users.length) { - throw new dbapi.DuplicateSecondaryIndexError() + throw new AllModel.DuplicateSecondaryIndexError() } else { - return dbapi.insertUserAdbKey(user.email, { + return UserModel.insertUserAdbKey(user.email, { title: data.title, fingerprint: data.fingerprint }) @@ -569,12 +570,12 @@ export default (async function(options) { ]) }) // @ts-ignore - .catch(dbapi.DuplicateSecondaryIndexError, function() { + .catch(AllModel.DuplicateSecondaryIndexError, function() { // No-op }) }) .on('user.keys.adb.remove', function(data) { - return dbapi + return UserModel .deleteUserAdbKey(user.email, data.fingerprint) .then(function() { socket.emit('user.keys.adb.removed', data) @@ -582,7 +583,7 @@ export default (async function(options) { }) .on('shell.settings.execute', function(data) { let command = data.command - dbapi.loadDevices().then(devices => { + AllModel.loadDevices().then(devices => { devices.forEach(device => { push.send([ device.channel, @@ -901,7 +902,7 @@ export default (async function(options) { }) .on('group.invite', async(channel, responseChannel, data) => { joinChannel(responseChannel) - const keys = await dbapi.getUserAdbKeys(user.email) + const keys = await UserModel.getUserAdbKeys(user.email) push.send([ channel, wireutil.transaction(responseChannel, new wire.GroupMessage( @@ -1342,7 +1343,7 @@ export default (async function(options) { ]) }) .on('policy.accept', function(data) { - dbapi.acceptPolicy(user.email) + UserModel.acceptPolicy(user.email) }) }) .finally(function() { From c94a61923b36f6e1809abcd34020d5d962ee4f4a Mon Sep 17 00:00:00 2001 From: Elmir Khalilov <52529096+e-khalilov@users.noreply.github.com> Date: Thu, 2 Oct 2025 11:53:48 +0300 Subject: [PATCH 07/23] fix heartbeat (#377) Co-authored-by: e.khalilov --- lib/units/processor/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/units/processor/index.ts b/lib/units/processor/index.ts index 0490196b0d..e98df9ab83 100644 --- a/lib/units/processor/index.ts +++ b/lib/units/processor/index.ts @@ -123,7 +123,6 @@ export default db.ensureConnectivity(async(options: Options) => { .on(GroupChangeMessage, defaultWireHandler) .on(DeviceGroupChangeMessage, defaultWireHandler) .on(GroupUserChangeMessage, defaultWireHandler) - .on(DeviceHeartbeatMessage, defaultWireHandler) .on(DeviceLogMessage, defaultWireHandler) .on(TransactionProgressMessage, defaultWireHandler) .on(TransactionDoneMessage, defaultWireHandler) @@ -297,6 +296,9 @@ export default db.ensureConnectivity(async(options: Options) => { reply.okay('success', {devices}) ]) }) + .on(DeviceHeartbeatMessage, (channel, message, data) => { + devDealer.send([ channel, data ]) + }) .handler(); devDealer.on('message', router) From 649b907e877d3c96606e9fa04db06e363a03fb01 Mon Sep 17 00:00:00 2001 From: Elmir Khalilov <52529096+e-khalilov@users.noreply.github.com> Date: Fri, 3 Oct 2025 12:38:36 +0300 Subject: [PATCH 08/23] overrides dependencies (#378) Co-authored-by: e.khalilov --- package-lock.json | 54 ++++++++++++++++++++++++++++++-------------- package.json | 5 ++-- ui/package-lock.json | 24 +++++--------------- ui/package.json | 3 +++ 4 files changed, 49 insertions(+), 37 deletions(-) diff --git a/package-lock.json b/package-lock.json index 95d72eab76..6139152d62 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5492,12 +5492,43 @@ "yauzl": "^2.7.0" } }, + "node_modules/appium-support/node_modules/axios": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz", + "integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/appium-support/node_modules/bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", "license": "MIT" }, + "node_modules/appium-support/node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/appium-support/node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -5734,17 +5765,6 @@ "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", "license": "MIT" }, - "node_modules/axios": { - "version": "0.30.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.30.0.tgz", - "integrity": "sha512-Z4F3LjCgfjZz8BMYalWdMgAQUnEtKDmpwNHjh/C8pQZWde32TF64cqnSeyL3xD/aTIASRU30RHTNzRiV/NpGMg==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.4", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, "node_modules/backoff": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz", @@ -6565,9 +6585,9 @@ } }, "node_modules/cmake-js/node_modules/axios": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", - "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz", + "integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -6576,9 +6596,9 @@ } }, "node_modules/cmake-js/node_modules/follow-redirects": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.10.tgz", - "integrity": "sha512-V7O/fFKM539IC2bweloFWuoiJ9OtI3W2uIqJPWM8IT5xxNyt73QtvVqmSpcDmk07ivmmlKB+rRY0vpQjIYNtKw==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "funding": [ { "type": "individual", diff --git a/package.json b/package.json index 257e9e1c0f..f31f526540 100644 --- a/package.json +++ b/package.json @@ -171,7 +171,8 @@ "socket.io-parser": "4.2.4", "request": "$request", "validator": "13.9.0", - "node-forge": "1.3.1" + "node-forge": "1.3.1", + "axios": "1.12.0" }, "engines": { "node": ">= 12", @@ -190,4 +191,4 @@ "pre-commit": "true" } } -} \ No newline at end of file +} diff --git a/ui/package-lock.json b/ui/package-lock.json index ab8c37f585..162c7aefc3 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -7379,17 +7379,6 @@ "react": "^15.x || ^16.x || ^17.x || ^18.x || ^19.x" } }, - "node_modules/console-feed/node_modules/linkifyjs": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-2.1.9.tgz", - "integrity": "sha512-74ivurkK6WHvHFozVaGtQWV38FzBwSTGNmJolEgFp7QgR2bl6ArUWlvT4GcHKbPe1z3nWYi+VUdDZk16zDOVug==", - "license": "MIT", - "peerDependencies": { - "jquery": ">= 1.11.0", - "react": ">= 0.14.0", - "react-dom": ">= 0.14.0" - } - }, "node_modules/console-feed/node_modules/react-inspector": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/react-inspector/-/react-inspector-5.1.1.tgz", @@ -11276,13 +11265,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/jquery": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", - "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", - "license": "MIT", - "peer": true - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -11640,6 +11622,12 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, + "node_modules/linkifyjs": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz", + "integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==", + "license": "MIT" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", diff --git a/ui/package.json b/ui/package.json index 4989f9d0da..fa9dabfd55 100644 --- a/ui/package.json +++ b/ui/package.json @@ -106,6 +106,9 @@ "vite-tsconfig-paths": "^5.0.1", "vitest": "^3.0.9" }, + "overrides": { + "linkifyjs": "4.3.2" + }, "msw": { "workerDirectory": [ "public" From 920ee10fccb66c66fe8487bfba7d96c23711e9de Mon Sep 17 00:00:00 2001 From: Elmir Khalilov <52529096+e-khalilov@users.noreply.github.com> Date: Mon, 6 Oct 2025 15:17:57 +0300 Subject: [PATCH 09/23] Automatic removal of disabled devices (#379) * clear dead devices * minor fix * revert api edits * revert ui (types) edits --------- Co-authored-by: e.khalilov --- lib/cli/local/index.js | 14 ++++- lib/cli/reaper/index.js | 12 ++++ lib/db/models/all/model.js | 14 +---- lib/db/models/device/model.js | 15 ++++- lib/db/models/group/model.js | 2 +- lib/units/processor/index.ts | 20 +++++-- lib/units/reaper/index.ts | 104 ++++++++++++++++++++++++++-------- lib/wire/wire.proto | 4 ++ lib/wire/wire.ts | 56 ++++++++++++++++++ 9 files changed, 196 insertions(+), 45 deletions(-) diff --git a/lib/cli/local/index.js b/lib/cli/local/index.js index 5acc202e36..83e57c0b94 100644 --- a/lib/cli/local/index.js +++ b/lib/cli/local/index.js @@ -302,6 +302,16 @@ export const builder = function(yargs) { type: 'boolean', default: true }) + .option('time-to-device-cleanup', { + describe: 'Time in seconds after which connected devices should be deleted. 0 - do not delete', + type: 'number', + default: 0 + }) + .option('device-cleanup-interval', { + describe: 'Interval for checking devices for cleanup in minutes', + type: 'number', + default: 120_000 + }) .epilog('Each option can be be overwritten with an environment variable ' + 'by converting the option to uppercase, replacing dashes with ' + 'underscores and prefixing it with `STF_LOCAL_` (e.g. ' + @@ -343,7 +353,9 @@ export const handler = function(argv) { [ // reaper one 'reaper', 'reaper001', '--connect-push', argv.bindDevPull, - '--connect-sub', argv.bindDevPub + '--connect-sub', argv.bindDevPub, + '--time-to-device-cleanup', argv.timeToDeviceCleanup, + '--device-cleanup-interval', argv.deviceCleanupInterval ], [ // provider 'provider', diff --git a/lib/cli/reaper/index.js b/lib/cli/reaper/index.js index 651889b528..b741bc2214 100644 --- a/lib/cli/reaper/index.js +++ b/lib/cli/reaper/index.js @@ -30,6 +30,16 @@ export const builder = function(yargs) { type: 'string', default: os.hostname() }) + .option('time-to-device-cleanup', { + describe: 'Time in minutes after which connected devices should be deleted. 0 - do not delete', + type: 'number', + default: 0 + }) + .option('device-cleanup-interval', { + describe: 'Interval for checking devices for cleanup in minutes', + type: 'number', + default: 2 + }) .epilog('Each option can be be overwritten with an environment variable ' + 'by converting the option to uppercase, replacing dashes with ' + 'underscores and prefixing it with `STF_REAPER_` (e.g. ' + @@ -39,6 +49,8 @@ export const handler = function(argv) { return reaper({ name: argv.name, heartbeatTimeout: argv.heartbeatTimeout, + timeToDeviceCleanup: argv.timeToDeviceCleanup, + deviceCleanupInterval: argv.deviceCleanupInterval, endpoints: { push: argv.connectPush, sub: argv.connectSub diff --git a/lib/db/models/all/model.js b/lib/db/models/all/model.js index 1082680ac9..0dd60610b3 100644 --- a/lib/db/models/all/model.js +++ b/lib/db/models/all/model.js @@ -9,7 +9,7 @@ import {v4 as uuidv4} from 'uuid' import * as apiutil from '../../../util/apiutil.js' import GroupModel from '../group/index.js' import UserModel from '../user/index.js' - +import DeviceModel from '../device/index.js' import logger from '../../../util/logger.js' import {getRootGroup, getGroup} from '../group/model.js' @@ -887,16 +887,6 @@ export const loadStandardDevices = function(groups, fields) { }, fields) } -// dbapi.loadPresentDevices = function() { -export const loadPresentDevices = function() { - return db.devices.find({present: true}).toArray() -} - -// dbapi.loadDeviceBySerial = function(serial) { -export const loadDeviceBySerial = function(serial) { - return findDevice({serial: serial}) -} - // dbapi.loadDevice = function(groups, serial) { export const loadDevice = function(groups, serial) { return findDevice({ @@ -1186,7 +1176,7 @@ export const setAbsentDisconnectedDevices = function() { // dbapi.getInstalledApplications = function(message) { export const getInstalledApplications = function(message) { - return loadDeviceBySerial(message.serial) + return DeviceModel.loadDeviceBySerial(message.serial) } // dbapi.setDeviceType = function(serial, type) { diff --git a/lib/db/models/device/model.js b/lib/db/models/device/model.js index ef29f27e49..b00c715014 100644 --- a/lib/db/models/device/model.js +++ b/lib/db/models/device/model.js @@ -32,6 +32,10 @@ export const loadPresentDevices = function() { return db.devices.find({present: true}).toArray() } +export const loadDeviceBySerial = function(serial) { + return db.devices.findOne({serial}) +} + // dbapi.loadDevicesBySerials = function(serials) { export const loadDevicesBySerials = function(serials) { return db.devices.find({serial: {$in: serials}}).toArray() @@ -97,6 +101,15 @@ export const getDeviceType = function(serial) { }) } +export const getDeadDevice = function(time = 60_000) { + return db.devices.find( + { + present: false, + presenceChangedAt: {$lt: new Date(Date.now() - time)} + } + ).project({serial: 1, present: 1, presenceChangedAt: 1}).toArray() +} + // dbapi.generateIndexes = function() { export const generateIndexes = function() { db.devices.createIndex({serial: -1}).then((result) => { @@ -111,8 +124,6 @@ export const generateIndexes = function() { */ - - // dbapi.deleteDevice = function(serial) { export const deleteDevice = function(serial) { return db.devices.deleteOne({serial: serial}) diff --git a/lib/db/models/group/model.js b/lib/db/models/group/model.js index a68fd84eb8..045093bc58 100644 --- a/lib/db/models/group/model.js +++ b/lib/db/models/group/model.js @@ -568,7 +568,7 @@ export const deleteUserGroup = async(id) => { await db.groups.deleteOne({id}) await Promise.all( - group?.users.map(email => db.groups.updateOne( + group?.users.map(email => db.users.updateOne( {email} , { $pull: {'groups.subscribed': id} diff --git a/lib/units/processor/index.ts b/lib/units/processor/index.ts index e98df9ab83..0e4578293b 100644 --- a/lib/units/processor/index.ts +++ b/lib/units/processor/index.ts @@ -9,6 +9,7 @@ import lifecycle from '../../util/lifecycle.js' import srv from '../../util/srv.js' import * as zmqutil from '../../util/zmqutil.js' import UserModel from '../../db/models/user/index.js' +import DeviceModel from '../../db/models/device/index.js' import { UserChangeMessage, GroupChangeMessage, @@ -53,8 +54,9 @@ import { DeleteDevice, SetAbsentDisconnectedDevices, GetServicesAvailabilityMessage, - DeviceRegisteredMessage, GetPresentDevices, DeviceGetIsInOrigin + DeviceRegisteredMessage, GetPresentDevices, DeviceGetIsInOrigin, GetDeadDevices } from '../../wire/wire.js' +import {getDeadDevice} from "../../db/models/device/model.js"; interface Options { name: string @@ -228,8 +230,8 @@ export default db.ensureConnectivity(async(options: Options) => { appDealer.send([channel, data]) }) .on(DeviceGetIsInOrigin, async (channel, message) => { - const device = await dbapi.loadDeviceBySerial(message.serial) - const isInOrigin = device.group.id === device.group.origin + const device = await DeviceModel.loadDeviceBySerial(message.serial) + const isInOrigin = device ? device.group.id === device.group.origin : false devDealer.send([ channel, reply.okay('success', {isInOrigin}) @@ -289,7 +291,7 @@ export default db.ensureConnectivity(async(options: Options) => { appDealer.send([channel, data]) }) .on(GetPresentDevices, async (channel, message, data) => { - const devices = await dbapi.loadPresentDevices() + const devices = await DeviceModel.loadPresentDevices() .then(devices => devices.map(d => d.serial)) devDealer.send([ channel, @@ -299,6 +301,16 @@ export default db.ensureConnectivity(async(options: Options) => { .on(DeviceHeartbeatMessage, (channel, message, data) => { devDealer.send([ channel, data ]) }) + .on(GetDeadDevices, async(channel, message, data) => { + const deadDevices = await DeviceModel.getDeadDevice(message.time) + devDealer.send([ + channel, + reply.okay('success', {deadDevices}) + ]) + }) + .on(DeleteDevice, async(channel, message, data) => { + DeviceModel.deleteDevice(message.serial) + }) .handler(); devDealer.on('message', router) diff --git a/lib/units/reaper/index.ts b/lib/units/reaper/index.ts index 0db7898f36..2fde098619 100644 --- a/lib/units/reaper/index.ts +++ b/lib/units/reaper/index.ts @@ -1,8 +1,9 @@ import logger from '../../util/logger.js' import { + DeleteDevice, DeviceAbsentMessage, DeviceHeartbeatMessage, - DeviceIntroductionMessage, DevicePresentMessage, + DeviceIntroductionMessage, DevicePresentMessage, GetDeadDevices, GetPresentDevices } from '../../wire/wire.js' import wireutil from '../../wire/util.js' @@ -17,6 +18,8 @@ const log = logger.createLogger('reaper') interface Options { heartbeatTimeout: number + timeToDeviceCleanup: number // in minutes + deviceCleanupInterval: number // in minutes endpoints: { sub: string[] push: string[] @@ -87,34 +90,85 @@ export default (async(options: Options) => { ttlset.stop() }) - try { - log.info('Reaping devices with no heartbeat') + const router = new WireRouter() + .on(DeviceIntroductionMessage, (channel, message) => { + ttlset.drop(message.serial, TTLSet.SILENT) + ttlset.bump(message.serial, Date.now()) + }) + .on(DeviceHeartbeatMessage, (channel, message) => { + ttlset.bump(message.serial, Date.now()) + }) + .on(DeviceAbsentMessage, (channel, message) => { + ttlset.drop(message.serial, TTLSet.SILENT) + }) - const router = new WireRouter() - .on(DeviceIntroductionMessage, (channel, message) => { - ttlset.drop(message.serial, TTLSet.SILENT) - ttlset.bump(message.serial, Date.now()) - }) - .on(DeviceHeartbeatMessage, (channel, message) => { - ttlset.bump(message.serial, Date.now()) - }) - .on(DeviceAbsentMessage, (channel, message) => { - ttlset.drop(message.serial, TTLSet.SILENT) - }) + if (options.timeToDeviceCleanup) { + log.info('deviceCleanerLoop enabled') - // Listen to changes - sub.on('message', router.handler()) + // This functionality is implemented in the Reaper unit because this unit cannot be replicated + const deviceCleanerLoop = () => setTimeout(async() => { + log.info('Checking dead devices [interval: %s]', options.deviceCleanupInterval) + try { + const absenceDuration = options.timeToDeviceCleanup + const {deadDevices} = await runTransactionDev(wireutil.global, GetDeadDevices, { + time: options.timeToDeviceCleanup * 60 * 1000 + }, {sub, push, router}) - // Load initial state - const {devices} = await runTransactionDev(wireutil.global, GetPresentDevices, {}, {sub, push, router}) + for (const {serial, present} of deadDevices) { + if (present) { + continue + } - const now = Date.now() - devices.forEach((serial: string) => { - ttlset.bump(serial, now, TTLSet.SILENT) - }) + log.info( // @ts-ignore + 'Removing a dead device [serial: %s, absence_duration: %.1f %s]', + serial, + ... ( + absenceDuration >= 60 // if more 1 hour + ? [absenceDuration / 60, 'hrs'] + : [absenceDuration, 'min'] + ) + ) + + push.send([ + wireutil.global, + wireutil.pack(DeleteDevice, {serial}) + ]) + } + } catch (err: any) { + log.error('Dead device check failed with error: %s', err?.message) + } finally { + deviceCleanerLoop() + } + }, options.deviceCleanupInterval * 60 * 1000) + + deviceCleanerLoop() } - catch (err: any) { - log.fatal('Unable to load initial state: %s', err?.message) - lifecycle.fatal() + + const init = async() => { + try { + log.info('Reaping devices with no heartbeat') + + // Listen to changes + sub.on('message', router.handler()) + + // Load initial state + const {devices} = await runTransactionDev(wireutil.global, GetPresentDevices, {}, {sub, push, router}) + + const now = Date.now() + devices?.forEach((serial: string) => { + ttlset.bump(serial, now, TTLSet.SILENT) + }) + } + catch (err: any) { + if (err?.message === 'Timeout when running transaction') { + log.error('Load initial state error: Timeout when running transaction, retry') + setTimeout(init, 2000) + return + } + log.fatal('Unable to load initial state: %s', err?.message) + lifecycle.fatal() + } } + + init() }) diff --git a/lib/wire/wire.proto b/lib/wire/wire.proto index 53be0505e9..655b7138a8 100644 --- a/lib/wire/wire.proto +++ b/lib/wire/wire.proto @@ -909,3 +909,7 @@ message DeviceGetIsInOrigin { message GetPresentDevices { } + +message GetDeadDevices { + required uint32 time = 1; +} diff --git a/lib/wire/wire.ts b/lib/wire/wire.ts index 85edad04eb..438ab51313 100644 --- a/lib/wire/wire.ts +++ b/lib/wire/wire.ts @@ -2397,6 +2397,15 @@ export interface DeviceGetIsInOrigin { */ export interface GetPresentDevices { } +/** + * @generated from protobuf message GetDeadDevices + */ +export interface GetDeadDevices { + /** + * @generated from protobuf field: required uint32 time = 1 + */ + time: number; +} /** * @generated from protobuf enum DeviceStatus */ @@ -11744,3 +11753,50 @@ class GetPresentDevices$Type extends MessageType { * @generated MessageType for protobuf message GetPresentDevices */ export const GetPresentDevices = new GetPresentDevices$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class GetDeadDevices$Type extends MessageType { + constructor() { + super("GetDeadDevices", [ + { no: 1, name: "time", kind: "scalar", T: 13 /*ScalarType.UINT32*/ } + ]); + } + create(value?: PartialMessage): GetDeadDevices { + const message = globalThis.Object.create((this.messagePrototype!)); + message.time = 0; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: GetDeadDevices): GetDeadDevices { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* required uint32 time */ 1: + message.time = reader.uint32(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: GetDeadDevices, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required uint32 time = 1; */ + if (message.time !== 0) + writer.tag(1, WireType.Varint).uint32(message.time); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message GetDeadDevices + */ +export const GetDeadDevices = new GetDeadDevices$Type(); From 91c8a0b1a2a97133619155f30dc80323d18e678b Mon Sep 17 00:00:00 2001 From: Elmir Khalilov <52529096+e-khalilov@users.noreply.github.com> Date: Mon, 13 Oct 2025 15:53:50 +0300 Subject: [PATCH 10/23] fix linkifyjs issue (#380) Co-authored-by: e.khalilov --- ui/package-lock.json | 21 +++++++++++++++++++++ ui/package.json | 2 ++ ui/vite.config.ts | 8 +++++++- 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/ui/package-lock.json b/ui/package-lock.json index 162c7aefc3..05631b6170 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -25,6 +25,8 @@ "i18next-http-backend": "^2.6.2", "inversify": "^6.2.1", "inversify-react": "^1.2.0", + "linkify-html": "^4.3.2", + "linkify-react": "^4.3.2", "lodash": "^4.17.21", "mobx": "^6.13.5", "mobx-persist-store": "^1.1.5", @@ -11622,6 +11624,25 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, + "node_modules/linkify-html": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/linkify-html/-/linkify-html-4.3.2.tgz", + "integrity": "sha512-RozNgrfSFrNQlprJSZIN7lF+ZVPj5Pz8POQcu1PYGAUhL9tKtvtWcOXOmlXjuGGEWHtC6gt6Q2U4+VUq9ELmng==", + "license": "MIT", + "peerDependencies": { + "linkifyjs": "^4.0.0" + } + }, + "node_modules/linkify-react": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/linkify-react/-/linkify-react-4.3.2.tgz", + "integrity": "sha512-mi744h1hf+WDsr+paJgSBBgYNLMWNSHyM9V9LVUo03RidNGdw1VpI7Twnt+K3pEh3nIzB4xiiAgZxpd61ItKpQ==", + "license": "MIT", + "peerDependencies": { + "linkifyjs": "^4.0.0", + "react": ">= 15.0.0" + } + }, "node_modules/linkifyjs": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz", diff --git a/ui/package.json b/ui/package.json index fa9dabfd55..de59eb82dc 100644 --- a/ui/package.json +++ b/ui/package.json @@ -45,6 +45,8 @@ "i18next-http-backend": "^2.6.2", "inversify": "^6.2.1", "inversify-react": "^1.2.0", + "linkify-html": "^4.3.2", + "linkify-react": "^4.3.2", "lodash": "^4.17.21", "mobx": "^6.13.5", "mobx-persist-store": "^1.1.5", diff --git a/ui/vite.config.ts b/ui/vite.config.ts index f0da2be776..91d36f544f 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -9,7 +9,13 @@ import tsconfigPaths from 'vite-tsconfig-paths' export default defineConfig({ plugins: [react({ tsDecorators: true }), tsconfigPaths(), svgr()], resolve: { - alias: [{ find: /^@vkontakte\/vkui$/, replacement: '@vkontakte/vkui/dist/cssm' }], + alias: [ + { find: /^@vkontakte\/vkui$/, replacement: '@vkontakte/vkui/dist/cssm' }, + + // TODO: REMOVE AFTER REPLACING `console-feed` WITH `chii` + { find: /^linkifyjs\/html$/, replacement: 'linkify-html' }, + { find: /^linkifyjs\/react$/, replacement: 'linkify-react' } + ], }, preview: { port: 5173, From fea328e743444a30caf4d3916ea8fc46fa1a73d8 Mon Sep 17 00:00:00 2001 From: Elmir Khalilov <52529096+e-khalilov@users.noreply.github.com> Date: Mon, 13 Oct 2025 20:58:47 +0300 Subject: [PATCH 11/23] Resolve conflicts (#381) * Hotfix: device imei issue (#371) * hotfix * minor fix * fix types * minor fix --------- Co-authored-by: e.khalilov * increase ram for v8 (#372) * hotfix (#373) Co-authored-by: e.khalilov * hotfix ADBObserver (#375) Co-authored-by: e.khalilov --------- Co-authored-by: e.khalilov Co-authored-by: Daniil <8039921+DaniilSmirnov@users.noreply.github.com> --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index eac6f90dca..f345e49bad 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,7 +32,7 @@ LABEL org.opencontainers.image.description="Control and manage Android and iOS d LABEL org.opencontainers.image.licenses=Apache-2.0 ENV PATH=/app/bin:$PATH -ENV NODE_OPTIONS="--max-old-space-size=8192" +ENV NODE_OPTIONS="--max-old-space-size=32768" EXPOSE 3000 WORKDIR /app From 2f10409f8c47c346fd28b6600d044a852c4f9621 Mon Sep 17 00:00:00 2001 From: Elmir Khalilov <52529096+e-khalilov@users.noreply.github.com> Date: Tue, 14 Oct 2025 18:53:41 +0300 Subject: [PATCH 12/23] Fixed transactions & minor optimizations (#382) * minor edits * fix transactions * minor fix * fix ts types --------- Co-authored-by: e.khalilov --- lib/db/models/all/model.js | 2 +- lib/db/proxiedModel.js | 56 ------------------------- lib/db/proxiedModel.ts | 38 +++++++++++++++++ lib/units/api/controllers/devices.js | 22 +++++----- lib/units/api/controllers/user.js | 33 ++++++++------- lib/units/api/helpers/useDevice.js | 35 +++++++++++----- lib/units/poorxy/index.js | 2 +- lib/util/zmqutil.js | 62 ++++++++++++++++++++++++---- 8 files changed, 147 insertions(+), 103 deletions(-) delete mode 100644 lib/db/proxiedModel.js create mode 100644 lib/db/proxiedModel.ts diff --git a/lib/db/models/all/model.js b/lib/db/models/all/model.js index 0dd60610b3..327a2427a9 100644 --- a/lib/db/models/all/model.js +++ b/lib/db/models/all/model.js @@ -1374,7 +1374,7 @@ export const updateDevicesOriginGroup = async(serial, group) => { db.devices.updateOne({serial}, update) ) - if (stats.modifiedCount) { + if (stats.modifiedCount || stats.matchedCount) { log.info( '[updateDevicesOriginGroup] Successfully updated origin group in device [serial: "%s", group: "%s", name: "%s"]', serial, diff --git a/lib/db/proxiedModel.js b/lib/db/proxiedModel.js deleted file mode 100644 index c78417cea6..0000000000 --- a/lib/db/proxiedModel.js +++ /dev/null @@ -1,56 +0,0 @@ -import * as Sentry from '@sentry/node' - -// ----------------------------------Proxy all methods for Sentry error tracing---------------------------------------// - -const STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg -const ARGUMENT_NAMES = /([^\s,]+)/g - -// TODO: argument names can be simplified after build -function getArgumentsNames(fn) { - const fnStr = fn.toString().replace(STRIP_COMMENTS, '') - let result = fnStr.slice(fnStr.indexOf('(') + 1, fnStr.indexOf(')')).match(ARGUMENT_NAMES) - return result || [] -} - -const getAddedAttributes = (fn, args) => Object.fromEntries( - getArgumentsNames(fn).map((argument, i) => [ - `dbapi.${argument}`, - args[i] - ]) -) - -/** - * @template ModelType - * @param {ModelType} model - * @return {ModelType} - */ -// @ts-ignore -export default (model) => new Proxy(model, { - - /** @param {string} prop */ - get(target, prop) { - return (...args) => Sentry.startSpan( - { - op: 'dbapi', - name: prop, - attributes: args?.length ? getAddedAttributes(target[prop], args) : {} - } - , () => target[prop](...args) - ) - } -}) - -// export default (model) => model ? Object.keys(model).reduce((proxiedModel, method) => { -// db.connect() -// // console.log('\n\n', method, '\n\n') -// proxiedModel[method] = (...args) => Sentry.startSpan( -// { -// op: 'dbapi' -// , name: method -// , attributes: getAddedAttributes(model[method], args) -// } -// , () => model[method](...args) -// ) -// return proxiedModel -// }, Object.create({})) : model - diff --git a/lib/db/proxiedModel.ts b/lib/db/proxiedModel.ts new file mode 100644 index 0000000000..a1c80dd255 --- /dev/null +++ b/lib/db/proxiedModel.ts @@ -0,0 +1,38 @@ +import * as Sentry from '@sentry/node' + +// ----------------------------------Proxy all methods for Sentry error tracing---------------------------------------// + +const STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg +const ARGUMENT_NAMES = /([^\s,]+)/g + +// TODO: argument names can be simplified after build +function getArgumentsNames(fn: Function) { + const fnStr = fn.toString().replace(STRIP_COMMENTS, '') + let result = fnStr.slice(fnStr.indexOf('(') + 1, fnStr.indexOf(')')).match(ARGUMENT_NAMES) + return result || [] +} + +const getAddedAttributes = (fn: Function, args: any[]) => Object.fromEntries( + getArgumentsNames(fn).map((argument, i) => [ + `dbapi.${argument}`, + args[i] + ]) +) + +export default >(model: T) => Object.keys(model).reduce((proxiedModel, method) => { + if (typeof model[method] !== 'function') { + proxiedModel[method] = model[method] + return proxiedModel + } + + proxiedModel[method] = (...args: any[]) => Sentry.startSpan( + { + op: 'dbapi', + name: method, + attributes: getAddedAttributes(model[method], args) + } + , () => model[method](...args) + ) + return proxiedModel +}, {} as any) as T + diff --git a/lib/units/api/controllers/devices.js b/lib/units/api/controllers/devices.js index 7d9f916420..58a037c5b0 100644 --- a/lib/units/api/controllers/devices.js +++ b/lib/units/api/controllers/devices.js @@ -16,7 +16,7 @@ import useDevice, {UseDeviceError} from '../helpers/useDevice.js' import * as Sentry from '@sentry/node' import {accessTokenAuth} from '../helpers/securityHandlers.js' import {DeviceOriginGroupMessage} from '../../../wire/wire.js' -var log = logger.createLogger('api:controllers:devices') +const log = logger.createLogger('api:controllers:devices') /* ------------------------------------ PRIVATE FUNCTIONS ------------------------------- */ function filterGenericDevices(req, res, devices) { @@ -237,8 +237,8 @@ function getDevices(req, res) { } } function getDeviceBySerial(req, res) { - var serial = req.params.serial - var fields = req.query.fields + const serial = req.params.serial + const fields = req.query.fields dbapi.loadDevice(req.user.groups.subscribed, serial) .then(device => { if (!device) { @@ -263,8 +263,8 @@ function getDeviceBySerial(req, res) { }) } function getDeviceSize(req, res) { - var serial = req.params.serial - dbapi.getDeviceDisplaySize(serial) + const serial = req.params.serial + return dbapi.getDeviceDisplaySize(serial) .then(response => { return res.status(200).json(response.display) }) @@ -295,8 +295,8 @@ function getDeviceGroups(req, res) { }) } function getDeviceOwner(req, res) { - var serial = req.params.serial - dbapi.getDeviceGroupOwner(serial) + const serial = req.params.serial + return dbapi.getDeviceGroupOwner(serial) .then(response => { if (!response) { return res.status(404).json({ @@ -312,8 +312,8 @@ function getDeviceOwner(req, res) { }) } function getDeviceType(req, res) { - var serial = req.params.serial - dbapi.getDeviceType(serial) + const serial = req.params.serial + return dbapi.getDeviceType(serial) .then(response => { return res.status(200).json(response) }) @@ -481,12 +481,12 @@ function removeOriginGroupDevice(req, res) { function putDeviceInfoBySerial(req, res) { const serial = req.params.serial const body = req.body - dbapi.loadDeviceBySerial(serial) + return dbapi.loadDeviceBySerial(serial) .then((data) => { if (!data) { return apiutil.respond(res, 404, `Not Found (${serial})`) } - var updates = [] + const updates = [] // Update fields based on given body if (_.has(body, 'note')) { updates.push(dbapi.setDeviceNote(serial, body.note)) diff --git a/lib/units/api/controllers/user.js b/lib/units/api/controllers/user.js index 8aef72fb95..390d7c7c5d 100644 --- a/lib/units/api/controllers/user.js +++ b/lib/units/api/controllers/user.js @@ -48,7 +48,7 @@ function getUser(req, res) { } function getUserDevices(req, res) { - var fields = req.query.fields + const fields = req.query.fields log.info('Loading user devices') dbapi.loadUserDevices(req.user.email) .then(list => { @@ -78,9 +78,9 @@ function getUserDevices(req, res) { } function getUserDeviceBySerial(req, res) { - var serial = req.params.serial - var fields = req.query.fields - dbapi.loadDevice(req.user.groups.subscribed, serial) + const serial = req.params.serial + const fields = req.query.fields + return dbapi.loadDevice(req.user.groups.subscribed, serial) .then(function(device) { if (!device) { return res.status(404).json({ @@ -95,7 +95,7 @@ function getUserDeviceBySerial(req, res) { description: 'Device is not owned by you' }) } - var responseDevice = device + let responseDevice = device if (fields) { responseDevice = _.pick(device, fields.split(',')) } @@ -116,7 +116,7 @@ function addUserDevice(req, res) { let timeout = Object.prototype.hasOwnProperty.call(req, 'body') ? req.body.timeout || null : req.query.timeout || null const lock = {} - lockutil.lockGenericDevice(req, res, lock, dbapi.lockDeviceByCurrent) + return lockutil.lockGenericDevice(req, res, lock, dbapi.lockDeviceByCurrent) .then(function(lockingSuccessed) { if (lockingSuccessed) { const device = lock.device @@ -183,7 +183,7 @@ function deleteUserDeviceBySerial(req, res) { else { serial = req.params.serial } - dbapi.loadDevice(req.user.groups.subscribed, serial) + return dbapi.loadDevice(req.user.groups.subscribed, serial) .then(async function(device) { if (!device) { if (isInternal) { @@ -216,13 +216,18 @@ function deleteUserDeviceBySerial(req, res) { } } - await runTransaction(device.channel, UngroupMessage.create({ + await runTransaction(device.channel, UngroupMessage, { requirements: wireutil.toDeviceRequirements({ serial: { value: serial, match: 'exact' } - })})) + }) + }, { + sub: req.options.sub, + push: req.options.push, + channelRouter: req.options.channelRouter + }) }) .catch(function(err) { let errSerial @@ -244,7 +249,7 @@ function deleteUserDeviceBySerial(req, res) { function remoteConnectUserDeviceBySerial(req, res) { let serial = req.params.serial - dbapi.loadDevice(req.user.groups.subscribed, serial) + return dbapi.loadDevice(req.user.groups.subscribed, serial) .then(function(device) { if (!device) { return res.status(404).json({ @@ -302,7 +307,7 @@ function remoteDisconnectUserDeviceBySerial(req, res) { else { serial = req.params.serial } - dbapi.loadDevice(req.user.groups.subscribed, serial) + return dbapi.loadDevice(req.user.groups.subscribed, serial) .then(function(device) { if (!device) { if (isInternal) { @@ -327,10 +332,10 @@ function remoteDisconnectUserDeviceBySerial(req, res) { }) } } - var responseChannel = 'txn_' + uuidv4() + const responseChannel = 'txn_' + uuidv4() req.options.sub.subscribe(responseChannel) // Timer will be called if no JoinGroupMessage is received till 5 seconds - var timer = setTimeoutS(function() { + const timer = setTimeoutS(function() { req.options.channelRouter.removeListener(responseChannel, messageListener) req.options.sub.unsubscribe(responseChannel) if (isInternal) { @@ -340,7 +345,7 @@ function remoteDisconnectUserDeviceBySerial(req, res) { return apiutil.respond(res, 504, 'Device is not responding') } }, apiutil.GRPC_WAIT_TIMEOUT) - var messageListener = new WireRouter() + const messageListener = new WireRouter() .on(ConnectStoppedMessage, function(channel, message) { if (message.serial === serial) { clearTimeout(timer) diff --git a/lib/units/api/helpers/useDevice.js b/lib/units/api/helpers/useDevice.js index 93c3a267a7..01b3ffba78 100644 --- a/lib/units/api/helpers/useDevice.js +++ b/lib/units/api/helpers/useDevice.js @@ -9,7 +9,7 @@ import wire from '../../../wire/index.js' import {v4 as uuidv4} from 'uuid' import {Log} from '../../../util/logger.js' import {runTransaction} from '../../../wire/transmanager.js' -import {ConnectStartedMessage, JoinGroupMessage} from '../../../wire/wire.js' +import {ConnectStartedMessage, GroupMessage, JoinGroupMessage, OwnerMessage, UngroupMessage} from '../../../wire/wire.js' export const UseDeviceError = Object.freeze({ NOT_FOUND: 0, @@ -50,10 +50,12 @@ const useDevice = ({user, device, channelRouter, push, sub, usage = null, log}) }) try { - await runTransaction(device.channel, new wire.UngroupMessage(deviceRequirements), {sub, push, channelRouter}) + await runTransaction(device.channel, UngroupMessage, { + requirements: deviceRequirements + }, {sub, push, channelRouter}) } catch (/** @type {any} */e) { - log?.info('Transaction failed: $s', e?.message) + log?.info('Transaction failed: %s', e?.message) } const responseTimeout = setTimeout(function() { @@ -73,7 +75,7 @@ const useDevice = ({user, device, channelRouter, push, sub, usage = null, log}) sub.subscribe(responseChannel) const connectTimeout = setTimeout(function() { - channelRouter.removeListener(responseChannel, useDeviceMessageListener) + channelRouter.removeListener(responseChannel, messageListener) sub.unsubscribe(responseChannel) reject(UseDeviceError.FAILED_CONNECT) @@ -102,13 +104,24 @@ const useDevice = ({user, device, channelRouter, push, sub, usage = null, log}) channelRouter.on(wireutil.global, useDeviceMessageListener) - await runTransaction(device.channel, new wire.GroupMessage( - new wire.OwnerMessage(user.email, user.name, user.group) - , timeout - , deviceRequirements - , usage - , user.adbKeys.map((/** @type {{ fingerprint: string }} */ k) => k.fingerprint) - ), {sub, push, channelRouter}) + try { + await runTransaction(device.channel, GroupMessage, { + owner: OwnerMessage.create({ + email: user.email, + name: user.name, + group: user.group + }), + requirements: deviceRequirements, + usage: usage || undefined, + keys: user.adbKeys?.map((/** @type {{ fingerprint: string }} */ k) => k.fingerprint) || [] + }, {sub, push, channelRouter}) + } + catch (/** @type {any} */e) { + log?.info('Transaction failed: %s', e?.message) + clearTimeout(responseTimeout) + channelRouter.removeListener(wireutil.global, useDeviceMessageListener) + return reject(UseDeviceError.FAILED_JOIN) + } }) export default useDevice diff --git a/lib/units/poorxy/index.js b/lib/units/poorxy/index.js index 6a6efc00d9..3b8320dd83 100644 --- a/lib/units/poorxy/index.js +++ b/lib/units/poorxy/index.js @@ -8,7 +8,7 @@ export default (function(options) { let server = http.createServer(app) let proxy = httpProxy.createProxyServer() proxy.on('error', function(err) { - log.error('Proxy had an error', err.stack) + log.error('Proxy had an error %s', err?.message) }) app.use(function(req, res, next) { res.setHeader('X-devicehub-unit', 'poorxy') diff --git a/lib/util/zmqutil.js b/lib/util/zmqutil.js index 6098ff06d7..3a4d67b438 100644 --- a/lib/util/zmqutil.js +++ b/lib/util/zmqutil.js @@ -20,9 +20,32 @@ const socketTypeMap = { reply: zmq.Reply } +// Shared ZMQ context to avoid creating multiple contexts with thread pools +// Each context creates ioThreads (4 by default), so sharing saves resources +/** @type {zmq.Context | null} */ +let sharedContext = null +const getSharedContext = () => { + if (!sharedContext) { + sharedContext = new zmq.Context({ + blocky: true, + ioThreads: 4, + ipv6: true, + maxSockets: 8192, + }) + } + return sharedContext +} + export class SocketWrapper extends EventEmitter { #sendQueue = Promise.resolve() + /** @type {AsyncIterator | null} */ + #iterator = null + + /** + * @param {string} type + * @param {number} keepAliveInterval + */ constructor(type, keepAliveInterval = 30) { super() @@ -34,18 +57,14 @@ export class SocketWrapper extends EventEmitter { this.isActive = true this.endpoints = new Set() + // @ts-ignore const SocketClass = socketTypeMap[type] this.socket = new SocketClass({ tcpKeepalive: 1, tcpKeepaliveIdle: keepAliveInterval, tcpKeepaliveInterval: keepAliveInterval, tcpKeepaliveCount: 100 - }, new zmq.Context({ - blocky: true, - ioThreads: 4, - ipv6: true, - maxSockets: 8192, - })) + }, getSharedContext()) } bindSync = (address) => this.socket.bindSync(address) @@ -86,17 +105,42 @@ export class SocketWrapper extends EventEmitter { } catch (/** @type {any} */ err) { log.error('Error on send: %s', err?.message || err?.toString() || JSON.stringify(err)) + throw err // Re-throw to properly handle in the promise chain } } + /** + * @param {any} args + */ send(args) { this.#sendQueue = this.#sendQueue.then(() => this.sendAsync(args)) return this } - close() { + async close() { this.isActive = false + + // Close async iterator if it exists + if (this.#iterator && typeof this.#iterator.return === 'function') { + try { + await this.#iterator.return() + } + catch { + // Ignore errors during cleanup + } + this.#iterator = null + } + + // Wait for send queue to drain before closing socket + try { + await this.#sendQueue.catch(() => {}) + } + catch { + // Ignore errors during cleanup + } + this.socket.close() + this.removeAllListeners() return this } @@ -118,10 +162,10 @@ export class SocketWrapper extends EventEmitter { } try { - const iterator = this.socket[Symbol.asyncIterator]() + this.#iterator = this.socket[Symbol.asyncIterator]() let result - while (this.isActive && !(result = await iterator.next()).done) { + while (this.isActive && this.#iterator && !(result = await this.#iterator.next()).done) { const message = result.value if (Array.isArray(message) && !!message[0]?.toString) { From 34ccd1ddff85af967a47ca3770d28415a3b94055 Mon Sep 17 00:00:00 2001 From: Maksim Alzhanov Date: Tue, 14 Oct 2025 21:25:45 +0300 Subject: [PATCH 13/23] Fix lint issues --- lib/units/base-device/plugins/group.js | 0 lib/units/device/plugins/group.js | 0 lib/units/tizen-device/plugins/webinspector/Replicator.ts | 2 -- lib/util/lifecycle.js | 0 4 files changed, 2 deletions(-) delete mode 100755 lib/units/base-device/plugins/group.js delete mode 100644 lib/units/device/plugins/group.js delete mode 100644 lib/util/lifecycle.js diff --git a/lib/units/base-device/plugins/group.js b/lib/units/base-device/plugins/group.js deleted file mode 100755 index e69de29bb2..0000000000 diff --git a/lib/units/device/plugins/group.js b/lib/units/device/plugins/group.js deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/lib/units/tizen-device/plugins/webinspector/Replicator.ts b/lib/units/tizen-device/plugins/webinspector/Replicator.ts index 4cb8800f01..4bd30fabd8 100644 --- a/lib/units/tizen-device/plugins/webinspector/Replicator.ts +++ b/lib/units/tizen-device/plugins/webinspector/Replicator.ts @@ -7,7 +7,6 @@ const KEY_REQUIRE_ESCAPING_RE = /^#*@(t|r)$/ const REMAINING_KEY = '__console_feed_remaining__' const GLOBAL = (function getGlobal() { // NOTE: see http://www.ecma-international.org/ecma-262/6.0/index.html#sec-performeval step 10 - // eslint-disable-next-line no-eval const savedEval = eval return savedEval('this') })() @@ -128,7 +127,6 @@ class EncodingTransformer { } const remaining = total - counter - // eslint-disable-next-line no-proto const name = obj?.__proto__?.constructor?.name if (name && name !== 'Object') { diff --git a/lib/util/lifecycle.js b/lib/util/lifecycle.js deleted file mode 100644 index e69de29bb2..0000000000 From 596d17d792e461ac5190cb368bcf0f50d5879e90 Mon Sep 17 00:00:00 2001 From: Elmir Khalilov <52529096+e-khalilov@users.noreply.github.com> Date: Mon, 17 Nov 2025 11:49:00 +0300 Subject: [PATCH 14/23] Minitouch refactoring (#388) * Develop to Master for 1.5.1 (#364) * Wire initial typescript (#338) * Wire initial typescript * Fix types * Something works * Finishing touches * Linter fixes * Build problems * hotfix (#361) Co-authored-by: e.khalilov * Remove all DB connections from device side (#368) * hotfix imei * Decrease apt install list in dockerfile (#365) * remove gm and jdk11 * remove libs * Remove console-feed & react dependencies [backend only] (#366) * remove dep console-feed * minor fix * linter fix --------- Co-authored-by: e.khalilov * fix default quotas hierarchy QA-19255 (#367) * -fix quotas -fix lock -fix test -fix lint * -fix test --------- Co-authored-by: a.chistov * dev units without db conn * fix types * minor fix * minor fix * minor fix * remove useless sockets --------- Co-authored-by: Maksim Alzhanov Co-authored-by: Maxim Co-authored-by: Daniil <8039921+DaniilSmirnov@users.noreply.github.com> Co-authored-by: e.khalilov Co-authored-by: Alexey Chistov <33050834+Alk2017@users.noreply.github.com> Co-authored-by: a.chistov * fix device type & types (#374) Co-authored-by: e.khalilov * reconnect in ADBObserver (#376) Co-authored-by: e.khalilov * QA-10057 Remove users watcher groups-engine (#369) * -fix after rebase -remove user watcher -user handler for alert message default quotas handler -fix merge del handler update quoats handler user handler method some fix -move without change method of devices to separate class -fix after move users method -move db.users method to separate class * add missing db method --------- Co-authored-by: a.chistov Co-authored-by: e.khalilov * fix heartbeat (#377) Co-authored-by: e.khalilov * overrides dependencies (#378) Co-authored-by: e.khalilov * Automatic removal of disabled devices (#379) * clear dead devices * minor fix * revert api edits * revert ui (types) edits --------- Co-authored-by: e.khalilov * fix linkifyjs issue (#380) Co-authored-by: e.khalilov * Resolve conflicts (#381) * Hotfix: device imei issue (#371) * hotfix * minor fix * fix types * minor fix --------- Co-authored-by: e.khalilov * increase ram for v8 (#372) * hotfix (#373) Co-authored-by: e.khalilov * hotfix ADBObserver (#375) Co-authored-by: e.khalilov --------- Co-authored-by: e.khalilov Co-authored-by: Daniil <8039921+DaniilSmirnov@users.noreply.github.com> * Fixed transactions & minor optimizations (#382) * minor edits * fix transactions * minor fix * fix ts types --------- Co-authored-by: e.khalilov * Fix lint issues --------- Co-authored-by: Maxim Co-authored-by: Elmir Khalilov <52529096+e-khalilov@users.noreply.github.com> Co-authored-by: e.khalilov Co-authored-by: Maksim Alzhanov Co-authored-by: Alexey Chistov <33050834+Alk2017@users.noreply.github.com> Co-authored-by: a.chistov * Hotfix: Fix broken import in ios Co-authored-by: e.khalilov * minitouch refactoring + TS --------- Co-authored-by: Daniil <8039921+DaniilSmirnov@users.noreply.github.com> Co-authored-by: Maxim Co-authored-by: e.khalilov Co-authored-by: Maksim Alzhanov Co-authored-by: Alexey Chistov <33050834+Alk2017@users.noreply.github.com> Co-authored-by: a.chistov --- lib/units/device/plugins/touch/index.js | 473 ----------------- lib/units/device/plugins/touch/index.ts | 642 ++++++++++++++++++++++++ lib/units/device/resources/minitouch.js | 75 --- lib/units/device/resources/minitouch.ts | 96 ++++ lib/units/ios-device/plugins/install.js | 3 +- lib/util/failcounter.js | 25 - lib/util/failcounter.ts | 31 ++ lib/util/riskystream.js | 46 -- lib/util/riskystream.ts | 56 +++ 9 files changed, 826 insertions(+), 621 deletions(-) delete mode 100644 lib/units/device/plugins/touch/index.js create mode 100644 lib/units/device/plugins/touch/index.ts delete mode 100644 lib/units/device/resources/minitouch.js create mode 100644 lib/units/device/resources/minitouch.ts delete mode 100644 lib/util/failcounter.js create mode 100644 lib/util/failcounter.ts delete mode 100644 lib/util/riskystream.js create mode 100644 lib/util/riskystream.ts diff --git a/lib/units/device/plugins/touch/index.js b/lib/units/device/plugins/touch/index.js deleted file mode 100644 index e3256c771b..0000000000 --- a/lib/units/device/plugins/touch/index.js +++ /dev/null @@ -1,473 +0,0 @@ -import util from 'util' -import Promise from 'bluebird' -import syrup from '@devicefarmer/stf-syrup' -import split from 'split' -import EventEmitter from 'eventemitter3' -import {Parser, Adb} from '@u4/adbkit' -import wire from '../../../../wire/index.js' -import logger from '../../../../util/logger.js' -import lifecycle from '../../../../util/lifecycle.js' -import SeqQueue from '../../../../wire/seqqueue.js' -import StateQueue from '../../../../util/statequeue.js' -import RiskyStream from '../../../../util/riskystream.js' -import FailCounter from '../../../../util/failcounter.js' -import adb from '../../support/adb.js' -import router from '../../../base-device/support/router.js' -import minitouch from '../../resources/minitouch.js' -import flags from '../util/flags.js' -import {GestureStartMessage, GestureStopMessage, TouchCommitMessage, TouchDownMessage, TouchMoveMessage, TouchResetMessage, TouchUpMessage} from '../../../../wire/wire.js' -export default syrup.serial() - .dependency(adb) - .dependency(router) - .dependency(minitouch) - .dependency(flags) - .define(function(options, adb, router, minitouch, flags) { - var log = logger.createLogger('device:plugins:touch') - function TouchConsumer(config) { - EventEmitter.call(this) - this.actionQueue = [] - this.runningState = TouchConsumer.STATE_STOPPED - this.desiredState = new StateQueue() - this.output = null - this.socket = null - this.banner = null - this.touchConfig = config - this.starter = Promise.resolve(true) - this.failCounter = new FailCounter(3, 10000) - this.failCounter.on('exceedLimit', this._failLimitExceeded.bind(this)) - this.failed = false - this.readableListener = this._readableListener.bind(this) - this.writeQueue = [] - } - util.inherits(TouchConsumer, EventEmitter) - TouchConsumer.STATE_STOPPED = 1 - TouchConsumer.STATE_STARTING = 2 - TouchConsumer.STATE_STARTED = 3 - TouchConsumer.STATE_STOPPING = 4 - TouchConsumer.prototype._queueWrite = function(writer) { - switch (this.runningState) { - case TouchConsumer.STATE_STARTED: - writer.call(this) - break - default: - this.writeQueue.push(writer) - break - } - } - TouchConsumer.prototype.touchDown = function(point) { - this._queueWrite(function() { - return this._write(util.format('d %s %s %s %s\n', point.contact, Math.ceil(this.touchConfig.origin.x(point) * this.banner.maxX), Math.ceil(this.touchConfig.origin.y(point) * this.banner.maxY), Math.ceil((point.pressure || 0.5) * this.banner.maxPressure))) - }) - } - TouchConsumer.prototype.touchMove = function(point) { - this._queueWrite(function() { - return this._write(util.format('m %s %s %s %s\n', point.contact, Math.ceil(this.touchConfig.origin.x(point) * this.banner.maxX), Math.ceil(this.touchConfig.origin.y(point) * this.banner.maxY), Math.ceil((point.pressure || 0.5) * this.banner.maxPressure))) - }) - } - TouchConsumer.prototype.touchUp = function(point) { - this._queueWrite(function() { - return this._write(util.format('u %s\n', point.contact)) - }) - } - TouchConsumer.prototype.touchCommit = function() { - this._queueWrite(function() { - return this._write('c\n') - }) - } - TouchConsumer.prototype.touchReset = function() { - this._queueWrite(function() { - return this._write('r\n') - }) - } - TouchConsumer.prototype.tap = function(point) { - this.touchDown(point) - this.touchCommit() - this.touchUp(point) - this.touchCommit() - } - TouchConsumer.prototype._ensureState = function() { - if (this.desiredState.empty()) { - return - } - if (this.failed) { - log.warn('Will not apply desired state due to too many failures') - return - } - switch (this.runningState) { - case TouchConsumer.STATE_STARTING: - case TouchConsumer.STATE_STOPPING: - // Just wait. - break - case TouchConsumer.STATE_STOPPED: - if (this.desiredState.next() === TouchConsumer.STATE_STARTED) { - this.runningState = TouchConsumer.STATE_STARTING - this.starter = this._startService() - .then((out) => { - this.output = new RiskyStream(out) - .on('unexpectedEnd', this._outputEnded.bind(this)) - return this._readOutput(this.output.stream) - }) - .then(() => { - return this._connectService() - }) - .then((socket) => { - this.socket = new RiskyStream(socket) - .on('unexpectedEnd', this._socketEnded.bind(this)) - return this._readBanner(this.socket.stream) - }) - .then((banner) => { - this.banner = banner - return this._readUnexpected(this.socket.stream) - }) - .then(() => { - this._processWriteQueue() - }) - .then(() => { - this.runningState = TouchConsumer.STATE_STARTED - this.emit('start') - }) - .catch(Promise.CancellationError, () => { - return this._stop() - }) - .catch((err) => { - return this._stop().finally(() => { - this.failCounter.inc() - this.emit('error', err) - }) - }) - .finally(() => { - this._ensureState() - }) - } - else { - setImmediate(this._ensureState.bind(this)) - } - break - case TouchConsumer.STATE_STARTED: - if (this.desiredState.next() === TouchConsumer.STATE_STOPPED) { - this.runningState = TouchConsumer.STATE_STOPPING - this._stop().finally(function() { - this._ensureState() - }) - } - else { - setImmediate(this._ensureState.bind(this)) - } - break - } - } - TouchConsumer.prototype.start = function() { - this.desiredState.push(TouchConsumer.STATE_STARTED) - this._ensureState() - } - TouchConsumer.prototype.stop = function() { - this.desiredState.push(TouchConsumer.STATE_STOPPED) - this._ensureState() - } - TouchConsumer.prototype.restart = function() { - switch (this.runningState) { - case TouchConsumer.STATE_STARTED: - case TouchConsumer.STATE_STARTING: - this._stop() - this.desiredState.push(TouchConsumer.STATE_STOPPED) - this.desiredState.push(TouchConsumer.STATE_STARTED) - this._ensureState() - break - } - } - TouchConsumer.prototype._configChanged = function() { - this.restart() - } - TouchConsumer.prototype._socketEnded = function() { - this.failCounter.inc() - this.restart() - } - TouchConsumer.prototype._outputEnded = function() { - this.failCounter.inc() - this.restart() - } - TouchConsumer.prototype._failLimitExceeded = function(limit, time) { - this._stop() - this.failed = true - this.emit('error', new Error(util.format('Failed more than %d times in %dms', limit, time))) - } - TouchConsumer.prototype._startService = async function() { - return await minitouch.run() - } - TouchConsumer.prototype._readOutput = function(out) { - out.pipe(split()).on('data', function(line) { - var trimmed = line.toString().trim() - if (trimmed === '') { - return - } - if (/ERROR/.test(line)) { - log.fatal('minitouch error: "%s"', line) - return lifecycle.fatal() - } - log.info('minitouch says: "%s"', line) - }) - } - TouchConsumer.prototype._connectService = function() { - function tryConnect(times, delay) { - return adb.getDevice(options.serial).openLocal('localabstract:minitouch') - .then(function(out) { - return out - }) - .catch(function(err) { - if (/closed/.test(err.message) && times > 1) { - return Promise.delay(delay) - .then(function() { - return tryConnect(times - 1, delay * 2) - }) - } - return Promise.reject(err) - }) - } - log.info('Connecting to minitouch service') - // SH-03G can be very slow to start sometimes. Make sure we try long - // enough. - return tryConnect(7, 100) - } - TouchConsumer.prototype._stop = function() { - return this._disconnectService(this.socket).bind(this) - .timeout(2000) - .then(function() { - return this._stopService(this.output).timeout(10000) - }) - .then(function() { - this.runningState = TouchConsumer.STATE_STOPPED - this.emit('stop') - }) - .catch(function(err) { - // In practice we _should_ never get here due to _stopService() - // being quite aggressive. But if we do, well... assume it - // stopped anyway for now. - this.runningState = TouchConsumer.STATE_STOPPED - this.emit('error', err) - this.emit('stop') - }) - .finally(function() { - this.output = null - this.socket = null - this.banner = null - }) - } - TouchConsumer.prototype._disconnectService = function(socket) { - log.info('Disconnecting from minitouch service') - if (!socket || socket.ended) { - return Promise.resolve(true) - } - socket.stream.removeListener('readable', this.readableListener) - var endListener - return new Promise(function(resolve) { - socket.on('end', endListener = function() { - resolve(true) - }) - socket.stream.resume() - socket.end() - }) - .finally(function() { - socket.removeListener('end', endListener) - }) - } - TouchConsumer.prototype._stopService = function(output) { - log.info('Stopping minitouch service') - if (!output || output.ended) { - return Promise.resolve(true) - } - var pid = this.banner ? this.banner.pid : -1 - function kill(signal) { - if (pid <= 0) { - return Promise.reject(new Error('Minitouch service pid is unknown')) - } - var signum = { - SIGTERM: -15, - SIGKILL: -9 - }[signal] - log.info('Sending %s to minitouch', signal) - return Promise.all([ - output.waitForEnd(), - adb.getDevice(options.serial).shell(['kill', signum, pid]) - .then(Adb.util.readAll) - .return(true) - ]) - .timeout(2000) - } - function kindKill() { - return kill('SIGTERM') - } - function forceKill() { - return kill('SIGKILL') - } - function forceEnd() { - log.info('Ending minitouch I/O as a last resort') - output.end() - return Promise.resolve(true) - } - return kindKill() - .catch(Promise.TimeoutError, forceKill) - .catch(forceEnd) - } - TouchConsumer.prototype._readBanner = async function(socket) { - log.info('Reading minitouch banner') - var parser = new Parser(socket) - var banner = { - pid: -1, // @todo - version: 0, - maxContacts: 0, - maxX: 0, - maxY: 0, - maxPressure: 0 - } - function readVersion() { - return parser.readLine() - .then(function(chunk) { - var args = chunk.toString().split(/ /g) - switch (args[0]) { - case 'v': - banner.version = Number(args[1]) - break - default: - throw new Error(util.format('Unexpected output "%s", expecting version line', chunk)) - } - }) - } - function readLimits() { - return parser.readLine() - .then(function(chunk) { - var args = chunk.toString().split(/ /g) - switch (args[0]) { - case '^': - banner.maxContacts = args[1] - banner.maxX = args[2] - banner.maxY = args[3] - banner.maxPressure = args[4] - break - default: - throw new Error(util.format('Unknown output "%s", expecting limits line', chunk)) - } - }) - } - function readPid() { - return parser.readLine() - .then(function(chunk) { - var args = chunk.toString().split(/ /g) - switch (args[0]) { - case '$': - banner.pid = Number(args[1]) - break - default: - throw new Error(util.format('Unexpected output "%s", expecting pid line', chunk)) - } - }) - } - await readVersion() - await readLimits() - await readPid() - return banner - } - TouchConsumer.prototype._readUnexpected = function(socket) { - socket.on('readable', this.readableListener) - // We may already have data pending. - this.readableListener() - } - TouchConsumer.prototype._readableListener = function() { - var chunk - while ((chunk = this.socket.stream.read())) { - log.warn('Unexpected output from minitouch socket', chunk) - } - } - TouchConsumer.prototype._processWriteQueue = function() { - for (var i = 0, l = this.writeQueue.length; i < l; ++i) { - this.writeQueue[i].call(this) - } - this.writeQueue = [] - } - TouchConsumer.prototype._write = function(chunk) { - this.socket.stream.write(chunk) - } - function startConsumer() { - var touchConsumer = new TouchConsumer({ - // Usually the touch origin is the same as the display's origin, - // but sometimes it might not be. - origin: (function(origin) { - log.info('Touch origin is %s', origin) - return { - 'top left': { - x: function(point) { - return point.x - }, - y: function(point) { - return point.y - } - }, // So far the only device we've seen exhibiting this behavior - // is Yoga Tablet 8. - - 'bottom left': { - x: function(point) { - return 1 - point.y - }, - y: function(point) { - return point.x - } - } - }[origin] - })(flags.get('forceTouchOrigin', 'top left')) - }) - var startListener, errorListener - return new Promise(function(resolve, reject) { - touchConsumer.on('start', startListener = function() { - resolve(touchConsumer) - }) - touchConsumer.on('error', errorListener = reject) - touchConsumer.start() - }) - .finally(function() { - touchConsumer.removeListener('start', startListener) - touchConsumer.removeListener('error', errorListener) - }) - } - return startConsumer() - .then(function(touchConsumer) { - var queue = new SeqQueue(100, 4) - touchConsumer.on('error', function(err) { - log.fatal('Touch consumer had an error', err.stack) - lifecycle.fatal() - }) - router - .on(GestureStartMessage, function(channel, message) { - queue.start(message.seq) - }) - .on(GestureStopMessage, function(channel, message) { - queue.push(message.seq, function() { - queue.stop() - }) - }) - .on(TouchDownMessage, function(channel, message) { - queue.push(message.seq, function() { - touchConsumer.touchDown(message) - }) - }) - .on(TouchMoveMessage, function(channel, message) { - queue.push(message.seq, function() { - touchConsumer.touchMove(message) - }) - }) - .on(TouchUpMessage, function(channel, message) { - queue.push(message.seq, function() { - touchConsumer.touchUp(message) - }) - }) - .on(TouchCommitMessage, function(channel, message) { - queue.push(message.seq, function() { - touchConsumer.touchCommit() - }) - }) - .on(TouchResetMessage, function(channel, message) { - queue.push(message.seq, function() { - touchConsumer.touchReset() - }) - }) - return touchConsumer - }) - }) diff --git a/lib/units/device/plugins/touch/index.ts b/lib/units/device/plugins/touch/index.ts new file mode 100644 index 0000000000..13e961efa3 --- /dev/null +++ b/lib/units/device/plugins/touch/index.ts @@ -0,0 +1,642 @@ +import util from 'util' +import syrup from '@devicefarmer/stf-syrup' +import type {Client} from '@u4/adbkit' +import split from 'split' +import EventEmitter from 'events' +import {Parser, Adb} from '@u4/adbkit' +import logger from '../../../../util/logger.js' +import lifecycle from '../../../../util/lifecycle.js' +import SeqQueue from '../../../../wire/seqqueue.js' +import StateQueue from '../../../../util/statequeue.js' +import RiskyStream from '../../../../util/riskystream.js' +import FailCounter from '../../../../util/failcounter.js' +import adb from '../../support/adb.js' +import router from '../../../base-device/support/router.js' +import minitouch from '../../resources/minitouch.js' +import flags from '../util/flags.js' +import { + GestureStartMessage, + GestureStopMessage, + TouchCommitMessage, + TouchDownMessage, + TouchMoveMessage, + TouchResetMessage, + TouchUpMessage +} from '../../../../wire/wire.js' + +interface TouchPoint { + contact: number + x: number + y: number + pressure?: number +} + +interface TouchOrigin { + x: (point: TouchPoint) => number + y: (point: TouchPoint) => number +} + +interface TouchConfig { + origin: TouchOrigin +} + +interface Banner { + pid: number + version: number + maxContacts: number + maxX: number + maxY: number + maxPressure: number +} + +interface MinitouchService { + bin: string + run: (cmd?: string) => Promise +} + +interface TouchOptions { + serial: string +} + +const log = logger.createLogger('device:plugins:touch') + +const STATE_STOPPED = 1 +const STATE_STARTING = 2 +const STATE_STARTED = 3 +const STATE_STOPPING = 4 + +type TouchState = typeof STATE_STOPPED | typeof STATE_STARTING | typeof STATE_STARTED | typeof STATE_STOPPING + +class TouchConsumer extends EventEmitter { + private actionQueue: any[] = [] + private runningState: TouchState = STATE_STOPPED + private desiredState: StateQueue + private output: RiskyStream | null = null + private socket: RiskyStream | null = null + private banner: Banner | null = null + private touchConfig: TouchConfig + private starter: Promise = Promise.resolve(true) + private failCounter: FailCounter + private failed: boolean = false + private readableListener: () => void + private writeQueue: Array<() => void> = [] + private options: TouchOptions + private adb: Client + private minitouch: MinitouchService + private ensureStateLock: boolean = false + private splitStream: any = null + + constructor(config: TouchConfig, options: TouchOptions, adb: Client, minitouch: MinitouchService) { + super() + this.options = options + this.adb = adb + this.minitouch = minitouch + this.desiredState = new StateQueue() + this.touchConfig = config + this.failCounter = new FailCounter(3, 10000) + this.failCounter.on('exceedLimit', this._failLimitExceeded.bind(this)) + this.readableListener = this._readableListener.bind(this) + } + + private _queueWrite(writer: () => void): void { + switch (this.runningState) { + case STATE_STARTED: + writer.call(this) + break + default: + this.writeQueue.push(writer) + break + } + } + + touchDown(point: TouchPoint): void { + this._queueWrite(() => { + const x = Math.ceil(this.touchConfig.origin.x(point) * this.banner!.maxX) + const y = Math.ceil(this.touchConfig.origin.y(point) * this.banner!.maxY) + const p = Math.ceil((point.pressure || 0.5) * this.banner!.maxPressure) + return this._write(`d ${point.contact} ${x} ${y} ${p}\n`) + }) + } + + touchMove(point: TouchPoint): void { + this._queueWrite(() => { + const x = Math.ceil(this.touchConfig.origin.x(point) * this.banner!.maxX) + const y = Math.ceil(this.touchConfig.origin.y(point) * this.banner!.maxY) + const p = Math.ceil((point.pressure || 0.5) * this.banner!.maxPressure) + return this._write(`m ${point.contact} ${x} ${y} ${p}\n`) + }) + } + + touchUp(point: TouchPoint): void { + this._queueWrite(() => { + return this._write(`u ${point.contact}\n`) + }) + } + + touchCommit(): void { + this._queueWrite(() => { + return this._write('c\n') + }) + } + + touchReset(): void { + this._queueWrite(() => { + return this._write('r\n') + }) + } + + tap(point: TouchPoint): void { + this.touchDown(point) + this.touchCommit() + this.touchUp(point) + this.touchCommit() + } + + private async startState(): Promise { + if (this.desiredState.next() !== STATE_STARTED) { + this.ensureStateLock = false + setImmediate(() => this._ensureState()) + return + } + + this.runningState = STATE_STARTING + try { + const out = await this._startService() + this.output = new RiskyStream(out) + .on('unexpectedEnd', this._outputEnded.bind(this)) + + this._readOutput(this.output.stream) + + const socket = await this._connectService() + this.socket = new RiskyStream(socket) + .on('unexpectedEnd', this._socketEnded.bind(this)) + + const banner = await this._readBanner(this.socket.stream) + this.banner = banner + + this._readUnexpected(this.socket.stream) + this._processWriteQueue() + + this.runningState = STATE_STARTED + this.emit('start') + } catch (err: any) { + try { + await this._stop() + } finally { + if (err.name !== 'CancellationError') { + this.failCounter.inc() + this.emit('error', err) + } + } + } finally { + this.ensureStateLock = false + this._ensureState() + } + } + + private async stopState(): Promise { + if (this.desiredState.next() !== STATE_STOPPED) { + this.ensureStateLock = false + setImmediate(() => this._ensureState()) + return + } + + this.runningState = STATE_STOPPING + await this._stop() + .finally(() => { + this.ensureStateLock = false + this._ensureState() + }) + } + + private async _ensureState(): Promise { + if (this.desiredState.empty()) { + return + } + if (this.failed) { + log.warn('Will not apply desired state due to too many failures') + return + } + + // Prevent concurrent execution + if (this.ensureStateLock) { + return + } + + this.ensureStateLock = true + try { + switch (this.runningState) { + case STATE_STARTING: + case STATE_STOPPING: + // Just wait. + break + case STATE_STOPPED: + await this.startState() + break + case STATE_STARTED: + await this.stopState() + break + } + } catch (err) { + this.ensureStateLock = false + throw err + } + } + + start(): void { + this.desiredState.push(STATE_STARTED) + this._ensureState() + } + + stop(): void { + this.desiredState.push(STATE_STOPPED) + this._ensureState() + } + + async restart(): Promise { + switch (this.runningState) { + case STATE_STARTED: + case STATE_STARTING: + await this._stop() + this.desiredState.push(STATE_STOPPED) + this.desiredState.push(STATE_STARTED) + this._ensureState() + break + } + } + + private _configChanged(): void { + this.restart() + } + + private _socketEnded(): void { + this.failCounter.inc() + this.restart() + } + + private _outputEnded(): void { + this.failCounter.inc() + this.restart() + } + + private _failLimitExceeded(limit: number, time: number): void { + this._stop() + this.failed = true + this.emit('error', new Error(util.format('Failed more than %d times in %dms', limit, time))) + } + + private async _startService(): Promise { + return await this.minitouch.run() + } + + private _readOutput(out: NodeJS.ReadableStream): void { + // Clean up previous split stream if exists + if (this.splitStream) { + this.splitStream.removeAllListeners('data') + this.splitStream.destroy() + } + + this.splitStream = out.pipe(split()).on('data', (line: any) => { + const trimmed = line.toString().trim() + if (trimmed === '') { + return + } + if (line.includes('ERROR')) { + log.fatal('minitouch error: "%s"', line) + return lifecycle.fatal() + } + log.info('minitouch says: "%s"', line) + }) + } + + private async _connectService(): Promise { + const tryConnect = async (times: number, delay: number): Promise => { + try { + const out = await this.adb.getDevice(this.options.serial).openLocal('localabstract:minitouch') + return out + } catch (err: any) { + if (err.message?.includes('closed') && times > 1) { + await new Promise(resolve => setTimeout(resolve, delay)) + return tryConnect(times - 1, delay * 2) + } + throw err + } + } + + log.info('Connecting to minitouch service') + // SH-03G can be very slow to start sometimes. Make sure we try long + // enough. + return tryConnect(7, 100) + } + + private async _stop(): Promise { + try { + await this._disconnectService(this.socket) + await Promise.race([ + this._stopService(this.output), + new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 10000)) + ]) + this.runningState = STATE_STOPPED + this.emit('stop') + } catch (err) { + // In practice we _should_ never get here due to _stopService() + // being quite aggressive. But if we do, well... assume it + // stopped anyway for now. + this.runningState = STATE_STOPPED + this.emit('error', err) + this.emit('stop') + } finally { + // Clean up split stream + if (this.splitStream) { + this.splitStream.removeAllListeners('data') + this.splitStream.destroy() + this.splitStream = null + } + + this.output = null + this.socket = null + this.banner = null + } + } + + private async _disconnectService(socket: RiskyStream | null): Promise { + log.info('Disconnecting from minitouch service') + + if (!socket || socket.ended) { + return true + } + + socket.stream.removeListener('readable', this.readableListener) + + return new Promise((resolve) => { + const endListener = () => { + socket.removeListener('end', endListener) + resolve(true) + } + socket.on('end', endListener) + socket.stream.resume() + socket.end() + + // Add timeout + setTimeout(() => { + socket.removeListener('end', endListener) + resolve(true) + }, 2000) + }) + } + + private async _stopService(output: RiskyStream | null): Promise { + log.info('Stopping minitouch service') + + if (!output || output.ended) { + return true + } + + const pid = this.banner ? this.banner.pid : -1 + + const kill = async (signal: 'SIGTERM' | 'SIGKILL'): Promise => { + if (pid <= 0) { + throw new Error('Minitouch service pid is unknown') + } + const signum = { + SIGTERM: -15, + SIGKILL: -9 + }[signal] + + log.info('Sending %s to minitouch', signal) + + await Promise.race([ + Promise.all([ + output.waitForEnd(), + this.adb.getDevice(this.options.serial).shell(['kill', signum.toString(), pid.toString()]) + .then(Adb.util.readAll) + ]), + new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 2000)) + ]) + + return true + } + + const kindKill = () => kill('SIGTERM') + const forceKill = () => kill('SIGKILL') + const forceEnd = () => { + log.info('Ending minitouch I/O as a last resort') + output.end() + return true + } + + try { + return await kindKill() + } catch (err: any) { + if (err.message === 'Timeout') { + try { + return await forceKill() + } catch { + return forceEnd() + } + } + return forceEnd() + } + } + + private async _readBanner(socket: any): Promise { + log.info('Reading minitouch banner') + + const parser = new Parser(socket) + const banner: Banner = { + pid: -1, + version: 0, + maxContacts: 0, + maxX: 0, + maxY: 0, + maxPressure: 0 + } + + const readVersion = async (): Promise => { + const chunk = await parser.readLine() + const args = chunk.toString().split(/ /g) + switch (args[0]) { + case 'v': + banner.version = Number(args[1]) + break + default: + throw new Error(util.format('Unexpected output "%s", expecting version line', chunk)) + } + } + + const readLimits = async (): Promise => { + const chunk = await parser.readLine() + const args = chunk.toString().split(/ /g) + switch (args[0]) { + case '^': + banner.maxContacts = Number(args[1]) + banner.maxX = Number(args[2]) + banner.maxY = Number(args[3]) + banner.maxPressure = Number(args[4]) + break + default: + throw new Error(util.format('Unknown output "%s", expecting limits line', chunk)) + } + } + + const readPid = async (): Promise => { + const chunk = await parser.readLine() + const args = chunk.toString().split(/ /g) + switch (args[0]) { + case '$': + banner.pid = Number(args[1]) + break + default: + throw new Error(util.format('Unexpected output "%s", expecting pid line', chunk)) + } + } + + await readVersion() + await readLimits() + await readPid() + return banner + } + + private _readUnexpected(socket: any): void { + socket.on('readable', this.readableListener) + // We may already have data pending. + this.readableListener() + } + + private _readableListener(): void { + let chunk + while ((chunk = this.socket?.stream.read())) { + log.warn('Unexpected output from minitouch socket: %s', chunk) + } + } + + private _processWriteQueue(): void { + while (this.writeQueue.length > 0) { + const writer = this.writeQueue.shift() + writer?.call(this) + } + } + + private _write(chunk: string): void { + if (!this.socket?.stream) { + return + } + + // Handle backpressure + const canWrite = this.socket.stream.write(chunk) + if (!canWrite) { + log.warn('Socket buffer is full, experiencing backpressure') + } + } + + destroy(): void { + // Clean up all resources + if (this.splitStream) { + this.splitStream.removeAllListeners('data') + this.splitStream.destroy() + this.splitStream = null + } + + if (this.socket) { + this.socket.stream.removeListener('readable', this.readableListener) + this.socket.removeAllListeners() + } + + if (this.output) { + this.output.removeAllListeners() + } + + this.failCounter.removeAllListeners() + this.removeAllListeners() + this.writeQueue = [] + } +} + +export default syrup.serial() + .dependency(adb) + .dependency(router) + .dependency(minitouch) + .dependency(flags) + .define(async (options: TouchOptions, adb: Client, router: any, minitouch: MinitouchService, flags: any) => { + const startConsumer = async (): Promise => { + const origin = flags.get('forceTouchOrigin', 'top left') + log.info('Touch origin is %s', origin) + + const touchOrigins: Record = { + 'top left': { + x: (point: TouchPoint) => point.x, + y: (point: TouchPoint) => point.y + }, + // So far the only device we've seen exhibiting this behavior + // is Yoga Tablet 8. + 'bottom left': { + x: (point: TouchPoint) => 1 - point.y, + y: (point: TouchPoint) => point.x + } + } + + const touchConsumer = new TouchConsumer({ + // Usually the touch origin is the same as the display's origin, + // but sometimes it might not be. + origin: touchOrigins[origin] + }, options, adb, minitouch) + + // Use Promise.race with once() for cleaner event handling + touchConsumer.start() + + return Promise.race([ + new Promise((resolve) => { + touchConsumer.once('start', () => resolve(touchConsumer)) + }), + new Promise((_, reject) => { + touchConsumer.once('error', reject) + }) + ]) + } + + const touchConsumer = await startConsumer() + const queue = new SeqQueue(100, 4) + + touchConsumer.on('error', (err: Error) => { + log.fatal('Touch consumer had an error %s: %s', err?.message, err?.stack) + lifecycle.fatal() + }) + + router + .on(GestureStartMessage, (channel: any, message: any) => { + queue.start(message.seq) + }) + .on(GestureStopMessage, (channel: any, message: any) => { + queue.push(message.seq, () => { + queue.stop() + }) + }) + .on(TouchDownMessage, (channel: any, message: any) => { + queue.push(message.seq, () => { + touchConsumer.touchDown(message) + }) + }) + .on(TouchMoveMessage, (channel: any, message: any) => { + queue.push(message.seq, () => { + touchConsumer.touchMove(message) + }) + }) + .on(TouchUpMessage, (channel: any, message: any) => { + queue.push(message.seq, () => { + touchConsumer.touchUp(message) + }) + }) + .on(TouchCommitMessage, (channel: any, message: any) => { + queue.push(message.seq, () => { + touchConsumer.touchCommit() + }) + }) + .on(TouchResetMessage, (channel: any, message: any) => { + queue.push(message.seq, () => { + touchConsumer.touchReset() + }) + }) + + return touchConsumer + }) + diff --git a/lib/units/device/resources/minitouch.js b/lib/units/device/resources/minitouch.js deleted file mode 100644 index d9a0f8b26e..0000000000 --- a/lib/units/device/resources/minitouch.js +++ /dev/null @@ -1,75 +0,0 @@ -import util from 'util' -import fs from 'fs' -import Promise from 'bluebird' -import syrup from '@devicefarmer/stf-syrup' -import logger from '../../../util/logger.js' -import * as pathutil from '../../../util/pathutil.cjs' -import devutil from '../../../util/devutil.js' -import * as streamutil from '../../../util/streamutil.js' -import Resource from './util/resource.js' -import adb from '../support/adb.js' -import abi from '../support/abi.js' -export default syrup.serial() - .dependency(adb) - .dependency(abi) - .dependency(devutil) - .define(function(options, adb, abi, devutil) { - var log = logger.createLogger('device:resources:minitouch') - var resources = { - bin: new Resource({ - src: pathutil.requiredMatch(abi.all.map(function(supportedAbi) { - return pathutil.module(util.format('@devicefarmer/minitouch-prebuilt/prebuilt/%s/bin/minitouch%s', supportedAbi, abi.pie ? '' : '-nopie')) - })), - dest: [ - '/data/local/tmp/minitouch', - '/data/data/com.android.shell/minitouch' - ], - comm: 'minitouch', - mode: 0o755 - }) - } - async function removeResource(res) { - const out = await adb.getDevice(options.serial).shell(['rm', '-f', res.dest]) - await streamutil.readAll(out) - return res - } - async function pushResource(res) { - const transfer = await adb.getDevice(options.serial).push(res.src, res.dest, res.mode) - await transfer.waitForEnd() - return res - } - async function installResource(res) { - log.info('Installing "%s" as "%s"', res.src, res.dest) - async function checkExecutable(res) { - const stats = await adb.getDevice(options.serial).stat(res.dest) - return (stats.mode & fs.constants.S_IXUSR) === fs.constants.S_IXUSR - } - const removeResult = await removeResource(res) - const res2 = await pushResource(removeResult) - const ok = await checkExecutable(res2) - if (!ok) { - log.info('Pushed "%s" not executable, attempting fallback location', res.comm) - res.shift() - return installResource(res) - } - return res - } - function installAll() { - return Promise.all([ - installResource(resources.bin) - ]) - } - function stop() { - return devutil.killProcsByComm(resources.bin.comm, resources.bin.dest) - } - return stop() - .then(installAll) - .then(function() { - return { - bin: resources.bin.dest, - run: function(cmd) { - return adb.getDevice(options.serial).shell(util.format('exec %s%s', resources.bin.dest, cmd ? util.format(' %s', cmd) : '')) - } - } - }) - }) diff --git a/lib/units/device/resources/minitouch.ts b/lib/units/device/resources/minitouch.ts new file mode 100644 index 0000000000..fcb5cca49d --- /dev/null +++ b/lib/units/device/resources/minitouch.ts @@ -0,0 +1,96 @@ +import util from 'util' +import fs from 'fs' +import syrup from '@devicefarmer/stf-syrup' +import type {Client} from '@u4/adbkit' +import logger from '../../../util/logger.js' +import * as pathutil from '../../../util/pathutil.cjs' +import devutil from '../../../util/devutil.js' +import Resource from './util/resource.js' +import adb from '../support/adb.js' +import abi from '../support/abi.js' +import lifecycle from '../../../util/lifecycle.js' + +interface MinitouchOptions { + serial: string +} + +interface MinitouchResource { + bin: Resource +} + +interface MinitouchResult { + bin: string + run: (cmd?: string) => Promise +} + +export default syrup.serial() + .dependency(adb) + .dependency(abi) + .dependency(devutil) + .define(async (options: MinitouchOptions, adb: Client, abi: any, devutil: any): Promise => { + const log = logger.createLogger('device:resources:minitouch') + const resources: MinitouchResource = { + bin: new Resource({ + src: pathutil.requiredMatch(abi.all.map((supportedAbi: string) => + pathutil.module(util.format('@devicefarmer/minitouch-prebuilt/prebuilt/%s/bin/minitouch%s', supportedAbi, abi.pie ? '' : '-nopie')) + )), + dest: [ + '/data/local/tmp/minitouch', + '/data/data/com.android.shell/minitouch' + ], + comm: 'minitouch', + mode: 0o755 + }) + } + + const removeResource = async (res: Resource) => { + await adb.getDevice(options.serial).execOut(['rm', '-f', res.dest]) + } + + const pushResource = async (res: Resource) => { + const transfer = await adb.getDevice(options.serial).push(res.src, res.dest, res.mode) + await transfer.waitForEnd() + } + + const checkExecutable = async (res: Resource) => { + const stats = await adb.getDevice(options.serial).stat(res.dest) + return (stats.mode & fs.constants.S_IXUSR) === fs.constants.S_IXUSR + } + + const installResource = async (res: Resource): Promise => { + if (await checkExecutable(res)) return; + + log.info('Installing "%s" as "%s"', res.src, res.dest) + + await removeResource(res) + await pushResource(res) + const ok = await checkExecutable(res) + + if (!ok) { + log.error('Pushed "%s" not executable, attempting fallback location', res.comm) + res.shift() + return installResource(res) + } + } + + const plugin = { + bin: resources.bin.dest, + run: (cmd?: string) => + adb.getDevice(options.serial).shell(`exec ${resources.bin.dest} ${cmd || ''}`), + + stop: async () => { + const pid = (await adb.getDevice(options.serial).execOut('pidof minitouch')).toString().trim() + if (!pid?.length) return; + + log.info('Stopping minitouch process %s', pid) + return adb.getDevice(options.serial).execOut(['kill', '-9', pid]) + } + } + + lifecycle.observe(() => plugin.stop()) + + await plugin.stop() + await installResource(resources.bin) + + return plugin + }) \ No newline at end of file diff --git a/lib/units/ios-device/plugins/install.js b/lib/units/ios-device/plugins/install.js index 0d95efd686..a0f9e19338 100755 --- a/lib/units/ios-device/plugins/install.js +++ b/lib/units/ios-device/plugins/install.js @@ -1,7 +1,6 @@ import {v4 as uuidv4} from 'uuid' import syrup from '@devicefarmer/stf-syrup' import logger from '../../../util/logger.js' -import wire from '../../../wire/index.js' import wireutil from '../../../wire/util.js' import Promise from 'bluebird' import {exec} from 'child_process' @@ -10,7 +9,7 @@ import router from '../../base-device/support/router.js' import push from '../../base-device/support/push.js' import storage from '../../base-device/support/storage.js' import deviceutil from '../../../util/deviceutil.js' -import {InstallMessage} from '../../../wire/wire.js' +import {InstallMessage, UninstallIosMessage} from '../../../wire/wire.js' function execShellCommand(cmd) { return new Promise((resolve, reject) => { diff --git a/lib/util/failcounter.js b/lib/util/failcounter.js deleted file mode 100644 index f5d5a216d6..0000000000 --- a/lib/util/failcounter.js +++ /dev/null @@ -1,25 +0,0 @@ -import util from 'util' -import EventEmitter from 'eventemitter3' -function FailCounter(threshold, time) { - EventEmitter.call(this) - this.threshold = threshold - this.time = time - this.values = [] -} -util.inherits(FailCounter, EventEmitter) -FailCounter.prototype.inc = function() { - var now = Date.now() - while (this.values.length) { - if (now - this.values[0] >= this.time) { - this.values.shift() - } - else { - break - } - } - this.values.push(now) - if (this.values.length > this.threshold) { - this.emit('exceedLimit', this.threshold, this.time) - } -} -export default FailCounter diff --git a/lib/util/failcounter.ts b/lib/util/failcounter.ts new file mode 100644 index 0000000000..8ca6c20a97 --- /dev/null +++ b/lib/util/failcounter.ts @@ -0,0 +1,31 @@ +import EventEmitter from 'events' + +class FailCounter extends EventEmitter { + private threshold: number + private time: number + private values: number[] = [] + + constructor(threshold: number, time: number) { + super() + this.threshold = threshold + this.time = time + } + + inc(): void { + const now = Date.now() + while (this.values.length) { + if (now - this.values[0] >= this.time) { + this.values.shift() + } else { + break + } + } + this.values.push(now) + if (this.values.length > this.threshold) { + this.emit('exceedLimit', this.threshold, this.time) + } + } +} + +export default FailCounter + diff --git a/lib/util/riskystream.js b/lib/util/riskystream.js deleted file mode 100644 index 20b57a9001..0000000000 --- a/lib/util/riskystream.js +++ /dev/null @@ -1,46 +0,0 @@ -import util from 'util' -import Promise from 'bluebird' -import EventEmitter from 'eventemitter3' -function RiskyStream(stream) { - EventEmitter.call(this) - this.endListener = function() { - this.ended = true - this.stream.removeListener('end', this.endListener) - if (!this.expectingEnd) { - this.emit('unexpectedEnd') - } - this.emit('end') - }.bind(this) - this.stream = stream - .on('end', this.endListener) - this.expectingEnd = false - this.ended = false -} -util.inherits(RiskyStream, EventEmitter) -RiskyStream.prototype.end = function() { - this.expectEnd() - return this.stream.end() -} -RiskyStream.prototype.expectEnd = function() { - this.expectingEnd = true - return this -} -RiskyStream.prototype.waitForEnd = function() { - var stream = this.stream - var endListener - this.expectEnd() - return new Promise(function(resolve) { - if (stream.ended) { - return resolve(true) - } - stream.on('end', endListener = function() { - resolve(true) - }) - // Make sure we actually have a chance to get the 'end' event. - stream.resume() - }) - .finally(function() { - stream.removeListener('end', endListener) - }) -} -export default RiskyStream diff --git a/lib/util/riskystream.ts b/lib/util/riskystream.ts new file mode 100644 index 0000000000..b7f765b779 --- /dev/null +++ b/lib/util/riskystream.ts @@ -0,0 +1,56 @@ +import EventEmitter from 'events' + +class RiskyStream extends EventEmitter { + stream: NodeJS.ReadableStream | NodeJS.WritableStream | any + expectingEnd: boolean = false + ended: boolean = false + private endListener: () => void + + constructor(stream: NodeJS.ReadableStream | NodeJS.WritableStream | any) { + super() + + this.endListener = () => { + this.ended = true + this.stream.removeListener('end', this.endListener) + if (!this.expectingEnd) { + this.emit('unexpectedEnd') + } + this.emit('end') + } + + this.stream = stream.on('end', this.endListener) + } + + end(): any { + this.expectEnd() + return this.stream.end() + } + + expectEnd(): this { + this.expectingEnd = true + return this + } + + async waitForEnd(): Promise { + const stream = this.stream + this.expectEnd() + + return new Promise((resolve) => { + if (stream.ended) { + return resolve(true) + } + + const endListener = () => { + stream.removeListener('end', endListener) + resolve(true) + } + + stream.on('end', endListener) + // Make sure we actually have a chance to get the 'end' event. + stream.resume() + }) + } +} + +export default RiskyStream + From 0805b9b785e04fe017ec235d8cc11043f49726e5 Mon Sep 17 00:00:00 2001 From: Elmir Khalilov <52529096+e-khalilov@users.noreply.github.com> Date: Tue, 18 Nov 2025 17:54:31 +0300 Subject: [PATCH 15/23] Sleep / wake keyevents (#390) * Develop to Master for 1.5.1 (#364) * Wire initial typescript (#338) * Wire initial typescript * Fix types * Something works * Finishing touches * Linter fixes * Build problems * hotfix (#361) Co-authored-by: e.khalilov * Remove all DB connections from device side (#368) * hotfix imei * Decrease apt install list in dockerfile (#365) * remove gm and jdk11 * remove libs * Remove console-feed & react dependencies [backend only] (#366) * remove dep console-feed * minor fix * linter fix --------- Co-authored-by: e.khalilov * fix default quotas hierarchy QA-19255 (#367) * -fix quotas -fix lock -fix test -fix lint * -fix test --------- Co-authored-by: a.chistov * dev units without db conn * fix types * minor fix * minor fix * minor fix * remove useless sockets --------- Co-authored-by: Maksim Alzhanov Co-authored-by: Maxim Co-authored-by: Daniil <8039921+DaniilSmirnov@users.noreply.github.com> Co-authored-by: e.khalilov Co-authored-by: Alexey Chistov <33050834+Alk2017@users.noreply.github.com> Co-authored-by: a.chistov * fix device type & types (#374) Co-authored-by: e.khalilov * reconnect in ADBObserver (#376) Co-authored-by: e.khalilov * QA-10057 Remove users watcher groups-engine (#369) * -fix after rebase -remove user watcher -user handler for alert message default quotas handler -fix merge del handler update quoats handler user handler method some fix -move without change method of devices to separate class -fix after move users method -move db.users method to separate class * add missing db method --------- Co-authored-by: a.chistov Co-authored-by: e.khalilov * fix heartbeat (#377) Co-authored-by: e.khalilov * overrides dependencies (#378) Co-authored-by: e.khalilov * Automatic removal of disabled devices (#379) * clear dead devices * minor fix * revert api edits * revert ui (types) edits --------- Co-authored-by: e.khalilov * fix linkifyjs issue (#380) Co-authored-by: e.khalilov * Resolve conflicts (#381) * Hotfix: device imei issue (#371) * hotfix * minor fix * fix types * minor fix --------- Co-authored-by: e.khalilov * increase ram for v8 (#372) * hotfix (#373) Co-authored-by: e.khalilov * hotfix ADBObserver (#375) Co-authored-by: e.khalilov --------- Co-authored-by: e.khalilov Co-authored-by: Daniil <8039921+DaniilSmirnov@users.noreply.github.com> * Fixed transactions & minor optimizations (#382) * minor edits * fix transactions * minor fix * fix ts types --------- Co-authored-by: e.khalilov * Fix lint issues --------- Co-authored-by: Maxim Co-authored-by: Elmir Khalilov <52529096+e-khalilov@users.noreply.github.com> Co-authored-by: e.khalilov Co-authored-by: Maksim Alzhanov Co-authored-by: Alexey Chistov <33050834+Alk2017@users.noreply.github.com> Co-authored-by: a.chistov * Hotfix: Fix broken import in ios Co-authored-by: e.khalilov * fix vulnerable deps (#384) * sleep/wake event & fs checking fix * fix ports slicing --------- Co-authored-by: Daniil <8039921+DaniilSmirnov@users.noreply.github.com> Co-authored-by: Maxim Co-authored-by: e.khalilov Co-authored-by: Maksim Alzhanov Co-authored-by: Alexey Chistov <33050834+Alk2017@users.noreply.github.com> Co-authored-by: a.chistov --- lib/units/device/plugins/group.ts | 4 +- lib/units/device/resources/minitouch.ts | 14 +- lib/units/provider/index.ts | 33 ++- package-lock.json | 72 +----- package.json | 3 +- test/e2e/Dockerfile | 2 +- test/e2e/package-lock.json | 24 +- test/e2e/package.json | 2 +- ui/package-lock.json | 303 +++++++++++++++--------- ui/package.json | 8 +- 10 files changed, 242 insertions(+), 223 deletions(-) diff --git a/lib/units/device/plugins/group.ts b/lib/units/device/plugins/group.ts index c628918805..bdc5ef8b9f 100644 --- a/lib/units/device/plugins/group.ts +++ b/lib/units/device/plugins/group.ts @@ -31,7 +31,7 @@ export default syrup.serial() group.on('join', () => { service.freezeRotation(0) - service.wake() + service.sendCommand('input keyevent 224') // KEYCODE_WAKEUP service.acquireWakeLock() }) @@ -54,7 +54,7 @@ export default syrup.serial() service.sendCommand('settings put system screen_brightness_mode 0'), service.sendCommand('settings put system screen_brightness 0'), service.setMasterMute(true), - service.sendCommand('input keyevent 26'), + service.sendCommand('input keyevent 223'), // KEYCODE_SLEEP service.sendCommand('settings put global http_proxy :0'), service.sendCommand('pm clear com.android.chrome'), service.sendCommand('pm clear com.chrome.beta'), diff --git a/lib/units/device/resources/minitouch.ts b/lib/units/device/resources/minitouch.ts index fcb5cca49d..f6c529c233 100644 --- a/lib/units/device/resources/minitouch.ts +++ b/lib/units/device/resources/minitouch.ts @@ -53,8 +53,12 @@ export default syrup.serial() } const checkExecutable = async (res: Resource) => { - const stats = await adb.getDevice(options.serial).stat(res.dest) - return (stats.mode & fs.constants.S_IXUSR) === fs.constants.S_IXUSR + try { + const stats = await adb.getDevice(options.serial).stat(res.dest) + return (stats.mode & fs.constants.S_IXUSR) === fs.constants.S_IXUSR + } catch (err: any) { + return false + } } const installResource = async (res: Resource): Promise => { @@ -65,7 +69,7 @@ export default syrup.serial() await removeResource(res) await pushResource(res) const ok = await checkExecutable(res) - + if (!ok) { log.error('Pushed "%s" not executable, attempting fallback location', res.comm) res.shift() @@ -75,7 +79,7 @@ export default syrup.serial() const plugin = { bin: resources.bin.dest, - run: (cmd?: string) => + run: (cmd?: string) => adb.getDevice(options.serial).shell(`exec ${resources.bin.dest} ${cmd || ''}`), stop: async () => { @@ -93,4 +97,4 @@ export default syrup.serial() await installResource(resources.bin) return plugin - }) \ No newline at end of file + }) diff --git a/lib/units/provider/index.ts b/lib/units/provider/index.ts index 86f55abb54..56e63cc5b9 100644 --- a/lib/units/provider/index.ts +++ b/lib/units/provider/index.ts @@ -17,6 +17,8 @@ interface DeviceWorker { resolveRegister?: () => void register: Promise waitingTimeoutTimer?: NodeJS.Timeout + ports: number[] + delete: () => void } export interface Options { @@ -145,16 +147,17 @@ export default (async function(options: Options) { return } - let allocatedPorts = ports.splice(0, 4) - - const proc = options.fork(device, allocatedPorts) + const proc = options.fork(device, workers[device.serial].ports) log.info('Spawned a device worker') - const exitListener = (code?: number, signal?: string) => { - ports.push(...allocatedPorts) + const cleanup = () => { proc.removeAllListeners('exit') proc.removeAllListeners('error') proc.removeAllListeners('message') + } + + const exitListener = (code?: number, signal?: string) => { + cleanup() if (signal) { log.warn('Device worker "%s" was killed with signal %s, assuming deliberate action and not restarting', device.serial, signal) @@ -175,6 +178,7 @@ export default (async function(options: Options) { if (!workers[device.serial]) { procutil.gracefullyKill(proc, options.killTimeout) onError(new Error('Device has been killed')) + cleanup() } workers[device.serial].terminate = () => exitListener(0) @@ -199,11 +203,7 @@ export default (async function(options: Options) { return { kill: () => { - // Return used ports to the main pool - ports.push(...allocatedPorts) - proc.removeAllListeners('exit') - proc.removeAllListeners('error') - proc.removeAllListeners('message') + cleanup() log.info('Gracefully killing device worker "%s"', device.serial) return procutil.gracefullyKill(proc, options.killTimeout) @@ -269,7 +269,8 @@ export default (async function(options: Options) { // Worker stop workers[device.serial].terminate = async() => { resolveRegister() - delete workers[device.serial] + + workers[device.serial].delete() await worker?.kill?.() // if process exited - no effect log.info('Cleaning up device worker "%s"', device.serial) @@ -317,7 +318,12 @@ export default (async function(options: Options) { state: 'waiting', time: Date.now(), terminate: () => {}, - register: register(device) // Register device immediately, before 'running' state + register: register(device), // Register device immediately, before 'running' state + ports: ports.splice(0, 2), + delete: () => { + ports.push(...workers[device.serial].ports) + delete workers[device.serial] + } } stats() @@ -357,7 +363,8 @@ export default (async function(options: Options) { log.info('Disconnect device "%s" [%s]', device.serial, device.type) clearTimeout(workers[device.serial]?.waitingTimeoutTimer) await stop(device) - delete workers[device.serial] + + workers[device.serial].delete() })) tracker.start() diff --git a/package-lock.json b/package-lock.json index 6139152d62..bb8bed333a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,7 +50,7 @@ "express-validator": "4.3.0", "fast-printf": "^1.6.10", "file-saver": "1.3.3", - "follow-redirects": "1.15.5", + "follow-redirects": "1.15.6", "formidable": "1.2.6", "gm": "1.25.1", "http-proxy": "1.18.1", @@ -117,7 +117,6 @@ "devDependencies": { "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.33.0", - "@playwright/test": "^1.52.0", "@types/bluebird": "^3.5.42", "@types/chalk": "^0.4.31", "@types/eventemitter3": "^1.2.0", @@ -2966,22 +2965,6 @@ "node": ">=14" } }, - "node_modules/@playwright/test": { - "version": "1.54.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.1.tgz", - "integrity": "sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright": "1.54.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/@postman/form-data": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@postman/form-data/-/form-data-3.1.1.tgz", @@ -8372,9 +8355,9 @@ "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", @@ -8545,21 +8528,6 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "license": "ISC" }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/ftp-response-parser": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ftp-response-parser/-/ftp-response-parser-1.0.1.tgz", @@ -11384,38 +11352,6 @@ "node": ">= 14.15.0" } }, - "node_modules/playwright": { - "version": "1.54.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.1.tgz", - "integrity": "sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.54.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.54.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.1.tgz", - "integrity": "sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/please-upgrade-node": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", diff --git a/package.json b/package.json index f31f526540..6ed1237c03 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "express-validator": "4.3.0", "fast-printf": "^1.6.10", "file-saver": "1.3.3", - "follow-redirects": "1.15.5", + "follow-redirects": "1.15.6", "formidable": "1.2.6", "gm": "1.25.1", "http-proxy": "1.18.1", @@ -135,7 +135,6 @@ "devDependencies": { "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.33.0", - "@playwright/test": "^1.52.0", "@types/bluebird": "^3.5.42", "@types/chalk": "^0.4.31", "@types/eventemitter3": "^1.2.0", diff --git a/test/e2e/Dockerfile b/test/e2e/Dockerfile index 3e915f2153..b9107285c0 100644 --- a/test/e2e/Dockerfile +++ b/test/e2e/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/playwright:v1.52.0-noble +FROM mcr.microsoft.com/playwright:v1.55.1-noble WORKDIR /tests diff --git a/test/e2e/package-lock.json b/test/e2e/package-lock.json index 34a0217df7..70fd01ac88 100644 --- a/test/e2e/package-lock.json +++ b/test/e2e/package-lock.json @@ -8,7 +8,7 @@ "name": "vk-devicehub-e2e-tests", "version": "0.0.1", "dependencies": { - "@playwright/test": "^1.52.0", + "@playwright/test": "^1.55.1", "typescript": "^5.5.3" }, "devDependencies": { @@ -230,12 +230,12 @@ } }, "node_modules/@playwright/test": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz", - "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", + "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", "license": "Apache-2.0", "dependencies": { - "playwright": "1.52.0" + "playwright": "1.56.1" }, "bin": { "playwright": "cli.js" @@ -2324,12 +2324,12 @@ } }, "node_modules/playwright": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", - "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", + "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.52.0" + "playwright-core": "1.56.1" }, "bin": { "playwright": "cli.js" @@ -2342,9 +2342,9 @@ } }, "node_modules/playwright-core": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz", - "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", + "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" diff --git a/test/e2e/package.json b/test/e2e/package.json index df6949660c..f81cd67ae7 100644 --- a/test/e2e/package.json +++ b/test/e2e/package.json @@ -9,7 +9,7 @@ "scripts": { }, "dependencies": { - "@playwright/test": "^1.52.0", + "@playwright/test": "^1.55.1", "typescript": "^5.5.3" }, "devDependencies": { diff --git a/ui/package-lock.json b/ui/package-lock.json index 05631b6170..ad744371f5 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -16,7 +16,7 @@ "@tanstack/react-virtual": "^3.10.8", "@vkontakte/icons": "^2.148.0", "@vkontakte/vkui": "^7.1.2", - "axios": "^1.11.0", + "axios": "^1.12.0", "classnames": "^2.5.1", "console-feed": "^3.8.0", "date-fns": "^4.1.0", @@ -55,8 +55,8 @@ "@types/react-slider": "^1.3.6", "@types/wicg-file-system-access": "^2023.10.5", "@vitejs/plugin-react-swc": "^3.5.0", - "@vitest/coverage-v8": "^3.0.9", - "@vitest/ui": "^3.0.9", + "@vitest/coverage-v8": "^3.2.4", + "@vitest/ui": "^3.2.4", "eslint": "^9.11.1", "eslint-import-resolver-typescript": "^3.6.3", "eslint-plugin-fp": "^2.3.0", @@ -83,7 +83,7 @@ "typescript": "^5.5.3", "typescript-eslint": "^8.16.0", "typescript-plugin-css-modules": "^5.1.0", - "vite": "^6.2.2", + "vite": "^6.4.1", "vite-plugin-svgr": "^4.2.0", "vite-tsconfig-paths": "^5.0.1", "vitest": "^3.0.9" @@ -2887,9 +2887,9 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -4021,9 +4021,9 @@ } }, "node_modules/@polka/url": { - "version": "1.0.0-next.28", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", - "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==", + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", "dev": true, "license": "MIT" }, @@ -5588,6 +5588,17 @@ "license": "MIT", "peer": true }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", @@ -5595,6 +5606,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/es-aggregate-error": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/es-aggregate-error/-/es-aggregate-error-1.0.6.tgz", @@ -5968,22 +5986,23 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.9.tgz", - "integrity": "sha512-15OACZcBtQ34keIEn19JYTVuMFTlFrClclwWjHo/IRPg/8ELpkgNTl0o7WLP9WO9XGH6+tip9CPYtEOrIDJvBA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^1.0.2", - "debug": "^4.4.0", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.1.7", "magic-string": "^0.30.17", "magicast": "^0.3.5", - "std-env": "^3.8.0", + "std-env": "^3.9.0", "test-exclude": "^7.0.1", "tinyrainbow": "^2.0.0" }, @@ -5991,8 +6010,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "3.0.9", - "vitest": "3.0.9" + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -6001,14 +6020,15 @@ } }, "node_modules/@vitest/expect": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.9.tgz", - "integrity": "sha512-5eCqRItYgIML7NNVgJj6TVCmdzE7ZVgJhruW0ziSQV4V7PvLkDL1bBkBdcTs/VuIz0IxPb5da1IDSqc1TR9eig==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.0.9", - "@vitest/utils": "3.0.9", + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -6017,13 +6037,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.9.tgz", - "integrity": "sha512-ryERPIBOnvevAkTq+L1lD+DTFBRcjueL9lOUfXsLfwP92h4e+Heb+PjiqS3/OURWPtywfafK0kj++yDFjWUmrA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.0.9", + "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -6032,7 +6052,7 @@ }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0" + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "peerDependenciesMeta": { "msw": { @@ -6054,9 +6074,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.9.tgz", - "integrity": "sha512-OW9F8t2J3AwFEwENg3yMyKWweF7oRJlMyHOMIhO5F3n0+cgQAJZBjNgrF8dLwFTEXl5jUqBLXd9QyyKv8zEcmA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", "dev": true, "license": "MIT", "dependencies": { @@ -6067,27 +6087,28 @@ } }, "node_modules/@vitest/runner": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.9.tgz", - "integrity": "sha512-NX9oUXgF9HPfJSwl8tUZCMP1oGx2+Sf+ru6d05QjzQz4OwWg0psEzwY6VexP2tTHWdOkhKHUIZH+fS6nA7jfOw==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.0.9", - "pathe": "^2.0.3" + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.9.tgz", - "integrity": "sha512-AiLUiuZ0FuA+/8i19mTYd+re5jqjEc2jZbgJ2up0VY0Ddyyxg/uUtBDpIFAy4uzKaQxOW8gMgBdAJJ2ydhu39A==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.0.9", + "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -6096,49 +6117,49 @@ } }, "node_modules/@vitest/spy": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.9.tgz", - "integrity": "sha512-/CcK2UDl0aQ2wtkp3YVWldrpLRNCfVcIOFGlVGKO4R5eajsH393Z1yiXLVQ7vWsj26JOEjeZI0x5sm5P4OGUNQ==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", "dev": true, "license": "MIT", "dependencies": { - "tinyspy": "^3.0.2" + "tinyspy": "^4.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/ui": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-3.0.9.tgz", - "integrity": "sha512-FpZD4aIv/qNpwkV3XbLV6xldWFHMgoNWAJEgg5GmpObmAOLAErpYjew9dDwXdYdKOS3iZRKdwI+P3JOJcYeUBg==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-3.2.4.tgz", + "integrity": "sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.0.9", + "@vitest/utils": "3.2.4", "fflate": "^0.8.2", "flatted": "^3.3.3", "pathe": "^2.0.3", "sirv": "^3.0.1", - "tinyglobby": "^0.2.12", + "tinyglobby": "^0.2.14", "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "3.0.9" + "vitest": "3.2.4" } }, "node_modules/@vitest/utils": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.9.tgz", - "integrity": "sha512-ilHM5fHhZ89MCp5aAaM9uhfl1c2JdxVxl3McqsdVyVNN6JffnEen8UMCdRTzOhGXNQGo5GNL9QugHrz727Wnng==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.0.9", - "loupe": "^3.1.3", + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" }, "funding": { @@ -6618,6 +6639,35 @@ "dev": true, "license": "MIT" }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.8.tgz", + "integrity": "sha512-szgSZqUxI5T8mLKvS7WTjF9is+MVbOeLADU73IseOcrqhxr/VAvy6wfoVE39KnKzA7JRhjF5eUagNlHwvZPlKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^9.0.1" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -6719,9 +6769,9 @@ } }, "node_modules/axios": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", - "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.0.tgz", + "integrity": "sha512-zt40Pz4zcRXra9CVV31KeyofwiNvAbJ5B6YPz9pMJ+yOSLikvPT4Yi5LjfgjRa9CawVYBaD1JQzIVcIvBejKeA==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -7098,9 +7148,9 @@ } }, "node_modules/chai": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", - "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", "dev": true, "license": "MIT", "dependencies": { @@ -7111,7 +7161,7 @@ "pathval": "^2.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/chalk": { @@ -7753,9 +7803,9 @@ } }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -8392,9 +8442,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", - "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", "dev": true, "license": "MIT" }, @@ -9176,9 +9226,9 @@ } }, "node_modules/expect-type": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.0.tgz", - "integrity": "sha512-80F22aiJ3GLyVnS/B3HzgR6RelZVumzj9jkL0Rhz4h0xYbNW9PjlQz5h3J/SShErbXBc295vseR4/MIbVmUbeA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -11834,9 +11884,9 @@ } }, "node_modules/loupe": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", - "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", "dev": true, "license": "MIT" }, @@ -13313,9 +13363,9 @@ "license": "MIT" }, "node_modules/pathval": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", - "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", "dev": true, "license": "MIT", "engines": { @@ -15371,9 +15421,9 @@ } }, "node_modules/sirv": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz", - "integrity": "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", "dev": true, "license": "MIT", "dependencies": { @@ -15537,9 +15587,9 @@ } }, "node_modules/std-env": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.1.tgz", - "integrity": "sha512-vj5lIj3Mwf9D79hBkltk5qmkFI+biIKWS2IBxEyEU3AX1tUf7AoL8nSazCOiiqQsGKIq01SClsKEzweu34uwvA==", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", "dev": true, "license": "MIT" }, @@ -15814,6 +15864,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/stylelint": { "version": "16.15.0", "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.15.0.tgz", @@ -16388,9 +16458,9 @@ } }, "node_modules/tinypool": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", - "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", "dev": true, "license": "MIT", "engines": { @@ -16408,9 +16478,9 @@ } }, "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", "dev": true, "license": "MIT", "engines": { @@ -16945,9 +17015,9 @@ } }, "node_modules/vite": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", - "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", "dependencies": { @@ -17020,17 +17090,17 @@ } }, "node_modules/vite-node": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.9.tgz", - "integrity": "sha512-w3Gdx7jDcuT9cNn9jExXgOyKmf5UOTb6WMHz8LGAm54eS1Elf5OuBhCxl6zJxGhEeIkgsE1WbHuoL0mj/UXqXg==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", "dev": true, "license": "MIT", "dependencies": { "cac": "^6.7.14", - "debug": "^4.4.0", - "es-module-lexer": "^1.6.0", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0" + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" @@ -17099,31 +17169,34 @@ } }, "node_modules/vitest": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.9.tgz", - "integrity": "sha512-BbcFDqNyBlfSpATmTtXOAOj71RNKDDvjBM/uPfnxxVGrG+FSH2RQIwgeEngTaTkuU/h0ScFvf+tRcKfYXzBybQ==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "3.0.9", - "@vitest/mocker": "3.0.9", - "@vitest/pretty-format": "^3.0.9", - "@vitest/runner": "3.0.9", - "@vitest/snapshot": "3.0.9", - "@vitest/spy": "3.0.9", - "@vitest/utils": "3.0.9", + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", "chai": "^5.2.0", - "debug": "^4.4.0", - "expect-type": "^1.1.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", - "std-env": "^3.8.0", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", - "tinypool": "^1.0.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.0.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "bin": { @@ -17139,8 +17212,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.0.9", - "@vitest/ui": "3.0.9", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, diff --git a/ui/package.json b/ui/package.json index de59eb82dc..ac22e5449a 100644 --- a/ui/package.json +++ b/ui/package.json @@ -36,7 +36,7 @@ "@tanstack/react-virtual": "^3.10.8", "@vkontakte/icons": "^2.148.0", "@vkontakte/vkui": "^7.1.2", - "axios": "^1.11.0", + "axios": "^1.12.0", "classnames": "^2.5.1", "console-feed": "^3.8.0", "date-fns": "^4.1.0", @@ -75,8 +75,8 @@ "@types/react-slider": "^1.3.6", "@types/wicg-file-system-access": "^2023.10.5", "@vitejs/plugin-react-swc": "^3.5.0", - "@vitest/coverage-v8": "^3.0.9", - "@vitest/ui": "^3.0.9", + "@vitest/coverage-v8": "^3.2.4", + "@vitest/ui": "^3.2.4", "eslint": "^9.11.1", "eslint-import-resolver-typescript": "^3.6.3", "eslint-plugin-fp": "^2.3.0", @@ -103,7 +103,7 @@ "typescript": "^5.5.3", "typescript-eslint": "^8.16.0", "typescript-plugin-css-modules": "^5.1.0", - "vite": "^6.2.2", + "vite": "^6.4.1", "vite-plugin-svgr": "^4.2.0", "vite-tsconfig-paths": "^5.0.1", "vitest": "^3.0.9" From 56195388e1f27ad5571200b1e11bd5151818f3f8 Mon Sep 17 00:00:00 2001 From: Elmir Khalilov <52529096+e-khalilov@users.noreply.github.com> Date: Thu, 27 Nov 2025 20:13:00 +0300 Subject: [PATCH 16/23] Health checking of devices in provider (#391) Co-authored-by: e.khalilov --- lib/units/provider/ADBObserver.ts | 394 +++++++++++++++++++++++++++--- lib/units/provider/index.ts | 203 ++++++++++++--- 2 files changed, 526 insertions(+), 71 deletions(-) diff --git a/lib/units/provider/ADBObserver.ts b/lib/units/provider/ADBObserver.ts index dfa1fbab77..7ef556a93f 100644 --- a/lib/units/provider/ADBObserver.ts +++ b/lib/units/provider/ADBObserver.ts @@ -1,11 +1,12 @@ import EventEmitter from 'events' import net, {Socket} from 'net' -export type ADBDeviceType = 'unknown' | 'bootloader' | 'device' | 'recovery' | 'sideload' | 'offline' | 'unauthorized' | 'unknown' // https://android.googlesource.com/platform/system/core/+/android-4.4_r1/adb/adb.c#394 +export type ADBDeviceType = 'unknown' | 'bootloader' | 'device' | 'recovery' | 'sideload' | 'offline' | 'unauthorized' // https://android.googlesource.com/platform/system/core/+/android-4.4_r1/adb/adb.c#394 interface ADBDevice { serial: string type: ADBDeviceType + isStuck: boolean reconnect: () => Promise } @@ -16,46 +17,72 @@ interface ADBDeviceEntry { type PrevADBDeviceType = ADBDevice['type'] +interface DeviceHealthCheck { + attempts: number + timeout: number + firstFailureTime: number + lastAttemptTime: number +} + interface ADBEvents { connect: [ADBDevice] update: [ADBDevice, PrevADBDeviceType] disconnect: [ADBDevice] + stuck: [ADBDevice, DeviceHealthCheck] + healthcheck: [{ ok: number, bad: number }] error: [Error] } +const isOnline = (type: string) => ['device', 'emulator'].includes(type) + class ADBObserver extends EventEmitter { static instance: ADBObserver | null = null private readonly intervalMs: number = 1000 // Default 1 second polling + private readonly healthCheckIntervalMs: number = 10000 // Default 1 minute health check + private readonly maxHealthCheckAttempts: number = 3 + private readonly host: string = 'localhost' private readonly port: number = 5037 private devices: Map = new Map() + private deviceHealthAttempts: Map = new Map() + private pollTimeout: NodeJS.Timeout | null = null - private isPolling: boolean = false - private isDestroyed: boolean = false - private shouldContinuePolling: boolean = false + private healthCheckTimeout: NodeJS.Timeout | null = null + + private readonly requestTimeoutMs: number = 5000 // 5 second timeout per request + private readonly initialReconnectDelayMs: number = 100 + private readonly maxReconnectAttempts: number = 8 + + private reconnectAttempt: number = 0 + private connection: Socket | null = null - private isConnecting: boolean = false private requestQueue: Array<{ command: string resolve: (value: string) => void reject: (error: Error) => void + needData: boolean + isRawStream?: boolean // For device commands after transport (shell:, logcat:, etc.) timer?: NodeJS.Timeout // Set when request is in-flight + rawStreamBuffer?: Buffer // Accumulated data for raw stream responses + rawStreamStarted?: boolean // True once OKAY received and we're accumulating raw stream data }> = [] - private readonly requestTimeoutMs: number = 5000 // 5 second timeout per request - private readonly maxReconnectAttempts: number = 8 - private readonly initialReconnectDelayMs: number = 100 - private reconnectAttempt: number = 0 + + private shouldContinuePolling: boolean = false + private isPolling: boolean = false + private isDestroyed: boolean = false + private isConnecting: boolean = false private isReconnecting: boolean = false - constructor(options?: {intervalMs?: number; host?: string; port?: number}) { + constructor(options?: {intervalMs?: number; healthCheckIntervalMs?: number; host?: string; port?: number}) { if (ADBObserver.instance) { return ADBObserver.instance } super() this.intervalMs = options?.intervalMs || this.intervalMs + this.healthCheckIntervalMs = options?.healthCheckIntervalMs || this.healthCheckIntervalMs this.host = options?.host || this.host this.port = options?.port || this.port @@ -80,6 +107,7 @@ class ADBObserver extends EventEmitter { this.pollDevices() this.scheduleNextPoll() + this.scheduleNextHealthCheck() } /** @@ -91,6 +119,10 @@ class ADBObserver extends EventEmitter { clearTimeout(this.pollTimeout) this.pollTimeout = null } + if (this.healthCheckTimeout) { + clearTimeout(this.healthCheckTimeout) + this.healthCheckTimeout = null + } this.closeConnection() ADBObserver.instance = null } @@ -99,6 +131,7 @@ class ADBObserver extends EventEmitter { this.isDestroyed = true this.stop() this.devices.clear() + this.deviceHealthAttempts.clear() this.removeAllListeners() } @@ -126,7 +159,6 @@ class ADBObserver extends EventEmitter { const currentSerials = new Set(currentDevices.map(d => d.serial)) const previousSerials = new Set(this.devices.keys()) - // Find new devices (connect events) for (const deviceEntry of currentDevices) { const existingDevice = this.devices.get(deviceEntry.serial) @@ -140,6 +172,11 @@ class ADBObserver extends EventEmitter { // Device state changed (update event) const oldType = existingDevice.type existingDevice.type = deviceEntry.state as ADBDevice['type'] + + if (isOnline(existingDevice.type)) { + existingDevice.isStuck = false + } + this.emit('update', existingDevice, oldType) } } @@ -149,6 +186,7 @@ class ADBObserver extends EventEmitter { if (!currentSerials.has(serial)) { const device = this.devices.get(serial)! this.devices.delete(serial) + this.deviceHealthAttempts.delete(serial) // Clean up health check tracking this.emit('disconnect', device) } } @@ -178,6 +216,111 @@ class ADBObserver extends EventEmitter { }, this.intervalMs) } + /** + * Schedule the next health check cycle + */ + private scheduleNextHealthCheck(): void { + if (this.isDestroyed) { + return + } + + this.healthCheckTimeout = setTimeout(async() => { + await this.performHealthChecks() + + if (!this.isDestroyed) { + this.scheduleNextHealthCheck() + } + }, this.healthCheckIntervalMs) + } + + /** + * Perform health checks on all tracked devices using getprop command + */ + private async performHealthChecks(): Promise { + if (this.isDestroyed || this.devices.size === 0) { + return + } + + try { + let now = 0, + ok = 0, + bad = 0 + + // Check each tracked device + for (const [serial, device] of this.devices.entries()) { + if (this.isDestroyed || !this.shouldContinuePolling) { + break + } + + if (device.isStuck || !isOnline(device.type)) { + continue + } + + now = Date.now() + + try { + // Use shell command to check if device is responsive + // This is more reliable than get-state + // sendADBCommand already has a timeout (requestTimeoutMs) + const res = await this.sendADBCommand('shell:getprop ro.build.version.sdk', serial) + + console.log('RESPONSE:\n', res, '\n', typeof res, res?.length) + // Device responded successfully - reset failure tracking + if (this.deviceHealthAttempts.has(serial)) { + this.deviceHealthAttempts.delete(serial) + } + + ok++ + } + catch (error: any) { + console.log(error) + // Device didn't respond - track failure and potentially reconnect + this.handleDeviceHealthCheckFailure(serial, device, now) + bad++ + } + } + + this.emit('healthcheck', { ok, bad }) + } + catch (error: any) { + this.emit('error', new Error(`Health check failed: ${error.message}`)) + } + } + + /** + * Handle health check failure with backoff and reconnection attempts + */ + private async handleDeviceHealthCheckFailure(serial: string, device: ADBDevice, now: number): Promise { + const attemptInfo = this.deviceHealthAttempts.get(serial) + + if (!attemptInfo) { + // First failure - initialize tracking + this.deviceHealthAttempts.set(serial, { + attempts: 1, + timeout: this.requestTimeoutMs, + firstFailureTime: now, + lastAttemptTime: now + }) + return + } + + attemptInfo.attempts++ + attemptInfo.lastAttemptTime = now + + if (attemptInfo.attempts >= this.maxHealthCheckAttempts) { + device.isStuck = true + this.devices.set(device.serial, device) + this.emit('stuck', device, attemptInfo) + + // Reset tracking for potential future recovery + this.deviceHealthAttempts.delete(serial) + + // Attempt reconnection (for network devices) + await device.reconnect() + return + } + } + private async getADBDevices(): Promise { try { const response = await this.sendADBCommand('host:devices') @@ -224,7 +367,13 @@ class ADBObserver extends EventEmitter { this.isConnecting = true return new Promise((resolve, reject) => { - const client = net.createConnection(this.port, this.host, () => { + const client = net.createConnection({ + port: this.port, + host: this.host, + noDelay: true, + keepAlive: true, + keepAliveInitialDelay: 30000 + }, () => { this.connection = client this.isConnecting = false this.reconnectAttempt = 0 // Reset reconnection counter on successful connection @@ -254,6 +403,22 @@ class ADBObserver extends EventEmitter { client.on('close', () => { this.connection = null + // Special handling for raw stream in progress - connection close means command completed + if (this.requestQueue.length > 0 && this.requestQueue[0].rawStreamStarted) { + const request = this.requestQueue.shift()! + if (request.timer) { + clearTimeout(request.timer) + } + const responseData = request.rawStreamBuffer!.toString('utf-8').trim() + request.resolve(responseData) + + // Process next request in queue (will reconnect if needed) + if (this.shouldContinuePolling && !this.isDestroyed) { + this.processNextRequest() + } + return + } + // Clear the timeout of in-flight request but keep it for potential retry if (this.requestQueue.length > 0 && this.requestQueue[0].timer) { clearTimeout(this.requestQueue[0].timer) @@ -266,8 +431,8 @@ class ADBObserver extends EventEmitter { } else { // Reject all queued requests (including in-flight one) - for (const {reject} of this.requestQueue) { - reject(new Error('Connection closed')) + for (const request of this.requestQueue) { + request.reject(new Error('Connection closed')) } this.requestQueue = [] } @@ -283,29 +448,136 @@ class ADBObserver extends EventEmitter { * Process ADB protocol responses and return remaining buffer */ private processADBResponses(buffer: Buffer): Buffer { + if (!this.requestQueue.length) { + return buffer + } + + const request = this.requestQueue[0]! let offset = 0 - while (offset < buffer.length) { - // Need at least 8 bytes for status (4) + length (4) - if (buffer.length - offset < 8) { - break + // Special handling for raw stream that's already started + // Once OKAY is received for raw stream, we only accumulate data (no more status codes) + if (request.rawStreamStarted) { + // Accumulate all data + if (buffer.length > 0) { + request.rawStreamBuffer = Buffer.concat([request.rawStreamBuffer || Buffer.alloc(0), buffer]) + + // Check if we have a complete line (newline detected) + // For commands like getprop that return single-line output, complete immediately + const bufferString = request.rawStreamBuffer.toString('utf-8') + if (bufferString.includes('\n')) { + if (this.requestQueue.length > 0 && this.requestQueue[0] === request) { + this.requestQueue.shift() + if (request.timer) { + clearTimeout(request.timer) + } + const responseData = bufferString.trim() + request.resolve(responseData) + + // After transport session, close connection for next device/command + this.closeConnectionAfterTransport() + + // Process next request in queue (will reconnect) + this.processNextRequest() + } + } + } + + return Buffer.alloc(0) // All data consumed + } + + // Check if we have at least status bytes + if (buffer.length < 4) { + return buffer + } + + const status = buffer.subarray(offset, offset + 4).toString('ascii') + + if (status === 'FAIL') { + // For FAIL responses, we always have length-prefixed error message + if (buffer.length < 8) { + return buffer // Need more data for length } - const status = buffer.subarray(offset, offset + 4).toString('ascii') const lengthHex = buffer.subarray(offset + 4, offset + 8).toString('ascii') const dataLength = parseInt(lengthHex, 16) - // Check if we have the complete response - if (buffer.length - offset < 8 + dataLength) { - break + if (buffer.length < 8 + dataLength) { + return buffer // Need more data for complete error message } - const responseData = buffer.subarray(offset + 8, offset + 8 + dataLength).toString('utf-8') + const errorMessage = buffer.subarray(offset + 8, offset + 8 + dataLength).toString('utf-8') + + if (this.requestQueue.length > 0) { + const request = this.requestQueue.shift()! + if (request.timer) { + clearTimeout(request.timer) + } + request.reject(new Error(errorMessage || 'ADB command failed')) + // Process next request in queue + this.processNextRequest() + } + + return buffer.subarray(offset + 8 + dataLength) + } + + if (status === 'OKAY') { + offset += 4 // Consume OKAY status + + // Handle different response types based on request + if (request.isRawStream) { + // For device commands after transport (shell:, logcat:, etc.) + // Response is: OKAY + raw unstructured stream (no length prefix) + + // Mark that we've started raw stream mode + // This prevents processing any further status codes for this request + request.rawStreamStarted = true + request.rawStreamBuffer = Buffer.alloc(0) + + // Accumulate any data that came with OKAY in this packet + if (buffer.length > offset) { + request.rawStreamBuffer = Buffer.concat([request.rawStreamBuffer, buffer.subarray(offset)]) + } + + // Check if we already have a complete line (newline detected) + const bufferString = request.rawStreamBuffer.toString('utf-8') + if (bufferString.includes('\n')) { + if (this.requestQueue.length > 0) { + this.requestQueue.shift() + if (request.timer) { + clearTimeout(request.timer) + } + const responseData = bufferString.trim() + request.resolve(responseData) + + // After transport session, close connection for next device/command + this.closeConnectionAfterTransport() + + // Process next request in queue (will reconnect if needed) + this.processNextRequest() + } + } + // If no newline yet, wait for more data (will be handled by rawStreamStarted check above) + + return Buffer.alloc(0) // All data consumed + } + else if (request.needData) { + // For host commands with length-prefixed data + if (buffer.length - offset < 4) { + return buffer.subarray(offset - 4) // Need more data for length, return including OKAY + } + + const lengthHex = buffer.subarray(offset, offset + 4).toString('ascii') + const dataLength = parseInt(lengthHex, 16) + + if (buffer.length - offset < 4 + dataLength) { + return buffer.subarray(offset - 4) // Need more data, return including OKAY + } + + const responseData = buffer.subarray(offset + 4, offset + 4 + dataLength).toString('utf-8') - if (status === 'OKAY') { - // Resolve the in-flight request (first in queue) if (this.requestQueue.length > 0) { - const request = this.requestQueue.shift()! + this.requestQueue.shift() if (request.timer) { clearTimeout(request.timer) } @@ -313,37 +585,60 @@ class ADBObserver extends EventEmitter { // Process next request in queue this.processNextRequest() } + + return buffer.subarray(offset + 4 + dataLength) } - else if (status === 'FAIL') { - // Reject the in-flight request (first in queue) + else { + // For commands that only expect OKAY (like host:transport:) if (this.requestQueue.length > 0) { - const request = this.requestQueue.shift()! + this.requestQueue.shift() if (request.timer) { clearTimeout(request.timer) } - request.reject(new Error(responseData || 'ADB command failed')) + request.resolve('') // Process next request in queue this.processNextRequest() } - } - offset += 8 + dataLength + return buffer.subarray(offset) + } } - // Return remaining incomplete data in buffer - return offset > 0 ? buffer.subarray(offset) : buffer + // Unknown status or need more data + return buffer } /** * Send command to ADB server using persistent connection * Requests are queued and processed sequentially */ - private async sendADBCommand(command: string): Promise { + private async sendADBCommand(command: string, host?: string): Promise { await this.ensureConnection() return new Promise((resolve, reject) => { - // Add request to the queue - this.requestQueue.push({command, resolve, reject}) + if (host) { + // First, switch to device transport mode + this.requestQueue.push({ + command: `host:transport:${host}`, + needData: false, + resolve: () => { + // After transport succeeds, socket is now a tunnel to device's adbd + // Device commands (shell:, logcat:, etc.) return raw streams, not length-prefixed data + this.requestQueue.push({ + command, + resolve, + reject, + needData: false, + isRawStream: true // Mark as raw stream response + }) + this.processNextRequest() + }, + reject + }) + } else { + // Host commands have length-prefixed responses + this.requestQueue.push({command, resolve, reject, needData: true}) + } // Try to process the queue if no request is currently in-flight this.processNextRequest() @@ -462,6 +757,21 @@ class ADBObserver extends EventEmitter { this.requestQueue = [] } + /** + * Close connection after transport session (device-specific command) + * This is necessary because after host:transport:, the socket becomes + * a dedicated tunnel to that device and cannot be reused for other commands + */ + private closeConnectionAfterTransport(): void { + if (this.connection && !this.connection.destroyed) { + this.connection.destroy() + this.connection = null + } + + // Don't reject queued requests - they will be processed with a new connection + // Don't reset reconnection state - let it continue if needed + } + /** * Close the persistent connection */ @@ -517,11 +827,12 @@ class ADBObserver extends EventEmitter { const device: ADBDevice = { serial: deviceEntry.serial, type: deviceEntry.state, + isStuck: false, reconnect: async(): Promise => { try { // Try to reconnect the device using ADB protocol (for network devices) // For USB devices, this might not be applicable - if (device.serial.includes(':')) { + if (device.serial.includes(':') && !this.isDestroyed) { if (this.devices.has(device.serial)) { try { await this.sendADBCommand(`host:disconnect:${device.serial}`) @@ -535,10 +846,13 @@ class ADBObserver extends EventEmitter { await new Promise(resolve => setTimeout(resolve, 1000)) const devices = await this.getADBDevices() - const reconnectedDevice = devices.find(d => d.serial === device.serial) + const reconnectedDevice = devices.find(d => + d.serial === device.serial + ) - if (reconnectedDevice && reconnectedDevice.state === 'device') { + if (reconnectedDevice && isOnline(reconnectedDevice.state)) { device.type = 'device' + device.isStuck = false return true } } diff --git a/lib/units/provider/index.ts b/lib/units/provider/index.ts index 56e63cc5b9..2f0266ffef 100644 --- a/lib/units/provider/index.ts +++ b/lib/units/provider/index.ts @@ -8,10 +8,10 @@ import srv from '../../util/srv.js' import * as zmqutil from '../../util/zmqutil.js' import {ChildProcess} from 'node:child_process' import ADBObserver, {ADBDevice} from './ADBObserver.js' -import { DeviceRegisteredMessage } from '../../wire/wire.ts' +import { DeviceRegisteredMessage } from '../../wire/wire.js' interface DeviceWorker { - state: 'waiting' | 'running' + state: 'waiting' | 'starting' | 'running' time: number terminate: () => Promise | void resolveRegister?: () => void @@ -40,6 +40,9 @@ export interface Options { export default (async function(options: Options) { const log = logger.createLogger('provider') + // Startup timeout for device workers + const STARTUP_TIMEOUT_MS = 10 * 60 * 1000 + // Check whether the ipv4 address contains a port indication if (options.adbHost.includes(':')) { log.error('Please specify adbHost without port') @@ -90,7 +93,7 @@ export default (async function(options: Options) { .on(DeviceRegisteredMessage, (channel, message: {serial: string}) => { if (workers[message.serial]?.resolveRegister) { workers[message.serial].resolveRegister!() - delete workers[message.serial]?.resolveRegister + delete workers[message.serial].resolveRegister } }) .handler() @@ -122,7 +125,23 @@ export default (async function(options: Options) { const stop = async(device: ADBDevice) => { if (workers[device.serial]) { log.info('Shutting down device worker "%s" [%s]', device.serial, device.type) - return workers[device.serial].terminate() + return workers[device.serial]?.terminate() + } + } + + const removeDevice = async(device: ADBDevice) => { + try { + log.info('Removing device %s', device.serial) + await stop(device) + } + catch (err) { + log.error('Error stopping device worker "%s": %s', device.serial, err) + } + finally { + // Always clean up and return ports, even if stop() fails + if (workers[device.serial]) { + workers[device.serial].delete() + } } } @@ -147,8 +166,8 @@ export default (async function(options: Options) { return } - const proc = options.fork(device, workers[device.serial].ports) - log.info('Spawned a device worker') + const proc = options.fork(device, [...workers[device.serial].ports]) + log.info('Spawned a device worker with ports [ %s ]', workers[device.serial].ports.join(', ')) const cleanup = () => { proc.removeAllListeners('exit') @@ -183,7 +202,7 @@ export default (async function(options: Options) { workers[device.serial].terminate = () => exitListener(0) const errorListener = (err: any) => { - log.error('Device worker "%s" had an error: %s', device.serial, err.message) + log.error('Device worker "%s" had an error: %s', device.serial, err?.message) onError(err) } @@ -217,6 +236,12 @@ export default (async function(options: Options) { } log.info('Starting to work for device "%s"', device.serial) + + if (workers[device.serial].state !== 'starting') { + workers[device.serial].state = 'starting' + workers[device.serial].time = Date.now() + } + let resolveReady: () => void let rejectReady: () => void @@ -227,26 +252,47 @@ export default (async function(options: Options) { const resolveRegister = () => { if (workers[device.serial]?.resolveRegister) { workers[device.serial].resolveRegister!() - delete workers[device.serial]?.resolveRegister + delete workers[device.serial].resolveRegister } } const handleError = async(err: any) => { - log.error('Failed start device worker "%s": %s', device.serial, err) + log.error('Device "%s" error: %s', device.serial, err) rejectReady() resolveRegister() + if (!workers[device.serial]) { + // Device was disconnected, don't restart + return + } + if (err instanceof procutil.ExitError) { log.error('Device worker "%s" died with code %s', device.serial, err.code) log.info('Restarting device worker "%s"', device.serial) await new Promise(r => setTimeout(r, 2000)) + if (!workers[device.serial]) { + log.info('Restart of device "%s" cancelled', device.serial) + return + } + work(device) return } } const worker = spawn(device, resolveReady!, handleError) + if (!worker) { + log.error('Device "%s" startup failed', device.serial) + + // Clean up worker and return ports + if (workers[device.serial]) { + resolveRegister() + workers[device.serial]?.delete() + } + + return + } try { await Promise.all([ @@ -258,7 +304,15 @@ export default (async function(options: Options) { ) ]) } - catch (e) { + catch (e: any) { + log.error('Device "%s" startup failed: %s', device.serial, e?.message) + + // Clean up worker and return ports + if (workers[device.serial]) { + resolveRegister() + await worker?.kill?.() + workers[device.serial]?.delete() + } return } @@ -270,17 +324,11 @@ export default (async function(options: Options) { workers[device.serial].terminate = async() => { resolveRegister() - workers[device.serial].delete() + workers[device.serial]?.delete() await worker?.kill?.() // if process exited - no effect log.info('Cleaning up device worker "%s"', device.serial) - // Tell others the device is gone - push.send([ - wireutil.global, - wireutil.envelope(new wire.DeviceAbsentMessage(device.serial)) - ]) - stats() // Wait while DeviceAbsentMessage processed on app side (1s) @@ -304,6 +352,43 @@ export default (async function(options: Options) { port: options.adbPort, host: options.adbHost }) + + // Worker health monitoring - check for stuck workers + const checkWorkerHealth = async() => { + const now = Date.now() + const stuckWorkers: string[] = [] + + for (const serial of Object.keys(workers)) { + const worker = workers[serial] + + // Check if worker has been in "starting" state for longer than startup timeout + if (worker.state === 'starting' && (now - worker.time) > STARTUP_TIMEOUT_MS) { + log.warn('Worker "%s" has been stuck in starting state for %s ms', serial, now - worker.time) + stuckWorkers.push(serial) + } + } + + // Stop and restart stuck workers + for (const serial of stuckWorkers) { + log.error('Restarting stuck worker "%s"', serial) + + try { + const device = tracker.getDevice(serial) + if (device) { + await removeDevice(device) + } + } + catch (err) { + log.error('Error restarting stuck worker "%s": %s', serial, err) + } + } + } + + tracker.on('healthcheck', stats => { + log.info('Healthcheck [OK: %s, BAD: %s]', stats.ok, stats.bad) + checkWorkerHealth() + }) + log.info('Tracking devices') tracker.on('connect', filterDevice((device) => { @@ -321,8 +406,15 @@ export default (async function(options: Options) { register: register(device), // Register device immediately, before 'running' state ports: ports.splice(0, 2), delete: () => { + log.warn('DELETING DEVICE %s', device.serial) ports.push(...workers[device.serial].ports) delete workers[device.serial] + + // Tell others the device is gone + push.send([ + wireutil.global, + wireutil.envelope(new wire.DeviceAbsentMessage(device.serial)) + ]) } } @@ -344,29 +436,70 @@ export default (async function(options: Options) { } })) - tracker.on('update', filterDevice((device, oldType) => { + tracker.on('update', filterDevice(async(device, oldType) => { + log.info('Device "%s" is now "%s" (was "%s")', device.serial, device.type, oldType) + if (!['device', 'emulator'].includes(device.type)) { - log.info('Lost device "%s" [%s]', device.serial, device.type) - return stop(device) - } + // Device went offline - stop worker but keep it in waiting state + log.info('Device "%s" went offline [%s]', device.serial, device.type) - log.info('Device "%s" is now "%s" (was "%s")', device.serial, device.type, oldType) + if (workers[device.serial] && workers[device.serial].state !== 'waiting') { + try { + await stop(device) + } + catch (err) { + log.error('Error stopping device worker "%s": %s', device.serial, err) + } + + // Set back to waiting state (keep worker and ports allocated) + if (workers[device.serial]) { + workers[device.serial].state = 'waiting' + workers[device.serial].time = Date.now() + } + } + return + } - // If not running, but can - if (device.type === 'device' && workers[device.serial]?.state === 'waiting') { + if (device.type === 'device' && workers[device.serial]?.state !== 'running') { clearTimeout(workers[device.serial].waitingTimeoutTimer) work(device) } })) + tracker.on('stuck', async(device, health) => { + log.warn( + 'Device %s is stuck [attempts: %s, first_healthcheck: %s, last_healthcheck: %s]', + device.serial, + new Date(health.firstFailureTime).toISOString(), + new Date(health.lastAttemptTime).toISOString() + ) + + if (!workers[device.serial]) { + log.warn('Device is stuck, but worker is not running') + return + } + + removeDevice(device) + }) + tracker.on('disconnect', filterDevice(async(device) => { - log.info('Disconnect device "%s" [%s]', device.serial, device.type) - clearTimeout(workers[device.serial]?.waitingTimeoutTimer) - await stop(device) + log.info('Device is disconnected "%s" [%s]', device.serial, device.type) - workers[device.serial].delete() + if (!workers[device.serial]) { + log.warn('Device is disconnected, but worker is not running%s', device.isStuck ? ' [Device got stuck earlier]' : '') + return + } + + if (!device.isStuck) { + clearTimeout(workers[device.serial].waitingTimeoutTimer) + removeDevice(device) + } })) + tracker.on('error', err => { + log.error('ADBObserver error: %s', err?.message) + }) + tracker.start() let statsTimer: NodeJS.Timeout @@ -374,17 +507,22 @@ export default (async function(options: Options) { const all = Object.keys(workers).length const result: any = { waiting: [], + starting: [], running: [] } for (const serial of Object.keys(workers)) { if (workers[serial].state === 'running') { result.running.push(serial) - continue } - result.waiting.push(serial) + else if (workers[serial].state === 'starting') { + result.starting.push(serial) + } + else { + result.waiting.push(serial) + } } - log.info(`Providing ${result.running.length} of ${all} device(s); waiting for [${result.waiting.join(', ')}]`) + log.info(`Providing ${result.running.length} of ${all} device(s); starting: [${result.starting.join(', ')}], waiting: [${result.waiting.join(', ')}]`) log.info(`Providing all ${all} of ${tracker.count} device(s)`) log.info(`Providing all ${tracker.count} device(s)`) @@ -395,6 +533,9 @@ export default (async function(options: Options) { } lifecycle.observe(async() => { + // Clear timers + clearTimeout(statsTimer) + await Promise.all( Object.values(workers) .map(worker => From 74bb4e54d7dcddad73b4b155b26a6de88025a715 Mon Sep 17 00:00:00 2001 From: Elmir Khalilov <52529096+e-khalilov@users.noreply.github.com> Date: Mon, 15 Dec 2025 22:27:13 +0300 Subject: [PATCH 17/23] General Process Manager & iOS Refactoring (#394) * add common process manager & refactor iOS * update wda * revert logger edits --------- Co-authored-by: e.khalilov --- WebDriverAgent/.azure-pipelines.yml | 2 +- WebDriverAgent/.eslintignore | 3 - WebDriverAgent/.eslintrc.json | 29 - WebDriverAgent/.releaserc | 6 +- WebDriverAgent/CHANGELOG.md | 411 +++ .../Configurations/IOSSettings.xcconfig | 2 +- .../Configurations/TVOSSettings.xcconfig | 2 +- .../Configurations/TVOSTestSettings.xcconfig | 1 - WebDriverAgent/PATENTS | 33 - .../XCTestManager_ManagerInterface-Protocol.h | 1 + WebDriverAgent/README.md | 2 +- .../Scripts/build-webdriveragent.js | 2 +- WebDriverAgent/Scripts/build.sh | 3 +- WebDriverAgent/Scripts/ci/build-real.sh | 21 +- WebDriverAgent/Scripts/ci/build-sim.sh | 14 +- WebDriverAgent/Scripts/fetch-prebuilt-wda.js | 2 +- .../WebDriverAgent.xcodeproj/project.pbxproj | 94 +- .../xcschemes/WebDriverAgentRunner.xcscheme | 5 + .../WebDriverAgentRunner_tvOS.xcscheme | 5 + .../FBXCElementSnapshotWrapper+Helpers.h | 16 +- .../FBXCElementSnapshotWrapper+Helpers.m | 41 +- .../NSDictionary+FBUtf8SafeDictionary.h | 3 +- .../NSDictionary+FBUtf8SafeDictionary.m | 3 +- .../Categories/NSExpression+FBFormat.h | 3 +- .../Categories/NSExpression+FBFormat.m | 3 +- .../Categories/NSString+FBVisualLength.h | 3 +- .../Categories/NSString+FBVisualLength.m | 3 +- .../Categories/NSString+FBXMLSafeString.h | 3 +- .../Categories/NSString+FBXMLSafeString.m | 3 +- .../XCAXClient_iOS+FBSnapshotReqParams.h | 3 +- .../XCAXClient_iOS+FBSnapshotReqParams.m | 3 +- .../Categories/XCTIssue+FBPatcher.h | 3 +- .../Categories/XCTIssue+FBPatcher.m | 3 +- .../Categories/XCUIApplication+FBAlert.h | 3 +- .../Categories/XCUIApplication+FBAlert.m | 7 +- .../Categories/XCUIApplication+FBFocused.h | 3 +- .../Categories/XCUIApplication+FBHelpers.h | 3 +- .../Categories/XCUIApplication+FBHelpers.m | 204 +- .../Categories/XCUIApplication+FBQuiescence.h | 3 +- .../Categories/XCUIApplication+FBQuiescence.m | 3 +- .../XCUIApplication+FBTouchAction.h | 3 +- .../XCUIApplication+FBTouchAction.m | 3 +- .../XCUIApplication+FBUIInterruptions.h | 3 +- .../XCUIApplication+FBUIInterruptions.m | 3 +- .../XCUIApplicationProcess+FBQuiescence.h | 3 +- .../XCUIApplicationProcess+FBQuiescence.m | 3 +- .../Categories/XCUIDevice+FBHealthCheck.h | 3 +- .../Categories/XCUIDevice+FBHealthCheck.m | 3 +- .../Categories/XCUIDevice+FBHelpers.h | 3 +- .../Categories/XCUIDevice+FBHelpers.m | 3 +- .../Categories/XCUIDevice+FBRotation.h | 3 +- .../Categories/XCUIDevice+FBRotation.m | 3 +- .../Categories/XCUIElement+FBAccessibility.h | 5 +- .../Categories/XCUIElement+FBAccessibility.m | 22 +- .../Categories/XCUIElement+FBCaching.h | 6 +- .../Categories/XCUIElement+FBCaching.m | 26 +- .../Categories/XCUIElement+FBClassChain.h | 6 +- .../Categories/XCUIElement+FBClassChain.m | 3 +- .../Categories/XCUIElement+FBFind.h | 3 +- .../Categories/XCUIElement+FBFind.m | 14 +- .../Categories/XCUIElement+FBForceTouch.h | 3 +- .../Categories/XCUIElement+FBForceTouch.m | 3 +- .../Categories/XCUIElement+FBIsVisible.h | 5 +- .../Categories/XCUIElement+FBIsVisible.m | 210 +- .../Categories/XCUIElement+FBMinMax.h | 33 + .../Categories/XCUIElement+FBMinMax.m | 75 + .../Categories/XCUIElement+FBPickerWheel.h | 3 +- .../Categories/XCUIElement+FBPickerWheel.m | 10 +- .../Categories/XCUIElement+FBResolve.h | 6 +- .../Categories/XCUIElement+FBResolve.m | 27 +- .../Categories/XCUIElement+FBScrolling.h | 3 +- .../Categories/XCUIElement+FBScrolling.m | 98 +- .../Categories/XCUIElement+FBSwiping.h | 3 +- .../Categories/XCUIElement+FBSwiping.m | 3 +- .../Categories/XCUIElement+FBTVFocuse.h | 3 +- .../Categories/XCUIElement+FBTVFocuse.m | 3 +- .../Categories/XCUIElement+FBTyping.h | 3 +- .../Categories/XCUIElement+FBTyping.m | 15 +- .../Categories/XCUIElement+FBUID.h | 3 +- .../Categories/XCUIElement+FBUID.m | 7 +- .../Categories/XCUIElement+FBUtilities.h | 80 +- .../Categories/XCUIElement+FBUtilities.m | 131 +- .../Categories/XCUIElement+FBVisibleFrame.h | 35 + .../Categories/XCUIElement+FBVisibleFrame.m | 52 + .../XCUIElement+FBWebDriverAttributes.h | 5 +- .../XCUIElement+FBWebDriverAttributes.m | 92 +- .../Categories/XCUIElementQuery+FBHelpers.h | 3 +- .../Categories/XCUIElementQuery+FBHelpers.m | 3 +- .../Commands/FBAlertViewCommands.h | 3 +- .../Commands/FBAlertViewCommands.m | 3 +- .../Commands/FBCustomCommands.h | 3 +- .../Commands/FBCustomCommands.m | 6 +- .../Commands/FBDebugCommands.h | 3 +- .../Commands/FBDebugCommands.m | 3 +- .../Commands/FBElementCommands.h | 3 +- .../Commands/FBElementCommands.m | 127 +- .../Commands/FBFindElementCommands.h | 3 +- .../Commands/FBFindElementCommands.m | 21 +- .../Commands/FBOrientationCommands.h | 3 +- .../Commands/FBOrientationCommands.m | 3 +- .../Commands/FBScreenshotCommands.h | 3 +- .../Commands/FBScreenshotCommands.m | 3 +- .../Commands/FBSessionCommands.h | 3 +- .../Commands/FBSessionCommands.m | 58 +- .../Commands/FBTouchActionCommands.h | 3 +- .../Commands/FBTouchActionCommands.m | 3 +- .../Commands/FBTouchIDCommands.h | 3 +- .../Commands/FBTouchIDCommands.m | 3 +- .../Commands/FBUnknownCommands.h | 3 +- .../Commands/FBUnknownCommands.m | 3 +- .../Commands/FBVideoCommands.h | 3 +- .../Commands/FBVideoCommands.m | 3 +- WebDriverAgent/WebDriverAgentLib/FBAlert.h | 3 +- WebDriverAgent/WebDriverAgentLib/FBAlert.m | 14 +- WebDriverAgent/WebDriverAgentLib/Info.plist | 44 +- .../Routing/FBCommandHandler.h | 3 +- .../Routing/FBCommandStatus.h | 5 +- .../Routing/FBCommandStatus.m | 8 +- .../WebDriverAgentLib/Routing/FBElement.h | 18 +- .../Routing/FBElementCache.h | 17 +- .../Routing/FBElementCache.m | 50 +- .../Routing/FBElementUtils.h | 3 +- .../Routing/FBElementUtils.m | 3 +- .../Routing/FBExceptionHandler.h | 3 +- .../Routing/FBExceptionHandler.m | 3 +- .../WebDriverAgentLib/Routing/FBExceptions.h | 3 +- .../WebDriverAgentLib/Routing/FBExceptions.m | 3 +- .../Routing/FBResponseJSONPayload.h | 3 +- .../Routing/FBResponseJSONPayload.m | 3 +- .../Routing/FBResponsePayload.h | 3 +- .../Routing/FBResponsePayload.m | 103 +- .../WebDriverAgentLib/Routing/FBRoute.h | 3 +- .../WebDriverAgentLib/Routing/FBRoute.m | 3 +- .../Routing/FBRouteRequest-Private.h | 3 +- .../Routing/FBRouteRequest.h | 3 +- .../Routing/FBRouteRequest.m | 3 +- .../Routing/FBScreenRecordingContainer.h | 3 +- .../Routing/FBScreenRecordingContainer.m | 3 +- .../Routing/FBScreenRecordingPromise.h | 3 +- .../Routing/FBScreenRecordingPromise.m | 3 +- .../Routing/FBScreenRecordingRequest.h | 3 +- .../Routing/FBScreenRecordingRequest.m | 3 +- .../Routing/FBSession-Private.h | 3 +- .../WebDriverAgentLib/Routing/FBSession.h | 19 +- .../WebDriverAgentLib/Routing/FBSession.m | 61 +- .../WebDriverAgentLib/Routing/FBTCPSocket.h | 3 +- .../WebDriverAgentLib/Routing/FBTCPSocket.m | 3 +- .../WebDriverAgentLib/Routing/FBWebServer.h | 3 +- .../WebDriverAgentLib/Routing/FBWebServer.m | 15 +- .../Routing/FBXCAccessibilityElement.h | 3 +- .../Routing/FBXCAccessibilityElement.m | 3 +- .../Routing/FBXCDeviceEvent.h | 3 +- .../Routing/FBXCDeviceEvent.m | 3 +- .../Routing/FBXCElementSnapshot.h | 3 +- .../Routing/FBXCElementSnapshot.m | 3 +- .../Routing/FBXCElementSnapshotWrapper.h | 3 +- .../Routing/FBXCElementSnapshotWrapper.m | 3 +- .../Utilities/FBAccessibilityTraits.h | 20 + .../Utilities/FBAccessibilityTraits.m | 61 + .../Utilities/FBActiveAppDetectionPoint.h | 3 +- .../Utilities/FBActiveAppDetectionPoint.m | 3 +- .../Utilities/FBAlertsMonitor.h | 3 +- .../Utilities/FBAlertsMonitor.m | 3 +- .../Utilities/FBBaseActionsSynthesizer.h | 3 +- .../Utilities/FBBaseActionsSynthesizer.m | 3 +- .../Utilities/FBCapabilities.h | 3 +- .../Utilities/FBCapabilities.m | 3 +- .../Utilities/FBClassChainQueryParser.h | 3 +- .../Utilities/FBClassChainQueryParser.m | 3 +- .../Utilities/FBConfiguration.h | 74 +- .../Utilities/FBConfiguration.m | 90 +- .../Utilities/FBDebugLogDelegateDecorator.h | 3 +- .../Utilities/FBDebugLogDelegateDecorator.m | 3 +- .../Utilities/FBElementHelpers.h | 29 + .../Utilities/FBElementHelpers.m | 21 + .../Utilities/FBElementTypeTransformer.h | 3 +- .../Utilities/FBElementTypeTransformer.m | 3 +- .../Utilities/FBErrorBuilder.h | 3 +- .../Utilities/FBErrorBuilder.m | 3 +- .../Utilities/FBFailureProofTestCase.h | 3 +- .../Utilities/FBFailureProofTestCase.m | 3 +- .../Utilities/FBImageProcessor.h | 3 +- .../Utilities/FBImageProcessor.m | 3 +- .../Utilities/FBImageUtils.h | 3 +- .../Utilities/FBImageUtils.m | 3 +- .../WebDriverAgentLib/Utilities/FBKeyboard.h | 3 +- .../WebDriverAgentLib/Utilities/FBKeyboard.m | 3 +- .../WebDriverAgentLib/Utilities/FBLogger.h | 3 +- .../WebDriverAgentLib/Utilities/FBLogger.m | 3 +- .../WebDriverAgentLib/Utilities/FBMacros.h | 3 +- .../WebDriverAgentLib/Utilities/FBMathUtils.h | 3 +- .../WebDriverAgentLib/Utilities/FBMathUtils.m | 3 +- .../Utilities/FBMjpegServer.h | 3 +- .../Utilities/FBMjpegServer.m | 3 +- .../Utilities/FBNotificationsHelper.h | 3 +- .../Utilities/FBNotificationsHelper.m | 3 +- .../Utilities/FBPasteboard.h | 3 +- .../Utilities/FBPasteboard.m | 3 +- .../Utilities/FBProtocolHelpers.h | 3 +- .../Utilities/FBProtocolHelpers.m | 3 +- .../Utilities/FBReflectionUtils.h | 3 +- .../Utilities/FBReflectionUtils.m | 3 +- .../Utilities/FBRunLoopSpinner.h | 3 +- .../Utilities/FBRunLoopSpinner.m | 3 +- .../Utilities/FBRuntimeUtils.h | 3 +- .../Utilities/FBRuntimeUtils.m | 3 +- .../WebDriverAgentLib/Utilities/FBScreen.h | 3 +- .../WebDriverAgentLib/Utilities/FBScreen.m | 3 +- .../Utilities/FBScreenshot.h | 3 +- .../Utilities/FBScreenshot.m | 5 +- .../WebDriverAgentLib/Utilities/FBSettings.h | 12 +- .../WebDriverAgentLib/Utilities/FBSettings.m | 10 +- .../Utilities/FBTVNavigationTracker-Private.h | 3 +- .../Utilities/FBTVNavigationTracker.h | 3 +- .../Utilities/FBTVNavigationTracker.m | 7 +- .../Utilities/FBUnattachedAppLauncher.h | 3 +- .../Utilities/FBUnattachedAppLauncher.m | 3 +- .../Utilities/FBW3CActionsHelpers.h | 3 +- .../Utilities/FBW3CActionsHelpers.m | 3 +- .../Utilities/FBW3CActionsSynthesizer.h | 3 +- .../Utilities/FBW3CActionsSynthesizer.m | 5 +- .../Utilities/FBWebServerParams.h | 3 +- .../Utilities/FBWebServerParams.m | 3 +- .../Utilities/FBXCAXClientProxy.h | 12 +- .../Utilities/FBXCAXClientProxy.m | 52 +- .../Utilities/FBXCTestDaemonsProxy.h | 3 +- .../Utilities/FBXCTestDaemonsProxy.m | 3 +- .../Utilities/FBXCodeCompatibility.h | 7 +- .../Utilities/FBXCodeCompatibility.m | 7 +- .../Utilities/FBXMLGenerationOptions.h | 3 +- .../Utilities/FBXMLGenerationOptions.m | 3 +- .../Utilities/FBXPath-Private.h | 10 +- .../WebDriverAgentLib/Utilities/FBXPath.h | 5 +- .../WebDriverAgentLib/Utilities/FBXPath.m | 356 +- .../Utilities/LRUCache/LRUCache.h | 7 + .../Utilities/LRUCache/LRUCache.m | 8 + .../Utilities/NSPredicate+FBFormat.h | 3 +- .../Utilities/NSPredicate+FBFormat.m | 9 +- .../Utilities/XCTestPrivateSymbols.h | 14 +- .../Utilities/XCTestPrivateSymbols.m | 22 +- .../Utilities/XCUIApplicationProcessDelay.h | 3 +- .../Utilities/XCUIApplicationProcessDelay.m | 3 +- .../Vendor/CocoaAsyncSocket/GCDAsyncSocket.m | 3200 +++++++++-------- .../CocoaAsyncSocket/GCDAsyncUdpSocket.m | 1984 +++++----- .../Vendor/CocoaHTTPServer/HTTPMessage.h | 23 +- .../Vendor/CocoaHTTPServer/HTTPMessage.m | 303 +- .../WebDriverAgentLib/WebDriverAgentLib.h | 12 +- .../WebDriverAgentRunner/UITestingUITests.m | 3 +- .../IntegrationApp/Classes/AppDelegate.h | 6 +- .../IntegrationApp/Classes/AppDelegate.m | 26 +- .../Classes/FBAlertViewController.h | 3 +- .../Classes/FBAlertViewController.m | 3 +- .../Classes/FBNavigationController.h | 3 +- .../Classes/FBNavigationController.m | 3 +- .../Classes/FBScrollViewController.h | 3 +- .../Classes/FBScrollViewController.m | 3 +- .../Classes/FBTableDataSource.h | 3 +- .../Classes/FBTableDataSource.m | 3 +- .../IntegrationApp/Classes/SceneDelegate.h | 20 + .../IntegrationApp/Classes/SceneDelegate.m | 48 + .../IntegrationApp/Classes/TouchSpotView.h | 3 +- .../IntegrationApp/Classes/TouchSpotView.m | 3 +- .../Classes/TouchViewController.h | 3 +- .../Classes/TouchViewController.m | 3 +- .../IntegrationApp/Classes/TouchableView.h | 3 +- .../IntegrationApp/Classes/TouchableView.m | 3 +- .../IntegrationApp/Classes/ViewController.h | 3 +- .../IntegrationApp/Classes/ViewController.m | 3 +- .../IntegrationApp/Info.plist | 19 +- .../Resources/Base.lproj/Main.storyboard | 71 +- .../WebDriverAgentTests/IntegrationApp/main.m | 3 +- .../IntegrationTests/FBAlertTests.m | 3 +- .../FBAutoAlertsHandlerTests.m | 3 +- .../IntegrationTests/FBConfigurationTests.m | 3 +- .../FBElementAttributeTests.m | 68 +- .../IntegrationTests/FBElementSwipingTests.m | 5 +- .../FBElementVisibilityTests.m | 3 +- .../FBFailureProofTestCaseTests.m | 3 +- .../IntegrationTests/FBForceTouchTests.m | 3 +- .../IntegrationTests/FBImageProcessorTests.m | 3 +- .../IntegrationTests/FBIntegrationTestCase.h | 3 +- .../IntegrationTests/FBIntegrationTestCase.m | 3 +- .../IntegrationTests/FBKeyboardTests.m | 3 +- .../IntegrationTests/FBPasteboardTests.m | 3 +- .../FBPickerWheelSelectTests.m | 3 +- .../IntegrationTests/FBSafariAlertTests.m | 3 +- .../IntegrationTests/FBScreenTests.m | 3 +- .../IntegrationTests/FBScrollingTests.m | 8 +- .../FBSessionIntegrationTests.m | 3 +- .../IntegrationTests/FBTapTest.m | 3 +- .../IntegrationTests/FBTestMacros.h | 3 +- .../IntegrationTests/FBTypingTest.m | 3 +- .../IntegrationTests/FBVideoRecordingTests.m | 3 +- .../FBW3CMultiTouchActionsIntegrationTests.m | 3 +- .../FBW3CTouchActionsIntegrationTests.m | 5 +- .../IntegrationTests/FBW3CTypeActionsTests.m | 3 +- .../FBXPathIntegrationTests.m | 63 +- .../XCElementSnapshotHelperTests.m | 34 +- .../XCElementSnapshotHitPointTests.m | 6 +- .../XCUIApplicationHelperTests.m | 26 +- .../XCUIDeviceHealthCheckTests.m | 3 +- .../IntegrationTests/XCUIDeviceHelperTests.m | 5 +- .../XCUIDeviceRotationTests.m | 3 +- .../XCUIElementAttributesTests.m | 3 +- .../IntegrationTests/XCUIElementFBFindTests.m | 18 +- .../XCUIElementHelperIntegrationTests.m | 10 +- .../Doubles/XCElementSnapshotDouble.h | 4 +- .../Doubles/XCElementSnapshotDouble.m | 19 +- .../UnitTests/Doubles/XCUIApplicationDouble.h | 3 +- .../UnitTests/Doubles/XCUIApplicationDouble.m | 3 +- .../UnitTests/Doubles/XCUIElementDouble.h | 10 +- .../UnitTests/Doubles/XCUIElementDouble.m | 20 +- .../UnitTests/FBClassChainTests.m | 3 +- .../UnitTests/FBConfigurationTests.m | 15 +- .../UnitTests/FBElementCacheTests.m | 5 +- .../UnitTests/FBElementTypeTransformerTests.m | 3 +- .../UnitTests/FBElementUtilitiesTests.m | 3 +- .../UnitTests/FBErrorBuilderTests.m | 3 +- .../UnitTests/FBExceptionHandlerTests.m | 3 +- .../UnitTests/FBLRUCacheTests.m | 32 +- .../UnitTests/FBMathUtilsTests.m | 3 +- .../UnitTests/FBProtocolHelpersTests.m | 3 +- .../UnitTests/FBRouteTests.m | 3 +- .../UnitTests/FBRunLoopSpinnerTests.m | 3 +- .../UnitTests/FBRuntimeUtilsTests.m | 3 +- .../UnitTests/FBSDKVersionTests.m | 3 +- .../UnitTests/FBSessionTests.m | 3 +- .../UnitTests/FBXMLSafeStringTests.m | 3 +- .../UnitTests/FBXPathTests.m | 30 +- .../UnitTests/NSDictionaryFBUtf8SafeTests.m | 3 +- .../UnitTests/NSExpressionFBFormatTests.m | 3 +- .../UnitTests/NSPredicateFBFormatTests.m | 3 +- .../UnitTests/XCUIElementHelpersTests.m | 3 +- .../Doubles/XCUIElementDouble.h | 7 +- .../Doubles/XCUIElementDouble.m | 10 +- .../FBTVNavigationTrackerTests.m | 3 +- .../azure-templates/bootstrap_steps.yml | 2 +- WebDriverAgent/eslint.config.mjs | 13 + WebDriverAgent/lib/check-dependencies.js | 12 +- WebDriverAgent/lib/types.ts | 77 + WebDriverAgent/lib/utils.js | 39 +- WebDriverAgent/lib/webdriveragent.js | 44 +- WebDriverAgent/lib/xcodebuild.js | 62 +- WebDriverAgent/package.json | 42 +- .../test/functional/helpers/simulator.js | 4 +- .../functional/webdriveragent-e2e-specs.js | 6 +- WebDriverAgent/test/unit/utils-specs.js | 80 +- .../test/unit/webdriveragent-specs.js | 43 +- eslint.config.js | 2 +- lib/cli/ios-device/index.js | 14 +- lib/cli/ios-provider/index.js | 26 +- lib/cli/provider/index.js | 12 +- lib/db/models/all/model.js | 66 +- lib/units/ios-device/index.js | 2 +- lib/units/ios-device/plugins/info/index.js | 33 +- .../ios-device/plugins/wda/WDAService.js | 11 +- lib/units/ios-device/plugins/wda/client.js | 62 +- .../{IOSSimObserver.js => IOSSimObserver.ts} | 26 +- lib/units/ios-provider/index.js | 220 -- lib/units/ios-provider/index.ts | 414 +++ lib/units/ios-provider/redirect-ports.js | 40 - lib/units/ios-provider/redirect-ports.ts | 41 + lib/units/processor/index.ts | 12 +- lib/units/provider/ADBObserver.ts | 21 +- lib/units/provider/index.ts | 512 +-- lib/util/ProcessManager.ts | 457 +++ 366 files changed, 7353 insertions(+), 5092 deletions(-) delete mode 100644 WebDriverAgent/.eslintignore delete mode 100644 WebDriverAgent/.eslintrc.json delete mode 100644 WebDriverAgent/PATENTS mode change 100644 => 100755 WebDriverAgent/Scripts/build.sh mode change 100644 => 100755 WebDriverAgent/Scripts/ci/build-real.sh mode change 100644 => 100755 WebDriverAgent/Scripts/ci/build-sim.sh create mode 100644 WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBMinMax.h create mode 100644 WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBMinMax.m create mode 100644 WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBVisibleFrame.h create mode 100644 WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBVisibleFrame.m create mode 100644 WebDriverAgent/WebDriverAgentLib/Utilities/FBAccessibilityTraits.h create mode 100644 WebDriverAgent/WebDriverAgentLib/Utilities/FBAccessibilityTraits.m create mode 100644 WebDriverAgent/WebDriverAgentLib/Utilities/FBElementHelpers.h create mode 100644 WebDriverAgent/WebDriverAgentLib/Utilities/FBElementHelpers.m mode change 100644 => 100755 WebDriverAgent/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncSocket.m mode change 100644 => 100755 WebDriverAgent/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncUdpSocket.m create mode 100644 WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/SceneDelegate.h create mode 100644 WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/SceneDelegate.m create mode 100644 WebDriverAgent/eslint.config.mjs rename lib/units/ios-provider/{IOSSimObserver.js => IOSSimObserver.ts} (63%) delete mode 100644 lib/units/ios-provider/index.js create mode 100644 lib/units/ios-provider/index.ts delete mode 100644 lib/units/ios-provider/redirect-ports.js create mode 100644 lib/units/ios-provider/redirect-ports.ts create mode 100644 lib/util/ProcessManager.ts diff --git a/WebDriverAgent/.azure-pipelines.yml b/WebDriverAgent/.azure-pipelines.yml index 89887d258a..eca3f55cf8 100644 --- a/WebDriverAgent/.azure-pipelines.yml +++ b/WebDriverAgent/.azure-pipelines.yml @@ -1,6 +1,6 @@ # https://docs.microsoft.com/en-us/azure/devops/pipelines/agents/hosted?view=azure-devops&tabs=yaml variables: - MIN_VM_IMAGE: macOS-14 + MIN_VM_IMAGE: macOS-13 MIN_XCODE_VERSION: 14.3.1 MIN_PLATFORM_VERSION: 16.4 MIN_TV_PLATFORM_VERSION: 16.4 diff --git a/WebDriverAgent/.eslintignore b/WebDriverAgent/.eslintignore deleted file mode 100644 index 84930aead4..0000000000 --- a/WebDriverAgent/.eslintignore +++ /dev/null @@ -1,3 +0,0 @@ -Resources -coverage -build diff --git a/WebDriverAgent/.eslintrc.json b/WebDriverAgent/.eslintrc.json deleted file mode 100644 index 3d7db0846d..0000000000 --- a/WebDriverAgent/.eslintrc.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "extends": ["@appium/eslint-config-appium-ts"], - "overrides": [ - { - "files": "test/**/*.js", - "rules": { - "func-names": "off", - "@typescript-eslint/no-var-requires": "off" - } - }, - { - "files": "Scripts/**/*.js", - "parserOptions": {"sourceType": "script"}, - "rules": { - "@typescript-eslint/no-var-requires": "off" - } - }, - { - "files": "ci-jobs/scripts/*.js", - "parserOptions": {"sourceType": "script"}, - "rules": { - "@typescript-eslint/no-var-requires": "off" - } - } - ], - "rules": { - "require-await": "error" - } -} diff --git a/WebDriverAgent/.releaserc b/WebDriverAgent/.releaserc index 0cb17f2207..3987c1e97d 100644 --- a/WebDriverAgent/.releaserc +++ b/WebDriverAgent/.releaserc @@ -35,7 +35,11 @@ ["@semantic-release/github", { "assets": [ "WebDriverAgentRunner-Runner.zip", - "WebDriverAgentRunner_tvOS-Runner.zip" + "WebDriverAgentRunner_tvOS-Runner.zip", + "WebDriverAgentRunner-Build-Sim-arm64.zip", + "WebDriverAgentRunner-Build-Sim-x86_64.zip", + "WebDriverAgentRunner_tvOS-Build-Sim-arm64.zip", + "WebDriverAgentRunner_tvOS-Build-Sim-x86_64.zip" ]}] ] } diff --git a/WebDriverAgent/CHANGELOG.md b/WebDriverAgent/CHANGELOG.md index 1a56783d3a..fe6e4b8587 100644 --- a/WebDriverAgent/CHANGELOG.md +++ b/WebDriverAgent/CHANGELOG.md @@ -1,3 +1,414 @@ +## [10.4.2](https://github.com/appium/WebDriverAgent/compare/v10.4.1...v10.4.2) (2025-12-11) + +### Miscellaneous Chores + +* **deps-dev:** bump @types/node from 24.10.3 to 25.0.0 ([#1081](https://github.com/appium/WebDriverAgent/issues/1081)) ([6157d8e](https://github.com/appium/WebDriverAgent/commit/6157d8edd12f0e40938fd38fa8473d84999ce360)) + +## [10.4.1](https://github.com/appium/WebDriverAgent/compare/v10.4.0...v10.4.1) (2025-12-07) + +### Miscellaneous Chores + +* Ditch usage of @appium/test-support ([#1080](https://github.com/appium/WebDriverAgent/issues/1080)) ([5c7cd2e](https://github.com/appium/WebDriverAgent/commit/5c7cd2ef2dec11b00dddd7a357bdaafd6df62e73)) + +## [10.4.0](https://github.com/appium/WebDriverAgent/compare/v10.3.0...v10.4.0) (2025-12-06) + +### Features + +* Migrate IntegrationApp to use UIScene lifecycle ([#1079](https://github.com/appium/WebDriverAgent/issues/1079)) ([dfba786](https://github.com/appium/WebDriverAgent/commit/dfba7863195651535a498c246e7b6461eaa24f8b)) + +## [10.3.0](https://github.com/appium/WebDriverAgent/compare/v10.2.7...v10.3.0) (2025-12-03) + +### Features + +* Deprecate CFNetwork usage ([#1078](https://github.com/appium/WebDriverAgent/issues/1078)) ([6df0c5f](https://github.com/appium/WebDriverAgent/commit/6df0c5fb442879a1af115f02ac79055a7ed76719)) + +## [10.2.7](https://github.com/appium/WebDriverAgent/compare/v10.2.6...v10.2.7) (2025-12-03) + +### Miscellaneous Chores + +* use any iphone/tvos simulator devices to build WDA for sim ([#1077](https://github.com/appium/WebDriverAgent/issues/1077)) ([9600d83](https://github.com/appium/WebDriverAgent/commit/9600d837bf35f420f71d00772fc682e3db9f257d)) + +## [10.2.6](https://github.com/appium/WebDriverAgent/compare/v10.2.5...v10.2.6) (2025-12-02) + +### Miscellaneous Chores + +* **deps:** bump actions/setup-node from 4 to 6 ([#1075](https://github.com/appium/WebDriverAgent/issues/1075)) ([5042063](https://github.com/appium/WebDriverAgent/commit/504206338a1d7038f1d95ef241006997c5512757)) + +## [10.2.5](https://github.com/appium/WebDriverAgent/compare/v10.2.4...v10.2.5) (2025-11-29) + +### Miscellaneous Chores + +* **deps:** bump actions/checkout from 4 to 6 ([#1076](https://github.com/appium/WebDriverAgent/issues/1076)) ([276be79](https://github.com/appium/WebDriverAgent/commit/276be795ad36ea4d3beddff47b0ed3aa2c1f9461)) + +## [10.2.4](https://github.com/appium/WebDriverAgent/compare/v10.2.3...v10.2.4) (2025-11-29) + +### Miscellaneous Chores + +* Deprecate idb ([#1073](https://github.com/appium/WebDriverAgent/issues/1073)) ([260bc31](https://github.com/appium/WebDriverAgent/commit/260bc319795aff26468eca261d5f286b31216270)) +* **deps:** bump appium-ios-simulator from 7.0.3 to 8.0.0 ([#1070](https://github.com/appium/WebDriverAgent/issues/1070)) ([7cb4b6e](https://github.com/appium/WebDriverAgent/commit/7cb4b6ecd37717c7aad214f8e387c404d4937534)) + +## [10.2.3](https://github.com/appium/WebDriverAgent/compare/v10.2.2...v10.2.3) (2025-11-29) + +### Bug Fixes + +* fix type and mark deprecated as no usage for unused xcodeVersion and deprecated idb ([#1072](https://github.com/appium/WebDriverAgent/issues/1072)) ([4499fb2](https://github.com/appium/WebDriverAgent/commit/4499fb22ae4884df84e5e2d2bd0570c90ee4848b)) + +## [10.2.2](https://github.com/appium/WebDriverAgent/compare/v10.2.1...v10.2.2) (2025-11-15) + +### Miscellaneous Chores + +* publish via trusted publisher ([#1068](https://github.com/appium/WebDriverAgent/issues/1068)) ([6321379](https://github.com/appium/WebDriverAgent/commit/6321379d3e97dfa846bf0ff6f0b8e8b9ef85ba1c)) + +## [10.2.1](https://github.com/appium/WebDriverAgent/compare/v10.2.0...v10.2.1) (2025-11-01) + +### Miscellaneous Chores + +* update xcodebuild commands for the new binding ip capability ([#1067](https://github.com/appium/WebDriverAgent/issues/1067)) ([d12f421](https://github.com/appium/WebDriverAgent/commit/d12f4214b958855022c21b4d700b6726740294c1)) + +## [10.2.0](https://github.com/appium/WebDriverAgent/compare/v10.1.4...v10.2.0) (2025-10-31) + +### Features + +* Let binding IP address to be configurable via USE_IP environment variable ([#1066](https://github.com/appium/WebDriverAgent/issues/1066)) ([70ed7cf](https://github.com/appium/WebDriverAgent/commit/70ed7cf0a74e0df3763b99f6155a7923dde17c9d)) + +## [10.1.4](https://github.com/appium/WebDriverAgent/compare/v10.1.3...v10.1.4) (2025-10-31) + +### Miscellaneous Chores + +* Improve type declarations ([#1065](https://github.com/appium/WebDriverAgent/issues/1065)) ([5aadcb8](https://github.com/appium/WebDriverAgent/commit/5aadcb8fa99459e7f8852f75d2549a76f3e55b07)) + +## [10.1.3](https://github.com/appium/WebDriverAgent/compare/v10.1.2...v10.1.3) (2025-10-17) + +### Miscellaneous Chores + +* **deps-dev:** bump semantic-release from 24.2.9 to 25.0.0 ([#1064](https://github.com/appium/WebDriverAgent/issues/1064)) ([6c2cffa](https://github.com/appium/WebDriverAgent/commit/6c2cffa4ee6fcd0c86ae7aa171f25cb800908932)) + +## [10.1.2](https://github.com/appium/WebDriverAgent/compare/v10.1.1...v10.1.2) (2025-10-08) + +### Miscellaneous Chores + +* Skip staleness checks for subelement lookups ([#1063](https://github.com/appium/WebDriverAgent/issues/1063)) ([ada7760](https://github.com/appium/WebDriverAgent/commit/ada77604f9fa9bfc85c61cabbd2a9f4de00aceb9)) + +## [10.1.1](https://github.com/appium/WebDriverAgent/compare/v10.1.0...v10.1.1) (2025-09-12) + +### Miscellaneous Chores + +* remove patents file ([#1061](https://github.com/appium/WebDriverAgent/issues/1061)) ([b001c4e](https://github.com/appium/WebDriverAgent/commit/b001c4e39ef71cb8b91ef7391b418f32a7ebe21c)) + +## [10.1.0](https://github.com/appium/WebDriverAgent/compare/v10.0.1...v10.1.0) (2025-09-03) + +### Features + +* Add process and bundle identifiers to the application node in the XML source ([#1055](https://github.com/appium/WebDriverAgent/issues/1055)) ([088cff2](https://github.com/appium/WebDriverAgent/commit/088cff2b2bc19ddde698ec06f1db37c6989cf392)) + +## [10.0.1](https://github.com/appium/WebDriverAgent/compare/v10.0.0...v10.0.1) (2025-08-23) + +### Miscellaneous Chores + +* **deps-dev:** bump chai from 5.3.2 to 6.0.0 ([#1053](https://github.com/appium/WebDriverAgent/issues/1053)) ([9e9ec38](https://github.com/appium/WebDriverAgent/commit/9e9ec381bd6695e1c8b89f2a9c304b12385c0134)) + +## [10.0.0](https://github.com/appium/WebDriverAgent/compare/v9.15.3...v10.0.0) (2025-08-17) + +### ⚠ BREAKING CHANGES + +* Required Node.js version has been bumped to ^20.19.0 || ^22.12.0 || >=24.0.0 +* Required npm version has been bumped to >=10 +* Required base driver version has been bumped to >=10.0.0-rc.1 + +### Features + +* Update server compatibility ([#1051](https://github.com/appium/WebDriverAgent/issues/1051)) ([f9ea1e5](https://github.com/appium/WebDriverAgent/commit/f9ea1e5e2f5306030387d5293f073b2a6fe658e7)) + +## [9.15.3](https://github.com/appium/WebDriverAgent/compare/v9.15.2...v9.15.3) (2025-08-12) + +### Miscellaneous Chores + +* Cache application instances for their PIDs ([#1049](https://github.com/appium/WebDriverAgent/issues/1049)) ([e9cbf64](https://github.com/appium/WebDriverAgent/commit/e9cbf640c21243c304b476a497f33802e0501a7d)) + +## [9.15.2](https://github.com/appium/WebDriverAgent/compare/v9.15.1...v9.15.2) (2025-08-04) + +### Miscellaneous Chores + +* bump appium-ios-device to 2.9.0 ([#1047](https://github.com/appium/WebDriverAgent/issues/1047)) ([305019d](https://github.com/appium/WebDriverAgent/commit/305019d4dde89853e44c58170e17ec23c89de2f3)) + +## [9.15.1](https://github.com/appium/WebDriverAgent/compare/v9.15.0...v9.15.1) (2025-07-17) + +### Miscellaneous Chores + +* Remove the redundant check after activating the system app ([#1043](https://github.com/appium/WebDriverAgent/issues/1043)) ([33ccba1](https://github.com/appium/WebDriverAgent/commit/33ccba1ab3bc2980349f8553fd30aa5b08141b6b)) + +## [9.15.0](https://github.com/appium/WebDriverAgent/compare/v9.14.6...v9.15.0) (2025-07-10) + +### Features + +* HTTPS support for wda-client if webDriverAgentUrl is set ([#1042](https://github.com/appium/WebDriverAgent/issues/1042)) ([f7c4193](https://github.com/appium/WebDriverAgent/commit/f7c41939c793cdbc62e9c14d8eb91e06957bb566)) + +## [9.14.6](https://github.com/appium/WebDriverAgent/compare/v9.14.5...v9.14.6) (2025-06-24) + +### Miscellaneous Chores + +* add missing arch ([#1039](https://github.com/appium/WebDriverAgent/issues/1039)) ([a8dd958](https://github.com/appium/WebDriverAgent/commit/a8dd958bd92ef685bc1798ec04e92080b798d7d2)) + +## [9.14.5](https://github.com/appium/WebDriverAgent/compare/v9.14.4...v9.14.5) (2025-06-24) + +### Miscellaneous Chores + +* keep entire app for simulators ([d2bbcc6](https://github.com/appium/WebDriverAgent/commit/d2bbcc6d7af6b8eea076e24cd18429b74eeaffd6)) + +## [9.14.4](https://github.com/appium/WebDriverAgent/compare/v9.14.3...v9.14.4) (2025-06-23) + +### Miscellaneous Chores + +* include wda sim prebuilt for gh release ([#1038](https://github.com/appium/WebDriverAgent/issues/1038)) ([4423ecb](https://github.com/appium/WebDriverAgent/commit/4423ecb4f23c50343d8ffbf56a7753b985cbab81)) + +## [9.14.3](https://github.com/appium/WebDriverAgent/compare/v9.14.2...v9.14.3) (2025-06-13) + +### Miscellaneous Chores + +* **deps-dev:** bump sinon from 20.0.0 to 21.0.0 ([#1034](https://github.com/appium/WebDriverAgent/issues/1034)) ([5b205f4](https://github.com/appium/WebDriverAgent/commit/5b205f493f35cd1744cf9e33bce21e0f9e7c3bea)) + +## [9.14.2](https://github.com/appium/WebDriverAgent/compare/v9.14.1...v9.14.2) (2025-06-10) + +### Miscellaneous Chores + +* **deps-dev:** bump @types/node from 22.15.31 to 24.0.0 ([#1033](https://github.com/appium/WebDriverAgent/issues/1033)) ([e9705d9](https://github.com/appium/WebDriverAgent/commit/e9705d964e63222daaf0710bd3b860ca2ba6850f)) + +## [9.14.1](https://github.com/appium/WebDriverAgent/compare/v9.14.0...v9.14.1) (2025-06-09) + +### Miscellaneous Chores + +* add -Wno-reserved-identifier option ([#1032](https://github.com/appium/WebDriverAgent/issues/1032)) ([005dc21](https://github.com/appium/WebDriverAgent/commit/005dc216d9f41757763fe5b1714b68697fa8ee30)) + +## [9.14.0](https://github.com/appium/WebDriverAgent/compare/v9.13.0...v9.14.0) (2025-06-09) + +### Features + +* add minimum and maximum value attributes to page source ([#1031](https://github.com/appium/WebDriverAgent/issues/1031)) ([0e4e7e7](https://github.com/appium/WebDriverAgent/commit/0e4e7e7c483b9196edae576481f4e37f99fc8705)) + +## [9.13.0](https://github.com/appium/WebDriverAgent/compare/v9.12.0...v9.13.0) (2025-06-05) + +### Features + +* expose nativeFrame attribute in XML page source ([#1029](https://github.com/appium/WebDriverAgent/issues/1029)) ([5b56a45](https://github.com/appium/WebDriverAgent/commit/5b56a453f836cbc4358ce24ae43032658467c35c)) + +## [9.12.0](https://github.com/appium/WebDriverAgent/compare/v9.11.0...v9.12.0) (2025-06-04) + +### Features + +* add accessibility traits to XML page source ([#1028](https://github.com/appium/WebDriverAgent/issues/1028)) ([2df6649](https://github.com/appium/WebDriverAgent/commit/2df6649cb532d65a8c14633591b76c90185644cb)) + +## [9.11.0](https://github.com/appium/WebDriverAgent/compare/v9.10.1...v9.11.0) (2025-06-03) + +### Features + +* Add includeHittableInSource setting for including real hittable attribute in XML source ([#1026](https://github.com/appium/WebDriverAgent/issues/1026)) ([0fa4e74](https://github.com/appium/WebDriverAgent/commit/0fa4e7417404b5975445d381d111753fe681edd4)) + +## [9.10.1](https://github.com/appium/WebDriverAgent/compare/v9.10.0...v9.10.1) (2025-05-30) + +### Miscellaneous Chores + +* Make sure the same import style is used everywhere ([#1024](https://github.com/appium/WebDriverAgent/issues/1024)) ([1c50072](https://github.com/appium/WebDriverAgent/commit/1c50072457a8b82eec3684029386ccfa9432eccc)) + +## [9.10.0](https://github.com/appium/WebDriverAgent/compare/v9.9.0...v9.10.0) (2025-05-27) + +### Features + +* Add accessibility traits of the element ([#1020](https://github.com/appium/WebDriverAgent/issues/1020)) ([9465aaf](https://github.com/appium/WebDriverAgent/commit/9465aafd5e81ef57be7f78e9f2e188d3c1ba1bee)) + +### Bug Fixes + +* Use native snapshots if hittable attribute is requested in xPath ([#1023](https://github.com/appium/WebDriverAgent/issues/1023)) ([49d26cb](https://github.com/appium/WebDriverAgent/commit/49d26cb02a8515d1a1b52b65b7cb65512dfd749b)) + +## [9.9.0](https://github.com/appium/WebDriverAgent/compare/v9.8.0...v9.9.0) (2025-05-26) + +### Features + +* Use another snapshotting mechanism for the hittable attribute calculation ([#1022](https://github.com/appium/WebDriverAgent/issues/1022)) ([13c9f45](https://github.com/appium/WebDriverAgent/commit/13c9f453d890ad9b78fa7c47728ebae33880966a)) + +## [9.8.0](https://github.com/appium/WebDriverAgent/compare/v9.7.1...v9.8.0) (2025-05-21) + +### Features + +* Add a native frame property of the element ([#1017](https://github.com/appium/WebDriverAgent/issues/1017)) ([09214c4](https://github.com/appium/WebDriverAgent/commit/09214c4228ed5a49c02adead452cb0bb8dd83b6d)) + +## [9.7.1](https://github.com/appium/WebDriverAgent/compare/v9.7.0...v9.7.1) (2025-05-21) + +### Miscellaneous Chores + +* **deps-dev:** bump conventional-changelog-conventionalcommits ([#1019](https://github.com/appium/WebDriverAgent/issues/1019)) ([7108f7f](https://github.com/appium/WebDriverAgent/commit/7108f7f79575a1758bc7f05bd4ef790fd7694784)) + +## [9.7.0](https://github.com/appium/WebDriverAgent/compare/v9.6.3...v9.7.0) (2025-05-20) + +### Features + +* add placeholderValue to page source tree ([#1016](https://github.com/appium/WebDriverAgent/issues/1016)) ([509c207](https://github.com/appium/WebDriverAgent/commit/509c207b1366dd8582ba273edcdf77bfb30f53c9)) + +## [9.6.3](https://github.com/appium/WebDriverAgent/compare/v9.6.2...v9.6.3) (2025-05-18) + +### Miscellaneous Chores + +* Move the FBDoesElementSupportInnerText helper to a separate utility file ([#1018](https://github.com/appium/WebDriverAgent/issues/1018)) ([f17b07d](https://github.com/appium/WebDriverAgent/commit/f17b07d03abb6c2100405fda04326b7c35bfb48b)) + +## [9.6.2](https://github.com/appium/WebDriverAgent/compare/v9.6.1...v9.6.2) (2025-05-01) + +### Bug Fixes + +* release element screenshot data ([#1013](https://github.com/appium/WebDriverAgent/issues/1013)) ([a85f327](https://github.com/appium/WebDriverAgent/commit/a85f3271991556941234fbc888528051b1569db1)) + +## [9.6.1](https://github.com/appium/WebDriverAgent/compare/v9.6.0...v9.6.1) (2025-04-22) + +### Bug Fixes + +* allow setting precise resolution for the MJPEG stream ([#1009](https://github.com/appium/WebDriverAgent/issues/1009)) ([3f86eda](https://github.com/appium/WebDriverAgent/commit/3f86edafda42d955929f7cca870e2b8da54ae930)) + +## [9.6.0](https://github.com/appium/WebDriverAgent/compare/v9.5.2...v9.6.0) (2025-04-20) + +### Features + +* Split custom and standard snapshotting methods ([#1008](https://github.com/appium/WebDriverAgent/issues/1008)) ([8358856](https://github.com/appium/WebDriverAgent/commit/8358856f5968977b13d5cbdafac97f3053dae56e)) + +## [9.5.2](https://github.com/appium/WebDriverAgent/compare/v9.5.1...v9.5.2) (2025-04-19) + +### Bug Fixes + +* Missing text in long text for get text/value ([#1007](https://github.com/appium/WebDriverAgent/issues/1007)) ([6603a0b](https://github.com/appium/WebDriverAgent/commit/6603a0ba384917d39389509958ccac03ad174610)) + +## [9.5.1](https://github.com/appium/WebDriverAgent/compare/v9.5.0...v9.5.1) (2025-04-10) + +### Bug Fixes + +* Make sure we don't store element snapshot in the cache ([#1001](https://github.com/appium/WebDriverAgent/issues/1001)) ([cfe052b](https://github.com/appium/WebDriverAgent/commit/cfe052bb3adb3f3b24d0a34f386c60cf1516b308)) + +## [9.5.0](https://github.com/appium/WebDriverAgent/compare/v9.4.1...v9.5.0) (2025-04-10) + +### Features + +* Add support for the autoClickAlertSelector setting ([#1002](https://github.com/appium/WebDriverAgent/issues/1002)) ([fd31b95](https://github.com/appium/WebDriverAgent/commit/fd31b9589199d0a7bc76919f6aa7c7c74c498b90)) + +## [9.4.1](https://github.com/appium/WebDriverAgent/compare/v9.4.0...v9.4.1) (2025-04-05) + +### Miscellaneous Chores + +* bump appium-ios-simulator ([445741d](https://github.com/appium/WebDriverAgent/commit/445741d03313019016d4232f49e656d50f673f16)) + +## [9.4.0](https://github.com/appium/WebDriverAgent/compare/v9.3.3...v9.4.0) (2025-04-02) + +### Features + +* Always apply the native snapshotting strategy for XCUIApplication instances ([#998](https://github.com/appium/WebDriverAgent/issues/998)) ([60f5aef](https://github.com/appium/WebDriverAgent/commit/60f5aeffdda85faffd60aba416dc9d92987f19ac)) + +## [9.3.3](https://github.com/appium/WebDriverAgent/compare/v9.3.2...v9.3.3) (2025-03-27) + +### Bug Fixes + +* Properly set snapshot lookup scope if limitXpathContextScope is disabled ([#996](https://github.com/appium/WebDriverAgent/issues/996)) ([03ca7cd](https://github.com/appium/WebDriverAgent/commit/03ca7cd27b7cd92a45b344eb661db973c5dde809)) + +## [9.3.2](https://github.com/appium/WebDriverAgent/compare/v9.3.1...v9.3.2) (2025-03-26) + +### Bug Fixes + +* Adjust limitXPathContextScope setting name ([#995](https://github.com/appium/WebDriverAgent/issues/995)) ([9789e39](https://github.com/appium/WebDriverAgent/commit/9789e393b55bc682a9a8ef5a65fba5e4dbf752ce)) + +## [9.3.1](https://github.com/appium/WebDriverAgent/compare/v9.3.0...v9.3.1) (2025-03-25) + +### Miscellaneous Chores + +* **deps-dev:** bump sinon from 19.0.5 to 20.0.0 ([#994](https://github.com/appium/WebDriverAgent/issues/994)) ([f55462f](https://github.com/appium/WebDriverAgent/commit/f55462f4fa63314dfea48670d17ee54dc5fe2d96)) + +## [9.3.0](https://github.com/appium/WebDriverAgent/compare/v9.2.0...v9.3.0) (2025-03-21) + +### Features + +* Add /window/rect W3C endpoint ([#991](https://github.com/appium/WebDriverAgent/issues/991)) ([34f9510](https://github.com/appium/WebDriverAgent/commit/34f95107997bdec63219a2fd917de899de3e198c)) + +## [9.2.0](https://github.com/appium/WebDriverAgent/compare/v9.1.0...v9.2.0) (2025-03-13) + +### Features + +* Add 'limitXpathContextScope' setting ([#988](https://github.com/appium/WebDriverAgent/issues/988)) ([9c9d8af](https://github.com/appium/WebDriverAgent/commit/9c9d8af9c98ba7b2843a42f54354b78e126d2d27)) + +## [9.1.0](https://github.com/appium/WebDriverAgent/compare/v9.0.6...v9.1.0) (2025-03-09) + +### Features + +* add placeholderValue ([#987](https://github.com/appium/WebDriverAgent/issues/987)) ([8c3a1cb](https://github.com/appium/WebDriverAgent/commit/8c3a1cb30655ed8d1a77d25bbeca71ee48c2ec3e)) + +## [9.0.6](https://github.com/appium/WebDriverAgent/compare/v9.0.5...v9.0.6) (2025-02-28) + +### Bug Fixes + +* optimize LRU cache ([#985](https://github.com/appium/WebDriverAgent/issues/985)) ([46dc417](https://github.com/appium/WebDriverAgent/commit/46dc417da9f4a843838b414c0b154d6f478dbc0b)) + +## [9.0.5](https://github.com/appium/WebDriverAgent/compare/v9.0.4...v9.0.5) (2025-02-26) + +### Bug Fixes + +* add autorelease pool to drain temporary objects ([#983](https://github.com/appium/WebDriverAgent/issues/983)) ([f92f1cd](https://github.com/appium/WebDriverAgent/commit/f92f1cde0fe914086103a110844bbe3bc0e3c4a6)) + +## [9.0.4](https://github.com/appium/WebDriverAgent/compare/v9.0.3...v9.0.4) (2025-02-21) + +### Bug Fixes + +* Accept reqBasePath proxy option ([#982](https://github.com/appium/WebDriverAgent/issues/982)) ([19efbdd](https://github.com/appium/WebDriverAgent/commit/19efbdd69ff9edff20c0c318bd39c29963d4d51d)) + +## [9.0.3](https://github.com/appium/WebDriverAgent/compare/v9.0.2...v9.0.3) (2025-02-05) + +### Bug Fixes + +* add nullable signature ([#979](https://github.com/appium/WebDriverAgent/issues/979)) ([34b303c](https://github.com/appium/WebDriverAgent/commit/34b303c4e226d6a75a45a14eee7ca5e253e67737)) + +## [9.0.2](https://github.com/appium/WebDriverAgent/compare/v9.0.1...v9.0.2) (2025-02-03) + +### Bug Fixes + +* update docs link in xcodebuild error message ([#978](https://github.com/appium/WebDriverAgent/issues/978)) ([ea3863a](https://github.com/appium/WebDriverAgent/commit/ea3863a67d5cfa8bc2e48a1dc2c59052acd47937)) + +## [9.0.1](https://github.com/appium/WebDriverAgent/compare/v9.0.0...v9.0.1) (2025-01-17) + +### Miscellaneous Chores + +* Optimize stable instance retrieval ([#973](https://github.com/appium/WebDriverAgent/issues/973)) ([f2c752d](https://github.com/appium/WebDriverAgent/commit/f2c752db4707b3864efb62b95b64abb487d28e4b)) + +## [9.0.0](https://github.com/appium/WebDriverAgent/compare/v8.12.2...v9.0.0) (2025-01-16) + +### ⚠ BREAKING CHANGES + +* snapshotTimeout and customSnapshotTimeout settings have been removed as a result of the custom snapshotting logic removal + +### Features + +* Refactor snapshotting mechanism ([#970](https://github.com/appium/WebDriverAgent/issues/970)) ([08f1306](https://github.com/appium/WebDriverAgent/commit/08f13060119c710f53b34a98c95683287c0365a0)) + +## [8.12.2](https://github.com/appium/WebDriverAgent/compare/v8.12.1...v8.12.2) (2025-01-13) + +### Miscellaneous Chores + +* Exclude element visibility and accessibility info from the accessibility audit details ([#968](https://github.com/appium/WebDriverAgent/issues/968)) ([f62afc3](https://github.com/appium/WebDriverAgent/commit/f62afc372c123bdd8dd7bb493f653bb128144d24)) + +## [8.12.1](https://github.com/appium/WebDriverAgent/compare/v8.12.0...v8.12.1) (2025-01-03) + +### Miscellaneous Chores + +* Bump eslint ([#965](https://github.com/appium/WebDriverAgent/issues/965)) ([17f49ec](https://github.com/appium/WebDriverAgent/commit/17f49ec5a54e97b0ef0d20a3e39fc96b32575e43)) + +## [8.12.0](https://github.com/appium/WebDriverAgent/compare/v8.11.3...v8.12.0) (2024-12-13) + +### Features + +* look for critical notification in respectSystemAlerts ([#962](https://github.com/appium/WebDriverAgent/issues/962)) ([916c8c5](https://github.com/appium/WebDriverAgent/commit/916c8c557a9366608df211f33b5b7fbb0354dad3)) + +## [8.11.3](https://github.com/appium/WebDriverAgent/compare/v8.11.2...v8.11.3) (2024-12-06) + +### Miscellaneous Chores + +* **deps:** bump @appium/support from 5.1.8 to 6.0.0 ([#960](https://github.com/appium/WebDriverAgent/issues/960)) ([dbeb09c](https://github.com/appium/WebDriverAgent/commit/dbeb09c89f8c02e00a7bdffe7899650d435f3575)) + +## [8.11.2](https://github.com/appium/WebDriverAgent/compare/v8.11.1...v8.11.2) (2024-12-03) + +### Miscellaneous Chores + +* **deps-dev:** bump mocha from 10.8.2 to 11.0.1 ([#959](https://github.com/appium/WebDriverAgent/issues/959)) ([55b49c8](https://github.com/appium/WebDriverAgent/commit/55b49c83581c9e88f70806d98015238de3104f19)) + ## [8.11.1](https://github.com/appium/WebDriverAgent/compare/v8.11.0...v8.11.1) (2024-11-11) ### Miscellaneous Chores diff --git a/WebDriverAgent/Configurations/IOSSettings.xcconfig b/WebDriverAgent/Configurations/IOSSettings.xcconfig index b9969a48b2..3bde5a21f2 100644 --- a/WebDriverAgent/Configurations/IOSSettings.xcconfig +++ b/WebDriverAgent/Configurations/IOSSettings.xcconfig @@ -28,4 +28,4 @@ RUN_CLANG_STATIC_ANALYZER = YES GCC_PREPROCESSOR_DEFINITIONS = $(inherited) -WARNING_CFLAGS = $(inherited) -Weverything -Wno-objc-missing-property-synthesis -Wno-unused-macros -Wno-disabled-macro-expansion -Wno-gnu-statement-expression -Wno-language-extension-token -Wno-overriding-method-mismatch -Wno-missing-variable-declarations -Rno-module-build -Wno-auto-import -Wno-objc-interface-ivars -Wno-documentation-unknown-command -Wno-reserved-id-macro -Wno-unused-parameter -Wno-gnu-conditional-omitted-operand -Wno-explicit-ownership-type -Wno-date-time -Wno-cast-align -Wno-cstring-format-directive -Wno-double-promotion -Wno-partial-availability -Wno-declaration-after-statement -Wno-objc-messaging-id -Wno-direct-ivar-access -Wno-cast-qual -Wno-deprecated-declarations +WARNING_CFLAGS = $(inherited) -Weverything -Wno-objc-missing-property-synthesis -Wno-unused-macros -Wno-disabled-macro-expansion -Wno-gnu-statement-expression -Wno-language-extension-token -Wno-overriding-method-mismatch -Wno-missing-variable-declarations -Rno-module-build -Wno-auto-import -Wno-objc-interface-ivars -Wno-documentation-unknown-command -Wno-reserved-id-macro -Wno-unused-parameter -Wno-gnu-conditional-omitted-operand -Wno-explicit-ownership-type -Wno-date-time -Wno-cast-align -Wno-cstring-format-directive -Wno-double-promotion -Wno-partial-availability -Wno-declaration-after-statement -Wno-objc-messaging-id -Wno-direct-ivar-access -Wno-cast-qual -Wno-deprecated-declarations -Wno-reserved-identifier diff --git a/WebDriverAgent/Configurations/TVOSSettings.xcconfig b/WebDriverAgent/Configurations/TVOSSettings.xcconfig index b9969a48b2..3bde5a21f2 100644 --- a/WebDriverAgent/Configurations/TVOSSettings.xcconfig +++ b/WebDriverAgent/Configurations/TVOSSettings.xcconfig @@ -28,4 +28,4 @@ RUN_CLANG_STATIC_ANALYZER = YES GCC_PREPROCESSOR_DEFINITIONS = $(inherited) -WARNING_CFLAGS = $(inherited) -Weverything -Wno-objc-missing-property-synthesis -Wno-unused-macros -Wno-disabled-macro-expansion -Wno-gnu-statement-expression -Wno-language-extension-token -Wno-overriding-method-mismatch -Wno-missing-variable-declarations -Rno-module-build -Wno-auto-import -Wno-objc-interface-ivars -Wno-documentation-unknown-command -Wno-reserved-id-macro -Wno-unused-parameter -Wno-gnu-conditional-omitted-operand -Wno-explicit-ownership-type -Wno-date-time -Wno-cast-align -Wno-cstring-format-directive -Wno-double-promotion -Wno-partial-availability -Wno-declaration-after-statement -Wno-objc-messaging-id -Wno-direct-ivar-access -Wno-cast-qual -Wno-deprecated-declarations +WARNING_CFLAGS = $(inherited) -Weverything -Wno-objc-missing-property-synthesis -Wno-unused-macros -Wno-disabled-macro-expansion -Wno-gnu-statement-expression -Wno-language-extension-token -Wno-overriding-method-mismatch -Wno-missing-variable-declarations -Rno-module-build -Wno-auto-import -Wno-objc-interface-ivars -Wno-documentation-unknown-command -Wno-reserved-id-macro -Wno-unused-parameter -Wno-gnu-conditional-omitted-operand -Wno-explicit-ownership-type -Wno-date-time -Wno-cast-align -Wno-cstring-format-directive -Wno-double-promotion -Wno-partial-availability -Wno-declaration-after-statement -Wno-objc-messaging-id -Wno-direct-ivar-access -Wno-cast-qual -Wno-deprecated-declarations -Wno-reserved-identifier diff --git a/WebDriverAgent/Configurations/TVOSTestSettings.xcconfig b/WebDriverAgent/Configurations/TVOSTestSettings.xcconfig index 08679e4214..17824cc366 100644 --- a/WebDriverAgent/Configurations/TVOSTestSettings.xcconfig +++ b/WebDriverAgent/Configurations/TVOSTestSettings.xcconfig @@ -1,2 +1 @@ EXCLUDED_ARCHS = i386 - diff --git a/WebDriverAgent/PATENTS b/WebDriverAgent/PATENTS deleted file mode 100644 index f5c989c54e..0000000000 --- a/WebDriverAgent/PATENTS +++ /dev/null @@ -1,33 +0,0 @@ -Additional Grant of Patent Rights Version 2 - -"Software" means the WebDriverAgent software distributed by Facebook, Inc. - -Facebook, Inc. ("Facebook") hereby grants to each recipient of the Software -("you") a perpetual, worldwide, royalty-free, non-exclusive, irrevocable -(subject to the termination provision below) license under any Necessary -Claims, to make, have made, use, sell, offer to sell, import, and otherwise -transfer the Software. For avoidance of doubt, no license is granted under -Facebook’s rights in any patent claims that are infringed by (i) modifications -to the Software made by you or any third party or (ii) the Software in -combination with any software or other technology. - -The license granted hereunder will terminate, automatically and without notice, -if you (or any of your subsidiaries, corporate affiliates or agents) initiate -directly or indirectly, or take a direct financial interest in, any Patent -Assertion: (i) against Facebook or any of its subsidiaries or corporate -affiliates, (ii) against any party if such Patent Assertion arises in whole or -in part from any software, technology, product or service of Facebook or any of -its subsidiaries or corporate affiliates, or (iii) against any party relating -to the Software. Notwithstanding the foregoing, if Facebook or any of its -subsidiaries or corporate affiliates files a lawsuit alleging patent -infringement against you in the first instance, and you respond by filing a -patent infringement counterclaim in that lawsuit against that party that is -unrelated to the Software, the license granted hereunder will not terminate -under section (i) of this paragraph due to such counterclaim. - -A "Necessary Claim" is a claim of a patent owned by Facebook that is -necessarily infringed by the Software standing alone. - -A "Patent Assertion" is any lawsuit or other action alleging direct, indirect, -or contributory infringement or inducement to infringe any patent, including a -cross-claim or counterclaim. diff --git a/WebDriverAgent/PrivateHeaders/XCTest/XCTestManager_ManagerInterface-Protocol.h b/WebDriverAgent/PrivateHeaders/XCTest/XCTestManager_ManagerInterface-Protocol.h index 8ed03ebf75..d68f25c179 100644 --- a/WebDriverAgent/PrivateHeaders/XCTest/XCTestManager_ManagerInterface-Protocol.h +++ b/WebDriverAgent/PrivateHeaders/XCTest/XCTestManager_ManagerInterface-Protocol.h @@ -27,6 +27,7 @@ - (void)_XCT_requestElementAtPoint:(CGPoint)arg1 reply:(void (^)(id/*XCAccessibilityElement*/, NSError *))arg2; - (void)_XCT_fetchParameterizedAttributeForElement:(id/*XCAccessibilityElement*/)arg1 attributes:(NSNumber *)arg2 parameter:(id)arg3 reply:(void (^)(id, NSError *))arg4; - (void)_XCT_setAttribute:(NSNumber *)arg1 value:(id)arg2 element:(id/*XCAccessibilityElement*/)arg3 reply:(void (^)(BOOL, NSError *))arg4; +- (void)_XCT_fetchAttributes:(id)attributes forElement:(id)element reply:(void (^)(NSDictionary *, NSError *))reply; - (void)_XCT_fetchAttributesForElement:(id/*XCAccessibilityElement*/)arg1 attributes:(NSArray *)arg2 reply:(void (^)(NSDictionary *, NSError *))arg3; - (void)_XCT_terminateApplicationWithBundleID:(NSString *)arg1 completion:(void (^)(NSError *))arg2; - (void)_XCT_performAccessibilityAction:(int)arg1 onElement:(id/*XCAccessibilityElement*/)arg2 withValue:(id)arg3 reply:(void (^)(NSError *))arg4; diff --git a/WebDriverAgent/README.md b/WebDriverAgent/README.md index 547559ebc8..a367763f77 100644 --- a/WebDriverAgent/README.md +++ b/WebDriverAgent/README.md @@ -40,7 +40,7 @@ Then, you find `WebDriverAgentRunner-Runner-sim-.zip` for iOS and `Web ## License -[`WebDriverAgent` is BSD-licensed](LICENSE). We also provide an additional [patent grant](PATENTS). +[`WebDriverAgent` is BSD-licensed](LICENSE). ## Third Party Sources diff --git a/WebDriverAgent/Scripts/build-webdriveragent.js b/WebDriverAgent/Scripts/build-webdriveragent.js index 1958b876da..b37bcbbc91 100644 --- a/WebDriverAgent/Scripts/build-webdriveragent.js +++ b/WebDriverAgent/Scripts/build-webdriveragent.js @@ -34,7 +34,7 @@ async function buildWebDriverAgent (xcodeVersion) { await exec('xcodebuild', ['clean', '-derivedDataPath', DERIVED_DATA_PATH, '-scheme', 'WebDriverAgentRunner'], { cwd: ROOT_DIR }); - } catch (ign) {} + } catch {} // Get Xcode version xcodeVersion = xcodeVersion || await xcode.getVersion(); diff --git a/WebDriverAgent/Scripts/build.sh b/WebDriverAgent/Scripts/build.sh old mode 100644 new mode 100755 index 64195368c3..698d58b006 --- a/WebDriverAgent/Scripts/build.sh +++ b/WebDriverAgent/Scripts/build.sh @@ -4,8 +4,7 @@ # All rights reserved. # # This source code is licensed under the BSD-style license found in the -# LICENSE file in the root directory of this source tree. An additional grant -# of patent rights can be found in the PATENTS file in the same directory. +# LICENSE file in the root directory of this source tree. # set -ex diff --git a/WebDriverAgent/Scripts/ci/build-real.sh b/WebDriverAgent/Scripts/ci/build-real.sh old mode 100644 new mode 100755 index 93eec11451..bb41b0b6f3 --- a/WebDriverAgent/Scripts/ci/build-real.sh +++ b/WebDriverAgent/Scripts/ci/build-real.sh @@ -1,7 +1,5 @@ #!/bin/bash -# To run build script for CI - xcodebuild clean build-for-testing \ -project WebDriverAgent.xcodeproj \ -derivedDataPath $DERIVED_DATA_PATH \ @@ -9,15 +7,18 @@ xcodebuild clean build-for-testing \ -destination "$DESTINATION" \ CODE_SIGNING_ALLOWED=NO ARCHS=arm64 -# Only .app is needed. - pushd $WD -# to remove test packages to refer to the device local instead of embedded ones -# XCTAutomationSupport.framework, XCTest.framewor, XCTestCore.framework, -# XCUIAutomation.framework, XCUnit.framework -rm -rf $SCHEME-Runner.app/Frameworks/XC*.framework - -zip -r $ZIP_PKG_NAME $SCHEME-Runner.app +# The reason why here excludes several frameworks are: +# - to remove test packages to refer to the device local instead of embedded ones +# XCTAutomationSupport.framework, XCTest.framewor, XCTestCore.framework, +# XCUIAutomation.framework, XCUnit.framework. +# This can be excluded only for real devices. +# - Xcode 16 started generating 5.9MB of 'Testing.framework', but it might not be necessary for WDA. +# - libXCTestSwiftSupport is used for Swift testing. WDA doesn't include Swift stuff, thus this is not needed. +zip -r $ZIP_PKG_NAME $SCHEME-Runner.app \ + -x "$SCHEME-Runner.app/Frameworks/XC*.framework*" \ + "$SCHEME-Runner.app/Frameworks/Testing.framework*" \ + "$SCHEME-Runner.app/Frameworks/libXCTestSwiftSupport.dylib" popd mv $WD/$ZIP_PKG_NAME ./ diff --git a/WebDriverAgent/Scripts/ci/build-sim.sh b/WebDriverAgent/Scripts/ci/build-sim.sh old mode 100644 new mode 100755 index de52abccf1..b04cb48b60 --- a/WebDriverAgent/Scripts/ci/build-sim.sh +++ b/WebDriverAgent/Scripts/ci/build-sim.sh @@ -1,19 +1,15 @@ #!/bin/bash -# To run build script for CI - xcodebuild clean build-for-testing \ -project WebDriverAgent.xcodeproj \ - -derivedDataPath wda_build \ + -derivedDataPath $DERIVED_DATA_PATH \ -scheme $SCHEME \ -destination "$DESTINATION" \ CODE_SIGNING_ALLOWED=NO ARCHS=$ARCHS -# simulator needs to build entire build files +pushd $WD -pushd wda_build -# to remove unnecessary space consuming files -rm -rf Build/Intermediates.noindex -zip -r $ZIP_PKG_NAME Build +# Simulators might have an issue to lauch if we drop frameworks even we don't use them. +zip -r $ZIP_PKG_NAME $SCHEME-Runner.app popd -mv wda_build/$ZIP_PKG_NAME ./ +mv $WD/$ZIP_PKG_NAME ./ diff --git a/WebDriverAgent/Scripts/fetch-prebuilt-wda.js b/WebDriverAgent/Scripts/fetch-prebuilt-wda.js index 8bedd9a3d5..4bd6840fac 100644 --- a/WebDriverAgent/Scripts/fetch-prebuilt-wda.js +++ b/WebDriverAgent/Scripts/fetch-prebuilt-wda.js @@ -47,7 +47,7 @@ async function fetchPrebuiltWebDriverAgentAssets () { try { const nameOfAgent = _.last(url.split('/')); agentsDownloading.push(downloadAgent(url, path.join(webdriveragentsDir, nameOfAgent))); - } catch (ign) { } + } catch { } } // Wait for them all to finish diff --git a/WebDriverAgent/WebDriverAgent.xcodeproj/project.pbxproj b/WebDriverAgent/WebDriverAgent.xcodeproj/project.pbxproj index bfc4868641..fa64f46fe2 100644 --- a/WebDriverAgent/WebDriverAgent.xcodeproj/project.pbxproj +++ b/WebDriverAgent/WebDriverAgent.xcodeproj/project.pbxproj @@ -7,6 +7,10 @@ objects = { /* Begin PBXBuildFile section */ + 0E0413382DF1E15100AF007C /* XCUIElement+FBMinMax.m in Sources */ = {isa = PBXBuildFile; fileRef = 0E0413372DF1E15100AF007C /* XCUIElement+FBMinMax.m */; }; + 0E0413392DF1E15100AF007C /* XCUIElement+FBMinMax.m in Sources */ = {isa = PBXBuildFile; fileRef = 0E0413372DF1E15100AF007C /* XCUIElement+FBMinMax.m */; }; + 0E04133B2DF1E15900AF007C /* XCUIElement+FBMinMax.h in Headers */ = {isa = PBXBuildFile; fileRef = 0E04133A2DF1E15900AF007C /* XCUIElement+FBMinMax.h */; }; + 0E04133C2DF1E15900AF007C /* XCUIElement+FBMinMax.h in Headers */ = {isa = PBXBuildFile; fileRef = 0E04133A2DF1E15900AF007C /* XCUIElement+FBMinMax.h */; }; 1357E296233D05240054BDB8 /* XCUIHitPointResult.h in Headers */ = {isa = PBXBuildFile; fileRef = 1357E295233D05240054BDB8 /* XCUIHitPointResult.h */; }; 1357E297233D05240054BDB8 /* XCUIHitPointResult.h in Headers */ = {isa = PBXBuildFile; fileRef = 1357E295233D05240054BDB8 /* XCUIHitPointResult.h */; }; 13815F6F2328D20400CDAB61 /* FBActiveAppDetectionPoint.h in Headers */ = {isa = PBXBuildFile; fileRef = 13815F6D2328D20400CDAB61 /* FBActiveAppDetectionPoint.h */; }; @@ -21,12 +25,12 @@ 13DE7A4A287C4005003243C6 /* FBXCDeviceEvent.h in Headers */ = {isa = PBXBuildFile; fileRef = 13DE7A47287C4005003243C6 /* FBXCDeviceEvent.h */; }; 13DE7A4B287C4005003243C6 /* FBXCDeviceEvent.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DE7A48287C4005003243C6 /* FBXCDeviceEvent.m */; }; 13DE7A4C287C4005003243C6 /* FBXCDeviceEvent.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DE7A48287C4005003243C6 /* FBXCDeviceEvent.m */; }; - 13DE7A4F287C46BB003243C6 /* FBXCElementSnapshot.h in Headers */ = {isa = PBXBuildFile; fileRef = 13DE7A4D287C46BB003243C6 /* FBXCElementSnapshot.h */; }; - 13DE7A50287C46BB003243C6 /* FBXCElementSnapshot.h in Headers */ = {isa = PBXBuildFile; fileRef = 13DE7A4D287C46BB003243C6 /* FBXCElementSnapshot.h */; }; + 13DE7A4F287C46BB003243C6 /* FBXCElementSnapshot.h in Headers */ = {isa = PBXBuildFile; fileRef = 13DE7A4D287C46BB003243C6 /* FBXCElementSnapshot.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 13DE7A50287C46BB003243C6 /* FBXCElementSnapshot.h in Headers */ = {isa = PBXBuildFile; fileRef = 13DE7A4D287C46BB003243C6 /* FBXCElementSnapshot.h */; settings = {ATTRIBUTES = (Public, ); }; }; 13DE7A51287C46BB003243C6 /* FBXCElementSnapshot.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DE7A4E287C46BB003243C6 /* FBXCElementSnapshot.m */; }; 13DE7A52287C46BB003243C6 /* FBXCElementSnapshot.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DE7A4E287C46BB003243C6 /* FBXCElementSnapshot.m */; }; - 13DE7A55287CA1EC003243C6 /* FBXCElementSnapshotWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 13DE7A53287CA1EC003243C6 /* FBXCElementSnapshotWrapper.h */; }; - 13DE7A56287CA1EC003243C6 /* FBXCElementSnapshotWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 13DE7A53287CA1EC003243C6 /* FBXCElementSnapshotWrapper.h */; }; + 13DE7A55287CA1EC003243C6 /* FBXCElementSnapshotWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 13DE7A53287CA1EC003243C6 /* FBXCElementSnapshotWrapper.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 13DE7A56287CA1EC003243C6 /* FBXCElementSnapshotWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 13DE7A53287CA1EC003243C6 /* FBXCElementSnapshotWrapper.h */; settings = {ATTRIBUTES = (Public, ); }; }; 13DE7A57287CA1EC003243C6 /* FBXCElementSnapshotWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DE7A54287CA1EC003243C6 /* FBXCElementSnapshotWrapper.m */; }; 13DE7A58287CA1EC003243C6 /* FBXCElementSnapshotWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DE7A54287CA1EC003243C6 /* FBXCElementSnapshotWrapper.m */; }; 13DE7A5B287CA444003243C6 /* FBXCElementSnapshotWrapper+Helpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 13DE7A59287CA444003243C6 /* FBXCElementSnapshotWrapper+Helpers.h */; }; @@ -374,6 +378,10 @@ 7155D704211DCEF400166C20 /* FBMjpegServer.m in Sources */ = {isa = PBXBuildFile; fileRef = 7155D702211DCEF400166C20 /* FBMjpegServer.m */; }; 7157B291221DADD2001C348C /* FBXCAXClientProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = 7157B28F221DADD2001C348C /* FBXCAXClientProxy.h */; }; 7157B292221DADD2001C348C /* FBXCAXClientProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = 7157B290221DADD2001C348C /* FBXCAXClientProxy.m */; }; + 715A84CF2DD92AD3007134CC /* FBElementHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 715A84CE2DD92AD3007134CC /* FBElementHelpers.m */; }; + 715A84D02DD92AD3007134CC /* FBElementHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 715A84CD2DD92AD3007134CC /* FBElementHelpers.h */; }; + 715A84D12DD92AD3007134CC /* FBElementHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 715A84CE2DD92AD3007134CC /* FBElementHelpers.m */; }; + 715A84D22DD92AD3007134CC /* FBElementHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 715A84CD2DD92AD3007134CC /* FBElementHelpers.h */; }; 715AFAC11FFA29180053896D /* FBScreen.h in Headers */ = {isa = PBXBuildFile; fileRef = 715AFABF1FFA29180053896D /* FBScreen.h */; }; 715AFAC21FFA29180053896D /* FBScreen.m in Sources */ = {isa = PBXBuildFile; fileRef = 715AFAC01FFA29180053896D /* FBScreen.m */; }; 715AFAC41FFA2AAF0053896D /* FBScreenTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 715AFAC31FFA2AAF0053896D /* FBScreenTests.m */; }; @@ -451,6 +459,10 @@ 71A7EAFA1E224648001DA4F2 /* FBClassChainQueryParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 71A7EAF81E224648001DA4F2 /* FBClassChainQueryParser.m */; }; 71A7EAFC1E229302001DA4F2 /* FBClassChainTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 71A7EAFB1E229302001DA4F2 /* FBClassChainTests.m */; }; 71ACF5B8242F2FDC00F0AAD4 /* FBSafariAlertTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 71ACF5B7242F2FDC00F0AAD4 /* FBSafariAlertTests.m */; }; + 71AE3CF72D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.h in Headers */ = {isa = PBXBuildFile; fileRef = 71AE3CF52D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.h */; }; + 71AE3CF82D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.m in Sources */ = {isa = PBXBuildFile; fileRef = 71AE3CF62D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.m */; }; + 71AE3CF92D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.h in Headers */ = {isa = PBXBuildFile; fileRef = 71AE3CF52D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.h */; }; + 71AE3CFA2D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.m in Sources */ = {isa = PBXBuildFile; fileRef = 71AE3CF62D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.m */; }; 71B155DA23070ECF00646AFB /* FBHTTPStatusCodes.h in Headers */ = {isa = PBXBuildFile; fileRef = 71B155D923070ECF00646AFB /* FBHTTPStatusCodes.h */; settings = {ATTRIBUTES = (Public, ); }; }; 71B155DC230711E900646AFB /* FBCommandStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = 71B155DB230711E900646AFB /* FBCommandStatus.m */; }; 71B155DF23080CA600646AFB /* FBProtocolHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 71B155DD23080CA600646AFB /* FBProtocolHelpers.h */; }; @@ -531,6 +543,10 @@ ADBC39981D07842800327304 /* XCUIElementDouble.m in Sources */ = {isa = PBXBuildFile; fileRef = ADBC39971D07842800327304 /* XCUIElementDouble.m */; }; ADDA07241D6BB2BF001700AC /* FBScrollViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = ADDA07231D6BB2BF001700AC /* FBScrollViewController.m */; }; ADEF63AF1D09DEBE0070A7E3 /* FBRuntimeUtilsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = ADEF63AE1D09DEBE0070A7E3 /* FBRuntimeUtilsTests.m */; }; + B316351C2DDF0CF5007D9317 /* FBAccessibilityTraits.m in Sources */ = {isa = PBXBuildFile; fileRef = B316351B2DDF0CF5007D9317 /* FBAccessibilityTraits.m */; }; + B316351D2DDF0CF5007D9317 /* FBAccessibilityTraits.m in Sources */ = {isa = PBXBuildFile; fileRef = B316351B2DDF0CF5007D9317 /* FBAccessibilityTraits.m */; }; + B316351F2DDF0D0B007D9317 /* FBAccessibilityTraits.h in Headers */ = {isa = PBXBuildFile; fileRef = B316351E2DDF0D0B007D9317 /* FBAccessibilityTraits.h */; }; + B31635202DDF0D0B007D9317 /* FBAccessibilityTraits.h in Headers */ = {isa = PBXBuildFile; fileRef = B316351E2DDF0D0B007D9317 /* FBAccessibilityTraits.h */; }; C845206222D5E79400EA68CB /* FBUnattachedAppLauncher.h in Headers */ = {isa = PBXBuildFile; fileRef = C8FB547722D4C1FC00B69954 /* FBUnattachedAppLauncher.h */; }; C845206322D5E79700EA68CB /* FBUnattachedAppLauncher.m in Sources */ = {isa = PBXBuildFile; fileRef = C8FB547822D4C1FC00B69954 /* FBUnattachedAppLauncher.m */; }; C8FB547422D3949C00B69954 /* LSApplicationWorkspace.h in Headers */ = {isa = PBXBuildFile; fileRef = C8FB547322D3949C00B69954 /* LSApplicationWorkspace.h */; }; @@ -783,6 +799,7 @@ EE9B76591CF7987800275851 /* FBRouteTests.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9B76571CF7987300275851 /* FBRouteTests.m */; }; EE9B768E1CF7997600275851 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9B76831CF7997600275851 /* AppDelegate.m */; }; EE9B768F1CF7997600275851 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9B76851CF7997600275851 /* ViewController.m */; }; + AABBCCDDEEFF001122334457 /* SceneDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = AABBCCDDEEFF001122334456 /* SceneDelegate.m */; }; EE9B76911CF7997600275851 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9B76871CF7997600275851 /* main.m */; }; EE9B76941CF7997600275851 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = EE9B768C1CF7997600275851 /* Main.storyboard */; }; EE9B769A1CF799F400275851 /* FBAlertTests.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9B76991CF799F400275851 /* FBAlertTests.m */; }; @@ -906,6 +923,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 0E0413372DF1E15100AF007C /* XCUIElement+FBMinMax.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "XCUIElement+FBMinMax.m"; sourceTree = ""; }; + 0E04133A2DF1E15900AF007C /* XCUIElement+FBMinMax.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XCUIElement+FBMinMax.h"; sourceTree = ""; }; 1357E295233D05240054BDB8 /* XCUIHitPointResult.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = XCUIHitPointResult.h; sourceTree = ""; }; 13815F6D2328D20400CDAB61 /* FBActiveAppDetectionPoint.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBActiveAppDetectionPoint.h; sourceTree = ""; }; 13815F6E2328D20400CDAB61 /* FBActiveAppDetectionPoint.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBActiveAppDetectionPoint.m; sourceTree = ""; }; @@ -1002,6 +1021,8 @@ 7155D702211DCEF400166C20 /* FBMjpegServer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBMjpegServer.m; sourceTree = ""; }; 7157B28F221DADD2001C348C /* FBXCAXClientProxy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBXCAXClientProxy.h; sourceTree = ""; }; 7157B290221DADD2001C348C /* FBXCAXClientProxy.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBXCAXClientProxy.m; sourceTree = ""; }; + 715A84CD2DD92AD3007134CC /* FBElementHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBElementHelpers.h; sourceTree = ""; }; + 715A84CE2DD92AD3007134CC /* FBElementHelpers.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBElementHelpers.m; sourceTree = ""; }; 715AFABF1FFA29180053896D /* FBScreen.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBScreen.h; sourceTree = ""; }; 715AFAC01FFA29180053896D /* FBScreen.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBScreen.m; sourceTree = ""; }; 715AFAC31FFA2AAF0053896D /* FBScreenTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBScreenTests.m; sourceTree = ""; }; @@ -1050,6 +1071,8 @@ 71A7EAF81E224648001DA4F2 /* FBClassChainQueryParser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBClassChainQueryParser.m; sourceTree = ""; }; 71A7EAFB1E229302001DA4F2 /* FBClassChainTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBClassChainTests.m; sourceTree = ""; }; 71ACF5B7242F2FDC00F0AAD4 /* FBSafariAlertTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBSafariAlertTests.m; sourceTree = ""; }; + 71AE3CF52D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XCUIElement+FBVisibleFrame.h"; sourceTree = ""; }; + 71AE3CF62D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "XCUIElement+FBVisibleFrame.m"; sourceTree = ""; }; 71B155D923070ECF00646AFB /* FBHTTPStatusCodes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBHTTPStatusCodes.h; sourceTree = ""; }; 71B155DB230711E900646AFB /* FBCommandStatus.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBCommandStatus.m; sourceTree = ""; }; 71B155DD23080CA600646AFB /* FBProtocolHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBProtocolHelpers.h; sourceTree = ""; }; @@ -1103,6 +1126,8 @@ ADDA07221D6BB2BF001700AC /* FBScrollViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBScrollViewController.h; sourceTree = ""; }; ADDA07231D6BB2BF001700AC /* FBScrollViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBScrollViewController.m; sourceTree = ""; }; ADEF63AE1D09DEBE0070A7E3 /* FBRuntimeUtilsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBRuntimeUtilsTests.m; sourceTree = ""; }; + B316351B2DDF0CF5007D9317 /* FBAccessibilityTraits.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBAccessibilityTraits.m; sourceTree = ""; }; + B316351E2DDF0D0B007D9317 /* FBAccessibilityTraits.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBAccessibilityTraits.h; sourceTree = ""; }; C8FB547322D3949C00B69954 /* LSApplicationWorkspace.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSApplicationWorkspace.h; sourceTree = ""; }; C8FB547722D4C1FC00B69954 /* FBUnattachedAppLauncher.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBUnattachedAppLauncher.h; sourceTree = ""; }; C8FB547822D4C1FC00B69954 /* FBUnattachedAppLauncher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBUnattachedAppLauncher.m; sourceTree = ""; }; @@ -1344,6 +1369,8 @@ EE9B76581CF7987300275851 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; EE9B76821CF7997600275851 /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; EE9B76831CF7997600275851 /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + AABBCCDDEEFF001122334455 /* SceneDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SceneDelegate.h; sourceTree = ""; }; + AABBCCDDEEFF001122334456 /* SceneDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SceneDelegate.m; sourceTree = ""; }; EE9B76841CF7997600275851 /* ViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; EE9B76851CF7997600275851 /* ViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; EE9B76861CF7997600275851 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -1767,6 +1794,8 @@ EE8DDD7C20C5733B004D4925 /* XCUIElement+FBForceTouch.m */, EE9AB7471CAEDF0C008C271F /* XCUIElement+FBIsVisible.h */, EE9AB7481CAEDF0C008C271F /* XCUIElement+FBIsVisible.m */, + 0E04133A2DF1E15900AF007C /* XCUIElement+FBMinMax.h */, + 0E0413372DF1E15100AF007C /* XCUIElement+FBMinMax.m */, 7136A4771E8918E60024FC3D /* XCUIElement+FBPickerWheel.h */, 7136A4781E8918E60024FC3D /* XCUIElement+FBPickerWheel.m */, 71D3B3D3267FC7260076473D /* XCUIElement+FBResolve.h */, @@ -1781,6 +1810,8 @@ 71B49EC61ED1A58100D51AD6 /* XCUIElement+FBUID.m */, EEE3763F1D59F81400ED88DD /* XCUIElement+FBUtilities.h */, EEE376401D59F81400ED88DD /* XCUIElement+FBUtilities.m */, + 71AE3CF52D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.h */, + 71AE3CF62D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.m */, EEE376471D59FAE900ED88DD /* XCUIElement+FBWebDriverAttributes.h */, EEE376481D59FAE900ED88DD /* XCUIElement+FBWebDriverAttributes.m */, 641EE7042240CDCF00173FCB /* XCUIElement+FBTVFocuse.h */, @@ -1904,6 +1935,8 @@ EE9B76A21CF7A43900275851 /* FBConfiguration.m */, EE7E27181D06C69F001BEC7B /* FBDebugLogDelegateDecorator.h */, EE7E27191D06C69F001BEC7B /* FBDebugLogDelegateDecorator.m */, + 715A84CD2DD92AD3007134CC /* FBElementHelpers.h */, + 715A84CE2DD92AD3007134CC /* FBElementHelpers.m */, EE9AB78F1CAEDF0C008C271F /* FBElementTypeTransformer.h */, EE9AB7901CAEDF0C008C271F /* FBElementTypeTransformer.m */, EE3A18601CDE618F00DE4205 /* FBErrorBuilder.h */, @@ -1963,6 +1996,8 @@ EE6B64FC1D0F86EF00E85F5D /* XCTestPrivateSymbols.m */, 633E904A220DEE7F007CADF9 /* XCUIApplicationProcessDelay.h */, 6385F4A5220A40760095BBDB /* XCUIApplicationProcessDelay.m */, + B316351B2DDF0CF5007D9317 /* FBAccessibilityTraits.m */, + B316351E2DDF0D0B007D9317 /* FBAccessibilityTraits.h */, ); name = Utilities; path = WebDriverAgentLib/Utilities; @@ -2075,6 +2110,8 @@ children = ( EE9B76821CF7997600275851 /* AppDelegate.h */, EE9B76831CF7997600275851 /* AppDelegate.m */, + AABBCCDDEEFF001122334455 /* SceneDelegate.h */, + AABBCCDDEEFF001122334456 /* SceneDelegate.m */, EE1E06E51D182E95007CF043 /* FBAlertViewController.h */, EE1E06E61D182E95007CF043 /* FBAlertViewController.m */, EE8BA9781DCCED9A00A9DEF8 /* FBNavigationController.h */, @@ -2276,6 +2313,8 @@ 641EE6392240C5CA00173FCB /* FBRouteRequest.h in Headers */, 648C10AC22AAAD9C00B81B9A /* UIKeyboardImpl.h in Headers */, 718226CD2587443700661B83 /* GCDAsyncSocket.h in Headers */, + 13DE7A50287C46BB003243C6 /* FBXCElementSnapshot.h in Headers */, + 13DE7A56287CA1EC003243C6 /* FBXCElementSnapshotWrapper.h in Headers */, 71F3E7D525417FF400E0C22B /* FBSettings.h in Headers */, 641EE63A2240C5CA00173FCB /* XCTest.h in Headers */, 641EE63B2240C5CA00173FCB /* FBAlertsMonitor.h in Headers */, @@ -2324,7 +2363,6 @@ 641EE6632240C5CA00173FCB /* FBUnknownCommands.h in Headers */, 641EE7062240CDCF00173FCB /* XCUIElement+FBTVFocuse.h in Headers */, 71822738258744B800661B83 /* HTTPConnection.h in Headers */, - 13DE7A56287CA1EC003243C6 /* FBXCElementSnapshotWrapper.h in Headers */, 641EE6642240C5CA00173FCB /* NSPredicate+FBFormat.h in Headers */, 641EE6652240C5CA00173FCB /* UILongPressGestureRecognizer-RecordingAdditions.h in Headers */, 641EE6662240C5CA00173FCB /* XCTestCase.h in Headers */, @@ -2346,11 +2384,13 @@ 641EE6732240C5CA00173FCB /* FBDebugCommands.h in Headers */, 641EE6742240C5CA00173FCB /* XCTestSuite.h in Headers */, 641EE6752240C5CA00173FCB /* XCUICoordinate.h in Headers */, + 715A84D22DD92AD3007134CC /* FBElementHelpers.h in Headers */, 641EE6762240C5CA00173FCB /* XCTNSPredicateExpectation.h in Headers */, 641EE6772240C5CA00173FCB /* XCTestObservationCenter.h in Headers */, 641EE6782240C5CA00173FCB /* XCTNSNotificationExpectation.h in Headers */, 641EE6792240C5CA00173FCB /* XCUIRecorderNodeFinder.h in Headers */, 641EE67A2240C5CA00173FCB /* XCUIElement+FBAccessibility.h in Headers */, + 0E04133C2DF1E15900AF007C /* XCUIElement+FBMinMax.h in Headers */, 641EE67B2240C5CA00173FCB /* XCUIRecorderUtilities.h in Headers */, 6496A5DA230D6EB30087F8CB /* AXSettings.h in Headers */, 641EE67C2240C5CA00173FCB /* XCTestCaseRun.h in Headers */, @@ -2399,9 +2439,11 @@ 641EE6A42240C5CA00173FCB /* FBCommandHandler.h in Headers */, 641EE6A52240C5CA00173FCB /* FBSessionCommands.h in Headers */, 641EE70C2240CE2D00173FCB /* FBTVNavigationTracker.h in Headers */, + 71AE3CF72D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.h in Headers */, 641EE6A62240C5CA00173FCB /* FBImageProcessor.h in Headers */, 641EE6A72240C5CA00173FCB /* FBSession-Private.h in Headers */, 641EE6A82240C5CA00173FCB /* NSString+FBXMLSafeString.h in Headers */, + B316351F2DDF0D0B007D9317 /* FBAccessibilityTraits.h in Headers */, 64E3502F2AC0B6FE005F3ACB /* NSDictionary+FBUtf8SafeDictionary.h in Headers */, 641EE6A92240C5CA00173FCB /* FBCommandStatus.h in Headers */, 71822702258744A400661B83 /* HTTPResponseProxy.h in Headers */, @@ -2483,7 +2525,6 @@ 641EE6E72240C5CA00173FCB /* XCUIElement+FBFind.h in Headers */, 641EE6E82240C5CA00173FCB /* XCTestManager_ManagerInterface-Protocol.h in Headers */, 641EE6E92240C5CA00173FCB /* FBFailureProofTestCase.h in Headers */, - 13DE7A50287C46BB003243C6 /* FBXCElementSnapshot.h in Headers */, 641EE6EA2240C5CA00173FCB /* XCTTestRunSessionDelegate-Protocol.h in Headers */, 641EE6EB2240C5CA00173FCB /* XCTestCaseSuite.h in Headers */, 641EE6EC2240C5CA00173FCB /* _XCInternalTestRun.h in Headers */, @@ -2554,6 +2595,7 @@ EE35AD721E3B77D600A02D78 /* XCUIElementHitPointCoordinate.h in Headers */, EE35AD3F1E3B77D600A02D78 /* XCTDarwinNotificationExpectation.h in Headers */, EE35AD5F1E3B77D600A02D78 /* XCTRunnerAutomationSession.h in Headers */, + 13DE7A4F287C46BB003243C6 /* FBXCElementSnapshot.h in Headers */, 71C9EAAC25E8415A00470CD8 /* FBScreenshot.h in Headers */, EE35AD371E3B77D600A02D78 /* XCSourceCodeTreeNodeEnumerator.h in Headers */, EE158AB01CBD456F00A3E3F0 /* XCUIElement+FBIsVisible.h in Headers */, @@ -2585,6 +2627,7 @@ 714EAA0D2673FDFE005C5B47 /* FBCapabilities.h in Headers */, EE35AD5C1E3B77D600A02D78 /* XCTNSPredicateExpectation.h in Headers */, EE35AD521E3B77D600A02D78 /* XCTestObservationCenter.h in Headers */, + 71AE3CF92D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.h in Headers */, EE35AD5B1E3B77D600A02D78 /* XCTNSNotificationExpectation.h in Headers */, E444DC97249131D40060D7EB /* HTTPServer.h in Headers */, E444DCAE24913C220060D7EB /* HTTPResponseProxy.h in Headers */, @@ -2595,6 +2638,7 @@ EE35AD781E3B77D600A02D78 /* XCUIRecorderUtilities.h in Headers */, EE35AD421E3B77D600A02D78 /* XCTestCaseRun.h in Headers */, EE35AD441E3B77D600A02D78 /* XCTestConfiguration.h in Headers */, + 715A84D02DD92AD3007134CC /* FBElementHelpers.h in Headers */, EE35AD0B1E3B77D600A02D78 /* _XCTDarwinNotificationExpectationImplementation.h in Headers */, 718226CA2587443700661B83 /* GCDAsyncUdpSocket.h in Headers */, EE35AD491E3B77D600A02D78 /* XCTestExpectation.h in Headers */, @@ -2649,7 +2693,6 @@ EE158AB81CBD456F00A3E3F0 /* FBAlertViewCommands.h in Headers */, EE35AD651E3B77D600A02D78 /* XCTWaiter.h in Headers */, EE35AD681E3B77D600A02D78 /* XCTWaiterManagement-Protocol.h in Headers */, - 13DE7A4F287C46BB003243C6 /* FBXCElementSnapshot.h in Headers */, EE35AD451E3B77D600A02D78 /* XCTestContext.h in Headers */, EE35AD661E3B77D600A02D78 /* XCTWaiterDelegate-Protocol.h in Headers */, EE35AD0E1E3B77D600A02D78 /* _XCTestExpectationImplementation.h in Headers */, @@ -2672,6 +2715,7 @@ EEE9B4721CD02B88009D2030 /* FBRunLoopSpinner.h in Headers */, EE3A18621CDE618F00DE4205 /* FBErrorBuilder.h in Headers */, EE35AD261E3B77D600A02D78 /* XCApplicationMonitor_iOS.h in Headers */, + 0E04133B2DF1E15900AF007C /* XCUIElement+FBMinMax.h in Headers */, EE3A18661CDE734B00DE4205 /* FBKeyboard.h in Headers */, AD6C269C1CF2494200F8B5FF /* XCUIApplication+FBHelpers.h in Headers */, 714D88CC2733FB970074A925 /* FBXMLGenerationOptions.h in Headers */, @@ -2692,6 +2736,7 @@ EE35AD571E3B77D600A02D78 /* XCTestSuiteRun.h in Headers */, EE35AD701E3B77D600A02D78 /* XCUIElementAsynchronousHandlerWrapper.h in Headers */, EE35AD4C1E3B77D600A02D78 /* XCTestLog.h in Headers */, + B31635202DDF0D0B007D9317 /* FBAccessibilityTraits.h in Headers */, 71BB58E82B96328700CB9BFE /* FBScreenRecordingRequest.h in Headers */, EE35AD231E3B77D600A02D78 /* UITapGestureRecognizer-RecordingAdditions.h in Headers */, EE35AD2A1E3B77D600A02D78 /* XCDebugLogDelegate-Protocol.h in Headers */, @@ -3153,6 +3198,7 @@ 641EE6082240C5CA00173FCB /* FBRuntimeUtils.m in Sources */, 641EE6092240C5CA00173FCB /* XCUIElement+FBUtilities.m in Sources */, 641EE60A2240C5CA00173FCB /* FBLogger.m in Sources */, + B316351D2DDF0CF5007D9317 /* FBAccessibilityTraits.m in Sources */, 641EE60B2240C5CA00173FCB /* FBCustomCommands.m in Sources */, 71BB58E42B9631F100CB9BFE /* FBScreenRecordingPromise.m in Sources */, 641EE60C2240C5CA00173FCB /* XCUIDevice+FBHelpers.m in Sources */, @@ -3160,6 +3206,8 @@ 641EE60E2240C5CA00173FCB /* XCUIElement+FBTyping.m in Sources */, 641EE60F2240C5CA00173FCB /* XCUIElement+FBAccessibility.m in Sources */, 641EE6102240C5CA00173FCB /* FBImageUtils.m in Sources */, + 71AE3CF82D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.m in Sources */, + 715A84D12DD92AD3007134CC /* FBElementHelpers.m in Sources */, 641EE6112240C5CA00173FCB /* FBSession.m in Sources */, 641EE6122240C5CA00173FCB /* FBFindElementCommands.m in Sources */, 71A5C67629A4F39600421C37 /* XCTIssue+FBPatcher.m in Sources */, @@ -3169,6 +3217,7 @@ 71BB58F92B96531900CB9BFE /* FBScreenRecordingContainer.m in Sources */, 641EE6152240C5CA00173FCB /* XCUIElement+FBScrolling.m in Sources */, 641EE6162240C5CA00173FCB /* FBSessionCommands.m in Sources */, + 0E0413392DF1E15100AF007C /* XCUIElement+FBMinMax.m in Sources */, 641EE6192240C5CA00173FCB /* FBConfiguration.m in Sources */, 641EE61A2240C5CA00173FCB /* FBElementCache.m in Sources */, 71F5BE26252E576C00EE9EBA /* XCUIElement+FBSwiping.m in Sources */, @@ -3216,6 +3265,7 @@ 13DE7A45287C2A8D003243C6 /* FBXCAccessibilityElement.m in Sources */, 641EE70E2240CE4800173FCB /* FBTVNavigationTracker.m in Sources */, 71BD20741F86116100B36EC2 /* XCUIApplication+FBTouchAction.m in Sources */, + 0E0413382DF1E15100AF007C /* XCUIElement+FBMinMax.m in Sources */, EE158AE71CBD456F00A3E3F0 /* FBWebServer.m in Sources */, 715557D4211DBCE700613B26 /* FBTCPSocket.m in Sources */, EE3A18631CDE618F00DE4205 /* FBErrorBuilder.m in Sources */, @@ -3232,8 +3282,10 @@ 713AE576243A53BE0000D657 /* FBW3CActionsHelpers.m in Sources */, 71B155E123080CA600646AFB /* FBProtocolHelpers.m in Sources */, EE158AB11CBD456F00A3E3F0 /* XCUIElement+FBIsVisible.m in Sources */, + 71AE3CFA2D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.m in Sources */, EEBBD48C1D47746D00656A81 /* XCUIElement+FBFind.m in Sources */, EE158ADD1CBD456F00A3E3F0 /* FBResponsePayload.m in Sources */, + B316351C2DDF0CF5007D9317 /* FBAccessibilityTraits.m in Sources */, E444DCB524913C220060D7EB /* RouteRequest.m in Sources */, C8FB547A22D4C1FC00B69954 /* FBUnattachedAppLauncher.m in Sources */, EE158ADF1CBD456F00A3E3F0 /* FBRoute.m in Sources */, @@ -3302,6 +3354,7 @@ EE158AB31CBD456F00A3E3F0 /* XCUIElement+FBScrolling.m in Sources */, 718226CE2587443700661B83 /* GCDAsyncSocket.m in Sources */, EE158AC91CBD456F00A3E3F0 /* FBSessionCommands.m in Sources */, + 715A84CF2DD92AD3007134CC /* FBElementHelpers.m in Sources */, EE9B76A71CF7A43900275851 /* FBConfiguration.m in Sources */, E444DC9C249131D40060D7EB /* HTTPServer.m in Sources */, 71414ED82670A1EE003A8C5D /* LRUCache.m in Sources */, @@ -3405,6 +3458,7 @@ buildActionMask = 2147483647; files = ( EE9B768E1CF7997600275851 /* AppDelegate.m in Sources */, + AABBCCDDEEFF001122334457 /* SceneDelegate.m in Sources */, EE1E06E71D182E95007CF043 /* FBAlertViewController.m in Sources */, 315A15072518CC2800A3A064 /* TouchSpotView.m in Sources */, EE9B76911CF7997600275851 /* main.m in Sources */, @@ -3525,7 +3579,6 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; "CODE_SIGN_IDENTITY[sdk=appletvos*]" = "iPhone Developer"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = 48LP2W34XH; ENABLE_TESTING_SEARCH_PATHS = YES; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = WebDriverAgentRunner/Info.plist; @@ -3540,7 +3593,7 @@ ); MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.vkqa.WebDriverAgentRunner; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.WebDriverAgentRunner; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; TARGETED_DEVICE_FAMILY = 3; @@ -3586,7 +3639,6 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; "CODE_SIGN_IDENTITY[sdk=appletvos*]" = "iPhone Developer"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = 48LP2W34XH; ENABLE_TESTING_SEARCH_PATHS = YES; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = WebDriverAgentRunner/Info.plist; @@ -3601,7 +3653,7 @@ ); MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.vkqa.WebDriverAgentRunner; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.WebDriverAgentRunner; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; TARGETED_DEVICE_FAMILY = 3; @@ -3662,7 +3714,7 @@ /Developer/Library/Frameworks, ); OTHER_LDFLAGS = ""; - PRODUCT_BUNDLE_IDENTIFIER = com.vkqa.WebDriverAgentLib; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.WebDriverAgentLib; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; SKIP_INSTALL = YES; @@ -3729,7 +3781,7 @@ ); ONLY_ACTIVE_ARCH = YES; OTHER_LDFLAGS = ""; - PRODUCT_BUNDLE_IDENTIFIER = com.vkqa.WebDriverAgentLib; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.WebDriverAgentLib; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = appletvos; @@ -3960,11 +4012,9 @@ baseConfigurationReference = EEE5CABF1C80361500CBBDD9 /* IOSSettings.xcconfig */; buildSettings = { CLANG_STATIC_ANALYZER_MODE = deep; - CODE_SIGN_IDENTITY = "Apple Development"; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; DEFINES_MODULE = NO; - DEVELOPMENT_TEAM = 48LP2W34XH; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -3986,7 +4036,7 @@ /System/Developer/Library/Frameworks, ); OTHER_LDFLAGS = ""; - PRODUCT_BUNDLE_IDENTIFIER = com.vkqa.WebDriverAgentLib; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.WebDriverAgentLib; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; VERSIONING_SYSTEM = "apple-generic"; @@ -4027,10 +4077,8 @@ baseConfigurationReference = EEE5CABF1C80361500CBBDD9 /* IOSSettings.xcconfig */; buildSettings = { CLANG_STATIC_ANALYZER_MODE = deep; - CODE_SIGN_IDENTITY = "Apple Development"; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = NO; - DEVELOPMENT_TEAM = 48LP2W34XH; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -4053,7 +4101,7 @@ ); ONLY_ACTIVE_ARCH = YES; OTHER_LDFLAGS = ""; - PRODUCT_BUNDLE_IDENTIFIER = com.vkqa.WebDriverAgentLib; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.WebDriverAgentLib; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; VERSIONING_SYSTEM = "apple-generic"; @@ -4207,6 +4255,7 @@ CLANG_ANALYZER_NONNULL = YES; DEBUG_INFORMATION_FORMAT = dwarf; INFOPLIST_FILE = WebDriverAgentTests/IntegrationApp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -4223,6 +4272,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ANALYZER_NONNULL = YES; INFOPLIST_FILE = WebDriverAgentTests/IntegrationApp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -4277,7 +4327,6 @@ buildSettings = { CLANG_STATIC_ANALYZER_MODE = deep; DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = 48LP2W34XH; ENABLE_TESTING_SEARCH_PATHS = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = WebDriverAgentRunner/Info.plist; @@ -4294,7 +4343,7 @@ "$(inherited)", "-all_load", ); - PRODUCT_BUNDLE_IDENTIFIER = com.vkqa.WebDriverAgentRunner; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.WebDriverAgentRunner; PRODUCT_NAME = "$(TARGET_NAME)"; USES_XCTRUNNER = YES; WARNING_CFLAGS = ( @@ -4330,7 +4379,6 @@ baseConfigurationReference = EEE5CABF1C80361500CBBDD9 /* IOSSettings.xcconfig */; buildSettings = { CLANG_STATIC_ANALYZER_MODE = deep; - DEVELOPMENT_TEAM = 48LP2W34XH; ENABLE_TESTING_SEARCH_PATHS = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = WebDriverAgentRunner/Info.plist; @@ -4348,7 +4396,7 @@ "$(inherited)", "-all_load", ); - PRODUCT_BUNDLE_IDENTIFIER = com.vkqa.WebDriverAgentRunner; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.WebDriverAgentRunner; PRODUCT_NAME = "$(TARGET_NAME)"; USES_XCTRUNNER = YES; WARNING_CFLAGS = ( diff --git a/WebDriverAgent/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentRunner.xcscheme b/WebDriverAgent/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentRunner.xcscheme index 8857b934cf..4c2919f78f 100644 --- a/WebDriverAgent/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentRunner.xcscheme +++ b/WebDriverAgent/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentRunner.xcscheme @@ -66,6 +66,11 @@ value = "$(USE_PORT)" isEnabled = "YES"> + + + + snapshot, NSArray *types); @@ -65,10 +68,17 @@ - (NSString *)fb_description } - (id)fb_attributeValue:(NSString *)attribute + error:(NSError **)error { + NSDate *start = [NSDate date]; NSDictionary *result = [FBXCAXClientProxy.sharedClient attributesForElement:[self accessibilityElement] - attributes:@[attribute]]; - return result[attribute]; + attributes:@[attribute] + error:error]; + NSTimeInterval elapsed = ABS([start timeIntervalSinceNow]); + if (elapsed > ATTRIBUTE_FETCH_WARN_TIME_LIMIT) { + NSLog(@"! Fetching of %@ value for %@ took %@s", attribute, self.fb_description, @(elapsed)); + } + return [result objectForKey:attribute]; } inline static BOOL areValuesEqual(id value1, id value2); @@ -140,29 +150,6 @@ - (BOOL)fb_framelessFuzzyMatchesElement:(id)snapshot return targetCellSnapshot; } -- (CGRect)fb_visibleFrameWithFallback -{ - CGRect thisVisibleFrame = [self visibleFrame]; - if (!CGRectIsEmpty(thisVisibleFrame)) { - return thisVisibleFrame; - } - - NSDictionary *visibleFrameDict = (NSDictionary*)[self fb_attributeValue:@"XC_kAXXCAttributeVisibleFrame"]; - if (visibleFrameDict == nil) { - return thisVisibleFrame; - } - - id x = [visibleFrameDict objectForKey:@"X"]; - id y = [visibleFrameDict objectForKey:@"Y"]; - id height = [visibleFrameDict objectForKey:@"Height"]; - id width = [visibleFrameDict objectForKey:@"Width"]; - if (x != nil && y != nil && height != nil && width != nil) { - return CGRectMake([x doubleValue], [y doubleValue], [width doubleValue], [height doubleValue]); - } - - return thisVisibleFrame; -} - - (NSValue *)fb_hitPoint { NSError *error; diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/NSDictionary+FBUtf8SafeDictionary.h b/WebDriverAgent/WebDriverAgentLib/Categories/NSDictionary+FBUtf8SafeDictionary.h index 0f657ab6f1..cb2b539a1a 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/NSDictionary+FBUtf8SafeDictionary.h +++ b/WebDriverAgent/WebDriverAgentLib/Categories/NSDictionary+FBUtf8SafeDictionary.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/NSDictionary+FBUtf8SafeDictionary.m b/WebDriverAgent/WebDriverAgentLib/Categories/NSDictionary+FBUtf8SafeDictionary.m index 399769142e..0a5905daf8 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/NSDictionary+FBUtf8SafeDictionary.m +++ b/WebDriverAgent/WebDriverAgentLib/Categories/NSDictionary+FBUtf8SafeDictionary.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "NSDictionary+FBUtf8SafeDictionary.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/NSExpression+FBFormat.h b/WebDriverAgent/WebDriverAgentLib/Categories/NSExpression+FBFormat.h index feafbac458..274d420118 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/NSExpression+FBFormat.h +++ b/WebDriverAgent/WebDriverAgentLib/Categories/NSExpression+FBFormat.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/NSExpression+FBFormat.m b/WebDriverAgent/WebDriverAgentLib/Categories/NSExpression+FBFormat.m index 81858d65aa..d4c32aeba5 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/NSExpression+FBFormat.m +++ b/WebDriverAgent/WebDriverAgentLib/Categories/NSExpression+FBFormat.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "NSExpression+FBFormat.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/NSString+FBVisualLength.h b/WebDriverAgent/WebDriverAgentLib/Categories/NSString+FBVisualLength.h index a24eb690ae..ec065e28f4 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/NSString+FBVisualLength.h +++ b/WebDriverAgent/WebDriverAgentLib/Categories/NSString+FBVisualLength.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/NSString+FBVisualLength.m b/WebDriverAgent/WebDriverAgentLib/Categories/NSString+FBVisualLength.m index 9487bbebe3..652f1a06a8 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/NSString+FBVisualLength.m +++ b/WebDriverAgent/WebDriverAgentLib/Categories/NSString+FBVisualLength.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "NSString+FBVisualLength.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/NSString+FBXMLSafeString.h b/WebDriverAgent/WebDriverAgentLib/Categories/NSString+FBXMLSafeString.h index e51407b793..7cdceb7962 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/NSString+FBXMLSafeString.h +++ b/WebDriverAgent/WebDriverAgentLib/Categories/NSString+FBXMLSafeString.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/NSString+FBXMLSafeString.m b/WebDriverAgent/WebDriverAgentLib/Categories/NSString+FBXMLSafeString.m index cdc7ab3b5f..dbb7573f06 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/NSString+FBXMLSafeString.m +++ b/WebDriverAgent/WebDriverAgentLib/Categories/NSString+FBXMLSafeString.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "NSString+FBXMLSafeString.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.h b/WebDriverAgent/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.h index 8080f20975..d984e0b9e9 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.h +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.m b/WebDriverAgent/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.m index 3dc6754d56..0d39ba0b21 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.m +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCAXClient_iOS+FBSnapshotReqParams.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCTIssue+FBPatcher.h b/WebDriverAgent/WebDriverAgentLib/Categories/XCTIssue+FBPatcher.h index b4cdf1d44c..fc81daa045 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCTIssue+FBPatcher.h +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCTIssue+FBPatcher.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCTIssue+FBPatcher.m b/WebDriverAgent/WebDriverAgentLib/Categories/XCTIssue+FBPatcher.m index a3b0993096..70472e4a21 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCTIssue+FBPatcher.m +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCTIssue+FBPatcher.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCTIssue+FBPatcher.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.h b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.h index 7c057699b9..01bea467d6 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.h +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.m b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.m index 38b6586ef2..36628bc611 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.m +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIApplication+FBAlert.h" @@ -52,7 +51,7 @@ - (nullable XCUIElement *)fb_alertElementFromSafariWithScrollView:(XCUIElement * // and conatins at least one text view __block NSUInteger buttonsCount = 0; __block NSUInteger textViewsCount = 0; - id snapshot = candidate.fb_cachedSnapshot ?: candidate.fb_takeSnapshot; + id snapshot = candidate.fb_cachedSnapshot ?: [candidate fb_customSnapshot]; [snapshot enumerateDescendantsUsingBlock:^(id descendant) { XCUIElementType curType = descendant.elementType; if (curType == XCUIElementTypeButton) { @@ -73,7 +72,7 @@ - (XCUIElement *)fb_alertElement if (nil == alert) { return nil; } - id alertSnapshot = alert.fb_cachedSnapshot ?: alert.fb_takeSnapshot; + id alertSnapshot = alert.fb_cachedSnapshot ?: [alert fb_customSnapshot]; if (alertSnapshot.elementType == XCUIElementTypeAlert) { return alert; diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBFocused.h b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBFocused.h index 6951e760c4..4df5025c08 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBFocused.h +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBFocused.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.h b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.h index 54d4bd2816..f5d358ff90 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.h +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m index 6cdae945bd..e5abde9057 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIApplication+FBHelpers.h" @@ -37,9 +36,21 @@ #import "XCUIElement+FBUtilities.h" #import "XCUIElement+FBWebDriverAttributes.h" #import "XCUIElementQuery.h" +#import "FBElementHelpers.h" static NSString* const FBUnknownBundleId = @"unknown"; +static NSString* const FBExclusionAttributeFrame = @"frame"; +static NSString* const FBExclusionAttributeEnabled = @"enabled"; +static NSString* const FBExclusionAttributeVisible = @"visible"; +static NSString* const FBExclusionAttributeAccessible = @"accessible"; +static NSString* const FBExclusionAttributeFocused = @"focused"; +static NSString* const FBExclusionAttributePlaceholderValue = @"placeholderValue"; +static NSString* const FBExclusionAttributeNativeFrame = @"nativeFrame"; +static NSString* const FBExclusionAttributeTraits = @"traits"; +static NSString* const FBExclusionAttributeMinValue = @"minValue"; +static NSString* const FBExclusionAttributeMaxValue = @"maxValue"; + _Nullable id extractIssueProperty(id issue, NSString *propertyName) { SEL selector = NSSelectorFromString(propertyName); NSMethodSignature *methodSignature = [issue methodSignatureForSelector:selector]; @@ -88,6 +99,17 @@ _Nullable id extractIssueProperty(id issue, NSString *propertyName) { return result; } +NSDictionary *customExclusionAttributesMap(void) { + static dispatch_once_t onceToken; + static NSDictionary *result; + dispatch_once(&onceToken, ^{ + result = @{ + FBExclusionAttributeVisible: FB_XCAXAIsVisibleAttributeName, + FBExclusionAttributeAccessible: FB_XCAXAIsElementAttributeName, + }; + }); + return result; +} @implementation XCUIApplication (FBHelpers) @@ -156,25 +178,23 @@ - (NSDictionary *)fb_tree return [self fb_tree:nil]; } -- (NSDictionary *)fb_tree:(nullable NSSet *) excludedAttributes +- (NSDictionary *)fb_tree:(nullable NSSet *)excludedAttributes { - id snapshot = self.fb_isResolvedFromCache.boolValue - ? self.lastSnapshot - : [self fb_snapshotWithAllAttributesAndMaxDepth:nil]; - return [self.class dictionaryForElement:snapshot recursive:YES excludedAttributes:excludedAttributes]; + id snapshot = [self fb_standardSnapshot]; + return [self.class dictionaryForElement:snapshot + recursive:YES + excludedAttributes:excludedAttributes]; } - (NSDictionary *)fb_accessibilityTree { - id snapshot = self.fb_isResolvedFromCache.boolValue - ? self.lastSnapshot - : [self fb_snapshotWithAllAttributesAndMaxDepth:nil]; + id snapshot = [self fb_standardSnapshot]; return [self.class accessibilityInfoForElement:snapshot]; } + (NSDictionary *)dictionaryForElement:(id)snapshot recursive:(BOOL)recursive - excludedAttributes:(nullable NSSet *) excludedAttributes + excludedAttributes:(nullable NSSet *)excludedAttributes { NSMutableDictionary *info = [[NSMutableDictionary alloc] init]; info[@"type"] = [FBElementTypeTransformer shortStringWithElementType:snapshot.elementType]; @@ -185,28 +205,21 @@ + (NSDictionary *)dictionaryForElement:(id)snapshot info[@"label"] = FBValueOrNull(wrappedSnapshot.wdLabel); info[@"rect"] = wrappedSnapshot.wdRect; - NSDictionary *attributeBlocks = @{ - @"frame": ^{ - return NSStringFromCGRect(wrappedSnapshot.wdFrame); - }, - @"enabled": ^{ - return [@([wrappedSnapshot isWDEnabled]) stringValue]; - }, - @"visible": ^{ - return [@([wrappedSnapshot isWDVisible]) stringValue]; - }, - @"accessible": ^{ - return [@([wrappedSnapshot isWDAccessible]) stringValue]; - }, - @"focused": ^{ - return [@([wrappedSnapshot isWDFocused]) stringValue]; - } - }; + NSDictionary *attributeBlocks = [self fb_attributeBlockMapForWrappedSnapshot:wrappedSnapshot]; + + NSSet *nonPrefixedKeys = [NSSet setWithObjects: + FBExclusionAttributeFrame, + FBExclusionAttributePlaceholderValue, + FBExclusionAttributeNativeFrame, + FBExclusionAttributeTraits, + FBExclusionAttributeMinValue, + FBExclusionAttributeMaxValue, + nil]; for (NSString *key in attributeBlocks) { if (excludedAttributes == nil || ![excludedAttributes containsObject:key]) { NSString *value = ((NSString * (^)(void))attributeBlocks[key])(); - if ([key isEqualToString:@"frame"]) { + if ([nonPrefixedKeys containsObject:key]) { info[key] = value; } else { info[[NSString stringWithFormat:@"is%@", [key capitalizedString]]] = value; @@ -222,14 +235,69 @@ + (NSDictionary *)dictionaryForElement:(id)snapshot if ([childElements count]) { info[@"children"] = [[NSMutableArray alloc] init]; for (id childSnapshot in childElements) { - [info[@"children"] addObject:[self dictionaryForElement:childSnapshot - recursive:YES - excludedAttributes:excludedAttributes]]; + @autoreleasepool { + [info[@"children"] addObject:[self dictionaryForElement:childSnapshot + recursive:YES + excludedAttributes:excludedAttributes]]; + } } } return info; } +// Helper used by `dictionaryForElement:` to assemble attribute value blocks, +// including both common attributes and conditionally included ones like placeholderValue. ++ (NSDictionary *)fb_attributeBlockMapForWrappedSnapshot:(FBXCElementSnapshotWrapper *)wrappedSnapshot + +{ + // Base attributes common to every element + NSMutableDictionary *blocks = + [@{ + FBExclusionAttributeFrame: ^{ + return NSStringFromCGRect(wrappedSnapshot.wdFrame); + }, + FBExclusionAttributeNativeFrame: ^{ + return NSStringFromCGRect(wrappedSnapshot.wdNativeFrame); + }, + FBExclusionAttributeEnabled: ^{ + return [@([wrappedSnapshot isWDEnabled]) stringValue]; + }, + FBExclusionAttributeVisible: ^{ + return [@([wrappedSnapshot isWDVisible]) stringValue]; + }, + FBExclusionAttributeAccessible: ^{ + return [@([wrappedSnapshot isWDAccessible]) stringValue]; + }, + FBExclusionAttributeFocused: ^{ + return [@([wrappedSnapshot isWDFocused]) stringValue]; + }, + FBExclusionAttributeTraits: ^{ + return wrappedSnapshot.wdTraits; + } + } mutableCopy]; + + XCUIElementType elementType = wrappedSnapshot.elementType; + + // Text-input placeholder (only for elements that support inner text) + if (FBDoesElementSupportInnerText(elementType)) { + blocks[FBExclusionAttributePlaceholderValue] = ^{ + return (NSString *)FBValueOrNull(wrappedSnapshot.wdPlaceholderValue); + }; + } + + // Only for elements that support min/max value + if (FBDoesElementSupportMinMaxValue(elementType)) { + blocks[FBExclusionAttributeMinValue] = ^{ + return wrappedSnapshot.wdMinValue; + }; + blocks[FBExclusionAttributeMaxValue] = ^{ + return wrappedSnapshot.wdMaxValue; + }; + } + + return [blocks copy]; +} + + (NSDictionary *)accessibilityInfoForElement:(id)snapshot { FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:snapshot]; @@ -246,9 +314,11 @@ + (NSDictionary *)accessibilityInfoForElement:(id)snapshot } else { NSMutableArray *children = [[NSMutableArray alloc] init]; for (id childSnapshot in snapshot.children) { - NSDictionary *childInfo = [self accessibilityInfoForElement:childSnapshot]; - if ([childInfo count]) { - [children addObject: childInfo]; + @autoreleasepool { + NSDictionary *childInfo = [self accessibilityInfoForElement:childSnapshot]; + if ([childInfo count]) { + [children addObject: childInfo]; + } } } if ([children count]) { @@ -396,35 +466,41 @@ - (BOOL)fb_dismissKeyboardWithKeyNames:(nullable NSArray *)keyNames return nil; } + // These custom attributes could take too long to fetch, thus excluded + NSSet *customAttributesToExclude = [NSSet setWithArray:[customExclusionAttributesMap() allKeys]]; NSMutableArray *resultArray = [NSMutableArray array]; NSMethodSignature *methodSignature = [self methodSignatureForSelector:selector]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; [invocation setSelector:selector]; [invocation setArgument:&auditTypes atIndex:2]; BOOL (^issueHandler)(id) = ^BOOL(id issue) { - NSString *auditType = @""; - NSDictionary *valuesToNamesMap = auditTypeValuesToNames(); - NSNumber *auditTypeValue = [issue valueForKey:@"auditType"]; - if (nil != auditTypeValue) { - auditType = valuesToNamesMap[auditTypeValue] ?: [auditTypeValue stringValue]; - } - - id extractedElement = extractIssueProperty(issue, @"element"); - - id elementSnapshot = [extractedElement fb_takeSnapshot]; - NSDictionary *elementAttributes = elementSnapshot - ? [self.class dictionaryForElement:elementSnapshot recursive:NO excludedAttributes:nil] + @autoreleasepool { + NSString *auditType = @""; + NSDictionary *valuesToNamesMap = auditTypeValuesToNames(); + NSNumber *auditTypeValue = [issue valueForKey:@"auditType"]; + if (nil != auditTypeValue) { + auditType = valuesToNamesMap[auditTypeValue] ?: [auditTypeValue stringValue]; + } + + id extractedElement = extractIssueProperty(issue, @"element"); + + id elementSnapshot = [extractedElement fb_cachedSnapshot] ?: [extractedElement fb_standardSnapshot]; + NSDictionary *elementAttributes = elementSnapshot + ? [self.class dictionaryForElement:elementSnapshot + recursive:NO + excludedAttributes:customAttributesToExclude] : @{}; - - [resultArray addObject:@{ - @"detailedDescription": extractIssueProperty(issue, @"detailedDescription") ?: @"", - @"compactDescription": extractIssueProperty(issue, @"compactDescription") ?: @"", - @"auditType": auditType, - @"element": [extractedElement description] ?: @"", - @"elementDescription": [extractedElement debugDescription] ?: @"", - @"elementAttributes": elementAttributes ?: @{}, - }]; - return YES; + + [resultArray addObject:@{ + @"detailedDescription": extractIssueProperty(issue, @"detailedDescription") ?: @"", + @"compactDescription": extractIssueProperty(issue, @"compactDescription") ?: @"", + @"auditType": auditType, + @"element": [extractedElement description] ?: @"", + @"elementDescription": [extractedElement debugDescription] ?: @"", + @"elementAttributes": elementAttributes ?: @{}, + }]; + return YES; + } }; [invocation setArgument:&issueHandler atIndex:3]; [invocation setArgument:&error atIndex:4]; @@ -544,23 +620,17 @@ + (BOOL)fb_switchToSystemApplicationWithError:(NSError **)error { XCUIApplication *systemApp = self.fb_systemApplication; @try { - if (!systemApp.running) { - [systemApp launch]; - } else { + if (systemApp.running) { [systemApp activate]; + } else { + [systemApp launch]; } } @catch (NSException *e) { return [[[FBErrorBuilder alloc] withDescription:nil == e ? @"Cannot open the home screen" : e.reason] buildError:error]; } - return [[[[FBRunLoopSpinner new] - timeout:5] - timeoutErrorMessage:@"Timeout waiting until the home screen is visible"] - spinUntilTrue:^BOOL{ - return [systemApp fb_isSameAppAs:self.fb_activeApplication]; - } - error:error]; + return YES; } - (BOOL)fb_isSameAppAs:(nullable XCUIApplication *)otherApp diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBQuiescence.h b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBQuiescence.h index ef036053ee..962a481464 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBQuiescence.h +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBQuiescence.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBQuiescence.m b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBQuiescence.m index 66f1f118bf..72febaa51d 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBQuiescence.m +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBQuiescence.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIApplication+FBQuiescence.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBTouchAction.h b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBTouchAction.h index 6be298b498..79b220c507 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBTouchAction.h +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBTouchAction.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBTouchAction.m b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBTouchAction.m index 622dac479e..bbd1e5746b 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBTouchAction.m +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBTouchAction.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBUIInterruptions.h b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBUIInterruptions.h index 2078d8c20c..e95273216b 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBUIInterruptions.h +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBUIInterruptions.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBUIInterruptions.m b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBUIInterruptions.m index 5d48dffa64..b662a1e5a9 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBUIInterruptions.m +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBUIInterruptions.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIApplication+FBUIInterruptions.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplicationProcess+FBQuiescence.h b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplicationProcess+FBQuiescence.h index f7aacb5c51..f911cbfdcf 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplicationProcess+FBQuiescence.h +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplicationProcess+FBQuiescence.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplicationProcess+FBQuiescence.m b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplicationProcess+FBQuiescence.m index 9c1d06eba7..1f6272dbce 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplicationProcess+FBQuiescence.m +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplicationProcess+FBQuiescence.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIApplicationProcess+FBQuiescence.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIDevice+FBHealthCheck.h b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIDevice+FBHealthCheck.h index 65257ee1f6..1c1ab63cd8 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIDevice+FBHealthCheck.h +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIDevice+FBHealthCheck.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIDevice+FBHealthCheck.m b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIDevice+FBHealthCheck.m index 315f06744b..8f9de6ee2c 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIDevice+FBHealthCheck.m +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIDevice+FBHealthCheck.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIDevice+FBHealthCheck.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.h b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.h index ca7fc53d8f..2f9c9d86ff 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.h +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m index ef8664fb6f..660885630f 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIDevice+FBHelpers.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIDevice+FBRotation.h b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIDevice+FBRotation.h index 993c301c7d..b947fcdda7 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIDevice+FBRotation.h +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIDevice+FBRotation.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIDevice+FBRotation.m b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIDevice+FBRotation.m index 68b9b20a9b..164b23e7ba 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIDevice+FBRotation.m +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIDevice+FBRotation.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIDevice+FBRotation.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBAccessibility.h b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBAccessibility.h index 2af37a8d59..baa56565de 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBAccessibility.h +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBAccessibility.h @@ -3,12 +3,11 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import -#import "FBXCElementSnapshotWrapper.h" +#import NS_ASSUME_NONNULL_BEGIN diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBAccessibility.m b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBAccessibility.m index 327b2171a3..47d62e58f5 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBAccessibility.m +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBAccessibility.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIElement+FBAccessibility.h" @@ -18,8 +17,7 @@ @implementation XCUIElement (FBAccessibility) - (BOOL)fb_isAccessibilityElement { - id snapshot = [self fb_snapshotWithAttributes:@[FB_XCAXAIsElementAttributeName] - maxDepth:@1]; + id snapshot = [self fb_standardSnapshot]; return [FBXCElementSnapshotWrapper ensureWrapped:snapshot].fb_isAccessibilityElement; } @@ -33,8 +31,20 @@ - (BOOL)fb_isAccessibilityElement if (nil != isAccessibilityElement) { return isAccessibilityElement.boolValue; } - - return [(NSNumber *)[self fb_attributeValue:FB_XCAXAIsElementAttributeName] boolValue]; + + NSError *error; + NSNumber *attributeValue = [self fb_attributeValue:FB_XCAXAIsElementAttributeName + error:&error]; + if (nil != attributeValue) { + NSMutableDictionary *updatedValue = [NSMutableDictionary dictionaryWithDictionary:self.additionalAttributes ?: @{}]; + [updatedValue setObject:attributeValue forKey:FB_XCAXAIsElementAttribute]; + self.snapshot.additionalAttributes = updatedValue.copy; + return [attributeValue boolValue]; + } + + NSLog(@"Cannot determine accessibility of '%@' natively: %@. Defaulting to: %@", + self.fb_description, error.description, @(NO)); + return NO; } @end diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBCaching.h b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBCaching.h index 109a263711..119723aaa7 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBCaching.h +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBCaching.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import @@ -13,9 +12,6 @@ NS_ASSUME_NONNULL_BEGIN @interface XCUIElement (FBCaching) -/*! This property is set to YES if the given element has been resolved from the cache, so it is safe to use the `lastSnapshot` property */ -@property (nullable, nonatomic) NSNumber *fb_isResolvedFromCache; - @property (nonatomic, readonly) NSString *fb_cacheId; @end diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBCaching.m b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBCaching.m index faa42ea86f..c2e9f11211 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBCaching.m +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBCaching.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIElement+FBCaching.h" @@ -18,20 +17,6 @@ @implementation XCUIElement (FBCaching) -static char XCUIELEMENT_IS_RESOLVED_FROM_CACHE_KEY; - -@dynamic fb_isResolvedFromCache; - -- (void)setFb_isResolvedFromCache:(NSNumber *)isResolvedFromCache -{ - objc_setAssociatedObject(self, &XCUIELEMENT_IS_RESOLVED_FROM_CACHE_KEY, isResolvedFromCache, OBJC_ASSOCIATION_RETAIN_NONATOMIC); -} - -- (NSNumber *)fb_isResolvedFromCache -{ - return (NSNumber *)objc_getAssociatedObject(self, &XCUIELEMENT_IS_RESOLVED_FROM_CACHE_KEY); -} - static char XCUIELEMENT_CACHE_ID_KEY; @dynamic fb_cacheId; @@ -43,14 +28,7 @@ - (NSString *)fb_cacheId return (NSString *)result; } - NSString *uid; - if ([self isKindOfClass:XCUIApplication.class]) { - uid = self.fb_uid; - } else { - id snapshot = self.fb_cachedSnapshot ?: self.fb_takeSnapshot; - uid = [FBXCElementSnapshotWrapper wdUIDWithSnapshot:snapshot]; - } - + NSString *uid = self.fb_uid; objc_setAssociatedObject(self, &XCUIELEMENT_CACHE_ID_KEY, uid, OBJC_ASSOCIATION_RETAIN_NONATOMIC); return uid; } diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBClassChain.h b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBClassChain.h index 1cd1e8b937..0e56bfeaa9 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBClassChain.h +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBClassChain.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import @@ -50,7 +49,8 @@ NS_ASSUME_NONNULL_BEGIN @return an array of descendants matching given class chain @throws FBUnknownAttributeException if any of predicates in the chain contains unknown attribute(s) */ -- (NSArray *)fb_descendantsMatchingClassChain:(NSString *)classChainQuery shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch; +- (NSArray *)fb_descendantsMatchingClassChain:(NSString *)classChainQuery + shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch; @end diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBClassChain.m b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBClassChain.m index 9f70955bbb..88197eccda 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBClassChain.m +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBClassChain.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIElement+FBClassChain.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBFind.h b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBFind.h index 664d828b01..8a91fcdb83 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBFind.h +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBFind.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBFind.m b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBFind.m index 4d46f4c8f2..ceab0adf05 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBFind.m +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBFind.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ @@ -12,6 +11,7 @@ #import "FBMacros.h" #import "FBElementTypeTransformer.h" +#import "FBConfiguration.h" #import "NSPredicate+FBFormat.h" #import "FBXCElementSnapshotWrapper+Helpers.h" #import "FBXCodeCompatibility.h" @@ -109,9 +109,9 @@ @implementation XCUIElement (FBFind) id snapshot = matchingSnapshots.firstObject; matchingSnapshots = @[snapshot]; } - return [self fb_filterDescendantsWithSnapshots:matchingSnapshots - selfUID:[FBXCElementSnapshotWrapper wdUIDWithSnapshot:self.lastSnapshot] - onlyChildren:NO]; + XCUIElement *scopeRoot = FBConfiguration.limitXpathContextScope ? self : self.application; + return [scopeRoot fb_filterDescendantsWithSnapshots:matchingSnapshots + onlyChildren:NO]; } @@ -122,7 +122,9 @@ @implementation XCUIElement (FBFind) { NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(id snapshot, NSDictionary * _Nullable bindings) { - return [[FBXCElementSnapshotWrapper wdNameWithSnapshot:snapshot] isEqualToString:accessibilityId]; + @autoreleasepool { + return [[FBXCElementSnapshotWrapper wdNameWithSnapshot:snapshot] isEqualToString:accessibilityId]; + } }]; return [self fb_descendantsMatchingPredicate:predicate shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch]; diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBForceTouch.h b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBForceTouch.h index 154cab24e4..8005b22933 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBForceTouch.h +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBForceTouch.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBForceTouch.m b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBForceTouch.m index 1c7a9ce0ef..bd5d0bdd37 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBForceTouch.m +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBForceTouch.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIElement+FBForceTouch.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBIsVisible.h b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBIsVisible.h index 2acc56511c..fd17acca61 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBIsVisible.h +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBIsVisible.h @@ -3,11 +3,10 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ -#import "FBXCElementSnapshotWrapper.h" +#import NS_ASSUME_NONNULL_BEGIN diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBIsVisible.m b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBIsVisible.m index 87841fc387..75a228a3e0 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBIsVisible.m +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBIsVisible.m @@ -3,218 +3,76 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIElement+FBIsVisible.h" -#import "FBConfiguration.h" #import "FBElementUtils.h" -#import "FBMathUtils.h" -#import "FBActiveAppDetectionPoint.h" -#import "FBSession.h" -#import "FBXCAccessibilityElement.h" #import "FBXCodeCompatibility.h" #import "FBXCElementSnapshotWrapper+Helpers.h" #import "XCUIElement+FBUtilities.h" -#import "XCUIElement+FBUID.h" +#import "XCUIElement+FBVisibleFrame.h" #import "XCTestPrivateSymbols.h" -@implementation XCUIElement (FBIsVisible) - -- (BOOL)fb_isVisible +NSNumber* _Nullable fetchSnapshotVisibility(id snapshot) { - id snapshot = [self fb_snapshotWithAttributes:@[FB_XCAXAIsVisibleAttributeName] - maxDepth:@1]; - return [FBXCElementSnapshotWrapper ensureWrapped:snapshot].fb_isVisible; + return nil == snapshot.additionalAttributes ? nil : snapshot.additionalAttributes[FB_XCAXAIsVisibleAttribute]; } -@end - -@implementation FBXCElementSnapshotWrapper (FBIsVisible) - -+ (NSString *)fb_uniqIdWithSnapshot:(id)snapshot -{ - return [FBXCElementSnapshotWrapper wdUIDWithSnapshot:snapshot] ?: [NSString stringWithFormat:@"%p", (void *)snapshot]; -} +@implementation XCUIElement (FBIsVisible) -- (nullable NSNumber *)fb_cachedVisibilityValue +- (BOOL)fb_isVisible { - NSMutableDictionary *cache = FBSession.activeSession.elementsVisibilityCache; - if (nil == cache) { - return nil; + @autoreleasepool { + id snapshot = [self fb_standardSnapshot]; + return [FBXCElementSnapshotWrapper ensureWrapped:snapshot].fb_isVisible; } - - NSDictionary *result = cache[@(self.generation)]; - if (nil == result) { - // There is no need to keep the cached data for the previous generations - [cache removeAllObjects]; - cache[@(self.generation)] = [NSMutableDictionary dictionary]; - return nil; - } - return result[[self.class fb_uniqIdWithSnapshot:self.snapshot]]; } -- (BOOL)fb_cacheVisibilityWithValue:(BOOL)isVisible - forAncestors:(nullable NSArray> *)ancestors -{ - NSMutableDictionary *cache = FBSession.activeSession.elementsVisibilityCache; - if (nil == cache) { - return isVisible; - } - NSMutableDictionary *destination = cache[@(self.generation)]; - if (nil == destination) { - return isVisible; - } - - NSNumber *visibleObj = [NSNumber numberWithBool:isVisible]; - destination[[self.class fb_uniqIdWithSnapshot:self.snapshot]] = visibleObj; - if (isVisible && nil != ancestors) { - // if an element is visible then all its ancestors must be visible as well - for (id ancestor in ancestors) { - NSString *ancestorId = [self.class fb_uniqIdWithSnapshot:ancestor]; - if (nil == destination[ancestorId]) { - destination[ancestorId] = visibleObj; - } - } - } - return isVisible; -} +@end -- (CGRect)fb_frameInContainer:(id)container - hierarchyIntersection:(nullable NSValue *)intersectionRectange -{ - CGRect currentRectangle = nil == intersectionRectange ? self.frame : [intersectionRectange CGRectValue]; - id parent = self.parent; - CGRect parentFrame = parent.frame; - CGRect containerFrame = container.frame; - if (CGSizeEqualToSize(parentFrame.size, CGSizeZero) && - CGPointEqualToPoint(parentFrame.origin, CGPointZero)) { - // Special case (or XCTest bug). Shift the origin and return immediately after shift - id nextParent = parent.parent; - BOOL isGrandparent = YES; - while (nextParent && nextParent != container) { - CGRect nextParentFrame = nextParent.frame; - if (isGrandparent && - CGSizeEqualToSize(nextParentFrame.size, CGSizeZero) && - CGPointEqualToPoint(nextParentFrame.origin, CGPointZero)) { - // Double zero-size container inclusion means that element coordinates are absolute - return CGRectIntersection(currentRectangle, containerFrame); - } - isGrandparent = NO; - if (!CGPointEqualToPoint(nextParentFrame.origin, CGPointZero)) { - currentRectangle.origin.x += nextParentFrame.origin.x; - currentRectangle.origin.y += nextParentFrame.origin.y; - return CGRectIntersection(currentRectangle, containerFrame); - } - nextParent = nextParent.parent; - } - return CGRectIntersection(currentRectangle, containerFrame); - } - // Skip parent containers if they are outside of the viewport - CGRect intersectionWithParent = CGRectIntersectsRect(parentFrame, containerFrame) || parent.elementType != XCUIElementTypeOther - ? CGRectIntersection(currentRectangle, parentFrame) - : currentRectangle; - if (CGRectIsEmpty(intersectionWithParent) && - parent != container && - self.elementType == XCUIElementTypeOther) { - // Special case (or XCTest bug). Shift the origin - if (CGSizeEqualToSize(parentFrame.size, containerFrame.size) || - // The size might be inverted in landscape - CGSizeEqualToSize(parentFrame.size, CGSizeMake(containerFrame.size.height, containerFrame.size.width)) || - CGSizeEqualToSize(self.frame.size, CGSizeZero)) { - // Covers ActivityListView and RemoteBridgeView cases - currentRectangle.origin.x += parentFrame.origin.x; - currentRectangle.origin.y += parentFrame.origin.y; - return CGRectIntersection(currentRectangle, containerFrame); - } - } - if (CGRectIsEmpty(intersectionWithParent) || parent == container) { - return intersectionWithParent; - } - return [[FBXCElementSnapshotWrapper ensureWrapped:parent] fb_frameInContainer:container - hierarchyIntersection:[NSValue valueWithCGRect:intersectionWithParent]]; -} +@implementation FBXCElementSnapshotWrapper (FBIsVisible) -- (BOOL)fb_hasAnyVisibleLeafs +- (BOOL)fb_hasVisibleDescendants { - NSArray> *children = self.children; - if (0 == children.count) { - return self.fb_isVisible; - } - - for (id child in children) { - if ([FBXCElementSnapshotWrapper ensureWrapped:child].fb_hasAnyVisibleLeafs) { + for (id descendant in (self._allDescendants ?: @[])) { + if ([fetchSnapshotVisibility(descendant) boolValue]) { return YES; } } - return NO; } - (BOOL)fb_isVisible { - NSNumber *isVisible = self.additionalAttributes[FB_XCAXAIsVisibleAttribute]; - if (isVisible != nil) { + NSNumber *isVisible = fetchSnapshotVisibility(self); + if (nil != isVisible) { return isVisible.boolValue; } - - NSNumber *cachedValue = [self fb_cachedVisibilityValue]; - if (nil != cachedValue) { - return [cachedValue boolValue]; - } - CGRect selfFrame = self.frame; - if (CGRectIsEmpty(selfFrame)) { - return [self fb_cacheVisibilityWithValue:NO forAncestors:nil]; + // Fetching the attribute value is expensive. + // Shortcircuit here to save time and assume if any of descendants + // is already determined as visible then the container should be visible as well + if ([self fb_hasVisibleDescendants]) { + return YES; } - NSArray> *ancestors = self.fb_ancestors; - if ([FBConfiguration shouldUseTestManagerForVisibilityDetection]) { - BOOL visibleAttrValue = [(NSNumber *)[self fb_attributeValue:FB_XCAXAIsVisibleAttributeName] boolValue]; - return [self fb_cacheVisibilityWithValue:visibleAttrValue forAncestors:ancestors]; - } - - id parentWindow = ancestors.count > 1 ? [ancestors objectAtIndex:ancestors.count - 2] : nil; - CGRect visibleRect = selfFrame; - if (nil != parentWindow) { - visibleRect = [self fb_frameInContainer:parentWindow hierarchyIntersection:nil]; - } - if (CGRectIsEmpty(visibleRect)) { - return [self fb_cacheVisibilityWithValue:NO forAncestors:ancestors]; - } - CGPoint midPoint = CGPointMake(visibleRect.origin.x + visibleRect.size.width / 2, - visibleRect.origin.y + visibleRect.size.height / 2); - id hitElement = [FBActiveAppDetectionPoint axElementWithPoint:midPoint]; - if (nil != hitElement) { - if (FBIsAXElementEqualToOther(self.accessibilityElement, hitElement)) { - return [self fb_cacheVisibilityWithValue:YES forAncestors:ancestors]; - } - for (id ancestor in ancestors) { - if (FBIsAXElementEqualToOther(hitElement, ancestor.accessibilityElement)) { - return [self fb_cacheVisibilityWithValue:YES forAncestors:ancestors]; - } + NSError *error; + NSNumber *attributeValue = [self fb_attributeValue:FB_XCAXAIsVisibleAttributeName + error:&error]; + if (nil != attributeValue) { + NSMutableDictionary *updatedValue = [NSMutableDictionary dictionaryWithDictionary:self.additionalAttributes ?: @{}]; + [updatedValue setObject:attributeValue forKey:FB_XCAXAIsVisibleAttribute]; + self.snapshot.additionalAttributes = updatedValue.copy; + @autoreleasepool { + return [attributeValue boolValue]; } } - if (self.children.count > 0) { - if (nil != hitElement) { - for (id descendant in self._allDescendants) { - if (FBIsAXElementEqualToOther(hitElement, descendant.accessibilityElement)) { - return [self fb_cacheVisibilityWithValue:YES - forAncestors:[FBXCElementSnapshotWrapper ensureWrapped:descendant].fb_ancestors]; - } - } - } - if (self.fb_hasAnyVisibleLeafs) { - return [self fb_cacheVisibilityWithValue:YES forAncestors:ancestors]; - } - } else if (nil == hitElement) { - // Sometimes XCTest returns nil for leaf elements hit test even if such elements are hittable - // Assume such elements are visible if their rectInContainer is visible - return [self fb_cacheVisibilityWithValue:YES forAncestors:ancestors]; - } - return [self fb_cacheVisibilityWithValue:NO forAncestors:ancestors]; + + NSLog(@"Cannot determine visiblity of %@ natively: %@. Defaulting to: %@", + self.fb_description, error.description, @(NO)); + return NO; } @end diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBMinMax.h b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBMinMax.h new file mode 100644 index 0000000000..0873a65a41 --- /dev/null +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBMinMax.h @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface XCUIElement (FBMinMax) + +/*! Minimum value (minValue) – may be nil if the element does not have this attribute */ +@property (nonatomic, readonly, nullable) NSNumber *fb_minValue; + +/*! Maximum value (maxValue) - may be nil if the element does not have this attribute */ +@property (nonatomic, readonly, nullable) NSNumber *fb_maxValue; + +@end + +@interface FBXCElementSnapshotWrapper (FBMinMax) + +/*! Minimum value (minValue) – may be nil if the element does not have this attribute */ +@property (nonatomic, readonly, nullable) NSNumber *fb_minValue; + +/*! Maximum value (maxValue) - may be nil if the element does not have this attribute */ +@property (nonatomic, readonly, nullable) NSNumber *fb_maxValue; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBMinMax.m b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBMinMax.m new file mode 100644 index 0000000000..875149060f --- /dev/null +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBMinMax.m @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBLogger.h" +#import "XCUIElement+FBMinMax.h" +#import "FBXCElementSnapshotWrapper+Helpers.h" +#import "XCUIElement+FBUtilities.h" +#import "XCTestPrivateSymbols.h" + +@interface FBXCElementSnapshotWrapper (FBMinMaxInternal) + +- (NSNumber *)fb_numericAttribute:(NSString *)attributeName symbol:(NSNumber *)symbol; + +@end + +@implementation XCUIElement (FBMinMax) + +- (NSNumber *)fb_minValue +{ + @autoreleasepool { + id snapshot = [self fb_standardSnapshot]; + return [[FBXCElementSnapshotWrapper ensureWrapped:snapshot] fb_minValue]; + } +} + +- (NSNumber *)fb_maxValue +{ + @autoreleasepool { + id snapshot = [self fb_standardSnapshot]; + return [[FBXCElementSnapshotWrapper ensureWrapped:snapshot] fb_maxValue]; + } +} + +@end + +@implementation FBXCElementSnapshotWrapper (FBMinMax) + +- (NSNumber *)fb_minValue +{ + return [self fb_numericAttribute:FB_XCAXACustomMinValueAttributeName + symbol:FB_XCAXACustomMinValueAttribute]; +} + +- (NSNumber *)fb_maxValue +{ + return [self fb_numericAttribute:FB_XCAXACustomMaxValueAttributeName + symbol:FB_XCAXACustomMaxValueAttribute]; +} + +- (NSNumber *)fb_numericAttribute:(NSString *)attributeName symbol:(NSNumber *)symbol +{ + NSNumber *cached = (self.snapshot.additionalAttributes ?: @{})[symbol]; + if (cached) { + return cached; + } + + NSError *error = nil; + NSNumber *raw = [self fb_attributeValue:attributeName error:&error]; + if (nil != raw) { + NSMutableDictionary *updated = [NSMutableDictionary dictionaryWithDictionary:self.additionalAttributes ?: @{}]; + updated[symbol] = raw; + self.snapshot.additionalAttributes = updated.copy; + return raw; + } + + [FBLogger logFmt:@"[FBMinMax] Cannot determine %@ for %@: %@", attributeName, self.fb_description, error.localizedDescription]; + return nil; +} + +@end diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBPickerWheel.h b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBPickerWheel.h index a63cf934e9..72762a3c43 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBPickerWheel.h +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBPickerWheel.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBPickerWheel.m b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBPickerWheel.m index 71ebaf6524..551892135c 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBPickerWheel.m +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBPickerWheel.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIElement+FBPickerWheel.h" @@ -12,6 +11,7 @@ #import "FBRunLoopSpinner.h" #import "FBXCElementSnapshot.h" #import "FBXCodeCompatibility.h" +#import "XCUIElement+FBUID.h" #import "XCUICoordinate.h" #import "XCUIElement+FBCaching.h" #import "XCUIElement+FBResolve.h" @@ -23,9 +23,7 @@ @implementation XCUIElement (FBPickerWheel) - (BOOL)fb_scrollWithOffset:(CGFloat)relativeHeightOffset error:(NSError **)error { - id snapshot = self.fb_isResolvedFromCache.boolValue - ? self.lastSnapshot - : self.fb_takeSnapshot; + id snapshot = [self fb_standardSnapshot]; NSString *previousValue = snapshot.value; XCUICoordinate *startCoord = [self coordinateWithNormalizedOffset:CGVectorMake(0.5, 0.5)]; XCUICoordinate *endCoord = [startCoord coordinateWithOffset:CGVectorMake(0.0, relativeHeightOffset * snapshot.frame.size.height)]; @@ -35,7 +33,7 @@ - (BOOL)fb_scrollWithOffset:(CGFloat)relativeHeightOffset error:(NSError **)erro // Fetching stable instance of an element allows it to be bounded to the // unique element identifier (UID), so it could be found next time even if its // id is different from the initial one. See https://github.com/appium/appium/issues/17569 - XCUIElement *stableInstance = self.fb_stableInstance; + XCUIElement *stableInstance = [self fb_stableInstanceWithUid:[FBXCElementSnapshotWrapper wdUIDWithSnapshot:snapshot]]; [endCoord tap]; return [[[[FBRunLoopSpinner new] timeout:VALUE_CHANGE_TIMEOUT] diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBResolve.h b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBResolve.h index 80b218b066..09b174f22c 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBResolve.h +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBResolve.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import @@ -27,10 +26,11 @@ NS_ASSUME_NONNULL_BEGIN Although, if the cached element instance is the one returned by this API call then the same element is going to be matched and no staleness exception will be thrown. + @param uid Element UUID @return Either the same element instance if `fb_isResolvedNatively` was set to NO (usually the cache for elements matched by xpath locators) or the stable instance of the self element based on the query by element's UUID. */ -- (XCUIElement *)fb_stableInstance; +- (XCUIElement *)fb_stableInstanceWithUid:(NSString *__nullable)uid; @end diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBResolve.m b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBResolve.m index f52c72bf6d..f288114dde 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBResolve.m +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBResolve.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIElement+FBResolve.h" @@ -32,23 +31,21 @@ - (NSNumber *)fb_isResolvedNatively return nil == result ? @YES : result; } -- (XCUIElement *)fb_stableInstance +- (XCUIElement *)fb_stableInstanceWithUid:(NSString *)uid { - if (![self.fb_isResolvedNatively boolValue]) { + if (nil == uid || ![self.fb_isResolvedNatively boolValue] || [self isKindOfClass:XCUIApplication.class]) { return self; } - - XCUIElementQuery *query = [self isKindOfClass:XCUIApplication.class] - ? self.application.fb_query - : [self.application.fb_query descendantsMatchingType:XCUIElementTypeAny]; - NSString *uid = nil == self.fb_cachedSnapshot - ? self.fb_uid - : [FBXCElementSnapshotWrapper wdUIDWithSnapshot:(id)self.fb_cachedSnapshot]; - if (nil == uid) { - return self; + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%K = %@", FBStringify(FBXCElementSnapshotWrapper, fb_uid), uid]; + @autoreleasepool { + XCUIElementQuery *query = [self.application.fb_query descendantsMatchingType:XCUIElementTypeAny]; + XCUIElement *result = [query matchingPredicate:predicate].allElementsBoundByIndex.firstObject; + if (nil != result) { + result.fb_isResolvedNatively = @NO; + return result; + } } - NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%K = %@",FBStringify(FBXCElementSnapshotWrapper, fb_uid), uid]; - return [query matchingPredicate:predicate].allElementsBoundByIndex.firstObject ?: self; + return self; } @end diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBScrolling.h b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBScrolling.h index 680fddbfc3..416544e370 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBScrolling.h +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBScrolling.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBScrolling.m b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBScrolling.m index 52be1a5574..6b63d0a987 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBScrolling.m +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBScrolling.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIElement+FBScrolling.h" @@ -20,9 +19,11 @@ #import "XCUIApplication.h" #import "XCUICoordinate.h" #import "XCUIElement+FBIsVisible.h" +#import "XCUIElement+FBVisibleFrame.h" #import "XCUIElement.h" #import "XCUIElement+FBUtilities.h" #import "XCUIElement+FBWebDriverAttributes.h" +#import "XCTestPrivateSymbols.h" const CGFloat FBFuzzyPointThreshold = 20.f; //Smallest determined value that is not interpreted as touch const CGFloat FBScrollToVisibleNormalizedDistance = .5f; @@ -47,45 +48,35 @@ @implementation XCUIElement (FBScrolling) - (BOOL)fb_nativeScrollToVisibleWithError:(NSError **)error { - id snapshot = self.fb_isResolvedFromCache.boolValue - ? self.lastSnapshot - : self.fb_takeSnapshot; + id snapshot = [self fb_customSnapshot]; return nil != [self _hitPointByAttemptingToScrollToVisibleSnapshot:snapshot error:error]; } - (void)fb_scrollUpByNormalizedDistance:(CGFloat)distance { - id snapshot = self.fb_isResolvedFromCache.boolValue - ? self.lastSnapshot - : self.fb_takeSnapshot; + id snapshot = [self fb_customSnapshot]; [[FBXCElementSnapshotWrapper ensureWrapped:snapshot] fb_scrollUpByNormalizedDistance:distance inApplication:self.application]; } - (void)fb_scrollDownByNormalizedDistance:(CGFloat)distance { - id snapshot = self.fb_isResolvedFromCache.boolValue - ? self.lastSnapshot - : self.fb_takeSnapshot; + id snapshot = [self fb_customSnapshot]; [[FBXCElementSnapshotWrapper ensureWrapped:snapshot] fb_scrollDownByNormalizedDistance:distance inApplication:self.application]; } - (void)fb_scrollLeftByNormalizedDistance:(CGFloat)distance { - id snapshot = self.fb_isResolvedFromCache.boolValue - ? self.lastSnapshot - : self.fb_takeSnapshot; + id snapshot = [self fb_customSnapshot]; [[FBXCElementSnapshotWrapper ensureWrapped:snapshot] fb_scrollLeftByNormalizedDistance:distance inApplication:self.application]; } - (void)fb_scrollRightByNormalizedDistance:(CGFloat)distance { - id snapshot = self.fb_isResolvedFromCache.boolValue - ? self.lastSnapshot - : self.fb_takeSnapshot; + id snapshot = [self fb_customSnapshot]; [[FBXCElementSnapshotWrapper ensureWrapped:snapshot] fb_scrollRightByNormalizedDistance:distance inApplication:self.application]; } @@ -95,7 +86,8 @@ - (BOOL)fb_scrollToVisibleWithError:(NSError **)error return [self fb_scrollToVisibleWithNormalizedScrollDistance:FBScrollToVisibleNormalizedDistance error:error]; } -- (BOOL)fb_scrollToVisibleWithNormalizedScrollDistance:(CGFloat)normalizedScrollDistance error:(NSError **)error +- (BOOL)fb_scrollToVisibleWithNormalizedScrollDistance:(CGFloat)normalizedScrollDistance + error:(NSError **)error { return [self fb_scrollToVisibleWithNormalizedScrollDistance:normalizedScrollDistance scrollDirection:FBXCUIElementScrollDirectionUnknown @@ -106,7 +98,8 @@ - (BOOL)fb_scrollToVisibleWithNormalizedScrollDistance:(CGFloat)normalizedScroll scrollDirection:(FBXCUIElementScrollDirection)scrollDirection error:(NSError **)error { - FBXCElementSnapshotWrapper *prescrollSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:[self fb_takeSnapshot]]; + FBXCElementSnapshotWrapper *prescrollSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:[self fb_customSnapshot]]; + if (prescrollSnapshot.isWDVisible) { return YES; } @@ -138,12 +131,12 @@ - (BOOL)fb_scrollToVisibleWithNormalizedScrollDistance:(CGFloat)normalizedScroll FBXCElementSnapshotWrapper *wrappedCellSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:cellSnapshot]; if (wrappedCellSnapshot.wdVisible) { [visibleCellSnapshots addObject:cellSnapshot]; + if (visibleCellSnapshots.count > 1) { + return YES; + } } } - if (visibleCellSnapshots.count > 1) { - return YES; - } return NO; }]; @@ -184,23 +177,25 @@ - (BOOL)fb_scrollToVisibleWithNormalizedScrollDistance:(CGFloat)normalizedScroll FBXCElementSnapshotWrapper *scrollViewWrapped = [FBXCElementSnapshotWrapper ensureWrapped:scrollView]; // Scrolling till cell is visible and get current value of frames while (![self fb_isEquivalentElementSnapshotVisible:prescrollSnapshot] && scrollCount < maxScrollCount) { - if (targetCellIndex < visibleCellIndex) { - scrollDirection == FBXCUIElementScrollDirectionVertical ? - [scrollViewWrapped fb_scrollUpByNormalizedDistance:normalizedScrollDistance - inApplication:self.application] : - [scrollViewWrapped fb_scrollLeftByNormalizedDistance:normalizedScrollDistance - inApplication:self.application]; - } - else { - scrollDirection == FBXCUIElementScrollDirectionVertical ? - [scrollViewWrapped fb_scrollDownByNormalizedDistance:normalizedScrollDistance + @autoreleasepool { + if (targetCellIndex < visibleCellIndex) { + scrollDirection == FBXCUIElementScrollDirectionVertical ? + [scrollViewWrapped fb_scrollUpByNormalizedDistance:normalizedScrollDistance inApplication:self.application] : - [scrollViewWrapped fb_scrollRightByNormalizedDistance:normalizedScrollDistance - inApplication:self.application]; + [scrollViewWrapped fb_scrollLeftByNormalizedDistance:normalizedScrollDistance + inApplication:self.application]; + } + else { + scrollDirection == FBXCUIElementScrollDirectionVertical ? + [scrollViewWrapped fb_scrollDownByNormalizedDistance:normalizedScrollDistance + inApplication:self.application] : + [scrollViewWrapped fb_scrollRightByNormalizedDistance:normalizedScrollDistance + inApplication:self.application]; + } + scrollCount++; + // Wait for scroll animation + [self fb_waitUntilStableWithTimeout:FBConfiguration.animationCoolOffTimeout]; } - scrollCount++; - // Wait for scroll animation - [self fb_waitUntilStableWithTimeout:FBConfiguration.animationCoolOffTimeout]; } if (scrollCount >= maxScrollCount) { @@ -213,9 +208,9 @@ - (BOOL)fb_scrollToVisibleWithNormalizedScrollDistance:(CGFloat)normalizedScroll // Cell is now visible, but it might be only partialy visible, scrolling till whole frame is visible. // Sometimes, attempting to grab the parent snapshot of the target cell after scrolling is complete causes a stale element reference exception. // Trying fb_cachedSnapshot first - FBXCElementSnapshotWrapper *targetCellSnapshotWrapped = [FBXCElementSnapshotWrapper ensureWrapped:([self fb_cachedSnapshot] ?: [self fb_takeSnapshot])]; + FBXCElementSnapshotWrapper *targetCellSnapshotWrapped = [FBXCElementSnapshotWrapper ensureWrapped:[self fb_customSnapshot]]; targetCellSnapshot = [targetCellSnapshotWrapped fb_parentCellSnapshot]; - CGRect visibleFrame = [FBXCElementSnapshotWrapper ensureWrapped:targetCellSnapshot].fb_visibleFrameWithFallback; + CGRect visibleFrame = [FBXCElementSnapshotWrapper ensureWrapped:targetCellSnapshot].fb_visibleFrame; CGVector scrollVector = CGVectorMake(visibleFrame.size.width - targetCellSnapshot.frame.size.width, visibleFrame.size.height - targetCellSnapshot.frame.size.height @@ -233,7 +228,7 @@ - (BOOL)fb_isEquivalentElementSnapshotVisible:(id)snapshot return YES; } - id appSnapshot = [self.application fb_takeSnapshot]; + id appSnapshot = [self.application fb_standardSnapshot]; for (id elementSnapshot in appSnapshot._allDescendants.copy) { FBXCElementSnapshotWrapper *wrappedElementSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:elementSnapshot]; // We are comparing pre-scroll snapshot so frames are irrelevant. @@ -255,27 +250,32 @@ - (CGRect)scrollingFrame return self.visibleFrame; } -- (void)fb_scrollUpByNormalizedDistance:(CGFloat)distance inApplication:(XCUIApplication *)application +- (void)fb_scrollUpByNormalizedDistance:(CGFloat)distance + inApplication:(XCUIApplication *)application { [self fb_scrollByNormalizedVector:CGVectorMake(0.0, distance) inApplication:application]; } -- (void)fb_scrollDownByNormalizedDistance:(CGFloat)distance inApplication:(XCUIApplication *)application +- (void)fb_scrollDownByNormalizedDistance:(CGFloat)distance + inApplication:(XCUIApplication *)application { [self fb_scrollByNormalizedVector:CGVectorMake(0.0, -distance) inApplication:application]; } -- (void)fb_scrollLeftByNormalizedDistance:(CGFloat)distance inApplication:(XCUIApplication *)application +- (void)fb_scrollLeftByNormalizedDistance:(CGFloat)distance + inApplication:(XCUIApplication *)application { [self fb_scrollByNormalizedVector:CGVectorMake(distance, 0.0) inApplication:application]; } -- (void)fb_scrollRightByNormalizedDistance:(CGFloat)distance inApplication:(XCUIApplication *)application +- (void)fb_scrollRightByNormalizedDistance:(CGFloat)distance + inApplication:(XCUIApplication *)application { [self fb_scrollByNormalizedVector:CGVectorMake(-distance, 0.0) inApplication:application]; } -- (BOOL)fb_scrollByNormalizedVector:(CGVector)normalizedScrollVector inApplication:(XCUIApplication *)application +- (BOOL)fb_scrollByNormalizedVector:(CGVector)normalizedScrollVector + inApplication:(XCUIApplication *)application { CGVector scrollVector = CGVectorMake(CGRectGetWidth(self.scrollingFrame) * normalizedScrollVector.dx, CGRectGetHeight(self.scrollingFrame) * normalizedScrollVector.dy @@ -283,7 +283,9 @@ - (BOOL)fb_scrollByNormalizedVector:(CGVector)normalizedScrollVector inApplicati return [self fb_scrollByVector:scrollVector inApplication:application error:nil]; } -- (BOOL)fb_scrollByVector:(CGVector)vector inApplication:(XCUIApplication *)application error:(NSError **)error +- (BOOL)fb_scrollByVector:(CGVector)vector + inApplication:(XCUIApplication *)application + error:(NSError **)error { CGVector scrollBoundingVector = CGVectorMake( CGRectGetWidth(self.scrollingFrame) * FBScrollTouchProportion, @@ -314,7 +316,9 @@ - (CGVector)fb_hitPointOffsetForScrollingVector:(CGVector)scrollingVector return CGVectorMake((CGFloat)floor(x), (CGFloat)floor(y)); } -- (BOOL)fb_scrollAncestorScrollViewByVectorWithinScrollViewFrame:(CGVector)vector inApplication:(XCUIApplication *)application error:(NSError **)error +- (BOOL)fb_scrollAncestorScrollViewByVectorWithinScrollViewFrame:(CGVector)vector + inApplication:(XCUIApplication *)application + error:(NSError **)error { CGVector hitpointOffset = [self fb_hitPointOffsetForScrollingVector:vector]; diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBSwiping.h b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBSwiping.h index c30707dffb..4a2b5c1a1c 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBSwiping.h +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBSwiping.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBSwiping.m b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBSwiping.m index 4be4d34dc3..8f64b4832a 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBSwiping.m +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBSwiping.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIElement+FBSwiping.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBTVFocuse.h b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBTVFocuse.h index 6b25f9ab4f..0a3bc0794a 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBTVFocuse.h +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBTVFocuse.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBTVFocuse.m b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBTVFocuse.m index 30037646db..5aa8508435 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBTVFocuse.m +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBTVFocuse.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIElement+FBTVFocuse.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBTyping.h b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBTyping.h index c44aa7c57d..0b775589d2 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBTyping.h +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBTyping.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBTyping.m b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBTyping.m index 8b59ad0319..1d76f6ca04 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBTyping.m +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBTyping.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIElement+FBTyping.h" @@ -91,7 +90,7 @@ - (void)fb_prepareForTextInputWithSnapshot:(FBXCElementSnapshotWrapper *)snapsho [FBLogger logFmt:@"Trying to tap the \"%@\" element to have it focused", snapshot.fb_description]; [self tap]; // It might take some time to update the UI - [self fb_takeSnapshot]; + [self fb_standardSnapshot]; #endif } @@ -110,9 +109,7 @@ - (BOOL)fb_typeText:(NSString *)text frequency:(NSUInteger)frequency error:(NSError **)error { - id snapshot = self.fb_isResolvedFromCache.boolValue - ? self.lastSnapshot - : self.fb_takeSnapshot; + id snapshot = [self fb_standardSnapshot]; FBXCElementSnapshotWrapper *wrapped = [FBXCElementSnapshotWrapper ensureWrapped:snapshot]; [self fb_prepareForTextInputWithSnapshot:wrapped]; if (shouldClear && ![self fb_clearTextWithSnapshot:wrapped shouldPrepareForInput:NO error:error]) { @@ -123,9 +120,7 @@ - (BOOL)fb_typeText:(NSString *)text - (BOOL)fb_clearTextWithError:(NSError **)error { - id snapshot = self.fb_isResolvedFromCache.boolValue - ? self.lastSnapshot - : self.fb_takeSnapshot; + id snapshot = [self fb_standardSnapshot]; return [self fb_clearTextWithSnapshot:[FBXCElementSnapshotWrapper ensureWrapped:snapshot] shouldPrepareForInput:YES error:error]; @@ -182,7 +177,7 @@ - (BOOL)fb_clearTextWithSnapshot:(FBXCElementSnapshotWrapper *)snapshot return NO; } - currentValue = self.fb_takeSnapshot.value; + currentValue = [self fb_standardSnapshot].value; if (nil != placeholderValue && [currentValue isEqualToString:placeholderValue]) { // Short circuit if only the placeholder value left return YES; diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBUID.h b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBUID.h index 8b1a2da649..5a406adeae 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBUID.h +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBUID.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBXCElementSnapshotWrapper.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBUID.m b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBUID.m index ecac2cd91a..ebbb066549 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBUID.m +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBUID.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import @@ -22,14 +21,14 @@ - (unsigned long long)fb_accessibiltyId { return [FBElementUtils idWithAccessibilityElement:([self isKindOfClass:XCUIApplication.class] ? [(XCUIApplication *)self accessibilityElement] - : [self fb_takeSnapshot].accessibilityElement)]; + : [self fb_standardSnapshot].accessibilityElement)]; } - (NSString *)fb_uid { return [self isKindOfClass:XCUIApplication.class] ? [FBElementUtils uidWithAccessibilityElement:[(XCUIApplication *)self accessibilityElement]] - : [FBXCElementSnapshotWrapper ensureWrapped:[self fb_takeSnapshot]].fb_uid; + : [FBXCElementSnapshotWrapper ensureWrapped:[self fb_standardSnapshot]].fb_uid; } @end diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBUtilities.h b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBUtilities.h index 66b9ee2c93..6f4666688c 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBUtilities.h +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBUtilities.h @@ -3,13 +3,12 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import #import -#import "FBXCElementSnapshot.h" +#import NS_ASSUME_NONNULL_BEGIN @@ -18,70 +17,71 @@ NS_ASSUME_NONNULL_BEGIN /** Gets the most recent snapshot of the current element. The element will be automatically resolved if the snapshot is not available yet. - Calls to this method mutate the `lastSnapshot` instance property.. - Calls to this method reset the `fb_isResolvedFromCache` property value to `NO`. + Calls to this method mutate the `lastSnapshot` instance property. + The snapshot is taken by the native API provided by XCTest. + The maximum snapshot tree depth is set by `FBConfiguration.snapshotMaxDepth` + + Snapshot specifics: + - Most performant + - Memory-friedly + - `children` property is set to `nil` if not taken from XCUIApplication + - `value` property is cut off to max 512 bytes @return The recent snapshot of the element @throws FBStaleElementException if the element is not present in DOM and thus no snapshot could be made */ -- (id)fb_takeSnapshot; +- (id)fb_standardSnapshot; /** - Extracts the cached element snapshot from its query. - No requests to the accessiblity framework is made. - It is only safe to use this call right after element lookup query - has been executed. + Gets the most recent snapshot of the current element. The element will be + automatically resolved if the snapshot is not available yet. + Calls to this method mutate the `lastSnapshot` instance property. + The maximum snapshot tree depth is set by `FBConfiguration.snapshotMaxDepth` - @return Either the cached snapshot or nil + Snapshot specifics: + - Less performant in comparison to the standard one + - `children` property is always defined + - `value` property is not cut off + + @return The recent snapshot of the element + @throws FBStaleElementException if the element is not present in DOM and thus no snapshot could be made */ -- (nullable id)fb_cachedSnapshot; +- (id)fb_customSnapshot; /** - Gets the most recent snapshot of the current element and already resolves the accessibility attributes - needed for creating the page source of this element. No additional calls to the accessibility layer - are required. + Gets the most recent snapshot of the current element. The element will be + automatically resolved if the snapshot is not available yet. Calls to this method mutate the `lastSnapshot` instance property. - Calls to this method reset the `fb_isResolvedFromCache` property value to `NO`. + The maximum snapshot tree depth is set by `FBConfiguration.snapshotMaxDepth` + + Snapshot specifics: + - Less performant in comparison to the standard one + - The `hittable` property calculation is aligned with the native calculation logic - @param maxDepth The maximum depth of the snapshot. nil value means to use the default depth. - with custom attributes cannot be resolved - - @return The recent snapshot of the element with all attributes resolved or a snapshot with default - attributes resolved if there was a failure while resolving additional attributes + @return The recent snapshot of the element @throws FBStaleElementException if the element is not present in DOM and thus no snapshot could be made */ -- (nullable id)fb_snapshotWithAllAttributesAndMaxDepth:(nullable NSNumber *)maxDepth; +- (id)fb_nativeSnapshot; /** - Gets the most recent snapshot of the current element with given attributes resolved. - No additional calls to the accessibility layer are required. - Calls to this method mutate the `lastSnapshot` instance property. - Calls to this method reset the `fb_isResolvedFromCache` property value to `NO`. - - @param attributeNames The list of attribute names to resolve. Must be one of - FB_...Name values exported by XCTestPrivateSymbols.h module. - `nil` value means that only the default attributes must be extracted - @param maxDepth The maximum depth of the snapshot. nil value means to use the default depth. + Extracts the cached element snapshot from its query. + No requests to the accessiblity framework is made. + It is only safe to use this call right after element lookup query + has been executed. - @return The recent snapshot of the element with the given attributes resolved or a snapshot with default - attributes resolved if there was a failure while resolving additional attributes - @throws FBStaleElementException if the element is not present in DOM and thus no snapshot could be made -*/ -- (nullable id)fb_snapshotWithAttributes:(nullable NSArray *)attributeNames - maxDepth:(nullable NSNumber *)maxDepth; + @return Either the cached snapshot or nil + */ +- (nullable id)fb_cachedSnapshot; /** Filters elements by matching them to snapshots from the corresponding array @param snapshots Array of snapshots to be matched with - @param selfUID Optionally the unique identifier of the current element. - Providing it as an argument improves the performance of the method. @param onlyChildren Whether to only look for direct element children @return Array of filtered elements, which have matches in snapshots array */ - (NSArray *)fb_filterDescendantsWithSnapshots:(NSArray> *)snapshots - selfUID:(nullable NSString *)selfUID onlyChildren:(BOOL)onlyChildren; /** diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBUtilities.m b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBUtilities.m index 5f9fd96f4a..b627fc862f 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBUtilities.m +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBUtilities.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIElement+FBUtilities.h" @@ -14,6 +13,7 @@ #import "FBConfiguration.h" #import "FBExceptions.h" #import "FBImageUtils.h" +#import "FBElementUtils.h" #import "FBLogger.h" #import "FBMacros.h" #import "FBMathUtils.h" @@ -32,6 +32,7 @@ #import "XCTestPrivateSymbols.h" #import "XCTRunnerDaemonSession.h" #import "XCUIApplicationProcess+FBQuiescence.h" +#import "XCUIApplication.h" #import "XCUIElement+FBCaching.h" #import "XCUIElement+FBWebDriverAttributes.h" #import "XCUIElementQuery.h" @@ -40,94 +41,50 @@ #import "XCUIScreen.h" #import "XCUIElement+FBResolve.h" -#define DEFAULT_AX_TIMEOUT 60. - @implementation XCUIElement (FBUtilities) -- (id)fb_takeSnapshot +- (id)fb_takeSnapshot:(BOOL)isCustom { - NSError *error = nil; - self.fb_isResolvedFromCache = @(NO); - self.lastSnapshot = [self.fb_query fb_uniqueSnapshotWithError:&error]; - if (nil == self.lastSnapshot) { - NSString *hintText = @"Make sure the application UI has the expected state"; - if (nil != error - && [error.localizedDescription containsString:@"Identity Binding"]) { - hintText = [NSString stringWithFormat:@"%@. You could also try to switch the binding strategy using the 'boundElementsByIndex' setting for the element lookup", hintText]; - } - NSString *reason = [NSString stringWithFormat:@"The previously found element \"%@\" is not present in the current view anymore. %@", self.description, hintText]; - if (nil != error) { - reason = [NSString stringWithFormat:@"%@. Original error: %@", reason, error.localizedDescription]; + __block id snapshot = nil; + @autoreleasepool { + NSError *error = nil; + snapshot = isCustom + ? [self.fb_query fb_uniqueSnapshotWithError:&error] + : (id)[self snapshotWithError:&error]; + if (nil == snapshot) { + [self fb_raiseStaleElementExceptionWithError:error]; } - @throw [NSException exceptionWithName:FBStaleElementException reason:reason userInfo:@{}]; } + self.lastSnapshot = snapshot; return self.lastSnapshot; } -- (id)fb_cachedSnapshot +- (id)fb_standardSnapshot { - return [self.query fb_cachedSnapshot]; + return [self fb_takeSnapshot:NO]; } -- (nullable id)fb_snapshotWithAllAttributesAndMaxDepth:(NSNumber *)maxDepth +- (id)fb_customSnapshot { - NSMutableArray *allNames = [NSMutableArray arrayWithArray:FBStandardAttributeNames()]; - [allNames addObjectsFromArray:FBCustomAttributeNames()]; - return [self fb_snapshotWithAttributes:allNames.copy - maxDepth:maxDepth]; + return [self fb_takeSnapshot:YES]; } -- (nullable id)fb_snapshotWithAttributes:(NSArray *)attributeNames - maxDepth:(NSNumber *)maxDepth +- (id)fb_nativeSnapshot { - NSSet *standardAttributes = [NSSet setWithArray:FBStandardAttributeNames()]; - id snapshot = self.fb_takeSnapshot; - NSTimeInterval axTimeout = FBConfiguration.customSnapshotTimeout; - if (nil == attributeNames - || [[NSSet setWithArray:attributeNames] isSubsetOfSet:standardAttributes] - || axTimeout < DBL_EPSILON) { - // return the "normal" element snapshot if no custom attributes are requested - return snapshot; - } - - id axElement = snapshot.accessibilityElement; - if (nil == axElement) { - return nil; - } - - NSError *setTimeoutError; - BOOL isTimeoutSet = [FBXCAXClientProxy.sharedClient setAXTimeout:axTimeout - error:&setTimeoutError]; - if (!isTimeoutSet) { - [FBLogger logFmt:@"Cannot set snapshoting timeout to %.1fs. Original error: %@", - axTimeout, setTimeoutError.localizedDescription]; - } - - NSError *error; - id snapshotWithAttributes = [FBXCAXClientProxy.sharedClient snapshotForElement:axElement - attributes:attributeNames - maxDepth:maxDepth - error:&error]; - if (nil == snapshotWithAttributes) { - NSString *description = [FBXCElementSnapshotWrapper ensureWrapped:snapshot].fb_description; - [FBLogger logFmt:@"Cannot take a snapshot with attribute(s) %@ of '%@' after %.2f seconds", - attributeNames, description, axTimeout]; - [FBLogger logFmt:@"This timeout could be customized via '%@' setting", FB_SETTING_CUSTOM_SNAPSHOT_TIMEOUT]; - [FBLogger logFmt:@"Internal error: %@", error.localizedDescription]; - [FBLogger logFmt:@"Falling back to the default snapshotting mechanism for the element '%@' (some attribute values, like visibility or accessibility might not be precise though)", description]; - snapshotWithAttributes = self.lastSnapshot; - } else { - self.lastSnapshot = snapshotWithAttributes; + NSError *error = nil; + BOOL isSuccessful = [self resolveOrRaiseTestFailure:NO error:&error]; + if (nil == self.lastSnapshot || !isSuccessful) { + [self fb_raiseStaleElementExceptionWithError:error]; } + return self.lastSnapshot; +} - if (isTimeoutSet) { - [FBXCAXClientProxy.sharedClient setAXTimeout:DEFAULT_AX_TIMEOUT error:nil]; - } - return snapshotWithAttributes; +- (id)fb_cachedSnapshot +{ + return [self.query fb_cachedSnapshot]; } - (NSArray *)fb_filterDescendantsWithSnapshots:(NSArray> *)snapshots - selfUID:(NSString *)selfUID onlyChildren:(BOOL)onlyChildren { if (0 == snapshots.count) { @@ -135,21 +92,19 @@ @implementation XCUIElement (FBUtilities) } NSMutableArray *matchedIds = [NSMutableArray new]; for (id snapshot in snapshots) { - NSString *uid = [FBXCElementSnapshotWrapper wdUIDWithSnapshot:snapshot]; - if (nil != uid) { - [matchedIds addObject:uid]; + @autoreleasepool { + NSString *uid = [FBXCElementSnapshotWrapper wdUIDWithSnapshot:snapshot]; + if (nil != uid) { + [matchedIds addObject:uid]; + } } } NSMutableArray *matchedElements = [NSMutableArray array]; - NSString *uid = selfUID; - if (nil == uid) { - uid = self.fb_isResolvedFromCache.boolValue - ? [FBXCElementSnapshotWrapper wdUIDWithSnapshot:self.lastSnapshot] - : self.fb_uid; - } + NSString *uid = nil == self.lastSnapshot + ? self.fb_uid + : [FBXCElementSnapshotWrapper wdUIDWithSnapshot:self.lastSnapshot]; if (nil != uid && [matchedIds containsObject:uid]) { - XCUIElement *stableSelf = self.fb_stableInstance; - stableSelf.fb_isResolvedNatively = @NO; + XCUIElement *stableSelf = [self fb_stableInstanceWithUid:uid]; if (1 == snapshots.count) { return @[stableSelf]; } @@ -198,4 +153,18 @@ - (void)fb_waitUntilStableWithTimeout:(NSTimeInterval)timeout FBConfiguration.waitForIdleTimeout = previousTimeout; } +- (void)fb_raiseStaleElementExceptionWithError:(NSError *)error __attribute__((noreturn)) +{ + NSString *hintText = @"Make sure the application UI has the expected state"; + if (nil != error && [error.localizedDescription containsString:@"Identity Binding"]) { + hintText = [NSString stringWithFormat:@"%@. You could also try to switch the binding strategy using the 'boundElementsByIndex' setting for the element lookup", hintText]; + } + NSString *reason = [NSString stringWithFormat:@"The previously found element \"%@\" is not present in the current view anymore. %@", + self.description, hintText]; + if (nil != error) { + reason = [NSString stringWithFormat:@"%@. Original error: %@", reason, error.localizedDescription]; + } + @throw [NSException exceptionWithName:FBStaleElementException reason:reason userInfo:@{}]; +} + @end diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBVisibleFrame.h b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBVisibleFrame.h new file mode 100644 index 0000000000..e2355c9951 --- /dev/null +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBVisibleFrame.h @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBXCElementSnapshotWrapper.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface XCUIElement (FBVisibleFrame) + +/** + Returns the snapshot visibleFrame with a fallback to direct attribute retrieval from FBXCAXClient in case of a snapshot fault (nil visibleFrame) + + @return the snapshot visibleFrame + */ +- (CGRect)fb_visibleFrame; + +@end + +@interface FBXCElementSnapshotWrapper (FBVisibleFrame) + +/** + Returns the snapshot visibleFrame with a fallback to direct attribute retrieval from FBXCAXClient in case of a snapshot fault (nil visibleFrame) + + @return the snapshot visibleFrame + */ +- (CGRect)fb_visibleFrame; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBVisibleFrame.m b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBVisibleFrame.m new file mode 100644 index 0000000000..8ba5b0eaca --- /dev/null +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBVisibleFrame.m @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "XCUIElement+FBVisibleFrame.h" +#import "FBElementUtils.h" +#import "FBXCodeCompatibility.h" +#import "FBXCElementSnapshotWrapper+Helpers.h" +#import "XCUIElement+FBUtilities.h" +#import "XCTestPrivateSymbols.h" + +@implementation XCUIElement (FBVisibleFrame) + +- (CGRect)fb_visibleFrame +{ + id snapshot = [self fb_standardSnapshot]; + return [FBXCElementSnapshotWrapper ensureWrapped:snapshot].fb_visibleFrame; +} + +@end + +@implementation FBXCElementSnapshotWrapper (FBVisibleFrame) + +- (CGRect)fb_visibleFrame +{ + CGRect thisVisibleFrame = [self visibleFrame]; + if (!CGRectIsEmpty(thisVisibleFrame)) { + return thisVisibleFrame; + } + + NSDictionary *visibleFrameDict = [self fb_attributeValue:FB_XCAXAVisibleFrameAttributeName + error:nil]; + if (nil == visibleFrameDict) { + return thisVisibleFrame; + } + + id x = [visibleFrameDict objectForKey:@"X"]; + id y = [visibleFrameDict objectForKey:@"Y"]; + id height = [visibleFrameDict objectForKey:@"Height"]; + id width = [visibleFrameDict objectForKey:@"Width"]; + if (x != nil && y != nil && height != nil && width != nil) { + return CGRectMake([x doubleValue], [y doubleValue], [width doubleValue], [height doubleValue]); + } + + return thisVisibleFrame; +} + +@end diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.h b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.h index fdeedaf91d..8e598d7a67 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.h +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.h @@ -3,13 +3,12 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import #import -#import "FBXCElementSnapshotWrapper.h" +#import NS_ASSUME_NONNULL_BEGIN diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.m b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.m index b29c5a8b26..ca3023bfbc 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.m +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.m @@ -3,13 +3,13 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIElement+FBWebDriverAttributes.h" #import "FBElementTypeTransformer.h" +#import "FBElementHelpers.h" #import "FBLogger.h" #import "FBMacros.h" #import "FBXCElementSnapshotWrapper.h" @@ -21,6 +21,8 @@ #import "FBElementUtils.h" #import "XCTestPrivateSymbols.h" #import "XCUIHitPointResult.h" +#import "FBAccessibilityTraits.h" +#import "XCUIElement+FBMinMax.h" #define BROKEN_RECT CGRectMake(-1, -1, 0, 0) @@ -28,24 +30,20 @@ @implementation XCUIElement (WebDriverAttributesForwarding) - (id)fb_snapshotForAttributeName:(NSString *)name { - // These attributes are special, because we can only retrieve them from - // the snapshot if we explicitly ask XCTest to include them into the query while taking it. - // That is why fb_snapshotWithAllAttributes method must be used instead of the default snapshot - // call - if ([name isEqualToString:FBStringify(XCUIElement, isWDVisible)]) { - return [self fb_snapshotWithAttributes:@[FB_XCAXAIsVisibleAttributeName] - maxDepth:@1]; + // https://github.com/appium/appium-xcuitest-driver/pull/2565 + if ([name isEqualToString:FBStringify(XCUIElement, isWDHittable)]) { + return [self fb_nativeSnapshot]; } - if ([name isEqualToString:FBStringify(XCUIElement, isWDAccessible)]) { - return [self fb_snapshotWithAttributes:@[FB_XCAXAIsElementAttributeName] - maxDepth:@1]; + // https://github.com/appium/appium-xcuitest-driver/issues/2552 + BOOL isValueRequest = [name isEqualToString:FBStringify(XCUIElement, wdValue)]; + if ([self isKindOfClass:XCUIApplication.class] && !isValueRequest) { + return [self fb_standardSnapshot]; } - if ([name isEqualToString:FBStringify(XCUIElement, isWDAccessibilityContainer)]) { - return [self fb_snapshotWithAttributes:@[FB_XCAXAIsElementAttributeName] - maxDepth:nil]; - } - - return self.fb_takeSnapshot; + BOOL isCustomSnapshot = [name isEqualToString:FBStringify(XCUIElement, isWDAccessible)] + || [name isEqualToString:FBStringify(XCUIElement, isWDAccessibilityContainer)] + || [name isEqualToString:FBStringify(XCUIElement, wdIndex)] + || isValueRequest; + return isCustomSnapshot ? [self fb_customSnapshot] : [self fb_standardSnapshot]; } - (id)fb_valueForWDAttributeName:(NSString *)name @@ -78,6 +76,16 @@ - (id)fb_valueForWDAttributeName:(NSString *)name return [self valueForKey:[FBElementUtils wdAttributeNameForAttributeName:name]]; } +- (NSNumber *)wdMinValue +{ + return self.fb_minValue; +} + +- (NSNumber *)wdMaxValue +{ + return self.fb_maxValue; +} + - (NSString *)wdValue { id value = self.value; @@ -90,9 +98,7 @@ - (NSString *)wdValue value = FBFirstNonEmptyValue(value, isSelected); } else if (elementType == XCUIElementTypeSwitch) { value = @([value boolValue]); - } else if (elementType == XCUIElementTypeTextView || - elementType == XCUIElementTypeTextField || - elementType == XCUIElementTypeSecureTextField) { + } else if (FBDoesElementSupportInnerText(elementType)) { NSString *placeholderValue = self.placeholderValue; value = FBFirstNonEmptyValue(value, placeholderValue); } @@ -120,12 +126,18 @@ - (NSString *)wdName - (NSString *)wdLabel { - NSString *label = self.label; XCUIElementType elementType = self.elementType; - if (elementType == XCUIElementTypeTextField || elementType == XCUIElementTypeSecureTextField ) { - return label; - } - return FBTransferEmptyStringToNil(label); + return (elementType == XCUIElementTypeTextField + || elementType == XCUIElementTypeSecureTextField) + ? self.label + : FBTransferEmptyStringToNil(self.label); +} + +- (NSString *)wdPlaceholderValue +{ + return FBDoesElementSupportInnerText(self.elementType) + ? self.placeholderValue + : FBTransferEmptyStringToNil(self.placeholderValue); } - (NSString *)wdType @@ -150,6 +162,30 @@ - (CGRect)wdFrame : CGRectIntegral(frame); } +- (CGRect)wdNativeFrame +{ + // To avoid confusion regarding the frame returned by `wdFrame`, + // the current property is provided to represent the element's + // actual rendered frame. + return self.frame; +} + +/** + Returns a comma-separated string of accessibility traits for the element. + This method converts the element's accessibility traits bitmask into human-readable strings + using FBAccessibilityTraitsToStringsArray. The traits represent various accessibility + characteristics of the element such as Button, Link, Image, etc. + You can find the list of possible traits in the Apple documentation: + https://developer.apple.com/documentation/uikit/uiaccessibilitytraits?language=objc + + @return A comma-separated string of accessibility traits, or an empty string if no traits are set + */ +- (NSString *)wdTraits +{ + NSArray *traits = FBAccessibilityTraitsToStringsArray(self.snapshot.traits); + return [traits componentsJoinedByString:@", "]; +} + - (BOOL)isWDVisible { return self.fb_isVisible; @@ -183,8 +219,8 @@ - (BOOL)isWDAccessible // In the scenario when table provides Search results controller, table could be marked as accessible element, even though it isn't // As it is highly unlikely that table view should ever be an accessibility element itself, // for now we work around that by skipping Table View in container checks - if ([FBXCElementSnapshotWrapper ensureWrapped:parentSnapshot].fb_isAccessibilityElement - && parentSnapshot.elementType != XCUIElementTypeTable) { + if (parentSnapshot.elementType != XCUIElementTypeTable + && [FBXCElementSnapshotWrapper ensureWrapped:parentSnapshot].fb_isAccessibilityElement) { return NO; } parentSnapshot = parentSnapshot.parent; diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElementQuery+FBHelpers.h b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElementQuery+FBHelpers.h index 04ff8bfad1..8c82890741 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElementQuery+FBHelpers.h +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElementQuery+FBHelpers.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElementQuery+FBHelpers.m b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElementQuery+FBHelpers.m index 81e2a522e6..d2438a0cb6 100644 --- a/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElementQuery+FBHelpers.m +++ b/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElementQuery+FBHelpers.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIElementQuery+FBHelpers.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Commands/FBAlertViewCommands.h b/WebDriverAgent/WebDriverAgentLib/Commands/FBAlertViewCommands.h index 3d4c2638cc..687c39ce02 100644 --- a/WebDriverAgent/WebDriverAgentLib/Commands/FBAlertViewCommands.h +++ b/WebDriverAgent/WebDriverAgentLib/Commands/FBAlertViewCommands.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Commands/FBAlertViewCommands.m b/WebDriverAgent/WebDriverAgentLib/Commands/FBAlertViewCommands.m index 760244629a..50479936e0 100644 --- a/WebDriverAgent/WebDriverAgentLib/Commands/FBAlertViewCommands.m +++ b/WebDriverAgent/WebDriverAgentLib/Commands/FBAlertViewCommands.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBAlertViewCommands.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Commands/FBCustomCommands.h b/WebDriverAgent/WebDriverAgentLib/Commands/FBCustomCommands.h index 869d14fd1a..dca1542293 100644 --- a/WebDriverAgent/WebDriverAgentLib/Commands/FBCustomCommands.h +++ b/WebDriverAgent/WebDriverAgentLib/Commands/FBCustomCommands.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Commands/FBCustomCommands.m b/WebDriverAgent/WebDriverAgentLib/Commands/FBCustomCommands.m index 2ab039e6b5..d20c485d0b 100644 --- a/WebDriverAgent/WebDriverAgentLib/Commands/FBCustomCommands.m +++ b/WebDriverAgent/WebDriverAgentLib/Commands/FBCustomCommands.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBCustomCommands.h" @@ -567,7 +566,8 @@ + (NSString *)timeZone FBElementCache *elementCache = request.session.elementCache; BOOL hasElement = ![request.parameters[@"uuid"] isEqual:@"0"]; XCUIElement *destination = hasElement - ? [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]] + ? [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"] + checkStaleness:YES] : request.session.activeApplication; id keys = request.arguments[@"keys"]; diff --git a/WebDriverAgent/WebDriverAgentLib/Commands/FBDebugCommands.h b/WebDriverAgent/WebDriverAgentLib/Commands/FBDebugCommands.h index 32f28b1b23..99728e03af 100644 --- a/WebDriverAgent/WebDriverAgentLib/Commands/FBDebugCommands.h +++ b/WebDriverAgent/WebDriverAgentLib/Commands/FBDebugCommands.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Commands/FBDebugCommands.m b/WebDriverAgent/WebDriverAgentLib/Commands/FBDebugCommands.m index c3f9a78160..42ea74b3d5 100644 --- a/WebDriverAgent/WebDriverAgentLib/Commands/FBDebugCommands.m +++ b/WebDriverAgent/WebDriverAgentLib/Commands/FBDebugCommands.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBDebugCommands.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Commands/FBElementCommands.h b/WebDriverAgent/WebDriverAgentLib/Commands/FBElementCommands.h index fcd1c86ea9..3c07ee6d60 100644 --- a/WebDriverAgent/WebDriverAgentLib/Commands/FBElementCommands.h +++ b/WebDriverAgent/WebDriverAgentLib/Commands/FBElementCommands.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Commands/FBElementCommands.m b/WebDriverAgent/WebDriverAgentLib/Commands/FBElementCommands.m index 762bad3275..9c2ea009cd 100644 --- a/WebDriverAgent/WebDriverAgentLib/Commands/FBElementCommands.m +++ b/WebDriverAgent/WebDriverAgentLib/Commands/FBElementCommands.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBElementCommands.h" @@ -35,6 +34,7 @@ #import "XCUIElement+FBWebDriverAttributes.h" #import "XCUIElement+FBTVFocuse.h" #import "XCUIElement+FBResolve.h" +#import "XCUIElement+FBUID.h" #import "FBElementTypeTransformer.h" #import "XCUIElement.h" #import "XCUIElementQuery.h" @@ -52,6 +52,7 @@ + (NSArray *)routes return @[ [[FBRoute GET:@"/window/size"] respondWithTarget:self action:@selector(handleGetWindowSize:)], + [[FBRoute GET:@"/window/rect"] respondWithTarget:self action:@selector(handleGetWindowRect:)], [[FBRoute GET:@"/window/size"].withoutSession respondWithTarget:self action:@selector(handleGetWindowSize:)], [[FBRoute GET:@"/element/:uuid/enabled"] respondWithTarget:self action:@selector(handleGetEnabled:)], [[FBRoute GET:@"/element/:uuid/rect"] respondWithTarget:self action:@selector(handleGetRect:)], @@ -126,38 +127,22 @@ + (NSArray *)routes { FBElementCache *elementCache = request.session.elementCache; XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; - BOOL isEnabled = [FBXCElementSnapshotWrapper ensureWrapped:element.lastSnapshot].isWDEnabled; - return FBResponseWithObject(isEnabled ? @YES : @NO); + return FBResponseWithObject(@(element.isWDEnabled)); } + (id)handleGetRect:(FBRouteRequest *)request { FBElementCache *elementCache = request.session.elementCache; XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; - return FBResponseWithObject([FBXCElementSnapshotWrapper ensureWrapped:element.lastSnapshot].wdRect); + return FBResponseWithObject(element.wdRect); } + (id)handleGetAttribute:(FBRouteRequest *)request { FBElementCache *elementCache = request.session.elementCache; NSString *attributeName = request.parameters[@"name"]; - NSString *wdAttributeName = [FBElementUtils wdAttributeNameForAttributeName:attributeName]; - NSArray *additionalAttributes = nil; - NSNumber *maxDepth = nil; - if ([wdAttributeName isEqualToString:FBStringify(XCUIElement, isWDVisible)]) { - additionalAttributes = @[FB_XCAXAIsVisibleAttributeName]; - maxDepth = @1; - } else if ([wdAttributeName isEqualToString:FBStringify(XCUIElement, isWDEnabled)]) { - additionalAttributes = @[FB_XCAXAIsElementAttributeName]; - maxDepth = @1; - } else if ([wdAttributeName isEqualToString:FBStringify(XCUIElement, isWDAccessibilityContainer)]) { - additionalAttributes = @[FB_XCAXAIsElementAttributeName]; - } - XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"] - resolveForAdditionalAttributes:additionalAttributes - andMaxDepth:maxDepth]; - FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:element.lastSnapshot]; - id attributeValue = [wrappedSnapshot fb_valueForWDAttributeName:attributeName]; + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; + id attributeValue = [element fb_valueForWDAttributeName:attributeName]; return FBResponseWithObject(attributeValue ?: [NSNull null]); } @@ -165,7 +150,9 @@ + (NSArray *)routes { FBElementCache *elementCache = request.session.elementCache; XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; - FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:element.lastSnapshot]; + // https://github.com/appium/appium-xcuitest-driver/issues/2552 + id snapshot = [element fb_customSnapshot]; + FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:snapshot]; id text = FBFirstNonEmptyValue(wrappedSnapshot.wdValue, wrappedSnapshot.wdLabel); return FBResponseWithObject(text ?: @""); } @@ -173,48 +160,43 @@ + (NSArray *)routes + (id)handleGetDisplayed:(FBRouteRequest *)request { FBElementCache *elementCache = request.session.elementCache; - XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"] - resolveForAdditionalAttributes:@[FB_XCAXAIsVisibleAttributeName] - andMaxDepth:@1]; - return FBResponseWithObject(@([FBXCElementSnapshotWrapper ensureWrapped:element.lastSnapshot].isWDVisible)); + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; + return FBResponseWithObject(@(element.isWDVisible)); } + (id)handleGetAccessible:(FBRouteRequest *)request { FBElementCache *elementCache = request.session.elementCache; - XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"] - resolveForAdditionalAttributes:@[FB_XCAXAIsElementAttributeName] - andMaxDepth:@1]; - return FBResponseWithObject(@([FBXCElementSnapshotWrapper ensureWrapped:element.lastSnapshot].isWDAccessible)); + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; + return FBResponseWithObject(@(element.isWDAccessible)); } + (id)handleGetIsAccessibilityContainer:(FBRouteRequest *)request { FBElementCache *elementCache = request.session.elementCache; - XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"] - resolveForAdditionalAttributes:@[FB_XCAXAIsElementAttributeName] - andMaxDepth:nil]; - return FBResponseWithObject(@([FBXCElementSnapshotWrapper ensureWrapped:element.lastSnapshot].isWDAccessibilityContainer)); + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; + return FBResponseWithObject(@(element.isWDAccessibilityContainer)); } + (id)handleGetName:(FBRouteRequest *)request { FBElementCache *elementCache = request.session.elementCache; XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; - return FBResponseWithObject([FBXCElementSnapshotWrapper ensureWrapped:element.lastSnapshot].wdType); + return FBResponseWithObject(element.wdType); } + (id)handleGetSelected:(FBRouteRequest *)request { FBElementCache *elementCache = request.session.elementCache; XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; - return FBResponseWithObject(@([FBXCElementSnapshotWrapper ensureWrapped:element.lastSnapshot].wdSelected)); + return FBResponseWithObject(@(element.wdSelected)); } + (id)handleSetValue:(FBRouteRequest *)request { FBElementCache *elementCache = request.session.elementCache; - XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"] + checkStaleness:YES]; id value = request.arguments[@"value"] ?: request.arguments[@"text"]; if (!value) { return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Neither 'value' nor 'text' parameter is provided" traceback:nil]); @@ -222,7 +204,7 @@ + (NSArray *)routes NSString *textToType = [value isKindOfClass:NSArray.class] ? [value componentsJoinedByString:@""] : value; - XCUIElementType elementType = [(id)element.lastSnapshot elementType]; + XCUIElementType elementType = [element elementType]; #if !TARGET_OS_TV if (elementType == XCUIElementTypePickerWheel) { [element adjustToPickerWheelValue:textToType]; @@ -251,7 +233,7 @@ + (NSArray *)routes + (id)handleClick:(FBRouteRequest *)request { FBElementCache *elementCache = request.session.elementCache; - XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"] checkStaleness:YES]; #if TARGET_OS_IOS [element tap]; #elif TARGET_OS_TV @@ -285,7 +267,10 @@ + (NSArray *)routes if (focusedElement != nil) { FBElementCache *elementCache = request.session.elementCache; BOOL useNativeCachingStrategy = request.session.useNativeCachingStrategy; - NSString *focusedUUID = [elementCache storeElement:(useNativeCachingStrategy ? focusedElement : focusedElement.fb_stableInstance)]; + NSString *focusedUUID = [elementCache storeElement:(useNativeCachingStrategy + ? focusedElement + : [focusedElement fb_stableInstanceWithUid:focusedElement.fb_uid])]; + focusedElement.lastSnapshot = nil; if (focusedUUID && [focusedUUID isEqualToString:(id)request.parameters[@"uuid"]]) { isFocused = YES; } @@ -353,7 +338,7 @@ + (NSArray *)routes FBElementCache *elementCache = request.session.elementCache; XCUIElement *element = [self targetFromRequest:request]; [element pressForDuration:[request.arguments[@"pressDuration"] doubleValue] - thenDragToElement:[elementCache elementForUUID:(NSString *)request.arguments[@"toElement"]] + thenDragToElement:[elementCache elementForUUID:(NSString *)request.arguments[@"toElement"] checkStaleness:YES] withVelocity:[request.arguments[@"velocity"] doubleValue] thenHoldForDuration:[request.arguments[@"holdDuration"] doubleValue]]; return FBResponseWithOK(); @@ -441,10 +426,7 @@ + (NSArray *)routes + (id)handleDrag:(FBRouteRequest *)request { - NSString *elementUdid = (NSString *)request.parameters[@"uuid"]; - XCUIElement *target = nil == elementUdid - ? request.session.activeApplication - : [request.session.elementCache elementForUUID:elementUdid]; + XCUIElement *target = [self targetFromRequest:request]; CGVector startOffset = CGVectorMake([request.arguments[@"fromX"] doubleValue], [request.arguments[@"fromY"] doubleValue]); XCUICoordinate *startCoordinate = [self.class gestureCoordinateWithOffset:startOffset element:target]; @@ -546,13 +528,32 @@ + (NSArray *)routes { XCUIApplication *app = request.session.activeApplication ?: XCUIApplication.fb_activeApplication; + CGRect frame = app.wdFrame; #if TARGET_OS_TV - CGSize screenSize = app.frame.size; + CGSize screenSize = frame.size; #else + CGSize screenSize = FBAdjustDimensionsForApplication(frame.size, app.interfaceOrientation); +#endif + return FBResponseWithObject(@{ + @"width": @(screenSize.width), + @"height": @(screenSize.height), + }); +} + + ++ (id)handleGetWindowRect:(FBRouteRequest *)request +{ + XCUIApplication *app = request.session.activeApplication ?: XCUIApplication.fb_activeApplication; + CGRect frame = app.wdFrame; +#if TARGET_OS_TV + CGSize screenSize = frame.size; +#else CGSize screenSize = FBAdjustDimensionsForApplication(frame.size, app.interfaceOrientation); #endif return FBResponseWithObject(@{ + @"x": @(frame.origin.x), + @"y": @(frame.origin.y), @"width": @(screenSize.width), @"height": @(screenSize.height), }); @@ -560,16 +561,23 @@ + (NSArray *)routes + (id)handleElementScreenshot:(FBRouteRequest *)request { - FBElementCache *elementCache = request.session.elementCache; - XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; - NSData *screenshotData = [element.screenshot PNGRepresentation]; - if (nil == screenshotData) { - NSString *errMsg = [NSString stringWithFormat:@"Cannot take a screenshot of %@", element.description]; - return FBResponseWithStatus([FBCommandStatus unableToCaptureScreenErrorWithMessage:errMsg - traceback:nil]); + @autoreleasepool { + FBElementCache *elementCache = request.session.elementCache; + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"] + checkStaleness:YES]; + NSData *screenshotData = nil; + @autoreleasepool { + screenshotData = [element.screenshot PNGRepresentation]; + if (nil == screenshotData) { + NSString *errMsg = [NSString stringWithFormat:@"Cannot take a screenshot of %@", element.description]; + return FBResponseWithStatus([FBCommandStatus unableToCaptureScreenErrorWithMessage:errMsg + traceback:nil]); + } + } + NSString *screenshot = [screenshotData base64EncodedStringWithOptions:0]; + screenshotData = nil; + return FBResponseWithObject(screenshot); } - NSString *screenshot = [screenshotData base64EncodedStringWithOptions:0]; - return FBResponseWithObject(screenshot); } @@ -581,8 +589,9 @@ + (NSArray *)routes + (id)handleWheelSelect:(FBRouteRequest *)request { FBElementCache *elementCache = request.session.elementCache; - XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; - if ([element.lastSnapshot elementType] != XCUIElementTypePickerWheel) { + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"] + checkStaleness:YES]; + if ([element elementType] != XCUIElementTypePickerWheel) { NSString *errMsg = [NSString stringWithFormat:@"The element is expected to be a valid Picker Wheel control. '%@' was given instead", element.wdType]; return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:errMsg traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]); @@ -688,7 +697,7 @@ + (XCUIElement *)targetFromRequest:(FBRouteRequest *)request NSString *elementUuid = (NSString *)request.parameters[@"uuid"]; return nil == elementUuid ? request.session.activeApplication - : [elementCache elementForUUID:elementUuid]; + : [elementCache elementForUUID:elementUuid checkStaleness:YES]; } #endif diff --git a/WebDriverAgent/WebDriverAgentLib/Commands/FBFindElementCommands.h b/WebDriverAgent/WebDriverAgentLib/Commands/FBFindElementCommands.h index 9c20b0d459..7349e10c9b 100644 --- a/WebDriverAgent/WebDriverAgentLib/Commands/FBFindElementCommands.h +++ b/WebDriverAgent/WebDriverAgentLib/Commands/FBFindElementCommands.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Commands/FBFindElementCommands.m b/WebDriverAgent/WebDriverAgentLib/Commands/FBFindElementCommands.m index ba30a71707..c370f11cef 100644 --- a/WebDriverAgent/WebDriverAgentLib/Commands/FBFindElementCommands.m +++ b/WebDriverAgent/WebDriverAgentLib/Commands/FBFindElementCommands.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBFindElementCommands.h" @@ -80,15 +79,13 @@ + (NSArray *)routes + (id)handleFindVisibleCells:(FBRouteRequest *)request { FBElementCache *elementCache = request.session.elementCache; - XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"] - resolveForAdditionalAttributes:@[FB_XCAXAIsVisibleAttributeName] - andMaxDepth:nil]; - NSArray> *visibleCellSnapshots = [element.lastSnapshot descendantsByFilteringWithBlock:^BOOL(id snapshot) { - return snapshot.elementType == XCUIElementTypeCell - && [FBXCElementSnapshotWrapper ensureWrapped:snapshot].wdVisible; + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; + id snapshot = [element fb_customSnapshot]; + NSArray> *visibleCellSnapshots = [snapshot descendantsByFilteringWithBlock:^BOOL(id shot) { + return shot.elementType == XCUIElementTypeCell + && [FBXCElementSnapshotWrapper ensureWrapped:shot].wdVisible; }]; NSArray *cells = [element fb_filterDescendantsWithSnapshots:visibleCellSnapshots - selfUID:[FBXCElementSnapshotWrapper wdUIDWithSnapshot:element.lastSnapshot] onlyChildren:NO]; return FBResponseWithCachedElements(cells, request.session.elementCache, FBConfiguration.shouldUseCompactResponses); } @@ -96,7 +93,8 @@ + (NSArray *)routes + (id)handleFindSubElement:(FBRouteRequest *)request { FBElementCache *elementCache = request.session.elementCache; - XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"] + checkStaleness:NO]; XCUIElement *foundElement = [self.class elementUsing:request.arguments[@"using"] withValue:request.arguments[@"value"] under:element]; @@ -109,7 +107,8 @@ + (NSArray *)routes + (id)handleFindSubElements:(FBRouteRequest *)request { FBElementCache *elementCache = request.session.elementCache; - XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"] + checkStaleness:NO]; NSArray *foundElements = [self.class elementsUsing:request.arguments[@"using"] withValue:request.arguments[@"value"] under:element diff --git a/WebDriverAgent/WebDriverAgentLib/Commands/FBOrientationCommands.h b/WebDriverAgent/WebDriverAgentLib/Commands/FBOrientationCommands.h index 50bd5b6b11..1aaaacd63b 100644 --- a/WebDriverAgent/WebDriverAgentLib/Commands/FBOrientationCommands.h +++ b/WebDriverAgent/WebDriverAgentLib/Commands/FBOrientationCommands.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Commands/FBOrientationCommands.m b/WebDriverAgent/WebDriverAgentLib/Commands/FBOrientationCommands.m index aa4de26f7f..8e0bea4394 100644 --- a/WebDriverAgent/WebDriverAgentLib/Commands/FBOrientationCommands.m +++ b/WebDriverAgent/WebDriverAgentLib/Commands/FBOrientationCommands.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBOrientationCommands.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Commands/FBScreenshotCommands.h b/WebDriverAgent/WebDriverAgentLib/Commands/FBScreenshotCommands.h index ecb4a5eb85..3f4fa4a2ce 100644 --- a/WebDriverAgent/WebDriverAgentLib/Commands/FBScreenshotCommands.h +++ b/WebDriverAgent/WebDriverAgentLib/Commands/FBScreenshotCommands.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Commands/FBScreenshotCommands.m b/WebDriverAgent/WebDriverAgentLib/Commands/FBScreenshotCommands.m index 0b6e424274..71d6ba594e 100644 --- a/WebDriverAgent/WebDriverAgentLib/Commands/FBScreenshotCommands.m +++ b/WebDriverAgent/WebDriverAgentLib/Commands/FBScreenshotCommands.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBScreenshotCommands.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Commands/FBSessionCommands.h b/WebDriverAgent/WebDriverAgentLib/Commands/FBSessionCommands.h index 3f925aca8b..95f3f258fd 100644 --- a/WebDriverAgent/WebDriverAgentLib/Commands/FBSessionCommands.h +++ b/WebDriverAgent/WebDriverAgentLib/Commands/FBSessionCommands.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Commands/FBSessionCommands.m b/WebDriverAgent/WebDriverAgentLib/Commands/FBSessionCommands.m index 1972186d2c..1a9ddf417c 100644 --- a/WebDriverAgent/WebDriverAgentLib/Commands/FBSessionCommands.m +++ b/WebDriverAgent/WebDriverAgentLib/Commands/FBSessionCommands.m @@ -3,13 +3,13 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBSessionCommands.h" #import "FBCapabilities.h" +#import "FBClassChainQueryParser.h" #import "FBConfiguration.h" #import "FBExceptions.h" #import "FBLogger.h" @@ -336,7 +336,6 @@ + (NSArray *)routes FB_SETTING_SCREENSHOT_QUALITY: @([FBConfiguration screenshotQuality]), FB_SETTING_KEYBOARD_AUTOCORRECTION: @([FBConfiguration keyboardAutocorrection]), FB_SETTING_KEYBOARD_PREDICTION: @([FBConfiguration keyboardPrediction]), - FB_SETTING_CUSTOM_SNAPSHOT_TIMEOUT: @([FBConfiguration customSnapshotTimeout]), FB_SETTING_SNAPSHOT_MAX_DEPTH: @([FBConfiguration snapshotMaxDepth]), FB_SETTING_USE_FIRST_MATCH: @([FBConfiguration useFirstMatch]), FB_SETTING_WAIT_FOR_IDLE_TIMEOUT: @([FBConfiguration waitForIdleTimeout]), @@ -348,10 +347,15 @@ + (NSArray *)routes FB_SETTING_INCLUDE_NON_MODAL_ELEMENTS: @([FBConfiguration includeNonModalElements]), FB_SETTING_ACCEPT_ALERT_BUTTON_SELECTOR: FBConfiguration.acceptAlertButtonSelector, FB_SETTING_DISMISS_ALERT_BUTTON_SELECTOR: FBConfiguration.dismissAlertButtonSelector, + FB_SETTING_AUTO_CLICK_ALERT_SELECTOR: FBConfiguration.autoClickAlertSelector, FB_SETTING_DEFAULT_ALERT_ACTION: request.session.defaultAlertAction ?: @"", FB_SETTING_MAX_TYPING_FREQUENCY: @([FBConfiguration maxTypingFrequency]), FB_SETTING_RESPECT_SYSTEM_ALERTS: @([FBConfiguration shouldRespectSystemAlerts]), FB_SETTING_USE_CLEAR_TEXT_SHORTCUT: @([FBConfiguration useClearTextShortcut]), + FB_SETTING_INCLUDE_HITTABLE_IN_PAGE_SOURCE: @([FBConfiguration includeHittableInPageSource]), + FB_SETTING_INCLUDE_NATIVE_FRAME_IN_PAGE_SOURCE: @([FBConfiguration includeNativeFrameInPageSource]), + FB_SETTING_INCLUDE_MIN_MAX_VALUE_IN_PAGE_SOURCE: @([FBConfiguration includeMinMaxValueInPageSource]), + FB_SETTING_LIMIT_XPATH_CONTEXT_SCOPE: @([FBConfiguration limitXpathContextScope]), #if !TARGET_OS_TV FB_SETTING_SCREENSHOT_ORIENTATION: [FBConfiguration humanReadableScreenshotOrientation], #endif @@ -381,7 +385,7 @@ + (NSArray *)routes [FBConfiguration setScreenshotQuality:[[settings objectForKey:FB_SETTING_SCREENSHOT_QUALITY] unsignedIntegerValue]]; } if (nil != [settings objectForKey:FB_SETTING_MJPEG_SCALING_FACTOR]) { - [FBConfiguration setMjpegScalingFactor:[[settings objectForKey:FB_SETTING_MJPEG_SCALING_FACTOR] unsignedIntegerValue]]; + [FBConfiguration setMjpegScalingFactor:[[settings objectForKey:FB_SETTING_MJPEG_SCALING_FACTOR] floatValue]]; } if (nil != [settings objectForKey:FB_SETTING_MJPEG_FIX_ORIENTATION]) { [FBConfiguration setMjpegShouldFixOrientation:[[settings objectForKey:FB_SETTING_MJPEG_FIX_ORIENTATION] boolValue]]; @@ -395,13 +399,6 @@ + (NSArray *)routes if (nil != [settings objectForKey:FB_SETTING_RESPECT_SYSTEM_ALERTS]) { [FBConfiguration setShouldRespectSystemAlerts:[[settings objectForKey:FB_SETTING_RESPECT_SYSTEM_ALERTS] boolValue]]; } - // SNAPSHOT_TIMEOUT setting is deprecated. Please use CUSTOM_SNAPSHOT_TIMEOUT instead - if (nil != [settings objectForKey:FB_SETTING_SNAPSHOT_TIMEOUT]) { - [FBConfiguration setCustomSnapshotTimeout:[[settings objectForKey:FB_SETTING_SNAPSHOT_TIMEOUT] doubleValue]]; - } - if (nil != [settings objectForKey:FB_SETTING_CUSTOM_SNAPSHOT_TIMEOUT]) { - [FBConfiguration setCustomSnapshotTimeout:[[settings objectForKey:FB_SETTING_CUSTOM_SNAPSHOT_TIMEOUT] doubleValue]]; - } if (nil != [settings objectForKey:FB_SETTING_SNAPSHOT_MAX_DEPTH]) { [FBConfiguration setSnapshotMaxDepth:[[settings objectForKey:FB_SETTING_SNAPSHOT_MAX_DEPTH] intValue]]; } @@ -438,6 +435,13 @@ + (NSArray *)routes if (nil != [settings objectForKey:FB_SETTING_DISMISS_ALERT_BUTTON_SELECTOR]) { [FBConfiguration setDismissAlertButtonSelector:(NSString *)[settings objectForKey:FB_SETTING_DISMISS_ALERT_BUTTON_SELECTOR]]; } + if (nil != [settings objectForKey:FB_SETTING_AUTO_CLICK_ALERT_SELECTOR]) { + FBCommandStatus *status = [self.class configureAutoClickAlertWithSelector:settings[FB_SETTING_AUTO_CLICK_ALERT_SELECTOR] + forSession:request.session]; + if (status.hasError) { + return FBResponseWithStatus(status); + } + } if (nil != [settings objectForKey:FB_SETTING_WAIT_FOR_IDLE_TIMEOUT]) { [FBConfiguration setWaitForIdleTimeout:[[settings objectForKey:FB_SETTING_WAIT_FOR_IDLE_TIMEOUT] doubleValue]]; } @@ -453,6 +457,18 @@ + (NSArray *)routes if (nil != [settings objectForKey:FB_SETTING_USE_CLEAR_TEXT_SHORTCUT]) { [FBConfiguration setUseClearTextShortcut:[[settings objectForKey:FB_SETTING_USE_CLEAR_TEXT_SHORTCUT] boolValue]]; } + if (nil != [settings objectForKey:FB_SETTING_INCLUDE_HITTABLE_IN_PAGE_SOURCE]) { + [FBConfiguration setIncludeHittableInPageSource:[[settings objectForKey:FB_SETTING_INCLUDE_HITTABLE_IN_PAGE_SOURCE] boolValue]]; + } + if (nil != [settings objectForKey:FB_SETTING_INCLUDE_NATIVE_FRAME_IN_PAGE_SOURCE]) { + [FBConfiguration setIncludeNativeFrameInPageSource:[[settings objectForKey:FB_SETTING_INCLUDE_NATIVE_FRAME_IN_PAGE_SOURCE] boolValue]]; + } + if (nil != [settings objectForKey:FB_SETTING_INCLUDE_MIN_MAX_VALUE_IN_PAGE_SOURCE]) { + [FBConfiguration setIncludeMinMaxValueInPageSource:[[settings objectForKey:FB_SETTING_INCLUDE_MIN_MAX_VALUE_IN_PAGE_SOURCE] boolValue]]; + } + if (nil != [settings objectForKey:FB_SETTING_LIMIT_XPATH_CONTEXT_SCOPE]) { + [FBConfiguration setLimitXpathContextScope:[[settings objectForKey:FB_SETTING_LIMIT_XPATH_CONTEXT_SCOPE] boolValue]]; + } #if !TARGET_OS_TV if (nil != [settings objectForKey:FB_SETTING_SCREENSHOT_ORIENTATION]) { @@ -471,6 +487,26 @@ + (NSArray *)routes #pragma mark - Helpers ++ (FBCommandStatus *)configureAutoClickAlertWithSelector:(NSString *)selector + forSession:(FBSession *)session +{ + if (0 == [selector length]) { + [FBConfiguration setAutoClickAlertSelector:selector]; + [session disableAlertsMonitor]; + return [FBCommandStatus ok]; + } + + NSError *error; + FBClassChain *parsedChain = [FBClassChainQueryParser parseQuery:selector error:&error]; + if (nil == parsedChain) { + return [FBCommandStatus invalidSelectorErrorWithMessage:error.localizedDescription + traceback:nil]; + } + [FBConfiguration setAutoClickAlertSelector:selector]; + [session enableAlertsMonitor]; + return [FBCommandStatus ok]; +} + + (NSString *)buildTimestamp { return [NSString stringWithFormat:@"%@ %@", diff --git a/WebDriverAgent/WebDriverAgentLib/Commands/FBTouchActionCommands.h b/WebDriverAgent/WebDriverAgentLib/Commands/FBTouchActionCommands.h index b3a8e3f2aa..d9b84fc7d6 100644 --- a/WebDriverAgent/WebDriverAgentLib/Commands/FBTouchActionCommands.h +++ b/WebDriverAgent/WebDriverAgentLib/Commands/FBTouchActionCommands.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Commands/FBTouchActionCommands.m b/WebDriverAgent/WebDriverAgentLib/Commands/FBTouchActionCommands.m index c14edcb631..7788175e85 100644 --- a/WebDriverAgent/WebDriverAgentLib/Commands/FBTouchActionCommands.m +++ b/WebDriverAgent/WebDriverAgentLib/Commands/FBTouchActionCommands.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBTouchActionCommands.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Commands/FBTouchIDCommands.h b/WebDriverAgent/WebDriverAgentLib/Commands/FBTouchIDCommands.h index 6240a5f0ae..ffcf2e8126 100644 --- a/WebDriverAgent/WebDriverAgentLib/Commands/FBTouchIDCommands.h +++ b/WebDriverAgent/WebDriverAgentLib/Commands/FBTouchIDCommands.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Commands/FBTouchIDCommands.m b/WebDriverAgent/WebDriverAgentLib/Commands/FBTouchIDCommands.m index f965c1f080..9594dd6f5e 100644 --- a/WebDriverAgent/WebDriverAgentLib/Commands/FBTouchIDCommands.m +++ b/WebDriverAgent/WebDriverAgentLib/Commands/FBTouchIDCommands.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBTouchIDCommands.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Commands/FBUnknownCommands.h b/WebDriverAgent/WebDriverAgentLib/Commands/FBUnknownCommands.h index 54262cafb5..3e37d7894c 100644 --- a/WebDriverAgent/WebDriverAgentLib/Commands/FBUnknownCommands.h +++ b/WebDriverAgent/WebDriverAgentLib/Commands/FBUnknownCommands.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Commands/FBUnknownCommands.m b/WebDriverAgent/WebDriverAgentLib/Commands/FBUnknownCommands.m index ca27e9a757..7fc35b96b6 100644 --- a/WebDriverAgent/WebDriverAgentLib/Commands/FBUnknownCommands.m +++ b/WebDriverAgent/WebDriverAgentLib/Commands/FBUnknownCommands.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBUnknownCommands.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Commands/FBVideoCommands.h b/WebDriverAgent/WebDriverAgentLib/Commands/FBVideoCommands.h index b2e3eb795c..a3e7a0a650 100644 --- a/WebDriverAgent/WebDriverAgentLib/Commands/FBVideoCommands.h +++ b/WebDriverAgent/WebDriverAgentLib/Commands/FBVideoCommands.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Commands/FBVideoCommands.m b/WebDriverAgent/WebDriverAgentLib/Commands/FBVideoCommands.m index 8366c3a796..a5c36a564e 100644 --- a/WebDriverAgent/WebDriverAgentLib/Commands/FBVideoCommands.m +++ b/WebDriverAgent/WebDriverAgentLib/Commands/FBVideoCommands.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBVideoCommands.h" diff --git a/WebDriverAgent/WebDriverAgentLib/FBAlert.h b/WebDriverAgent/WebDriverAgentLib/FBAlert.h index 22f28a710a..8e9ec8cda5 100644 --- a/WebDriverAgent/WebDriverAgentLib/FBAlert.h +++ b/WebDriverAgent/WebDriverAgentLib/FBAlert.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/FBAlert.m b/WebDriverAgent/WebDriverAgentLib/FBAlert.m index b06e267888..2e2de763d8 100644 --- a/WebDriverAgent/WebDriverAgentLib/FBAlert.m +++ b/WebDriverAgent/WebDriverAgentLib/FBAlert.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBAlert.h" @@ -50,7 +49,7 @@ - (BOOL)isPresent if (nil == self.alertElement) { return NO; } - [self.alertElement fb_takeSnapshot]; + [self.alertElement fb_customSnapshot]; return YES; } @catch (NSException *) { return NO; @@ -82,7 +81,7 @@ - (NSString *)text } NSMutableArray *resultText = [NSMutableArray array]; - id snapshot = self.alertElement.lastSnapshot; + id snapshot = self.alertElement.lastSnapshot ?: [self.alertElement fb_customSnapshot]; BOOL isSafariAlert = [self.class isSafariWebAlertWithSnapshot:snapshot]; [snapshot enumerateDescendantsUsingBlock:^(id descendant) { XCUIElementType elementType = descendant.elementType; @@ -145,7 +144,8 @@ - (NSArray *)buttonLabels } NSMutableArray *labels = [NSMutableArray array]; - [self.alertElement.lastSnapshot enumerateDescendantsUsingBlock:^(id descendant) { + id alertSnapshot = self.alertElement.lastSnapshot ?: [self.alertElement fb_customSnapshot]; + [alertSnapshot enumerateDescendantsUsingBlock:^(id descendant) { if (descendant.elementType != XCUIElementTypeButton) { return; } @@ -163,7 +163,7 @@ - (BOOL)acceptWithError:(NSError **)error return [self notPresentWithError:error]; } - id alertSnapshot = self.alertElement.lastSnapshot; + id alertSnapshot = self.alertElement.lastSnapshot ?: [self.alertElement fb_customSnapshot]; XCUIElement *acceptButton = nil; if (FBConfiguration.acceptAlertButtonSelector.length) { NSString *errorReason = nil; @@ -204,7 +204,7 @@ - (BOOL)dismissWithError:(NSError **)error return [self notPresentWithError:error]; } - id alertSnapshot = self.alertElement.lastSnapshot; + id alertSnapshot = self.alertElement.lastSnapshot ?: [self.alertElement fb_customSnapshot]; XCUIElement *dismissButton = nil; if (FBConfiguration.dismissAlertButtonSelector.length) { NSString *errorReason = nil; diff --git a/WebDriverAgent/WebDriverAgentLib/Info.plist b/WebDriverAgent/WebDriverAgentLib/Info.plist index 4dd4120c63..4b6601c1a4 100644 --- a/WebDriverAgent/WebDriverAgentLib/Info.plist +++ b/WebDriverAgent/WebDriverAgentLib/Info.plist @@ -1,26 +1,26 @@ - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - FMWK - CFBundleShortVersionString - 8.11.1 - CFBundleSignature - ???? - CFBundleVersion - 8.11.1 - NSPrincipalClass - - + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 10.4.2 + CFBundleSignature + ???? + CFBundleVersion + 10.4.2 + NSPrincipalClass + + diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBCommandHandler.h b/WebDriverAgent/WebDriverAgentLib/Routing/FBCommandHandler.h index bde028d68b..c4ac83b714 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBCommandHandler.h +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBCommandHandler.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBCommandStatus.h b/WebDriverAgent/WebDriverAgentLib/Routing/FBCommandStatus.h index 82bee1f606..0929ff10ce 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBCommandStatus.h +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBCommandStatus.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import @@ -19,7 +18,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, nullable, readonly) NSString* message; @property (nonatomic, nullable, readonly) NSString* traceback; @property (nonatomic, readonly) HTTPStatusCode statusCode; - +@property (nonatomic, readonly) BOOL hasError; + (instancetype)ok; diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBCommandStatus.m b/WebDriverAgent/WebDriverAgentLib/Routing/FBCommandStatus.m index 453fb6192c..fb5d2439e7 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBCommandStatus.m +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBCommandStatus.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBCommandStatus.h" @@ -109,6 +108,11 @@ - (instancetype)initWithError:(NSString *)error return self; } +- (BOOL)hasError +{ + return self.statusCode != kHTTPStatusCodeOK; +} + + (instancetype)ok { return [[FBCommandStatus alloc] initWithValue:nil]; diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBElement.h b/WebDriverAgent/WebDriverAgentLib/Routing/FBElement.h index 7cc8f269c2..b4e6a75224 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBElement.h +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBElement.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import @@ -20,6 +19,9 @@ NS_ASSUME_NONNULL_BEGIN /*! Element's frame in normalized (rounded dimensions without Infinity values) CGRect format */ @property (nonatomic, readonly, assign) CGRect wdFrame; +/*! Represents the element's frame as a CGRect, preserving the actual values. */ +@property (nonatomic, readonly, assign) CGRect wdNativeFrame; + /*! Element's wsFrame in NSDictionary format */ @property (nonatomic, readonly, copy) NSDictionary *wdRect; @@ -35,6 +37,9 @@ NS_ASSUME_NONNULL_BEGIN /*! Element's type */ @property (nonatomic, readonly, copy) NSString *wdType; +/*! Element's accessibility traits as a comma-separated string */ +@property (nonatomic, readonly, copy) NSString *wdTraits; + /*! Element's value */ @property (nonatomic, readonly, strong, nullable) NSString *wdValue; @@ -62,6 +67,15 @@ NS_ASSUME_NONNULL_BEGIN /*! Element's index relatively to its parent. Starts from zero */ @property (nonatomic, readonly) NSUInteger wdIndex; +/*! Element's placeholder value */ +@property (nonatomic, readonly, copy, nullable) NSString *wdPlaceholderValue; + +/*! Element's minimum value */ +@property (nonatomic, readonly, strong, nullable) NSNumber *wdMinValue; + +/*! Element's maximum value */ +@property (nonatomic, readonly, strong, nullable) NSNumber *wdMaxValue; + /** Returns value of given property specified in WebDriver Spec Check the FBElement protocol to get list of supported attributes. diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBElementCache.h b/WebDriverAgent/WebDriverAgentLib/Routing/FBElementCache.h index 13100eeb2c..1c7eb47642 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBElementCache.h +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBElementCache.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import @@ -35,26 +34,20 @@ extern const int ELEMENT_CACHE_SIZE; @param uuid uuid of element to fetch @return element - @throws FBStaleElementException if the found element is not present in DOM anymore @throws FBInvalidArgumentException if uuid is nil */ - (XCUIElement *)elementForUUID:(NSString *)uuid; /** - Returns cached element + Returns cached element resolved with default snapshot attributes @param uuid uuid of element to fetch - @param additionalAttributes Add additonal attribute names if the snapshot should contain - them in `addtionalAttributes` section. nil value resolves the snapshot with standard attributes. - @param maxDepth The maximum depth of the snapshot. Only works if additional attributes are provided. - `nil` value means to use the default maximum depth value. + @param checkStaleness Whether to throw FBStaleElementException if the found element is not present in DOM anymore @return element - @throws FBStaleElementException if the found element is not present in DOM anymore + @throws FBStaleElementException if `checkStaleness` is enabled @throws FBInvalidArgumentException if uuid is nil */ -- (XCUIElement *)elementForUUID:(NSString *)uuid - resolveForAdditionalAttributes:(nullable NSArray *)additionalAttributes - andMaxDepth:(nullable NSNumber *)maxDepth; +- (XCUIElement *)elementForUUID:(NSString *)uuid checkStaleness:(BOOL)checkStaleness; /** Checks element existence in the cache diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBElementCache.m b/WebDriverAgent/WebDriverAgentLib/Routing/FBElementCache.m index 1f069bec65..756624246a 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBElementCache.m +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBElementCache.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBElementCache.h" @@ -26,7 +25,6 @@ @interface FBElementCache () @property (nonatomic, strong) LRUCache *elementCache; -@property (nonatomic) BOOL elementsNeedReset; @end @implementation FBElementCache @@ -38,7 +36,6 @@ - (instancetype)init return nil; } _elementCache = [[LRUCache alloc] initWithCapacity:ELEMENT_CACHE_SIZE]; - _elementsNeedReset = NO; return self; } @@ -50,19 +47,16 @@ - (NSString *)storeElement:(XCUIElement *)element } @synchronized (self.elementCache) { [self.elementCache setObject:element forKey:uuid]; - self.elementsNeedReset = YES; } return uuid; } - (XCUIElement *)elementForUUID:(NSString *)uuid { - return [self elementForUUID:uuid resolveForAdditionalAttributes:nil andMaxDepth:nil]; + return [self elementForUUID:uuid checkStaleness:NO]; } -- (XCUIElement *)elementForUUID:(NSString *)uuid - resolveForAdditionalAttributes:(NSArray *)additionalAttributes - andMaxDepth:(NSNumber *)maxDepth +- (XCUIElement *)elementForUUID:(NSString *)uuid checkStaleness:(BOOL)checkStaleness { if (!uuid) { NSString *reason = [NSString stringWithFormat:@"Cannot extract cached element for UUID: %@", uuid]; @@ -71,23 +65,25 @@ - (XCUIElement *)elementForUUID:(NSString *)uuid XCUIElement *element; @synchronized (self.elementCache) { - [self resetElements]; element = [self.elementCache objectForKey:uuid]; } if (nil == element) { NSString *reason = [NSString stringWithFormat:@"The element identified by \"%@\" is either not present or it has expired from the internal cache. Try to find it again", uuid]; @throw [NSException exceptionWithName:FBStaleElementException reason:reason userInfo:@{}]; } - // This will throw FBStaleElementException exception if the element is stale - // or resolve the element and set lastSnapshot property - if (nil == additionalAttributes) { - [element fb_takeSnapshot]; - } else { - NSMutableArray *attributes = [NSMutableArray arrayWithArray:FBStandardAttributeNames()]; - [attributes addObjectsFromArray:additionalAttributes]; - [element fb_snapshotWithAttributes:attributes.copy maxDepth:maxDepth]; + if (checkStaleness) { + @try { + [element fb_standardSnapshot]; + } @catch (NSException *exception) { + // if the snapshot method threw FBStaleElementException (implying the element is stale) we need to explicitly remove it from the cache, PR: https://github.com/appium/WebDriverAgent/pull/985 + if ([exception.name isEqualToString:FBStaleElementException]) { + @synchronized (self.elementCache) { + [self.elementCache removeObjectForKey:uuid]; + } + } + @throw exception; + } } - element.fb_isResolvedFromCache = @(YES); return element; } @@ -101,20 +97,4 @@ - (BOOL)hasElementWithUUID:(NSString *)uuid } } -- (void)resetElements -{ - if (!self.elementsNeedReset) { - return; - } - - for (XCUIElement *element in self.elementCache.allObjects) { - element.lastSnapshot = nil; - if (nil != element.query) { - element.query.rootElementSnapshot = nil; - } - element.fb_isResolvedFromCache = @(NO); - } - self.elementsNeedReset = NO; -} - @end diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBElementUtils.h b/WebDriverAgent/WebDriverAgentLib/Routing/FBElementUtils.h index a0b6da8394..0c4de4b7f1 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBElementUtils.h +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBElementUtils.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBElementUtils.m b/WebDriverAgent/WebDriverAgentLib/Routing/FBElementUtils.m index 8aec9d8a85..cc89d2fc7c 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBElementUtils.m +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBElementUtils.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBExceptionHandler.h b/WebDriverAgent/WebDriverAgentLib/Routing/FBExceptionHandler.h index 693e5c4078..cc1dc0f221 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBExceptionHandler.h +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBExceptionHandler.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBExceptionHandler.m b/WebDriverAgent/WebDriverAgentLib/Routing/FBExceptionHandler.m index 811a0efb50..b5e1ec0606 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBExceptionHandler.m +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBExceptionHandler.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBExceptionHandler.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBExceptions.h b/WebDriverAgent/WebDriverAgentLib/Routing/FBExceptions.h index 13fda9f4ce..b802da673b 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBExceptions.h +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBExceptions.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBExceptions.m b/WebDriverAgent/WebDriverAgentLib/Routing/FBExceptions.m index 93c1837e75..feb8a4d50c 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBExceptions.m +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBExceptions.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBExceptions.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBResponseJSONPayload.h b/WebDriverAgent/WebDriverAgentLib/Routing/FBResponseJSONPayload.h index 14f6c1c84b..8c863e9992 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBResponseJSONPayload.h +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBResponseJSONPayload.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBResponseJSONPayload.m b/WebDriverAgent/WebDriverAgentLib/Routing/FBResponseJSONPayload.m index 4ad1c37ad1..8782b8e9e2 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBResponseJSONPayload.m +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBResponseJSONPayload.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBResponseJSONPayload.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBResponsePayload.h b/WebDriverAgent/WebDriverAgentLib/Routing/FBResponsePayload.h index 9ff7a9308f..b29b8f98a4 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBResponsePayload.h +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBResponsePayload.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBResponsePayload.m b/WebDriverAgent/WebDriverAgentLib/Routing/FBResponsePayload.m index 08fcd63fed..809cd41e8b 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBResponsePayload.m +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBResponsePayload.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBResponsePayload.h" @@ -34,24 +33,41 @@ return FBResponseWithStatus([FBCommandStatus okWithValue:object]); } -id FBResponseWithCachedElement(XCUIElement *element, FBElementCache *elementCache, BOOL compact) +XCUIElement *maybeStable(XCUIElement *element) { BOOL useNativeCachingStrategy = nil == FBSession.activeSession ? YES : FBSession.activeSession.useNativeCachingStrategy; - [elementCache storeElement:(useNativeCachingStrategy ? element : element.fb_stableInstance)]; - return FBResponseWithStatus([FBCommandStatus okWithValue:FBDictionaryResponseWithElement(element, compact)]); + if (useNativeCachingStrategy) { + return element; + } + + XCUIElement *result = element; + id snapshot = element.lastSnapshot + ?: element.fb_cachedSnapshot + ?: [element fb_standardSnapshot]; + NSString *uid = [FBXCElementSnapshotWrapper wdUIDWithSnapshot:snapshot]; + if (nil != uid) { + result = [element fb_stableInstanceWithUid:uid]; + } + return result; +} + +id FBResponseWithCachedElement(XCUIElement *element, FBElementCache *elementCache, BOOL compact) +{ + [elementCache storeElement:maybeStable(element)]; + NSDictionary *response = FBDictionaryResponseWithElement(element, compact); + element.lastSnapshot = nil; + return FBResponseWithStatus([FBCommandStatus okWithValue:response]); } id FBResponseWithCachedElements(NSArray *elements, FBElementCache *elementCache, BOOL compact) { NSMutableArray *elementsResponse = [NSMutableArray array]; - BOOL useNativeCachingStrategy = nil == FBSession.activeSession - ? YES - : FBSession.activeSession.useNativeCachingStrategy; for (XCUIElement *element in elements) { - [elementCache storeElement:(useNativeCachingStrategy ? element : element.fb_stableInstance)]; + [elementCache storeElement:maybeStable(element)]; [elementsResponse addObject:FBDictionaryResponseWithElement(element, compact)]; + element.lastSnapshot = nil; } return FBResponseWithStatus([FBCommandStatus okWithValue:elementsResponse]); } @@ -91,41 +107,42 @@ inline NSDictionary *FBDictionaryResponseWithElement(XCUIElement *element, BOOL compact) { - id snapshot = nil; - if (nil != element.query.rootElementSnapshot) { - snapshot = element.fb_cachedSnapshot; - } - if (nil == snapshot) { - snapshot = element.lastSnapshot ?: element.fb_takeSnapshot; - } - NSDictionary *compactResult = FBToElementDict((NSString *)[FBXCElementSnapshotWrapper wdUIDWithSnapshot:snapshot]); - if (compact) { - return compactResult; - } + __block NSDictionary *elementResponse = nil; + @autoreleasepool { + id snapshot = element.lastSnapshot + ?: element.fb_cachedSnapshot + ?: [element fb_customSnapshot]; + NSDictionary *compactResult = FBToElementDict((NSString *)[FBXCElementSnapshotWrapper wdUIDWithSnapshot:snapshot]); + if (compact) { + elementResponse = compactResult; + return elementResponse; + } - NSMutableDictionary *result = compactResult.mutableCopy; - FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:snapshot]; - NSArray *fields = [FBConfiguration.elementResponseAttributes componentsSeparatedByString:@","]; - for (NSString *field in fields) { - // 'name' here is the w3c-approved identifier for what we mean by 'type' - if ([field isEqualToString:@"name"] || [field isEqualToString:@"type"]) { - result[field] = wrappedSnapshot.wdType; - } else if ([field isEqualToString:@"text"]) { - result[field] = FBFirstNonEmptyValue(wrappedSnapshot.wdValue, wrappedSnapshot.wdLabel) ?: [NSNull null]; - } else if ([field isEqualToString:@"rect"]) { - result[field] = wrappedSnapshot.wdRect; - } else if ([field isEqualToString:@"enabled"]) { - result[field] = @(wrappedSnapshot.wdEnabled); - } else if ([field isEqualToString:@"displayed"]) { - result[field] = @(wrappedSnapshot.wdVisible); - } else if ([field isEqualToString:@"selected"]) { - result[field] = @(wrappedSnapshot.wdSelected); - } else if ([field isEqualToString:@"label"]) { - result[field] = wrappedSnapshot.wdLabel ?: [NSNull null]; - } else if ([field hasPrefix:arbitraryAttrPrefix]) { - NSString *attributeName = [field substringFromIndex:[arbitraryAttrPrefix length]]; - result[field] = [wrappedSnapshot fb_valueForWDAttributeName:attributeName] ?: [NSNull null]; + NSMutableDictionary *result = compactResult.mutableCopy; + FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:snapshot]; + NSArray *fields = [FBConfiguration.elementResponseAttributes componentsSeparatedByString:@","]; + for (NSString *field in fields) { + // 'name' here is the w3c-approved identifier for what we mean by 'type' + if ([field isEqualToString:@"name"] || [field isEqualToString:@"type"]) { + result[field] = wrappedSnapshot.wdType; + } else if ([field isEqualToString:@"text"]) { + result[field] = FBFirstNonEmptyValue(wrappedSnapshot.wdValue, wrappedSnapshot.wdLabel) ?: [NSNull null]; + } else if ([field isEqualToString:@"rect"]) { + result[field] = wrappedSnapshot.wdRect; + } else if ([field isEqualToString:@"enabled"]) { + result[field] = @(wrappedSnapshot.wdEnabled); + } else if ([field isEqualToString:@"displayed"]) { + result[field] = @(wrappedSnapshot.wdVisible); + } else if ([field isEqualToString:@"selected"]) { + result[field] = @(wrappedSnapshot.wdSelected); + } else if ([field isEqualToString:@"label"]) { + result[field] = wrappedSnapshot.wdLabel ?: [NSNull null]; + } else if ([field hasPrefix:arbitraryAttrPrefix]) { + NSString *attributeName = [field substringFromIndex:[arbitraryAttrPrefix length]]; + result[field] = [wrappedSnapshot fb_valueForWDAttributeName:attributeName] ?: [NSNull null]; + } } + elementResponse = result.copy; } - return result.copy; + return elementResponse; } diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBRoute.h b/WebDriverAgent/WebDriverAgentLib/Routing/FBRoute.h index 2d3138686c..fce8dd8a98 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBRoute.h +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBRoute.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBRoute.m b/WebDriverAgent/WebDriverAgentLib/Routing/FBRoute.m index 5d740de7e3..fbe69b8c3c 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBRoute.m +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBRoute.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBRoute.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBRouteRequest-Private.h b/WebDriverAgent/WebDriverAgentLib/Routing/FBRouteRequest-Private.h index d5bd31f10d..7144bd80ba 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBRouteRequest-Private.h +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBRouteRequest-Private.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBRouteRequest.h b/WebDriverAgent/WebDriverAgentLib/Routing/FBRouteRequest.h index 7647cd823f..c7938d5d67 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBRouteRequest.h +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBRouteRequest.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBRouteRequest.m b/WebDriverAgent/WebDriverAgentLib/Routing/FBRouteRequest.m index e7d2743f23..b8656d2852 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBRouteRequest.m +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBRouteRequest.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBRouteRequest-Private.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBScreenRecordingContainer.h b/WebDriverAgent/WebDriverAgentLib/Routing/FBScreenRecordingContainer.h index 48e5c7481a..ce655dd7c0 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBScreenRecordingContainer.h +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBScreenRecordingContainer.h @@ -4,8 +4,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBScreenRecordingContainer.m b/WebDriverAgent/WebDriverAgentLib/Routing/FBScreenRecordingContainer.m index b0a744dc0a..608d7bfdfc 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBScreenRecordingContainer.m +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBScreenRecordingContainer.m @@ -4,8 +4,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBScreenRecordingContainer.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBScreenRecordingPromise.h b/WebDriverAgent/WebDriverAgentLib/Routing/FBScreenRecordingPromise.h index a869187617..6b3fcb3c6d 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBScreenRecordingPromise.h +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBScreenRecordingPromise.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBScreenRecordingPromise.m b/WebDriverAgent/WebDriverAgentLib/Routing/FBScreenRecordingPromise.m index 9de9dbaf11..e1193a1335 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBScreenRecordingPromise.m +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBScreenRecordingPromise.m @@ -4,8 +4,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBScreenRecordingPromise.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBScreenRecordingRequest.h b/WebDriverAgent/WebDriverAgentLib/Routing/FBScreenRecordingRequest.h index 5e24e5588d..f0a61ef247 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBScreenRecordingRequest.h +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBScreenRecordingRequest.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBScreenRecordingRequest.m b/WebDriverAgent/WebDriverAgentLib/Routing/FBScreenRecordingRequest.m index 5249b76b03..32c5b05796 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBScreenRecordingRequest.m +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBScreenRecordingRequest.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBScreenRecordingRequest.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBSession-Private.h b/WebDriverAgent/WebDriverAgentLib/Routing/FBSession-Private.h index 45d5a8c3c9..792fcd7384 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBSession-Private.h +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBSession-Private.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBSession.h b/WebDriverAgent/WebDriverAgentLib/Routing/FBSession.h index b617ae0960..61b1f87429 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBSession.h +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBSession.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import @@ -121,6 +120,22 @@ extern NSString* const FB_SAFARI_BUNDLE_ID; */ - (NSUInteger)applicationStateWithBundleId:(NSString *)bundleIdentifier; +/** + Allows to enable automated session alerts monitoring. + Repeated calls are ignored if alerts monitoring has been already enabled. + + @returns YES if the actual alerts monitoring state has been changed + */ +- (BOOL)enableAlertsMonitor; + +/** + Allows to disable automated alerts monitoring + Repeated calls are ignored if alerts monitoring has been already disabled. + + @returns YES if the actual alerts monitoring state has been changed + */ +- (BOOL)disableAlertsMonitor; + @end NS_ASSUME_NONNULL_END diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBSession.m b/WebDriverAgent/WebDriverAgentLib/Routing/FBSession.m index a4460b4a88..b8de5edf6a 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBSession.m +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBSession.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBSession.h" @@ -25,6 +24,7 @@ #import "FBXCTestDaemonsProxy.h" #import "XCUIApplication+FBQuiescence.h" #import "XCUIElement.h" +#import "XCUIElement+FBClassChain.h" /*! The intial value for the default application property. @@ -53,6 +53,22 @@ @implementation FBSession (FBAlertsMonitorDelegate) - (void)didDetectAlert:(FBAlert *)alert { + NSString *autoClickAlertSelector = FBConfiguration.autoClickAlertSelector; + if ([autoClickAlertSelector length] > 0) { + @try { + NSArray *matches = [alert.alertElement fb_descendantsMatchingClassChain:autoClickAlertSelector + shouldReturnAfterFirstMatch:YES]; + if (matches.count > 0) { + [[matches objectAtIndex:0] tap]; + } + } @catch (NSException *e) { + [FBLogger logFmt:@"Could not click at the alert element '%@'. Original error: %@", + autoClickAlertSelector, e.description]; + } + // This setting has priority over other settings if enabled + return; + } + if (nil == self.defaultAlertAction || 0 == self.defaultAlertAction.length) { return; } @@ -125,23 +141,41 @@ + (instancetype)initWithApplication:(nullable XCUIApplication *)application defaultAlertAction:(NSString *)defaultAlertAction { FBSession *session = [self.class initWithApplication:application]; - session.alertsMonitor = [[FBAlertsMonitor alloc] init]; - session.alertsMonitor.delegate = (id)session; session.defaultAlertAction = [defaultAlertAction lowercaseString]; - [session.alertsMonitor enable]; + [session enableAlertsMonitor]; return session; } +- (BOOL)enableAlertsMonitor +{ + if (nil != self.alertsMonitor) { + return NO; + } + + self.alertsMonitor = [[FBAlertsMonitor alloc] init]; + self.alertsMonitor.delegate = (id)self; + [self.alertsMonitor enable]; + return YES; +} + +- (BOOL)disableAlertsMonitor +{ + if (nil == self.alertsMonitor) { + return NO; + } + + [self.alertsMonitor disable]; + self.alertsMonitor = nil; + return YES; +} + - (void)kill { if (nil == _activeSession) { return; } - if (nil != self.alertsMonitor) { - [self.alertsMonitor disable]; - self.alertsMonitor = nil; - } + [self disableAlertsMonitor]; FBScreenRecordingPromise *activeScreenRecording = FBScreenRecordingContainer.sharedInstance.screenRecordingPromise; if (nil != activeScreenRecording) { @@ -178,10 +212,13 @@ - (XCUIApplication *)activeApplication if (nil != self.testedApplication) { XCUIApplicationState testedAppState = self.testedApplication.state; if (testedAppState >= XCUIApplicationStateRunningForeground) { - // We look for `SBTransientOverlayWindow` elements for half modals. See https://github.com/appium/WebDriverAgent/pull/946 - NSPredicate *searchPredicate = [NSPredicate predicateWithFormat:@"%K == %@ OR %K == %@", + NSPredicate *searchPredicate = [NSPredicate predicateWithFormat:@"%K == %@ OR %K IN {%@, %@}", @"elementType", @(XCUIElementTypeAlert), - @"identifier", @"SBTransientOverlayWindow"]; + // To look for `SBTransientOverlayWindow` elements. See https://github.com/appium/WebDriverAgent/pull/946 + @"identifier", @"SBTransientOverlayWindow", + // To look for 'criticalAlertSetting' elements https://developer.apple.com/documentation/usernotifications/unnotificationsettings/criticalalertsetting + // See https://github.com/appium/appium/issues/20835 + @"NotificationShortLookView"]; if ([FBConfiguration shouldRespectSystemAlerts] && [[XCUIApplication.fb_systemApplication descendantsMatchingType:XCUIElementTypeAny] matchingPredicate:searchPredicate].count > 0) { diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBTCPSocket.h b/WebDriverAgent/WebDriverAgentLib/Routing/FBTCPSocket.h index 895ae75f54..31adc7e22f 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBTCPSocket.h +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBTCPSocket.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "GCDAsyncSocket.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBTCPSocket.m b/WebDriverAgent/WebDriverAgentLib/Routing/FBTCPSocket.m index 527acdf4aa..fabd22074b 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBTCPSocket.m +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBTCPSocket.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBTCPSocket.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBWebServer.h b/WebDriverAgent/WebDriverAgentLib/Routing/FBWebServer.h index a857a10c95..34bca6515d 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBWebServer.h +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBWebServer.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBWebServer.m b/WebDriverAgent/WebDriverAgentLib/Routing/FBWebServer.m index ff9e4c424b..0b82eaff58 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBWebServer.m +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBWebServer.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBWebServer.h" @@ -93,6 +92,12 @@ - (void)startHTTPServer [self registerServerKeyRouteHandlers]; NSRange serverPortRange = FBConfiguration.bindingPortRange; + NSString *bindingIP = FBConfiguration.bindingIPAddress; + if (bindingIP != nil) { + [self.server setInterface:bindingIP]; + [FBLogger logFmt:@"Using custom binding IP address: %@", bindingIP]; + } + NSError *error; BOOL serverStarted = NO; @@ -112,7 +117,9 @@ - (void)startHTTPServer [FBLogger logFmt:@"Last attempt to start web server failed with error %@", [error description]]; abort(); } - [FBLogger logFmt:@"%@http://%@:%d%@", FBServerURLBeginMarker, [XCUIDevice sharedDevice].fb_wifiIPAddress ?: @"localhost", [self.server port], FBServerURLEndMarker]; + + NSString *serverHost = bindingIP ?: ([XCUIDevice sharedDevice].fb_wifiIPAddress ?: @"127.0.0.1"); + [FBLogger logFmt:@"%@http://%@:%d%@", FBServerURLBeginMarker, serverHost, [self.server port], FBServerURLEndMarker]; } - (void)initScreenshotsBroadcaster @@ -142,7 +149,7 @@ - (void)readMjpegSettingsFromEnv NSDictionary *env = NSProcessInfo.processInfo.environment; NSString *scalingFactor = [env objectForKey:@"MJPEG_SCALING_FACTOR"]; if (scalingFactor != nil && [scalingFactor length] > 0) { - [FBConfiguration setMjpegScalingFactor:[scalingFactor integerValue]]; + [FBConfiguration setMjpegScalingFactor:[scalingFactor floatValue]]; } NSString *screenshotQuality = [env objectForKey:@"MJPEG_SERVER_SCREENSHOT_QUALITY"]; if (screenshotQuality != nil && [screenshotQuality length] > 0) { diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBXCAccessibilityElement.h b/WebDriverAgent/WebDriverAgentLib/Routing/FBXCAccessibilityElement.h index 701edcd8db..dd35a37855 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBXCAccessibilityElement.h +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBXCAccessibilityElement.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBXCAccessibilityElement.m b/WebDriverAgent/WebDriverAgentLib/Routing/FBXCAccessibilityElement.m index 4535be6761..39f22913cf 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBXCAccessibilityElement.m +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBXCAccessibilityElement.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBXCAccessibilityElement.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBXCDeviceEvent.h b/WebDriverAgent/WebDriverAgentLib/Routing/FBXCDeviceEvent.h index b27b667e0f..64139f9f64 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBXCDeviceEvent.h +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBXCDeviceEvent.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBXCDeviceEvent.m b/WebDriverAgent/WebDriverAgentLib/Routing/FBXCDeviceEvent.m index 8f2c9609b3..6a673e1518 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBXCDeviceEvent.m +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBXCDeviceEvent.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBXCDeviceEvent.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBXCElementSnapshot.h b/WebDriverAgent/WebDriverAgentLib/Routing/FBXCElementSnapshot.h index a3076ec25c..500d9a8463 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBXCElementSnapshot.h +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBXCElementSnapshot.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBXCElementSnapshot.m b/WebDriverAgent/WebDriverAgentLib/Routing/FBXCElementSnapshot.m index 11d106890c..5fe6020488 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBXCElementSnapshot.m +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBXCElementSnapshot.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBXCElementSnapshot.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBXCElementSnapshotWrapper.h b/WebDriverAgent/WebDriverAgentLib/Routing/FBXCElementSnapshotWrapper.h index e9b763143c..2309d6b580 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBXCElementSnapshotWrapper.h +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBXCElementSnapshotWrapper.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBXCElementSnapshot.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Routing/FBXCElementSnapshotWrapper.m b/WebDriverAgent/WebDriverAgentLib/Routing/FBXCElementSnapshotWrapper.m index 1beb87bb29..4b3e0e7f5e 100644 --- a/WebDriverAgent/WebDriverAgentLib/Routing/FBXCElementSnapshotWrapper.m +++ b/WebDriverAgent/WebDriverAgentLib/Routing/FBXCElementSnapshotWrapper.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBXCElementSnapshotWrapper.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBAccessibilityTraits.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBAccessibilityTraits.h new file mode 100644 index 0000000000..b5ffa5998f --- /dev/null +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBAccessibilityTraits.h @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + Converts accessibility traits bitmask to an array of string representations + @param traits The accessibility traits bitmask + @return Array of strings representing the accessibility traits + */ +NSArray *FBAccessibilityTraitsToStringsArray(unsigned long long traits); + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBAccessibilityTraits.m b/WebDriverAgent/WebDriverAgentLib/Utilities/FBAccessibilityTraits.m new file mode 100644 index 0000000000..74ce9ec7ce --- /dev/null +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBAccessibilityTraits.m @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBAccessibilityTraits.h" + +NSArray *FBAccessibilityTraitsToStringsArray(unsigned long long traits) { + NSMutableArray *traitStringsArray; + NSNumber *key; + + static NSDictionary *traitsMapping; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + NSMutableDictionary *mapping = [@{ + @(UIAccessibilityTraitNone): @"None", + @(UIAccessibilityTraitButton): @"Button", + @(UIAccessibilityTraitLink): @"Link", + @(UIAccessibilityTraitHeader): @"Header", + @(UIAccessibilityTraitSearchField): @"SearchField", + @(UIAccessibilityTraitImage): @"Image", + @(UIAccessibilityTraitSelected): @"Selected", + @(UIAccessibilityTraitPlaysSound): @"PlaysSound", + @(UIAccessibilityTraitKeyboardKey): @"KeyboardKey", + @(UIAccessibilityTraitStaticText): @"StaticText", + @(UIAccessibilityTraitSummaryElement): @"SummaryElement", + @(UIAccessibilityTraitNotEnabled): @"NotEnabled", + @(UIAccessibilityTraitUpdatesFrequently): @"UpdatesFrequently", + @(UIAccessibilityTraitStartsMediaSession): @"StartsMediaSession", + @(UIAccessibilityTraitAdjustable): @"Adjustable", + @(UIAccessibilityTraitAllowsDirectInteraction): @"AllowsDirectInteraction", + @(UIAccessibilityTraitCausesPageTurn): @"CausesPageTurn", + @(UIAccessibilityTraitTabBar): @"TabBar" + } mutableCopy]; + + #if __clang_major__ >= 16 + // Add iOS 17.0 specific traits if available + if (@available(iOS 17.0, *)) { + [mapping addEntriesFromDictionary:@{ + @(UIAccessibilityTraitToggleButton): @"ToggleButton", + @(UIAccessibilityTraitSupportsZoom): @"SupportsZoom" + }]; + } + #endif + + traitsMapping = [mapping copy]; + }); + + traitStringsArray = [NSMutableArray array]; + for (key in traitsMapping) { + if (traits & [key unsignedLongLongValue] && nil != traitsMapping[key]) { + [traitStringsArray addObject:(id)traitsMapping[key]]; + } + } + + return [traitStringsArray copy]; +} diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBActiveAppDetectionPoint.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBActiveAppDetectionPoint.h index 6bc4887e93..a65bd38800 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBActiveAppDetectionPoint.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBActiveAppDetectionPoint.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBActiveAppDetectionPoint.m b/WebDriverAgent/WebDriverAgentLib/Utilities/FBActiveAppDetectionPoint.m index db571b5843..2f05ab8787 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBActiveAppDetectionPoint.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBActiveAppDetectionPoint.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the -* LICENSE file in the root directory of this source tree. An additional grant -* of patent rights can be found in the PATENTS file in the same directory. +* LICENSE file in the root directory of this source tree. */ #import "FBActiveAppDetectionPoint.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBAlertsMonitor.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBAlertsMonitor.h index 79bb6a3f1a..069a73474a 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBAlertsMonitor.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBAlertsMonitor.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBAlertsMonitor.m b/WebDriverAgent/WebDriverAgentLib/Utilities/FBAlertsMonitor.m index 1a1b6f34c8..3dd221198c 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBAlertsMonitor.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBAlertsMonitor.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBAlertsMonitor.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.h index 5f779194fd..7dc7c424fe 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBElementCache.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.m b/WebDriverAgent/WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.m index 6d4c046a52..57f9d2d8e0 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBBaseActionsSynthesizer.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBCapabilities.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBCapabilities.h index 20dad743e4..d1339c7a66 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBCapabilities.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBCapabilities.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBCapabilities.m b/WebDriverAgent/WebDriverAgentLib/Utilities/FBCapabilities.m index b6ccc9ce43..351f23e684 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBCapabilities.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBCapabilities.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBCapabilities.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBClassChainQueryParser.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBClassChainQueryParser.h index 080810647d..c2ca9c524b 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBClassChainQueryParser.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBClassChainQueryParser.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBClassChainQueryParser.m b/WebDriverAgent/WebDriverAgentLib/Utilities/FBClassChainQueryParser.m index f04240d0de..933d0ad890 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBClassChainQueryParser.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBClassChainQueryParser.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBClassChainQueryParser.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBConfiguration.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBConfiguration.h index 7c337826cb..e6a7d45528 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBConfiguration.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBConfiguration.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import @@ -103,6 +102,14 @@ extern NSString *const FBSnapshotMaxDepthKey; + (NSUInteger)mjpegServerFramerate; + (void)setMjpegServerFramerate:(NSUInteger)framerate; +/** + Whether to limit the XPath scope to descendant items only while performing a lookup + in an element context. Enabled by default. Being disabled, allows to use XPath locators + like ".." in order to match parent items of the current context root. + */ ++ (BOOL)limitXpathContextScope; ++ (void)setLimitXpathContextScope:(BOOL)enabled; + /** The quality of display screenshots. The higher quality you set is the bigger screenshot size is. The highest quality value is 0 (lossless PNG) or 3 (lossless HEIC). The lowest quality is 2 (highly compressed JPEG). @@ -117,6 +124,12 @@ extern NSString *const FBSnapshotMaxDepthKey; */ + (NSRange)bindingPortRange; +/** + The IP address that the HTTP Server should bind to on launch. + Returns nil if not specified, which causes the server to listen on all interfaces. + */ ++ (NSString * _Nullable)bindingIPAddress; + /** The port number where the background screenshots broadcaster is supposed to run */ @@ -128,8 +141,8 @@ extern NSString *const FBSnapshotMaxDepthKey; ! Setting this to a value less than 100, especially together with orientation fixing enabled ! may lead to WDA process termination because of an excessive CPU usage. */ -+ (NSUInteger)mjpegScalingFactor; -+ (void)setMjpegScalingFactor:(NSUInteger)scalingFactor; ++ (CGFloat)mjpegScalingFactor; ++ (void)setMjpegScalingFactor:(CGFloat)scalingFactor; /** YES if verbose logging is enabled. NO otherwise. @@ -177,14 +190,6 @@ typedef NS_ENUM(NSInteger, FBConfigurationKeyboardPreference) { + (void)setKeyboardPrediction:(BOOL)isEnabled; + (FBConfigurationKeyboardPreference)keyboardPrediction; -/** - * The maximum time to wait until accessibility snapshot is taken - * - * @param timeout The number of float seconds to wait (15 seconds by default) - */ -+ (void)setCustomSnapshotTimeout:(NSTimeInterval)timeout; -+ (NSTimeInterval)customSnapshotTimeout; - /** Sets maximum depth for traversing elements tree from parents to children while requesting XCElementSnapshot. Used to set maxDepth value in a dictionary provided by XCAXClient_iOS's method defaultParams. @@ -283,6 +288,12 @@ typedef NS_ENUM(NSInteger, FBConfigurationKeyboardPreference) { + (void)setDismissAlertButtonSelector:(NSString *)classChainSelector; + (NSString *)dismissAlertButtonSelector; +/** + Sets class chain selector to apply for an automated alert click + */ ++ (void)setAutoClickAlertSelector:(NSString *)classChainSelector; ++ (NSString *)autoClickAlertSelector; + /** * Whether to use HIDEvent for text clear. * By default this is enabled and HIDEvent is used for text clear. @@ -326,6 +337,45 @@ typedef NS_ENUM(NSInteger, FBConfigurationKeyboardPreference) { */ + (void)resetSessionSettings; +/** + * Whether to calculate `hittable` attribute using native APIs + * instead of legacy heuristics. + * This flag improves accuracy, but may affect performance. + * Disabled by default. + * + * @param enabled Either YES or NO + */ ++ (void)setIncludeHittableInPageSource:(BOOL)enabled; ++ (BOOL)includeHittableInPageSource; + +/** + * Whether to include `nativeFrame` attribute in the XML page source. + * + * When enabled, the XML representation will contain the precise rendered + * frame of the UI element. + * + * This value is more accurate than the legacy `wdFrame`, which applies rounding + * and may introduce inconsistencies in size and position calculations. + * + * The value is disabled by default to avoid potential performance overhead. + * + * @param enabled Either YES or NO + */ ++ (void)setIncludeNativeFrameInPageSource:(BOOL)enabled; ++ (BOOL)includeNativeFrameInPageSource; + +/** + * Whether to include `minValue`/`maxValue` attributes in the page source. + * These attributes are retrieved from native element snapshots and represent + * value boundaries for elements like sliders or progress indicators. + * This may affect performance if used on many elements. + * Disabled by default. + * + * @param enabled Either YES or NO + */ ++ (void)setIncludeMinMaxValueInPageSource:(BOOL)enabled; ++ (BOOL)includeMinMaxValueInPageSource; + @end NS_ASSUME_NONNULL_END diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBConfiguration.m b/WebDriverAgent/WebDriverAgentLib/Utilities/FBConfiguration.m index dab8cb4dba..8dfe31bf05 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBConfiguration.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBConfiguration.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBConfiguration.h" @@ -37,7 +36,7 @@ static BOOL FBShouldUseSingletonTestManager = YES; static BOOL FBShouldRespectSystemAlerts = NO; -static NSUInteger FBMjpegScalingFactor = 100; +static CGFloat FBMjpegScalingFactor = 100.0; static BOOL FBMjpegShouldFixOrientation = NO; static NSUInteger FBMjpegServerScreenshotQuality = 25; static NSUInteger FBMjpegServerFramerate = 10; @@ -46,20 +45,24 @@ static BOOL FBShouldTerminateApp; static NSNumber* FBMaxTypingFrequency; static NSUInteger FBScreenshotQuality; -static NSTimeInterval FBCustomSnapshotTimeout; static BOOL FBShouldUseFirstMatch; static BOOL FBShouldBoundElementsByIndex; static BOOL FBIncludeNonModalElements; static NSString *FBAcceptAlertButtonSelector; static NSString *FBDismissAlertButtonSelector; +static NSString *FBAutoClickAlertSelector; static NSTimeInterval FBWaitForIdleTimeout; static NSTimeInterval FBAnimationCoolOffTimeout; static BOOL FBShouldUseCompactResponses; static NSString *FBElementResponseAttributes; static BOOL FBUseClearTextShortcut; +static BOOL FBLimitXpathContextScope = YES; #if !TARGET_OS_TV static UIInterfaceOrientation FBScreenshotOrientation; #endif +static BOOL FBShouldIncludeHittableInPageSource = NO; +static BOOL FBShouldIncludeNativeFrameInPageSource = NO; +static BOOL FBShouldIncludeMinMaxValueInPageSource = NO; @implementation FBConfiguration @@ -134,6 +137,17 @@ + (NSRange)bindingPortRange return NSMakeRange(DefaultStartingPort, DefaultPortRange); } ++ (NSString *)bindingIPAddress +{ + // Existence of USE_IP in the environment allows specifying which interface to bind to + if (NSProcessInfo.processInfo.environment[@"USE_IP"] && + [NSProcessInfo.processInfo.environment[@"USE_IP"] length] > 0) { + return NSProcessInfo.processInfo.environment[@"USE_IP"]; + } + + return nil; +} + + (NSInteger)mjpegServerPort { if (self.mjpegServerPortFromArguments != NSNotFound) { @@ -148,12 +162,12 @@ + (NSInteger)mjpegServerPort return DefaultMjpegServerPort; } -+ (NSUInteger)mjpegScalingFactor ++ (CGFloat)mjpegScalingFactor { return FBMjpegScalingFactor; } -+ (void)setMjpegScalingFactor:(NSUInteger)scalingFactor { ++ (void)setMjpegScalingFactor:(CGFloat)scalingFactor { FBMjpegScalingFactor = scalingFactor; } @@ -359,16 +373,6 @@ + (void)setKeyboardPrediction:(BOOL)isEnabled [self configureKeyboardsPreference:isEnabled forPreferenceKey:FBKeyboardPredictionKey]; } -+ (void)setCustomSnapshotTimeout:(NSTimeInterval)timeout -{ - FBCustomSnapshotTimeout = timeout; -} - -+ (NSTimeInterval)customSnapshotTimeout -{ - return FBCustomSnapshotTimeout; -} - + (void)setSnapshotMaxDepth:(int)maxDepth { FBSetCustomParameterForElementSnapshot(FBSnapshotMaxDepthKey, @(maxDepth)); @@ -439,6 +443,16 @@ + (NSString *)dismissAlertButtonSelector return FBDismissAlertButtonSelector; } ++ (void)setAutoClickAlertSelector:(NSString *)classChainSelector +{ + FBAutoClickAlertSelector = classChainSelector; +} + ++ (NSString *)autoClickAlertSelector +{ + return FBAutoClickAlertSelector; +} + + (void)setUseClearTextShortcut:(BOOL)enabled { FBUseClearTextShortcut = enabled; @@ -449,6 +463,16 @@ + (BOOL)useClearTextShortcut return FBUseClearTextShortcut; } ++ (BOOL)limitXpathContextScope +{ + return FBLimitXpathContextScope; +} + ++ (void)setLimitXpathContextScope:(BOOL)enabled +{ + FBLimitXpathContextScope = enabled; +} + #if !TARGET_OS_TV + (BOOL)setScreenshotOrientation:(NSString *)orientation error:(NSError **)error { @@ -491,6 +515,7 @@ + (NSString *)humanReadableScreenshotOrientation return @"landscapeLeft"; case UIInterfaceOrientationUnknown: return @"auto"; + default: break; } } #endif @@ -502,7 +527,6 @@ + (void)resetSessionSettings FBElementResponseAttributes = @"type,label"; FBMaxTypingFrequency = @([self defaultTypingFrequency]); FBScreenshotQuality = 3; - FBCustomSnapshotTimeout = 15.; FBShouldUseFirstMatch = NO; FBShouldBoundElementsByIndex = NO; // This is diabled by default because enabling it prevents the accessbility snapshot to be taken @@ -510,11 +534,13 @@ + (void)resetSessionSettings FBIncludeNonModalElements = NO; FBAcceptAlertButtonSelector = @""; FBDismissAlertButtonSelector = @""; + FBAutoClickAlertSelector = @""; FBWaitForIdleTimeout = 10.; FBAnimationCoolOffTimeout = 2.; // 50 should be enough for the majority of the cases. The performance is acceptable for values up to 100. FBSetCustomParameterForElementSnapshot(FBSnapshotMaxDepthKey, @50); FBUseClearTextShortcut = YES; + FBLimitXpathContextScope = YES; #if !TARGET_OS_TV FBScreenshotOrientation = UIInterfaceOrientationUnknown; #endif @@ -630,4 +656,34 @@ + (BOOL)reduceMotionEnabled return NO; } ++ (void)setIncludeHittableInPageSource:(BOOL)enabled +{ + FBShouldIncludeHittableInPageSource = enabled; +} + ++ (BOOL)includeHittableInPageSource +{ + return FBShouldIncludeHittableInPageSource; +} + ++ (void)setIncludeNativeFrameInPageSource:(BOOL)enabled +{ + FBShouldIncludeNativeFrameInPageSource = enabled; +} + ++ (BOOL)includeNativeFrameInPageSource +{ + return FBShouldIncludeNativeFrameInPageSource; +} + ++ (void)setIncludeMinMaxValueInPageSource:(BOOL)enabled +{ + FBShouldIncludeMinMaxValueInPageSource = enabled; +} + ++ (BOOL)includeMinMaxValueInPageSource +{ + return FBShouldIncludeMinMaxValueInPageSource; +} + @end diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBDebugLogDelegateDecorator.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBDebugLogDelegateDecorator.h index edc1c0fc43..02ebf89dd2 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBDebugLogDelegateDecorator.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBDebugLogDelegateDecorator.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBDebugLogDelegateDecorator.m b/WebDriverAgent/WebDriverAgentLib/Utilities/FBDebugLogDelegateDecorator.m index 2846a1813e..b4c2c5399b 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBDebugLogDelegateDecorator.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBDebugLogDelegateDecorator.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBDebugLogDelegateDecorator.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBElementHelpers.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBElementHelpers.h new file mode 100644 index 0000000000..d0cf1b3db5 --- /dev/null +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBElementHelpers.h @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + Checks if the element is a text field + + @param elementType XCTest element type + @return YES if the element is a text field + */ +BOOL FBDoesElementSupportInnerText(XCUIElementType elementType); + +/** + Checks if the element supports min/max value attributes + + @param elementType XCTest element type + @return YES if the element type supports min/max value attributes + */ +BOOL FBDoesElementSupportMinMaxValue(XCUIElementType elementType); + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBElementHelpers.m b/WebDriverAgent/WebDriverAgentLib/Utilities/FBElementHelpers.m new file mode 100644 index 0000000000..6b9d340fed --- /dev/null +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBElementHelpers.m @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBElementHelpers.h" + +BOOL FBDoesElementSupportInnerText(XCUIElementType elementType) { + return elementType == XCUIElementTypeTextView + || elementType == XCUIElementTypeTextField + || elementType == XCUIElementTypeSearchField + || elementType == XCUIElementTypeSecureTextField; +} + +BOOL FBDoesElementSupportMinMaxValue(XCUIElementType elementType) { + return elementType == XCUIElementTypeSlider + || elementType == XCUIElementTypeStepper; +} diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBElementTypeTransformer.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBElementTypeTransformer.h index b4097eee42..a94ff62a93 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBElementTypeTransformer.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBElementTypeTransformer.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBElementTypeTransformer.m b/WebDriverAgent/WebDriverAgentLib/Utilities/FBElementTypeTransformer.m index 7dc5b1e62b..d3748a44c2 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBElementTypeTransformer.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBElementTypeTransformer.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBElementTypeTransformer.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBErrorBuilder.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBErrorBuilder.h index 7678daf191..8435eba98d 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBErrorBuilder.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBErrorBuilder.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBErrorBuilder.m b/WebDriverAgent/WebDriverAgentLib/Utilities/FBErrorBuilder.m index cafaf503be..3ee5d8fa76 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBErrorBuilder.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBErrorBuilder.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBErrorBuilder.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBFailureProofTestCase.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBFailureProofTestCase.h index 5d744107e9..80d86483ed 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBFailureProofTestCase.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBFailureProofTestCase.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBFailureProofTestCase.m b/WebDriverAgent/WebDriverAgentLib/Utilities/FBFailureProofTestCase.m index 72d79ee1c6..ab91fd94d1 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBFailureProofTestCase.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBFailureProofTestCase.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBFailureProofTestCase.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBImageProcessor.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBImageProcessor.h index a350bd9ce0..e83b830f70 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBImageProcessor.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBImageProcessor.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBImageProcessor.m b/WebDriverAgent/WebDriverAgentLib/Utilities/FBImageProcessor.m index 16601ce8e7..7d2ada7593 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBImageProcessor.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBImageProcessor.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBImageProcessor.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBImageUtils.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBImageUtils.h index 15a04f6fe1..07501657fe 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBImageUtils.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBImageUtils.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBImageUtils.m b/WebDriverAgent/WebDriverAgentLib/Utilities/FBImageUtils.m index b1d2f9b5b8..d991cea180 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBImageUtils.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBImageUtils.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBImageUtils.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBKeyboard.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBKeyboard.h index fa1515e6b6..c3c5f8ecf0 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBKeyboard.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBKeyboard.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBKeyboard.m b/WebDriverAgent/WebDriverAgentLib/Utilities/FBKeyboard.m index d90a68e295..af3529eb3f 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBKeyboard.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBKeyboard.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBKeyboard.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBLogger.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBLogger.h index 3626fa520f..c92b7ef7e7 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBLogger.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBLogger.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBLogger.m b/WebDriverAgent/WebDriverAgentLib/Utilities/FBLogger.m index 9466b86c45..060b8f00ba 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBLogger.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBLogger.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBLogger.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBMacros.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBMacros.h index dae0c5fafb..ba77c2b596 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBMacros.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBMacros.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ // Typedef to help with storing constant strings for enums. diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBMathUtils.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBMathUtils.h index 2d52d3e6db..2dbce05a55 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBMathUtils.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBMathUtils.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBMathUtils.m b/WebDriverAgent/WebDriverAgentLib/Utilities/FBMathUtils.m index 44ee257dc9..87a7ecf637 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBMathUtils.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBMathUtils.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBMathUtils.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBMjpegServer.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBMjpegServer.h index 6ab15a300c..294c399f83 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBMjpegServer.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBMjpegServer.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBTCPSocket.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBMjpegServer.m b/WebDriverAgent/WebDriverAgentLib/Utilities/FBMjpegServer.m index 691e57746b..4c786ba030 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBMjpegServer.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBMjpegServer.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBMjpegServer.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBNotificationsHelper.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBNotificationsHelper.h index b0ca35b361..b337cb116e 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBNotificationsHelper.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBNotificationsHelper.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBNotificationsHelper.m b/WebDriverAgent/WebDriverAgentLib/Utilities/FBNotificationsHelper.m index 60cc509d6d..457047888a 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBNotificationsHelper.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBNotificationsHelper.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBNotificationsHelper.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBPasteboard.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBPasteboard.h index 9595162d41..f61d7d64cc 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBPasteboard.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBPasteboard.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBPasteboard.m b/WebDriverAgent/WebDriverAgentLib/Utilities/FBPasteboard.m index 0171b259ca..08e4e9f339 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBPasteboard.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBPasteboard.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBPasteboard.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBProtocolHelpers.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBProtocolHelpers.h index eff0a518b5..e25573e7ed 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBProtocolHelpers.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBProtocolHelpers.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBProtocolHelpers.m b/WebDriverAgent/WebDriverAgentLib/Utilities/FBProtocolHelpers.m index 3cd0375867..a463294a94 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBProtocolHelpers.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBProtocolHelpers.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBProtocolHelpers.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBReflectionUtils.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBReflectionUtils.h index 0e4cca3147..262702a072 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBReflectionUtils.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBReflectionUtils.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBReflectionUtils.m b/WebDriverAgent/WebDriverAgentLib/Utilities/FBReflectionUtils.m index 9114c312f1..8ef8419fa8 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBReflectionUtils.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBReflectionUtils.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBReflectionUtils.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBRunLoopSpinner.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBRunLoopSpinner.h index bc6b543c33..5f8faf52ec 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBRunLoopSpinner.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBRunLoopSpinner.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBRunLoopSpinner.m b/WebDriverAgent/WebDriverAgentLib/Utilities/FBRunLoopSpinner.m index 386238734d..0912325421 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBRunLoopSpinner.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBRunLoopSpinner.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBRunLoopSpinner.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBRuntimeUtils.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBRuntimeUtils.h index 8e54ed4c5f..b16b123ade 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBRuntimeUtils.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBRuntimeUtils.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBRuntimeUtils.m b/WebDriverAgent/WebDriverAgentLib/Utilities/FBRuntimeUtils.m index bb08de7e3e..6904cf7602 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBRuntimeUtils.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBRuntimeUtils.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBRuntimeUtils.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBScreen.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBScreen.h index 9c4cca87ed..87c22c7cb4 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBScreen.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBScreen.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBScreen.m b/WebDriverAgent/WebDriverAgentLib/Utilities/FBScreen.m index 17b7d8b19b..dc6536408a 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBScreen.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBScreen.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBScreen.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBScreenshot.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBScreenshot.h index 74e6b5886c..b0fdfbe712 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBScreenshot.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBScreenshot.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBScreenshot.m b/WebDriverAgent/WebDriverAgentLib/Utilities/FBScreenshot.m index 3d9716deaf..f13f967386 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBScreenshot.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBScreenshot.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBScreenshot.h" @@ -28,7 +27,7 @@ NSString *formatTimeInterval(NSTimeInterval interval) { NSUInteger milliseconds = (NSUInteger)(interval * 1000); - return [NSString stringWithFormat:@"%ld ms", milliseconds]; + return [NSString stringWithFormat:@"%lu ms", milliseconds]; } @implementation FBScreenshot diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBSettings.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBSettings.h index b1f2f1fca5..b98a070b6c 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBSettings.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBSettings.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import @@ -22,9 +21,6 @@ extern NSString* const FB_SETTING_MJPEG_SCALING_FACTOR; extern NSString* const FB_SETTING_SCREENSHOT_QUALITY; extern NSString* const FB_SETTING_KEYBOARD_AUTOCORRECTION; extern NSString* const FB_SETTING_KEYBOARD_PREDICTION; -// This setting is deprecated. Please use CUSTOM_SNAPSHOT_TIMEOUT instead -extern NSString* const FB_SETTING_SNAPSHOT_TIMEOUT; -extern NSString* const FB_SETTING_CUSTOM_SNAPSHOT_TIMEOUT; extern NSString* const FB_SETTING_SNAPSHOT_MAX_DEPTH; extern NSString* const FB_SETTING_USE_FIRST_MATCH; extern NSString* const FB_SETTING_BOUND_ELEMENTS_BY_INDEX; @@ -41,6 +37,10 @@ extern NSString* const FB_SETTING_ANIMATION_COOL_OFF_TIMEOUT; extern NSString* const FB_SETTING_MAX_TYPING_FREQUENCY; extern NSString* const FB_SETTING_RESPECT_SYSTEM_ALERTS; extern NSString* const FB_SETTING_USE_CLEAR_TEXT_SHORTCUT; - +extern NSString* const FB_SETTING_LIMIT_XPATH_CONTEXT_SCOPE; +extern NSString* const FB_SETTING_AUTO_CLICK_ALERT_SELECTOR; +extern NSString *const FB_SETTING_INCLUDE_HITTABLE_IN_PAGE_SOURCE; +extern NSString *const FB_SETTING_INCLUDE_NATIVE_FRAME_IN_PAGE_SOURCE; +extern NSString *const FB_SETTING_INCLUDE_MIN_MAX_VALUE_IN_PAGE_SOURCE; NS_ASSUME_NONNULL_END diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBSettings.m b/WebDriverAgent/WebDriverAgentLib/Utilities/FBSettings.m index ada529eed5..17b8ab79d1 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBSettings.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBSettings.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBSettings.h" @@ -18,8 +17,6 @@ NSString* const FB_SETTING_SCREENSHOT_QUALITY = @"screenshotQuality"; NSString* const FB_SETTING_KEYBOARD_AUTOCORRECTION = @"keyboardAutocorrection"; NSString* const FB_SETTING_KEYBOARD_PREDICTION = @"keyboardPrediction"; -NSString* const FB_SETTING_SNAPSHOT_TIMEOUT = @"snapshotTimeout"; -NSString* const FB_SETTING_CUSTOM_SNAPSHOT_TIMEOUT = @"customSnapshotTimeout"; NSString* const FB_SETTING_SNAPSHOT_MAX_DEPTH = @"snapshotMaxDepth"; NSString* const FB_SETTING_USE_FIRST_MATCH = @"useFirstMatch"; NSString* const FB_SETTING_BOUND_ELEMENTS_BY_INDEX = @"boundElementsByIndex"; @@ -36,3 +33,8 @@ NSString* const FB_SETTING_MAX_TYPING_FREQUENCY = @"maxTypingFrequency"; NSString* const FB_SETTING_RESPECT_SYSTEM_ALERTS = @"respectSystemAlerts"; NSString* const FB_SETTING_USE_CLEAR_TEXT_SHORTCUT = @"useClearTextShortcut"; +NSString* const FB_SETTING_LIMIT_XPATH_CONTEXT_SCOPE = @"limitXPathContextScope"; +NSString* const FB_SETTING_AUTO_CLICK_ALERT_SELECTOR = @"autoClickAlertSelector"; +NSString* const FB_SETTING_INCLUDE_HITTABLE_IN_PAGE_SOURCE = @"includeHittableInPageSource"; +NSString* const FB_SETTING_INCLUDE_NATIVE_FRAME_IN_PAGE_SOURCE = @"includeNativeFrameInPageSource"; +NSString* const FB_SETTING_INCLUDE_MIN_MAX_VALUE_IN_PAGE_SOURCE = @"includeMinMaxValueInPageSource"; diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBTVNavigationTracker-Private.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBTVNavigationTracker-Private.h index 2496ccbdaa..8b7f775dfa 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBTVNavigationTracker-Private.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBTVNavigationTracker-Private.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBTVNavigationTracker.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBTVNavigationTracker.h index 9e539f9e09..3a80583b44 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBTVNavigationTracker.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBTVNavigationTracker.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBTVNavigationTracker.m b/WebDriverAgent/WebDriverAgentLib/Utilities/FBTVNavigationTracker.m index 11becfa72d..19fa4a0b61 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBTVNavigationTracker.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBTVNavigationTracker.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBTVNavigationTracker.h" @@ -57,9 +56,7 @@ - (instancetype)initWithTargetElement:(XCUIElement *)targetElement self = [super init]; if (self) { _targetElement = targetElement; - CGRect frame = targetElement.fb_isResolvedFromCache.boolValue - ? [FBXCElementSnapshotWrapper ensureWrapped:targetElement.lastSnapshot].wdFrame - : targetElement.wdFrame; + CGRect frame = targetElement.wdFrame; _targetCenter = FBRectGetCenter(frame); _navigationItems = [NSMutableDictionary dictionary]; } diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBUnattachedAppLauncher.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBUnattachedAppLauncher.h index eabf476f74..f640405b04 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBUnattachedAppLauncher.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBUnattachedAppLauncher.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBUnattachedAppLauncher.m b/WebDriverAgent/WebDriverAgentLib/Utilities/FBUnattachedAppLauncher.m index f877b67c65..a8679a872a 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBUnattachedAppLauncher.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBUnattachedAppLauncher.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBUnattachedAppLauncher.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBW3CActionsHelpers.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBW3CActionsHelpers.h index 3c194d4cd9..3add9b1b67 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBW3CActionsHelpers.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBW3CActionsHelpers.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the -* LICENSE file in the root directory of this source tree. An additional grant -* of patent rights can be found in the PATENTS file in the same directory. +* LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBW3CActionsHelpers.m b/WebDriverAgent/WebDriverAgentLib/Utilities/FBW3CActionsHelpers.m index ae0270dc3d..f7a052d4f0 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBW3CActionsHelpers.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBW3CActionsHelpers.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the -* LICENSE file in the root directory of this source tree. An additional grant -* of patent rights can be found in the PATENTS file in the same directory. +* LICENSE file in the root directory of this source tree. */ #import "FBW3CActionsHelpers.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.h index e5ed479a74..746c38e36f 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBBaseActionsSynthesizer.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.m b/WebDriverAgent/WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.m index c488ca4ac4..ef17e03508 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBW3CActionsSynthesizer.h" @@ -661,7 +660,7 @@ @implementation FBW3CActionsSynthesizer if ([origin isKindOfClass:XCUIElement.class]) { instance = origin; } else if ([origin isKindOfClass:NSString.class]) { - instance = [self.elementCache elementForUUID:(NSString *)origin]; + instance = [self.elementCache elementForUUID:(NSString *)origin checkStaleness:YES]; } else { [result addObject:actionItem]; continue; diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBWebServerParams.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBWebServerParams.h index c92daecb2c..aba0fa34f7 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBWebServerParams.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBWebServerParams.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBWebServerParams.m b/WebDriverAgent/WebDriverAgentLib/Utilities/FBWebServerParams.m index 65e85f65a2..9ced0a38de 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBWebServerParams.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBWebServerParams.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBWebServerParams.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBXCAXClientProxy.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBXCAXClientProxy.h index cf6f497a9b..b216e303f5 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBXCAXClientProxy.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBXCAXClientProxy.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import @@ -27,7 +26,7 @@ NS_ASSUME_NONNULL_BEGIN - (nullable id)snapshotForElement:(id)element attributes:(nullable NSArray *)attributes - maxDepth:(nullable NSNumber *)maxDepth + inDepth:(BOOL)inDepth error:(NSError **)error; - (NSArray> *)activeApplications; @@ -39,10 +38,11 @@ NS_ASSUME_NONNULL_BEGIN - (void)notifyWhenNoAnimationsAreActiveForApplication:(XCUIApplication *)application reply:(void (^)(void))reply; -- (NSDictionary *)attributesForElement:(id)element - attributes:(NSArray *)attributes; +- (nullable NSDictionary *)attributesForElement:(id)element + attributes:(NSArray *)attributes + error:(NSError**)error; -- (XCUIApplication *)monitoredApplicationWithProcessIdentifier:(int)pid; +- (nullable XCUIApplication *)monitoredApplicationWithProcessIdentifier:(int)pid; @end diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBXCAXClientProxy.m b/WebDriverAgent/WebDriverAgentLib/Utilities/FBXCAXClientProxy.m index 907bb25b1b..f84f803fb1 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBXCAXClientProxy.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBXCAXClientProxy.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBXCAXClientProxy.h" @@ -14,9 +13,16 @@ #import "FBMacros.h" #import "XCAXClient_iOS+FBSnapshotReqParams.h" #import "XCUIDevice.h" +#import "XCUIApplication.h" static id FBAXClient = nil; +@interface FBXCAXClientProxy () + +@property (nonatomic) NSMutableDictionary *appsCache; + +@end + @implementation FBXCAXClientProxy + (instancetype)sharedClient @@ -25,6 +31,7 @@ + (instancetype)sharedClient static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [[self alloc] init]; + instance.appsCache = [NSMutableDictionary dictionary]; FBAXClient = [XCUIDevice.sharedDevice accessibilityInterface]; }); return instance; @@ -37,12 +44,12 @@ - (BOOL)setAXTimeout:(NSTimeInterval)timeout error:(NSError **)error - (id)snapshotForElement:(id)element attributes:(NSArray *)attributes - maxDepth:(nullable NSNumber *)maxDepth + inDepth:(BOOL)inDepth error:(NSError **)error { NSMutableDictionary *parameters = [NSMutableDictionary dictionaryWithDictionary:self.defaultParameters]; - if (nil != maxDepth) { - parameters[FBSnapshotMaxDepthKey] = maxDepth; + if (!inDepth) { + parameters[FBSnapshotMaxDepthKey] = @1; } id result = [FBAXClient requestSnapshotForElement:element @@ -76,20 +83,37 @@ - (void)notifyWhenNoAnimationsAreActiveForApplication:(XCUIApplication *)applica - (NSDictionary *)attributesForElement:(id)element attributes:(NSArray *)attributes + error:(NSError**)error; { - NSError *error = nil; - NSDictionary* result = [FBAXClient attributesForElement:element - attributes:attributes - error:&error]; - if (error) { - [FBLogger logFmt:@"Cannot retrieve element attribute(s) %@. Original error: %@", attributes, error.description]; - } - return result; + return [FBAXClient attributesForElement:element + attributes:attributes + error:error]; } - (XCUIApplication *)monitoredApplicationWithProcessIdentifier:(int)pid { - return [[FBAXClient applicationProcessTracker] monitoredApplicationWithProcessIdentifier:pid]; + NSMutableSet *terminatedAppIds = [NSMutableSet set]; + for (NSNumber *appPid in self.appsCache) { + if (![self.appsCache[appPid] running]) { + [terminatedAppIds addObject:appPid]; + } + } + for (NSNumber *appPid in terminatedAppIds) { + [self.appsCache removeObjectForKey:appPid]; + } + + XCUIApplication *result = [self.appsCache objectForKey:@(pid)]; + if (nil != result) { + return result; + } + + XCUIApplication *app = [[FBAXClient applicationProcessTracker] + monitoredApplicationWithProcessIdentifier:pid]; + if (nil == app) { + return nil; + } + [self.appsCache setObject:app forKey:@(pid)]; + return app; } @end diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.h index 18761248bf..41514a5cd2 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.m b/WebDriverAgent/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.m index 59e3a64be8..e29b94e995 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBXCTestDaemonsProxy.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBXCodeCompatibility.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBXCodeCompatibility.h index 0a7dd9a0c4..75d1ee2035 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBXCodeCompatibility.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBXCodeCompatibility.h @@ -3,13 +3,14 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import #import "XCPointerEvent.h" +@class FBXCElementSnapshot; + /** The version of testmanagerd process which is running on the device. @@ -46,7 +47,7 @@ NS_ASSUME_NONNULL_BEGIN @param error The error instance if there was a failure while retrieveing the snapshot @returns The cached unqiue snapshot or nil if the element is stale */ -- (nullable XCElementSnapshot *)fb_uniqueSnapshotWithError:(NSError **)error; +- (nullable id)fb_uniqueSnapshotWithError:(NSError **)error; @end diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBXCodeCompatibility.m b/WebDriverAgent/WebDriverAgentLib/Utilities/FBXCodeCompatibility.m index b13a79e554..f2cf03a24a 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBXCodeCompatibility.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBXCodeCompatibility.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBXCodeCompatibility.h" @@ -20,9 +19,9 @@ @implementation XCUIElementQuery (FBCompatibility) -- (XCElementSnapshot *)fb_uniqueSnapshotWithError:(NSError **)error +- (id)fb_uniqueSnapshotWithError:(NSError **)error { - return [self uniqueMatchingSnapshotWithError:error]; + return (id)[self uniqueMatchingSnapshotWithError:error]; } - (XCUIElement *)fb_firstMatch diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBXMLGenerationOptions.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBXMLGenerationOptions.h index 0fd71e1392..8a014c12bd 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBXMLGenerationOptions.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBXMLGenerationOptions.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBXMLGenerationOptions.m b/WebDriverAgent/WebDriverAgentLib/Utilities/FBXMLGenerationOptions.m index 23bfa5892a..40dcebd232 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBXMLGenerationOptions.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBXMLGenerationOptions.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBXMLGenerationOptions.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBXPath-Private.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBXPath-Private.h index 31bb403bf1..db9732c41f 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBXPath-Private.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBXPath-Private.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import @@ -25,7 +24,7 @@ NS_ASSUME_NONNULL_BEGIN If `query` argument is assigned then `excludedAttributes` argument is effectively ignored. @return zero if the method has completed successfully */ -+ (int)xmlRepresentationWithRootElement:(id)root ++ (int)xmlRepresentationWithRootElement:(id)root writer:(xmlTextWriterPtr)writer elementStore:(nullable NSMutableDictionary *)elementStore query:(nullable NSString*)query @@ -45,9 +44,12 @@ NS_ASSUME_NONNULL_BEGIN @param xpathQuery actual query. Should be valid XPath 1.0-compatible expression @param document libxml2-compatible document pointer + @param contextNode Optonal context node instance @return pointer to a libxml2-compatible structure with set of matched nodes or NULL in case of failure */ -+ (xmlXPathObjectPtr)evaluate:(NSString *)xpathQuery document:(xmlDocPtr)doc; ++ (xmlXPathObjectPtr)evaluate:(NSString *)xpathQuery + document:(xmlDocPtr)doc + contextNode:(nullable xmlNodePtr)contextNode; @end diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBXPath.h b/WebDriverAgent/WebDriverAgentLib/Utilities/FBXPath.h index 50c8519782..1135c7228a 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBXPath.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBXPath.h @@ -3,13 +3,12 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import #import -#import "FBXCElementSnapshot.h" +#import #ifdef __clang__ #pragma clang diagnostic push diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/FBXPath.m b/WebDriverAgent/WebDriverAgentLib/Utilities/FBXPath.m index eae3ee4383..79e8f75f38 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/FBXPath.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/FBXPath.m @@ -3,24 +3,28 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBXPath.h" #import "FBConfiguration.h" #import "FBExceptions.h" +#import "FBElementUtils.h" #import "FBLogger.h" #import "FBMacros.h" #import "FBXMLGenerationOptions.h" #import "FBXCElementSnapshotWrapper+Helpers.h" #import "NSString+FBXMLSafeString.h" +#import "XCUIApplication.h" #import "XCUIElement.h" #import "XCUIElement+FBCaching.h" #import "XCUIElement+FBUtilities.h" #import "XCUIElement+FBWebDriverAttributes.h" #import "XCTestPrivateSymbols.h" +#import "FBElementHelpers.h" +#import "FBXCAXClientProxy.h" +#import "FBXCAccessibilityElement.h" @interface FBElementAttribute : NSObject @@ -31,6 +35,7 @@ + (nonnull NSString *)name; + (nullable NSString *)valueForElement:(id)element; + (int)recordWithWriter:(xmlTextWriterPtr)writer forElement:(id)element; ++ (int)recordWithWriter:(xmlTextWriterPtr)writer forValue:(nullable NSString *)value; + (NSArray *)supportedAttributes; @@ -96,7 +101,33 @@ @interface FBInternalIndexAttribute : FBElementAttribute @property (nonatomic, nonnull, readonly) NSString* indexValue; -+ (int)recordWithWriter:(xmlTextWriterPtr)writer forValue:(NSString *)value; +@end + +@interface FBApplicationBundleIdAttribute : FBElementAttribute + +@end + +@interface FBApplicationPidAttribute : FBElementAttribute + +@end + +@interface FBPlaceholderValueAttribute : FBElementAttribute + +@end + +@interface FBNativeFrameAttribute : FBElementAttribute + +@end + +@interface FBTraitsAttribute : FBElementAttribute + +@end + +@interface FBMinValueAttribute : FBElementAttribute + +@end + +@interface FBMaxValueAttribute : FBElementAttribute @end @@ -141,7 +172,11 @@ + (nullable NSString *)xmlStringWithRootElement:(id)root } if (rc >= 0) { - rc = [self xmlRepresentationWithRootElement:root + [self waitUntilStableWithElement:root]; + // If 'includeHittableInPageSource' setting is enabled, then use native snapshots + // to calculate a more accurate value for the 'hittable' attribute. + rc = [self xmlRepresentationWithRootElement:[self snapshotWithRoot:root + useNative:FBConfiguration.includeHittableInPageSource] writer:writer elementStore:nil query:nil @@ -189,10 +224,35 @@ + (nullable NSString *)xmlStringWithRootElement:(id)root } NSMutableDictionary *elementStore = [NSMutableDictionary dictionary]; int rc = xmlTextWriterStartDocument(writer, NULL, _UTF8Encoding, NULL); + id lookupScopeSnapshot = nil; + id contextRootSnapshot = nil; + BOOL useNativeSnapshot = nil == xpathQuery + ? NO + : [[self.class elementAttributesWithXPathQuery:xpathQuery] containsObject:FBHittableAttribute.class]; if (rc < 0) { [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartDocument. Error code: %d", rc]; } else { - rc = [self xmlRepresentationWithRootElement:root + [self waitUntilStableWithElement:root]; + if (FBConfiguration.limitXpathContextScope) { + lookupScopeSnapshot = [self snapshotWithRoot:root useNative:useNativeSnapshot]; + } else { + if ([root isKindOfClass:XCUIElement.class]) { + lookupScopeSnapshot = [self snapshotWithRoot:[(XCUIElement *)root application] + useNative:useNativeSnapshot]; + contextRootSnapshot = [root isKindOfClass:XCUIApplication.class] + ? nil + : ([(XCUIElement *)root lastSnapshot] ?: [self snapshotWithRoot:(XCUIElement *)root + useNative:useNativeSnapshot]); + } else { + lookupScopeSnapshot = (id)root; + contextRootSnapshot = nil == lookupScopeSnapshot.parent ? nil : (id)root; + while (nil != lookupScopeSnapshot.parent) { + lookupScopeSnapshot = lookupScopeSnapshot.parent; + } + } + } + + rc = [self xmlRepresentationWithRootElement:lookupScopeSnapshot writer:writer elementStore:elementStore query:xpathQuery @@ -210,7 +270,22 @@ + (nullable NSString *)xmlStringWithRootElement:(id)root return [self throwException:FBXPathQueryEvaluationException forQuery:xpathQuery]; } - xmlXPathObjectPtr queryResult = [self evaluate:xpathQuery document:doc]; + xmlXPathObjectPtr contextNodeQueryResult = [self matchNodeInDocument:doc + elementStore:elementStore.copy + forSnapshot:contextRootSnapshot]; + xmlNodePtr contextNode = NULL; + if (NULL != contextNodeQueryResult) { + xmlNodeSetPtr nodeSet = contextNodeQueryResult->nodesetval; + if (!xmlXPathNodeSetIsEmpty(nodeSet)) { + contextNode = nodeSet->nodeTab[0]; + } + } + xmlXPathObjectPtr queryResult = [self evaluate:xpathQuery + document:doc + contextNode:contextNode]; + if (NULL != contextNodeQueryResult) { + xmlXPathFreeObject(contextNodeQueryResult); + } if (NULL == queryResult) { xmlFreeTextWriter(writer); xmlFreeDoc(doc); @@ -252,6 +327,36 @@ + (NSArray *)collectMatchingSnapshots:(xmlNodeSetPtr)nodeSet return matchingSnapshots.copy; } ++ (nullable xmlXPathObjectPtr)matchNodeInDocument:(xmlDocPtr)doc + elementStore:(NSDictionary> *)elementStore + forSnapshot:(nullable id)snapshot +{ + if (nil == snapshot) { + return NULL; + } + + NSString *contextRootUid = [FBElementUtils uidWithAccessibilityElement:[(id)snapshot accessibilityElement]]; + if (nil == contextRootUid) { + return NULL; + } + + for (NSString *key in elementStore) { + id value = [elementStore objectForKey:key]; + NSString *snapshotUid = [FBElementUtils uidWithAccessibilityElement:[value accessibilityElement]]; + if (nil == snapshotUid || ![snapshotUid isEqualToString:contextRootUid]) { + continue; + } + NSString *indexQuery = [NSString stringWithFormat:@"//*[@%@=\"%@\"]", kXMLIndexPathKey, key]; + xmlXPathObjectPtr queryResult = [self evaluate:indexQuery + document:doc + contextNode:NULL]; + if (NULL != queryResult) { + return queryResult; + } + } + return NULL; +} + + (NSSet *)elementAttributesWithXPathQuery:(NSString *)query { if ([query rangeOfString:@"[^\\w@]@\\*[^\\w]" options:NSRegularExpressionSearch].location != NSNotFound) { @@ -267,7 +372,7 @@ + (NSArray *)collectMatchingSnapshots:(xmlNodeSetPtr)nodeSet return result.copy; } -+ (int)xmlRepresentationWithRootElement:(id)root ++ (int)xmlRepresentationWithRootElement:(id)root writer:(xmlTextWriterPtr)writer elementStore:(nullable NSMutableDictionary *)elementStore query:(nullable NSString*)query @@ -278,9 +383,20 @@ + (int)xmlRepresentationWithRootElement:(id)root NSMutableSet *includedAttributes; if (nil == query) { includedAttributes = [NSMutableSet setWithArray:FBElementAttribute.supportedAttributes]; - // The hittable attribute is expensive to calculate for each snapshot item - // thus we only include it when requested by an xPath query - [includedAttributes removeObject:FBHittableAttribute.class]; + if (!FBConfiguration.includeHittableInPageSource) { + // The hittable attribute is expensive to calculate for each snapshot item + // thus we only include it when requested explicitly + [includedAttributes removeObject:FBHittableAttribute.class]; + } + if (!FBConfiguration.includeNativeFrameInPageSource) { + // Include nativeFrame only when requested + [includedAttributes removeObject:FBNativeFrameAttribute.class]; + } + if (!FBConfiguration.includeMinMaxValueInPageSource) { + // minValue/maxValue are retrieved from private APIs and may be slow on deep trees + [includedAttributes removeObject:FBMinValueAttribute.class]; + [includedAttributes removeObject:FBMaxValueAttribute.class]; + } if (nil != excludedAttributes) { for (NSString *excludedAttributeName in excludedAttributes) { for (Class supportedAttribute in FBElementAttribute.supportedAttributes) { @@ -308,14 +424,16 @@ + (int)xmlRepresentationWithRootElement:(id)root return 0; } -+ (xmlXPathObjectPtr)evaluate:(NSString *)xpathQuery document:(xmlDocPtr)doc ++ (xmlXPathObjectPtr)evaluate:(NSString *)xpathQuery + document:(xmlDocPtr)doc + contextNode:(nullable xmlNodePtr)contextNode { xmlXPathContextPtr xpathCtx = xmlXPathNewContext(doc); if (NULL == xpathCtx) { [FBLogger logFmt:@"Failed to invoke libxml2>xmlXPathNewContext for XPath query \"%@\"", xpathQuery]; return NULL; } - xpathCtx->node = doc->children; + xpathCtx->node = NULL == contextNode ? doc->children : contextNode; xmlXPathObjectPtr xpathObj = xmlXPathEvalExpression((const xmlChar *)[xpathQuery UTF8String], xpathCtx); if (NULL == xpathObj) { @@ -342,6 +460,16 @@ + (int)recordElementAttributes:(xmlTextWriterPtr)writer if (includedAttributes && ![includedAttributes containsObject:attributeCls]) { continue; } + // Text-input placeholder (only for elements that support inner text) + if ((attributeCls == FBPlaceholderValueAttribute.class) && + !FBDoesElementSupportInnerText(element.elementType)) { + continue; + } + // Only for elements that support min/max value + if ((attributeCls == FBMinValueAttribute.class || attributeCls == FBMaxValueAttribute.class) && + !FBDoesElementSupportMinMaxValue(element.elementType)) { + continue; + } int rc = [attributeCls recordWithWriter:writer forElement:[FBXCElementSnapshotWrapper ensureWrapped:element]]; if (rc < 0) { @@ -353,10 +481,31 @@ + (int)recordElementAttributes:(xmlTextWriterPtr)writer // index path is the special case return [FBInternalIndexAttribute recordWithWriter:writer forValue:indexPath]; } + if (element.elementType == XCUIElementTypeApplication) { + // only record process identifier and bundle identifier for the application element + int pid = [element.accessibilityElement processIdentifier]; + if (pid > 0) { + int rc = [FBApplicationPidAttribute recordWithWriter:writer + forValue:[NSString stringWithFormat:@"%d", pid]]; + if (rc < 0) { + return rc; + } + XCUIApplication *app = [[FBXCAXClientProxy sharedClient] + monitoredApplicationWithProcessIdentifier:pid]; + NSString *bundleID = [app bundleID]; + if (nil != bundleID) { + rc = [FBApplicationBundleIdAttribute recordWithWriter:writer + forValue:bundleID]; + if (rc < 0) { + return rc; + } + } + } + } return 0; } -+ (int)writeXmlWithRootElement:(id)root ++ (int)writeXmlWithRootElement:(id)root indexPath:(nullable NSString *)indexPath elementStore:(nullable NSMutableDictionary *)elementStore includedAttributes:(nullable NSSet *)includedAttributes @@ -364,30 +513,13 @@ + (int)writeXmlWithRootElement:(id)root { NSAssert((indexPath == nil && elementStore == nil) || (indexPath != nil && elementStore != nil), @"Either both or none of indexPath and elementStore arguments should be equal to nil", nil); - id currentSnapshot; - NSArray> *children; - if ([root isKindOfClass:XCUIElement.class]) { - XCUIElement *element = (XCUIElement *)root; - NSMutableArray *snapshotAttributes = [NSMutableArray arrayWithArray:FBStandardAttributeNames()]; - if (nil == includedAttributes || [includedAttributes containsObject:FBVisibleAttribute.class]) { - [snapshotAttributes addObject:FB_XCAXAIsVisibleAttributeName]; - // If the app is not idle state while we retrieve the visiblity state - // then the snapshot retrieval operation might freeze and time out - [element.application fb_waitUntilStableWithTimeout:FBConfiguration.animationCoolOffTimeout]; - } - currentSnapshot = [element fb_snapshotWithAttributes:snapshotAttributes.copy - maxDepth:nil]; - children = currentSnapshot.children; - } else { - currentSnapshot = (id)root; - children = currentSnapshot.children; - } + NSArray> *children = root.children; if (elementStore != nil && indexPath != nil && [indexPath isEqualToString:topNodeIndexPath]) { - [elementStore setObject:currentSnapshot forKey:topNodeIndexPath]; + [elementStore setObject:root forKey:topNodeIndexPath]; } - FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:currentSnapshot]; + FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:root]; int rc = xmlTextWriterStartElement(writer, (xmlChar *)[wrappedSnapshot.wdType UTF8String]); if (rc < 0) { [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartElement for the tag value '%@'. Error code: %d", wrappedSnapshot.wdType, rc]; @@ -395,7 +527,7 @@ + (int)writeXmlWithRootElement:(id)root } rc = [self recordElementAttributes:writer - forElement:currentSnapshot + forElement:root indexPath:indexPath includedAttributes:includedAttributes]; if (rc < 0) { @@ -403,18 +535,20 @@ + (int)writeXmlWithRootElement:(id)root } for (NSUInteger i = 0; i < [children count]; i++) { - id childSnapshot = [children objectAtIndex:i]; - NSString *newIndexPath = (indexPath != nil) ? [indexPath stringByAppendingFormat:@",%lu", (unsigned long)i] : nil; - if (elementStore != nil && newIndexPath != nil) { - [elementStore setObject:childSnapshot forKey:(id)newIndexPath]; - } - rc = [self writeXmlWithRootElement:[FBXCElementSnapshotWrapper ensureWrapped:childSnapshot] - indexPath:newIndexPath - elementStore:elementStore - includedAttributes:includedAttributes - writer:writer]; - if (rc < 0) { - return rc; + @autoreleasepool { + id childSnapshot = [children objectAtIndex:i]; + NSString *newIndexPath = (indexPath != nil) ? [indexPath stringByAppendingFormat:@",%lu", (unsigned long)i] : nil; + if (elementStore != nil && newIndexPath != nil) { + [elementStore setObject:childSnapshot forKey:(id)newIndexPath]; + } + rc = [self writeXmlWithRootElement:[FBXCElementSnapshotWrapper ensureWrapped:childSnapshot] + indexPath:newIndexPath + elementStore:elementStore + includedAttributes:includedAttributes + writer:writer]; + if (rc < 0) { + return rc; + } } } @@ -426,6 +560,30 @@ + (int)writeXmlWithRootElement:(id)root return 0; } ++ (id)snapshotWithRoot:(id)root + useNative:(BOOL)useNative +{ + if (![root isKindOfClass:XCUIElement.class]) { + return (id)root; + } + + if (useNative) { + return [(XCUIElement *)root fb_nativeSnapshot]; + } + return [root isKindOfClass:XCUIApplication.class] + ? [(XCUIElement *)root fb_standardSnapshot] + : [(XCUIElement *)root fb_customSnapshot]; +} + ++ (void)waitUntilStableWithElement:(id)root +{ + if ([root isKindOfClass:XCUIElement.class]) { + // If the app is not idle state while we retrieve the visiblity state + // then the snapshot retrieval operation might freeze and time out + [[(XCUIElement *)root application] fb_waitUntilStableWithTimeout:FBConfiguration.animationCoolOffTimeout]; + } +} + @end @@ -457,6 +615,11 @@ + (NSString *)valueForElement:(id)element + (int)recordWithWriter:(xmlTextWriterPtr)writer forElement:(id)element { NSString *value = [self valueForElement:element]; + return [self recordWithWriter:writer forValue:value]; +} + ++ (int)recordWithWriter:(xmlTextWriterPtr)writer forValue:(nullable NSString *)value +{ if (nil == value) { // Skip the attribute if the value equals to nil return 0; @@ -490,6 +653,11 @@ + (int)recordWithWriter:(xmlTextWriterPtr)writer forElement:(id)eleme FBHeightAttribute.class, FBIndexAttribute.class, FBHittableAttribute.class, + FBPlaceholderValueAttribute.class, + FBTraitsAttribute.class, + FBNativeFrameAttribute.class, + FBMinValueAttribute.class, + FBMaxValueAttribute.class, ]; } @@ -697,18 +865,90 @@ + (NSString *)name return kXMLIndexPathKey; } -+ (int)recordWithWriter:(xmlTextWriterPtr)writer forValue:(NSString *)value +@end + +@implementation FBApplicationBundleIdAttribute : FBElementAttribute + ++ (NSString *)name { - if (nil == value) { - // Skip the attribute if the value equals to nil - return 0; - } - int rc = xmlTextWriterWriteAttribute(writer, - (xmlChar *)[[FBXPath safeXmlStringWithString:[self name]] UTF8String], - (xmlChar *)[[FBXPath safeXmlStringWithString:value] UTF8String]); - if (rc < 0) { - [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterWriteAttribute(%@='%@'). Error code: %d", [self name], value, rc]; - } - return rc; + return @"bundleId"; } + +@end + +@implementation FBApplicationPidAttribute : FBElementAttribute + ++ (NSString *)name +{ + return @"processId"; +} + +@end + +@implementation FBPlaceholderValueAttribute + ++ (NSString *)name +{ + return @"placeholderValue"; +} + ++ (NSString *)valueForElement:(id)element +{ + return element.wdPlaceholderValue; +} +@end + +@implementation FBNativeFrameAttribute + ++ (NSString *)name +{ + return @"nativeFrame"; +} + ++ (NSString *)valueForElement:(id)element +{ + return NSStringFromCGRect(element.wdNativeFrame); +} +@end + +@implementation FBTraitsAttribute + ++ (NSString *)name +{ + return @"traits"; +} + ++ (NSString *)valueForElement:(id)element +{ + return element.wdTraits; +} + +@end + +@implementation FBMinValueAttribute + ++ (NSString *)name +{ + return @"minValue"; +} + ++ (NSString *)valueForElement:(id)element +{ + return [element.wdMinValue stringValue]; +} + +@end + +@implementation FBMaxValueAttribute + ++ (NSString *)name +{ + return @"maxValue"; +} + ++ (NSString *)valueForElement:(id)element +{ + return [element.wdMaxValue stringValue]; +} + @end diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/LRUCache/LRUCache.h b/WebDriverAgent/WebDriverAgentLib/Utilities/LRUCache/LRUCache.h index 70614235f6..dbfb4cfbc4 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/LRUCache/LRUCache.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/LRUCache/LRUCache.h @@ -54,6 +54,13 @@ NS_ASSUME_NONNULL_BEGIN */ - (NSArray *)allObjects; +/** + Removes the object associated with the specified key from the cache. + + @param key The key identifying the object to remove. + */ +- (void)removeObjectForKey:(id)key; + @end NS_ASSUME_NONNULL_END diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/LRUCache/LRUCache.m b/WebDriverAgent/WebDriverAgentLib/Utilities/LRUCache/LRUCache.m index 4f3a9ad0d2..731d744046 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/LRUCache/LRUCache.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/LRUCache/LRUCache.m @@ -135,4 +135,12 @@ - (void)alignSize } } +- (void)removeObjectForKey:(id)key +{ + LRUCacheNode *node = self.store[key]; + if (node != nil) { + [self removeNode:node]; + } +} + @end diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/NSPredicate+FBFormat.h b/WebDriverAgent/WebDriverAgentLib/Utilities/NSPredicate+FBFormat.h index 89ce4adeeb..a7ad285d53 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/NSPredicate+FBFormat.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/NSPredicate+FBFormat.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/NSPredicate+FBFormat.m b/WebDriverAgent/WebDriverAgentLib/Utilities/NSPredicate+FBFormat.m index 8a033e7076..65af951a09 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/NSPredicate+FBFormat.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/NSPredicate+FBFormat.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "NSPredicate+FBFormat.h" @@ -59,8 +58,10 @@ + (instancetype)fb_snapshotBlockPredicateWithPredicate:(NSPredicate *)input NSPredicate *wdPredicate = [self.class fb_formatSearchPredicate:input]; return [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary * _Nullable bindings) { - FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:evaluatedObject]; - return [wdPredicate evaluateWithObject:wrappedSnapshot]; + @autoreleasepool { + FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:evaluatedObject]; + return [wdPredicate evaluateWithObject:wrappedSnapshot]; + } }]; } diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/XCTestPrivateSymbols.h b/WebDriverAgent/WebDriverAgentLib/Utilities/XCTestPrivateSymbols.h index 87199ff35c..853ba414dc 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/XCTestPrivateSymbols.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/XCTestPrivateSymbols.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import @@ -19,6 +18,17 @@ extern NSString *FB_XCAXAIsVisibleAttributeName; extern NSNumber *FB_XCAXAIsElementAttribute; extern NSString *FB_XCAXAIsElementAttributeName; +/*! Accessibility identifier for visible frame attribute */ +extern NSString *FB_XCAXAVisibleFrameAttributeName; + +/*! Accessibility identifier для минимума */ +extern NSNumber *FB_XCAXACustomMinValueAttribute; +extern NSString *FB_XCAXACustomMinValueAttributeName; + +/*! Accessibility identifier для максимума */ +extern NSNumber *FB_XCAXACustomMaxValueAttribute; +extern NSString *FB_XCAXACustomMaxValueAttributeName; + /*! Getter for XCTest logger */ extern id (*XCDebugLogger)(void); diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/XCTestPrivateSymbols.m b/WebDriverAgent/WebDriverAgentLib/Utilities/XCTestPrivateSymbols.m index 861a78660b..244e3fdbf2 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/XCTestPrivateSymbols.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/XCTestPrivateSymbols.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCTestPrivateSymbols.h" @@ -18,6 +17,11 @@ NSString *FB_XCAXAIsVisibleAttributeName = @"XC_kAXXCAttributeIsVisible"; NSNumber *FB_XCAXAIsElementAttribute; NSString *FB_XCAXAIsElementAttributeName = @"XC_kAXXCAttributeIsElement"; +NSString *FB_XCAXAVisibleFrameAttributeName = @"XC_kAXXCAttributeVisibleFrame"; +NSNumber *FB_XCAXACustomMinValueAttribute; +NSString *FB_XCAXACustomMinValueAttributeName = @"XC_kAXXCAttributeMinValue"; +NSNumber *FB_XCAXACustomMaxValueAttribute; +NSString *FB_XCAXACustomMaxValueAttributeName = @"XC_kAXXCAttributeMaxValue"; void (*XCSetDebugLogger)(id ); id (*XCDebugLogger)(void); @@ -41,6 +45,16 @@ NSCAssert(FB_XCAXAIsVisibleAttribute != nil , @"Failed to retrieve FB_XCAXAIsVisibleAttribute", FB_XCAXAIsVisibleAttribute); NSCAssert(FB_XCAXAIsElementAttribute != nil , @"Failed to retrieve FB_XCAXAIsElementAttribute", FB_XCAXAIsElementAttribute); + + NSString *XC_kAXXCAttributeMinValue = *(NSString *__autoreleasing *)FBRetrieveXCTestSymbol([FB_XCAXACustomMinValueAttributeName UTF8String]); + NSString *XC_kAXXCAttributeMaxValue = *(NSString *__autoreleasing *)FBRetrieveXCTestSymbol([FB_XCAXACustomMaxValueAttributeName UTF8String]); + + NSArray *minMaxAttrs = XCAXAccessibilityAttributesForStringAttributes(@[XC_kAXXCAttributeMinValue, XC_kAXXCAttributeMaxValue]); + FB_XCAXACustomMinValueAttribute = minMaxAttrs[0]; + FB_XCAXACustomMaxValueAttribute = minMaxAttrs[1]; + + NSCAssert(FB_XCAXACustomMinValueAttribute != nil, @"Failed to retrieve FB_XCAXACustomMinValueAttribute", FB_XCAXACustomMinValueAttribute); + NSCAssert(FB_XCAXACustomMaxValueAttribute != nil, @"Failed to retrieve FB_XCAXACustomMaxValueAttribute", FB_XCAXACustomMaxValueAttribute); } void *FBRetrieveXCTestSymbol(const char *name) @@ -73,7 +87,9 @@ dispatch_once(&onceCustomAttributeNamesToken, ^{ customNames = @[ FB_XCAXAIsVisibleAttributeName, - FB_XCAXAIsElementAttributeName + FB_XCAXAIsElementAttributeName, + FB_XCAXACustomMinValueAttributeName, + FB_XCAXACustomMaxValueAttributeName ]; }); return customNames; diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/XCUIApplicationProcessDelay.h b/WebDriverAgent/WebDriverAgentLib/Utilities/XCUIApplicationProcessDelay.h index ed11a9a51c..3cbb5efb0c 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/XCUIApplicationProcessDelay.h +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/XCUIApplicationProcessDelay.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentLib/Utilities/XCUIApplicationProcessDelay.m b/WebDriverAgent/WebDriverAgentLib/Utilities/XCUIApplicationProcessDelay.m index 86c1fc45cb..e41a0a78c0 100644 --- a/WebDriverAgent/WebDriverAgentLib/Utilities/XCUIApplicationProcessDelay.m +++ b/WebDriverAgent/WebDriverAgentLib/Utilities/XCUIApplicationProcessDelay.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIApplicationProcessDelay.h" diff --git a/WebDriverAgent/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncSocket.m b/WebDriverAgent/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncSocket.m old mode 100644 new mode 100755 index de18142fe9..480c116a6e --- a/WebDriverAgent/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncSocket.m +++ b/WebDriverAgent/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncSocket.m @@ -1,6 +1,6 @@ // // GCDAsyncSocket.m -// +// // This class is in the public domain. // Originally created by Robbie Hanson in Q4 2010. // Updated and maintained by Deusty LLC and the Apple development community. @@ -11,7 +11,17 @@ #import "GCDAsyncSocket.h" #if TARGET_OS_IPHONE -#import +#import +#import +// Note: CFStream APIs are still used for TLS support and are part of CoreFoundation +// CFStream SSL constants are needed only for the legacy CFStream TLS path (when GCDAsyncSocketUseCFStreamForTLS is set) +// The default path uses SecureTransport which doesn't require these constants +// Declare SSL constants as extern to avoid importing deprecated CFNetwork framework +// These symbols are linked at runtime from CFNetwork framework +extern const CFStringRef kCFStreamPropertySSLSettings; +extern const CFStringRef kCFStreamSSLPeerName; +extern const CFStringRef kCFStreamSSLCertificates; +extern const CFStringRef kCFStreamSSLIsServer; #endif #import @@ -48,7 +58,7 @@ // Logging uses the CocoaLumberjack framework (which is also GCD based). // https://github.com/robbiehanson/CocoaLumberjack -// +// // It allows us to do a lot of logging without significantly slowing down the code. #import "DDLog.h" @@ -184,16 +194,16 @@ * than is being requested by current read request. * In this case we slurp up all data from the socket (to minimize sys calls), * and store additional yet unread data in a "prebuffer". - * + * * The prebuffer is entirely drained before we read from the socket again. * In other words, a large chunk of data is written is written to the prebuffer. * The prebuffer is then drained via a series of one or more reads (for subsequent read request(s)). - * + * * A ring buffer was once used for this purpose. * But a ring buffer takes up twice as much memory as needed (double the size for mirroring). * In fact, it generally takes up more than twice the needed size as everything has to be rounded up to vm_page_size. * And since the prebuffer is always completely drained after being written to, a full ring buffer isn't needed. - * + * * The current design is very simple and straight-forward, while also keeping memory requirements lower. **/ @@ -201,7 +211,7 @@ @interface GCDAsyncSocketPreBuffer : NSObject { uint8_t *preBuffer; size_t preBufferSize; - + uint8_t *readPointer; uint8_t *writePointer; } @@ -242,7 +252,7 @@ - (instancetype)initWithCapacity:(size_t)numBytes { preBufferSize = numBytes; preBuffer = malloc(preBufferSize); - + readPointer = preBuffer; writePointer = preBuffer; } @@ -258,20 +268,20 @@ - (void)dealloc - (void)ensureCapacityForWrite:(size_t)numBytes { size_t availableSpace = [self availableSpace]; - + if (numBytes > availableSpace) { size_t additionalBytes = numBytes - availableSpace; - + size_t newPreBufferSize = preBufferSize + additionalBytes; uint8_t *newPreBuffer = realloc(preBuffer, newPreBufferSize); - + size_t readPointerOffset = readPointer - preBuffer; size_t writePointerOffset = writePointer - preBuffer; - + preBuffer = newPreBuffer; preBufferSize = newPreBufferSize; - + readPointer = preBuffer + readPointerOffset; writePointer = preBuffer + writePointerOffset; } @@ -296,7 +306,7 @@ - (void)getReadBuffer:(uint8_t **)bufferPtr availableBytes:(size_t *)availableBy - (void)didRead:(size_t)bytesRead { readPointer += bytesRead; - + if (readPointer == writePointer) { // The prebuffer has been drained. Reset pointers. @@ -404,7 +414,7 @@ - (instancetype)initWithData:(NSMutableData *)d readLength = l; term = [e copy]; tag = i; - + if (d) { buffer = d; @@ -418,7 +428,7 @@ - (instancetype)initWithData:(NSMutableData *)d buffer = [[NSMutableData alloc] initWithLength:readLength]; else buffer = [[NSMutableData alloc] initWithLength:0]; - + startOffset = 0; bufferOwner = YES; originalBufferLength = 0; @@ -434,13 +444,13 @@ - (void)ensureCapacityForAdditionalDataOfLength:(NSUInteger)bytesToRead { NSUInteger buffSize = [buffer length]; NSUInteger buffUsed = startOffset + bytesDone; - + NSUInteger buffSpace = buffSize - buffUsed; - + if (bytesToRead > buffSpace) { NSUInteger buffInc = bytesToRead - buffSpace; - + [buffer increaseLengthBy:buffInc]; } } @@ -448,23 +458,23 @@ - (void)ensureCapacityForAdditionalDataOfLength:(NSUInteger)bytesToRead /** * This method is used when we do NOT know how much data is available to be read from the socket. * This method returns the default value unless it exceeds the specified readLength or maxLength. - * + * * Furthermore, the shouldPreBuffer decision is based upon the packet type, * and whether the returned value would fit in the current buffer without requiring a resize of the buffer. **/ - (NSUInteger)optimalReadLengthWithDefault:(NSUInteger)defaultValue shouldPreBuffer:(BOOL *)shouldPreBufferPtr { NSUInteger result; - + if (readLength > 0) { // Read a specific length of data result = readLength - bytesDone; - + // There is no need to prebuffer since we know exactly how much data we need to read. // Even if the buffer isn't currently big enough to fit this amount of data, // it would have to be resized eventually anyway. - + if (shouldPreBufferPtr) *shouldPreBufferPtr = NO; } @@ -472,88 +482,88 @@ - (NSUInteger)optimalReadLengthWithDefault:(NSUInteger)defaultValue shouldPreBuf { // Either reading until we find a specified terminator, // or we're simply reading all available data. - // + // // In other words, one of: - // + // // - readDataToData packet // - readDataWithTimeout packet - + if (maxLength > 0) result = MIN(defaultValue, (maxLength - bytesDone)); else result = defaultValue; - + // Since we don't know the size of the read in advance, // the shouldPreBuffer decision is based upon whether the returned value would fit // in the current buffer without requiring a resize of the buffer. - // + // // This is because, in all likelyhood, the amount read from the socket will be less than the default value. // Thus we should avoid over-allocating the read buffer when we can simply use the pre-buffer instead. - + if (shouldPreBufferPtr) { NSUInteger buffSize = [buffer length]; NSUInteger buffUsed = startOffset + bytesDone; - + NSUInteger buffSpace = buffSize - buffUsed; - + if (buffSpace >= result) *shouldPreBufferPtr = NO; else *shouldPreBufferPtr = YES; } } - + return result; } /** * For read packets without a set terminator, returns the amount of data * that can be read without exceeding the readLength or maxLength. - * + * * The given parameter indicates the number of bytes estimated to be available on the socket, * which is taken into consideration during the calculation. - * + * * The given hint MUST be greater than zero. **/ - (NSUInteger)readLengthForNonTermWithHint:(NSUInteger)bytesAvailable { NSAssert(term == nil, @"This method does not apply to term reads"); NSAssert(bytesAvailable > 0, @"Invalid parameter: bytesAvailable"); - + if (readLength > 0) { // Read a specific length of data - + return MIN(bytesAvailable, (readLength - bytesDone)); - + // No need to avoid resizing the buffer. // If the user provided their own buffer, // and told us to read a certain length of data that exceeds the size of the buffer, // then it is clear that our code will resize the buffer during the read operation. - // + // // This method does not actually do any resizing. // The resizing will happen elsewhere if needed. } else { // Read all available data - + NSUInteger result = bytesAvailable; - + if (maxLength > 0) { result = MIN(result, (maxLength - bytesDone)); } - + // No need to avoid resizing the buffer. // If the user provided their own buffer, // and told us to read all available data without giving us a maxLength, // then it is clear that our code might resize the buffer during the read operation. - // + // // This method does not actually do any resizing. // The resizing will happen elsewhere if needed. - + return result; } } @@ -561,10 +571,10 @@ - (NSUInteger)readLengthForNonTermWithHint:(NSUInteger)bytesAvailable /** * For read packets with a set terminator, returns the amount of data * that can be read without exceeding the maxLength. - * + * * The given parameter indicates the number of bytes estimated to be available on the socket, * which is taken into consideration during the calculation. - * + * * To optimize memory allocations, mem copies, and mem moves * the shouldPreBuffer boolean value will indicate if the data should be read into a prebuffer first, * or if the data can be read directly into the read packet's buffer. @@ -573,41 +583,41 @@ - (NSUInteger)readLengthForTermWithHint:(NSUInteger)bytesAvailable shouldPreBuff { NSAssert(term != nil, @"This method does not apply to non-term reads"); NSAssert(bytesAvailable > 0, @"Invalid parameter: bytesAvailable"); - - + + NSUInteger result = bytesAvailable; - + if (maxLength > 0) { result = MIN(result, (maxLength - bytesDone)); } - + // Should the data be read into the read packet's buffer, or into a pre-buffer first? - // + // // One would imagine the preferred option is the faster one. // So which one is faster? - // + // // Reading directly into the packet's buffer requires: // 1. Possibly resizing packet buffer (malloc/realloc) // 2. Filling buffer (read) // 3. Searching for term (memcmp) // 4. Possibly copying overflow into prebuffer (malloc/realloc, memcpy) - // + // // Reading into prebuffer first: // 1. Possibly resizing prebuffer (malloc/realloc) // 2. Filling buffer (read) // 3. Searching for term (memcmp) // 4. Copying underflow into packet buffer (malloc/realloc, memcpy) // 5. Removing underflow from prebuffer (memmove) - // + // // Comparing the performance of the two we can see that reading // data into the prebuffer first is slower due to the extra memove. - // + // // However: // The implementation of NSMutableData is open source via core foundation's CFMutableData. // Decreasing the length of a mutable data object doesn't cause a realloc. // In other words, the capacity of a mutable data object can grow, but doesn't shrink. - // + // // This means the prebuffer will rarely need a realloc. // The packet buffer, on the other hand, may often need a realloc. // This is especially true if we are the buffer owner. @@ -615,24 +625,24 @@ - (NSUInteger)readLengthForTermWithHint:(NSUInteger)bytesAvailable shouldPreBuff // and then moving the overflow into the prebuffer, // then we're consistently over-allocating memory for each term read. // And now we get into a bit of a tradeoff between speed and memory utilization. - // + // // The end result is that the two perform very similarly. // And we can answer the original question very simply by another means. - // + // // If we can read all the data directly into the packet's buffer without resizing it first, // then we do so. Otherwise we use the prebuffer. - + if (shouldPreBufferPtr) { NSUInteger buffSize = [buffer length]; NSUInteger buffUsed = startOffset + bytesDone; - + if ((buffSize - buffUsed) >= result) *shouldPreBufferPtr = NO; else *shouldPreBufferPtr = YES; } - + return result; } @@ -640,103 +650,103 @@ - (NSUInteger)readLengthForTermWithHint:(NSUInteger)bytesAvailable shouldPreBuff * For read packets with a set terminator, * returns the amount of data that can be read from the given preBuffer, * without going over a terminator or the maxLength. - * + * * It is assumed the terminator has not already been read. **/ - (NSUInteger)readLengthForTermWithPreBuffer:(GCDAsyncSocketPreBuffer *)preBuffer found:(BOOL *)foundPtr { NSAssert(term != nil, @"This method does not apply to non-term reads"); NSAssert([preBuffer availableBytes] > 0, @"Invoked with empty pre buffer!"); - + // We know that the terminator, as a whole, doesn't exist in our own buffer. // But it is possible that a _portion_ of it exists in our buffer. // So we're going to look for the terminator starting with a portion of our own buffer. - // + // // Example: - // + // // term length = 3 bytes // bytesDone = 5 bytes // preBuffer length = 5 bytes - // + // // If we append the preBuffer to our buffer, // it would look like this: - // + // // --------------------- // |B|B|B|B|B|P|P|P|P|P| // --------------------- - // + // // So we start our search here: - // + // // --------------------- // |B|B|B|B|B|P|P|P|P|P| // -------^-^-^--------- - // + // // And move forwards... - // + // // --------------------- // |B|B|B|B|B|P|P|P|P|P| // ---------^-^-^------- - // + // // Until we find the terminator or reach the end. - // + // // --------------------- // |B|B|B|B|B|P|P|P|P|P| // ---------------^-^-^- - + BOOL found = NO; - + NSUInteger termLength = [term length]; NSUInteger preBufferLength = [preBuffer availableBytes]; - + if ((bytesDone + preBufferLength) < termLength) { // Not enough data for a full term sequence yet return preBufferLength; } - + NSUInteger maxPreBufferLength; if (maxLength > 0) { maxPreBufferLength = MIN(preBufferLength, (maxLength - bytesDone)); - + // Note: maxLength >= termLength } else { maxPreBufferLength = preBufferLength; } - + #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wvla" uint8_t seq[termLength]; #pragma clang diagnostic pop const void *termBuf = [term bytes]; - + NSUInteger bufLen = MIN(bytesDone, (termLength - 1)); uint8_t *buf = (uint8_t *)[buffer mutableBytes] + startOffset + bytesDone - bufLen; - + NSUInteger preLen = termLength - bufLen; const uint8_t *pre = [preBuffer readBuffer]; - + NSUInteger loopCount = bufLen + maxPreBufferLength - termLength + 1; // Plus one. See example above. - + NSUInteger result = maxPreBufferLength; - + NSUInteger i; for (i = 0; i < loopCount; i++) { if (bufLen > 0) { // Combining bytes from buffer and preBuffer - + memcpy(seq, buf, bufLen); memcpy(seq + bufLen, pre, preLen); - + if (memcmp(seq, termBuf, termLength) == 0) { result = preLen; found = YES; break; } - + buf++; bufLen--; preLen++; @@ -744,22 +754,22 @@ - (NSUInteger)readLengthForTermWithPreBuffer:(GCDAsyncSocketPreBuffer *)preBuffe else { // Comparing directly from preBuffer - + if (memcmp(pre, termBuf, termLength) == 0) { NSUInteger preOffset = pre - [preBuffer readBuffer]; // pointer arithmetic - + result = preOffset + termLength; found = YES; break; } - + pre++; } } - + // There is no need to avoid resizing the buffer in this particular situation. - + if (foundPtr) *foundPtr = found; return result; } @@ -767,12 +777,12 @@ - (NSUInteger)readLengthForTermWithPreBuffer:(GCDAsyncSocketPreBuffer *)preBuffe /** * For read packets with a set terminator, scans the packet buffer for the term. * It is assumed the terminator had not been fully read prior to the new bytes. - * + * * If the term is found, the number of excess bytes after the term are returned. * If the term is not found, this method will return -1. - * + * * Note: A return value of zero means the term was found at the very end. - * + * * Prerequisites: * The given number of bytes have been added to the end of our buffer. * Our bytesDone variable has NOT been changed due to the prebuffered bytes. @@ -780,33 +790,33 @@ - (NSUInteger)readLengthForTermWithPreBuffer:(GCDAsyncSocketPreBuffer *)preBuffe - (NSInteger)searchForTermAfterPreBuffering:(ssize_t)numBytes { NSAssert(term != nil, @"This method does not apply to non-term reads"); - + // The implementation of this method is very similar to the above method. // See the above method for a discussion of the algorithm used here. - + uint8_t *buff = [buffer mutableBytes]; NSUInteger buffLength = bytesDone + numBytes; - + const void *termBuff = [term bytes]; NSUInteger termLength = [term length]; - + // Note: We are dealing with unsigned integers, // so make sure the math doesn't go below zero. - + NSUInteger i = ((buffLength - numBytes) >= termLength) ? (buffLength - numBytes - termLength + 1) : 0; - + while (i + termLength <= buffLength) { uint8_t *subBuffer = buff + startOffset + i; - + if (memcmp(subBuffer, termBuff, termLength) == 0) { return buffLength - (i + termLength); } - + i++; } - + return -1; } @@ -900,10 +910,10 @@ @implementation GCDAsyncSocket { uint32_t flags; uint16_t config; - + __weak id delegate; dispatch_queue_t delegateQueue; - + int socket4FD; int socket6FD; int socketUN; @@ -912,9 +922,9 @@ @implementation GCDAsyncSocket NSData * connectInterface4; NSData * connectInterface6; NSData * connectInterfaceUN; - + dispatch_queue_t socketQueue; - + dispatch_source_t accept4Source; dispatch_source_t accept6Source; dispatch_source_t acceptUNSource; @@ -923,17 +933,17 @@ @implementation GCDAsyncSocket dispatch_source_t writeSource; dispatch_source_t readTimer; dispatch_source_t writeTimer; - + NSMutableArray *readQueue; NSMutableArray *writeQueue; - + GCDAsyncReadPacket *currentRead; GCDAsyncWritePacket *currentWrite; - + unsigned long socketFDBytesAvailable; - + GCDAsyncSocketPreBuffer *preBuffer; - + #if TARGET_OS_IPHONE CFStreamClientContext streamContext; CFReadStreamRef readStream; @@ -944,9 +954,9 @@ @implementation GCDAsyncSocket size_t sslWriteCachedLength; OSStatus sslErrCode; OSStatus lastSSLHandshakeError; - + void *IsOnSocketQueueOrTargetQueueKey; - + id userData; NSTimeInterval alternateAddressDelay; } @@ -972,17 +982,17 @@ - (instancetype)initWithDelegate:(id)aDelegate delegateQ { delegate = aDelegate; delegateQueue = dq; - + #if !OS_OBJECT_USE_OBJC if (dq) dispatch_retain(dq); #endif - + socket4FD = SOCKET_NULL; socket6FD = SOCKET_NULL; socketUN = SOCKET_NULL; socketUrl = nil; stateIndex = 0; - + if (sq) { NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), @@ -991,7 +1001,7 @@ - (instancetype)initWithDelegate:(id)aDelegate delegateQ @"The given socketQueue parameter must not be a concurrent queue."); NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), @"The given socketQueue parameter must not be a concurrent queue."); - + socketQueue = sq; #if !OS_OBJECT_USE_OBJC dispatch_retain(sq); @@ -1001,7 +1011,7 @@ - (instancetype)initWithDelegate:(id)aDelegate delegateQ { socketQueue = dispatch_queue_create([GCDAsyncSocketQueueName UTF8String], NULL); } - + // The dispatch_queue_set_specific() and dispatch_get_specific() functions take a "void *key" parameter. // From the documentation: // @@ -1018,18 +1028,18 @@ - (instancetype)initWithDelegate:(id)aDelegate delegateQ // So we're going to make it so it doesn't matter if we use the '&' or not, // by assigning the value of the ivar to the address of the ivar. // Thus: IsOnSocketQueueOrTargetQueueKey == &IsOnSocketQueueOrTargetQueueKey; - + IsOnSocketQueueOrTargetQueueKey = &IsOnSocketQueueOrTargetQueueKey; - + void *nonNullUnusedPointer = (__bridge void *)self; dispatch_queue_set_specific(socketQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL); - + readQueue = [[NSMutableArray alloc] initWithCapacity:5]; currentRead = nil; - + writeQueue = [[NSMutableArray alloc] initWithCapacity:5]; currentWrite = nil; - + preBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)]; alternateAddressDelay = 0.3; } @@ -1039,11 +1049,11 @@ - (instancetype)initWithDelegate:(id)aDelegate delegateQ - (void)dealloc { LogInfo(@"%@ - %@ (start)", THIS_METHOD, self); - + // Set dealloc flag. // This is used by closeWithError to ensure we don't accidentally retain ourself. flags |= kDealloc; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { [self closeWithError:nil]; @@ -1054,19 +1064,19 @@ - (void)dealloc [self closeWithError:nil]; }); } - + delegate = nil; - + #if !OS_OBJECT_USE_OBJC if (delegateQueue) dispatch_release(delegateQueue); #endif delegateQueue = NULL; - + #if !OS_OBJECT_USE_OBJC if (socketQueue) dispatch_release(socketQueue); #endif socketQueue = NULL; - + LogInfo(@"%@ - %@ (finish)", THIS_METHOD, self); } @@ -1094,7 +1104,7 @@ + (nullable instancetype)socketFromConnectedSocketFD:(int)socketFD delegate:(nul NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketOtherError", @"GCDAsyncSocket", [NSBundle mainBundle], @"Attempt to create socket from socket FD failed. getpeername() failed", nil); - + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; innerError = [NSError errorWithDomain:GCDAsyncSocketErrorDomain @@ -1102,7 +1112,7 @@ + (nullable instancetype)socketFromConnectedSocketFD:(int)socketFD delegate:(nul userInfo:userInfo]; return; } - + if (addr.sa_family == AF_INET) { socket->socket4FD = socketFD; @@ -1116,9 +1126,9 @@ + (nullable instancetype)socketFromConnectedSocketFD:(int)socketFD delegate:(nul NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketOtherError", @"GCDAsyncSocket", [NSBundle mainBundle], @"Attempt to create socket from socket FD failed. socket FD is neither IPv4 nor IPv6", nil); - + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; - + innerError = [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketOtherError userInfo:userInfo]; @@ -1148,11 +1158,11 @@ - (id)delegate else { __block id result; - + dispatch_sync(socketQueue, ^{ result = self->delegate; }); - + return result; } } @@ -1162,7 +1172,7 @@ - (void)setDelegate:(id)newDelegate synchronously:(BOOL)synchronously dispatch_block_t block = ^{ self->delegate = newDelegate; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { block(); } @@ -1193,11 +1203,11 @@ - (dispatch_queue_t)delegateQueue else { __block dispatch_queue_t result; - + dispatch_sync(socketQueue, ^{ result = self->delegateQueue; }); - + return result; } } @@ -1205,15 +1215,15 @@ - (dispatch_queue_t)delegateQueue - (void)setDelegateQueue:(dispatch_queue_t)newDelegateQueue synchronously:(BOOL)synchronously { dispatch_block_t block = ^{ - + #if !OS_OBJECT_USE_OBJC if (self->delegateQueue) dispatch_release(self->delegateQueue); if (newDelegateQueue) dispatch_retain(newDelegateQueue); #endif - + self->delegateQueue = newDelegateQueue; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { block(); } @@ -1246,12 +1256,12 @@ - (void)getDelegate:(id *)delegatePtr delegateQueue:(dis { __block id dPtr = NULL; __block dispatch_queue_t dqPtr = NULL; - + dispatch_sync(socketQueue, ^{ dPtr = self->delegate; dqPtr = self->delegateQueue; }); - + if (delegatePtr) *delegatePtr = dPtr; if (delegateQueuePtr) *delegateQueuePtr = dqPtr; } @@ -1260,17 +1270,17 @@ - (void)getDelegate:(id *)delegatePtr delegateQueue:(dis - (void)setDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue synchronously:(BOOL)synchronously { dispatch_block_t block = ^{ - + self->delegate = newDelegate; - + #if !OS_OBJECT_USE_OBJC if (self->delegateQueue) dispatch_release(self->delegateQueue); if (newDelegateQueue) dispatch_retain(newDelegateQueue); #endif - + self->delegateQueue = newDelegateQueue; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { block(); } @@ -1295,7 +1305,7 @@ - (void)synchronouslySetDelegate:(id)newDelegate delegat - (BOOL)isIPv4Enabled { // Note: YES means kIPv4Disabled is OFF - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { return ((config & kIPv4Disabled) == 0); @@ -1303,11 +1313,11 @@ - (BOOL)isIPv4Enabled else { __block BOOL result; - + dispatch_sync(socketQueue, ^{ result = ((self->config & kIPv4Disabled) == 0); }); - + return result; } } @@ -1315,15 +1325,15 @@ - (BOOL)isIPv4Enabled - (void)setIPv4Enabled:(BOOL)flag { // Note: YES means kIPv4Disabled is OFF - + dispatch_block_t block = ^{ - + if (flag) self->config &= ~kIPv4Disabled; else self->config |= kIPv4Disabled; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else @@ -1333,7 +1343,7 @@ - (void)setIPv4Enabled:(BOOL)flag - (BOOL)isIPv6Enabled { // Note: YES means kIPv6Disabled is OFF - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { return ((config & kIPv6Disabled) == 0); @@ -1341,11 +1351,11 @@ - (BOOL)isIPv6Enabled else { __block BOOL result; - + dispatch_sync(socketQueue, ^{ result = ((self->config & kIPv6Disabled) == 0); }); - + return result; } } @@ -1353,15 +1363,15 @@ - (BOOL)isIPv6Enabled - (void)setIPv6Enabled:(BOOL)flag { // Note: YES means kIPv6Disabled is OFF - + dispatch_block_t block = ^{ - + if (flag) self->config &= ~kIPv6Disabled; else self->config |= kIPv6Disabled; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else @@ -1371,7 +1381,7 @@ - (void)setIPv6Enabled:(BOOL)flag - (BOOL)isIPv4PreferredOverIPv6 { // Note: YES means kPreferIPv6 is OFF - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { return ((config & kPreferIPv6) == 0); @@ -1379,11 +1389,11 @@ - (BOOL)isIPv4PreferredOverIPv6 else { __block BOOL result; - + dispatch_sync(socketQueue, ^{ result = ((self->config & kPreferIPv6) == 0); }); - + return result; } } @@ -1391,15 +1401,15 @@ - (BOOL)isIPv4PreferredOverIPv6 - (void)setIPv4PreferredOverIPv6:(BOOL)flag { // Note: YES means kPreferIPv6 is OFF - + dispatch_block_t block = ^{ - + if (flag) self->config &= ~kPreferIPv6; else self->config |= kPreferIPv6; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else @@ -1431,30 +1441,30 @@ - (void) setAlternateAddressDelay:(NSTimeInterval)delay { - (id)userData { __block id result = nil; - + dispatch_block_t block = ^{ - + result = self->userData; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); - + return result; } - (void)setUserData:(id)arbitraryUserData { dispatch_block_t block = ^{ - + if (self->userData != arbitraryUserData) { self->userData = arbitraryUserData; } }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else @@ -1473,189 +1483,189 @@ - (BOOL)acceptOnPort:(uint16_t)port error:(NSError **)errPtr - (BOOL)acceptOnInterface:(NSString *)inInterface port:(uint16_t)port error:(NSError **)errPtr { LogTrace(); - + // Just in-case interface parameter is immutable. NSString *interface = [inInterface copy]; - + __block BOOL result = NO; __block NSError *err = nil; - + // CreateSocket Block // This block will be invoked within the dispatch block below. - + int(^createSocket)(int, NSData*) = ^int (int domain, NSData *interfaceAddr) { - + int socketFD = socket(domain, SOCK_STREAM, 0); - + if (socketFD == SOCKET_NULL) { NSString *reason = @"Error in socket() function"; err = [self errorWithErrno:errno reason:reason]; - + return SOCKET_NULL; } - + int status; - + // Set socket options - + status = fcntl(socketFD, F_SETFL, O_NONBLOCK); if (status == -1) { NSString *reason = @"Error enabling non-blocking IO on socket (fcntl)"; err = [self errorWithErrno:errno reason:reason]; - + LogVerbose(@"close(socketFD)"); close(socketFD); return SOCKET_NULL; } - + int reuseOn = 1; status = setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); if (status == -1) { NSString *reason = @"Error enabling address reuse (setsockopt)"; err = [self errorWithErrno:errno reason:reason]; - + LogVerbose(@"close(socketFD)"); close(socketFD); return SOCKET_NULL; } - + // Bind socket - + status = bind(socketFD, (const struct sockaddr *)[interfaceAddr bytes], (socklen_t)[interfaceAddr length]); if (status == -1) { NSString *reason = @"Error in bind() function"; err = [self errorWithErrno:errno reason:reason]; - + LogVerbose(@"close(socketFD)"); close(socketFD); return SOCKET_NULL; } - + // Listen - + status = listen(socketFD, 1024); if (status == -1) { NSString *reason = @"Error in listen() function"; err = [self errorWithErrno:errno reason:reason]; - + LogVerbose(@"close(socketFD)"); close(socketFD); return SOCKET_NULL; } - + return socketFD; }; - + // Create dispatch block and run on socketQueue - + dispatch_block_t block = ^{ @autoreleasepool { - + if (self->delegate == nil) // Must have delegate set { NSString *msg = @"Attempting to accept without a delegate. Set a delegate first."; err = [self badConfigError:msg]; - + return_from_block; } - + if (self->delegateQueue == NULL) // Must have delegate queue set { NSString *msg = @"Attempting to accept without a delegate queue. Set a delegate queue first."; err = [self badConfigError:msg]; - + return_from_block; } - + BOOL isIPv4Disabled = (self->config & kIPv4Disabled) ? YES : NO; BOOL isIPv6Disabled = (self->config & kIPv6Disabled) ? YES : NO; - + if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled { NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first."; err = [self badConfigError:msg]; - + return_from_block; } - + if (![self isDisconnected]) // Must be disconnected { NSString *msg = @"Attempting to accept while connected or accepting connections. Disconnect first."; err = [self badConfigError:msg]; - + return_from_block; } - + // Clear queues (spurious read/write requests post disconnect) [self->readQueue removeAllObjects]; [self->writeQueue removeAllObjects]; - + // Resolve interface from description - + NSMutableData *interface4 = nil; NSMutableData *interface6 = nil; - + [self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:port]; - + if ((interface4 == nil) && (interface6 == nil)) { NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; err = [self badParamError:msg]; - + return_from_block; } - + if (isIPv4Disabled && (interface6 == nil)) { NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6."; err = [self badParamError:msg]; - + return_from_block; } - + if (isIPv6Disabled && (interface4 == nil)) { NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4."; err = [self badParamError:msg]; - + return_from_block; } - + BOOL enableIPv4 = !isIPv4Disabled && (interface4 != nil); BOOL enableIPv6 = !isIPv6Disabled && (interface6 != nil); - + // Create sockets, configure, bind, and listen - + if (enableIPv4) { LogVerbose(@"Creating IPv4 socket"); self->socket4FD = createSocket(AF_INET, interface4); - + if (self->socket4FD == SOCKET_NULL) { return_from_block; } } - + if (enableIPv6) { LogVerbose(@"Creating IPv6 socket"); - + if (enableIPv4 && (port == 0)) { // No specific port was specified, so we allowed the OS to pick an available port for us. // Now we need to make sure the IPv6 socket listens on the same port as the IPv4 socket. - + struct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)[interface6 mutableBytes]; addr6->sin6_port = htons([self localPort4]); } - + self->socket6FD = createSocket(AF_INET6, interface6); - + if (self->socket6FD == SOCKET_NULL) { if (self->socket4FD != SOCKET_NULL) @@ -1664,241 +1674,241 @@ - (BOOL)acceptOnInterface:(NSString *)inInterface port:(uint16_t)port error:(NSE close(self->socket4FD); self->socket4FD = SOCKET_NULL; } - + return_from_block; } } - + // Create accept sources - + if (enableIPv4) { self->accept4Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, self->socket4FD, 0, self->socketQueue); - + int socketFD = self->socket4FD; dispatch_source_t acceptSource = self->accept4Source; - + __weak GCDAsyncSocket *weakSelf = self; - + dispatch_source_set_event_handler(self->accept4Source, ^{ @autoreleasepool { #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" - + __strong GCDAsyncSocket *strongSelf = weakSelf; if (strongSelf == nil) return_from_block; - + LogVerbose(@"event4Block"); - + unsigned long i = 0; unsigned long numPendingConnections = dispatch_source_get_data(acceptSource); - + LogVerbose(@"numPendingConnections: %lu", numPendingConnections); - + while ([strongSelf doAccept:socketFD] && (++i < numPendingConnections)); - + #pragma clang diagnostic pop }}); - - + + dispatch_source_set_cancel_handler(self->accept4Source, ^{ #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" - + #if !OS_OBJECT_USE_OBJC LogVerbose(@"dispatch_release(accept4Source)"); dispatch_release(acceptSource); #endif - + LogVerbose(@"close(socket4FD)"); close(socketFD); - + #pragma clang diagnostic pop }); - + LogVerbose(@"dispatch_resume(accept4Source)"); dispatch_resume(self->accept4Source); } - + if (enableIPv6) { self->accept6Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, self->socket6FD, 0, self->socketQueue); - + int socketFD = self->socket6FD; dispatch_source_t acceptSource = self->accept6Source; - + __weak GCDAsyncSocket *weakSelf = self; - + dispatch_source_set_event_handler(self->accept6Source, ^{ @autoreleasepool { #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" - + __strong GCDAsyncSocket *strongSelf = weakSelf; if (strongSelf == nil) return_from_block; - + LogVerbose(@"event6Block"); - + unsigned long i = 0; unsigned long numPendingConnections = dispatch_source_get_data(acceptSource); - + LogVerbose(@"numPendingConnections: %lu", numPendingConnections); - + while ([strongSelf doAccept:socketFD] && (++i < numPendingConnections)); - + #pragma clang diagnostic pop }}); - + dispatch_source_set_cancel_handler(self->accept6Source, ^{ #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" - + #if !OS_OBJECT_USE_OBJC LogVerbose(@"dispatch_release(accept6Source)"); dispatch_release(acceptSource); #endif - + LogVerbose(@"close(socket6FD)"); close(socketFD); - + #pragma clang diagnostic pop }); - + LogVerbose(@"dispatch_resume(accept6Source)"); dispatch_resume(self->accept6Source); } - + self->flags |= kSocketStarted; - + result = YES; }}; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); - + if (result == NO) { LogInfo(@"Error in accept: %@", err); - + if (errPtr) *errPtr = err; } - + return result; } - (BOOL)acceptOnUrl:(NSURL *)url error:(NSError **)errPtr { LogTrace(); - + __block BOOL result = NO; __block NSError *err = nil; - + // CreateSocket Block // This block will be invoked within the dispatch block below. - + int(^createSocket)(int, NSData*) = ^int (int domain, NSData *interfaceAddr) { - + int socketFD = socket(domain, SOCK_STREAM, 0); - + if (socketFD == SOCKET_NULL) { NSString *reason = @"Error in socket() function"; err = [self errorWithErrno:errno reason:reason]; - + return SOCKET_NULL; } - + int status; - + // Set socket options - + status = fcntl(socketFD, F_SETFL, O_NONBLOCK); if (status == -1) { NSString *reason = @"Error enabling non-blocking IO on socket (fcntl)"; err = [self errorWithErrno:errno reason:reason]; - + LogVerbose(@"close(socketFD)"); close(socketFD); return SOCKET_NULL; } - + int reuseOn = 1; status = setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); if (status == -1) { NSString *reason = @"Error enabling address reuse (setsockopt)"; err = [self errorWithErrno:errno reason:reason]; - + LogVerbose(@"close(socketFD)"); close(socketFD); return SOCKET_NULL; } - + // Bind socket - + status = bind(socketFD, (const struct sockaddr *)[interfaceAddr bytes], (socklen_t)[interfaceAddr length]); if (status == -1) { NSString *reason = @"Error in bind() function"; err = [self errorWithErrno:errno reason:reason]; - + LogVerbose(@"close(socketFD)"); close(socketFD); return SOCKET_NULL; } - + // Listen - + status = listen(socketFD, 1024); if (status == -1) { NSString *reason = @"Error in listen() function"; err = [self errorWithErrno:errno reason:reason]; - + LogVerbose(@"close(socketFD)"); close(socketFD); return SOCKET_NULL; } - + return socketFD; }; - + // Create dispatch block and run on socketQueue - + dispatch_block_t block = ^{ @autoreleasepool { - + if (self->delegate == nil) // Must have delegate set { NSString *msg = @"Attempting to accept without a delegate. Set a delegate first."; err = [self badConfigError:msg]; - + return_from_block; } - + if (self->delegateQueue == NULL) // Must have delegate queue set { NSString *msg = @"Attempting to accept without a delegate queue. Set a delegate queue first."; err = [self badConfigError:msg]; - + return_from_block; } - + if (![self isDisconnected]) // Must be disconnected { NSString *msg = @"Attempting to accept while connected or accepting connections. Disconnect first."; err = [self badConfigError:msg]; - + return_from_block; } - + // Clear queues (spurious read/write requests post disconnect) [self->readQueue removeAllObjects]; [self->writeQueue removeAllObjects]; - + // Remove a previous socket - + NSError *error = nil; NSFileManager *fileManager = [NSFileManager defaultManager]; NSString *urlPath = url.path; @@ -1906,155 +1916,155 @@ - (BOOL)acceptOnUrl:(NSURL *)url error:(NSError **)errPtr if (![fileManager removeItemAtURL:url error:&error]) { NSString *msg = @"Could not remove previous unix domain socket at given url."; err = [self otherError:msg]; - + return_from_block; } } - + // Resolve interface from description - + NSData *interface = [self getInterfaceAddressFromUrl:url]; - + if (interface == nil) { NSString *msg = @"Invalid unix domain url. Specify a valid file url that does not exist (e.g. \"file:///tmp/socket\")"; err = [self badParamError:msg]; - + return_from_block; } - + // Create sockets, configure, bind, and listen - + LogVerbose(@"Creating unix domain socket"); self->socketUN = createSocket(AF_UNIX, interface); - + if (self->socketUN == SOCKET_NULL) { return_from_block; } - + self->socketUrl = url; - + // Create accept sources - + self->acceptUNSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, self->socketUN, 0, self->socketQueue); - + int socketFD = self->socketUN; dispatch_source_t acceptSource = self->acceptUNSource; - + __weak GCDAsyncSocket *weakSelf = self; - + dispatch_source_set_event_handler(self->acceptUNSource, ^{ @autoreleasepool { - + __strong GCDAsyncSocket *strongSelf = weakSelf; - + LogVerbose(@"eventUNBlock"); - + unsigned long i = 0; unsigned long numPendingConnections = dispatch_source_get_data(acceptSource); - + LogVerbose(@"numPendingConnections: %lu", numPendingConnections); - + while ([strongSelf doAccept:socketFD] && (++i < numPendingConnections)); }}); - + dispatch_source_set_cancel_handler(self->acceptUNSource, ^{ - + #if !OS_OBJECT_USE_OBJC LogVerbose(@"dispatch_release(acceptUNSource)"); dispatch_release(acceptSource); #endif - + LogVerbose(@"close(socketUN)"); close(socketFD); }); - + LogVerbose(@"dispatch_resume(acceptUNSource)"); dispatch_resume(self->acceptUNSource); - + self->flags |= kSocketStarted; - + result = YES; }}; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); - + if (result == NO) { LogInfo(@"Error in accept: %@", err); - + if (errPtr) *errPtr = err; } - - return result; + + return result; } - (BOOL)doAccept:(int)parentSocketFD { LogTrace(); - + int socketType; int childSocketFD; NSData *childSocketAddress; - + if (parentSocketFD == socket4FD) { socketType = 0; - + struct sockaddr_in addr; socklen_t addrLen = sizeof(addr); - + childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen); - + if (childSocketFD == -1) { LogWarn(@"Accept failed with error: %@", [self errnoError]); return NO; } - + childSocketAddress = [NSData dataWithBytes:&addr length:addrLen]; } else if (parentSocketFD == socket6FD) { socketType = 1; - + struct sockaddr_in6 addr; socklen_t addrLen = sizeof(addr); - + childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen); - + if (childSocketFD == -1) { LogWarn(@"Accept failed with error: %@", [self errnoError]); return NO; } - + childSocketAddress = [NSData dataWithBytes:&addr length:addrLen]; } else // if (parentSocketFD == socketUN) { socketType = 2; - + struct sockaddr_un addr; socklen_t addrLen = sizeof(addr); - + childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen); - + if (childSocketFD == -1) { LogWarn(@"Accept failed with error: %@", [self errnoError]); return NO; } - + childSocketAddress = [NSData dataWithBytes:&addr length:addrLen]; } - + // Enable non-blocking IO on the socket - + int result = fcntl(childSocketFD, F_SETFL, O_NONBLOCK); if (result == -1) { @@ -2063,69 +2073,69 @@ - (BOOL)doAccept:(int)parentSocketFD close(childSocketFD); return NO; } - + // Prevent SIGPIPE signals - + int nosigpipe = 1; setsockopt(childSocketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); - + // Notify delegate - + if (delegateQueue) { __strong id theDelegate = delegate; - + dispatch_async(delegateQueue, ^{ @autoreleasepool { - + // Query delegate for custom socket queue - + dispatch_queue_t childSocketQueue = NULL; - + if ([theDelegate respondsToSelector:@selector(newSocketQueueForConnectionFromAddress:onSocket:)]) { childSocketQueue = [theDelegate newSocketQueueForConnectionFromAddress:childSocketAddress onSocket:self]; } - + // Create GCDAsyncSocket instance for accepted socket - + GCDAsyncSocket *acceptedSocket = [[[self class] alloc] initWithDelegate:theDelegate delegateQueue:self->delegateQueue socketQueue:childSocketQueue]; - + if (socketType == 0) acceptedSocket->socket4FD = childSocketFD; else if (socketType == 1) acceptedSocket->socket6FD = childSocketFD; else acceptedSocket->socketUN = childSocketFD; - + acceptedSocket->flags = (kSocketStarted | kConnected); - + // Setup read and write sources for accepted socket - + dispatch_async(acceptedSocket->socketQueue, ^{ @autoreleasepool { - + [acceptedSocket setupReadAndWriteSourcesForNewlyConnectedSocket:childSocketFD]; }}); - + // Notify delegate - + if ([theDelegate respondsToSelector:@selector(socket:didAcceptNewSocket:)]) { [theDelegate socket:self didAcceptNewSocket:acceptedSocket]; } - + // Release the socket queue returned from the delegate (it was retained by acceptedSocket) #if !OS_OBJECT_USE_OBJC if (childSocketQueue) dispatch_release(childSocketQueue); #endif - + // The accepted socket should have been retained by the delegate. // Otherwise it gets properly released when exiting the block. }}); } - + return YES; } @@ -2136,12 +2146,12 @@ - (BOOL)doAccept:(int)parentSocketFD /** * This method runs through the various checks required prior to a connection attempt. * It is shared between the connectToHost and connectToAddress methods. - * + * **/ - (BOOL)preConnectWithInterface:(NSString *)interface error:(NSError **)errPtr { NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - + if (delegate == nil) // Must have delegate set { if (errPtr) @@ -2151,7 +2161,7 @@ - (BOOL)preConnectWithInterface:(NSString *)interface error:(NSError **)errPtr } return NO; } - + if (delegateQueue == NULL) // Must have delegate queue set { if (errPtr) @@ -2161,7 +2171,7 @@ - (BOOL)preConnectWithInterface:(NSString *)interface error:(NSError **)errPtr } return NO; } - + if (![self isDisconnected]) // Must be disconnected { if (errPtr) @@ -2171,10 +2181,10 @@ - (BOOL)preConnectWithInterface:(NSString *)interface error:(NSError **)errPtr } return NO; } - + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; - + if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled { if (errPtr) @@ -2184,14 +2194,14 @@ - (BOOL)preConnectWithInterface:(NSString *)interface error:(NSError **)errPtr } return NO; } - + if (interface) { NSMutableData *interface4 = nil; NSMutableData *interface6 = nil; - + [self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:0]; - + if ((interface4 == nil) && (interface6 == nil)) { if (errPtr) @@ -2201,7 +2211,7 @@ - (BOOL)preConnectWithInterface:(NSString *)interface error:(NSError **)errPtr } return NO; } - + if (isIPv4Disabled && (interface6 == nil)) { if (errPtr) @@ -2211,7 +2221,7 @@ - (BOOL)preConnectWithInterface:(NSString *)interface error:(NSError **)errPtr } return NO; } - + if (isIPv6Disabled && (interface4 == nil)) { if (errPtr) @@ -2221,22 +2231,22 @@ - (BOOL)preConnectWithInterface:(NSString *)interface error:(NSError **)errPtr } return NO; } - + connectInterface4 = interface4; connectInterface6 = interface6; } - + // Clear queues (spurious read/write requests post disconnect) [readQueue removeAllObjects]; [writeQueue removeAllObjects]; - + return YES; } - (BOOL)preConnectWithUrl:(NSURL *)url error:(NSError **)errPtr { NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - + if (delegate == nil) // Must have delegate set { if (errPtr) @@ -2246,7 +2256,7 @@ - (BOOL)preConnectWithUrl:(NSURL *)url error:(NSError **)errPtr } return NO; } - + if (delegateQueue == NULL) // Must have delegate queue set { if (errPtr) @@ -2256,7 +2266,7 @@ - (BOOL)preConnectWithUrl:(NSURL *)url error:(NSError **)errPtr } return NO; } - + if (![self isDisconnected]) // Must be disconnected { if (errPtr) @@ -2266,9 +2276,9 @@ - (BOOL)preConnectWithUrl:(NSURL *)url error:(NSError **)errPtr } return NO; } - + NSData *interface = [self getInterfaceAddressFromUrl:url]; - + if (interface == nil) { if (errPtr) @@ -2278,13 +2288,13 @@ - (BOOL)preConnectWithUrl:(NSURL *)url error:(NSError **)errPtr } return NO; } - + connectInterfaceUN = interface; - + // Clear queues (spurious read/write requests post disconnect) [readQueue removeAllObjects]; [writeQueue removeAllObjects]; - + return YES; } @@ -2308,64 +2318,64 @@ - (BOOL)connectToHost:(NSString *)inHost error:(NSError **)errPtr { LogTrace(); - + // Just in case immutable objects were passed NSString *host = [inHost copy]; NSString *interface = [inInterface copy]; - + __block BOOL result = NO; __block NSError *preConnectErr = nil; - + dispatch_block_t block = ^{ @autoreleasepool { - + // Check for problems with host parameter - + if ([host length] == 0) { NSString *msg = @"Invalid host parameter (nil or \"\"). Should be a domain name or IP address string."; preConnectErr = [self badParamError:msg]; - + return_from_block; } - + // Run through standard pre-connect checks - + if (![self preConnectWithInterface:interface error:&preConnectErr]) { return_from_block; } - + // We've made it past all the checks. // It's time to start the connection process. - + self->flags |= kSocketStarted; - + LogVerbose(@"Dispatching DNS lookup..."); - + // It's possible that the given host parameter is actually a NSMutableString. // So we want to copy it now, within this block that will be executed synchronously. // This way the asynchronous lookup block below doesn't have to worry about it changing. - + NSString *hostCpy = [host copy]; - + int aStateIndex = self->stateIndex; __weak GCDAsyncSocket *weakSelf = self; - + dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(globalConcurrentQueue, ^{ @autoreleasepool { #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" - + NSError *lookupErr = nil; NSMutableArray *addresses = [[self class] lookupHost:hostCpy port:port error:&lookupErr]; - + __strong GCDAsyncSocket *strongSelf = weakSelf; if (strongSelf == nil) return_from_block; - + if (lookupErr) { dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { - + [strongSelf lookup:aStateIndex didFail:lookupErr]; }}); } @@ -2373,7 +2383,7 @@ - (BOOL)connectToHost:(NSString *)inHost { NSData *address4 = nil; NSData *address6 = nil; - + for (NSData *address in addresses) { if (!address4 && [[self class] isIPv4Address:address]) @@ -2385,27 +2395,27 @@ - (BOOL)connectToHost:(NSString *)inHost address6 = address; } } - + dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { - + [strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6]; }}); } - + #pragma clang diagnostic pop }}); - + [self startConnectTimeout:timeout]; - + result = YES; }}; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); - - + + if (errPtr) *errPtr = preConnectErr; return result; } @@ -2426,25 +2436,25 @@ - (BOOL)connectToAddress:(NSData *)inRemoteAddr error:(NSError **)errPtr { LogTrace(); - + // Just in case immutable objects were passed NSData *remoteAddr = [inRemoteAddr copy]; NSString *interface = [inInterface copy]; - + __block BOOL result = NO; __block NSError *err = nil; - + dispatch_block_t block = ^{ @autoreleasepool { - + // Check for problems with remoteAddr parameter - + NSData *address4 = nil; NSData *address6 = nil; - + if ([remoteAddr length] >= sizeof(struct sockaddr)) { const struct sockaddr *sockaddr = (const struct sockaddr *)[remoteAddr bytes]; - + if (sockaddr->sa_family == AF_INET) { if ([remoteAddr length] == sizeof(struct sockaddr_in)) @@ -2460,169 +2470,169 @@ - (BOOL)connectToAddress:(NSData *)inRemoteAddr } } } - + if ((address4 == nil) && (address6 == nil)) { NSString *msg = @"A valid IPv4 or IPv6 address was not given"; err = [self badParamError:msg]; - + return_from_block; } - + BOOL isIPv4Disabled = (self->config & kIPv4Disabled) ? YES : NO; BOOL isIPv6Disabled = (self->config & kIPv6Disabled) ? YES : NO; - + if (isIPv4Disabled && (address4 != nil)) { NSString *msg = @"IPv4 has been disabled and an IPv4 address was passed."; err = [self badParamError:msg]; - + return_from_block; } - + if (isIPv6Disabled && (address6 != nil)) { NSString *msg = @"IPv6 has been disabled and an IPv6 address was passed."; err = [self badParamError:msg]; - + return_from_block; } - + // Run through standard pre-connect checks - + if (![self preConnectWithInterface:interface error:&err]) { return_from_block; } - + // We've made it past all the checks. // It's time to start the connection process. - + if (![self connectWithAddress4:address4 address6:address6 error:&err]) { return_from_block; } - + self->flags |= kSocketStarted; - + [self startConnectTimeout:timeout]; - + result = YES; }}; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); - + if (result == NO) { if (errPtr) *errPtr = err; } - + return result; } - (BOOL)connectToUrl:(NSURL *)url withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr { LogTrace(); - + __block BOOL result = NO; __block NSError *err = nil; - + dispatch_block_t block = ^{ @autoreleasepool { - + // Check for problems with host parameter - + if ([url.path length] == 0) { NSString *msg = @"Invalid unix domain socket url."; err = [self badParamError:msg]; - + return_from_block; } - + // Run through standard pre-connect checks - + if (![self preConnectWithUrl:url error:&err]) { return_from_block; } - + // We've made it past all the checks. // It's time to start the connection process. - + self->flags |= kSocketStarted; - + // Start the normal connection process - + NSError *connectError = nil; if (![self connectWithAddressUN:self->connectInterfaceUN error:&connectError]) { [self closeWithError:connectError]; - + return_from_block; } [self startConnectTimeout:timeout]; - + result = YES; }}; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); - + if (result == NO) { if (errPtr) *errPtr = err; } - + return result; } - (void)lookup:(int)aStateIndex didSucceedWithAddress4:(NSData *)address4 address6:(NSData *)address6 { LogTrace(); - + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); NSAssert(address4 || address6, @"Expected at least one valid address"); - + if (aStateIndex != stateIndex) { LogInfo(@"Ignoring lookupDidSucceed, already disconnected"); - + // The connect operation has been cancelled. // That is, socket was disconnected, or connection has already timed out. return; } - + // Check for problems - + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; - + if (isIPv4Disabled && (address6 == nil)) { NSString *msg = @"IPv4 has been disabled and DNS lookup found no IPv6 address."; - + [self closeWithError:[self otherError:msg]]; return; } - + if (isIPv6Disabled && (address4 == nil)) { NSString *msg = @"IPv6 has been disabled and DNS lookup found no IPv4 address."; - + [self closeWithError:[self otherError:msg]]; return; } - + // Start the normal connection process - + NSError *err = nil; if (![self connectWithAddress4:address4 address6:address6 error:&err]) { @@ -2633,7 +2643,7 @@ - (void)lookup:(int)aStateIndex didSucceedWithAddress4:(NSData *)address4 addres /** * This method is called if the DNS lookup fails. * This method is executed on the socketQueue. - * + * * Since the DNS lookup executed synchronously on a global concurrent queue, * the original connection request may have already been cancelled or timed-out by the time this method is invoked. * The lookupIndex tells us whether the lookup is still valid or not. @@ -2641,19 +2651,19 @@ - (void)lookup:(int)aStateIndex didSucceedWithAddress4:(NSData *)address4 addres - (void)lookup:(int)aStateIndex didFail:(NSError *)error { LogTrace(); - + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - - + + if (aStateIndex != stateIndex) { LogInfo(@"Ignoring lookup:didFail: - already disconnected"); - + // The connect operation has been cancelled. // That is, socket was disconnected, or connection has already timed out. return; } - + [self endConnectTimeout]; [self closeWithError:error]; } @@ -2661,59 +2671,59 @@ - (void)lookup:(int)aStateIndex didFail:(NSError *)error - (BOOL)bindSocket:(int)socketFD toInterface:(NSData *)connectInterface error:(NSError **)errPtr { // Bind the socket to the desired interface (if needed) - + if (connectInterface) { LogVerbose(@"Binding socket..."); - + if ([[self class] portFromAddress:connectInterface] > 0) { // Since we're going to be binding to a specific port, // we should turn on reuseaddr to allow us to override sockets in time_wait. - + int reuseOn = 1; setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); } - + const struct sockaddr *interfaceAddr = (const struct sockaddr *)[connectInterface bytes]; - + int result = bind(socketFD, interfaceAddr, (socklen_t)[connectInterface length]); if (result != 0) { if (errPtr) *errPtr = [self errorWithErrno:errno reason:@"Error in bind() function"]; - + return NO; } } - + return YES; } - (int)createSocket:(int)family connectInterface:(NSData *)connectInterface errPtr:(NSError **)errPtr { int socketFD = socket(family, SOCK_STREAM, 0); - + if (socketFD == SOCKET_NULL) { if (errPtr) *errPtr = [self errorWithErrno:errno reason:@"Error in socket() function"]; - + return socketFD; } - + if (![self bindSocket:socketFD toInterface:connectInterface error:errPtr]) { [self closeSocket:socketFD]; - + return SOCKET_NULL; } - + // Prevent SIGPIPE signals - + int nosigpipe = 1; setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); - + return socketFD; } @@ -2725,40 +2735,40 @@ - (void)connectSocket:(int)socketFD address:(NSData *)address stateIndex:(int)aS [self closeSocket:socketFD]; return; } - + // Start the connection process in a background queue - + __weak GCDAsyncSocket *weakSelf = self; - + dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(globalConcurrentQueue, ^{ #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" - + int result = connect(socketFD, (const struct sockaddr *)[address bytes], (socklen_t)[address length]); int err = errno; - + __strong GCDAsyncSocket *strongSelf = weakSelf; if (strongSelf == nil) return_from_block; - + dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { - + if (strongSelf.isConnected) { [strongSelf closeSocket:socketFD]; return_from_block; } - + if (result == 0) { [self closeUnusedSocket:socketFD]; - + [strongSelf didConnect:aStateIndex]; } else { [strongSelf closeSocket:socketFD]; - + // If there are no more sockets trying to connect, we inform the error to the delegate if (strongSelf.socket4FD == SOCKET_NULL && strongSelf.socket6FD == SOCKET_NULL) { @@ -2767,10 +2777,10 @@ - (void)connectSocket:(int)socketFD address:(NSData *)address stateIndex:(int)aS } } }}); - + #pragma clang diagnostic pop }); - + LogVerbose(@"Connecting..."); } @@ -2780,7 +2790,7 @@ - (void)closeSocket:(int)socketFD (socketFD == socket6FD || socketFD == socket4FD)) { close(socketFD); - + if (socketFD == socket4FD) { LogVerbose(@"close(socket4FD)"); @@ -2809,40 +2819,40 @@ - (void)closeUnusedSocket:(int)usedSocketFD - (BOOL)connectWithAddress4:(NSData *)address4 address6:(NSData *)address6 error:(NSError **)errPtr { LogTrace(); - + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - + LogVerbose(@"IPv4: %@:%hu", [[self class] hostFromAddress:address4], [[self class] portFromAddress:address4]); LogVerbose(@"IPv6: %@:%hu", [[self class] hostFromAddress:address6], [[self class] portFromAddress:address6]); - + // Determine socket type - + BOOL preferIPv6 = (config & kPreferIPv6) ? YES : NO; - + // Create and bind the sockets - + if (address4) { LogVerbose(@"Creating IPv4 socket"); - + socket4FD = [self createSocket:AF_INET connectInterface:connectInterface4 errPtr:errPtr]; } - + if (address6) { LogVerbose(@"Creating IPv6 socket"); - + socket6FD = [self createSocket:AF_INET6 connectInterface:connectInterface6 errPtr:errPtr]; } - + if (socket4FD == SOCKET_NULL && socket6FD == SOCKET_NULL) { return NO; } - + int socketFD, alternateSocketFD; NSData *address, *alternateAddress; - + if ((preferIPv6 && socket6FD != SOCKET_NULL) || socket4FD == SOCKET_NULL) { socketFD = socket6FD; @@ -2859,79 +2869,79 @@ - (BOOL)connectWithAddress4:(NSData *)address4 address6:(NSData *)address6 error } int aStateIndex = stateIndex; - + [self connectSocket:socketFD address:address stateIndex:aStateIndex]; - + if (alternateAddress) { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(alternateAddressDelay * NSEC_PER_SEC)), socketQueue, ^{ [self connectSocket:alternateSocketFD address:alternateAddress stateIndex:aStateIndex]; }); } - + return YES; } - (BOOL)connectWithAddressUN:(NSData *)address error:(NSError **)errPtr { LogTrace(); - + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - + // Create the socket - + int socketFD; - + LogVerbose(@"Creating unix domain socket"); - + socketUN = socket(AF_UNIX, SOCK_STREAM, 0); - + socketFD = socketUN; - + if (socketFD == SOCKET_NULL) { if (errPtr) *errPtr = [self errorWithErrno:errno reason:@"Error in socket() function"]; - + return NO; } - + // Bind the socket to the desired interface (if needed) - + LogVerbose(@"Binding socket..."); - + int reuseOn = 1; setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); // const struct sockaddr *interfaceAddr = (const struct sockaddr *)[address bytes]; -// +// // int result = bind(socketFD, interfaceAddr, (socklen_t)[address length]); // if (result != 0) // { // if (errPtr) // *errPtr = [self errnoErrorWithReason:@"Error in bind() function"]; -// +// // return NO; // } - + // Prevent SIGPIPE signals - + int nosigpipe = 1; setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); - + // Start the connection process in a background queue - + int aStateIndex = stateIndex; - + dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(globalConcurrentQueue, ^{ - + const struct sockaddr *addr = (const struct sockaddr *)[address bytes]; int result = connect(socketFD, addr, addr->sa_len); if (result == 0) { dispatch_async(self->socketQueue, ^{ @autoreleasepool { - + [self didConnect:aStateIndex]; }}); } @@ -2940,112 +2950,112 @@ - (BOOL)connectWithAddressUN:(NSData *)address error:(NSError **)errPtr // TODO: Bad file descriptor perror("connect"); NSError *error = [self errorWithErrno:errno reason:@"Error in connect() function"]; - + dispatch_async(self->socketQueue, ^{ @autoreleasepool { - + [self didNotConnect:aStateIndex error:error]; }}); } }); - + LogVerbose(@"Connecting..."); - + return YES; } - (void)didConnect:(int)aStateIndex { LogTrace(); - + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - - + + if (aStateIndex != stateIndex) { LogInfo(@"Ignoring didConnect, already disconnected"); - + // The connect operation has been cancelled. // That is, socket was disconnected, or connection has already timed out. return; } - + flags |= kConnected; - + [self endConnectTimeout]; - + #if TARGET_OS_IPHONE // The endConnectTimeout method executed above incremented the stateIndex. aStateIndex = stateIndex; #endif - + // Setup read/write streams (as workaround for specific shortcomings in the iOS platform) - // + // // Note: // There may be configuration options that must be set by the delegate before opening the streams. // The primary example is the kCFStreamNetworkServiceTypeVoIP flag, which only works on an unopened stream. - // + // // Thus we wait until after the socket:didConnectToHost:port: delegate method has completed. // This gives the delegate time to properly configure the streams if needed. - + dispatch_block_t SetupStreamsPart1 = ^{ #if TARGET_OS_IPHONE - + if (![self createReadAndWriteStream]) { [self closeWithError:[self otherError:@"Error creating CFStreams"]]; return; } - + if (![self registerForStreamCallbacksIncludingReadWrite:NO]) { [self closeWithError:[self otherError:@"Error in CFStreamSetClient"]]; return; } - + #endif }; dispatch_block_t SetupStreamsPart2 = ^{ #if TARGET_OS_IPHONE - + if (aStateIndex != self->stateIndex) { // The socket has been disconnected. return; } - + if (![self addStreamsToRunLoop]) { [self closeWithError:[self otherError:@"Error in CFStreamScheduleWithRunLoop"]]; return; } - + if (![self openStreams]) { [self closeWithError:[self otherError:@"Error creating CFStreams"]]; return; } - + #endif }; - + // Notify delegate - + NSString *host = [self connectedHost]; uint16_t port = [self connectedPort]; NSURL *url = [self connectedUrl]; - + __strong id theDelegate = delegate; if (delegateQueue && host != nil && [theDelegate respondsToSelector:@selector(socket:didConnectToHost:port:)]) { SetupStreamsPart1(); - + dispatch_async(delegateQueue, ^{ @autoreleasepool { - + [theDelegate socket:self didConnectToHost:host port:port]; - + dispatch_async(self->socketQueue, ^{ @autoreleasepool { - + SetupStreamsPart2(); }}); }}); @@ -3053,13 +3063,13 @@ - (void)didConnect:(int)aStateIndex else if (delegateQueue && url != nil && [theDelegate respondsToSelector:@selector(socket:didConnectToUrl:)]) { SetupStreamsPart1(); - + dispatch_async(delegateQueue, ^{ @autoreleasepool { - + [theDelegate socket:self didConnectToUrl:url]; - + dispatch_async(self->socketQueue, ^{ @autoreleasepool { - + SetupStreamsPart2(); }}); }}); @@ -3069,28 +3079,28 @@ - (void)didConnect:(int)aStateIndex SetupStreamsPart1(); SetupStreamsPart2(); } - + // Get the connected socket - + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; - + // Enable non-blocking IO on the socket - + int result = fcntl(socketFD, F_SETFL, O_NONBLOCK); if (result == -1) { NSString *errMsg = @"Error enabling non-blocking IO on socket (fcntl)"; [self closeWithError:[self otherError:errMsg]]; - + return; } - + // Setup our read/write sources - + [self setupReadAndWriteSourcesForNewlyConnectedSocket:socketFD]; - + // Dequeue any pending read/write requests - + [self maybeDequeueRead]; [self maybeDequeueWrite]; } @@ -3098,19 +3108,19 @@ - (void)didConnect:(int)aStateIndex - (void)didNotConnect:(int)aStateIndex error:(NSError *)error { LogTrace(); - + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - - + + if (aStateIndex != stateIndex) { LogInfo(@"Ignoring didNotConnect, already disconnected"); - + // The connect operation has been cancelled. // That is, socket was disconnected, or connection has already timed out. return; } - + [self closeWithError:error]; } @@ -3119,37 +3129,37 @@ - (void)startConnectTimeout:(NSTimeInterval)timeout if (timeout >= 0.0) { connectTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue); - + __weak GCDAsyncSocket *weakSelf = self; - + dispatch_source_set_event_handler(connectTimer, ^{ @autoreleasepool { #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" - + __strong GCDAsyncSocket *strongSelf = weakSelf; if (strongSelf == nil) return_from_block; - + [strongSelf doConnectTimeout]; - + #pragma clang diagnostic pop }}); - + #if !OS_OBJECT_USE_OBJC dispatch_source_t theConnectTimer = connectTimer; dispatch_source_set_cancel_handler(connectTimer, ^{ #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" - + LogVerbose(@"dispatch_release(connectTimer)"); dispatch_release(theConnectTimer); - + #pragma clang diagnostic pop }); #endif - + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)); dispatch_source_set_timer(connectTimer, tt, DISPATCH_TIME_FOREVER, 0); - + dispatch_resume(connectTimer); } } @@ -3157,21 +3167,21 @@ - (void)startConnectTimeout:(NSTimeInterval)timeout - (void)endConnectTimeout { LogTrace(); - + if (connectTimer) { dispatch_source_cancel(connectTimer); connectTimer = NULL; } - + // Increment stateIndex. // This will prevent us from processing results from any related background asynchronous operations. - // + // // Note: This should be called from close method even if connectTimer is NULL. // This is because one might disconnect a socket prior to a successful connection which had no timeout. - + stateIndex++; - + if (connectInterface4) { connectInterface4 = nil; @@ -3185,7 +3195,7 @@ - (void)endConnectTimeout - (void)doConnectTimeout { LogTrace(); - + [self endConnectTimeout]; [self closeWithError:[self connectTimeoutError]]; } @@ -3198,23 +3208,23 @@ - (void)closeWithError:(NSError *)error { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - + [self endConnectTimeout]; - + if (currentRead != nil) [self endCurrentRead]; if (currentWrite != nil) [self endCurrentWrite]; - + [readQueue removeAllObjects]; [writeQueue removeAllObjects]; - + [preBuffer reset]; - + #if TARGET_OS_IPHONE { if (readStream || writeStream) { [self removeStreamsFromRunLoop]; - + if (readStream) { CFReadStreamSetClient(readStream, kCFStreamEventNone, NULL, NULL); @@ -3232,31 +3242,31 @@ - (void)closeWithError:(NSError *)error } } #endif - + [sslPreBuffer reset]; sslErrCode = lastSSLHandshakeError = noErr; - + if (sslContext) { // Getting a linker error here about the SSLx() functions? // You need to add the Security Framework to your application. - + SSLClose(sslContext); - + #if TARGET_OS_IPHONE || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 1080) CFRelease(sslContext); #else SSLDisposeContext(sslContext); #endif - + sslContext = NULL; } - + // For some crazy reason (in my opinion), cancelling a dispatch source doesn't // invoke the cancel handler if the dispatch source is paused. // So we have to unpause the source if needed. // This allows the cancel handler to be run, which in turn releases the source and closes the socket. - + if (!accept4Source && !accept6Source && !acceptUNSource && !readSource && !writeSource) { LogVerbose(@"manually closing close"); @@ -3274,7 +3284,7 @@ - (void)closeWithError:(NSError *)error close(socket6FD); socket6FD = SOCKET_NULL; } - + if (socketUN != SOCKET_NULL) { LogVerbose(@"close(socketUN)"); @@ -3290,96 +3300,96 @@ - (void)closeWithError:(NSError *)error { LogVerbose(@"dispatch_source_cancel(accept4Source)"); dispatch_source_cancel(accept4Source); - + // We never suspend accept4Source - + accept4Source = NULL; } - + if (accept6Source) { LogVerbose(@"dispatch_source_cancel(accept6Source)"); dispatch_source_cancel(accept6Source); - + // We never suspend accept6Source - + accept6Source = NULL; } - + if (acceptUNSource) { LogVerbose(@"dispatch_source_cancel(acceptUNSource)"); dispatch_source_cancel(acceptUNSource); - + // We never suspend acceptUNSource - + acceptUNSource = NULL; } - + if (readSource) { LogVerbose(@"dispatch_source_cancel(readSource)"); dispatch_source_cancel(readSource); - + [self resumeReadSource]; - + readSource = NULL; } - + if (writeSource) { LogVerbose(@"dispatch_source_cancel(writeSource)"); dispatch_source_cancel(writeSource); - + [self resumeWriteSource]; - + writeSource = NULL; } - + // The sockets will be closed by the cancel handlers of the corresponding source - + socket4FD = SOCKET_NULL; socket6FD = SOCKET_NULL; socketUN = SOCKET_NULL; } - + // If the client has passed the connect/accept method, then the connection has at least begun. // Notify delegate that it is now ending. BOOL shouldCallDelegate = (flags & kSocketStarted) ? YES : NO; BOOL isDeallocating = (flags & kDealloc) ? YES : NO; - + // Clear stored socket info and all flags (config remains as is) socketFDBytesAvailable = 0; flags = 0; sslWriteCachedLength = 0; - + if (shouldCallDelegate) { __strong id theDelegate = delegate; __strong id theSelf = isDeallocating ? nil : self; - + if (delegateQueue && [theDelegate respondsToSelector: @selector(socketDidDisconnect:withError:)]) { dispatch_async(delegateQueue, ^{ @autoreleasepool { - + [theDelegate socketDidDisconnect:theSelf withError:error]; }}); - } + } } } - (void)disconnect { dispatch_block_t block = ^{ @autoreleasepool { - + if (self->flags & kSocketStarted) { [self closeWithError:nil]; } }}; - + // Synchronous disconnection, as documented in the header file - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else @@ -3389,7 +3399,7 @@ - (void)disconnect - (void)disconnectAfterReading { dispatch_async(socketQueue, ^{ @autoreleasepool { - + if (self->flags & kSocketStarted) { self->flags |= (kForbidReadsWrites | kDisconnectAfterReads); @@ -3401,7 +3411,7 @@ - (void)disconnectAfterReading - (void)disconnectAfterWriting { dispatch_async(socketQueue, ^{ @autoreleasepool { - + if (self->flags & kSocketStarted) { self->flags |= (kForbidReadsWrites | kDisconnectAfterWrites); @@ -3413,7 +3423,7 @@ - (void)disconnectAfterWriting - (void)disconnectAfterReadingAndWriting { dispatch_async(socketQueue, ^{ @autoreleasepool { - + if (self->flags & kSocketStarted) { self->flags |= (kForbidReadsWrites | kDisconnectAfterReads | kDisconnectAfterWrites); @@ -3430,9 +3440,9 @@ - (void)disconnectAfterReadingAndWriting - (void)maybeClose { NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - + BOOL shouldClose = NO; - + if (flags & kDisconnectAfterReads) { if (([readQueue count] == 0) && (currentRead == nil)) @@ -3457,7 +3467,7 @@ - (void)maybeClose shouldClose = YES; } } - + if (shouldClose) { [self closeWithError:nil]; @@ -3471,14 +3481,14 @@ - (void)maybeClose - (NSError *)badConfigError:(NSString *)errMsg { NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; - + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketBadConfigError userInfo:userInfo]; } - (NSError *)badParamError:(NSString *)errMsg { NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; - + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketBadParamError userInfo:userInfo]; } @@ -3486,7 +3496,7 @@ + (NSError *)gaiError:(int)gai_error { NSString *errMsg = [NSString stringWithCString:gai_strerror(gai_error) encoding:NSASCIIStringEncoding]; NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; - + return [NSError errorWithDomain:@"kCFStreamErrorDomainNetDB" code:gai_error userInfo:userInfo]; } @@ -3495,7 +3505,7 @@ - (NSError *)errorWithErrno:(int)err reason:(NSString *)reason NSString *errMsg = [NSString stringWithUTF8String:strerror(err)]; NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg, NSLocalizedFailureReasonErrorKey : reason}; - + return [NSError errorWithDomain:NSPOSIXErrorDomain code:err userInfo:userInfo]; } @@ -3503,7 +3513,7 @@ - (NSError *)errnoError { NSString *errMsg = [NSString stringWithUTF8String:strerror(errno)]; NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; - + return [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:userInfo]; } @@ -3511,7 +3521,7 @@ - (NSError *)sslError:(OSStatus)ssl_error { NSString *msg = @"Error code definition can be found in Apple's SecureTransport.h"; NSDictionary *userInfo = @{NSLocalizedRecoverySuggestionErrorKey : msg}; - + return [NSError errorWithDomain:@"kCFStreamErrorDomainSSL" code:ssl_error userInfo:userInfo]; } @@ -3520,9 +3530,9 @@ - (NSError *)connectTimeoutError NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketConnectTimeoutError", @"GCDAsyncSocket", [NSBundle mainBundle], @"Attempt to connect to host timed out", nil); - + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; - + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketConnectTimeoutError userInfo:userInfo]; } @@ -3534,9 +3544,9 @@ - (NSError *)readMaxedOutError NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketReadMaxedOutError", @"GCDAsyncSocket", [NSBundle mainBundle], @"Read operation reached set maximum length", nil); - + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; - + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketReadMaxedOutError userInfo:info]; } @@ -3548,9 +3558,9 @@ - (NSError *)readTimeoutError NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketReadTimeoutError", @"GCDAsyncSocket", [NSBundle mainBundle], @"Read operation timed out", nil); - + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; - + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketReadTimeoutError userInfo:userInfo]; } @@ -3562,9 +3572,9 @@ - (NSError *)writeTimeoutError NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketWriteTimeoutError", @"GCDAsyncSocket", [NSBundle mainBundle], @"Write operation timed out", nil); - + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; - + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketWriteTimeoutError userInfo:userInfo]; } @@ -3573,16 +3583,16 @@ - (NSError *)connectionClosedError NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketClosedError", @"GCDAsyncSocket", [NSBundle mainBundle], @"Socket closed by remote peer", nil); - + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; - + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketClosedError userInfo:userInfo]; } - (NSError *)otherError:(NSString *)errMsg { NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; - + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketOtherError userInfo:userInfo]; } @@ -3593,32 +3603,32 @@ - (NSError *)otherError:(NSString *)errMsg - (BOOL)isDisconnected { __block BOOL result = NO; - + dispatch_block_t block = ^{ result = (self->flags & kSocketStarted) ? NO : YES; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); - + return result; } - (BOOL)isConnected { __block BOOL result = NO; - + dispatch_block_t block = ^{ result = (self->flags & kConnected) ? YES : NO; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); - + return result; } @@ -3630,21 +3640,21 @@ - (NSString *)connectedHost return [self connectedHostFromSocket4:socket4FD]; if (socket6FD != SOCKET_NULL) return [self connectedHostFromSocket6:socket6FD]; - + return nil; } else { __block NSString *result = nil; - + dispatch_sync(socketQueue, ^{ @autoreleasepool { - + if (self->socket4FD != SOCKET_NULL) result = [self connectedHostFromSocket4:self->socket4FD]; else if (self->socket6FD != SOCKET_NULL) result = [self connectedHostFromSocket6:self->socket6FD]; }}); - + return result; } } @@ -3657,22 +3667,22 @@ - (uint16_t)connectedPort return [self connectedPortFromSocket4:socket4FD]; if (socket6FD != SOCKET_NULL) return [self connectedPortFromSocket6:socket6FD]; - + return 0; } else { __block uint16_t result = 0; - + dispatch_sync(socketQueue, ^{ // No need for autorelease pool - + if (self->socket4FD != SOCKET_NULL) result = [self connectedPortFromSocket4:self->socket4FD]; else if (self->socket6FD != SOCKET_NULL) result = [self connectedPortFromSocket6:self->socket6FD]; }); - + return result; } } @@ -3683,19 +3693,19 @@ - (NSURL *)connectedUrl { if (socketUN != SOCKET_NULL) return [self connectedUrlFromSocketUN:socketUN]; - + return nil; } else { __block NSURL *result = nil; - + dispatch_sync(socketQueue, ^{ @autoreleasepool { - + if (self->socketUN != SOCKET_NULL) result = [self connectedUrlFromSocketUN:self->socketUN]; }}); - + return result; } } @@ -3708,21 +3718,21 @@ - (NSString *)localHost return [self localHostFromSocket4:socket4FD]; if (socket6FD != SOCKET_NULL) return [self localHostFromSocket6:socket6FD]; - + return nil; } else { __block NSString *result = nil; - + dispatch_sync(socketQueue, ^{ @autoreleasepool { - + if (self->socket4FD != SOCKET_NULL) result = [self localHostFromSocket4:self->socket4FD]; else if (self->socket6FD != SOCKET_NULL) result = [self localHostFromSocket6:self->socket6FD]; }}); - + return result; } } @@ -3735,22 +3745,22 @@ - (uint16_t)localPort return [self localPortFromSocket4:socket4FD]; if (socket6FD != SOCKET_NULL) return [self localPortFromSocket6:socket6FD]; - + return 0; } else { __block uint16_t result = 0; - + dispatch_sync(socketQueue, ^{ // No need for autorelease pool - + if (self->socket4FD != SOCKET_NULL) result = [self localPortFromSocket4:self->socket4FD]; else if (self->socket6FD != SOCKET_NULL) result = [self localPortFromSocket6:self->socket6FD]; }); - + return result; } } @@ -3759,7 +3769,7 @@ - (NSString *)connectedHost4 { if (socket4FD != SOCKET_NULL) return [self connectedHostFromSocket4:socket4FD]; - + return nil; } @@ -3767,7 +3777,7 @@ - (NSString *)connectedHost6 { if (socket6FD != SOCKET_NULL) return [self connectedHostFromSocket6:socket6FD]; - + return nil; } @@ -3775,7 +3785,7 @@ - (uint16_t)connectedPort4 { if (socket4FD != SOCKET_NULL) return [self connectedPortFromSocket4:socket4FD]; - + return 0; } @@ -3783,7 +3793,7 @@ - (uint16_t)connectedPort6 { if (socket6FD != SOCKET_NULL) return [self connectedPortFromSocket6:socket6FD]; - + return 0; } @@ -3791,7 +3801,7 @@ - (NSString *)localHost4 { if (socket4FD != SOCKET_NULL) return [self localHostFromSocket4:socket4FD]; - + return nil; } @@ -3799,7 +3809,7 @@ - (NSString *)localHost6 { if (socket6FD != SOCKET_NULL) return [self localHostFromSocket6:socket6FD]; - + return nil; } @@ -3807,7 +3817,7 @@ - (uint16_t)localPort4 { if (socket4FD != SOCKET_NULL) return [self localPortFromSocket4:socket4FD]; - + return 0; } @@ -3815,7 +3825,7 @@ - (uint16_t)localPort6 { if (socket6FD != SOCKET_NULL) return [self localPortFromSocket6:socket6FD]; - + return 0; } @@ -3823,7 +3833,7 @@ - (NSString *)connectedHostFromSocket4:(int)socketFD { struct sockaddr_in sockaddr4; socklen_t sockaddr4len = sizeof(sockaddr4); - + if (getpeername(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) { return nil; @@ -3835,7 +3845,7 @@ - (NSString *)connectedHostFromSocket6:(int)socketFD { struct sockaddr_in6 sockaddr6; socklen_t sockaddr6len = sizeof(sockaddr6); - + if (getpeername(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) { return nil; @@ -3847,7 +3857,7 @@ - (uint16_t)connectedPortFromSocket4:(int)socketFD { struct sockaddr_in sockaddr4; socklen_t sockaddr4len = sizeof(sockaddr4); - + if (getpeername(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) { return 0; @@ -3859,7 +3869,7 @@ - (uint16_t)connectedPortFromSocket6:(int)socketFD { struct sockaddr_in6 sockaddr6; socklen_t sockaddr6len = sizeof(sockaddr6); - + if (getpeername(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) { return 0; @@ -3871,7 +3881,7 @@ - (NSURL *)connectedUrlFromSocketUN:(int)socketFD { struct sockaddr_un sockaddr; socklen_t sockaddrlen = sizeof(sockaddr); - + if (getpeername(socketFD, (struct sockaddr *)&sockaddr, &sockaddrlen) < 0) { return 0; @@ -3883,7 +3893,7 @@ - (NSString *)localHostFromSocket4:(int)socketFD { struct sockaddr_in sockaddr4; socklen_t sockaddr4len = sizeof(sockaddr4); - + if (getsockname(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) { return nil; @@ -3895,7 +3905,7 @@ - (NSString *)localHostFromSocket6:(int)socketFD { struct sockaddr_in6 sockaddr6; socklen_t sockaddr6len = sizeof(sockaddr6); - + if (getsockname(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) { return nil; @@ -3907,7 +3917,7 @@ - (uint16_t)localPortFromSocket4:(int)socketFD { struct sockaddr_in sockaddr4; socklen_t sockaddr4len = sizeof(sockaddr4); - + if (getsockname(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) { return 0; @@ -3919,7 +3929,7 @@ - (uint16_t)localPortFromSocket6:(int)socketFD { struct sockaddr_in6 sockaddr6; socklen_t sockaddr6len = sizeof(sockaddr6); - + if (getsockname(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) { return 0; @@ -3930,72 +3940,72 @@ - (uint16_t)localPortFromSocket6:(int)socketFD - (NSData *)connectedAddress { __block NSData *result = nil; - + dispatch_block_t block = ^{ if (self->socket4FD != SOCKET_NULL) { struct sockaddr_in sockaddr4; socklen_t sockaddr4len = sizeof(sockaddr4); - + if (getpeername(self->socket4FD, (struct sockaddr *)&sockaddr4, &sockaddr4len) == 0) { result = [[NSData alloc] initWithBytes:&sockaddr4 length:sockaddr4len]; } } - + if (self->socket6FD != SOCKET_NULL) { struct sockaddr_in6 sockaddr6; socklen_t sockaddr6len = sizeof(sockaddr6); - + if (getpeername(self->socket6FD, (struct sockaddr *)&sockaddr6, &sockaddr6len) == 0) { result = [[NSData alloc] initWithBytes:&sockaddr6 length:sockaddr6len]; } } }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); - + return result; } - (NSData *)localAddress { __block NSData *result = nil; - + dispatch_block_t block = ^{ if (self->socket4FD != SOCKET_NULL) { struct sockaddr_in sockaddr4; socklen_t sockaddr4len = sizeof(sockaddr4); - + if (getsockname(self->socket4FD, (struct sockaddr *)&sockaddr4, &sockaddr4len) == 0) { result = [[NSData alloc] initWithBytes:&sockaddr4 length:sockaddr4len]; } } - + if (self->socket6FD != SOCKET_NULL) { struct sockaddr_in6 sockaddr6; socklen_t sockaddr6len = sizeof(sockaddr6); - + if (getsockname(self->socket6FD, (struct sockaddr *)&sockaddr6, &sockaddr6len) == 0) { result = [[NSData alloc] initWithBytes:&sockaddr6 length:sockaddr6len]; } } }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); - + return result; } @@ -4008,11 +4018,11 @@ - (BOOL)isIPv4 else { __block BOOL result = NO; - + dispatch_sync(socketQueue, ^{ result = (self->socket4FD != SOCKET_NULL); }); - + return result; } } @@ -4026,11 +4036,11 @@ - (BOOL)isIPv6 else { __block BOOL result = NO; - + dispatch_sync(socketQueue, ^{ result = (self->socket6FD != SOCKET_NULL); }); - + return result; } } @@ -4044,11 +4054,11 @@ - (BOOL)isSecure else { __block BOOL result; - + dispatch_sync(socketQueue, ^{ result = (self->flags & kSocketSecure) ? YES : NO; }); - + return result; } } @@ -4060,10 +4070,10 @@ - (BOOL)isSecure /** * Finds the address of an interface description. * An inteface description may be an interface name (en0, en1, lo0) or corresponding IP (192.168.4.34). - * + * * The interface description may optionally contain a port number at the end, separated by a colon. * If a non-zero port parameter is provided, any port number in the interface description is ignored. - * + * * The returned value is a 'struct sockaddr' wrapped in an NSMutableData object. **/ - (void)getInterfaceAddress4:(NSMutableData **)interfaceAddr4Ptr @@ -4073,9 +4083,9 @@ - (void)getInterfaceAddress4:(NSMutableData **)interfaceAddr4Ptr { NSMutableData *addr4 = nil; NSMutableData *addr6 = nil; - + NSString *interface = nil; - + NSArray *components = [interfaceDescription componentsSeparatedByString:@":"]; if ([components count] > 0) { @@ -4089,66 +4099,66 @@ - (void)getInterfaceAddress4:(NSMutableData **)interfaceAddr4Ptr { NSString *temp = [components objectAtIndex:1]; long portL = strtol([temp UTF8String], NULL, 10); - + if (portL > 0 && portL <= UINT16_MAX) { port = (uint16_t)portL; } } - + if (interface == nil) { // ANY address - + struct sockaddr_in sockaddr4; memset(&sockaddr4, 0, sizeof(sockaddr4)); - + sockaddr4.sin_len = sizeof(sockaddr4); sockaddr4.sin_family = AF_INET; sockaddr4.sin_port = htons(port); sockaddr4.sin_addr.s_addr = htonl(INADDR_ANY); - + struct sockaddr_in6 sockaddr6; memset(&sockaddr6, 0, sizeof(sockaddr6)); - + sockaddr6.sin6_len = sizeof(sockaddr6); sockaddr6.sin6_family = AF_INET6; sockaddr6.sin6_port = htons(port); sockaddr6.sin6_addr = in6addr_any; - + addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; } else if ([interface isEqualToString:@"localhost"] || [interface isEqualToString:@"loopback"]) { // LOOPBACK address - + struct sockaddr_in sockaddr4; memset(&sockaddr4, 0, sizeof(sockaddr4)); - + sockaddr4.sin_len = sizeof(sockaddr4); sockaddr4.sin_family = AF_INET; sockaddr4.sin_port = htons(port); sockaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); - + struct sockaddr_in6 sockaddr6; memset(&sockaddr6, 0, sizeof(sockaddr6)); - + sockaddr6.sin6_len = sizeof(sockaddr6); sockaddr6.sin6_family = AF_INET6; sockaddr6.sin6_port = htons(port); sockaddr6.sin6_addr = in6addr_loopback; - + addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; } else { const char *iface = [interface UTF8String]; - + struct ifaddrs *addrs; const struct ifaddrs *cursor; - + if ((getifaddrs(&addrs) == 0)) { cursor = addrs; @@ -4157,30 +4167,30 @@ - (void)getInterfaceAddress4:(NSMutableData **)interfaceAddr4Ptr if ((addr4 == nil) && (cursor->ifa_addr->sa_family == AF_INET)) { // IPv4 - + struct sockaddr_in nativeAddr4; memcpy(&nativeAddr4, cursor->ifa_addr, sizeof(nativeAddr4)); - + if (strcmp(cursor->ifa_name, iface) == 0) { // Name match - + nativeAddr4.sin_port = htons(port); - + addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; } else { char ip[INET_ADDRSTRLEN]; - + const char *conversion = inet_ntop(AF_INET, &nativeAddr4.sin_addr, ip, sizeof(ip)); - + if ((conversion != NULL) && (strcmp(ip, iface) == 0)) { // IP match - + nativeAddr4.sin_port = htons(port); - + addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; } } @@ -4188,42 +4198,42 @@ - (void)getInterfaceAddress4:(NSMutableData **)interfaceAddr4Ptr else if ((addr6 == nil) && (cursor->ifa_addr->sa_family == AF_INET6)) { // IPv6 - + struct sockaddr_in6 nativeAddr6; memcpy(&nativeAddr6, cursor->ifa_addr, sizeof(nativeAddr6)); - + if (strcmp(cursor->ifa_name, iface) == 0) { // Name match - + nativeAddr6.sin6_port = htons(port); - + addr6 = [NSMutableData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; } else { char ip[INET6_ADDRSTRLEN]; - + const char *conversion = inet_ntop(AF_INET6, &nativeAddr6.sin6_addr, ip, sizeof(ip)); - + if ((conversion != NULL) && (strcmp(ip, iface) == 0)) { // IP match - + nativeAddr6.sin6_port = htons(port); - + addr6 = [NSMutableData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; } } } - + cursor = cursor->ifa_next; } - + freeifaddrs(addrs); } } - + if (interfaceAddr4Ptr) *interfaceAddr4Ptr = addr4; if (interfaceAddr6Ptr) *interfaceAddr6Ptr = addr6; } @@ -4234,13 +4244,13 @@ - (NSData *)getInterfaceAddressFromUrl:(NSURL *)url if (path.length == 0) { return nil; } - + struct sockaddr_un nativeAddr; nativeAddr.sun_family = AF_UNIX; strlcpy(nativeAddr.sun_path, path.fileSystemRepresentation, sizeof(nativeAddr.sun_path)); nativeAddr.sun_len = (unsigned char)SUN_LEN(&nativeAddr); NSData *interface = [NSData dataWithBytes:&nativeAddr length:sizeof(struct sockaddr_un)]; - + return interface; } @@ -4248,104 +4258,104 @@ - (void)setupReadAndWriteSourcesForNewlyConnectedSocket:(int)socketFD { readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socketFD, 0, socketQueue); writeSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, socketFD, 0, socketQueue); - + // Setup event handlers - + __weak GCDAsyncSocket *weakSelf = self; - + dispatch_source_set_event_handler(readSource, ^{ @autoreleasepool { #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" - + __strong GCDAsyncSocket *strongSelf = weakSelf; if (strongSelf == nil) return_from_block; - + LogVerbose(@"readEventBlock"); - + strongSelf->socketFDBytesAvailable = dispatch_source_get_data(strongSelf->readSource); LogVerbose(@"socketFDBytesAvailable: %lu", strongSelf->socketFDBytesAvailable); - + if (strongSelf->socketFDBytesAvailable > 0) [strongSelf doReadData]; else [strongSelf doReadEOF]; - + #pragma clang diagnostic pop }}); - + dispatch_source_set_event_handler(writeSource, ^{ @autoreleasepool { #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" - + __strong GCDAsyncSocket *strongSelf = weakSelf; if (strongSelf == nil) return_from_block; - + LogVerbose(@"writeEventBlock"); - + strongSelf->flags |= kSocketCanAcceptBytes; [strongSelf doWriteData]; - + #pragma clang diagnostic pop }}); - + // Setup cancel handlers - + __block int socketFDRefCount = 2; - + #if !OS_OBJECT_USE_OBJC dispatch_source_t theReadSource = readSource; dispatch_source_t theWriteSource = writeSource; #endif - + dispatch_source_set_cancel_handler(readSource, ^{ #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" - + LogVerbose(@"readCancelBlock"); - + #if !OS_OBJECT_USE_OBJC LogVerbose(@"dispatch_release(readSource)"); dispatch_release(theReadSource); #endif - + if (--socketFDRefCount == 0) { LogVerbose(@"close(socketFD)"); close(socketFD); } - + #pragma clang diagnostic pop }); - + dispatch_source_set_cancel_handler(writeSource, ^{ #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" - + LogVerbose(@"writeCancelBlock"); - + #if !OS_OBJECT_USE_OBJC LogVerbose(@"dispatch_release(writeSource)"); dispatch_release(theWriteSource); #endif - + if (--socketFDRefCount == 0) { LogVerbose(@"close(socketFD)"); close(socketFD); } - + #pragma clang diagnostic pop }); - + // We will not be able to read until data arrives. // But we should be able to write immediately. - + socketFDBytesAvailable = 0; flags &= ~kReadSourceSuspended; - + LogVerbose(@"dispatch_resume(readSource)"); dispatch_resume(readSource); - + flags |= kSocketCanAcceptBytes; flags |= kWriteSourceSuspended; } @@ -4353,34 +4363,34 @@ - (void)setupReadAndWriteSourcesForNewlyConnectedSocket:(int)socketFD - (BOOL)usingCFStreamForTLS { #if TARGET_OS_IPHONE - + if ((flags & kSocketSecure) && (flags & kUsingCFStreamForTLS)) { // The startTLS method was given the GCDAsyncSocketUseCFStreamForTLS flag. - + return YES; } - + #endif - + return NO; } - (BOOL)usingSecureTransportForTLS { // Invoking this method is equivalent to ![self usingCFStreamForTLS] (just more readable) - + #if TARGET_OS_IPHONE - + if ((flags & kSocketSecure) && (flags & kUsingCFStreamForTLS)) { // The startTLS method was given the GCDAsyncSocketUseCFStreamForTLS flag. - + return NO; } - + #endif - + return YES; } @@ -4389,7 +4399,7 @@ - (void)suspendReadSource if (!(flags & kReadSourceSuspended)) { LogVerbose(@"dispatch_suspend(readSource)"); - + dispatch_suspend(readSource); flags |= kReadSourceSuspended; } @@ -4400,7 +4410,7 @@ - (void)resumeReadSource if (flags & kReadSourceSuspended) { LogVerbose(@"dispatch_resume(readSource)"); - + dispatch_resume(readSource); flags &= ~kReadSourceSuspended; } @@ -4411,7 +4421,7 @@ - (void)suspendWriteSource if (!(flags & kWriteSourceSuspended)) { LogVerbose(@"dispatch_suspend(writeSource)"); - + dispatch_suspend(writeSource); flags |= kWriteSourceSuspended; } @@ -4422,7 +4432,7 @@ - (void)resumeWriteSource if (flags & kWriteSourceSuspended) { LogVerbose(@"dispatch_resume(writeSource)"); - + dispatch_resume(writeSource); flags &= ~kWriteSourceSuspended; } @@ -4455,7 +4465,7 @@ - (void)readDataWithTimeout:(NSTimeInterval)timeout LogWarn(@"Cannot read: offset > [buffer length]"); return; } - + GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer startOffset:offset maxLength:length @@ -4463,18 +4473,18 @@ - (void)readDataWithTimeout:(NSTimeInterval)timeout readLength:0 terminator:nil tag:tag]; - + dispatch_async(socketQueue, ^{ @autoreleasepool { - + LogTrace(); - + if ((self->flags & kSocketStarted) && !(self->flags & kForbidReadsWrites)) { [self->readQueue addObject:packet]; [self maybeDequeueRead]; } }}); - + // Do not rely on the block being run in order to release the packet, // as the queue might get released without the block completing. } @@ -4498,7 +4508,7 @@ - (void)readDataToLength:(NSUInteger)length LogWarn(@"Cannot read: offset > [buffer length]"); return; } - + GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer startOffset:offset maxLength:0 @@ -4506,18 +4516,18 @@ - (void)readDataToLength:(NSUInteger)length readLength:length terminator:nil tag:tag]; - + dispatch_async(socketQueue, ^{ @autoreleasepool { - + LogTrace(); - + if ((self->flags & kSocketStarted) && !(self->flags & kForbidReadsWrites)) { [self->readQueue addObject:packet]; [self maybeDequeueRead]; } }}); - + // Do not rely on the block being run in order to release the packet, // as the queue might get released without the block completing. } @@ -4560,7 +4570,7 @@ - (void)readDataToData:(NSData *)data LogWarn(@"Cannot read: maxLength > 0 && maxLength < [data length]"); return; } - + GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer startOffset:offset maxLength:maxLength @@ -4568,18 +4578,18 @@ - (void)readDataToData:(NSData *)data readLength:0 terminator:data tag:tag]; - + dispatch_async(socketQueue, ^{ @autoreleasepool { - + LogTrace(); - + if ((self->flags & kSocketStarted) && !(self->flags & kForbidReadsWrites)) { [self->readQueue addObject:packet]; [self maybeDequeueRead]; } }}); - + // Do not rely on the block being run in order to release the packet, // as the queue might get released without the block completing. } @@ -4587,17 +4597,17 @@ - (void)readDataToData:(NSData *)data - (float)progressOfReadReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr { __block float result = 0.0F; - + dispatch_block_t block = ^{ - + if (!self->currentRead || ![self->currentRead isKindOfClass:[GCDAsyncReadPacket class]]) { // We're not reading anything right now. - + if (tagPtr != NULL) *tagPtr = 0; if (donePtr != NULL) *donePtr = 0; if (totalPtr != NULL) *totalPtr = 0; - + result = NAN; } else @@ -4605,44 +4615,44 @@ - (float)progressOfReadReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)doneP // It's only possible to know the progress of our read if we're reading to a certain length. // If we're reading to data, we of course have no idea when the data will arrive. // If we're reading to timeout, then we have no idea when the next chunk of data will arrive. - + NSUInteger done = self->currentRead->bytesDone; NSUInteger total = self->currentRead->readLength; - + if (tagPtr != NULL) *tagPtr = self->currentRead->tag; if (donePtr != NULL) *donePtr = done; if (totalPtr != NULL) *totalPtr = total; - + if (total > 0) result = (float)done / (float)total; else result = 1.0F; } }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); - + return result; } /** * This method starts a new read, if needed. - * + * * It is called when: * - a user requests a read * - after a read request has finished (to handle the next request) * - immediately after the socket opens to handle any pending requests - * + * * This method also handles auto-disconnect post read/write completion. **/ - (void)maybeDequeueRead { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - + // If we're not currently processing a read AND we have an available read stream if ((currentRead == nil) && (flags & kConnected)) { @@ -4651,25 +4661,25 @@ - (void)maybeDequeueRead // Dequeue the next object in the write queue currentRead = [readQueue objectAtIndex:0]; [readQueue removeObjectAtIndex:0]; - - + + if ([currentRead isKindOfClass:[GCDAsyncSpecialPacket class]]) { LogVerbose(@"Dequeued GCDAsyncSpecialPacket"); - + // Attempt to start TLS flags |= kStartingReadTLS; - + // This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are set [self maybeStartTLS]; } else { LogVerbose(@"Dequeued GCDAsyncReadPacket"); - + // Setup read timer (if needed) [self setupReadTimerWithTimeout:currentRead->timeout]; - + // Immediately read, if possible [self doReadData]; } @@ -4691,17 +4701,17 @@ - (void)maybeDequeueRead else if (flags & kSocketSecure) { [self flushSSLBuffers]; - + // Edge case: - // + // // We just drained all data from the ssl buffers, // and all known data from the socket (socketFDBytesAvailable). - // + // // If we didn't get any data from this process, // then we may have reached the end of the TCP stream. - // + // // Be sure callbacks are enabled so we're notified about a disconnection. - + if ([preBuffer availableBytes] == 0) { if ([self usingCFStreamForTLS]) { @@ -4718,99 +4728,99 @@ - (void)maybeDequeueRead - (void)flushSSLBuffers { LogTrace(); - + NSAssert((flags & kSocketSecure), @"Cannot flush ssl buffers on non-secure socket"); - + if ([preBuffer availableBytes] > 0) { // Only flush the ssl buffers if the prebuffer is empty. // This is to avoid growing the prebuffer inifinitely large. - + return; } - + #if TARGET_OS_IPHONE - + if ([self usingCFStreamForTLS]) { if ((flags & kSecureSocketHasBytesAvailable) && CFReadStreamHasBytesAvailable(readStream)) { LogVerbose(@"%@ - Flushing ssl buffers into prebuffer...", THIS_METHOD); - + CFIndex defaultBytesToRead = (1024 * 4); - + [preBuffer ensureCapacityForWrite:defaultBytesToRead]; - + uint8_t *buffer = [preBuffer writeBuffer]; - + CFIndex result = CFReadStreamRead(readStream, buffer, defaultBytesToRead); LogVerbose(@"%@ - CFReadStreamRead(): result = %i", THIS_METHOD, (int)result); - + if (result > 0) { [preBuffer didWrite:result]; } - + flags &= ~kSecureSocketHasBytesAvailable; } - + return; } - + #endif - + __block NSUInteger estimatedBytesAvailable = 0; - + dispatch_block_t updateEstimatedBytesAvailable = ^{ - + // Figure out if there is any data available to be read - // + // // socketFDBytesAvailable <- Number of encrypted bytes we haven't read from the bsd socket // [sslPreBuffer availableBytes] <- Number of encrypted bytes we've buffered from bsd socket // sslInternalBufSize <- Number of decrypted bytes SecureTransport has buffered - // + // // We call the variable "estimated" because we don't know how many decrypted bytes we'll get // from the encrypted bytes in the sslPreBuffer. // However, we do know this is an upper bound on the estimation. - + estimatedBytesAvailable = self->socketFDBytesAvailable + [self->sslPreBuffer availableBytes]; - + size_t sslInternalBufSize = 0; SSLGetBufferedReadSize(self->sslContext, &sslInternalBufSize); - + estimatedBytesAvailable += sslInternalBufSize; }; - + updateEstimatedBytesAvailable(); - + if (estimatedBytesAvailable > 0) { LogVerbose(@"%@ - Flushing ssl buffers into prebuffer...", THIS_METHOD); - + BOOL done = NO; do { LogVerbose(@"%@ - estimatedBytesAvailable = %lu", THIS_METHOD, (unsigned long)estimatedBytesAvailable); - + // Make sure there's enough room in the prebuffer - + [preBuffer ensureCapacityForWrite:estimatedBytesAvailable]; - + // Read data into prebuffer - + uint8_t *buffer = [preBuffer writeBuffer]; size_t bytesRead = 0; - + OSStatus result = SSLRead(sslContext, buffer, (size_t)estimatedBytesAvailable, &bytesRead); LogVerbose(@"%@ - read from secure socket = %u", THIS_METHOD, (unsigned)bytesRead); - + if (bytesRead > 0) { [preBuffer didWrite:bytesRead]; } - + LogVerbose(@"%@ - prebuffer.length = %zu", THIS_METHOD, [preBuffer availableBytes]); - + if (result != noErr) { done = YES; @@ -4819,7 +4829,7 @@ - (void)flushSSLBuffers { updateEstimatedBytesAvailable(); } - + } while (!done && estimatedBytesAvailable > 0); } } @@ -4827,35 +4837,35 @@ - (void)flushSSLBuffers - (void)doReadData { LogTrace(); - + // This method is called on the socketQueue. // It might be called directly, or via the readSource when data is available to be read. - + if ((currentRead == nil) || (flags & kReadsPaused)) { LogVerbose(@"No currentRead or kReadsPaused"); - + // Unable to read at this time - + if (flags & kSocketSecure) { // Here's the situation: - // + // // We have an established secure connection. // There may not be a currentRead, but there might be encrypted data sitting around for us. // When the user does get around to issuing a read, that encrypted data will need to be decrypted. - // + // // So why make the user wait? // We might as well get a head start on decrypting some data now. - // + // // The other reason we do this has to do with detecting a socket disconnection. // The SSL/TLS protocol has it's own disconnection handshake. // So when a secure socket is closed, a "goodbye" packet comes across the wire. // We want to make sure we read the "goodbye" packet so we can properly detect the TCP disconnection. - + [self flushSSLBuffers]; } - + if ([self usingCFStreamForTLS]) { // CFReadStream only fires once when there is available data. @@ -4865,10 +4875,10 @@ - (void)doReadData { // If the readSource is firing, we need to pause it // or else it will continue to fire over and over again. - // + // // If the readSource is not firing, // we want it to continue monitoring the socket. - + if (socketFDBytesAvailable > 0) { [self suspendReadSource]; @@ -4876,96 +4886,96 @@ - (void)doReadData } return; } - + BOOL hasBytesAvailable = NO; unsigned long estimatedBytesAvailable = 0; - + if ([self usingCFStreamForTLS]) { #if TARGET_OS_IPHONE - + // Requested CFStream, rather than SecureTransport, for TLS (via GCDAsyncSocketUseCFStreamForTLS) - + estimatedBytesAvailable = 0; if ((flags & kSecureSocketHasBytesAvailable) && CFReadStreamHasBytesAvailable(readStream)) hasBytesAvailable = YES; else hasBytesAvailable = NO; - + #endif } else { estimatedBytesAvailable = socketFDBytesAvailable; - + if (flags & kSocketSecure) { // There are 2 buffers to be aware of here. - // + // // We are using SecureTransport, a TLS/SSL security layer which sits atop TCP. // We issue a read to the SecureTranport API, which in turn issues a read to our SSLReadFunction. // Our SSLReadFunction then reads from the BSD socket and returns the encrypted data to SecureTransport. // SecureTransport then decrypts the data, and finally returns the decrypted data back to us. - // + // // The first buffer is one we create. // SecureTransport often requests small amounts of data. // This has to do with the encypted packets that are coming across the TCP stream. // But it's non-optimal to do a bunch of small reads from the BSD socket. // So our SSLReadFunction reads all available data from the socket (optimizing the sys call) // and may store excess in the sslPreBuffer. - + estimatedBytesAvailable += [sslPreBuffer availableBytes]; - + // The second buffer is within SecureTransport. // As mentioned earlier, there are encrypted packets coming across the TCP stream. // SecureTransport needs the entire packet to decrypt it. // But if the entire packet produces X bytes of decrypted data, // and we only asked SecureTransport for X/2 bytes of data, // it must store the extra X/2 bytes of decrypted data for the next read. - // + // // The SSLGetBufferedReadSize function will tell us the size of this internal buffer. // From the documentation: - // + // // "This function does not block or cause any low-level read operations to occur." - + size_t sslInternalBufSize = 0; SSLGetBufferedReadSize(sslContext, &sslInternalBufSize); - + estimatedBytesAvailable += sslInternalBufSize; } - + hasBytesAvailable = (estimatedBytesAvailable > 0); } - + if ((hasBytesAvailable == NO) && ([preBuffer availableBytes] == 0)) { LogVerbose(@"No data available to read..."); - + // No data available to read. - + if (![self usingCFStreamForTLS]) { // Need to wait for readSource to fire and notify us of // available data in the socket's internal read buffer. - + [self resumeReadSource]; } return; } - + if (flags & kStartingReadTLS) { LogVerbose(@"Waiting for SSL/TLS handshake to complete"); - + // The readQueue is waiting for SSL/TLS handshake to complete. - + if (flags & kStartingWriteTLS) { if ([self usingSecureTransportForTLS] && lastSSLHandshakeError == errSSLWouldBlock) { // We are in the process of a SSL Handshake. // We were waiting for incoming data which has just arrived. - + [self ssl_continueSSLHandshake]; } } @@ -4973,90 +4983,90 @@ - (void)doReadData { // We are still waiting for the writeQueue to drain and start the SSL/TLS process. // We now know data is available to read. - + if (![self usingCFStreamForTLS]) { // Suspend the read source or else it will continue to fire nonstop. - + [self suspendReadSource]; } } - + return; } - + BOOL done = NO; // Completed read operation NSError *error = nil; // Error occurred - + NSUInteger totalBytesReadForCurrentRead = 0; - - // + + // // STEP 1 - READ FROM PREBUFFER - // - + // + if ([preBuffer availableBytes] > 0) { // There are 3 types of read packets: - // + // // 1) Read all available data. // 2) Read a specific length of data. // 3) Read up to a particular terminator. - + NSUInteger bytesToCopy; - + if (currentRead->term != nil) { // Read type #3 - read up to a terminator - + bytesToCopy = [currentRead readLengthForTermWithPreBuffer:preBuffer found:&done]; } else { // Read type #1 or #2 - + bytesToCopy = [currentRead readLengthForNonTermWithHint:[preBuffer availableBytes]]; } - + // Make sure we have enough room in the buffer for our read. - + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToCopy]; - + // Copy bytes from prebuffer into packet buffer - + uint8_t *buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + currentRead->bytesDone; - + memcpy(buffer, [preBuffer readBuffer], bytesToCopy); - + // Remove the copied bytes from the preBuffer [preBuffer didRead:bytesToCopy]; - + LogVerbose(@"copied(%lu) preBufferLength(%zu)", (unsigned long)bytesToCopy, [preBuffer availableBytes]); - + // Update totals - + currentRead->bytesDone += bytesToCopy; totalBytesReadForCurrentRead += bytesToCopy; - + // Check to see if the read operation is done - + if (currentRead->readLength > 0) { // Read type #2 - read a specific length of data - + done = (currentRead->bytesDone == currentRead->readLength); } else if (currentRead->term != nil) { // Read type #3 - read up to a terminator - + // Our 'done' variable was updated via the readLengthForTermWithPreBuffer:found: method - + if (!done && currentRead->maxLength > 0) { // We're not done and there's a set maxLength. // Have we reached that maxLength yet? - + if (currentRead->bytesDone >= currentRead->maxLength) { error = [self readMaxedOutError]; @@ -5066,69 +5076,69 @@ - (void)doReadData else { // Read type #1 - read all available data - // + // // We're done as soon as // - we've read all available data (in prebuffer and socket) // - we've read the maxLength of read packet. - + done = ((currentRead->maxLength > 0) && (currentRead->bytesDone == currentRead->maxLength)); } - + } - - // + + // // STEP 2 - READ FROM SOCKET - // - + // + BOOL socketEOF = (flags & kSocketHasReadEOF) ? YES : NO; // Nothing more to read via socket (end of file) BOOL waiting = !done && !error && !socketEOF && !hasBytesAvailable; // Ran out of data, waiting for more - + if (!done && !error && !socketEOF && hasBytesAvailable) { NSAssert(([preBuffer availableBytes] == 0), @"Invalid logic"); - + BOOL readIntoPreBuffer = NO; uint8_t *buffer = NULL; size_t bytesRead = 0; - + if (flags & kSocketSecure) { if ([self usingCFStreamForTLS]) { #if TARGET_OS_IPHONE - + // Using CFStream, rather than SecureTransport, for TLS - + NSUInteger defaultReadLength = (1024 * 32); - + NSUInteger bytesToRead = [currentRead optimalReadLengthWithDefault:defaultReadLength shouldPreBuffer:&readIntoPreBuffer]; - + // Make sure we have enough room in the buffer for our read. // // We are either reading directly into the currentRead->buffer, // or we're reading into the temporary preBuffer. - + if (readIntoPreBuffer) { [preBuffer ensureCapacityForWrite:bytesToRead]; - + buffer = [preBuffer writeBuffer]; } else { [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead]; - + buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + currentRead->bytesDone; } - + // Read data into buffer - + CFIndex result = CFReadStreamRead(readStream, buffer, (CFIndex)bytesToRead); LogVerbose(@"CFReadStreamRead(): result = %i", (int)result); - + if (result < 0) { error = (__bridge_transfer NSError *)CFReadStreamCopyError(readStream); @@ -5142,12 +5152,12 @@ - (void)doReadData waiting = YES; bytesRead = (size_t)result; } - + // We only know how many decrypted bytes were read. // The actual number of bytes read was likely more due to the overhead of the encryption. // So we reset our flag, and rely on the next callback to alert us of more data. flags &= ~kSecureSocketHasBytesAvailable; - + #endif } else @@ -5163,63 +5173,63 @@ - (void)doReadData // - how many encypted bytes are sitting in the sslContext // // So we play the regular game of using an upper bound instead. - + NSUInteger defaultReadLength = (1024 * 32); - + if (defaultReadLength < estimatedBytesAvailable) { defaultReadLength = estimatedBytesAvailable + (1024 * 16); } - + NSUInteger bytesToRead = [currentRead optimalReadLengthWithDefault:defaultReadLength shouldPreBuffer:&readIntoPreBuffer]; - + if (bytesToRead > SIZE_MAX) { // NSUInteger may be bigger than size_t bytesToRead = SIZE_MAX; } - + // Make sure we have enough room in the buffer for our read. // // We are either reading directly into the currentRead->buffer, // or we're reading into the temporary preBuffer. - + if (readIntoPreBuffer) { [preBuffer ensureCapacityForWrite:bytesToRead]; - + buffer = [preBuffer writeBuffer]; } else { [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead]; - + buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + currentRead->bytesDone; } - + // The documentation from Apple states: - // + // // "a read operation might return errSSLWouldBlock, // indicating that less data than requested was actually transferred" - // + // // However, starting around 10.7, the function will sometimes return noErr, // even if it didn't read as much data as requested. So we need to watch out for that. - + OSStatus result; do { void *loop_buffer = buffer + bytesRead; size_t loop_bytesToRead = (size_t)bytesToRead - bytesRead; size_t loop_bytesRead = 0; - + result = SSLRead(sslContext, loop_buffer, loop_bytesToRead, &loop_bytesRead); LogVerbose(@"read from secure socket = %u", (unsigned)loop_bytesRead); - + bytesRead += loop_bytesRead; - + } while ((result == noErr) && (bytesRead < bytesToRead)); - - + + if (result != noErr) { if (result == errSSLWouldBlock) @@ -5241,13 +5251,13 @@ - (void)doReadData // It's possible that bytesRead > 0, even if the result was errSSLWouldBlock. // This happens when the SSLRead function is able to read some data, // but not the entire amount we requested. - + if (bytesRead <= 0) { bytesRead = 0; } } - + // Do not modify socketFDBytesAvailable. // It will be updated via the SSLReadFunction(). } @@ -5255,67 +5265,67 @@ - (void)doReadData else { // Normal socket operation - + NSUInteger bytesToRead; - + // There are 3 types of read packets: // // 1) Read all available data. // 2) Read a specific length of data. // 3) Read up to a particular terminator. - + if (currentRead->term != nil) { // Read type #3 - read up to a terminator - + bytesToRead = [currentRead readLengthForTermWithHint:estimatedBytesAvailable shouldPreBuffer:&readIntoPreBuffer]; } else { // Read type #1 or #2 - + bytesToRead = [currentRead readLengthForNonTermWithHint:estimatedBytesAvailable]; } - + if (bytesToRead > SIZE_MAX) { // NSUInteger may be bigger than size_t (read param 3) bytesToRead = SIZE_MAX; } - + // Make sure we have enough room in the buffer for our read. // // We are either reading directly into the currentRead->buffer, // or we're reading into the temporary preBuffer. - + if (readIntoPreBuffer) { [preBuffer ensureCapacityForWrite:bytesToRead]; - + buffer = [preBuffer writeBuffer]; } else { [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead]; - + buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + currentRead->bytesDone; } - + // Read data into buffer - + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; - + ssize_t result = read(socketFD, buffer, (size_t)bytesToRead); LogVerbose(@"read from socket = %i", (int)result); - + if (result < 0) { if (errno == EWOULDBLOCK) waiting = YES; else error = [self errorWithErrno:errno reason:@"Error in read() function"]; - + socketFDBytesAvailable = 0; } else if (result == 0) @@ -5326,7 +5336,7 @@ - (void)doReadData else { bytesRead = result; - + if (bytesRead < bytesToRead) { // The read returned less data than requested. @@ -5341,81 +5351,81 @@ - (void)doReadData else socketFDBytesAvailable -= bytesRead; } - + if (socketFDBytesAvailable == 0) { waiting = YES; } } } - + if (bytesRead > 0) { // Check to see if the read operation is done - + if (currentRead->readLength > 0) { // Read type #2 - read a specific length of data - // + // // Note: We should never be using a prebuffer when we're reading a specific length of data. - + NSAssert(readIntoPreBuffer == NO, @"Invalid logic"); - + currentRead->bytesDone += bytesRead; totalBytesReadForCurrentRead += bytesRead; - + done = (currentRead->bytesDone == currentRead->readLength); } else if (currentRead->term != nil) { // Read type #3 - read up to a terminator - + if (readIntoPreBuffer) { // We just read a big chunk of data into the preBuffer - + [preBuffer didWrite:bytesRead]; LogVerbose(@"read data into preBuffer - preBuffer.length = %zu", [preBuffer availableBytes]); - + // Search for the terminating sequence - + NSUInteger bytesToCopy = [currentRead readLengthForTermWithPreBuffer:preBuffer found:&done]; LogVerbose(@"copying %lu bytes from preBuffer", (unsigned long)bytesToCopy); - + // Ensure there's room on the read packet's buffer - + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToCopy]; - + // Copy bytes from prebuffer into read buffer - + uint8_t *readBuf = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + currentRead->bytesDone; - + memcpy(readBuf, [preBuffer readBuffer], bytesToCopy); - + // Remove the copied bytes from the prebuffer [preBuffer didRead:bytesToCopy]; LogVerbose(@"preBuffer.length = %zu", [preBuffer availableBytes]); - + // Update totals currentRead->bytesDone += bytesToCopy; totalBytesReadForCurrentRead += bytesToCopy; - + // Our 'done' variable was updated via the readLengthForTermWithPreBuffer:found: method above } else { // We just read a big chunk of data directly into the packet's buffer. // We need to move any overflow into the prebuffer. - + NSInteger overflow = [currentRead searchForTermAfterPreBuffering:bytesRead]; - + if (overflow == 0) { // Perfect match! // Every byte we read stays in the read buffer, // and the last byte we read was the last byte of the term. - + currentRead->bytesDone += bytesRead; totalBytesReadForCurrentRead += bytesRead; done = YES; @@ -5425,22 +5435,22 @@ - (void)doReadData // The term was found within the data that we read, // and there are extra bytes that extend past the end of the term. // We need to move these excess bytes out of the read packet and into the prebuffer. - + NSInteger underflow = bytesRead - overflow; - + // Copy excess data into preBuffer - + LogVerbose(@"copying %ld overflow bytes into preBuffer", (long)overflow); [preBuffer ensureCapacityForWrite:overflow]; - + uint8_t *overflowBuffer = buffer + underflow; memcpy([preBuffer writeBuffer], overflowBuffer, overflow); - + [preBuffer didWrite:overflow]; LogVerbose(@"preBuffer.length = %zu", [preBuffer availableBytes]); - + // Note: The completeCurrentRead method will trim the buffer for us. - + currentRead->bytesDone += underflow; totalBytesReadForCurrentRead += underflow; done = YES; @@ -5448,18 +5458,18 @@ - (void)doReadData else { // The term was not found within the data that we read. - + currentRead->bytesDone += bytesRead; totalBytesReadForCurrentRead += bytesRead; done = NO; } } - + if (!done && currentRead->maxLength > 0) { // We're not done and there's a set maxLength. // Have we reached that maxLength yet? - + if (currentRead->bytesDone >= currentRead->maxLength) { error = [self readMaxedOutError]; @@ -5469,32 +5479,32 @@ - (void)doReadData else { // Read type #1 - read all available data - + if (readIntoPreBuffer) { // We just read a chunk of data into the preBuffer - + [preBuffer didWrite:bytesRead]; - + // Now copy the data into the read packet. - // + // // Recall that we didn't read directly into the packet's buffer to avoid // over-allocating memory since we had no clue how much data was available to be read. - // + // // Ensure there's room on the read packet's buffer - + [currentRead ensureCapacityForAdditionalDataOfLength:bytesRead]; - + // Copy bytes from prebuffer into read buffer - + uint8_t *readBuf = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + currentRead->bytesDone; - + memcpy(readBuf, [preBuffer readBuffer], bytesRead); - + // Remove the copied bytes from the prebuffer [preBuffer didRead:bytesRead]; - + // Update totals currentRead->bytesDone += bytesRead; totalBytesReadForCurrentRead += bytesRead; @@ -5504,30 +5514,30 @@ - (void)doReadData currentRead->bytesDone += bytesRead; totalBytesReadForCurrentRead += bytesRead; } - + done = YES; } - + } // if (bytesRead > 0) - + } // if (!done && !error && !socketEOF && hasBytesAvailable) - - + + if (!done && currentRead->readLength == 0 && currentRead->term == nil) { // Read type #1 - read all available data - // + // // We might arrive here if we read data from the prebuffer but not from the socket. - + done = (totalBytesReadForCurrentRead > 0); } - + // Check to see if we're done, or if we've made progress - + if (done) { [self completeCurrentRead]; - + if (!error && (!socketEOF || [preBuffer availableBytes] > 0)) { [self maybeDequeueRead]; @@ -5544,20 +5554,20 @@ - (void)doReadData waiting = YES; __strong id theDelegate = delegate; - + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReadPartialDataOfLength:tag:)]) { long theReadTag = currentRead->tag; - + dispatch_async(delegateQueue, ^{ @autoreleasepool { - + [theDelegate socket:self didReadPartialDataOfLength:totalBytesReadForCurrentRead tag:theReadTag]; }}); } } - + // Check for errors - + if (error) { [self closeWithError:error]; @@ -5574,37 +5584,37 @@ - (void)doReadData [self resumeReadSource]; } } - + // Do not add any code here without first adding return statements in the error cases above. } - (void)doReadEOF { LogTrace(); - + // This method may be called more than once. // If the EOF is read while there is still data in the preBuffer, // then this method may be called continually after invocations of doReadData to see if it's time to disconnect. - + flags |= kSocketHasReadEOF; - + if (flags & kSocketSecure) { // If the SSL layer has any buffered data, flush it into the preBuffer now. - + [self flushSSLBuffers]; } - + BOOL shouldDisconnect = NO; NSError *error = nil; - + if ((flags & kStartingReadTLS) || (flags & kStartingWriteTLS)) { // We received an EOF during or prior to startTLS. // The SSL/TLS handshake is now impossible, so this is an unrecoverable situation. - + shouldDisconnect = YES; - + if ([self usingSecureTransportForTLS]) { error = [self sslError:errSSLClosedAbort]; @@ -5616,19 +5626,19 @@ - (void)doReadEOF // The config allows half-duplex connections. // We've previously checked the socket, and it appeared writeable. // So we marked the read stream as closed and notified the delegate. - // + // // As per the half-duplex contract, the socket will be closed when a write fails, // or when the socket is manually closed. - + shouldDisconnect = NO; } else if ([preBuffer availableBytes] > 0) { LogVerbose(@"Socket reached EOF, but there is still data available in prebuffer"); - + // Although we won't be able to read any more data from the socket, // there is existing data that has been prebuffered that we can read. - + shouldDisconnect = NO; } else if (config & kAllowHalfDuplexConnection) @@ -5636,33 +5646,33 @@ - (void)doReadEOF // We just received an EOF (end of file) from the socket's read stream. // This means the remote end of the socket (the peer we're connected to) // has explicitly stated that it will not be sending us any more data. - // + // // Query the socket to see if it is still writeable. (Perhaps the peer will continue reading data from us) - + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; - + struct pollfd pfd[1]; pfd[0].fd = socketFD; pfd[0].events = POLLOUT; pfd[0].revents = 0; - + poll(pfd, 1, 0); - + if (pfd[0].revents & POLLOUT) { // Socket appears to still be writeable - + shouldDisconnect = NO; flags |= kReadStreamClosed; - + // Notify the delegate that we're going half-duplex - + __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidCloseReadStream:)]) { dispatch_async(delegateQueue, ^{ @autoreleasepool { - + [theDelegate socketDidCloseReadStream:self]; }}); } @@ -5676,8 +5686,8 @@ - (void)doReadEOF { shouldDisconnect = YES; } - - + + if (shouldDisconnect) { if (error == nil) @@ -5705,7 +5715,7 @@ - (void)doReadEOF if (![self usingCFStreamForTLS]) { // Suspend the read source (if needed) - + [self suspendReadSource]; } } @@ -5714,18 +5724,18 @@ - (void)doReadEOF - (void)completeCurrentRead { LogTrace(); - + NSAssert(currentRead, @"Trying to complete current read when there is no current read."); - - + + NSData *result = nil; - + if (currentRead->bufferOwner) { // We created the buffer on behalf of the user. // Trim our buffer to be the proper size. [currentRead->buffer setLength:currentRead->bytesDone]; - + result = currentRead->buffer; } else @@ -5733,34 +5743,34 @@ - (void)completeCurrentRead // We did NOT create the buffer. // The buffer is owned by the caller. // Only trim the buffer if we had to increase its size. - + if ([currentRead->buffer length] > currentRead->originalBufferLength) { NSUInteger readSize = currentRead->startOffset + currentRead->bytesDone; NSUInteger origSize = currentRead->originalBufferLength; - + NSUInteger buffSize = MAX(readSize, origSize); - + [currentRead->buffer setLength:buffSize]; } - + uint8_t *buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset; - + result = [NSData dataWithBytesNoCopy:buffer length:currentRead->bytesDone freeWhenDone:NO]; } - + __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReadData:withTag:)]) { GCDAsyncReadPacket *theRead = currentRead; // Ensure currentRead retained since result may not own buffer - + dispatch_async(delegateQueue, ^{ @autoreleasepool { - + [theDelegate socket:self didReadData:result withTag:theRead->tag]; }}); } - + [self endCurrentRead]; } @@ -5771,7 +5781,7 @@ - (void)endCurrentRead dispatch_source_cancel(readTimer); readTimer = NULL; } - + currentRead = nil; } @@ -5780,36 +5790,36 @@ - (void)setupReadTimerWithTimeout:(NSTimeInterval)timeout if (timeout >= 0.0) { readTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue); - + __weak GCDAsyncSocket *weakSelf = self; - + dispatch_source_set_event_handler(readTimer, ^{ @autoreleasepool { #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" - + __strong GCDAsyncSocket *strongSelf = weakSelf; if (strongSelf == nil) return_from_block; - + [strongSelf doReadTimeout]; - + #pragma clang diagnostic pop }}); - + #if !OS_OBJECT_USE_OBJC dispatch_source_t theReadTimer = readTimer; dispatch_source_set_cancel_handler(readTimer, ^{ #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" - + LogVerbose(@"dispatch_release(readTimer)"); dispatch_release(theReadTimer); - + #pragma clang diagnostic pop }); #endif - + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)); - + dispatch_source_set_timer(readTimer, tt, DISPATCH_TIME_FOREVER, 0); dispatch_resume(readTimer); } @@ -5821,25 +5831,25 @@ - (void)doReadTimeout // Ideally we'd like to synchronously query the delegate about a timeout extension. // But if we do so synchronously we risk a possible deadlock. // So instead we have to do so asynchronously, and callback to ourselves from within the delegate block. - + flags |= kReadsPaused; - + __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:shouldTimeoutReadWithTag:elapsed:bytesDone:)]) { GCDAsyncReadPacket *theRead = currentRead; - + dispatch_async(delegateQueue, ^{ @autoreleasepool { - + NSTimeInterval timeoutExtension = 0.0; - + timeoutExtension = [theDelegate socket:self shouldTimeoutReadWithTag:theRead->tag elapsed:theRead->timeout bytesDone:theRead->bytesDone]; - + dispatch_async(self->socketQueue, ^{ @autoreleasepool { - + [self doReadTimeoutWithExtension:timeoutExtension]; }}); }}); @@ -5857,11 +5867,11 @@ - (void)doReadTimeoutWithExtension:(NSTimeInterval)timeoutExtension if (timeoutExtension > 0.0) { currentRead->timeout += timeoutExtension; - + // Reschedule the timer dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeoutExtension * NSEC_PER_SEC)); dispatch_source_set_timer(readTimer, tt, DISPATCH_TIME_FOREVER, 0); - + // Unpause reads, and continue flags &= ~kReadsPaused; [self doReadData]; @@ -5869,7 +5879,7 @@ - (void)doReadTimeoutWithExtension:(NSTimeInterval)timeoutExtension else { LogVerbose(@"ReadTimeout"); - + [self closeWithError:[self readTimeoutError]]; } } @@ -5882,20 +5892,20 @@ - (void)doReadTimeoutWithExtension:(NSTimeInterval)timeoutExtension - (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag { if ([data length] == 0) return; - + GCDAsyncWritePacket *packet = [[GCDAsyncWritePacket alloc] initWithData:data timeout:timeout tag:tag]; - + dispatch_async(socketQueue, ^{ @autoreleasepool { - + LogTrace(); - + if ((self->flags & kSocketStarted) && !(self->flags & kForbidReadsWrites)) { [self->writeQueue addObject:packet]; [self maybeDequeueWrite]; } }}); - + // Do not rely on the block being run in order to release the packet, // as the queue might get released without the block completing. } @@ -5903,56 +5913,56 @@ - (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)t - (float)progressOfWriteReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr { __block float result = 0.0F; - + dispatch_block_t block = ^{ - + if (!self->currentWrite || ![self->currentWrite isKindOfClass:[GCDAsyncWritePacket class]]) { // We're not writing anything right now. - + if (tagPtr != NULL) *tagPtr = 0; if (donePtr != NULL) *donePtr = 0; if (totalPtr != NULL) *totalPtr = 0; - + result = NAN; } else { NSUInteger done = self->currentWrite->bytesDone; NSUInteger total = [self->currentWrite->buffer length]; - + if (tagPtr != NULL) *tagPtr = self->currentWrite->tag; if (donePtr != NULL) *donePtr = done; if (totalPtr != NULL) *totalPtr = total; - + result = (float)done / (float)total; } }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); - + return result; } /** * Conditionally starts a new write. - * + * * It is called when: * - a user requests a write * - after a write request has finished (to handle the next request) * - immediately after the socket opens to handle any pending requests - * + * * This method also handles auto-disconnect post read/write completion. **/ - (void)maybeDequeueWrite { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - - + + // If we're not currently processing a write AND we have an available write stream if ((currentWrite == nil) && (flags & kConnected)) { @@ -5961,25 +5971,25 @@ - (void)maybeDequeueWrite // Dequeue the next object in the write queue currentWrite = [writeQueue objectAtIndex:0]; [writeQueue removeObjectAtIndex:0]; - - + + if ([currentWrite isKindOfClass:[GCDAsyncSpecialPacket class]]) { LogVerbose(@"Dequeued GCDAsyncSpecialPacket"); - + // Attempt to start TLS flags |= kStartingWriteTLS; - + // This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are set [self maybeStartTLS]; } else { LogVerbose(@"Dequeued GCDAsyncWritePacket"); - + // Setup write timer (if needed) [self setupWriteTimerWithTimeout:currentWrite->timeout]; - + // Immediately write, if possible [self doWriteData]; } @@ -6004,15 +6014,15 @@ - (void)maybeDequeueWrite - (void)doWriteData { LogTrace(); - + // This method is called by the writeSource via the socketQueue - + if ((currentWrite == nil) || (flags & kWritesPaused)) { LogVerbose(@"No currentWrite or kWritesPaused"); - + // Unable to write at this time - + if ([self usingCFStreamForTLS]) { // CFWriteStream only fires once when there is available data. @@ -6022,7 +6032,7 @@ - (void)doWriteData { // If the writeSource is firing, we need to pause it // or else it will continue to fire over and over again. - + if (flags & kSocketCanAcceptBytes) { [self suspendWriteSource]; @@ -6030,36 +6040,36 @@ - (void)doWriteData } return; } - + if (!(flags & kSocketCanAcceptBytes)) { LogVerbose(@"No space available to write..."); - + // No space available to write. - + if (![self usingCFStreamForTLS]) { // Need to wait for writeSource to fire and notify us of // available space in the socket's internal write buffer. - + [self resumeWriteSource]; } return; } - + if (flags & kStartingWriteTLS) { LogVerbose(@"Waiting for SSL/TLS handshake to complete"); - + // The writeQueue is waiting for SSL/TLS handshake to complete. - + if (flags & kStartingReadTLS) { if ([self usingSecureTransportForTLS] && lastSSLHandshakeError == errSSLWouldBlock) { // We are in the process of a SSL Handshake. // We were waiting for available space in the socket's internal OS buffer to continue writing. - + [self ssl_continueSSLHandshake]; } } @@ -6067,46 +6077,46 @@ - (void)doWriteData { // We are still waiting for the readQueue to drain and start the SSL/TLS process. // We now know we can write to the socket. - + if (![self usingCFStreamForTLS]) { // Suspend the write source or else it will continue to fire nonstop. - + [self suspendWriteSource]; } } - + return; } - + // Note: This method is not called if currentWrite is a GCDAsyncSpecialPacket (startTLS packet) - + BOOL waiting = NO; NSError *error = nil; size_t bytesWritten = 0; - + if (flags & kSocketSecure) { if ([self usingCFStreamForTLS]) { #if TARGET_OS_IPHONE - - // + + // // Writing data using CFStream (over internal TLS) - // - + // + const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + currentWrite->bytesDone; - + NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone; - + if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3) { bytesToWrite = SIZE_MAX; } - + CFIndex result = CFWriteStreamWrite(writeStream, buffer, (CFIndex)bytesToWrite); LogVerbose(@"CFWriteStreamWrite(%lu) = %li", (unsigned long)bytesToWrite, result); - + if (result < 0) { error = (__bridge_transfer NSError *)CFWriteStreamCopyError(writeStream); @@ -6114,75 +6124,75 @@ - (void)doWriteData else { bytesWritten = (size_t)result; - + // We always set waiting to true in this scenario. // CFStream may have altered our underlying socket to non-blocking. // Thus if we attempt to write without a callback, we may end up blocking our queue. waiting = YES; } - + #endif } else { // We're going to use the SSLWrite function. - // + // // OSStatus SSLWrite(SSLContextRef context, const void *data, size_t dataLength, size_t *processed) - // + // // Parameters: // context - An SSL session context reference. // data - A pointer to the buffer of data to write. // dataLength - The amount, in bytes, of data to write. // processed - On return, the length, in bytes, of the data actually written. - // + // // It sounds pretty straight-forward, // but there are a few caveats you should be aware of. - // + // // The SSLWrite method operates in a non-obvious (and rather annoying) manner. // According to the documentation: - // + // // Because you may configure the underlying connection to operate in a non-blocking manner, // a write operation might return errSSLWouldBlock, indicating that less data than requested // was actually transferred. In this case, you should repeat the call to SSLWrite until some // other result is returned. - // + // // This sounds perfect, but when our SSLWriteFunction returns errSSLWouldBlock, // then the SSLWrite method returns (with the proper errSSLWouldBlock return value), // but it sets processed to dataLength !! - // + // // In other words, if the SSLWrite function doesn't completely write all the data we tell it to, // then it doesn't tell us how many bytes were actually written. So, for example, if we tell it to // write 256 bytes then it might actually write 128 bytes, but then report 0 bytes written. - // + // // You might be wondering: // If the SSLWrite function doesn't tell us how many bytes were written, // then how in the world are we supposed to update our parameters (buffer & bytesToWrite) // for the next time we invoke SSLWrite? - // + // // The answer is that SSLWrite cached all the data we told it to write, // and it will push out that data next time we call SSLWrite. // If we call SSLWrite with new data, it will push out the cached data first, and then the new data. // If we call SSLWrite with empty data, then it will simply push out the cached data. - // + // // For this purpose we're going to break large writes into a series of smaller writes. // This allows us to report progress back to the delegate. - + OSStatus result; - + BOOL hasCachedDataToWrite = (sslWriteCachedLength > 0); BOOL hasNewDataToWrite = YES; - + if (hasCachedDataToWrite) { size_t processed = 0; - + result = SSLWrite(sslContext, NULL, 0, &processed); - + if (result == noErr) { bytesWritten = sslWriteCachedLength; sslWriteCachedLength = 0; - + if ([currentWrite->buffer length] == (currentWrite->bytesDone + bytesWritten)) { // We've written all data for the current write. @@ -6199,42 +6209,42 @@ - (void)doWriteData { error = [self sslError:result]; } - + // Can't write any new data since we were unable to write the cached data. hasNewDataToWrite = NO; } } - + if (hasNewDataToWrite) { const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + currentWrite->bytesDone + bytesWritten; - + NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone - bytesWritten; - + if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3) { bytesToWrite = SIZE_MAX; } - + size_t bytesRemaining = bytesToWrite; - + BOOL keepLooping = YES; while (keepLooping) { const size_t sslMaxBytesToWrite = 32768; size_t sslBytesToWrite = MIN(bytesRemaining, sslMaxBytesToWrite); size_t sslBytesWritten = 0; - + result = SSLWrite(sslContext, buffer, sslBytesToWrite, &sslBytesWritten); - + if (result == noErr) { buffer += sslBytesWritten; bytesWritten += sslBytesWritten; bytesRemaining -= sslBytesWritten; - + keepLooping = (bytesRemaining > 0); } else @@ -6248,35 +6258,35 @@ - (void)doWriteData { error = [self sslError:result]; } - + keepLooping = NO; } - + } // while (keepLooping) - + } // if (hasNewDataToWrite) } } else { - // + // // Writing data directly over raw socket - // - + // + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; - + const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + currentWrite->bytesDone; - + NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone; - + if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3) { bytesToWrite = SIZE_MAX; } - + ssize_t result = write(socketFD, buffer, (size_t)bytesToWrite); LogVerbose(@"wrote to socket = %zd", result); - + // Check results if (result < 0) { @@ -6294,48 +6304,48 @@ - (void)doWriteData bytesWritten = result; } } - + // We're done with our writing. // If we explictly ran into a situation where the socket told us there was no room in the buffer, // then we immediately resume listening for notifications. - // + // // We must do this before we dequeue another write, // as that may in turn invoke this method again. - // + // // Note that if CFStream is involved, it may have maliciously put our socket in blocking mode. - + if (waiting) { flags &= ~kSocketCanAcceptBytes; - + if (![self usingCFStreamForTLS]) { [self resumeWriteSource]; } } - + // Check our results - + BOOL done = NO; - + if (bytesWritten > 0) { // Update total amount read for the current write currentWrite->bytesDone += bytesWritten; LogVerbose(@"currentWrite->bytesDone = %lu", (unsigned long)currentWrite->bytesDone); - + // Is packet done? done = (currentWrite->bytesDone == [currentWrite->buffer length]); } - + if (done) { [self completeCurrentWrite]; - + if (!error) { dispatch_async(socketQueue, ^{ @autoreleasepool{ - + [self maybeDequeueWrite]; }}); } @@ -6344,66 +6354,66 @@ - (void)doWriteData { // We were unable to finish writing the data, // so we're waiting for another callback to notify us of available space in the lower-level output buffer. - + if (!waiting && !error) { // This would be the case if our write was able to accept some data, but not all of it. - + flags &= ~kSocketCanAcceptBytes; - + if (![self usingCFStreamForTLS]) { [self resumeWriteSource]; } } - + if (bytesWritten > 0) { // We're not done with the entire write, but we have written some bytes - + __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didWritePartialDataOfLength:tag:)]) { long theWriteTag = currentWrite->tag; - + dispatch_async(delegateQueue, ^{ @autoreleasepool { - + [theDelegate socket:self didWritePartialDataOfLength:bytesWritten tag:theWriteTag]; }}); } } } - + // Check for errors - + if (error) { [self closeWithError:[self errorWithErrno:errno reason:@"Error in write() function"]]; } - + // Do not add any code here without first adding a return statement in the error case above. } - (void)completeCurrentWrite { LogTrace(); - + NSAssert(currentWrite, @"Trying to complete current write when there is no current write."); - + __strong id theDelegate = delegate; - + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didWriteDataWithTag:)]) { long theWriteTag = currentWrite->tag; - + dispatch_async(delegateQueue, ^{ @autoreleasepool { - + [theDelegate socket:self didWriteDataWithTag:theWriteTag]; }}); } - + [self endCurrentWrite]; } @@ -6414,7 +6424,7 @@ - (void)endCurrentWrite dispatch_source_cancel(writeTimer); writeTimer = NULL; } - + currentWrite = nil; } @@ -6423,36 +6433,36 @@ - (void)setupWriteTimerWithTimeout:(NSTimeInterval)timeout if (timeout >= 0.0) { writeTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue); - + __weak GCDAsyncSocket *weakSelf = self; - + dispatch_source_set_event_handler(writeTimer, ^{ @autoreleasepool { #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" - + __strong GCDAsyncSocket *strongSelf = weakSelf; if (strongSelf == nil) return_from_block; - + [strongSelf doWriteTimeout]; - + #pragma clang diagnostic pop }}); - + #if !OS_OBJECT_USE_OBJC dispatch_source_t theWriteTimer = writeTimer; dispatch_source_set_cancel_handler(writeTimer, ^{ #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" - + LogVerbose(@"dispatch_release(writeTimer)"); dispatch_release(theWriteTimer); - + #pragma clang diagnostic pop }); #endif - + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)); - + dispatch_source_set_timer(writeTimer, tt, DISPATCH_TIME_FOREVER, 0); dispatch_resume(writeTimer); } @@ -6464,25 +6474,25 @@ - (void)doWriteTimeout // Ideally we'd like to synchronously query the delegate about a timeout extension. // But if we do so synchronously we risk a possible deadlock. // So instead we have to do so asynchronously, and callback to ourselves from within the delegate block. - + flags |= kWritesPaused; - + __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:shouldTimeoutWriteWithTag:elapsed:bytesDone:)]) { GCDAsyncWritePacket *theWrite = currentWrite; - + dispatch_async(delegateQueue, ^{ @autoreleasepool { - + NSTimeInterval timeoutExtension = 0.0; - + timeoutExtension = [theDelegate socket:self shouldTimeoutWriteWithTag:theWrite->tag elapsed:theWrite->timeout bytesDone:theWrite->bytesDone]; - + dispatch_async(self->socketQueue, ^{ @autoreleasepool { - + [self doWriteTimeoutWithExtension:timeoutExtension]; }}); }}); @@ -6500,11 +6510,11 @@ - (void)doWriteTimeoutWithExtension:(NSTimeInterval)timeoutExtension if (timeoutExtension > 0.0) { currentWrite->timeout += timeoutExtension; - + // Reschedule the timer dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeoutExtension * NSEC_PER_SEC)); dispatch_source_set_timer(writeTimer, tt, DISPATCH_TIME_FOREVER, 0); - + // Unpause writes, and continue flags &= ~kWritesPaused; [self doWriteData]; @@ -6512,7 +6522,7 @@ - (void)doWriteTimeoutWithExtension:(NSTimeInterval)timeoutExtension else { LogVerbose(@"WriteTimeout"); - + [self closeWithError:[self writeTimeoutError]]; } } @@ -6525,36 +6535,36 @@ - (void)doWriteTimeoutWithExtension:(NSTimeInterval)timeoutExtension - (void)startTLS:(NSDictionary *)tlsSettings { LogTrace(); - + if (tlsSettings == nil) { // Passing nil/NULL to CFReadStreamSetProperty will appear to work the same as passing an empty dictionary, // but causes problems if we later try to fetch the remote host's certificate. - // + // // To be exact, it causes the following to return NULL instead of the normal result: // CFReadStreamCopyProperty(readStream, kCFStreamPropertySSLPeerCertificates) - // + // // So we use an empty dictionary instead, which works perfectly. - + tlsSettings = [NSDictionary dictionary]; } - + GCDAsyncSpecialPacket *packet = [[GCDAsyncSpecialPacket alloc] initWithTLSSettings:tlsSettings]; - + dispatch_async(socketQueue, ^{ @autoreleasepool { - + if ((self->flags & kSocketStarted) && !(self->flags & kQueuedTLS) && !(self->flags & kForbidReadsWrites)) { [self->readQueue addObject:packet]; [self->writeQueue addObject:packet]; - + self->flags |= kQueuedTLS; - + [self maybeDequeueRead]; [self maybeDequeueWrite]; } }}); - + } - (void)maybeStartTLS @@ -6562,13 +6572,13 @@ - (void)maybeStartTLS // We can't start TLS until: // - All queued reads prior to the user calling startTLS are complete // - All queued writes prior to the user calling startTLS are complete - // + // // We'll know these conditions are met when both kStartingReadTLS and kStartingWriteTLS are set - + if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS)) { BOOL useSecureTransport = YES; - + #if TARGET_OS_IPHONE { GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead; @@ -6581,7 +6591,7 @@ - (void)maybeStartTLS useSecureTransport = NO; } #endif - + if (useSecureTransport) { [self ssl_startTLS]; @@ -6602,82 +6612,82 @@ - (void)maybeStartTLS - (OSStatus)sslReadWithBuffer:(void *)buffer length:(size_t *)bufferLength { LogVerbose(@"sslReadWithBuffer:%p length:%lu", buffer, (unsigned long)*bufferLength); - + if ((socketFDBytesAvailable == 0) && ([sslPreBuffer availableBytes] == 0)) { LogVerbose(@"%@ - No data available to read...", THIS_METHOD); - + // No data available to read. - // + // // Need to wait for readSource to fire and notify us of // available data in the socket's internal read buffer. - + [self resumeReadSource]; - + *bufferLength = 0; return errSSLWouldBlock; } - + size_t totalBytesRead = 0; size_t totalBytesLeftToBeRead = *bufferLength; - + BOOL done = NO; BOOL socketError = NO; - - // + + // // STEP 1 : READ FROM SSL PRE BUFFER - // - + // + size_t sslPreBufferLength = [sslPreBuffer availableBytes]; - + if (sslPreBufferLength > 0) { LogVerbose(@"%@: Reading from SSL pre buffer...", THIS_METHOD); - + size_t bytesToCopy; if (sslPreBufferLength > totalBytesLeftToBeRead) bytesToCopy = totalBytesLeftToBeRead; else bytesToCopy = sslPreBufferLength; - + LogVerbose(@"%@: Copying %zu bytes from sslPreBuffer", THIS_METHOD, bytesToCopy); - + memcpy(buffer, [sslPreBuffer readBuffer], bytesToCopy); [sslPreBuffer didRead:bytesToCopy]; - + LogVerbose(@"%@: sslPreBuffer.length = %zu", THIS_METHOD, [sslPreBuffer availableBytes]); - + totalBytesRead += bytesToCopy; totalBytesLeftToBeRead -= bytesToCopy; - + done = (totalBytesLeftToBeRead == 0); - + if (done) LogVerbose(@"%@: Complete", THIS_METHOD); } - - // + + // // STEP 2 : READ FROM SOCKET - // - + // + if (!done && (socketFDBytesAvailable > 0)) { LogVerbose(@"%@: Reading from socket...", THIS_METHOD); - + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; - + BOOL readIntoPreBuffer; size_t bytesToRead; uint8_t *buf; - + if (socketFDBytesAvailable > totalBytesLeftToBeRead) { // Read all available data from socket into sslPreBuffer. // Then copy requested amount into dataBuffer. - + LogVerbose(@"%@: Reading into sslPreBuffer...", THIS_METHOD); - + [sslPreBuffer ensureCapacityForWrite:socketFDBytesAvailable]; - + readIntoPreBuffer = YES; bytesToRead = (size_t)socketFDBytesAvailable; buf = [sslPreBuffer writeBuffer]; @@ -6685,58 +6695,58 @@ - (OSStatus)sslReadWithBuffer:(void *)buffer length:(size_t *)bufferLength else { // Read available data from socket directly into dataBuffer. - + LogVerbose(@"%@: Reading directly into dataBuffer...", THIS_METHOD); - + readIntoPreBuffer = NO; bytesToRead = totalBytesLeftToBeRead; buf = (uint8_t *)buffer + totalBytesRead; } - + ssize_t result = read(socketFD, buf, bytesToRead); LogVerbose(@"%@: read from socket = %zd", THIS_METHOD, result); - + if (result < 0) { LogVerbose(@"%@: read errno = %i", THIS_METHOD, errno); - + if (errno != EWOULDBLOCK) { socketError = YES; } - + socketFDBytesAvailable = 0; } else if (result == 0) { LogVerbose(@"%@: read EOF", THIS_METHOD); - + socketError = YES; socketFDBytesAvailable = 0; } else { size_t bytesReadFromSocket = result; - + if (socketFDBytesAvailable > bytesReadFromSocket) socketFDBytesAvailable -= bytesReadFromSocket; else socketFDBytesAvailable = 0; - + if (readIntoPreBuffer) { [sslPreBuffer didWrite:bytesReadFromSocket]; - + size_t bytesToCopy = MIN(totalBytesLeftToBeRead, bytesReadFromSocket); - + LogVerbose(@"%@: Copying %zu bytes out of sslPreBuffer", THIS_METHOD, bytesToCopy); - + memcpy((uint8_t *)buffer + totalBytesRead, [sslPreBuffer readBuffer], bytesToCopy); [sslPreBuffer didRead:bytesToCopy]; - + totalBytesRead += bytesToCopy; totalBytesLeftToBeRead -= bytesToCopy; - + LogVerbose(@"%@: sslPreBuffer.length = %zu", THIS_METHOD, [sslPreBuffer availableBytes]); } else @@ -6744,21 +6754,21 @@ - (OSStatus)sslReadWithBuffer:(void *)buffer length:(size_t *)bufferLength totalBytesRead += bytesReadFromSocket; totalBytesLeftToBeRead -= bytesReadFromSocket; } - + done = (totalBytesLeftToBeRead == 0); - + if (done) LogVerbose(@"%@: Complete", THIS_METHOD); } } - + *bufferLength = totalBytesRead; - + if (done) return noErr; - + if (socketError) return errSSLClosedAbort; - + return errSSLWouldBlock; } @@ -6767,33 +6777,33 @@ - (OSStatus)sslWriteWithBuffer:(const void *)buffer length:(size_t *)bufferLengt if (!(flags & kSocketCanAcceptBytes)) { // Unable to write. - // + // // Need to wait for writeSource to fire and notify us of // available space in the socket's internal write buffer. - + [self resumeWriteSource]; - + *bufferLength = 0; return errSSLWouldBlock; } - + size_t bytesToWrite = *bufferLength; size_t bytesWritten = 0; - + BOOL done = NO; BOOL socketError = NO; - + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; - + ssize_t result = write(socketFD, buffer, bytesToWrite); - + if (result < 0) { if (errno != EWOULDBLOCK) { socketError = YES; } - + flags &= ~kSocketCanAcceptBytes; } else if (result == 0) @@ -6803,69 +6813,69 @@ - (OSStatus)sslWriteWithBuffer:(const void *)buffer length:(size_t *)bufferLengt else { bytesWritten = result; - + done = (bytesWritten == bytesToWrite); } - + *bufferLength = bytesWritten; - + if (done) return noErr; - + if (socketError) return errSSLClosedAbort; - + return errSSLWouldBlock; } static OSStatus SSLReadFunction(SSLConnectionRef connection, void *data, size_t *dataLength) { GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)connection; - + NSCAssert(dispatch_get_specific(asyncSocket->IsOnSocketQueueOrTargetQueueKey), @"What the deuce?"); - + return [asyncSocket sslReadWithBuffer:data length:dataLength]; } static OSStatus SSLWriteFunction(SSLConnectionRef connection, const void *data, size_t *dataLength) { GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)connection; - + NSCAssert(dispatch_get_specific(asyncSocket->IsOnSocketQueueOrTargetQueueKey), @"What the deuce?"); - + return [asyncSocket sslWriteWithBuffer:data length:dataLength]; } - (void)ssl_startTLS { LogTrace(); - + LogVerbose(@"Starting TLS (via SecureTransport)..."); - + OSStatus status; - + GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead; if (tlsPacket == nil) // Code to quiet the analyzer { NSAssert(NO, @"Logic error"); - + [self closeWithError:[self otherError:@"Logic error"]]; return; } NSDictionary *tlsSettings = tlsPacket->tlsSettings; - + // Create SSLContext, and setup IO callbacks and connection ref - + NSNumber *isServerNumber = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLIsServer]; BOOL isServer = [isServerNumber boolValue]; - + #if TARGET_OS_IPHONE || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 1080) { if (isServer) sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLServerSide, kSSLStreamType); else sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLClientSide, kSSLStreamType); - + if (sslContext == NULL) { [self closeWithError:[self otherError:@"Error in SSLCreateContext"]]; @@ -6882,14 +6892,14 @@ - (void)ssl_startTLS } } #endif - + status = SSLSetIOFuncs(sslContext, &SSLReadFunction, &SSLWriteFunction); if (status != noErr) { [self closeWithError:[self otherError:@"Error in SSLSetIOFuncs"]]; return; } - + status = SSLSetConnection(sslContext, (__bridge SSLConnectionRef)self); if (status != noErr) { @@ -6906,35 +6916,35 @@ - (void)ssl_startTLS [self closeWithError:[self otherError:@"Manual trust validation is not supported for server sockets"]]; return; } - + status = SSLSetSessionOption(sslContext, kSSLSessionOptionBreakOnServerAuth, true); if (status != noErr) { [self closeWithError:[self otherError:@"Error in SSLSetSessionOption"]]; return; } - + #if !TARGET_OS_IPHONE && (__MAC_OS_X_VERSION_MIN_REQUIRED < 1080) - + // Note from Apple's documentation: // // It is only necessary to call SSLSetEnableCertVerify on the Mac prior to OS X 10.8. // On OS X 10.8 and later setting kSSLSessionOptionBreakOnServerAuth always disables the // built-in trust evaluation. All versions of iOS behave like OS X 10.8 and thus // SSLSetEnableCertVerify is not available on that platform at all. - + status = SSLSetEnableCertVerify(sslContext, NO); if (status != noErr) { [self closeWithError:[self otherError:@"Error in SSLSetEnableCertVerify"]]; return; } - + #endif } // Configure SSLContext from given settings - // + // // Checklist: // 1. kCFStreamSSLPeerName // 2. kCFStreamSSLCertificates @@ -6953,19 +6963,19 @@ - (void)ssl_startTLS // 12. kCFStreamSSLAllowsExpiredCertificates // 13. kCFStreamSSLValidatesCertificateChain // 14. kCFStreamSSLLevel - + NSObject *value; - + // 1. kCFStreamSSLPeerName - + value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLPeerName]; if ([value isKindOfClass:[NSString class]]) { NSString *peerName = (NSString *)value; - + const char *peer = [peerName UTF8String]; size_t peerLen = strlen(peer); - + status = SSLSetPeerDomainName(sslContext, peer, peerLen); if (status != noErr) { @@ -6976,18 +6986,18 @@ - (void)ssl_startTLS else if (value) { NSAssert(NO, @"Invalid value for kCFStreamSSLPeerName. Value must be of type NSString."); - + [self closeWithError:[self otherError:@"Invalid value for kCFStreamSSLPeerName."]]; return; } - + // 2. kCFStreamSSLCertificates - + value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLCertificates]; if ([value isKindOfClass:[NSArray class]]) { NSArray *certs = (NSArray *)value; - + status = SSLSetCertificate(sslContext, (__bridge CFArrayRef)certs); if (status != noErr) { @@ -6998,18 +7008,18 @@ - (void)ssl_startTLS else if (value) { NSAssert(NO, @"Invalid value for kCFStreamSSLCertificates. Value must be of type NSArray."); - + [self closeWithError:[self otherError:@"Invalid value for kCFStreamSSLCertificates."]]; return; } - + // 3. GCDAsyncSocketSSLPeerID - + value = [tlsSettings objectForKey:GCDAsyncSocketSSLPeerID]; if ([value isKindOfClass:[NSData class]]) { NSData *peerIdData = (NSData *)value; - + status = SSLSetPeerID(sslContext, [peerIdData bytes], [peerIdData length]); if (status != noErr) { @@ -7022,13 +7032,13 @@ - (void)ssl_startTLS NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLPeerID. Value must be of type NSData." @" (You can convert strings to data using a method like" @" [string dataUsingEncoding:NSUTF8StringEncoding])"); - + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLPeerID."]]; return; } - + // 4. GCDAsyncSocketSSLProtocolVersionMin - + value = [tlsSettings objectForKey:GCDAsyncSocketSSLProtocolVersionMin]; if ([value isKindOfClass:[NSNumber class]]) { @@ -7046,13 +7056,13 @@ - (void)ssl_startTLS else if (value) { NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLProtocolVersionMin. Value must be of type NSNumber."); - + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLProtocolVersionMin."]]; return; } - + // 5. GCDAsyncSocketSSLProtocolVersionMax - + value = [tlsSettings objectForKey:GCDAsyncSocketSSLProtocolVersionMax]; if ([value isKindOfClass:[NSNumber class]]) { @@ -7070,13 +7080,13 @@ - (void)ssl_startTLS else if (value) { NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLProtocolVersionMax. Value must be of type NSNumber."); - + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLProtocolVersionMax."]]; return; } - + // 6. GCDAsyncSocketSSLSessionOptionFalseStart - + value = [tlsSettings objectForKey:GCDAsyncSocketSSLSessionOptionFalseStart]; if ([value isKindOfClass:[NSNumber class]]) { @@ -7091,13 +7101,13 @@ - (void)ssl_startTLS else if (value) { NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLSessionOptionFalseStart. Value must be of type NSNumber."); - + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLSessionOptionFalseStart."]]; return; } - + // 7. GCDAsyncSocketSSLSessionOptionSendOneByteRecord - + value = [tlsSettings objectForKey:GCDAsyncSocketSSLSessionOptionSendOneByteRecord]; if ([value isKindOfClass:[NSNumber class]]) { @@ -7114,13 +7124,13 @@ - (void)ssl_startTLS { NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLSessionOptionSendOneByteRecord." @" Value must be of type NSNumber."); - + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLSessionOptionSendOneByteRecord."]]; return; } - + // 8. GCDAsyncSocketSSLCipherSuites - + value = [tlsSettings objectForKey:GCDAsyncSocketSSLCipherSuites]; if ([value isKindOfClass:[NSArray class]]) { @@ -7130,14 +7140,14 @@ - (void)ssl_startTLS #pragma clang diagnostic ignored "-Wvla" SSLCipherSuite ciphers[numberCiphers]; #pragma clang diagnostic pop - + NSUInteger cipherIndex; for (cipherIndex = 0; cipherIndex < numberCiphers; cipherIndex++) { NSNumber *cipherObject = [cipherSuites objectAtIndex:cipherIndex]; ciphers[cipherIndex] = (SSLCipherSuite)[cipherObject unsignedIntValue]; } - + status = SSLSetEnabledCiphers(sslContext, ciphers, numberCiphers); if (status != noErr) { @@ -7148,19 +7158,19 @@ - (void)ssl_startTLS else if (value) { NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLCipherSuites. Value must be of type NSArray."); - + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLCipherSuites."]]; return; } - + // 9. GCDAsyncSocketSSLDiffieHellmanParameters - + #if !TARGET_OS_IPHONE value = [tlsSettings objectForKey:GCDAsyncSocketSSLDiffieHellmanParameters]; if ([value isKindOfClass:[NSData class]]) { NSData *diffieHellmanData = (NSData *)value; - + status = SSLSetDiffieHellmanParams(sslContext, [diffieHellmanData bytes], [diffieHellmanData length]); if (status != noErr) { @@ -7171,7 +7181,7 @@ - (void)ssl_startTLS else if (value) { NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLDiffieHellmanParameters. Value must be of type NSData."); - + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLDiffieHellmanParameters."]]; return; } @@ -7201,15 +7211,15 @@ - (void)ssl_startTLS else if (value) { NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLALPN. Value must be of type NSArray."); - + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLALPN."]]; return; } - + // DEPRECATED checks - + // 10. kCFStreamSSLAllowsAnyRoot - + #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLAllowsAnyRoot]; @@ -7218,13 +7228,13 @@ - (void)ssl_startTLS { NSAssert(NO, @"Security option unavailable - kCFStreamSSLAllowsAnyRoot" @" - You must use manual trust evaluation"); - + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLAllowsAnyRoot"]]; return; } - + // 11. kCFStreamSSLAllowsExpiredRoots - + #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLAllowsExpiredRoots]; @@ -7233,13 +7243,13 @@ - (void)ssl_startTLS { NSAssert(NO, @"Security option unavailable - kCFStreamSSLAllowsExpiredRoots" @" - You must use manual trust evaluation"); - + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLAllowsExpiredRoots"]]; return; } - + // 12. kCFStreamSSLValidatesCertificateChain - + #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLValidatesCertificateChain]; @@ -7248,13 +7258,13 @@ - (void)ssl_startTLS { NSAssert(NO, @"Security option unavailable - kCFStreamSSLValidatesCertificateChain" @" - You must use manual trust evaluation"); - + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLValidatesCertificateChain"]]; return; } - + // 13. kCFStreamSSLAllowsExpiredCertificates - + #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLAllowsExpiredCertificates]; @@ -7263,13 +7273,13 @@ - (void)ssl_startTLS { NSAssert(NO, @"Security option unavailable - kCFStreamSSLAllowsExpiredCertificates" @" - You must use manual trust evaluation"); - + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLAllowsExpiredCertificates"]]; return; } - + // 14. kCFStreamSSLLevel - + #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLLevel]; @@ -7278,79 +7288,79 @@ - (void)ssl_startTLS { NSAssert(NO, @"Security option unavailable - kCFStreamSSLLevel" @" - You must use GCDAsyncSocketSSLProtocolVersionMin & GCDAsyncSocketSSLProtocolVersionMax"); - + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLLevel"]]; return; } - + // Setup the sslPreBuffer - // + // // Any data in the preBuffer needs to be moved into the sslPreBuffer, // as this data is now part of the secure read stream. - + sslPreBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)]; - + size_t preBufferLength = [preBuffer availableBytes]; - + if (preBufferLength > 0) { [sslPreBuffer ensureCapacityForWrite:preBufferLength]; - + memcpy([sslPreBuffer writeBuffer], [preBuffer readBuffer], preBufferLength); [preBuffer didRead:preBufferLength]; [sslPreBuffer didWrite:preBufferLength]; } - + sslErrCode = lastSSLHandshakeError = noErr; - + // Start the SSL Handshake process - + [self ssl_continueSSLHandshake]; } - (void)ssl_continueSSLHandshake { LogTrace(); - + // If the return value is noErr, the session is ready for normal secure communication. // If the return value is errSSLWouldBlock, the SSLHandshake function must be called again. // If the return value is errSSLServerAuthCompleted, we ask delegate if we should trust the // server and then call SSLHandshake again to resume the handshake or close the connection // errSSLPeerBadCert SSL error. // Otherwise, the return value indicates an error code. - + OSStatus status = SSLHandshake(sslContext); lastSSLHandshakeError = status; - + if (status == noErr) { LogVerbose(@"SSLHandshake complete"); - + flags &= ~kStartingReadTLS; flags &= ~kStartingWriteTLS; - + flags |= kSocketSecure; - + __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidSecure:)]) { dispatch_async(delegateQueue, ^{ @autoreleasepool { - + [theDelegate socketDidSecure:self]; }}); } - + [self endCurrentRead]; [self endCurrentWrite]; - + [self maybeDequeueRead]; [self maybeDequeueWrite]; } else if (status == errSSLPeerAuthCompleted) { LogVerbose(@"SSLHandshake peerAuthCompleted - awaiting delegate approval"); - + __block SecTrustRef trust = NULL; status = SSLCopyPeerTrust(sslContext, &trust); if (status != noErr) @@ -7358,39 +7368,39 @@ - (void)ssl_continueSSLHandshake [self closeWithError:[self sslError:status]]; return; } - + int aStateIndex = stateIndex; dispatch_queue_t theSocketQueue = socketQueue; - + __weak GCDAsyncSocket *weakSelf = self; - + void (^comletionHandler)(BOOL) = ^(BOOL shouldTrust){ @autoreleasepool { #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" - + dispatch_async(theSocketQueue, ^{ @autoreleasepool { - + if (trust) { CFRelease(trust); trust = NULL; } - + __strong GCDAsyncSocket *strongSelf = weakSelf; if (strongSelf) { [strongSelf ssl_shouldTrustPeer:shouldTrust stateIndex:aStateIndex]; } }}); - + #pragma clang diagnostic pop }}; - + __strong id theDelegate = delegate; - + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReceiveTrust:completionHandler:)]) { dispatch_async(delegateQueue, ^{ @autoreleasepool { - + [theDelegate socket:self didReceiveTrust:trust completionHandler:comletionHandler]; }}); } @@ -7400,10 +7410,10 @@ - (void)ssl_continueSSLHandshake CFRelease(trust); trust = NULL; } - + NSString *msg = @"GCDAsyncSocketManuallyEvaluateTrust specified in tlsSettings," @" but delegate doesn't implement socket:shouldTrustPeer:"; - + [self closeWithError:[self otherError:msg]]; return; } @@ -7411,9 +7421,9 @@ - (void)ssl_continueSSLHandshake else if (status == errSSLWouldBlock) { LogVerbose(@"SSLHandshake continues..."); - + // Handshake continues... - // + // // This method will be called again from doReadData or doWriteData. } else @@ -7425,22 +7435,22 @@ - (void)ssl_continueSSLHandshake - (void)ssl_shouldTrustPeer:(BOOL)shouldTrust stateIndex:(int)aStateIndex { LogTrace(); - + if (aStateIndex != stateIndex) { LogInfo(@"Ignoring ssl_shouldTrustPeer - invalid state (maybe disconnected)"); - + // One of the following is true // - the socket was disconnected // - the startTLS operation timed out // - the completionHandler was already invoked once - + return; } - + // Increment stateIndex to ensure completionHandler can only be called once. stateIndex++; - + if (shouldTrust) { NSAssert(lastSSLHandshakeError == errSSLPeerAuthCompleted, @"ssl_shouldTrustPeer called when last error is %d and not errSSLPeerAuthCompleted", (int)lastSSLHandshakeError); @@ -7461,27 +7471,27 @@ - (void)ssl_shouldTrustPeer:(BOOL)shouldTrust stateIndex:(int)aStateIndex - (void)cf_finishSSLHandshake { LogTrace(); - + if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS)) { flags &= ~kStartingReadTLS; flags &= ~kStartingWriteTLS; - + flags |= kSocketSecure; - + __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidSecure:)]) { dispatch_async(delegateQueue, ^{ @autoreleasepool { - + [theDelegate socketDidSecure:self]; }}); } - + [self endCurrentRead]; [self endCurrentWrite]; - + [self maybeDequeueRead]; [self maybeDequeueWrite]; } @@ -7490,12 +7500,12 @@ - (void)cf_finishSSLHandshake - (void)cf_abortSSLHandshake:(NSError *)error { LogTrace(); - + if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS)) { flags &= ~kStartingReadTLS; flags &= ~kStartingWriteTLS; - + [self closeWithError:error]; } } @@ -7503,87 +7513,87 @@ - (void)cf_abortSSLHandshake:(NSError *)error - (void)cf_startTLS { LogTrace(); - + LogVerbose(@"Starting TLS (via CFStream)..."); - + if ([preBuffer availableBytes] > 0) { NSString *msg = @"Invalid TLS transition. Handshake has already been read from socket."; - + [self closeWithError:[self otherError:msg]]; return; } - + [self suspendReadSource]; [self suspendWriteSource]; - + socketFDBytesAvailable = 0; flags &= ~kSocketCanAcceptBytes; flags &= ~kSecureSocketHasBytesAvailable; - + flags |= kUsingCFStreamForTLS; - + if (![self createReadAndWriteStream]) { [self closeWithError:[self otherError:@"Error in CFStreamCreatePairWithSocket"]]; return; } - + if (![self registerForStreamCallbacksIncludingReadWrite:YES]) { [self closeWithError:[self otherError:@"Error in CFStreamSetClient"]]; return; } - + if (![self addStreamsToRunLoop]) { [self closeWithError:[self otherError:@"Error in CFStreamScheduleWithRunLoop"]]; return; } - + NSAssert([currentRead isKindOfClass:[GCDAsyncSpecialPacket class]], @"Invalid read packet for startTLS"); NSAssert([currentWrite isKindOfClass:[GCDAsyncSpecialPacket class]], @"Invalid write packet for startTLS"); - + GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead; CFDictionaryRef tlsSettings = (__bridge CFDictionaryRef)tlsPacket->tlsSettings; - + // Getting an error concerning kCFStreamPropertySSLSettings ? - // You need to add the CFNetwork framework to your iOS application. - + // CFNetwork/CFStream.h is imported for SSL constants needed by CFStream TLS support. + BOOL r1 = CFReadStreamSetProperty(readStream, kCFStreamPropertySSLSettings, tlsSettings); BOOL r2 = CFWriteStreamSetProperty(writeStream, kCFStreamPropertySSLSettings, tlsSettings); - + // For some reason, starting around the time of iOS 4.3, // the first call to set the kCFStreamPropertySSLSettings will return true, // but the second will return false. - // + // // Order doesn't seem to matter. // So you could call CFReadStreamSetProperty and then CFWriteStreamSetProperty, or you could reverse the order. // Either way, the first call will return true, and the second returns false. - // + // // Interestingly, this doesn't seem to affect anything. // Which is not altogether unusual, as the documentation seems to suggest that (for many settings) // setting it on one side of the stream automatically sets it for the other side of the stream. - // + // // Although there isn't anything in the documentation to suggest that the second attempt would fail. - // + // // Furthermore, this only seems to affect streams that are negotiating a security upgrade. // In other words, the socket gets connected, there is some back-and-forth communication over the unsecure // connection, and then a startTLS is issued. // So this mostly affects newer protocols (XMPP, IMAP) as opposed to older protocols (HTTPS). - + if (!r1 && !r2) // Yes, the && is correct - workaround for apple bug. { [self closeWithError:[self otherError:@"Error in CFStreamSetProperty"]]; return; } - + if (![self openStreams]) { [self closeWithError:[self otherError:@"Error in CFStreamOpen"]]; return; } - + LogVerbose(@"Waiting for SSL Handshake to complete..."); } @@ -7601,16 +7611,16 @@ + (void)ignore:(id)_ + (void)startCFStreamThreadIfNeeded { LogTrace(); - + static dispatch_once_t predicate; dispatch_once(&predicate, ^{ - + cfstreamThreadRetainCount = 0; cfstreamThreadSetupQueue = dispatch_queue_create("GCDAsyncSocket-CFStreamThreadSetup", DISPATCH_QUEUE_SERIAL); }); - + dispatch_sync(cfstreamThreadSetupQueue, ^{ @autoreleasepool { - + if (++cfstreamThreadRetainCount == 1) { cfstreamThread = [[NSThread alloc] initWithTarget:self @@ -7624,38 +7634,38 @@ + (void)startCFStreamThreadIfNeeded + (void)stopCFStreamThreadIfNeeded { LogTrace(); - + // The creation of the cfstreamThread is relatively expensive. // So we'd like to keep it available for recycling. // However, there's a tradeoff here, because it shouldn't remain alive forever. // So what we're going to do is use a little delay before taking it down. // This way it can be reused properly in situations where multiple sockets are continually in flux. - + int delayInSeconds = 30; dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); dispatch_after(when, cfstreamThreadSetupQueue, ^{ @autoreleasepool { #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" - + if (cfstreamThreadRetainCount == 0) { LogWarn(@"Logic error concerning cfstreamThread start / stop"); return_from_block; } - + if (--cfstreamThreadRetainCount == 0) { [cfstreamThread cancel]; // set isCancelled flag - + // wake up the thread [[self class] performSelector:@selector(ignore:) onThread:cfstreamThread withObject:[NSNull null] waitUntilDone:NO]; - + cfstreamThread = nil; } - + #pragma clang diagnostic pop }}); } @@ -7663,9 +7673,9 @@ + (void)stopCFStreamThreadIfNeeded + (void)cfstreamThread:(id)unused { @autoreleasepool { [[NSThread currentThread] setName:GCDAsyncSocketThreadName]; - + LogInfo(@"CFStreamThread: Started"); - + // We can't run the run loop unless it has an associated input source or a timer. // So we'll just create a timer that will never fire - unless the server runs for decades. [NSTimer scheduledTimerWithTimeInterval:[[NSDate distantFuture] timeIntervalSinceNow] @@ -7673,17 +7683,17 @@ + (void)cfstreamThread:(id)unused { @autoreleasepool selector:@selector(ignore:) userInfo:nil repeats:YES]; - + NSThread *currentThread = [NSThread currentThread]; NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop]; - + BOOL isCancelled = [currentThread isCancelled]; - + while (!isCancelled && [currentRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]) { isCancelled = [currentThread isCancelled]; } - + LogInfo(@"CFStreamThread: Stopped"); }} @@ -7691,12 +7701,12 @@ + (void)scheduleCFStreams:(GCDAsyncSocket *)asyncSocket { LogTrace(); NSAssert([NSThread currentThread] == cfstreamThread, @"Invoked on wrong thread"); - + CFRunLoopRef runLoop = CFRunLoopGetCurrent(); - + if (asyncSocket->readStream) CFReadStreamScheduleWithRunLoop(asyncSocket->readStream, runLoop, kCFRunLoopDefaultMode); - + if (asyncSocket->writeStream) CFWriteStreamScheduleWithRunLoop(asyncSocket->writeStream, runLoop, kCFRunLoopDefaultMode); } @@ -7705,12 +7715,12 @@ + (void)unscheduleCFStreams:(GCDAsyncSocket *)asyncSocket { LogTrace(); NSAssert([NSThread currentThread] == cfstreamThread, @"Invoked on wrong thread"); - + CFRunLoopRef runLoop = CFRunLoopGetCurrent(); - + if (asyncSocket->readStream) CFReadStreamUnscheduleFromRunLoop(asyncSocket->readStream, runLoop, kCFRunLoopDefaultMode); - + if (asyncSocket->writeStream) CFWriteStreamUnscheduleFromRunLoop(asyncSocket->writeStream, runLoop, kCFRunLoopDefaultMode); } @@ -7726,17 +7736,17 @@ static void CFReadStreamCallback (CFReadStreamRef stream, CFStreamEventType type case kCFStreamEventHasBytesAvailable: { dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { - + LogCVerbose(@"CFReadStreamCallback - HasBytesAvailable"); - + if (asyncSocket->readStream != stream) return_from_block; - + if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) { // If we set kCFStreamPropertySSLSettings before we opened the streams, this might be a lie. // (A callback related to the tcp stream, but not to the SSL layer). - + if (CFReadStreamHasBytesAvailable(asyncSocket->readStream)) { asyncSocket->flags |= kSecureSocketHasBytesAvailable; @@ -7749,25 +7759,25 @@ static void CFReadStreamCallback (CFReadStreamRef stream, CFStreamEventType type [asyncSocket doReadData]; } }}); - + break; } default: { NSError *error = (__bridge_transfer NSError *)CFReadStreamCopyError(stream); - + if (error == nil && type == kCFStreamEventEndEncountered) { error = [asyncSocket connectionClosedError]; } - + dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { - + LogCVerbose(@"CFReadStreamCallback - Other"); - + if (asyncSocket->readStream != stream) return_from_block; - + if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) { [asyncSocket cf_abortSSLHandshake:error]; @@ -7777,7 +7787,7 @@ static void CFReadStreamCallback (CFReadStreamRef stream, CFStreamEventType type [asyncSocket closeWithError:error]; } }}); - + break; } } @@ -7795,17 +7805,17 @@ static void CFWriteStreamCallback (CFWriteStreamRef stream, CFStreamEventType ty case kCFStreamEventCanAcceptBytes: { dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { - + LogCVerbose(@"CFWriteStreamCallback - CanAcceptBytes"); - + if (asyncSocket->writeStream != stream) return_from_block; - + if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) { // If we set kCFStreamPropertySSLSettings before we opened the streams, this might be a lie. // (A callback related to the tcp stream, but not to the SSL layer). - + if (CFWriteStreamCanAcceptBytes(asyncSocket->writeStream)) { asyncSocket->flags |= kSocketCanAcceptBytes; @@ -7818,25 +7828,25 @@ static void CFWriteStreamCallback (CFWriteStreamRef stream, CFStreamEventType ty [asyncSocket doWriteData]; } }}); - + break; } default: { NSError *error = (__bridge_transfer NSError *)CFWriteStreamCopyError(stream); - + if (error == nil && type == kCFStreamEventEndEncountered) { error = [asyncSocket connectionClosedError]; } - + dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { - + LogCVerbose(@"CFWriteStreamCallback - Other"); - + if (asyncSocket->writeStream != stream) return_from_block; - + if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) { [asyncSocket cf_abortSSLHandshake:error]; @@ -7846,7 +7856,7 @@ static void CFWriteStreamCallback (CFWriteStreamRef stream, CFStreamEventType ty [asyncSocket closeWithError:error]; } }}); - + break; } } @@ -7856,46 +7866,46 @@ static void CFWriteStreamCallback (CFWriteStreamRef stream, CFStreamEventType ty - (BOOL)createReadAndWriteStream { LogTrace(); - + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - - + + if (readStream || writeStream) { // Streams already created return YES; } - + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; - + if (socketFD == SOCKET_NULL) { // Cannot create streams without a file descriptor return NO; } - + if (![self isConnected]) { // Cannot create streams until file descriptor is connected return NO; } - + LogVerbose(@"Creating read and write stream..."); - + CFStreamCreatePairWithSocket(NULL, (CFSocketNativeHandle)socketFD, &readStream, &writeStream); - + // The kCFStreamPropertyShouldCloseNativeSocket property should be false by default (for our case). // But let's not take any chances. - + if (readStream) CFReadStreamSetProperty(readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); if (writeStream) CFWriteStreamSetProperty(writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); - + if ((readStream == NULL) || (writeStream == NULL)) { LogWarn(@"Unable to create read and write stream..."); - + if (readStream) { CFReadStreamClose(readStream); @@ -7908,58 +7918,58 @@ - (BOOL)createReadAndWriteStream CFRelease(writeStream); writeStream = NULL; } - + return NO; } - + return YES; } - (BOOL)registerForStreamCallbacksIncludingReadWrite:(BOOL)includeReadWrite { LogVerbose(@"%@ %@", THIS_METHOD, (includeReadWrite ? @"YES" : @"NO")); - + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); - + streamContext.version = 0; streamContext.info = (__bridge void *)(self); streamContext.retain = nil; streamContext.release = nil; streamContext.copyDescription = nil; - + CFOptionFlags readStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; if (includeReadWrite) readStreamEvents |= kCFStreamEventHasBytesAvailable; - + if (!CFReadStreamSetClient(readStream, readStreamEvents, &CFReadStreamCallback, &streamContext)) { return NO; } - + CFOptionFlags writeStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; if (includeReadWrite) writeStreamEvents |= kCFStreamEventCanAcceptBytes; - + if (!CFWriteStreamSetClient(writeStream, writeStreamEvents, &CFWriteStreamCallback, &streamContext)) { return NO; } - + return YES; } - (BOOL)addStreamsToRunLoop { LogTrace(); - + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); - + if (!(flags & kAddedStreamsToRunLoop)) { LogVerbose(@"Adding streams to runloop..."); - + [[self class] startCFStreamThreadIfNeeded]; dispatch_sync(cfstreamThreadSetupQueue, ^{ [[self class] performSelector:@selector(scheduleCFStreams:) @@ -7969,21 +7979,21 @@ - (BOOL)addStreamsToRunLoop }); flags |= kAddedStreamsToRunLoop; } - + return YES; } - (void)removeStreamsFromRunLoop { LogTrace(); - + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); - + if (flags & kAddedStreamsToRunLoop) { LogVerbose(@"Removing streams from runloop..."); - + dispatch_sync(cfstreamThreadSetupQueue, ^{ [[self class] performSelector:@selector(unscheduleCFStreams:) onThread:cfstreamThread @@ -7991,7 +8001,7 @@ - (void)removeStreamsFromRunLoop waitUntilDone:YES]; }); [[self class] stopCFStreamThreadIfNeeded]; - + flags &= ~kAddedStreamsToRunLoop; } } @@ -7999,27 +8009,27 @@ - (void)removeStreamsFromRunLoop - (BOOL)openStreams { LogTrace(); - + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); - + CFStreamStatus readStatus = CFReadStreamGetStatus(readStream); CFStreamStatus writeStatus = CFWriteStreamGetStatus(writeStream); - + if ((readStatus == kCFStreamStatusNotOpen) || (writeStatus == kCFStreamStatusNotOpen)) { LogVerbose(@"Opening read and write stream..."); - + BOOL r1 = CFReadStreamOpen(readStream); BOOL r2 = CFWriteStreamOpen(writeStream); - + if (!r1 || !r2) { LogError(@"Error in CFStreamOpen"); return NO; } } - + return YES; } @@ -8035,7 +8045,7 @@ - (BOOL)openStreams - (BOOL)autoDisconnectOnClosedReadStream { // Note: YES means kAllowHalfDuplexConnection is OFF - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { return ((config & kAllowHalfDuplexConnection) == 0); @@ -8043,11 +8053,11 @@ - (BOOL)autoDisconnectOnClosedReadStream else { __block BOOL result; - + dispatch_sync(socketQueue, ^{ result = ((self->config & kAllowHalfDuplexConnection) == 0); }); - + return result; } } @@ -8058,15 +8068,15 @@ - (BOOL)autoDisconnectOnClosedReadStream - (void)setAutoDisconnectOnClosedReadStream:(BOOL)flag { // Note: YES means kAllowHalfDuplexConnection is OFF - + dispatch_block_t block = ^{ - + if (flag) self->config &= ~kAllowHalfDuplexConnection; else self->config |= kAllowHalfDuplexConnection; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else @@ -8112,7 +8122,7 @@ - (int)socketFD LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); return SOCKET_NULL; } - + if (socket4FD != SOCKET_NULL) return socket4FD; else @@ -8129,7 +8139,7 @@ - (int)socket4FD LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); return SOCKET_NULL; } - + return socket4FD; } @@ -8143,7 +8153,7 @@ - (int)socket6FD LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); return SOCKET_NULL; } - + return socket6FD; } @@ -8159,10 +8169,10 @@ - (CFReadStreamRef)readStream LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); return NULL; } - + if (readStream == NULL) [self createReadAndWriteStream]; - + return readStream; } @@ -8176,10 +8186,10 @@ - (CFWriteStreamRef)writeStream LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); return NULL; } - + if (writeStream == NULL) [self createReadAndWriteStream]; - + return writeStream; } @@ -8190,11 +8200,11 @@ - (BOOL)enableBackgroundingOnSocketWithCaveat:(BOOL)caveat // Error occurred creating streams (perhaps socket isn't open) return NO; } - + BOOL r1, r2; - + LogVerbose(@"Enabling backgrouding on socket"); - + #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" r1 = CFReadStreamSetProperty(readStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); @@ -8205,7 +8215,7 @@ - (BOOL)enableBackgroundingOnSocketWithCaveat:(BOOL)caveat { return NO; } - + if (!caveat) { if (![self openStreams]) @@ -8213,7 +8223,7 @@ - (BOOL)enableBackgroundingOnSocketWithCaveat:(BOOL)caveat return NO; } } - + return YES; } @@ -8223,13 +8233,13 @@ - (BOOL)enableBackgroundingOnSocketWithCaveat:(BOOL)caveat - (BOOL)enableBackgroundingOnSocket { LogTrace(); - + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); return NO; } - + return [self enableBackgroundingOnSocketWithCaveat:NO]; } @@ -8238,15 +8248,15 @@ - (BOOL)enableBackgroundingOnSocketWithCaveat // Deprecated in iOS 4.??? // This method was created as a workaround for a bug in iOS. // Apple has since fixed this bug. // I'm not entirely sure which version of iOS they fixed it in... - + LogTrace(); - + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); return NO; } - + return [self enableBackgroundingOnSocketWithCaveat:YES]; } @@ -8259,7 +8269,7 @@ - (SSLContextRef)sslContext LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); return NULL; } - + return sslContext; } @@ -8270,10 +8280,10 @@ - (SSLContextRef)sslContext + (NSMutableArray *)lookupHost:(NSString *)host port:(uint16_t)port error:(NSError **)errPtr { LogTrace(); - + NSMutableArray *addresses = nil; NSError *error = nil; - + if ([host isEqualToString:@"localhost"] || [host isEqualToString:@"loopback"]) { // Use LOOPBACK address @@ -8283,7 +8293,7 @@ + (NSMutableArray *)lookupHost:(NSString *)host port:(uint16_t)port error:(NSErr nativeAddr4.sin_port = htons(port); nativeAddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); memset(&(nativeAddr4.sin_zero), 0, sizeof(nativeAddr4.sin_zero)); - + struct sockaddr_in6 nativeAddr6; nativeAddr6.sin6_len = sizeof(struct sockaddr_in6); nativeAddr6.sin6_family = AF_INET6; @@ -8291,12 +8301,12 @@ + (NSMutableArray *)lookupHost:(NSString *)host port:(uint16_t)port error:(NSErr nativeAddr6.sin6_flowinfo = 0; nativeAddr6.sin6_addr = in6addr_loopback; nativeAddr6.sin6_scope_id = 0; - + // Wrap the native address structures - + NSData *address4 = [NSData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; NSData *address6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; - + addresses = [NSMutableArray arrayWithCapacity:2]; [addresses addObject:address4]; [addresses addObject:address6]; @@ -8304,16 +8314,16 @@ + (NSMutableArray *)lookupHost:(NSString *)host port:(uint16_t)port error:(NSErr else { NSString *portStr = [NSString stringWithFormat:@"%hu", port]; - + struct addrinfo hints, *res, *res0; - + memset(&hints, 0, sizeof(hints)); hints.ai_family = PF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = IPPROTO_TCP; - + int gai_error = getaddrinfo([host UTF8String], [portStr UTF8String], &hints, &res0); - + if (gai_error) { error = [self gaiError:gai_error]; @@ -8327,16 +8337,16 @@ + (NSMutableArray *)lookupHost:(NSString *)host port:(uint16_t)port error:(NSErr capacity++; } } - + addresses = [NSMutableArray arrayWithCapacity:capacity]; - + for (res = res0; res; res = res->ai_next) { if (res->ai_family == AF_INET) { // Found IPv4 address. // Wrap the native address structure, and add to results. - + NSData *address4 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; [addresses addObject:address4]; } @@ -8344,10 +8354,10 @@ + (NSMutableArray *)lookupHost:(NSString *)host port:(uint16_t)port error:(NSErr { // Fixes connection issues with IPv6 // https://github.com/robbiehanson/CocoaAsyncSocket/issues/429#issuecomment-222477158 - + // Found IPv6 address. // Wrap the native address structure, and add to results. - + struct sockaddr_in6 *sockaddr = (struct sockaddr_in6 *)(void *)res->ai_addr; in_port_t *portPtr = &sockaddr->sin6_port; if ((portPtr != NULL) && (*portPtr == 0)) { @@ -8359,14 +8369,14 @@ + (NSMutableArray *)lookupHost:(NSString *)host port:(uint16_t)port error:(NSErr } } freeaddrinfo(res0); - + if ([addresses count] == 0) { error = [self gaiError:EAI_FAIL]; } } } - + if (errPtr) *errPtr = error; return addresses; } @@ -8374,24 +8384,24 @@ + (NSMutableArray *)lookupHost:(NSString *)host port:(uint16_t)port error:(NSErr + (NSString *)hostFromSockaddr4:(const struct sockaddr_in *)pSockaddr4 { char addrBuf[INET_ADDRSTRLEN]; - + if (inet_ntop(AF_INET, &pSockaddr4->sin_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL) { addrBuf[0] = '\0'; } - + return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; } + (NSString *)hostFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6 { char addrBuf[INET6_ADDRSTRLEN]; - + if (inet_ntop(AF_INET6, &pSockaddr6->sin6_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL) { addrBuf[0] = '\0'; } - + return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; } @@ -8414,7 +8424,7 @@ + (NSURL *)urlFromSockaddrUN:(const struct sockaddr_un *)pSockaddr + (NSString *)hostFromAddress:(NSData *)address { NSString *host; - + if ([self getHost:&host port:NULL fromAddress:address]) return host; else @@ -8424,7 +8434,7 @@ + (NSString *)hostFromAddress:(NSData *)address + (uint16_t)portFromAddress:(NSData *)address { uint16_t port; - + if ([self getHost:NULL port:&port fromAddress:address]) return port; else @@ -8436,12 +8446,12 @@ + (BOOL)isIPv4Address:(NSData *)address if ([address length] >= sizeof(struct sockaddr)) { const struct sockaddr *sockaddrX = [address bytes]; - + if (sockaddrX->sa_family == AF_INET) { return YES; } } - + return NO; } @@ -8450,12 +8460,12 @@ + (BOOL)isIPv6Address:(NSData *)address if ([address length] >= sizeof(struct sockaddr)) { const struct sockaddr *sockaddrX = [address bytes]; - + if (sockaddrX->sa_family == AF_INET6) { return YES; } } - + return NO; } @@ -8469,18 +8479,18 @@ + (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr family:(sa_family_ if ([address length] >= sizeof(struct sockaddr)) { const struct sockaddr *sockaddrX = [address bytes]; - + if (sockaddrX->sa_family == AF_INET) { if ([address length] >= sizeof(struct sockaddr_in)) { struct sockaddr_in sockaddr4; memcpy(&sockaddr4, sockaddrX, sizeof(sockaddr4)); - + if (hostPtr) *hostPtr = [self hostFromSockaddr4:&sockaddr4]; if (portPtr) *portPtr = [self portFromSockaddr4:&sockaddr4]; if (afPtr) *afPtr = AF_INET; - + return YES; } } @@ -8490,16 +8500,16 @@ + (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr family:(sa_family_ { struct sockaddr_in6 sockaddr6; memcpy(&sockaddr6, sockaddrX, sizeof(sockaddr6)); - + if (hostPtr) *hostPtr = [self hostFromSockaddr6:&sockaddr6]; if (portPtr) *portPtr = [self portFromSockaddr6:&sockaddr6]; if (afPtr) *afPtr = AF_INET6; - + return YES; } } } - + return NO; } @@ -8523,6 +8533,6 @@ + (NSData *)ZeroData return [NSData dataWithBytes:"" length:1]; } -@end +@end #pragma clang diagnostic pop diff --git a/WebDriverAgent/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncUdpSocket.m b/WebDriverAgent/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncUdpSocket.m old mode 100644 new mode 100755 index 8972caa62c..5f65f9717d --- a/WebDriverAgent/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncUdpSocket.m +++ b/WebDriverAgent/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncUdpSocket.m @@ -1,10 +1,10 @@ -// +// // GCDAsyncUdpSocket -// +// // This class is in the public domain. // Originally created by Robbie Hanson of Deusty LLC. // Updated and maintained by Deusty LLC and the Apple development community. -// +// // https://github.com/robbiehanson/CocoaAsyncSocket // @@ -16,8 +16,11 @@ #endif #if TARGET_OS_IPHONE - #import + #import #import + #import + // Note: CFStream APIs are still used for backgrounding support and are part of CoreFoundation + // kCFStreamPropertyShouldCloseNativeSocket is available from CoreFoundation/CFStream.h #endif #import @@ -37,7 +40,7 @@ // Logging uses the CocoaLumberjack framework (which is also GCD based). // https://github.com/robbiehanson/CocoaLumberjack -// +// // It allows us to do a lot of logging without significantly slowing down the code. #import "DDLog.h" @@ -99,7 +102,7 @@ /** * Just to type less code. **/ -#define AutoreleasedBlock(block) ^{ @autoreleasepool { block(); }} +#define AutoreleasedBlock(block) ^{ @autoreleasepool { block(); }} @class GCDAsyncUdpSendPacket; @@ -154,57 +157,57 @@ @interface GCDAsyncUdpSocket () __unsafe_unretained id delegate; #endif dispatch_queue_t delegateQueue; - + GCDAsyncUdpSocketReceiveFilterBlock receiveFilterBlock; dispatch_queue_t receiveFilterQueue; BOOL receiveFilterAsync; - + GCDAsyncUdpSocketSendFilterBlock sendFilterBlock; dispatch_queue_t sendFilterQueue; BOOL sendFilterAsync; - + uint32_t flags; uint16_t config; - + uint16_t max4ReceiveSize; uint32_t max6ReceiveSize; - + uint16_t maxSendSize; - + int socket4FD; int socket6FD; - + dispatch_queue_t socketQueue; - + dispatch_source_t send4Source; dispatch_source_t send6Source; dispatch_source_t receive4Source; dispatch_source_t receive6Source; dispatch_source_t sendTimer; - + GCDAsyncUdpSendPacket *currentSend; NSMutableArray *sendQueue; - + unsigned long socket4FDBytesAvailable; unsigned long socket6FDBytesAvailable; - + uint32_t pendingFilterOperations; - + NSData *cachedLocalAddress4; NSString *cachedLocalHost4; uint16_t cachedLocalPort4; - + NSData *cachedLocalAddress6; NSString *cachedLocalHost6; uint16_t cachedLocalPort6; - + NSData *cachedConnectedAddress; NSString *cachedConnectedHost; uint16_t cachedConnectedPort; int cachedConnectedFamily; - void *IsOnSocketQueueOrTargetQueueKey; - + void *IsOnSocketQueueOrTargetQueueKey; + #if TARGET_OS_IPHONE CFStreamClientContext streamContext; CFReadStreamRef readStream4; @@ -212,7 +215,7 @@ @interface GCDAsyncUdpSocket () CFWriteStreamRef writeStream4; CFWriteStreamRef writeStream6; #endif - + id userData; } @@ -272,13 +275,13 @@ @interface GCDAsyncUdpSendPacket : NSObject { NSData *buffer; NSTimeInterval timeout; long tag; - + BOOL resolveInProgress; BOOL filterInProgress; - + NSArray *resolvedAddresses; NSError *resolveError; - + NSData *address; int addressFamily; } @@ -303,7 +306,7 @@ - (instancetype)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i buffer = d; timeout = t; tag = i; - + resolveInProgress = NO; } return self; @@ -319,9 +322,9 @@ - (instancetype)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i @interface GCDAsyncUdpSpecialPacket : NSObject { @public // uint8_t type; - + BOOL resolveInProgress; - + NSArray *addresses; NSError *error; } @@ -350,32 +353,32 @@ @implementation GCDAsyncUdpSocket - (instancetype)init { LogTrace(); - + return [self initWithDelegate:nil delegateQueue:NULL socketQueue:NULL]; } - (instancetype)initWithSocketQueue:(dispatch_queue_t)sq { LogTrace(); - + return [self initWithDelegate:nil delegateQueue:NULL socketQueue:sq]; } - (instancetype)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq { LogTrace(); - + return [self initWithDelegate:aDelegate delegateQueue:dq socketQueue:NULL]; } - (instancetype)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq { LogTrace(); - + if ((self = [super init])) { delegate = aDelegate; - + if (dq) { delegateQueue = dq; @@ -383,15 +386,15 @@ - (instancetype)initWithDelegate:(id)aDelegate delega dispatch_retain(delegateQueue); #endif } - + max4ReceiveSize = 65535; max6ReceiveSize = 65535; - + maxSendSize = 65535; - + socket4FD = SOCKET_NULL; socket6FD = SOCKET_NULL; - + if (sq) { NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), @@ -400,7 +403,7 @@ - (instancetype)initWithDelegate:(id)aDelegate delega @"The given socketQueue parameter must not be a concurrent queue."); NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), @"The given socketQueue parameter must not be a concurrent queue."); - + socketQueue = sq; #if !OS_OBJECT_USE_OBJC dispatch_retain(socketQueue); @@ -432,10 +435,10 @@ - (instancetype)initWithDelegate:(id)aDelegate delega void *nonNullUnusedPointer = (__bridge void *)self; dispatch_queue_set_specific(socketQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL); - + currentSend = nil; sendQueue = [[NSMutableArray alloc] initWithCapacity:5]; - + #if TARGET_OS_IPHONE [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillEnterForeground:) @@ -449,11 +452,11 @@ - (instancetype)initWithDelegate:(id)aDelegate delega - (void)dealloc { LogInfo(@"%@ - %@ (start)", THIS_METHOD, self); - + #if TARGET_OS_IPHONE [[NSNotificationCenter defaultCenter] removeObserver:self]; #endif - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { [self closeWithError:nil]; @@ -464,18 +467,18 @@ - (void)dealloc [self closeWithError:nil]; }); } - + delegate = nil; #if !OS_OBJECT_USE_OBJC if (delegateQueue) dispatch_release(delegateQueue); #endif delegateQueue = NULL; - + #if !OS_OBJECT_USE_OBJC if (socketQueue) dispatch_release(socketQueue); #endif socketQueue = NULL; - + LogInfo(@"%@ - %@ (finish)", THIS_METHOD, self); } @@ -492,11 +495,11 @@ - (void)dealloc else { __block id result = nil; - + dispatch_sync(socketQueue, ^{ result = self->delegate; }); - + return result; } } @@ -506,7 +509,7 @@ - (void)setDelegate:(id)newDelegate synchronously:(BO dispatch_block_t block = ^{ self->delegate = newDelegate; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { block(); } @@ -537,11 +540,11 @@ - (dispatch_queue_t)delegateQueue else { __block dispatch_queue_t result = NULL; - + dispatch_sync(socketQueue, ^{ result = self->delegateQueue; }); - + return result; } } @@ -549,15 +552,15 @@ - (dispatch_queue_t)delegateQueue - (void)setDelegateQueue:(dispatch_queue_t)newDelegateQueue synchronously:(BOOL)synchronously { dispatch_block_t block = ^{ - + #if !OS_OBJECT_USE_OBJC if (self->delegateQueue) dispatch_release(self->delegateQueue); if (newDelegateQueue) dispatch_retain(newDelegateQueue); #endif - + self->delegateQueue = newDelegateQueue; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { block(); } @@ -590,12 +593,12 @@ - (void)getDelegate:(id *)delegatePtr delegateQueue:( { __block id dPtr = NULL; __block dispatch_queue_t dqPtr = NULL; - + dispatch_sync(socketQueue, ^{ dPtr = self->delegate; dqPtr = self->delegateQueue; }); - + if (delegatePtr) *delegatePtr = dPtr; if (delegateQueuePtr) *delegateQueuePtr = dqPtr; } @@ -604,17 +607,17 @@ - (void)getDelegate:(id *)delegatePtr delegateQueue:( - (void)setDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue synchronously:(BOOL)synchronously { dispatch_block_t block = ^{ - + self->delegate = newDelegate; - + #if !OS_OBJECT_USE_OBJC if (self->delegateQueue) dispatch_release(self->delegateQueue); if (newDelegateQueue) dispatch_retain(newDelegateQueue); #endif - + self->delegateQueue = newDelegateQueue; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { block(); } @@ -639,36 +642,36 @@ - (void)synchronouslySetDelegate:(id)newDelegate dele - (BOOL)isIPv4Enabled { // Note: YES means kIPv4Disabled is OFF - + __block BOOL result = NO; - + dispatch_block_t block = ^{ - + result = ((self->config & kIPv4Disabled) == 0); }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); - + return result; } - (void)setIPv4Enabled:(BOOL)flag { // Note: YES means kIPv4Disabled is OFF - + dispatch_block_t block = ^{ - + LogVerbose(@"%@ %@", THIS_METHOD, (flag ? @"YES" : @"NO")); - + if (flag) self->config &= ~kIPv4Disabled; else self->config |= kIPv4Disabled; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else @@ -678,36 +681,36 @@ - (void)setIPv4Enabled:(BOOL)flag - (BOOL)isIPv6Enabled { // Note: YES means kIPv6Disabled is OFF - + __block BOOL result = NO; - + dispatch_block_t block = ^{ - + result = ((self->config & kIPv6Disabled) == 0); }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); - + return result; } - (void)setIPv6Enabled:(BOOL)flag { // Note: YES means kIPv6Disabled is OFF - + dispatch_block_t block = ^{ - + LogVerbose(@"%@ %@", THIS_METHOD, (flag ? @"YES" : @"NO")); - + if (flag) self->config &= ~kIPv6Disabled; else self->config |= kIPv6Disabled; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else @@ -717,62 +720,62 @@ - (void)setIPv6Enabled:(BOOL)flag - (BOOL)isIPv4Preferred { __block BOOL result = NO; - + dispatch_block_t block = ^{ result = (self->config & kPreferIPv4) ? YES : NO; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); - + return result; } - (BOOL)isIPv6Preferred { __block BOOL result = NO; - + dispatch_block_t block = ^{ result = (self->config & kPreferIPv6) ? YES : NO; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); - + return result; } - (BOOL)isIPVersionNeutral { __block BOOL result = NO; - + dispatch_block_t block = ^{ result = (self->config & (kPreferIPv4 | kPreferIPv6)) == 0; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); - + return result; } - (void)setPreferIPv4 { dispatch_block_t block = ^{ - + LogTrace(); - + self->config |= kPreferIPv4; self->config &= ~kPreferIPv6; - + }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else @@ -782,14 +785,14 @@ - (void)setPreferIPv4 - (void)setPreferIPv6 { dispatch_block_t block = ^{ - + LogTrace(); - + self->config &= ~kPreferIPv4; self->config |= kPreferIPv6; - + }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else @@ -799,14 +802,14 @@ - (void)setPreferIPv6 - (void)setIPVersionNeutral { dispatch_block_t block = ^{ - + LogTrace(); - + self->config &= ~kPreferIPv4; self->config &= ~kPreferIPv6; - + }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else @@ -816,29 +819,29 @@ - (void)setIPVersionNeutral - (uint16_t)maxReceiveIPv4BufferSize { __block uint16_t result = 0; - + dispatch_block_t block = ^{ - + result = self->max4ReceiveSize; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); - + return result; } - (void)setMaxReceiveIPv4BufferSize:(uint16_t)max { dispatch_block_t block = ^{ - + LogVerbose(@"%@ %u", THIS_METHOD, (unsigned)max); - + self->max4ReceiveSize = max; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else @@ -848,29 +851,29 @@ - (void)setMaxReceiveIPv4BufferSize:(uint16_t)max - (uint32_t)maxReceiveIPv6BufferSize { __block uint32_t result = 0; - + dispatch_block_t block = ^{ - + result = self->max6ReceiveSize; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); - + return result; } - (void)setMaxReceiveIPv6BufferSize:(uint32_t)max { dispatch_block_t block = ^{ - + LogVerbose(@"%@ %u", THIS_METHOD, (unsigned)max); - + self->max6ReceiveSize = max; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else @@ -880,12 +883,12 @@ - (void)setMaxReceiveIPv6BufferSize:(uint32_t)max - (void)setMaxSendBufferSize:(uint16_t)max { dispatch_block_t block = ^{ - + LogVerbose(@"%@ %u", THIS_METHOD, (unsigned)max); - + self->maxSendSize = max; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else @@ -895,47 +898,47 @@ - (void)setMaxSendBufferSize:(uint16_t)max - (uint16_t)maxSendBufferSize { __block uint16_t result = 0; - + dispatch_block_t block = ^{ - + result = self->maxSendSize; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); - + return result; } - (id)userData { __block id result = nil; - + dispatch_block_t block = ^{ - + result = self->userData; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); - + return result; } - (void)setUserData:(id)arbitraryUserData { dispatch_block_t block = ^{ - + if (self->userData != arbitraryUserData) { self->userData = arbitraryUserData; } }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else @@ -949,14 +952,14 @@ - (void)setUserData:(id)arbitraryUserData - (void)notifyDidConnectToAddress:(NSData *)anAddress { LogTrace(); - + __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:@selector(udpSocket:didConnectToAddress:)]) { NSData *address = [anAddress copy]; // In case param is NSMutableData - + dispatch_async(delegateQueue, ^{ @autoreleasepool { - + [theDelegate udpSocket:self didConnectToAddress:address]; }}); } @@ -965,12 +968,12 @@ - (void)notifyDidConnectToAddress:(NSData *)anAddress - (void)notifyDidNotConnect:(NSError *)error { LogTrace(); - + __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:@selector(udpSocket:didNotConnect:)]) { dispatch_async(delegateQueue, ^{ @autoreleasepool { - + [theDelegate udpSocket:self didNotConnect:error]; }}); } @@ -979,12 +982,12 @@ - (void)notifyDidNotConnect:(NSError *)error - (void)notifyDidSendDataWithTag:(long)tag { LogTrace(); - + __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:@selector(udpSocket:didSendDataWithTag:)]) { dispatch_async(delegateQueue, ^{ @autoreleasepool { - + [theDelegate udpSocket:self didSendDataWithTag:tag]; }}); } @@ -993,12 +996,12 @@ - (void)notifyDidSendDataWithTag:(long)tag - (void)notifyDidNotSendDataWithTag:(long)tag dueToError:(NSError *)error { LogTrace(); - + __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:@selector(udpSocket:didNotSendDataWithTag:dueToError:)]) { dispatch_async(delegateQueue, ^{ @autoreleasepool { - + [theDelegate udpSocket:self didNotSendDataWithTag:tag dueToError:error]; }}); } @@ -1007,14 +1010,14 @@ - (void)notifyDidNotSendDataWithTag:(long)tag dueToError:(NSError *)error - (void)notifyDidReceiveData:(NSData *)data fromAddress:(NSData *)address withFilterContext:(id)context { LogTrace(); - + SEL selector = @selector(udpSocket:didReceiveData:fromAddress:withFilterContext:); - + __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:selector]) { dispatch_async(delegateQueue, ^{ @autoreleasepool { - + [theDelegate udpSocket:self didReceiveData:data fromAddress:address withFilterContext:context]; }}); } @@ -1023,12 +1026,12 @@ - (void)notifyDidReceiveData:(NSData *)data fromAddress:(NSData *)address withFi - (void)notifyDidCloseWithError:(NSError *)error { LogTrace(); - + __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:@selector(udpSocketDidClose:withError:)]) { dispatch_async(delegateQueue, ^{ @autoreleasepool { - + [theDelegate udpSocketDidClose:self withError:error]; }}); } @@ -1041,7 +1044,7 @@ - (void)notifyDidCloseWithError:(NSError *)error - (NSError *)badConfigError:(NSString *)errMsg { NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; - + return [NSError errorWithDomain:GCDAsyncUdpSocketErrorDomain code:GCDAsyncUdpSocketBadConfigError userInfo:userInfo]; @@ -1050,7 +1053,7 @@ - (NSError *)badConfigError:(NSString *)errMsg - (NSError *)badParamError:(NSString *)errMsg { NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; - + return [NSError errorWithDomain:GCDAsyncUdpSocketErrorDomain code:GCDAsyncUdpSocketBadParamError userInfo:userInfo]; @@ -1060,7 +1063,7 @@ - (NSError *)gaiError:(int)gai_error { NSString *errMsg = [NSString stringWithCString:gai_strerror(gai_error) encoding:NSASCIIStringEncoding]; NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; - + return [NSError errorWithDomain:@"kCFStreamErrorDomainNetDB" code:gai_error userInfo:userInfo]; } @@ -1068,13 +1071,13 @@ - (NSError *)errnoErrorWithReason:(NSString *)reason { NSString *errMsg = [NSString stringWithUTF8String:strerror(errno)]; NSDictionary *userInfo; - + if (reason) userInfo = @{NSLocalizedDescriptionKey : errMsg, NSLocalizedFailureReasonErrorKey : reason}; else userInfo = @{NSLocalizedDescriptionKey : errMsg}; - + return [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:userInfo]; } @@ -1091,9 +1094,9 @@ - (NSError *)sendTimeoutError NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncUdpSocketSendTimeoutError", @"GCDAsyncUdpSocket", [NSBundle mainBundle], @"Send operation timed out", nil); - + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; - + return [NSError errorWithDomain:GCDAsyncUdpSocketErrorDomain code:GCDAsyncUdpSocketSendTimeoutError userInfo:userInfo]; @@ -1104,16 +1107,16 @@ - (NSError *)socketClosedError NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncUdpSocketClosedError", @"GCDAsyncUdpSocket", [NSBundle mainBundle], @"Socket closed", nil); - + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; - + return [NSError errorWithDomain:GCDAsyncUdpSocketErrorDomain code:GCDAsyncUdpSocketClosedError userInfo:userInfo]; } - (NSError *)otherError:(NSString *)errMsg { NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; - + return [NSError errorWithDomain:GCDAsyncUdpSocketErrorDomain code:GCDAsyncUdpSocketOtherError userInfo:userInfo]; @@ -1126,7 +1129,7 @@ - (NSError *)otherError:(NSString *)errMsg - (BOOL)preOp:(NSError **)errPtr { NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - + if (delegate == nil) // Must have delegate set { if (errPtr) @@ -1136,7 +1139,7 @@ - (BOOL)preOp:(NSError **)errPtr } return NO; } - + if (delegateQueue == NULL) // Must have delegate queue set { if (errPtr) @@ -1146,7 +1149,7 @@ - (BOOL)preOp:(NSError **)errPtr } return NO; } - + return YES; } @@ -1159,56 +1162,56 @@ - (void)asyncResolveHost:(NSString *)aHost withCompletionBlock:(void (^)(NSArray *addresses, NSError *error))completionBlock { LogTrace(); - + // Check parameter(s) - + if (aHost == nil) { NSString *msg = @"The host param is nil. Should be domain name or IP address string."; NSError *error = [self badParamError:msg]; - + // We should still use dispatch_async since this method is expected to be asynchronous - + dispatch_async(socketQueue, ^{ @autoreleasepool { - + completionBlock(nil, error); }}); - + return; } - + // It's possible that the given aHost parameter is actually a NSMutableString. // So we want to copy it now, within this block that will be executed synchronously. // This way the asynchronous lookup block below doesn't have to worry about it changing. - + NSString *host = [aHost copy]; - - + + dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(globalConcurrentQueue, ^{ @autoreleasepool { - + NSMutableArray *addresses = [NSMutableArray arrayWithCapacity:2]; NSError *error = nil; - + if ([host isEqualToString:@"localhost"] || [host isEqualToString:@"loopback"]) { // Use LOOPBACK address struct sockaddr_in sockaddr4; memset(&sockaddr4, 0, sizeof(sockaddr4)); - + sockaddr4.sin_len = sizeof(struct sockaddr_in); sockaddr4.sin_family = AF_INET; sockaddr4.sin_port = htons(port); sockaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); - + struct sockaddr_in6 sockaddr6; memset(&sockaddr6, 0, sizeof(sockaddr6)); - + sockaddr6.sin6_len = sizeof(struct sockaddr_in6); sockaddr6.sin6_family = AF_INET6; sockaddr6.sin6_port = htons(port); sockaddr6.sin6_addr = in6addr_loopback; - + // Wrap the native address structures and add to list [addresses addObject:[NSData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]]; [addresses addObject:[NSData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]]; @@ -1216,16 +1219,16 @@ - (void)asyncResolveHost:(NSString *)aHost else { NSString *portStr = [NSString stringWithFormat:@"%hu", port]; - + struct addrinfo hints, *res, *res0; - + memset(&hints, 0, sizeof(hints)); hints.ai_family = PF_UNSPEC; hints.ai_socktype = SOCK_DGRAM; hints.ai_protocol = IPPROTO_UDP; - + int gai_error = getaddrinfo([host UTF8String], [portStr UTF8String], &hints, &res0); - + if (gai_error) { error = [self gaiError:gai_error]; @@ -1238,7 +1241,7 @@ - (void)asyncResolveHost:(NSString *)aHost { // Found IPv4 address // Wrap the native address structure and add to list - + [addresses addObject:[NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]]; } else if (res->ai_family == AF_INET6) @@ -1258,26 +1261,26 @@ - (void)asyncResolveHost:(NSString *)aHost } } freeaddrinfo(res0); - + if ([addresses count] == 0) { error = [self gaiError:EAI_FAIL]; } } } - + dispatch_async(self->socketQueue, ^{ @autoreleasepool { - + completionBlock(addresses, error); }}); - + }}); } /** * This method picks an address from the given list of addresses. * The address picked depends upon which protocols are disabled, deactived, & preferred. - * + * * Returns the address family (AF_INET or AF_INET6) of the picked address, * or AF_UNSPEC and the corresponding error is there's a problem. **/ @@ -1285,93 +1288,93 @@ - (int)getAddress:(NSData **)addressPtr error:(NSError **)errorPtr fromAddresses { NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); NSAssert([addresses count] > 0, @"Expected at least one address"); - + int resultAF = AF_UNSPEC; NSData *resultAddress = nil; NSError *resultError = nil; - + // Check for problems - + BOOL resolvedIPv4Address = NO; BOOL resolvedIPv6Address = NO; - + for (NSData *address in addresses) { switch ([[self class] familyFromAddress:address]) { case AF_INET : resolvedIPv4Address = YES; break; case AF_INET6 : resolvedIPv6Address = YES; break; - + default : NSAssert(NO, @"Addresses array contains invalid address"); } } - + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; - + if (isIPv4Disabled && !resolvedIPv6Address) { NSString *msg = @"IPv4 has been disabled and DNS lookup found no IPv6 address(es)."; resultError = [self otherError:msg]; - + if (addressPtr) *addressPtr = resultAddress; if (errorPtr) *errorPtr = resultError; - + return resultAF; } - + if (isIPv6Disabled && !resolvedIPv4Address) { NSString *msg = @"IPv6 has been disabled and DNS lookup found no IPv4 address(es)."; resultError = [self otherError:msg]; - + if (addressPtr) *addressPtr = resultAddress; if (errorPtr) *errorPtr = resultError; - + return resultAF; } - + BOOL isIPv4Deactivated = (flags & kIPv4Deactivated) ? YES : NO; BOOL isIPv6Deactivated = (flags & kIPv6Deactivated) ? YES : NO; - + if (isIPv4Deactivated && !resolvedIPv6Address) { NSString *msg = @"IPv4 has been deactivated due to bind/connect, and DNS lookup found no IPv6 address(es)."; resultError = [self otherError:msg]; - + if (addressPtr) *addressPtr = resultAddress; if (errorPtr) *errorPtr = resultError; - + return resultAF; } - + if (isIPv6Deactivated && !resolvedIPv4Address) { NSString *msg = @"IPv6 has been deactivated due to bind/connect, and DNS lookup found no IPv4 address(es)."; resultError = [self otherError:msg]; - + if (addressPtr) *addressPtr = resultAddress; if (errorPtr) *errorPtr = resultError; - + return resultAF; } - + // Extract first IPv4 and IPv6 address in list - + BOOL ipv4WasFirstInList = YES; NSData *address4 = nil; NSData *address6 = nil; - + for (NSData *address in addresses) { int af = [[self class] familyFromAddress:address]; - + if (af == AF_INET) { if (address4 == nil) { address4 = address; - + if (address6) break; else @@ -1383,7 +1386,7 @@ - (int)getAddress:(NSData **)addressPtr error:(NSError **)errorPtr fromAddresses if (address6 == nil) { address6 = address; - + if (address4) break; else @@ -1391,18 +1394,18 @@ - (int)getAddress:(NSData **)addressPtr error:(NSError **)errorPtr fromAddresses } } } - + // Determine socket type - + BOOL preferIPv4 = (config & kPreferIPv4) ? YES : NO; BOOL preferIPv6 = (config & kPreferIPv6) ? YES : NO; - + BOOL useIPv4 = ((preferIPv4 && address4) || (address6 == nil)); BOOL useIPv6 = ((preferIPv6 && address6) || (address4 == nil)); - + NSAssert(!(preferIPv4 && preferIPv6), @"Invalid config state"); NSAssert(!(useIPv4 && useIPv6), @"Invalid logic"); - + if (useIPv4 || (!useIPv6 && ipv4WasFirstInList)) { resultAF = AF_INET; @@ -1413,10 +1416,10 @@ - (int)getAddress:(NSData **)addressPtr error:(NSError **)errorPtr fromAddresses resultAF = AF_INET6; resultAddress = address6; } - + if (addressPtr) *addressPtr = resultAddress; if (errorPtr) *errorPtr = resultError; - + return resultAF; } @@ -1431,27 +1434,27 @@ - (void)convertIntefaceDescription:(NSString *)interfaceDescription { NSData *addr4 = nil; NSData *addr6 = nil; - + if (interfaceDescription == nil) { // ANY address - + struct sockaddr_in sockaddr4; memset(&sockaddr4, 0, sizeof(sockaddr4)); - + sockaddr4.sin_len = sizeof(sockaddr4); sockaddr4.sin_family = AF_INET; sockaddr4.sin_port = htons(port); sockaddr4.sin_addr.s_addr = htonl(INADDR_ANY); - + struct sockaddr_in6 sockaddr6; memset(&sockaddr6, 0, sizeof(sockaddr6)); - + sockaddr6.sin6_len = sizeof(sockaddr6); sockaddr6.sin6_family = AF_INET6; sockaddr6.sin6_port = htons(port); sockaddr6.sin6_addr = in6addr_any; - + addr4 = [NSData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; addr6 = [NSData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; } @@ -1459,33 +1462,33 @@ - (void)convertIntefaceDescription:(NSString *)interfaceDescription [interfaceDescription isEqualToString:@"loopback"]) { // LOOPBACK address - + struct sockaddr_in sockaddr4; memset(&sockaddr4, 0, sizeof(sockaddr4)); - + sockaddr4.sin_len = sizeof(struct sockaddr_in); sockaddr4.sin_family = AF_INET; sockaddr4.sin_port = htons(port); sockaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); - + struct sockaddr_in6 sockaddr6; memset(&sockaddr6, 0, sizeof(sockaddr6)); - + sockaddr6.sin6_len = sizeof(struct sockaddr_in6); sockaddr6.sin6_family = AF_INET6; sockaddr6.sin6_port = htons(port); sockaddr6.sin6_addr = in6addr_loopback; - + addr4 = [NSData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; addr6 = [NSData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; } else { const char *iface = [interfaceDescription UTF8String]; - + struct ifaddrs *addrs; const struct ifaddrs *cursor; - + if ((getifaddrs(&addrs) == 0)) { cursor = addrs; @@ -1494,32 +1497,32 @@ - (void)convertIntefaceDescription:(NSString *)interfaceDescription if ((addr4 == nil) && (cursor->ifa_addr->sa_family == AF_INET)) { // IPv4 - + struct sockaddr_in *addr = (struct sockaddr_in *)(void *)cursor->ifa_addr; - + if (strcmp(cursor->ifa_name, iface) == 0) { // Name match - + struct sockaddr_in nativeAddr4 = *addr; nativeAddr4.sin_port = htons(port); - + addr4 = [NSData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; } else { char ip[INET_ADDRSTRLEN]; - + const char *conversion; conversion = inet_ntop(AF_INET, &addr->sin_addr, ip, sizeof(ip)); - + if ((conversion != NULL) && (strcmp(ip, iface) == 0)) { // IP match - + struct sockaddr_in nativeAddr4 = *addr; nativeAddr4.sin_port = htons(port); - + addr4 = [NSData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; } } @@ -1527,44 +1530,44 @@ - (void)convertIntefaceDescription:(NSString *)interfaceDescription else if ((addr6 == nil) && (cursor->ifa_addr->sa_family == AF_INET6)) { // IPv6 - + const struct sockaddr_in6 *addr = (const struct sockaddr_in6 *)(const void *)cursor->ifa_addr; - + if (strcmp(cursor->ifa_name, iface) == 0) { // Name match - + struct sockaddr_in6 nativeAddr6 = *addr; nativeAddr6.sin6_port = htons(port); - + addr6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; } else { char ip[INET6_ADDRSTRLEN]; - + const char *conversion; conversion = inet_ntop(AF_INET6, &addr->sin6_addr, ip, sizeof(ip)); - + if ((conversion != NULL) && (strcmp(ip, iface) == 0)) { // IP match - + struct sockaddr_in6 nativeAddr6 = *addr; nativeAddr6.sin6_port = htons(port); - + addr6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; } } } - + cursor = cursor->ifa_next; } - + freeifaddrs(addrs); } } - + if (interfaceAddr4Ptr) *interfaceAddr4Ptr = addr4; if (interfaceAddr6Ptr) *interfaceAddr6Ptr = addr6; } @@ -1580,19 +1583,19 @@ - (void)convertNumericHost:(NSString *)numericHost { NSData *addr4 = nil; NSData *addr6 = nil; - + if (numericHost) { NSString *portStr = [NSString stringWithFormat:@"%hu", port]; - + struct addrinfo hints, *res, *res0; - + memset(&hints, 0, sizeof(hints)); hints.ai_family = PF_UNSPEC; hints.ai_socktype = SOCK_DGRAM; hints.ai_protocol = IPPROTO_UDP; hints.ai_flags = AI_NUMERICHOST; // No name resolution should be attempted - + if (getaddrinfo([numericHost UTF8String], [portStr UTF8String], &hints, &res0) == 0) { for (res = res0; res; res = res->ai_next) @@ -1613,7 +1616,7 @@ - (void)convertNumericHost:(NSString *)numericHost freeaddrinfo(res0); } } - + if (addr4Ptr) *addr4Ptr = addr4; if (addr6Ptr) *addr6Ptr = addr6; } @@ -1623,15 +1626,15 @@ - (BOOL)isConnectedToAddress4:(NSData *)someAddr4 NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); NSAssert(flags & kDidConnect, @"Not connected"); NSAssert(cachedConnectedAddress, @"Expected cached connected address"); - + if (cachedConnectedFamily != AF_INET) { return NO; } - + const struct sockaddr_in *sSockaddr4 = (const struct sockaddr_in *)[someAddr4 bytes]; const struct sockaddr_in *cSockaddr4 = (const struct sockaddr_in *)[cachedConnectedAddress bytes]; - + if (memcmp(&sSockaddr4->sin_addr, &cSockaddr4->sin_addr, sizeof(struct in_addr)) != 0) { return NO; @@ -1640,7 +1643,7 @@ - (BOOL)isConnectedToAddress4:(NSData *)someAddr4 { return NO; } - + return YES; } @@ -1649,15 +1652,15 @@ - (BOOL)isConnectedToAddress6:(NSData *)someAddr6 NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); NSAssert(flags & kDidConnect, @"Not connected"); NSAssert(cachedConnectedAddress, @"Expected cached connected address"); - + if (cachedConnectedFamily != AF_INET6) { return NO; } - + const struct sockaddr_in6 *sSockaddr6 = (const struct sockaddr_in6 *)[someAddr6 bytes]; const struct sockaddr_in6 *cSockaddr6 = (const struct sockaddr_in6 *)[cachedConnectedAddress bytes]; - + if (memcmp(&sSockaddr6->sin6_addr, &cSockaddr6->sin6_addr, sizeof(struct in6_addr)) != 0) { return NO; @@ -1666,7 +1669,7 @@ - (BOOL)isConnectedToAddress6:(NSData *)someAddr6 { return NO; } - + return YES; } @@ -1676,13 +1679,13 @@ - (unsigned int)indexOfInterfaceAddr4:(NSData *)interfaceAddr4 return 0; if ([interfaceAddr4 length] != sizeof(struct sockaddr_in)) return 0; - + int result = 0; const struct sockaddr_in *ifaceAddr = (const struct sockaddr_in *)[interfaceAddr4 bytes]; - + struct ifaddrs *addrs; const struct ifaddrs *cursor; - + if ((getifaddrs(&addrs) == 0)) { cursor = addrs; @@ -1691,22 +1694,22 @@ - (unsigned int)indexOfInterfaceAddr4:(NSData *)interfaceAddr4 if (cursor->ifa_addr->sa_family == AF_INET) { // IPv4 - + const struct sockaddr_in *addr = (const struct sockaddr_in *)(const void *)cursor->ifa_addr; - + if (memcmp(&addr->sin_addr, &ifaceAddr->sin_addr, sizeof(struct in_addr)) == 0) { result = if_nametoindex(cursor->ifa_name); break; } } - + cursor = cursor->ifa_next; } - + freeifaddrs(addrs); } - + return result; } @@ -1716,13 +1719,13 @@ - (unsigned int)indexOfInterfaceAddr6:(NSData *)interfaceAddr6 return 0; if ([interfaceAddr6 length] != sizeof(struct sockaddr_in6)) return 0; - + int result = 0; const struct sockaddr_in6 *ifaceAddr = (const struct sockaddr_in6 *)[interfaceAddr6 bytes]; - + struct ifaddrs *addrs; const struct ifaddrs *cursor; - + if ((getifaddrs(&addrs) == 0)) { cursor = addrs; @@ -1731,22 +1734,22 @@ - (unsigned int)indexOfInterfaceAddr6:(NSData *)interfaceAddr6 if (cursor->ifa_addr->sa_family == AF_INET6) { // IPv6 - + const struct sockaddr_in6 *addr = (const struct sockaddr_in6 *)(const void *)cursor->ifa_addr; - + if (memcmp(&addr->sin6_addr, &ifaceAddr->sin6_addr, sizeof(struct in6_addr)) == 0) { result = if_nametoindex(cursor->ifa_name); break; } } - + cursor = cursor->ifa_next; } - + freeifaddrs(addrs); } - + return result; } @@ -1754,22 +1757,22 @@ - (void)setupSendAndReceiveSourcesForSocket4 { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - + send4Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, socket4FD, 0, socketQueue); receive4Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket4FD, 0, socketQueue); - + // Setup event handlers - + dispatch_source_set_event_handler(send4Source, ^{ @autoreleasepool { - + LogVerbose(@"send4EventBlock"); LogVerbose(@"dispatch_source_get_data(send4Source) = %lu", dispatch_source_get_data(send4Source)); - + self->flags |= kSock4CanAcceptBytes; - + // If we're ready to send data, do so immediately. // Otherwise pause the send source or it will continue to fire over and over again. - + if (self->currentSend == nil) { LogVerbose(@"Nothing to send"); @@ -1789,74 +1792,74 @@ - (void)setupSendAndReceiveSourcesForSocket4 { [self doSend]; } - + }}); - + dispatch_source_set_event_handler(receive4Source, ^{ @autoreleasepool { - + LogVerbose(@"receive4EventBlock"); - + self->socket4FDBytesAvailable = dispatch_source_get_data(self->receive4Source); LogVerbose(@"socket4FDBytesAvailable: %lu", socket4FDBytesAvailable); - + if (self->socket4FDBytesAvailable > 0) [self doReceive]; else [self doReceiveEOF]; - + }}); - + // Setup cancel handlers - + __block int socketFDRefCount = 2; - + int theSocketFD = socket4FD; - + #if !OS_OBJECT_USE_OBJC dispatch_source_t theSendSource = send4Source; dispatch_source_t theReceiveSource = receive4Source; #endif - + dispatch_source_set_cancel_handler(send4Source, ^{ - + LogVerbose(@"send4CancelBlock"); - + #if !OS_OBJECT_USE_OBJC LogVerbose(@"dispatch_release(send4Source)"); dispatch_release(theSendSource); #endif - + if (--socketFDRefCount == 0) { LogVerbose(@"close(socket4FD)"); close(theSocketFD); } }); - + dispatch_source_set_cancel_handler(receive4Source, ^{ - + LogVerbose(@"receive4CancelBlock"); - + #if !OS_OBJECT_USE_OBJC LogVerbose(@"dispatch_release(receive4Source)"); dispatch_release(theReceiveSource); #endif - + if (--socketFDRefCount == 0) { LogVerbose(@"close(socket4FD)"); close(theSocketFD); } }); - + // We will not be able to receive until the socket is bound to a port, // either explicitly via bind, or implicitly by connect or by sending data. - // + // // But we should be able to send immediately. - + socket4FDBytesAvailable = 0; flags |= kSock4CanAcceptBytes; - + flags |= kSend4SourceSuspended; flags |= kReceive4SourceSuspended; } @@ -1865,22 +1868,22 @@ - (void)setupSendAndReceiveSourcesForSocket6 { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - + send6Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, socket6FD, 0, socketQueue); receive6Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket6FD, 0, socketQueue); - + // Setup event handlers - + dispatch_source_set_event_handler(send6Source, ^{ @autoreleasepool { - + LogVerbose(@"send6EventBlock"); LogVerbose(@"dispatch_source_get_data(send6Source) = %lu", dispatch_source_get_data(send6Source)); - + self->flags |= kSock6CanAcceptBytes; - + // If we're ready to send data, do so immediately. // Otherwise pause the send source or it will continue to fire over and over again. - + if (self->currentSend == nil) { LogVerbose(@"Nothing to send"); @@ -1900,74 +1903,74 @@ - (void)setupSendAndReceiveSourcesForSocket6 { [self doSend]; } - + }}); - + dispatch_source_set_event_handler(receive6Source, ^{ @autoreleasepool { - + LogVerbose(@"receive6EventBlock"); - + self->socket6FDBytesAvailable = dispatch_source_get_data(self->receive6Source); LogVerbose(@"socket6FDBytesAvailable: %lu", socket6FDBytesAvailable); - + if (self->socket6FDBytesAvailable > 0) [self doReceive]; else [self doReceiveEOF]; - + }}); - + // Setup cancel handlers - + __block int socketFDRefCount = 2; - + int theSocketFD = socket6FD; - + #if !OS_OBJECT_USE_OBJC dispatch_source_t theSendSource = send6Source; dispatch_source_t theReceiveSource = receive6Source; #endif - + dispatch_source_set_cancel_handler(send6Source, ^{ - + LogVerbose(@"send6CancelBlock"); - + #if !OS_OBJECT_USE_OBJC LogVerbose(@"dispatch_release(send6Source)"); dispatch_release(theSendSource); #endif - + if (--socketFDRefCount == 0) { LogVerbose(@"close(socket6FD)"); close(theSocketFD); } }); - + dispatch_source_set_cancel_handler(receive6Source, ^{ - + LogVerbose(@"receive6CancelBlock"); - + #if !OS_OBJECT_USE_OBJC LogVerbose(@"dispatch_release(receive6Source)"); dispatch_release(theReceiveSource); #endif - + if (--socketFDRefCount == 0) { LogVerbose(@"close(socket6FD)"); close(theSocketFD); } }); - + // We will not be able to receive until the socket is bound to a port, // either explicitly via bind, or implicitly by connect or by sending data. - // + // // But we should be able to send immediately. - + socket6FDBytesAvailable = 0; flags |= kSock6CanAcceptBytes; - + flags |= kSend6SourceSuspended; flags |= kReceive6SourceSuspended; } @@ -1975,61 +1978,61 @@ - (void)setupSendAndReceiveSourcesForSocket6 - (BOOL)createSocket4:(BOOL)useIPv4 socket6:(BOOL)useIPv6 error:(NSError * __autoreleasing *)errPtr { LogTrace(); - + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); NSAssert(((flags & kDidCreateSockets) == 0), @"Sockets have already been created"); - + // CreateSocket Block // This block will be invoked below. - + int(^createSocket)(int) = ^int (int domain) { - + int socketFD = socket(domain, SOCK_DGRAM, 0); - + if (socketFD == SOCKET_NULL) { if (errPtr) *errPtr = [self errnoErrorWithReason:@"Error in socket() function"]; - + return SOCKET_NULL; } - + int status; - + // Set socket options - + status = fcntl(socketFD, F_SETFL, O_NONBLOCK); if (status == -1) { if (errPtr) *errPtr = [self errnoErrorWithReason:@"Error enabling non-blocking IO on socket (fcntl)"]; - + close(socketFD); return SOCKET_NULL; } - + int reuseaddr = 1; status = setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseaddr, sizeof(reuseaddr)); if (status == -1) { if (errPtr) *errPtr = [self errnoErrorWithReason:@"Error enabling address reuse (setsockopt)"]; - + close(socketFD); return SOCKET_NULL; } - + int nosigpipe = 1; status = setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); if (status == -1) { if (errPtr) *errPtr = [self errnoErrorWithReason:@"Error disabling sigpipe (setsockopt)"]; - + close(socketFD); return SOCKET_NULL; } - + /** * The theoretical maximum size of any IPv4 UDP packet is UINT16_MAX = 65535. * The theoretical maximum size of any IPv6 UDP packet is UINT32_MAX = 4294967295. @@ -2043,7 +2046,7 @@ - (BOOL)createSocket4:(BOOL)useIPv4 socket6:(BOOL)useIPv6 error:(NSError * __aut * Enlarge the maximum size of UDP packet. * I can not ensure the protocol type now so that the max size is set to 65535 :) **/ - + status = setsockopt(socketFD, SOL_SOCKET, SO_SNDBUF, (const char*)&self->maxSendSize, sizeof(int)); if (status == -1) { @@ -2052,7 +2055,7 @@ - (BOOL)createSocket4:(BOOL)useIPv4 socket6:(BOOL)useIPv6 error:(NSError * __aut close(socketFD); return SOCKET_NULL; } - + status = setsockopt(socketFD, SOL_SOCKET, SO_RCVBUF, (const char*)&self->maxSendSize, sizeof(int)); if (status == -1) { @@ -2062,16 +2065,16 @@ - (BOOL)createSocket4:(BOOL)useIPv4 socket6:(BOOL)useIPv6 error:(NSError * __aut return SOCKET_NULL; } - + return socketFD; }; - + // Create sockets depending upon given configuration. - + if (useIPv4) { LogVerbose(@"Creating IPv4 socket"); - + socket4FD = createSocket(AF_INET); if (socket4FD == SOCKET_NULL) { @@ -2079,33 +2082,33 @@ - (BOOL)createSocket4:(BOOL)useIPv4 socket6:(BOOL)useIPv6 error:(NSError * __aut return NO; } } - + if (useIPv6) { LogVerbose(@"Creating IPv6 socket"); - + socket6FD = createSocket(AF_INET6); if (socket6FD == SOCKET_NULL) { // errPtr set in local createSocket() block - + if (socket4FD != SOCKET_NULL) { close(socket4FD); socket4FD = SOCKET_NULL; } - + return NO; } } - + // Setup send and receive sources - + if (useIPv4) [self setupSendAndReceiveSourcesForSocket4]; if (useIPv6) [self setupSendAndReceiveSourcesForSocket6]; - + flags |= kDidCreateSockets; return YES; } @@ -2113,10 +2116,10 @@ - (BOOL)createSocket4:(BOOL)useIPv4 socket6:(BOOL)useIPv6 error:(NSError * __aut - (BOOL)createSockets:(NSError **)errPtr { LogTrace(); - + BOOL useIPv4 = [self isIPv4Enabled]; BOOL useIPv6 = [self isIPv6Enabled]; - + return [self createSocket4:useIPv4 socket6:useIPv6 error:errPtr]; } @@ -2125,7 +2128,7 @@ - (void)suspendSend4Source if (send4Source && !(flags & kSend4SourceSuspended)) { LogVerbose(@"dispatch_suspend(send4Source)"); - + dispatch_suspend(send4Source); flags |= kSend4SourceSuspended; } @@ -2136,7 +2139,7 @@ - (void)suspendSend6Source if (send6Source && !(flags & kSend6SourceSuspended)) { LogVerbose(@"dispatch_suspend(send6Source)"); - + dispatch_suspend(send6Source); flags |= kSend6SourceSuspended; } @@ -2147,7 +2150,7 @@ - (void)resumeSend4Source if (send4Source && (flags & kSend4SourceSuspended)) { LogVerbose(@"dispatch_resume(send4Source)"); - + dispatch_resume(send4Source); flags &= ~kSend4SourceSuspended; } @@ -2158,7 +2161,7 @@ - (void)resumeSend6Source if (send6Source && (flags & kSend6SourceSuspended)) { LogVerbose(@"dispatch_resume(send6Source)"); - + dispatch_resume(send6Source); flags &= ~kSend6SourceSuspended; } @@ -2169,7 +2172,7 @@ - (void)suspendReceive4Source if (receive4Source && !(flags & kReceive4SourceSuspended)) { LogVerbose(@"dispatch_suspend(receive4Source)"); - + dispatch_suspend(receive4Source); flags |= kReceive4SourceSuspended; } @@ -2180,7 +2183,7 @@ - (void)suspendReceive6Source if (receive6Source && !(flags & kReceive6SourceSuspended)) { LogVerbose(@"dispatch_suspend(receive6Source)"); - + dispatch_suspend(receive6Source); flags |= kReceive6SourceSuspended; } @@ -2191,7 +2194,7 @@ - (void)resumeReceive4Source if (receive4Source && (flags & kReceive4SourceSuspended)) { LogVerbose(@"dispatch_resume(receive4Source)"); - + dispatch_resume(receive4Source); flags &= ~kReceive4SourceSuspended; } @@ -2202,7 +2205,7 @@ - (void)resumeReceive6Source if (receive6Source && (flags & kReceive6SourceSuspended)) { LogVerbose(@"dispatch_resume(receive6Source)"); - + dispatch_resume(receive6Source); flags &= ~kReceive6SourceSuspended; } @@ -2214,32 +2217,32 @@ - (void)closeSocket4 { LogVerbose(@"dispatch_source_cancel(send4Source)"); dispatch_source_cancel(send4Source); - + LogVerbose(@"dispatch_source_cancel(receive4Source)"); dispatch_source_cancel(receive4Source); - + // For some crazy reason (in my opinion), cancelling a dispatch source doesn't // invoke the cancel handler if the dispatch source is paused. // So we have to unpause the source if needed. // This allows the cancel handler to be run, which in turn releases the source and closes the socket. - + [self resumeSend4Source]; [self resumeReceive4Source]; - + // The sockets will be closed by the cancel handlers of the corresponding source - + send4Source = NULL; receive4Source = NULL; - + socket4FD = SOCKET_NULL; - + // Clear socket states - + socket4FDBytesAvailable = 0; flags &= ~kSock4CanAcceptBytes; - + // Clear cached info - + cachedLocalAddress4 = nil; cachedLocalHost4 = nil; cachedLocalPort4 = 0; @@ -2252,32 +2255,32 @@ - (void)closeSocket6 { LogVerbose(@"dispatch_source_cancel(send6Source)"); dispatch_source_cancel(send6Source); - + LogVerbose(@"dispatch_source_cancel(receive6Source)"); dispatch_source_cancel(receive6Source); - + // For some crazy reason (in my opinion), cancelling a dispatch source doesn't // invoke the cancel handler if the dispatch source is paused. // So we have to unpause the source if needed. // This allows the cancel handler to be run, which in turn releases the source and closes the socket. - + [self resumeSend6Source]; [self resumeReceive6Source]; - + send6Source = NULL; receive6Source = NULL; - + // The sockets will be closed by the cancel handlers of the corresponding source - + socket6FD = SOCKET_NULL; - + // Clear socket states - + socket6FDBytesAvailable = 0; flags &= ~kSock6CanAcceptBytes; - + // Clear cached info - + cachedLocalAddress6 = nil; cachedLocalHost6 = nil; cachedLocalPort6 = 0; @@ -2288,7 +2291,7 @@ - (void)closeSockets { [self closeSocket4]; [self closeSocket6]; - + flags &= ~kDidCreateSockets; } @@ -2302,16 +2305,16 @@ - (BOOL)getLocalAddress:(NSData **)dataPtr forSocket:(int)socketFD withFamily:(int)socketFamily { - + NSData *data = nil; NSString *host = nil; uint16_t port = 0; - + if (socketFamily == AF_INET) { struct sockaddr_in sockaddr4; socklen_t sockaddr4len = sizeof(sockaddr4); - + if (getsockname(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) == 0) { data = [NSData dataWithBytes:&sockaddr4 length:sockaddr4len]; @@ -2327,7 +2330,7 @@ - (BOOL)getLocalAddress:(NSData **)dataPtr { struct sockaddr_in6 sockaddr6; socklen_t sockaddr6len = sizeof(sockaddr6); - + if (getsockname(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) == 0) { data = [NSData dataWithBytes:&sockaddr6 length:sockaddr6len]; @@ -2339,30 +2342,30 @@ - (BOOL)getLocalAddress:(NSData **)dataPtr LogWarn(@"Error in getsockname: %@", [self errnoError]); } } - + if (dataPtr) *dataPtr = data; if (hostPtr) *hostPtr = host; if (portPtr) *portPtr = port; - + return (data != nil); } - (void)maybeUpdateCachedLocalAddress4Info { NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - + if ( cachedLocalAddress4 || ((flags & kDidBind) == 0) || (socket4FD == SOCKET_NULL) ) { return; } - + NSData *address = nil; NSString *host = nil; uint16_t port = 0; - + if ([self getLocalAddress:&address host:&host port:&port forSocket:socket4FD withFamily:AF_INET]) { - + cachedLocalAddress4 = address; cachedLocalHost4 = host; cachedLocalPort4 = port; @@ -2372,19 +2375,19 @@ - (void)maybeUpdateCachedLocalAddress4Info - (void)maybeUpdateCachedLocalAddress6Info { NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - + if ( cachedLocalAddress6 || ((flags & kDidBind) == 0) || (socket6FD == SOCKET_NULL) ) { return; } - + NSData *address = nil; NSString *host = nil; uint16_t port = 0; - + if ([self getLocalAddress:&address host:&host port:&port forSocket:socket6FD withFamily:AF_INET6]) { - + cachedLocalAddress6 = address; cachedLocalHost6 = host; cachedLocalPort6 = port; @@ -2394,9 +2397,9 @@ - (void)maybeUpdateCachedLocalAddress6Info - (NSData *)localAddress { __block NSData *result = nil; - + dispatch_block_t block = ^{ - + if (self->socket4FD != SOCKET_NULL) { [self maybeUpdateCachedLocalAddress4Info]; @@ -2407,23 +2410,23 @@ - (NSData *)localAddress [self maybeUpdateCachedLocalAddress6Info]; result = self->cachedLocalAddress6; } - + }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, AutoreleasedBlock(block)); - + return result; } - (NSString *)localHost { __block NSString *result = nil; - + dispatch_block_t block = ^{ - + if (self->socket4FD != SOCKET_NULL) { [self maybeUpdateCachedLocalAddress4Info]; @@ -2435,21 +2438,21 @@ - (NSString *)localHost result = self->cachedLocalHost6; } }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, AutoreleasedBlock(block)); - + return result; } - (uint16_t)localPort { __block uint16_t result = 0; - + dispatch_block_t block = ^{ - + if (self->socket4FD != SOCKET_NULL) { [self maybeUpdateCachedLocalAddress4Info]; @@ -2461,142 +2464,142 @@ - (uint16_t)localPort result = self->cachedLocalPort6; } }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, AutoreleasedBlock(block)); - + return result; } - (NSData *)localAddress_IPv4 { __block NSData *result = nil; - + dispatch_block_t block = ^{ - + [self maybeUpdateCachedLocalAddress4Info]; result = self->cachedLocalAddress4; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, AutoreleasedBlock(block)); - + return result; } - (NSString *)localHost_IPv4 { __block NSString *result = nil; - + dispatch_block_t block = ^{ - + [self maybeUpdateCachedLocalAddress4Info]; result = self->cachedLocalHost4; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, AutoreleasedBlock(block)); - + return result; } - (uint16_t)localPort_IPv4 { __block uint16_t result = 0; - + dispatch_block_t block = ^{ - + [self maybeUpdateCachedLocalAddress4Info]; result = self->cachedLocalPort4; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, AutoreleasedBlock(block)); - + return result; } - (NSData *)localAddress_IPv6 { __block NSData *result = nil; - + dispatch_block_t block = ^{ - + [self maybeUpdateCachedLocalAddress6Info]; result = self->cachedLocalAddress6; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, AutoreleasedBlock(block)); - + return result; } - (NSString *)localHost_IPv6 { __block NSString *result = nil; - + dispatch_block_t block = ^{ - + [self maybeUpdateCachedLocalAddress6Info]; result = self->cachedLocalHost6; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, AutoreleasedBlock(block)); - + return result; } - (uint16_t)localPort_IPv6 { __block uint16_t result = 0; - + dispatch_block_t block = ^{ - + [self maybeUpdateCachedLocalAddress6Info]; result = self->cachedLocalPort6; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, AutoreleasedBlock(block)); - + return result; } - (void)maybeUpdateCachedConnectedAddressInfo { NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - + if (cachedConnectedAddress || (flags & kDidConnect) == 0) { return; } - + NSData *data = nil; NSString *host = nil; uint16_t port = 0; int family = AF_UNSPEC; - + if (socket4FD != SOCKET_NULL) { struct sockaddr_in sockaddr4; socklen_t sockaddr4len = sizeof(sockaddr4); - + if (getpeername(socket4FD, (struct sockaddr *)&sockaddr4, &sockaddr4len) == 0) { data = [NSData dataWithBytes:&sockaddr4 length:sockaddr4len]; @@ -2613,7 +2616,7 @@ - (void)maybeUpdateCachedConnectedAddressInfo { struct sockaddr_in6 sockaddr6; socklen_t sockaddr6len = sizeof(sockaddr6); - + if (getpeername(socket6FD, (struct sockaddr *)&sockaddr6, &sockaddr6len) == 0) { data = [NSData dataWithBytes:&sockaddr6 length:sockaddr6len]; @@ -2626,8 +2629,8 @@ - (void)maybeUpdateCachedConnectedAddressInfo LogWarn(@"Error in getpeername: %@", [self errnoError]); } } - - + + cachedConnectedAddress = data; cachedConnectedHost = host; cachedConnectedPort = port; @@ -2637,96 +2640,96 @@ - (void)maybeUpdateCachedConnectedAddressInfo - (NSData *)connectedAddress { __block NSData *result = nil; - + dispatch_block_t block = ^{ - + [self maybeUpdateCachedConnectedAddressInfo]; result = self->cachedConnectedAddress; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, AutoreleasedBlock(block)); - + return result; } - (NSString *)connectedHost { __block NSString *result = nil; - + dispatch_block_t block = ^{ - + [self maybeUpdateCachedConnectedAddressInfo]; result = self->cachedConnectedHost; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, AutoreleasedBlock(block)); - + return result; } - (uint16_t)connectedPort { __block uint16_t result = 0; - + dispatch_block_t block = ^{ - + [self maybeUpdateCachedConnectedAddressInfo]; result = self->cachedConnectedPort; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, AutoreleasedBlock(block)); - + return result; } - (BOOL)isConnected { __block BOOL result = NO; - + dispatch_block_t block = ^{ result = (self->flags & kDidConnect) ? YES : NO; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); - + return result; } - (BOOL)isClosed { __block BOOL result = YES; - + dispatch_block_t block = ^{ - + result = (self->flags & kDidCreateSockets) ? NO : YES; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); - + return result; } - (BOOL)isIPv4 { __block BOOL result = NO; - + dispatch_block_t block = ^{ - + if (self->flags & kDidCreateSockets) { result = (self->socket4FD != SOCKET_NULL); @@ -2736,21 +2739,21 @@ - (BOOL)isIPv4 result = [self isIPv4Enabled]; } }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); - + return result; } - (BOOL)isIPv6 { __block BOOL result = NO; - + dispatch_block_t block = ^{ - + if (self->flags & kDidCreateSockets) { result = (self->socket6FD != SOCKET_NULL); @@ -2760,12 +2763,12 @@ - (BOOL)isIPv6 result = [self isIPv6Enabled]; } }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); - + return result; } @@ -2783,7 +2786,7 @@ - (BOOL)preBind:(NSError **)errPtr { return NO; } - + if (flags & kDidBind) { if (errPtr) @@ -2793,7 +2796,7 @@ - (BOOL)preBind:(NSError **)errPtr } return NO; } - + if ((flags & kConnecting) || (flags & kDidConnect)) { if (errPtr) @@ -2803,10 +2806,10 @@ - (BOOL)preBind:(NSError **)errPtr } return NO; } - + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; - + if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled { if (errPtr) @@ -2816,7 +2819,7 @@ - (BOOL)preBind:(NSError **)errPtr } return NO; } - + return YES; } @@ -2829,57 +2832,57 @@ - (BOOL)bindToPort:(uint16_t)port interface:(NSString *)interface error:(NSError { __block BOOL result = NO; __block NSError *err = nil; - + dispatch_block_t block = ^{ @autoreleasepool { - + // Run through sanity checks - + if (![self preBind:&err]) { return_from_block; } - + // Check the given interface - + NSData *interface4 = nil; NSData *interface6 = nil; - + [self convertIntefaceDescription:interface port:port intoAddress4:&interface4 address6:&interface6]; - + if ((interface4 == nil) && (interface6 == nil)) { NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; err = [self badParamError:msg]; - + return_from_block; } - + BOOL isIPv4Disabled = (self->config & kIPv4Disabled) ? YES : NO; BOOL isIPv6Disabled = (self->config & kIPv6Disabled) ? YES : NO; - + if (isIPv4Disabled && (interface6 == nil)) { NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6."; err = [self badParamError:msg]; - + return_from_block; } - + if (isIPv6Disabled && (interface4 == nil)) { NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4."; err = [self badParamError:msg]; - + return_from_block; } - + // Determine protocol(s) - + BOOL useIPv4 = !isIPv4Disabled && (interface4 != nil); BOOL useIPv6 = !isIPv6Disabled && (interface6 != nil); - + // Create the socket(s) if needed - + if ((self->flags & kDidCreateSockets) == 0) { if (![self createSocket4:useIPv4 socket6:useIPv6 error:&err]) @@ -2887,61 +2890,61 @@ - (BOOL)bindToPort:(uint16_t)port interface:(NSString *)interface error:(NSError return_from_block; } } - + // Bind the socket(s) - + LogVerbose(@"Binding socket to port(%hu) interface(%@)", port, interface); - + if (useIPv4) { int status = bind(self->socket4FD, (const struct sockaddr *)[interface4 bytes], (socklen_t)[interface4 length]); if (status == -1) { [self closeSockets]; - + NSString *reason = @"Error in bind() function"; err = [self errnoErrorWithReason:reason]; - + return_from_block; } } - + if (useIPv6) { int status = bind(self->socket6FD, (const struct sockaddr *)[interface6 bytes], (socklen_t)[interface6 length]); if (status == -1) { [self closeSockets]; - + NSString *reason = @"Error in bind() function"; err = [self errnoErrorWithReason:reason]; - + return_from_block; } } - + // Update flags - + self->flags |= kDidBind; - + if (!useIPv4) self->flags |= kIPv4Deactivated; if (!useIPv6) self->flags |= kIPv6Deactivated; - + result = YES; - + }}; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); - + if (err) LogError(@"Error binding to port/interface: %@", err); - + if (errPtr) *errPtr = err; - + return result; } @@ -2949,57 +2952,57 @@ - (BOOL)bindToAddress:(NSData *)localAddr error:(NSError **)errPtr { __block BOOL result = NO; __block NSError *err = nil; - + dispatch_block_t block = ^{ @autoreleasepool { - + // Run through sanity checks - + if (![self preBind:&err]) { return_from_block; } - + // Check the given address - + int addressFamily = [[self class] familyFromAddress:localAddr]; - + if (addressFamily == AF_UNSPEC) { NSString *msg = @"A valid IPv4 or IPv6 address was not given"; err = [self badParamError:msg]; - + return_from_block; } - + NSData *localAddr4 = (addressFamily == AF_INET) ? localAddr : nil; NSData *localAddr6 = (addressFamily == AF_INET6) ? localAddr : nil; - + BOOL isIPv4Disabled = (self->config & kIPv4Disabled) ? YES : NO; BOOL isIPv6Disabled = (self->config & kIPv6Disabled) ? YES : NO; - + if (isIPv4Disabled && localAddr4) { NSString *msg = @"IPv4 has been disabled and an IPv4 address was passed."; err = [self badParamError:msg]; - + return_from_block; } - + if (isIPv6Disabled && localAddr6) { NSString *msg = @"IPv6 has been disabled and an IPv6 address was passed."; err = [self badParamError:msg]; - + return_from_block; } - + // Determine protocol(s) - + BOOL useIPv4 = !isIPv4Disabled && (localAddr4 != nil); BOOL useIPv6 = !isIPv6Disabled && (localAddr6 != nil); - + // Create the socket(s) if needed - + if ((self->flags & kDidCreateSockets) == 0) { if (![self createSocket4:useIPv4 socket6:useIPv6 error:&err]) @@ -3007,23 +3010,23 @@ - (BOOL)bindToAddress:(NSData *)localAddr error:(NSError **)errPtr return_from_block; } } - + // Bind the socket(s) - + if (useIPv4) { LogVerbose(@"Binding socket to address(%@:%hu)", [[self class] hostFromAddress:localAddr4], [[self class] portFromAddress:localAddr4]); - + int status = bind(self->socket4FD, (const struct sockaddr *)[localAddr4 bytes], (socklen_t)[localAddr4 length]); if (status == -1) { [self closeSockets]; - + NSString *reason = @"Error in bind() function"; err = [self errnoErrorWithReason:reason]; - + return_from_block; } } @@ -3032,41 +3035,41 @@ - (BOOL)bindToAddress:(NSData *)localAddr error:(NSError **)errPtr LogVerbose(@"Binding socket to address(%@:%hu)", [[self class] hostFromAddress:localAddr6], [[self class] portFromAddress:localAddr6]); - + int status = bind(self->socket6FD, (const struct sockaddr *)[localAddr6 bytes], (socklen_t)[localAddr6 length]); if (status == -1) { [self closeSockets]; - + NSString *reason = @"Error in bind() function"; err = [self errnoErrorWithReason:reason]; - + return_from_block; } } - + // Update flags - + self->flags |= kDidBind; - + if (!useIPv4) self->flags |= kIPv4Deactivated; if (!useIPv6) self->flags |= kIPv6Deactivated; - + result = YES; - + }}; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); - + if (err) LogError(@"Error binding to address: %@", err); - + if (errPtr) *errPtr = err; - + return result; } @@ -3084,7 +3087,7 @@ - (BOOL)preConnect:(NSError **)errPtr { return NO; } - + if ((flags & kConnecting) || (flags & kDidConnect)) { if (errPtr) @@ -3094,10 +3097,10 @@ - (BOOL)preConnect:(NSError **)errPtr } return NO; } - + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; - + if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled { if (errPtr) @@ -3107,7 +3110,7 @@ - (BOOL)preConnect:(NSError **)errPtr } return NO; } - + return YES; } @@ -3115,28 +3118,28 @@ - (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port error:(NSError **)e { __block BOOL result = NO; __block NSError *err = nil; - + dispatch_block_t block = ^{ @autoreleasepool { - + // Run through sanity checks. - + if (![self preConnect:&err]) { return_from_block; } - + // Check parameter(s) - + if (host == nil) { NSString *msg = @"The host param is nil. Should be domain name or IP address string."; err = [self badParamError:msg]; - + return_from_block; } - + // Create the socket(s) if needed - + if ((self->flags & kDidCreateSockets) == 0) { if (![self createSockets:&err]) @@ -3144,51 +3147,51 @@ - (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port error:(NSError **)e return_from_block; } } - + // Create special connect packet - + GCDAsyncUdpSpecialPacket *packet = [[GCDAsyncUdpSpecialPacket alloc] init]; packet->resolveInProgress = YES; - + // Start asynchronous DNS resolve for host:port on background queue - + LogVerbose(@"Dispatching DNS resolve for connect..."); - + [self asyncResolveHost:host port:port withCompletionBlock:^(NSArray *addresses, NSError *error) { - + // The asyncResolveHost:port:: method asynchronously dispatches a task onto the global concurrent queue, // and immediately returns. Once the async resolve task completes, // this block is executed on our socketQueue. - + packet->resolveInProgress = NO; - + packet->addresses = addresses; packet->error = error; - + [self maybeConnect]; }]; - + // Updates flags, add connect packet to send queue, and pump send queue - + self->flags |= kConnecting; - + [self->sendQueue addObject:packet]; [self maybeDequeueSend]; - + result = YES; }}; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); - + if (err) LogError(@"Error connecting to host/port: %@", err); - + if (errPtr) *errPtr = err; - + return result; } @@ -3196,28 +3199,28 @@ - (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr { __block BOOL result = NO; __block NSError *err = nil; - + dispatch_block_t block = ^{ @autoreleasepool { - + // Run through sanity checks. - + if (![self preConnect:&err]) { return_from_block; } - + // Check parameter(s) - + if (remoteAddr == nil) { NSString *msg = @"The address param is nil. Should be a valid address."; err = [self badParamError:msg]; - + return_from_block; } - + // Create the socket(s) if needed - + if ((self->flags & kDidCreateSockets) == 0) { if (![self createSockets:&err]) @@ -3225,37 +3228,37 @@ - (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr return_from_block; } } - + // The remoteAddr parameter could be of type NSMutableData. // So we copy it to be safe. - + NSData *address = [remoteAddr copy]; NSArray *addresses = [NSArray arrayWithObject:address]; - + GCDAsyncUdpSpecialPacket *packet = [[GCDAsyncUdpSpecialPacket alloc] init]; packet->addresses = addresses; - + // Updates flags, add connect packet to send queue, and pump send queue - + self->flags |= kConnecting; - + [self->sendQueue addObject:packet]; [self maybeDequeueSend]; - + result = YES; }}; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); - + if (err) LogError(@"Error connecting to address: %@", err); - + if (errPtr) *errPtr = err; - + return result; } @@ -3263,14 +3266,14 @@ - (void)maybeConnect { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - - + + BOOL sendQueueReady = [currentSend isKindOfClass:[GCDAsyncUdpSpecialPacket class]]; - + if (sendQueueReady) { GCDAsyncUdpSpecialPacket *connectPacket = (GCDAsyncUdpSpecialPacket *)currentSend; - + if (connectPacket->resolveInProgress) { LogVerbose(@"Waiting for DNS resolve..."); @@ -3285,29 +3288,30 @@ - (void)maybeConnect { NSData *address = nil; NSError *error = nil; - + int addressFamily = [self getAddress:&address error:&error fromAddresses:connectPacket->addresses]; - + // Perform connect - + BOOL result = NO; - + switch (addressFamily) { case AF_INET : result = [self connectWithAddress4:address error:&error]; break; case AF_INET6 : result = [self connectWithAddress6:address error:&error]; break; + default: break; } - + if (result) { flags |= kDidBind; flags |= kDidConnect; - + cachedConnectedAddress = address; cachedConnectedHost = [[self class] hostFromAddress:address]; cachedConnectedPort = [[self class] portFromAddress:address]; cachedConnectedFamily = addressFamily; - + [self notifyDidConnectToAddress:address]; } else @@ -3315,9 +3319,9 @@ - (void)maybeConnect [self notifyDidNotConnect:error]; } } - + flags &= ~kConnecting; - + [self endCurrentSend]; [self maybeDequeueSend]; } @@ -3328,19 +3332,19 @@ - (BOOL)connectWithAddress4:(NSData *)address4 error:(NSError **)errPtr { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - + int status = connect(socket4FD, (const struct sockaddr *)[address4 bytes], (socklen_t)[address4 length]); if (status != 0) { if (errPtr) *errPtr = [self errnoErrorWithReason:@"Error in connect() function"]; - + return NO; } - + [self closeSocket6]; flags |= kIPv6Deactivated; - + return YES; } @@ -3348,19 +3352,19 @@ - (BOOL)connectWithAddress6:(NSData *)address6 error:(NSError **)errPtr { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - + int status = connect(socket6FD, (const struct sockaddr *)[address6 bytes], (socklen_t)[address6 length]); if (status != 0) { if (errPtr) *errPtr = [self errnoErrorWithReason:@"Error in connect() function"]; - + return NO; } - + [self closeSocket4]; flags |= kIPv4Deactivated; - + return YES; } @@ -3374,7 +3378,7 @@ - (BOOL)preJoin:(NSError **)errPtr { return NO; } - + if (!(flags & kDidBind)) { if (errPtr) @@ -3384,7 +3388,7 @@ - (BOOL)preJoin:(NSError **)errPtr } return NO; } - + if ((flags & kConnecting) || (flags & kDidConnect)) { if (errPtr) @@ -3394,7 +3398,7 @@ - (BOOL)preJoin:(NSError **)errPtr } return NO; } - + return YES; } @@ -3427,109 +3431,109 @@ - (BOOL)performMulticastRequest:(int)requestType { __block BOOL result = NO; __block NSError *err = nil; - + dispatch_block_t block = ^{ @autoreleasepool { - + // Run through sanity checks - + if (![self preJoin:&err]) { return_from_block; } - + // Convert group to address - + NSData *groupAddr4 = nil; NSData *groupAddr6 = nil; - + [self convertNumericHost:group port:0 intoAddress4:&groupAddr4 address6:&groupAddr6]; - + if ((groupAddr4 == nil) && (groupAddr6 == nil)) { NSString *msg = @"Unknown group. Specify valid group IP address."; err = [self badParamError:msg]; - + return_from_block; } - + // Convert interface to address - + NSData *interfaceAddr4 = nil; NSData *interfaceAddr6 = nil; - + [self convertIntefaceDescription:interface port:0 intoAddress4:&interfaceAddr4 address6:&interfaceAddr6]; - + if ((interfaceAddr4 == nil) && (interfaceAddr6 == nil)) { NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; err = [self badParamError:msg]; - + return_from_block; } - + // Perform join - + if ((self->socket4FD != SOCKET_NULL) && groupAddr4 && interfaceAddr4) { const struct sockaddr_in *nativeGroup = (const struct sockaddr_in *)[groupAddr4 bytes]; const struct sockaddr_in *nativeIface = (const struct sockaddr_in *)[interfaceAddr4 bytes]; - + struct ip_mreq imreq; imreq.imr_multiaddr = nativeGroup->sin_addr; imreq.imr_interface = nativeIface->sin_addr; - + int status = setsockopt(self->socket4FD, IPPROTO_IP, requestType, (const void *)&imreq, sizeof(imreq)); if (status != 0) { err = [self errnoErrorWithReason:@"Error in setsockopt() function"]; - + return_from_block; } - + // Using IPv4 only [self closeSocket6]; - + result = YES; } else if ((self->socket6FD != SOCKET_NULL) && groupAddr6 && interfaceAddr6) { const struct sockaddr_in6 *nativeGroup = (const struct sockaddr_in6 *)[groupAddr6 bytes]; - + struct ipv6_mreq imreq; imreq.ipv6mr_multiaddr = nativeGroup->sin6_addr; imreq.ipv6mr_interface = [self indexOfInterfaceAddr6:interfaceAddr6]; - + int status = setsockopt(self->socket6FD, IPPROTO_IPV6, requestType, (const void *)&imreq, sizeof(imreq)); if (status != 0) { err = [self errnoErrorWithReason:@"Error in setsockopt() function"]; - + return_from_block; } - + // Using IPv6 only [self closeSocket4]; - + result = YES; } else { NSString *msg = @"Socket, group, and interface do not have matching IP versions"; err = [self badParamError:msg]; - + return_from_block; } - + }}; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); - + if (errPtr) *errPtr = err; - + return result; } @@ -3552,7 +3556,7 @@ - (BOOL)sendIPv4MulticastOnInterface:(NSString*)interface error:(NSError **)errP return_from_block; } } - + // Convert interface to address NSData *interfaceAddr4 = nil; @@ -3577,7 +3581,7 @@ - (BOOL)sendIPv4MulticastOnInterface:(NSString*)interface error:(NSError **)errP result = YES; } } - + }}; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) @@ -3610,7 +3614,7 @@ - (BOOL)sendIPv6MulticastOnInterface:(NSString*)interface error:(NSError **)errP return_from_block; } } - + // Convert interface to address NSData *interfaceAddr4 = nil; @@ -3634,7 +3638,7 @@ - (BOOL)sendIPv6MulticastOnInterface:(NSString*)interface error:(NSError **)errP } result = YES; } - + }}; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) @@ -3656,14 +3660,14 @@ - (BOOL)enableReusePort:(BOOL)flag error:(NSError **)errPtr { __block BOOL result = NO; __block NSError *err = nil; - + dispatch_block_t block = ^{ @autoreleasepool { - + if (![self preOp:&err]) { return_from_block; } - + if ((self->flags & kDidCreateSockets) == 0) { if (![self createSockets:&err]) @@ -3671,44 +3675,44 @@ - (BOOL)enableReusePort:(BOOL)flag error:(NSError **)errPtr return_from_block; } } - + int value = flag ? 1 : 0; if (self->socket4FD != SOCKET_NULL) { int error = setsockopt(self->socket4FD, SOL_SOCKET, SO_REUSEPORT, (const void *)&value, sizeof(value)); - + if (error) { err = [self errnoErrorWithReason:@"Error in setsockopt() function"]; - + return_from_block; } result = YES; } - + if (self->socket6FD != SOCKET_NULL) { int error = setsockopt(self->socket6FD, SOL_SOCKET, SO_REUSEPORT, (const void *)&value, sizeof(value)); - + if (error) { err = [self errnoErrorWithReason:@"Error in setsockopt() function"]; - + return_from_block; } result = YES; } - + }}; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); - + if (errPtr) *errPtr = err; - + return result; } @@ -3720,14 +3724,14 @@ - (BOOL)enableBroadcast:(BOOL)flag error:(NSError **)errPtr { __block BOOL result = NO; __block NSError *err = nil; - + dispatch_block_t block = ^{ @autoreleasepool { - + if (![self preOp:&err]) { return_from_block; } - + if ((self->flags & kDidCreateSockets) == 0) { if (![self createSockets:&err]) @@ -3735,34 +3739,34 @@ - (BOOL)enableBroadcast:(BOOL)flag error:(NSError **)errPtr return_from_block; } } - + if (self->socket4FD != SOCKET_NULL) { int value = flag ? 1 : 0; int error = setsockopt(self->socket4FD, SOL_SOCKET, SO_BROADCAST, (const void *)&value, sizeof(value)); - + if (error) { err = [self errnoErrorWithReason:@"Error in setsockopt() function"]; - + return_from_block; } result = YES; } - + // IPv6 does not implement broadcast, the ability to send a packet to all hosts on the attached link. // The same effect can be achieved by sending a packet to the link-local all hosts multicast group. - + }}; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); - + if (errPtr) *errPtr = err; - + return result; } @@ -3778,23 +3782,23 @@ - (void)sendData:(NSData *)data withTag:(long)tag - (void)sendData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag { LogTrace(); - + if ([data length] == 0) { LogWarn(@"Ignoring attempt to send nil/empty data."); return; } - - - + + + GCDAsyncUdpSendPacket *packet = [[GCDAsyncUdpSendPacket alloc] initWithData:data timeout:timeout tag:tag]; - + dispatch_async(socketQueue, ^{ @autoreleasepool { - + [self->sendQueue addObject:packet]; [self maybeDequeueSend]; }}); - + } - (void)sendData:(NSData *)data @@ -3804,59 +3808,59 @@ - (void)sendData:(NSData *)data tag:(long)tag { LogTrace(); - + if ([data length] == 0) { LogWarn(@"Ignoring attempt to send nil/empty data."); return; } - + GCDAsyncUdpSendPacket *packet = [[GCDAsyncUdpSendPacket alloc] initWithData:data timeout:timeout tag:tag]; packet->resolveInProgress = YES; - + [self asyncResolveHost:host port:port withCompletionBlock:^(NSArray *addresses, NSError *error) { - + // The asyncResolveHost:port:: method asynchronously dispatches a task onto the global concurrent queue, // and immediately returns. Once the async resolve task completes, // this block is executed on our socketQueue. - + packet->resolveInProgress = NO; - + packet->resolvedAddresses = addresses; packet->resolveError = error; - + if (packet == self->currentSend) { LogVerbose(@"currentSend - address resolved"); [self doPreSend]; } }]; - + dispatch_async(socketQueue, ^{ @autoreleasepool { - + [self->sendQueue addObject:packet]; [self maybeDequeueSend]; - + }}); - + } - (void)sendData:(NSData *)data toAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout tag:(long)tag { LogTrace(); - + if ([data length] == 0) { LogWarn(@"Ignoring attempt to send nil/empty data."); return; } - + GCDAsyncUdpSendPacket *packet = [[GCDAsyncUdpSendPacket alloc] initWithData:data timeout:timeout tag:tag]; packet->addressFamily = [GCDAsyncUdpSocket familyFromAddress:remoteAddr]; packet->address = remoteAddr; - + dispatch_async(socketQueue, ^{ @autoreleasepool { - + [self->sendQueue addObject:packet]; [self maybeDequeueSend]; }}); @@ -3873,29 +3877,29 @@ - (void)setSendFilter:(GCDAsyncUdpSocketSendFilterBlock)filterBlock { GCDAsyncUdpSocketSendFilterBlock newFilterBlock = NULL; dispatch_queue_t newFilterQueue = NULL; - + if (filterBlock) { NSAssert(filterQueue, @"Must provide a dispatch_queue in which to run the filter block."); - + newFilterBlock = [filterBlock copy]; newFilterQueue = filterQueue; #if !OS_OBJECT_USE_OBJC dispatch_retain(newFilterQueue); #endif } - + dispatch_block_t block = ^{ - + #if !OS_OBJECT_USE_OBJC if (self->sendFilterQueue) dispatch_release(self->sendFilterQueue); #endif - + self->sendFilterBlock = newFilterBlock; self->sendFilterQueue = newFilterQueue; self->sendFilterAsync = isAsynchronous; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else @@ -3906,7 +3910,7 @@ - (void)maybeDequeueSend { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - + // If we don't have a send operation already in progress if (currentSend == nil) { @@ -3920,38 +3924,38 @@ - (void)maybeDequeueSend return; } } - + while ([sendQueue count] > 0) { // Dequeue the next object in the queue currentSend = [sendQueue objectAtIndex:0]; [sendQueue removeObjectAtIndex:0]; - + if ([currentSend isKindOfClass:[GCDAsyncUdpSpecialPacket class]]) { [self maybeConnect]; - + return; // The maybeConnect method, if it connects, will invoke this method again } else if (currentSend->resolveError) { // Notify delegate [self notifyDidNotSendDataWithTag:currentSend->tag dueToError:currentSend->resolveError]; - + // Clear currentSend currentSend = nil; - + continue; } else { // Start preprocessing checks on the send packet [self doPreSend]; - + break; } } - + if ((currentSend == nil) && (flags & kCloseAfterSends)) { [self closeWithError:nil]; @@ -3963,24 +3967,24 @@ - (void)maybeDequeueSend * This method is called after a sendPacket has been dequeued. * It performs various preprocessing checks on the packet, * and queries the sendFilter (if set) to determine if the packet can be sent. - * + * * If the packet passes all checks, it will be passed on to the doSend method. **/ - (void)doPreSend { LogTrace(); - - // + + // // 1. Check for problems with send packet - // - + // + BOOL waitingForResolve = NO; NSError *error = nil; - + if (flags & kDidConnect) { // Connected socket - + if (currentSend->resolveInProgress || currentSend->resolvedAddresses || currentSend->resolveError) { NSString *msg = @"Cannot specify destination of packet for connected socket"; @@ -3995,7 +3999,7 @@ - (void)doPreSend else { // Non-Connected socket - + if (currentSend->resolveInProgress) { // We're waiting for the packet's destination to be resolved. @@ -4015,67 +4019,67 @@ - (void)doPreSend else { // Pick the proper address to use (out of possibly several resolved addresses) - + NSData *address = nil; int addressFamily = AF_UNSPEC; - + addressFamily = [self getAddress:&address error:&error fromAddresses:currentSend->resolvedAddresses]; - + currentSend->address = address; currentSend->addressFamily = addressFamily; } } } - + if (waitingForResolve) { // We're waiting for the packet's destination to be resolved. - + LogVerbose(@"currentSend - waiting for address resolve"); - + if (flags & kSock4CanAcceptBytes) { [self suspendSend4Source]; } if (flags & kSock6CanAcceptBytes) { [self suspendSend6Source]; } - + return; } - + if (error) { // Unable to send packet due to some error. // Notify delegate and move on. - + [self notifyDidNotSendDataWithTag:currentSend->tag dueToError:error]; [self endCurrentSend]; [self maybeDequeueSend]; - + return; } - - // + + // // 2. Query sendFilter (if applicable) - // - + // + if (sendFilterBlock && sendFilterQueue) { // Query sendFilter - + if (sendFilterAsync) { // Scenario 1 of 3 - Need to asynchronously query sendFilter - + currentSend->filterInProgress = YES; GCDAsyncUdpSendPacket *sendPacket = currentSend; - + dispatch_async(sendFilterQueue, ^{ @autoreleasepool { - + BOOL allowed = self->sendFilterBlock(sendPacket->buffer, sendPacket->address, sendPacket->tag); - + dispatch_async(self->socketQueue, ^{ @autoreleasepool { - + sendPacket->filterInProgress = NO; if (sendPacket == self->currentSend) { @@ -4086,7 +4090,7 @@ - (void)doPreSend else { LogVerbose(@"currentSend - silently dropped by sendFilter"); - + [self notifyDidSendDataWithTag:self->currentSend->tag]; [self endCurrentSend]; [self maybeDequeueSend]; @@ -4098,14 +4102,14 @@ - (void)doPreSend else { // Scenario 2 of 3 - Need to synchronously query sendFilter - + __block BOOL allowed = YES; - + dispatch_sync(sendFilterQueue, ^{ @autoreleasepool { - + allowed = self->sendFilterBlock(self->currentSend->buffer, self->currentSend->address, self->currentSend->tag); }}); - + if (allowed) { [self doSend]; @@ -4113,7 +4117,7 @@ - (void)doPreSend else { LogVerbose(@"currentSend - silently dropped by sendFilter"); - + [self notifyDidSendDataWithTag:currentSend->tag]; [self endCurrentSend]; [self maybeDequeueSend]; @@ -4123,32 +4127,32 @@ - (void)doPreSend else // if (!sendFilterBlock || !sendFilterQueue) { // Scenario 3 of 3 - No sendFilter. Just go straight into sending. - + [self doSend]; } } /** * This method performs the actual sending of data in the currentSend packet. - * It should only be called if the + * It should only be called if the **/ - (void)doSend { LogTrace(); - + NSAssert(currentSend != nil, @"Invalid logic"); - + // Perform the actual send - + ssize_t result = 0; - + if (flags & kDidConnect) { // Connected socket - + const void *buffer = [currentSend->buffer bytes]; size_t length = (size_t)[currentSend->buffer length]; - + if (currentSend->addressFamily == AF_INET) { result = send(socket4FD, buffer, length, 0); @@ -4163,13 +4167,13 @@ - (void)doSend else { // Non-Connected socket - + const void *buffer = [currentSend->buffer bytes]; size_t length = (size_t)[currentSend->buffer length]; - + const void *dst = [currentSend->address bytes]; socklen_t dstSize = (socklen_t)[currentSend->address length]; - + if (currentSend->addressFamily == AF_INET) { result = sendto(socket4FD, buffer, length, 0, dst, dstSize); @@ -4181,24 +4185,24 @@ - (void)doSend LogVerbose(@"sendto(socket6FD) = %d", result); } } - + // If the socket wasn't bound before, it is now - + if ((flags & kDidBind) == 0) { flags |= kDidBind; } - + // Check the results. - // + // // From the send() & sendto() manpage: - // + // // Upon successful completion, the number of bytes which were sent is returned. // Otherwise, -1 is returned and the global variable errno is set to indicate the error. - + BOOL waitingForSocket = NO; NSError *socketError = nil; - + if (result == 0) { waitingForSocket = YES; @@ -4210,26 +4214,26 @@ - (void)doSend else socketError = [self errnoErrorWithReason:@"Error in send() function."]; } - + if (waitingForSocket) { // Not enough room in the underlying OS socket send buffer. // Wait for a notification of available space. - + LogVerbose(@"currentSend - waiting for socket"); - + if (!(flags & kSock4CanAcceptBytes)) { [self resumeSend4Source]; } if (!(flags & kSock6CanAcceptBytes)) { [self resumeSend6Source]; } - + if ((sendTimer == NULL) && (currentSend->timeout >= 0.0)) { // Unable to send packet right away. // Start timer to timeout the send operation. - + [self setupSendTimerWithTimeout:currentSend->timeout]; } } @@ -4258,7 +4262,7 @@ - (void)endCurrentSend #endif sendTimer = NULL; } - + currentSend = nil; } @@ -4268,7 +4272,7 @@ - (void)endCurrentSend - (void)doSendTimeout { LogTrace(); - + [self notifyDidNotSendDataWithTag:currentSend->tag dueToError:[self sendTimeoutError]]; [self endCurrentSend]; [self maybeDequeueSend]; @@ -4282,18 +4286,18 @@ - (void)setupSendTimerWithTimeout:(NSTimeInterval)timeout { NSAssert(sendTimer == NULL, @"Invalid logic"); NSAssert(timeout >= 0.0, @"Invalid logic"); - + LogTrace(); - + sendTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue); - + dispatch_source_set_event_handler(sendTimer, ^{ @autoreleasepool { - + [self doSendTimeout]; }}); - + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)); - + dispatch_source_set_timer(sendTimer, tt, DISPATCH_TIME_FOREVER, 0); dispatch_resume(sendTimer); } @@ -4305,104 +4309,104 @@ - (void)setupSendTimerWithTimeout:(NSTimeInterval)timeout - (BOOL)receiveOnce:(NSError **)errPtr { LogTrace(); - + __block BOOL result = NO; __block NSError *err = nil; - + dispatch_block_t block = ^{ - + if ((self->flags & kReceiveOnce) == 0) { if ((self->flags & kDidCreateSockets) == 0) { NSString *msg = @"Must bind socket before you can receive data. " @"You can do this explicitly via bind, or implicitly via connect or by sending data."; - + err = [self badConfigError:msg]; return_from_block; } - + self->flags |= kReceiveOnce; // Enable self->flags &= ~kReceiveContinuous; // Disable - + dispatch_async(self->socketQueue, ^{ @autoreleasepool { - + [self doReceive]; }}); } - + result = YES; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); - + if (err) LogError(@"Error in beginReceiving: %@", err); - + if (errPtr) *errPtr = err; - + return result; } - (BOOL)beginReceiving:(NSError **)errPtr { LogTrace(); - + __block BOOL result = NO; __block NSError *err = nil; - + dispatch_block_t block = ^{ - + if ((self->flags & kReceiveContinuous) == 0) { if ((self->flags & kDidCreateSockets) == 0) { NSString *msg = @"Must bind socket before you can receive data. " @"You can do this explicitly via bind, or implicitly via connect or by sending data."; - + err = [self badConfigError:msg]; return_from_block; } - + self->flags |= kReceiveContinuous; // Enable self->flags &= ~kReceiveOnce; // Disable - + dispatch_async(self->socketQueue, ^{ @autoreleasepool { - + [self doReceive]; }}); } - + result = YES; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); - + if (err) LogError(@"Error in beginReceiving: %@", err); - + if (errPtr) *errPtr = err; - + return result; } - (void)pauseReceiving { LogTrace(); - + dispatch_block_t block = ^{ - + self->flags &= ~kReceiveOnce; // Disable self->flags &= ~kReceiveContinuous; // Disable - + if (self->socket4FDBytesAvailable > 0) { [self suspendReceive4Source]; } @@ -4410,7 +4414,7 @@ - (void)pauseReceiving [self suspendReceive6Source]; } }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else @@ -4428,29 +4432,29 @@ - (void)setReceiveFilter:(GCDAsyncUdpSocketReceiveFilterBlock)filterBlock { GCDAsyncUdpSocketReceiveFilterBlock newFilterBlock = NULL; dispatch_queue_t newFilterQueue = NULL; - + if (filterBlock) { NSAssert(filterQueue, @"Must provide a dispatch_queue in which to run the filter block."); - + newFilterBlock = [filterBlock copy]; newFilterQueue = filterQueue; #if !OS_OBJECT_USE_OBJC dispatch_retain(newFilterQueue); #endif } - + dispatch_block_t block = ^{ - + #if !OS_OBJECT_USE_OBJC if (self->receiveFilterQueue) dispatch_release(self->receiveFilterQueue); #endif - + self->receiveFilterBlock = newFilterBlock; self->receiveFilterQueue = newFilterQueue; self->receiveFilterAsync = isAsynchronous; }; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else @@ -4460,71 +4464,71 @@ - (void)setReceiveFilter:(GCDAsyncUdpSocketReceiveFilterBlock)filterBlock - (void)doReceive { LogTrace(); - + if ((flags & (kReceiveOnce | kReceiveContinuous)) == 0) { LogVerbose(@"Receiving is paused..."); - + if (socket4FDBytesAvailable > 0) { [self suspendReceive4Source]; } if (socket6FDBytesAvailable > 0) { [self suspendReceive6Source]; } - + return; } - + if ((flags & kReceiveOnce) && (pendingFilterOperations > 0)) { LogVerbose(@"Receiving is temporarily paused (pending filter operations)..."); - + if (socket4FDBytesAvailable > 0) { [self suspendReceive4Source]; } if (socket6FDBytesAvailable > 0) { [self suspendReceive6Source]; } - + return; } - + if ((socket4FDBytesAvailable == 0) && (socket6FDBytesAvailable == 0)) { LogVerbose(@"No data available to receive..."); - + if (socket4FDBytesAvailable == 0) { [self resumeReceive4Source]; } if (socket6FDBytesAvailable == 0) { [self resumeReceive6Source]; } - + return; } - + // Figure out if we should receive on socket4 or socket6 - + BOOL doReceive4; - + if (flags & kDidConnect) { // Connected socket - + doReceive4 = (socket4FD != SOCKET_NULL); } else { // Non-Connected socket - + if (socket4FDBytesAvailable > 0) { if (socket6FDBytesAvailable > 0) { // Bytes available on socket4 & socket6 - + doReceive4 = (flags & kFlipFlop) ? YES : NO; - + flags ^= kFlipFlop; // flags = flags xor kFlipFlop; (toggle flip flop bit) } else { @@ -4537,42 +4541,42 @@ - (void)doReceive doReceive4 = NO; } } - + // Perform socket IO - + ssize_t result = 0; - + NSData *data = nil; NSData *addr4 = nil; NSData *addr6 = nil; - + if (doReceive4) { NSAssert(socket4FDBytesAvailable > 0, @"Invalid logic"); LogVerbose(@"Receiving on IPv4"); - + struct sockaddr_in sockaddr4; socklen_t sockaddr4len = sizeof(sockaddr4); - - // #222: GCD does not necessarily return the size of an entire UDP packet + + // #222: GCD does not necessarily return the size of an entire UDP packet // from dispatch_source_get_data(), so we must use the maximum packet size. size_t bufSize = max4ReceiveSize; void *buf = malloc(bufSize); - + result = recvfrom(socket4FD, buf, bufSize, 0, (struct sockaddr *)&sockaddr4, &sockaddr4len); LogVerbose(@"recvfrom(socket4FD) = %i", (int)result); - + if (result > 0) { if ((size_t)result >= socket4FDBytesAvailable) socket4FDBytesAvailable = 0; else socket4FDBytesAvailable -= result; - + if ((size_t)result != bufSize) { buf = realloc(buf, result); } - + data = [NSData dataWithBytesNoCopy:buf length:result freeWhenDone:YES]; addr4 = [NSData dataWithBytes:&sockaddr4 length:sockaddr4len]; } @@ -4587,29 +4591,29 @@ - (void)doReceive { NSAssert(socket6FDBytesAvailable > 0, @"Invalid logic"); LogVerbose(@"Receiving on IPv6"); - + struct sockaddr_in6 sockaddr6; socklen_t sockaddr6len = sizeof(sockaddr6); - - // #222: GCD does not necessarily return the size of an entire UDP packet + + // #222: GCD does not necessarily return the size of an entire UDP packet // from dispatch_source_get_data(), so we must use the maximum packet size. size_t bufSize = max6ReceiveSize; void *buf = malloc(bufSize); - + result = recvfrom(socket6FD, buf, bufSize, 0, (struct sockaddr *)&sockaddr6, &sockaddr6len); LogVerbose(@"recvfrom(socket6FD) -> %i", (int)result); - + if (result > 0) { if ((size_t)result >= socket6FDBytesAvailable) socket6FDBytesAvailable = 0; else socket6FDBytesAvailable -= result; - + if ((size_t)result != bufSize) { buf = realloc(buf, result); } - + data = [NSData dataWithBytesNoCopy:buf length:result freeWhenDone:YES]; addr6 = [NSData dataWithBytes:&sockaddr6 length:sockaddr6len]; } @@ -4620,14 +4624,14 @@ - (void)doReceive free(buf); } } - - + + BOOL waitingForSocket = NO; BOOL notifiedDelegate = NO; BOOL ignored = NO; - + NSError *socketError = nil; - + if (result == 0) { waitingForSocket = YES; @@ -4648,30 +4652,30 @@ - (void)doReceive if (addr6 && ![self isConnectedToAddress6:addr6]) ignored = YES; } - + NSData *addr = (addr4 != nil) ? addr4 : addr6; - + if (!ignored) { if (receiveFilterBlock && receiveFilterQueue) { // Run data through filter, and if approved, notify delegate - + __block id filterContext = nil; __block BOOL allowed = NO; - + if (receiveFilterAsync) { pendingFilterOperations++; dispatch_async(receiveFilterQueue, ^{ @autoreleasepool { - + allowed = self->receiveFilterBlock(data, addr, &filterContext); - + // Transition back to socketQueue to get the current delegate / delegateQueue dispatch_async(self->socketQueue, ^{ @autoreleasepool { - + self->pendingFilterOperations--; - + if (allowed) { [self notifyDidReceiveData:data fromAddress:addr withFilterContext:filterContext]; @@ -4680,7 +4684,7 @@ - (void)doReceive { LogVerbose(@"received packet silently dropped by receiveFilter"); } - + if (self->flags & kReceiveOnce) { if (allowed) @@ -4703,10 +4707,10 @@ - (void)doReceive else // if (!receiveFilterAsync) { dispatch_sync(receiveFilterQueue, ^{ @autoreleasepool { - + allowed = self->receiveFilterBlock(data, addr, &filterContext); }}); - + if (allowed) { [self notifyDidReceiveData:data fromAddress:addr withFilterContext:filterContext]; @@ -4726,11 +4730,11 @@ - (void)doReceive } } } - + if (waitingForSocket) { // Wait for a notification of available data. - + if (socket4FDBytesAvailable == 0) { [self resumeReceive4Source]; } @@ -4773,7 +4777,7 @@ - (void)doReceive - (void)doReceiveEOF { LogTrace(); - + [self closeWithError:[self socketClosedError]]; } @@ -4784,26 +4788,26 @@ - (void)doReceiveEOF - (void)closeWithError:(NSError *)error { LogVerbose(@"closeWithError: %@", error); - + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - + if (currentSend) [self endCurrentSend]; - + [sendQueue removeAllObjects]; - + // If a socket has been created, we should notify the delegate. BOOL shouldCallDelegate = (flags & kDidCreateSockets) ? YES : NO; - + // Close all sockets, send/receive sources, cfstreams, etc #if TARGET_OS_IPHONE [self removeStreamsFromRunLoop]; [self closeReadAndWriteStreams]; #endif [self closeSockets]; - + // Clear all flags (config remains as is) flags = 0; - + if (shouldCallDelegate) { [self notifyDidCloseWithError:error]; @@ -4813,12 +4817,12 @@ - (void)closeWithError:(NSError *)error - (void)close { LogTrace(); - + dispatch_block_t block = ^{ @autoreleasepool { - + [self closeWithError:nil]; }}; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else @@ -4828,17 +4832,17 @@ - (void)close - (void)closeAfterSending { LogTrace(); - + dispatch_block_t block = ^{ @autoreleasepool { - + self->flags |= kCloseAfterSends; - + if (self->currentSend == nil && [self->sendQueue count] == 0) { [self closeWithError:nil]; } }}; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else @@ -4860,7 +4864,7 @@ + (void)startListenerThreadIfNeeded { static dispatch_once_t predicate; dispatch_once(&predicate, ^{ - + listenerThread = [[NSThread alloc] initWithTarget:self selector:@selector(listenerThread:) object:nil]; @@ -4871,11 +4875,11 @@ + (void)startListenerThreadIfNeeded + (void)listenerThread:(id)unused { @autoreleasepool { - + [[NSThread currentThread] setName:GCDAsyncUdpSocketThreadName]; - + LogInfo(@"ListenerThread: Started"); - + // We can't run the run loop unless it has an associated input source or a timer. // So we'll just create a timer that will never fire - unless the server runs for a decades. [NSTimer scheduledTimerWithTimeInterval:[[NSDate distantFuture] timeIntervalSinceNow] @@ -4883,9 +4887,9 @@ + (void)listenerThread:(id)unused selector:@selector(ignore:) userInfo:nil repeats:YES]; - + [[NSRunLoop currentRunLoop] run]; - + LogInfo(@"ListenerThread: Stopped"); } } @@ -4894,18 +4898,18 @@ + (void)addStreamListener:(GCDAsyncUdpSocket *)asyncUdpSocket { LogTrace(); NSAssert([NSThread currentThread] == listenerThread, @"Invoked on wrong thread"); - + CFRunLoopRef runLoop = CFRunLoopGetCurrent(); - + if (asyncUdpSocket->readStream4) CFReadStreamScheduleWithRunLoop(asyncUdpSocket->readStream4, runLoop, kCFRunLoopDefaultMode); - + if (asyncUdpSocket->readStream6) CFReadStreamScheduleWithRunLoop(asyncUdpSocket->readStream6, runLoop, kCFRunLoopDefaultMode); - + if (asyncUdpSocket->writeStream4) CFWriteStreamScheduleWithRunLoop(asyncUdpSocket->writeStream4, runLoop, kCFRunLoopDefaultMode); - + if (asyncUdpSocket->writeStream6) CFWriteStreamScheduleWithRunLoop(asyncUdpSocket->writeStream6, runLoop, kCFRunLoopDefaultMode); } @@ -4914,18 +4918,18 @@ + (void)removeStreamListener:(GCDAsyncUdpSocket *)asyncUdpSocket { LogTrace(); NSAssert([NSThread currentThread] == listenerThread, @"Invoked on wrong thread"); - + CFRunLoopRef runLoop = CFRunLoopGetCurrent(); - + if (asyncUdpSocket->readStream4) CFReadStreamUnscheduleFromRunLoop(asyncUdpSocket->readStream4, runLoop, kCFRunLoopDefaultMode); - + if (asyncUdpSocket->readStream6) CFReadStreamUnscheduleFromRunLoop(asyncUdpSocket->readStream6, runLoop, kCFRunLoopDefaultMode); - + if (asyncUdpSocket->writeStream4) CFWriteStreamUnscheduleFromRunLoop(asyncUdpSocket->writeStream4, runLoop, kCFRunLoopDefaultMode); - + if (asyncUdpSocket->writeStream6) CFWriteStreamUnscheduleFromRunLoop(asyncUdpSocket->writeStream6, runLoop, kCFRunLoopDefaultMode); } @@ -4957,23 +4961,23 @@ static void CFReadStreamCallback(CFReadStreamRef stream, CFStreamEventType type, { error = [asyncUdpSocket socketClosedError]; } - + dispatch_async(asyncUdpSocket->socketQueue, ^{ @autoreleasepool { - + LogCVerbose(@"CFReadStreamCallback - %@", (type == kCFStreamEventErrorOccurred) ? @"Error" : @"EndEncountered"); - + if (stream != asyncUdpSocket->readStream4 && stream != asyncUdpSocket->readStream6 ) { LogCVerbose(@"CFReadStreamCallback - Ignored"); return_from_block; } - + [asyncUdpSocket closeWithError:error]; - + }}); - + break; } default: @@ -5012,23 +5016,23 @@ static void CFWriteStreamCallback(CFWriteStreamRef stream, CFStreamEventType typ { error = [asyncUdpSocket socketClosedError]; } - + dispatch_async(asyncUdpSocket->socketQueue, ^{ @autoreleasepool { - + LogCVerbose(@"CFWriteStreamCallback - %@", (type == kCFStreamEventErrorOccurred) ? @"Error" : @"EndEncountered"); - + if (stream != asyncUdpSocket->writeStream4 && stream != asyncUdpSocket->writeStream6 ) { LogCVerbose(@"CFWriteStreamCallback - Ignored"); return_from_block; } - + [asyncUdpSocket closeWithError:error]; - + }}); - + break; } default: @@ -5044,25 +5048,25 @@ - (BOOL)createReadAndWriteStreams:(NSError **)errPtr { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - + NSError *err = nil; - + if (readStream4 || writeStream4 || readStream6 || writeStream6) { // Streams already created return YES; } - + if (socket4FD == SOCKET_NULL && socket6FD == SOCKET_NULL) { err = [self otherError:@"Cannot create streams without a file descriptor"]; goto Failed; } - + // Create streams - + LogVerbose(@"Creating read and write stream(s)..."); - + if (socket4FD != SOCKET_NULL) { CFStreamCreatePairWithSocket(NULL, (CFSocketNativeHandle)socket4FD, &readStream4, &writeStream4); @@ -5072,7 +5076,7 @@ - (BOOL)createReadAndWriteStreams:(NSError **)errPtr goto Failed; } } - + if (socket6FD != SOCKET_NULL) { CFStreamCreatePairWithSocket(NULL, (CFSocketNativeHandle)socket6FD, &readStream6, &writeStream6); @@ -5082,17 +5086,17 @@ - (BOOL)createReadAndWriteStreams:(NSError **)errPtr goto Failed; } } - + // Ensure the CFStream's don't close our underlying socket - + CFReadStreamSetProperty(readStream4, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); CFWriteStreamSetProperty(writeStream4, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); - + CFReadStreamSetProperty(readStream6, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); CFWriteStreamSetProperty(writeStream6, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); - + return YES; - + Failed: if (readStream4) { @@ -5118,34 +5122,34 @@ - (BOOL)createReadAndWriteStreams:(NSError **)errPtr CFRelease(writeStream6); writeStream6 = NULL; } - + if (errPtr) *errPtr = err; - + return NO; } - (BOOL)registerForStreamCallbacks:(NSError **)errPtr { LogTrace(); - + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); NSAssert(readStream4 || writeStream4 || readStream6 || writeStream6, @"Read/Write streams are null"); - + NSError *err = nil; - + streamContext.version = 0; streamContext.info = (__bridge void *)self; streamContext.retain = nil; streamContext.release = nil; streamContext.copyDescription = nil; - + CFOptionFlags readStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; CFOptionFlags writeStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; - + // readStreamEvents |= (kCFStreamEventOpenCompleted | kCFStreamEventHasBytesAvailable); // writeStreamEvents |= (kCFStreamEventOpenCompleted | kCFStreamEventCanAcceptBytes); - + if (socket4FD != SOCKET_NULL) { if (readStream4 == NULL || writeStream4 == NULL) @@ -5153,17 +5157,17 @@ - (BOOL)registerForStreamCallbacks:(NSError **)errPtr err = [self otherError:@"Read/Write stream4 is null"]; goto Failed; } - + BOOL r1 = CFReadStreamSetClient(readStream4, readStreamEvents, &CFReadStreamCallback, &streamContext); BOOL r2 = CFWriteStreamSetClient(writeStream4, writeStreamEvents, &CFWriteStreamCallback, &streamContext); - + if (!r1 || !r2) { err = [self otherError:@"Error in CFStreamSetClient(), [IPv4]"]; goto Failed; } } - + if (socket6FD != SOCKET_NULL) { if (readStream6 == NULL || writeStream6 == NULL) @@ -5171,19 +5175,19 @@ - (BOOL)registerForStreamCallbacks:(NSError **)errPtr err = [self otherError:@"Read/Write stream6 is null"]; goto Failed; } - + BOOL r1 = CFReadStreamSetClient(readStream6, readStreamEvents, &CFReadStreamCallback, &streamContext); BOOL r2 = CFWriteStreamSetClient(writeStream6, writeStreamEvents, &CFWriteStreamCallback, &streamContext); - + if (!r1 || !r2) { err = [self otherError:@"Error in CFStreamSetClient() [IPv6]"]; goto Failed; } } - + return YES; - + Failed: if (readStream4) { CFReadStreamSetClient(readStream4, kCFStreamEventNone, NULL, NULL); @@ -5197,7 +5201,7 @@ - (BOOL)registerForStreamCallbacks:(NSError **)errPtr if (writeStream6) { CFWriteStreamSetClient(writeStream6, kCFStreamEventNone, NULL, NULL); } - + if (errPtr) *errPtr = err; return NO; } @@ -5205,10 +5209,10 @@ - (BOOL)registerForStreamCallbacks:(NSError **)errPtr - (BOOL)addStreamsToRunLoop:(NSError **)errPtr { LogTrace(); - + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); NSAssert(readStream4 || writeStream4 || readStream6 || writeStream6, @"Read/Write streams are null"); - + if (!(flags & kAddedStreamListener)) { [[self class] startListenerThreadIfNeeded]; @@ -5216,48 +5220,48 @@ - (BOOL)addStreamsToRunLoop:(NSError **)errPtr onThread:listenerThread withObject:self waitUntilDone:YES]; - + flags |= kAddedStreamListener; } - + return YES; } - (BOOL)openStreams:(NSError **)errPtr { LogTrace(); - + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); NSAssert(readStream4 || writeStream4 || readStream6 || writeStream6, @"Read/Write streams are null"); - + NSError *err = nil; - + if (socket4FD != SOCKET_NULL) { BOOL r1 = CFReadStreamOpen(readStream4); BOOL r2 = CFWriteStreamOpen(writeStream4); - + if (!r1 || !r2) { err = [self otherError:@"Error in CFStreamOpen() [IPv4]"]; goto Failed; } } - + if (socket6FD != SOCKET_NULL) { BOOL r1 = CFReadStreamOpen(readStream6); BOOL r2 = CFWriteStreamOpen(writeStream6); - + if (!r1 || !r2) { err = [self otherError:@"Error in CFStreamOpen() [IPv6]"]; goto Failed; } } - + return YES; - + Failed: if (errPtr) *errPtr = err; return NO; @@ -5267,14 +5271,14 @@ - (void)removeStreamsFromRunLoop { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - + if (flags & kAddedStreamListener) { [[self class] performSelector:@selector(removeStreamListener:) onThread:listenerThread withObject:self waitUntilDone:YES]; - + flags &= ~kAddedStreamListener; } } @@ -5282,7 +5286,7 @@ - (void)removeStreamsFromRunLoop - (void)closeReadAndWriteStreams { LogTrace(); - + if (readStream4) { CFReadStreamSetClient(readStream4, kCFStreamEventNone, NULL, NULL); @@ -5319,16 +5323,16 @@ - (void)closeReadAndWriteStreams - (void)applicationWillEnterForeground:(NSNotification *)notification { LogTrace(); - + // If the application was backgrounded, then iOS may have shut down our sockets. // So we take a quick look to see if any of them received an EOF. - + dispatch_block_t block = ^{ @autoreleasepool { - + [self resumeReceive4Source]; [self resumeReceive6Source]; }}; - + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else @@ -5373,7 +5377,7 @@ - (int)socketFD THIS_FILE, THIS_METHOD); return SOCKET_NULL; } - + if (socket4FD != SOCKET_NULL) return socket4FD; else @@ -5388,7 +5392,7 @@ - (int)socket4FD THIS_FILE, THIS_METHOD); return SOCKET_NULL; } - + return socket4FD; } @@ -5400,7 +5404,7 @@ - (int)socket6FD THIS_FILE, THIS_METHOD); return SOCKET_NULL; } - + return socket6FD; } @@ -5414,16 +5418,16 @@ - (CFReadStreamRef)readStream THIS_FILE, THIS_METHOD); return NULL; } - + NSError *err = nil; if (![self createReadAndWriteStreams:&err]) { LogError(@"Error creating CFStream(s): %@", err); return NULL; } - + // Todo... - + if (readStream4) return readStream4; else @@ -5438,14 +5442,14 @@ - (CFWriteStreamRef)writeStream THIS_FILE, THIS_METHOD); return NULL; } - + NSError *err = nil; if (![self createReadAndWriteStreams:&err]) { LogError(@"Error creating CFStream(s): %@", err); return NULL; } - + if (writeStream4) return writeStream4; else @@ -5460,54 +5464,54 @@ - (BOOL)enableBackgroundingOnSockets THIS_FILE, THIS_METHOD); return NO; } - + // Why is this commented out? // See comments below. - + // NSError *err = nil; // if (![self createReadAndWriteStreams:&err]) // { // LogError(@"Error creating CFStream(s): %@", err); // return NO; // } -// +// // LogVerbose(@"Enabling backgrouding on socket"); -// +// // BOOL r1, r2; -// +// // if (readStream4 && writeStream4) // { // r1 = CFReadStreamSetProperty(readStream4, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); // r2 = CFWriteStreamSetProperty(writeStream4, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); -// +// // if (!r1 || !r2) // { // LogError(@"Error setting voip type (IPv4)"); // return NO; // } // } -// +// // if (readStream6 && writeStream6) // { // r1 = CFReadStreamSetProperty(readStream6, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); // r2 = CFWriteStreamSetProperty(writeStream6, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); -// +// // if (!r1 || !r2) // { // LogError(@"Error setting voip type (IPv6)"); // return NO; // } // } -// +// // return YES; - + // The above code will actually appear to work. // The methods will return YES, and everything will appear fine. - // + // // One tiny problem: the sockets will still get closed when the app gets backgrounded. - // + // // Apple does not officially support backgrounding UDP sockets. - + return NO; } @@ -5520,24 +5524,24 @@ - (BOOL)enableBackgroundingOnSockets + (NSString *)hostFromSockaddr4:(const struct sockaddr_in *)pSockaddr4 { char addrBuf[INET_ADDRSTRLEN]; - + if (inet_ntop(AF_INET, &pSockaddr4->sin_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL) { addrBuf[0] = '\0'; } - + return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; } + (NSString *)hostFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6 { char addrBuf[INET6_ADDRSTRLEN]; - + if (inet_ntop(AF_INET6, &pSockaddr6->sin6_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL) { addrBuf[0] = '\0'; } - + return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; } @@ -5555,7 +5559,7 @@ + (NSString *)hostFromAddress:(NSData *)address { NSString *host = nil; [self getHost:&host port:NULL family:NULL fromAddress:address]; - + return host; } @@ -5563,7 +5567,7 @@ + (uint16_t)portFromAddress:(NSData *)address { uint16_t port = 0; [self getHost:NULL port:&port family:NULL fromAddress:address]; - + return port; } @@ -5571,7 +5575,7 @@ + (int)familyFromAddress:(NSData *)address { int af = AF_UNSPEC; [self getHost:NULL port:NULL family:&af fromAddress:address]; - + return af; } @@ -5579,7 +5583,7 @@ + (BOOL)isIPv4Address:(NSData *)address { int af = AF_UNSPEC; [self getHost:NULL port:NULL family:&af fromAddress:address]; - + return (af == AF_INET); } @@ -5587,7 +5591,7 @@ + (BOOL)isIPv6Address:(NSData *)address { int af = AF_UNSPEC; [self getHost:NULL port:NULL family:&af fromAddress:address]; - + return (af == AF_INET6); } @@ -5601,17 +5605,17 @@ + (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr family:(int *)afPt if ([address length] >= sizeof(struct sockaddr)) { const struct sockaddr *addrX = (const struct sockaddr *)[address bytes]; - + if (addrX->sa_family == AF_INET) { if ([address length] >= sizeof(struct sockaddr_in)) { const struct sockaddr_in *addr4 = (const struct sockaddr_in *)(const void *)addrX; - + if (hostPtr) *hostPtr = [self hostFromSockaddr4:addr4]; if (portPtr) *portPtr = [self portFromSockaddr4:addr4]; if (afPtr) *afPtr = AF_INET; - + return YES; } } @@ -5620,20 +5624,20 @@ + (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr family:(int *)afPt if ([address length] >= sizeof(struct sockaddr_in6)) { const struct sockaddr_in6 *addr6 = (const struct sockaddr_in6 *)(const void *)addrX; - + if (hostPtr) *hostPtr = [self hostFromSockaddr6:addr6]; if (portPtr) *portPtr = [self portFromSockaddr6:addr6]; if (afPtr) *afPtr = AF_INET6; - + return YES; } } } - + if (hostPtr) *hostPtr = nil; if (portPtr) *portPtr = 0; if (afPtr) *afPtr = AF_UNSPEC; - + return NO; } diff --git a/WebDriverAgent/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.h b/WebDriverAgent/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.h index 04e0bd2f40..401830e56f 100644 --- a/WebDriverAgent/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.h +++ b/WebDriverAgent/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.h @@ -1,21 +1,26 @@ /** - * The HTTPMessage class is a simple Objective-C wrapper around Apple's CFHTTPMessage class. + * The HTTPMessage class is a simple Objective-C wrapper for HTTP message parsing. + * Migrated from CFHTTPMessage to use Foundation and Network framework. **/ #import -#if TARGET_OS_IPHONE -// Note: You may need to add the CFNetwork Framework to your project -#import -#endif - -#define HTTPVersion1_0 ((NSString *)kCFHTTPVersion1_0) -#define HTTPVersion1_1 ((NSString *)kCFHTTPVersion1_1) +#define HTTPVersion1_0 @"HTTP/1.0" +#define HTTPVersion1_1 @"HTTP/1.1" @interface HTTPMessage : NSObject { - CFHTTPMessageRef message; + NSMutableDictionary *_headers; + NSMutableData *_body; + NSString *_version; + NSString *_method; + NSURL *_url; + NSInteger _statusCode; + NSString *_statusDescription; + BOOL _isRequest; + BOOL _headerComplete; + NSMutableData *_rawData; } - (id)initEmptyRequest; diff --git a/WebDriverAgent/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.m b/WebDriverAgent/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.m index 345d84759e..920f7a2800 100644 --- a/WebDriverAgent/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.m +++ b/WebDriverAgent/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.m @@ -8,107 +8,350 @@ @implementation HTTPMessage -- (id)initEmptyRequest +- (id)init { if ((self = [super init])) { - message = CFHTTPMessageCreateEmpty(NULL, YES); + _headers = [[NSMutableDictionary alloc] init]; + _body = [[NSMutableData alloc] init]; + _rawData = [[NSMutableData alloc] init]; + _version = HTTPVersion1_1; + _headerComplete = NO; + _isRequest = YES; + } + return self; +} + +- (id)initEmptyRequest +{ + if ((self = [self init])) + { + _isRequest = YES; } return self; } - (id)initRequestWithMethod:(NSString *)method URL:(NSURL *)url version:(NSString *)version { - if ((self = [super init])) + if ((self = [self init])) { - message = CFHTTPMessageCreateRequest(NULL, - (__bridge CFStringRef)method, - (__bridge CFURLRef)url, - (__bridge CFStringRef)version); + _isRequest = YES; + _method = [method copy]; + _url = [url copy]; + _version = version ? [version copy] : HTTPVersion1_1; } return self; } - (id)initResponseWithStatusCode:(NSInteger)code description:(NSString *)description version:(NSString *)version { - if ((self = [super init])) + if ((self = [self init])) { - message = CFHTTPMessageCreateResponse(NULL, - (CFIndex)code, - (__bridge CFStringRef)description, - (__bridge CFStringRef)version); + _isRequest = NO; + _statusCode = code; + _statusDescription = [description copy]; + _version = version ? [version copy] : HTTPVersion1_1; } return self; } -- (void)dealloc +- (BOOL)appendData:(NSData *)data { - if (message) + if (!data || [data length] == 0) { - CFRelease(message); + return NO; } + + [_rawData appendData:data]; + + if (!_headerComplete) + { + // Look for the end of headers (CRLF CRLF or LF LF) + NSData *headerEndMarker = [@"\r\n\r\n" dataUsingEncoding:NSASCIIStringEncoding]; + NSRange headerEndRange = [_rawData rangeOfData:headerEndMarker options:0 range:NSMakeRange(0, [_rawData length])]; + + if (headerEndRange.location == NSNotFound) + { + // Also check for LF LF (some clients use this) + NSData *lfMarker = [@"\n\n" dataUsingEncoding:NSASCIIStringEncoding]; + headerEndRange = [_rawData rangeOfData:lfMarker options:0 range:NSMakeRange(0, [_rawData length])]; + } + + if (headerEndRange.location != NSNotFound) + { + _headerComplete = YES; + + // Parse the header data + NSData *headerData = [_rawData subdataWithRange:NSMakeRange(0, headerEndRange.location + headerEndRange.length)]; + NSString *headerString = [[NSString alloc] initWithData:headerData encoding:NSASCIIStringEncoding]; + + if (headerString) + { + [self parseHeaders:headerString]; + } + + // Extract body data if any + NSUInteger bodyStart = headerEndRange.location + headerEndRange.length; + if ([_rawData length] > bodyStart) + { + NSData *bodyData = [_rawData subdataWithRange:NSMakeRange(bodyStart, [_rawData length] - bodyStart)]; + [_body appendData:bodyData]; + } + + [_rawData setLength:0]; + } + } + else + { + // Headers are complete, append to body + [_body appendData:data]; + } + + return YES; } -- (BOOL)appendData:(NSData *)data +- (void)parseHeaders:(NSString *)headerString { - return CFHTTPMessageAppendBytes(message, [data bytes], [data length]); + NSArray *lines; + + // Try splitting by "\r\n" first (standard HTTP line ending) + // Check if the string actually contains "\r\n" delimiter + if ([headerString rangeOfString:@"\r\n"].location != NSNotFound) + { + // Found "\r\n" delimiter, use this split + lines = [headerString componentsSeparatedByString:@"\r\n"]; + } + else + { + // No "\r\n" found, try "\n" (some clients use just LF) + lines = [headerString componentsSeparatedByString:@"\n"]; + } + + // componentsSeparatedByString: always returns at least one element, + // so check if we have meaningful content (non-empty first line) + if ([lines count] == 0 || [[lines objectAtIndex:0] length] == 0) + { + return; + } + + // Parse first line (request line or status line) + NSString *firstLine = [lines objectAtIndex:0]; + NSArray *firstLineParts = [firstLine componentsSeparatedByString:@" "]; + + if (_isRequest && [firstLineParts count] >= 3) + { + // Request line: METHOD URL VERSION + _method = [[firstLineParts objectAtIndex:0] copy]; + NSString *urlString = [firstLineParts objectAtIndex:1]; + + // Handle both absolute URLs and relative paths + // Try absolute URL first + NSURL *parsedURL = [NSURL URLWithString:urlString]; + + // If that fails (nil), it's likely a relative path like "/endpoint" + // Create a URL with a base URL to handle relative paths + if (!parsedURL) + { + // Use a dummy base URL to allow relative path parsing + NSURL *baseURL = [NSURL URLWithString:@"http://localhost"]; + parsedURL = [NSURL URLWithString:urlString relativeToURL:baseURL]; + } + + _url = [parsedURL copy]; + if ([firstLineParts count] >= 3) + { + _version = [[firstLineParts objectAtIndex:2] copy]; + } + } + else if (!_isRequest && [firstLineParts count] >= 3) + { + // Status line: VERSION CODE DESCRIPTION + _version = [[firstLineParts objectAtIndex:0] copy]; + _statusCode = [[firstLineParts objectAtIndex:1] integerValue]; + NSMutableArray *descParts = [NSMutableArray arrayWithArray:firstLineParts]; + [descParts removeObjectAtIndex:0]; + [descParts removeObjectAtIndex:0]; + _statusDescription = [[descParts componentsJoinedByString:@" "] copy]; + } + + // Parse header fields + for (NSUInteger i = 1; i < [lines count]; i++) + { + NSString *line = [lines objectAtIndex:i]; + if ([line length] == 0) + { + continue; + } + + NSRange colonRange = [line rangeOfString:@":"]; + if (colonRange.location != NSNotFound) + { + NSString *headerName = [[line substringToIndex:colonRange.location] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + NSString *headerValue = [[line substringFromIndex:colonRange.location + 1] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + + if ([headerName length] > 0) + { + // HTTP headers are case-insensitive, but we'll store them with their original case + // For lookup, we'll use case-insensitive comparison + [_headers setObject:headerValue forKey:headerName]; + } + } + } } - (BOOL)isHeaderComplete { - return CFHTTPMessageIsHeaderComplete(message); + return _headerComplete; } - (NSString *)version { - return (__bridge_transfer NSString *)CFHTTPMessageCopyVersion(message); + return _version; } - (NSString *)method { - return (__bridge_transfer NSString *)CFHTTPMessageCopyRequestMethod(message); + return _method; } - (NSURL *)url { - return (__bridge_transfer NSURL *)CFHTTPMessageCopyRequestURL(message); + return _url; } - (NSInteger)statusCode { - return (NSInteger)CFHTTPMessageGetResponseStatusCode(message); + return _statusCode; } - (NSDictionary *)allHeaderFields { - return (__bridge_transfer NSDictionary *)CFHTTPMessageCopyAllHeaderFields(message); + return [_headers copy]; } - (NSString *)headerField:(NSString *)headerField { - return (__bridge_transfer NSString *)CFHTTPMessageCopyHeaderFieldValue(message, (__bridge CFStringRef)headerField); + // Case-insensitive lookup + for (NSString *key in [_headers allKeys]) + { + if ([key caseInsensitiveCompare:headerField] == NSOrderedSame) + { + return [_headers objectForKey:key]; + } + } + return nil; } - (void)setHeaderField:(NSString *)headerField value:(NSString *)headerFieldValue { - CFHTTPMessageSetHeaderFieldValue(message, - (__bridge CFStringRef)headerField, - (__bridge CFStringRef)headerFieldValue); + if (headerField && headerFieldValue) + { + // Remove existing header with same name (case-insensitive) + NSMutableArray *keysToRemove = [NSMutableArray array]; + for (NSString *key in [_headers allKeys]) + { + if ([key caseInsensitiveCompare:headerField] == NSOrderedSame) + { + [keysToRemove addObject:key]; + } + } + [_headers removeObjectsForKeys:keysToRemove]; + + // Add new header + [_headers setObject:headerFieldValue forKey:headerField]; + } } - (NSData *)messageData { - return (__bridge_transfer NSData *)CFHTTPMessageCopySerializedMessage(message); + NSMutableString *messageString = [NSMutableString string]; + + if (_isRequest) + { + // Request line + // For relative URLs, use the path component; for absolute URLs, use absoluteString + NSString *urlString = nil; + if (_url) + { + // If it's a relative URL (has a base), use the relative path + // Otherwise use absoluteString or path + if ([_url baseURL]) + { + // Relative URL - use the relative portion + urlString = [_url relativeString]; + } + else + { + // Absolute URL + urlString = [_url absoluteString]; + if (!urlString) + { + urlString = [_url path]; + } + } + } + [messageString appendFormat:@"%@ %@ %@\r\n", _method ?: @"GET", urlString ?: @"/", _version ?: HTTPVersion1_1]; + } + else + { + // Status line + [messageString appendFormat:@"%@ %ld %@\r\n", _version ?: HTTPVersion1_1, (long)_statusCode, _statusDescription ?: @""]; + } + + // Headers + for (NSString *key in [_headers allKeys]) + { + NSString *value = [_headers objectForKey:key]; + [messageString appendFormat:@"%@: %@\r\n", key, value]; + } + + // Empty line to separate headers from body + [messageString appendString:@"\r\n"]; + + NSMutableData *data = [NSMutableData dataWithData:(id)[messageString dataUsingEncoding:NSASCIIStringEncoding]]; + + // Append body if present + if ([_body length] > 0) + { + [data appendData:_body]; + } + + return data; } - (NSData *)body { - return (__bridge_transfer NSData *)CFHTTPMessageCopyBody(message); + return [_body copy]; } - (void)setBody:(NSData *)body { - CFHTTPMessageSetBody(message, (__bridge CFDataRef)body); + if (body) + { + _body = [body mutableCopy]; + } + else + { + _body = [[NSMutableData alloc] init]; + } +} + +- (void)dealloc +{ + // ARC automatically releases all instance variables, but we include this + // for clarity and to match the pattern of the original CFNetwork implementation. + // All Objective-C objects (_headers, _body, _rawData, _version, _method, _url, _statusDescription) + // will be automatically released by ARC when this object is deallocated. +#if ! __has_feature(objc_arc) + [_headers release]; + [_body release]; + [_rawData release]; + [_version release]; + [_method release]; + [_url release]; + [_statusDescription release]; + [super dealloc]; +#endif } @end diff --git a/WebDriverAgent/WebDriverAgentLib/WebDriverAgentLib.h b/WebDriverAgent/WebDriverAgentLib/WebDriverAgentLib.h index f14994b861..d0e3f7391b 100644 --- a/WebDriverAgent/WebDriverAgentLib/WebDriverAgentLib.h +++ b/WebDriverAgent/WebDriverAgentLib/WebDriverAgentLib.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import @@ -15,6 +14,7 @@ FOUNDATION_EXPORT double WebDriverAgentLib_VersionNumber; //! Project version string for WebDriverAgentLib_. FOUNDATION_EXPORT const unsigned char WebDriverAgentLib_VersionString[]; +#import #import #import #import @@ -38,9 +38,17 @@ FOUNDATION_EXPORT const unsigned char WebDriverAgentLib_VersionString[]; #import #import #import +#import +#import +#import +#import +#import +#import +#import #import #import #import +#import #import #import #import diff --git a/WebDriverAgent/WebDriverAgentRunner/UITestingUITests.m b/WebDriverAgent/WebDriverAgentRunner/UITestingUITests.m index cc7c780739..ad2ff45504 100644 --- a/WebDriverAgent/WebDriverAgentRunner/UITestingUITests.m +++ b/WebDriverAgent/WebDriverAgentRunner/UITestingUITests.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/AppDelegate.h b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/AppDelegate.h index 9561057fd9..ff03ff7766 100644 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/AppDelegate.h +++ b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/AppDelegate.h @@ -3,14 +3,12 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import #import -@interface AppDelegate : UIResponder -@property (strong, nonatomic) UIWindow *window; +@interface AppDelegate : UIResponder @end diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/AppDelegate.m b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/AppDelegate.m index 0a0e1d46dd..102b1887b4 100644 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/AppDelegate.m +++ b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/AppDelegate.m @@ -3,14 +3,36 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "AppDelegate.h" +#import "SceneDelegate.h" @interface AppDelegate () @end @implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + // Override point for customization after application launch. + return YES; +} + +#pragma mark - UISceneSession lifecycle + +- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + UISceneConfiguration *configuration = [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role]; + configuration.delegateClass = [SceneDelegate class]; + return configuration; +} + +- (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet *)sceneSessions { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after `application:didFinishLaunchingWithOptions`. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. +} + @end diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/FBAlertViewController.h b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/FBAlertViewController.h index deb3ddfb40..e5b2088c87 100644 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/FBAlertViewController.h +++ b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/FBAlertViewController.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/FBAlertViewController.m b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/FBAlertViewController.m index e431e8684d..1f1c77e11b 100644 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/FBAlertViewController.m +++ b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/FBAlertViewController.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBAlertViewController.h" diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/FBNavigationController.h b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/FBNavigationController.h index c83f8d262f..e9a6b8d95d 100644 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/FBNavigationController.h +++ b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/FBNavigationController.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/FBNavigationController.m b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/FBNavigationController.m index f404c0ba3e..3b0bec2663 100644 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/FBNavigationController.m +++ b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/FBNavigationController.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBNavigationController.h" diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/FBScrollViewController.h b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/FBScrollViewController.h index 4c79fb0f51..7cd517b43e 100644 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/FBScrollViewController.h +++ b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/FBScrollViewController.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/FBScrollViewController.m b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/FBScrollViewController.m index 4a49f9ce6c..a8fe797739 100644 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/FBScrollViewController.m +++ b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/FBScrollViewController.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBScrollViewController.h" diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/FBTableDataSource.h b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/FBTableDataSource.h index 28cbe4783a..d82f8b6513 100644 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/FBTableDataSource.h +++ b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/FBTableDataSource.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/FBTableDataSource.m b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/FBTableDataSource.m index ba28aed797..335f28afd5 100644 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/FBTableDataSource.m +++ b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/FBTableDataSource.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBTableDataSource.h" diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/SceneDelegate.h b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/SceneDelegate.h new file mode 100644 index 0000000000..972eb671e9 --- /dev/null +++ b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/SceneDelegate.h @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface SceneDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end + +NS_ASSUME_NONNULL_END + diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/SceneDelegate.m b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/SceneDelegate.m new file mode 100644 index 0000000000..646192cd4a --- /dev/null +++ b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/SceneDelegate.m @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "SceneDelegate.h" + +@implementation SceneDelegate + +- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be set and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession:` instead). +} + +- (void)sceneDidDisconnect:(UIScene *)scene { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). +} + +- (void)sceneDidBecomeActive:(UIScene *)scene { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. +} + +- (void)sceneWillResignActive:(UIScene *)scene { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). +} + +- (void)sceneWillEnterForeground:(UIScene *)scene { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. +} + +- (void)sceneDidEnterBackground:(UIScene *)scene { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. +} + +@end + diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/TouchSpotView.h b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/TouchSpotView.h index 3a312cb884..ed6a9cd6c5 100644 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/TouchSpotView.h +++ b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/TouchSpotView.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the -* LICENSE file in the root directory of this source tree. An additional grant -* of patent rights can be found in the PATENTS file in the same directory. +* LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/TouchSpotView.m b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/TouchSpotView.m index 539add43d7..ed7969404a 100644 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/TouchSpotView.m +++ b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/TouchSpotView.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the -* LICENSE file in the root directory of this source tree. An additional grant -* of patent rights can be found in the PATENTS file in the same directory. +* LICENSE file in the root directory of this source tree. */ #import "TouchSpotView.h" diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/TouchViewController.h b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/TouchViewController.h index 33bd085985..7125306a3a 100644 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/TouchViewController.h +++ b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/TouchViewController.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the -* LICENSE file in the root directory of this source tree. An additional grant -* of patent rights can be found in the PATENTS file in the same directory. +* LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/TouchViewController.m b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/TouchViewController.m index 1fd07671c3..25b3f99300 100644 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/TouchViewController.m +++ b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/TouchViewController.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the -* LICENSE file in the root directory of this source tree. An additional grant -* of patent rights can be found in the PATENTS file in the same directory. +* LICENSE file in the root directory of this source tree. */ #import "TouchViewController.h" diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/TouchableView.h b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/TouchableView.h index 46111c0185..53d0c1836e 100644 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/TouchableView.h +++ b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/TouchableView.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the -* LICENSE file in the root directory of this source tree. An additional grant -* of patent rights can be found in the PATENTS file in the same directory. +* LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/TouchableView.m b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/TouchableView.m index 55b2fd11ef..9e7412ab7b 100644 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/TouchableView.m +++ b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/TouchableView.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the -* LICENSE file in the root directory of this source tree. An additional grant -* of patent rights can be found in the PATENTS file in the same directory. +* LICENSE file in the root directory of this source tree. */ #import "TouchableView.h" diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/ViewController.h b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/ViewController.h index e4df859ab4..9349604846 100644 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/ViewController.h +++ b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/ViewController.h @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/ViewController.m b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/ViewController.m index 7807e59f11..1267c28be4 100644 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/ViewController.m +++ b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/ViewController.m @@ -3,8 +3,7 @@ * All rights reserved. * * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "ViewController.h" diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Info.plist b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Info.plist index 2dfb754cd6..5dc963a62d 100644 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Info.plist +++ b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Info.plist @@ -32,8 +32,23 @@ Yo Yo UILaunchStoryboardName LaunchScreen - UIMainStoryboardFile - Main + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneStoryboardFile + Main + + + + UIRequiredDeviceCapabilities armv7 diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Resources/Base.lproj/Main.storyboard b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Resources/Base.lproj/Main.storyboard index db52210458..5fb805f28f 100644 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Resources/Base.lproj/Main.storyboard +++ b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Resources/Base.lproj/Main.storyboard @@ -1,9 +1,10 @@ - + - + + @@ -19,7 +20,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/main.m b/WebDriverAgent/WebDriverAgentTests/IntegrationApp/main.m deleted file mode 100644 index 58bd6a8c01..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationApp/main.m +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -int main(int argc, char * argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, @"AppDelegate"); - } -} diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBAlertTests.m b/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBAlertTests.m deleted file mode 100644 index d02af4b643..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBAlertTests.m +++ /dev/null @@ -1,188 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import - -#import "FBConfiguration.h" -#import "FBIntegrationTestCase.h" -#import "FBTestMacros.h" -#import "FBMacros.h" - -@interface FBAlertTests : FBIntegrationTestCase -@end - -@implementation FBAlertTests - -- (void)setUp -{ - [super setUp]; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self launchApplication]; - [self goToAlertsPage]; - [FBConfiguration disableApplicationUIInterruptionsHandling]; - }); - [self clearAlert]; -} - -- (void)tearDown -{ - [self clearAlert]; - [super tearDown]; -} - -- (void)showApplicationAlert -{ - [self.testedApplication.buttons[FBShowAlertButtonName] tap]; - FBAssertWaitTillBecomesTrue(self.testedApplication.alerts.count != 0); -} - -- (void)showApplicationSheet -{ - [self.testedApplication.buttons[FBShowSheetAlertButtonName] tap]; - FBAssertWaitTillBecomesTrue(self.testedApplication.sheets.count != 0); -} - -- (void)testAlertPresence -{ - FBAlert *alert = [FBAlert alertWithApplication:self.testedApplication]; - XCTAssertFalse(alert.isPresent); - [self showApplicationAlert]; - XCTAssertTrue(alert.isPresent); -} - -- (void)testAlertText -{ - FBAlert *alert = [FBAlert alertWithApplication:self.testedApplication]; - XCTAssertNil(alert.text); - [self showApplicationAlert]; - XCTAssertTrue([alert.text containsString:@"Magic"]); - XCTAssertTrue([alert.text containsString:@"Should read"]); -} - -- (void)testAlertLabels -{ - FBAlert* alert = [FBAlert alertWithApplication:self.testedApplication]; - XCTAssertNil(alert.buttonLabels); - [self showApplicationAlert]; - XCTAssertNotNil(alert.buttonLabels); - XCTAssertEqual(1, alert.buttonLabels.count); - XCTAssertEqualObjects(@"Will do", alert.buttonLabels[0]); -} - -- (void)testClickAlertButton -{ - FBAlert* alert = [FBAlert alertWithApplication:self.testedApplication]; - XCTAssertFalse([alert clickAlertButton:@"Invalid" error:nil]); - [self showApplicationAlert]; - XCTAssertFalse([alert clickAlertButton:@"Invalid" error:nil]); - FBAssertWaitTillBecomesTrue(alert.isPresent); - XCTAssertTrue([alert clickAlertButton:@"Will do" error:nil]); - FBAssertWaitTillBecomesTrue(!alert.isPresent); -} - -- (void)testAcceptingAlert -{ - NSError *error; - [self showApplicationAlert]; - XCTAssertTrue([[FBAlert alertWithApplication:self.testedApplication] acceptWithError:&error]); - FBAssertWaitTillBecomesTrue(self.testedApplication.alerts.count == 0); - XCTAssertNil(error); -} - -- (void)testAcceptingAlertWithCustomLocator -{ - NSError *error; - [self showApplicationAlert]; - [FBConfiguration setAcceptAlertButtonSelector:@"**/XCUIElementTypeButton[-1]"]; - @try { - XCTAssertTrue([[FBAlert alertWithApplication:self.testedApplication] acceptWithError:&error]); - FBAssertWaitTillBecomesTrue(self.testedApplication.alerts.count == 0); - XCTAssertNil(error); - } @finally { - [FBConfiguration setAcceptAlertButtonSelector:@""]; - } -} - -- (void)testDismissingAlert -{ - NSError *error; - [self showApplicationAlert]; - XCTAssertTrue([[FBAlert alertWithApplication:self.testedApplication] dismissWithError:&error]); - FBAssertWaitTillBecomesTrue(self.testedApplication.alerts.count == 0); - XCTAssertNil(error); -} - -- (void)testDismissingAlertWithCustomLocator -{ - NSError *error; - [self showApplicationAlert]; - [FBConfiguration setDismissAlertButtonSelector:@"**/XCUIElementTypeButton[-1]"]; - @try { - XCTAssertTrue([[FBAlert alertWithApplication:self.testedApplication] dismissWithError:&error]); - FBAssertWaitTillBecomesTrue(self.testedApplication.alerts.count == 0); - XCTAssertNil(error); - } @finally { - [FBConfiguration setDismissAlertButtonSelector:@""]; - } -} - -- (void)testAlertElement -{ - [self showApplicationAlert]; - XCUIElement *alertElement = [FBAlert alertWithApplication:self.testedApplication].alertElement; - XCTAssertTrue(alertElement.exists); - XCTAssertTrue(alertElement.elementType == XCUIElementTypeAlert); -} - -- (void)testNotificationAlert -{ - FBAlert *alert = [FBAlert alertWithApplication:self.testedApplication]; - XCTAssertNil(alert.text); - [self.testedApplication.buttons[@"Create Notification Alert"] tap]; - FBAssertWaitTillBecomesTrue(alert.isPresent); - - XCTAssertTrue([alert.text containsString:@"Would Like to Send You Notifications"]); - XCTAssertTrue([alert.text containsString:@"Notifications may include"]); -} - -// This test case depends on the local app permission state. -- (void)testCameraRollAlert -{ - FBAlert *alert = [FBAlert alertWithApplication:self.testedApplication]; - XCTAssertNil(alert.text); - - [self.testedApplication.buttons[@"Create Camera Roll Alert"] tap]; - FBAssertWaitTillBecomesTrue(alert.isPresent); - - // "Would Like to Access Your Photos" or "Would Like to Access Your Photo Library" displayes on the alert button. - XCTAssertTrue([alert.text containsString:@"Would Like to Access Your Photo"]); - // iOS 15 has different UI flow - if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"15.0")) { - [[FBAlert alertWithApplication:self.testedApplication] dismissWithError:nil]; - // CI env could take longer time to show up the button, thus it needs to wait a bit. - XCTAssertTrue([self.testedApplication.buttons[@"Cancel"] waitForExistenceWithTimeout:30.0]); - [self.testedApplication.buttons[@"Cancel"] tap]; - } -} - -- (void)testGPSAccessAlert -{ - FBAlert *alert = [FBAlert alertWithApplication:self.testedApplication]; - XCTAssertNil(alert.text); - - [self.testedApplication.buttons[@"Create GPS access Alert"] tap]; - FBAssertWaitTillBecomesTrue(alert.isPresent); - - XCTAssertTrue([alert.text containsString:@"location"]); - XCTAssertTrue([alert.text containsString:@"Yo Yo"]); -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBAutoAlertsHandlerTests.m b/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBAutoAlertsHandlerTests.m deleted file mode 100644 index d086e35777..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBAutoAlertsHandlerTests.m +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBIntegrationTestCase.h" -#import "FBMacros.h" -#import "FBSession.h" -#import "FBXCodeCompatibility.h" -#import "FBTestMacros.h" -#import "XCUIElement+FBUtilities.h" - - -@interface FBAutoAlertsHandlerTests : FBIntegrationTestCase -@property (nonatomic) FBSession *session; -@end - - -@implementation FBAutoAlertsHandlerTests - -- (void)setUp -{ - [super setUp]; - - [self launchApplication]; - [self goToAlertsPage]; - - [self clearAlert]; -} - -- (void)tearDown -{ - [self clearAlert]; - - if (self.session) { - [self.session kill]; - } - - [super tearDown]; -} - -// The test is flaky on slow Travis CI -- (void)disabled_testAutoAcceptingOfAlerts -{ - self.session = [FBSession - initWithApplication:XCUIApplication.fb_activeApplication - defaultAlertAction:@"accept"]; - for (int i = 0; i < 2; i++) { - [self.testedApplication.buttons[FBShowAlertButtonName] tap]; - [self.testedApplication fb_waitUntilStable]; - FBAssertWaitTillBecomesTrue(self.testedApplication.alerts.count == 0); - } -} - -// The test is flaky on slow Travis CI -- (void)disabled_testAutoDismissingOfAlerts -{ - self.session = [FBSession - initWithApplication:XCUIApplication.fb_activeApplication - defaultAlertAction:@"dismiss"]; - for (int i = 0; i < 2; i++) { - [self.testedApplication.buttons[FBShowAlertButtonName] tap]; - [self.testedApplication fb_waitUntilStable]; - FBAssertWaitTillBecomesTrue(self.testedApplication.alerts.count == 0); - } -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBConfigurationTests.m b/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBConfigurationTests.m deleted file mode 100644 index 0993aff6ca..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBConfigurationTests.m +++ /dev/null @@ -1,38 +0,0 @@ -/** -* Copyright (c) 2015-present, Facebook, Inc. -* All rights reserved. -* -* This source code is licensed under the BSD-style license found in the -* LICENSE file in the root directory of this source tree. -*/ - -#import -#import "FBIntegrationTestCase.h" - -#import "FBConfiguration.h" -#import "FBRuntimeUtils.h" - -@interface FBConfigurationTests : FBIntegrationTestCase - -@end - -@implementation FBConfigurationTests - -- (void)setUp -{ - [super setUp]; - [self launchApplication]; -} - -- (void)testReduceMotion -{ - BOOL defaultReduceMotionEnabled = [FBConfiguration reduceMotionEnabled]; - - [FBConfiguration setReduceMotionEnabled:YES]; - XCTAssertTrue([FBConfiguration reduceMotionEnabled]); - - [FBConfiguration setReduceMotionEnabled:defaultReduceMotionEnabled]; - XCTAssertEqual([FBConfiguration reduceMotionEnabled], defaultReduceMotionEnabled); -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBElementAttributeTests.m b/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBElementAttributeTests.m deleted file mode 100644 index 9242638b7a..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBElementAttributeTests.m +++ /dev/null @@ -1,250 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBIntegrationTestCase.h" -#import "FBFindElementCommands.h" -#import "FBTestMacros.h" -#import "FBXCodeCompatibility.h" -#import "XCUIElement+FBAccessibility.h" -#import "XCUIElement+FBIsVisible.h" -#import "XCUIElement+FBWebDriverAttributes.h" - -@interface FBElementAttributeTests : FBIntegrationTestCase -@end - -@implementation FBElementAttributeTests - -- (void)setUp -{ - [super setUp]; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self launchApplication]; - [self goToAttributesPage]; - }); -} - -- (void)testElementAccessibilityAttributes -{ - // "Button" is accessibility element, and therefore isn't accessibility container - XCUIElement *buttonElement = self.testedApplication.buttons[@"Button"]; - XCTAssertTrue(buttonElement.exists); - XCTAssertTrue(buttonElement.fb_isAccessibilityElement); - XCTAssertFalse(buttonElement.isWDAccessibilityContainer); -} - -- (void)testContainerAccessibilityAttributes -{ - // "not_accessible" isn't accessibility element, but contains accessibility elements, so it is accessibility container - XCUIElement *inaccessibleButtonElement = self.testedApplication.buttons[@"not_accessible"]; - XCTAssertTrue(inaccessibleButtonElement.exists); - XCTAssertFalse(inaccessibleButtonElement.fb_isAccessibilityElement); - // FIXME: Xcode 11 environment returns false even if iOS 12 - // We must fix here to XCTAssertTrue if Xcode version will return the value properly - XCTAssertFalse(inaccessibleButtonElement.isWDAccessibilityContainer); -} - -- (void)testIgnoredAccessibilityAttributes -{ - // Images are neither accessibility elements nor contain them, so both checks should fail - XCUIElement *imageElement = self.testedApplication.images.allElementsBoundByIndex.firstObject; - XCTAssertTrue(imageElement.exists); - XCTAssertFalse(imageElement.fb_isAccessibilityElement); - XCTAssertFalse(imageElement.isWDAccessibilityContainer); -} - -- (void)testButtonAttributes -{ - XCUIElement *element = self.testedApplication.buttons[@"Button"]; - XCTAssertTrue(element.exists); - XCTAssertEqualObjects(element.wdType, @"XCUIElementTypeButton"); - XCTAssertEqualObjects(element.wdName, @"Button"); - XCTAssertEqualObjects(element.wdLabel, @"Button"); - XCTAssertNil(element.wdValue); - XCTAssertFalse(element.wdSelected); - XCTAssertTrue(element.fb_isVisible); - [element tap]; - XCTAssertTrue(element.wdValue.boolValue); - XCTAssertTrue(element.wdSelected); -} - -- (void)testLabelAttributes -{ - XCUIElement *element = self.testedApplication.staticTexts[@"Label"]; - XCTAssertTrue(element.exists); - XCTAssertEqualObjects(element.wdType, @"XCUIElementTypeStaticText"); - XCTAssertEqualObjects(element.wdName, @"Label"); - XCTAssertEqualObjects(element.wdLabel, @"Label"); - XCTAssertEqualObjects(element.wdValue, @"Label"); -} - -- (void)testIndexAttributes -{ - XCUIElement *element = self.testedApplication.buttons[@"Button"]; - XCTAssertTrue(element.exists); - XCTAssertEqual(element.wdIndex, 2); - XCUIElement *element2 = self.testedApplication; - XCTAssertTrue(element2.exists); - XCTAssertEqual(element2.wdIndex, 0); -} - -- (void)testAccessibilityTraits -{ - XCUIElement *button = self.testedApplication.buttons.firstMatch; - XCTAssertTrue(button.exists); - NSArray *buttonTraits = [button.wdTraits componentsSeparatedByString:@", "]; - NSArray *expectedButtonTraits = @[@"Button"]; - XCTAssertEqual(buttonTraits.count, expectedButtonTraits.count, @"Button should have exactly 1 trait"); - XCTAssertEqualObjects(buttonTraits, expectedButtonTraits); - XCTAssertEqualObjects(button.wdType, @"XCUIElementTypeButton"); - - XCUIElement *toggle = self.testedApplication.switches.firstMatch; - XCTAssertTrue(toggle.exists); - - // iOS 17.0 specific traits if available - NSArray *toggleTraits = [toggle.wdTraits componentsSeparatedByString:@", "]; - NSArray *expectedToggleTraits; - - #if __clang_major__ >= 16 - if (@available(iOS 17.0, *)) { - expectedToggleTraits = @[@"ToggleButton", @"Button"]; - XCTAssertEqual(toggleTraits.count, 2, @"Toggle should have exactly 2 traits on iOS 17+"); - } - #else - expectedToggleTraits = @[@"Button"]; - XCTAssertEqual(toggleTraits.count, 1, @"Toggle should have exactly 1 trait on iOS < 17"); - #endif - XCTAssertEqualObjects(toggleTraits, expectedToggleTraits); - XCTAssertEqualObjects(toggle.wdType, @"XCUIElementTypeSwitch"); - - XCUIElement *slider = self.testedApplication.sliders.firstMatch; - XCTAssertTrue(slider.exists); - NSArray *sliderTraits = [slider.wdTraits componentsSeparatedByString:@", "]; - NSArray *expectedSliderTraits = @[@"Adjustable"]; - XCTAssertEqual(sliderTraits.count, expectedSliderTraits.count, @"Slider should have exactly 1 trait"); - XCTAssertEqualObjects(sliderTraits, expectedSliderTraits); - XCTAssertEqualObjects(slider.wdType, @"XCUIElementTypeSlider"); - - XCUIElement *picker = self.testedApplication.pickerWheels.firstMatch; - XCTAssertTrue(picker.exists); - NSArray *pickerTraits = [picker.wdTraits componentsSeparatedByString:@", "]; - NSArray *expectedPickerTraits = @[@"Adjustable"]; - XCTAssertEqual(pickerTraits.count, expectedPickerTraits.count, @"Picker should have exactly 1 trait"); - XCTAssertEqualObjects(pickerTraits, expectedPickerTraits); - XCTAssertEqualObjects(picker.wdType, @"XCUIElementTypePickerWheel"); -} - -- (void)testTextFieldAttributes -{ - XCUIElement *element = self.testedApplication.textFields[@"Value"]; - XCTAssertTrue(element.exists); - XCTAssertEqualObjects(element.wdType, @"XCUIElementTypeTextField"); - XCTAssertNil(element.wdName); - XCTAssertEqualObjects(element.wdLabel, @""); - XCTAssertEqualObjects(element.wdValue, @"Value"); -} - -- (void)testTextFieldWithAccessibilityIdentifiersAttributes -{ - XCUIElement *element = self.testedApplication.textFields[@"aIdentifier"]; - XCTAssertTrue(element.exists); - XCTAssertEqualObjects(element.wdType, @"XCUIElementTypeTextField"); - XCTAssertEqualObjects(element.wdName, @"aIdentifier"); - XCTAssertEqualObjects(element.wdLabel, @"aLabel"); - XCTAssertEqualObjects(element.wdValue, @"Value2"); -} - -- (void)testSegmentedControlAttributes -{ - XCUIElement *element = self.testedApplication.segmentedControls.element; - XCTAssertTrue(element.exists); - XCTAssertEqualObjects(element.wdType, @"XCUIElementTypeSegmentedControl"); - XCTAssertNil(element.wdName); - XCTAssertNil(element.wdLabel); - XCTAssertNil(element.wdValue); -} - -- (void)testSliderAttributes -{ - XCUIElement *element = self.testedApplication.sliders.element; - XCTAssertTrue(element.exists); - XCTAssertEqualObjects(element.wdType, @"XCUIElementTypeSlider"); - XCTAssertNil(element.wdName); - XCTAssertNil(element.wdLabel); - XCTAssertTrue([element.wdValue containsString:@"50"]); - - NSNumber *minValue = element.wdMinValue; - NSNumber *maxValue = element.wdMaxValue; - - XCTAssertNotNil(minValue, @"Slider minValue should not be nil"); - XCTAssertNotNil(maxValue, @"Slider maxValue should not be nil"); - - XCTAssertEqualObjects(minValue, @0); - XCTAssertEqualObjects(maxValue, @1); -} - - -- (void)testActivityIndicatorAttributes -{ - XCUIElement *element = self.testedApplication.activityIndicators.element; - XCTAssertTrue(element.exists); - XCTAssertEqualObjects(element.wdType, @"XCUIElementTypeActivityIndicator"); - XCTAssertEqualObjects(element.wdName, @"Progress halted"); - XCTAssertEqualObjects(element.wdLabel, @"Progress halted"); - XCTAssertEqualObjects(element.wdValue, @"0"); -} - -- (void)testSwitchAttributes -{ - XCUIElement *element = self.testedApplication.switches.element; - XCTAssertTrue(element.exists); - XCTAssertEqualObjects(element.wdType, @"XCUIElementTypeSwitch"); - XCTAssertNil(element.wdName); - XCTAssertNil(element.wdLabel); - XCTAssertNil(element.wdPlaceholderValue); - XCTAssertEqualObjects(element.wdValue, @"1"); - XCTAssertFalse(element.wdSelected); - XCTAssertEqual(element.wdHittable, element.hittable); - [element tap]; - XCTAssertEqualObjects(element.wdValue, @"0"); - XCTAssertFalse(element.wdSelected); -} - -- (void)testPickerWheelAttributes -{ - XCUIElement *element = self.testedApplication.pickerWheels[@"Today"]; - XCTAssertTrue(element.exists); - XCTAssertEqualObjects(element.wdType, @"XCUIElementTypePickerWheel"); - XCTAssertNil(element.wdName); - XCTAssertNil(element.wdLabel); - XCTAssertEqualObjects(element.wdValue, @"Today"); -} - -- (void)testPageIndicatorAttributes -{ - XCUIElement *element = self.testedApplication.pageIndicators.element; - XCTAssertTrue(element.exists); - XCTAssertEqualObjects(element.wdType, @"XCUIElementTypePageIndicator"); - XCTAssertNil(element.wdName); - XCTAssertNil(element.wdLabel); - XCTAssertEqualObjects(element.wdValue, @"page 1 of 3"); -} - -- (void)testTextViewAttributes -{ - XCUIElement *element = self.testedApplication.textViews.element; - XCTAssertTrue(element.exists); - XCTAssertEqualObjects(element.wdType, @"XCUIElementTypeTextView"); - XCTAssertNil(element.wdName); - XCTAssertNil(element.wdLabel); - XCTAssertEqualObjects(element.wdValue, @"1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901"); -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBElementSwipingTests.m b/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBElementSwipingTests.m deleted file mode 100644 index f652c81b42..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBElementSwipingTests.m +++ /dev/null @@ -1,127 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBIntegrationTestCase.h" -#import "FBTestMacros.h" -#import "XCUIElement+FBWebDriverAttributes.h" -#import "FBXCodeCompatibility.h" -#import "XCUIElement+FBSwiping.h" - -@interface FBElementSwipingTests : FBIntegrationTestCase -@property (nonatomic, strong) XCUIElement *scrollView; -- (void)openScrollView; -@end - -@implementation FBElementSwipingTests - -- (void)openScrollView -{ - [self launchApplication]; - [self goToScrollPageWithCells:YES]; - self.scrollView = [[self.testedApplication.query descendantsMatchingType:XCUIElementTypeAny] matchingIdentifier:@"scrollView"].element; -} - -- (void)setUp -{ - [super setUp]; - if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"15.0")) { - [self openScrollView]; - } else { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self openScrollView]; - }); - } -} - -- (void)tearDown -{ - if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"15.0")) { - // Move to top page once to reset the scroll place - // since iOS 15 seems cannot handle cell visibility well when the view keps the view - [self.testedApplication terminate]; - } -} - -- (void)testSwipeUp -{ - [self.scrollView fb_swipeWithDirection:@"up" velocity:nil]; - FBAssertInvisibleCell(@"0"); -} - -- (void)testSwipeDown -{ - [self.scrollView fb_swipeWithDirection:@"up" velocity:nil]; - FBAssertInvisibleCell(@"0"); - [self.scrollView fb_swipeWithDirection:@"down" velocity:nil]; - FBAssertVisibleCell(@"0"); -} - -- (void)testSwipeDownWithVelocity -{ - if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) { - XCTSkip(@"Failed on Azure Pipeline. Local run succeeded."); - } - [self.scrollView fb_swipeWithDirection:@"up" velocity:@2500]; - FBAssertInvisibleCell(@"0"); - [self.scrollView fb_swipeWithDirection:@"down" velocity:@3000]; - FBAssertVisibleCell(@"0"); -} - -@end - -@interface FBElementSwipingApplicationTests : FBIntegrationTestCase -@property (nonatomic, strong) XCUIElement *scrollView; -- (void)openScrollView; -@end - -@implementation FBElementSwipingApplicationTests - -- (void)openScrollView -{ - [self launchApplication]; - [self goToScrollPageWithCells:YES]; -} - -- (void)setUp -{ - [super setUp]; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self openScrollView]; - }); -} - -- (void)testSwipeUp -{ - [self.testedApplication fb_swipeWithDirection:@"up" velocity:nil]; - FBAssertInvisibleCell(@"0"); -} - -- (void)testSwipeDown -{ - [self.testedApplication fb_swipeWithDirection:@"up" velocity:nil]; - FBAssertInvisibleCell(@"0"); - [self.testedApplication fb_swipeWithDirection:@"down" velocity:nil]; - FBAssertVisibleCell(@"0"); -} - -- (void)testSwipeDownWithVelocity -{ - if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) { - XCTSkip(@"Failed on Azure Pipeline. Local run succeeded."); - } - [self.testedApplication fb_swipeWithDirection:@"up" velocity:@2500]; - FBAssertInvisibleCell(@"0"); - [self.testedApplication fb_swipeWithDirection:@"down" velocity:@2500]; - FBAssertVisibleCell(@"0"); -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBElementVisibilityTests.m b/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBElementVisibilityTests.m deleted file mode 100644 index 87a9094924..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBElementVisibilityTests.m +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBIntegrationTestCase.h" -#import "FBMacros.h" -#import "FBTestMacros.h" -#import "FBXCodeCompatibility.h" -#import "XCUIElement+FBIsVisible.h" - -@interface FBElementVisibilityTests : FBIntegrationTestCase -@end - -@implementation FBElementVisibilityTests - -- (void)testSpringBoardIcons -{ - if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) { - return; - } - [self launchApplication]; - [self goToSpringBoardFirstPage]; - - // Check Icons on first screen - // Note: Calender app exits 2 (an app icon + a widget) exist on the home screen - // on iOS 15+. The firstMatch is for it. - XCTAssertTrue(self.springboard.icons[@"Calendar"].firstMatch.fb_isVisible); - XCTAssertTrue(self.springboard.icons[@"Reminders"].fb_isVisible); - - // Check Icons on second screen screen - XCTAssertFalse(self.springboard.icons[@"IntegrationApp"].firstMatch.fb_isVisible); -} - -- (void)testSpringBoardSubfolder -{ - if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad - || SYSTEM_VERSION_GREATER_THAN(@"12.0")) { - return; - } - [self launchApplication]; - [self goToSpringBoardExtras]; - XCTAssertFalse(self.springboard.icons[@"Extras"].otherElements[@"Contacts"].fb_isVisible); -} - -- (void)disabled_testIconsFromSearchDashboard -{ - // This test causes: - // Failure fetching attributes for element Device element: Error Domain=XCTDaemonErrorDomain Code=13 "Value for attribute 5017 is an error." UserInfo={NSLocalizedDescription=Value for attribute 5017 is an error.} - [self launchApplication]; - [self goToSpringBoardDashboard]; - XCTAssertFalse(self.springboard.icons[@"Reminders"].fb_isVisible); - XCTAssertFalse([[[self.springboard descendantsMatchingType:XCUIElementTypeIcon] - matchingIdentifier:@"IntegrationApp"] - firstMatch].fb_isVisible); -} - -- (void)testTableViewCells -{ - [self launchApplication]; - [self goToScrollPageWithCells:YES]; - for (int i = 0 ; i < 10 ; i++) { - FBAssertWaitTillBecomesTrue(self.testedApplication.cells.allElementsBoundByIndex[i].fb_isVisible); - FBAssertWaitTillBecomesTrue(self.testedApplication.staticTexts.allElementsBoundByIndex[i].fb_isVisible); - } -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBFailureProofTestCaseTests.m b/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBFailureProofTestCaseTests.m deleted file mode 100644 index 8355c3db85..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBFailureProofTestCaseTests.m +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBFailureProofTestCase.h" -#import "FBExceptionHandler.h" - -@interface FBFailureProofTestCaseTests : FBFailureProofTestCase -@end - -@implementation FBFailureProofTestCaseTests - -- (void)setUp -{ - [super setUp]; - [[XCUIApplication new] launch]; -} - -- (void)testPreventElementSearchFailure -{ - [[XCUIApplication new].buttons[@"kaboom"] tap]; -} - -- (void)testInactiveAppSearch -{ - [[XCUIDevice sharedDevice] pressButton:XCUIDeviceButtonHome]; - [[XCUIApplication new].buttons[@"kaboom"] tap]; -} - -- (void)testPreventAssertFailure -{ - XCTAssertNotNil(nil); -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBForceTouchTests.m b/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBForceTouchTests.m deleted file mode 100644 index 512732143c..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBForceTouchTests.m +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBIntegrationTestCase.h" - -#import "FBMacros.h" -#import "FBElementCache.h" -#import "FBTestMacros.h" -#import "XCUIDevice.h" -#import "XCUIDevice+FBRotation.h" -#import "XCUIElement+FBForceTouch.h" -#import "XCUIElement+FBIsVisible.h" - -@interface FBForceTouchTests : FBIntegrationTestCase -@end - -// It is recommnded to verify these tests with different iOS versions - -@implementation FBForceTouchTests - -- (void)verifyForceTapWithOrientation:(UIDeviceOrientation)orientation -{ - [[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:orientation]; - NSError *error; - XCTAssertTrue(self.testedApplication.alerts.count == 0); - [self.testedApplication.buttons[FBShowAlertForceTouchButtonName] fb_forceTouchCoordinate:nil - pressure:nil - duration:nil - error:&error]; - FBAssertWaitTillBecomesTrue(self.testedApplication.alerts.count > 0); -} - -- (void)setUp -{ - [super setUp]; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self launchApplication]; - [self goToAlertsPage]; - }); - [self clearAlert]; -} - -- (void)tearDown -{ - [self clearAlert]; - [self resetOrientation]; - [super tearDown]; -} - -- (void)testForceTap -{ - if (![XCUIDevice sharedDevice].supportsPressureInteraction) { - return; - } - - [self verifyForceTapWithOrientation:UIDeviceOrientationPortrait]; -} - -- (void)testForceTapInLandscapeLeft -{ - if (![XCUIDevice sharedDevice].supportsPressureInteraction) { - return; - } - - [self verifyForceTapWithOrientation:UIDeviceOrientationLandscapeLeft]; -} - -- (void)testForceTapInLandscapeRight -{ - if (![XCUIDevice sharedDevice].supportsPressureInteraction) { - return; - } - - [self verifyForceTapWithOrientation:UIDeviceOrientationLandscapeRight]; -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBImageProcessorTests.m b/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBImageProcessorTests.m deleted file mode 100644 index 4eddbc43e1..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBImageProcessorTests.m +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBImageProcessor.h" -#import "FBIntegrationTestCase.h" - - -@interface FBImageProcessorTests : FBIntegrationTestCase - -@property (nonatomic) NSData *originalImage; -@property (nonatomic) CGSize originalSize; - -@end - -@implementation FBImageProcessorTests - -- (void)setUp { - XCUIApplication *app = [[XCUIApplication alloc] init]; - [app launch]; - XCUIScreenshot *screenshot = app.screenshot; - self.originalImage = UIImageJPEGRepresentation(screenshot.image, 1.0); - self.originalSize = [FBImageProcessorTests scaledSizeFromImage:screenshot.image]; -} - -- (void)testScaling { - CGFloat halfScale = 0.5; - CGSize expectedHalfScaleSize = [FBImageProcessorTests sizeFromSize:self.originalSize scalingFactor:0.5]; - [self scaleImageWithFactor:halfScale - expectedSize:expectedHalfScaleSize]; - - // 0 is the smalles scaling factor we accept - CGFloat minScale = 0.0; - CGSize expectedMinScaleSize = [FBImageProcessorTests sizeFromSize:self.originalSize scalingFactor:0.01]; - [self scaleImageWithFactor:minScale - expectedSize:expectedMinScaleSize]; - - // For scaling factors above 100 we don't perform any scaling and just return the unmodified image - [self scaleImageWithFactor:1.0 - expectedSize:self.originalSize]; - [self scaleImageWithFactor:2.0 - expectedSize:self.originalSize]; -} - -- (void)scaleImageWithFactor:(CGFloat)scalingFactor expectedSize:(CGSize)excpectedSize { - FBImageProcessor *scaler = [[FBImageProcessor alloc] init]; - - id expScaled = [self expectationWithDescription:@"Receive scaled image"]; - - [scaler submitImageData:self.originalImage - scalingFactor:scalingFactor - completionHandler:^(NSData *scaled) { - UIImage *scaledImage = [UIImage imageWithData:scaled]; - CGSize scaledSize = [FBImageProcessorTests scaledSizeFromImage:scaledImage]; - - XCTAssertEqualWithAccuracy(scaledSize.width, excpectedSize.width, 1.0); - XCTAssertEqualWithAccuracy(scaledSize.height, excpectedSize.height, 1.0); - - [expScaled fulfill]; - }]; - - [self waitForExpectations:@[expScaled] - timeout:0.5]; - -} - -+ (CGSize)scaledSizeFromImage:(UIImage *)image { - return CGSizeMake(image.size.width * image.scale, image.size.height * image.scale); -} - -+ (CGSize)sizeFromSize:(CGSize)size scalingFactor:(CGFloat)scalingFactor { - return CGSizeMake(round(size.width * scalingFactor), round(size.height * scalingFactor)); -} - -@end - diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBIntegrationTestCase.h b/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBIntegrationTestCase.h deleted file mode 100644 index 9c7ee5710b..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBIntegrationTestCase.h +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -extern NSString *const FBShowAlertButtonName; -extern NSString *const FBShowSheetAlertButtonName; -extern NSString *const FBShowAlertForceTouchButtonName; -extern NSString *const FBTouchesCountLabelIdentifier; -extern NSString *const FBTapsCountLabelIdentifier; - -/** - XCTestCase helper class used for integration tests - */ -@interface FBIntegrationTestCase : XCTestCase -@property (nonatomic, strong, readonly) XCUIApplication *testedApplication; -@property (nonatomic, strong, readonly) XCUIApplication *springboard; - -/** - Launches application and resets side effects of testing like orientation etc. - */ -- (void)launchApplication; - -/** - Navigates integration app to attributes page - */ -- (void)goToAttributesPage; - -/** - Navigates integration app to alerts page - */ -- (void)goToAlertsPage; - -/** - Navigates integration app to touch page - */ -- (void)goToTouchPage; - -/** - Navigates to SpringBoard first page - */ -- (void)goToSpringBoardFirstPage; - -/** - Navigates to SpringBoard path with Extras folder - */ -- (void)goToSpringBoardExtras; - -/** - Navigates to SpringBoard's dashboard - */ -- (void)goToSpringBoardDashboard; - -/** - Navigates integration app to scrolling page - @param showCells whether should navigate to view with cell or plain scrollview - */ -- (void)goToScrollPageWithCells:(BOOL)showCells; - -/** - Verifies no alerts are present on the page. - If an alert exists then it is going to be dismissed. - */ -- (void)clearAlert; - -/** - Resets device orientation to portrait mode - */ -- (void)resetOrientation; - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBIntegrationTestCase.m b/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBIntegrationTestCase.m deleted file mode 100644 index bfdd3dbb91..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBIntegrationTestCase.m +++ /dev/null @@ -1,140 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBAlert.h" -#import "FBTestMacros.h" -#import "FBIntegrationTestCase.h" -#import "FBConfiguration.h" -#import "FBMacros.h" -#import "FBRunLoopSpinner.h" -#import "XCUIApplication+FBHelpers.h" -#import "XCUIDevice+FBRotation.h" -#import "XCUIElement.h" -#import "XCUIElement+FBIsVisible.h" -#import "XCUIElement+FBUtilities.h" -#import "XCTestConfiguration.h" - -NSString *const FBShowAlertButtonName = @"Create App Alert"; -NSString *const FBShowSheetAlertButtonName = @"Create Sheet Alert"; -NSString *const FBShowAlertForceTouchButtonName = @"Create Alert (Force Touch)"; -NSString *const FBTouchesCountLabelIdentifier = @"numberOfTouchesLabel"; -NSString *const FBTapsCountLabelIdentifier = @"numberOfTapsLabel"; - -@interface FBIntegrationTestCase () -@property (nonatomic, strong) XCUIApplication *testedApplication; -@property (nonatomic, strong) XCUIApplication *springboard; -@end - -@implementation FBIntegrationTestCase - -- (void)setUp -{ - // Enable it to get extended XCTest logs printed into the console - // [FBConfiguration enableXcTestDebugLogs]; - [super setUp]; - [FBConfiguration disableRemoteQueryEvaluation]; - [FBConfiguration disableAttributeKeyPathAnalysis]; - [FBConfiguration configureDefaultKeyboardPreferences]; - [FBConfiguration disableApplicationUIInterruptionsHandling]; - [FBConfiguration disableScreenshots]; - self.continueAfterFailure = NO; - self.springboard = XCUIApplication.fb_systemApplication; - self.testedApplication = [XCUIApplication new]; -} - -- (void)resetOrientation -{ - if ([XCUIDevice sharedDevice].orientation != UIDeviceOrientationPortrait) { - [[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:UIDeviceOrientationPortrait]; - } -} - -- (void)launchApplication -{ - [self.testedApplication launch]; - [self.testedApplication fb_waitUntilStable]; - FBAssertWaitTillBecomesTrue(self.testedApplication.buttons[@"Alerts"].fb_isVisible); -} - -- (void)goToAttributesPage -{ - [self.testedApplication.buttons[@"Attributes"] tap]; - [self.testedApplication fb_waitUntilStable]; - FBAssertWaitTillBecomesTrue(self.testedApplication.buttons[@"Button"].fb_isVisible); -} - -- (void)goToAlertsPage -{ - [self.testedApplication.buttons[@"Alerts"] tap]; - [self.testedApplication fb_waitUntilStable]; - FBAssertWaitTillBecomesTrue(self.testedApplication.buttons[FBShowAlertButtonName].fb_isVisible); - FBAssertWaitTillBecomesTrue(self.testedApplication.buttons[FBShowSheetAlertButtonName].fb_isVisible); -} - -- (void)goToTouchPage -{ - [self.testedApplication.buttons[@"Touch"] tap]; - [self.testedApplication fb_waitUntilStable]; - FBAssertWaitTillBecomesTrue(self.testedApplication.staticTexts[FBTouchesCountLabelIdentifier].fb_isVisible); - FBAssertWaitTillBecomesTrue(self.testedApplication.staticTexts[FBTapsCountLabelIdentifier].fb_isVisible); -} - -- (void)goToSpringBoardFirstPage -{ - [[XCUIDevice sharedDevice] pressButton:XCUIDeviceButtonHome]; - [self.testedApplication fb_waitUntilStable]; - FBAssertWaitTillBecomesTrue(XCUIApplication.fb_systemApplication.icons[@"Safari"].exists); - [[XCUIDevice sharedDevice] pressButton:XCUIDeviceButtonHome]; - [self.testedApplication fb_waitUntilStable]; - FBAssertWaitTillBecomesTrue(XCUIApplication.fb_systemApplication.icons[@"Calendar"].firstMatch.fb_isVisible); -} - -- (void)goToSpringBoardExtras -{ - [self goToSpringBoardFirstPage]; - [self.springboard swipeLeft]; - [self.testedApplication fb_waitUntilStable]; - FBAssertWaitTillBecomesTrue(self.springboard.icons[@"Extras"].fb_isVisible); -} - -- (void)goToSpringBoardDashboard -{ - [self goToSpringBoardFirstPage]; - [self.springboard swipeRight]; - [self.testedApplication fb_waitUntilStable]; - NSPredicate *predicate = - [NSPredicate predicateWithFormat: - @"%K IN %@", - FBStringify(XCUIElement, identifier), - @[@"SBSearchEtceteraIsolatedView", @"SpotlightSearchField"] - ]; - FBAssertWaitTillBecomesTrue([[self.springboard descendantsMatchingType:XCUIElementTypeAny] elementMatchingPredicate:predicate].fb_isVisible); - FBAssertWaitTillBecomesTrue(!self.springboard.icons[@"Calendar"].fb_isVisible); -} - -- (void)goToScrollPageWithCells:(BOOL)showCells -{ - [self.testedApplication.buttons[@"Scrolling"] tap]; - [self.testedApplication fb_waitUntilStable]; - FBAssertWaitTillBecomesTrue(self.testedApplication.buttons[@"TableView"].fb_isVisible); - [self.testedApplication.buttons[showCells ? @"TableView": @"ScrollView"] tap]; - [self.testedApplication fb_waitUntilStable]; - FBAssertWaitTillBecomesTrue(self.testedApplication.staticTexts[@"3"].fb_isVisible); -} - -- (void)clearAlert -{ - [self.testedApplication fb_waitUntilStable]; - [[FBAlert alertWithApplication:self.testedApplication] dismissWithError:nil]; - [self.testedApplication fb_waitUntilStable]; - FBAssertWaitTillBecomesTrue(self.testedApplication.alerts.count == 0); -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBKeyboardTests.m b/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBKeyboardTests.m deleted file mode 100644 index 5b8bf310d3..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBKeyboardTests.m +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBMacros.h" -#import "FBIntegrationTestCase.h" -#import "FBKeyboard.h" -#import "FBRunLoopSpinner.h" -#import "XCUIApplication+FBHelpers.h" - -@interface FBKeyboardTests : FBIntegrationTestCase -@end - -@implementation FBKeyboardTests - -- (void)setUp -{ - [super setUp]; - [self launchApplication]; - [self goToAttributesPage]; -} - -- (void)testKeyboardDismissal -{ - XCUIElement *textField = self.testedApplication.textFields[@"aIdentifier"]; - [textField tap]; - - if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"15.0")) { - // A workaround until find out to clear tutorial on iOS 15 - XCUIElement *textField = self.testedApplication.staticTexts[@"Continue"]; - if (textField.hittable) { - [textField tap]; - } - } - - NSError *error; - XCTAssertTrue([FBKeyboard waitUntilVisibleForApplication:self.testedApplication timeout:1 error:&error]); - XCTAssertNil(error); - if ([UIDevice.currentDevice userInterfaceIdiom] == UIUserInterfaceIdiomPad) { - XCTAssertTrue([self.testedApplication fb_dismissKeyboardWithKeyNames:nil error:&error]); - XCTAssertNil(error); - } else { - XCTAssertFalse([self.testedApplication fb_dismissKeyboardWithKeyNames:@[@"return"] error:&error]); - XCTAssertNotNil(error); - } -} - -- (void)testKeyboardPresenceVerification -{ - NSError *error; - XCTAssertFalse([FBKeyboard waitUntilVisibleForApplication:self.testedApplication timeout:1 error:&error]); - XCTAssertNotNil(error); -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBPasteboardTests.m b/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBPasteboardTests.m deleted file mode 100644 index 73e9275c9d..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBPasteboardTests.m +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBIntegrationTestCase.h" -#import "XCUIElement+FBTyping.h" -#import "FBPasteboard.h" -#import "FBTestMacros.h" -#import "FBXCodeCompatibility.h" - -@interface FBPasteboardTests : FBIntegrationTestCase -@end - -@implementation FBPasteboardTests - -- (void)setUp -{ - [super setUp]; - [self launchApplication]; - [self goToAttributesPage]; -} - -- (void)testSetPasteboard -{ - NSString *text = @"Happy pasting"; - XCUIElement *textField = self.testedApplication.textFields[@"aIdentifier"]; - NSError *error; - BOOL result = [FBPasteboard setData:(NSData *)[text dataUsingEncoding:NSUTF8StringEncoding] - forType:@"plaintext" - error:&error]; - XCTAssertTrue(result); - XCTAssertNil(error); - [textField tap]; - XCTAssertTrue([textField fb_clearTextWithError:&error]); - [textField pressForDuration:2.0]; - XCUIElementQuery *pastItemsQuery = [[self.testedApplication descendantsMatchingType:XCUIElementTypeAny] matchingIdentifier:@"Paste"]; - if (![pastItemsQuery.firstMatch waitForExistenceWithTimeout:2.0]) { - XCTFail(@"No matched element named 'Paste'"); - } - XCUIElement *pasteItem = pastItemsQuery.firstMatch; - XCTAssertNotNil(pasteItem); - [pasteItem tap]; - FBAssertWaitTillBecomesTrue([textField.value isEqualToString:text]); -} - -- (void)testGetPasteboard -{ - NSString *text = @"Happy copying"; - XCUIElement *textField = self.testedApplication.textFields[@"aIdentifier"]; - NSError *error; - XCTAssertTrue([textField fb_typeText:text shouldClear:NO error:&error]); - [textField pressForDuration:2.0]; - XCUIElement *selectAllItem = [[self.testedApplication descendantsMatchingType:XCUIElementTypeAny] - matchingIdentifier:@"Select All"].firstMatch; - XCTAssertTrue([selectAllItem waitForExistenceWithTimeout:5]); - [selectAllItem tap]; - if (SYSTEM_VERSION_LESS_THAN(@"16.0")) { - [textField pressForDuration:2.0]; - } - XCUIElement *copyItem = [[self.testedApplication descendantsMatchingType:XCUIElementTypeAny] - matchingIdentifier:@"Copy"].firstMatch; - XCTAssertTrue([copyItem waitForExistenceWithTimeout:5]); - [copyItem tap]; - FBWaitExact(1.0); - NSData *result = [FBPasteboard dataForType:@"plaintext" error:&error]; - XCTAssertNil(error); - XCTAssertEqualObjects(textField.value, [[NSString alloc] initWithData:result encoding:NSUTF8StringEncoding]); -} - -- (void)testUrlCopyPaste -{ - NSString *urlString = @"http://appium.io?some=value"; - NSError *error; - XCTAssertTrue([FBPasteboard setData:(NSData *)[urlString dataUsingEncoding:NSUTF8StringEncoding] - forType:@"url" - error:&error]); - XCTAssertNil(error); - NSData *result = [FBPasteboard dataForType:@"url" error:&error]; - XCTAssertNil(error); - XCTAssertEqualObjects(urlString, [[NSString alloc] initWithData:result encoding:NSUTF8StringEncoding]); -} - -@end - - diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBPickerWheelSelectTests.m b/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBPickerWheelSelectTests.m deleted file mode 100644 index 3b32df4b8f..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBPickerWheelSelectTests.m +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBIntegrationTestCase.h" -#import "XCUIElement+FBPickerWheel.h" -#import "XCUIElement+FBWebDriverAttributes.h" -#import "FBXCodeCompatibility.h" - -@interface FBPickerWheelSelectTests : FBIntegrationTestCase -@end - -@implementation FBPickerWheelSelectTests - -static const CGFloat DEFAULT_OFFSET = (CGFloat)0.2; - -- (void)setUp -{ - [super setUp]; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self launchApplication]; - [self goToAttributesPage]; - }); -} - -- (void)testSelectNextPickerValue -{ - XCUIElement *element = self.testedApplication.pickerWheels.allElementsBoundByIndex.firstObject; - XCTAssertTrue(element.exists); - XCTAssertEqualObjects(element.wdType, @"XCUIElementTypePickerWheel"); - NSError *error; - NSString *previousValue = element.wdValue; - XCTAssertTrue([element fb_selectNextOptionWithOffset:DEFAULT_OFFSET error:&error]); - XCTAssertNotEqualObjects(previousValue, element.wdValue); -} - -- (void)testSelectPreviousPickerValue -{ - XCUIElement *element = [self.testedApplication.pickerWheels elementBoundByIndex:1]; - XCTAssertTrue(element.exists); - XCTAssertEqualObjects(element.wdType, @"XCUIElementTypePickerWheel"); - NSError *error; - NSString *previousValue = element.wdValue; - XCTAssertTrue([element fb_selectPreviousOptionWithOffset:DEFAULT_OFFSET error:&error]); - XCTAssertNotEqualObjects(previousValue, element.wdValue); -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBSafariAlertTests.m b/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBSafariAlertTests.m deleted file mode 100644 index a9c6ddc5d1..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBSafariAlertTests.m +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBIntegrationTestCase.h" -#import "FBTestMacros.h" -#import "FBMacros.h" -#import "FBSession.h" -#import "FBXCodeCompatibility.h" -#import "XCUIElement+FBTyping.h" -#import "FBAlert.h" -#import "XCUIApplication+FBAlert.h" - - -@interface FBSafariAlertIntegrationTests : FBIntegrationTestCase -@property (nonatomic) FBSession *session; -@property (nonatomic) XCUIApplication *safariApp; -@end - - -@implementation FBSafariAlertIntegrationTests - -- (void)setUp -{ - [super setUp]; - self.session = [FBSession initWithApplication:XCUIApplication.fb_activeApplication]; - [self.session launchApplicationWithBundleId:FB_SAFARI_BUNDLE_ID - shouldWaitForQuiescence:nil - arguments:nil - environment:nil]; - self.safariApp = self.session.activeApplication; -} - -- (void)tearDown -{ - [self.session terminateApplicationWithBundleId:FB_SAFARI_BUNDLE_ID]; -} - -- (void)disabled_testCanHandleSafariInputPrompt -{ - XCUIElement *urlInput = [[self.safariApp - descendantsMatchingType:XCUIElementTypeTextField] - matchingPredicate:[ - NSPredicate predicateWithFormat:@"label == 'Address' or label == 'URL'" - ]].firstMatch; - if (!urlInput.exists) { - [[[self.safariApp descendantsMatchingType:XCUIElementTypeButton] matchingPredicate:[ - NSPredicate predicateWithFormat:@"label == 'Address' or label == 'URL'"]].firstMatch tap]; - } - XCTAssertTrue([urlInput fb_typeText:@"https://www.w3schools.com/js/tryit.asp?filename=tryjs_alert" - shouldClear:YES - error:nil]); - [[[self.safariApp descendantsMatchingType:XCUIElementTypeButton] matchingIdentifier:@"Go"].firstMatch tap]; - XCUIElement *clickMeButton = [[self.safariApp descendantsMatchingType:XCUIElementTypeButton] - matchingPredicate:[NSPredicate predicateWithFormat:@"label == 'Try it'"]].firstMatch; - XCTAssertTrue([clickMeButton waitForExistenceWithTimeout:15.0]); - [clickMeButton tap]; - FBAlert *alert = [FBAlert alertWithApplication:self.safariApp]; - FBAssertWaitTillBecomesTrue([alert.text isEqualToString:@"I am an alert box!"]); - NSArray *buttonLabels = alert.buttonLabels; - XCTAssertEqualObjects(buttonLabels.firstObject, @"Close"); - XCTAssertNotNil([self.safariApp fb_descendantsMatchingXPathQuery:@"//XCUIElementTypeButton[@label='Close']" - shouldReturnAfterFirstMatch:YES].firstObject); - XCTAssertTrue([alert acceptWithError:nil]); -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBScreenTests.m b/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBScreenTests.m deleted file mode 100644 index 1300141e1d..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBScreenTests.m +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBIntegrationTestCase.h" -#import "FBScreen.h" - -@interface FBScreenTests : FBIntegrationTestCase -@end - -@implementation FBScreenTests - -- (void)setUp -{ - [super setUp]; - [self launchApplication]; -} - -- (void)testScreenScale -{ - XCTAssertTrue([FBScreen scale] >= 2); -} - -@end - diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBScrollingTests.m b/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBScrollingTests.m deleted file mode 100644 index 915ca7b41d..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBScrollingTests.m +++ /dev/null @@ -1,126 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBIntegrationTestCase.h" -#import "FBTestMacros.h" - -#import "FBMacros.h" -#import "XCUIElement+FBIsVisible.h" -#import "XCUIElement+FBScrolling.h" -#import "XCUIElement+FBClassChain.h" -#import "FBXCodeCompatibility.h" - - -@interface FBScrollingTests : FBIntegrationTestCase -@property (nonatomic, strong) XCUIElement *scrollView; -@end - -@implementation FBScrollingTests - -+ (BOOL)shouldShowCells -{ - return YES; -} - -- (void)setUp -{ - [super setUp]; - [self launchApplication]; - [self goToScrollPageWithCells:YES]; - self.scrollView = [[self.testedApplication.query descendantsMatchingType:XCUIElementTypeAny] matchingIdentifier:@"scrollView"].element; -} - -- (void)testCellVisibility -{ - FBAssertVisibleCell(@"0"); - FBAssertVisibleCell(@"10"); - XCUIElement *cell10 = FBCellElementWithLabel(@"10"); - XCTAssertEqual([cell10 isWDHittable], [cell10 isHittable]); - FBAssertInvisibleCell(@"30"); - FBAssertInvisibleCell(@"50"); - XCUIElement *cell50 = FBCellElementWithLabel(@"50"); - XCTAssertEqual([cell50 isWDHittable], [cell50 isHittable]); -} - -- (void)testSimpleScroll -{ - if (SYSTEM_VERSION_LESS_THAN(@"16.0")) { - // This test is unstable in CI env - return; - } - - FBAssertVisibleCell(@"0"); - FBAssertVisibleCell(@"10"); - [self.scrollView fb_scrollDownByNormalizedDistance:1.0]; - FBAssertInvisibleCell(@"0"); - FBAssertInvisibleCell(@"10"); - XCTAssertTrue(self.testedApplication.staticTexts.count > 0); - [self.scrollView fb_scrollUpByNormalizedDistance:1.0]; - FBAssertVisibleCell(@"0"); - FBAssertVisibleCell(@"10"); -} - -- (void)testScrollToVisible -{ - NSString *cellName = @"30"; - FBAssertInvisibleCell(cellName); - NSError *error; - XCTAssertTrue([FBCellElementWithLabel(cellName) fb_scrollToVisibleWithError:&error]); - XCTAssertNil(error); - FBAssertVisibleCell(cellName); -} - -- (void)testFarScrollToVisible -{ - NSString *cellName = @"80"; - NSError *error; - FBAssertInvisibleCell(cellName); - XCTAssertTrue([FBCellElementWithLabel(cellName) fb_scrollToVisibleWithError:&error]); - XCTAssertNil(error); - FBAssertVisibleCell(cellName); -} - -- (void)testNativeFarScrollToVisible -{ - if (SYSTEM_VERSION_LESS_THAN(@"16.0")) { - // This test is unstable in CI env - return; - } - - NSString *cellName = @"80"; - NSError *error; - FBAssertInvisibleCell(cellName); - XCTAssertTrue([FBCellElementWithLabel(cellName) fb_nativeScrollToVisibleWithError:&error]); - XCTAssertNil(error); - FBAssertVisibleCell(cellName); -} - -- (void)testAttributeWithNullScrollToVisible -{ - NSError *error; - NSArray *queryMatches = [self.testedApplication fb_descendantsMatchingClassChain:@"**/XCUIElementTypeTable/XCUIElementTypeCell[60]" shouldReturnAfterFirstMatch:NO]; - XCTAssertEqual(queryMatches.count, 1); - XCUIElement *element = queryMatches.firstObject; - XCTAssertFalse(element.fb_isVisible); - [element fb_scrollToVisibleWithError:&error]; - XCTAssertNil(error); - XCTAssertTrue(element.fb_isVisible); - - if (SYSTEM_VERSION_LESS_THAN(@"16.0")) { - // This test is unstable in CI env - return; - } - - [element tap]; - XCTAssertTrue(element.wdSelected); -} - -@end - diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBSessionIntegrationTests.m b/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBSessionIntegrationTests.m deleted file mode 100644 index d12495eda4..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBSessionIntegrationTests.m +++ /dev/null @@ -1,133 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBIntegrationTestCase.h" -#import "FBExceptions.h" -#import "FBMacros.h" -#import "FBSession.h" -#import "FBXCodeCompatibility.h" -#import "FBTestMacros.h" -#import "FBUnattachedAppLauncher.h" -#import "XCUIApplication+FBHelpers.h" -#import "XCUIApplication.h" - -@interface FBSession (Tests) - -@end - -@interface FBSessionIntegrationTests : FBIntegrationTestCase -@property (nonatomic) FBSession *session; -@end - - -static NSString *const SETTINGS_BUNDLE_ID = @"com.apple.Preferences"; - -@implementation FBSessionIntegrationTests - -- (void)setUp -{ - [super setUp]; - [self launchApplication]; - XCUIApplication *app = [[XCUIApplication alloc] initWithBundleIdentifier:self.testedApplication.bundleID]; - self.session = [FBSession initWithApplication:app]; -} - -- (void)tearDown -{ - [self.session kill]; - [super tearDown]; -} - -- (void)testSettingsAppCanBeOpenedInScopeOfTheCurrentSession -{ - XCUIApplication *testedApp = XCUIApplication.fb_activeApplication; - [self.session launchApplicationWithBundleId:SETTINGS_BUNDLE_ID - shouldWaitForQuiescence:nil - arguments:nil - environment:nil]; - FBAssertWaitTillBecomesTrue([self.session.activeApplication.bundleID isEqualToString:SETTINGS_BUNDLE_ID]); - XCTAssertEqual([self.session applicationStateWithBundleId:SETTINGS_BUNDLE_ID], 4); - [self.session activateApplicationWithBundleId:testedApp.bundleID]; - FBAssertWaitTillBecomesTrue([self.session.activeApplication.bundleID isEqualToString: testedApp.bundleID]); - XCTAssertEqual([self.session applicationStateWithBundleId:testedApp.bundleID], 4); -} - -- (void)testSettingsAppCanBeReopenedInScopeOfTheCurrentSession -{ - XCUIApplication *systemApp = self.springboard; - [self.session launchApplicationWithBundleId:SETTINGS_BUNDLE_ID - shouldWaitForQuiescence:nil - arguments:nil - environment:nil]; - FBAssertWaitTillBecomesTrue([self.session.activeApplication.bundleID isEqualToString:SETTINGS_BUNDLE_ID]); - XCTAssertTrue([self.session terminateApplicationWithBundleId:SETTINGS_BUNDLE_ID]); - FBAssertWaitTillBecomesTrue([systemApp.bundleID isEqualToString:self.session.activeApplication.bundleID]); - [self.session launchApplicationWithBundleId:SETTINGS_BUNDLE_ID - shouldWaitForQuiescence:nil - arguments:nil - environment:nil]; - FBAssertWaitTillBecomesTrue([self.session.activeApplication.bundleID isEqualToString:SETTINGS_BUNDLE_ID]); -} - -- (void)testMainAppCanBeReactivatedInScopeOfTheCurrentSession -{ - XCUIApplication *testedApp = XCUIApplication.fb_activeApplication; - [self.session launchApplicationWithBundleId:SETTINGS_BUNDLE_ID - shouldWaitForQuiescence:nil - arguments:nil - environment:nil]; - FBAssertWaitTillBecomesTrue([self.session.activeApplication.bundleID isEqualToString:SETTINGS_BUNDLE_ID]); - [self.session activateApplicationWithBundleId:testedApp.bundleID]; - FBAssertWaitTillBecomesTrue([self.session.activeApplication.bundleID isEqualToString:testedApp.bundleID]); -} - -- (void)testMainAppCanBeRestartedInScopeOfTheCurrentSession -{ - XCUIApplication *systemApp = self.springboard; - XCUIApplication *testedApp = [[XCUIApplication alloc] initWithBundleIdentifier:self.testedApplication.bundleID]; - [self.session terminateApplicationWithBundleId:testedApp.bundleID]; - FBAssertWaitTillBecomesTrue([self.session.activeApplication.bundleID isEqualToString:systemApp.bundleID]); - [self.session launchApplicationWithBundleId:testedApp.bundleID - shouldWaitForQuiescence:nil - arguments:nil - environment:nil]; - FBAssertWaitTillBecomesTrue([self.session.activeApplication.bundleID isEqualToString:testedApp.bundleID]); -} - -- (void)testLaunchUnattachedApp -{ - [FBUnattachedAppLauncher launchAppWithBundleId:SETTINGS_BUNDLE_ID]; - [self.session kill]; - XCTAssertEqualObjects(SETTINGS_BUNDLE_ID, XCUIApplication.fb_activeApplication.bundleID); -} - -- (void)testAppWithInvalidBundleIDCannotBeStarted -{ - XCUIApplication *testedApp = [[XCUIApplication alloc] initWithBundleIdentifier:@"yolo"]; - @try { - [testedApp launch]; - XCTFail(@"An exception is expected to be thrown"); - } @catch (NSException *exception) { - XCTAssertEqualObjects(FBApplicationMissingException, exception.name); - } -} - -- (void)testAppWithInvalidBundleIDCannotBeActivated -{ - XCUIApplication *testedApp = [[XCUIApplication alloc] initWithBundleIdentifier:@"yolo"]; - @try { - [testedApp activate]; - XCTFail(@"An exception is expected to be thrown"); - } @catch (NSException *exception) { - XCTAssertEqualObjects(FBApplicationMissingException, exception.name); - } -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBTapTest.m b/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBTapTest.m deleted file mode 100644 index 2d3d2ba8b6..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBTapTest.m +++ /dev/null @@ -1,103 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBIntegrationTestCase.h" - -#import "FBElementCache.h" -#import "FBTestMacros.h" -#import "XCUIDevice+FBRotation.h" -#import "XCUIElement+FBIsVisible.h" - -@interface FBTapTest : FBIntegrationTestCase -@end - -// It is recommnded to verify these tests with different iOS versions - -@implementation FBTapTest - -- (void)verifyTapWithOrientation:(UIDeviceOrientation)orientation -{ - [[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:orientation]; - [self.testedApplication.buttons[FBShowAlertButtonName] tap]; - FBAssertWaitTillBecomesTrue(self.testedApplication.alerts.count > 0); -} - -- (void)setUp -{ - // Launch the app everytime to ensure the orientation for each test. - [super setUp]; - [self launchApplication]; - [self goToAlertsPage]; - [self clearAlert]; -} - -- (void)tearDown -{ - [self clearAlert]; - [self resetOrientation]; - [super tearDown]; -} - -- (void)testTap -{ - [self verifyTapWithOrientation:UIDeviceOrientationPortrait]; -} - -- (void)testTapInLandscapeLeft -{ - [self verifyTapWithOrientation:UIDeviceOrientationLandscapeLeft]; -} - -- (void)testTapInLandscapeRight -{ - - [self verifyTapWithOrientation:UIDeviceOrientationLandscapeRight]; -} - -- (void)testTapInPortraitUpsideDown -{ - if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) { - XCTSkip(@"Failed on Azure Pipeline. Local run succeeded."); - } - [self verifyTapWithOrientation:UIDeviceOrientationPortraitUpsideDown]; -} - -- (void)verifyTapByCoordinatesWithOrientation:(UIDeviceOrientation)orientation -{ - [[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:orientation]; - XCUIElement *dstButton = self.testedApplication.buttons[FBShowAlertButtonName]; - [[dstButton coordinateWithNormalizedOffset:CGVectorMake(0.5, 0.5)] tap]; - FBAssertWaitTillBecomesTrue(self.testedApplication.alerts.count > 0); -} - -- (void)testTapCoordinates -{ - [self verifyTapByCoordinatesWithOrientation:UIDeviceOrientationPortrait]; -} - -- (void)testTapCoordinatesInLandscapeLeft -{ - [self verifyTapByCoordinatesWithOrientation:UIDeviceOrientationLandscapeLeft]; -} - -- (void)testTapCoordinatesInLandscapeRight -{ - [self verifyTapByCoordinatesWithOrientation:UIDeviceOrientationLandscapeRight]; -} - -- (void)testTapCoordinatesInPortraitUpsideDown -{ - if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) { - XCTSkip(@"Failed on Azure Pipeline. Local run succeeded."); - } - [self verifyTapByCoordinatesWithOrientation:UIDeviceOrientationPortraitUpsideDown]; -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBTestMacros.h b/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBTestMacros.h deleted file mode 100644 index 7e545a7237..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBTestMacros.h +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -/** - Macro used to wait till certain condition is true. - If condition will not become true within default timeout (1m) it will fail running test - */ -#define FBAssertWaitTillBecomesTrue(condition) \ - ({ \ - NSError *__error; \ - XCTAssertTrue([[[FBRunLoopSpinner new] \ - interval:1.0] \ - spinUntilTrue:^BOOL{ \ - return (condition); \ - }]); \ - XCTAssertNil(__error); \ - }) - -#define FBWaitExact(timeoutSeconds) \ - ({ \ - [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:(timeoutSeconds)]]; \ - }) - -#define FBCellElementWithLabel(label) ([self.testedApplication descendantsMatchingType:XCUIElementTypeAny][label]) -#define FBAssertVisibleCell(label) FBAssertWaitTillBecomesTrue(FBCellElementWithLabel(label).fb_isVisible) -#define FBAssertInvisibleCell(label) FBAssertWaitTillBecomesTrue(!FBCellElementWithLabel(label).fb_isVisible) diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBTypingTest.m b/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBTypingTest.m deleted file mode 100644 index c8a7d0d093..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBTypingTest.m +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBIntegrationTestCase.h" -#import "XCUIElement.h" -#import "XCUIElement+FBTyping.h" - -@interface FBTypingTest : FBIntegrationTestCase -@end - -@implementation FBTypingTest - -- (void)setUp -{ - [super setUp]; - [self launchApplication]; - [self goToAttributesPage]; -} - -- (void)testTextTyping -{ - NSString *text = @"Happy typing"; - XCUIElement *textField = self.testedApplication.textFields[@"aIdentifier"]; - NSError *error; - XCTAssertTrue([textField fb_typeText:text shouldClear:NO error:&error]); - XCTAssertNil(error); - XCTAssertEqualObjects(textField.value, text); -} - -- (void)testTextTypingOnFocusedElement -{ - NSString *text = @"Happy typing"; - XCUIElement *textField = self.testedApplication.textFields[@"aIdentifier"]; - [textField tap]; - XCTAssertTrue(textField.hasKeyboardFocus); - NSError *error; - XCTAssertTrue([textField fb_typeText:text shouldClear:NO error:&error]); - XCTAssertNil(error); - XCTAssertTrue([textField fb_typeText:text shouldClear:NO error:&error]); - XCTAssertNil(error); - NSString *expectedText = [NSString stringWithFormat:@"%@%@", text, text]; - XCTAssertEqualObjects(textField.value, expectedText); -} - -- (void)testTextClearing -{ - XCUIElement *textField = self.testedApplication.textFields[@"aIdentifier"]; - [textField tap]; - [textField typeText:@"Happy typing"]; - XCTAssertTrue([textField.value length] > 0); - NSError *error; - XCTAssertTrue([textField fb_clearTextWithError:&error]); - XCTAssertNil(error); - XCTAssertEqualObjects(textField.value, @""); - XCTAssertTrue([textField fb_typeText:@"Happy typing" shouldClear:YES error:&error]); - XCTAssertTrue([textField fb_typeText:@"Happy typing 2" shouldClear:YES error:&error]); - XCTAssertEqualObjects(textField.value, @"Happy typing 2"); - XCTAssertNil(error); -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBVideoRecordingTests.m b/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBVideoRecordingTests.m deleted file mode 100644 index 00688755f9..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBVideoRecordingTests.m +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBIntegrationTestCase.h" - -#import "FBConfiguration.h" -#import "FBMacros.h" -#import "FBScreenRecordingPromise.h" -#import "FBScreenRecordingRequest.h" -#import "FBScreenRecordingContainer.h" -#import "FBXCTestDaemonsProxy.h" - -@interface FBVideoRecordingTests : FBIntegrationTestCase -@end - -@implementation FBVideoRecordingTests - -- (void)setUp -{ - [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"DisableDiagnosticScreenRecordings"]; - [super setUp]; -} - -- (void)testStartingAndStoppingVideoRecording -{ - XCTSkip(@"Failed on Azure Pipeline. Local run succeeded."); - - // Video recording is only available since iOS 17 - if (SYSTEM_VERSION_LESS_THAN(@"17.0")) { - return; - } - - FBScreenRecordingRequest *recordingRequest = [[FBScreenRecordingRequest alloc] initWithFps:24 - codec:0]; - NSError *error; - FBScreenRecordingPromise *promise = [FBXCTestDaemonsProxy startScreenRecordingWithRequest:recordingRequest - error:&error]; - XCTAssertNotNil(promise); - XCTAssertNotNil(promise.identifier); - XCTAssertNil(error); - - [FBScreenRecordingContainer.sharedInstance storeScreenRecordingPromise:promise - fps:24 - codec:0]; - XCTAssertEqual(FBScreenRecordingContainer.sharedInstance.screenRecordingPromise, promise); - - [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:2.]]; - - BOOL isSuccessfull = [FBXCTestDaemonsProxy stopScreenRecordingWithUUID:promise.identifier error:&error]; - XCTAssertTrue(isSuccessfull); - XCTAssertNil(error); - - [FBScreenRecordingContainer.sharedInstance reset]; - XCTAssertNil(FBScreenRecordingContainer.sharedInstance.screenRecordingPromise); -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBW3CMultiTouchActionsIntegrationTests.m b/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBW3CMultiTouchActionsIntegrationTests.m deleted file mode 100644 index 016c8701d3..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBW3CMultiTouchActionsIntegrationTests.m +++ /dev/null @@ -1,124 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBIntegrationTestCase.h" - -#import "XCUIElement.h" -#import "XCUIApplication+FBTouchAction.h" -#import "FBTestMacros.h" -#import "XCUIDevice+FBRotation.h" -#import "FBRunLoopSpinner.h" - -@interface FBW3CMultiTouchActionsIntegrationTests : FBIntegrationTestCase - -@end - - -@implementation FBW3CMultiTouchActionsIntegrationTests - -- (void)verifyGesture:(NSArray *> *)gesture orientation:(UIDeviceOrientation)orientation -{ - [[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:orientation]; - NSError *error; - XCTAssertTrue([self.testedApplication fb_performW3CActions:gesture elementCache:nil error:&error]); - FBAssertWaitTillBecomesTrue(self.testedApplication.alerts.count > 0); -} - -- (void)setUp -{ - [super setUp]; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self launchApplication]; - [self goToAlertsPage]; - }); - [self clearAlert]; -} - -- (void)tearDown -{ - [self clearAlert]; - [self resetOrientation]; - [super tearDown]; -} - -- (void)testErroneousGestures -{ - NSArray *> *> *invalidGestures = - @[ - // One of the chains has duplicated id - @[ - @{ - @"type": @"pointer", - @"id": @"finger1", - @"parameters": @{@"pointerType": @"touch"}, - @"actions": @[ - @{@"type": @"pointerMove", @"duration": @0, @"x": @1, @"y": @1}, - @{@"type": @"pointerDown"}, - @{@"type": @"pause", @"duration": @100}, - @{@"type": @"pointerUp"}, - ], - }, - @{ - @"type": @"pointer", - @"id": @"finger1", - @"parameters": @{@"pointerType": @"touch"}, - @"actions": @[ - @{@"type": @"pointerMove", @"duration": @0, @"x": @1, @"y": @1}, - @{@"type": @"pointerDown"}, - @{@"type": @"pause", @"duration": @100}, - @{@"type": @"pointerUp"}, - ], - }, - ], - - ]; - - for (NSArray *> *invalidGesture in invalidGestures) { - NSError *error; - XCTAssertFalse([self.testedApplication fb_performW3CActions:invalidGesture elementCache:nil error:&error]); - XCTAssertNotNil(error); - } -} - -- (void)testSymmetricTwoFingersTap -{ - XCUIElement *element = self.testedApplication.buttons[FBShowAlertButtonName]; - NSArray *> *gesture = - @[ - @{ - @"type": @"pointer", - @"id": @"finger1", - @"parameters": @{@"pointerType": @"touch"}, - @"actions": @[ - @{@"type": @"pointerMove", @"duration": @0, @"origin": element, @"x": @0, @"y": @0}, - @{@"type": @"pointerDown"}, - @{@"type": @"pause", @"duration": @100}, - @{@"type": @"pointerUp"}, - ], - }, - @{ - @"type": @"pointer", - @"id": @"finger2", - @"parameters": @{@"pointerType": @"touch"}, - @"actions": @[ - @{@"type": @"pointerMove", @"duration": @0, @"origin": element, @"x": @0, @"y": @0}, - @{@"type": @"pointerDown"}, - @{@"type": @"pause", @"duration": @100}, - @{@"type": @"pointerUp"}, - ], - }, - ]; - - [self verifyGesture:gesture orientation:UIDeviceOrientationPortrait]; -} - -@end - diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBW3CTouchActionsIntegrationTests.m b/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBW3CTouchActionsIntegrationTests.m deleted file mode 100644 index 2d8015d9f9..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBW3CTouchActionsIntegrationTests.m +++ /dev/null @@ -1,491 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBIntegrationTestCase.h" - -#import "XCUIElement.h" -#import "XCUIDevice.h" -#import "XCUIApplication+FBTouchAction.h" -#import "FBTestMacros.h" -#import "XCUIDevice+FBRotation.h" -#import "FBRunLoopSpinner.h" -#import "FBXCodeCompatibility.h" - -@interface FBW3CTouchActionsIntegrationTestsPart1 : FBIntegrationTestCase -@end - -@interface FBW3CTouchActionsIntegrationTestsPart2 : FBIntegrationTestCase -@property (nonatomic) XCUIElement *pickerWheel; -@end - - -@implementation FBW3CTouchActionsIntegrationTestsPart1 - -- (void)verifyGesture:(NSArray *> *)gesture orientation:(UIDeviceOrientation)orientation -{ - [[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:orientation]; - NSError *error; - XCTAssertTrue([self.testedApplication fb_performW3CActions:gesture elementCache:nil error:&error]); - FBAssertWaitTillBecomesTrue(self.testedApplication.alerts.count > 0); -} - -- (void)setUp -{ - [super setUp]; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self launchApplication]; - [self goToAlertsPage]; - }); - [self clearAlert]; -} - -- (void)tearDown -{ - [self clearAlert]; - [self resetOrientation]; - [super tearDown]; -} - -- (void)testErroneousGestures -{ - NSArray *> *> *invalidGestures = - @[ - // Empty chain - @[], - - // Chain element without 'actions' key - @[@{ - @"type": @"pointer", - @"id": @"finger1", - @"parameters": @{@"pointerType": @"touch"}, - }, - ], - - // Chain element without type - @[@{ - @"id": @"finger1", - @"parameters": @{@"pointerType": @"touch"}, - @"actions": @[ - @{@"type": @"pointerMove", @"duration": @0, @"x": @100, @"y": @100}, - ], - }, - ], - - // Chain element without id - @[@{ - @"type": @"pointer", - @"parameters": @{@"pointerType": @"touch"}, - @"actions": @[ - @{@"type": @"pointerMove", @"duration": @0, @"x": @100, @"y": @100}, - ], - }, - ], - - // Chain element with empty id - @[@{ - @"type": @"pointer", - @"id": @"", - @"parameters": @{@"pointerType": @"touch"}, - @"actions": @[ - @{@"type": @"pointerMove", @"duration": @0, @"x": @100, @"y": @100}, - ], - }, - ], - - // Chain element with unsupported type - @[@{ - @"type": @"key", - @"id": @"finger1", - @"parameters": @{@"pointerType": @"touch"}, - @"actions": @[ - @{@"type": @"pointerMove", @"duration": @0, @"x": @100, @"y": @100}, - ], - }, - ], - - // Chain element with unsupported pointerType (default) - @[@{ - @"type": @"pointer", - @"id": @"finger1", - @"actions": @[ - @{@"type": @"pointerMove", @"duration": @0, @"x": @100, @"y": @100}, - ], - }, - ], - - // Chain element with unsupported pointerType (non-default) - @[@{ - @"type": @"pointer", - @"id": @"finger1", - @"parameters": @{@"pointerType": @"pen"}, - @"actions": @[ - @{@"type": @"pointerMove", @"duration": @0, @"x": @100, @"y": @100}, - ], - }, - ], - - // Chain element without action item type - @[@{ - @"type": @"pointer", - @"id": @"finger1", - @"parameters": @{@"pointerType": @"touch"}, - @"actions": @[ - @{@"duration": @0, @"x": @1, @"y": @1}, - @{@"type": @"pointerDown"}, - @{@"type": @"pause", @"duration": @100}, - @{@"type": @"pointerUp"}, - ], - }, - ], - - // Chain element with singe up action - @[@{ - @"type": @"pointer", - @"id": @"finger1", - @"parameters": @{@"pointerType": @"touch"}, - @"actions": @[ - @{@"type": @"pointerUp"}, - ], - }, - ], - - // Chain element containing action item without y coordinate - @[@{ - @"type": @"pointer", - @"id": @"finger1", - @"parameters": @{@"pointerType": @"touch"}, - @"actions": @[ - @{@"type": @"pointerMove", @"duration": @0, @"x": @1}, - @{@"type": @"pointerDown"}, - @{@"type": @"pause", @"duration": @100}, - @{@"type": @"pointerUp"}, - ], - }, - ], - - // Chain element containing action item with an unknown type - @[@{ - @"type": @"pointer", - @"id": @"finger1", - @"parameters": @{@"pointerType": @"touch"}, - @"actions": @[ - @{@"type": @"pointerMoved", @"duration": @0, @"x": @1, @"y": @1}, - @{@"type": @"pointerDown"}, - @{@"type": @"pause", @"duration": @100}, - @{@"type": @"pointerUp"}, - ], - }, - ], - - // Chain element where action items start with an incorrect item - @[@{ - @"type": @"pointer", - @"id": @"finger1", - @"parameters": @{@"pointerType": @"touch"}, - @"actions": @[ - @{@"type": @"pause", @"duration": @100}, - @{@"type": @"pointerMove", @"duration": @0, @"x": @1, @"y": @1}, - @{@"type": @"pointerDown"}, - @{@"type": @"pause", @"duration": @100}, - @{@"type": @"pointerUp"}, - ], - }, - ], - - // Chain element where pointerMove action item does not contain coordinates - @[@{ - @"type": @"pointer", - @"id": @"finger1", - @"parameters": @{@"pointerType": @"touch"}, - @"actions": @[ - @{@"type": @"pointerMove", @"duration": @0}, - @{@"type": @"pointerDown"}, - @{@"type": @"pause", @"duration": @100}, - @{@"type": @"pointerUp"}, - ], - }, - ], - - // Chain element where pointerMove action item cannot use coordinates of the previous item - @[@{ - @"type": @"pointer", - @"id": @"finger1", - @"parameters": @{@"pointerType": @"touch"}, - @"actions": @[ - @{@"type": @"pointerMove", @"duration": @0, @"origin": @"pointer"}, - @{@"type": @"pointerDown"}, - @{@"type": @"pause", @"duration": @100}, - @{@"type": @"pointerUp"}, - ], - }, - ], - - // Chain element where action items contains negative duration - @[@{ - @"type": @"pointer", - @"id": @"finger1", - @"parameters": @{@"pointerType": @"touch"}, - @"actions": @[ - @{@"type": @"pointerMove", @"duration": @0, @"x": @1, @"y": @1}, - @{@"type": @"pointerDown"}, - @{@"type": @"pause", @"duration": @-100}, - @{@"type": @"pointerUp"}, - ], - }, - ], - - // Chain element where action items start with an incorrect one, because the correct one is canceled - @[@{ - @"type": @"pointer", - @"id": @"finger1", - @"parameters": @{@"pointerType": @"touch"}, - @"actions": @[ - @{@"type": @"pointerMove", @"duration": @0, @"x": @1, @"y": @1}, - @{@"type": @"pointerCancel"}, - @{@"type": @"pointerDown"}, - @{@"type": @"pause", @"duration": @-100}, - @{@"type": @"pointerUp"}, - ], - }, - ], - - ]; - - for (NSArray *> *invalidGesture in invalidGestures) { - NSError *error; - XCTAssertFalse([self.testedApplication fb_performW3CActions:invalidGesture elementCache:nil error:&error]); - XCTAssertNotNil(error); - } -} - -- (void)testNothingDoesWithoutError -{ - NSArray *> *gesture = - @[@{ - @"type": @"pointer", - @"id": @"finger1", - @"parameters": @{@"pointerType": @"touch"}, - @"actions": @[], - }, - ]; - NSError *error; - XCTAssertTrue([self.testedApplication fb_performW3CActions:gesture elementCache:nil error:&error]); - XCTAssertNil(error); -} - -- (void)testTap -{ - NSArray *> *gesture = - @[@{ - @"type": @"pointer", - @"id": @"finger1", - @"parameters": @{@"pointerType": @"touch"}, - @"actions": @[ - @{@"type": @"pointerMove", @"duration": @0, @"origin": self.testedApplication.buttons[FBShowAlertButtonName], @"x": @0, @"y": @0}, - @{@"type": @"pointerDown"}, - @{@"type": @"pause", @"duration": @100}, - @{@"type": @"pointerUp"}, - ], - }, - ]; - [self verifyGesture:gesture orientation:UIDeviceOrientationPortrait]; -} - -- (void)testDoubleTap -{ - NSArray *> *gesture = - @[@{ - @"type": @"pointer", - @"id": @"finger1", - @"parameters": @{@"pointerType": @"touch"}, - @"actions": @[ - @{@"type": @"pointerMove", @"duration": @0, @"origin": self.testedApplication.buttons[FBShowAlertButtonName]}, - @{@"type": @"pointerDown"}, - @{@"type": @"pause", @"duration": @50}, - @{@"type": @"pointerUp"}, - @{@"type": @"pause", @"duration": @200}, - @{@"type": @"pointerDown"}, - @{@"type": @"pause", @"duration": @50}, - @{@"type": @"pointerUp"}, - ], - }, - ]; - [self verifyGesture:gesture orientation:UIDeviceOrientationLandscapeLeft]; -} - -- (void)testLongPressWithCombinedPause -{ - NSArray *> *gesture = - @[@{ - @"type": @"pointer", - @"id": @"finger1", - @"parameters": @{@"pointerType": @"touch"}, - @"actions": @[ - @{@"type": @"pointerMove", @"duration": @0, @"origin": self.testedApplication.buttons[FBShowAlertButtonName], @"x": @5, @"y": @5}, - @{@"type": @"pointerDown"}, - @{@"type": @"pause", @"duration": @200}, - @{@"type": @"pause", @"duration": @200}, - @{@"type": @"pause", @"duration": @100}, - @{@"type": @"pointerUp"}, - ], - }, - ]; - [self verifyGesture:gesture orientation:UIDeviceOrientationLandscapeRight]; -} - -- (void)testLongPress -{ - if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) { - XCTSkip(@"Failed on Azure Pipeline. Local run succeeded."); - } - UIDeviceOrientation orientation = UIDeviceOrientationLandscapeLeft; - [[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:orientation]; - CGRect elementFrame = self.testedApplication.buttons[FBShowAlertButtonName].frame; - NSArray *> *gesture = - @[@{ - @"type": @"pointer", - @"id": @"finger1", - @"parameters": @{@"pointerType": @"touch"}, - @"actions": @[ - @{@"type": @"pointerMove", @"duration": @0, @"x": @(elementFrame.origin.x + 1), @"y": @(elementFrame.origin.y + 1)}, - @{@"type": @"pointerDown"}, - @{@"type": @"pause", @"duration": @500}, - @{@"type": @"pointerUp"}, - ], - }, - ]; - [self verifyGesture:gesture orientation:orientation]; -} - -- (void)testForceTap -{ - if (![XCUIDevice.sharedDevice supportsPressureInteraction]) { - return; - } - - NSArray *> *gesture = - @[@{ - @"type": @"pointer", - @"id": @"finger1", - @"parameters": @{@"pointerType": @"touch"}, - @"actions": @[ - @{@"type": @"pointerMove", @"duration": @0, @"origin": self.testedApplication.buttons[FBShowAlertButtonName]}, - @{@"type": @"pointerDown"}, - @{@"type": @"pause", @"duration": @500}, - @{@"type": @"pointerDown", @"pressure": @1.0}, - @{@"type": @"pause", @"duration": @50}, - @{@"type": @"pointerDown", @"pressure": @1.0}, - @{@"type": @"pause", @"duration": @50}, - @{@"type": @"pointerUp"}, - ], - }, - ]; - [self verifyGesture:gesture orientation:UIDeviceOrientationLandscapeLeft]; -} - -@end - - -@implementation FBW3CTouchActionsIntegrationTestsPart2 - -- (void)setUp -{ - [super setUp]; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self launchApplication]; - [self goToAttributesPage]; - }); - self.pickerWheel = self.testedApplication.pickerWheels.allElementsBoundByIndex.firstObject; -} - -- (void)tearDown -{ - [self resetOrientation]; - [super tearDown]; -} - -- (void)verifyPickerWheelPositionChangeWithGesture:(NSArray *> *)gesture -{ - NSString *previousValue = self.pickerWheel.value; - NSError *error; - XCTAssertTrue([self.testedApplication fb_performW3CActions:gesture elementCache:nil error:&error]); - XCTAssertNil(error); - XCTAssertTrue([[[[FBRunLoopSpinner new] - timeout:2.0] - timeoutErrorMessage:@"Picker wheel value has not been changed after 2 seconds timeout"] - spinUntilTrue:^BOOL{ - return ![[self.pickerWheel fb_standardSnapshot].value isEqualToString:previousValue]; - } - error:&error]); - XCTAssertNil(error); -} - -- (void)testSwipePickerWheelWithElementCoordinates -{ - CGRect pickerFrame = self.pickerWheel.frame; - NSArray *> *gesture = - @[@{ - @"type": @"pointer", - @"id": @"finger1", - @"parameters": @{@"pointerType": @"touch"}, - @"actions": @[ - @{@"type": @"pointerMove", @"duration": @0, @"origin": self.pickerWheel, @"x": @0, @"y":@0}, - @{@"type": @"pointerDown"}, - @{@"type": @"pointerMove", @"duration": @500, @"origin": self.pickerWheel, @"x": @0, @"y": @(pickerFrame.size.height / 2)}, - @{@"type": @"pointerUp"}, - ], - }, - ]; - [self verifyPickerWheelPositionChangeWithGesture:gesture]; -} - -- (void)testSwipePickerWheelWithRelativeCoordinates -{ - CGRect pickerFrame = self.pickerWheel.frame; - NSArray *> *gesture = - @[@{ - @"type": @"pointer", - @"id": @"finger1", - @"parameters": @{@"pointerType": @"touch"}, - @"actions": @[ - @{@"type": @"pointerMove", @"duration": @250, @"origin": self.pickerWheel, @"x": @0, @"y": @0}, - @{@"type": @"pointerDown"}, - @{@"type": @"pointerMove", @"duration": @500, @"origin": @"pointer", @"x": @0, @"y": @(-pickerFrame.size.height / 2)}, - @{@"type": @"pointerUp"}, - ], - }, - ]; - [self verifyPickerWheelPositionChangeWithGesture:gesture]; -} - -- (void)testSwipePickerWheelWithAbsoluteCoordinates -{ - CGRect pickerFrame = self.pickerWheel.frame; - NSArray *> *gesture = - @[@{ - @"type": @"pointer", - @"id": @"finger1", - @"parameters": @{@"pointerType": @"touch"}, - @"actions": @[ - @{@"type": @"pointerMove", @"duration": @0, @"x": @(pickerFrame.origin.x + pickerFrame.size.width / 2), @"y": @(pickerFrame.origin.y + pickerFrame.size.height / 2)}, - @{@"type": @"pointerDown"}, - @{@"type": @"pointerMove", @"duration": @500, @"origin": @"pointer", @"x": @0, @"y": @(pickerFrame.size.height / 2)}, - @{@"type": @"pointerUp"}, - ], - }, - ]; - [self verifyPickerWheelPositionChangeWithGesture:gesture]; -} - -@end - - diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBW3CTypeActionsTests.m b/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBW3CTypeActionsTests.m deleted file mode 100644 index 84a5c6c748..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBW3CTypeActionsTests.m +++ /dev/null @@ -1,184 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBIntegrationTestCase.h" -#import "XCUIElement.h" -#import "XCUIElement+FBTyping.h" -#import "XCUIApplication+FBTouchAction.h" -#import "XCUIElement+FBWebDriverAttributes.h" -#import "FBRuntimeUtils.h" -#import "FBXCodeCompatibility.h" - - -@interface FBW3CTypeActionsTests : FBIntegrationTestCase -@end - -@implementation FBW3CTypeActionsTests - -- (void)setUp -{ - [super setUp]; - [self launchApplication]; - [self goToAttributesPage]; -} - -- (void)testErroneousGestures -{ - if (![XCPointerEvent.class fb_areKeyEventsSupported]) { - return; - } - - NSArray *> *> *invalidGestures = - @[ - - // missing balance 1 - @[@{ - @"type": @"key", - @"id": @"keyboard", - @"actions": @[ - @{@"type": @"keyDown", @"value": @"h"}, - @{@"type": @"keyUp", @"value": @"k"}, - ], - }, - ], - - // missing balance 2 - @[@{ - @"type": @"key", - @"id": @"keyboard", - @"actions": @[ - @{@"type": @"keyDown", @"value": @"h"}, - ], - }, - ], - - // missing balance 3 - @[@{ - @"type": @"key", - @"id": @"keyboard", - @"actions": @[ - @{@"type": @"keyUp", @"value": @"h"}, - ], - }, - ], - - // missing key value - @[@{ - @"type": @"key", - @"id": @"keyboard", - @"actions": @[ - @{@"type": @"keyUp"}, - ], - }, - ], - - // wrong key value - @[@{ - @"type": @"key", - @"id": @"keyboard", - @"actions": @[ - @{@"type": @"keyUp", @"value": @500}, - ], - }, - ], - - // missing duration value - @[@{ - @"type": @"key", - @"id": @"keyboard", - @"actions": @[ - @{@"type": @"pause"}, - ], - }, - ], - - // wrong duration value - @[@{ - @"type": @"key", - @"id": @"keyboard", - @"actions": @[ - @{@"type": @"duration", @"duration": @"bla"}, - ], - }, - ], - - ]; - - for (NSArray *> *invalidGesture in invalidGestures) { - NSError *error; - XCTAssertFalse([self.testedApplication fb_performW3CActions:invalidGesture elementCache:nil error:&error]); - XCTAssertNotNil(error); - } -} - -- (void)testTextTyping -{ - if (![XCPointerEvent.class fb_areKeyEventsSupported]) { - return; - } - - XCUIElement *textField = self.testedApplication.textFields[@"aIdentifier"]; - [textField tap]; - NSArray *> *typeAction = - @[ - @{ - @"type": @"key", - @"id": @"keyboard2", - @"actions": @[ - @{@"type": @"pause", @"duration": @500}, - @{@"type": @"keyDown", @"value": @"🏀"}, - @{@"type": @"keyUp", @"value": @"🏀"}, - @{@"type": @"keyDown", @"value": @"N"}, - @{@"type": @"keyUp", @"value": @"N"}, - @{@"type": @"keyDown", @"value": @"B"}, - @{@"type": @"keyUp", @"value": @"B"}, - @{@"type": @"keyDown", @"value": @"A"}, - @{@"type": @"keyUp", @"value": @"A"}, - @{@"type": @"keyDown", @"value": @"a"}, - @{@"type": @"keyUp", @"value": @"a"}, - @{@"type": @"keyDown", @"value": [NSString stringWithFormat:@"%C", 0xE003]}, - @{@"type": @"keyUp", @"value": [NSString stringWithFormat:@"%C", 0xE003]}, - @{@"type": @"pause", @"duration": @500}, - ], - }, - ]; - NSError *error; - XCTAssertTrue([self.testedApplication fb_performW3CActions:typeAction - elementCache:nil - error:&error]); - XCTAssertNil(error); - XCTAssertEqualObjects(textField.wdValue, @"🏀NBA"); -} - -- (void)testTextTypingWithEmptyActions -{ - if (![XCPointerEvent.class fb_areKeyEventsSupported]) { - return; - } - - XCUIElement *textField = self.testedApplication.textFields[@"aIdentifier"]; - [textField tap]; - NSArray *> *typeAction = - @[ - @{ - @"type": @"pointer", - @"id": @"touch", - @"actions": @[], - }, - ]; - NSError *error; - XCTAssertTrue([self.testedApplication fb_performW3CActions:typeAction - elementCache:nil - error:&error]); - XCTAssertNil(error); - XCTAssertEqualObjects(textField.value, @""); -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBXPathIntegrationTests.m b/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBXPathIntegrationTests.m deleted file mode 100644 index 7082224efb..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBXPathIntegrationTests.m +++ /dev/null @@ -1,157 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBIntegrationTestCase.h" -#import "FBMacros.h" -#import "FBTestMacros.h" -#import "FBXPath.h" -#import "FBXCAccessibilityElement.h" -#import "FBXCodeCompatibility.h" -#import "FBXCElementSnapshotWrapper+Helpers.h" -#import "FBXMLGenerationOptions.h" -#import "XCUIApplication.h" -#import "XCUIElement.h" -#import "XCUIElement+FBFind.h" -#import "XCUIElement+FBUtilities.h" -#import "XCUIElement+FBWebDriverAttributes.h" - - -@interface FBXPathIntegrationTests : FBIntegrationTestCase -@property (nonatomic, strong) XCUIElement *testedView; -@end - -@implementation FBXPathIntegrationTests - -- (void)setUp -{ - [super setUp]; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self launchApplication]; - }); - self.testedView = self.testedApplication.otherElements[@"MainView"]; - XCTAssertTrue(self.testedView.exists); - FBAssertWaitTillBecomesTrue(self.testedView.buttons.count > 0); -} - -- (id)destinationSnapshot -{ - XCUIElement *matchingElement = self.testedView.buttons.allElementsBoundByIndex.firstObject; - id snapshot = [matchingElement fb_customSnapshot]; - // Over iOS13, snapshot returns a child. - // The purpose of here is return a single element to replace children with an empty array for testing. - snapshot.children = @[]; - return snapshot; -} - -- (void)testApplicationNodeXMLRepresentation -{ - id snapshot = [self.testedApplication fb_customSnapshot]; - snapshot.children = @[]; - FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:snapshot]; - NSString *xmlStr = [FBXPath xmlStringWithRootElement:wrappedSnapshot - options:nil]; - int pid = [snapshot.accessibilityElement processIdentifier]; - XCTAssertNotNil(xmlStr); - NSString *expectedXml = [NSString stringWithFormat:@"\n<%@ type=\"%@\" name=\"%@\" label=\"%@\" enabled=\"%@\" visible=\"%@\" accessible=\"%@\" x=\"%@\" y=\"%@\" width=\"%@\" height=\"%@\" index=\"%lu\" traits=\"%@\" processId=\"%d\" bundleId=\"%@\"/>\n", wrappedSnapshot.wdType, wrappedSnapshot.wdType, wrappedSnapshot.wdName, wrappedSnapshot.wdLabel, FBBoolToString(wrappedSnapshot.wdEnabled), FBBoolToString(wrappedSnapshot.wdVisible), FBBoolToString(wrappedSnapshot.wdAccessible), [wrappedSnapshot.wdRect[@"x"] stringValue], [wrappedSnapshot.wdRect[@"y"] stringValue], [wrappedSnapshot.wdRect[@"width"] stringValue], [wrappedSnapshot.wdRect[@"height"] stringValue], wrappedSnapshot.wdIndex, wrappedSnapshot.wdTraits, pid, [self.testedApplication bundleID]]; - XCTAssertEqualObjects(xmlStr, expectedXml); -} - -- (void)testSingleDescendantXMLRepresentation -{ - id snapshot = self.destinationSnapshot; - FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:snapshot]; - NSString *xmlStr = [FBXPath xmlStringWithRootElement:wrappedSnapshot - options:nil]; - XCTAssertNotNil(xmlStr); - NSString *expectedXml = [NSString stringWithFormat:@"\n<%@ type=\"%@\" name=\"%@\" label=\"%@\" enabled=\"%@\" visible=\"%@\" accessible=\"%@\" x=\"%@\" y=\"%@\" width=\"%@\" height=\"%@\" index=\"%lu\" traits=\"%@\"/>\n", wrappedSnapshot.wdType, wrappedSnapshot.wdType, wrappedSnapshot.wdName, wrappedSnapshot.wdLabel, FBBoolToString(wrappedSnapshot.wdEnabled), FBBoolToString(wrappedSnapshot.wdVisible), FBBoolToString(wrappedSnapshot.wdAccessible), [wrappedSnapshot.wdRect[@"x"] stringValue], [wrappedSnapshot.wdRect[@"y"] stringValue], [wrappedSnapshot.wdRect[@"width"] stringValue], [wrappedSnapshot.wdRect[@"height"] stringValue], wrappedSnapshot.wdIndex, wrappedSnapshot.wdTraits]; - XCTAssertEqualObjects(xmlStr, expectedXml); -} - -- (void)testSingleDescendantXMLRepresentationWithScope -{ - id snapshot = self.destinationSnapshot; - NSString *scope = @"AppiumAUT"; - FBXMLGenerationOptions *options = [[FBXMLGenerationOptions new] withScope:scope]; - FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:snapshot]; - NSString *xmlStr = [FBXPath xmlStringWithRootElement:wrappedSnapshot - options:options]; - XCTAssertNotNil(xmlStr); - NSString *expectedXml = [NSString stringWithFormat:@"\n<%@>\n <%@ type=\"%@\" name=\"%@\" label=\"%@\" enabled=\"%@\" visible=\"%@\" accessible=\"%@\" x=\"%@\" y=\"%@\" width=\"%@\" height=\"%@\" index=\"%lu\" traits=\"%@\"/>\n\n", scope, wrappedSnapshot.wdType, wrappedSnapshot.wdType, wrappedSnapshot.wdName, wrappedSnapshot.wdLabel, FBBoolToString(wrappedSnapshot.wdEnabled), FBBoolToString(wrappedSnapshot.wdVisible), FBBoolToString(wrappedSnapshot.wdAccessible), [wrappedSnapshot.wdRect[@"x"] stringValue], [wrappedSnapshot.wdRect[@"y"] stringValue], [wrappedSnapshot.wdRect[@"width"] stringValue], [wrappedSnapshot.wdRect[@"height"] stringValue], wrappedSnapshot.wdIndex, wrappedSnapshot.wdTraits, scope]; - XCTAssertEqualObjects(xmlStr, expectedXml); -} - -- (void)testSingleDescendantXMLRepresentationWithoutAttributes -{ - id snapshot = self.destinationSnapshot; - FBXMLGenerationOptions *options = [[FBXMLGenerationOptions new] - withExcludedAttributes:@[@"visible", @"enabled", @"index", @"blabla"]]; - FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:snapshot]; - NSString *xmlStr = [FBXPath xmlStringWithRootElement:wrappedSnapshot - options:options]; - XCTAssertNotNil(xmlStr); - NSString *expectedXml = [NSString stringWithFormat:@"\n<%@ type=\"%@\" name=\"%@\" label=\"%@\" accessible=\"%@\" x=\"%@\" y=\"%@\" width=\"%@\" height=\"%@\" traits=\"%@\"/>\n", wrappedSnapshot.wdType, wrappedSnapshot.wdType, wrappedSnapshot.wdName, wrappedSnapshot.wdLabel, FBBoolToString(wrappedSnapshot.wdAccessible), [wrappedSnapshot.wdRect[@"x"] stringValue], [wrappedSnapshot.wdRect[@"y"] stringValue], [wrappedSnapshot.wdRect[@"width"] stringValue], [wrappedSnapshot.wdRect[@"height"] stringValue], wrappedSnapshot.wdTraits]; - XCTAssertEqualObjects(xmlStr, expectedXml); -} - -- (void)testFindMatchesInElement -{ - NSArray> *matchingSnapshots = [FBXPath matchesWithRootElement:self.testedApplication forQuery:@"//XCUIElementTypeButton"]; - XCTAssertEqual([matchingSnapshots count], 5); - for (id element in matchingSnapshots) { - XCTAssertTrue([[FBXCElementSnapshotWrapper ensureWrapped:element].wdType isEqualToString:@"XCUIElementTypeButton"]); - } -} - -- (void)testFindMatchesWithoutContextScopeLimit -{ - XCUIElement *button = self.testedApplication.buttons.firstMatch; - BOOL previousValue = FBConfiguration.limitXpathContextScope; - FBConfiguration.limitXpathContextScope = NO; - @try { - NSArray *parentSnapshots = [FBXPath matchesWithRootElement:button forQuery:@".."]; - XCTAssertEqual(parentSnapshots.count, 1); - XCTAssertEqualObjects( - [FBXCElementSnapshotWrapper ensureWrapped:[parentSnapshots objectAtIndex:0]].wdLabel, - @"MainView" - ); - NSArray *elements = [button.application fb_filterDescendantsWithSnapshots:parentSnapshots onlyChildren:NO]; - XCTAssertEqual(elements.count, 1); - XCTAssertEqualObjects( - [[elements objectAtIndex:0] wdLabel], - @"MainView" - ); - NSArray *currentSnapshots = [FBXPath matchesWithRootElement:button forQuery:@"."]; - XCTAssertEqual(currentSnapshots.count, 1); - XCTAssertEqualObjects( - [FBXCElementSnapshotWrapper ensureWrapped:[currentSnapshots objectAtIndex:0]].wdType, - @"XCUIElementTypeButton" - ); - NSArray *currentElements = [button.application fb_filterDescendantsWithSnapshots:currentSnapshots onlyChildren:NO]; - XCTAssertEqual(currentElements.count, 1); - XCTAssertEqualObjects( - [[currentElements objectAtIndex:0] wdType], - @"XCUIElementTypeButton" - ); - } @finally { - FBConfiguration.limitXpathContextScope = previousValue; - } -} - -- (void)testFindMatchesInElementWithDotNotation -{ - NSArray> *matchingSnapshots = [FBXPath matchesWithRootElement:self.testedApplication forQuery:@".//XCUIElementTypeButton"]; - XCTAssertEqual([matchingSnapshots count], 5); - for (id element in matchingSnapshots) { - XCTAssertTrue([[FBXCElementSnapshotWrapper ensureWrapped:element].wdType isEqualToString:@"XCUIElementTypeButton"]); - } -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/Info.plist b/WebDriverAgent/WebDriverAgentTests/IntegrationTests/Info.plist deleted file mode 100644 index 6104b22a08..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/Info.plist +++ /dev/null @@ -1,24 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - com.facebook.wda.integrationTests - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1 - - diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/XCElementSnapshotHelperTests.m b/WebDriverAgent/WebDriverAgentTests/IntegrationTests/XCElementSnapshotHelperTests.m deleted file mode 100644 index 3258f6bbfe..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/XCElementSnapshotHelperTests.m +++ /dev/null @@ -1,209 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBIntegrationTestCase.h" -#import "FBTestMacros.h" -#import "XCUIElement.h" -#import "XCUIElement+FBIsVisible.h" -#import "XCUIElement+FBUtilities.h" -#import "XCUIElement+FBWebDriverAttributes.h" -#import "FBXCElementSnapshotWrapper+Helpers.h" -#import "FBXCodeCompatibility.h" - -@interface XCElementSnapshotHelperTests : FBIntegrationTestCase -@property (nonatomic, strong) XCUIElement *testedView; -@end - -@implementation XCElementSnapshotHelperTests - -- (void)setUp -{ - [super setUp]; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self launchApplication]; - }); - self.testedView = self.testedApplication.otherElements[@"MainView"]; - XCTAssertTrue(self.testedView.exists); -} - -- (void)testDescendantsMatchingType -{ - NSSet *expectedLabels = [NSSet setWithArray:@[ - @"Alerts", - @"Attributes", - @"Scrolling", - @"Deadlock app", - @"Touch", - ]]; - NSArray> *matchingSnapshots = [[FBXCElementSnapshotWrapper ensureWrapped: - [self.testedView fb_customSnapshot]] - fb_descendantsMatchingType:XCUIElementTypeButton]; - XCTAssertEqual(matchingSnapshots.count, expectedLabels.count); - NSArray *labels = [matchingSnapshots valueForKeyPath:@"@distinctUnionOfObjects.label"]; - XCTAssertEqualObjects([NSSet setWithArray:labels], expectedLabels); - - NSArray *types = [matchingSnapshots valueForKeyPath:@"@distinctUnionOfObjects.elementType"]; - XCTAssertEqual(types.count, 1, @"matchingSnapshots should contain only one type"); - XCTAssertEqualObjects(types.lastObject, @(XCUIElementTypeButton), @"matchingSnapshots should contain only one type"); -} - -- (void)testParentMatchingType -{ - XCUIElement *button = self.testedApplication.buttons[@"Alerts"]; - FBAssertWaitTillBecomesTrue(button.exists); - id windowSnapshot = [[FBXCElementSnapshotWrapper ensureWrapped: - [self.testedView fb_customSnapshot]] - fb_parentMatchingType:XCUIElementTypeWindow]; - XCTAssertNotNil(windowSnapshot); - XCTAssertEqual(windowSnapshot.elementType, XCUIElementTypeWindow); -} - -@end - -@interface XCElementSnapshotHelperTests_AttributePage : FBIntegrationTestCase -@end - -@implementation XCElementSnapshotHelperTests_AttributePage - -- (void)setUp -{ - [super setUp]; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self launchApplication]; - [self goToAttributesPage]; - }); -} - -- (void)testParentMatchingOneOfTypes -{ - XCUIElement *todayPickerWheel = self.testedApplication.pickerWheels[@"Today"]; - FBAssertWaitTillBecomesTrue(todayPickerWheel.exists); - id datePicker = [[FBXCElementSnapshotWrapper ensureWrapped: - [todayPickerWheel fb_customSnapshot]] - fb_parentMatchingOneOfTypes:@[@(XCUIElementTypeDatePicker), @(XCUIElementTypeWindow)]]; - XCTAssertNotNil(datePicker); - XCTAssertEqual(datePicker.elementType, XCUIElementTypeDatePicker); -} - -- (void)testParentMatchingOneOfTypesWithXCUIElementTypeAny -{ - XCUIElement *todayPickerWheel = self.testedApplication.pickerWheels[@"Today"]; - FBAssertWaitTillBecomesTrue(todayPickerWheel.exists); - id otherSnapshot =[[FBXCElementSnapshotWrapper ensureWrapped: - [todayPickerWheel fb_customSnapshot]] - fb_parentMatchingOneOfTypes:@[@(XCUIElementTypeAny)]]; - XCTAssertNotNil(otherSnapshot); -} - -- (void)testParentMatchingOneOfTypesWithAbsentParents -{ - XCUIElement *todayPickerWheel = self.testedApplication.pickerWheels[@"Today"]; - FBAssertWaitTillBecomesTrue(todayPickerWheel.exists); - id otherSnapshot = [[FBXCElementSnapshotWrapper ensureWrapped: - [todayPickerWheel fb_customSnapshot]] - fb_parentMatchingOneOfTypes:@[@(XCUIElementTypeTab), @(XCUIElementTypeLink)]]; - XCTAssertNil(otherSnapshot); -} - -@end - -@interface XCElementSnapshotHelperTests_ScrollView : FBIntegrationTestCase -@end - -@implementation XCElementSnapshotHelperTests_ScrollView - -- (void)setUp -{ - [super setUp]; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self launchApplication]; - [self goToScrollPageWithCells:false]; - }); -} - -- (void)testParentMatchingOneOfTypesWithFilter -{ - XCUIElement *threeStaticText = self.testedApplication.staticTexts[@"3"]; - FBAssertWaitTillBecomesTrue(threeStaticText.exists); - NSArray *acceptedParents = @[ - @(XCUIElementTypeScrollView), - @(XCUIElementTypeCollectionView), - @(XCUIElementTypeTable), - ]; - id scrollView = [[FBXCElementSnapshotWrapper ensureWrapped: - [threeStaticText fb_customSnapshot]] - fb_parentMatchingOneOfTypes:acceptedParents - filter:^BOOL(id snapshot) { - return [[FBXCElementSnapshotWrapper ensureWrapped:snapshot] isWDVisible]; - }]; - XCTAssertEqualObjects(scrollView.identifier, @"scrollView"); -} - -- (void)testParentMatchingOneOfTypesWithFilterRetruningNo -{ - XCUIElement *threeStaticText = self.testedApplication.staticTexts[@"3"]; - FBAssertWaitTillBecomesTrue(threeStaticText.exists); - NSArray *acceptedParents = @[ - @(XCUIElementTypeScrollView), - @(XCUIElementTypeCollectionView), - @(XCUIElementTypeTable), - ]; - id scrollView = [[FBXCElementSnapshotWrapper ensureWrapped: - [threeStaticText fb_customSnapshot]] - fb_parentMatchingOneOfTypes:acceptedParents - filter:^BOOL(id snapshot) { - return NO; - }]; - XCTAssertNil(scrollView); -} - -- (void)testDescendantsCellSnapshots -{ - XCUIElement *scrollView = self.testedApplication.scrollViews[@"scrollView"]; - FBAssertWaitTillBecomesTrue(self.testedApplication.staticTexts[@"3"].fb_isVisible); - NSArray *cells = [[FBXCElementSnapshotWrapper ensureWrapped: - [scrollView fb_customSnapshot]] - fb_descendantsCellSnapshots]; - XCTAssertGreaterThanOrEqual(cells.count, 10); - id element = cells.firstObject; - XCTAssertEqualObjects(element.label, @"0"); -} - -@end - -@interface XCElementSnapshotHelperTests_ScrollViewCells : FBIntegrationTestCase -@end - -@implementation XCElementSnapshotHelperTests_ScrollViewCells - -- (void)setUp -{ - [super setUp]; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self launchApplication]; - [self goToScrollPageWithCells:true]; - }); -} - -- (void)testParentCellSnapshot -{ - FBAssertWaitTillBecomesTrue(self.testedApplication.staticTexts[@"3"].fb_isVisible); - XCUIElement *threeStaticText = self.testedApplication.staticTexts[@"3"]; - id xcuiElementCell = [[FBXCElementSnapshotWrapper ensureWrapped: - [threeStaticText fb_customSnapshot]] - fb_parentCellSnapshot]; - XCTAssertEqual(xcuiElementCell.elementType, 75); -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/XCElementSnapshotHitPointTests.m b/WebDriverAgent/WebDriverAgentTests/IntegrationTests/XCElementSnapshotHitPointTests.m deleted file mode 100644 index 453d0aea7e..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/XCElementSnapshotHitPointTests.m +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import "FBIntegrationTestCase.h" -#import "FBTestMacros.h" -#import "FBXCElementSnapshotWrapper+Helpers.h" -#import "XCUIElement.h" -#import "XCUIElement+FBUtilities.h" - -@interface XCElementSnapshotHitPoint : FBIntegrationTestCase -@end - -@implementation XCElementSnapshotHitPoint - -- (void)testAccessibilityActivationPoint -{ - [self launchApplication]; - [self goToAttributesPage]; - XCUIElement *dstBtn = self.testedApplication.buttons[@"not_accessible"]; - CGPoint hitPoint = [FBXCElementSnapshotWrapper - ensureWrapped:[dstBtn fb_standardSnapshot]].fb_hitPoint.CGPointValue; - XCTAssertTrue(hitPoint.x > 0 && hitPoint.y > 0); -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/XCUIApplicationHelperTests.m b/WebDriverAgent/WebDriverAgentTests/IntegrationTests/XCUIApplicationHelperTests.m deleted file mode 100644 index 3d78a0d8c7..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/XCUIApplicationHelperTests.m +++ /dev/null @@ -1,163 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import - -#import "FBIntegrationTestCase.h" -#import "FBElement.h" -#import "FBMacros.h" -#import "FBTestMacros.h" -#import "XCUIApplication.h" -#import "XCUIApplication+FBHelpers.h" -#import "XCUIElement+FBIsVisible.h" -#import "FBXCodeCompatibility.h" - -void calculateMaxTreeDepth(NSDictionary *tree, NSNumber *currentDepth, NSNumber** maxDepth) { - if (nil == maxDepth) { - return; - } - - NSArray *children = tree[@"children"]; - if (nil == children || 0 == children.count) { - return; - } - for (NSDictionary *child in children) { - if (currentDepth.integerValue > [*maxDepth integerValue]) { - *maxDepth = currentDepth; - } - calculateMaxTreeDepth(child, @(currentDepth.integerValue + 1), maxDepth); - } -} - -@interface XCUIApplicationHelperTests : FBIntegrationTestCase -@end - -@implementation XCUIApplicationHelperTests - -- (void)setUp -{ - [super setUp]; - [self launchApplication]; -} - -- (void)testQueringSpringboard -{ - [self goToSpringBoardFirstPage]; - XCTAssertTrue(XCUIApplication.fb_systemApplication.icons[@"Safari"].exists); - XCTAssertTrue(XCUIApplication.fb_systemApplication.icons[@"Calendar"].firstMatch.exists); -} - -- (void)testApplicationTree -{ - NSDictionary *tree = self.testedApplication.fb_tree; - XCTAssertNotNil(tree); - NSNumber *maxDepth; - calculateMaxTreeDepth(tree, @0, &maxDepth); - XCTAssertGreaterThan(maxDepth.integerValue, 3); - XCTAssertNotNil(self.testedApplication.fb_accessibilityTree); -} - -- (void)testApplicationTreeAttributesFiltering -{ - NSDictionary *applicationTree = [self.testedApplication fb_tree:[NSSet setWithArray:@[@"visible"]]]; - XCTAssertNotNil(applicationTree); - XCTAssertNil([applicationTree objectForKey:@"isVisible"], @"'isVisible' key should not be present in the application tree"); -} - -- (void)testDeactivateApplication -{ - NSError *error; - uint64_t timeStarted = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW); - NSTimeInterval backgroundDuration = 5.0; - XCTAssertTrue([self.testedApplication fb_deactivateWithDuration:backgroundDuration error:&error]); - NSTimeInterval timeElapsed = (clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW) - timeStarted) / NSEC_PER_SEC; - XCTAssertNil(error); - XCTAssertEqualWithAccuracy(timeElapsed, backgroundDuration, 3.0); - XCTAssertTrue(self.testedApplication.buttons[@"Alerts"].exists); -} - -- (void)testActiveApplication -{ - XCUIApplication *systemApp = XCUIApplication.fb_systemApplication; - XCTAssertTrue([XCUIApplication fb_activeApplication].buttons[@"Alerts"].fb_isVisible); - [self goToSpringBoardFirstPage]; - XCTAssertEqualObjects([XCUIApplication fb_activeApplication].bundleID, systemApp.bundleID); - XCTAssertTrue(systemApp.icons[@"Safari"].fb_isVisible); -} - -- (void)testActiveElement -{ - [self goToAttributesPage]; - XCTAssertNil(self.testedApplication.fb_activeElement); - XCUIElement *textField = self.testedApplication.textFields[@"aIdentifier"]; - [textField tap]; - FBAssertWaitTillBecomesTrue(nil != self.testedApplication.fb_activeElement); - XCTAssertEqualObjects(((id)self.testedApplication.fb_activeElement).wdUID, - ((id)textField).wdUID); -} - -- (void)testActiveApplicationsInfo -{ - NSArray *appsInfo = [XCUIApplication fb_activeAppsInfo]; - XCTAssertTrue(appsInfo.count > 0); - BOOL isAppActive = NO; - for (NSDictionary *appInfo in appsInfo) { - if ([appInfo[@"bundleId"] isEqualToString:self.testedApplication.bundleID]) { - isAppActive = YES; - break; - } - } - XCTAssertTrue(isAppActive); -} - -- (void)testTestmanagerdVersion -{ - XCTAssertGreaterThan(FBTestmanagerdVersion(), 0); -} - -- (void)testAccessbilityAudit -{ - if (SYSTEM_VERSION_LESS_THAN(@"17.0")) { - return; - } - - NSError *error; - NSArray *auditIssues1 = [XCUIApplication.fb_activeApplication fb_performAccessibilityAuditWithAuditTypes:~0UL - error:&error]; - XCTAssertNotNil(auditIssues1); - XCTAssertNil(error); - - NSMutableSet *set = [NSMutableSet new]; - [set addObject:@"XCUIAccessibilityAuditTypeAll"]; - NSArray *auditIssues2 = [XCUIApplication.fb_activeApplication fb_performAccessibilityAuditWithAuditTypesSet:set.copy - error:&error]; - // 'elementDescription' is not in this list because it could have - // different object id's debug description in XCTest. - NSArray *checkKeys = @[ - @"auditType", - @"compactDescription", - @"detailedDescription", - @"element", - @"elementAttributes" - ]; - - XCTAssertEqual([auditIssues1 count], [auditIssues2 count]); - for (int i = 1; i < [auditIssues1 count]; i++) { - for (NSString *k in checkKeys) { - XCTAssertEqualObjects( - [auditIssues1[i] objectForKey:k], - [auditIssues2[i] objectForKey:k] - ); - } - } - XCTAssertNil(error); -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/XCUIDeviceHealthCheckTests.m b/WebDriverAgent/WebDriverAgentTests/IntegrationTests/XCUIDeviceHealthCheckTests.m deleted file mode 100644 index dfbd6f88d2..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/XCUIDeviceHealthCheckTests.m +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBIntegrationTestCase.h" -#import "XCUIDevice+FBHealthCheck.h" -#import "XCUIElement.h" - -@interface XCUIDeviceHealthCheckTests : FBIntegrationTestCase -@end - -@implementation XCUIDeviceHealthCheckTests - -- (void)testHealthCheck -{ - [self launchApplication]; - XCTAssertTrue(self.testedApplication.exists); - XCTAssertTrue([[XCUIDevice sharedDevice] fb_healthCheckWithApplication:self.testedApplication]); -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/XCUIDeviceHelperTests.m b/WebDriverAgent/WebDriverAgentTests/IntegrationTests/XCUIDeviceHelperTests.m deleted file mode 100644 index 97908add4b..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/XCUIDeviceHelperTests.m +++ /dev/null @@ -1,220 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBIntegrationTestCase.h" -#import "FBImageUtils.h" -#import "FBMacros.h" -#import "FBTestMacros.h" -#import "XCUIApplication.h" -#import "XCUIApplication+FBHelpers.h" -#import "XCUIDevice+FBHelpers.h" -#import "XCUIDevice+FBRotation.h" -#import "XCUIScreen.h" - -@interface XCUIDeviceHelperTests : FBIntegrationTestCase -@end - -@implementation XCUIDeviceHelperTests - -- (void)restorePortraitOrientation -{ - if ([XCUIDevice sharedDevice].orientation != UIDeviceOrientationPortrait) { - [[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:UIDeviceOrientationPortrait]; - } -} - -- (void)setUp -{ - [super setUp]; - [self launchApplication]; - [self restorePortraitOrientation]; -} - -- (void)tearDown -{ - [self restorePortraitOrientation]; - [super tearDown]; -} - -- (void)testScreenshot -{ - NSError *error = nil; - NSData *screenshotData = [[XCUIDevice sharedDevice] fb_screenshotWithError:&error]; - XCTAssertNotNil(screenshotData); - XCTAssertNil(error); - XCTAssertTrue(FBIsPngImage(screenshotData)); - - UIImage *screenshot = [UIImage imageWithData:screenshotData]; - XCTAssertNotNil(screenshot); - - XCUIScreen *mainScreen = XCUIScreen.mainScreen; - UIImage *screenshotExact = ((XCUIScreenshot *)mainScreen.screenshot).image; - XCTAssertEqualWithAccuracy(screenshotExact.size.height * mainScreen.scale, - screenshot.size.height, - FLT_EPSILON); - XCTAssertEqualWithAccuracy(screenshotExact.size.width * mainScreen.scale, - screenshot.size.width, - FLT_EPSILON); -} - -- (void)testLandscapeScreenshot -{ - XCTAssertTrue([[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:UIDeviceOrientationLandscapeLeft]); - NSError *error = nil; - NSData *screenshotData = [[XCUIDevice sharedDevice] fb_screenshotWithError:&error]; - XCTAssertNotNil(screenshotData); - XCTAssertTrue(FBIsPngImage(screenshotData)); - XCTAssertNil(error); - - UIImage *screenshot = [UIImage imageWithData:screenshotData]; - XCTAssertNotNil(screenshot); - XCTAssertTrue(screenshot.size.width > screenshot.size.height); - - XCUIScreen *mainScreen = XCUIScreen.mainScreen; - UIImage *screenshotExact = ((XCUIScreenshot *)mainScreen.screenshot).image; - CGSize realMainScreenSize = screenshotExact.size.height > screenshot.size.width - ? CGSizeMake(screenshotExact.size.height * mainScreen.scale, screenshotExact.size.width * mainScreen.scale) - : CGSizeMake(screenshotExact.size.width * mainScreen.scale, screenshotExact.size.height * mainScreen.scale); - XCTAssertEqualWithAccuracy(realMainScreenSize.height, screenshot.size.height, FLT_EPSILON); - XCTAssertEqualWithAccuracy(realMainScreenSize.width, screenshot.size.width, FLT_EPSILON); -} - -- (void)testWifiAddress -{ - NSString *adderss = [XCUIDevice sharedDevice].fb_wifiIPAddress; - if (!adderss) { - return; - } - NSRange range = [adderss rangeOfString:@"^([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})" options:NSRegularExpressionSearch]; - XCTAssertTrue(range.location != NSNotFound); -} - -- (void)testGoToHomeScreen -{ - NSError *error; - XCTAssertTrue([[XCUIDevice sharedDevice] fb_goToHomescreenWithError:&error]); - XCTAssertNil(error); - FBAssertWaitTillBecomesTrue([XCUIApplication fb_activeApplication].icons[@"Safari"].exists); -} - -- (void)testLockUnlockScreen -{ - XCTAssertFalse([[XCUIDevice sharedDevice] fb_isScreenLocked]); - NSError *error; - XCTAssertTrue([[XCUIDevice sharedDevice] fb_lockScreen:&error]); - XCTAssertTrue([[XCUIDevice sharedDevice] fb_isScreenLocked]); - XCTAssertNil(error); - XCTAssertTrue([[XCUIDevice sharedDevice] fb_unlockScreen:&error]); - XCTAssertFalse([[XCUIDevice sharedDevice] fb_isScreenLocked]); - XCTAssertNil(error); -} - -- (void)testUrlSchemeActivation -{ - if (SYSTEM_VERSION_LESS_THAN(@"16.4")) { - return; - } - - NSError *error; - XCTAssertTrue([XCUIDevice.sharedDevice fb_openUrl:@"https://apple.com" error:&error]); - FBAssertWaitTillBecomesTrue([XCUIApplication.fb_activeApplication.bundleID isEqualToString:@"com.apple.mobilesafari"]); - XCTAssertNil(error); -} - -- (void)testUrlSchemeActivationWithApp -{ - if (SYSTEM_VERSION_LESS_THAN(@"16.4")) { - return; - } - - NSError *error; - XCTAssertTrue([XCUIDevice.sharedDevice fb_openUrl:@"https://apple.com" - withApplication:@"com.apple.mobilesafari" - error:&error]); - FBAssertWaitTillBecomesTrue([XCUIApplication.fb_activeApplication.bundleID isEqualToString:@"com.apple.mobilesafari"]); - XCTAssertNil(error); -} - -#if !TARGET_OS_TV -- (void)testSimulatedLocationSetup -{ - if (SYSTEM_VERSION_LESS_THAN(@"16.4")) { - return; - } - - CLLocation *simulatedLocation = [[CLLocation alloc] initWithLatitude:50 longitude:50]; - NSError *error; - XCTAssertTrue([XCUIDevice.sharedDevice fb_setSimulatedLocation:simulatedLocation error:&error]); - XCTAssertNil(error); - CLLocation *currentLocation = [XCUIDevice.sharedDevice fb_getSimulatedLocation:&error]; - XCTAssertNil(error); - XCTAssertNotNil(currentLocation); - XCTAssertEqualWithAccuracy(simulatedLocation.coordinate.latitude, currentLocation.coordinate.latitude, 0.1); - XCTAssertEqualWithAccuracy(simulatedLocation.coordinate.longitude, currentLocation.coordinate.longitude, 0.1); - XCTAssertTrue([XCUIDevice.sharedDevice fb_clearSimulatedLocation:&error]); - XCTAssertNil(error); - currentLocation = [XCUIDevice.sharedDevice fb_getSimulatedLocation:&error]; - XCTAssertNil(error); - XCTAssertNotEqualWithAccuracy(simulatedLocation.coordinate.latitude, currentLocation.coordinate.latitude, 0.1); - XCTAssertNotEqualWithAccuracy(simulatedLocation.coordinate.longitude, currentLocation.coordinate.longitude, 0.1); -} -#endif - -- (void)testPressingUnsupportedButton -{ - NSError *error; - NSNumber *duration = nil; - XCTAssertFalse([XCUIDevice.sharedDevice fb_pressButton:@"volumeUpp" - forDuration:duration - error:&error]); - XCTAssertNotNil(error); -} - -- (void)testPressingSupportedButton -{ - NSError *error; - XCTAssertTrue([XCUIDevice.sharedDevice fb_pressButton:@"home" - forDuration:nil - error:&error]); - XCTAssertNil(error); -} - -- (void)testPressingSupportedButtonNumber -{ - NSError *error; - XCTAssertTrue([XCUIDevice.sharedDevice fb_pressButton:@"home" - forDuration:[NSNumber numberWithDouble:1.0] - error:&error]); - XCTAssertNil(error); -} - -- (void)testLongPressHomeButton -{ - NSError *error; - // kHIDPage_Consumer = 0x0C - // kHIDUsage_Csmr_Menu = 0x40 - XCTAssertTrue([XCUIDevice.sharedDevice fb_performIOHIDEventWithPage:0x0C - usage:0x40 - duration:1.0 - error:&error]); - XCTAssertNil(error); -} - -- (void)testAppearance -{ - if (SYSTEM_VERSION_LESS_THAN(@"15.0")) { - return; - } - NSError *error; - XCTAssertTrue([XCUIDevice.sharedDevice fb_setAppearance:FBUIInterfaceAppearanceDark error:&error]); - XCTAssertNil(error); -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/XCUIDeviceRotationTests.m b/WebDriverAgent/WebDriverAgentTests/IntegrationTests/XCUIDeviceRotationTests.m deleted file mode 100644 index 608798010d..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/XCUIDeviceRotationTests.m +++ /dev/null @@ -1,84 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import -#import -#import "FBIntegrationTestCase.h" -#import "XCUIDevice+FBRotation.h" - -@interface XCUIDeviceRotationTests : FBIntegrationTestCase - -@end - -@implementation XCUIDeviceRotationTests - -- (void)setUp -{ - [super setUp]; - [self launchApplication]; -} - -- (void)tearDown -{ - [self resetOrientation]; - [super tearDown]; -} - -- (void)testLandscapeRightOrientation -{ - BOOL success = [[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:UIDeviceOrientationLandscapeRight]; - XCTAssertTrue(success, @"Device should support LandscapeRight"); - // Device rotation gives opposite interface rotation - XCTAssertTrue(self.testedApplication.staticTexts[@"LandscapeLeft"].exists); -} - -- (void)testLandscapeLeftOrientation -{ - BOOL success = [[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:UIDeviceOrientationLandscapeLeft]; - XCTAssertTrue(success, @"Device should support LandscapeLeft"); - // Device rotation gives opposite interface rotation - XCTAssertTrue(self.testedApplication.staticTexts[@"LandscapeRight"].exists); -} - -- (void)testLandscapeRightRotation -{ - BOOL success = [[XCUIDevice sharedDevice] fb_setDeviceRotation:@{ - @"x" : @(0), - @"y" : @(0), - @"z" : @(90) - }]; - XCTAssertTrue(success, @"Device should support LandscapeRight"); - // Device rotation gives opposite interface rotation - XCTAssertTrue(self.testedApplication.staticTexts[@"LandscapeLeft"].exists); -} - -- (void)testLandscapeLeftRotation -{ - BOOL success = [[XCUIDevice sharedDevice] fb_setDeviceRotation:@{ - @"x" : @(0), - @"y" : @(0), - @"z" : @(270) - }]; - XCTAssertTrue(success, @"Device should support LandscapeLeft"); - // Device rotation gives opposite interface rotation - XCTAssertTrue(self.testedApplication.staticTexts[@"LandscapeRight"].exists); -} - -- (void)testRotationTiltRotation -{ - UIDeviceOrientation currentRotation = [XCUIDevice sharedDevice].orientation; - BOOL success = [[XCUIDevice sharedDevice] fb_setDeviceRotation:@{ - @"x" : @(15), - @"y" : @(0), - @"z" : @(0)} - ]; - XCTAssertFalse(success, @"Device should not support tilt"); - XCTAssertEqual(currentRotation, [XCUIDevice sharedDevice].orientation, @"Device doesnt support tilt, should be at previous orientation"); -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/XCUIElementAttributesTests.m b/WebDriverAgent/WebDriverAgentTests/IntegrationTests/XCUIElementAttributesTests.m deleted file mode 100644 index 26773b3fd8..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/XCUIElementAttributesTests.m +++ /dev/null @@ -1,248 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBIntegrationTestCase.h" -#import "FBTestMacros.h" -#import "XCUIElement+FBWebDriverAttributes.h" -#import "XCUIElement+FBFind.h" -#import "FBElementUtils.h" -#import "FBConfiguration.h" -#import "FBResponsePayload.h" -#import "FBXCodeCompatibility.h" - -@interface XCUIElementAttributesTests : FBIntegrationTestCase -@property (nonatomic, strong) XCUIElement *matchingElement; -@end - -@implementation XCUIElementAttributesTests - -- (void)setUp -{ - [super setUp]; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self launchApplication]; - }); - XCUIElement *testedView = self.testedApplication.otherElements[@"MainView"]; - FBAssertWaitTillBecomesTrue(testedView.exists); - self.matchingElement = [[testedView fb_descendantsMatchingIdentifier:@"Alerts" shouldReturnAfterFirstMatch:YES] firstObject]; - XCTAssertNotNil(self.matchingElement); -} - -- (void)verifyGettingAttributeWithShortcut:(NSString *)shortcutName expectedValue:(id)expectedValue -{ - NSString *fullAttributeName = [NSString stringWithFormat:@"wd%@", [NSString stringWithFormat:@"%@%@", [[shortcutName substringToIndex:1] uppercaseString], [shortcutName substringFromIndex:1]]]; - id actualValue = [self.matchingElement fb_valueForWDAttributeName:fullAttributeName]; - id actualShortcutValue = [self.matchingElement fb_valueForWDAttributeName:shortcutName]; - if (nil == expectedValue) { - XCTAssertNil(actualValue); - XCTAssertNil(actualShortcutValue); - return; - } - if ([actualValue isKindOfClass:NSString.class]) { - XCTAssertTrue([actualValue isEqualToString:expectedValue]); - XCTAssertTrue([actualShortcutValue isEqualToString:expectedValue]); - } else if ([actualValue isKindOfClass:NSNumber.class]) { - XCTAssertTrue([actualValue isEqualToNumber:expectedValue]); - XCTAssertTrue([actualShortcutValue isEqualToNumber:expectedValue]); - } else { - XCTAssertEqual(actualValue, expectedValue); - XCTAssertEqual(actualShortcutValue, expectedValue); - } -} - -- (void)testGetNameAttribute -{ - [self verifyGettingAttributeWithShortcut:@"name" expectedValue:self.matchingElement.wdName]; -} - -- (void)testGetValueAttribute -{ - [self verifyGettingAttributeWithShortcut:@"value" expectedValue:self.matchingElement.wdValue]; -} - -- (void)testGetLabelAttribute -{ - [self verifyGettingAttributeWithShortcut:@"label" expectedValue:self.matchingElement.wdLabel]; -} - -- (void)testGetTypeAttribute -{ - [self verifyGettingAttributeWithShortcut:@"type" expectedValue:self.matchingElement.wdType]; -} - -- (void)testGetRectAttribute -{ - NSString *shortcutName = @"rect"; - for (NSString *key in @[@"x", @"y", @"width", @"height"]) { - NSNumber *actualValue = [self.matchingElement fb_valueForWDAttributeName:[FBElementUtils wdAttributeNameForAttributeName:shortcutName]][key]; - NSNumber *actualShortcutValue = [self.matchingElement fb_valueForWDAttributeName:shortcutName][key]; - NSNumber *expectedValue = self.matchingElement.wdRect[key]; - XCTAssertTrue([actualValue isEqualToNumber:expectedValue]); - XCTAssertTrue([actualShortcutValue isEqualToNumber:expectedValue]); - } -} - -- (void)testGetEnabledAttribute -{ - [self verifyGettingAttributeWithShortcut:@"enabled" expectedValue:[NSNumber numberWithBool:self.matchingElement.wdEnabled]]; -} - -- (void)testGetAccessibleAttribute -{ - [self verifyGettingAttributeWithShortcut:@"accessible" expectedValue:[NSNumber numberWithBool:self.matchingElement.wdAccessible]]; -} - -- (void)testGetUidAttribute -{ - [self verifyGettingAttributeWithShortcut:@"UID" expectedValue:self.matchingElement.wdUID]; -} - -- (void)testGetVisibleAttribute -{ - [self verifyGettingAttributeWithShortcut:@"visible" expectedValue:[NSNumber numberWithBool:self.matchingElement.wdVisible]]; -} - -- (void)testGetAccessibilityContainerAttribute -{ - [self verifyGettingAttributeWithShortcut:@"accessibilityContainer" expectedValue:[NSNumber numberWithBool:self.matchingElement.wdAccessibilityContainer]]; -} - -- (void)testGetInvalidAttribute -{ - XCTAssertThrowsSpecificNamed([self verifyGettingAttributeWithShortcut:@"invalid" expectedValue:@"blabla"], NSException, FBUnknownAttributeException); -} - -@end - -@interface XCUIElementFBFindTests_CompactResponses : FBIntegrationTestCase -@end - - -@implementation XCUIElementFBFindTests_CompactResponses - -- (void)setUp -{ - [super setUp]; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self launchApplication]; - }); -} - -- (void)testCompactResponseYes -{ - XCUIElement *alertsButton = self.testedApplication.buttons[@"Alerts"]; - NSDictionary *fields = FBDictionaryResponseWithElement(alertsButton, YES); - XCTAssertNotNil(fields[@"ELEMENT"]); - XCTAssertNotNil(fields[@"element-6066-11e4-a52e-4f735466cecf"]); - XCTAssertEqual(fields.count, 2); -} - -- (void)testCompactResponseNo -{ - XCUIElement *alertsButton = self.testedApplication.buttons[@"Alerts"]; - NSDictionary *fields = FBDictionaryResponseWithElement(alertsButton, NO); - XCTAssertNotNil(fields[@"ELEMENT"]); - XCTAssertNotNil(fields[@"element-6066-11e4-a52e-4f735466cecf"]); - XCTAssertEqualObjects(fields[@"type"], @"XCUIElementTypeButton"); - XCTAssertEqualObjects(fields[@"label"], @"Alerts"); - XCTAssertEqual(fields.count, 4); -} - -@end - - -@interface XCUIElementFBFindTests_ResponseFields : FBIntegrationTestCase -@end - -@implementation XCUIElementFBFindTests_ResponseFields - -- (void)setUp -{ - [super setUp]; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self launchApplication]; - }); -} - -- (void)testCompactResponseYesWithResponseAttributesSet -{ - [FBConfiguration setElementResponseAttributes:@"name,text,enabled"]; - XCUIElement *alertsButton = self.testedApplication.buttons[@"Alerts"]; - NSDictionary *fields = FBDictionaryResponseWithElement(alertsButton, YES); - XCTAssertNotNil(fields[@"ELEMENT"]); - XCTAssertNotNil(fields[@"element-6066-11e4-a52e-4f735466cecf"]); - XCTAssertEqual(fields.count, 2); -} - -- (void)testCompactResponseNoWithResponseAttributesSet -{ - [FBConfiguration setElementResponseAttributes:@"name,text,enabled"]; - XCUIElement *alertsButton = self.testedApplication.buttons[@"Alerts"]; - NSDictionary *fields = FBDictionaryResponseWithElement(alertsButton, NO); - XCTAssertNotNil(fields[@"ELEMENT"]); - XCTAssertNotNil(fields[@"element-6066-11e4-a52e-4f735466cecf"]); - XCTAssertEqualObjects(fields[@"name"], @"XCUIElementTypeButton"); - XCTAssertEqualObjects(fields[@"text"], @"Alerts"); - XCTAssertEqualObjects(fields[@"enabled"], @(YES)); - XCTAssertEqual(fields.count, 5); -} - -- (void)testInvalidAttribute -{ - [FBConfiguration setElementResponseAttributes:@"invalid_field,name"]; - XCUIElement *alertsButton = self.testedApplication.buttons[@"Alerts"]; - NSDictionary *fields = FBDictionaryResponseWithElement(alertsButton, NO); - XCTAssertNotNil(fields[@"ELEMENT"]); - XCTAssertNotNil(fields[@"element-6066-11e4-a52e-4f735466cecf"]); - XCTAssertEqualObjects(fields[@"name"], @"XCUIElementTypeButton"); - XCTAssertEqual(fields.count, 3); -} - -- (void)testKnownAttributes -{ - [FBConfiguration setElementResponseAttributes:@"name,type,label,text,rect,enabled,displayed,selected"]; - XCUIElement *alertsButton = self.testedApplication.buttons[@"Alerts"]; - NSDictionary *fields = FBDictionaryResponseWithElement(alertsButton, NO); - XCTAssertNotNil(fields[@"ELEMENT"]); - XCTAssertNotNil(fields[@"element-6066-11e4-a52e-4f735466cecf"]); - XCTAssertEqualObjects(fields[@"name"], @"XCUIElementTypeButton"); - XCTAssertEqualObjects(fields[@"type"], @"XCUIElementTypeButton"); - XCTAssertEqualObjects(fields[@"label"], @"Alerts"); - XCTAssertEqualObjects(fields[@"text"], @"Alerts"); - XCTAssertTrue(matchesRegex([fields[@"rect"] description], @"\\{\\s*height = [0-9]+;\\s*width = [0-9]+;\\s*x = [0-9]+;\\s*y = [0-9]+;\\s*\\}")); - XCTAssertEqualObjects(fields[@"enabled"], @(YES)); - XCTAssertEqualObjects(fields[@"displayed"], @(YES)); - XCTAssertEqualObjects(fields[@"selected"], @(NO)); - XCTAssertEqual(fields.count, 10); -} - -- (void)testArbitraryAttributes -{ - [FBConfiguration setElementResponseAttributes:@"attribute/name,attribute/value"]; - XCUIElement *alertsButton = self.testedApplication.buttons[@"Alerts"]; - NSDictionary *fields = FBDictionaryResponseWithElement(alertsButton, NO); - XCTAssertNotNil(fields[@"ELEMENT"]); - XCTAssertNotNil(fields[@"element-6066-11e4-a52e-4f735466cecf"]); - XCTAssertEqualObjects(fields[@"attribute/name"], @"Alerts"); - XCTAssertEqualObjects(fields[@"attribute/value"], [NSNull null]); - XCTAssertEqual(fields.count, 4); -} - -static BOOL matchesRegex(NSString *target, NSString *pattern) { - if (!target) - return NO; - NSRegularExpression* regex = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:NULL]; - return [regex numberOfMatchesInString:target options:0 range:NSMakeRange(0, target.length)] == 1; -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/XCUIElementFBFindTests.m b/WebDriverAgent/WebDriverAgentTests/IntegrationTests/XCUIElementFBFindTests.m deleted file mode 100644 index 296c89170d..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/XCUIElementFBFindTests.m +++ /dev/null @@ -1,472 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBIntegrationTestCase.h" -#import "FBElementUtils.h" -#import "FBExceptions.h" -#import "FBTestMacros.h" -#import "XCUIElement.h" -#import "XCUIElement+FBFind.h" -#import "XCUIElement+FBUID.h" -#import "FBXCElementSnapshotWrapper+Helpers.h" -#import "XCUIElement+FBIsVisible.h" -#import "XCUIElement+FBClassChain.h" -#import "XCUIElement+FBResolve.h" -#import "FBXPath.h" -#import "FBXCodeCompatibility.h" - -@interface XCUIElementFBFindTests : FBIntegrationTestCase -@property (nonatomic, strong) XCUIElement *testedView; -@end - -@implementation XCUIElementFBFindTests - -- (void)setUp -{ - [super setUp]; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self launchApplication]; - }); - self.testedView = self.testedApplication.otherElements[@"MainView"]; - FBAssertWaitTillBecomesTrue(self.testedView.exists); -} - -- (void)testDescendantsWithClassName -{ - NSSet *expectedLabels = [NSSet setWithArray:@[ - @"Alerts", - @"Attributes", - @"Scrolling", - @"Deadlock app", - @"Touch", - ]]; - NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingClassName:@"XCUIElementTypeButton" - shouldReturnAfterFirstMatch:NO]; - XCTAssertEqual(matchingSnapshots.count, expectedLabels.count); - NSArray *labels = [matchingSnapshots valueForKeyPath:@"@distinctUnionOfObjects.label"]; - XCTAssertEqualObjects([NSSet setWithArray:labels], expectedLabels); - - NSArray *types = [matchingSnapshots valueForKeyPath:@"@distinctUnionOfObjects.elementType"]; - XCTAssertEqual(types.count, 1, @"matchingSnapshots should contain only one type"); - XCTAssertEqualObjects(types.lastObject, @(XCUIElementTypeButton), @"matchingSnapshots should contain only one type"); -} - -- (void)testSingleDescendantWithClassName -{ - NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingClassName:@"XCUIElementTypeButton" - shouldReturnAfterFirstMatch:YES]; - XCTAssertEqual(matchingSnapshots.count, 1); - XCTAssertEqual(matchingSnapshots.lastObject.elementType, XCUIElementTypeButton); -} - -- (void)testDescendantsWithIdentifier -{ - NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingIdentifier:@"Alerts" - shouldReturnAfterFirstMatch:NO]; - int snapshotsCount = 2; - XCTAssertEqual(matchingSnapshots.count, snapshotsCount); - XCTAssertEqual(matchingSnapshots.firstObject.elementType, XCUIElementTypeButton); - XCTAssertEqualObjects(matchingSnapshots.lastObject.label, @"Alerts"); - NSArray *selfElementsById = [matchingSnapshots.lastObject fb_descendantsMatchingIdentifier:@"Alerts" shouldReturnAfterFirstMatch:NO]; - XCTAssertEqual(selfElementsById.count, 1); -} - -- (void)testSingleDescendantWithIdentifier -{ - NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingIdentifier:@"Alerts" - shouldReturnAfterFirstMatch:YES]; - XCTAssertEqual(matchingSnapshots.count, 1); - XCTAssertEqual(matchingSnapshots.lastObject.elementType, XCUIElementTypeButton); - XCTAssertEqualObjects(matchingSnapshots.lastObject.label, @"Alerts"); -} - -- (void)testStableInstance -{ - NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingIdentifier:@"Alerts" - shouldReturnAfterFirstMatch:YES]; - XCTAssertEqual(matchingSnapshots.count, 1); - for (XCUIElement *el in @[ - matchingSnapshots.lastObject, - [matchingSnapshots.lastObject fb_stableInstanceWithUid:[matchingSnapshots.lastObject fb_uid]] - ]) { - XCTAssertEqual(el.elementType, XCUIElementTypeButton); - XCTAssertEqualObjects(el.label, @"Alerts"); - } -} - -- (void)testSingleDescendantWithMissingIdentifier -{ - NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingIdentifier:@"blabla" shouldReturnAfterFirstMatch:YES]; - XCTAssertEqual(matchingSnapshots.count, 0); -} - -- (void)testDescendantsWithXPathQuery -{ - NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingXPathQuery:@"//XCUIElementTypeButton[@label='Alerts']" - shouldReturnAfterFirstMatch:NO]; - XCTAssertEqual(matchingSnapshots.count, 1); - XCTAssertEqual(matchingSnapshots.lastObject.elementType, XCUIElementTypeButton); - XCTAssertEqualObjects(matchingSnapshots.lastObject.label, @"Alerts"); -} - -- (void)testSelfWithXPathQuery -{ - NSArray *matchingSnapshots = [self.testedApplication fb_descendantsMatchingXPathQuery:@"//XCUIElementTypeApplication" - shouldReturnAfterFirstMatch:NO]; - XCTAssertEqual(matchingSnapshots.count, 1); - XCTAssertEqual(matchingSnapshots.lastObject.elementType, XCUIElementTypeApplication); -} - -- (void)testSingleDescendantWithXPathQuery -{ - NSArray *matchingSnapshots = [self.testedApplication fb_descendantsMatchingXPathQuery:@"//XCUIElementTypeButton[@hittable='true']" - shouldReturnAfterFirstMatch:YES]; - XCTAssertEqual(matchingSnapshots.count, 1); - XCUIElement *matchingSnapshot = [matchingSnapshots firstObject]; - XCTAssertNotNil(matchingSnapshot); - XCTAssertEqual(matchingSnapshot.elementType, XCUIElementTypeButton); - XCTAssertEqualObjects(matchingSnapshot.label, @"Alerts"); -} - -- (void)testSingleDescendantWithXPathQueryNoMatches -{ - XCUIElement *matchingSnapshot = [[self.testedView fb_descendantsMatchingXPathQuery:@"//XCUIElementTypeButtonnn" - shouldReturnAfterFirstMatch:YES] firstObject]; - XCTAssertNil(matchingSnapshot); -} - -- (void)testSingleLastDescendantWithXPathQuery -{ - XCUIElement *matchingSnapshot = [[self.testedView fb_descendantsMatchingXPathQuery:@"(//XCUIElementTypeButton)[last()]" - shouldReturnAfterFirstMatch:YES] firstObject]; - XCTAssertNotNil(matchingSnapshot); - XCTAssertEqual(matchingSnapshot.elementType, XCUIElementTypeButton); -} - -- (void)testDescendantsWithXPathQueryNoMatches -{ - NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingXPathQuery:@"//XCUIElementTypeButton[@label='Alerts1']" - shouldReturnAfterFirstMatch:NO]; - XCTAssertEqual(matchingSnapshots.count, 0); -} - -- (void)testDescendantsWithComplexXPathQuery -{ - NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingXPathQuery:@"//*[@label='Scrolling']/preceding::*[boolean(string(@label))]" - shouldReturnAfterFirstMatch:NO]; - int snapshotsCount = 6; - XCTAssertEqual(matchingSnapshots.count, snapshotsCount); -} - -- (void)testDescendantsWithWrongXPathQuery -{ - XCTAssertThrowsSpecificNamed([self.testedView fb_descendantsMatchingXPathQuery:@"//*[blabla(@label, Scrolling')]" - shouldReturnAfterFirstMatch:NO], - NSException, FBInvalidXPathException); -} - -- (void)testFirstDescendantWithWrongXPathQuery -{ - XCTAssertThrowsSpecificNamed([self.testedView fb_descendantsMatchingXPathQuery:@"//*[blabla(@label, Scrolling')]" - shouldReturnAfterFirstMatch:YES], - NSException, FBInvalidXPathException); -} - -- (void)testVisibleDescendantWithXPathQuery -{ - NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingXPathQuery:@"//XCUIElementTypeButton[@name='Alerts' and @enabled='true' and @visible='true']" shouldReturnAfterFirstMatch:NO]; - XCTAssertEqual(matchingSnapshots.count, 1); - XCTAssertEqual(matchingSnapshots.lastObject.elementType, XCUIElementTypeButton); - XCTAssertTrue(matchingSnapshots.lastObject.isEnabled); - XCTAssertTrue(matchingSnapshots.lastObject.fb_isVisible); - XCTAssertEqualObjects(matchingSnapshots.lastObject.label, @"Alerts"); -} - -- (void)testDescendantsWithPredicateString -{ - NSPredicate *predicate = [NSPredicate predicateWithFormat:@"label = 'Alerts'"]; - NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingPredicate:predicate - shouldReturnAfterFirstMatch:NO]; - int snapshotsCount = 2; - XCTAssertEqual(matchingSnapshots.count, snapshotsCount); - XCTAssertEqual(matchingSnapshots.firstObject.elementType, XCUIElementTypeButton); - XCTAssertEqualObjects(matchingSnapshots.lastObject.label, @"Alerts"); - NSPredicate *selfPredicate = [NSPredicate predicateWithFormat:@"label == 'Alerts'"]; - NSArray *selfElementsByPredicate = [matchingSnapshots.lastObject fb_descendantsMatchingPredicate:selfPredicate - shouldReturnAfterFirstMatch:NO]; - XCTAssertEqual(selfElementsByPredicate.count, 1); -} - -- (void)testSelfWithPredicateString -{ - NSPredicate *predicate = [NSPredicate predicateWithFormat:@"type == 'XCUIElementTypeApplication'"]; - NSArray *matchingSnapshots = [self.testedApplication fb_descendantsMatchingPredicate:predicate - shouldReturnAfterFirstMatch:NO]; - XCTAssertEqual(matchingSnapshots.count, 1); - XCTAssertEqual(matchingSnapshots.lastObject.elementType, XCUIElementTypeApplication); -} - -- (void)testSingleDescendantWithPredicateString -{ - NSPredicate *predicate = [NSPredicate predicateWithFormat:@"type = 'XCUIElementTypeButton'"]; - NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingPredicate:predicate shouldReturnAfterFirstMatch:YES]; - XCTAssertEqual(matchingSnapshots.count, 1); - XCTAssertEqual(matchingSnapshots.lastObject.elementType, XCUIElementTypeButton); -} - -- (void)testSingleDescendantWithPredicateStringByIndex -{ - NSPredicate *predicate = [NSPredicate predicateWithFormat:@"type == 'XCUIElementTypeButton' AND index == 2"]; - NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingPredicate:predicate shouldReturnAfterFirstMatch:NO]; - XCTAssertEqual(matchingSnapshots.count, 1); - XCTAssertEqual(matchingSnapshots.lastObject.elementType, XCUIElementTypeButton); -} - -- (void)testDescendantsWithPropertyStrict -{ - NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingProperty:@"label" - value:@"Alert" - partialSearch:NO]; - XCTAssertEqual(matchingSnapshots.count, 0); - matchingSnapshots = [self.testedView fb_descendantsMatchingProperty:@"label" value:@"Alerts" partialSearch:NO]; - int snapshotsCount = 2; - XCTAssertEqual(matchingSnapshots.count, snapshotsCount); - XCTAssertEqual(matchingSnapshots.firstObject.elementType, XCUIElementTypeButton); - XCTAssertEqualObjects(matchingSnapshots.lastObject.label, @"Alerts"); -} - -- (void)testGlobalWithPropertyStrict -{ - NSArray *matchingSnapshots = [self.testedApplication fb_descendantsMatchingProperty:@"label" - value:@"Alert" - partialSearch:NO]; - XCTAssertEqual(matchingSnapshots.count, 0); - matchingSnapshots = [self.testedApplication fb_descendantsMatchingProperty:@"label" value:@"Alerts" partialSearch:NO]; - int snapshotsCount = 2; - XCTAssertEqual(matchingSnapshots.count, snapshotsCount); - XCTAssertEqual(matchingSnapshots.firstObject.elementType, XCUIElementTypeButton); - XCTAssertEqualObjects(matchingSnapshots.lastObject.label, @"Alerts"); -} - -- (void)testDescendantsWithPropertyPartial -{ - NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingProperty:@"label" - value:@"Alerts" - partialSearch:NO]; - int snapshotsCount = 2; - XCTAssertEqual(matchingSnapshots.count, snapshotsCount); - XCTAssertEqual(matchingSnapshots.firstObject.elementType, XCUIElementTypeButton); - XCTAssertEqualObjects(matchingSnapshots.lastObject.label, @"Alerts"); -} - -- (void)testDescendantsWithClassChain -{ - NSArray *matchingSnapshots; - NSString *queryString =@"XCUIElementTypeWindow/XCUIElementTypeOther/**/XCUIElementTypeButton"; - matchingSnapshots = [self.testedApplication fb_descendantsMatchingClassChain:queryString - shouldReturnAfterFirstMatch:NO]; - XCTAssertEqual(matchingSnapshots.count, 5); // /XCUIElementTypeButton - for (XCUIElement *matchingSnapshot in matchingSnapshots) { - XCTAssertEqual(matchingSnapshot.elementType, XCUIElementTypeButton); - } -} - -- (void)testDescendantsWithClassChainWithIndex -{ - NSArray *matchingSnapshots; - // iPhone - NSString *queryString = @"XCUIElementTypeWindow/*/*/*/*[2]/*/*/XCUIElementTypeButton"; - matchingSnapshots = [self.testedApplication fb_descendantsMatchingClassChain:queryString - shouldReturnAfterFirstMatch:NO]; - if (matchingSnapshots.count == 0) { - // iPad - queryString = @"XCUIElementTypeWindow/*/*/*/*/*[2]/*/*/XCUIElementTypeButton"; - matchingSnapshots = [self.testedApplication fb_descendantsMatchingClassChain:queryString - shouldReturnAfterFirstMatch:NO]; - } - XCTAssertEqual(matchingSnapshots.count, 5); // /XCUIElementTypeButton - for (XCUIElement *matchingSnapshot in matchingSnapshots) { - XCTAssertEqual(matchingSnapshot.elementType, XCUIElementTypeButton); - } -} - -- (void)testDescendantsWithClassChainAndPredicates -{ - NSArray *matchingSnapshots; - NSString *queryString = @"XCUIElementTypeWindow/**/XCUIElementTypeButton[`label BEGINSWITH 'A'`]"; - matchingSnapshots = [self.testedApplication fb_descendantsMatchingClassChain:queryString - shouldReturnAfterFirstMatch:NO]; - XCTAssertEqual(matchingSnapshots.count, 2); - XCTAssertEqualObjects([matchingSnapshots firstObject].label, @"Alerts"); - XCTAssertEqualObjects([matchingSnapshots lastObject].label, @"Attributes"); -} - -- (void)testDescendantsWithIndirectClassChainAndPredicates -{ - NSString *queryString = @"XCUIElementTypeWindow/**/XCUIElementTypeButton[`label BEGINSWITH 'A'`]"; - NSArray *simpleQueryMatches = [self.testedApplication fb_descendantsMatchingClassChain:queryString - shouldReturnAfterFirstMatch:NO]; - NSArray *deepQueryMatches = [self.testedApplication fb_descendantsMatchingClassChain:@"XCUIElementTypeWindow/**/XCUIElementTypeButton[`label BEGINSWITH 'A'`]" - shouldReturnAfterFirstMatch:NO]; - XCTAssertEqual(simpleQueryMatches.count, deepQueryMatches.count); - XCTAssertEqualObjects([simpleQueryMatches firstObject].label, [deepQueryMatches firstObject].label); - XCTAssertEqualObjects([simpleQueryMatches lastObject].label, [deepQueryMatches lastObject].label); -} - -- (void)testClassChainWithDescendantPredicate -{ - NSArray *simpleQueryMatches = [self.testedApplication fb_descendantsMatchingClassChain:@"XCUIElementTypeWindow/*/*[1]" - shouldReturnAfterFirstMatch:NO]; - NSArray *predicateQueryMatches = [self.testedApplication fb_descendantsMatchingClassChain:@"XCUIElementTypeWindow/*/*[$type == 'XCUIElementTypeButton' AND label BEGINSWITH 'A'$]" - shouldReturnAfterFirstMatch:NO]; - XCTAssertEqual(simpleQueryMatches.count, predicateQueryMatches.count); - XCTAssertEqual([simpleQueryMatches firstObject].elementType, [predicateQueryMatches firstObject].elementType); - XCTAssertEqual([simpleQueryMatches lastObject].elementType, [predicateQueryMatches lastObject].elementType); -} - -- (void)testSingleDescendantWithComplexIndirectClassChain -{ - NSArray *queryMatches = [self.testedApplication fb_descendantsMatchingClassChain:@"**/*/XCUIElementTypeButton[2]" - shouldReturnAfterFirstMatch:NO]; - XCTAssertEqual(queryMatches.count, 1); - XCTAssertEqual(queryMatches.lastObject.elementType, XCUIElementTypeButton); - XCTAssertEqualObjects(queryMatches.lastObject.label, @"Deadlock app"); -} - -- (void)testSingleDescendantWithComplexIndirectClassChainAndZeroMatches -{ - NSArray *queryMatches = [self.testedApplication fb_descendantsMatchingClassChain:@"**/*/XCUIElementTypeWindow" - shouldReturnAfterFirstMatch:NO]; - XCTAssertEqual(queryMatches.count, 0); -} - -- (void)testDescendantsWithClassChainAndPredicatesAndIndexes -{ - NSArray *matchingSnapshots; - NSString *queryString = @"XCUIElementTypeWindow[`name != 'bla'`]/**/XCUIElementTypeButton[`label BEGINSWITH \"A\"`][1]"; - matchingSnapshots = [self.testedApplication fb_descendantsMatchingClassChain:queryString - shouldReturnAfterFirstMatch:NO]; - XCTAssertEqual(matchingSnapshots.count, 1); - XCTAssertEqualObjects([matchingSnapshots firstObject].label, @"Alerts"); -} - -- (void)testSingleDescendantWithClassChain -{ - NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingClassChain:@"XCUIElementTypeButton" - shouldReturnAfterFirstMatch:YES]; - - XCTAssertEqual(matchingSnapshots.count, 1); - XCTAssertEqual(matchingSnapshots.lastObject.elementType, XCUIElementTypeButton); - XCTAssertTrue([matchingSnapshots.lastObject.label isEqualToString:@"Alerts"]); -} - -- (void)testSingleDescendantWithClassChainAndNegativeIndex -{ - NSArray *matchingSnapshots; - matchingSnapshots = [self.testedView fb_descendantsMatchingClassChain:@"XCUIElementTypeButton[-1]" - shouldReturnAfterFirstMatch:YES]; - - XCTAssertEqual(matchingSnapshots.count, 1); - XCTAssertEqual(matchingSnapshots.lastObject.elementType, XCUIElementTypeButton); - XCTAssertTrue([matchingSnapshots.lastObject.label isEqualToString:@"Touch"]); - - matchingSnapshots = [self.testedView fb_descendantsMatchingClassChain:@"XCUIElementTypeButton[-10]" - shouldReturnAfterFirstMatch:YES]; - XCTAssertEqual(matchingSnapshots.count, 0); -} - -- (void)testInvalidQueryWithClassChain -{ - XCTAssertThrowsSpecificNamed([self.testedView fb_descendantsMatchingClassChain:@"NoXCUIElementTypePrefix" - shouldReturnAfterFirstMatch:YES], - NSException, FBClassChainQueryParseException); -} - -- (void)testHandleInvalidQueryWithClassChainAsNoElementWithoutError -{ - NSArray *matchingSnapshots = [self.testedView - fb_descendantsMatchingClassChain:@"XCUIElementTypeBlabla" - shouldReturnAfterFirstMatch:YES]; - XCTAssertEqual(matchingSnapshots.count, 0); -} - -- (void)testClassChainWithInvalidPredicate -{ - XCTAssertThrowsSpecificNamed([self.testedApplication fb_descendantsMatchingClassChain:@"XCUIElementTypeWindow[`bla != 'bla'`]" - shouldReturnAfterFirstMatch:NO], - NSException, FBUnknownAttributeException);; -} - -@end - -@interface XCUIElementFBFindTests_AttributesPage : FBIntegrationTestCase -@end -@implementation XCUIElementFBFindTests_AttributesPage - -- (void)setUp -{ - [super setUp]; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self launchApplication]; - [self goToAttributesPage]; - }); -} - -- (void)testNestedQueryWithClassChain -{ - NSString *queryString = @"XCUIElementTypePicker"; - FBAssertWaitTillBecomesTrue(self.testedApplication.buttons[@"Button"].fb_isVisible); - XCUIElement *datePicker = [self.testedApplication - descendantsMatchingType:XCUIElementTypeDatePicker].allElementsBoundByIndex.firstObject; - NSArray *matches = [datePicker fb_descendantsMatchingClassChain:queryString - shouldReturnAfterFirstMatch:NO]; - XCTAssertEqual(matches.count, 1); - - XCUIElementType expectedType = XCUIElementTypePicker; - XCTAssertEqual([matches firstObject].elementType, expectedType); -} - -@end - -@interface XCUIElementFBFindTests_ScrollPage : FBIntegrationTestCase -@end -@implementation XCUIElementFBFindTests_ScrollPage - -- (void)setUp -{ - [super setUp]; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self launchApplication]; - [self goToScrollPageWithCells:YES]; - }); -} - -- (void)testInvisibleDescendantWithXPathQuery -{ - NSArray *matchingSnapshots = [self.testedApplication fb_descendantsMatchingXPathQuery:@"//XCUIElementTypeStaticText[@visible='false']" - shouldReturnAfterFirstMatch:NO]; - XCTAssertGreaterThan(matchingSnapshots.count, 1); - XCTAssertEqual(matchingSnapshots.lastObject.elementType, XCUIElementTypeStaticText); - XCTAssertFalse(matchingSnapshots.lastObject.fb_isVisible); -} - -- (void)testNonHittableDescendantWithXPathQuery -{ - NSArray *matchingSnapshots = [self.testedApplication fb_descendantsMatchingXPathQuery:@"//XCUIElementTypeStaticText[@hittable='false']" - shouldReturnAfterFirstMatch:NO]; - XCTAssertGreaterThan(matchingSnapshots.count, 1); - XCTAssertEqual(matchingSnapshots.lastObject.elementType, XCUIElementTypeStaticText); - XCTAssertFalse(matchingSnapshots.lastObject.fb_isVisible); -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/XCUIElementHelperIntegrationTests.m b/WebDriverAgent/WebDriverAgentTests/IntegrationTests/XCUIElementHelperIntegrationTests.m deleted file mode 100644 index 531a96214b..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/IntegrationTests/XCUIElementHelperIntegrationTests.m +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import -#import "XCTest/XCUIElementTypes.h" - -#import "FBIntegrationTestCase.h" -#import "FBTestMacros.h" -#import "FBElement.h" -#import "FBElementUtils.h" -#import "FBXCElementSnapshot.h" -#import "XCUIElement+FBUtilities.h" - -@interface XCUIElementHelperIntegrationTests : FBIntegrationTestCase -@end - -@implementation XCUIElementHelperIntegrationTests - -- (void)setUp -{ - [super setUp]; - [self launchApplication]; - [self goToAlertsPage]; -} - -- (void)testDescendantsFiltering -{ - NSArray *buttons = self.testedApplication.buttons.allElementsBoundByIndex; - XCTAssertTrue(buttons.count > 0); - NSArray *windows = self.testedApplication.windows.allElementsBoundByIndex; - XCTAssertTrue(windows.count > 0); - - NSMutableArray *allElements = [NSMutableArray array]; - [allElements addObjectsFromArray:buttons]; - [allElements addObjectsFromArray:windows]; - - NSMutableArray> *buttonSnapshots = [NSMutableArray array]; - [buttonSnapshots addObject:[buttons.firstObject fb_customSnapshot]]; - - NSArray *result = [self.testedApplication fb_filterDescendantsWithSnapshots:buttonSnapshots - onlyChildren:NO]; - XCTAssertEqual(1, result.count); - XCTAssertEqual([result.firstObject elementType], XCUIElementTypeButton); -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/UnitTests/Doubles/XCElementSnapshotDouble.h b/WebDriverAgent/WebDriverAgentTests/UnitTests/Doubles/XCElementSnapshotDouble.h deleted file mode 100644 index 140547f8e1..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/UnitTests/Doubles/XCElementSnapshotDouble.h +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -@interface XCElementSnapshotDouble : NSObject -@property (readwrite, nullable) id value; -@property (readwrite, nullable, copy) NSString *label; -@property (nonatomic, assign) UIAccessibilityTraits traits; -@end diff --git a/WebDriverAgent/WebDriverAgentTests/UnitTests/Doubles/XCElementSnapshotDouble.m b/WebDriverAgent/WebDriverAgentTests/UnitTests/Doubles/XCElementSnapshotDouble.m deleted file mode 100644 index f8592ac34e..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/UnitTests/Doubles/XCElementSnapshotDouble.m +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import "XCElementSnapshotDouble.h" - -#import "FBXCAccessibilityElement.h" -#import "FBXCElementSnapshot.h" -#import "XCUIHitPointResult.h" - -@implementation XCElementSnapshotDouble - -- (id)init -{ - self = [super init]; - self->_value = @"magicValue"; - self->_label = @"testLabel"; - return self; -} - -- (NSString *)identifier -{ - return @"testName"; -} - -- (CGRect)frame -{ - return CGRectZero; -} - -- (NSString *)title -{ - return @"testTitle"; -} - -- (XCUIElementType)elementType -{ - return XCUIElementTypeOther; -} - -- (BOOL)isEnabled -{ - return YES; -} - -- (XCUIUserInterfaceSizeClass)horizontalSizeClass -{ - return XCUIUserInterfaceSizeClassUnspecified; -} - -- (XCUIUserInterfaceSizeClass)verticalSizeClass -{ - return XCUIUserInterfaceSizeClassUnspecified; -} - -- (NSString *)placeholderValue -{ - return @"testPlaceholderValue"; -} - -- (BOOL)isSelected -{ - return YES; -} - -- (BOOL)hasFocus -{ - return YES; -} - -- (NSDictionary *)additionalAttributes -{ - return @{}; -} - -- (id)accessibilityElement -{ - return nil; -} - -- (id)parent -{ - return nil; -} - -- (XCUIHitPointResult *)hitPoint:(NSError **)error -{ - return [[XCUIHitPointResult alloc] initWithHitPoint:CGPointZero hittable:YES]; -} - -- (NSArray *)children -{ - return @[]; -} - -- (NSArray *)_allDescendants -{ - return @[]; -} - -- (CGRect)visibleFrame -{ - return CGRectZero; -} - -- (UIAccessibilityTraits)traits -{ - return UIAccessibilityTraitButton; -} -@end diff --git a/WebDriverAgent/WebDriverAgentTests/UnitTests/Doubles/XCUIApplicationDouble.h b/WebDriverAgent/WebDriverAgentTests/UnitTests/Doubles/XCUIApplicationDouble.h deleted file mode 100644 index f0d7b59906..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/UnitTests/Doubles/XCUIApplicationDouble.h +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -@interface XCUIApplicationDouble : NSObject -@property (nonatomic, assign, readonly) BOOL didTerminate; -@property (nonatomic, strong) NSString* bundleID; -@property (nonatomic) BOOL fb_shouldWaitForQuiescence; - -- (BOOL)running; -@end diff --git a/WebDriverAgent/WebDriverAgentTests/UnitTests/Doubles/XCUIApplicationDouble.m b/WebDriverAgent/WebDriverAgentTests/UnitTests/Doubles/XCUIApplicationDouble.m deleted file mode 100644 index 2cff790a57..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/UnitTests/Doubles/XCUIApplicationDouble.m +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import "XCUIApplicationDouble.h" - -@interface XCUIApplicationDouble () -@property (nonatomic, assign, readwrite) BOOL didTerminate; -@end - -@implementation XCUIApplicationDouble - -- (instancetype)init -{ - self = [super init]; - if (self) { - _bundleID = @"some.bundle.identifier"; - } - return self; -} - -- (void)terminate -{ - self.didTerminate = YES; -} - -- (NSUInteger)processID -{ - return 0; -} - -- (NSString *)bundleID -{ - return @"com.facebook.awesome"; -} - -- (void)fb_nativeResolve -{ - -} - -- (id)query -{ - return nil; -} - -- (BOOL)fb_shouldWaitForQuiescence -{ - return NO; -} - --(void)setFb_shouldWaitForQuiescence:(BOOL)value -{ - -} - -- (BOOL)running -{ - return NO; -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/UnitTests/Doubles/XCUIElementDouble.h b/WebDriverAgent/WebDriverAgentTests/UnitTests/Doubles/XCUIElementDouble.h deleted file mode 100644 index 90d523bf5d..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/UnitTests/Doubles/XCUIElementDouble.h +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import -#import -#import - -@class XCUIApplication; - -@interface XCUIElementDouble : NSObject -@property (nonatomic, strong, nonnull) XCUIApplication *application; -@property (nonatomic, readwrite, assign) CGRect frame; -@property (nonatomic, readwrite, nullable) id lastSnapshot; -@property (nonatomic, assign) BOOL fb_isObstructedByAlert; -@property (nonatomic, readonly, nonnull) NSString *fb_cacheId; -@property (nonatomic, readwrite, copy, nonnull) NSDictionary *wdRect; -@property (nonatomic, readwrite, assign) CGRect wdFrame; -@property (nonatomic, readwrite, copy, nonnull) NSString *wdUID; -@property (nonatomic, copy, readwrite, nullable) NSString *wdName; -@property (nonatomic, copy, readwrite, nullable) NSString *wdLabel; -@property (nonatomic, copy, readwrite, nonnull) NSString *wdType; -@property (nonatomic, strong, readwrite, nullable) NSString *wdValue; -@property (nonatomic, readwrite, getter=isWDEnabled) BOOL wdEnabled; -@property (nonatomic, readwrite, getter=isWDSelected) BOOL wdSelected; -@property (nonatomic, readwrite, assign) CGRect wdNativeFrame; -@property (nonatomic, readwrite) NSUInteger wdIndex; -@property (nonatomic, readwrite, getter=isWDVisible) BOOL wdVisible; -@property (nonatomic, readwrite, getter=isWDAccessible) BOOL wdAccessible; -@property (nonatomic, readwrite, getter = isWDFocused) BOOL wdFocused; -@property (nonatomic, readwrite, getter = isWDHittable) BOOL wdHittable; -@property (nonatomic, copy, readwrite, nullable) NSString *wdPlaceholderValue; -@property (copy, nonnull) NSArray *children; -@property (nonatomic, readwrite, assign) XCUIElementType elementType; -@property (nonatomic, readwrite, getter=isWDAccessibilityContainer) BOOL wdAccessibilityContainer; -@property (nonatomic, copy, readwrite, nullable) NSString *wdTraits; - -- (void)resolve; -- (id _Nonnull)fb_standardSnapshot; -- (id _Nonnull)fb_customSnapshot; -- (nullable id)query; - -// Checks -@property (nonatomic, assign, readonly) BOOL didResolve; - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/UnitTests/Doubles/XCUIElementDouble.m b/WebDriverAgent/WebDriverAgentTests/UnitTests/Doubles/XCUIElementDouble.m deleted file mode 100644 index f58134620e..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/UnitTests/Doubles/XCUIElementDouble.m +++ /dev/null @@ -1,103 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import "XCUIElementDouble.h" - -@interface XCUIElementDouble () -@property (nonatomic, assign, readwrite) BOOL didResolve; -@end - -@implementation XCUIElementDouble - -- (id)init -{ - self = [super init]; - if (self) { - self.wdFrame = CGRectZero; - self.wdNativeFrame = CGRectZero; - self.wdName = @"testName"; - self.wdLabel = @"testLabel"; - self.wdValue = @"magicValue"; - self.wdPlaceholderValue = @"testPlaceholderValue"; - self.wdTraits = @"testTraits"; - self.wdVisible = YES; - self.wdAccessible = YES; - self.wdEnabled = YES; - self.wdSelected = YES; - self.wdFocused = YES; - self.wdHittable = YES; - self.wdIndex = 0; -#if TARGET_OS_TV - self.wdFocused = YES; -#endif - self.children = @[]; - self.wdRect = @{@"x": @0, - @"y": @0, - @"width": @0, - @"height": @0, - }; - self.wdAccessibilityContainer = NO; - self.elementType = XCUIElementTypeOther; - self.wdType = @"XCUIElementTypeOther"; - self.wdUID = @"0"; - self.lastSnapshot = nil; - } - return self; -} - -- (id)fb_valueForWDAttributeName:(NSString *)name -{ - return @"test"; -} - -- (id)query -{ - return nil; -} - -- (void)resolve -{ - self.didResolve = YES; -} - -- (void)fb_nativeResolve -{ - self.didResolve = YES; -} - -- (id _Nonnull)fb_standardSnapshot; -{ - return [self lastSnapshot]; -} - -- (id _Nonnull)fb_customSnapshot; -{ - return [self lastSnapshot]; -} - -- (NSString *)fb_cacheId -{ - return self.wdUID; -} - -- (id)lastSnapshot -{ - return self; -} - -- (id)fb_uid -{ - return self.wdUID; -} - -- (NSString *)wdTraits -{ - return self.wdTraits; -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/UnitTests/FBClassChainTests.m b/WebDriverAgent/WebDriverAgentTests/UnitTests/FBClassChainTests.m deleted file mode 100644 index 0409af6584..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/UnitTests/FBClassChainTests.m +++ /dev/null @@ -1,313 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "XCUIElementDouble.h" -#import "FBClassChainQueryParser.h" - -@interface FBClassChainTests : XCTestCase -@end - -@implementation FBClassChainTests - -- (void)testValidChain -{ - NSError *error; - FBClassChain *result = [FBClassChainQueryParser parseQuery:@"XCUIElementTypeWindow/XCUIElementTypeButton" error:&error]; - XCTAssertNotNil(result); - XCTAssertEqual(result.elements.count, 2); - - FBClassChainItem *firstElement = [result.elements firstObject]; - XCTAssertEqual(firstElement.type, XCUIElementTypeWindow); - XCTAssertNil(firstElement.position); - XCTAssertFalse(firstElement.isDescendant); - - FBClassChainItem *secondElement = [result.elements objectAtIndex:1]; - XCTAssertEqual(secondElement.type, XCUIElementTypeButton); - XCTAssertNil(secondElement.position); - XCTAssertFalse(secondElement.isDescendant); -} - -- (void)testValidChainWithStar -{ - NSError *error; - FBClassChain *result = [FBClassChainQueryParser parseQuery:@"XCUIElementTypeWindow/XCUIElementTypeButton[3]/*[4]/*[5]/XCUIElementTypeAlert" error:&error]; - XCTAssertNotNil(result); - XCTAssertEqual(result.elements.count, 5); - - FBClassChainItem *firstElement = [result.elements firstObject]; - XCTAssertEqual(firstElement.type, XCUIElementTypeWindow); - XCTAssertNil(firstElement.position); - XCTAssertFalse(firstElement.isDescendant); - - FBClassChainItem *secondElement = [result.elements objectAtIndex:1]; - XCTAssertEqual(secondElement.type, XCUIElementTypeButton); - XCTAssertEqual(secondElement.position.integerValue, 3); - XCTAssertFalse(secondElement.isDescendant); - - FBClassChainItem *thirdElement = [result.elements objectAtIndex:2]; - XCTAssertEqual(thirdElement.type, XCUIElementTypeAny); - XCTAssertEqual(thirdElement.position.integerValue, 4); - XCTAssertFalse(thirdElement.isDescendant); - - FBClassChainItem *fourthElement = [result.elements objectAtIndex:3]; - XCTAssertEqual(fourthElement.type, XCUIElementTypeAny); - XCTAssertEqual(fourthElement.position.integerValue, 5); - XCTAssertFalse(fourthElement.isDescendant); - - FBClassChainItem *fifthsElement = [result.elements objectAtIndex:4]; - XCTAssertEqual(fifthsElement.type, XCUIElementTypeAlert); - XCTAssertNil(fifthsElement.position); - XCTAssertFalse(fifthsElement.isDescendant); -} - -- (void)testValidSingleStarChain -{ - NSError *error; - FBClassChain *result = [FBClassChainQueryParser parseQuery:@"*" error:&error]; - XCTAssertNotNil(result); - XCTAssertEqual(result.elements.count, 1); - - FBClassChainItem *firstElement = [result.elements firstObject]; - XCTAssertEqual(firstElement.type, XCUIElementTypeAny); - XCTAssertNil(firstElement.position); - XCTAssertFalse(firstElement.isDescendant); -} - -- (void)testValidSingleStarIndirectChain -{ - NSError *error; - FBClassChain *result = [FBClassChainQueryParser parseQuery:@"**/*/*/XCUIElementTypeButton" error:&error]; - XCTAssertNotNil(result); - XCTAssertEqual(result.elements.count, 3); - - FBClassChainItem *firstElement = [result.elements firstObject]; - XCTAssertEqual(firstElement.type, XCUIElementTypeAny); - XCTAssertNil(firstElement.position); - XCTAssertTrue(firstElement.isDescendant); - - FBClassChainItem *secondElement = [result.elements objectAtIndex:1]; - XCTAssertEqual(secondElement.type, XCUIElementTypeAny); - XCTAssertNil(secondElement.position); - XCTAssertFalse(secondElement.isDescendant); - - FBClassChainItem *thirdElement = [result.elements objectAtIndex:2]; - XCTAssertEqual(thirdElement.type, XCUIElementTypeButton); - XCTAssertNil(thirdElement.position); - XCTAssertFalse(thirdElement.isDescendant); -} - -- (void)testValidDoubleIndirectChainAndStar -{ - NSError *error; - FBClassChain *result = [FBClassChainQueryParser parseQuery:@"**/XCUIElementTypeButton/**/*" error:&error]; - XCTAssertNotNil(result); - XCTAssertEqual(result.elements.count, 2); - - FBClassChainItem *firstElement = [result.elements firstObject]; - XCTAssertEqual(firstElement.type, XCUIElementTypeButton); - XCTAssertNil(firstElement.position); - XCTAssertTrue(firstElement.isDescendant); - - FBClassChainItem *secondElement = [result.elements objectAtIndex:1]; - XCTAssertEqual(secondElement.type, XCUIElementTypeAny); - XCTAssertNil(secondElement.position); - XCTAssertTrue(secondElement.isDescendant); -} - -- (void)testValidDoubleIndirectChainAndClassName -{ - NSError *error; - FBClassChain *result = [FBClassChainQueryParser parseQuery:@"**/XCUIElementTypeButton/**/XCUIElementTypeImage" error:&error]; - XCTAssertNotNil(result); - XCTAssertEqual(result.elements.count, 2); - - FBClassChainItem *firstElement = [result.elements firstObject]; - XCTAssertEqual(firstElement.type, XCUIElementTypeButton); - XCTAssertNil(firstElement.position); - XCTAssertTrue(firstElement.isDescendant); - - FBClassChainItem *secondElement = [result.elements objectAtIndex:1]; - XCTAssertEqual(secondElement.type, XCUIElementTypeImage); - XCTAssertNil(secondElement.position); - XCTAssertTrue(secondElement.isDescendant); -} - -- (void)testValidChainWithNegativeIndex -{ - NSError *error; - FBClassChain *result = [FBClassChainQueryParser parseQuery:@"XCUIElementTypeWindow/XCUIElementTypeButton[-1]" error:&error]; - XCTAssertNotNil(result); - XCTAssertEqual(result.elements.count, 2); - - FBClassChainItem *firstElement = [result.elements firstObject]; - XCTAssertEqual(firstElement.type, XCUIElementTypeWindow); - XCTAssertNil(firstElement.position); - XCTAssertEqual(firstElement.predicates.count, 0); - XCTAssertFalse(firstElement.isDescendant); - - FBClassChainItem *secondElement = [result.elements objectAtIndex:1]; - XCTAssertEqual(secondElement.type, XCUIElementTypeButton); - XCTAssertEqual(secondElement.position.integerValue, -1); - XCTAssertEqual(secondElement.predicates.count, 0); - XCTAssertFalse(secondElement.isDescendant); -} - -- (void)testValidChainWithSinglePredicate -{ - NSError *error; - FBClassChain *result = [FBClassChainQueryParser parseQuery:@"XCUIElementTypeWindow[`name == 'blabla'`]/XCUIElementTypeButton" error:&error]; - XCTAssertNotNil(result); - XCTAssertEqual(result.elements.count, 2); - - FBClassChainItem *firstElement = [result.elements firstObject]; - XCTAssertEqual(firstElement.type, XCUIElementTypeWindow); - XCTAssertNil(firstElement.position); - XCTAssertEqual(firstElement.predicates.count, 1); - XCTAssertFalse(firstElement.isDescendant); - - FBClassChainItem *secondElement = [result.elements objectAtIndex:1]; - XCTAssertEqual(secondElement.type, XCUIElementTypeButton); - XCTAssertNil(secondElement.position); - XCTAssertEqual(secondElement.predicates.count, 0); - XCTAssertFalse(secondElement.isDescendant); -} - -- (void)testValidChainWithMultiplePredicates -{ - NSError *error; - FBClassChain *result = [FBClassChainQueryParser parseQuery:@"XCUIElementTypeWindow[`name == 'blabla'`]/XCUIElementTypeButton[`value == 'blabla'`]" error:&error]; - XCTAssertNotNil(result); - XCTAssertEqual(result.elements.count, 2); - - FBClassChainItem *firstElement = [result.elements firstObject]; - XCTAssertEqual(firstElement.type, XCUIElementTypeWindow); - XCTAssertNil(firstElement.position); - XCTAssertEqual(firstElement.predicates.count, 1); - XCTAssertFalse(firstElement.isDescendant); - - FBClassChainItem *secondElement = [result.elements objectAtIndex:1]; - XCTAssertEqual(secondElement.type, XCUIElementTypeButton); - XCTAssertNil(secondElement.position); - XCTAssertEqual(secondElement.predicates.count, 1); - XCTAssertFalse(secondElement.isDescendant); -} - -- (void)testValidChainWithIndirectSearchAndPredicates -{ - NSError *error; - FBClassChain *result = [FBClassChainQueryParser parseQuery:@"**/XCUIElementTypeTable[`name == 'blabla'`][10]/**/XCUIElementTypeButton[`value == 'blabla'`]" error:&error]; - XCTAssertNotNil(result); - XCTAssertEqual(result.elements.count, 2); - - FBClassChainItem *firstElement = [result.elements firstObject]; - XCTAssertEqual(firstElement.type, XCUIElementTypeTable); - XCTAssertEqual(firstElement.position.integerValue, 10); - XCTAssertEqual(firstElement.predicates.count, 1); - XCTAssertTrue(firstElement.isDescendant); - - FBClassChainItem *secondElement = [result.elements objectAtIndex:1]; - XCTAssertEqual(secondElement.type, XCUIElementTypeButton); - XCTAssertNil(secondElement.position); - XCTAssertEqual(secondElement.predicates.count, 1); - XCTAssertTrue(secondElement.isDescendant); -} - -- (void)testValidChainWithMultiplePredicatesAndPositions -{ - NSError *error; - FBClassChain *result = [FBClassChainQueryParser parseQuery:@"*[`name == \"к``ири````'лиця\"`][3]/XCUIElementTypeButton[`value == \"blabla\"`][-1]" error:&error]; - XCTAssertNotNil(result); - XCTAssertEqual(result.elements.count, 2); - - FBClassChainItem *firstElement = [result.elements firstObject]; - XCTAssertEqual(firstElement.type, XCUIElementTypeAny); - XCTAssertEqual(firstElement.position.integerValue, 3); - XCTAssertEqual(firstElement.predicates.count, 1); - XCTAssertFalse(firstElement.isDescendant); - - FBClassChainItem *secondElement = [result.elements objectAtIndex:1]; - XCTAssertEqual(secondElement.type, XCUIElementTypeButton); - XCTAssertEqual(secondElement.position.integerValue, -1); - XCTAssertEqual(secondElement.predicates.count, 1); - XCTAssertFalse(secondElement.isDescendant); -} - -- (void)testValidChainWithDescendantPredicate -{ - NSError *error; - FBClassChain *result = [FBClassChainQueryParser parseQuery:@"**/XCUIElementTypeTable[$type == 'XCUIElementTypeImage' AND name == 'olala'$][`name == 'blabla'`][10]" error:&error]; - XCTAssertNotNil(result); - XCTAssertEqual(result.elements.count, 1); - - FBClassChainItem *firstElement = [result.elements firstObject]; - XCTAssertEqual(firstElement.type, XCUIElementTypeTable); - XCTAssertEqual(firstElement.position.integerValue, 10); - XCTAssertEqual(firstElement.predicates.count, 2); - XCTAssertTrue(firstElement.isDescendant); -} - -- (void)testValidChainWithMultipleDescendantPredicates -{ - NSError *error; - FBClassChain *result = [FBClassChainQueryParser parseQuery:@"**/XCUIElementTypeTable[$type == 'XCUIElementTypeImage' AND name == 'olala'$][`value == 'peace'`][$value == 'yolo'$][`name == 'blabla'`][10]" error:&error]; - XCTAssertNotNil(result); - XCTAssertEqual(result.elements.count, 1); - - FBClassChainItem *firstElement = [result.elements firstObject]; - XCTAssertEqual(firstElement.type, XCUIElementTypeTable); - XCTAssertEqual(firstElement.position.integerValue, 10); - XCTAssertEqual(firstElement.predicates.count, 4); - XCTAssertTrue(firstElement.isDescendant); -} - -- (void)testInvalidChains -{ - NSArray *invalidQueries = @[ - @"/XCUIElementTypeWindow" - ,@"XCUIElementTypeWindow/" - ,@"XCUIElementTypeWindow//*" - ,@"XCUIElementTypeWindow*/*" - ,@"**" - ,@"***" - ,@"**/*/**" - ,@"/**" - ,@"XCUIElementTypeWindow/**" - ,@"**[1]/XCUIElementTypeWindow" - ,@"**[`name == '1'`]/XCUIElementTypeWindow" - ,@"XCUIElementTypeWindow[0]" - ,@"XCUIElementTypeWindow[1][1]" - ,@"blabla" - ,@"XCUIElementTypeWindow/blabla" - ,@" XCUIElementTypeWindow" - ,@"XCUIElementTypeWindow[ 2 ]" - ,@"XCUIElementTypeWindow[[2]" - ,@"XCUIElementTypeWindow[2]]" - ,@"XCUIElementType[Window[2]]" - ,@"XCUIElementTypeWindow[visible = 1]" - ,@"XCUIElementTypeWindow[1][`visible = 1`]" - ,@"XCUIElementTypeWindow[1] [`visible = 1`]" - ,@"XCUIElementTypeWindow[ `visible = 1`]" - ,@"XCUIElementTypeWindow[`visible = 1][`name = \"bla\"`]" - ,@"XCUIElementTypeWindow[`visible = 1]" - ,@"XCUIElementTypeWindow[$visible = 1]" - ,@"XCUIElementTypeWindow[``]" - ,@"XCUIElementTypeWindow[$$]" - ,@"XCUIElementTypeWindow[`name = \"bla```bla\"`]" - ,@"XCUIElementTypeWindow[$name = \"bla$$$bla\"$]" - ]; - for (NSString *invalidQuery in invalidQueries) { - NSError *error; - FBClassChain *result = [FBClassChainQueryParser parseQuery:invalidQuery error:&error]; - XCTAssertNil(result); - XCTAssertNotNil(error); - } -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/UnitTests/FBConfigurationTests.m b/WebDriverAgent/WebDriverAgentTests/UnitTests/FBConfigurationTests.m deleted file mode 100644 index 6d25ea6b06..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/UnitTests/FBConfigurationTests.m +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBConfiguration.h" - -@interface FBConfigurationTests : XCTestCase - -@end - -@implementation FBConfigurationTests - -- (void)setUp -{ - [super setUp]; - unsetenv("USE_PORT"); - unsetenv("USE_IP"); - unsetenv("VERBOSE_LOGGING"); -} - -- (void)testBindingPortDefault -{ - XCTAssertTrue(NSEqualRanges([FBConfiguration bindingPortRange], NSMakeRange(8100, 100))); -} - -- (void)testBindingPortEnvironmentOverwrite -{ - setenv("USE_PORT", "1000", 1); - XCTAssertTrue(NSEqualRanges([FBConfiguration bindingPortRange], NSMakeRange(1000, 1))); -} - -- (void)testVerboseLoggingDefault -{ - XCTAssertFalse([FBConfiguration verboseLoggingEnabled]); -} - -- (void)testVerboseLoggingEnvironmentOverwrite -{ - setenv("VERBOSE_LOGGING", "YES", 1); - XCTAssertTrue([FBConfiguration verboseLoggingEnabled]); -} - -- (void)testBindingIPDefault -{ - XCTAssertNil([FBConfiguration bindingIPAddress]); -} - -- (void)testBindingIPEnvironmentOverwrite -{ - setenv("USE_IP", "192.168.1.100", 1); - XCTAssertEqualObjects([FBConfiguration bindingIPAddress], @"192.168.1.100"); -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/UnitTests/FBElementCacheTests.m b/WebDriverAgent/WebDriverAgentTests/UnitTests/FBElementCacheTests.m deleted file mode 100644 index 11e0c6fe10..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/UnitTests/FBElementCacheTests.m +++ /dev/null @@ -1,124 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBElementCache.h" -#import "XCUIElementDouble.h" -#import "XCUIElement+FBCaching.h" -#import "XCUIElement+FBUtilities.h" - -@interface FBElementCacheTests : XCTestCase -@property (nonatomic, strong) FBElementCache *cache; -@end - -@implementation FBElementCacheTests - -- (void)setUp -{ - [super setUp]; - self.cache = [FBElementCache new]; -} - -- (void)testStoringElement -{ - XCUIElementDouble *el1 = XCUIElementDouble.new; - el1.wdUID = @"1"; - XCUIElementDouble *el2 = XCUIElementDouble.new; - el2.wdUID = @"2"; - NSString *firstUUID = [self.cache storeElement:(XCUIElement *)el1]; - NSString *secondUUID = [self.cache storeElement:(XCUIElement *)el2]; - XCTAssertEqualObjects(firstUUID, el1.wdUID); - XCTAssertEqualObjects(secondUUID, el2.wdUID); -} - -- (void)testFetchingElement -{ - XCUIElement *element = (XCUIElement *)XCUIElementDouble.new; - NSString *uuid = [self.cache storeElement:element]; - XCTAssertNotNil(uuid, @"Stored index should be higher than 0"); - XCUIElement *cachedElement = [self.cache elementForUUID:uuid]; - XCTAssertEqual(element, cachedElement); -} - -- (void)testFetchingBadIndex -{ - XCTAssertThrows([self.cache elementForUUID:@"random"]); -} - -- (void)testLinearCacheExpulsion -{ - const int ELEMENT_COUNT = 1050; - - NSMutableArray *elements = [NSMutableArray arrayWithCapacity:ELEMENT_COUNT]; - NSMutableArray *elementIds = [NSMutableArray arrayWithCapacity:ELEMENT_COUNT]; - for(int i = 0; i < ELEMENT_COUNT; i++) { - XCUIElementDouble *el = XCUIElementDouble.new; - el.wdUID = [NSString stringWithFormat:@"%@", @(i)]; - [elements addObject:(XCUIElement *)el]; - } - - // The capacity of the cache is limited to 1024 elements. Add 1050 - // elements and make sure: - // - The first 26 elements are no longer present in the cache - // - The remaining 1024 elements are present in the cache - for(int i = 0; i < ELEMENT_COUNT; i++) { - [elementIds addObject:[self.cache storeElement:elements[i]]]; - } - - for(int i = 0; i < ELEMENT_COUNT - ELEMENT_CACHE_SIZE; i++) { - XCTAssertThrows([self.cache elementForUUID:elementIds[i]]); - } - for(int i = ELEMENT_COUNT - ELEMENT_CACHE_SIZE; i < ELEMENT_COUNT; i++) { - XCTAssertEqual(elements[i], [self.cache elementForUUID:elementIds[i]]); - } -} - -- (void)testMRUCacheExpulsion -{ - const int ELEMENT_COUNT = 1050; - const int ACCESSED_ELEMENT_COUNT = 24; - - NSMutableArray *elements = [NSMutableArray arrayWithCapacity:ELEMENT_COUNT]; - NSMutableArray *elementIds = [NSMutableArray arrayWithCapacity:ELEMENT_COUNT]; - for(int i = 0; i < ELEMENT_COUNT; i++) { - XCUIElementDouble *el = XCUIElementDouble.new; - el.wdUID = [NSString stringWithFormat:@"%@", @(i)]; - [elements addObject:(XCUIElement *)el]; - } - - // The capacity of the cache is limited to 1024 elements. Add 1050 - // elements, but with a twist: access the first 24 elements before - // adding the last 50 elements. Then, make sure: - // - The first 24 elements are present in the cache - // - The next 26 elements are not present in the cache - // - The remaining 1000 elements are present in the cache - for(int i = 0; i < ELEMENT_CACHE_SIZE; i++) { - [elementIds addObject:[self.cache storeElement:elements[i]]]; - } - - for(int i = 0; i < ACCESSED_ELEMENT_COUNT; i++) { - [self.cache elementForUUID:elementIds[i]]; - } - - for(int i = ELEMENT_CACHE_SIZE; i < ELEMENT_COUNT; i++) { - [elementIds addObject:[self.cache storeElement:elements[i]]]; - } - - for(int i = 0; i < ACCESSED_ELEMENT_COUNT; i++) { - XCTAssertEqual(elements[i], [self.cache elementForUUID:elementIds[i]]); - } - for(int i = ACCESSED_ELEMENT_COUNT; i < ELEMENT_COUNT - ELEMENT_CACHE_SIZE + ACCESSED_ELEMENT_COUNT; i++) { - XCTAssertThrows([self.cache elementForUUID:elementIds[i]]); - } - for(int i = ELEMENT_COUNT - ELEMENT_CACHE_SIZE + ACCESSED_ELEMENT_COUNT; i < ELEMENT_COUNT; i++) { - XCTAssertEqual(elements[i], [self.cache elementForUUID:elementIds[i]]); - } -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/UnitTests/FBElementTypeTransformerTests.m b/WebDriverAgent/WebDriverAgentTests/UnitTests/FBElementTypeTransformerTests.m deleted file mode 100644 index ad1aa98441..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/UnitTests/FBElementTypeTransformerTests.m +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBElementTypeTransformer.h" - -@interface FBElementTypeTransformerTests : XCTestCase -@end - -@implementation FBElementTypeTransformerTests - -- (void)testStringWithElementType -{ - XCTAssertEqualObjects(@"XCUIElementTypeAny", [FBElementTypeTransformer stringWithElementType:XCUIElementTypeAny]); - XCTAssertEqualObjects(@"XCUIElementTypeIcon", [FBElementTypeTransformer stringWithElementType:XCUIElementTypeIcon]); - XCTAssertEqualObjects(@"XCUIElementTypeTab", [FBElementTypeTransformer stringWithElementType:XCUIElementTypeTab]); - XCTAssertEqualObjects(@"XCUIElementTypeOther", [FBElementTypeTransformer stringWithElementType:XCUIElementTypeOther]); -} - -- (void)testShortStringWithElementType -{ - XCTAssertEqualObjects(@"Any", [FBElementTypeTransformer shortStringWithElementType:XCUIElementTypeAny]); - XCTAssertEqualObjects(@"Icon", [FBElementTypeTransformer shortStringWithElementType:XCUIElementTypeIcon]); - XCTAssertEqualObjects(@"Tab", [FBElementTypeTransformer shortStringWithElementType:XCUIElementTypeTab]); - XCTAssertEqualObjects(@"Other", [FBElementTypeTransformer shortStringWithElementType:XCUIElementTypeOther]); -} - -- (void)testElementTypeWithElementTypeName -{ - XCTAssertEqual(XCUIElementTypeAny, [FBElementTypeTransformer elementTypeWithTypeName:@"XCUIElementTypeAny"]); - XCTAssertEqual(XCUIElementTypeIcon, [FBElementTypeTransformer elementTypeWithTypeName:@"XCUIElementTypeIcon"]); - XCTAssertEqual(XCUIElementTypeTab, [FBElementTypeTransformer elementTypeWithTypeName:@"XCUIElementTypeTab"]); - XCTAssertEqual(XCUIElementTypeOther, [FBElementTypeTransformer elementTypeWithTypeName:@"XCUIElementTypeOther"]); - XCTAssertThrows([FBElementTypeTransformer elementTypeWithTypeName:@"Whatever"]); - XCTAssertThrows([FBElementTypeTransformer elementTypeWithTypeName:nil]); - XCTAssertEqual(XCUIElementTypeOther, [FBElementTypeTransformer elementTypeWithTypeName:@"XCUIElementTypeNewType"]); -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/UnitTests/FBElementUtilitiesTests.m b/WebDriverAgent/WebDriverAgentTests/UnitTests/FBElementUtilitiesTests.m deleted file mode 100644 index 5dad1c79de..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/UnitTests/FBElementUtilitiesTests.m +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - - -#import - -#import "FBElement.h" -#import "XCUIElementDouble.h" -#import "FBElementUtils.h" - -@interface FBElementUtilitiesTests : XCTestCase -@end - -@implementation FBElementUtilitiesTests - -- (void)testTypesFiltering { - NSMutableArray *elements = [NSMutableArray new]; - XCUIElementDouble *el1 = [XCUIElementDouble new]; - [elements addObject:el1]; - XCUIElementDouble *el2 = [XCUIElementDouble new]; - el2.elementType = XCUIElementTypeAlert; - el2.wdType = @"XCUIElementTypeAlert"; - [elements addObject:el2]; - XCUIElementDouble *el3 = [XCUIElementDouble new]; - [elements addObject:el3]; - - NSSet *result = [FBElementUtils uniqueElementTypesWithElements:elements]; - XCTAssertEqual([result count], 2); -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/UnitTests/FBErrorBuilderTests.m b/WebDriverAgent/WebDriverAgentTests/UnitTests/FBErrorBuilderTests.m deleted file mode 100644 index cc28116d14..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/UnitTests/FBErrorBuilderTests.m +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBErrorBuilder.h" - -@interface FBErrorBuilderTests : XCTestCase -@end - -@implementation FBErrorBuilderTests - -- (void)testErrorWithDescription -{ - NSString *expectedDescription = @"Magic description"; - NSError *error = - [[[FBErrorBuilder builder] - withDescription:expectedDescription] - build]; - XCTAssertNotNil(error); - XCTAssertEqualObjects([error localizedDescription], expectedDescription); -} - -- (void)testErrorWithDescriptionFormat -{ - NSError *error = - [[[FBErrorBuilder builder] - withDescriptionFormat:@"Magic %@", @"bob"] - build]; - XCTAssertEqualObjects([error localizedDescription], @"Magic bob"); -} - -- (void)testInnerError -{ - NSError *innerError = [NSError errorWithDomain:@"Domain" code:1 userInfo:@{}]; - NSError *error = - [[[FBErrorBuilder builder] - withInnerError:innerError] - build]; - XCTAssertEqual(error.userInfo[NSUnderlyingErrorKey], innerError); -} - -- (void)testBuildWithError -{ - NSString *expectedDescription = @"Magic description"; - NSError *error; - BOOL result = - [[[FBErrorBuilder builder] - withDescription:expectedDescription] - buildError:&error]; - XCTAssertNotNil(error); - XCTAssertEqualObjects(error.localizedDescription, expectedDescription); - XCTAssertFalse(result); -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/UnitTests/FBExceptionHandlerTests.m b/WebDriverAgent/WebDriverAgentTests/UnitTests/FBExceptionHandlerTests.m deleted file mode 100644 index 64845dd1a4..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/UnitTests/FBExceptionHandlerTests.m +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBAlert.h" -#import "FBExceptionHandler.h" -#import "FBExceptions.h" - - -@interface RouteResponseDouble : NSObject -- (void)setHeader:(NSString *)field value:(NSString *)value; -- (void)setStatusCode:(NSUInteger)code; -- (void)respondWithData:(NSData *)data; -@end - -@implementation RouteResponseDouble -- (void)setHeader:(NSString *)field value:(NSString *)value {} -- (void)setStatusCode:(NSUInteger)code {} -- (void)respondWithData:(NSData *)data {} -@end - - -@interface FBExceptionHandlerTests : XCTestCase -@property (nonatomic) FBExceptionHandler *exceptionHandler; -@end - -@implementation FBExceptionHandlerTests - -- (void)setUp -{ - self.exceptionHandler = [FBExceptionHandler new]; -} - -- (void)testMatchingErrorHandling -{ - NSException *exception = [NSException exceptionWithName:FBElementNotVisibleException - reason:@"reason" - userInfo:@{}]; - [self.exceptionHandler handleException:exception - forResponse:(RouteResponse *)[RouteResponseDouble new]]; -} - -- (void)testNonMatchingErrorHandling -{ - NSException *exception = [NSException exceptionWithName:@"something" - reason:@"reason" - userInfo:@{}]; - [self.exceptionHandler handleException:exception - forResponse:(RouteResponse *)[RouteResponseDouble new]]; -} - - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/UnitTests/FBLRUCacheTests.m b/WebDriverAgent/WebDriverAgentTests/UnitTests/FBLRUCacheTests.m deleted file mode 100644 index e8c6528205..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/UnitTests/FBLRUCacheTests.m +++ /dev/null @@ -1,127 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "LRUCache.h" - -@interface FBLRUCacheTests : XCTestCase -@end - -@implementation FBLRUCacheTests - -- (void)assertArray:(NSArray *)array1 equalsTo:(NSArray *)array2 -{ - XCTAssertEqualObjects(array1, array2); -} - -- (void)testRecentlyInsertedObjectReplacesTheOldestOne -{ - LRUCache *cache = [[LRUCache alloc] initWithCapacity:1]; - [cache setObject:@"foo" forKey:@"bar"]; - [cache setObject:@"foo2" forKey:@"bar2"]; - [cache setObject:@"foo3" forKey:@"bar3"]; - XCTAssertEqualObjects(@[@"foo3"], cache.allObjects); -} - -- (void)testRecentObjectReplacementAndBump -{ - LRUCache *cache = [[LRUCache alloc] initWithCapacity:2]; - [cache setObject:@"foo" forKey:@"bar"]; - [cache setObject:@"foo2" forKey:@"bar2"]; - [self assertArray:@[@"foo2", @"foo"] equalsTo:cache.allObjects]; - XCTAssertNotNil([cache objectForKey:@"bar"]); - [self assertArray:@[@"foo", @"foo2"] equalsTo:cache.allObjects]; - [cache setObject:@"foo3" forKey:@"bar3"]; - [self assertArray:@[@"foo3", @"foo"] equalsTo:cache.allObjects]; - [cache setObject:@"foo0" forKey:@"bar"]; - [self assertArray:@[@"foo0", @"foo3"] equalsTo:cache.allObjects]; - [cache setObject:@"foo4" forKey:@"bar4"]; - [self assertArray:@[@"foo4", @"foo0"] equalsTo:cache.allObjects]; -} - -- (void)testBumpFromHead -{ - LRUCache *cache = [[LRUCache alloc] initWithCapacity:3]; - [cache setObject:@"foo" forKey:@"bar"]; - [cache setObject:@"foo2" forKey:@"bar2"]; - [cache setObject:@"foo3" forKey:@"bar3"]; - XCTAssertNotNil([cache objectForKey:@"bar3"]); - [self assertArray:@[@"foo3", @"foo2", @"foo"] equalsTo:cache.allObjects]; - [cache setObject:@"foo4" forKey:@"bar4"]; - [cache setObject:@"foo5" forKey:@"bar5"]; - [self assertArray:@[@"foo5", @"foo4", @"foo3"] equalsTo:cache.allObjects]; -} - -- (void)testBumpFromMiddle -{ - LRUCache *cache = [[LRUCache alloc] initWithCapacity:3]; - [cache setObject:@"foo" forKey:@"bar"]; - [cache setObject:@"foo2" forKey:@"bar2"]; - [cache setObject:@"foo3" forKey:@"bar3"]; - XCTAssertNotNil([cache objectForKey:@"bar2"]); - [self assertArray:@[@"foo2", @"foo3", @"foo"] equalsTo:cache.allObjects]; - [cache setObject:@"foo4" forKey:@"bar4"]; - [cache setObject:@"foo5" forKey:@"bar5"]; - [self assertArray:@[@"foo5", @"foo4", @"foo2"] equalsTo:cache.allObjects]; -} - -- (void)testBumpFromTail -{ - LRUCache *cache = [[LRUCache alloc] initWithCapacity:3]; - [cache setObject:@"foo" forKey:@"bar"]; - [cache setObject:@"foo2" forKey:@"bar2"]; - [cache setObject:@"foo3" forKey:@"bar3"]; - XCTAssertNotNil([cache objectForKey:@"bar3"]); - [self assertArray:@[@"foo3", @"foo2", @"foo"] equalsTo:cache.allObjects]; - [cache setObject:@"foo4" forKey:@"bar4"]; - [cache setObject:@"foo5" forKey:@"bar5"]; - [self assertArray:@[@"foo5", @"foo4", @"foo3"] equalsTo:cache.allObjects]; -} - -- (void)testInsertionLoop -{ - LRUCache *cache = [[LRUCache alloc] initWithCapacity:1]; - NSUInteger count = 100; - for (NSUInteger i = 0; i <= count; ++i) { - [cache setObject:@(i) forKey:@(i)]; - XCTAssertNotNil([cache objectForKey:@(i)]); - } - XCTAssertEqualObjects(@[@(count)], cache.allObjects); -} - -- (void)testRemoveExistingObjectForKey { - LRUCache *cache = [[LRUCache alloc] initWithCapacity:3]; - [cache setObject:@"foo" forKey:@"bar"]; - [cache setObject:@"foo2" forKey:@"bar2"]; - [cache setObject:@"foo3" forKey:@"bar3"]; - [self assertArray:@[@"foo3", @"foo2", @"foo"] equalsTo:cache.allObjects]; - [cache removeObjectForKey:@"bar2"]; - XCTAssertNil([cache objectForKey:@"bar2"]); - [self assertArray:@[@"foo3", @"foo"] equalsTo:cache.allObjects]; -} - -- (void)testRemoveNonExistingObjectForKey { - LRUCache *cache = [[LRUCache alloc] initWithCapacity:2]; - [cache setObject:@"foo" forKey:@"bar"]; - [cache removeObjectForKey:@"nonExisting"]; - XCTAssertNotNil([cache objectForKey:@"bar"]); - [self assertArray:@[@"foo"] equalsTo:cache.allObjects]; -} - -- (void)testRemoveAndInsertFlow { - LRUCache *cache = [[LRUCache alloc] initWithCapacity:2]; - [cache setObject:@"foo" forKey:@"bar"]; - [cache setObject:@"foo2" forKey:@"bar2"]; - [cache removeObjectForKey:@"bar"]; - XCTAssertNil([cache objectForKey:@"bar"]); - [cache setObject:@"foo3" forKey:@"bar3"]; - [self assertArray:@[@"foo3", @"foo2"] equalsTo:cache.allObjects]; -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/UnitTests/FBMathUtilsTests.m b/WebDriverAgent/WebDriverAgentTests/UnitTests/FBMathUtilsTests.m deleted file mode 100644 index 8ca9b24dba..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/UnitTests/FBMathUtilsTests.m +++ /dev/null @@ -1,113 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBMathUtils.h" - -@interface FBMathUtilsTests : XCTestCase -@end - -@implementation FBMathUtilsTests - -- (void)testGetCenter -{ - XCTAssertTrue(CGPointEqualToPoint(FBRectGetCenter(CGRectMake(0, 0, 4, 4)), CGPointMake(2, 2))); - XCTAssertTrue(CGPointEqualToPoint(FBRectGetCenter(CGRectMake(1, 1, 4, 4)), CGPointMake(3, 3))); - XCTAssertTrue(CGPointEqualToPoint(FBRectGetCenter(CGRectMake(1, 3, 6, 14)), CGPointMake(4, 10))); -} - -- (void)testFuzzyEqualFloats -{ - XCTAssertTrue(FBFloatFuzzyEqualToFloat(0, 0, 0)); - XCTAssertTrue(FBFloatFuzzyEqualToFloat(0.5, 0.6, 0.2)); - XCTAssertTrue(FBFloatFuzzyEqualToFloat(0.6, 0.5, 0.2)); - XCTAssertTrue(FBFloatFuzzyEqualToFloat(0.5, 0.6, 0.10001)); -} - -- (void)testFuzzyNotEqualFloats -{ - XCTAssertFalse(FBFloatFuzzyEqualToFloat(0, 1, 0)); - XCTAssertFalse(FBFloatFuzzyEqualToFloat(1, 0, 0)); - XCTAssertFalse(FBFloatFuzzyEqualToFloat(0.5, 0.6, 0.05)); - XCTAssertFalse(FBFloatFuzzyEqualToFloat(0.6, 0.5, 0.05)); -} - -- (void)testFuzzyEqualPoints -{ - CGPoint referencePoint = CGPointMake(3, 3); - XCTAssertTrue(FBPointFuzzyEqualToPoint(referencePoint, CGPointMake(3, 3), 2)); - XCTAssertTrue(FBPointFuzzyEqualToPoint(referencePoint, CGPointMake(3, 4), 2)); - XCTAssertTrue(FBPointFuzzyEqualToPoint(referencePoint, CGPointMake(4, 3), 2)); -} - -- (void)testFuzzyNotEqualPoints -{ - CGPoint referencePoint = CGPointMake(3, 3); - XCTAssertFalse(FBPointFuzzyEqualToPoint(referencePoint, CGPointMake(5, 5), 1)); - XCTAssertFalse(FBPointFuzzyEqualToPoint(referencePoint, CGPointMake(3, 5), 1)); - XCTAssertFalse(FBPointFuzzyEqualToPoint(referencePoint, CGPointMake(5, 3), 1)); -} - -- (void)testFuzzyEqualSizes -{ - CGSize referenceSize = CGSizeMake(3, 3); - XCTAssertTrue(FBSizeFuzzyEqualToSize(referenceSize, CGSizeMake(3, 3), 2)); - XCTAssertTrue(FBSizeFuzzyEqualToSize(referenceSize, CGSizeMake(3, 4), 2)); - XCTAssertTrue(FBSizeFuzzyEqualToSize(referenceSize, CGSizeMake(4, 3), 2)); -} - -- (void)testFuzzyNotEqualSizes -{ - CGSize referenceSize = CGSizeMake(3, 3); - XCTAssertFalse(FBSizeFuzzyEqualToSize(referenceSize, CGSizeMake(5, 5), 1)); - XCTAssertFalse(FBSizeFuzzyEqualToSize(referenceSize, CGSizeMake(3, 5), 1)); - XCTAssertFalse(FBSizeFuzzyEqualToSize(referenceSize, CGSizeMake(5, 3), 1)); -} - -- (void)testFuzzyEqualRects -{ - CGRect referenceRect = CGRectMake(3, 3, 3, 3); - XCTAssertTrue(FBRectFuzzyEqualToRect(referenceRect, CGRectMake(3, 3, 3, 3), 2)); - XCTAssertTrue(FBRectFuzzyEqualToRect(referenceRect, CGRectMake(3, 4, 3, 3), 2)); - XCTAssertTrue(FBRectFuzzyEqualToRect(referenceRect, CGRectMake(4, 3, 3, 3), 2)); - XCTAssertTrue(FBRectFuzzyEqualToRect(referenceRect, CGRectMake(3, 3, 3, 4), 2)); - XCTAssertTrue(FBRectFuzzyEqualToRect(referenceRect, CGRectMake(3, 3, 4, 3), 2)); -} - -- (void)testFuzzyNotEqualRects -{ - CGRect referenceRect = CGRectMake(3, 3, 3, 3); - XCTAssertFalse(FBRectFuzzyEqualToRect(referenceRect, CGRectMake(5, 5, 5, 5), 1)); - XCTAssertFalse(FBRectFuzzyEqualToRect(referenceRect, CGRectMake(3, 5, 3, 3), 1)); - XCTAssertFalse(FBRectFuzzyEqualToRect(referenceRect, CGRectMake(5, 3, 3, 3), 1)); - XCTAssertFalse(FBRectFuzzyEqualToRect(referenceRect, CGRectMake(3, 3, 3, 5), 1)); - XCTAssertFalse(FBRectFuzzyEqualToRect(referenceRect, CGRectMake(3, 3, 5, 3), 1)); -} - -- (void)testFuzzyEqualRectsSymmetry -{ - CGRect referenceRect = CGRectMake(0, 0, 2, 2); - XCTAssertFalse(FBRectFuzzyEqualToRect(referenceRect, CGRectMake(1, 1, 3, 3), 1)); - XCTAssertFalse(FBRectFuzzyEqualToRect(referenceRect, CGRectMake(-1, -1, 1, 1), 1)); -} - -- (void)testSizeInversion -{ - const CGSize screenSizePortrait = CGSizeMake(10, 15); - const CGSize screenSizeLandscape = CGSizeMake(15, 10); - const CGFloat t = FBDefaultFrameFuzzyThreshold; - XCTAssertTrue(FBSizeFuzzyEqualToSize(screenSizePortrait, FBAdjustDimensionsForApplication(screenSizePortrait, UIInterfaceOrientationPortrait), t)); - XCTAssertTrue(FBSizeFuzzyEqualToSize(screenSizePortrait, FBAdjustDimensionsForApplication(screenSizePortrait, UIInterfaceOrientationPortraitUpsideDown), t)); - XCTAssertTrue(FBSizeFuzzyEqualToSize(screenSizeLandscape, FBAdjustDimensionsForApplication(screenSizePortrait, UIInterfaceOrientationLandscapeLeft), t)); - XCTAssertTrue(FBSizeFuzzyEqualToSize(screenSizeLandscape, FBAdjustDimensionsForApplication(screenSizePortrait, UIInterfaceOrientationLandscapeRight), t)); - XCTAssertTrue(FBSizeFuzzyEqualToSize(screenSizeLandscape, FBAdjustDimensionsForApplication(screenSizeLandscape, UIInterfaceOrientationLandscapeLeft), t)); - XCTAssertTrue(FBSizeFuzzyEqualToSize(screenSizeLandscape, FBAdjustDimensionsForApplication(screenSizeLandscape, UIInterfaceOrientationLandscapeRight), t)); -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/UnitTests/FBProtocolHelpersTests.m b/WebDriverAgent/WebDriverAgentTests/UnitTests/FBProtocolHelpersTests.m deleted file mode 100644 index bc21b1b1aa..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/UnitTests/FBProtocolHelpersTests.m +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBProtocolHelpers.h" - -@interface FBProtocolHelpersTests : XCTestCase -@end - -@implementation FBProtocolHelpersTests - -- (void)testValidPrefixedCapsParsing -{ - NSError *error = nil; - NSDictionary *parsedCaps = FBParseCapabilities(@{ - @"firstMatch": @[@{ - @"appium:bundleId": @"com.example.id" - }] - }, &error); - XCTAssertNil(error); - XCTAssertEqualObjects(parsedCaps[@"bundleId"], @"com.example.id"); -} - -- (void)testValidPrefixedCapsMerging -{ - NSError *error = nil; - NSDictionary *parsedCaps = FBParseCapabilities(@{ - @"firstMatch": @[@{ - @"bundleId": @"com.example.id" - }], - @"alwaysMatch": @{ - @"google:cap": @"super" - } - }, &error); - XCTAssertNil(error); - XCTAssertEqualObjects(parsedCaps[@"bundleId"], @"com.example.id"); - XCTAssertEqualObjects(parsedCaps[@"google:cap"], @"super"); -} - -- (void)testEmptyCaps -{ - NSError *error = nil; - NSDictionary *parsedCaps = FBParseCapabilities(@{}, &error); - XCTAssertNil(error); - XCTAssertEqual(parsedCaps.count, 0); -} - -- (void)testCapsMergingFailure -{ - NSError *error = nil; - NSDictionary *parsedCaps = FBParseCapabilities(@{ - @"firstMatch": @[@{ - @"appium:bundleId": @"com.example.id" - }], - @"alwaysMatch": @{ - @"bundleId": @"other" - } - }, &error); - XCTAssertNil(parsedCaps); - XCTAssertNotNil(error); -} - -- (void)testPrefixingStandardCapability -{ - NSError *error = nil; - NSDictionary *parsedCaps = FBParseCapabilities(@{ - @"firstMatch": @[@{ - @"appium:platformName": @"com.example.id" - }] - }, &error); - XCTAssertNil(parsedCaps); - XCTAssertNotNil(error); -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/UnitTests/FBRouteTests.m b/WebDriverAgent/WebDriverAgentTests/UnitTests/FBRouteTests.m deleted file mode 100644 index 5dede0c5f2..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/UnitTests/FBRouteTests.m +++ /dev/null @@ -1,129 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBRoute.h" - -@class RouteResponse; - -@interface FBHandlerMock : NSObject -@property (nonatomic, assign) BOOL didCallSomeSelector; -@end - -@implementation FBHandlerMock -- (id)someSelector:(id)arg -{ - self.didCallSomeSelector = YES; - return nil; -}; - -@end - -@interface FBRouteTests : XCTestCase -@end - -@implementation FBRouteTests - -- (void)testGetRoute -{ - FBRoute *route = [FBRoute GET:@"/"]; - XCTAssertEqualObjects(route.verb, @"GET"); -} - -- (void)testPostRoute -{ - FBRoute *route = [FBRoute POST:@"/"]; - XCTAssertEqualObjects(route.verb, @"POST"); -} - -- (void)testPutRoute -{ - FBRoute *route = [FBRoute PUT:@"/"]; - XCTAssertEqualObjects(route.verb, @"PUT"); -} - -- (void)testDeleteRoute -{ - FBRoute *route = [FBRoute DELETE:@"/"]; - XCTAssertEqualObjects(route.verb, @"DELETE"); -} - -- (void)testTargetAction -{ - FBHandlerMock *mock = [FBHandlerMock new]; - FBRoute *route = [[FBRoute new] respondWithTarget:mock action:@selector(someSelector:)]; - [route mountRequest:(id)NSObject.new intoResponse:(id)NSObject.new]; - XCTAssertTrue(mock.didCallSomeSelector); -} - -- (void)testRespond -{ - XCTestExpectation *expectation = [self expectationWithDescription:@"Calling respond block works!"]; - FBRoute *route = [[FBRoute new] respondWithBlock:^id(FBRouteRequest *request) { - [expectation fulfill]; - return nil; - }]; - [route mountRequest:(id)NSObject.new intoResponse:(id)NSObject.new]; - [self waitForExpectationsWithTimeout:0.0 handler:nil]; -} - -- (void)testRouteWithSessionWithSlash -{ - FBRoute *route = [[FBRoute POST:@"/deactivateApp"] respondWithTarget:self action:@selector(dummyHandler:)]; - XCTAssertEqualObjects(route.path, @"/session/:sessionID/deactivateApp"); -} - -- (void)testRouteWithSession -{ - FBRoute *route = [[FBRoute POST:@"deactivateApp"] respondWithTarget:self action:@selector(dummyHandler:)]; - XCTAssertEqualObjects(route.path, @"/session/:sessionID/deactivateApp"); -} - -- (void)testRouteWithoutSessionWithSlash -{ - FBRoute *route = [[FBRoute POST:@"/deactivateApp"].withoutSession respondWithTarget:self action:@selector(dummyHandler:)]; - XCTAssertEqualObjects(route.path, @"/deactivateApp"); -} - -- (void)testRouteWithoutSession -{ - FBRoute *route = [[FBRoute POST:@"deactivateApp"].withoutSession respondWithTarget:self action:@selector(dummyHandler:)]; - XCTAssertEqualObjects(route.path, @"/deactivateApp"); -} - -- (void)testEmptyRouteWithSession -{ - FBRoute *route = [[FBRoute POST:@""] respondWithTarget:self action:@selector(dummyHandler:)]; - XCTAssertEqualObjects(route.path, @"/session/:sessionID"); -} - -- (void)testEmptyRouteWithoutSession -{ - FBRoute *route = [[FBRoute POST:@""].withoutSession respondWithTarget:self action:@selector(dummyHandler:)]; - XCTAssertEqualObjects(route.path, @"/"); -} - -- (void)testEmptyRouteWithSessionWithSlash -{ - FBRoute *route = [[FBRoute POST:@"/"] respondWithTarget:self action:@selector(dummyHandler:)]; - XCTAssertEqualObjects(route.path, @"/session/:sessionID"); -} - -- (void)testEmptyRouteWithoutSessionWithSlash -{ - FBRoute *route = [[FBRoute POST:@"/"].withoutSession respondWithTarget:self action:@selector(dummyHandler:)]; - XCTAssertEqualObjects(route.path, @"/"); -} - -+ (id)dummyHandler:(FBRouteRequest *)request -{ - return nil; -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/UnitTests/FBRunLoopSpinnerTests.m b/WebDriverAgent/WebDriverAgentTests/UnitTests/FBRunLoopSpinnerTests.m deleted file mode 100644 index da3c41552d..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/UnitTests/FBRunLoopSpinnerTests.m +++ /dev/null @@ -1,98 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBRunLoopSpinner.h" - -@interface FBRunLoopSpinnerTests : XCTestCase -@property (nonatomic, strong) FBRunLoopSpinner *spinner; -@end - -/** - Non of the test methods should block testing thread. - If they do, that means they are broken - */ -@implementation FBRunLoopSpinnerTests - -- (void)setUp -{ - [super setUp]; - self.spinner = [[FBRunLoopSpinner new] timeout:0.1]; -} - -- (void)testSpinUntilCompletion -{ - __block BOOL _didExecuteBlock = NO; - [FBRunLoopSpinner spinUntilCompletion:^(void (^completion)(void)) { - _didExecuteBlock = YES; - completion(); - }]; - XCTAssertTrue(_didExecuteBlock); -} - -- (void)testSpinUntilTrue -{ - __block BOOL _didExecuteBlock = NO; - BOOL didSucceed = - [self.spinner spinUntilTrue:^BOOL{ - _didExecuteBlock = YES; - return YES; - }]; - XCTAssertTrue(didSucceed); - XCTAssertTrue(_didExecuteBlock); -} - -- (void)testSpinUntilTrueTimeout -{ - NSError *error; - BOOL didSucceed = - [self.spinner spinUntilTrue:^BOOL{ - return NO; - } error:&error]; - XCTAssertFalse(didSucceed); - XCTAssertNotNil(error); -} - -- (void)testSpinUntilTrueTimeoutMessage -{ - NSString *expectedMessage = @"Magic message"; - NSError *error; - BOOL didSucceed = - [[self.spinner timeoutErrorMessage:expectedMessage] - spinUntilTrue:^BOOL{ - return NO; - } error:&error]; - XCTAssertFalse(didSucceed); - XCTAssertEqual(error.localizedDescription, expectedMessage); -} - -- (void)testSpinUntilNotNil -{ - __block id expectedObject = NSObject.new; - NSError *error; - id returnedObject = - [self.spinner spinUntilNotNil:^id{ - return expectedObject; - } error:&error]; - XCTAssertNil(error); - XCTAssertEqual(returnedObject, expectedObject); -} - -- (void)testSpinUntilNotNilTimeout -{ - NSError *error; - id element = - [self.spinner spinUntilNotNil:^id{ - return nil; - } error:&error]; - XCTAssertNil(element); - XCTAssertNotNil(error); -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/UnitTests/FBRuntimeUtilsTests.m b/WebDriverAgent/WebDriverAgentTests/UnitTests/FBRuntimeUtilsTests.m deleted file mode 100644 index e937ecae76..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/UnitTests/FBRuntimeUtilsTests.m +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBRuntimeUtils.h" -#import "XCTestPrivateSymbols.h" - -@protocol FBMagicProtocol -@end - -const NSString *FBRuntimeUtilsTestsConstString = @"FBRuntimeUtilsTestsConstString"; - -@interface FBRuntimeUtilsTests : XCTestCase -@end - -@implementation FBRuntimeUtilsTests - -- (void)testClassesThatConformsToProtocol -{ - XCTAssertEqualObjects(@[self.class], FBClassesThatConformsToProtocol(@protocol(FBMagicProtocol))); -} - -- (void)testRetrievingFrameworkSymbols -{ - NSString *binaryPath = [NSBundle bundleForClass:self.class].executablePath; - NSString *symbolPointer = *(NSString*__autoreleasing*)FBRetrieveSymbolFromBinary(binaryPath.UTF8String, "FBRuntimeUtilsTestsConstString"); - XCTAssertNotNil(symbolPointer); - XCTAssertEqualObjects(symbolPointer, FBRuntimeUtilsTestsConstString); -} - -- (void)testXCTestSymbols -{ - XCTAssertTrue(XCDebugLogger != NULL); - XCTAssertTrue(XCSetDebugLogger != NULL); - XCTAssertNotNil(FB_XCAXAIsVisibleAttribute); - XCTAssertNotNil(FB_XCAXAIsElementAttribute); -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/UnitTests/FBSDKVersionTests.m b/WebDriverAgent/WebDriverAgentTests/UnitTests/FBSDKVersionTests.m deleted file mode 100644 index 666e912b63..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/UnitTests/FBSDKVersionTests.m +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBRuntimeUtils.h" - -@interface FBSDKVersionTests : XCTestCase -@property (nonatomic, readonly) NSString *currentSDKVersion; -@property (nonatomic, readonly) NSString *lowerSDKVersion; -@property (nonatomic, readonly) NSString *higherSDKVersion; -@end - -@implementation FBSDKVersionTests - -- (void)setUp -{ - [super setUp]; - NSDictionary *bundleDict = [[NSBundle mainBundle] infoDictionary]; - [bundleDict setValue:@"11.0" forKey:@"DTSDKName"]; - _currentSDKVersion = FBSDKVersion(); - _lowerSDKVersion = [NSString stringWithFormat:@"%@", @((int)[self.currentSDKVersion doubleValue] - 1)]; - _higherSDKVersion = [NSString stringWithFormat:@"%@", @((int)[self.currentSDKVersion doubleValue] + 1)]; -} - -- (void)testIsSDKVersionLessThanOrEqualTo -{ - XCTAssertTrue(isSDKVersionLessThanOrEqualTo(self.higherSDKVersion)); - XCTAssertFalse(isSDKVersionLessThanOrEqualTo(self.lowerSDKVersion)); - XCTAssertTrue(isSDKVersionLessThanOrEqualTo(self.currentSDKVersion)); -} - -- (void)testIsSDKVersionLessThan -{ - XCTAssertTrue(isSDKVersionLessThan(self.higherSDKVersion)); - XCTAssertFalse(isSDKVersionLessThan(self.lowerSDKVersion)); - XCTAssertFalse(isSDKVersionLessThan(self.currentSDKVersion)); -} - -- (void)testIsSDKVersionEqualTo -{ - XCTAssertFalse(isSDKVersionEqualTo(self.higherSDKVersion)); - XCTAssertFalse(isSDKVersionEqualTo(self.lowerSDKVersion)); - XCTAssertTrue(isSDKVersionEqualTo(self.currentSDKVersion)); -} - -- (void)testIsSDKVersionGreaterThanOrEqualTo -{ - XCTAssertFalse(isSDKVersionGreaterThanOrEqualTo(self.higherSDKVersion)); - XCTAssertTrue(isSDKVersionGreaterThanOrEqualTo(self.lowerSDKVersion)); - XCTAssertTrue(isSDKVersionGreaterThanOrEqualTo(self.currentSDKVersion)); -} - -- (void)testIsSDKVersionGreaterThan -{ - XCTAssertFalse(isSDKVersionGreaterThan(self.higherSDKVersion)); - XCTAssertTrue(isSDKVersionGreaterThan(self.lowerSDKVersion)); - XCTAssertFalse(isSDKVersionGreaterThan(self.currentSDKVersion)); -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/UnitTests/FBSessionTests.m b/WebDriverAgent/WebDriverAgentTests/UnitTests/FBSessionTests.m deleted file mode 100644 index 53713a351e..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/UnitTests/FBSessionTests.m +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBSession.h" -#import "FBConfiguration.h" -#import "XCUIApplicationDouble.h" - -@interface FBSessionTests : XCTestCase -@property (nonatomic, strong) FBSession *session; -@property (nonatomic, strong) XCUIApplication *testedApplication; -@property (nonatomic) BOOL shouldTerminateAppValue; -@end - -@implementation FBSessionTests - -- (void)setUp -{ - [super setUp]; - self.testedApplication = (id)XCUIApplicationDouble.new; - self.shouldTerminateAppValue = FBConfiguration.shouldTerminateApp; - [FBConfiguration setShouldTerminateApp:NO]; - self.session = [FBSession initWithApplication:self.testedApplication]; -} - -- (void)tearDown -{ - [self.session kill]; - [FBConfiguration setShouldTerminateApp:self.shouldTerminateAppValue]; - [super tearDown]; -} - -- (void)testSessionFetching -{ - FBSession *fetchedSession = [FBSession sessionWithIdentifier:self.session.identifier]; - XCTAssertEqual(self.session, fetchedSession); -} - -- (void)testSessionFetchingBadIdentifier -{ - XCTAssertNil([FBSession sessionWithIdentifier:@"FAKE_IDENTIFIER"]); -} - -- (void)testSessionCreation -{ - XCTAssertNotNil(self.session.identifier); - XCTAssertNotNil(self.session.elementCache); -} - -- (void)testActiveSession -{ - XCTAssertEqual(self.session, [FBSession activeSession]); -} - -- (void)testActiveSessionIsNilAfterKilling -{ - [self.session kill]; - XCTAssertNil([FBSession activeSession]); -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/UnitTests/FBXMLSafeStringTests.m b/WebDriverAgent/WebDriverAgentTests/UnitTests/FBXMLSafeStringTests.m deleted file mode 100644 index 9ddd39bb05..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/UnitTests/FBXMLSafeStringTests.m +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "NSString+FBXMLSafeString.h" - -@interface FBXMLSafeStringTests : XCTestCase -@end - -@implementation FBXMLSafeStringTests - -- (void)testSafeXmlStringTransformationWithEmptyReplacement { - NSString *withInvalidChar = [NSString stringWithFormat:@"bla%@", @"\uFFFF"]; - NSString *withoutInvalidChar = @"bla"; - XCTAssertNotEqualObjects(withInvalidChar, withoutInvalidChar); - XCTAssertEqualObjects([withInvalidChar fb_xmlSafeStringWithReplacement:@""], withoutInvalidChar); -} - -- (void)testSafeXmlStringTransformationWithNonEmptyReplacement { - NSString *withInvalidChar = [NSString stringWithFormat:@"bla%@", @"\uFFFF"]; - XCTAssertEqualObjects([withInvalidChar fb_xmlSafeStringWithReplacement:@"1"], @"bla1"); -} - -- (void)testSafeXmlStringTransformationWithSmileys { - NSString *validString = @"Yo👿"; - XCTAssertEqualObjects([validString fb_xmlSafeStringWithReplacement:@""], validString); -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/UnitTests/FBXPathTests.m b/WebDriverAgent/WebDriverAgentTests/UnitTests/FBXPathTests.m deleted file mode 100644 index dbe52e6549..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/UnitTests/FBXPathTests.m +++ /dev/null @@ -1,152 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBMacros.h" -#import "FBXPath.h" -#import "FBXPath-Private.h" -#import "XCUIElementDouble.h" -#import "XCElementSnapshotDouble.h" -#import "FBXCElementSnapshotWrapper+Helpers.h" - -@interface FBXPathTests : XCTestCase -@end - -@implementation FBXPathTests - -- (NSString *)xmlStringWithElement:(id)snapshot - xpathQuery:(nullable NSString *)query - excludingAttributes:(nullable NSArray *)excludedAttributes -{ - xmlDocPtr doc; - - xmlTextWriterPtr writer = xmlNewTextWriterDoc(&doc, 0); - NSMutableDictionary *elementStore = [NSMutableDictionary dictionary]; - int buffersize; - xmlChar *xmlbuff = NULL; - int rc = xmlTextWriterStartDocument(writer, NULL, "UTF-8", NULL); - if (rc >= 0) { - rc = [FBXPath xmlRepresentationWithRootElement:snapshot - writer:writer - elementStore:elementStore - query:query - excludingAttributes:excludedAttributes]; - if (rc >= 0) { - rc = xmlTextWriterEndDocument(writer); - } - } - if (rc >= 0) { - xmlDocDumpFormatMemory(doc, &xmlbuff, &buffersize, 1); - } - xmlFreeTextWriter(writer); - xmlFreeDoc(doc); - - XCTAssertTrue(rc >= 0); - XCTAssertEqual(1, [elementStore count]); - - NSString *result = [NSString stringWithCString:(const char *)xmlbuff encoding:NSUTF8StringEncoding]; - xmlFree(xmlbuff); - return result; -} - -- (void)testDefaultXPathPresentation -{ - XCElementSnapshotDouble *snapshot = [XCElementSnapshotDouble new]; - id element = (id)[FBXCElementSnapshotWrapper ensureWrapped:(id)snapshot]; - NSString *resultXml = [self xmlStringWithElement:(id)element - xpathQuery:nil - excludingAttributes:nil]; - NSLog(@"[DefaultXPath] Result XML:\n%@", resultXml); - NSString *expectedXml = [NSString stringWithFormat:@"\n<%@ type=\"%@\" value=\"%@\" name=\"%@\" label=\"%@\" enabled=\"%@\" visible=\"%@\" accessible=\"%@\" x=\"%@\" y=\"%@\" width=\"%@\" height=\"%@\" index=\"%lu\" traits=\"%@\" private_indexPath=\"top\"/>\n", - element.wdType, element.wdType, element.wdValue, element.wdName, element.wdLabel, FBBoolToString(element.wdEnabled), FBBoolToString(element.wdVisible), FBBoolToString(element.wdAccessible), element.wdRect[@"x"], element.wdRect[@"y"], element.wdRect[@"width"], element.wdRect[@"height"], element.wdIndex, element.wdTraits]; - XCTAssertTrue([resultXml isEqualToString: expectedXml]); -} - -- (void)testtXPathPresentationWithSomeAttributesExcluded -{ - XCElementSnapshotDouble *snapshot = [XCElementSnapshotDouble new]; - id element = (id)[FBXCElementSnapshotWrapper ensureWrapped:(id)snapshot]; - NSString *resultXml = [self xmlStringWithElement:(id)element - xpathQuery:nil - excludingAttributes:@[@"type", @"visible", @"value", @"index", @"traits", @"nativeFrame"]]; - NSString *expectedXml = [NSString stringWithFormat:@"\n<%@ name=\"%@\" label=\"%@\" enabled=\"%@\" accessible=\"%@\" x=\"%@\" y=\"%@\" width=\"%@\" height=\"%@\" private_indexPath=\"top\"/>\n", - element.wdType, element.wdName, element.wdLabel, FBBoolToString(element.wdEnabled), FBBoolToString(element.wdAccessible), element.wdRect[@"x"], element.wdRect[@"y"], element.wdRect[@"width"], element.wdRect[@"height"]]; - XCTAssertEqualObjects(resultXml, expectedXml); -} - -- (void)testXPathPresentationBasedOnQueryMatchingAllAttributes -{ - XCElementSnapshotDouble *snapshot = [XCElementSnapshotDouble new]; - snapshot.value = @"йоло<>&\""; - snapshot.label = @"a\nb"; - id element = (id)[FBXCElementSnapshotWrapper ensureWrapped:(id)snapshot]; - NSString *resultXml = [self xmlStringWithElement:(id)element - xpathQuery:[NSString stringWithFormat:@"//%@[@*]", element.wdType] - excludingAttributes:@[@"visible"]]; - NSString *expectedXml = [NSString stringWithFormat:@"\n<%@ type=\"%@\" value=\"%@\" name=\"%@\" label=\"%@\" enabled=\"%@\" visible=\"%@\" accessible=\"%@\" x=\"%@\" y=\"%@\" width=\"%@\" height=\"%@\" index=\"%lu\" hittable=\"%@\" traits=\"%@\" nativeFrame=\"%@\" private_indexPath=\"top\"/>\n", - element.wdType, element.wdType, @"йоло<>&"", element.wdName, @"a b", FBBoolToString(element.wdEnabled), FBBoolToString(element.wdVisible), FBBoolToString(element.wdAccessible), element.wdRect[@"x"], element.wdRect[@"y"], element.wdRect[@"width"], element.wdRect[@"height"], element.wdIndex, FBBoolToString(element.wdHittable), element.wdTraits, NSStringFromCGRect(element.wdNativeFrame)]; - XCTAssertEqualObjects(expectedXml, resultXml); -} - -- (void)testXPathPresentationBasedOnQueryMatchingSomeAttributes -{ - XCElementSnapshotDouble *snapshot = [XCElementSnapshotDouble new]; - id element = (id)[FBXCElementSnapshotWrapper ensureWrapped:(id)snapshot]; - NSString *resultXml = [self xmlStringWithElement:(id)element - xpathQuery:[NSString stringWithFormat:@"//%@[@%@ and contains(@%@, 'blabla')]", element.wdType, @"value", @"name"] - excludingAttributes:nil]; - NSString *expectedXml = [NSString stringWithFormat:@"\n<%@ value=\"%@\" name=\"%@\" private_indexPath=\"top\"/>\n", - element.wdType, element.wdValue, element.wdName]; - XCTAssertTrue([resultXml isEqualToString: expectedXml]); -} - -- (void)testSnapshotXPathResultsMatching -{ - xmlDocPtr doc; - - xmlTextWriterPtr writer = xmlNewTextWriterDoc(&doc, 0); - NSMutableDictionary *elementStore = [NSMutableDictionary dictionary]; - XCElementSnapshotDouble *snapshot = [XCElementSnapshotDouble new]; - id root = (id)[FBXCElementSnapshotWrapper ensureWrapped:(id)snapshot]; - NSString *query = [NSString stringWithFormat:@"//%@", root.wdType]; - int rc = xmlTextWriterStartDocument(writer, NULL, "UTF-8", NULL); - if (rc >= 0) { - rc = [FBXPath xmlRepresentationWithRootElement:(id)root - writer:writer - elementStore:elementStore - query:query - excludingAttributes:nil]; - if (rc >= 0) { - rc = xmlTextWriterEndDocument(writer); - } - } - if (rc < 0) { - xmlFreeTextWriter(writer); - xmlFreeDoc(doc); - XCTFail(@"Unable to create the source XML document"); - } - - xmlXPathObjectPtr queryResult = [FBXPath evaluate:query document:doc contextNode:NULL]; - if (NULL == queryResult) { - xmlFreeTextWriter(writer); - xmlFreeDoc(doc); - XCTAssertNotEqual(NULL, queryResult); - } - - NSArray *matchingSnapshots = [FBXPath collectMatchingSnapshots:queryResult->nodesetval - elementStore:elementStore]; - xmlXPathFreeObject(queryResult); - xmlFreeTextWriter(writer); - xmlFreeDoc(doc); - - XCTAssertNotNil(matchingSnapshots); - XCTAssertEqual(1, [matchingSnapshots count]); -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/UnitTests/Info.plist b/WebDriverAgent/WebDriverAgentTests/UnitTests/Info.plist deleted file mode 100644 index bf5eabced4..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/UnitTests/Info.plist +++ /dev/null @@ -1,24 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - com.facebook.wda.unitTests - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1 - - diff --git a/WebDriverAgent/WebDriverAgentTests/UnitTests/NSDictionaryFBUtf8SafeTests.m b/WebDriverAgent/WebDriverAgentTests/UnitTests/NSDictionaryFBUtf8SafeTests.m deleted file mode 100644 index 6662852c1f..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/UnitTests/NSDictionaryFBUtf8SafeTests.m +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "NSDictionary+FBUtf8SafeDictionary.h" - -@interface NSDictionaryFBUtf8SafeTests : XCTestCase -@end - -@implementation NSDictionaryFBUtf8SafeTests - -- (void)testEmptySafeDictConversion -{ - NSDictionary *d = @{}; - XCTAssertEqualObjects(d, d.fb_utf8SafeDictionary); -} - -- (void)testNonEmptySafeDictConversion -{ - NSDictionary *d = @{ - @"1": @[@3, @4], - @"5": @{@"6": @7, @"8": @9}, - @"10": @"11" - }; - XCTAssertEqualObjects(d, d.fb_utf8SafeDictionary); -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/UnitTests/NSExpressionFBFormatTests.m b/WebDriverAgent/WebDriverAgentTests/UnitTests/NSExpressionFBFormatTests.m deleted file mode 100644 index 6b7f3164f2..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/UnitTests/NSExpressionFBFormatTests.m +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "NSExpression+FBFormat.h" -#import "FBElementUtils.h" - -@interface NSExpressionFBFormatTests : XCTestCase -@end - -@implementation NSExpressionFBFormatTests - -- (void)testFormattingForExistingProperty -{ - NSExpression *expr = [NSExpression expressionWithFormat:@"wdName"]; - NSExpression *prop = [NSExpression fb_wdExpressionWithExpression:expr]; - XCTAssertEqualObjects([prop keyPath], @"wdName"); -} - -- (void)testFormattingForExistingPropertyShortcut -{ - NSExpression *expr = [NSExpression expressionWithFormat:@"visible"]; - NSExpression *prop = [NSExpression fb_wdExpressionWithExpression:expr]; - XCTAssertEqualObjects([prop keyPath], @"isWDVisible"); -} - -- (void)testFormattingForValidExpressionWOKeys -{ - NSExpression *expr = [NSExpression expressionWithFormat:@"1"]; - NSExpression *prop = [NSExpression fb_wdExpressionWithExpression:expr]; - XCTAssertEqualObjects([prop constantValue], [NSNumber numberWithInt:1]); -} - -- (void)testFormattingForExistingComplexProperty -{ - NSExpression *expr = [NSExpression expressionWithFormat:@"wdRect.x"]; - NSExpression *prop = [NSExpression fb_wdExpressionWithExpression:expr]; - XCTAssertEqualObjects([prop keyPath], @"wdRect.x"); -} - -- (void)testFormattingForExistingComplexPropertyWOPrefix -{ - NSExpression *expr = [NSExpression expressionWithFormat:@"rect.x"]; - NSExpression *prop = [NSExpression fb_wdExpressionWithExpression:expr]; - XCTAssertEqualObjects([prop keyPath], @"wdRect.x"); -} - -- (void)testFormattingForPredicateWithUnknownKey -{ - NSExpression *expr = [NSExpression expressionWithFormat:@"title"]; - XCTAssertThrowsSpecificNamed([NSExpression fb_wdExpressionWithExpression:expr], NSException, FBUnknownAttributeException); -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/UnitTests/NSPredicateFBFormatTests.m b/WebDriverAgent/WebDriverAgentTests/UnitTests/NSPredicateFBFormatTests.m deleted file mode 100644 index ef01af4a2c..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/UnitTests/NSPredicateFBFormatTests.m +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "NSPredicate+FBFormat.h" - -@interface NSPredicateFBFormatTests : XCTestCase -@end - -@implementation NSPredicateFBFormatTests - -- (void)testFormattingForExistingProperty -{ - NSPredicate *expr = [NSPredicate predicateWithFormat:@"wdName == 'blabla'"]; - XCTAssertNotNil([NSPredicate fb_formatSearchPredicate:expr]); -} - -- (void)testFormattingForExistingPropertyOnTheRightSide -{ - NSPredicate *expr = [NSPredicate predicateWithFormat:@"0 == wdAccessible"]; - XCTAssertNotNil([NSPredicate fb_formatSearchPredicate:expr]); -} - -- (void)testFormattingForExistingPropertyShortcut -{ - NSPredicate *expr = [NSPredicate predicateWithFormat:@"visible == 1"]; - XCTAssertNotNil([NSPredicate fb_formatSearchPredicate:expr]); -} - -- (void)testFormattingForComplexExpression -{ - NSPredicate *expr = [NSPredicate predicateWithFormat:@"visible == 1 AND NOT (type == 'blabla' OR NOT (label IN {'3', '4'}))"]; - XCTAssertNotNil([NSPredicate fb_formatSearchPredicate:expr]); -} - -- (void)testFormattingForValidExpressionWOKeys -{ - NSPredicate *expr = [NSPredicate predicateWithFormat:@"1 = 1"]; - XCTAssertNotNil([NSPredicate fb_formatSearchPredicate:expr]); -} - -- (void)testFormattingForExistingComplexProperty -{ - NSPredicate *expr = [NSPredicate predicateWithFormat:@"wdRect.x == '0'"]; - XCTAssertNotNil([NSPredicate fb_formatSearchPredicate:expr]); -} - -- (void)testFormattingForExistingComplexPropertyWOPrefix -{ - NSPredicate *expr = [NSPredicate predicateWithFormat:@"rect.x == '0'"]; - XCTAssertNotNil([NSPredicate fb_formatSearchPredicate:expr]); -} - -- (void)testFormattingForPredicateWithUnknownKey -{ - NSPredicate *expr = [NSPredicate predicateWithFormat:@"title == 'blabla'"]; - XCTAssertThrows([NSPredicate fb_formatSearchPredicate:expr]); -} - -- (void)testBlockPredicateCreation -{ - NSPredicate *expr = [NSPredicate predicateWithFormat:@"rect.x == '0'"]; - XCTAssertNotNil([NSPredicate fb_snapshotBlockPredicateWithPredicate:expr]); -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/UnitTests/XCUIElementHelpersTests.m b/WebDriverAgent/WebDriverAgentTests/UnitTests/XCUIElementHelpersTests.m deleted file mode 100644 index a12e73fcae..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/UnitTests/XCUIElementHelpersTests.m +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "FBElementUtils.h" - -@interface XCUIElementHelpersTests : XCTestCase -@property (nonatomic) NSDictionary *namesMapping; -@end - -@implementation XCUIElementHelpersTests - -- (void)setUp -{ - [super setUp]; - self.namesMapping = [FBElementUtils wdAttributeNamesMapping]; -} - -- (void)testMappingContainsNamesAndAliases -{ - XCTAssertTrue([self.namesMapping.allKeys containsObject:@"wdName"]); - XCTAssertTrue([self.namesMapping.allKeys containsObject:@"name"]); -} - -- (void)testMappingContainsCorrectValueForAttrbutesWithoutGetters -{ - XCTAssertTrue([[self.namesMapping objectForKey:@"label"] isEqualToString:@"wdLabel"]); - XCTAssertTrue([[self.namesMapping objectForKey:@"wdLabel"] isEqualToString:@"wdLabel"]); -} - -- (void)testMappingContainsCorrectValueForAttrbutesWithGetters -{ - XCTAssertTrue([[self.namesMapping objectForKey:@"visible"] isEqualToString:@"isWDVisible"]); - XCTAssertTrue([[self.namesMapping objectForKey:@"wdVisible"] isEqualToString:@"isWDVisible"]); -} - -- (void)testEachPropertyHasAlias -{ - NSArray *aliases = [self.namesMapping.allKeys filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"NOT(SELF beginsWith[c] 'wd')"]]; - NSArray *names = [self.namesMapping.allKeys filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"SELF beginsWith[c] 'wd'"]]; - XCTAssertEqual(aliases.count, names.count); -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/UnitTests_tvOS/Doubles/XCUIElementDouble.h b/WebDriverAgent/WebDriverAgentTests/UnitTests_tvOS/Doubles/XCUIElementDouble.h deleted file mode 100644 index 2a81730e23..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/UnitTests_tvOS/Doubles/XCUIElementDouble.h +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright (c) 2018-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import -#import -#import - -@class XCUIApplication; - -@interface XCUIElementDouble : NSObject -@property (nonatomic, strong, nonnull) XCUIApplication *application; -@property (nonatomic, readwrite, assign) CGRect frame; -@property (nonatomic, readwrite, nullable) id lastSnapshot; -@property (nonatomic, assign) BOOL fb_isObstructedByAlert; -@property (nonatomic, readwrite, copy, nonnull) NSDictionary *wdRect; -@property (nonatomic, readwrite, assign) CGRect wdFrame; -@property (nonatomic, readwrite, copy, nonnull) NSString *wdUID; -@property (nonatomic, copy, readwrite, nullable) NSString *wdName; -@property (nonatomic, copy, readwrite, nullable) NSString *wdLabel; -@property (nonatomic, copy, readwrite, nonnull) NSString *wdType; -@property (nonatomic, strong, readwrite, nullable) NSString *wdValue; -@property (nonatomic, readwrite, getter=isWDEnabled) BOOL wdEnabled; -@property (nonatomic, readwrite, getter=isWDSelected) BOOL wdSelected; -@property (nonatomic, readwrite) NSUInteger wdIndex; -@property (nonatomic, readwrite, getter=isWDVisible) BOOL wdVisible; -@property (nonatomic, readwrite, getter=isWDAccessible) BOOL wdAccessible; -@property (nonatomic, readwrite, getter=isWDFocused) BOOL wdFocused; -@property (nonatomic, readwrite, getter = isWDHittable) BOOL wdHittable; -@property (copy, nonnull) NSArray *children; -@property (nonatomic, readwrite, assign) XCUIElementType elementType; -@property (nonatomic, readwrite, getter=isWDAccessibilityContainer) BOOL wdAccessibilityContainer; - -- (void)resolve; -- (id _Nonnull)fb_standardSnapshot; -- (id _Nonnull)fb_customSnapshot; - -// Checks -@property (nonatomic, assign, readonly) BOOL didResolve; - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/UnitTests_tvOS/Doubles/XCUIElementDouble.m b/WebDriverAgent/WebDriverAgentTests/UnitTests_tvOS/Doubles/XCUIElementDouble.m deleted file mode 100644 index f3a7914fe6..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/UnitTests_tvOS/Doubles/XCUIElementDouble.m +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Copyright (c) 2018-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import "XCUIElementDouble.h" - -@interface XCUIElementDouble () -@property (nonatomic, assign, readwrite) BOOL didResolve; -@end - -@implementation XCUIElementDouble - -- (id)init -{ - self = [super init]; - if (self) { - self.wdFrame = CGRectMake(0, 0, 0, 0); - self.wdName = @"testName"; - self.wdLabel = @"testLabel"; - self.wdValue = @"magicValue"; - self.wdVisible = YES; - self.wdAccessible = YES; - self.wdEnabled = YES; - self.wdSelected = YES; - self.wdHittable = YES; - self.wdIndex = 0; -#if TARGET_OS_TV - self.wdFocused = YES; -#endif - self.children = @[]; - self.wdRect = @{@"x": @0, - @"y": @0, - @"width": @0, - @"height": @0, - }; - self.wdAccessibilityContainer = NO; - self.elementType = XCUIElementTypeOther; - self.wdType = @"XCUIElementTypeOther"; - self.wdUID = @"0"; - self.lastSnapshot = nil; - } - return self; -} - -- (id)fb_valueForWDAttributeName:(NSString *)name -{ - return @"test"; -} - -- (id)fb_standardSnapshot -{ - return [self lastSnapshot]; -} - -- (id)fb_customSnapshot -{ - return [self lastSnapshot]; -} - -- (void)resolve -{ - self.didResolve = YES; -} - -- (id)lastSnapshot -{ - return self; -} - -- (id)fb_uid -{ - return self.wdUID; -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/UnitTests_tvOS/FBTVNavigationTrackerTests.m b/WebDriverAgent/WebDriverAgentTests/UnitTests_tvOS/FBTVNavigationTrackerTests.m deleted file mode 100644 index 27fb94d1a1..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/UnitTests_tvOS/FBTVNavigationTrackerTests.m +++ /dev/null @@ -1,86 +0,0 @@ -/** - * Copyright (c) 2018-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import - -#import "XCUIElementDouble.h" -#import "FBTVNavigationTracker.h" -#import "FBTVNavigationTracker-Private.h" - -@interface FBTVNavigationTrackerTests : XCTestCase -@end - -@implementation FBTVNavigationTrackerTests - -- (void)testHorizontalDirectionWithItemShouldBeRight -{ - XCUIElementDouble *el1 = XCUIElementDouble.new; - - FBTVNavigationItem *item = [FBTVNavigationItem itemWithUid:@"123456789"]; - FBTVNavigationTracker *tracker = [FBTVNavigationTracker trackerWithTargetElement:(XCUIElement *)el1]; - - FBTVDirection direction = [tracker horizontalDirectionWithItem:item andDelta:0.1]; - XCTAssertEqual(FBTVDirectionRight, direction); -} - -- (void)testHorizontalDirectionWithItemShouldBeLeft -{ - XCUIElementDouble *el1 = XCUIElementDouble.new; - - FBTVNavigationItem *item = [FBTVNavigationItem itemWithUid:@"123456789"]; - FBTVNavigationTracker *tracker = [FBTVNavigationTracker trackerWithTargetElement:(XCUIElement *)el1]; - - FBTVDirection direction = [tracker horizontalDirectionWithItem:item andDelta:-0.1]; - XCTAssertEqual(FBTVDirectionLeft, direction); -} - -- (void)testHorizontalDirectionWithItemShouldBeNone -{ - XCUIElementDouble *el1 = XCUIElementDouble.new; - - FBTVNavigationItem *item = [FBTVNavigationItem itemWithUid:@"123456789"]; - FBTVNavigationTracker *tracker = [FBTVNavigationTracker trackerWithTargetElement:(XCUIElement *)el1]; - - FBTVDirection direction = [tracker horizontalDirectionWithItem:item andDelta:DBL_EPSILON]; - XCTAssertEqual(FBTVDirectionNone, direction); -} - -- (void)testVerticalDirectionWithItemShouldBeDown -{ - XCUIElementDouble *el1 = XCUIElementDouble.new; - - FBTVNavigationItem *item = [FBTVNavigationItem itemWithUid:@"123456789"]; - FBTVNavigationTracker *tracker = [FBTVNavigationTracker trackerWithTargetElement:(XCUIElement *)el1]; - - FBTVDirection direction = [tracker verticalDirectionWithItem:item andDelta:0.1]; - XCTAssertEqual(FBTVDirectionDown, direction); -} - -- (void)testVerticalDirectionWithItemShouldBeUp -{ - XCUIElementDouble *el1 = XCUIElementDouble.new; - - FBTVNavigationItem *item = [FBTVNavigationItem itemWithUid:@"123456789"]; - FBTVNavigationTracker *tracker = [FBTVNavigationTracker trackerWithTargetElement:(XCUIElement *)el1]; - - FBTVDirection direction = [tracker verticalDirectionWithItem:item andDelta:-0.1]; - XCTAssertEqual(FBTVDirectionUp, direction); -} - -- (void)testVerticalDirectionWithItemShouldBeNone -{ - XCUIElementDouble *el1 = XCUIElementDouble.new; - - FBTVNavigationItem *item = [FBTVNavigationItem itemWithUid:@"123456789"]; - FBTVNavigationTracker *tracker = [FBTVNavigationTracker trackerWithTargetElement:(XCUIElement *)el1]; - - FBTVDirection direction = [tracker verticalDirectionWithItem:item andDelta:DBL_EPSILON]; - XCTAssertEqual(FBTVDirectionNone, direction); -} - -@end diff --git a/WebDriverAgent/WebDriverAgentTests/UnitTests_tvOS/Info.plist b/WebDriverAgent/WebDriverAgentTests/UnitTests_tvOS/Info.plist deleted file mode 100644 index bf5eabced4..0000000000 --- a/WebDriverAgent/WebDriverAgentTests/UnitTests_tvOS/Info.plist +++ /dev/null @@ -1,24 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - com.facebook.wda.unitTests - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1 - - diff --git a/WebDriverAgent/azure-templates/base_job.yml b/WebDriverAgent/azure-templates/base_job.yml deleted file mode 100644 index e3b62c0d52..0000000000 --- a/WebDriverAgent/azure-templates/base_job.yml +++ /dev/null @@ -1,36 +0,0 @@ -parameters: - name: '' - action: '' - target: '' - dest: '' - sdk: '' - iphoneModel: '' - ipadModel: '' - tvModel: '' - iosVersion: '' - xcodeVersion: '' - tvVersion: '' - vmImage: '' - extraXcArgs: '' - - -jobs: - - job: ${{ parameters.name }} - pool: - vmImage: ${{ parameters.vmImage }} - variables: - ACTION: ${{ parameters.action }} - TARGET: ${{ parameters.target }} - DEST: ${{ parameters.dest }} - SDK: ${{ parameters.sdk }} - CODE_SIGN: ${{ parameters.codeSign }} - IPHONE_MODEL: ${{ parameters.iphoneModel }} - TV_MODEL: ${{ parameters.tvModel }} - IPAD_MODEL: ${{ parameters.ipadModel }} - IOS_VERSION: ${{ parameters.iosVersion }} - XCODE_VERSION: ${{ parameters.xcodeVersion }} - TV_VERSION: ${{ parameters.tvVersion }} - EXTRA_XC_ARGS: ${{ parameters.extraXcArgs }} - steps: - - template: bootstrap_steps.yml - - script: ./Scripts/build.sh diff --git a/WebDriverAgent/azure-templates/bootstrap_steps.yml b/WebDriverAgent/azure-templates/bootstrap_steps.yml deleted file mode 100644 index 14c127e115..0000000000 --- a/WebDriverAgent/azure-templates/bootstrap_steps.yml +++ /dev/null @@ -1,7 +0,0 @@ -steps: - - script: sudo xcode-select --switch "/Applications/Xcode_$(XCODE_VERSION).app/Contents/Developer" - - script: mkdir -p ./Resources/WebDriverAgent.bundle - - task: UseRubyVersion@0 - inputs: - versionSpec: '3.3' - addToPath: true diff --git a/WebDriverAgent/azure-templates/node_setup_steps.yml b/WebDriverAgent/azure-templates/node_setup_steps.yml deleted file mode 100644 index 2da7b6b187..0000000000 --- a/WebDriverAgent/azure-templates/node_setup_steps.yml +++ /dev/null @@ -1,4 +0,0 @@ -steps: - - task: NodeTool@0 - inputs: - versionSpec: "$(DEFAULT_NODE_VERSION)" diff --git a/WebDriverAgent/ci-jobs/scripts/azure-print-tag-name.js b/WebDriverAgent/ci-jobs/scripts/azure-print-tag-name.js deleted file mode 100644 index 4471529798..0000000000 --- a/WebDriverAgent/ci-jobs/scripts/azure-print-tag-name.js +++ /dev/null @@ -1,3 +0,0 @@ - -const branch = process.env.BUILD_SOURCEBRANCH || ''; -console.log(branch.replace(/^refs\/tags\//, '')); // eslint-disable-line no-console \ No newline at end of file diff --git a/WebDriverAgent/ci-jobs/scripts/build-webdriveragents.js b/WebDriverAgent/ci-jobs/scripts/build-webdriveragents.js deleted file mode 100644 index be7a70496a..0000000000 --- a/WebDriverAgent/ci-jobs/scripts/build-webdriveragents.js +++ /dev/null @@ -1,57 +0,0 @@ -const buildWebDriverAgent = require('./build-webdriveragent'); -const { asyncify } = require('asyncbox'); -const { fs, logger } = require('@appium/support'); -const { exec } = require('teen_process'); -const path = require('path'); - -const log = new logger.getLogger('WDABuild'); - -async function buildAndUploadWebDriverAgents () { - // Get all xcode paths from /Applications/ - const xcodePaths = (await fs.readdir('/Applications/')) - .filter((file) => file.toLowerCase().startsWith('xcode_')); - - // Determine which xcodes need to be skipped - let excludedXcodeArr = (process.env.EXCLUDE_XCODE || '').replace(/\s/g, '').split(','); - log.info(`Will skip xcode versions: '${excludedXcodeArr}'`); - - for (let xcodePath of xcodePaths) { - if (xcodePath.includes('beta')) { - log.info(`Skipping beta Xcode '${xcodePath}'`); - continue; - } - - // Skip if .0 because redundant (example: skip 11.4.0 because it already does 11.4) - const [, , patch] = xcodePath.split('.'); - if (patch === '0') { - log.info(`Skipping xcode '${xcodePath}'`); - continue; - } - - // Build webdriveragent for this xcode version - log.info(`Running xcode-select for '${xcodePath}'`); - await exec('sudo', ['xcode-select', '-s', `/Applications/${xcodePath}/Contents/Developer`]); - const xcodeVersion = path.parse(xcodePath).name.split('_', 2)[1]; - - if (excludedXcodeArr.includes(xcodeVersion)) { - log.info(`Skipping xcode version '${xcodeVersion}'`); - continue; - } - - log.info('Building webdriveragent for xcode version', xcodeVersion); - try { - await buildWebDriverAgent(xcodeVersion); - } catch (e) { - log.error(`Skipping build for '${xcodeVersion} due to error: ${e}'`); - } - } - - // Divider log line - log.info('\n'); -} - -if (require.main === module) { - asyncify(buildAndUploadWebDriverAgents); -} - -module.exports = buildAndUploadWebDriverAgents; diff --git a/WebDriverAgent/ci-jobs/templates/build.yml b/WebDriverAgent/ci-jobs/templates/build.yml deleted file mode 100644 index d32d34a8f1..0000000000 --- a/WebDriverAgent/ci-jobs/templates/build.yml +++ /dev/null @@ -1,38 +0,0 @@ -parameters: - vmImage: 'macOS-11' - name: macOS_11 - excludeXcode: $(excludeXcode) -jobs: - - job: ${{ parameters.name }} - variables: - EXCLUDE_XCODE: ${{ parameters.excludeXcode }} - pool: - vmImage: ${{ parameters.vmImage }} - dependsOn: create_github_release - steps: - - script: node ./ci-jobs/scripts/azure-print-tag-name - displayName: Print Tag Name - - script: ls /Applications/ - displayName: List Installed Applications - - name: Use Node.js - uses: actions/setup-node@v3 - with: - node-version: lts/* - - script: npm install - displayName: Install Node Modules - - script: mkdir -p Resources/WebDriverAgent.bundle - displayName: Make Resources Folder - - script: node ./Scripts/build-webdriveragent.js - displayName: Build WebDriverAgents - - script: ls ./bundles - displayName: List WDA Bundles - - task: PublishPipelineArtifact@0 - inputs: - targetPath: bundles/ - artifactName: ${{ parameters.name }} - - script: | - brew install ghr - ghr $(node ./ci-jobs/scripts/azure-print-tag-name) bundles/ - env: - GITHUB_TOKEN: $(GITHUB_TOKEN) - displayName: Upload to GitHub Releases diff --git a/WebDriverAgent/eslint.config.mjs b/WebDriverAgent/eslint.config.mjs deleted file mode 100644 index 020f8c2d2c..0000000000 --- a/WebDriverAgent/eslint.config.mjs +++ /dev/null @@ -1,13 +0,0 @@ -import appiumConfig from '@appium/eslint-config-appium-ts'; - -export default [ - ...appiumConfig, - { - ignores: [ - 'Configurations/**', - 'Fastlane/**', - 'PrivateHeaders/**', - 'WebDriverAgent*/**' - ], - }, -]; diff --git a/WebDriverAgent/index.ts b/WebDriverAgent/index.ts deleted file mode 100644 index d8e3996bdf..0000000000 --- a/WebDriverAgent/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { checkForDependencies, bundleWDASim } from './lib/check-dependencies'; -export { NoSessionProxy } from './lib/no-session-proxy'; -export { WebDriverAgent } from './lib/webdriveragent'; -export { WDA_BASE_URL, WDA_RUNNER_BUNDLE_ID, PROJECT_FILE } from './lib/constants'; -export { resetTestProcesses, BOOTSTRAP_PATH } from './lib/utils'; - -export * from './lib/types'; diff --git a/WebDriverAgent/lib/check-dependencies.js b/WebDriverAgent/lib/check-dependencies.js deleted file mode 100644 index 40fd689a1b..0000000000 --- a/WebDriverAgent/lib/check-dependencies.js +++ /dev/null @@ -1,43 +0,0 @@ -import { fs } from '@appium/support'; -import { exec } from 'teen_process'; -import path from 'node:path'; -import { - WDA_SCHEME, SDK_SIMULATOR, WDA_RUNNER_APP -} from './constants'; -import { BOOTSTRAP_PATH } from './utils'; -import log from './logger'; - -async function buildWDASim () { - const args = [ - '-project', path.join(BOOTSTRAP_PATH, 'WebDriverAgent.xcodeproj'), - '-scheme', WDA_SCHEME, - '-sdk', SDK_SIMULATOR, - 'CODE_SIGN_IDENTITY=""', - 'CODE_SIGNING_REQUIRED="NO"', - 'GCC_TREAT_WARNINGS_AS_ERRORS=0', - ]; - await exec('xcodebuild', args); -} - -export async function checkForDependencies () { - log.debug('Dependencies are up to date'); - return false; -} - -/** - * - * @param {import('./xcodebuild').XcodeBuild} xcodebuild - * @returns {Promise} - */ -export async function bundleWDASim (xcodebuild) { - const derivedDataPath = await xcodebuild.retrieveDerivedDataPath(); - if (!derivedDataPath) { - throw new Error('Cannot retrieve the path to the Xcode derived data folder'); - } - const wdaBundlePath = path.join(derivedDataPath, 'Build', 'Products', 'Debug-iphonesimulator', WDA_RUNNER_APP); - if (await fs.exists(wdaBundlePath)) { - return wdaBundlePath; - } - await buildWDASim(); - return wdaBundlePath; -} diff --git a/WebDriverAgent/lib/constants.js b/WebDriverAgent/lib/constants.js deleted file mode 100644 index fd6ed48031..0000000000 --- a/WebDriverAgent/lib/constants.js +++ /dev/null @@ -1,24 +0,0 @@ -import path from 'path'; - -const DEFAULT_TEST_BUNDLE_SUFFIX = '.xctrunner'; -const WDA_RUNNER_BUNDLE_ID = 'com.facebook.WebDriverAgentRunner'; -const WDA_RUNNER_BUNDLE_ID_FOR_XCTEST = `${WDA_RUNNER_BUNDLE_ID}${DEFAULT_TEST_BUNDLE_SUFFIX}`; -const WDA_RUNNER_APP = 'WebDriverAgentRunner-Runner.app'; -const WDA_SCHEME = 'WebDriverAgentRunner'; -const PROJECT_FILE = 'project.pbxproj'; -const WDA_BASE_URL = 'http://127.0.0.1'; - -const PLATFORM_NAME_TVOS = 'tvOS'; -const PLATFORM_NAME_IOS = 'iOS'; - -const SDK_SIMULATOR = 'iphonesimulator'; -const SDK_DEVICE = 'iphoneos'; - -const WDA_UPGRADE_TIMESTAMP_PATH = path.join('.appium', 'webdriveragent', 'upgrade.time'); - -export { - WDA_RUNNER_BUNDLE_ID, WDA_RUNNER_APP, PROJECT_FILE, - WDA_SCHEME, PLATFORM_NAME_TVOS, PLATFORM_NAME_IOS, - SDK_SIMULATOR, SDK_DEVICE, WDA_BASE_URL, WDA_UPGRADE_TIMESTAMP_PATH, - WDA_RUNNER_BUNDLE_ID_FOR_XCTEST, DEFAULT_TEST_BUNDLE_SUFFIX -}; diff --git a/WebDriverAgent/lib/logger.js b/WebDriverAgent/lib/logger.js deleted file mode 100644 index 4727be0ffc..0000000000 --- a/WebDriverAgent/lib/logger.js +++ /dev/null @@ -1,5 +0,0 @@ -import { logger } from '@appium/support'; - -const log = logger.getLogger('WebDriverAgent'); - -export default log; diff --git a/WebDriverAgent/lib/no-session-proxy.js b/WebDriverAgent/lib/no-session-proxy.js deleted file mode 100644 index f0760c7013..0000000000 --- a/WebDriverAgent/lib/no-session-proxy.js +++ /dev/null @@ -1,26 +0,0 @@ -import { JWProxy } from '@appium/base-driver'; - - -class NoSessionProxy extends JWProxy { - constructor (opts = {}) { - super(opts); - } - - getUrlForProxy (url) { - if (url === '') { - url = '/'; - } - const proxyBase = `${this.scheme}://${this.server}:${this.port}${this.base}`; - let remainingUrl = ''; - if ((new RegExp('^/')).test(url)) { - remainingUrl = url; - } else { - throw new Error(`Did not know what to do with url '${url}'`); - } - remainingUrl = remainingUrl.replace(/\/$/, ''); // can't have trailing slashes - return proxyBase + remainingUrl; - } -} - -export { NoSessionProxy }; -export default NoSessionProxy; diff --git a/WebDriverAgent/lib/types.ts b/WebDriverAgent/lib/types.ts deleted file mode 100644 index 1d1d1af549..0000000000 --- a/WebDriverAgent/lib/types.ts +++ /dev/null @@ -1,129 +0,0 @@ -// WebDriverAgentLib/Utilities/FBSettings.h -export interface WDASettings { - elementResponseAttribute?: string; - shouldUseCompactResponses?: boolean; - mjpegServerScreenshotQuality?: number; - mjpegServerFramerate?: number; - screenshotQuality?: number; - elementResponseAttributes?: string; - mjpegScalingFactor?: number; - mjpegFixOrientation?: boolean; - keyboardAutocorrection?: boolean; - keyboardPrediction?: boolean; - customSnapshotTimeout?: number; - snapshotMaxDepth?: number; - useFirstMatch?: boolean; - boundElementsByIndex?: boolean; - reduceMotion?: boolean; - defaultActiveApplication?: string; - activeAppDetectionPoint?: string; - includeNonModalElements?: boolean; - defaultAlertAction?: 'accept' | 'dismiss'; - acceptAlertButtonSelector?: string; - dismissAlertButtonSelector?: string; - screenshotOrientation?: 'auto' | 'portrait' | 'portraitUpsideDown' | 'landscapeRight' | 'landscapeLeft' - waitForIdleTimeout?: number; - animationCoolOffTimeout?: number; - maxTypingFrequency?: number; - useClearTextShortcut?: boolean; -} - -// WebDriverAgentLib/Utilities/FBCapabilities.h -export interface WDACapabilities { - bundleId?: string; - initialUrl?: string; - arguments?: string[]; - environment?: Record; - eventloopIdleDelaySec?: number; - shouldWaitForQuiescence?: boolean; - shouldUseTestManagerForVisibilityDetection?: boolean; - maxTypingFrequency?: number; - shouldUseSingletonTestManager?: boolean; - waitForIdleTimeout?: number; - shouldUseCompactResponses?: number; - elementResponseFields?: unknown; - disableAutomaticScreenshots?: boolean; - shouldTerminateApp?: boolean; - forceAppLaunch?: boolean; - useNativeCachingStrategy?: boolean; - forceSimulatorSoftwareKeyboardPresence?: boolean; - defaultAlertAction?: 'accept' | 'dismiss'; - appLaunchStateTimeoutSec?: number; -} - -export interface WebDriverAgentArgs { - device: AppleDevice; // Required - platformVersion?: string; - platformName?: string; - iosSdkVersion?: string; - host?: string; - realDevice?: boolean; - wdaBundlePath?: string; - bootstrapPath?: string; - agentPath?: string; - wdaLocalPort?: number; - wdaRemotePort?: number; - wdaBaseUrl?: string; - wdaBindingIP?: string; - prebuildWDA?: boolean; - webDriverAgentUrl?: string; - wdaConnectionTimeout?: number; - useXctestrunFile?: boolean; - usePrebuiltWDA?: boolean; - derivedDataPath?: string; - mjpegServerPort?: number; - updatedWDABundleId?: string; - wdaLaunchTimeout?: number; - usePreinstalledWDA?: boolean; - updatedWDABundleIdSuffix?: string; - showXcodeLog?: boolean; - xcodeConfigFile?: string; - xcodeOrgId?: string; - xcodeSigningId?: string; - keychainPath?: string; - keychainPassword?: string; - useSimpleBuildTest?: boolean; - allowProvisioningDeviceRegistration?: boolean; - resultBundlePath?: string; - resultBundleVersion?: string; - reqBasePath?: string; - launchTimeout?: number; -} - -export interface AppleDevice { - udid: string; - simctl?: any; - devicectl?: any; - /** @deprecated We'll stop supporting idb */ - idb?: any; - [key: string]: any; -} - -export interface XcodeBuildArgs { - realDevice: boolean; // Required - agentPath: string; // Required - bootstrapPath: string; // Required - platformVersion?: string; - platformName?: string; - iosSdkVersion?: string; - showXcodeLog?: boolean; - xcodeConfigFile?: string; - xcodeOrgId?: string; - xcodeSigningId?: string; - keychainPath?: string; - keychainPassword?: string; - prebuildWDA?: boolean; - usePrebuiltWDA?: boolean; - useSimpleBuildTest?: boolean; - useXctestrunFile?: boolean; - launchTimeout?: number; - wdaRemotePort?: number; - wdaBindingIP?: string; - updatedWDABundleId?: string; - derivedDataPath?: string; - mjpegServerPort?: number; - prebuildDelay?: number; - allowProvisioningDeviceRegistration?: boolean; - resultBundlePath?: string; - resultBundleVersion?: string; -} diff --git a/WebDriverAgent/lib/utils.js b/WebDriverAgent/lib/utils.js deleted file mode 100644 index 3d0bb3ec09..0000000000 --- a/WebDriverAgent/lib/utils.js +++ /dev/null @@ -1,407 +0,0 @@ -import { fs, plist } from '@appium/support'; -import { exec } from 'teen_process'; -import path from 'path'; -import log from './logger'; -import _ from 'lodash'; -import { WDA_RUNNER_BUNDLE_ID, PLATFORM_NAME_TVOS } from './constants'; -import B from 'bluebird'; -import _fs from 'fs'; -import { waitForCondition } from 'asyncbox'; -import { arch } from 'os'; - -const PROJECT_FILE = 'project.pbxproj'; - -/** - * Calculates the path to the current module's root folder - * - * @returns {string} The full path to module root - * @throws {Error} If the current module root folder cannot be determined - */ -const getModuleRoot = _.memoize(function getModuleRoot () { - let currentDir = path.dirname(path.resolve(__filename)); - let isAtFsRoot = false; - while (!isAtFsRoot) { - const manifestPath = path.join(currentDir, 'package.json'); - try { - if (_fs.existsSync(manifestPath) && - JSON.parse(_fs.readFileSync(manifestPath, 'utf8')).name === 'appium-webdriveragent') { - return currentDir; - } - } catch {} - currentDir = path.dirname(currentDir); - isAtFsRoot = currentDir.length <= path.dirname(currentDir).length; - } - throw new Error('Cannot find the root folder of the appium-webdriveragent Node.js module'); -}); - -export const BOOTSTRAP_PATH = getModuleRoot(); - -async function getPIDsUsingPattern (pattern) { - const args = [ - '-if', // case insensitive, full cmdline match - pattern, - ]; - try { - const {stdout} = await exec('pgrep', args); - return stdout.split(/\s+/) - .map((x) => parseInt(x, 10)) - .filter(_.isInteger) - .map((x) => `${x}`); - } catch (err) { - log.debug(`'pgrep ${args.join(' ')}' didn't detect any matching processes. Return code: ${err.code}`); - return []; - } -} - -async function killAppUsingPattern (pgrepPattern) { - const signals = [2, 15, 9]; - for (const signal of signals) { - const matchedPids = await getPIDsUsingPattern(pgrepPattern); - if (_.isEmpty(matchedPids)) { - return; - } - const args = [`-${signal}`, ...matchedPids]; - try { - await exec('kill', args); - } catch (err) { - log.debug(`kill ${args.join(' ')} -> ${err.message}`); - } - if (signal === _.last(signals)) { - // there is no need to wait after SIGKILL - return; - } - try { - await waitForCondition(async () => { - const pidCheckPromises = matchedPids - .map((pid) => exec('kill', ['-0', pid]) - // the process is still alive - .then(() => false) - // the process is dead - .catch(() => true) - ); - return (await B.all(pidCheckPromises)) - .every((x) => x === true); - }, { - waitMs: 1000, - intervalMs: 100, - }); - return; - } catch { - // try the next signal - } - } -} - -/** - * Return true if the platformName is tvOS - * @param {string} platformName The name of the platorm - * @returns {boolean} Return true if the platformName is tvOS - */ -function isTvOS (platformName) { - return _.toLower(platformName) === _.toLower(PLATFORM_NAME_TVOS); -} - -async function replaceInFile (file, find, replace) { - let contents = await fs.readFile(file, 'utf8'); - - let newContents = contents.replace(find, replace); - if (newContents !== contents) { - await fs.writeFile(file, newContents, 'utf8'); - } -} - -/** - * Update WebDriverAgentRunner project bundle ID with newBundleId. - * This method assumes project file is in the correct state. - * @param {string} agentPath - Path to the .xcodeproj directory. - * @param {string} newBundleId the new bundle ID used to update. - */ -async function updateProjectFile (agentPath, newBundleId) { - let projectFilePath = path.resolve(agentPath, PROJECT_FILE); - try { - // Assuming projectFilePath is in the correct state, create .old from projectFilePath - await fs.copyFile(projectFilePath, `${projectFilePath}.old`); - await replaceInFile(projectFilePath, new RegExp(_.escapeRegExp(WDA_RUNNER_BUNDLE_ID), 'g'), newBundleId); - log.debug(`Successfully updated '${projectFilePath}' with bundle id '${newBundleId}'`); - } catch (err) { - log.debug(`Error updating project file: ${err.message}`); - log.warn(`Unable to update project file '${projectFilePath}' with ` + - `bundle id '${newBundleId}'. WebDriverAgent may not start`); - } -} - -/** - * Reset WebDriverAgentRunner project bundle ID to correct state. - * @param {string} agentPath - Path to the .xcodeproj directory. - */ -async function resetProjectFile (agentPath) { - const projectFilePath = path.join(agentPath, PROJECT_FILE); - try { - // restore projectFilePath from .old file - if (!await fs.exists(`${projectFilePath}.old`)) { - return; // no need to reset - } - await fs.mv(`${projectFilePath}.old`, projectFilePath); - log.debug(`Successfully reset '${projectFilePath}' with bundle id '${WDA_RUNNER_BUNDLE_ID}'`); - } catch (err) { - log.debug(`Error resetting project file: ${err.message}`); - log.warn(`Unable to reset project file '${projectFilePath}' with ` + - `bundle id '${WDA_RUNNER_BUNDLE_ID}'. WebDriverAgent has been ` + - `modified and not returned to the original state.`); - } -} - -async function setRealDeviceSecurity (keychainPath, keychainPassword) { - log.debug('Setting security for iOS device'); - await exec('security', ['-v', 'list-keychains', '-s', keychainPath]); - await exec('security', ['-v', 'unlock-keychain', '-p', keychainPassword, keychainPath]); - await exec('security', ['set-keychain-settings', '-t', '3600', '-l', keychainPath]); -} - -/** - * Information of the device under test - * @typedef {Object} DeviceInfo - * @property {boolean} isRealDevice - Equals to true if the current device is a real device - * @property {string} udid - The device UDID. - * @property {string} platformVersion - The platform version of OS. - * @property {string} platformName - The platform name of iOS, tvOS -*/ - -/** - * Arguments for setting xctestrun file - * @typedef {Object} XctestrunFileArgs - * @property {DeviceInfo} deviceInfo - Information of the device under test - * @property {string} sdkVersion - The Xcode SDK version of OS. - * @property {string} bootstrapPath - The folder path containing xctestrun file. - * @property {number|string} wdaRemotePort - The remote port WDA is listening on. - * @property {string} [wdaBindingIP] - The IP address to bind to. If not given, it binds to all interfaces. - */ -/** - * Creates xctestrun file per device & platform version. - * We expects to have WebDriverAgentRunner_iphoneos${sdkVersion|platformVersion}-arm64.xctestrun for real device - * and WebDriverAgentRunner_iphonesimulator${sdkVersion|platformVersion}-${x86_64|arm64}.xctestrun for simulator located @bootstrapPath - * Newer Xcode (Xcode 10.0 at least) generate xctestrun file following sdkVersion. - * e.g. Xcode which has iOS SDK Version 12.2 on an intel Mac host machine generates WebDriverAgentRunner_iphonesimulator.2-x86_64.xctestrun - * even if the cap has platform version 11.4 - * - * @param {XctestrunFileArgs} args - * @return {Promise} returns xctestrunFilePath for given device - * @throws if WebDriverAgentRunner_iphoneos${sdkVersion|platformVersion}-arm64.xctestrun for real device - * or WebDriverAgentRunner_iphonesimulator${sdkVersion|platformVersion}-x86_64.xctestrun for simulator is not found @bootstrapPath, - * then it will throw a file not found exception - */ -async function setXctestrunFile (args) { - const {deviceInfo, sdkVersion, bootstrapPath, wdaRemotePort, wdaBindingIP} = args; - const xctestrunFilePath = await getXctestrunFilePath(deviceInfo, sdkVersion, bootstrapPath); - const xctestRunContent = await plist.parsePlistFile(xctestrunFilePath); - const updateWDAPort = getAdditionalRunContent(deviceInfo.platformName, wdaRemotePort, wdaBindingIP); - const newXctestRunContent = _.merge(xctestRunContent, updateWDAPort); - await plist.updatePlistFile(xctestrunFilePath, newXctestRunContent, true); - - return xctestrunFilePath; -} - -/** - * Return the WDA object which appends existing xctest runner content - * @param {string} platformName - The name of the platform - * @param {number|string} wdaRemotePort - The remote port number - * @param {string} [wdaBindingIP] - The IP address to bind to. If not given, it binds to all interfaces. - * @return {object} returns a runner object which has USE_PORT and optionally USE_IP - */ -function getAdditionalRunContent (platformName, wdaRemotePort, wdaBindingIP) { - const runner = `WebDriverAgentRunner${isTvOS(platformName) ? '_tvOS' : ''}`; - return { - [runner]: { - EnvironmentVariables: { - // USE_PORT must be 'string' - USE_PORT: `${wdaRemotePort}`, - ...(wdaBindingIP ? { USE_IP: wdaBindingIP } : {}), - } - } - }; -} - -/** - * Return the path of xctestrun if it exists - * @param {DeviceInfo} deviceInfo - * @param {string} sdkVersion - The Xcode SDK version of OS. - * @param {string} bootstrapPath - The folder path containing xctestrun file. - * @returns {Promise} - */ -async function getXctestrunFilePath (deviceInfo, sdkVersion, bootstrapPath) { - // First try the SDK path, for Xcode 10 (at least) - const sdkBased = [ - path.resolve(bootstrapPath, `${deviceInfo.udid}_${sdkVersion}.xctestrun`), - sdkVersion, - ]; - // Next try Platform path, for earlier Xcode versions - const platformBased = [ - path.resolve(bootstrapPath, `${deviceInfo.udid}_${deviceInfo.platformVersion}.xctestrun`), - deviceInfo.platformVersion, - ]; - - for (const [filePath, version] of [sdkBased, platformBased]) { - if (await fs.exists(filePath)) { - log.info(`Using '${filePath}' as xctestrun file`); - return filePath; - } - const originalXctestrunFile = path.resolve(bootstrapPath, getXctestrunFileName(deviceInfo, version)); - if (await fs.exists(originalXctestrunFile)) { - // If this is first time run for given device, then first generate xctestrun file for device. - // We need to have a xctestrun file **per device** because we cant not have same wda port for all devices. - await fs.copyFile(originalXctestrunFile, filePath); - log.info(`Using '${filePath}' as xctestrun file copied by '${originalXctestrunFile}'`); - return filePath; - } - } - - throw new Error( - `If you are using 'useXctestrunFile' capability then you ` + - `need to have a xctestrun file (expected: ` + - `'${path.resolve(bootstrapPath, getXctestrunFileName(deviceInfo, sdkVersion))}')` - ); -} - - -/** - * Return the name of xctestrun file - * @param {DeviceInfo} deviceInfo - * @param {string} version - The Xcode SDK version of OS. - * @return {string} returns xctestrunFilePath for given device - */ -function getXctestrunFileName (deviceInfo, version) { - const archSuffix = deviceInfo.isRealDevice - ? `os${version}-arm64` - : `simulator${version}-${arch() === 'arm64' ? 'arm64' : 'x86_64'}`; - return `WebDriverAgentRunner_${isTvOS(deviceInfo.platformName) ? 'tvOS_appletv' : 'iphone'}${archSuffix}.xctestrun`; -} - -/** - * Ensures the process is killed after the timeout - * - * @param {string} name - * @param {import('teen_process').SubProcess} proc - * @returns {Promise} - */ -async function killProcess (name, proc) { - if (!proc || !proc.isRunning) { - return; - } - - log.info(`Shutting down '${name}' process (pid '${proc.proc?.pid}')`); - - log.info(`Sending 'SIGTERM'...`); - try { - await proc.stop('SIGTERM', 1000); - return; - } catch (err) { - if (!err.message.includes(`Process didn't end after`)) { - throw err; - } - log.debug(`${name} process did not end in a timely fashion: '${err.message}'.`); - } - - log.info(`Sending 'SIGKILL'...`); - try { - await proc.stop('SIGKILL'); - } catch (err) { - if (err.message.includes('not currently running')) { - // the process ended but for some reason we were not informed - return; - } - throw err; - } -} - -/** - * Generate a random integer. - * - * @return {number} A random integer number in range [low, hight). `low`` is inclusive and `high` is exclusive. - */ -function randomInt (low, high) { - return Math.floor(Math.random() * (high - low) + low); -} - -/** - * Retrieves WDA upgrade timestamp - * - * @return {Promise} The UNIX timestamp of the package manifest. The manifest only gets modified on - * package upgrade. - */ -async function getWDAUpgradeTimestamp () { - const packageManifest = path.resolve(getModuleRoot(), 'package.json'); - if (!await fs.exists(packageManifest)) { - return null; - } - const {mtime} = await fs.stat(packageManifest); - return mtime.getTime(); -} - -/** - * Kills running XCTest processes for the particular device. - * - * @param {string} udid - The device UDID. - * @param {boolean} isSimulator - Equals to true if the current device is a Simulator - */ -async function resetTestProcesses (udid, isSimulator) { - const processPatterns = [`xcodebuild.*${udid}`]; - if (isSimulator) { - processPatterns.push(`${udid}.*XCTRunner`); - // The pattern to find in case idb was used - processPatterns.push(`xctest.*${udid}`); - } - log.debug(`Killing running processes '${processPatterns.join(', ')}' for the device ${udid}...`); - await B.all(processPatterns.map(killAppUsingPattern)); -} - -/** - * Get the IDs of processes listening on the particular system port. - * It is also possible to apply additional filtering based on the - * process command line. - * - * @param {string|number} port - The port number. - * @param {?Function} filteringFunc - Optional lambda function, which - * receives command line string of the particular process - * listening on given port, and is expected to return - * either true or false to include/exclude the corresponding PID - * from the resulting array. - * @returns {Promise} - the list of matched process ids. - */ -async function getPIDsListeningOnPort (port, filteringFunc = null) { - const result = []; - try { - // This only works since Mac OS X El Capitan - const {stdout} = await exec('lsof', ['-ti', `tcp:${port}`]); - result.push(...(stdout.trim().split(/\n+/))); - } catch (e) { - if (e.code !== 1) { - // code 1 means no processes. Other errors need reporting - log.debug(`Error getting processes listening on port '${port}': ${e.stderr || e.message}`); - } - return result; - } - - if (!_.isFunction(filteringFunc)) { - return result; - } - return await B.filter(result, async (pid) => { - let stdout; - try { - ({stdout} = await exec('ps', ['-p', pid, '-o', 'command'])); - } catch (e) { - if (e.code === 1) { - // The process does not exist anymore, there's nothing to filter - return false; - } - throw e; - } - return await filteringFunc(stdout); - }); -} - -export { updateProjectFile, resetProjectFile, setRealDeviceSecurity, - getAdditionalRunContent, getXctestrunFileName, - setXctestrunFile, getXctestrunFilePath, killProcess, randomInt, - getWDAUpgradeTimestamp, resetTestProcesses, - getPIDsListeningOnPort, killAppUsingPattern, isTvOS -}; diff --git a/WebDriverAgent/lib/webdriveragent.js b/WebDriverAgent/lib/webdriveragent.js deleted file mode 100644 index e9e92d8725..0000000000 --- a/WebDriverAgent/lib/webdriveragent.js +++ /dev/null @@ -1,753 +0,0 @@ -import { waitForCondition } from 'asyncbox'; -import _ from 'lodash'; -import path from 'path'; -import url from 'url'; -import B from 'bluebird'; -import { JWProxy } from '@appium/base-driver'; -import { fs, util, plist } from '@appium/support'; -import defaultLogger from './logger'; -import { NoSessionProxy } from './no-session-proxy'; -import { - getWDAUpgradeTimestamp, resetTestProcesses, getPIDsListeningOnPort, BOOTSTRAP_PATH -} from './utils'; -import {XcodeBuild} from './xcodebuild'; -import AsyncLock from 'async-lock'; -import { exec } from 'teen_process'; -import { bundleWDASim } from './check-dependencies'; -import { - WDA_RUNNER_BUNDLE_ID, WDA_RUNNER_APP, - WDA_BASE_URL, WDA_UPGRADE_TIMESTAMP_PATH, DEFAULT_TEST_BUNDLE_SUFFIX -} from './constants'; -import {Xctest} from 'appium-ios-device'; -import {strongbox} from '@appium/strongbox'; - -const WDA_LAUNCH_TIMEOUT = 60 * 1000; -const WDA_AGENT_PORT = 8100; -const WDA_CF_BUNDLE_NAME = 'WebDriverAgentRunner-Runner'; -const SHARED_RESOURCES_GUARD = new AsyncLock(); -const RECENT_MODULE_VERSION_ITEM_NAME = 'recentWdaModuleVersion'; - -export class WebDriverAgent { - /** @type {string} */ - bootstrapPath; - - /** @type {string} */ - agentPath; - - /** @type {any} @deprecated for removal */ - idb; - - /** - * @param {import('appium-xcode').XcodeVersion | undefined} xcodeVersion @deprecated Will be removed as no actual usage. - * @param {import('./types').WebDriverAgentArgs} args - * @param {import('@appium/types').AppiumLogger?} [log=null] - */ - constructor (xcodeVersion, args, log = null) { - this.xcodeVersion = xcodeVersion; - - this.args = _.clone(args); - this.log = log ?? defaultLogger; - - this.device = args.device; - this.platformVersion = args.platformVersion; - this.platformName = args.platformName; - this.iosSdkVersion = args.iosSdkVersion; - this.host = args.host; - this.isRealDevice = !!args.realDevice; - /** @deprecated We'll stop supporting idb */ - this.idb = args.device.idb; - this.wdaBundlePath = args.wdaBundlePath; - - this.setWDAPaths(args.bootstrapPath, args.agentPath); - - this.wdaLocalPort = args.wdaLocalPort; - this.wdaRemotePort = ((this.isRealDevice ? args.wdaRemotePort : null) ?? args.wdaLocalPort) - || WDA_AGENT_PORT; - this.wdaBaseUrl = args.wdaBaseUrl || WDA_BASE_URL; - this.wdaBindingIP = args.wdaBindingIP; - this.prebuildWDA = args.prebuildWDA; - - // this.args.webDriverAgentUrl guiarantees the capabilities acually - // gave 'appium:webDriverAgentUrl' but 'this.webDriverAgentUrl' - // could be used for caching WDA with xcodebuild. - this.webDriverAgentUrl = args.webDriverAgentUrl; - - this.started = false; - - this.wdaConnectionTimeout = args.wdaConnectionTimeout; - - this.useXctestrunFile = args.useXctestrunFile; - this.usePrebuiltWDA = args.usePrebuiltWDA; - this.derivedDataPath = args.derivedDataPath; - this.mjpegServerPort = args.mjpegServerPort; - - this.updatedWDABundleId = args.updatedWDABundleId; - - this.wdaLaunchTimeout = args.wdaLaunchTimeout || WDA_LAUNCH_TIMEOUT; - this.usePreinstalledWDA = args.usePreinstalledWDA; - this.xctestApiClient = null; - this.updatedWDABundleIdSuffix = args.updatedWDABundleIdSuffix ?? DEFAULT_TEST_BUNDLE_SUFFIX; - - this.xcodebuild = this.canSkipXcodebuild - ? null - : new XcodeBuild(this.xcodeVersion, this.device, { - platformVersion: this.platformVersion, - platformName: this.platformName, - iosSdkVersion: this.iosSdkVersion, - agentPath: this.agentPath, - bootstrapPath: this.bootstrapPath, - realDevice: this.isRealDevice, - showXcodeLog: args.showXcodeLog, - xcodeConfigFile: args.xcodeConfigFile, - xcodeOrgId: args.xcodeOrgId, - xcodeSigningId: args.xcodeSigningId, - keychainPath: args.keychainPath, - keychainPassword: args.keychainPassword, - useSimpleBuildTest: args.useSimpleBuildTest, - usePrebuiltWDA: args.usePrebuiltWDA, - updatedWDABundleId: this.updatedWDABundleId, - launchTimeout: this.wdaLaunchTimeout, - wdaRemotePort: this.wdaRemotePort, - wdaBindingIP: this.wdaBindingIP, - useXctestrunFile: this.useXctestrunFile, - derivedDataPath: args.derivedDataPath, - mjpegServerPort: this.mjpegServerPort, - allowProvisioningDeviceRegistration: args.allowProvisioningDeviceRegistration, - resultBundlePath: args.resultBundlePath, - resultBundleVersion: args.resultBundleVersion, - }, this.log); - } - - /** - * Return true if the session does not need xcodebuild. - * @returns {boolean} Whether the session needs/has xcodebuild. - */ - get canSkipXcodebuild () { - // Use this.args.webDriverAgentUrl to guarantee - // the capabilities set gave the `appium:webDriverAgentUrl`. - return this.usePreinstalledWDA || !!this.args.webDriverAgentUrl; - } - - /** - * Return bundle id for WebDriverAgent to launch the WDA. - * The primary usage is with 'this.usePreinstalledWDA'. - * It adds `.xctrunner` as suffix by default but 'this.updatedWDABundleIdSuffix' - * lets skip it. - * - * @returns {string} Bundle ID for Xctest. - */ - get bundleIdForXctest () { - return `${this.updatedWDABundleId ? this.updatedWDABundleId : WDA_RUNNER_BUNDLE_ID}${this.updatedWDABundleIdSuffix}`; - } - - /** - * @param {string} [bootstrapPath] - * @param {string} [agentPath] - */ - setWDAPaths (bootstrapPath, agentPath) { - // allow the user to specify a place for WDA. This is undocumented and - // only here for the purposes of testing development of WDA - this.bootstrapPath = bootstrapPath || BOOTSTRAP_PATH; - this.log.info(`Using WDA path: '${this.bootstrapPath}'`); - - // for backward compatibility we need to be able to specify agentPath too - this.agentPath = agentPath || path.resolve(this.bootstrapPath, 'WebDriverAgent.xcodeproj'); - this.log.info(`Using WDA agent: '${this.agentPath}'`); - } - - /** - * @returns {Promise} - */ - async cleanupObsoleteProcesses () { - const obsoletePids = await getPIDsListeningOnPort(/** @type {string} */ (this.url.port), - (cmdLine) => cmdLine.includes('/WebDriverAgentRunner') && - !cmdLine.toLowerCase().includes(this.device.udid.toLowerCase())); - - if (_.isEmpty(obsoletePids)) { - this.log.debug(`No obsolete cached processes from previous WDA sessions ` + - `listening on port ${this.url.port} have been found`); - return; - } - - this.log.info(`Detected ${obsoletePids.length} obsolete cached process${obsoletePids.length === 1 ? '' : 'es'} ` + - `from previous WDA sessions. Cleaning them up`); - try { - await exec('kill', obsoletePids); - } catch (e) { - this.log.warn(`Failed to kill obsolete cached process${obsoletePids.length === 1 ? '' : 'es'} '${obsoletePids}'. ` + - `Original error: ${e.message}`); - } - } - - /** - * Return boolean if WDA is running or not - * @return {Promise} True if WDA is running - * @throws {Error} If there was invalid response code or body - */ - async isRunning () { - return !!(await this.getStatus()); - } - - /** - * @returns {string} - */ - get basePath () { - if (this.url.path === '/') { - return ''; - } - return this.url.path || ''; - } - - /** - * Return current running WDA's status like below - * { - * "state": "success", - * "os": { - * "name": "iOS", - * "version": "11.4", - * "sdkVersion": "11.3" - * }, - * "ios": { - * "simulatorVersion": "11.4", - * "ip": "172.254.99.34" - * }, - * "build": { - * "time": "Jun 24 2018 17:08:21", - * "productBundleIdentifier": "com.facebook.WebDriverAgentRunner" - * } - * } - * - * @param {number} [timeoutMs=0] If the given timeoutMs is zero or negative number, - * this function will return the response of `/status` immediately. If the given timeoutMs, - * this function will try to get the response of `/status` up to the timeoutMs. - * @return {Promise} State Object - * @throws {Error} If there was an error within timeoutMs timeout. - * No error is raised if zero or negative number for the timeoutMs. - */ - async getStatus (timeoutMs = 0) { - const noSessionProxy = new NoSessionProxy({ - server: this.url.hostname, - port: this.url.port, - base: this.basePath, - timeout: 3000, - }); - - const sendGetStatus = async () => await /** @type import('@appium/types').StringRecord */ (noSessionProxy.command('/status', 'GET')); - - if (_.isNil(timeoutMs) || timeoutMs <= 0) { - try { - return await sendGetStatus(); - } catch (err) { - this.log.debug(`WDA is not listening at '${this.url.href}'. Original error:: ${err.message}`); - return null; - } - } - - let lastError = null; - let status = null; - try { - await waitForCondition(async () => { - try { - status = await sendGetStatus(); - return true; - } catch (err) { - lastError = err; - } - return false; - }, { - waitMs: timeoutMs, - intervalMs: 300, - }); - } catch (err) { - this.log.debug(`Failed to get the status endpoint in ${timeoutMs} ms. ` + - `The last error while accessing ${this.url.href}: ${lastError}. Original error:: ${err.message}.`); - throw new Error(`WDA was not ready in ${timeoutMs} ms.`); - } - return status; - } - - /** - * Uninstall WDAs from the test device. - * Over Xcode 11, multiple WDA can be in the device since Xcode 11 generates different WDA. - * Appium does not expect multiple WDAs are running on a device. - * - * @returns {Promise} - */ - async uninstall () { - try { - const bundleIds = await this.device.getUserInstalledBundleIdsByBundleName(WDA_CF_BUNDLE_NAME); - if (_.isEmpty(bundleIds)) { - this.log.debug('No WDAs on the device.'); - return; - } - - this.log.debug(`Uninstalling WDAs: '${bundleIds}'`); - for (const bundleId of bundleIds) { - await this.device.removeApp(bundleId); - } - } catch (e) { - this.log.debug(e); - this.log.warn(`WebDriverAgent uninstall failed. Perhaps, it is already uninstalled? ` + - `Original error: ${e.message}`); - } - } - - async _cleanupProjectIfFresh () { - if (this.canSkipXcodebuild) { - return; - } - - const packageInfo = JSON.parse(await fs.readFile(path.join(BOOTSTRAP_PATH, 'package.json'), 'utf8')); - const box = strongbox(packageInfo.name); - let boxItem = box.getItem(RECENT_MODULE_VERSION_ITEM_NAME); - if (!boxItem) { - const timestampPath = path.resolve(process.env.HOME ?? '', WDA_UPGRADE_TIMESTAMP_PATH); - if (await fs.exists(timestampPath)) { - // TODO: It is probably a bit ugly to hardcode the recent version string, - // TODO: hovewer it should do the job as a temporary transition trick - // TODO: to switch from a hardcoded file path to the strongbox usage. - try { - boxItem = await box.createItemWithValue(RECENT_MODULE_VERSION_ITEM_NAME, '5.0.0'); - } catch (e) { - this.log.warn(`The actual module version cannot be persisted: ${e.message}`); - return; - } - } else { - this.log.info('There is no need to perform the project cleanup. A fresh install has been detected'); - try { - await box.createItemWithValue(RECENT_MODULE_VERSION_ITEM_NAME, packageInfo.version); - } catch (e) { - this.log.warn(`The actual module version cannot be persisted: ${e.message}`); - } - return; - } - } - - let recentModuleVersion = await boxItem.read(); - try { - recentModuleVersion = util.coerceVersion(recentModuleVersion, true); - } catch (e) { - this.log.warn(`The persisted module version string has been damaged: ${e.message}`); - this.log.info(`Updating it to '${packageInfo.version}' assuming the project clenup is not needed`); - await boxItem.write(packageInfo.version); - return; - } - - if (util.compareVersions(recentModuleVersion, '>=', packageInfo.version)) { - this.log.info( - `WebDriverAgent does not need a cleanup. The project sources are up to date ` + - `(${recentModuleVersion} >= ${packageInfo.version})` - ); - return; - } - - this.log.info( - `Cleaning up the WebDriverAgent project after the module upgrade has happened ` + - `(${recentModuleVersion} < ${packageInfo.version})` - ); - try { - // @ts-ignore xcodebuild should be set - await this.xcodebuild.cleanProject(); - await boxItem.write(packageInfo.version); - } catch (e) { - this.log.warn(`Cannot perform WebDriverAgent project cleanup. Original error: ${e.message}`); - } - } - - - /** - * @typedef {Object} LaunchWdaViaDeviceCtlOptions - * @property {Record} [env] environment variables for the launching WDA process - */ - - /** - * Launch WDA with preinstalled package with 'xcrun devicectl device process launch'. - * The WDA package must be prepared properly like published via - * https://github.com/appium/WebDriverAgent/releases - * with proper sign for this case. - * - * When we implement launching XCTest service via appium-ios-device, - * this implementation can be replaced with it. - * - * @param {LaunchWdaViaDeviceCtlOptions} [opts={}] launching WDA with devicectl command options. - * @return {Promise} - */ - async _launchViaDevicectl(opts = {}) { - const {env} = opts; - - await this.device.devicectl.launchApp( - this.bundleIdForXctest, { env, terminateExisting: true } - ); - } - - /** - * Launch WDA with preinstalled package without xcodebuild. - * @param {string} sessionId Launch WDA and establish the session with this sessionId - * @return {Promise} State Object - * @throws {Error} If there was an error within timeoutMs timeout. - * No error is raised if zero or negative number for the timeoutMs. - */ - async launchWithPreinstalledWDA(sessionId) { - const xctestEnv = { - USE_PORT: this.wdaLocalPort || WDA_AGENT_PORT, - WDA_PRODUCT_BUNDLE_IDENTIFIER: this.bundleIdForXctest - }; - if (this.mjpegServerPort) { - xctestEnv.MJPEG_SERVER_PORT = this.mjpegServerPort; - } - if (this.wdaBindingIP) { - xctestEnv.USE_IP = this.wdaBindingIP; - } - this.log.info('Launching WebDriverAgent on the device without xcodebuild'); - if (this.isRealDevice) { - // Current method to launch WDA process can be done via 'xcrun devicectl', - // but it has limitation about the WDA preinstalled package. - // https://github.com/appium/appium/issues/19206#issuecomment-2014182674 - if (this.platformVersion && util.compareVersions(this.platformVersion, '>=', '17.0')) { - await this._launchViaDevicectl({env: xctestEnv}); - } else { - this.xctestApiClient = new Xctest(this.device.udid, this.bundleIdForXctest, null, {env: xctestEnv}); - await this.xctestApiClient.start(); - } - } else { - await this.device.simctl.exec('launch', { - args: [ - '--terminate-running-process', - this.device.udid, - this.bundleIdForXctest, - ], - env: xctestEnv, - }); - } - - this.setupProxies(sessionId); - let status; - try { - status = await this.getStatus(this.wdaLaunchTimeout); - } catch { - throw new Error( - `Failed to start the preinstalled WebDriverAgent in ${this.wdaLaunchTimeout} ms. ` + - `The WebDriverAgent might not be properly built or the device might be locked. ` + - `The 'appium:wdaLaunchTimeout' capability modifies the timeout.` - ); - } - this.started = true; - return status; - } - - /** - * Return current running WDA's status like below after launching WDA - * { - * "state": "success", - * "os": { - * "name": "iOS", - * "version": "11.4", - * "sdkVersion": "11.3" - * }, - * "ios": { - * "simulatorVersion": "11.4", - * "ip": "172.254.99.34" - * }, - * "build": { - * "time": "Jun 24 2018 17:08:21", - * "productBundleIdentifier": "com.facebook.WebDriverAgentRunner" - * } - * } - * - * @param {string} sessionId Launch WDA and establish the session with this sessionId - * @return {Promise} State Object - * @throws {Error} If there was invalid response code or body - */ - async launch (sessionId) { - if (this.webDriverAgentUrl) { - this.log.info(`Using provided WebdriverAgent at '${this.webDriverAgentUrl}'`); - this.url = this.webDriverAgentUrl; - this.setupProxies(sessionId); - return await this.getStatus(); - } - - if (this.usePreinstalledWDA) { - return await this.launchWithPreinstalledWDA(sessionId); - } - - this.log.info('Launching WebDriverAgent on the device'); - - this.setupProxies(sessionId); - - if (!this.useXctestrunFile && !await fs.exists(this.agentPath)) { - throw new Error(`Trying to use WebDriverAgent project at '${this.agentPath}' but the ` + - 'file does not exist'); - } - - // useXctestrunFile and usePrebuiltWDA use existing dependencies - // It depends on user side - if (this.idb || this.useXctestrunFile || this.usePrebuiltWDA) { - this.log.info('Skipped WDA project cleanup according to the provided capabilities'); - } else { - const synchronizationKey = path.normalize(this.bootstrapPath); - await SHARED_RESOURCES_GUARD.acquire(synchronizationKey, - async () => await this._cleanupProjectIfFresh()); - } - - // We need to provide WDA local port, because it might be occupied - await resetTestProcesses(this.device.udid, !this.isRealDevice); - - if (this.idb) { - return await this.startWithIDB(); - } - - // @ts-ignore xcodebuild should be set - await this.xcodebuild.init(this.noSessionProxy); - - // Start the xcodebuild process - if (this.prebuildWDA) { - // @ts-ignore xcodebuild should be set - await this.xcodebuild.prebuild(); - } - // @ts-ignore xcodebuild should be set - return await this.xcodebuild.start(); - } - - /** - * @deprecated We'll stop supporting idb. Deprecated for removal. - * @returns {Promise} - */ - async startWithIDB () { - this.log.info('Will launch WDA with idb instead of xcodebuild since the corresponding flag is enabled'); - const {wdaBundleId, testBundleId} = await this.prepareWDA(); - const env = { - USE_PORT: this.wdaRemotePort, - WDA_PRODUCT_BUNDLE_IDENTIFIER: this.bundleIdForXctest, - }; - if (this.mjpegServerPort) { - env.MJPEG_SERVER_PORT = this.mjpegServerPort; - } - if (this.wdaBindingIP) { - env.USE_IP = this.wdaBindingIP; - } - - return await this.idb.runXCUITest(wdaBundleId, wdaBundleId, testBundleId, {env}); - } - - /** - * - * @param {string} wdaBundlePath - * @returns {Promise} - */ - async parseBundleId (wdaBundlePath) { - const infoPlistPath = path.join(wdaBundlePath, 'Info.plist'); - const infoPlist = await plist.parsePlist(await fs.readFile(infoPlistPath)); - if (!infoPlist.CFBundleIdentifier) { - throw new Error(`Could not find bundle id in '${infoPlistPath}'`); - } - return infoPlist.CFBundleIdentifier; - } - - /** - * @deprecated We'll stop using idb - * @returns {Promise<{wdaBundleId: string, testBundleId: string, wdaBundlePath: string}>} - */ - async prepareWDA () { - const wdaBundlePath = this.wdaBundlePath || await this.fetchWDABundle(); - const wdaBundleId = await this.parseBundleId(wdaBundlePath); - if (!await this.device.isAppInstalled(wdaBundleId)) { - await this.device.installApp(wdaBundlePath); - } - const testBundleId = await this.idb.installXCTestBundle(path.join(wdaBundlePath, 'PlugIns', 'WebDriverAgentRunner.xctest')); - return {wdaBundleId, testBundleId, wdaBundlePath}; - } - - /** - * @returns {Promise} - */ - async fetchWDABundle () { - if (!this.derivedDataPath) { - return await bundleWDASim(/** @type {XcodeBuild} */ (this.xcodebuild)); - } - const wdaBundlePaths = await fs.glob(`${this.derivedDataPath}/**/*${WDA_RUNNER_APP}/`, { - absolute: true, - }); - if (_.isEmpty(wdaBundlePaths)) { - throw new Error(`Could not find the WDA bundle in '${this.derivedDataPath}'`); - } - return wdaBundlePaths[0]; - } - - /** - * @returns {Promise} - */ - async isSourceFresh () { - const existsPromises = [ - 'Resources', - `Resources${path.sep}WebDriverAgent.bundle`, - ].map((subPath) => fs.exists(path.resolve(/** @type {String} */ (this.bootstrapPath), subPath))); - return (await B.all(existsPromises)).some((v) => v === false); - } - - /** - * @param {string} sessionId - * @returns {void} - */ - setupProxies (sessionId) { - const proxyOpts = { - log: this.log, - server: this.url.hostname ?? undefined, - port: parseInt(this.url.port ?? '', 10) || undefined, - base: this.basePath, - timeout: this.wdaConnectionTimeout, - keepAlive: true, - scheme: this.url.protocol ? this.url.protocol.replace(':', '') : 'http', - }; - if (this.args.reqBasePath) { - proxyOpts.reqBasePath = this.args.reqBasePath; - } - - this.jwproxy = new JWProxy(proxyOpts); - this.jwproxy.sessionId = sessionId; - this.proxyReqRes = this.jwproxy.proxyReqRes.bind(this.jwproxy); - - this.noSessionProxy = new NoSessionProxy(proxyOpts); - } - - /** - * @returns {Promise} - */ - async quit () { - if (this.usePreinstalledWDA) { - this.log.info('Stopping the XCTest session'); - if (this.xctestApiClient) { - this.xctestApiClient.stop(); - this.xctestApiClient = null; - } else { - try { - await this.device.simctl.terminateApp(this.bundleIdForXctest); - } catch (e) { - this.log.warn(e.message); - } - } - } else if (!this.args.webDriverAgentUrl) { - this.log.info('Shutting down sub-processes'); - await this.xcodebuild?.quit(); - await this.xcodebuild?.reset(); - } else { - this.log.debug('Do not stop xcodebuild nor XCTest session ' + - 'since the WDA session is managed by outside this driver.'); - } - - if (this.jwproxy) { - this.jwproxy.sessionId = null; - } - - this.started = false; - - if (!this.args.webDriverAgentUrl) { - // if we populated the url ourselves (during `setupCaching` call, for instance) - // then clean that up. If the url was supplied, we want to keep it - this.webDriverAgentUrl = undefined; - } - } - - /** - * @returns {import('url').UrlWithStringQuery} - */ - get url () { - if (!this._url) { - if (this.webDriverAgentUrl) { - this._url = url.parse(this.webDriverAgentUrl); - } else { - const port = this.wdaLocalPort || WDA_AGENT_PORT; - const {protocol, hostname} = url.parse(this.wdaBaseUrl || WDA_BASE_URL); - this._url = url.parse(`${protocol}//${this.wdaBindingIP || hostname}:${port}`); - } - } - return this._url; - } - - /** - * @param {string} _url - * @returns {void} - */ - set url (_url) { - this._url = url.parse(_url); - } - - /** - * @returns {boolean} - */ - get fullyStarted () { - return this.started; - } - - /** - * @param {boolean} started - * @returns {void}s - */ - set fullyStarted (started) { - this.started = started ?? false; - } - - /** - * @returns {Promise} - */ - async retrieveDerivedDataPath () { - if (this.canSkipXcodebuild) { - return; - } - return await /** @type {XcodeBuild} */ (this.xcodebuild).retrieveDerivedDataPath(); - } - - /** - * Reuse running WDA if it has the same bundle id with updatedWDABundleId. - * Or reuse it if it has the default id without updatedWDABundleId. - * Uninstall it if the method faces an exception for the above situation. - * @returns {Promise} - */ - async setupCaching () { - const status = await this.getStatus(); - if (!status || !status.build) { - this.log.debug('WDA is currently not running. There is nothing to cache'); - return; - } - - const { - productBundleIdentifier, - upgradedAt, - } = status.build; - // for real device - if (util.hasValue(productBundleIdentifier) && util.hasValue(this.updatedWDABundleId) && this.updatedWDABundleId !== productBundleIdentifier) { - this.log.info(`Will uninstall running WDA since it has different bundle id. The actual value is '${productBundleIdentifier}'.`); - return await this.uninstall(); - } - // for simulator - if (util.hasValue(productBundleIdentifier) && !util.hasValue(this.updatedWDABundleId) && WDA_RUNNER_BUNDLE_ID !== productBundleIdentifier) { - this.log.info(`Will uninstall running WDA since its bundle id is not equal to the default value ${WDA_RUNNER_BUNDLE_ID}`); - return await this.uninstall(); - } - - const actualUpgradeTimestamp = await getWDAUpgradeTimestamp(); - this.log.debug(`Upgrade timestamp of the currently bundled WDA: ${actualUpgradeTimestamp}`); - this.log.debug(`Upgrade timestamp of the WDA on the device: ${upgradedAt}`); - if (actualUpgradeTimestamp && upgradedAt && _.toLower(`${actualUpgradeTimestamp}`) !== _.toLower(`${upgradedAt}`)) { - this.log.info('Will uninstall running WDA since it has different version in comparison to the one ' + - `which is bundled with appium-xcuitest-driver module (${actualUpgradeTimestamp} != ${upgradedAt})`); - return await this.uninstall(); - } - - const message = util.hasValue(productBundleIdentifier) - ? `Will reuse previously cached WDA instance at '${this.url.href}' with '${productBundleIdentifier}'` - : `Will reuse previously cached WDA instance at '${this.url.href}'`; - this.log.info(`${message}. Set the wdaLocalPort capability to a value different from ${this.url.port} if this is an undesired behavior.`); - this.webDriverAgentUrl = this.url.href; - } - - /** - * Quit and uninstall running WDA. - * @returns {Promise} - */ - async quitAndUninstall () { - await this.quit(); - await this.uninstall(); - } -} - -export default WebDriverAgent; diff --git a/WebDriverAgent/lib/xcodebuild.js b/WebDriverAgent/lib/xcodebuild.js deleted file mode 100644 index 1cdfdd61b6..0000000000 --- a/WebDriverAgent/lib/xcodebuild.js +++ /dev/null @@ -1,479 +0,0 @@ -import { retryInterval } from 'asyncbox'; -import { SubProcess, exec } from 'teen_process'; -import { logger, timing } from '@appium/support'; -import defaultLogger from './logger'; -import B from 'bluebird'; -import { - setRealDeviceSecurity, setXctestrunFile, - updateProjectFile, resetProjectFile, killProcess, - getWDAUpgradeTimestamp, isTvOS -} from './utils'; -import _ from 'lodash'; -import path from 'path'; -import { WDA_RUNNER_BUNDLE_ID } from './constants'; - - -const DEFAULT_SIGNING_ID = 'iPhone Developer'; -const PREBUILD_DELAY = 0; -const RUNNER_SCHEME_IOS = 'WebDriverAgentRunner'; -const LIB_SCHEME_IOS = 'WebDriverAgentLib'; - -const ERROR_WRITING_ATTACHMENT = 'Error writing attachment data to file'; -const ERROR_COPYING_ATTACHMENT = 'Error copying testing attachment'; -const IGNORED_ERRORS = [ - ERROR_WRITING_ATTACHMENT, - ERROR_COPYING_ATTACHMENT, - 'Failed to remove screenshot at path', -]; -const IGNORED_ERRORS_PATTERN = new RegExp( - '(' + - IGNORED_ERRORS - .map((errStr) => _.escapeRegExp(errStr)) - .join('|') + - ')' -); - -const RUNNER_SCHEME_TV = 'WebDriverAgentRunner_tvOS'; -const LIB_SCHEME_TV = 'WebDriverAgentLib_tvOS'; - -const REAL_DEVICES_CONFIG_DOCS_LINK = 'https://appium.github.io/appium-xcuitest-driver/latest/preparation/real-device-config/'; - -const xcodeLog = logger.getLogger('Xcode'); - - -export class XcodeBuild { - /** @type {SubProcess} */ - xcodebuild; - - /** - * @param {import('appium-xcode').XcodeVersion | undefined} xcodeVersion @deprecated Will be removed as no actual usage. - * @param {import('./types').AppleDevice} device - * @param {import('./types').XcodeBuildArgs} args - * @param {import('@appium/types').AppiumLogger | null} [log=null] - */ - constructor (xcodeVersion, device, args, log = null) { - this.xcodeVersion = xcodeVersion; - - this.device = device; - this.log = log ?? defaultLogger; - - this.realDevice = args.realDevice; - - this.agentPath = args.agentPath; - this.bootstrapPath = args.bootstrapPath; - - this.platformVersion = args.platformVersion; - this.platformName = args.platformName; - this.iosSdkVersion = args.iosSdkVersion; - - this.showXcodeLog = args.showXcodeLog; - - this.xcodeConfigFile = args.xcodeConfigFile; - this.xcodeOrgId = args.xcodeOrgId; - this.xcodeSigningId = args.xcodeSigningId || DEFAULT_SIGNING_ID; - this.keychainPath = args.keychainPath; - this.keychainPassword = args.keychainPassword; - - this.prebuildWDA = args.prebuildWDA; - this.usePrebuiltWDA = args.usePrebuiltWDA; - this.useSimpleBuildTest = args.useSimpleBuildTest; - - this.useXctestrunFile = args.useXctestrunFile; - - this.launchTimeout = args.launchTimeout; - - this.wdaRemotePort = args.wdaRemotePort; - this.wdaBindingIP = args.wdaBindingIP; - - this.updatedWDABundleId = args.updatedWDABundleId; - this.derivedDataPath = args.derivedDataPath; - - this.mjpegServerPort = args.mjpegServerPort; - - this.prebuildDelay = _.isNumber(args.prebuildDelay) ? args.prebuildDelay : PREBUILD_DELAY; - - this.allowProvisioningDeviceRegistration = args.allowProvisioningDeviceRegistration; - - this.resultBundlePath = args.resultBundlePath; - this.resultBundleVersion = args.resultBundleVersion; - - this._didBuildFail = false; - this._didProcessExit = false; - } - - /** - * - * @param {any} noSessionProxy - * @returns {Promise} - */ - async init (noSessionProxy) { - this.noSessionProxy = noSessionProxy; - - if (this.useXctestrunFile) { - /** @type {import('./utils').DeviceInfo} */ - const deviceInfo = { - isRealDevice: !!this.realDevice, - udid: this.device.udid, - platformVersion: this.platformVersion || '', - platformName: this.platformName || '' - }; - this.xctestrunFilePath = await setXctestrunFile({ - deviceInfo, - sdkVersion: this.iosSdkVersion || '', - bootstrapPath: this.bootstrapPath, - wdaRemotePort: this.wdaRemotePort || 8100, - wdaBindingIP: this.wdaBindingIP - }); - return; - } - - // if necessary, update the bundleId to user's specification - if (this.realDevice) { - // In case the project still has the user specific bundle ID, reset the project file first. - // - We do this reset even if updatedWDABundleId is not specified, - // since the previous updatedWDABundleId test has generated the user specific bundle ID project file. - // - We don't call resetProjectFile for simulator, - // since simulator test run will work with any user specific bundle ID. - await resetProjectFile(this.agentPath); - if (this.updatedWDABundleId) { - await updateProjectFile(this.agentPath, this.updatedWDABundleId); - } - } - } - - /** - * @returns {Promise} - */ - async retrieveDerivedDataPath () { - if (this.derivedDataPath) { - return this.derivedDataPath; - } - - // avoid race conditions - if (this._derivedDataPathPromise) { - return await this._derivedDataPathPromise; - } - - this._derivedDataPathPromise = (async () => { - let stdout; - try { - ({stdout} = await exec('xcodebuild', ['-project', this.agentPath, '-showBuildSettings'])); - } catch (err) { - this.log.warn(`Cannot retrieve WDA build settings. Original error: ${err.message}`); - return; - } - - const pattern = /^\s*BUILD_DIR\s+=\s+(\/.*)/m; - const match = pattern.exec(stdout); - if (!match) { - this.log.warn(`Cannot parse WDA build dir from ${_.truncate(stdout, {length: 300})}`); - return; - } - this.log.debug(`Parsed BUILD_DIR configuration value: '${match[1]}'`); - // Derived data root is two levels higher over the build dir - this.derivedDataPath = path.dirname(path.dirname(path.normalize(match[1]))); - this.log.debug(`Got derived data root: '${this.derivedDataPath}'`); - return this.derivedDataPath; - })(); - return await this._derivedDataPathPromise; - } - - /** - * @returns {Promise} - */ - async reset () { - // if necessary, reset the bundleId to original value - if (this.realDevice && this.updatedWDABundleId) { - await resetProjectFile(this.agentPath); - } - } - - /** - * @returns {Promise} - */ - async prebuild () { - // first do a build phase - this.log.debug('Pre-building WDA before launching test'); - this.usePrebuiltWDA = true; - await this.start(true); - - if (this.prebuildDelay > 0) { - // pause a moment - await B.delay(this.prebuildDelay); - } - } - - /** - * @returns {Promise} - */ - async cleanProject () { - const libScheme = isTvOS(this.platformName || '') ? LIB_SCHEME_TV : LIB_SCHEME_IOS; - const runnerScheme = isTvOS(this.platformName || '') ? RUNNER_SCHEME_TV : RUNNER_SCHEME_IOS; - - for (const scheme of [libScheme, runnerScheme]) { - this.log.debug(`Cleaning the project scheme '${scheme}' to make sure there are no leftovers from previous installs`); - await exec('xcodebuild', [ - 'clean', - '-project', this.agentPath, - '-scheme', scheme, - ]); - } - } - - /** - * - * @param {boolean} [buildOnly=false] - * @returns {{cmd: string, args: string[]}} - */ - getCommand (buildOnly = false) { - const cmd = 'xcodebuild'; - /** @type {string[]} */ - const args = []; - - // figure out the targets for xcodebuild - const [buildCmd, testCmd] = this.useSimpleBuildTest ? ['build', 'test'] : ['build-for-testing', 'test-without-building']; - if (buildOnly) { - args.push(buildCmd); - } else if (this.usePrebuiltWDA || this.useXctestrunFile) { - args.push(testCmd); - } else { - args.push(buildCmd, testCmd); - } - - if (this.allowProvisioningDeviceRegistration) { - // To -allowProvisioningDeviceRegistration flag takes effect, -allowProvisioningUpdates needs to be passed as well. - args.push('-allowProvisioningUpdates', '-allowProvisioningDeviceRegistration'); - } - - if (this.resultBundlePath) { - args.push('-resultBundlePath', this.resultBundlePath); - } - - if (this.resultBundleVersion) { - args.push('-resultBundleVersion', this.resultBundleVersion); - } - - if (this.useXctestrunFile && this.xctestrunFilePath) { - args.push('-xctestrun', this.xctestrunFilePath); - } else { - const runnerScheme = isTvOS(this.platformName || '') ? RUNNER_SCHEME_TV : RUNNER_SCHEME_IOS; - args.push('-project', this.agentPath, '-scheme', runnerScheme); - if (this.derivedDataPath) { - args.push('-derivedDataPath', this.derivedDataPath); - } - } - args.push('-destination', `id=${this.device.udid}`); - - let versionMatch; - if (this.platformVersion && (versionMatch = new RegExp(/^(\d+)\.(\d+)/).exec(this.platformVersion))) { - args.push( - `${isTvOS(this.platformName || '') ? 'TV' : 'IPHONE'}OS_DEPLOYMENT_TARGET=${versionMatch[1]}.${versionMatch[2]}` - ); - } else { - this.log.warn( - `Cannot parse major and minor version numbers from platformVersion "${this.platformVersion}". ` + - 'Will build for the default platform instead' - ); - } - - if (this.realDevice) { - if (this.xcodeConfigFile) { - this.log.debug(`Using Xcode configuration file: '${this.xcodeConfigFile}'`); - args.push('-xcconfig', this.xcodeConfigFile); - } - if (this.xcodeOrgId && this.xcodeSigningId) { - args.push( - `DEVELOPMENT_TEAM=${this.xcodeOrgId}`, - `CODE_SIGN_IDENTITY=${this.xcodeSigningId}`, - ); - } - } - - if (!process.env.APPIUM_XCUITEST_TREAT_WARNINGS_AS_ERRORS) { - // This sometimes helps to survive Xcode updates - args.push('GCC_TREAT_WARNINGS_AS_ERRORS=0'); - } - - // Below option slightly reduces build time in debug build - // with preventing to generate `/Index/DataStore` which is used by development - args.push('COMPILER_INDEX_STORE_ENABLE=NO'); - - return {cmd, args}; - } - - /** - * @param {boolean} [buildOnly=false] - * @returns {Promise} - */ - async createSubProcess (buildOnly = false) { - if (!this.useXctestrunFile && this.realDevice) { - if (this.keychainPath && this.keychainPassword) { - await setRealDeviceSecurity(this.keychainPath, this.keychainPassword); - } - } - - const {cmd, args} = this.getCommand(buildOnly); - this.log.debug(`Beginning ${buildOnly ? 'build' : 'test'} with command '${cmd} ${args.join(' ')}' ` + - `in directory '${this.bootstrapPath}'`); - /** @type {Record} */ - const env = Object.assign({}, process.env, { - USE_PORT: this.wdaRemotePort, - WDA_PRODUCT_BUNDLE_IDENTIFIER: this.updatedWDABundleId || WDA_RUNNER_BUNDLE_ID, - }); - if (this.mjpegServerPort) { - // https://github.com/appium/WebDriverAgent/pull/105 - env.MJPEG_SERVER_PORT = this.mjpegServerPort; - } - if (this.wdaBindingIP) { - env.USE_IP = this.wdaBindingIP; - } - const upgradeTimestamp = await getWDAUpgradeTimestamp(); - if (upgradeTimestamp) { - env.UPGRADE_TIMESTAMP = upgradeTimestamp; - } - this._didBuildFail = false; - const xcodebuild = new SubProcess(cmd, args, { - cwd: this.bootstrapPath, - env, - detached: true, - stdio: ['ignore', 'pipe', 'pipe'], - }); - - let logXcodeOutput = !!this.showXcodeLog; - const logMsg = _.isBoolean(this.showXcodeLog) - ? `Output from xcodebuild ${this.showXcodeLog ? 'will' : 'will not'} be logged` - : 'Output from xcodebuild will only be logged if any errors are present there'; - this.log.debug(`${logMsg}. To change this, use 'showXcodeLog' desired capability`); - - const onStreamLine = (/** @type {string} */ line) => { - if (this.showXcodeLog === false || IGNORED_ERRORS_PATTERN.test(line)) { - return; - } - // if we have an error we want to output the logs - // otherwise the failure is inscrutible - // but do not log permission errors from trying to write to attachments folder - if (line.includes('Error Domain=')) { - logXcodeOutput = true; - // handle case where xcode returns 0 but is failing - this._didBuildFail = true; - } - if (logXcodeOutput) { - xcodeLog.info(line); - } - }; - for (const streamName of ['stderr', 'stdout']) { - xcodebuild.on(`line-${streamName}`, onStreamLine); - } - - return xcodebuild; - } - - - /** - * @param {boolean} [buildOnly=false] - * @returns {Promise} - */ - async start (buildOnly = false) { - this.xcodebuild = await this.createSubProcess(buildOnly); - - // wrap the start procedure in a promise so that we can catch, and report, - // any startup errors that are thrown as events - return await new B((resolve, reject) => { - this.xcodebuild.once('exit', (code, signal) => { - xcodeLog.error(`xcodebuild exited with code '${code}' and signal '${signal}'`); - this.xcodebuild.removeAllListeners(); - this.didProcessExit = true; - if (this._didBuildFail || (!signal && code !== 0)) { - let errorMessage = `xcodebuild failed with code ${code}.` + - ` This usually indicates an issue with the local Xcode setup or WebDriverAgent` + - ` project configuration or the driver-to-platform version mismatch.`; - if (!this.showXcodeLog) { - errorMessage += ` Consider setting 'showXcodeLog' capability to true in` + - ` order to check the Appium server log for build-related error messages.`; - } else if (this.realDevice) { - errorMessage += ` Consider checking the WebDriverAgent configuration guide` + - ` for real iOS devices at ${REAL_DEVICES_CONFIG_DOCS_LINK}.`; - } - return reject(new Error(errorMessage)); - } - // in the case of just building, the process will exit and that is our finish - if (buildOnly) { - return resolve(); - } - }); - - return (async () => { - try { - const timer = new timing.Timer().start(); - await this.xcodebuild.start(true); - if (!buildOnly) { - resolve(/** @type {import('@appium/types').StringRecord} */ (await this.waitForStart(timer))); - } - } catch (err) { - let msg = `Unable to start WebDriverAgent: ${err}`; - this.log.error(msg); - reject(new Error(msg)); - } - })(); - }); - } - - /** - * - * @param {any} timer - * @returns {Promise} - */ - async waitForStart (timer) { - // try to connect once every 0.5 seconds, until `launchTimeout` is up - const timeout = this.launchTimeout || 60000; // Default to 60 seconds if not set - this.log.debug(`Waiting up to ${timeout}ms for WebDriverAgent to start`); - let currentStatus = null; - try { - const retries = Math.trunc(timeout / 500); - await retryInterval(retries, 1000, async () => { - if (this._didProcessExit) { - // there has been an error elsewhere and we need to short-circuit - return currentStatus; - } - - const proxyTimeout = this.noSessionProxy.timeout; - this.noSessionProxy.timeout = 1000; - try { - currentStatus = await this.noSessionProxy.command('/status', 'GET'); - if (currentStatus && currentStatus.ios && currentStatus.ios.ip) { - this.agentUrl = currentStatus.ios.ip; - } - this.log.debug(`WebDriverAgent information:`); - this.log.debug(JSON.stringify(currentStatus, null, 2)); - } catch (err) { - throw new Error(`Unable to connect to running WebDriverAgent: ${err.message}`); - } finally { - this.noSessionProxy.timeout = proxyTimeout; - } - }); - - if (this._didProcessExit) { - // there has been an error elsewhere and we need to short-circuit - return currentStatus; - } - - this.log.debug(`WebDriverAgent successfully started after ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`); - } catch (err) { - this.log.debug(err.stack); - throw new Error( - `We were not able to retrieve the /status response from the WebDriverAgent server after ${timeout}ms timeout.` + - `Try to increase the value of 'appium:wdaLaunchTimeout' capability as a possible workaround.` - ); - } - return currentStatus; - } - - /** - * @returns {Promise} - */ - async quit () { - await killProcess('xcodebuild', this.xcodebuild); - } -} - -export default XcodeBuild; diff --git a/WebDriverAgent/package.json b/WebDriverAgent/package.json deleted file mode 100644 index 62ecc4c239..0000000000 --- a/WebDriverAgent/package.json +++ /dev/null @@ -1,104 +0,0 @@ -{ - "name": "appium-webdriveragent", - "version": "10.4.2", - "description": "Package bundling WebDriverAgent", - "main": "./build/index.js", - "types": "./build/index.d.ts", - "scripts": { - "build": "tsc -b", - "dev": "npm run build -- --watch", - "clean": "npm run build -- --clean", - "lint": "eslint .", - "format": "prettier -w ./lib", - "lint:fix": "npm run lint -- --fix", - "prepare": "npm run build", - "version": "npm run sync-wda-version", - "test": "mocha --exit --timeout 1m \"./test/unit/**/*-specs.js\"", - "e2e-test": "mocha --exit --timeout 10m \"./test/functional/**/*-specs.js\"", - "bundle": "npm run bundle:ios && npm run bundle:tv", - "bundle:ios": "TARGET=runner SDK=sim node ./Scripts/build-webdriveragent.js", - "bundle:tv": "TARGET=tv_runner SDK=tv_sim node ./Scripts/build-webdriveragent.js", - "fetch-prebuilt-wda": "node ./Scripts/fetch-prebuilt-wda.js", - "sync-wda-version": "node ./scripts/update-wda-version.js --package-version=${npm_package_version} && git add WebDriverAgentLib/Info.plist" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", - "npm": ">=10" - }, - "prettier": { - "bracketSpacing": false, - "printWidth": 100, - "singleQuote": true - }, - "repository": { - "type": "git", - "url": "git+https://github.com/appium/WebDriverAgent.git" - }, - "keywords": [ - "Appium", - "iOS", - "WebDriver", - "Selenium", - "WebDriverAgent" - ], - "author": "Appium Contributors", - "license": "Apache-2.0", - "bugs": { - "url": "https://github.com/appium/WebDriverAgent/issues" - }, - "homepage": "https://github.com/appium/WebDriverAgent#readme", - "devDependencies": { - "@appium/eslint-config-appium-ts": "^2.0.0-rc.1", - "@appium/tsconfig": "^1.0.0-rc.1", - "@appium/types": "^1.0.0-rc.1", - "@semantic-release/changelog": "^6.0.1", - "@semantic-release/git": "^10.0.1", - "@types/bluebird": "^3.5.38", - "@types/lodash": "^4.14.196", - "@types/mocha": "^10.0.1", - "@types/node": "^25.0.0", - "appium-xcode": "^6.0.0", - "chai": "^6.0.0", - "chai-as-promised": "^8.0.0", - "conventional-changelog-conventionalcommits": "^9.0.0", - "node-simctl": "^8.0.0", - "mocha": "^11.0.1", - "prettier": "^3.0.0", - "semantic-release": "^25.0.2", - "semver": "^7.3.7", - "sinon": "^21.0.0", - "ts-node": "^10.9.1", - "typescript": "^5.4.2" - }, - "dependencies": { - "@appium/base-driver": "^10.0.0-rc.1", - "@appium/strongbox": "^1.0.0-rc.1", - "@appium/support": "^7.0.0-rc.1", - "appium-ios-device": "^3.0.0", - "appium-ios-simulator": "^8.0.0", - "async-lock": "^1.0.0", - "asyncbox": "^3.0.0", - "axios": "^1.4.0", - "bluebird": "^3.5.5", - "lodash": "^4.17.11", - "source-map-support": "^0.x", - "teen_process": "^3.0.0" - }, - "files": [ - "index.ts", - "lib", - "build/index.*", - "build/lib", - "Scripts/build.sh", - "Scripts/fetch-prebuilt-wda.js", - "Scripts/build-webdriveragent.js", - "Configurations", - "PrivateHeaders", - "WebDriverAgent.xcodeproj", - "WebDriverAgentLib", - "WebDriverAgentRunner", - "WebDriverAgentTests", - "XCTWebDriverAgentLib", - "CHANGELOG.md" - ] -} diff --git a/WebDriverAgent/test/functional/desired.js b/WebDriverAgent/test/functional/desired.js deleted file mode 100644 index 52a3d19329..0000000000 --- a/WebDriverAgent/test/functional/desired.js +++ /dev/null @@ -1,5 +0,0 @@ -import { util } from '@appium/support'; - -export const PLATFORM_VERSION = process.env.PLATFORM_VERSION ? process.env.PLATFORM_VERSION : '11.3'; -export const DEVICE_NAME = process.env.DEVICE_NAME - || (util.compareVersions(PLATFORM_VERSION, '>=', '13.0') ? 'iPhone X' : 'iPhone 6'); diff --git a/WebDriverAgent/test/functional/helpers/simulator.js b/WebDriverAgent/test/functional/helpers/simulator.js deleted file mode 100644 index f315f2035a..0000000000 --- a/WebDriverAgent/test/functional/helpers/simulator.js +++ /dev/null @@ -1,41 +0,0 @@ -import _ from 'lodash'; -import { Simctl } from 'node-simctl'; -import { retryInterval } from 'asyncbox'; -import { killAllSimulators as simKill } from 'appium-ios-simulator'; -import { resetTestProcesses } from '../../../lib/utils'; - - -async function killAllSimulators () { - if (process.env.CLOUD) { - return; - } - - const simctl = new Simctl(); - const allDevices = _.flatMap(_.values(await simctl.getDevices())); - const bootedDevices = allDevices.filter((device) => device.state === 'Booted'); - - for (const {udid} of bootedDevices) { - // It is necessary to stop the corresponding xcodebuild process before killing - // the simulator, otherwise it will be automatically restarted - await resetTestProcesses(udid, true); - simctl.udid = udid; - await simctl.shutdownDevice(); - } - await simKill(); -} - -async function shutdownSimulator (device) { - // stop XCTest processes if running to avoid unexpected side effects - await resetTestProcesses(device.udid, true); - await device.shutdown(); -} - -async function deleteDeviceWithRetry (udid) { - const simctl = new Simctl({udid}); - try { - await retryInterval(10, 1000, simctl.deleteDevice.bind(simctl)); - } catch {} -} - - -export { killAllSimulators, shutdownSimulator, deleteDeviceWithRetry }; diff --git a/WebDriverAgent/test/functional/webdriveragent-e2e-specs.js b/WebDriverAgent/test/functional/webdriveragent-e2e-specs.js deleted file mode 100644 index bb1ce67c91..0000000000 --- a/WebDriverAgent/test/functional/webdriveragent-e2e-specs.js +++ /dev/null @@ -1,129 +0,0 @@ -import { Simctl } from 'node-simctl'; -import { getVersion } from 'appium-xcode'; -import { getSimulator } from 'appium-ios-simulator'; -import { killAllSimulators, shutdownSimulator } from './helpers/simulator'; -import { SubProcess } from 'teen_process'; -import { PLATFORM_VERSION, DEVICE_NAME } from './desired'; -import { retryInterval } from 'asyncbox'; -import { WebDriverAgent } from '../../lib/webdriveragent'; -import axios from 'axios'; - -const MOCHA_TIMEOUT_MS = 60 * 1000 * 5; - -const SIM_DEVICE_NAME = 'webDriverAgentTest'; -const SIM_STARTUP_TIMEOUT_MS = MOCHA_TIMEOUT_MS; - -let testUrl = 'http://localhost:8100/tree'; - -function getStartOpts (device) { - return { - device, - platformVersion: PLATFORM_VERSION, - host: 'localhost', - port: 8100, - realDevice: false, - showXcodeLog: true, - wdaLaunchTimeout: 60 * 3 * 1000, - }; -} - - -describe('WebDriverAgent', function () { - this.timeout(MOCHA_TIMEOUT_MS); - let chai; - let xcodeVersion; - - before(async function () { - chai = await import('chai'); - const chaiAsPromised = await import('chai-as-promised'); - - chai.should(); - chai.use(chaiAsPromised.default); - - // Don't do these tests on Sauce Labs - if (process.env.CLOUD) { - this.skip(); - } - - xcodeVersion = await getVersion(true); - }); - describe('with fresh sim', function () { - let device; - let simctl; - - before(async function () { - simctl = new Simctl(); - simctl.udid = await simctl.createDevice( - SIM_DEVICE_NAME, - DEVICE_NAME, - PLATFORM_VERSION - ); - device = await getSimulator(simctl.udid); - - // Prebuild WDA - const wda = new WebDriverAgent(xcodeVersion, { - iosSdkVersion: PLATFORM_VERSION, - platformVersion: PLATFORM_VERSION, - showXcodeLog: true, - device, - }); - await wda.xcodebuild.start(true); - }); - - after(async function () { - this.timeout(MOCHA_TIMEOUT_MS); - - await shutdownSimulator(device); - - await simctl.deleteDevice(); - }); - - describe('with running sim', function () { - this.timeout(6 * 60 * 1000); - beforeEach(async function () { - await killAllSimulators(); - await device.run({startupTimeout: SIM_STARTUP_TIMEOUT_MS}); - }); - afterEach(async function () { - try { - await retryInterval(5, 1000, async function () { - await shutdownSimulator(device); - }); - } catch {} - }); - - it('should launch agent on a sim', async function () { - const agent = new WebDriverAgent(xcodeVersion, getStartOpts(device)); - - await agent.launch('sessionId'); - await axios({url: testUrl}).should.be.eventually.rejected; - await agent.quit(); - }); - - it('should fail if xcodebuild fails', async function () { - // short timeout - this.timeout(35 * 1000); - - const agent = new WebDriverAgent(xcodeVersion, getStartOpts(device)); - - agent.xcodebuild.createSubProcess = async function () { - let args = [ - '-workspace', - `${this.agentPath}dfgs`, - // '-scheme', - // 'XCTUITestRunner', - // '-destination', - // `id=${this.device.udid}`, - // 'test' - ]; - return new SubProcess('xcodebuild', args, {detached: true}); - }; - - await agent.launch('sessionId') - .should.eventually.be.rejectedWith('xcodebuild failed'); - - await agent.quit(); - }); - }); - }); -}); diff --git a/WebDriverAgent/test/unit/utils-specs.js b/WebDriverAgent/test/unit/utils-specs.js deleted file mode 100644 index 53f1d45df3..0000000000 --- a/WebDriverAgent/test/unit/utils-specs.js +++ /dev/null @@ -1,162 +0,0 @@ -import { getXctestrunFilePath, getAdditionalRunContent, getXctestrunFileName } from '../../lib/utils'; -import { PLATFORM_NAME_IOS, PLATFORM_NAME_TVOS } from '../../lib/constants'; -import { fs } from '@appium/support'; -import path from 'path'; -import { fail } from 'assert'; -import { arch } from 'os'; -import sinon from 'sinon'; - -function get_arch() { - return arch() === 'arm64' ? 'arm64' : 'x86_64'; -} - -describe('utils', function () { - let chai; - - before(async function() { - chai = await import('chai'); - const chaiAsPromised = await import('chai-as-promised'); - - chai.should(); - chai.use(chaiAsPromised.default); - }); - - describe('#getXctestrunFilePath', function () { - const platformVersion = '12.0'; - const sdkVersion = '12.2'; - const udid = 'xxxxxyyyyyyzzzzzz'; - const bootstrapPath = 'path/to/data'; - const platformName = PLATFORM_NAME_IOS; - let sandbox; - - beforeEach(function () { - sandbox = sinon.createSandbox(); - }); - - afterEach(function () { - sandbox.restore(); - }); - - it('should return sdk based path with udid', async function () { - sandbox.stub(fs, 'exists') - .withArgs(path.resolve(`${bootstrapPath}/${udid}_${sdkVersion}.xctestrun`)) - .resolves(true); - sandbox.stub(fs, 'copyFile'); - const deviceInfo = {isRealDevice: true, udid, platformVersion, platformName}; - await getXctestrunFilePath(deviceInfo, sdkVersion, bootstrapPath) - .should.eventually.equal(path.resolve(`${bootstrapPath}/${udid}_${sdkVersion}.xctestrun`)); - sandbox.assert.notCalled(fs.copyFile); - }); - - it('should return sdk based path without udid, copy them', async function () { - const existsStub = sandbox.stub(fs, 'exists'); - existsStub.withArgs(path.resolve(`${bootstrapPath}/${udid}_${sdkVersion}.xctestrun`)).resolves(false); - existsStub.withArgs(path.resolve(`${bootstrapPath}/WebDriverAgentRunner_iphoneos${sdkVersion}-arm64.xctestrun`)).resolves(true); - sandbox.stub(fs, 'copyFile') - .withArgs( - path.resolve(`${bootstrapPath}/WebDriverAgentRunner_iphoneos${sdkVersion}-arm64.xctestrun`), - path.resolve(`${bootstrapPath}/${udid}_${sdkVersion}.xctestrun`) - ) - .resolves(); - const deviceInfo = {isRealDevice: true, udid, platformVersion}; - await getXctestrunFilePath(deviceInfo, sdkVersion, bootstrapPath) - .should.eventually.equal(path.resolve(`${bootstrapPath}/${udid}_${sdkVersion}.xctestrun`)); - }); - - it('should return platform based path', async function () { - const existsStub = sandbox.stub(fs, 'exists'); - existsStub.withArgs(path.resolve(`${bootstrapPath}/${udid}_${sdkVersion}.xctestrun`)).resolves(false); - existsStub.withArgs(path.resolve(`${bootstrapPath}/WebDriverAgentRunner_iphonesimulator${sdkVersion}-${get_arch()}.xctestrun`)).resolves(false); - existsStub.withArgs(path.resolve(`${bootstrapPath}/${udid}_${platformVersion}.xctestrun`)).resolves(true); - sandbox.stub(fs, 'copyFile'); - const deviceInfo = {isRealDevice: false, udid, platformVersion}; - await getXctestrunFilePath(deviceInfo, sdkVersion, bootstrapPath) - .should.eventually.equal(path.resolve(`${bootstrapPath}/${udid}_${platformVersion}.xctestrun`)); - sandbox.assert.notCalled(fs.copyFile); - }); - - it('should return platform based path without udid, copy them', async function () { - const existsStub = sandbox.stub(fs, 'exists'); - existsStub.withArgs(path.resolve(`${bootstrapPath}/${udid}_${sdkVersion}.xctestrun`)).resolves(false); - existsStub.withArgs(path.resolve(`${bootstrapPath}/WebDriverAgentRunner_iphonesimulator${sdkVersion}-${get_arch()}.xctestrun`)).resolves(false); - existsStub.withArgs(path.resolve(`${bootstrapPath}/${udid}_${platformVersion}.xctestrun`)).resolves(false); - existsStub.withArgs(path.resolve(`${bootstrapPath}/WebDriverAgentRunner_iphonesimulator${platformVersion}-${get_arch()}.xctestrun`)).resolves(true); - sandbox.stub(fs, 'copyFile') - .withArgs( - path.resolve(`${bootstrapPath}/WebDriverAgentRunner_iphonesimulator${platformVersion}-${get_arch()}.xctestrun`), - path.resolve(`${bootstrapPath}/${udid}_${platformVersion}.xctestrun`) - ) - .resolves(); - - const deviceInfo = {isRealDevice: false, udid, platformVersion}; - await getXctestrunFilePath(deviceInfo, sdkVersion, bootstrapPath) - .should.eventually.equal(path.resolve(`${bootstrapPath}/${udid}_${platformVersion}.xctestrun`)); - }); - - it('should raise an exception because of no files', async function () { - const expected = path.resolve(`${bootstrapPath}/WebDriverAgentRunner_iphonesimulator${sdkVersion}-${get_arch()}.xctestrun`); - sandbox.stub(fs, 'exists').resolves(false); - - const deviceInfo = {isRealDevice: false, udid, platformVersion}; - try { - await getXctestrunFilePath(deviceInfo, sdkVersion, bootstrapPath); - fail(); - } catch (err) { - err.message.should.equal(`If you are using 'useXctestrunFile' capability then you need to have a xctestrun file (expected: '${expected}')`); - } - }); - }); - - describe('#getAdditionalRunContent', function () { - it('should return ios format', function () { - const wdaPort = getAdditionalRunContent(PLATFORM_NAME_IOS, 8000); - wdaPort.WebDriverAgentRunner - .EnvironmentVariables.USE_PORT - .should.equal('8000'); - }); - - it('should return tvos format', function () { - const wdaPort = getAdditionalRunContent(PLATFORM_NAME_TVOS, '9000'); - wdaPort.WebDriverAgentRunner_tvOS - .EnvironmentVariables.USE_PORT - .should.equal('9000'); - }); - }); - - describe('#getXctestrunFileName', function () { - const platformVersion = '12.0'; - const udid = 'xxxxxyyyyyyzzzzzz'; - - it('should return ios format, real device', function () { - const platformName = 'iOs'; - const deviceInfo = {isRealDevice: true, udid, platformVersion, platformName}; - - getXctestrunFileName(deviceInfo, '10.2.0').should.equal( - 'WebDriverAgentRunner_iphoneos10.2.0-arm64.xctestrun'); - }); - - it('should return ios format, simulator', function () { - const platformName = 'ios'; - const deviceInfo = {isRealDevice: false, udid, platformVersion, platformName}; - - getXctestrunFileName(deviceInfo, '10.2.0').should.equal( - `WebDriverAgentRunner_iphonesimulator10.2.0-${get_arch()}.xctestrun`); - }); - - it('should return tvos format, real device', function () { - const platformName = 'tVos'; - const deviceInfo = {isRealDevice: true, udid, platformVersion, platformName}; - - getXctestrunFileName(deviceInfo, '10.2.0').should.equal( - 'WebDriverAgentRunner_tvOS_appletvos10.2.0-arm64.xctestrun'); - }); - - it('should return tvos format, simulator', function () { - const platformName = 'tvOS'; - const deviceInfo = {isRealDevice: false, udid, platformVersion, platformName}; - - getXctestrunFileName(deviceInfo, '10.2.0').should.equal( - `WebDriverAgentRunner_tvOS_appletvsimulator10.2.0-${get_arch()}.xctestrun`); - }); - }); -}); diff --git a/WebDriverAgent/test/unit/webdriveragent-specs.js b/WebDriverAgent/test/unit/webdriveragent-specs.js deleted file mode 100644 index a9179062ae..0000000000 --- a/WebDriverAgent/test/unit/webdriveragent-specs.js +++ /dev/null @@ -1,417 +0,0 @@ -import { BOOTSTRAP_PATH } from '../../lib/utils'; -import { WebDriverAgent } from '../../lib/webdriveragent'; -import * as utils from '../../lib/utils'; -import path from 'path'; -import _ from 'lodash'; -import sinon from 'sinon'; - -const fakeConstructorArgs = { - device: { - udid: 'some-sim-udid', - simctl: {}, - devicectl: {}, - idb: null - }, - platformVersion: '9', - host: 'me', - port: '5000', - realDevice: false -}; - -const defaultAgentPath = path.resolve(BOOTSTRAP_PATH, 'WebDriverAgent.xcodeproj'); -const customBootstrapPath = '/path/to/wda'; -const customAgentPath = '/path/to/some/agent/WebDriverAgent.xcodeproj'; -const customDerivedDataPath = '/path/to/some/agent/DerivedData/'; - -describe('Constructor', function () { - let chai; - - before(async function() { - chai = await import('chai'); - const chaiAsPromised = await import('chai-as-promised'); - - chai.should(); - chai.use(chaiAsPromised.default); - }); - - it('should have a default wda agent if not specified', function () { - let agent = new WebDriverAgent({}, fakeConstructorArgs); - agent.bootstrapPath.should.eql(BOOTSTRAP_PATH); - agent.agentPath.should.eql(defaultAgentPath); - }); - it('should have custom wda bootstrap and default agent if only bootstrap specified', function () { - let agent = new WebDriverAgent({}, _.defaults({ - bootstrapPath: customBootstrapPath, - }, fakeConstructorArgs)); - agent.bootstrapPath.should.eql(customBootstrapPath); - agent.agentPath.should.eql(path.resolve(customBootstrapPath, 'WebDriverAgent.xcodeproj')); - }); - it('should have custom wda bootstrap and agent if both specified', function () { - let agent = new WebDriverAgent({}, _.defaults({ - bootstrapPath: customBootstrapPath, - agentPath: customAgentPath, - }, fakeConstructorArgs)); - agent.bootstrapPath.should.eql(customBootstrapPath); - agent.agentPath.should.eql(customAgentPath); - }); - it('should have custom derivedDataPath if specified', function () { - let agent = new WebDriverAgent({}, _.defaults({ - derivedDataPath: customDerivedDataPath - }, fakeConstructorArgs)); - agent.xcodebuild.derivedDataPath.should.eql(customDerivedDataPath); - }); -}); - -describe('launch', function () { - it('should use webDriverAgentUrl override and return current status', async function () { - const override = 'http://mockurl:8100/'; - const args = Object.assign({}, fakeConstructorArgs); - args.webDriverAgentUrl = override; - const agent = new WebDriverAgent({}, args); - const wdaStub = sinon.stub(agent, 'getStatus'); - wdaStub.callsFake(function () { - return {build: 'data'}; - }); - - await agent.launch('sessionId').should.eventually.eql({build: 'data'}); - agent.url.href.should.eql(override); - agent.jwproxy.server.should.eql('mockurl'); - agent.jwproxy.port.should.eql(8100); - agent.jwproxy.base.should.eql(''); - agent.jwproxy.scheme.should.eql('http'); - agent.noSessionProxy.server.should.eql('mockurl'); - agent.noSessionProxy.port.should.eql(8100); - agent.noSessionProxy.base.should.eql(''); - agent.noSessionProxy.scheme.should.eql('http'); - wdaStub.reset(); - }); -}); - -describe('use wda proxy url', function () { - it('should use webDriverAgentUrl wda proxy url', async function () { - const override = 'http://127.0.0.1:8100/aabbccdd'; - const args = Object.assign({}, fakeConstructorArgs); - args.webDriverAgentUrl = override; - const agent = new WebDriverAgent({}, args); - const wdaStub = sinon.stub(agent, 'getStatus'); - wdaStub.callsFake(function () { - return {build: 'data'}; - }); - - await agent.launch('sessionId').should.eventually.eql({build: 'data'}); - - agent.url.port.should.eql('8100'); - agent.url.hostname.should.eql('127.0.0.1'); - agent.url.path.should.eql('/aabbccdd'); - agent.jwproxy.server.should.eql('127.0.0.1'); - agent.jwproxy.port.should.eql(8100); - agent.jwproxy.base.should.eql('/aabbccdd'); - agent.jwproxy.scheme.should.eql('http'); - agent.noSessionProxy.server.should.eql('127.0.0.1'); - agent.noSessionProxy.port.should.eql(8100); - agent.noSessionProxy.base.should.eql('/aabbccdd'); - agent.noSessionProxy.scheme.should.eql('http'); - }); -}); - -describe('get url', function () { - it('should use default WDA listening url', function () { - const args = Object.assign({}, fakeConstructorArgs); - const agent = new WebDriverAgent({}, args); - agent.url.href.should.eql('http://127.0.0.1:8100/'); - agent.setupProxies('mysession'); - agent.jwproxy.scheme.should.eql('http'); - agent.noSessionProxy.scheme.should.eql('http'); - }); - it('should use default WDA listening url with emply base url', function () { - const wdaLocalPort = '9100'; - const wdaBaseUrl = ''; - - const args = Object.assign({}, fakeConstructorArgs); - args.wdaBaseUrl = wdaBaseUrl; - args.wdaLocalPort = wdaLocalPort; - - const agent = new WebDriverAgent({}, args); - agent.url.href.should.eql('http://127.0.0.1:9100/'); - agent.setupProxies('mysession'); - agent.jwproxy.scheme.should.eql('http'); - agent.noSessionProxy.scheme.should.eql('http'); - }); - it('should use customised WDA listening url', function () { - const wdaLocalPort = '9100'; - const wdaBaseUrl = 'http://mockurl'; - - const args = Object.assign({}, fakeConstructorArgs); - args.wdaBaseUrl = wdaBaseUrl; - args.wdaLocalPort = wdaLocalPort; - - const agent = new WebDriverAgent({}, args); - agent.url.href.should.eql('http://mockurl:9100/'); - agent.setupProxies('mysession'); - agent.jwproxy.scheme.should.eql('http'); - agent.noSessionProxy.scheme.should.eql('http'); - }); - it('should use customised WDA listening url with slash', function () { - const wdaLocalPort = '9100'; - const wdaBaseUrl = 'http://mockurl/'; - - const args = Object.assign({}, fakeConstructorArgs); - args.wdaBaseUrl = wdaBaseUrl; - args.wdaLocalPort = wdaLocalPort; - - const agent = new WebDriverAgent({}, args); - agent.url.href.should.eql('http://mockurl:9100/'); - agent.setupProxies('mysession'); - agent.jwproxy.scheme.should.eql('http'); - agent.noSessionProxy.scheme.should.eql('http'); - }); - it('should use the given webDriverAgentUrl and ignore other params', function () { - const args = Object.assign({}, fakeConstructorArgs); - args.wdaBaseUrl = 'http://mockurl/'; - args.wdaLocalPort = '9100'; - args.webDriverAgentUrl = 'https://127.0.0.1:8100/'; - - const agent = new WebDriverAgent({}, args); - agent.url.href.should.eql('https://127.0.0.1:8100/'); - }); - it('should set scheme to https for https webDriverAgentUrl', function () { - const args = Object.assign({}, fakeConstructorArgs); - args.webDriverAgentUrl = 'https://127.0.0.1:8100/'; - const agent = new WebDriverAgent({}, args); - agent.setupProxies('mysession'); - agent.jwproxy.scheme.should.eql('https'); - agent.noSessionProxy.scheme.should.eql('https'); - }); -}); - -describe('setupCaching()', function () { - let wda; - let wdaStub; - let wdaStubUninstall; - const getTimestampStub = sinon.stub(utils, 'getWDAUpgradeTimestamp'); - - beforeEach(function () { - wda = new WebDriverAgent('1', fakeConstructorArgs); - wdaStub = sinon.stub(wda, 'getStatus'); - wdaStubUninstall = sinon.stub(wda, 'uninstall'); - }); - - afterEach(function () { - for (const stub of [wdaStub, wdaStubUninstall, getTimestampStub]) { - if (stub) { - stub.reset(); - } - } - }); - - it('should not call uninstall since no Running WDA', async function () { - wdaStub.callsFake(function () { - return null; - }); - wdaStubUninstall.callsFake(_.noop); - - await wda.setupCaching(); - wdaStub.calledOnce.should.be.true; - wdaStubUninstall.notCalled.should.be.true; - _.isUndefined(wda.webDriverAgentUrl).should.be.true; - }); - - it('should not call uninstall since running WDA has only time', async function () { - wdaStub.callsFake(function () { - return {build: { time: 'Jun 24 2018 17:08:21' }}; - }); - wdaStubUninstall.callsFake(_.noop); - - await wda.setupCaching(); - wdaStub.calledOnce.should.be.true; - wdaStubUninstall.notCalled.should.be.true; - wda.webDriverAgentUrl.should.equal('http://127.0.0.1:8100/'); - }); - - it('should call uninstall once since bundle id is not default without updatedWDABundleId capability', async function () { - wdaStub.callsFake(function () { - return {build: { time: 'Jun 24 2018 17:08:21', productBundleIdentifier: 'com.example.WebDriverAgent' }}; - }); - wdaStubUninstall.callsFake(_.noop); - - await wda.setupCaching(); - wdaStub.calledOnce.should.be.true; - wdaStubUninstall.calledOnce.should.be.true; - _.isUndefined(wda.webDriverAgentUrl).should.be.true; - }); - - it('should call uninstall once since bundle id is different with updatedWDABundleId capability', async function () { - wdaStub.callsFake(function () { - return {build: { time: 'Jun 24 2018 17:08:21', productBundleIdentifier: 'com.example.different.WebDriverAgent' }}; - }); - - wdaStubUninstall.callsFake(_.noop); - - await wda.setupCaching(); - wdaStub.calledOnce.should.be.true; - wdaStubUninstall.calledOnce.should.be.true; - _.isUndefined(wda.webDriverAgentUrl).should.be.true; - }); - - it('should not call uninstall since bundle id is equal to updatedWDABundleId capability', async function () { - wda = new WebDriverAgent('1', { ...fakeConstructorArgs, updatedWDABundleId: 'com.example.WebDriverAgent' }); - wdaStub = sinon.stub(wda, 'getStatus'); - wdaStubUninstall = sinon.stub(wda, 'uninstall'); - - wdaStub.callsFake(function () { - return {build: { time: 'Jun 24 2018 17:08:21', productBundleIdentifier: 'com.example.WebDriverAgent' }}; - }); - - wdaStubUninstall.callsFake(_.noop); - - await wda.setupCaching(); - wdaStub.calledOnce.should.be.true; - wdaStubUninstall.notCalled.should.be.true; - wda.webDriverAgentUrl.should.equal('http://127.0.0.1:8100/'); - }); - - it('should call uninstall if current revision differs from the bundled one', async function () { - wdaStub.callsFake(function () { - return {build: { upgradedAt: '1' }}; - }); - getTimestampStub.callsFake(() => '2'); - wdaStubUninstall.callsFake(_.noop); - - await wda.setupCaching(); - wdaStub.calledOnce.should.be.true; - wdaStubUninstall.calledOnce.should.be.true; - }); - - it('should not call uninstall if current revision is the same as the bundled one', async function () { - wdaStub.callsFake(function () { - return {build: { upgradedAt: '1' }}; - }); - getTimestampStub.callsFake(() => '1'); - wdaStubUninstall.callsFake(_.noop); - - await wda.setupCaching(); - wdaStub.calledOnce.should.be.true; - wdaStubUninstall.notCalled.should.be.true; - }); - - it('should not call uninstall if current revision cannot be retrieved from WDA status', async function () { - wdaStub.callsFake(function () { - return {build: {}}; - }); - getTimestampStub.callsFake(() => '1'); - wdaStubUninstall.callsFake(_.noop); - - await wda.setupCaching(); - wdaStub.calledOnce.should.be.true; - wdaStubUninstall.notCalled.should.be.true; - }); - - it('should not call uninstall if current revision cannot be retrieved from the file system', async function () { - wdaStub.callsFake(function () { - return {build: { upgradedAt: '1' }}; - }); - getTimestampStub.callsFake(() => null); - wdaStubUninstall.callsFake(_.noop); - - await wda.setupCaching(); - wdaStub.calledOnce.should.be.true; - wdaStubUninstall.notCalled.should.be.true; - }); - - describe('uninstall', function () { - let device; - let wda; - let deviceGetBundleIdsStub; - let deviceRemoveAppStub; - - beforeEach(function () { - device = { - getUserInstalledBundleIdsByBundleName: () => {}, - removeApp: () => {} - }; - wda = new WebDriverAgent('1', {device}); - deviceGetBundleIdsStub = sinon.stub(device, 'getUserInstalledBundleIdsByBundleName'); - deviceRemoveAppStub = sinon.stub(device, 'removeApp'); - }); - - afterEach(function () { - for (const stub of [deviceGetBundleIdsStub, deviceRemoveAppStub]) { - if (stub) { - stub.reset(); - } - } - }); - - it('should not call uninstall', async function () { - deviceGetBundleIdsStub.callsFake(() => []); - - await wda.uninstall(); - deviceGetBundleIdsStub.calledOnce.should.be.true; - deviceRemoveAppStub.notCalled.should.be.true; - }); - - it('should call uninstall once', async function () { - const uninstalledBundIds = []; - deviceGetBundleIdsStub.callsFake(() => ['com.appium.WDA1']); - deviceRemoveAppStub.callsFake((id) => uninstalledBundIds.push(id)); - - await wda.uninstall(); - deviceGetBundleIdsStub.calledOnce.should.be.true; - deviceRemoveAppStub.calledOnce.should.be.true; - uninstalledBundIds.should.eql(['com.appium.WDA1']); - }); - - it('should call uninstall twice', async function () { - const uninstalledBundIds = []; - deviceGetBundleIdsStub.callsFake(() => ['com.appium.WDA1', 'com.appium.WDA2']); - deviceRemoveAppStub.callsFake((id) => uninstalledBundIds.push(id)); - - await wda.uninstall(); - deviceGetBundleIdsStub.calledOnce.should.be.true; - deviceRemoveAppStub.calledTwice.should.be.true; - uninstalledBundIds.should.eql(['com.appium.WDA1', 'com.appium.WDA2']); - }); - }); -}); - - -describe('usePreinstalledWDA related functions', function () { - describe('bundleIdForXctest', function () { - it('should have xctrunner automatically', function () { - const args = Object.assign({}, fakeConstructorArgs); - args.updatedWDABundleId = 'io.appium.wda'; - const agent = new WebDriverAgent({}, args); - agent.bundleIdForXctest.should.equal('io.appium.wda.xctrunner'); - }); - - it('should have xctrunner automatically with default bundle id', function () { - const args = Object.assign({}, fakeConstructorArgs); - const agent = new WebDriverAgent({}, args); - agent.bundleIdForXctest.should.equal('com.facebook.WebDriverAgentRunner.xctrunner'); - }); - - it('should allow an empty string as xctrunner suffix', function () { - const args = Object.assign({}, fakeConstructorArgs); - args.updatedWDABundleId = 'io.appium.wda'; - args.updatedWDABundleIdSuffix = ''; - const agent = new WebDriverAgent({}, args); - agent.bundleIdForXctest.should.equal('io.appium.wda'); - }); - - it('should allow an empty string as xctrunner suffix with default bundle id', function () { - const args = Object.assign({}, fakeConstructorArgs); - args.updatedWDABundleIdSuffix = ''; - const agent = new WebDriverAgent({}, args); - agent.bundleIdForXctest.should.equal('com.facebook.WebDriverAgentRunner'); - }); - - it('should have an arbitrary xctrunner suffix', function () { - const args = Object.assign({}, fakeConstructorArgs); - args.updatedWDABundleId = 'io.appium.wda'; - args.updatedWDABundleIdSuffix = '.customsuffix'; - const agent = new WebDriverAgent({}, args); - agent.bundleIdForXctest.should.equal('io.appium.wda.customsuffix'); - }); - - }); -}); diff --git a/WebDriverAgent/tsconfig.json b/WebDriverAgent/tsconfig.json deleted file mode 100644 index e7becfe3b5..0000000000 --- a/WebDriverAgent/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig", - "extends": "@appium/tsconfig/tsconfig.json", - "compilerOptions": { - "strict": false, // TODO: make this flag true - "esModuleInterop": true, - "outDir": "build", - "types": ["node"], - "checkJs": true - }, - "include": [ - "index.ts", - "lib" - ] -} diff --git a/bin/stf.mjs b/bin/stf.mjs index 95cb6fdb83..0032c528e1 100755 --- a/bin/stf.mjs +++ b/bin/stf.mjs @@ -1,4 +1,4 @@ -#!/usr/bin/env -S ./node_modules/.bin/tsx --import ./lib/util/instrument.mjs +#!/usr/bin/env -S node --import ./lib/util/instrument.mjs import packageData from '../package.json' with { type: 'json' } console.log(`Starting DeviceHub ${packageData.version}`) import '../lib/cli/index.js' diff --git a/lib/cli/ios-device/example b/lib/cli/ios-device/example deleted file mode 100644 index 89f57065db..0000000000 --- a/lib/cli/ios-device/example +++ /dev/null @@ -1,15 +0,0 @@ -stf ios-device \ - --serial "5D43EFDC-B845-4D3D-8465-FAA493C54A2E" \ - --host localhost \ - --screen-port 7409 \ - --mjpeg-port 9100 \ - --provider di-smirnov \ - --public-ip localhost \ - --screen-ws-url-pattern "ws://localhost:7409" \ - --storage-url http://localhost:7100/ \ - --connect-sub tcp://127.0.0.1:7114 \ - --connect-push tcp://127.0.0.1:7116 \ - --connect-app-dealer tcp://127.0.0.1:7112 \ - --connect-dev-dealer tcp://127.0.0.1:7115 \ - --wda-host 127.0.0.1 \ - --wda-port 8100 diff --git a/lib/cli/ios-device/index.js b/lib/cli/ios-device/index.js index 6afa3428ec..799639f7ab 100644 --- a/lib/cli/ios-device/index.js +++ b/lib/cli/ios-device/index.js @@ -4,11 +4,6 @@ export const describe = 'Start an ios device provider.' export const builder = function(yargs) { return yargs .strict() - .option('boot-complete-timeout', { - describe: 'How long to wait for boot to complete during device setup.', - type: 'number', - default: 60000 - }) .option('cleanup', { describe: 'Attempt to reset the device between uses by uninstalling' + 'apps, resetting accounts and clearing caches. Does not do a perfect ' + @@ -176,7 +171,6 @@ export const handler = function(argv) { wdaPort: argv.wdaPort, mjpegPort: argv.mjpegPort, heartbeatInterval: argv.heartbeatInterval, - bootCompleteTimeout: argv.bootCompleteTimeout, lockRotation: argv.lockRotation, cleanup: argv.cleanup, screenReset: argv.screenReset, diff --git a/lib/cli/ios-device/install-wda.sh b/lib/cli/ios-device/install-wda.sh deleted file mode 100755 index af36abb733..0000000000 --- a/lib/cli/ios-device/install-wda.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash -set -o pipefail - -cd "$(git rev-parse --show-toplevel)" || exit - - -if [ -n "$1" ]; then - deviceId=$1 -else - deviceId=$(idb list-targets | grep "device" | awk -F '|' '{ gsub(/ /, "", $0); print $2 }' | head -1) -fi - -if [ -n "$2" ]; then - deviceNum=$2 -else - deviceNum=0 -fi -deviceName=$(idb list-targets | grep "$deviceId" | awk -F '|' '{ gsub(/ $/, "", $1); print $1 }' | head -1) -echo "Using device $deviceId ($deviceName) with nmber $deviceNum" >&2 -pushd WebDriverAgent || exit -xcodebuild \ - -project WebDriverAgent.xcodeproj \ - -scheme WebDriverAgentRunner \ - -destination "id=$deviceId" \ - -allowProvisioningUpdates \ - build || exit - -xcodebuild \ - -project WebDriverAgent.xcodeproj \ - -scheme WebDriverAgentRunner \ - -destination "id=$deviceId" \ - -allowProvisioningUpdates \ - test diff --git a/lib/cli/ios-device/run-wda.sh b/lib/cli/ios-device/run-wda.sh deleted file mode 100755 index 607d81d746..0000000000 --- a/lib/cli/ios-device/run-wda.sh +++ /dev/null @@ -1,78 +0,0 @@ -#!/bin/bash -set -o pipefail - -cd "$(git rev-parse --show-toplevel)" || exit - - -if [ -n "$1" ]; then - deviceId=$1 -else - deviceId=$(idb list-targets | grep "device" | awk -F '|' '{ gsub(/ /, "", $0); print $2 }' | head -1) -fi - -if [ -z "$deviceId" ]; then - echo "Could not find device with empty" - exit -fi - -if [ -n "$2" ]; then - deviceNum=$2 -else - deviceNum=0 -fi -deviceName=$(idb list-targets | grep "$deviceId" | awk -F '|' '{ gsub(/ $/, "", $1); print $1 }' | head -1) - -echo "Using device $deviceId ($deviceName) with number $deviceNum" >&2 - -trap exit EXIT - -pushd WebDriverAgent || exit - -xcodebuild \ - -project WebDriverAgent.xcodeproj \ - -scheme WebDriverAgentRunner \ - -destination "id=$deviceId" \ - -allowProvisioningUpdates \ - build || exit - -xcodebuild \ - -project WebDriverAgent.xcodeproj \ - -scheme WebDriverAgentRunner \ - -destination "id=$deviceId" \ - -allowProvisioningUpdates \ - test | tee /tmp/wdalog.txt & -wdapid=$! - -# source ~/Documents/pymobiledevice3/.venv/bin/activate -pymobiledevice3 usbmux forward --serial $deviceId 910$deviceNum 9100 & -mjpegpid=$! -pymobiledevice3 usbmux forward --serial $deviceId 810$deviceNum 8100 & -wdaproxypic=$! - -function exit() { - kill $wdapid - kill $mjpegpid - kill $wdaproxypic - wait -} - -gtail -n1 -f /tmp/wdalog.txt | grep -q "ServerURLHere" > /dev/null -# echo "Found line" - -cd "$(git rev-parse --show-toplevel)" || exit -MONGODB_PORT_27017_TCP=mongodb://devicehub-mongo:27017 stf ios-device \ - --serial "$deviceId" \ - --host localhost \ - --screen-port 1800$deviceNum \ - --mjpeg-port 910$deviceNum \ - --connect-port 1820$deviceNum \ - --provider mightyworker \ - --public-ip localhost \ - --screen-ws-url-pattern "ws://localhost:1800$deviceNum" \ - --storage-url http://localhost:7100/ \ - --connect-sub tcp://127.0.0.1:7250 \ - --connect-push tcp://127.0.0.1:7270 \ - --connect-app-dealer tcp://127.0.0.1:7160 \ - --connect-dev-dealer tcp://127.0.0.1:7260 \ - --wda-host 127.0.0.1 \ - --wda-port 810$deviceNum || exit diff --git a/lib/cli/ios-device/start-ios.sh b/lib/cli/ios-device/start-ios.sh index 758c083b34..c19d0458bd 100755 --- a/lib/cli/ios-device/start-ios.sh +++ b/lib/cli/ios-device/start-ios.sh @@ -1,7 +1,6 @@ #!/bin/bash stf ios-device \ --serial "2BFBC585-B481-450B-914C-640D114BAAC7" \ - --device-name "iPhone 11" \ --host localhost \ --screen-port 7409 \ --mjpeg-port 9100 \ @@ -12,7 +11,5 @@ stf ios-device \ --storage-url http://localhost:7100/ \ --connect-sub tcp://127.0.0.1:7114 \ --connect-push tcp://127.0.0.1:7116 \ - --connect-app-dealer tcp://127.0.0.1:7112 \ - --connect-dev-dealer tcp://127.0.0.1:7115 \ --wda-host 127.0.0.1 \ --wda-port 8100 diff --git a/lib/cli/ios-provider/index.js b/lib/cli/ios-provider/index.js index 8f974f9eb9..a0bfb9e8cb 100644 --- a/lib/cli/ios-provider/index.js +++ b/lib/cli/ios-provider/index.js @@ -19,15 +19,10 @@ export const builder = function(yargs) { default: null }) // copied from cli/ios-device.js - .option('boot-complete-timeout', { - describe: 'How long to wait for boot to complete during device setup.', - type: 'number', - default: 60000 - }) .option('cleanup', { describe: 'Attempt to reset the device between uses by uninstalling' + 'apps, resetting accounts and clearing caches. Does not do a perfect ' + - 'job currently. Negate with --no-cleanup.', + 'job currently.', type: 'boolean', default: true }) @@ -120,7 +115,7 @@ export const builder = function(yargs) { }) .option('screen-reset', { describe: 'Go back to home screen and reset screen rotation ' + - 'when user releases device. Negate with --no-screen-reset.', + 'when user releases device.', type: 'boolean', default: true }) @@ -161,6 +156,9 @@ export const handler = function(argv) { argv.serial = argv.serial.filter(s => !!s.trim().length) } + argv.connectPush = argv.connectPush.filter(s => !!s.trim().length) + argv.connectSub = argv.connectSub.filter(s => !!s.trim().length) + const cli = path.resolve(import.meta.dirname, '..') const allPorts = _.range(argv.portRangeMin, argv.portRangeMax) const [wdaPorts, screenWsPorts] = _.chunk(allPorts, Math.ceil(allPorts.length / 2)) @@ -176,9 +174,10 @@ export const handler = function(argv) { filter: !argv.serial?.length ? null : (serial => argv.serial.find(serial)), screenWsUrlPattern: argv.screenWsUrlPattern, killTimeout: 30000, + publicIp: argv.publicIp, endpoints: { - push: argv.connectPush.filter(s => !!s.trim().length), - sub: argv.connectSub.filter(s => !!s.trim().length) + push: argv.connectPush, + sub: argv.connectSub }, fork: (serial, opts) => { const args = [ @@ -190,24 +189,26 @@ export const handler = function(argv) { '--provider', argv.provider, '--public-ip', argv.publicIp, '--screen-ws-url-pattern', argv.screenWsUrlPattern, - '--storage-url', argv.storageUrl, + '--connect-url-pattern', argv.connectUrlPattern, + '--screen-ping-interval', argv.screenPingInterval, + '--screen-jpeg-quality', argv.screenJpegQuality, + '--heartbeat-interval', argv.heartbeatInterval, '--connect-port', opts.connectPort, + '--storage-url', argv.storageUrl, '--wda-host', '127.0.0.1', '--wda-port', opts.wdaPort, - '--secret', argv.secret + '--secret', argv.secret, + '--is-simulator', opts.isSimulator + '', + + ... argv.connectSub.reduce((all, val) => all.concat(['--connect-sub', val]), []), + ... argv.connectPush.reduce((all, val) => all.concat(['--connect-push', val]), []), + + + ... (opts.isSimulator ? ['--is-simulator', 'true'] : []), + ... (opts.esp32Path ? ['--esp-32-path', opts.esp32Path] : []), + ... (argv.wdaPath ? ['--wda-path', argv.wdaPath] : []), + ... (argv.lockRotation ? ['--lock-rotation', argv.lockRotation] : []) ] - .concat(argv.connectSub.reduce(function(all, val) { - return all.concat(['--connect-sub', val]) - }, [])) - .concat(argv.connectPush.reduce(function(all, val) { - return all.concat(['--connect-push', val]) - }, [])) - .concat(opts.esp32Path ? ['--esp-32-path', opts.esp32Path] : []) - .concat(argv.wdaPath ? ['--wda-path', argv.wdaPath] : []) - .concat(opts.isSimulator ? ['--is-simulator'] : []) - // .concat(argv.lockRotation ? ['--lock-rotation'] : []) - // .concat(!argv.cleanup ? ['--no-cleanup'] : []) - // .concat(!argv.screenReset ? ['--no-screen-reset'] : []) return fork(cli, args) } }) diff --git a/lib/db/models/all/model.js b/lib/db/models/all/model.js index 732170fb69..ee5eb4d042 100644 --- a/lib/db/models/all/model.js +++ b/lib/db/models/all/model.js @@ -1186,6 +1186,7 @@ export const updateIosDevice = function(message) { $set: { id: message.id, model: message.name, + marketName: message.marketName, platform: message.platform, sdk: message.sdk, abi: message.architecture, @@ -1209,15 +1210,16 @@ export const setDeviceIosVersion = function(message) { ) } -// dbapi.sizeIosDevice = function(serial, height, width, scale) { -export const sizeIosDevice = function(serial, height, width, scale) { +// dbapi.sizeIosDevice = function(serial, height, width, scale, url) { +export const sizeIosDevice = function(serial, height, width, scale, url) { return db.devices.updateOne( {serial: serial}, { $set: { 'display.scale': scale, 'display.height': height, - 'display.width': width + 'display.width': width, + 'display.url': url } } ) @@ -1259,13 +1261,9 @@ export const setDeviceType = function(serial, type) { // dbapi.initializeIosDeviceState = function(publicIp, message) { export const initializeIosDeviceState = function(publicIp, message) { - const screenWsUrlPattern = - message.provider.screenWsUrlPattern || `ws://${publicIp}:${message.ports.screenPort}/` - const data = { present: true, presenceChangedAt: getNow(), - provider: message.provider, owner: null, status: message.status, statusChangedAt: getNow(), @@ -1274,23 +1272,12 @@ export const initializeIosDeviceState = function(publicIp, message) { remoteConnect: false, remoteConnectUrl: null, usage: null, - display: { - density: DEFAULT_IOS_DEVICE_ARGS.DENSITY, - fps: DEFAULT_IOS_DEVICE_ARGS.FPS, - id: DEFAULT_IOS_DEVICE_ARGS.ID, - rotation: DEFAULT_IOS_DEVICE_ARGS.ROTATION, - secure: DEFAULT_IOS_DEVICE_ARGS.SECURE, - size: DEFAULT_IOS_DEVICE_ARGS.SIZE, - xdpi: DEFAULT_IOS_DEVICE_ARGS.XDPI, - ydpi: DEFAULT_IOS_DEVICE_ARGS.YDPI, - url: screenWsUrlPattern - }, 'group.owner.email': process.env.STF_ADMIN_EMAIL || 'administrator@fakedomain.com', 'group.owner.name': process.env.STF_ADMIN_NAME || 'administrator', screenPort: message.ports.screenPort, connectPort: message.ports.connectPort, model: message.options.name, - marketName: message.options.name, + marketName: message.options.marketName, product: message.options.name, platform: message.options.platform, sdk: message.options.sdk, diff --git a/lib/units/app/index.js b/lib/units/app/index.js index 5c45f2276c..ef00414a46 100644 --- a/lib/units/app/index.js +++ b/lib/units/app/index.js @@ -3,7 +3,7 @@ import express from 'express' import bodyParser from 'body-parser' import serveStatic from 'serve-static' import logger from '../../util/logger.js' -import * as pathutil from '../../util/pathutil.cjs' +import {root, reactFrontend} from '../../util/pathutil.js' import rateLimitConfig from '../ratelimit/index.js' import * as markdownServe from 'markdown-serve' export default (async function(options) { @@ -26,7 +26,7 @@ export default (async function(options) { }) let server = http.createServer(app) app.use('/static/wiki', markdownServe.middleware({ - rootDirectory: pathutil.root('node_modules/@devicefarmer/stf-wiki'), + rootDirectory: root('node_modules/@devicefarmer/stf-wiki'), view: 'docs' })) app.set('strict routing', true) @@ -37,9 +37,9 @@ export default (async function(options) { app.get('/auth', function(req, res) { res.redirect(options.authUrl) }) - app.use('/', serveStatic(pathutil.reactFrontend('dist'), {fallthrough: true})) - app.use('/assets', serveStatic(pathutil.reactFrontend('dist/assets'))) - app.use('/locales', serveStatic(pathutil.reactFrontend('dist/locales'))) + app.use('/', serveStatic(reactFrontend('dist'), {fallthrough: true})) + app.use('/assets', serveStatic(reactFrontend('dist/assets'))) + app.use('/locales', serveStatic(reactFrontend('dist/locales'))) app.get('/app/api/v1/auth_url', function(req, res) { res.send({ @@ -71,5 +71,5 @@ export default (async function(options) { }) server.listen(options.port) - log.info('Listening on port %d', options.port) + log.info('Listening on port %s', options.port) }) diff --git a/lib/units/auth/ldap.js b/lib/units/auth/ldap.js index bc66396ba6..fbb3283b9b 100644 --- a/lib/units/auth/ldap.js +++ b/lib/units/auth/ldap.js @@ -8,7 +8,7 @@ import logger from '../../util/logger.js' import * as requtil from '../../util/requtil.js' import * as ldaputil from '../../util/ldaputil.js' import * as jwtutil from '../../util/jwtutil.js' -import * as pathutil from '../../util/pathutil.cjs' +import {reactFrontend} from '../../util/pathutil.js' import lifecycle from '../../util/lifecycle.js' import rateLimitConfig from '../ratelimit/index.js' import apiutil, {ONE_DAY} from '../../util/apiutil.js' @@ -69,7 +69,7 @@ export default (function(options) { res.redirect('/#/auth/ldap/') }) app.get('/auth/ldap/*', (req, res) => { - res.sendFile(pathutil.reactFrontend('dist/auth/auth-ldap.html')) + res.sendFile(reactFrontend('dist/auth/auth-ldap.html')) }) app.post('/auth/api/v1/ldap', function(req, res) { const log = logger.createLogger('auth-ldap') diff --git a/lib/units/auth/mock.js b/lib/units/auth/mock.js index 678e41f084..6776cfcb7f 100644 --- a/lib/units/auth/mock.js +++ b/lib/units/auth/mock.js @@ -8,7 +8,7 @@ import basicAuth from 'basic-auth' import logger from '../../util/logger.js' import * as requtil from '../../util/requtil.js' import * as jwtutil from '../../util/jwtutil.js' -import * as pathutil from '../../util/pathutil.cjs' +import {reactFrontend} from '../../util/pathutil.js' import lifecycle from '../../util/lifecycle.js' import rateLimitConfig from '../ratelimit/index.js' import {ONE_DAY} from '../../util/apiutil.js' @@ -74,7 +74,7 @@ export default (function(options) { res.redirect('/#/auth/mock/') }) app.get('/auth/mock/*', (req, res) => { - res.sendFile(pathutil.reactFrontend('dist/auth/auth-mock.html')) + res.sendFile(reactFrontend('dist/auth/auth-mock.html')) }) app.post('/auth/api/v1/mock', function(req, res) { log.setLocalIdentifier(req.ip) diff --git a/lib/units/auth/openid.js b/lib/units/auth/openid.js index ec453714ae..a78475e3ca 100644 --- a/lib/units/auth/openid.js +++ b/lib/units/auth/openid.js @@ -7,16 +7,15 @@ import cookieParser from 'cookie-parser' import bodyParser from 'body-parser' import logger from '../../util/logger.js' import * as jwtutil from '../../util/jwtutil.js' -import * as pathutil from '../../util/pathutil.cjs' import _ from 'lodash' import rateLimitConfig from '../ratelimit/index.js' export default (function(options) { const log = logger.createLogger('auth-openid') - let app = express() - let client + const app = express() const callbackPath = '/auth/openid/callback' const redirectUri = _.trimEnd(options.appUrl, '/') + callbackPath + let client function tryConnect(times, delay) { openid.Issuer.discover(options.openid.identifierUrl).then((keycloakIssuer) => { client = new keycloakIssuer.Client({ diff --git a/lib/units/base-device/plugins/heartbeat.js b/lib/units/base-device/plugins/heartbeat.ts similarity index 57% rename from lib/units/base-device/plugins/heartbeat.js rename to lib/units/base-device/plugins/heartbeat.ts index f9151378b1..f4ad7aa969 100644 --- a/lib/units/base-device/plugins/heartbeat.js +++ b/lib/units/base-device/plugins/heartbeat.ts @@ -1,24 +1,29 @@ import syrup from '@devicefarmer/stf-syrup' import lifecycle from '../../../util/lifecycle.js' -import wire from '../../../wire/index.js' import wireutil from '../../../wire/util.js' import push from '../support/push.js' import EventEmitter from 'events' +import {DeviceHeartbeatMessage} from "../../../wire/wire.js" export default syrup.serial() .dependency(push) .define((options, push) => { - const emitter = new EventEmitter() - let timer - const beat = () => { + const emitter = new EventEmitter<{ + beat: [] + }>() + const payload = [ + wireutil.global, + wireutil.pack(DeviceHeartbeatMessage, { serial: options.serial }) + ] + + let timer: NodeJS.Timeout + const beat = () => ( timer = setTimeout(() => { + push.send(payload) beat() - push.send([ - wireutil.global, - wireutil.envelope(new wire.DeviceHeartbeatMessage(options.serial)) - ]) emitter.emit('beat') }, options.heartbeatInterval) - } + ) + beat() lifecycle.observe(() => clearTimeout(timer)) return emitter diff --git a/lib/units/base-device/plugins/solo.js b/lib/units/base-device/plugins/solo.ts similarity index 74% rename from lib/units/base-device/plugins/solo.js rename to lib/units/base-device/plugins/solo.ts index 8a0c3dabf9..a1c70653b0 100755 --- a/lib/units/base-device/plugins/solo.js +++ b/lib/units/base-device/plugins/solo.ts @@ -1,16 +1,17 @@ import crypto from 'crypto' import syrup from '@devicefarmer/stf-syrup' import logger from '../../../util/logger.js' -import wire from '../../../wire/index.js' import wireutil from '../../../wire/util.js' -import Promise from 'bluebird' import sub from '../support/sub.js' import push from '../support/push.js' +import {DeviceReadyMessage} from "../../../wire/wire.js" + export default syrup.serial() .dependency(sub) .dependency(push) .define((options, sub, push) => { const log = logger.createLogger('base-device:plugins:solo') + // The channel should keep the same value between restarts, so that // having the client side up to date all the time is not horribly painful. const makeChannelId = () => { @@ -18,19 +19,22 @@ export default syrup.serial() hash.update(options.serial) return hash.digest('base64') } + const channel = makeChannelId() + log.info('Subscribing to permanent channel "%s"', channel) sub.subscribe(channel) + return { channel: channel, poke: () => { - Promise.delay(5 * 1000) - .then(() => { - push.send([ - wireutil.global, - wireutil.envelope(new wire.DeviceReadyMessage(options.serial, channel)) - ]) + push.send([ + wireutil.global, + wireutil.pack(DeviceReadyMessage, { + serial: options.serial, + channel }) + ]) } } }) diff --git a/lib/units/device/index.ts b/lib/units/device/index.ts index 449935d910..768673cc49 100644 --- a/lib/units/device/index.ts +++ b/lib/units/device/index.ts @@ -30,10 +30,10 @@ import mobileService from './plugins/mobile-service.js' import remotedebug from './plugins/remotedebug.js' import {trackModuleReadyness} from './readyness.js' import wireutil from '../../wire/util.js' -import wire from '../../wire/index.js' import push from '../base-device/support/push.js' import adb from './support/adb.js' import router from '../base-device/support/router.js' +import {DeviceIntroductionMessage, DeviceRegisteredMessage, ProviderMessage} from "../../wire/wire.js"; export default (function(options: any) { return syrup.serial() @@ -51,7 +51,7 @@ export default (function(options: any) { let listener: ((...args: any[]) => void) | null = null const waitRegister = Promise.race([ new Promise(resolve => - router.on(wire.DeviceRegisteredMessage, listener = (...args: any[]) => resolve(args)) + router.on(DeviceRegisteredMessage, listener = (...args: any[]) => resolve(args)) ), new Promise(r => setTimeout(r, 15000)) ]) @@ -59,11 +59,19 @@ export default (function(options: any) { const type = await adb.getDevice(options.serial).getState() push?.send([ wireutil.global, - wireutil.envelope(new wire.DeviceIntroductionMessage(options.serial, wireutil.toDeviceStatus(type), new wire.ProviderMessage(solo.channel, `standalone-${options.serial}`))) + wireutil.pack(DeviceIntroductionMessage, { + serial: options.serial, + // @ts-ignore + status: wireutil.toDeviceStatus(type), + provider: { + channel: solo.channel, + name: `standalone-${options.serial}` + } + }) ]) await waitRegister - router.removeListener(wire.DeviceRegisteredMessage, listener!) + router.removeListener(DeviceRegisteredMessage, listener!) listener = null } diff --git a/lib/units/device/resources/minicap.js b/lib/units/device/resources/minicap.js index 0b1bdeac17..9c987b30cd 100644 --- a/lib/units/device/resources/minicap.js +++ b/lib/units/device/resources/minicap.js @@ -4,7 +4,7 @@ import Promise from 'bluebird' import syrup from '@devicefarmer/stf-syrup' import fs from 'fs' import logger from '../../../util/logger.js' -import * as pathutil from '../../../util/pathutil.cjs' +import {requiredMatch, module, match} from '../../../util/pathutil.js' import devutil from '../../../util/devutil.js' import * as streamutil from '../../../util/streamutil.js' import Resource from './util/resource.js' @@ -20,8 +20,8 @@ export default syrup.serial() let log = logger.createLogger('device:resources:minicap') let resources = { bin: new Resource({ - src: pathutil.requiredMatch(abi.all.map(function(supportedAbi) { - return pathutil.module(util.format('@devicefarmer/minicap-prebuilt/prebuilt/%s/bin/minicap%s', supportedAbi, abi.pie ? '' : '-nopie')) + src: requiredMatch(abi.all.map(function(supportedAbi) { + return module(util.format('@devicefarmer/minicap-prebuilt/prebuilt/%s/bin/minicap%s', supportedAbi, abi.pie ? '' : '-nopie')) })), dest: [ '/data/local/tmp/minicap', @@ -33,10 +33,10 @@ export default syrup.serial() lib: new Resource({ // @todo The lib ABI should match the bin ABI. Currently we don't // have an x86_64 version of the binary while the lib supports it. - src: pathutil.match(abi.all.reduce(function(all, supportedAbi) { + src: match(abi.all.reduce(function(all, supportedAbi) { return all.concat([ - pathutil.module(util.format('@devicefarmer/minicap-prebuilt/prebuilt/%s/lib/android-%s/minicap.so', supportedAbi, sdk.previewLevel)), - pathutil.module(util.format('@devicefarmer/minicap-prebuilt/prebuilt/%s/lib/android-%s/minicap.so', supportedAbi, sdk.level)) + module(util.format('@devicefarmer/minicap-prebuilt/prebuilt/%s/lib/android-%s/minicap.so', supportedAbi, sdk.previewLevel)), + module(util.format('@devicefarmer/minicap-prebuilt/prebuilt/%s/lib/android-%s/minicap.so', supportedAbi, sdk.level)) ]) }, [])), dest: [ @@ -47,7 +47,7 @@ export default syrup.serial() mode: 0o755 }), apk: new Resource({ - src: pathutil.match([pathutil.module('@devicefarmer/minicap-prebuilt/prebuilt/noarch/minicap.apk')]), + src: match([module('@devicefarmer/minicap-prebuilt/prebuilt/noarch/minicap.apk')]), dest: ['/data/local/tmp/minicap.apk'], comm: 'minicap.apk', mode: 0o755 diff --git a/lib/units/device/resources/minirev.js b/lib/units/device/resources/minirev.js index 95818f8476..391dba0839 100644 --- a/lib/units/device/resources/minirev.js +++ b/lib/units/device/resources/minirev.js @@ -3,7 +3,7 @@ import util from 'util' import Promise from 'bluebird' import syrup from '@devicefarmer/stf-syrup' import logger from '../../../util/logger.js' -import * as pathutil from '../../../util/pathutil.cjs' +import {requiredMatch, vendor} from '../../../util/pathutil.js' import devutil from '../../../util/devutil.js' import * as streamutil from '../../../util/streamutil.js' import Resource from './util/resource.js' @@ -17,8 +17,8 @@ export default syrup.serial() var log = logger.createLogger('device:resources:minirev') var resources = { bin: new Resource({ - src: pathutil.requiredMatch(abi.all.map(function(supportedAbi) { - return pathutil.vendor(util.format('minirev/%s/minirev%s', supportedAbi, abi.pie ? '' : '-nopie')) + src: requiredMatch(abi.all.map(function(supportedAbi) { + return vendor(util.format('minirev/%s/minirev%s', supportedAbi, abi.pie ? '' : '-nopie')) })), dest: [ '/data/local/tmp/minirev', diff --git a/lib/units/device/resources/minitouch.ts b/lib/units/device/resources/minitouch.ts index f6c529c233..dfcac6a89d 100644 --- a/lib/units/device/resources/minitouch.ts +++ b/lib/units/device/resources/minitouch.ts @@ -3,7 +3,7 @@ import fs from 'fs' import syrup from '@devicefarmer/stf-syrup' import type {Client} from '@u4/adbkit' import logger from '../../../util/logger.js' -import * as pathutil from '../../../util/pathutil.cjs' +import {requiredMatch, module} from '../../../util/pathutil.js' import devutil from '../../../util/devutil.js' import Resource from './util/resource.js' import adb from '../support/adb.js' @@ -31,8 +31,8 @@ export default syrup.serial() const log = logger.createLogger('device:resources:minitouch') const resources: MinitouchResource = { bin: new Resource({ - src: pathutil.requiredMatch(abi.all.map((supportedAbi: string) => - pathutil.module(util.format('@devicefarmer/minitouch-prebuilt/prebuilt/%s/bin/minitouch%s', supportedAbi, abi.pie ? '' : '-nopie')) + src: requiredMatch(abi.all.map((supportedAbi: string) => + module(util.format('@devicefarmer/minitouch-prebuilt/prebuilt/%s/bin/minitouch%s', supportedAbi, abi.pie ? '' : '-nopie')) )), dest: [ '/data/local/tmp/minitouch', diff --git a/lib/units/device/resources/service.js b/lib/units/device/resources/service.js index 8a10ed4841..1107f34468 100644 --- a/lib/units/device/resources/service.js +++ b/lib/units/device/resources/service.js @@ -2,7 +2,7 @@ import util from 'util' import syrup from '@devicefarmer/stf-syrup' import ProtoBuf from 'protobufjs' import semver from 'semver' -import * as pathutil from '../../../util/pathutil.cjs' +import {vendor} from '../../../util/pathutil.js' import * as streamutil from '../../../util/streamutil.js' import logger from '../../../util/logger.js' import {Adb} from '@u4/adbkit' @@ -11,12 +11,12 @@ export default syrup.serial() .dependency(adb) .define(function(options, adb) { let log = logger.createLogger('device:resources:service') - let builder = ProtoBuf.loadProtoFile(pathutil.vendor('STFService/wire.proto')) + let builder = ProtoBuf.loadProtoFile(vendor('STFService/wire.proto')) let STFServiceResource = { requiredVersion: '3.0.0', pkg: 'jp.co.cyberagent.stf', main: 'jp.co.cyberagent.stf.Agent', - apk: pathutil.vendor('STFService/STFService.apk'), + apk: vendor('STFService/STFService.apk'), wire: builder.build().jp.co.cyberagent.stf.proto, builder: builder, startIntent: { diff --git a/lib/units/ios-device/index.js b/lib/units/ios-device/index.js deleted file mode 100755 index befd862265..0000000000 --- a/lib/units/ios-device/index.js +++ /dev/null @@ -1,95 +0,0 @@ -import syrup from '@devicefarmer/stf-syrup' -import logger from '../../util/logger.js' -import lifecycle from '../../util/lifecycle.js' -import logger$0 from '../base-device/support/logger.js' -import heartbeat from '../base-device/plugins/heartbeat.js' -import solo from '../base-device/plugins/solo.js' -import info from './plugins/info/index.js' -import wda from './plugins/wda/index.js' -import push from '../base-device/support/push.js' -import sub from '../base-device/support/sub.js' -import group from '../base-device/plugins/group.js' -import storage from '../base-device/support/storage.js' -import devicelog from './plugins/devicelog.js' -import stream from './plugins/screen/stream.js' -import install from './plugins/install.js' -import reboot from './plugins/reboot.js' -import clipboard from './plugins/clipboard.js' -import remotedebug from './plugins/remotedebug.js' -import filesystem from './plugins/filesystem.js' -import * as iosutil from './plugins/util/iosutil.js' -import {execFileSync} from 'child_process' -import connect from './plugins/wda/connect.js' -import WDAService from './plugins/wda/WDAService.js' - -export default (async(/** @type {{ serial: any; } & any} */ options) => { - const wdaService = new WDAService(options.wdaPath) - try { - await wdaService.start( - options.serial, - // @ts-ignore - ...(!options.isSimulator ? [] : [options.wdaPort, options.mjpegPort]) - ) - - } - catch (err) { - await wdaService.cleanup(options.serial) - lifecycle.fatal(err) - } - - return syrup.serial() - .dependency(logger$0) - .define(function(options) { - const log = logger.createLogger('ios-device') - log.info('Preparing device options: %s', JSON.stringify(options)) - - const deviceInfo = JSON.parse(execFileSync('idb', ['describe', '--udid', options.serial, '--json']).toString()) - options.deviceInfo = deviceInfo - if (!options.isSimulator) { - options.deviceName = iosutil.getModelName(deviceInfo.extended.device.ProductType) - } - else { - options.deviceName = `Simulator ${deviceInfo.name}` - } - - return syrup.serial() - .dependency(heartbeat) - .dependency(solo) - .dependency(info) - .dependency(wda) - .dependency(connect) - .dependency(push) - .dependency(sub) - .dependency(group) - .dependency(storage) - .dependency(devicelog) - .dependency(stream) - .dependency(install) - .dependency(reboot) - .dependency(clipboard) - .dependency(remotedebug) - .dependency(filesystem) - .define(async(options, heartbeat, solo, info, wda, connect) => { - - try { - await info.init() - - wda.connect() - solo.poke() - connect() - - if (process.send) { - process.send('ready') - } - } - catch (err) { - lifecycle.fatal(err) - } - }) - .consume(options) - }) - .consume(options) - .catch((err) => { - lifecycle.fatal(err) - }) -}) diff --git a/lib/units/ios-device/index.ts b/lib/units/ios-device/index.ts new file mode 100755 index 0000000000..c21cab6475 --- /dev/null +++ b/lib/units/ios-device/index.ts @@ -0,0 +1,225 @@ +import syrup from '@devicefarmer/stf-syrup' +import logger from '../../util/logger.js' +import lifecycle from '../../util/lifecycle.js' +import heartbeat from '../base-device/plugins/heartbeat.js' +import solo from '../base-device/plugins/solo.js' +import info from './plugins/info.js' +import wdaClient from './plugins/wda/client.js' +import wda from './plugins/wda/index.js' +import push from '../base-device/support/push.js' +import sub from '../base-device/support/sub.js' +import group from '../base-device/plugins/group.js' +import storage from '../base-device/support/storage.js' +import devicelog from './plugins/devicelog.js' +import stream from './plugins/screen/stream.js' +import install from './plugins/install.js' +import reboot from './plugins/reboot.js' +import clipboard from './plugins/clipboard.js' +import remotedebug from './plugins/remotedebug.js' +import filesystem from './plugins/filesystem.js' +import connect from './plugins/wda/connect.js' +import {DeviceAbsentMessage, DeviceStatus, DeviceStatusMessage} from "../../wire/wire.js" +import wireutil from "../../wire/util.js" +import {WebDriverAgent} from "appium-webdriveragent" +import { openPort } from "./redirect-ports.js" + +interface Options { + serial: string + provider: string + isSimulator: boolean + wdaPath: string + wdaHost: string + wdaPort: number + mjpegPort: number + + publicIp: string, + endpoints: { + sub: string[] + push: string[] + }, + groupTimeout: number + storageUrl: string + screenJpegQuality: string + screenPingInterval: number + screenPort: number + screenWsUrlPattern: string + connectUrlPattern: string + heartbeatInterval: number + lockRotation: boolean + cleanup: boolean + screenReset: boolean + deviceName: string + host: string + esp32Path: string + secret: string + connectPort: number + disableLogsOverWire: boolean +} + +export default (async(options: Options) => { + const [stopWDAPortForwarding, stopMJPEGPortForwarding] = options.isSimulator + ? [async() => {}, async() => {}] + : await Promise.all([ + openPort(options.wdaPort, options.wdaPort, options.serial), + openPort(options.mjpegPort, options.mjpegPort, options.serial), + ]) + + const stopPortForwarding = async() => { + if (options.isSimulator) return + await Promise.all([ + stopWDAPortForwarding(), + stopMJPEGPortForwarding() + ]) + } + + const WDA = new WebDriverAgent({ + device: {udid: options.serial}, + realDevice: !options.isSimulator, + wdaRemotePort: options.wdaPort, + wdaConnectionTimeout: 60_000, + wdaLaunchTimeout: 60_000, + prebuildWDA: true, + usePrebuiltWDA: false, + mjpegServerPort: options.mjpegPort, + usePreinstalledWDA: false, + allowProvisioningDeviceRegistration: true, + showXcodeLog: true, + updatedWDABundleId: 'com.dhub.WebDriverAgentRunner' + }) + + lifecycle.observe(async() => { + await WDA.quit() + await stopPortForwarding() + }) + + const waitWDA = async(attempts = 100) => { + try { + const response = await fetch(`http://${options.wdaHost}:${options.wdaPort}/status`) + const res = await response.json() + if (res?.value?.state !== 'success') { + throw new Error(`WebDriverAgent error: invalid state ${JSON.stringify(res)}`) + } + + return + } catch (e) { + if (--attempts) { + await new Promise(r => setTimeout(r, 1000)) + return waitWDA(attempts) + } + throw e + } + } + + try { + await WDA.setupCaching() + await WDA.launch(options.provider) + await new Promise(r => setTimeout(r, 5000)) + await waitWDA() + } catch (e: any) { + await WDA.quit() + await stopPortForwarding() + + lifecycle.fatal(e) + } + + + return syrup.serial() + .dependency(heartbeat) + .dependency(wdaClient) + .dependency(push) + .dependency(wda) + .define(async(options, heartbeat, wdaClient, push) => { + const log = logger.createLogger('ios-device') + log.info('Preparing device options: %s', JSON.stringify(options)) + + push.send([ + wireutil.global, + wireutil.pack(DeviceStatusMessage, { + serial: options.serial, + status: DeviceStatus.CONNECTING + }) + ]) + + const absentDevice = () => + push.send([ + wireutil.global, + wireutil.pack(DeviceAbsentMessage, { serial: options.serial }) + ]) + + let failedWdaChecks = 0 + wdaClient.once('connected', () => { + heartbeat.on('beat', async() => { + if (await wdaClient.healthCheck()) { + failedWdaChecks = 0 + return + } + + if (++failedWdaChecks >= 4) { + absentDevice() + lifecycle.fatal('WDA request error: unable to get response') + } + }) + }) + + return syrup.serial() + .dependency(solo) + .dependency(info) + .dependency(connect) + .dependency(group) + .dependency(sub) + .dependency(storage) + .dependency(devicelog) + .dependency(stream) + .dependency(install) + .dependency(reboot) + .dependency(clipboard) + .dependency(remotedebug) + .dependency(filesystem) + .define(async(options, solo, info, connect, group) => { + try { + + // one-time session for init additional device info + await new Promise(resolve => { + wdaClient.once('connected', async() => { + await wdaClient.startSession() + await wdaClient.stopSession() + resolve() + }) + wdaClient.connect() + }) + + group.on('join', async() => { + await wdaClient.startSession() + }) + + group.on('leave', async() => { + await wdaClient.lock() + await wdaClient.stopSession() + }) + + connect() + solo.poke() + + push.send([ + wireutil.global, + wireutil.pack(DeviceStatusMessage, { + serial: options.serial, + status: DeviceStatus.ONLINE + }) + ]) + + if (process.send) { + process.send('ready') + } + } + catch (err: any) { + lifecycle.fatal(err) + } + }) + .consume(options) + }) + .consume(options) + .catch((err) => { + lifecycle.fatal(err) + }) +}) diff --git a/lib/units/ios-device/plugins/clipboard.js b/lib/units/ios-device/plugins/clipboard.js deleted file mode 100755 index 140f4b3f69..0000000000 --- a/lib/units/ios-device/plugins/clipboard.js +++ /dev/null @@ -1,32 +0,0 @@ -import syrup from '@devicefarmer/stf-syrup' -import wire from '../../../wire/index.js' -import wireutil from '../../../wire/util.js' -import router from '../../base-device/support/router.js' -import push from '../../base-device/support/push.js' -import wdaClient from './wda/client.js' -import Logger from '../../../util/logger.js' -import {CopyMessage} from '../../../wire/wire.js' -const log = Logger.createLogger('ios-device:clipboard') -export default syrup.serial() - .dependency(router) - .dependency(push) - .dependency(wdaClient) - .define(function(options, router, push, wdaClient) { - router.on(CopyMessage, function(channel) { - const reply = wireutil.reply(options.serial) - wdaClient.getClipBoard() - .then(clipboard => { - push.send([ - channel, - reply.okay(clipboard) - ]) - }) - .catch((err) => { - log.error('Error on getting clipboard: ', err) - push.send([ - channel, - reply.fail('') - ]) - }) - }) - }) diff --git a/lib/units/ios-device/plugins/clipboard.ts b/lib/units/ios-device/plugins/clipboard.ts new file mode 100755 index 0000000000..6f516168c9 --- /dev/null +++ b/lib/units/ios-device/plugins/clipboard.ts @@ -0,0 +1,21 @@ +import syrup from '@devicefarmer/stf-syrup' +import wireutil from '../../../wire/util.js' +import router from '../../base-device/support/router.js' +import push from '../../base-device/support/push.js' +import wdaClient from './wda/client.js' +import {CopyMessage} from '../../../wire/wire.js' + +export default syrup.serial() + .dependency(router) + .dependency(push) + .dependency(wdaClient) + .define((options, router, push, wdaClient) => { + router.on(CopyMessage, async(channel) => { + const reply = wireutil.reply(options.serial) + const clipboard = await wdaClient.getClipBoard() + push.send([ + channel, + reply.okay(clipboard) + ]) + }) + }) diff --git a/lib/units/ios-device/plugins/devicenotifier.js b/lib/units/ios-device/plugins/devicenotifier.js deleted file mode 100644 index da9f985eef..0000000000 --- a/lib/units/ios-device/plugins/devicenotifier.js +++ /dev/null @@ -1,42 +0,0 @@ -import syrup from '@devicefarmer/stf-syrup' -import wire from '../../../wire/index.js' -import wireutil from '../../../wire/util.js' -import logger from '../../../util/logger.js' -import lifecycle from '../../../util/lifecycle.js' -import push from '../../base-device/support/push.js' -import group from '../../base-device/plugins/group.js' -export default syrup.serial() - .dependency(push) - .dependency(group) - .define(function(options, push, group) { - const log = logger.createLogger('device:plugins:notifier') - const notifier = {} - notifier.setDeviceTemporaryUnavailable = function(err) { - group.get() - .then((currentGroup) => { - push.send([ - currentGroup.group, - wireutil.envelope(new wire.TemporarilyUnavailableMessage(options.serial)) - ]) - }) - .catch(err => { - log.error('Cannot set device temporary unavailable', err) - }) - } - notifier.setDeviceAbsent = function(err) { - if (err.statusCode) { - push.send([ - wireutil.global, - wireutil.envelope(new wire.DeviceStatusMessage(options.serial, 1)) - ]) - } - else { - push.send([ - wireutil.global, - wireutil.envelope(new wire.DeviceAbsentMessage(options.serial)) - ]) - } - lifecycle.graceful(err) - } - return notifier - }) diff --git a/lib/units/ios-device/plugins/info.ts b/lib/units/ios-device/plugins/info.ts new file mode 100644 index 0000000000..55a32e5fc5 --- /dev/null +++ b/lib/units/ios-device/plugins/info.ts @@ -0,0 +1,116 @@ +import syrup from '@devicefarmer/stf-syrup' +import wireutil from '../../../wire/util.js' +import logger from '../../../util/logger.js' +import _ from 'lodash' +import push from '../../base-device/support/push.js' +import {getModelName} from './util/iosutil.js' +import wdaClient from './wda/client.js' +import { + BatteryEvent, + InitializeIosDeviceState, + IosDevicePorts, + RotationEvent, + SdkIosVersion, + SizeIosDevice, + UpdateIosDevice +} from '../../../wire/wire.js' +import {execFileSync} from 'child_process' + +export default syrup.serial() + .dependency(push) + .dependency(wdaClient) + .define(async(options, push, wdaClient) => { + const log = logger.createLogger('device:info') + + const deviceInfo = JSON.parse( + execFileSync('idb', ['describe', '--udid', options.serial, '--json']).toString() + ) + + options.deviceInfo = deviceInfo + const marketName = getModelName(deviceInfo?.extended?.device?.ProductType) || deviceInfo?.name || 'unknown' + options.deviceName = deviceInfo?.name || marketName + + if (options.isSimulator) { + options.deviceName = `Simulator ${options.deviceName}` + } + + log.info('Device name: ' + options.deviceName) + + const os = options.deviceInfo?.os_version?.split(' ') + + push.send([ + wireutil.global, + wireutil.pack(InitializeIosDeviceState, { + serial: options.serial, + status: wireutil.toDeviceStatus('device'), + ports: IosDevicePorts.create({ + screenPort: options.screenPort, + connectPort: options.mjpegPort + }), + options: UpdateIosDevice.create({ + id: options.serial, + name: options.deviceName, + platform: os ? os[0] : 'unknown', + architecture: options.deviceInfo?.architecture || 'unknown', + sdk: os ? os[1] : 'unknown', + service: {hasAPNS: true}, + marketName + }) + }) + ]) + + + + wdaClient.on('session', sdk => { + if (!sdk) return + push.send([ + wireutil.global, + wireutil.pack(SdkIosVersion, { + id: options.serial, + sdkVersion: sdk + }) + ]) + }) + + wdaClient.on('battery', (batteryState, batteryLevel) => { + push.send([ + wireutil.global, + wireutil.pack(BatteryEvent, { + serial: options.serial, + status: batteryState, + health: 'good', + source: 'usb', + level: batteryLevel, + scale: 1, + temp: 0.0, + voltage: 5 + }) + ]) + }) + + wdaClient.on('rotation', (orientation, rotationDegrees) => { + if (!rotationDegrees) return + push.send([ + wireutil.global, + wireutil.pack(RotationEvent, { + serial: options.serial, + rotation: rotationDegrees + }) + ]) + }) + + wdaClient.on('display', (display) => { + push.send([ + wireutil.global, + wireutil.pack(SizeIosDevice, { + id: options.serial, + url: _.template(options.screenWsUrlPattern || '')({ + publicIp: options.publicIp, + publicPort: options.screenPort, + serial: options.serial + }), + ...display + }) + ]) + }) + }) diff --git a/lib/units/ios-device/plugins/info/index.js b/lib/units/ios-device/plugins/info/index.js deleted file mode 100644 index 11f55815d2..0000000000 --- a/lib/units/ios-device/plugins/info/index.js +++ /dev/null @@ -1,147 +0,0 @@ -import syrup from '@devicefarmer/stf-syrup' -import wireutil from '../../../../wire/util.js' -import wire from '../../../../wire/index.js' -import logger from '../../../../util/logger.js' -import request from 'request-promise' -import _ from 'lodash' -import push from '../../../base-device/support/push.js' -import * as iosutil from '../util/iosutil.js' -import {InitializeIosDeviceState, IosDevicePorts, ProviderIosMessage, UpdateIosDevice} from '../../../../wire/wire.js' -export default syrup.serial() - .dependency(push) - .define(async(options, push) => { - const log = logger.createLogger('device:info') - - const baseUrl = iosutil.getUri(options.wdaHost, options.wdaPort) - const extendedInfo = {} - - log.info('device.name: ' + options.deviceName) - - let solo = wireutil.makePrivateChannel() - let osName = options.deviceInfo.os_version.split(' ')[0] - let osVersion = options.deviceInfo.os_version.split(' ')[1] - - const serviceData = {hasAPNS: true} - const wsUrl = _.template(options.screenWsUrlPattern || '')({ - publicIp: options.publicIp, - publicPort: options.screenPort, - serial: options.serial - }) - - push.send([ - wireutil.global, - wireutil.pack(InitializeIosDeviceState, { - serial: options.serial, - status: wireutil.toDeviceStatus('device'), - provider: ProviderIosMessage.create({ - channel: solo, - name: options.provider, - screenWsUrlPattern: wsUrl - }), - ports: IosDevicePorts.create({ - screenPort: options.screenPort, - connectPort: options.mjpegPort - }), - options: UpdateIosDevice.create({ - id: options.serial, - name: options.deviceName, - platform: osName, - architecture: options.deviceInfo.architecture, - sdk: osVersion, - service: serviceData - }) - }) - ]) - - const handleRequest = (reqOptions) => { - return new Promise((resolve, reject) => { - request(reqOptions) - .then((res) => { - resolve(res) - }) - .catch((err) => { - reject(err) - }) - }) - } - const init = async() => { - // Get device type - let deviceType - const deviceInfo = await handleRequest({ - method: 'GET', - uri: `${baseUrl}/wda/device/info`, - json: true - }) - let deviceInfoModel = deviceInfo.value.model.toLowerCase() - let deviceInfoName = deviceInfo.value.name.toLowerCase() - if (deviceInfoModel.includes('tv') || deviceInfoName.includes('tv')) { - deviceType = 'Apple TV' - } - else { - deviceType = 'iPhone' - } - // Store device type - log.info('Storing device type value: ' + deviceType) - push.send([ - wireutil.global, - wireutil.envelope(new wire.DeviceTypeMessage(options.serial, deviceType)) - ]) - const sessionResponse = await handleRequest({ - method: 'POST', - uri: `${baseUrl}/session`, - body: {capabilities: {}}, - json: true, - }) - let sessionId = sessionResponse.sessionId - // Store device version - log.info('Storing device version') - push.send([ - wireutil.global, - wireutil.envelope(new wire.SdkIosVersion(options.serial, sessionResponse.value.capabilities.sdkVersion)) - ]) - // Store battery info - if (deviceType !== 'Apple TV') { - const batteryInfoResponse = await handleRequest({ - method: 'GET', - uri: `${baseUrl}/session/${sessionId}/wda/batteryInfo`, - json: true, - }) - let batteryState = iosutil.batteryState(batteryInfoResponse.value.state) - let batteryLevel = iosutil.batteryLevel(batteryInfoResponse.value.level) - push.send([ - wireutil.global, - wireutil.envelope(new wire.BatteryEvent(options.serial, batteryState, 'good', 'usb', batteryLevel, 1, 0.0, 5)) - ]) - } - // Store size info - const firstSessionSize = await handleRequest({ - method: 'GET', - uri: `${baseUrl}/session/${sessionId}/window/size`, - json: true - }) - let deviceSize = firstSessionSize.value - options.deviceInfo.screenSize = deviceSize - let {width, height} = deviceSize - const scaleResponse = await handleRequest({ - method: 'GET', - uri: `${baseUrl}/session/${sessionId}/wda/screen`, - }) - let parsedResponse = JSON.parse(scaleResponse) - log.debug(`Screen sizes: ${JSON.stringify(deviceSize)}, ${scaleResponse}`) - let scale = parsedResponse.value.scale - height *= scale - width *= scale - - Object.assign(extendedInfo, {width, height, scale}) - log.info('Storing device size/scale') - - push.send([ - wireutil.global, - wireutil.envelope(new wire.SizeIosDevice(options.serial, height, width, scale)) - ]) - } - - return { - init, extendedInfo - } - }) diff --git a/lib/units/ios-device/plugins/screen/stream.js b/lib/units/ios-device/plugins/screen/stream.ts similarity index 87% rename from lib/units/ios-device/plugins/screen/stream.js rename to lib/units/ios-device/plugins/screen/stream.ts index 0ef15cfce6..5485fa1b7b 100755 --- a/lib/units/ios-device/plugins/screen/stream.js +++ b/lib/units/ios-device/plugins/screen/stream.ts @@ -7,7 +7,6 @@ import request from 'postman-request' import logger from '../../../../util/logger.js' import * as iosutil from '../util/iosutil.js' import solo from '../../../base-device/plugins/solo.js' -import devicenotifier from '../devicenotifier.js' import wdaClient from '../wda/client.js' import push from '../../../base-device/support/push.js' import group from '../../../base-device/plugins/group.js' @@ -15,14 +14,14 @@ import {NoGroupError} from '../../../../util/grouputil.js' import {decode} from '../../../../util/jwtutil.js' export default syrup.serial() .dependency(solo) - .dependency(devicenotifier) .dependency(wdaClient) .dependency(push) .dependency(group) - .define(function(options, solo, notifier, WdaClient, push, group) { + .define(function(options, solo, WdaClient, push, group) { const log = logger.createLogger('device:plugins:screen:stream') const wss = new webSocketServer.Server({port: options.screenPort}) - let url = iosutil.getUri(options.wdaHost, options.mjpegPort) + const url = iosutil.getUri(options.wdaHost, options.mjpegPort) + wss.on('connection', async(ws, req) => { // Extract token from WebSocket subprotocols const token = ws.protocol.substring('access_token.'.length) @@ -64,7 +63,7 @@ export default syrup.serial() message: 'Authentication successful' })) } - catch (/** @type {any} */err) { + catch (err: any) { if (!fail && err instanceof NoGroupError) { await new Promise(r => setTimeout(r, 1000)) return tryCheckDeviceGroup(true) @@ -99,8 +98,7 @@ export default syrup.serial() let isConnectionAlive = true function handleSocketError(err, message) { - log.error(message, err) - notifier.setDeviceTemporaryUnavailable(err) + log.error(`${message} %s`, err) ws.close() } @@ -133,15 +131,15 @@ export default syrup.serial() } chain .then(() => handleSocketError({message: 'Connection failed to WDA MJPEG port'}, 'Consumer error')) - .catch(result => { + .catch(async(result) => { if (result) { result.response.pipe(consumer).pipe(stream) // [VD] We can't launch homeBtn otherwise opening in STF corrupt test automation run. Also no sense to execute connect - WdaClient.startSession() + await WdaClient.startSession() // WdaClient.homeBtn() //no existing session detected so we can press home button to wake up device automatically // override already existing error handler result.frameStream.on('error', function(err) { - handleSocketError(err, 'frameStream error ') + handleSocketError(err, 'frameStream error') }) } }) @@ -150,48 +148,51 @@ export default syrup.serial() consumer.on('error', (err) => { handleSocketError(err, 'Consumer error') frameStream.req.end() - // doConnectionToMJPEGStream(fn) + // doConnectionToMJPEGStream(fn) }) stream.on('error', () => { - // handleSocketError(err, 'Stream error ') + // handleSocketError(err, 'Stream error ') frameStream.req.end() }) stream.socket.on('error', () => { - // handleSocketError(err, 'Websocket stream error ') + // handleSocketError(err, 'Websocket stream error ') frameStream.req.end() }) ws.on('close', async() => { - // @TODO handle close event - // stream.socket.onclose() + // @TODO handle close event + // stream.socket.onclose() if (!authed) { return } frameStream.req.end() const orientation = WdaClient.orientation - const stoppingSession = () => { - WdaClient.stopSession() + + const stoppingSession = async() => { + await WdaClient.stopSession() isConnectionAlive = false log.important('ws on close event') } + if (WdaClient.deviceType === 'Apple TV' || orientation === 'PORTRAIT') { return stoppingSession() } // #770: Reset rotation to Portrait when closing device // Ensure that rotation is done, then stop session - WdaClient.rotation({orientation: 'PORTRAIT'}) + + await WdaClient.rotation('PORTRAIT') await new Promise(r => setTimeout(r, 2000)) stoppingSession() }) - ws.on('error', function() { - // @TODO handle error event - // stream.socket.onclose() + ws.on('error', async() => { + // @TODO handle error event + // stream.socket.onclose() if (!authed) { return } - WdaClient.stopSession() + await WdaClient.stopSession() isConnectionAlive = false log.important('ws on error event') }) diff --git a/lib/units/ios-device/plugins/util/devices.json b/lib/units/ios-device/plugins/util/devices.json index 147990d12f..e80282a1a8 100644 --- a/lib/units/ios-device/plugins/util/devices.json +++ b/lib/units/ios-device/plugins/util/devices.json @@ -1,66 +1,120 @@ [ { - "full_name": "iPhone (Original/1st Gen/EDGE)", - "introduced": "January 9, 2007*", - "discontinued": "June 9, 2008**", - "model": "A1203 (EMC N/A)", - "device_id": "iPhone1,1", - "order": "MA712LL/A*", - "full_family": "iPhone (Original)" + "full_name": "iPad Air Wi-Fi/TD-LTE - China", + "introduced": "April 1, 2014", + "discontinued": "March 21, 2016**", + "model": "A1476 (EMC N/A)", + "device_id": "iPad4,3", + "order": "MD785CH/A*", + "full_family": "iPad Air (Wi-Fi + Cellular CN)" }, { - "full_name": "iPhone 3G", - "introduced": "June 9, 2008", - "discontinued": "June 7, 2010*", - "model": "A1241 (EMC 2347*)", - "device_id": "iPhone1,2", - "order": "MB046LL/A*", - "full_family": "iPhone 3G" + "full_name": "iPad mini 2 (Retina/2nd Gen, China)", + "introduced": "April 1, 2014", + "discontinued": "March 21, 2017*", + "model": "A1491 (EMC 2696*)", + "device_id": "iPad4,6", + "order": "MF247CH/A*", + "full_family": "iPad mini Retina (Wi-Fi + Cellular CN)" }, { - "full_name": "iPhone 3GS", - "introduced": "June 8, 2009*", - "discontinued": "September 12, 2012*", - "model": "A1303 (EMC 2315*)", - "device_id": "iPhone2,1", - "order": "MB715LL/A*", - "full_family": "iPhone 3GS" + "full_name": "iPhone SE", + "introduced": "April 15, 2020", + "discontinued": "March 8, 2022", + "model": "A2275/A2296/A2298 (EMC 3500*)", + "device_id": "iPhone12,8", + "order": "MX9A2LL/A*", + "full_family": "iPhone SE" }, { - "full_name": "iPhone 3G (China/No Wi-Fi)", - "introduced": "October 30, 2009*", - "discontinued": "August 9, 2010", - "model": "A1324 (EMC N/A)", - "device_id": "iPhone1,2*", - "order": "MC176CH/A", - "full_family": "iPhone 3G (China)" + "full_name": "Apple TV 4K (2nd Gen, 2021)", + "introduced": "April 20, 2021*", + "discontinued": "October 18, 2022", + "model": "A2169 (EMC 3111)", + "device_id": "AppleTV11,1", + "order": "MXGY2LL/A*", + "full_family": "4K (2nd Gen)" }, { - "full_name": "iPhone 3GS (China/No Wi-Fi)", - "introduced": "October 30, 2009*", - "discontinued": "August 9, 2010", - "model": "A1325 (EMC N/A)", - "device_id": "iPhone2,1*", - "order": "N/A", - "full_family": "iPhone 3GS (China)" + "full_name": "iPad Pro 12.9\" (Wi-Fi/Cell Global - 5th Gen)", + "introduced": "April 20, 2021*", + "discontinued": "October 18, 2022", + "model": "A2461 (EMC 3687)", + "device_id": "iPad13,10", + "order": "MHR53X/A*", + "full_family": "iPad Pro 12.9\" (5th Gen - Wi-Fi+C)" }, { - "full_name": "iPhone 4 (GSM)", - "introduced": "June 7, 2010*", - "discontinued": "October 4, 2011", - "model": "A1332 (EMC 380A/380B*)", - "device_id": "iPhone3,1", - "order": "MC318LL/A*", - "full_family": "iPhone 4" + "full_name": "iPad Pro 12.9\" (Wi-Fi/Cell China - 5th Gen)", + "introduced": "April 20, 2021*", + "discontinued": "October 18, 2022", + "model": "A2462 (EMC 3688)", + "device_id": "iPad13,11", + "order": "MHRG3CH/A*", + "full_family": "iPad Pro 12.9\" (5th Gen - Wi-Fi+C)" }, { - "full_name": "iPhone 4 (GSM, Revision A)", - "introduced": "October 4, 2011*", - "discontinued": "September 10, 2013*", - "model": "A1332 (EMC 380A/380B*)", - "device_id": "iPhone3,2", - "order": "MD126LL/A*", - "full_family": "iPhone 4" + "full_name": "iPad Pro 11\" (Wi-Fi Only - 3rd Gen)", + "introduced": "April 20, 2021*", + "discontinued": "October 18, 2022", + "model": "A2377 (EMC 3681)", + "device_id": "iPad13,4", + "order": "MHQT3LL/A*", + "full_family": "iPad Pro 11\" (3rd Gen - Wi-Fi)" + }, + { + "full_name": "iPad Pro 11\" (Wi-Fi/Cell US - 3rd Gen)", + "introduced": "April 20, 2021*", + "discontinued": "October 18, 2022", + "model": "A2301 (EMC 3682)", + "device_id": "iPad13,5", + "order": "MHMU3LL/A*", + "full_family": "iPad Pro 11\" (3rd Gen - Wi-Fi+C)" + }, + { + "full_name": "iPad Pro 11\" (Wi-Fi/Cell Global - 3rd Gen)", + "introduced": "April 20, 2021*", + "discontinued": "October 18, 2022", + "model": "A2459 (EMC 3683)", + "device_id": "iPad13,6", + "order": "MHW63X/A*", + "full_family": "iPad Pro 11\" (3rd Gen - Wi-Fi+C)" + }, + { + "full_name": "iPad Pro 11\" (Wi-Fi/Cell China - 3rd Gen)", + "introduced": "April 20, 2021*", + "discontinued": "October 18, 2022", + "model": "A2460 (EMC 3684)", + "device_id": "iPad13,7", + "order": "MHWH3CH/A*", + "full_family": "iPad Pro 11\" (3rd Gen - Wi-Fi+C)" + }, + { + "full_name": "iPad Pro 12.9\" (Wi-Fi Only - 5th Gen)", + "introduced": "April 20, 2021*", + "discontinued": "October 18, 2022", + "model": "A2378 (EMC 3689)", + "device_id": "iPad13,8", + "order": "MHNG3LL/A*", + "full_family": "iPad Pro 12.9\" (5th Gen - Wi-Fi)" + }, + { + "full_name": "iPad Pro 12.9\" (Wi-Fi/Cell US - 5th Gen)", + "introduced": "April 20, 2021*", + "discontinued": "October 18, 2022", + "model": "A2379 (EMC 3686)", + "device_id": "iPad13,9", + "order": "MHNT3LL/A*", + "full_family": "iPad Pro 12.9\" (5th Gen - Wi-Fi+C)" + }, + { + "full_name": "iPhone 16e", + "introduced": "February 28, 2025", + "discontinued": "N/A", + "model": "A3212/A3408/A3409/A3410 (EMC 8727*)", + "device_id": "iPhone17,5", + "order": "MD0E4LL/A*", + "full_family": "iPhone 16e" }, { "full_name": "iPhone 4 (CDMA/Verizon/Sprint)", @@ -71,15 +125,6 @@ "order": "MC676LL/A*", "full_family": "iPhone 4 CDMA" }, - { - "full_name": "iPhone 4S (4s*)", - "introduced": "October 4, 2011*", - "discontinued": "September 9, 2014*", - "model": "A1387 (EMC 2430)", - "device_id": "iPhone4,1", - "order": "MC918LL/A*", - "full_family": "iPhone 4S (4s)" - }, { "full_name": "iPhone 4S (GSM China/WAPI)", "introduced": "January 13, 2012*", @@ -90,4294 +135,1198 @@ "full_family": "iPhone 4S (GSM China)" }, { - "full_name": "iPhone 5 (GSM/LTE 4, 17/North America)", - "introduced": "September 12, 2012", - "discontinued": "April 12, 2013*", - "model": "A1428 (EMC N/A)", - "device_id": "iPhone5,1", - "order": "MD634LL/A*", - "full_family": "iPhone 5" + "full_name": "Apple HomePod 2nd Gen (Smart Speaker)", + "introduced": "January 18, 2023*", + "discontinued": "N/A", + "model": "A2825 (EMC 8208)", + "device_id": "AudioAccessory6,1", + "order": "MQJ73LL/A*", + "full_family": "HomePod 2nd Gen" }, { - "full_name": "iPhone 5 (CDMA/LTE, Sprint/Verizon/KDDI)", - "introduced": "September 12, 2012", - "discontinued": "September 10, 2013", - "model": "A1429 (EMC N/A)", - "device_id": "iPhone5,2", - "order": "MD656LL/A*", - "full_family": "iPhone 5" + "full_name": "iPad Wi-Fi", + "introduced": "January 27, 2010*", + "discontinued": "March 2, 2011", + "model": "A1219/A1337 (EMC 2311)", + "device_id": "iPad1,1", + "order": "MB292LL/A*", + "full_family": "iPad" }, { - "full_name": "iPhone 5 (GSM/LTE 1, 3, 5/International)", - "introduced": "September 12, 2012", - "discontinued": "September 10, 2013", - "model": "A1429 (EMC 2610*)", - "device_id": "iPhone5,1", - "order": "MD297B/A*", - "full_family": "iPhone 5" + "full_name": "Apple TV (3rd Generation, Early 2013)", + "introduced": "January 29, 2013*", + "discontinued": "October 3, 2016**", + "model": "A1469 (EMC 2633)", + "device_id": "AppleTV3,2", + "order": "MD199LL/A", + "full_family": "3rd Gen" }, { - "full_name": "iPhone 5 (CDMA China/UIM/WAPI)", - "introduced": "December 14, 2012*", - "discontinued": "September 10, 2013", - "model": "A1442 (EMC N/A)", - "device_id": "iPhone5,2", - "order": "ME039CH/A*", - "full_family": "iPhone 5 (CDMA China)" + "full_name": "Apple TV (Original/1st Gen)", + "introduced": "January 9, 2007*", + "discontinued": "September 1, 2010*", + "model": "A1218 (EMC 2123)", + "device_id": "AppleTV1,1", + "order": "MA711LL/A*", + "full_family": "Apple TV" }, { - "full_name": "iPhone 5 (GSM/LTE/AWS/North America)", - "introduced": "March 26, 2013*", - "discontinued": "September 10, 2013", - "model": "A1428 (EMC N/A)", - "device_id": "iPhone5,1", - "order": "ME486LL/A*", - "full_family": "iPhone 5 (AWS**)" + "full_name": "iPhone (Original/1st Gen/EDGE)", + "introduced": "January 9, 2007*", + "discontinued": "June 9, 2008**", + "model": "A1203 (EMC N/A)", + "device_id": "iPhone1,1", + "order": "MA712LL/A*", + "full_family": "iPhone (Original)" }, { - "full_name": "iPhone 5c (GSM/North America/A1532)", - "introduced": "September 10, 2013*", - "discontinued": "September 9, 2015", - "model": "A1532 (EMC 2644*)", - "device_id": "iPhone5,3", - "order": "ME505LL/A*", - "full_family": "iPhone 5c" + "full_name": "Apple HomePod (Smart Speaker)", + "introduced": "June 5, 2017*", + "discontinued": "March 12, 2021**", + "model": "A1639 (EMC N/A*)", + "device_id": "AudioAccessory1,1", + "order": "MQHV2LL/A*", + "full_family": "HomePod" }, { - "full_name": "iPhone 5c (CDMA/Verizon/A1532)", - "introduced": "September 10, 2013*", - "discontinued": "September 9, 2015", - "model": "A1532 (EMC 2644*)", - "device_id": "iPhone5,3", - "order": "ME553LL/A*", - "full_family": "iPhone 5c" + "full_name": "iPad Pro 12.9\" (Wi-Fi Only - 2nd Gen)", + "introduced": "June 5, 2017*", + "discontinued": "October 30, 2018", + "model": "A1670 (EMC 3149*)", + "device_id": "iPad7,1", + "order": "MQDC2LL/A*", + "full_family": "iPad Pro 12.9\" (2nd Gen - Wi-Fi)" }, { - "full_name": "iPhone 5c (CDMA/China Telecom/A1532)", - "introduced": "September 10, 2013*", - "discontinued": "September 9, 2015", - "model": "A1532 (EMC 2644*)", - "device_id": "iPhone5,3", - "order": "MF364CH/A*", - "full_family": "iPhone 5c" + "full_name": "iPad Pro 12.9\" (Wi-Fi/Cell - 2nd Gen)", + "introduced": "June 5, 2017*", + "discontinued": "October 30, 2018", + "model": "A1671 (EMC 3150*)", + "device_id": "iPad7,2", + "order": "MQEE2LL/A*", + "full_family": "iPad Pro 12.9\" (2nd Gen - Wi-Fi+Cell)" }, { - "full_name": "iPhone 5c (CDMA/US/Japan/A1456)", - "introduced": "September 10, 2013*", - "discontinued": "September 9, 2015", - "model": "A1456 (EMC 2644*)", - "device_id": "iPhone5,3", - "order": "ME565LL/A*", - "full_family": "iPhone 5c" + "full_name": "iPad Pro 10.5\" (Wi-Fi Only)", + "introduced": "June 5, 2017*", + "discontinued": "March 18, 2019", + "model": "A1701 (EMC 3140*)", + "device_id": "iPad7,3", + "order": "MQDW2LL/A*", + "full_family": "iPad Pro 10.5\" (Wi-Fi)" }, { - "full_name": "iPhone 5c (UK/Europe/Middle East/A1507)", - "introduced": "September 10, 2013*", - "discontinued": "September 9, 2015", - "model": "A1507 (EMC 2694*)", - "device_id": "iPhone5,4", - "order": "ME499B/A*", - "full_family": "iPhone 5c" + "full_name": "iPad Pro 10.5\" (Wi-Fi/Cellular)", + "introduced": "June 5, 2017*", + "discontinued": "March 18, 2019", + "model": "A1709 (EMC 3141*)", + "device_id": "iPad7,4", + "order": "MQF02LL/A*", + "full_family": "iPad Pro 10.5\" (Wi-Fi + Cellular)" }, { - "full_name": "iPhone 5c (China Unicom/A1526)", - "introduced": "September 10, 2013*", - "discontinued": "September 9, 2015", - "model": "A1526 (EMC N/A)", - "device_id": "iPhone5,4", - "order": "ME679CH/A*", - "full_family": "iPhone 5c" + "full_name": "Apple Vision Pro (Original)", + "introduced": "June 5, 2023*", + "discontinued": "N/A", + "model": "A2117 (EMC 4075*)", + "device_id": "RealityDevice14,1", + "order": "MQL83LL/A*", + "full_family": "Vision Pro" }, { - "full_name": "iPhone 5c (Asia Pacific/A1529)", - "introduced": "September 10, 2013*", - "discontinued": "September 9, 2015", - "model": "A1529 (EMC 2694*)", - "device_id": "iPhone5,4", - "order": "MF321X/A*", - "full_family": "iPhone 5c" + "full_name": "iPhone 4 (GSM)", + "introduced": "June 7, 2010*", + "discontinued": "October 4, 2011", + "model": "A1332 (EMC 380A/380B*)", + "device_id": "iPhone3,1", + "order": "MC318LL/A*", + "full_family": "iPhone 4" }, { - "full_name": "iPhone 5c (China Mobile/A1516)", - "introduced": "December 22, 2013*", - "discontinued": "September 9, 2015", - "model": "A1516 (EMC N/A*)", - "device_id": "iPhone5,4", - "order": "N/A*", - "full_family": "iPhone 5c" + "full_name": "iPhone 3GS", + "introduced": "June 8, 2009*", + "discontinued": "September 12, 2012*", + "model": "A1303 (EMC 2315*)", + "device_id": "iPhone2,1", + "order": "MB715LL/A*", + "full_family": "iPhone 3GS" }, { - "full_name": "iPhone 5s (GSM/North America/A1533)", - "introduced": "September 10, 2013*", - "discontinued": "March 21, 2016**", - "model": "A1533 (EMC 2642*)", - "device_id": "iPhone6,1", - "order": "ME305LL/A*", - "full_family": "iPhone 5s" + "full_name": "iPhone 3G", + "introduced": "June 9, 2008", + "discontinued": "June 7, 2010*", + "model": "A1241 (EMC 2347*)", + "device_id": "iPhone1,2", + "order": "MB046LL/A*", + "full_family": "iPhone 3G" }, { - "full_name": "iPhone 5s (CDMA/Verizon/A1533)", - "introduced": "September 10, 2013*", - "discontinued": "March 21, 2016**", - "model": "A1533 (EMC 2642*)", - "device_id": "iPhone6,1", - "order": "ME341LL/A*", - "full_family": "iPhone 5s" + "full_name": "iPad Air 11\" (M3, 2025 Wi‑Fi)", + "introduced": "March 12, 2025", + "discontinued": "N/A", + "model": "A3266 (EMC 8776*)", + "device_id": "iPad15,3", + "order": "N/A*", + "full_family": "iPad Air 11\" (M3, 2025)" }, { - "full_name": "iPhone 5s (CDMA/China Telecom/A1533)", - "introduced": "September 10, 2013*", - "discontinued": "March 21, 2016**", - "model": "A1533 (EMC 2642*)", - "device_id": "iPhone6,1", - "order": "MF381CH/A*", - "full_family": "iPhone 5s" + "full_name": "iPad Air 11\" (M3, 2025 Wi‑Fi + Cellular)", + "introduced": "March 12, 2025", + "discontinued": "N/A", + "model": "A3267/A3270 (EMC 8777*)", + "device_id": "iPad15,4", + "order": "N/A*", + "full_family": "iPad Air 11\" (M3, 2025)" }, { - "full_name": "iPhone 5s (CDMA/US/Japan/A1453)", - "introduced": "September 10, 2013*", - "discontinued": "March 21, 2016**", - "model": "A1453 (EMC 2642*)", - "device_id": "iPhone6,1", - "order": "ME350LL/A*", - "full_family": "iPhone 5s" + "full_name": "iPad Air 13\" (M3, 2025 Wi‑Fi)", + "introduced": "March 12, 2025", + "discontinued": "N/A", + "model": "A3268 (EMC 8778*)", + "device_id": "iPad15,5", + "order": "N/A*", + "full_family": "iPad Air 13\" (M3, 2025)" }, { - "full_name": "iPhone 5s (UK/Europe/Middle East/A1457)", - "introduced": "September 10, 2013*", - "discontinued": "March 21, 2016**", - "model": "A1457 (EMC 2643*)", - "device_id": "iPhone6,2", - "order": "ME432B/A*", - "full_family": "iPhone 5s" + "full_name": "iPad Air 13\" (M3, 2025 Wi‑Fi + Cellular)", + "introduced": "March 12, 2025", + "discontinued": "N/A", + "model": "A3269/A3271 (EMC 8779*)", + "device_id": "iPad15,6", + "order": "N/A*", + "full_family": "iPad Air 13\" (M3, 2025)" }, { - "full_name": "iPhone 5s (China Unicom/A1528)", - "introduced": "September 10, 2013*", - "discontinued": "March 21, 2016**", - "model": "A1528 (EMC N/A)", - "device_id": "iPhone6,2", - "order": "ME450CH/A*", - "full_family": "iPhone 5s" + "full_name": "iPad mini 5th Gen (Wi-Fi Only)", + "introduced": "March 18, 2019", + "discontinued": "September 14, 2021", + "model": "A2133 (EMC 3313*)", + "device_id": "iPad11,1", + "order": "MUQX2LL/A*", + "full_family": "iPad mini 5th Gen (Wi-Fi)" }, { - "full_name": "iPhone 5s (Asia Pacific/A1530)", - "introduced": "September 10, 2013*", - "discontinued": "March 21, 2016**", - "model": "A1530 (EMC 2643*)", - "device_id": "iPhone6,2", - "order": "MF352X/A*", - "full_family": "iPhone 5s" + "full_name": "iPad mini 5th Gen", + "introduced": "March 18, 2019", + "discontinued": "September 14, 2021", + "model": "A2126/A2124/A2125 (EMC 3314*)", + "device_id": "iPad11,2", + "order": "MUXG2LL/A*", + "full_family": "iPad mini 5th Gen" }, { - "full_name": "iPhone 5s (China Mobile/A1518)", - "introduced": "December 22, 2013*", - "discontinued": "March 21, 2016**", - "model": "A1518 (EMC N/A*)", - "device_id": "iPhone6,2", - "order": "MF341CH/A*", - "full_family": "iPhone 5s" + "full_name": "iPad Air 3rd Gen (Wi-Fi Only)", + "introduced": "March 18, 2019", + "discontinued": "September 15, 2020", + "model": "A2152 (EMC 3315*)", + "device_id": "iPad11,3", + "order": "MUUK2LL/A*", + "full_family": "iPad Air 3rd Gen (Wi-Fi)" }, { - "full_name": "iPhone 6 (GSM/North America/A1549)", - "introduced": "September 9, 2014*", - "discontinued": "September 12, 2018*", - "model": "A1549 (EMC 2816*)", - "device_id": "iPhone7,2", - "order": "MG4P2LL/A*", - "full_family": "iPhone 6" + "full_name": "iPad Air 3rd Gen", + "introduced": "March 18, 2019", + "discontinued": "September 15, 2020", + "model": "A2153/A2123/A2154 (EMC 3316*)", + "device_id": "iPad11,4", + "order": "MV162LL/A*", + "full_family": "iPad Air 3rd Gen" }, { - "full_name": "iPhone 6 (CDMA/Verizon/A1549)", - "introduced": "September 9, 2014*", - "discontinued": "September 7, 2016*", - "model": "A1549 (EMC 2816*)", - "device_id": "iPhone7,2", - "order": "MG5X2LL/A*", - "full_family": "iPhone 6" + "full_name": "iPad Pro 11\"", + "introduced": "March 18, 2020", + "discontinued": "April 20, 2021", + "model": "A2068/A2230/A2231 (EMC 3350)", + "device_id": "iPad8,10", + "order": "MY342LL/A*", + "full_family": "iPad Pro 11\"" }, { - "full_name": "iPhone 6 (Global/Sprint/A1586)", - "introduced": "September 9, 2014*", - "discontinued": "September 12, 2018*", - "model": "A1586 (EMC 2816*)", - "device_id": "iPhone7,2", - "order": "MG6A2LL/A*", - "full_family": "iPhone 6" + "full_name": "iPad Pro 12.9\" (Wi-Fi Only - 4th Gen)", + "introduced": "March 18, 2020", + "discontinued": "April 20, 2021", + "model": "A2229 (EMC 3353)", + "device_id": "iPad8,11", + "order": "MY2J2LL/A*", + "full_family": "iPad Pro 12.9\" (4th Gen - Wi-Fi)" }, { - "full_name": "iPhone 6 (China Mobile/A1589)", - "introduced": "September 9, 2014*", - "discontinued": "September 7, 2016*", - "model": "A1589 (EMC 2816*)", - "device_id": "iPhone7,2", - "order": "N/A*", - "full_family": "iPhone 6" + "full_name": "iPad Pro 12.9\"", + "introduced": "March 18, 2020", + "discontinued": "April 20, 2021", + "model": "A2069/A2232/A2233 (EMC 3354)", + "device_id": "iPad8,12", + "order": "MY3K2LL/A*", + "full_family": "iPad Pro 12.9\"" }, { - "full_name": "iPhone 6 Plus (GSM/North America/A1522)", - "introduced": "September 9, 2014*", - "discontinued": "September 7, 2016*", - "model": "A1522 (EMC 2817*)", - "device_id": "iPhone7,1", - "order": "MGAM2LL/A*", - "full_family": "iPhone 6 Plus" + "full_name": "iPad Pro 11\" (Wi-Fi Only - 2nd Gen)", + "introduced": "March 18, 2020", + "discontinued": "April 20, 2021", + "model": "A2228 (EMC 3349)", + "device_id": "iPad8,9", + "order": "MY252LL/A*", + "full_family": "iPad Pro 11\" (2nd Gen - Wi-Fi)" }, { - "full_name": "iPhone 6 Plus (CDMA/Verizon/A1522)", - "introduced": "September 9, 2014*", - "discontinued": "September 7, 2016*", - "model": "A1522 (EMC 2817*)", - "device_id": "iPhone7,1", - "order": "MGCL2LL/A*", - "full_family": "iPhone 6 Plus" + "full_name": "iPad 2 (Wi-Fi Only)", + "introduced": "March 2, 2011*", + "discontinued": "March 18, 2014**", + "model": "A1395 (EMC 2415)", + "device_id": "iPad2,1", + "order": "MC769LL/A*", + "full_family": "iPad 2" }, { - "full_name": "iPhone 6 Plus (Global/Sprint/A1524)", - "introduced": "September 9, 2014*", - "discontinued": "September 7, 2016*", - "model": "A1524 (EMC 2817*)", - "device_id": "iPhone7,1", - "order": "MGCW2LL/A*", - "full_family": "iPhone 6 Plus" + "full_name": "iPad 2 (Wi-Fi/GSM/GPS)", + "introduced": "March 2, 2011*", + "discontinued": "March 18, 2014**", + "model": "A1396 (EMC 2416)", + "device_id": "iPad2,2", + "order": "MC773LL/A*", + "full_family": "iPad 2 3G (AT&T)" }, { - "full_name": "iPhone 6 Plus (China Mobile/A1593)", - "introduced": "September 9, 2014*", - "discontinued": "September 7, 2016*", - "model": "A1593 (EMC 2817*)", - "device_id": "iPhone7,1", - "order": "N/A*", - "full_family": "iPhone 6 Plus" + "full_name": "iPad 2 (Wi-Fi/CDMA/GPS)", + "introduced": "March 2, 2011*", + "discontinued": "March 18, 2014**", + "model": "A1397 (EMC 2424)", + "device_id": "iPad2,3", + "order": "MC755LL/A*", + "full_family": "iPad 2 3G (Verizon)" }, { - "full_name": "iPhone 6s (AT&T/SIM Free/A1633)", - "introduced": "September 9, 2015*", - "discontinued": "September 12, 2018*", - "model": "A1633 (EMC 2946*)", - "device_id": "iPhone8,1", - "order": "MKQ62LL/A*", - "full_family": "iPhone 6s" + "full_name": "iPad Pro 9.7\" (Wi-Fi Only)", + "introduced": "March 21, 2016*", + "discontinued": "June 5, 2017", + "model": "A1673 (EMC 2976)", + "device_id": "iPad6,3", + "order": "MLMP2LL/A*", + "full_family": "iPad Pro 9.7\" Wi-Fi" }, { - "full_name": "iPhone 6s (Global/A1688)", - "introduced": "September 9, 2015*", - "discontinued": "September 12, 2018*", - "model": "A1688 (EMC 2946*)", - "device_id": "iPhone8,1", - "order": "MKT82LL/A*", - "full_family": "iPhone 6s" + "full_name": "iPad Pro 9.7\" (Wi-Fi/Cellular)", + "introduced": "March 21, 2016*", + "discontinued": "June 5, 2017", + "model": "A1674 (EMC 2977)", + "device_id": "iPad6,4", + "order": "MLPX2LL/A*", + "full_family": "iPad Pro 9.7\" Wi-Fi + Cellular" }, { - "full_name": "iPhone 6s (Mainland China/A1700)", - "introduced": "September 9, 2015*", + "full_name": "iPhone SE", + "introduced": "March 21, 2016*", "discontinued": "September 12, 2018*", - "model": "A1700 (EMC 2946*)", - "device_id": "iPhone8,1", - "order": "ML7D2CH/A*", - "full_family": "iPhone 6s" + "model": "A1662/A1723/A1724 (EMC 3042*)", + "device_id": "iPhone8,4", + "order": "MLLX2LL/A*", + "full_family": "iPhone SE" }, { - "full_name": "iPhone 6s (China Mobile/A1691)", - "introduced": "September 9, 2015*", - "discontinued": "September 12, 2018*", - "model": "A1691 (EMC 2946*)", - "device_id": "iPhone8,1", - "order": "N/A*", - "full_family": "iPhone 6s" + "full_name": "iPad 9.7\" 5th Gen (Wi-Fi Only)", + "introduced": "March 21, 2017*", + "discontinued": "March 27, 2018", + "model": "A1822 (EMC 3017*)", + "device_id": "iPad6,11", + "order": "MP2G2LL/A*", + "full_family": "iPad 5th Gen (Wi-Fi)" }, { - "full_name": "iPhone 6s Plus (AT&T/SIM Free/A1634)", - "introduced": "September 9, 2015*", - "discontinued": "September 12, 2018*", - "model": "A1634 (EMC 2944*)", - "device_id": "iPhone8,2", - "order": "MKTM2LL/A*", - "full_family": "iPhone 6s Plus" + "full_name": "iPad 9.7\" 5th Gen (Wi-Fi/Cellular)", + "introduced": "March 21, 2017*", + "discontinued": "March 27, 2018", + "model": "A1823 (EMC 3108*)", + "device_id": "iPad6,12", + "order": "MP252LL/A*", + "full_family": "iPad 5th Gen (Wi-Fi + Cellular)" }, { - "full_name": "iPhone 6s Plus (Global/A1687)", - "introduced": "September 9, 2015*", - "discontinued": "September 12, 2018*", - "model": "A1687 (EMC 2944*)", - "device_id": "iPhone8,2", - "order": "MKVP2LL/A*", - "full_family": "iPhone 6s Plus" + "full_name": "iPad 2 (Wi-Fi Only, iPad2,4)", + "introduced": "March 25, 2012*", + "discontinued": "March 18, 2014", + "model": "A1395 (EMC 2560)", + "device_id": "iPad2,4", + "order": "MC769LL/A*", + "full_family": "iPad 2" }, { - "full_name": "iPhone 6s Plus (Mainland China/A1699)", - "introduced": "September 9, 2015*", - "discontinued": "September 12, 2018*", - "model": "A1699 (EMC 2944*)", - "device_id": "iPhone8,2", - "order": "ML6C2CH/A*", - "full_family": "iPhone 6s Plus" - }, - { - "full_name": "iPhone 6s Plus (China Mobile/A1690)", - "introduced": "September 9, 2015*", - "discontinued": "September 12, 2018*", - "model": "A1690 (EMC 2944*)", - "device_id": "iPhone8,2", - "order": "N/A*", - "full_family": "iPhone 6s Plus" - }, - { - "full_name": "iPhone SE (United States/A1662)", - "introduced": "March 21, 2016*", - "discontinued": "September 12, 2018*", - "model": "A1662 (EMC 3042*)", - "device_id": "iPhone8,4", - "order": "MLLX2LL/A*", - "full_family": "iPhone SE" - }, - { - "full_name": "iPhone SE (Global/Sprint/A1723)", - "introduced": "March 21, 2016*", - "discontinued": "September 12, 2018*", - "model": "A1723 (EMC 3042*)", - "device_id": "iPhone8,4", - "order": "MLM32LL/A*", - "full_family": "iPhone SE" - }, - { - "full_name": "iPhone SE (China Mobile/A1724)", - "introduced": "March 21, 2016*", - "discontinued": "September 12, 2018*", - "model": "A1724 (EMC 3042*)", - "device_id": "iPhone8,4", - "order": "MLLP2CH/A*", - "full_family": "iPhone SE" - }, - { - "full_name": "iPhone 7 (Verizon/Sprint/China/A1660)", - "introduced": "September 7, 2016*", - "discontinued": "September 10, 2019", - "model": "A1660 (EMC 3091*)", - "device_id": "iPhone9,1", - "order": "MNAD2LL/A*", - "full_family": "iPhone 7" - }, - { - "full_name": "iPhone 7 (AT&T/T-Mobile/Global/A1778)", - "introduced": "September 7, 2016*", - "discontinued": "September 10, 2019", - "model": "A1778 (EMC 3091*)", - "device_id": "iPhone9,3", - "order": "MN9E2LL/A*", - "full_family": "iPhone 7" - }, - { - "full_name": "iPhone 7 (Japan/A1779)", - "introduced": "September 7, 2016*", - "discontinued": "September 10, 2019", - "model": "A1779 (EMC 3091*)", - "device_id": "iPhone9,1", - "order": "MNCF2J/A*", - "full_family": "iPhone 7" - }, - { - "full_name": "iPhone 7 (China Mobile/A1780)", - "introduced": "September 7, 2016*", - "discontinued": "September 10, 2019", - "model": "A1780 (EMC 3091*)", - "device_id": "iPhone9,1", - "order": "N/A*", - "full_family": "iPhone 7" - }, - { - "full_name": "iPhone 7 Plus (Verizon/Sprint/China/A1661)", - "introduced": "September 7, 2016*", - "discontinued": "September 10, 2019", - "model": "A1661 (EMC 3092*)", - "device_id": "iPhone9,2", - "order": "MNR22LL/A*", - "full_family": "iPhone 7 Plus" - }, - { - "full_name": "iPhone 7 Plus (AT&T/T-Mobile/Global/A1784)", - "introduced": "September 7, 2016*", - "discontinued": "September 10, 2019", - "model": "A1784 (EMC 3092*)", - "device_id": "iPhone9,4", - "order": "MNQT2LL/A*", - "full_family": "iPhone 7 Plus" - }, - { - "full_name": "iPhone 7 Plus (Japan/A1785)", - "introduced": "September 7, 2016*", - "discontinued": "September 10, 2019", - "model": "A1785 (EMC 3092*)", - "device_id": "iPhone9,2", - "order": "MNRA2J/A*", - "full_family": "iPhone 7 Plus" - }, - { - "full_name": "iPhone 7 Plus (China Mobile/A1786)", - "introduced": "September 7, 2016*", - "discontinued": "September 10, 2019", - "model": "A1786 (EMC 3092*)", - "device_id": "iPhone9,2", - "order": "N/A*", - "full_family": "iPhone 7 Plus" - }, - { - "full_name": "iPhone 8 (Verizon/Sprint/China/A1863)", - "introduced": "September 12, 2017*", - "discontinued": "April 15, 2020", - "model": "A1863* (EMC 3159*)", - "device_id": "iPhone10,1", - "order": "MQ742LL/A*", - "full_family": "iPhone 8" - }, - { - "full_name": "iPhone 8 (AT&T/T-Mobile/Global/A1905)", - "introduced": "September 12, 2017*", - "discontinued": "April 15, 2020", - "model": "A1905* (EMC 3172*)", - "device_id": "iPhone10,4", - "order": "MQ6X2LL/A*", - "full_family": "iPhone 8" - }, - { - "full_name": "iPhone 8 (Japan/A1906)", - "introduced": "September 12, 2017*", - "discontinued": "April 15, 2020", - "model": "A1906* (EMC 3171*)", - "device_id": "iPhone10,1", - "order": "MQ7A2J/A*", - "full_family": "iPhone 8" - }, - { - "full_name": "iPhone 8 (China Mobile/A1907)", - "introduced": "September 12, 2017*", - "discontinued": "April 15, 2020", - "model": "A1907* (EMC N/A*)", - "device_id": "iPhone10,1", - "order": "N/A*", - "full_family": "iPhone 8" - }, - { - "full_name": "iPhone 8 Plus (Verizon/Sprint/China/A1864)", - "introduced": "September 12, 2017*", - "discontinued": "April 15, 2020", - "model": "A1864* (EMC 3160*)", - "device_id": "iPhone10,2", - "order": "MQ982LL/A*", - "full_family": "iPhone 8 Plus" - }, - { - "full_name": "iPhone 8 Plus (AT&T/T-Mobile/Global/A1897)", - "introduced": "September 12, 2017*", - "discontinued": "April 15, 2020", - "model": "A1897* (EMC 3174*)", - "device_id": "iPhone10,5", - "order": "MQ8V2LL/A*", - "full_family": "iPhone 8 Plus" - }, - { - "full_name": "iPhone 8 Plus (Japan/A1898)", - "introduced": "September 12, 2017*", - "discontinued": "April 15, 2020", - "model": "A1898* (EMC 3173*)", - "device_id": "iPhone10,2", - "order": "MQ9M2J/A*", - "full_family": "iPhone 8 Plus" - }, - { - "full_name": "iPhone 8 Plus (China Mobile/A1899)", - "introduced": "September 12, 2017*", - "discontinued": "April 15, 2020", - "model": "A1899* (EMC N/A*)", - "device_id": "iPhone10,2", - "order": "N/A*", - "full_family": "iPhone 8 Plus" - }, - { - "full_name": "iPhone X (Verizon/Sprint/China/A1865)", - "introduced": "September 12, 2017*", - "discontinued": "September 12, 2018", - "model": "A1865* (EMC 3161*)", - "device_id": "iPhone10,3", - "order": "MQCK2LL/A*", - "full_family": "iPhone X" - }, - { - "full_name": "iPhone X (AT&T/T-Mobile/Global/A1901)", - "introduced": "September 12, 2017*", - "discontinued": "September 12, 2018", - "model": "A1901* (EMC 3175*)", - "device_id": "iPhone10,6", - "order": "MQAJ2LL/A*", - "full_family": "iPhone X" - }, - { - "full_name": "iPhone X (Japan/A1902)", - "introduced": "September 12, 2017*", - "discontinued": "September 12, 2018", - "model": "A1902* (EMC 3176*)", - "device_id": "iPhone10,3", - "order": "MQAX2J/A*", - "full_family": "iPhone X" - }, - { - "full_name": "iPhone Xs (US/Canada/Hong Kong/A1920)", - "introduced": "September 12, 2018*", - "discontinued": "September 10, 2019", - "model": "A1920* (EMC 3218*)", - "device_id": "iPhone11,2", - "order": "MT952LL/A*", - "full_family": "iPhone Xs" - }, - { - "full_name": "iPhone Xs (Global/A2097)", - "introduced": "September 12, 2018*", - "discontinued": "September 10, 2019", - "model": "A2097* (EMC 3232*)", - "device_id": "iPhone11,2", - "order": "MT9F2B/A*", - "full_family": "iPhone Xs" - }, - { - "full_name": "iPhone Xs (Japan/A2098)", - "introduced": "September 12, 2018*", - "discontinued": "September 10, 2019", - "model": "A2098* (EMC 3233*)", - "device_id": "iPhone11,2", - "order": "MTAX2J/A*", - "full_family": "iPhone Xs" - }, - { - "full_name": "iPhone Xs (China/A2100)", - "introduced": "September 12, 2018*", - "discontinued": "September 10, 2019", - "model": "A2100* (EMC 3262*)", - "device_id": "iPhone11,2", - "order": "MT9Q2CH/A*", - "full_family": "iPhone Xs" - }, - { - "full_name": "iPhone Xs Max (US/Canada/A1921)", - "introduced": "September 12, 2018*", - "discontinued": "September 10, 2019", - "model": "A1921* (EMC 3219*)", - "device_id": "iPhone11,6", - "order": "MT5A2LL/A*", - "full_family": "iPhone Xs Max" - }, - { - "full_name": "iPhone Xs Max (Global/A2101)", - "introduced": "September 12, 2018*", - "discontinued": "September 10, 2019", - "model": "A2101* (EMC 3234*)", - "device_id": "iPhone11,6", - "order": "MT512B/A*", - "full_family": "iPhone Xs Max" - }, - { - "full_name": "iPhone Xs Max (Japan/A2102)", - "introduced": "September 12, 2018*", - "discontinued": "September 10, 2019", - "model": "A2102* (EMC 3235*)", - "device_id": "iPhone11,6", - "order": "MT6R2J/A*", - "full_family": "iPhone Xs Max" - }, - { - "full_name": "iPhone Xs Max (China/Hong Kong/A2104)", - "introduced": "September 12, 2018*", - "discontinued": "September 10, 2019", - "model": "A2104* (EMC 3261*)", - "device_id": "iPhone11,6", - "order": "MT722CH/A*", - "full_family": "iPhone Xs Max" - }, - { - "full_name": "iPhone XR (US/Canada/A1984)", - "introduced": "September 12, 2018*", - "discontinued": "September 14, 2021", - "model": "A1984* (EMC 3220*)", - "device_id": "iPhone11,8", - "order": "MT3L2LL/A*", - "full_family": "iPhone XR" - }, - { - "full_name": "iPhone XR (Global/A2105)", - "introduced": "September 12, 2018*", - "discontinued": "September 14, 2021", - "model": "A2105* (EMC 3227*)", - "device_id": "iPhone11,8", - "order": "MRY52B/A*", - "full_family": "iPhone XR" - }, - { - "full_name": "iPhone XR (Japan/A2106)", - "introduced": "September 12, 2018*", - "discontinued": "September 14, 2021", - "model": "A2106* (EMC 3238*)", - "device_id": "iPhone11,8", - "order": "MT032J/A*", - "full_family": "iPhone XR" - }, - { - "full_name": "iPhone XR (China/Hong Kong/A2108)", - "introduced": "September 12, 2018*", - "discontinued": "September 14, 2021", - "model": "A2108* (EMC 3260*)", - "device_id": "iPhone11,8", - "order": "MT132CH/A*", - "full_family": "iPhone XR" - }, - { - "full_name": "iPhone 11 (US/Canada/A2111)", - "introduced": "September 10, 2019", - "discontinued": "September 7, 2022", - "model": "A2111* (EMC 3309*)", - "device_id": "iPhone12,1", - "order": "MWHU2LL/A*", - "full_family": "iPhone 11" - }, - { - "full_name": "iPhone 11 (Global/A2221)", - "introduced": "September 10, 2019", - "discontinued": "September 7, 2022", - "model": "A2221* (EMC 3304*)", - "device_id": "iPhone12,1", - "order": "MWLU2B/A*", - "full_family": "iPhone 11" - }, - { - "full_name": "iPhone 11 (China/Hong Kong/A2223)", - "introduced": "September 10, 2019", - "discontinued": "September 7, 2022", - "model": "A2223* (EMC 3309*)", - "device_id": "iPhone12,1", - "order": "MWN12ZA/A*", - "full_family": "iPhone 11" - }, - { - "full_name": "iPhone 11 Pro (US/Canada/A2160)", - "introduced": "September 10, 2019", - "discontinued": "October 13, 2020", - "model": "A2160* (EMC 3305*)", - "device_id": "iPhone12,3", - "order": "MW9E2LL/A*", - "full_family": "iPhone 11 Pro" - }, - { - "full_name": "iPhone 11 Pro (Global/A2215)", - "introduced": "September 10, 2019", - "discontinued": "October 13, 2020", - "model": "A2215* (EMC 3307*)", - "device_id": "iPhone12,3", - "order": "MWC52B/A*", - "full_family": "iPhone 11 Pro" - }, - { - "full_name": "iPhone 11 Pro (China/Hong Kong/A2217)", - "introduced": "September 10, 2019", - "discontinued": "October 13, 2020", - "model": "A2217* (EMC 3305*)", - "device_id": "iPhone12,3", - "order": "MWDC2ZA/A*", - "full_family": "iPhone 11 Pro" - }, - { - "full_name": "iPhone 11 Pro Max (US/CA/A2161)", - "introduced": "September 10, 2019", - "discontinued": "October 13, 2020", - "model": "A2161* (EMC 3306*)", - "device_id": "iPhone12,5", - "order": "MWFC2LL/A*", - "full_family": "iPhone 11 Pro Max" - }, - { - "full_name": "iPhone 11 Pro Max (Global/A2218)", - "introduced": "September 10, 2019", - "discontinued": "October 13, 2020", - "model": "A2218* (EMC 3308*)", - "device_id": "iPhone12,5", - "order": "MWHG2B/A*", - "full_family": "iPhone 11 Pro Max" - }, - { - "full_name": "iPhone 11 Pro Max (CN/HK/A2220)", - "introduced": "September 10, 2019", - "discontinued": "October 13, 2020", - "model": "A2220* (EMC 3306*)", - "device_id": "iPhone12,5", - "order": "MWEX2ZA/A*", - "full_family": "iPhone 11 Pro Max" - }, - { - "full_name": "iPhone SE (2 - 4.7\" 2020 US/CA A2275)", - "introduced": "April 15, 2020", - "discontinued": "March 8, 2022", - "model": "A2275* (EMC 3500*)", - "device_id": "iPhone12,8", - "order": "MX9A2LL/A*", - "full_family": "iPhone SE (2nd Gen)" - }, - { - "full_name": "iPhone SE (2 - 4.7\" 2020 Global A2296)", - "introduced": "April 15, 2020", - "discontinued": "March 8, 2022", - "model": "A2296* (EMC 3500*)", - "device_id": "iPhone12,8", - "order": "MX9T2B/A*", - "full_family": "iPhone SE (2nd Gen)" - }, - { - "full_name": "iPhone SE (2 - 4.7\" 2020 China A2298)", - "introduced": "April 15, 2020", - "discontinued": "March 8, 2022", - "model": "A2298* (EMC 3500*)", - "device_id": "iPhone12,8", - "order": "MXAN2CH/A*", - "full_family": "iPhone SE (2nd Gen)" - }, - { - "full_name": "iPhone 12 mini (US/A2176)", - "introduced": "October 13, 2020*", - "discontinued": "September 7, 2022", - "model": "A2176* (EMC 3539*)", - "device_id": "iPhone13,1", - "order": "MG633LL/A*", - "full_family": "iPhone 12 mini" - }, - { - "full_name": "iPhone 12 mini (Canada/Japan/A2398)", - "introduced": "October 13, 2020*", - "discontinued": "September 7, 2022", - "model": "A2398* (EMC 3540*)", - "device_id": "iPhone13,1", - "order": "MGAP3VC/A*", - "full_family": "iPhone 12 mini" - }, - { - "full_name": "iPhone 12 mini (Global/A2399)", - "introduced": "October 13, 2020*", - "discontinued": "September 7, 2022", - "model": "A2399* (EMC 3541*)", - "device_id": "iPhone13,1", - "order": "MGE13B/A*", - "full_family": "iPhone 12 mini" - }, - { - "full_name": "iPhone 12 mini (China/A2400)", - "introduced": "October 13, 2020*", - "discontinued": "September 7, 2022", - "model": "A2400* (EMC 3541*)", - "device_id": "iPhone13,1", - "order": "MG823CH/A*", - "full_family": "iPhone 12 mini" - }, - { - "full_name": "iPhone 12 (US/A2172)", - "introduced": "October 13, 2020*", - "discontinued": "September 12, 2023", - "model": "A2172* (EMC 3542*)", - "device_id": "iPhone13,2", - "order": "MGEK3LL/A*", - "full_family": "iPhone 12" - }, - { - "full_name": "iPhone 12 (Canada/Japan/A2402)", - "introduced": "October 13, 2020*", - "discontinued": "September 12, 2023", - "model": "A2402* (EMC 3543*)", - "device_id": "iPhone13,2", - "order": "MGHR3VC/A*", - "full_family": "iPhone 12" - }, - { - "full_name": "iPhone 12 (Global/A2403)", - "introduced": "October 13, 2020*", - "discontinued": "September 12, 2023", - "model": "A2403* (EMC 3544*)", - "device_id": "iPhone13,2", - "order": "MGJ83B/A*", - "full_family": "iPhone 12" - }, - { - "full_name": "iPhone 12 (China/A2404)", - "introduced": "October 13, 2020*", - "discontinued": "September 12, 2023", - "model": "A2404* (EMC 3544*)", - "device_id": "iPhone13,2", - "order": "MGGQ3CH/A*", - "full_family": "iPhone 12" - }, - { - "full_name": "iPhone 12 Pro (US/A2341)", - "introduced": "October 13, 2020*", - "discontinued": "September 14, 2021", - "model": "A2341* (EMC 3545*)", - "device_id": "iPhone13,3", - "order": "MGJQ3LL/A*", - "full_family": "iPhone 12 Pro" - }, - { - "full_name": "iPhone 12 Pro (Canada/Japan/A2406)", - "introduced": "October 13, 2020*", - "discontinued": "September 14, 2021", - "model": "A2406* (EMC 3546*)", - "device_id": "iPhone13,3", - "order": "MGM83VC/A*", - "full_family": "iPhone 12 Pro" - }, - { - "full_name": "iPhone 12 Pro (Global/A2407)", - "introduced": "October 13, 2020*", - "discontinued": "September 14, 2021", - "model": "A2407* (EMC 3547*)", - "device_id": "iPhone13,3", - "order": "MGMN3B/A*", - "full_family": "iPhone 12 Pro" - }, - { - "full_name": "iPhone 12 Pro (China/A2408)", - "introduced": "October 13, 2020*", - "discontinued": "September 14, 2021", - "model": "A2408* (EMC 3547*)", - "device_id": "iPhone13,3", - "order": "MGLD3CH/A*", - "full_family": "iPhone 12 Pro" - }, - { - "full_name": "iPhone 12 Pro Max (US/A2342)", - "introduced": "October 13, 2020*", - "discontinued": "September 14, 2021", - "model": "A2342* (EMC 3548*)", - "device_id": "iPhone13,4", - "order": "MG913LL/A*", - "full_family": "iPhone 12 Pro Max" - }, - { - "full_name": "iPhone 12 Pro Max (Canada/Japan/A2410)", - "introduced": "October 13, 2020*", - "discontinued": "September 14, 2021", - "model": "A2410* (EMC 3549*)", - "device_id": "iPhone13,4", - "order": "MGCX3VC/A*", - "full_family": "iPhone 12 Pro Max" - }, - { - "full_name": "iPhone 12 Pro Max (Global/A2411)", - "introduced": "October 13, 2020*", - "discontinued": "September 14, 2021", - "model": "A2411* (EMC 3550*)", - "device_id": "iPhone13,4", - "order": "MGDA3B/A*", - "full_family": "iPhone 12 Pro Max" - }, - { - "full_name": "iPhone 12 Pro Max (China/A2412)", - "introduced": "October 13, 2020*", - "discontinued": "September 14, 2021", - "model": "A2412* (EMC 3550*)", - "device_id": "iPhone13,4", - "order": "MGC33CH/A*", - "full_family": "iPhone 12 Pro Max" - }, - { - "full_name": "iPhone 13 mini (US/A2481)", - "introduced": "September 14, 2021*", - "discontinued": "September 12, 2023", - "model": "A2481* (EMC 3994*)", - "device_id": "iPhone14,4", - "order": "MLHP3LL/A*", - "full_family": "iPhone 13 mini" - }, - { - "full_name": "iPhone 13 mini (CA/MX/JP/SA/A2626)", - "introduced": "September 14, 2021*", - "discontinued": "September 12, 2023", - "model": "A2626* (EMC 3996*)", - "device_id": "iPhone14,4", - "order": "MLJF3VC/A*", - "full_family": "iPhone 13 mini" - }, - { - "full_name": "iPhone 13 mini (Global/A2628)", - "introduced": "September 14, 2021*", - "discontinued": "September 12, 2023", - "model": "A2628* (EMC 4029*)", - "device_id": "iPhone14,4", - "order": "MLK23B/A*", - "full_family": "iPhone 13 mini" - }, - { - "full_name": "iPhone 13 mini (China/A2629)", - "introduced": "September 14, 2021*", - "discontinued": "September 12, 2023", - "model": "A2629* (EMC 4030*)", - "device_id": "iPhone14,4", - "order": "MLDE3CH/A*", - "full_family": "iPhone 13 mini" - }, - { - "full_name": "iPhone 13 mini (Russia/A2630)", - "introduced": "September 14, 2021*", - "discontinued": "September 12, 2023", - "model": "A2630* (EMC 4030*)", - "device_id": "iPhone14,4", - "order": "MLLX3RU/A*", - "full_family": "iPhone 13 mini" - }, - { - "full_name": "iPhone 13 (US/A2482)", - "introduced": "September 14, 2021*", - "discontinued": "September 9, 2024", - "model": "A2482* (EMC 3997*)", - "device_id": "iPhone14,5", - "order": "MLMN3LL/A*", - "full_family": "iPhone 13" - }, - { - "full_name": "iPhone 13 (CA/MX/JP/SA/A2631)", - "introduced": "September 14, 2021*", - "discontinued": "September 9, 2024", - "model": "A2631* (EMC 3999*)", - "device_id": "iPhone14,5", - "order": "MLNE3VC/A*", - "full_family": "iPhone 13" - }, - { - "full_name": "iPhone 13 (Global/A2633)", - "introduced": "September 14, 2021*", - "discontinued": "September 9, 2024", - "model": "A2633* (EMC 4031*)", - "device_id": "iPhone14,5", - "order": "MLPH3B/A*", - "full_family": "iPhone 13" - }, - { - "full_name": "iPhone 13 (China/A2634)", - "introduced": "September 14, 2021*", - "discontinued": "September 9, 2024", - "model": "A2634* (EMC 4032*)", - "device_id": "iPhone14,5", - "order": "MLDW3CH/A*", - "full_family": "iPhone 13" - }, - { - "full_name": "iPhone 13 (Russia/A2635)", - "introduced": "September 14, 2021*", - "discontinued": "September 9, 2024", - "model": "A2635* (EMC 4032*)", - "device_id": "iPhone14,5", - "order": "MLNY3RU/A*", - "full_family": "iPhone 13" - }, - { - "full_name": "iPhone 13 Pro (US/A2483)", - "introduced": "September 14, 2021*", - "discontinued": "September 7, 2022", - "model": "A2483* (EMC 4000*)", - "device_id": "iPhone14,2", - "order": "MLTT3LL/A*", - "full_family": "iPhone 13 Pro" - }, - { - "full_name": "iPhone 13 Pro (CA/MX/JP/SA/A2636)", - "introduced": "September 14, 2021*", - "discontinued": "September 7, 2022", - "model": "A2636* (EMC 4002*)", - "device_id": "iPhone14,2", - "order": "MLUK3VC/A*", - "full_family": "iPhone 13 Pro" - }, - { - "full_name": "iPhone 13 Pro (Global/A2638)", - "introduced": "September 14, 2021*", - "discontinued": "September 7, 2022", - "model": "A2638* (EMC 4033*)", - "device_id": "iPhone14,2", - "order": "MLVD3B/A*", - "full_family": "iPhone 13 Pro" - }, - { - "full_name": "iPhone 13 Pro (China/A2639)", - "introduced": "September 14, 2021*", - "discontinued": "September 7, 2022", - "model": "A2639* (EMC 4034*)", - "device_id": "iPhone14,2", - "order": "MLT83CH/A*", - "full_family": "iPhone 13 Pro" - }, - { - "full_name": "iPhone 13 Pro (Russia/A2640)", - "introduced": "September 14, 2021*", - "discontinued": "September 7, 2022", - "model": "A2640* (EMC 4034*)", - "device_id": "iPhone14,2", - "order": "MLW43RU/A*", - "full_family": "iPhone 13 Pro" - }, - { - "full_name": "iPhone 13 Pro Max (US/A2484)", - "introduced": "September 14, 2021*", - "discontinued": "September 7, 2022", - "model": "A2484* (EMC 4003*)", - "device_id": "iPhone14,3", - "order": "MLKP3LL/A*", - "full_family": "iPhone 13 Pro Max" - }, - { - "full_name": "iPhone 13 Pro Max (CA/MX/JP/SA/A2641)", - "introduced": "September 14, 2021*", - "discontinued": "September 7, 2022", - "model": "A2641* (EMC 4005*)", - "device_id": "iPhone14,3", - "order": "MLJ73VC/A*", - "full_family": "iPhone 13 Pro Max" - }, - { - "full_name": "iPhone 13 Pro Max (Global/A2643)", - "introduced": "September 14, 2021*", - "discontinued": "September 7, 2022", - "model": "A2643* (EMC 4035*)", - "device_id": "iPhone14,3", - "order": "MLL93B/A*", - "full_family": "iPhone 13 Pro Max" - }, - { - "full_name": "iPhone 13 Pro Max (China/A2644)", - "introduced": "September 14, 2021*", - "discontinued": "September 7, 2022", - "model": "A2644* (EMC 4036*)", - "device_id": "iPhone14,3", - "order": "MLH73CH/A*", - "full_family": "iPhone 13 Pro Max" - }, - { - "full_name": "iPhone 13 Pro Max (Russia/A2645)", - "introduced": "September 14, 2021*", - "discontinued": "September 7, 2022", - "model": "A2645* (EMC 4036*)", - "device_id": "iPhone14,3", - "order": "MLLU3RU/A*", - "full_family": "iPhone 13 Pro Max" - }, - { - "full_name": "iPhone SE (3 - 4.7\" 2022 US/CA A2595)", - "introduced": "March 8, 2022", - "discontinued": "February 19, 2025", - "model": "A2595* (EMC 4082*)", - "device_id": "iPhone14,6", - "order": "MMX63LL/A*", - "full_family": "iPhone SE (3rd Gen)" - }, - { - "full_name": "iPhone SE (3 - 4.7\" 2022 Japan A2782)", - "introduced": "March 8, 2022", - "discontinued": "February 19, 2025", - "model": "A2782* (EMC 8064*)", - "device_id": "iPhone14,6", - "order": "MMYD3J/A*", - "full_family": "iPhone SE (3rd Gen)" - }, - { - "full_name": "iPhone SE (3 - 4.7\" 2022 Global A2783)", - "introduced": "March 8, 2022", - "discontinued": "February 19, 2025", - "model": "A2783* (EMC 4083*)", - "device_id": "iPhone14,6", - "order": "MMXG3B/A*", - "full_family": "iPhone SE (3rd Gen)" - }, - { - "full_name": "iPhone SE (3 - 4.7\" 2022 China A2785)", - "introduced": "March 8, 2022", - "discontinued": "February 19, 2025", - "model": "A2785* (EMC 8076*)", - "device_id": "iPhone14,6", - "order": "MMWW3CH/A*", - "full_family": "iPhone SE (3rd Gen)" - }, - { - "full_name": "iPhone 14 (US/A2649)", - "introduced": "September 7, 2022*", - "discontinued": "February 19, 2025", - "model": "A2649* (EMC 8138*)", - "device_id": "iPhone14,7", - "order": "MPVH3LL/A*", - "full_family": "iPhone 14" - }, - { - "full_name": "iPhone 14 (CA/MX/JP/SA/A2881)", - "introduced": "September 7, 2022*", - "discontinued": "February 19, 2025", - "model": "A2881* (EMC 8142*)", - "device_id": "iPhone14,7", - "order": "MPVJ3VC/A*", - "full_family": "iPhone 14" - }, - { - "full_name": "iPhone 14 (Global/A2882)", - "introduced": "September 7, 2022*", - "discontinued": "February 19, 2025", - "model": "A2882* (EMC 8143*)", - "device_id": "iPhone14,7", - "order": "MPVN3ZD/A*", - "full_family": "iPhone 14" - }, - { - "full_name": "iPhone 14 (China/A2884)", - "introduced": "September 7, 2022*", - "discontinued": "February 19, 2025", - "model": "A2884* (EMC 8144*)", - "device_id": "iPhone14,7", - "order": "MPVG3ZA/A*", - "full_family": "iPhone 14" - }, - { - "full_name": "iPhone 14 (Russia/A2883)", - "introduced": "September 7, 2022*", - "discontinued": "February 19, 2025", - "model": "A2883* (EMC 8144*)", - "device_id": "iPhone14,7", - "order": "MPVQ3RU/A*", - "full_family": "iPhone 14" - }, - { - "full_name": "iPhone 14 Plus (US/A2632)", - "introduced": "September 7, 2022*", - "discontinued": "February 19, 2025", - "model": "A2632* (EMC 8139*)", - "device_id": "iPhone14,8", - "order": "MQ3W3LL/A*", - "full_family": "iPhone 14 Plus" - }, - { - "full_name": "iPhone 14 Plus (CA/MX/JP/SA/A2885)", - "introduced": "September 7, 2022*", - "discontinued": "February 19, 2025", - "model": "A2885* (EMC 8146*)", - "device_id": "iPhone14,8", - "order": "MQ4H3VC/A*", - "full_family": "iPhone 14 Plus" - }, - { - "full_name": "iPhone 14 Plus (Global/A2886)", - "introduced": "September 7, 2022*", - "discontinued": "February 19, 2025", - "model": "A2886* (EMC 8147*)", - "device_id": "iPhone14,8", - "order": "MQ523ZD/A*", - "full_family": "iPhone 14 Plus" - }, - { - "full_name": "iPhone 14 Plus (China/A2888)", - "introduced": "September 7, 2022*", - "discontinued": "February 19, 2025", - "model": "A2888* (EMC 8148*)", - "device_id": "iPhone14,8", - "order": "MQ3A3ZA/A*", - "full_family": "iPhone 14 Plus" - }, - { - "full_name": "iPhone 14 Plus (Russia/A2887)", - "introduced": "September 7, 2022*", - "discontinued": "February 19, 2025", - "model": "A2887* (EMC 8148*)", - "device_id": "iPhone14,8", - "order": "MQ5N3RU/A*", - "full_family": "iPhone 14 Plus" - }, - { - "full_name": "iPhone 14 Pro (US/A2650)", - "introduced": "September 7, 2022*", - "discontinued": "September 12, 2023", - "model": "A2650* (EMC 8140*)", - "device_id": "iPhone15,2", - "order": "MQ0E3LL/A*", - "full_family": "iPhone 14 Pro" - }, - { - "full_name": "iPhone 14 Pro (CA/MX/JP/SA/A2889)", - "introduced": "September 7, 2022*", - "discontinued": "September 12, 2023", - "model": "A2889* (EMC 8150*)", - "device_id": "iPhone15,2", - "order": "MQ0F3VC/A*", - "full_family": "iPhone 14 Pro" - }, - { - "full_name": "iPhone 14 Pro (Global/A2890)", - "introduced": "September 7, 2022*", - "discontinued": "September 12, 2023", - "model": "A2890* (EMC 8151*)", - "device_id": "iPhone15,2", - "order": "MQ0G3ZD/A*", - "full_family": "iPhone 14 Pro" - }, - { - "full_name": "iPhone 14 Pro (China/A2892)", - "introduced": "September 7, 2022*", - "discontinued": "September 12, 2023", - "model": "A2892* (EMC 8152*)", - "device_id": "iPhone15,2", - "order": "MQ0D3ZA/A*", - "full_family": "iPhone 14 Pro" - }, - { - "full_name": "iPhone 14 Pro (Russia/A2891)", - "introduced": "September 7, 2022*", - "discontinued": "September 12, 2023", - "model": "A2891* (EMC 8152*)", - "device_id": "iPhone15,2", - "order": "MQ0H3RU/A*", - "full_family": "iPhone 14 Pro" - }, - { - "full_name": "iPhone 14 Pro Max (US/A2651)", - "introduced": "September 7, 2022*", - "discontinued": "September 12, 2023", - "model": "A2651* (EMC 8141*)", - "device_id": "iPhone15,3", - "order": "MQ8R3LL/A*", - "full_family": "iPhone 14 Pro Max" - }, - { - "full_name": "iPhone 14 Pro Max (CA/MX/JP/SA/A2893)", - "introduced": "September 7, 2022*", - "discontinued": "September 12, 2023", - "model": "A2893* (EMC 8154*)", - "device_id": "iPhone15,3", - "order": "MQ993VC/A*", - "full_family": "iPhone 14 Pro Max" - }, - { - "full_name": "iPhone 14 Pro Max (Global/A2894)", - "introduced": "September 7, 2022*", - "discontinued": "September 12, 2023", - "model": "A2894* (EMC 8155*)", - "device_id": "iPhone15,3", - "order": "MQ9T3ZD/A*", - "full_family": "iPhone 14 Pro Max" - }, - { - "full_name": "iPhone 14 Pro Max (China/A2896)", - "introduced": "September 7, 2022*", - "discontinued": "September 12, 2023", - "model": "A2896* (EMC 8156*)", - "device_id": "iPhone15,3", - "order": "MQ863ZA/A*", - "full_family": "iPhone 14 Pro Max" - }, - { - "full_name": "iPhone 14 Pro Max (Russia/A2895)", - "introduced": "September 7, 2022*", - "discontinued": "September 12, 2023", - "model": "A2895* (EMC 8156*)", - "device_id": "iPhone15,3", - "order": "MQC93RU/A*", - "full_family": "iPhone 14 Pro Max" - }, - { - "full_name": "iPhone 15 (US/A2846)", - "introduced": "September 12, 2023*", - "discontinued": "N/A", - "model": "A2846* (EMC 8427*)", - "device_id": "iPhone15,4", - "order": "MTLY3LL/A*", - "full_family": "iPhone 15" - }, - { - "full_name": "iPhone 15 (CA/MX/JP/SA/A3089)", - "introduced": "September 12, 2023*", - "discontinued": "N/A", - "model": "A3089* (EMC 8428*)", - "device_id": "iPhone15,4", - "order": "MTML3VC/A*", - "full_family": "iPhone 15" - }, - { - "full_name": "iPhone 15 (Global/A3090)", - "introduced": "September 12, 2023*", - "discontinued": "N/A", - "model": "A3090* (EMC 8429*)", - "device_id": "iPhone15,4", - "order": "MTP43ZD/A*", - "full_family": "iPhone 15" - }, - { - "full_name": "iPhone 15 (China/A3092)", - "introduced": "September 12, 2023*", - "discontinued": "N/A", - "model": "A3092* (EMC 8430*)", - "device_id": "iPhone15,4", - "order": "MTLG3CH/A*", - "full_family": "iPhone 15" - }, - { - "full_name": "iPhone 15 Plus (US/A2847)", - "introduced": "September 12, 2023*", - "discontinued": "N/A", - "model": "A2847* (EMC 8431*)", - "device_id": "iPhone15,5", - "order": "MTXV3LL/A*", - "full_family": "iPhone 15 Plus" - }, - { - "full_name": "iPhone 15 Plus (CA/MX/JP/SA/A3093)", - "introduced": "September 12, 2023*", - "discontinued": "N/A", - "model": "A3093* (EMC 8432*)", - "device_id": "iPhone15,5", - "order": "MU0D3VC/A*", - "full_family": "iPhone 15 Plus" - }, - { - "full_name": "iPhone 15 Plus (Global/A3094)", - "introduced": "September 12, 2023*", - "discontinued": "N/A", - "model": "A3094* (EMC 8433*)", - "device_id": "iPhone15,5", - "order": "MU163ZD/A*", - "full_family": "iPhone 15 Plus" - }, - { - "full_name": "iPhone 15 Plus (China/A3096)", - "introduced": "September 12, 2023*", - "discontinued": "N/A", - "model": "A3096* (EMC 8434*)", - "device_id": "iPhone15,5", - "order": "MTXD3CH/A*", - "full_family": "iPhone 15 Plus" - }, - { - "full_name": "iPhone 15 Pro (US/A2848)", - "introduced": "September 12, 2023*", - "discontinued": "September 9, 2024", - "model": "A2848* (EMC 8435*)", - "device_id": "iPhone16,1", - "order": "MTQP3LL/A*", - "full_family": "iPhone 15 Pro" - }, - { - "full_name": "iPhone 15 Pro (CA/MX/JP/SA/A3101)", - "introduced": "September 12, 2023*", - "discontinued": "September 9, 2024", - "model": "A3101* (EMC 8436*)", - "device_id": "iPhone16,1", - "order": "MTU93VC/A*", - "full_family": "iPhone 15 Pro" - }, - { - "full_name": "iPhone 15 Pro (Global/A3102)", - "introduced": "September 12, 2023*", - "discontinued": "September 9, 2024", - "model": "A3102* (EMC 8437*)", - "device_id": "iPhone16,1", - "order": "MTUX3ZD/A*", - "full_family": "iPhone 15 Pro" - }, - { - "full_name": "iPhone 15 Pro (China/A3104)", - "introduced": "September 12, 2023*", - "discontinued": "September 9, 2024", - "model": "A3104* (EMC 8438*)", - "device_id": "iPhone16,1", - "order": "MTQ63CH/A*", - "full_family": "iPhone 15 Pro" - }, - { - "full_name": "iPhone 15 Pro Max (US/A2849)", - "introduced": "September 12, 2023*", - "discontinued": "September 9, 2024", - "model": "A2849* (EMC 8439*)", - "device_id": "iPhone16,2", - "order": "MU683LL/A*", - "full_family": "iPhone 15 Pro Max" - }, - { - "full_name": "iPhone 15 Pro Max (CA/MX/JP/SA/A3105)", - "introduced": "September 12, 2023*", - "discontinued": "September 9, 2024", - "model": "A3105* (EMC 8440*)", - "device_id": "iPhone16,2", - "order": "MU6R3VC/A*", - "full_family": "iPhone 15 Pro Max" - }, - { - "full_name": "iPhone 15 Pro Max (Global/A3106)", - "introduced": "September 12, 2023*", - "discontinued": "September 9, 2024", - "model": "A3106* (EMC 8441*)", - "device_id": "iPhone16,2", - "order": "MU793ZD/A*", - "full_family": "iPhone 15 Pro Max" - }, - { - "full_name": "iPhone 15 Pro Max (China/A3108)", - "introduced": "September 12, 2023*", - "discontinued": "September 9, 2024", - "model": "A3108* (EMC 8442*)", - "device_id": "iPhone16,2", - "order": "MU2Q3CH/A*", - "full_family": "iPhone 15 Pro Max" - }, - { - "full_name": "iPhone 16 (US/A3081)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A3081* (EMC 8688*)", - "device_id": "iPhone17,3", - "order": "MYAT3LL/A*", - "full_family": "iPhone 16" - }, - { - "full_name": "iPhone 16 (CA/MX/JP/SA/A3286)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A3286* (EMC 8689*)", - "device_id": "iPhone17,3", - "order": "MYDU3VC/A*", - "full_family": "iPhone 16" - }, - { - "full_name": "iPhone 16 (Global/A3287)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A3287* (EMC 8690*)", - "device_id": "iPhone17,3", - "order": "MYEC3QN/A*", - "full_family": "iPhone 16" - }, - { - "full_name": "iPhone 16 (China/A3288)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A3288* (EMC 8691*)", - "device_id": "iPhone17,3", - "order": "MYEY3CH/A*", - "full_family": "iPhone 16" - }, - { - "full_name": "iPhone 16 Plus (US/A3082)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A3082* (EMC 8692*)", - "device_id": "iPhone17,4", - "order": "MXUW3LL/A*", - "full_family": "iPhone 16 Plus" - }, - { - "full_name": "iPhone 16 Plus (CA/MX/JP/SA/A3289)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A3289* (EMC 8693*)", - "device_id": "iPhone17,4", - "order": "MXVE3VC/A*", - "full_family": "iPhone 16 Plus" - }, - { - "full_name": "iPhone 16 Plus (Global/A3290)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A3290* (EMC 8694*)", - "device_id": "iPhone17,4", - "order": "MXVX3QN/A*", - "full_family": "iPhone 16 Plus" - }, - { - "full_name": "iPhone 16 Plus (China/A3291)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A3291* (EMC 8695*)", - "device_id": "iPhone17,4", - "order": "MXUD3CH/A*", - "full_family": "iPhone 16 Plus" - }, - { - "full_name": "iPhone 16 Pro (US/A3083)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A3083* (EMC 8666*)", - "device_id": "iPhone17,1", - "order": "MYMC3LL/A*", - "full_family": "iPhone 16 Pro" - }, - { - "full_name": "iPhone 16 Pro (CA/MX/JP/SA/A3292)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A3292* (EMC 8667*)", - "device_id": "iPhone17,1", - "order": "MYMX3VC/A*", - "full_family": "iPhone 16 Pro" - }, - { - "full_name": "iPhone 16 Pro (Global/A3293)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A3293* (EMC 8668*)", - "device_id": "iPhone17,1", - "order": "MYNF3QN/A*", - "full_family": "iPhone 16 Pro" - }, - { - "full_name": "iPhone 16 Pro (China/A3294)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A3294* (EMC 8683*)", - "device_id": "iPhone17,1", - "order": "MYLQ3CH/A*", - "full_family": "iPhone 16 Pro" - }, - { - "full_name": "iPhone 16 Pro Max (US/A3084)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A3084* (EMC 8684*)", - "device_id": "iPhone17,2", - "order": "MYW53LL/A*", - "full_family": "iPhone 16 Pro Max" - }, - { - "full_name": "iPhone 16 Pro Max (CA/MX/JP/SA/A3295)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A3295* (EMC 8685*)", - "device_id": "iPhone17,2", - "order": "MYWJ3VC/A*", - "full_family": "iPhone 16 Pro Max" - }, - { - "full_name": "iPhone 16 Pro Max (Global/A3296)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A3296* (EMC 8686*)", - "device_id": "iPhone17,2", - "order": "MYWX3QN/A*", - "full_family": "iPhone 16 Pro Max" - }, - { - "full_name": "iPhone 16 Pro Max (China/A3297)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A3297* (EMC 8687*)", - "device_id": "iPhone17,2", - "order": "MYTP3CH/A*", - "full_family": "iPhone 16 Pro Max" - }, - { - "full_name": "iPhone 16e (US/A3212)", - "introduced": "February 19, 2025", - "discontinued": "N/A", - "model": "A3212* (EMC 8725*)", - "device_id": "iPhone17,5", - "order": "MD0E4LL/A*", - "full_family": "iPhone 16e" - }, - { - "full_name": "iPhone 16e (CA/MX/SA/A3408)", - "introduced": "February 19, 2025", - "discontinued": "N/A", - "model": "A3408* (EMC 8726*)", - "device_id": "iPhone17,5", - "order": "MD1G4VC/A*", - "full_family": "iPhone 16e" - }, - { - "full_name": "iPhone 16e (Global/A3409)", - "introduced": "February 19, 2025", - "discontinued": "N/A", - "model": "A3409* (EMC 8727*)", - "device_id": "iPhone17,5", - "order": "MD1R4QN/A*", - "full_family": "iPhone 16e" - }, - { - "full_name": "iPhone 16e (China/A3410)", - "introduced": "February 19, 2025", - "discontinued": "N/A", - "model": "A3410* (EMC 8728*)", - "device_id": "iPhone17,5", - "order": "MD2C4CH/A*", - "full_family": "iPhone 16e" - }, - { - "full_name": "iPad Wi-Fi (Original/1st Gen)", - "introduced": "January 27, 2010*", - "discontinued": "March 2, 2011", - "model": "A1219 (EMC 2311)", - "device_id": "iPad1,1", - "order": "MB292LL/A*", - "full_family": "iPad (Original)" - }, - { - "full_name": "iPad Wi-Fi/3G/GPS (Original/1st Gen)", - "introduced": "January 27, 2010*", - "discontinued": "March 2, 2011", - "model": "A1337 (EMC 2328)", - "device_id": "iPad1,1", - "order": "MC349LL/A*", - "full_family": "iPad 3G (Original)" - }, - { - "full_name": "iPad 2 (Wi-Fi Only)", - "introduced": "March 2, 2011*", - "discontinued": "March 18, 2014**", - "model": "A1395 (EMC 2415)", - "device_id": "iPad2,1", - "order": "MC769LL/A*", - "full_family": "iPad 2" - }, - { - "full_name": "iPad 2 (Wi-Fi/GSM/GPS)", - "introduced": "March 2, 2011*", - "discontinued": "March 18, 2014**", - "model": "A1396 (EMC 2416)", - "device_id": "iPad2,2", - "order": "MC773LL/A*", - "full_family": "iPad 2 3G (AT&T)" - }, - { - "full_name": "iPad 2 (Wi-Fi/CDMA/GPS)", - "introduced": "March 2, 2011*", - "discontinued": "March 18, 2014**", - "model": "A1397 (EMC 2424)", - "device_id": "iPad2,3", - "order": "MC755LL/A*", - "full_family": "iPad 2 3G (Verizon)" - }, - { - "full_name": "iPad 2 (Wi-Fi Only, iPad2,4)", - "introduced": "March 25, 2012*", - "discontinued": "March 18, 2014", - "model": "A1395 (EMC 2560)", - "device_id": "iPad2,4", - "order": "MC769LL/A*", - "full_family": "iPad 2" - }, - { - "full_name": "iPad 3rd Gen (Wi-Fi Only)", - "introduced": "March 7, 2012*", - "discontinued": "October 23, 2012", - "model": "A1416 (EMC 2498)", - "device_id": "iPad3,1", - "order": "MC705LL/A*", - "full_family": "iPad 3rd Gen" - }, - { - "full_name": "iPad 3rd Gen (Wi-Fi/Cellular AT&T/GPS)", - "introduced": "March 7, 2012*", - "discontinued": "October 23, 2012", - "model": "A1430 (EMC 2578)", - "device_id": "iPad3,3", - "order": "MD366LL/A*", - "full_family": "iPad 3rd Gen (Wi-Fi + Cellular)" - }, - { - "full_name": "iPad 3rd Gen (Wi-Fi/Cellular Verizon/GPS)", - "introduced": "March 7, 2012*", - "discontinued": "October 23, 2012", - "model": "A1403 (EMC 2499)", - "device_id": "iPad3,2", - "order": "MC733LL/A*", - "full_family": "iPad 3rd Gen (Wi-Fi + Cellular VZ)" - }, - { - "full_name": "iPad 4th Gen (Wi-Fi Only)", - "introduced": "October 23, 2012*", - "discontinued": "October 16, 2014**", - "model": "A1458 (EMC 2604*)", - "device_id": "iPad3,4", - "order": "MD510LL/A*", - "full_family": "iPad 4th Gen (Wi-Fi)" - }, - { - "full_name": "iPad 4th Gen (Wi-Fi/AT&T/GPS)", - "introduced": "October 23, 2012*", - "discontinued": "October 16, 2014**", - "model": "A1459 (EMC 2605*)", - "device_id": "iPad3,5", - "order": "MD516LL/A*", - "full_family": "iPad 4th Gen (Wi-Fi + Cellular)" - }, - { - "full_name": "iPad 4th Gen (Wi-Fi/Verizon & Sprint/GPS)", - "introduced": "October 23, 2012*", - "discontinued": "October 16, 2014**", - "model": "A1460 (EMC 2606*)", - "device_id": "iPad3,6", - "order": "MD522LL/A*", - "full_family": "iPad 4th Gen (Wi-Fi + Cellular MM)" - }, - { - "full_name": "iPad mini Wi-Fi Only/1st Gen", - "introduced": "October 23, 2012*", - "discontinued": "June 19, 2015**", - "model": "A1432 (EMC 2607*)", - "device_id": "iPad2,5", - "order": "MD528LL/A*", - "full_family": "iPad mini (Wi-Fi)" - }, - { - "full_name": "iPad mini Wi-Fi/AT&T/GPS - 1st Gen", - "introduced": "October 23, 2012*", - "discontinued": "June 19, 2015**", - "model": "A1454 (EMC 2608*)", - "device_id": "iPad2,6", - "order": "MD534LL/A*", - "full_family": "iPad mini (Wi-Fi + Cellular)" - }, - { - "full_name": "iPad mini Wi-Fi/VZ & Sprint/GPS - 1st Gen", - "introduced": "October 23, 2012*", - "discontinued": "June 19, 2015**", - "model": "A1455 (EMC 2609*)", - "device_id": "iPad2,7", - "order": "MD540LL/A*", - "full_family": "iPad mini (Wi-Fi + Cellular MM)" - }, - { - "full_name": "iPad Air Wi-Fi Only", - "introduced": "October 22, 2013*", - "discontinued": "March 21, 2016**", - "model": "A1474 (EMC 2646*)", - "device_id": "iPad4,1", - "order": "MD785LL/A*", - "full_family": "iPad Air (Wi-Fi)" - }, - { - "full_name": "iPad Air Wi-Fi/Cellular", - "introduced": "October 22, 2013*", - "discontinued": "March 21, 2016**", - "model": "A1475 (EMC 2647*)", - "device_id": "iPad4,2", - "order": "ME991LL/A*", - "full_family": "iPad Air (Wi-Fi + Cellular)" - }, - { - "full_name": "iPad Air Wi-Fi/TD-LTE - China", - "introduced": "April 1, 2014", - "discontinued": "March 21, 2016**", - "model": "A1476 (EMC N/A)", - "device_id": "iPad4,3", - "order": "MD785CH/A*", - "full_family": "iPad Air (Wi-Fi + Cellular CN)" - }, - { - "full_name": "iPad mini 2 (Retina/2nd Gen, Wi-Fi Only)", - "introduced": "October 22, 2013*", - "discontinued": "March 21, 2017*", - "model": "A1489 (EMC 2695*)", - "device_id": "iPad4,4", - "order": "ME276LL/A*", - "full_family": "iPad mini Retina (Wi-Fi)" - }, - { - "full_name": "iPad mini 2 (Retina/2nd Gen, Wi-Fi/Cellular)", - "introduced": "October 22, 2013*", - "discontinued": "March 21, 2017*", - "model": "A1490 (EMC 2696*)", - "device_id": "iPad4,5", - "order": "MF066LL/A*", - "full_family": "iPad mini Retina (Wi-Fi + Cellular)" - }, - { - "full_name": "iPad mini 2 (Retina/2nd Gen, China)", - "introduced": "April 1, 2014", - "discontinued": "March 21, 2017*", - "model": "A1491 (EMC 2696*)", - "device_id": "iPad4,6", - "order": "MF247CH/A*", - "full_family": "iPad mini Retina (Wi-Fi + Cellular CN)" - }, - { - "full_name": "iPad Air 2 (Wi-Fi Only)", - "introduced": "October 16, 2014", - "discontinued": "March 21, 2017*", - "model": "A1566 (EMC 2822*)", - "device_id": "iPad5,3", - "order": "MGLW2LL/A*", - "full_family": "iPad Air 2 (Wi-Fi)" - }, - { - "full_name": "iPad Air 2 (Wi-Fi/Cellular)", - "introduced": "October 16, 2014", - "discontinued": "March 21, 2017*", - "model": "A1567 (EMC 2823*)", - "device_id": "iPad5,4", - "order": "MH2V2LL/A*", - "full_family": "iPad Air 2 (Wi-Fi + Cellular)" - }, - { - "full_name": "iPad mini 3 (Wi-Fi Only)", - "introduced": "October 16, 2014", - "discontinued": "September 9, 2015", - "model": "A1599 (EMC 2848*)", - "device_id": "iPad4,7", - "order": "MGNV2LL/A*", - "full_family": "iPad mini 3 (Wi-Fi)" - }, - { - "full_name": "iPad mini 3 (Wi-Fi/Cellular)", - "introduced": "October 16, 2014", - "discontinued": "September 9, 2015", - "model": "A1600 (EMC 2849*)", - "device_id": "iPad4,8", - "order": "MH3F2LL/A*", - "full_family": "iPad mini 3 (Wi-Fi + Cellular)" - }, - { - "full_name": "iPad mini 3 (Wi-Fi/Cellular, China)", - "introduced": "October 16, 2014*", - "discontinued": "September 9, 2015", - "model": "A1601 (EMC 2849*)", - "device_id": "iPad4,9", - "order": "MGPW2CH/A*", - "full_family": "iPad mini 3 (Wi-Fi + Cellular, China)" - }, - { - "full_name": "iPad mini 4 (Wi-Fi Only)", - "introduced": "September 9, 2015*", - "discontinued": "March 18, 2019*", - "model": "A1538 (EMC 2824*)", - "device_id": "iPad5,1", - "order": "MK6K2LL/A*", - "full_family": "iPad mini 4 (Wi-Fi)" - }, - { - "full_name": "iPad mini 4 (Wi-Fi/Cellular)", - "introduced": "September 9, 2015*", - "discontinued": "March 18, 2019*", - "model": "A1550 (EMC 2825*)", - "device_id": "iPad5,2", - "order": "MK872LL/A*", - "full_family": "iPad mini 4 (Wi-Fi + Cellular)" - }, - { - "full_name": "iPad Pro 12.9\" (Wi-Fi Only)", - "introduced": "September 9, 2015*", - "discontinued": "June 5, 2017", - "model": "A1584 (EMC 2838)", - "device_id": "iPad6,7", - "order": "ML0G2LL/A*", - "full_family": "iPad Pro (Wi-Fi)" - }, - { - "full_name": "iPad Pro 12.9\" (Wi-Fi/Cellular)", - "introduced": "September 9, 2015*", - "discontinued": "June 5, 2017", - "model": "A1652 (EMC 2827)", - "device_id": "iPad6,8", - "order": "ML3N2LL/A*", - "full_family": "iPad Pro (Wi-Fi + Cellular)" - }, - { - "full_name": "iPad Pro 9.7\" (Wi-Fi Only)", - "introduced": "March 21, 2016*", - "discontinued": "June 5, 2017", - "model": "A1673 (EMC 2976)", - "device_id": "iPad6,3", - "order": "MLMP2LL/A*", - "full_family": "iPad Pro 9.7\" Wi-Fi" - }, - { - "full_name": "iPad Pro 9.7\" (Wi-Fi/Cellular)", - "introduced": "March 21, 2016*", - "discontinued": "June 5, 2017", - "model": "A1674 (EMC 2977)", - "device_id": "iPad6,4", - "order": "MLPX2LL/A*", - "full_family": "iPad Pro 9.7\" Wi-Fi + Cellular" - }, - { - "full_name": "iPad 9.7\" 5th Gen (Wi-Fi Only)", - "introduced": "March 21, 2017*", - "discontinued": "March 27, 2018", - "model": "A1822 (EMC 3017*)", - "device_id": "iPad6,11", - "order": "MP2G2LL/A*", - "full_family": "iPad 5th Gen (Wi-Fi)" - }, - { - "full_name": "iPad 9.7\" 5th Gen (Wi-Fi/Cellular)", - "introduced": "March 21, 2017*", - "discontinued": "March 27, 2018", - "model": "A1823 (EMC 3108*)", - "device_id": "iPad6,12", - "order": "MP252LL/A*", - "full_family": "iPad 5th Gen (Wi-Fi + Cellular)" - }, - { - "full_name": "iPad Pro 10.5\" (Wi-Fi Only)", - "introduced": "June 5, 2017*", - "discontinued": "March 18, 2019", - "model": "A1701 (EMC 3140*)", - "device_id": "iPad7,3", - "order": "MQDW2LL/A*", - "full_family": "iPad Pro 10.5\" (Wi-Fi)" - }, - { - "full_name": "iPad Pro 10.5\" (Wi-Fi/Cellular)", - "introduced": "June 5, 2017*", - "discontinued": "March 18, 2019", - "model": "A1709 (EMC 3141*)", - "device_id": "iPad7,4", - "order": "MQF02LL/A*", - "full_family": "iPad Pro 10.5\" (Wi-Fi + Cellular)" - }, - { - "full_name": "iPad Pro 12.9\" (Wi-Fi Only - 2nd Gen)", - "introduced": "June 5, 2017*", - "discontinued": "October 30, 2018", - "model": "A1670 (EMC 3149*)", - "device_id": "iPad7,1", - "order": "MQDC2LL/A*", - "full_family": "iPad Pro 12.9\" (2nd Gen - Wi-Fi)" - }, - { - "full_name": "iPad Pro 12.9\" (Wi-Fi/Cell - 2nd Gen)", - "introduced": "June 5, 2017*", - "discontinued": "October 30, 2018", - "model": "A1671 (EMC 3150*)", - "device_id": "iPad7,2", - "order": "MQEE2LL/A*", - "full_family": "iPad Pro 12.9\" (2nd Gen - Wi-Fi+Cell)" - }, - { - "full_name": "iPad 9.7\" 6th Gen (Wi-Fi Only)", - "introduced": "March 27, 2018", - "discontinued": "September 10, 2019", - "model": "A1893 (EMC 3210*)", - "device_id": "iPad7,5", - "order": "MR7G2LL/A*", - "full_family": "iPad 6th Gen (Wi-Fi)" - }, - { - "full_name": "iPad 9.7\" 6th Gen (Wi-Fi/Cellular)", - "introduced": "March 27, 2018", - "discontinued": "September 10, 2019", - "model": "A1954 (EMC 3211*)", - "device_id": "iPad7,6", - "order": "MR702LL/A*", - "full_family": "iPad 6th Gen (Wi-Fi + Cellular)" - }, - { - "full_name": "iPad Pro 11\" (Wi-Fi Only)", - "introduced": "October 30, 2018*", - "discontinued": "March 18, 2020", - "model": "A1980 (EMC 3221*)", - "device_id": "iPad8,1, iPad8,2**", - "order": "MTXP2LL/A*", - "full_family": "iPad Pro 11\" (Wi-Fi)" - }, - { - "full_name": "iPad Pro 11\" (Wi-Fi/Cellular - US/CA)", - "introduced": "October 30, 2018*", - "discontinued": "March 18, 2020", - "model": "A2013 (EMC 3222*)", - "device_id": "iPad8,3, iPad8,4**", - "order": "MU0Y2LL/A*", - "full_family": "iPad Pro 11\" (Wi-Fi + Cellular)" - }, - { - "full_name": "iPad Pro 11\" (Wi-Fi/Cellular - Global)", - "introduced": "October 30, 2018*", - "discontinued": "March 18, 2020", - "model": "A1934 (EMC 3222*)", - "device_id": "iPad8,3, iPad8,4**", - "order": "MU0U2B/A*", - "full_family": "iPad Pro 11\" (Wi-Fi + Cellular)" - }, - { - "full_name": "iPad Pro 11\" (Wi-Fi/Cellular - China)", - "introduced": "October 30, 2018*", - "discontinued": "March 18, 2020", - "model": "A1979 (EMC 3222*)", - "device_id": "iPad8,3, iPad8,4**", - "order": "MU0X2CH/A*", - "full_family": "iPad Pro 11\" (Wi-Fi + Cellular)" - }, - { - "full_name": "iPad Pro 12.9\" (Wi-Fi Only - 3rd Gen)", - "introduced": "October 30, 2018*", - "discontinued": "March 18, 2020", - "model": "A1876 (EMC 3223*)", - "device_id": "iPad8,5, iPad8,6**", - "order": "MTEM2LL/A*", - "full_family": "iPad Pro 12.9\" (3rd Gen - Wi-Fi)" - }, - { - "full_name": "iPad Pro 12.9\" (Wi-Fi+Cell US/CA - 3rd Gen)", - "introduced": "October 30, 2018*", - "discontinued": "March 18, 2020", - "model": "A2014 (EMC 3224*)", - "device_id": "iPad8,7, iPad8,8**", - "order": "MTHU2LL/A*", - "full_family": "iPad Pro 12.9\" (3rd Gen - Wi-Fi+C)" - }, - { - "full_name": "iPad Pro 12.9\" (Wi-Fi+Cell Global - 3rd Gen)", - "introduced": "October 30, 2018*", - "discontinued": "March 18, 2020", - "model": "A1895 (EMC 3224*)", - "device_id": "iPad8,7, iPad8,8**", - "order": "MTHP2B/A*", - "full_family": "iPad Pro 12.9\" (3rd Gen - Wi-Fi+C)" - }, - { - "full_name": "iPad Pro 12.9\" (Wi-Fi+Cell China - 3rd Gen)", - "introduced": "October 30, 2018*", - "discontinued": "March 18, 2020", - "model": "A1983 (EMC 3224*)", - "device_id": "iPad8,7, iPad8,8**", - "order": "MTHT2CH/A*", - "full_family": "iPad Pro 12.9\" (3rd Gen - Wi-Fi+C)" - }, - { - "full_name": "iPad Air 3rd Gen (Wi-Fi Only)", - "introduced": "March 18, 2019", - "discontinued": "September 15, 2020", - "model": "A2152 (EMC 3315*)", - "device_id": "iPad11,3", - "order": "MUUK2LL/A*", - "full_family": "iPad Air 3rd Gen (Wi-Fi)" - }, - { - "full_name": "iPad Air 3rd Gen (Wi-Fi+Cell US/CA)", - "introduced": "March 18, 2019", - "discontinued": "September 15, 2020", - "model": "A2153 (EMC 3316*)", - "device_id": "iPad11,4", - "order": "MV162LL/A*", - "full_family": "iPad Air 3rd Gen (Wi-Fi + Cellular)" - }, - { - "full_name": "iPad Air 3rd Gen (Wi-Fi+Cell Global)", - "introduced": "March 18, 2019", - "discontinued": "September 15, 2020", - "model": "A2123 (EMC 3316*)", - "device_id": "iPad11,4", - "order": "MV0E2B/A*", - "full_family": "iPad Air 3rd Gen (Wi-Fi + Cellular)" - }, - { - "full_name": "iPad Air 3rd Gen (Wi-Fi+Cell China)", - "introduced": "March 18, 2019", - "discontinued": "September 15, 2020", - "model": "A2154 (EMC 3316*)", - "device_id": "iPad11,4", - "order": "MV0U2CH/A*", - "full_family": "iPad Air 3rd Gen (Wi-Fi + Cellular)" - }, - { - "full_name": "iPad mini 5th Gen (Wi-Fi Only)", - "introduced": "March 18, 2019", - "discontinued": "September 14, 2021", - "model": "A2133 (EMC 3313*)", - "device_id": "iPad11,1", - "order": "MUQX2LL/A*", - "full_family": "iPad mini 5th Gen (Wi-Fi)" - }, - { - "full_name": "iPad mini 5th Gen (Wi-Fi+Cell US/CA)", - "introduced": "March 18, 2019", - "discontinued": "September 14, 2021", - "model": "A2126 (EMC 3314*)", - "device_id": "iPad11,2", - "order": "MUXG2LL/A*", - "full_family": "iPad mini 5th Gen (Wi-Fi + Cellular)" - }, - { - "full_name": "iPad mini 5th Gen (Wi-Fi+Cell Global)", - "introduced": "March 18, 2019", - "discontinued": "September 14, 2021", - "model": "A2124 (EMC 3314*)", - "device_id": "iPad11,2", - "order": "MUX62B/A*", - "full_family": "iPad mini 5th Gen (Wi-Fi + Cellular)" - }, - { - "full_name": "iPad mini 5th Gen (Wi-Fi+Cell China)", - "introduced": "March 18, 2019", - "discontinued": "September 14, 2021", - "model": "A2125 (EMC 3314*)", - "device_id": "iPad11,2", - "order": "MUXR2CH/A*", - "full_family": "iPad mini 5th Gen (Wi-Fi + Cellular)" - }, - { - "full_name": "iPad 10.2\" 7th Gen (Wi-Fi Only)", - "introduced": "September 10, 2019", - "discontinued": "September 15, 2020", - "model": "A2197 (EMC 3323*)", - "device_id": "iPad7,11", - "order": "MW752LL/A*", - "full_family": "iPad 7th Gen (Wi-Fi)" - }, - { - "full_name": "iPad 10.2\" 7th Gen (Wi-Fi/Cellular, US/CA)", - "introduced": "September 10, 2019", - "discontinued": "September 15, 2020", - "model": "A2200 (EMC 3324*)", - "device_id": "iPad7,12", - "order": "MW6X2LL/A*", - "full_family": "iPad 7th Gen (Wi-Fi + Cellular)" - }, - { - "full_name": "iPad 10.2\" 7th Gen (Wi-Fi/Cellular, Global)", - "introduced": "September 10, 2019", - "discontinued": "September 15, 2020", - "model": "A2198 (EMC 3324*)", - "device_id": "iPad7,12", - "order": "MW6C2B/A*", - "full_family": "iPad 7th Gen (Wi-Fi + Cellular)" - }, - { - "full_name": "iPad 10.2\" 7th Gen (Wi-Fi/Cellular, China)", - "introduced": "September 10, 2019", - "discontinued": "September 15, 2020", - "model": "A2199 (EMC 3324*)", - "device_id": "iPad7,12", - "order": "MW6Q2CH/A*", - "full_family": "iPad 7th Gen (Wi-Fi + Cellular, CN)" - }, - { - "full_name": "iPad Pro 11\" (Wi-Fi Only - 2nd Gen)", - "introduced": "March 18, 2020", - "discontinued": "April 20, 2021", - "model": "A2228 (EMC 3349)", - "device_id": "iPad8,9", - "order": "MY252LL/A*", - "full_family": "iPad Pro 11\" (2nd Gen - Wi-Fi)" - }, - { - "full_name": "iPad Pro 11\" (Wi-Fi/Cell US/CA - 2nd Gen)", - "introduced": "March 18, 2020", - "discontinued": "April 20, 2021", - "model": "A2068 (EMC 3350)", - "device_id": "iPad8,10", - "order": "MY342LL/A*", - "full_family": "iPad Pro 11\" (2nd Gen - Wi-Fi+C)" - }, - { - "full_name": "iPad Pro 11\" (Wi-Fi/Cell Global - 2nd Gen)", - "introduced": "March 18, 2020", - "discontinued": "April 20, 2021", - "model": "A2230 (EMC 3350)", - "device_id": "iPad8,10", - "order": "MY2W2ZP/A*", - "full_family": "iPad Pro 11\" (2nd Gen - Wi-Fi+C)" - }, - { - "full_name": "iPad Pro 11\" (Wi-Fi/Cell China - 2nd Gen)", - "introduced": "March 18, 2020", - "discontinued": "April 20, 2021", - "model": "A2231 (EMC 3350)", - "device_id": "iPad8,10", - "order": "MY322CH/A*", - "full_family": "iPad Pro 11\" (2nd Gen - Wi-Fi+C)" - }, - { - "full_name": "iPad Pro 12.9\" (Wi-Fi Only - 4th Gen)", - "introduced": "March 18, 2020", - "discontinued": "April 20, 2021", - "model": "A2229 (EMC 3353)", - "device_id": "iPad8,11", - "order": "MY2J2LL/A*", - "full_family": "iPad Pro 12.9\" (4th Gen - Wi-Fi)" - }, - { - "full_name": "iPad Pro 12.9\" (Wi-Fi/Cell US/CA - 4th Gen)", - "introduced": "March 18, 2020", - "discontinued": "April 20, 2021", - "model": "A2069 (EMC 3354)", - "device_id": "iPad8,12", - "order": "MY3K2LL/A*", - "full_family": "iPad Pro 12.9\" (4th Gen - Wi-Fi+C)" - }, - { - "full_name": "iPad Pro 12.9\" (Wi-Fi/Cell Global - 4th Gen)", - "introduced": "March 18, 2020", - "discontinued": "April 20, 2021", - "model": "A2232 (EMC 3354)", - "device_id": "iPad8,12", - "order": "MY3D2ZP/A*", - "full_family": "iPad Pro 12.9\" (4th Gen - Wi-Fi+C)" - }, - { - "full_name": "iPad Pro 12.9\" (Wi-Fi/Cell China - 4th Gen)", - "introduced": "March 18, 2020", - "discontinued": "April 20, 2021", - "model": "A2233 (EMC 3354)", - "device_id": "iPad8,12", - "order": "MY3H2CH/A*", - "full_family": "iPad Pro 12.9\" (4th Gen - Wi-Fi+C)" - }, - { - "full_name": "iPad 10.2\" 8th Gen (Wi-Fi Only)", - "introduced": "September 15, 2020", - "discontinued": "September 14, 2021", - "model": "A2270 (EMC 3574*)", - "device_id": "iPad11,6", - "order": "MYLA2LL/A*", - "full_family": "iPad 8th Gen (Wi-Fi)" - }, - { - "full_name": "iPad 10.2\" 8th Gen (Wi-Fi/Cellular, US/CA)", - "introduced": "September 15, 2020", - "discontinued": "September 14, 2021", - "model": "A2428 (EMC 3575*)", - "device_id": "iPad11,7", - "order": "MYN52LL/A*", - "full_family": "iPad 8th Gen (Wi-Fi + Cellular)" - }, - { - "full_name": "iPad 10.2\" 8th Gen (Wi-Fi/Cellular, Global)", - "introduced": "September 15, 2020", - "discontinued": "September 14, 2021", - "model": "A2429 (EMC 3575*)", - "device_id": "iPad11,7", - "order": "MYMJ2B/A*", - "full_family": "iPad 8th Gen (Wi-Fi + Cellular)" - }, - { - "full_name": "iPad 10.2\" 8th Gen (Wi-Fi/Cellular, China)", - "introduced": "September 15, 2020", - "discontinued": "September 14, 2021", - "model": "A2430 (EMC 3575*)", - "device_id": "iPad11,7", - "order": "MYMX2CH/A*", - "full_family": "iPad 8th Gen (Wi-Fi + Cellular, CN)" - }, - { - "full_name": "iPad Air 4th Gen (Wi-Fi Only)", - "introduced": "September 15, 2020*", - "discontinued": "March 8, 2022", - "model": "A2316 (EMC 3570*)", - "device_id": "iPad13,1", - "order": "MYFN2LL/A*", - "full_family": "iPad Air 4th Gen (Wi-Fi)" - }, - { - "full_name": "iPad Air 4th Gen (Wi-Fi+Cell US/CA)", - "introduced": "September 15, 2020*", - "discontinued": "March 8, 2022", - "model": "A2324 (EMC 3571*)", - "device_id": "iPad13,2", - "order": "MYHY2LL/A*", - "full_family": "iPad Air 4th Gen (Wi-Fi+Cell)" - }, - { - "full_name": "iPad Air 4th Gen (Wi-Fi+Cell Global)", - "introduced": "September 15, 2020*", - "discontinued": "March 8, 2022", - "model": "A2072 (EMC 3571*)", - "device_id": "iPad13,2", - "order": "MYGX2ZP/A*", - "full_family": "iPad Air 4th Gen (Wi-Fi+Cell)" - }, - { - "full_name": "iPad Air 4th Gen (Wi-Fi+Cell China)", - "introduced": "September 15, 2020*", - "discontinued": "March 8, 2022", - "model": "A2325 (EMC 3571*)", - "device_id": "iPad13,2", - "order": "MYHM2CH/A*", - "full_family": "iPad Air 4th Gen (Wi-Fi+Cell)" - }, - { - "full_name": "iPad Pro 11\" (Wi-Fi Only - 3rd Gen)", - "introduced": "April 20, 2021*", - "discontinued": "October 18, 2022", - "model": "A2377 (EMC 3681)", - "device_id": "iPad13,4", - "order": "MHQT3LL/A*", - "full_family": "iPad Pro 11\" (3rd Gen - Wi-Fi)" - }, - { - "full_name": "iPad Pro 11\" (Wi-Fi/Cell US - 3rd Gen)", - "introduced": "April 20, 2021*", - "discontinued": "October 18, 2022", - "model": "A2301 (EMC 3682)", - "device_id": "iPad13,5", - "order": "MHMU3LL/A*", - "full_family": "iPad Pro 11\" (3rd Gen - Wi-Fi+C)" - }, - { - "full_name": "iPad Pro 11\" (Wi-Fi/Cell Global - 3rd Gen)", - "introduced": "April 20, 2021*", - "discontinued": "October 18, 2022", - "model": "A2459 (EMC 3683)", - "device_id": "iPad13,6", - "order": "MHW63X/A*", - "full_family": "iPad Pro 11\" (3rd Gen - Wi-Fi+C)" - }, - { - "full_name": "iPad Pro 11\" (Wi-Fi/Cell China - 3rd Gen)", - "introduced": "April 20, 2021*", - "discontinued": "October 18, 2022", - "model": "A2460 (EMC 3684)", - "device_id": "iPad13,7", - "order": "MHWH3CH/A*", - "full_family": "iPad Pro 11\" (3rd Gen - Wi-Fi+C)" - }, - { - "full_name": "iPad Pro 12.9\" (Wi-Fi Only - 5th Gen)", - "introduced": "April 20, 2021*", - "discontinued": "October 18, 2022", - "model": "A2378 (EMC 3689)", - "device_id": "iPad13,8", - "order": "MHNG3LL/A*", - "full_family": "iPad Pro 12.9\" (5th Gen - Wi-Fi)" - }, - { - "full_name": "iPad Pro 12.9\" (Wi-Fi/Cell US - 5th Gen)", - "introduced": "April 20, 2021*", - "discontinued": "October 18, 2022", - "model": "A2379 (EMC 3686)", - "device_id": "iPad13,9", - "order": "MHNT3LL/A*", - "full_family": "iPad Pro 12.9\" (5th Gen - Wi-Fi+C)" - }, - { - "full_name": "iPad Pro 12.9\" (Wi-Fi/Cell Global - 5th Gen)", - "introduced": "April 20, 2021*", - "discontinued": "October 18, 2022", - "model": "A2461 (EMC 3687)", - "device_id": "iPad13,10", - "order": "MHR53X/A*", - "full_family": "iPad Pro 12.9\" (5th Gen - Wi-Fi+C)" - }, - { - "full_name": "iPad Pro 12.9\" (Wi-Fi/Cell China - 5th Gen)", - "introduced": "April 20, 2021*", - "discontinued": "October 18, 2022", - "model": "A2462 (EMC 3688)", - "device_id": "iPad13,11", - "order": "MHRG3CH/A*", - "full_family": "iPad Pro 12.9\" (5th Gen - Wi-Fi+C)" - }, - { - "full_name": "iPad 10.2\" 9th Gen (Wi-Fi Only)", - "introduced": "September 14, 2021*", - "discontinued": "May 7, 2024", - "model": "A2602 (EMC 4045*)", - "device_id": "iPad12,1", - "order": "MK2L3LL/A*", - "full_family": "iPad 9th Gen (Wi-Fi)" - }, - { - "full_name": "iPad 10.2\" 9th Gen (Wi-Fi/Cellular, US/CA)", - "introduced": "September 14, 2021*", - "discontinued": "May 7, 2024", - "model": "A2603 (EMC 4046*)", - "device_id": "iPad12,2", - "order": "MK673LL/A*", - "full_family": "iPad 9th Gen (Wi-Fi + Cellular)" - }, - { - "full_name": "iPad 10.2\" 9th Gen (Wi-Fi/Cellular, Global)", - "introduced": "September 14, 2021*", - "discontinued": "May 7, 2024", - "model": "A2604 (EMC 4046*)", - "device_id": "iPad12,2", - "order": "MK493B/A*", - "full_family": "iPad 9th Gen (Wi-Fi + Cellular)" - }, - { - "full_name": "iPad 10.2\" 9th Gen (Wi-Fi/Cellular, China)", - "introduced": "September 14, 2021*", - "discontinued": "May 7, 2024", - "model": "A2605 (EMC 4046*)", - "device_id": "iPad12,2", - "order": "MK613CH/A*", - "full_family": "iPad 9th Gen (Wi-Fi + Cellular, CN)" - }, - { - "full_name": "iPad mini 6th Gen (Wi-Fi Only)", - "introduced": "September 14, 2021*", - "discontinued": "October 15, 2024", - "model": "A2567 (EMC 4043*)", - "device_id": "iPad14,1", - "order": "MK7M3LL/A*", - "full_family": "iPad mini 6th Gen (Wi-Fi)" - }, - { - "full_name": "iPad mini 6th Gen (Wi-Fi/Cell US/Global)", - "introduced": "September 14, 2021*", - "discontinued": "October 15, 2024", - "model": "A2568 (EMC 4044*)", - "device_id": "iPad14,2", - "order": "MK893LL/A*", - "full_family": "iPad mini 6th Gen (Wi-Fi+Cell)" - }, - { - "full_name": "iPad mini 6th Gen (Wi-Fi/Cell China)", - "introduced": "September 14, 2021*", - "discontinued": "October 15, 2024", - "model": "A2569 (EMC 4044*)", - "device_id": "iPad14,2", - "order": "MK8Y3CH/A*", - "full_family": "iPad mini 6th Gen (Wi-Fi+Cell CN)" - }, - { - "full_name": "iPad Air 5th Gen (Wi-Fi Only)", - "introduced": "March 8, 2022", - "discontinued": "May 7, 2024", - "model": "A2588 (EMC 4041*)", - "device_id": "iPad13,16", - "order": "MM9C3LL/A*", - "full_family": "iPad Air 5th Gen (Wi-Fi)" - }, - { - "full_name": "iPad Air 5th Gen (Wi-Fi+Cell A2589)", - "introduced": "March 8, 2022", - "discontinued": "May 7, 2024", - "model": "A2589 (EMC 4042*)", - "device_id": "iPad13,17", - "order": "MM6R3LL/A*", - "full_family": "iPad Air 5th Gen (Wi-Fi+Cell)" - }, - { - "full_name": "iPad Air 5th Gen (Wi-Fi+Cell China)", - "introduced": "March 8, 2022", - "discontinued": "May 7, 2024", - "model": "A2591 (EMC 4042*)", - "device_id": "iPad13,17", - "order": "MM753CH/A*", - "full_family": "iPad Air 5th Gen (Wi-Fi+Cell)" - }, - { - "full_name": "iPad 10.9\" 10th Gen (Wi-Fi Only)", - "introduced": "October 18, 2022", - "discontinued": "March 4, 2025", - "model": "A2696 (EMC 8137*)", - "device_id": "iPad13,18", - "order": "MPQ03LL/A*", - "full_family": "iPad 10th Gen (Wi-Fi)" - }, - { - "full_name": "iPad 10.9\" 10th Gen (Wi-Fi/Cellular)", - "introduced": "October 18, 2022", - "discontinued": "March 4, 2025", - "model": "A2757 (EMC 8135*)", - "device_id": "iPad13,19", - "order": "MQ6J3LL/A*", - "full_family": "iPad 10th Gen (Wi-Fi + Cellular)" - }, - { - "full_name": "iPad 10.9\" 10th Gen (Wi-Fi/Cellular, China)", - "introduced": "October 18, 2022", - "discontinued": "March 4, 2025", - "model": "A2777 (EMC 8135*)", - "device_id": "iPad13,19", - "order": "MQ6X3CH/A*", - "full_family": "iPad 10th Gen (Wi-Fi + Cell, CN)" - }, - { - "full_name": "iPad Pro 11\" (Wi-Fi Only - 4th Gen)", - "introduced": "October 18, 2022", - "discontinued": "May 7, 2024", - "model": "A2759 (EMC 8166*)", - "device_id": "iPad14,3", - "order": "MNXE3LL/A*", - "full_family": "iPad Pro 11\" (4th Gen - Wi-Fi)" - }, - { - "full_name": "iPad Pro 11\" (Wi-Fi/Cell US - 4th Gen)", - "introduced": "October 18, 2022", - "discontinued": "May 7, 2024", - "model": "A2435 (EMC 8167*)", - "device_id": "iPad14,4", - "order": "MP563LL/A*", - "full_family": "iPad Pro 11\" (4th Gen - Wi-Fi+C)" - }, - { - "full_name": "iPad Pro 11\" (Wi-Fi/Cell Global - 4th Gen)", - "introduced": "October 18, 2022", - "discontinued": "May 7, 2024", - "model": "A2761 (EMC 8168*)", - "device_id": "iPad14,4", - "order": "MNYD3B/A*", - "full_family": "iPad Pro 11\" (4th Gen - Wi-Fi+C)" - }, - { - "full_name": "iPad Pro 11\" (Wi-Fi/Cell China - 4th Gen)", - "introduced": "October 18, 2022", - "discontinued": "May 7, 2024", - "model": "A2762 (EMC 8168*)", - "device_id": "iPad14,4", - "order": "MNYP3CH/A*", - "full_family": "iPad Pro 11\" (4th Gen - Wi-Fi+C)" - }, - { - "full_name": "iPad Pro 12.9\" (Wi-Fi Only - 6th Gen)", - "introduced": "October 18, 2022", - "discontinued": "May 7, 2024", - "model": "A2436 (EMC 8169*)", - "device_id": "iPad14,5", - "order": "MNXQ3LL/A*", - "full_family": "iPad Pro 12.9\" (6th Gen - Wi-Fi)" - }, - { - "full_name": "iPad Pro 12.9\" (Wi-Fi/Cell US - 6th Gen)", - "introduced": "October 18, 2022", - "discontinued": "May 7, 2024", - "model": "A2764 (EMC 8176*)", - "device_id": "iPad14,6", - "order": "MP5Y3LL/A*", - "full_family": "iPad Pro 12.9\" (6th Gen - Wi-Fi+C)" - }, - { - "full_name": "iPad Pro 12.9\" (Wi-Fi/Cell Global - 6th Gen)", - "introduced": "October 18, 2022", - "discontinued": "May 7, 2024", - "model": "A2437 (EMC 8177*)", - "device_id": "iPad14,6", - "order": "MP1Y3B/A*", - "full_family": "iPad Pro 12.9\" (6th Gen - Wi-Fi+C)" - }, - { - "full_name": "iPad Pro 12.9\" (Wi-Fi/Cell China - 6th Gen)", - "introduced": "October 18, 2022", - "discontinued": "May 7, 2024", - "model": "A2766 (EMC 8177*)", - "device_id": "iPad14,6", - "order": "MP293CH/A*", - "full_family": "iPad Pro 12.9\" (6th Gen - Wi-Fi+C)" - }, - { - "full_name": "iPad Air M2 11\" (Wi-Fi Only)", - "introduced": "May 7, 2024", - "discontinued": "March 4, 2025", - "model": "A2902 (EMC 8392*)", - "device_id": "iPad14,8", - "order": "MUWD3LL/A*", - "full_family": "iPad Air 11\" M2 (Wi-Fi)" - }, - { - "full_name": "iPad Air M2 11\" (Wi-Fi+Cell)", - "introduced": "May 7, 2024", - "discontinued": "March 4, 2025", - "model": "A2903 (EMC 8393*)", - "device_id": "iPad14,9", - "order": "MUXE3LL/A*", - "full_family": "iPad Air 11\" M2 (Wi-Fi+Cell)" - }, - { - "full_name": "iPad Air M2 11\" (Wi-Fi+Cell China)", - "introduced": "May 7, 2024", - "discontinued": "March 4, 2025", - "model": "A2904 (EMC 8393*)", - "device_id": "iPad14,9", - "order": "MUXX3CH/A*", - "full_family": "iPad Air 11\" M2 (China)" - }, - { - "full_name": "iPad Air M2 13\" (Wi-Fi Only)", - "introduced": "May 7, 2024", - "discontinued": "March 4, 2025", - "model": "A2898 (EMC 8394*)", - "device_id": "iPad14,10", - "order": "MV283LL/A*", - "full_family": "iPad Air 13\" M2 (Wi-Fi)" - }, - { - "full_name": "iPad Air M2 13\" (Wi-Fi+Cell)", - "introduced": "May 7, 2024", - "discontinued": "March 4, 2025", - "model": "A2899 (EMC 8395*)", - "device_id": "iPad14,11", - "order": "MV6R3LL/A*", - "full_family": "iPad Air 13\" M2 (Wi-Fi+Cell)" - }, - { - "full_name": "iPad Air M2 13\" (Wi-Fi+Cell China)", - "introduced": "May 7, 2024", - "discontinued": "March 4, 2025", - "model": "A2900 (EMC 8395*)", - "device_id": "iPad14,11", - "order": "MV793CH/A*", - "full_family": "iPad Air 13\" M2 (China)" - }, - { - "full_name": "iPad Pro M4 11\" (Wi-Fi Only)", - "introduced": "May 7, 2024", - "discontinued": "N/A", - "model": "A2836 (EMC 8472*)", - "device_id": "iPad16,3", - "order": "MVV93LL/A*", - "full_family": "iPad Pro 11\" M4 (Wi-Fi)" - }, - { - "full_name": "iPad Pro M4 11\" (Wi-Fi+Cell)", - "introduced": "May 7, 2024", - "discontinued": "N/A", - "model": "A2837 (EMC 8473*)", - "device_id": "iPad16,4", - "order": "MVW23LL/A*", - "full_family": "iPad Pro 11\" M4 (Wi-Fi+Cell)" - }, - { - "full_name": "iPad Pro M4 11\" (Wi-Fi+Cell China)", - "introduced": "May 7, 2024", - "discontinued": "N/A", - "model": "A3006 (EMC 8473*)", - "device_id": "iPad16,4", - "order": "MVWA3CH/A*", - "full_family": "iPad Pro 11\" M4 (China)" - }, - { - "full_name": "iPad Pro M4 13\" (Wi-Fi Only)", - "introduced": "May 7, 2024", - "discontinued": "N/A", - "model": "A2925 (EMC 8474*)", - "device_id": "iPad16,5", - "order": "MVX33LL/A*", - "full_family": "iPad Pro 13\" M4 (Wi-Fi)" - }, - { - "full_name": "iPad Pro M4 13\" (Wi-Fi+Cell)", - "introduced": "May 7, 2024", - "discontinued": "N/A", - "model": "A2926 (EMC 8475*)", - "device_id": "iPad16,6", - "order": "MVXT3LL/A*", - "full_family": "iPad Pro 13\" M4 (Wi-Fi+Cell)" - }, - { - "full_name": "iPad Pro M4 13\" (Wi-Fi+Cell China)", - "introduced": "May 7, 2024", - "discontinued": "N/A", - "model": "A3007 (EMC 8475*)", - "device_id": "iPad16,6", - "order": "MVY23CH/A*", - "full_family": "iPad Pro 13\" M4 (China)" - }, - { - "full_name": "iPad mini A17 Pro - 7th Gen (Wi-Fi Only)", - "introduced": "October 15, 2024*", - "discontinued": "N/A", - "model": "A2993 (EMC 8470*)", - "device_id": "iPad16,1", - "order": "MXN73LL/A*", - "full_family": "iPad mini A17 Pro (Wi-Fi)" - }, - { - "full_name": "iPad mini A17 Pro - 7th Gen (Wi-Fi/Cell)", - "introduced": "October 15, 2024*", - "discontinued": "N/A", - "model": "A2995 (EMC 8471*)", - "device_id": "iPad16,2", - "order": "MXPP3LL/A*", - "full_family": "iPad mini A17 Pro (Wi-Fi+Cell)" - }, - { - "full_name": "iPad mini A17 Pro - 7th Gen (Wi-Fi/Cell China)", - "introduced": "October 15, 2024*", - "discontinued": "N/A", - "model": "A2996 (EMC 8471*)", - "device_id": "iPad16,2", - "order": "MXQ13CH/A*", - "full_family": "iPad mini A17 Pro (Wi-Fi+Cell CN)" - }, - { - "full_name": "iPad A16 - 11th Gen (Wi-Fi Only)", - "introduced": "March 4, 2025", - "discontinued": "N/A", - "model": "A3354 (EMC 8805*)", - "device_id": "Pending", - "order": "Pending", - "full_family": "iPad A16 (Wi-Fi)" - }, - { - "full_name": "iPad A16 - 11th Gen (Wi-Fi/Cell)", - "introduced": "March 4, 2025", - "discontinued": "N/A", - "model": "A3355 (EMC 8806*)", - "device_id": "Pending", - "order": "Pending", - "full_family": "iPad A16 (Wi-Fi + Cellular)" - }, - { - "full_name": "iPad A16 - 11th Gen (Wi-Fi/Cell, China)", - "introduced": "March 4, 2025", - "discontinued": "N/A", - "model": "A3356 (EMC 8806*)", - "device_id": "Pending", - "order": "Pending", - "full_family": "iPad A16 (Wi-Fi + Cellular, China)" - }, - { - "full_name": "iPad Air M3 11\" (Wi-Fi Only)", - "introduced": "March 4, 2025", - "discontinued": "N/A", - "model": "A3266 (EMC 8776*)", - "device_id": "Pending", - "order": "Pending", - "full_family": "iPad Air 11\" M3 (Wi-Fi)" - }, - { - "full_name": "iPad Air M3 11\" (Wi-Fi+Cell)", - "introduced": "March 4, 2025", - "discontinued": "N/A", - "model": "A3267 (EMC 8777*)", - "device_id": "Pending", - "order": "Pending", - "full_family": "iPad Air 11\" M3 (Wi-Fi+Cell)" - }, - { - "full_name": "iPad Air M3 11\" (Wi-Fi+Cell, China)", - "introduced": "March 4, 2025", - "discontinued": "N/A", - "model": "A3270 (EMC 8777*)", - "device_id": "Pending", - "order": "Pending", - "full_family": "iPad Air 11\" M3 (China)" - }, - { - "full_name": "iPad Air M3 13\" (Wi-Fi Only)", - "introduced": "March 4, 2025", - "discontinued": "N/A", - "model": "A3268 (EMC 8778*)", - "device_id": "Pending", - "order": "Pending", - "full_family": "iPad Air 13\" M3 (Wi-Fi)" - }, - { - "full_name": "iPad Air M3 13\" (Wi-Fi+Cell)", - "introduced": "March 4, 2025", - "discontinued": "N/A", - "model": "A3269 (EMC 8779*)", - "device_id": "Pending", - "order": "Pending", - "full_family": "iPad Air 13\" M3 (Wi-Fi+Cell)" - }, - { - "full_name": "iPad Air M3 13\" (Wi-Fi+Cell, China)", - "introduced": "March 4, 2025", - "discontinued": "N/A", - "model": "A3271 (EMC 8779*)", - "device_id": "Pending", - "order": "Pending", - "full_family": "iPad Air 13\" M3 (China)" - }, - { - "full_name": "Apple TV (Original/1st Gen)", - "introduced": "January 9, 2007*", - "discontinued": "September 1, 2010*", - "model": "A1218 (EMC 2123)", - "device_id": "AppleTV1,1", - "order": "MA711LL/A*", - "full_family": "Apple TV" - }, - { - "full_name": "Apple TV (2nd Generation)", - "introduced": "September 1, 2010*", - "discontinued": "March 7, 2012", - "model": "A1378 (EMC 2411)", - "device_id": "AppleTV2,1", - "order": "MC572LL/A", - "full_family": "2nd Gen" - }, - { - "full_name": "Apple TV (3rd Generation, Early 2012)", - "introduced": "March 7, 2012*", - "discontinued": "March 10, 2013*", - "model": "A1427 (EMC 2528)", - "device_id": "AppleTV3,1", - "order": "MD199LL/A", - "full_family": "3rd Gen" - }, - { - "full_name": "Apple TV (3rd Generation, Early 2013)", - "introduced": "January 29, 2013*", - "discontinued": "October 3, 2016**", - "model": "A1469 (EMC 2633)", - "device_id": "AppleTV3,2", - "order": "MD199LL/A", - "full_family": "3rd Gen" - }, - { - "full_name": "Apple TV HD (4th Generation, Siri)", - "introduced": "September 9, 2015*", - "discontinued": "October 18, 2022*", - "model": "A1625 (EMC 2907)", - "device_id": "AppleTV5,3", - "order": "MGY52LL/A*", - "full_family": "4th Gen" - }, - { - "full_name": "Apple TV 4K (2017, Black Siri Remote)", - "introduced": "September 12, 2017", - "discontinued": "April 20, 2021", - "model": "A1842 (EMC 3124)", - "device_id": "AppleTV6,2", - "order": "MQD22LL/A*", - "full_family": "4K" - }, - { - "full_name": "Apple TV 4K (2nd Gen, 2021)", - "introduced": "April 20, 2021*", - "discontinued": "October 18, 2022", - "model": "A2169 (EMC 3111)", - "device_id": "AppleTV11,1", - "order": "MXGY2LL/A*", - "full_family": "4K (2nd Gen)" - }, - { - "full_name": "Apple TV 4K (3rd Gen, 2022, Wi-Fi Only)", - "introduced": "October 18, 2022*", - "discontinued": "N/A", - "model": "A2737 (EMC 8101)", - "device_id": "AppleTV14,1", - "order": "MN873LL/A", - "full_family": "4K (3rd Gen)" - }, - { - "full_name": "Apple TV 4K (3rd Gen, 2022, Wi-Fi + Ethernet)", - "introduced": "October 18, 2022*", - "discontinued": "N/A", - "model": "A2843 (EMC 8165)", - "device_id": "AppleTV14,1", - "order": "MN893LL/A", - "full_family": "4K (3rd Gen)" - }, - { - "full_name": "Apple Watch Series 0 (Regular - Steel, 38 mm)", - "introduced": "March 9, 2015*", - "discontinued": "September 7, 2016", - "model": "A1553 (EMC 2870*)", - "device_id": "Watch1,1", - "order": "MJ302LL/A*", - "full_family": "Watch Standard 38mm" - }, - { - "full_name": "Apple Watch Series 0 (Regular - Steel, 42 mm)", - "introduced": "March 9, 2015*", - "discontinued": "September 7, 2016", - "model": "A1554 (EMC 2871*)", - "device_id": "Watch1,2", - "order": "MJ3V2LL/A*", - "full_family": "Watch Standard 42mm" - }, - { - "full_name": "Apple Watch Series 0 Sport (Aluminum, 38 mm)", - "introduced": "March 9, 2015*", - "discontinued": "September 7, 2016", - "model": "A1553 (EMC 2870*)", - "device_id": "Watch1,1", - "order": "MJ2T2LL/A*", - "full_family": "Watch Sport 38mm" - }, - { - "full_name": "Apple Watch Series 0 Sport (Aluminum, 42 mm)", - "introduced": "March 9, 2015*", - "discontinued": "September 7, 2016", - "model": "A1554 (EMC 2871*)", - "device_id": "Watch1,2", - "order": "MJ3N2LL/A*", - "full_family": "Watch Sport 42mm" - }, - { - "full_name": "Apple Watch Series 0 Edition (18k Gold, 38 mm)", - "introduced": "March 9, 2015*", - "discontinued": "September 7, 2016", - "model": "A1553 (EMC 2870*)", - "device_id": "Watch1,1", - "order": "MJ8P2LL/A*", - "full_family": "Watch Edition 38mm" - }, - { - "full_name": "Apple Watch Series 0 Edition (18k Gold, 42 mm)", - "introduced": "March 9, 2015*", - "discontinued": "September 7, 2016", - "model": "A1554 (EMC 2871*)", - "device_id": "Watch1,2", - "order": "MJ4A2LL/A*", - "full_family": "Watch Edition 42mm" - }, - { - "full_name": "Apple Watch Series 0 Hermes (Steel, 38 mm)", - "introduced": "September 9, 2015*", - "discontinued": "September 7, 2016", - "model": "A1553 (EMC 2870*)", - "device_id": "Watch1,1", - "order": "MLCN2LL/A*", - "full_family": "Watch Herm\u0018®s 38mm" - }, - { - "full_name": "Apple Watch Series 0 Hermes (Steel, 42 mm)", - "introduced": "September 9, 2015*", - "discontinued": "September 7, 2016", - "model": "A1554 (EMC 2871*)", - "device_id": "Watch1,2", - "order": "MLCC2LL/A*", - "full_family": "Watch Herm\u0018®s 42mm" - }, - { - "full_name": "Apple Watch Series 1 (Aluminum, 38 mm)", - "introduced": "September 7, 2016*", - "discontinued": "September 12, 2018", - "model": "A1802* (EMC 3102*)", - "device_id": "Watch2,6", - "order": "MNNG2LL/A*", - "full_family": "Watch Series 1 38mm" - }, - { - "full_name": "Apple Watch Series 1 (Aluminum, 42 mm)", - "introduced": "September 7, 2016*", - "discontinued": "September 12, 2018", - "model": "A1803* (EMC 3103*)", - "device_id": "Watch2,7", - "order": "MNNL2LL/A*", - "full_family": "Watch Series 1 42mm" - }, - { - "full_name": "Apple Watch Series 2 (Aluminum/Steel, 38 mm)", - "introduced": "September 7, 2016*", - "discontinued": "September 12, 2017", - "model": "A1757* (EMC 3104*)", - "device_id": "Watch2,3", - "order": "MNNW2LL/A*", - "full_family": "Watch Series 2 38mm" - }, - { - "full_name": "Apple Watch Series 2 (Aluminum/Steel, 42 mm)", - "introduced": "September 7, 2016*", - "discontinued": "September 12, 2017", - "model": "A1758* (EMC 3105*)", - "device_id": "Watch2,4", - "order": "MNPJ2LL/A*", - "full_family": "Watch Series 2 42mm" - }, - { - "full_name": "Apple Watch Series 2 (Nike+, 38 mm)", - "introduced": "September 7, 2016*", - "discontinued": "September 12, 2017", - "model": "A1757* (EMC 3104*)", - "device_id": "Watch2,3", - "order": "MP082LL/A*", - "full_family": "Watch Series 2 38mm" - }, - { - "full_name": "Apple Watch Series 2 (Nike+, 42 mm)", - "introduced": "September 7, 2016*", - "discontinued": "September 12, 2017", - "model": "A1758* (EMC 3105*)", - "device_id": "Watch2,4", - "order": "MP0A2LL/A*", - "full_family": "Watch Series 2 42mm" - }, - { - "full_name": "Apple Watch Series 2 (Hermes, 38 mm)", - "introduced": "September 7, 2016*", - "discontinued": "September 12, 2017", - "model": "A1757* (EMC 3104*)", - "device_id": "Watch2,3", - "order": "MNQ72LL/A*", - "full_family": "Watch Series 2 38mm" - }, - { - "full_name": "Apple Watch Series 2 (Hermes, 42 mm)", - "introduced": "September 7, 2016*", - "discontinued": "September 12, 2017", - "model": "A1758* (EMC 3105*)", - "device_id": "Watch2,4", - "order": "MNQC2LL/A*", - "full_family": "Watch Series 2 42mm" - }, - { - "full_name": "Apple Watch Series 2 (Edition, 38 mm)", - "introduced": "September 7, 2016*", - "discontinued": "September 12, 2017", - "model": "A1816* (EMC 3109*)", - "device_id": "Watch2,3", - "order": "MNPF2LL/A", - "full_family": "Watch Series 2 38mm" - }, - { - "full_name": "Apple Watch Series 2 (Edition, 42 mm)", - "introduced": "September 7, 2016*", - "discontinued": "September 12, 2017", - "model": "A1817* (EMC 3110*)", - "device_id": "Watch2,4", - "order": "MNPQ2LL/A", - "full_family": "Watch Series 2 42mm" - }, - { - "full_name": "Apple Watch Series 3 (Aluminum, GPS, 38 mm)", - "introduced": "September 12, 2017", - "discontinued": "September 7, 2022", - "model": "A1858* (EMC 3165*)", - "device_id": "Watch3,3", - "order": "MQKU2LL/A**", - "full_family": "Watch Series 3 38 mm" - }, - { - "full_name": "Apple Watch Series 3 (Aluminum, GPS, 42 mm)", - "introduced": "September 12, 2017", - "discontinued": "September 7, 2022", - "model": "A1859* (EMC 3166*)", - "device_id": "Watch3,4", - "order": "MQL02LL/A**", - "full_family": "Watch Series 3 42 mm" - }, - { - "full_name": "Apple Watch Series 3 (Cellular, US/CA, 38 mm)", - "introduced": "September 12, 2017", - "discontinued": "September 15, 2020", - "model": "A1860* (EMC 3169*)", - "device_id": "Watch3,1", - "order": "MQJN2LL/A**", - "full_family": "Watch Series 3 38 mm" - }, - { - "full_name": "Apple Watch Series 3 (Cellular, US/CA, 42 mm)", - "introduced": "September 12, 2017", - "discontinued": "September 15, 2020", - "model": "A1861* (EMC 3167*)", - "device_id": "Watch3,2", - "order": "MQK12LL/A**", - "full_family": "Watch Series 3 42 mm" - }, - { - "full_name": "Apple Watch Series 3 (Cellular, Intl, 38 mm)", - "introduced": "September 12, 2017", - "discontinued": "September 15, 2020", - "model": "A1889* (EMC 3169*)", - "device_id": "Watch3,1", - "order": "MQKF2B/A**", - "full_family": "Watch Series 3 38 mm" - }, - { - "full_name": "Apple Watch Series 3 (Cellular, Intl, 42 mm)", - "introduced": "September 12, 2017", - "discontinued": "September 15, 2020", - "model": "A1891* (EMC 3167*)", - "device_id": "Watch3,2", - "order": "MQKM2B/A**", - "full_family": "Watch Series 3 42 mm" - }, - { - "full_name": "Apple Watch Series 3 (Cellular, China, 38 mm)", - "introduced": "September 12, 2017", - "discontinued": "September 15, 2020", - "model": "A1890* (EMC 3169*)", - "device_id": "Watch3,1", - "order": "MQQE2CH/A**", - "full_family": "Watch Series 3 38 mm" - }, - { - "full_name": "Apple Watch Series 3 (Cellular, China, 42 mm)", - "introduced": "September 12, 2017", - "discontinued": "September 15, 2020", - "model": "A1892* (EMC 3167*)", - "device_id": "Watch3,2", - "order": "MQQR2CH/A**", - "full_family": "Watch Series 3 42 mm" - }, - { - "full_name": "Apple Watch Series 3 (Nike+, GPS, 38 mm)", - "introduced": "September 12, 2017", - "discontinued": "September 15, 2020", - "model": "A1858* (EMC 3165*)", - "device_id": "Watch3,3", - "order": "MQKX2LL/A**", - "full_family": "Watch Series 3 38 mm" - }, - { - "full_name": "Apple Watch Series 3 (Nike+, GPS, 42 mm)", - "introduced": "September 12, 2017", - "discontinued": "September 15, 2020", - "model": "A1859* (EMC 3166*)", - "device_id": "Watch3,4", - "order": "MQL32LL/A**", - "full_family": "Watch Series 3 42 mm" - }, - { - "full_name": "Apple Watch Series 3 (Nike+, US/CA, 38 mm)", - "introduced": "September 12, 2017", - "discontinued": "September 15, 2020", - "model": "A1860* (EMC 3169*)", - "device_id": "Watch3,1", - "order": "MQL52LL/A**", - "full_family": "Watch Series 3 38 mm" - }, - { - "full_name": "Apple Watch Series 3 (Nike+, US/CA, 42 mm)", - "introduced": "September 12, 2017", - "discontinued": "September 15, 2020", - "model": "A1861* (EMC 3167*)", - "device_id": "Watch3,2", - "order": "MQLC2LL/A**", - "full_family": "Watch Series 3 42 mm" - }, - { - "full_name": "Apple Watch Series 3 (Nike+, Intl, 38 mm)", - "introduced": "September 12, 2017", - "discontinued": "September 15, 2020", - "model": "A1889* (EMC 3169*)", - "device_id": "Watch3,1", - "order": "MQM72B/A**", - "full_family": "Watch Series 3 38 mm" - }, - { - "full_name": "Apple Watch Series 3 (Nike+, Intl, 42 mm)", - "introduced": "September 12, 2017", - "discontinued": "September 15, 2020", - "model": "A1891* (EMC 3167*)", - "device_id": "Watch3,2", - "order": "MQME2B/A**", - "full_family": "Watch Series 3 42 mm" - }, - { - "full_name": "Apple Watch Series 3 (Nike+, China, 38 mm)", - "introduced": "September 12, 2017", - "discontinued": "September 15, 2020", - "model": "A1890* (EMC 3169*)", - "device_id": "Watch3,1", - "order": "MQR42CH/A**", - "full_family": "Watch Series 3 38 mm" - }, - { - "full_name": "Apple Watch Series 3 (Nike+, China, 42 mm)", - "introduced": "September 12, 2017", - "discontinued": "September 15, 2020", - "model": "A1892* (EMC 3167*)", - "device_id": "Watch3,2", - "order": "MQRG2CH/A**", - "full_family": "Watch Series 3 42 mm" - }, - { - "full_name": "Apple Watch Series 3 (Hermes, US/CA, 38 mm)", - "introduced": "September 12, 2017", - "discontinued": "September 12, 2018", - "model": "A1860* (EMC 3169*)", - "device_id": "Watch3,1", - "order": "MQLN2LL/A**", - "full_family": "Watch Series 3 38 mm" - }, - { - "full_name": "Apple Watch Series 3 (Hermes, US/CA, 42 mm)", - "introduced": "September 12, 2017", - "discontinued": "September 12, 2018", - "model": "A1861* (EMC 3167*)", - "device_id": "Watch3,2", - "order": "MQLU2LL/A**", - "full_family": "Watch Series 3 42 mm" - }, - { - "full_name": "Apple Watch Series 3 (Hermes, Intl, 38 mm)", - "introduced": "September 12, 2017", - "discontinued": "September 12, 2018", - "model": "A1889* (EMC 3169*)", - "device_id": "Watch3,1", - "order": "MQMQ2B/A**", - "full_family": "Watch Series 3 38 mm" - }, - { - "full_name": "Apple Watch Series 3 (Hermes, Intl, 42 mm)", - "introduced": "September 12, 2017", - "discontinued": "September 12, 2018", - "model": "A1891* (EMC 3167*)", - "device_id": "Watch3,2", - "order": "MQMW2B/A**", - "full_family": "Watch Series 3 42 mm" - }, - { - "full_name": "Apple Watch Series 3 (Hermes, China, 38 mm)", - "introduced": "September 12, 2017", - "discontinued": "September 12, 2018", - "model": "A1890* (EMC 3169*)", - "device_id": "Watch3,1", - "order": "MQRF2CH/A**", - "full_family": "Watch Series 3 38 mm" - }, - { - "full_name": "Apple Watch Series 3 (Hermes, China, 42 mm)", - "introduced": "September 12, 2017", - "discontinued": "September 12, 2018", - "model": "A1892* (EMC 3167*)", - "device_id": "Watch3,2", - "order": "MQRT2CH/A**", - "full_family": "Watch Series 3 42 mm" - }, - { - "full_name": "Apple Watch Series 3 (Edition, US/CA, 38 mm)", - "introduced": "September 12, 2017", - "discontinued": "September 12, 2018", - "model": "A1860* (EMC 3170*)", - "device_id": "Watch3,1", - "order": "MQJY2LL/A**", - "full_family": "Watch Series 3 38 mm" - }, - { - "full_name": "Apple Watch Series 3 (Edition, US/CA, 42 mm)", - "introduced": "September 12, 2017", - "discontinued": "September 12, 2018", - "model": "A1861* (EMC 3168*)", - "device_id": "Watch3,2", - "order": "MQKD2LL/A**", - "full_family": "Watch Series 3 42 mm" - }, - { - "full_name": "Apple Watch Series 3 (Edition, Intl, 38 mm)", - "introduced": "September 12, 2017", - "discontinued": "September 12, 2018", - "model": "A1889* (EMC 3170*)", - "device_id": "Watch3,1", - "order": "MQM32B/A**", - "full_family": "Watch Series 3 38 mm" - }, - { - "full_name": "Apple Watch Series 3 (Edition, Intl, 42 mm)", - "introduced": "September 12, 2017", - "discontinued": "September 12, 2018", - "model": "A1891* (EMC 3168*)", - "device_id": "Watch3,2", - "order": "MQM52B/A**", - "full_family": "Watch Series 3 42 mm" - }, - { - "full_name": "Apple Watch Series 3 (Edition, China, 38 mm)", - "introduced": "September 12, 2017", - "discontinued": "September 12, 2018", - "model": "A1890* (EMC 3170*)", - "device_id": "Watch3,1", - "order": "MQQP2CH/A**", - "full_family": "Watch Series 3 38 mm" - }, - { - "full_name": "Apple Watch Series 3 (Edition, China, 42 mm)", - "introduced": "September 12, 2017", - "discontinued": "September 12, 2018", - "model": "A1892* (EMC 3168*)", - "device_id": "Watch3,2", - "order": "MQR22CH/A**", - "full_family": "Watch Series 3 42 mm" - }, - { - "full_name": "Apple Watch Series 4 (Aluminum, GPS, 40 mm)", - "introduced": "September 12, 2018*", - "discontinued": "September 10, 2019", - "model": "A1977* (EMC 3225*)", - "device_id": "Watch4,1", - "order": "MU642LL/A**", - "full_family": "Watch Series 4 40 mm" - }, - { - "full_name": "Apple Watch Series 4 (Aluminum, GPS, 44 mm)", - "introduced": "September 12, 2018*", - "discontinued": "September 10, 2019", - "model": "A1978* (EMC 3229*)", - "device_id": "Watch4,2", - "order": "MU6A2LL/A**", - "full_family": "Watch Series 4 44 mm" - }, - { - "full_name": "Apple Watch Series 4 (Cellular, US/CA, 40 mm)", - "introduced": "September 12, 2018*", - "discontinued": "September 10, 2019", - "model": "A1975* (EMC 3227*)", - "device_id": "Watch4,3", - "order": "MTUD2LL/A**", - "full_family": "Watch Series 4 40 mm" - }, - { - "full_name": "Apple Watch Series 4 (Cellular, US/CA, 44 mm)", - "introduced": "September 12, 2018*", - "discontinued": "September 10, 2019", - "model": "A1976* (EMC 3228*)", - "device_id": "Watch4,4", - "order": "MTUU2LL/A**", - "full_family": "Watch Series 4 44 mm" - }, - { - "full_name": "Apple Watch Series 4 (Cellular, Global, 40 mm)", - "introduced": "September 12, 2018*", - "discontinued": "September 10, 2019", - "model": "A2007* (EMC 3227*)", - "device_id": "Watch4,3", - "order": "MTVA2B/A**", - "full_family": "Watch Series 4 40 mm" - }, - { - "full_name": "Apple Watch Series 4 (Cellular, Global, 44 mm)", - "introduced": "September 12, 2018*", - "discontinued": "September 10, 2019", - "model": "A2008* (EMC 3228*)", - "device_id": "Watch4,4", - "order": "MTVR2B/A**", - "full_family": "Watch Series 4 44 mm" - }, - { - "full_name": "Apple Watch Series 4 (Nike+, GPS, 40 mm)", - "introduced": "September 12, 2018*", - "discontinued": "September 10, 2019", - "model": "A1977* (EMC 3225*)", - "device_id": "Watch4,1", - "order": "MU6H2LL/A**", - "full_family": "Watch Series 4 40 mm" - }, - { - "full_name": "Apple Watch Series 4 (Nike+, GPS, 44 mm)", - "introduced": "September 12, 2018*", - "discontinued": "September 10, 2019", - "model": "A1978* (EMC 3229*)", - "device_id": "Watch4,2", - "order": "MU6K2LL/A**", - "full_family": "Watch Series 4 44 mm" - }, - { - "full_name": "Apple Watch Series 4 (Nike+, US/CA, 40 mm)", - "introduced": "September 12, 2018*", - "discontinued": "September 10, 2019", - "model": "A1975* (EMC 3227*)", - "device_id": "Watch4,3", - "order": "MTV92LL/A**", - "full_family": "Watch Series 4 40 mm" - }, - { - "full_name": "Apple Watch Series 4 (Nike+, US/CA, 44 mm)", - "introduced": "September 12, 2018*", - "discontinued": "September 10, 2019", - "model": "A1976* (EMC 3228*)", - "device_id": "Watch4,4", - "order": "MTXC2LL/A**", - "full_family": "Watch Series 4 44 mm" - }, - { - "full_name": "Apple Watch Series 4 (Nike+, Global, 40 mm)", - "introduced": "September 12, 2018*", - "discontinued": "September 10, 2019", - "model": "A2007* (EMC 3227*)", - "device_id": "Watch4,3", - "order": "MTX62B/A**", - "full_family": "Watch Series 4 40 mm" - }, - { - "full_name": "Apple Watch Series 4 (Nike+, Global, 44 mm)", - "introduced": "September 12, 2018*", - "discontinued": "September 10, 2019", - "model": "A2008* (EMC 3228*)", - "device_id": "Watch4,4", - "order": "MTXK2B/A**", - "full_family": "Watch Series 4 44 mm" - }, - { - "full_name": "Apple Watch Series 4 (Hermes, US/CA, 40 mm)", - "introduced": "September 12, 2018*", - "discontinued": "September 10, 2019", - "model": "A1975* (EMC 3227*)", - "device_id": "Watch4,3", - "order": "MU6N2LL/A**", - "full_family": "Watch Series 4 40 mm" - }, - { - "full_name": "Apple Watch Series 4 (Hermes, US/CA, 44 mm)", - "introduced": "September 12, 2018*", - "discontinued": "September 10, 2019", - "model": "A1976* (EMC 3228*)", - "device_id": "Watch4,4", - "order": "MU6X2LL/A**", - "full_family": "Watch Series 4 44 mm" - }, - { - "full_name": "Apple Watch Series 4 (Hermes, Global, 40 mm)", - "introduced": "September 12, 2018*", + "full_name": "iPad 9.7\" 6th Gen (Wi-Fi Only)", + "introduced": "March 27, 2018", "discontinued": "September 10, 2019", - "model": "A2007* (EMC 3227*)", - "device_id": "Watch4,3", - "order": "MU702B/A**", - "full_family": "Watch Series 4 40 mm" + "model": "A1893 (EMC 3210*)", + "device_id": "iPad7,5", + "order": "MR7G2LL/A*", + "full_family": "iPad 6th Gen (Wi-Fi)" }, { - "full_name": "Apple Watch Series 4 (Hermes, Global, 44 mm)", - "introduced": "September 12, 2018*", + "full_name": "iPad 9.7\" 6th Gen (Wi-Fi/Cellular)", + "introduced": "March 27, 2018", "discontinued": "September 10, 2019", - "model": "A2008* (EMC 3228*)", - "device_id": "Watch4,4", - "order": "MU782B/A**", - "full_family": "Watch Series 4 44 mm" - }, - { - "full_name": "Apple Watch Series 5 (Aluminum, GPS, 40 mm)", - "introduced": "September 10, 2019*", - "discontinued": "September 15, 2020", - "model": "A2092* (EMC 3317*)", - "device_id": "Watch5,1", - "order": "MWV82LL/A**", - "full_family": "Watch Series 5 40 mm" - }, - { - "full_name": "Apple Watch Series 5 (Aluminum, GPS, 44 mm)", - "introduced": "September 10, 2019*", - "discontinued": "September 15, 2020", - "model": "A2093* (EMC 3318*)", - "device_id": "Watch5,2", - "order": "MWVF2LL/A**", - "full_family": "Watch Series 5 44 mm" + "model": "A1954 (EMC 3211*)", + "device_id": "iPad7,6", + "order": "MR702LL/A*", + "full_family": "iPad 6th Gen (Wi-Fi + Cellular)" }, { - "full_name": "Apple Watch Series 5 (Cellular, US/CA, 40 mm)", - "introduced": "September 10, 2019*", - "discontinued": "September 15, 2020", - "model": "A2094* (EMC 3319*)", - "device_id": "Watch5,3", - "order": "MWWQ2LL/A**", - "full_family": "Watch Series 5 40 mm" + "full_name": "iPad A16 - 11th Gen", + "introduced": "March 4, 2025", + "discontinued": "N/A", + "model": "A3354/A3355/A3356/A3266/A3267/A3270/A3268/A3269/A3271 (EMC 8805*)", + "device_id": "Pending", + "order": "Pending", + "full_family": "iPad A16" }, { - "full_name": "Apple Watch Series 5 (Cellular, US/CA, 44 mm)", - "introduced": "September 10, 2019*", - "discontinued": "September 15, 2020", - "model": "A2095* (EMC 3320*)", - "device_id": "Watch5,4", - "order": "MWW12LL/A**", - "full_family": "Watch Series 5 44 mm" + "full_name": "Apple TV (3rd Generation, Early 2012)", + "introduced": "March 7, 2012*", + "discontinued": "March 10, 2013*", + "model": "A1427 (EMC 2528)", + "device_id": "AppleTV3,1", + "order": "MD199LL/A", + "full_family": "3rd Gen" }, { - "full_name": "Apple Watch Series 5 (Cellular, Global, 40 mm)", - "introduced": "September 10, 2019*", - "discontinued": "September 15, 2020", - "model": "A2156* (EMC 3319*)", - "device_id": "Watch5,3", - "order": "MWX32B/A**", - "full_family": "Watch Series 5 40 mm" + "full_name": "iPad 3rd Gen (Wi-Fi Only)", + "introduced": "March 7, 2012*", + "discontinued": "October 23, 2012", + "model": "A1416 (EMC 2498)", + "device_id": "iPad3,1", + "order": "MC705LL/A*", + "full_family": "iPad 3rd Gen" }, { - "full_name": "Apple Watch Series 5 (Cellular, Global, 44 mm)", - "introduced": "September 10, 2019*", - "discontinued": "September 15, 2020", - "model": "A2157* (EMC 3320*)", - "device_id": "Watch5,4", - "order": "MWWE2B/A**", - "full_family": "Watch Series 5 44 mm" + "full_name": "iPad 3rd Gen (Wi-Fi/Cellular Verizon/GPS)", + "introduced": "March 7, 2012*", + "discontinued": "October 23, 2012", + "model": "A1403 (EMC 2499)", + "device_id": "iPad3,2", + "order": "MC733LL/A*", + "full_family": "iPad 3rd Gen (Wi-Fi + Cellular VZ)" }, - { - "full_name": "Apple Watch Series 5 (Nike+, GPS, 40 mm)", - "introduced": "September 10, 2019*", - "discontinued": "September 15, 2020", - "model": "A2092* (EMC 3317*)", - "device_id": "Watch5,1", - "order": "MX3R2LL/A**", - "full_family": "Watch Series 5 40 mm" + { + "full_name": "iPad 3rd Gen (Wi-Fi/Cellular AT&T/GPS)", + "introduced": "March 7, 2012*", + "discontinued": "October 23, 2012", + "model": "A1430 (EMC 2578)", + "device_id": "iPad3,3", + "order": "MD366LL/A*", + "full_family": "iPad 3rd Gen (Wi-Fi + Cellular)" }, { - "full_name": "Apple Watch Series 5 (Nike+, GPS, 44 mm)", - "introduced": "September 10, 2019*", - "discontinued": "September 15, 2020", - "model": "A2093* (EMC 3318*)", - "device_id": "Watch5,2", - "order": "MX3V2LL/A**", - "full_family": "Watch Series 5 44 mm" + "full_name": "iPad Air 5th Gen (Wi-Fi Only)", + "introduced": "March 8, 2022", + "discontinued": "May 7, 2024", + "model": "A2588 (EMC 4041*)", + "device_id": "iPad13,16", + "order": "MM9C3LL/A*", + "full_family": "iPad Air 5th Gen (Wi-Fi)" }, { - "full_name": "Apple Watch Series 5 (Nike+, US/CA, 40 mm)", - "introduced": "September 10, 2019*", - "discontinued": "September 15, 2020", - "model": "A2094* (EMC 3319*)", - "device_id": "Watch5,3", - "order": "MX372LL/A**", - "full_family": "Watch Series 5 40 mm" + "full_name": "iPad Air 5th Gen", + "introduced": "March 8, 2022", + "discontinued": "May 7, 2024", + "model": "A2589/A2591 (EMC 4042*)", + "device_id": "iPad13,17", + "order": "MM6R3LL/A*", + "full_family": "iPad Air 5th Gen" }, { - "full_name": "Apple Watch Series 5 (Nike+, US/CA, 44 mm)", - "introduced": "September 10, 2019*", - "discontinued": "September 15, 2020", - "model": "A2095* (EMC 3320*)", - "device_id": "Watch5,4", - "order": "MX392LL/A**", - "full_family": "Watch Series 5 44 mm" + "full_name": "iPhone SE", + "introduced": "March 8, 2022", + "discontinued": "February 19, 2025", + "model": "A2595/A2782/A2783/A2785 (EMC 4082*)", + "device_id": "iPhone14,6", + "order": "MMX63LL/A*", + "full_family": "iPhone SE" }, { - "full_name": "Apple Watch Series 5 (Nike+, Global, 40 mm)", - "introduced": "September 10, 2019*", - "discontinued": "September 15, 2020", - "model": "A2156* (EMC 3319*)", - "device_id": "Watch5,3", - "order": "MX3C2B/A**", - "full_family": "Watch Series 5 40 mm" + "full_name": "Apple Watch Series 0", + "introduced": "March 9, 2015*", + "discontinued": "September 7, 2016", + "model": "A1553 (EMC 2870*)", + "device_id": "Watch1,1", + "order": "MJ302LL/A*", + "full_family": "Watch Standard 38mm" }, { - "full_name": "Apple Watch Series 5 (Nike+, Global, 44 mm)", - "introduced": "September 10, 2019*", - "discontinued": "September 15, 2020", - "model": "A2157* (EMC 3320*)", - "device_id": "Watch5,4", - "order": "MX3E2B/A**", - "full_family": "Watch Series 5 44 mm" + "full_name": "Apple Watch Series 0", + "introduced": "March 9, 2015*", + "discontinued": "September 7, 2016", + "model": "A1554 (EMC 2871*)", + "device_id": "Watch1,2", + "order": "MJ3V2LL/A*", + "full_family": "Watch Standard 42mm" }, { - "full_name": "Apple Watch Series 5 (Hermes, US/CA, 40 mm)", - "introduced": "September 10, 2019*", - "discontinued": "September 15, 2020", - "model": "A2094* (EMC 3319*)", - "device_id": "Watch5,3", - "order": "MWX02LL/A**", - "full_family": "Watch Series 5 40 mm" + "full_name": "iPad Air M2 13\"", + "introduced": "May 7, 2024", + "discontinued": "March 4, 2025", + "model": "A2898/A2904 (EMC 8394*)", + "device_id": "iPad14,10", + "order": "MV283LL/A*", + "full_family": "iPad Air 13\" M2" }, { - "full_name": "Apple Watch Series 5 (Hermes, US/CA, 44 mm)", - "introduced": "September 10, 2019*", - "discontinued": "September 15, 2020", - "model": "A2095* (EMC 3320*)", - "device_id": "Watch5,4", - "order": "MWW92LL/A**", - "full_family": "Watch Series 5 44 mm" + "full_name": "iPad Air M2 13\"", + "introduced": "May 7, 2024", + "discontinued": "March 4, 2025", + "model": "A2899/A2900/A2905 (EMC 8395*)", + "device_id": "iPad14,11", + "order": "MV6R3LL/A*", + "full_family": "iPad Air 13\" M2" }, { - "full_name": "Apple Watch Series 5 (Hermes, Global, 40 mm)", - "introduced": "September 10, 2019*", - "discontinued": "September 15, 2020", - "model": "A2156* (EMC 3319*)", - "device_id": "Watch5,3", - "order": "MWXC2B/A**", - "full_family": "Watch Series 5 40 mm" + "full_name": "iPad Air M2 11\"", + "introduced": "May 7, 2024", + "discontinued": "March 4, 2025", + "model": "A2902 (EMC 8392*)", + "device_id": "iPad14,8", + "order": "MUWD3LL/A*", + "full_family": "iPad Air 11\" M2" }, { - "full_name": "Apple Watch Series 5 (Hermes, Global, 44 mm)", - "introduced": "September 10, 2019*", - "discontinued": "September 15, 2020", - "model": "A2157* (EMC 3320*)", - "device_id": "Watch5,4", - "order": "MWWM2B/A**", - "full_family": "Watch Series 5 44 mm" + "full_name": "iPad Air M2 11\"", + "introduced": "May 7, 2024", + "discontinued": "March 4, 2025", + "model": "A2903/A2904 (EMC 8393*)", + "device_id": "iPad14,9", + "order": "MUXE3LL/A*", + "full_family": "iPad Air 11\" M2" }, { - "full_name": "Apple Watch Series 5 (Edition, US/CA, 40 mm)", - "introduced": "September 10, 2019*", - "discontinued": "September 15, 2020", - "model": "A2094* (EMC 3319*)", - "device_id": "Watch5,3", - "order": "MWQ12LL/A**", - "full_family": "Watch Series 5 40 mm" + "full_name": "iPad Pro M4 11\"", + "introduced": "May 7, 2024", + "discontinued": "N/A", + "model": "A2836 (EMC 8472*)", + "device_id": "iPad16,3", + "order": "MVV93LL/A*", + "full_family": "iPad Pro 11\" M4" }, { - "full_name": "Apple Watch Series 5 (Edition, US/CA, 44 mm)", - "introduced": "September 10, 2019*", - "discontinued": "September 15, 2020", - "model": "A2095* (EMC 3320*)", - "device_id": "Watch5,4", - "order": "MWQT2LL/A**", - "full_family": "Watch Series 5 44 mm" + "full_name": "iPad Pro M4 11\"", + "introduced": "May 7, 2024", + "discontinued": "N/A", + "model": "A2837/A3006 (EMC 8473*)", + "device_id": "iPad16,4", + "order": "MVW23LL/A*", + "full_family": "iPad Pro 11\" M4" }, { - "full_name": "Apple Watch Series 5 (Edition, Global, 40 mm)", - "introduced": "September 10, 2019*", - "discontinued": "September 15, 2020", - "model": "A2156* (EMC 3319*)", - "device_id": "Watch5,3", - "order": "MWQE2B/A**", - "full_family": "Watch Series 5 40 mm" + "full_name": "iPad Pro M4 13\"", + "introduced": "May 7, 2024", + "discontinued": "N/A", + "model": "A2925/A2923 (EMC 8474*)", + "device_id": "iPad16,5", + "order": "MVX33LL/A*", + "full_family": "iPad Pro 13\" M4" }, { - "full_name": "Apple Watch Series 5 (Edition, Global, 44 mm)", - "introduced": "September 10, 2019*", - "discontinued": "September 15, 2020", - "model": "A2157* (EMC 3320*)", - "device_id": "Watch5,4", - "order": "MWR62B/A**", - "full_family": "Watch Series 5 44 mm" + "full_name": "iPad Pro M4 13\"", + "introduced": "May 7, 2024", + "discontinued": "N/A", + "model": "A2926/A3007/A2924 (EMC 8475*)", + "device_id": "iPad16,6", + "order": "MVXT3LL/A*", + "full_family": "iPad Pro 13\" M4" }, { - "full_name": "Apple Watch Series 6 (Aluminum, GPS, 40 mm)", - "introduced": "September 15, 2020", - "discontinued": "October 8, 2021*", - "model": "A2291* (EMC 3479*)", - "device_id": "Watch6,1", - "order": "MG143LL/A**", - "full_family": "Watch Series 6 40 mm" + "full_name": "Apple HomePod mini (Smart Speaker)", + "introduced": "October 13, 2020*", + "discontinued": "N/A", + "model": "A2374 (EMC 3506*)", + "device_id": "AudioAccessory5,1", + "order": "MY5H2LL/A*", + "full_family": "HomePod mini" }, { - "full_name": "Apple Watch Series 6 (Aluminum, GPS, 44 mm)", - "introduced": "September 15, 2020", - "discontinued": "October 8, 2021*", - "model": "A2292* (EMC 3480*)", - "device_id": "Watch6,2", - "order": "M00J3LL/A**", - "full_family": "Watch Series 6 44 mm" + "full_name": "iPhone 12 mini", + "introduced": "October 13, 2020*", + "discontinued": "September 7, 2022", + "model": "A2176/A2398/A2399/A2400 (EMC 3539*)", + "device_id": "iPhone13,1", + "order": "MG633LL/A*", + "full_family": "iPhone 12 mini" }, { - "full_name": "Apple Watch Series 6 (Cellular, US/CA, 40 mm)", - "introduced": "September 15, 2020", - "discontinued": "October 8, 2021*", - "model": "A2293* (EMC 3481*)", - "device_id": "Watch6,3", - "order": "M02R3LL/A**", - "full_family": "Watch Series 6 40 mm" + "full_name": "iPhone 12", + "introduced": "October 13, 2020*", + "discontinued": "September 12, 2023", + "model": "A2172/A2402/A2403/A2404 (EMC 3542*)", + "device_id": "iPhone13,2", + "order": "MGEK3LL/A*", + "full_family": "iPhone 12" }, { - "full_name": "Apple Watch Series 6 (Cellular, US/CA, 44 mm)", - "introduced": "September 15, 2020", - "discontinued": "October 8, 2021*", - "model": "A2294* (EMC 3482*)", - "device_id": "Watch6,4", - "order": "M07J3LL/A**", - "full_family": "Watch Series 6 44 mm" + "full_name": "iPhone 12 Pro", + "introduced": "October 13, 2020*", + "discontinued": "September 14, 2021", + "model": "A2341/A2406/A2407/A2408 (EMC 3545*)", + "device_id": "iPhone13,3", + "order": "MGJQ3LL/A*", + "full_family": "iPhone 12 Pro" }, { - "full_name": "Apple Watch Series 6 (Cellular, Global, 40 mm)", - "introduced": "September 15, 2020", - "discontinued": "October 8, 2021*", - "model": "A2375* (EMC 3481*)", - "device_id": "Watch6,3", - "order": "M06Q3B/A**", - "full_family": "Watch Series 6 40 mm" + "full_name": "iPhone 12 Pro Max", + "introduced": "October 13, 2020*", + "discontinued": "September 14, 2021", + "model": "A2342/A2410/A2411/A2412 (EMC 3548*)", + "device_id": "iPhone13,4", + "order": "MG913LL/A*", + "full_family": "iPhone 12 Pro Max" }, { - "full_name": "Apple Watch Series 6 (Cellular, Global, 44 mm)", - "introduced": "September 15, 2020", - "discontinued": "October 8, 2021*", - "model": "A2376* (EMC 3482*)", - "device_id": "Watch6,4", - "order": "M09A3B/A**", - "full_family": "Watch Series 6 44 mm" + "full_name": "iPad mini A17 Pro - 7th Gen", + "introduced": "October 15, 2024*", + "discontinued": "N/A", + "model": "A2993/A28xx (EMC 8470*)", + "device_id": "iPad16,1", + "order": "MXN73LL/A*", + "full_family": "iPad mini A17 Pro" }, { - "full_name": "Apple Watch Series 6 (Nike, GPS, 40 mm)", - "introduced": "September 15, 2020", - "discontinued": "October 8, 2021*", - "model": "A2291* (EMC 3479*)", - "device_id": "Watch6,1", - "order": "M00X3LL/A**", - "full_family": "Watch Series 6 40 mm" + "full_name": "iPad mini A17 Pro - 7th Gen", + "introduced": "October 15, 2024*", + "discontinued": "N/A", + "model": "A2995/A2996/A28xx (EMC 8471*)", + "device_id": "iPad16,2", + "order": "MXPP3LL/A*", + "full_family": "iPad mini A17 Pro" }, { - "full_name": "Apple Watch Series 6 (Nike, GPS, 44 mm)", - "introduced": "September 15, 2020", - "discontinued": "October 8, 2021*", - "model": "A2292* (EMC 3480*)", - "device_id": "Watch6,2", - "order": "MG173LL/A**", - "full_family": "Watch Series 6 44 mm" + "full_name": "iPad Pro 11\" (M5, 2025 Wi‑Fi)", + "introduced": "October 15, 2025", + "discontinued": "N/A", + "model": "A3357/A3358 (EMC 8853*)", + "device_id": "iPad17,1", + "order": "N/A*", + "full_family": "iPad Pro 11\" (M5, 2025)" }, { - "full_name": "Apple Watch Series 6 (Nike, US/CA, 40 mm)", - "introduced": "September 15, 2020", - "discontinued": "October 8, 2021*", - "model": "A2293* (EMC 3481*)", - "device_id": "Watch6,3", - "order": "M06L3LL/A**", - "full_family": "Watch Series 6 40 mm" + "full_name": "iPad Pro 11\" (M5, 2025 Wi‑Fi + Cellular China)", + "introduced": "October 15, 2025", + "discontinued": "N/A", + "model": "A3359 (EMC 8854*)", + "device_id": "iPad17,2", + "order": "N/A*", + "full_family": "iPad Pro 11\" (M5, 2025)" }, { - "full_name": "Apple Watch Series 6 (Nike, US/CA, 44 mm)", - "introduced": "September 15, 2020", - "discontinued": "October 8, 2021*", - "model": "A2294* (EMC 3482*)", - "device_id": "Watch6,4", - "order": "MG2J3LL/A**", - "full_family": "Watch Series 6 44 mm" + "full_name": "iPad Pro 13\" (M5, 2025 Wi‑Fi + Cellular China)", + "introduced": "October 15, 2025", + "discontinued": "N/A", + "model": "A3362 (EMC 8857*)", + "device_id": "iPad17,3", + "order": "N/A*", + "full_family": "iPad Pro 13\" (M5, 2025)" }, { - "full_name": "Apple Watch Series 6 (Nike, Global, 40 mm)", - "introduced": "September 15, 2020", - "discontinued": "October 8, 2021*", - "model": "A2375* (EMC 3481*)", - "device_id": "Watch6,3", - "order": "M07E3B/A**", - "full_family": "Watch Series 6 40 mm" + "full_name": "iPad Pro 13\" (M5, 2025 Wi‑Fi)", + "introduced": "October 15, 2025", + "discontinued": "N/A", + "model": "A3360/A3361 (EMC 8856*)", + "device_id": "iPad17,4", + "order": "N/A*", + "full_family": "iPad Pro 13\" (M5, 2025)" }, { - "full_name": "Apple Watch Series 6 (Nike, Global, 44 mm)", - "introduced": "September 15, 2020", - "discontinued": "October 8, 2021*", - "model": "A2376* (EMC 3482*)", - "device_id": "Watch6,4", - "order": "M09Y3B/A**", - "full_family": "Watch Series 6 44 mm" + "full_name": "iPad mini 3 (Wi-Fi Only)", + "introduced": "October 16, 2014", + "discontinued": "September 9, 2015", + "model": "A1599 (EMC 2848*)", + "device_id": "iPad4,7", + "order": "MGNV2LL/A*", + "full_family": "iPad mini 3 (Wi-Fi)" }, { - "full_name": "Apple Watch Series 6 (Hermes, US/CA, 40 mm)", - "introduced": "September 15, 2020", - "discontinued": "October 8, 2021*", - "model": "A2293* (EMC 3481*)", - "device_id": "Watch6,3", - "order": "MG2Y3LL/A**", - "full_family": "Watch Series 6 40 mm" + "full_name": "iPad mini 3 (Wi-Fi/Cellular)", + "introduced": "October 16, 2014", + "discontinued": "September 9, 2015", + "model": "A1600 (EMC 2849*)", + "device_id": "iPad4,8", + "order": "MH3F2LL/A*", + "full_family": "iPad mini 3 (Wi-Fi + Cellular)" }, { - "full_name": "Apple Watch Series 6 (Hermes, US/CA, 44 mm)", - "introduced": "September 15, 2020", - "discontinued": "October 8, 2021*", - "model": "A2294* (EMC 3482*)", - "device_id": "Watch6,4", - "order": "MG323LL/A**", - "full_family": "Watch Series 6 44 mm" + "full_name": "iPad Air 2 (Wi-Fi Only)", + "introduced": "October 16, 2014", + "discontinued": "March 21, 2017*", + "model": "A1566 (EMC 2822*)", + "device_id": "iPad5,3", + "order": "MGLW2LL/A*", + "full_family": "iPad Air 2 (Wi-Fi)" }, { - "full_name": "Apple Watch Series 6 (Hermes, Global, 40 mm)", - "introduced": "September 15, 2020", - "discontinued": "October 8, 2021*", - "model": "A2375* (EMC 3481*)", - "device_id": "Watch6,3", - "order": "MG343B/A**", - "full_family": "Watch Series 6 40 mm" + "full_name": "iPad Air 2 (Wi-Fi/Cellular)", + "introduced": "October 16, 2014", + "discontinued": "March 21, 2017*", + "model": "A1567 (EMC 2823*)", + "device_id": "iPad5,4", + "order": "MH2V2LL/A*", + "full_family": "iPad Air 2 (Wi-Fi + Cellular)" }, { - "full_name": "Apple Watch Series 6 (Hermes, Global, 44 mm)", - "introduced": "September 15, 2020", - "discontinued": "October 8, 2021*", - "model": "A2376* (EMC 3482*)", - "device_id": "Watch6,4", - "order": "MG3A3B/A**", - "full_family": "Watch Series 6 44 mm" + "full_name": "iPad mini 3 (Wi-Fi/Cellular, China)", + "introduced": "October 16, 2014*", + "discontinued": "September 9, 2015", + "model": "A1601 (EMC 2849*)", + "device_id": "iPad4,9", + "order": "MGPW2CH/A*", + "full_family": "iPad mini 3 (Wi-Fi + Cellular, China)" }, { - "full_name": "Apple Watch Series 6 (Edition, US/CA, 40 mm)", - "introduced": "September 15, 2020", - "discontinued": "October 8, 2021*", - "model": "A2293* (EMC 3481*)", - "device_id": "Watch6,3", - "order": "M0DH3LL/A**", - "full_family": "Watch Series 6 40 mm" + "full_name": "iPad 10.9\" 10th Gen (Wi-Fi Only)", + "introduced": "October 18, 2022", + "discontinued": "March 4, 2025", + "model": "A2696 (EMC 8137*)", + "device_id": "iPad13,18", + "order": "MPQ03LL/A*", + "full_family": "iPad 10th Gen (Wi-Fi)" }, { - "full_name": "Apple Watch Series 6 (Edition, US/CA, 44 mm)", - "introduced": "September 15, 2020", - "discontinued": "October 8, 2021*", - "model": "A2294* (EMC 3482*)", - "device_id": "Watch6,4", - "order": "M0GJ3LL/A**", - "full_family": "Watch Series 6 44 mm" + "full_name": "iPad 10.9\" 10th Gen", + "introduced": "October 18, 2022", + "discontinued": "March 4, 2025", + "model": "A2757/A2777 (EMC 8135*)", + "device_id": "iPad13,19", + "order": "MQ6J3LL/A*", + "full_family": "iPad 10th Gen" }, { - "full_name": "Apple Watch Series 6 (Edition, Global, 40 mm)", - "introduced": "September 15, 2020", - "discontinued": "October 8, 2021*", - "model": "A2375* (EMC 3481*)", - "device_id": "Watch6,3", - "order": "M0DY3B/A**", - "full_family": "Watch Series 6 40 mm" + "full_name": "iPad Pro 11\" (Wi-Fi Only - 4th Gen)", + "introduced": "October 18, 2022", + "discontinued": "May 7, 2024", + "model": "A2759 (EMC 8166*)", + "device_id": "iPad14,3", + "order": "MNXE3LL/A*", + "full_family": "iPad Pro 11\" (4th Gen - Wi-Fi)" }, { - "full_name": "Apple Watch Series 6 (Edition, Global, 44 mm)", - "introduced": "September 15, 2020", - "discontinued": "October 8, 2021*", - "model": "A2376* (EMC 3482*)", - "device_id": "Watch6,4", - "order": "M0H23B/A**", - "full_family": "Watch Series 6 44 mm" + "full_name": "iPad Pro 11\"", + "introduced": "October 18, 2022", + "discontinued": "May 7, 2024", + "model": "A2435/A2761/A2762 (EMC 8167*)", + "device_id": "iPad14,4", + "order": "MP563LL/A*", + "full_family": "iPad Pro 11\"" }, { - "full_name": "Apple Watch SE (GPS, 40 mm)", - "introduced": "September 15, 2020", - "discontinued": "September 7, 2022", - "model": "A2351* (EMC 3485*)", - "device_id": "Watch5,9", - "order": "MYDN2LL/A**", - "full_family": "Watch SE 40 mm" + "full_name": "iPad Pro 12.9\" (Wi-Fi Only - 6th Gen)", + "introduced": "October 18, 2022", + "discontinued": "May 7, 2024", + "model": "A2436 (EMC 8169*)", + "device_id": "iPad14,5", + "order": "MNXQ3LL/A*", + "full_family": "iPad Pro 12.9\" (6th Gen - Wi-Fi)" }, { - "full_name": "Apple Watch SE (GPS, 44 mm)", - "introduced": "September 15, 2020", - "discontinued": "September 7, 2022", - "model": "A2352* (EMC 3486*)", - "device_id": "Watch5,10", - "order": "MYDR2LL/A**", - "full_family": "Watch SE 44 mm" + "full_name": "iPad Pro 12.9\"", + "introduced": "October 18, 2022", + "discontinued": "May 7, 2024", + "model": "A2764/A2437/A2766 (EMC 8176*)", + "device_id": "iPad14,6", + "order": "MP5Y3LL/A*", + "full_family": "iPad Pro 12.9\"" }, { - "full_name": "Apple Watch SE (Cellular, US/CA, 40 mm)", - "introduced": "September 15, 2020", - "discontinued": "September 7, 2022", - "model": "A2353* (EMC 3487*)", - "device_id": "Watch5,11", - "order": "MYEA2LL/A**", - "full_family": "Watch SE 40 mm" + "full_name": "Apple TV 4K", + "introduced": "October 18, 2022*", + "discontinued": "N/A", + "model": "A2737/A2843 (EMC 8101)", + "device_id": "AppleTV14,1", + "order": "MN873LL/A", + "full_family": "4K" }, { - "full_name": "Apple Watch SE (Cellular, US/CA, 44 mm)", - "introduced": "September 15, 2020", - "discontinued": "September 7, 2022", - "model": "A2354* (EMC 3488*)", - "device_id": "Watch5,12", - "order": "MYEP2LL/A**", - "full_family": "Watch SE 44 mm" + "full_name": "iPad Air Wi-Fi Only", + "introduced": "October 22, 2013*", + "discontinued": "March 21, 2016**", + "model": "A1474 (EMC 2646*)", + "device_id": "iPad4,1", + "order": "MD785LL/A*", + "full_family": "iPad Air (Wi-Fi)" }, { - "full_name": "Apple Watch SE (Cellular, Global, 40 mm)", - "introduced": "September 15, 2020", - "discontinued": "September 7, 2022", - "model": "A2355* (EMC 3487*)", - "device_id": "Watch5,11", - "order": "MYEH2B/A**", - "full_family": "Watch SE 40 mm" + "full_name": "iPad Air Wi-Fi/Cellular", + "introduced": "October 22, 2013*", + "discontinued": "March 21, 2016**", + "model": "A1475 (EMC 2647*)", + "device_id": "iPad4,2", + "order": "ME991LL/A*", + "full_family": "iPad Air (Wi-Fi + Cellular)" }, { - "full_name": "Apple Watch SE (Cellular, Global, 44 mm)", - "introduced": "September 15, 2020", - "discontinued": "September 7, 2022", - "model": "A2356* (EMC 3488*)", - "device_id": "Watch5,12", - "order": "MYEX2B/A**", - "full_family": "Watch SE 44 mm" + "full_name": "iPad mini 2 (Retina/2nd Gen, Wi-Fi Only)", + "introduced": "October 22, 2013*", + "discontinued": "March 21, 2017*", + "model": "A1489 (EMC 2695*)", + "device_id": "iPad4,4", + "order": "ME276LL/A*", + "full_family": "iPad mini Retina (Wi-Fi)" }, { - "full_name": "Apple Watch SE (Nike, GPS, 40 mm)", - "introduced": "September 15, 2020", - "discontinued": "September 7, 2022", - "model": "A2351* (EMC 3485*)", - "device_id": "Watch5,9", - "order": "MYYF2LL/A**", - "full_family": "Watch SE 40 mm" + "full_name": "iPad mini 2 (Retina/2nd Gen, Wi-Fi/Cellular)", + "introduced": "October 22, 2013*", + "discontinued": "March 21, 2017*", + "model": "A1490 (EMC 2696*)", + "device_id": "iPad4,5", + "order": "MF066LL/A*", + "full_family": "iPad mini Retina (Wi-Fi + Cellular)" }, { - "full_name": "Apple Watch SE (Nike, GPS, 44 mm)", - "introduced": "September 15, 2020", - "discontinued": "September 7, 2022", - "model": "A2352* (EMC 3486*)", - "device_id": "Watch5,10", - "order": "MYYK2LL/A**", - "full_family": "Watch SE 44 mm" + "full_name": "iPad mini Wi-Fi Only/1st Gen", + "introduced": "October 23, 2012*", + "discontinued": "June 19, 2015**", + "model": "A1432 (EMC 2607*)", + "device_id": "iPad2,5", + "order": "MD528LL/A*", + "full_family": "iPad mini (Wi-Fi)" }, { - "full_name": "Apple Watch SE (Nike, US/CA, 40 mm)", - "introduced": "September 15, 2020", - "discontinued": "September 7, 2022", - "model": "A2353* (EMC 3487*)", - "device_id": "Watch5,11", - "order": "MYYU2LL/A**", - "full_family": "Watch SE 40 mm" + "full_name": "iPad mini Wi-Fi/AT&T/GPS - 1st Gen", + "introduced": "October 23, 2012*", + "discontinued": "June 19, 2015**", + "model": "A1454 (EMC 2608*)", + "device_id": "iPad2,6", + "order": "MD534LL/A*", + "full_family": "iPad mini (Wi-Fi + Cellular)" }, { - "full_name": "Apple Watch SE (Nike, US/CA, 44 mm)", - "introduced": "September 15, 2020", - "discontinued": "September 7, 2022", - "model": "A2354* (EMC 3488*)", - "device_id": "Watch5,12", - "order": "MG063LL/A**", - "full_family": "Watch SE 44 mm" + "full_name": "iPad mini Wi-Fi/VZ & Sprint/GPS - 1st Gen", + "introduced": "October 23, 2012*", + "discontinued": "June 19, 2015**", + "model": "A1455 (EMC 2609*)", + "device_id": "iPad2,7", + "order": "MD540LL/A*", + "full_family": "iPad mini (Wi-Fi + Cellular MM)" }, { - "full_name": "Apple Watch SE (Nike, Global, 40 mm)", - "introduced": "September 15, 2020", - "discontinued": "September 7, 2022", - "model": "A2355* (EMC 3487*)", - "device_id": "Watch5,11", - "order": "MG013B/A**", - "full_family": "Watch SE 40 mm" + "full_name": "iPad 4th Gen (Wi-Fi Only)", + "introduced": "October 23, 2012*", + "discontinued": "October 16, 2014**", + "model": "A1458 (EMC 2604*)", + "device_id": "iPad3,4", + "order": "MD510LL/A*", + "full_family": "iPad 4th Gen (Wi-Fi)" }, { - "full_name": "Apple Watch SE (Nike, Global, 44 mm)", - "introduced": "September 15, 2020", - "discontinued": "September 7, 2022", - "model": "A2356* (EMC 3488*)", - "device_id": "Watch5,12", - "order": "MG0A3B/A**", - "full_family": "Watch SE 44 mm" + "full_name": "iPad 4th Gen (Wi-Fi/AT&T/GPS)", + "introduced": "October 23, 2012*", + "discontinued": "October 16, 2014**", + "model": "A1459 (EMC 2605*)", + "device_id": "iPad3,5", + "order": "MD516LL/A*", + "full_family": "iPad 4th Gen (Wi-Fi + Cellular)" }, { - "full_name": "Apple Watch Series 7 (Aluminum, GPS, 41 mm)", - "introduced": "September 14, 2021*", - "discontinued": "September 7, 2022", - "model": "A2473* (EMC 3982*)", - "device_id": "Watch6,6", - "order": "MKN03LL/A**", - "full_family": "Watch Series 7 41 mm" + "full_name": "iPad 4th Gen (Wi-Fi/Verizon & Sprint/GPS)", + "introduced": "October 23, 2012*", + "discontinued": "October 16, 2014**", + "model": "A1460 (EMC 2606*)", + "device_id": "iPad3,6", + "order": "MD522LL/A*", + "full_family": "iPad 4th Gen (Wi-Fi + Cellular MM)" }, { - "full_name": "Apple Watch Series 7 (Aluminum, GPS, 45 mm)", - "introduced": "September 14, 2021*", - "discontinued": "September 7, 2022", - "model": "A2474* (EMC 3983*)", - "device_id": "Watch6,7", - "order": "MKN73LL/A**", - "full_family": "Watch Series 7 45 mm" + "full_name": "iPad (11th Gen, 2024 Wi‑Fi)", + "introduced": "October 29, 2024", + "discontinued": "N/A", + "model": "A29xx (EMC N/A*)", + "device_id": "iPad15,7", + "order": "N/A*", + "full_family": "iPad (11th Gen)" }, { - "full_name": "Apple Watch Series 7 (Cellular, US/CA, 41 mm)", - "introduced": "September 14, 2021*", - "discontinued": "September 7, 2022", - "model": "A2475* (EMC 3984*)", - "device_id": "Watch6,8", - "order": "MKHG3LL/A**", - "full_family": "Watch Series 7 41 mm" + "full_name": "iPad (11th Gen, 2024 Wi‑Fi + Cellular)", + "introduced": "October 29, 2024", + "discontinued": "N/A", + "model": "A29xx (EMC N/A*)", + "device_id": "iPad15,8", + "order": "N/A*", + "full_family": "iPad (11th Gen)" }, { - "full_name": "Apple Watch Series 7 (Cellular, US/CA, 45 mm)", - "introduced": "September 14, 2021*", - "discontinued": "September 7, 2022", - "model": "A2477* (EMC 3985*)", - "device_id": "Watch6,9", - "order": "MKJF3LL/A**", - "full_family": "Watch Series 7 45 mm" + "full_name": "iPhone 3G (China/No Wi-Fi)", + "introduced": "October 30, 2009*", + "discontinued": "August 9, 2010", + "model": "A1324 (EMC N/A)", + "device_id": "iPhone1,2*", + "order": "MC176CH/A", + "full_family": "iPhone 3G (China)" }, { - "full_name": "Apple Watch Series 7 (Cellular, Global, 41 mm)", - "introduced": "September 14, 2021*", - "discontinued": "September 7, 2022", - "model": "A2476* (EMC 3984*)", - "device_id": "Watch6,8", - "order": "MKHY3B/A**", - "full_family": "Watch Series 7 41 mm" + "full_name": "iPhone 3GS (China/No Wi-Fi)", + "introduced": "October 30, 2009*", + "discontinued": "August 9, 2010", + "model": "A1325 (EMC N/A)", + "device_id": "iPhone2,1*", + "order": "N/A", + "full_family": "iPhone 3GS (China)" }, { - "full_name": "Apple Watch Series 7 (Cellular, Global, 45 mm)", - "introduced": "September 14, 2021*", - "discontinued": "September 7, 2022", - "model": "A2478* (EMC 3985*)", - "device_id": "Watch6,9", - "order": "MKJX3B/A**", - "full_family": "Watch Series 7 45 mm" + "full_name": "iPad Pro 11\" (Wi-Fi Only)", + "introduced": "October 30, 2018*", + "discontinued": "March 18, 2020", + "model": "A1980 (EMC 3221*)", + "device_id": "iPad8,1, iPad8,2**", + "order": "MTXP2LL/A*", + "full_family": "iPad Pro 11\" (Wi-Fi)" }, { - "full_name": "Apple Watch Series 7 (Nike, GPS, 41 mm)", - "introduced": "September 14, 2021*", - "discontinued": "September 7, 2022", - "model": "A2473* (EMC 3982*)", - "device_id": "Watch6,6", - "order": "MKN33LL/A**", - "full_family": "Watch Series 7 41 mm" + "full_name": "iPad Pro 11\"", + "introduced": "October 30, 2018*", + "discontinued": "March 18, 2020", + "model": "A2013/A1934/A1979 (EMC 3222*)", + "device_id": "iPad8,3, iPad8,4**", + "order": "MU0Y2LL/A*", + "full_family": "iPad Pro 11\"" }, { - "full_name": "Apple Watch Series 7 (Nike, GPS, 45 mm)", - "introduced": "September 14, 2021*", - "discontinued": "September 7, 2022", - "model": "A2474* (EMC 3983*)", - "device_id": "Watch6,7", - "order": "MKNA3LL/A**", - "full_family": "Watch Series 7 45 mm" + "full_name": "iPad Pro 12.9\" (Wi-Fi Only - 3rd Gen)", + "introduced": "October 30, 2018*", + "discontinued": "March 18, 2020", + "model": "A1876 (EMC 3223*)", + "device_id": "iPad8,5, iPad8,6**", + "order": "MTEM2LL/A*", + "full_family": "iPad Pro 12.9\" (3rd Gen - Wi-Fi)" }, { - "full_name": "Apple Watch Series 7 (Nike, US/CA, 41 mm)", - "introduced": "September 14, 2021*", - "discontinued": "September 7, 2022", - "model": "A2475* (EMC 3984*)", - "device_id": "Watch6,8", - "order": "MKHL3LL/A**", - "full_family": "Watch Series 7 41 mm" + "full_name": "iPad Pro 12.9\"", + "introduced": "October 30, 2018*", + "discontinued": "March 18, 2020", + "model": "A2014/A1895/A1983 (EMC 3224*)", + "device_id": "iPad8,7, iPad8,8**", + "order": "MTHU2LL/A*", + "full_family": "iPad Pro 12.9\"" }, { - "full_name": "Apple Watch Series 7 (Nike, US/CA, 45 mm)", - "introduced": "September 14, 2021*", - "discontinued": "September 7, 2022", - "model": "A2477* (EMC 3985*)", - "device_id": "Watch6,9", - "order": "MKJK3LL/A**", - "full_family": "Watch Series 7 45 mm" + "full_name": "iPhone 4 (GSM, Revision A)", + "introduced": "October 4, 2011*", + "discontinued": "September 10, 2013*", + "model": "A1332 (EMC 380A/380B*)", + "device_id": "iPhone3,2", + "order": "MD126LL/A*", + "full_family": "iPhone 4" }, { - "full_name": "Apple Watch Series 7 (Nike, Global, 41 mm)", - "introduced": "September 14, 2021*", - "discontinued": "September 7, 2022", - "model": "A2476* (EMC 3984*)", - "device_id": "Watch6,8", - "order": "MKJ33B/A**", - "full_family": "Watch Series 7 41 mm" + "full_name": "iPhone 4S (4s*)", + "introduced": "October 4, 2011*", + "discontinued": "September 9, 2014*", + "model": "A1387 (EMC 2430)", + "device_id": "iPhone4,1", + "order": "MC918LL/A*", + "full_family": "iPhone 4S (4s)" }, { - "full_name": "Apple Watch Series 7 (Nike, Global, 45 mm)", - "introduced": "September 14, 2021*", - "discontinued": "September 7, 2022", - "model": "A2478* (EMC 3985*)", - "device_id": "Watch6,9", - "order": "MKL43B/A**", - "full_family": "Watch Series 7 45 mm" + "full_name": "Apple TV (2nd Generation)", + "introduced": "September 1, 2010*", + "discontinued": "March 7, 2012", + "model": "A1378 (EMC 2411)", + "device_id": "AppleTV2,1", + "order": "MC572LL/A", + "full_family": "2nd Gen" }, { - "full_name": "Apple Watch Series 7 (Hermes, US/CA, 41 mm)", - "introduced": "September 14, 2021*", - "discontinued": "September 7, 2022", - "model": "A2475* (EMC 3984*)", - "device_id": "Watch6,8", - "order": "MKLK3LL/A**", - "full_family": "Watch Series 7 41 mm" + "full_name": "iPhone 5c", + "introduced": "September 10, 2013*", + "discontinued": "September 9, 2015", + "model": "A1532/A1456 (EMC 2644*)", + "device_id": "iPhone5,3", + "order": "ME505LL/A*", + "full_family": "iPhone 5c" }, { - "full_name": "Apple Watch Series 7 (Hermes, US/CA, 45 mm)", - "introduced": "September 14, 2021*", - "discontinued": "September 7, 2022", - "model": "A2477* (EMC 3985*)", - "device_id": "Watch6,9", - "order": "MKMG3LL/A**", - "full_family": "Watch Series 7 45 mm" + "full_name": "iPhone 5c", + "introduced": "September 10, 2013*", + "discontinued": "September 9, 2015", + "model": "A1507/A1526/A1529/A1516 (EMC 2694*)", + "device_id": "iPhone5,4", + "order": "ME499B/A*", + "full_family": "iPhone 5c" }, { - "full_name": "Apple Watch Series 7 (Hermes, Global, 41 mm)", - "introduced": "September 14, 2021*", - "discontinued": "September 7, 2022", - "model": "A2476* (EMC 3984*)", - "device_id": "Watch6,8", - "order": "MKLY3B/A**", - "full_family": "Watch Series 7 41 mm" + "full_name": "iPhone 5s", + "introduced": "September 10, 2013*", + "discontinued": "March 21, 2016**", + "model": "A1533/A1453 (EMC 2642*)", + "device_id": "iPhone6,1", + "order": "ME305LL/A*", + "full_family": "iPhone 5s" }, { - "full_name": "Apple Watch Series 7 (Hermes, Global, 45 mm)", - "introduced": "September 14, 2021*", - "discontinued": "September 7, 2022", - "model": "A2478* (EMC 3985*)", - "device_id": "Watch6,9", - "order": "MKMV3B/A**", - "full_family": "Watch Series 7 45 mm" + "full_name": "iPhone 5s", + "introduced": "September 10, 2013*", + "discontinued": "March 21, 2016**", + "model": "A1457/A1528/A1530/A1518 (EMC 2643*)", + "device_id": "iPhone6,2", + "order": "ME432B/A*", + "full_family": "iPhone 5s" }, { - "full_name": "Apple Watch Series 7 (Edition, US/CA, 41 mm)", - "introduced": "September 14, 2021*", - "discontinued": "September 7, 2022", - "model": "A2475* (EMC 3984*)", - "device_id": "Watch6,8", - "order": "ML8U3LL/A**", - "full_family": "Watch Series 7 41 mm" + "full_name": "iPad 10.2\" 7th Gen (Wi-Fi Only)", + "introduced": "September 10, 2019", + "discontinued": "September 15, 2020", + "model": "A2197 (EMC 3323*)", + "device_id": "iPad7,11", + "order": "MW752LL/A*", + "full_family": "iPad 7th Gen (Wi-Fi)" }, { - "full_name": "Apple Watch Series 7 (Edition, US/CA, 45 mm)", - "introduced": "September 14, 2021*", - "discontinued": "September 7, 2022", - "model": "A2477* (EMC 3985*)", - "device_id": "Watch6,9", - "order": "ML8W3LL/A**", - "full_family": "Watch Series 7 45 mm" + "full_name": "iPad 10.2\" 7th Gen", + "introduced": "September 10, 2019", + "discontinued": "September 15, 2020", + "model": "A2200/A2198/A2199 (EMC 3324*)", + "device_id": "iPad7,12", + "order": "MW6X2LL/A*", + "full_family": "iPad 7th Gen" }, { - "full_name": "Apple Watch Series 7 (Edition, Global, 41 mm)", - "introduced": "September 14, 2021*", + "full_name": "iPhone 11", + "introduced": "September 10, 2019", "discontinued": "September 7, 2022", - "model": "A2476* (EMC 3984*)", - "device_id": "Watch6,8", - "order": "ML913B/A**", - "full_family": "Watch Series 7 41 mm" + "model": "A2111/A2221/A2223 (EMC 3309*)", + "device_id": "iPhone12,1", + "order": "MWHU2LL/A*", + "full_family": "iPhone 11" }, { - "full_name": "Apple Watch Series 7 (Edition, Global, 45 mm)", - "introduced": "September 14, 2021*", - "discontinued": "September 7, 2022", - "model": "A2478* (EMC 3985*)", - "device_id": "Watch6,9", - "order": "ML8Y3B/A**", - "full_family": "Watch Series 7 45 mm" + "full_name": "iPhone 11 Pro", + "introduced": "September 10, 2019", + "discontinued": "October 13, 2020", + "model": "A2160/A2215/A2217 (EMC 3305*)", + "device_id": "iPhone12,3", + "order": "MW9E2LL/A*", + "full_family": "iPhone 11 Pro" }, { - "full_name": "Apple Watch Series 8 (Aluminum, GPS, 41 mm)", - "introduced": "September 7, 2022", - "discontinued": "September 12, 2023", - "model": "A2770* (EMC 8094*)", - "device_id": "Watch6,14", - "order": "MNU93LL/A*", - "full_family": "Watch Series 8 41 mm" + "full_name": "iPhone 11 Pro Max", + "introduced": "September 10, 2019", + "discontinued": "October 13, 2020", + "model": "A2161/A2218/A2220 (EMC 3306*)", + "device_id": "iPhone12,5", + "order": "MWFC2LL/A*", + "full_family": "iPhone 11 Pro Max" }, { - "full_name": "Apple Watch Series 8 (Aluminum, GPS, 45 mm)", - "introduced": "September 7, 2022", - "discontinued": "September 12, 2023", - "model": "A2771* (EMC 8095*)", - "device_id": "Watch6,15", - "order": "MNUP3LL/A*", - "full_family": "Watch Series 8 45 mm" + "full_name": "Apple Watch Series 5", + "introduced": "September 10, 2019*", + "discontinued": "September 15, 2020", + "model": "A2092 (EMC 3317*)", + "device_id": "Watch5,1", + "order": "MWV82LL/A**", + "full_family": "Watch Series 5 40 mm" }, { - "full_name": "Apple Watch Series 8 (Cellular, US/CA, 41 mm)", - "introduced": "September 7, 2022", - "discontinued": "September 12, 2023", - "model": "A2772* (EMC 8096*)", - "device_id": "Watch6,16", - "order": "MNUX3LL/A*", - "full_family": "Watch Series 8 41 mm" + "full_name": "Apple Watch Series 5", + "introduced": "September 10, 2019*", + "discontinued": "September 15, 2020", + "model": "A2093 (EMC 3318*)", + "device_id": "Watch5,2", + "order": "MWVF2LL/A**", + "full_family": "Watch Series 5 44 mm" }, { - "full_name": "Apple Watch Series 8 (Cellular, US/CA, 45 mm)", - "introduced": "September 7, 2022", - "discontinued": "September 12, 2023", - "model": "A2774* (EMC 8097*)", - "device_id": "Watch6,17", - "order": "MNVP3LL/A*", - "full_family": "Watch Series 8 45 mm" + "full_name": "Apple Watch Series 5", + "introduced": "September 10, 2019*", + "discontinued": "September 15, 2020", + "model": "A2094/A2156 (EMC 3319*)", + "device_id": "Watch5,3", + "order": "MWWQ2LL/A**", + "full_family": "Watch Series 5 40 mm" }, { - "full_name": "Apple Watch Series 8 (Cellular, Global, 41 mm)", - "introduced": "September 7, 2022", - "discontinued": "September 12, 2023", - "model": "A2773* (EMC 8096*)", - "device_id": "Watch6,16", - "order": "MNHY3B/A*", - "full_family": "Watch Series 8 41 mm" + "full_name": "Apple Watch Series 5", + "introduced": "September 10, 2019*", + "discontinued": "September 15, 2020", + "model": "A2095/A2157 (EMC 3320*)", + "device_id": "Watch5,4", + "order": "MWW12LL/A**", + "full_family": "Watch Series 5 44 mm" }, { - "full_name": "Apple Watch Series 8 (Cellular, Global, 45 mm)", - "introduced": "September 7, 2022", - "discontinued": "September 12, 2023", - "model": "A2775* (EMC 8097*)", - "device_id": "Watch6,17", - "order": "MNK73B/A*", - "full_family": "Watch Series 8 45 mm" + "full_name": "iPhone 5", + "introduced": "September 12, 2012", + "discontinued": "April 12, 2013*", + "model": "A1428/A1429 (EMC N/A)", + "device_id": "iPhone5,1", + "order": "MD634LL/A*", + "full_family": "iPhone 5" }, { - "full_name": "Apple Watch Series 8 (Cellular, China, 41 mm)", - "introduced": "September 7, 2022", - "discontinued": "September 12, 2023", - "model": "A2857* (EMC 8096*)", - "device_id": "Watch6,16", - "order": "MNJ03CH/A*", - "full_family": "Watch Series 8 41 mm" + "full_name": "iPhone 5", + "introduced": "September 12, 2012", + "discontinued": "September 10, 2013", + "model": "A1429/A1442 (EMC N/A)", + "device_id": "iPhone5,2", + "order": "MD656LL/A*", + "full_family": "iPhone 5" }, { - "full_name": "Apple Watch Series 8 (Cellular, China, 45 mm)", - "introduced": "September 7, 2022", - "discontinued": "September 12, 2023", - "model": "A2858* (EMC 8097*)", - "device_id": "Watch6,17", - "order": "MNK83CH/A*", - "full_family": "Watch Series 8 45 mm" + "full_name": "Apple TV 4K (2017, Black Siri Remote)", + "introduced": "September 12, 2017", + "discontinued": "April 20, 2021", + "model": "A1842 (EMC 3124)", + "device_id": "AppleTV6,2", + "order": "MQD22LL/A*", + "full_family": "4K" }, { - "full_name": "Apple Watch Series 8 (Hermes, US/CA, 41 mm)", - "introduced": "September 7, 2022", - "discontinued": "September 12, 2023", - "model": "A2772* (EMC 8096*)", - "device_id": "Watch6,16", - "order": "MNN03LL/A*", - "full_family": "Watch Series 8 41 mm" + "full_name": "Apple Watch Series 3", + "introduced": "September 12, 2017", + "discontinued": "September 15, 2020", + "model": "A1860/A1889/A1890 (EMC 3169*)", + "device_id": "Watch3,1", + "order": "MQJN2LL/A**", + "full_family": "Watch Series 3 38 mm" }, { - "full_name": "Apple Watch Series 8 (Hermes, US/CA, 45 mm)", - "introduced": "September 7, 2022", - "discontinued": "September 12, 2023", - "model": "A2774* (EMC 8097*)", - "device_id": "Watch6,17", - "order": "MNNR3LL/A*", - "full_family": "Watch Series 8 45 mm" + "full_name": "Apple Watch Series 3", + "introduced": "September 12, 2017", + "discontinued": "September 15, 2020", + "model": "A1861/A1891/A1892 (EMC 3167*)", + "device_id": "Watch3,2", + "order": "MQK12LL/A**", + "full_family": "Watch Series 3 42 mm" }, { - "full_name": "Apple Watch Series 8 (Hermes, Global, 41 mm)", - "introduced": "September 7, 2022", - "discontinued": "September 12, 2023", - "model": "A2773* (EMC 8096*)", - "device_id": "Watch6,16", - "order": "MNN13B/A*", - "full_family": "Watch Series 8 41 mm" + "full_name": "Apple Watch Series 3", + "introduced": "September 12, 2017", + "discontinued": "September 7, 2022", + "model": "A1858 (EMC 3165*)", + "device_id": "Watch3,3", + "order": "MQKU2LL/A**", + "full_family": "Watch Series 3 38 mm" }, { - "full_name": "Apple Watch Series 8 (Hermes, Global, 45 mm)", - "introduced": "September 7, 2022", - "discontinued": "September 12, 2023", - "model": "A2775* (EMC 8097*)", - "device_id": "Watch6,17", - "order": "MNNT3B/A*", - "full_family": "Watch Series 8 45 mm" + "full_name": "Apple Watch Series 3", + "introduced": "September 12, 2017", + "discontinued": "September 7, 2022", + "model": "A1859 (EMC 3166*)", + "device_id": "Watch3,4", + "order": "MQL02LL/A**", + "full_family": "Watch Series 3 42 mm" }, { - "full_name": "Apple Watch Series 8 (Hermes, China, 41 mm)", - "introduced": "September 7, 2022", - "discontinued": "September 12, 2023", - "model": "A2857* (EMC 8096*)", - "device_id": "Watch6,16", - "order": "MNN23CH/A*", - "full_family": "Watch Series 8 41 mm" + "full_name": "iPhone 8", + "introduced": "September 12, 2017*", + "discontinued": "April 15, 2020", + "model": "A1863/A1906/A1907 (EMC 3159*)", + "device_id": "iPhone10,1", + "order": "MQ742LL/A*", + "full_family": "iPhone 8" }, - { - "full_name": "Apple Watch Series 8 (Hermes, China, 45 mm)", - "introduced": "September 7, 2022", - "discontinued": "September 12, 2023", - "model": "A2858* (EMC 8097*)", - "device_id": "Watch6,17", - "order": "MNNW3CH/A*", - "full_family": "Watch Series 8 45 mm" + { + "full_name": "iPhone 8 Plus", + "introduced": "September 12, 2017*", + "discontinued": "April 15, 2020", + "model": "A1864/A1898/A1899 (EMC 3160*)", + "device_id": "iPhone10,2", + "order": "MQ982LL/A*", + "full_family": "iPhone 8 Plus" }, { - "full_name": "Apple Watch SE 2 (GPS, 40 mm)", - "introduced": "September 7, 2022", - "discontinued": "N/A", - "model": "A2722* (EMC 8090*)", - "device_id": "Watch6,10", - "order": "MNT33LL/A*", - "full_family": "Watch SE 2 40 mm" + "full_name": "iPhone X", + "introduced": "September 12, 2017*", + "discontinued": "September 12, 2018", + "model": "A1865/A1902 (EMC 3161*)", + "device_id": "iPhone10,3", + "order": "MQCK2LL/A*", + "full_family": "iPhone X" }, { - "full_name": "Apple Watch SE 2 (GPS, 44 mm)", - "introduced": "September 7, 2022", - "discontinued": "N/A", - "model": "A2723* (EMC 8091*)", - "device_id": "Watch6,11", - "order": "MNTD3LL/A*", - "full_family": "Watch SE 2 44 mm" + "full_name": "iPhone 8 (AT&T/T-Mobile/Global/A1905)", + "introduced": "September 12, 2017*", + "discontinued": "April 15, 2020", + "model": "A1905 (EMC 3172*)", + "device_id": "iPhone10,4", + "order": "MQ6X2LL/A*", + "full_family": "iPhone 8" }, { - "full_name": "Apple Watch SE 2 (Cellular, US/CA, 40 mm)", - "introduced": "September 7, 2022", - "discontinued": "N/A", - "model": "A2726* (EMC 8092*)", - "device_id": "Watch6,12", - "order": "MNTK3LL/A*", - "full_family": "Watch SE 2 40 mm" + "full_name": "iPhone 8 Plus (AT&T/T-Mobile/Global/A1897)", + "introduced": "September 12, 2017*", + "discontinued": "April 15, 2020", + "model": "A1897 (EMC 3174*)", + "device_id": "iPhone10,5", + "order": "MQ8V2LL/A*", + "full_family": "iPhone 8 Plus" }, { - "full_name": "Apple Watch SE 2 (Cellular, US/CA, 44 mm)", - "introduced": "September 7, 2022", - "discontinued": "N/A", - "model": "A2727* (EMC 8093*)", - "device_id": "Watch6,13", - "order": "MNTW3LL/A*", - "full_family": "Watch SE 2 44 mm" + "full_name": "iPhone X (AT&T/T-Mobile/Global/A1901)", + "introduced": "September 12, 2017*", + "discontinued": "September 12, 2018", + "model": "A1901 (EMC 3175*)", + "device_id": "iPhone10,6", + "order": "MQAJ2LL/A*", + "full_family": "iPhone X" }, { - "full_name": "Apple Watch SE 2 (Cellular, Global, 40 mm)", - "introduced": "September 7, 2022", - "discontinued": "N/A", - "model": "A2725* (EMC 8092*)", - "device_id": "Watch6,12", - "order": "MNPH3B/A*", - "full_family": "Watch SE 2 40 mm" + "full_name": "iPhone Xs", + "introduced": "September 12, 2018*", + "discontinued": "September 10, 2019", + "model": "A1920/A2097/A2098/A2100 (EMC 3218*)", + "device_id": "iPhone11,2", + "order": "MT952LL/A*", + "full_family": "iPhone Xs" }, { - "full_name": "Apple Watch SE 2 (Cellular, Global, 44 mm)", - "introduced": "September 7, 2022", - "discontinued": "N/A", - "model": "A2724* (EMC 8093*)", - "device_id": "Watch6,13", - "order": "MNPT3B/A*", - "full_family": "Watch SE 2 44 mm" + "full_name": "iPhone Xs Max", + "introduced": "September 12, 2018*", + "discontinued": "September 10, 2019", + "model": "A1921/A2101/A2102/A2104 (EMC 3219*)", + "device_id": "iPhone11,6", + "order": "MT5A2LL/A*", + "full_family": "iPhone Xs Max" }, { - "full_name": "Apple Watch SE 2 (Cellular, China, 40 mm)", - "introduced": "September 7, 2022", - "discontinued": "N/A", - "model": "A2855* (EMC 8092*)", - "device_id": "Watch6,12", - "order": "MNPJ3CH/A*", - "full_family": "Watch SE 2 40 mm" + "full_name": "iPhone XR", + "introduced": "September 12, 2018*", + "discontinued": "September 14, 2021", + "model": "A1984/A2105/A2106/A2108 (EMC 3220*)", + "device_id": "iPhone11,8", + "order": "MT3L2LL/A*", + "full_family": "iPhone XR" }, { - "full_name": "Apple Watch SE 2 (Cellular, China, 44 mm)", - "introduced": "September 7, 2022", - "discontinued": "N/A", - "model": "A2856* (EMC 8093*)", - "device_id": "Watch6,13", - "order": "MNPW3CH/A*", - "full_family": "Watch SE 2 44 mm" + "full_name": "Apple Watch Series 4", + "introduced": "September 12, 2018*", + "discontinued": "September 10, 2019", + "model": "A1977 (EMC 3225*)", + "device_id": "Watch4,1", + "order": "MU642LL/A**", + "full_family": "Watch Series 4 40 mm" }, { - "full_name": "Apple Watch Ultra (US/CA, 49 mm)", - "introduced": "September 7, 2022", - "discontinued": "September 12, 2023", - "model": "A2622* (EMC 8098*)", - "device_id": "Watch6,18", - "order": "MNHC3LL/A*", - "full_family": "Watch Ultra" + "full_name": "Apple Watch Series 4", + "introduced": "September 12, 2018*", + "discontinued": "September 10, 2019", + "model": "A1978 (EMC 3229*)", + "device_id": "Watch4,2", + "order": "MU6A2LL/A**", + "full_family": "Watch Series 4 44 mm" }, { - "full_name": "Apple Watch Ultra (Global, 49 mm)", - "introduced": "September 7, 2022", - "discontinued": "September 12, 2023", - "model": "A2684* (EMC 8098*)", - "device_id": "Watch6,18", - "order": "MNHJ3B/A*", - "full_family": "Watch Ultra" + "full_name": "Apple Watch Series 4", + "introduced": "September 12, 2018*", + "discontinued": "September 10, 2019", + "model": "A1975/A2007 (EMC 3227*)", + "device_id": "Watch4,3", + "order": "MTUD2LL/A**", + "full_family": "Watch Series 4 40 mm" }, { - "full_name": "Apple Watch Ultra (China, 49 mm)", - "introduced": "September 7, 2022", - "discontinued": "September 12, 2023", - "model": "A2859* (EMC 8098*)", - "device_id": "Watch6,18", - "order": "MNHQ3CH/A*", - "full_family": "Watch Ultra" + "full_name": "Apple Watch Series 4", + "introduced": "September 12, 2018*", + "discontinued": "September 10, 2019", + "model": "A1976/A2008 (EMC 3228*)", + "device_id": "Watch4,4", + "order": "MTUU2LL/A**", + "full_family": "Watch Series 4 44 mm" }, { "full_name": "Apple Watch Series 9 (Aluminum, GPS, 41 mm)", "introduced": "September 12, 2023", "discontinued": "September 9, 2024", - "model": "A2978* (EMC 8397*)", + "model": "A2978 (EMC 8397*)", "device_id": "Watch7,1", "order": "MR933LL/A*", "full_family": "Watch Series 9 41 mm" @@ -4386,657 +1335,666 @@ "full_name": "Apple Watch Series 9 (Aluminum, GPS, 45 mm)", "introduced": "September 12, 2023", "discontinued": "September 9, 2024", - "model": "A2980* (EMC 8398*)", + "model": "A2980 (EMC 8398*)", "device_id": "Watch7,2", "order": "MR9G3LL/A*", "full_family": "Watch Series 9 45 mm" }, { - "full_name": "Apple Watch Series 9 (Cellular, US/CA, 41 mm)", + "full_name": "Apple Watch Series 9", "introduced": "September 12, 2023", "discontinued": "September 9, 2024", - "model": "A2982* (EMC 8399*)", + "model": "A2982/A2983 (EMC 8399*)", "device_id": "Watch7,3", "order": "MRHY3LL/A*", "full_family": "Watch Series 9 41 mm" }, { - "full_name": "Apple Watch Series 9 (Cellular, US/CA, 45 mm)", + "full_name": "Apple Watch Series 9", "introduced": "September 12, 2023", "discontinued": "September 9, 2024", - "model": "A2984* (EMC 8400*)", + "model": "A2984/A2985/A2986 (EMC 8400*)", "device_id": "Watch7,4", "order": "MRMK3LL/A*", "full_family": "Watch Series 9 45 mm" }, { - "full_name": "Apple Watch Series 9 (Cellular, Global, 41 mm)", + "full_name": "Apple Watch Ultra 2", "introduced": "September 12, 2023", - "discontinued": "September 9, 2024", - "model": "A2982* (EMC 8399*)", - "device_id": "Watch7,3", - "order": "MRHY3QA/A*", - "full_family": "Watch Series 9 41 mm" + "discontinued": "N/A", + "model": "A2986/A2987 (EMC 8401*)", + "device_id": "Watch7,5", + "order": "MREK3LL/A*", + "full_family": "Watch Ultra 2" }, { - "full_name": "Apple Watch Series 9 (Cellular, Global, 45 mm)", - "introduced": "September 12, 2023", - "discontinued": "September 9, 2024", - "model": "A2984* (EMC 8400*)", - "device_id": "Watch7,4", - "order": "MRMK3QA/A*", - "full_family": "Watch Series 9 45 mm" + "full_name": "iPhone 15", + "introduced": "September 12, 2023*", + "discontinued": "N/A", + "model": "A2846/A3089/A3090/A3092 (EMC 8427*)", + "device_id": "iPhone15,4", + "order": "MTLY3LL/A*", + "full_family": "iPhone 15" }, { - "full_name": "Apple Watch Series 9 (Cellular, China, 41 mm)", - "introduced": "September 12, 2023", - "discontinued": "September 9, 2024", - "model": "A2983* (EMC 8399*)", - "device_id": "Watch7,3", - "order": "MRJP3CH/A*", - "full_family": "Watch Series 9 41 mm" + "full_name": "iPhone 15 Plus", + "introduced": "September 12, 2023*", + "discontinued": "N/A", + "model": "A2847/A3093/A3094/A3096 (EMC 8431*)", + "device_id": "iPhone15,5", + "order": "MTXV3LL/A*", + "full_family": "iPhone 15 Plus" }, { - "full_name": "Apple Watch Series 9 (Cellular, China, 45 mm)", - "introduced": "September 12, 2023", + "full_name": "iPhone 15 Pro", + "introduced": "September 12, 2023*", "discontinued": "September 9, 2024", - "model": "A2985* (EMC 8400*)", - "device_id": "Watch7,4", - "order": "MRPC3CH/A*", - "full_family": "Watch Series 9 45 mm" + "model": "A2848/A3101/A3102/A3104 (EMC 8435*)", + "device_id": "iPhone16,1", + "order": "MTQP3LL/A*", + "full_family": "iPhone 15 Pro" }, { - "full_name": "Apple Watch Series 9 (Hermes, US/CA, 41 mm)", - "introduced": "September 12, 2023", + "full_name": "iPhone 15 Pro Max", + "introduced": "September 12, 2023*", "discontinued": "September 9, 2024", - "model": "A2982* (EMC 8399*)", - "device_id": "Watch7,3", - "order": "MRQ43LL/A*", - "full_family": "Watch Series 9 41 mm" + "model": "A2849/A3105/A3106/A3108 (EMC 8439*)", + "device_id": "iPhone16,2", + "order": "MU683LL/A*", + "full_family": "iPhone 15 Pro Max" }, { - "full_name": "Apple Watch Series 9 (Hermes, US/CA, 45 mm)", - "introduced": "September 12, 2023", - "discontinued": "September 9, 2024", - "model": "A2984* (EMC 8400*)", - "device_id": "Watch7,4", - "order": "MRQP3LL/A*", - "full_family": "Watch Series 9 45 mm" + "full_name": "iPad 10.2\" 9th Gen (Wi-Fi Only)", + "introduced": "September 14, 2021*", + "discontinued": "May 7, 2024", + "model": "A2602 (EMC 4045*)", + "device_id": "iPad12,1", + "order": "MK2L3LL/A*", + "full_family": "iPad 9th Gen (Wi-Fi)" }, { - "full_name": "Apple Watch Series 9 (Hermes, Global, 41 mm)", - "introduced": "September 12, 2023", - "discontinued": "September 9, 2024", - "model": "A2982* (EMC 8399*)", - "device_id": "Watch7,3", - "order": "MRQ43QA/A*", - "full_family": "Watch Series 9 41 mm" + "full_name": "iPad 10.2\" 9th Gen", + "introduced": "September 14, 2021*", + "discontinued": "May 7, 2024", + "model": "A2603/A2604/A2605 (EMC 4046*)", + "device_id": "iPad12,2", + "order": "MK673LL/A*", + "full_family": "iPad 9th Gen" }, { - "full_name": "Apple Watch Series 9 (Hermes, Global, 45 mm)", - "introduced": "September 12, 2023", - "discontinued": "September 9, 2024", - "model": "A2984* (EMC 8400*)", - "device_id": "Watch7,4", - "order": "MRQP3QA/A*", - "full_family": "Watch Series 9 45 mm" + "full_name": "iPad mini 6th Gen (Wi-Fi Only)", + "introduced": "September 14, 2021*", + "discontinued": "October 15, 2024", + "model": "A2567 (EMC 4043*)", + "device_id": "iPad14,1", + "order": "MK7M3LL/A*", + "full_family": "iPad mini 6th Gen (Wi-Fi)" }, { - "full_name": "Apple Watch Series 9 (Hermes, China, 41 mm)", - "introduced": "September 12, 2023", - "discontinued": "September 9, 2024", - "model": "A2983* (EMC 8399*)", - "device_id": "Watch7,3", - "order": "MRQE3CH/A*", - "full_family": "Watch Series 9 41 mm" + "full_name": "iPad mini 6th Gen", + "introduced": "September 14, 2021*", + "discontinued": "October 15, 2024", + "model": "A2568/A2569 (EMC 4044*)", + "device_id": "iPad14,2", + "order": "MK893LL/A*", + "full_family": "iPad mini 6th Gen" }, { - "full_name": "Apple Watch Series 9 (Hermes, China, 45 mm)", - "introduced": "September 12, 2023", - "discontinued": "September 9, 2024", - "model": "A2985* (EMC 8400*)", - "device_id": "Watch7,4", - "order": "MRR03CH/A*", - "full_family": "Watch Series 9 45 mm" + "full_name": "iPhone 13 Pro", + "introduced": "September 14, 2021*", + "discontinued": "September 7, 2022", + "model": "A2483/A2636/A2638/A2639/A2640 (EMC 4000*)", + "device_id": "iPhone14,2", + "order": "MLTT3LL/A*", + "full_family": "iPhone 13 Pro" }, { - "full_name": "Apple Watch Ultra 2 (US/CA, 49 mm)", - "introduced": "September 12, 2023", - "discontinued": "N/A", - "model": "A2986* (EMC 8401*)", - "device_id": "Watch7,5", - "order": "MREK3LL/A*", - "full_family": "Watch Ultra 2" + "full_name": "iPhone 13 Pro Max", + "introduced": "September 14, 2021*", + "discontinued": "September 7, 2022", + "model": "A2484/A2641/A2643/A2644/A2645 (EMC 4003*)", + "device_id": "iPhone14,3", + "order": "MLKP3LL/A*", + "full_family": "iPhone 13 Pro Max" }, { - "full_name": "Apple Watch Ultra 2 (Global, 49 mm)", - "introduced": "September 12, 2023", - "discontinued": "N/A", - "model": "A2986* (EMC 8401*)", - "device_id": "Watch7,5", - "order": "MREG3B/A*", - "full_family": "Watch Ultra 2" + "full_name": "iPhone 13 mini", + "introduced": "September 14, 2021*", + "discontinued": "September 12, 2023", + "model": "A2481/A2626/A2628/A2629/A2630 (EMC 3994*)", + "device_id": "iPhone14,4", + "order": "MLHP3LL/A*", + "full_family": "iPhone 13 mini" }, { - "full_name": "Apple Watch Ultra 2 (China, 49 mm)", - "introduced": "September 12, 2023", - "discontinued": "N/A", - "model": "A2987 (EMC 8401*)", - "device_id": "Watch7,5", - "order": "MRFA3CH/A*", - "full_family": "Watch Ultra 2" + "full_name": "iPhone 13", + "introduced": "September 14, 2021*", + "discontinued": "September 9, 2024", + "model": "A2482/A2631/A2633/A2634/A2635 (EMC 3997*)", + "device_id": "iPhone14,5", + "order": "MLMN3LL/A*", + "full_family": "iPhone 13" }, { - "full_name": "Apple Watch Ultra 2 (Hermes, US/CA, 49 mm)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A2986* (EMC 8401*)", - "device_id": "Watch7,5", - "order": "MX773LW/A*", - "full_family": "Watch Ultra 2" + "full_name": "Apple Watch Series 7", + "introduced": "September 14, 2021*", + "discontinued": "September 7, 2022", + "model": "A2473 (EMC 3982*)", + "device_id": "Watch6,6", + "order": "MKN03LL/A**", + "full_family": "Watch Series 7 41 mm" }, { - "full_name": "Apple Watch Ultra 2 (Hermes, Global, 49 mm)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A2986* (EMC 8401*)", - "device_id": "Watch7,5", - "order": "MX773QA/A*", - "full_family": "Watch Ultra 2" + "full_name": "Apple Watch Series 7", + "introduced": "September 14, 2021*", + "discontinued": "September 7, 2022", + "model": "A2474 (EMC 3983*)", + "device_id": "Watch6,7", + "order": "MKN73LL/A**", + "full_family": "Watch Series 7 45 mm" }, { - "full_name": "Apple Watch Ultra 2 (Hermes, China, 49 mm)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A2987 (EMC 8401*)", - "device_id": "Watch7,5", - "order": "MXDY3CH/B*", - "full_family": "Watch Ultra 2" + "full_name": "Apple Watch Series 7", + "introduced": "September 14, 2021*", + "discontinued": "September 7, 2022", + "model": "A2475/A2476 (EMC 3984*)", + "device_id": "Watch6,8", + "order": "MKHG3LL/A**", + "full_family": "Watch Series 7 41 mm" }, { - "full_name": "Apple Watch Series 10 (Aluminum, GPS, 42 mm)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A2997* (EMC 8729*)", - "device_id": "Watch7,8", - "order": "MWWA3LW/A*", - "full_family": "Watch Series 10 42 mm" + "full_name": "Apple Watch Series 7", + "introduced": "September 14, 2021*", + "discontinued": "September 7, 2022", + "model": "A2477/A2478 (EMC 3985*)", + "device_id": "Watch6,9", + "order": "MKJF3LL/A**", + "full_family": "Watch Series 7 45 mm" }, { - "full_name": "Apple Watch Series 10 (Aluminum, GPS, 46 mm)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A2999* (EMC 8735*)", - "device_id": "Watch7,9", - "order": "MWWL3LW/A*", - "full_family": "Watch Series 10 46 mm" + "full_name": "iPad 10.2\" 8th Gen (Wi-Fi Only)", + "introduced": "September 15, 2020", + "discontinued": "September 14, 2021", + "model": "A2270 (EMC 3574*)", + "device_id": "iPad11,6", + "order": "MYLA2LL/A*", + "full_family": "iPad 8th Gen (Wi-Fi)" }, { - "full_name": "Apple Watch Series 10 (Cellular, 42 mm)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A3001* (EMC 8733*)", - "device_id": "Watch7,10", - "order": "MWX33LW/A*", - "full_family": "Watch Series 10 42 mm" + "full_name": "iPad 10.2\" 8th Gen", + "introduced": "September 15, 2020", + "discontinued": "September 14, 2021", + "model": "A2428/A2429/A2430 (EMC 3575*)", + "device_id": "iPad11,7", + "order": "MYN52LL/A*", + "full_family": "iPad 8th Gen" }, { - "full_name": "Apple Watch Series 10 (Cellular, 46 mm)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A3003* (EMC 8737*)", - "device_id": "Watch7,11", - "order": "MWY03LW/A*", - "full_family": "Watch Series 10 46 mm" + "full_name": "Apple Watch SE", + "introduced": "September 15, 2020", + "discontinued": "September 7, 2022", + "model": "A2352 (EMC 3486*)", + "device_id": "Watch5,10", + "order": "MYDR2LL/A**", + "full_family": "Watch SE 44 mm" }, { - "full_name": "Apple Watch Series 10 (Cellular, China, 42 mm)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A3002* (EMC 8734*)", - "device_id": "Watch7,10", - "order": "MWXJ3CH/B*", - "full_family": "Watch Series 10 42 mm" + "full_name": "Apple Watch SE", + "introduced": "September 15, 2020", + "discontinued": "September 7, 2022", + "model": "A2353/A2355 (EMC 3487*)", + "device_id": "Watch5,11", + "order": "MYEA2LL/A**", + "full_family": "Watch SE 40 mm" }, { - "full_name": "Apple Watch Series 10 (Cellular, China, 46 mm)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A3206* (EMC 8738*)", - "device_id": "Watch7,11", - "order": "MWYF3CH/B*", - "full_family": "Watch Series 10 46 mm" + "full_name": "Apple Watch SE", + "introduced": "September 15, 2020", + "discontinued": "September 7, 2022", + "model": "A2354/A2356 (EMC 3488*)", + "device_id": "Watch5,12", + "order": "MYEP2LL/A**", + "full_family": "Watch SE 44 mm" }, { - "full_name": "Apple Watch Series 10 (Hermes, 42 mm)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A3001* (EMC 8733*)", - "device_id": "Watch7,10", - "order": "MX0Q3LW/A*", - "full_family": "Watch Series 10 42 mm" + "full_name": "Apple Watch SE", + "introduced": "September 15, 2020", + "discontinued": "September 7, 2022", + "model": "A2351 (EMC 3485*)", + "device_id": "Watch5,9", + "order": "MYDN2LL/A**", + "full_family": "Watch SE 40 mm" }, { - "full_name": "Apple Watch Series 10 (Hermes, 46 mm)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A3003* (EMC 8737*)", - "device_id": "Watch7,11", - "order": "MX193LW/A*", - "full_family": "Watch Series 10 46 mm" + "full_name": "Apple Watch Series 6", + "introduced": "September 15, 2020", + "discontinued": "October 8, 2021*", + "model": "A2291 (EMC 3479*)", + "device_id": "Watch6,1", + "order": "MG143LL/A**", + "full_family": "Watch Series 6 40 mm" }, { - "full_name": "Apple Watch Series 10 (Hermes, China, 42 mm)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A3002* (EMC 8734*)", - "device_id": "Watch7,10", - "order": "MX103CH/B*", - "full_family": "Watch Series 10 42 mm" + "full_name": "Apple Watch Series 6", + "introduced": "September 15, 2020", + "discontinued": "October 8, 2021*", + "model": "A2292 (EMC 3480*)", + "device_id": "Watch6,2", + "order": "M00J3LL/A**", + "full_family": "Watch Series 6 44 mm" }, { - "full_name": "Apple Watch Series 10 (Hermes, China, 46 mm)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A3206* (EMC 8738*)", - "device_id": "Watch7,11", - "order": "MX1H3CH/B*", - "full_family": "Watch Series 10 46 mm" + "full_name": "Apple Watch Series 6", + "introduced": "September 15, 2020", + "discontinued": "October 8, 2021*", + "model": "A2293/A2375 (EMC 3481*)", + "device_id": "Watch6,3", + "order": "M02R3LL/A**", + "full_family": "Watch Series 6 40 mm" }, { - "full_name": "Apple HomePod (Smart Speaker)", - "introduced": "June 5, 2017*", - "discontinued": "March 12, 2021**", - "model": "A1639 (EMC N/A*)", - "device_id": "AudioAccessory1,1", - "order": "MQHV2LL/A*", - "full_family": "HomePod" + "full_name": "Apple Watch Series 6", + "introduced": "September 15, 2020", + "discontinued": "October 8, 2021*", + "model": "A2294/A2376 (EMC 3482*)", + "device_id": "Watch6,4", + "order": "M07J3LL/A**", + "full_family": "Watch Series 6 44 mm" }, { - "full_name": "Apple HomePod mini (Smart Speaker)", - "introduced": "October 13, 2020*", - "discontinued": "N/A", - "model": "A2374 (EMC 3506*)", - "device_id": "AudioAccessory5,1", - "order": "MY5H2LL/A*", - "full_family": "HomePod mini" + "full_name": "iPad Air 4th Gen (Wi-Fi Only)", + "introduced": "September 15, 2020*", + "discontinued": "March 8, 2022", + "model": "A2316 (EMC 3570*)", + "device_id": "iPad13,1", + "order": "MYFN2LL/A*", + "full_family": "iPad Air 4th Gen (Wi-Fi)" }, { - "full_name": "Apple HomePod 2nd Gen (Smart Speaker)", - "introduced": "January 18, 2023*", - "discontinued": "N/A", - "model": "A2825 (EMC 8208)", - "device_id": "AudioAccessory6,1", - "order": "MQJ73LL/A*", - "full_family": "HomePod 2nd Gen" + "full_name": "iPad Air 4th Gen", + "introduced": "September 15, 2020*", + "discontinued": "March 8, 2022", + "model": "A2324/A2072/A2325 (EMC 3571*)", + "device_id": "iPad13,2", + "order": "MYHY2LL/A*", + "full_family": "iPad Air 4th Gen" }, { - "full_name": "Apple Vision Pro (Original)", - "introduced": "June 5, 2023*", + "full_name": "iPhone 17 Pro", + "introduced": "September 2025", "discontinued": "N/A", - "model": "A2117* (EMC 4075*)", - "device_id": "RealityDevice14,1", - "order": "MQL83LL/A*", - "full_family": "Vision Pro" + "model": "A3256/A3522/A3523/A3524 (EMC 8949*)", + "device_id": "iPhone18,1", + "order": "N/A*", + "full_family": "iPhone 17 Pro" }, { - "full_name": "iPhone 15 Pro (US)", - "introduced": "September 12, 2023", - "discontinued": "September 9, 2024", - "model": "A2848 (EMC N/A*)", - "device_id": "iPhone16,1", + "full_name": "iPhone 17 Pro Max", + "introduced": "September 2025", + "discontinued": "N/A", + "model": "A3257/A3525/A3526/A3527 (EMC 8961*)", + "device_id": "iPhone18,2", "order": "N/A*", - "full_family": "iPhone 15 Pro" + "full_family": "iPhone 17 Pro Max" }, { - "full_name": "iPhone 15 Pro (CA/JP/MX/SA)", - "introduced": "September 12, 2023", - "discontinued": "September 9, 2024", - "model": "A3101 (EMC N/A*)", - "device_id": "iPhone16,1", + "full_name": "iPhone 17", + "introduced": "September 2025", + "discontinued": "N/A", + "model": "A3258/A3519/A3520/A3521 (EMC 8947*)", + "device_id": "iPhone18,3", "order": "N/A*", - "full_family": "iPhone 15 Pro" + "full_family": "iPhone 17" }, { - "full_name": "iPhone 15 Pro (Global)", - "introduced": "September 12, 2023", - "discontinued": "September 9, 2024", - "model": "A3102 (EMC N/A*)", - "device_id": "iPhone16,1", + "full_name": "iPhone 17 Air", + "introduced": "September 2025", + "discontinued": "N/A", + "model": "A3260/A3517 (EMC 8955*)", + "device_id": "iPhone18,4", "order": "N/A*", - "full_family": "iPhone 15 Pro" + "full_family": "iPhone 17 Air" }, { - "full_name": "iPhone 15 Pro (China/HK/Macau)", - "introduced": "September 12, 2023", - "discontinued": "September 9, 2024", - "model": "A3104 (EMC N/A*)", - "device_id": "iPhone16,1", - "order": "N/A*", - "full_family": "iPhone 15 Pro" + "full_name": "iPhone 7", + "introduced": "September 7, 2016*", + "discontinued": "September 10, 2019", + "model": "A1660/A1779/A1780 (EMC 3091*)", + "device_id": "iPhone9,1", + "order": "MNAD2LL/A*", + "full_family": "iPhone 7" }, { - "full_name": "iPhone 15 Pro Max (US)", - "introduced": "September 12, 2023", - "discontinued": "September 9, 2024", - "model": "A2849 (EMC N/A*)", - "device_id": "iPhone16,2", - "order": "N/A*", - "full_family": "iPhone 15 Pro Max" + "full_name": "iPhone 7 Plus", + "introduced": "September 7, 2016*", + "discontinued": "September 10, 2019", + "model": "A1661/A1785/A1786 (EMC 3092*)", + "device_id": "iPhone9,2", + "order": "MNR22LL/A*", + "full_family": "iPhone 7 Plus" }, { - "full_name": "iPhone 15 Pro Max (CA/JP/MX/SA)", - "introduced": "September 12, 2023", - "discontinued": "September 9, 2024", - "model": "A3105 (EMC N/A*)", - "device_id": "iPhone16,2", - "order": "N/A*", - "full_family": "iPhone 15 Pro Max" + "full_name": "iPhone 7 (AT&T/T-Mobile/Global/A1778)", + "introduced": "September 7, 2016*", + "discontinued": "September 10, 2019", + "model": "A1778 (EMC 3091*)", + "device_id": "iPhone9,3", + "order": "MN9E2LL/A*", + "full_family": "iPhone 7" }, { - "full_name": "iPhone 15 Pro Max (Global)", - "introduced": "September 12, 2023", - "discontinued": "September 9, 2024", - "model": "A3106 (EMC N/A*)", - "device_id": "iPhone16,2", - "order": "N/A*", - "full_family": "iPhone 15 Pro Max" + "full_name": "iPhone 7 Plus (AT&T/T-Mobile/Global/A1784)", + "introduced": "September 7, 2016*", + "discontinued": "September 10, 2019", + "model": "A1784 (EMC 3092*)", + "device_id": "iPhone9,4", + "order": "MNQT2LL/A*", + "full_family": "iPhone 7 Plus" }, { - "full_name": "iPhone 15 Pro Max (China/HK/Macau)", - "introduced": "September 12, 2023", - "discontinued": "September 9, 2024", - "model": "A3108 (EMC N/A*)", - "device_id": "iPhone16,2", - "order": "N/A*", - "full_family": "iPhone 15 Pro Max" + "full_name": "Apple Watch Series 2", + "introduced": "September 7, 2016*", + "discontinued": "September 12, 2017", + "model": "A1757/A1816 (EMC 3104*)", + "device_id": "Watch2,3", + "order": "MNNW2LL/A*", + "full_family": "Watch Series 2 38mm" }, { - "full_name": "iPhone 16 (US)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A3081 (EMC N/A*)", - "device_id": "iPhone17,4", - "order": "N/A*", - "full_family": "iPhone 16" + "full_name": "Apple Watch Series 2", + "introduced": "September 7, 2016*", + "discontinued": "September 12, 2017", + "model": "A1758/A1817 (EMC 3105*)", + "device_id": "Watch2,4", + "order": "MNPJ2LL/A*", + "full_family": "Watch Series 2 42mm" }, { - "full_name": "iPhone 16 (CA/JP/MX/SA)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A3286 (EMC N/A*)", - "device_id": "iPhone17,4", - "order": "N/A*", - "full_family": "iPhone 16" + "full_name": "Apple Watch Series 1 (Aluminum, 38 mm)", + "introduced": "September 7, 2016*", + "discontinued": "September 12, 2018", + "model": "A1802 (EMC 3102*)", + "device_id": "Watch2,6", + "order": "MNNG2LL/A*", + "full_family": "Watch Series 1 38mm" }, { - "full_name": "iPhone 16 (Global)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A3287 (EMC N/A*)", - "device_id": "iPhone17,4", - "order": "N/A*", - "full_family": "iPhone 16" + "full_name": "Apple Watch Series 1 (Aluminum, 42 mm)", + "introduced": "September 7, 2016*", + "discontinued": "September 12, 2018", + "model": "A1803 (EMC 3103*)", + "device_id": "Watch2,7", + "order": "MNNL2LL/A*", + "full_family": "Watch Series 1 42mm" }, { - "full_name": "iPhone 16 (China/HK/Macau)", - "introduced": "September 9, 2024", + "full_name": "Apple Watch SE 2 (GPS, 40 mm)", + "introduced": "September 7, 2022", "discontinued": "N/A", - "model": "A3288 (EMC N/A*)", - "device_id": "iPhone17,4", - "order": "N/A*", - "full_family": "iPhone 16" + "model": "A2722 (EMC 8090*)", + "device_id": "Watch6,10", + "order": "MNT33LL/A*", + "full_family": "Watch SE 2 40 mm" }, { - "full_name": "iPhone 16 Plus (US)", - "introduced": "September 9, 2024", + "full_name": "Apple Watch SE 2 (GPS, 44 mm)", + "introduced": "September 7, 2022", "discontinued": "N/A", - "model": "A3080 (EMC N/A*)", - "device_id": "iPhone17,5", - "order": "N/A*", - "full_family": "iPhone 16 Plus" + "model": "A2723 (EMC 8091*)", + "device_id": "Watch6,11", + "order": "MNTD3LL/A*", + "full_family": "Watch SE 2 44 mm" }, { - "full_name": "iPhone 16 Plus (CA/JP/MX/SA)", - "introduced": "September 9, 2024", + "full_name": "Apple Watch SE 2", + "introduced": "September 7, 2022", "discontinued": "N/A", - "model": "A3290 (EMC N/A*)", - "device_id": "iPhone17,5", - "order": "N/A*", - "full_family": "iPhone 16 Plus" + "model": "A2726/A2725/A2855 (EMC 8092*)", + "device_id": "Watch6,12", + "order": "MNTK3LL/A*", + "full_family": "Watch SE 2 40 mm" }, { - "full_name": "iPhone 16 Plus (Global)", - "introduced": "September 9, 2024", + "full_name": "Apple Watch SE 2", + "introduced": "September 7, 2022", "discontinued": "N/A", - "model": "A3291 (EMC N/A*)", - "device_id": "iPhone17,5", - "order": "N/A*", - "full_family": "iPhone 16 Plus" + "model": "A2727/A2724/A2856 (EMC 8093*)", + "device_id": "Watch6,13", + "order": "MNTW3LL/A*", + "full_family": "Watch SE 2 44 mm" }, { - "full_name": "iPhone 16 Plus (China/HK/Macau)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A3296 (EMC N/A*)", - "device_id": "iPhone17,5", - "order": "N/A*", - "full_family": "iPhone 16 Plus" + "full_name": "Apple Watch Series 8 (Aluminum, GPS, 41 mm)", + "introduced": "September 7, 2022", + "discontinued": "September 12, 2023", + "model": "A2770 (EMC 8094*)", + "device_id": "Watch6,14", + "order": "MNU93LL/A*", + "full_family": "Watch Series 8 41 mm" }, { - "full_name": "iPhone 16 Pro (US)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A3083 (EMC N/A*)", - "device_id": "iPhone17,1", - "order": "N/A*", - "full_family": "iPhone 16 Pro" + "full_name": "Apple Watch Series 8 (Aluminum, GPS, 45 mm)", + "introduced": "September 7, 2022", + "discontinued": "September 12, 2023", + "model": "A2771 (EMC 8095*)", + "device_id": "Watch6,15", + "order": "MNUP3LL/A*", + "full_family": "Watch Series 8 45 mm" }, { - "full_name": "iPhone 16 Pro (CA/JP/MX/SA)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A3292 (EMC N/A*)", - "device_id": "iPhone17,1", - "order": "N/A*", - "full_family": "iPhone 16 Pro" + "full_name": "Apple Watch Series 8", + "introduced": "September 7, 2022", + "discontinued": "September 12, 2023", + "model": "A2772/A2773/A2857 (EMC 8096*)", + "device_id": "Watch6,16", + "order": "MNUX3LL/A*", + "full_family": "Watch Series 8 41 mm" }, { - "full_name": "iPhone 16 Pro (Global)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A3293 (EMC N/A*)", - "device_id": "iPhone17,1", - "order": "N/A*", - "full_family": "iPhone 16 Pro" + "full_name": "Apple Watch Series 8", + "introduced": "September 7, 2022", + "discontinued": "September 12, 2023", + "model": "A2774/A2775/A2858 (EMC 8097*)", + "device_id": "Watch6,17", + "order": "MNVP3LL/A*", + "full_family": "Watch Series 8 45 mm" }, { - "full_name": "iPhone 16 Pro (China/HK/Macau)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A3294 (EMC N/A*)", - "device_id": "iPhone17,1", - "order": "N/A*", - "full_family": "iPhone 16 Pro" + "full_name": "Apple Watch Ultra", + "introduced": "September 7, 2022", + "discontinued": "September 12, 2023", + "model": "A2622/A2684/A2859 (EMC 8098*)", + "device_id": "Watch6,18", + "order": "MNHC3LL/A*", + "full_family": "Watch Ultra" }, { - "full_name": "iPhone 16 Pro Max (US)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A3084 (EMC N/A*)", - "device_id": "iPhone17,2", - "order": "N/A*", - "full_family": "iPhone 16 Pro Max" + "full_name": "iPhone 14", + "introduced": "September 7, 2022*", + "discontinued": "February 19, 2025", + "model": "A2649/A2881/A2882/A2884/A2883 (EMC 8138*)", + "device_id": "iPhone14,7", + "order": "MPVH3LL/A*", + "full_family": "iPhone 14" }, { - "full_name": "iPhone 16 Pro Max (CA/JP/MX/SA)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A3295 (EMC N/A*)", - "device_id": "iPhone17,2", - "order": "N/A*", - "full_family": "iPhone 16 Pro Max" + "full_name": "iPhone 14 Plus", + "introduced": "September 7, 2022*", + "discontinued": "February 19, 2025", + "model": "A2632/A2885/A2886/A2888/A2887 (EMC 8139*)", + "device_id": "iPhone14,8", + "order": "MQ3W3LL/A*", + "full_family": "iPhone 14 Plus" }, { - "full_name": "iPhone 16 Pro Max (Global)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A3297 (EMC N/A*)", - "device_id": "iPhone17,2", - "order": "N/A*", - "full_family": "iPhone 16 Pro Max" + "full_name": "iPhone 14 Pro", + "introduced": "September 7, 2022*", + "discontinued": "September 12, 2023", + "model": "A2650/A2889/A2890/A2892/A2891 (EMC 8140*)", + "device_id": "iPhone15,2", + "order": "MQ0E3LL/A*", + "full_family": "iPhone 14 Pro" }, { - "full_name": "iPhone 16 Pro Max (China/HK/Macau)", - "introduced": "September 9, 2024", - "discontinued": "N/A", - "model": "A3298 (EMC N/A*)", - "device_id": "iPhone17,2", - "order": "N/A*", - "full_family": "iPhone 16 Pro Max" + "full_name": "iPhone 14 Pro Max", + "introduced": "September 7, 2022*", + "discontinued": "September 12, 2023", + "model": "A2651/A2893/A2894/A2896/A2895 (EMC 8141*)", + "device_id": "iPhone15,3", + "order": "MQ8R3LL/A*", + "full_family": "iPhone 14 Pro Max" }, { - "full_name": "iPad Pro 11\" (M4, 2024 Wi‑Fi)", - "introduced": "May 7, 2024", - "discontinued": "N/A", - "model": "A2836 (EMC N/A*)", - "device_id": "iPad16,3", - "order": "N/A*", - "full_family": "iPad Pro 11\" (M4, 2024)" + "full_name": "iPhone 6 Plus", + "introduced": "September 9, 2014*", + "discontinued": "September 7, 2016*", + "model": "A1522/A1524/A1593 (EMC 2817*)", + "device_id": "iPhone7,1", + "order": "MGAM2LL/A*", + "full_family": "iPhone 6 Plus" }, { - "full_name": "iPad Pro 11\" (M4, 2024 Wi‑Fi + Cellular)", - "introduced": "May 7, 2024", - "discontinued": "N/A", - "model": "A2837 (EMC N/A*)", - "device_id": "iPad16,4", - "order": "N/A*", - "full_family": "iPad Pro 11\" (M4, 2024)" + "full_name": "iPhone 6", + "introduced": "September 9, 2014*", + "discontinued": "September 12, 2018*", + "model": "A1549/A1586/A1589 (EMC 2816*)", + "device_id": "iPhone7,2", + "order": "MG4P2LL/A*", + "full_family": "iPhone 6" }, { - "full_name": "iPad Pro 13\" (M4, 2024 Wi‑Fi)", - "introduced": "May 7, 2024", - "discontinued": "N/A", - "model": "A2923 (EMC N/A*)", - "device_id": "iPad16,5", - "order": "N/A*", - "full_family": "iPad Pro 13\" (M4, 2024)" + "full_name": "Apple TV HD (4th Generation, Siri)", + "introduced": "September 9, 2015*", + "discontinued": "October 18, 2022*", + "model": "A1625 (EMC 2907)", + "device_id": "AppleTV5,3", + "order": "MGY52LL/A*", + "full_family": "4th Gen" }, { - "full_name": "iPad Pro 13\" (M4, 2024 Wi‑Fi + Cellular)", - "introduced": "May 7, 2024", - "discontinued": "N/A", - "model": "A2924 (EMC N/A*)", - "device_id": "iPad16,6", - "order": "N/A*", - "full_family": "iPad Pro 13\" (M4, 2024)" + "full_name": "iPad mini 4 (Wi-Fi Only)", + "introduced": "September 9, 2015*", + "discontinued": "March 18, 2019*", + "model": "A1538 (EMC 2824*)", + "device_id": "iPad5,1", + "order": "MK6K2LL/A*", + "full_family": "iPad mini 4 (Wi-Fi)" }, { - "full_name": "iPad Air 11\" (M2, 2024 Wi‑Fi)", - "introduced": "May 7, 2024", - "discontinued": "N/A", - "model": "A2902 (EMC N/A*)", - "device_id": "iPad14,8", - "order": "N/A*", - "full_family": "iPad Air 11\" (M2, 2024)" + "full_name": "iPad mini 4 (Wi-Fi/Cellular)", + "introduced": "September 9, 2015*", + "discontinued": "March 18, 2019*", + "model": "A1550 (EMC 2825*)", + "device_id": "iPad5,2", + "order": "MK872LL/A*", + "full_family": "iPad mini 4 (Wi-Fi + Cellular)" }, { - "full_name": "iPad Air 11\" (M2, 2024 Wi‑Fi + Cellular)", - "introduced": "May 7, 2024", - "discontinued": "N/A", - "model": "A2903 (EMC N/A*)", - "device_id": "iPad14,9", - "order": "N/A*", - "full_family": "iPad Air 11\" (M2, 2024)" + "full_name": "iPad Pro 12.9\" (Wi-Fi Only)", + "introduced": "September 9, 2015*", + "discontinued": "June 5, 2017", + "model": "A1584 (EMC 2838)", + "device_id": "iPad6,7", + "order": "ML0G2LL/A*", + "full_family": "iPad Pro (Wi-Fi)" }, { - "full_name": "iPad Air 13\" (M2, 2024 Wi‑Fi)", - "introduced": "May 7, 2024", - "discontinued": "N/A", - "model": "A2904 (EMC N/A*)", - "device_id": "iPad14,10", - "order": "N/A*", - "full_family": "iPad Air 13\" (M2, 2024)" + "full_name": "iPad Pro 12.9\" (Wi-Fi/Cellular)", + "introduced": "September 9, 2015*", + "discontinued": "June 5, 2017", + "model": "A1652 (EMC 2827)", + "device_id": "iPad6,8", + "order": "ML3N2LL/A*", + "full_family": "iPad Pro (Wi-Fi + Cellular)" }, { - "full_name": "iPad Air 13\" (M2, 2024 Wi‑Fi + Cellular)", - "introduced": "May 7, 2024", - "discontinued": "N/A", - "model": "A2905 (EMC N/A*)", - "device_id": "iPad14,11", - "order": "N/A*", - "full_family": "iPad Air 13\" (M2, 2024)" + "full_name": "iPhone 6s", + "introduced": "September 9, 2015*", + "discontinued": "September 12, 2018*", + "model": "A1633/A1688/A1700/A1691 (EMC 2946*)", + "device_id": "iPhone8,1", + "order": "MKQ62LL/A*", + "full_family": "iPhone 6s" }, { - "full_name": "iPad mini (7th Gen, 2024 Wi‑Fi)", - "introduced": "October 29, 2024", + "full_name": "iPhone 6s Plus", + "introduced": "September 9, 2015*", + "discontinued": "September 12, 2018*", + "model": "A1634/A1687/A1699/A1690 (EMC 2944*)", + "device_id": "iPhone8,2", + "order": "MKTM2LL/A*", + "full_family": "iPhone 6s Plus" + }, + { + "full_name": "iPhone 16 Pro", + "introduced": "September 9, 2024", "discontinued": "N/A", - "model": "A28xx (EMC N/A*)", - "device_id": "iPad16,1", - "order": "N/A*", - "full_family": "iPad mini (7th Gen)" + "model": "A3083/A3292/A3293/A3294 (EMC 8666*)", + "device_id": "iPhone17,1", + "order": "MYMC3LL/A*", + "full_family": "iPhone 16 Pro" }, { - "full_name": "iPad mini (7th Gen, 2024 Wi‑Fi + Cellular)", - "introduced": "October 29, 2024", + "full_name": "iPhone 16 Pro Max", + "introduced": "September 9, 2024", "discontinued": "N/A", - "model": "A28xx (EMC N/A*)", - "device_id": "iPad16,2", - "order": "N/A*", - "full_family": "iPad mini (7th Gen)" + "model": "A3084/A3295/A3296/A3297 (EMC 8684*)", + "device_id": "iPhone17,2", + "order": "MYW53LL/A*", + "full_family": "iPhone 16 Pro Max" }, { - "full_name": "iPad (11th Gen, 2024 Wi‑Fi)", - "introduced": "October 29, 2024", + "full_name": "iPhone 16", + "introduced": "September 9, 2024", "discontinued": "N/A", - "model": "A29xx (EMC N/A*)", - "device_id": "iPad15,7", - "order": "N/A*", - "full_family": "iPad (11th Gen)" + "model": "A3081/A3286/A3287/A3288 (EMC 8688*)", + "device_id": "iPhone17,3", + "order": "MYAT3LL/A*", + "full_family": "iPhone 16" }, { - "full_name": "iPad (11th Gen, 2024 Wi‑Fi + Cellular)", - "introduced": "October 29, 2024", + "full_name": "iPhone 16 Plus", + "introduced": "September 9, 2024", "discontinued": "N/A", - "model": "A29xx (EMC N/A*)", - "device_id": "iPad15,8", - "order": "N/A*", - "full_family": "iPad (11th Gen)" + "model": "A3082/A3289/A3290/A3291 (EMC 8692*)", + "device_id": "iPhone17,4", + "order": "MXUW3LL/A*", + "full_family": "iPhone 16 Plus" }, { - "full_name": "Apple Watch Series 10 (41mm, Aluminum)", + "full_name": "Apple Watch Series 10", "introduced": "September 9, 2024", "discontinued": "N/A", - "model": "A2997 (EMC N/A*)", - "device_id": "Watch7,8", - "order": "N/A*", - "full_family": "Apple Watch Series 10" + "model": "A3001/A3002 (EMC 8733*)", + "device_id": "Watch7,10", + "order": "MWX33LW/A*", + "full_family": "Watch Series 10 42 mm" }, { - "full_name": "Apple Watch Series 10 (45/46mm, Aluminum)", + "full_name": "Apple Watch Series 10", "introduced": "September 9, 2024", "discontinued": "N/A", - "model": "A2999 (EMC N/A*)", - "device_id": "Watch7,9", - "order": "N/A*", - "full_family": "Apple Watch Series 10" + "model": "A3003/A3206 (EMC 8737*)", + "device_id": "Watch7,11", + "order": "MWY03LW/A*", + "full_family": "Watch Series 10 46 mm" }, { - "full_name": "Apple Watch Series 10 (Titanium)", + "full_name": "Apple Watch Series 10", "introduced": "September 9, 2024", "discontinued": "N/A", - "model": "A3000 (EMC N/A*)", - "device_id": "Watch7,9", - "order": "N/A*", - "full_family": "Apple Watch Series 10" + "model": "A2997 (EMC 8729*)", + "device_id": "Watch7,8", + "order": "MWWA3LW/A*", + "full_family": "Watch Series 10 42 mm" }, { - "full_name": "Apple Watch Ultra 2", - "introduced": "September 12, 2023", + "full_name": "Apple Watch Series 10", + "introduced": "September 9, 2024", "discontinued": "N/A", - "model": "A2986 (EMC N/A*)", - "device_id": "Watch7,4", - "order": "N/A*", - "full_family": "Apple Watch Ultra 2" + "model": "A2999/A3000 (EMC 8735*)", + "device_id": "Watch7,9", + "order": "MWWL3LW/A*", + "full_family": "Watch Series 10 46 mm" } -] +] \ No newline at end of file diff --git a/lib/units/ios-device/plugins/util/iosutil.js b/lib/units/ios-device/plugins/util/iosutil.js deleted file mode 100755 index 649b9db758..0000000000 --- a/lib/units/ios-device/plugins/util/iosutil.js +++ /dev/null @@ -1,168 +0,0 @@ -import logger from '../../../../util/logger.js' -import bluebird from 'bluebird' -import _ from 'lodash' -const {Promise} = bluebird -const log = logger.createLogger('iosutil') - -import devices from './devices.json' with {type: 'json'} - -/** - * @param {string} key key from wire - * @returns {string | null} string to send to wda - */ -export function asciiparser(key) { - switch (key) { - case 'tab': - return '\x09' - case 'enter': - return '\r' - case 'del': - return '\x08' - // Disable keys (otherwise it sends the first character of key string on default case) - case 'dpad_left': - return '\v' - case 'dpad_up': - return '\0' - case 'dpad_right': - return '\f' - case 'dpad_down': - return '\x18' - case 'caps_lock': - case 'escape': - case 'home': - return null - default: - return key - } -} - -/** @typedef {'PORTRAIT' | 'LANDSCAPE' | 'UIA_DEVICE_ORIENTATION_LANDSCAPERIGHT' | 'UIA_DEVICE_ORIENTATION_PORTRAIT_UPSIDEDOWN'} Orientation */ - -/** - * @param {number} degree angle - * @returns {Orientation | null} orientation for WDA - */ -export function degreesToOrientation(degree) { - switch (degree) { - case 0: - return 'PORTRAIT' - case 90: - return 'LANDSCAPE' - case 180: - return 'UIA_DEVICE_ORIENTATION_PORTRAIT_UPSIDEDOWN' - case 270: - return 'UIA_DEVICE_ORIENTATION_LANDSCAPERIGHT' - } - return null -} - - -/** - * @param {Orientation} orientation orientation from wda - * @returns {number} Angle of rotation - */ -// eslint is not aware of typescript - -export function orientationToDegrees(orientation) { - switch (orientation) { - case 'PORTRAIT': - return 0 - case 'LANDSCAPE': - return 90 - case 'UIA_DEVICE_ORIENTATION_PORTRAIT_UPSIDEDOWN': - return 180 - case 'UIA_DEVICE_ORIENTATION_LANDSCAPERIGHT': - return 270 - } -} - -/** - * @param {any} orientation - * @param {{ fromX: number; fromY: number; toX: number; toY: number; duration: any; }} params - * @param {{ width: number; height: number; }} deviceSize - */ -export function swipe(orientation, params, deviceSize) { - switch (orientation) { - case 'PORTRAIT': - return { - fromX: params.fromX * deviceSize.width, - fromY: params.fromY * deviceSize.height, - toX: params.toX * deviceSize.width, - toY: params.toY * deviceSize.height, - duration: params.duration - } - case 'LANDSCAPE': - return { - fromX: params.fromX * deviceSize.width, - fromY: params.fromY * deviceSize.height, - toX: params.toX * deviceSize.width, - toY: params.toY * deviceSize.height, - duration: params.duration - } - default: - return { - fromX: params.fromX * deviceSize.width, - fromY: params.fromY * deviceSize.height, - toX: params.toX * deviceSize.width, - toY: params.toY * deviceSize.height, - duration: params.duration - } - } -} - -/** - * @param {any} host - * @param {any} port - */ -export function getUri(host, port) { - return `http://${host}:${port}` -} - -/** - * @param {any} state - */ -export function batteryState(state) { - switch (state) { - case 0: - return 'full' - case 1: - return 'unplugged' - case 2: - return 'charging' - case 3: - return 'full' - default: - break - } -} - -/** - * @param {number | string} level - */ -export function batteryLevel(level) { - switch (level) { - case -1: - return 1 - default: - if (typeof level === 'string') { - return Math.round(parseInt(level, 10)) - } - else { - return Math.round(level) - } - } -} - -const deviceById = _.keyBy(devices, 'device_id') - -/** - * @param {string} identifier - * @returns {string} device family name - */ -export function getModelName(identifier) { - const deviceInfo = deviceById[identifier] - if(deviceInfo) { - return deviceInfo.full_family || `${identifier} (unknown full family)` - } - return identifier -} diff --git a/lib/units/ios-device/plugins/util/iosutil.ts b/lib/units/ios-device/plugins/util/iosutil.ts new file mode 100755 index 0000000000..d95f60a836 --- /dev/null +++ b/lib/units/ios-device/plugins/util/iosutil.ts @@ -0,0 +1,106 @@ +import {IOSBatteryState, IOSOrientation, SwipeParams} from '../wda/WDAClient.js' +import _ from 'lodash' +import devices from './devices.json' with {type: 'json'} + +export function asciiparser(key: string): string | null { + switch (key) { + case 'tab': + return '\x09' + case 'enter': + return '\r' + case 'del': + return '\x08' + // Disable keys (otherwise it sends the first character of key string on default case) + case 'dpad_left': + return '\v' + case 'dpad_up': + return '\0' + case 'dpad_right': + return '\f' + case 'dpad_down': + return '\x18' + case 'caps_lock': + case 'escape': + case 'home': + return null + default: + return key + } +} + +export function degreesToOrientation(degree: number): IOSOrientation { + switch (degree) { + case 90: + return 'LANDSCAPE' + case 180: + return 'UIA_DEVICE_ORIENTATION_PORTRAIT_UPSIDEDOWN' + case 270: + return 'UIA_DEVICE_ORIENTATION_LANDSCAPERIGHT' + default: // 0deg or any other + return 'PORTRAIT' + } +} + +export function orientationToDegrees(orientation: IOSOrientation): number { + switch (orientation) { + case 'PORTRAIT': + return 0 + case 'LANDSCAPE': + return 90 + case 'UIA_DEVICE_ORIENTATION_PORTRAIT_UPSIDEDOWN': + return 180 + case 'UIA_DEVICE_ORIENTATION_LANDSCAPERIGHT': + return 270 + } +} + +export function swipe(params: SwipeParams, deviceSize: Record<'width' | 'height', number>) { + return { + fromX: params.fromX * deviceSize.width, + fromY: params.fromY * deviceSize.height, + toX: params.toX * deviceSize.width, + toY: params.toY * deviceSize.height, + duration: params.duration || 0 + } +} + +export function getUri(host: string, port: number | string) { + return `http://${host}:${port}` +} + +export function batteryState(state: number): IOSBatteryState { + switch (state) { + case 0: + return 'full' + case 1: + return 'unplugged' + case 2: + return 'charging' + default: + return 'full' + } +} + +export function batteryLevel(level: number | string) { + if (level === -1) { + return 1 + } + + if (typeof level === 'string') { + return Math.round(parseInt(level, 10)) + } + + return Math.round(level) +} + +const deviceById = _.keyBy(devices, 'device_id') + +export function getModelName(identifier: string): string | null { + if (!identifier) return null + const deviceInfo = deviceById[identifier] + if (deviceInfo) { + return deviceInfo.full_family || null + } + + return identifier +} diff --git a/lib/units/ios-device/plugins/wda/WDAClient.ts b/lib/units/ios-device/plugins/wda/WDAClient.ts new file mode 100644 index 0000000000..86136626e3 --- /dev/null +++ b/lib/units/ios-device/plugins/wda/WDAClient.ts @@ -0,0 +1,835 @@ +import * as iosutil from '../util/iosutil.js' +import EventEmitter from 'events' + +export type IOSOrientation = 'PORTRAIT' | 'LANDSCAPE' | 'UIA_DEVICE_ORIENTATION_LANDSCAPERIGHT' | 'UIA_DEVICE_ORIENTATION_PORTRAIT_UPSIDEDOWN' +export type IOSBatteryState = 'full' | 'unplugged' | 'charging' +export type IOSSDKVersion = string + +export type DisplayInfo = Record<'width' | 'height' | 'scale', number> + +type Enumerate = Acc['length'] extends N + ? Acc[number] + : Enumerate + +type Range = Exclude, Enumerate> + +type BatteryLevel = Range<0, 100> +type RotationDegree = Range<0, 270> + +type DeviceType = 'iPhone' | 'iPad' | 'Apple TV' + +type RequestMethod = 'GET' | 'POST' | 'DELETE' +interface RequestOptions { + method: RequestMethod + uri: string + body?: any +} + +interface TouchParams { + x: number + y: number +} + +export interface SwipeParams { + fromX: number + fromY: number + toX: number + toY: number + duration?: any +} + +interface DeviceSize { + width: number + height: number +} + +interface WDAEvents { + connected: [] + disconnected: [] + session: [IOSSDKVersion | null] + battery: [IOSBatteryState, BatteryLevel] + rotation: [IOSOrientation | null, RotationDegree] + display: [DisplayInfo] + error: [Error] +} + +export default class WdaClient extends EventEmitter { + public readonly baseUrl: string + public orientation: IOSOrientation | null = null + + private requestTimeout: number = 10_000 // 10 sec + + private deviceSize: DeviceSize | null = null + private deviceType: DeviceType | null = null + private displayInfo: DisplayInfo = { + width: 0, + height: 0, + scale: 0 + } + + private sessionId: string | null = null + + // Touch state machine + private touchState: 'idle' | 'down' | 'moving' = 'idle' + private touchBusy = false + private touchStartPos: TouchParams = {x: 0, y: 0} + private touchStartTime = 0 + private moveBuffer: Array<{x: number, y: number, time: number}> = [] + + // Tuning constants for gesture detection + private static readonly TAP_MAX_DURATION_MS = 500 + private static readonly TAP_MAX_DISTANCE = 0.02 + + // Curvature detection: pre-squared threshold in normalized (0-1) coords + // 0.015 = 6pt deviation on a 375pt-wide iPhone screen + private static readonly CURVE_THRESHOLD_SQ = 0.015 * 0.015 + private static readonly MAX_CURVE_POINTS = 5 + + private upperCase = false + private isRotating = false + + private connected = false + private ready = false + + public sdk: IOSSDKVersion | null = null + + constructor(parameters: { + wdaHost: string, + wdaPort: number, + requestTimeout?: number + }) { + super() + this.baseUrl = iosutil.getUri(parameters.wdaHost, parameters.wdaPort) + this.requestTimeout = parameters.requestTimeout ?? this.requestTimeout + } + + async connect() { + if (!this.connected) { + await this.healthCheck() + } + } + + async healthCheck() { + await this.requestStatus() + + if (!this.connected && this.ready) { + this.emit('connected') + } + + this.connected = this.ready + + if (!this.connected) { + this.emit('disconnected') + } + + return this.ready + } + + setDeviceType(rawType: string) { + switch (rawType) { + case 'apple-tv': + this.deviceType = 'Apple TV' + break + case 'ipad': + this.deviceType = 'iPad' + break + default: + this.deviceType = 'iPhone' + } + } + + async requestStatus() { + const statusResponse = await this.handleRequest({ + method: 'GET', + uri: `${this.baseUrl}/status` + }) + + if (!statusResponse) { + this.sessionId = null + this.ready = false + return + } + + this.setDeviceType(statusResponse.value.device) + + this.sessionId = statusResponse.sessionId || null + this.ready = statusResponse.value.ready + } + + async requestSession() { + const sessionResponse = await this.handleRequest({ + method: 'GET', + uri: `${this.baseUrl}/session/${this.sessionId}` + }) + + this.sessionId = sessionResponse?.sessionId || null + + if (!sessionResponse) { + return + } + + this.sdk = sessionResponse.value.capabilities.sdkVersion + this.setDeviceType(sessionResponse.value.capabilities.device) + + this.emit('session', this.sdk) + } + + async setSize() { + if (this.deviceSize !== null) { + return + } + + await this.requestDisplayInfo() + const {width, height, scale} = this.displayInfo + + if (!width || !height || !scale) { + this.emit('error', new Error('WDAClient error: displayInfo returned zero values')) + return + } + + // Set device size in points based on orientation, default is PORTRAIT + // WDA screenSize returns values in points; gesture endpoints expect point coordinates + if ( + !this.orientation || ['PORTRAIT', 'UIA_DEVICE_ORIENTATION_PORTRAIT_UPSIDEDOWN'].includes(this.orientation) + ) { + this.deviceSize = {height, width} + } else if ( + ['LANDSCAPE', 'UIA_DEVICE_ORIENTATION_LANDSCAPERIGHT'].includes(this.orientation) + ) { + this.deviceSize = { + height: width, + width: height + } + } else if (this.deviceType === 'Apple TV') { + this.deviceSize = {height, width} + } + } + + async startSession() { + await this.requestStatus() + + const processSession = () => + Promise.all([ + ... (this.deviceType !== 'Apple TV' ? [ + this.requestOrientation(), + this.requestBatteryInfo(), + this.requestDisplayInfo() + ] : []), + this.requestSession() + ]) + + if (this.sessionId) { + await processSession() + return true + } + + const sessionResponse = await this.handleRequest({ + method: 'POST', + uri: `${this.baseUrl}/session`, + body: {capabilities: {}} + }) + + if (sessionResponse?.sessionId) { + this.sessionId = sessionResponse.sessionId + await processSession() + + return true + } + + return false + } + + async stopSession() { + if (!this.sessionId) { + return + } + + const currentSessionId = this.sessionId + this.sessionId = null + + await this.handleRequest({ + method: 'DELETE', + uri: `${this.baseUrl}/session/${currentSessionId}` + }) + } + + async requestOrientation() { + const orientationResponse = await this.handleRequest({ + method: 'GET', + uri: `${this.baseUrl}/session/${this.sessionId}/orientation` + }) + + this.orientation = orientationResponse?.value || null + this.setSize() + } + + async requestBatteryInfo() { + const batteryInfoResponse = await this.handleRequest({ + method: 'GET', + uri: `${this.baseUrl}/session/${this.sessionId}/wda/batteryInfo` + }) + + if (!batteryInfoResponse) { + return + } + + const batteryState = iosutil.batteryState(batteryInfoResponse.value.state) + const batteryLevel = iosutil.batteryLevel(batteryInfoResponse.value.level) as BatteryLevel + this.emit('battery', batteryState, batteryLevel) + } + + async requestDisplayInfo() { + if (this.displayInfo.width) return + const displayInfoResponse = await this.handleRequest({ + method: 'GET', + uri: `${this.baseUrl}/session/${this.sessionId}/wda/screen` + }) + + if (!displayInfoResponse) { + return + } + + this.displayInfo = { + ...(displayInfoResponse?.value?.screenSize || this.displayInfo), + scale: displayInfoResponse?.value?.scale || 0, + } + + this.emit('display', this.displayInfo) + } + + async typeKey(value: string) { + if (!value) { + return + } + + // register keyDown and keyUp for current char + if (this.upperCase) { + value = value.toUpperCase() + } + + if (this.deviceType === 'Apple TV') { + const sendKey = (name: string) => this.handleRequest({ + method: 'POST', + uri: `${this.baseUrl}/session/${this.sessionId}/wda/pressButton`, + body: {name} + }) + + // Apple TV keys + switch (value) { + case '\v': + await sendKey('left') + break + + case '\f': + await sendKey('right') + break + + case '\0': + await sendKey('up') + break + + case '\x18': + await sendKey('down') + break + + case '\r': + await sendKey('select') + break + + default: + break + } + + return + } + + await this.handleRequest({ + method: 'POST', + uri: `${this.baseUrl}/session/${this.sessionId}/actions`, + body: { + actions: [ + { + type: 'key', + id: 'keyboard', + actions: [ + {type: 'keyDown', value}, + {type: 'keyUp', value} + ], + } + ] + } + }) + } + + async homeBtn() { + if (this.deviceType === 'Apple TV') { + await this.handleRequest({ + method: 'POST', + uri: `${this.baseUrl}/session/${this.sessionId}/wda/pressButton`, + body: {name: 'menu'} + }) + + return + } + + await this.handleRequest({ + method: 'POST', + uri: `${this.baseUrl}/session/${this.sessionId}/wda/pressButton`, + body: {name: 'home'} + }) + } + + touchDown(params: TouchParams) { + if (this.deviceType === 'Apple TV') return + + if (this.touchBusy) { + // WDA is still processing a previous gesture -- drop this one + this.touchState = 'idle' + return + } + + this.touchState = 'down' + this.touchStartPos = {x: params.x, y: params.y} + this.touchStartTime = Date.now() + this.moveBuffer = [] + } + + touchMove(params: TouchParams) { + if (this.touchState === 'idle' || this.deviceType === 'Apple TV') return + + // Ignore micro-movements (jitter) while still in 'down' state + const dx = Math.abs(params.x - this.touchStartPos.x) + const dy = Math.abs(params.y - this.touchStartPos.y) + + const filtered = this.touchState === 'down' && dx < WdaClient.TAP_MAX_DISTANCE && dy < WdaClient.TAP_MAX_DISTANCE + + if (filtered) { + return + } + + this.touchState = 'moving' + this.moveBuffer.push({x: params.x, y: params.y, time: Date.now()}) + } + + async touchUp() { + if (this.touchState === 'idle') return + + // Apple TV directional input based on touch position + if (this.deviceType === 'Apple TV') { + await this.handleAppleTVTouch() + this.touchState = 'idle' + return + } + + if (!this.deviceSize) { + this.touchState = 'idle' + return + } + + const duration = Date.now() - this.touchStartTime + + this.touchBusy = true + try { + if (this.touchState === 'down') { + // No significant movement -- tap or long press + if (duration >= WdaClient.TAP_MAX_DURATION_MS) { + await this.performLongPress(duration) + } else { + await this.performTap() + } + } else { + // Had movement -- send complete swipe as single W3C action + await this.performSwipe() + } + } finally { + this.touchBusy = false + } + + this.touchState = 'idle' + } + + private async performTap() { + if (!this.deviceSize) return + + const x = this.touchStartPos.x * this.deviceSize.width + const y = this.touchStartPos.y * this.deviceSize.height + + await this.handleRequest({ + method: 'POST', + uri: `${this.baseUrl}/session/${this.sessionId}/wda/touchAndHold`, + body: {x, y, duration: 0.1} + }) + } + + private async performLongPress(durationMs: number) { + if (!this.deviceSize) return + + const x = this.touchStartPos.x * this.deviceSize.width + const y = this.touchStartPos.y * this.deviceSize.height + + await this.handleRequest({ + method: 'POST', + uri: `${this.baseUrl}/session/${this.sessionId}/wda/touchAndHold`, + body: {x, y, duration: durationMs / 1000} + }) + } + + private static readonly MIN_SWIPE_VELOCITY = 500 // points per second + + private async performSwipe() { + if (!this.deviceSize || this.moveBuffer.length === 0) return + + const w = this.deviceSize.width + const h = this.deviceSize.height + + const lastMove = this.moveBuffer[this.moveBuffer.length - 1] + + const fromX = this.touchStartPos.x * w + const fromY = this.touchStartPos.y * h + const toX = lastMove.x * w + const toY = lastMove.y * h + + const dx = toX - fromX + const dy = toY - fromY + const distance = Math.sqrt(dx * dx + dy * dy) + + // --- O(n) curvature detection in normalized space, zero allocations --- + const lineDx = lastMove.x - this.touchStartPos.x + const lineDy = lastMove.y - this.touchStartPos.y + const lineLenSq = lineDx * lineDx + lineDy * lineDy + + let maxCrossSq = 0 + for (let i = 0, len = this.moveBuffer.length; i < len; i++) { + const p = this.moveBuffer[i] + const cross = (p.y - this.touchStartPos.y) * lineDx - (p.x - this.touchStartPos.x) * lineDy + const cSq = cross * cross + if (cSq > maxCrossSq) maxCrossSq = cSq + } + + const isCurved = lineLenSq > 0 && maxCrossSq > WdaClient.CURVE_THRESHOLD_SQ * lineLenSq + + // Derive velocity from actual gesture speed, with a minimum floor + const gestureDurationSec = Math.max((lastMove.time - this.touchStartTime) / 1000, 0.05) + const velocity = Math.max(distance / gestureDurationSec, WdaClient.MIN_SWIPE_VELOCITY) + + if (isCurved) { + await this.performCurvedSwipe(w, h, gestureDurationSec) + return + } + + // Straight line: fast native endpoint (unchanged) + await this.handleRequest({ + method: 'POST', + uri: `${this.baseUrl}/session/${this.sessionId}/wda/pressAndDragWithVelocity`, + body: { + fromX, + fromY, + toX, + toY, + pressDuration: 0, + velocity, + holdDuration: 0, + }, + }) + } + + private async performCurvedSwipe(w: number, h: number, totalDurationSec: number) { + const buf = this.moveBuffer + const n = buf.length + const pointCount = Math.min(n, WdaClient.MAX_CURVE_POINTS) + const step = n > 1 ? (n - 1) / (pointCount - 1) : 0 + const moveDurationMs = Math.max(Math.round((totalDurationSec * 1000) / (pointCount + 1)), 10) + + const actions: Array> = [ + {type: 'pointerMove', duration: 0, x: Math.round(this.touchStartPos.x * w), y: Math.round(this.touchStartPos.y * h), origin: 'viewport'}, + {type: 'pointerDown', button: 0}, + ] + + for (let i = 0; i < pointCount; i++) { + const idx = Math.round(i * step) + actions.push({ + type: 'pointerMove', + duration: moveDurationMs, + x: Math.round(buf[idx].x * w), + y: Math.round(buf[idx].y * h), + origin: 'viewport', + }) + } + + actions.push({type: 'pointerUp', button: 0}) + + await this.handleRequest({ + method: 'POST', + uri: `${this.baseUrl}/session/${this.sessionId}/actions`, + body: { + actions: [{ + type: 'pointer', + id: 'finger1', + parameters: {pointerType: 'touch'}, + actions, + }], + }, + }) + } + + private async handleAppleTVTouch() { + if (!this.deviceSize) return + + const x = this.touchStartPos.x * this.deviceSize.width + const y = this.touchStartPos.y * this.deviceSize.height + + if (x < 0 || y < 0) return + + if (x < 300) { + await this.pressButtonSendRequest('left') + return + } + if (x > 1650) { + await this.pressButtonSendRequest('right') + return + } + if (y > 850) { + await this.pressButtonSendRequest('down') + return + } + if (y < 250) { + await this.pressButtonSendRequest('up') + return + } + + await this.pressButtonSendRequest('select') + } + + async tapDeviceTreeElement(label: string) { + const response = await this.handleRequest({ + method: 'POST', + uri: `${this.baseUrl}/session/${this.sessionId}/elements`, + body: { + using: 'link text', + value: `label=${label}` + } + }) + + const {ELEMENT} = response.value[0] + await this.handleRequest({ + method: 'POST', + uri: `${this.baseUrl}/session/${this.sessionId}/element/${ELEMENT}/click` + }) + } + + async doubleClick() { + if (this.touchState === 'moving' || !this.deviceSize) { + return + } + + await this.handleRequest({ + method: 'POST', + uri: `${this.baseUrl}/session/${this.sessionId}/wda/doubleTap`, + body: { + x: this.touchStartPos.x * this.deviceSize.width, + y: this.touchStartPos.y * this.deviceSize.height + } + }) + } + + async openUrl(url: string) { + await this.handleRequest({ + method: 'POST', + uri: `${this.baseUrl}/session/${this.sessionId}/url`, + body: {url} + }) + } + + screenshot() { + return this.handleRequest({ + method: 'GET', + uri: `${this.baseUrl}/screenshot` + }) + } + + async rotation(orientation: IOSOrientation) { + if (this.isRotating) return + this.isRotating = true + await this.handleRequest({ + method: 'POST', + uri: `${this.baseUrl}/session/${this.sessionId}/orientation`, + body: {orientation} + }) + + await this.requestOrientation() + + const rotationDegrees = ( + this.orientation ? iosutil.orientationToDegrees(this.orientation) : 0 + ) as RotationDegree + + this.emit('rotation', this.orientation, rotationDegrees) + this.isRotating = false + } + + async getTreeElements(): Promise { + return await this.handleRequest({ + method: 'GET', + uri: `${this.baseUrl}/source?format=json` + }) || null + } + + async pressButtonSendRequest(name: string) { + await this.handleRequest({ + method: 'POST', + uri: `${this.baseUrl}/session/${this.sessionId}/wda/pressButton`, + body: {name} + }) + + return true + } + + switchCase() { + this.upperCase = !this.upperCase + } + + async appActivate(bundleId: string) { + await this.handleRequest({ + method: 'POST', + uri: `${this.baseUrl}/session/${this.sessionId}/wda/apps/activate`, + body: {bundleId} + }) + } + + async lock() { + await this.handleRequest({ + method: 'POST', + uri: `${this.baseUrl}/session/${this.sessionId}/wda/lock` + }) + } + + async pressPower() { + const response = await this.handleRequest({ + method: 'GET', + uri: `${this.baseUrl}/session/${this.sessionId}/wda/locked` + }) + + if (!response) { + return + } + + await this.handleRequest({ + method: 'POST', + uri: `${this.baseUrl}/session/${this.sessionId}/wda/${response?.value ? 'un' : ''}lock` + }) + } + + async getClipBoard() { + const response = await this.handleRequest({ + method: 'POST', + uri: `${this.baseUrl}/session/${this.sessionId}/wda/getPasteboard` + }) + + if (!response) { + return 'No clipboard data' + } + + return Buffer.from(JSON.parse(response).value, 'base64').toString('utf-8') || 'No clipboard data' + } + + /** + * Handles WDA HTTP requests with graceful error recovery using native fetch API. + * + * WDA can have transient failures that shouldn't crash the device connection. + * The device notifier and lifecycle manager handle fatal errors separately. + */ + private handleRequest = async (requestOpt: RequestOptions): Promise => { + const isTouchReq = requestOpt.uri.includes('/actions') || requestOpt.uri.includes('/touchAndHold') || requestOpt.uri.includes('/doubleTap') || requestOpt.uri.includes('/pressAndDragWithVelocity') + const startMs = isTouchReq ? Date.now() : 0 + + try { + const response = await fetch(requestOpt.uri, { + method: requestOpt.method, + headers: {'Content-Type': 'application/json'}, + body: requestOpt.body ? JSON.stringify(requestOpt.body) : undefined, + signal: AbortSignal.timeout(this.requestTimeout) + }) + + const body = await response.json() + + return body + } + catch (err: any) { + this.emit('error', new Error(`WDA request error: ${err?.error?.value?.message || err?.message || err}`)) + } + } + + async pressButton(key: string) { + const aApple = + (name: string) => this.appActivate(`com.apple.${name}`) + + switch (key) { + case 'settings': + if (this.deviceType === 'Apple TV') { + return aApple('TVSettings') + } + + return aApple('Preferences') + + case 'store': + if (this.deviceType === 'Apple TV') { + return aApple('TVAppStore') + } + + return aApple('AppStore') + + case 'volume_up': + return this.pressButtonSendRequest('volumeUp') + + case 'volume_down': + return this.pressButtonSendRequest('volumeDown') + + case 'power': + return this.pressPower() + + case 'camera': + return aApple('camera') + + case 'search': + if (this.deviceType === 'Apple TV') { + return aApple('TVSearch') + } + + return aApple('mobilesafari') + + case 'finder': + return aApple('findmy') + + case 'home': + return this.homeBtn() + + case 'mute': + for (let i = 0; i < 16; i++) { + if (!await this.pressButtonSendRequest('volumeDown')) { + return false + } + await new Promise(r => setTimeout(r, 600)) + } + + return true + + case 'switch_charset': + return this.switchCase() + + // Media button requests in case there's future WDA compatibility + case 'media_play_pause': + return false + case 'media_stop': + return false + case 'media_next': + return false + case 'media_previous': + return false + case 'media_fast_forward': + return false + case 'media_rewind': + return false + default: + return this.pressButtonSendRequest(key) + } + } +} diff --git a/lib/units/ios-device/plugins/wda/WDAService.js b/lib/units/ios-device/plugins/wda/WDAService.js deleted file mode 100644 index e937f7b200..0000000000 --- a/lib/units/ios-device/plugins/wda/WDAService.js +++ /dev/null @@ -1,136 +0,0 @@ -import {resolve} from 'path' -import logger from '../../../../util/logger.js' -import childProcess from 'child_process' -import assert from 'assert' -import EventEmitter from 'events' -import {tmpdir} from 'os' -import {writeFileSync} from 'fs' -// @ts-ignore -import lockfile from 'proper-lockfile' - -const log = logger.createLogger('wda') -const lockFilePath = resolve(tmpdir(), 'wda') -writeFileSync(lockFilePath, '') - -const waitNLock = async(attempt = 0) => { - try { - return await lockfile.lock(lockFilePath, { - stale: 10 * 60 * 1000, // 10 min - update: 1500 // per 1.5 sec - }) - } - catch (/** @type {any} */ e) { // if locked - try again later - if (e.code !== 'ELOCKED' || ++attempt >= 720) { // max 720 attempts - 30 min - throw e - } - await new Promise(r => setTimeout(r, 2500)) // retry per 2.5 sec - return waitNLock() - } -} - -export default class WDAService { - - /** - * @param {string | null} path wda path - */ - constructor(path) { - if (path) { - this.wdaPath = path - } - else { - this.wdaPath = resolve(import.meta.dirname, '../../../../../WebDriverAgent') - } - - /** - * @type {Object.} amogus - */ - this.testProcs = {} - } - - async prepareWda(simulator, udid) { - const buildProc = childProcess.spawn( - `xcodebuild -project WebDriverAgent.xcodeproj -scheme WebDriverAgentRunner -destination 'platform=iOS${ - simulator ? ' Simulator' : '' - },id=${udid}' -allowProvisioningUpdates build`, - {cwd: this.wdaPath, shell: true, timeout: 10 * 60 * 1000, stdio: 'inherit'} - ) - assert(buildProc) - await EventEmitter.once(buildProc, 'exit') - if (buildProc.exitCode !== 0) { - throw Error(`Could not build wda. Exit code is ${buildProc.exitCode}`) - } - } - - /** - * - * @param {string} udid device udid - * @param {number=} port - * @param {number=} screenPort - * @returns {Promise} nothing - */ - async start(udid, port, screenPort) { - await this.cleanup(udid) - - const unlock = await waitNLock() - try { - await this.prepareWda(!!port, udid) - const portArg = port ? ` USE_PORT=${port}` : '' - const screenPortArg = screenPort ? ` MJPEG_SERVER_PORT=${screenPort}` : '' - const command = - 'xcodebuild -project WebDriverAgent.xcodeproj ' + - '-scheme WebDriverAgentRunner ' + - `-destination "id=${udid}" ` + - '-allowProvisioningUpdates ' + - 'test' + - portArg + screenPortArg - - const testproc = childProcess.spawn(command, {cwd: this.wdaPath, shell: true, stdio: 'pipe'}) - this.testProcs[udid] = testproc - await new Promise((resolve, reject) => { // Wait for server init - assert(testproc) - testproc.on('exit', reject) - testproc?.stdout?.on('data', (chunk) => { - const findRes = /ServerURLHere->(.*)<-ServerURLHere/g.exec(chunk) - if (findRes) { - assert(testproc) - testproc.removeListener('exit', reject) - resolve(findRes[0]) - } - }) - testproc?.stdout?.pipe(process.stdout) - testproc?.stderr?.pipe(process.stderr) - }) - testproc.on('exit', async() => { - await this.cleanup(udid) - log.error(`WDA process for ${udid} exited`) - }) - } - catch (e) { - await this.cleanup(udid) - throw e - } - finally { - await unlock() - } - - } - - /** - * @param {string} udid device udid - * @returns {Promise} nothing - */ - async cleanup(udid) { - log.debug('Stopped WDA') - if (!this.testProcs[udid]) { - return - } - - const proc = this.testProcs[udid] - delete this.testProcs[udid] - proc.kill(9) - if (proc.exitCode === null) { - await EventEmitter.once(proc, 'exit') - } - } -} - diff --git a/lib/units/ios-device/plugins/wda/client.js b/lib/units/ios-device/plugins/wda/client.js deleted file mode 100644 index 165d59c557..0000000000 --- a/lib/units/ios-device/plugins/wda/client.js +++ /dev/null @@ -1,732 +0,0 @@ -import net from 'net' -import request from 'request-promise' // TODO: replace with fastest/standart lib -import Promise from 'bluebird' -import syrup from '@devicefarmer/stf-syrup' -import logger from '../../../../util/logger.js' -import * as iosutil from '../util/iosutil.js' -import wireutil from '../../../../wire/util.js' -import wire from '../../../../wire/index.js' -import lifecycle from '../../../../util/lifecycle.js' -import db from '../../../../db/index.js' -import dbapi from '../../../../db/api.js' -import devicenotifier from '../devicenotifier.js' -import info from '../info/index.js' -import push from '../../../base-device/support/push.js' -const LOG_REQUEST_MSG = 'Request has been sent to WDA with data: ' -export default syrup.serial() - .dependency(devicenotifier) - .dependency(push) - .dependency(info) - .define(async(options, notifier, push, info) => { - const log = logger.createLogger('wdaClient') - log.info('WdaClient.js initializing...') - await db.connect() - const socket = new net.Socket() // wtf why is this not part of the WdaClient object? - - class WdaClient { - baseUrl = iosutil.getUri(options.wdaHost, options.wdaPort) - sessionId = null - orientation = null - touchDownParams = {} - tapStartAt = 0 - - /** @type {any}*/ - deviceSize = null - - /** - * @type {{ type: string; value: any; }[]} - */ - typeKeyActions = [] - typeKeyTimerId = null - typeKeyDelay = 250 - upperCase = false - isSwiping = false - isRotating = false - deviceType = null - getDeviceType() { - if (this.deviceType !== null) { - return this.deviceType - } - return dbapi.getDeviceType(options.serial).then((deviceType) => { - if (!deviceType) { - return null - } - log.info('Reusing device type value: ', deviceType) - this.deviceType = deviceType - return this.deviceType - }).catch((err) => { - log.error('Error getting device type from DB') - return lifecycle.fatal(err) - }) - } - startSession() { - log.info('verifying wda session status...') - this.getDeviceType() - const params = { - capabilities: {}, - } - return this.handleRequest({ - method: 'POST', - uri: `${this.baseUrl}/session`, - body: params, - json: true, - }) - .then((sessionResponse) => { - return this.handleRequest({ - method: 'GET', - uri: `${this.baseUrl}/status`, - json: true, - }) - .then((statusResponse) => { - log.info(`status response: ${JSON.stringify(statusResponse)}`) - // handles case of existing session - if (statusResponse.sessionId) { - this.sessionId = statusResponse.sessionId - log.info(`reusing existing wda session: ${this.sessionId}`) - this.setStatus(3) - if (this.deviceType !== 'Apple TV') { - this.getOrientation() - this.batteryIosEvent() - } - this.setVersion(sessionResponse) - return this.size() - } - log.info('starting wda session...') - return this.handleRequest({ - method: 'POST', - uri: `${this.baseUrl}/session`, - body: params, - json: true, - }) - .then((sessionResponse) => { - log.info(`startSession response: ${JSON.stringify(sessionResponse)}`) - this.setVersion(sessionResponse) - this.sessionId = sessionResponse.sessionId - log.info(`sessionId: ${this.sessionId}`) - if (this.deviceType !== 'Apple TV') { - this.getOrientation() - this.batteryIosEvent() - } - this.setStatus(3) - return this.size() - }) - .catch((err) => { - log.error('"startSession" No valid response from web driver!', err) - return Promise.reject(err) - }) - }) - }) - } - stopSession() { - log.info('stopping wda session: ', this.sessionId) - let currentSessionId = this.sessionId - this.sessionId = null - if (currentSessionId === null) { - return Promise.resolve() - } - return this.handleRequest({ - method: 'DELETE', - uri: `${this.baseUrl}/session/${currentSessionId}` - }) - } - setStatus(status) { - push.send([ - wireutil.global, - wireutil.envelope(new wire.DeviceStatusMessage(options.serial, status)) - ]) - } - typeKey(params) { - // collect several chars till the space and do mass actions... - if (!params.value || !params.value[0]) { - return - } - let [value] = params.value - // register keyDown and keyUp for current char - if (this.upperCase) { - value = value.toUpperCase() - } - this.typeKeyActions.push({type: 'keyDown', value}) - this.typeKeyActions.push({type: 'keyUp', value}) - const handleRequest = () => { - const requestParams = { - method: 'POST', - uri: `${this.baseUrl}/session/${this.sessionId}/actions`, - body: { - actions: [ - { - type: 'key', - id: 'keyboard', - actions: this.typeKeyActions, - } - ] - }, - json: true, - } - // reset this.typeKeyActions array as we are going to send word or char(s) by timeout - this.typeKeyActions = [] - if (this.typeKeyTimerId) { - // reset type key timer as we are going to send word or char(s) by timeout - clearTimeout(this.typeKeyTimerId) - this.typeKeyTimerId = null - } - if (this.deviceType !== 'Apple TV') { - return this.handleRequest(requestParams) - } - // Apple TV keys - switch (true) { - case value === '\v': - return this.handleRequest({ - method: 'POST', - uri: `${this.baseUrl}/session/${this.sessionId}/wda/pressButton`, - body: {name: 'left'}, - json: true, - }) - case value === '\f': - return this.handleRequest({ - method: 'POST', - uri: `${this.baseUrl}/session/${this.sessionId}/wda/pressButton`, - body: {name: 'right'}, - json: true, - }) - case value === '\0': - return this.handleRequest({ - method: 'POST', - uri: `${this.baseUrl}/session/${this.sessionId}/wda/pressButton`, - body: {name: 'up'}, - json: true, - }) - case value === '\x18': - return this.handleRequest({ - method: 'POST', - uri: `${this.baseUrl}/session/${this.sessionId}/wda/pressButton`, - body: {name: 'down'}, - json: true, - }) - case value === '\r': - return this.handleRequest({ - method: 'POST', - uri: `${this.baseUrl}/session/${this.sessionId}/wda/pressButton`, - body: {name: 'select'}, - json: true, - }) - default: - break - } - } - if (value === ' ') { - // as only space detected send full word to the iOS device - handleRequest() - } - else { - // reset timer to start tracker again from the latest char. Final flush will happen if no types during this.typeKeyDelay ms - if (this.typeKeyTimerId) { - clearTimeout(this.typeKeyTimerId) - } - // @ts-ignore - this.typeKeyTimerId = setTimeout(handleRequest, this.typeKeyDelay) - } - } - tap(params) { - this.tapStartAt = (new Date()).getTime() - this.touchDownParams = params - } - homeBtn() { - if (this.deviceType !== 'Apple TV') { - return this.handleRequest({ - method: 'POST', - uri: `${this.baseUrl}/session/${this.sessionId}/wda/pressButton`, - body: {name: 'home'}, - json: true - }).then(() => { - // #801 Reset coordinates to Portrait mode after pressing home button - return this.rotation({orientation: 'PORTRAIT'}) - }) - } - else { - // #749: Fixing button action for AppleTV - return this.handleRequest({ - method: 'POST', - uri: `${this.baseUrl}/session/${this.sessionId}/wda/pressButton`, - body: {name: 'menu'}, - json: true - }) - } - } - swipe(params) { - const scale = iosutil.swipe(this.orientation, params, this.deviceSize) - const body = { - actions: [ - { - type: 'pointer', - id: 'finger1', - parameters: {pointerType: 'touch'}, - actions: [ - {type: 'pointerMove', duration: 0, x: scale.fromX, y: scale.fromY}, - - {type: 'pointerMove', - duration: scale.duration * 1000, - x: scale.toX, - // eslint-disable-next-line no-nested-ternary - y: (scale.fromY < scale.toY) ? scale.toY - (scale.toY / 4) : (scale.fromY - scale.toY >= 50 ? scale.toY + (scale.toY / 4) : scale.toY)}, - {type: 'pointerUp'} - ], - } - ], - } - if (this.deviceType === 'Apple TV') { - return log.error('Swipe is not supported') - } - let swipeOperation = () => { - if (!this.isSwiping) { - this.isSwiping = true - this.handleRequest({ - method: 'POST', - uri: `${this.baseUrl}/session/${this.sessionId}/actions`, - body, - json: true, - }).then((response) => { - log.info('swipe response: ', response) - this.isSwiping = false - }) - } - } - return swipeOperation() - } - touchUp() { - if (!this.isSwiping && this.deviceSize) { - let {x, y} = this.touchDownParams - x *= this.deviceSize.width - y *= this.deviceSize.height - if (((new Date()).getTime() - this.tapStartAt) <= 1000 || !this.tapStartAt) { - const body = { - actions: [ - { - type: 'pointer', - id: 'finger1', - parameters: {pointerType: 'touch'}, - actions: [ - {type: 'pointerMove', duration: 0, x, y}, - {type: 'pointerMove', duration: 0, x, y}, - {type: 'pointerUp'} - ], - } - ], - } - if (this.deviceType !== 'Apple TV') { - log.info(options.deviceName) - return this.handleRequest({ - method: 'POST', - uri: `${this.baseUrl}/session/${this.sessionId}/actions`, - body, - json: true, - }) - // else if (deviceType === 'Watch_OS') {...} - } - else { - // Avoid crash, wait until width/height values are available - if (x >= 0 && y >= 0) { - switch (true) { - case x < 300: - return this.handleRequest({ - method: 'POST', - uri: `${this.baseUrl}/session/${this.sessionId}/wda/pressButton`, - body: {name: 'left'}, - json: true, - }) - case x > 1650: - return this.handleRequest({ - method: 'POST', - uri: `${this.baseUrl}/session/${this.sessionId}/wda/pressButton`, - body: {name: 'right'}, - json: true, - }) - case y > 850: - return this.handleRequest({ - method: 'POST', - uri: `${this.baseUrl}/session/${this.sessionId}/wda/pressButton`, - body: {name: 'down'}, - json: true, - }) - case y < 250: - return this.handleRequest({ - method: 'POST', - uri: `${this.baseUrl}/session/${this.sessionId}/wda/pressButton`, - body: {name: 'up'}, - json: true, - }) - default: - return this.handleRequest({ - method: 'POST', - uri: `${this.baseUrl}/session/${this.sessionId}/wda/pressButton`, - body: {name: 'select'}, - json: true, - }) - } - } - } - } - else { - if (this.deviceType === 'Apple TV') { - return log.error('Holding tap is not supported') - } - return this.handleRequest({ - method: 'POST', - uri: `${this.baseUrl}/session/${this.sessionId}/wda/touchAndHold`, - body: {x, y, duration: 1}, - json: true, - }) - } - } - } - tapDeviceTreeElement(message) { - const params = { - using: 'link text', - value: 'label=' + message.label, - } - return new Promise((resolve, reject) => { - this.handleRequest({ - method: 'POST', - uri: `${this.baseUrl}/session/${this.sessionId}/elements`, - body: params, - json: true - }) - .then(response => { - const {ELEMENT} = response.value[0] - return this.handleRequest({ - method: 'POST', - uri: `${this.baseUrl}/session/${this.sessionId}/element/${ELEMENT}/click`, - body: {}, - json: true - }) - }) - .catch(err => { - log.error(err) - }) - }) - } - doubleClick() { - if (!this.isSwiping && this.deviceSize) { - const {x, y} = this.touchDownParams - const params = { - x: x * this.deviceSize.width, - y: y * this.deviceSize.height - } - return this.handleRequest({ - method: 'POST', - uri: `${this.baseUrl}/session/${this.sessionId}/wda/doubleTap`, - body: params, - json: true - }) - } - } - size() { - if (this.deviceSize !== null) { - return this.deviceSize - } - log.info('getting device window size...') - const {width, height, scale} = info.extendedInfo - - if (!width || !height || !scale) { - return null - } - - // Set device size based on orientation, default is PORTRAIT - if (this.orientation === 'PORTRAIT' || !this.orientation) { - this.deviceSize = {height: height / scale, width: width / scale} - } - else if (this.orientation === 'LANDSCAPE') { - this.deviceSize = {height: width / scale, width: height / scale} - } - else if (this.deviceType === 'Apple TV') { - this.deviceSize = {height: height, width: width} - } - return this.deviceSize - } - setVersion(currentSession) { - log.info('Setting current device version: ' + currentSession.value.capabilities.sdkVersion) - push.send([ - wireutil.global, - wireutil.envelope(new wire.SdkIosVersion(options.serial, currentSession.value.capabilities.sdkVersion)) - ]) - } - openUrl(message) { - const params = { - url: message.url - } - return this.handleRequest({ - method: 'POST', - uri: `${this.baseUrl}/session/` + this.sessionId + '/url', - body: params, - json: true - }) - } - screenshot() { - return new Promise((resolve, reject) => { - this.handleRequest({ - method: 'GET', - uri: `${this.baseUrl}/screenshot`, - json: true - }) - .then(response => { - try { - resolve(response) - } - catch (e) { - reject(e) - } - }) - .catch(err => reject(err)) - }) - } - getOrientation() { - return this.handleRequest({ - method: 'GET', - uri: `${this.baseUrl}/session/${this.sessionId}/orientation`, - json: true - }).then((orientationResponse) => { - this.orientation = orientationResponse.value - log.info('Current device orientation: ' + this.orientation) - }) - } - rotation(params) { - this.orientation = params.orientation - this.isRotating = true - return this.handleRequest({ - method: 'POST', - uri: `${this.baseUrl}/session/${this.sessionId}/orientation`, - body: params, - json: true - }).then(val => { - this.getOrientation() - this.size() - // @ts-ignore - const rotationDegrees = iosutil.orientationToDegrees(this.orientation) - push.send([ - wireutil.global, - wireutil.envelope(new wire.RotationEvent(options.serial, rotationDegrees)) - ]) - this.isRotating = false - }) - } - batteryIosEvent() { - return this.handleRequest({ - method: 'GET', - uri: `${this.baseUrl}/session/${this.sessionId}/wda/batteryInfo`, - json: true, - }) - .then((batteryInfoResponse) => { - let batteryState = iosutil.batteryState(batteryInfoResponse.value.state) - let batteryLevel = iosutil.batteryLevel(batteryInfoResponse.value.level) - push.send([ - wireutil.global, - wireutil.envelope(new wire.BatteryEvent(options.serial, batteryState, 'good', 'usb', batteryLevel, 1, 0.0, 5)) - ]) - }) - .then(() => { - log.info('Setting new device battery info') - }) - .catch((err) => log.info(err)) - } - getTreeElements() { - return this.handleRequest({ - method: 'GET', - uri: `${this.baseUrl}/source?format=json`, - json: true - }) - } - pressButtonSendRequest(params) { - return this.handleRequest({ - method: 'POST', - uri: `${this.baseUrl}/session/${this.sessionId}/wda/pressButton`, - body: { - name: params - }, - json: true - }) - } - switchCharset() { - this.upperCase = !this.upperCase - log.info(this.upperCase) - } - appActivate(params) { - return this.handleRequest({ - method: 'POST', - uri: `${this.baseUrl}/session/${this.sessionId}/wda/apps/activate`, - body: { - bundleId: params - }, - json: true - }) - } - pressPower() { - return this.handleRequest({ - method: 'GET', - uri: `${this.baseUrl}/session/${this.sessionId}/wda/locked`, - json: true - }) - .then(response => { - let url = '' - if (response.value === true) { - url = `${this.baseUrl}/session/${this.sessionId}/wda/unlock` - } - else { - url = `${this.baseUrl}/session/${this.sessionId}/wda/lock` - } - return this.handleRequest({ - method: 'POST', - uri: url, - json: true - }) - }) - } - getClipBoard() { - return this.handleRequest({ - method: 'POST', - uri: `${this.baseUrl}/session/${this.sessionId}/wda/getPasteboard` - }) - .then(res => { - let clipboard = Buffer.from(JSON.parse(res).value, 'base64').toString('utf-8') - return clipboard || 'No clipboard data' - }) - } - handleRequest(requestOpt) { - return new Promise((resolve, reject) => { - request(requestOpt) - .then(response => { - log.verbose(LOG_REQUEST_MSG, JSON.stringify(requestOpt)) - return resolve(response) - }) - .catch(async(err) => { - const errMes = err?.error?.value?.message - - if (!errMes || [ - 'Timed out while waiting until the screen gets locked', - 'unlocked', - 'Unable To Rotate Device' - ].includes(errMes)) { - resolve() - return - } - - if (errMes.includes('Session does not exist')) { - await this.startSession() - resolve( - await this.handleRequest(requestOpt) - ) - return - } - - if (errMes.includes('StatusCodeError')) { - log.error(`WDA request failed: ${err}`) - } - else { - log.warn(`Unexpected WDA request error: ${err?.message || err}`) - } - - resolve() // TODO: or reject? - - // TODO: refactoring needed - // #409: capture wda/appium crash asap and exit with status 1 from stf - // notifier.setDeviceTemporaryUnavialable(err) - // notifier.setDeviceAbsent(err) - // lifecycle.fatal(err) // exit with error code 1 is the best way to activate valid auto-healing steps with container(s) restart - }) - }) - } - pressButton(key) { - switch (key) { - case 'settings': - if (this.deviceType === 'Apple TV') { - return this.appActivate('com.apple.TVSettings') - } - return this.appActivate('com.apple.Preferences') - case 'store': - if (this.deviceType === 'Apple TV') { - return this.appActivate('com.apple.TVAppStore') - } - return this.appActivate('com.apple.AppStore') - case 'volume_up': - return this.pressButtonSendRequest('volumeUp') - case 'volume_down': - return this.pressButtonSendRequest('volumeDown') - case 'power': - return this.pressPower() - case 'camera': - return this.appActivate('com.apple.camera') - case 'search': - if (this.deviceType === 'Apple TV') { - return this.appActivate('com.apple.TVSearch') - } - return this.appActivate('com.apple.mobilesafari') - case 'finder': - return this.appActivate('com.apple.findmy') - case 'home': - return this.homeBtn() - case 'mute': { - let i - for (i = 0; i < 16; i++) { - Promise.delay(1000).then(() => { - this.pressButtonSendRequest('volumeDown') - }) - } - return true - } - case 'switch_charset': { - return this.switchCharset() - } - // Media button requests in case there's future WDA compatibility - case 'media_play_pause': - return log.error('Non-existent button in WDA') - case 'media_stop': - return log.error('Non-existent button in WDA') - case 'media_next': - return log.error('Non-existent button in WDA') - case 'media_previous': - return log.error('Non-existent button in WDA') - case 'media_fast_forward': - return log.error('Non-existent button in WDA') - case 'media_rewind': - return log.error('Non-existent button in WDA') - default: - return this.pressButtonSendRequest(key) - } - } - } - - /* - * WDA MJPEG connection is stable enough to be track status wda server itself. - * As only connection is closed or error detected we have to restart STF - */ - function connectToWdaMjpeg(options) { - log.info('connecting to WdaMjpeg') - socket.connect(options.mjpegPort, options.wdaHost, () => { - log.info(`Connected to WdaMjpeg ${options.wdaHost}:${options.mjpegPort}`) - }) - // #410: Use status 6 (preparing) on WDA startup - push.send([ - wireutil.global, - wireutil.envelope(new wire.DeviceStatusMessage(options.serial, 6)) - ]) - } - - let retry = 4 - async function wdaMjpegCloseEventHandler(hadError) { - console.log(`WdaMjpeg connection was closed${hadError ? ' by error' : ''}`) - notifier.setDeviceAbsent('WdaMjpeg connection is lost') - - if (!retry) { - push.send([ - wireutil.global, - wireutil.envelope(new wire.DeviceStatusMessage(options.serial, 3)) - ]) - lifecycle.fatal('WdaMjpeg connection is lost') - } - - await new Promise(r => setTimeout(r, 2000)) - --retry - connectToWdaMjpeg(options) - } - socket.on('close', wdaMjpegCloseEventHandler) - connectToWdaMjpeg(options) - return new WdaClient() - }) diff --git a/lib/units/ios-device/plugins/wda/client.ts b/lib/units/ios-device/plugins/wda/client.ts new file mode 100644 index 0000000000..1e7c5b071e --- /dev/null +++ b/lib/units/ios-device/plugins/wda/client.ts @@ -0,0 +1,61 @@ +import net from 'net' +import Promise from 'bluebird' +import syrup from '@devicefarmer/stf-syrup' +import logger from '../../../../util/logger.js' +import lifecycle from '../../../../util/lifecycle.js' +import WdaClient from './WDAClient.js' + +export default syrup.serial() + .define(async(options) => { + const log = logger.createLogger('wda:wdaClient') + log.info('Initializing WDA connection') + + const wdaClient = new WdaClient({ + wdaHost: options.wdaHost, + wdaPort: options.wdaPort + }) + + wdaClient.on('connected', () => { + log.info('WDA client successfully received response') + }) + + wdaClient.on('disconnected', () => { + lifecycle.fatal('WDA request error: unable to get response') + }) + + wdaClient.on('error', err => { + log.error(err.message) + }) + + const socket = new net.Socket() + + // TODO: WDA MJPEG connection only on group + // & No fatal on error + const connectToWdaMjpeg = (options: any) => { + log.info('Connecting to WdaMjpeg') + socket.connect(options.mjpegPort, options.wdaHost, () => { + log.info(`Connected to WdaMjpeg ${options.wdaHost}:${options.mjpegPort}`) + }) + } + + let retry = 4 + const wdaMjpegCloseEventHandler = async (hadError: boolean) => { + log.error(`WdaMjpeg connection was closed${hadError ? ' by error' : ''}`) + + if (!--retry) { + lifecycle.fatal('WdaMjpeg connection is lost') + } + + await new Promise(r => setTimeout(r, 2000)) + connectToWdaMjpeg(options) + } + + socket.on('close', wdaMjpegCloseEventHandler) + socket.on('error', err => { + log.error('WdaMjpeg connection error: %s', err?.message) + }) + + connectToWdaMjpeg(options) + + return wdaClient + }) diff --git a/lib/units/ios-device/plugins/wda/connect.js b/lib/units/ios-device/plugins/wda/connect.ts similarity index 75% rename from lib/units/ios-device/plugins/wda/connect.js rename to lib/units/ios-device/plugins/wda/connect.ts index 6435c89ce3..c05f95c7ee 100755 --- a/lib/units/ios-device/plugins/wda/connect.js +++ b/lib/units/ios-device/plugins/wda/connect.ts @@ -11,17 +11,26 @@ export default syrup.serial() .dependency(connector) .define((options, wdaClient, urlformat, connector) => { const log = logger.createLogger('ios-device:plugins:wda:connect') - let proxy = null + + let proxy: any = null const plugin = { url: urlformat(options.connectUrlPattern, options.connectPort), start: () => new Promise((resolve, reject) => { - proxy = proxy || httpProxy.createProxyServer({target: wdaClient.baseUrl}) + if (proxy) { + resolve(plugin.url) + return + } + + proxy = httpProxy.createProxyServer({target: wdaClient.baseUrl}) .on('error', (err) => { - log.error('WDA Proxy error: %s', err) + log.error('WDA Proxy error: %s', err?.message) reject(err) }) .listen(options.connectPort) - resolve(plugin.url) + + proxy.once('start', () => { + resolve(plugin.url) + }) }), stop: async() => { diff --git a/lib/units/ios-device/plugins/wda/index.js b/lib/units/ios-device/plugins/wda/index.js deleted file mode 100755 index acda59c6e4..0000000000 --- a/lib/units/ios-device/plugins/wda/index.js +++ /dev/null @@ -1,181 +0,0 @@ -import logger from '../../../../util/logger.js' -import Promise from 'bluebird' -import request from 'postman-request' -import url from 'url' -import util from 'util' -import syrup from '@devicefarmer/stf-syrup' -import wire from '../../../../wire/index.js' -import {WireRouter} from '../../../../wire/router.js' -import wireutil from '../../../../wire/util.js' -import * as iosutil from '../util/iosutil.js' -import push from '../../../base-device/support/push.js' -import sub from '../../../base-device/support/sub.js' -import wdaClient from './client.js' -import {Esp32Touch} from '../touch/esp32touch.js' -import {BrowserOpenMessage, DashboardOpenMessage, KeyDownMessage, KeyPressMessage, PhysicalIdentifyMessage, RotateMessage, ScreenCaptureMessage, StoreOpenMessage, TapDeviceTreeElement, TouchDownMessage, TouchMoveIosMessage, TouchMoveMessage, TouchUpMessage, TypeMessage} from '../../../../wire/wire.js' -export default syrup.serial() - .dependency(push) - .dependency(sub) - .dependency(wdaClient) - .define((options, push, sub, wdaClient) => { - const log = logger.createLogger('wda:client') - const Wda = {} - - Wda.connect = () => { - - /** - * @type {Esp32Touch | null} - */ - let cursorDevice = null - if (options.esp32Path) { - cursorDevice = new Esp32Touch(options.deviceInfo.screenSize.width, options.deviceInfo.screenSize.height, options.esp32Path) - cursorDevice.on('paired', () => { - push.send([ - wireutil.global, - wireutil.envelope(new wire.CapabilitiesMessage(options.serial, true, true)) - ]) - }) - cursorDevice.on('disconnected', () => { - cursorDevice?.reboot() - push.send([ - wireutil.global, - wireutil.envelope(new wire.CapabilitiesMessage(options.serial, true, false)) - ]) - }) - cursorDevice.on('ready', () => { - cursorDevice?.setName(options.deviceName) - }) - } - sub.on('message', new WireRouter() - .on(KeyPressMessage, (channel, message) => { - if (wdaClient.orientation === 'LANDSCAPE' && message.key === 'home') { - wdaClient.rotation({orientation: 'PORTRAIT'}) - .then(() => { - try { - wdaClient.pressButton(message.key) - } - catch (err) { - log.error('Error while pressing button ', err) - } - }) - .catch(err => { - log.error('Failed to rotate device ', err) - }) - } - else { - try { - wdaClient.pressButton(message.key) - } - catch (err) { - log.error('Error while pressing button ', err) - } - } - }) - .on(StoreOpenMessage, (channel, message) => { - wdaClient.pressButton('store') - }) - .on(DashboardOpenMessage, (channel, message) => { - wdaClient.pressButton('settings') - }) - .on(PhysicalIdentifyMessage, (channel, message) => { - wdaClient.pressButton('finder') - }) - .on(TouchDownMessage, (channel, message) => { - if(cursorDevice?.state === 'paired') { - cursorDevice.press() - } - else { - wdaClient.tap(message) - } - }) - .on(TouchMoveIosMessage, (channel, message) => { - if(cursorDevice?.state !== 'paired') { - wdaClient.swipe(message) - } - }) - .on(TouchMoveMessage, (channel, message) => { - if(cursorDevice?.state === 'paired') { - cursorDevice.move(message.x, message.y) - } - }) - .on(TouchUpMessage, (channel, message) => { - if(cursorDevice?.state === 'paired') { - cursorDevice.release() - } - else { - wdaClient.touchUp() - } - }) - .on(TapDeviceTreeElement, (channel, message) => { - wdaClient.tapDeviceTreeElement(message) - }) - .on(TypeMessage, (channel, message) => { - log.verbose('wire.TypeMessage: ', message) - wdaClient.typeKey({value: [iosutil.asciiparser(message.text)]}) - }) - .on(KeyDownMessage, (channel, message) => { - log.verbose('wire.KeyDownMessage: ', message) - if (message.key === 'home') { - wdaClient.homeBtn() - } - else { - wdaClient.typeKey({value: [iosutil.asciiparser(message.key)]}) - } - }) - .on(BrowserOpenMessage, (channel, message) => { - wdaClient.openUrl(message) - }) - .on(RotateMessage, (channel, message) => { - if (wdaClient.isRotating) { - return - } - const rotation = iosutil.degreesToOrientation(message.rotation) - wdaClient.rotation({orientation: rotation}) - .then(() => { - push.send([ - wireutil.global, - wireutil.envelope(new wire.RotationEvent(options.serial, message.rotation)) - ]) - }) - .catch(err => { - log.error('Failed to rotate device to : ', rotation, err) - }) - }) - .on(ScreenCaptureMessage, (channel, message) => { - wdaClient.screenshot() - .then(response => { - let reply = wireutil.reply(options.serial) - let args = { - url: url.resolve(options.storageUrl, util.format('s/upload/%s', 'image')) - } - const imageBuffer = new Buffer(response.value, 'base64') - let req = request.post(args, (err, res, body) => { - try { - let result = JSON.parse(body) - push.send([ - channel, - reply.okay('success', result.resources.file) - ]) - } - catch (/** @type {any} */ err) { - log.error('Invalid JSON in response', err.stack, body) - } - }) - req.form().append('file', imageBuffer, { - filename: util.format('%s.png', options.serial), - contentType: 'image/png' - }) - }) - .catch(err => { - log.error('Failed to get screenshot', err) - }) - }) - .handler()) - push.send([ - wireutil.global, - wireutil.envelope(new wire.CapabilitiesMessage(options.serial, true, false)) - ]) - return Promise.resolve() - } - return Wda - }) diff --git a/lib/units/ios-device/plugins/wda/index.ts b/lib/units/ios-device/plugins/wda/index.ts new file mode 100755 index 0000000000..f8104987d8 --- /dev/null +++ b/lib/units/ios-device/plugins/wda/index.ts @@ -0,0 +1,190 @@ +import logger from '../../../../util/logger.js' +import syrup from '@devicefarmer/stf-syrup' +import {WireRouter} from '../../../../wire/router.js' +import wireutil from '../../../../wire/util.js' +import * as iosutil from '../util/iosutil.js' +import push from '../../../base-device/support/push.js' +import sub from '../../../base-device/support/sub.js' +import wdaClient from './client.js' +import {Esp32Touch} from '../touch/esp32touch.js' +import { + BrowserOpenMessage, + CapabilitiesMessage, + DashboardOpenMessage, + KeyDownMessage, + KeyPressMessage, + PhysicalIdentifyMessage, + RotateMessage, RotationEvent, + ScreenCaptureMessage, + StoreOpenMessage, + TapDeviceTreeElement, + TouchDownMessage, + TouchMoveMessage, + TouchUpMessage, + TypeMessage +} from '../../../../wire/wire.js' +import {Readable} from 'stream' +import storage from '../../../base-device/support/storage.js' + +export default syrup.serial() + .dependency(push) + .dependency(sub) + .dependency(wdaClient) + .dependency(storage) + .define((options, push, sub, wdaClient, storage) => { + const log = logger.createLogger('wda:index') + + let cursorDevice: Esp32Touch | null = null + let cursorIsPaired = false + + if (options.esp32Path) { + cursorDevice = new Esp32Touch(options.deviceInfo.screenSize.width, options.deviceInfo.screenSize.height, options.esp32Path) + + cursorDevice.on('paired', () => { + cursorIsPaired = true + push.send([ + wireutil.global, + wireutil.pack(CapabilitiesMessage, { + serial: options.serial, + hasTouch: true, + hasCursor: true + }) + ]) + }) + + cursorDevice.on('disconnected', () => { + cursorIsPaired = false + cursorDevice?.reboot() + push.send([ + wireutil.global, + wireutil.pack(CapabilitiesMessage, { + serial: options.serial, + hasTouch: true, + hasCursor: false + }) + ]) + }) + + cursorDevice.on('ready', () => { + cursorDevice?.setName(options.deviceName) + }) + } + + const router = new WireRouter() + .on(KeyPressMessage, async(channel, message) => { + if (wdaClient.orientation === 'LANDSCAPE' && message.key === 'home') { + await wdaClient.rotation('PORTRAIT') + await wdaClient.pressButton(message.key) + return + } + + wdaClient.pressButton(message.key) + }) + .on(StoreOpenMessage, (channel, message) => { + wdaClient.pressButton('store') + }) + .on(DashboardOpenMessage, (channel, message) => { + wdaClient.pressButton('settings') + }) + .on(PhysicalIdentifyMessage, (channel, message) => { + wdaClient.pressButton('finder') + }) + .on(TouchDownMessage, (channel, message) => { + if(cursorIsPaired) { + cursorDevice!.press() + return + } + + wdaClient.touchDown(message) + }) + .on(TouchMoveMessage, (channel, message) => { + if(cursorIsPaired) { + cursorDevice!.move(message.x, message.y) + return + } + + wdaClient.touchMove(message) + }) + .on(TouchUpMessage, (channel, message) => { + if(cursorIsPaired) { + cursorDevice!.release() + return + } + + wdaClient.touchUp() + }) + .on(TapDeviceTreeElement, (channel, message) => { + wdaClient.tapDeviceTreeElement(message.label) + }) + .on(TypeMessage, (channel, message) => { + if (!message.text) { + return + } + + const key = iosutil.asciiparser(message.text) + if (key) { + wdaClient.typeKey(key) + } + }) + .on(KeyDownMessage, (channel, message) => { + if (message.key === 'home') { + wdaClient.homeBtn() + return + } + + const key = iosutil.asciiparser(message.key) + if (key) { + wdaClient.typeKey(key) + } + }) + .on(BrowserOpenMessage, (channel, message) => { + wdaClient.openUrl(message.url) + }) + .on(RotateMessage, async(channel, message) => { + const orientation = iosutil.degreesToOrientation(message.rotation) + await wdaClient.rotation(orientation) + + push.send([ + wireutil.global, + wireutil.pack(RotationEvent, { + serial: options.serial, + rotation: message.rotation + }) + ]) + }) + .on(ScreenCaptureMessage, async(channel, message) => { + try { + const response = await wdaClient.screenshot() + const imageBuffer = Buffer.from(response.value, 'base64') + + const transfer = Readable.from(imageBuffer) + + storage.store('blob', transfer, { + filename: `${Date.now()}_${options.serial}_screenshot.png`, + contentType: 'image/png', + knownLength: imageBuffer.length, + jwt: message.jwt + }) + } catch (err: any) { + log.error('iOS ScreenCaptureMessage error: %s', err?.message) + } + }) + .handler() + + wdaClient.on('connected', () => { + sub.on('message', router) + }) + + wdaClient.on('disconnected', () => { + sub.removeListener('message', router) + }) + + push.send([ + wireutil.global, + wireutil.pack(CapabilitiesMessage, { + serial: options.serial, + hasTouch: true, + hasCursor: false + }) + ]) + }) diff --git a/lib/units/ios-device/redirect-ports.ts b/lib/units/ios-device/redirect-ports.ts new file mode 100644 index 0000000000..a75fecc027 --- /dev/null +++ b/lib/units/ios-device/redirect-ports.ts @@ -0,0 +1,67 @@ +import * as usbmux from '@irdk/usbmux' +import logger from '../../util/logger.js' + +const log = logger.createLogger('ios:redirect-ports') + +/** + * Open ports from an iOS device to a host. + * Currently, works only for unix based systems. + * Returns stop function. + */ +export async function openPort( + devicePort: number, + listenPort: number, + udid: string, + usbmuxPath = '/var/run/usbmuxd' +): Promise<() => Promise> { + try { + usbmux.address.path = usbmuxPath + + const relay = new usbmux.Relay(devicePort, listenPort, { + udid: udid + }) + + relay.on('error', (error: any) => { + log.error(`Relay error: ${error.message} (code: ${error.number || 'unknown'})`) + }) + + await new Promise((resolve, reject) => { + const readyHandler = () => { + relay.removeListener('error', errorHandler) + log.debug(`Relay ready: ${devicePort} -> ${listenPort} (${udid})`) + resolve() + } + + const errorHandler = (error: any) => { + relay.removeListener('ready', readyHandler) + reject(error) + } + + relay.once('ready', readyHandler) + relay.once('error', errorHandler) + }) + + relay.once('detached', (deviceUdid: string) => { + log.warn(`Device detached: ${deviceUdid}`) + }) + + return () => new Promise((resolve, reject) => { + relay.removeAllListeners('error') + relay.once('close', () => { + log.debug(`Relay closed: ${devicePort} -> ${listenPort}`) + resolve() + }) + + relay.once('error', (reason: any) => { + log.error(`Error during relay stop: ${reason.message}`) + reject(reason) + }) + + relay.stop() + }) + + } catch (error: any) { + log.error(`Failed to create relay: ${error.message}`) + throw error + } +} diff --git a/lib/units/ios-provider/IOSObserver.ts b/lib/units/ios-provider/IOSObserver.ts new file mode 100644 index 0000000000..7e769ba093 --- /dev/null +++ b/lib/units/ios-provider/IOSObserver.ts @@ -0,0 +1,120 @@ +import EventEmitter from 'node:events' +import {spawn} from 'child_process' +import usb from 'usb-hotplug' + +type IsSimulator = boolean + +interface IOSSimEvents { + attached: [string, IsSimulator] + detached: [string, IsSimulator] +} + +export default class IOSObserver extends EventEmitter { + + private sims = new Set() + private usbListenerStarted = false + listnerInterval: NodeJS.Timeout | undefined + + constructor() { + super() + } + + getXCRunSimctlDevices = (): Promise => + new Promise((resolve, reject) => { + const proc = spawn('sh', [ + '-c', + `xcrun simctl list devices | grep "(Booted)" | grep -E -o "([0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12})" || true` + ], { + timeout: 10 * 60 * 1000, + stdio: ['ignore', 'pipe', 'pipe'] + }) + + let output = '' + + proc.stdout?.on('data', (data: Buffer) => { + output += data.toString() + }) + + proc.on('error', reject) + + proc.once('exit', (code) => { + proc.removeAllListeners('data') + proc.removeAllListeners('error') + + // Exit codes 0 and 1 are acceptable (1 means grep found no matches) + if (code !== null && code > 1) { + reject(new Error(`Process exited with code ${code}`)) + return + } + + const lines = output.trim().split('\n').filter(line => line.trim()) + resolve(lines) + }) + }) + + async processSimulators(): Promise { + const devices = await this.getXCRunSimctlDevices() + + for (const udid of Array.from(this.sims)) { + if (!devices.includes(udid)) { + this.sims.delete(udid) + this.emit('detached', udid, true) + } + } + + for (const device of devices) { + if (!this.sims.has(device)) { + this.sims.add(device) + this.emit('attached', device, true) + } + } + } + + private formatUDID(serial: string): string { + if (serial.length === 24) { + return `${serial.slice(0, 8)}-${serial.slice(8)}`.toUpperCase() + } else if (serial.length === 40) { + return serial.toLowerCase() + } + + return serial + } + + listen = (): void => { + new Promise(async() => { + if (!this.usbListenerStarted) { + const currentDevices = usb.listDevices() + for (const device of currentDevices) { + if (!device.serialNumber || device.vendorId !== 1452) continue + this.emit('attached', this.formatUDID(device.serialNumber), false) + } + + usb.watchDevices((err, event) => { + if (!event.serialNumber) { + return + } + + if (event.eventType === 'Connected' && event.device?.vendorId === 1452) { + this.emit('attached', this.formatUDID(event.serialNumber), false) + } else { + this.emit('detached', this.formatUDID(event.serialNumber), false) + } + }) + + this.usbListenerStarted = true + } + + await this.processSimulators() + + this.listnerInterval = setTimeout(this.listen, 2_000) + }) + } + + stop(): void { + if (this.usbListenerStarted) { + usb.stopWatching() + } + clearTimeout(this.listnerInterval) + } +} + diff --git a/lib/units/ios-provider/IOSSimObserver.ts b/lib/units/ios-provider/IOSSimObserver.ts deleted file mode 100644 index cd27633072..0000000000 --- a/lib/units/ios-provider/IOSSimObserver.ts +++ /dev/null @@ -1,61 +0,0 @@ -import EventEmitter from 'node:events' -import {Simctl} from 'node-simctl' -import {DeviceInfo} from 'node-simctl/build/lib/subcommands/list.js' - -interface IOSSimEvents { - attached: [string] - detached: [string] -} - -export default class IOSSimObserver extends EventEmitter { - simctl = new Simctl() - - /** @description list of UDIDs of booted simulators */ - state: Set = new Set() - listnerInterval: NodeJS.Timeout | undefined - - constructor() { - super() - } - - async getBootedSimulators(): Promise { - const devices = await this.simctl.getDevices() as Record - if (!devices) { - return [] - } - - return Object.entries(devices) - .flatMap(([, sims]) => - sims.flatMap(sim => sim.state === 'Booted' ? [sim.udid] : []) - ) - } - - async processState(sims: string[]): Promise { - for (const prevSim of Array.from(this.state)) { - if (!sims.includes(prevSim)) { - this.state.delete(prevSim) - this.emit('detached', prevSim) - } - } - - for (const sim of sims) { - if (!this.state.has(sim)) { - this.state.add(sim) - this.emit('attached', sim) - } - } - } - - listen = (): void => { - new Promise(async() => { - const sims = await this.getBootedSimulators() - await this.processState(sims) - this.listnerInterval = setTimeout(this.listen, 2_000) - }) - } - - stop(): void { - clearTimeout(this.listnerInterval) - } -} - diff --git a/lib/units/ios-provider/index.ts b/lib/units/ios-provider/index.ts index c16bc40ace..6179bbe750 100644 --- a/lib/units/ios-provider/index.ts +++ b/lib/units/ios-provider/index.ts @@ -1,10 +1,8 @@ import _ from 'lodash' import logger from '../../util/logger.js' import lifecycle from '../../util/lifecycle.js' -import * as usbmux from '@irdk/usbmux' -import {openPort} from './redirect-ports.js' import {Esp32Touch} from '../ios-device/plugins/touch/esp32touch.js' -import IOSSimObserver from './IOSSimObserver.js' +import IOSObserver from './IOSObserver.js' import {ChildProcess} from 'node:child_process' import {ProcessManager, ResourcePool} from '../../util/ProcessManager.js' import wireutil from '../../wire/util.js' @@ -13,7 +11,8 @@ import { DeviceAbsentMessage, DeviceStatusMessage, DeviceIosIntroductionMessage, - ProviderIosMessage + ProviderIosMessage, + DeviceStatus } from '../../wire/wire.js' import srv from '../../util/srv.js' import * as zmqutil from '../../util/zmqutil.js' @@ -25,8 +24,6 @@ interface DeviceContext { isSimulator: boolean register: Promise resolveRegister?: () => void - wdaStopForwarding?: () => void - screenStopForwarding?: () => void } interface ResourceType { @@ -59,6 +56,7 @@ interface Options { filter: null | ((serial: string) => boolean) screenWsUrlPattern: string killTimeout: number + publicIp: string endpoints: { push: string[] sub: string[] @@ -159,24 +157,20 @@ export default async (options: Options): Promise => { const processManager = new ProcessManager({ spawn: async(id, context, [resource]) => { log.info('Spawning device process "%s" with ports [%s]', id, Object.values(resource).join(', ')) + push.send([ + wireutil.global, + wireutil.pack(DeviceStatusMessage, { + serial: id, + status: DeviceStatus.PREPARING + }) + ]) + const esp32ToUse = _.sample(_.differenceBy(curEsp32, usedEsp32, 'path')) if (esp32ToUse) { usedEsp32.push(esp32ToUse) log.info(`Using ${esp32ToUse.path} ESP32`) } - if (!context.isSimulator) { - log.info(`Starting port forwarding for device ${id}`) - - const [wdaStopForwarding, screenStopForwarding] = await Promise.all([ - openPort(8100, resource.wdaPort, id, options.usbmuxPath), - openPort(9100, resource.screenPort, id, options.usbmuxPath) - ]) - - context.wdaStopForwarding = wdaStopForwarding - context.screenStopForwarding = screenStopForwarding - } - return options.fork(id, { ...resource, isSimulator: context.isSimulator, @@ -193,9 +187,6 @@ export default async (options: Options): Promise => { // Resolve register if pending context.resolveRegister?.() - context.wdaStopForwarding?.() - context.screenStopForwarding?.() - // Tell others the device is gone push.send([ wireutil.global, @@ -272,8 +263,7 @@ export default async (options: Options): Promise => { status: wireutil.toDeviceStatus('device'), provider: ProviderIosMessage.create({ channel: solo, - name: options.name, - screenWsUrlPattern: options.screenWsUrlPattern + name: options.name }) }) ]) @@ -330,15 +320,6 @@ export default async (options: Options): Promise => { } stats() - - // Tell others the device state changed - push.send([ - wireutil.global, - wireutil.pack(DeviceStatusMessage, { - serial: udid, - status: wireutil.toDeviceStatus('device') - }) - ]) } const onAttach = filterDevice( @@ -352,23 +333,23 @@ export default async (options: Options): Promise => { // Create device context with registration promise const deviceContext: DeviceContext = { - udid, isSimulator, - - // Register device immediately, before 'running' state - register: register(udid) + udid, isSimulator, register: Promise.resolve() } // Create managed process - const created = await processManager.create(udid, deviceContext, { + const process = await processManager.create(udid, deviceContext, { initialState: 'waiting', resourceCount: 1 }) - if (!created) { + if (!process) { log.error('Failed to create process for device "%s"', udid) return } + // Register device immediately, before 'running' state + deviceContext.register = register(udid) + stats() startDeviceWork(udid) } @@ -383,16 +364,10 @@ export default async (options: Options): Promise => { ) // TODO: add option.disallowSimulators (default: false) - const simObserver = new IOSSimObserver() - simObserver.on('attached', udid => onAttach(udid, true)) - simObserver.on('detached', udid => onDetach(udid, true)) - - simObserver.listen() - - // TODO: add option.onlySimulators (default: false) - const usbObserver = usbmux.createListener() - usbObserver.on('attached', onAttach) - usbObserver.on('detached', onDetach) + const iosObserver = new IOSObserver() + iosObserver.on('attached', onAttach) + iosObserver.on('detached', onDetach) + iosObserver.listen() log.info('Listening for devices') @@ -402,7 +377,6 @@ export default async (options: Options): Promise => { clearTimeout(statsTimer) stats(false) - usbObserver.destroy() ;[push, sub].forEach((sock) => sock.close() diff --git a/lib/units/ios-provider/redirect-ports.ts b/lib/units/ios-provider/redirect-ports.ts deleted file mode 100644 index f2cb7f1db9..0000000000 --- a/lib/units/ios-provider/redirect-ports.ts +++ /dev/null @@ -1,41 +0,0 @@ -import * as usbmux from '@irdk/usbmux' -import logger from '../../util/logger.js' - -const log = logger.createLogger('ios:redirect-ports') - -/** - * Open ports from an iOS device to a host. - * Currently, works only for unix based systems. - * Returns stop function. - */ -export async function openPort( - devicePort: number, - listenPort: number, - udid: string, - usbmuxPath: string -): Promise<() => Promise> { - usbmux.address.path = usbmuxPath - const relay = new usbmux.Relay(devicePort, listenPort, { - udid: udid - }) - - await new Promise((resolve, reject) => { - relay.on('ready', (...args: unknown[]) => { - relay.removeListener('error', reject) - resolve(args) - }) - relay.on('error', reject) - }) - - return () => - new Promise((resolve, reject) => { - relay.on('close', () => { - relay.removeListener('error', reject) - resolve() - }) - relay.on('error', reject) - relay.stop() - }) -} - - diff --git a/lib/units/processor/index.ts b/lib/units/processor/index.ts index c49f6ac8ee..8287cd0a72 100644 --- a/lib/units/processor/index.ts +++ b/lib/units/processor/index.ts @@ -284,7 +284,7 @@ export default db.ensureConnectivity(async(options: Options) => { dbapi.setDeviceIosVersion(message) }) .on(SizeIosDevice, (channel, message, data) => { - dbapi.sizeIosDevice(message.id, message.height, message.width, message.scale) + dbapi.sizeIosDevice(message.id, message.height, message.width, message.scale, message.url) appDealer.send([channel, data]) }) .on(DeviceTypeMessage, (channel, message, data) => { diff --git a/lib/units/websocket/index.js b/lib/units/websocket/index.js index 6f59f52e4c..280fb188d9 100644 --- a/lib/units/websocket/index.js +++ b/lib/units/websocket/index.js @@ -24,7 +24,44 @@ import {Server} from 'socket.io' import db from '../../db/index.js' import EventEmitter from 'events' import generateToken from '../api/helpers/generateToken.js' -import {UpdateAccessTokenMessage, DeleteUserMessage, DeviceChangeMessage, UserChangeMessage, GroupChangeMessage, DeviceGroupChangeMessage, GroupUserChangeMessage, DeviceLogMessage, DeviceIntroductionMessage, DeviceReadyMessage, DevicePresentMessage, DeviceAbsentMessage, InstalledApplications, JoinGroupMessage, JoinGroupByAdbFingerprintMessage, LeaveGroupMessage, DeviceStatusMessage, DeviceIdentityMessage, TransactionProgressMessage, TransactionDoneMessage, TransactionTreeMessage, DeviceLogcatEntryMessage, AirplaneModeEvent, BatteryEvent, GetServicesAvailabilityMessage, DeviceBrowserMessage, ConnectivityEvent, PhoneStateEvent, RotationEvent, CapabilitiesMessage, ReverseForwardsEvent, TemporarilyUnavailableMessage, UpdateRemoteConnectUrl} from '../../wire/wire.js' +import { + UpdateAccessTokenMessage, + DeleteUserMessage, + DeviceChangeMessage, + UserChangeMessage, + GroupChangeMessage, + DeviceGroupChangeMessage, + GroupUserChangeMessage, + DeviceLogMessage, + DeviceIntroductionMessage, + DeviceReadyMessage, + DevicePresentMessage, + DeviceAbsentMessage, + InstalledApplications, + JoinGroupMessage, + JoinGroupByAdbFingerprintMessage, + LeaveGroupMessage, + DeviceStatusMessage, + DeviceIdentityMessage, + TransactionProgressMessage, + TransactionDoneMessage, + TransactionTreeMessage, + DeviceLogcatEntryMessage, + AirplaneModeEvent, + BatteryEvent, + GetServicesAvailabilityMessage, + DeviceBrowserMessage, + ConnectivityEvent, + PhoneStateEvent, + RotationEvent, + CapabilitiesMessage, + ReverseForwardsEvent, + TemporarilyUnavailableMessage, + UpdateRemoteConnectUrl, + KeyDownMessage, + KeyUpMessage, + KeyPressMessage +} from '../../wire/wire.js' import AllModel from '../../db/models/all/index.js' import UserModel from '../../db/models/user/index.js' const request = Promise.promisifyAll(postmanRequest) @@ -99,7 +136,9 @@ export default (async function(options) { function createKeyHandler(Klass) { return function(channel, data) { if (user?.ownedChannels?.has(channel)) { - push.send([channel, wireutil.envelope(new Klass(data.key))]) + push.send([channel, wireutil.pack(Klass, { + key: data.key + })]) } } } @@ -624,10 +663,9 @@ export default (async function(options) { } }) .on('input.touchMoveIos', function(channel, data) { - data.duration = 0.042 trySendPush([ channel, - wireutil.envelope(new wire.TouchMoveIosMessage(data.toX, data.toY, data.fromX, data.fromY, data.duration)) + wireutil.envelope(new wire.TouchMoveIosMessage(data.toX, data.toY, data.fromX, data.fromY, data.duration || 0)) ]) }) .on('tapDeviceTreeElement', function(channel, data) { @@ -667,9 +705,9 @@ export default (async function(options) { ]) }) // Key events - .on('input.keyDown', createKeyHandler(wire.KeyDownMessage)) - .on('input.keyUp', createKeyHandler(wire.KeyUpMessage)) - .on('input.keyPress', createKeyHandler(wire.KeyPressMessage)) + .on('input.keyDown', createKeyHandler(KeyDownMessage)) + .on('input.keyUp', createKeyHandler(KeyUpMessage)) + .on('input.keyPress', createKeyHandler(KeyPressMessage)) .on('input.type', function(channel, data) { trySendPush([ channel, diff --git a/lib/util/ProcessManager.ts b/lib/util/ProcessManager.ts index 9b689e807d..2d4c5c4781 100644 --- a/lib/util/ProcessManager.ts +++ b/lib/util/ProcessManager.ts @@ -103,7 +103,7 @@ export class ProcessManager { initialState?: ProcessState resourceCount?: number } = {} - ): Promise { + ) { if (this.processes.has(id)) { this.log.warn('Process "%s" already exists', id) return false @@ -116,7 +116,7 @@ export class ProcessManager { if (!allocated) { // TODO: emit resource allocation error event this.log.error(`Failed to allocate ${options.resourceCount} resources for process "${id}"`) - return false + return null } resources = allocated } @@ -132,7 +132,7 @@ export class ProcessManager { this.processes.set(id, process) this.log.info('Created process "%s" with state "%s"', id, process.state) - return true + return process } // Start a process (spawn child process) diff --git a/lib/util/devutil.js b/lib/util/devutil.js index 132663ee93..48ba01adf1 100644 --- a/lib/util/devutil.js +++ b/lib/util/devutil.js @@ -101,12 +101,10 @@ export default syrup.serial() } devutil.waitForProcsToDie = function(comm, bin) { return devutil.listPidsByComm(comm, bin) - .then(function(pids) { + .then(async(pids) => { if (pids.length) { - return Promise.delay(100) - .then(function() { - return devutil.waitForProcsToDie(comm, bin) - }) + await new Promise(r => setTimeout(r, 100)) + return devutil.waitForProcsToDie(comm, bin) } }) } diff --git a/lib/util/lifecycle.ts b/lib/util/lifecycle.ts index 0948079e8d..afc65dacf6 100644 --- a/lib/util/lifecycle.ts +++ b/lib/util/lifecycle.ts @@ -1,42 +1,42 @@ -import EventEmitter from "node:events"; -import logger from "./logger.ts"; +import EventEmitter from 'node:events' +import logger from './logger.ts' -const log = logger.createLogger("util:lifecycle"); +const log = logger.createLogger('util:lifecycle') -type LifecycleObserver = () => Promise | unknown; +type LifecycleObserver = () => Promise | unknown export default new (class Lifecycle { - cleanups: LifecycleObserver[] = []; - ending = false; + cleanups: LifecycleObserver[] = [] + ending = false constructor() { - process.on("SIGINT", this.graceful.bind(this)); - process.on("SIGTERM", this.graceful.bind(this)); + process.on("SIGINT", this.graceful.bind(this)) + process.on("SIGTERM", this.graceful.bind(this)) } share(name: string, emitter: EventEmitter) { emitter.on("end", () => { if (!this.ending) { - log.fatal(`${name} ended; we shall share its fate`) - this.fatal(); + log.fatal(`${name} ended we shall share its fate`) + this.fatal() } - }); + }) emitter.on("error", (err) => { if (!this.ending) { log.fatal(`${name} had an error ${err.stack}`) - this.fatal(); + this.fatal() } - }); + }) if ('end' in emitter) { this.observe(() => { if(typeof emitter.end === 'function') { - emitter.end(); + emitter.end() } - }); + }) } - return emitter; + return emitter } graceful(err: Error) { @@ -44,23 +44,23 @@ export default new (class Lifecycle { if (this.ending) { log.error( "Repeated gracefull shutdown request. Exiting immediately." - ); - process.exit(1); + ) + process.exit(1) } - this.ending = true; + this.ending = true return Promise.all(this.cleanups.map((fn) => fn())).then(() => process.exit(0) - ); + ) } fatal(err?: Error | string): never { log.fatal(`Shutting down due to fatal error ${err || ''}`) - this.ending = true; - process.exit(1); + this.ending = true + process.exit(1) } observe(cleanupFn: LifecycleObserver) { - this.cleanups.push(cleanupFn); + this.cleanups.push(cleanupFn) } -})(); +})() diff --git a/lib/util/pathutil.cjs b/lib/util/pathutil.cjs deleted file mode 100644 index bab76cc608..0000000000 --- a/lib/util/pathutil.cjs +++ /dev/null @@ -1,47 +0,0 @@ -var path = require('path') -var fs = require('fs') -var util = require('util') - -// Export -module.exports.root = function(target) { - return path.resolve(__dirname, '../..', target) -} - -// Export -module.exports.reactFrontend = function(target) { - return path.resolve(__dirname, '../../ui', target) -} - -// Export -module.exports.vendor = function(target) { - return path.resolve(__dirname, '../../vendor', target) -} - -// Export -module.exports.module = function(target) { - return path.resolve(__dirname, '../../node_modules', target) -} - -// Export -module.exports.match = function(candidates) { - for (var i = 0, l = candidates.length; i < l; ++i) { - if (fs.existsSync(candidates[i])) { - return candidates[i] - } - } - return undefined -} - -// Export -module.exports.requiredMatch = function(candidates) { - var matched = this.match(candidates) - if(matched !== undefined) { - return matched - } - else { - throw new Error(util.format( - 'At least one of these paths should exist: %s' - , candidates.join(', ') - )) - } -} diff --git a/lib/util/pathutil.ts b/lib/util/pathutil.ts new file mode 100644 index 0000000000..187d0ab9a3 --- /dev/null +++ b/lib/util/pathutil.ts @@ -0,0 +1,58 @@ +import { fileURLToPath } from 'url' +import { dirname, join, resolve } from 'path' +import { existsSync } from 'fs' +import util from 'util' +import {PathLike} from "node:fs"; + +export function findProjectRoot(startPath: string) { + let currentPath = startPath + + while (currentPath !== '/') { + if (existsSync(join(currentPath, 'README.md'))) { + return currentPath + } + currentPath = dirname(currentPath) + } + + throw new Error('Could not find project root') +} + +export const projectRoot = findProjectRoot(dirname(fileURLToPath(import.meta.url))) + +export function root(target: string) { + return resolve(projectRoot, target) +} + +export function reactFrontend(target: string) { + return resolve(projectRoot, 'ui', target) +} + +export function vendor(target: string) { + return resolve(projectRoot, 'vendor', target) +} + +export function module(target: string) { + return resolve(projectRoot, 'node_modules', target) +} + +export function match(candidates: PathLike[]) { + for (let i = 0, l = candidates.length; i < l; ++i) { + if (existsSync(candidates[i])) { + return candidates[i] + } + } + return undefined +} + +export function requiredMatch(candidates: PathLike[]) { + let matched = match(candidates) + if(matched !== undefined) { + return matched + } + else { + throw new Error(util.format( + 'At least one of these paths should exist: %s' + , candidates.join(', ') + )) + } +} diff --git a/lib/wire/wire.proto b/lib/wire/wire.proto index 655b7138a8..c213f71719 100644 --- a/lib/wire/wire.proto +++ b/lib/wire/wire.proto @@ -197,7 +197,7 @@ message FileSystemListMessage { message FileSystemGetMessage { required string file = 1; - optional string jwt = 2; + required string jwt = 2; } message TransactionProgressMessage { @@ -283,13 +283,12 @@ message DeviceIntroductionMessage { message DeviceIosIntroductionMessage { required string serial = 1; required DeviceStatus status = 2; - required ProviderMessage provider = 3; + required ProviderIosMessage provider = 3; } message InitializeIosDeviceState { required string serial = 1; required DeviceStatus status = 2; - required ProviderIosMessage provider = 3; required IosDevicePorts ports = 4; required UpdateIosDevice options = 5; } @@ -664,6 +663,7 @@ message StoreOpenMessage { } message ScreenCaptureMessage { + required string jwt = 1; } message ConnectStartMessage { @@ -842,7 +842,8 @@ message UpdateIosDevice{ required string platform = 3; required string architecture = 4; required string sdk = 5; - required IosServiceMessage service = 6; + required string marketName = 6; + required IosServiceMessage service = 7; } message SdkIosVersion{ @@ -854,6 +855,7 @@ message SizeIosDevice{ required double height = 2; required double width = 3; required int32 scale = 4; + required string url = 5; } message DashboardOpenMessage { } diff --git a/lib/wire/wire.ts b/lib/wire/wire.ts index 438ab51313..563e3f9ac2 100644 --- a/lib/wire/wire.ts +++ b/lib/wire/wire.ts @@ -567,9 +567,9 @@ export interface FileSystemGetMessage { */ file: string; /** - * @generated from protobuf field: optional string jwt = 2 + * @generated from protobuf field: required string jwt = 2 */ - jwt?: string; + jwt: string; } /** * @generated from protobuf message TransactionProgressMessage @@ -811,9 +811,9 @@ export interface DeviceIosIntroductionMessage { */ status: DeviceStatus; /** - * @generated from protobuf field: required ProviderMessage provider = 3 + * @generated from protobuf field: required ProviderIosMessage provider = 3 */ - provider?: ProviderMessage; + provider?: ProviderIosMessage; } /** * @generated from protobuf message InitializeIosDeviceState @@ -827,10 +827,6 @@ export interface InitializeIosDeviceState { * @generated from protobuf field: required DeviceStatus status = 2 */ status: DeviceStatus; - /** - * @generated from protobuf field: required ProviderIosMessage provider = 3 - */ - provider?: ProviderIosMessage; /** * @generated from protobuf field: required IosDevicePorts ports = 4 */ @@ -1785,6 +1781,10 @@ export interface StoreOpenMessage { * @generated from protobuf message ScreenCaptureMessage */ export interface ScreenCaptureMessage { + /** + * @generated from protobuf field: required string jwt = 1 + */ + jwt: string; } /** * @generated from protobuf message ConnectStartMessage @@ -2243,7 +2243,11 @@ export interface UpdateIosDevice { */ sdk: string; /** - * @generated from protobuf field: required IosServiceMessage service = 6 + * @generated from protobuf field: required string marketName = 6 + */ + marketName: string; + /** + * @generated from protobuf field: required IosServiceMessage service = 7 */ service?: IosServiceMessage; } @@ -2280,6 +2284,10 @@ export interface SizeIosDevice { * @generated from protobuf field: required int32 scale = 4 */ scale: number; + /** + * @generated from protobuf field: required string url = 5 + */ + url: string; } /** * @generated from protobuf message DashboardOpenMessage @@ -4379,12 +4387,13 @@ class FileSystemGetMessage$Type extends MessageType { constructor() { super("FileSystemGetMessage", [ { no: 1, name: "file", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, - { no: 2, name: "jwt", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ } + { no: 2, name: "jwt", kind: "scalar", T: 9 /*ScalarType.STRING*/ } ]); } create(value?: PartialMessage): FileSystemGetMessage { const message = globalThis.Object.create((this.messagePrototype!)); message.file = ""; + message.jwt = ""; if (value !== undefined) reflectionMergePartial(this, message, value); return message; @@ -4397,7 +4406,7 @@ class FileSystemGetMessage$Type extends MessageType { case /* required string file */ 1: message.file = reader.string(); break; - case /* optional string jwt */ 2: + case /* required string jwt */ 2: message.jwt = reader.string(); break; default: @@ -4415,8 +4424,8 @@ class FileSystemGetMessage$Type extends MessageType { /* required string file = 1; */ if (message.file !== "") writer.tag(1, WireType.LengthDelimited).string(message.file); - /* optional string jwt = 2; */ - if (message.jwt !== undefined) + /* required string jwt = 2; */ + if (message.jwt !== "") writer.tag(2, WireType.LengthDelimited).string(message.jwt); let u = options.writeUnknownFields; if (u !== false) @@ -5189,7 +5198,7 @@ class DeviceIosIntroductionMessage$Type extends MessageType ["DeviceStatus", DeviceStatus] }, - { no: 3, name: "provider", kind: "message", T: () => ProviderMessage } + { no: 3, name: "provider", kind: "message", T: () => ProviderIosMessage } ]); } create(value?: PartialMessage): DeviceIosIntroductionMessage { @@ -5211,8 +5220,8 @@ class DeviceIosIntroductionMessage$Type extends MessageType ["DeviceStatus", DeviceStatus] }, - { no: 3, name: "provider", kind: "message", T: () => ProviderIosMessage }, { no: 4, name: "ports", kind: "message", T: () => IosDevicePorts }, { no: 5, name: "options", kind: "message", T: () => UpdateIosDevice } ]); @@ -5275,9 +5283,6 @@ class InitializeIosDeviceState$Type extends MessageType { constructor() { - super("ScreenCaptureMessage", []); + super("ScreenCaptureMessage", [ + { no: 1, name: "jwt", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); } create(value?: PartialMessage): ScreenCaptureMessage { const message = globalThis.Object.create((this.messagePrototype!)); + message.jwt = ""; if (value !== undefined) reflectionMergePartial(this, message, value); return message; @@ -8991,6 +8996,9 @@ class ScreenCaptureMessage$Type extends MessageType { while (reader.pos < end) { let [fieldNo, wireType] = reader.tag(); switch (fieldNo) { + case /* required string jwt */ 1: + message.jwt = reader.string(); + break; default: let u = options.readUnknownField; if (u === "throw") @@ -9003,6 +9011,9 @@ class ScreenCaptureMessage$Type extends MessageType { return message; } internalBinaryWrite(message: ScreenCaptureMessage, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* required string jwt = 1; */ + if (message.jwt !== "") + writer.tag(1, WireType.LengthDelimited).string(message.jwt); let u = options.writeUnknownFields; if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); @@ -10862,7 +10873,8 @@ class UpdateIosDevice$Type extends MessageType { { no: 3, name: "platform", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, { no: 4, name: "architecture", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, { no: 5, name: "sdk", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, - { no: 6, name: "service", kind: "message", T: () => IosServiceMessage } + { no: 6, name: "marketName", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 7, name: "service", kind: "message", T: () => IosServiceMessage } ]); } create(value?: PartialMessage): UpdateIosDevice { @@ -10872,6 +10884,7 @@ class UpdateIosDevice$Type extends MessageType { message.platform = ""; message.architecture = ""; message.sdk = ""; + message.marketName = ""; if (value !== undefined) reflectionMergePartial(this, message, value); return message; @@ -10896,7 +10909,10 @@ class UpdateIosDevice$Type extends MessageType { case /* required string sdk */ 5: message.sdk = reader.string(); break; - case /* required IosServiceMessage service */ 6: + case /* required string marketName */ 6: + message.marketName = reader.string(); + break; + case /* required IosServiceMessage service */ 7: message.service = IosServiceMessage.internalBinaryRead(reader, reader.uint32(), options, message.service); break; default: @@ -10926,9 +10942,12 @@ class UpdateIosDevice$Type extends MessageType { /* required string sdk = 5; */ if (message.sdk !== "") writer.tag(5, WireType.LengthDelimited).string(message.sdk); - /* required IosServiceMessage service = 6; */ + /* required string marketName = 6; */ + if (message.marketName !== "") + writer.tag(6, WireType.LengthDelimited).string(message.marketName); + /* required IosServiceMessage service = 7; */ if (message.service) - IosServiceMessage.internalBinaryWrite(message.service, writer.tag(6, WireType.LengthDelimited).fork(), options).join(); + IosServiceMessage.internalBinaryWrite(message.service, writer.tag(7, WireType.LengthDelimited).fork(), options).join(); let u = options.writeUnknownFields; if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); @@ -11001,7 +11020,8 @@ class SizeIosDevice$Type extends MessageType { { no: 1, name: "id", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, { no: 2, name: "height", kind: "scalar", T: 1 /*ScalarType.DOUBLE*/ }, { no: 3, name: "width", kind: "scalar", T: 1 /*ScalarType.DOUBLE*/ }, - { no: 4, name: "scale", kind: "scalar", T: 5 /*ScalarType.INT32*/ } + { no: 4, name: "scale", kind: "scalar", T: 5 /*ScalarType.INT32*/ }, + { no: 5, name: "url", kind: "scalar", T: 9 /*ScalarType.STRING*/ } ]); } create(value?: PartialMessage): SizeIosDevice { @@ -11010,6 +11030,7 @@ class SizeIosDevice$Type extends MessageType { message.height = 0; message.width = 0; message.scale = 0; + message.url = ""; if (value !== undefined) reflectionMergePartial(this, message, value); return message; @@ -11031,6 +11052,9 @@ class SizeIosDevice$Type extends MessageType { case /* required int32 scale */ 4: message.scale = reader.int32(); break; + case /* required string url */ 5: + message.url = reader.string(); + break; default: let u = options.readUnknownField; if (u === "throw") @@ -11055,6 +11079,9 @@ class SizeIosDevice$Type extends MessageType { /* required int32 scale = 4; */ if (message.scale !== 0) writer.tag(4, WireType.Varint).int32(message.scale); + /* required string url = 5; */ + if (message.url !== "") + writer.tag(5, WireType.LengthDelimited).string(message.url); let u = options.writeUnknownFields; if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); diff --git a/package-lock.json b/package-lock.json index 340fd282f6..72c2916a66 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "vk-devicehub", "version": "1.5.0", + "hasInstallScript": true, "dependencies": { "@aws-sdk/client-s3": "^3.772.0", "@aws-sdk/credential-providers": "^3.772.0", @@ -28,6 +29,7 @@ "@types/cookie-parser": "^1.4.10", "@u4/adbkit": "^5.1.7", "appium-sdb": "^1.0.1-beta.1", + "appium-webdriveragent": "^11.1.4", "basic-auth": "1.1.0", "bluebird": "2.11.0", "body-parser": "^1.20.2", @@ -101,6 +103,7 @@ "tsx": "4.20.3", "underscore.string": "3.3.6", "url-join": "1.1.0", + "usb-hotplug": "^0.1.6", "utf-8-validate": "5.0.9", "uuid": "^11.0.3", "webpack": "^5.88.1", @@ -111,9 +114,9 @@ "zeromq": "6.4.2" }, "bin": { - "devicehub": "bin/stf.mjs", - "dh": "bin/stf.mjs", - "stf": "bin/stf.mjs" + "devicehub": ".build/bin/stf.mjs", + "dh": ".build/bin/stf.mjs", + "stf": ".build/bin/stf.mjs" }, "devDependencies": { "@eslint/eslintrc": "^3.3.1", @@ -134,6 +137,7 @@ "@types/ws": "^3.2.1", "async": "2.6.4", "cli-docs-generator": "1.0.7", + "copyfiles": "^2.4.1", "esbuild": "0.25.8", "eslint": "^9.30.1", "event-stream": "3.3.5", @@ -155,226 +159,1241 @@ "version": "2.7.0", "license": "Apache-2.0" }, - "node_modules/@appium/logger": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@appium/logger/-/logger-1.7.1.tgz", - "integrity": "sha512-9C2o9X/lBEDBUnKfAi3mRo9oG7Z03nmISLwsGkWxIWjMAvBdJD0RRSJMekWVKzfXN3byrI1WlCXTITzN4LAoLw==", - "license": "ISC", + "node_modules/@appium/base-driver": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@appium/base-driver/-/base-driver-10.2.0.tgz", + "integrity": "sha512-PCjZVWP0J+uhcdvRVxVXGWrN3sxP5Ik7WkNDx/tYIeknlHnvboIfeXnuy0HS0DgNFeyP/U+4KDx6/ug7eTwPOQ==", + "license": "Apache-2.0", "dependencies": { - "console-control-strings": "1.1.0", - "lodash": "4.17.21", - "lru-cache": "10.4.3", - "set-blocking": "2.0.0" + "@appium/support": "^7.0.5", + "@appium/types": "^1.2.0", + "@colors/colors": "1.6.0", + "async-lock": "1.4.1", + "asyncbox": "6.0.1", + "axios": "1.13.3", + "bluebird": "3.7.2", + "body-parser": "2.2.2", + "express": "5.2.1", + "fastest-levenshtein": "1.0.16", + "http-status-codes": "2.3.0", + "lodash": "4.17.23", + "lru-cache": "11.2.5", + "method-override": "3.0.0", + "morgan": "1.10.1", + "path-to-regexp": "8.3.0", + "serve-favicon": "2.5.1", + "type-fest": "5.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": ">=10" }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0", - "npm": ">=8" + "optionalDependencies": { + "spdy": "4.0.2" } }, - "node_modules/@aws-crypto/crc32": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", - "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", - "license": "Apache-2.0", + "node_modules/@appium/base-driver/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" }, "engines": { - "node": ">=16.0.0" + "node": ">= 0.6" } }, - "node_modules/@aws-crypto/crc32c": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", - "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", + "node_modules/@appium/base-driver/node_modules/asyncbox": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/asyncbox/-/asyncbox-6.0.1.tgz", + "integrity": "sha512-vmeYcLDV9uxj1/0h36Kdj/3H/Bk5ugtwi3nSS8Sir9D7Ia2wDJWbh52LjMDaSI3s3iScmNL42kqDf7I4p8JXjg==", "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": ">=10" } }, - "node_modules/@aws-crypto/sha1-browser": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", - "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/supports-web-crypto": "^5.2.0", - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "@aws-sdk/util-locate-window": "^3.0.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } + "node_modules/@appium/base-driver/node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "license": "MIT" }, - "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "license": "Apache-2.0", + "node_modules/@appium/base-driver/node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", "dependencies": { - "tslib": "^2.6.2" + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { - "node": ">=14.0.0" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, + "node_modules/@appium/base-driver/node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", "engines": { - "node": ">=14.0.0" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, + "node_modules/@appium/base-driver/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", "engines": { - "node": ">=14.0.0" + "node": ">= 0.6" } }, - "node_modules/@aws-crypto/sha256-browser": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", - "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-js": "^5.2.0", - "@aws-crypto/supports-web-crypto": "^5.2.0", - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "@aws-sdk/util-locate-window": "^3.0.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" + "node_modules/@appium/base-driver/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" } }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "license": "Apache-2.0", + "node_modules/@appium/base-driver/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", "dependencies": { - "tslib": "^2.6.2" + "ms": "^2.1.3" }, "engines": { - "node": ">=14.0.0" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "license": "Apache-2.0", + "node_modules/@appium/base-driver/node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" }, "engines": { - "node": ">=14.0.0" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "license": "Apache-2.0", + "node_modules/@appium/base-driver/node_modules/express/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@appium/base-driver/node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" }, "engines": { - "node": ">=14.0.0" + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/@aws-crypto/sha256-js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", - "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", - "license": "Apache-2.0", + "node_modules/@appium/base-driver/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { - "node": ">=16.0.0" + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/@aws-crypto/supports-web-crypto": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", - "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", - "license": "Apache-2.0", + "node_modules/@appium/base-driver/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", "dependencies": { - "tslib": "^2.6.2" + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/@aws-crypto/util": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", - "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.222.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" + "node_modules/@appium/base-driver/node_modules/lru-cache": { + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" } }, - "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, + "node_modules/@appium/base-driver/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", "engines": { - "node": ">=14.0.0" + "node": ">= 0.8" } }, - "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, + "node_modules/@appium/base-driver/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", "engines": { - "node": ">=14.0.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "license": "Apache-2.0", + "node_modules/@appium/base-driver/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" + "mime-db": "^1.54.0" }, "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-cognito-identity": { - "version": "3.857.0", + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@appium/base-driver/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/@appium/base-driver/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@appium/base-driver/node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@appium/base-driver/node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@appium/base-driver/node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@appium/base-driver/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/@appium/base-driver/node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@appium/base-driver/node_modules/send/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@appium/base-driver/node_modules/serve-favicon": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/serve-favicon/-/serve-favicon-2.5.1.tgz", + "integrity": "sha512-JndLBslCLA/ebr7rS3d+/EKkzTsTi1jI2T9l+vHfAaGJ7A7NhtDpSZ0lx81HCNWnnE0yHncG+SSnVf9IMxOwXQ==", + "license": "MIT", + "dependencies": { + "etag": "~1.8.1", + "fresh": "~0.5.2", + "ms": "~2.1.3", + "parseurl": "~1.3.2", + "safe-buffer": "~5.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@appium/base-driver/node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@appium/base-driver/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@appium/base-driver/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@appium/logger": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@appium/logger/-/logger-1.7.1.tgz", + "integrity": "sha512-9C2o9X/lBEDBUnKfAi3mRo9oG7Z03nmISLwsGkWxIWjMAvBdJD0RRSJMekWVKzfXN3byrI1WlCXTITzN4LAoLw==", + "license": "ISC", + "dependencies": { + "console-control-strings": "1.1.0", + "lodash": "4.17.21", + "lru-cache": "10.4.3", + "set-blocking": "2.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0", + "npm": ">=8" + } + }, + "node_modules/@appium/schema": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@appium/schema/-/schema-1.0.1.tgz", + "integrity": "sha512-ph4Rd3OQZgsUi2LSLLuBG5OjxTBldNBYjbco3shDBL84H6+SEbHY0Lby8PAe1RqT6+0Y0BklDBVmfaaiszy5Bg==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "0.4.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": ">=10" + } + }, + "node_modules/@appium/strongbox": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@appium/strongbox/-/strongbox-1.0.1.tgz", + "integrity": "sha512-z9rt6hbUgZXS2ebRBfuNj9kWdDEm54zwnbCFEMbE/xyh0AZZG5Ypqoq4vRXHOOvTdr+8P7vAmdWgRcXdmVQgAA==", + "license": "Apache-2.0", + "dependencies": { + "env-paths": "4.0.0", + "slugify": "1.6.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": ">=10" + } + }, + "node_modules/@appium/support": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@appium/support/-/support-7.0.5.tgz", + "integrity": "sha512-89Fm5yRFURWteLvY+MAAxQsLzy5QADLdfjeqp0oZzMLLFPD19i58kpF0EGLIkxlZ/0Pb0dw+MuaUyVEWL9daMg==", + "license": "Apache-2.0", + "dependencies": { + "@appium/logger": "^2.0.4", + "@appium/tsconfig": "^1.1.1", + "@appium/types": "^1.2.0", + "@colors/colors": "1.6.0", + "archiver": "7.0.1", + "axios": "1.13.3", + "base64-stream": "1.0.0", + "bluebird": "3.7.2", + "bplist-creator": "0.1.1", + "bplist-parser": "0.3.2", + "form-data": "4.0.5", + "get-stream": "9.0.1", + "glob": "13.0.0", + "jsftp": "2.1.3", + "klaw": "4.1.0", + "lockfile": "1.0.4", + "lodash": "4.17.23", + "log-symbols": "7.0.1", + "moment": "2.30.1", + "ncp": "2.0.0", + "package-directory": "8.1.0", + "plist": "3.1.0", + "pluralize": "8.0.0", + "read-pkg": "10.0.0", + "resolve-from": "5.0.0", + "sanitize-filename": "1.6.3", + "semver": "7.7.3", + "shell-quote": "1.8.3", + "supports-color": "10.2.2", + "teen_process": "4.0.8", + "type-fest": "5.4.1", + "uuid": "13.0.0", + "which": "6.0.0", + "yauzl": "3.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": ">=10" + }, + "optionalDependencies": { + "sharp": "0.34.5" + } + }, + "node_modules/@appium/support/node_modules/@appium/logger": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@appium/logger/-/logger-2.0.4.tgz", + "integrity": "sha512-DH4IXGjn/9q4BfrbPdacremNS2FZXIu8vMQyUChWfP+rVRJ1TKMxXF1/ZXNpOfuu0FRrknclO96tUWG3HJj9Xg==", + "license": "ISC", + "dependencies": { + "console-control-strings": "1.1.0", + "lodash": "4.17.23", + "lru-cache": "11.2.5", + "set-blocking": "2.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": ">=10" + } + }, + "node_modules/@appium/support/node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@appium/support/node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "license": "MIT", + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@appium/support/node_modules/archiver-utils/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@appium/support/node_modules/archiver-utils/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@appium/support/node_modules/archiver/node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@appium/support/node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/@appium/support/node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "license": "MIT" + }, + "node_modules/@appium/support/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@appium/support/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/@appium/support/node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@appium/support/node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@appium/support/node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@appium/support/node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@appium/support/node_modules/get-stream/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@appium/support/node_modules/glob": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", + "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "path-scurry": "^2.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@appium/support/node_modules/glob/node_modules/path-scurry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", + "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@appium/support/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/@appium/support/node_modules/klaw": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-4.1.0.tgz", + "integrity": "sha512-1zGZ9MF9H22UnkpVeuaGKOjfA2t6WrfdrJmGjy16ykcjnKQDmHVX+KI477rpbGevz/5FD4MC3xf1oxylBgcaQw==", + "license": "MIT", + "engines": { + "node": ">=14.14.0" + } + }, + "node_modules/@appium/support/node_modules/lru-cache": { + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@appium/support/node_modules/minimatch": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", + "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@appium/support/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@appium/support/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@appium/support/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@appium/support/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@appium/support/node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/@appium/support/node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/@appium/support/node_modules/teen_process": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/teen_process/-/teen_process-4.0.8.tgz", + "integrity": "sha512-0DTX2KfgVOr6+8TVmheEdiJHZ/bPOPeJuX0yvv5VOX3x+OFteNkmWkI+hX6zTkzxjddrktsrXkacfS2Gom1YyA==", + "license": "Apache-2.0", + "dependencies": { + "lodash": "^4.17.21", + "shell-quote": "^1.8.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": ">=10" + } + }, + "node_modules/@appium/support/node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/@appium/support/node_modules/which": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", + "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@appium/support/node_modules/yauzl": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.2.0.tgz", + "integrity": "sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@appium/support/node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@appium/tsconfig": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@appium/tsconfig/-/tsconfig-1.1.1.tgz", + "integrity": "sha512-ikjo037sWgY2Oy0HRPGnrKHnOdUh9JyzstD7E6HlFqcZu8hvOP1hDQmKdoBTz8gkmSbZWcMRZmWaL3Yqaz2pLw==", + "license": "Apache-2.0", + "dependencies": { + "@tsconfig/node20": "20.1.8" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": ">=10" + } + }, + "node_modules/@appium/types": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@appium/types/-/types-1.2.0.tgz", + "integrity": "sha512-ksKUiUdi5BhqQRsdyOtoobV6tQLtrt+LbG7SY3eWTTBjiAu0oWU+390PnWOdN8VQhVnP2w8Ad8KZZnx7Mezkyg==", + "license": "Apache-2.0", + "dependencies": { + "@appium/logger": "^2.0.4", + "@appium/schema": "^1.0.1", + "@appium/tsconfig": "^1.1.1", + "type-fest": "5.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": ">=10" + } + }, + "node_modules/@appium/types/node_modules/@appium/logger": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@appium/logger/-/logger-2.0.4.tgz", + "integrity": "sha512-DH4IXGjn/9q4BfrbPdacremNS2FZXIu8vMQyUChWfP+rVRJ1TKMxXF1/ZXNpOfuu0FRrknclO96tUWG3HJj9Xg==", + "license": "ISC", + "dependencies": { + "console-control-strings": "1.1.0", + "lodash": "4.17.23", + "lru-cache": "11.2.5", + "set-blocking": "2.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": ">=10" + } + }, + "node_modules/@appium/types/node_modules/lru-cache": { + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/crc32c": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", + "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", + "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity": { + "version": "3.857.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.857.0.tgz", "integrity": "sha512-jGCi6cqanpdRhXBh/lqKHw/CwXqE/mvxvy1pvbiKaQHTjiHjX3Nn3d/yIMI1FAUtCTXwwIW2Q304C/w0NLoncw==", "license": "Apache-2.0", @@ -811,105 +1830,326 @@ "@smithy/node-config-provider": "^4.1.3", "@smithy/protocol-http": "^5.1.2", "@smithy/types": "^4.3.1", - "@smithy/util-middleware": "^4.0.4", - "@smithy/util-stream": "^4.2.3", - "@smithy/util-utf8": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-stream": "^4.2.3", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.840.0.tgz", + "integrity": "sha512-ub+hXJAbAje94+Ya6c6eL7sYujoE8D4Bumu1NUI8TXjUhVVn0HzVWQjpRLshdLsUp1AW7XyeJaxyajRaJQ8+Xg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.840.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.840.0.tgz", + "integrity": "sha512-KVLD0u0YMF3aQkVF8bdyHAGWSUY6N1Du89htTLgqCcIhSxxAJ9qifrosVZ9jkAzqRW99hcufyt2LylcVU2yoKQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.840.0", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.840.0.tgz", + "integrity": "sha512-lSV8FvjpdllpGaRspywss4CtXV8M7NNNH+2/j86vMH+YCOZ6fu2T/TyFd/tHwZ92vDfHctWkRbQxg0bagqwovA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.840.0", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.840.0.tgz", + "integrity": "sha512-Gu7lGDyfddyhIkj1Z1JtrY5NHb5+x/CRiB87GjaSrKxkDaydtX2CU977JIABtt69l9wLbcGDIQ+W0uJ5xPof7g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.840.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.857.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.857.0.tgz", + "integrity": "sha512-qKbr6I6+4kRvI9guR1xnTX3dS37JaIM042/uLYzb65/dUfOm7oxBTDW0+7Jdu92nj5bAChYloKQGEsr7dwKxeg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.857.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-arn-parser": "3.804.0", + "@smithy/core": "^3.7.2", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/protocol-http": "^5.1.2", + "@smithy/signature-v4": "^5.1.2", + "@smithy/smithy-client": "^4.4.9", + "@smithy/types": "^4.3.1", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-stream": "^4.2.3", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.840.0.tgz", + "integrity": "sha512-CBZP9t1QbjDFGOrtnUEHL1oAvmnCUUm7p0aPNbIdSzNtH42TNKjPRN3TuEIJDGjkrqpL3MXyDSmNayDcw/XW7Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.840.0", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.857.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.857.0.tgz", + "integrity": "sha512-JPqTxJGwc5QyxpCpDuOi64+z+9krpkv9FidnWjPqqNwLy25Da8espksTzptPivsMzUukdObFWJsDG89/8/I6TQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.857.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.848.0", + "@smithy/core": "^3.7.2", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.857.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.857.0.tgz", + "integrity": "sha512-3P1GP34hu3Yb7C8bcIqIGASMt/MT/1Lxwy37UJwCn4IrccrvYM3i8y5XX4wW8sn1J5832wB4kdb4HTYbEz6+zw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.857.0", + "@aws-sdk/middleware-host-header": "3.840.0", + "@aws-sdk/middleware-logger": "3.840.0", + "@aws-sdk/middleware-recursion-detection": "3.840.0", + "@aws-sdk/middleware-user-agent": "3.857.0", + "@aws-sdk/region-config-resolver": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.848.0", + "@aws-sdk/util-user-agent-browser": "3.840.0", + "@aws-sdk/util-user-agent-node": "3.857.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.7.2", + "@smithy/fetch-http-handler": "^5.1.0", + "@smithy/hash-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.17", + "@smithy/middleware-retry": "^4.1.18", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.1.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.9", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.25", + "@smithy/util-defaults-mode-node": "^4.0.25", + "@smithy/util-endpoints": "^3.0.6", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.6", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.840.0.tgz", + "integrity": "sha512-Qjnxd/yDv9KpIMWr90ZDPtRj0v75AqGC92Lm9+oHXZ8p1MjG5JE2CW0HL8JRgK9iKzgKBL7pPQRXI8FkvEVfrA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.840.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.857.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.857.0.tgz", + "integrity": "sha512-KVpHAtRjv4oNydwXwAEf2ur4BOAWjjZiT/QtLtTKYbEbnXW1eOFZg4kWwJwHa/T/w2zfPMVf6LhZvyFwLU9XGg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "3.857.0", + "@aws-sdk/types": "3.840.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/signature-v4": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.857.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.857.0.tgz", + "integrity": "sha512-4DBZw+QHpsnpYLXzQtDYCEP9KFFQlYAmNnrCK1bsWoKqnUgjKgwr9Re0yhtNiieHhEE4Lhu+E+IAiNwDx2ClVw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.857.0", + "@aws-sdk/nested-clients": "3.857.0", + "@aws-sdk/types": "3.840.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.840.0.tgz", + "integrity": "sha512-xliuHaUFZxEx1NSXeLLZ9Dyu6+EJVQKEoD+yM+zqUo3YDZ7medKJWY6fIOKiPX/N7XbLdBYwajb15Q7IL8KkeA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.840.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.840.0.tgz", - "integrity": "sha512-ub+hXJAbAje94+Ya6c6eL7sYujoE8D4Bumu1NUI8TXjUhVVn0HzVWQjpRLshdLsUp1AW7XyeJaxyajRaJQ8+Xg==", + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.804.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.804.0.tgz", + "integrity": "sha512-wmBJqn1DRXnZu3b4EkE6CWnoWMo1ZMvlfkqU5zPz67xx1GMaXlDCchFvKAXMjk4jn/L1O3tKnoFDNsoLV1kgNQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.840.0", - "@smithy/protocol-http": "^5.1.2", - "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/middleware-location-constraint": { - "version": "3.840.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.840.0.tgz", - "integrity": "sha512-KVLD0u0YMF3aQkVF8bdyHAGWSUY6N1Du89htTLgqCcIhSxxAJ9qifrosVZ9jkAzqRW99hcufyt2LylcVU2yoKQ==", + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.848.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.848.0.tgz", + "integrity": "sha512-fY/NuFFCq/78liHvRyFKr+aqq1aA/uuVSANjzr5Ym8c+9Z3HRPE9OrExAHoMrZ6zC8tHerQwlsXYYH5XZ7H+ww==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.840.0", "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-endpoints": "^3.0.6", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/middleware-logger": { - "version": "3.840.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.840.0.tgz", - "integrity": "sha512-lSV8FvjpdllpGaRspywss4CtXV8M7NNNH+2/j86vMH+YCOZ6fu2T/TyFd/tHwZ92vDfHctWkRbQxg0bagqwovA==", + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.804.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.804.0.tgz", + "integrity": "sha512-zVoRfpmBVPodYlnMjgVjfGoEZagyRF5IPn3Uo6ZvOZp24chnW/FRstH7ESDHDDRga4z3V+ElUQHKpFDXWyBW5A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.840.0", - "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/middleware-recursion-detection": { + "node_modules/@aws-sdk/util-user-agent-browser": { "version": "3.840.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.840.0.tgz", - "integrity": "sha512-Gu7lGDyfddyhIkj1Z1JtrY5NHb5+x/CRiB87GjaSrKxkDaydtX2CU977JIABtt69l9wLbcGDIQ+W0uJ5xPof7g==", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.840.0.tgz", + "integrity": "sha512-JdyZM3EhhL4PqwFpttZu1afDpPJCCc3eyZOLi+srpX11LsGj6sThf47TYQN75HT1CarZ7cCdQHGzP2uy3/xHfQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.840.0", - "@smithy/protocol-http": "^5.1.2", "@smithy/types": "^4.3.1", + "bowser": "^2.11.0", "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/middleware-sdk-s3": { + "node_modules/@aws-sdk/util-user-agent-node": { "version": "3.857.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.857.0.tgz", - "integrity": "sha512-qKbr6I6+4kRvI9guR1xnTX3dS37JaIM042/uLYzb65/dUfOm7oxBTDW0+7Jdu92nj5bAChYloKQGEsr7dwKxeg==", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.857.0.tgz", + "integrity": "sha512-xWNfAnD2t5yACGW1wM3iLoy2FvRM8N/XjkjgJE1O35gBHn00evtLC9q4nkR4x7+vXdZb8cVw4Y6GmcfMckgFQg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.857.0", + "@aws-sdk/middleware-user-agent": "3.857.0", "@aws-sdk/types": "3.840.0", - "@aws-sdk/util-arn-parser": "3.804.0", - "@smithy/core": "^3.7.2", "@smithy/node-config-provider": "^4.1.3", - "@smithy/protocol-http": "^5.1.2", - "@smithy/signature-v4": "^5.1.2", - "@smithy/smithy-client": "^4.4.9", "@smithy/types": "^4.3.1", - "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.4", - "@smithy/util-stream": "^4.2.3", - "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } } }, - "node_modules/@aws-sdk/middleware-ssec": { - "version": "3.840.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.840.0.tgz", - "integrity": "sha512-CBZP9t1QbjDFGOrtnUEHL1oAvmnCUUm7p0aPNbIdSzNtH42TNKjPRN3TuEIJDGjkrqpL3MXyDSmNayDcw/XW7Q==", + "node_modules/@aws-sdk/xml-builder": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.821.0.tgz", + "integrity": "sha512-DIIotRnefVL6DiaHtO6/21DhJ4JZnnIwdNbpwiAhdt/AVbttcE4yw925gsjur0OGv5BTYXQXU3YnANBYnZjuQA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.840.0", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, @@ -917,618 +2157,946 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.857.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.857.0.tgz", - "integrity": "sha512-JPqTxJGwc5QyxpCpDuOi64+z+9krpkv9FidnWjPqqNwLy25Da8espksTzptPivsMzUukdObFWJsDG89/8/I6TQ==", + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz", + "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bufbuild/protobuf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.6.3.tgz", + "integrity": "sha512-w/gJKME9mYN7ZoUAmSMAWXk4hkVpxRKvEJCb3dV5g9wwWdxTJJ0ayOJAVcNxtdqaxDyFuC0uz4RSGVacJ030PQ==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@bufbuild/protoplugin": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/@bufbuild/protoplugin/-/protoplugin-2.6.3.tgz", + "integrity": "sha512-VceMuxeRukxGeABfo34SXq0VqY1MU+mzS+PBf0HAWo97ylFut8F6sQ3mV0tKiM08UQ/xQco7lxCn83BkoxrWrA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.857.0", - "@aws-sdk/types": "3.840.0", - "@aws-sdk/util-endpoints": "3.848.0", - "@smithy/core": "^3.7.2", - "@smithy/protocol-http": "^5.1.2", - "@smithy/types": "^4.3.1", - "tslib": "^2.6.2" + "@bufbuild/protobuf": "2.6.3", + "@typescript/vfs": "^1.5.2", + "typescript": "5.4.5" + } + }, + "node_modules/@bufbuild/protoplugin/node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" }, "engines": { - "node": ">=18.0.0" + "node": ">=14.17" } }, - "node_modules/@aws-sdk/nested-clients": { - "version": "3.857.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.857.0.tgz", - "integrity": "sha512-3P1GP34hu3Yb7C8bcIqIGASMt/MT/1Lxwy37UJwCn4IrccrvYM3i8y5XX4wW8sn1J5832wB4kdb4HTYbEz6+zw==", + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@devicefarmer/adbkit-apkreader": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@devicefarmer/adbkit-apkreader/-/adbkit-apkreader-3.2.4.tgz", + "integrity": "sha512-WtVmPuirX3/lBN9Z2AC/4DnK7XrXcMi0h3yhxNogxgp6+GVbO6W4RqmuCKlNm1PjyI2GRbw4XwGRQ8cR/IRN6A==", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.857.0", - "@aws-sdk/middleware-host-header": "3.840.0", - "@aws-sdk/middleware-logger": "3.840.0", - "@aws-sdk/middleware-recursion-detection": "3.840.0", - "@aws-sdk/middleware-user-agent": "3.857.0", - "@aws-sdk/region-config-resolver": "3.840.0", - "@aws-sdk/types": "3.840.0", - "@aws-sdk/util-endpoints": "3.848.0", - "@aws-sdk/util-user-agent-browser": "3.840.0", - "@aws-sdk/util-user-agent-node": "3.857.0", - "@smithy/config-resolver": "^4.1.4", - "@smithy/core": "^3.7.2", - "@smithy/fetch-http-handler": "^5.1.0", - "@smithy/hash-node": "^4.0.4", - "@smithy/invalid-dependency": "^4.0.4", - "@smithy/middleware-content-length": "^4.0.4", - "@smithy/middleware-endpoint": "^4.1.17", - "@smithy/middleware-retry": "^4.1.18", - "@smithy/middleware-serde": "^4.0.8", - "@smithy/middleware-stack": "^4.0.4", - "@smithy/node-config-provider": "^4.1.3", - "@smithy/node-http-handler": "^4.1.0", - "@smithy/protocol-http": "^5.1.2", - "@smithy/smithy-client": "^4.4.9", - "@smithy/types": "^4.3.1", - "@smithy/url-parser": "^4.0.4", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.25", - "@smithy/util-defaults-mode-node": "^4.0.25", - "@smithy/util-endpoints": "^3.0.6", - "@smithy/util-middleware": "^4.0.4", - "@smithy/util-retry": "^4.0.6", - "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" + "bluebird": "^3.4.7", + "debug": "~4.3.1", + "yauzl": "^2.7.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@devicefarmer/adbkit-apkreader/node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "license": "MIT" + }, + "node_modules/@devicefarmer/adbkit-monkey": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@devicefarmer/adbkit-monkey/-/adbkit-monkey-1.2.1.tgz", + "integrity": "sha512-ZzZY/b66W2Jd6NHbAhLyDWOEIBWC11VizGFk7Wx7M61JZRz7HR9Cq5P+65RKWUU7u6wgsE8Lmh9nE4Mz+U2eTg==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.10.4" + } + }, + "node_modules/@devicefarmer/minicap-prebuilt": { + "resolved": "minicap-prebuilt", + "link": true + }, + "node_modules/@devicefarmer/minitouch-prebuilt": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@devicefarmer/minitouch-prebuilt/-/minitouch-prebuilt-1.3.0.tgz", + "integrity": "sha512-vZKh2b4EHxeySLmuO+4YTF5JUEsSuQXOkiVearkBgXerdnbQsVIBRyBvU5xVPkLWz7AI4b9eWIQWjz/a8nwodA==", + "license": "Apache-2.0" + }, + "node_modules/@devicefarmer/stf-appstore-db": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@devicefarmer/stf-appstore-db/-/stf-appstore-db-1.0.0.tgz", + "integrity": "sha512-dkGLTU6ax8nmOBQlW3iJH6cf3ufLW4kY37TlfT13MYS2yKwoatn5Kf8CzwzYMSVH4tFDoIHNTPCXTnaLherk3A==", + "license": "Apache-2.0" + }, + "node_modules/@devicefarmer/stf-browser-db": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@devicefarmer/stf-browser-db/-/stf-browser-db-1.0.2.tgz", + "integrity": "sha512-9Yu4hdqJeloLE7D6DfGFveNOkGJCg3/XYOb7/37U36MLI95IYEH7JZk9Cma222nxt7dx6Yb7BYI0NH0y6RyIIQ==", + "license": "Apache-2.0" + }, + "node_modules/@devicefarmer/stf-device-db": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@devicefarmer/stf-device-db/-/stf-device-db-1.4.0.tgz", + "integrity": "sha512-MTtmDWxmsFc1GPBQoHjS3+Tbf2kd39EYO70jqe9e7H6NqzUKxndF8h4e6LI2SU1JzPtACoiYxRd2rpxEQBoqkw==", + "license": "CC-BY-SA-4.0" + }, + "node_modules/@devicefarmer/stf-syrup": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@devicefarmer/stf-syrup/-/stf-syrup-1.0.2.tgz", + "integrity": "sha512-5I/m/9lUF4sSrXAeihMIMJobuRK7ZvkG5bad3d+k+wd3BRSLlMHdBzkWiUVY91xRKXeh0jBvQ2x8yr8UTCEDGQ==", + "license": "Apache-2.0", + "dependencies": { + "bluebird": "~1.1.0", + "lodash": "~2.4.1" }, "engines": { - "node": ">=18.0.0" + "node": ">= 0.10" } }, - "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.840.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.840.0.tgz", - "integrity": "sha512-Qjnxd/yDv9KpIMWr90ZDPtRj0v75AqGC92Lm9+oHXZ8p1MjG5JE2CW0HL8JRgK9iKzgKBL7pPQRXI8FkvEVfrA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.840.0", - "@smithy/node-config-provider": "^4.1.3", - "@smithy/types": "^4.3.1", - "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.4", - "tslib": "^2.6.2" - }, + "node_modules/@devicefarmer/stf-syrup/node_modules/bluebird": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-1.1.1.tgz", + "integrity": "sha512-2zqtRvlriJL+CZg6muZ9Hzhdvhp+NzT9xUHR47vaPNkq6cY+y2Uri2FHFq+ry5HYHIarulomYqiTWbxJmrOcQw==", + "license": "MIT" + }, + "node_modules/@devicefarmer/stf-wiki": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@devicefarmer/stf-wiki/-/stf-wiki-1.0.0.tgz", + "integrity": "sha512-p87QMgXmJS5K8lWkVvYgwmSHmxSsB+N8ctcZZsGx26wOh+0/RiDfsVQAIMv+SME6IWVXIjo/FiZRsGNlvw85Sg==", + "license": "CC-BY-SA-4.0" + }, + "node_modules/@e-khalilov/usb-hotplug-darwin-arm64": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@e-khalilov/usb-hotplug-darwin-arm64/-/usb-hotplug-darwin-arm64-0.1.6.tgz", + "integrity": "sha512-Z4ev7qhFeWReb6xONTMmkhkV13tATbbHbjPIki7nSmf4pfbfa0AUvMW7dcaaagyUNzBjib4w0UuW9LykmHKcGQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=18.0.0" + "node": ">=16" } }, - "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.857.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.857.0.tgz", - "integrity": "sha512-KVpHAtRjv4oNydwXwAEf2ur4BOAWjjZiT/QtLtTKYbEbnXW1eOFZg4kWwJwHa/T/w2zfPMVf6LhZvyFwLU9XGg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-sdk-s3": "3.857.0", - "@aws-sdk/types": "3.840.0", - "@smithy/protocol-http": "^5.1.2", - "@smithy/signature-v4": "^5.1.2", - "@smithy/types": "^4.3.1", - "tslib": "^2.6.2" - }, + "node_modules/@e-khalilov/usb-hotplug-darwin-x64": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@e-khalilov/usb-hotplug-darwin-x64/-/usb-hotplug-darwin-x64-0.1.6.tgz", + "integrity": "sha512-Vyyw2BQT5fz/moM6fEcARzK+QPnugOrnQsEcLinIL2+hZMrWJOKloCo+oesiZoYuOaC4L6/DRvW59w/WGvO1aA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=18.0.0" + "node": ">=16" } }, - "node_modules/@aws-sdk/token-providers": { - "version": "3.857.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.857.0.tgz", - "integrity": "sha512-4DBZw+QHpsnpYLXzQtDYCEP9KFFQlYAmNnrCK1bsWoKqnUgjKgwr9Re0yhtNiieHhEE4Lhu+E+IAiNwDx2ClVw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.857.0", - "@aws-sdk/nested-clients": "3.857.0", - "@aws-sdk/types": "3.840.0", - "@smithy/property-provider": "^4.0.4", - "@smithy/shared-ini-file-loader": "^4.0.4", - "@smithy/types": "^4.3.1", - "tslib": "^2.6.2" - }, + "node_modules/@e-khalilov/usb-hotplug-linux-x64-gnu": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@e-khalilov/usb-hotplug-linux-x64-gnu/-/usb-hotplug-linux-x64-gnu-0.1.6.tgz", + "integrity": "sha512-a/od34AiFQxO+wDOHfhqG9BrUNKUqhQ19SbrwdzFTFBQ5yoLk3JR7Iz1MuUoxwUSi7sl7vh/tYNd/NzYwXSKRg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.0.0" + "node": ">=16" } }, - "node_modules/@aws-sdk/types": { - "version": "3.840.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.840.0.tgz", - "integrity": "sha512-xliuHaUFZxEx1NSXeLLZ9Dyu6+EJVQKEoD+yM+zqUo3YDZ7medKJWY6fIOKiPX/N7XbLdBYwajb15Q7IL8KkeA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.3.1", - "tslib": "^2.6.2" - }, + "node_modules/@e-khalilov/usb-hotplug-win32-x64-msvc": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@e-khalilov/usb-hotplug-win32-x64-msvc/-/usb-hotplug-win32-x64-msvc-0.1.6.tgz", + "integrity": "sha512-X3Wpc4D/eGrQx3+DdKGokeg+v0YzxPsnYcI4o7bS7r/Q9qxLMc9HuV2qtw8gQ9c2eDpnVamB3aai8dj+7ZgYZA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=18.0.0" + "node": ">=16" } }, - "node_modules/@aws-sdk/util-arn-parser": { - "version": "3.804.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.804.0.tgz", - "integrity": "sha512-wmBJqn1DRXnZu3b4EkE6CWnoWMo1ZMvlfkqU5zPz67xx1GMaXlDCchFvKAXMjk4jn/L1O3tKnoFDNsoLV1kgNQ==", - "license": "Apache-2.0", + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "license": "MIT", + "optional": true, "dependencies": { - "tslib": "^2.6.2" - }, + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", + "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=18.0.0" + "node": ">=18" } }, - "node_modules/@aws-sdk/util-endpoints": { - "version": "3.848.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.848.0.tgz", - "integrity": "sha512-fY/NuFFCq/78liHvRyFKr+aqq1aA/uuVSANjzr5Ym8c+9Z3HRPE9OrExAHoMrZ6zC8tHerQwlsXYYH5XZ7H+ww==", - "license": "Apache-2.0", + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", "dependencies": { - "@aws-sdk/types": "3.840.0", - "@smithy/types": "^4.3.1", - "@smithy/url-parser": "^4.0.4", - "@smithy/util-endpoints": "^3.0.6", - "tslib": "^2.6.2" + "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": ">=18.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@aws-sdk/util-locate-window": { - "version": "3.804.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.804.0.tgz", - "integrity": "sha512-zVoRfpmBVPodYlnMjgVjfGoEZagyRF5IPn3Uo6ZvOZp24chnW/FRstH7ESDHDDRga4z3V+ElUQHKpFDXWyBW5A==", + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=18.0.0" + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.840.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.840.0.tgz", - "integrity": "sha512-JdyZM3EhhL4PqwFpttZu1afDpPJCCc3eyZOLi+srpX11LsGj6sThf47TYQN75HT1CarZ7cCdQHGzP2uy3/xHfQ==", + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.840.0", - "@smithy/types": "^4.3.1", - "bowser": "^2.11.0", - "tslib": "^2.6.2" + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.857.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.857.0.tgz", - "integrity": "sha512-xWNfAnD2t5yACGW1wM3iLoy2FvRM8N/XjkjgJE1O35gBHn00evtLC9q4nkR4x7+vXdZb8cVw4Y6GmcfMckgFQg==", + "node_modules/@eslint/config-helpers": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "dev": true, "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-user-agent": "3.857.0", - "@aws-sdk/types": "3.840.0", - "@smithy/node-config-provider": "^4.1.3", - "@smithy/types": "^4.3.1", - "tslib": "^2.6.2" - }, "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@aws-sdk/xml-builder": { - "version": "3.821.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.821.0.tgz", - "integrity": "sha512-DIIotRnefVL6DiaHtO6/21DhJ4JZnnIwdNbpwiAhdt/AVbttcE4yw925gsjur0OGv5BTYXQXU3YnANBYnZjuQA==", + "node_modules/@eslint/core": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", - "tslib": "^2.6.2" + "@types/json-schema": "^7.0.15" }, "engines": { - "node": ">=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" }, "engines": { - "node": ">=6.9.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, "license": "MIT", "engines": { - "node": ">=6.9.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/runtime": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz", - "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==", + "node_modules/@eslint/js": { + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz", + "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", + "dev": true, "license": "MIT", "engines": { - "node": ">=6.9.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, - "node_modules/@bufbuild/protobuf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.6.3.tgz", - "integrity": "sha512-w/gJKME9mYN7ZoUAmSMAWXk4hkVpxRKvEJCb3dV5g9wwWdxTJJ0ayOJAVcNxtdqaxDyFuC0uz4RSGVacJ030PQ==", - "license": "(Apache-2.0 AND BSD-3-Clause)" + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } }, - "node_modules/@bufbuild/protoplugin": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/@bufbuild/protoplugin/-/protoplugin-2.6.3.tgz", - "integrity": "sha512-VceMuxeRukxGeABfo34SXq0VqY1MU+mzS+PBf0HAWo97ylFut8F6sQ3mV0tKiM08UQ/xQco7lxCn83BkoxrWrA==", + "node_modules/@eslint/plugin-kit": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", + "dev": true, "license": "Apache-2.0", "dependencies": { - "@bufbuild/protobuf": "2.6.3", - "@typescript/vfs": "^1.5.2", - "typescript": "5.4.5" + "@eslint/core": "^0.15.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@bufbuild/protoplugin/node_modules/typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "dev": true, "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" + "dependencies": { + "@types/json-schema": "^7.0.15" }, "engines": { - "node": ">=14.17" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@devicefarmer/adbkit-apkreader": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@devicefarmer/adbkit-apkreader/-/adbkit-apkreader-3.2.4.tgz", - "integrity": "sha512-WtVmPuirX3/lBN9Z2AC/4DnK7XrXcMi0h3yhxNogxgp6+GVbO6W4RqmuCKlNm1PjyI2GRbw4XwGRQ8cR/IRN6A==", + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, "license": "Apache-2.0", "dependencies": { - "bluebird": "^3.4.7", - "debug": "~4.3.1", - "yauzl": "^2.7.0" + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" }, "engines": { - "node": ">= 4" + "node": ">=18.18.0" } }, - "node_modules/@devicefarmer/adbkit-apkreader/node_modules/bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "license": "MIT" - }, - "node_modules/@devicefarmer/adbkit-monkey": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@devicefarmer/adbkit-monkey/-/adbkit-monkey-1.2.1.tgz", - "integrity": "sha512-ZzZY/b66W2Jd6NHbAhLyDWOEIBWC11VizGFk7Wx7M61JZRz7HR9Cq5P+65RKWUU7u6wgsE8Lmh9nE4Mz+U2eTg==", + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, "license": "Apache-2.0", "engines": { - "node": ">= 0.10.4" + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@devicefarmer/minicap-prebuilt": { - "resolved": "minicap-prebuilt", - "link": true + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, - "node_modules/@devicefarmer/minitouch-prebuilt": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@devicefarmer/minitouch-prebuilt/-/minitouch-prebuilt-1.3.0.tgz", - "integrity": "sha512-vZKh2b4EHxeySLmuO+4YTF5JUEsSuQXOkiVearkBgXerdnbQsVIBRyBvU5xVPkLWz7AI4b9eWIQWjz/a8nwodA==", - "license": "Apache-2.0" + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, - "node_modules/@devicefarmer/stf-appstore-db": { + "node_modules/@img/colour": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@devicefarmer/stf-appstore-db/-/stf-appstore-db-1.0.0.tgz", - "integrity": "sha512-dkGLTU6ax8nmOBQlW3iJH6cf3ufLW4kY37TlfT13MYS2yKwoatn5Kf8CzwzYMSVH4tFDoIHNTPCXTnaLherk3A==", - "license": "Apache-2.0" - }, - "node_modules/@devicefarmer/stf-browser-db": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@devicefarmer/stf-browser-db/-/stf-browser-db-1.0.2.tgz", - "integrity": "sha512-9Yu4hdqJeloLE7D6DfGFveNOkGJCg3/XYOb7/37U36MLI95IYEH7JZk9Cma222nxt7dx6Yb7BYI0NH0y6RyIIQ==", - "license": "Apache-2.0" - }, - "node_modules/@devicefarmer/stf-device-db": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@devicefarmer/stf-device-db/-/stf-device-db-1.4.0.tgz", - "integrity": "sha512-MTtmDWxmsFc1GPBQoHjS3+Tbf2kd39EYO70jqe9e7H6NqzUKxndF8h4e6LI2SU1JzPtACoiYxRd2rpxEQBoqkw==", - "license": "CC-BY-SA-4.0" + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } }, - "node_modules/@devicefarmer/stf-syrup": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@devicefarmer/stf-syrup/-/stf-syrup-1.0.2.tgz", - "integrity": "sha512-5I/m/9lUF4sSrXAeihMIMJobuRK7ZvkG5bad3d+k+wd3BRSLlMHdBzkWiUVY91xRKXeh0jBvQ2x8yr8UTCEDGQ==", + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], "license": "Apache-2.0", - "dependencies": { - "bluebird": "~1.1.0", - "lodash": "~2.4.1" + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">= 0.10" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" } }, - "node_modules/@devicefarmer/stf-syrup/node_modules/bluebird": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-1.1.1.tgz", - "integrity": "sha512-2zqtRvlriJL+CZg6muZ9Hzhdvhp+NzT9xUHR47vaPNkq6cY+y2Uri2FHFq+ry5HYHIarulomYqiTWbxJmrOcQw==", - "license": "MIT" + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } }, - "node_modules/@devicefarmer/stf-wiki": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@devicefarmer/stf-wiki/-/stf-wiki-1.0.0.tgz", - "integrity": "sha512-p87QMgXmJS5K8lWkVvYgwmSHmxSsB+N8ctcZZsGx26wOh+0/RiDfsVQAIMv+SME6IWVXIjo/FiZRsGNlvw85Sg==", - "license": "CC-BY-SA-4.0" + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", - "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", "cpu": [ "arm64" ], - "license": "MIT", + "license": "LGPL-3.0-or-later", "optional": true, "os": [ - "darwin" + "linux" ], - "engines": { - "node": ">=18" + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "url": "https://opencollective.com/libvips" } }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://opencollective.com/libvips" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.6", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@eslint/config-helpers": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", - "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@eslint/core": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", - "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", - "dev": true, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" } }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" } }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" } }, - "node_modules/@eslint/js": { - "version": "9.33.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz", - "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", - "dev": true, - "license": "MIT", + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { - "url": "https://eslint.org/donate" + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" } }, - "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", - "dev": true, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" } }, - "node_modules/@eslint/plugin-kit": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", - "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", - "dev": true, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.15.1", - "levn": "^0.4.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" } }, - "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", - "dev": true, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" } }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.18.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" } }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dev": true, - "license": "Apache-2.0", + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" + "@emnapi/runtime": "^1.7.0" }, "engines": { - "node": ">=18.18.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "license": "Apache-2.0", + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=18.18" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "url": "https://opencollective.com/libvips" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=12.22" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "url": "https://opencollective.com/libvips" } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=18.18" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "url": "https://opencollective.com/libvips" } }, "node_modules/@irdk/usbmux": { @@ -1568,6 +3136,27 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", + "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -3180,6 +4769,12 @@ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", "license": "BSD-3-Clause" }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "license": "MIT" + }, "node_modules/@sentry/core": { "version": "8.55.0", "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.55.0.tgz", @@ -4259,6 +5854,12 @@ "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", "license": "MIT" }, + "node_modules/@tsconfig/node20": { + "version": "20.1.8", + "resolved": "https://registry.npmjs.org/@tsconfig/node20/-/node20-20.1.8.tgz", + "integrity": "sha512-Em+IdPfByIzWRRpqWL4Z7ArLHZGxmc36BxE3jCz9nBFSm+5aLaPMZyjwu4yetvyKXeogWcxik4L1jB5JTWfw7A==", + "license": "MIT" + }, "node_modules/@types/bluebird": { "version": "3.5.42", "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.42.tgz", @@ -4476,6 +6077,12 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "license": "MIT" + }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -5436,24 +7043,331 @@ "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=0.10.0" + } + }, + "node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/any-base": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz", + "integrity": "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==", + "license": "MIT" + }, + "node_modules/appium-ios-device": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/appium-ios-device/-/appium-ios-device-3.1.9.tgz", + "integrity": "sha512-j4zNwszDvBHqyZKqX99RLcif3CnuYDpVKA9E2DBM/4mOFYT+o3bPAMrPZRx2Tt3RImoTCUrTCUinYopsPqX2Eg==", + "license": "Apache-2.0", + "dependencies": { + "@appium/support": "^7.0.0-rc.1", + "asyncbox": "^6.0.1", + "axios": "^1.6.7", + "bluebird": "^3.1.1", + "bplist-creator": "^0.x", + "bplist-parser": "^0.x", + "lodash": "^4.17.15", + "semver": "^7.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": ">=10" + } + }, + "node_modules/appium-ios-device/node_modules/asyncbox": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/asyncbox/-/asyncbox-6.1.0.tgz", + "integrity": "sha512-KZwKNVnDdDe0ubN+fFMuHhSljZNHnbjdJABImoqFzQP61oIg6sMlhXIqOIu3WRd7YwW89q+eVj2Ty/Ax5dbh2Q==", + "license": "Apache-2.0", + "dependencies": { + "p-limit": "^7.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": ">=10" + } + }, + "node_modules/appium-ios-device/node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "license": "MIT" + }, + "node_modules/appium-ios-device/node_modules/p-limit": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.3.0.tgz", + "integrity": "sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.2.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/appium-ios-device/node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/appium-ios-simulator": { + "version": "8.0.11", + "resolved": "https://registry.npmjs.org/appium-ios-simulator/-/appium-ios-simulator-8.0.11.tgz", + "integrity": "sha512-DMm+XyS4o4iNPJwcGI7eecfmH6B7E8Q/1pANjWIOQhac3hOdUdo1KZxbnIQ4lUbSLzvLWL1T5Rts1UP//Jtwtg==", + "license": "Apache-2.0", + "dependencies": { + "@appium/support": "^7.0.0-rc.1", + "@xmldom/xmldom": "^0.x", + "appium-xcode": "^6.0.0", + "async-lock": "^1.0.0", + "asyncbox": "^6.0.1", + "bluebird": "^3.5.1", + "lodash": "^4.2.1", + "node-simctl": "^8.1.1", + "semver": "^7.0.0", + "teen_process": "^4.0.4" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": ">=10" + } + }, + "node_modules/appium-ios-simulator/node_modules/@appium/logger": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@appium/logger/-/logger-2.0.4.tgz", + "integrity": "sha512-DH4IXGjn/9q4BfrbPdacremNS2FZXIu8vMQyUChWfP+rVRJ1TKMxXF1/ZXNpOfuu0FRrknclO96tUWG3HJj9Xg==", + "license": "ISC", + "dependencies": { + "console-control-strings": "1.1.0", + "lodash": "4.17.23", + "lru-cache": "11.2.5", + "set-blocking": "2.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": ">=10" + } + }, + "node_modules/appium-ios-simulator/node_modules/asyncbox": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/asyncbox/-/asyncbox-6.1.0.tgz", + "integrity": "sha512-KZwKNVnDdDe0ubN+fFMuHhSljZNHnbjdJABImoqFzQP61oIg6sMlhXIqOIu3WRd7YwW89q+eVj2Ty/Ax5dbh2Q==", + "license": "Apache-2.0", + "dependencies": { + "p-limit": "^7.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": ">=10" + } + }, + "node_modules/appium-ios-simulator/node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "license": "MIT" + }, + "node_modules/appium-ios-simulator/node_modules/glob": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.1.tgz", + "integrity": "sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.1.2", + "minipass": "^7.1.2", + "path-scurry": "^2.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/appium-ios-simulator/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/appium-ios-simulator/node_modules/lru-cache": { + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/appium-ios-simulator/node_modules/minimatch": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", + "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/appium-ios-simulator/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/appium-ios-simulator/node_modules/node-simctl": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/node-simctl/-/node-simctl-8.1.5.tgz", + "integrity": "sha512-8lQlne56cXGpPHjv49QXLQSOJuH+onlxHemlguSsutwbSdW+/ChC+xX932BEoG3qx62fpMPzRj3v2I1wVT4Ezw==", + "license": "Apache-2.0", + "dependencies": { + "@appium/logger": "^2.0.0-rc.1", + "asyncbox": "^6.0.1", + "bluebird": "^3.5.1", + "lodash": "^4.2.1", + "rimraf": "^6.0.1", + "semver": "^7.0.0", + "teen_process": "^4.0.4", + "uuid": "^13.0.0", + "which": "^6.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": ">=10" + } + }, + "node_modules/appium-ios-simulator/node_modules/p-limit": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.3.0.tgz", + "integrity": "sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.2.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/appium-ios-simulator/node_modules/path-scurry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", + "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/appium-ios-simulator/node_modules/rimraf": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.2.tgz", + "integrity": "sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==", + "license": "BlueOak-1.0.0", + "dependencies": { + "glob": "^13.0.0", + "package-json-from-dist": "^1.0.1" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/appium-ios-simulator/node_modules/teen_process": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/teen_process/-/teen_process-4.0.9.tgz", + "integrity": "sha512-AdH4nuHQTTiFEnib3wWnepnfa7Vz8QzOZ7EsLM8iz8pOlZmshjnODmWTt/8OA6v6A9gACURKc0OddGX28UoxFQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash": "^4.17.21", + "shell-quote": "^1.8.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": ">=10" + } + }, + "node_modules/appium-ios-simulator/node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/appium-ios-simulator/node_modules/which": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", + "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "node_modules/appium-ios-simulator/node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/any-base": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz", - "integrity": "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==", - "license": "MIT" - }, "node_modules/appium-sdb": { "version": "1.0.1-beta.1", "resolved": "https://registry.npmjs.org/appium-sdb/-/appium-sdb-1.0.1-beta.1.tgz", @@ -5515,43 +7429,12 @@ "yauzl": "^2.7.0" } }, - "node_modules/appium-support/node_modules/axios": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz", - "integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" - } - }, "node_modules/appium-support/node_modules/bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", "license": "MIT" }, - "node_modules/appium-support/node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/appium-support/node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -5577,6 +7460,168 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/appium-webdriveragent": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/appium-webdriveragent/-/appium-webdriveragent-11.1.4.tgz", + "integrity": "sha512-5NmVn2Qi7jezSXEHAOLt3E5qnp/9Mv9v1nzdbvjn8YN0O15pACEz3eqN1SBpjQ8A3pR8jBW7h4olJLoTs/+Y+A==", + "license": "Apache-2.0", + "dependencies": { + "@appium/base-driver": "^10.0.0-rc.1", + "@appium/strongbox": "^1.0.0-rc.1", + "@appium/support": "^7.0.0-rc.1", + "appium-ios-device": "^3.0.0", + "appium-ios-simulator": "^8.0.0", + "async-lock": "^1.0.0", + "asyncbox": "^6.1.0", + "axios": "^1.4.0", + "bluebird": "^3.5.5", + "lodash": "^4.17.11", + "teen_process": "^4.0.7" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": ">=10" + } + }, + "node_modules/appium-webdriveragent/node_modules/asyncbox": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/asyncbox/-/asyncbox-6.1.0.tgz", + "integrity": "sha512-KZwKNVnDdDe0ubN+fFMuHhSljZNHnbjdJABImoqFzQP61oIg6sMlhXIqOIu3WRd7YwW89q+eVj2Ty/Ax5dbh2Q==", + "license": "Apache-2.0", + "dependencies": { + "p-limit": "^7.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": ">=10" + } + }, + "node_modules/appium-webdriveragent/node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "license": "MIT" + }, + "node_modules/appium-webdriveragent/node_modules/p-limit": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.3.0.tgz", + "integrity": "sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.2.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/appium-webdriveragent/node_modules/teen_process": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/teen_process/-/teen_process-4.0.9.tgz", + "integrity": "sha512-AdH4nuHQTTiFEnib3wWnepnfa7Vz8QzOZ7EsLM8iz8pOlZmshjnODmWTt/8OA6v6A9gACURKc0OddGX28UoxFQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash": "^4.17.21", + "shell-quote": "^1.8.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": ">=10" + } + }, + "node_modules/appium-webdriveragent/node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/appium-xcode": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/appium-xcode/-/appium-xcode-6.1.8.tgz", + "integrity": "sha512-nk86u0wo4ZPCxQsiF/1PR/HewpB5NxSkNxUttRLiWEbMTA8FPqDlbUfrD/xaywMYUYkYIk+W9ufz3Gg0CKMBAA==", + "license": "Apache-2.0", + "dependencies": { + "@appium/support": "^7.0.0-rc.1", + "asyncbox": "^6.0.1", + "bluebird": "^3.7.2", + "lodash": "^4.17.4", + "plist": "^3.0.1", + "semver": "^7.0.0", + "teen_process": "^4.0.4" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": ">=10" + } + }, + "node_modules/appium-xcode/node_modules/asyncbox": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/asyncbox/-/asyncbox-6.1.0.tgz", + "integrity": "sha512-KZwKNVnDdDe0ubN+fFMuHhSljZNHnbjdJABImoqFzQP61oIg6sMlhXIqOIu3WRd7YwW89q+eVj2Ty/Ax5dbh2Q==", + "license": "Apache-2.0", + "dependencies": { + "p-limit": "^7.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": ">=10" + } + }, + "node_modules/appium-xcode/node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "license": "MIT" + }, + "node_modules/appium-xcode/node_modules/p-limit": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.3.0.tgz", + "integrity": "sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.2.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/appium-xcode/node_modules/teen_process": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/teen_process/-/teen_process-4.0.9.tgz", + "integrity": "sha512-AdH4nuHQTTiFEnib3wWnepnfa7Vz8QzOZ7EsLM8iz8pOlZmshjnODmWTt/8OA6v6A9gACURKc0OddGX28UoxFQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash": "^4.17.21", + "shell-quote": "^1.8.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": ">=10" + } + }, + "node_modules/appium-xcode/node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/aproba": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", @@ -5745,6 +7790,12 @@ "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", "license": "MIT" }, + "node_modules/async-lock": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", + "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==", + "license": "MIT" + }, "node_modules/asyncbox": { "version": "2.9.4", "resolved": "https://registry.npmjs.org/asyncbox/-/asyncbox-2.9.4.tgz", @@ -5788,6 +7839,31 @@ "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", "license": "MIT" }, + "node_modules/axios": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz", + "integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/b4a": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", + "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, "node_modules/backoff": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz", @@ -5806,6 +7882,20 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -6607,37 +8697,6 @@ "node": ">= 14.15.0" } }, - "node_modules/cmake-js/node_modules/axios": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz", - "integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/cmake-js/node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/cmake-js/node_modules/fs-extra": { "version": "11.3.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", @@ -6975,7 +9034,91 @@ "keygrip": "~1.1.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.8" + } + }, + "node_modules/copyfiles": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz", + "integrity": "sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^7.0.5", + "minimatch": "^3.0.3", + "mkdirp": "^1.0.4", + "noms": "0.0.0", + "through2": "^2.0.1", + "untildify": "^4.0.0", + "yargs": "^16.1.0" + }, + "bin": { + "copyfiles": "copyfiles", + "copyup": "copyfiles" + } + }, + "node_modules/copyfiles/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/copyfiles/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/copyfiles/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/copyfiles/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/copyfiles/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" } }, "node_modules/core-js": { @@ -7256,6 +9399,23 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "license": "MIT", + "optional": true + }, "node_modules/devtools-protocol": { "version": "0.0.927104", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.927104.tgz", @@ -7471,6 +9631,21 @@ "node": ">=10.13.0" } }, + "node_modules/env-paths": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-4.0.0.tgz", + "integrity": "sha512-pxP8eL2SwwaTRi/KHYwLYXinDs7gL3jxFcBYmEdYfZmZXbaVDvdppd0XBU8qVz03rDfKZMXg1omHCbsJjZrMsw==", + "license": "MIT", + "dependencies": { + "is-safe-filename": "^0.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/epoch-charting": { "version": "0.8.4", "resolved": "https://registry.npmjs.org/epoch-charting/-/epoch-charting-0.8.4.tgz", @@ -7918,6 +10093,15 @@ "node": ">=0.8.x" } }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/exif-parser": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz", @@ -8136,6 +10320,12 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -8222,6 +10412,15 @@ "fxparser": "src/cli/cli.js" } }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -8348,6 +10547,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/find-up-simple": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", + "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/find-versions": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-4.0.0.tgz", @@ -8864,6 +11075,13 @@ "dev": true, "license": "MIT" }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "license": "MIT", + "optional": true + }, "node_modules/har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -8962,6 +11180,73 @@ "node": "*" } }, + "node_modules/hosted-git-info": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz", + "integrity": "sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==", + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "optional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "license": "MIT", + "optional": true + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -9018,6 +11303,12 @@ "node": ">=0.10" } }, + "node_modules/http-status-codes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", + "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", + "license": "MIT" + }, "node_modules/husky": { "version": "4.3.8", "resolved": "https://registry.npmjs.org/husky/-/husky-4.3.8.tgz", @@ -9272,6 +11563,18 @@ "not-in-publish": "not-in-publish.js" } }, + "node_modules/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -9401,12 +11704,54 @@ "node": ">=0.12.0" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-safe-filename": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-safe-filename/-/is-safe-filename-0.1.1.tgz", + "integrity": "sha512-4SrR7AdnY11LHfDKTZY1u6Ga3RuxZdl3YKWWShO5iyuG5h8QS4GD2tOb04peBJ5I7pXbR+CGBNEhTcwK+FzN3g==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", "license": "MIT" }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -9895,6 +12240,22 @@ "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", "license": "MIT" }, + "node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/long": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz", @@ -10040,6 +12401,36 @@ "node": ">= 8" } }, + "node_modules/method-override": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/method-override/-/method-override-3.0.0.tgz", + "integrity": "sha512-IJ2NNN/mSl9w3kzWB92rcdHpz+HjkxhDJWNDBqSlas+zQdP8wBiJzITPg08M/k2uVvMow7Sk41atndNtt/PHSA==", + "license": "MIT", + "dependencies": { + "debug": "3.1.0", + "methods": "~1.1.2", + "parseurl": "~1.3.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/method-override/node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/method-override/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -10113,6 +12504,13 @@ "dom-walk": "^0.1.0" } }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC", + "optional": true + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -10260,6 +12658,70 @@ "whatwg-url": "^14.1.0 || ^13.0.0" } }, + "node_modules/morgan": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.1.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/morgan/node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -10555,19 +13017,71 @@ "npm": ">=8" } }, - "node_modules/node-simctl/node_modules/which": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", - "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", - "license": "ISC", + "node_modules/node-simctl/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/noms": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/noms/-/noms-0.0.0.tgz", + "integrity": "sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==", + "dev": true, + "license": "ISC", + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "~1.0.31" + } + }, + "node_modules/noms/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/noms/node_modules/readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/noms/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-package-data": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-8.0.0.tgz", + "integrity": "sha512-RWk+PI433eESQ7ounYxIp67CYuVsS1uYSonX3kA6ps/3LWfjVQa/ptEg6Y3T6uAMq1mWpX9PQ+qx+QaHpsc7gQ==", + "license": "BSD-2-Clause", "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" + "hosted-git-info": "^9.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/normalize-path": { @@ -10664,6 +13178,13 @@ "http-https": "^1.0.0" } }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "license": "MIT", + "optional": true + }, "node_modules/oidc-token-hash": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.1.0.tgz", @@ -11030,6 +13551,21 @@ "node": ">=6" } }, + "node_modules/package-directory": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/package-directory/-/package-directory-8.1.0.tgz", + "integrity": "sha512-qHKRW0pw3lYdZMQVkjDBqh8HlamH/LCww2PH7OWEp4Qrt3SFeYMNpnJrQzlSnGrDD5zGR51XqBh7FnNCdVNEHA==", + "license": "MIT", + "dependencies": { + "find-up-simple": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -11877,6 +14413,54 @@ "node": ">=0.10.0" } }, + "node_modules/read-pkg": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-10.0.0.tgz", + "integrity": "sha512-A70UlgfNdKI5NSvTTfHzLQj7NJRpJ4mT5tGafkllJ4wh71oYuGm/pzphHcmW4s35iox56KSK721AihodoXSc/A==", + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.4", + "normalize-package-data": "^8.0.0", + "parse-json": "^8.3.0", + "type-fest": "^5.2.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/parse-json/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -12320,6 +14904,55 @@ "integrity": "sha512-fJhQQI5tLrQvYIYFpOnFinzv9dwmR7hRnUz1XqP3OJ1jIweTNOd6aTO4jwQSgcBSFUB+/KHJxuGneime+FdzOw==", "license": "MIT" }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/router/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -12431,6 +15064,13 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "license": "MIT", + "optional": true + }, "node_modules/semver": { "version": "7.5.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", @@ -12699,6 +15339,64 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -12825,6 +15523,15 @@ "node": ">=8" } }, + "node_modules/slugify": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz", + "integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -12953,6 +15660,70 @@ "memory-pager": "^1.0.2" } }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", + "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", + "license": "CC0-1.0" + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, "node_modules/split": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", @@ -13057,6 +15828,17 @@ "bluebird": "^2.6.2" } }, + "node_modules/streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, "node_modules/strict-uri-encode": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", @@ -13273,6 +16055,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tapable": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", @@ -13434,6 +16228,15 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "license": "MIT" }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/throttleit": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.1.tgz", @@ -13449,6 +16252,43 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "license": "MIT" }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/through2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/through2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/timm": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/timm/-/timm-1.7.1.tgz", @@ -13713,6 +16553,21 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.4.1.tgz", + "integrity": "sha512-xygQcmneDyzsEuKZrFbRMne5HDqMs++aFzefrJTgEIKjQ3rekM+RPfFCVq2Gp1VIDqddoYeppCj4Pcb+RZW0GQ==", + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -13800,6 +16655,18 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unidragger": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/unidragger/-/unidragger-1.1.5.tgz", @@ -13877,6 +16744,16 @@ "node": ">= 0.8" } }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", @@ -13932,6 +16809,21 @@ "requires-port": "^1.0.0" } }, + "node_modules/usb-hotplug": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/usb-hotplug/-/usb-hotplug-0.1.6.tgz", + "integrity": "sha512-fgM12UBGIclItx+SmX+AlxSS+vO4KUGNvshdGXMSyN/gXWDnjLkYdj9tpyypKd7bvHz4HJRbqQEr9zQ/ommjXg==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@e-khalilov/usb-hotplug-darwin-arm64": "0.1.6", + "@e-khalilov/usb-hotplug-darwin-x64": "0.1.6", + "@e-khalilov/usb-hotplug-linux-x64-gnu": "0.1.6", + "@e-khalilov/usb-hotplug-win32-x64-msvc": "0.1.6" + } + }, "node_modules/utf-8-validate": { "version": "5.0.9", "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.9.tgz", @@ -13988,6 +16880,16 @@ "uuid": "dist/esm/bin/uuid" } }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, "node_modules/validator": { "version": "13.9.0", "resolved": "https://registry.npmjs.org/validator/-/validator-13.9.0.tgz", @@ -14066,6 +16968,16 @@ "node": ">=10.13.0" } }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "license": "MIT", + "optional": true, + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -14512,6 +17424,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zeromq": { "version": "6.4.2", "resolved": "https://registry.npmjs.org/zeromq/-/zeromq-6.4.2.tgz", diff --git a/package.json b/package.json index 70748fb03d..6369834519 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vk-devicehub", - "version": "1.5.0", + "version": "1.5.2", "description": "Smartphone Test Farm", "type": "module", "keywords": [ @@ -12,9 +12,9 @@ "remote" ], "bin": { - "stf": "./bin/stf.mjs", - "devicehub": "./bin/stf.mjs", - "dh": "./bin/stf.mjs" + "stf": "./.build/bin/stf.mjs", + "devicehub": "./.build/bin/stf.mjs", + "dh": "./.build/bin/stf.mjs" }, "yargs": { "duplicate-arguments-array": false @@ -26,9 +26,15 @@ "build:swagger:tests": "cd ./test/api && poetry run just regen-schema", "build:swagger:ui": "cd ./ui && npm run generate-api", "build:swagger": "cd ./lib/units/api && python3 ./gen_routes.py & npm run build:swagger:ui", + "build": "tsc -p ./tsconfig.node.json", "protoc": "npx protoc --ts_out ./lib/wire --ts_opt generate_dependencies --proto_path ./lib/wire ./lib/wire/wire.proto", - "build": "tsc", - "dev": "tsx ./bin/devstf.mjs" + "copy-assets": "copyfiles -e '**/*.ts' -e '**/*.tsx' -e '**/*.js' -e '**/*.mjs' -e '**/*.jsx' 'lib/**/*' .build", + "chmod-bin": "node -e \"require('fs').chmodSync('.build/bin/stf.mjs', 0o755)\"", + "postinstall": "tsc -p ./tsconfig.node.json && npm run copy-assets && npm run chmod-bin && cd ./ui && npm ci", + "prepare-wda-bundle-id": "LC_ALL=C sed -i '' -e 's/com\\.facebook\\.WebDriverAgentRunner/com.dhub.WebDriverAgentRunner/g' -e 's/com\\.facebook/com.dh/g' node_modules/appium-webdriveragent/WebDriverAgent.xcodeproj/project.pbxproj", + "setup-wda": "npm run prepare-wda-bundle-id && echo \"Enable 'Automatically manage signing' for all targets and select your Development Team.\n\nOpening xcode project...\n\" && open node_modules/appium-webdriveragent/WebDriverAgent.xcodeproj", + "save-wda-sign": "npx patch-package appium-webdriveragent", + "apply-wda-sign": "npx patch-package" }, "dependencies": { "@aws-sdk/client-s3": "^3.772.0", @@ -51,6 +57,7 @@ "@types/cookie-parser": "^1.4.10", "@u4/adbkit": "^5.1.7", "appium-sdb": "^1.0.1-beta.1", + "appium-webdriveragent": "^11.1.4", "basic-auth": "1.1.0", "bluebird": "2.11.0", "body-parser": "^1.20.2", @@ -124,6 +131,7 @@ "tsx": "4.20.3", "underscore.string": "3.3.6", "url-join": "1.1.0", + "usb-hotplug": "^0.1.6", "utf-8-validate": "5.0.9", "uuid": "^11.0.3", "webpack": "^5.88.1", @@ -152,6 +160,7 @@ "@types/ws": "^3.2.1", "async": "2.6.4", "cli-docs-generator": "1.0.7", + "copyfiles": "^2.4.1", "esbuild": "0.25.8", "eslint": "^9.30.1", "event-stream": "3.3.5", diff --git a/tsconfig.json b/tsconfig.json index 77913205eb..d8be88dfba 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,7 @@ "strictFunctionTypes": true, "strictPropertyInitialization": true, "strictBindCallApply": true, + "skipLibCheck": true, "noImplicitThis": true, "noImplicitReturns": false, "alwaysStrict": true, @@ -28,7 +29,8 @@ "./lib/**/*.js", "./lib/**/*.mjs", "./lib/types/**/*.d.ts", - "./lib/**/*.ts" + "./lib/**/*.ts", + "./bin/**/*.mjs" ], "exclude": ["node_modules/**"], "verbose": true diff --git a/tsconfig.node.json b/tsconfig.node.json index 0a5d47e9ef..733a8fd93b 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -1,11 +1,9 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "allowJs": false, + "allowJs": true, "checkJs": false, "noImplicitAny": false, - "skipLibCheck": true - }, - "include": ["./lib/**/*.ts"], - "exclude": ["./lib/**/*.js"] + "outDir": "./.build" + } } diff --git a/tsconfig.typecheck.json b/tsconfig.typecheck.json index 186301f938..8dccd1c157 100644 --- a/tsconfig.typecheck.json +++ b/tsconfig.typecheck.json @@ -3,6 +3,9 @@ "compilerOptions": { "noImplicitAny": false, "noEmit": true, - "skipLibCheck": true - } + "skipLibCheck": true, + "checkJs": false, + "allowJs": true + }, + "exclude": ["./lib/**/*.js"] } diff --git a/ui/package-lock.json b/ui/package-lock.json index ad744371f5..f1885b095d 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "vk-devicehub-ui", "version": "1.5.0", + "hasInstallScript": true, "dependencies": { "@lukemorales/query-key-factory": "^1.3.4", "@tanstack/match-sorter-utils": "^8.19.4", diff --git a/ui/package.json b/ui/package.json index ac22e5449a..e406976d84 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,7 +1,7 @@ { "name": "vk-devicehub-ui", "private": true, - "version": "1.5.0", + "version": "1.5.2", "type": "module", "scripts": { "dev": "vite", @@ -25,7 +25,8 @@ "bundle-analyzer": "npx vite-bundle-visualizer --config ./vite.config.ts", "generate-code": "plop", "generate-api": "orval --config ./orval.config.ts", - "type-checking": "tsc --p ./tsconfig.app.json --noEmit" + "type-checking": "tsc --p ./tsconfig.app.json --noEmit", + "postinstall": "npm run build:prod" }, "dependencies": { "@lukemorales/query-key-factory": "^1.3.4", diff --git a/ui/src/services/touch-service/touch-service.ts b/ui/src/services/touch-service/touch-service.ts index 73be961df0..37372c376a 100644 --- a/ui/src/services/touch-service/touch-service.ts +++ b/ui/src/services/touch-service/touch-service.ts @@ -26,7 +26,6 @@ import type { MouseEvent as ReactMouseEvent } from 'react' @deviceConnectionRequired() export class TouchService { private readonly cycle = 100 - private prevCoords: { x: number; y: number } = { x: 0, y: 0 } private slotted: Record = {} private seq = -1 private fakePinch = false @@ -93,11 +92,6 @@ export class TouchService { screenBoundingRect, }) - this.prevCoords = { - x: scaled.coords.xP, - y: scaled.coords.yP, - } - const pressure = 0.5 this.deviceControlStore.touchDown({ @@ -145,7 +139,7 @@ export class TouchService { this.lastPossiblyBuggyMouseUpEvent = null } - mouseUpListener({ isRightButtonPressed, mousePageX, mousePageY }: MouseUpListener): void { + mouseUpListener({ isRightButtonPressed }: MouseUpListener): void { if (!this.isMouseDown) return this.isMouseDown = false @@ -153,39 +147,6 @@ export class TouchService { // NOTE: Skip right button click if (isRightButtonPressed) return - const { data: device } = this.deviceBySerialStore.deviceQueryResult() - - if (!device?.display?.width || !device.display?.height || !this.deviceScreenStore.getCanvasWrapper) return - - const screenBoundingRect = this.deviceScreenStore.getCanvasWrapper.getBoundingClientRect() - - const scaled = this.getScaledCoords({ - displayWidth: device.display.width, - displayHeight: device.display.height, - isIosDevice: device.manufacturer === 'Apple', - mousePageX, - mousePageY, - screenBoundingRect, - }) - - const pressure = 0.5 - - if ( - (Math.abs(this.prevCoords.x - scaled.coords.xP) >= 0.1 || - Math.abs(this.prevCoords.y - scaled.coords.yP) >= 0.1) && - device.manufacturer === 'Apple' - ) { - this.deviceControlStore.touchMoveIos({ - x: scaled.coords.xP, - y: scaled.coords.yP, - pX: this.prevCoords.x, - pY: this.prevCoords.y, - pressure, - seq: this.nextSeq(), - contact: 0, - }) - } - this.deviceControlStore.touchUp(this.nextSeq(), 0) if (this.fakePinch) { From d10c1fe334c7c5e9c055f08afad44b3e87ea1190 Mon Sep 17 00:00:00 2001 From: Konstantin Solovev <34122262+koctuk999@users.noreply.github.com> Date: Fri, 27 Feb 2026 20:58:11 +0300 Subject: [PATCH 20/23] fix error message and button on auth-ldap-page (#393) Co-authored-by: kvsolovev --- ui/src/components/views/auth/auth-ldap-page.tsx | 13 +++++++------ ui/src/lib/hooks/use-ldap-auth.hook.ts | 4 ++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/ui/src/components/views/auth/auth-ldap-page.tsx b/ui/src/components/views/auth/auth-ldap-page.tsx index 75adcf917a..eb1a21ca81 100644 --- a/ui/src/components/views/auth/auth-ldap-page.tsx +++ b/ui/src/components/views/auth/auth-ldap-page.tsx @@ -21,7 +21,7 @@ export const AuthLdapPage = () => { const [usernameError, setUsernameError] = useState('') const [passwordError, setPasswordError] = useState('') const [formError, setFormError] = useState('') - const { data: authData, error, mutate: auth, isSuccess } = useLdapAuth() + const { data: authData, error, mutate: auth, isSuccess, isPending } = useLdapAuth() const { data: authContact } = useGetAuthContact() const onFormSubmit = async (event: FormEvent) => { @@ -51,8 +51,8 @@ export const AuthLdapPage = () => { }, [authData]) useEffect(() => { - if (error?.response?.data.error === 'ValidationError') { - for (const item of error.response.data.validationErrors) { + if (error?.data.error === 'ValidationError') { + for (const item of error.data.validationErrors) { if (item.param === 'username') { setUsernameError(item.msg) } @@ -65,13 +65,13 @@ export const AuthLdapPage = () => { return } - if (error?.response?.data.error === 'InvalidCredentialsError') { + if (error?.data.error === 'InvalidCredentialsError') { setFormError('Incorrect login details') return } - if (error?.response?.data.error) { + if (error?.data.error) { setFormError('We do not recognize you. Please check your spelling and try again or use another login option') } }, [error]) @@ -113,7 +113,8 @@ export const AuthLdapPage = () => {