简体   繁体   中英

Difference in terminal stdin handling between Python and OCaml

I'm trying to do something very specific, involving sending control chars to stdout and reading from stdin.

I have a working implementation in Python and I am trying to translate it to OCaml.

I was pleasantly surprised that it was possible to translate very directly, almost line-for-line. But when I run it the behaviour is different and the OCaml one does not work.

It appears to me the problem must be some obscure difference between how OCaml and Python runtimes handle the terminal, perhaps stdin specifically.

Firstly here is the working Python code:

import os, select, sys, time, termios, tty

def query_colours():
  fp = sys.stdin
  fd = fp.fileno()
  if os.isatty(fd):
      old_settings = termios.tcgetattr(fd)
      tty.setraw(fd)
      try:
          print('\033]10;?\07\033]11;?\07')
          r, _, _ = select.select([ fp ], [], [], 0.1)
          if fp in r:
              return fp.read(48)
          else:
              print("no input available")
              return None
      finally:
          termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
  else:
      raise ValueError("Not a tty")

And my OCaml translation looks like:

let query_colours () =
  let fd = Unix.stdin in
  if Unix.isatty fd then
    let old_settings = Unix.tcgetattr fd in
    set_raw fd;
    Fun.protect
      ~finally:(fun () -> Unix.tcsetattr fd Unix.TCSADRAIN old_settings)
      (fun () ->
          print_string "\o033]10;?\o007\o033]11;?\o007";
          let r, _, _ = Unix.select [fd] [] [] 0.1 in
          let buf = Bytes.create 48 in
          Printf.printf ">> len r: %d\n" (List.length r);  (* debugging *)
          ignore @@ (
            match List.exists (fun (el) -> el == fd) r with
            | true -> Unix.read fd buf 0 48
            | false -> failwith "No input available"
          );
          Bytes.to_string buf
        )
  else
    invalid_arg "Not a tty"

Note that we had to make an OCaml implementation of tty.setraw . First, here is the source from Python stdlib:

def setraw(fd, when=TCSAFLUSH):
    """Put terminal into a raw mode."""
    mode = tcgetattr(fd)
    mode[IFLAG] = mode[IFLAG] & ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON)
    mode[OFLAG] = mode[OFLAG] & ~(OPOST)
    mode[CFLAG] = mode[CFLAG] & ~(CSIZE | PARENB)
    mode[CFLAG] = mode[CFLAG] | CS8
    mode[LFLAG] = mode[LFLAG] & ~(ECHO | ICANON | IEXTEN | ISIG)
    mode[CC][VMIN] = 1
    mode[CC][VTIME] = 0
    tcsetattr(fd, when, mode)

iflag , oflag , cflag , lflag are bit-masked integers

In OCaml side the Stdlib has provided instead of four bit-masked ints a single record with all the boolean values: https://ocaml.org/api/Unix.html#TYPEterminal_io

My OCaml translation of tty.setraw looks like:

let set_raw ?(set_when=Unix.TCSAFLUSH) fd =
  let mode : Unix.terminal_io = {
    (Unix.tcgetattr fd) with
    c_brkint = false;
    c_icrnl = false;
    c_inpck = false;
    c_istrip = false;
    c_ixon = false;
    c_opost = false;
    c_csize = 8;
    c_parenb = false;
    c_echo = false;
    c_icanon = false;
    (* c_iexten = false; ...does not exist on Unix.terminal_io  *)
    c_ixoff = false; (* IEXTEN and IXOFF appear to set the same bit *)
    c_isig = false;
    c_vmin = 1;
    c_vtime = 0;
  } in
  Unix.tcsetattr fd set_when mode

Ok, now the problem...

When I run the Python version it just returns a string like:

'\x1b]10;rgb:c7f1/c7f1/c7f1\x07\x1b]11;rgb:0000/0000/0000\x07'

which is the intended behaviour. I do not hear the BEL sound or any other content printed to screen.

When I run my OCaml version, I hear the BEL sound and I see:

╰─ dune exec -- ./bin/cli.exe
>> len r: 0
Fatal error: exception Failure("No input available")
^[]10;rgb:c7f1/c7f1/c7f1^G^[]11;rgb:0000/0000/0000^G%

╭─    ~/Documents/Dev/ *5 !4 ?4       2 ✘  18:20:26 
╰─ 10;rgb:c7f1/c7f1/c7f1

╭─    ~/Documents/Dev/ *5 !4 ?4       2 ✘  18:20:26 
╰─ 11;rgb:0000/0000/0000

We can see from the print debugging len r: 0 that the select call did not find stdin ready for reading.

Instead we see the results sent to stdin in the terminal after my program has exited.

FWIW if I run the Python script from inside an OCaml program via Unix.open_process_in then I get the same (broken) behaviour from the Python script:

utop # run "bin/query.py";;
- : string list = ["\027]10;?\007\027]11;?\007"; "no input available"]

I realise this is maybe a bit of an obscure corner but would be very grateful if anyone has any insight.

This is a lot of code to read but just from the description it sounds like you're returning the terminal to its old state before flushing the output.

This isn't anything particularly strange about OCaml, but OCaml does have a tendency to hold on to buffered output longer than some other languages.

You might try adding this after the print_string :

flush stdout

Like I said, there's a lot of code to read and this is just my quick take.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM