chore: convert to webpack

This commit is contained in:
Simon Chan 2020-08-20 22:00:09 +08:00
parent 9420b907af
commit 33cb585264
49 changed files with 7598 additions and 1766 deletions

View file

@ -16,7 +16,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [13.x] node-version: [14.x]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@ -28,6 +28,6 @@ jobs:
env: env:
CI: true CI: true
- run: npm run build --if-present - run: npm run build --if-present
- run: npm test # - run: npm test
env: # env:
CI: true # CI: true

4
.gitignore vendored
View file

@ -1,2 +1,4 @@
node_modules node_modules
# lib lib
*.log
tsconfig.tsbuildinfo

11
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,11 @@
{
"cSpell.words": [
"fluentui",
"reimplement",
"webadb",
"webusb",
"wifi",
"wirelessly",
"yume"
]
}

View file

@ -10,7 +10,7 @@ Currently only the interactive shell (`adb shell`) is implemented, but I think i
WebUSB API gives JavaScript running in supported web browsers access to USB devices, including Android phones. WebUSB API gives JavaScript running in supported web browsers access to USB devices, including Android phones.
ADB uses a fairly simple protocol to commnunicate, so it's pretty easy to reimplement with JavaScript. ADB uses a fairly simple protocol to communicate, so it's pretty easy to reimplement with JavaScript.
`adb shell`, the interactive shell, uses plain PTY protocol, and [xterm.js](https://github.com/xtermjs/xterm.js/) can handle it very well. `adb shell`, the interactive shell, uses plain PTY protocol, and [xterm.js](https://github.com/xtermjs/xterm.js/) can handle it very well.
@ -28,7 +28,7 @@ npm start
And navigate to `http://localhost:8080/test.html`. And navigate to `http://localhost:8080/test.html`.
WebUSB API requires a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts) (basicly means HTTPS). WebUSB API requires a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts) (basically means HTTPS).
Chrome will treat `localhost` as one, but if you want to access test server running on another machine, you can configure you Chrome as following: Chrome will treat `localhost` as one, but if you want to access test server running on another machine, you can configure you Chrome as following:

6
lerna.json Normal file
View file

@ -0,0 +1,6 @@
{
"packages": [
"packages/*"
],
"version": "0.0.0"
}

View file

@ -1,171 +0,0 @@
/**
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
* https://github.com/chjj/term.js
* @license MIT
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* Originally forked from (with the author's permission):
* Fabrice Bellard's javascript vt100 for jslinux:
* http://bellard.org/jslinux/
* Copyright (c) 2011 Fabrice Bellard
* The original design remains. The terminal itself
* has been extended to include xterm CSI codes, among
* other features.
*/
/**
* Default styles for xterm.js
*/
.xterm {
font-feature-settings: "liga" 0;
position: relative;
user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
}
.xterm.focus,
.xterm:focus {
outline: none;
}
.xterm .xterm-helpers {
position: absolute;
top: 0;
/**
* The z-index of the helpers must be higher than the canvases in order for
* IMEs to appear on top.
*/
z-index: 5;
}
.xterm .xterm-helper-textarea {
/*
* HACK: to fix IE's blinking cursor
* Move textarea out of the screen to the far left, so that the cursor is not visible.
*/
position: absolute;
opacity: 0;
left: -9999em;
top: 0;
width: 0;
height: 0;
z-index: -5;
/** Prevent wrapping so the IME appears against the textarea at the correct position */
white-space: nowrap;
overflow: hidden;
resize: none;
}
.xterm .composition-view {
/* TODO: Composition position got messed up somewhere */
background: #000;
color: #FFF;
display: none;
position: absolute;
white-space: nowrap;
z-index: 1;
}
.xterm .composition-view.active {
display: block;
}
.xterm .xterm-viewport {
/* On OS X this is required in order for the scroll bar to appear fully opaque */
background-color: #000;
overflow-y: scroll;
cursor: default;
position: absolute;
right: 0;
left: 0;
top: 0;
bottom: 0;
}
.xterm .xterm-screen {
position: relative;
}
.xterm .xterm-screen canvas {
position: absolute;
left: 0;
top: 0;
}
.xterm .xterm-scroll-area {
visibility: hidden;
}
.xterm-char-measure-element {
display: inline-block;
visibility: hidden;
position: absolute;
top: 0;
left: -9999em;
line-height: normal;
}
.xterm {
cursor: text;
}
.xterm.enable-mouse-events {
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
cursor: default;
}
.xterm.xterm-cursor-pointer {
cursor: pointer;
}
.xterm.column-select.focus {
/* Column selection mode */
cursor: crosshair;
}
.xterm .xterm-accessibility,
.xterm .xterm-message {
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
z-index: 10;
color: transparent;
}
.xterm .live-region {
position: absolute;
left: -9999px;
width: 1px;
height: 1px;
overflow: hidden;
}
.xterm-dim {
opacity: 0.5;
}
.xterm-underline {
text-decoration: underline;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

550
package-lock.json generated
View file

@ -1,550 +0,0 @@
{
"name": "webadb.js",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@rollup/plugin-commonjs": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-14.0.0.tgz",
"integrity": "sha512-+PSmD9ePwTAeU106i9FRdc+Zb3XUWyW26mo5Atr2mk82hor8+nPwkztEjFo8/B1fJKfaQDg9aM2bzQkjhi7zOw==",
"dev": true,
"requires": {
"@rollup/pluginutils": "^3.0.8",
"commondir": "^1.0.1",
"estree-walker": "^1.0.1",
"glob": "^7.1.2",
"is-reference": "^1.1.2",
"magic-string": "^0.25.2",
"resolve": "^1.11.0"
}
},
"@rollup/plugin-node-resolve": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-8.4.0.tgz",
"integrity": "sha512-LFqKdRLn0ShtQyf6SBYO69bGE1upV6wUhBX0vFOUnLAyzx5cwp8svA0eHUnu8+YU57XOkrMtfG63QOpQx25pHQ==",
"dev": true,
"requires": {
"@rollup/pluginutils": "^3.1.0",
"@types/resolve": "1.17.1",
"builtin-modules": "^3.1.0",
"deep-freeze": "^0.0.1",
"deepmerge": "^4.2.2",
"is-module": "^1.0.0",
"resolve": "^1.17.0"
},
"dependencies": {
"@rollup/pluginutils": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz",
"integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==",
"dev": true,
"requires": {
"@types/estree": "0.0.39",
"estree-walker": "^1.0.1",
"picomatch": "^2.2.2"
}
},
"resolve": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz",
"integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==",
"dev": true,
"requires": {
"path-parse": "^1.0.6"
}
}
}
},
"@rollup/plugin-typescript": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-3.1.1.tgz",
"integrity": "sha512-VPY1MbzIJT+obpav9Kns4MlipVJ1FuefwzO4s1uCVXAzVWya+bhhNauOmmqR/hy1zj7tePfh3t9iBN+HbIzyRA==",
"dev": true,
"requires": {
"@rollup/pluginutils": "^3.0.1",
"resolve": "^1.14.1"
}
},
"@rollup/pluginutils": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.0.10.tgz",
"integrity": "sha512-d44M7t+PjmMrASHbhgpSbVgtL6EFyX7J4mYxwQ/c5eoaE6N2VgCgEcWVzNnwycIloti+/MpwFr8qfw+nRw00sw==",
"dev": true,
"requires": {
"@types/estree": "0.0.39",
"estree-walker": "^1.0.1",
"picomatch": "^2.2.2"
}
},
"@types/estree": {
"version": "0.0.39",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz",
"integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==",
"dev": true
},
"@types/node": {
"version": "14.0.23",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.23.tgz",
"integrity": "sha512-Z4U8yDAl5TFkmYsZdFPdjeMa57NOvnaf1tljHzhouaPEp7LCj2JKkejpI1ODviIAQuW4CcQmxkQ77rnLsOOoKw==",
"dev": true
},
"@types/resolve": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
"integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/w3c-web-usb": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.4.tgz",
"integrity": "sha512-aaOB3EL5WCWBBOYX7W1MKuzspOM9ZJI9s3iziRVypr1N+QyvIgXzCM4lm1iiOQ1VFzZioUPX9bsa23myCbKK4A==",
"dev": true
},
"@yume-chan/async-operation-manager": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@yume-chan/async-operation-manager/-/async-operation-manager-1.0.3.tgz",
"integrity": "sha512-FWzejjiDb3o1kpuruRCKhwh2ho5KOmix1GwGGDov8hUqUw6ODNeD3OszqxfPiVLAmtJ1Ib35/FREv14JndV6UA=="
},
"async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"dev": true,
"requires": {
"lodash": "^4.17.14"
}
},
"at-least-node": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
"integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
"dev": true
},
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true
},
"basic-auth": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-1.1.0.tgz",
"integrity": "sha1-RSIe5Cn37h5QNb4/UVM/HN/SmIQ=",
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"builtin-modules": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.1.0.tgz",
"integrity": "sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==",
"dev": true
},
"colors": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
"integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==",
"dev": true
},
"commondir": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
"integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=",
"dev": true
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true
},
"corser": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz",
"integrity": "sha1-jtolLsqrWEDc2XXOuQ2TcMgZ/4c=",
"dev": true
},
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"dev": true,
"requires": {
"ms": "^2.1.1"
}
},
"deep-freeze": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/deep-freeze/-/deep-freeze-0.0.1.tgz",
"integrity": "sha1-OgsABd4YZygZ39OM0x+RF5yJPoQ=",
"dev": true
},
"deepmerge": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
"dev": true
},
"ecstatic": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/ecstatic/-/ecstatic-3.3.2.tgz",
"integrity": "sha512-fLf9l1hnwrHI2xn9mEDT7KIi22UDqA2jaCwyCbSUJh9a1V+LEUSL/JO/6TIz/QyuBURWUHrFL5Kg2TtO1bkkog==",
"dev": true,
"requires": {
"he": "^1.1.1",
"mime": "^1.6.0",
"minimist": "^1.1.0",
"url-join": "^2.0.5"
}
},
"estree-walker": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz",
"integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==",
"dev": true
},
"eventemitter3": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.4.tgz",
"integrity": "sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ==",
"dev": true
},
"follow-redirects": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.11.0.tgz",
"integrity": "sha512-KZm0V+ll8PfBrKwMzdo5D13b1bur9Iq9Zd/RMmAoQQcl2PxxFml8cxXPaaPYVbV0RjNjq1CU7zIzAOqtUPudmA==",
"dev": true,
"requires": {
"debug": "^3.0.0"
}
},
"fs-extra": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.0.tgz",
"integrity": "sha512-pmEYSk3vYsG/bF651KPUXZ+hvjpgWYw/Gc7W9NFUe3ZVLczKKWIij3IKpOrQcdw4TILtibFslZ0UmR8Vvzig4g==",
"dev": true,
"requires": {
"at-least-node": "^1.0.0",
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^1.0.0"
}
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true
},
"fsevents": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz",
"integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==",
"dev": true,
"optional": true
},
"glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"graceful-fs": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz",
"integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==",
"dev": true
},
"he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"dev": true
},
"http-proxy": {
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
"integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
"dev": true,
"requires": {
"eventemitter3": "^4.0.0",
"follow-redirects": "^1.0.0",
"requires-port": "^1.0.0"
}
},
"http-server": {
"version": "0.12.3",
"resolved": "https://registry.npmjs.org/http-server/-/http-server-0.12.3.tgz",
"integrity": "sha512-be0dKG6pni92bRjq0kvExtj/NrrAd28/8fCXkaI/4piTwQMSDSLMhWyW0NI1V+DBI3aa1HMlQu46/HjVLfmugA==",
"dev": true,
"requires": {
"basic-auth": "^1.0.3",
"colors": "^1.4.0",
"corser": "^2.0.1",
"ecstatic": "^3.3.2",
"http-proxy": "^1.18.0",
"minimist": "^1.2.5",
"opener": "^1.5.1",
"portfinder": "^1.0.25",
"secure-compare": "3.0.1",
"union": "~0.5.0"
}
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"dev": true,
"requires": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
},
"is-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
"integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=",
"dev": true
},
"is-reference": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
"integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
"dev": true,
"requires": {
"@types/estree": "*"
}
},
"jsonfile": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz",
"integrity": "sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.6",
"universalify": "^1.0.0"
}
},
"lodash": {
"version": "4.17.19",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==",
"dev": true
},
"magic-string": {
"version": "0.25.7",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",
"integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==",
"dev": true,
"requires": {
"sourcemap-codec": "^1.4.4"
}
},
"mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"dev": true
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true
},
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"dev": true,
"requires": {
"minimist": "^1.2.5"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"requires": {
"wrappy": "1"
}
},
"opener": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/opener/-/opener-1.5.1.tgz",
"integrity": "sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA==",
"dev": true
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"dev": true
},
"path-parse": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
"dev": true
},
"picomatch": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
"integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==",
"dev": true
},
"portfinder": {
"version": "1.0.26",
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.26.tgz",
"integrity": "sha512-Xi7mKxJHHMI3rIUrnm/jjUgwhbYMkp/XKEcZX3aG4BrumLpq3nmoQMX+ClYnDZnZ/New7IatC1no5RX0zo1vXQ==",
"dev": true,
"requires": {
"async": "^2.6.2",
"debug": "^3.1.1",
"mkdirp": "^0.5.1"
}
},
"qs": {
"version": "6.9.4",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz",
"integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==",
"dev": true
},
"requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
"dev": true
},
"resolve": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.0.tgz",
"integrity": "sha512-+hTmAldEGE80U2wJJDC1lebb5jWqvTYAfm3YZ1ckk1gBr0MnCqUKlwK1e+anaFljIl+F5tR5IoZcm4ZDA1zMQw==",
"dev": true,
"requires": {
"path-parse": "^1.0.6"
}
},
"rollup": {
"version": "2.23.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.23.0.tgz",
"integrity": "sha512-vLNmZFUGVwrnqNAJ/BvuLk1MtWzu4IuoqsH9UWK5AIdO3rt8/CSiJNvPvCIvfzrbNsqKbNzPAG1V2O4eTe2XZg==",
"dev": true,
"requires": {
"fsevents": "~2.1.2"
}
},
"rollup-plugin-css-only": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/rollup-plugin-css-only/-/rollup-plugin-css-only-2.1.0.tgz",
"integrity": "sha512-pfdcqAWEmRMFy+ABXAQPA/DKyPqLuBTOf+lWSOgtrVs1v/q7DSXzYa9QZg4myd8/1F7NHcdvPkWnfWqMxq9vrw==",
"dev": true,
"requires": {
"@rollup/pluginutils": "^3.0.0",
"fs-extra": "^9.0.0"
}
},
"secure-compare": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz",
"integrity": "sha1-8aAymzCLIh+uN7mXTz1XjQypmeM=",
"dev": true
},
"sourcemap-codec": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
"dev": true
},
"tslib": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz",
"integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g=="
},
"typescript": {
"version": "3.9.7",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz",
"integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==",
"dev": true
},
"union": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz",
"integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==",
"dev": true,
"requires": {
"qs": "^6.4.0"
}
},
"universalify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz",
"integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==",
"dev": true
},
"url-join": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/url-join/-/url-join-2.0.5.tgz",
"integrity": "sha1-WvIvGMBSoACkjXuCxenC4v7tpyg=",
"dev": true
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true
},
"xterm": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/xterm/-/xterm-4.6.0.tgz",
"integrity": "sha512-98211RIDrAECqpsxs6gbilwMcxLtxSDIvtzZUIqP1xIByXtuccJ4pmMhHGJATZeEGe/reARPMqwPINK8T7jGZg=="
},
"xterm-addon-search": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/xterm-addon-search/-/xterm-addon-search-0.7.0.tgz",
"integrity": "sha512-6060evmJJ+tZcjnx33FXaeEHLpuXEa7l9UzUsYfMlCKbu88AbE+5LJocTKCHYd71cwCwb9pjmv/G1o9Rf9Zbcg=="
}
}
}

