From 9069c71f1805e0148e455755abd12e0faa91e710 Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Sun, 19 Feb 2023 01:43:23 +0900 Subject: [PATCH] fixed #72 --- src-tauri/src/plugins/devmode.rs | 62 ++++++++++++------- src/app/core/services/dev-mode.service.ts | 4 ++ .../core/services/device-manager.service.ts | 40 ++++++++---- src/app/core/services/remote-luna.service.ts | 21 ++++++- src/app/debug/debug.component.html | 6 ++ src/app/debug/debug.module.ts | 4 +- .../debug/messages/messages.component.html | 3 + .../debug/messages/messages.component.scss | 0 .../debug/messages/messages.component.spec.ts | 25 ++++++++ src/app/debug/messages/messages.component.ts | 49 +++++++++++++++ src/app/info/info.component.html | 5 +- src/app/info/info.component.ts | 31 +++++++++- 12 files changed, 212 insertions(+), 38 deletions(-) create mode 100644 src/app/debug/messages/messages.component.html create mode 100644 src/app/debug/messages/messages.component.scss create mode 100644 src/app/debug/messages/messages.component.spec.ts create mode 100644 src/app/debug/messages/messages.component.ts diff --git a/src-tauri/src/plugins/devmode.rs b/src-tauri/src/plugins/devmode.rs index 547a0254..c07de3b2 100644 --- a/src-tauri/src/plugins/devmode.rs +++ b/src-tauri/src/plugins/devmode.rs @@ -4,7 +4,7 @@ use tauri::plugin::{Builder, TauriPlugin}; use tauri::regex::Regex; use tauri::{Runtime, State}; -use crate::device_manager::Device; +use crate::device_manager::{Device, DeviceManager}; use crate::error::Error; use crate::session_manager::SessionManager; @@ -23,6 +23,17 @@ struct DevModeSession { error_msg: Option, } +#[tauri::command] +async fn token(manager: State<'_, SessionManager>, device: Device) -> Result { + if device.username != "prisoner" { + return Err(Error::Unsupported); + } + if let Some(token) = valid_token(&manager, device).await? { + return Ok(token); + } + return Err(Error::Unsupported); +} + #[tauri::command] async fn status( manager: State<'_, SessionManager>, @@ -31,6 +42,31 @@ async fn status( if device.username != "prisoner" { return Err(Error::Unsupported); } + if let Some(token) = valid_token(&manager, device).await? { + let url = Url::parse_with_params( + "https://developer.lge.com/secure/CheckDevModeSession.dev", + &[("sessionToken", token.clone())], + ) + .expect("should be valid url"); + let session: DevModeSession = reqwest::get(url).await?.json().await?; + if session.result == "success" { + return Ok(DevModeStatus { + token: Some(token), + remaining: Some(session.error_msg.unwrap_or(String::from(""))), + }); + } + return Ok(DevModeStatus { + token: Some(token), + remaining: None, + }); + } + return Ok(DevModeStatus { + token: None, + remaining: None, + }); +} + +async fn valid_token(manager: &SessionManager, device: Device) -> Result, Error> { let token = match manager .exec(device, "cat /var/luna/preferences/devmode_enabled", None) .await @@ -43,32 +79,14 @@ async fn status( }; let regex = Regex::new("^[0-9a-zA-Z]+$").unwrap(); if !regex.is_match(&token) { - return Ok(DevModeStatus { - token: None, - remaining: None, - }); + return Ok(None); } - let url = Url::parse_with_params( - "https://developer.lge.com/secure/CheckDevModeSession.dev", - &[("sessionToken", token.clone())], - ) - .expect("should be valid url"); - let session: DevModeSession = reqwest::get(url).await?.json().await?; - if session.result == "success" { - return Ok(DevModeStatus { - token: Some(token), - remaining: Some(session.error_msg.unwrap_or(String::from(""))), - }); - } - return Ok(DevModeStatus { - token: Some(token), - remaining: None, - }); + return Ok(Some(token)); } /// Initializes the plugin. pub fn plugin(name: &'static str) -> TauriPlugin { Builder::new(name) - .invoke_handler(tauri::generate_handler![status,]) + .invoke_handler(tauri::generate_handler![status, token]) .build() } diff --git a/src/app/core/services/dev-mode.service.ts b/src/app/core/services/dev-mode.service.ts index 83506003..073dc5f8 100644 --- a/src/app/core/services/dev-mode.service.ts +++ b/src/app/core/services/dev-mode.service.ts @@ -14,6 +14,10 @@ export class DevModeService extends BackendClient { async status(device: Device): Promise { return this.invoke('status', {device}); } + + async token(device: Device): Promise { + return this.invoke('token', {device}); + } } export interface DevModeStatus { diff --git a/src/app/core/services/device-manager.service.ts b/src/app/core/services/device-manager.service.ts index 40d88ee7..34bc3247 100644 --- a/src/app/core/services/device-manager.service.ts +++ b/src/app/core/services/device-manager.service.ts @@ -5,10 +5,12 @@ import {BackendClient} from "./backend-client"; import {FileSessionImpl} from "./file.session"; import {HomebrewChannelConfiguration, SystemInfo} from "../../types/luna-apis"; import {basename} from "@tauri-apps/api/path"; -import {RemoteLunaService} from "./remote-luna.service"; +import {LunaResponseError, RemoteLunaService} from "./remote-luna.service"; import {RemoteCommandService} from "./remote-command.service"; import {Buffer} from "buffer"; import {RemoteFileService} from "./remote-file.service"; +import {app} from "@tauri-apps/api"; +import {DevModeService} from "./dev-mode.service"; @Injectable({ providedIn: 'root' @@ -18,7 +20,8 @@ export class DeviceManagerService extends BackendClient { private devicesSubject: Subject; private selectedSubject: Subject; - constructor(zone: NgZone, private cmd: RemoteCommandService, private file: RemoteFileService, private luna: RemoteLunaService) { + constructor(zone: NgZone, private cmd: RemoteCommandService, private file: RemoteFileService, + private luna: RemoteLunaService, private devMode: DevModeService) { super(zone, 'device-manager'); this.devicesSubject = new BehaviorSubject([]); this.selectedSubject = new BehaviorSubject(null); @@ -69,14 +72,7 @@ export class DeviceManagerService extends BackendClient { } async devModeToken(device: Device): Promise { - return await this.file.read(device, '/var/luna/preferences/devmode_enabled', 'utf-8') - .then(()=> 'cf961c5c0c87c79ec42a80762971cb06dccbc1a087c3a31a7e49338881311112') - .then(s => { - if (!s || !s.match(/^[0-9a-zA-Z]+$/)) { - throw new Error('No valid dev mode token'); - } - return s; - }); + return await this.devMode.token(device); } async listCrashReports(device: Device): Promise { @@ -110,6 +106,25 @@ export class DeviceManagerService extends BackendClient { }); } + async takeScreenshot(device: DeviceLike): Promise { + const tmpPath = `/tmp/devman_shot_${Date.now()}.png` + const param: Record = { + path: tmpPath, + method: "DISPLAY", + format: "PNG", + width: 1920, + height: 1080 + }; + await (this.luna.call(device, 'luna://com.webos.service.capture/executeOneShot', param, false) + .catch((e) => { + if (LunaResponseError.isCompatible(e) && e['errorText']?.includes('Service does not exist')) { + return this.luna.call(device, 'luna://com.webos.service.tv.capture/executeOneShot', param, false); + } + throw e; + })); + return tmpPath; + } + async getHbChannelConfig(device: Device): Promise> { return await this.luna.call(device, 'luna://org.webosbrew.hbchannel.service/getConfiguration', {}); } @@ -156,7 +171,10 @@ export class CrashReport implements CrashReportEntry { saveName = summary.replace(/\//g, '_'); } if (appDirIdx < 0) { - return {title: `${processName} (${processId})`, summary, saveName}; + if (processName && processId && summary) { + return {title: `${processName} (${processId})`, summary, saveName}; + } + return {title: 'Unknown crash', summary: name, saveName} } const substr = name.substring(appDirIdx + appDirPrefix.length); const firstSlash = substr.indexOf('/'), lastSlash = substr.lastIndexOf('/'); diff --git a/src/app/core/services/remote-luna.service.ts b/src/app/core/services/remote-luna.service.ts index 049908d9..dc41f28f 100644 --- a/src/app/core/services/remote-luna.service.ts +++ b/src/app/core/services/remote-luna.service.ts @@ -3,6 +3,7 @@ import {Injectable} from "@angular/core"; import {DeviceLike} from "../../types"; import {catchError, finalize, Observable} from "rxjs"; import {map} from "rxjs/operators"; +import {omit} from "lodash"; export declare interface LunaResponse extends Record { returnValue: boolean, @@ -34,7 +35,7 @@ export class RemoteLunaService { throw new Error(`Bad response ${out}`); } if (!typed.returnValue) { - throw new Error(out); + throw new LunaResponseError(typed); } return typed; }); @@ -62,3 +63,21 @@ export class LunaUnsupportedError extends Error { super(message); } } + +export class LunaResponseError extends Error { + declare returnValue: false; + details: string; + + [values: string]: any; + + constructor(payload: Record) { + super(`Luna call returned negative response: ${payload['errorText']}`); + this.details = payload['errorText']; + Object.assign(this, omit(payload, 'message', 'reason', 'details')) + } + + static isCompatible(e: any): e is LunaResponseError { + return typeof (e.message) === 'string' && e.returnValue === false; + } + +} diff --git a/src/app/debug/debug.component.html b/src/app/debug/debug.component.html index e07097b1..0ba56f2d 100644 --- a/src/app/debug/debug.component.html +++ b/src/app/debug/debug.component.html @@ -6,6 +6,12 @@ +
  • + System Logs + + + +
  • diff --git a/src/app/debug/debug.module.ts b/src/app/debug/debug.module.ts index 85519f63..a7ca82f7 100644 --- a/src/app/debug/debug.module.ts +++ b/src/app/debug/debug.module.ts @@ -5,12 +5,14 @@ import {DebugComponent} from './debug.component'; import {NgbNavModule, NgbTooltipModule} from "@ng-bootstrap/ng-bootstrap"; import {CrashesComponent} from "./crashes/crashes.component"; import {SharedModule} from "../shared/shared.module"; +import { MessagesComponent } from './messages/messages.component'; @NgModule({ declarations: [ DebugComponent, - CrashesComponent + CrashesComponent, + MessagesComponent ], imports: [ CommonModule, diff --git a/src/app/debug/messages/messages.component.html b/src/app/debug/messages/messages.component.html new file mode 100644 index 00000000..4078f8cf --- /dev/null +++ b/src/app/debug/messages/messages.component.html @@ -0,0 +1,3 @@ +
      +
    • {{log}}
    • +
    diff --git a/src/app/debug/messages/messages.component.scss b/src/app/debug/messages/messages.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/debug/messages/messages.component.spec.ts b/src/app/debug/messages/messages.component.spec.ts new file mode 100644 index 00000000..69163f1d --- /dev/null +++ b/src/app/debug/messages/messages.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MessagesComponent } from './messages.component'; + +describe('MessagesComponent', () => { + let component: MessagesComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ MessagesComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(MessagesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/debug/messages/messages.component.ts b/src/app/debug/messages/messages.component.ts new file mode 100644 index 00000000..6e1bf31b --- /dev/null +++ b/src/app/debug/messages/messages.component.ts @@ -0,0 +1,49 @@ +import {Component, Input, OnDestroy, OnInit} from '@angular/core'; +import {Device} from "../../types"; +import {RemoteCommandService} from "../../core/services/remote-command.service"; +import {noop, Observable, Subscription, tap} from "rxjs"; + +@Component({ + selector: 'app-log-messages', + templateUrl: './messages.component.html', + styleUrls: ['./messages.component.scss'] +}) +export class MessagesComponent implements OnDestroy { + + logs: string[] = []; + + private deviceField: Device | null = null; + private subscription?: Subscription; + + constructor(private cmd: RemoteCommandService) { + } + + ngOnDestroy(): void { + this.subscription?.unsubscribe(); + } + + get device(): Device | null { + return this.deviceField; + } + + @Input() + set device(device: Device | null) { + this.deviceField = device; + this.subscription?.unsubscribe(); + this.subscription = undefined; + this.logs = []; + if (device) { + this.reload(device).catch(noop); + } + } + + private async reload(device: Device): Promise { + this.subscription = (await this.logread(device)).subscribe((row) => { + this.logs.push(row); + }); + } + + private async logread(device: Device) { + return await this.cmd.popen(device, 'tail -f /var/log/messages', 'utf-8'); + } +} diff --git a/src/app/info/info.component.html b/src/app/info/info.component.html index 5acc50fc..7aa5a246 100644 --- a/src/app/info/info.component.html +++ b/src/app/info/info.component.html @@ -10,6 +10,9 @@
    Device - {{sysInfo.modelName}}

    webOS version: {{sysInfo.sdkVersion}}

    +
    + +
    @@ -19,7 +22,7 @@
    Dev Mode
    Remaining duration: {{devModeRemaining | async}}
    Remaining time displaying on your TV will only update on boot.

    -
    +
    diff --git a/src/app/info/info.component.ts b/src/app/info/info.component.ts index e0e30fe8..88ea8338 100644 --- a/src/app/info/info.component.ts +++ b/src/app/info/info.component.ts @@ -2,9 +2,9 @@ import {Component, Injector} from '@angular/core'; import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; import * as moment from 'moment'; import 'moment-duration-format'; -import {Observable, of, timer} from 'rxjs'; +import {noop, Observable, of, timer} from 'rxjs'; import {map} from 'rxjs/operators'; -import {Device, RawPackageInfo} from '../types'; +import {Device, FileSession, RawPackageInfo} from '../types'; import { AppManagerService, AppsRepoService, @@ -16,6 +16,10 @@ import { import {ProgressDialogComponent} from '../shared/components/progress-dialog/progress-dialog.component'; import {RenewScriptComponent} from './renew-script/renew-script.component'; import {HomebrewChannelConfiguration, SystemInfo} from "../types/luna-apis"; +import {MessageDialogComponent} from "../shared/components/message-dialog/message-dialog.component"; +import {LunaResponseError} from "../core/services/remote-luna.service"; +import {RemoteFileService} from "../core/services/remote-file.service"; +import {open as openPath} from "@tauri-apps/api/shell"; @Component({ selector: 'app-info', @@ -36,6 +40,7 @@ export class InfoComponent { constructor( private modalService: NgbModal, private deviceManager: DeviceManagerService, + private files: RemoteFileService, private appManager: AppManagerService, private appsRepo: AppsRepoService, private devMode: DevModeService @@ -70,6 +75,28 @@ export class InfoComponent { }); } + async takeScreenshot(): Promise { + // TODO: unify root check + const device = this.device; + if (!device || device.username !== 'root') return; + const progress = ProgressDialogComponent.open(this.modalService); + try { + const imgPath = await this.deviceManager.takeScreenshot(device); + const tempPath = await this.files.getTemp(device, imgPath) + .finally(() => this.files.rm(device, imgPath, false).catch(noop)); + await openPath(tempPath); + } catch (e) { + console.log(JSON.stringify(e)); + MessageDialogComponent.open(this.modalService, { + message: 'Failed to take screenshot', + error: e as Error, + positive: 'OK' + }) + } finally { + progress.dismiss(); + } + } + private async loadDeviceInfo(): Promise { if (!this.device) return; this.infoError = null;