mirror of
https://github.com/wavetermdev/backup.git
synced 2026-04-22 15:26:58 -07:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| de271d3873 | |||
| 8aaea9f1c8 | |||
| 1307a77b56 | |||
| 2772c76728 | |||
| 4384525ca6 | |||
| d6b82e3792 | |||
| 4f06cd8095 | |||
| fbc7bc9688 | |||
| 67df71d1ca | |||
| d177fab6e3 | |||
| 8ea22c4c1f | |||
| af32c8f6f3 | |||
| f2e31a6483 | |||
| 8b8b7ea2b6 | |||
| dbc91d8a4e | |||
| de81464082 | |||
| 5e6cbf30e7 | |||
| 75ae8807eb | |||
| 84227a7128 | |||
| 498f91d7fe | |||
| 4d05ef9998 | |||
| 1eb7ff512e | |||
| ba5a089382 | |||
| e40f93b858 | |||
| 30b9453bcd | |||
| 2a79057314 | |||
| 268bfabec2 | |||
| 30d1e4dfad | |||
| c522d55a23 |
@@ -22,7 +22,7 @@ jobs:
|
||||
- uses: dashcamio/testdriver@main
|
||||
id: testdriver
|
||||
with:
|
||||
version: v2.10.2
|
||||
version: v2.12.5
|
||||
prerun: |
|
||||
rm ~/Desktop/WITH-LOVE-FROM-AMERICA.txt
|
||||
cd ~/actions-runner/_work/testdriver/testdriver/
|
||||
|
||||
Generated
+14710
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -38,7 +38,7 @@
|
||||
"monaco-editor": "0.48.0",
|
||||
"mustache": "^4.2.0",
|
||||
"node-fetch": "^3.2.10",
|
||||
"overlayscrollbars": "^2.6.1",
|
||||
"overlayscrollbars": "^2.7.3",
|
||||
"overlayscrollbars-react": "^0.5.5",
|
||||
"papaparse": "^5.4.1",
|
||||
"react": "^18.1.0",
|
||||
@@ -48,6 +48,7 @@
|
||||
"remark-gfm": "^4.0.0",
|
||||
"sprintf-js": "^1.1.2",
|
||||
"throttle-debounce": "^5.0.0",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"tsx-control-statements": "^5.1.1",
|
||||
"uuid": "^9.0.0",
|
||||
"winston": "^3.8.2",
|
||||
|
||||
@@ -47,7 +47,10 @@ class CodeBlockMarkdown extends React.Component<
|
||||
let clickHandler: (e: React.MouseEvent<HTMLElement>, blockIndex: number) => void;
|
||||
let inputModel = GlobalModel.inputModel;
|
||||
clickHandler = (e: React.MouseEvent<HTMLElement>, blockIndex: number) => {
|
||||
inputModel.setCodeSelectSelectedCodeBlock(blockIndex);
|
||||
const sel = window.getSelection();
|
||||
if (sel?.toString().length == 0) {
|
||||
inputModel.setCodeSelectSelectedCodeBlock(blockIndex);
|
||||
}
|
||||
};
|
||||
let selected = this.blockIndex == this.props.codeSelectSelectedIndex;
|
||||
return (
|
||||
|
||||
@@ -23,7 +23,7 @@ let MagicLayout = {
|
||||
MainSidebarDefaultWidth: 240,
|
||||
|
||||
RightSidebarMinWidth: 0,
|
||||
RightSidebarMaxWidth: 300,
|
||||
RightSidebarMaxWidth: 400,
|
||||
RightSidebarSnapThreshold: 90,
|
||||
RightSidebarDragResistance: 50,
|
||||
RightSidebarDefaultWidth: 240,
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
.sidebar-aichat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
|
||||
.titlebar {
|
||||
background-color: var(--app-panel-bg-color);
|
||||
color: var(--term-blue);
|
||||
padding: 6px 15px;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid var(--app-border-color);
|
||||
font: var(--base-font);
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
line-height: 18px;
|
||||
overflow: hidden;
|
||||
|
||||
.title-string {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
> .content {
|
||||
flex-flow: column nowrap;
|
||||
flex: 1;
|
||||
margin-bottom: 0;
|
||||
overflow-y: auto;
|
||||
|
||||
.chat-window {
|
||||
display: flex;
|
||||
// margin-bottom: 5px;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
// This is the filler that will push the chat messages to the bottom until the chat window is full
|
||||
.filler {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-msg {
|
||||
padding: calc(var(--termpad) * 2);
|
||||
display: flex;
|
||||
|
||||
.chat-msg-header {
|
||||
display: flex;
|
||||
margin-bottom: 2px;
|
||||
|
||||
i {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-msg-assistant {
|
||||
color: var(--app-text-color);
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-msg-error {
|
||||
color: var(--cmdinput-text-error);
|
||||
font-family: var(--markdown-font);
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
padding: 16px 15px 7px;
|
||||
flex: 0 0 auto;
|
||||
border-top: 1px solid var(--app-border-color);
|
||||
|
||||
.chat-textarea {
|
||||
color: var(--app-text-primary-color);
|
||||
background-color: var(--cmdinput-textarea-bg);
|
||||
resize: none;
|
||||
width: 100%;
|
||||
border: transparent;
|
||||
outline: none;
|
||||
overflow: auto;
|
||||
overflow-wrap: anywhere;
|
||||
font-family: var(--termfontfamily);
|
||||
font-weight: normal;
|
||||
line-height: var(--termlineheight);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import * as mobx from "mobx";
|
||||
import { GlobalModel } from "@/models";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import { If, For } from "tsx-control-statements/components";
|
||||
import { Markdown } from "@/elements";
|
||||
import type { OverlayScrollbars } from "overlayscrollbars";
|
||||
import { OverlayScrollbarsComponent, OverlayScrollbarsComponentRef } from "overlayscrollbars-react";
|
||||
import tinycolor from "tinycolor2";
|
||||
|
||||
import "./aichat.less";
|
||||
|
||||
class AIChatKeybindings extends React.Component<{ AIChatObject: AIChat }, {}> {
|
||||
componentDidMount(): void {
|
||||
const AIChatObject = this.props.AIChatObject;
|
||||
const keybindManager = GlobalModel.keybindManager;
|
||||
const inputModel = GlobalModel.inputModel;
|
||||
|
||||
keybindManager.registerKeybinding("pane", "aichat", "generic:confirm", (waveEvent) => {
|
||||
AIChatObject.onEnterKeyPressed();
|
||||
return true;
|
||||
});
|
||||
keybindManager.registerKeybinding("pane", "aichat", "generic:expandTextInput", (waveEvent) => {
|
||||
AIChatObject.onExpandInputPressed();
|
||||
return true;
|
||||
});
|
||||
keybindManager.registerKeybinding("pane", "aichat", "generic:cancel", (waveEvent) => {
|
||||
inputModel.closeAuxView();
|
||||
return true;
|
||||
});
|
||||
keybindManager.registerKeybinding("pane", "aichat", "aichat:clearHistory", (waveEvent) => {
|
||||
inputModel.clearAIAssistantChat();
|
||||
return true;
|
||||
});
|
||||
keybindManager.registerKeybinding("pane", "aichat", "generic:selectAbove", (waveEvent) => {
|
||||
return AIChatObject.onArrowUpPressed();
|
||||
});
|
||||
keybindManager.registerKeybinding("pane", "aichat", "generic:selectBelow", (waveEvent) => {
|
||||
return AIChatObject.onArrowDownPressed();
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
GlobalModel.keybindManager.unregisterDomain("aichat");
|
||||
}
|
||||
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class ChatContent extends React.Component<{}, {}> {
|
||||
chatListKeyCount: number = 0;
|
||||
containerRef: React.RefObject<OverlayScrollbarsComponentRef> = React.createRef();
|
||||
chatWindowRef: React.RefObject<HTMLDivElement> = React.createRef();
|
||||
osInstance: OverlayScrollbars = null;
|
||||
|
||||
componentDidUpdate() {
|
||||
this.chatListKeyCount = 0;
|
||||
if (this.containerRef?.current && this.osInstance) {
|
||||
const { viewport } = this.osInstance.elements();
|
||||
viewport.scrollTo({
|
||||
behavior: "auto",
|
||||
top: this.chatWindowRef.current.scrollHeight,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.osInstance) {
|
||||
this.osInstance.destroy();
|
||||
this.osInstance = null;
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
onScrollbarInitialized(instance) {
|
||||
this.osInstance = instance;
|
||||
const { viewport } = instance.elements();
|
||||
viewport.scrollTo({
|
||||
behavior: "auto",
|
||||
top: this.chatWindowRef.current.scrollHeight,
|
||||
});
|
||||
}
|
||||
|
||||
renderError(err: string): any {
|
||||
return <div className="chat-msg-error">{err}</div>;
|
||||
}
|
||||
|
||||
renderChatMessage(chatItem: OpenAICmdInfoChatMessageType): any {
|
||||
const curKey = "chatmsg-" + this.chatListKeyCount;
|
||||
this.chatListKeyCount++;
|
||||
const senderClassName = chatItem.isassistantresponse ? "chat-msg-assistant" : "chat-msg-user";
|
||||
const msgClassName = `chat-msg ${senderClassName}`;
|
||||
|
||||
let innerHTML: React.JSX.Element = (
|
||||
<>
|
||||
<div className="chat-msg-header">
|
||||
<i className="fa-sharp fa-solid fa-user"></i>
|
||||
</div>
|
||||
<div className="msg-text">{chatItem.userquery}</div>
|
||||
</>
|
||||
);
|
||||
if (chatItem.isassistantresponse) {
|
||||
if (chatItem.assistantresponse.error != null && chatItem.assistantresponse.error !== "") {
|
||||
innerHTML = this.renderError(chatItem.assistantresponse.error);
|
||||
} else {
|
||||
innerHTML = (
|
||||
<>
|
||||
<div className="chat-msg-header">
|
||||
<i className="fa-sharp fa-solid fa-sparkles"></i>
|
||||
</div>
|
||||
<Markdown text={chatItem.assistantresponse.message} codeSelect />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const cssVar = GlobalModel.isDev ? "--app-panel-bg-color-dev" : "--app-panel-bg-color";
|
||||
const panelBgColor = getComputedStyle(document.documentElement).getPropertyValue(cssVar).trim();
|
||||
const color = tinycolor(panelBgColor);
|
||||
const newColor = color.isValid() ? tinycolor(panelBgColor).darken(6).toString() : "none";
|
||||
const backgroundColor = this.chatListKeyCount % 2 === 0 ? "none" : newColor;
|
||||
|
||||
return (
|
||||
<div className={msgClassName} key={curKey} style={{ backgroundColor }}>
|
||||
{innerHTML}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const chatMessageItems = GlobalModel.inputModel.AICmdInfoChatItems.slice();
|
||||
const chitem: OpenAICmdInfoChatMessageType = null;
|
||||
return (
|
||||
<OverlayScrollbarsComponent
|
||||
ref={this.containerRef}
|
||||
className="content"
|
||||
options={{ scrollbars: { autoHide: "leave" } }}
|
||||
events={{ initialized: this.onScrollbarInitialized }}
|
||||
>
|
||||
<div ref={this.chatWindowRef} className="chat-window">
|
||||
<div className="filler"></div>
|
||||
<For each="chitem" index="idx" of={chatMessageItems}>
|
||||
{this.renderChatMessage(chitem)}
|
||||
</For>
|
||||
</div>
|
||||
</OverlayScrollbarsComponent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class AIChat extends React.Component<{}, {}> {
|
||||
textAreaRef: React.RefObject<HTMLTextAreaElement> = React.createRef();
|
||||
termFontSize: number = 14;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
mobx.makeObservable(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const inputModel = GlobalModel.inputModel;
|
||||
if (this.textAreaRef.current != null) {
|
||||
this.textAreaRef.current.focus();
|
||||
// inputModel.setCmdInfoChatRefs(this.textAreaRef, this.chatWindowScrollRef);
|
||||
}
|
||||
this.requestChatUpdate();
|
||||
this.onTextAreaChange(null);
|
||||
}
|
||||
|
||||
requestChatUpdate() {
|
||||
const chatMessageItems = GlobalModel.inputModel.AICmdInfoChatItems.slice();
|
||||
if (chatMessageItems == null || chatMessageItems.length == 0) {
|
||||
this.submitChatMessage("");
|
||||
}
|
||||
}
|
||||
|
||||
submitChatMessage(messageStr: string) {
|
||||
const curLine = GlobalModel.inputModel.curLine;
|
||||
const prtn = GlobalModel.submitChatInfoCommand(messageStr, curLine, false);
|
||||
prtn.then((rtn) => {
|
||||
if (!rtn.success) {
|
||||
console.log("submit chat command error: " + rtn.error);
|
||||
}
|
||||
}).catch((_) => {});
|
||||
}
|
||||
|
||||
getLinePos(elem: any): { numLines: number; linePos: number } {
|
||||
const numLines = elem.value.split("\n").length;
|
||||
const linePos = elem.value.substr(0, elem.selectionStart).split("\n").length;
|
||||
return { numLines, linePos };
|
||||
}
|
||||
|
||||
@mobx.action.bound
|
||||
onTextAreaFocused(e: any) {
|
||||
GlobalModel.inputModel.setAuxViewFocus(true);
|
||||
this.onTextAreaChange(e);
|
||||
}
|
||||
|
||||
@mobx.action.bound
|
||||
onTextAreaBlur(e: any) {
|
||||
mobx.action(() => {
|
||||
GlobalModel.inputModel.setAuxViewFocus(false);
|
||||
})();
|
||||
}
|
||||
|
||||
// Adjust the height of the textarea to fit the text
|
||||
@boundMethod
|
||||
onTextAreaChange(e: any) {
|
||||
// Calculate the bounding height of the text area
|
||||
const textAreaMaxLines = 4;
|
||||
const textAreaLineHeight = this.termFontSize * 1.5;
|
||||
const textAreaMinHeight = textAreaLineHeight;
|
||||
const textAreaMaxHeight = textAreaLineHeight * textAreaMaxLines;
|
||||
|
||||
// Get the height of the wrapped text area content. Courtesy of https://stackoverflow.com/questions/995168/textarea-to-resize-based-on-content-length
|
||||
this.textAreaRef.current.style.height = "1px";
|
||||
const scrollHeight: number = this.textAreaRef.current.scrollHeight;
|
||||
|
||||
// Set the new height of the text area, bounded by the min and max height.
|
||||
const newHeight = Math.min(Math.max(scrollHeight, textAreaMinHeight), textAreaMaxHeight);
|
||||
this.textAreaRef.current.style.height = newHeight + "px";
|
||||
GlobalModel.inputModel.codeSelectDeselectAll();
|
||||
}
|
||||
|
||||
onEnterKeyPressed() {
|
||||
const inputModel = GlobalModel.inputModel;
|
||||
const currentRef = this.textAreaRef.current;
|
||||
if (currentRef == null) {
|
||||
return;
|
||||
}
|
||||
if (inputModel.getCodeSelectSelectedIndex() == -1) {
|
||||
const messageStr = currentRef.value;
|
||||
this.submitChatMessage(messageStr);
|
||||
currentRef.value = "";
|
||||
} else {
|
||||
mobx.action(() => {
|
||||
inputModel.grabCodeSelectSelection();
|
||||
inputModel.setAuxViewFocus(false);
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
onExpandInputPressed() {
|
||||
const currentRef = this.textAreaRef.current;
|
||||
if (currentRef == null) {
|
||||
return;
|
||||
}
|
||||
currentRef.setRangeText("\n", currentRef.selectionStart, currentRef.selectionEnd, "end");
|
||||
GlobalModel.inputModel.codeSelectDeselectAll();
|
||||
}
|
||||
|
||||
onArrowUpPressed(): boolean {
|
||||
const currentRef = this.textAreaRef.current;
|
||||
if (currentRef == null) {
|
||||
return false;
|
||||
}
|
||||
if (this.getLinePos(currentRef).linePos > 1) {
|
||||
// normal up arrow
|
||||
GlobalModel.inputModel.codeSelectDeselectAll();
|
||||
return false;
|
||||
}
|
||||
GlobalModel.inputModel.codeSelectSelectNextOldestCodeBlock();
|
||||
return true;
|
||||
}
|
||||
|
||||
onArrowDownPressed(): boolean {
|
||||
const currentRef = this.textAreaRef.current;
|
||||
const inputModel = GlobalModel.inputModel;
|
||||
if (currentRef == null) {
|
||||
return false;
|
||||
}
|
||||
if (inputModel.getCodeSelectSelectedIndex() == inputModel.codeSelectBottom) {
|
||||
GlobalModel.inputModel.codeSelectDeselectAll();
|
||||
return false;
|
||||
}
|
||||
inputModel.codeSelectSelectNextNewestCodeBlock();
|
||||
return true;
|
||||
}
|
||||
|
||||
@mobx.action
|
||||
@boundMethod
|
||||
onKeyDown(e: any) {}
|
||||
|
||||
renderError(err: string): any {
|
||||
return <div className="chat-msg-error">{err}</div>;
|
||||
}
|
||||
|
||||
render() {
|
||||
const chatMessageItems = GlobalModel.inputModel.AICmdInfoChatItems.slice();
|
||||
return (
|
||||
<div className="sidebar-aichat">
|
||||
<AIChatKeybindings AIChatObject={this}></AIChatKeybindings>
|
||||
<div className="titlebar">
|
||||
<div className="title-string">Wave AI</div>
|
||||
</div>
|
||||
<If condition={chatMessageItems.length > 0}>
|
||||
<ChatContent />
|
||||
</If>
|
||||
<div className="chat-input">
|
||||
<textarea
|
||||
key="main"
|
||||
ref={this.textAreaRef}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoFocus={true}
|
||||
id="chat-cmd-input"
|
||||
onChange={this.onTextAreaChange}
|
||||
onKeyDown={this.onKeyDown}
|
||||
style={{ fontSize: this.termFontSize }}
|
||||
className="chat-textarea"
|
||||
placeholder="Send a Message..."
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { AIChat };
|
||||
+50
-44
@@ -13,58 +13,64 @@
|
||||
font-weight: var(--sidebar-font-weight);
|
||||
border-left: 1px solid var(--app-border-color);
|
||||
|
||||
.header {
|
||||
height: 39px;
|
||||
.sidebar-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 0 5px;
|
||||
border-bottom: 1px solid var(--app-border-color);
|
||||
}
|
||||
|
||||
.rsb-modes {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 5px 10px;
|
||||
gap: 5px;
|
||||
border-bottom: 1px solid var(--app-border-color);
|
||||
|
||||
.icon-container {
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon-container:hover {
|
||||
background-color: var(--app-selected-mask-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.keybind-debug-pane {
|
||||
padding: 10px;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
|
||||
.keybind-pane-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
padding-bottom: 5px;
|
||||
.header {
|
||||
height: 39px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 5px;
|
||||
border-bottom: 1px solid var(--app-border-color);
|
||||
}
|
||||
|
||||
.keybind-level {
|
||||
margin-top: 10px;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
.rsb-modes {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 5px 10px;
|
||||
gap: 5px;
|
||||
|
||||
.icon-container {
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon-container:hover {
|
||||
background-color: var(--app-selected-mask-color);
|
||||
}
|
||||
}
|
||||
|
||||
.keybind-domain {
|
||||
font-size: 14px;
|
||||
margin-left: 20px;
|
||||
white-space: nowrap;
|
||||
&.collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.keybind-debug-pane {
|
||||
padding: 10px;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
|
||||
.keybind-pane-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.keybind-level {
|
||||
margin-top: 10px;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.keybind-domain {
|
||||
font-size: 14px;
|
||||
margin-left: 20px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+32
-21
@@ -11,6 +11,7 @@ import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||
import { GlobalModel } from "@/models";
|
||||
import { ResizableSidebar, Button } from "@/elements";
|
||||
import { WaveBookDisplay } from "./wavebook";
|
||||
import { AIChat } from "./aichat";
|
||||
|
||||
import "./right.less";
|
||||
|
||||
@@ -77,35 +78,45 @@ class RightSideBar extends React.Component<RightSideBarProps, {}> {
|
||||
{(toggleCollapse) => (
|
||||
<React.Fragment>
|
||||
<div className="header">
|
||||
<div className="rsb-modes">
|
||||
<div
|
||||
className="icon-container"
|
||||
title="Show Keybinding Debugger"
|
||||
onClick={() => this.setMode("ai")}
|
||||
>
|
||||
<i className="fa-sharp fa-regular fa-sparkles fa-fw" />
|
||||
</div>
|
||||
<div className="flex-spacer" />
|
||||
<If condition={GlobalModel.isDev}>
|
||||
<div
|
||||
className="icon-container"
|
||||
title="Show Keybinding Debugger"
|
||||
onClick={() => this.setMode("keybind")}
|
||||
>
|
||||
<i className="fa-fw fa-sharp fa-keyboard fa-solid" />
|
||||
</div>
|
||||
</If>
|
||||
<div
|
||||
className="icon-container"
|
||||
title="Show Keybinding Debugger"
|
||||
onClick={() => this.setMode("wavebook")}
|
||||
>
|
||||
<i className="fa-sharp fa-solid fa-book-sparkles"></i>
|
||||
</div>
|
||||
</div>
|
||||
<Button className="secondary ghost" onClick={toggleCollapse}>
|
||||
<i className="fa-sharp fa-regular fa-xmark"></i>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="rsb-modes">
|
||||
<div className="flex-spacer" />
|
||||
<If condition={GlobalModel.isDev}>
|
||||
<div
|
||||
className="icon-container"
|
||||
title="Show Keybinding Debugger"
|
||||
onClick={() => this.setMode("keybind")}
|
||||
>
|
||||
<i className="fa-fw fa-sharp fa-keyboard fa-solid" />
|
||||
</div>
|
||||
</If>
|
||||
<div
|
||||
className="icon-container"
|
||||
title="Show Keybinding Debugger"
|
||||
onClick={() => this.setMode("wavebook")}
|
||||
>
|
||||
<i className="fa-sharp fa-solid fa-book-sparkles"></i>
|
||||
</div>
|
||||
</div>
|
||||
<If condition={this.mode.get() == "keybind"}>
|
||||
{/* <If condition={this.mode.get() == "keybind"}>
|
||||
<KeybindDevPane></KeybindDevPane>
|
||||
</If>
|
||||
</If> */}
|
||||
<If condition={this.mode.get() == "wavebook"}>
|
||||
<WaveBookDisplay></WaveBookDisplay>
|
||||
</If>
|
||||
<If condition={this.mode.get() == "ai"}>
|
||||
<AIChat />
|
||||
</If>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</ResizableSidebar>
|
||||
|
||||
@@ -10,7 +10,7 @@ import "./auxview.less";
|
||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
|
||||
|
||||
interface AuxiliaryCmdViewProps {
|
||||
title: string;
|
||||
title?: string;
|
||||
className?: string;
|
||||
iconClass?: string;
|
||||
titleBarContents?: React.ReactElement[];
|
||||
|
||||
+31
-35
@@ -34,9 +34,6 @@ let wasActive = true;
|
||||
let wasInFg = true;
|
||||
let currentGlobalShortcut: string | null = null;
|
||||
let initialClientData: ClientDataType = null;
|
||||
let windows: Windows = {};
|
||||
|
||||
interface Windows extends Record<string, Electron.BrowserWindow> {}
|
||||
|
||||
checkPromptMigrate();
|
||||
ensureDir(waveHome);
|
||||
@@ -325,10 +322,7 @@ function shFrameNavHandler(event: Electron.Event<Electron.WebContentsWillFrameNa
|
||||
console.log("frame navigation canceled");
|
||||
}
|
||||
|
||||
function createWindow(id: string, clientData: ClientDataType | null): Electron.BrowserWindow {
|
||||
if (windows[id]) {
|
||||
console.error(`createWindow called for existing window ${id}`);
|
||||
}
|
||||
function createWindow(clientData: ClientDataType | null): Electron.BrowserWindow {
|
||||
const bounds = calcBounds(clientData);
|
||||
setKeyUtilPlatform(platform());
|
||||
const win = new electron.BrowserWindow({
|
||||
@@ -376,18 +370,9 @@ function createWindow(id: string, clientData: ClientDataType | null): Electron.B
|
||||
wasInFg = true;
|
||||
wasActive = true;
|
||||
});
|
||||
win.on("close", () => {
|
||||
delete windows[id];
|
||||
});
|
||||
win.webContents.on("zoom-changed", (e) => {
|
||||
win.webContents.send("zoom-changed");
|
||||
});
|
||||
windows[id] = win;
|
||||
return win;
|
||||
}
|
||||
|
||||
function createMainWindow(clientData: ClientDataType | null) {
|
||||
const win = createWindow("main", clientData);
|
||||
win.webContents.setWindowOpenHandler(({ url, frameName }) => {
|
||||
if (url.startsWith("https://docs.waveterm.dev/")) {
|
||||
console.log("openExternal docs", url);
|
||||
@@ -408,6 +393,7 @@ function createMainWindow(clientData: ClientDataType | null) {
|
||||
console.log("window-open denied", url);
|
||||
return { action: "deny" };
|
||||
});
|
||||
return win;
|
||||
}
|
||||
|
||||
function mainResizeHandler(_: any, win: Electron.BrowserWindow) {
|
||||
@@ -673,13 +659,13 @@ async function getClientData(willRetry: boolean, retryNum: number): Promise<Clie
|
||||
}
|
||||
|
||||
function sendWSSC() {
|
||||
if (windows["main"] != null) {
|
||||
electron.BrowserWindow.getAllWindows().forEach((win) => {
|
||||
if (waveSrvProc == null) {
|
||||
windows["main"].webContents.send("wavesrv-status-change", false);
|
||||
return;
|
||||
win.webContents.send("wavesrv-status-change", false);
|
||||
} else {
|
||||
win.webContents.send("wavesrv-status-change", true, waveSrvProc.pid);
|
||||
}
|
||||
windows["main"].webContents.send("wavesrv-status-change", true, waveSrvProc.pid);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function runWaveSrv() {
|
||||
@@ -747,7 +733,7 @@ electron.ipcMain.on("context-editmenu", (_, { x, y }, opts) => {
|
||||
menu.popup({ x, y });
|
||||
});
|
||||
|
||||
async function createMainWindowWrap() {
|
||||
async function createWindowWrap() {
|
||||
let clientData: ClientDataType | null = null;
|
||||
try {
|
||||
clientData = await getClientDataPoll(1);
|
||||
@@ -755,9 +741,9 @@ async function createMainWindowWrap() {
|
||||
} catch (e) {
|
||||
console.log("error getting wavesrv clientdata", e.toString());
|
||||
}
|
||||
createMainWindow(clientData);
|
||||
const win = createWindow(clientData);
|
||||
if (clientData?.winsize.fullscreen) {
|
||||
windows["main"].setFullScreen(true);
|
||||
win.setFullScreen(true);
|
||||
}
|
||||
configureAutoUpdaterStartup(clientData);
|
||||
}
|
||||
@@ -776,7 +762,7 @@ function logActiveState() {
|
||||
console.log("error logging active state", err);
|
||||
});
|
||||
// for next iteration
|
||||
wasInFg = windows["main"]?.isFocused();
|
||||
wasInFg = electron.BrowserWindow.getFocusedWindow()?.isFocused() ?? false;
|
||||
wasActive = false;
|
||||
}
|
||||
|
||||
@@ -802,9 +788,13 @@ function reregisterGlobalShortcut(shortcut: string) {
|
||||
currentGlobalShortcut = null;
|
||||
return;
|
||||
}
|
||||
const ok = electron.globalShortcut.register(shortcut, () => {
|
||||
const ok = electron.globalShortcut.register(shortcut, async () => {
|
||||
console.log("global shortcut triggered, showing window");
|
||||
windows["main"]?.show();
|
||||
if (electron.BrowserWindow.getAllWindows().length == 0) {
|
||||
await createWindowWrap();
|
||||
}
|
||||
const winToShow = electron.BrowserWindow.getFocusedWindow() ?? electron.BrowserWindow.getAllWindows()[0];
|
||||
winToShow?.show();
|
||||
});
|
||||
console.log("registered global shortcut", shortcut, ok ? "ok" : "failed");
|
||||
if (!ok) {
|
||||
@@ -829,9 +819,9 @@ let lastUpdateCheck: Date = null;
|
||||
*/
|
||||
function setAppUpdateStatus(status: string) {
|
||||
appUpdateStatus = status;
|
||||
if (windows["main"] != null) {
|
||||
windows["main"].webContents.send("app-update-status", appUpdateStatus);
|
||||
}
|
||||
electron.BrowserWindow.getAllWindows().forEach((window) => {
|
||||
window.webContents.send("app-update-status", appUpdateStatus);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -915,9 +905,14 @@ async function installAppUpdate() {
|
||||
detail: "A new version has been downloaded. Restart the application to apply the updates.",
|
||||
};
|
||||
|
||||
await electron.dialog.showMessageBox(windows["main"], dialogOpts).then(({ response }) => {
|
||||
if (response === 0) autoUpdater.quitAndInstall();
|
||||
});
|
||||
const allWindows = electron.BrowserWindow.getAllWindows();
|
||||
if (allWindows.length > 0) {
|
||||
await electron.dialog
|
||||
.showMessageBox(electron.BrowserWindow.getFocusedWindow() ?? allWindows[0], dialogOpts)
|
||||
.then(({ response }) => {
|
||||
if (response === 0) autoUpdater.quitAndInstall();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
electron.ipcMain.on("install-app-update", () => fireAndForget(() => installAppUpdate()));
|
||||
@@ -989,10 +984,11 @@ function configureAutoUpdater(enabled: boolean) {
|
||||
}
|
||||
setTimeout(runActiveTimer, 5000); // start active timer, wait 5s just to be safe
|
||||
await app.whenReady();
|
||||
await createMainWindowWrap();
|
||||
await createWindowWrap();
|
||||
|
||||
app.on("activate", () => {
|
||||
if (electron.BrowserWindow.getAllWindows().length === 0) {
|
||||
createMainWindowWrap().then();
|
||||
createWindowWrap().then();
|
||||
}
|
||||
checkForUpdates();
|
||||
});
|
||||
|
||||
@@ -1128,6 +1128,11 @@ func main() {
|
||||
log.Printf("[error] migrate up: %v\n", err)
|
||||
return
|
||||
}
|
||||
// err = blockstore.MigrateBlockstore()
|
||||
// if err != nil {
|
||||
// log.Printf("[error] migrate blockstore: %v\n", err)
|
||||
// return
|
||||
// }
|
||||
clientData, err := sstore.EnsureClientData(context.Background())
|
||||
if err != nil {
|
||||
log.Printf("[error] ensuring client data: %v\n", err)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
-- nothing
|
||||
@@ -0,0 +1,19 @@
|
||||
CREATE TABLE block_file (
|
||||
blockid varchar(36) NOT NULL,
|
||||
name varchar(200) NOT NULL,
|
||||
maxsize bigint NOT NULL,
|
||||
circular boolean NOT NULL,
|
||||
size bigint NOT NULL,
|
||||
createdts bigint NOT NULL,
|
||||
modts bigint NOT NULL,
|
||||
meta json NOT NULL,
|
||||
PRIMARY KEY (blockid, name)
|
||||
);
|
||||
|
||||
CREATE TABLE block_data (
|
||||
blockid varchar(36) NOT NULL,
|
||||
name varchar(200) NOT NULL,
|
||||
partidx int NOT NULL,
|
||||
data blob NOT NULL,
|
||||
PRIMARY KEY(blockid, name, partidx)
|
||||
);
|
||||
@@ -10,3 +10,6 @@ import "embed"
|
||||
|
||||
//go:embed migrations/*.sql
|
||||
var MigrationFS embed.FS
|
||||
|
||||
//go:embed blockstore-migrations/*.sql
|
||||
var BlockstoreMigrationFS embed.FS
|
||||
|
||||
@@ -5,7 +5,6 @@ go 1.22
|
||||
toolchain go1.22.0
|
||||
|
||||
require (
|
||||
github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9
|
||||
github.com/alessio/shellescape v1.4.1
|
||||
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2
|
||||
github.com/creack/pty v1.1.18
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 h1:ez/4by2iGztzR4L0zgAOR8lTQK9VlyBVVd7G4omaOQs=
|
||||
github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
|
||||
github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0=
|
||||
github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
|
||||
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 h1:7Ip0wMmLHLRJdrloDxZfhMm0xrLXZS8+COSu2bXmEQs=
|
||||
@@ -57,7 +55,6 @@ github.com/sawka/txwrap v0.1.2 h1:v8xS0Z1LE7/6vMZA81PYihI+0TSR6Zm1MalzzBIuXKc=
|
||||
github.com/sawka/txwrap v0.1.2/go.mod h1:T3nlw2gVpuolo6/XEetvBbk1oMXnY978YmBFy1UyHvw=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/wavetermdev/ssh_config v0.0.0-20240306041034-17e2087ebde2 h1:onqZrJVap1sm15AiIGTfWzdr6cEF0KdtddeuuOVhzyY=
|
||||
@@ -74,8 +71,6 @@ golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
|
||||
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
mvdan.cc/sh/v3 v3.7.0 h1:lSTjdP/1xsddtaKfGg7Myu7DnlHItd3/M2tomOcNNBg=
|
||||
|
||||
@@ -10,8 +10,6 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/alecthomas/units"
|
||||
)
|
||||
|
||||
type FileOptsType struct {
|
||||
@@ -32,7 +30,11 @@ type FileInfo struct {
|
||||
Meta FileMeta
|
||||
}
|
||||
|
||||
const MaxBlockSize = int64(128 * units.Kilobyte)
|
||||
const UnitsKB = 1024 * 1024
|
||||
const UnitsMB = 1024 * UnitsKB
|
||||
const UnitsGB = 1024 * UnitsMB
|
||||
|
||||
const MaxBlockSize = int64(128 * UnitsKB)
|
||||
const DefaultFlushTimeout = 1 * time.Second
|
||||
|
||||
type CacheEntry struct {
|
||||
@@ -79,16 +81,23 @@ type BlockStore interface {
|
||||
GetAllBlockIds(ctx context.Context) []string
|
||||
}
|
||||
|
||||
var cache map[string]*CacheEntry = make(map[string]*CacheEntry)
|
||||
var blockstoreCache map[string]*CacheEntry = make(map[string]*CacheEntry)
|
||||
var globalLock *sync.Mutex = &sync.Mutex{}
|
||||
var appendLock *sync.Mutex = &sync.Mutex{}
|
||||
var flushTimeout = DefaultFlushTimeout
|
||||
var lastWriteTime time.Time
|
||||
|
||||
// for testing
|
||||
func clearCache() {
|
||||
globalLock.Lock()
|
||||
defer globalLock.Unlock()
|
||||
blockstoreCache = make(map[string]*CacheEntry)
|
||||
}
|
||||
|
||||
func InsertFileIntoDB(ctx context.Context, fileInfo FileInfo) error {
|
||||
metaJson, err := json.Marshal(fileInfo.Meta)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error writing file %s to db: %v", fileInfo.Name, err)
|
||||
return fmt.Errorf("error writing file %s to db: %v", fileInfo.Name, err)
|
||||
}
|
||||
txErr := WithTx(ctx, func(tx *TxWrap) error {
|
||||
query := `INSERT INTO block_file VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
@@ -96,7 +105,7 @@ func InsertFileIntoDB(ctx context.Context, fileInfo FileInfo) error {
|
||||
return nil
|
||||
})
|
||||
if txErr != nil {
|
||||
return fmt.Errorf("Error writing file %s to db: %v", fileInfo.Name, txErr)
|
||||
return fmt.Errorf("error writing file %s to db: %v", fileInfo.Name, txErr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -104,7 +113,7 @@ func InsertFileIntoDB(ctx context.Context, fileInfo FileInfo) error {
|
||||
func WriteFileToDB(ctx context.Context, fileInfo FileInfo) error {
|
||||
metaJson, err := json.Marshal(fileInfo.Meta)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error writing file %s to db: %v", fileInfo.Name, err)
|
||||
return fmt.Errorf("error writing file %s to db: %v", fileInfo.Name, err)
|
||||
}
|
||||
txErr := WithTx(ctx, func(tx *TxWrap) error {
|
||||
query := `UPDATE block_file SET blockid = ?, name = ?, maxsize = ?, circular = ?, size = ?, createdts = ?, modts = ?, meta = ? where blockid = ? and name = ?`
|
||||
@@ -112,7 +121,7 @@ func WriteFileToDB(ctx context.Context, fileInfo FileInfo) error {
|
||||
return nil
|
||||
})
|
||||
if txErr != nil {
|
||||
return fmt.Errorf("Error writing file %s to db: %v", fileInfo.Name, txErr)
|
||||
return fmt.Errorf("error writing file %s to db: %v", fileInfo.Name, txErr)
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -125,7 +134,7 @@ func WriteDataBlockToDB(ctx context.Context, blockId string, name string, index
|
||||
return nil
|
||||
})
|
||||
if txErr != nil {
|
||||
return fmt.Errorf("Error writing data block to db: %v", txErr)
|
||||
return fmt.Errorf("error writing data block to db: %v", txErr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -152,7 +161,7 @@ func WriteToCacheBlockNum(ctx context.Context, blockId string, name string, p []
|
||||
defer cacheEntry.Lock.Unlock()
|
||||
block, err := GetCacheBlock(ctx, blockId, name, cacheNum, pullFromDB)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("Error getting cache block: %v", err)
|
||||
return 0, 0, fmt.Errorf("error getting cache block: %v", err)
|
||||
}
|
||||
var bytesWritten = 0
|
||||
blockLen := len(block.data)
|
||||
@@ -192,7 +201,7 @@ func ReadFromCacheBlock(ctx context.Context, blockId string, name string, block
|
||||
}
|
||||
}()
|
||||
if pos > len(block.data) {
|
||||
return 0, fmt.Errorf("Reading past end of cache block, should never happen")
|
||||
return 0, fmt.Errorf("reading past end of cache block, should never happen")
|
||||
}
|
||||
bytesWritten := 0
|
||||
index := pos
|
||||
@@ -216,7 +225,7 @@ func ReadFromCacheBlock(ctx context.Context, blockId string, name string, block
|
||||
return bytesWritten, nil
|
||||
}
|
||||
|
||||
const MaxSizeError = "Hit Max Size"
|
||||
const MaxSizeError = "MaxSizeError"
|
||||
|
||||
func WriteToCacheBuf(buf *[]byte, p []byte, pos int, length int, maxWrite int64) (int, error) {
|
||||
bytesToWrite := length
|
||||
@@ -260,7 +269,7 @@ func GetValuesFromCacheId(cacheId string) (blockId string, name string) {
|
||||
func GetCacheEntry(ctx context.Context, blockId string, name string) (*CacheEntry, bool) {
|
||||
globalLock.Lock()
|
||||
defer globalLock.Unlock()
|
||||
if curCacheEntry, found := cache[GetCacheId(blockId, name)]; found {
|
||||
if curCacheEntry, found := blockstoreCache[GetCacheId(blockId, name)]; found {
|
||||
return curCacheEntry, true
|
||||
} else {
|
||||
return nil, false
|
||||
@@ -279,7 +288,7 @@ func GetCacheEntryOrPopulate(ctx context.Context, blockId string, name string) (
|
||||
if cacheEntry, found := GetCacheEntry(ctx, blockId, name); found {
|
||||
return cacheEntry, nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("Error getting cache entry %v %v", blockId, name)
|
||||
return nil, fmt.Errorf("error getting cache entry %v %v", blockId, name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,16 +297,16 @@ func GetCacheEntryOrPopulate(ctx context.Context, blockId string, name string) (
|
||||
func SetCacheEntry(ctx context.Context, cacheId string, cacheEntry *CacheEntry) {
|
||||
globalLock.Lock()
|
||||
defer globalLock.Unlock()
|
||||
if _, found := cache[cacheId]; found {
|
||||
if _, found := blockstoreCache[cacheId]; found {
|
||||
return
|
||||
}
|
||||
cache[cacheId] = cacheEntry
|
||||
blockstoreCache[cacheId] = cacheEntry
|
||||
}
|
||||
|
||||
func DeleteCacheEntry(ctx context.Context, blockId string, name string) {
|
||||
globalLock.Lock()
|
||||
defer globalLock.Unlock()
|
||||
delete(cache, GetCacheId(blockId, name))
|
||||
delete(blockstoreCache, GetCacheId(blockId, name))
|
||||
}
|
||||
|
||||
func GetCacheBlock(ctx context.Context, blockId string, name string, cacheNum int, pullFromDB bool) (*CacheBlock, error) {
|
||||
@@ -392,7 +401,7 @@ func WriteAtHelper(ctx context.Context, blockId string, name string, p []byte, o
|
||||
}
|
||||
fInfo, err := Stat(ctx, blockId, name)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("Write At err: %v", err)
|
||||
return 0, fmt.Errorf("WriteAt err: %v", err)
|
||||
}
|
||||
if off > fInfo.Opts.MaxSize && fInfo.Opts.Circular {
|
||||
numOver := off / fInfo.Opts.MaxSize
|
||||
@@ -416,12 +425,12 @@ func WriteAtHelper(ctx context.Context, blockId string, name string, p []byte, o
|
||||
b, err := WriteAtHelper(ctx, blockId, name, p, 0, false)
|
||||
bytesWritten += b
|
||||
if err != nil {
|
||||
return bytesWritten, fmt.Errorf("Write to cache error: %v", err)
|
||||
return bytesWritten, fmt.Errorf("write to cache error: %v", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
} else {
|
||||
return bytesWritten, fmt.Errorf("Write to cache error: %v", err)
|
||||
return bytesWritten, fmt.Errorf("write to cache error: %v", err)
|
||||
}
|
||||
}
|
||||
if len(p) == b {
|
||||
@@ -452,7 +461,7 @@ func GetAllBlockSizes(dataBlocks []*CacheBlock) (int, int) {
|
||||
}
|
||||
|
||||
func FlushCache(ctx context.Context) error {
|
||||
for _, cacheEntry := range cache {
|
||||
for _, cacheEntry := range blockstoreCache {
|
||||
err := WriteFileToDB(ctx, *cacheEntry.Info)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -485,14 +494,14 @@ func ReadAt(ctx context.Context, blockId string, name string, p *[]byte, off int
|
||||
bytesRead := 0
|
||||
fInfo, err := Stat(ctx, blockId, name)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("Read At err: %v", err)
|
||||
return 0, fmt.Errorf("ReadAt err: %v", err)
|
||||
}
|
||||
if off > fInfo.Opts.MaxSize && fInfo.Opts.Circular {
|
||||
numOver := off / fInfo.Opts.MaxSize
|
||||
off = off - (numOver * fInfo.Opts.MaxSize)
|
||||
}
|
||||
if off > fInfo.Size {
|
||||
return 0, fmt.Errorf("Read At error: tried to read past the end of the file")
|
||||
return 0, fmt.Errorf("ReadAt error: tried to read past the end of the file")
|
||||
}
|
||||
endReadPos := math.Min(float64(int64(len(*p))+off), float64(fInfo.Size))
|
||||
bytesToRead := int64(endReadPos) - off
|
||||
@@ -505,7 +514,7 @@ func ReadAt(ctx context.Context, blockId string, name string, p *[]byte, off int
|
||||
for index := curCacheNum; index < curCacheNum+numCaches; index++ {
|
||||
curCacheBlock, err := GetCacheBlock(ctx, blockId, name, index, true)
|
||||
if err != nil {
|
||||
return bytesRead, fmt.Errorf("Error getting cache block: %v", err)
|
||||
return bytesRead, fmt.Errorf("error getting cache block: %v", err)
|
||||
}
|
||||
cacheOffset := off - (int64(index) * MaxBlockSize)
|
||||
if cacheOffset < 0 {
|
||||
@@ -540,7 +549,7 @@ func ReadAt(ctx context.Context, blockId string, name string, p *[]byte, off int
|
||||
break
|
||||
}
|
||||
} else {
|
||||
return bytesRead, fmt.Errorf("Read from cache error: %v", err)
|
||||
return bytesRead, fmt.Errorf("read from cache error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -552,7 +561,7 @@ func AppendData(ctx context.Context, blockId string, name string, p []byte) (int
|
||||
defer appendLock.Unlock()
|
||||
fInfo, err := Stat(ctx, blockId, name)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("Append stat error: %v", err)
|
||||
return 0, fmt.Errorf("append stat error: %v", err)
|
||||
}
|
||||
return WriteAt(ctx, blockId, name, p, fInfo.Size)
|
||||
}
|
||||
@@ -564,12 +573,12 @@ func DeleteFile(ctx context.Context, blockId string, name string) error {
|
||||
}
|
||||
|
||||
func DeleteBlock(ctx context.Context, blockId string) error {
|
||||
for cacheId, _ := range cache {
|
||||
for cacheId := range blockstoreCache {
|
||||
curBlockId, name := GetValuesFromCacheId(cacheId)
|
||||
if curBlockId == blockId {
|
||||
err := DeleteFile(ctx, blockId, name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error deleting %v %v: %v", blockId, name, err)
|
||||
return fmt.Errorf("error deleting %v %v: %v", blockId, name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,16 @@ import (
|
||||
"path"
|
||||
"sync"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
_ "github.com/golang-migrate/migrate/v4/database/sqlite3"
|
||||
"github.com/golang-migrate/migrate/v4/source/iofs"
|
||||
"github.com/jmoiron/sqlx"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/sawka/txwrap"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/dbutil"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbase"
|
||||
|
||||
dbfs "github.com/wavetermdev/waveterm/wavesrv/db"
|
||||
)
|
||||
|
||||
const DBFileName = "blockstore.db"
|
||||
@@ -21,12 +26,64 @@ type SingleConnDBGetter struct {
|
||||
SingleConnLock *sync.Mutex
|
||||
}
|
||||
|
||||
var dbWrap *SingleConnDBGetter
|
||||
var dbWrap *SingleConnDBGetter = &SingleConnDBGetter{SingleConnLock: &sync.Mutex{}}
|
||||
|
||||
type TxWrap = txwrap.TxWrap
|
||||
|
||||
func InitDBState() {
|
||||
dbWrap = &SingleConnDBGetter{SingleConnLock: &sync.Mutex{}}
|
||||
func MakeBlockstoreMigrate() (*migrate.Migrate, error) {
|
||||
fsVar, err := iofs.New(dbfs.BlockstoreMigrationFS, "blockstore-migrations")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("opening iofs: %w", err)
|
||||
}
|
||||
dbUrl := fmt.Sprintf("sqlite3://%s", GetDBName())
|
||||
m, err := migrate.NewWithSourceInstance("iofs", fsVar, dbUrl)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("making blockstore migration db[%s]: %w", GetDBName(), err)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func MigrateBlockstore() error {
|
||||
log.Printf("migrate blockstore\n")
|
||||
m, err := MakeBlockstoreMigrate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
curVersion, dirty, err := GetMigrateVersion(m)
|
||||
if dirty {
|
||||
return fmt.Errorf("cannot migrate up, database is dirty")
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot get current migration version: %v", err)
|
||||
}
|
||||
defer m.Close()
|
||||
err = m.Up()
|
||||
if err != nil && err != migrate.ErrNoChange {
|
||||
return fmt.Errorf("migrating blockstore: %w", err)
|
||||
}
|
||||
newVersion, _, err := GetMigrateVersion(m)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot get new migration version: %v", err)
|
||||
}
|
||||
if newVersion != curVersion {
|
||||
log.Printf("[db] blockstore migration done, version %d -> %d\n", curVersion, newVersion)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetMigrateVersion(m *migrate.Migrate) (uint, bool, error) {
|
||||
if m == nil {
|
||||
var err error
|
||||
m, err = MakeBlockstoreMigrate()
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
}
|
||||
curVersion, dirty, err := m.Version()
|
||||
if err == migrate.ErrNilVersion {
|
||||
return 0, false, nil
|
||||
}
|
||||
return curVersion, dirty, err
|
||||
}
|
||||
|
||||
func (dbg *SingleConnDBGetter) GetDB(ctx context.Context) (*sqlx.DB, error) {
|
||||
@@ -62,8 +119,12 @@ func WithTxRtn[RT any](ctx context.Context, fn func(tx *TxWrap) (RT, error)) (RT
|
||||
var globalDBLock = &sync.Mutex{}
|
||||
var globalDB *sqlx.DB
|
||||
var globalDBErr error
|
||||
var overrideDBName string
|
||||
|
||||
func GetDBName() string {
|
||||
if overrideDBName != "" {
|
||||
return overrideDBName
|
||||
}
|
||||
scHome := scbase.GetWaveHomeDir()
|
||||
return path.Join(scHome, DBFileName)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user