4:43 AM commit

back:
- fix broken deps
- add connection check to panel
- pretty-print panel errors
- touch timestamps of updates
- simplify u_server api
- simplify u_lib api

front:
- add routing
- use async instead of rxjs
- define tables' columns in templates, not in ts
- use different templates for different tables
pull/1/head
plazmoid 2 years ago
parent 7b59031bfe
commit 544c07cf8d
  1. 3
      Cargo.toml
  2. 4
      bin/u_panel/src/argparse.rs
  3. 11
      bin/u_panel/src/server/fe/src/app/app-routing.module.ts
  4. 17
      bin/u_panel/src/server/fe/src/app/app.component.html
  5. 36
      bin/u_panel/src/server/fe/src/app/core/services/api.service.ts
  6. 68
      bin/u_panel/src/server/fe/src/app/core/tables/agent.component.html
  7. 83
      bin/u_panel/src/server/fe/src/app/core/tables/agent.component.ts
  8. 3
      bin/u_panel/src/server/fe/src/app/core/tables/dialogs/agent-info-dialog.html
  9. 68
      bin/u_panel/src/server/fe/src/app/core/tables/job.component.html
  10. 39
      bin/u_panel/src/server/fe/src/app/core/tables/job.component.ts
  11. 68
      bin/u_panel/src/server/fe/src/app/core/tables/result.component.html
  12. 47
      bin/u_panel/src/server/fe/src/app/core/tables/result.component.ts
  13. 37
      bin/u_panel/src/server/fe/src/app/core/tables/table.component.html
  14. 3
      bin/u_panel/src/server/fe/src/app/core/tables/table.component.less
  15. 44
      bin/u_panel/src/server/fe/src/app/core/tables/table.component.ts
  16. 4
      bin/u_panel/src/server/fe/src/app/core/utils.ts
  17. 10
      bin/u_panel/src/server/mod.rs
  18. 2
      bin/u_server/Cargo.toml
  19. 11
      bin/u_server/src/db.rs
  20. 21
      bin/u_server/src/handlers.rs
  21. 17
      bin/u_server/src/u_server.rs
  22. 5
      integration/tests/fixtures/agent.rs
  23. 2
      lib/u_lib/Cargo.toml
  24. 50
      lib/u_lib/src/api.rs
  25. 1
      lib/u_lib/src/config.rs
  26. 1
      lib/u_lib/src/logging.rs
  27. 5
      lib/u_lib/src/messaging/mod.rs
  28. 4
      lib/u_lib/src/models/agent.rs
  29. 4
      lib/u_lib/src/models/jobs/assigned.rs
  30. 33
      lib/u_lib/src/runner.rs

@ -9,10 +9,11 @@ members = [
] ]
[workspace.dependencies] [workspace.dependencies]
anyhow = "1.0.58" anyhow = "=1.0.63"
reqwest = { version = "0.11", features = ["json"] } reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
thiserror = "=1.0.31"
tokio = { version = "1.11", features = ["macros"] } tokio = { version = "1.11", features = ["macros"] }
tracing = "0.1.35" tracing = "0.1.35"
tracing-appender = "0.2.0" tracing-appender = "0.2.0"

@ -20,6 +20,7 @@ enum Cmd {
Agents(RUD), Agents(RUD),
Jobs(JobCRUD), Jobs(JobCRUD),
Map(JobMapCRUD), Map(JobMapCRUD),
Ping,
//TUI(TUIArgs), //TUI(TUIArgs),
Serve, Serve,
} }
@ -119,13 +120,14 @@ pub async fn process_cmd(client: ClientHandler, args: Args) -> UResult<String> {
} }
JobMapCRUD::RUD(RUD::Delete { uid }) => to_json(client.del(uid).await), JobMapCRUD::RUD(RUD::Delete { uid }) => to_json(client.del(uid).await),
}, },
Cmd::Ping => to_json(client.ping().await),
/*Cmd::TUI(args) => crate::tui::init_tui(&args) /*Cmd::TUI(args) => crate::tui::init_tui(&args)
.await .await
.map_err(|e| UError::PanelError(e.to_string()))?,*/ .map_err(|e| UError::PanelError(e.to_string()))?,*/
Cmd::Serve => { Cmd::Serve => {
crate::server::serve(client) crate::server::serve(client)
.await .await
.map_err(|e| UError::PanelError(e.to_string()))?; .map_err(|e| UError::PanelError(format!("{e:?}")))?;
String::new() String::new()
} }
}) })