View file

@ -1,37 +1,14 @@
{ {
"name": "webadb.js", "name": "root",
"version": "1.0.0", "private": true,
"description": "``` let webusb = await Adb.open(\"WebUSB\"); let adb = await webusb.connectAdb(\"host::\"); let shell = await adb.shell(\"uname -a\"); console.log(await shell.receive()); ```",
"main": "webadb.js",
"scripts": { "scripts": {
"start": "hs", "postinstall": "lerna bootstrap",
"build": "rollup -c" "build": "lerna run --scope @yume-chan/webadb build",
"build:watch": "lerna run --scope @yume-chan/webadb --stream build:watch",
"build:demo": "lerna run --scope demo --stream build",
"start:demo": "lerna run --scope demo --stream start"
}, },
"repository": {
"type": "git",
"url": "git+https://github.com/webadb/webadb.js.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/webadb/webadb.js/issues"
},
"homepage": "https://github.com/webadb/webadb.js#readme",
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "14.0.0", "lerna": "^3.22.1"
"@rollup/plugin-node-resolve": "8.4.0",
"@rollup/plugin-typescript": "3.1.1",
"@types/w3c-web-usb": "1.0.4",
"http-server": "0.12.3",
"rollup": "2.23.0",
"rollup-plugin-css-only": "2.1.0",
"typescript": "3.9.7"
},
"dependencies": {
"@yume-chan/async-operation-manager": "^1.0.3",
"tslib": "^2.0.0",
"xterm": "^4.6.0",
"xterm-addon-search": "0.7.0"
} }
} }

23
packages/demo/README.md Normal file
View file

@ -0,0 +1,23 @@
# WebADB Demo
## Start
In `demo` folder:
```sh
npm start
```
Or in root folder:
```sh
npm run start:demo
```
## Watch changes in `webadb`
From another shell, run in root folder:
```sh
npm run build:watch
```

