Thursday, 21 May 2015

[Linux] A progress bar implemented in C on Linux and MacOS X

A progress bar implemented in C on Linux and MacOS X

Implementations

  • CR - carriage return (\r)
  • CSI_1 - move up one row and back to column 1, and then erase display from cursor to end of display (\033[1F\033[J)
  • CSI_2 - move up one row and back to column 1, and then erase display from start of display to cursor (\033[1F\033[1J)
  • CSI_3 - move up one row and then erase whole line (\033[1A\033[2K)
CSI Sequence Reference:

Source code


// An implementation of progress bar in C on Linux
//
// Usage:
//    ./prog [type]
//        type: CR, CSI_1, CSI_2, CSI_3
//
// Bar style: 
//  0 - 10% [====                                                 ] 
//  1 -     |====.................................................| 10%
//  2 -     [||||.................................................] 10%
//
// Implementations
//  CR    - carriage return (\r) 
//  CSI_1 - move up one row and back to column 1, and then 
//          erase display from cursor to end of display (\033[1F\033[J)
//  CSI_2 - move up one row and back to column 1, and then 
//          erase display from start of display to cursor (\033[1F\033[1J)
//  CSI_3 - move up one row and then erase whole line (\033[1A\033[2K)
//  
//  Reference for CSI sequence: man console_codes(4)
//

#include 
#include 
//#include  // usleep deprecated
#include    // nanosleep
#include    // strcmp

//#define BAR_STYLE   0
//#define BAR_STYLE   1
#define BAR_STYLE   2

typedef enum
{
    T_CR,
    T_CSI_1,
    T_CSI_2,
    T_CSI_3,
}TYPE_OF_CONTROL;

void usage()
{
    printf("./prog [type]\n");
    printf("    type: CR, CSI_1, CSI_2, CSI_3\n");
}

void prog_bar (int done, int all, int width, TYPE_OF_CONTROL t)
{
    float ration = (done*100)/(float)all;
    int d = (int)(ration * width)/100;
    int i;

#if BAR_STYLE == 0
    printf("%d%% [", (int)ration); 
    for(i = 0; i < d; i++)
        printf("=");

    for(i = d; i < width; i++)
        printf(" ");

    printf("]"); 
#elif BAR_STYLE == 1
    printf("|"); 
    for(i = 0; i < d; i++)
        printf("=");

    for(i = d; i < width; i++)
        printf(".");

    printf("| %d%% ", (int)ration); 
#elif BAR_STYLE == 2
    printf("["); 
    for(i = 0; i < d; i++)
        printf("|");

    for(i = d; i < width; i++)
        printf(".");

    printf("] %d%% ", (int)ration); 
#endif
    switch(t)
    {
        case T_CR: 
            printf("\r");
            fflush(stdout);
            break;
        case T_CSI_1:
            // move cursor up for 1 line and then erase from cursor to end of display 
            printf("\n\033[1F\033[J");
            break;
        case T_CSI_2:
            // move cursor up for 1 line and then erase from start of display to cursor
            printf("\n\033[1F\033[1J");
            break;
        case T_CSI_3:
            // move cursor up for 1 line and then erase whole line. 
            printf("\n\033[1A\033[2K");
            break;
        default:
            break;
    }
}

int main(int argc, char *argv[])
{
    int i = 0;
    const struct timespec t = {0, 100000000}; //100ms
    TYPE_OF_CONTROL ctrl_type = T_CR;

    if(0 == isatty(1))
    {
        printf("Not a terminal.\n");
        fflush(stdout);
    }

    switch (argc)
    {
        case 1:
            ctrl_type = T_CR;
            break;
        case 2:
            if (strcmp(argv[1], "CR") == 0)
            {
                ctrl_type = T_CR;
            }
            else if (strcmp(argv[1], "CSI_1") == 0)
            {
                ctrl_type = T_CSI_1;
            }
            else if (strcmp(argv[1], "CSI_2") == 0)
            {
                ctrl_type = T_CSI_2;
            }
            else if (strcmp(argv[1], "CSI_3") == 0)
            {
                ctrl_type = T_CSI_3;
            }
            else
            {
                usage(); 
                exit(-1);
            }
            break;
        default:
            usage();
            exit(-1);
    }

    for(i = 0; i <= 100; i++)
    { 
        if(isatty(1))
        { 
            prog_bar(i, 100, 100, ctrl_type); 
        } 
        nanosleep(&t, NULL); //0.1 sec
    }

    exit(0);
}

Test

MacOS X

Different terminal emulators
Apple Terminal
  • Works: CR, CSI_1, CSI_2, CSI_3
  • $echo $TERM is xterm-color
iTerm2
  • Works: CR, CSI_1, CSI_2, CSI_3
  • $echo $TERM is xterm-color
xterm
  • Works: CR, CSI_1, CSI_2, CSI_3
  • $echo $TERM is xterm

Ubuntu

virtual terminal (tty)
  • Works: CR, CSI_1, CSI_2, CSI_3
  • $echo $TERM is linux
gnome-terminal
  • Works: CR, CSI_1, CSI_2, CSI_3
  • $echo $TERM is gnome-terminal
xterm
  • Works: CR, CSI_1, CSI_2, CSI_3
  • $echo $TERM is xterm
ssh from iTerm2 on MacOS X
  • Works: CR, CSI_2, CSI_3
  • Doesn’t work: CSI_1
  • $echo $TERM is xterm-color
ssh from xterm on MacOS X
  • Works: CR, CSI_1, CSI_2, CSI_3
  • $echo $TERM is xterm

Wednesday, 20 May 2015

[Linux] tty, ttyS, ttyUSB, pty, console .... etc

Virtual Console (tty)

Linux console

Linux console aims for kernel or other processes
  • to output text-based messages to the user (display)
  • to receive text-based input from the user (keyboard)
Therefore, it is a combination of display and keyboard for user interface.
System console
System console is the device which receives all kernel messages and warnings and which allows logins in single user mode. (Reference: CONFIG_VT_CONSOLE)

Linux console devices

  • Virtual terminals
    • Usually have 6 virtual terminals and switch by Alt+F1, …, and Alt+F6 in text mode (Number 7 for X)
    • Or Ctrl+Alt+F1, …, and Ctrl+Alt+F6 in X window mode
    • /dev/tty0 or /dev/console: current console or system console
    • /dev/tty1, /dev/tty2, …
      • virtual consoles
  • Serial port (ttyS0, ttyS1…)
    • Embedded Linux
  • USB serial port (ttyUSB0, ttyUSB1, …)
    • such as USB Dongle 3G module
  • VGA in text-mode
  • Framebuffer (fb)
    • Embedded Linux (connect to LCD display)

Linux console_codes or escape code

console_codes(4) - Linux console escape and control sequences

Terminal Emulator

Terminal emulator is a program that emulates a video terminal within some other display architecture, such as
  • Konsole
  • gnome-terminal
  • tilda
  • xterm (X Window System)
  • iTerm (Mac OS X)

Pseudo devices

Pseudo devices are device nodes on Unix-like systems do not necessarily have to correspond to physical devices.

Examples

  • /dev/null
  • /dev/zero
  • /dev/full
  • /dev/random
  • /dev/pts/

Pseudo-terminals

Pseudo-terminals, or PTY, s a pair of pseudo-devices, one of which, the slave, emulates a real text terminal device, the other of which, the master, provides the means by which a terminal emulator process controls the slave.

Master and Slave

  • PTM (Pseudo-Terminal Master) & PTS (Pseudo-Terminal Slave)
  • Master is not a terminal and just a normal device for read/write.
  • Slave is a terminal.
    write → [Master] — [Slave] → read (stdin for process connected)
read ← [Master] — [Slave] ← write (stdin for master)
  • Write to master is treated as input to the foreground process that is connected to slave (like input from keyboard terminal)
    • Ctrl+C in terminal emulator will send a SIGINT signal to the foreground process that is connected to slave
  • Write to slave is treated as output to master

Other references

Device file

  • /dev/ptmx character file (major 5 & minor 2): used to create a pseudoterminal master and slave pair
  • /dev/pts/n pseudoterminal slave device

Common framework to use pseudo terminal

  • Open /dev/ptmx file
    • fd_ptm = posix_openpt(O_RDWR);
    • or fd_ptm = open("/dev/ptmx", O_RDWR);
      • By opening ptmx device, a fd is returned. Besides, a slave device, let’s say 4, is created under /dev/pts/4.
  • Grant access to the slave pseudoterminal
    • ret = grantpt(fd_ptm);
  • Unlock a pseudoterminal master/slave pair
    • ret = unlockpt(fd_ptm);
  • Open slave device
    • fd_pts = open(ptsname(fd_ptm), O_RDWR);
      • ptsname get the corresponding slave device name, such as /dev/pts/4.
  • Fork a child process
  • In parent process,
    • close fd_pts since it never uses it
    • use select to listen message from stdin or from master device (fd_ptm)
      • if it is from stdin, then write to master device (fd_ptm). Finally child process will get this message from its stdin.
      • if it is from fd_ptm, so it is also from child process (by write to fd_pts), so display it by writing to stdout
  • In child process,
    • close fd_ptm
    • close opened stdin, stdout and stderr because we need to redirect them to fd_pts
    • set terminal parameter of slave to RAW mode by tcsetattr
    • use dup(fd_pts); three times to duplicate 0, 1, and 2 fro fd_pts
    • then use execl-like functions to execute a command such as echo, bc.
    • so for command, its stdin, stdout and stderr are all redirected to fd_pts.

Examples

  • terminal emulator
  • script(1)
  • expect(1)
  • ssh(1), telnet(1), rlogin(1) etc

Tuesday, 19 May 2015

[Unix] a potential usage of script(1) command - test script for progress bar

script(1) command

script(1) command on Unix or Linux aims to make typescript of terminal session (Reference: Unix Help or man 1 script ). Its implementation is based on pseudoterminal. Here’s a diagram that shows how script is implemented.
For example, $script -c "echo helloworld" will run echo helloword to print helloword on terminal (stdout). In addition, it will produce a typescript file that is a copy of everything printed on terminal of the command. In this case, typescript likes below
    Script started on Tue 19 May 2015 10:34:26 BST
    helloworld

    Script done on Tue 19 May 2015 10:34:26 BST

one use case

Scenario

If we have a command that will show a progress bar (like scp) and we need to write a test case for this command to test if the progress bar works as expected, we can not easily capture its output by redirecting its stdout to a file because usually this command will check if the file descriptor for output is a terminal or not by isatty function.
if(isatty(fd))
{
// show a progress bar
}
With redirection like $cmd > file, stdout points to a file. Therefore, isatty function will return 0. Finally progress bar is hidden. That’s not what we expect.

Solution

script -c "cmd" | sed "s/\\r/\\n/g" > prog.txt
With script, the output of cmd is still a terminal (stdout) and so we can capture its output. Besides, through sed, we replace carriage return (\r) by line feed (\n), and then every print by cmd can be captured.
Now prog.txt will record the output of progress bar in this cmd command. That’s what we expected.