Windows set up permission button #261 (#300)

* style(privilege): Fix typo 'excusive' -> 'exclusive'

* style(privilege): remove unused imports

* style(privilege): Fix typo 'initizalized$' -> 'initialized$'

* feat(log): Add application wide logger and error handler

It is help to debug electron install app on different device

* feat(privilege): Add windows USB driver installation

* build: I need the windows installer to test the app on windows

* fix(privilege): change wdi-simpler installer to zadic

* feat(log): change log level to debug in renderer process

* chore: Add author in package.json

* feat(privilege): Add privilege setter file as extraResource

* fix(log): Allowed transport level change only in main process

* fix(privilege): Fix app path calculation

* fix(privilege): Take the scriptPath between double quote

* build: revert the appveyor settings

* refactor(privilege): Extract vendor ID, product ID and MAX_PAYLOAD_SIZE into constants file

* refactor(privilege): Add both 32 and 64 bit zadics to extraResource of the installer

* feat(device): Add HID API communication protocol

* build: Fix npm install process

* build: Fix npm install process v2

* ci: Add libudev-dev as travis apt dependencies

* ci: Merge travis apt packages

* ci: remove node-hid from build:usb

* ci: try to fix linux build

* ci: node-hid use git repo

* ci: Add libusb-1.0-0-dev to travis apt dependency

* feat(device): Use logging service when communicate with the device

* build: create test build

* build: PUBLISH_FOR_PULL_REQUEST override

* build: revert TEST_BUILD to false

* build: node-hid use package version instead of git repo

* refactor: remove unused device store files from PR

* ci: Manage test build from environment variable

* fix(privilege): Set rules files dir base on dev or prod mode

* fix(log): Extract nested properties of the logged object

* feat(log): use util.inspect in logger service

* build: upgrade @types/node-hid -> 0.5.2

* fix(device): Add extra logging when try to open device.

* fix(device): log device description and not the device

* fix(device): add win specific write

* fix(device): add report id as first byte

* style(privilege): Reformat else and comment in privilege-checker component

* fix(privilege): Comment out windows branch
This commit is contained in:
Róbert Kiss
2017-07-02 20:33:28 +02:00
committed by László Monda
parent 84b13d3219
commit 2df8f2ea54
23 changed files with 556 additions and 214 deletions

View File

@@ -28,6 +28,9 @@ addons:
- libgnome-keyring-dev
- libsecret-1-dev
- icnsutils
- libudev-dev
- build-essential
- libusb-1.0-0-dev
install:
- nvm install
@@ -45,9 +48,3 @@ script:
cache:
directories:
- node_modules
addons:
apt:
packages:
- build-essential
- libudev-dev

View File

@@ -1,4 +1,4 @@
import { NgModule } from '@angular/core';
import { ErrorHandler, NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
@@ -75,7 +75,9 @@ import { SafeStylePipe } from './shared/pipes';
import { CaptureService } from './shared/services/capture.service';
import { MapperService } from './shared/services/mapper.service';
import { SvgModuleProviderService } from './shared/services/svg-module-provider.service';
import { UhkDeviceService } from './services/uhk-device.service';
import { UhkLibUsbApiService } from './services/uhk-lib-usb-api.service';
import { UhkHidApiService } from './services/uhk-hid-api.service';
import { uhkDeviceProvider } from './services/uhk-device-provider';
import { AutoUpdateSettingsEffects, KeymapEffects, MacroEffects, UserConfigEffects } from './shared/store/effects';
import { ApplicationEffect, AppUpdateEffect } from './store/effects';
@@ -90,6 +92,9 @@ import { UhkDeviceUninitializedGuard } from './services/uhk-device-uninitialized
import { DATA_STORAGE_REPOSITORY } from './shared/services/datastorage-repository.service';
import { ElectronDataStorageRepositoryService } from './services/electron-datastorage-repository.service';
import { DefaultUserConfigurationService } from './shared/services/default-user-configuration.service';
import { ElectronLogService } from './services/electron-log.service';
import { LOG_SERVICE } from '../../shared/src/services/logger.service';
import { ElectronErrorHandlerService } from './services/electron-error-handler.service';
import { AppUpdateRendererService } from './services/app-update-renderer.service';
import { reducer } from './store';
import { AutoUpdateSettings } from './shared/components/auto-update-settings/auto-update-settings';
@@ -185,10 +190,15 @@ import { AutoUpdateSettings } from './shared/components/auto-update-settings/aut
KeymapEditGuard,
MacroNotFoundGuard,
CaptureService,
UhkDeviceService,
{ provide: DATA_STORAGE_REPOSITORY, useClass: ElectronDataStorageRepositoryService },
DefaultUserConfigurationService,
AppUpdateRendererService
{ provide: LOG_SERVICE, useClass: ElectronLogService },
{ provide: ErrorHandler, useClass: ElectronErrorHandlerService },
AppUpdateRendererService,
UhkHidApiService,
UhkLibUsbApiService,
uhkDeviceProvider()
],
bootstrap: [AppComponent]
})

