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 '../../base/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/+/main: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/+/main: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((data) => 55 this.onStreamData(data, transferFinished), 56 ); 57 this.byteStream.addOnStreamCloseCallback( 58 () => (this.isPushOngoing = false), 59 ); 60 61 const sendMessage = new ArrayBufferBuilder(); 62 // 'SEND' is the API method used to send a file to device. 63 sendMessage.append('SEND'); 64 // The remote file name is split into two parts separated by the last 65 // comma (","). The first part is the actual path, while the second is a 66 // decimal encoded file mode containing the permissions of the file on 67 // device. 68 sendMessage.append(path.length + 6); 69 sendMessage.append(path); 70 sendMessage.append(','); 71 sendMessage.append(FILE_PERMISSIONS.toString()); 72 this.byteStream.write(new Uint8Array(sendMessage.toArrayBuffer())); 73 74 while (!(await this.sendNextDataChunk(binary))); 75 76 return transferFinished; 77 } 78 79 private onStreamData(data: Uint8Array, transferFinished: Deferred<void>) { 80 this.sentByteCount = 0; 81 const response = textDecoder.decode(data); 82 if (response.split('\n')[0].includes('FAIL')) { 83 // Sample failure response (when the file is transferred successfully 84 // but the date is not formatted correctly): 85 // 'OKAYFAIL\npath too long' 86 transferFinished.reject( 87 new RecordingError(`${BINARY_PUSH_FAILURE}: ${response}`), 88 ); 89 } else if (textDecoder.decode(data).substring(0, 4) === 'OKAY') { 90 // In case of success, the server responds to the last request with 91 // 'OKAY'. 92 transferFinished.resolve(); 93 } else { 94 throw new RecordingError(`${BINARY_PUSH_UNKNOWN_RESPONSE}: ${response}`); 95 } 96 } 97 98 private async sendNextDataChunk(binary: Uint8Array): Promise<boolean> { 99 const endPosition = Math.min( 100 this.sentByteCount + MAX_SYNC_SEND_CHUNK_SIZE, 101 binary.byteLength, 102 ); 103 const chunk = await binary.slice(this.sentByteCount, endPosition); 104 // The file is sent in chunks. Each chunk is prefixed with "DATA" and the 105 // chunk length. This is repeated until the entire file is transferred. Each 106 // chunk must not be larger than 64k. 107 const chunkLength = chunk.byteLength; 108 const dataMessage = new ArrayBufferBuilder(); 109 dataMessage.append('DATA'); 110 dataMessage.append(chunkLength); 111 dataMessage.append( 112 new Uint8Array(chunk.buffer, chunk.byteOffset, chunkLength), 113 ); 114 115 this.sentByteCount += chunkLength; 116 const isDone = this.sentByteCount === binary.byteLength; 117 118 if (isDone) { 119 // When the file is transferred a sync request "DONE" is sent, together 120 // with a timestamp, representing the last modified time for the file. The 121 // server responds to this last request. 122 dataMessage.append('DONE'); 123 // We send the date in seconds. 124 dataMessage.append(Math.floor(Date.now() / 1000)); 125 } 126 this.byteStream.write(new Uint8Array(dataMessage.toArrayBuffer())); 127 return isDone; 128 } 129} 130