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

View File

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

9
.gitignore vendored
View File

@ -22,3 +22,12 @@ test/
.vscode/ .vscode/
make/ make/
waveterm-builds.zip 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 install go
brew tap scripthaus-dev/scripthaus brew tap scripthaus-dev/scripthaus
brew install scripthaus brew install scripthaus
npm install -g yarn corepack enable
yarn install
scripthaus run build-backend scripthaus run build-backend
echo "Yarn" echo "Yarn"
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): Download and install Go (must be at least go 1.18):
``` ```sh
brew install go brew install go
``` ```
Download and install ScriptHaus (to run the build commands): Download and install ScriptHaus (to run the build commands):
``` ```sh
brew tap scripthaus-dev/scripthaus brew tap scripthaus-dev/scripthaus
brew install scripthaus brew install scripthaus
``` ```
You also need a relatively modern nodejs with npm and yarn installed. You also need a relatively modern nodejs with npm and yarn installed.
- Node can be installed from [https://nodejs.org](https://nodejs.org). Node can be installed from [https://nodejs.org](https://nodejs.org).
- npm can install yarn using:
``` 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.
npm install -g yarn
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 ## Clone the Repo
``` ```sh
git clone git@github.com:wavetermdev/waveterm.git git clone git@github.com:wavetermdev/waveterm.git
``` ```
## Building WaveShell / WaveSrv ## Building WaveShell / WaveSrv
``` ```sh
scripthaus run build-backend 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): Install modules (we use yarn):
``` ```sh
yarn 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: We use webpack to build both the React and Electron App Wrapper code. They are both run together using:
``` ```sh
scripthaus run webpack-watch 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: 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 scripthaus run electron
``` ```

View File

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

View File

@ -115,15 +115,3 @@ scripthaus run fullbuild-waveshell
echo building wavesrv echo building wavesrv
scripthaus run build-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 React from "react";
import * as mobxReact from "mobx-react"; import * as mobxReact from "mobx-react";
import * as mobx from "mobx"; import * as mobx from "mobx";
import cn from "classnames"; import { clsx } from "clsx";
import { boundMethod } from "autobind-decorator"; import { boundMethod } from "autobind-decorator";
import { If } from "tsx-control-statements/components"; import { If } from "tsx-control-statements/components";
@ -133,7 +133,7 @@ class App extends React.Component<{}, {}> {
const rightSidebarCollapsed = GlobalModel.rightSidebarModel.getCollapsed(); const rightSidebarCollapsed = GlobalModel.rightSidebarModel.getCollapsed();
const activeMainView = GlobalModel.activeMainView.get(); const activeMainView = GlobalModel.activeMainView.get();
const lightDarkClass = GlobalModel.isDarkTheme.get() ? "is-dark" : "is-light"; const lightDarkClass = GlobalModel.isDarkTheme.get() ? "is-dark" : "is-light";
const mainClassName = cn( const mainClassName = clsx(
"platform-" + platform, "platform-" + platform,
{ {
"mainsidebar-collapsed": mainSidebarCollapsed, "mainsidebar-collapsed": mainSidebarCollapsed,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,7 +9,7 @@ import { GlobalModel } from "@/models";
import { Modal, LinkButton } from "@/elements"; import { Modal, LinkButton } from "@/elements";
import * as util from "@/util/util"; import * as util from "@/util/util";
import * as appconst from "@/app/appconst"; import * as appconst from "@/app/appconst";
import cn from "classnames"; import { clsx } from "clsx";
import { If } from "tsx-control-statements/components"; import { If } from "tsx-control-statements/components";
import logo from "@/assets/waveterm-logo-with-bg.svg"; 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"; const isUpToDate = !showUpdateStatus || GlobalModel.appUpdateStatus.get() !== "ready";
return ( return (
<div className={cn("status", { outdated: !isUpToDate })}> <div className={clsx("status", { outdated: !isUpToDate })}>
<If condition={!isUpToDate}> <If condition={!isUpToDate}>
<div> <div>
<i className="fa-sharp fa-solid fa-triangle-exclamation" /> <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 * as mobx from "mobx";
import { boundMethod } from "autobind-decorator"; import { boundMethod } from "autobind-decorator";
import { For } from "tsx-control-statements/components"; import { For } from "tsx-control-statements/components";
import cn from "classnames"; import { clsx } from "clsx";
import { GlobalModel, GlobalCommandRunner, Screen } from "@/models"; import { GlobalModel, GlobalCommandRunner, Screen } from "@/models";
import { SettingsError, Modal, Dropdown, Tooltip } from "@/elements"; import { SettingsError, Modal, Dropdown, Tooltip } from "@/elements";
import * as util from "@/util/util"; import * as util from "@/util/util";

View File

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

View File

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

View File

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

View File

@ -6,7 +6,7 @@ import * as mobxReact from "mobx-react";
import * as mobx from "mobx"; import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator"; import { boundMethod } from "autobind-decorator";
import { If, For } from "tsx-control-statements/components"; import { If, For } from "tsx-control-statements/components";
import cn from "classnames"; import { clsx } from "clsx";
import { GlobalModel, RemotesModel, GlobalCommandRunner } from "@/models"; import { GlobalModel, RemotesModel, GlobalCommandRunner } from "@/models";
import { Button, Status } from "@/common/elements"; import { Button, Status } from "@/common/elements";
import * as util from "@/util/util"; 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}> <For index="idx" each="item" of={items}>
<tr <tr
key={item.remoteid} key={item.remoteid}
className={cn("connections-item", { className={clsx("connections-item", {
hovered: this.state.hoveredItemId === item.remoteid, hovered: this.state.hoveredItemId === item.remoteid,
})} })}
onClick={() => this.handleRead(item.remoteid)} // Moved onClick here 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 { If, For } from "tsx-control-statements/components";
import { sprintf } from "sprintf-js"; import { sprintf } from "sprintf-js";
import { boundMethod } from "autobind-decorator"; import { boundMethod } from "autobind-decorator";
import cn from "classnames"; import { clsx } from "clsx";
import { GlobalModel, GlobalCommandRunner } from "@/models"; import { GlobalModel, GlobalCommandRunner } from "@/models";
import dayjs from "dayjs"; import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat"; import localizedFormat from "dayjs/plugin/localizedFormat";
@ -137,7 +137,7 @@ class HistoryCmdStr extends React.Component<
render() { render() {
const { cmdstr, fontSize, limitHeight } = this.props; const { cmdstr, fontSize, limitHeight } = this.props;
return ( 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"> <div key="code" className="code-div">
<code>{cmdstr}</code> <code>{cmdstr}</code>
</div> </div>
@ -490,7 +490,7 @@ class HistoryView extends React.Component<{}, {}> {
</Button> </Button>
</div> </div>
</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"> <div className="control-checkbox" onClick={this.handleControlCheckbox} title="Toggle Selection">
<HistoryCheckbox <HistoryCheckbox
checked={numSelected > 0 && numSelected == items.length} checked={numSelected > 0 && numSelected == items.length}
@ -498,7 +498,7 @@ class HistoryView extends React.Component<{}, {}> {
/> />
</div> </div>
<div <div
className={cn( className={clsx(
"control-button delete-button", "control-button delete-button",
{ "is-disabled": numSelected == 0 }, { "is-disabled": numSelected == 0 },
{ "is-active": hvm.deleteActive.get() } { "is-active": hvm.deleteActive.get() }
@ -515,14 +515,14 @@ class HistoryView extends React.Component<{}, {}> {
Showing {offset + 1}-{offset + items.length} Showing {offset + 1}-{offset + items.length}
</div> </div>
<div <div
className={cn("showing-btn", { "is-disabled": offset == 0 })} className={clsx("showing-btn", { "is-disabled": offset == 0 })}
onClick={offset != 0 ? this.handlePrev : null} onClick={offset != 0 ? this.handlePrev : null}
> >
<ChevronLeftIcon className="icon" /> <ChevronLeftIcon className="icon" />
</div> </div>
<div className="btn-spacer" /> <div className="btn-spacer" />
<div <div
className={cn("showing-btn", { "is-disabled": !hasMore })} className={clsx("showing-btn", { "is-disabled": !hasMore })}
onClick={hasMore ? this.handleNext : null} onClick={hasMore ? this.handleNext : null}
> >
<ChevronRightIcon className="icon" /> <ChevronRightIcon className="icon" />
@ -538,7 +538,7 @@ class HistoryView extends React.Component<{}, {}> {
<For index="idx" each="item" of={items}> <For index="idx" each="item" of={items}>
<div <div
key={item.historyid} key={item.historyid}
className={cn("row history-item", { className={clsx("row history-item", {
"is-selected": hvm.selectedItems.get(item.historyid), "is-selected": hvm.selectedItems.get(item.historyid),
})} })}
> >
@ -608,21 +608,21 @@ class HistoryView extends React.Component<{}, {}> {
</div> </div>
<div <div
key="control2" 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="spacer" />
<div className="showing-text"> <div className="showing-text">
Showing {offset + 1}-{offset + items.length} Showing {offset + 1}-{offset + items.length}
</div> </div>
<div <div
className={cn("showing-btn", { "is-disabled": offset == 0 })} className={clsx("showing-btn", { "is-disabled": offset == 0 })}
onClick={offset != 0 ? this.handlePrev : null} onClick={offset != 0 ? this.handlePrev : null}
> >
<ChevronLeftIcon className="icon" /> <ChevronLeftIcon className="icon" />
</div> </div>
<div className="btn-spacer" /> <div className="btn-spacer" />
<div <div
className={cn("showing-btn", { "is-disabled": !hasMore })} className={clsx("showing-btn", { "is-disabled": !hasMore })}
onClick={hasMore ? this.handleNext : null} onClick={hasMore ? this.handleNext : null}
> >
<ChevronRightIcon className="icon" /> <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 { Choose, If, Otherwise, When } from "tsx-control-statements/components";
import { GlobalModel, GlobalCommandRunner, Cmd } from "@/models"; import { GlobalModel, GlobalCommandRunner, Cmd } from "@/models";
import { termHeightFromRows } from "@/util/textmeasure"; import { termHeightFromRows } from "@/util/textmeasure";
import cn from "classnames"; import { clsx } from "clsx";
import { getTermPtyData } from "@/util/modelutil"; import { getTermPtyData } from "@/util/modelutil";
import { renderCmdText } from "@/common/elements"; import { renderCmdText } from "@/common/elements";
@ -172,7 +172,7 @@ class LineActions extends React.Component<{ screen: LineContainerType; line: Lin
<div <div
key="bookmark" key="bookmark"
title="Bookmark" title="Bookmark"
className={cn("line-icon", "line-bookmark")} className={clsx("line-icon", "line-bookmark")}
onClick={this.clickBookmark} onClick={this.clickBookmark}
> >
<i className="fa-sharp fa-regular fa-bookmark fa-fw" /> <i className="fa-sharp fa-regular fa-bookmark fa-fw" />
@ -180,7 +180,7 @@ class LineActions extends React.Component<{ screen: LineContainerType; line: Lin
<div <div
key="minimize" key="minimize"
title={`${isMinimized ? "Show Output" : "Hide Output"}`} title={`${isMinimized ? "Show Output" : "Hide Output"}`}
className={cn("line-icon", isMinimized ? "active" : "")} className={clsx("line-icon", isMinimized ? "active" : "")}
onClick={this.clickMinimize} onClick={this.clickMinimize}
> >
<Choose> <Choose>
@ -220,7 +220,7 @@ class LineActions extends React.Component<{ screen: LineContainerType; line: Lin
<div <div
key="bookmark" key="bookmark"
title="Bookmark" title="Bookmark"
className={cn("line-icon", "line-bookmark")} className={clsx("line-icon", "line-bookmark")}
onClick={this.clickBookmark} onClick={this.clickBookmark}
> >
<i className="fa-sharp fa-regular fa-bookmark fa-fw" /> <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> <React.Fragment>
<div <div
key="meta2" key="meta2"
className={cn( className={clsx(
"meta meta-line2 cmdtext-expanded no-highlight-scrollbar scrollbar-hide-until-hover", "meta meta-line2 cmdtext-expanded no-highlight-scrollbar scrollbar-hide-until-hover",
{ {
"is-multiline": isMultiLine, "is-multiline": isMultiLine,
@ -304,7 +304,7 @@ class LineHeader extends React.Component<{ screen: LineContainerType; line: Line
const { line, cmd } = this.props; const { line, cmd } = this.props;
const hidePrompt = getIsHidePrompt(line); const hidePrompt = getIsHidePrompt(line);
return ( return (
<div key="header" className={cn("line-header", { "hide-prompt": hidePrompt })}> <div key="header" className={clsx("line-header", { "hide-prompt": hidePrompt })}>
{this.renderMeta1(cmd)} {this.renderMeta1(cmd)}
<If condition={!hidePrompt}>{this.renderCmdText(cmd)}</If> <If condition={!hidePrompt}>{this.renderCmdText(cmd)}</If>
</div> </div>
@ -349,7 +349,7 @@ class SmallLineAvatar extends React.Component<{ line: LineType; cmd: Cmd; onRigh
return ( return (
<> <>
<div className="linenum">{lineNumStr}</div> <div className="linenum">{lineNumStr}</div>
<div title={iconTitle} className={cn("status-icon", "status-" + status)}> <div title={iconTitle} className={clsx("status-icon", "status-" + status)}>
{icon} {icon}
</div> </div>
</> </>
@ -567,7 +567,7 @@ class LineCmd extends React.Component<
const { screen, line, width } = this.props; const { screen, line, width } = this.props;
contentHeight = screen.getUsedRows(lineutil.getRendererContext(line), line, cmd, width); 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) { if (DebugHeightProblems && line.linenum >= MinLine && line.linenum <= MaxLine) {
heightLog[line.linenum] = heightLog[line.linenum] || {}; heightLog[line.linenum] = heightLog[line.linenum] || {};
heightLog[line.linenum].contentHeight = contentHeight; heightLog[line.linenum].contentHeight = contentHeight;
@ -582,7 +582,7 @@ class LineCmd extends React.Component<
> >
<LineHeader screen={screen} line={line} cmd={cmd} /> <LineHeader screen={screen} line={line} cmd={cmd} />
<div <div
className={cn("line-content", { "zero-height": contentHeight == 0 })} className={clsx("line-content", { "zero-height": contentHeight == 0 })}
style={{ height: contentHeight }} style={{ height: contentHeight }}
/> />
</div> </div>
@ -801,7 +801,7 @@ class LineCmd extends React.Component<
.get(); .get();
const isRunning = cmd.isRunning(); const isRunning = cmd.isRunning();
const cmdError = cmdShouldMarkError(cmd); const cmdError = cmdShouldMarkError(cmd);
const mainDivCn = cn( const mainDivCn = clsx(
"line", "line",
"line-cmd", "line-cmd",
{ selected: isSelected }, { selected: isSelected },
@ -830,7 +830,7 @@ class LineCmd extends React.Component<
onContextMenu={this.handleContextMenu} onContextMenu={this.handleContextMenu}
> >
<If condition={isSelected || cmdError}> <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> </If>
<LineActions screen={screen} line={line} cmd={cmd} /> <LineActions screen={screen} line={line} cmd={cmd} />
<LineHeader 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", name: "computed-isSelected",
}) })
.get(); .get();
const mainClass = cn("line", "line-text", "focus-parent", { selected: isSelected }); const mainClass = clsx("line", "line-text", "focus-parent", { selected: isSelected });
return ( return (
<div <div
className={mainClass} className={mainClass}

View File

@ -7,7 +7,7 @@ import * as mobx from "mobx";
import { sprintf } from "sprintf-js"; import { sprintf } from "sprintf-js";
import { boundMethod } from "autobind-decorator"; import { boundMethod } from "autobind-decorator";
import { If, For, When, Otherwise, Choose } from "tsx-control-statements/components"; import { If, For, When, Otherwise, Choose } from "tsx-control-statements/components";
import cn from "classnames"; import { clsx } from "clsx";
import dayjs from "dayjs"; import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat"; import localizedFormat from "dayjs/plugin/localizedFormat";
import { debounce, throttle } from "throttle-debounce"; 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}/>; // 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); 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 ( return (
<div key="lines" className={linesClass} onScroll={this.scrollHandler} ref={this.linesRef}> <div key="lines" className={linesClass} onScroll={this.scrollHandler} ref={this.linesRef}>
<div className="lines-spacer"></div> <div className="lines-spacer"></div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@ import * as mobx from "mobx";
import { sprintf } from "sprintf-js"; import { sprintf } from "sprintf-js";
import { boundMethod } from "autobind-decorator"; import { boundMethod } from "autobind-decorator";
import { If } from "tsx-control-statements/components"; import { If } from "tsx-control-statements/components";
import cn from "classnames"; import { clsx } from "clsx";
import { debounce } from "throttle-debounce"; import { debounce } from "throttle-debounce";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { GlobalCommandRunner, ForwardLineContainer, GlobalModel, ScreenLines, Screen, Session } from "@/models"; 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="screen-view" ref={this.screenViewRef}>
<div className="window-view" style={{ width: "100%" }}> <div className="window-view" style={{ width: "100%" }}>
<div key="lines" className="lines"></div> <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"> <div className="flex-centered-column">
<code className="text-standard">[no workspace]</code> <code className="text-standard">[no workspace]</code>
<If condition={sessionCount == 0}> <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="screen-view" ref={this.screenViewRef}>
<div className="window-view" style={{ width: "100%" }}> <div className="window-view" style={{ width: "100%" }}>
<div key="lines" className="lines"></div> <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"> <div className="flex-centered-column">
<code className="text-standard">[no active tab]</code> <code className="text-standard">[no active tab]</code>
<If condition={screens.length == 0}> <If condition={screens.length == 0}>
@ -479,7 +479,7 @@ class ScreenWindowView extends React.Component<ScreenWindowViewProps, {}> {
return ( return (
<div className="window-view" ref={this.windowViewRef} data-screenid={screen.screenId} style={{ width }}> <div className="window-view" ref={this.windowViewRef} data-screenid={screen.screenId} style={{ width }}>
<div key="lines" className="lines"></div> <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 className="text-standard">{message}</div>
</div> </div>
</div> </div>
@ -559,7 +559,7 @@ class ScreenWindowView extends React.Component<ScreenWindowViewProps, {}> {
<If condition={lines.length == 0 && screen.nextLineNum.get() != 1}> <If condition={lines.length == 0 && screen.nextLineNum.get() != 1}>
<div className="window-empty" ref={this.windowViewRef} data-screenid={screen.screenId}> <div className="window-empty" ref={this.windowViewRef} data-screenid={screen.screenId}>
<div key="lines" className="lines"></div> <div key="lines" className="lines"></div>
<div key="window-empty" className={cn("window-empty")}> <div key="window-empty" className={clsx("window-empty")}>
<div> <div>
<code className="text-standard"> <code className="text-standard">
[workspace="{session.name.get()}" tab="{screen.name.get()}"] [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 mobxReact from "mobx-react";
import * as mobx from "mobx"; import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator"; import { boundMethod } from "autobind-decorator";
import cn from "classnames"; import { clsx } from "clsx";
import { GlobalModel, GlobalCommandRunner, Screen } from "@/models"; import { GlobalModel, GlobalCommandRunner, Screen } from "@/models";
import { ActionsIcon, StatusIndicator, CenteredIcon } from "@/common/icons/icons"; import { ActionsIcon, StatusIndicator, CenteredIcon } from "@/common/icons/icons";
import * as constants from "@/app/appconst"; import * as constants from "@/app/appconst";
@ -131,7 +131,7 @@ class ScreenTab extends React.Component<
value={screen} value={screen}
id={"screentab-" + screen.screenId} id={"screentab-" + screen.screenId}
data-screenid={screen.screenId} data-screenid={screen.screenId}
className={cn( className={clsx(
"screen-tab", "screen-tab",
{ "is-active": activeScreenId == screen.screenId, "is-archived": screen.archived.get() }, { "is-active": activeScreenId == screen.screenId, "is-archived": screen.archived.get() },
"color-" + screen.getTabColor() "color-" + screen.getTabColor()

View File

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

View File

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

View File

@ -14,7 +14,7 @@ import * as waveutil from "../util/util";
import { sprintf } from "sprintf-js"; import { sprintf } from "sprintf-js";
import { handleJsonFetchResponse, fireAndForget } from "@/util/util"; import { handleJsonFetchResponse, fireAndForget } from "@/util/util";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { checkKeyPressed, adaptFromElectronKeyEvent, setKeyUtilPlatform } from "@/util/keyutil"; import { adaptFromElectronKeyEvent, setKeyUtilPlatform } from "@/util/keyutil";
import { platform } from "os"; import { platform } from "os";
const WaveAppPathVarName = "WAVETERM_APP_PATH"; const WaveAppPathVarName = "WAVETERM_APP_PATH";
@ -22,7 +22,6 @@ const WaveDevVarName = "WAVETERM_DEV";
const AuthKeyFile = "waveterm.authkey"; const AuthKeyFile = "waveterm.authkey";
const DevServerEndpoint = "http://127.0.0.1:8090"; const DevServerEndpoint = "http://127.0.0.1:8090";
const ProdServerEndpoint = "http://127.0.0.1:1619"; const ProdServerEndpoint = "http://127.0.0.1:1619";
const startTs = Date.now();
const isDev = process.env[WaveDevVarName] != null; const isDev = process.env[WaveDevVarName] != null;
const waveHome = getWaveHomeDir(); const waveHome = getWaveHomeDir();
@ -35,7 +34,6 @@ let wasActive = true;
let wasInFg = true; let wasInFg = true;
let currentGlobalShortcut: string | null = null; let currentGlobalShortcut: string | null = null;
let initialClientData: ClientDataType = null; let initialClientData: ClientDataType = null;
let MainWindow: Electron.BrowserWindow | null = null;
checkPromptMigrate(); checkPromptMigrate();
ensureDir(waveHome); ensureDir(waveHome);
@ -201,14 +199,15 @@ function readAuthKey(): string {
} }
const reloadAcceleratorKey = unamePlatform == "darwin" ? "Option+R" : "Super+R"; const reloadAcceleratorKey = unamePlatform == "darwin" ? "Option+R" : "Super+R";
const cmdOrAlt = process.platform === "darwin" ? "Cmd" : "Alt"; const cmdOrAlt = process.platform === "darwin" ? "Cmd" : "Alt";
let viewSubMenu: Electron.MenuItemConstructorOptions[] = []; let viewSubMenu: Electron.MenuItemConstructorOptions[] = [];
viewSubMenu.push({ role: "reload", accelerator: reloadAcceleratorKey }); viewSubMenu.push({ role: "reload", accelerator: reloadAcceleratorKey });
viewSubMenu.push({ role: "toggleDevTools" }); viewSubMenu.push({ role: "toggleDevTools" });
if (isDev) { if (isDev) {
viewSubMenu.push({ viewSubMenu.push({
label: "Toggle Dev UI", label: "Toggle Dev UI",
click: () => { click: (_, window) => {
MainWindow?.webContents.send("toggle-devui"); window?.webContents.send("toggle-devui");
}, },
}); });
} }
@ -216,36 +215,33 @@ viewSubMenu.push({ type: "separator" });
viewSubMenu.push({ viewSubMenu.push({
label: "Actual Size", label: "Actual Size",
accelerator: cmdOrAlt + "+0", accelerator: cmdOrAlt + "+0",
click: () => { click: (_, window) => {
if (MainWindow == null) { window?.webContents.setZoomFactor(1);
return; window?.webContents.send("zoom-changed");
}
MainWindow.webContents.setZoomFactor(1);
MainWindow.webContents.send("zoom-changed");
}, },
}); });
viewSubMenu.push({ viewSubMenu.push({
label: "Zoom In", label: "Zoom In",
accelerator: cmdOrAlt + "+Plus", accelerator: cmdOrAlt + "+Plus",
click: () => { click: (_, window) => {
if (MainWindow == null) { if (window == null) {
return; return;
} }
const zoomFactor = MainWindow.webContents.getZoomFactor(); const zoomFactor = window.webContents.getZoomFactor();
MainWindow.webContents.setZoomFactor(zoomFactor * 1.1); window.webContents.setZoomFactor(zoomFactor * 1.1);
MainWindow.webContents.send("zoom-changed"); window.webContents.send("zoom-changed");
}, },
}); });
viewSubMenu.push({ viewSubMenu.push({
label: "Zoom Out", label: "Zoom Out",
accelerator: cmdOrAlt + "+-", accelerator: cmdOrAlt + "+-",
click: () => { click: (_, window) => {
if (MainWindow == null) { if (window == null) {
return; return;
} }
const zoomFactor = MainWindow.webContents.getZoomFactor(); const zoomFactor = window.webContents.getZoomFactor();
MainWindow.webContents.setZoomFactor(zoomFactor / 1.1); window.webContents.setZoomFactor(zoomFactor / 1.1);
MainWindow.webContents.send("zoom-changed"); window.webContents.send("zoom-changed");
}, },
}); });
viewSubMenu.push({ type: "separator" }); viewSubMenu.push({ type: "separator" });
@ -256,8 +252,8 @@ const menuTemplate: Electron.MenuItemConstructorOptions[] = [
submenu: [ submenu: [
{ {
label: "About Wave Terminal", label: "About Wave Terminal",
click: () => { click: (_, window) => {
MainWindow?.webContents.send("menu-item-about"); window?.webContents.send("menu-item-about");
}, },
}, },
{ type: "separator" }, { type: "separator" },
@ -326,7 +322,7 @@ function shFrameNavHandler(event: Electron.Event<Electron.WebContentsWillFrameNa
console.log("frame navigation canceled"); console.log("frame navigation canceled");
} }
function createMainWindow(clientData: ClientDataType | null): Electron.BrowserWindow { function createWindow(clientData: ClientDataType | null): Electron.BrowserWindow {
const bounds = calcBounds(clientData); const bounds = calcBounds(clientData);
setKeyUtilPlatform(platform()); setKeyUtilPlatform(platform());
const win = new electron.BrowserWindow({ const win = new electron.BrowserWindow({
@ -374,9 +370,6 @@ function createMainWindow(clientData: ClientDataType | null): Electron.BrowserWi
wasInFg = true; wasInFg = true;
wasActive = true; wasActive = true;
}); });
win.on("close", () => {
MainWindow = null;
});
win.webContents.on("zoom-changed", (e) => { win.webContents.on("zoom-changed", (e) => {
win.webContents.send("zoom-changed"); win.webContents.send("zoom-changed");
}); });
@ -400,7 +393,6 @@ function createMainWindow(clientData: ClientDataType | null): Electron.BrowserWi
console.log("window-open denied", url); console.log("window-open denied", url);
return { action: "deny" }; return { action: "deny" };
}); });
return win; return win;
} }
@ -475,8 +467,9 @@ app.on("window-all-closed", () => {
}); });
electron.ipcMain.on("toggle-developer-tools", (event) => { electron.ipcMain.on("toggle-developer-tools", (event) => {
if (MainWindow != null) { const window = getWindowForEvent(event);
MainWindow.webContents.toggleDevTools(); if (window != null) {
window.webContents.toggleDevTools();
} }
event.returnValue = true; event.returnValue = true;
}); });
@ -488,8 +481,8 @@ function convertMenuDefArrToMenu(menuDefArr: ElectronContextMenuItem[]): electro
role: menuDef.role as any, role: menuDef.role as any,
label: menuDef.label, label: menuDef.label,
type: menuDef.type, type: menuDef.type,
click: () => { click: (_, window) => {
MainWindow?.webContents.send("contextmenu-click", menuDef.id); window?.webContents.send("contextmenu-click", menuDef.id);
}, },
}; };
if (menuDef.submenu != null) { if (menuDef.submenu != null) {
@ -501,6 +494,11 @@ function convertMenuDefArrToMenu(menuDefArr: ElectronContextMenuItem[]): electro
return electron.Menu.buildFromTemplate(menuItems); 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 }) => { electron.ipcMain.on("contextmenu-show", (event, menuDefArr: ElectronContextMenuItem[], { x, y }) => {
if (menuDefArr == null || menuDefArr.length == 0) { if (menuDefArr == null || menuDefArr.length == 0) {
return; return;
@ -511,8 +509,9 @@ electron.ipcMain.on("contextmenu-show", (event, menuDefArr: ElectronContextMenuI
}); });
electron.ipcMain.on("hide-window", (event) => { electron.ipcMain.on("hide-window", (event) => {
if (MainWindow != null) { const window = getWindowForEvent(event);
MainWindow.hide(); if (window) {
window.hide();
} }
event.returnValue = true; event.returnValue = true;
}); });
@ -553,8 +552,9 @@ electron.ipcMain.on("restart-server", (event) => {
}); });
electron.ipcMain.on("reload-window", (event) => { electron.ipcMain.on("reload-window", (event) => {
if (MainWindow != null) { const window = getWindowForEvent(event);
MainWindow.reload(); if (window) {
window.reload();
} }
event.returnValue = true; event.returnValue = true;
}); });
@ -593,9 +593,9 @@ electron.ipcMain.on("set-nativethemesource", (event, themeSource: "system" | "li
}); });
electron.nativeTheme.on("updated", () => { electron.nativeTheme.on("updated", () => {
if (MainWindow != null) { electron.BrowserWindow.getAllWindows().forEach((win) => {
MainWindow.webContents.send("nativetheme-updated"); win.webContents.send("nativetheme-updated");
} });
}); });
function readLastLinesOfFile(filePath: string, lineCount: number) { function readLastLinesOfFile(filePath: string, lineCount: number) {
@ -659,13 +659,13 @@ async function getClientData(willRetry: boolean, retryNum: number): Promise<Clie
} }
function sendWSSC() { function sendWSSC() {
if (MainWindow != null) { electron.BrowserWindow.getAllWindows().forEach((win) => {
if (waveSrvProc == null) { if (waveSrvProc == null) {
MainWindow.webContents.send("wavesrv-status-change", false); win.webContents.send("wavesrv-status-change", false);
return; } else {
win.webContents.send("wavesrv-status-change", true, waveSrvProc.pid);
} }
MainWindow.webContents.send("wavesrv-status-change", true, waveSrvProc.pid); });
}
} }
function runWaveSrv() { function runWaveSrv() {
@ -733,7 +733,7 @@ electron.ipcMain.on("context-editmenu", (_, { x, y }, opts) => {
menu.popup({ x, y }); menu.popup({ x, y });
}); });
async function createMainWindowWrap() { async function createWindowWrap() {
let clientData: ClientDataType | null = null; let clientData: ClientDataType | null = null;
try { try {
clientData = await getClientDataPoll(1); clientData = await getClientDataPoll(1);
@ -741,9 +741,9 @@ async function createMainWindowWrap() {
} catch (e) { } catch (e) {
console.log("error getting wavesrv clientdata", e.toString()); console.log("error getting wavesrv clientdata", e.toString());
} }
MainWindow = createMainWindow(clientData); const win = createWindow(clientData);
if (clientData && clientData.winsize.fullscreen) { if (clientData?.winsize.fullscreen) {
MainWindow.setFullScreen(true); win.setFullScreen(true);
} }
configureAutoUpdaterStartup(clientData); configureAutoUpdaterStartup(clientData);
} }
@ -762,7 +762,7 @@ function logActiveState() {
console.log("error logging active state", err); console.log("error logging active state", err);
}); });
// for next iteration // for next iteration
wasInFg = MainWindow != null && MainWindow.isFocused(); wasInFg = electron.BrowserWindow.getFocusedWindow()?.isFocused() ?? false;
wasActive = false; wasActive = false;
} }
@ -788,9 +788,13 @@ function reregisterGlobalShortcut(shortcut: string) {
currentGlobalShortcut = null; currentGlobalShortcut = null;
return; return;
} }
const ok = electron.globalShortcut.register(shortcut, () => { const ok = electron.globalShortcut.register(shortcut, async () => {
console.log("global shortcut triggered, showing window"); 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"); console.log("registered global shortcut", shortcut, ok ? "ok" : "failed");
if (!ok) { if (!ok) {
@ -802,10 +806,12 @@ function reregisterGlobalShortcut(shortcut: string) {
// ====== AUTO-UPDATER ====== // // ====== AUTO-UPDATER ====== //
let autoUpdateLock = false; let autoUpdateLock = false;
let autoUpdateEnabled = false;
let autoUpdateInterval: NodeJS.Timeout | null = null; let autoUpdateInterval: NodeJS.Timeout | null = null;
let availableUpdateReleaseName: string | null = null; let availableUpdateReleaseName: string | null = null;
let availableUpdateReleaseNotes: string | null = null; let availableUpdateReleaseNotes: string | null = null;
let appUpdateStatus = "unavailable"; let appUpdateStatus = "unavailable";
let lastUpdateCheck: Date = null;
/** /**
* Sets the app update status and sends it to the main window * Sets the app update status and sends it to the main window
@ -813,8 +819,22 @@ let appUpdateStatus = "unavailable";
*/ */
function setAppUpdateStatus(status: string) { function setAppUpdateStatus(status: string) {
appUpdateStatus = status; appUpdateStatus = status;
if (MainWindow != null) { electron.BrowserWindow.getAllWindows().forEach((window) => {
MainWindow.webContents.send("app-update-status", appUpdateStatus); 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.", body: "A new version of Wave Terminal is ready to install.",
}); });
updateNotification.on("click", () => { updateNotification.on("click", () => {
fireAndForget(installAppUpdate); fireAndForget(() => installAppUpdate());
}); });
updateNotification.show(); updateNotification.show();
}); });
// check for updates right away and keep checking later // check for updates right away and keep checking later
autoUpdater.checkForUpdates(); checkForUpdates();
return setInterval(() => fireAndForget(autoUpdater.checkForUpdates), 3600000); // 1 hour in ms 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.", detail: "A new version has been downloaded. Restart the application to apply the updates.",
}; };
await electron.dialog.showMessageBox(MainWindow, dialogOpts).then(({ response }) => { const allWindows = electron.BrowserWindow.getAllWindows();
if (response === 0) autoUpdater.quitAndInstall(); 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) => { electron.ipcMain.on("get-app-update-status", (event) => {
event.returnValue = appUpdateStatus; event.returnValue = appUpdateStatus;
}); });
@ -919,15 +946,22 @@ function configureAutoUpdater(enabled: boolean) {
console.log("auto-update configuration already in progress, skipping"); console.log("auto-update configuration already in progress, skipping");
return; return;
} }
autoUpdateEnabled = enabled;
autoUpdateLock = true; autoUpdateLock = true;
if (enabled && autoUpdateInterval == null) { if (autoUpdateEnabled && autoUpdateInterval == null) {
lastUpdateCheck = null;
try { try {
console.log("configuring auto updater"); console.log("configuring auto updater");
autoUpdateInterval = initUpdater(); autoUpdateInterval = initUpdater();
} catch (e) { } catch (e) {
console.log("error configuring auto updater", e.toString()); console.log("error configuring auto updater", e.toString());
} }
} else if (!autoUpdateEnabled && autoUpdateInterval != null) {
console.log("disabling auto updater");
clearInterval(autoUpdateInterval);
autoUpdateInterval = null;
} }
autoUpdateLock = false; autoUpdateLock = false;
} }
@ -950,11 +984,13 @@ function configureAutoUpdater(enabled: boolean) {
} }
setTimeout(runActiveTimer, 5000); // start active timer, wait 5s just to be safe setTimeout(runActiveTimer, 5000); // start active timer, wait 5s just to be safe
await app.whenReady(); await app.whenReady();
await createMainWindowWrap(); await createWindowWrap();
app.on("activate", () => { app.on("activate", () => {
if (electron.BrowserWindow.getAllWindows().length === 0) { 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 { boundMethod } from "autobind-decorator";
import Editor, { Monaco } from "@monaco-editor/react"; import Editor, { Monaco } from "@monaco-editor/react";
import type * as MonacoTypes from "monaco-editor/esm/vs/editor/editor.api"; 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 { If } from "tsx-control-statements/components";
import { Markdown, Button } from "@/elements"; import { Markdown, Button } from "@/elements";
import { GlobalModel, GlobalCommandRunner } from "@/models"; import { GlobalModel, GlobalCommandRunner } from "@/models";
@ -579,7 +579,7 @@ class SourceCodeRenderer extends React.Component<
<div className="flex-spacer" /> <div className="flex-spacer" />
<div className="code-statusbar"> <div className="code-statusbar">
<If condition={message != null}> <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} {this.state.message.text}
</div> </div>
</If> </If>

View File

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

View File

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

View File

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

View File

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

View File

@ -28,7 +28,7 @@ func readFullRunPacket(packetParser *packet.PacketParser) (*packet.RunPacketType
return runPacket, nil return runPacket, nil
} }
if !ok { 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") return nil, fmt.Errorf("no run packet received")
@ -97,7 +97,7 @@ func handleSingle() {
func handleUsage() { func handleUsage() {
usage := ` 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: Options:
--help - prints this message --help - prints this message
@ -106,7 +106,7 @@ Options:
--single - run a single command (connected to multiplexer) --single - run a single command (connected to multiplexer)
--single --version - return an init packet with version info --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 it communicates exclusively through stdin/stdout with an attached process
via a JSON packet format. via a JSON packet format.
` `
@ -124,7 +124,7 @@ func main() {
handleUsage() handleUsage()
return return
} else if firstArg == "--version" { } 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 return
} else if firstArg == "--single" || firstArg == "--single-from-server" { } else if firstArg == "--single" || firstArg == "--single-from-server" {
base.ProcessType = base.ProcessType_WaveShellSingle base.ProcessType = base.ProcessType_WaveShellSingle

View File

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

View File

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

View File

@ -455,7 +455,7 @@ func (m *MServer) writeFile(pk *packet.WriteFilePacketType, wfc *WriteFileContex
} }
var writeFd *os.File var writeFd *os.File
if pk.UseTemp { 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 { if err != nil {
resp := packet.MakeWriteFileReadyPacket(pk.ReqId) resp := packet.MakeWriteFileReadyPacket(pk.ReqId)
resp.Error = fmt.Sprintf("cannot create temp file: %v", err) 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")) m.Sender.SendErrorResponse(runPacket.ReqId, fmt.Errorf("test error"))
return return
} }
ecmd, err := shexec.MakeMShellSingleCmd() ecmd, err := shexec.MakeWaveshellSingleCmd()
if err != nil { if err != nil {
m.Sender.SendErrorResponse(runPacket.ReqId, fmt.Errorf("server run packets require valid ck: %s", err)) m.Sender.SendErrorResponse(runPacket.ReqId, fmt.Errorf("server run packets require valid ck: %s", err))
return return
} }
cproc, err := shexec.MakeClientProc(context.Background(), shexec.CmdWrap{Cmd: ecmd}) cproc, err := shexec.MakeClientProc(context.Background(), shexec.CmdWrap{Cmd: ecmd})
if err != nil { 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 return
} }
m.Lock.Lock() m.Lock.Lock()
@ -833,7 +833,7 @@ func (server *MServer) runReadLoop() {
} }
continue 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 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) { func StreamCommandWithExtraFd(ctx context.Context, ecmd *exec.Cmd, outputCh chan []byte, extraFdNum int, endBytes []byte, stdinDataCh chan []byte) ([]byte, error) {
defer close(outputCh) defer close(outputCh)
ecmd.Env = os.Environ() ecmd.Env = os.Environ()
shellutil.UpdateCmdEnv(ecmd, shellutil.MShellEnvVars(shellutil.DefaultTermType)) shellutil.UpdateCmdEnv(ecmd, shellutil.WaveshellEnvVars(shellutil.DefaultTermType))
cmdPty, cmdTty, err := pty.Open() cmdPty, cmdTty, err := pty.Open()
if err != nil { if err != nil {
return nil, fmt.Errorf("opening new pty: %w", err) 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) { func RunSimpleCmdInPty(ecmd *exec.Cmd, endBytes []byte) ([]byte, error) {
ecmd.Env = os.Environ() ecmd.Env = os.Environ()
shellutil.UpdateCmdEnv(ecmd, shellutil.MShellEnvVars(shellutil.DefaultTermType)) shellutil.UpdateCmdEnv(ecmd, shellutil.WaveshellEnvVars(shellutil.DefaultTermType))
cmdPty, cmdTty, err := pty.Open() cmdPty, cmdTty, err := pty.Open()
if err != nil { if err != nil {
return nil, fmt.Errorf("opening new pty: %w", err) 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) // for debugging (not for production use)
func writeStateToFile(shellType string, outputBytes []byte) error { func writeStateToFile(shellType string, outputBytes []byte) error {
msHome := base.GetMShellHomeDir() wsHome := base.GetWaveshellHomeDir()
stateFileName := path.Join(msHome, shellType+"-state.txt") stateFileName := path.Join(wsHome, shellType+"-state.txt")
os.WriteFile(stateFileName, outputBytes, 0644) os.WriteFile(stateFileName, outputBytes, 0644)
return nil return nil
} }

View File

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

View File

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

View File

@ -170,8 +170,8 @@ type WaveshellLaunchError struct {
func (wle WaveshellLaunchError) Error() string { func (wle WaveshellLaunchError) Error() string {
if wle.InitPk.NotFound { if wle.InitPk.NotFound {
return "waveshell client not found" return "waveshell client not found"
} else if semver.MajorMinor(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.MShellVersion)) 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) 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() cproc.Close()
return nil, WaveshellLaunchError{InitPk: initPk} 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() cproc.Close()
return nil, WaveshellLaunchError{InitPk: initPk} return nil, WaveshellLaunchError{InitPk: initPk}
} }

View File

@ -72,7 +72,7 @@ fi
` `
func MakeClientCommandStr() string { func MakeClientCommandStr() string {
return strings.ReplaceAll(ClientCommandFmt, "[%VERSION%]", semver.MajorMinor(base.MShellVersion)) return strings.ReplaceAll(ClientCommandFmt, "[%VERSION%]", semver.MajorMinor(base.WaveshellVersion))
} }
const InstallCommandFmt = ` const InstallCommandFmt = `
@ -88,10 +88,10 @@ fi
` `
func MakeInstallCommandStr() string { 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 { type ReturnStateBuf struct {
Lock *sync.Mutex Lock *sync.Mutex
@ -277,7 +277,7 @@ func (c *ShExecType) MakeCmdStartPacket(reqId string) *packet.CmdStartPacketType
startPacket.Ts = time.Now().UnixMilli() startPacket.Ts = time.Now().UnixMilli()
startPacket.CK = c.CK startPacket.CK = c.CK
startPacket.Pid = c.Cmd.Process.Pid startPacket.Pid = c.Cmd.Process.Pid
startPacket.MShellPid = os.Getpid() startPacket.WaveshellPid = os.Getpid()
return startPacket return startPacket
} }
@ -295,7 +295,7 @@ func MakeSimpleStaticWriterPipe(data []byte) (*os.File, error) {
} }
func MakeRunnerExec(ck base.CommandKey) (*exec.Cmd, error) { func MakeRunnerExec(ck base.CommandKey) (*exec.Cmd, error) {
msPath, err := base.GetMShellPath() msPath, err := base.GetWaveshellPath()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -317,7 +317,7 @@ func MakeDetachedExecCmd(pk *packet.RunPacketType, cmdTty *os.File) (*exec.Cmd,
ecmd.Env = os.Environ() ecmd.Env = os.Environ()
} }
shellutil.UpdateCmdEnv(ecmd, shellenv.EnvMapFromState(state)) shellutil.UpdateCmdEnv(ecmd, shellenv.EnvMapFromState(state))
shellutil.UpdateCmdEnv(ecmd, shellutil.MShellEnvVars(getTermType(pk))) shellutil.UpdateCmdEnv(ecmd, shellutil.WaveshellEnvVars(getTermType(pk)))
if state.Cwd != "" { if state.Cwd != "" {
ecmd.Dir = base.ExpandHomeDir(state.Cwd) ecmd.Dir = base.ExpandHomeDir(state.Cwd)
} }
@ -470,10 +470,10 @@ type ClientOpts struct {
UsePty bool UsePty bool
} }
func MakeMShellSingleCmd() (*exec.Cmd, error) { func MakeWaveshellSingleCmd() (*exec.Cmd, error) {
execFile, err := os.Executable() execFile, err := os.Executable()
if err != nil { 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") ecmd := exec.Command(execFile, "--single-from-server")
return ecmd, nil 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) { func GetTerminalSize() (int, int, error) {
fd, err := os.Open("/dev/tty") fd, err := os.Open("/dev/tty")
if err != nil { if err != nil {
@ -610,19 +585,19 @@ func ValidateRemoteFds(rfds []packet.RemoteFd) error {
dupMap := make(map[int]bool) dupMap := make(map[int]bool)
for _, rfd := range rfds { for _, rfd := range rfds {
if rfd.FdNum < 0 { 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 { 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 { 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] { 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 { 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 { if !rfd.Read && !rfd.Write {
return fmt.Errorf("invalid fd=%d, neither reading or writing mode specified", rfd.FdNum) 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 return nil
} }
func sendMShellBinary(input io.WriteCloser, mshellStream io.Reader) { func sendWaveshellBinary(input io.WriteCloser, waveshellStream io.Reader) {
go func() { go func() {
defer input.Close() 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() inputWriter, err := ecmd.StdinPipe()
if err != nil { if err != nil {
return fmt.Errorf("creating stdin pipe: %v", err) return fmt.Errorf("creating stdin pipe: %v", err)
@ -655,8 +630,8 @@ func RunInstallFromCmd(ctx context.Context, ecmd ConnInterface, tryDetect bool,
go func() { go func() {
io.Copy(os.Stderr, stderrReader) io.Copy(os.Stderr, stderrReader)
}() }()
if mshellStream != nil { if waveshellStream != nil {
sendMShellBinary(inputWriter, mshellStream) sendWaveshellBinary(inputWriter, waveshellStream)
} }
packetParser := packet.MakePacketParser(stdoutReader, nil) packetParser := packet.MakePacketParser(stdoutReader, nil)
err = ecmd.Start() err = ecmd.Start()
@ -686,24 +661,24 @@ func RunInstallFromCmd(ctx context.Context, ecmd ConnInterface, tryDetect bool,
} }
goos, goarch, err := DetectGoArch(initPacket.UName) goos, goarch, err := DetectGoArch(initPacket.UName)
if err != nil { 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) msgFn(msgStr)
detectedMSS, err := mshellReaderFn(base.MShellVersion, goos, goarch) detectedMSS, err := waveshellReaderFn(base.WaveshellVersion, goos, goarch)
if err != nil { if err != nil {
return err return err
} }
defer detectedMSS.Close() defer detectedMSS.Close()
sendMShellBinary(inputWriter, detectedMSS) sendWaveshellBinary(inputWriter, detectedMSS)
continue continue
} }
if pk.GetType() == packet.InitPacketStr && !firstInit { if pk.GetType() == packet.InitPacketStr && !firstInit {
initPacket := pk.(*packet.InitPacketType) initPacket := pk.(*packet.InitPacketType)
if initPacket.Version == base.MShellVersion { if initPacket.Version == base.WaveshellVersion {
return nil 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 { if pk.GetType() == packet.RawPacketStr {
rawPk := pk.(*packet.RawPacketType) rawPk := pk.(*packet.RawPacketType)
@ -770,7 +745,7 @@ func DetectGoArch(uname string) (string, string, error) {
osVal := strings.TrimSpace(strings.ToLower(fields[0])) osVal := strings.TrimSpace(strings.ToLower(fields[0]))
archVal := strings.TrimSpace(strings.ToLower(fields[1])) archVal := strings.TrimSpace(strings.ToLower(fields[1]))
if osVal != "darwin" && osVal != "linux" { 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 goos := osVal
goarch := "" goarch := ""
@ -780,7 +755,7 @@ func DetectGoArch(uname string) (string, string, error) {
goarch = "arm64" goarch = "arm64"
} }
if goarch == "" { 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) { if !base.ValidGoArch(goos, goarch) {
return "", "", fmt.Errorf("invalid arch detected %s.%s", 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() cmdTty.Close()
}() }()
cmd.CmdPty = cmdPty cmd.CmdPty = cmdPty
shellutil.UpdateCmdEnv(cmd.Cmd, shellutil.MShellEnvVars(getTermType(pk))) shellutil.UpdateCmdEnv(cmd.Cmd, shellutil.WaveshellEnvVars(getTermType(pk)))
} }
if cmdTty != nil { if cmdTty != nil {
cmd.Cmd.Stdin = cmdTty 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 // in detached run mode, we don't want waveshell to die from signals since
// since we want mshell to persist even if the mshell --server is terminated // we want waveshell to persist even if the waveshell --server is terminated
func SetupSignalsForDetach() { func SetupSignalsForDetach() {
sigCh := make(chan os.Signal, 1) sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGPIPE) 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 // in detached run mode, we don't want waveshell to die from signals since
// since we want mshell to persist even if the mshell --server is terminated // we want waveshell to persist even if the waveshell --server is terminated
func IgnoreSigPipe() { func IgnoreSigPipe() {
sigCh := make(chan os.Signal, 1) sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGPIPE) signal.Notify(sigCh, syscall.SIGPIPE)
@ -1241,10 +1216,10 @@ func (c *ShExecType) WaitForCommand() *packet.CmdDonePacketType {
func MakeInitPacket() *packet.InitPacketType { func MakeInitPacket() *packet.InitPacketType {
initPacket := packet.MakeInitPacket() initPacket := packet.MakeInitPacket()
initPacket.Version = base.MShellVersion initPacket.Version = base.WaveshellVersion
initPacket.BuildTime = base.BuildTime initPacket.BuildTime = base.BuildTime
initPacket.HomeDir = base.GetHomeDir() initPacket.HomeDir = base.GetHomeDir()
initPacket.MShellHomeDir = base.GetMShellHomeDir() initPacket.WaveshellHomeDir = base.GetWaveshellHomeDir()
if user, _ := user.Current(); user != nil { if user, _ := user.Current(); user != nil {
initPacket.User = user.Username 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")) WriteJsonError(w, fmt.Errorf("invalid line, no remote"))
return return
} }
msh := remote.GetRemoteById(cmd.Remote.RemoteId) wsh := remote.GetRemoteById(cmd.Remote.RemoteId)
if msh == nil { if wsh == nil {
WriteJsonError(w, fmt.Errorf("invalid line, cannot resolve remote")) WriteJsonError(w, fmt.Errorf("invalid line, cannot resolve remote"))
return return
} }
rrState := msh.GetRemoteRuntimeState() rrState := wsh.GetRemoteRuntimeState()
fullPath, err := rrState.ExpandHomeDir(params.Path) fullPath, err := rrState.ExpandHomeDir(params.Path)
if err != nil { if err != nil {
WriteJsonError(w, fmt.Errorf("error expanding homedir: %v", err)) WriteJsonError(w, fmt.Errorf("error expanding homedir: %v", err))
@ -472,7 +472,7 @@ func HandleWriteFile(w http.ResponseWriter, r *http.Request) {
} else { } else {
writePk.Path = filepath.Join(cwd, fullPath) writePk.Path = filepath.Join(cwd, fullPath)
} }
iter, err := msh.PacketRpcIter(r.Context(), writePk) iter, err := wsh.PacketRpcIter(r.Context(), writePk)
if err != nil { if err != nil {
WriteJsonError(w, fmt.Errorf("error: %v", err)) WriteJsonError(w, fmt.Errorf("error: %v", err))
return return
@ -502,7 +502,7 @@ func HandleWriteFile(w http.ResponseWriter, r *http.Request) {
} else if err != nil { } else if err != nil {
dataErr := fmt.Errorf("error reading file data: %v", err) dataErr := fmt.Errorf("error reading file data: %v", err)
dataPk.Error = dataErr.Error() dataPk.Error = dataErr.Error()
msh.SendFileData(dataPk) wsh.SendFileData(dataPk)
WriteJsonError(w, dataErr) WriteJsonError(w, dataErr)
return return
} }
@ -510,7 +510,7 @@ func HandleWriteFile(w http.ResponseWriter, r *http.Request) {
dataPk.Data = make([]byte, nr) dataPk.Data = make([]byte, nr)
copy(dataPk.Data, bufSlice[0:nr]) copy(dataPk.Data, bufSlice[0:nr])
} }
msh.SendFileData(dataPk) wsh.SendFileData(dataPk)
if dataPk.Eof { if dataPk.Eof {
break break
} }
@ -581,13 +581,13 @@ func HandleReadFile(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("invalid line, no remote")) w.Write([]byte("invalid line, no remote"))
return return
} }
msh := remote.GetRemoteById(cmd.Remote.RemoteId) wsh := remote.GetRemoteById(cmd.Remote.RemoteId)
if msh == nil { if wsh == nil {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("invalid line, cannot resolve remote")) w.Write([]byte("invalid line, cannot resolve remote"))
return return
} }
rrState := msh.GetRemoteRuntimeState() rrState := wsh.GetRemoteRuntimeState()
fullPath, err := rrState.ExpandHomeDir(path) fullPath, err := rrState.ExpandHomeDir(path)
if err != nil { if err != nil {
WriteJsonError(w, fmt.Errorf("error expanding homedir: %v", err)) WriteJsonError(w, fmt.Errorf("error expanding homedir: %v", err))
@ -601,7 +601,7 @@ func HandleReadFile(w http.ResponseWriter, r *http.Request) {
} else { } else {
streamPk.Path = filepath.Join(cwd, fullPath) streamPk.Path = filepath.Join(cwd, fullPath)
} }
iter, err := msh.StreamFile(r.Context(), streamPk) iter, err := wsh.StreamFile(r.Context(), streamPk)
if err != nil { if err != nil {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(fmt.Sprintf("error trying to stream file: %v", err))) 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)) WriteJsonError(w, fmt.Errorf(ErrorDecodingJson, err))
return return
} }
log.Printf("Running ephemeral command: %v\n", commandPk)
if commandPk.EphemeralOpts == nil { if commandPk.EphemeralOpts == nil {
commandPk.EphemeralOpts = &ephemeral.EphemeralRunOpts{} commandPk.EphemeralOpts = &ephemeral.EphemeralRunOpts{}
@ -1129,6 +1128,11 @@ func main() {
log.Printf("[error] migrate up: %v\n", err) log.Printf("[error] migrate up: %v\n", err)
return return
} }
// err = blockstore.MigrateBlockstore()
// if err != nil {
// log.Printf("[error] migrate blockstore: %v\n", err)
// return
// }
clientData, err := sstore.EnsureClientData(context.Background()) clientData, err := sstore.EnsureClientData(context.Background())
if err != nil { if err != nil {
log.Printf("[error] ensuring client data: %v\n", err) 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 //go:embed migrations/*.sql
var MigrationFS embed.FS 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 toolchain go1.22.0
require ( require (
github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9
github.com/alessio/shellescape v1.4.1 github.com/alessio/shellescape v1.4.1
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2
github.com/creack/pty v1.1.18 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 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0=
github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 h1:7Ip0wMmLHLRJdrloDxZfhMm0xrLXZS8+COSu2bXmEQs= 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/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/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.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 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/wavetermdev/ssh_config v0.0.0-20240306041034-17e2087ebde2 h1:onqZrJVap1sm15AiIGTfWzdr6cEF0KdtddeuuOVhzyY= 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 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= 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= 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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
mvdan.cc/sh/v3 v3.7.0 h1:lSTjdP/1xsddtaKfGg7Myu7DnlHItd3/M2tomOcNNBg= mvdan.cc/sh/v3 v3.7.0 h1:lSTjdP/1xsddtaKfGg7Myu7DnlHItd3/M2tomOcNNBg=

View File

@ -10,8 +10,6 @@ import (
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/alecthomas/units"
) )
type FileOptsType struct { type FileOptsType struct {
@ -32,7 +30,11 @@ type FileInfo struct {
Meta FileMeta 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 const DefaultFlushTimeout = 1 * time.Second
type CacheEntry struct { type CacheEntry struct {
@ -79,16 +81,23 @@ type BlockStore interface {
GetAllBlockIds(ctx context.Context) []string 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 globalLock *sync.Mutex = &sync.Mutex{}
var appendLock *sync.Mutex = &sync.Mutex{} var appendLock *sync.Mutex = &sync.Mutex{}
var flushTimeout = DefaultFlushTimeout var flushTimeout = DefaultFlushTimeout
var lastWriteTime time.Time 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 { func InsertFileIntoDB(ctx context.Context, fileInfo FileInfo) error {
metaJson, err := json.Marshal(fileInfo.Meta) metaJson, err := json.Marshal(fileInfo.Meta)
if err != nil { 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 { txErr := WithTx(ctx, func(tx *TxWrap) error {
query := `INSERT INTO block_file VALUES (?, ?, ?, ?, ?, ?, ?, ?)` query := `INSERT INTO block_file VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
@ -96,7 +105,7 @@ func InsertFileIntoDB(ctx context.Context, fileInfo FileInfo) error {
return nil return nil
}) })
if txErr != 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 return nil
} }
@ -104,7 +113,7 @@ func InsertFileIntoDB(ctx context.Context, fileInfo FileInfo) error {
func WriteFileToDB(ctx context.Context, fileInfo FileInfo) error { func WriteFileToDB(ctx context.Context, fileInfo FileInfo) error {
metaJson, err := json.Marshal(fileInfo.Meta) metaJson, err := json.Marshal(fileInfo.Meta)
if err != nil { 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 { txErr := WithTx(ctx, func(tx *TxWrap) error {
query := `UPDATE block_file SET blockid = ?, name = ?, maxsize = ?, circular = ?, size = ?, createdts = ?, modts = ?, meta = ? where blockid = ? and name = ?` 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 return nil
}) })
if txErr != 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 return nil
@ -125,7 +134,7 @@ func WriteDataBlockToDB(ctx context.Context, blockId string, name string, index
return nil return nil
}) })
if txErr != 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 return nil
} }
@ -152,7 +161,7 @@ func WriteToCacheBlockNum(ctx context.Context, blockId string, name string, p []
defer cacheEntry.Lock.Unlock() defer cacheEntry.Lock.Unlock()
block, err := GetCacheBlock(ctx, blockId, name, cacheNum, pullFromDB) block, err := GetCacheBlock(ctx, blockId, name, cacheNum, pullFromDB)
if err != nil { 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 var bytesWritten = 0
blockLen := len(block.data) blockLen := len(block.data)
@ -192,7 +201,7 @@ func ReadFromCacheBlock(ctx context.Context, blockId string, name string, block
} }
}() }()
if pos > len(block.data) { 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 bytesWritten := 0
index := pos index := pos
@ -216,7 +225,7 @@ func ReadFromCacheBlock(ctx context.Context, blockId string, name string, block
return bytesWritten, nil 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) { func WriteToCacheBuf(buf *[]byte, p []byte, pos int, length int, maxWrite int64) (int, error) {
bytesToWrite := length 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) { func GetCacheEntry(ctx context.Context, blockId string, name string) (*CacheEntry, bool) {
globalLock.Lock() globalLock.Lock()
defer globalLock.Unlock() defer globalLock.Unlock()
if curCacheEntry, found := cache[GetCacheId(blockId, name)]; found { if curCacheEntry, found := blockstoreCache[GetCacheId(blockId, name)]; found {
return curCacheEntry, true return curCacheEntry, true
} else { } else {
return nil, false return nil, false
@ -279,7 +288,7 @@ func GetCacheEntryOrPopulate(ctx context.Context, blockId string, name string) (
if cacheEntry, found := GetCacheEntry(ctx, blockId, name); found { if cacheEntry, found := GetCacheEntry(ctx, blockId, name); found {
return cacheEntry, nil return cacheEntry, nil
} else { } 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) { func SetCacheEntry(ctx context.Context, cacheId string, cacheEntry *CacheEntry) {
globalLock.Lock() globalLock.Lock()
defer globalLock.Unlock() defer globalLock.Unlock()
if _, found := cache[cacheId]; found { if _, found := blockstoreCache[cacheId]; found {
return return
} }
cache[cacheId] = cacheEntry blockstoreCache[cacheId] = cacheEntry
} }
func DeleteCacheEntry(ctx context.Context, blockId string, name string) { func DeleteCacheEntry(ctx context.Context, blockId string, name string) {
globalLock.Lock() globalLock.Lock()
defer globalLock.Unlock() 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) { 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) fInfo, err := Stat(ctx, blockId, name)
if err != nil { 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 { if off > fInfo.Opts.MaxSize && fInfo.Opts.Circular {
numOver := off / fInfo.Opts.MaxSize 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) b, err := WriteAtHelper(ctx, blockId, name, p, 0, false)
bytesWritten += b bytesWritten += b
if err != nil { if err != nil {
return bytesWritten, fmt.Errorf("Write to cache error: %v", err) return bytesWritten, fmt.Errorf("write to cache error: %v", err)
} }
break break
} }
} else { } 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 { if len(p) == b {
@ -452,7 +461,7 @@ func GetAllBlockSizes(dataBlocks []*CacheBlock) (int, int) {
} }
func FlushCache(ctx context.Context) error { func FlushCache(ctx context.Context) error {
for _, cacheEntry := range cache { for _, cacheEntry := range blockstoreCache {
err := WriteFileToDB(ctx, *cacheEntry.Info) err := WriteFileToDB(ctx, *cacheEntry.Info)
if err != nil { if err != nil {
return err return err
@ -485,14 +494,14 @@ func ReadAt(ctx context.Context, blockId string, name string, p *[]byte, off int
bytesRead := 0 bytesRead := 0
fInfo, err := Stat(ctx, blockId, name) fInfo, err := Stat(ctx, blockId, name)
if err != nil { 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 { if off > fInfo.Opts.MaxSize && fInfo.Opts.Circular {
numOver := off / fInfo.Opts.MaxSize numOver := off / fInfo.Opts.MaxSize
off = off - (numOver * fInfo.Opts.MaxSize) off = off - (numOver * fInfo.Opts.MaxSize)
} }
if off > fInfo.Size { 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)) endReadPos := math.Min(float64(int64(len(*p))+off), float64(fInfo.Size))
bytesToRead := int64(endReadPos) - off 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++ { for index := curCacheNum; index < curCacheNum+numCaches; index++ {
curCacheBlock, err := GetCacheBlock(ctx, blockId, name, index, true) curCacheBlock, err := GetCacheBlock(ctx, blockId, name, index, true)
if err != nil { 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) cacheOffset := off - (int64(index) * MaxBlockSize)
if cacheOffset < 0 { if cacheOffset < 0 {
@ -540,7 +549,7 @@ func ReadAt(ctx context.Context, blockId string, name string, p *[]byte, off int
break break
} }
} else { } 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() defer appendLock.Unlock()
fInfo, err := Stat(ctx, blockId, name) fInfo, err := Stat(ctx, blockId, name)
if err != nil { 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) 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 { func DeleteBlock(ctx context.Context, blockId string) error {
for cacheId, _ := range cache { for cacheId := range blockstoreCache {
curBlockId, name := GetValuesFromCacheId(cacheId) curBlockId, name := GetValuesFromCacheId(cacheId)
if curBlockId == blockId { if curBlockId == blockId {
err := DeleteFile(ctx, blockId, name) err := DeleteFile(ctx, blockId, name)
if err != nil { 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" "path"
"sync" "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/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
"github.com/sawka/txwrap" "github.com/sawka/txwrap"
"github.com/wavetermdev/waveterm/wavesrv/pkg/dbutil" "github.com/wavetermdev/waveterm/wavesrv/pkg/dbutil"
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbase" "github.com/wavetermdev/waveterm/wavesrv/pkg/scbase"
dbfs "github.com/wavetermdev/waveterm/wavesrv/db"
) )
const DBFileName = "blockstore.db" const DBFileName = "blockstore.db"
@ -21,12 +26,64 @@ type SingleConnDBGetter struct {
SingleConnLock *sync.Mutex SingleConnLock *sync.Mutex
} }
var dbWrap *SingleConnDBGetter var dbWrap *SingleConnDBGetter = &SingleConnDBGetter{SingleConnLock: &sync.Mutex{}}
type TxWrap = txwrap.TxWrap type TxWrap = txwrap.TxWrap
func InitDBState() { func MakeBlockstoreMigrate() (*migrate.Migrate, error) {
dbWrap = &SingleConnDBGetter{SingleConnLock: &sync.Mutex{}} 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) { 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 globalDBLock = &sync.Mutex{}
var globalDB *sqlx.DB var globalDB *sqlx.DB
var globalDBErr error var globalDBErr error
var overrideDBName string
func GetDBName() string { func GetDBName() string {
if overrideDBName != "" {
return overrideDBName
}
scHome := scbase.GetWaveHomeDir() scHome := scbase.GetWaveHomeDir()
return path.Join(scHome, DBFileName) return path.Join(scHome, DBFileName)
} }

View File

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

View File

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

View File

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

View File

@ -65,8 +65,8 @@ func doCompGen(ctx context.Context, prefix string, compType string, compCtx Comp
if !packet.IsValidCompGenType(compType) { if !packet.IsValidCompGenType(compType) {
return nil, fmt.Errorf("/_compgen invalid type '%s'", compType) return nil, fmt.Errorf("/_compgen invalid type '%s'", compType)
} }
msh := remote.GetRemoteById(compCtx.RemotePtr.RemoteId) wsh := remote.GetRemoteById(compCtx.RemotePtr.RemoteId)
if msh == nil { if wsh == nil {
return nil, fmt.Errorf("invalid remote '%s', not found", compCtx.RemotePtr) return nil, fmt.Errorf("invalid remote '%s', not found", compCtx.RemotePtr)
} }
cgPacket := packet.MakeCompGenPacket() cgPacket := packet.MakeCompGenPacket()
@ -74,7 +74,7 @@ func doCompGen(ctx context.Context, prefix string, compType string, compCtx Comp
cgPacket.CompType = compType cgPacket.CompType = compType
cgPacket.Prefix = prefix cgPacket.Prefix = prefix
cgPacket.Cwd = compCtx.Cwd cgPacket.Cwd = compCtx.Cwd
resp, err := msh.PacketRpc(ctx, cgPacket) resp, err := wsh.PacketRpc(ctx, cgPacket)
if err != nil { if err != nil {
return nil, err 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 WaveDevDirName = ".waveterm-dev" // must match emain.ts
const WaveAppPathVarName = "WAVETERM_APP_PATH" const WaveAppPathVarName = "WAVETERM_APP_PATH"
const WaveAuthKeyFileName = "waveterm.authkey" 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) // initialized by InitialzeWaveAuthKey (called by main-server)
var WaveAuthKey string var WaveAuthKey string
@ -73,7 +73,7 @@ func GetWaveHomeDir() string {
return scHome return scHome
} }
func MShellBinaryDir() string { func WaveshellBinaryDir() string {
appPath := os.Getenv(WaveAppPathVarName) appPath := os.Getenv(WaveAppPathVarName)
if appPath == "" { if appPath == "" {
appPath = "." appPath = "."
@ -81,32 +81,32 @@ func MShellBinaryDir() string {
return filepath.Join(appPath, "bin", "mshell") 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) { if !base.ValidGoArch(goos, goarch) {
return "", fmt.Errorf("invalid goos/goarch combination: %s/%s", goos, goarch) return "", fmt.Errorf("invalid goos/goarch combination: %s/%s", goos, goarch)
} }
binaryDir := MShellBinaryDir() binaryDir := WaveshellBinaryDir()
versionStr := semver.MajorMinor(version) versionStr := semver.MajorMinor(version)
if versionStr == "" { 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) fileName := fmt.Sprintf("mshell-%s-%s.%s", versionStr, goos, goarch)
fullFileName := filepath.Join(binaryDir, fileName) fullFileName := filepath.Join(binaryDir, fileName)
return fullFileName, nil return fullFileName, nil
} }
func LocalMShellBinaryPath() (string, error) { func LocalWaveshellBinaryPath() (string, error) {
return MShellBinaryPath(MShellVersion, runtime.GOOS, runtime.GOARCH) return WaveshellBinaryPath(WaveshellVersion, runtime.GOOS, runtime.GOARCH)
} }
func MShellBinaryReader(version string, goos string, goarch string) (io.ReadCloser, error) { func WaveshellBinaryReader(version string, goos string, goarch string) (io.ReadCloser, error) {
mshellPath, err := MShellBinaryPath(version, goos, goarch) waveshellPath, err := WaveshellBinaryPath(version, goos, goarch)
if err != nil { if err != nil {
return nil, err return nil, err
} }
fd, err := os.Open(mshellPath) fd, err := os.Open(waveshellPath)
if err != nil { 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 return fd, nil
} }

View File

@ -326,9 +326,9 @@ func sendCmdInput(pk *scpacket.FeInputPacketType) error {
if pk.Remote.RemoteId == "" { if pk.Remote.RemoteId == "" {
return fmt.Errorf("input must set remoteid") return fmt.Errorf("input must set remoteid")
} }
msh := remote.GetRemoteById(pk.Remote.RemoteId) wsh := remote.GetRemoteById(pk.Remote.RemoteId)
if msh == nil { if wsh == nil {
return fmt.Errorf("remote %s not found", pk.Remote.RemoteId) 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 { return WithTx(ctx, func(tx *TxWrap) error {
query := `UPDATE cmd SET cmdpid = ?, remotepid = ? WHERE screenid = ? AND lineid = ?` 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 return nil
}) })
} }

View File

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

View File

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

20112
yarn.lock

File diff suppressed because it is too large Load Diff