refactore: create feature modules (#387)

* add @angular/cli to the project

* increase nodejs version -> 8.2.1

* add lerna

* merge web and shared module

* move electron module into packages as uhk-agent

Electron agent functionality is not working

* delete symlinker

* convert private properties to public of component if used in html

* revert uhk-message.component

* fix component path

* fix the correct name of the uhk-message.component.scss

* building web and electron module

* delete uhk-renderer package

* handle device connect disconnect state

* add privilege detection

* fix set privilege functionality

* turn back download keymap functionality

* add bootstrap, select2 js and fix null pointer exception

* turn back upload data to keyboard

* fix send keymap

* fix test-serializer

* add missing package.json

* merging

* fix appveyor build

* fix linting

* turn back electron storage service

* commit the missing electron-datastorage-repository

* update node to 8.3.0 in .nvmrc and log node version in appveyor build

* set exact version number in appveyor build

* vertical align privilege and missing device components

* set back node version to 8 in appveyor

* move node-usb dependency from usb dir to root

maybe it is fix the appveyor build

* revert usb to root

* fix electron builder script

* fix electron builder script

* turn off electron devtools

* remove CTRL+U functionality

* fix CTRL+o

* fix lint error

* turnoff store freeze

* start process when got `Error: EPERM: operation not permitted` error

* move files from root usb dir -> packages/usb
This commit is contained in:
Róbert Kiss
2017-08-19 20:02:17 +02:00
committed by László Monda
parent 97770f67c0
commit 0f558e4132
524 changed files with 25606 additions and 5036 deletions

View File

@@ -0,0 +1 @@
declare module 'command-line-args';

View File

@@ -0,0 +1 @@
declare module 'electron-is-dev';

View File

@@ -0,0 +1,22 @@
/// <reference path="./custom_types/electron-is-dev.d.ts"/>
/*
* Install DevTool extensions when Electron is in development mode
*/
import { app } from 'electron';
import * as isDev from 'electron-is-dev';
if (isDev) {
app.once('ready', () => {
const { default: installExtension, REDUX_DEVTOOLS } = require('electron-devtools-installer');
installExtension(REDUX_DEVTOOLS)
.then((name: string) => console.log(`Added Extension: ${name}`))
.catch((err: any) => console.log('An error occurred: ', err));
require('electron-debug')({ showDevTools: true });
});
}

View File

@@ -0,0 +1,117 @@
/// <reference path="./custom_types/electron-is-dev.d.ts"/>
/// <reference path="./custom_types/command-line-args.d.ts"/>
import './polyfills';
import { app, BrowserWindow, ipcMain } from 'electron';
import { autoUpdater } from 'electron-updater';
import * as path from 'path';
import * as url from 'url';
import * as commandLineArgs from 'command-line-args';
// import { ElectronDataStorageRepositoryService } from './services/electron-datastorage-repository.service';
import { CommandLineArgs } from 'uhk-common';
import { DeviceService } from './services/device.service';
import { logger } from './services/logger.service';
import { AppUpdateService } from './services/app-update.service';
import { AppService } from './services/app.service';
import { SudoService } from './services/sudo.service';
const optionDefinitions = [
{ name: 'addons', type: Boolean, defaultOption: false }
];
const options: CommandLineArgs = commandLineArgs(optionDefinitions);
// import './dev-extension';
// require('electron-debug')({ showDevTools: true, enabled: true });
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win: Electron.BrowserWindow;
autoUpdater.logger = logger;
let deviceService: DeviceService;
let appUpdateService: AppUpdateService;
let appService: AppService;
let sudoService: SudoService;
function createWindow() {
// Create the browser window.
win = new BrowserWindow({
title: 'UHK Agent',
width: 1024,
height: 768,
webPreferences: {
nodeIntegration: true
},
icon: 'assets/images/agent-icon.png'
});
win.setMenuBarVisibility(false);
win.maximize();
deviceService = new DeviceService(logger, win);
appUpdateService = new AppUpdateService(logger, win, app);
appService = new AppService(logger, win, deviceService, options);
sudoService = new SudoService(logger);
// and load the index.html of the app.
win.loadURL(url.format({
pathname: path.join(__dirname, 'renderer/index.html'),
protocol: 'file:',
slashes: true
}));
win.on('page-title-updated', (event: any) => {
event.preventDefault();
});
// Emitted when the window is closed.
win.on('closed', () => {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
win = null;
deviceService = null;
appUpdateService = null;
appService = null;
sudoService = null;
});
win.webContents.on('did-finish-load', () => {
});
win.webContents.on('crashed', (event: any) => {
logger.error(event);
});
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow);
// Quit when all windows are closed.
app.on('window-all-closed', () => {
// On macOS it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('will-quit', () => {
if (appUpdateService) {
appUpdateService.saveFirtsRun();
}
});
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (win === null) {
createWindow();
}
});
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here

