feat: 🎉 Add gnirehtet reverse tethering function

This commit is contained in:
viarotel 2023-10-30 16:36:16 +08:00
parent f9a32d6f28
commit 2c9718997b
19 changed files with 293 additions and 39 deletions

View File

@ -54,12 +54,18 @@
### macOS && Linux
> 注意:这些平台没有集成 [Adb](https://developer.android.com/studio/releases/platform-tools?hl=zh-cn) 及 [Scrcpy](https://github.com/Genymobile/scrcpy) 需要手动安装
> 注意:这些平台没有集成 [Scrcpy](https://github.com/Genymobile/scrcpy) 需要手动安装
1. Linux 可参阅的 [安装文档](https://github.com/Genymobile/scrcpy/blob/master/doc/linux.md)
2. macOS 可参阅的 [安装文档](https://github.com/Genymobile/scrcpy/blob/master/doc/macos.md)
3. 安装上述依赖成功后步骤同 USB 连接 和 WIFI 连接
### Gnirehtet 反向供网
> 注意: macOS 内部没有集成如需使用需要手动安装 [安装文档](https://github.com/Genymobile/gnirehtet)
Windows 及 Linux 端内部集成了 Gnirehtet 用于提供 PC 到安卓设备的反向供网功能。
## 快捷键
请参阅 [scrcpy/doc/shortcuts](https://github.com/Genymobile/scrcpy/blob/master/doc/shortcuts.md)
@ -135,7 +141,7 @@
8. 添加 macOS 及 linux 操作系统的支持 ✅
9. 支持国际化 ✅
10. 对深色模式的支持 ✅
11. 添加 Gnirehtet 反向供网功能 🚧
11. 添加 Gnirehtet 反向供网功能
12. 添加对游戏的增强功能,如游戏键位映射 🚧
## 常见问题

View File

@ -58,6 +58,12 @@
2. Refer to the [installation document](https://github.com/Genymobile/scrcpy/blob/master/doc/macos.md) for macOS
3. Follow steps in USB Connection and WIFI Connection after dependencies are installed successfully
### Gnirehtet Reverse Tethering
> Note: macOS does not have Gnirehtet built-in. You need to manually install it to use this feature [Installation Guide](https://github.com/Genymobile/gnirehtet).
Gnirehtet is built into the Windows and Linux apps to provide reverse tethering from PC to Android devices.
## Shortcuts
Refer to [scrcpy/doc/shortcuts](https://github.com/Genymobile/scrcpy/blob/master/doc/shortcuts.md)
@ -133,7 +139,7 @@ Refer to [scrcpy/doc/shortcuts](https://github.com/Genymobile/scrcpy/blob/master
8. Add support for macOS and linux operating systems ✅
9. Support internationalization ✅
10. Support for dark mode ✅
11. Add Gnirehtet reverse network function 🚧
11. Add Gnirehtet reverse network function
12. Add game enhancement features such as game keyboard mapping 🚧
## FAQ

View File

@ -0,0 +1,19 @@
import { extraResolve } from '@electron/helpers/index.js'
import which from 'which'
export const getGnirehtetPath = () => {
switch (process.platform) {
// case 'darwin':
// return extraResolve('mac/gnirehtet/gnirehtet')
case 'win32':
return extraResolve('win/gnirehtet/gnirehtet.exe')
case 'linux':
return extraResolve('linux/gnirehtet/gnirehtet')
default:
return which.sync('gnirehtet', { nothrow: true })
}
}
export const gnirehtetPath = getGnirehtetPath()
export const gnirehtetApkPath = extraResolve('common/gnirehtet/gnirehtet.apk')

View File

@ -6,6 +6,8 @@ export { adbPath } from './android-platform-tools/index.js'
export { scrcpyPath } from './scrcpy/index.js'
export { gnirehtetPath, gnirehtetApkPath } from './gnirehtet/index.js'
export const desktopPath = process.env.DESKTOP_PATH
export const devPublishPath = resolve('dev-publish.yml')

View File

@ -54,6 +54,8 @@ const getDeviceIP = async (id) => {
const reg = /inet ([0-9.]+)\/\d+/
const match = stdout.match(reg)
const value = match[1]
console.log('adbkit.getDeviceIP', value)
return value
}
catch (error) {
@ -97,6 +99,8 @@ const screencap = async (deviceId, options = {}) => {
const install = async (id, path) => client.getDevice(id).install(path)
const isInstalled = async (id, pkg) => client.getDevice(id).isInstalled(pkg)
const version = async () => client.version()
const display = async (deviceId) => {
@ -167,6 +171,7 @@ export default () => {
tcpip,
screencap,
install,
isInstalled,
version,
display,
watch,

View File

@ -0,0 +1,139 @@
import { spawn } from 'node:child_process'
import appStore from '@electron/helpers/store.js'
import {
adbPath,
gnirehtetApkPath,
gnirehtetPath,
} from '@electron/configs/index.js'
const appDebug = appStore.get('common.debug') || false
let adbkit = null
const shell = async (command, { debug = false, stdout, stderr } = {}) => {
const spawnPath = appStore.get('common.gnirehtet') || gnirehtetPath
const ADB = appStore.get('common.adbPath') || adbPath
const GNIREHTET_APK = gnirehtetApkPath
const args = command.split(' ')
console.log('gnirehtet.shell.spawnPath', spawnPath)
console.log('gnirehtet.shell.adbPath', adbPath)
const gnirehtetProcess = spawn(`"${spawnPath}"`, args, {
env: { ...process.env, ADB, GNIREHTET_APK },
shell: true,
encoding: 'utf8',
})
gnirehtetProcess.stdout.on('data', (data) => {
const stringData = data.toString()
if (debug) {
console.log('gnirehtetProcess.stdout.data:', stringData)
}
if (stdout) {
stdout(stringData, gnirehtetProcess)
}
})
gnirehtetProcess.stderr.on('data', (data) => {
const stringData = data.toString()
if (debug) {
console.error('gnirehtetProcess.stderr.data:', stringData)
}
if (stderr) {
stderr(stringData, gnirehtetProcess)
}
})
return new Promise((resolve, reject) => {
gnirehtetProcess.on('close', (code) => {
if (code === 0) {
resolve()
}
else {
reject(new Error(`Command failed with code ${code}`))
}
})
gnirehtetProcess.on('error', (err) => {
reject(err)
})
})
}
let relayProcess = null
const relay = async (args) => {
if (relayProcess) {
return relayProcess
}
return new Promise((resolve, reject) => {
shell('relay', {
...args,
debug: appDebug,
stdout: (_, process) => {
if (!relayProcess) {
relayProcess = process
}
resolve(process)
},
}).catch((error) => {
reject(error)
})
})
}
const install = deviceId => shell(`install ${deviceId}`)
const start = deviceId => shell(`start ${deviceId}`)
const stop = deviceId => shell(`stop ${deviceId}`)
const tunnel = deviceId => shell(`tunnel ${deviceId}`)
const installed = async (deviceId) => {
const res = await adbkit.isInstalled(deviceId, 'com.genymobile.gnirehtet')
console.log('gnirehtet.apk.installed', res)
return res
}
const run = async (deviceId) => {
await relay().catch((e) => {
throw new Error('Gnirehtet Relay fail')
})
console.log('run.relay.success')
await install(deviceId).catch((e) => {
throw new Error('Gnirehtet Install Client fail')
})
console.log('run.install.success')
await start(deviceId).catch((e) => {
throw new Error('Gnirehtet Start fail')
})
console.log('run.start.success')
}
window.addEventListener('beforeunload', () => {
stop()
if (relayProcess) {
relayProcess.kill()
}
})
export default (options = {}) => {
adbkit = options.adbkit
return {
shell,
relay,
install,
installed,
start,
stop,
tunnel,
run,
}
}

View File

@ -1,6 +1,6 @@
import path from 'node:path'
import log from '@electron/helpers/log.js'
import appLog from '@electron/helpers/log.js'
import '@electron/helpers/console.js'
import store from '@electron/helpers/store.js'
@ -9,12 +9,13 @@ import * as configs from '@electron/configs/index.js'
import electron from './electron/index.js'
import adbkit from './adbkit/index.js'
import scrcpy from './scrcpy/index.js'
import gnirehtet from './gnirehtet/index.js'
export default {
init(expose) {
expose('nodePath', path)
expose('appLog', log)
expose('appLog', appLog)
expose('appStore', store)
@ -23,7 +24,12 @@ export default {
configs,
})
expose('adbkit', adbkit({ log }))
expose('scrcpy', scrcpy({ log }))
const adbkitExecute = adbkit()
expose('adbkit', adbkitExecute)
expose('scrcpy', scrcpy())
expose('gnirehtet', gnirehtet({ adbkit: adbkitExecute }))
},
}

Binary file not shown.

View File

@ -0,0 +1,2 @@
@gnirehtet.exe run
@pause

Binary file not shown.

Binary file not shown.

View File

@ -53,24 +53,15 @@ export default {
},
methods: {
async showTips() {
if (this.$electron.process.platform === 'win32') {
return false
}
const { adbPath, scrcpyPath } = this.$electron?.configs || {}
if (adbPath) {
return false
}
const { scrcpyPath } = this.$electron?.configs || {}
if (scrcpyPath) {
return false
}
this.$alert(
`<div>该软件依赖与
<a class="hover:underline text-primary-500" href="https://developer.android.com/studio/releases/platform-tools?hl=zh-cn" target="_blank">adb</a>
以及
`<div>
该软件依赖与
<a class="hover:underline text-primary-500" href="https://github.com/Genymobile/scrcpy" target="_blank">scrcpy</a>
请确保已正确安装所述依赖项或者在偏好设置中手动配置依赖项所在位置
<div>`,

View File

@ -1,13 +1,18 @@
<template>
<div class="bg-primary-100 dark:bg-gray-800 -my-[8px]">
<div
ref="wheelContainer"
class="bg-primary-100 dark:bg-gray-800 -my-[8px] flex flex-nowrap overflow-hidden"
title="滚动查看被遮盖的菜单"
>
<el-button
v-for="(item, index) in controlModel"
:key="index"
type="primary"
plain
class="!border-none !mx-0 bg-transparent !rounded-0"
class="!border-none !mx-0 bg-transparent !rounded-0 flex-none"
:disabled="device.$unauthorized"
:title="item.tips"
:title="item.tips ? $t(item.tips) : ''"
@wheel.prevent="onWheel"
@click="handleClick(item)"
>
<template #icon>
@ -16,7 +21,7 @@
<component :is="item.elIcon" />
</el-icon>
</template>
{{ item.label }}
{{ $t(item.label) }}
</el-button>
</div>
</template>
@ -36,57 +41,88 @@ export default {
return {
controlModel: [
{
label: this.$t('device.control.switch'),
label: 'device.control.switch',
elIcon: 'Switch',
command: 'input keyevent KEYCODE_APP_SWITCH',
},
{
label: this.$t('device.control.home'),
label: 'device.control.home',
elIcon: 'HomeFilled',
command: 'input keyevent KEYCODE_HOME',
},
{
label: this.$t('device.control.return'),
label: 'device.control.return',
elIcon: 'Back',
command: 'input keyevent KEYCODE_BACK',
},
{
label: this.$t('device.control.notification'),
label: 'device.control.notification',
elIcon: 'Notification',
command: 'cmd statusbar expand-notifications',
tips: this.$t('device.control.notification.tips'),
tips: 'device.control.notification.tips',
},
{
label: this.$t('device.control.power'),
label: 'device.control.power',
elIcon: 'SwitchButton',
command: 'input keyevent KEYCODE_POWER',
tips: this.$t('device.control.power.tips'),
tips: 'device.control.power.tips',
},
{
label: this.$t('device.control.reboot'),
label: 'device.control.reboot',
elIcon: 'RefreshLeft',
command: 'reboot',
},
{
label: this.$t('device.control.capture'),
label: 'device.control.capture',
elIcon: 'Crop',
handle: this.handleScreenCap,
tips: '',
},
{
label: this.$t('device.control.install'),
label: 'device.control.install',
svgIcon: 'install',
handle: this.handleInstall,
tips: '',
},
{
label: 'device.control.gnirehtet',
elIcon: 'Link',
handle: this.handleGnirehtet,
tips: 'device.control.gnirehtet.tips',
},
],
}
},
computed: {},
methods: {
onWheel(event) {
const container = this.$refs.wheelContainer
container.scrollLeft += event.deltaY
},
preferenceData(...args) {
return this.$store.preference.getData(...args)
},
async handleGnirehtet(device) {
const messageEl = this.$message({
message: this.$t('device.control.gnirehtet.progress', {
deviceName: device.$name,
}),
icon: LoadingIcon,
duration: 0,
})
try {
await this.$gnirehtet.run(device.id)
this.$message.success(this.$t('device.control.gnirehtet.success'))
}
catch (error) {
if (error.message) {
this.$message.warning(error.message)
}
}
messageEl.close()
},
async handleInstall(device) {
let files = null

View File

@ -355,16 +355,28 @@ export default {
async handleWifi(row) {
try {
const host = await this.$adb.getDeviceIP(row.id)
const port = await this.$adb.tcpip(row.id, 5555)
if (!host) {
throw new Error(this.$t('device.wireless.mode.error'))
}
this.formData.host = host
const port = await this.$adb.tcpip(row.id, 5555)
this.formData.port = port
console.log('host:port', `${host}:${port}`)
await sleep()
this.handleConnect()
}
catch (error) {
console.warn(error.message)
if (error?.message || error?.cause?.message) {
this.$message.warning(error?.message || error?.cause?.message)
}
}
},
onPairSuccess() {

View File

@ -21,6 +21,7 @@
"device.permission.error": "Device permission error, please reconnect device and allow USB debugging",
"device.wireless.name": "Wireless",
"device.wireless.mode": "Wireless Mode",
"device.wireless.mode.error": "Do not get the local area network connection address, please check the network",
"device.wireless.connect.name": "Connect",
"device.wireless.connect.error.title": "Connect failed",
"device.wireless.connect.error.detail": "Error details",
@ -83,6 +84,10 @@
"device.control.return": "Return",
"device.control.home": "Home",
"device.control.switch": "Switch",
"device.control.gnirehtet": "Gnirehtet",
"device.control.gnirehtet.tips": "Gnirehtet provides reverse tethering for Android; Note: Initial connection requires authorization on the device.",
"device.control.gnirehtet.progress": "Starting Gnirehtet reverse tethering service...",
"device.control.gnirehtet.success": "Gnirehtet reverse tethering feature started successfully",
"preferences.name": "Preferences",
"preferences.reset": "Reset to Default",
"preferences.scope.global": "Global",
@ -119,6 +124,9 @@
"preferences.common.scrcpy.name": "Scrcpy Path",
"preferences.common.scrcpy.placeholder": "Set scrcpy path",
"preferences.common.scrcpy.tips": "scrcpy path to connect device",
"preferences.common.gnirehtet.name": "Gnirehtet Path",
"preferences.common.gnirehtet.placeholder": "Set gnirehtet path",
"preferences.common.gnirehtet.tips": "The path for gnirehtet used to provide reverse tethering for devices.",
"preferences.common.language.name": "Language",
"preferences.common.language.placeholder": "Select language",
"preferences.common.language.chinese": "中文",

View File

@ -21,6 +21,7 @@
"device.permission.error": "设备可能未授权成功请重新插拔设备并点击允许USB调试",
"device.wireless.name": "无线连接",
"device.wireless.mode": "无线模式",
"device.wireless.mode.error": "没有获取到局域网连接地址,请检查网络",
"device.wireless.connect.name": "连接设备",
"device.wireless.connect.error.title": "连接设备失败",
"device.wireless.connect.error.detail": "错误详情",
@ -83,6 +84,10 @@
"device.control.return": "返回键",
"device.control.home": "主屏幕",
"device.control.switch": "切换键",
"device.control.gnirehtet": "反向供网",
"device.control.gnirehtet.tips": "使用 Gnirehtet 为 Android 提供反向网络共享;注意:首次连接需要在设备上进行授权",
"device.control.gnirehtet.progress": "正在启动 Gnirehtet 反向供网服务中...",
"device.control.gnirehtet.success": "Gnirehtet 反向网络共享功能启动成功",
"preferences.name": "偏好设置",
"preferences.reset": "恢复默认值",
"preferences.scope.global": "全局",
@ -119,6 +124,9 @@
"preferences.common.scrcpy.name": "scrcpy 路径",
"preferences.common.scrcpy.placeholder": "请设置 scrcpy 路径",
"preferences.common.scrcpy.tips": "用于连接设备的 scrcpy 地址。",
"preferences.common.gnirehtet.name": "gnirehtet 路径",
"preferences.common.gnirehtet.placeholder": "请设置 gnirehtet 路径",
"preferences.common.gnirehtet.tips": "用于为设备反向供网的 gnirehtet 地址。",
"preferences.common.language.name": "语言",
"preferences.common.language.placeholder": "选择你需要的语言",
"preferences.common.language.chinese": "中文",

View File

@ -26,13 +26,14 @@ app.use(icons)
app.use(i18n)
window.t = t
app.config.globalProperties.$electron = window.electron
app.config.globalProperties.$adb = window.adbkit
app.config.globalProperties.$scrcpy = window.scrcpy
app.config.globalProperties.$path = window.nodePath
app.config.globalProperties.$appStore = window.appStore
app.config.globalProperties.$appLog = window.appLog
app.config.globalProperties.$electron = window.electron
app.config.globalProperties.$adb = window.adbkit
app.config.globalProperties.$scrcpy = window.scrcpy
app.config.globalProperties.$gnirehtet = window.gnirehtet
app.config.globalProperties.$replaceIP = replaceIP

View File

@ -1,4 +1,5 @@
const { adbPath, scrcpyPath, desktopPath } = window?.electron?.configs || {}
const { adbPath, scrcpyPath, gnirehtetPath, desktopPath }
= window?.electron?.configs || {}
const defaultLanguage = window.electron?.process?.env?.LOCALE
@ -76,6 +77,18 @@ export default {
properties: ['openFile'],
filters: [{ name: 'preferences.common.scrcpy.name', extensions: ['*'] }],
},
gnirehtetPath: {
label: 'preferences.common.gnirehtet.name',
field: 'gnirehtetPath',
value: gnirehtetPath,
type: 'Input.path',
placeholder: 'preferences.common.gnirehtet.placeholder',
tips: 'preferences.common.gnirehtet.tips',
properties: ['openFile'],
filters: [
{ name: 'preferences.common.gnirehtet.name', extensions: ['*'] },
],
},
debug: {
label: 'preferences.common.debug.name',
field: 'debug',