Commit da3a1104 authored by wudizhanche1000's avatar wudizhanche1000

合并InstallService和AppService

parent 0759508b
...@@ -7,7 +7,6 @@ import {DownloadService} from "./download.service"; ...@@ -7,7 +7,6 @@ import {DownloadService} from "./download.service";
import {clipboard, remote} from "electron"; import {clipboard, remote} from "electron";
import * as path from "path"; import * as path from "path";
import * as fs from 'fs'; import * as fs from 'fs';
import {InstallService} from "./install.service";
import mkdirp = require("mkdirp"); import mkdirp = require("mkdirp");
declare const Notification: any; declare const Notification: any;
...@@ -30,8 +29,7 @@ export class AppDetailComponent implements OnInit { ...@@ -30,8 +29,7 @@ export class AppDetailComponent implements OnInit {
referencesInstall: {[id: string]: boolean}; referencesInstall: {[id: string]: boolean};
constructor(private appsService: AppsService, private settingsService: SettingsService, constructor(private appsService: AppsService, private settingsService: SettingsService,
private downloadService: DownloadService, private installService: InstallService, private downloadService: DownloadService, private ref: ChangeDetectorRef) {
private ref: ChangeDetectorRef) {
} }
// public File[] listRoots() { // public File[] listRoots() {
...@@ -103,8 +101,12 @@ export class AppDetailComponent implements OnInit { ...@@ -103,8 +101,12 @@ export class AppDetailComponent implements OnInit {
async uninstall(app: App) { async uninstall(app: App) {
if (confirm("确认删除?")) { if (confirm("确认删除?")) {
await this.installService.uninstall(app); try {
app.status.status = "init"; await this.appsService.uninstall(app);
app.status.status = "init";
} catch (e) {
alert(e);
}
} }
} }
...@@ -131,7 +133,7 @@ export class AppDetailComponent implements OnInit { ...@@ -131,7 +133,7 @@ export class AppDetailComponent implements OnInit {
let volume = this.installOption.installLibrary.slice(7); let volume = this.installOption.installLibrary.slice(7);
let library = path.join(volume, "MyCardLibrary"); let library = path.join(volume, "MyCardLibrary");
try { try {
await this.installService.createDirectory(library); await this.appsService.createDirectory(library);
this.installOption.installLibrary = library; this.installOption.installLibrary = library;
this.settingsService.addLibrary(library, true); this.settingsService.addLibrary(library, true);
} catch (e) { } catch (e) {
......
import {Injectable, ApplicationRef} from "@angular/core"; import {Injectable, ApplicationRef, EventEmitter} from "@angular/core";
import {Http} from "@angular/http"; import {Http} from "@angular/http";
import {App, AppStatus, Action} from "./app"; import {App, AppStatus, Action} from "./app";
import {SettingsService} from "./settings.sevices"; import {SettingsService} from "./settings.sevices";
...@@ -8,17 +8,30 @@ import * as child_process from "child_process"; ...@@ -8,17 +8,30 @@ import * as child_process from "child_process";
import {ChildProcess} from "child_process"; import {ChildProcess} from "child_process";
import {remote} from "electron"; import {remote} from "electron";
import "rxjs/Rx"; import "rxjs/Rx";
import * as readline from "readline";
import {AppLocal} from "./app-local"; import {AppLocal} from "./app-local";
import * as ini from "ini"; import * as ini from "ini";
import Timer = NodeJS.Timer; import Timer = NodeJS.Timer;
import {DownloadService} from "./download.service"; import {DownloadService} from "./download.service";
import {InstallOption} from "./install-option"; import {InstallOption} from "./install-option";
import {InstallService} from "./install.service";
import {ComparableSet} from "./shared/ComparableSet"; import {ComparableSet} from "./shared/ComparableSet";
import mkdirp = require("mkdirp");
import {Observable, Observer} from "rxjs/Rx";
import ReadableStream = NodeJS.ReadableStream;
const Aria2 = require('aria2'); const Aria2 = require('aria2');
const sudo = require('electron-sudo'); const sudo = require('electron-sudo');
interface InstallTask {
app: App;
option: InstallOption;
}
interface InstallStatus {
status: string;
progress: number;
total: number;
lastItem: string;
}
interface Connection { interface Connection {
connection: WebSocket, address: string | null connection: WebSocket, address: string | null
} }
...@@ -29,7 +42,17 @@ export class AppsService { ...@@ -29,7 +42,17 @@ export class AppsService {
private apps: Map<string,App>; private apps: Map<string,App>;
constructor(private http: Http, private settingsService: SettingsService, private ref: ApplicationRef, constructor(private http: Http, private settingsService: SettingsService, private ref: ApplicationRef,
private downloadService: DownloadService, private installService: InstallService) { private downloadService: DownloadService) {
if (process.platform === "win32") {
if (process.env['NODE_ENV'] == 'production') {
this.tarPath = path.join(process.resourcesPath, 'bin', 'bsdtar.exe');
} else {
this.tarPath = path.join('bin', 'bsdtar.exe');
}
} else {
this.tarPath = "bsdtar"
}
} }
loadApps() { loadApps() {
...@@ -234,7 +257,7 @@ export class AppsService { ...@@ -234,7 +257,7 @@ export class AppsService {
let o = new InstallOption(result.app, option.installLibrary); let o = new InstallOption(result.app, option.installLibrary);
o.downloadFiles = result.files; o.downloadFiles = result.files;
let task = this.installService.push({app: result.app, option: o}); let task = this.push({app: result.app, option: o});
installTasks.push(task); installTasks.push(task);
} }
await Promise.all(installTasks); await Promise.all(installTasks);
...@@ -439,4 +462,283 @@ export class AppsService { ...@@ -439,4 +462,283 @@ export class AppsService {
this.ref.tick(); this.ref.tick();
}; };
} }
tarPath: string;
installingId: string = '';
eventEmitter = new EventEmitter<void>();
readonly checksumURL = "https://thief.mycard.moe/checksums/";
readonly updateServerURL = 'https://thief.mycard.moe/update/metalinks';
installQueue: Map<string,InstallTask> = new Map();
map: Map<string,string> = new Map();
private createId(): string {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
s4() + '-' + s4() + s4() + s4();
}
// installProgress(id: string): Observable<InstallStatus>|undefined {
// let app = this.map.get(id);
// if (app) {
//
// }
// }
async push(task: InstallTask): Promise<void> {
if (!task.app.readyForInstall()) {
await new Promise((resolve, reject) => {
this.eventEmitter.subscribe(() => {
if (task.app.readyForInstall()) {
resolve();
} else if (task.app.findDependencies().find((dependency: App) => !dependency.isInstalled())) {
reject("Dependencies failed");
}
});
});
}
await this.doInstall(task);
}
async doInstall(task: InstallTask) {
try {
let app = task.app;
let dependencies = app.findDependencies();
let readyForInstall = dependencies.every((dependency) => {
return dependency.isReady();
});
if (readyForInstall) {
let option = task.option;
let installDir = option.installDir;
// if (!app.isInstalled()) {
let checksumFile = await this.getChecksumFile(app);
if (app.parent) {
// mod需要安装到parent路径
installDir = app.parent.local!.path;
let parentFiles = new ComparableSet(Array.from(app.parent.local!.files.keys()));
let appFiles = new ComparableSet(Array.from(checksumFile.keys()));
let conflictFiles = appFiles.intersection(parentFiles);
if (conflictFiles.size > 0) {
let backupPath = path.join(option.installLibrary, "backup", app.parent.id);
await this.backupFiles(app.parent.local!.path, backupPath, conflictFiles);
}
}
let allFiles = new Set(checksumFile.keys());
app.status.status = "installing";
app.status.total = allFiles.size;
app.status.progress = 0;
// let timeNow = new Date().getTime();
for (let file of option.downloadFiles) {
await this.createDirectory(installDir);
let interval = setInterval(() => {
}, 500);
await new Promise((resolve, reject) => {
this.extract(file, installDir).subscribe(
(lastItem: string) => {
app.status.progress += 1;
app.status.progressMessage = lastItem;
},
(error) => {
reject(error);
},
() => {
resolve();
});
});
clearInterval(interval);
}
await this.postInstall(app, installDir);
console.log("post install success");
let local = new AppLocal();
local.path = installDir;
local.files = checksumFile;
local.version = app.version;
app.local = local;
this.saveAppLocal(app);
app.status.status = "ready";
}
// }
} catch (e) {
console.log("exception in doInstall", e);
throw e;
}
finally {
this.eventEmitter.emit();
}
}
createDirectory(dir: string) {
return new Promise((resolve, reject) => {
mkdirp(dir, resolve);
})
}
extract(file: string, dir: string): Observable<string> {
return Observable.create((observer: Observer<string>) => {
let tarProcess = child_process.spawn(this.tarPath, ['xvf', file, '-C', dir]);
let rl = readline.createInterface({
input: <ReadableStream>tarProcess.stderr,
});
rl.on('line', (input: string) => {
observer.next(input.split(" ", 2)[1]);
});
tarProcess.on('exit', (code) => {
if (code === 0) {
observer.complete();
} else {
observer.error(code);
}
});
return () => {
}
})
}
async postInstall(app: App, appPath: string) {
let action = app.actions.get('install');
if (action) {
let env = Object.assign({}, action.env);
let command: string[] = [];
command.push(path.join(appPath, action.execute));
command.push(...action.args);
let open = action.open;
if (open) {
let openAction: any = open.actions.get("main");
env = Object.assign(env, openAction.env);
command.unshift(...openAction.args);
command.unshift(path.join((<AppLocal>open.local).path, openAction.execute));
}
return new Promise((resolve, reject) => {
let child = child_process.spawn(<string>command.shift(), command, {
env: env,
stdio: 'inherit',
shell: true,
});
child.on('error', (error) => {
reject(error);
});
child.on('exit', (code) => {
if (code === 0) {
resolve(code);
} else {
reject(code);
}
})
})
}
}
saveAppLocal(app: App) {
if (app.local) {
localStorage.setItem(app.id, JSON.stringify(app.local));
}
}
async backupFiles(dir: string, backupDir: string, files: Iterable<string>) {
for (let file of files) {
await new Promise(async(resolve, reject) => {
let srcPath = path.join(dir, file);
let backupPath = path.join(backupDir, file);
await this.createDirectory(path.dirname(backupPath));
fs.unlink(backupPath, (err) => {
fs.rename(srcPath, backupPath, resolve);
});
});
}
}
async restoreFiles(dir: string, backupDir: string, files: Iterable<string>) {
for (let file of files) {
await new Promise((resolve, reject) => {
let backupPath = path.join(backupDir, file);
let srcPath = path.join(dir, file);
fs.unlink(srcPath, (err) => {
fs.rename(backupPath, srcPath, resolve);
})
})
}
}
async getChecksumFile(app: App): Promise<Map<string,string> > {
let checksumUrl = this.checksumURL + app.id;
if (["ygopro", 'desmume'].includes(app.id)) {
checksumUrl = this.checksumURL + app.id + "-" + process.platform;
}
return this.http.get(checksumUrl)
.map((response) => {
let map = new Map<string,string>();
for (let line of response.text().split('\n')) {
if (line !== "") {
let [checksum,filename]=line.split(' ', 2);
if (filename.endsWith("\\") || filename.endsWith("/")) {
map.set(filename, "");
}
map.set(filename, checksum);
}
}
return map;
}).toPromise();
}
deleteFile(file: string): Promise<string> {
return new Promise((resolve, reject) => {
fs.lstat(file, (err, stats) => {
if (err) return resolve(path);
if (stats.isDirectory()) {
fs.rmdir(file, (err) => {
resolve(file);
});
} else {
fs.unlink(file, (err) => {
resolve(file);
});
}
});
})
}
async uninstall(app: App) {
let children = this.findChildren(app);
let hasInstalledChild = children.find((child) => {
return child.isInstalled();
});
if (hasInstalledChild) {
throw "无法卸载,还有依赖此程序的游戏。"
}
if (app.isReady()) {
app.status.status = "uninstalling";
let appDir = app.local!.path;
let files = Array.from(app.local!.files.keys()).sort().reverse();
app.status.total = files.length;
for (let file of files) {
app.status.progress += 1;
await this.deleteFile(path.join(appDir, file));
}
if (app.parent) {
let backupDir = path.join(path.dirname(appDir), "backup", app.parent.id)
let fileSet = new ComparableSet(files);
let parentSet = new ComparableSet(Array.from(app.parent.local!.files.keys()));
let difference = parentSet.intersection(fileSet);
if (difference) {
this.restoreFiles(appDir, backupDir, Array.from(difference))
}
}
app.local = null;
localStorage.removeItem(app.id);
}
}
} }
\ No newline at end of file
/**
* Created by weijian on 2016/11/2.
*/
import {Injectable, ApplicationRef, EventEmitter} from "@angular/core";
import {App, Category} from "./app";
import {InstallOption} from "./install-option";
import * as path from "path";
import * as child_process from "child_process";
import * as mkdirp from "mkdirp";
import * as readline from "readline";
import * as fs from "fs";
import {AppLocal} from "./app-local";
import {Http} from "@angular/http";
import {ComparableSet} from "./shared/ComparableSet"
import ReadableStream = NodeJS.ReadableStream;
import {Observable, Observer} from "rxjs/Rx";
export interface InstallTask {
app: App;
option: InstallOption;
}
export interface InstallStatus {
status: string;
progress: number;
total: number;
lastItem: string;
}
@Injectable()
export class InstallService {
tarPath: string;
installingId: string = '';
eventEmitter = new EventEmitter<void>();
readonly checksumURL = "https://thief.mycard.moe/checksums/";
readonly updateServerURL = 'https://thief.mycard.moe/update/metalinks';
installQueue: Map<string,InstallTask> = new Map();
map: Map<string,string> = new Map();
constructor(private http: Http, private ref: ApplicationRef) {
if (process.platform === "win32") {
if (process.env['NODE_ENV'] == 'production') {
this.tarPath = path.join(process.resourcesPath, 'bin', 'bsdtar.exe');
} else {
this.tarPath = path.join('bin', 'bsdtar.exe');
}
} else {
this.tarPath = "bsdtar"
}
}
private createId(): string {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
s4() + '-' + s4() + s4() + s4();
}
// installProgress(id: string): Observable<InstallStatus>|undefined {
// let app = this.map.get(id);
// if (app) {
//
// }
// }
async push(task: InstallTask): Promise<void> {
if (!task.app.readyForInstall()) {
await new Promise((resolve, reject) => {
this.eventEmitter.subscribe(() => {
if (task.app.readyForInstall()) {
resolve();
} else if (task.app.findDependencies().find((dependency: App) => !dependency.isInstalled())) {
reject("Dependencies failed");
}
});
});
}
await this.doInstall(task);
}
async doInstall(task: InstallTask) {
try {
let app = task.app;
let dependencies = app.findDependencies();
let readyForInstall = dependencies.every((dependency) => {
return dependency.isReady();
});
if (readyForInstall) {
let option = task.option;
let installDir = option.installDir;
// if (!app.isInstalled()) {
let checksumFile = await this.getChecksumFile(app);
if (app.parent) {
// mod需要安装到parent路径
installDir = app.parent.local!.path;
let parentFiles = new ComparableSet(Array.from(app.parent.local!.files.keys()));
let appFiles = new ComparableSet(Array.from(checksumFile.keys()));
let conflictFiles = appFiles.intersection(parentFiles);
if (conflictFiles.size > 0) {
let backupPath = path.join(option.installLibrary, "backup", app.parent.id);
await this.backupFiles(app.parent.local!.path, backupPath, conflictFiles);
}
}
let allFiles = new Set(checksumFile.keys());
app.status.status = "installing";
app.status.total = allFiles.size;
app.status.progress = 0;
// let timeNow = new Date().getTime();
for (let file of option.downloadFiles) {
await this.createDirectory(installDir);
let interval = setInterval(() => {
}, 500);
await new Promise((resolve, reject) => {
this.extract(file, installDir).subscribe(
(lastItem: string) => {
app.status.progress += 1;
app.status.progressMessage = lastItem;
},
(error) => {
reject(error);
},
() => {
resolve();
});
});
clearInterval(interval);
}
await this.postInstall(app, installDir);
console.log("post install success");
let local = new AppLocal();
local.path = installDir;
local.files = checksumFile;
local.version = app.version;
app.local = local;
this.saveAppLocal(app);
app.status.status = "ready";
}
// }
} catch (e) {
console.log("exception in doInstall", e);
throw e;
}
finally {
this.eventEmitter.emit();
}
}
createDirectory(dir: string) {
return new Promise((resolve, reject) => {
mkdirp(dir, resolve);
})
}
extract(file: string, dir: string): Observable<string> {
return Observable.create((observer: Observer<string>) => {
let tarProcess = child_process.spawn(this.tarPath, ['xvf', file, '-C', dir]);
let rl = readline.createInterface({
input: <ReadableStream>tarProcess.stderr,
});
rl.on('line', (input: string) => {
observer.next(input.split(" ", 2)[1]);
});
tarProcess.on('exit', (code) => {
if (code === 0) {
observer.complete();
} else {
observer.error(code);
}
});
return () => {
}
})
}
async postInstall(app: App, appPath: string) {
let action = app.actions.get('install');
if (action) {
let env = Object.assign({}, action.env);
let command: string[] = [];
command.push(path.join(appPath, action.execute));
command.push(...action.args);
let open = action.open;
if (open) {
let openAction: any = open.actions.get("main");
env = Object.assign(env, openAction.env);
command.unshift(...openAction.args);
command.unshift(path.join((<AppLocal>open.local).path, openAction.execute));
}
return new Promise((resolve, reject) => {
let child = child_process.spawn(<string>command.shift(), command, {
env: env,
stdio: 'inherit',
shell: true,
});
child.on('error', (error) => {
reject(error);
});
child.on('exit', (code) => {
if (code === 0) {
resolve(code);
} else {
reject(code);
}
})
})
}
}
saveAppLocal(app: App) {
if (app.local) {
localStorage.setItem(app.id, JSON.stringify(app.local));
}
}
async backupFiles(dir: string, backupDir: string, files: Iterable<string>) {
for (let file of files) {
await new Promise(async(resolve, reject) => {
let srcPath = path.join(dir, file);
let backupPath = path.join(backupDir, file);
await this.createDirectory(path.dirname(backupPath));
fs.unlink(backupPath, (err) => {
fs.rename(srcPath, backupPath, resolve);
});
});
}
}
async restoreFiles(dir: string, backupDir: string, files: Iterable<string>) {
for (let file of files) {
await new Promise((resolve, reject) => {
let backupPath = path.join(backupDir, file);
let srcPath = path.join(dir, file);
fs.unlink(srcPath, (err) => {
fs.rename(backupPath, srcPath, resolve);
})
})
}
}
async getChecksumFile(app: App): Promise<Map<string,string> > {
let checksumUrl = this.checksumURL + app.id;
if (["ygopro", 'desmume'].includes(app.id)) {
checksumUrl = this.checksumURL + app.id + "-" + process.platform;
}
return this.http.get(checksumUrl)
.map((response) => {
let map = new Map<string,string>();
for (let line of response.text().split('\n')) {
if (line !== "") {
let [checksum,filename]=line.split(' ', 2);
if (filename.endsWith("\\") || filename.endsWith("/")) {
map.set(filename, "");
}
map.set(filename, checksum);
}
}
return map;
}).toPromise();
}
deleteFile(file: string): Promise<string> {
return new Promise((resolve, reject) => {
fs.lstat(file, (err, stats) => {
if (err) return resolve(path);
if (stats.isDirectory()) {
fs.rmdir(file, (err) => {
resolve(file);
});
} else {
fs.unlink(file, (err) => {
resolve(file);
});
}
});
})
}
async uninstall(app: App) {
if (app.isReady()) {
app.status.status = "uninstalling";
let appDir = app.local!.path;
let files = Array.from(app.local!.files.keys()).sort().reverse();
app.status.total = files.length;
for (let file of files) {
app.status.progress += 1;
await this.deleteFile(path.join(appDir, file));
}
if (app.parent) {
let backupDir = path.join(path.dirname(appDir), "backup", app.parent.id)
let fileSet = new ComparableSet(files);
let parentSet = new ComparableSet(Array.from(app.parent.local!.files.keys()));
let difference = parentSet.intersection(fileSet);
if (difference) {
this.restoreFiles(appDir, backupDir, Array.from(difference))
}
}
app.local = null;
localStorage.removeItem(app.id);
}
}
}
\ No newline at end of file
...@@ -6,7 +6,6 @@ import {AppsService} from "./apps.service"; ...@@ -6,7 +6,6 @@ import {AppsService} from "./apps.service";
import {LoginService} from "./login.service"; import {LoginService} from "./login.service";
import {App, Category} from "./app"; import {App, Category} from "./app";
import {DownloadService} from "./download.service"; import {DownloadService} from "./download.service";
import {InstallService} from "./install.service";
import {Http, URLSearchParams} from "@angular/http"; import {Http, URLSearchParams} from "@angular/http";
import {shell} from "electron"; import {shell} from "electron";
import WebViewElement = Electron.WebViewElement; import WebViewElement = Electron.WebViewElement;
...@@ -25,8 +24,7 @@ export class LobbyComponent implements OnInit { ...@@ -25,8 +24,7 @@ export class LobbyComponent implements OnInit {
currentApp: App; currentApp: App;
private apps: Map<string,App>; private apps: Map<string,App>;
constructor(private appsService: AppsService, private loginService: LoginService, private downloadService: DownloadService, constructor(private appsService: AppsService, private loginService: LoginService) {
private installService: InstallService, private http: Http) {
} }
async ngOnInit() { async ngOnInit() {
......
...@@ -14,7 +14,6 @@ import {AppsService} from "./apps.service"; ...@@ -14,7 +14,6 @@ import {AppsService} from "./apps.service";
import {SettingsService} from "./settings.sevices"; import {SettingsService} from "./settings.sevices";
import {LoginService} from "./login.service"; import {LoginService} from "./login.service";
import {DownloadService} from "./download.service"; import {DownloadService} from "./download.service";
import {InstallService} from "./install.service";
@NgModule({ @NgModule({
imports: [BrowserModule, FormsModule, ReactiveFormsModule, HttpModule], imports: [BrowserModule, FormsModule, ReactiveFormsModule, HttpModule],
...@@ -25,7 +24,6 @@ import {InstallService} from "./install.service"; ...@@ -25,7 +24,6 @@ import {InstallService} from "./install.service";
bootstrap: [MyCardComponent], bootstrap: [MyCardComponent],
providers: [ providers: [
AppsService, SettingsService, LoginService, DownloadService, AppsService, SettingsService, LoginService, DownloadService,
InstallService
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}) })
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment