Ammonite
Ammonite copied to clipboard
bash: /dev/tty: Device not configured
I'm trying to embed Ammonite into one of my programs, but always run into the same problem.
Running the standalone binary works just fine:
$ ammonite-repl-0.5.0-2.11.7
Loading...
Welcome to the Ammonite Repl 0.5.0
(Scala 2.11.7 Java 1.8.0_60)
@
However, including ammonite-repl and using ammonite.repl.Repl.debug, or including ammonite-sshd and starting a SshdRepl, it fails with bash: /dev/tty: Device not configured:
(Scala 2.11.6 Java 1.8.0_60)
bash: /dev/tty: Device not configured
bash: /dev/tty: Device not configured
java.lang.RuntimeException: Nonzero exit value: 1
at scala.sys.package$.error(package.scala:27)
at scala.sys.process.ProcessBuilderImpl$AbstractBuilder.slurp(ProcessBuilderImpl.scala:132)
at scala.sys.process.ProcessBuilderImpl$AbstractBuilder.$bang$bang(ProcessBuilderImpl.scala:102)
at ammonite.terminal.TTY$.stty(Utils.scala:85)
at ammonite.terminal.TTY$.init(Utils.scala:64)
at ammonite.terminal.TermCore$.x$6$lzycompute$1(TermCore.scala:242)
at ammonite.terminal.TermCore$.x$6$1(TermCore.scala:242)
at ammonite.terminal.TermCore$.initialConfig$lzycompute$1(TermCore.scala:242)
at ammonite.terminal.TermCore$.initialConfig$1(TermCore.scala:242)
at ammonite.terminal.TermCore$.readLine(TermCore.scala:252)
at ammonite.repl.frontend.AmmoniteFrontEnd.readLine(AmmoniteFrontEnd.scala:110)
at ammonite.repl.frontend.AmmoniteFrontEnd.action(AmmoniteFrontEnd.scala:25)
at ammonite.repl.Repl.action(Repl.scala:45)
at ammonite.repl.Repl.loop$1(Repl.scala:78)
at ammonite.repl.Repl.run(Repl.scala:96)
at ammonite.repl.Repl$.debug(Repl.scala:177)
$ ssh -p 2222 repl@localhost
Password authentication
Password:
Welcome to the Ammonite Repl 0.5.0
(Scala 2.11.6 Java 1.8.0_60)
bash: /dev/tty: Device not configured
bash: /dev/tty: Device not configured
Connection to localhost closed.
Info:
$ bash -c "/bin/stty -g < /dev/tty"
gfmt1:cflag=4b00:iflag=6b02:lflag=200005cf:oflag=3:discard=f:dsusp=19:eof=4:eol=ff:eol2=ff:erase=7f:intr=3:kill=15:lnext=16:min=1:quit=1c:reprint=12:start=11:status=14:stop=13:susp=1a:time=0:werase=17:ispeed=38400:ospeed=38400
Mac OS X 10.10.5
Hi,
I got the same error if I ran the app inside IntelliJ IDEA. If I run it from command line, it runs ok.
Same for me. IntelliJ 2016.1 on mac.
Still broken on IntelliJ 2016.3. Is this maybe a JLine issue?
I was having the same problem on 0.8.0 but now with ammonite and ammonite-sshd 0.8.1 it seems to be working (running in Intellij 2016.2 in Linux). I did have to set the repl.frontEnd. Here's what works for me:
object TestRepl {
def main(args: Array[String]): Unit = {
val server = new ammonite.sshd.SshdRepl(
SshServerConfig(
address = "localhost",
port = 2222,
username = "repl",
password = "test"
),
predef = "repl.frontEnd() = ammonite.repl.FrontEnd.JLineUnix"
)
server.start()
}
}
$ ssh repl@localhost -p 2222
Password authentication
Password:
Welcome to the Ammonite Repl 0.8.1
(Scala 2.11.8 Java 1.8.0_102)
@ java.util.Calendar.getInstance().getTime()
res0: java.util.Date = Thu Dec 15 11:03:58 GMT 2016
@
jline 3 has JNI/JNA bindings to appropriate windows-functionality (https://github.com/jline/jline3/blob/master/src/main/java/org/jline/terminal/impl/jna/win/JnaWinSysTerminal.java). Is it (jline-3) stable enough to start using it for Ammonite ???
I ran into a variation of this (bash: /dev/tty: No such device or address) when running Ammonite-SSHD inside a container. The problem stems from /dev/tty pointing to the current TTY, but JLine2 uses the real TTY rather than creating a pseudo-terminal. If you execute from a terminal, you're fine, but a background process will cause problems.
The best solution looks like ditching JLine2 for JLine3, which uses pseudo-terminals.
In the meantime, there's no real alternative but to run Ammonite-SSHD from a parent process that provides a terminal (real or pseudo) to it.
Thank you, Tim. BTW - are there any plans/enhancement for upgrading Ammonite to JLine3 ???
@iharh I had a look at it, but it's not a straight replacement. I'm not sure @lihaoyi will want to go to the effort, and I probably won't have time to submit a PR anytime soon.
If you decide to try submitting a PR yourself, I would recommend talking to @lihaoyi on Gitter first.
Hi, some exception occurred with http://ammonite.io/#Embedding when I attempt run:
object TestMain {
def main(args: Array[String]): Unit = {
val hello = "Hello"
// Break into debug REPL with
ammonite.Main(
predefCode = "println(\"Starting Debugging!\")"
).run(
"hello" -> hello,
"fooValue" -> foo()
)
}
def foo() = 1
}
Exception:
bash: /dev/tty: Device not configured
bash: /dev/tty: Device not configured
java.lang.RuntimeException: Nonzero exit value: 1
scala.sys.package$.error(package.scala:27)
scala.sys.process.ProcessBuilderImpl$AbstractBuilder.slurp(ProcessBuilderImpl.scala:134)
scala.sys.process.ProcessBuilderImpl$AbstractBuilder.$bang$bang(ProcessBuilderImpl.scala:104)
ammonite.terminal.TTY$.stty(Utils.scala:118)
ammonite.terminal.TTY$.init(Utils.scala:97)
ammonite.terminal.Terminal$.x$1$lzycompute$1(Terminal.scala:41)
ammonite.terminal.Terminal$.x$1$1(Terminal.scala:41)
ammonite.terminal.Terminal$.initialConfig$lzycompute$1(Terminal.scala:41)
ammonite.terminal.Terminal$.initialConfig$1(Terminal.scala:41)
ammonite.terminal.Terminal$.readLine(Terminal.scala:52)
ammonite.repl.AmmoniteFrontEnd.readLine(AmmoniteFrontEnd.scala:118)
ammonite.repl.AmmoniteFrontEnd.action(AmmoniteFrontEnd.scala:25)
ammonite.repl.Repl.$anonfun$action$1(Repl.scala:148)
ammonite.util.Catching.flatMap(Res.scala:109)
ammonite.repl.Repl.action(Repl.scala:132)
ammonite.repl.Repl.loop$1(Repl.scala:172)
ammonite.repl.Repl.run(Repl.scala:188)
ammonite.Main.$anonfun$run$2(Main.scala:191)
scala.Option.getOrElse(Option.scala:121)
ammonite.Main.run(Main.scala:178)
TestMain$.main(AmmEmbed.scala:13)
TestMain.main(AmmEmbed.scala)
Environment is scala 2.12.3, amm 1.0.1, macOS 10.12.5, local shell is zsh
I have the same problem when making ammonite docker container
Similar problem here when running in current Intellij the standard-test from http://ammonite.io/#Embedding sh: /dev/tty: No such device or address
object TestAmmonite {
def main(args: Array[String]): Unit = {
val hello = "Hello"
// Break into debug REPL with
ammonite.Main(
predefCode = "println(\"Starting Debugging!\")"
).run(
"hello" -> hello,
"fooValue" -> foo()
)
}
def foo() = 1
}
executing this within Intellij gives the following output
Starting Debugging!
Welcome to the Ammonite Repl 1.0.5
(Scala 2.11.12 Java 1.8.0_161)
If you like Ammonite, please support our development at www.patreon.com/lihaoyi
sh: /dev/tty: No such device or address
sh: /dev/tty: No such device or address
java.lang.RuntimeException: Nonzero exit value: 1
scala.sys.package$.error(package.scala:27)
scala.sys.process.ProcessBuilderImpl$AbstractBuilder.slurp(ProcessBuilderImpl.scala:132)
scala.sys.process.ProcessBuilderImpl$AbstractBuilder.$bang$bang(ProcessBuilderImpl.scala:102)
ammonite.terminal.TTY$.stty(Utils.scala:118)
ammonite.terminal.TTY$.init(Utils.scala:97)
ammonite.terminal.Terminal$.x$1$lzycompute$1(Terminal.scala:41)
ammonite.terminal.Terminal$.x$1$1(Terminal.scala:41)
ammonite.terminal.Terminal$.initialConfig$lzycompute$1(Terminal.scala:41)
ammonite.terminal.Terminal$.initialConfig$1(Terminal.scala:41)
ammonite.terminal.Terminal$.readLine(Terminal.scala:52)
ammonite.repl.AmmoniteFrontEnd.readLine(AmmoniteFrontEnd.scala:126)
ammonite.repl.AmmoniteFrontEnd.action(AmmoniteFrontEnd.scala:25)
ammonite.repl.Repl$$anonfun$action$2.apply(Repl.scala:149)
ammonite.repl.Repl$$anonfun$action$2.apply(Repl.scala:141)
ammonite.util.Catching.flatMap(Res.scala:114)
ammonite.repl.Repl.action(Repl.scala:141)
ammonite.repl.Repl.loop$1(Repl.scala:181)
ammonite.repl.Repl.run(Repl.scala:197)
ammonite.Main$$anonfun$run$2.apply(Main.scala:201)
ammonite.Main$$anonfun$run$2.apply(Main.scala:188)
scala.Option.getOrElse(Option.scala:121)
ammonite.Main.run(Main.scala:188)
TestAmmonite$.main(TestAmmonite.scala:7)
TestAmmonite.main(TestAmmonite.scala)
which continues in an infinite loop...
So the text is slightly different, but I guess it is still the same issue sh: /dev/tty: No such device or address
versions: IntelliJ IDEA 2017.3.4 (Community Edition) Build #IC-173.4548.28, built on January 30, 2018 JRE: 1.8.0_152-release-1024-b11 amd64 JVM: OpenJDK 64-Bit Server VM by JetBrains s.r.o Linux 3.10.0-693.17.1.el7.x86_64 ammonite 1.0.5 for Scala 2.11
I get the same error when running inside Singularity container on a slurm cluster (guess it is because of red hat). It works running it on NixOs, but not on Red Hat it seems. Any way of fixing this? It is affecting a lot of people it seems.
I'm seeing the same issue when running within a notebook.
https://gist.github.com/mpkocher/685237b7408b64fde2574f1ea2aed87c
I have encountered this issue also when trying to build docker image. There is a user who has found a workaround apparently and it worked for me. In case anyone can benefit from this also, here is the link: https://github.com/fan-ka-yagi/ammonite/blob/master/2.12.4/Dockerfile#L52
I don't fully understand what is happening there, but seems like some terminal device switching along with saving command history or something. But it works anyway somehow! I don't know if and how this is applicable to Intellij Idea or ssh-related use cases, but perhaps it can give someone an idea where to start from
@fan-ka-yagi Thank you
Yes, it works! I remember that I changed the way of thinking about this problem after struggling with it for a few hours: Just forward the output to another place. :-)
Seeing this in almond jupyter notebooks (example).
scala.sys.process can run ls:
import scala.sys.process._
"ls" lineStream // Stream("tty-err.ipynb")
Ammonite fails:
import ammonite.ops.ImplicitWd.implicitCwd
ammonite.ops.ls!
// sh: /dev/tty: Device not configured
// java.lang.RuntimeException: Nonzero exit value: 1
// scala.sys.package$.error(package.scala:30)
// scala.sys.process.ProcessBuilderImpl$AbstractBuilder.slurp(ProcessBuilderImpl.scala:138)
// scala.sys.process.ProcessBuilderImpl$AbstractBuilder.$bang$bang(ProcessBuilderImpl.scala:108)
// ammonite.terminal.TTY$.consoleDim(Utils.scala:94)
// ammonite.terminal.ConsoleDim.$anonfun$dims$2(ConsoleDim.scala:34)
@ryan-williams Depending on how you run the container you'll need to allocate it a tty, so like -t in docker run,
@dispalt interesting, thanks, but my example is running directly on my macbook, no container involved.
The problem is that the Ammonite FrontEnd is dependent on the tty to grab the width and height, and also does some initialisation code. I hacked around this by creating a custom FrontEnd:
new SshdRepl(
SshServerConfig(
...
),
predef = "repl.frontEnd() = AmmoniteUtil.frontEnd"
)
object AmmoniteUtil {
lazy val frontEnd = new MyAmmoniteFrontEnd()
}
class MyAmmoniteFrontEnd() extends AmmoniteFrontEnd() {
override def width = 160
override def height = 50
override def readLine(reader: java.io.Reader,
output: OutputStream,
prompt: String,
colors: Colors,
compilerComplete: (Int, String) => (Int, Seq[String], Seq[String]),
history: IndexedSeq[String]) = {
val writer = new OutputStreamWriter(output)
val autocompleteFilter: Filter = Filter.action(SpecialKeys.Tab){
case TermState(rest, b, c, _) =>
val (newCursor, completions, details) = compilerComplete(c, b.mkString)
val details2 = for (d <- details) yield {
Highlighter.defaultHighlight(
d.toVector,
colors.comment(),
colors.`type`(),
colors.literal(),
colors.keyword(),
fansi.Attr.Reset
).mkString
}
lazy val common = FrontEndUtils.findPrefix(completions, 0)
val blacklisted = Seq(
"!=",
"==",
"asInstanceOf",
"equals",
"getClass",
"hashCode",
"isInstanceOf",
"toString",
"|>"
)
val completions2 = for(comp <- completions.filterNot(blacklisted.contains)) yield {
val (left, right) = comp.splitAt(common.length)
(colors.comment()(left) ++ right).render
}
val stdout =
FrontEndUtils.printCompletions(completions2, details2)
.mkString
if (details.nonEmpty || completions.isEmpty)
Printing(TermState(rest, b, c), stdout)
else{
val newBuffer = b.take(newCursor) ++ common ++ b.drop(c)
Printing(TermState(rest, newBuffer, newCursor + common.length), stdout)
}
}
// Enter
val multilineFilter = Filter.action(
SpecialKeys.NewLine,
ti => Parsers.split(ti.ts.buffer.mkString).isEmpty
){
case TermState(rest, b, c, _) => BasicFilters.injectNewLine(b, c, rest)
}
val historyFilter = new HistoryFilter(
() => history.reverse, colors.comment()
)
val selectionFilter = GUILikeFilters.SelectionFilter(indent = 2)
val allFilters = Filter.merge(
UndoFilter(),
historyFilter,
extraFilters,
selectionFilter,
GUILikeFilters.altFilter,
GUILikeFilters.fnFilter,
ReadlineFilters.navFilter,
autocompleteFilter,
cutPasteFilter,
multilineFilter,
BasicFilters.all
)
val res = readLineImpl(
prompt,
reader,
writer,
allFilters,
displayTransform = { (buffer, cursor) =>
val indices = Highlighter.defaultHighlightIndices(
buffer,
colors.comment(),
colors.`type`(),
colors.literal(),
colors.keyword(),
fansi.Attr.Reset
)
val highlighted = fansi.Str(Highlighter.flattenIndices(indices, buffer).mkString)
val (newBuffer, offset) = SelectionFilter.mangleBuffer(
selectionFilter, highlighted, cursor, colors.selected()
)
val newNewBuffer = HistoryFilter.mangleBuffer(
historyFilter, newBuffer, cursor, fansi.Underlined.On
)
(newNewBuffer, offset)
}
)
res
}
def readLineImpl(prompt: Prompt,
reader: java.io.Reader,
writer: java.io.Writer,
filters: Filter,
displayTransform: (Vector[Char], Int) => (fansi.Str, Int) = LineReader.noTransform)
: Option[String] = {
new LineReader(160, prompt, reader, writer, filters, displayTransform)
.readChar(TermState(LazyList.continually(reader.read()), Vector.empty, 0, ""), 0)
}
}
+1
WebStorm 2019.1 MacOS 10.14.5
+1
WebStorm 2019.1 MacOS 10.14.5
have you solve this problem ?
Thanks @mark9white for your custom fronted hack.
Since it did not support terminal sizes other than 160x50, I've added on @mark9white hack to support any size and resizing the terminal window. I've also fixed autocompletion. I tested it in a Docker container on Kubernetes.
Apache sshd provides an Environment object when calling Command.start(env: Environment). The environment is updated by sshd when the terminal is resized.
I've modified ShellSession to pass the env to SshdRepl.runRepl().
A variable __sshd_environment is added to replArgs so that it can be passed to the frontend:
"repl.frontEnd() = new SshdAmmoniteFrontend(__sshd_environment)"
I know this is rather hackish, but I'm pretty new to Ammonite. I'm sure there are cleaner ways to pass the environment to the frontend (which should be initialized automatically and not as part of predef).
Maybe @lihaoyi is interested in a PR.
// AmmoniteReplSsh.scala
import java.io.{InputStream, OutputStream, PrintStream}
import ammonite.Main
import ammonite.runtime.Storage
import ammonite.sshd.SshServerConfig
import ammonite.sshd.util.{Environment => AmmoniteEnvironment}
import ammonite.util.{Bind, Colors}
import org.apache.sshd.common.{Factory, FactoryManager}
import org.apache.sshd.server.auth.password.AcceptAllPasswordAuthenticator
import org.apache.sshd.server.{Environment, ExitCallback, SshServer}
object AmmoniteReplSsh {
def main(args: Array[String]): Unit = {
val pwdAuth = AcceptAllPasswordAuthenticator.INSTANCE
val replServer = new SshdRepl(
SshServerConfig(
address = "0.0.0.0",
port = 22222,
passwordAuthenticator = Some(pwdAuth) // or publicKeyAuthenticator
),
predef =
"repl.frontEnd() = new SshdAmmoniteFrontend(__sshd_environment)"
)
replServer.start()
// wait indefinitely here
}
}
/**
* An ssh server which serves ammonite repl as it's shell channel.
* To start listening for incoming connections call
* [[start()]] method. You can [[stop()]] the server at any moment.
* It will also close all running sessions
* @param sshConfig configuration of ssh server,
* such as users credentials or port to be listening to
* @param predef predef that will be installed on repl instances served by this server
* @param replArgs arguments to pass to ammonite repl on initialization of the session;
* an argument named "session" containing the SSHD session will be added
* @param classLoader classloader for ammonite to use
*/
class SshdRepl(sshConfig: SshServerConfig,
predef: String = "",
defaultPredef: Boolean = true,
wd: os.Path = os.pwd,
replArgs: Seq[Bind[_]] = Nil,
classLoader: ClassLoader = SshdRepl.getClass.getClassLoader) {
private lazy val sshd: SshServer = SshServer(
sshConfig,
shellServer = SshdRepl.runRepl(
sshConfig.ammoniteHome,
predef,
defaultPredef,
wd,
replArgs,
classLoader
)
)
def port = sshd.getPort
def start(): Unit = sshd.start()
def stop(): Unit = sshd.stop()
def stopImmediately(): Unit = sshd.stop(true)
}
object SshdRepl {
// Actually runs a repl inside of session serving a remote user shell.
private def runRepl(homePath: os.Path,
predefCode: String,
defaultPredef: Boolean,
wd: os.Path,
replArgs: Seq[Bind[_]],
replServerClassLoader: ClassLoader)
(in: InputStream, out: OutputStream, env: Environment): Unit = {
// since sshd server has it's own customised environment,
// where things like System.out will output to the
// server's console, we need to prepare individual environment
// to serve this particular user's session
AmmoniteEnvironment.withEnvironment(AmmoniteEnvironment(replServerClassLoader, in, out)) {
try {
Main(
predefCode = predefCode,
predefFile = None,
defaultPredef = defaultPredef,
storageBackend = new Storage.Folder(homePath),
wd = wd,
inputStream = in, outputStream = out, errorStream = out,
verboseOutput = false,
remoteLogging = false,
colors = Colors.Default
).run(replArgs :+ Bind("__sshd_environment", env):_*)
} catch {
case any: Throwable =>
val sshClientOutput = new PrintStream(out)
sshClientOutput.println("What a terrible failure, the REPL just blow up!")
any.printStackTrace(sshClientOutput)
}
}
}
}
import java.util.Collections
import org.apache.sshd.agent.SshAgentFactory
import org.apache.sshd.common.file.FileSystemFactory
import org.apache.sshd.common.session.{ConnectionService, Session}
import org.apache.sshd.server.auth.keyboard.DefaultKeyboardInteractiveAuthenticator
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider
import org.apache.sshd.server.{Command, CommandFactory, SshServer => SshServerImpl}
/**
* A factory to simplify creation of ssh server
*/
object SshServer {
def apply(options: SshServerConfig, shellServer: ShellSession.Server) = {
val sshServer = SshServerImpl.setUpDefaultServer()
sshServer.setHost(options.address)
sshServer.setPort(options.port)
options.passwordAuthenticator.foreach { auth =>
sshServer.setPasswordAuthenticator(auth)
sshServer.setKeyboardInteractiveAuthenticator(
new DefaultKeyboardInteractiveAuthenticator()
)
}
options.publicKeyAuthenticator.foreach { auth =>
sshServer.setPublickeyAuthenticator(auth)
}
sshServer.setKeyPairProvider(keyPairProvider(options))
sshServer.setShellFactory(new Factory[Command] {
override def create(): Command = new ShellSession(shellServer)
})
disableUnsupportedChannels(sshServer)
}
def keyPairProvider(options: SshServerConfig) = {
val hostKeyFile = touch(
options.hostKeyFile.getOrElse(fallbackHostkeyFilePath(options))
)
val provider = new SimpleGeneratorHostKeyProvider(hostKeyFile.wrapped)
provider.setAlgorithm("RSA")
provider
}
private def disableUnsupportedChannels(sshServer: SshServerImpl) = {
// exec can't really be disabled
// but it can report error on trying to run any command it received
sshServer.setCommandFactory(new CommandFactory {
override def createCommand(command: String): Command =
throw new IllegalArgumentException("exec is not supported")
})
sshServer.setSubsystemFactories(Collections.emptyList())
sshServer.setTcpipForwardingFilter(null)
sshServer.setAgentFactory(new SshAgentFactory {
override def createServer(service: ConnectionService) = null
override def createClient(manager: FactoryManager) = null
override def getChannelForwardingFactory = null
})
sshServer.setFileSystemFactory(new FileSystemFactory {
override def createFileSystem(session: Session) = null
})
sshServer
}
// this is a user-safe options.
// Server should have stable key
// to not violate the user under threat of MITM attack
private def fallbackHostkeyFilePath(options:SshServerConfig) =
options.ammoniteHome/'cache/'ssh/'hostkeys
def touch(file: os.Path): os.Path = {
if (!os.exists(file)) {
os.write(file, Array.empty[Byte], createFolders = true)
}
file
}
}
import java.io.{InputStream, OutputStream}
/**
* Implementation of ssh server's remote shell session,
* which will be serving remote user.
* @param remoteShell actual shell implementation,
* which will serve remote user's shell session.
*/
class ShellSession(remoteShell: ShellSession.Server)
extends Command {
var in: InputStream = _
var out: OutputStream = _
var exit: ExitCallback = _
var env: Environment = _
lazy val thread = createShellServingThread()
override def setInputStream(in: InputStream) = {
this.in = in
}
override def setOutputStream(out: OutputStream) = {
this.out = new SshOutputStream(out)
}
/* ammonite doesn't uses err stream so we don't need this */
override def setErrorStream(err: OutputStream) = {}
/**
* called by ssh server to instrument this session
* with a callback that it finished serving a user
*/
override def setExitCallback(exit: ExitCallback) {
this.exit = exit
}
/**
* called when ssh server is ready to start this session.
* Starts the actual shell-serving task.
*/
override def start(env: Environment) = {
this.env = env
thread.start()
}
/**
* called when ssh server wants to destroy shell session.
* Whatever shell session serving a user was doing at this moment
* we are free to stop it.
*/
override def destroy() = {
thread.interrupt()
}
private def createShellServingThread(): Thread = new Thread {
override def run(): Unit = {
remoteShell(in, out, env)
exit.onExit(0, "repl finished")
}
}
// proxy which fixes output to the remote side to be ssh compatible.
private class SshOutputStream(out: OutputStream) extends OutputStream {
override def close() = { out.close() }
override def flush() = { out.flush() }
override def write(byte: Int): Unit = {
// ssh client's only accepts new lines with \r so we make \n to be \r\n.
// Unneeded \r will not be seen anyway
if (byte.toChar == '\n') out.write('\r')
out.write(byte)
}
override def write(bytes: Array[Byte]): Unit = for {
i ← bytes.indices
} write(bytes(i))
override def write(bytes: Array[Byte], offset: Int, length: Int): Unit = {
write(bytes.slice(offset, offset + length))
}
}
}
object ShellSession {
type Server = ((InputStream, OutputStream, Environment) => Unit)
}
// SshdAmmoniteFrontend.scala
import java.io.{OutputStream, OutputStreamWriter}
import ammonite.interp.Parsers
import ammonite.repl.{AmmoniteFrontEnd, Highlighter}
import ammonite.terminal.filters.GUILikeFilters.SelectionFilter
import ammonite.terminal.filters._
import ammonite.terminal._
import ammonite.util.Colors
import ammonite.util.Util.newLine
import org.apache.sshd.server.Environment
import scala.annotation.tailrec
class SshdAmmoniteFrontend(env: Environment) extends AmmoniteFrontEnd() {
override def width: Int = env.getEnv.get("COLUMNS").toInt
override def height: Int = env.getEnv.get("LINES").toInt
def tabulate(snippetsRaw: Seq[fansi.Str], width: Int): Iterator[String] = {
val gap = 2
val snippets = if (snippetsRaw.isEmpty) Seq(fansi.Str("")) else snippetsRaw
val maxLength = snippets.maxBy(_.length).length + gap
val columns = math.max(1, width / maxLength)
val grouped =
snippets.toList
.grouped(math.ceil(snippets.length * 1.0 / columns).toInt)
.toList
ammonite.util.Util.transpose(grouped).iterator.flatMap{
case first :+ last => first.map(
x => x ++ " " * (width / columns - x.length)
) :+ last :+ fansi.Str(newLine)
}.map(_.render)
}
@tailrec private def findPrefix(strings: Seq[String], i: Int = 0): String = {
if (strings.count(_.length > i) == 0) strings(0).take(i)
else if(strings.collect{ case x if x.length > i => x(i)}.distinct.length > 1)
strings(0).take(i)
else findPrefix(strings, i + 1)
}
def printCompletions(completions: Seq[String],
details: Seq[String]): List[String] = {
val prelude =
if (details.length != 0 || completions.length != 0) List(newLine)
else Nil
val detailsText =
if (details.length == 0) Nil
else tabulate(details.map(fansi.Str(_)), width)
val completionText =
if (completions.length == 0) Nil
else tabulate(completions.map(fansi.Str(_)), width)
prelude ++ detailsText ++ completionText
}
override def readLine(reader: java.io.Reader,
output: OutputStream,
prompt: String,
colors: Colors,
compilerComplete: (Int, String) => (Int, Seq[String], Seq[String]),
history: IndexedSeq[String]): Option[String] = {
val writer = new OutputStreamWriter(output)
val autocompleteFilter: Filter = Filter.action(SpecialKeys.Tab){
case TermState(rest, b, c, _) =>
val (newCursor, completions, details) = compilerComplete(c, b.mkString)
val details2 = for (d <- details) yield {
Highlighter.defaultHighlight(
d.toVector,
colors.comment(),
colors.`type`(),
colors.literal(),
colors.keyword(),
fansi.Attr.Reset
).mkString
}
lazy val common = findPrefix(completions, 0)
val blacklisted = Seq(
"!=",
"==",
"asInstanceOf",
"equals",
"getClass",
"hashCode",
"isInstanceOf",
"toString",
"|>"
)
val completions2 = for(comp <- completions.filterNot(blacklisted.contains)) yield {
val (left, right) = comp.splitAt(common.length)
(colors.comment()(left) ++ right).render
}
val stdout =
printCompletions(completions2, details2)
.mkString
if (details.nonEmpty || completions.isEmpty)
Printing(TermState(rest, b, c), stdout)
else{
val newBuffer = b.take(newCursor) ++ common ++ b.drop(c)
Printing(TermState(rest, newBuffer, newCursor + common.length), stdout)
}
}
// Enter
val multilineFilter = Filter.action(
SpecialKeys.NewLine,
ti => Parsers.split(ti.ts.buffer.mkString).isEmpty
){
case TermState(rest, b, c, _) => BasicFilters.injectNewLine(b, c, rest)
}
val historyFilter = new HistoryFilter(
() => history.reverse, colors.comment()
)
val selectionFilter = GUILikeFilters.SelectionFilter(indent = 2)
val allFilters = Filter.merge(
UndoFilter(),
historyFilter,
extraFilters,
selectionFilter,
GUILikeFilters.altFilter,
GUILikeFilters.fnFilter,
ReadlineFilters.navFilter,
autocompleteFilter,
cutPasteFilter,
multilineFilter,
BasicFilters.all
)
val res = readLineImpl(
prompt,
reader,
writer,
allFilters,
displayTransform = { (buffer, cursor) =>
val indices = Highlighter.defaultHighlightIndices(
buffer,
colors.comment(),
colors.`type`(),
colors.literal(),
colors.keyword(),
fansi.Attr.Reset
)
val highlighted = fansi.Str(Highlighter.flattenIndices(indices, buffer).mkString)
val (newBuffer, offset) = SelectionFilter.mangleBuffer(
selectionFilter, highlighted, cursor, colors.selected()
)
val newNewBuffer = HistoryFilter.mangleBuffer(
historyFilter, newBuffer, cursor, fansi.Underlined.On
)
(newNewBuffer, offset)
}
)
res
}
def readLineImpl(prompt: Prompt,
reader: java.io.Reader,
writer: java.io.Writer,
filters: Filter,
displayTransform: (Vector[Char], Int) => (fansi.Str, Int) = LineReader.noTransform)
: Option[String] = {
new LineReader(width, prompt, reader, writer, filters, displayTransform)
.readChar(TermState(LazyList.continually(reader.read()), Vector.empty, 0, ""), 0)
}
}
I have been using successfully something like (sleep 3 ; echo 'println("Hello, World!")\n//Some code' ; printf "\nsys.exit\n") | script --quiet --command amm
script comes from util-linux, here it is used as a command line tty emulator.