5880
packages/demo/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,44 @@
{
"name": "demo",
"version": "0.0.1",
"private": true,
"description": "Demo for WebADB",
"author": "Simon Chan <cnsimonchan@live.com>",
"homepage": "https://github.com/yume-chan/ya-webadb#readme",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/yume-chan/ya-webadb.git"
},
"scripts": {
"start": "webpack-dev-server",
"build": "webpack --mode production",
"build:dev": "webpack"
},
"bugs": {
"url": "https://github.com/yume-chan/ya-webadb/issues"
},
"devDependencies": {
"@types/react": "^16.9.46",
"@types/react-dom": "^16.9.8",
"@types/react-router-dom": "^5.1.5",
"css-loader": "^4.2.1",
"mini-css-extract-plugin": "^0.10.0",
"ts-loader": "^8.0.2",
"typescript": "^3.9.7",
"webpack": "^4.44.1",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0"
},
"dependencies": {
"@fluentui/react": "^7.129.0",
"@uifabric/react-hooks": "^7.9.0",
"@yume-chan/webadb": "^0.0.1",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-router-dom": "^5.2.0",
"xterm": "^4.8.1",
"xterm-addon-fit": "^0.4.0",
"xterm-addon-search": "^0.7.0"
}
}

View file

@ -0,0 +1,48 @@
import React, { CSSProperties, HTMLAttributes, PropsWithChildren, useCallback, useRef } from 'react';
export interface ResizeObserverProps extends HTMLAttributes<HTMLDivElement>, PropsWithChildren<{}> {
onResize: () => void;
}
const iframeStyle: CSSProperties = {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
visibility: 'hidden',
};
export default function ResizeObserver({ onResize, style, children, ...rest }: ResizeObserverProps): JSX.Element | null {
const onResizeRef = useRef<() => void>(onResize);
onResizeRef.current = onResize;
const handleResize = useCallback(() => {
onResizeRef.current();
}, []);
const handleIframeRef = useCallback((element: HTMLIFrameElement | null) => {
if (element) {
element.contentWindow!.addEventListener('resize', handleResize);
}
}, []);
const containerStyle: CSSProperties = React.useMemo(() => {
if (!style) {
return { position: 'relative' };
}
if (!style.position) {
return { ...style, position: 'relative' };
}
return style;
}, [style]);
return (
<div style={containerStyle} {...rest}>
<iframe ref={handleIframeRef} style={iframeStyle} />
{children}
</div>
)
}

View file

@ -0,0 +1,76 @@
import { WebAdb } from '@yume-chan/webadb';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useRouteMatch } from 'react-router-dom';
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
// import { SearchAddon } from 'xterm-addon-search';
import 'xterm/css/xterm.css';
import ResizeObserver from './ResizeObserver';
export default function Shell({ device }: { device?: WebAdb }): JSX.Element | null {
const routeMatch = useRouteMatch();
const [cached, setCached] = useState(false);
useEffect(() => {
setCached(true);
}, [routeMatch]);
const terminalRef = useRef<Terminal>();
const fitAddonRef = useRef<FitAddon>();
const handleContainerRef = useCallback((element: HTMLDivElement | null) => {
if (!element) {
return;
}
const terminal = new Terminal({
scrollback: 9001,
});
const fitAddon = new FitAddon();
fitAddonRef.current = fitAddon;
terminal.loadAddon(fitAddon);
terminalRef.current = terminal;
terminal.open(element);
fitAddon.fit();
}, []);
useEffect(() => {
return () => terminalRef.current?.dispose();
}, []);
useEffect(() => {
if (!device || !terminalRef.current) {
return;
}
(async () => {
const shell = await device.shell();
const textEncoder = new TextEncoder();
terminalRef.current!.onData(data => {
const { buffer } = textEncoder.encode(data);
shell.write(buffer);
});
shell.onData(data => {
terminalRef.current!.write(new Uint8Array(data));
});
})();
return () => {
terminalRef.current!.reset();
terminalRef.current!.clear();
};
}, [device, terminalRef.current]);
const handleResize = useCallback(() => {
fitAddonRef.current?.fit();
}, []);
if (!cached) {
return null;
}
return (
<ResizeObserver onResize={handleResize}>
<div ref={handleContainerRef} style={{ height: '100%' }} />
</ResizeObserver>
)
}

View file

@ -0,0 +1,5 @@
html, body, #container {
width: 100%;
height: 100%;
margin: 0;
}

261
packages/demo/src/index.tsx Normal file
View file

@ -0,0 +1,261 @@
import { DefaultButton, Dialog, DialogFooter, DialogType, Link, Label, MessageBar, Nav, PrimaryButton, ProgressIndicator, Separator, Stack, StackItem, Text, TextField, IStackItemComponent } from '@fluentui/react';
import { useId } from '@uifabric/react-hooks';
import { WebAdb, WebUsbTransportation } from '@yume-chan/webadb';
import { initializeIcons } from 'office-ui-fabric-react/lib/Icons';
import React, { useCallback } from 'react';
import ReactDOM from 'react-dom';
import { HashRouter, Redirect, Route, useLocation } from 'react-router-dom';
import './index.css';
import Shell from './Shell';
initializeIcons();
function App(): JSX.Element | null {
const location = useLocation();
const [device, setDevice] = React.useState<WebAdb | undefined>();
const [connecting, setConnecting] = React.useState(false);
const [connectError, setConnectError] = React.useState<string | undefined>(undefined);
const handleConnectClick = React.useCallback(async () => {
try {
const transportation = await WebUsbTransportation.pickDevice();
if (transportation) {
const device = new WebAdb(transportation);
setConnecting(true);
await device.connect();
setDevice(device);
}
} catch (e) {
setConnectError(e.message);
} finally {
setConnecting(false);
}
}, []);
const disconnect = useCallback(async () => {
try {
await device?.dispose();
} catch (e) {
console.log(e);
}
setDevice(undefined);
}, [device]);
const [tcpPort, setTcpPort] = React.useState<number | undefined>();
const queryTcpPort = React.useCallback(async () => {
if (!device) {
return;
}
const result = await device.shell('getprop service.adb.tcp.port');
setTcpPort(Number.parseInt(result, 10));
}, [device]);
const [tcpPortValue, setTcpPortValue] = React.useState('5555');
const tcpPortInputId = useId('tcpPort');
const enableTcp = React.useCallback(async () => {
if (!device) {
return;
}
const result = await device.tcpip(Number.parseInt(tcpPortValue, 10));
console.log(result);
}, [device, tcpPortValue]);
const disableTcp = React.useCallback(async () => {
if (!device) {
return;
}
const result = await device.usb();
console.log(result);
}, [device]);
return (
<Stack verticalFill>
<StackItem tokens={{ padding: 8 }}>
<Text variant="xxLarge">WebADB Demo</Text>
</StackItem>
<StackItem>
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 8, padding: 8 }}>
{!device && <StackItem>
<PrimaryButton text="Connect" onClick={handleConnectClick} />
</StackItem>}
{device && <StackItem>
<DefaultButton text="Disconnect" onClick={disconnect} />
</StackItem>}
<StackItem>
{device && `Connected to ${device.name}`}
</StackItem>
</Stack>
</StackItem>
<StackItem>
<Separator />
</StackItem>
<StackItem grow styles={{ root: { minHeight: 0 } }}>
<Stack horizontal verticalFill tokens={{ childrenGap: 8 }}>
<StackItem>
<Nav
styles={{ root: { width: 250 } }}
groups={[{
links: [
{ key: '/intro', name: 'Introduction', url: '#/intro' },
{ key: '/adb-over-wifi', name: 'ADB over WiFi', url: '#/adb-over-wifi' },
{ key: '/shell', name: 'Interactive Shell', url: '#/shell' },
]
}]}
selectedKey={location.pathname}
/>
</StackItem>
<StackItem grow styles={{ root: { minHeight: 0 } }}>
<Route path="/intro">
<Stack tokens={{ childrenGap: 8, padding: 8 }}>
<Text block>
This demo can connect to your Android devices using the{' '}
<Link href="https://developer.mozilla.org/en-US/docs/Web/API/USB" target="_blank">WebUSB</Link>{' '}
API.
</Text>
<Text block>
Before start, please make sure your adb server is not running (`adb kill-server`), as there can be only one connection to your device at same time.
</Text>
</Stack>
</Route>
<Route path="/adb-over-wifi">
<Stack verticalFill tokens={{ childrenGap: 8, padding: 8 }}>
<StackItem>
<MessageBar >
<Text>Although WebADB can enable ADB over WiFi for you, it can't connect to your device wirelessly.</Text>
</MessageBar>
</StackItem>
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 8 }}>
<StackItem>
<PrimaryButton text="Update Status" disabled={!device} onClick={queryTcpPort} />
</StackItem>
<StackItem>
{tcpPort !== undefined &&
(tcpPort !== 0
? `Enabled at port ${tcpPort}`
: 'Disabled')}
</StackItem>
</Stack>
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 8 }}>
<StackItem>
<Label htmlFor={tcpPortInputId}>Port: </Label>
</StackItem>
<StackItem>
<TextField
id={tcpPortInputId}
width={300}
disabled={!device}
value={tcpPortValue}
onChange={(e, value) => setTcpPortValue(value!)}
/>
</StackItem>
<StackItem>
<PrimaryButton
text="Enable"
disabled={!device}
onClick={enableTcp}
/>
</StackItem>
</Stack>
<StackItem>
<PrimaryButton
text="Disable"
disabled={!device || tcpPort === undefined || tcpPort === 0}
onClick={disableTcp}
/>
</StackItem>
</Stack>
</Route>
<Route path="/shell" >
{({ match }) => (
<Stack
verticalFill
styles={{ root: { visibility: match ? 'visible' : 'hidden' } }}
tokens={{ childrenGap: 8, padding: 8 }}
>
<StackItem grow styles={{ root: { minHeight: 0 } }}>
<Shell device={device} />
</StackItem>
</Stack>
)}
</Route>
</StackItem>
</Stack>
</StackItem>
<Dialog
hidden={!connecting}
dialogContentProps={{
title: 'Connecting',
subText: 'Please authorize the connection on your device'
}}
>
<ProgressIndicator />
</Dialog>
<Dialog
hidden={!connectError}
dialogContentProps={{
type: DialogType.normal,
title: 'Connection Error',
subText: connectError,
}}
>
<DialogFooter>
<PrimaryButton text="OK" onClick={() => setConnectError(undefined)} />
</DialogFooter>
</Dialog>
</Stack>
)
}
ReactDOM.render(
<HashRouter>
<App />
<Redirect to='/intro' />
</HashRouter>,
document.getElementById('container'));
// document.getElementById('start')!.onclick = async () => {
// const transportation = await WebUsbTransportation.pickDevice();
// const device = new WebAdb(transportation);
// const textEncoder = new TextEncoder();
// const output = await device.shell('echo', '1');
// console.log(output);
// const shell = await device.shell();
// const terminal = new Terminal({
// scrollback: 9001,
// });
// const searchAddon = new SearchAddon();
// terminal.loadAddon(searchAddon);
// const keyword = document.getElementById('search-keyword')! as HTMLInputElement;
// keyword.addEventListener('input', () => {
// searchAddon.findNext(keyword.value, { incremental: true });
// });
// const next = document.getElementById('search-next')!;
// next.addEventListener('click', () => {
// searchAddon.findNext(keyword.value);
// });
// const prev = document.getElementById('search-prev')!;
// prev.addEventListener('click', () => {
// searchAddon.findPrevious(keyword.value);
// });
// terminal.open(document.getElementById('terminal')!);
// terminal.onData(data => {
// const { buffer } = textEncoder.encode(data);
// shell.write(buffer);
// });
// shell.onData(data => {
// terminal.write(new Uint8Array(data));
// });
// };