View File

@@ -0,0 +1,6 @@
{
"name": "UHK-Agent",
"version": "1.0",
"devtools_page": "foo.html",
"default_locale": "en"
}

View File

@@ -0,0 +1,19 @@
{
"name": "uhk-agent",
"main": "electron-main.js",
"version": "0.0.0",
"description": "Agent is the configuration application of the Ultimate Hacking Keyboard.",
"author": "Ultimate Gadget Laboratories",
"repository": {
"type": "git",
"url": "git@github.com:UltimateHackingKeyboard/agent.git"
},
"license": "GPL-3.0",
"engines": {
"node": ">=8.1.0 <9.0.0",
"npm": ">=5.1.0 <6.0.0"
},
"dependencies": {
"node-hid": "0.5.4"
}
}

View File

@@ -0,0 +1,5 @@
/** Evergreen browsers require these. **/
import 'core-js/es6/reflect';
import 'core-js/es7/array';
import 'core-js/es7/object';
import 'core-js/es7/reflect';

View File

@@ -0,0 +1,110 @@
import { ipcMain, BrowserWindow } from 'electron';
import { autoUpdater } from 'electron-updater';
import { ProgressInfo } from 'electron-builder-http/out/ProgressCallbackTransform';
import { VersionInfo } from 'electron-builder-http/out/publishOptions';
import * as settings from 'electron-settings';
import * as isDev from 'electron-is-dev';
import { IpcEvents, LogService } from 'uhk-common';
import { MainServiceBase } from './main-service-base';
export class AppUpdateService extends MainServiceBase {
constructor(protected logService: LogService,
protected win: Electron.BrowserWindow,
private app: Electron.App) {
super(logService, win);
this.initListeners();
logService.info('AppUpdateService init success');
}
saveFirtsRun() {
settings.set('firstRunVersion', this.app.getVersion());
}
private initListeners() {
autoUpdater.on('checking-for-update', () => {
this.sendIpcToWindow(IpcEvents.autoUpdater.checkingForUpdate);
});
autoUpdater.on('update-available', (ev: any, info: VersionInfo) => {
autoUpdater.downloadUpdate();
this.sendIpcToWindow(IpcEvents.autoUpdater.updateAvailable, info);
});
autoUpdater.on('update-not-available', (ev: any, info: VersionInfo) => {
this.sendIpcToWindow(IpcEvents.autoUpdater.updateNotAvailable, info);
});
autoUpdater.on('error', (ev: any, err: string) => {
this.sendIpcToWindow(IpcEvents.autoUpdater.autoUpdateError, err.substr(0, 100));
});
autoUpdater.on('download-progress', (progressObj: ProgressInfo) => {
this.sendIpcToWindow(IpcEvents.autoUpdater.autoUpdateDownloadProgress, progressObj);
});
autoUpdater.on('update-downloaded', (ev: any, info: VersionInfo) => {
this.sendIpcToWindow(IpcEvents.autoUpdater.autoUpdateDownloaded, info);
});
ipcMain.on(IpcEvents.autoUpdater.updateAndRestart, () => autoUpdater.quitAndInstall(true));
ipcMain.on(IpcEvents.app.appStarted, () => {
if (this.checkForUpdateAtStartup()) {
this.checkForUpdate();
}
});
ipcMain.on(IpcEvents.autoUpdater.checkForUpdate, () => this.checkForUpdate());
}
private checkForUpdate() {
if (isDev) {
const msg = 'Application update is not working in dev mode.';
this.logService.info(msg);
this.sendIpcToWindow(IpcEvents.autoUpdater.checkForUpdateNotAvailable, msg);
return;
}
if (this.isFirstRun()) {
const msg = 'Application update is skipping at first run.';
this.logService.info(msg);
this.sendIpcToWindow(IpcEvents.autoUpdater.checkForUpdateNotAvailable, msg);
return;
}
autoUpdater.allowPrerelease = this.allowPreRelease();
autoUpdater.checkForUpdates();
}
private isFirstRun() {
if (!settings.has('firstRunVersion')) {
return true;
}
const firstRunVersion = settings.get('firstRunVersion');
this.logService.info(`firstRunVersion: ${firstRunVersion}`);
this.logService.info(`package.version: ${this.app.getVersion()}`);
return firstRunVersion !== this.app.getVersion();
}
private allowPreRelease() {
const autoUpdateSettings = this.getAutoUpdateSettings();
return autoUpdateSettings && autoUpdateSettings.usePreReleaseUpdate;
}
private checkForUpdateAtStartup() {
const autoUpdateSettings = this.getAutoUpdateSettings();
return autoUpdateSettings && autoUpdateSettings.checkForUpdateOnStartUp;
}
private getAutoUpdateSettings() {
// const storageService = new ElectronDataStorageRepositoryService();
// return storageService.getAutoUpdateSettings();
return { checkForUpdateOnStartUp: false, usePreReleaseUpdate: false };
}
}