@ -1,7 +1,16 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { AgentComponent } from './core/tables/agent.component';
import { JobComponent } from './core/tables/job.component';
import { ResultComponent } from './core/tables/result.component';
import { AgentInfoDialogComponent } from './core/tables/dialogs/agent_info.component';
const routes: Routes = []; const routes: Routes = [
{ path: '', redirectTo: 'agents', pathMatch: 'full' },
{ path: 'agents', component: AgentComponent },
{ path: 'jobs', component: JobComponent },
{ path: 'results', component: ResultComponent },
];
@NgModule({ @NgModule({
imports: [RouterModule.forRoot(routes)], imports: [RouterModule.forRoot(routes)],

@ -1,11 +1,6 @@
<mat-tab-group animationDuration="0ms" mat-align-tabs="center"> <nav mat-tab-nav-bar animationDuration="0ms" mat-align-tabs="center">
<mat-tab label="Agents"> <a mat-tab-link routerLink="/agents" routerLinkActive="active" ariaCurrentWhenActive="page">Agents</a>
<agent-table></agent-table> <a mat-tab-link routerLink="/jobs" routerLinkActive="active" ariaCurrentWhenActive="page">Jobs</a>
</mat-tab> <a mat-tab-link routerLink="/results" routerLinkActive="active" ariaCurrentWhenActive="page">Results</a>
<mat-tab label="Jobs"> </nav>
<job-table></job-table> <router-outlet></router-outlet>
</mat-tab>
<mat-tab label="Results">
<result-table></result-table>
</mat-tab>
</mat-tab-group>

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { environment } from 'src/environments/environment'; import { environment } from 'src/environments/environment';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs'; import { firstValueFrom } from 'rxjs';
interface ServerResponse<T> { interface ServerResponse<T> {
status: "ok" | "err", status: "ok" | "err",
@ -17,27 +17,37 @@ export class ApiTableService<T> {
requestUrl = `${environment.server}/cmd/`; requestUrl = `${environment.server}/cmd/`;
req<R>(cmd: string): Observable<ServerResponse<R>> { async req<R>(cmd: string): Promise<ServerResponse<R>> {
return this.http.post<ServerResponse<R>>(this.requestUrl, cmd); return await firstValueFrom(this.http.post<ServerResponse<R>>(this.requestUrl, cmd))
} }
getOne(id: string): Observable<ServerResponse<T>> { async getOne(id: string): Promise<ServerResponse<T>> {
return this.req(`${this.area} read ${id}`) const resp = await this.req<T[]>(`${this.area} read ${id}`)
if (resp.data.length === 0) {
return {
status: 'err',
data: `${id} not found in ${this.area}`
}
}
return {
status: resp.status,
data: resp.data[0]
}
} }
getMany(): Observable<ServerResponse<T[]>> { async getMany(): Promise<ServerResponse<T[]>> {
return this.req(`${this.area} read`) return await this.req(`${this.area} read`)
} }
update(item: T): Observable<ServerResponse<void>> { async update(item: T): Promise<ServerResponse<void>> {
return this.req(`${this.area} update '${JSON.stringify(item)}'`) return await this.req(`${this.area} update '${JSON.stringify(item)}'`)
} }
delete(id: string): Observable<ServerResponse<void>> { async delete(id: string): Promise<ServerResponse<void>> {
return this.req(`${this.area} delete ${id}`) return await this.req(`${this.area} delete ${id}`)
} }
create(item: string): Observable<ServerResponse<void>> { async create(item: string): Promise<ServerResponse<void>> {
return this.req(`${this.area} create ${item}`) return await this.req(`${this.area} create ${item}`)
} }
} }

@ -0,0 +1,68 @@
<div class="mat-elevation-z8">
<div class="table-container">
<div class="loading-shade" *ngIf="isLoadingResults">
<mat-spinner *ngIf="isLoadingResults"></mat-spinner>
</div>
<mat-form-field appearance="standard">
<mat-label>Filter</mat-label>
<input matInput (keyup)="apply_filter($event)" #input>
</mat-form-field>
<button id="refresh_btn" mat-raised-button color="primary" (click)="fetchMany()">Refresh</button>
<table mat-table [dataSource]="table_data" class="data-table" matSort matSortActive="id" matSortDisableClear
matSortDirection="desc">
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef>ID</th>
<td mat-cell *matCellDef="let row">
{{row.id}}
</td>
</ng-container>
<ng-container matColumnDef="alias">
<th mat-header-cell *matHeaderCellDef>Alias</th>
<td mat-cell *matCellDef="let row">
{{row.alias}}
</td>
</ng-container>
<ng-container matColumnDef="username">
<th mat-header-cell *matHeaderCellDef>User</th>
<td mat-cell *matCellDef="let row">
{{row.username}}
</td>
</ng-container>
<ng-container matColumnDef="hostname">
<th mat-header-cell *matHeaderCellDef>Hostname</th>
<td mat-cell *matCellDef="let row">
{{row.hostname}}
</td>
</ng-container>
<ng-container matColumnDef="last_active">
<th mat-header-cell *matHeaderCellDef>Last active</th>
<td mat-cell *matCellDef="let row">
{{row.last_active.secs_since_epoch * 1000 | date:'long'}}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let row">
<button mat-raised-button routerLink='' [queryParams]="{id: row.id}">Info</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row class="data-table-row" *matRowDef="let row; columns: displayedColumns;"></tr>
<tr class="mat-row" *matNoDataRow>
<td class="mat-cell">No data</td>
</tr>
</table>
</div>
<!-- <mat-paginator [length]="resultsLength" [pageSize]="30" aria-label="Select page of GitHub search results">
</mat-paginator> -->
</div>

@ -1,57 +1,62 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { TablesComponent } from './table.component'; import { TablesComponent } from './table.component';
import { AgentModel } from '../models'; import { AgentModel } from '../models';
import { AgentInfoDialogComponent } from './dialogs/agent_info.component'; import { AgentInfoDialogComponent } from './dialogs/agent_info.component';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { epochToStr } from '../utils'; import { epochToStr } from '../utils';
import { ActivatedRoute, Router } from '@angular/router';
import { emitErr } from '../utils';
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'agent-table', selector: 'agent-table',
templateUrl: './table.component.html', templateUrl: './agent.component.html',
styleUrls: ['./table.component.less'] styleUrls: ['./table.component.less']
}) })
export class AgentComponent extends TablesComponent<AgentModel> { export class AgentComponent extends TablesComponent<AgentModel> implements OnDestroy, OnInit {
constructor(public override _httpClient: HttpClient, public override info_dlg: MatDialog) { dialogSubscr!: Subscription;
area = 'agents' as const;
displayedColumns = ['id', 'alias', 'username', 'hostname', 'last_active', 'actions']
constructor(
public override _httpClient: HttpClient,
public override info_dlg: MatDialog,
public route: ActivatedRoute,
public router: Router
) {
super(_httpClient, info_dlg); super(_httpClient, info_dlg);
} }
area = 'agents' as const; override ngOnInit(): void {
super.ngOnInit()
this.dialogSubscr = this.route.queryParams.subscribe(params => {
const id = params['id']
if (id) {
this.show_item_dialog(id);
}
})
}
show_item_dialog(id: string) {
this.data_source!.getOne(id).then(resp => {
if (resp.status === 'ok') {
const dialog = this.info_dlg.open(AgentInfoDialogComponent, {
data: resp.data as AgentModel
});
dialog.afterClosed().subscribe(result => {
this.router.navigate(['.'], { relativeTo: this.route })
})
} else {
emitErr(resp.data)
}
}).catch(emitErr)
}
columns = [ ngOnDestroy(): void {
{ this.dialogSubscr.unsubscribe()
def: "id",
name: "ID",
cell: (cell: AgentModel) => cell.id
},
{
def: "alias",
name: "Alias",
cell: (cell: AgentModel) => cell.alias ?? ""
},
{
def: "username",
name: "User",
cell: (cell: AgentModel) => cell.username
},
{
def: "hostname",
name: "Host",
cell: (cell: AgentModel) => cell.hostname
},
{
def: "last_active",
name: "Last active",
cell: (cell: AgentModel) => epochToStr(cell.last_active.secs_since_epoch)
},
]
displayedColumns = this.columns.map((c) => c.def);
show_item_dialog(obj: AgentModel) {
const dialog = this.info_dlg.open(AgentInfoDialogComponent, {
data: obj
});
} }
} }

@ -1,9 +1,12 @@
<mat-dialog-content> <mat-dialog-content>
<div> <div>
<p>ID: {{data.id}}</p>
<p>Alias: {{data.alias}}</p> <p>Alias: {{data.alias}}</p>
<p>Username: {{data.username}}</p> <p>Username: {{data.username}}</p>
<p>Hostname: {{data.hostname}}</p> <p>Hostname: {{data.hostname}}</p>
<p>Platform: {{data.platform}}</p> <p>Platform: {{data.platform}}</p>
<p>Registration time: {{data.regtime.secs_since_epoch * 1000 | date:'long'}}</p>
<p>Last active time: {{data.last_active.secs_since_epoch * 1000 | date:'long'}}</p>
</div> </div>
</mat-dialog-content> </mat-dialog-content>
<mat-dialog-actions align="end"> <mat-dialog-actions align="end">

@ -0,0 +1,68 @@
<div class="mat-elevation-z8">
<div class="table-container">
<div class="loading-shade" *ngIf="isLoadingResults">
<mat-spinner *ngIf="isLoadingResults"></mat-spinner>
</div>
<mat-form-field appearance="standard">
<mat-label>Filter</mat-label>
<input matInput (keyup)="apply_filter($event)" #input>
</mat-form-field>
<button id="refresh_btn" mat-raised-button color="primary" (click)="fetchMany()">Refresh</button>
<table mat-table [dataSource]="table_data" class="data-table" matSort matSortActive="id" matSortDisableClear
matSortDirection="desc">
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef>ID</th>
<td mat-cell *matCellDef="let row">
{{row.id}}
</td>
</ng-container>
<ng-container matColumnDef="alias">
<th mat-header-cell *matHeaderCellDef>Alias</th>
<td mat-cell *matCellDef="let row">
{{row.alias}}
</td>
</ng-container>
<ng-container matColumnDef="argv">
<th mat-header-cell *matHeaderCellDef>Cmd-line args</th>
<td mat-cell *matCellDef="let row">
{{row.argv}}
</td>
</ng-container>
<ng-container matColumnDef="platform">
<th mat-header-cell *matHeaderCellDef>Platform</th>
<td mat-cell *matCellDef="let row">
{{row.platform}}
</td>
</ng-container>
<ng-container matColumnDef="payload">
<th mat-header-cell *matHeaderCellDef>Payload</th>
<td mat-cell *matCellDef="let row">
{{row.payload}}
</td>
</ng-container>
<ng-container matColumnDef="exec_type">
<th mat-header-cell *matHeaderCellDef>Type</th>
<td mat-cell *matCellDef="let row">
{{row.exec_type}}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row class="data-table-row" *matRowDef="let row; columns: displayedColumns;"></tr>
<tr class="mat-row" *matNoDataRow>
<td class="mat-cell">No data</td>
</tr>
</table>
</div>
<!-- <mat-paginator [length]="resultsLength" [pageSize]="30" aria-label="Select page of GitHub search results">
</mat-paginator> -->
</div>

@ -4,45 +4,12 @@ import { JobModel } from '../models';
@Component({ @Component({
selector: 'job-table', selector: 'job-table',
templateUrl: './table.component.html', templateUrl: './job.component.html',
styleUrls: ['./table.component.less'] styleUrls: ['./table.component.less']
}) })
export class JobComponent extends TablesComponent<JobModel> { export class JobComponent extends TablesComponent<JobModel> {
area = 'jobs' as const; area = 'jobs' as const;
displayedColumns = ['id', 'alias', 'argv', 'platform', 'payload', 'exec_type']
columns = [ show_item_dialog(id: string) { }
{
def: "id",
name: "ID",
cell: (cell: JobModel) => cell.id
},
{
def: "alias",
name: "Alias",
cell: (cell: JobModel) => cell.alias
},
{
def: "argv",
name: "Cmd-line args",
cell: (cell: JobModel) => cell.argv
},
{
def: "platform",
name: "Platform",
cell: (cell: JobModel) => cell.platform
},
{
def: "payload",
name: "Payload",
cell: (cell: JobModel) => `${cell.payload}`
},
{
def: "etype",
name: "Type",
cell: (cell: JobModel) => cell.exec_type
},
]
displayedColumns = this.columns.map((c) => c.def);
show_item_dialog(obj: JobModel) { }
} }

@ -0,0 +1,68 @@
<div class="mat-elevation-z8">
<div class="table-container">
<div class="loading-shade" *ngIf="isLoadingResults">
<mat-spinner *ngIf="isLoadingResults"></mat-spinner>
</div>
<mat-form-field appearance="standard">
<mat-label>Filter</mat-label>
<input matInput (keyup)="apply_filter($event)" #input>
</mat-form-field>
<button id="refresh_btn" mat-raised-button color="primary" (click)="fetchMany()">Refresh</button>
<table mat-table [dataSource]="table_data" class="data-table" matSort matSortActive="id" matSortDisableClear
matSortDirection="desc">
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef>ID</th>
<td mat-cell *matCellDef="let row">
{{row.id}}
</td>
</ng-container>
<ng-container matColumnDef="alias">
<th mat-header-cell *matHeaderCellDef>Alias</th>
<td mat-cell *matCellDef="let row">
{{row.alias}}
</td>
</ng-container>
<ng-container matColumnDef="agent_id">
<th mat-header-cell *matHeaderCellDef>Agent</th>
<td mat-cell *matCellDef="let row">
<a routerLink='/agents' [queryParams]="{id: row.agent_id}">{{row.agent_id}}</a>
</td>
</ng-container>
<ng-container matColumnDef="job_id">
<th mat-header-cell *matHeaderCellDef>Job</th>
<td mat-cell *matCellDef="let row">
{{row.job_id}}
</td>
</ng-container>
<ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef>State</th>
<td mat-cell *matCellDef="let row">
{{row.state}} {{(row.state === "Finished") ? '(' + row.retcode + ')' : ''}}
</td>
</ng-container>
<ng-container matColumnDef="last_updated">
<th mat-header-cell *matHeaderCellDef>ID</th>
<td mat-cell *matCellDef="let row">
{{row.updated.secs_since_epoch * 1000| date:'long'}}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row class="data-table-row" *matRowDef="let row; columns: displayedColumns;"></tr>
<tr class="mat-row" *matNoDataRow>
<td class="mat-cell">No data</td>
</tr>
</table>
</div>
<!-- <mat-paginator [length]="resultsLength" [pageSize]="30" aria-label="Select page of GitHub search results">
</mat-paginator> -->
</div>

@ -4,48 +4,23 @@ import { ResultModel } from '../models';
import { epochToStr } from '../utils'; import { epochToStr } from '../utils';
@Component({ @Component({
selector: 'result-table', selector: 'results-table',
templateUrl: './table.component.html', templateUrl: './result.component.html',
styleUrls: ['./table.component.less'] styleUrls: ['./table.component.less']
}) })
export class ResultComponent extends TablesComponent<ResultModel> { export class ResultComponent extends TablesComponent<ResultModel> {
area = 'map' as const; area = 'map' as const;
columns = [ displayedColumns = [
{ 'id',
def: "id", 'alias',
name: "ID", 'agent_id',
cell: (cell: ResultModel) => cell.id 'job_id',
}, 'state',
{ 'last_updated'
def: "alias", ];
name: "Alias",
cell: (cell: ResultModel) => cell.alias
},
{
def: "agent_id",
name: "Agent ID",
cell: (cell: ResultModel) => cell.agent_id
},
{
def: "job_id",
name: "Job ID",
cell: (cell: ResultModel) => cell.job_id
},
{
def: "state",
name: "State",
cell: (cell: ResultModel) => `${cell.state} `.concat((cell.state === "Finished") ? `(${cell.retcode})` : '')
},
{
def: "last_updated",
name: "Last updated",
cell: (cell: ResultModel) => epochToStr(cell.updated.secs_since_epoch)
},
]
displayedColumns = this.columns.map((c) => c.def);
show_item_dialog(obj: ResultModel) { show_item_dialog(id: string) {
} }
} }

@ -1,37 +0,0 @@
<div class="mat-elevation-z8">
<div class="table-container">
<div class="loading-shade" *ngIf="isLoadingResults">
<mat-spinner *ngIf="isLoadingResults"></mat-spinner>
</div>
<mat-form-field appearance="standard">
<mat-label>Filter</mat-label>
<input matInput (keyup)="apply_filter($event)" #input>
</mat-form-field>
<button id="refresh_btn" mat-raised-button color="primary" (click)="fetch_many()">Refresh</button>
<table mat-table [dataSource]="table_data" class="data-table" matSort matSortActive="id" matSortDisableClear
matSortDirection="desc">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.def">
<th mat-header-cell *matHeaderCellDef>
{{column.name}}
</th>
<td mat-cell *matCellDef="let row">
{{column.cell(row)}}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row class="data-table-row" *matRowDef="let row; columns: displayedColumns;"
(click)="show_item_dialog(row)"></tr>
<tr class="mat-row" *matNoDataRow>
<td class="mat-cell">No data</td>
</tr>
</table>
</div>
<!-- <mat-paginator [length]="resultsLength" [pageSize]="30" aria-label="Select page of GitHub search results">
</mat-paginator> -->
</div>

@ -12,7 +12,7 @@
left: 0; left: 0;
bottom: 56px; bottom: 56px;
right: 0; right: 0;
background: rgba(0, 0, 0, 0.15); //background: rgba(0, 0, 0, 0.15);
z-index: 1; z-index: 1;
display: flex; display: flex;
align-items: center; align-items: center;
@ -25,5 +25,4 @@
.data-table-row:hover { .data-table-row:hover {
background: whitesmoke; background: whitesmoke;
cursor: pointer;
} }

@ -9,7 +9,7 @@ import { MatDialog } from '@angular/material/dialog';
@Directive() @Directive()
export abstract class TablesComponent<T> implements OnInit { export abstract class TablesComponent<T> implements OnInit {
abstract area: "agents" | "jobs" | "map"; abstract area: "agents" | "jobs" | "map";
data_source!: ApiTableService<T> | null; data_source!: ApiTableService<T>;
table_data!: MatTableDataSource<T>; table_data!: MatTableDataSource<T>;
isLoadingResults = true; isLoadingResults = true;
@ -20,34 +20,20 @@ export abstract class TablesComponent<T> implements OnInit {
ngOnInit() { ngOnInit() {
this.data_source = new ApiTableService(this._httpClient, this.area); this.data_source = new ApiTableService(this._httpClient, this.area);
this.fetch_many(); this.fetchMany();
// If the user changes the sort order, reset back to the first page.
//this.sort.sortChange.subscribe(() => (this.paginator.pageIndex = 0));
} }
fetch_many() { async fetchMany() {
timer(0) this.isLoadingResults = true;
.pipe( //possibly needs try/catch
startWith({}), const data = await this.data_source!.getMany();
switchMap(() => { this.isLoadingResults = false;
this.isLoadingResults = true;
return this.data_source!.getMany().pipe(catchError(() => observableOf(null))); if (typeof data.data !== 'string') {
}), this.table_data.data = data.data
map(data => { } else {
this.isLoadingResults = false; alert(`Error: ${data}`)
};
if (data === null) {
return "no data returned"
}
// Only refresh the result length if there is new data. In case of rate
// limit errors, we do not want to reset the paginator to zero, as that
// would prevent users from re-triggering requests.
return data.data;
}),
)
.subscribe(data => { if (typeof data !== 'string') { this.table_data.data = data } else { alert(`Error: ${data}`) } });
} }
apply_filter(event: Event) { apply_filter(event: Event) {
@ -55,10 +41,8 @@ export abstract class TablesComponent<T> implements OnInit {
this.table_data.filter = filterValue.trim().toLowerCase(); this.table_data.filter = filterValue.trim().toLowerCase();
} }
abstract show_item_dialog(obj: T): void;
abstract columns: ColumnDef<T>[];
abstract displayedColumns: string[]; abstract displayedColumns: string[];
abstract show_item_dialog(id: string): void;
} }
type ColumnDef<C> = { type ColumnDef<C> = {

@ -1,3 +1,7 @@
export function epochToStr(epoch: number): string { export function epochToStr(epoch: number): string {
return new Date(epoch * 1000).toLocaleString('en-GB') return new Date(epoch * 1000).toLocaleString('en-GB')
} }
export function emitErr(e: any) {
alert(e)
}

@ -77,9 +77,12 @@ async fn send_cmd(
) )
} }
pub async fn serve(client: ClientHandler) -> std::io::Result<()> { pub async fn serve(client: ClientHandler) -> anyhow::Result<()> {
info!("Connecting to u_server...");
client.ping().await?;
let addr = "127.0.0.1:8080"; let addr = "127.0.0.1:8080";
info!("Serving at http://{}", addr); info!("Connected, instanciating u_panel at http://{}", addr);
HttpServer::new(move || { HttpServer::new(move || {
App::new() App::new()
@ -92,5 +95,6 @@ pub async fn serve(client: ClientHandler) -> std::io::Result<()> {
}) })
.bind(addr)? .bind(addr)?
.run() .run()
.await .await?;
Ok(())
} }

@ -12,7 +12,7 @@ once_cell = "1.7.2"
openssl = "*" openssl = "*"
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
thiserror = "*" thiserror = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
tokio = { workspace = true, features = ["macros"] } tokio = { workspace = true, features = ["macros"] }
uuid = { workspace = true, features = ["serde", "v4"] } uuid = { workspace = true, features = ["serde", "v4"] }

@ -152,13 +152,10 @@ impl UDB {
let job_requests = job_uids let job_requests = job_uids
.iter() .iter()
.map(|job_uid| { .map(|job_uid| AssignedJob {
debug!("set_jobs_for_agent: set {} for {}", job_uid, agent_uid); job_id: *job_uid,
AssignedJob { agent_id: *agent_uid,
job_id: *job_uid, ..Default::default()
agent_id: *agent_uid,
..Default::default()
}
}) })
.collect::<Vec<AssignedJob>>(); .collect::<Vec<AssignedJob>>();

@ -1,5 +1,3 @@
use std::time::SystemTime;
use crate::db::UDB; use crate::db::UDB;
use crate::error::Error; use crate::error::Error;
use u_lib::{ use u_lib::{
@ -33,19 +31,22 @@ impl Endpoints {
.map_err(From::from) .map_err(From::from)
} }
pub async fn get_personal_jobs(uid: Option<Uuid>) -> EndpResult<Vec<AssignedJob>> { pub async fn get_personal_jobs(uid: Uuid) -> EndpResult<Vec<AssignedJob>> {
let agents = UDB::lock_db().get_agents(uid)?; let db = UDB::lock_db();
let mut agents = db.get_agents(Some(uid))?;
if agents.is_empty() { if agents.is_empty() {
let db = UDB::lock_db(); db.insert_agent(&Agent::with_id(uid))?;
db.insert_agent(&Agent::with_id(uid.unwrap()))?;
let job = db let job = db
.find_job_by_alias("agent_hello")? .find_job_by_alias("agent_hello")?
.expect("agent_hello job not found"); .expect("agent_hello job not found");
db.set_jobs_for_agent(&uid.unwrap(), &[job.id])?; db.set_jobs_for_agent(&uid, &[job.id])?;
} else {
let mut agent = agents.pop().unwrap();
agent.touch();
db.update_agent(&agent)?;
} }
let result = UDB::lock_db().get_exact_jobs(uid, true)?; let result = db.get_exact_jobs(Some(uid), true)?;
let db = UDB::lock_db();
for j in result.iter() { for j in result.iter() {
db.update_job_status(j.id, JobState::Running)?; db.update_job_status(j.id, JobState::Running)?;
} }
@ -106,7 +107,7 @@ impl Endpoints {
continue; continue;
} }
result.state = JobState::Finished; result.state = JobState::Finished;
result.updated = SystemTime::now(); result.touch();
match result.exec_type { match result.exec_type {
JobType::Init => match &result.result { JobType::Init => match &result.result {
Some(rbytes) => { Some(rbytes) => {

@ -91,7 +91,7 @@ pub fn init_endpoints(
.map(into_message); .map(into_message);
let get_personal_jobs = path("get_personal_jobs") let get_personal_jobs = path("get_personal_jobs")
.and(warp::path::param::<Uuid>().map(Some)) .and(warp::path::param::<Uuid>())
.and_then(Endpoints::get_personal_jobs) .and_then(Endpoints::get_personal_jobs)
.map(into_message); .map(into_message);
@ -131,6 +131,8 @@ pub fn init_endpoints(
.and_then(Endpoints::download) .and_then(Endpoints::download)
.map(ok); .map(ok);
let ping = path("ping").map(reply);
let auth_token = format!("Bearer {auth_token}",).into_boxed_str(); let auth_token = format!("Bearer {auth_token}",).into_boxed_str();
let auth_header = warp::header::exact("authorization", Box::leak(auth_token)); let auth_header = warp::header::exact("authorization", Box::leak(auth_token));
@ -143,7 +145,8 @@ pub fn init_endpoints(
.or(update_agent) .or(update_agent)
.or(update_job) .or(update_job)
.or(update_assigned_job) .or(update_assigned_job)
.or(download)) .or(download)
.or(ping))
.and(auth_header); .and(auth_header);
let agent_zone = get_jobs.or(get_personal_jobs).or(report).or(download); let agent_zone = get_jobs.or(get_personal_jobs).or(report).or(download);
@ -151,7 +154,7 @@ pub fn init_endpoints(
auth_zone.or(agent_zone) auth_zone.or(agent_zone)
} }
pub fn prefill_jobs() -> Result<(), ServerError> { pub fn preload_jobs() -> Result<(), ServerError> {
let job_alias = "agent_hello"; let job_alias = "agent_hello";
let if_job_exists = UDB::lock_db().find_job_by_alias(job_alias)?; let if_job_exists = UDB::lock_db().find_job_by_alias(job_alias)?;
if if_job_exists.is_none() { if if_job_exists.is_none() {
@ -167,7 +170,7 @@ pub fn prefill_jobs() -> Result<(), ServerError> {
pub async fn serve() -> Result<(), ServerError> { pub async fn serve() -> Result<(), ServerError> {
init_logger(Some("u_server")); init_logger(Some("u_server"));
prefill_jobs()?; preload_jobs()?;
let certs_dir = PathBuf::from("certs"); let certs_dir = PathBuf::from("certs");
let env = load_env::<ServEnv>().map_err(|e| ServerError::Other(e.to_string()))?; let env = load_env::<ServEnv>().map_err(|e| ServerError::Other(e.to_string()))?;
@ -192,6 +195,7 @@ async fn handle_rejection(rej: Rejection) -> Result<Response, Infallible> {
} else if rej.is_not_found() { } else if rej.is_not_found() {
RejResponse::not_found("not found placeholder") RejResponse::not_found("not found placeholder")
} else { } else {
error!("{:?}", rej);
RejResponse::internal() RejResponse::internal()
}; };
Ok(resp.into_response()) Ok(resp.into_response())
@ -199,7 +203,7 @@ async fn handle_rejection(rej: Rejection) -> Result<Response, Infallible> {
fn logger(info: Info<'_>) { fn logger(info: Info<'_>) {
info!(target: "warp", info!(target: "warp",
"{raddr} {agent_uid} \"{path}\"", "{raddr} {agent_uid} \"{path}\" {status}",
raddr = info.remote_addr().unwrap_or(([0, 0, 0, 0], 0).into()), raddr = info.remote_addr().unwrap_or(([0, 0, 0, 0], 0).into()),
path = info.path(), path = info.path(),
agent_uid = info.user_agent() agent_uid = info.user_agent()
@ -207,7 +211,8 @@ fn logger(info: Info<'_>) {
.take(2) .take(2)
.collect::<String>() .collect::<String>()
) )
.unwrap_or_else(|| "NO_AGENT".to_string()) .unwrap_or_else(|| "NO_AGENT".to_string()),
status = info.status()
); );
} }

@ -29,10 +29,7 @@ pub async fn register_agent() -> RegisteredAgent {
assert_eq!(job.alias, Some("agent_hello".to_string())); assert_eq!(job.alias, Some("agent_hello".to_string()));
let mut agent_data = AssignedJob::from(&job); let mut agent_data = AssignedJob::from(&job);
agent_data.agent_id = agent_uid; agent_data.agent_id = agent_uid;
agent_data.set_result(&Agent { agent_data.set_result(&Agent::with_id(agent_uid));
id: agent_uid,
..Default::default()
});
cli.report(Reportable::Assigned(agent_data)).await.unwrap(); cli.report(Reportable::Assigned(agent_data)).await.unwrap();
RegisteredAgent { uid: agent_uid } RegisteredAgent { uid: agent_uid }
} }

@ -26,7 +26,7 @@ shlex = "1.0.0"
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
strum = { version = "0.20", features = ["derive"] } strum = { version = "0.20", features = ["derive"] }
thiserror = "*" thiserror = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "sync", "macros", "process", "time"] } tokio = { workspace = true, features = ["rt-multi-thread", "sync", "macros", "process", "time"] }
tracing-appender = { workspace = true } tracing-appender = { workspace = true }
tracing-subscriber = { workspace = true, features = ["env-filter"] } tracing-subscriber = { workspace = true, features = ["env-filter"] }

@ -3,7 +3,7 @@ use std::fmt::Debug;
use crate::{ use crate::{
config::{get_self_uid, MASTER_PORT}, config::{get_self_uid, MASTER_PORT},
messaging::{self, AsMsg, BaseMessage, Empty}, messaging::{self, AsMsg, BaseMessage},
models::{self}, models::{self},
utils::{opt_to_string, OneOrVec}, utils::{opt_to_string, OneOrVec},
UError, UError,
@ -48,17 +48,24 @@ impl ClientHandler {
} }
} }
async fn _req<P: AsMsg, M: AsMsg + DeserializeOwned + Default>( async fn req<R: AsMsg + DeserializeOwned + Default>(&self, url: impl AsRef<str>) -> Result<R> {
self.req_with_payload(url, ()).await
}
async fn req_with_payload<P: AsMsg, R: AsMsg + DeserializeOwned + Default>(
&self, &self,
url: impl AsRef<str>, url: impl AsRef<str>,
payload: P, payload: P,
) -> Result<M> { ) -> Result<R> {
let request = self let request = self
.client .client
.post(self.base_url.join(url.as_ref()).unwrap()) .post(self.base_url.join(url.as_ref()).unwrap())
.json(&payload.as_message()); .json(&payload.as_message());
let response = request.send().await.context("send")?; let response = request
.send()
.await
.context("error while sending request")?;
let content_len = response.content_length(); let content_len = response.content_length();
let is_success = match response.error_for_status_ref() { let is_success = match response.error_for_status_ref() {
Ok(_) => Ok(()), Ok(_) => Ok(()),
@ -67,7 +74,7 @@ impl ClientHandler {
let resp = response.text().await.context("resp")?; let resp = response.text().await.context("resp")?;
debug!("url = {}, resp = {}", url.as_ref(), resp); debug!("url = {}, resp = {}", url.as_ref(), resp);
match is_success { match is_success {
Ok(_) => from_str::<BaseMessage<M>>(&resp) Ok(_) => from_str::<BaseMessage<R>>(&resp)
.map(|msg| msg.into_inner()) .map(|msg| msg.into_inner())
.or_else(|e| match content_len { .or_else(|e| match content_len {
Some(0) => Ok(Default::default()), Some(0) => Ok(Default::default()),
@ -81,24 +88,22 @@ impl ClientHandler {
// get jobs for client // get jobs for client
pub async fn get_personal_jobs(&self, url_param: Uuid) -> Result<Vec<models::AssignedJobById>> { pub async fn get_personal_jobs(&self, url_param: Uuid) -> Result<Vec<models::AssignedJobById>> {
self._req(format!("get_personal_jobs/{}", url_param), Empty) self.req(format!("get_personal_jobs/{}", url_param)).await
.await
} }
// send something to server // send something to server
pub async fn report(&self, payload: impl OneOrVec<messaging::Reportable>) -> Result<Empty> { pub async fn report(&self, payload: impl OneOrVec<messaging::Reportable>) -> Result<()> {
self._req("report", payload.into_vec()).await self.req_with_payload("report", payload.into_vec()).await
} }
// download file // download file
pub async fn dl(&self, file: String) -> Result<Vec<u8>> { pub async fn dl(&self, file: String) -> Result<Vec<u8>> {
self._req(format!("dl/{file}"), Empty).await self.req(format!("dl/{file}")).await
} }
/// get all available jobs /// get all available jobs
pub async fn get_jobs(&self, job: Option<Uuid>) -> Result<Vec<models::JobMeta>> { pub async fn get_jobs(&self, job: Option<Uuid>) -> Result<Vec<models::JobMeta>> {
self._req(format!("get_jobs/{}", opt_to_string(job)), Empty) self.req(format!("get_jobs/{}", opt_to_string(job))).await
.await
} }
} }
@ -107,23 +112,24 @@ impl ClientHandler {
impl ClientHandler { impl ClientHandler {
/// agent listing /// agent listing
pub async fn get_agents(&self, agent: Option<Uuid>) -> Result<Vec<models::Agent>> { pub async fn get_agents(&self, agent: Option<Uuid>) -> Result<Vec<models::Agent>> {
self._req(format!("get_agents/{}", opt_to_string(agent)), Empty) self.req(format!("get_agents/{}", opt_to_string(agent)))
.await .await
} }
/// update something /// update something
pub async fn update_item(&self, item: impl AsMsg + Debug) -> Result<Empty> { pub async fn update_item(&self, item: impl AsMsg + Debug) -> Result<()> {
self._req("update_item", item).await self.req_with_payload("update_item", item).await
} }
/// create and upload job /// create and upload job
pub async fn upload_jobs(&self, payload: impl OneOrVec<models::JobMeta>) -> Result<Empty> { pub async fn upload_jobs(&self, payload: impl OneOrVec<models::JobMeta>) -> Result<()> {
self._req("upload_jobs", payload.into_vec()).await self.req_with_payload("upload_jobs", payload.into_vec())
.await
} }
/// delete something /// delete something
pub async fn del(&self, item: Uuid) -> Result<i32> { pub async fn del(&self, item: Uuid) -> Result<i32> {
self._req(format!("del/{item}"), Empty).await self.req(format!("del/{item}")).await
} }
/// set jobs for any agent /// set jobs for any agent
@ -132,13 +138,17 @@ impl ClientHandler {
agent: Uuid, agent: Uuid,
job_idents: impl OneOrVec<String>, job_idents: impl OneOrVec<String>,
) -> Result<Vec<Uuid>> { ) -> Result<Vec<Uuid>> {
self._req(format!("set_jobs/{agent}"), job_idents.into_vec()) self.req_with_payload(format!("set_jobs/{agent}"), job_idents.into_vec())
.await .await
} }
/// get jobs for any agent /// get jobs for any agent
pub async fn get_agent_jobs(&self, agent: Option<Uuid>) -> Result<Vec<models::AssignedJob>> { pub async fn get_agent_jobs(&self, agent: Option<Uuid>) -> Result<Vec<models::AssignedJob>> {
self._req(format!("get_agent_jobs/{}", opt_to_string(agent)), Empty) self.req(format!("get_agent_jobs/{}", opt_to_string(agent)))
.await .await
} }
pub async fn ping(&self) -> Result<()> {
self.req("ping").await
}
} }

@ -7,6 +7,7 @@ lazy_static! {
static ref UID: Uuid = Uuid::new_v4(); static ref UID: Uuid = Uuid::new_v4();
} }
#[inline]
pub fn get_self_uid() -> Uuid { pub fn get_self_uid() -> Uuid {
*UID *UID
} }

@ -12,6 +12,7 @@ pub fn init_logger(logfile: Option<impl AsRef<Path> + Send + Sync + 'static>) {
let reg = registry() let reg = registry()
.with(EnvFilter::from_default_env()) .with(EnvFilter::from_default_env())
.with(fmt::layer()); .with(fmt::layer());
match logfile { match logfile {
Some(file) => reg Some(file) => reg
.with( .with(

@ -16,12 +16,9 @@ impl AsMsg for Reportable {}
impl AsMsg for JobMeta {} impl AsMsg for JobMeta {}
impl AsMsg for String {} impl AsMsg for String {}
impl AsMsg for Uuid {} impl AsMsg for Uuid {}
impl AsMsg for Empty {}
impl AsMsg for i32 {} impl AsMsg for i32 {}
impl AsMsg for u8 {} impl AsMsg for u8 {}
impl AsMsg for () {}
#[derive(Serialize, Deserialize, Clone, Default, Debug)]
pub struct Empty;
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
pub enum Reportable { pub enum Reportable {

@ -79,6 +79,10 @@ impl Agent {
} }
} }
pub fn touch(&mut self) {
self.last_active = SystemTime::now();
}
#[cfg(unix)] #[cfg(unix)]
pub async fn gather() -> Self { pub async fn gather() -> Self {
let mut builder = NamedJobRunner::from_shell(vec![ let mut builder = NamedJobRunner::from_shell(vec![

@ -112,4 +112,8 @@ impl AssignedJob {
pub fn set_result<S: Serialize>(&mut self, result: &S) { pub fn set_result<S: Serialize>(&mut self, result: &S) {
self.result = Some(serde_json::to_vec(result).unwrap()); self.result = Some(serde_json::to_vec(result).unwrap());
} }
pub fn touch(&mut self) {
self.updated = SystemTime::now()
}
} }

@ -12,24 +12,24 @@ use tokio::process::Command;
pub struct JobRunner { pub struct JobRunner {
waiter: Waiter, waiter: Waiter,
is_running: bool,
} }
impl JobRunner { impl JobRunner {
pub fn from_jobs(jobs: impl OneOrVec<AssignedJobById>) -> CombinedResult<Self> { pub fn from_jobs(jobs: impl OneOrVec<AssignedJobById>) -> CombinedResult<Self> {
let jobs = jobs.into_vec(); let jobs = jobs.into_vec();
let mut waiter = Waiter::new(); let mut waiter = Waiter::new();
let mut result = CombinedResult::<Self, _>::new(); let mut result = CombinedResult::new();
for job in jobs { for job in jobs {
//waiting for try-blocks stabilization //waiting for try-blocks stabilization
let built_job = (|| -> UResult<()> { let built_job: UResult<()> = (|| {
let meta = JobCache::get(job.job_id).ok_or(UError::NoJob(job.job_id))?; let meta = JobCache::get(job.job_id).ok_or(UError::NoJob(job.job_id))?;
let curr_platform = Platform::current(); let curr_platform = Platform::current();
if !curr_platform.matches(&meta.platform) { if !curr_platform.matches(&meta.platform) {
return Err(UError::InsuitablePlatform( return Err(UError::InsuitablePlatform(
meta.platform.clone(), meta.platform.clone(),
curr_platform.into_string(), curr_platform.into_string(),
) ));
.into());
} }
let job = AssignedJob::from((&*meta, job)); let job = AssignedJob::from((&*meta, job));
waiter.push(run_assigned_job(job)); waiter.push(run_assigned_job(job));
@ -39,7 +39,10 @@ impl JobRunner {
result.err(e) result.err(e)
} }
} }
result.ok(Self { waiter }); result.ok(Self {
waiter,
is_running: false,
});
result result
} }
@ -48,10 +51,10 @@ impl JobRunner {
.into_vec() .into_vec()
.into_iter() .into_iter()
.map(|jm| { .map(|jm| {
let job_uid = jm.id; let job_id = jm.id;
JobCache::insert(jm); JobCache::insert(jm);
AssignedJobById { AssignedJobById {
job_id: job_uid, job_id,
..Default::default() ..Default::default()
} }
}) })
@ -62,17 +65,23 @@ impl JobRunner {
/// Spawn jobs /// Spawn jobs
pub async fn spawn(mut self) -> Self { pub async fn spawn(mut self) -> Self {
self.waiter = self.waiter.spawn().await; self.waiter = self.waiter.spawn().await;
self.is_running = true;
self self
} }
/// Spawn jobs and wait for result /// Spawn jobs and wait for result
pub async fn wait(self) -> Vec<ExecResult> { pub async fn wait(self) -> Vec<ExecResult> {
self.waiter.spawn().await.wait().await let waiter = if !self.is_running {
self.spawn().await.waiter
} else {
self.waiter
};
waiter.wait().await
} }
/// Spawn one job and wait for result /// Spawn one job and wait for result
pub async fn wait_one(self) -> ExecResult { pub async fn wait_one(self) -> ExecResult {
self.waiter.spawn().await.wait().await.pop().unwrap() self.wait().await.pop().unwrap()
} }
} }
@ -125,7 +134,7 @@ pub async fn run_assigned_job(mut job: AssignedJob) -> ExecResult {
/// Store jobs and get results by name /// Store jobs and get results by name
pub struct NamedJobRunner { pub struct NamedJobRunner {
builder: Option<JobRunner>, runner: Option<JobRunner>,
job_names: Vec<&'static str>, job_names: Vec<&'static str>,
results: HashMap<&'static str, ExecResult>, results: HashMap<&'static str, ExecResult>,
} }
@ -163,14 +172,14 @@ impl NamedJobRunner {
}) })
.collect(); .collect();
Self { Self {
builder: Some(JobRunner::from_meta(job_metas).unwrap_one()), runner: Some(JobRunner::from_meta(job_metas).unwrap_one()),
job_names, job_names,
results: HashMap::new(), results: HashMap::new(),
} }
} }
pub async fn wait(mut self) -> Self { pub async fn wait(mut self) -> Self {
let results = self.builder.take().unwrap().wait().await; let results = self.runner.take().unwrap().wait().await;
for (name, result) in self.job_names.iter().zip(results.into_iter()) { for (name, result) in self.job_names.iter().zip(results.into_iter()) {
self.results.insert(name, result); self.results.insert(name, result);
} }

Loading…
Cancel
Save