View file

@ -0,0 +1,66 @@
{
"compilerOptions": {
/* Basic Options */
"incremental": true, // /* Enable incremental compilation */
"target": "ES2018", // /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "ESNext", // /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"lib": [ // /* Specify library files to be included in the compilation. */
"DOM",
"ESNext"
],
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
"jsx": "react", // /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
"sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "lib/index.js", // /* Concatenate and emit output to single file. */
"outDir": "lib", // /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
"importHelpers": true, // /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, // /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
"noImplicitReturns": true, // /* Report error when not all code paths in function return a value. */
"noFallthroughCasesInSwitch": true, // /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
"moduleResolution": "node", // /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, // /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"forceConsistentCasingInFileNames": true ///* Disallow inconsistently-cased references to the same file. */
},
"include": [
"src"
]
}

View file

@ -0,0 +1,42 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
var path_1 = __importDefault(require("path"));
var mini_css_extract_plugin_1 = __importDefault(require("mini-css-extract-plugin"));
var context = path_1.default.resolve(process.cwd());
var config = {
mode: 'development',
devtool: 'eval-source-map',
context: context,
target: 'web',
entry: {
index: './src/index.tsx',
},
output: {
publicPath: '/lib/',
path: path_1.default.resolve(context, 'lib'),
filename: '[name].js',
},
resolve: {
extensions: ['.ts', '.tsx', '.js'],
},
plugins: [
new mini_css_extract_plugin_1.default({
filename: '[name].css',
esModule: true,
}),
],
module: {
rules: [
{ test: /.css$/i, loader: [mini_css_extract_plugin_1.default.loader, 'css-loader'] },
{ test: /.tsx?$/i, loader: 'ts-loader' },
],
},
devServer: {
publicPath: '/lib/',
contentBase: path_1.default.resolve(context, 'www'),
port: 9000
},
};
module.exports = config;

View file

@ -0,0 +1,12 @@
<html>
<head>
<link rel="stylesheet" href="lib/index.css">
</head>
<body>
<div id="container" />
<script src="lib/index.js"></script>
</body>
</html>

11
packages/event/README.md Normal file
View file

@ -0,0 +1,11 @@
# `event`
> TODO: description
## Usage
```
const event = require('event');
// TODO: DEMONSTRATE API
```

19
packages/event/package-lock.json generated Normal file
View file

@ -0,0 +1,19 @@
{
"name": "@yume-chan/event",
"version": "0.0.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"tslib": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz",
"integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ=="
},
"typescript": {
"version": "3.9.7",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz",
"integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==",
"dev": true
}
}
}

View file

@ -0,0 +1,29 @@
{
"name": "@yume-chan/event",
"version": "0.0.1",
"description": "Event/EventEmitter",
"keywords": [
"event"
],
"author": "Simon Chan <cnsimonchan@live.com>",
"homepage": "https://github.com/yume-chan/ya-webadb#readme",
"license": "MIT",
"main": "lib/index.js",
"repository": {
"type": "git",
"url": "git+https://github.com/yume-chan/ya-webadb.git"
},
"scripts": {
"build": "tsc -b",
"build:watch": "tsc -b -w"
},
"bugs": {
"url": "https://github.com/yume-chan/ya-webadb/issues"
},
"devDependencies": {
"typescript": "^3.9.7"
},
"dependencies": {
"tslib": "^2.0.1"
}
}

View file

@ -0,0 +1,26 @@
export interface Disposable {
dispose(): void;
}
export class AutoDisposable implements Disposable {
private disposables: Disposable[] = [];
protected addDisposable<T extends Disposable>(disposable: T): T {
this.disposables.push(disposable);
return disposable;
}
public dispose() {
for (const disposable of this.disposables) {
disposable.dispose();
}
this.disposables = [];
}
}
export class DisposableList extends AutoDisposable {
public add<T extends Disposable>(disposable: T): T {
return this.addDisposable(disposable);
}
}

View file

@ -0,0 +1,58 @@
import { Disposable } from './disposable';
import { EventListener, RemoveEventListener } from './event';
interface EventListenerInfo<TEvent> {
listener: EventListener<TEvent, any, any, unknown>;
thisArg: unknown;
args: unknown[];
}
export class EventEmitter<TEvent> implements Disposable {
private listeners: EventListenerInfo<TEvent>[] = [];
public constructor() {
this.event = this.event.bind(this);
}
public event(
listener: EventListener<TEvent, unknown, [], unknown>
): RemoveEventListener;
public event<TThis, TArgs extends unknown[]>(
listener: EventListener<TEvent, TThis, TArgs, unknown>,
thisArg: TThis,
...args: TArgs
): RemoveEventListener;
public event<TThis, TArgs extends unknown[]>(
listener: EventListener<TEvent, TThis, TArgs, unknown>,
thisArg?: TThis,
...args: TArgs
): RemoveEventListener {
const info: EventListenerInfo<TEvent> = {
listener,
thisArg,
args,
};
this.listeners.push(info);
const remove: RemoveEventListener = () => {
const index = this.listeners.indexOf(info);
if (index > 0) {
this.listeners.splice(index, 1);
}
};
remove.dispose = remove;
return remove;
}
public fire(e: TEvent) {
for (const info of this.listeners) {
info.listener.apply(info.thisArg, [e, ...info.args]);
}
}
public dispose() {
this.listeners.length = 0;
}
}

View file

@ -0,0 +1,19 @@
import { Disposable } from './disposable';
export interface EventListener<TEvent, TThis, TArgs extends unknown[], TResult> {
(this: TThis, e: TEvent, ...args: TArgs): TResult;
}
export interface RemoveEventListener extends Disposable {
(): void;
}
export interface Event<TEvent> {
(listener: EventListener<TEvent, unknown, [], unknown>): RemoveEventListener;
<TThis, TArgs extends unknown[]>(
listener: EventListener<TEvent, TThis, TArgs, unknown>,
thisArg: TThis,
...args: TArgs
): RemoveEventListener;
}

View file

@ -0,0 +1,3 @@
export * from './disposable';
export * from './event';
export * from './event-emitter';

View file

@ -15,13 +15,13 @@
"declarationMap": true, // /* Generates a sourcemap for each corresponding '.d.ts' file. */ "declarationMap": true, // /* Generates a sourcemap for each corresponding '.d.ts' file. */
"sourceMap": true, /* Generates corresponding '.map' file. */ "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "lib/index.js", // /* Concatenate and emit output to single file. */ // "outFile": "lib/index.js", // /* Concatenate and emit output to single file. */
// "outDir": "lib", /* Redirect output structure to the directory. */ "outDir": "lib", // /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ "rootDir": "./src", // /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */ "composite": true, // /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */ // "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */ // "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */ "importHelpers": true, // /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */ /* Strict Type-Checking Options */
@ -36,8 +36,8 @@
/* Additional Checks */ /* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */ // "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */ // "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ "noImplicitReturns": true, // /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ "noFallthroughCasesInSwitch": true, // /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */ /* Module Resolution Options */
"moduleResolution": "node", // /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ "moduleResolution": "node", // /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */

21
packages/webadb/LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Simon Chan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

11
packages/webadb/README.md Normal file
View file

