temp fix to enable arrow up/down keybindings

This commit is contained in:
Red Adaya 2024-05-08 07:54:40 +08:00
commit 4758b7351d
84 changed files with 27551 additions and 10442 deletions

View File

@ -45,14 +45,15 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: ${{env.NODE_VERSION}}
cache: "yarn"
- name: Install yarn
run: |
corepack enable
yarn install
- name: Set Version
id: set-version
run: |
VERSION=$(node -e 'console.log(require("./version.js"))')
echo "WAVETERM_VERSION=${VERSION}" >> "$GITHUB_OUTPUT"
- name: Install Yarn Dependencies
run: yarn --frozen-lockfile
- name: Build ${{ matrix.platform }}/${{ matrix.arch }}
run: scripthaus run ${{ matrix.scripthaus }}
env:

View File

@ -1,65 +1,66 @@
name: TestDriver.ai Regression Testing
on:
push:
branches:
- main
pull_request:
branches:
- main
schedule:
- cron: 0 21 * * *
workflow_dispatch: null
push:
branches:
- main
pull_request:
branches:
- main
schedule:
- cron: 0 21 * * *
workflow_dispatch: null
permissions:
contents: read # To allow the action to read repository contents
pull-requests: write # To allow the action to create/update pull request comments
contents: read # To allow the action to read repository contents
pull-requests: write # To allow the action to create/update pull request comments
jobs:
test:
name: TestDriver
runs-on: ubuntu-latest
steps:
- uses: dashcamio/testdriver@main
id: testdriver
with:
version: v2.10.2
prerun: |
rm ~/Desktop/WITH-LOVE-FROM-AMERICA.txt
cd ~/actions-runner/_work/testdriver/testdriver/
brew install go
brew tap scripthaus-dev/scripthaus
brew install scripthaus
npm install -g yarn
scripthaus run build-backend
echo "Yarn"
yarn
echo "Rebuild"
scripthaus run electron-rebuild
echo "Webpack"
scripthaus run webpack-build
echo "Starting Electron"
scripthaus run electron 1>/dev/null 2>&1 &
echo "Electron Done"
exit
prompt: |
1. wait 10 seconds
1. click "Continue"
1. click "Create new tab"
1. validate that overlapping text does not appear in the application
1. focus the Wave input with the keyboard shorcut Command + I
1. type 'ls' into the input
1. press return
1. validate Wave shows the result of 'ls'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: peter-evans/create-or-update-comment@v4
if: ${{always()}}
with:
issue-number: ${{ github.event.pull_request.number }}
body: |
## TestDriver Summary
${{ steps.testdriver.outputs.markdown }}
${{ steps.testdriver.outputs.summary }}
reactions: |
+1
-1
test:
name: TestDriver
runs-on: ubuntu-latest
steps:
- uses: dashcamio/testdriver@main
id: testdriver
with:
version: v2.12.5
prerun: |
rm ~/Desktop/WITH-LOVE-FROM-AMERICA.txt
cd ~/actions-runner/_work/testdriver/testdriver/
brew install go
brew tap scripthaus-dev/scripthaus
brew install corepack
brew install scripthaus
corepack enable
yarn install
scripthaus run build-backend
echo "Yarn"
yarn
echo "Rebuild"
scripthaus run electron-rebuild
echo "Webpack"
scripthaus run webpack-build
echo "Starting Electron"
scripthaus run electron 1>/dev/null 2>&1 &
echo "Electron Done"
exit
prompt: |
1. wait 10 seconds
1. click "Get Started"
1. validate that overlapping text does not appear in the application
1. focus the Wave input with the keyboard shorcut Command + I
1. type 'ls' into the input
1. press return
1. validate Wave shows the result of 'ls'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: peter-evans/create-or-update-comment@v4
if: ${{always()}}
with:
issue-number: ${{ github.event.pull_request.number }}
body: |
## TestDriver Summary
${{ steps.testdriver.outputs.markdown }}
${{ steps.testdriver.outputs.summary }}
reactions: |
+1
-1

9
.gitignore vendored
View File

@ -22,3 +22,12 @@ test/
.vscode/
make/
waveterm-builds.zip
# Yarn Modern
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

View File

@ -3,7 +3,8 @@ cd ~/actions-runner/_work/testdriver/testdriver/
brew install go
brew tap scripthaus-dev/scripthaus
brew install scripthaus
npm install -g yarn
corepack enable
yarn install
scripthaus run build-backend
echo "Yarn"
yarn

1
.yarnrc.yml Normal file
View File

@ -0,0 +1 @@
nodeLinker: node-modules

View File

@ -11,35 +11,43 @@ If you install the production version of Wave, you'll see a semi-transparent gra
Download and install Go (must be at least go 1.18):
```
```sh
brew install go
```
Download and install ScriptHaus (to run the build commands):
```
```sh
brew tap scripthaus-dev/scripthaus
brew install scripthaus
```
You also need a relatively modern nodejs with npm and yarn installed.
- Node can be installed from [https://nodejs.org](https://nodejs.org).
- npm can install yarn using:
Node can be installed from [https://nodejs.org](https://nodejs.org).
```
npm install -g yarn
We use Yarn Modern to manage our packages. The recommended way to install Yarn Modern is using Corepack, a new utility shipped by NodeJS that lets you manage your package manager versioning as you would any packages.
If you installed NodeJS from the official feed (via the website or using NVM), this should come preinstalled. If you use Homebrew or some other feed, you may need to manually install Corepack using `npm install -g corepack`.
For more information on Corepack, check out [this link](https://yarnpkg.com/corepack).
Once you've verified that you have Corepack installed, run the following script to set up Yarn for the repository:
```sh
corepack enable
yarn install
```
## Clone the Repo
```
```sh
git clone git@github.com:wavetermdev/waveterm.git
```
## Building WaveShell / WaveSrv
```
```sh
scripthaus run build-backend
```
@ -49,7 +57,7 @@ This builds the Golang backends for Wave. The binaries will put in waveshell/bin
Install modules (we use yarn):
```
```sh
yarn
```
@ -57,7 +65,7 @@ yarn
We use webpack to build both the React and Electron App Wrapper code. They are both run together using:
```
```sh
scripthaus run webpack-watch
```
@ -65,7 +73,7 @@ scripthaus run webpack-watch
Now that webpack is running (and watching for file changes) we can finally run the WaveTerm Dev Client! To start the client run:
```
```sh
scripthaus run electron
```

View File

@ -1,5 +1,5 @@
# Open-Source Acknowledgements
We make use of many amazing open-source projects to build Wave Terminal. We automatically generate license reports via FOSSA to comply with the license distribution requirements of our dependencies. Below is a summary of the licenses used by our product. Clicking on the image will take you to the full report on FOSSA's website.
We make use of many amazing open-source projects to build Wave Terminal. We automatically generate license reports via FOSSA to comply with the license distribution requirements of our dependencies. Below is a summary of the licenses used by our product. For a full report, see [here](https://app.fossa.com/reports/24d13570-624b-4450-8c22-756e513060c9?full=true) (the page may take 20-30s to load).
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fwavetermdev%2Fwaveterm.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fwavetermdev%2Fwaveterm?ref=badge_large)

14710
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@
},
"productName": "Wave",
"description": "An Open-Source, AI-Native, Terminal Built for Seamless Workflows",
"version": "0.7.3",
"version": "0.7.5",
"main": "dist/emain.js",
"license": "Apache-2.0",
"repository": {
@ -26,7 +26,7 @@
"@tanstack/react-table": "^8.10.3",
"autobind-decorator": "^2.4.0",
"base64-js": "^1.5.1",
"classnames": "^2.3.1",
"clsx": "^2.1.1",
"dayjs": "^1.11.3",
"dompurify": "^3.0.2",
"electron-squirrel-startup": "^1.0.0",
@ -71,7 +71,6 @@
"@babel/preset-typescript": "^7.17.12",
"@electron/rebuild": "^3.6.0",
"@svgr/webpack": "^8.1.0",
"@types/classnames": "^2.3.1",
"@types/electron": "^1.6.10",
"@types/node": "^20.11.0",
"@types/papaparse": "^5.3.10",
@ -108,5 +107,6 @@
},
"scripts": {
"postinstall": "electron-builder install-app-deps"
}
},
"packageManager": "yarn@4.1.1"
}

View File

