Run a command in a new terminal window

Hi all,

I’m trying to run a Command asynchronously and direct the output to a new terminal window. I understand that I can do this:

run(pipeline(`echo hello`, stdout="out.txt"))

which works to re-direct the output to a file, but what I’d really like is for the output to go to a new terminal window. Is this possible?

Cheers,

Colin

PS I’m on Ubuntu 18.04 if this makes any difference

You just want to see in the output of the command in another terminal window?

You could simply run watch out.txt or look into writing and reading from named pipes if you don’t want to write to disk.

2 Likes

You could simply run watch out.txt

Yes, but if I call this command from Julia won’t it just start watching that file in the terminal that I am currently already running Julia in? The goal here is to re-direct the output to a new terminal window.

writing and reading from named pipes

This is what I’m trying to do with the pipeline call, but I don’t know how to re-direct the output from the pipe to a new terminal window (which I would need to open programatically).

Apologies if I have misunderstood what you’re trying to suggest here.

What about

run(`xterm -e watch cat out.txt`, wait = false)
run(pipeline(`echo hello`, stdout="out.txt"))
1 Like

This should work, thank you.

As an aside it does feel a bit hacky that I need to write via a file. Perhaps I might be able to run xterm in a separate pipeline and then write directly to that pipeline. I’ll have a play around and see what I can get working. Thanks again.

What about using Distributed for this? The remote process could open a terminal, run your commands, and print the result into the terminal.

1 Like

If you can identify the tty of the other terminal you can redirect stdout/stderr to that one:

Edit: So from inside Julia your example would be

run(pipeline(`echo hello`, stdout="/dev/pts/14"))
3 Likes

Assuming you’re on a UNIX-like system (which seems to be the case if you assume xterm to be available), you could use UNIX sockets instead of files.

# Make sure the UNIX socket does not exist yet
rm("/tmp/unix.sock", force=true)

# This is the code that the server will run
server_code = quote
    using Sockets
    foreach(println, eachline(accept(listen("/tmp/unix.sock"))))
end |> string

# Spawn a new julia process in a new terminal
julia = Base.julia_cmd()[1]
run(`xterm -e "$julia -e '$server_code'"`, wait=false)


# Connect to the socket and send some strings to it
using Sockets
s = connect("/tmp/unix.sock")
write(s, "Hello...\n"); flush(s); sleep(3)
write(s, "World!\n");   flush(s); sleep(3)

# It can be used to redirect the output of a pipeline, too
run(pipeline(`echo Hello from an other process`, stdout=s))
sleep(3)

# The server process will exit when this connection is closed
# ... so will the terminal.
close(s)

And if you’re not on a UNIX system, I guess this example could easily be adapted to TCP sockets (but you’d still have to handle the issue of how to spawn a terminal in a portable way)

3 Likes

But perhaps I misunderstood: did you intend the process to be run as a server, receiving commands and processing them, and displaying output in the terminal window over and over again? Or just once?

1 Like

This would be a good way of emulating Stata’s interface, where you can scroll through your output without losing your writing space. OP, if this is what you are trying to do, check out jupyterlab’s console mode.

1 Like

I just want to make sure you realize that what you are asking for is highly unusual and require some serious code to make happen.

Normally Julia runs in a terminal, it doesn’t know anything ABOUT the terminal it just knows it’s a terminal. If you just search for “terminal” in your Linux Distro’s application install you will probably see lots of different programs that provide a terminal:

  • Byobu Terminal
  • Deepin Terminal
  • Xfce Terminal
  • MATE Terminal
  • LXTTerminal
  • QTerminal
  • etc…

It might even be in a text terminal like when you hit Ctrl Alt F2. Or it could be a terminal over a Telnet or SSH connection. Julia doesn’t really know, and doesn’t really care. Normally these terminals don’t communicate with each other and there is no “common” API to tell the terminal you want to duplicate a terminal and run a program in it.

So you are going to need to decide which terminal application you want to interact with and actually launch it and tell it to run your program. i.e. your program will required that terminal X is installed on the machine. On my system I use xfce4-terminal for my terminals. Bringing up the man page for that shows that I can pass in a -e argument with the command to run.

So on my system I could execute xfce4-terminal -e htop to create a new terminal and run htop in it. When I exit htop the terminal automatically closes (which is probably specific to the xfce4-terminal program).

To make matters funner (worse) the output Julia would receive is the output from the terminal application NOT the output from the program running INSIDE the terminal. xfce4-terminal actually returns right after the new terminal is launched, so run() returns after the terminal is launch, NOT once the program running inside the terminal terminates.

So if you want your Julia program to receive data from the launch program you will need to tell that program to pass the data back to the original program somehow, sockets, TCP/IP, or a file would probably be the easiest.

2 Likes

This would definitely work, although it isn’t obvious how to obtain the tty of the terminal I’d like to direct the output to from within my current program. However, for my particular use-case it is feasible to just open the terminal myself, grab the tty, and then paste it into my program before I run it. So thanks, this is very helpful. Based on the other responses below, it seems obtaining the tty of a new terminal programmatically is going to be very much not straightforward.

Thanks for this. For my use-case, I actually think I can adapt this to work without the sockets. The Julia code in question uses a command to call a node.js program that sends data back to my original Julia program using a socket, but what I’m really trying to do is just ensure that the print statements from the node.js program print in a different terminal, as it makes things vastly easier to monitor in real-time when they don’t mix in with the print statements from my Julia program.

It looks like I can just adapt the run command from your code to call my node.js program inside a terminal emulator and this will accomplish what I’m after.

