mirror of
https://github.com/wavetermdev/backup.git
synced 2026-08-05 13:57:07 -07:00
zsh support (#227)
adds zsh support to waveterm. big change, lots going on here. lots of other improvements and bug fixes added while debugging and building out the feature. Commits: * refactor shexec parser.go into new package shellenv. separate out bash specific parsing from generic functions * checkpoint * work on refactoring shexec. created two new packages shellapi (for bash/zsh specific stuff), and shellutil (shared between shellapi and shexec) * more refactoring * create shellapi interface to abstract bash specific functionality * more refactoring, move bash shell state parsing to shellapi * move makeRcFile to shellapi. remove all of the 'client' options CLI options from waveshell * get shellType passed through to server/single paths for waveshell * add a local shelltype detector * mock out a zshapi * move shelltype through more of the code * get a command to run via zsh * zsh can now switch directories. poc, needs cleanup * working on ShellState encoding differences between zsh/bash. Working on parsing zsh decls. move utilfn package into waveshell (shouldn't have been in wavesrv) * switch to use []byte for vardecl serialization + diffs * progress on zsh environment. still have issues reconciling init environment with trap environment * fix typeset argument parsing * parse promptvars, more zsh specific ignores * fix bug with promptvar not getting set (wrong check in FeState func) * add sdk (issue #188) to list of rtnstate commands * more zsh compatibility -- working with a larger ohmyzsh environment. ignore more variables, handle exit trap better. unique path/fpath. add a processtype variable to base. * must return a value * zsh alias parsing/restoring. diff changes (and rtnstate changes). introduces linediff v1. * force zmodload of zsh/parameter * starting work on zsh functions * need a v1 of mapdiff as well (to handle null chars) * pack/unpack of ints was wrong (one used int and one use uint). turned out we only ever encoded '0' so it worked. that also means it is safe to change unpack to unpackUInt * reworking for binary encoding of aliases and functions (because of zsh allows any character, including nulls, in names and values) * fixes, working on functions, issue with line endings * zsh functions. lots of ugliness here around dealing with line dicipline and cooked stty. new runcommand function to grab output from a non-tty fd. note that we still to run the actual command in a stty to get the proper output. * write uuid tempdir, cleanup with tmprcfilename code * hack in some simple zsh function declaration finding code for rtnstate. create function diff for rtnstate that supports zsh * make sure key order is constant so shell hashes are consistent * fix problems with state diffs to support new zsh formats. add diff/apply code to shellapi (moved from shellenv), that is now specific to zsh or bash * add log packet and new shellstate packets * switch to shellstate map that's also keyed by shelltype * add shelltype to remoteinstance * remove shell argument from waveshell * added new shelltype statemap to remote.go (msh), deal with fallout * move shellstate out of init packet, and move to an explicit reinit call. try to initialize all of the active shell states * change dont always store init state (only store on demand). initialize shell states on demand (if not already initialized). allow reset to change shells * add shellpref field to remote table. use to drive the default shell choice for new tabs * show shelltag on cmdinput, pass through ri and remote (defaultshellstate) * bump mshell version to v0.4 * better version validation for shellstate. also relax compatibility requirements for diffing states (shelltype + major version need to match) * better error handling, check shellstate compatibility during run (on waveshell server) * add extra separator for bash shellstate processing to deal with spurious output from rc files * special migration for v30 -- flag invalid bash shell states and show special button in UI to fix * format * remove zsh-decls (unused) * remove test code * remove debug print * fix typo
This commit is contained in:
@@ -7,10 +7,10 @@ signAsync({
|
||||
app: "temp/Wave.app",
|
||||
binaries: [
|
||||
waveAppPath + "/Contents/Resources/app/bin/wavesrv",
|
||||
waveAppPath + "/Contents/Resources/app/bin/mshell/mshell-v0.3-linux.amd64",
|
||||
waveAppPath + "/Contents/Resources/app/bin/mshell/mshell-v0.3-linux.arm64",
|
||||
waveAppPath + "/Contents/Resources/app/bin/mshell/mshell-v0.3-darwin.amd64",
|
||||
waveAppPath + "/Contents/Resources/app/bin/mshell/mshell-v0.3-darwin.arm64",
|
||||
waveAppPath + "/Contents/Resources/app/bin/mshell/mshell-v0.4-linux.amd64",
|
||||
waveAppPath + "/Contents/Resources/app/bin/mshell/mshell-v0.4-linux.arm64",
|
||||
waveAppPath + "/Contents/Resources/app/bin/mshell/mshell-v0.4-darwin.amd64",
|
||||
waveAppPath + "/Contents/Resources/app/bin/mshell/mshell-v0.4-darwin.arm64",
|
||||
],
|
||||
}).then(() => {
|
||||
console.log("signing success");
|
||||
|
||||
+12
-12
@@ -44,10 +44,10 @@ rm -rf bin/
|
||||
rm -rf build/
|
||||
node_modules/.bin/webpack --env prod
|
||||
GO_LDFLAGS="-s -w -X main.BuildTime=$(date +'%Y%m%d%H%M')"
|
||||
(cd waveshell; CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="$GO_LDFLAGS" -o ../bin/mshell/mshell-v0.3-darwin.amd64 main-waveshell.go)
|
||||
(cd waveshell; CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="$GO_LDFLAGS" -o ../bin/mshell/mshell-v0.3-darwin.arm64 main-waveshell.go)
|
||||
(cd waveshell; CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="$GO_LDFLAGS" -o ../bin/mshell/mshell-v0.3-linux.amd64 main-waveshell.go)
|
||||
(cd waveshell; CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="$GO_LDFLAGS" -o ../bin/mshell/mshell-v0.3-linux.arm64 main-waveshell.go)
|
||||
(cd waveshell; CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="$GO_LDFLAGS" -o ../bin/mshell/mshell-v0.4-darwin.amd64 main-waveshell.go)
|
||||
(cd waveshell; CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="$GO_LDFLAGS" -o ../bin/mshell/mshell-v0.4-darwin.arm64 main-waveshell.go)
|
||||
(cd waveshell; CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="$GO_LDFLAGS" -o ../bin/mshell/mshell-v0.4-linux.amd64 main-waveshell.go)
|
||||
(cd waveshell; CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="$GO_LDFLAGS" -o ../bin/mshell/mshell-v0.4-linux.arm64 main-waveshell.go)
|
||||
(cd wavesrv; CGO_ENABLED=1 go build -tags "osusergo,netgo,sqlite_omit_load_extension" -ldflags "-X main.BuildTime=$(date +'%Y%m%d%H%M')" -o ../bin/wavesrv ./cmd)
|
||||
node_modules/.bin/electron-forge make
|
||||
```
|
||||
@@ -60,10 +60,10 @@ rm -rf bin/
|
||||
rm -rf build/
|
||||
node_modules/.bin/webpack --env prod
|
||||
GO_LDFLAGS="-s -w -X main.BuildTime=$(date +'%Y%m%d%H%M')"
|
||||
(cd waveshell; CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="$GO_LDFLAGS" -o ../bin/mshell/mshell-v0.3-darwin.amd64 main-waveshell.go)
|
||||
(cd waveshell; CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="$GO_LDFLAGS" -o ../bin/mshell/mshell-v0.3-darwin.arm64 main-waveshell.go)
|
||||
(cd waveshell; CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="$GO_LDFLAGS" -o ../bin/mshell/mshell-v0.3-linux.amd64 main-waveshell.go)
|
||||
(cd waveshell; CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="$GO_LDFLAGS" -o ../bin/mshell/mshell-v0.3-linux.arm64 main-waveshell.go)
|
||||
(cd waveshell; CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="$GO_LDFLAGS" -o ../bin/mshell/mshell-v0.4-darwin.amd64 main-waveshell.go)
|
||||
(cd waveshell; CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="$GO_LDFLAGS" -o ../bin/mshell/mshell-v0.4-darwin.arm64 main-waveshell.go)
|
||||
(cd waveshell; CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="$GO_LDFLAGS" -o ../bin/mshell/mshell-v0.4-linux.amd64 main-waveshell.go)
|
||||
(cd waveshell; CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="$GO_LDFLAGS" -o ../bin/mshell/mshell-v0.4-linux.arm64 main-waveshell.go)
|
||||
# adds -extldflags=-static, *only* on linux (macos does not support fully static binaries) to avoid a glibc dependency
|
||||
(cd wavesrv; CGO_ENABLED=1 go build -tags "osusergo,netgo,sqlite_omit_load_extension" -ldflags "-linkmode 'external' -extldflags=-static $GO_LDFLAGS" -o ../bin/wavesrv ./cmd)
|
||||
node_modules/.bin/electron-forge make
|
||||
@@ -86,10 +86,10 @@ CGO_ENABLED=1 go build -tags "osusergo,netgo,sqlite_omit_load_extension" -ldflag
|
||||
set -e
|
||||
cd waveshell
|
||||
GO_LDFLAGS="-s -w -X main.BuildTime=$(date +'%Y%m%d%H%M')"
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="$GO_LDFLAGS" -o ../bin/mshell/mshell-v0.3-linux.amd64 main-waveshell.go
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="$GO_LDFLAGS" -o ../bin/mshell/mshell-v0.3-linux.arm64 main-waveshell.go
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="$GO_LDFLAGS" -o ../bin/mshell/mshell-v0.3-darwin.amd64 main-waveshell.go
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="$GO_LDFLAGS" -o ../bin/mshell/mshell-v0.3-darwin.arm64 main-waveshell.go
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="$GO_LDFLAGS" -o ../bin/mshell/mshell-v0.4-linux.amd64 main-waveshell.go
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="$GO_LDFLAGS" -o ../bin/mshell/mshell-v0.4-linux.arm64 main-waveshell.go
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="$GO_LDFLAGS" -o ../bin/mshell/mshell-v0.4-darwin.amd64 main-waveshell.go
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="$GO_LDFLAGS" -o ../bin/mshell/mshell-v0.4-darwin.arm64 main-waveshell.go
|
||||
```
|
||||
|
||||
```bash
|
||||
|
||||
@@ -25,6 +25,7 @@ class CreateRemoteConnModal extends React.Component<{}, {}> {
|
||||
tempConnectMode: OV<string>;
|
||||
tempPassword: OV<string>;
|
||||
tempKeyFile: OV<string>;
|
||||
tempShellPref: OV<string>;
|
||||
errorStr: OV<string>;
|
||||
remoteEdit: T.RemoteEditType;
|
||||
model: RemotesModel;
|
||||
@@ -40,6 +41,7 @@ class CreateRemoteConnModal extends React.Component<{}, {}> {
|
||||
this.tempConnectMode = mobx.observable.box("auto", { name: "CreateRemote-connectMode" });
|
||||
this.tempKeyFile = mobx.observable.box("", { name: "CreateRemote-keystr" });
|
||||
this.tempPassword = mobx.observable.box("", { name: "CreateRemote-password" });
|
||||
this.tempShellPref = mobx.observable.box("detect", { name: "CreateRemote-shellPref" });
|
||||
this.errorStr = mobx.observable.box(this.remoteEdit?.errorstr ?? null, { name: "CreateRemote-errorStr" });
|
||||
}
|
||||
|
||||
@@ -121,6 +123,7 @@ class CreateRemoteConnModal extends React.Component<{}, {}> {
|
||||
kwargs["password"] = "";
|
||||
}
|
||||
kwargs["connectmode"] = this.tempConnectMode.get();
|
||||
kwargs["shellpref"] = this.tempShellPref.get();
|
||||
kwargs["visual"] = "1";
|
||||
kwargs["submit"] = "1";
|
||||
let prtn = GlobalCommandRunner.createRemote(cname, kwargs, false);
|
||||
@@ -174,6 +177,13 @@ class CreateRemoteConnModal extends React.Component<{}, {}> {
|
||||
})();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleChangeShellPref(value: string): void {
|
||||
mobx.action(() => {
|
||||
this.tempShellPref.set(value);
|
||||
})();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleChangePort(value: string): void {
|
||||
mobx.action(() => {
|
||||
@@ -357,6 +367,20 @@ class CreateRemoteConnModal extends React.Component<{}, {}> {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="shellpref-section">
|
||||
<Dropdown
|
||||
label="Shell Preference"
|
||||
options={[
|
||||
{ value: "detect", label: "detect" },
|
||||
{ value: "bash", label: "bash" },
|
||||
{ value: "zsh", label: "zsh" },
|
||||
]}
|
||||
value={this.tempShellPref.get()}
|
||||
onChange={(val: string) => {
|
||||
this.tempShellPref.set(val);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<If condition={!util.isBlank(this.getErrorStr() as string)}>
|
||||
<div className="settings-field settings-error">Error: {this.getErrorStr()}</div>
|
||||
</If>
|
||||
|
||||
@@ -24,6 +24,7 @@ class EditRemoteConnModal extends React.Component<{}, {}> {
|
||||
tempPassword: OV<string>;
|
||||
tempConnectMode: OV<string>;
|
||||
tempAuthMode: OV<string>;
|
||||
tempShellPref: OV<string>;
|
||||
model: RemotesModel;
|
||||
|
||||
constructor(props: { remotesModel?: RemotesModel }) {
|
||||
@@ -34,6 +35,7 @@ class EditRemoteConnModal extends React.Component<{}, {}> {
|
||||
this.tempKeyFile = mobx.observable.box(null, { name: "EditRemoteSettings-tempKeyFile" });
|
||||
this.tempPassword = mobx.observable.box(null, { name: "EditRemoteSettings-tempPassword" });
|
||||
this.tempConnectMode = mobx.observable.box(null, { name: "EditRemoteSettings-tempConnectMode" });
|
||||
this.tempShellPref = mobx.observable.box(null, { name: "EditRemoteSettings-tempShellPref" });
|
||||
}
|
||||
|
||||
get selectedRemoteId() {
|
||||
@@ -52,6 +54,10 @@ class EditRemoteConnModal extends React.Component<{}, {}> {
|
||||
return this.model.isAuthEditMode();
|
||||
}
|
||||
|
||||
isLocalRemote(): boolean {
|
||||
return this.selectedRemote?.local;
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
mobx.action(() => {
|
||||
this.tempAlias.set(this.selectedRemote?.remotealias);
|
||||
@@ -59,6 +65,7 @@ class EditRemoteConnModal extends React.Component<{}, {}> {
|
||||
this.tempPassword.set(this.remoteEdit?.haspassword ? PasswordUnchangedSentinel : "");
|
||||
this.tempConnectMode.set(this.selectedRemote?.connectmode);
|
||||
this.tempAuthMode.set(this.selectedRemote?.authtype);
|
||||
this.tempShellPref.set(this.selectedRemote?.shellpref);
|
||||
})();
|
||||
}
|
||||
|
||||
@@ -103,6 +110,13 @@ class EditRemoteConnModal extends React.Component<{}, {}> {
|
||||
})();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleChangeShellPref(value: string): void {
|
||||
mobx.action(() => {
|
||||
this.tempShellPref.set(value);
|
||||
})();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
canResetPw(): boolean {
|
||||
if (this.remoteEdit == null) {
|
||||
@@ -154,6 +168,9 @@ class EditRemoteConnModal extends React.Component<{}, {}> {
|
||||
if (!util.isStrEq(this.tempConnectMode.get(), this.selectedRemote?.connectmode)) {
|
||||
kwargs["connectmode"] = this.tempConnectMode.get();
|
||||
}
|
||||
if (!util.isStrEq(this.tempShellPref.get(), this.selectedRemote?.shellpref)) {
|
||||
kwargs["shellpref"] = this.tempShellPref.get();
|
||||
}
|
||||
kwargs["visual"] = "1";
|
||||
kwargs["submit"] = "1";
|
||||
GlobalCommandRunner.editRemote(this.selectedRemote?.remoteid, kwargs);
|
||||
@@ -183,11 +200,150 @@ class EditRemoteConnModal extends React.Component<{}, {}> {
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
renderAlias() {
|
||||
return (
|
||||
<div className="alias-section">
|
||||
<TextField
|
||||
label="Alias"
|
||||
onChange={this.handleChangeAlias}
|
||||
value={this.tempAlias.get()}
|
||||
maxLength={100}
|
||||
decoration={{
|
||||
endDecoration: (
|
||||
<InputDecoration>
|
||||
<Tooltip
|
||||
message={`(Optional) A short alias to use when selecting or displaying this connection.`}
|
||||
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||
>
|
||||
<i className="fa-sharp fa-regular fa-circle-question" />
|
||||
</Tooltip>
|
||||
</InputDecoration>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderConnectMode() {
|
||||
return (
|
||||
<div className="connectmode-section">
|
||||
<Dropdown
|
||||
label="Connect Mode"
|
||||
options={[
|
||||
{ value: "startup", label: "startup" },
|
||||
{ value: "auto", label: "auto" },
|
||||
{ value: "manual", label: "manual" },
|
||||
]}
|
||||
value={this.tempConnectMode.get()}
|
||||
onChange={this.handleChangeConnectMode}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderShellPref() {
|
||||
return (
|
||||
<div className="shellpref-section">
|
||||
<Dropdown
|
||||
label="Shell Preference"
|
||||
options={[
|
||||
{ value: "detect", label: "detect" },
|
||||
{ value: "bash", label: "bash" },
|
||||
{ value: "zsh", label: "zsh" },
|
||||
]}
|
||||
value={this.tempShellPref.get()}
|
||||
onChange={this.handleChangeShellPref}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderAuthMode() {
|
||||
let authMode = this.tempAuthMode.get();
|
||||
return (
|
||||
<>
|
||||
<div className="authmode-section">
|
||||
<Dropdown
|
||||
label="Auth Mode"
|
||||
options={[
|
||||
{ value: "none", label: "none" },
|
||||
{ value: "key", label: "key" },
|
||||
{ value: "password", label: "password" },
|
||||
{ value: "key+password", label: "key+password" },
|
||||
]}
|
||||
value={this.tempAuthMode.get()}
|
||||
onChange={this.handleChangeAuthMode}
|
||||
decoration={{
|
||||
endDecoration: (
|
||||
<InputDecoration>
|
||||
<Tooltip
|
||||
message={
|
||||
<ul>
|
||||
<li>
|
||||
<b>none</b> - no authentication, or authentication is already
|
||||
configured in your ssh config.
|
||||
</li>
|
||||
<li>
|
||||
<b>key</b> - use a private key.
|
||||
</li>
|
||||
<li>
|
||||
<b>password</b> - use a password.
|
||||
</li>
|
||||
<li>
|
||||
<b>key+password</b> - use a key with a passphrase.
|
||||
</li>
|
||||
</ul>
|
||||
}
|
||||
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||
>
|
||||
<i className="fa-sharp fa-regular fa-circle-question" />
|
||||
</Tooltip>
|
||||
</InputDecoration>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<If condition={authMode == "key" || authMode == "key+password"}>
|
||||
<TextField
|
||||
label="SSH Keyfile"
|
||||
placeholder="keyfile path"
|
||||
onChange={this.handleChangeKeyFile}
|
||||
value={this.tempKeyFile.get()}
|
||||
maxLength={400}
|
||||
required={true}
|
||||
decoration={{
|
||||
endDecoration: (
|
||||
<InputDecoration>
|
||||
<Tooltip
|
||||
message={`(Required) The path to your ssh key file.`}
|
||||
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||
>
|
||||
<i className="fa-sharp fa-regular fa-circle-question" />
|
||||
</Tooltip>
|
||||
</InputDecoration>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</If>
|
||||
<If condition={authMode == "password" || authMode == "key+password"}>
|
||||
<PasswordField
|
||||
label={authMode == "password" ? "SSH Password" : "Key Passphrase"}
|
||||
placeholder="password"
|
||||
onChange={this.handleChangePassword}
|
||||
value={this.tempPassword.get()}
|
||||
maxLength={400}
|
||||
/>
|
||||
</If>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.remoteEdit === null || !this.isAuthEditMode) {
|
||||
return null;
|
||||
}
|
||||
let isLocal = this.isLocalRemote();
|
||||
return (
|
||||
<Modal className="erconn-modal">
|
||||
<Modal.Header title="Edit Connection" onClose={this.model.closeModal} />
|
||||
@@ -195,110 +351,10 @@ class EditRemoteConnModal extends React.Component<{}, {}> {
|
||||
<div className="name-actions-section">
|
||||
<div className="name text-primary">{util.getRemoteName(this.selectedRemote)}</div>
|
||||
</div>
|
||||
<div className="alias-section">
|
||||
<TextField
|
||||
label="Alias"
|
||||
onChange={this.handleChangeAlias}
|
||||
value={this.tempAlias.get()}
|
||||
maxLength={100}
|
||||
decoration={{
|
||||
endDecoration: (
|
||||
<InputDecoration>
|
||||
<Tooltip
|
||||
message={`(Optional) A short alias to use when selecting or displaying this connection.`}
|
||||
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||
>
|
||||
<i className="fa-sharp fa-regular fa-circle-question" />
|
||||
</Tooltip>
|
||||
</InputDecoration>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="authmode-section">
|
||||
<Dropdown
|
||||
label="Auth Mode"
|
||||
options={[
|
||||
{ value: "none", label: "none" },
|
||||
{ value: "key", label: "key" },
|
||||
{ value: "password", label: "password" },
|
||||
{ value: "key+password", label: "key+password" },
|
||||
]}
|
||||
value={this.tempAuthMode.get()}
|
||||
onChange={this.handleChangeAuthMode}
|
||||
decoration={{
|
||||
endDecoration: (
|
||||
<InputDecoration>
|
||||
<Tooltip
|
||||
message={
|
||||
<ul>
|
||||
<li>
|
||||
<b>none</b> - no authentication, or authentication is already
|
||||
configured in your ssh config.
|
||||
</li>
|
||||
<li>
|
||||
<b>key</b> - use a private key.
|
||||
</li>
|
||||
<li>
|
||||
<b>password</b> - use a password.
|
||||
</li>
|
||||
<li>
|
||||
<b>key+password</b> - use a key with a passphrase.
|
||||
</li>
|
||||
</ul>
|
||||
}
|
||||
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||
>
|
||||
<i className="fa-sharp fa-regular fa-circle-question" />
|
||||
</Tooltip>
|
||||
</InputDecoration>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<If condition={authMode == "key" || authMode == "key+password"}>
|
||||
<TextField
|
||||
label="SSH Keyfile"
|
||||
placeholder="keyfile path"
|
||||
onChange={this.handleChangeKeyFile}
|
||||
value={this.tempKeyFile.get()}
|
||||
maxLength={400}
|
||||
required={true}
|
||||
decoration={{
|
||||
endDecoration: (
|
||||
<InputDecoration>
|
||||
<Tooltip
|
||||
message={`(Required) The path to your ssh key file.`}
|
||||
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||
>
|
||||
<i className="fa-sharp fa-regular fa-circle-question" />
|
||||
</Tooltip>
|
||||
</InputDecoration>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</If>
|
||||
<If condition={authMode == "password" || authMode == "key+password"}>
|
||||
<PasswordField
|
||||
label={authMode == "password" ? "SSH Password" : "Key Passphrase"}
|
||||
placeholder="password"
|
||||
onChange={this.handleChangePassword}
|
||||
value={this.tempPassword.get()}
|
||||
maxLength={400}
|
||||
/>
|
||||
</If>
|
||||
<div className="connectmode-section">
|
||||
<Dropdown
|
||||
label="Connect Mode"
|
||||
options={[
|
||||
{ value: "startup", label: "startup" },
|
||||
{ value: "auto", label: "auto" },
|
||||
{ value: "manual", label: "manual" },
|
||||
]}
|
||||
value={this.tempConnectMode.get()}
|
||||
onChange={this.handleChangeConnectMode}
|
||||
/>
|
||||
</div>
|
||||
<If condition={!isLocal}>{this.renderAlias()}</If>
|
||||
<If condition={!isLocal}>{this.renderAuthMode()}</If>
|
||||
<If condition={!isLocal}>{this.renderConnectMode()}</If>
|
||||
{this.renderShellPref()}
|
||||
<If condition={!util.isBlank(this.remoteEdit?.errorstr)}>
|
||||
<div className="settings-field settings-error">Error: {this.remoteEdit?.errorstr}</div>
|
||||
</If>
|
||||
|
||||
@@ -206,7 +206,6 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
|
||||
);
|
||||
if (remote.local) {
|
||||
installNowButton = <></>;
|
||||
updateAuthButton = <></>;
|
||||
cancelInstallButton = <></>;
|
||||
}
|
||||
if (remote.sshconfigsrc == "sshconfig-import") {
|
||||
@@ -352,6 +351,10 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
|
||||
<div className="settings-label">Connect Mode</div>
|
||||
<div className="settings-input">{remote.connectmode}</div>
|
||||
</div>
|
||||
<div className="settings-field">
|
||||
<div className="settings-label">Shell Pref</div>
|
||||
<div className="settings-input">{remote.shellpref}</div>
|
||||
</div>
|
||||
{this.renderInstallStatus(remote)}
|
||||
<div className="flex-spacer" style={{ minHeight: 20 }} />
|
||||
<div className="status">
|
||||
|
||||
@@ -128,6 +128,22 @@
|
||||
padding: 1em 2px;
|
||||
}
|
||||
|
||||
.textareainput-div {
|
||||
position: relative;
|
||||
|
||||
.shelltag {
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
right: 3px;
|
||||
font-size: 10px;
|
||||
color: @text-secondary;
|
||||
line-height: 1;
|
||||
padding: 0px 8px 3px 8px;
|
||||
background-color: @textarea-background;
|
||||
border-radius: 0 0 5px 5px;
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
color: @term-bright-white;
|
||||
background-color: @textarea-background;
|
||||
|
||||
@@ -99,6 +99,11 @@ class CmdInput extends React.Component<{}, {}> {
|
||||
})();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
clickResetState(): void {
|
||||
GlobalCommandRunner.resetShellState();
|
||||
}
|
||||
|
||||
render() {
|
||||
let model = GlobalModel;
|
||||
let inputModel = model.inputModel;
|
||||
@@ -115,6 +120,7 @@ class CmdInput extends React.Component<{}, {}> {
|
||||
remote = GlobalModel.getRemote(ri.remoteid);
|
||||
feState = ri.festate;
|
||||
}
|
||||
feState = feState || {};
|
||||
let infoShow = inputModel.infoShow.get();
|
||||
let historyShow = !infoShow && inputModel.historyShow.get();
|
||||
let aiChatShow = inputModel.aIChatShow.get();
|
||||
@@ -162,6 +168,18 @@ class CmdInput extends React.Component<{}, {}> {
|
||||
</If>
|
||||
</div>
|
||||
</If>
|
||||
<If condition={feState["invalidshellstate"]}>
|
||||
<div className="remote-status-warning">
|
||||
WARNING: The shell state for this tab is invalid (
|
||||
<a target="_blank" href="https://docs.waveterm.dev/reference/faq">
|
||||
see FAQ
|
||||
</a>
|
||||
). Must reset to continue.
|
||||
<div className="button is-wave-green is-outlined is-small" onClick={this.clickResetState}>
|
||||
reset shell state
|
||||
</div>
|
||||
</div>
|
||||
</If>
|
||||
<div key="prompt" className="cmd-input-context">
|
||||
<div className="has-text-white">
|
||||
<span ref={this.promptRef}>
|
||||
|
||||
@@ -5,6 +5,8 @@ import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import * as mobx from "mobx";
|
||||
import type * as T from "../../../types/types";
|
||||
import * as util from "../../../util/util";
|
||||
import { If } from "tsx-control-statements/components";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import cn from "classnames";
|
||||
import { GlobalModel, GlobalCommandRunner, Screen } from "../../../model/model";
|
||||
@@ -585,8 +587,24 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
||||
let computedInnerHeight = displayLines * (termFontSize * 1.5) + 2 * 0.5 * termFontSize;
|
||||
// inner height + 2*1em padding
|
||||
let computedOuterHeight = computedInnerHeight + 2 * 1.0 * termFontSize;
|
||||
let shellType: string = "";
|
||||
let screen = GlobalModel.getActiveScreen();
|
||||
if (screen != null) {
|
||||
let ri = screen.getCurRemoteInstance();
|
||||
console.log("got ri", ri);
|
||||
if (ri != null && ri.shelltype != null) {
|
||||
shellType = ri.shelltype;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className="control is-expanded" ref={this.controlRef} style={{ height: computedOuterHeight }}>
|
||||
<div
|
||||
className="textareainput-div control is-expanded"
|
||||
ref={this.controlRef}
|
||||
style={{ height: computedOuterHeight }}
|
||||
>
|
||||
<If condition={!disabled && !util.isBlank(shellType)}>
|
||||
<div className="shelltag">{shellType}</div>
|
||||
</If>
|
||||
<textarea
|
||||
key="main"
|
||||
ref={this.mainInputRef}
|
||||
|
||||
@@ -1213,6 +1213,7 @@ class Session {
|
||||
remoteid: rptr.remoteid,
|
||||
name: rptr.name,
|
||||
festate: remote.defaultfestate,
|
||||
shelltype: remote.defaultshelltype,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
@@ -4567,6 +4568,10 @@ class CommandRunner {
|
||||
GlobalModel.submitCommand("history", null, null, kwargs, true);
|
||||
}
|
||||
|
||||
resetShellState() {
|
||||
GlobalModel.submitCommand("reset", null, null, null, true);
|
||||
}
|
||||
|
||||
historyPurgeLines(lines: string[]): Promise<CommandRtnType> {
|
||||
let prtn = GlobalModel.submitCommand("history", "purge", lines, { nohist: "1" }, false);
|
||||
return prtn;
|
||||
|
||||
@@ -121,6 +121,8 @@ type RemoteType = {
|
||||
remoteopts?: RemoteOptsType;
|
||||
local: boolean;
|
||||
remove?: boolean;
|
||||
shellpref: string;
|
||||
defaultshelltype: string;
|
||||
};
|
||||
|
||||
type RemoteStateType = {
|
||||
@@ -136,6 +138,7 @@ type RemoteInstanceType = {
|
||||
remoteownerid: string;
|
||||
remoteid: string;
|
||||
festate: Record<string, string>;
|
||||
shelltype: string;
|
||||
|
||||
remove?: boolean;
|
||||
};
|
||||
|
||||
+18
-444
File diff suppressed because it is too large
Load Diff
@@ -30,7 +30,7 @@ const SSHCommandVarName = "SSH_COMMAND"
|
||||
const MShellDebugVarName = "MSHELL_DEBUG"
|
||||
const SessionsDirBaseName = "sessions"
|
||||
const RcFilesDirBaseName = "rcfiles"
|
||||
const MShellVersion = "v0.3.0"
|
||||
const MShellVersion = "v0.4.0"
|
||||
const RemoteIdFile = "remoteid"
|
||||
const DefaultMShellInstallBinDir = "/opt/mshell/bin"
|
||||
const LogFileName = "mshell.log"
|
||||
@@ -39,6 +39,13 @@ const ForceDebugLog = false
|
||||
const DebugFlag_LogRcFile = "logrc"
|
||||
const LogRcFileName = "debug.rcfile"
|
||||
|
||||
const (
|
||||
ProcessType_Unknown = "unknown"
|
||||
ProcessType_WaveSrv = "wavesrv"
|
||||
ProcessType_WaveShellSingle = "waveshell-single"
|
||||
ProcessType_WaveShellServer = "waveshell-server"
|
||||
)
|
||||
|
||||
// keys are sessionids (also the key RcFilesDirBaseName)
|
||||
var ensureDirCache = make(map[string]bool)
|
||||
var baseLock = &sync.Mutex{}
|
||||
@@ -46,6 +53,8 @@ var DebugLogEnabled = false
|
||||
var DebugLogger *log.Logger
|
||||
var BuildTime string = "0"
|
||||
|
||||
var ProcessType string = ProcessType_Unknown
|
||||
|
||||
type CommandFileNames struct {
|
||||
PtyOutFile string
|
||||
StdinFifo string
|
||||
@@ -58,6 +67,10 @@ func SetBuildTime(build string) {
|
||||
BuildTime = build
|
||||
}
|
||||
|
||||
func IsWaveSrv() bool {
|
||||
return ProcessType == ProcessType_WaveSrv
|
||||
}
|
||||
|
||||
func MakeCommandKey(sessionId string, cmdId string) CommandKey {
|
||||
if sessionId == "" && cmdId == "" {
|
||||
return CommandKey("")
|
||||
|
||||
@@ -44,9 +44,9 @@ func PackStrArr(w io.Writer, strs []string) error {
|
||||
return PackValue(w, barr)
|
||||
}
|
||||
|
||||
func PackInt(w io.Writer, ival int) error {
|
||||
func PackUInt(w io.Writer, ival uint64) error {
|
||||
viBuf := make([]byte, binary.MaxVarintLen64)
|
||||
l := binary.PutUvarint(viBuf, uint64(ival))
|
||||
l := binary.PutUvarint(viBuf, ival)
|
||||
_, err := w.Write(viBuf[0:l])
|
||||
return err
|
||||
}
|
||||
@@ -80,8 +80,16 @@ func UnpackStrArr(r FullByteReader) ([]string, error) {
|
||||
return strs, nil
|
||||
}
|
||||
|
||||
func UnpackInt(r io.ByteReader) (int, error) {
|
||||
ival64, err := binary.ReadVarint(r)
|
||||
func UnpackUInt(r io.ByteReader) (uint64, error) {
|
||||
ival64, err := binary.ReadUvarint(r)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return ival64, nil
|
||||
}
|
||||
|
||||
func UnpackUIntAsInt(r io.ByteReader) (int, error) {
|
||||
ival64, err := UnpackUInt(r)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -99,15 +107,15 @@ func (u *Unpacker) UnpackValue(name string) []byte {
|
||||
return rtn
|
||||
}
|
||||
|
||||
func (u *Unpacker) UnpackInt(name string) int {
|
||||
func (u *Unpacker) UnpackUInt(name string) int {
|
||||
if u.Err != nil {
|
||||
return 0
|
||||
}
|
||||
rtn, err := UnpackInt(u.R)
|
||||
rtn, err := UnpackUInt(u.R)
|
||||
if err != nil {
|
||||
u.Err = fmt.Errorf("cannot unpack %s: %v", name, err)
|
||||
}
|
||||
return rtn
|
||||
return int(rtn)
|
||||
}
|
||||
|
||||
func (u *Unpacker) UnpackStrArr(name string) []string {
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"os"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/base"
|
||||
)
|
||||
@@ -59,11 +60,18 @@ const (
|
||||
WriteFileReadyPacketStr = "writefileready" // rpc-response
|
||||
WriteFileDonePacketStr = "writefiledone" // rpc-response
|
||||
FileDataPacketStr = "filedata"
|
||||
LogPacketStr = "log" // logging packet (sent from waveshell back to server)
|
||||
ShellStatePacketStr = "shellstate"
|
||||
|
||||
OpenAIPacketStr = "openai" // other
|
||||
OpenAICloudReqStr = "openai-cloudreq"
|
||||
)
|
||||
|
||||
const (
|
||||
ShellType_bash = "bash"
|
||||
ShellType_zsh = "zsh"
|
||||
)
|
||||
|
||||
const PacketSenderQueueSize = 20
|
||||
|
||||
const PacketEOFStr = "EOF"
|
||||
@@ -102,6 +110,8 @@ func init() {
|
||||
TypeStrToFactory[WriteFilePacketStr] = reflect.TypeOf(WriteFilePacketType{})
|
||||
TypeStrToFactory[WriteFileReadyPacketStr] = reflect.TypeOf(WriteFileReadyPacketType{})
|
||||
TypeStrToFactory[WriteFileDonePacketStr] = reflect.TypeOf(WriteFileDonePacketType{})
|
||||
TypeStrToFactory[LogPacketStr] = reflect.TypeOf(LogPacketType{})
|
||||
TypeStrToFactory[ShellStatePacketStr] = reflect.TypeOf(ShellStatePacketType{})
|
||||
|
||||
var _ RpcPacketType = (*RunPacketType)(nil)
|
||||
var _ RpcPacketType = (*GetCmdPacketType)(nil)
|
||||
@@ -119,6 +129,7 @@ func init() {
|
||||
var _ RpcResponsePacketType = (*FileDataPacketType)(nil)
|
||||
var _ RpcResponsePacketType = (*WriteFileReadyPacketType)(nil)
|
||||
var _ RpcResponsePacketType = (*WriteFileDonePacketType)(nil)
|
||||
var _ RpcResponsePacketType = (*ShellStatePacketType)(nil)
|
||||
|
||||
var _ CommandPacketType = (*DataPacketType)(nil)
|
||||
var _ CommandPacketType = (*DataAckPacketType)(nil)
|
||||
@@ -382,8 +393,9 @@ func MakeCdPacket() *CdPacketType {
|
||||
}
|
||||
|
||||
type ReInitPacketType struct {
|
||||
Type string `json:"type"`
|
||||
ReqId string `json:"reqid"`
|
||||
Type string `json:"type"`
|
||||
ShellType string `json:"shelltype"`
|
||||
ReqId string `json:"reqid"`
|
||||
}
|
||||
|
||||
func (*ReInitPacketType) GetType() string {
|
||||
@@ -543,6 +555,54 @@ func MakeRawPacket(val string) *RawPacketType {
|
||||
return &RawPacketType{Type: RawPacketStr, Data: val}
|
||||
}
|
||||
|
||||
type LogPacketType struct {
|
||||
Type string `json:"type"`
|
||||
Ts int64 `json:"ts"` // log timestamp
|
||||
ReqId string `json:"reqid,omitempty"` // if this log line is related to an rpc request
|
||||
ProcInfo string `json:"procinfo,omitempty"` // server/single
|
||||
LogLine string `json:"logline"` // the logline data
|
||||
}
|
||||
|
||||
func (*LogPacketType) GetType() string {
|
||||
return LogPacketStr
|
||||
}
|
||||
|
||||
func (p *LogPacketType) String() string {
|
||||
return "log"
|
||||
}
|
||||
|
||||
func MakeLogPacket() *LogPacketType {
|
||||
return &LogPacketType{Type: LogPacketStr, Ts: time.Now().UnixMilli()}
|
||||
}
|
||||
|
||||
type ShellStatePacketType struct {
|
||||
Type string `json:"type"`
|
||||
ShellType string `json:"shelltype"`
|
||||
RespId string `json:"respid,omitempty"`
|
||||
State *ShellState `json:"state"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (*ShellStatePacketType) GetType() string {
|
||||
return ShellStatePacketStr
|
||||
}
|
||||
|
||||
func (p *ShellStatePacketType) String() string {
|
||||
return fmt.Sprintf("shellstate[%s]", p.ShellType)
|
||||
}
|
||||
|
||||
func (p *ShellStatePacketType) GetResponseId() string {
|
||||
return p.RespId
|
||||
}
|
||||
|
||||
func (p *ShellStatePacketType) GetResponseDone() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func MakeShellStatePacket() *ShellStatePacketType {
|
||||
return &ShellStatePacketType{Type: ShellStatePacketStr}
|
||||
}
|
||||
|
||||
type MessagePacketType struct {
|
||||
Type string `json:"type"`
|
||||
CK base.CommandKey `json:"ck,omitempty"`
|
||||
@@ -567,19 +627,18 @@ func FmtMessagePacket(fmtStr string, args ...interface{}) *MessagePacketType {
|
||||
}
|
||||
|
||||
type InitPacketType struct {
|
||||
Type string `json:"type"`
|
||||
RespId string `json:"respid,omitempty"`
|
||||
Version string `json:"version"`
|
||||
BuildTime string `json:"buildtime,omitempty"`
|
||||
MShellHomeDir string `json:"mshellhomedir,omitempty"`
|
||||
HomeDir string `json:"homedir,omitempty"`
|
||||
State *ShellState `json:"state,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
HostName string `json:"hostname,omitempty"`
|
||||
NotFound bool `json:"notfound,omitempty"`
|
||||
UName string `json:"uname,omitempty"`
|
||||
Shell string `json:"shell,omitempty"`
|
||||
RemoteId string `json:"remoteid,omitempty"`
|
||||
Type string `json:"type"`
|
||||
RespId string `json:"respid,omitempty"`
|
||||
Version string `json:"version"`
|
||||
BuildTime string `json:"buildtime,omitempty"`
|
||||
MShellHomeDir string `json:"mshellhomedir,omitempty"`
|
||||
HomeDir string `json:"homedir,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
HostName string `json:"hostname,omitempty"`
|
||||
NotFound bool `json:"notfound,omitempty"`
|
||||
UName string `json:"uname,omitempty"`
|
||||
Shell string `json:"shell,omitempty"`
|
||||
RemoteId string `json:"remoteid,omitempty"`
|
||||
}
|
||||
|
||||
func (*InitPacketType) GetType() string {
|
||||
@@ -701,6 +760,7 @@ type RunPacketType struct {
|
||||
Type string `json:"type"`
|
||||
ReqId string `json:"reqid"`
|
||||
CK base.CommandKey `json:"ck"`
|
||||
ShellType string `json:"shelltype"` // new in v0.6.0 (either "bash" or "zsh") (set by remote.go)
|
||||
Command string `json:"command"`
|
||||
State *ShellState `json:"state,omitempty"`
|
||||
StateDiff *ShellStateDiff `json:"statediff,omitempty"`
|
||||
|
||||
@@ -144,14 +144,20 @@ func (p *PacketParser) getRpcEntry(reqId string) *RpcEntry {
|
||||
return entry
|
||||
}
|
||||
|
||||
// returns true if sent to an RPC channel. false if not (which then allows the packet to be sent to MainCh)
|
||||
// if GetResponseId() returns "", then this will return false
|
||||
func (p *PacketParser) trySendRpcResponse(pk PacketType) bool {
|
||||
respPk, ok := pk.(RpcResponsePacketType)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
respId := respPk.GetResponseId()
|
||||
if respId == "" {
|
||||
return false
|
||||
}
|
||||
p.Lock.Lock()
|
||||
defer p.Lock.Unlock()
|
||||
entry := p.RpcMap[respPk.GetResponseId()]
|
||||
entry := p.RpcMap[respId]
|
||||
if entry == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ type ShellState struct {
|
||||
}
|
||||
|
||||
type ShellStateDiff struct {
|
||||
Version string `json:"version"` // [type] [semver]
|
||||
Version string `json:"version"` // [type] [semver] (note this should *always* be set even if the same as base)
|
||||
BaseHash string `json:"basehash"`
|
||||
DiffHashArr []string `json:"diffhasharr,omitempty"`
|
||||
Cwd string `json:"cwd,omitempty"`
|
||||
@@ -41,6 +41,66 @@ type ShellStateDiff struct {
|
||||
HashVal string `json:"-"`
|
||||
}
|
||||
|
||||
func (state ShellState) GetShellType() string {
|
||||
shell, _, _ := ParseShellStateVersion(state.Version)
|
||||
return shell
|
||||
}
|
||||
|
||||
// returns (shell, version, error)
|
||||
func ParseShellStateVersion(fullVersionStr string) (string, string, error) {
|
||||
if fullVersionStr == "" {
|
||||
return "", "", fmt.Errorf("empty shellstate version")
|
||||
}
|
||||
fields := strings.Split(fullVersionStr, " ")
|
||||
if len(fields) != 2 {
|
||||
return "", "", fmt.Errorf("invalid shellstate version format: %q", fullVersionStr)
|
||||
}
|
||||
shell := fields[0]
|
||||
version := fields[1]
|
||||
if shell != ShellType_zsh && shell != ShellType_bash {
|
||||
return "", "", fmt.Errorf("invalid shellstate shell type: %q", fullVersionStr)
|
||||
}
|
||||
if !semver.IsValid(version) {
|
||||
return "", "", fmt.Errorf("invalid shellstate semver: %q", fullVersionStr)
|
||||
}
|
||||
return shell, version, nil
|
||||
}
|
||||
|
||||
// we're going to allow different versions (as long as shelltype is the same)
|
||||
// before we required version numbers to match exactly which was too restrictive
|
||||
func StateVersionsCompatible(v1 string, v2 string) bool {
|
||||
if v1 == v2 {
|
||||
return true
|
||||
}
|
||||
shell1, version1, err := ParseShellStateVersion(v1)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
shell2, version2, err := ParseShellStateVersion(v2)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if shell1 != shell2 {
|
||||
return false
|
||||
}
|
||||
if semver.Major(version1) != semver.Major(version2) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (diff ShellStateDiff) GetShellType() string {
|
||||
shell, _, _ := ParseShellStateVersion(diff.Version)
|
||||
return shell
|
||||
}
|
||||
|
||||
func (state ShellState) GetLineDiffSplitString() string {
|
||||
if state.GetShellType() == ShellType_zsh {
|
||||
return "\x00"
|
||||
}
|
||||
return "\n"
|
||||
}
|
||||
|
||||
func (state ShellState) IsEmpty() bool {
|
||||
return state.Version == "" && state.Cwd == "" && len(state.ShellVars) == 0 && state.Aliases == "" && state.Funcs == "" && state.Error == ""
|
||||
}
|
||||
@@ -55,7 +115,7 @@ func sha1Hash(data []byte) string {
|
||||
// returns (SHA1, encoded-state)
|
||||
func (state ShellState) EncodeAndHash() (string, []byte) {
|
||||
var buf bytes.Buffer
|
||||
binpack.PackInt(&buf, ShellStatePackVersion)
|
||||
binpack.PackUInt(&buf, ShellStatePackVersion)
|
||||
binpack.PackValue(&buf, []byte(state.Version))
|
||||
binpack.PackValue(&buf, []byte(state.Cwd))
|
||||
binpack.PackValue(&buf, state.ShellVars)
|
||||
@@ -66,7 +126,7 @@ func (state ShellState) EncodeAndHash() (string, []byte) {
|
||||
}
|
||||
|
||||
// returns a string like "v4" ("" is an unparseable version)
|
||||
func GetBashMajorVersion(versionStr string) string {
|
||||
func GetMajorVersion(versionStr string) string {
|
||||
if versionStr == "" {
|
||||
return ""
|
||||
}
|
||||
@@ -94,7 +154,7 @@ func (state *ShellState) DecodeShellState(barr []byte) error {
|
||||
state.HashVal = sha1Hash(barr)
|
||||
buf := bytes.NewBuffer(barr)
|
||||
u := binpack.MakeUnpacker(buf)
|
||||
version := u.UnpackInt("ShellState pack version")
|
||||
version := u.UnpackUInt("ShellState pack version")
|
||||
if version != ShellStatePackVersion {
|
||||
return fmt.Errorf("invalid ShellState pack version: %d", version)
|
||||
}
|
||||
@@ -118,7 +178,7 @@ func (state *ShellState) UnmarshalJSON(jsonBytes []byte) error {
|
||||
|
||||
func (sdiff ShellStateDiff) EncodeAndHash() (string, []byte) {
|
||||
var buf bytes.Buffer
|
||||
binpack.PackInt(&buf, ShellStateDiffPackVersion)
|
||||
binpack.PackUInt(&buf, ShellStateDiffPackVersion)
|
||||
binpack.PackValue(&buf, []byte(sdiff.Version))
|
||||
binpack.PackValue(&buf, []byte(sdiff.BaseHash))
|
||||
binpack.PackStrArr(&buf, sdiff.DiffHashArr)
|
||||
@@ -139,7 +199,7 @@ func (sdiff *ShellStateDiff) DecodeShellStateDiff(barr []byte) error {
|
||||
sdiff.HashVal = sha1Hash(barr)
|
||||
buf := bytes.NewBuffer(barr)
|
||||
u := binpack.MakeUnpacker(buf)
|
||||
version := u.UnpackInt("ShellState pack version")
|
||||
version := u.UnpackUInt("ShellState pack version")
|
||||
if version != ShellStateDiffPackVersion {
|
||||
return fmt.Errorf("invalid ShellStateDiff pack version: %d", version)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package packet
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestShellVersions(t *testing.T) {
|
||||
if !StateVersionsCompatible("bash v5.0.17", "bash v5.0.17") {
|
||||
t.Errorf("versions should be compatible")
|
||||
}
|
||||
if !StateVersionsCompatible("bash v5.0.17", "bash v5.0.18") {
|
||||
t.Errorf("versions should be compatible")
|
||||
}
|
||||
if !StateVersionsCompatible("bash v5.0.17", "bash v5.1.0") {
|
||||
t.Errorf("versions should be compatible")
|
||||
}
|
||||
if StateVersionsCompatible("bash v5.0.17", "bash v6.0.0") {
|
||||
t.Errorf("versions should not be compatible")
|
||||
}
|
||||
if StateVersionsCompatible("bash v5.0.17", "zsh v5.0.17") {
|
||||
t.Errorf("versions should not be compatible")
|
||||
}
|
||||
|
||||
shell, version, err := ParseShellStateVersion("bash v5.0.17")
|
||||
if err != nil {
|
||||
t.Errorf("version should be valid, got error %v", err)
|
||||
}
|
||||
if shell != ShellType_bash {
|
||||
t.Errorf("shell should be bash")
|
||||
}
|
||||
if version != "v5.0.17" {
|
||||
t.Errorf("version should be v5.0.17")
|
||||
}
|
||||
shell, version, err = ParseShellStateVersion("zsh v5.0.17")
|
||||
if err != nil {
|
||||
t.Errorf("version should be valid, got error %v", err)
|
||||
}
|
||||
if shell != ShellType_zsh {
|
||||
t.Errorf("shell should be zsh")
|
||||
}
|
||||
if version != "v5.0.17" {
|
||||
t.Errorf("version should be v5.0.17")
|
||||
}
|
||||
_, _, err = ParseShellStateVersion("fish v5.0.17")
|
||||
if err == nil {
|
||||
t.Errorf("version should be invalid")
|
||||
}
|
||||
_, _, err = ParseShellStateVersion("bash v5.0.17.1")
|
||||
if err == nil {
|
||||
t.Errorf("version should be invalid")
|
||||
}
|
||||
_, _, err = ParseShellStateVersion("bash")
|
||||
if err == nil {
|
||||
t.Errorf("version should be invalid")
|
||||
}
|
||||
_, _, err = ParseShellStateVersion("bash v5.0.17 extrastuff")
|
||||
if err == nil {
|
||||
t.Errorf("version should be invalid")
|
||||
}
|
||||
}
|
||||
+128
-36
@@ -20,7 +20,9 @@ import (
|
||||
"github.com/alessio/shellescape"
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/base"
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/packet"
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/shellapi"
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/shexec"
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/utilfn"
|
||||
)
|
||||
|
||||
const MaxFileDataPacketSize = 16 * 1024
|
||||
@@ -28,6 +30,17 @@ const WriteFileContextTimeout = 30 * time.Second
|
||||
const cleanLoopTime = 5 * time.Second
|
||||
const MaxWriteFileContextData = 100
|
||||
|
||||
type shellStateMapKey struct {
|
||||
ShellType string
|
||||
Hash string
|
||||
}
|
||||
|
||||
type ShellStateMap struct {
|
||||
Lock *sync.Mutex
|
||||
StateMap map[shellStateMapKey]*packet.ShellState // shelltype+hash -> state
|
||||
CurrentStateMap map[string]string // shelltype -> hash
|
||||
}
|
||||
|
||||
// TODO create unblockable packet-sender (backed by an array) for clientproc
|
||||
type MServer struct {
|
||||
Lock *sync.Mutex
|
||||
@@ -35,9 +48,8 @@ type MServer struct {
|
||||
Sender *packet.PacketSender
|
||||
ClientMap map[base.CommandKey]*shexec.ClientProc
|
||||
Debug bool
|
||||
StateMap map[string]*packet.ShellState // sha1->state
|
||||
CurrentState string // sha1
|
||||
WriteErrorCh chan bool // closed if there is a I/O write error
|
||||
StateMap *ShellStateMap
|
||||
WriteErrorCh chan bool // closed if there is a I/O write error
|
||||
WriteErrorChOnce *sync.Once
|
||||
WriteFileContextMap map[string]*WriteFileContext
|
||||
Done bool
|
||||
@@ -146,11 +158,15 @@ func (m *MServer) ProcessCommandPacket(pk packet.CommandPacketType) {
|
||||
}
|
||||
|
||||
func runSingleCompGen(cwd string, compType string, prefix string) ([]string, bool, error) {
|
||||
sapi, err := shellapi.MakeShellApi(packet.ShellType_bash)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if !packet.IsValidCompGenType(compType) {
|
||||
return nil, false, fmt.Errorf("invalid compgen type '%s'", compType)
|
||||
}
|
||||
compGenCmdStr := fmt.Sprintf("cd %s; compgen -A %s -- %s | sort | uniq | head -n %d", shellescape.Quote(cwd), shellescape.Quote(compType), shellescape.Quote(prefix), packet.MaxCompGenValues+1)
|
||||
ecmd := exec.Command(shexec.GetLocalBashPath(), "-c", compGenCmdStr)
|
||||
ecmd := exec.Command(sapi.GetLocalShellPath(), "-c", compGenCmdStr)
|
||||
outputBytes, err := ecmd.Output()
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("compgen error: %w", err)
|
||||
@@ -230,26 +246,19 @@ func (m *MServer) runCompGen(compPk *packet.CompGenPacketType) {
|
||||
return
|
||||
}
|
||||
|
||||
func (m *MServer) setCurrentState(state *packet.ShellState) {
|
||||
if state == nil {
|
||||
return
|
||||
}
|
||||
hval, _ := state.EncodeAndHash()
|
||||
m.Lock.Lock()
|
||||
defer m.Lock.Unlock()
|
||||
m.StateMap[hval] = state
|
||||
m.CurrentState = hval
|
||||
}
|
||||
|
||||
func (m *MServer) reinit(reqId string) {
|
||||
initPk, err := shexec.MakeServerInitPacket()
|
||||
func (m *MServer) reinit(reqId string, shellType string) {
|
||||
ssPk, err := shexec.MakeShellStatePacket(shellType)
|
||||
if err != nil {
|
||||
m.Sender.SendErrorResponse(reqId, fmt.Errorf("error creating init packet: %w", err))
|
||||
return
|
||||
}
|
||||
m.setCurrentState(initPk.State)
|
||||
initPk.RespId = reqId
|
||||
m.Sender.SendPacket(initPk)
|
||||
err = m.StateMap.SetCurrentState(ssPk.State.GetShellType(), ssPk.State)
|
||||
if err != nil {
|
||||
m.Sender.SendErrorResponse(reqId, fmt.Errorf("error setting current state: %w", err))
|
||||
return
|
||||
}
|
||||
ssPk.RespId = reqId
|
||||
m.Sender.SendPacket(ssPk)
|
||||
}
|
||||
|
||||
func makeTemp(path string, mode fs.FileMode) (*os.File, error) {
|
||||
@@ -564,8 +573,8 @@ func (m *MServer) ProcessRpcPacket(pk packet.RpcPacketType) {
|
||||
go m.runCompGen(compPk)
|
||||
return
|
||||
}
|
||||
if _, ok := pk.(*packet.ReInitPacketType); ok {
|
||||
go m.reinit(reqId)
|
||||
if reinitPk, ok := pk.(*packet.ReInitPacketType); ok {
|
||||
go m.reinit(reqId, reinitPk.ShellType)
|
||||
return
|
||||
}
|
||||
if streamPk, ok := pk.(*packet.StreamFilePacketType); ok {
|
||||
@@ -581,13 +590,7 @@ func (m *MServer) ProcessRpcPacket(pk packet.RpcPacketType) {
|
||||
return
|
||||
}
|
||||
|
||||
func (m *MServer) getCurrentState() (string, *packet.ShellState) {
|
||||
m.Lock.Lock()
|
||||
defer m.Lock.Unlock()
|
||||
return m.CurrentState, m.StateMap[m.CurrentState]
|
||||
}
|
||||
|
||||
func (m *MServer) clientPacketCallback(pk packet.PacketType) {
|
||||
func (m *MServer) clientPacketCallback(shellType string, pk packet.PacketType) {
|
||||
if pk.GetType() != packet.CmdDonePacketStr {
|
||||
return
|
||||
}
|
||||
@@ -595,16 +598,25 @@ func (m *MServer) clientPacketCallback(pk packet.PacketType) {
|
||||
if donePk.FinalState == nil {
|
||||
return
|
||||
}
|
||||
stateHash, curState := m.getCurrentState()
|
||||
stateHash, curState := m.StateMap.GetCurrentState(shellType)
|
||||
if curState == nil {
|
||||
return
|
||||
}
|
||||
diff, err := shexec.MakeShellStateDiff(*curState, stateHash, *donePk.FinalState)
|
||||
sapi, err := shellapi.MakeShellApi(curState.GetShellType())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
diff, err := sapi.MakeShellStateDiff(curState, stateHash, donePk.FinalState)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
donePk.FinalState = nil
|
||||
donePk.FinalStateDiff = &diff
|
||||
donePk.FinalStateDiff = diff
|
||||
}
|
||||
|
||||
func (m *MServer) isShellInitialized(shellType string) bool {
|
||||
_, curState := m.StateMap.GetCurrentState(shellType)
|
||||
return curState != nil
|
||||
}
|
||||
|
||||
func (m *MServer) runCommand(runPacket *packet.RunPacketType) {
|
||||
@@ -612,7 +624,29 @@ func (m *MServer) runCommand(runPacket *packet.RunPacketType) {
|
||||
m.Sender.SendErrorResponse(runPacket.ReqId, fmt.Errorf("server run packets require valid ck: %s", err))
|
||||
return
|
||||
}
|
||||
ecmd, err := shexec.SSHOpts{}.MakeMShellSingleCmd(true)
|
||||
if runPacket.ShellType == "" {
|
||||
m.Sender.SendErrorResponse(runPacket.ReqId, fmt.Errorf("server run packets require shell type"))
|
||||
return
|
||||
}
|
||||
_, curInitState := m.StateMap.GetCurrentState(runPacket.ShellType)
|
||||
if curInitState == nil {
|
||||
m.Sender.SendErrorResponse(runPacket.ReqId, fmt.Errorf("shell type %q is not initialized", runPacket.ShellType))
|
||||
return
|
||||
}
|
||||
if runPacket.State == nil {
|
||||
m.Sender.SendErrorResponse(runPacket.ReqId, fmt.Errorf("server run packets require state"))
|
||||
return
|
||||
}
|
||||
_, _, err := packet.ParseShellStateVersion(runPacket.State.Version)
|
||||
if err != nil {
|
||||
m.Sender.SendErrorResponse(runPacket.ReqId, fmt.Errorf("invalid shellstate version: %w", err))
|
||||
return
|
||||
}
|
||||
if !packet.StateVersionsCompatible(runPacket.State.Version, curInitState.Version) {
|
||||
m.Sender.SendErrorResponse(runPacket.ReqId, fmt.Errorf("shellstate version %q is not compatible with current shell version %q", runPacket.State.Version, curInitState.Version))
|
||||
return
|
||||
}
|
||||
ecmd, err := shexec.MakeMShellSingleCmd()
|
||||
if err != nil {
|
||||
m.Sender.SendErrorResponse(runPacket.ReqId, fmt.Errorf("server run packets require valid ck: %s", err))
|
||||
return
|
||||
@@ -640,7 +674,9 @@ func (m *MServer) runCommand(runPacket *packet.RunPacketType) {
|
||||
cproc.Close()
|
||||
}()
|
||||
shexec.SendRunPacketAndRunData(context.Background(), cproc.Input, runPacket)
|
||||
cproc.ProxySingleOutput(runPacket.CK, m.Sender, m.clientPacketCallback)
|
||||
cproc.ProxySingleOutput(runPacket.CK, m.Sender, func(pk packet.PacketType) {
|
||||
m.clientPacketCallback(runPacket.ShellType, pk)
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -699,7 +735,7 @@ func RunServer() (int, error) {
|
||||
server := &MServer{
|
||||
Lock: &sync.Mutex{},
|
||||
ClientMap: make(map[base.CommandKey]*shexec.ClientProc),
|
||||
StateMap: make(map[string]*packet.ShellState),
|
||||
StateMap: MakeShellStateMap(),
|
||||
Debug: debug,
|
||||
WriteErrorCh: make(chan bool),
|
||||
WriteErrorChOnce: &sync.Once{},
|
||||
@@ -725,7 +761,6 @@ func RunServer() (int, error) {
|
||||
if err != nil {
|
||||
return 1, err
|
||||
}
|
||||
server.setCurrentState(initPacket.State)
|
||||
server.Sender.SendPacket(initPacket)
|
||||
ticker := time.NewTicker(1 * time.Minute)
|
||||
go func() {
|
||||
@@ -748,3 +783,60 @@ func RunServer() (int, error) {
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func MakeShellStateMap() *ShellStateMap {
|
||||
return &ShellStateMap{
|
||||
Lock: &sync.Mutex{},
|
||||
StateMap: make(map[shellStateMapKey]*packet.ShellState),
|
||||
CurrentStateMap: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
func (sm *ShellStateMap) GetCurrentState(shellType string) (string, *packet.ShellState) {
|
||||
sm.Lock.Lock()
|
||||
defer sm.Lock.Unlock()
|
||||
hval := sm.CurrentStateMap[shellType]
|
||||
return hval, sm.StateMap[shellStateMapKey{ShellType: shellType, Hash: hval}]
|
||||
}
|
||||
|
||||
func (sm *ShellStateMap) SetCurrentState(shellType string, state *packet.ShellState) error {
|
||||
if state == nil {
|
||||
return fmt.Errorf("cannot set nil state")
|
||||
}
|
||||
if shellType != state.GetShellType() {
|
||||
return fmt.Errorf("shell type mismatch: %s != %s", shellType, state.GetShellType())
|
||||
}
|
||||
sm.Lock.Lock()
|
||||
defer sm.Lock.Unlock()
|
||||
hval, _ := state.EncodeAndHash()
|
||||
key := shellStateMapKey{ShellType: shellType, Hash: hval}
|
||||
sm.StateMap[key] = state
|
||||
sm.CurrentStateMap[shellType] = hval
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sm *ShellStateMap) GetStateByHash(shellType string, hash string) *packet.ShellState {
|
||||
sm.Lock.Lock()
|
||||
defer sm.Lock.Unlock()
|
||||
return sm.StateMap[shellStateMapKey{ShellType: shellType, Hash: hash}]
|
||||
}
|
||||
|
||||
func (sm *ShellStateMap) Clear() {
|
||||
sm.Lock.Lock()
|
||||
defer sm.Lock.Unlock()
|
||||
sm.StateMap = make(map[shellStateMapKey]*packet.ShellState)
|
||||
sm.CurrentStateMap = make(map[string]string)
|
||||
}
|
||||
|
||||
func (sm *ShellStateMap) GetShells() []string {
|
||||
sm.Lock.Lock()
|
||||
defer sm.Lock.Unlock()
|
||||
return utilfn.GetMapKeys(sm.CurrentStateMap)
|
||||
}
|
||||
|
||||
func (sm *ShellStateMap) HasShell(shellType string) bool {
|
||||
sm.Lock.Lock()
|
||||
defer sm.Lock.Unlock()
|
||||
_, found := sm.CurrentStateMap[shellType]
|
||||
return found
|
||||
}
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package shellapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/alessio/shellescape"
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/packet"
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/shellenv"
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/statediff"
|
||||
)
|
||||
|
||||
const BaseBashOpts = `set +m; set +H; shopt -s extglob`
|
||||
|
||||
const BashShellVersionCmdStr = `echo bash v${BASH_VERSINFO[0]}.${BASH_VERSINFO[1]}.${BASH_VERSINFO[2]}`
|
||||
const RemoteBashPath = "bash"
|
||||
|
||||
// TODO fix bash path in these constants
|
||||
const RunBashSudoCommandFmt = `sudo -n -C %d bash /dev/fd/%d`
|
||||
const RunBashSudoPasswordCommandFmt = `cat /dev/fd/%d | sudo -k -S -C %d bash -c "echo '[from-mshell]'; exec %d>&-; bash /dev/fd/%d < /dev/fd/%d"`
|
||||
|
||||
// do not use these directly, call GetLocalMajorVersion()
|
||||
var localBashMajorVersionOnce = &sync.Once{}
|
||||
var localBashMajorVersion = ""
|
||||
|
||||
// the "exec 2>" line also adds an extra printf at the *beginning* to strip out spurious rc file output
|
||||
var GetBashShellStateCmds = []string{
|
||||
"exec 2> /dev/null;",
|
||||
BashShellVersionCmdStr + ";",
|
||||
`pwd;`,
|
||||
`declare -p $(compgen -A variable);`,
|
||||
`alias -p;`,
|
||||
`declare -f;`,
|
||||
GetGitBranchCmdStr + ";",
|
||||
}
|
||||
|
||||
type bashShellApi struct{}
|
||||
|
||||
func (b bashShellApi) GetShellType() string {
|
||||
return packet.ShellType_bash
|
||||
}
|
||||
|
||||
func (b bashShellApi) MakeExitTrap(fdNum int) string {
|
||||
return MakeBashExitTrap(fdNum)
|
||||
}
|
||||
|
||||
func (b bashShellApi) GetLocalMajorVersion() string {
|
||||
return GetLocalBashMajorVersion()
|
||||
}
|
||||
|
||||
func (b bashShellApi) GetLocalShellPath() string {
|
||||
return GetLocalBashPath()
|
||||
}
|
||||
|
||||
func (b bashShellApi) GetRemoteShellPath() string {
|
||||
return RemoteBashPath
|
||||
}
|
||||
|
||||
func (b bashShellApi) MakeRunCommand(cmdStr string, opts RunCommandOpts) string {
|
||||
if !opts.Sudo {
|
||||
return fmt.Sprintf(RunCommandFmt, cmdStr)
|
||||
}
|
||||
if opts.SudoWithPass {
|
||||
return fmt.Sprintf(RunBashSudoPasswordCommandFmt, opts.PwFdNum, opts.MaxFdNum+1, opts.PwFdNum, opts.CommandFdNum, opts.CommandStdinFdNum)
|
||||
} else {
|
||||
return fmt.Sprintf(RunBashSudoCommandFmt, opts.MaxFdNum+1, opts.CommandFdNum)
|
||||
}
|
||||
}
|
||||
|
||||
func (b bashShellApi) MakeShExecCommand(cmdStr string, rcFileName string, usePty bool) *exec.Cmd {
|
||||
return MakeBashShExecCommand(cmdStr, rcFileName, usePty)
|
||||
}
|
||||
|
||||
func (b bashShellApi) GetShellState() (*packet.ShellState, error) {
|
||||
return GetBashShellState()
|
||||
}
|
||||
|
||||
func (b bashShellApi) GetBaseShellOpts() string {
|
||||
return BaseBashOpts
|
||||
}
|
||||
|
||||
func (b bashShellApi) ParseShellStateOutput(output []byte) (*packet.ShellState, error) {
|
||||
return parseBashShellStateOutput(output)
|
||||
}
|
||||
|
||||
func (b bashShellApi) MakeRcFileStr(pk *packet.RunPacketType) string {
|
||||
var rcBuf bytes.Buffer
|
||||
rcBuf.WriteString(b.GetBaseShellOpts() + "\n")
|
||||
varDecls := shellenv.VarDeclsFromState(pk.State)
|
||||
for _, varDecl := range varDecls {
|
||||
if varDecl.IsExport() || varDecl.IsReadOnly() {
|
||||
continue
|
||||
}
|
||||
rcBuf.WriteString(BashDeclareStmt(varDecl))
|
||||
rcBuf.WriteString("\n")
|
||||
}
|
||||
if pk.State != nil && pk.State.Funcs != "" {
|
||||
rcBuf.WriteString(pk.State.Funcs)
|
||||
rcBuf.WriteString("\n")
|
||||
}
|
||||
if pk.State != nil && pk.State.Aliases != "" {
|
||||
rcBuf.WriteString(pk.State.Aliases)
|
||||
rcBuf.WriteString("\n")
|
||||
}
|
||||
return rcBuf.String()
|
||||
}
|
||||
|
||||
func GetBashShellStateCmd() string {
|
||||
return strings.Join(GetBashShellStateCmds, ` printf "\x00\x00";`)
|
||||
}
|
||||
|
||||
func execGetLocalBashShellVersion() string {
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), GetStateTimeout)
|
||||
defer cancelFn()
|
||||
ecmd := exec.CommandContext(ctx, "bash", "-c", BashShellVersionCmdStr)
|
||||
out, err := ecmd.Output()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
versionStr := strings.TrimSpace(string(out))
|
||||
if strings.Index(versionStr, "bash ") == -1 {
|
||||
// invalid shell version (only bash is supported)
|
||||
return ""
|
||||
}
|
||||
return versionStr
|
||||
}
|
||||
|
||||
func GetLocalBashMajorVersion() string {
|
||||
localBashMajorVersionOnce.Do(func() {
|
||||
fullVersion := execGetLocalBashShellVersion()
|
||||
localBashMajorVersion = packet.GetMajorVersion(fullVersion)
|
||||
})
|
||||
return localBashMajorVersion
|
||||
}
|
||||
|
||||
func GetBashShellState() (*packet.ShellState, error) {
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), GetStateTimeout)
|
||||
defer cancelFn()
|
||||
cmdStr := BaseBashOpts + "; " + GetBashShellStateCmd()
|
||||
ecmd := exec.CommandContext(ctx, GetLocalBashPath(), "-l", "-i", "-c", cmdStr)
|
||||
outputBytes, err := RunSimpleCmdInPty(ecmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parseBashShellStateOutput(outputBytes)
|
||||
}
|
||||
|
||||
func GetLocalBashPath() string {
|
||||
if runtime.GOOS == "darwin" {
|
||||
macShell := GetMacUserShell()
|
||||
if strings.Index(macShell, "bash") != -1 {
|
||||
return shellescape.Quote(macShell)
|
||||
}
|
||||
}
|
||||
return "bash"
|
||||
}
|
||||
|
||||
func GetLocalZshPath() string {
|
||||
if runtime.GOOS == "darwin" {
|
||||
macShell := GetMacUserShell()
|
||||
if strings.Index(macShell, "zsh") != -1 {
|
||||
return shellescape.Quote(macShell)
|
||||
}
|
||||
}
|
||||
return "zsh"
|
||||
}
|
||||
|
||||
func GetBashShellStateRedirectCommandStr(outputFdNum int) string {
|
||||
return fmt.Sprintf("cat <(%s) > /dev/fd/%d", GetBashShellStateCmd(), outputFdNum)
|
||||
}
|
||||
|
||||
func MakeBashExitTrap(fdNum int) string {
|
||||
stateCmd := GetBashShellStateRedirectCommandStr(fdNum)
|
||||
fmtStr := `
|
||||
_waveshell_exittrap () {
|
||||
%s
|
||||
}
|
||||
trap _waveshell_exittrap EXIT
|
||||
`
|
||||
return fmt.Sprintf(fmtStr, stateCmd)
|
||||
}
|
||||
|
||||
func MakeBashShExecCommand(cmdStr string, rcFileName string, usePty bool) *exec.Cmd {
|
||||
if usePty {
|
||||
return exec.Command(GetLocalBashPath(), "--rcfile", rcFileName, "-i", "-c", cmdStr)
|
||||
} else {
|
||||
return exec.Command(GetLocalBashPath(), "--rcfile", rcFileName, "-c", cmdStr)
|
||||
}
|
||||
}
|
||||
|
||||
func (bashShellApi) MakeShellStateDiff(oldState *packet.ShellState, oldStateHash string, newState *packet.ShellState) (*packet.ShellStateDiff, error) {
|
||||
if oldState == nil {
|
||||
return nil, fmt.Errorf("cannot diff, oldState is nil")
|
||||
}
|
||||
if newState == nil {
|
||||
return nil, fmt.Errorf("cannot diff, newState is nil")
|
||||
}
|
||||
if !packet.StateVersionsCompatible(oldState.Version, newState.Version) {
|
||||
return nil, fmt.Errorf("cannot diff, incompatible shell versions: %q %q", oldState.Version, newState.Version)
|
||||
}
|
||||
rtn := &packet.ShellStateDiff{}
|
||||
rtn.BaseHash = oldStateHash
|
||||
rtn.Version = newState.Version // always set version in the diff
|
||||
if oldState.Cwd != newState.Cwd {
|
||||
rtn.Cwd = newState.Cwd
|
||||
}
|
||||
rtn.Error = newState.Error
|
||||
oldVars := shellenv.ShellStateVarsToMap(oldState.ShellVars)
|
||||
newVars := shellenv.ShellStateVarsToMap(newState.ShellVars)
|
||||
rtn.VarsDiff = statediff.MakeMapDiff(oldVars, newVars)
|
||||
rtn.AliasesDiff = statediff.MakeLineDiff(oldState.Aliases, newState.Aliases, oldState.GetLineDiffSplitString())
|
||||
rtn.FuncsDiff = statediff.MakeLineDiff(oldState.Funcs, newState.Funcs, oldState.GetLineDiffSplitString())
|
||||
return rtn, nil
|
||||
}
|
||||
|
||||
func (bashShellApi) ApplyShellStateDiff(oldState *packet.ShellState, diff *packet.ShellStateDiff) (*packet.ShellState, error) {
|
||||
if oldState == nil {
|
||||
return nil, fmt.Errorf("cannot apply diff, oldState is nil")
|
||||
}
|
||||
if diff == nil {
|
||||
return oldState, nil
|
||||
}
|
||||
rtnState := &packet.ShellState{}
|
||||
var err error
|
||||
rtnState.Version = oldState.Version
|
||||
// work around a bug (before v0.6.0) where version could be invalid.
|
||||
// so only overwrite the oldversion if diff version is valid
|
||||
_, _, diffVersionErr := packet.ParseShellStateVersion(diff.Version)
|
||||
if diffVersionErr == nil {
|
||||
rtnState.Version = diff.Version
|
||||
}
|
||||
rtnState.Cwd = oldState.Cwd
|
||||
if diff.Cwd != "" {
|
||||
rtnState.Cwd = diff.Cwd
|
||||
}
|
||||
rtnState.Error = diff.Error
|
||||
oldVars := shellenv.ShellStateVarsToMap(oldState.ShellVars)
|
||||
newVars, err := statediff.ApplyMapDiff(oldVars, diff.VarsDiff)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("applying mapdiff 'vars': %v", err)
|
||||
}
|
||||
rtnState.ShellVars = shellenv.StrMapToShellStateVars(newVars)
|
||||
rtnState.Aliases, err = statediff.ApplyLineDiff(oldState.Aliases, diff.AliasesDiff)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("applying diff 'aliases': %v", err)
|
||||
}
|
||||
rtnState.Funcs, err = statediff.ApplyLineDiff(oldState.Funcs, diff.FuncsDiff)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("applying diff 'funcs': %v", err)
|
||||
}
|
||||
return rtnState, nil
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package shellapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/alessio/shellescape"
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/packet"
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/shellenv"
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/utilfn"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbase"
|
||||
"mvdan.cc/sh/v3/expand"
|
||||
"mvdan.cc/sh/v3/syntax"
|
||||
)
|
||||
|
||||
type DeclareDeclType = shellenv.DeclareDeclType
|
||||
|
||||
func doCmdSubst(commandStr string, w io.Writer, word *syntax.CmdSubst) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func doProcSubst(w *syntax.ProcSubst) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
type bashParseEnviron struct {
|
||||
Env map[string]string
|
||||
}
|
||||
|
||||
func (e *bashParseEnviron) Get(name string) expand.Variable {
|
||||
val, ok := e.Env[name]
|
||||
if !ok {
|
||||
return expand.Variable{}
|
||||
}
|
||||
return expand.Variable{
|
||||
Exported: true,
|
||||
Kind: expand.String,
|
||||
Str: val,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *bashParseEnviron) Each(fn func(name string, vr expand.Variable) bool) {
|
||||
for key := range e.Env {
|
||||
rtn := fn(key, e.Get(key))
|
||||
if !rtn {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func GetParserConfig(envMap map[string]string) *expand.Config {
|
||||
cfg := &expand.Config{
|
||||
Env: &bashParseEnviron{Env: envMap},
|
||||
GlobStar: false,
|
||||
NullGlob: false,
|
||||
NoUnset: false,
|
||||
CmdSubst: func(w io.Writer, word *syntax.CmdSubst) error { return doCmdSubst("", w, word) },
|
||||
ProcSubst: doProcSubst,
|
||||
ReadDir: nil,
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// https://wiki.bash-hackers.org/syntax/shellvars
|
||||
var BashNoStoreVarNames = map[string]bool{
|
||||
"BASH": true,
|
||||
"BASHOPTS": true,
|
||||
"BASHPID": true,
|
||||
"BASH_ALIASES": true,
|
||||
"BASH_ARGC": true,
|
||||
"BASH_ARGV": true,
|
||||
"BASH_ARGV0": true,
|
||||
"BASH_CMDS": true,
|
||||
"BASH_COMMAND": true,
|
||||
"BASH_EXECUTION_STRING": true,
|
||||
"LINENO": true,
|
||||
"BASH_LINENO": true,
|
||||
"BASH_REMATCH": true,
|
||||
"BASH_SOURCE": true,
|
||||
"BASH_SUBSHELL": true,
|
||||
"COPROC": true,
|
||||
"DIRSTACK": true,
|
||||
"EPOCHREALTIME": true,
|
||||
"EPOCHSECONDS": true,
|
||||
"FUNCNAME": true,
|
||||
"HISTCMD": true,
|
||||
"OLDPWD": true,
|
||||
"PIPESTATUS": true,
|
||||
"PPID": true,
|
||||
"PWD": true,
|
||||
"RANDOM": true,
|
||||
"SECONDS": true,
|
||||
"SHLVL": true,
|
||||
"HISTFILE": true,
|
||||
"HISTFILESIZE": true,
|
||||
"HISTCONTROL": true,
|
||||
"HISTIGNORE": true,
|
||||
"HISTSIZE": true,
|
||||
"HISTTIMEFORMAT": true,
|
||||
"SRANDOM": true,
|
||||
"COLUMNS": true,
|
||||
"LINES": true,
|
||||
|
||||
// we want these in our remote state object
|
||||
// "EUID": true,
|
||||
// "SHELLOPTS": true,
|
||||
// "UID": true,
|
||||
// "BASH_VERSINFO": true,
|
||||
// "BASH_VERSION": true,
|
||||
}
|
||||
|
||||
var declareDeclArgsRe = regexp.MustCompile("^[aAxrifx]*$")
|
||||
var bashValidIdentifierRe = regexp.MustCompile("^[a-zA-Z_][a-zA-Z0-9_]*$")
|
||||
|
||||
func bashValidate(d *DeclareDeclType) error {
|
||||
if len(d.Name) == 0 || !isValidBashIdentifier(d.Name) {
|
||||
return fmt.Errorf("invalid shell variable name (invalid bash identifier)")
|
||||
}
|
||||
if strings.Index(d.Value, "\x00") >= 0 {
|
||||
return fmt.Errorf("invalid shell variable value (cannot contain 0 byte)")
|
||||
}
|
||||
if !declareDeclArgsRe.MatchString(d.Args) {
|
||||
return fmt.Errorf("invalid shell variable type %s", shellescape.Quote(d.Args))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isValidBashIdentifier(s string) bool {
|
||||
return bashValidIdentifierRe.MatchString(s)
|
||||
}
|
||||
|
||||
func bashParseDeclareStmt(stmt *syntax.Stmt, src string) (*DeclareDeclType, error) {
|
||||
cmd := stmt.Cmd
|
||||
decl, ok := cmd.(*syntax.DeclClause)
|
||||
if !ok || decl.Variant.Value != "declare" || len(decl.Args) != 2 {
|
||||
return nil, fmt.Errorf("invalid declare variant")
|
||||
}
|
||||
rtn := &DeclareDeclType{}
|
||||
declArgs := decl.Args[0]
|
||||
if !declArgs.Naked || len(declArgs.Value.Parts) != 1 {
|
||||
return nil, fmt.Errorf("wrong number of declare args parts")
|
||||
}
|
||||
declArgsLit, ok := declArgs.Value.Parts[0].(*syntax.Lit)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("declare args is not a literal")
|
||||
}
|
||||
if !strings.HasPrefix(declArgsLit.Value, "-") {
|
||||
return nil, fmt.Errorf("declare args not an argument (does not start with '-')")
|
||||
}
|
||||
if declArgsLit.Value == "--" {
|
||||
rtn.Args = ""
|
||||
} else {
|
||||
rtn.Args = declArgsLit.Value[1:]
|
||||
}
|
||||
declAssign := decl.Args[1]
|
||||
if declAssign.Name == nil {
|
||||
return nil, fmt.Errorf("declare does not have a valid name")
|
||||
}
|
||||
rtn.Name = declAssign.Name.Value
|
||||
if declAssign.Naked || declAssign.Index != nil || declAssign.Append {
|
||||
return nil, fmt.Errorf("invalid decl format")
|
||||
}
|
||||
if declAssign.Value != nil {
|
||||
rtn.Value = string(src[declAssign.Value.Pos().Offset():declAssign.Value.End().Offset()])
|
||||
} else if declAssign.Array != nil {
|
||||
rtn.Value = string(src[declAssign.Array.Pos().Offset():declAssign.Array.End().Offset()])
|
||||
} else {
|
||||
return nil, fmt.Errorf("invalid decl, not plain value or array")
|
||||
}
|
||||
err := bashNormalize(rtn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = bashValidate(rtn); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rtn, nil
|
||||
}
|
||||
|
||||
func bashParseDeclareOutput(state *packet.ShellState, declareBytes []byte, pvarBytes []byte) error {
|
||||
declareStr := string(declareBytes)
|
||||
r := bytes.NewReader(declareBytes)
|
||||
parser := syntax.NewParser(syntax.Variant(syntax.LangBash))
|
||||
file, err := parser.Parse(r, "aliases")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var firstParseErr error
|
||||
declMap := make(map[string]*DeclareDeclType)
|
||||
for _, stmt := range file.Stmts {
|
||||
decl, err := bashParseDeclareStmt(stmt, declareStr)
|
||||
if err != nil {
|
||||
if firstParseErr == nil {
|
||||
firstParseErr = err
|
||||
}
|
||||
}
|
||||
if decl != nil && !BashNoStoreVarNames[decl.Name] {
|
||||
declMap[decl.Name] = decl
|
||||
}
|
||||
}
|
||||
pvarMap := parsePVarOutput(pvarBytes, false)
|
||||
utilfn.CombineMaps(declMap, pvarMap)
|
||||
state.ShellVars = shellenv.SerializeDeclMap(declMap) // this writes out the decls in a canonical order
|
||||
if firstParseErr != nil {
|
||||
state.Error = firstParseErr.Error()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseBashShellStateOutput(outputBytes []byte) (*packet.ShellState, error) {
|
||||
if scbase.IsDevMode() && DebugState {
|
||||
writeStateToFile(packet.ShellType_bash, outputBytes)
|
||||
}
|
||||
// 7 fields: ignored [0], version [1], cwd [2], env/vars [3], aliases [4], funcs [5], pvars [6]
|
||||
fields := bytes.Split(outputBytes, []byte{0, 0})
|
||||
if len(fields) != 7 {
|
||||
return nil, fmt.Errorf("invalid bash shell state output, wrong number of fields, fields=%d", len(fields))
|
||||
}
|
||||
rtn := &packet.ShellState{}
|
||||
rtn.Version = strings.TrimSpace(string(fields[1]))
|
||||
if rtn.GetShellType() != packet.ShellType_bash {
|
||||
return nil, fmt.Errorf("invalid bash shell state output, wrong shell type: %q", rtn.Version)
|
||||
}
|
||||
if _, _, err := packet.ParseShellStateVersion(rtn.Version); err != nil {
|
||||
return nil, fmt.Errorf("invalid bash shell state output, invalid version: %v", err)
|
||||
}
|
||||
cwdStr := string(fields[2])
|
||||
if strings.HasSuffix(cwdStr, "\r\n") {
|
||||
cwdStr = cwdStr[0 : len(cwdStr)-2]
|
||||
} else if strings.HasSuffix(cwdStr, "\n") {
|
||||
cwdStr = cwdStr[0 : len(cwdStr)-1]
|
||||
}
|
||||
rtn.Cwd = string(cwdStr)
|
||||
err := bashParseDeclareOutput(rtn, fields[3], fields[6])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rtn.Aliases = strings.ReplaceAll(string(fields[4]), "\r\n", "\n")
|
||||
rtn.Funcs = strings.ReplaceAll(string(fields[5]), "\r\n", "\n")
|
||||
rtn.Funcs = shellenv.RemoveFunc(rtn.Funcs, "_waveshell_exittrap")
|
||||
return rtn, nil
|
||||
}
|
||||
|
||||
func bashNormalize(d *DeclareDeclType) error {
|
||||
if d.DataType() == shellenv.DeclTypeAssocArray {
|
||||
return bashNormalizeAssocArrayDecl(d)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// normalizes order of assoc array keys so value is stable
|
||||
func bashNormalizeAssocArrayDecl(d *DeclareDeclType) error {
|
||||
if d.DataType() != shellenv.DeclTypeAssocArray {
|
||||
return fmt.Errorf("invalid decltype passed to assocArrayDeclToStr: %s", d.DataType())
|
||||
}
|
||||
varMap, err := bashAssocArrayVarToMap(d)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
keys := make([]string, 0, len(varMap))
|
||||
for key := range varMap {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
var buf bytes.Buffer
|
||||
buf.WriteByte('(')
|
||||
for _, key := range keys {
|
||||
buf.WriteByte('[')
|
||||
buf.WriteString(key)
|
||||
buf.WriteByte(']')
|
||||
buf.WriteByte('=')
|
||||
buf.WriteString(varMap[key])
|
||||
buf.WriteByte(' ')
|
||||
}
|
||||
buf.WriteByte(')')
|
||||
d.Value = buf.String()
|
||||
return nil
|
||||
}
|
||||
|
||||
func bashAssocArrayVarToMap(d *DeclareDeclType) (map[string]string, error) {
|
||||
if d.DataType() != shellenv.DeclTypeAssocArray {
|
||||
return nil, fmt.Errorf("decl is not an assoc-array")
|
||||
}
|
||||
refStr := "X=" + d.Value
|
||||
r := strings.NewReader(refStr)
|
||||
parser := syntax.NewParser(syntax.Variant(syntax.LangBash))
|
||||
file, err := parser.Parse(r, "assocdecl")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(file.Stmts) != 1 {
|
||||
return nil, fmt.Errorf("invalid assoc-array parse (multiple stmts)")
|
||||
}
|
||||
stmt := file.Stmts[0]
|
||||
callExpr, ok := stmt.Cmd.(*syntax.CallExpr)
|
||||
if !ok || len(callExpr.Args) != 0 || len(callExpr.Assigns) != 1 {
|
||||
return nil, fmt.Errorf("invalid assoc-array parse (bad expr)")
|
||||
}
|
||||
assign := callExpr.Assigns[0]
|
||||
arrayExpr := assign.Array
|
||||
if arrayExpr == nil {
|
||||
return nil, fmt.Errorf("invalid assoc-array parse (no array expr)")
|
||||
}
|
||||
rtn := make(map[string]string)
|
||||
for _, elem := range arrayExpr.Elems {
|
||||
indexStr := refStr[elem.Index.Pos().Offset():elem.Index.End().Offset()]
|
||||
valStr := refStr[elem.Value.Pos().Offset():elem.Value.End().Offset()]
|
||||
rtn[indexStr] = valStr
|
||||
}
|
||||
return rtn, nil
|
||||
}
|
||||
|
||||
func BashDeclareStmt(d *DeclareDeclType) string {
|
||||
var argsStr string
|
||||
if d.Args == "" {
|
||||
argsStr = "--"
|
||||
} else {
|
||||
argsStr = "-" + d.Args
|
||||
}
|
||||
return fmt.Sprintf("declare %s %s=%s", argsStr, d.Name, d.Value)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user