feat: show udev rules on missing device screen (#842)

* feat: show udev rules on missing device screen

* delete MissingDeviceComponent

* feat: change privilege and message device screen texts

* feat: change message device screen texts

* fix: load config from device

* fix: load config when hasPermission = true

* fix: load app start info after set permission rules

* fix: action dispatch

* fix: load config from device when UdevRulesInfo.Ok

* fix: load config from device when 0 device available

* feat: add extra space between the "old udev rule" and permission button
This commit is contained in:
Róbert Kiss
2018-11-09 01:30:40 +01:00
committed by László Monda
parent 10cd06c70b
commit 1a9bd7de83
26 changed files with 275 additions and 131 deletions

View File

@@ -1 +1 @@
<uhk-message header="Cannot find your UHK" subtitle="Please plug it in!"></uhk-message>
<uhk-message [header]="state.header" [subtitle]="state.subtitle"></uhk-message>

View File

@@ -1,14 +1,27 @@
import { Component } from '@angular/core';
import { Component, OnDestroy } from '@angular/core';
import { Store } from '@ngrx/store';
import { Subscription } from 'rxjs/Subscription';
import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/ignoreElements';
import 'rxjs/add/operator/takeWhile';
import { AppState, getMissingDeviceState } from '../../store';
import { MissingDeviceState } from '../../models/missing-device-state';
@Component({
selector: 'missing-device',
templateUrl: './missing-device.component.html'
})
export class MissingDeviceComponent {
export class MissingDeviceComponent implements OnDestroy {
constructor() {}
state: MissingDeviceState;
private stateSubscription: Subscription;
constructor(private store: Store<AppState>) {
this.stateSubscription = this.store
.select(getMissingDeviceState)
.subscribe(state => this.state = state);
}
ngOnDestroy(): void {
this.stateSubscription.unsubscribe();
}
}

View File

@@ -2,8 +2,14 @@
<uhk-message header="Cannot talk to your UHK"
subtitle="Your UHK has been detected, but its permissions are not set up yet, so Agent can't talk to it."></uhk-message>
<button class="btn btn-default btn-lg btn-primary"
(click)="setUpPermissions()"> Set up permissions
<div *ngIf="state.updateUdevRules">
You seem to have an old udev rule file installed. New Agent versions require and updated udev rule file to find your UHK.
</div>
<button class="btn btn-default btn-lg btn-primary mt-10"
(click)="setUpPermissions()">
<span *ngIf="!state.updateUdevRules">Set up permissions</span>
<span *ngIf="state.updateUdevRules">Update udev rule file</span>
</button>
<div class="mt-10">
@@ -22,21 +28,7 @@
<div *ngIf="state.showWhatWillThisDoContent">
If you want to set up permissions manually:
<ol>
<li>Open a terminal.</li>
<li>Run <code>su</code> to become root.</li>
<li>Paste the following script, and <a class="link-inline" (click)="retry()">retry</a>.</li>
</ol>
<div class="copy-container">
<span class="fa fa-2x fa-copy"
ngxClipboard
[cbContent]="command"
title="Copy to clipboard"
data-toggle="tooltip"
data-placement="top"></span>
<pre><code>{{ command }}</code></pre>
</div>
<udev-rules></udev-rules>
</div>
</div>
</div>

View File

@@ -18,17 +18,6 @@ export class PrivilegeCheckerComponent implements OnInit, OnDestroy {
state: PrivilagePageSate;
command = `cat <<EOF >/etc/udev/rules.d/50-uhk60.rules
# Ultimate Hacking Keyboard rules
# These are the udev rules for accessing the USB interfaces of the UHK as non-root users.
# Copy this file to /etc/udev/rules.d and physically reconnect the UHK afterwards.
SUBSYSTEM=="input", GROUP="input", MODE="0666"
SUBSYSTEMS=="usb", ATTRS{idVendor}=="1d50", ATTRS{idProduct}=="612[0-7]", MODE:="0666", GROUP="plugdev"
KERNEL=="hidraw*", ATTRS{idVendor}=="1d50", ATTRS{idProduct}=="612[0-7]", MODE="0666", GROUP="plugdev"
EOF
udevadm trigger
udevadm settle`;
private stateSubscription: Subscription;
constructor(private store: Store<AppState>,

View File

@@ -0,0 +1,15 @@
<ol>
<li>Open a terminal.</li>
<li>Run <code>su</code> to become root.</li>
<li>Paste the following script:</li>
</ol>
<div class="copy-container">
<span class="fa fa-2x fa-copy"
ngxClipboard
[cbContent]="command"
title="Copy to clipboard"
data-toggle="tooltip"
data-placement="top"></span>
<pre><code>{{ command }}</code></pre>
</div>

View File

@@ -0,0 +1,19 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'udev-rules',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './udev-rules.component.html'
})
export class UdevRulesComponent {
command = `cat <<EOF >/etc/udev/rules.d/50-uhk60.rules
# Ultimate Hacking Keyboard rules
# These are the udev rules for accessing the USB interfaces of the UHK as non-root users.
# Copy this file to /etc/udev/rules.d and physically reconnect the UHK afterwards.
SUBSYSTEM=="input", GROUP="input", MODE="0666"
SUBSYSTEMS=="usb", ATTRS{idVendor}=="1d50", ATTRS{idProduct}=="612[0-7]", MODE:="0666", GROUP="plugdev"
KERNEL=="hidraw*", ATTRS{idVendor}=="1d50", ATTRS{idProduct}=="612[0-7]", MODE="0666", GROUP="plugdev"
EOF
udevadm trigger
udevadm settle`;
}

View File

@@ -0,0 +1,4 @@
export interface MissingDeviceState {
header: string;
subtitle: string;
}

View File

@@ -2,4 +2,5 @@ export interface PrivilagePageSate {
showWhatWillThisDo: boolean;
showWhatWillThisDoContent: boolean;
permissionSetupFailed: boolean;
updateUdevRules: boolean;
}

View File

@@ -111,6 +111,7 @@ import { UhkDeviceBootloaderNotActiveGuard } from './services/uhk-device-bootloa
import { FileUploadComponent } from './components/file-upload';
import { AutoGrowInputComponent } from './components/auto-grow-input';
import { HelpPageComponent } from './components/agent/help-page/help-page.component';
import { UdevRulesComponent } from './components/udev-rules/udev-rules.component';
@NgModule({
declarations: [
@@ -188,7 +189,8 @@ import { HelpPageComponent } from './components/agent/help-page/help-page.compon
AutoGrowInputComponent,
HelpPageComponent,
ExternalUrlDirective,
SvgSecondaryRoleComponent
SvgSecondaryRoleComponent,
UdevRulesComponent
],
imports: [
CommonModule,

View File

@@ -66,11 +66,7 @@ export class ApplicationEffects {
this.logService.debug('[AppEffect][processStartInfo] payload:', appInfo);
return [
new ApplyAppStartInfoAction(appInfo),
new ConnectionStateChangedAction({
connected: appInfo.deviceConnected,
hasPermission: appInfo.hasPermission,
bootloaderActive: appInfo.bootloaderActive
})
new ConnectionStateChangedAction(appInfo.deviceConnectionState)
];
});

View File

@@ -18,6 +18,7 @@ import {
HardwareConfiguration,
IpcResponse,
NotificationType,
UdevRulesInfo,
UserConfiguration
} from 'uhk-common';
import {
@@ -39,9 +40,10 @@ import {
UpdateFirmwareSuccessAction,
UpdateFirmwareWithAction
} from '../actions/device';
import { AppRendererService } from '../../services/app-renderer.service';
import { DeviceRendererService } from '../../services/device-renderer.service';
import { SetupPermissionErrorAction, ShowNotificationAction } from '../actions/app';
import { AppState, getRouterState } from '../index';
import { AppState, deviceConnected, getRouterState } from '../index';
import {
ActionTypes as UserConfigActions,
ApplyUserConfigurationFromFileAction,
@@ -57,7 +59,7 @@ export class DeviceEffects {
@Effect()
deviceConnectionStateChange$: Observable<Action> = this.actions$
.ofType<ConnectionStateChangedAction>(ActionTypes.CONNECTION_STATE_CHANGED)
.withLatestFrom(this.store.select(getRouterState))
.withLatestFrom(this.store.select(getRouterState), this.store.select(deviceConnected))
.do(([action, route]) => {
const state = action.payload;
@@ -65,23 +67,24 @@ export class DeviceEffects {
return;
}
if (!state.hasPermission) {
this.router.navigate(['/privilege']);
if (!state.hasPermission || !state.zeroInterfaceAvailable) {
return this.router.navigate(['/privilege']);
}
else if (state.bootloaderActive) {
this.router.navigate(['/recovery-device']);
}
else if (state.connected) {
this.router.navigate(['/']);
}
else {
this.router.navigate(['/detection']);
}
})
.switchMap(([action, route]) => {
const state = action.payload;
if (state.connected && state.hasPermission) {
if (state.bootloaderActive) {
return this.router.navigate(['/recovery-device']);
}
if (state.connected && state.zeroInterfaceAvailable) {
return this.router.navigate(['/']);
}
return this.router.navigate(['/detection']);
})
.switchMap(([action, route, connected]) => {
const payload = action.payload;
if (connected && payload.hasPermission && payload.zeroInterfaceAvailable) {
return Observable.of(new LoadConfigFromDeviceAction());
}
@@ -99,16 +102,13 @@ export class DeviceEffects {
setPrivilegeOnLinuxReply$: Observable<Action> = this.actions$
.ofType<SetPrivilegeOnLinuxReplyAction>(ActionTypes.SET_PRIVILEGE_ON_LINUX_REPLY)
.map(action => action.payload)
.map((response: any): any => {
.switchMap((response: any): any => {
if (response.success) {
return new ConnectionStateChangedAction({
connected: true,
hasPermission: true,
bootloaderActive: false
});
this.appRendererService.getAppStartInfo();
return Observable.empty();
}
return new SetupPermissionErrorAction(response.error);
return Observable.of(new SetupPermissionErrorAction(response.error));
});
@Effect({dispatch: false})
@@ -243,6 +243,7 @@ export class DeviceEffects {
constructor(private actions$: Actions,
private router: Router,
private appRendererService: AppRendererService,
private deviceRendererService: DeviceRendererService,
private store: Store<AppState>,
private dataStorageRepository: DataStorageRepositoryService,

View File

@@ -1,6 +1,6 @@
import { createSelector } from 'reselect';
import { ActionReducerMap, MetaReducer } from '@ngrx/store';
import { RouterReducerState, routerReducer } from '@ngrx/router-store';
import { routerReducer, RouterReducerState } from '@ngrx/router-store';
import { storeFreeze } from 'ngrx-store-freeze';
import { HardwareModules, Keymap, UserConfiguration } from 'uhk-common';
@@ -14,6 +14,7 @@ import * as fromSelectors from './reducers/selectors';
import { initProgressButtonState } from './reducers/progress-button-state';
import { environment } from '../../environments/environment';
import { RouterStateUrl } from './router-util';
import { PrivilagePageSate } from '../models/privilage-page-sate';
import { isVersionGte } from '../util';
// State interface for the application
@@ -52,7 +53,6 @@ export const runningInElectron = createSelector(appState, fromApp.runningInElect
export const getKeyboardLayout = createSelector(appState, fromApp.getKeyboardLayout);
export const deviceConfigurationLoaded = createSelector(appState, fromApp.deviceConfigurationLoaded);
export const getAgentVersionInfo = createSelector(appState, fromApp.getAgentVersionInfo);
export const getPrivilegePageState = createSelector(appState, fromApp.getPrivilagePageState);
export const getOperatingSystem = createSelector(appState, fromSelectors.getOperatingSystem);
export const keypressCapturing = createSelector(appState, fromApp.keypressCapturing);
export const runningOnNotSupportedWindows = createSelector(appState, fromApp.runningOnNotSupportedWindows);
@@ -66,14 +66,21 @@ export const getAutoUpdateSettings = createSelector(appUpdateSettingsState, auto
export const getCheckingForUpdate = createSelector(appUpdateSettingsState, autoUpdateSettings.checkingForUpdate);
export const deviceState = (state: AppState) => state.device;
export const isDeviceConnected = createSelector(deviceState, fromDevice.isDeviceConnected);
export const deviceConnected = createSelector(runningInElectron, isDeviceConnected, (electron, connected) => {
return !electron ? true : connected;
});
export const devicePermission = createSelector(deviceState, fromDevice.hasDevicePermission);
export const hasDevicePermission = createSelector(runningInElectron, devicePermission, (electron, permission) => {
return !electron ? true : permission;
});
export const deviceConnected = createSelector(
runningInElectron, deviceState, appState,
(electron, device, app) => {
if (!electron) {
return true;
}
if (app.platform === 'linux') {
return device.connected && (device.zeroInterfaceAvailable || device.updatingFirmware);
}
return device.connected;
});
export const hasDevicePermission = createSelector(deviceState, fromDevice.hasDevicePermission);
export const getMissingDeviceState = createSelector(deviceState, fromDevice.getMissingDeviceState);
export const saveToKeyboardStateSelector = createSelector(deviceState, fromDevice.getSaveToKeyboardState);
export const saveToKeyboardState = createSelector(runningInElectron, saveToKeyboardStateSelector, (electron, state) => {
return electron ? state : initProgressButtonState;
@@ -88,6 +95,18 @@ export const getRestoreUserConfiguration = createSelector(deviceState, fromDevic
export const bootloaderActive = createSelector(deviceState, fromDevice.bootloaderActive);
export const firmwareUpgradeFailed = createSelector(deviceState, fromDevice.firmwareUpgradeFailed);
export const firmwareUpgradeSuccess = createSelector(deviceState, fromDevice.firmwareUpgradeSuccess);
export const getUpdateUdevRules = createSelector(deviceState, fromDevice.updateUdevRules);
export const getPrivilegePageState = createSelector(appState, getUpdateUdevRules, (app, updateUdevRules): PrivilagePageSate => {
const permissionSetupFailed = !!app.permissionError;
return {
permissionSetupFailed,
updateUdevRules,
showWhatWillThisDo: !app.privilegeWhatWillThisDoClicked && !permissionSetupFailed,
showWhatWillThisDoContent: app.privilegeWhatWillThisDoClicked || permissionSetupFailed
};
});
export const getSideMenuPageState = createSelector(
showAddonMenu,

View File

@@ -183,15 +183,6 @@ export const getKeyboardLayout = (state: State): KeyboardLayout => {
};
export const deviceConfigurationLoaded = (state: State) => !state.runningInElectron ? true : !!state.hardwareConfig;
export const getAgentVersionInfo = (state: State) => state.agentVersionInfo || {} as VersionInformation;
export const getPrivilagePageState = (state: State): PrivilagePageSate => {
const permissionSetupFailed = !!state.permissionError;
return {
permissionSetupFailed,
showWhatWillThisDo: !state.privilegeWhatWillThisDoClicked && !permissionSetupFailed,
showWhatWillThisDoContent: state.privilegeWhatWillThisDoClicked || permissionSetupFailed
};
};
export const runningOnNotSupportedWindows = (state: State): boolean => {
if (!state.osVersion || state.platform !== 'win32') {

View File

@@ -1,12 +1,12 @@
import { Action } from '@ngrx/store';
import { HardwareModules } from 'uhk-common';
import { HardwareModules, UdevRulesInfo } from 'uhk-common';
import {
ActionTypes,
ConnectionStateChangedAction,
HardwareModulesLoadedAction,
SaveConfigurationAction,
HasBackupUserConfigurationAction,
SaveConfigurationAction,
UpdateFirmwareFailedAction,
UpdateFirmwareSuccessAction
} from '../actions/device';
@@ -14,11 +14,14 @@ import { ActionTypes as AppActions, ElectronMainLogReceivedAction } from '../act
import { initProgressButtonState, ProgressButtonState } from './progress-button-state';
import { XtermCssClass, XtermLog } from '../../models/xterm-log';
import { RestoreConfigurationState } from '../../models/restore-configuration-state';
import { MissingDeviceState } from '../../models/missing-device-state';
export interface State {
connected: boolean;
hasPermission: boolean;
bootloaderActive: boolean;
zeroInterfaceAvailable: boolean;
udevRuleInfo: UdevRulesInfo;
saveToKeyboard: ProgressButtonState;
savingToKeyboard: boolean;
updatingFirmware: boolean;
@@ -35,6 +38,8 @@ export const initialState: State = {
connected: true,
hasPermission: true,
bootloaderActive: false,
zeroInterfaceAvailable: true,
udevRuleInfo: UdevRulesInfo.Unkonwn,
saveToKeyboard: initProgressButtonState,
savingToKeyboard: false,
updatingFirmware: false,
@@ -61,7 +66,9 @@ export function reducer(state = initialState, action: Action): State {
...state,
connected: data.connected,
hasPermission: data.hasPermission,
bootloaderActive: data.bootloaderActive
zeroInterfaceAvailable: data.zeroInterfaceAvailable,
bootloaderActive: data.bootloaderActive,
udevRuleInfo: data.udevRulesInfo
};
}
@@ -222,7 +229,20 @@ export function reducer(state = initialState, action: Action): State {
export const updatingFirmware = (state: State) => state.updatingFirmware;
export const isDeviceConnected = (state: State) => state.connected || state.updatingFirmware;
export const hasDevicePermission = (state: State) => state.hasPermission;
export const hasDevicePermission = (state: State) => state.udevRuleInfo === UdevRulesInfo.Ok;
export const getMissingDeviceState = (state: State): MissingDeviceState => {
if (state.connected && !state.zeroInterfaceAvailable) {
return {
header: 'Cannot find your UHK',
subtitle: 'Please reconnect it!'
};
}
return {
header: 'Cannot find your UHK',
subtitle: 'Please plug it in!'
};
};
export const getSaveToKeyboardState = (state: State) => state.saveToKeyboard;
export const xtermLog = (state: State) => state.log;
export const getHardwareModules = (state: State) => state.modules;
@@ -236,3 +256,4 @@ export const getBackupUserConfigurationState = (state: State): RestoreConfigurat
export const bootloaderActive = (state: State) => state.bootloaderActive;
export const firmwareUpgradeFailed = (state: State) => state.firmwareUpdateFailed;
export const firmwareUpgradeSuccess = (state: State) => state.firmwareUpdateSuccess;
export const updateUdevRules = (state: State) => state.udevRuleInfo === UdevRulesInfo.Different;