• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2022 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15import {_TextDecoder, _TextEncoder} from 'custom_utils';
16
17import {defer, Deferred} from '../../base/deferred';
18import {assertExists, assertFalse, assertTrue} from '../../base/logging';
19import {isString} from '../../base/object_utils';
20import {CmdType} from '../../controller/adb_interfaces';
21
22import {AdbConnectionImpl} from './adb_connection_impl';
23import {AdbKeyManager, maybeStoreKey} from './auth/adb_key_manager';
24import {RecordingError, wrapRecordingError} from './recording_error_handling';
25import {
26  ByteStream,
27  OnStreamCloseCallback,
28  OnStreamDataCallback,
29} from './recording_interfaces_v2';
30import {ALLOW_USB_DEBUGGING, findInterfaceAndEndpoint} from './recording_utils';
31
32const textEncoder = new _TextEncoder();
33const textDecoder = new _TextDecoder();
34
35export const VERSION_WITH_CHECKSUM = 0x01000000;
36export const VERSION_NO_CHECKSUM = 0x01000001;
37export const DEFAULT_MAX_PAYLOAD_BYTES = 256 * 1024;
38
39export enum AdbState {
40  DISCONNECTED = 0,
41  // Authentication steps, see AdbConnectionOverWebUsb's handleAuthentication().
42  AUTH_STARTED = 1,
43  AUTH_WITH_PRIVATE = 2,
44  AUTH_WITH_PUBLIC = 3,
45
46  CONNECTED = 4,
47}
48
49enum AuthCmd {
50  TOKEN = 1,
51  SIGNATURE = 2,
52  RSAPUBLICKEY = 3,
53}
54
55function generateChecksum(data: Uint8Array): number {
56  let res = 0;
57  for (let i = 0; i < data.byteLength; i++) res += data[i];
58  return res & 0xffffffff;
59}
60
61// Message to be written to the adb connection. Contains the message itself
62// and the corresponding stream identifier.
63interface WriteQueueElement {
64  message: Uint8Array;
65  localStreamId: number;
66}
67
68export class AdbConnectionOverWebusb extends AdbConnectionImpl {
69  private state: AdbState = AdbState.DISCONNECTED;
70  private connectingStreams = new Map<number, Deferred<AdbOverWebusbStream>>();
71  private streams = new Set<AdbOverWebusbStream>();
72  private maxPayload = DEFAULT_MAX_PAYLOAD_BYTES;
73  private writeInProgress = false;
74  private writeQueue: WriteQueueElement[] = [];
75
76  // Devices after Dec 2017 don't use checksum. This will be auto-detected
77  // during the connection.
78  private useChecksum = true;
79
80  private lastStreamId = 0;
81  private usbInterfaceNumber?: number;
82  private usbReadEndpoint = -1;
83  private usbWriteEpEndpoint = -1;
84  private isUsbReceiveLoopRunning = false;
85
86  private pendingConnPromises: Array<Deferred<void>> = [];
87
88  // We use a key pair for authenticating with the device, which we do in
89  // two ways:
90  // - Firstly, signing with the private key.
91  // - Secondly, sending over the public key (at which point the device asks the
92  //   user for permissions).
93  // Once we've sent the public key, for future recordings we only need to
94  // sign with the private key, so the user doesn't need to give permissions
95  // again.
96  constructor(private device: USBDevice, private keyManager: AdbKeyManager) {
97    super();
98  }
99
100  shell(cmd: string): Promise<AdbOverWebusbStream> {
101    return this.openStream('shell:' + cmd);
102  }
103
104  connectSocket(path: string): Promise<AdbOverWebusbStream> {
105    return this.openStream(path);
106  }
107
108  async canConnectWithoutContention(): Promise<boolean> {
109    await this.device.open();
110    const usbInterfaceNumber = await this.setupUsbInterface();
111    try {
112      await this.device.claimInterface(usbInterfaceNumber);
113      await this.device.releaseInterface(usbInterfaceNumber);
114      return true;
115    } catch (e) {
116      return false;
117    }
118  }
119
120  protected async openStream(
121    destination: string,
122  ): Promise<AdbOverWebusbStream> {
123    const streamId = ++this.lastStreamId;
124    const connectingStream = defer<AdbOverWebusbStream>();
125    this.connectingStreams.set(streamId, connectingStream);
126    // We create the stream before trying to establish the connection, so
127    // that if we fail to connect, we will reject the connecting stream.
128    await this.ensureConnectionEstablished();
129    await this.sendMessage('OPEN', streamId, 0, destination);
130    return connectingStream;
131  }
132
133  private async ensureConnectionEstablished(): Promise<void> {
134    if (this.state === AdbState.CONNECTED) {
135      return;
136    }
137
138    if (this.state === AdbState.DISCONNECTED) {
139      await this.device.open();
140      if (!(await this.canConnectWithoutContention())) {
141        await this.device.reset();
142      }
143      const usbInterfaceNumber = await this.setupUsbInterface();
144      await this.device.claimInterface(usbInterfaceNumber);
145    }
146
147    await this.startAdbAuth();
148    if (!this.isUsbReceiveLoopRunning) {
149      this.usbReceiveLoop();
150    }
151    const connPromise = defer<void>();
152    this.pendingConnPromises.push(connPromise);
153    await connPromise;
154  }
155
156  private async setupUsbInterface(): Promise<number> {
157    const interfaceAndEndpoint = findInterfaceAndEndpoint(this.device);
158    // `findInterfaceAndEndpoint` will always return a non-null value because
159    // we check for this in 'android_webusb_target_factory'. If no interface and
160    // endpoints are found, we do not create a target, so we can not connect to
161    // it, so we will never reach this logic.
162    const {configurationValue, usbInterfaceNumber, endpoints} =
163      assertExists(interfaceAndEndpoint);
164    this.usbInterfaceNumber = usbInterfaceNumber;
165    this.usbReadEndpoint = this.findEndpointNumber(endpoints, 'in');
166    this.usbWriteEpEndpoint = this.findEndpointNumber(endpoints, 'out');
167    assertTrue(this.usbReadEndpoint >= 0 && this.usbWriteEpEndpoint >= 0);
168    await this.device.selectConfiguration(configurationValue);
169    return usbInterfaceNumber;
170  }
171
172  async streamClose(stream: AdbOverWebusbStream): Promise<void> {
173    const otherStreamsQueue = this.writeQueue.filter(
174      (queueElement) => queueElement.localStreamId !== stream.localStreamId,
175    );
176    const droppedPacketCount =
177      this.writeQueue.length - otherStreamsQueue.length;
178    if (droppedPacketCount > 0) {
179      console.debug(
180        `Dropping ${droppedPacketCount} queued messages due to stream closing.`,
181      );
182      this.writeQueue = otherStreamsQueue;
183    }
184
185    this.streams.delete(stream);
186    if (this.streams.size === 0) {
187      // We disconnect BEFORE calling `signalStreamClosed`. Otherwise, there can
188      // be a race condition:
189      // Stream A: streamA.onStreamClose
190      // Stream B: device.open
191      // Stream A: device.releaseInterface
192      // Stream B: device.transferOut -> CRASH
193      await this.disconnect();
194    }
195    stream.signalStreamClosed();
196  }
197
198  streamWrite(msg: string | Uint8Array, stream: AdbOverWebusbStream): void {
199    const raw = isString(msg) ? textEncoder.encode(msg) : msg;
200    if (this.writeInProgress) {
201      this.writeQueue.push({message: raw, localStreamId: stream.localStreamId});
202      return;
203    }
204    this.writeInProgress = true;
205    this.sendMessage('WRTE', stream.localStreamId, stream.remoteStreamId, raw);
206  }
207
208  // We disconnect in 2 cases:
209  // 1. When we close the last stream of the connection. This is to prevent the
210  // browser holding onto the USB interface after having finished a trace
211  // recording, which would make it impossible to use "adb shell" from the same
212  // machine until the browser is closed.
213  // 2. When we get a USB disconnect event. This happens for instance when the
214  // device is unplugged.
215  async disconnect(disconnectMessage?: string): Promise<void> {
216    if (this.state === AdbState.DISCONNECTED) {
217      return;
218    }
219    // Clear the resources in a synchronous method, because this can be used
220    // for error handling callbacks as well.
221    this.reachDisconnectState(disconnectMessage);
222
223    // We have already disconnected so there is no need to pass a callback
224    // which clears resources or notifies the user into 'wrapRecordingError'.
225    await wrapRecordingError(
226      this.device.releaseInterface(assertExists(this.usbInterfaceNumber)),
227      () => {},
228    );
229    this.usbInterfaceNumber = undefined;
230  }
231
232  // This is a synchronous method which clears all resources.
233  // It can be used as a callback for error handling.
234  reachDisconnectState(disconnectMessage?: string): void {
235    // We need to delete the streams BEFORE checking the Adb state because:
236    //
237    // We create streams before changing the Adb state from DISCONNECTED.
238    // In case we can not claim the device, we will create a stream, but fail
239    // to connect to the WebUSB device so the state will remain DISCONNECTED.
240    const streamsToDelete = this.connectingStreams.entries();
241    // Clear the streams before rejecting so we are not caught in a loop of
242    // handling promise rejections.
243    this.connectingStreams.clear();
244    for (const [id, stream] of streamsToDelete) {
245      stream.reject(
246        `Failed to open stream with id ${id} because adb was disconnected.`,
247      );
248    }
249
250    if (this.state === AdbState.DISCONNECTED) {
251      return;
252    }
253
254    this.state = AdbState.DISCONNECTED;
255    this.writeInProgress = false;
256
257    this.writeQueue = [];
258
259    this.streams.forEach((stream) => stream.close());
260    this.onDisconnect(disconnectMessage);
261  }
262
263  private async startAdbAuth(): Promise<void> {
264    const VERSION = this.useChecksum
265      ? VERSION_WITH_CHECKSUM
266      : VERSION_NO_CHECKSUM;
267    this.state = AdbState.AUTH_STARTED;
268    await this.sendMessage('CNXN', VERSION, this.maxPayload, 'host:1:UsbADB');
269  }
270
271  private findEndpointNumber(
272    endpoints: USBEndpoint[],
273    direction: 'out' | 'in',
274    type = 'bulk',
275  ): number {
276    const ep = endpoints.find(
277      (ep) => ep.type === type && ep.direction === direction,
278    );
279
280    if (ep) return ep.endpointNumber;
281
282    throw new RecordingError(`Cannot find ${direction} endpoint`);
283  }
284
285  private async usbReceiveLoop(): Promise<void> {
286    assertFalse(this.isUsbReceiveLoopRunning);
287    this.isUsbReceiveLoopRunning = true;
288    for (; this.state !== AdbState.DISCONNECTED; ) {
289      const res = await this.wrapUsb(
290        this.device.transferIn(this.usbReadEndpoint, ADB_MSG_SIZE),
291      );
292      if (!res) {
293        this.isUsbReceiveLoopRunning = false;
294        return;
295      }
296      if (res.status !== 'ok') {
297        // Log and ignore messages with invalid status. These can occur
298        // when the device is connected/disconnected repeatedly.
299        console.error(
300          `Received message with unexpected status '${res.status}'`,
301        );
302        continue;
303      }
304
305      const msg = AdbMsg.decodeHeader(res.data!);
306      if (msg.dataLen > 0) {
307        const resp = await this.wrapUsb(
308          this.device.transferIn(this.usbReadEndpoint, msg.dataLen),
309        );
310        if (!resp) {
311          this.isUsbReceiveLoopRunning = false;
312          return;
313        }
314        msg.data = new Uint8Array(
315          resp.data!.buffer,
316          resp.data!.byteOffset,
317          resp.data!.byteLength,
318        );
319      }
320
321      if (this.useChecksum && generateChecksum(msg.data) !== msg.dataChecksum) {
322        // We ignore messages with an invalid checksum. These sometimes appear
323        // when the page is re-loaded in a middle of a recording.
324        continue;
325      }
326      // The server can still send messages streams for previous streams.
327      // This happens for instance if we record, reload the recording page and
328      // then record again. We can also receive a 'WRTE' or 'OKAY' after
329      // we have sent a 'CLSE' and marked the state as disconnected.
330      if (
331        (msg.cmd === 'CLSE' || msg.cmd === 'WRTE') &&
332        !this.getStreamForLocalStreamId(msg.arg1)
333      ) {
334        continue;
335      } else if (
336        msg.cmd === 'OKAY' &&
337        !this.connectingStreams.has(msg.arg1) &&
338        !this.getStreamForLocalStreamId(msg.arg1)
339      ) {
340        continue;
341      } else if (
342        msg.cmd === 'AUTH' &&
343        msg.arg0 === AuthCmd.TOKEN &&
344        this.state === AdbState.AUTH_WITH_PUBLIC
345      ) {
346        // If we start a recording but fail because of a faulty physical
347        // connection to the device, when we start a new recording, we will
348        // received multiple AUTH tokens, of which we should ignore all but
349        // one.
350        continue;
351      }
352
353      // handle the ADB message from the device
354      if (msg.cmd === 'CLSE') {
355        assertExists(this.getStreamForLocalStreamId(msg.arg1)).close();
356      } else if (msg.cmd === 'AUTH' && msg.arg0 === AuthCmd.TOKEN) {
357        const key = await this.keyManager.getKey();
358        if (this.state === AdbState.AUTH_STARTED) {
359          // During this step, we send back the token received signed with our
360          // private key. If the device has previously received our public key,
361          // the dialog asking for user confirmation will not be displayed on
362          // the device.
363          this.state = AdbState.AUTH_WITH_PRIVATE;
364          await this.sendMessage(
365            'AUTH',
366            AuthCmd.SIGNATURE,
367            0,
368            key.sign(msg.data),
369          );
370        } else {
371          // If our signature with the private key is not accepted by the
372          // device, we generate a new keypair and send the public key.
373          this.state = AdbState.AUTH_WITH_PUBLIC;
374          await this.sendMessage(
375            'AUTH',
376            AuthCmd.RSAPUBLICKEY,
377            0,
378            key.getPublicKey() + '\0',
379          );
380          this.onStatus(ALLOW_USB_DEBUGGING);
381          await maybeStoreKey(key);
382        }
383      } else if (msg.cmd === 'CNXN') {
384        assertTrue(
385          [AdbState.AUTH_WITH_PRIVATE, AdbState.AUTH_WITH_PUBLIC].includes(
386            this.state,
387          ),
388        );
389        this.state = AdbState.CONNECTED;
390        this.maxPayload = msg.arg1;
391
392        const deviceVersion = msg.arg0;
393
394        if (
395          ![VERSION_WITH_CHECKSUM, VERSION_NO_CHECKSUM].includes(deviceVersion)
396        ) {
397          throw new RecordingError(`Version ${msg.arg0} not supported.`);
398        }
399        this.useChecksum = deviceVersion === VERSION_WITH_CHECKSUM;
400        this.state = AdbState.CONNECTED;
401
402        // This will resolve the promises awaited by
403        // "ensureConnectionEstablished".
404        this.pendingConnPromises.forEach((connPromise) =>
405          connPromise.resolve(),
406        );
407        this.pendingConnPromises = [];
408      } else if (msg.cmd === 'OKAY') {
409        if (this.connectingStreams.has(msg.arg1)) {
410          const connectingStream = assertExists(
411            this.connectingStreams.get(msg.arg1),
412          );
413          const stream = new AdbOverWebusbStream(this, msg.arg1, msg.arg0);
414          this.streams.add(stream);
415          this.connectingStreams.delete(msg.arg1);
416          connectingStream.resolve(stream);
417        } else {
418          assertTrue(this.writeInProgress);
419          this.writeInProgress = false;
420          for (; this.writeQueue.length; ) {
421            // We go through the queued writes and choose the first one
422            // corresponding to a stream that's still active.
423            const queuedElement = assertExists(this.writeQueue.shift());
424            const queuedStream = this.getStreamForLocalStreamId(
425              queuedElement.localStreamId,
426            );
427            if (queuedStream) {
428              queuedStream.write(queuedElement.message);
429              break;
430            }
431          }
432        }
433      } else if (msg.cmd === 'WRTE') {
434        const stream = assertExists(this.getStreamForLocalStreamId(msg.arg1));
435        await this.sendMessage(
436          'OKAY',
437          stream.localStreamId,
438          stream.remoteStreamId,
439        );
440        stream.signalStreamData(msg.data);
441      } else {
442        this.isUsbReceiveLoopRunning = false;
443        throw new RecordingError(
444          `Unexpected message ${msg} in state ${this.state}`,
445        );
446      }
447    }
448    this.isUsbReceiveLoopRunning = false;
449  }
450
451  private getStreamForLocalStreamId(
452    localStreamId: number,
453  ): AdbOverWebusbStream | undefined {
454    for (const stream of this.streams) {
455      if (stream.localStreamId === localStreamId) {
456        return stream;
457      }
458    }
459    return undefined;
460  }
461
462  //  The header and the message data must be sent consecutively. Using 2 awaits
463  //  Another message can interleave after the first header has been sent,
464  //  resulting in something like [header1] [header2] [data1] [data2];
465  //  In this way we are waiting both promises to be resolved before continuing.
466  private async sendMessage(
467    cmd: CmdType,
468    arg0: number,
469    arg1: number,
470    data?: Uint8Array | string,
471  ): Promise<void> {
472    const msg = AdbMsg.create({
473      cmd,
474      arg0,
475      arg1,
476      data,
477      useChecksum: this.useChecksum,
478    });
479
480    const msgHeader = msg.encodeHeader();
481    const msgData = msg.data;
482    assertTrue(
483      msgHeader.length <= this.maxPayload && msgData.length <= this.maxPayload,
484    );
485
486    const sendPromises = [
487      this.wrapUsb(
488        this.device.transferOut(this.usbWriteEpEndpoint, msgHeader.buffer),
489      ),
490    ];
491    if (msg.data.length > 0) {
492      sendPromises.push(
493        this.wrapUsb(
494          this.device.transferOut(this.usbWriteEpEndpoint, msgData.buffer),
495        ),
496      );
497    }
498    await Promise.all(sendPromises);
499  }
500
501  private wrapUsb<T>(promise: Promise<T>): Promise<T | undefined> {
502    return wrapRecordingError(promise, this.reachDisconnectState.bind(this));
503  }
504}
505
506// An AdbOverWebusbStream is instantiated after the creation of a socket to the
507// device. Thanks to this, we can send commands and receive their output.
508// Messages are received in the main adb class, and are forwarded to an instance
509// of this class based on a stream id match.
510export class AdbOverWebusbStream implements ByteStream {
511  private adbConnection: AdbConnectionOverWebusb;
512  private _isConnected: boolean;
513  private onStreamDataCallbacks: OnStreamDataCallback[] = [];
514  private onStreamCloseCallbacks: OnStreamCloseCallback[] = [];
515  localStreamId: number;
516  remoteStreamId = -1;
517
518  constructor(
519    adb: AdbConnectionOverWebusb,
520    localStreamId: number,
521    remoteStreamId: number,
522  ) {
523    this.adbConnection = adb;
524    this.localStreamId = localStreamId;
525    this.remoteStreamId = remoteStreamId;
526    // When the stream is created, the connection has been already established.
527    this._isConnected = true;
528  }
529
530  addOnStreamDataCallback(onStreamData: OnStreamDataCallback): void {
531    this.onStreamDataCallbacks.push(onStreamData);
532  }
533
534  addOnStreamCloseCallback(onStreamClose: OnStreamCloseCallback): void {
535    this.onStreamCloseCallbacks.push(onStreamClose);
536  }
537
538  // Used by the connection object to signal newly received data, not exposed
539  // in the interface.
540  signalStreamData(data: Uint8Array): void {
541    for (const onStreamData of this.onStreamDataCallbacks) {
542      onStreamData(data);
543    }
544  }
545
546  // Used by the connection object to signal the stream is closed, not exposed
547  // in the interface.
548  signalStreamClosed(): void {
549    for (const onStreamClose of this.onStreamCloseCallbacks) {
550      onStreamClose();
551    }
552    this.onStreamDataCallbacks = [];
553    this.onStreamCloseCallbacks = [];
554  }
555
556  close(): void {
557    this.closeAndWaitForTeardown();
558  }
559
560  async closeAndWaitForTeardown(): Promise<void> {
561    this._isConnected = false;
562    await this.adbConnection.streamClose(this);
563  }
564
565  write(msg: string | Uint8Array): void {
566    this.adbConnection.streamWrite(msg, this);
567  }
568
569  isConnected(): boolean {
570    return this._isConnected;
571  }
572}
573
574const ADB_MSG_SIZE = 6 * 4; // 6 * int32.
575
576class AdbMsg {
577  data: Uint8Array;
578  readonly cmd: CmdType;
579  readonly arg0: number;
580  readonly arg1: number;
581  readonly dataLen: number;
582  readonly dataChecksum: number;
583  readonly useChecksum: boolean;
584
585  constructor(
586    cmd: CmdType,
587    arg0: number,
588    arg1: number,
589    dataLen: number,
590    dataChecksum: number,
591    useChecksum = false,
592  ) {
593    assertTrue(cmd.length === 4);
594    this.cmd = cmd;
595    this.arg0 = arg0;
596    this.arg1 = arg1;
597    this.dataLen = dataLen;
598    this.data = new Uint8Array(dataLen);
599    this.dataChecksum = dataChecksum;
600    this.useChecksum = useChecksum;
601  }
602
603  static create({
604    cmd,
605    arg0,
606    arg1,
607    data,
608    useChecksum = true,
609  }: {
610    cmd: CmdType;
611    arg0: number;
612    arg1: number;
613    data?: Uint8Array | string;
614    useChecksum?: boolean;
615  }): AdbMsg {
616    const encodedData = this.encodeData(data);
617    const msg = new AdbMsg(cmd, arg0, arg1, encodedData.length, 0, useChecksum);
618    msg.data = encodedData;
619    return msg;
620  }
621
622  get dataStr() {
623    return textDecoder.decode(this.data);
624  }
625
626  toString() {
627    return `${this.cmd} [${this.arg0},${this.arg1}] ${this.dataStr}`;
628  }
629
630  // A brief description of the message can be found here:
631  // https://android.googlesource.com/platform/system/core/+/main/adb/protocol.txt
632  //
633  // struct amessage {
634  //     uint32_t command;    // command identifier constant
635  //     uint32_t arg0;       // first argument
636  //     uint32_t arg1;       // second argument
637  //     uint32_t data_length;// length of payload (0 is allowed)
638  //     uint32_t data_check; // checksum of data payload
639  //     uint32_t magic;      // command ^ 0xffffffff
640  // };
641  static decodeHeader(dv: DataView): AdbMsg {
642    assertTrue(dv.byteLength === ADB_MSG_SIZE);
643    const cmd = textDecoder.decode(dv.buffer.slice(0, 4)) as CmdType;
644    const cmdNum = dv.getUint32(0, true);
645    const arg0 = dv.getUint32(4, true);
646    const arg1 = dv.getUint32(8, true);
647    const dataLen = dv.getUint32(12, true);
648    const dataChecksum = dv.getUint32(16, true);
649    const cmdChecksum = dv.getUint32(20, true);
650    assertTrue(cmdNum === (cmdChecksum ^ 0xffffffff));
651    return new AdbMsg(cmd, arg0, arg1, dataLen, dataChecksum);
652  }
653
654  encodeHeader(): Uint8Array {
655    const buf = new Uint8Array(ADB_MSG_SIZE);
656    const dv = new DataView(buf.buffer);
657    const cmdBytes: Uint8Array = textEncoder.encode(this.cmd);
658    const rawMsg = AdbMsg.encodeData(this.data);
659    const checksum = this.useChecksum ? generateChecksum(rawMsg) : 0;
660    for (let i = 0; i < 4; i++) dv.setUint8(i, cmdBytes[i]);
661
662    dv.setUint32(4, this.arg0, true);
663    dv.setUint32(8, this.arg1, true);
664    dv.setUint32(12, rawMsg.byteLength, true);
665    dv.setUint32(16, checksum, true);
666    dv.setUint32(20, dv.getUint32(0, true) ^ 0xffffffff, true);
667
668    return buf;
669  }
670
671  static encodeData(data?: Uint8Array | string): Uint8Array {
672    if (data === undefined) return new Uint8Array([]);
673    if (isString(data)) return textEncoder.encode(data + '\0');
674    return data;
675  }
676}
677