View File

@@ -1,12 +1,11 @@
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/ignoreElements';
import 'rxjs/add/operator/takeWhile';
import { UhkDeviceService } from './../../services/uhk-device.service';
import { UhkDeviceService } from '../../services/uhk-device.service';
@Component({
selector: 'missing-device',

View File

@@ -1,7 +1,7 @@
/// <reference path="../../custom_types/sudo-prompt.d.ts"/>
import { Component } from '@angular/core';
import { Component, Inject } from '@angular/core';
import { Router } from '@angular/router';
import * as isDev from 'electron-is-dev';
import { Observable } from 'rxjs/Observable';
import { ReplaySubject } from 'rxjs/ReplaySubject';
import 'rxjs/add/observable/of';
@@ -15,6 +15,7 @@ import * as path from 'path';
import * as sudo from 'sudo-prompt';
import { UhkDeviceService } from '../../services/uhk-device.service';
import { ILogService, LOG_SERVICE } from '../../../../shared/src/services/logger.service';
@Component({
selector: 'privilege-checker',
@@ -23,7 +24,18 @@ import { UhkDeviceService } from '../../services/uhk-device.service';
})
export class PrivilegeCheckerComponent {
constructor(private router: Router, private uhkDevice: UhkDeviceService) {
private rootDir: string;
constructor(private router: Router,
private uhkDevice: UhkDeviceService,
@Inject(LOG_SERVICE) private logService: ILogService) {
if (isDev) {
this.rootDir = path.resolve(path.join(remote.process.cwd(), remote.process.argv[1]), '..');
} else {
this.rootDir = path.dirname(remote.app.getAppPath());
}
this.logService.info('App root dir: ', this.rootDir);
uhkDevice.isConnected()
.distinctUntilChanged()
.takeWhile(connected => connected)
@@ -50,14 +62,21 @@ export class PrivilegeCheckerComponent {
case 'linux':
permissionSetter = this.setUpPermissionsOnLinux();
break;
// HID API shouldn't need privilege escalation on Windows
// TODO: If all HID API test success then delete this branch and setUpPermissionsOnWin() method
// case 'win32':
// permissionSetter = this.setUpPermissionsOnWin();
// break;
default:
permissionSetter = Observable.throw('Permissions couldn\'t be set. Invalid platform: ' + process.platform);
break;
}
permissionSetter.subscribe({
error: e => console.error(e),
error: e => {
console.log(e);
},
complete: () => {
console.log('Permissions has been successfully set');
this.logService.info('Permissions has been successfully set');
this.uhkDevice.initialize();
this.router.navigate(['/']);
}
@@ -66,12 +85,13 @@ export class PrivilegeCheckerComponent {
private setUpPermissionsOnLinux(): Observable<void> {
const subject = new ReplaySubject<void>();
const rootDir = path.resolve(path.join(remote.process.cwd(), remote.process.argv[1]), '..');
const scriptPath = path.resolve(rootDir, 'rules/setup-rules.sh');
const scriptPath = path.resolve(this.rootDir, 'rules/setup-rules.sh');
const options = {
name: 'Setting UHK access rules'
};
sudo.exec(`sh ${scriptPath}`, options, (error: any) => {
const command = `sh ${scriptPath}`;
console.log(command);
sudo.exec(command, options, (error: any) => {
if (error) {
subject.error(error);
} else {
@@ -82,4 +102,34 @@ export class PrivilegeCheckerComponent {
return subject.asObservable();
}
// TODO: If all HID API test success then delete this method.
// and remove zadic-${process.arch}.exe files from windows installer resources
// private setUpPermissionsOnWin(): Observable<void> {
// const subject = new ReplaySubject<void>();
//
// // source code: https://github.com/pbatard/libwdi
// const scriptPath = path.resolve(this.rootDir, `rules/zadic-${process.arch}.exe`);
// const options = {
// name: 'Setting UHK access rules'
// };
// const params = [
// `--vid $\{Constants.VENDOR_ID}`,
// `--pid $\{Constants.PRODUCT_ID}`,
// '--iface 0', // interface ID
// '--usealldevices', // if the device has installed USB driver than overwrite it
// '--noprompt' // return at the end of the installation and not waiting for any user command
// ];
// const paramsString = params.join(' ');
// const command = `"${scriptPath}" ${paramsString}`;
//
// sudo.exec(command, options, (error: any) => {
// if (error) {
// subject.error(error);
// } else {
// subject.complete();
// }
// });
//
// return subject.asObservable();
// }
}

View File

@@ -13,7 +13,7 @@ import { IpcEvents } from './shared/util';
import { ElectronDataStorageRepositoryService } from './services/electron-datastorage-repository.service';
// import './dev-extension';
// require('electron-debug')({ showDevTools: true, enabled: true });
require('electron-debug')({ showDevTools: false, 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.

View File

@@ -11,6 +11,7 @@
<!--<script src="vendor/usb/usb.js"></script>-->
<script>
usb = require('usb');
nodeHid = require('node-hid');
</script>
</head>
<body>

View File

@@ -0,0 +1,6 @@
import { Observer } from 'rxjs/Observer';
export interface SenderMessage {
buffer: Buffer;
observer: Observer<any>;
}

View File

@@ -14,6 +14,7 @@
"npm": ">=3.10.7 <4.0.0"
},
"dependencies": {
"node-hid": "0.5.4",
"usb": "git+https://github.com/aktary/node-usb.git"
}
}

View File

@@ -0,0 +1,10 @@
import { ErrorHandler, Inject } from '@angular/core';
import { ILogService, LOG_SERVICE } from '../../../shared/src/services/logger.service';
export class ElectronErrorHandlerService implements ErrorHandler {
constructor(@Inject(LOG_SERVICE)private logService: ILogService) {}
handleError(error: any) {
this.logService.error(error);
}
}

View File

@@ -0,0 +1,29 @@
import { Injectable } from '@angular/core';
import * as log from 'electron-log';
import * as util from 'util';
import { ILogService } from '../../../shared/src/services/logger.service';
/**
* This service use the electron-log package to write log in file.
* The logger usable in main and renderer process.
* The location of the log files:
* - on Linux: ~/.config/<app name>/log.log
* - on OS X: ~/Library/Logs/<app name>/log.log
* - on Windows: %USERPROFILE%\AppData\Roaming\<app name>\log.log
* The app name: UHK Agent. The up to date value in the scripts/release.js file.
*/
@Injectable()
export class ElectronLogService implements ILogService {
private static getErrorText(args: any) {
return util.inspect(args);
}
error(...args: any[]): void {
log.error(ElectronLogService.getErrorText(args));
}
info(...args: any[]): void {
log.info(ElectronLogService.getErrorText(args));
}
}

View File

@@ -0,0 +1,18 @@
import { Provider } from '@angular/core';
import { UhkDeviceService } from './uhk-device.service';
import { UhkHidApiService } from './uhk-hid-api.service';
export function uhkDeviceProvider(): Provider {
// HID API officially support MAC, WIN and linux x64 platform
// https://github.com/node-hid/node-hid#platform-support
if (process.platform === 'darwin' ||
process.platform === 'win32' ||
(process.platform === 'linux' && process.arch === 'x64')) {
return { provide: UhkDeviceService, useClass: UhkHidApiService };
}
// On other platform use libUsb, but we try to test on all platform
// return { provide: UhkDeviceService, useClass: UhkLibUsbApiService };
return { provide: UhkDeviceService, useClass: UhkHidApiService };
}

View File

@@ -1,195 +1,49 @@
import { Injectable, OnDestroy, NgZone } from '@angular/core';
import { Inject } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Observer } from 'rxjs/Observer';
import { ConnectableObservable } from 'rxjs/observable/ConnectableObservable';
import { Subject } from 'rxjs/Subject';
import { Observer } from 'rxjs/Observer';
import { Subscriber } from 'rxjs/Subscriber';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Subscription } from 'rxjs/Subscription';
import 'rxjs/add/observable/empty';
import 'rxjs/add/observable/from';
import 'rxjs/add/observable/of';
import 'rxjs/add/observable/timer';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/concat';
import 'rxjs/add/operator/combineLatest';
import 'rxjs/add/operator/concatMap';
import 'rxjs/add/operator/first';
import 'rxjs/add/operator/mergeMap';
import 'rxjs/add/operator/publish';
import 'rxjs/add/operator/switchMap';
import { Device, Interface, InEndpoint, OutEndpoint, findByIds, on } from 'usb';
import { Layer } from '../shared/config-serializer/config-items/layer';
import { UhkBuffer } from '../shared/config-serializer/uhk-buffer';
const vendorId = 0x1d50;
const productId = 0x6122;
const MAX_PAYLOAD_SIZE = 64;
import { ILogService, LOG_SERVICE } from '../shared/services/logger.service';
import { SenderMessage } from '../models/sender-message';
import { Constants } from '../shared/util/constants';
enum Command {
UploadConfig = 8,
ApplyConfig = 9
}
interface SenderMessage {
buffer: Buffer;
observer: Observer<any>;
}
export abstract class UhkDeviceService {
protected connected$: BehaviorSubject<boolean>;
protected initialized$: BehaviorSubject<boolean>;
protected deviceOpened$: BehaviorSubject<boolean>;
protected outSubscription: Subscription;
@Injectable()
export class UhkDeviceService implements OnDestroy {
protected messageIn$: Observable<Buffer>;
protected messageOut$: Subject<SenderMessage>;
private device: Device;
private deviceOpened$: BehaviorSubject<boolean>;
private connected$: BehaviorSubject<boolean>;
private initizalized$: BehaviorSubject<boolean>;
private messageIn$: Observable<Buffer>;
private messageOut$: Subject<SenderMessage>;
private outSubscription: Subscription;
constructor(zone: NgZone) {
constructor(@Inject(LOG_SERVICE) protected logService: ILogService) {
this.messageOut$ = new Subject<SenderMessage>();
this.initizalized$ = new BehaviorSubject(false);
this.initialized$ = new BehaviorSubject(false);
this.connected$ = new BehaviorSubject(false);
this.deviceOpened$ = new BehaviorSubject(false);
this.outSubscription = Subscription.EMPTY;
this.initialize();
// The change detection doesn't work properly if the callbacks are called outside Angular Zone
on('attach', (device: Device) => zone.run(() => this.onDeviceAttach(device)));
on('detach', (device: Device) => zone.run(() => this.onDeviceDetach(device)));
}
ngOnDestroy() {
this.disconnect();
this.initizalized$.unsubscribe();
this.initialized$.unsubscribe();
this.connected$.unsubscribe();
this.deviceOpened$.unsubscribe();
}
initialize(): void {
if (this.initizalized$.getValue()) {
return;
}
this.device = findByIds(vendorId, productId);
this.connected$.next(!!this.device);
if (!this.device) {
return;
}
try {
this.device.open();
this.deviceOpened$.next(true);
} catch (error) {
console.log(error);
return;
}
const usbInterface: Interface = this.device.interface(0);
// https://github.com/tessel/node-usb/issues/147
// The function 'isKernelDriverActive' is not available on Windows and not even needed.
if (process.platform !== 'win32' && usbInterface.isKernelDriverActive()) {
usbInterface.detachKernelDriver();
}
// https://github.com/tessel/node-usb/issues/30
// Mac is not allow excusive right to use USB
if (process.platform !== 'darwin') {
usbInterface.claim();
}
this.messageIn$ = Observable.create((subscriber: Subscriber<Buffer>) => {
const inEndPoint: InEndpoint = <InEndpoint>usbInterface.endpoints[0];
console.log('Try to read');
inEndPoint.transfer(MAX_PAYLOAD_SIZE, (error: string, receivedBuffer: Buffer) => {
if (error) {
console.error('reading error', error);
subscriber.error(error);
} else {
console.log('read data', receivedBuffer);
subscriber.next(receivedBuffer);
subscriber.complete();
}
});
});
const outEndPoint: OutEndpoint = <OutEndpoint>usbInterface.endpoints[1];
const outSending = this.messageOut$.concatMap(senderPackage => {
return (<Observable<void>>Observable.create((subscriber: Subscriber<void>) => {
console.log('transfering', senderPackage.buffer);
outEndPoint.transfer(senderPackage.buffer, error => {
if (error) {
console.error('transfering errored', error);
subscriber.error(error);
} else {
console.log('transfering finished');
subscriber.complete();
}
});
})).concat(this.messageIn$)
.do(buffer => senderPackage.observer.next(buffer) && senderPackage.observer.complete())
.catch((error: string) => {
senderPackage.observer.error(error);
return Observable.empty<void>();
});
}).publish();
this.outSubscription = outSending.connect();
this.initizalized$.next(true);
}
disconnect() {
this.outSubscription.unsubscribe();
this.messageIn$ = undefined;
this.initizalized$.next(false);
this.deviceOpened$.next(false);
this.connected$.next(false);
}
isInitialized(): Observable<boolean> {
return this.initizalized$.asObservable();
}
isConnected(): Observable<boolean> {
return this.connected$.asObservable();
}
hasPermissions(): Observable<boolean> {
return this.isConnected()
.combineLatest(this.deviceOpened$)
.map((latest: boolean[]) => {
const connected = latest[0];
const opened = latest[1];
if (!connected) {
return false;
} else if (opened) {
return true;
}
try {
this.device.open();
} catch (error) {
return false;
}
this.device.close();
return true;
});
}
isOpened(): Observable<boolean> {
return this.deviceOpened$.asObservable();
}
sendConfig(configBuffer: Buffer): Observable<Buffer> {
return Observable.create((subscriber: Subscriber<Buffer>) => {
console.log('Sending...', configBuffer);
this.logService.info('Sending...', configBuffer);
const fragments: Buffer[] = [];
const MAX_SENDING_PAYLOAD_SIZE = MAX_PAYLOAD_SIZE - 4;
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
@@ -206,7 +60,7 @@ export class UhkDeviceService implements OnDestroy {
if (buffers.length === fragments.length) {
subscriber.next(Buffer.concat(buffers));
subscriber.complete();
console.log('Sending finished');
this.logService.info('Sending finished');
}
}
};
@@ -219,7 +73,7 @@ export class UhkDeviceService implements OnDestroy {
applyConfig(): Observable<Buffer> {
return Observable.create((subscriber: Subscriber<Buffer>) => {
console.log('Applying configuration');
this.logService.info('Applying configuration');
this.messageOut$.next({
buffer: new Buffer([Command.ApplyConfig]),
observer: subscriber
@@ -227,21 +81,27 @@ export class UhkDeviceService implements OnDestroy {
});
}
onDeviceAttach(device: Device) {
if (device.deviceDescriptor.idVendor !== vendorId || device.deviceDescriptor.idProduct !== productId) {
return;
}
// Ugly hack: device is not openable (on Windows) right after the attach
Observable.timer(100)
.first()
.subscribe(() => this.initialize());
isInitialized(): Observable<boolean> {
return this.initialized$.asObservable();
}
onDeviceDetach(device: Device) {
if (device.deviceDescriptor.idVendor !== vendorId || device.deviceDescriptor.idProduct !== productId) {
return;
}
this.disconnect();
isConnected(): Observable<boolean> {
return this.connected$.asObservable();
}
isOpened(): Observable<boolean> {
return this.deviceOpened$.asObservable();
}
disconnect() {
this.outSubscription.unsubscribe();
this.messageIn$ = undefined;
this.initialized$.next(false);
this.deviceOpened$.next(false);
this.connected$.next(false);
}
abstract initialize(): void;
abstract hasPermissions(): Observable<boolean>;
}

View File

@@ -0,0 +1,164 @@
import { Inject, Injectable, OnDestroy } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Subscriber } from 'rxjs/Subscriber';
import { Subscription } from 'rxjs/Subscription';
import { Device, devices, HID } from 'node-hid';
import 'rxjs/add/observable/empty';
import 'rxjs/add/observable/timer';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/concat';
import 'rxjs/add/operator/combineLatest';
import 'rxjs/add/operator/concatMap';
import 'rxjs/add/operator/publish';
import 'rxjs/add/operator/do';
import { ILogService, LOG_SERVICE } from '../shared/services/logger.service';
import { Constants } from '../shared/util';
import { UhkDeviceService } from './uhk-device.service';
@Injectable()
export class UhkHidApiService extends UhkDeviceService implements OnDestroy {
private device: HID;
private pollTimer$: Subscription;
constructor(@Inject(LOG_SERVICE) protected logService: ILogService) {
super(logService);
this.pollUhkDevice();
}
ngOnDestroy() {
super.ngOnDestroy();
this.pollTimer$.unsubscribe();
}
initialize(): void {
if (this.initialized$.getValue()) {
return;
}
this.device = this.getDevice();
if (!this.device) {
return;
}
this.deviceOpened$.next(true);
this.messageIn$ = Observable.create((subscriber: Subscriber<Buffer>) => {
this.logService.info('Try to read');
this.device.read((error: any, data: any = []) => {
if (error) {
this.logService.error('reading error', error);
subscriber.error(error);
} else {
this.logService.info('read data', data);
subscriber.next(data);
subscriber.complete();
}
});
});
const outSending = this.messageOut$.concatMap(senderPackage => {
return (<Observable<void>>Observable.create((subscriber: Subscriber<void>) => {
this.logService.info('transfering', senderPackage.buffer);
const data = Array.prototype.slice.call(senderPackage.buffer, 0);
// 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);
try {
this.device.write(data);
this.logService.info('transfering finished');
subscriber.complete();
}
catch (error) {
this.logService.error('transfering errored', error);
subscriber.error(error);
}
})).concat(this.messageIn$)
.do(buffer => senderPackage.observer.next(buffer) && senderPackage.observer.complete())
.catch((error: string) => {
senderPackage.observer.error(error);
return Observable.empty<void>();
});
}).publish();
this.outSubscription = outSending.connect();
this.initialized$.next(true);
}
hasPermissions(): Observable<boolean> {
return this.isConnected()
.combineLatest(this.deviceOpened$)
.map((latest: boolean[]) => {
const connected = latest[0];
const opened = latest[1];
if (!connected) {
return false;
} else if (opened) {
return true;
}
return true;
});
}
/**
* Return the 0 interface of the keyboard.
* @returns {HID}
*/
getDevice(): HID {
try {
const devs = devices();
this.logService.info('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));
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;
}
/**
* 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() {
this.pollTimer$ = Observable.timer(0, 1000)
.map(() => {
return devices().some((dev: Device) => dev.vendorId === Constants.VENDOR_ID &&
dev.productId === Constants.PRODUCT_ID);
})
.distinctUntilChanged()
.do((connected: boolean) => {
this.connected$.next(connected);
if (connected) {
this.initialize();
} else {
this.disconnect();
}
})
.subscribe();
}
}

View File

@@ -0,0 +1,142 @@
import { Inject, Injectable, NgZone, OnDestroy } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Subscriber } from 'rxjs/Subscriber';
import 'rxjs/add/observable/empty';
import 'rxjs/add/observable/timer';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/concat';
import 'rxjs/add/operator/combineLatest';
import 'rxjs/add/operator/concatMap';
import 'rxjs/add/operator/first';
import 'rxjs/add/operator/publish';
import 'rxjs/add/operator/do';
import { Device, findByIds, InEndpoint, Interface, on, OutEndpoint } from 'usb';
import { ILogService, LOG_SERVICE } from '../shared/services/logger.service';
import { Constants } from '../shared/util';
import { UhkDeviceService } from './uhk-device.service';
@Injectable()
export class UhkLibUsbApiService extends UhkDeviceService implements OnDestroy {
private device: Device;
static isUhkDevice(device: Device) {
return device.deviceDescriptor.idVendor === Constants.VENDOR_ID &&
device.deviceDescriptor.idProduct === Constants.PRODUCT_ID;
}
constructor(zone: NgZone,
@Inject(LOG_SERVICE) protected logService: ILogService) {
super(logService);
this.initialize();
// The change detection doesn't work properly if the callbacks are called outside Angular Zone
on('attach', (device: Device) => zone.run(() => this.onDeviceAttach(device)));
on('detach', (device: Device) => zone.run(() => this.onDeviceDetach(device)));
}
initialize(): void {
if (this.initialized$.getValue()) {
return;
}
this.device = findByIds(Constants.VENDOR_ID, Constants.PRODUCT_ID);
this.connected$.next(!!this.device);
if (!this.device) {
return;
}
try {
this.device.open();
this.deviceOpened$.next(true);
} catch (error) {
this.logService.error(error);
return;
}
const usbInterface: Interface = this.device.interface(0);
// https://github.com/tessel/node-usb/issues/147
// The function 'isKernelDriverActive' is not available on Windows and not even needed.
if (usbInterface.isKernelDriverActive()) {
usbInterface.detachKernelDriver();
}
this.messageIn$ = Observable.create((subscriber: Subscriber<Buffer>) => {
const inEndPoint: InEndpoint = <InEndpoint>usbInterface.endpoints[0];
this.logService.info('Try to read');
inEndPoint.transfer(Constants.MAX_PAYLOAD_SIZE, (error: string, receivedBuffer: Buffer) => {
if (error) {
this.logService.error('reading error', error);
subscriber.error(error);
} else {
this.logService.info('read data', receivedBuffer);
subscriber.next(receivedBuffer);
subscriber.complete();
}
});
});
const outEndPoint: OutEndpoint = <OutEndpoint>usbInterface.endpoints[1];
const outSending = this.messageOut$.concatMap(senderPackage => {
return (<Observable<void>>Observable.create((subscriber: Subscriber<void>) => {
this.logService.info('transfering', senderPackage.buffer);
outEndPoint.transfer(senderPackage.buffer, error => {
if (error) {
this.logService.error('transfering errored', error);
subscriber.error(error);
} else {
this.logService.info('transfering finished');
subscriber.complete();
}
});
})).concat(this.messageIn$)
.do(buffer => senderPackage.observer.next(buffer) && senderPackage.observer.complete())
.catch((error: string) => {
senderPackage.observer.error(error);
return Observable.empty<void>();
});
}).publish();
this.outSubscription = outSending.connect();
this.initialized$.next(true);
}
hasPermissions(): Observable<boolean> {
return this.isConnected()
.combineLatest(this.deviceOpened$)
.map((latest: boolean[]) => {
const connected = latest[0];
const opened = latest[1];
if (!connected) {
return false;
} else if (opened) {
return true;
}
try {
this.device.open();
} catch (error) {
return false;
}
this.device.close();
return true;
});
}
onDeviceAttach(device: Device) {
if (!UhkLibUsbApiService.isUhkDevice(device)) {
return;
}
// Ugly hack: device is not openable (on Windows) right after the attach
Observable.timer(100)
.first()
.subscribe(() => this.initialize());
}
onDeviceDetach(device: Device) {
if (!UhkLibUsbApiService.isUhkDevice(device)) {
return;
}
this.disconnect();
}
}

View File

@@ -18,7 +18,8 @@ module.exports = {
},
target: 'electron-renderer',
externals: {
usb: 'usb'
usb: 'usb',
'node-hid':'nodeHid'
},
devtool: 'source-map',
resolve: {
@@ -40,7 +41,8 @@ module.exports = {
use: ['raw-loader', 'sass-loader']
},
{ test: /jquery/, loader: 'expose-loader?$!expose-loader?jQuery' },
{ test: require.resolve("usb"), loader: "expose-loader?usb" }
{ test: require.resolve("usb"), loader: "expose-loader?usb" },
{ test: require.resolve("node-hid"), loader: "expose-loader?node-hid" }
]
},
plugins: [

View File

@@ -1,5 +1,6 @@
{
"name": "uhk-agent",
"author": "Ultimate Gadget Laboratories",
"main": "electron/dist/electron-main.js",
"version": "1.0.0",
"description": "Agent is the configuration application of the Ultimate Hacking Keyboard.",
@@ -21,6 +22,7 @@
"@types/file-saver": "0.0.1",
"@types/jquery": "3.2.1",
"@types/node": "^6.0.78",
"@types/node-hid": "^0.5.2",
"@types/usb": "^1.1.3",
"angular2-template-loader": "0.6.2",
"copy-webpack-plugin": "^4.0.1",
@@ -37,6 +39,7 @@
"npm-run-all": "4.0.2",
"path": "^0.12.7",
"raw-loader": "^0.5.1",
"rimraf": "^2.6.1",
"sass-loader": "^6.0.3",
"standard-version": "^4.0.0",
"stylelint": "^7.10.1",
@@ -75,6 +78,7 @@
"ng2-dragula": "1.5.0",
"ng2-select2": "1.0.0-beta.10",
"ngrx-store-freeze": "^0.1.9",
"node-hid": "0.5.4",
"reselect": "3.0.1",
"rxjs": "^5.4.1",
"select2": "^4.0.3",
@@ -85,17 +89,17 @@
"zone.js": "0.8.12"
},
"scripts": {
"postinstall": "run-p build:usb \"symlink -- -i\" ",
"postinstall": "run-p \"symlink -- -i\" ",
"test": "cd ./test-serializer && node ./test-serializer.js",
"lint": "run-s -scn lint:ts lint:style",
"lint:ts": "tslint \"electron/src/**/*.ts\" \"web/src/**/*.ts\" \"shared/**/*.ts\" \"test-serializer/**/*.ts\"",
"lint:style": "stylelint \"electron/**/*.scss\" \"web/**/*.scss\" \"shared/**/*.scss\" --syntax scss",
"build": "run-p build:web build:electron",
"build:web": "webpack --config \"web/src/webpack.config.js\"",
"build:electron": "run-s -scn build:electron:main build:electron:app",
"build:electron": "run-s -sn build:electron:main build:electron:app install:build-deps",
"build:electron:main": "webpack --config \"electron/src/webpack.config.electron-main.js\"",
"build:electron:app": "webpack --config \"electron/src/webpack.config.js\"",
"build:usb": "electron-rebuild -w usb -p",
"build:usb": "electron-rebuild -w usb,node-hid -p -m electron/dist",
"build:test": "webpack --config \"test-serializer/webpack.config.js\"",
"server:web": "webpack-dev-server --config \"web/src/webpack.config.js\" --content-base \"./web/dist\"",
"server:electron": "webpack --config \"electron/src/webpack.config.js\" --watch",
@@ -103,7 +107,8 @@
"symlink": "node ./tools/symlinker",
"standard-version": "standard-version",
"pack": "node ./scripts/release.js",
"install:build-deps": "cd electron/dist && npm i",
"release": "npm run install:build-deps && node ./scripts/release.js"
"install:build-deps": "cd electron/dist && npm i && cd .. && npm run build:usb",
"release": "node ./scripts/release.js",
"clean": "rimraf ./node_modules ./electron/dist"
}
}

BIN
rules/zadic-ia32.exe Normal file

Binary file not shown.

BIN
rules/zadic-x64.exe Normal file

Binary file not shown.

View File

@@ -1,7 +1,11 @@
'use strict';
const jsonfile = require('jsonfile');
const TEST_BUILD = false; // set true if you would like to test on your local machince
const TEST_BUILD = process.env.TEST_BUILD; // set true if you would like to test on your local machince
// electron-builder security override.
// Need if wanna create test release build from PR
process.env.PUBLISH_FOR_PULL_REQUEST = TEST_BUILD;
if (!process.env.CI && !TEST_BUILD) {
console.error('Create release only on CI server');
@@ -25,7 +29,7 @@ if (process.env.TRAVIS) {
repoName = process.env.APPVEYOR_REPO_NAME;
}
console.log({branchName, pullRequestNr, gitTag, repoName});
console.log({ branchName, pullRequestNr, gitTag, repoName });
const isReleaseCommit = TEST_BUILD || branchName === gitTag && repoName === 'UltimateHackingKeyboard/agent';
@@ -51,16 +55,23 @@ if (process.env.TRAVIS) {
let target = '';
let artifactName = 'UHK.Agent-${version}-${os}';
let extraResources = [];
if (process.platform === 'darwin') {
target = Platform.MAC.createTarget();
artifactName += '.${ext}';
} else if (process.platform === 'win32') {
target = Platform.WINDOWS.createTarget();
// TODO: If all HID API test success then remove zadic extra resources
extraResources.push(`rules/zadic-ia32.exe`);
extraResources.push(`rules/zadic-x64.exe`);
artifactName += '-${arch}.${ext}';
extraResources.push(`rules/zadic-${process.arch}.exe`);
} else if (process.platform === 'linux') {
target = Platform.LINUX.createTarget();
artifactName += '.${ext}';
extraResources.push('rules/setup-rules.sh');
extraResources.push('rules/50-uhk60.rules');
} else {
console.error(`I dunno how to publish a release for ${process.platform} :(`);
process.exit(1);
@@ -78,7 +89,7 @@ if (TEST_BUILD || gitTag) {
updateVersionNumberIn2rndPackageJson(jsonVersion);
builder.build({
dir: true,
dir: TEST_BUILD,
targets: target,
appMetadata: {
main: 'electron-main.js',
@@ -97,6 +108,12 @@ if (TEST_BUILD || gitTag) {
mac: {
category: 'public.app-category.utilities'
},
win: {
extraResources
},
linux: {
extraResources
},
publish: 'github',
artifactName,
files: [

View File

@@ -0,0 +1,20 @@
import {Injectable, InjectionToken} from '@angular/core';
export interface ILogService {
error(...args: any[]): void;
info(...args: any[]): void;
}
export let LOG_SERVICE = new InjectionToken('logger-service');
@Injectable()
export class ConsoleLogService implements ILogService {
error(...args: any[]): void {
console.error(args);
}
info(...args: any[]): void {
console.info(args);
}
}

View File

@@ -0,0 +1,5 @@
export namespace Constants {
export const VENDOR_ID = 0x1D50;
export const PRODUCT_ID = 0x6122;
export const MAX_PAYLOAD_SIZE = 64;
}

View File

@@ -1,3 +1,7 @@
import { Constants } from './constants';
export { Constants };
// Source: http://stackoverflow.com/questions/13720256/javascript-regex-camelcase-to-sentence
export function camelCaseToSentence(camelCasedText: string): string {
return camelCasedText.replace(/^[a-z]|[A-Z]/g, function (v, i) {

View File

@@ -76,7 +76,8 @@ import { MacroNotFoundGuard } from './shared/components/macro/not-found';
import { DATA_STORAGE_REPOSITORY } from './shared/services/datastorage-repository.service';
import { LocalDataStorageRepositoryService } from './shared/services/local-datastorage-repository.service';
import { DefaultUserConfigurationService } from './shared/services/default-user-configuration.service';
import { reducer } from './shared/store/reducers/index';
import { reducer } from '../../shared/src/store/reducers/index';
import { ConsoleLogService, LOG_SERVICE } from '../../shared/src/services/logger.service';
import { AutoUpdateSettings } from './shared/components/auto-update-settings/auto-update-settings';
@NgModule({
@@ -159,10 +160,11 @@ import { AutoUpdateSettings } from './shared/components/auto-update-settings/aut
KeymapEditGuard,
MacroNotFoundGuard,
CaptureService,
{ provide: DATA_STORAGE_REPOSITORY, useClass: LocalDataStorageRepositoryService },
{provide: DATA_STORAGE_REPOSITORY, useClass: LocalDataStorageRepositoryService},
DefaultUserConfigurationService,
{ provide: LOG_SERVICE, useClass: ConsoleLogService },
DefaultUserConfigurationService
],
bootstrap: [MainAppComponent]
})
export class AppModule {
}
export class AppModule { }