@ -115,15 +115,3 @@ scripthaus run fullbuild-waveshell
echo building wavesrv
scripthaus run build-wavesrv
```
```bash
# @scripthaus command generate-license-disclaimers
DISCLAIMER_DIR="./acknowledgements"
DISCLAIMER_OUTPUT_DIR="$DISCLAIMER_DIR/disclaimers"
if [ -d "$DISCLAIMER_OUTPUT_DIR" ]; then
rm -rf "$DISCLAIMER_OUTPUT_DIR"
fi
mkdir "$DISCLAIMER_OUTPUT_DIR"
go run github.com/google/go-licenses@latest report ./wavesrv/... ./waveshell/... --template "$DISCLAIMER_DIR/go_licenses_report.tpl" --ignore github.com/wavetermdev/waveterm > "$DISCLAIMER_OUTPUT_DIR/backend.md"
yarn licenses generate-disclaimer > "$DISCLAIMER_OUTPUT_DIR/frontend.md"
```

View File

@ -4,7 +4,7 @@
import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import cn from "classnames";
import { clsx } from "clsx";
import { boundMethod } from "autobind-decorator";
import { If } from "tsx-control-statements/components";
@ -133,7 +133,7 @@ class App extends React.Component<{}, {}> {
const rightSidebarCollapsed = GlobalModel.rightSidebarModel.getCollapsed();
const activeMainView = GlobalModel.activeMainView.get();
const lightDarkClass = GlobalModel.isDarkTheme.get() ? "is-dark" : "is-light";
const mainClassName = cn(
const mainClassName = clsx(
"platform-" + platform,
{
"mainsidebar-collapsed": mainSidebarCollapsed,

View File

@ -6,7 +6,7 @@ import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { If, For } from "tsx-control-statements/components";
import cn from "classnames";
import { clsx } from "clsx";
import { GlobalModel } from "@/models";
import { CmdStrCode, Markdown } from "@/common/elements";
@ -152,11 +152,11 @@ class Bookmark extends React.Component<BookmarkProps, {}> {
return (
<div
data-bookmarkid={bm.bookmarkid}
className={cn("bookmark focus-parent is-editing", {
className={clsx("bookmark focus-parent is-editing", {
"pending-delete": model.pendingDelete.get() == bm.bookmarkid,
})}
>
<div className={cn("focus-indicator", { active: isSelected })} />
<div className={clsx("focus-indicator", { active: isSelected })} />
<div className="bookmark-edit">
<div className="field">
<label className="label">Description (markdown)</label>
@ -198,12 +198,12 @@ class Bookmark extends React.Component<BookmarkProps, {}> {
}
return (
<div
className={cn("bookmark focus-parent", {
className={clsx("bookmark focus-parent", {
"pending-delete": model.pendingDelete.get() == bm.bookmarkid,
})}
onClick={this.handleClick}
>
<div className={cn("focus-indicator", { active: isSelected })} />
<div className={clsx("focus-indicator", { active: isSelected })} />
<div className="bookmark-id-div">{bm.bookmarkid.substr(0, 8)}</div>
<div className="bookmark-content">
<If condition={hasDesc}>

View File

@ -1,6 +1,6 @@
import * as React from "react";
import { boundMethod } from "autobind-decorator";
import cn from "classnames";
import { clsx } from "clsx";
import "./button.less";
@ -35,7 +35,7 @@ class Button extends React.Component<ButtonProps> {
return (
<button
className={cn("wave-button", { disabled }, { "term-inline": termInline }, className)}
className={clsx("wave-button", { disabled }, { "term-inline": termInline }, className)}
onClick={this.handleClick}
disabled={disabled}
style={style}

View File

@ -3,7 +3,7 @@
import * as React from "react";
import * as mobx from "mobx";
import cn from "classnames";
import { clsx } from "clsx";
import "./checkbox.less";
@ -49,7 +49,7 @@ class Checkbox extends React.Component<
const checkboxId = id || this.generatedId;
return (
<div className={cn("checkbox", className)}>
<div className={clsx("checkbox", className)}>
<input
type="checkbox"
id={checkboxId}

View File

@ -3,7 +3,7 @@
import * as React from "react";
import { boundMethod } from "autobind-decorator";
import cn from "classnames";
import { clsx } from "clsx";
import { If } from "tsx-control-statements/components";
import { ReactComponent as CheckIcon } from "@/assets/icons/line/check.svg";
@ -41,7 +41,7 @@ class CmdStrCode extends React.Component<
render() {
let { isCopied, cmdstr, fontSize, limitHeight } = this.props;
return (
<div className={cn("cmdstr-code", { "is-large": fontSize == "large" }, { "limit-height": limitHeight })}>
<div className={clsx("cmdstr-code", { "is-large": fontSize == "large" }, { "limit-height": limitHeight })}>
<If condition={isCopied}>
<div key="copied" className="copied-indicator">
<div>copied</div>

View File

@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef, createRef } from "react";
import * as mobx from "mobx";
import ReactDOM from "react-dom";
import dayjs from "dayjs";
import cn from "classnames";
import { clsx } from "clsx";
import { Button } from "@/elements";
import { If } from "tsx-control-statements/components";
import { GlobalModel } from "@/models";
@ -102,7 +102,7 @@ const DatePicker: React.FC<DatePickerProps> = ({ selectedDate, format = "MM/DD/Y
return (
<div className="day-picker-header">
<div
className={cn({ fade: showYearAccordion })}
className={clsx({ fade: showYearAccordion })}
onClick={() => {
if (!showYearAccordion) {
setExpandedYear(selDate.year()); // Set expandedYear when opening accordion
@ -111,7 +111,7 @@ const DatePicker: React.FC<DatePickerProps> = ({ selectedDate, format = "MM/DD/Y
}}
>
{selDate.format("MMMM YYYY")}
<span className={cn("dropdown-arrow", { fade: showYearAccordion })}></span>
<span className={clsx("dropdown-arrow", { fade: showYearAccordion })}></span>
</div>
<If condition={!showYearAccordion}>
<div className="arrows">
@ -250,14 +250,14 @@ const DatePicker: React.FC<DatePickerProps> = ({ selectedDate, format = "MM/DD/Y
</div>
<If condition={expandedYear === year}>
<div
className={cn("month-container", {
className={clsx("month-container", {
expanded: expandedYear === year,
})}
>
{Array.from({ length: 12 }, (_, i) => i + 1).map((month) => (
<div
key={month}
className={cn("month", {
className={clsx("month", {
selected: year === currentYear && month === selDate.month() + 1,
})}
onClick={() => handleMonthYearSelect(month, year)}

View File

@ -4,7 +4,7 @@
import * as React from "react";
import * as mobxReact from "mobx-react";
import { boundMethod } from "autobind-decorator";
import cn from "classnames";
import { clsx } from "clsx";
import { If } from "tsx-control-statements/components";
import ReactDOM from "react-dom";
import { v4 as uuidv4 } from "uuid";
@ -239,11 +239,11 @@ class Dropdown extends React.Component<DropdownProps, DropdownState> {
const dropdownMenu = isOpen
? ReactDOM.createPortal(
<div className={cn("wave-dropdown-menu")} ref={this.menuRef} style={this.calculatePosition()}>
<div className={clsx("wave-dropdown-menu")} ref={this.menuRef} style={this.calculatePosition()}>
{options.map((option, index) => (
<div
key={option.value}
className={cn("wave-dropdown-item unselectable", {
className={clsx("wave-dropdown-item unselectable", {
"wave-dropdown-item-highlighted": index === highlightedIndex,
})}
onClick={(e) => this.handleSelect(option, e)}
@ -265,7 +265,7 @@ class Dropdown extends React.Component<DropdownProps, DropdownState> {
}
return (
<div
className={cn("wave-dropdown", className, {
className={clsx("wave-dropdown", className, {
"wave-dropdown-error": isError,
"no-label": !label,
})}
@ -279,7 +279,7 @@ class Dropdown extends React.Component<DropdownProps, DropdownState> {
{decoration?.startDecoration && <>{decoration.startDecoration}</>}
<If condition={label}>
<div
className={cn("wave-dropdown-label unselectable", {
className={clsx("wave-dropdown-label unselectable", {
float: shouldLabelFloat,
"offset-left": decoration?.startDecoration,
})}
@ -288,14 +288,14 @@ class Dropdown extends React.Component<DropdownProps, DropdownState> {
</div>
</If>
<div
className={cn("wave-dropdown-display unselectable truncate", {
className={clsx("wave-dropdown-display unselectable truncate", {
"offset-left": decoration?.startDecoration,
})}
style={selectedOptionLabelStyle}
>
{selectedOptionLabel}
</div>
<div className={cn("wave-dropdown-arrow", { "wave-dropdown-arrow-rotate": isOpen })}>
<div className={clsx("wave-dropdown-arrow", { "wave-dropdown-arrow-rotate": isOpen })}>
<i className="fa-sharp fa-solid fa-chevron-down"></i>
</div>
{dropdownMenu}

View File

@ -5,7 +5,7 @@ import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import cn from "classnames";
import { clsx } from "clsx";
import { If } from "tsx-control-statements/components";
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "@/util/keyutil";
import { GlobalModel } from "@/models";
@ -121,7 +121,7 @@ class InlineSettingsTextEdit extends React.Component<
render() {
if (this.isEditing.get()) {
return (
<div className={cn("settings-input inline-edit", "edit-active")}>
<div className={clsx("settings-input inline-edit", "edit-active")}>
<div className="field has-addons">
<div className="control">
<input
@ -163,7 +163,7 @@ class InlineSettingsTextEdit extends React.Component<
);
} else {
return (
<div onClick={this.clickEdit} className={cn("settings-input inline-edit", "edit-not-active")}>
<div onClick={this.clickEdit} className={clsx("settings-input inline-edit", "edit-not-active")}>
{this.props.text}
<If condition={this.props.showIcon}>
<i className="fa-sharp fa-solid fa-pen" />

View File

@ -3,7 +3,7 @@
import * as React from "react";
import * as mobxReact from "mobx-react";
import cn from "classnames";
import { clsx } from "clsx";
import "./inputdecoration.less";
@ -18,7 +18,7 @@ class InputDecoration extends React.Component<InputDecorationProps, {}> {
const { children, position = "end" } = this.props;
return (
<div
className={cn("wave-input-decoration", {
className={clsx("wave-input-decoration", {
"start-position": position === "start",
"end-position": position === "end",
})}

View File

@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import * as React from "react";
import cn from "classnames";
import { clsx } from "clsx";
import { ButtonProps } from "./button";
interface LinkButtonProps extends ButtonProps {
@ -16,7 +16,7 @@ class LinkButton extends React.Component<LinkButtonProps> {
const { leftIcon, rightIcon, children, className, ...rest } = this.props;
return (
<a {...rest} className={cn(`wave-button link-button`, className)}>
<a {...rest} className={clsx(`wave-button link-button`, className)}>
{leftIcon && <span className="icon-left">{leftIcon}</span>}
{children}
{rightIcon && <span className="icon-right">{rightIcon}</span>}

View File

@ -3,7 +3,7 @@
import * as React from "react";
import * as mobxReact from "mobx-react";
import cn from "classnames";
import { clsx } from "clsx";
import { GlobalModel } from "@/models";
import "./mainview.less";
@ -24,7 +24,7 @@ class MainView extends React.Component<{
const maxWidthSubtractor = sidebarModel.getCollapsed() ? 0 : sidebarModel.getWidth();
return (
<div
className={cn("mainview", this.props.className)}
className={clsx("mainview", this.props.className)}
style={{ maxWidth: `calc(100vw - ${maxWidthSubtractor}px)` }}
>
<div className="header-container">

View File

@ -5,7 +5,7 @@ import * as React from "react";
import * as mobxReact from "mobx-react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import cn from "classnames";
import { clsx } from "clsx";
import { GlobalModel } from "@/models";
import { v4 as uuidv4 } from "uuid";
@ -22,7 +22,7 @@ function LinkRenderer(props: any): any {
}
function HeaderRenderer(props: any, hnum: number): any {
return <div className={cn("title", "is-" + hnum)}>{props.children}</div>;
return <div className={clsx("title", "is-" + hnum)}>{props.children}</div>;
}
function CodeRenderer(props: any): any {
@ -53,7 +53,7 @@ class CodeBlockMarkdown extends React.Component<
console.log("this.blockIndex", this.blockIndex);
let selected = this.blockIndex == this.props.codeSelectSelectedIndex;
return (
<pre ref={this.blockRef} className={cn({ selected: selected })} onClick={this.handleClick}>
<pre ref={this.blockRef} className={clsx({ selected: selected })} onClick={this.handleClick}>
{this.props.children}
</pre>
);
@ -108,7 +108,7 @@ class Markdown extends React.Component<
pre: (props) => this.codeBlockRenderer(props, codeSelect, curCodeSelectIndex, this.curUuid),
};
return (
<div className={cn("markdown content", this.props.extraClassName)} style={this.props.style}>
<div className={clsx("markdown content", this.props.extraClassName)} style={this.props.style}>
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
{text}
</ReactMarkdown>

View File

@ -4,7 +4,7 @@
import * as React from "react";
import * as mobxReact from "mobx-react";
import { boundMethod } from "autobind-decorator";
import cn from "classnames";
import { clsx } from "clsx";
import { If } from "tsx-control-statements/components";
import { TextFieldState, TextField } from "./textfield";
@ -48,7 +48,7 @@ class PasswordField extends TextField {
// The input should always receive the real value
const inputProps = {
className: cn("wave-textfield-inner-input", { "offset-left": decoration?.startDecoration }),
className: clsx("wave-textfield-inner-input", { "offset-left": decoration?.startDecoration }),
ref: this.inputRef,
id: label,
value: inputValue, // Always use the real value here
@ -63,7 +63,7 @@ class PasswordField extends TextField {
return (
<div
className={cn(`wave-textfield wave-password ${className || ""}`, {
className={clsx(`wave-textfield wave-password ${className || ""}`, {
focused: focused,
error: error,
"no-label": !label,
@ -72,7 +72,7 @@ class PasswordField extends TextField {
{decoration?.startDecoration && <>{decoration.startDecoration}</>}
<div className="wave-textfield-inner">
<label
className={cn("wave-textfield-inner-label", {
className={clsx("wave-textfield-inner-label", {
float: this.state.hasContent || this.state.focused || placeholder,
"offset-left": decoration?.startDecoration,
})}

View File

@ -5,7 +5,7 @@ import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import cn from "classnames";
import { clsx } from "clsx";
import { GlobalCommandRunner, SidebarModel } from "@/models";
import { MagicLayout } from "@/app/magiclayout";
@ -142,7 +142,7 @@ class ResizableSidebar extends React.Component<ResizableSidebarProps> {
const isCollapsed = model.getCollapsed();
return (
<div className={cn("sidebar", className, { collapsed: isCollapsed })} style={{ width, minWidth: width }}>
<div className={clsx("sidebar", className, { collapsed: isCollapsed })} style={{ width, minWidth: width }}>
<div className="sidebar-content">{children(this.toggleCollapsed)}</div>
<div
className="sidebar-handle"

View File

@ -3,7 +3,7 @@
import * as React from "react";
import { isBlank } from "@/util/util";
import cn from "classnames";
import { clsx } from "clsx";
class TabIcon extends React.Component<{ icon: string; color: string }> {
render() {
@ -20,7 +20,7 @@ class TabIcon extends React.Component<{ icon: string; color: string }> {
color = "green";
}
return (
<div className={cn("tabicon", "color-" + color)}>
<div className={clsx("tabicon", "color-" + color)}>
<i className={iconClass} />
</div>
);

View File

@ -3,7 +3,7 @@
import * as React from "react";
import { boundMethod } from "autobind-decorator";
import cn from "classnames";
import { clsx } from "clsx";
import { If } from "tsx-control-statements/components";
import "./textfield.less";
@ -140,7 +140,7 @@ class TextField extends React.Component<TextFieldProps, TextFieldState> {
return (
<div
className={cn("wave-textfield", className, {
className={clsx("wave-textfield", className, {
focused: focused,
error: error,
disabled: disabled,
@ -154,7 +154,7 @@ class TextField extends React.Component<TextFieldProps, TextFieldState> {
<div className="wave-textfield-inner">
<If condition={label}>
<label
className={cn("wave-textfield-inner-label", {
className={clsx("wave-textfield-inner-label", {
float: this.state.hasContent || this.state.focused || placeholder,
"offset-left": decoration?.startDecoration,
})}
@ -164,7 +164,7 @@ class TextField extends React.Component<TextFieldProps, TextFieldState> {
</label>
</If>
<input
className={cn("wave-textfield-inner-input", "wave-input", {
className={clsx("wave-textfield-inner-input", "wave-input", {
"offset-left": decoration?.startDecoration,
})}
ref={this.inputRef}

View File

@ -4,7 +4,7 @@
import * as React from "react";
import * as mobxReact from "mobx-react";
import { boundMethod } from "autobind-decorator";
import cn from "classnames";
import { clsx } from "clsx";
import ReactDOM from "react-dom";
import "./tooltip.less";
@ -63,7 +63,7 @@ class Tooltip extends React.Component<TooltipProps, TooltipState> {
const style = this.calculatePosition();
return ReactDOM.createPortal(
<div className={cn("wave-tooltip", this.props.className)} style={style}>
<div className={clsx("wave-tooltip", this.props.className)} style={style}>
{this.props.icon && <div className="wave-tooltip-icon">{this.props.icon}</div>}
<div className="wave-tooltip-message">{this.props.message}</div>
</div>,

View File

@ -1,5 +1,5 @@
import React, { Component, ReactNode } from "react";
import cn from "classnames";
import { clsx } from "clsx";
interface ErrorBoundaryState {
hasError: boolean;
@ -43,7 +43,7 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
const { plugin } = this.props;
return (
<div className={cn("load-error-text", { "view-error": !plugin })}>
<div className={clsx("load-error-text", { "view-error": !plugin })}>
<div>{`${error?.message}`}</div>
{plugin && <div>An error occurred in the {plugin} plugin</div>}
</div>

View File

@ -1,9 +1,6 @@
import React from "react";
import cn from "classnames";
import { clsx } from "clsx";
import { ReactComponent as SpinnerIndicator } from "@/assets/icons/spinner-indicator.svg";
import { boundMethod } from "autobind-decorator";
import * as mobx from "mobx";
import * as mobxReact from "mobx-react";
import * as appconst from "@/app/appconst";
import { ReactComponent as RotateIconSvg } from "@/assets/icons/line/rotate.svg";
@ -15,77 +12,52 @@ interface PositionalIconProps {
divRef?: React.RefObject<HTMLDivElement>;
}
export class FrontIcon extends React.Component<PositionalIconProps> {
render() {
return (
<div
ref={this.props.divRef}
className={cn("front-icon", "positional-icon", this.props.className)}
onClick={this.props.onClick}
>
<div className="positional-icon-inner">{this.props.children}</div>
</div>
);
}
}
export const FrontIcon: React.FC<PositionalIconProps> = (props) => {
return (
<div
ref={props.divRef}
className={clsx("front-icon", "positional-icon", props.className)}
onClick={props.onClick}
>
<div className="positional-icon-inner">{props.children}</div>
</div>
);
};
export class CenteredIcon extends React.Component<PositionalIconProps> {
render() {
return (
<div
ref={this.props.divRef}
className={cn("centered-icon", "positional-icon", this.props.className)}
onClick={this.props.onClick}
>
<div className="positional-icon-inner">{this.props.children}</div>
</div>
);
}
}
export const CenteredIcon: React.FC<PositionalIconProps> = (props) => {
return (
<div
ref={props.divRef}
className={clsx("centered-icon", "positional-icon", props.className)}
onClick={props.onClick}
>
<div className="positional-icon-inner">{props.children}</div>
</div>
);
};
interface ActionsIconProps {
onClick: React.MouseEventHandler<HTMLDivElement>;
}
export class ActionsIcon extends React.Component<ActionsIconProps> {
render() {
return (
<CenteredIcon className="actions" onClick={this.props.onClick}>
<div className="icon hoverEffect fa-sharp fa-solid fa-1x fa-ellipsis-vertical"></div>
</CenteredIcon>
);
}
}
export const ActionsIcon: React.FC<ActionsIconProps> = (props) => {
return (
<CenteredIcon className="actions" onClick={props.onClick}>
<div className="icon hoverEffect fa-sharp fa-solid fa-1x fa-ellipsis-vertical"></div>
</CenteredIcon>
);
};
class SyncSpin extends React.Component<{
classRef?: React.RefObject<HTMLDivElement>;
export const SyncSpin: React.FC<{
classRef?: React.RefObject<Element>;
children?: React.ReactNode;
shouldSync?: boolean;
}> {
listenerAdded: boolean = false;
}> = (props) => {
const { classRef, children, shouldSync } = props;
const [listenerAdded, setListenerAdded] = React.useState(false);
componentDidMount() {
this.syncSpinner();
}
componentDidUpdate() {
this.syncSpinner();
}
componentWillUnmount(): void {
const classRef = this.props.classRef;
if (classRef.current != null && this.listenerAdded) {
const elem = classRef.current;
const svgElem = elem.querySelector("svg");
if (svgElem != null) {
svgElem.removeEventListener("animationstart", this.handleAnimationStart);
}
}
}
@boundMethod
handleAnimationStart(e: AnimationEvent) {
const classRef = this.props.classRef;
const handleAnimationStart = (e: AnimationEvent) => {
const classRef = props.classRef;
if (classRef.current == null) {
return;
}
@ -98,10 +70,9 @@ class SyncSpin extends React.Component<{
return;
}
animArr[0].startTime = 0;
}
};
syncSpinner() {
const { classRef, shouldSync } = this.props;
React.useEffect(() => {
const shouldSyncVal = shouldSync ?? true;
if (!shouldSyncVal || classRef.current == null) {
return;
@ -111,21 +82,24 @@ class SyncSpin extends React.Component<{
if (svgElem == null) {
return;
}
if (!this.listenerAdded) {
svgElem.addEventListener("animationstart", this.handleAnimationStart);
this.listenerAdded = true;
if (!listenerAdded) {
svgElem.addEventListener("animationstart", handleAnimationStart);
setListenerAdded(true);
}
const animArr = svgElem.getAnimations();
if (animArr == null || animArr.length == 0) {
return;
}
animArr[0].startTime = 0;
}
render() {
return this.props.children;
}
}
return () => {
if (listenerAdded) {
svgElem.removeEventListener("animationstart", handleAnimationStart);
setListenerAdded(false);
}
};
});
return children;
};
interface StatusIndicatorProps {
/**
@ -142,97 +116,79 @@ interface StatusIndicatorProps {
/**
* This component is used to show the status of a command. It will show a spinner around the status indicator if there are running commands. It will also delay showing the spinner for a short time to prevent flickering.
*/
@mobxReact.observer
export class StatusIndicator extends React.Component<StatusIndicatorProps> {
iconRef: React.RefObject<HTMLDivElement> = React.createRef();
spinnerVisible: mobx.IObservableValue<boolean> = mobx.observable.box(false);
timeout: NodeJS.Timeout;
export const StatusIndicator: React.FC<StatusIndicatorProps> = (props) => {
const { level, className, runningCommands } = props;
const iconRef = React.useRef<HTMLDivElement>();
const [spinnerVisible, setSpinnerVisible] = React.useState(false);
const [timeoutState, setTimeoutState] = React.useState<NodeJS.Timeout>(undefined);
clearSpinnerTimeout() {
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
const clearSpinnerTimeout = () => {
if (timeoutState) {
clearTimeout(timeoutState);
setTimeoutState(undefined);
}
mobx.action(() => {
this.spinnerVisible.set(false);
})();
}
setSpinnerVisible(false);
};
/**
* This will apply a delay after there is a running command before showing the spinner. This prevents flickering for commands that return quickly.
*/
updateMountCallback() {
const runningCommands = this.props.runningCommands ?? false;
if (runningCommands && !this.timeout) {
this.timeout = setTimeout(
mobx.action(() => {
this.spinnerVisible.set(true);
}),
100
React.useEffect(() => {
if (runningCommands && !timeoutState) {
console.log("show spinner");
setTimeoutState(
setTimeout(() => {
setSpinnerVisible(true);
}, 100)
);
} else if (!runningCommands) {
this.clearSpinnerTimeout();
console.log("clear spinner");
clearSpinnerTimeout();
}
}
return () => {
clearSpinnerTimeout();
};
}, [runningCommands]);
componentDidUpdate(): void {
this.updateMountCallback();
}
componentDidMount(): void {
this.updateMountCallback();
}
componentWillUnmount(): void {
this.clearSpinnerTimeout();
}
render() {
const { level, className, runningCommands } = this.props;
const spinnerVisible = this.spinnerVisible.get();
let statusIndicator = null;
if (level != appconst.StatusIndicatorLevel.None || spinnerVisible) {
let indicatorLevelClass = null;
switch (level) {
case appconst.StatusIndicatorLevel.Output:
indicatorLevelClass = "output";
break;
case appconst.StatusIndicatorLevel.Success:
indicatorLevelClass = "success";
break;
case appconst.StatusIndicatorLevel.Error:
indicatorLevelClass = "error";
break;
}
const spinnerVisibleClass = spinnerVisible ? "spinner-visible" : null;
statusIndicator = (
<CenteredIcon
divRef={this.iconRef}
className={cn(className, indicatorLevelClass, spinnerVisibleClass, "status-indicator")}
>
<SpinnerIndicator className={spinnerVisible ? "spin" : null} />
</CenteredIcon>
);
let statusIndicator = null;
if (level != appconst.StatusIndicatorLevel.None || spinnerVisible) {
let indicatorLevelClass = null;
switch (level) {
case appconst.StatusIndicatorLevel.Output:
indicatorLevelClass = "output";
break;
case appconst.StatusIndicatorLevel.Success:
indicatorLevelClass = "success";
break;
case appconst.StatusIndicatorLevel.Error:
indicatorLevelClass = "error";
break;
}
return (
<SyncSpin classRef={this.iconRef} shouldSync={runningCommands}>
{statusIndicator}
</SyncSpin>
const spinnerVisibleClass = spinnerVisible ? "spinner-visible" : null;
statusIndicator = (
<CenteredIcon
divRef={iconRef}
className={clsx(className, indicatorLevelClass, spinnerVisibleClass, "status-indicator")}
>
<SpinnerIndicator className={spinnerVisible ? "spin" : null} />
</CenteredIcon>
);
}
}
return (
<SyncSpin classRef={iconRef} shouldSync={runningCommands}>
{statusIndicator}
</SyncSpin>
);
};
export class RotateIcon extends React.Component<{
className?: string;
onClick?: React.MouseEventHandler<HTMLDivElement>;
}> {
iconRef: React.RefObject<HTMLDivElement> = React.createRef();
render() {
return (
<SyncSpin classRef={this.iconRef}>
<RotateIconSvg className={this.props.className ?? ""} />
</SyncSpin>
);
}
}
export const RotateIcon: React.FC<{ className?: string; onClick?: React.MouseEventHandler<SVGSVGElement> }> = (
props
) => {
const iconRef = React.useRef<SVGSVGElement>();
return (
<SyncSpin classRef={iconRef}>
<RotateIconSvg ref={iconRef} className={props.className ?? ""} onClick={props.onClick} />
</SyncSpin>
);
};

View File

@ -9,7 +9,7 @@ import { GlobalModel } from "@/models";
import { Modal, LinkButton } from "@/elements";
import * as util from "@/util/util";
import * as appconst from "@/app/appconst";
import cn from "classnames";
import { clsx } from "clsx";
import { If } from "tsx-control-statements/components";
import logo from "@/assets/waveterm-logo-with-bg.svg";
@ -36,7 +36,7 @@ class AboutModal extends React.Component<{}, {}> {
const isUpToDate = !showUpdateStatus || GlobalModel.appUpdateStatus.get() !== "ready";
return (
<div className={cn("status", { outdated: !isUpToDate })}>
<div className={clsx("status", { outdated: !isUpToDate })}>
<If condition={!isUpToDate}>
<div>
<i className="fa-sharp fa-solid fa-triangle-exclamation" />

View File

@ -6,7 +6,7 @@ import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { For } from "tsx-control-statements/components";
import cn from "classnames";
import { clsx } from "clsx";
import { GlobalModel, GlobalCommandRunner, Screen } from "@/models";
import { SettingsError, Modal, Dropdown, Tooltip } from "@/elements";
import * as util from "@/util/util";

View File

@ -6,7 +6,7 @@ import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { If, For } from "tsx-control-statements/components";
import cn from "classnames";
import { clsx } from "clsx";
import { GlobalModel, GlobalCommandRunner } from "@/models";
import { Modal, TextField, InputDecoration, Tooltip } from "@/elements";
import * as util from "@/util/util";
@ -287,7 +287,7 @@ class TabSwitcherModal extends React.Component<{}, {}> {
<div
key={option.sessionId + "/" + option.screenId}
ref={this.optionRefs[index]}
className={cn("search-option unselectable", {
className={clsx("search-option unselectable", {
"focused-option": this.focusedIdx.get() === index,
})}
onClick={() => this.handleSelect(index)}

View File

@ -6,7 +6,7 @@ import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { If, For } from "tsx-control-statements/components";
import cn from "classnames";
import { clsx } from "clsx";
import { GlobalModel, GlobalCommandRunner, RemotesModel } from "@/models";
import { Modal, Tooltip, Button, Status } from "@/elements";
import * as util from "@/util/util";
@ -140,12 +140,12 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
renderInstallStatus(remote: RemoteType): any {
let statusStr: string = null;
if (remote.installstatus == "disconnected") {
if (remote.needsmshellupgrade) {
statusStr = "mshell " + remote.mshellversion + " - needs upgrade";
} else if (util.isBlank(remote.mshellversion)) {
statusStr = "mshell unknown";
if (remote.needswaveshellupgrade) {
statusStr = "waveshell " + remote.waveshellversion + " - needs upgrade";
} else if (util.isBlank(remote.waveshellversion)) {
statusStr = "waveshell unknown";
} else {
statusStr = "mshell " + remote.mshellversion + " - current";
statusStr = "waveshell " + remote.waveshellversion + " - current";
}
} else {
statusStr = remote.installstatus;
@ -231,7 +231,7 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
} else if (remote.status == "disconnected") {
buttons.push(connectButton);
} else if (remote.status == "error") {
if (remote.needsmshellupgrade) {
if (remote.needswaveshellupgrade) {
if (remote.installstatus == "connecting") {
buttons.push(cancelInstallButton);
} else {
@ -270,7 +270,7 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
} else if (remote.status == "error") {
if (remote.noinitpk) {
message = "Error, could not connect.";
} else if (remote.needsmshellupgrade) {
} else if (remote.needswaveshellupgrade) {
if (remote.installstatus == "connecting") {
message = "Installing...";
} else {
@ -370,7 +370,7 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
</div>
<div
key="term"
className={cn(
className={clsx(
"terminal-wrapper",
{ focus: isTermFocused },
remote != null ? "status-" + remote.status : null

View File

@ -6,7 +6,7 @@ import * as mobxReact from "mobx-react";
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel } from "@/models";
import cn from "classnames";
import { clsx } from "clsx";
import { isBlank } from "@/util/util";
import "./prompt.less";
@ -108,7 +108,7 @@ class Prompt extends React.Component<
let remoteElem = null;
if (remoteStr != "local") {
remoteElem = (
<span title={remoteTitle} className={cn("term-prompt-remote", remoteColorClass)}>
<span title={remoteTitle} className={clsx("term-prompt-remote", remoteColorClass)}>
[{remoteStr}]{" "}
</span>
);
@ -124,10 +124,10 @@ class Prompt extends React.Component<
render() {
const rptr = this.props.rptr;
if (rptr == null || isBlank(rptr.remoteid)) {
return <span className={cn("term-prompt", "color-green")}>&nbsp;</span>;
return <span className={clsx("term-prompt", "color-green")}>&nbsp;</span>;
}
let { remoteElem, isRoot } = this.getRemoteElem();
let termClassNames = cn(
let termClassNames = clsx(
"term-prompt",
{ "term-prompt-color": this.props.color },
{ "term-prompt-isroot": isRoot }
@ -172,16 +172,16 @@ class Prompt extends React.Component<
</span>
);
}
if (!isBlank(festate["K8SCONTEXT"])) {
const k8sContext = festate["K8SCONTEXT"];
const k8sNs = festate["K8SNAMESPACE"];
k8sElem = (
<span title="k8s context:namespace" className="term-prompt-k8s">
k8s:({k8sContext}
{isBlank(k8sNs) ? "" : ":" + k8sNs}){" "}
</span>
);
}
// if (!isBlank(festate["K8SCONTEXT"])) {
// const k8sContext = festate["K8SCONTEXT"];
// const k8sNs = festate["K8SNAMESPACE"];
// k8sElem = (
// <span title="k8s context:namespace" className="term-prompt-k8s">
// k8s:({k8sContext}
// {isBlank(k8sNs) ? "" : ":" + k8sNs}){" "}
// </span>
// );
// }
return (
<span className={termClassNames}>
{remoteElem} {cwdElem} {branchElem} {condaElem} {pythonElem} {k8sElem}

View File

@ -6,7 +6,7 @@ import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { If, For } from "tsx-control-statements/components";
import cn from "classnames";
import { clsx } from "clsx";
import { GlobalModel, RemotesModel, GlobalCommandRunner } from "@/models";
import { Button, Status } from "@/common/elements";
import * as util from "@/util/util";
@ -185,7 +185,7 @@ class ConnectionsView extends React.Component<{ model: RemotesModel }, { hovered
<For index="idx" each="item" of={items}>
<tr
key={item.remoteid}
className={cn("connections-item", {
className={clsx("connections-item", {
hovered: this.state.hoveredItemId === item.remoteid,
})}
onClick={() => this.handleRead(item.remoteid)} // Moved onClick here

View File

@ -7,7 +7,7 @@ import * as mobx from "mobx";
import { If, For } from "tsx-control-statements/components";
import { sprintf } from "sprintf-js";
import { boundMethod } from "autobind-decorator";
import cn from "classnames";
import { clsx } from "clsx";
import { GlobalModel, GlobalCommandRunner } from "@/models";
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
@ -137,7 +137,7 @@ class HistoryCmdStr extends React.Component<
render() {
const { cmdstr, fontSize, limitHeight } = this.props;
return (
<div className={cn("cmdstr-code", { "is-large": fontSize == "large" }, { "limit-height": limitHeight })}>
<div className={clsx("cmdstr-code", { "is-large": fontSize == "large" }, { "limit-height": limitHeight })}>
<div key="code" className="code-div">
<code>{cmdstr}</code>
</div>
@ -490,7 +490,7 @@ class HistoryView extends React.Component<{}, {}> {
</Button>
</div>
</div>
<div key="control1" className={cn("control-bar", "is-top", { "is-hidden": items.length == 0 })}>
<div key="control1" className={clsx("control-bar", "is-top", { "is-hidden": items.length == 0 })}>
<div className="control-checkbox" onClick={this.handleControlCheckbox} title="Toggle Selection">
<HistoryCheckbox
checked={numSelected > 0 && numSelected == items.length}
@ -498,7 +498,7 @@ class HistoryView extends React.Component<{}, {}> {
/>
</div>
<div
className={cn(
className={clsx(
"control-button delete-button",
{ "is-disabled": numSelected == 0 },
{ "is-active": hvm.deleteActive.get() }
@ -515,14 +515,14 @@ class HistoryView extends React.Component<{}, {}> {
Showing {offset + 1}-{offset + items.length}
</div>
<div
className={cn("showing-btn", { "is-disabled": offset == 0 })}
className={clsx("showing-btn", { "is-disabled": offset == 0 })}
onClick={offset != 0 ? this.handlePrev : null}
>
<ChevronLeftIcon className="icon" />
</div>
<div className="btn-spacer" />
<div
className={cn("showing-btn", { "is-disabled": !hasMore })}
className={clsx("showing-btn", { "is-disabled": !hasMore })}
onClick={hasMore ? this.handleNext : null}
>
<ChevronRightIcon className="icon" />
@ -538,7 +538,7 @@ class HistoryView extends React.Component<{}, {}> {
<For index="idx" each="item" of={items}>
<div
key={item.historyid}
className={cn("row history-item", {
className={clsx("row history-item", {
"is-selected": hvm.selectedItems.get(item.historyid),
})}
>
@ -608,21 +608,21 @@ class HistoryView extends React.Component<{}, {}> {
</div>
<div
key="control2"
className={cn("control-bar", "is-bottom", { "is-hidden": items.length == 0 || !hasMore })}
className={clsx("control-bar", "is-bottom", { "is-hidden": items.length == 0 || !hasMore })}
>
<div className="spacer" />
<div className="showing-text">
Showing {offset + 1}-{offset + items.length}
</div>
<div
className={cn("showing-btn", { "is-disabled": offset == 0 })}
className={clsx("showing-btn", { "is-disabled": offset == 0 })}
onClick={offset != 0 ? this.handlePrev : null}
>
<ChevronLeftIcon className="icon" />
</div>
<div className="btn-spacer" />
<div
className={cn("showing-btn", { "is-disabled": !hasMore })}
className={clsx("showing-btn", { "is-disabled": !hasMore })}
onClick={hasMore ? this.handleNext : null}
>
<ChevronRightIcon className="icon" />

View File

@ -11,7 +11,7 @@ import localizedFormat from "dayjs/plugin/localizedFormat";
import { Choose, If, Otherwise, When } from "tsx-control-statements/components";
import { GlobalModel, GlobalCommandRunner, Cmd } from "@/models";
import { termHeightFromRows } from "@/util/textmeasure";
import cn from "classnames";
import { clsx } from "clsx";
import { getTermPtyData } from "@/util/modelutil";
import { renderCmdText } from "@/common/elements";
@ -172,7 +172,7 @@ class LineActions extends React.Component<{ screen: LineContainerType; line: Lin
<div
key="bookmark"
title="Bookmark"
className={cn("line-icon", "line-bookmark")}
className={clsx("line-icon", "line-bookmark")}
onClick={this.clickBookmark}
>
<i className="fa-sharp fa-regular fa-bookmark fa-fw" />
@ -180,7 +180,7 @@ class LineActions extends React.Component<{ screen: LineContainerType; line: Lin
<div
key="minimize"
title={`${isMinimized ? "Show Output" : "Hide Output"}`}
className={cn("line-icon", isMinimized ? "active" : "")}
className={clsx("line-icon", isMinimized ? "active" : "")}
onClick={this.clickMinimize}
>
<Choose>
@ -220,7 +220,7 @@ class LineActions extends React.Component<{ screen: LineContainerType; line: Lin
<div
key="bookmark"
title="Bookmark"
className={cn("line-icon", "line-bookmark")}
className={clsx("line-icon", "line-bookmark")}
onClick={this.clickBookmark}
>
<i className="fa-sharp fa-regular fa-bookmark fa-fw" />
@ -254,7 +254,7 @@ class LineHeader extends React.Component<{ screen: LineContainerType; line: Line
<React.Fragment>
<div
key="meta2"
className={cn(
className={clsx(
"meta meta-line2 cmdtext-expanded no-highlight-scrollbar scrollbar-hide-until-hover",
{
"is-multiline": isMultiLine,
@ -304,7 +304,7 @@ class LineHeader extends React.Component<{ screen: LineContainerType; line: Line
const { line, cmd } = this.props;
const hidePrompt = getIsHidePrompt(line);
return (
<div key="header" className={cn("line-header", { "hide-prompt": hidePrompt })}>
<div key="header" className={clsx("line-header", { "hide-prompt": hidePrompt })}>
{this.renderMeta1(cmd)}
<If condition={!hidePrompt}>{this.renderCmdText(cmd)}</If>
</div>
@ -349,7 +349,7 @@ class SmallLineAvatar extends React.Component<{ line: LineType; cmd: Cmd; onRigh
return (
<>
<div className="linenum">{lineNumStr}</div>
<div title={iconTitle} className={cn("status-icon", "status-" + status)}>
<div title={iconTitle} className={clsx("status-icon", "status-" + status)}>
{icon}
</div>
</>
@ -567,7 +567,7 @@ class LineCmd extends React.Component<
const { screen, line, width } = this.props;
contentHeight = screen.getUsedRows(lineutil.getRendererContext(line), line, cmd, width);
}
const mainDivCn = cn("line", "line-cmd");
const mainDivCn = clsx("line", "line-cmd");
if (DebugHeightProblems && line.linenum >= MinLine && line.linenum <= MaxLine) {
heightLog[line.linenum] = heightLog[line.linenum] || {};
heightLog[line.linenum].contentHeight = contentHeight;
@ -582,7 +582,7 @@ class LineCmd extends React.Component<
>
<LineHeader screen={screen} line={line} cmd={cmd} />
<div
className={cn("line-content", { "zero-height": contentHeight == 0 })}
className={clsx("line-content", { "zero-height": contentHeight == 0 })}
style={{ height: contentHeight }}
/>
</div>
@ -801,7 +801,7 @@ class LineCmd extends React.Component<
.get();
const isRunning = cmd.isRunning();
const cmdError = cmdShouldMarkError(cmd);
const mainDivCn = cn(
const mainDivCn = clsx(
"line",
"line-cmd",
{ selected: isSelected },
@ -830,7 +830,7 @@ class LineCmd extends React.Component<
onContextMenu={this.handleContextMenu}
>
<If condition={isSelected || cmdError}>
<div key="mask" className={cn("line-mask", { "error-mask": cmdError })}></div>
<div key="mask" className={clsx("line-mask", { "error-mask": cmdError })}></div>
</If>
<LineActions screen={screen} line={line} cmd={cmd} />
<LineHeader screen={screen} line={line} cmd={cmd} />
@ -971,7 +971,7 @@ class LineText extends React.Component<
name: "computed-isSelected",
})
.get();
const mainClass = cn("line", "line-text", "focus-parent", { selected: isSelected });
const mainClass = clsx("line", "line-text", "focus-parent", { selected: isSelected });
return (
<div
className={mainClass}

View File

@ -7,7 +7,7 @@ import * as mobx from "mobx";
import { sprintf } from "sprintf-js";
import { boundMethod } from "autobind-decorator";
import { If, For, When, Otherwise, Choose } from "tsx-control-statements/components";
import cn from "classnames";
import { clsx } from "clsx";
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { debounce, throttle } from "throttle-debounce";
@ -496,7 +496,7 @@ class LinesView extends React.Component<
// let lineElem = <Line key={line.lineid} line={line} screen={screen} width={width} visible={this.visibleMap.get(lineNumStr)} staticRender={this.staticRender.get()} onHeightChange={this.onHeightChange} overrideCollapsed={this.collapsedMap.get(lineNumStr)} topBorder={topBorder} renderMode={renderMode}/>;
lineElements.push(lineElem);
}
let linesClass = cn("lines", renderMode == "normal" ? "lines-expanded" : "lines-collapsed", "wide-scrollbar");
let linesClass = clsx("lines", renderMode == "normal" ? "lines-expanded" : "lines-collapsed", "wide-scrollbar");
return (
<div key="lines" className={linesClass} onScroll={this.scrollHandler} ref={this.linesRef}>
<div className="lines-spacer"></div>

View File

@ -165,6 +165,11 @@ class AIChat extends React.Component<{}, {}> {
chatWindowRef: React.RefObject<HTMLDivElement> = React.createRef();
termFontSize: number = 14;
constructor(props) {
super(props);
mobx.makeObservable(this);
}
componentDidMount() {
const inputModel = GlobalModel.inputModel;
@ -223,8 +228,7 @@ class AIChat extends React.Component<{}, {}> {
return { numLines, linePos };
}
@mobx.action
@boundMethod
@mobx.action.bound
onTextAreaFocused(e: any) {
GlobalModel.inputModel.setAuxViewFocus(true);
GlobalModel.inputModel.setActiveAuxView(appconst.InputAuxView_AIChat);
@ -299,8 +303,7 @@ class AIChat extends React.Component<{}, {}> {
return true;
}
@mobx.action
@boundMethod
@mobx.action.bound
onKeyDown(e: any) {}
renderError(err: string): any {

View File

@ -5,7 +5,7 @@ import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import cn from "classnames";
import { clsx } from "clsx";
import dayjs from "dayjs";
import { If } from "tsx-control-statements/components";
@ -38,7 +38,7 @@ class SideBarItem extends React.Component<{
render() {
return (
<div
className={cn("item", "unselectable", "hoverEffect", this.props.className)}
className={clsx("item", "unselectable", "hoverEffect", this.props.className)}
onClick={this.props.onClick}
>
<FrontIcon>{this.props.frontIcon}</FrontIcon>
@ -201,7 +201,7 @@ class MainSideBar extends React.Component<MainSideBarProps, {}> {
return (
<SideBarItem
key={session.sessionId}
className={cn({ bold: isActive, highlight: showHighlight })}
className={clsx({ bold: isActive, highlight: showHighlight })}
frontIcon={<span className="index">{index + 1}</span>}
contents={session.name.get()}
endIcons={[
@ -269,7 +269,7 @@ class MainSideBar extends React.Component<MainSideBarProps, {}> {
<SideBarItem
key="history"
frontIcon={<i className="fa-sharp fa-regular fa-clock-rotate-left icon" />}
className={cn({ highlight: historyActive })}
className={clsx({ highlight: historyActive })}
contents="History"
endIcons={[<HotKeyIcon key="hotkey" hotkey="H" />]}
onClick={this.handleHistoryClick}
@ -278,7 +278,7 @@ class MainSideBar extends React.Component<MainSideBarProps, {}> {
<SideBarItem
key="connections"
frontIcon={<i className="fa-sharp fa-regular fa-globe icon " />}
className={cn({ highlight: connectionsActive })}
className={clsx({ highlight: connectionsActive })}
contents="Connections"
onClick={this.handleConnectionsClick}
/>
@ -325,7 +325,7 @@ class MainSideBar extends React.Component<MainSideBarProps, {}> {
<SideBarItem
key="settings"
frontIcon={<SettingsIcon className="icon" />}
className={cn({ highlight: settingsActive })}
className={clsx({ highlight: settingsActive })}
contents="Settings"
onClick={this.handleSettingsClick}
/>

View File

@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import * as React from "react";
import cn from "classnames";
import { clsx } from "clsx";
import { Choose, If, Otherwise, When } from "tsx-control-statements/components";
import { observer } from "mobx-react";
@ -24,7 +24,7 @@ export const AuxiliaryCmdView: React.FC<AuxiliaryCmdViewProps> = observer((props
const { title, className, iconClass, titleBarContents, children, onClose, onScrollbarInitialized } = props;
return (
<div className={cn("auxview", className)}>
<div className={clsx("auxview", className)}>
<If condition={title || onClose || titleBarContents || iconClass}>
<div className="auxview-titlebar">
<If condition={iconClass != null}>

View File

@ -6,7 +6,7 @@ import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { Choose, If, When } from "tsx-control-statements/components";
import cn from "classnames";
import { clsx } from "clsx";
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel, GlobalCommandRunner, Screen } from "@/models";
@ -86,9 +86,9 @@ class CmdInput extends React.Component<{}, {}> {
e.stopPropagation();
const inputModel = GlobalModel.inputModel;
if (inputModel.getActiveAuxView() === appconst.InputAuxView_AIChat) {
// inputModel.closeAuxView();
inputModel.closeAuxView();
} else {
// inputModel.openAIAssistantChat();
inputModel.openAIAssistantChat();
}
}
@ -185,16 +185,16 @@ class CmdInput extends React.Component<{}, {}> {
}
return (
<div ref={this.cmdInputRef} className={cn("cmd-input", hasOpenView, { active: focusVal })}>
<div ref={this.cmdInputRef} className={clsx("cmd-input", hasOpenView, { active: focusVal })}>
<Choose>
<When condition={openView === appconst.InputAuxView_History}>
<div className="cmd-input-grow-spacer"></div>
<HistoryInfo />
</When>
{/* <When condition={openView === appconst.InputAuxView_AIChat}>
<When condition={openView === appconst.InputAuxView_AIChat}>
<div className="cmd-input-grow-spacer"></div>
<AIChat />
</When> */}
</When>
<When condition={openView === appconst.InputAuxView_Info}>
<InfoMsg key="infomsg" />
</When>
@ -239,7 +239,7 @@ class CmdInput extends React.Component<{}, {}> {
<If condition={numRunningLines > 0}>
<div
key="running"
className={cn("cmdinput-icon", "running-cmds", { active: filterRunning })}
className={clsx("cmdinput-icon", "running-cmds", { active: filterRunning })}
title="Filter for Running Commands"
onClick={() => this.toggleFilter(screen)}
>
@ -277,7 +277,7 @@ class CmdInput extends React.Component<{}, {}> {
</If>
<div
key="input"
className={cn(
className={clsx(
"cmd-input-field field has-addons",
inputMode != null ? "inputmode-" + inputMode : null
)}

View File

@ -7,7 +7,7 @@ import * as mobx from "mobx";
import { sprintf } from "sprintf-js";
import { boundMethod } from "autobind-decorator";
import { If, For } from "tsx-control-statements/components";
import cn from "classnames";
import { clsx } from "clsx";
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel } from "@/models";
@ -132,7 +132,7 @@ class HItem extends React.Component<
return (
<div
key={hitem.historynum}
className={cn(
className={clsx(
"history-item",
{ "is-selected": isSelected },
{ "history-haderror": hitem.haderror },
@ -256,7 +256,7 @@ class HistoryInfo extends React.Component<{}, {}> {
onScrollbarInitialized={this.handleScrollbarInitialized}
>
<div
className={cn(
className={clsx(
"history-items",
{ "show-remotes": !opts.limitRemote },
{ "show-sessions": opts.queryType == "global" }

View File

@ -5,7 +5,7 @@ import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { If, For } from "tsx-control-statements/components";
import cn from "classnames";
import { clsx } from "clsx";
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel } from "@/models";
@ -64,7 +64,11 @@ class InfoMsg extends React.Component<{}, {}> {
}
return (
<AuxiliaryCmdView title={titleStr} className="cmd-input-info">
<AuxiliaryCmdView
title={titleStr}
className="cmd-input-info"
onClose={() => GlobalModel.inputModel.closeAuxView()}
>
<If condition={infoMsg?.infomsg}>
<div key="infomsg" className="info-msg">
<If condition={infoMsg.infomsghtml}>
@ -86,7 +90,7 @@ class InfoMsg extends React.Component<{}, {}> {
<div
onClick={() => this.handleCompClick(istr)}
key={idx}
className={cn(
className={clsx(
"info-comp",
{ "has-space": this.hasSpace(istr) },
{ "metacmd-comp": istr.startsWith("^") }

View File

@ -7,7 +7,7 @@ import * as mobx from "mobx";
import * as util from "@/util/util";
import { If } from "tsx-control-statements/components";
import { boundMethod } from "autobind-decorator";
import cn from "classnames";
import { clsx } from "clsx";
import { GlobalModel, GlobalCommandRunner, Screen } from "@/models";
import { getMonoFontSize } from "@/util/textmeasure";
import * as appconst from "@/app/appconst";
@ -617,8 +617,9 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
}
}
}
const renderCmdInputKeybindings = inputModel.shouldRenderAuxViewKeybindings(null);
const renderCmdInputKeybindings =
inputModel.shouldRenderAuxViewKeybindings(null) ||
inputModel.shouldRenderAuxViewKeybindings(appconst.InputAuxView_Info);
const renderHistoryKeybindings = inputModel.shouldRenderAuxViewKeybindings(appconst.InputAuxView_History);
console.log("renderCmdInputKeybindings", renderCmdInputKeybindings);
console.log("renderHistoryKeybindings", renderHistoryKeybindings);
@ -654,7 +655,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
onSelect={this.onSelect}
placeholder="Type here..."
maxLength={MaxInputLength}
className={cn("textarea", { "display-disabled": auxViewFocused })}
className={clsx("textarea", { "display-disabled": auxViewFocused })}
></textarea>
<input
key="history"

View File

@ -3,7 +3,7 @@ import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { If, For } from "tsx-control-statements/components";
import cn from "classnames";
import { clsx } from "clsx";
import { GlobalCommandRunner, GlobalModel, Screen } from "@/models";
import { TextField, Dropdown } from "@/elements";
import { getRemoteStrWithAlias } from "@/common/prompt/prompt";
@ -183,7 +183,7 @@ class TabRemoteSelector extends React.Component<{ screen: Screen; errorMessage?:
startDecoration: (
<div className="lefticon">
<GlobeIcon className="globe-icon" />
<StatusCircleIcon className={cn("status-icon", "status-" + curRemote.status)} />
<StatusCircleIcon className={clsx("status-icon", "status-" + curRemote.status)} />
</div>
),
}}

View File

@ -7,7 +7,7 @@ import * as mobx from "mobx";
import { sprintf } from "sprintf-js";
import { boundMethod } from "autobind-decorator";
import { If } from "tsx-control-statements/components";
import cn from "classnames";
import { clsx } from "clsx";
import { debounce } from "throttle-debounce";
import dayjs from "dayjs";
import { GlobalCommandRunner, ForwardLineContainer, GlobalModel, ScreenLines, Screen, Session } from "@/models";
@ -116,7 +116,7 @@ class ScreenView extends React.Component<{ session: Session; screen: Screen }, {
<div className="screen-view" ref={this.screenViewRef}>
<div className="window-view" style={{ width: "100%" }}>
<div key="lines" className="lines"></div>
<div key="window-empty" className={cn("window-empty")}>
<div key="window-empty" className={clsx("window-empty")}>
<div className="flex-centered-column">
<code className="text-standard">[no workspace]</code>
<If condition={sessionCount == 0}>
@ -136,7 +136,7 @@ class ScreenView extends React.Component<{ session: Session; screen: Screen }, {
<div className="screen-view" ref={this.screenViewRef}>
<div className="window-view" style={{ width: "100%" }}>
<div key="lines" className="lines"></div>
<div key="window-empty" className={cn("window-empty")}>
<div key="window-empty" className={clsx("window-empty")}>
<div className="flex-centered-column">
<code className="text-standard">[no active tab]</code>
<If condition={screens.length == 0}>
@ -479,7 +479,7 @@ class ScreenWindowView extends React.Component<ScreenWindowViewProps, {}> {
return (
<div className="window-view" ref={this.windowViewRef} data-screenid={screen.screenId} style={{ width }}>
<div key="lines" className="lines"></div>
<div key="window-empty" className={cn("window-empty", { "should-fade": fade })}>
<div key="window-empty" className={clsx("window-empty", { "should-fade": fade })}>
<div className="text-standard">{message}</div>
</div>
</div>
@ -559,7 +559,7 @@ class ScreenWindowView extends React.Component<ScreenWindowViewProps, {}> {
<If condition={lines.length == 0 && screen.nextLineNum.get() != 1}>
<div className="window-empty" ref={this.windowViewRef} data-screenid={screen.screenId}>
<div key="lines" className="lines"></div>
<div key="window-empty" className={cn("window-empty")}>
<div key="window-empty" className={clsx("window-empty")}>
<div>
<code className="text-standard">
[workspace="{session.name.get()}" tab="{screen.name.get()}"]

View File

@ -5,7 +5,7 @@ import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import cn from "classnames";
import { clsx } from "clsx";
import { GlobalModel, GlobalCommandRunner, Screen } from "@/models";
import { ActionsIcon, StatusIndicator, CenteredIcon } from "@/common/icons/icons";
import * as constants from "@/app/appconst";
@ -131,7 +131,7 @@ class ScreenTab extends React.Component<
value={screen}
id={"screentab-" + screen.screenId}
data-screenid={screen.screenId}
className={cn(
className={clsx(
"screen-tab",
{ "is-active": activeScreenId == screen.screenId, "is-archived": screen.archived.get() },
"color-" + screen.getTabColor()

View File

@ -7,7 +7,7 @@ import * as mobx from "mobx";
import { sprintf } from "sprintf-js";
import { boundMethod } from "autobind-decorator";
import { For, If } from "tsx-control-statements/components";
import cn from "classnames";
import { clsx } from "clsx";
import { GlobalModel, GlobalCommandRunner, Session, Screen } from "@/models";
import { ReactComponent as AddIcon } from "@/assets/icons/add.svg";
import { Reorder } from "framer-motion";

View File

@ -4,7 +4,7 @@
import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import cn from "classnames";
import { clsx } from "clsx";
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { If } from "tsx-control-statements/components";
@ -234,7 +234,7 @@ class WorkspaceView extends React.Component<{}, {}> {
return (
<div
ref={this.sessionRef}
className={cn("mainview", "session-view", { "is-hidden": isHidden })}
className={clsx("mainview", "session-view", { "is-hidden": isHidden })}
id={sessionId}
data-sessionid={sessionId}
style={{
@ -246,7 +246,7 @@ class WorkspaceView extends React.Component<{}, {}> {
</If>
<ScreenTabs key={"tabs-" + sessionId} session={session} />
<If condition={activeScreen != null}>
<div key="pulldown" className={cn("tab-settings-pulldown", { closed: !showTabSettings })}>
<div key="pulldown" className={clsx("tab-settings-pulldown", { closed: !showTabSettings })}>
<Button className="close-button secondary ghost" onClick={this.toggleTabSettings}>
<i className="fa-solid fa-sharp fa-xmark-large" />
</Button>

View File

@ -14,7 +14,7 @@ import * as waveutil from "../util/util";
import { sprintf } from "sprintf-js";
import { handleJsonFetchResponse, fireAndForget } from "@/util/util";
import { v4 as uuidv4 } from "uuid";
import { checkKeyPressed, adaptFromElectronKeyEvent, setKeyUtilPlatform } from "@/util/keyutil";
import { adaptFromElectronKeyEvent, setKeyUtilPlatform } from "@/util/keyutil";
import { platform } from "os";
const WaveAppPathVarName = "WAVETERM_APP_PATH";
@ -22,7 +22,6 @@ const WaveDevVarName = "WAVETERM_DEV";
const AuthKeyFile = "waveterm.authkey";
const DevServerEndpoint = "http://127.0.0.1:8090";
const ProdServerEndpoint = "http://127.0.0.1:1619";
const startTs = Date.now();
const isDev = process.env[WaveDevVarName] != null;
const waveHome = getWaveHomeDir();
@ -35,7 +34,6 @@ let wasActive = true;
let wasInFg = true;
let currentGlobalShortcut: string | null = null;
let initialClientData: ClientDataType = null;
let MainWindow: Electron.BrowserWindow | null = null;
checkPromptMigrate();
ensureDir(waveHome);
@ -201,14 +199,15 @@ function readAuthKey(): string {
}
const reloadAcceleratorKey = unamePlatform == "darwin" ? "Option+R" : "Super+R";
const cmdOrAlt = process.platform === "darwin" ? "Cmd" : "Alt";
let viewSubMenu: Electron.MenuItemConstructorOptions[] = [];
viewSubMenu.push({ role: "reload", accelerator: reloadAcceleratorKey });
viewSubMenu.push({ role: "toggleDevTools" });
if (isDev) {
viewSubMenu.push({
label: "Toggle Dev UI",
click: () => {
MainWindow?.webContents.send("toggle-devui");
click: (_, window) => {
window?.webContents.send("toggle-devui");
},
});
}
@ -216,36 +215,33 @@ viewSubMenu.push({ type: "separator" });
viewSubMenu.push({
label: "Actual Size",
accelerator: cmdOrAlt + "+0",
click: () => {
if (MainWindow == null) {
return;
}
MainWindow.webContents.setZoomFactor(1);
MainWindow.webContents.send("zoom-changed");
click: (_, window) => {
window?.webContents.setZoomFactor(1);
window?.webContents.send("zoom-changed");
},
});
viewSubMenu.push({
label: "Zoom In",
accelerator: cmdOrAlt + "+Plus",
click: () => {
if (MainWindow == null) {
click: (_, window) => {
if (window == null) {
return;
}
const zoomFactor = MainWindow.webContents.getZoomFactor();
MainWindow.webContents.setZoomFactor(zoomFactor * 1.1);
MainWindow.webContents.send("zoom-changed");
const zoomFactor = window.webContents.getZoomFactor();
window.webContents.setZoomFactor(zoomFactor * 1.1);
window.webContents.send("zoom-changed");
},
});
viewSubMenu.push({
label: "Zoom Out",
accelerator: cmdOrAlt + "+-",
click: () => {
if (MainWindow == null) {
click: (_, window) => {
if (window == null) {
return;
}
const zoomFactor = MainWindow.webContents.getZoomFactor();
MainWindow.webContents.setZoomFactor(zoomFactor / 1.1);
MainWindow.webContents.send("zoom-changed");
const zoomFactor = window.webContents.getZoomFactor();
window.webContents.setZoomFactor(zoomFactor / 1.1);
window.webContents.send("zoom-changed");
},
});
viewSubMenu.push({ type: "separator" });
@ -256,8 +252,8 @@ const menuTemplate: Electron.MenuItemConstructorOptions[] = [
submenu: [
{
label: "About Wave Terminal",
click: () => {
MainWindow?.webContents.send("menu-item-about");
click: (_, window) => {
window?.webContents.send("menu-item-about");
},
},
{ type: "separator" },
@ -326,7 +322,7 @@ function shFrameNavHandler(event: Electron.Event<Electron.WebContentsWillFrameNa
console.log("frame navigation canceled");
}
function createMainWindow(clientData: ClientDataType | null): Electron.BrowserWindow {
function createWindow(clientData: ClientDataType | null): Electron.BrowserWindow {
const bounds = calcBounds(clientData);
setKeyUtilPlatform(platform());
const win = new electron.BrowserWindow({
@ -374,9 +370,6 @@ function createMainWindow(clientData: ClientDataType | null): Electron.BrowserWi
wasInFg = true;
wasActive = true;
});
win.on("close", () => {
MainWindow = null;
});
win.webContents.on("zoom-changed", (e) => {
win.webContents.send("zoom-changed");
});
@ -400,7 +393,6 @@ function createMainWindow(clientData: ClientDataType | null): Electron.BrowserWi
console.log("window-open denied", url);
return { action: "deny" };
});
return win;
}
@ -475,8 +467,9 @@ app.on("window-all-closed", () => {
});
electron.ipcMain.on("toggle-developer-tools", (event) => {
if (MainWindow != null) {
MainWindow.webContents.toggleDevTools();
const window = getWindowForEvent(event);
if (window != null) {
window.webContents.toggleDevTools();
}
event.returnValue = true;
});
@ -488,8 +481,8 @@ function convertMenuDefArrToMenu(menuDefArr: ElectronContextMenuItem[]): electro
role: menuDef.role as any,
label: menuDef.label,
type: menuDef.type,
click: () => {
MainWindow?.webContents.send("contextmenu-click", menuDef.id);
click: (_, window) => {
window?.webContents.send("contextmenu-click", menuDef.id);
},
};
if (menuDef.submenu != null) {
@ -501,6 +494,11 @@ function convertMenuDefArrToMenu(menuDefArr: ElectronContextMenuItem[]): electro
return electron.Menu.buildFromTemplate(menuItems);
}
function getWindowForEvent(event: Electron.IpcMainEvent): Electron.BrowserWindow {
const windowId = event.sender.id;
return electron.BrowserWindow.fromId(windowId);
}
electron.ipcMain.on("contextmenu-show", (event, menuDefArr: ElectronContextMenuItem[], { x, y }) => {
if (menuDefArr == null || menuDefArr.length == 0) {
return;
@ -511,8 +509,9 @@ electron.ipcMain.on("contextmenu-show", (event, menuDefArr: ElectronContextMenuI
});
electron.ipcMain.on("hide-window", (event) => {
if (MainWindow != null) {
MainWindow.hide();
const window = getWindowForEvent(event);
if (window) {
window.hide();
}
event.returnValue = true;
});
@ -553,8 +552,9 @@ electron.ipcMain.on("restart-server", (event) => {
});
electron.ipcMain.on("reload-window", (event) => {
if (MainWindow != null) {
MainWindow.reload();
const window = getWindowForEvent(event);
if (window) {
window.reload();
}
event.returnValue = true;
});
@ -593,9 +593,9 @@ electron.ipcMain.on("set-nativethemesource", (event, themeSource: "system" | "li
});
electron.nativeTheme.on("updated", () => {
if (MainWindow != null) {
MainWindow.webContents.send("nativetheme-updated");
}
electron.BrowserWindow.getAllWindows().forEach((win) => {
win.webContents.send("nativetheme-updated");
});
});
function readLastLinesOfFile(filePath: string, lineCount: number) {
@ -659,13 +659,13 @@ async function getClientData(willRetry: boolean, retryNum: number): Promise<Clie
}
function sendWSSC() {
if (MainWindow != null) {
electron.BrowserWindow.getAllWindows().forEach((win) => {
if (waveSrvProc == null) {
MainWindow.webContents.send("wavesrv-status-change", false);
return;
win.webContents.send("wavesrv-status-change", false);
} else {
win.webContents.send("wavesrv-status-change", true, waveSrvProc.pid);
}
MainWindow.webContents.send("wavesrv-status-change", true, waveSrvProc.pid);
}
});
}
function runWaveSrv() {
@ -733,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);
@ -741,9 +741,9 @@ async function createMainWindowWrap() {
} catch (e) {
console.log("error getting wavesrv clientdata", e.toString());
}
MainWindow = createMainWindow(clientData);
if (clientData && clientData.winsize.fullscreen) {
MainWindow.setFullScreen(true);
const win = createWindow(clientData);
if (clientData?.winsize.fullscreen) {
win.setFullScreen(true);
}
configureAutoUpdaterStartup(clientData);
}
@ -762,7 +762,7 @@ function logActiveState() {
console.log("error logging active state", err);
});
// for next iteration
wasInFg = MainWindow != null && MainWindow.isFocused();
wasInFg = electron.BrowserWindow.getFocusedWindow()?.isFocused() ?? false;
wasActive = false;
}
@ -788,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");
MainWindow?.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) {
@ -802,10 +806,12 @@ function reregisterGlobalShortcut(shortcut: string) {
// ====== AUTO-UPDATER ====== //
let autoUpdateLock = false;
let autoUpdateEnabled = false;
let autoUpdateInterval: NodeJS.Timeout | null = null;
let availableUpdateReleaseName: string | null = null;
let availableUpdateReleaseNotes: string | null = null;
let appUpdateStatus = "unavailable";
let lastUpdateCheck: Date = null;
/**
* Sets the app update status and sends it to the main window
@ -813,8 +819,22 @@ let appUpdateStatus = "unavailable";
*/
function setAppUpdateStatus(status: string) {
appUpdateStatus = status;
if (MainWindow != null) {
MainWindow.webContents.send("app-update-status", appUpdateStatus);
electron.BrowserWindow.getAllWindows().forEach((window) => {
window.webContents.send("app-update-status", appUpdateStatus);
});
}
/**
* Checks if an hour has passed since the last update check, and if so, checks for updates using the `autoUpdater` object
*/
function checkForUpdates() {
if (!autoUpdateEnabled) {
return;
}
const now = new Date();
if (!lastUpdateCheck || Math.abs(now.getTime() - lastUpdateCheck.getTime()) > 3600000) {
fireAndForget(() => autoUpdater.checkForUpdates());
lastUpdateCheck = now;
}
}
@ -861,14 +881,16 @@ function initUpdater(): NodeJS.Timeout {
body: "A new version of Wave Terminal is ready to install.",
});
updateNotification.on("click", () => {
fireAndForget(installAppUpdate);
fireAndForget(() => installAppUpdate());
});
updateNotification.show();
});
// check for updates right away and keep checking later
autoUpdater.checkForUpdates();
return setInterval(() => fireAndForget(autoUpdater.checkForUpdates), 3600000); // 1 hour in ms
checkForUpdates();
return setInterval(() => {
checkForUpdates();
}, 600000); // intervals are unreliable when an app is suspended so we will check every 10 mins if an hour has passed.
}
/**
@ -883,12 +905,17 @@ async function installAppUpdate() {
detail: "A new version has been downloaded. Restart the application to apply the updates.",
};
await electron.dialog.showMessageBox(MainWindow, 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));
electron.ipcMain.on("install-app-update", () => fireAndForget(() => installAppUpdate()));
electron.ipcMain.on("get-app-update-status", (event) => {
event.returnValue = appUpdateStatus;
});
@ -919,15 +946,22 @@ function configureAutoUpdater(enabled: boolean) {
console.log("auto-update configuration already in progress, skipping");
return;
}
autoUpdateEnabled = enabled;
autoUpdateLock = true;
if (enabled && autoUpdateInterval == null) {
if (autoUpdateEnabled && autoUpdateInterval == null) {
lastUpdateCheck = null;
try {
console.log("configuring auto updater");
autoUpdateInterval = initUpdater();
} catch (e) {
console.log("error configuring auto updater", e.toString());
}
} else if (!autoUpdateEnabled && autoUpdateInterval != null) {
console.log("disabling auto updater");
clearInterval(autoUpdateInterval);
autoUpdateInterval = null;
}
autoUpdateLock = false;
}
@ -950,11 +984,13 @@ 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();
});
})();

View File

@ -6,7 +6,7 @@ import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import Editor, { Monaco } from "@monaco-editor/react";
import type * as MonacoTypes from "monaco-editor/esm/vs/editor/editor.api";
import cn from "classnames";
import { clsx } from "clsx";
import { If } from "tsx-control-statements/components";
import { Markdown, Button } from "@/elements";
import { GlobalModel, GlobalCommandRunner } from "@/models";
@ -579,7 +579,7 @@ class SourceCodeRenderer extends React.Component<
<div className="flex-spacer" />
<div className="code-statusbar">
<If condition={message != null}>
<div className={cn("code-message", { error: message.status == "error" })}>
<div className={clsx("code-message", { error: message.status == "error" })}>
{this.state.message.text}
</div>
</If>

View File

@ -7,7 +7,7 @@ import * as mobx from "mobx";
import { debounce } from "throttle-debounce";
import * as util from "@/util/util";
import { GlobalModel } from "@/models";
import cn from "classnames";
import { clsx } from "clsx";
class SimpleBlobRendererModel {
context: RendererContext;
@ -247,7 +247,7 @@ class SimpleBlobRenderer extends React.Component<
return (
<div
ref={this.wrapperDivRef}
className={cn("renderer-loading", { "zero-height": height == 0 })}
className={clsx("renderer-loading", { "zero-height": height == 0 })}
style={{ minHeight: height, fontSize: model.opts.termFontSize }}
>
loading content <i className="fa fa-ellipsis fa-fade" />
@ -260,7 +260,7 @@ class SimpleBlobRenderer extends React.Component<
}
let { festate, cmdstr, exitcode } = this.props.initParams.rawCmd;
return (
<div ref={this.wrapperDivRef} className={cn("sr-wrapper", { "zero-height": model.savedHeight == 0 })}>
<div ref={this.wrapperDivRef} className={clsx("sr-wrapper", { "zero-height": model.savedHeight == 0 })}>
<Comp
cwd={festate.cwd}
cmdstr={cmdstr}

View File

@ -15,7 +15,7 @@ import {
import { useTableNav } from "@table-nav/react";
import SortUpIcon from "./img/sort-up-solid.svg";
import SortDownIcon from "./img/sort-down-solid.svg";
import cn from "classnames";
import { clsx } from "clsx";
import "./csv.less";
@ -190,7 +190,7 @@ const CSVRenderer: FC<Props> = (props: Props) => {
return (
<div
className={cn("csv-renderer", { show: tableLoaded })}
className={clsx("csv-renderer", { show: tableLoaded })}
style={{ height: tableLoaded ? "auto" : savedHeight }}
>
<table className="probe">

View File

@ -10,7 +10,7 @@ import localizedFormat from "dayjs/plugin/localizedFormat";
import { If } from "tsx-control-statements/components";
import { GlobalModel } from "@/models";
import { termHeightFromRows } from "@/util/textmeasure";
import cn from "classnames";
import { clsx } from "clsx";
import * as lineutil from "@/app/line/lineutil";
import "./terminal.less";
@ -207,7 +207,7 @@ class TerminalRenderer extends React.Component<{
<div
ref={this.elemRef}
key="term-wrap"
className={cn(
className={clsx(
"terminal-wrapper",
{ focus: isFocused },
{ "cmd-done": !cmd.isRunning() },

View File

@ -121,8 +121,8 @@ declare global {
sshconfigsrc: string;
archived: boolean;
uname: string;
mshellversion: string;
needsmshellupgrade: boolean;
waveshellversion: string;
needswaveshellupgrade: boolean;
noinitpk: boolean;
authtype: string;
waitingforpassword: boolean;

View File

@ -28,7 +28,7 @@ func readFullRunPacket(packetParser *packet.PacketParser) (*packet.RunPacketType
return runPacket, nil
}
if !ok {
return nil, fmt.Errorf("invalid packet '%s' sent to mshell", pk.GetType())
return nil, fmt.Errorf("invalid packet '%s' sent to waveshell", pk.GetType())
}
}
return nil, fmt.Errorf("no run packet received")
@ -97,7 +97,7 @@ func handleSingle() {
func handleUsage() {
usage := `
mshell is a helper program for wave terminal. it is used to execute commands
waveshell is a helper program for wave terminal. it is used to execute commands
Options:
--help - prints this message
@ -106,7 +106,7 @@ Options:
--single - run a single command (connected to multiplexer)
--single --version - return an init packet with version info
mshell does not open any external ports and does not require any additional permissions.
waveshell does not open any external ports and does not require any additional permissions.
it communicates exclusively through stdin/stdout with an attached process
via a JSON packet format.
`
@ -124,7 +124,7 @@ func main() {
handleUsage()
return
} else if firstArg == "--version" {
fmt.Printf("mshell %s+%s\n", base.MShellVersion, base.BuildTime)
fmt.Printf("waveshell %s+%s\n", base.WaveshellVersion, base.BuildTime)
return
} else if firstArg == "--single" || firstArg == "--single-from-server" {
base.ProcessType = base.ProcessType_WaveShellSingle

View File

@ -20,18 +20,18 @@ import (
)
const HomeVarName = "HOME"
const DefaultMShellHome = "~/.mshell"
const DefaultMShellName = "mshell"
const MShellPathVarName = "MSHELL_PATH"
const MShellHomeVarName = "MSHELL_HOME"
const MShellInstallBinVarName = "MSHELL_INSTALLBIN_PATH"
const DefaultWaveshellHome = "~/.mshell"
const DefaultWaveshellName = "mshell"
const WaveshellPathVarName = "MSHELL_PATH"
const WaveshellHomeVarName = "MSHELL_HOME"
const WaveshellInstallBinVarName = "MSHELL_INSTALLBIN_PATH"
const SSHCommandVarName = "SSH_COMMAND"
const MShellDebugVarName = "MSHELL_DEBUG"
const WaveshellDebugVarName = "MSHELL_DEBUG"
const SessionsDirBaseName = "sessions"
const RcFilesDirBaseName = "rcfiles"
const MShellVersion = "v0.7.0"
const WaveshellVersion = "v0.7.0"
const RemoteIdFile = "remoteid"
const DefaultMShellInstallBinDir = "/opt/mshell/bin"
const DefaultWaveshellInstallBinDir = "/opt/mshell/bin"
const LogFileName = "mshell.log"
const ForceDebugLog = false
@ -90,7 +90,7 @@ func Logf(fmtStr string, args ...interface{}) {
}
func InitDebugLog(prefix string) {
homeDir := GetMShellHomeDir()
homeDir := GetWaveshellHomeDir()
err := os.MkdirAll(homeDir, 0777)
if err != nil {
return
@ -163,7 +163,7 @@ func (ckey CommandKey) Validate(typeStr string) error {
}
func HasDebugFlag(envMap map[string]string, flagName string) bool {
msDebug := envMap[MShellDebugVarName]
msDebug := envMap[WaveshellDebugVarName]
flags := strings.Split(msDebug, ",")
for _, flag := range flags {
if strings.TrimSpace(flag) == flagName {
@ -174,13 +174,13 @@ func HasDebugFlag(envMap map[string]string, flagName string) bool {
}
func GetDebugRcFileName() string {
msHome := GetMShellHomeDir()
return path.Join(msHome, DebugRcFileName)
wsHome := GetWaveshellHomeDir()
return path.Join(wsHome, DebugRcFileName)
}
func GetDebugReturnStateFileName() string {
msHome := GetMShellHomeDir()
return path.Join(msHome, DebugReturnStateFileName)
wsHome := GetWaveshellHomeDir()
return path.Join(wsHome, DebugReturnStateFileName)
}
func GetHomeDir() string {
@ -191,16 +191,16 @@ func GetHomeDir() string {
return homeVar
}
func GetMShellHomeDir() string {
homeVar := os.Getenv(MShellHomeVarName)
func GetWaveshellHomeDir() string {
homeVar := os.Getenv(WaveshellHomeVarName)
if homeVar != "" {
return homeVar
}
return ExpandHomeDir(DefaultMShellHome)
return ExpandHomeDir(DefaultWaveshellHome)
}
func EnsureRcFilesDir() (string, error) {
mhome := GetMShellHomeDir()
mhome := GetWaveshellHomeDir()
dirName := path.Join(mhome, RcFilesDirBaseName)
err := CacheEnsureDir(dirName, RcFilesDirBaseName, 0700, "rcfiles dir")
if err != nil {
@ -209,18 +209,18 @@ func EnsureRcFilesDir() (string, error) {
return dirName, nil
}
func GetMShellPath() (string, error) {
msPath := os.Getenv(MShellPathVarName) // use MSHELL_PATH
if msPath != "" {
return exec.LookPath(msPath)
func GetWaveshellPath() (string, error) {
wsPath := os.Getenv(WaveshellPathVarName) // use MSHELL_PATH -- will require rename
if wsPath != "" {
return exec.LookPath(wsPath)
}
mhome := GetMShellHomeDir()
userMShellPath := path.Join(mhome, DefaultMShellName) // look in ~/.mshell
msPath, err := exec.LookPath(userMShellPath)
mhome := GetWaveshellHomeDir()
userWaveshellPath := path.Join(mhome, DefaultWaveshellName) // look in ~/.mshell -- will require rename
wsPath, err := exec.LookPath(userWaveshellPath)
if err == nil {
return msPath, nil
return wsPath, nil
}
return exec.LookPath(DefaultMShellName) // standard path lookup for 'mshell'
return exec.LookPath(DefaultWaveshellName) // standard path lookup for 'mshell'-- will require rename
}
func ExpandHomeDir(pathStr string) string {
@ -239,9 +239,9 @@ func ValidGoArch(goos string, goarch string) bool {
}
func GoArchOptFile(version string, goos string, goarch string) string {
installBinDir := os.Getenv(MShellInstallBinVarName)
installBinDir := os.Getenv(WaveshellInstallBinVarName)
if installBinDir == "" {
installBinDir = DefaultMShellInstallBinDir
installBinDir = DefaultWaveshellInstallBinDir
}
versionStr := semver.MajorMinor(version)
if versionStr == "" {
@ -252,22 +252,22 @@ func GoArchOptFile(version string, goos string, goarch string) string {
}
func GetRemoteId() (string, error) {
mhome := GetMShellHomeDir()
homeInfo, err := os.Stat(mhome)
wsHome := GetWaveshellHomeDir()
homeInfo, err := os.Stat(wsHome)
if errors.Is(err, fs.ErrNotExist) {
err = os.MkdirAll(mhome, 0777)
err = os.MkdirAll(wsHome, 0777)
if err != nil {
return "", fmt.Errorf("cannot make mshell home directory[%s]: %w", mhome, err)
return "", fmt.Errorf("cannot make waveshell home directory[%s]: %w", wsHome, err)
}
homeInfo, err = os.Stat(mhome)
homeInfo, err = os.Stat(wsHome)
}
if err != nil {
return "", fmt.Errorf("cannot stat mshell home directory[%s]: %w", mhome, err)
return "", fmt.Errorf("cannot stat waveshell home directory[%s]: %w", wsHome, err)
}
if !homeInfo.IsDir() {
return "", fmt.Errorf("mshell home directory[%s] is not a directory", mhome)
return "", fmt.Errorf("waveshell home directory[%s] is not a directory", wsHome)
}
remoteIdFile := path.Join(mhome, RemoteIdFile)
remoteIdFile := path.Join(wsHome, RemoteIdFile)
fd, err := os.Open(remoteIdFile)
if errors.Is(err, fs.ErrNotExist) {
// write the file

View File

@ -687,18 +687,18 @@ func FmtMessagePacket(fmtStr string, args ...interface{}) *MessagePacketType {
}
type InitPacketType struct {
Type string `json:"type"`
RespId string `json:"respid,omitempty"`
Version string `json:"version"`
BuildTime string `json:"buildtime,omitempty"`
MShellHomeDir string `json:"mshellhomedir,omitempty"`
HomeDir string `json:"homedir,omitempty"`
User string `json:"user,omitempty"`
HostName string `json:"hostname,omitempty"`
NotFound bool `json:"notfound,omitempty"`
UName string `json:"uname,omitempty"`
Shell string `json:"shell,omitempty"`
RemoteId string `json:"remoteid,omitempty"`
Type string `json:"type"`
RespId string `json:"respid,omitempty"`
Version string `json:"version"`
BuildTime string `json:"buildtime,omitempty"`
WaveshellHomeDir string `json:"waveshellhomedir,omitempty"`
HomeDir string `json:"homedir,omitempty"`
User string `json:"user,omitempty"`
HostName string `json:"hostname,omitempty"`
NotFound bool `json:"notfound,omitempty"`
UName string `json:"uname,omitempty"`
Shell string `json:"shell,omitempty"`
RemoteId string `json:"remoteid,omitempty"`
}
func (*InitPacketType) GetType() string {
@ -772,12 +772,12 @@ func MakeCmdDonePacket(ck base.CommandKey) *CmdDonePacketType {
}
type CmdStartPacketType struct {
Type string `json:"type"`
RespId string `json:"respid,omitempty"`
Ts int64 `json:"ts"`
CK base.CommandKey `json:"ck"`
Pid int `json:"pid,omitempty"`
MShellPid int `json:"mshellpid,omitempty"`
Type string `json:"type"`
RespId string `json:"respid,omitempty"`
Ts int64 `json:"ts"`
CK base.CommandKey `json:"ck"`
Pid int `json:"pid,omitempty"`
WaveshellPid int `json:"waveshellpid,omitempty"`
}
func (*CmdStartPacketType) GetType() string {

View File

@ -455,7 +455,7 @@ func (m *MServer) writeFile(pk *packet.WriteFilePacketType, wfc *WriteFileContex
}
var writeFd *os.File
if pk.UseTemp {
writeFd, err = os.CreateTemp("", "mshell.writefile.*") // "" means make this file in standard TempDir
writeFd, err = os.CreateTemp("", "waveshell.writefile.*") // "" means make this file in standard TempDir
if err != nil {
resp := packet.MakeWriteFileReadyPacket(pk.ReqId)
resp.Error = fmt.Sprintf("cannot create temp file: %v", err)
@ -754,14 +754,14 @@ func (m *MServer) runCommand(runPacket *packet.RunPacketType) {
m.Sender.SendErrorResponse(runPacket.ReqId, fmt.Errorf("test error"))
return
}
ecmd, err := shexec.MakeMShellSingleCmd()
ecmd, err := shexec.MakeWaveshellSingleCmd()
if err != nil {
m.Sender.SendErrorResponse(runPacket.ReqId, fmt.Errorf("server run packets require valid ck: %s", err))
return
}
cproc, err := shexec.MakeClientProc(context.Background(), shexec.CmdWrap{Cmd: ecmd})
if err != nil {
m.Sender.SendErrorResponse(runPacket.ReqId, fmt.Errorf("starting mshell client: %s", err))
m.Sender.SendErrorResponse(runPacket.ReqId, fmt.Errorf("starting waveshell client: %s", err))
return
}
m.Lock.Lock()
@ -833,7 +833,7 @@ func (server *MServer) runReadLoop() {
}
continue
}
server.Sender.SendMessageFmt("invalid packet '%s' sent to mshell server", packet.AsString(pk))
server.Sender.SendMessageFmt("invalid packet '%s' sent to waveshell server", packet.AsString(pk))
continue
}
}

View File

@ -162,7 +162,7 @@ const FirstExtraFilesFdNum = 3
func StreamCommandWithExtraFd(ctx context.Context, ecmd *exec.Cmd, outputCh chan []byte, extraFdNum int, endBytes []byte, stdinDataCh chan []byte) ([]byte, error) {
defer close(outputCh)
ecmd.Env = os.Environ()
shellutil.UpdateCmdEnv(ecmd, shellutil.MShellEnvVars(shellutil.DefaultTermType))
shellutil.UpdateCmdEnv(ecmd, shellutil.WaveshellEnvVars(shellutil.DefaultTermType))
cmdPty, cmdTty, err := pty.Open()
if err != nil {
return nil, fmt.Errorf("opening new pty: %w", err)
@ -232,7 +232,7 @@ func StreamCommandWithExtraFd(ctx context.Context, ecmd *exec.Cmd, outputCh chan
func RunSimpleCmdInPty(ecmd *exec.Cmd, endBytes []byte) ([]byte, error) {
ecmd.Env = os.Environ()
shellutil.UpdateCmdEnv(ecmd, shellutil.MShellEnvVars(shellutil.DefaultTermType))
shellutil.UpdateCmdEnv(ecmd, shellutil.WaveshellEnvVars(shellutil.DefaultTermType))
cmdPty, cmdTty, err := pty.Open()
if err != nil {
return nil, fmt.Errorf("opening new pty: %w", err)
@ -311,8 +311,8 @@ func parseExtVarOutput(pvarBytes []byte, promptOutput string, zmodsOutput string
// for debugging (not for production use)
func writeStateToFile(shellType string, outputBytes []byte) error {
msHome := base.GetMShellHomeDir()
stateFileName := path.Join(msHome, shellType+"-state.txt")
wsHome := base.GetWaveshellHomeDir()
stateFileName := path.Join(wsHome, shellType+"-state.txt")
os.WriteFile(stateFileName, outputBytes, 0644)
return nil
}

View File

@ -531,8 +531,6 @@ for var in "${(@k)dis_functions_source}"; do
done
printf "[%SECTIONSEP%]";
[%GITBRANCH%]
[%K8SCONTEXT%]
[%K8SNAMESPACE%]
printf "[%SECTIONSEP%]";
print -P "$PS1"
printf "[%SECTIONSEP%]";

View File

@ -15,13 +15,13 @@ const DefaultTermType = "xterm-256color"
const DefaultTermRows = 24
const DefaultTermCols = 80
func MShellEnvVars(termType string) map[string]string {
func WaveshellEnvVars(termType string) map[string]string {
rtn := make(map[string]string)
if termType != "" {
rtn["TERM"] = termType
}
rtn["WAVESHELL"], _ = os.Executable()
rtn["WAVESHELL_VERSION"] = base.MShellVersion
rtn["WAVESHELL_VERSION"] = base.WaveshellVersion
return rtn
}

View File

@ -170,8 +170,8 @@ type WaveshellLaunchError struct {
func (wle WaveshellLaunchError) Error() string {
if wle.InitPk.NotFound {
return "waveshell client not found"
} else if semver.MajorMinor(wle.InitPk.Version) != semver.MajorMinor(base.MShellVersion) {
return fmt.Sprintf("invalid remote waveshell version '%s', must be '=%s'", wle.InitPk.Version, semver.MajorMinor(base.MShellVersion))
} else if semver.MajorMinor(wle.InitPk.Version) != semver.MajorMinor(base.WaveshellVersion) {
return fmt.Sprintf("invalid remote waveshell version '%s', must be '=%s'", wle.InitPk.Version, semver.MajorMinor(base.WaveshellVersion))
}
return fmt.Sprintf("invalid waveshell: init packet=%v", *wle.InitPk)
}
@ -232,7 +232,7 @@ func MakeClientProc(ctx context.Context, ecmd ConnInterface) (*ClientProc, error
cproc.Close()
return nil, WaveshellLaunchError{InitPk: initPk}
}
if semver.MajorMinor(initPk.Version) != semver.MajorMinor(base.MShellVersion) {
if semver.MajorMinor(initPk.Version) != semver.MajorMinor(base.WaveshellVersion) {
cproc.Close()
return nil, WaveshellLaunchError{InitPk: initPk}
}

View File

@ -72,7 +72,7 @@ fi
`
func MakeClientCommandStr() string {
return strings.ReplaceAll(ClientCommandFmt, "[%VERSION%]", semver.MajorMinor(base.MShellVersion))
return strings.ReplaceAll(ClientCommandFmt, "[%VERSION%]", semver.MajorMinor(base.WaveshellVersion))
}
const InstallCommandFmt = `
@ -88,10 +88,10 @@ fi
`
func MakeInstallCommandStr() string {
return strings.ReplaceAll(InstallCommandFmt, "[%VERSION%]", semver.MajorMinor(base.MShellVersion))
return strings.ReplaceAll(InstallCommandFmt, "[%VERSION%]", semver.MajorMinor(base.WaveshellVersion))
}
type MShellBinaryReaderFn func(version string, goos string, goarch string) (io.ReadCloser, error)
type WaveshellBinaryReaderFn func(version string, goos string, goarch string) (io.ReadCloser, error)
type ReturnStateBuf struct {
Lock *sync.Mutex
@ -277,7 +277,7 @@ func (c *ShExecType) MakeCmdStartPacket(reqId string) *packet.CmdStartPacketType
startPacket.Ts = time.Now().UnixMilli()
startPacket.CK = c.CK
startPacket.Pid = c.Cmd.Process.Pid
startPacket.MShellPid = os.Getpid()
startPacket.WaveshellPid = os.Getpid()
return startPacket
}
@ -295,7 +295,7 @@ func MakeSimpleStaticWriterPipe(data []byte) (*os.File, error) {
}
func MakeRunnerExec(ck base.CommandKey) (*exec.Cmd, error) {
msPath, err := base.GetMShellPath()
msPath, err := base.GetWaveshellPath()
if err != nil {
return nil, err
}
@ -317,7 +317,7 @@ func MakeDetachedExecCmd(pk *packet.RunPacketType, cmdTty *os.File) (*exec.Cmd,
ecmd.Env = os.Environ()
}
shellutil.UpdateCmdEnv(ecmd, shellenv.EnvMapFromState(state))
shellutil.UpdateCmdEnv(ecmd, shellutil.MShellEnvVars(getTermType(pk)))
shellutil.UpdateCmdEnv(ecmd, shellutil.WaveshellEnvVars(getTermType(pk)))
if state.Cwd != "" {
ecmd.Dir = base.ExpandHomeDir(state.Cwd)
}
@ -470,10 +470,10 @@ type ClientOpts struct {
UsePty bool
}
func MakeMShellSingleCmd() (*exec.Cmd, error) {
func MakeWaveshellSingleCmd() (*exec.Cmd, error) {
execFile, err := os.Executable()
if err != nil {
return nil, fmt.Errorf("cannot find local mshell executable: %w", err)
return nil, fmt.Errorf("cannot find local waveshell executable: %w", err)
}
ecmd := exec.Command(execFile, "--single-from-server")
return ecmd, nil
@ -528,31 +528,6 @@ func (opts SSHOpts) MakeSSHExecCmd(remoteCommand string, sapi shellapi.ShellApi)
}
}
func (opts SSHOpts) MakeMShellSSHOpts() string {
var moreSSHOpts []string
if opts.SSHIdentity != "" {
identityOpt := fmt.Sprintf("-i %s", shellescape.Quote(opts.SSHIdentity))
moreSSHOpts = append(moreSSHOpts, identityOpt)
}
if opts.SSHUser != "" {
userOpt := fmt.Sprintf("-l %s", shellescape.Quote(opts.SSHUser))
moreSSHOpts = append(moreSSHOpts, userOpt)
}
if opts.SSHPort != 0 {
portOpt := fmt.Sprintf("-p %d", opts.SSHPort)
moreSSHOpts = append(moreSSHOpts, portOpt)
}
if opts.SSHOptsStr != "" {
optsOpt := fmt.Sprintf("--ssh-opts %s", shellescape.Quote(opts.SSHOptsStr))
moreSSHOpts = append(moreSSHOpts, optsOpt)
}
if opts.SSHHost != "" {
sshArg := fmt.Sprintf("--ssh %s", shellescape.Quote(opts.SSHHost))
moreSSHOpts = append(moreSSHOpts, sshArg)
}
return strings.Join(moreSSHOpts, " ")
}
func GetTerminalSize() (int, int, error) {
fd, err := os.Open("/dev/tty")
if err != nil {
@ -610,19 +585,19 @@ func ValidateRemoteFds(rfds []packet.RemoteFd) error {
dupMap := make(map[int]bool)
for _, rfd := range rfds {
if rfd.FdNum < 0 {
return fmt.Errorf("mshell negative fd numbers fd=%d", rfd.FdNum)
return fmt.Errorf("waveshell negative fd numbers fd=%d", rfd.FdNum)
}
if rfd.FdNum < FirstExtraFilesFdNum {
return fmt.Errorf("mshell does not support re-opening fd=%d (0, 1, and 2, are always open)", rfd.FdNum)
return fmt.Errorf("waveshell does not support re-opening fd=%d (0, 1, and 2, are always open)", rfd.FdNum)
}
if rfd.FdNum > MaxFdNum {
return fmt.Errorf("mshell does not support opening fd numbers above %d", MaxFdNum)
return fmt.Errorf("waveshell does not support opening fd numbers above %d", MaxFdNum)
}
if dupMap[rfd.FdNum] {
return fmt.Errorf("mshell got duplicate entries for fd=%d", rfd.FdNum)
return fmt.Errorf("waveshell got duplicate entries for fd=%d", rfd.FdNum)
}
if rfd.Read && rfd.Write {
return fmt.Errorf("mshell does not support opening fd numbers for reading and writing, fd=%d", rfd.FdNum)
return fmt.Errorf("waveshell does not support opening fd numbers for reading and writing, fd=%d", rfd.FdNum)
}
if !rfd.Read && !rfd.Write {
return fmt.Errorf("invalid fd=%d, neither reading or writing mode specified", rfd.FdNum)
@ -632,14 +607,14 @@ func ValidateRemoteFds(rfds []packet.RemoteFd) error {
return nil
}
func sendMShellBinary(input io.WriteCloser, mshellStream io.Reader) {
func sendWaveshellBinary(input io.WriteCloser, waveshellStream io.Reader) {
go func() {
defer input.Close()
io.Copy(input, mshellStream)
io.Copy(input, waveshellStream)
}()
}
func RunInstallFromCmd(ctx context.Context, ecmd ConnInterface, tryDetect bool, mshellStream io.Reader, mshellReaderFn MShellBinaryReaderFn, msgFn func(string)) error {
func RunInstallFromCmd(ctx context.Context, ecmd ConnInterface, tryDetect bool, waveshellStream io.Reader, waveshellReaderFn WaveshellBinaryReaderFn, msgFn func(string)) error {
inputWriter, err := ecmd.StdinPipe()
if err != nil {
return fmt.Errorf("creating stdin pipe: %v", err)
@ -655,8 +630,8 @@ func RunInstallFromCmd(ctx context.Context, ecmd ConnInterface, tryDetect bool,
go func() {
io.Copy(os.Stderr, stderrReader)
}()
if mshellStream != nil {
sendMShellBinary(inputWriter, mshellStream)
if waveshellStream != nil {
sendWaveshellBinary(inputWriter, waveshellStream)
}
packetParser := packet.MakePacketParser(stdoutReader, nil)
err = ecmd.Start()
@ -686,24 +661,24 @@ func RunInstallFromCmd(ctx context.Context, ecmd ConnInterface, tryDetect bool,
}
goos, goarch, err := DetectGoArch(initPacket.UName)
if err != nil {
return fmt.Errorf("arch cannot be detected (might be incompatible with mshell): %w", err)
return fmt.Errorf("arch cannot be detected (might be incompatible with waveshell): %w", err)
}
msgStr := fmt.Sprintf("mshell detected remote architecture as '%s.%s'\n", goos, goarch)
msgStr := fmt.Sprintf("waveshell detected remote architecture as '%s.%s'\n", goos, goarch)
msgFn(msgStr)
detectedMSS, err := mshellReaderFn(base.MShellVersion, goos, goarch)
detectedMSS, err := waveshellReaderFn(base.WaveshellVersion, goos, goarch)
if err != nil {
return err
}
defer detectedMSS.Close()
sendMShellBinary(inputWriter, detectedMSS)
sendWaveshellBinary(inputWriter, detectedMSS)
continue
}
if pk.GetType() == packet.InitPacketStr && !firstInit {
initPacket := pk.(*packet.InitPacketType)
if initPacket.Version == base.MShellVersion {
if initPacket.Version == base.WaveshellVersion {
return nil
}
return fmt.Errorf("invalid version '%s' received from client, expecting '%s'", initPacket.Version, base.MShellVersion)
return fmt.Errorf("invalid version '%s' received from client, expecting '%s'", initPacket.Version, base.WaveshellVersion)
}
if pk.GetType() == packet.RawPacketStr {
rawPk := pk.(*packet.RawPacketType)
@ -770,7 +745,7 @@ func DetectGoArch(uname string) (string, string, error) {
osVal := strings.TrimSpace(strings.ToLower(fields[0]))
archVal := strings.TrimSpace(strings.ToLower(fields[1]))
if osVal != "darwin" && osVal != "linux" {
return "", "", fmt.Errorf("invalid uname OS '%s', mshell only supports OS X (darwin) and linux", osVal)
return "", "", fmt.Errorf("invalid uname OS '%s', waveshell only supports OS X (darwin) and linux", osVal)
}
goos := osVal
goarch := ""
@ -780,7 +755,7 @@ func DetectGoArch(uname string) (string, string, error) {
goarch = "arm64"
}
if goarch == "" {
return "", "", fmt.Errorf("invalid uname machine type '%s', mshell only supports aarch64 (amd64) and x86_64 (amd64)", archVal)
return "", "", fmt.Errorf("invalid uname machine type '%s', waveshell only supports aarch64 (amd64) and x86_64 (amd64)", archVal)
}
if !base.ValidGoArch(goos, goarch) {
return "", "", fmt.Errorf("invalid arch detected %s.%s", goos, goarch)
@ -975,7 +950,7 @@ func RunCommandSimple(pk *packet.RunPacketType, sender *packet.PacketSender, fro
cmdTty.Close()
}()
cmd.CmdPty = cmdPty
shellutil.UpdateCmdEnv(cmd.Cmd, shellutil.MShellEnvVars(getTermType(pk)))
shellutil.UpdateCmdEnv(cmd.Cmd, shellutil.WaveshellEnvVars(getTermType(pk)))
}
if cmdTty != nil {
cmd.Cmd.Stdin = cmdTty
@ -1151,8 +1126,8 @@ func (rs *ReturnStateBuf) Run() {
}
}
// in detached run mode, we don't want mshell to die from signals
// since we want mshell to persist even if the mshell --server is terminated
// in detached run mode, we don't want waveshell to die from signals since
// we want waveshell to persist even if the waveshell --server is terminated
func SetupSignalsForDetach() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGPIPE)
@ -1163,8 +1138,8 @@ func SetupSignalsForDetach() {
}()
}
// in detached run mode, we don't want mshell to die from signals
// since we want mshell to persist even if the mshell --server is terminated
// in detached run mode, we don't want waveshell to die from signals since
// we want waveshell to persist even if the waveshell --server is terminated
func IgnoreSigPipe() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGPIPE)
@ -1241,10 +1216,10 @@ func (c *ShExecType) WaitForCommand() *packet.CmdDonePacketType {
func MakeInitPacket() *packet.InitPacketType {
initPacket := packet.MakeInitPacket()
initPacket.Version = base.MShellVersion
initPacket.Version = base.WaveshellVersion
initPacket.BuildTime = base.BuildTime
initPacket.HomeDir = base.GetHomeDir()
initPacket.MShellHomeDir = base.GetMShellHomeDir()
initPacket.WaveshellHomeDir = base.GetWaveshellHomeDir()
if user, _ := user.Current(); user != nil {
initPacket.User = user.Username
}

View File

@ -452,12 +452,12 @@ func HandleWriteFile(w http.ResponseWriter, r *http.Request) {
WriteJsonError(w, fmt.Errorf("invalid line, no remote"))
return
}
msh := remote.GetRemoteById(cmd.Remote.RemoteId)
if msh == nil {
wsh := remote.GetRemoteById(cmd.Remote.RemoteId)
if wsh == nil {
WriteJsonError(w, fmt.Errorf("invalid line, cannot resolve remote"))
return
}
rrState := msh.GetRemoteRuntimeState()
rrState := wsh.GetRemoteRuntimeState()
fullPath, err := rrState.ExpandHomeDir(params.Path)
if err != nil {
WriteJsonError(w, fmt.Errorf("error expanding homedir: %v", err))
@ -472,7 +472,7 @@ func HandleWriteFile(w http.ResponseWriter, r *http.Request) {
} else {
writePk.Path = filepath.Join(cwd, fullPath)
}
iter, err := msh.PacketRpcIter(r.Context(), writePk)
iter, err := wsh.PacketRpcIter(r.Context(), writePk)
if err != nil {
WriteJsonError(w, fmt.Errorf("error: %v", err))
return
@ -502,7 +502,7 @@ func HandleWriteFile(w http.ResponseWriter, r *http.Request) {
} else if err != nil {
dataErr := fmt.Errorf("error reading file data: %v", err)
dataPk.Error = dataErr.Error()
msh.SendFileData(dataPk)
wsh.SendFileData(dataPk)
WriteJsonError(w, dataErr)
return
}
@ -510,7 +510,7 @@ func HandleWriteFile(w http.ResponseWriter, r *http.Request) {
dataPk.Data = make([]byte, nr)
copy(dataPk.Data, bufSlice[0:nr])
}
msh.SendFileData(dataPk)
wsh.SendFileData(dataPk)
if dataPk.Eof {
break
}
@ -581,13 +581,13 @@ func HandleReadFile(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("invalid line, no remote"))
return
}
msh := remote.GetRemoteById(cmd.Remote.RemoteId)
if msh == nil {
wsh := remote.GetRemoteById(cmd.Remote.RemoteId)
if wsh == nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("invalid line, cannot resolve remote"))
return
}
rrState := msh.GetRemoteRuntimeState()
rrState := wsh.GetRemoteRuntimeState()
fullPath, err := rrState.ExpandHomeDir(path)
if err != nil {
WriteJsonError(w, fmt.Errorf("error expanding homedir: %v", err))
@ -601,7 +601,7 @@ func HandleReadFile(w http.ResponseWriter, r *http.Request) {
} else {
streamPk.Path = filepath.Join(cwd, fullPath)
}
iter, err := msh.StreamFile(r.Context(), streamPk)
iter, err := wsh.StreamFile(r.Context(), streamPk)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(fmt.Sprintf("error trying to stream file: %v", err)))
@ -733,7 +733,6 @@ func HandleRunEphemeralCommand(w http.ResponseWriter, r *http.Request) {
WriteJsonError(w, fmt.Errorf(ErrorDecodingJson, err))
return
}
log.Printf("Running ephemeral command: %v\n", commandPk)
if commandPk.EphemeralOpts == nil {
commandPk.EphemeralOpts = &ephemeral.EphemeralRunOpts{}
@ -1129,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)

View File

@ -0,0 +1 @@
-- nothing

View File

@ -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)
);

View File

@ -10,3 +10,6 @@ import "embed"
//go:embed migrations/*.sql
var MigrationFS embed.FS
//go:embed blockstore-migrations/*.sql
var BlockstoreMigrationFS embed.FS

View File

@ -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

View File

@ -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=

View File

@ -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)
}
}
}

View File

@ -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)
}

View File

@ -6,15 +6,17 @@ import (
"crypto/md5"
"crypto/rand"
"log"
"os"
"sync"
"testing"
"time"
"github.com/alecthomas/units"
"github.com/wavetermdev/waveterm/wavesrv/pkg/dbutil"
)
const testOverrideDBName = "test-blockstore.db"
const bigFileSize = 10 * UnitsMB
type TestBlockType struct {
BlockId string
Name string
@ -22,6 +24,22 @@ type TestBlockType struct {
Data []byte
}
func initTestDb(t *testing.T) {
log.Printf("initTestDb: %v", t.Name())
os.Remove(testOverrideDBName)
overrideDBName = testOverrideDBName
err := MigrateBlockstore()
if err != nil {
t.Fatalf("MigrateBlockstore error: %v", err)
}
}
func cleanupTestDB(t *testing.T) {
clearCache()
CloseDB()
os.Remove(testOverrideDBName)
}
func (b *TestBlockType) ToMap() map[string]interface{} {
rtn := make(map[string]interface{})
return rtn
@ -35,22 +53,17 @@ func (b *TestBlockType) FromMap(m map[string]interface{}) bool {
return true
}
func Cleanup(t *testing.T, ctx context.Context) {
DeleteBlock(ctx, "test-block-id")
}
func CleanupName(t *testing.T, ctx context.Context, blockId string) {
DeleteBlock(ctx, blockId)
}
func TestGetDB(t *testing.T) {
initTestDb(t)
defer cleanupTestDB(t)
GetDBTimeout := 10 * time.Second
ctx, _ := context.WithTimeout(context.Background(), GetDBTimeout)
ctx, cancelFn := context.WithTimeout(context.Background(), GetDBTimeout)
defer cancelFn()
_, err := GetDB(ctx)
if err != nil {
t.Errorf("TestInitDB error: %v", err)
}
CloseDB()
}
func SimpleAssert(t *testing.T, condition bool, description string) {
@ -82,9 +95,11 @@ func InsertIntoBlockData(t *testing.T, ctx context.Context, blockId string, name
}
func TestTx(t *testing.T) {
initTestDb(t)
defer cleanupTestDB(t)
ctx := context.Background()
SetFlushTimeout(2 * time.Minute)
InitDBState()
txErr := WithTx(ctx, func(tx *TxWrap) error {
query := `INSERT into block_data values ('test-block-id', 'test-file-name', 0, 256)`
tx.Exec(query)
@ -127,11 +142,13 @@ func TestTx(t *testing.T) {
if txErr != nil {
t.Errorf("TestTx error deleting test entries: %v", txErr)
}
CloseDB()
}
func TestMultipleChunks(t *testing.T) {
initTestDb(t)
defer cleanupTestDB(t)
ctx := context.Background()
InitDBState()
InsertIntoBlockData(t, ctx, "test-block-id", "file-1", 0, make([]byte, 5))
InsertIntoBlockData(t, ctx, "test-block-id", "file-1", 1, make([]byte, 5))
InsertIntoBlockData(t, ctx, "test-block-id", "file-1", 2, make([]byte, 5))
@ -178,7 +195,9 @@ func TestMultipleChunks(t *testing.T) {
}
func TestMakeFile(t *testing.T) {
InitDBState()
initTestDb(t)
defer cleanupTestDB(t)
ctx := context.Background()
fileMeta := make(FileMeta)
fileMeta["test-descriptor"] = true
@ -205,7 +224,7 @@ func TestMakeFile(t *testing.T) {
log.Printf("cur file info: %v", curFileInfo)
SimpleAssert(t, curFileInfo.Name == "file-1", "correct file name")
SimpleAssert(t, curFileInfo.Meta["test-descriptor"] == true, "meta correct")
curCacheEntry := cache[GetCacheId("test-block-id", "file-1")]
curCacheEntry := blockstoreCache[GetCacheId("test-block-id", "file-1")]
curFileInfo = curCacheEntry.Info
log.Printf("cache entry: %v", curCacheEntry)
SimpleAssert(t, curFileInfo.Name == "file-1", "cache correct file name")
@ -218,15 +237,16 @@ func TestMakeFile(t *testing.T) {
if txErr != nil {
t.Errorf("TestTx error deleting test entries: %v", txErr)
}
Cleanup(t, ctx)
}
func TestWriteAt(t *testing.T) {
InitDBState()
initTestDb(t)
defer cleanupTestDB(t)
ctx := context.Background()
fileMeta := make(FileMeta)
fileMeta["test-descriptor"] = true
fileOpts := FileOptsType{MaxSize: int64(5 * units.Gigabyte), Circular: false, IJson: false}
fileOpts := FileOptsType{MaxSize: bigFileSize, Circular: false, IJson: false}
err := MakeFile(ctx, "test-block-id", "file-1", fileMeta, fileOpts)
if err != nil {
t.Fatalf("MakeFile error: %v", err)
@ -244,7 +264,10 @@ func TestWriteAt(t *testing.T) {
} else {
log.Printf("Write at no errors: %v", bytesWritten)
}
SimpleAssert(t, bytesWritten == len(testBytesToWrite), "Correct num bytes written")
if bytesWritten != len(testBytesToWrite) {
t.Errorf("WriteAt error: towrite:%d written:%d err:%v\n", len(testBytesToWrite), bytesWritten, err)
return
}
cacheData, err = GetCacheBlock(ctx, "test-block-id", "file-1", 0, false)
if err != nil {
t.Errorf("Error getting cache: %v", err)
@ -313,15 +336,16 @@ func TestWriteAt(t *testing.T) {
}
log.Printf("Got stat: %v", fInfo)
SimpleAssert(t, int64(len(cacheData.data)) == fInfo.Size, "Correct fInfo size")
Cleanup(t, ctx)
}
func TestWriteAtLeftPad(t *testing.T) {
InitDBState()
initTestDb(t)
defer cleanupTestDB(t)
ctx := context.Background()
fileMeta := make(FileMeta)
fileMeta["test-descriptor"] = true
fileOpts := FileOptsType{MaxSize: int64(5 * units.Gigabyte), Circular: false, IJson: false}
fileOpts := FileOptsType{MaxSize: bigFileSize, Circular: false, IJson: false}
err := MakeFile(ctx, "test-block-id", "file-1", fileMeta, fileOpts)
if err != nil {
t.Fatalf("MakeFile error: %v", err)
@ -349,14 +373,16 @@ func TestWriteAtLeftPad(t *testing.T) {
}
log.Printf("Got stat: %v %v %v", fInfo, fInfo.Size, len(cacheData.data))
SimpleAssert(t, int64(len(cacheData.data)) == fInfo.Size, "Correct fInfo size")
Cleanup(t, ctx)
}
func TestReadAt(t *testing.T) {
initTestDb(t)
defer cleanupTestDB(t)
ctx := context.Background()
fileMeta := make(FileMeta)
fileMeta["test-descriptor"] = true
fileOpts := FileOptsType{MaxSize: int64(5 * units.Gigabyte), Circular: false, IJson: false}
fileOpts := FileOptsType{MaxSize: bigFileSize, Circular: false, IJson: false}
err := MakeFile(ctx, "test-block-id", "file-1", fileMeta, fileOpts)
if err != nil {
t.Fatalf("MakeFile error: %v", err)
@ -399,14 +425,16 @@ func TestReadAt(t *testing.T) {
}
SimpleAssert(t, bytesRead == (11-4), "Correct num bytes read")
log.Printf("bytes read: %v string: %s", read, string(read))
Cleanup(t, ctx)
}
func TestFlushCache(t *testing.T) {
initTestDb(t)
defer cleanupTestDB(t)
ctx := context.Background()
fileMeta := make(FileMeta)
fileMeta["test-descriptor"] = true
fileOpts := FileOptsType{MaxSize: int64(5 * units.Gigabyte), Circular: false, IJson: false}
fileOpts := FileOptsType{MaxSize: bigFileSize, Circular: false, IJson: false}
err := MakeFile(ctx, "test-block-id", "file-1", fileMeta, fileOpts)
if err != nil {
t.Fatalf("MakeFile error: %v", err)
@ -461,17 +489,16 @@ func TestFlushCache(t *testing.T) {
t.Errorf("get data from db error: %v", txErr)
}
log.Printf("DB Data: %v", dbData)
Cleanup(t, ctx)
}
var largeDataFlushFullWriteSize int64 = int64(1024 * units.Megabyte)
var largeDataFlushFullWriteSize int64 = 64 * UnitsKB
func WriteLargeDataFlush(t *testing.T, ctx context.Context) {
writeSize := int64(64 - 16)
fullWriteSize := largeDataFlushFullWriteSize
fileMeta := make(FileMeta)
fileMeta["test-descriptor"] = true
fileOpts := FileOptsType{MaxSize: int64(5 * units.Gigabyte), Circular: false, IJson: false}
fileOpts := FileOptsType{MaxSize: bigFileSize, Circular: false, IJson: false}
err := MakeFile(ctx, "test-block-id", "file-1", fileMeta, fileOpts)
if err != nil {
t.Fatalf("MakeFile error: %v", err)
@ -524,6 +551,9 @@ func WriteLargeDataFlush(t *testing.T, ctx context.Context) {
SimpleAssert(t, bytes.Equal(readHashBuf, hashBuf), "hashes are equal")
}
func TestWriteAtMaxSize(t *testing.T) {
initTestDb(t)
defer cleanupTestDB(t)
ctx := context.Background()
fileMeta := make(FileMeta)
fileMeta["test-descriptor"] = true
@ -544,11 +574,12 @@ func TestWriteAtMaxSize(t *testing.T) {
log.Printf("readbuf: %v\n", readBuf)
SimpleAssert(t, bytesRead == 4, "Correct num bytes read")
SimpleAssert(t, bytes.Equal(readBuf[:4], readTest), "Correct bytes read")
Cleanup(t, ctx)
}
func TestWriteAtMaxSizeMultipleBlocks(t *testing.T) {
InitDBState()
initTestDb(t)
defer cleanupTestDB(t)
ctx := context.Background()
fileMeta := make(FileMeta)
fileMeta["test-descriptor"] = true
@ -569,11 +600,12 @@ func TestWriteAtMaxSizeMultipleBlocks(t *testing.T) {
log.Printf("readbuf multiple: %v %v %v\n", readBuf, bytesRead, bytesWritten)
SimpleAssert(t, bytesRead == 4, "Correct num bytes read")
SimpleAssert(t, bytes.Equal(readBuf[:4], readTest), "Correct bytes read")
Cleanup(t, ctx)
}
func TestWriteAtCircular(t *testing.T) {
InitDBState()
initTestDb(t)
defer cleanupTestDB(t)
ctx := context.Background()
fileMeta := make(FileMeta)
fileMeta["test-descriptor"] = true
@ -603,11 +635,12 @@ func TestWriteAtCircular(t *testing.T) {
SimpleAssert(t, bytesRead == 7, "Correct num bytes read")
SimpleAssert(t, bytes.Equal(readBuf[:7], readTest), "Correct bytes read")
log.Printf("readbuf circular %v %v, %v", readBuf, string(readBuf), bytesRead)
Cleanup(t, ctx)
}
func TestWriteAtCircularWierdOffset(t *testing.T) {
InitDBState()
initTestDb(t)
defer cleanupTestDB(t)
ctx := context.Background()
fileMeta := make(FileMeta)
fileMeta["test-descriptor"] = true
@ -646,11 +679,12 @@ func TestWriteAtCircularWierdOffset(t *testing.T) {
SimpleAssert(t, bytesRead == 7, "Correct num bytes read")
SimpleAssert(t, bytes.Equal(readBuf[:7], readTest), "Correct bytes read")
log.Printf("readbuf circular %v %v, %v", readBuf, string(readBuf), bytesRead)
Cleanup(t, ctx)
}
func TestAppend(t *testing.T) {
InitDBState()
initTestDb(t)
defer cleanupTestDB(t)
ctx := context.Background()
fileMeta := make(FileMeta)
fileMeta["test-descriptor"] = true
@ -691,7 +725,6 @@ func TestAppend(t *testing.T) {
}
SimpleAssert(t, bytesRead == bytesWritten+4, "Correct num bytes read")
SimpleAssert(t, bytes.Equal(readBuf, readTestBytes), "Correct bytes read")
Cleanup(t, ctx)
}
func AppendSyncWorker(t *testing.T, ctx context.Context, wg *sync.WaitGroup) {
@ -705,13 +738,15 @@ func AppendSyncWorker(t *testing.T, ctx context.Context, wg *sync.WaitGroup) {
SimpleAssert(t, bytesWritten == 1, "Correct bytes written")
}
func TestAppendSync(t *testing.T) {
InitDBState()
initTestDb(t)
defer cleanupTestDB(t)
var wg sync.WaitGroup
numWorkers := 10
ctx := context.Background()
fileMeta := make(FileMeta)
fileMeta["test-descriptor"] = true
fileOpts := FileOptsType{MaxSize: int64(5 * units.Gigabyte), Circular: false, IJson: false}
fileOpts := FileOptsType{MaxSize: bigFileSize, Circular: false, IJson: false}
err := MakeFile(ctx, "test-block-id-sync", "file-1", fileMeta, fileOpts)
if err != nil {
t.Fatalf("MakeFile error: %v", err)
@ -729,15 +764,6 @@ func TestAppendSync(t *testing.T) {
}
log.Printf("read buf : %v", readBuf)
SimpleAssert(t, bytesRead == numWorkers, "Correct bytes read")
CleanupName(t, ctx, "test-block-id-sync")
}
func TestAppendSyncMultiple(t *testing.T) {
numTests := 100
for index := 0; index < numTests; index++ {
TestAppendSync(t)
log.Printf("finished test: %v", index)
}
}
func WriteAtSyncWorker(t *testing.T, ctx context.Context, wg *sync.WaitGroup, index int64) {
@ -753,13 +779,15 @@ func WriteAtSyncWorker(t *testing.T, ctx context.Context, wg *sync.WaitGroup, in
}
func TestWriteAtSync(t *testing.T) {
InitDBState()
initTestDb(t)
defer cleanupTestDB(t)
var wg sync.WaitGroup
numWorkers := 10
ctx := context.Background()
fileMeta := make(FileMeta)
fileMeta["test-descriptor"] = true
fileOpts := FileOptsType{MaxSize: int64(5 * units.Gigabyte), Circular: false, IJson: false}
fileOpts := FileOptsType{MaxSize: bigFileSize, Circular: false, IJson: false}
err := MakeFile(ctx, "test-block-id-sync", "file-1", fileMeta, fileOpts)
if err != nil {
t.Fatalf("MakeFile error: %v", err)
@ -777,22 +805,16 @@ func TestWriteAtSync(t *testing.T) {
}
log.Printf("read buf : %v", readBuf)
SimpleAssert(t, bytesRead == numWorkers, "Correct num bytes read")
CleanupName(t, ctx, "test-block-id-sync")
}
func TestWriteAtSyncMultiple(t *testing.T) {
numTests := 100
for index := 0; index < numTests; index++ {
TestWriteAtSync(t)
}
}
func TestWriteFile(t *testing.T) {
InitDBState()
initTestDb(t)
defer cleanupTestDB(t)
ctx := context.Background()
fileMeta := make(FileMeta)
fileMeta["test-descriptor"] = true
fileOpts := FileOptsType{MaxSize: int64(5 * units.Gigabyte), Circular: false, IJson: false}
fileOpts := FileOptsType{MaxSize: bigFileSize, Circular: false, IJson: false}
testBytesToWrite := []byte{'T', 'E', 'S', 'T', 'M', 'E', 'S', 'S', 'A', 'G', 'E'}
bytesWritten, err := WriteFile(ctx, "test-block-id", "file-1", fileMeta, fileOpts, testBytesToWrite)
if err != nil {
@ -807,15 +829,16 @@ func TestWriteFile(t *testing.T) {
SimpleAssert(t, bytesRead == bytesWritten, "Correct num bytes read")
log.Printf("bytes read: %v string: %s", read, string(read))
SimpleAssert(t, bytes.Equal(read, testBytesToWrite), "Correct bytes read")
Cleanup(t, ctx)
}
func TestWriteMeta(t *testing.T) {
InitDBState()
initTestDb(t)
defer cleanupTestDB(t)
ctx := context.Background()
fileMeta := make(FileMeta)
fileMeta["test-descriptor"] = true
fileOpts := FileOptsType{MaxSize: int64(5 * units.Gigabyte), Circular: false, IJson: false}
fileOpts := FileOptsType{MaxSize: bigFileSize, Circular: false, IJson: false}
err := MakeFile(ctx, "test-block-id", "file-1", fileMeta, fileOpts)
if err != nil {
t.Fatalf("MakeFile error: %v", err)
@ -843,15 +866,16 @@ func TestWriteMeta(t *testing.T) {
}
log.Printf("meta: %v", fInfo.Meta)
SimpleAssert(t, fInfo.Meta["second-test-descriptor"] == "test1", "Retrieved second meta correctly")
Cleanup(t, ctx)
}
func TestGetAllBlockIds(t *testing.T) {
InitDBState()
initTestDb(t)
defer cleanupTestDB(t)
ctx := context.Background()
fileMeta := make(FileMeta)
fileMeta["test-descriptor"] = true
fileOpts := FileOptsType{MaxSize: int64(5 * units.Gigabyte), Circular: false, IJson: false}
fileOpts := FileOptsType{MaxSize: bigFileSize, Circular: false, IJson: false}
err := MakeFile(ctx, "test-block-id", "file-1", fileMeta, fileOpts)
err = MakeFile(ctx, "test-block-id-2", "file-1", fileMeta, fileOpts)
err = MakeFile(ctx, "test-block-id-2", "file-2", fileMeta, fileOpts)
@ -864,16 +888,17 @@ func TestGetAllBlockIds(t *testing.T) {
testBlockIdArr := []string{"test-block-id", "test-block-id-2", "test-block-id-3"}
for idx, val := range blockIds {
SimpleAssert(t, testBlockIdArr[idx] == val, "Correct blockid value")
CleanupName(t, ctx, val)
}
}
func TestListFiles(t *testing.T) {
InitDBState()
initTestDb(t)
defer cleanupTestDB(t)
ctx := context.Background()
fileMeta := make(FileMeta)
fileMeta["test-descriptor"] = true
fileOpts := FileOptsType{MaxSize: int64(5 * units.Gigabyte), Circular: false, IJson: false}
fileOpts := FileOptsType{MaxSize: bigFileSize, Circular: false, IJson: false}
err := MakeFile(ctx, "test-block-id", "file-1", fileMeta, fileOpts)
err = MakeFile(ctx, "test-block-id-2", "file-1", fileMeta, fileOpts)
err = MakeFile(ctx, "test-block-id-2", "file-2", fileMeta, fileOpts)
@ -893,19 +918,18 @@ func TestListFiles(t *testing.T) {
for idx, val := range files {
SimpleAssert(t, val.Name == blockid_1_files[idx], "Correct file name")
}
CleanupName(t, ctx, "test-block-id")
CleanupName(t, ctx, "test-block-id-2")
CleanupName(t, ctx, "test-block-id-3")
}
func TestFlushTimer(t *testing.T) {
initTestDb(t)
defer cleanupTestDB(t)
testFlushTimeout := 10 * time.Second
SetFlushTimeout(testFlushTimeout)
InitDBState()
ctx := context.Background()
fileMeta := make(FileMeta)
fileMeta["test-descriptor"] = true
fileOpts := FileOptsType{MaxSize: int64(5 * units.Gigabyte), Circular: false, IJson: false}
fileOpts := FileOptsType{MaxSize: bigFileSize, Circular: false, IJson: false}
err := MakeFile(ctx, "test-block-id", "file-1", fileMeta, fileOpts)
if err != nil {
t.Fatalf("MakeFile error: %v", err)
@ -958,22 +982,12 @@ func TestFlushTimer(t *testing.T) {
t.Errorf("get data from db error: %v", txErr)
}
log.Printf("DB Data: %v", dbData)
Cleanup(t, ctx)
}
func TestFlushTimerMultiple(t *testing.T) {
testFlushTimeout := 1 * time.Second
SetFlushTimeout(testFlushTimeout)
numTests := 10
for index := 0; index < numTests; index++ {
TestWriteAt(t)
time.Sleep(500 * time.Millisecond)
}
}
// time consuming test
func TestWriteAtMiddle(t *testing.T) {
initTestDb(t)
defer cleanupTestDB(t)
ctx := context.Background()
WriteLargeDataFlush(t, ctx)
testBytesToWrite := []byte{'T', 'E', 'S', 'T', 'M', 'E', 'S', 'S', 'A', 'G', 'E'}
@ -989,23 +1003,26 @@ func TestWriteAtMiddle(t *testing.T) {
log.Printf("readBuf: %v %v", readBuf, string(readBuf))
SimpleAssert(t, bytesRead == bytesWritten, "Correct num bytes read")
SimpleAssert(t, bytes.Equal(readBuf, testBytesToWrite), "read correct bytes")
Cleanup(t, ctx)
}
func TestWriteLargeDataFlush(t *testing.T) {
initTestDb(t)
defer cleanupTestDB(t)
ctx := context.Background()
WriteLargeDataFlush(t, ctx)
Cleanup(t, ctx)
}
func TestWriteLargeDataNoFlush(t *testing.T) {
InitDBState()
initTestDb(t)
defer cleanupTestDB(t)
writeSize := int64(64 - 16)
fullWriteSize := int64(1024 * units.Megabyte)
fullWriteSize := int64(64 * UnitsKB)
ctx := context.Background()
fileMeta := make(FileMeta)
fileMeta["test-descriptor"] = true
fileOpts := FileOptsType{MaxSize: int64(5 * units.Gigabyte), Circular: false, IJson: false}
fileOpts := FileOptsType{MaxSize: bigFileSize, Circular: false, IJson: false}
err := MakeFile(ctx, "test-block-id", "file-1", fileMeta, fileOpts)
if err != nil {
t.Fatalf("MakeFile error: %v", err)
@ -1028,11 +1045,13 @@ func TestWriteLargeDataNoFlush(t *testing.T) {
copy(hashBuf, hash.Sum(nil))
bytesWritten, err := WriteAt(ctx, "test-block-id", "file-1", writeBuf, writeIndex)
if int64(bytesWritten) != writeSize {
log.Printf("write issue: %v %v \n", bytesWritten, writeSize)
t.Errorf("write issue: %v %v %v err:%v\n", bytesWritten, writeSize, writeIndex, err)
return
}
if err != nil {
log.Printf("error: %v", err)
t.Errorf("Write At error: %v\n", err)
return
}
writeIndex += int64(bytesWritten)
}
@ -1060,7 +1079,6 @@ func TestWriteLargeDataNoFlush(t *testing.T) {
}
log.Printf("final hash: %v readBuf: %v, bytesRead: %v", readHashBuf, readBuf, readIndex)
SimpleAssert(t, bytes.Equal(readHashBuf, hashBuf), "hashes are equal")
Cleanup(t, ctx)
}
// saving this code for later

View File

@ -1311,7 +1311,7 @@ func checkForWriteFinished(ctx context.Context, iter *packet.RpcResponseIter) er
return nil
}
func doCopyLocalFileToRemote(ctx context.Context, cmd *sstore.CmdType, remote_msh *remote.MShellProc, localPath string, destPath string, outputPos int64) {
func doCopyLocalFileToRemote(ctx context.Context, cmd *sstore.CmdType, remoteWsh *remote.WaveshellProc, localPath string, destPath string, outputPos int64) {
var exitSuccess bool
startTime := time.Now()
defer func() {
@ -1326,7 +1326,7 @@ func doCopyLocalFileToRemote(ctx context.Context, cmd *sstore.CmdType, remote_ms
writePk := packet.MakeWriteFilePacket()
writePk.ReqId = uuid.New().String()
writePk.Path = destPath
iter, err := remote_msh.WriteFile(ctx, writePk)
iter, err := remoteWsh.WriteFile(ctx, writePk)
if err != nil {
writeStringToPty(ctx, cmd, fmt.Sprintf("Error starting file write: %v\r\n", err), &outputPos)
return
@ -1358,7 +1358,7 @@ func doCopyLocalFileToRemote(ctx context.Context, cmd *sstore.CmdType, remote_ms
} else if err != nil {
dataErr := fmt.Sprintf("error reading file data: %v", err)
dataPk.Error = dataErr
remote_msh.SendFileData(dataPk)
remoteWsh.SendFileData(dataPk)
writeStringToPty(ctx, cmd, dataErr, &outputPos)
return
}
@ -1373,7 +1373,7 @@ func doCopyLocalFileToRemote(ctx context.Context, cmd *sstore.CmdType, remote_ms
lastFileTransferPercentage = fileTransferPercentage
}
}
remote_msh.SendFileData(dataPk)
remoteWsh.SendFileData(dataPk)
if dataPk.Eof {
break
}
@ -1405,7 +1405,7 @@ func getStatusBarString(filePercentageInt int) string {
return statusBarString
}
func doCopyRemoteFileToRemote(ctx context.Context, cmd *sstore.CmdType, sourceMsh *remote.MShellProc, destMsh *remote.MShellProc, sourcePath string, destPath string, outputPos int64) {
func doCopyRemoteFileToRemote(ctx context.Context, cmd *sstore.CmdType, sourceWsh *remote.WaveshellProc, destWsh *remote.WaveshellProc, sourcePath string, destPath string, outputPos int64) {
var exitSuccess bool
startTime := time.Now()
defer func() {
@ -1414,7 +1414,7 @@ func doCopyRemoteFileToRemote(ctx context.Context, cmd *sstore.CmdType, sourceMs
streamPk := packet.MakeStreamFilePacket()
streamPk.ReqId = uuid.New().String()
streamPk.Path = sourcePath
sourceStreamIter, err := sourceMsh.StreamFile(ctx, streamPk)
sourceStreamIter, err := sourceWsh.StreamFile(ctx, streamPk)
if err != nil {
writeStringToPty(ctx, cmd, fmt.Sprintf("Error getting file data packet: %v\r\n", err), &outputPos)
return
@ -1443,7 +1443,7 @@ func doCopyRemoteFileToRemote(ctx context.Context, cmd *sstore.CmdType, sourceMs
writePk := packet.MakeWriteFilePacket()
writePk.ReqId = uuid.New().String()
writePk.Path = destPath
destWriteIter, err := destMsh.WriteFile(ctx, writePk)
destWriteIter, err := destWsh.WriteFile(ctx, writePk)
if err != nil {
writeStringToPty(ctx, cmd, fmt.Sprintf("Error starting file write: %v\r\n", err), &outputPos)
return
@ -1482,7 +1482,7 @@ func doCopyRemoteFileToRemote(ctx context.Context, cmd *sstore.CmdType, sourceMs
writeDataPk.Type = dataPk.Type
writeDataPk.Data = make([]byte, int64(len(dataPk.Data)))
copy(writeDataPk.Data, dataPk.Data)
err = destMsh.SendFileData(writeDataPk)
err = destWsh.SendFileData(writeDataPk)
if err != nil {
writeStringToPty(ctx, cmd, fmt.Sprintf("error sending file to dest: %v\r\n", err), &outputPos)
return
@ -1542,7 +1542,7 @@ func doCopyLocalFileToLocal(ctx context.Context, cmd *sstore.CmdType, sourcePath
exitSuccess = true
}
func doCopyRemoteFileToLocal(ctx context.Context, cmd *sstore.CmdType, remote_msh *remote.MShellProc, sourcePath string, localPath string, outputPos int64) {
func doCopyRemoteFileToLocal(ctx context.Context, cmd *sstore.CmdType, remoteWsh *remote.WaveshellProc, sourcePath string, localPath string, outputPos int64) {
var exitSuccess bool
startTime := time.Now()
defer func() {
@ -1551,7 +1551,7 @@ func doCopyRemoteFileToLocal(ctx context.Context, cmd *sstore.CmdType, remote_ms
streamPk := packet.MakeStreamFilePacket()
streamPk.ReqId = uuid.New().String()
streamPk.Path = sourcePath
iter, err := remote_msh.StreamFile(ctx, streamPk)
iter, err := remoteWsh.StreamFile(ctx, streamPk)
if err != nil {
writeStringToPty(ctx, cmd, fmt.Sprintf("Error getting file data packet: %v\r\n", err), &outputPos)
return
@ -1700,11 +1700,11 @@ func CopyFileCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scb
var sourceFullPath string
var destFullPath string
sourceMsh := sourceRemoteId.MShell
if sourceMsh == nil {
return nil, fmt.Errorf("failure getting source remote mshell")
sourceWsh := sourceRemoteId.Waveshell
if sourceWsh == nil {
return nil, fmt.Errorf("failure getting source remote waveshell")
}
sourceRRState := sourceMsh.GetRemoteRuntimeState()
sourceRRState := sourceWsh.GetRemoteRuntimeState()
sourcePathWithHome, err := sourceRRState.ExpandHomeDir(sourcePath)
if err != nil {
return nil, fmt.Errorf("expand home dir err: %v", err)
@ -1720,11 +1720,11 @@ func CopyFileCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scb
sourceFileName := filepath.Base(sourceFullPath)
destPath = filepath.Join(destPath, sourceFileName)
}
destMsh := destRemoteId.MShell
if destMsh == nil {
return nil, fmt.Errorf("failure getting dest remote mshell")
destWsh := destRemoteId.Waveshell
if destWsh == nil {
return nil, fmt.Errorf("failure getting dest remote waveshell")
}
destRRState := destMsh.GetRemoteRuntimeState()
destRRState := destWsh.GetRemoteRuntimeState()
destPathWithHome, err := destRRState.ExpandHomeDir(destPath)
if err != nil {
return nil, fmt.Errorf("expand home dir err: %v", err)
@ -1757,7 +1757,7 @@ func CopyFileCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scb
update.AddUpdate(sstore.InteractiveUpdate(pk.Interactive))
if destRemote != ConnectedRemote && destRemoteId != nil && !destRemoteId.RState.IsConnected() {
writeStringToPty(ctx, cmd, fmt.Sprintf("Attempting to autoconnect to remote %v\r\n", destRemote), &outputPos)
err = destRemoteId.MShell.TryAutoConnect()
err = destRemoteId.Waveshell.TryAutoConnect()
if err != nil {
writeStringToPty(ctx, cmd, fmt.Sprintf("Couldn't connect to remote %v\r\n", sourceRemote), &outputPos)
} else {
@ -1766,7 +1766,7 @@ func CopyFileCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scb
}
if sourceRemote != LocalRemote && sourceRemoteId != nil && !sourceRemoteId.RState.IsConnected() {
writeStringToPty(ctx, cmd, fmt.Sprintf("Attempting to autoconnect to remote %v\r\n", sourceRemote), &outputPos)
err = sourceRemoteId.MShell.TryAutoConnect()
err = sourceRemoteId.Waveshell.TryAutoConnect()
if err != nil {
writeStringToPty(ctx, cmd, fmt.Sprintf("Couldn't connect to remote %v\r\n", sourceRemote), &outputPos)
} else {
@ -1778,11 +1778,11 @@ func CopyFileCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scb
if destRemote == LocalRemote && sourceRemote == LocalRemote {
go doCopyLocalFileToLocal(context.Background(), cmd, sourceFullPath, destFullPath, outputPos)
} else if destRemote == LocalRemote && sourceRemote != LocalRemote {
go doCopyRemoteFileToLocal(context.Background(), cmd, sourceMsh, sourceFullPath, destFullPath, outputPos)
go doCopyRemoteFileToLocal(context.Background(), cmd, sourceWsh, sourceFullPath, destFullPath, outputPos)
} else if destRemote != LocalRemote && sourceRemote == LocalRemote {
go doCopyLocalFileToRemote(context.Background(), cmd, destMsh, sourceFullPath, destFullPath, outputPos)
go doCopyLocalFileToRemote(context.Background(), cmd, destWsh, sourceFullPath, destFullPath, outputPos)
} else if destRemote != LocalRemote && sourceRemote != LocalRemote {
go doCopyRemoteFileToRemote(context.Background(), cmd, sourceMsh, destMsh, sourceFullPath, destFullPath, outputPos)
go doCopyRemoteFileToRemote(context.Background(), cmd, sourceWsh, destWsh, sourceFullPath, destFullPath, outputPos)
}
return update, nil
}
@ -1792,8 +1792,8 @@ func RemoteInstallCommand(ctx context.Context, pk *scpacket.FeCommandPacketType)
if err != nil {
return nil, err
}
mshell := ids.Remote.MShell
go mshell.RunInstall(false)
wshell := ids.Remote.Waveshell
go wshell.RunInstall(false)
return createRemoteViewRemoteIdUpdate(ids.Remote.RemotePtr.RemoteId), nil
}
@ -1802,8 +1802,8 @@ func RemoteInstallCancelCommand(ctx context.Context, pk *scpacket.FeCommandPacke
if err != nil {
return nil, err
}
mshell := ids.Remote.MShell
go mshell.CancelInstall()
wshell := ids.Remote.Waveshell
go wshell.CancelInstall()
return createRemoteViewRemoteIdUpdate(ids.Remote.RemotePtr.RemoteId), nil
}
@ -1812,7 +1812,7 @@ func RemoteConnectCommand(ctx context.Context, pk *scpacket.FeCommandPacketType)
if err != nil {
return nil, err
}
go ids.Remote.MShell.Launch(true)
go ids.Remote.Waveshell.Launch(true)
return createRemoteViewRemoteIdUpdate(ids.Remote.RemotePtr.RemoteId), nil
}
@ -1822,7 +1822,7 @@ func RemoteDisconnectCommand(ctx context.Context, pk *scpacket.FeCommandPacketTy
return nil, err
}
force := resolveBool(pk.Kwargs["force"], false)
go ids.Remote.MShell.Disconnect(force)
go ids.Remote.Waveshell.Disconnect(force)
return createRemoteViewRemoteIdUpdate(ids.Remote.RemotePtr.RemoteId), nil
}
@ -2082,7 +2082,7 @@ func RemoteSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sc
}
visualEdit := resolveBool(pk.Kwargs["visual"], false)
isSubmitted := resolveBool(pk.Kwargs["submit"], false)
editArgs, err := parseRemoteEditArgs(false, pk, ids.Remote.MShell.IsLocal())
editArgs, err := parseRemoteEditArgs(false, pk, ids.Remote.Waveshell.IsLocal())
if err != nil {
return makeRemoteEditErrorReturn_edit(ids, visualEdit, fmt.Errorf("/remote:new %v", err))
}
@ -2092,7 +2092,7 @@ func RemoteSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sc
if !visualEdit && len(editArgs.EditMap) == 0 {
return nil, fmt.Errorf("/remote:set no updates, can set %s. (set visual=1 to edit in UI)", formatStrs(RemoteSetArgs, "or", false))
}
err = ids.Remote.MShell.UpdateRemote(ctx, editArgs.EditMap)
err = ids.Remote.Waveshell.UpdateRemote(ctx, editArgs.EditMap)
if err != nil {
return makeRemoteEditErrorReturn_edit(ids, visualEdit, fmt.Errorf("/remote:new error updating remote: %v", err))
}
@ -2367,19 +2367,19 @@ func RemoteConfigParseCommand(ctx context.Context, pk *scpacket.FeCommandPacketT
editMap[sstore.RemoteField_SSHKey] = hostInfo.SshKeyFile
}
editMap[sstore.RemoteField_ShellPref] = hostInfo.ShellPref
msh := remote.GetRemoteById(previouslyImportedRemote.RemoteId)
if msh == nil {
wsh := remote.GetRemoteById(previouslyImportedRemote.RemoteId)
if wsh == nil {
remoteChangeList["updateErr"] = append(remoteChangeList["updateErr"], hostInfo.CanonicalName)
log.Printf("strange, msh for remote %s [%s] not found\n", hostInfo.CanonicalName, previouslyImportedRemote.RemoteId)
log.Printf("strange, wsh for remote %s [%s] not found\n", hostInfo.CanonicalName, previouslyImportedRemote.RemoteId)
continue
}
if msh.Remote.ConnectMode == hostInfo.ConnectMode && msh.Remote.SSHOpts.SSHIdentity == hostInfo.SshKeyFile && msh.Remote.RemoteAlias == hostInfo.Host && msh.Remote.ShellPref == hostInfo.ShellPref {
if wsh.Remote.ConnectMode == hostInfo.ConnectMode && wsh.Remote.SSHOpts.SSHIdentity == hostInfo.SshKeyFile && wsh.Remote.RemoteAlias == hostInfo.Host && wsh.Remote.ShellPref == hostInfo.ShellPref {
// silently skip this one. it didn't fail, but no changes were needed
continue
}
err := msh.UpdateRemote(ctx, editMap)
err := wsh.UpdateRemote(ctx, editMap)
if err != nil {
remoteChangeList["updateErr"] = append(remoteChangeList["updateErr"], hostInfo.CanonicalName)
log.Printf("error updating remote[%s]: %v\n", hostInfo.CanonicalName, err)
@ -2548,11 +2548,11 @@ func crShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType, ids re
}
for _, ri := range riArr {
rptr := sstore.RemotePtrType{RemoteId: ri.RemoteId, Name: ri.Name}
msh := remote.GetRemoteById(ri.RemoteId)
if msh == nil {
wsh := remote.GetRemoteById(ri.RemoteId)
if wsh == nil {
continue
}
baseDisplayName := msh.GetDisplayName()
baseDisplayName := wsh.GetDisplayName()
displayName := rptr.GetDisplayName(baseDisplayName)
cwdStr := "-"
if ri.FeState["cwd"] != "" {
@ -3006,17 +3006,17 @@ func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.Upd
if rstate.Archived {
return nil, fmt.Errorf("/%s error: remote %q cannot switch to archived remote", GetCmdStr(pk), newRemote)
}
newMsh := remote.GetRemoteById(rptr.RemoteId)
if newMsh == nil {
return nil, fmt.Errorf("/%s error: remote %q not found (msh)", GetCmdStr(pk), newRemote)
newWsh := remote.GetRemoteById(rptr.RemoteId)
if newWsh == nil {
return nil, fmt.Errorf("/%s error: remote %q not found (wsh)", GetCmdStr(pk), newRemote)
}
if !newMsh.IsConnected() {
err := newMsh.TryAutoConnect()
if !newWsh.IsConnected() {
err := newWsh.TryAutoConnect()
if err != nil {
return nil, fmt.Errorf("%q is disconnected, auto-connect failed: %w", rstate.GetBaseDisplayName(), err)
}
if !newMsh.IsConnected() {
if newMsh.GetRemoteCopy().ConnectMode == sstore.ConnectModeManual {
if !newWsh.IsConnected() {
if newWsh.GetRemoteCopy().ConnectMode == sstore.ConnectModeManual {
return nil, fmt.Errorf("%q is disconnected (must manually connect)", rstate.GetBaseDisplayName())
}
return nil, fmt.Errorf("%q is disconnected", rstate.GetBaseDisplayName())
@ -3057,7 +3057,7 @@ func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.Upd
ScreenId: ids.ScreenId,
RPtr: *rptr,
}
go doAsyncResetCommand(newMsh, opts, cmd)
go doAsyncResetCommand(newWsh, opts, cmd)
return update, nil
} else {
outputStr := fmt.Sprintf("reconnected to %s", GetFullRemoteDisplayName(rptr, rstate))
@ -3298,7 +3298,7 @@ func doCompGen(ctx context.Context, pk *scpacket.FeCommandPacketType, prefix str
cgPacket.CompType = compType
cgPacket.Prefix = prefix
cgPacket.Cwd = ids.Remote.FeState["cwd"]
resp, err := ids.Remote.MShell.PacketRpc(ctx, cgPacket)
resp, err := ids.Remote.Waveshell.PacketRpc(ctx, cgPacket)
if err != nil {
return nil, false, err
}
@ -3942,7 +3942,7 @@ func ClearSudoCache(ctx context.Context, pk *scpacket.FeCommandPacketType) (rtnU
if err != nil {
return nil, err
}
ids.Remote.MShell.ClearCachedSudoPw()
ids.Remote.Waveshell.ClearCachedSudoPw()
pluralize := ""
clearAll := resolveBool(pk.Kwargs["all"], false)
@ -3966,7 +3966,7 @@ func RemoteResetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (
if err != nil {
return nil, err
}
if !ids.Remote.MShell.IsConnected() {
if !ids.Remote.Waveshell.IsConnected() {
return nil, fmt.Errorf("cannot reinit, remote is not connected")
}
verbose := resolveBool(pk.Kwargs["verbose"], false)
@ -3994,7 +3994,7 @@ func RemoteResetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (
ScreenId: ids.ScreenId,
RPtr: ids.Remote.RemotePtr,
}
go doAsyncResetCommand(ids.Remote.MShell, opts, cmd)
go doAsyncResetCommand(ids.Remote.Waveshell, opts, cmd)
return update, nil
}
@ -4007,7 +4007,7 @@ type connectOptsType struct {
}
// this does the asynchroneous part of the connection reset
func doAsyncResetCommand(msh *remote.MShellProc, opts connectOptsType, cmd *sstore.CmdType) {
func doAsyncResetCommand(wsh *remote.WaveshellProc, opts connectOptsType, cmd *sstore.CmdType) {
ctx, cancelFn := context.WithCancel(context.Background())
defer cancelFn()
startTime := time.Now()
@ -4025,7 +4025,7 @@ func doAsyncResetCommand(msh *remote.MShellProc, opts connectOptsType, cmd *ssto
writeStringToPty(ctx, cmd, string(data), &outputPos)
}
origStatePtr, _ := sstore.GetRemoteStatePtr(ctx, opts.SessionId, opts.ScreenId, opts.RPtr)
ssPk, err := msh.ReInit(ctx, base.MakeCommandKey(cmd.ScreenId, cmd.LineId), opts.ShellType, dataFn, opts.Verbose)
ssPk, err := wsh.ReInit(ctx, base.MakeCommandKey(cmd.ScreenId, cmd.LineId), opts.ShellType, dataFn, opts.Verbose)
if err != nil {
rtnErr = err
return
@ -4296,11 +4296,11 @@ func resizeRunningCommand(ctx context.Context, cmd *sstore.CmdType, newCols int)
feInput := scpacket.MakeFeInputPacket()
feInput.CK = base.MakeCommandKey(cmd.ScreenId, cmd.LineId)
feInput.WinSize = &packet.WinSize{Rows: int(cmd.TermOpts.Rows), Cols: newCols}
msh := remote.GetRemoteById(cmd.Remote.RemoteId)
if msh == nil {
wsh := remote.GetRemoteById(cmd.Remote.RemoteId)
if wsh == nil {
return fmt.Errorf("cannot resize, cmd remote not found")
}
err := msh.HandleFeInput(feInput)
err := wsh.HandleFeInput(feInput)
if err != nil {
return err
}
@ -4421,12 +4421,12 @@ func LineRestartCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (
if cmd.Status == sstore.CmdStatusRunning || cmd.Status == sstore.CmdStatusDetached {
killCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
err = ids.Remote.MShell.KillRunningCommandAndWait(killCtx, base.MakeCommandKey(ids.ScreenId, lineId))
err = ids.Remote.Waveshell.KillRunningCommandAndWait(killCtx, base.MakeCommandKey(ids.ScreenId, lineId))
if err != nil {
return nil, err
}
}
ids.Remote.MShell.ResetDataPos(base.MakeCommandKey(ids.ScreenId, lineId))
ids.Remote.Waveshell.ResetDataPos(base.MakeCommandKey(ids.ScreenId, lineId))
err = sstore.ClearCmdPtyFile(ctx, ids.ScreenId, lineId)
if err != nil {
return nil, fmt.Errorf("error clearing existing pty file: %v", err)
@ -5065,8 +5065,8 @@ func ViewStatCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scb
return nil, err
}
streamPk.StatOnly = true
msh := ids.Remote.MShell
iter, err := msh.StreamFile(ctx, streamPk)
wsh := ids.Remote.Waveshell
iter, err := wsh.StreamFile(ctx, streamPk)
if err != nil {
return nil, fmt.Errorf("/view:stat error: %v", err)
}
@ -5116,8 +5116,8 @@ func ViewTestCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scb
if err != nil {
return nil, err
}
msh := ids.Remote.MShell
iter, err := msh.StreamFile(ctx, streamPk)
wsh := ids.Remote.Waveshell
iter, err := wsh.StreamFile(ctx, streamPk)
if err != nil {
return nil, fmt.Errorf("/view:test error: %v", err)
}
@ -5413,8 +5413,8 @@ func EditTestCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scb
} else {
writePk.Path = filepath.Join(cwd, fileArg)
}
msh := ids.Remote.MShell
iter, err := msh.PacketRpcIter(ctx, writePk)
wsh := ids.Remote.Waveshell
iter, err := wsh.PacketRpcIter(ctx, writePk)
if err != nil {
return nil, fmt.Errorf("/edit:test error: %v", err)
}
@ -5433,7 +5433,7 @@ func EditTestCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scb
dataPk := packet.MakeFileDataPacket(writePk.ReqId)
dataPk.Data = []byte(content)
dataPk.Eof = true
err = msh.SendFileData(dataPk)
err = wsh.SendFileData(dataPk)
if err != nil {
return nil, fmt.Errorf("/edit:test error sending data packet: %v", err)
}
@ -5500,17 +5500,17 @@ func SignalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus
if !sigNameRe.MatchString(sigArg) {
return nil, fmt.Errorf("invalid signal name/number: %q", sigArg)
}
msh := remote.GetRemoteById(cmd.Remote.RemoteId)
if msh == nil {
wsh := remote.GetRemoteById(cmd.Remote.RemoteId)
if wsh == nil {
return nil, fmt.Errorf("cannot send signal, no remote found for command")
}
if !msh.IsConnected() {
if !wsh.IsConnected() {
return nil, fmt.Errorf("cannot send signal, remote is not connected")
}
inputPk := scpacket.MakeFeInputPacket()
inputPk.CK = base.MakeCommandKey(cmd.ScreenId, cmd.LineId)
inputPk.SigName = sigArg
err = msh.HandleFeInput(inputPk)
err = wsh.HandleFeInput(inputPk)
if err != nil {
return nil, fmt.Errorf("cannot send signal: %v", err)
}

View File

@ -39,7 +39,7 @@ type resolvedIds struct {
type ResolvedRemote struct {
DisplayName string
RemotePtr sstore.RemotePtrType
MShell *remote.MShellProc
Waveshell *remote.WaveshellProc
RState remote.RemoteRuntimeState
RemoteCopy *sstore.RemoteType
ShellType string // default remote shell preference
@ -201,11 +201,11 @@ func resolveRemoteArg(remoteArg string) (*sstore.RemotePtrType, error) {
if rrUser != "" {
return nil, fmt.Errorf("remoteusers not supported")
}
msh := remote.GetRemoteByArg(rrRemote)
if msh == nil {
wsh := remote.GetRemoteByArg(rrRemote)
if wsh == nil {
return nil, nil
}
rcopy := msh.GetRemoteCopy()
rcopy := wsh.GetRemoteCopy()
return &sstore.RemotePtrType{RemoteId: rcopy.RemoteId, Name: rrName}, nil
}
@ -269,7 +269,7 @@ func resolveUiIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype i
}
if rtype&R_RemoteConnected > 0 {
if !rtn.Remote.RState.IsConnected() {
err = rtn.Remote.MShell.TryAutoConnect()
err = rtn.Remote.Waveshell.TryAutoConnect()
if err != nil {
return rtn, fmt.Errorf("error trying to auto-connect remote [%s]: %w", rtn.Remote.DisplayName, err)
}
@ -464,18 +464,18 @@ func ResolveRemoteFromPtr(ctx context.Context, rptr *sstore.RemotePtrType, sessi
if rptr == nil || rptr.RemoteId == "" {
return nil, nil
}
msh := remote.GetRemoteById(rptr.RemoteId)
if msh == nil {
wsh := remote.GetRemoteById(rptr.RemoteId)
if wsh == nil {
return nil, fmt.Errorf("invalid remote '%s', not found", rptr.RemoteId)
}
rstate := msh.GetRemoteRuntimeState()
rcopy := msh.GetRemoteCopy()
rstate := wsh.GetRemoteRuntimeState()
rcopy := wsh.GetRemoteCopy()
displayName := rstate.GetDisplayName(rptr)
rtn := &ResolvedRemote{
DisplayName: displayName,
RemotePtr: *rptr,
RState: rstate,
MShell: msh,
Waveshell: wsh,
RemoteCopy: &rcopy,
StatePtr: nil,
FeState: nil,
@ -488,7 +488,7 @@ func ResolveRemoteFromPtr(ctx context.Context, rptr *sstore.RemotePtrType, sessi
// continue with state set to nil
} else {
if ri == nil {
rtn.ShellType = msh.GetShellPref()
rtn.ShellType = wsh.GetShellPref()
rtn.StatePtr = nil
rtn.FeState = nil
} else {

View File

@ -65,8 +65,8 @@ func doCompGen(ctx context.Context, prefix string, compType string, compCtx Comp
if !packet.IsValidCompGenType(compType) {
return nil, fmt.Errorf("/_compgen invalid type '%s'", compType)
}
msh := remote.GetRemoteById(compCtx.RemotePtr.RemoteId)
if msh == nil {
wsh := remote.GetRemoteById(compCtx.RemotePtr.RemoteId)
if wsh == nil {
return nil, fmt.Errorf("invalid remote '%s', not found", compCtx.RemotePtr)
}
cgPacket := packet.MakeCompGenPacket()
@ -74,7 +74,7 @@ func doCompGen(ctx context.Context, prefix string, compType string, compCtx Comp
cgPacket.CompType = compType
cgPacket.Prefix = prefix
cgPacket.Cwd = compCtx.Cwd
resp, err := msh.PacketRpc(ctx, cgPacket)
resp, err := wsh.PacketRpc(ctx, cgPacket)
if err != nil {
return nil, err
}

File diff suppressed because it is too large Load Diff

View File

@ -36,7 +36,7 @@ const WaveDirName = ".waveterm" // must match emain.ts
const WaveDevDirName = ".waveterm-dev" // must match emain.ts
const WaveAppPathVarName = "WAVETERM_APP_PATH"
const WaveAuthKeyFileName = "waveterm.authkey"
const MShellVersion = "v0.7.0" // must match base.MShellVersion
const WaveshellVersion = "v0.7.0" // must match base.WaveshellVersion
// initialized by InitialzeWaveAuthKey (called by main-server)
var WaveAuthKey string
@ -73,7 +73,7 @@ func GetWaveHomeDir() string {
return scHome
}
func MShellBinaryDir() string {
func WaveshellBinaryDir() string {
appPath := os.Getenv(WaveAppPathVarName)
if appPath == "" {
appPath = "."
@ -81,32 +81,32 @@ func MShellBinaryDir() string {
return filepath.Join(appPath, "bin", "mshell")
}
func MShellBinaryPath(version string, goos string, goarch string) (string, error) {
func WaveshellBinaryPath(version string, goos string, goarch string) (string, error) {
if !base.ValidGoArch(goos, goarch) {
return "", fmt.Errorf("invalid goos/goarch combination: %s/%s", goos, goarch)
}
binaryDir := MShellBinaryDir()
binaryDir := WaveshellBinaryDir()
versionStr := semver.MajorMinor(version)
if versionStr == "" {
return "", fmt.Errorf("invalid mshell version: %q", version)
return "", fmt.Errorf("invalid waveshell version: %q", version)
}
fileName := fmt.Sprintf("mshell-%s-%s.%s", versionStr, goos, goarch)
fullFileName := filepath.Join(binaryDir, fileName)
return fullFileName, nil
}
func LocalMShellBinaryPath() (string, error) {
return MShellBinaryPath(MShellVersion, runtime.GOOS, runtime.GOARCH)
func LocalWaveshellBinaryPath() (string, error) {
return WaveshellBinaryPath(WaveshellVersion, runtime.GOOS, runtime.GOARCH)
}
func MShellBinaryReader(version string, goos string, goarch string) (io.ReadCloser, error) {
mshellPath, err := MShellBinaryPath(version, goos, goarch)
func WaveshellBinaryReader(version string, goos string, goarch string) (io.ReadCloser, error) {
waveshellPath, err := WaveshellBinaryPath(version, goos, goarch)
if err != nil {
return nil, err
}
fd, err := os.Open(mshellPath)
fd, err := os.Open(waveshellPath)
if err != nil {
return nil, fmt.Errorf("cannot open mshell binary %q: %v", mshellPath, err)
return nil, fmt.Errorf("cannot open waveshell binary %q: %v", waveshellPath, err)
}
return fd, nil
}

View File

@ -326,9 +326,9 @@ func sendCmdInput(pk *scpacket.FeInputPacketType) error {
if pk.Remote.RemoteId == "" {
return fmt.Errorf("input must set remoteid")
}
msh := remote.GetRemoteById(pk.Remote.RemoteId)
if msh == nil {
wsh := remote.GetRemoteById(pk.Remote.RemoteId)
if wsh == nil {
return fmt.Errorf("remote %s not found", pk.Remote.RemoteId)
}
return msh.HandleFeInput(pk)
return wsh.HandleFeInput(pk)
}

View File

@ -751,10 +751,10 @@ func UpdateCmdForRestart(ctx context.Context, ck base.CommandKey, ts int64, cmdP
})
}
func UpdateCmdStartInfo(ctx context.Context, ck base.CommandKey, cmdPid int, mshellPid int) error {
func UpdateCmdStartInfo(ctx context.Context, ck base.CommandKey, cmdPid int, waveshellPid int) error {
return WithTx(ctx, func(tx *TxWrap) error {
query := `UPDATE cmd SET cmdpid = ?, remotepid = ? WHERE screenid = ? AND lineid = ?`
tx.Exec(query, cmdPid, mshellPid, ck.GetGroupId(), lineIdFromCK(ck))
tx.Exec(query, cmdPid, waveshellPid, ck.GetGroupId(), lineIdFromCK(ck))
return nil
})
}

View File

@ -17,7 +17,7 @@ import (
_ "github.com/golang-migrate/migrate/v4/source/file"
"github.com/golang-migrate/migrate/v4/source/iofs"
_ "github.com/mattn/go-sqlite3"
sh2db "github.com/wavetermdev/waveterm/wavesrv/db"
dbfs "github.com/wavetermdev/waveterm/wavesrv/db"
"github.com/golang-migrate/migrate/v4"
)
@ -29,7 +29,7 @@ const CmdLineSpecialMigration = 20
const RISpecialMigration = 30
func MakeMigrate() (*migrate.Migrate, error) {
fsVar, err := iofs.New(sh2db.MigrationFS, "migrations")
fsVar, err := iofs.New(dbfs.MigrationFS, "migrations")
if err != nil {
return nil, fmt.Errorf("opening iofs: %w", err)
}

View File

@ -758,34 +758,34 @@ const (
)
type RemoteRuntimeState struct {
RemoteType string `json:"remotetype"`
RemoteId string `json:"remoteid"`
RemoteAlias string `json:"remotealias,omitempty"`
RemoteCanonicalName string `json:"remotecanonicalname"`
RemoteVars map[string]string `json:"remotevars"`
Status string `json:"status"`
ConnectTimeout int `json:"connecttimeout,omitempty"`
CountdownActive bool `json:"countdownactive"`
ErrorStr string `json:"errorstr,omitempty"`
InstallStatus string `json:"installstatus"`
InstallErrorStr string `json:"installerrorstr,omitempty"`
NeedsMShellUpgrade bool `json:"needsmshellupgrade,omitempty"`
NoInitPk bool `json:"noinitpk,omitempty"`
AuthType string `json:"authtype,omitempty"`
ConnectMode string `json:"connectmode"`
AutoInstall bool `json:"autoinstall"`
Archived bool `json:"archived,omitempty"`
RemoteIdx int64 `json:"remoteidx"`
SSHConfigSrc string `json:"sshconfigsrc"`
UName string `json:"uname"`
MShellVersion string `json:"mshellversion"`
WaitingForPassword bool `json:"waitingforpassword,omitempty"`
Local bool `json:"local,omitempty"`
IsSudo bool `json:"issudo,omitempty"`
RemoteOpts *RemoteOptsType `json:"remoteopts,omitempty"`
CanComplete bool `json:"cancomplete,omitempty"`
ShellPref string `json:"shellpref,omitempty"`
DefaultShellType string `json:"defaultshelltype,omitempty"`
RemoteType string `json:"remotetype"`
RemoteId string `json:"remoteid"`
RemoteAlias string `json:"remotealias,omitempty"`
RemoteCanonicalName string `json:"remotecanonicalname"`
RemoteVars map[string]string `json:"remotevars"`
Status string `json:"status"`
ConnectTimeout int `json:"connecttimeout,omitempty"`
CountdownActive bool `json:"countdownactive"`
ErrorStr string `json:"errorstr,omitempty"`
InstallStatus string `json:"installstatus"`
InstallErrorStr string `json:"installerrorstr,omitempty"`
NeedsWaveshellUpgrade bool `json:"needswaveshellupgrade,omitempty"`
NoInitPk bool `json:"noinitpk,omitempty"`
AuthType string `json:"authtype,omitempty"`
ConnectMode string `json:"connectmode"`
AutoInstall bool `json:"autoinstall"`
Archived bool `json:"archived,omitempty"`
RemoteIdx int64 `json:"remoteidx"`
SSHConfigSrc string `json:"sshconfigsrc"`
UName string `json:"uname"`
WaveshellVersion string `json:"waveshellversion"`
WaitingForPassword bool `json:"waitingforpassword,omitempty"`
Local bool `json:"local,omitempty"`
IsSudo bool `json:"issudo,omitempty"`
RemoteOpts *RemoteOptsType `json:"remoteopts,omitempty"`
CanComplete bool `json:"cancomplete,omitempty"`
ShellPref string `json:"shellpref,omitempty"`
DefaultShellType string `json:"defaultshelltype,omitempty"`
}
func (state RemoteRuntimeState) IsConnected() bool {

20112
yarn.lock

File diff suppressed because it is too large Load Diff