From aad7772a713303c38ad840f63240a1e92657309d Mon Sep 17 00:00:00 2001 From: plazmoid Date: Fri, 21 Oct 2022 01:17:22 +0500 Subject: [PATCH] done. back: - remove tui deadcode - redirect all unknown links to index.html - fix integration tests - fix linking errors by removing wrong libc.a - split update_item into concrete endpoints - simplify ProcOutput (retain only stderr header) - fix PanelResult front: - load all assets from /core/ - improve current tab highlighting - show, edit and delete job and result info - allow to add and assign jobs --- bin/u_panel/src/argparse.rs | 115 +++---- bin/u_panel/src/main.rs | 4 +- .../src/server/{errors.rs => error.rs} | 0 bin/u_panel/src/server/fe/angular.json | 3 +- .../src/server/fe/src/app/app.component.html | 5 +- .../src/server/fe/src/app/app.component.ts | 5 + .../src/server/fe/src/app/app.module.ts | 21 +- .../fe/src/app/core/models/agent.model.ts | 4 +- .../server/fe/src/app/core/models/index.ts | 8 +- .../fe/src/app/core/models/job.model.ts | 8 +- .../fe/src/app/core/models/result.model.ts | 6 +- .../fe/src/app/core/services/api.service.ts | 18 +- .../src/app/core/tables/agent.component.html | 11 +- .../fe/src/app/core/tables/agent.component.ts | 58 +--- .../tables/dialogs/agent-info-dialog.html | 2 +- .../tables/dialogs/agent_info.component.ts | 4 +- .../tables/dialogs/assign-job-dialog.html | 12 + .../tables/dialogs/assign_job.component.ts | 33 ++ .../fe/src/app/core/tables/dialogs/index.ts | 5 +- .../tables/dialogs/info-dialog.component.less | 11 + .../core/tables/dialogs/job-info-dialog.html | 50 +++ .../core/tables/dialogs/job_info.component.ts | 30 ++ .../tables/dialogs/result-info-dialog.html | 53 +++ .../tables/dialogs/result_info.component.ts | 20 ++ .../fe/src/app/core/tables/job.component.html | 27 +- .../fe/src/app/core/tables/job.component.ts | 48 ++- .../src/app/core/tables/result.component.html | 19 +- .../src/app/core/tables/result.component.ts | 19 +- .../fe/src/app/core/tables/table.component.ts | 60 +++- .../src/server/fe/src/app/core/utils.ts | 4 - .../fe/src/environments/environment.prod.ts | 2 +- .../server/fe/src/environments/environment.ts | 2 +- bin/u_panel/src/server/fe/src/index.html | 6 +- bin/u_panel/src/server/mod.rs | 52 ++- bin/u_panel/src/tui/impls.rs | 61 ---- bin/u_panel/src/tui/mod.rs | 187 ---------- bin/u_panel/src/tui/utils.rs | 58 ---- bin/u_panel/src/tui/windows/confirm.rs | 127 ------- bin/u_panel/src/tui/windows/main_wnd.rs | 325 ------------------ bin/u_panel/src/tui/windows/mod.rs | 223 ------------ bin/u_server/src/db.rs | 8 +- bin/u_server/src/handlers.rs | 5 +- bin/u_server/src/u_server.rs | 14 +- images/musl-libs.Dockerfile | 2 +- integration/integration_tests.py | 9 +- integration/tests/helpers/panel.rs | 2 +- integration/tests/integration/behaviour.rs | 2 +- lib/u_lib/src/api.rs | 18 +- lib/u_lib/src/datatypes.rs | 16 + lib/u_lib/src/models/agent.rs | 40 +-- lib/u_lib/src/models/jobs/assigned.rs | 26 +- lib/u_lib/src/models/jobs/meta.rs | 9 +- lib/u_lib/src/models/schema.rs | 18 +- lib/u_lib/src/runner.rs | 10 +- lib/u_lib/src/utils/proc_output.rs | 113 ++---- scripts/build_musl_libs.sh | 4 +- 56 files changed, 645 insertions(+), 1357 deletions(-) rename bin/u_panel/src/server/{errors.rs => error.rs} (100%) create mode 100644 bin/u_panel/src/server/fe/src/app/core/tables/dialogs/assign-job-dialog.html create mode 100644 bin/u_panel/src/server/fe/src/app/core/tables/dialogs/assign_job.component.ts create mode 100644 bin/u_panel/src/server/fe/src/app/core/tables/dialogs/job-info-dialog.html create mode 100644 bin/u_panel/src/server/fe/src/app/core/tables/dialogs/job_info.component.ts create mode 100644 bin/u_panel/src/server/fe/src/app/core/tables/dialogs/result-info-dialog.html create mode 100644 bin/u_panel/src/server/fe/src/app/core/tables/dialogs/result_info.component.ts delete mode 100644 bin/u_panel/src/tui/impls.rs delete mode 100644 bin/u_panel/src/tui/mod.rs delete mode 100644 bin/u_panel/src/tui/utils.rs delete mode 100644 bin/u_panel/src/tui/windows/confirm.rs delete mode 100644 bin/u_panel/src/tui/windows/main_wnd.rs delete mode 100644 bin/u_panel/src/tui/windows/mod.rs diff --git a/bin/u_panel/src/argparse.rs b/bin/u_panel/src/argparse.rs index 267f383..4e43067 100644 --- a/bin/u_panel/src/argparse.rs +++ b/bin/u_panel/src/argparse.rs @@ -1,4 +1,4 @@ -use anyhow::Result as AnyResult; +use serde_json::{from_str, to_value, Value}; use structopt::StructOpt; use u_lib::{ api::ClientHandler, @@ -21,16 +21,9 @@ enum Cmd { Jobs(JobCRUD), Map(JobMapCRUD), Ping, - //TUI(TUIArgs), Serve, } -#[derive(StructOpt, Debug)] -pub struct TUIArgs { - #[structopt(long)] - pub nogui: bool, -} - #[derive(StructOpt, Debug)] enum JobCRUD { Create { @@ -77,58 +70,60 @@ fn parse_uuid(src: &str) -> Result { Uuid::parse_str(src).map_err(|e| e.to_string()) } -pub async fn process_cmd(client: ClientHandler, args: Args) -> UResult { - fn to_json(data: AnyResult) -> String { - let result = match data { - Ok(r) => PanelResult::Ok(r), - Err(e) => PanelResult::Err(e.downcast().expect("unknown error type")), - }; - serde_json::to_string(&result).unwrap() - } +pub fn into_value(data: M) -> Value { + to_value(data).unwrap() +} - Ok(match args.cmd { - Cmd::Agents(action) => match action { - RUD::Read { uid } => to_json(client.get_agents(uid).await), - RUD::Update { item } => { - let agent = serde_json::from_str::(&item)?; - to_json(client.update_item(agent).await) +pub async fn process_cmd(client: ClientHandler, args: Args) -> PanelResult { + let catcher: UResult = (|| async { + Ok(match args.cmd { + Cmd::Agents(action) => match action { + RUD::Read { uid } => into_value(client.get_agents(uid).await?), + RUD::Update { item } => { + let agent = from_str::(&item)?; + into_value(client.update_agent(agent).await?) + } + RUD::Delete { uid } => into_value(client.del(uid).await?), + }, + Cmd::Jobs(action) => match action { + JobCRUD::Create { job } => { + let raw_job = from_str::(&job)?; + let job = raw_job.validated()?; + into_value(client.upload_jobs(job).await?) + } + JobCRUD::RUD(RUD::Read { uid }) => into_value(client.get_jobs(uid).await?), + JobCRUD::RUD(RUD::Update { item }) => { + let raw_job = from_str::(&item)?; + let job = raw_job.validated()?; + into_value(client.update_job(job).await?) + } + JobCRUD::RUD(RUD::Delete { uid }) => into_value(client.del(uid).await?), + }, + Cmd::Map(action) => match action { + JobMapCRUD::Create { + agent_uid, + job_idents, + } => into_value(client.set_jobs(agent_uid, job_idents).await?), + JobMapCRUD::RUD(RUD::Read { uid }) => into_value(client.get_agent_jobs(uid).await?), + JobMapCRUD::RUD(RUD::Update { item }) => { + let assigned = from_str::(&item)?; + into_value(client.update_result(assigned).await?) + } + JobMapCRUD::RUD(RUD::Delete { uid }) => into_value(client.del(uid).await?), + }, + Cmd::Ping => into_value(client.ping().await?), + Cmd::Serve => { + crate::server::serve(client) + .await + .map_err(|e| UError::PanelError(format!("{e:?}")))?; + Value::Null } - RUD::Delete { uid } => to_json(client.del(uid).await), - }, - Cmd::Jobs(action) => match action { - JobCRUD::Create { job } => { - let raw_job = serde_json::from_str::(&job)?; - let job = raw_job.into_builder().build()?; - to_json(client.upload_jobs(job).await) - } - JobCRUD::RUD(RUD::Read { uid }) => to_json(client.get_jobs(uid).await), - JobCRUD::RUD(RUD::Update { item }) => { - let job = serde_json::from_str::(&item)?; - to_json(client.update_item(job).await) - } - JobCRUD::RUD(RUD::Delete { uid }) => to_json(client.del(uid).await), - }, - Cmd::Map(action) => match action { - JobMapCRUD::Create { - agent_uid, - job_idents, - } => to_json(client.set_jobs(agent_uid, job_idents).await), - JobMapCRUD::RUD(RUD::Read { uid }) => to_json(client.get_agent_jobs(uid).await), - JobMapCRUD::RUD(RUD::Update { item }) => { - let assigned = serde_json::from_str::(&item)?; - to_json(client.update_item(assigned).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) - .await - .map_err(|e| UError::PanelError(e.to_string()))?,*/ - Cmd::Serve => { - crate::server::serve(client) - .await - .map_err(|e| UError::PanelError(format!("{e:?}")))?; - String::new() - } - }) + }) + })() + .await; + + match catcher { + Ok(r) => PanelResult::Ok(r), + Err(e) => PanelResult::Err(e), + } } diff --git a/bin/u_panel/src/main.rs b/bin/u_panel/src/main.rs index ea263b8..595cbfa 100644 --- a/bin/u_panel/src/main.rs +++ b/bin/u_panel/src/main.rs @@ -1,6 +1,5 @@ mod argparse; mod server; -//mod tui; #[macro_use] extern crate tracing; @@ -27,7 +26,8 @@ async fn main() -> AnyResult<()> { let args = Args::from_args(); init_logger(None::<&str>); - let result = process_cmd(client, args).await?; + + let result = process_cmd(client, args).await.to_string(); println!("{result}"); Ok(()) } diff --git a/bin/u_panel/src/server/errors.rs b/bin/u_panel/src/server/error.rs similarity index 100% rename from bin/u_panel/src/server/errors.rs rename to bin/u_panel/src/server/error.rs diff --git a/bin/u_panel/src/server/fe/angular.json b/bin/u_panel/src/server/fe/angular.json index 3972326..d35487b 100644 --- a/bin/u_panel/src/server/fe/angular.json +++ b/bin/u_panel/src/server/fe/angular.json @@ -38,6 +38,7 @@ }, "configurations": { "production": { + "baseHref": "/core/", "budgets": [ { "type": "initial", @@ -110,4 +111,4 @@ } }, "defaultProject": "fe" -} +} \ No newline at end of file diff --git a/bin/u_panel/src/server/fe/src/app/app.component.html b/bin/u_panel/src/server/fe/src/app/app.component.html index 8bb207a..b35dfd7 100644 --- a/bin/u_panel/src/server/fe/src/app/app.component.html +++ b/bin/u_panel/src/server/fe/src/app/app.component.html @@ -1,6 +1,5 @@ \ No newline at end of file diff --git a/bin/u_panel/src/server/fe/src/app/app.component.ts b/bin/u_panel/src/server/fe/src/app/app.component.ts index a76961e..566f46b 100644 --- a/bin/u_panel/src/server/fe/src/app/app.component.ts +++ b/bin/u_panel/src/server/fe/src/app/app.component.ts @@ -6,4 +6,9 @@ import { Component, ViewChild, AfterViewInit } from '@angular/core'; styleUrls: ['./app.component.less'] }) export class AppComponent { + tabs = [ + { name: 'Agents', link: '/agents' }, + { name: 'Jobs', link: '/jobs' }, + { name: 'Results', link: '/results' } + ]; } diff --git a/bin/u_panel/src/server/fe/src/app/app.module.ts b/bin/u_panel/src/server/fe/src/app/app.module.ts index 037df53..c1572f4 100644 --- a/bin/u_panel/src/server/fe/src/app/app.module.ts +++ b/bin/u_panel/src/server/fe/src/app/app.module.ts @@ -14,7 +14,16 @@ import { MatDialogModule } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; import { FormsModule } from '@angular/forms'; import { AgentComponent, JobComponent, ResultComponent } from './core/tables'; -import { AgentInfoDialogComponent } from './core/tables/dialogs'; +import { + AgentInfoDialogComponent, + AssignJobDialogComponent, + JobInfoDialogComponent, + ResultInfoDialogComponent +} from './core/tables/dialogs'; +import { APP_BASE_HREF } from '@angular/common'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; +import { MatListModule } from '@angular/material/list'; @NgModule({ declarations: [ @@ -22,7 +31,10 @@ import { AgentInfoDialogComponent } from './core/tables/dialogs'; AgentComponent, JobComponent, ResultComponent, - AgentInfoDialogComponent + AgentInfoDialogComponent, + JobInfoDialogComponent, + ResultInfoDialogComponent, + AssignJobDialogComponent ], imports: [ BrowserModule, @@ -36,10 +48,13 @@ import { AgentInfoDialogComponent } from './core/tables/dialogs'; MatDialogModule, MatProgressSpinnerModule, MatIconModule, + MatTooltipModule, + MatSnackBarModule, + MatListModule, FormsModule, BrowserAnimationsModule ], - providers: [], + providers: [{ provide: APP_BASE_HREF, useValue: '/' }], bootstrap: [AppComponent] }) export class AppModule { } diff --git a/bin/u_panel/src/server/fe/src/app/core/models/agent.model.ts b/bin/u_panel/src/server/fe/src/app/core/models/agent.model.ts index cd8ef76..d798f0e 100644 --- a/bin/u_panel/src/server/fe/src/app/core/models/agent.model.ts +++ b/bin/u_panel/src/server/fe/src/app/core/models/agent.model.ts @@ -1,6 +1,6 @@ -import { UTCDate } from "."; +import { UTCDate, ApiModel } from "."; -export interface AgentModel { +export interface AgentModel extends ApiModel { alias: string | null, hostname: string, host_info: string, diff --git a/bin/u_panel/src/server/fe/src/app/core/models/index.ts b/bin/u_panel/src/server/fe/src/app/core/models/index.ts index 38623ef..dad077a 100644 --- a/bin/u_panel/src/server/fe/src/app/core/models/index.ts +++ b/bin/u_panel/src/server/fe/src/app/core/models/index.ts @@ -5,4 +5,10 @@ export * from './job.model'; export interface UTCDate { secs_since_epoch: number, nanos_since_epoch: number -} \ No newline at end of file +} + +export abstract class ApiModel { } + +export interface Empty extends ApiModel { } + +export type Area = "agents" | "jobs" | "map"; \ No newline at end of file diff --git a/bin/u_panel/src/server/fe/src/app/core/models/job.model.ts b/bin/u_panel/src/server/fe/src/app/core/models/job.model.ts index 66f1012..9db303f 100644 --- a/bin/u_panel/src/server/fe/src/app/core/models/job.model.ts +++ b/bin/u_panel/src/server/fe/src/app/core/models/job.model.ts @@ -1,8 +1,12 @@ -export interface JobModel { +import { ApiModel } from "."; + +export interface JobModel extends ApiModel { alias: string, argv: string, id: string, exec_type: string, platform: string, - payload: Uint8Array | null, + payload: number[] | null, + payload_path: string | null, + schedule: string | null, } \ No newline at end of file diff --git a/bin/u_panel/src/server/fe/src/app/core/models/result.model.ts b/bin/u_panel/src/server/fe/src/app/core/models/result.model.ts index 825323b..c699787 100644 --- a/bin/u_panel/src/server/fe/src/app/core/models/result.model.ts +++ b/bin/u_panel/src/server/fe/src/app/core/models/result.model.ts @@ -1,12 +1,12 @@ -import { UTCDate } from "."; +import { UTCDate, ApiModel } from "."; -export interface ResultModel { +export interface ResultModel extends ApiModel { agent_id: string, alias: string, created: UTCDate, id: string, job_id: string, - result: Uint8Array, + result: number[], state: "Queued" | "Running" | "Finished", retcode: number | null, updated: UTCDate, diff --git a/bin/u_panel/src/server/fe/src/app/core/services/api.service.ts b/bin/u_panel/src/server/fe/src/app/core/services/api.service.ts index 26d5810..84348e2 100644 --- a/bin/u_panel/src/server/fe/src/app/core/services/api.service.ts +++ b/bin/u_panel/src/server/fe/src/app/core/services/api.service.ts @@ -1,23 +1,23 @@ -import { Injectable } from '@angular/core'; import { environment } from 'src/environments/environment'; import { HttpClient } from '@angular/common/http'; import { firstValueFrom } from 'rxjs'; +import { ApiModel, Empty, Area } from '../models'; -interface ServerResponse { +interface ServerResponse { status: "ok" | "err", data: T | string } -export class ApiTableService { - area: string; +export class ApiTableService { + area: Area; - constructor(private http: HttpClient, area: string) { + constructor(private http: HttpClient, area: Area) { this.area = area; } requestUrl = `${environment.server}/cmd/`; - async req(cmd: string): Promise> { + async req(cmd: string): Promise> { return await firstValueFrom(this.http.post>(this.requestUrl, cmd)) } @@ -39,15 +39,15 @@ export class ApiTableService { return await this.req(`${this.area} read`) } - async update(item: T): Promise> { + async update(item: T): Promise> { return await this.req(`${this.area} update '${JSON.stringify(item)}'`) } - async delete(id: string): Promise> { + async delete(id: string): Promise> { return await this.req(`${this.area} delete ${id}`) } - async create(item: string): Promise> { + async create(item: string): Promise> { return await this.req(`${this.area} create ${item}`) } } \ No newline at end of file diff --git a/bin/u_panel/src/server/fe/src/app/core/tables/agent.component.html b/bin/u_panel/src/server/fe/src/app/core/tables/agent.component.html index 1bfd13a..fbaaba9 100644 --- a/bin/u_panel/src/server/fe/src/app/core/tables/agent.component.html +++ b/bin/u_panel/src/server/fe/src/app/core/tables/agent.component.html @@ -10,8 +10,8 @@ - +
@@ -51,9 +51,14 @@
ID - + | + + | diff --git a/bin/u_panel/src/server/fe/src/app/core/tables/agent.component.ts b/bin/u_panel/src/server/fe/src/app/core/tables/agent.component.ts index dae0ad0..c72b8dc 100644 --- a/bin/u_panel/src/server/fe/src/app/core/tables/agent.component.ts +++ b/bin/u_panel/src/server/fe/src/app/core/tables/agent.component.ts @@ -2,54 +2,35 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { TablesComponent } from './table.component'; import { AgentModel } from '../models'; import { AgentInfoDialogComponent } from './dialogs/agent_info.component'; -import { HttpClient } from '@angular/common/http'; -import { MatDialog } from '@angular/material/dialog'; -import { epochToStr } from '../utils'; -import { ActivatedRoute, Router } from '@angular/router'; -import { emitErr } from '../utils'; -import { Subscription } from 'rxjs'; +import { HttpErrorResponse } from '@angular/common/http'; +import { AssignJobDialogComponent } from './dialogs'; @Component({ selector: 'agent-table', templateUrl: './agent.component.html', styleUrls: ['./table.component.less'] }) -export class AgentComponent extends TablesComponent implements OnDestroy, OnInit { +export class AgentComponent extends TablesComponent implements OnInit { - dialogSubscr!: Subscription; + //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); - } - - 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, { + const dialog = this.infoDialog.open(AgentInfoDialogComponent, { data: resp.data as AgentModel, - width: '500px', + width: '1000px', }); const saveSub = dialog.componentInstance.onSave.subscribe(result => { - this.data_source!.update(result).then(_ => this.loadTableData()).catch(emitErr) + this.data_source!.update(result).then(_ => { + this.openSnackBar('Saved', false) + this.loadTableData() + }) + .catch((err: HttpErrorResponse) => this.openSnackBar(err.error)) }) dialog.afterClosed().subscribe(result => { @@ -57,18 +38,15 @@ export class AgentComponent extends TablesComponent implements OnDes this.router.navigate(['.'], { relativeTo: this.route }) }) } else { - emitErr(resp.data) + this.openSnackBar(resp.data) } - }).catch(emitErr) - } - - deleteItem(id: string) { - if (confirm(`Delete ${id}?`)) { - this.data_source!.delete(id).catch(emitErr) - } + }).catch((err: HttpErrorResponse) => this.openSnackBar(err.error)) } - ngOnDestroy(): void { - this.dialogSubscr.unsubscribe() + assignJobs(id: string) { + const dialog = this.infoDialog.open(AssignJobDialogComponent, { + data: id, + width: '1000px', + }); } } diff --git a/bin/u_panel/src/server/fe/src/app/core/tables/dialogs/agent-info-dialog.html b/bin/u_panel/src/server/fe/src/app/core/tables/dialogs/agent-info-dialog.html index 260b345..ff66ad5 100644 --- a/bin/u_panel/src/server/fe/src/app/core/tables/dialogs/agent-info-dialog.html +++ b/bin/u_panel/src/server/fe/src/app/core/tables/dialogs/agent-info-dialog.html @@ -28,7 +28,7 @@

Host info -

diff --git a/bin/u_panel/src/server/fe/src/app/core/tables/dialogs/agent_info.component.ts b/bin/u_panel/src/server/fe/src/app/core/tables/dialogs/agent_info.component.ts index 9195e61..d24d3a9 100644 --- a/bin/u_panel/src/server/fe/src/app/core/tables/dialogs/agent_info.component.ts +++ b/bin/u_panel/src/server/fe/src/app/core/tables/dialogs/agent_info.component.ts @@ -2,7 +2,6 @@ import { Component, Inject } from '@angular/core'; import { MAT_DIALOG_DATA } from '@angular/material/dialog'; import { AgentModel } from '../../models/agent.model'; import { EventEmitter } from '@angular/core'; -import { Input } from '@angular/core'; @Component({ selector: 'agent-info-dialog', @@ -13,11 +12,10 @@ export class AgentInfoDialogComponent { is_preview = true; onSave = new EventEmitter(); - constructor(@Inject(MAT_DIALOG_DATA) public data: AgentModel) { } updateAgent() { console.log(this.data); this.onSave.emit(this.data); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/bin/u_panel/src/server/fe/src/app/core/tables/dialogs/assign-job-dialog.html b/bin/u_panel/src/server/fe/src/app/core/tables/dialogs/assign-job-dialog.html new file mode 100644 index 0000000..2ebe970 --- /dev/null +++ b/bin/u_panel/src/server/fe/src/app/core/tables/dialogs/assign-job-dialog.html @@ -0,0 +1,12 @@ +

Assign job

+ + + + {{row}} + + + + + + + \ No newline at end of file diff --git a/bin/u_panel/src/server/fe/src/app/core/tables/dialogs/assign_job.component.ts b/bin/u_panel/src/server/fe/src/app/core/tables/dialogs/assign_job.component.ts new file mode 100644 index 0000000..cd278d6 --- /dev/null +++ b/bin/u_panel/src/server/fe/src/app/core/tables/dialogs/assign_job.component.ts @@ -0,0 +1,33 @@ +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { HttpClient } from '@angular/common/http'; +import { ApiTableService } from '../../services'; +import { JobModel } from '../../models'; +import { MatListOption } from '@angular/material/list'; + +@Component({ + selector: 'assign-job-dialog', + templateUrl: 'assign-job-dialog.html', + styleUrls: [] +}) +export class AssignJobDialogComponent { + rows: string[] = []; + selected_rows: string[] = []; + + constructor(@Inject(MAT_DIALOG_DATA) public agent_id: string, private http: HttpClient) { + new ApiTableService(http, "jobs").getMany().then(result => { + if (result.status == "ok") { + const jobs = result.data as JobModel[] + this.rows = jobs.map(j => `${j.id} ${j.alias}`) + } else { + alert(result.data as string) + } + }).catch(err => alert(err)) + } + + assignSelectedJobs() { + const job_ids = this.selected_rows.map(row => row.split(' ', 1)[0]).join(' '); + const request = `${this.agent_id} ${job_ids}` + new ApiTableService(this.http, "map").create(request).catch(err => alert(err)) + } +} \ No newline at end of file diff --git a/bin/u_panel/src/server/fe/src/app/core/tables/dialogs/index.ts b/bin/u_panel/src/server/fe/src/app/core/tables/dialogs/index.ts index 1aa08ec..4bdb1aa 100644 --- a/bin/u_panel/src/server/fe/src/app/core/tables/dialogs/index.ts +++ b/bin/u_panel/src/server/fe/src/app/core/tables/dialogs/index.ts @@ -1 +1,4 @@ -export * from './agent_info.component'; \ No newline at end of file +export * from './agent_info.component'; +export * from './result_info.component'; +export * from './job_info.component'; +export * from './assign_job.component'; \ No newline at end of file diff --git a/bin/u_panel/src/server/fe/src/app/core/tables/dialogs/info-dialog.component.less b/bin/u_panel/src/server/fe/src/app/core/tables/dialogs/info-dialog.component.less index bc8e5fb..23b8bc5 100644 --- a/bin/u_panel/src/server/fe/src/app/core/tables/dialogs/info-dialog.component.less +++ b/bin/u_panel/src/server/fe/src/app/core/tables/dialogs/info-dialog.component.less @@ -1,3 +1,14 @@ .info-dlg-field { width: 100%; +} + +div.info-dialog-forms-box { + width: 100%; + margin-right: 10px; +} + +div.info-dialog-forms-box-smol { + width: 30%; + float: left; + margin-right: 10px; } \ No newline at end of file diff --git a/bin/u_panel/src/server/fe/src/app/core/tables/dialogs/job-info-dialog.html b/bin/u_panel/src/server/fe/src/app/core/tables/dialogs/job-info-dialog.html new file mode 100644 index 0000000..79108e6 --- /dev/null +++ b/bin/u_panel/src/server/fe/src/app/core/tables/dialogs/job-info-dialog.html @@ -0,0 +1,50 @@ +

Job info

+

Editing job info

+ +
+ + ID + + + + Alias + + + + Args + + +
+
+ + Type + + + + Platform + + + + Schedule + + +
+
+ + Payload path + + +
+
+ + Payload + + +
+
+ + + + + \ No newline at end of file diff --git a/bin/u_panel/src/server/fe/src/app/core/tables/dialogs/job_info.component.ts b/bin/u_panel/src/server/fe/src/app/core/tables/dialogs/job_info.component.ts new file mode 100644 index 0000000..08c29da --- /dev/null +++ b/bin/u_panel/src/server/fe/src/app/core/tables/dialogs/job_info.component.ts @@ -0,0 +1,30 @@ +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { JobModel } from '../../models/job.model'; +import { EventEmitter } from '@angular/core'; + +@Component({ + selector: 'job-info-dialog', + templateUrl: 'job-info-dialog.html', + styleUrls: ['info-dialog.component.less'] +}) +export class JobInfoDialogComponent { + is_preview = true; + decodedPayload: string; + onSave = new EventEmitter(); + + constructor(@Inject(MAT_DIALOG_DATA) public data: JobModel) { + if (data.payload !== null) { + this.decodedPayload = new TextDecoder().decode(new Uint8Array(data.payload)) + } else { + this.decodedPayload = "" + } + } + + updateJob() { + if (this.decodedPayload.length > 0) { + this.data.payload = Array.from(new TextEncoder().encode(this.decodedPayload)) + } + this.onSave.emit(this.data); + } +} \ No newline at end of file diff --git a/bin/u_panel/src/server/fe/src/app/core/tables/dialogs/result-info-dialog.html b/bin/u_panel/src/server/fe/src/app/core/tables/dialogs/result-info-dialog.html new file mode 100644 index 0000000..6ce43c2 --- /dev/null +++ b/bin/u_panel/src/server/fe/src/app/core/tables/dialogs/result-info-dialog.html @@ -0,0 +1,53 @@ +

Result

+ +
+ + ID + + + + Job ID + + + + Agent ID + + +
+
+ + Alias + + + + State + + + + Return code + + +
+
+ + Created + + + + Updated + + +
+
+

+ + Result + + +

+
+
+ + + \ No newline at end of file diff --git a/bin/u_panel/src/server/fe/src/app/core/tables/dialogs/result_info.component.ts b/bin/u_panel/src/server/fe/src/app/core/tables/dialogs/result_info.component.ts new file mode 100644 index 0000000..b02fae5 --- /dev/null +++ b/bin/u_panel/src/server/fe/src/app/core/tables/dialogs/result_info.component.ts @@ -0,0 +1,20 @@ +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { ResultModel } from '../../models/result.model'; + +@Component({ + selector: 'result-info-dialog', + templateUrl: 'result-info-dialog.html', + styleUrls: ['info-dialog.component.less'] +}) +export class ResultInfoDialogComponent { + decodedResult: string; + + constructor(@Inject(MAT_DIALOG_DATA) public data: ResultModel) { + if (data.result !== null) { + this.decodedResult = new TextDecoder().decode(new Uint8Array(data.result)) + } else { + this.decodedResult = "" + } + } +} \ No newline at end of file diff --git a/bin/u_panel/src/server/fe/src/app/core/tables/job.component.html b/bin/u_panel/src/server/fe/src/app/core/tables/job.component.html index f99fbfd..bfd99b2 100644 --- a/bin/u_panel/src/server/fe/src/app/core/tables/job.component.html +++ b/bin/u_panel/src/server/fe/src/app/core/tables/job.component.html @@ -8,10 +8,12 @@ Filter - + + - +
@@ -41,10 +43,10 @@ - - + + @@ -55,6 +57,19 @@ + + + + + diff --git a/bin/u_panel/src/server/fe/src/app/core/tables/job.component.ts b/bin/u_panel/src/server/fe/src/app/core/tables/job.component.ts index 0403bea..4a5e1cf 100644 --- a/bin/u_panel/src/server/fe/src/app/core/tables/job.component.ts +++ b/bin/u_panel/src/server/fe/src/app/core/tables/job.component.ts @@ -1,6 +1,8 @@ import { Component, OnInit } from '@angular/core'; import { TablesComponent } from './table.component'; import { JobModel } from '../models'; +import { JobInfoDialogComponent } from './dialogs'; +import { HttpErrorResponse } from '@angular/common/http'; @Component({ selector: 'job-table', @@ -9,7 +11,49 @@ import { JobModel } from '../models'; }) export class JobComponent extends TablesComponent { area = 'jobs' as const; - displayedColumns = ['id', 'alias', 'argv', 'platform', 'payload', 'exec_type'] + displayedColumns = ['id', 'alias', 'platform', 'schedule', 'exec_type', 'actions'] - show_item_dialog(id: string) { } + show_item_dialog(id: string | null) { + const show_dlg = (id: string, edit: boolean) => { + this.data_source!.getOne(id).then(resp => { + if (resp.status === 'ok') { + var dialog = this.infoDialog.open(JobInfoDialogComponent, { + data: resp.data as JobModel, + width: '1000px', + }); + if (edit) { + dialog.componentInstance.is_preview = false + } + + const saveSub = dialog.componentInstance.onSave.subscribe(result => { + this.data_source!.update(result) + .then(_ => { + this.openSnackBar("Saved", false) + this.loadTableData() + }) + .catch((err: HttpErrorResponse) => this.openSnackBar(err.error)) + }) + + dialog.afterClosed().subscribe(result => { + saveSub.unsubscribe() + this.router.navigate(['.'], { relativeTo: this.route }) + }) + } else { + this.openSnackBar(resp.data) + } + }).catch((err: HttpErrorResponse) => this.openSnackBar(err.error)) + } + + if (id) { + show_dlg(id, false) + } else { + this.data_source!.create('"{}"').then(resp => { + if (resp.status === 'ok') { + show_dlg(resp.data[0], true) + } else { + this.openSnackBar(resp.data) + } + }).catch((err: HttpErrorResponse) => this.openSnackBar(err.error)) + } + } } diff --git a/bin/u_panel/src/server/fe/src/app/core/tables/result.component.html b/bin/u_panel/src/server/fe/src/app/core/tables/result.component.html index d8bf8d9..afda1cb 100644 --- a/bin/u_panel/src/server/fe/src/app/core/tables/result.component.html +++ b/bin/u_panel/src/server/fe/src/app/core/tables/result.component.html @@ -10,8 +10,8 @@ -
ID PayloadSchedule - {{row.payload}} + {{row.schedule}} + + | + +
+
@@ -37,7 +37,7 @@ @@ -55,6 +55,19 @@ + + + + + diff --git a/bin/u_panel/src/server/fe/src/app/core/tables/result.component.ts b/bin/u_panel/src/server/fe/src/app/core/tables/result.component.ts index b477094..ec98ab7 100644 --- a/bin/u_panel/src/server/fe/src/app/core/tables/result.component.ts +++ b/bin/u_panel/src/server/fe/src/app/core/tables/result.component.ts @@ -1,7 +1,8 @@ import { Component, OnInit } from '@angular/core'; import { TablesComponent } from './table.component'; import { ResultModel } from '../models'; -import { epochToStr } from '../utils'; +import { ResultInfoDialogComponent } from './dialogs'; +import { HttpErrorResponse } from '@angular/common/http'; @Component({ selector: 'results-table', @@ -17,10 +18,24 @@ export class ResultComponent extends TablesComponent { 'agent_id', 'job_id', 'state', - 'last_updated' + 'last_updated', + 'actions' ]; show_item_dialog(id: string) { + this.data_source!.getOne(id).then(resp => { + if (resp.status === 'ok') { + const dialog = this.infoDialog.open(ResultInfoDialogComponent, { + data: resp.data as ResultModel, + width: '1000px', + }); + dialog.afterClosed().subscribe(result => { + this.router.navigate(['.'], { relativeTo: this.route }) + }) + } else { + this.openSnackBar(resp.data) + } + }).catch((err: HttpErrorResponse) => this.openSnackBar(err.message)) } } diff --git a/bin/u_panel/src/server/fe/src/app/core/tables/table.component.ts b/bin/u_panel/src/server/fe/src/app/core/tables/table.component.ts index 062fb90..5a7bd23 100644 --- a/bin/u_panel/src/server/fe/src/app/core/tables/table.component.ts +++ b/bin/u_panel/src/server/fe/src/app/core/tables/table.component.ts @@ -1,26 +1,45 @@ -import { Component, OnInit, Directive } from '@angular/core'; -import { timer, of as observableOf } from 'rxjs'; -import { catchError, map, startWith, switchMap } from 'rxjs/operators'; +import { OnInit, Directive } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { ApiTableService } from '../'; import { MatTableDataSource } from '@angular/material/table'; import { MatDialog } from '@angular/material/dialog'; +import { ApiModel, Area } from '../models'; +import { ActivatedRoute, Router } from '@angular/router'; +import { interval } from 'rxjs'; +import { MatSnackBar, MatSnackBarConfig } from '@angular/material/snack-bar'; @Directive() -export abstract class TablesComponent implements OnInit { - abstract area: "agents" | "jobs" | "map"; +export abstract class TablesComponent implements OnInit { + abstract area: Area; data_source!: ApiTableService; table_data!: MatTableDataSource; isLoadingResults = true; - constructor(public _httpClient: HttpClient, public info_dlg: MatDialog) { + constructor( + public httpClient: HttpClient, + public infoDialog: MatDialog, + public route: ActivatedRoute, + public router: Router, + public snackBar: MatSnackBar + ) { this.table_data = new MatTableDataSource; } ngOnInit() { - this.data_source = new ApiTableService(this._httpClient, this.area); + this.data_source = new ApiTableService(this.httpClient, this.area); this.loadTableData(); + this.route.queryParams.subscribe(params => { + const id = params['id'] + const new_agent = params['new'] + if (id) { + this.show_item_dialog(id); + } + if (new_agent) { + this.show_item_dialog(null); + } + }) + //interval(10000).subscribe(_ => this.loadTableData()); } async loadTableData() { @@ -41,12 +60,25 @@ export abstract class TablesComponent implements OnInit { this.table_data.filter = filterValue.trim().toLowerCase(); } - abstract displayedColumns: string[]; - abstract show_item_dialog(id: string): void; -} + deleteItem(id: string) { + if (confirm(`Delete ${id}?`)) { + this.data_source!.delete(id).catch(this.openSnackBar) + } + } -type ColumnDef = { - def: string, - name: string, - cell: (cell: C) => string + openSnackBar(message: any, error: boolean = true) { + const msg = JSON.stringify(message) + const _config = (duration: number): MatSnackBarConfig => { + return { + horizontalPosition: 'right', + verticalPosition: 'bottom', + duration + } + } + const cfg = error ? _config(0) : _config(2000) + this.snackBar.open(msg, 'Ok', cfg); + } + + abstract displayedColumns: string[]; + abstract show_item_dialog(id: string | null): void; } \ No newline at end of file diff --git a/bin/u_panel/src/server/fe/src/app/core/utils.ts b/bin/u_panel/src/server/fe/src/app/core/utils.ts index 128a6a2..bc6c422 100644 --- a/bin/u_panel/src/server/fe/src/app/core/utils.ts +++ b/bin/u_panel/src/server/fe/src/app/core/utils.ts @@ -1,7 +1,3 @@ export function epochToStr(epoch: number): string { return new Date(epoch * 1000).toLocaleString('en-GB') } - -export function emitErr(e: any) { - alert(e) -} \ No newline at end of file diff --git a/bin/u_panel/src/server/fe/src/environments/environment.prod.ts b/bin/u_panel/src/server/fe/src/environments/environment.prod.ts index 95d0187..cc43895 100644 --- a/bin/u_panel/src/server/fe/src/environments/environment.prod.ts +++ b/bin/u_panel/src/server/fe/src/environments/environment.prod.ts @@ -1,4 +1,4 @@ export const environment = { production: true, - server: "" + server: "", }; diff --git a/bin/u_panel/src/server/fe/src/environments/environment.ts b/bin/u_panel/src/server/fe/src/environments/environment.ts index c016fed..1b3b824 100644 --- a/bin/u_panel/src/server/fe/src/environments/environment.ts +++ b/bin/u_panel/src/server/fe/src/environments/environment.ts @@ -4,7 +4,7 @@ export const environment = { production: false, - server: "http://127.0.0.1:8080" + server: "http://127.0.0.1:8080", }; /* diff --git a/bin/u_panel/src/server/fe/src/index.html b/bin/u_panel/src/server/fe/src/index.html index 94a6658..e299aec 100644 --- a/bin/u_panel/src/server/fe/src/index.html +++ b/bin/u_panel/src/server/fe/src/index.html @@ -1,16 +1,18 @@ + Fe - + - + + \ No newline at end of file diff --git a/bin/u_panel/src/server/mod.rs b/bin/u_panel/src/server/mod.rs index 1640e45..897be93 100644 --- a/bin/u_panel/src/server/mod.rs +++ b/bin/u_panel/src/server/mod.rs @@ -1,22 +1,9 @@ -/* -Tabs: Agents, Tasks, Summary -every tab has list page and item page with more info/actions - -Agents: - -| id | alias | ..see struct | tasks done -| stripped | alias | ... | clickable number of assigned jobs - -almost all fields are editable, rows are deletable - -*/ - -mod errors; +mod error; use crate::{process_cmd, Args}; use actix_cors::Cors; use actix_web::{get, middleware::Logger, post, web, App, HttpResponse, HttpServer, Responder}; -use errors::Error; +use error::Error; use futures_util::StreamExt; use rust_embed::RustEmbed; use std::borrow::Cow; @@ -34,16 +21,16 @@ impl Files { } } -#[get("/")] -async fn main_page() -> impl Responder { +async fn spa_main() -> impl Responder { let index = Files::get_static("index.html").unwrap(); HttpResponse::Ok().body(index) } -#[get("/{path}")] -async fn static_files_adapter(path: web::Path<(String,)>) -> impl Responder { +#[get("/core/{path}")] +async fn resources_adapter(path: web::Path<(String,)>) -> impl Responder { let path = path.into_inner().0; let mimetype = mime_guess::from_path(&path).first_or_octet_stream(); + match Files::get_static(path) { Some(data) => HttpResponse::Ok() .content_type(mimetype.to_string()) @@ -58,23 +45,33 @@ async fn send_cmd( client: web::Data, ) -> Result { let mut bytes = web::BytesMut::new(); + while let Some(item) = body.next().await { bytes.extend_from_slice( &item.map_err(|e| Error::JustError(format!("payload loading failure: {e}")))?, ); } + let cmd = String::from_utf8(bytes.to_vec()) .map_err(|_| Error::JustError("cmd contains non-utf8 data".to_string()))?; let mut cmd = shlex::split(&cmd).ok_or(Error::JustError("argparse failed".to_string()))?; + info!("cmd: {:?}", cmd); cmd.insert(0, String::from("u_panel")); + let parsed_cmd = Args::from_iter_safe(cmd)?; - Ok( - match process_cmd(client.as_ref().clone(), parsed_cmd).await { - Ok(r) => HttpResponse::Ok().body(r), - Err(e) => HttpResponse::BadRequest().body(e.to_string()), - }, - ) + let result = process_cmd(client.as_ref().clone(), parsed_cmd).await; + let result_string = result.to_string(); + + let response = if result.is_ok() { + HttpResponse::Ok().body(result_string) + } else if result.is_err() { + HttpResponse::BadRequest().body(result_string) + } else { + unreachable!() + }; + + Ok(response) } pub async fn serve(client: ClientHandler) -> anyhow::Result<()> { @@ -89,9 +86,10 @@ pub async fn serve(client: ClientHandler) -> anyhow::Result<()> { .wrap(Logger::default()) .wrap(Cors::permissive()) .app_data(web::Data::new(client.clone())) - .service(main_page) .service(send_cmd) - .service(static_files_adapter) + .service(resources_adapter) + .service(web::resource("/").to(spa_main)) + .service(web::resource("/{_}").to(spa_main)) }) .bind(addr)? .run() diff --git a/bin/u_panel/src/tui/impls.rs b/bin/u_panel/src/tui/impls.rs deleted file mode 100644 index a20adb4..0000000 --- a/bin/u_panel/src/tui/impls.rs +++ /dev/null @@ -1,61 +0,0 @@ -use crate::tui::windows::{ConfirmWnd, MainWnd, WndId}; -use crate::CLIENT; -use u_lib::models::{Agent, AssignedJob, JobMeta}; -use u_lib::UResult; -use uuid::Uuid; - -pub trait Id { - fn id(&self) -> T; -} - -#[macro_export] -macro_rules! impl_id { - ($id_type:tt => $($type:tt),+) => { - $( - impl Id<$id_type> for $type { - fn id(&self) -> $id_type { - self.id - } - })+ - }; -} - -impl_id!(Uuid => Agent, JobMeta, AssignedJob); -impl_id!(WndId => MainWnd, ConfirmWnd); - -#[async_trait] -pub trait CRUD: Id -where - Self: Sized, -{ - async fn read() -> UResult>; - - async fn delete(uid: Uuid) -> UResult { - CLIENT.del(Some(uid)).await - } - //TODO: other crud -} - -#[async_trait] -impl CRUD for Agent { - async fn read() -> UResult> { - CLIENT.get_agents(None).await.map(|r| r.into_builtin_vec()) - } -} - -#[async_trait] -impl CRUD for AssignedJob { - async fn read() -> UResult> { - CLIENT - .get_agent_jobs(None) - .await - .map(|r| r.into_builtin_vec()) - } -} - -#[async_trait] -impl CRUD for JobMeta { - async fn read() -> UResult> { - CLIENT.get_jobs(None).await.map(|r| r.into_builtin_vec()) - } -} diff --git a/bin/u_panel/src/tui/mod.rs b/bin/u_panel/src/tui/mod.rs deleted file mode 100644 index 3a86c84..0000000 --- a/bin/u_panel/src/tui/mod.rs +++ /dev/null @@ -1,187 +0,0 @@ -mod impls; -mod utils; -mod windows; - -use crate::argparse::TUIArgs; -use anyhow::Result as AResult; -use backtrace::Backtrace; -use crossterm::event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}; -use crossterm::execute; -use crossterm::terminal::{ - disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, -}; -use once_cell::sync::Lazy; -use std::{ - env, - io::{stdout, Stdout}, - panic::set_hook, - process::exit, - sync::atomic::{AtomicBool, Ordering}, - thread, - time::Duration, -}; -use tui::{backend::CrosstermBackend, Terminal}; -use utils::{AsyncChannel, Channel}; -use windows::{MainWnd, SharedWnd, WndId, WM}; - -pub type Backend = CrosstermBackend; -pub type Frame<'f> = tui::Frame<'f, Backend>; - -const EVENT_GEN_PERIOD: Duration = Duration::from_millis(70); - -static GENERAL_EVENT_CHANNEL: Lazy> = Lazy::new(|| Channel::new()); -static ACTIVE_LOOP: AtomicBool = AtomicBool::new(true); - -enum GEvent { - CreateWnd(SharedWnd), - CloseWnd { wid: WndId, force: bool }, - Exit, - Key(KeyCode), - Tick, -} - -fn get_terminal() -> AResult> { - let backend = CrosstermBackend::new(stdout()); - Ok(Terminal::new(backend)?) -} - -pub async fn init_tui(args: &TUIArgs) -> AResult<()> { - let gui = !args.nogui; - init_logger(); - info!("Initializing u_panel"); - - init_signal_handlers(gui); - init_panic_handler(gui); - term_setup(gui)?; - info!("Starting loop"); - - let result = init_loop(args).await; - info!("Exiting"); - term_teardown(gui)?; - result -} - -async fn init_loop(args: &TUIArgs) -> AResult<()> { - let gui = !args.nogui; - if gui { - WM::lock().await.clear()?; - } - WM::lock().await.push_new::().await; - - thread::spawn(move || { - while is_running() { - if event::poll(EVENT_GEN_PERIOD).unwrap() { - match event::read().unwrap() { - Event::Key(key) => { - GENERAL_EVENT_CHANNEL.send(GEvent::Key(key.code)); - GENERAL_EVENT_CHANNEL.send(GEvent::Tick); - } - _ => (), - } - } else { - GENERAL_EVENT_CHANNEL.send(GEvent::Tick) - } - } - }); - - while is_running() { - match GENERAL_EVENT_CHANNEL.recv() { - GEvent::Tick => { - let mut wh = WM::lock().await; - wh.update().await?; - if gui { - wh.draw().await?; - } - } - GEvent::CloseWnd { wid, force } => { - WM::lock().await.close(wid, force).await; - } - GEvent::Key(key) => { - info!(?key, "pressed"); - if let KeyCode::Char('q') = key { - break_global(); - } else { - WM::lock().await.send_handle_kbd(key).await; - } - } - GEvent::CreateWnd(wnd) => { - WM::lock().await.push_dyn(wnd).await; - } - GEvent::Exit => { - break_global(); - } - } - } - Ok(()) -} - -#[inline] -fn is_running() -> bool { - ACTIVE_LOOP.load(Ordering::Relaxed) -} - -fn break_global() { - ACTIVE_LOOP.store(false, Ordering::Relaxed); -} - -fn init_signal_handlers(gui: bool) { - use signal_hook::{ - consts::{SIGINT, SIGTERM}, - iterator::Signals, - }; - - thread::spawn(move || { - let mut signals = Signals::new(&[SIGINT, SIGTERM]).unwrap(); - for sig in signals.forever() { - match sig { - SIGINT => break_global(), - SIGTERM => { - break_global(); - term_teardown(gui).ok(); - exit(3); - } - _ => (), - } - } - }); -} - -fn init_logger() { - use tracing_appender::rolling::{RollingFileAppender, Rotation}; - use tracing_subscriber::EnvFlter; - - if env::var("RUST_LOG").is_err() { - env::set_var("RUST_LOG", "info") - } - - tracing_subscriber::fmt::fmt() - .with_env_filter(EnvFilter::from_default_env()) - .with_writer(|| RollingFileAppender::new(Rotation::NEVER, ".", "u_panel.log")) - .init(); -} - -fn init_panic_handler(gui: bool) { - set_hook(Box::new(move |panic_info| { - term_teardown(gui).ok(); - eprintln!("CRITICAL PANIK ENCOUNTRD OMFG!11! go see logz lul"); - error!("{}\n{:?}", panic_info, Backtrace::new()); - exit(254); - })); -} - -fn term_setup(gui: bool) -> AResult<()> { - enable_raw_mode()?; - if gui { - execute!(stdout(), EnterAlternateScreen, EnableMouseCapture)?; - } - Ok(()) -} - -fn term_teardown(gui: bool) -> AResult<()> { - disable_raw_mode()?; - if gui { - get_terminal()?.show_cursor()?; - execute!(stdout(), LeaveAlternateScreen, DisableMouseCapture)?; - } - Ok(()) -} diff --git a/bin/u_panel/src/tui/utils.rs b/bin/u_panel/src/tui/utils.rs deleted file mode 100644 index 6b508be..0000000 --- a/bin/u_panel/src/tui/utils.rs +++ /dev/null @@ -1,58 +0,0 @@ -use async_channel::{ - unbounded as unbounded_async, Receiver as AsyncReceiver, RecvError, SendError, - Sender as AsyncSender, -}; -use crossbeam::channel::{unbounded, Receiver, Sender}; - -pub struct Channel { - pub tx: Sender, - pub rx: Receiver, -} - -impl Channel { - pub fn new() -> Self { - let (tx, rx) = unbounded::(); - Self { tx, rx } - } - - pub fn send(&self, msg: T) { - self.tx.send(msg).unwrap() - } - - pub fn recv(&self) -> T { - self.rx.recv().unwrap() - } -} - -impl Default for Channel { - fn default() -> Self { - Channel::new() - } -} - -#[derive(Clone)] -pub struct AsyncChannel { - pub tx: AsyncSender, - pub rx: AsyncReceiver, -} - -impl AsyncChannel { - pub fn new() -> Self { - let (tx, rx) = unbounded_async::(); - Self { tx, rx } - } - - pub async fn send(&self, msg: T) -> Result<(), SendError> { - self.tx.send(msg).await - } - - pub async fn recv(&self) -> Result { - self.rx.recv().await - } -} - -impl Default for AsyncChannel { - fn default() -> Self { - AsyncChannel::new() - } -} diff --git a/bin/u_panel/src/tui/windows/confirm.rs b/bin/u_panel/src/tui/windows/confirm.rs deleted file mode 100644 index 319c10c..0000000 --- a/bin/u_panel/src/tui/windows/confirm.rs +++ /dev/null @@ -1,127 +0,0 @@ -use super::{ReturnVal, Window, WndId}; -use crate::tui::Frame; -use anyhow::Result as AResult; -use crossterm::event::KeyCode; -use tui::layout::{Alignment, Constraint, Direction, Layout, Rect}; -use tui::style::{Color, Modifier, Style}; -use tui::widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph}; - -#[derive(Default)] -pub struct ConfirmWnd { - pub id: WndId, - msg: String, - variants: Vec, - state: ListState, -} - -impl ConfirmWnd { - pub fn new(msg: impl Into, variants: &[&str]) -> Self { - let variants = if !variants.is_empty() { - variants - } else { - &["Yes", "No"] - }; - let mut this = Self { - msg: msg.into(), - variants: variants.into_iter().map(|s| s.to_string()).collect(), - ..Default::default() - }; - this.state.select(Some(0)); - this - } - - pub fn on_right(&mut self) { - let selected = self.state.selected().unwrap_or(0); - self.state - .select(Some((selected + 1).rem_euclid(self.variants.len()))); - } - - pub fn on_left(&mut self) { - let selected = self.state.selected().unwrap_or(0); - let vars_len = self.variants.len(); - self.state - .select(Some((selected + vars_len - 1).rem_euclid(vars_len))); - } -} - -#[async_trait] -impl Window for ConfirmWnd { - async fn handle_kbd(&mut self, k: KeyCode) -> AResult<()> { - match k { - KeyCode::Right => self.on_right(), - KeyCode::Left => self.on_left(), - KeyCode::Enter => self.close(false, true).await, - KeyCode::Esc => self.close(false, false).await, - _ => (), - } - Ok(()) - } - - async fn handle_update(&mut self) -> AResult<()> { - Ok(()) - } - - async fn handle_close(&mut self) -> bool { - true - } - - fn draw(&mut self, f: &mut Frame) { - let size = f.size(); - let rect = centered_rect(60, 40, size); - let popup = Block::default().title("Popup").borders(Borders::ALL); - f.render_widget(Clear, rect); - f.render_widget(popup, rect); - - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints(vec![Constraint::Percentage(40), Constraint::Percentage(30)]) - .split(rect); - let msg = Paragraph::new(self.msg.as_ref()); - f.render_widget(msg, chunks[0]); - - let options = self - .variants - .iter() - .map(AsRef::as_ref) - .map(ListItem::new) - .collect::>(); - let list = - List::new(options).highlight_style(Style::default().bg(Color::Gray).fg(Color::Black)); - f.render_stateful_widget(list, chunks[1], &mut self.state); - } - - fn retval(&self) -> ReturnVal { - let value = self - .variants - .get(self.state.selected().unwrap()) - .unwrap() - .to_owned(); - ReturnVal::String(value) - } -} - -fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { - let popup_layout = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Percentage((100 - percent_y) / 2), - Constraint::Percentage(percent_y), - Constraint::Percentage((100 - percent_y) / 2), - ] - .as_ref(), - ) - .split(r); - - Layout::default() - .direction(Direction::Horizontal) - .constraints( - [ - Constraint::Percentage((100 - percent_x) / 2), - Constraint::Percentage(percent_x), - Constraint::Percentage((100 - percent_x) / 2), - ] - .as_ref(), - ) - .split(popup_layout[1])[1] -} diff --git a/bin/u_panel/src/tui/windows/main_wnd.rs b/bin/u_panel/src/tui/windows/main_wnd.rs deleted file mode 100644 index d6494ee..0000000 --- a/bin/u_panel/src/tui/windows/main_wnd.rs +++ /dev/null @@ -1,325 +0,0 @@ -use super::{ConfirmWnd, ReturnVal, Window}; -use crate::tui::{impls::CRUD, windows::WndId, Frame}; -use anyhow::Result as AResult; -use crossterm::event::KeyCode; -use std::{fmt::Display, str::FromStr}; -use strum::VariantNames; -use tokio::join; -use tui::widgets::ListState; -use u_lib::models::{Agent, AssignedJob, JobMeta}; -use uuid::Uuid; - -use tui::layout::{Constraint, Direction, Layout}; -use tui::style::{Color, Modifier, Style}; -use tui::text::Spans; -use tui::widgets::{Block, Borders, List, ListItem, Tabs}; - -#[derive(strum::Display, strum::EnumVariantNames, strum::EnumString)] -pub enum UiTabs { - Agents, - Jobs, - Map, -} - -impl UiTabs { - pub fn variants() -> &'static [&'static str] { - Self::VARIANTS - } - - pub fn index(&self) -> usize { - let ss = self.to_string(); - Self::VARIANTS.iter().position(|el| **el == ss).unwrap() - } - - pub fn next(&self) -> Self { - let next_idx = (self.index() + 1).rem_euclid(Self::VARIANTS.len()); - Self::from_str(Self::VARIANTS[next_idx]).unwrap() - } - - pub fn prev(&self) -> Self { - let vlen = Self::VARIANTS.len(); - let next_idx = (self.index() + vlen - 1).rem_euclid(vlen); - Self::from_str(Self::VARIANTS[next_idx]).unwrap() - } -} - -pub struct StatefulList { - pub updated: bool, - pub inner: Vec, - pub state: ListState, -} - -impl StatefulList { - pub async fn update(&mut self) -> AResult<()> { - if !self.updated { - let new_values = T::read().await?; - self.inner = new_values; - self.updated = true; - } - Ok(()) - } - - pub async fn delete(&mut self) -> AResult<()> { - if let Some(s) = self.state.selected() { - let uid = self.inner[s].id(); - T::delete(uid).await?; - } - Ok(()) - } - - pub fn get(&self, id: Uuid) -> Option<&T> { - for item in self.inner.iter() { - if item.id() == id { - return Some(item); - } - } - None - } -} - -impl Default for StatefulList { - fn default() -> Self { - let mut state = ListState::default(); - state.select(Some(0)); - StatefulList { - updated: false, - inner: vec![], - state, - } - } -} - -pub struct MainWnd { - pub id: WndId, - pub active_tab: UiTabs, - pub last_error: Option, - pub agents: StatefulList, - pub jobs: StatefulList, - pub map: StatefulList, -} - -impl Default for MainWnd { - fn default() -> Self { - MainWnd { - active_tab: UiTabs::Agents, - last_error: None, - agents: Default::default(), - jobs: Default::default(), - map: Default::default(), - id: Default::default(), - } - } -} - -impl MainWnd { - pub fn next_tab(&mut self) { - self.active_tab = self.active_tab.next() - } - - pub fn prev_tab(&mut self) { - self.active_tab = self.active_tab.prev() - } - - fn check_err(&mut self, res: AResult<()>) -> bool { - if let Err(e) = res { - self.last_error = Some(e.to_string()); - true - } else { - false - } - } - - pub async fn check_updates(&mut self) { - if !self.agents.updated || !self.jobs.updated || !self.map.updated { - let state = self.tab_list_state(); - if let None = state.selected() { - state.select(Some(0)) - } - - let (a, j, m) = join! { - self.agents.update(), - self.jobs.update(), - self.map.update() - }; - for res in [a, j, m] { - self.check_err(res); - } - } - } - - pub fn tab_data(&self) -> Vec { - match self.active_tab { - UiTabs::Agents => self - .agents - .inner - .iter() - .map(|i| format!("{}: {}-{}", crop(i.id, 6), i.username, i.hostname)) - .collect(), - UiTabs::Jobs => self - .jobs - .inner - .iter() - .map(|i| format!("{}: {}", crop(i.id, 6), i.alias.clone().unwrap_or_default())) - .collect(), - UiTabs::Map => self - .map - .inner - .iter() - .map(|i| { - let job = self.jobs.get(i.job_id).unwrap(); - let job_id = crop(i.job_id, 6); - let job_ident = if let Some(alias) = job.alias.as_ref() { - format!("{} ({})", alias, job_id) - } else { - format!("{}", job_id) - }; - let agent = self.agents.get(i.agent_id).unwrap(); - let agent_id = crop(i.agent_id, 6); - let agent_ident = if let Some(alias) = agent.alias.as_ref() { - format!("{} ({})", alias, agent_id) - } else { - format!("{}-{} ({})", agent.username, agent.hostname, agent_id) - }; - format!("{}: {} for {}", crop(i.id, 6), job_ident, agent_ident) - }) - .collect(), - } - } - - pub fn tab_list_state(&mut self) -> &mut ListState { - match self.active_tab { - UiTabs::Agents => &mut self.agents.state, - UiTabs::Jobs => &mut self.jobs.state, - UiTabs::Map => &mut self.map.state, - } - } - - pub fn update_tab(&mut self) { - match self.active_tab { - UiTabs::Agents => { - self.agents.updated = false; - self.jobs.updated = false; - self.map.updated = false; - } - UiTabs::Jobs => self.jobs.updated = false, - UiTabs::Map => self.map.updated = false, - } - } - - pub fn on_down(&mut self) { - let (list_len, list_state) = match self.active_tab { - UiTabs::Agents => (self.agents.inner.len(), &mut self.agents.state), - UiTabs::Jobs => (self.jobs.inner.len(), &mut self.jobs.state), - UiTabs::Map => (self.map.inner.len(), &mut self.map.state), - }; - if list_len == 0 { - list_state.select(None); - } else { - let selected = list_state.selected().unwrap_or(list_len - 1); - list_state.select(Some((selected + 1).rem_euclid(list_len))); - } - } - - pub fn on_up(&mut self) { - let (list_len, list_state) = match self.active_tab { - UiTabs::Agents => (self.agents.inner.len(), &mut self.agents.state), - UiTabs::Jobs => (self.jobs.inner.len(), &mut self.jobs.state), - UiTabs::Map => (self.map.inner.len(), &mut self.map.state), - }; - if list_len == 0 { - list_state.select(None); - } else { - let selected = list_state.selected().unwrap_or(1); - list_state.select(Some((selected + list_len - 1).rem_euclid(list_len))); - } - } - - pub async fn delete(&mut self) { - let res = match self.active_tab { - UiTabs::Agents => self.agents.delete().await, - UiTabs::Jobs => self.jobs.delete().await, - UiTabs::Map => self.map.delete().await, - }; - if !self.check_err(res) { - self.on_up(); - } - } -} - -#[async_trait] -impl Window for MainWnd { - async fn handle_kbd(&mut self, k: KeyCode) -> AResult<()> { - match k { - KeyCode::Esc => self.close(false, false).await, - KeyCode::Left => self.prev_tab(), - KeyCode::Right => self.next_tab(), - KeyCode::Up => self.on_up(), - KeyCode::Down => self.on_down(), - KeyCode::Delete => { - if let ReturnVal::String(ref s) = - ConfirmWnd::new("Delete?", &["Yes", "No"]).wait_retval() - { - if s == "Yes" { - self.delete().await; - self.update_tab(); - } - } - } - KeyCode::F(5) => self.update_tab(), - _ => (), - }; - Ok(()) - } - - async fn handle_update(&mut self) -> AResult<()> { - self.check_updates().await; - Ok(()) - } - - async fn handle_close(&mut self) -> bool { - true - } - - fn draw(&mut self, f: &mut Frame) { - let size = f.size(); - let chunks = Layout::default() - .direction(Direction::Vertical) - .margin(1) - .constraints([Constraint::Length(3), Constraint::Min(0)].as_ref()) - .split(size); - let titles = UiTabs::variants() - .iter() - .cloned() - .map(Spans::from) - .collect(); - let tabs = Tabs::new(titles) - .block( - Block::default() - .title("The whole that you need to know") - .borders(Borders::ALL), - ) - .style(Style::default().fg(Color::White)) - .highlight_style(Style::default().fg(Color::Yellow)) - .divider("-") - .select(self.active_tab.index()); - f.render_widget(tabs, chunks[0]); - - let tab_data = self - .tab_data() - .into_iter() - .map(ListItem::new) - .collect::>(); - let list = List::new(tab_data) - .block(Block::default().borders(Borders::ALL)) - .highlight_style(Style::default().add_modifier(Modifier::BOLD)); - f.render_stateful_widget(list, chunks[1], self.tab_list_state()); - } - - fn retval(&self) -> ReturnVal { - ReturnVal::None - } -} - -fn crop(data: T, retain: usize) -> String { - data.to_string()[..retain].to_string() -} diff --git a/bin/u_panel/src/tui/windows/mod.rs b/bin/u_panel/src/tui/windows/mod.rs deleted file mode 100644 index ec86b3f..0000000 --- a/bin/u_panel/src/tui/windows/mod.rs +++ /dev/null @@ -1,223 +0,0 @@ -mod confirm; -mod main_wnd; -pub use confirm::ConfirmWnd; -pub use main_wnd::MainWnd; - -use crate::tui::{ - get_terminal, impls::Id, AsyncChannel, Backend, Channel, Frame, GEvent, GENERAL_EVENT_CHANNEL, -}; -use anyhow::Result as AResult; -use crossterm::event::KeyCode; -use once_cell::sync::{Lazy, OnceCell}; -use std::collections::BTreeMap; -use std::sync::{Arc, Mutex as StdMutex, MutexGuard as StdMutexGuard}; -use tokio::sync::{Mutex, MutexGuard}; -use tokio::task::{self, JoinHandle}; -use tui::Terminal; - -static WINDOWS: Lazy>> = - Lazy::new(|| Arc::new(Mutex::new(WM::new(get_terminal().unwrap())))); - -static LAST_WND_ID: OnceCell> = OnceCell::new(); - -static RETVAL: Lazy> = Lazy::new(|| Channel::new()); - -pub type SharedWnd = Arc>; - -pub enum ReturnVal { - String(String), - None, -} - -#[derive(Clone, Debug)] -pub enum WndEvent { - Key(KeyCode), -} - -#[derive(PartialEq, PartialOrd, Eq, Ord, Hash, Copy, Clone, Debug)] -pub struct WndId(u64); - -impl WndId { - fn last_wid() -> StdMutexGuard<'static, WndId> { - LAST_WND_ID - .get_or_init(|| StdMutex::new(WndId(0))) - .lock() - .unwrap() - } -} - -impl Default for WndId { - fn default() -> Self { - let mut wid = Self::last_wid(); - wid.0 += 1; - *wid - } -} - -#[async_trait] -pub trait Window: Id + Send { - async fn handle_event(&mut self, ev: WndEvent) { - match ev { - WndEvent::Key(k) => { - self.handle_kbd(k).await.unwrap(); - } - } - } - - async fn close(&mut self, force: bool, save_retval: bool) { - let rv = if save_retval { - self.retval() - } else { - ReturnVal::None - }; - RETVAL.send(rv); - GENERAL_EVENT_CHANNEL.send(GEvent::CloseWnd { - wid: self.id(), - force, - }); - } - - fn retval(&self) -> ReturnVal; - - fn wait_retval(self) -> ReturnVal - where - Self: Sized + 'static, - { - GENERAL_EVENT_CHANNEL.send(GEvent::CreateWnd(Arc::new(Mutex::new(self)))); - RETVAL.recv() - } - - async fn handle_update(&mut self) -> AResult<()>; - - async fn handle_close(&mut self) -> bool; - - async fn handle_kbd(&mut self, k: KeyCode) -> AResult<()>; - - fn draw(&mut self, f: &mut Frame); -} - -pub struct WndLoop { - chan: AsyncChannel, - wnd_loop: JoinHandle<()>, - window: SharedWnd, -} - -impl WndLoop { - pub async fn with_wnd(window: SharedWnd) -> Self { - let wnd = window.clone(); - let chan = AsyncChannel::::new(); - let ch = chan.clone(); - let wnd_loop = async move { - loop { - match ch.recv().await { - Ok(ev) => wnd.lock().await.handle_event(ev).await, - Err(_) => break, - } - } - }; - WndLoop { - chan, - window, - wnd_loop: task::spawn(wnd_loop), - } - } - - pub async fn send(&self, ev: WndEvent) { - let wnd_id = self.window.lock().await.id(); - let event = ev.clone(); - debug!(?event, ?wnd_id, "sending"); - self.chan.send(ev).await.expect("send failed"); - } -} - -pub struct WM { - queue: BTreeMap, - term: Terminal, -} - -impl WM { - fn get_last_wnd(&self) -> &WndLoop { - let last_id = self.queue.keys().rev().next().expect("No windows found"); - self.queue.get(last_id).unwrap() - } - - pub async fn lock<'l>() -> MutexGuard<'l, Self> { - WINDOWS.lock().await - } - - fn new(term: Terminal) -> Self { - Self { - term, - queue: BTreeMap::new(), - } - } - - pub async fn push(&mut self, window: W) -> SharedWnd { - self.push_dyn(Arc::new(Mutex::new(window))).await - } - - pub async fn push_new(&mut self) -> SharedWnd { - self.push(W::default()).await - } - - pub async fn push_dyn(&mut self, window: SharedWnd) -> SharedWnd { - let wid = window.lock().await.id(); - self.queue - .insert(wid, WndLoop::with_wnd(window.clone()).await); - window - } - - pub fn clear(&mut self) -> AResult<()> { - self.term.clear()?; - Ok(()) - } - - pub async fn draw(&mut self) -> AResult<()> { - for wid in self.queue.keys().collect::>() { - let mut wnd_locked = match self.queue.get(&wid) { - Some(w) => match w.window.try_lock() { - Ok(w) => w, - Err(_) => { - warn!("Can't lock window {:?}", wid); - continue; - } - }, - None => { - warn!("Can't redraw window {:?}, not found", wid); - continue; - } - }; - self.term.draw(move |f| wnd_locked.draw(f))?; - } - Ok(()) - } - - pub async fn close(&mut self, wid: WndId, force: bool) { - let wnd = match self.queue.get(&wid) { - Some(w) => w.clone(), - None => { - warn!("Can't close window {:?}, not found", wid); - return; - } - }; - if wnd.window.lock().await.handle_close().await || force { - let WndLoop { wnd_loop, .. } = self.queue.remove(&wid).unwrap(); - wnd_loop.abort(); - } - - if self.queue.is_empty() { - GENERAL_EVENT_CHANNEL.send(GEvent::Exit); - } - } - - pub async fn send_handle_kbd(&self, k: KeyCode) { - let current_wnd = self.get_last_wnd(); - current_wnd.send(WndEvent::Key(k)).await; - } - - pub async fn update(&mut self) -> AResult<()> { - let current_wnd = self.get_last_wnd(); - current_wnd.window.lock().await.handle_update().await?; - Ok(()) - } -} diff --git a/bin/u_server/src/db.rs b/bin/u_server/src/db.rs index e18824f..5bee420 100644 --- a/bin/u_server/src/db.rs +++ b/bin/u_server/src/db.rs @@ -42,14 +42,14 @@ impl UDB { .unwrap() } - pub fn insert_jobs(&self, job_metas: &[JobMeta]) -> Result<()> { + pub fn insert_jobs(&self, job_metas: &[JobMeta]) -> Result> { use schema::jobs; diesel::insert_into(jobs::table) .values(job_metas) - .execute(&self.conn) - .map_err(with_err_ctx("Can't insert jobs"))?; - Ok(()) + .get_results(&self.conn) + .map(|rows| rows.iter().map(|job: &JobMeta| job.id).collect()) + .map_err(with_err_ctx("Can't insert jobs")) } pub fn get_jobs(&self, ouid: Option) -> Result> { diff --git a/bin/u_server/src/handlers.rs b/bin/u_server/src/handlers.rs index eb890e4..c0907c9 100644 --- a/bin/u_server/src/handlers.rs +++ b/bin/u_server/src/handlers.rs @@ -35,7 +35,8 @@ impl Endpoints { let db = UDB::lock_db(); let mut agents = db.get_agents(Some(uid))?; if agents.is_empty() { - db.insert_agent(&Agent::with_id(uid))?; + let new_agent = Agent::with_id(uid); + db.insert_agent(&new_agent)?; let job = db .find_job_by_alias("agent_hello")? .expect("agent_hello job not found"); @@ -53,7 +54,7 @@ impl Endpoints { Ok(result) } - pub async fn upload_jobs(msg: BaseMessage<'static, Vec>) -> EndpResult<()> { + pub async fn upload_jobs(msg: BaseMessage<'static, Vec>) -> EndpResult> { UDB::lock_db() .insert_jobs(&msg.into_inner()) .map_err(From::from) diff --git a/bin/u_server/src/u_server.rs b/bin/u_server/src/u_server.rs index 70a5b9f..4a6d2db 100644 --- a/bin/u_server/src/u_server.rs +++ b/bin/u_server/src/u_server.rs @@ -70,7 +70,7 @@ pub fn init_endpoints( let upload_jobs = path("upload_jobs") .and(get_content::>()) .and_then(Endpoints::upload_jobs) - .map(ok); + .map(into_message); let get_jobs = path("get_jobs") .and( @@ -111,17 +111,17 @@ pub fn init_endpoints( .and_then(Endpoints::report) .map(ok); - let update_agent = path("update_item") + let update_agent = path("update_agent") .and(get_content::()) .and_then(Endpoints::update_agent) .map(ok); - let update_job = path("update_item") + let update_job = path("update_job") .and(get_content::()) .and_then(Endpoints::update_job) .map(ok); - let update_assigned_job = path("update_item") + let update_assigned_job = path("update_result") .and(get_content::()) .and_then(Endpoints::update_assigned_job) .map(ok); @@ -142,9 +142,7 @@ pub fn init_endpoints( .or(del) .or(set_jobs) .or(get_agent_jobs) - .or(update_agent) - .or(update_job) - .or(update_assigned_job) + .or(update_agent.or(update_job).or(update_assigned_job)) .or(download) .or(ping)) .and(auth_header); @@ -163,7 +161,7 @@ pub fn preload_jobs() -> Result<(), ServerError> { .with_alias(job_alias) .build() .unwrap(); - UDB::lock_db().insert_jobs(&[agent_hello])? + UDB::lock_db().insert_jobs(&[agent_hello])?; } Ok(()) } diff --git a/images/musl-libs.Dockerfile b/images/musl-libs.Dockerfile index 2441b80..c2e0b01 100644 --- a/images/musl-libs.Dockerfile +++ b/images/musl-libs.Dockerfile @@ -38,7 +38,7 @@ RUN apt-get update && apt-get install -y \ # This helps continuing manually if anything breaks. ENV SSL_VER="1.0.2u" \ CURL_VER="7.77.0" \ - ZLIB_VER="1.2.11" \ + ZLIB_VER="1.2.13" \ PQ_VER="11.12" \ SQLITE_VER="3350500" \ CC=musl-gcc \ diff --git a/integration/integration_tests.py b/integration/integration_tests.py index e2b2028..512ea98 100644 --- a/integration/integration_tests.py +++ b/integration/integration_tests.py @@ -17,7 +17,7 @@ def fail(msg): def usage_exit(): usage = f"""Usage: - python {__file__.split('/')[-1]} [--rebuild] [--preserve] [--no-run]""" + python {__file__.split('/')[-1]} [--rebuild] [--preserve] [--no-run] [--down]""" fail(usage) @@ -32,13 +32,14 @@ def create_integration_workspace(): def run_tests(): - allowed_args = set(["--rebuild", "--preserve", "--no-run", "--release"]) + allowed_args = set(["--rebuild", "--preserve", "--no-run", "--release", "--down"]) args = sys.argv[1:] if not set(args).issubset(allowed_args): usage_exit() force_rebuild = '--rebuild' in args preserve_containers = '--preserve' in args only_setup_cluster = '--no-run' in args + down_cluster = "--down" in args def _cleanup(): if not preserve_containers and not only_setup_cluster: @@ -49,6 +50,10 @@ def run_tests(): warn(f'Received signal: {s}, gracefully stopping...') _cleanup() + if down_cluster: + _cleanup() + return + for s in (signal.SIGTERM, signal.SIGINT, signal.SIGHUP): signal.signal(s, abort_handler) rebuild_images_if_needed(force_rebuild) diff --git a/integration/tests/helpers/panel.rs b/integration/tests/helpers/panel.rs index 4caecb2..f8fa576 100644 --- a/integration/tests/helpers/panel.rs +++ b/integration/tests/helpers/panel.rs @@ -18,7 +18,7 @@ impl Panel { pub fn output_argv(argv: &[&str]) -> PanelResult { let result = Self::run(argv); - let output = ProcOutput::from_output(&result).to_appropriate(); + let output = ProcOutput::from_output(&result).into_vec(); from_slice(&output) .map_err(|e| { eprintln!( diff --git a/integration/tests/integration/behaviour.rs b/integration/tests/integration/behaviour.rs index 6472505..932cb03 100644 --- a/integration/tests/integration/behaviour.rs +++ b/integration/tests/integration/behaviour.rs @@ -31,7 +31,7 @@ async fn test_setup_tasks() -> TestResult { }; let job_alias = "passwd_contents"; let job = json!( - {"alias": job_alias, "payload": b"cat /etc/passwd" } + {"alias": job_alias, "payload": b"cat /etc/passwd", "argv": "/bin/bash {}" } ); let cmd = format!("jobs create '{}'", to_string(&job).unwrap()); Panel::check_status(cmd); diff --git a/lib/u_lib/src/api.rs b/lib/u_lib/src/api.rs index b64913c..28463d4 100644 --- a/lib/u_lib/src/api.rs +++ b/lib/u_lib/src/api.rs @@ -116,13 +116,23 @@ impl ClientHandler { .await } - /// update something - pub async fn update_item(&self, item: impl AsMsg + Debug) -> Result<()> { - self.req_with_payload("update_item", item).await + /// update agent + pub async fn update_agent(&self, agent: models::Agent) -> Result<()> { + self.req_with_payload("update_agent", agent).await + } + + /// update job + pub async fn update_job(&self, job: models::JobMeta) -> Result<()> { + self.req_with_payload("update_job", job).await + } + + /// update result + pub async fn update_result(&self, result: models::AssignedJob) -> Result<()> { + self.req_with_payload("update_result", result).await } /// create and upload job - pub async fn upload_jobs(&self, payload: impl OneOrVec) -> Result<()> { + pub async fn upload_jobs(&self, payload: impl OneOrVec) -> Result> { self.req_with_payload("upload_jobs", payload.into_vec()) .await } diff --git a/lib/u_lib/src/datatypes.rs b/lib/u_lib/src/datatypes.rs index f7d09a6..25b954a 100644 --- a/lib/u_lib/src/datatypes.rs +++ b/lib/u_lib/src/datatypes.rs @@ -8,3 +8,19 @@ pub enum PanelResult { Ok(M), Err(UError), } + +impl PanelResult { + pub fn is_ok(&self) -> bool { + matches!(self, PanelResult::Ok(_)) + } + + pub fn is_err(&self) -> bool { + matches!(self, PanelResult::Err(_)) + } +} + +impl ToString for PanelResult { + fn to_string(&self) -> String { + serde_json::to_string(self).unwrap() + } +} diff --git a/lib/u_lib/src/models/agent.rs b/lib/u_lib/src/models/agent.rs index 61caf8d..ba28da5 100644 --- a/lib/u_lib/src/models/agent.rs +++ b/lib/u_lib/src/models/agent.rs @@ -3,18 +3,13 @@ use diesel::{AsChangeset, Identifiable, Insertable, Queryable}; #[cfg(feature = "server")] use diesel_derive_enum::DbEnum; use serde::{Deserialize, Serialize}; -use std::{fmt, time::SystemTime}; +use std::time::SystemTime; use strum::Display; #[cfg(feature = "server")] use crate::models::schema::*; -use crate::{ - config::get_self_uid, - executor::ExecResult, - runner::NamedJobRunner, - utils::{systime_to_string, Platform}, -}; +use crate::{config::get_self_uid, executor::ExecResult, runner::NamedJobRunner, utils::Platform}; use uuid::Uuid; @@ -55,22 +50,6 @@ pub struct Agent { pub username: String, } -impl fmt::Display for Agent { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Agent: {}", self.id)?; - if let Some(ref alias) = self.alias { - write!(f, " ({})", alias)? - } - writeln!(f, "\nUsername: {}", self.username)?; - writeln!(f, "Hostname: {}", self.hostname)?; - writeln!(f, "Is root: {}", self.is_root)?; - writeln!(f, "Root allowed: {}", self.is_root_allowed)?; - writeln!(f, "Last active: {}", systime_to_string(&self.last_active))?; - writeln!(f, "Platform: {}", self.platform)?; - writeln!(f, "State: {}", self.state) - } -} - #[cfg(not(target_arch = "wasm32"))] impl Agent { pub fn with_id(uid: Uuid) -> Self { @@ -87,7 +66,7 @@ impl Agent { #[cfg(unix)] pub async fn gather() -> Self { let mut builder = NamedJobRunner::from_shell(vec![ - ("hostname", "hostnamectl hostname"), + ("hostname", "uname -a"), ("host_info", "hostnamectl --json=pretty"), ("is_root", "id -u"), ("username", "id -un"), @@ -96,7 +75,7 @@ impl Agent { .wait() .await; let decoder = - |job_result: ExecResult| job_result.unwrap().to_string_result().trim().to_string(); + |job_result: ExecResult| job_result.unwrap().to_str_result().trim().to_string(); Self { hostname: decoder(builder.pop("hostname")), @@ -133,14 +112,3 @@ impl Default for Agent { } } } - -// #[cfg(test)] -// mod tests { -// use super::*; - -// #[tokio::test] -// async fn test_gather() { -// let cli_info = Agent::gather().await; -// assert_eq!(cli_info.alias, None) -// } -// } diff --git a/lib/u_lib/src/models/jobs/assigned.rs b/lib/u_lib/src/models/jobs/assigned.rs index 78850a4..112671d 100644 --- a/lib/u_lib/src/models/jobs/assigned.rs +++ b/lib/u_lib/src/models/jobs/assigned.rs @@ -1,12 +1,12 @@ use super::{JobMeta, JobState, JobType}; +#[cfg(not(target_arch = "wasm32"))] +use crate::config::get_self_uid; #[cfg(feature = "server")] use crate::models::schema::*; -#[cfg(not(target_arch = "wasm32"))] -use crate::{config::get_self_uid, utils::ProcOutput}; #[cfg(feature = "server")] use diesel::{Identifiable, Insertable, Queryable}; use serde::{Deserialize, Serialize}; -use std::time::SystemTime; +use std::{borrow::Cow, time::SystemTime}; use uuid::Uuid; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] @@ -87,26 +87,16 @@ impl Default for AssignedJob { } } -#[cfg(not(target_arch = "wasm32"))] impl AssignedJob { - pub fn as_job_output(&self) -> Option { - self.result - .as_ref() - .and_then(|r| ProcOutput::from_combined(r)) - } - - pub fn to_raw_result(&self) -> Vec { + pub fn to_raw_result(&self) -> &[u8] { match self.result.as_ref() { - Some(r) => match ProcOutput::from_combined(r) { - Some(o) => o.to_appropriate(), - None => r.clone(), - }, - None => b"No data".to_vec(), + Some(r) => r, + None => b"No data yet", } } - pub fn to_string_result(&self) -> String { - String::from_utf8_lossy(&self.to_raw_result()).into_owned() + pub fn to_str_result(&self) -> Cow<'_, str> { + String::from_utf8_lossy(self.to_raw_result()) } pub fn set_result(&mut self, result: &S) { diff --git a/lib/u_lib/src/models/jobs/meta.rs b/lib/u_lib/src/models/jobs/meta.rs index 8150313..c8980b7 100644 --- a/lib/u_lib/src/models/jobs/meta.rs +++ b/lib/u_lib/src/models/jobs/meta.rs @@ -51,8 +51,8 @@ impl JobMeta { JobMetaBuilder::default() } - pub fn into_builder(self) -> JobMetaBuilder { - JobMetaBuilder { inner: self } + pub fn validated(self) -> UResult { + JobMetaBuilder { inner: self }.build() } pub fn from_shell(cmd: impl Into) -> UResult { @@ -113,7 +113,7 @@ impl JobMetaBuilder { JobType::Shell => { if inner.argv.is_empty() { // TODO: fix detecting - inner.argv = String::from("/bin/bash -c {}") + inner.argv = String::from("echo 'hello, world!'") } let argv_parts = shlex::split(&inner.argv).ok_or(UError::JobArgsError("Shlex failed".into()))?; @@ -127,7 +127,7 @@ impl JobMetaBuilder { inner.payload = Some(data) } match inner.payload.as_ref() { - Some(_) => { + Some(p) if p.len() > 0 => { if !inner.argv.contains("{}") { return Err(UError::JobArgsError( "Argv contains no executable placeholder".into(), @@ -144,6 +144,7 @@ impl JobMetaBuilder { .into()); } } + _ => (), }; if !Platform::new(&inner.platform).check() { return Err(UError::JobArgsError(format!( diff --git a/lib/u_lib/src/models/schema.rs b/lib/u_lib/src/models/schema.rs index 1b47bc7..11da1d0 100644 --- a/lib/u_lib/src/models/schema.rs +++ b/lib/u_lib/src/models/schema.rs @@ -1,4 +1,6 @@ -table! { +// @generated automatically by Diesel CLI. + +diesel::table! { use crate::schema_exports::*; agents (id) { @@ -19,7 +21,7 @@ table! { } } -table! { +diesel::table! { use crate::schema_exports::*; certificates (id) { @@ -29,7 +31,7 @@ table! { } } -table! { +diesel::table! { use crate::schema_exports::*; jobs (id) { @@ -44,7 +46,7 @@ table! { } } -table! { +diesel::table! { use crate::schema_exports::*; results (id) { @@ -61,8 +63,8 @@ table! { } } -joinable!(certificates -> agents (agent_id)); -joinable!(results -> agents (agent_id)); -joinable!(results -> jobs (job_id)); +diesel::joinable!(certificates -> agents (agent_id)); +diesel::joinable!(results -> agents (agent_id)); +diesel::joinable!(results -> jobs (job_id)); -allow_tables_to_appear_in_same_query!(agents, certificates, jobs, results,); +diesel::allow_tables_to_appear_in_same_query!(agents, certificates, jobs, results,); diff --git a/lib/u_lib/src/runner.rs b/lib/u_lib/src/runner.rs index 9d5576b..2025abd 100644 --- a/lib/u_lib/src/runner.rs +++ b/lib/u_lib/src/runner.rs @@ -111,13 +111,13 @@ pub async fn run_assigned_job(mut job: AssignedJob) -> ExecResult { let cmd_result = Command::new(cmd).args(args).output().await; let (data, retcode) = match cmd_result { Ok(output) => ( - ProcOutput::from_output(&output).into_combined(), + ProcOutput::from_output(&output).into_vec(), output.status.code(), ), Err(e) => ( ProcOutput::new() .stderr(e.to_string().into_bytes()) - .into_combined(), + .into_vec(), None, ), }; @@ -260,7 +260,7 @@ mod tests { .wait_one() .await .unwrap(); - let result = result.to_string_result(); + let result = result.to_str_result(); assert_eq!(result.trim(), expected_result); Ok(()) } @@ -277,7 +277,7 @@ mod tests { .await .unwrap(); assert_eq!(ls.retcode.unwrap(), 0); - let folders = ls.to_string_result(); + let folders = ls.to_str_result(); let subfolders_jobs: Vec = folders .lines() .map(|f| JobMeta::from_shell(format!("ls {}", f)).unwrap()) @@ -319,7 +319,7 @@ mod tests { .wait_one() .await .unwrap(); - let output = job_result.to_string_result(); + let output = job_result.to_str_result(); assert!(output.contains("No such file")); assert!(job_result.retcode.is_none()); Ok(()) diff --git a/lib/u_lib/src/utils/proc_output.rs b/lib/u_lib/src/utils/proc_output.rs index d060e89..ded026e 100644 --- a/lib/u_lib/src/utils/proc_output.rs +++ b/lib/u_lib/src/utils/proc_output.rs @@ -7,26 +7,12 @@ pub struct ProcOutput { } impl ProcOutput { - const STREAM_BORDER: &'static str = "***"; - const STDOUT: &'static str = "STDOUT"; - const STDERR: &'static str = "STDERR"; - - #[inline] - fn create_delim(header: &'static str) -> Vec { - format!( - "<{border}{head}{border}>", - border = Self::STREAM_BORDER, - head = header - ) - .into_bytes() - } + const STDERR_DELIMETER: &[u8] = b"\n[STDERR]\n"; pub fn from_output(output: &Output) -> Self { - let mut this = Self::new().stdout(output.stdout.to_vec()); - if !output.status.success() && output.stderr.len() > 0 { - this.stderr = output.stderr.to_vec(); - } - this + Self::new() + .stdout(output.stdout.to_vec()) + .stderr(output.stderr.to_vec()) } pub fn new() -> Self { @@ -46,114 +32,73 @@ impl ProcOutput { self } - /// Make bytestring like '<***STDOUT***>...<***STDERR***>...' - pub fn into_combined(self) -> Vec { + pub fn into_vec(self) -> Vec { let mut result: Vec = vec![]; if !self.stdout.is_empty() { - result.extend(Self::create_delim(Self::STDOUT)); result.extend(self.stdout); } if !self.stderr.is_empty() { - result.extend(Self::create_delim(Self::STDERR)); + result.extend(Self::STDERR_DELIMETER); result.extend(self.stderr); } result } - pub fn from_combined(raw: &[u8]) -> Option { - enum ParseFirst { - Stdout, - Stderr, - } - fn split_by_subslice<'s>(slice: &'s [u8], subslice: &[u8]) -> Option<(&'s [u8], &'s [u8])> { - slice - .windows(subslice.len()) - .position(|w| w == subslice) - .map(|split_pos| { - let splitted = slice.split_at(split_pos); - (&splitted.0[..split_pos], &splitted.1[subslice.len()..]) - }) - } - let splitter = |p: ParseFirst| { - let (first_hdr, second_hdr) = match p { - ParseFirst::Stdout => (Self::STDOUT, Self::STDERR), - ParseFirst::Stderr => (Self::STDERR, Self::STDOUT), - }; - let first_hdr = Self::create_delim(first_hdr); - let second_hdr = Self::create_delim(second_hdr); - split_by_subslice(raw, &first_hdr).map(|(_, p2)| { - match split_by_subslice(p2, &second_hdr) { - Some((p2_1, p2_2)) => Self::new().stdout(p2_1.to_vec()).stderr(p2_2.to_vec()), - None => Self::new().stdout(p2.to_vec()), + pub fn from_raw_proc_output(raw: &[u8]) -> Option { + let stderr_delim_len = Self::STDERR_DELIMETER.len(); + raw.windows(stderr_delim_len) + .position(|w| w == Self::STDERR_DELIMETER) + .map(|split_pos| { + let (stdout, stderr) = raw.split_at(split_pos); + let result = Self::new().stdout(stdout.to_vec()); + if stderr.len() <= stderr_delim_len { + result.stderr(stderr[stderr_delim_len..].to_vec()) + } else { + result } }) - }; - splitter(ParseFirst::Stdout).or_else(|| splitter(ParseFirst::Stderr)) - } - - /// Chooses between stdout and stderr or both wisely - pub fn to_appropriate(&self) -> Vec { - let mut result: Vec = vec![]; - let mut altered = false; - if !self.stdout.is_empty() { - result.extend(&self.stdout); - altered = true; - } - if !self.stderr.is_empty() { - if altered { - result.push(b'\n'); - } - result.extend(&self.stderr); - altered = true; - } - if !altered { - result.extend(b""); - } - result } } #[cfg(test)] mod tests { use crate::utils::{bytes_to_string, ProcOutput}; + use std::str; - const STDOUT: &str = "<***STDOUT***>"; - const STDERR: &str = "<***STDERR***>"; + const STDERR_DELIMETER: &'static str = + unsafe { str::from_utf8_unchecked(ProcOutput::STDERR_DELIMETER) }; #[rstest] #[case::stdout_stderr( "lol", "kek", - &format!("{}lol{}kek", STDOUT, STDERR) + &format!("lol{}kek", STDERR_DELIMETER) )] #[case::stderr( "", "kek", - &format!("{}kek", STDERR) + &format!("{}kek", STDERR_DELIMETER) )] fn test_to_combined(#[case] stdout: &str, #[case] stderr: &str, #[case] result: &str) { let output = ProcOutput::new() .stdout(stdout.as_bytes().to_vec()) .stderr(stderr.as_bytes().to_vec()); - assert_eq!(&bytes_to_string(&output.into_combined()), result) + assert_eq!(&bytes_to_string(&output.into_vec()), result) } #[rstest] #[case::stdout_stderr( - &format!("{}lal{}kik", STDOUT, STDERR), - "lal\nkik" + &format!("lal{}kik", STDERR_DELIMETER), )] #[case::stdout( - &format!("{}qeq", STDOUT), - "qeq" + &format!("qeq"), )] #[case::stderr( - &format!("{}vev", STDERR), - "vev" + &format!("{}vev", STDERR_DELIMETER), )] - fn test_from_combined(#[case] src: &str, #[case] result: &str) { - let output = ProcOutput::from_combined(src.as_bytes()).unwrap(); - assert_eq!(bytes_to_string(&output.to_appropriate()).trim(), result); + fn test_from_combined(#[case] src_result: &str) { + let output = ProcOutput::from_raw_proc_output(src_result.as_bytes()).unwrap(); + assert_eq!(bytes_to_string(&output.into_vec()).trim(), src_result); } } diff --git a/scripts/build_musl_libs.sh b/scripts/build_musl_libs.sh index 8c688a5..2ffe66b 100755 --- a/scripts/build_musl_libs.sh +++ b/scripts/build_musl_libs.sh @@ -5,8 +5,8 @@ ARGS=$@ STATIC_LIBS=./static DOCKER_EXCHG=/musl-share IMAGE=unki/musllibs -if [[ ! -d ./static ]]; then - mkdir $STATIC_LIBS +if [[ ! $(find ./static ! -empty -type d) ]]; then + mkdir -p $STATIC_LIBS cd $ROOTDIR/images && docker build -t $IMAGE . -f musl-libs.Dockerfile docker run \ -v $ROOTDIR/$STATIC_LIBS:$DOCKER_EXCHG \
IDJob - {{row.job_id}} + {{row.job_id}} + + | + +