Ticket #3692: 3692-mc_subshell-V4.patch

File 3692-mc_subshell-V4.patch, 33.5 KB (added by alllexx88, 7 years ago)
  • lib/shell.c

    Author: alllexx88 <opotapenko@gmail.com>
    Date:   Tue, 22 Nov 2016 19:41:46 +0300
    
        More sophisticated shell type detection method
    
        * Make tests on shell binary instead of trying to guess shell type from
          path. Most supported shells set specific variables, hence by testing
          whether such variables are set, we can guess shell type in a more
          reliable way. This works with bash, zsh, tcsh and fish. For guessing
          dash or BusyBox ash (which are treated the same), we run a more peculiar
          test on whether shell supports expansion in PS1 as a prompt string. The
          latter test is also designed to differentiate a legacy pre 1.20 BusyBox
          ash, which allows to apply printf workaround in the case of such shell.
        * Remove chdir command from subshell initialization code, and use full
          paths for init_file instead. Changing dir only allows to use relative
          init_file paths, but can instead potentially lead to some problems, as
          previously noted in the comments; so by not doing this we add additional
          layer of protection against bugs.
        * Remove unneeded SHELL_SH shell type, and 'name' mc_shell_t field, since
          the latter was only being used as arg0 when initializing subshell, and
          all shells apparently initialize fine regardless of arg0 after being
          correctly detected, except for zsh, which has to be invoked as "zsh",
          probably because it behaves in a bourne compatible way otherwise
          (so we just leave arg0 as "zsh" for it)
        * Also add a little error verbosity in scope of detecting shell type and
          subshell initialization
    
    ---
     lib/shell.c           | 541 +++++++++++++++++++++++++++++++++++++++++++++-----
     lib/shell.h           |  50 ++++-
     src/subshell/common.c |  58 ++----
     3 files changed, 550 insertions(+), 99 deletions(-)
    
    diff --git a/lib/shell.c b/lib/shell.c
    index 6f07cb0..782ff0a 100644
    a b  
    3333#include <stdarg.h> 
    3434#include <stdio.h> 
    3535#include <stdlib.h> 
     36#include <sys/wait.h> 
     37#include <poll.h> 
    3638 
    3739#include "global.h" 
    3840#include "util.h" 
     
    4446 
    4547/*** file scope type declarations ****************************************************************/ 
    4648 
     49/* For pipes */ 
     50enum 
     51{ 
     52    READ = 0, 
     53    WRITE = 1 
     54}; 
     55 
    4756/*** file scope variables ************************************************************************/ 
    4857 
    4958static char rp_shell[PATH_MAX]; 
    mc_shell_get_from_env (void) 
    137146    return mc_shell; 
    138147} 
    139148 
    140 /* --------------------------------------------------------------------------------------------- */ 
     149/* Close fd (we expect a pipe), appending all remaining data in case of input pipe to *dest. 
     150   If (dest == NULL), we still read all the data in an input pipe to close it properly. 
     151   Set the fd to -1 to indicate it's closed */ 
    141152 
    142153static void 
    143 mc_shell_recognize_real_path (mc_shell_t * mc_shell) 
     154mc_shell_finalize_fd (int * fd, GString * dest) 
    144155{ 
    145     if (strstr (mc_shell->path, "/zsh") != NULL || strstr (mc_shell->real_path, "/zsh") != NULL 
    146         || getenv ("ZSH_VERSION") != NULL) 
     156    struct pollfd fdinfo; 
     157    int ret; 
     158    char buff[256]; 
     159    ssize_t size; 
     160 
     161    if ((fd == NULL) || (*fd < 0)) 
     162        return; 
     163 
     164    fdinfo.fd = *fd; 
     165    fdinfo.events = POLLIN; 
     166 
     167    ret = poll (&fdinfo, 1, 0); 
     168 
     169    if ((ret > 0) && ((fdinfo.revents & POLLIN) != 0)) 
     170        fcntl (*fd, F_SETFD, fcntl (*fd, F_GETFD) | O_NONBLOCK); 
     171 
     172    while ((ret > 0) && ((fdinfo.revents & POLLIN) != 0)) 
    147173    { 
    148         /* Also detects ksh symlinked to zsh */ 
    149         mc_shell->type = SHELL_ZSH; 
    150         mc_shell->name = "zsh"; 
     174        size = read (*fd, &buff[0], 255); 
     175 
     176        if (size > 0) 
     177        { 
     178            buff[size] = '\0'; 
     179            g_string_append (dest, buff); 
     180        } 
     181 
     182        ret = poll (&fdinfo, 1, 0); 
    151183    } 
    152     else if (strstr (mc_shell->path, "/tcsh") != NULL 
    153              || strstr (mc_shell->real_path, "/tcsh") != NULL) 
     184 
     185    if ((fdinfo.revents & POLLNVAL) == 0) 
     186        close (*fd); 
     187 
     188    *fd = -1; 
     189} 
     190 
     191static void * 
     192mc_shell_waitpid_thread (void * args) 
     193{ 
     194    int * argsint = (int*)(args), pid = argsint[0], * exit_status = &argsint[1]; 
     195 
     196    /* wait for child to terminate and get exit_status, 
     197       or set exit_status to '-1' on waitpid error */ 
     198    if (waitpid (pid, exit_status, 0) == -1) 
     199        *exit_status = -1; 
     200 
     201    /* this signales the parent thread that child terminated */ 
     202    argsint[0] = 0; 
     203 
     204    pthread_exit (NULL); 
     205} 
     206 
     207/* Wait for child process to terminate and read/write requested pipes in the process. 
     208   In case the child is running longer than the timeout value, terminate it with a 
     209   SIGKILL signal. Timeout is in milliseconds, 0 timeout means no timeout. 
     210   Returns child exit_status, or '-1' on non positive pid or waitpid () failure */ 
     211static int 
     212mc_shell_child_wait (int pid, guint timeout, pipes_data_t * pipes) 
     213{ 
     214    int args[2] = {pid}, i, err; 
     215    guint64 start = mc_timer_elapsed (mc_global.timer); 
     216    pthread_t thread[1]; 
     217 
     218    if (pid <= 0) 
     219        return -1; 
     220 
     221    /* launch thread that waits for child process termination and gets the exit status */ 
     222    err = pthread_create(thread, NULL, mc_shell_waitpid_thread, args); 
     223 
     224    if (err != 0) /* failed to create thread */ 
    154225    { 
    155         /* Also detects csh symlinked to tcsh */ 
    156         mc_shell->type = SHELL_TCSH; 
    157         mc_shell->name = "tcsh"; 
     226        return -1; 
    158227    } 
    159     else if (strstr (mc_shell->path, "/csh") != NULL 
    160              || strstr (mc_shell->real_path, "/csh") != NULL) 
     228 
     229    if ((pipes != NULL) && (pipes->n > 0)) 
    161230    { 
    162         mc_shell->type = SHELL_TCSH; 
    163         mc_shell->name = "csh"; 
     231        /* we have pipes to poll */ 
     232        int ret, n = pipes->n; 
     233        struct pollfd * fdinfo = g_new (struct pollfd, n); 
     234        ssize_t size; 
     235        char buff[256]; 
     236 
     237        /* prepare pollfd struct array for poll () and make all pipes non blocking */ 
     238        for (i = 0; i < n; i++) 
     239        { 
     240            fdinfo[i].fd = pipes->poll_fds[i]; 
     241            fdinfo[i].events = POLLIN | POLLOUT; 
     242            fcntl (fdinfo[i].fd, F_SETFD, fcntl (fdinfo[i].fd, F_GETFD) | O_NONBLOCK); 
     243        } 
     244 
     245        ret = poll (fdinfo, n, 0); 
     246 
     247        /* while child is running, read/write from/to all requested pipes */ 
     248        while (args[0] > 0) 
     249        { 
     250            if (mc_timer_elapsed (mc_global.timer) >= start + 1000 * timeout) 
     251            { 
     252                /* kill spawned child on timeout */ 
     253                kill (pid, SIGKILL); 
     254                break; 
     255            } else if (ret > 0) 
     256                for (i = n - 1; i >= 0; i--) 
     257                { 
     258                    if ((fdinfo[i].revents & POLLIN) != 0) 
     259                    { 
     260                        /* this is an input pipe, and it has data ready for reading */ 
     261                        size = read (fdinfo[i].fd, buff, 255); 
     262 
     263                        if ((pipes->buff[i] != NULL) && (size > 0)) 
     264                        { 
     265                            buff[size] = '\0'; 
     266                            g_string_append (pipes->buff[i], buff); 
     267                        } 
     268                    } else if ((fdinfo[i].revents & POLLOUT) != 0) 
     269                    { 
     270                        /* this is an output pipe, and it is ready for writing */ 
     271 
     272                        if ((pipes->buff[i] != NULL) && ((pipes->buff[i])->len > 0)) 
     273                        { 
     274                            GString * source = pipes->buff[i]; 
     275 
     276                            size = write (fdinfo[i].fd, source->str, source->len); 
     277 
     278                            if (size > 0) 
     279                                g_string_erase (source, 0, size); 
     280 
     281                            if (source->len == 0) 
     282                            { 
     283                                /* done writing: close the pipe, and remove from fdinfo */ 
     284                                close (fdinfo[i].fd); 
     285                                fdinfo[i].fd = pipes->poll_fds[i] = -1; 
     286                            } 
     287                        } else 
     288                        { 
     289                            /* nothing to write: close the pipe, and remove from fdinfo */ 
     290                            close (fdinfo[i].fd); 
     291                            fdinfo[i].fd = pipes->poll_fds[i] = -1; 
     292                        } 
     293                    } 
     294                } 
     295 
     296            ret = poll (fdinfo, n, 0); 
     297        } 
     298 
     299        g_free (fdinfo); 
     300    } else if (timeout > 0) 
     301        while (args[0] > 0) /* loop here until child terminates or command timeouts */ 
     302            if (mc_timer_elapsed (mc_global.timer) >= start + 1000 * timeout) 
     303            { 
     304                /* kill spawned child on timeout */ 
     305                kill (pid, SIGKILL); 
     306                break; 
     307            } 
     308 
     309    pthread_join (thread [0], NULL); 
     310 
     311    /* read all the remaining data in the pipes and close them */ 
     312    if ((pipes != NULL) && (pipes->n > 0)) 
     313        for (i = 0; i < pipes->n; i++) 
     314            mc_shell_finalize_fd (&pipes->poll_fds[i], pipes->buff[i]); 
     315 
     316    return args[1]; 
     317} 
     318 
     319/* --------------------------------------------------------------------------------------------- 
     320   This function is similar to g_spawn_sync () function with some additional functionality: 
     321       * add optional timeout to SIGKILL the child after its expiration 
     322       * optionally feed a string to child's stdin 
     323       * optionally poll and read/write requested pipes during child execution 
     324   --------------------------------------------------------------------------------------------- */ 
     325 
     326static gboolean 
     327mc_shell_execute (const char * working_directory, 
     328                        char ** argv, 
     329                        char ** envp, 
     330                        GSpawnFlags flags, 
     331                        GSpawnChildSetupFunc child_setup, 
     332                        void * user_data, 
     333                        const char * feed_standard_input, 
     334                        char ** standard_output, 
     335                        char ** standard_error, 
     336                        guint timeout, /* timeout is in milliseconds, 0 timeout means no timeout */ 
     337                        pipes_data_t * pipes, 
     338                        int * exit_status, 
     339                        GError ** error) 
     340{ 
     341    int i, pid, stdin_fd, stdout_fd, stderr_fd, status, 
     342        * stdin_return = NULL, * stdout_return = NULL, * stderr_return = NULL; 
     343    pipes_data_t pipes_data; 
     344 
     345    pipes_data.n = 0; 
     346 
     347    if ((pipes != NULL) && (pipes->n > 0)) 
     348    { 
     349        pipes_data.n = pipes->n; 
     350 
     351        /* without this the requested pipes will be closed for the child */ 
     352        flags |= G_SPAWN_LEAVE_DESCRIPTORS_OPEN; 
     353 
     354        /* make parent sides of the pipes get closed for the child */ 
     355        for (i = 0; i < pipes->n; i++) 
     356            fcntl (pipes->poll_fds[i], F_SETFD, fcntl (pipes->poll_fds[i], F_GETFD) | FD_CLOEXEC); 
    164357    } 
    165     else if (strstr (mc_shell->path, "/fish") != NULL 
    166              || strstr (mc_shell->real_path, "/fish") != NULL) 
     358 
     359    /* we'll reap the child ourselves with waitpid () */ 
     360    flags |= G_SPAWN_DO_NOT_REAP_CHILD; 
     361 
     362    if (feed_standard_input != NULL) 
    167363    { 
    168         mc_shell->type = SHELL_FISH; 
    169         mc_shell->name = "fish"; 
     364        stdin_return = &stdin_fd; 
     365        pipes_data.n++; 
    170366    } 
    171     else if (strstr (mc_shell->path, "/dash") != NULL 
    172              || strstr (mc_shell->real_path, "/dash") != NULL) 
     367 
     368    if (standard_output != NULL) 
    173369    { 
    174         /* Debian ash (also found if symlinked to by ash/sh) */ 
    175         mc_shell->type = SHELL_DASH; 
    176         mc_shell->name = "dash"; 
     370        stdout_return = &stdout_fd; 
     371        pipes_data.n++; 
    177372    } 
    178     else if (strstr (mc_shell->real_path, "/busybox") != NULL) 
     373    else 
     374        flags |= G_SPAWN_STDOUT_TO_DEV_NULL; 
     375 
     376    if (standard_error != NULL) 
    179377    { 
    180         /* If shell is symlinked to busybox, assume it is an ash, even though theoretically 
    181          * it could also be a hush (a mini shell for non-MMU systems deactivated by default). 
    182          * For simplicity's sake we assume that busybox always contains an ash, not a hush. 
    183          * On embedded platforms or on server systems, /bin/sh often points to busybox. 
    184          * Sometimes even bash is symlinked to busybox (CONFIG_FEATURE_BASH_IS_ASH option), 
    185          * so we need to check busybox symlinks *before* checking for the name "bash" 
    186          * in order to avoid that case. */ 
    187         mc_shell->type = SHELL_ASH_BUSYBOX; 
    188         mc_shell->name = mc_shell->path; 
     378        stderr_return = &stderr_fd; 
     379        pipes_data.n++; 
    189380    } 
    190381    else 
    191         mc_shell->type = SHELL_NONE; 
     382        flags |= G_SPAWN_STDERR_TO_DEV_NULL; 
     383 
     384    if (!g_spawn_async_with_pipes (working_directory, argv, envp, flags, 
     385             child_setup, user_data, &pid, stdin_return, stdout_return, stderr_return, error)) 
     386    { 
     387        g_strfreev (envp); 
     388        return FALSE; 
     389    } 
     390 
     391    pipes_data.poll_fds = g_new (int, pipes_data.n); 
     392    pipes_data.buff = g_new (GString*, pipes_data.n); 
     393    pipes_data.n = 0; 
     394 
     395    if ((pipes != NULL) && (pipes->n > 0)) 
     396    { 
     397        pipes_data.n = pipes->n; 
     398 
     399        for (i = 0; i < pipes->n; i++) 
     400        { 
     401            pipes_data.poll_fds[i] = pipes->poll_fds[i]; 
     402            pipes_data.buff[i] = pipes->buff[i]; 
     403            /* close child sides of pipes for the parent */ 
     404            mc_shell_finalize_fd (&pipes->close_fds[i], NULL); 
     405        } 
     406    } 
     407 
     408    if (standard_output != NULL) 
     409    { 
     410        pipes_data.poll_fds[pipes_data.n] = stdout_fd; 
     411        pipes_data.buff[pipes_data.n] = g_string_new (NULL); 
     412        pipes_data.n++; 
     413    } 
     414 
     415    if (standard_error != NULL) 
     416    { 
     417        pipes_data.poll_fds[pipes_data.n] = stderr_fd; 
     418        pipes_data.buff[pipes_data.n] = g_string_new (NULL); 
     419        pipes_data.n++; 
     420    } 
     421 
     422    if (feed_standard_input != NULL) 
     423    { 
     424        pipes_data.poll_fds[pipes_data.n] = stdin_fd; 
     425        pipes_data.buff[pipes_data.n] = g_string_new (feed_standard_input); 
     426        pipes_data.n++; 
     427    } 
     428 
     429    status = mc_shell_child_wait (pid, timeout, &pipes_data); 
     430 
     431    if (exit_status != NULL) 
     432        *exit_status = status; 
     433 
     434    if (feed_standard_input != NULL) 
     435    { 
     436        g_string_free (pipes_data.buff[pipes_data.n - 1], TRUE); 
     437        pipes_data.n--; 
     438    } 
     439 
     440    if (standard_error != NULL) 
     441    { 
     442        *standard_error = g_string_free (pipes_data.buff[pipes_data.n - 1], FALSE); 
     443        pipes_data.n--; 
     444    } 
     445 
     446    if (standard_output != NULL) 
     447    { 
     448        *standard_output = g_string_free (pipes_data.buff[pipes_data.n - 1], FALSE); 
     449        pipes_data.n--; 
     450    } 
     451 
     452    if ((pipes != NULL) && (pipes->n > 0)) 
     453    { 
     454        /* the poll_fds are supposed to be closed now, and pipes_data.poll_fds[i] set to '-1', so let 
     455           the function caller know they're closed by setting pipes->poll_fds[i] to '-1' too */ 
     456        for (i = 0; i < pipes->n; i++) 
     457            pipes->poll_fds[i] = pipes_data.poll_fds[i]; 
     458    } 
     459 
     460    g_free (pipes_data.poll_fds); 
     461    g_free (pipes_data.buff); 
     462 
     463    return TRUE; 
     464} 
     465 
     466/* --------------------------------------------------------------------------------------------- 
     467   This function returns TRUE for a shell if it sets a variable with respective name. We unset 
     468   environmental variable of the same name in the child envp to make sure it's not inherited. 
     469   We use three different commands for the respective shell syntaxes: bourne, C and fish. 
     470   We test for exit code and stdout output to check if the command was successful. 
     471   If we test a shell with a wrong syntax, it returns error code, and function returns FALSE, 
     472   so in fact we test for syntax first, and only then for shell setting the variable. 
     473   --------------------------------------------------------------------------------------------- */ 
     474static gboolean 
     475mc_shell_internal_variable_set (mc_shell_t * mc_shell, const char * name, 
     476                                const shell_syntax_t shell_syntax) 
     477{ 
     478    /* define arg1 instead of using "-c" directly when assigning argv to 
     479       silence [-Wdiscarded-qualifiers] compiler warning */ 
     480    char arg1[] = "-c", * argv[] = {mc_shell->path, arg1, NULL, NULL}, * standard_output = NULL; 
     481    char ** envp; 
     482    int exit_status; 
     483    size_t i, shift, len1, len2; 
     484    gboolean success; 
     485 
     486    /* copy parent's envp and unset the environmental variable in question if set */ 
     487    envp = g_strdupv (environ); 
     488    len1 = g_strv_length (envp); 
     489    len2 = strlen (name); 
     490 
     491    for (i = shift = 0; i < len1; i++) 
     492    { 
     493        if ((strncmp (name, envp [i], len2) == 0) && (envp [i][len2] == '=')) 
     494        { 
     495            g_free (envp [i]); 
     496            shift++; 
     497        } else 
     498            envp [i - shift] = envp [i]; 
     499    } 
     500    for (i = len1 - shift; i < len1; i++) 
     501        envp [i] = NULL; 
     502 
     503    /* for proper shells these commands return 0 exit code and print 'name' value to stdout */ 
     504    if (shell_syntax == SHELL_SYNTAX_BOURNE) 
     505        argv[2] = g_strdup_printf ("if [ -z ${%s+x} ]; then " 
     506                                       "exit 1;" 
     507                                   "else " 
     508                                       "printf '%s';" 
     509                                   "fi", name, name); 
     510    else if (shell_syntax == SHELL_SYNTAX_C) 
     511        argv[2] = g_strdup_printf ("if !( $?%s ) then\n" 
     512                                       "exit 1\n" 
     513                                   "else\n" 
     514                                       "printf '%s'\n" 
     515                                   "endif", name, name); 
     516    else /* shell_syntax == SHELL_SYNTAX_FISH */ 
     517        argv[2] = g_strdup_printf ("if set -q %s;" 
     518                                       "printf '%s';" 
     519                                   "else;" 
     520                                       "exit 1;" 
     521                                   "end", name, name); 
     522 
     523 
     524    success = mc_shell_execute (NULL, argv, envp, 0, NULL, NULL, NULL, &standard_output, 
     525                                NULL, 100, NULL, &exit_status, NULL) && 
     526              g_spawn_check_exit_status (exit_status, NULL) && 
     527              (g_strcmp0  (standard_output, name) == 0); 
     528 
     529    g_free (argv[2]); 
     530    g_free (standard_output); 
     531 
     532    return success; 
    192533} 
    193534 
    194535/* --------------------------------------------------------------------------------------------- */ 
    195536 
    196537static void 
    197 mc_shell_recognize_path (mc_shell_t * mc_shell) 
     538mc_shell_recognize_from_internal_variable (mc_shell_t * mc_shell) 
    198539{ 
    199     /* If shell is not symlinked to busybox, it is safe to assume it is a real shell */ 
    200     if (strstr (mc_shell->path, "/bash") != NULL || getenv ("BASH") != NULL) 
    201     { 
     540    /* These tests recognize bash, zsh, tcsh and fish by testing for 
     541       variables that only these shells set */ 
     542    if (mc_shell_internal_variable_set (mc_shell, "BASH", SHELL_SYNTAX_BOURNE)) { 
    202543        mc_shell->type = SHELL_BASH; 
    203         mc_shell->name = "bash"; 
    204544    } 
    205     else if (strstr (mc_shell->path, "/sh") != NULL || getenv ("SH") != NULL) 
     545    else if (mc_shell_internal_variable_set (mc_shell, "ZSH_NAME", SHELL_SYNTAX_BOURNE)) 
     546    { 
     547        mc_shell->type = SHELL_ZSH; 
     548    } 
     549    else if (mc_shell_internal_variable_set (mc_shell, "tcsh", SHELL_SYNTAX_C)) 
    206550    { 
    207         mc_shell->type = SHELL_SH; 
    208         mc_shell->name = "sh"; 
     551        mc_shell->type = SHELL_TCSH; 
    209552    } 
    210     else if (strstr (mc_shell->path, "/ash") != NULL || getenv ("ASH") != NULL) 
     553    else if (mc_shell_internal_variable_set (mc_shell, "fish_greeting", SHELL_SYNTAX_FISH)) 
    211554    { 
    212         mc_shell->type = SHELL_ASH_BUSYBOX; 
    213         mc_shell->name = "ash"; 
     555        mc_shell->type = SHELL_FISH; 
    214556    } 
    215557    else 
    216558        mc_shell->type = SHELL_NONE; 
    217559} 
    218560 
     561/* --------------------------------------------------------------------------------------------- 
     562   This function tests whether a shell treats PS1 as prompt string that is being expanded. 
     563   We test for an old BusyBox ash 4-digit octal codes bug in printf along the way too. 
     564   mc_shell->type will be set to: 
     565      SHELL_DASH: Test for PS1 expansion succeeds fully. This can mean dash, or BusyBox ash 
     566                  with CONFIG_ASH_EXPAND_PRMT enabled, or something other compatible 
     567      SHELL_ASH_BUSYBOX_LEGACY: PS1 is being expanded, but printf suffers from the 4-digit octal 
     568                                codes bug, so apply the printf workaround 
     569      SHELL_NONE: Test failed. Possible reasons: PS1 is not being treated as a prompt string, 
     570                  PS1 is not being expanded (no CONFIG_ASH_EXPAND_PRMT in BusyBox ash?), 
     571                  shell doesn't recognize syntax, failed to execute shell, etc. 
     572   --------------------------------------------------------------------------------------------- */ 
     573static void 
     574mc_shell_test_prompt_expansion (mc_shell_t * mc_shell) 
     575{ 
     576    /* define arg1 instead of using "-i" directly when assigning argv to 
     577       silence [-Wdiscarded-qualifiers] compiler warning */ 
     578    char arg1[] = "-i", * argv[] = {mc_shell->path, arg1, NULL}, 
     579         ** envp, * standard_input; 
     580    int subshell_pipe[2], exit_status; 
     581    size_t i, len; 
     582 
     583    GString * out_buff = g_string_new (NULL); 
     584    pipes_data_t pipes; 
     585 
     586    if (pipe (subshell_pipe)) 
     587    { 
     588        /* failed to create pipe */ 
     589        mc_shell->type = SHELL_NONE; 
     590        return; 
     591    } 
     592 
     593    /* copy parent's envp and override the PS1 env variable to safe non-interactive if set */ 
     594    envp = g_strdupv (environ); 
     595    len = g_strv_length (envp); 
     596 
     597    for (i = 0; i < len; i++) 
     598    { 
     599        if (strncmp ("PS1=", envp [i], 4) == 0) 
     600        { 
     601            g_free (envp [i]); 
     602            envp [i] = g_strdup ("PS1=$ "); 
     603        } 
     604    } 
     605 
     606    /* Check if executing `PS1='$(printf "%b" "\\0057a\\0057\\n" >&subshell_pipe[WRITE])'` command 
     607    in interactive mode gets PS1 evaluated before the next `exit 0` command, by capturing the pipe 
     608    output, and comparing it to the expected output for dash / BusyBox ash ("/a/"), and 
     609    if it doesn't match - to BusyBox pre 1.20 broken printf output ("\005""7a""\005""7") */ 
     610 
     611    standard_input = g_strdup_printf ("PS1='$(printf \"%%b\" \"\\\\0057a\\\\0057\" >&%d)'\n" 
     612                                      "exit 0\n", subshell_pipe[WRITE]); 
     613 
     614    pipes.buff      = &out_buff;             /* pipes.buff is an array of pointers to GString */ 
     615    pipes.poll_fds  = &subshell_pipe[READ];  /* int array of pipes to poll */ 
     616    pipes.close_fds = &subshell_pipe[WRITE]; /* int array of pipes to close for the parent */ 
     617    pipes.n         = 1;                     /* pipes.n is the number of pipes */ 
     618 
     619    if (mc_shell_execute (NULL, argv, envp, 0, NULL, NULL, standard_input, NULL, 
     620                          NULL, 100, &pipes, &exit_status, NULL) && 
     621        g_spawn_check_exit_status (exit_status, NULL)) 
     622    { 
     623        if (g_strcmp0  (out_buff->str, "/a/") == 0) 
     624            mc_shell->type = SHELL_DASH; 
     625        else if (g_strcmp0  (out_buff->str, "\005""7a""\005""7") == 0) 
     626            mc_shell->type = SHELL_ASH_BUSYBOX_LEGACY; 
     627        else 
     628            mc_shell->type = SHELL_NONE; 
     629    } 
     630    else 
     631        mc_shell->type = SHELL_NONE; 
     632 
     633    g_string_free (out_buff, TRUE); 
     634    g_free (standard_input); 
     635 
     636    /* if mc_shell_execute () failed */ 
     637    mc_shell_finalize_fd (&subshell_pipe[WRITE], NULL); 
     638    mc_shell_finalize_fd (&subshell_pipe[READ], NULL); 
     639} 
     640 
    219641/* --------------------------------------------------------------------------------------------- */ 
    220642/*** public functions ****************************************************************************/ 
    221643/* --------------------------------------------------------------------------------------------- */ 
    mc_shell_init (void) 
    232654 
    233655    mc_shell->real_path = mc_realpath (mc_shell->path, rp_shell); 
    234656 
    235     /* Find out what type of shell we have. Also consider real paths (resolved symlinks) 
    236      * because e.g. csh might point to tcsh, ash to dash or busybox, sh to anything. */ 
     657    /* Find out what type of shell we have. Use tests for specific variables that 
     658     * different shells set for most shell types. To recognize dash, or compatible 
     659     * BusyBox ash, we test whether prompt expansion works. */ 
    237660 
    238     if (mc_shell->real_path != NULL) 
    239         mc_shell_recognize_real_path (mc_shell); 
     661    if (mc_shell->real_path != NULL) { 
     662       mc_shell_recognize_from_internal_variable (mc_shell); 
    240663 
    241     if (mc_shell->type == SHELL_NONE) 
    242         mc_shell_recognize_path (mc_shell); 
     664       if (mc_shell->type == SHELL_NONE) 
     665           mc_shell_test_prompt_expansion (mc_shell); 
    243666 
    244     if (mc_shell->type == SHELL_NONE) 
    245         mc_global.tty.use_subshell = FALSE; 
     667       if (mc_shell->type == SHELL_NONE) 
     668           fprintf (stderr, __FILE__ ": failed to recognize shell \"%s\" as supported subshell. " 
     669                                       "Supported shells are: bash, zsh, tcsh, fish, dash and " 
     670                                       "BusyBox ash with enabled CONFIG_ASH_EXPAND_PRMT\r\n", 
     671                                        mc_shell->path); 
     672   } else { 
     673       mc_shell->type = SHELL_NONE; 
     674       fprintf (stderr, __FILE__ ": wrong \"%s\" shell: No such file\r\n", mc_shell->path); 
     675   } 
    246676 
     677    mc_global.tty.use_subshell = mc_shell->type != SHELL_NONE; 
    247678    mc_global.shell = mc_shell; 
    248679} 
    249680 
  • lib/shell.h

    diff --git a/lib/shell.h b/lib/shell.h
    index 9afcd90..039d5dd 100644
    a b  
    1212typedef enum 
    1313{ 
    1414    SHELL_NONE, 
    15     SHELL_SH, 
    1615    SHELL_BASH, 
    17     SHELL_ASH_BUSYBOX,          /* BusyBox default shell (ash) */ 
    18     SHELL_DASH,                 /* Debian variant of ash */ 
     16    SHELL_DASH,       /* Debian variant of ash, or BusyBox ash shell with CONFIG_ASH_EXPAND_PRMT */ 
     17    SHELL_ASH_BUSYBOX_LEGACY,   /* Legacy BusyBox ash shell with broken printf */ 
    1918    SHELL_TCSH, 
    2019    SHELL_ZSH, 
    2120    SHELL_FISH 
    2221} shell_type_t; 
    2322 
     23typedef enum 
     24{ 
     25    SHELL_SYNTAX_BOURNE, 
     26    SHELL_SYNTAX_C, 
     27    SHELL_SYNTAX_FISH 
     28} shell_syntax_t; 
     29 
    2430/*** structures declarations (and typedefs of structures)*****************************************/ 
    2531 
    2632typedef struct 
    2733{ 
    2834    shell_type_t type; 
    29     const char *name; 
    3035    char *path; 
    3136    char *real_path; 
    3237} mc_shell_t; 
    3338 
     39/* This structure stores information on input/output pipes fds and buffers. 
     40   Input pipes should have their data appended to *(buff[i]), 
     41   and output pipes should have *(buff[i]) written to them. 
     42   On successful writes to output pipes respective *(buff[i]) gets 
     43   gradually emptied, and when all data is written, the write end of the 
     44   pipe should be closed */ 
     45typedef struct 
     46{ 
     47    int * poll_fds; 
     48    int * close_fds; 
     49    GString ** buff; 
     50    int n; 
     51} pipes_data_t; 
     52 
    3453/*** global variables defined in .c file *********************************************************/ 
    3554 
    3655/*** declarations of public functions ************************************************************/ 
    void mc_shell_deinit (void); 
    4059 
    4160/*** inline functions **************************************************/ 
    4261 
     62static inline const char* shell_type_string (shell_type_t shell_type) 
     63{ 
     64    switch (shell_type) 
     65    { 
     66    case SHELL_NONE: 
     67        return "NONE"; 
     68    case SHELL_BASH: 
     69        return "BASH"; 
     70    case SHELL_DASH: 
     71        return "DASH"; 
     72    case SHELL_ASH_BUSYBOX_LEGACY: 
     73        return "ASH_BUSYBOX_LEGACY"; 
     74    case SHELL_TCSH: 
     75        return "TCSH"; 
     76    case SHELL_ZSH: 
     77        return "ZSH"; 
     78    case SHELL_FISH: 
     79        return "FISH"; 
     80    default: 
     81        return "UNKNOWN"; 
     82    } 
     83} 
     84 
    4385#endif /* MC_SHELL_H */ 
  • src/subshell/common.c

    diff --git a/src/subshell/common.c b/src/subshell/common.c
    index a1f054e..b3cf8bc 100644
    a b init_subshell_child (const char *pty_name) 
    249249    tty_resize (subshell_pty_slave); 
    250250 
    251251    /* Set up the subshell's environment and init file name */ 
    252  
    253     /* It simplifies things to change to our home directory here, */ 
    254     /* and the user's startup file may do a 'cd' command anyway   */ 
    255252    { 
    256253        int ret; 
    257254 
    258         ret = chdir (mc_config_get_home_dir ());        /* FIXME? What about when we re-run the subshell? */ 
    259255        (void) ret; 
    260256    } 
    261257 
    init_subshell_child (const char *pty_name) 
    279275        if (!exist_file (init_file)) 
    280276        { 
    281277            g_free (init_file); 
    282             init_file = g_strdup (".bashrc"); 
     278            init_file = g_build_filename (g_getenv ("HOME"), ".bashrc", (char *) NULL); 
    283279        } 
    284280 
    285281        /* Make MC's special commands not show up in bash's history and also suppress 
    init_subshell_child (const char *pty_name) 
    301297 
    302298        break; 
    303299 
    304     case SHELL_ASH_BUSYBOX: 
     300    case SHELL_ASH_BUSYBOX_LEGACY: 
    305301    case SHELL_DASH: 
    306302        /* Do we have a custom init file ~/.local/share/mc/ashrc? */ 
    307303        init_file = mc_config_get_full_path ("ashrc"); 
    init_subshell_child (const char *pty_name) 
    310306        if (!exist_file (init_file)) 
    311307        { 
    312308            g_free (init_file); 
    313             init_file = g_strdup (".profile"); 
     309            init_file = g_build_filename (g_getenv ("HOME"), ".profile", (char *) NULL); 
    314310        } 
    315311 
    316312        /* Put init file to ENV variable used by ash */ 
    init_subshell_child (const char *pty_name) 
    327323        break; 
    328324 
    329325    default: 
    330         fprintf (stderr, __FILE__ ": unimplemented subshell type %u\r\n", mc_global.shell->type); 
     326        fprintf (stderr, __FILE__ ": unimplemented subshell type %s\r\n", shell_type_string (mc_global.shell->type)); 
    331327        my_exit (FORK_FAILURE); 
    332328    } 
    333329 
    init_subshell_child (const char *pty_name) 
    355351    switch (mc_global.shell->type) 
    356352    { 
    357353    case SHELL_BASH: 
    358         execl (mc_global.shell->path, "bash", "-rcfile", init_file, (char *) NULL); 
     354        execl (mc_global.shell->path, mc_global.shell->path, "-rcfile", init_file, (char *) NULL); 
    359355        break; 
    360356 
    361357    case SHELL_ZSH: 
    362358        /* Use -g to exclude cmds beginning with space from history 
    363359         * and -Z to use the line editor on non-interactive term */ 
     360        /* zsh is sensitive about arg0 value to the point of failing 
     361           to initialize subshell when arg0 basename isn't "zsh" */ 
    364362        execl (mc_global.shell->path, "zsh", "-Z", "-g", (char *) NULL); 
    365363 
    366364        break; 
    367365 
    368     case SHELL_ASH_BUSYBOX: 
     366    case SHELL_ASH_BUSYBOX_LEGACY: 
    369367    case SHELL_DASH: 
    370368    case SHELL_TCSH: 
    371369    case SHELL_FISH: 
    init_subshell_precmd (char *precmd, size_t buff_size) 
    801799                    "PS1='\\u@\\h:\\w\\$ '\n", subshell_pipe[WRITE]); 
    802800        break; 
    803801 
    804     case SHELL_ASH_BUSYBOX: 
    805         /* BusyBox ash needs a somewhat complicated precmd emulation via PS1, and it is vital 
    806          * that BB be built with active CONFIG_ASH_EXPAND_PRMT, but this is the default anyway. 
    807          * 
    808          * A: This leads to a stopped subshell (=frozen mc) if user calls "ash" command 
    809          *    "PS1='$(pwd>&%d; kill -STOP $$)\\u@\\h:\\w\\$ '\n", 
    810          * 
    811          * B: This leads to "sh: precmd: not found" in sub-subshell if user calls "ash" command 
    812          *    "precmd() { pwd>&%d; kill -STOP $$; }; " 
    813          *    "PS1='$(precmd)\\u@\\h:\\w\\$ '\n", 
    814          * 
    815          * C: This works if user calls "ash" command because in sub-subshell 
    816          *    PRECMD is unfedined, thus evaluated to empty string - no damage done. 
    817          *    Attention: BusyBox must be built with FEATURE_EDITING_FANCY_PROMPT to 
    818          *    permit \u, \w, \h, \$ escape sequences. Unfortunately this cannot be guaranteed, 
    819          *    especially on embedded systems where people try to save space, so let's use 
    820          *    the dash version below. It should work on virtually all systems. 
    821          *    "precmd() { pwd>&%d; kill -STOP $$; }; " 
    822          *    "PRECMD=precmd; " 
    823          *    "PS1='$(eval $PRECMD)\\u@\\h:\\w\\$ '\n", 
    824          */ 
     802    case SHELL_ASH_BUSYBOX_LEGACY: 
    825803    case SHELL_DASH: 
    826         /* Debian ash needs a precmd emulation via PS1, similar to BusyBox ash, 
    827          * but does not support escape sequences for user, host and cwd in prompt. 
     804        /* Debian ash needs a precmd emulation via PS1. 
    828805         * Attention! Make sure that the buffer for precmd is big enough. 
    829806         * 
    830          * We want to have a fancy dynamic prompt with user@host:cwd just like in the BusyBox 
    831          * examples above, but because replacing the home directory part of the path by "~" is 
     807         * We want to have a fancy dynamic prompt with user@host:cwd, 
     808         * but because replacing the home directory part of the path by "~" is 
    832809         * complicated, it bloats the precmd to a size > BUF_SMALL (128). 
    833810         * 
    834811         * The following example is a little less fancy (home directory not replaced) 
    subshell_name_quote (const char *s) 
    924901        quote_cmd_start = "(printf \"%b\" '"; 
    925902        quote_cmd_end = "')"; 
    926903    } 
    927     /* TODO: When BusyBox printf is fixed, get rid of this "else if", see 
    928        http://lists.busybox.net/pipermail/busybox/2012-March/077460.html */ 
    929     /* else if (subshell_type == ASH_BUSYBOX) 
     904    /* see http://lists.busybox.net/pipermail/busybox/2012-March/077460.html */ 
     905    else if (mc_global.shell->type == SHELL_ASH_BUSYBOX_LEGACY) 
    930906       { 
    931907       quote_cmd_start = "\"`echo -en '"; 
    932908       quote_cmd_end = "'`\""; 
    933        } */ 
     909       } 
    934910    else 
    935911    { 
    936912        quote_cmd_start = "\"`printf \"%b\" '"; 
    init_subshell (void) 
    10591035                return; 
    10601036            } 
    10611037        } 
    1062         else if (pipe (subshell_pipe))  /* subshell_type is BASH, ASH_BUSYBOX, DASH or ZSH */ 
     1038        else if (pipe (subshell_pipe))  /* subshell_type is BASH, ASH_BUSYBOX_LEGACY, DASH or ZSH */ 
    10631039        { 
    10641040            perror (__FILE__ ": couldn't create pipe"); 
    10651041            mc_global.tty.use_subshell = FALSE; 
    init_subshell (void) 
    11021078    tty_disable_interrupt_key (); 
    11031079    if (!subshell_alive) 
    11041080        mc_global.tty.use_subshell = FALSE;     /* Subshell died instantly, so don't use it */ 
     1081    if (!mc_global.tty.use_subshell) 
     1082        fprintf (stderr, __FILE__ ": failed to initialize \"%s\" shell of type %s\r\n", mc_global.shell->path, shell_type_string (mc_global.shell->type)); 
    11051083} 
    11061084 
    11071085/* --------------------------------------------------------------------------------------------- */