From f89dcc7ba7c94973ce7cfbadaddf163739611d93 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Sun, 13 Dec 2020 22:00:37 -0800 Subject: [PATCH] feat(adb): implement push (#4697) --- android-types-internal.d.ts | 1 + src/client/android.ts | 10 ++++++++-- src/dispatchers/androidDispatcher.ts | 4 ++++ src/protocol/channels.ts | 10 ++++++++++ src/protocol/protocol.yml | 6 ++++++ src/protocol/validator.ts | 5 +++++ src/server/android/android.ts | 24 ++++++++++++++++++++++++ 7 files changed, 58 insertions(+), 2 deletions(-) diff --git a/android-types-internal.d.ts b/android-types-internal.d.ts index e835a5d364..7201b9eda4 100644 --- a/android-types-internal.d.ts +++ b/android-types-internal.d.ts @@ -30,6 +30,7 @@ export interface AndroidDevice exte shell(command: string): Promise; open(command: string): Promise; installApk(file: string | Buffer, options?: { args?: string[] }): Promise; + push(file: string | Buffer, path: string, options?: { mode?: number }): Promise; launchBrowser(options?: BrowserContextOptions & { packageName?: string }): Promise; close(): Promise; diff --git a/src/client/android.ts b/src/client/android.ts index 8f709fa4c9..3f68a75a4a 100644 --- a/src/client/android.ts +++ b/src/client/android.ts @@ -213,7 +213,13 @@ export class AndroidDevice extends ChannelOwner { return this._wrapApiCall('androidDevice.installApk', async () => { - await this._channel.installApk({ file: await readApkFile(file), args: options && options.args }); + await this._channel.installApk({ file: await loadFile(file), args: options && options.args }); + }); + } + + async push(file: string | Buffer, path: string, options?: { mode: number }): Promise { + return this._wrapApiCall('androidDevice.push', async () => { + await this._channel.push({ file: await loadFile(file), path, mode: options ? options.mode : undefined }); }); } @@ -261,7 +267,7 @@ export class AndroidSocket extends ChannelOwner { +async function loadFile(file: string | Buffer): Promise { if (isString(file)) return (await util.promisify(fs.readFile)(file)).toString('base64'); return file.toString('base64'); diff --git a/src/dispatchers/androidDispatcher.ts b/src/dispatchers/androidDispatcher.ts index 197345f9c1..bbd826de6c 100644 --- a/src/dispatchers/androidDispatcher.ts +++ b/src/dispatchers/androidDispatcher.ts @@ -149,6 +149,10 @@ export class AndroidDeviceDispatcher extends Dispatcher { const context = await this._object.launchBrowser(params.packageName, params); return { context: new BrowserContextDispatcher(this._scope, context) }; diff --git a/src/protocol/channels.ts b/src/protocol/channels.ts index ca6b387a24..49302dd627 100644 --- a/src/protocol/channels.ts +++ b/src/protocol/channels.ts @@ -2471,6 +2471,7 @@ export interface AndroidDeviceChannel extends Channel { open(params: AndroidDeviceOpenParams, metadata?: Metadata): Promise; shell(params: AndroidDeviceShellParams, metadata?: Metadata): Promise; installApk(params: AndroidDeviceInstallApkParams, metadata?: Metadata): Promise; + push(params: AndroidDevicePushParams, metadata?: Metadata): Promise; setDefaultTimeoutNoReply(params: AndroidDeviceSetDefaultTimeoutNoReplyParams, metadata?: Metadata): Promise; connectToWebView(params: AndroidDeviceConnectToWebViewParams, metadata?: Metadata): Promise; close(params?: AndroidDeviceCloseParams, metadata?: Metadata): Promise; @@ -2757,6 +2758,15 @@ export type AndroidDeviceInstallApkOptions = { args?: string[], }; export type AndroidDeviceInstallApkResult = void; +export type AndroidDevicePushParams = { + file: Binary, + path: string, + mode?: number, +}; +export type AndroidDevicePushOptions = { + mode?: number, +}; +export type AndroidDevicePushResult = void; export type AndroidDeviceSetDefaultTimeoutNoReplyParams = { timeout: number, }; diff --git a/src/protocol/protocol.yml b/src/protocol/protocol.yml index 98b4d4d220..37a8a1eae6 100644 --- a/src/protocol/protocol.yml +++ b/src/protocol/protocol.yml @@ -2312,6 +2312,12 @@ AndroidDevice: type: array? items: string + push: + parameters: + file: binary + path: string + mode: number? + setDefaultTimeoutNoReply: parameters: timeout: number diff --git a/src/protocol/validator.ts b/src/protocol/validator.ts index 16c8363dc5..696956ea73 100644 --- a/src/protocol/validator.ts +++ b/src/protocol/validator.ts @@ -1039,6 +1039,11 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { file: tBinary, args: tOptional(tArray(tString)), }); + scheme.AndroidDevicePushParams = tObject({ + file: tBinary, + path: tString, + mode: tOptional(tNumber), + }); scheme.AndroidDeviceSetDefaultTimeoutNoReplyParams = tObject({ timeout: tNumber, }); diff --git a/src/server/android/android.ts b/src/server/android/android.ts index ce7f8760c8..20bbc78ac5 100644 --- a/src/server/android/android.ts +++ b/src/server/android/android.ts @@ -287,6 +287,30 @@ export class AndroidDevice extends EventEmitter { debug('pw:android')('Written driver bytes: ' + success); } + async push(content: Buffer, path: string, mode = 0o644): Promise { + const socket = await this._backend.open(`sync:`); + const sendHeader = async (command: string, length: number) => { + const buffer = Buffer.alloc(command.length + 4); + buffer.write(command, 0); + buffer.writeUInt32LE(length, command.length); + await socket.write(buffer); + }; + const send = async (command: string, data: Buffer) => { + await sendHeader(command, data.length); + await socket.write(data); + }; + await send('SEND', Buffer.from(`${path},${mode}`)); + const maxChunk = 65535; + for (let i = 0; i < content.length; i += maxChunk) + await send('DATA', content.slice(i, i + maxChunk)); + await sendHeader('DONE', (Date.now() / 1000) | 0); + const result = await new Promise(f => socket.once('data', f)); + const code = result.slice(0, 4).toString(); + if (code !== 'OKAY') + throw new Error('Could not push: ' + code); + await socket.close(); + } + private async _refreshWebViews() { const sockets = (await this._backend.runCommand(`shell:cat /proc/net/unix | grep webview_devtools_remote`)).toString().split('\n'); if (this._isClosed)