@ -0,0 +1,11 @@
# `webadb`
> TODO: description
## Usage
```
const webadb = require('webadb');
// TODO: DEMONSTRATE API
```

29
packages/webadb/package-lock.json generated Normal file
View file

@ -0,0 +1,29 @@
{
"name": "@yume-chan/webadb",
"version": "0.0.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@types/w3c-web-usb": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.4.tgz",
"integrity": "sha512-aaOB3EL5WCWBBOYX7W1MKuzspOM9ZJI9s3iziRVypr1N+QyvIgXzCM4lm1iiOQ1VFzZioUPX9bsa23myCbKK4A=="
},
"@yume-chan/async-operation-manager": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@yume-chan/async-operation-manager/-/async-operation-manager-1.0.3.tgz",
"integrity": "sha512-FWzejjiDb3o1kpuruRCKhwh2ho5KOmix1GwGGDov8hUqUw6ODNeD3OszqxfPiVLAmtJ1Ib35/FREv14JndV6UA=="
},
"tslib": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz",
"integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ=="
},
"typescript": {
"version": "3.9.7",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz",
"integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==",
"dev": true
}
}
}

View file

@ -0,0 +1,33 @@
{
"name": "@yume-chan/webadb",
"version": "0.0.1",
"description": "ADB (Android Debugging Bridge) for Web browsers",
"keywords": [
"adb",
"webusb"
],
"author": "Simon Chan <cnsimonchan@live.com>",
"homepage": "https://github.com/yume-chan/ya-webadb#readme",
"license": "MIT",
"main": "lib/index.js",
"repository": {
"type": "git",
"url": "git+https://github.com/yume-chan/ya-webadb.git"
},
"scripts": {
"build": "tsc -b",
"build:watch": "tsc -b -w"
},
"bugs": {
"url": "https://github.com/yume-chan/ya-webadb/issues"
},
"devDependencies": {
"typescript": "^3.9.7"
},
"dependencies": {
"@types/w3c-web-usb": "^1.0.4",
"@yume-chan/async-operation-manager": "^1.0.3",
"@yume-chan/event": "^0.0.1",
"tslib": "^2.0.1"
}
}

View file

@ -0,0 +1,59 @@
let characterSet: string[] = [];
const pairs = [
['A', 'Z'],
['a', 'z'],
['0', '9'],
].map(pair => pair.map(character => character.charCodeAt(0)));
for (const [begin, end] of pairs) {
for (let i = begin; i <= end; i += 1) {
characterSet.push(String.fromCharCode(i));
}
}
characterSet.push('+', '/');
export default function toBase64(buffer: ArrayBuffer) {
const array = new Uint8Array(buffer);
const length = buffer.byteLength;
const remainder = length % 3;
let result = '';
for (let i = 0; i < length - remainder; i += 3) {
// aaaaaabb
const x = array[i];
// bbbbcccc
const y = array[i + 1];
// ccdddddd
const z = array[i + 2];
const a = x >> 2;
const b = ((x & 0b11) << 4) | (y >> 4);
const c = ((y & 0b1111) << 2) | (z >> 6);
const d = z & 0b111111;
result += characterSet[a] + characterSet[b] + characterSet[c] + characterSet[d];
}
if (remainder === 1) {
// aaaaaabb
const x = array[length - 1];
const a = x >> 2;
const b = ((x & 0b11) << 4);
result += characterSet[a] + characterSet[b] + '==';
} else if (remainder === 2) {
// aaaaaabb
const x = array[length - 2];
// bbbbcccc
const y = array[length - 1];
const a = x >> 2;
const b = ((x & 0b11) << 4) | (y >> 4);
const c = ((y & 0b1111) << 2);
result = characterSet[a] + characterSet[b] + characterSet[c] + '=';
}
return result;
}

View file

@ -0,0 +1,2 @@
export * from './webadb';
export * from './transportation';

View file

@ -4,9 +4,9 @@ const textDecoder = new TextDecoder();
// TextEncoder.prototype.encodeInto added in Chrome 74 // TextEncoder.prototype.encodeInto added in Chrome 74
// Edge for Android 44 is still using Chromium 73 // Edge for Android 44 is still using Chromium 73
if (!TextEncoder.prototype.encodeInto) { if (!TextEncoder.prototype.encodeInto) {
TextEncoder.prototype.encodeInto = function (source: string, destniation: Uint8Array) { TextEncoder.prototype.encodeInto = function (source: string, destination: Uint8Array) {
const array = this.encode(source); const array = this.encode(source);
destniation.set(array); destination.set(array);
return { read: source.length, written: array.length }; return { read: source.length, written: array.length };
} }
} }

View file

@ -0,0 +1,66 @@
import { PromiseResolver } from '@yume-chan/async-operation-manager';
import { WebAdb } from './webadb';
import { Event, EventEmitter } from '@yume-chan/event';
class AutoResetEvent {
private _list: PromiseResolver<void>[] = [];
private _blocking: boolean = false;
public wait(): Promise<void> {
if (!this._blocking) {
this._blocking = true;
if (this._list.length === 0) {
return Promise.resolve();
}
}
const resolver = new PromiseResolver<void>();
this._list.push(resolver);
return resolver.promise;
}
public notify() {
if (this._list.length !== 0) {
this._list.pop()!.resolve();
} else {
this._blocking = false;
}
}
}
export class AdbStream {
private _writeMutex = new AutoResetEvent();
public onDataEvent: EventEmitter<ArrayBuffer> = new EventEmitter();
public get onData(): Event<ArrayBuffer> { return this.onDataEvent.event; }
public onCloseEvent: EventEmitter<void> = new EventEmitter();
public get onClose(): Event<void> { return this.onCloseEvent.event; }
private _adb: WebAdb;
public localId: number;
public remoteId: number;
public constructor(adb: WebAdb, localId: number, remoteId: number) {
this._adb = adb;
this.localId = localId;
this.remoteId = remoteId;
}
public async write(data: ArrayBuffer): Promise<void> {
await this._writeMutex.wait();
await this._adb.sendMessage('WRTE', this.localId, this.remoteId, data);
}
public async close(): Promise<void> {
await this._adb.sendMessage('CLSE', this.localId, this.remoteId);
}
public ack(): void {
this._writeMutex.notify();
}
}

View file

@ -0,0 +1,109 @@
export interface WebAdbTransportation {
readonly name: string | undefined;
write(buffer: ArrayBuffer): void | Promise<void>;
read(length: number): ArrayBuffer | Promise<ArrayBuffer>;
dispose(): void | Promise<void>;
}
export const WebUsbDeviceFilter: USBDeviceFilter = {
classCode: 0xFF,
subclassCode: 0x42,
protocolCode: 1,
};
export class WebUsbTransportation implements WebAdbTransportation {
public static async pickDevice() {
try {
const device = await navigator.usb.requestDevice({ filters: [WebUsbDeviceFilter] });
await device.open();
let inEndpointNumber!: number;
let outEndpointNumber!: number;
for (const configuration of device.configurations) {
for (const interface_ of configuration.interfaces) {
for (const alternate of interface_.alternates) {
if (alternate.interfaceSubclass === WebUsbDeviceFilter.subclassCode &&
alternate.interfaceClass === WebUsbDeviceFilter.classCode &&
alternate.interfaceSubclass === WebUsbDeviceFilter.subclassCode) {
if (device.configuration?.configurationValue !== configuration.configurationValue) {
await device.selectConfiguration(configuration.configurationValue);
}
if (!interface_.claimed) {
await device.claimInterface(interface_.interfaceNumber);
}
if (interface_.alternate.alternateSetting !== alternate.alternateSetting) {
await device.selectAlternateInterface(interface_.interfaceNumber, alternate.alternateSetting);
}
for (const endpoint of alternate.endpoints) {
switch (endpoint.direction) {
case 'in':
inEndpointNumber = endpoint.endpointNumber;
if (outEndpointNumber !== undefined) {
return new WebUsbTransportation(device, inEndpointNumber, outEndpointNumber);
}
break;
case 'out':
outEndpointNumber = endpoint.endpointNumber;
if (inEndpointNumber !== undefined) {
return new WebUsbTransportation(device, inEndpointNumber, outEndpointNumber);
}
break;
}
}
}
}
}
}
throw new Error('Unknown error');
} catch (e) {
switch (e.name) {
case 'NetworkError':
throw new Error('Cannot connect to device. Make sure ADB is not running');
case 'NotFoundError':
return undefined;
default:
throw e;
}
}
}
private _device: USBDevice;
public get name() { return this._device.productName; }
private _inEndpointNumber!: number;
private _outEndpointNumber!: number;
private constructor(device: USBDevice, inEndPointNumber: number, outEndPointNumber: number) {
this._device = device;
this._inEndpointNumber = inEndPointNumber;
this._outEndpointNumber = outEndPointNumber;
}
public async write(buffer: ArrayBuffer): Promise<void> {
await this._device.transferOut(this._outEndpointNumber, buffer);
}
public async read(length: number): Promise<ArrayBuffer> {
const result = await this._device.transferIn(this._inEndpointNumber, length);
if (result.status === 'stall') {
await this._device.clearHalt('in', this._inEndpointNumber);
}
return result.data!.buffer;
}
public async dispose() {
await this._device.close();
}
}

View file

