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} from 'custom_utils'; 16 17import {defer, Deferred} from '../../base/deferred'; 18import {assertFalse} from '../../base/logging'; 19import {ArrayBufferBuilder} from '../array_buffer_builder'; 20 21import {RecordingError} from './recording_error_handling'; 22import {ByteStream} from './recording_interfaces_v2'; 23import { 24 BINARY_PUSH_FAILURE, 25 BINARY_PUSH_UNKNOWN_RESPONSE, 26} from './recording_utils'; 27 28// https://cs.android.com/android/platform/superproject/+/master:packages/ 29// modules/adb/file_sync_protocol.h;l=144 30const MAX_SYNC_SEND_CHUNK_SIZE = 64 * 1024; 31 32// Adb does not accurately send some file permissions. If you need a special set 33// of permissions, do not rely on this value. Rather, send a shell command which 34// explicitly sets permissions, such as: 35// 'shell:chmod ${permissions} ${path}' 36const FILE_PERMISSIONS = 2 ** 15 + 0o644; 37 38const textDecoder = new _TextDecoder(); 39 40// For details about the protocol, see: 41// https://cs.android.com/android/platform/superproject/+/master:packages/modules/adb/SYNC.TXT 42export class AdbFileHandler { 43 private sentByteCount = 0; 44 private isPushOngoing: boolean = false; 45 46 constructor(private byteStream: ByteStream) {} 47 48 async pushBinary(binary: Uint8Array, path: string): Promise<void> { 49 // For a given byteStream, we only support pushing one binary at a time. 50 assertFalse(this.isPushOngoing); 51 this.isPushOngoing = true; 52 const transferFinished = defer<void>(); 53 54 this.byteStream.addOnStreamDataCallback( 55 (data) => this.onStreamData(data, transferFinished)); 56 this.byteStream.addOnStreamCloseCallback(() => this.isPushOngoing = false); 57 58 const sendMessage = new ArrayBufferBuilder(); 59 // 'SEND' is the API method used to send a file to device. 60 sendMessage.append('SEND'); 61 // The remote file name is split into two parts separated by the last 62 // comma (","). The first part is the actual path, while the second is a 63 // decimal encoded file mode containing the permissions of the file on 64 // device. 65 sendMessage.append(path.length + 6); 66 sendMessage.append(path); 67 sendMessage.append(','); 68 sendMessage.append(FILE_PERMISSIONS.toString()); 69 this.byteStream.write(new Uint8Array(sendMessage.toArrayBuffer())); 70 71 while (!(await this.sendNextDataChunk(binary))) 72 ; 73 74 return transferFinished; 75 } 76 77 private onStreamData(data: Uint8Array, transferFinished: Deferred<void>) { 78 this.sentByteCount = 0; 79 const response = textDecoder.decode(data); 80 if (response.split('\n')[0].includes('FAIL')) { 81 // Sample failure response (when the file is transferred successfully 82 // but the date is not formatted correctly): 83 // 'OKAYFAIL\npath too long' 84 transferFinished.reject( 85 new RecordingError(`${BINARY_PUSH_FAILURE}: ${response}`)); 86 } else if (textDecoder.decode(data).substring(0, 4) === 'OKAY') { 87 // In case of success, the server responds to the last request with 88 // 'OKAY'. 89 transferFinished.resolve(); 90 } else { 91 throw new RecordingError(`${BINARY_PUSH_UNKNOWN_RESPONSE}: ${response}`); 92 } 93 } 94 95 private async sendNextDataChunk(binary: Uint8Array): Promise<boolean> { 96 const endPosition = Math.min( 97 this.sentByteCount + MAX_SYNC_SEND_CHUNK_SIZE, binary.byteLength); 98 const chunk = await binary.slice(this.sentByteCount, endPosition); 99 // The file is sent in chunks. Each chunk is prefixed with "DATA" and the 100 // chunk length. This is repeated until the entire file is transferred. Each 101 // chunk must not be larger than 64k. 102 const chunkLength = chunk.byteLength; 103 const dataMessage = new ArrayBufferBuilder(); 104 dataMessage.append('DATA'); 105 dataMessage.append(chunkLength); 106 dataMessage.append( 107 new Uint8Array(chunk.buffer, chunk.byteOffset, chunkLength)); 108 109 this.sentByteCount += chunkLength; 110 const isDone = this.sentByteCount === binary.byteLength; 111 112 if (isDone) { 113 // When the file is transferred a sync request "DONE" is sent, together 114 // with a timestamp, representing the last modified time for the file. The 115 // server responds to this last request. 116 dataMessage.append('DONE'); 117 // We send the date in seconds. 118 dataMessage.append(Math.floor(Date.now() / 1000)); 119 } 120 this.byteStream.write(new Uint8Array(dataMessage.toArrayBuffer())); 121 return isDone; 122 } 123} 124