Screenshot for core dump (#2066)

* start of screenshot, need uploader

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* cleanup

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* some cleanup

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* most things working

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* bump the world

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* mime type

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
This commit is contained in:
Jess Frazelle
2024-04-11 13:15:49 -07:00
committed by GitHub
parent a90fe3c066
commit c094d0ced1
23 changed files with 236 additions and 21 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

@ -38,6 +38,7 @@
"decamelize": "^6.0.0",
"formik": "^2.4.5",
"fuse.js": "^7.0.0",
"html2canvas-pro": "^1.4.3",
"http-server": "^14.1.1",
"json-rpc-2.0": "^1.6.0",
"jszip": "^3.10.1",

View File

@ -1,4 +1,4 @@
import { useCallback, MouseEventHandler, useEffect } from 'react'
import { useCallback, MouseEventHandler, useEffect, useRef } from 'react'
import { DebugPanel } from './components/DebugPanel'
import { uuidv4 } from 'lib/utils'
import { PaneType, useStore } from './useStore'
@ -41,6 +41,9 @@ export function App() {
const navigate = useNavigate()
const filePath = useAbsoluteFilePath()
const { onProjectOpen } = useLspContext()
// We need the ref for the outermost div so we can screenshot the app for
// the coredump.
const ref = useRef<HTMLDivElement>(null)
const projectName = project?.name || null
const projectPath = project?.path || null
@ -55,14 +58,20 @@ export function App() {
setOpenPanes,
didDragInStream,
streamDimensions,
setHtmlRef,
} = useStore((s) => ({
buttonDownInStream: s.buttonDownInStream,
openPanes: s.openPanes,
setOpenPanes: s.setOpenPanes,
didDragInStream: s.didDragInStream,
streamDimensions: s.streamDimensions,
setHtmlRef: s.setHtmlRef,
}))
useEffect(() => {
setHtmlRef(ref)
}, [ref])
const { settings } = useSettingsAuthContext()
const {
modeling: { showDebugPanel },
@ -140,6 +149,7 @@ export function App() {
<div
className="relative h-full flex flex-col"
onMouseMove={handleMouseMove}
ref={ref}
>
<AppHeader
className={

View File

@ -78,7 +78,14 @@ export const ModelingMachineProvider = ({
const token = auth?.context?.token
const streamRef = useRef<HTMLDivElement>(null)
useSetupEngineManager(streamRef, token, theme.current)
const coreDumpManager = new CoreDumpManager(engineCommandManager)
const { htmlRef } = useStore((s) => ({
htmlRef: s.htmlRef,
}))
const coreDumpManager = new CoreDumpManager(
engineCommandManager,
htmlRef,
token
)
useHotkeys('meta + shift + .', () => coreDump(coreDumpManager, true))
const {

View File

@ -9,14 +9,39 @@ import {
} from '@tauri-apps/plugin-os'
import { APP_VERSION } from 'routes/Settings'
import { UAParser } from 'ua-parser-js'
import screenshot from 'lib/screenshot'
import React from 'react'
import { VITE_KC_API_BASE_URL } from 'env'
// This is a class for getting all the values from the JS world to pass to the Rust world
// for a core dump.
export class CoreDumpManager {
engineCommandManager: EngineCommandManager
htmlRef: React.RefObject<HTMLDivElement> | null
token: string | undefined
baseUrl: string = VITE_KC_API_BASE_URL
constructor(engineCommandManager: EngineCommandManager) {
constructor(
engineCommandManager: EngineCommandManager,
htmlRef: React.RefObject<HTMLDivElement> | null,
token: string | undefined
) {
this.engineCommandManager = engineCommandManager
this.htmlRef = htmlRef
this.token = token
}
// Get the token.
authToken(): string {
if (!this.token) {
throw new Error('Token not set')
}
return this.token
}
// Get the base url.
baseApiUrl(): string {
return this.baseUrl
}
// Get the version of the app from the package.json.
@ -111,4 +136,15 @@ export class CoreDumpManager {
return JSON.stringify(webrtcStats)
})
}
// Return a data URL (png format) of the screenshot of the current page.
screenshot(): Promise<string> {
return screenshot(this.htmlRef)
.then((screenshot: string) => {
return screenshot
})
.catch((error: any) => {
throw new Error(`Error getting screenshot: ${error}`)
})
}
}

21
src/lib/screenshot.ts Normal file
View File

@ -0,0 +1,21 @@
import React from 'react'
import html2canvas from 'html2canvas-pro'
// Return a data URL (png format) of the screenshot of the current page.
export default async function screenshot(
htmlRef: React.RefObject<HTMLDivElement> | null
): Promise<string> {
if (htmlRef === null) {
throw new Error('htmlRef is null')
}
if (htmlRef.current === null) {
throw new Error('htmlRef is null')
}
return html2canvas(htmlRef.current)
.then((canvas) => {
return canvas.toDataURL()
})
.catch((error) => {
throw error
})
}

View File

@ -81,6 +81,8 @@ export interface StoreState {
streamWidth: number
streamHeight: number
}) => void
setHtmlRef: (ref: React.RefObject<HTMLDivElement>) => void
htmlRef: React.RefObject<HTMLDivElement> | null
showHomeMenu: boolean
setHomeShowMenu: (showMenu: boolean) => void
@ -132,6 +134,10 @@ export const useStore = create<StoreState>()(
setButtonDownInStream: (buttonDownInStream) => {
set({ buttonDownInStream })
},
setHtmlRef: (htmlRef) => {
set({ htmlRef })
},
htmlRef: null,
didDragInStream: false,
setDidDragInStream: (didDragInStream) => {
set({ didDragInStream })

View File

@ -1920,9 +1920,9 @@ dependencies = [
[[package]]
name = "kittycad"
version = "0.2.63"
version = "0.2.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93a332250e08fd715ad3d5826e04d36da1c5bb42d0c1b1ff1f0598278b9ebf3c"
checksum = "9e2897244f4600f863115561a0fd1cd7c87fca20253ffecfebc53ef642d0aceb"
dependencies = [
"anyhow",
"async-trait",
@ -1958,9 +1958,9 @@ dependencies = [
[[package]]
name = "kittycad-execution-plan"
version = "0.1.3"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e913f8e5f3ef7928cddca2e7b53c6582d7be6a8f900d18ce6c31c04083056270"
checksum = "acf8ffb148bd09de8889a8a2b3075a23ee86446c3a6e1c6dcf66b40fdc778158"
dependencies = [
"bytes",
"gltf-json",
@ -2043,9 +2043,9 @@ dependencies = [
[[package]]
name = "kittycad-modeling-session"
version = "0.1.2"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ee3a24232a086ec12ae4cfee443485c22e6c6959936d861006fa13bebef0904"
checksum = "bae9bc47fcc3cc30727b35e738c35666b97e1e5f48f3f4c60ddaeccb69b66559"
dependencies = [
"futures",
"kittycad",

View File

@ -59,12 +59,12 @@ members = [
]
[workspace.dependencies]
kittycad = { version = "0.2.63", default-features = false, features = ["js", "requests"] }
kittycad-execution-plan = "0.1.3"
kittycad = { version = "0.2.66", default-features = false, features = ["js", "requests"] }
kittycad-execution-plan = "0.1.4"
kittycad-execution-plan-macros = "0.1.9"
kittycad-execution-plan-traits = "0.1.14"
kittycad-modeling-cmds = "0.2.17"
kittycad-modeling-session = "0.1.2"
kittycad-modeling-session = "0.1.3"
[[test]]
name = "executor"

View File

@ -777,7 +777,7 @@ fn generate_code_block_test(
}
let ws = client
.modeling()
.commands_ws(None, None, None, None, None, Some(false))
.commands_ws(None, None, None, None, None,None, Some(false))
.await.unwrap();
let tokens = crate::token::lexer(#code_block);

View File

@ -14,6 +14,7 @@ keywords = ["kcl", "KittyCAD", "CAD"]
anyhow = { version = "1.0.82", features = ["backtrace"] }
async-recursion = "1.1.0"
async-trait = "0.1.79"
base64 = "0.22.0"
chrono = "0.4.37"
clap = { version = "4.5.4", features = ["cargo", "derive", "env", "unicode"], optional = true }
dashmap = "5.5.3"

View File

@ -19,8 +19,16 @@ impl Default for CoreDumper {
}
}
#[async_trait::async_trait]
#[async_trait::async_trait(?Send)]
impl CoreDump for CoreDumper {
fn token(&self) -> Result<String> {
Ok(std::env::var("KITTYCAD_API_TOKEN").unwrap_or_default())
}
fn base_api_url(&self) -> Result<String> {
Ok("https://api.zoo.dev".to_string())
}
fn version(&self) -> Result<String> {
Ok(env!("CARGO_PKG_VERSION").to_string())
}
@ -42,4 +50,9 @@ impl CoreDump for CoreDumper {
// TODO: we could actually implement this.
Ok(crate::coredump::WebrtcStats::default())
}
async fn screenshot(&self) -> Result<String> {
// Take a screenshot of the engine.
todo!()
}
}

View File

@ -6,11 +6,17 @@ pub mod local;
pub mod wasm;
use anyhow::Result;
use base64::Engine;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[async_trait::async_trait]
#[async_trait::async_trait(?Send)]
pub trait CoreDump: Clone {
/// Return the authentication token.
fn token(&self) -> Result<String>;
fn base_api_url(&self) -> Result<String>;
fn version(&self) -> Result<String>;
async fn os(&self) -> Result<OsInfo>;
@ -19,10 +25,43 @@ pub trait CoreDump: Clone {
async fn get_webrtc_stats(&self) -> Result<WebrtcStats>;
/// Return a screenshot of the app.
async fn screenshot(&self) -> Result<String>;
/// Get a screenshot of the app and upload it to public cloud storage.
async fn upload_screenshot(&self) -> Result<String> {
let screenshot = self.screenshot().await?;
let cleaned = screenshot.trim_start_matches("data:image/png;base64,");
// Create the zoo client.
let mut zoo = kittycad::Client::new(self.token()?);
zoo.set_base_url(&self.base_api_url()?);
// Base64 decode the screenshot.
let data = base64::engine::general_purpose::STANDARD.decode(cleaned)?;
// Upload the screenshot.
let links = zoo
.meta()
.create_debug_uploads(vec![kittycad::types::multipart::Attachment {
name: "".to_string(),
filename: Some("modeling-app/core-dump-screenshot.png".to_string()),
content_type: Some("image/png".to_string()),
data,
}])
.await
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
if links.is_empty() {
anyhow::bail!("Failed to upload screenshot");
}
Ok(links[0].clone())
}
/// Dump the app info.
async fn dump(&self) -> Result<AppInfo> {
let webrtc_stats = self.get_webrtc_stats().await?;
let os = self.os().await?;
let screenshot_url = self.upload_screenshot().await?;
let mut app_info = AppInfo {
version: self.version()?,
@ -33,7 +72,7 @@ pub trait CoreDump: Clone {
webrtc_stats,
github_issue_url: None,
};
app_info.set_github_issue_url()?;
app_info.set_github_issue_url(&screenshot_url)?;
Ok(app_info)
}
@ -68,12 +107,14 @@ pub struct AppInfo {
impl AppInfo {
/// Set the github issue url.
pub fn set_github_issue_url(&mut self) -> Result<()> {
pub fn set_github_issue_url(&mut self, screenshot_url: &str) -> Result<()> {
let tauri_or_browser_label = if self.tauri { "tauri" } else { "browser" };
let labels = ["coredump", "bug", tauri_or_browser_label];
let body = format!(
r#"[Insert a description of the issue here]
![Screenshot]({})
<details>
<summary><b>Core Dump</b></summary>
@ -82,6 +123,7 @@ impl AppInfo {
```
</details>
"#,
screenshot_url,
serde_json::to_string_pretty(&self)?
);
let urlencoded: String = form_urlencoded::byte_serialize(body.as_bytes()).collect();

View File

@ -10,6 +10,12 @@ extern "C" {
#[derive(Debug, Clone)]
pub type CoreDumpManager;
#[wasm_bindgen(method, js_name = authToken, catch)]
fn auth_token(this: &CoreDumpManager) -> Result<String, js_sys::Error>;
#[wasm_bindgen(method, js_name = baseApiUrl, catch)]
fn baseApiUrl(this: &CoreDumpManager) -> Result<String, js_sys::Error>;
#[wasm_bindgen(method, js_name = version, catch)]
fn version(this: &CoreDumpManager) -> Result<String, js_sys::Error>;
@ -21,6 +27,9 @@ extern "C" {
#[wasm_bindgen(method, js_name = getWebrtcStats, catch)]
fn get_webrtc_stats(this: &CoreDumpManager) -> Result<js_sys::Promise, js_sys::Error>;
#[wasm_bindgen(method, js_name = screenshot, catch)]
fn screenshot(this: &CoreDumpManager) -> Result<js_sys::Promise, js_sys::Error>;
}
#[derive(Debug, Clone)]
@ -37,8 +46,20 @@ impl CoreDumper {
unsafe impl Send for CoreDumper {}
unsafe impl Sync for CoreDumper {}
#[async_trait::async_trait]
#[async_trait::async_trait(?Send)]
impl CoreDump for CoreDumper {
fn token(&self) -> Result<String> {
self.manager
.auth_token()
.map_err(|e| anyhow::anyhow!("Failed to get response from token: {:?}", e))
}
fn base_api_url(&self) -> Result<String> {
self.manager
.baseApiUrl()
.map_err(|e| anyhow::anyhow!("Failed to get response from base api url: {:?}", e))
}
fn version(&self) -> Result<String> {
self.manager
.version()
@ -92,4 +113,22 @@ impl CoreDump for CoreDumper {
Ok(stats)
}
async fn screenshot(&self) -> Result<String> {
let promise = self
.manager
.screenshot()
.map_err(|e| anyhow::anyhow!("Failed to get promise from get screenshot: {:?}", e))?;
let value = JsFuture::from(promise)
.await
.map_err(|e| anyhow::anyhow!("Failed to get response from screenshot: {:?}", e))?;
// Parse the value as a string.
let s = value
.as_string()
.ok_or_else(|| anyhow::anyhow!("Failed to get string from response from screenshot: `{:?}`", value))?;
Ok(s)
}
}

View File

@ -85,6 +85,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
let batched_requests = WebSocketRequest::ModelingCmdBatchReq {
requests,
batch_id: uuid::Uuid::new_v4(),
responses: Some(false),
};
let final_req = if self.batch().lock().unwrap().len() == 1 {
@ -117,7 +118,11 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
// We pop off the responses to cleanup our mappings.
let id_final = match final_req {
WebSocketRequest::ModelingCmdBatchReq { requests: _, batch_id } => batch_id,
WebSocketRequest::ModelingCmdBatchReq {
requests: _,
batch_id,
responses: _,
} => batch_id,
WebSocketRequest::ModelingCmdReq { cmd: _, cmd_id } => cmd_id,
_ => {
return Err(KclError::Engine(KclErrorDetails {

View File

@ -29,7 +29,7 @@ async fn execute_and_snapshot(code: &str, units: kittycad::types::UnitLength) ->
let ws = client
.modeling()
.commands_ws(None, None, None, None, None, Some(false))
.commands_ws(None, None, None, None, None, None, Some(false))
.await?;
// Create a temporary file to write the output to.

View File

@ -33,7 +33,7 @@ async fn setup(code: &str, name: &str) -> Result<(ExecutorContext, Program, uuid
let ws = client
.modeling()
.commands_ws(None, None, None, None, None, Some(false))
.commands_ws(None, None, None, None, None, None, Some(false))
.await?;
let tokens = kcl_lib::token::lexer(code);

View File

@ -3516,6 +3516,11 @@ base16@^1.0.0:
resolved "https://registry.yarnpkg.com/base16/-/base16-1.0.0.tgz#e297f60d7ec1014a7a971a39ebc8a98c0b681e70"
integrity sha512-pNdYkNPiJUnEhnfXV56+sQy8+AaPcG3POZAUnwr4EeqCUZFz4u2PePbo3e5Gj4ziYPCWGUZT9RHisvJKnwFuBQ==
base64-arraybuffer@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc"
integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==
base64-js@^1.3.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
@ -4033,6 +4038,13 @@ crypto-js@^4.2.0:
resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631"
integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==
css-line-break@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-2.1.0.tgz#bfef660dfa6f5397ea54116bb3cb4873edbc4fa0"
integrity sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==
dependencies:
utrie "^1.0.2"
css-shorthand-properties@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/css-shorthand-properties/-/css-shorthand-properties-1.1.1.tgz#1c808e63553c283f289f2dd56fcee8f3337bd935"
@ -5620,6 +5632,14 @@ html-encoding-sniffer@^3.0.0:
dependencies:
whatwg-encoding "^2.0.0"
html2canvas-pro@^1.4.3:
version "1.4.3"
resolved "https://registry.yarnpkg.com/html2canvas-pro/-/html2canvas-pro-1.4.3.tgz#100124e2d17d4de483700ce03176d7447e90d49f"
integrity sha512-RB36SrUGxT9PTjImC7BsGxTinaI3y8cEne76ACdw+E7nRmeJ0jgDntxUP15B9Q9AM2mvEPN6SZo6zmkzwk8HKg==
dependencies:
css-line-break "^2.1.0"
text-segmentation "^1.0.3"
http-cache-semantics@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a"
@ -8416,6 +8436,13 @@ tar@^6.1.11:
mkdirp "^1.0.3"
yallist "^4.0.0"
text-segmentation@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/text-segmentation/-/text-segmentation-1.0.3.tgz#52a388159efffe746b24a63ba311b6ac9f2d7943"
integrity sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==
dependencies:
utrie "^1.0.2"
text-table@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
@ -8803,6 +8830,13 @@ util@^0.12.5:
is-typed-array "^1.1.3"
which-typed-array "^1.1.2"
utrie@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/utrie/-/utrie-1.0.2.tgz#d42fe44de9bc0119c25de7f564a6ed1b2c87a645"
integrity sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==
dependencies:
base64-arraybuffer "^1.0.2"
uuid@^9.0.1:
version "9.0.1"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"