feat(grpc): grpc模拟
parent
7ac0003942
commit
587cdb0e0d
|
|
@ -36,6 +36,10 @@ export default defineConfig({
|
|||
path: '/',
|
||||
component: '@/pages/components/Layout/index',
|
||||
routes: [
|
||||
{
|
||||
path: '/grpc',
|
||||
component: '@/pages/grpc/grpc',
|
||||
},
|
||||
{
|
||||
path: '/welcome',
|
||||
component: '@/pages/welcome',
|
||||
|
|
|
|||
|
|
@ -10,13 +10,17 @@
|
|||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.0.0",
|
||||
"@grpc/grpc-js": "^1.13.4",
|
||||
"@grpc/proto-loader": "^0.8.0",
|
||||
"antd": "^5.26.6",
|
||||
"axios": "^1.11.0",
|
||||
"classnames": "^2.5.1",
|
||||
"google-protobuf": "^4.0.0",
|
||||
"umi": "^4.0.42"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node14": "^1.0.3",
|
||||
"@types/google-protobuf": "^3.15.12",
|
||||
"@types/react": "^18.0.0",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"@umijs/plugin-electron": "^0.2.0",
|
||||
|
|
@ -3207,6 +3211,55 @@
|
|||
"use-isomorphic-layout-effect": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@grpc/grpc-js": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmmirror.com/@grpc/grpc-js/-/grpc-js-1.13.4.tgz",
|
||||
"integrity": "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@grpc/proto-loader": "^0.7.13",
|
||||
"@js-sdsl/ordered-map": "^4.4.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": {
|
||||
"version": "0.7.15",
|
||||
"resolved": "https://registry.npmmirror.com/@grpc/proto-loader/-/proto-loader-0.7.15.tgz",
|
||||
"integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"long": "^5.0.0",
|
||||
"protobufjs": "^7.2.5",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
"proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@grpc/proto-loader": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmmirror.com/@grpc/proto-loader/-/proto-loader-0.8.0.tgz",
|
||||
"integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"long": "^5.0.0",
|
||||
"protobufjs": "^7.5.3",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
"proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array": {
|
||||
"version": "0.13.0",
|
||||
"resolved": "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
|
||||
|
|
@ -3600,6 +3653,16 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@js-sdsl/ordered-map": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmmirror.com/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz",
|
||||
"integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/js-sdsl"
|
||||
}
|
||||
},
|
||||
"node_modules/@loadable/component": {
|
||||
"version": "5.15.2",
|
||||
"resolved": "https://registry.npmmirror.com/@loadable/component/-/component-5.15.2.tgz",
|
||||
|
|
@ -4153,6 +4216,70 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/aspromise": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/base64": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/@protobufjs/base64/-/base64-1.1.2.tgz",
|
||||
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/codegen": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/@protobufjs/codegen/-/codegen-2.0.4.tgz",
|
||||
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/eventemitter": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
|
||||
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/fetch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@protobufjs/fetch/-/fetch-1.1.0.tgz",
|
||||
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.1",
|
||||
"@protobufjs/inquire": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/float": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/@protobufjs/float/-/float-1.0.2.tgz",
|
||||
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/inquire": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@protobufjs/inquire/-/inquire-1.1.0.tgz",
|
||||
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/path": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/@protobufjs/path/-/path-1.1.2.tgz",
|
||||
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/pool": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@protobufjs/pool/-/pool-1.1.0.tgz",
|
||||
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/utf8": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@protobufjs/utf8/-/utf8-1.1.0.tgz",
|
||||
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@rc-component/async-validator": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/@rc-component/async-validator/-/async-validator-5.0.4.tgz",
|
||||
|
|
@ -4791,6 +4918,13 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/google-protobuf": {
|
||||
"version": "3.15.12",
|
||||
"resolved": "https://registry.npmmirror.com/@types/google-protobuf/-/google-protobuf-3.15.12.tgz",
|
||||
"integrity": "sha512-40um9QqwHjRS92qnOaDpL7RmDK15NuZYo9HihiJRbYkMQZlWnuH8AdvbMy8/o6lgLmKbDUKa+OALCltHdbOTpQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/graceful-fs": {
|
||||
"version": "4.1.9",
|
||||
"resolved": "https://registry.npmmirror.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
|
||||
|
|
@ -12033,6 +12167,12 @@
|
|||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/google-protobuf": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/google-protobuf/-/google-protobuf-4.0.0.tgz",
|
||||
"integrity": "sha512-b8wmenhUMf2WNL+xIJ/slvD/hEE6V3nRnG86O2bzkBrMweM9gnqZE1dfXlDjibY3aXJXDNbAHepevYyQ7qWKsQ==",
|
||||
"license": "(BSD-3-Clause AND Apache-2.0)"
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
|
||||
|
|
@ -14102,6 +14242,12 @@
|
|||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.camelcase": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.debounce": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
||||
|
|
@ -14140,6 +14286,12 @@
|
|||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmmirror.com/long/-/long-5.3.2.tgz",
|
||||
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
|
|
@ -16664,6 +16816,30 @@
|
|||
"license": "ISC",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/protobufjs": {
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmmirror.com/protobufjs/-/protobufjs-7.5.4.tgz",
|
||||
"integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.2",
|
||||
"@protobufjs/base64": "^1.1.2",
|
||||
"@protobufjs/codegen": "^2.0.4",
|
||||
"@protobufjs/eventemitter": "^1.1.0",
|
||||
"@protobufjs/fetch": "^1.1.0",
|
||||
"@protobufjs/float": "^1.0.2",
|
||||
"@protobufjs/inquire": "^1.1.0",
|
||||
"@protobufjs/path": "^1.1.2",
|
||||
"@protobufjs/pool": "^1.1.0",
|
||||
"@protobufjs/utf8": "^1.1.0",
|
||||
"@types/node": ">=13.7.0",
|
||||
"long": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
|
|
|
|||
|
|
@ -17,13 +17,17 @@
|
|||
"homepage": "http://10.209.8.11/users/sign_in",
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.0.0",
|
||||
"@grpc/grpc-js": "^1.13.4",
|
||||
"@grpc/proto-loader": "^0.8.0",
|
||||
"antd": "^5.26.6",
|
||||
"axios": "^1.11.0",
|
||||
"classnames": "^2.5.1",
|
||||
"google-protobuf": "^4.0.0",
|
||||
"umi": "^4.0.42"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node14": "^1.0.3",
|
||||
"@types/google-protobuf": "^3.15.12",
|
||||
"@types/react": "^18.0.0",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"@umijs/plugin-electron": "^0.2.0",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,194 @@
|
|||
// src/grpc/BTGrpcClient.ts 调用后端服务
|
||||
import * as grpc from '@grpc/grpc-js';
|
||||
import * as protoLoader from '@grpc/proto-loader';
|
||||
import path from 'path';
|
||||
import { app } from 'electron';
|
||||
|
||||
export interface DownloadRequest {
|
||||
torrent_url: string;
|
||||
item_name: string;
|
||||
item_id?: string;
|
||||
}
|
||||
|
||||
export interface ProgressCallback {
|
||||
(progress: ProgressUpdate): void;
|
||||
}
|
||||
|
||||
export interface ProgressUpdate {
|
||||
download_id: string;
|
||||
progress: number;
|
||||
download_speed: number;
|
||||
upload_speed: number;
|
||||
eta: number;
|
||||
total_size: number;
|
||||
downloaded_size: number;
|
||||
state: string;
|
||||
}
|
||||
|
||||
export class BTGrpcClient {
|
||||
private client: any;
|
||||
private progressCallbacks: Map<string, ProgressCallback[]> = new Map();
|
||||
private progressStream: any = null;
|
||||
|
||||
constructor() {
|
||||
this.initializeClient();
|
||||
}
|
||||
|
||||
private initializeClient() {
|
||||
try {
|
||||
const PROTO_PATH = path.join(__dirname, 'protos', 'bittorrent.proto');
|
||||
|
||||
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
|
||||
keepCase: true,
|
||||
longs: String,
|
||||
enums: String,
|
||||
defaults: true,
|
||||
oneofs: true,
|
||||
});
|
||||
|
||||
const protoDescriptor = grpc.loadPackageDefinition(packageDefinition);
|
||||
const bittorrent = protoDescriptor.bittorrent as any;
|
||||
|
||||
// 连接到后端 Agent,假设运行在 localhost:50051
|
||||
this.client = new bittorrent.BTDownloadService(
|
||||
'localhost:50051',
|
||||
grpc.credentials.createInsecure()
|
||||
);
|
||||
|
||||
console.log('gRPC客户端初始化成功');
|
||||
this.setupProgressStream();
|
||||
} catch (error) {
|
||||
console.error('gRPC客户端初始化失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置进度流监听
|
||||
private setupProgressStream() {
|
||||
try {
|
||||
this.progressStream = this.client.SubscribeProgress({});
|
||||
// 注册数据事件监听器,接收进度更新
|
||||
this.progressStream.on('data', (progress: ProgressUpdate) => {
|
||||
console.log('收到进度数据:', progress); // 添加调试日志
|
||||
// BTGrpcClient 通过IPC将进度发送给主进程
|
||||
this.progressCallbacks.forEach((callbacks, downloadId) => {
|
||||
if (downloadId === 'all' || downloadId === progress.download_id) {
|
||||
callbacks.forEach(callback => callback(progress));
|
||||
}
|
||||
});
|
||||
|
||||
// 如果有针对特定下载ID的回调,也要通知
|
||||
const specificCallbacks = this.progressCallbacks.get(progress.download_id);
|
||||
if (specificCallbacks) {
|
||||
specificCallbacks.forEach(callback => callback(progress));
|
||||
}
|
||||
});
|
||||
|
||||
this.progressStream.on('error', (error: Error) => {
|
||||
console.error('进度流错误:', error);
|
||||
});
|
||||
|
||||
this.progressStream.on('end', () => {
|
||||
console.log('进度流结束');
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('设置进度流失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 开始下载
|
||||
startDownload(request: DownloadRequest): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// MockBTService 接收请求,创建下载任务
|
||||
this.client.StartDownload(request, (error: grpc.ServiceError, response: any) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(response);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 停止下载
|
||||
stopDownload(downloadId: string): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.client.StopDownload({ download_id: downloadId }, (error: grpc.ServiceError, response: any) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(response);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 注册进度回调
|
||||
registerProgressCallback(downloadId: string, callback: ProgressCallback) {
|
||||
if (!this.progressCallbacks.has(downloadId)) {
|
||||
this.progressCallbacks.set(downloadId, []);
|
||||
}
|
||||
this.progressCallbacks.get(downloadId)!.push(callback);
|
||||
}
|
||||
|
||||
// 移除进度回调
|
||||
removeProgressCallback(downloadId: string, callback: ProgressCallback) {
|
||||
const callbacks = this.progressCallbacks.get(downloadId);
|
||||
if (callbacks) {
|
||||
const index = callbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
callbacks.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查连接状态
|
||||
isConnected(): boolean {
|
||||
return this.client && this.client.getChannel().getConnectivityState(true) === grpc.connectivityState.READY;
|
||||
}
|
||||
|
||||
// 重新连接
|
||||
reconnect() {
|
||||
try {
|
||||
this.client.close();
|
||||
this.initializeClient();
|
||||
} catch (error) {
|
||||
console.error('重新连接失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
getConnectionState(): string {
|
||||
if (!this.client) return 'NOT_CREATED';
|
||||
|
||||
const state = this.client.getChannel().getConnectivityState(false);
|
||||
const stateNames = {
|
||||
[grpc.connectivityState.IDLE]: 'IDLE',
|
||||
[grpc.connectivityState.CONNECTING]: 'CONNECTING',
|
||||
[grpc.connectivityState.READY]: 'READY',
|
||||
[grpc.connectivityState.TRANSIENT_FAILURE]: 'TRANSIENT_FAILURE',
|
||||
[grpc.connectivityState.SHUTDOWN]: 'SHUTDOWN'
|
||||
} as const;
|
||||
|
||||
// 使用类型断言确保 state 是合法的 key
|
||||
return stateNames[state as keyof typeof stateNames] || 'UNKNOWN';
|
||||
}
|
||||
|
||||
// 添加测试方法
|
||||
async testConnection(): Promise<boolean> {
|
||||
try {
|
||||
// 尝试调用一个简单的方法来测试连接
|
||||
await new Promise((resolve, reject) => {
|
||||
this.client.ListDownloads({}, (error: any, response: any) => {
|
||||
if (error && error.code !== grpc.status.UNIMPLEMENTED) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(response);
|
||||
}
|
||||
});
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('连接测试失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,323 @@
|
|||
// 本地测试用的 gRPC 服务器,用于模拟 BT 下载
|
||||
// src/grpc/MockBTService.ts
|
||||
import * as grpc from '@grpc/grpc-js';
|
||||
import * as protoLoader from '@grpc/proto-loader';
|
||||
import path from 'path';
|
||||
|
||||
// 进度更新接口
|
||||
interface ProgressUpdate {
|
||||
download_id: string;
|
||||
progress: number;
|
||||
download_speed: number;
|
||||
item_name?: string; // 可选的文件名字段
|
||||
upload_speed: number;
|
||||
eta: number;
|
||||
total_size: number;
|
||||
downloaded_size: number;
|
||||
state: string;
|
||||
}
|
||||
|
||||
// 模拟的下载任务
|
||||
interface MockDownloadTask {
|
||||
id: string;
|
||||
itemName: string;
|
||||
torrentUrl: string;
|
||||
progress: number;
|
||||
totalSize: number;
|
||||
downloadedSize: number;
|
||||
downloadSpeed: number;
|
||||
state: 'downloading' | 'completed' | 'error' | 'paused';
|
||||
startTime: number;
|
||||
}
|
||||
|
||||
export class MockBTService {
|
||||
private server: grpc.Server;
|
||||
private activeDownloads: Map<string, MockDownloadTask> = new Map();
|
||||
private progressIntervals: Map<string, NodeJS.Timeout> = new Map();
|
||||
// 存储所有的进度回调函数,发送信息
|
||||
private progressCallbacks: ((progress: ProgressUpdate) => void)[] = [];
|
||||
|
||||
constructor() {
|
||||
this.server = new grpc.Server();
|
||||
this.setupService();
|
||||
}
|
||||
|
||||
private setupService() {
|
||||
// 设置gRPC服务,加载 bittorrent.proto 定义,实现所有gRPC服务方法
|
||||
const PROTO_PATH = path.join(__dirname, 'protos', 'bittorrent.proto');
|
||||
|
||||
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
|
||||
keepCase: true,
|
||||
longs: String,
|
||||
enums: String,
|
||||
defaults: true,
|
||||
oneofs: true,
|
||||
});
|
||||
|
||||
const protoDescriptor = grpc.loadPackageDefinition(packageDefinition);
|
||||
const bittorrent = protoDescriptor.bittorrent as any;
|
||||
|
||||
// 实现 gRPC 服务方法
|
||||
this.server.addService(bittorrent.BTDownloadService.service, {
|
||||
StartDownload: this.startDownload.bind(this),
|
||||
StopDownload: this.stopDownload.bind(this),
|
||||
GetDownloadStatus: this.getDownloadStatus.bind(this),
|
||||
ListDownloads: this.listDownloads.bind(this),
|
||||
SubscribeProgress: this.subscribeProgress.bind(this),
|
||||
});
|
||||
}
|
||||
|
||||
// 开始下载
|
||||
// 在 startDownload 方法中,可以添加根据文件名设置不同大小的逻辑
|
||||
private startDownload(call: grpc.ServerUnaryCall<any, any>, callback: grpc.sendUnaryData<any>) {
|
||||
const { torrent_url, item_name, item_id } = call.request;
|
||||
const downloadId = item_id || `download-${Date.now()}`;
|
||||
|
||||
console.log(`模拟开始下载: ${item_name}, ID: ${downloadId}`);
|
||||
|
||||
// 根据文件名或ID设置不同大小的文件
|
||||
let fileSize = 1024 * 1024 * 100; // 默认100MB
|
||||
|
||||
if (item_name.includes('large') || item_name.includes('大文件')) {
|
||||
fileSize = 4 * 1024 * 1024 * 1024; // 4GB
|
||||
} else if (item_name.includes('medium') || item_name.includes('中等')) {
|
||||
fileSize = 1024 * 1024 * 1024; // 1GB
|
||||
} else if (item_name.includes('small') || item_name.includes('小文件')) {
|
||||
fileSize = 100 * 1024 * 1024; // 100MB
|
||||
}
|
||||
|
||||
// 创建模拟下载任务
|
||||
const task: MockDownloadTask = {
|
||||
id: downloadId,
|
||||
itemName: item_name,
|
||||
torrentUrl: torrent_url,
|
||||
progress: 0,
|
||||
totalSize: fileSize,
|
||||
downloadedSize: 0,
|
||||
downloadSpeed: 1024 * 1024 * 2, // 2MB/s
|
||||
state: 'downloading',
|
||||
startTime: Date.now(),
|
||||
};
|
||||
|
||||
this.activeDownloads.set(downloadId, task);
|
||||
console.log(`已创建下载任务: ${downloadId}, 大小: ${fileSize} bytes`);
|
||||
|
||||
// 启动进度模拟
|
||||
this.startProgressSimulation(downloadId);
|
||||
|
||||
callback(null, {
|
||||
success: true,
|
||||
message: '下载已开始',
|
||||
download_id: downloadId,
|
||||
});
|
||||
}
|
||||
|
||||
// 停止下载
|
||||
private stopDownload(call: grpc.ServerUnaryCall<any, any>, callback: grpc.sendUnaryData<any>) {
|
||||
const { download_id } = call.request;
|
||||
|
||||
if (this.activeDownloads.has(download_id)) {
|
||||
const task = this.activeDownloads.get(download_id)!;
|
||||
task.state = 'paused';
|
||||
|
||||
// 清除进度定时器
|
||||
if (this.progressIntervals.has(download_id)) {
|
||||
clearInterval(this.progressIntervals.get(download_id));
|
||||
this.progressIntervals.delete(download_id);
|
||||
}
|
||||
|
||||
console.log(`模拟停止下载: ${download_id}`);
|
||||
callback(null, { success: true, message: '下载已停止' });
|
||||
} else {
|
||||
callback({
|
||||
code: grpc.status.NOT_FOUND,
|
||||
message: `下载任务不存在: ${download_id}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 获取下载状态
|
||||
private getDownloadStatus(call: grpc.ServerUnaryCall<any, any>, callback: grpc.sendUnaryData<any>) {
|
||||
const { download_id } = call.request;
|
||||
|
||||
if (this.activeDownloads.has(download_id)) {
|
||||
const task = this.activeDownloads.get(download_id)!;
|
||||
callback(null, {
|
||||
download_id: task.id,
|
||||
progress: task.progress,
|
||||
download_speed: task.downloadSpeed,
|
||||
state: task.state,
|
||||
total_size: task.totalSize,
|
||||
downloaded_size: task.downloadedSize,
|
||||
});
|
||||
} else {
|
||||
callback({
|
||||
code: grpc.status.NOT_FOUND,
|
||||
message: `下载任务不存在: ${download_id}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 列出所有下载
|
||||
private listDownloads(call: grpc.ServerUnaryCall<any, any>, callback: grpc.sendUnaryData<any>) {
|
||||
const downloads = Array.from(this.activeDownloads.values()).map(task => ({
|
||||
download_id: task.id,
|
||||
item_name: task.itemName,
|
||||
progress: task.progress,
|
||||
download_speed: task.downloadSpeed,
|
||||
state: task.state,
|
||||
total_size: task.totalSize,
|
||||
downloaded_size: task.downloadedSize,
|
||||
}));
|
||||
|
||||
callback(null, { downloads });
|
||||
}
|
||||
|
||||
// 订阅进度更新(流式响应)
|
||||
private subscribeProgress(call: grpc.ServerWritableStream<any, any>) {
|
||||
console.log('客户端订阅了进度更新');
|
||||
|
||||
// 存储回调以便发送进度更新
|
||||
const callback = (progress: ProgressUpdate) => {
|
||||
try {
|
||||
call.write(progress);
|
||||
} catch (error) {
|
||||
console.error('发送进度更新失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
this.progressCallbacks.push(callback);
|
||||
|
||||
// 当客户端断开连接时清理
|
||||
call.on('cancelled', () => {
|
||||
console.log('客户端取消了进度订阅');
|
||||
const index = this.progressCallbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
this.progressCallbacks.splice(index, 1);
|
||||
}
|
||||
});
|
||||
|
||||
call.on('error', (error) => {
|
||||
console.error('进度流错误:', error);
|
||||
const index = this.progressCallbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
this.progressCallbacks.splice(index, 1);
|
||||
}
|
||||
});
|
||||
|
||||
// 处理客户端断开连接
|
||||
call.on('end', () => {
|
||||
console.log('客户端断开连接');
|
||||
const index = this.progressCallbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
this.progressCallbacks.splice(index, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 启动进度模拟
|
||||
// 在 MockBTService.ts 中修改 startProgressSimulation 方法
|
||||
private startProgressSimulation(downloadId: string) {
|
||||
const interval = setInterval(() => {
|
||||
if (this.activeDownloads.has(downloadId)) {
|
||||
const task = this.activeDownloads.get(downloadId)!;
|
||||
|
||||
if (task.state === 'downloading' && task.progress < 100) {
|
||||
// // 更新进度,但确保不超过100%
|
||||
// task.progress += Math.random() * 2;
|
||||
// 使用固定的进度增加而不是随机值,使下载更加稳定
|
||||
task.progress += 1.5; // 每秒增加1.5%的进度
|
||||
|
||||
// 确保进度不超过100%
|
||||
if (task.progress >= 100) {
|
||||
task.progress = 100;
|
||||
task.state = 'completed';
|
||||
task.downloadedSize = task.totalSize; // 确保已完成时已下载大小等于总大小
|
||||
console.log(`下载完成: ${downloadId}`);
|
||||
|
||||
// 清除定时器
|
||||
clearInterval(interval);
|
||||
this.progressIntervals.delete(downloadId);
|
||||
} else {
|
||||
// 根据进度计算已下载大小
|
||||
task.downloadedSize = (task.totalSize * task.progress) / 100;
|
||||
}
|
||||
|
||||
// 计算剩余时间(秒)
|
||||
let eta = 0;
|
||||
if (task.progress < 100) {
|
||||
// 基于当前速度计算剩余时间
|
||||
const progressPerSecond = 1.5; // 每秒进度百分比
|
||||
eta = Math.round(((100 - task.progress) / progressPerSecond));
|
||||
}
|
||||
|
||||
// 发送进度更新
|
||||
this.sendProgressUpdate({
|
||||
download_id: task.id,
|
||||
item_name: task.itemName,
|
||||
progress: task.progress,
|
||||
download_speed: task.downloadSpeed,
|
||||
upload_speed: 1024 * 512, // 512KB/s 上传速度
|
||||
eta: task.progress >= 100 ? 0 : Math.round(((100 - task.progress) / 2) * 1000),
|
||||
total_size: task.totalSize,
|
||||
downloaded_size: task.downloadedSize,
|
||||
state: task.state,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
clearInterval(interval);
|
||||
this.progressIntervals.delete(downloadId);
|
||||
}
|
||||
}, 1000); // 每秒更新一次进度
|
||||
|
||||
this.progressIntervals.set(downloadId, interval);
|
||||
}
|
||||
|
||||
// 发送进度更新给所有订阅者
|
||||
private sendProgressUpdate(progress: ProgressUpdate) {
|
||||
console.log('发送进度更新:', progress);
|
||||
// 通过已注册的回调函数传递给 BTGrpcClient
|
||||
this.progressCallbacks.forEach((callback, index) => {
|
||||
try {
|
||||
callback(progress);
|
||||
} catch (error) {
|
||||
console.error(`发送进度更新给回调 ${index} 失败:`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 绑定端口并启动服务器
|
||||
start(port: number = 50051): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.server.bindAsync(
|
||||
`0.0.0.0:${port}`,
|
||||
grpc.ServerCredentials.createInsecure(),
|
||||
(error, port) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
this.server.start();
|
||||
console.log(`Mock gRPC 服务器运行在端口 ${port}`);
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// 停止服务器
|
||||
stop(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
// 清理所有定时器
|
||||
this.progressIntervals.forEach(interval => clearInterval(interval));
|
||||
this.progressIntervals.clear();
|
||||
this.activeDownloads.clear();
|
||||
this.progressCallbacks = [];
|
||||
|
||||
this.server.tryShutdown(() => {
|
||||
console.log('Mock gRPC 服务器已停止');
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,219 @@
|
|||
梳理整个gRPC下载逻辑和模拟下载的执行流程。
|
||||
|
||||
## 1. 整体架构概览
|
||||
|
||||
整个gRPC下载系统分为以下几个主要部分:
|
||||
|
||||
1. **前端页面** (`grpc.tsx`) - 用户交互界面
|
||||
2. **预加载脚本** (`preload.ts`) - 桥接前端和主进程
|
||||
3. **主进程** (`platform.ts`) - 管理gRPC客户端和服务端
|
||||
4. **gRPC客户端** (`BTGrpcClient.ts`) - 调用后端服务
|
||||
5. **gRPC服务端** (`MockBTService.ts`) - 模拟下载服务
|
||||
6. **协议定义** (`bittorrent.proto`) - gRPC接口定义
|
||||
|
||||
## 2. 启动流程
|
||||
|
||||
### 2.1 应用启动时初始化gRPC
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[应用启动] --> B[app.whenReady()]
|
||||
B --> C[initializeGrpc()]
|
||||
C --> D{IS_TEST_MODE}
|
||||
D -->|true| E[创建MockBTService实例]
|
||||
D -->|false| F[连接真实后端]
|
||||
E --> G[启动Mock gRPC服务器]
|
||||
G --> H[创建BTGrpcClient实例]
|
||||
H --> I[注册进度回调]
|
||||
I --> J[启动健康检查]
|
||||
```
|
||||
|
||||
在 `platform.ts` 中:
|
||||
- 应用启动时调用 `initializeGrpc()` 函数
|
||||
- 根据 `IS_TEST_MODE` 决定是启动模拟服务还是连接真实后端
|
||||
- 创建 `MockBTService` 实例并启动服务器
|
||||
- 创建 `BTGrpcClient` 实例连接到gRPC服务
|
||||
- 注册进度回调函数,用于接收下载进度更新
|
||||
|
||||
### 2.2 gRPC服务端启动 (MockBTService)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant M as MockBTService
|
||||
participant S as gRPC Server
|
||||
participant C as BTGrpcClient
|
||||
|
||||
M->>S: 创建gRPC服务器实例
|
||||
M->>S: 加载proto定义
|
||||
M->>S: 实现服务方法
|
||||
M->>S: 绑定端口并启动
|
||||
S-->>M: 服务器启动成功
|
||||
C->>S: 连接服务器
|
||||
S-->>C: 连接建立
|
||||
```
|
||||
|
||||
在 `MockBTService.ts` 中:
|
||||
1. 构造函数调用 [setupService()](file://d:\project\vdi\pc-fe\src\main\grpc\MockBTService.ts#L43-L65) 设置gRPC服务
|
||||
2. 加载 `bittorrent.proto` 定义
|
||||
3. 实现所有gRPC服务方法:
|
||||
- `StartDownload` - 开始下载
|
||||
- `StopDownload` - 停止下载
|
||||
- `GetDownloadStatus` - 获取下载状态
|
||||
- `ListDownloads` - 列出所有下载
|
||||
- `SubscribeProgress` - 订阅进度更新
|
||||
4. 通过 `server.bindAsync()` 绑定端口并启动服务
|
||||
|
||||
## 3. 下载流程详解
|
||||
|
||||
### 3.1 用户触发下载
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[用户点击下载按钮] --> B[handleStartDownload()]
|
||||
B --> C[调用window.electronAPI.grpcStartDownload()]
|
||||
C --> D[IPC调用grpc-start-download]
|
||||
D --> E[主进程处理]
|
||||
E --> F[调用btGrpcClient.startDownload()]
|
||||
F --> G[gRPC调用StartDownload()]
|
||||
G --> H[MockBTService处理]
|
||||
H --> I[创建下载任务]
|
||||
I --> J[启动进度模拟]
|
||||
J --> K[返回下载ID]
|
||||
K --> L[前端接收响应]
|
||||
```
|
||||
|
||||
详细步骤:
|
||||
1. 用户点击"开始gRPC下载测试"按钮
|
||||
2. 调用 `handleStartDownload()` 函数
|
||||
3. 通过 `window.electronAPI.grpcStartDownload()` 发起IPC调用
|
||||
4. 主进程 `platform.ts` 接收 `grpc-start-download` 请求
|
||||
5. 调用 `btGrpcClient.startDownload()` 方法
|
||||
6. gRPC客户端向服务端发起 `StartDownload` 调用
|
||||
7. [MockBTService](file://d:\project\vdi\pc-fe\src\main\grpc\MockBTService.ts#L32-L305) 接收请求,创建下载任务
|
||||
8. 启动进度模拟定时器
|
||||
9. 返回下载ID给前端
|
||||
|
||||
### 3.2 进度更新机制
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant M as MockBTService
|
||||
participant I as Interval
|
||||
participant B as BTGrpcClient
|
||||
participant P as Platform主进程
|
||||
participant F as 前端
|
||||
|
||||
I->>M: 定时触发
|
||||
M->>M: 更新任务进度
|
||||
M->>M: 构造进度数据
|
||||
M->>B: 发送进度更新
|
||||
B->>P: 调用回调函数
|
||||
P->>F: IPC发送grpc-progress-update
|
||||
F->>F: 更新UI显示
|
||||
```
|
||||
|
||||
详细步骤:
|
||||
1. `startProgressSimulation()` 创建定时器
|
||||
2. 定时器每秒触发一次,更新下载进度
|
||||
3. 构造 `ProgressUpdate` 对象
|
||||
4. 调用 `sendProgressUpdate()` 发送进度更新
|
||||
5. 通过已注册的回调函数传递给 `BTGrpcClient`
|
||||
6. `BTGrpcClient` 通过IPC将进度发送给主进程
|
||||
7. 主进程通过 `webContents.send()` 将进度发送给前端
|
||||
8. 前端接收并更新UI
|
||||
|
||||
## 4. 关键代码解析
|
||||
|
||||
### 4.1 前端监听进度更新
|
||||
|
||||
```typescript
|
||||
// grpc.tsx
|
||||
useEffect(() => {
|
||||
// 设置进度监听
|
||||
const handleProgress = (progress: DownloadProgress) => {
|
||||
console.log('收到进度更新:', progress);
|
||||
setDownloads(prev => {
|
||||
const newMap = new Map(prev);
|
||||
newMap.set(progress.download_id, progress);
|
||||
return newMap;
|
||||
});
|
||||
};
|
||||
|
||||
window.electronAPI.onGrpcProgress(handleProgress);
|
||||
|
||||
return () => {
|
||||
window.electronAPI.removeAllGrpcProgressListeners();
|
||||
};
|
||||
}, []);
|
||||
```
|
||||
|
||||
### 4.2 主进程转发进度更新
|
||||
|
||||
```typescript
|
||||
// platform.ts
|
||||
btGrpcClient.registerProgressCallback('all', (progress) => {
|
||||
const mainWindow = BrowserWindow.getFocusedWindow();
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('grpc-progress-update', progress);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 4.3 Mock服务模拟进度
|
||||
|
||||
```typescript
|
||||
// MockBTService.ts
|
||||
private startProgressSimulation(downloadId: string) {
|
||||
const interval = setInterval(() => {
|
||||
if (this.activeDownloads.has(downloadId)) {
|
||||
const task = this.activeDownloads.get(downloadId)!;
|
||||
|
||||
if (task.state === 'downloading' && task.progress < 100) {
|
||||
// 更新进度
|
||||
task.progress += Math.random() * 2;
|
||||
task.downloadedSize = (task.totalSize * task.progress) / 100;
|
||||
|
||||
// 发送进度更新
|
||||
this.sendProgressUpdate({
|
||||
download_id: task.id,
|
||||
item_name: task.itemName,
|
||||
progress: task.progress,
|
||||
download_speed: task.downloadSpeed,
|
||||
upload_speed: 1024 * 512,
|
||||
eta: Math.round(((100 - task.progress) / 2) * 1000),
|
||||
total_size: task.totalSize,
|
||||
downloaded_size: task.downloadedSize,
|
||||
state: task.state,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
this.progressIntervals.set(downloadId, interval);
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 数据流总结
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[前端UI] --> B[preload.ts]
|
||||
B --> C[主进程platform.ts]
|
||||
C --> D[BTGrpcClient.ts]
|
||||
D --> E[MockBTService.ts]
|
||||
E --> F[定时模拟进度]
|
||||
F --> G[发送进度更新]
|
||||
G --> H[主进程转发]
|
||||
H --> I[前端接收更新]
|
||||
I --> A
|
||||
```
|
||||
|
||||
整个流程形成了一个闭环:
|
||||
1. 用户操作触发前端请求
|
||||
2. 请求通过IPC传递到主进程
|
||||
3. 主进程通过gRPC调用后端服务
|
||||
4. Mock服务模拟下载过程
|
||||
5. 进度更新通过相同路径返回前端
|
||||
6. 前端更新UI显示最新进度
|
||||
|
||||
这就是整个gRPC下载逻辑的完整执行流程。
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
// bittorrent.proto gRPC接口定义
|
||||
syntax = "proto3";
|
||||
|
||||
package bittorrent;
|
||||
|
||||
service BTDownloadService {
|
||||
rpc StartDownload (DownloadRequest) returns (DownloadResponse) {}
|
||||
rpc StopDownload (StopRequest) returns (StopResponse) {}
|
||||
rpc GetDownloadStatus (StatusRequest) returns (StatusResponse) {}
|
||||
rpc ListDownloads (ListRequest) returns (ListResponse) {}
|
||||
rpc SubscribeProgress (SubscribeRequest) returns (stream ProgressUpdate) {}
|
||||
}
|
||||
|
||||
message DownloadRequest {
|
||||
string torrent_url = 1;
|
||||
string item_name = 2;
|
||||
string item_id = 3;
|
||||
}
|
||||
|
||||
message DownloadResponse {
|
||||
bool success = 1;
|
||||
string message = 2;
|
||||
string download_id = 3;
|
||||
}
|
||||
|
||||
message StopRequest {
|
||||
string download_id = 1;
|
||||
}
|
||||
|
||||
message StopResponse {
|
||||
bool success = 1;
|
||||
string message = 2;
|
||||
}
|
||||
|
||||
message ProgressUpdate {
|
||||
string download_id = 1;
|
||||
double progress = 2;
|
||||
double download_speed = 3;
|
||||
double upload_speed = 4;
|
||||
int64 eta = 5;
|
||||
int64 total_size = 6;
|
||||
int64 downloaded_size = 7;
|
||||
string state = 8;
|
||||
}
|
||||
|
||||
message StatusRequest {
|
||||
string download_id = 1;
|
||||
}
|
||||
|
||||
message StatusResponse {
|
||||
string download_id = 1;
|
||||
double progress = 2;
|
||||
double download_speed = 3;
|
||||
string state = 4;
|
||||
int64 total_size = 5;
|
||||
int64 downloaded_size = 6;
|
||||
}
|
||||
|
||||
message ListRequest {
|
||||
}
|
||||
|
||||
message ListResponse {
|
||||
repeated DownloadInfo downloads = 1;
|
||||
}
|
||||
|
||||
message DownloadInfo {
|
||||
string download_id = 1;
|
||||
string item_name = 2;
|
||||
double progress = 3;
|
||||
double download_speed = 4;
|
||||
string state = 5;
|
||||
int64 total_size = 6;
|
||||
int64 downloaded_size = 7;
|
||||
}
|
||||
|
||||
message SubscribeRequest {
|
||||
}
|
||||
|
|
@ -1,11 +1,191 @@
|
|||
import { ipcMain,app } from 'electron';
|
||||
import { ipcMain,app,BrowserWindow } from 'electron';
|
||||
import { getDeviceId, getWiredConnectionName,netmaskToCidr } from '../utils/utils';
|
||||
const { exec } = require('child_process');
|
||||
const { promisify } = require('util');
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
// 模拟grpc服务端
|
||||
import { BTGrpcClient } from '../grpc/BTGrpcClient';
|
||||
import { MockBTService } from '../grpc/MockBTService';
|
||||
|
||||
// 客户端和服务端 通信
|
||||
const IS_TEST_MODE = true; // 设置为 false 时连接真实后端
|
||||
const GRPC_SERVER_PORT = 50051;
|
||||
// 先声明变量,但不立即初始化
|
||||
let btGrpcClient: BTGrpcClient | null = null;
|
||||
let mockServer: MockBTService | null = null;
|
||||
let healthCheckInterval: NodeJS.Timeout | null = null; // 声明一个变量来存储定时器
|
||||
|
||||
const window = getBrowserWindowRuntime();
|
||||
|
||||
// 初始化 gRPC 客户端
|
||||
async function initializeGrpc() {
|
||||
try {
|
||||
if (IS_TEST_MODE) {
|
||||
console.log('启动模拟 gRPC 服务器...');
|
||||
mockServer = new MockBTService();
|
||||
await mockServer.start(GRPC_SERVER_PORT);
|
||||
|
||||
// 给一点时间让服务器启动
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
// 创建 BTGrpcClient 实例连接到gRPC服务
|
||||
btGrpcClient = new BTGrpcClient();
|
||||
|
||||
// 注册进度回调函数,用于接收下载进度更新
|
||||
btGrpcClient.registerProgressCallback('all', (progress) => {
|
||||
const mainWindow = BrowserWindow.getFocusedWindow();
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('grpc-progress-update', progress);
|
||||
}
|
||||
});
|
||||
|
||||
// 测试连接
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const connected = await btGrpcClient!.testConnection();
|
||||
console.log('gRPC 连接状态:', connected ? '已连接' : '未连接');
|
||||
|
||||
const mainWindow = BrowserWindow.getFocusedWindow();
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('grpc-connection-status', {
|
||||
connected,
|
||||
isMock: IS_TEST_MODE
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('连接测试失败:', error);
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
// 启动健康检查
|
||||
startHealthCheck();
|
||||
|
||||
} catch (error) {
|
||||
console.error('gRPC 初始化失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 启动健康检查
|
||||
function startHealthCheck() {
|
||||
if (healthCheckInterval) {
|
||||
clearInterval(healthCheckInterval);
|
||||
}
|
||||
|
||||
healthCheckInterval = setInterval(async () => {
|
||||
try {
|
||||
if (!btGrpcClient) return;
|
||||
|
||||
const isConnected = await btGrpcClient.testConnection();
|
||||
const mainWindow = BrowserWindow.getFocusedWindow();
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('grpc-connection-status', {
|
||||
connected: isConnected,
|
||||
state: btGrpcClient.getConnectionState(),
|
||||
isMock: IS_TEST_MODE
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('健康检查失败:', error);
|
||||
}
|
||||
}, 5000); // 每5秒检查一次
|
||||
}
|
||||
|
||||
|
||||
// 应用启动时初始化
|
||||
app.whenReady().then(() => {
|
||||
initializeGrpc();
|
||||
});
|
||||
|
||||
// 应用退出时清理
|
||||
app.on('before-quit', async () => {
|
||||
if (healthCheckInterval) {
|
||||
clearInterval(healthCheckInterval);
|
||||
}
|
||||
|
||||
if (mockServer) {
|
||||
await mockServer.stop();
|
||||
}
|
||||
});
|
||||
|
||||
// 添加 gRPC 相关的 IPC 处理程序
|
||||
ipcMain.handle('grpc-start-download', async (event, config) => {
|
||||
try {
|
||||
if (!btGrpcClient) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'gRPC客户端未初始化'
|
||||
};
|
||||
}
|
||||
|
||||
console.log('开始gRPC下载:', config);
|
||||
// gRPC客户端向服务端发起 StartDownload 调用
|
||||
const result = await btGrpcClient.startDownload({
|
||||
torrent_url: config.torrentUrl,
|
||||
item_name: config.itemName,
|
||||
item_id: config.itemId
|
||||
});
|
||||
return { success: true, data: result };
|
||||
} catch (error: any) {
|
||||
console.error('gRPC下载失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message || '未知错误',
|
||||
details: error.details
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('grpc-stop-download', async (event, downloadId) => {
|
||||
try {
|
||||
if (!btGrpcClient) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'gRPC客户端未初始化'
|
||||
};
|
||||
}
|
||||
|
||||
const result = await btGrpcClient.stopDownload(downloadId);
|
||||
return { success: true, data: result };
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message || '未知错误'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('grpc-check-connection', async () => {
|
||||
if (!btGrpcClient) {
|
||||
return { connected: false, error: 'gRPC客户端未初始化' };
|
||||
}
|
||||
|
||||
return {
|
||||
connected: btGrpcClient.isConnected(),
|
||||
state: btGrpcClient.getConnectionState()
|
||||
};
|
||||
});
|
||||
|
||||
// 重新连接 gRPC
|
||||
ipcMain.handle('grpc-reconnect', async () => {
|
||||
try {
|
||||
if (btGrpcClient) {
|
||||
btGrpcClient.reconnect();
|
||||
return { success: true, message: '正在重新连接' };
|
||||
} else {
|
||||
await initializeGrpc();
|
||||
return { success: true, message: '正在初始化连接' };
|
||||
}
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message || '重新连接失败'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 监听渲染进程发送的消息
|
||||
ipcMain.handle('getPlatform', () => {
|
||||
return `hi, i'm from ${process.platform}`;
|
||||
|
|
@ -27,7 +207,6 @@ ipcMain.on('exit-kiosk', () => {
|
|||
});
|
||||
|
||||
|
||||
|
||||
ipcMain.handle('get-deviceid',async()=>{
|
||||
const deviceId = await getDeviceId();
|
||||
console.log(`Using device ID: ${deviceId}`);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { contextBridge, ipcRenderer } from 'electron';
|
||||
|
||||
// 页面调用的方法
|
||||
// 页面调用的方法:桥接前端和主进程
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
getPlatform: async () => {
|
||||
return await ipcRenderer.invoke('getPlatform');
|
||||
|
|
@ -8,10 +8,25 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||
closeApp: () => ipcRenderer.send('close-app'),
|
||||
minimizeApp: () => ipcRenderer.send('minimize-app'),
|
||||
exitKiosk: () => ipcRenderer.send('exit-kiosk'),
|
||||
|
||||
// 新增的 gRPC API
|
||||
grpcStartDownload: (config: any) => ipcRenderer.invoke('grpc-start-download', config),
|
||||
grpcStopDownload: (downloadId: string) => ipcRenderer.invoke('grpc-stop-download', downloadId),
|
||||
grpcCheckConnection: () => ipcRenderer.invoke('grpc-check-connection'),
|
||||
// gRPC 进度监听
|
||||
onGrpcProgress: (callback: (progress: any) => void) => {
|
||||
ipcRenderer.on('grpc-progress-update', (_, progress) => callback(progress));
|
||||
},
|
||||
// 移除监听器
|
||||
removeAllGrpcProgressListeners: () => {
|
||||
ipcRenderer.removeAllListeners('grpc-progress-update');
|
||||
},
|
||||
|
||||
// 事件监听
|
||||
onMainProcessMessage: (callback: (data: string) => void) => {
|
||||
ipcRenderer.on('main-process-message', (_, data) => callback(data));
|
||||
},
|
||||
|
||||
on(...args: Parameters<typeof ipcRenderer.on>) {
|
||||
const [channel, listener] = args
|
||||
return ipcRenderer.on(channel, (event, ...args) => listener(event, ...args))
|
||||
|
|
|
|||
|
|
@ -25,9 +25,9 @@ const MainLayout: React.FC = () => {
|
|||
|
||||
useEffect(() => {
|
||||
// TODO: 第一次来:判断是否配置ip/DHCP、服务ip绑定终端 绑定:直接到版本更新页面 未绑定:到配置ip/DHCP页面
|
||||
// setTimeout(() => {
|
||||
// history.push('/configSteps');
|
||||
// },1000)
|
||||
setTimeout(() => {
|
||||
history.push('/grpc');
|
||||
},1000)
|
||||
// const fetchDeviceId = async () => {
|
||||
// try {
|
||||
// const res = await window.electronAPI.invoke('get-deviceid');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
.welcomeCon{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color:#fff;
|
||||
.showTextCon{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-self: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,254 @@
|
|||
// index.tsx 修改
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import styles from './grpc.less';
|
||||
// import WelcomeIcon from '../../assets/welcome-icon.jpg';
|
||||
|
||||
// 类型定义
|
||||
interface DownloadProgress {
|
||||
download_id: string;
|
||||
item_name?: string;
|
||||
progress: number;
|
||||
download_speed: number;
|
||||
upload_speed: number;
|
||||
eta: number;
|
||||
total_size: number;
|
||||
downloaded_size: number;
|
||||
state: string;
|
||||
}
|
||||
|
||||
// declare global {
|
||||
// interface Window {
|
||||
// electronAPI: {
|
||||
// grpcStartDownload: (config: any) => Promise<any>;
|
||||
// grpcStopDownload: (downloadId: string) => Promise<any>;
|
||||
// grpcCheckConnection: () => Promise<{ connected: boolean }>;
|
||||
// onGrpcProgress: (callback: (progress: DownloadProgress) => void) => void;
|
||||
// removeAllGrpcProgressListeners: () => void;
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
|
||||
const Welcome: React.FC = () => {
|
||||
const [downloads, setDownloads] = useState<Map<string, DownloadProgress>>(new Map());
|
||||
const [connectionStatus, setConnectionStatus] = useState<'connecting' | 'connected' | 'disconnected'>('connecting');
|
||||
|
||||
useEffect(() => {
|
||||
// 检查 gRPC 连接状态
|
||||
const checkConnection = async () => {
|
||||
try {
|
||||
const result = await window.electronAPI.grpcCheckConnection();
|
||||
setConnectionStatus(result.connected ? 'connected' : 'disconnected');
|
||||
} catch (error) {
|
||||
setConnectionStatus('disconnected');
|
||||
}
|
||||
};
|
||||
|
||||
checkConnection();
|
||||
|
||||
// 设置进度监听
|
||||
const handleProgress = (progress: DownloadProgress) => {
|
||||
console.log('收到进度更新:', progress); // 添加调试日志
|
||||
setDownloads(prev => {
|
||||
const newMap = new Map(prev);
|
||||
newMap.set(progress.download_id, progress);
|
||||
return newMap;
|
||||
});
|
||||
};
|
||||
|
||||
window.electronAPI.onGrpcProgress(handleProgress);
|
||||
|
||||
return () => {
|
||||
window.electronAPI.removeAllGrpcProgressListeners();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 在 React 组件中添加连接状态监听
|
||||
useEffect(() => {
|
||||
const handleConnectionStatus = (event: any, status: any) => {
|
||||
console.log('收到gRPC连接状态更新:', status);
|
||||
setConnectionStatus(status.connected ? 'connected' : 'disconnected');
|
||||
console.log('gRPC连接状态:', status);
|
||||
};
|
||||
|
||||
window.electronAPI.on('grpc-connection-status', handleConnectionStatus);
|
||||
|
||||
return () => {
|
||||
window.electronAPI.off('grpc-connection-status', handleConnectionStatus);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleStartDownload = useCallback(async () => {
|
||||
try {
|
||||
// 根据时间生成不同的文件名,便于测试不同大小的文件
|
||||
const fileName = `测试文件-${Date.now()}`;
|
||||
const result = await window.electronAPI.grpcStartDownload({
|
||||
torrentUrl: 'magnet:?xt=urn:btih:EXAMPLEHASH123456789',
|
||||
itemName: fileName,
|
||||
itemId: `test-${Date.now()}`
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
console.log('下载开始成功:', result.data);
|
||||
} else {
|
||||
console.error('下载开始失败:', result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('调用下载失败:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleRefreshDownloads = useCallback(async () => {
|
||||
try {
|
||||
// 这里可以调用一个获取当前下载列表的API(如果有的话)
|
||||
console.log('当前下载任务:', downloads);
|
||||
} catch (error) {
|
||||
console.error('刷新下载列表失败:', error);
|
||||
}
|
||||
}, [downloads]);
|
||||
|
||||
// 添加一个测试大文件下载的函数
|
||||
const handleStartLargeDownload = useCallback(async () => {
|
||||
try {
|
||||
// 发起IPC调用
|
||||
const result = await window.electronAPI.grpcStartDownload({
|
||||
torrentUrl: 'magnet:?xt=urn:btih:EXAMPLEHASH123456789',
|
||||
itemName: 'large-大文件测试',
|
||||
itemId: `large-test-${Date.now()}`
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
console.log('大文件下载开始成功:', result.data);
|
||||
} else {
|
||||
console.error('大文件下载开始失败:', result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('调用大文件下载失败:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleReconnect = useCallback(async () => {
|
||||
try {
|
||||
console.log('尝试重新连接 gRPC...');
|
||||
const result = await window.electronAPI.invoke('grpc-reconnect');
|
||||
|
||||
if (result.success) {
|
||||
console.log('重新连接成功:', result.message);
|
||||
} else {
|
||||
console.error('重新连接失败:', result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('重新连接异常:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const renderConnectionStatus = () => {
|
||||
switch (connectionStatus) {
|
||||
case 'connecting':
|
||||
return <span style={{ color: 'orange' }}>● 连接中...</span>;
|
||||
case 'connected':
|
||||
return <span style={{ color: 'green' }}>● 已连接</span>;
|
||||
case 'disconnected':
|
||||
return <span style={{ color: 'red' }}>● 未连接</span>;
|
||||
default:
|
||||
return <span style={{ color: 'gray' }}>● 未知状态</span>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.welcomeCon}>
|
||||
<div className={styles.showTextCon}>
|
||||
{/* 连接状态显示 */}
|
||||
<div style={{ marginTop: '20px', fontSize: '14px' }}>
|
||||
后端连接状态: {renderConnectionStatus()}
|
||||
</div>
|
||||
|
||||
{/* 下载测试按钮 */}
|
||||
<div style={{ marginTop: '20px' }}>
|
||||
<button
|
||||
onClick={handleStartDownload}
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
backgroundColor: connectionStatus === 'connected' ? '#1890ff' : '#ccc',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: connectionStatus === 'connected' ? 'pointer' : 'not-allowed',
|
||||
marginRight: '10px'
|
||||
}}
|
||||
disabled={connectionStatus !== 'connected'}
|
||||
>
|
||||
开始gRPC下载测试
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleStartLargeDownload}
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
backgroundColor: connectionStatus === 'connected' ? '#722ed1' : '#ccc',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: connectionStatus === 'connected' ? 'pointer' : 'not-allowed'
|
||||
}}
|
||||
disabled={connectionStatus !== 'connected'}
|
||||
>
|
||||
开始大文件下载测试
|
||||
</button>
|
||||
<button
|
||||
onClick={handleRefreshDownloads}
|
||||
style={{
|
||||
padding: '5px 10px',
|
||||
backgroundColor: '#faad14',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
刷新下载列表
|
||||
</button>
|
||||
<button
|
||||
onClick={handleReconnect}
|
||||
style={{
|
||||
padding: '5px 10px',
|
||||
backgroundColor: '#52c41a',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
重新连接gRPC
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 显示当前下载数量 */}
|
||||
<div style={{ marginTop: '10px', fontSize: '14px' }}>
|
||||
当前下载任务数: {downloads.size}
|
||||
</div>
|
||||
|
||||
{/* 下载列表显示 */}
|
||||
{Array.from(downloads.entries()).map(([id, progress]) => (
|
||||
<div key={id} style={{
|
||||
marginTop: '15px',
|
||||
padding: '10px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
minWidth: '300px'
|
||||
}}>
|
||||
<div style={{ fontWeight: 'bold' }}>{progress.download_id}</div>
|
||||
<div>文件名: {progress.item_name || '未知'}</div>
|
||||
<div>进度: {progress.progress.toFixed(1)}%</div>
|
||||
<div>状态: {progress.state}</div>
|
||||
<div>速度: {(progress.download_speed / 1024 / 1024).toFixed(2)} MB/s</div>
|
||||
<div>已下载: {(progress.downloaded_size / 1024 / 1024).toFixed(2)} MB</div>
|
||||
<div>总大小: {(progress.total_size / 1024 / 1024).toFixed(2)} MB</div>
|
||||
<div>剩余时间: {Math.ceil(progress.eta / 60)} 分钟</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Welcome;
|
||||
Loading…
Reference in New Issue