Skip to content
This repository was archived by the owner on Sep 20, 2023. It is now read-only.

Commit bc9d2a7

Browse files
committed
added new terminal commands (#67):
credits, dl, shutdown,ping fixed some missing type warnings added check for UUID args
1 parent cf3cc5d commit bc9d2a7

File tree

10 files changed

+189
-29
lines changed

10 files changed

+189
-29
lines changed
Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,64 @@
11
import { SafeHtml } from '@angular/platform-browser';
2+
import {Device} from 'src/app/api/devices/device';
23

34
export interface TerminalAPI {
45
/**
56
* Outputs html to the terminal (followed by a line break)
67
* @param html HTML string
78
*/
8-
output(html: string);
9+
output(html: string): void;
910

1011
/**
1112
* Outputs html without a line break to the terminal
1213
* @param html HTML string
1314
*/
14-
outputRaw(html: string);
15+
outputRaw(html: string): void;
1516

1617
/**
1718
* Outputs text to the terminal
1819
* @param text Raw text
1920
*/
20-
outputText(text: string);
21+
outputText(text: string): void;
2122

2223
/**
2324
* Outputs a html node to the terminal
2425
* @param node `Node`
2526
*/
26-
outputNode(node: Node);
27+
outputNode(node: Node): void;
2728

2829
/**
2930
* Closes the terminal window
3031
*/
31-
closeTerminal();
32+
closeTerminal(): void;
3233

3334
/**
3435
* Clears the complete terminal
3536
*/
36-
clear();
37+
clear(): void;
3738

38-
changePrompt(prompt: string | SafeHtml, trust?: boolean);
39+
changePrompt(prompt: string | SafeHtml, trust?: boolean): void;
3940

40-
pushState(state: TerminalState);
41+
pushState(state: TerminalState): void;
4142

4243
popState(): TerminalState;
44+
45+
/**
46+
* Shutdowns the current device
47+
*
48+
* @returns: if the shutdown was successful
49+
*/
50+
shutdown(): Promise<boolean>;
51+
52+
getOwnerDevice(): Device;
4353
}
4454

4555

4656
export interface TerminalState {
47-
execute(command: string);
57+
execute(command: string): void;
4858

4959
autocomplete(content: string): Promise<string>;
5060

5161
getHistory(): string[];
5262

53-
refreshPrompt();
63+
refreshPrompt(): void;
5464
}

src/app/desktop/windows/terminal/terminal-states.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {WindowDelegate} from '../../window/window-delegate';
1212
import {File} from '../../../api/files/file';
1313
import {Shell} from 'src/app/shell/shell';
1414
import {ShellApi} from 'src/app/shell/shellapi';
15+
import {DeviceService} from 'src/app/api/devices/device.service';
1516

1617

1718
function escapeHtml(html: string): string {
@@ -331,8 +332,8 @@ export class DefaultTerminalState extends CommandTerminalState {
331332
working_dir: string = Path.ROOT; // UUID of the working directory
332333

333334
constructor(protected websocket: WebsocketService, private settings: SettingsService, private fileService: FileService,
334-
private domSanitizer: DomSanitizer, protected windowDelegate: WindowDelegate, protected activeDevice: Device,
335-
protected terminal: TerminalAPI, public promptColor: string = null) {
335+
private deviceService: DeviceService, private domSanitizer: DomSanitizer, protected windowDelegate: WindowDelegate,
336+
protected activeDevice: Device, protected terminal: TerminalAPI, public promptColor: string = null) {
336337
super();
337338
}
338339

@@ -1454,8 +1455,8 @@ export class DefaultTerminalState extends CommandTerminalState {
14541455
this.websocket.ms('device', ['device', 'info'], {device_uuid: args[0]}).subscribe(infoData => {
14551456
this.websocket.ms('service', ['part_owner'], {device_uuid: args[0]}).subscribe(partOwnerData => {
14561457
if (infoData['owner'] === this.websocket.account.uuid || partOwnerData['ok'] === true) {
1457-
this.terminal.pushState(new DefaultTerminalState(this.websocket, this.settings, this.fileService, this.domSanitizer,
1458-
this.windowDelegate, infoData, this.terminal, '#DD2C00'));
1458+
this.terminal.pushState(new DefaultTerminalState(this.websocket, this.settings, this.fileService, this.deviceService,
1459+
this.domSanitizer, this.windowDelegate, infoData, this.terminal, '#DD2C00'));
14591460
this.setExitCode(0);
14601461
} else {
14611462
iohandler.stderr('Access denied');
@@ -1957,7 +1958,7 @@ export class DefaultTerminalState extends CommandTerminalState {
19571958
msh(_: IOHandler) {
19581959
this.terminal.pushState(
19591960
new ShellTerminal(
1960-
this.websocket, this.settings, this.fileService,
1961+
this.websocket, this.settings, this.fileService, this.deviceService,
19611962
this.domSanitizer, this.windowDelegate, this.activeDevice,
19621963
this.terminal, this.promptColor
19631964
)
@@ -2165,11 +2166,11 @@ class ShellTerminal implements TerminalState {
21652166
private shell: Shell;
21662167

21672168
constructor(private websocket: WebsocketService, private settings: SettingsService, private fileService: FileService,
2168-
private domSanitizer: DomSanitizer, windowDelegate: WindowDelegate, private activeDevice: Device,
2169-
private terminal: TerminalAPI, private promptColor: string = null
2169+
private deviceService: DeviceService, private domSanitizer: DomSanitizer, windowDelegate: WindowDelegate,
2170+
private activeDevice: Device, private terminal: TerminalAPI, private promptColor: string = null
21702171
) {
21712172
const shellApi = new ShellApi(
2172-
this.websocket, this.settings, this.fileService,
2173+
this.websocket, this.settings, this.fileService, this.deviceService,
21732174
this.domSanitizer, windowDelegate, this.activeDevice,
21742175
terminal, this.promptColor, this.refreshPrompt.bind(this),
21752176
Path.ROOT

src/app/desktop/windows/terminal/terminal.component.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@ import {TerminalAPI, TerminalState} from './terminal-api';
55
import {DefaultTerminalState} from './terminal-states';
66
import {WebsocketService} from '../../../websocket.service';
77
import {DomSanitizer, SafeHtml} from '@angular/platform-browser';
8+
import {DeviceService} from '../../../api/devices/device.service';
89
import {FileService} from '../../../api/files/file.service';
910
import {WindowManager} from '../../window-manager/window-manager';
11+
import {Router} from '@angular/router';
12+
import {Device} from 'src/app/api/devices/device';
1013

1114
// noinspection AngularMissingOrInvalidDeclarationInModule
1215
@Component({
@@ -28,8 +31,10 @@ export class TerminalComponent extends WindowComponent implements OnInit, Termin
2831
private websocket: WebsocketService,
2932
private settings: SettingsService,
3033
private fileService: FileService,
34+
private deviceService: DeviceService,
3135
private windowManager: WindowManager,
32-
private domSanitizer: DomSanitizer
36+
private domSanitizer: DomSanitizer,
37+
private router: Router,
3338
) {
3439
super();
3540
}
@@ -40,6 +45,7 @@ export class TerminalComponent extends WindowComponent implements OnInit, Termin
4045
this.websocket,
4146
this.settings,
4247
this.fileService,
48+
this.deviceService,
4349
this.domSanitizer,
4450
this.delegate,
4551
this.delegate.device,
@@ -168,6 +174,20 @@ export class TerminalComponent extends WindowComponent implements OnInit, Termin
168174
clear() {
169175
this.history.nativeElement.value = '';
170176
}
177+
178+
async shutdown(): Promise<boolean> {
179+
const uuid = this.delegate.device['uuid'];
180+
try {
181+
await this.deviceService.togglePower(uuid).toPromise();
182+
} catch {
183+
return false;
184+
}
185+
return await this.router.navigate(['device'], {queryParams: {device: uuid}});
186+
}
187+
188+
getOwnerDevice(): Device {
189+
return this.delegate.device;
190+
}
171191
}
172192

173193
export class TerminalWindowDelegate extends WindowDelegate {

src/app/shell/builtins/builtins.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import {Cd} from './cd';
55
import {Ls} from './ls';
66
import {Clear} from './clear';
77
import {Exit} from './exit';
8+
import {Dl} from './dl';
9+
import {Shutdown} from './shutdown';
10+
import {Ping} from './ping';
11+
import {Credits} from './credits';
812

9-
export const BUILTINS = [Status, Hostname, Miner, Cd, Ls, Clear, Exit];
13+
export const BUILTINS = [Status, Hostname, Miner, Cd, Ls, Clear, Exit, Dl, Shutdown, Ping, Credits];
1014

src/app/shell/builtins/credits.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import {Command, IOHandler} from '../command';
2+
import {ShellApi} from '../shellapi';
3+
4+
export class Credits extends Command {
5+
constructor(shellApi: ShellApi) {
6+
super('credits', shellApi);
7+
this.addDescription('list all contributors');
8+
}
9+
10+
async run(iohandler: IOHandler): Promise<number> {
11+
const data = await fetch('https://api.admin.staging.cryptic-game.net/website/team');
12+
const members = JSON.parse(await data.text()).sort(() => Math.random() - 0.5);
13+
members.forEach((member: any) => {
14+
iohandler.stdout(member.name);
15+
});
16+
return 0;
17+
}
18+
}

src/app/shell/builtins/dl.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import {Command, IOHandler, ArgType} from '../command';
2+
import {ShellApi} from '../shellapi';
3+
import {Path} from 'src/app/api/files/path';
4+
import {File} from 'src/app/api/files/file';
5+
6+
export class Dl extends Command {
7+
constructor(shellApi: ShellApi) {
8+
super('dl', shellApi);
9+
this.addDescription('download a file to your own device');
10+
this.addPositionalArgument({name: 'source', argType: ArgType.FILE});
11+
this.addPositionalArgument({name: 'destination'});
12+
}
13+
14+
async run(iohandler: IOHandler): Promise<number> {
15+
let srcFile: File;
16+
let dstPath: Path;
17+
const ownerUuid = this.shellApi.terminal.getOwnerDevice()['uuid'];
18+
try {
19+
const srcPath = Path.fromString(iohandler.positionalArgs[0], this.shellApi.working_dir);
20+
srcFile = await this.shellApi.fileService.getFromPath(this.shellApi.activeDevice['uuid'], srcPath).toPromise();
21+
} catch {
22+
iohandler.stderr('The source file was not found');
23+
return 1;
24+
}
25+
if (srcFile.is_directory) {
26+
iohandler.stderr('Cannot download a directory');
27+
return 1;
28+
}
29+
try {
30+
dstPath = Path.fromString(iohandler.positionalArgs[1], this.shellApi.working_dir);
31+
} catch {
32+
iohandler.stderr('The specified destination path is not valid');
33+
return 1;
34+
}
35+
36+
try {
37+
await this.shellApi.fileService.getFromPath(ownerUuid, dstPath).toPromise();
38+
iohandler.stderr('That file already exists');
39+
return 1;
40+
} catch {}
41+
42+
const dstFileName = dstPath.path[dstPath.path.length - 1];
43+
try {
44+
await this.shellApi.fileService.createFile(ownerUuid, dstFileName, srcFile.content, dstPath.parentUUID).toPromise();
45+
return 0;
46+
} catch {
47+
iohandler.stderr('Could not create file');
48+
return 1;
49+
}
50+
}
51+
}

src/app/shell/builtins/ping.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import {Command, IOHandler, ArgType} from '../command';
2+
import {ShellApi} from '../shellapi';
3+
4+
export class Ping extends Command {
5+
constructor(shellApi: ShellApi) {
6+
super('ping', shellApi);
7+
this.addDescription('ping a device');
8+
this.addPositionalArgument({name: 'uuid', argType: ArgType.UUID});
9+
}
10+
11+
async run(iohandler: IOHandler): Promise<number> {
12+
const uuid = iohandler.positionalArgs[0];
13+
try {
14+
const status = await this.shellApi.deviceService.getDeviceState(uuid).toPromise();
15+
iohandler.stdout(`Device is ${status.online ? '' : 'not '}online`);
16+
return 0;
17+
} catch {
18+
iohandler.stderr('Device not found');
19+
return 1;
20+
}
21+
}
22+
}

src/app/shell/builtins/shutdown.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import {Command, IOHandler} from '../command';
2+
import {ShellApi} from '../shellapi';
3+
4+
export class Shutdown extends Command {
5+
constructor(shellApi: ShellApi) {
6+
super('shutdown', shellApi);
7+
this.addDescription('shutdown your own device');
8+
}
9+
10+
async run(_: IOHandler): Promise<number> {
11+
return await this.shellApi.terminal.shutdown() ? 0 : 1;
12+
}
13+
}

src/app/shell/command.ts

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,17 @@ import {ShellApi} from './shellapi';
33
export enum ArgType {
44
RAW, // just a String
55
PATH, // FILE or DIRECTORY
6-
FILE, // only FILE
7-
DIRECTORY // only DIRECTORY
6+
FILE, // only file
7+
DIRECTORY, // only directory
8+
UUID
9+
}
10+
11+
export function checkArgType(argType: ArgType, arg: string): boolean {
12+
if (argType === ArgType.UUID) {
13+
return arg.match(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/) !== null;
14+
} else {
15+
return true;
16+
}
817
}
918

1019
export interface PositionalArgument {
@@ -60,8 +69,9 @@ export abstract class Command {
6069
if (this.subcommands.size > 0) {
6170
stdout('subcommands:');
6271
this.subcommands.forEach((subcommand: Command, name: string) => {
63-
// TODO use \t
64-
// TODO align the descriptions
72+
// TODO:
73+
// use \t
74+
// align the descriptions
6575
stdout(` ${name} - ${subcommand.description}`);
6676
});
6777
}
@@ -82,14 +92,23 @@ export abstract class Command {
8292
this.showHelp(iohandler.stdout);
8393
return 0;
8494
}
95+
8596
const posArgsLen = this.positionalArgs.length;
86-
if (posArgsLen < args.length && this.capturesAllArgs
87-
|| args.length <= posArgsLen && args.length >= posArgsLen - this.optionalArgs
97+
if (!(posArgsLen < args.length && this.capturesAllArgs
98+
|| args.length <= posArgsLen && args.length >= posArgsLen - this.optionalArgs)
8899
) {
89-
return await this.run(iohandler);
100+
this.showHelp(iohandler.stdout);
101+
return 1;
102+
}
103+
// check all args for validity
104+
for (let i = 0; i < args.length; i++) {
105+
const arg = i >= posArgsLen ? this.positionalArgs[posArgsLen - 1] : this.positionalArgs[i];
106+
if (!checkArgType(arg.argType, args[i])) {
107+
iohandler.stderr(`Arg "${args[i]}" is invalid`);
108+
return 1;
109+
}
90110
}
91-
this.showHelp(iohandler.stdout);
92-
return 1;
111+
return await this.run(iohandler);
93112
}
94113

95114

src/app/shell/shellapi.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ import {FileService} from '../api/files/file.service';
66
import {Device} from '../api/devices/device';
77
import {WindowDelegate} from '../desktop/window/window-delegate';
88
import {File} from 'src/app/api/files/file';
9+
import {DeviceService} from '../api/devices/device.service';
910

1011

1112
export class ShellApi {
1213
constructor(
1314
public websocket: WebsocketService,
1415
public settings: SettingsService,
1516
public fileService: FileService,
17+
public deviceService: DeviceService,
1618
public domSanitizer: DomSanitizer,
1719
public windowDelegate: WindowDelegate,
1820
public activeDevice: Device,

0 commit comments

Comments
 (0)