@ -0,0 +1,180 @@
import AsyncOperationManager from '@yume-chan/async-operation-manager';
import toBase64 from './base64';
import { AdbPacket } from './packet';
import { AdbStream } from './stream';
import { WebUsbTransportation } from './transportation';
export const AdbDeviceFilter: USBDeviceFilter = { classCode: 0xFF, subclassCode: 0x42, protocolCode: 1 };
export class WebAdb {
private _transportation: WebUsbTransportation;
public get name() { return this._transportation.name; }
private _alive = true;
private _looping = false;
// ADB requires stream id to start from 1
// (0 means open failed)
private _streamInitializer = new AsyncOperationManager(1);
private _streams = new Map<number, AdbStream>();
public constructor(transportation: WebUsbTransportation) {
this._transportation = transportation;
}
private async receiveLoop(): Promise<void> {
if (this._looping) {
return;
}
this._looping = true;
while (this._alive) {
const response = await this.receiveMessage();
switch (response.command) {
case 'OKAY':
// OKAY has two meanings
// 1. The device has created the Stream
this._streamInitializer.resolve(response.arg1, response.arg0);
// 2. The device has received last WRTE to the Stream
this._streams.get(response.arg1)?.ack();
break;
case 'CLSE':
// CLSE also has two meanings
if (response.arg0 === 0) {
// 1. The device don't want to create the Stream
this._streamInitializer.reject(response.arg1, new Error('open failed'));
} else {
// 2. The device has closed the Stream
this._streams.get(response.arg1)?.onCloseEvent.fire();
this._streams.delete(response.arg1);
}
break;
case 'WRTE':
this._streams.get(response.arg1)?.onDataEvent.fire(response.payload!);
await this.sendMessage('OKAY', response.arg1, response.arg0);
break;
default:
this._transportation.dispose();
throw new Error('unknown command');
}
}
}
public async connect() {
const version = 0x01000001;
await this.sendMessage('CNXN', version, 256 * 1024, 'host::');
while (true) {
const response = await this.receiveMessage();
switch (response.command) {
case 'CNXN':
if (response.arg0 !== version) {
this._transportation.dispose();
throw new Error('version mismatch');
}
return;
case 'AUTH':
if (response.arg0 !== 1) {
this._transportation.dispose();
throw new Error('unknown auth type');
}
const key = await this.generateKey();
const publicKey = await crypto.subtle.exportKey('spki', key.publicKey);
await this.sendMessage('AUTH', 3, 0, toBase64(publicKey));
break;
default:
this._transportation.dispose();
throw new Error('unknown command');
}
}
}
public async shell(command: string, ...args: string[]): Promise<string>;
public async shell(): Promise<AdbStream>;
public async shell(command?: string, ...args: string[]): Promise<AdbStream | string> {
if (!command) {
return this.createStream('shell:');
} else {
return this.createStreamAndWait(`shell:${command} ${args.join(' ')}`);
}
}
public async tcpip(port = 5555): Promise<string> {
return this.createStreamAndWait(`tcpip:${port}`);
}
public async usb(): Promise<string> {
return this.createStreamAndWait('usb:');
}
public async sendMessage(command: string, arg0: number, arg1: number, payload?: string | ArrayBuffer): Promise<void> {
const packet = new AdbPacket(command, arg0, arg1, payload);
console.log('send', command, arg0, arg1, payload);
await this._transportation.write(packet.toBuffer());
if (packet.payloadLength !== 0) {
await this._transportation.write(packet.payload!);
}
}
public async createStream(command: string): Promise<AdbStream> {
const { id: localId, promise: initializer } = this._streamInitializer.add<number>();
await this.sendMessage('OPEN', localId, 0, command);
this.receiveLoop();
const remoteId = await initializer;
const stream = new AdbStream(this, localId, remoteId);
this._streams.set(localId, stream);
return stream;
}
public async createStreamAndWait(command: string): Promise<string> {
const stream = await this.createStream(command);
return new Promise<string>((resolve) => {
let output = '';
const decoder = new TextDecoder();
stream.onData((data) => {
output += decoder.decode(data);
});
stream.onClose(() => {
resolve(output);
});
});
}
public async receiveMessage() {
console.log('receiving');
const header = await this.receiveData(24);
const packet = AdbPacket.parse(header);
if (packet.payloadLength !== 0) {
packet.payload = await this.receiveData(packet.payloadLength);
}
console.log('receive', packet.command, packet.arg0, packet.arg1, packet.payload);
return packet;
}
private async receiveData(length: number): Promise<ArrayBuffer> {
return await this._transportation.read(length);
}
private async generateKey() {
return await crypto.subtle.generateKey({
name: 'RSASSA-PKCS1-v1_5',
modulusLength: 2048,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: { name: 'SHA-1' },
}, false, ['sign', 'verify']);
}
public async dispose() {
for (const [localId, stream] of this._streams) {
await stream.close();
}
await this._transportation.dispose();
}
}

View file

@ -0,0 +1,71 @@
{
"compilerOptions": {
/* Basic Options */
"incremental": true, // /* Enable incremental compilation */
"target": "ES2018", // /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "ESNext", // /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"lib": [ // /* Specify library files to be included in the compilation. */
"DOM",
"ESNext"
],
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
"declaration": true, // /* Generates corresponding '.d.ts' file. */
"declarationMap": true, // /* Generates a sourcemap for each corresponding '.d.ts' file. */
"sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "lib/index.js", // /* Concatenate and emit output to single file. */
"outDir": "lib", // /* Redirect output structure to the directory. */
"rootDir": "./src", // /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
"composite": true, // /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
"importHelpers": true, // /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, // /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
"noImplicitReturns": true, // /* Report error when not all code paths in function return a value. */
"noFallthroughCasesInSwitch": true, // /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
"moduleResolution": "node", // /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, // /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"forceConsistentCasingInFileNames": true ///* Disallow inconsistently-cased references to the same file. */
},
"include": [
"src"
],
"references": [
{
"path": "../event/tsconfig.json"
}
]
}

View file

@ -0,0 +1,11 @@
# `webpack-config`
> TODO: description
## Usage
```
const webpackConfig = require('webpack-config');
// TODO: DEMONSTRATE API
```

208
packages/webpack-config/package-lock.json generated Normal file
View file

