diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e2304801..cc6825f0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: - name: Install Node, NPM and Yarn uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 18 registry-url: 'https://registry.npmjs.org' - name: Install modules @@ -37,7 +37,9 @@ jobs: yarn test - name: Git config - run: git config --global user.email "bot@google.com" && git config --global user.name "bot" + run: | + git config --global user.email "bot@google.com" && git config --global user.name "bot" + password=${{ secrets.GITHUB_TOKEN }} - name: Apply version major if: contains(github.event.pull_request.labels.*.name, 'major') diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0abcc616..db534344 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,8 +1,8 @@ name: Build and test on: push jobs: - test: - name: Test and lint + build: + name: Build and lint runs-on: ubuntu-latest steps: - name: Checkout repo @@ -11,7 +11,7 @@ jobs: - name: Install Node, NPM and Yarn uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 18 - name: Install modules run: | @@ -24,10 +24,39 @@ jobs: - name: Lint run: | yarn lint - - - name: Test - run: | - yarn test - name: Test generate docs run: | yarn docs + - name: Upload lib Artifacts + uses: actions/upload-artifact@v2 + with: + name: lib + path: ./lib + test-versions: + needs: build + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [16, 17, 18, 19, 20, 21] + steps: + - uses: actions/checkout@v2 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + + - name: Setup Yarn + run: | + npm install -g yarn + - name: Install packages + run: | + yarn install --frozen-lockfile --ignore-engines + + - name: Download lib Artifacts + uses: actions/download-artifact@v2 + with: + name: lib + path: ./lib + - name: Test + run: yarn test diff --git a/.gitignore b/.gitignore index 59a75527..180ceb14 100644 --- a/.gitignore +++ b/.gitignore @@ -102,8 +102,10 @@ dist # TernJS port file .tern-port +.DS_Store + +# Custom lib -script.ts +script.* *.apk docs/ -.DS_Store diff --git a/README_DOCS.md b/README_DOCS.md index dc36af44..fd0838e4 100644 --- a/README_DOCS.md +++ b/README_DOCS.md @@ -83,3 +83,9 @@ adb.listDevices((devices) => { console.log(devices); }); ``` + +## Changes from V5 + +- `time` option in `touch` method is converted to UTC time. +- Tracker `change` event emits the same instance of the device instead of creating a new device object every time. +- `install` and `uninstall` commands will fail if any other response than `Success` is received. Until V5 the promise could have resolved even when the operation was not successful. diff --git a/__tests__/adbClient/connect.ts b/__tests__/adbClient/connect.ts index 30832362..f6014914 100644 --- a/__tests__/adbClient/connect.ts +++ b/__tests__/adbClient/connect.ts @@ -1,6 +1,5 @@ import { AdbMock } from '../../mockery/mockAdbServer'; import { Client } from '../../lib/client'; -import { promisify } from 'util'; describe('Connect', () => { it('Connect to default port', async () => { @@ -72,20 +71,4 @@ describe('Connect', () => { await adbMock.end(); } }); - - it('Connect callback overload', async () => { - const adbMock = new AdbMock([ - { cmd: 'host:connect:127.0.0.1:4444', res: 'connected to' } - ]); - try { - const port = await adbMock.start(); - const adb = new Client({ noAutoStart: true, port }); - const result = await promisify((cb) => { - adb.connect('127.0.0.1', 4444, cb); - })(); - expect(result).toBe('127.0.0.1:4444'); - } finally { - await adbMock.end(); - } - }); }); diff --git a/__tests__/adbClient/constructorAndStartServer.ts b/__tests__/adbClient/constructorAndStartServer.ts index d8063438..d50e71e3 100644 --- a/__tests__/adbClient/constructorAndStartServer.ts +++ b/__tests__/adbClient/constructorAndStartServer.ts @@ -6,7 +6,7 @@ describe('Client constructor tests', () => { const client = new Client(); expect(client['options']).toEqual({ port: 5037, - host: 'localhost', + host: '127.0.0.1', bin: 'adb', noAutoStart: false }); @@ -16,7 +16,7 @@ describe('Client constructor tests', () => { const client = new Client({ bin: undefined, port: 5036 }); expect(client['options']).toEqual({ port: 5036, - host: 'localhost', + host: '127.0.0.1', bin: 'adb', noAutoStart: false }); @@ -29,30 +29,4 @@ describe('Start server tests', () => { const client = new Client(); await expect(client.startServer()).resolves.toBeUndefined(); }); - - it('Start adb server error', async () => { - try { - mockExec(new Error('message')); - const client = new Client(); - await client.startServer(); - } catch (e: unknown) { - expect(e).toEqual(new Error('message')); - } - }); - - it('Start adb server callback overload', () => { - mockExec(null); - const client = new Client(); - client.startServer((err) => { - expect(err).toBeNull(); - }); - }); - - it('Start adb server callback overload error', () => { - mockExec(new Error('message')); - const client = new Client(); - client.startServer((err) => { - expect(err).toBeInstanceOf(Error); - }); - }); }); diff --git a/__tests__/adbClient/cp.ts b/__tests__/adbClient/cp.ts index 793ced15..80cc78fe 100644 --- a/__tests__/adbClient/cp.ts +++ b/__tests__/adbClient/cp.ts @@ -2,7 +2,6 @@ import crypto from 'crypto'; import { UnexpectedDataError } from '../../lib/util'; import { Client } from '../../lib/client'; import { AdbMock } from '../../mockery/mockAdbServer'; -import { promisify } from 'util'; beforeAll(() => { jest.spyOn(crypto, 'randomUUID').mockImplementation(() => { @@ -30,27 +29,6 @@ describe('Cp OKAY tests', () => { } }); - it('Should execute callback overload without options', async () => { - const adbMock = new AdbMock([ - { cmd: 'host:transport:serial', res: null, rawRes: true }, - { - cmd: `shell:(cp /file /other) || echo '1-2-3-4-5'`, - res: null, - rawRes: true - } - ]); - try { - const port = await adbMock.start(); - const adb = new Client({ noAutoStart: true, port }); - const result = await promisify((cb) => - adb.cp('serial', '/file', '/other', cb) - )(); - expect(result).toBeUndefined(); - } finally { - await adbMock.end(); - } - }); - it('Should execute with simple parameters without archive', async () => { const adbMock = new AdbMock([ { cmd: 'host:transport:serial', res: null, rawRes: true }, @@ -172,42 +150,6 @@ describe('Cp OKAY tests', () => { await adbMock.end(); } }); - - it('Should execute callback overload with preserve option with all', async () => { - const adbMock = new AdbMock([ - { cmd: 'host:transport:serial', res: null, rawRes: true }, - { - cmd: `shell:(cp --preserve=a /file /other) || echo '1-2-3-4-5'`, - res: 'data', - rawRes: true - } - ]); - try { - const port = await adbMock.start(); - const adb = new Client({ noAutoStart: true, port }); - const result = await promisify((cb) => - adb.cp( - 'serial', - '/file', - '/other', - { - preserve: { - all: true, - mode: true, - ownership: true, - timestamps: true, - context: true, - xattr: true - } - }, - cb - ) - )(); - expect(result).toBeUndefined(); - } finally { - await adbMock.end(); - } - }); }); describe('Cp FAIL tests', () => { diff --git a/__tests__/adbClient/disconnect.ts b/__tests__/adbClient/disconnect.ts index b80ce705..80737aa3 100644 --- a/__tests__/adbClient/disconnect.ts +++ b/__tests__/adbClient/disconnect.ts @@ -1,6 +1,5 @@ import { AdbMock } from '../../mockery/mockAdbServer'; import { Client } from '../../lib/client'; -import { promisify } from 'util'; describe('Disconnect', () => { it('Disconnect from default port', async () => { @@ -91,23 +90,4 @@ describe('Disconnect', () => { await adbMock.end(); } }); - - it('Connect callback overload', async () => { - const adbMock = new AdbMock([ - { - cmd: 'host:disconnect:127.0.0.1:4444', - res: 'disconnected' - } - ]); - try { - const port = await adbMock.start(); - const adb = new Client({ noAutoStart: true, port }); - const result = await promisify((cb) => { - adb.disconnect('127.0.0.1', 4444, cb); - })(); - expect(result).toBe('127.0.0.1:4444'); - } finally { - await adbMock.end(); - } - }); }); diff --git a/__tests__/adbClient/install.ts b/__tests__/adbClient/install.ts index 4f62c5af..23764ecc 100644 --- a/__tests__/adbClient/install.ts +++ b/__tests__/adbClient/install.ts @@ -2,7 +2,6 @@ import { AdbMockMulti } from '../../mockery/mockAdbServer'; import { Client } from '../../lib/client'; import { UnexpectedDataError } from '../../lib/util'; import { Readable } from 'stream'; -import { promisify } from 'util'; describe('Install OKAY tests', () => { it('Should install with Success response', async () => { @@ -117,165 +116,18 @@ describe('Install OKAY tests', () => { const adb = new Client({ noAutoStart: true, port }); await expect( - promisify((cb) => - adb.install( - 'serial', - Readable.from(Buffer.from([1, 0, 0, 0, 0, 0, 0])), - { - reinstall: true, - test: true, - internal: true, - allowDowngrade: true, - grandPermissions: true - }, - 'args', - cb - ) - )() - ).resolves.toBeUndefined(); - } finally { - adbMock.end(); - } - }); - - it('Should install with callback overload', async () => { - const buff = Buffer.from([4, 0, 0, 0]); - const adbMock = new AdbMockMulti([ - { cmd: 'host:transport:serial', res: null, rawRes: true }, - { - cmd: 'sync:', - res: 'OKAY' + buff.toString(), - rawRes: true, - end: true - }, - { cmd: 'host:transport:serial', res: null, rawRes: true }, - { - cmd: `shell:pm install "/data/local/tmp/_stream.apk"`, - res: 'Success\n', - rawRes: true, - end: true - }, - { cmd: 'host:transport:serial', res: null, rawRes: true }, - { - cmd: `shell:rm -f '/data/local/tmp/_stream.apk'`, - res: '123', - rawRes: true, - end: true - } - ]); - try { - const port = await adbMock.start(); - const adb = new Client({ noAutoStart: true, port }); - - await expect( - promisify((cb) => - adb.install( - 'serial', - Readable.from(Buffer.from([1, 0, 0, 0, 0, 0, 0])), - cb - ) - )() - ).resolves.toBeUndefined(); - } finally { - adbMock.end(); - } - }); - - it('Should install with callback and options', async () => { - const buff = Buffer.from([4, 0, 0, 0]); - const adbMock = new AdbMockMulti([ - { cmd: 'host:transport:serial', res: null, rawRes: true }, - { - cmd: 'sync:', - res: 'OKAY' + buff.toString(), - rawRes: true, - end: true - }, - { cmd: 'host:transport:serial', res: null, rawRes: true }, - { - cmd: `shell:pm install -r -t -f -d -g "/data/local/tmp/_stream.apk"`, - res: 'Success\n', - rawRes: true, - end: true - }, - { cmd: 'host:transport:serial', res: null, rawRes: true }, - { - cmd: `shell:rm -f '/data/local/tmp/_stream.apk'`, - res: '123', - rawRes: true, - end: true - } - ]); - try { - const port = await adbMock.start(); - const adb = new Client({ noAutoStart: true, port }); - - await expect( - promisify((cb) => - adb.install( - 'serial', - Readable.from(Buffer.from([1, 0, 0, 0, 0, 0, 0])), - { - reinstall: true, - test: true, - internal: true, - allowDowngrade: true, - grandPermissions: true - }, - cb - ) - )() - ).resolves.toBeUndefined(); - } finally { - adbMock.end(); - } - }); - - it('Should install with callback, options and args', async () => { - const buff = Buffer.from([4, 0, 0, 0]); - const adbMock = new AdbMockMulti([ - { cmd: 'host:transport:serial', res: null, rawRes: true }, - { - cmd: 'sync:', - res: 'OKAY' + buff.toString(), - rawRes: true, - end: true - }, - { cmd: 'host:transport:serial', res: null, rawRes: true }, - { - cmd: `shell:pm install -r -t -f -d -g "/data/local/tmp/_stream.apk" args`, - res: 'Success\n', - rawRes: true, - end: true - }, - { cmd: 'host:transport:serial', res: null, rawRes: true }, - { - cmd: `shell:rm -f '/data/local/tmp/_stream.apk'`, - res: '123', - rawRes: true, - end: true - } - ]); - try { - const port = await adbMock.start(); - const adb = new Client({ noAutoStart: true, port }); - - await expect( - promisify((cb) => - adb.install( - 'serial', - Readable.from(Buffer.from([1, 0, 0, 0, 0, 0, 0])), - { - reinstall: true, - test: true, - internal: true, - allowDowngrade: true, - grandPermissions: true - }, - 'args', - cb - ) - )() + adb.install( + 'serial', + Readable.from(Buffer.from([1, 0, 0, 0, 0, 0, 0])), + { + reinstall: true, + test: true, + internal: true, + allowDowngrade: true, + grandPermissions: true + }, + 'args' + ) ).resolves.toBeUndefined(); } finally { adbMock.end(); diff --git a/__tests__/adbClient/listDevices.ts b/__tests__/adbClient/listDevices.ts index 536a9000..9eb47462 100644 --- a/__tests__/adbClient/listDevices.ts +++ b/__tests__/adbClient/listDevices.ts @@ -143,4 +143,48 @@ describe('List devices', () => { await adbMock.end(); } }); + + it('Should throw unexpected error for missing state or id', async () => { + const adbMock = new AdbMock([ + { + cmd: 'host:devices-l', + res: 'usb:337641472X' + } + ]); + + try { + const port = await adbMock.start(); + const adb = new Client({ port, noAutoStart: true }); + await expect(() => adb.listDevices()).rejects.toEqual( + new UnexpectedDataError( + 'usb:337641472X', + ' :' + ) + ); + } finally { + await adbMock.end(); + } + }); + + it('Should throw unexpected error for transport_id', async () => { + const adbMock = new AdbMock([ + { + cmd: 'host:devices-l', + res: 'b137f5dc unauthorized usb:337641472X' + } + ]); + + try { + const port = await adbMock.start(); + const adb = new Client({ port, noAutoStart: true }); + await expect(() => adb.listDevices()).rejects.toEqual( + new UnexpectedDataError( + 'b137f5dc unauthorized usb:337641472X', + ' :' + ) + ); + } finally { + await adbMock.end(); + } + }); }); diff --git a/__tests__/adbClient/mv.ts b/__tests__/adbClient/mv.ts index 9a8ddadb..8f5efe98 100644 --- a/__tests__/adbClient/mv.ts +++ b/__tests__/adbClient/mv.ts @@ -2,7 +2,6 @@ import crypto from 'crypto'; import { UnexpectedDataError } from '../../lib/util'; import { Client } from '../../lib/client'; import { AdbMock } from '../../mockery/mockAdbServer'; -import { promisify } from 'util'; beforeAll(() => { jest.spyOn(crypto, 'randomUUID').mockImplementation(() => { @@ -30,27 +29,6 @@ describe('Mv OKAY tests', () => { } }); - it('Should execute callback overload without options', async () => { - const adbMock = new AdbMock([ - { cmd: 'host:transport:serial', res: null, rawRes: true }, - { - cmd: `shell:(mv /file /other) || echo '1-2-3-4-5'`, - res: 'data', - rawRes: true - } - ]); - try { - const port = await adbMock.start(); - const adb = new Client({ noAutoStart: true, port }); - const result = await promisify((cb) => - adb.mv('serial', '/file', '/other', cb) - )(); - expect(result).toBeUndefined(); - } finally { - await adbMock.end(); - } - }); - it('Should execute with options', async () => { const adbMock = new AdbMock([ { cmd: 'host:transport:serial', res: null, rawRes: true }, @@ -72,36 +50,6 @@ describe('Mv OKAY tests', () => { await adbMock.end(); } }); - - it('Should execute callback overload with parameters', async () => { - const adbMock = new AdbMock([ - { cmd: 'host:transport:serial', res: null, rawRes: true }, - { - cmd: `shell:(mv -f -n /file /other) || echo '1-2-3-4-5'`, - res: 'data', - rawRes: true - } - ]); - try { - const port = await adbMock.start(); - const adb = new Client({ noAutoStart: true, port }); - const result = await promisify((cb) => - adb.mv( - 'serial', - '/file', - '/other', - { - force: true, - noClobber: true - }, - cb - ) - )(); - expect(result).toBeUndefined(); - } finally { - await adbMock.end(); - } - }); }); describe('Mv FAIL tests', () => { diff --git a/__tests__/adbClient/rm.ts b/__tests__/adbClient/rm.ts index e809214c..1f1ca156 100644 --- a/__tests__/adbClient/rm.ts +++ b/__tests__/adbClient/rm.ts @@ -2,7 +2,6 @@ import crypto from 'crypto'; import { UnexpectedDataError } from '../../lib/util'; import { Client } from '../../lib/client'; import { AdbMock } from '../../mockery/mockAdbServer'; -import { promisify } from 'util'; beforeAll(() => { jest.spyOn(crypto, 'randomUUID').mockImplementation(() => { @@ -32,57 +31,6 @@ describe('Rm OKAY tests', () => { await adbMock.end(); } }); - - it('Should execute callback overload without options', async () => { - const adbMock = new AdbMock([ - { cmd: 'host:transport:serial', res: null, rawRes: true }, - { - cmd: `shell:(rm -f -rR /file) || echo '1-2-3-4-5'`, - res: null, - rawRes: true - } - ]); - try { - const port = await adbMock.start(); - const adb = new Client({ noAutoStart: true, port }); - const result = await promisify((cb) => - adb.rm( - 'serial', - '/file', - { - force: true, - recursive: true - }, - cb - ) - )(); - expect(result).toBeUndefined(); - } finally { - await adbMock.end(); - } - }); - - it('Should execute callback overload with options', async () => { - const adbMock = new AdbMock([ - { cmd: 'host:transport:serial', res: null, rawRes: true }, - { - cmd: `shell:(rm -f -rR /file) || echo '1-2-3-4-5'`, - res: 'data', - rawRes: true - } - ]); - try { - const port = await adbMock.start(); - const adb = new Client({ noAutoStart: true, port }); - const result = await adb.rm('serial', '/file', { - force: true, - recursive: true - }); - expect(result).toBeUndefined(); - } finally { - await adbMock.end(); - } - }); }); describe('Rm FAIL tests', () => { diff --git a/__tests__/adbClient/touch.ts b/__tests__/adbClient/touch.ts index 0341005a..3be4275a 100644 --- a/__tests__/adbClient/touch.ts +++ b/__tests__/adbClient/touch.ts @@ -1,5 +1,4 @@ import crypto from 'crypto'; -import moment from 'moment'; import { UnexpectedDataError } from '../../lib/util'; import { Client } from '../../lib/client'; import { AdbMock } from '../../mockery/mockAdbServer'; @@ -9,8 +8,18 @@ beforeAll(() => { return '1-2-3-4-5'; }); }); +function formatDate(date: Date): string { + const year = date.getUTCFullYear(); + const month = ('0' + (date.getUTCMonth() + 1)).slice(-2); + const day = ('0' + date.getUTCDate()).slice(-2); + const hours = ('0' + date.getUTCHours()).slice(-2); + const minutes = ('0' + date.getUTCMinutes()).slice(-2); + const seconds = ('0' + date.getUTCSeconds()).slice(-2); + const milliseconds = ('00' + date.getUTCMilliseconds()).slice(-3); + return `${year}${month}${day}${hours}${minutes}.${seconds}${milliseconds}`; +} const date = new Date('2022-12-13T12:41:42.418Z'); -const formattedTime = moment(date).format('YYYYMMDDHHmm[.]ssSSS'); +const formattedTime = formatDate(date); describe('Touch OKAY tests', () => { it('Should execute without parameters', async () => { const adbMock = new AdbMock([ diff --git a/__tests__/adbClient/trackDevices.ts b/__tests__/adbClient/trackDevices.ts index 713c8970..c77fd7df 100644 --- a/__tests__/adbClient/trackDevices.ts +++ b/__tests__/adbClient/trackDevices.ts @@ -79,6 +79,43 @@ describe('Track devices', () => { } }); + it('Devices emit add only once for the same device', async () => { + const adbMock = new AdbMock({ + cmd: 'host:track-devices-l', + res: null + }); + try { + const port = await adbMock.start(); + const adb = new Client({ noAutoStart: true, port }); + const tracker = await adb.trackDevices(); + let addCount = 0; + const result = await promisify((cb) => { + tracker.on('add', () => { + addCount++; + }); + tracker.on('error', (err) => { + cb(err, null); + }); + adbMock.forceWriteData( + 'b137f5dc unauthorized usb:337641472X transport_id:1\n' + ); + adbMock.forceWriteData( + 'b137f5dc unauthorized usb:337641472X transport_id:1\n' + ); + setTimeout(() => { + cb(null, addCount); + }, 1000); + })(); + try { + expect(result).toBe(1); + } finally { + tracker.end(); + } + } finally { + await adbMock.end(); + } + }); + it('Remove', async () => { const adbMock = new AdbMock({ cmd: 'host:track-devices-l', @@ -100,6 +137,9 @@ describe('Track devices', () => { adbMock.forceWrite(''); })(); try { + expect( + Object.prototype.hasOwnProperty.call(result, 'client') + ).toBe(false); expect(result).toEqual({ id: 'b137f5dc', state: 'unauthorized', @@ -141,6 +181,7 @@ describe('Track devices', () => { ); })(); try { + expect(result === tracker.Devices[0]).toBe(true); expect(result).toEqual( new Device(adb, { id: 'b137f5dc', @@ -164,7 +205,7 @@ describe('Track devices', () => { it('Error', async () => { const adbMock = new AdbMock({ cmd: 'host:track-devices-l', - res: 'b137f5dc unauthorized usb337641472X transport_id:1' + res: 'b137f5dc unauthorized usb:337641472X transport_id1' }); try { @@ -189,7 +230,7 @@ describe('Track devices', () => { it('End after error', async () => { const adbMock = new AdbMock({ cmd: 'host:track-devices-l', - res: 'b137f5dc unauthorized usb337641472X transport_id:1' + res: 'b137f5dc unauthorized usb:337641472X transport_id1' }); try { @@ -197,7 +238,7 @@ describe('Track devices', () => { const adb = new Client({ noAutoStart: true, port }); const tracker = await adb.trackDevices(); const result = await promisify((cb) => { - tracker.on('error', () => null); + tracker.on('error', () => {}); tracker.on('end', () => { cb(null, undefined); }); diff --git a/__tests__/adbClient/uninstall.ts b/__tests__/adbClient/uninstall.ts index 4b6aa0c7..3154264f 100644 --- a/__tests__/adbClient/uninstall.ts +++ b/__tests__/adbClient/uninstall.ts @@ -51,7 +51,7 @@ describe('Uninstall', () => { } }); - it('OKAY with Failure response', async () => { + it('Should throw error on Failure response', async () => { const adbMock = new AdbMock([ { cmd: 'host:transport:serial', @@ -60,15 +60,18 @@ describe('Uninstall', () => { }, { cmd: `shell:pm uninstall com.package`, - res: 'Failure\n', + res: 'Failure [CODE]\n', rawRes: true } ]); try { const port = await adbMock.start(); const adb = new Client({ noAutoStart: true, port }); - const result = await adb.uninstall('serial', 'com.package'); - expect(result).toBeUndefined(); + await expect( + adb.uninstall('serial', 'com.package') + ).rejects.toThrow( + new Error('com.package could not be uninstalled [CODE]') + ); } finally { await adbMock.end(); } @@ -90,8 +93,14 @@ describe('Uninstall', () => { try { const port = await adbMock.start(); const adb = new Client({ noAutoStart: true, port }); - const result = await adb.uninstall('serial', 'com.package'); - expect(result).toBeUndefined(); + await expect( + adb.uninstall('serial', 'com.package') + ).rejects.toThrow( + new UnexpectedDataError( + 'Unknown package:', + /^(Success|Failure \[(.*?)\])$/.toString() + ) + ); } finally { await adbMock.end(); } diff --git a/__tests__/constructDevice.ts b/__tests__/constructDevice.ts index c5359d84..11a63afb 100644 --- a/__tests__/constructDevice.ts +++ b/__tests__/constructDevice.ts @@ -2,15 +2,17 @@ import { constructDevice } from '../lib/commands/abstract/devices'; describe('Construct device tests', () => { it('Should construct usb device', () => { - const device = constructDevice([ - 'b137f5dc', - 'device', - 'usb:337641472X', - 'product:FP4eea', - 'model:FP4', - 'device:FP4', - 'transport_id:1' - ]); + const device = constructDevice( + [ + 'b137f5dc', + 'device', + 'usb:337641472X', + 'product:FP4eea', + 'model:FP4', + 'device:FP4', + 'transport_id:1' + ].join(' ') + ); expect(device).toEqual({ device: 'FP4', id: 'b137f5dc', @@ -24,15 +26,17 @@ describe('Construct device tests', () => { }); it('Should construct local ipv4 device with port', () => { - const device = constructDevice([ - '192.168.0.5:5555', - 'device', - 'usb:337641472X', - 'product:FP4eea', - 'model:FP4', - 'device:FP4', - 'transport_id:1' - ]); + const device = constructDevice( + [ + '192.168.0.5:5555', + 'device', + 'usb:337641472X', + 'product:FP4eea', + 'model:FP4', + 'device:FP4', + 'transport_id:1' + ].join(' ') + ); expect(device).toEqual({ device: 'FP4', id: '192.168.0.5:5555', diff --git a/__tests__/device/uninstall.ts b/__tests__/device/uninstall.ts index 5bde27ad..2feda604 100644 --- a/__tests__/device/uninstall.ts +++ b/__tests__/device/uninstall.ts @@ -47,27 +47,4 @@ describe('Uninstall tests', () => { await adbMock.end(); } }); - - it('Should uninstall package after getting Failure response', async () => { - const adbMock = new AdbMock([ - { - cmd: 'host:transport:serial', - res: null, - rawRes: true - }, - { - cmd: `shell:pm uninstall com.package`, - res: 'Failure\n', - rawRes: true - } - ]); - try { - const port = await adbMock.start(); - - const result = await getDevice(port).uninstall('com.package'); - expect(result).toBeUndefined(); - } finally { - await adbMock.end(); - } - }); }); diff --git a/__tests__/util.ts b/__tests__/util.ts index e760464d..35a631d4 100644 --- a/__tests__/util.ts +++ b/__tests__/util.ts @@ -1,18 +1,15 @@ +import { EventEmitter } from 'events'; import { decodeLength, encodeLength, encodeData, stringToType, - nodeify, - parseCbParam, - parseValueParam, parsePrimitiveParam, findMatches, - buildInputParams, PropertyValue, escape, escapeCompat, - buildFsParams + autoUnregister } from '../lib/util'; describe('Encode/decode length', () => { @@ -107,92 +104,6 @@ describe('String to type', () => { }); }); -describe('Nodeify', () => { - it('Resolve Promise', async () => { - const result = await nodeify(Promise.resolve(null), undefined); - expect(result).toBeNull(); - }); - - it('Reject Promise', async () => { - try { - await nodeify(Promise.reject(new Error('message')), undefined); - } catch (e: unknown) { - expect(e).toEqual(new Error('message')); - } - }); - - it('Resolve Callback', () => { - const result = nodeify(Promise.resolve(null), (err, value) => { - expect(err).toBeNull(); - expect(value).toBeNull(); - }); - expect(result).toBeUndefined(); - }); - - it('Reject Callback', () => { - const result = nodeify( - Promise.reject(new Error('message')), - (err, value) => { - expect(err?.message).toBe('message'); - expect(value).toBeUndefined(); - } - ); - expect(result).toBeUndefined(); - }); -}); - -describe('Parse value param', () => { - it('undefined', () => { - const result = parseValueParam(undefined); - expect(result).toBeUndefined(); - }); - - it('function', () => { - const result = parseValueParam(() => null); - expect(result).toBeUndefined(); - }); - - it('object', () => { - const result = parseValueParam({ one: 1 }); - expect(result).toEqual({ one: 1 }); - }); -}); - -describe('Parse cb params', () => { - it('undefined/undefined', () => { - const result = parseCbParam(undefined, undefined); - expect(result).toBeUndefined(); - }); - - it('function/undefined', () => { - const result = parseCbParam(() => null, undefined); - expect(typeof result).toBe('function'); - }); - - it('object/undefined', () => { - const result = parseCbParam({ one: 1 }, undefined); - expect(result).toBeUndefined(); - }); - - it('object/function', () => { - const result = parseCbParam({ one: 1 }, () => null); - expect(typeof result).toBe('function'); - }); - - it('undefined/function', () => { - const result = parseCbParam(undefined, () => null); - expect(typeof result).toBe('function'); - }); - - it('function/function', () => { - const result = parseCbParam( - () => 1, - () => 2 - ); - expect(result?.(null, null)).toBe(1); - }); -}); - describe('Parse primitive type', () => { it('undefined', () => { const result = parsePrimitiveParam(1, undefined); @@ -205,26 +116,6 @@ describe('Parse primitive type', () => { }); }); -describe('Build fs params', () => { - it('Options is function, cb is undefined', () => { - const { options_, cb_ } = buildFsParams(() => undefined, undefined); - expect(options_).toBeUndefined(); - expect(typeof cb_).toBe('function'); - }); - - it('Options is object, cb is undefined', () => { - const { options_, cb_ } = buildFsParams({}, undefined); - expect(options_).toEqual({}); - expect(cb_).toBeUndefined(); - }); - - it('Options is object, cb is function', () => { - const { options_, cb_ } = buildFsParams({}, () => undefined); - expect(options_).toEqual({}); - expect(typeof cb_).toBe('function'); - }); -}); - describe('Find matches', () => { it('Return array', () => { const result = findMatches( @@ -311,35 +202,6 @@ describe('Find matches', () => { }); }); -describe('Build input params', () => { - it('Params is function, cb is undefined', () => { - const { params, cb_ } = buildInputParams(() => undefined, undefined); - expect(params).toBeUndefined(); - expect(typeof cb_).toBe('function'); - }); - - it('Params is InputOptions, cb is function', () => { - const { params, cb_ } = buildInputParams( - { source: 'dpad' }, - () => undefined - ); - expect(params).toEqual({ source: 'dpad' }); - expect(typeof cb_).toBe('function'); - }); - - it('Params is InputSource, cb is function', () => { - const { params, cb_ } = buildInputParams('dpad', () => undefined); - expect(params).toEqual('dpad'); - expect(typeof cb_).toBe('function'); - }); - - it('Params is InputSource, cb is undefined', () => { - const { params, cb_ } = buildInputParams('dpad', undefined); - expect(params).toEqual('dpad'); - expect(typeof cb_).toBe('undefined'); - }); -}); - describe('Escape tests', () => { it('escape undefined', () => { const result = escape(undefined); @@ -385,3 +247,39 @@ describe('Escape compat tests', () => { expect(result).toBe(`${true}`); }); }); + +describe('Auto unregister', () => { + it('Unregister after success', async () => { + const emitter = new EventEmitter(); + const listener1 = jest.fn(); + emitter.on('test', listener1); + await autoUnregister(emitter, async (emitter) => { + const listener2 = jest.fn(); + emitter.on('test', listener2); + }); + const listeners = emitter + .eventNames() + .flatMap((event) => emitter.listeners(event)); + expect(listeners).toEqual([listener1]); + }); + + it('Unregister after error', async () => { + const emitter = new EventEmitter(); + const listener1 = jest.fn(); + emitter.on('test', listener1); + try { + await autoUnregister(emitter, async (emitter) => { + const listener2 = jest.fn(); + emitter.on('test', listener2); + throw new Error('Test'); + }); + } catch { + /* empty */ + } finally { + const listeners = emitter + .eventNames() + .flatMap((event) => emitter.listeners(event)); + expect(listeners).toEqual([listener1]); + } + }); +}); diff --git a/package.json b/package.json index 6d9c0a3e..da13d055 100644 --- a/package.json +++ b/package.json @@ -25,30 +25,29 @@ "url": "https://github.com/Maaaartin/adb-ts.git" }, "engines": { - "node": ">=16.0.0" + "node": ">=16" }, "main": "lib/index.js", "types": "lib/index.d.ts", "private": false, "scripts": { "test": "npx jest", - "build": "npx tsc", - "watch": "rm -rf lib && npx tsc -w", + "build": "npx tsc -p tsconfig.json", + "watch": "rm -rf lib && npx tsc -w -p tsconfig.json", "lint": "npx eslint . --max-warnings 0", "docs": "npx typedoc --options typedoc.js" }, "devDependencies": { - "@types/jest": "^29.5.5", + "@types/jest": "^29.5.12", "@types/node": "16.18.58", - "@typescript-eslint/eslint-plugin": "^6.7.5", - "@typescript-eslint/parser": "^6.7.5", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", "eslint": "^8.51.0", - "eslint-plugin-jest": "^27.4.2", + "eslint-plugin-jest": "^27.9.0", "jest": "^29.7.0", - "moment": "^2.29.4", - "ts-jest": "^29.1.1", - "typedoc": "^0.25.2", - "typescript": "^5.2.2" + "ts-jest": "^29.1.2", + "typedoc": "^0.25.13", + "typescript": "5.3.3" }, "dependencies": {}, "publishConfig": { diff --git a/src/client.ts b/src/client.ts index 8bd18807..11c9f54b 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,13 +1,9 @@ -/* eslint-disable @typescript-eslint/explicit-member-accessibility */ - import { AdbClientOptions, AdbClientOptionsValues, InputDurationOptions, CommandConstruct, CpOptions, - Callback, - ValueCallback, ForwardsObject, IDevice, InputSource, @@ -31,14 +27,10 @@ import { WaitForType, PropertyValue, TransportCommandConstruct, - buildInputParams, KeyCode, parsePrimitiveParam, - parseCbParam, - parseValueParam, - nodeify, AdbExecError, - buildFsParams + autoUnregister } from './util'; import { Sync, SyncMode } from './sync'; import { execFile } from 'child_process'; @@ -111,12 +103,11 @@ import Swipe from './commands/host-transport/input/swipe'; import Press from './commands/host-transport/input/press'; import KeyEvent from './commands/host-transport/input/keyEvent'; import Tap from './commands/host-transport/input/tap'; -import EventUnregister from './util/eventUnregister'; const ADB_DEFAULT_PORT = 5555; const DEFAULT_OPTIONS = { port: 5037, - host: 'localhost', + host: '127.0.0.1', bin: 'adb', noAutoStart: false } as const; @@ -139,17 +130,12 @@ export class Client { /** * Starts adb server if not running. */ - startServer(): Promise; - startServer(cb: Callback): void; - startServer(cb?: Callback): Promise | void { + public startServer(): Promise { const port = this.options.port; const args = ['-P', port.toString(), 'start-server']; - return nodeify( - promisify((cb_) => - execFile(this.options.bin, args, (err) => cb_(err)) - )(), - cb - ); + return promisify((cb_) => + execFile(this.options.bin, args, (err) => cb_(err)) + )(); } private connection(): Promise { @@ -191,39 +177,26 @@ export class Client { /** * Gets the adb server version. */ - version(): Promise; - version(cb: ValueCallback): void; - version(cb?: ValueCallback): Promise | void { - return nodeify( - this.connection().then((conn) => - new VersionCommand(conn).execute() - ), - cb - ); + public async version(): Promise { + return new VersionCommand(await this.connection()).execute(); } - private ipConnect( + private async ipConnect( Construct: IpConnectConstruct, host: string, - port: number | ValueCallback | undefined, - cb: ValueCallback | undefined - ): Promise | void { - let port_ = parseValueParam(port); + port: number | undefined + ): Promise { if (host.indexOf(':') !== -1) { const [h, p] = host.split(':', 2); host = h; - port_ = parseInt(p); + port = parseInt(p); } - return nodeify( - this.connection().then((conn) => - new Construct( - conn, - host, - parsePrimitiveParam(ADB_DEFAULT_PORT, port_) - ).execute() - ), - parseCbParam(port, cb) - ); + const conn = await this.connection(); + return new Construct( + conn, + host, + parsePrimitiveParam(ADB_DEFAULT_PORT, port) + ).execute(); } /** @@ -235,78 +208,52 @@ export class Client { * await adb.connect(ip); *}); */ - connect(host: string): Promise; - connect(host: string, port: number): Promise; - connect(host: string, cb: ValueCallback): void; - connect(host: string, port: number, cb: ValueCallback): void; - connect( - host: string, - port?: number | ValueCallback, - cb?: ValueCallback - ): Promise | void { - return this.ipConnect(Connect, host, port, cb); + public connect(host: string): Promise; + public connect(host: string, port: number): Promise; + public connect(host: string, port?: number): Promise { + return this.ipConnect(Connect, host, port); } /** * Disconnects from the given device. */ - disconnect(host: string): Promise; - disconnect(host: string, port: number): Promise; - disconnect(host: string, cb: ValueCallback): void; - disconnect(host: string, port: number, cb: ValueCallback): void; - disconnect( - host: string, - port?: ValueCallback | number, - cb?: ValueCallback - ): Promise | void { - return this.ipConnect(Disconnect, host, port, cb); + public disconnect(host: string): Promise; + public disconnect(host: string, port: number): Promise; + public disconnect(host: string, port?: number): Promise { + return this.ipConnect(Disconnect, host, port); } /** * Gets the list of currently connected devices and emulators. */ - listDevices(): Promise; - listDevices(cb: ValueCallback): void; - listDevices(cb?: ValueCallback): Promise | void { - return nodeify( - this.connection().then((conn) => - new ListDevicesCommand(conn).execute() - ), - cb - ); + public async listDevices(): Promise { + return new ListDevicesCommand(await this.connection()).execute(); } /** * Tracks connection status of devices. */ - trackDevices(): Promise; - trackDevices(cb: ValueCallback): void; - trackDevices(cb?: ValueCallback): Promise | void { - return nodeify( - this.connection().then((conn) => { - const command = new TrackCommand(conn); - return command.execute().then(() => new Tracker(command, this)); - }), - cb - ); + public async trackDevices(): Promise { + const conn = await this.connection(); + const command = new TrackCommand(conn); + await command.execute(); + return new Tracker(command, this); } /** * Kills the adb server. */ - kill(): Promise; - kill(cb: Callback): void; - kill(cb?: Callback): Promise | void { - return nodeify( - this.connection() - .catch((error) => { - if (error.code !== 'ECONNREFUSED') { - throw error; - } - }) - .then((conn) => conn && new KillCommand(conn).execute()), - cb - ); + public async kill(): Promise { + let connection: Connection | void; + try { + connection = await this.connection(); + } catch (error) { + if ((error as NodeJS.ErrnoException)?.code === 'ECONNREFUSED') { + return; + } + throw error; + } + return new KillCommand(connection).execute(); } /** @@ -314,103 +261,62 @@ export class Client { * Meant for getting serial number of local devices. * Analogous to `adb shell getprop ro.serialno`. */ - getSerialNo(serial: string): Promise; - getSerialNo(serial: string, cb: ValueCallback): void; - getSerialNo( - serial: string, - cb?: ValueCallback - ): Promise | void { - return cb - ? this.getProp(serial, 'ro.serialno', (e, v) => cb(e, `${v}`)) - : this.getProp(serial, 'ro.serialno').then((v) => `${v}`); + public async getSerialNo(serial: string): Promise { + const serialNo = await this.getProp(serial, 'ro.serialno'); + return String(serialNo); } /** * Gets the device path of the device identified by the device. */ - getDevicePath(serial: string): Promise; - getDevicePath(serial: string, cb: ValueCallback): void; - getDevicePath( - serial: string, - cb?: ValueCallback - ): Promise | void { - return nodeify( - this.connection().then((conn) => - new GetDevicePathCommand(conn, serial).execute() - ), - cb - ); + public async getDevicePath(serial: string): Promise { + return new GetDevicePathCommand( + await this.connection(), + serial + ).execute(); } /** * Lists properties of the device. * Analogous to `adb shell getprop`. */ - listProperties(serial: string): Promise; - listProperties(serial: string, cb: ValueCallback): void; - listProperties( - serial: string, - cb?: ValueCallback - ): Promise | void { - return nodeify( - this.connection().then((conn) => - new ListPropertiesCommand(conn, serial).execute() - ), - cb - ); + public async listProperties(serial: string): Promise { + return new ListPropertiesCommand( + await this.connection(), + serial + ).execute(); } /** * Lists features of the device. * Analogous to `adb shell pm list features`. */ - listFeatures(serial: string): Promise; - listFeatures(serial: string, cb: ValueCallback): void; - listFeatures( - serial: string, - cb?: ValueCallback - ): Promise | void { - return nodeify( - this.connection().then((conn) => - new ListFeaturesCommand(conn, serial).execute() - ), - cb - ); + public async listFeatures(serial: string): Promise { + return new ListFeaturesCommand( + await this.connection(), + serial + ).execute(); } /** * Lists installed packages. * Analogous to `adb shell pm list packages`. */ - listPackages(serial: string): Promise; - listPackages(serial: string, cb: ValueCallback): void; - listPackages( - serial: string, - cb?: ValueCallback - ): Promise | void { - return nodeify( - this.connection().then((conn) => - new ListPackagesCommand(conn, serial).execute() - ), - cb - ); + public async listPackages(serial: string): Promise { + return new ListPackagesCommand( + await this.connection(), + serial + ).execute(); } /** * Gets the ipv4 addresses of default wlan interface. */ - getIpAddress(serial: string): Promise; - getIpAddress(serial: string, cb: ValueCallback): void; - getIpAddress( - serial: string, - cb?: ValueCallback - ): Promise | void { - return nodeify( - this.connection().then((conn) => - new GetIpAddressCommand(conn, serial).execute() - ), - cb - ); + public async getIpAddress(serial: string): Promise { + return new GetIpAddressCommand( + await this.connection(), + serial + ).execute(); } /** @@ -419,38 +325,28 @@ export class Client { * @example * adb.forward('serial', 'tcp:9222', 'localabstract:chrome_devtools_remote') */ - forward(serial: string, local: string, remote: string): Promise; - forward(serial: string, local: string, remote: string, cb: Callback): void; - forward( + public async forward( serial: string, local: string, - remote: string, - cb?: Callback - ): Promise | void { - return nodeify( - this.connection().then((conn) => - new ForwardCommand(conn, serial, local, remote).execute() - ), - cb - ); + remote: string + ): Promise { + return new ForwardCommand( + await this.connection(), + serial, + local, + remote + ).execute(); } /** * Lists all forwarded connections. * Analogous to `adb forward --list`. */ - listForwards(serial: string): Promise; - listForwards(serial: string, cb: ValueCallback): void; - listForwards( - serial: string, - cb?: ValueCallback - ): Promise | void { - return nodeify( - this.connection().then((conn) => - new ListForwardsCommand(conn, serial).execute() - ), - cb - ); + public async listForwards(serial: string): Promise { + return new ListForwardsCommand( + await this.connection(), + serial + ).execute(); } /** @@ -459,74 +355,52 @@ export class Client { * @example * adb.reverse('serial', 'localabstract:chrome_devtools_remote', 'tcp:9222') */ - reverse(serial: string, local: string, remote: string): Promise; - reverse(serial: string, local: string, remote: string, cb: Callback): void; - reverse( + public async reverse( serial: string, local: string, - remote: string, - cb?: Callback - ): Promise | void { - return nodeify( - this.connection().then((conn) => - new ReverseCommand(conn, serial, local, remote).execute() - ), - cb - ); + remote: string + ): Promise { + return new ReverseCommand( + await this.connection(), + serial, + local, + remote + ).execute(); } /** * Lists all reversed connections. * Analogous to `adb reverse --list`. */ - listReverses(serial: string): Promise; - listReverses(serial: string, cb: ValueCallback): void; - listReverses( - serial: string, - cb?: ValueCallback - ): Promise | void { - return nodeify( - this.connection().then((conn) => { - return new ListReversesCommand(conn, serial).execute(); - }), - cb - ); + public async listReverses(serial: string): Promise { + return new ListReversesCommand( + await this.connection(), + serial + ).execute(); } - private deleteApk(serial: string, pathToApk: string): Promise { - return this.connection().then((conn) => { - return new DeleteApk(conn, serial, pathToApk).execute(); - }); + private async deleteApk(serial: string, pathToApk: string): Promise { + return new DeleteApk( + await this.connection(), + serial, + pathToApk + ).execute(); } /** * Reboots the device. * Analogous to `adb reboot`. */ - reboot(serial: string): Promise; - reboot(serial: string, cb: Callback): void; - reboot(serial: string, cb?: Callback): Promise | void { - return nodeify( - this.connection().then((conn) => - new RebootCommand(conn, serial).execute() - ), - cb - ); + public async reboot(serial: string): Promise { + return new RebootCommand(await this.connection(), serial).execute(); } /** * Shuts the device down. * Analogous to `adb reboot -p`. */ - shutdown(serial: string): Promise; - shutdown(serial: string, cb: Callback): void; - shutdown(serial: string, cb?: Callback): Promise | void { - return nodeify( - this.connection().then((conn) => - new ShutdownCommand(conn, serial).execute() - ), - cb - ); + public async shutdown(serial: string): Promise { + return new ShutdownCommand(await this.connection(), serial).execute(); } /** @@ -534,48 +408,24 @@ export class Client { * Can be done on a rooted device. Analogous to `adb remount`. * Analogous to `adb remount` */ - remount(serial: string): Promise; - remount(serial: string, cb: Callback): void; - remount(serial: string, cb?: Callback): Promise | void { - return nodeify( - this.connection().then((conn) => - new RemountCommand(conn, serial).execute() - ), - cb - ); + public async remount(serial: string): Promise { + return new RemountCommand(await this.connection(), serial).execute(); } /** * Attempts to which the device to the root mode. * Analogous to `adb root`. */ - root(serial: string): Promise; - root(serial: string, cb: Callback): void; - root(serial: string, cb?: Callback): Promise | void { - return nodeify( - this.connection().then((conn) => - new RootCommand(conn, serial).execute() - ), - cb - ); + public async root(serial: string): Promise { + return new RootCommand(await this.connection(), serial).execute(); } /** * Takes a screenshot on the specified device. * Analogous to `adb shell screencap -p`. */ - screenshot(serial: string): Promise; - screenshot(serial: string, cb: ValueCallback): void; - screenshot( - serial: string, - cb?: ValueCallback - ): Promise | void { - return nodeify( - this.connection().then((conn) => { - return new ScreenShotCommand(conn, serial).execute(); - }), - cb - ); + public async screenshot(serial: string): Promise { + return new ScreenShotCommand(await this.connection(), serial).execute(); } /** @@ -585,32 +435,23 @@ export class Client { * const socket = await adb.openTcp('serial', 5555); * // socket.write(...) */ - openTcp(serial: string, port: number): Promise; - openTcp(serial: string, port: number, host: string): Promise; - openTcp(serial: string, port: number, cb: ValueCallback): void; - openTcp( + public async openTcp(serial: string, port: number): Promise; + public async openTcp( serial: string, port: number, - host: string, - cb: ValueCallback - ): void; - openTcp( + host: string + ): Promise; + public async openTcp( serial: string, port: number, - host?: string | ValueCallback, - cb?: ValueCallback - ): Promise | void { - return nodeify( - this.connection().then((conn) => { - return new TcpCommand( - conn, - serial, - port, - parseValueParam(host) - ).execute(); - }), - parseCbParam(host, cb) - ); + host?: string + ): Promise { + return new TcpCommand( + await this.connection(), + serial, + port, + host + ).execute(); } /** @@ -620,40 +461,24 @@ export class Client { * @param x Horizontal coordinate. * @param y Vertical coordinate. */ - roll(serial: string, x: number, y: number): Promise; - roll( + public async roll(serial: string, x: number, y: number): Promise; + public async roll( serial: string, x: number, y: number, source: InputSource ): Promise; - roll(serial: string, x: number, y: number, cb: Callback): void; - roll( - serial: string, - x: number, - y: number, - source: InputSource, - cb: Callback - ): void; - roll( + public async roll( serial: string, x: number, y: number, - source?: InputSource | Callback, - cb?: Callback - ): Promise | void { - const { params, cb_ } = buildInputParams(source, cb); - - return nodeify( - this.connection().then((conn) => { - return new Roll(conn, serial, { - source: params, - x, - y - }).execute(); - }), - cb_ - ); + source?: InputSource + ): Promise { + return new Roll(await this.connection(), serial, { + source, + x, + y + }).execute(); } /** @@ -661,22 +486,10 @@ export class Client { * Analogous to `adb shell input trackball press`. * Default input source is `trackball`. */ - press(serial: string): Promise; - press(serial: string, source: InputSource): Promise; - press(serial: string, cb: Callback): void; - press(serial: string, source: InputSource, cb: Callback): void; - press( - serial: string, - source?: InputSource | Callback, - cb?: Callback - ): Promise | void { - const { params, cb_ } = buildInputParams(source, cb); - return nodeify( - this.connection().then((conn) => { - return new Press(conn, serial, params).execute(); - }), - cb_ - ); + public async press(serial: string): Promise; + public async press(serial: string, source: InputSource): Promise; + public async press(serial: string, source?: InputSource): Promise { + return new Press(await this.connection(), serial, source).execute(); } /** @@ -688,14 +501,14 @@ export class Client { * @param x2 Horizontal ending coordinate. * @param y2 Vertical ending coordinate. */ - dragAndDrop( + public async dragAndDrop( serial: string, x1: number, y1: number, x2: number, y2: number ): Promise; - dragAndDrop( + public async dragAndDrop( serial: string, x1: number, y1: number, @@ -703,46 +516,21 @@ export class Client { y2: number, options: InputDurationOptions ): Promise; - dragAndDrop( + public async dragAndDrop( serial: string, x1: number, y1: number, x2: number, y2: number, - cb: Callback - ): void; - dragAndDrop( - serial: string, - x1: number, - y1: number, - x2: number, - y2: number, - options: InputDurationOptions, - cb: Callback - ): void; - dragAndDrop( - serial: string, - x1: number, - y1: number, - x2: number, - y2: number, - options?: InputDurationOptions | Callback, - cb?: Callback - ): Promise | void { - const { params, cb_ } = buildInputParams(options, cb); - - return nodeify( - this.connection().then((conn) => { - return new DragAndDrop(conn, serial, { - x1, - y1, - x2, - y2, - options: params - }).execute(); - }), - cb_ - ); + options?: InputDurationOptions + ): Promise { + return new DragAndDrop(await this.connection(), serial, { + x1, + y1, + x2, + y2, + options + }).execute(); } /** @@ -754,14 +542,14 @@ export class Client { * @param x2 Horizontal ending coordinate. * @param y2 Vertical ending coordinate. */ - swipe( + public async swipe( serial: string, x1: number, y1: number, x2: number, y2: number ): Promise; - swipe( + public async swipe( serial: string, x1: number, y1: number, @@ -769,45 +557,21 @@ export class Client { y2: number, options: InputDurationOptions ): Promise; - swipe( - serial: string, - x1: number, - y1: number, - x2: number, - y2: number, - cb: Callback - ): void; - swipe( + public async swipe( serial: string, x1: number, y1: number, x2: number, y2: number, - options: InputDurationOptions, - cb: Callback - ): void; - swipe( - serial: string, - x1: number, - y1: number, - x2: number, - y2: number, - options?: InputDurationOptions | Callback, - cb?: Callback - ): Promise | void { - const { params, cb_ } = buildInputParams(options, cb); - return nodeify( - this.connection().then((conn) => { - return new Swipe(conn, serial, { - x1, - y1, - x2, - y2, - options: params - }).execute(); - }), - cb_ - ); + options?: InputDurationOptions + ): Promise { + return new Swipe(await this.connection(), serial, { + x1, + y1, + x2, + y2, + options + }).execute(); } /** @@ -816,66 +580,29 @@ export class Client { * Default input source is `keyboard`. * @param code Key code to send. */ - keyEvent( + public async keyEvent( serial: string, code: KeyCode | NonEmptyArray ): Promise; - keyEvent( + public async keyEvent( serial: string, code: number | NonEmptyArray ): Promise; - keyEvent( + public async keyEvent( serial: string, code: KeyCode | NonEmptyArray, options: KeyEventOptions ): Promise; - keyEvent( - serial: string, - code: number | NonEmptyArray, - options: KeyEventOptions - ): Promise; - - keyEvent( - serial: string, - code: KeyCode | NonEmptyArray, - cb: Callback - ): void; - keyEvent( - serial: string, - code: number | NonEmptyArray, - cb: Callback - ): void; - - keyEvent( - serial: string, - code: KeyCode | NonEmptyArray, - options: KeyEventOptions, - cb: Callback - ): void; - keyEvent( + public async keyEvent( serial: string, code: number | NonEmptyArray, - options: KeyEventOptions, - cb: Callback - ): void; - - keyEvent( - serial: string, - code: number | NonEmptyArray, - options?: KeyEventOptions | Callback, - cb?: Callback - ): Promise | void { - const { params, cb_ } = buildInputParams(options, cb); - return nodeify( - this.connection().then((conn) => { - return new KeyEvent(conn, serial, { - options: params, - code - }).execute(); - }), - cb_ - ); + options?: KeyEventOptions + ): Promise { + return new KeyEvent(await this.connection(), serial, { + options, + code + }).execute(); } /** @@ -885,40 +612,24 @@ export class Client { * @param x Horizontal coordinate. * @param y Vertical coordinate. */ - tap(serial: string, x: number, y: number): Promise; - tap( + public async tap(serial: string, x: number, y: number): Promise; + public async tap( serial: string, x: number, y: number, source: InputSource ): Promise; - tap(serial: string, x: number, y: number, cb: Callback): void; - tap( + public async tap( serial: string, x: number, y: number, - source: InputSource, - cb: Callback - ): void; - tap( - serial: string, - x: number, - y: number, - source?: InputSource | Callback, - cb?: Callback - ): Promise | void { - const { params, cb_ } = buildInputParams(source, cb); - - return nodeify( - this.connection().then((conn) => { - return new Tap(conn, serial, { - source: params, - x, - y - }).execute(); - }), - cb_ - ); + source?: InputSource + ): Promise { + return new Tap(await this.connection(), serial, { + source, + x, + y + }).execute(); } /** @@ -926,26 +637,21 @@ export class Client { * Analogous to `adb shell input touchscreen text ''`. * Default input source is `touchscreen`. */ - text(serial: string, text: string): Promise; - text(serial: string, text: string, source: InputSource): Promise; - text(serial: string, text: string, cb: Callback): void; - text(serial: string, text: string, source: InputSource, cb: Callback): void; - text( + public async text(serial: string, text: string): Promise; + public async text( serial: string, text: string, - source?: InputSource | Callback, - cb?: Callback - ): Promise | void { - const { params, cb_ } = buildInputParams(source, cb); - return nodeify( - this.connection().then((conn) => { - return new Text(conn, serial, { - source: params, - text - }).execute(); - }), - cb_ - ); + source: InputSource + ): Promise; + public async text( + serial: string, + text: string, + source?: InputSource + ): Promise { + return new Text(await this.connection(), serial, { + source, + text + }).execute(); } /** @@ -962,34 +668,20 @@ export class Client { * console.log(entry); * }); */ - openLogcat(serial: string): Promise; - openLogcat(serial: string, options: LogcatOptions): Promise; - openLogcat(serial: string, cb: ValueCallback): void; - openLogcat( + public async openLogcat(serial: string): Promise; + public async openLogcat( serial: string, - options: LogcatOptions, - cb: ValueCallback - ): void; - openLogcat( + options: LogcatOptions + ): Promise; + public async openLogcat( serial: string, - options?: ValueCallback | LogcatOptions, - cb?: ValueCallback - ): Promise | void { - if (typeof options === 'function') { - cb = options; - options = undefined; - } - - return nodeify( - this.connection().then((conn) => { - return new LogcatCommand( - conn, - serial, - typeof options === 'object' ? options : undefined - ).execute(); - }), - cb - ); + options?: LogcatOptions + ): Promise { + return new LogcatCommand( + await this.connection(), + serial, + options + ).execute(); } private syncService(serial: string): Promise { @@ -1002,36 +694,31 @@ export class Client { * Deletes all data associated with a package from the device. * Analogous to `adb shell pm clear `. */ - clear(serial: string, pkg: string): Promise; - clear(serial: string, pkg: string, cb: Callback): void; - clear(serial: string, pkg: string, cb?: Callback): Promise | void { - return nodeify( - this.connection().then((conn) => { - return new ClearCommand(conn, serial, pkg).execute(); - }), - cb - ); + public async clear(serial: string, pkg: string): Promise { + return new ClearCommand(await this.connection(), serial, pkg).execute(); } - private installRemote( + private async installRemote( serial: string, apk: string, - options?: InstallOptions, - args?: string + options: InstallOptions | undefined, + args: string | undefined ): Promise { - return this.connection().then((conn) => { - return new InstallCommand(conn, serial, apk, options, args) - .execute() - .then(() => this.deleteApk(serial, apk)); - }); + await new InstallCommand( + await this.connection(), + serial, + apk, + options, + args + ).execute(); + return this.deleteApk(serial, apk); } - /** * Installs an apk to the device. * Analogous to `adb install `. */ - install(serial: string, apk: string | Readable): Promise; - install( + public async install(serial: string, apk: string | Readable): Promise; + public async install( serial: string, apk: string | Readable, options: InstallOptions @@ -1039,55 +726,29 @@ export class Client { /** * @param args Extra arguments. E.g. `--fastdeploy` flag. */ - install( + public async install( serial: string, apk: string | Readable, options: InstallOptions, args: string ): Promise; - install(serial: string, apk: string | Readable, cb: Callback): void; - install( + public async install( serial: string, apk: string | Readable, - options: InstallOptions, - cb: Callback - ): void; - install( - serial: string, - apk: string | Readable, - options: InstallOptions, - args: string, - cb: Callback - ): void; - - install( - serial: string, - apk: string | Readable, - options?: InstallOptions | Callback, - args?: string | Callback, - cb?: Callback - ): Promise | void { + options?: InstallOptions, + args?: string + ): Promise { const temp = Sync.temp(typeof apk === 'string' ? apk : '_stream.apk'); - return nodeify( - this.push(serial, apk, temp).then((transfer) => { - const eventUnregister = new EventUnregister(transfer); - const promise = new Promise((resolve, reject) => { - eventUnregister.register((transfer) => - transfer.on('error', reject).on('end', (): void => { - this.installRemote( - serial, - temp, - parseValueParam(options), - parseValueParam(args) - ) - .then(resolve) - .catch(reject); - }) - ); - }); - return eventUnregister.unregisterAfter(promise); - }), - parseCbParam(options, cb) || parseCbParam(args, cb) + return autoUnregister( + await this.push(serial, apk, temp), + (transfer) => + new Promise((resolve, reject) => { + transfer.on('error', reject).on('end', () => { + this.installRemote(serial, temp, options, args) + .then(resolve) + .catch(reject); + }); + }) ); } @@ -1095,165 +756,107 @@ export class Client { * Uninstalls a package from the device. * Analogous to `adb uninstall`. */ - uninstall(serial: string, pkg: string): Promise; - uninstall( + public async uninstall(serial: string, pkg: string): Promise; + public async uninstall( serial: string, pkg: string, options: UninstallOptions ): Promise; - uninstall(serial: string, pkg: string, cb: Callback): void; - uninstall( - serial: string, - pkg: string, - options: UninstallOptions, - cb: Callback - ): void; - uninstall( + public async uninstall( serial: string, pkg: string, - options?: Callback | UninstallOptions, - cb?: Callback - ): Promise | void { - return nodeify( - this.connection().then((conn) => { - return new UninstallCommand( - conn, - serial, - pkg, - parseValueParam(options) - ).execute(); - }), - parseCbParam(options, cb) - ); + options?: UninstallOptions + ): Promise { + return new UninstallCommand( + await this.connection(), + serial, + pkg, + options + ).execute(); } /** * Tells if a package is installed or not. */ - isInstalled(serial: string, pkg: string): Promise; - isInstalled(serial: string, pkg: string, cb: ValueCallback): void; - isInstalled( - serial: string, - pkg: string, - cb?: ValueCallback - ): Promise | void { - return nodeify( - this.connection().then((conn) => { - return new IsInstalledCommand(conn, serial, pkg).execute(); - }), - cb - ); + public async isInstalled(serial: string, pkg: string): Promise { + return new IsInstalledCommand( + await this.connection(), + serial, + pkg + ).execute(); } /** * Starts a new activity with options. * Analogous to `adb shell am start `. */ - startActivity(serial: string, pkg: string, activity: string): Promise; - startActivity( + public async startActivity( serial: string, pkg: string, - activity: string, - options: StartActivityOptions + activity: string ): Promise; - startActivity( + public async startActivity( serial: string, pkg: string, activity: string, - cb: Callback - ): void; - startActivity( - serial: string, - pkg: string, - activity: string, - options: StartActivityOptions, - cb: Callback - ): void; - startActivity( + options: StartActivityOptions + ): Promise; + public async startActivity( serial: string, pkg: string, activity: string, - options?: StartActivityOptions | Callback, - cb?: Callback - ): Promise | void { - return nodeify( - this.connection().then((conn) => { - return new StartActivityCommand( - conn, - serial, - pkg, - activity, - parseValueParam(options) - ).execute(); - }), - parseCbParam(options, cb) - ); + options?: StartActivityOptions + ): Promise { + return new StartActivityCommand( + await this.connection(), + serial, + pkg, + activity, + options + ).execute(); } /** * Starts a new service with options. * Analogous to `adb shell am startservice `. */ - startService(serial: string, pkg: string, service: string): Promise; - startService( + public async startService( serial: string, pkg: string, - service: string, - options: StartServiceOptions + service: string ): Promise; - startService( + public async startService( serial: string, pkg: string, service: string, - cb: Callback - ): void; - startService( - serial: string, - pkg: string, - service: string, - options: StartServiceOptions, - cb: Callback - ): void; - startService( + options: StartServiceOptions + ): Promise; + public async startService( serial: string, pkg: string, service: string, - options?: StartServiceOptions | Callback, - cb?: Callback - ): Promise | void { - return nodeify( - this.connection().then((conn) => { - return new StartServiceCommand( - conn, - serial, - pkg, - service, - parseValueParam(options) - ).execute(); - }), - parseCbParam(options, cb) - ); + options?: StartServiceOptions + ): Promise { + return new StartServiceCommand( + await this.connection(), + serial, + pkg, + service, + options + ).execute(); } /** * Reads given directory. * The path should start with `/`. */ - readDir(serial: string, path: string): Promise; - readDir(serial: string, path: string, cb: ValueCallback): void; - readDir( - serial: string, - path: string, - cb?: ValueCallback - ): Promise | void { - return nodeify( - this.syncService(serial).then((sync) => { - return sync.readDir(path).finally(() => { - return sync.end(); - }); - }), - cb - ); + public async readDir(serial: string, path: string): Promise { + const sync = await this.syncService(serial); + try { + return await sync.readDir(path); + } finally { + sync.end(); + } } /** @@ -1269,22 +872,9 @@ export class Client { * console.log(data); * }); */ - pull(serial: string, path: string): Promise; - pull(serial: string, path: string, cb: ValueCallback): void; - - pull( - serial: string, - path: string, - cb?: ValueCallback - ): Promise | void { - return nodeify( - this.syncService(serial).then((sync) => { - return sync.pull(path).on('end', () => { - sync.end(); - }); - }), - cb - ); + public async pull(serial: string, path: string): Promise { + const sync = await this.syncService(serial); + return sync.pull(path).on('end', () => sync.end()); } /** @@ -1294,47 +884,25 @@ export class Client { * const transfer = await adb.push('serial', '/path-src', '/path-dest') * transfer.on('end', () => { }); */ - push( + public async push( serial: string, srcPath: string | Readable, destPath: string ): Promise; - push( + public async push( serial: string, srcPath: string | Readable, destPath: string, mode: SyncMode ): Promise; - push( + public async push( serial: string, srcPath: string | Readable, destPath: string, - cb: ValueCallback - ): void; - push( - serial: string, - srcPath: string | Readable, - destPath: string, - mode: SyncMode, - cb: ValueCallback - ): void; - push( - serial: string, - srcPath: string | Readable, - destPath: string, - mode?: ValueCallback | SyncMode, - cb?: ValueCallback - ): Promise | void { - return nodeify( - this.syncService(serial).then((sync) => { - return sync - .push(srcPath, destPath, parseValueParam(mode)) - .on('end', () => { - sync.end(); - }); - }), - parseCbParam(mode, cb) - ); + mode?: SyncMode + ): Promise { + const sync = await this.syncService(serial); + return sync.push(srcPath, destPath, mode).on('end', () => sync.end()); } private async awaitActiveDevice(serial: string): Promise { @@ -1352,20 +920,20 @@ export class Client { tracker.once('error', reject); tracker.once('remove', (device) => { if (device.id === serial) { - tracker.on('add', activeDeviceListener); - tracker.on('change', activeDeviceListener); + tracker.once('add', activeDeviceListener); + tracker.once('change', activeDeviceListener); } }); }); }; - const tracker_2 = await this.trackDevices(); + const tracker = await this.trackDevices(); try { return await Promise.race([ T.setTimeout(5000, undefined, { ref: false }), - track(tracker_2) + track(tracker) ]); } finally { - tracker_2.end(); + tracker.end(); } } @@ -1374,86 +942,58 @@ export class Client { * Afterwards it is possible to use `connect` method. * Analogous to `adb tcpip 5555`. */ - tcpip(serial: string): Promise; - tcpip(serial: string, port: number): Promise; - tcpip(serial: string, cb: Callback): void; - tcpip(serial: string, port: number, cb: Callback): void; - tcpip( - serial: string, - port?: Callback | number, - cb?: Callback - ): Promise | void { - return nodeify( - this.connection().then((conn) => { - return new TcpIpCommand( - conn, - serial, - this.awaitActiveDevice(serial), - parsePrimitiveParam(ADB_DEFAULT_PORT, parseValueParam(port)) - ).execute(); - }), - parseCbParam(port, cb) - ); + public async tcpip(serial: string): Promise; + public async tcpip(serial: string, port: number): Promise; + public async tcpip(serial: string, port?: number): Promise { + return new TcpIpCommand( + await this.connection(), + serial, + this.awaitActiveDevice(serial), + parsePrimitiveParam(ADB_DEFAULT_PORT, port) + ).execute(); } /** * Sets the device transport back to usb. */ - usb(serial: string): Promise; - usb(serial: string, cb: Callback): void; - usb(serial: string, cb?: Callback): Promise | void { - return nodeify( - this.connection().then((conn) => { - return new UsbCommand( - conn, - serial, - this.awaitActiveDevice(serial) - ).execute(); - }), - cb - ); + public async usb(serial: string): Promise { + return new UsbCommand( + await this.connection(), + serial, + this.awaitActiveDevice(serial) + ).execute(); } /** * Waits until the device has finished booting. */ - waitBootComplete(serial: string): Promise; - waitBootComplete(serial: string, cb: Callback): void; - waitBootComplete(serial: string, cb?: Callback): Promise | void { - return nodeify( - this.connection().then((conn) => { - return new WaitBootCompleteCommand(conn, serial).execute(); - }), - cb - ); + public async waitBootComplete(serial: string): Promise { + return new WaitBootCompleteCommand( + await this.connection(), + serial + ).execute(); } /** * Waits until the device is in the given state. * Analogous to `adb wait-for--`. */ - waitFor(transport: WaitForType, state: WaitForState): Promise; - waitFor(transport: WaitForType, state: WaitForState, cb?: Callback): void; - waitFor( + public async waitFor( transport: WaitForType, - state: WaitForState, - cb?: Callback - ): Promise | void { - return nodeify( - this.connection().then((conn) => { - return new WaitFor(conn, transport, state).execute(); - }), - cb - ); + state: WaitForState + ): Promise { + return new WaitFor(await this.connection(), transport, state).execute(); } /** * Maps through all connected devices. */ - async map(mapper: (device: Device) => Promise | T): Promise { + public async map( + mapper: (device: Device) => Promise | T + ): Promise { const devices = await this.listDevices(); return Promise.all( - devices.map((device_1) => mapper(new Device(this, device_1))) + devices.map((device) => mapper(new Device(this, device))) ); } @@ -1462,7 +1002,7 @@ export class Client { data: string | Readable, dest: string ): Promise { - const transfer = await this.push(serial, data, `${dest}`); + const transfer = await this.push(serial, data, dest); return new Promise((resolve, reject) => { transfer.once('end', resolve); transfer.once('error', reject); @@ -1472,126 +1012,68 @@ export class Client { /** * Wraps {@link push} method, provides API for quick data writing. */ - pushDataToFile( + public pushDataToFile( serial: string, data: string | Buffer | Readable, destPath: string - ): Promise; - pushDataToFile( - serial: string, - data: string | Buffer | Readable, - destPath: string, - cb: Callback - ): void; - pushDataToFile( - serial: string, - data: string | Buffer | Readable, - destPath: string, - cb?: Callback - ): Promise | void { - return nodeify( - this.pushInternal( - serial, - Readable.from( - typeof data === 'string' ? Buffer.from(data, 'utf-8') : data - ), - destPath + ): Promise { + return this.pushInternal( + serial, + Readable.from( + typeof data === 'string' ? Buffer.from(data, 'utf-8') : data ), - cb + destPath ); } /** * Wraps {@link push} method, reads the content of file on the host to a file on the device. */ - pushFile(serial: string, srcPath: string, destPath: string): Promise; - pushFile( + public pushFile( serial: string, srcPath: string, - destPath: string, - cb: Callback - ): void; - pushFile( - serial: string, - srcPath: string, - destPath: string, - cb?: Callback - ): Promise | void { - return nodeify(this.pushInternal(serial, srcPath, destPath), cb); + destPath: string + ): Promise { + return this.pushInternal(serial, srcPath, destPath); } /** * Wraps {@link pull} method, reads the file content and resolves with the output. */ - pullDataFromFile(serial: string, srcPath: string): Promise; - pullDataFromFile( - serial: string, - srcPath: string, - cb: ValueCallback - ): void; - pullDataFromFile( + public async pullDataFromFile( serial: string, - srcPath: string, - cb?: ValueCallback - ): Promise | void { - return nodeify( - this.pull(serial, `${srcPath}`).then( - (transfer: PullTransfer): Promise => { - return new Promise((resolve, reject) => { - let data = Buffer.alloc(0); - transfer.on('data', (chunk) => { - data = Buffer.isBuffer(chunk) - ? Buffer.concat([data, chunk]) - : data; - }); - transfer.on('end', () => { - resolve(data); - }); - transfer.on('error', reject); - }); - } - ), - cb - ); + srcPath: string + ): Promise { + const transfer = await this.pull(serial, srcPath); + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + transfer.on('data', (chunk) => { + Buffer.isBuffer(chunk) && chunks.push(chunk); + }); + transfer.on('end', () => resolve(Buffer.concat(chunks))); + transfer.on('error', reject); + }); } /** * Wraps {@link pull} method, reads the content of file on the device and write it to a file on the machine. */ - pullFile(serial: string, srcPath: string, destPath: string): Promise; - pullFile( + public async pullFile( serial: string, srcPath: string, - destPath: string, - cb: Callback - ): void; - pullFile( - serial: string, - srcPath: string, - destPath: string, - cb?: Callback - ): Promise | void { - return nodeify( - this.pull(serial, srcPath).then( - async (transfer: PullTransfer): Promise => { - const eventUnregister = new EventUnregister(transfer); - const promise = new Promise((resolve, reject) => { - eventUnregister.register((transfer_) => - transfer_ - .once('readable', () => - transfer_.pipe( - fs.createWriteStream(destPath) - ) - ) - .once('end', resolve) - .once('error', reject) - ); - }); - - return eventUnregister.unregisterAfter(promise); - } - ), - cb + destPath: string + ): Promise { + return autoUnregister( + this.pull(serial, srcPath), + (transfer) => + new Promise((resolve, reject) => { + transfer + .once('readable', () => + transfer.pipe(fs.createWriteStream(destPath)) + ) + .once('end', resolve) + .once('error', reject); + }) ); } @@ -1599,156 +1081,91 @@ export class Client { * Sets property on the device. * Analogues to `adb shell setprop `. */ - setProp(serial: string, prop: string, value: PrimitiveType): Promise; - setProp( - serial: string, - prop: string, - value: PrimitiveType, - cb?: Callback - ): void; - setProp( + public async setProp( serial: string, prop: string, - value: PrimitiveType, - cb?: Callback - ): Promise | void { - return nodeify( - this.connection().then((conn) => - new SetProp(conn, serial, prop, value).execute() - ), - cb - ); + value: PrimitiveType + ): Promise { + return new SetProp( + await this.connection(), + serial, + prop, + value + ).execute(); } /** * Gets property from the device. * Analogues to `adb shell getprop `. */ - getProp(serial: string, prop: string): Promise; - getProp( - serial: string, - prop: string, - cb: ValueCallback - ): void; - getProp( - serial: string, - prop: string, - cb?: ValueCallback - ): Promise | void { - return nodeify( - this.connection().then((conn) => { - return new GetPropertyCommand(conn, serial, prop).execute(); - }), - cb - ); + public async getProp(serial: string, prop: string): Promise { + return new GetPropertyCommand( + await this.connection(), + serial, + prop + ).execute(); } /** * Puts setting on the device. * Analogues to `adb shell settings put `. */ - putSetting( + public async putSetting( serial: string, mode: SettingsMode, name: string, value: PrimitiveType - ): Promise; - putSetting( - serial: string, - mode: SettingsMode, - name: string, - value: PrimitiveType, - cb: Callback - ): void; - putSetting( - serial: string, - mode: SettingsMode, - name: string, - value: PrimitiveType, - cb?: Callback - ): Promise | void { - return nodeify( - this.connection().then((conn) => { - return new PutSetting( - conn, - serial, - mode, - name, - value - ).execute(); - }), - cb - ); + ): Promise { + return new PutSetting( + await this.connection(), + serial, + mode, + name, + value + ).execute(); } /** * Lists settings of the device. * Analogues to `adb shell settings list `. */ - listSettings(serial: string, mode: SettingsMode): Promise; - listSettings( - serial: string, - mode: SettingsMode, - cb: ValueCallback - ): void; - listSettings( + public async listSettings( serial: string, - mode: SettingsMode, - cb?: ValueCallback - ): Promise | void { - return nodeify( - this.connection().then((conn) => { - return new ListSettingsCommand(conn, serial, mode).execute(); - }), - cb - ); + mode: SettingsMode + ): Promise { + return new ListSettingsCommand( + await this.connection(), + serial, + mode + ).execute(); } /** * Gets setting from the device. * Analogues to `adb shell settings get `. */ - getSetting( + public async getSetting( serial: string, mode: SettingsMode, name: string - ): Promise; - getSetting( - serial: string, - mode: SettingsMode, - name: string, - cb: ValueCallback - ): void; - getSetting( - serial: string, - mode: SettingsMode, - name: string, - cb?: ValueCallback - ): Promise | void { - return nodeify( - this.connection().then((conn) => { - return new GetSetting(conn, serial, mode, name).execute(); - }), - cb - ); + ): Promise { + return new GetSetting( + await this.connection(), + serial, + mode, + name + ).execute(); } /** * Executes a given shell command via adb console interface. Analogous to `adb -s shell `. */ - shell(serial: string, command: string): Promise; - shell(serial: string, command: string, cb: ValueCallback): void; - shell( - serial: string, - command: string, - cb?: ValueCallback - ): Promise | void { - return nodeify( - this.connection().then((conn) => { - return new ShellCommand(conn, serial, command).execute(); - }), - cb - ); + public async shell(serial: string, command: string): Promise { + return new ShellCommand( + await this.connection(), + serial, + command + ).execute(); } /** @@ -1775,7 +1192,7 @@ export class Client { * } * } */ - async custom( + public async custom( CustomCommand: CommandConstruct, ...args: P ): Promise { @@ -1801,25 +1218,19 @@ export class Client { * } * } */ - customTransport( + public async customTransport( CustomCommand: TransportCommandConstruct, serial: string, ...args: P ): Promise { - return this.connection().then((conn) => { - return new CustomCommand(conn, serial, ...args).execute(); - }); + const conn = await this.connection(); + return new CustomCommand(conn, serial, ...args).execute(); } /** * Establishes a new monkey connection on port `1080`. */ - openMonkey(serial: string): Promise; - openMonkey(serial: string, cb: ValueCallback): void; - openMonkey( - serial: string, - cb?: ValueCallback - ): Promise | void { + public openMonkey(serial: string): Promise { const tryConnect = async (times: number): Promise => { try { const stream = await this.openTcp(serial, 1080); @@ -1869,22 +1280,15 @@ export class Client { } ); }; - return nodeify(establishConnection(3), cb); + return establishConnection(3); } /** * Force stops given package. * Analogous to `adb shell am force-stop `. */ - killApp(serial: string, pkg: string): Promise; - killApp(serial: string, pkg: string, cb: Callback): void; - killApp(serial: string, pkg: string, cb?: Callback): Promise | void { - return nodeify( - this.shell(serial, `am force-stop ${pkg}`).then(() => { - return; - }), - cb - ); + public async killApp(serial: string, pkg: string): Promise { + await this.shell(serial, `am force-stop ${pkg}`); } private execInternal(...args: ReadonlyArray): Promise { @@ -1911,237 +1315,173 @@ export class Client { /** * Executes a given command via adb console interface. */ - exec(cmd: string): Promise; - exec(cmd: string, cb: ValueCallback): void; - exec(cmd: string, cb?: ValueCallback): Promise | void { - return nodeify(this.execInternal(cmd), cb); + public exec(cmd: string): Promise { + return this.execInternal(cmd); } /** * Executes a given command on specific device via adb console interface. * Analogous to `adb -s `. */ - execDevice(serial: string, cmd: string): Promise; - execDevice(serial: string, cmd: string, cb: ValueCallback): void; - execDevice( - serial: string, - cmd: string, - cb?: ValueCallback - ): Promise | void { - return nodeify(this.execInternal(...['-s', serial, cmd]), cb); + public execDevice(serial: string, cmd: string): Promise { + return this.execInternal(...['-s', serial, cmd]); } /** * Executes a given command on specific device shell via adb console interface. * Analogous to `adb -s shell ` . */ - execDeviceShell(serial: string, cmd: string): Promise; - execDeviceShell( - serial: string, - cmd: string, - cb: ValueCallback - ): void; - execDeviceShell( - serial: string, - cmd: string, - cb?: ValueCallback - ): Promise | void { - return nodeify(this.execInternal(...['-s', serial, 'shell', cmd]), cb); + public execDeviceShell(serial: string, cmd: string): Promise { + return this.execInternal(...['-s', serial, 'shell', cmd]); } /** * Retrieves current battery status. * Analogous to `adb -s shell dumpsys battery` . */ - batteryStatus(serial: string): Promise; - batteryStatus(serial: string, cb: ValueCallback): void; - batteryStatus( - serial: string, - cb?: ValueCallback - ): Promise | void { - return nodeify( - this.connection().then((conn) => { - return new BatteryStatusCommand(conn, serial).execute(); - }), - cb - ); + public async batteryStatus(serial: string): Promise { + return new BatteryStatusCommand( + await this.connection(), + serial + ).execute(); } /** * Removes file/folder specified by `path` parameter. * Analogous to `adb shell rm `. */ - rm(serial: string, path: string): Promise; - rm(serial: string, path: string, options: RmOptions): Promise; - rm(serial: string, path: string, cb: Callback): void; - rm(serial: string, path: string, options: RmOptions, cb: Callback): void; - rm( + public async rm(serial: string, path: string): Promise; + public async rm( serial: string, path: string, - options?: RmOptions | Callback, - cb?: Callback - ): Promise | void { - const { options_, cb_ } = buildFsParams(options, cb); - return nodeify( - this.connection().then((conn) => { - return new RmCommand(conn, serial, path, options_).execute(); - }), - cb_ - ); + options: RmOptions + ): Promise; + public async rm( + serial: string, + path: string, + options?: RmOptions + ): Promise { + return new RmCommand( + await this.connection(), + serial, + path, + options + ).execute(); } /** * Creates directory specified by `path` parameter. * Analogous to `adb shell mkdir `. */ - mkdir(serial: string, path: string): Promise; - mkdir(serial: string, path: string, options?: MkDirOptions): Promise; - mkdir(serial: string, path: string, cb: Callback): void; - mkdir( + public async mkdir(serial: string, path: string): Promise; + public async mkdir( serial: string, path: string, - options: MkDirOptions, - cb: Callback - ): void; - mkdir( + options: MkDirOptions + ): Promise; + public async mkdir( serial: string, path: string, - options?: MkDirOptions | Callback, - cb?: Callback - ): Promise | void { - const { options_, cb_ } = buildFsParams(options, cb); - return nodeify( - this.connection().then((conn) => { - return new MkDirCommand(conn, serial, path, options_).execute(); - }), - cb_ - ); + options?: MkDirOptions + ): Promise { + return new MkDirCommand( + await this.connection(), + serial, + path, + options + ).execute(); } /** * Updates access and modification times of file specified by `path` parameter, or creates a new file. * Analogous to `adb shell touch `. */ - touch(serial: string, path: string): Promise; - touch(serial: string, path: string, options: TouchOptions): Promise; - touch(serial: string, path: string, cb: Callback): void; - touch( + public async touch(serial: string, path: string): Promise; + public async touch( serial: string, path: string, - options: TouchOptions, - cb: Callback - ): void; - touch( + options: TouchOptions + ): Promise; + public async touch( serial: string, path: string, - options?: TouchOptions | Callback, - cb?: Callback - ): Promise | void { - const { options_, cb_ } = buildFsParams(options, cb); - return nodeify( - this.connection().then((conn) => { - return new TouchCommand(conn, serial, path, options_).execute(); - }), - cb_ - ); + options?: TouchOptions + ): Promise { + return new TouchCommand( + await this.connection(), + serial, + path, + options + ).execute(); } /** * Moves data with `srcPath` to `destPath` parameter. * Analogous to `adb shell mv `. */ - mv(serial: string, srcPath: string, destPath: string): Promise; - mv( + public async mv( serial: string, srcPath: string, - destPath: string, - options: MvOptions + destPath: string ): Promise; - mv(serial: string, srcPath: string, destPath: string, cb: Callback): void; - mv( + public async mv( serial: string, srcPath: string, destPath: string, - options: MvOptions, - cb: Callback - ): void; - mv( + options: MvOptions + ): Promise; + public async mv( serial: string, srcPath: string, destPath: string, - options?: Callback | MvOptions, - cb?: Callback - ): Promise | void { - const { options_, cb_ } = buildFsParams(options, cb); - return nodeify( - this.connection().then((conn) => { - return new MvCommand( - conn, - serial, - [srcPath, destPath], - options_ - ).execute(); - }), - cb_ - ); + options?: MvOptions + ): Promise { + return new MvCommand( + await this.connection(), + serial, + [srcPath, destPath], + options + ).execute(); } /** * Copies data with `srcPath` to `destPath` parameter. * Analogous to `adb shell cp `. */ - cp(serial: string, srcPath: string, destPath: string): Promise; - cp( + public async cp( serial: string, srcPath: string, - destPath: string, - options: CpOptions + destPath: string ): Promise; - cp(serial: string, srcPath: string, destPath: string, cb: Callback): void; - cp( + public async cp( serial: string, srcPath: string, destPath: string, - options: CpOptions, - cb: Callback - ): void; - cp( + options: CpOptions + ): Promise; + public async cp( serial: string, srcPath: string, destPath: string, - options?: Callback | CpOptions, - cb?: Callback - ): Promise | void { - const { options_, cb_ } = buildFsParams(options, cb); - return nodeify( - this.connection().then((conn) => { - return new CpCommand( - conn, - serial, - [srcPath, destPath], - options_ - ).execute(); - }), - cb_ - ); + options?: CpOptions + ): Promise { + return new CpCommand( + await this.connection(), + serial, + [srcPath, destPath], + options + ).execute(); } /** * Gets file stats for specified path. * Analogous to `adb stat `. */ - fileStat(serial: string, path: string): Promise; - fileStat(serial: string, path: string, cb: ValueCallback): void; - fileStat( - serial: string, - path: string, - cb?: ValueCallback - ): Promise | void { - return nodeify( - this.connection().then((conn) => { - return new FileStatCommand(conn, serial, path).execute(); - }), - cb - ); + public async fileStat(serial: string, path: string): Promise { + return new FileStatCommand( + await this.connection(), + serial, + path + ).execute(); } } diff --git a/src/commands/abstract/devices.ts b/src/commands/abstract/devices.ts index deedcb73..54f9d1e5 100644 --- a/src/commands/abstract/devices.ts +++ b/src/commands/abstract/devices.ts @@ -3,28 +3,47 @@ import { Connection } from '../../connection'; import { DeviceState, IDevice } from '../../util'; import { UnexpectedDataError } from '../../util'; import Command from '../command'; -const checkValues = ([_1, _2]: [string, string], expected: string[]): void => { - if (!_1 || !_2) { - throw new UnexpectedDataError([_1, _2].join(', '), expected.join(', ')); - } + +const expectedKeys = [ + 'usb', + 'product', + 'model', + 'device', + 'transport_id' +] as const; + +const throwUnexpected = (received: string): never => { + throw new UnexpectedDataError( + received, + ` <${expectedKeys.join('|')}>:` + ); }; -const parseProps = (values: string[]): Record => - values.slice(2).reduce>((acc, curr) => { - const [key, value] = curr.split(':'); - checkValues( - [key, value], - ['usb', 'product', 'model', 'device', 'transport_id'] +const parseProps = (values: string[]): Record => { + return values.reduce>((acc, curr) => { + const match = curr.match( + new RegExp(`(${expectedKeys.join('|')}):(\\S+)(?=\\s|$)`) ); + if (!match) { + return acc; + } + const [key, value] = match.slice(1); acc[key] = value; return acc; }, {}); +}; -export function constructDevice(values: string[]): IDevice { +export function constructDevice(line: string): IDevice { + const values = line.split(/\s+/); const [id, state] = values; - checkValues([id, state], ['id', 'state']); + if (!id || !state) { + return throwUnexpected(line); + } - const { usb, product, model, device, transport_id } = parseProps(values); + const { usb, product, model, device, transport_id } = parseProps( + values.slice(2) + ); + if (typeof transport_id === 'undefined') return throwUnexpected(line); return { id: id, state: @@ -35,7 +54,7 @@ export function constructDevice(values: string[]): IDevice { product: product, model: model, device: device, - transportId: transport_id as string, + transportId: transport_id, transport: net.isIPv4(/^(.*?):([0-9]+)$/.exec(id)?.[1] || '') ? 'local' : 'usb' @@ -52,8 +71,7 @@ export default abstract class DevicesCommand extends Command { } private parse(value: string): IDevice[] { - const lines = value.split('\n').filter(Boolean); - return lines.map((line) => constructDevice(line.split(/\s+/))); + return value.split('\n').filter(Boolean).map(constructDevice); } public async readDevices(): Promise { diff --git a/src/commands/abstract/package.ts b/src/commands/abstract/package.ts new file mode 100644 index 00000000..743f5050 --- /dev/null +++ b/src/commands/abstract/package.ts @@ -0,0 +1,28 @@ +import { Connection } from '../../connection'; +import TransportCommand from './transport'; + +export default abstract class PackageCommand extends TransportCommand { + protected abstract Cmd: string; + protected packageOrPath: string; + protected keepAlive = false; + + constructor(connection: Connection, serial: string, packageOrPath: string) { + super(connection, serial); + this.packageOrPath = packageOrPath; + } + + protected async postExecute(): Promise { + try { + const [, result, code] = await this.parser.searchLine( + /^(Success|Failure \[(.*?)\])$/, + false + ); + if (result !== 'Success') { + this.throwError(code); + } + } finally { + await this.parser.readAll(); + } + } + protected abstract throwError(code: string): never; +} diff --git a/src/commands/host-transport/fileSystem/touch.ts b/src/commands/host-transport/fileSystem/touch.ts index e05f1d48..926870ac 100644 --- a/src/commands/host-transport/fileSystem/touch.ts +++ b/src/commands/host-transport/fileSystem/touch.ts @@ -6,13 +6,13 @@ import FileSystemCommand from '../../abstract/fileSystem'; */ const formatToTimeFlag = (value: Date | string): string => { const date = new Date(value); - const year = date.getFullYear().toString(); - const month = (date.getMonth() + 1).toString().padStart(2, '0'); - const day = date.getDate().toString().padStart(2, '0'); - const hours = date.getHours().toString().padStart(2, '0'); - const minutes = date.getMinutes().toString().padStart(2, '0'); - const seconds = date.getSeconds().toString().padStart(2, '0'); - const milliseconds = date.getMilliseconds().toString().padStart(3, '0'); + const year = date.getUTCFullYear().toString(); + const month = (date.getUTCMonth() + 1).toString().padStart(2, '0'); + const day = date.getUTCDate().toString().padStart(2, '0'); + const hours = date.getUTCHours().toString().padStart(2, '0'); + const minutes = date.getUTCMinutes().toString().padStart(2, '0'); + const seconds = date.getUTCSeconds().toString().padStart(2, '0'); + const milliseconds = date.getUTCMilliseconds().toString().padStart(3, '0'); return `${year}${month}${day}${hours}${minutes}.${seconds}${milliseconds}`; }; @@ -32,7 +32,7 @@ export default class TouchCommand extends FileSystemCommand< ], time: (value: Date | string): string[] => [ '-t', - escape(formatToTimeFlag(new Date(value))) + escape(formatToTimeFlag(value)) ], reference: (value: string): string[] => ['-r', escape(value)] }; diff --git a/src/commands/host-transport/install.ts b/src/commands/host-transport/install.ts index 48a11e8e..6190403c 100644 --- a/src/commands/host-transport/install.ts +++ b/src/commands/host-transport/install.ts @@ -1,13 +1,10 @@ import { Connection } from '../../connection'; import { escapeCompat } from '../../util'; import { InstallOptions } from '../../util'; -import TransportCommand from '../abstract/transport'; +import PackageCommand from '../abstract/package'; -export default class InstallCommand extends TransportCommand { +export default class InstallCommand extends PackageCommand { protected Cmd: string; - private apk: string; - protected keepAlive = false; - constructor( connection: Connection, serial: string, @@ -15,31 +12,23 @@ export default class InstallCommand extends TransportCommand { options: InstallOptions | void, args: string | void ) { - super(connection, serial); - this.apk = apk; + super(connection, serial, apk); this.Cmd = [ 'shell:pm install', ...this.intentArgs(options), - escapeCompat(this.apk), + escapeCompat(apk), args ] .filter(Boolean) .join(' '); } - protected async postExecute(): Promise { - try { - const [, result, code] = await this.parser.searchLine( - /^(Success|Failure \[(.*?)\])$/ - ); - if (result !== 'Success') { - throw new Error(`${this.apk} could not be installed [${code}]`); - } - } finally { - await this.parser.readAll(); - this.endConnection(); - } + protected throwError(code: string): never { + throw new Error( + `${this.packageOrPath} could not be installed [${code}]` + ); } + private intentArgs(options: InstallOptions | void): string[] { const args: string[] = []; if (!options) { diff --git a/src/commands/host-transport/uninstall.ts b/src/commands/host-transport/uninstall.ts index 8b738779..aaeaeefd 100644 --- a/src/commands/host-transport/uninstall.ts +++ b/src/commands/host-transport/uninstall.ts @@ -1,31 +1,26 @@ import { Connection } from '../../connection'; import { UninstallOptions } from '../../util'; -import TransportCommand from '../abstract/transport'; +import PackageCommand from '../abstract/package'; -export default class UninstallCommand extends TransportCommand { - protected keepAlive = false; +export default class UninstallCommand extends PackageCommand { protected Cmd: string; constructor( connection: Connection, serial: string, pkg: string, - options: UninstallOptions = {} + options: UninstallOptions | void ) { - super(connection, serial); + super(connection, serial, pkg); this.Cmd = ['shell:pm uninstall'] - .concat(options.keepCache ? '-k' : []) + .concat(options?.keepCache ? '-k' : []) .concat(pkg) .join(' '); } - protected async postExecute(): Promise { - try { - await this.parser.searchLine( - /^(Success|Failure.*|.*Unknown package:.*)$/ - ); - } finally { - await this.parser.readAll(); - } + protected throwError(code: string): never { + throw new Error( + `${this.packageOrPath} could not be uninstalled [${code}]` + ); } } diff --git a/src/device.ts b/src/device.ts index bedd9abf..a2d12fc5 100644 --- a/src/device.ts +++ b/src/device.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/explicit-member-accessibility */ import { TransportCommandConstruct, CpOptions, @@ -20,7 +19,6 @@ import { TransportType, UninstallOptions, PropertyMap, - ValueCallback, PropertyValue, KeyEventOptions, InputDurationOptions, @@ -39,14 +37,14 @@ import SyncEntry from './sync/entry'; import { SyncMode } from './sync'; export class Device implements IDevice { - readonly id: string; - readonly state: DeviceState; - readonly path: string | undefined; - readonly device: string | undefined; - readonly model: string | undefined; - readonly product: string | undefined; - readonly transportId: string; - readonly transport: TransportType; + public readonly id: string; + public readonly state: DeviceState; + public readonly path: string | undefined; + public readonly device: string | undefined; + public readonly model: string | undefined; + public readonly product: string | undefined; + public readonly transportId: string; + public readonly transport: TransportType; private readonly client: Client; constructor(client: Client, props: IDevice) { @@ -61,90 +59,93 @@ export class Device implements IDevice { this.transport = props.transport; } - getSerialNo(): Promise { + public getSerialNo(): Promise { return this.client.getSerialNo(this.id); } - getDevicePath(): Promise { + public getDevicePath(): Promise { return this.client.getDevicePath(this.id); } - listProperties(): Promise { + public listProperties(): Promise { return this.client.listProperties(this.id); } - listFeatures(): Promise { + public listFeatures(): Promise { return this.client.listFeatures(this.id); } - listPackages(): Promise { + public listPackages(): Promise { return this.client.listPackages(this.id); } - getIpAddress(): Promise { + public getIpAddress(): Promise { return this.client.getIpAddress(this.id); } - forward(local: string, remote: string): Promise { + public forward(local: string, remote: string): Promise { return this.client.forward(this.id, local, remote); } - listForwards(): Promise { + public listForwards(): Promise { return this.client.listForwards(this.id); } - reverse(local: string, remote: string): Promise { + public reverse(local: string, remote: string): Promise { return this.client.reverse(this.id, local, remote); } - listReverses(): Promise { + public listReverses(): Promise { return this.client.listReverses(this.id); } - shell(command: string): Promise { + public shell(command: string): Promise { return this.client.shell(this.id, command); } - reboot(): Promise { + public reboot(): Promise { return this.client.reboot(this.id); } - shutdown(): Promise { + public shutdown(): Promise { return this.client.shutdown(this.id); } - remount(): Promise { + public remount(): Promise { return this.client.remount(this.id); } - root(): Promise { + public root(): Promise { return this.client.root(this.id); } - screenshot(): Promise { + public screenshot(): Promise { return this.client.screenshot(this.id); } - openTcp(port: number, host?: string): Promise { + public openTcp(port: number, host?: string): Promise { return this.client.openTcp(this.id, port, host as string); } - openLogcat(options?: LogcatOptions): Promise { + public openLogcat(options?: LogcatOptions): Promise { return this.client.openLogcat(this.id, options as LogcatOptions); } - clear(pkg: string): Promise { + public clear(pkg: string): Promise { return this.client.clear(this.id, pkg); } - install(apk: string | Readable): Promise; - install(apk: string | Readable, options?: InstallOptions): Promise; - install( + public install(apk: string | Readable): Promise; + public install( + apk: string | Readable, + options?: InstallOptions + ): Promise; + public install( apk: string | Readable, options?: InstallOptions, args?: string ): Promise; - install( + public install( apk: string | Readable, options?: InstallOptions, args?: string @@ -157,15 +158,15 @@ export class Device implements IDevice { ); } - uninstall(pkg: string, options?: UninstallOptions): Promise { + public uninstall(pkg: string, options?: UninstallOptions): Promise { return this.client.uninstall(this.id, pkg, options as UninstallOptions); } - isInstalled(pkg: string): Promise { + public isInstalled(pkg: string): Promise { return this.client.isInstalled(this.id, pkg); } - startActivity( + public startActivity( pkg: string, activity: string, options?: StartActivityOptions @@ -178,7 +179,7 @@ export class Device implements IDevice { ); } - startService( + public startService( pkg: string, service: string, options?: StartServiceOptions @@ -191,70 +192,73 @@ export class Device implements IDevice { ); } - readDir(path: string): Promise { + public readDir(path: string): Promise { return this.client.readDir(this.id, path); } - pushDataToFile( + public pushDataToFile( data: string | Buffer | Readable, destPath: string ): Promise { return this.client.pushDataToFile(this.id, data, destPath); } - pushFile(srcPath: string, destPath: string): Promise { + public pushFile(srcPath: string, destPath: string): Promise { return this.client.pushFile(this.id, srcPath, destPath); } - pullDataFromFile(srcPath: string): Promise { + public pullDataFromFile(srcPath: string): Promise { return this.client.pullDataFromFile(this.id, srcPath); } - pullFile(srcPath: string, destPath: string): Promise { + public pullFile(srcPath: string, destPath: string): Promise { return this.client.pullFile(this.id, srcPath, destPath); } - pull(path: string): Promise { + public pull(path: string): Promise { return this.client.pull(this.id, path); } - push( + public push( srcPath: string | Readable, destPath: string, - mode?: SyncMode | ValueCallback + mode?: SyncMode ): Promise { return this.client.push(this.id, srcPath, destPath, mode as SyncMode); } - tcpip(port = 5555): Promise { + public tcpip(port = 5555): Promise { return this.client.tcpip(this.id, port); } - usb(): Promise { + public usb(): Promise { return this.client.usb(this.id); } - waitBootComplete(): Promise { + public waitBootComplete(): Promise { return this.client.waitBootComplete(this.id); } - listSettings(mode: SettingsMode): Promise { + public listSettings(mode: SettingsMode): Promise { return this.client.listSettings(this.id, mode); } - getProp(prop: string): Promise { + public getProp(prop: string): Promise { return this.client.getProp(this.id, prop); } - setProp(prop: string, value: PrimitiveType): Promise { + public setProp(prop: string, value: PrimitiveType): Promise { return this.client.setProp(this.id, prop, value); } - getSetting(mode: SettingsMode, name: string): Promise { + public getSetting( + mode: SettingsMode, + name: string + ): Promise { return this.client.getSetting(this.id, mode, name); } - putSetting( + public putSetting( mode: SettingsMode, name: string, value: PrimitiveType @@ -262,22 +266,22 @@ export class Device implements IDevice { return this.client.putSetting(this.id, mode, name, value); } - tap(x: number, y: number, source?: InputSource): Promise { + public tap(x: number, y: number, source?: InputSource): Promise { return this.client.tap(this.id, x, y, source as InputSource); } - text(text: string, source?: InputSource): Promise { + public text(text: string, source?: InputSource): Promise { return this.client.text(this.id, text, source as InputSource); } - keyEvent( + public keyEvent( code: KeyCode | number | NonEmptyArray, options?: KeyEventOptions ): Promise { return this.client.keyEvent(this.id, code, options as KeyEventOptions); } - swipe( + public swipe( x1: number, y1: number, x2: number, @@ -294,7 +298,7 @@ export class Device implements IDevice { ); } - dragAndDrop( + public dragAndDrop( x1: number, y1: number, x2: number, @@ -311,15 +315,15 @@ export class Device implements IDevice { ); } - press(source?: InputSource): Promise { + public press(source?: InputSource): Promise { return this.client.press(this.id, source as InputSource); } - roll(x: number, y: number, source?: InputSource): Promise { + public roll(x: number, y: number, source?: InputSource): Promise { return this.client.roll(this.id, x, y, source as InputSource); } - custom( + public custom( CustomCommand: TransportCommandConstruct, ...args: P ): Promise { @@ -330,47 +334,55 @@ export class Device implements IDevice { ); } - openMonkey(): Promise { + public openMonkey(): Promise { return this.client.openMonkey(this.id); } - killApp(pkg: string): Promise { + public killApp(pkg: string): Promise { return this.client.killApp(this.id, pkg); } - exec(cmd: string): Promise { + public exec(cmd: string): Promise { return this.client.execDevice(this.id, cmd); } - execShell(cmd: string): Promise { + public execShell(cmd: string): Promise { return this.client.execDeviceShell(this.id, cmd); } - batteryStatus(): Promise { + public batteryStatus(): Promise { return this.client.batteryStatus(this.id); } - rm(path: string, options?: RmOptions): Promise { + public rm(path: string, options?: RmOptions): Promise { return this.client.rm(this.id, path, options as RmOptions); } - mkdir(path: string, options?: MkDirOptions): Promise { - return this.client.mkdir(this.id, path, options); + public mkdir(path: string, options?: MkDirOptions): Promise { + return this.client.mkdir(this.id, path, options as MkDirOptions); } - touch(path: string, options?: TouchOptions): Promise { + public touch(path: string, options?: TouchOptions): Promise { return this.client.touch(this.id, path, options as TouchOptions); } - mv(srcPath: string, destPath: string, options?: MvOptions): Promise { + public mv( + srcPath: string, + destPath: string, + options?: MvOptions + ): Promise { return this.client.mv(this.id, srcPath, destPath, options as MvOptions); } - cp(srcPath: string, destPath: string, options?: CpOptions): Promise { + public cp( + srcPath: string, + destPath: string, + options?: CpOptions + ): Promise { return this.client.cp(this.id, srcPath, destPath, options as CpOptions); } - fileStat(path: string): Promise { + public fileStat(path: string): Promise { return this.client.fileStat(this.id, path); } } diff --git a/src/parser.ts b/src/parser.ts index 24b153e0..79a593a8 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,9 +1,8 @@ import { decodeLength } from './util//functions'; -import { PrematureEOFError, UnexpectedDataError } from './util'; +import { PrematureEOFError, UnexpectedDataError, autoUnregister } from './util'; import { Writable } from 'stream'; import { Socket } from 'net'; import T from 'timers/promises'; -import EventUnregister from './util/eventUnregister'; export class Parser { public readonly socket: Socket; @@ -12,65 +11,61 @@ export class Parser { this.socket = socket; } public readBytes(howMany: number): Promise { - const eventUnregister = new EventUnregister(this.socket); - const promise = new Promise((resolve, reject) => { - const tryRead = (): void => { - if (!howMany) { - return resolve(Buffer.alloc(0)); - } - - const chunk = this.socket.read(howMany); - if (chunk) { - howMany -= chunk.length; - if (howMany === 0) { - return resolve(chunk); - } - } - - if (this.ended) { - return reject(new PrematureEOFError(howMany)); - } - }; - - eventUnregister.register((socket) => - socket - .on('readable', tryRead) - .on('error', reject) - .on('end', (): void => { - this.ended = true; - reject(new PrematureEOFError(howMany)); - }) - ); + return autoUnregister( + this.socket, + (socket) => + new Promise((resolve, reject) => { + const tryRead = (): void => { + if (!howMany) { + return resolve(Buffer.alloc(0)); + } + const chunk = this.socket.read(howMany); + if (chunk) { + howMany -= chunk.length; + if (howMany === 0) { + return resolve(chunk); + } + } - tryRead(); - }); - return eventUnregister.unregisterAfter(promise); + if (this.ended) { + return reject(new PrematureEOFError(howMany)); + } + }; + socket + .on('readable', tryRead) + .on('error', reject) + .on('end', (): void => { + this.ended = true; + reject(new PrematureEOFError(howMany)); + }); + tryRead(); + }) + ); } public end(): Promise { - const eventUnregister = new EventUnregister(this.socket); - const promise = new Promise((resolve, reject) => { - eventUnregister.register((socket) => - socket - .on('readable', (): void => { - while (this.socket.read()) { - continue; - } - }) - .on('error', reject) - .on('end', (): void => { - this.ended = true; - resolve(); - }) - ); - if (this.ended) { - return resolve(); - } - - this.socket.read(0); - this.socket.end(); - }); - return eventUnregister.unregisterAfter(promise); + return autoUnregister( + this.socket, + (socket) => + new Promise((resolve, reject) => { + socket + .on('readable', (): void => { + while (this.socket.read()) { + continue; + } + }) + .on('error', reject) + .on('end', (): void => { + this.ended = true; + resolve(); + }); + if (this.ended) { + return resolve(); + } + this.socket.read(0); + this.socket.end(); + }) + ); } public async readAscii(howMany: number): Promise { @@ -88,7 +83,7 @@ export class Parser { T.setTimeout(1000, new Error('Could not read error'), { ref: false }), - this.readValue().then((value) => new Error(value.toString())) + this.readValue().then(String).then(Error) ]); } @@ -100,42 +95,44 @@ export class Parser { howMany: number, targetStream: Writable ): Promise { - const eventUnregister = new EventUnregister(this.socket); - const promise = new Promise((resolve, reject) => { - const tryRead = (): void => { - if (!howMany) { - return resolve(); - } + return autoUnregister( + this.socket, + (socket) => + new Promise((resolve, reject) => { + const tryRead = (): void => { + if (!howMany) { + return resolve(); + } - const readAll = ( - chunk = this.socket.read(howMany) || this.socket.read() - ): boolean => { - if (!chunk) { - return false; - } - howMany -= chunk.length; - targetStream.write(chunk); - return howMany === 0 || readAll(); - }; - if (readAll()) { - return resolve(); - } - if (this.ended) { - return reject(new PrematureEOFError(howMany)); - } - }; - eventUnregister.register((socket) => - socket - .on('readable', tryRead) - .on('error', reject) - .on('end', (): void => { - this.ended = true; - reject(new PrematureEOFError(howMany)); - }) - ); - tryRead(); - }); - return eventUnregister.unregisterAfter(promise); + const readAll = ( + chunk = this.socket.read(howMany) || + this.socket.read() + ): boolean => { + if (!chunk) { + return false; + } + howMany -= chunk.length; + targetStream.write(chunk); + return howMany === 0 || readAll(); + }; + if (readAll()) { + return resolve(); + } + if (this.ended) { + return reject(new PrematureEOFError(howMany)); + } + }; + socket + .on('readable', tryRead) + .on('error', reject) + .on('end', (): void => { + this.ended = true; + reject(new PrematureEOFError(howMany)); + }); + + tryRead(); + }) + ); } private readUntil(code: number): Promise { @@ -173,33 +170,27 @@ export class Parser { } public readAll(): Promise { - const eventUnregister = new EventUnregister(this.socket); - const promise = new Promise((resolve, reject) => { - let all = Buffer.alloc(0); - const tryRead = (): void => { - const read = (acc: Buffer): Buffer => { - const chunk = this.socket.read(); - if (chunk) { - return read(Buffer.concat([acc, chunk])); - } - return acc; - }; - all = read(all); - - if (this.ended) { - return resolve(all); - } - }; - eventUnregister.register((socket) => - socket - .on('readable', tryRead) - .on('error', reject) - .on('end', (): void => { - this.ended = true; - return resolve(all); - }) - ); - }); - return eventUnregister.unregisterAfter(promise); + return autoUnregister( + this.socket, + (socket) => + new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + const tryRead = (): void => { + while (this.socket.readableLength) { + chunks.push(this.socket.read()); + } + if (this.ended) { + return resolve(Buffer.concat(chunks)); + } + }; + socket + .on('readable', tryRead) + .on('error', reject) + .on('end', (): void => { + this.ended = true; + return resolve(Buffer.concat(chunks)); + }); + }) + ); } } diff --git a/src/sync/sync.ts b/src/sync/sync.ts index b5b746af..c4d93335 100644 --- a/src/sync/sync.ts +++ b/src/sync/sync.ts @@ -1,4 +1,4 @@ -import { Reply } from '../util'; +import { Reply, autoUnregister } from '../util'; import { Connection } from '../connection'; import { EventEmitter } from 'events'; import { Parser } from '../parser'; @@ -10,7 +10,6 @@ import Stats from './stats'; import SyncEntry from './entry'; import fs from 'fs'; import { promisify } from 'util'; -import EventUnregister from '../util/eventUnregister'; export enum SyncMode { DEFAULT_CHMOD = 0x1a4, @@ -78,9 +77,6 @@ export class Sync extends EventEmitter { let canceled = false; const writeData = async (): Promise => { - const streamUnregister = new EventUnregister(stream); - const connectionUnregister = new EventUnregister(this.connection); - const { waitForDrain, unregisterDrainListener } = this.getDrainAwaiter(); const promise = new Promise((resolve, reject) => { @@ -97,26 +93,24 @@ export class Sync extends EventEmitter { return writeNext(); } }; - streamUnregister.register((stream_) => - stream_ - .on('end', (): void => { - this.sendCommandWithLength(Reply.DONE, timestamp); - resolve(); - }) - .on('readable', writeNext) - .on('error', reject) - ); - connectionUnregister.register((connection) => - connection.on('error', (err): void => { - stream.destroy(); - this.connection.end(); - reject(err); + + stream + .on('end', (): void => { + this.sendCommandWithLength(Reply.DONE, timestamp); + resolve(); }) - ); + .on('readable', writeNext) + .on('error', reject); + + this.connection.on('error', (err): void => { + stream.destroy(); + this.connection.end(); + reject(err); + }); }); await Promise.all([ - streamUnregister.unregisterAfter(promise), - connectionUnregister.unregisterAfter(promise) + autoUnregister(stream, promise), + autoUnregister(this.connection, promise) ]); unregisterDrainListener(); }; @@ -155,7 +149,7 @@ export class Sync extends EventEmitter { private pushStream( stream: Readable, path: string, - mode?: SyncMode | null + mode?: SyncMode ): PushTransfer { if (mode == null) { mode = SyncMode.DEFAULT_CHMOD; @@ -168,7 +162,7 @@ export class Sync extends EventEmitter { private pushFile( file: string, path: string, - mode?: SyncMode | null + mode?: SyncMode | void ): PushTransfer { if (mode === null) { mode = SyncMode.DEFAULT_CHMOD; @@ -180,7 +174,7 @@ export class Sync extends EventEmitter { public push( contents: string | Readable, path: string, - mode: SyncMode | null = null + mode?: SyncMode ): PushTransfer { if (typeof contents === 'string') { return this.pushFile(contents, path, mode); diff --git a/src/tracker.ts b/src/tracker.ts index d0ce46ea..f51d2d34 100644 --- a/src/tracker.ts +++ b/src/tracker.ts @@ -10,7 +10,7 @@ export class Tracker extends EventEmitter { private readonly command: TrackCommand; private ended = false; // assigning to null prevents emitting 'add' events on first read - private deviceMap: Map | null = null; + private deviceMap: Map | null = null; private readonly client: Client; /** @ignore */ constructor(command: TrackCommand, client: Client) { @@ -20,6 +20,10 @@ export class Tracker extends EventEmitter { this.hook(); } + public get Devices(): Device[] { + return Array.from(this.deviceMap?.values() || []); + } + private async hook(): Promise { // Listener for error not needed, error is handled in catch for read() // this.command.connection.on('error', (err) => this.emit('error', err)); @@ -56,16 +60,19 @@ export class Tracker extends EventEmitter { private update(list: IDevice[]): void { const newMap = list.reduce((map, d) => { - const oldDevice = this.deviceMap?.get(d.id); - map.set(d.id, d); + const currentDevice = + this.deviceMap?.get(d.id) || new Device(this.client, d); + + map.set(d.id, currentDevice); - if (oldDevice && d.state !== oldDevice.state) { - this.emit('change', new Device(this.client, d)); + if (d.state !== currentDevice.state) { + (currentDevice as IDevice).state = d.state; + this.emit('change', currentDevice); return map; } - if (this.deviceMap) { - this.emit('add', new Device(this.client, d)); + if (this.deviceMap && !this.deviceMap.has(d.id)) { + this.emit('add', currentDevice); return map; } @@ -74,7 +81,9 @@ export class Tracker extends EventEmitter { this.deviceMap?.forEach((d) => { if (!newMap.has(d.id)) { - this.emit('remove', d); + const deviceObject = { ...d } as Record; + delete deviceObject.client; + this.emit('remove', deviceObject); } }); diff --git a/src/util/autoEventUnregister.ts b/src/util/autoEventUnregister.ts new file mode 100644 index 00000000..2ba01c64 --- /dev/null +++ b/src/util/autoEventUnregister.ts @@ -0,0 +1,40 @@ +import EventEmitter from 'events'; + +const autoUnregister = async ( + emitter: T | Promise, + action: Promise | ((emitter: T) => Promise) +): Promise => { + const emitter_ = await emitter; + const getListeners = (): Map< + string | symbol, + ((...args: unknown[]) => void)[] + > => { + return emitter_ + .eventNames() + .reduce( + (map, event) => map.set(event, emitter_.listeners(event)), + new Map() + ); + }; + const prevListeners = getListeners(); + + const promise = action instanceof Promise ? action : action(emitter_); + const offListeners: [string | symbol, ((...args: unknown[]) => void)[]][] = + [...getListeners()].map(([event, listeners]) => [ + event, + listeners.filter( + (listener) => !prevListeners.get(event)?.includes(listener) + ) + ]); + try { + return await promise; + } finally { + offListeners.forEach(([event, listeners]) => { + listeners.forEach((listener) => { + emitter_.off(event, listener); + }); + }); + } +}; + +export default autoUnregister; diff --git a/src/util/eventUnregister.ts b/src/util/eventUnregister.ts deleted file mode 100644 index eb713b27..00000000 --- a/src/util/eventUnregister.ts +++ /dev/null @@ -1,54 +0,0 @@ -import EventEmitter from 'events'; - -export default class EventUnregister { - private instance: T; - private offListeners: [ - string | symbol, - ((...args: unknown[]) => void)[] - ][] = []; - constructor(instance: T) { - this.instance = instance; - } - - private getListenerMap(): Map< - string | symbol, - ((...args: unknown[]) => void)[] - > { - return this.instance - .eventNames() - .reduce( - (map, event) => map.set(event, this.instance.listeners(event)), - new Map() - ); - } - - public register(cb: (instance: T) => T): void { - const prevListeners = this.getListenerMap(); - cb(this.instance); - - this.offListeners = [...this.getListenerMap()].map( - ([event, listeners]) => [ - event, - listeners.filter( - (listener) => !prevListeners.get(event)?.includes(listener) - ) - ] - ); - } - - private unregister(): void { - this.offListeners.forEach(([event, listeners]) => { - listeners.forEach((listener) => { - this.instance.off(event, listener); - }); - }); - } - - public async unregisterAfter(promise: Promise): Promise { - try { - return await promise; - } finally { - this.unregister(); - } - } -} diff --git a/src/util/functions.ts b/src/util/functions.ts index 181e3f5c..f612ddfb 100644 --- a/src/util/functions.ts +++ b/src/util/functions.ts @@ -1,14 +1,4 @@ -import { callbackify } from 'util'; -import { - Callback, - ValueCallback, - InputOptions, - InputSource, - PropertyMap, - PropertyValue, - NonFunctionProperties, - PrimitiveType -} from './types'; +import { PropertyMap, PropertyValue, PrimitiveType } from './types'; export const decodeLength = (length: string): number => { return parseInt(length, 16); @@ -25,7 +15,7 @@ export const encodeData = (data: Buffer | string): Buffer => { ]); }; -export const stringToType = (value = ''): PropertyValue => { +export const stringToType = (value: string): PropertyValue => { try { const parsed = JSON.parse(value); if ( @@ -44,32 +34,6 @@ export const stringToType = (value = ''): PropertyValue => { } }; -export const nodeify: ( - promise: Promise, - cb: ((err: null | Error, value: T) => void) | undefined -) => Promise | void = (promise, cb) => { - return cb ? callbackify(() => promise)(cb) : promise; -}; - -export const parseValueParam = | string, R>( - param: T | ValueCallback | Callback | undefined -): T | undefined => { - if (typeof param === 'function') { - return; - } - return param; -}; - -export const parseCbParam = | string, R>( - param: T | ValueCallback | undefined, - cb: ValueCallback | undefined -): ValueCallback | undefined => { - if (typeof param === 'function') { - return param; - } - return cb; -}; - export const parsePrimitiveParam = (def: T, param: T | undefined): T => { if (typeof param === 'undefined') { return def; @@ -115,38 +79,6 @@ export function findMatches( } } -export function buildFsParams( - options: T | Callback | undefined, - cb: Callback | undefined -): { - options_: T | undefined; - cb_: Callback | undefined; -} { - if (typeof options === 'function') { - return { options_: undefined, cb_: options }; - } - if (typeof options === 'object') { - return { options_: options, cb_: cb }; - } - return { options_: undefined, cb_: cb }; -} - -export function buildInputParams( - params: T | Callback | undefined, - cb: Callback | undefined -): { - params: T | undefined; - cb_: Callback | undefined; -} { - if (typeof params === 'function') { - return { params: undefined, cb_: params }; - } - if (typeof params !== 'undefined') { - return { params, cb_: cb }; - } - return { params: undefined, cb_: cb }; -} - export function escape(arg: PrimitiveType): string { switch (typeof arg) { case 'undefined': diff --git a/src/util/index.ts b/src/util/index.ts index 1d2e6638..7c19f865 100644 --- a/src/util/index.ts +++ b/src/util/index.ts @@ -2,3 +2,5 @@ export * from './errors'; export * from './functions'; export * from './types'; export * from './keycode'; +import autoUnregister from './autoEventUnregister'; +export { autoUnregister }; diff --git a/src/util/types.ts b/src/util/types.ts index bea61b8e..61c839e1 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -17,10 +17,6 @@ export enum Reply { QUIT = 'QUIT' } -export type Callback = (err: null | Error) => void; - -export type ValueCallback = (err: null | Error, value: T) => void; - export type DeviceState = | 'offline' | 'device' @@ -326,11 +322,11 @@ export interface MkDirOptions { export interface TouchOptions extends SymlinkFSoption { /** - * Adds `-a` flag. Changes access time. + * Adds `-a` flag. Changes access time. UTC time. */ aTime?: boolean; /** - * Adds `-m` flag. Changes modification time. + * Adds `-m` flag. Changes modification time. UTC time. */ mTime?: boolean; /** @@ -338,11 +334,11 @@ export interface TouchOptions extends SymlinkFSoption { */ noCreate?: boolean; /** - * Adds `-d ` flag. + * Adds `-d ` flag. UTC time. */ date?: Date | string; /** - * Adds `-t