View File

@@ -0,0 +1,28 @@
import { ipcMain, BrowserWindow } from 'electron';
import { CommandLineArgs, IpcEvents, AppStartInfo, LogService } from 'uhk-common';
import { MainServiceBase } from './main-service-base';
import { DeviceService } from './device.service';
export class AppService extends MainServiceBase {
constructor(protected logService: LogService,
protected win: Electron.BrowserWindow,
private deviceService: DeviceService,
private options: CommandLineArgs) {
super(logService, win);
ipcMain.on(IpcEvents.app.getAppStartInfo, this.handleAppStartInfo.bind(this));
logService.info('AppService init success');
}
private handleAppStartInfo(event: Electron.Event) {
this.logService.info('getStartInfo');
const response: AppStartInfo = {
commandLineArgs: this.options,
deviceConnected: this.deviceService.isConnected,
hasPermission: this.deviceService.hasPermission()
};
this.logService.info('getStartInfo response:', response);
return event.sender.send(IpcEvents.app.getAppStartInfoReply, response);
}
}

View File

@@ -0,0 +1,167 @@
import { ipcMain, BrowserWindow } from 'electron';
import { Constants, IpcEvents, LogService, IpcResponse } from 'uhk-common';
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import { Device, devices, HID } from 'node-hid';
import 'rxjs/add/observable/interval';
import 'rxjs/add/operator/startWith';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/distinctUntilChanged';
enum Command {
UploadConfig = 8,
ApplyConfig = 9
}
export class DeviceService {
private static convertBufferToIntArray(buffer: Buffer): number[] {
return Array.prototype.slice.call(buffer, 0);
}
private pollTimer$: Subscription;
private connected: boolean = false;
constructor(private logService: LogService,
private win: Electron.BrowserWindow) {
this.pollUhkDevice();
ipcMain.on(IpcEvents.device.saveUserConfiguration, this.saveUserConfiguration.bind(this));
logService.info('DeviceService init success');
}
public get isConnected(): boolean {
return this.connected;
}
public hasPermission(): boolean {
try {
const devs = devices();
return true;
} catch (err) {
this.logService.error('[DeviceService] hasPermission', err);
}
return false;
}
/**
* HID API not support device attached and detached event.
* This method check the keyboard is attached to the computer or not.
* Every second check the HID device list.
*/
private pollUhkDevice(): void {
this.pollTimer$ = Observable.interval(1000)
.startWith(0)
.map(() => {
return devices().some((dev: Device) => dev.vendorId === Constants.VENDOR_ID &&
dev.productId === Constants.PRODUCT_ID);
})
.distinctUntilChanged()
.do((connected: boolean) => {
this.connected = connected;
this.win.webContents.send(IpcEvents.device.deviceConnectionStateChanged, connected);
this.logService.info(`Device connection state changed to: ${connected}`);
})
.subscribe();
}
private saveUserConfiguration(event: Electron.Event, json: string): void {
const response = new IpcResponse();
try {
const buffer: Buffer = new Buffer(JSON.parse(json).data);
const fragments = this.getTransferBuffers(buffer);
const device = this.getDevice();
device.read((err, data) => {
if (err) {
this.logService.error('Send data to device err:', err);
}
this.logService.debug('send data to device response:', data.toString());
});
for (const fragment of fragments) {
const transferData = this.getTransferData(fragment);
this.logService.debug('Fragment: ', JSON.stringify(transferData));
device.write(transferData);
}
const applyBuffer = new Buffer([Command.ApplyConfig]);
const applyTransferData = this.getTransferData(applyBuffer);
this.logService.debug('Fragment: ', JSON.stringify(applyTransferData));
device.write(applyTransferData);
response.success = true;
this.logService.info('transferring finished');
}
catch (error) {
this.logService.error('transferring error', error);
response.error = { message: error.message };
}
event.sender.send(IpcEvents.device.saveUserConfigurationReply, response);
}
private getTransferData(buffer: Buffer): number[] {
const data = DeviceService.convertBufferToIntArray(buffer);
// if data start with 0 need to add additional leading zero because HID API remove it.
// https://github.com/node-hid/node-hid/issues/187
if (data.length > 0 && data[0] === 0) {
data.unshift(0);
}
// From HID API documentation:
// http://www.signal11.us/oss/hidapi/hidapi/doxygen/html/group__API.html#gad14ea48e440cf5066df87cc6488493af
// The first byte of data[] must contain the Report ID.
// For devices which only support a single report, this must be set to 0x0.
data.unshift(0);
return data;
}
private getTransferBuffers(configBuffer: Buffer): Buffer[] {
const fragments: Buffer[] = [];
const MAX_SENDING_PAYLOAD_SIZE = Constants.MAX_PAYLOAD_SIZE - 4;
for (let offset = 0; offset < configBuffer.length; offset += MAX_SENDING_PAYLOAD_SIZE) {
const length = offset + MAX_SENDING_PAYLOAD_SIZE < configBuffer.length
? MAX_SENDING_PAYLOAD_SIZE
: configBuffer.length - offset;
const header = new Buffer([Command.UploadConfig, length, offset & 0xFF, offset >> 8]);
fragments.push(Buffer.concat([header, configBuffer.slice(offset, offset + length)]));
}
return fragments;
}
/**
* Return the 0 interface of the keyboard.
* @returns {HID}
*/
private getDevice(): HID {
try {
const devs = devices();
this.logService.silly('Available devices:', devs);
const dev = devs.find((x: Device) =>
x.vendorId === Constants.VENDOR_ID &&
x.productId === Constants.PRODUCT_ID &&
((x.usagePage === 128 && x.usage === 129) || x.interface === 0));
if (!dev) {
this.logService.info('[DeviceService] UHK Device not found:');
return null;
}
const device = new HID(dev.path);
this.logService.info('Used device:', dev);
return device;
}
catch (err) {
this.logService.error('Can not create device:', err);
}
return null;
}
}

