Update Electron
This commit is contained in:
parent
cae6d582ac
commit
c84ee948f5
22 changed files with 763 additions and 152 deletions
|
|
@ -1,62 +1,120 @@
|
|||
import { type BrowserWindow } from 'electron/main'
|
||||
import { AdbServerNodeTcpConnector } from '@yume-chan/adb-server-node-tcp'
|
||||
import type { AdbServerDevice, AdbServerTransport } from '@yume-chan/adb'
|
||||
import { Adb, AdbServerClient } from '@yume-chan/adb'
|
||||
import { DecodeUtf8Stream, WritableStream } from '@yume-chan/stream-extra'
|
||||
import type { AdbPacketData, AdbPacketInit } from '@yume-chan/adb'
|
||||
import { Adb, AdbDaemonTransport } from '@yume-chan/adb'
|
||||
import type { Consumable, ReadableWritablePair } from '@yume-chan/stream-extra'
|
||||
import { WritableStream } from '@yume-chan/stream-extra'
|
||||
import { AdbDaemonDirectSocketsDevice } from './adbaemonDirectSocketsDevice'
|
||||
import AdbWebCredentialStore from '@yume-chan/adb-credential-web'
|
||||
|
||||
let adb: Adb | undefined
|
||||
export function AdbTcpSocket(win: BrowserWindow | null, ipcMain: Electron.IpcMain) {
|
||||
if (!win) return
|
||||
|
||||
export function AdbDaemon(_win: BrowserWindow | null, ipcMain: Electron.IpcMain) {
|
||||
ipcMain.handle('adb', async () => {
|
||||
await createConnection()
|
||||
const adbConnectionList: { [key: string]: Adb } = {}
|
||||
|
||||
ipcMain.handle('adb:tcp:socket:getprop', async (_event, serial: string, key: string) => {
|
||||
if (!adbConnectionList[serial]) return Promise.reject('adb is not connected')
|
||||
return adbConnectionList[serial].getProp(key)
|
||||
})
|
||||
|
||||
ipcMain.handle('adb:shell', async (_event, command: string) => {
|
||||
if (!adb) {
|
||||
return
|
||||
ipcMain.handle('adb:tcp:socket:getDevices', () => {
|
||||
return Object.keys(adbConnectionList).map(serial => {
|
||||
return {
|
||||
name: adbConnectionList[serial].banner.product,
|
||||
serial: serial
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
ipcMain.handle('adb:tcp:socket:connect', async (_event, { host, port }: { host: string; port: number }) => {
|
||||
const device: AdbDaemonDirectSocketsDevice = new AdbDaemonDirectSocketsDevice({
|
||||
host,
|
||||
port
|
||||
})
|
||||
|
||||
if (Object.keys(adbConnectionList).includes(device.serial)) {
|
||||
return Promise.resolve({ name: device.name, serial: device.serial })
|
||||
}
|
||||
|
||||
const process = await adb.subprocess.shell(command)
|
||||
let result: string | null = null
|
||||
await process.stdout.pipeThrough(new DecodeUtf8Stream()).pipeTo(
|
||||
new WritableStream({
|
||||
write(chunk) {
|
||||
result += chunk
|
||||
}
|
||||
try {
|
||||
const connection: ReadableWritablePair<AdbPacketData, Consumable<AdbPacketInit>> = await device.connect()
|
||||
|
||||
const credentialStore: AdbWebCredentialStore = new AdbWebCredentialStore()
|
||||
|
||||
const transport = await AdbDaemonTransport.authenticate({
|
||||
serial: device.serial,
|
||||
connection: connection,
|
||||
credentialStore: credentialStore
|
||||
})
|
||||
)
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
async function createConnection() {
|
||||
const connector: AdbServerNodeTcpConnector = new AdbServerNodeTcpConnector({
|
||||
host: 'localhost',
|
||||
port: 5037
|
||||
adbConnectionList[device.serial] = new Adb(transport)
|
||||
|
||||
const productName = await adbConnectionList[device.serial].getProp('ro.product.name')
|
||||
return { name: productName, serial: device.serial }
|
||||
} catch (e) {
|
||||
return Promise.reject(e)
|
||||
}
|
||||
})
|
||||
|
||||
console.log('Connecting to ADB server...')
|
||||
connector.connect()
|
||||
ipcMain.handle(
|
||||
'adb:tcp:socket:shell',
|
||||
async (_event, { serial, command, callbackId }: { serial: string; command: string; callbackId: string }) => {
|
||||
if (!adbConnectionList[serial]) return Promise.reject('adb is not connected')
|
||||
|
||||
const client: AdbServerClient = new AdbServerClient(connector)
|
||||
const process = await adbConnectionList[serial].subprocess.shell(command)
|
||||
|
||||
const devices: AdbServerDevice[] = await client.getDevices()
|
||||
if (devices.length === 0) {
|
||||
console.log('No device found')
|
||||
return
|
||||
}
|
||||
await process.stdout
|
||||
.pipeTo(
|
||||
new WritableStream({
|
||||
write(chunk) {
|
||||
win.webContents.send(callbackId, chunk)
|
||||
}
|
||||
})
|
||||
)
|
||||
.then(() => {
|
||||
process.stdin.close()
|
||||
process.stderr.cancel()
|
||||
process.stdout.cancel()
|
||||
process.kill()
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
console.log('Devices found:', devices.map(device => device.serial).join(', '))
|
||||
ipcMain.handle(
|
||||
'adb:tcp:socket:spawn',
|
||||
async (_event, { serial, command, callbackId }: { serial: string; command: string; callbackId: string }) => {
|
||||
if (!adbConnectionList[serial]) return Promise.reject('adb is not connected')
|
||||
|
||||
const device: AdbServerDevice | undefined = devices.find(device => device.serial === 'd')
|
||||
const process = await adbConnectionList[serial].subprocess.spawn(command)
|
||||
|
||||
if (!device) {
|
||||
console.log('No device found')
|
||||
return
|
||||
}
|
||||
let buffer: Uint8Array = new Uint8Array()
|
||||
|
||||
console.log('Device found:', device.serial)
|
||||
const reader = process.stdout.getReader()
|
||||
|
||||
const transport: AdbServerTransport = await client.createTransport(device)
|
||||
adb = new Adb(transport)
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const { value, done } = await reader.read()
|
||||
if (done) break
|
||||
const oldBuffer = buffer
|
||||
buffer = new Uint8Array(value.length + buffer.length)
|
||||
buffer.set(oldBuffer)
|
||||
buffer.set(value, oldBuffer.length)
|
||||
}
|
||||
|
||||
reader.releaseLock()
|
||||
await reader.cancel()
|
||||
|
||||
await process.stdin.close()
|
||||
await process.stderr.cancel()
|
||||
await process.stdout.cancel()
|
||||
|
||||
await process.kill()
|
||||
|
||||
win.webContents.send(callbackId, buffer)
|
||||
}
|
||||
)
|
||||
|
||||
ipcMain.handle('adb:tcp:socket:disconnect', async (_event, serial: string) => {
|
||||
await adbConnectionList[serial].close()
|
||||
delete adbConnectionList[serial]
|
||||
})
|
||||
}
|
||||
|
|
|
|||
57
client-electron/electron/adbaemonDirectSocketsDevice.ts
Normal file
57
client-electron/electron/adbaemonDirectSocketsDevice.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import type { AdbDaemonDevice } from '@yume-chan/adb'
|
||||
import { AdbPacket, AdbPacketSerializeStream } from '@yume-chan/adb'
|
||||
import {
|
||||
StructDeserializeStream,
|
||||
UnwrapConsumableStream,
|
||||
WrapReadableStream,
|
||||
WrapWritableStream
|
||||
} from '@yume-chan/stream-extra'
|
||||
import { TCPSocket } from './socketToTCPSocket'
|
||||
|
||||
export interface AdbDaemonDirectSocketDeviceOptions {
|
||||
host: string
|
||||
port?: number
|
||||
name?: string
|
||||
unref?: boolean
|
||||
}
|
||||
|
||||
export class AdbDaemonDirectSocketsDevice implements AdbDaemonDevice {
|
||||
static isSupported(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
#options: AdbDaemonDirectSocketDeviceOptions
|
||||
|
||||
readonly serial: string
|
||||
|
||||
get host(): string {
|
||||
return this.#options.host
|
||||
}
|
||||
|
||||
readonly port: number
|
||||
|
||||
get name(): string | undefined {
|
||||
return this.#options.name
|
||||
}
|
||||
|
||||
constructor(options: AdbDaemonDirectSocketDeviceOptions) {
|
||||
this.#options = options
|
||||
this.port = options.port ?? 5555
|
||||
this.serial = `${this.host}:${this.port}`
|
||||
}
|
||||
|
||||
async connect() {
|
||||
const socket = new TCPSocket(this.host, this.port, {
|
||||
noDelay: true,
|
||||
unref: this.#options.unref
|
||||
})
|
||||
const { readable, writable } = await socket.opened
|
||||
|
||||
return {
|
||||
readable: new WrapReadableStream(readable).pipeThrough(new StructDeserializeStream(AdbPacket)),
|
||||
writable: new WrapWritableStream(writable)
|
||||
.bePipedThroughFrom(new UnwrapConsumableStream())
|
||||
.bePipedThroughFrom(new AdbPacketSerializeStream())
|
||||
}
|
||||
}
|
||||
}
|
||||
8
client-electron/electron/electron-env.d.ts
vendored
8
client-electron/electron/electron-env.d.ts
vendored
|
|
@ -27,4 +27,12 @@ interface Window {
|
|||
ipcRenderer: import('electron').IpcRenderer
|
||||
electronRuntime: boolean
|
||||
platform: NodeJS.Platform
|
||||
adbNativeTcpSocket: {
|
||||
getProp(serial: string, key: string): Promise<string>
|
||||
getDevices(): Promise<{ name: string; serial: string }[]>
|
||||
connect(host: string, port: number): Promise<{ name: string; serial: string }>
|
||||
shell(serial: string, command: string, callback: (chunk: Uint8Array) => void): Promise<void>
|
||||
spawn(serial: string, command: string, callback: (chunk: Uint8Array) => void): Promise<void>
|
||||
disconnect(): Promise<void>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { findCredentials, getPassword, setPassword, deletePassword } from '@postman/node-keytar'
|
||||
|
||||
export function eventGetKeyChain(icpMain: Electron.IpcMain) {
|
||||
icpMain.handle('get-keyChain', async (_event, serviceName, account) => {
|
||||
icpMain.handle('get-keyChain', async (_event, { serviceName, account }) => {
|
||||
return getPassword(serviceName, account)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { app, BrowserWindow, ipcMain, session, shell } from 'electron'
|
|||
import path from 'node:path'
|
||||
import deeplink from './deeplink'
|
||||
import { eventGetKeyChain } from './keychain'
|
||||
import { AdbDaemon } from './adb'
|
||||
import { AdbTcpSocket } from './adb'
|
||||
|
||||
// The built directory structure
|
||||
//
|
||||
|
|
@ -125,7 +125,7 @@ app.whenReady().then(() => {
|
|||
deeplink(app, win, ipcMain, shell)
|
||||
|
||||
// adb
|
||||
AdbDaemon(win, ipcMain)
|
||||
AdbTcpSocket(win, ipcMain)
|
||||
|
||||
//keychain
|
||||
eventGetKeyChain(ipcMain)
|
||||
|
|
@ -146,3 +146,8 @@ app.whenReady().then(() => {
|
|||
callback({ responseHeaders: details.responseHeaders })
|
||||
})
|
||||
})
|
||||
|
||||
process.on('unhandledRejection', () => {
|
||||
console.error('Unhandled Rejection ignoring')
|
||||
// Application specific logging, throwing an error, or other logic here
|
||||
})
|
||||
|
|
|
|||
|
|
@ -6,6 +6,38 @@ import { contextBridge, ipcRenderer } from 'electron'
|
|||
contextBridge.exposeInMainWorld('ipcRenderer', withPrototype(ipcRenderer))
|
||||
contextBridge.exposeInMainWorld('electronRuntime', true)
|
||||
contextBridge.exposeInMainWorld('platform', process.platform)
|
||||
contextBridge.exposeInMainWorld('adbNativeTcpSocket', {
|
||||
async getProp(serial: string, key: string) {
|
||||
return ipcRenderer.invoke('adb:tcp:socket:getprop', serial, key)
|
||||
},
|
||||
getDevices() {
|
||||
return ipcRenderer.invoke('adb:tcp:socket:getDevices')
|
||||
},
|
||||
async connect(host: string, port: number) {
|
||||
return ipcRenderer.invoke('adb:tcp:socket:connect', { host, port })
|
||||
},
|
||||
async shell(serial: string, command: string, callback: (chunk: Uint8Array) => void) {
|
||||
// generate a unique id for the callback
|
||||
const callbackId = `adb:tcp:socket:shell:output:${Date.now()}`
|
||||
|
||||
ipcRenderer.on(callbackId, (_event, chunk: Uint8Array) => {
|
||||
callback(chunk)
|
||||
})
|
||||
await ipcRenderer.invoke('adb:tcp:socket:shell', { serial, command, callbackId })
|
||||
},
|
||||
async spawn(serial: string, command: string, callback: (chunk: Uint8Array) => void) {
|
||||
const callbackId = `adb:tcp:socket:spawn:output:${Date.now()}`
|
||||
|
||||
ipcRenderer.on(callbackId, (_event, chunk: Uint8Array) => {
|
||||
callback(chunk)
|
||||
})
|
||||
|
||||
await ipcRenderer.invoke('adb:tcp:socket:spawn', { serial, command, callbackId })
|
||||
},
|
||||
disconnect() {
|
||||
return ipcRenderer.invoke('adb:tcp:socket:disconnect')
|
||||
}
|
||||
})
|
||||
|
||||
// `exposeInMainWorld` can't detect attributes and methods of `prototype`, manually patching it.
|
||||
function withPrototype(obj: Record<string, any>) {
|
||||
|
|
@ -27,9 +59,7 @@ function withPrototype(obj: Record<string, any>) {
|
|||
}
|
||||
|
||||
// --------- Preload scripts loading ---------
|
||||
function domReady(
|
||||
condition: DocumentReadyState[] = ['complete', 'interactive']
|
||||
) {
|
||||
function domReady(condition: DocumentReadyState[] = ['complete', 'interactive']) {
|
||||
return new Promise(resolve => {
|
||||
if (condition.includes(document.readyState)) {
|
||||
resolve(true)
|
||||
|
|
|
|||
86
client-electron/electron/socketToTCPSocket.ts
Normal file
86
client-electron/electron/socketToTCPSocket.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import { PromiseResolver } from '@yume-chan/async'
|
||||
import { PushReadableStream, WritableStream, type ReadableStream } from '@yume-chan/stream-extra'
|
||||
import { connect, type Socket } from 'node:net'
|
||||
|
||||
export interface TCPSocketOptions {
|
||||
noDelay?: boolean
|
||||
unref?: boolean
|
||||
}
|
||||
|
||||
export interface TCPSocketOpenInfo {
|
||||
readable: ReadableStream<Uint8Array>
|
||||
writable: WritableStream<Uint8Array>
|
||||
|
||||
remoteAddress: string
|
||||
remotePort: number
|
||||
|
||||
localAddress: string
|
||||
localPort: number
|
||||
}
|
||||
|
||||
export class TCPSocket {
|
||||
#socket: Socket
|
||||
#opened = new PromiseResolver<TCPSocketOpenInfo>()
|
||||
get opened(): Promise<TCPSocketOpenInfo> {
|
||||
return this.#opened.promise
|
||||
}
|
||||
|
||||
constructor(remoteAddress: string, remotePort: number, options?: TCPSocketOptions) {
|
||||
this.#socket = connect(remotePort, remoteAddress)
|
||||
|
||||
if (options?.noDelay) {
|
||||
this.#socket.setNoDelay(true)
|
||||
}
|
||||
if (options?.unref) {
|
||||
this.#socket.unref()
|
||||
}
|
||||
|
||||
this.#socket.on('connect', () => {
|
||||
const readable = new PushReadableStream<Uint8Array>(controller => {
|
||||
this.#socket.on('data', async data => {
|
||||
this.#socket.pause()
|
||||
await controller.enqueue(data)
|
||||
this.#socket.resume()
|
||||
})
|
||||
|
||||
this.#socket.on('end', () => {
|
||||
try {
|
||||
controller.close()
|
||||
} catch {
|
||||
console.error('Controller already closed')
|
||||
}
|
||||
})
|
||||
|
||||
controller.abortSignal.addEventListener('abort', () => {
|
||||
this.#socket.end()
|
||||
})
|
||||
})
|
||||
|
||||
this.#opened.resolve({
|
||||
remoteAddress,
|
||||
remotePort,
|
||||
localAddress: this.#socket.localAddress!,
|
||||
localPort: this.#socket.localPort!,
|
||||
readable,
|
||||
writable: new WritableStream({
|
||||
write: async chunk => {
|
||||
return new Promise<void>(resolve => {
|
||||
if (!this.#socket.write(chunk)) {
|
||||
this.#socket.once('drain', resolve)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
},
|
||||
close: async () => {
|
||||
this.#socket.end()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
this.#socket.on('error', error => {
|
||||
this.#opened.reject(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue