jeanp413 / open-remote-ssh

VSCode Remote Development: Open any folder on a remote machine using SSH.
MIT License
309 stars 43 forks source link

Cannot install server on host whose default shell is tcsh #100

Open luozf14 opened 1 year ago

luozf14 commented 1 year ago

I was trying to connect my server whose default shell is tcsh. The installation of open remote-ssh server was failed and gave me the following output.


[Trace  - 21:20:46.517] Server install command stderr:
Unmatched '.
Bad : modifier in $ (-).
DISTRO_VERSION=1.79.2: Command not found.
DISTRO_COMMIT=19c0f5cefaeb2637b45a9c7a356151fcfdcda1e9: Command not found.
DISTRO_QUALITY=stable: Command not found.
DISTRO_VSCODIUM_RELEASE=23166: Command not found.
SERVER_APP_NAME=codium-server: Command not found.
SERVER_INITIAL_EXTENSIONS=: Command not found.
SERVER_LISTEN_FLAG=--port=0: Command not found.
SERVER_DATA_DIR=/home/grgroup/luozf/.vscodium-server: Command not found.
SERVER_DATA_DIR: Undefined variable.
SERVER_DIR: Undefined variable.
SERVER_DATA_DIR: Undefined variable.
SERVER_DATA_DIR: Undefined variable.
SERVER_DATA_DIR: Undefined variable.
SERVER_OS=: Command not found.
SERVER_ARCH=: Command not found.
SERVER_CONNECTION_TOKEN=: Command not found.
SERVER_DOWNLOAD_URL=: Command not found.
LISTENING_ON=: Command not found.
OS_RELEASE_ID=: Command not found.
ARCH=: Command not found.
PLATFORM=: Command not found.
Badly placed ()'s.
LISTENING_ON: Undefined variable.
SERVER_CONNECTION_TOKEN: Undefined variable.
SERVER_LOGFILE: Undefined variable.
OS_RELEASE_ID: Undefined variable.
ARCH: Undefined variable.
PLATFORM: Undefined variable.
TMP_DIR: Undefined variable.
}: Command not found.
Illegal variable name.
PLATFORM: Undefined variable.
Too many )'s.
SERVER_OS=darwin: Command not found.
Too many )'s.
SERVER_OS=linux: Command not found.
Too many )'s.
SERVER_OS=freebsd: Command not found.
Too many )'s.
SERVER_OS=dragonfly: Command not found.
Too many )'s.
PLATFORM: Undefined variable.
print_install_results_and_exit: Command not found.
esac: Command not found.
Illegal variable name.
ARCH: Undefined variable.
Too many )'s.
SERVER_ARCH=x64: Command not found.
Too many )'s.
SERVER_ARCH=armhf: Command not found.
Too many )'s.
SERVER_ARCH=arm64: Command not found.
Too many )'s.
SERVER_ARCH=ppc64le: Command not found.
Too many )'s.
ARCH: Undefined variable.
print_install_results_and_exit: Command not found.
esac: Command not found.
Illegal variable name.
OS_RELEASE_ID: Undefined variable.
Illegal variable name.
OS_RELEASE_ID: Undefined variable.
OS_RELEASE_ID=unknown: Command not found.
fi: Command not found.
fi: Command not found.
SERVER_DIR: Undefined variable.
SERVER_DIR: Undefined variable.
if: Empty if.
then: Command not found.
print_install_results_and_exit: Command not found.
fi: Command not found.
fi: Command not found.
Illegal variable name.
SERVER_SCRIPT: Undefined variable.
SERVER_OS: Undefined variable.
SERVER_OS: Undefined variable.
print_install_results_and_exit: Command not found.
fi: Command not found.
SERVER_DIR: Undefined variable.
Illegal variable name.
SERVER_DOWNLOAD_URL: Undefined variable.
Illegal variable name.
SERVER_DOWNLOAD_URL: Undefined variable.
else: endif not found.

[Trace  - 21:20:46.517] Server install command stdout:
a30bc44d8f9679d54a4cb60a: start
exitCode====
a30bc44d8f9679d54a4cb60a: end
Error creating server install directory

[Error  - 21:20:46.518] Error resolving authority
Error: Couldn't install vscode server on remote server, install script returned non-zero exit status
    at t.installCodeServer (/home/xxx/.vscode-oss/extensions/jeanp413.open-remote-ssh-0.0.39/out/extension.js:1:423710)
    at process.processTicksAndRejections (node:internal/process/task_queues:96:5)
    at async /home/xxx/.vscode-oss/extensions/jeanp413.open-remote-ssh-0.0.39/out/extension.js:1:387702

It looks like the install script is written for bash. Any way to add support for tcsh?

MuntashirAkon commented 1 year ago

Same issue.

kwshi commented 1 week ago

I just ran into this issue. Digging through the source code I think that the install script being written for bash is not the cause of the error per se. In the sense that, so long as the remote host has bash installed then the script should work fine, because we are explicitly invoking bash to run the install script:

https://github.com/jeanp413/open-remote-ssh/blob/cc266b7d3dd2dde3d68f52c2e491c10542bc10ba/src/serverSetup.ts#L138

I think the real cause of this specific error (starting with Unmatched '.) is incompatible shell quoting syntax.

For context (at least this is my understanding), SSH can't directly pass arguments when running remote commands (a la child_process.spawn); instead everything is run as a shell command (a la child_process.exec, or sh -c) using the default shell specified in the remote environment variable $SHELL (that is, according to this StackOverflow discussion).

The code we use here accounts for that by manually shell-quoting/escaping the installServerScript argument; that's what the

`bash -c '${installServerScript.replace(/'/g, `'\\''`)}'`

business is all about.

Here's where the problem lies: different shells have (subtly) different quoting/escaping rules. In POSIX-compatible shells, this works fine because single-quoted strings preserve every character literally, with the only exception of single-quotes, hence the '\\'': close the string, then explicitly escape a single-quote-character, then reopen the string. Crucially, this includes newlines: multiline strings are OK in POSIX sh:

# this works just fine!
echo 'hello
world'

Unfortunately, tcsh is not POSIX-compatible. tcsh requires newlines to be escaped with \ in order to preserve them literally (see the "Lexical structure" section of the tcsh manpage). Thus

echo 'hello
world'

errors out with the message Unmatched '. (in newer versions the error message might be Unmatched '''? same idea though), which is exactly what we're seeing here. Instead, we'd have to do:

echo 'hello\
world'

one more caveat

There's one other discrepancy, which is that tcsh additionally allows "history substitution" inside single-quoted strings (again, see the tcsh man page); these begin with the special character !, so as long as we escape those also then I think we're fine.

Also, since backslashes now have special meaning inside strings as the "escape" character, literal backslashes also need to be escaped.

proposed fix

It looks like there's a PR already (#132) to address this problem generally by allowing custom install scripts for different platforms, languages, etc. But a more localized/ad-hoc/temporary/easy workaround is to just fix the quoting without rewriting the entire install script. Provided we can detect what shell we're running in upon establishing the SSH connection, we can then apply the right quoting rules accordingly:

// POSIX-style
`bash -c '${installServerScript.replace(/'/g, `'\\''`)}'`
// tcsh-style
`bash -c '${installServerScript.replace(/'/g, `'\\''`).replace(/[!\n\\]/, '\\$&')}'`

This takes each special character inside a single-quoted string, i.e. \ ! and newlines, and inserts a backslash before it.

As an aside, notice that the install script itself is still written and running in bash; the only thing we're changing is the syntax of how we're quoting the install-script argument when invoking bash. Because that's the part that depends on the default environment in the SSH remote host.