@ -0,0 +1,208 @@
{
"name": "webpack-config",
"version": "0.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@types/anymatch": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz",
"integrity": "sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==",
"dev": true
},
"@types/body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz",
"integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==",
"dev": true,
"requires": {
"@types/connect": "*",
"@types/node": "*"
}
},
"@types/connect": {
"version": "3.4.33",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz",
"integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/connect-history-api-fallback": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.3.tgz",
"integrity": "sha512-7SxFCd+FLlxCfwVwbyPxbR4khL9aNikJhrorw8nUIOqeuooc9gifBuDQOJw5kzN7i6i3vLn9G8Wde/4QDihpYw==",
"dev": true,
"requires": {
"@types/express-serve-static-core": "*",
"@types/node": "*"
}
},
"@types/express": {
"version": "4.17.7",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.7.tgz",
"integrity": "sha512-dCOT5lcmV/uC2J9k0rPafATeeyz+99xTt54ReX11/LObZgfzJqZNcW27zGhYyX+9iSEGXGt5qLPwRSvBZcLvtQ==",
"dev": true,
"requires": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "*",
"@types/qs": "*",
"@types/serve-static": "*"
}
},
"@types/express-serve-static-core": {
"version": "4.17.9",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.9.tgz",
"integrity": "sha512-DG0BYg6yO+ePW+XoDENYz8zhNGC3jDDEpComMYn7WJc4mY1Us8Rw9ax2YhJXxpyk2SF47PQAoQ0YyVT1a0bEkA==",
"dev": true,
"requires": {
"@types/node": "*",
"@types/qs": "*",
"@types/range-parser": "*"
}
},
"@types/http-proxy": {
"version": "1.17.4",
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.4.tgz",
"integrity": "sha512-IrSHl2u6AWXduUaDLqYpt45tLVCtYv7o4Z0s1KghBCDgIIS9oW5K1H8mZG/A2CfeLdEa7rTd1ACOiHBc1EMT2Q==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/http-proxy-middleware": {
"version": "0.19.3",
"resolved": "https://registry.npmjs.org/@types/http-proxy-middleware/-/http-proxy-middleware-0.19.3.tgz",
"integrity": "sha512-lnBTx6HCOUeIJMLbI/LaL5EmdKLhczJY5oeXZpX/cXE4rRqb3RmV7VcMpiEfYkmTjipv3h7IAyIINe4plEv7cA==",
"dev": true,
"requires": {
"@types/connect": "*",
"@types/http-proxy": "*",
"@types/node": "*"
}
},
"@types/mime": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz",
"integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==",
"dev": true
},
"@types/mini-css-extract-plugin": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/@types/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.1.tgz",
"integrity": "sha512-+mN04Oszdz9tGjUP/c1ReVwJXxSniLd7lF++sv+8dkABxVNthg6uccei+4ssKxRHGoMmPxdn7uBdJWONSJGTGQ==",
"dev": true,
"requires": {
"@types/webpack": "*"
}
},
"@types/node": {
"version": "14.0.27",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.27.tgz",
"integrity": "sha512-kVrqXhbclHNHGu9ztnAwSncIgJv/FaxmzXJvGXNdcCpV1b8u1/Mi6z6m0vwy0LzKeXFTPLH0NzwmoJ3fNCIq0g==",
"dev": true
},
"@types/qs": {
"version": "6.9.4",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.4.tgz",
"integrity": "sha512-+wYo+L6ZF6BMoEjtf8zB2esQsqdV6WsjRK/GP9WOgLPrq87PbNWgIxS76dS5uvl/QXtHGakZmwTznIfcPXcKlQ==",
"dev": true
},
"@types/range-parser": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz",
"integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==",
"dev": true
},
"@types/serve-static": {
"version": "1.13.5",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.5.tgz",
"integrity": "sha512-6M64P58N+OXjU432WoLLBQxbA0LRGBCRm7aAGQJ+SMC1IMl0dgRVi9EFfoDcS2a7Xogygk/eGN94CfwU9UF7UQ==",
"dev": true,
"requires": {
"@types/express-serve-static-core": "*",
"@types/mime": "*"
}
},
"@types/source-list-map": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz",
"integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==",
"dev": true
},
"@types/tapable": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.6.tgz",
"integrity": "sha512-W+bw9ds02rAQaMvaLYxAbJ6cvguW/iJXNT6lTssS1ps6QdrMKttqEAMEG/b5CR8TZl3/L7/lH0ZV5nNR1LXikA==",
"dev": true
},
"@types/uglify-js": {
"version": "3.9.3",
"resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.9.3.tgz",
"integrity": "sha512-KswB5C7Kwduwjj04Ykz+AjvPcfgv/37Za24O2EDzYNbwyzOo8+ydtvzUfZ5UMguiVu29Gx44l1A6VsPPcmYu9w==",
"dev": true,
"requires": {
"source-map": "^0.6.1"
}
},
"@types/webpack": {
"version": "4.41.21",
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.21.tgz",
"integrity": "sha512-2j9WVnNrr/8PLAB5csW44xzQSJwS26aOnICsP3pSGCEdsu6KYtfQ6QJsVUKHWRnm1bL7HziJsfh5fHqth87yKA==",
"dev": true,
"requires": {
"@types/anymatch": "*",
"@types/node": "*",
"@types/tapable": "*",
"@types/uglify-js": "*",
"@types/webpack-sources": "*",
"source-map": "^0.6.0"
}
},
"@types/webpack-dev-server": {
"version": "3.11.0",
"resolved": "https://registry.npmjs.org/@types/webpack-dev-server/-/webpack-dev-server-3.11.0.tgz",
"integrity": "sha512-3+86AgSzl18n5P1iUP9/lz3G3GMztCp+wxdDvVuNhx1sr1jE79GpYfKHL8k+Vht3N74K2n98CuAEw4YPJCYtDA==",
"dev": true,
"requires": {
"@types/connect-history-api-fallback": "*",
"@types/express": "*",
"@types/http-proxy-middleware": "*",
"@types/serve-static": "*",
"@types/webpack": "*"
}
},
"@types/webpack-sources": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-1.4.2.tgz",
"integrity": "sha512-77T++JyKow4BQB/m9O96n9d/UUHWLQHlcqXb9Vsf4F1+wKNrrlWNFPDLKNT92RJnCSL6CieTc+NDXtCVZswdTw==",
"dev": true,
"requires": {
"@types/node": "*",
"@types/source-list-map": "*",
"source-map": "^0.7.3"
},
"dependencies": {
"source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
"dev": true
}
}
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
},
"typescript": {
"version": "3.9.7",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz",
"integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==",
"dev": true
}
}
}

View file

@ -0,0 +1,27 @@
{
"name": "webpack-config",
"version": "0.0.0",
"private": true,
"description": "> TODO: description",
"author": "Simon Chan <cnsimonchan@live.com>",
"homepage": "https://github.com/yume-chan/ya-webadb#readme",
"license": "MIT",
"main": "lib/webpack-config.js",
"repository": {
"type": "git",
"url": "git+https://github.com/yume-chan/ya-webadb.git"
},
"scripts": {
"build": "tsc"
},
"bugs": {
"url": "https://github.com/yume-chan/ya-webadb/issues"
},
"devDependencies": {
"@types/mini-css-extract-plugin": "^0.9.1",
"@types/node": "^14.0.27",
"@types/webpack": "^4.41.21",
"@types/webpack-dev-server": "^3.11.0",
"typescript": "^3.9.7"
}
}

View file

@ -0,0 +1,42 @@
import path from 'path';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import webpack from 'webpack';
const context = path.resolve(process.cwd());
const config: webpack.Configuration = {
mode: 'development',
devtool: 'eval-source-map',
context,
target: 'web',
entry: {
index: './src/index.tsx',
},
output: {
publicPath: '/lib/',
path: path.resolve(context, 'lib'),
filename: '[name].js',
},
resolve: {
extensions: ['.ts', '.tsx', '.js'],
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
esModule: true,
}),
],
module: {
rules: [
{ test: /.css$/i, loader: [MiniCssExtractPlugin.loader, 'css-loader'] },
{ test: /.tsx?$/i, loader: 'ts-loader' },
],
},
devServer: {
publicPath: '/lib/',
contentBase: path.resolve(context, 'www'),
port: 9000
},
};
export = config;

View file

@ -0,0 +1,65 @@
{
"compilerOptions": {
/* Basic Options */
"incremental": true, // /* Enable incremental compilation */
"target": "ES5", // /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "CommonJS", // /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"lib": [ // /* Specify library files to be included in the compilation. */
"ESNext"
],
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "lib/index.js", // /* Concatenate and emit output to single file. */
"outDir": "../demo", // /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
"tsBuildInfoFile": "./tsconfig.tsbuildinfo", ///* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, // /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
"noImplicitReturns": true, // /* Report error when not all code paths in function return a value. */
"noFallthroughCasesInSwitch": true, // /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
"moduleResolution": "node", // /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, // /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"forceConsistentCasingInFileNames": true ///* Disallow inconsistently-cased references to the same file. */
},
"include": [
"src"
]
}

View file

@ -1,21 +0,0 @@
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import css from 'rollup-plugin-css-only';
export default {
input: 'src/test.ts',
plugins: [
resolve(),
css(),
typescript(),
commonjs({
extensions: ['.js', '.ts'],
}),
],
output: {
file: 'lib/test.js',
format: 'iife',
sourcemap: true,
},
};

View file

@ -1,117 +0,0 @@
import { PromiseResolver } from '@yume-chan/async-operation-manager';
import { WebAdb } from './webadb';
import { IEvent } from 'xterm';
interface IListener<T, U = void> {
(arg1: T, arg2: U): void;
}
export interface IEventEmitter<T, U = void> {
event: IEvent<T, U>;
fire(arg1: T, arg2: U): void;
dispose(): void;
}
export class EventEmitter<T, U = void> implements IEventEmitter<T, U> {
private _listeners: IListener<T, U>[] = [];
private _event?: IEvent<T, U>;
private _disposed: boolean = false;
public get event(): IEvent<T, U> {
if (!this._event) {
this._event = (listener: (arg1: T, arg2: U) => any) => {
this._listeners.push(listener);
const disposable = {
dispose: () => {
if (!this._disposed) {
for (let i = 0; i < this._listeners.length; i++) {
if (this._listeners[i] === listener) {
this._listeners.splice(i, 1);
return;
}
}
}
}
};
return disposable;
};
}
return this._event;
}
public fire(arg1: T, arg2: U): void {
const queue: IListener<T, U>[] = [];
for (let i = 0; i < this._listeners.length; i++) {
queue.push(this._listeners[i]);
}
for (let i = 0; i < queue.length; i++) {
queue[i].call(undefined, arg1, arg2);
}
}
public dispose(): void {
if (this._listeners) {
this._listeners.length = 0;
}
this._disposed = true;
}
}
class AutoResetEvent {
private _list: PromiseResolver<void>[] = [];
private _blocking: boolean = false;
public wait(): Promise<void> {
if (!this._blocking) {
this._blocking = true;
if (this._list.length === 0) {
return Promise.resolve();
}
}
const resolver = new PromiseResolver<void>();
this._list.push(resolver);
return resolver.promise;
}
public notify() {
if (this._list.length !== 0) {
this._list.pop()!.resolve();
} else {
this._blocking = false;
}
}
}
export class AdbStream {
private _writeMutex = new AutoResetEvent();
public onDataEvent: EventEmitter<ArrayBuffer> = new EventEmitter();
public get onData(): IEvent<ArrayBuffer> { return this.onDataEvent.event; }
public onCloseEvent: EventEmitter<void> = new EventEmitter();
public get onClose(): IEvent<void> { return this.onCloseEvent.event; }
private _adb: WebAdb;
public localId: number;
public remoteId: number;
public constructor(adb: WebAdb, localId: number, remoteId: number) {
this._adb = adb;
this.localId = localId;
this.remoteId = remoteId;
}
public async write(data: ArrayBuffer): Promise<void> {
await this._writeMutex.wait();
await this._adb.sendMessage('WRTE', this.localId, this.remoteId, data);
}
public ack(): void {
this._writeMutex.notify();
}
}

View file