View File

@@ -0,0 +1,4 @@
import * as log from 'electron-log';
log.transports.file.level = 'debug';
export const logger = log;

View File

@@ -0,0 +1,16 @@
import { LogService } from 'uhk-common';
export class MainServiceBase {
constructor(protected logService: LogService,
protected win: Electron.BrowserWindow) {}
protected sendIpcToWindow(message: string, arg?: any) {
this.logService.info('sendIpcToWindow:', message, arg);
if (!this.win || this.win.isDestroyed()) {
return;
}
this.win.webContents.send(message, arg);
}
}

View File

@@ -0,0 +1,57 @@
import { ipcMain, app } from 'electron';
import * as isDev from 'electron-is-dev';
import * as path from 'path';
import * as sudo from 'sudo-prompt';
import { IpcEvents, LogService, IpcResponse } from 'uhk-common';
export class SudoService {
private rootDir: string;
constructor(private logService: LogService) {
if (isDev) {
this.rootDir = path.join(path.join(process.cwd(), process.argv[1]), '../../..');
} else {
this.rootDir = path.dirname(app.getAppPath());
}
this.logService.info('[SudoService] App root dir: ', this.rootDir);
ipcMain.on(IpcEvents.device.setPrivilegeOnLinux, this.setPrivilege.bind(this));
}
private setPrivilege(event: Electron.Event) {
switch (process.platform) {
case 'linux':
this.setPrivilegeOnLinux(event);
break;
default:
const response: IpcResponse = {
success: false,
error: { message: 'Permissions couldn\'t be set. Invalid platform: ' + process.platform }
};
event.sender.send(IpcEvents.device.setPrivilegeOnLinuxReply, response);
break;
}
}
private setPrivilegeOnLinux(event: Electron.Event) {
const scriptPath = path.join(this.rootDir, 'rules/setup-rules.sh');
const options = {
name: 'Setting UHK access rules'
};
const command = `sh ${scriptPath}`;
console.log(command);
sudo.exec(command, options, (error: any) => {
const response = new IpcResponse();
if (error) {
response.success = false;
response.error = error;
} else {
response.success = true;
}
event.sender.send(IpcEvents.device.setPrivilegeOnLinuxReply, response);
});
}
}

View File

@@ -0,0 +1 @@
import 'sudo-prompt';