As it happens, I am also passing data from the node.js program back to Julia program via a domain socket, but this is ancillary to the issue with the print statements.

Thanks again.

Thanks for responding. No, that is much more complicated than what I’m after. I actually just have a Julia program that calls a node.js program via a Command to run asynchronously. The node.js program prints quite a lot of stuff, as does the Julia program, and it would be very helpful from a real-time monitoring perspective if the node.js print statements appear in a different window to the Julia print statements. As one of the other answerers points out, I can accomplish this just by calling the node.js program in my Command via a terminal emulator xterm.

Thanks for responding. I’ll have a look at what they do, although it does appear I’ve worked out a solution based on the responses in this thread.

Thanks for taking the time to type such a lengthy response.

Yes, I was surprised at the responses this thread generated. For the general case, it is far less straightforward than I had envisaged, and as you say, would require quite a bit of work to implement.

Fortunately, it looks like for my particular use-case, I’ll be able to get what I want just by planting a call to a node.js program in my Command object inside a call to xterm. My main goal was just to get the print statements from the node.js program in a separate terminal. As it happens, I am also passing some data from the node.js program back to the Julia program, but I’m doing this via domain sockets, and have already got that part of the code working nicely.

1 Like

I’m wondering whether it would make sense to create a package that helps with this. I can think of several applications (“watch” terminal for globals, table viewer, ascii plot output… essentially it would be a step towards a lightweight IDE).

From the discussion above, I imagine that this is likely to be fairly platform-specific. Is there a Windows equivalent to tty?

I don’t believe so.

What would probably be easiest is create a TCP/IP listener on a random port. Spawn a Command Prompt or x-terminal that starts julia with a script to connect to localhost on that random port and echo everything it receives over the TCP/IP connection to the screen.

Then anything your program writes out the TCP/IP connection will show up on the other terminal. You could do this N times to spawn N different terminals. And except for the command to launch the terminal it would be all very OS non-specific.

2 Likes

Hey,

I saw this post and it helped me a lot so I share my version that came from these idea:

get_next_pts_nums(task_num) = begin  # queriing the next terminal session IDs.
    pts_ls = read(`ls /dev/pts`, String)
    pts_nums = parse.(Int,split(pts_ls, '\n')[1:end-2])
    sort!(pts_nums)

    pts_ids = Vector{Int}(undef,task_num)
    x, i, j = 0, 1, 1 
    while i <= task_num
        while j <=length(pts_nums) && x == pts_nums[j]
            x+=1
            j+=1
        end
        pts_ids[i] = x
        x+=1
        i+=1
    end
    pts_ids
end
run_cmd(cmds::Vector{Cmd}, io) = for cmd in cmds run_cmd(cmd, io) end
run_cmd(cmd::Cmd, io) = begin
    run(`echo "+ $cmd"`, io, io, io)  
    run(cmd, io, io, io) 
end
run_in_terminal(cmd, t_id) = begin
    run(`gnome-terminal -- zsh`)
    pts_file = open("/dev/pts/$t_id", "w")
    run_cmd(cmd, pts_file)
end

For local run this is damn fast and clean:

macro aync_run(cmd)
    esc(:(for (i,t_id) in enumerate(get_next_pts_nums(length($cmd)))
        @async run_in_terminal($cmd[i], t_id)
    end))
end
# user, ip, cust_cmd = "testuser", "127.0.0.1", """echo "I am on the machine" """
@aync_run [`tty`,[`echo "haha whut"`, `tty`, `echo "I am on the machine"`, `echo "hell"`]]

Very ugly first try to run on different servers…

fn(user,ip) = begin
    term_pts = get_next_pts_nums(1)[1]
    run(`gnome-terminal -- zsh`)
    @async begin 
        term_io = open("/dev/pts/$term_pts","w")
        auth= `ssh -t $user@$ip`
        run(`$auth echo "hello cmd"`,term_io,term_io,term_io)
        run(`$auth tty`,term_io,term_io,term_io)
        run(`$auth echo helllo`,term_io,term_io,term_io)
    end
end
fn("testuser", "127.0.0.1")

I felt it would be too much time to parse the function with the macro add the terminal start and append the appropriate pts file for the run commands. So it is up to you to make it more clean. Also couldn’t agree on the best syntax, how do we want to use @async_run in terms of function calls, so broadcast on the vector of inputs or not… and so on.

Don’t forget I used “zsh” so replace it to you bash.
Warning: We are calculating the “upcoming” terminal windows ID before it start, so there is a 0.000800 sec time till if someone else also start a windows from an external source, then there will be some fun overthere. :slight_smile:

To cover the above mentioned issue, I tried to open my own pseudoterminal with the following commands but I didn’t know how to attach the next terminal on these file descriptors. I leave the efforst here, maybe someone will know how to finish.

pts_file = ccall((:open, "libc.so.6"), Cint, (Cstring, Cint), "/dev/ptmx",2) 
ret2 = ccall((:grantpt, "libc.so.6"), Cint, (Cint,), pts_file) 
ret3 = ccall((:unlockpt, "libc.so.6"), Cint, (Cint,), pts_file) 
pts_file_url = ccall((:ptsname, "libc.so.6"), Ptr{UInt8} , (Cint,), pts_file) 
@show unsafe_string(pts_file_url)   # you can see the next terminal... but you already took the place... so... this isn't the next one already. 

I hope these help someone who also wants multiple terminal windows in julia and send the appropriate commands to the approriate terminal window. :slight_smile:

1 Like