@ -1,47 +0,0 @@
import { Terminal } from 'xterm';
import { SearchAddon } from 'xterm-addon-search';
import { WebAdb } from './webadb.js';
import 'xterm/css/xterm.css';
document.getElementById('start')!.onclick = async () => {
const adb = await WebAdb.open();
await adb.connect();
const textEncoder = new TextEncoder();
const output = await adb.shell('echo', '1');
console.log(output);
const shell = await adb.shell();
const terminal = new Terminal({
scrollback: 9001,
});
const searchAddon = new SearchAddon();
terminal.loadAddon(searchAddon);
const keyword = document.getElementById('search-keyword')! as HTMLInputElement;
keyword.addEventListener('input', () => {
searchAddon.findNext(keyword.value, { incremental: true });
});
const next = document.getElementById('search-next')!;
next.addEventListener('click', () => {
searchAddon.findNext(keyword.value);
});
const prev = document.getElementById('search-prev')!;
prev.addEventListener('click', () => {
searchAddon.findPrevious(keyword.value);
});
terminal.open(document.getElementById('terminal')!);
terminal.onData(data => {
const { buffer } = textEncoder.encode(data);
shell.write(buffer);
});
shell.onData(data => {
terminal.write(new Uint8Array(data));
});
};

View file

@ -1,273 +0,0 @@
import { AdbPacket } from './packet';
import { AdbStream } from './stream';
import AsyncOperationManager from '@yume-chan/async-operation-manager';
export const AdbDeviceFilter: USBDeviceFilter = { classCode: 0xFF, subclassCode: 0x42, protocolCode: 1 };
export class WebAdb {
public static async open() {
const device = await navigator.usb.requestDevice({ filters: [AdbDeviceFilter] });
await device.open();
const webadb = new WebAdb(device);
await webadb.initialize();
return webadb;
}
private _device: USBDevice;
private _inEndpointNumber!: number;
private _outEndpointNumber!: number;
private _alive = true;
private _looping = false;
// ADB requires stream id to start from 1
// (0 means open failed)
private _streamInitializer = new AsyncOperationManager(1);
private _streams = new Map<number, AdbStream>();
public constructor(device: USBDevice) {
this._device = device;
}
private async initialize(): Promise<void> {
for (const configuration of this._device.configurations) {
for (const interface_ of configuration.interfaces) {
for (const alternate of interface_.alternates) {
if (alternate.interfaceProtocol === AdbDeviceFilter.protocolCode &&
alternate.interfaceClass === AdbDeviceFilter.classCode &&
alternate.interfaceSubclass === AdbDeviceFilter.subclassCode) {
if (this._device.configuration?.configurationValue !== configuration.configurationValue) {
await this._device.selectConfiguration(configuration.configurationValue);
}
if (!interface_.claimed) {
await this._device.claimInterface(interface_.interfaceNumber);
}
if (interface_.alternate.alternateSetting !== alternate.alternateSetting) {
await this._device.selectAlternateInterface(interface_.interfaceNumber, alternate.alternateSetting);
}
this._inEndpointNumber = this.getEndpointNumber(alternate.endpoints, 'in');
this._outEndpointNumber = this.getEndpointNumber(alternate.endpoints, 'out');
return;
}
}
}
}
}
private async receiveLoop(): Promise<void> {
if (this._looping) {
return;
}
this._looping = true;
while (this._alive) {
const response = await this.receiveMessage();
switch (response.command) {
case 'OKAY':
// OKAY has two meanings
// 1. The device has created the Stream
this._streamInitializer.resolve(response.arg1, response.arg0);
// 2. The device has received last WRTE to the Stream
this._streams.get(response.arg1)?.ack();
break;
case 'CLSE':
// CLSE also has two meanings
if (response.arg0 === 0) {
// 1. The device don't want to create the Stream
this._streamInitializer.reject(response.arg1, new Error('open failed'));
} else {
// 2. The device has closed the Stream
this._streams.get(response.arg1)?.onCloseEvent.fire();
this._streams.delete(response.arg1);
}
break;
case 'WRTE':
this._streams.get(response.arg1)?.onDataEvent.fire(response.payload!);
await this.sendMessage('OKAY', response.arg1, response.arg0);
break;
default:
this._device.close();
throw new Error('unknown command');
}
}
}
public async connect() {
const version = 0x01000001;
await this.sendMessage('CNXN', version, 256 * 1024, 'host::');
while (true) {
const response = await this.receiveMessage();
switch (response.command) {
case 'CNXN':
if (response.arg0 !== version) {
this._device.close();
throw new Error('version mismatch');
}
return;
case 'AUTH':
if (response.arg0 !== 1) {
this._device.close();
throw new Error('unknwon auth type');
}
const key = await this.generateKey();
const publicKey = await crypto.subtle.exportKey('spki', key.publicKey);
await this.sendMessage('AUTH', 3, 0, this.toBase64(publicKey));
break;
default:
this._device.close();
throw new Error('unknown command');
}
}
}
public async shell(command: string, ...args: string[]): Promise<string>;
public async shell(): Promise<AdbStream>;
public async shell(command?: string, ...args: string[]): Promise<AdbStream | string> {
if (!command) {
return this.createStream('shell:');
} else {
const stream = await this.createStream(`shell,v2,raw:${command} ${args.join(' ')}`);
return new Promise<string>((resolve) => {
let output = '';
const decoder = new TextDecoder();
stream.onData((data) => {
output += decoder.decode(data);
});
stream.onClose(() => {
resolve(output);
});
})
}
}
public async sendMessage(command: string, arg0: number, arg1: number, payload?: string | ArrayBuffer): Promise<void> {
const packet = new AdbPacket(command, arg0, arg1, payload);
console.log('send', command, arg0, arg1, payload);
await this._device.transferOut(this._outEndpointNumber, packet.toBuffer());
if (packet.payloadLength !== 0) {
await this._device.transferOut(this._outEndpointNumber, packet.payload!);
}
}
public async createStream(command: string): Promise<AdbStream> {
const { id: localId, promise: initializer } = this._streamInitializer.add<number>();
await this.sendMessage('OPEN', localId, 0, command);
this.receiveLoop();
const remoteId = await initializer;
const stream = new AdbStream(this, localId, remoteId);
this._streams.set(localId, stream);
return stream;
}
public async receiveMessage() {
console.log('receiving');
const header = await this.receiveData(24);
const packet = AdbPacket.parse(header);
if (packet.payloadLength !== 0) {
packet.payload = await this.receiveData(packet.payloadLength);
}
console.log('receive', packet.command, packet.arg0, packet.arg1, packet.payload);
return packet;
}
private getEndpointNumber(endpoints: USBEndpoint[], direction: USBDirection, type: USBEndpointType = 'bulk') {
for (const endpoint of endpoints) {
if (endpoint.direction === direction &&
endpoint.type === type) {
return endpoint.endpointNumber;
}
}
throw new Error('Cannot find endpoint');
}
private async receiveData(length: number): Promise<ArrayBuffer> {
const result = await this._device.transferIn(this._inEndpointNumber, length);
if (result.status === 'stall') {
console.log('clear halt in');
await this._device.clearHalt('in', this._inEndpointNumber);
}
return result.data!.buffer;
}
private async generateKey() {
return await crypto.subtle.generateKey({
name: 'RSASSA-PKCS1-v1_5',
modulusLength: 2048,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: { name: 'SHA-1' },
}, false, ['sign', 'verify']);
}
private toBase64(arraybuffer: ArrayBuffer) {
let characterSet = [];
const pairs = [
['A', 'Z'],
['a', 'z'],
['0', '9'],
].map(pair => pair.map(character => character.charCodeAt(0)));
for (const [begin, end] of pairs) {
for (let i = begin; i <= end; i += 1) {
characterSet.push(String.fromCharCode(i));
}
}
characterSet.push('+', '/');
const array = new Uint8Array(arraybuffer);
const length = arraybuffer.byteLength;
const remainder = length % 3;
let result = '';
for (let i = 0; i < length - remainder; i += 3) {
// aaaaaabb
const x = array[i];
// bbbbcccc
const y = array[i + 1];
// ccdddddd
const z = array[i + 2];
const a = x >> 2;
const b = ((x & 0b11) << 4) | (y >> 4);
const c = ((y & 0b1111) << 2) | (z >> 6);
const d = z & 0b111111;
result += characterSet[a] + characterSet[b] + characterSet[c] + characterSet[d];
}
if (remainder === 1) {
// aaaaaabb
const x = array[length - 1];
const a = x >> 2;
const b = ((x & 0b11) << 4);
result += characterSet[a] + characterSet[b] + '==';
} else if (remainder === 2) {
// aaaaaabb
const x = array[length - 2];
// bbbbcccc
const y = array[length - 1];
const a = x >> 2;
const b = ((x & 0b11) << 4) | (y >> 4);
const c = ((y & 0b1111) << 2);
result = characterSet[a] + characterSet[b] + characterSet[c] + '=';
}
return result;
}
}

View file

@ -1,16 +0,0 @@
<html>
<head>
<link rel="stylesheet" href="lib/test.css">
</head>
<body>
<button id="start">Start</button>
<input id="search-keyword">
<button id="search-next">Next</button>
<button id="search-prev">Prev</button>
<div id="terminal"></div>
<script src="lib/test.js"></script>
</body>
</html>