Ticket #3692: 3692-mc_subshell.patch
File 3692-mc_subshell.patch, 21.7 KB (added by alllexx88, 8 years ago) |
---|
-
lib/shell.c
Author: alllexx88 <opotapenko@gmail.com> Date: Sat Sep 24 09:41:02 2016 -0700 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 diffirentiate 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 unneded SHELL_SH shell type, and 'name' mc_shell_t field, since the latter was only being used as arg0 when initializing subshell, and it looks like all shells work fine regardless of arg0, except for zsh, (so we just leave arg0 as "zsh" for it), and we use path as arg0 * Also add a little error verbosity in scope of detecting shell type and subshell initialization --- lib/shell.c | 229 ++++++++++++++++++++++++++++++++++++-------------- lib/shell.h | 24 +++++- src/subshell/common.c | 58 ++++--------- 3 files changed, 200 insertions(+), 109 deletions(-) diff --git a/lib/shell.c b/lib/shell.c index 6f07cb0..c036c01 100644
a b 33 33 #include <stdarg.h> 34 34 #include <stdio.h> 35 35 #include <stdlib.h> 36 #include <sys/wait.h> 37 #include <unistd.h> 38 #include <sys/stat.h> 39 #include <fcntl.h> 36 40 37 41 #include "global.h" 38 42 #include "util.h" … … 42 46 43 47 /*** file scope macro definitions ****************************************************************/ 44 48 49 #ifndef WIFEXITED 50 #define WIFEXITED(stat_val) (((stat_val) & 255) == 0) 51 #endif 52 53 #ifndef WIFSIGNALED 54 #define WIFSIGNALED(stat_val) ((((stat_val) & 255) != 255) && !WIFEXITED(stat_val)) 55 #endif 56 57 #ifndef WEXITSTATUS 58 #define WEXITSTATUS(stat_val) ((unsigned)(stat_val) >> 8) 59 #endif 60 45 61 /*** file scope type declarations ****************************************************************/ 46 62 47 63 /*** file scope variables ************************************************************************/ … … mc_shell_get_from_env (void) 137 153 return mc_shell; 138 154 } 139 155 156 /* --------------------------------------------------------------------------------------------- 157 This function returns TRUE for a shell if it sets a variable with respective name. We unset 158 environmental variable of the same name in the child fork to make sure it's not inherited. 159 We use three different commands for the respective shell syntaxes: bourne, C and fish. 160 If we test a shell with a wrong syntax, it returns error code, and function returns FALSE, 161 so in fact we test for syntax first, and only then for shell setting the variable. 162 --------------------------------------------------------------------------------------------- */ 163 static gboolean 164 mc_shell_internal_variable_set (mc_shell_t * mc_shell, const char * name, const shell_syntax_t shell_syntax) 165 { 166 pid_t cpid, w; 167 int status, devNull; 168 char *command; 169 170 171 if (shell_syntax == SHELL_SYNTAX_BOURNE) 172 command = g_strdup_printf ("if [ -z ${%s+x} ]; then exit 1; else exit 0; fi", name); 173 else if (shell_syntax == SHELL_SYNTAX_C) 174 command = g_strdup_printf ("if !( $?%s ) exit 1", name); 175 else /* shell_syntax == SHELL_SYNTAX_FISH */ 176 command = g_strdup_printf ("if set -q %s; exit 0; else; exit 1; end", name); 177 178 179 cpid = fork (); 180 if (cpid == -1) { 181 /* failed to fork */ 182 g_free (command); 183 return FALSE; 184 } 185 186 if (cpid == 0) { /* Code executed by child */ 187 unsetenv (name); 188 /* silence stdout and stderr */ 189 devNull = open ("/dev/null", O_WRONLY); 190 dup2 (devNull, STDERR_FILENO); 191 dup2 (devNull, STDOUT_FILENO); 192 /* execute test command */ 193 execl (mc_shell->path, mc_shell->path, "-c", command, (char *) NULL); 194 /* execl failed */ 195 exit (1); 196 } else { /* Code executed by parent */ 197 g_free (command); 198 do { 199 w = waitpid (cpid, &status, WUNTRACED | WCONTINUED); 200 if (w == -1) { 201 /* waitpid error */ 202 return FALSE; 203 } 204 } while (!WIFEXITED(status) && !WIFSIGNALED(status)); 205 206 return (WIFEXITED(status)) && (WEXITSTATUS(status) == 0); 207 } 208 } 209 140 210 /* --------------------------------------------------------------------------------------------- */ 141 211 142 212 static void 143 mc_shell_recognize_ real_path(mc_shell_t * mc_shell)213 mc_shell_recognize_from_internal_variable (mc_shell_t * mc_shell) 144 214 { 145 if (strstr (mc_shell->path, "/zsh") != NULL || strstr (mc_shell->real_path, "/zsh") != NULL 146 || getenv ("ZSH_VERSION") != NULL) 215 /* These tests recognize bash, zsh, tcsh and fish by testing for 216 variables that only these shells set */ 217 if (mc_shell_internal_variable_set (mc_shell, "BASH", SHELL_SYNTAX_BOURNE)) 147 218 { 148 /* Also detects ksh symlinked to zsh */ 149 mc_shell->type = SHELL_ZSH; 150 mc_shell->name = "zsh"; 219 mc_shell->type = SHELL_BASH; 151 220 } 152 else if (strstr (mc_shell->path, "/tcsh") != NULL 153 || strstr (mc_shell->real_path, "/tcsh") != NULL) 221 else if (mc_shell_internal_variable_set (mc_shell, "ZSH_NAME", SHELL_SYNTAX_BOURNE)) 154 222 { 155 /* Also detects csh symlinked to tcsh */ 156 mc_shell->type = SHELL_TCSH; 157 mc_shell->name = "tcsh"; 223 mc_shell->type = SHELL_ZSH; 158 224 } 159 else if (strstr (mc_shell->path, "/csh") != NULL 160 || strstr (mc_shell->real_path, "/csh") != NULL) 225 else if (mc_shell_internal_variable_set (mc_shell, "tcsh", SHELL_SYNTAX_C)) 161 226 { 162 227 mc_shell->type = SHELL_TCSH; 163 mc_shell->name = "csh";164 228 } 165 else if (strstr (mc_shell->path, "/fish") != NULL 166 || strstr (mc_shell->real_path, "/fish") != NULL) 229 else if (mc_shell_internal_variable_set (mc_shell, "fish_greeting", SHELL_SYNTAX_FISH)) 167 230 { 168 231 mc_shell->type = SHELL_FISH; 169 mc_shell->name = "fish";170 }171 else if (strstr (mc_shell->path, "/dash") != NULL172 || strstr (mc_shell->real_path, "/dash") != NULL)173 {174 /* Debian ash (also found if symlinked to by ash/sh) */175 mc_shell->type = SHELL_DASH;176 mc_shell->name = "dash";177 }178 else if (strstr (mc_shell->real_path, "/busybox") != NULL)179 {180 /* If shell is symlinked to busybox, assume it is an ash, even though theoretically181 * 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;189 232 } 190 233 else 191 234 mc_shell->type = SHELL_NONE; 192 235 } 193 236 194 /* --------------------------------------------------------------------------------------------- */ 195 237 /* --------------------------------------------------------------------------------------------- 238 This function tests whether a shell treats PS1 as prompt string that is being expanded. 239 We test for an old BusyBox ash 4-digit octal codes bug in printf along the way too. 240 mc_shell->type will be set to: 241 SHELL_DASH: Test for PS1 expansion succeeds fully. This can mean dash, or BusyBox ash 242 with CONFIG_ASH_EXPAND_PRMT enabled, or something other compatible 243 SHELL_ASH_BUSYBOX_LEGACY: PS1 is being expanded, but printf suffers from the 4-digit octal 244 codes bug, so apply the printf workaround 245 SHELL_NONE: Test failed. Possible reasons: PS1 is not being treated as a prompt string, 246 PS1 is not being expanded (no CONFIG_ASH_EXPAND_PRMT in BusyBox ash?), 247 shell doesn't recognize syntax, failed to execute shell, etc. 248 --------------------------------------------------------------------------------------------- */ 196 249 static void 197 mc_shell_ recognize_path(mc_shell_t * mc_shell)250 mc_shell_test_prompt_expansion (mc_shell_t * mc_shell) 198 251 { 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 { 202 mc_shell->type = SHELL_BASH; 203 mc_shell->name = "bash"; 204 } 205 else if (strstr (mc_shell->path, "/sh") != NULL || getenv ("SH") != NULL) 206 { 207 mc_shell->type = SHELL_SH; 208 mc_shell->name = "sh"; 209 } 210 else if (strstr (mc_shell->path, "/ash") != NULL || getenv ("ASH") != NULL) 211 { 212 mc_shell->type = SHELL_ASH_BUSYBOX; 213 mc_shell->name = "ash"; 214 } 215 else 216 mc_shell->type = SHELL_NONE; 252 pid_t cpid, w; 253 int status, devNull; 254 char *command; 255 gboolean firstrun = TRUE; 256 257 258 do { 259 /* Now this looks complicated, but the idea is simple: to check if 260 after setting PS1='$(printf "%b" "\\0057a\\0057\\n" >&3)' in interactive mode, 261 it gets evaluated, by capturing 3-rd descriptor output, and comparing it to the expected 262 output for dash / BusyBox ash ("/a/") during first run, and if it doesn't match - 263 test again to compare to BusyBox pre 1.20 broken printf output ("7a7") */ 264 if (firstrun) 265 command = g_strdup_printf ("str=$( (printf \"PS1='\"'$(printf \"%%%%b\" \"\\\\0057a\\\\0057\\\\n\" >&3)'\"'\\nexit 0\\n\" | %s -i 1>/dev/null) 3>&1); if [ \"$str\" = \"/a/\" ]; then exit 0; else exit 1; fi", mc_shell->path); 266 else 267 command = g_strdup_printf ("str=$( (printf \"PS1='\"'$(printf \"%%%%b\" \"\\\\0057a\\\\0057\\\\n\" >&3)'\"'\\nexit 0\\n\" | %s -i 1>/dev/null) 3>&1); if [ \"$str\" = \"7a7\" ]; then exit 0; else exit 1; fi", mc_shell->path); 268 269 cpid = fork (); 270 if (cpid == -1) { 271 /* failed to fork */ 272 g_free (command); 273 mc_shell->type = SHELL_NONE; 274 return; 275 } 276 277 if (cpid == 0) { /* Code executed by child */ 278 /* silence stdout and stderr */ 279 devNull = open ("/dev/null", O_WRONLY); 280 dup2 (devNull, STDERR_FILENO); 281 dup2 (devNull, STDOUT_FILENO); 282 /* execute test command */ 283 execl (mc_shell->path, mc_shell->path, "-c", command, (char *) NULL); 284 /* execl failed */ 285 exit (1); 286 } else { /* Code executed by parent */ 287 g_free (command); 288 do { 289 w = waitpid (cpid, &status, WUNTRACED | WCONTINUED); 290 if (w == -1) { 291 /* waitpid error */ 292 mc_shell->type = SHELL_NONE; 293 return; 294 } 295 } while (!WIFEXITED(status) && !WIFSIGNALED(status)); 296 297 if ((WIFEXITED(status)) && (WEXITSTATUS(status) == 0)) { 298 if (firstrun) 299 mc_shell->type = SHELL_DASH; 300 else 301 mc_shell->type = SHELL_ASH_BUSYBOX_LEGACY; 302 return; 303 } 304 } 305 firstrun = !firstrun; 306 } while (!firstrun); 307 308 /* both tests failed */ 309 mc_shell->type = SHELL_NONE; 217 310 } 218 311 219 312 /* --------------------------------------------------------------------------------------------- */ … … mc_shell_init (void) 232 325 233 326 mc_shell->real_path = mc_realpath (mc_shell->path, rp_shell); 234 327 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. */ 328 /* Find out what type of shell we have. Use tests for specific variables that 329 * different shells set for most shell types. To recognize dash, or compatible 330 * BusyBox ash, we test whether prompt expansion works. */ 237 331 238 if (mc_shell->real_path != NULL) 239 mc_shell_recognize_real_path(mc_shell);332 if (mc_shell->real_path != NULL) { 333 mc_shell_recognize_from_internal_variable (mc_shell); 240 334 241 if (mc_shell->type == SHELL_NONE)242 mc_shell_recognize_path(mc_shell);335 if (mc_shell->type == SHELL_NONE) 336 mc_shell_test_prompt_expansion (mc_shell); 243 337 244 if (mc_shell->type == SHELL_NONE) 245 mc_global.tty.use_subshell = FALSE; 338 if (mc_shell->type == SHELL_NONE) 339 fprintf (stderr, __FILE__ ": failed to recognize shell \"%s\" as supported subshell. Supported shells are: bash, zsh, tcsh, fish, dash and BusyBox ash with enabled CONFIG_ASH_EXPAND_PRMT\r\n", mc_shell->path); 340 } else { 341 mc_shell->type = SHELL_NONE; 342 fprintf (stderr, __FILE__ ": wrong \"%s\" shell: No such file\r\n", mc_shell->path); 343 } 246 344 345 mc_global.tty.use_subshell = mc_shell->type != SHELL_NONE; 247 346 mc_global.shell = mc_shell; 248 347 } 249 348 -
lib/shell.h
diff --git a/lib/shell.h b/lib/shell.h index 9afcd90..e48f52c 100644
a b 7 7 8 8 /*** typedefs(not structures) and defined constants **********************************************/ 9 9 10 #define SHELL_TYPE_STRING(shell_type) (\ 11 shell_type == SHELL_NONE ? "NONE" : (\ 12 shell_type == SHELL_BASH ? "BASH" : (\ 13 shell_type == SHELL_DASH ? "DASH" : (\ 14 shell_type == SHELL_ASH_BUSYBOX_LEGACY ? "ASH_BUSYBOX_LEGACY" : (\ 15 shell_type == SHELL_TCSH ? "TCSH" : (\ 16 shell_type == SHELL_ZSH ? "ZSH" : (\ 17 shell_type == SHELL_FISH ? "FISH" : \ 18 "UNKNOWN" \ 19 ))))))) 20 10 21 /*** enums ***************************************************************************************/ 11 22 12 23 typedef enum 13 24 { 14 25 SHELL_NONE, 15 SHELL_SH,16 26 SHELL_BASH, 17 SHELL_ ASH_BUSYBOX, /* BusyBox default shell (ash)*/18 SHELL_ DASH, /* Debian variant of ash*/27 SHELL_DASH, /* Debian variant of ash, or BusyBox ash shell with CONFIG_ASH_EXPAND_PRMT */ 28 SHELL_ASH_BUSYBOX_LEGACY, /* Legacy BusyBox ash shell with broken printf */ 19 29 SHELL_TCSH, 20 30 SHELL_ZSH, 21 31 SHELL_FISH 22 32 } shell_type_t; 23 33 34 typedef enum 35 { 36 SHELL_SYNTAX_BOURNE, 37 SHELL_SYNTAX_C, 38 SHELL_SYNTAX_FISH 39 } shell_syntax_t; 40 24 41 /*** structures declarations (and typedefs of structures)*****************************************/ 25 42 26 43 typedef struct 27 44 { 28 45 shell_type_t type; 29 const char *name;30 46 char *path; 31 47 char *real_path; 32 48 } mc_shell_t; -
src/subshell/common.c
diff --git a/src/subshell/common.c b/src/subshell/common.c index 98968fa..085b805 100644
a b init_subshell_child (const char *pty_name) 249 249 tty_resize (subshell_pty_slave); 250 250 251 251 /* 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 */255 252 { 256 253 int ret; 257 254 258 ret = chdir (mc_config_get_home_dir ()); /* FIXME? What about when we re-run the subshell? */259 255 (void) ret; 260 256 } 261 257 … … init_subshell_child (const char *pty_name) 279 275 if (!exist_file (init_file)) 280 276 { 281 277 g_free (init_file); 282 init_file = g_ strdup (".bashrc");278 init_file = g_build_filename (g_getenv ("HOME"), ".bashrc", (char *) NULL); 283 279 } 284 280 285 281 /* Make MC's special commands not show up in bash's history and also suppress … … init_subshell_child (const char *pty_name) 301 297 302 298 break; 303 299 304 case SHELL_ASH_BUSYBOX :300 case SHELL_ASH_BUSYBOX_LEGACY: 305 301 case SHELL_DASH: 306 302 /* Do we have a custom init file ~/.local/share/mc/ashrc? */ 307 303 init_file = mc_config_get_full_path ("ashrc"); … … init_subshell_child (const char *pty_name) 310 306 if (!exist_file (init_file)) 311 307 { 312 308 g_free (init_file); 313 init_file = g_ strdup (".profile");309 init_file = g_build_filename (g_getenv ("HOME"), ".profile", (char *) NULL); 314 310 } 315 311 316 312 /* Put init file to ENV variable used by ash */ … … init_subshell_child (const char *pty_name) 327 323 break; 328 324 329 325 default: 330 fprintf (stderr, __FILE__ ": unimplemented subshell type % d\r\n", mc_global.shell->type);326 fprintf (stderr, __FILE__ ": unimplemented subshell type %s\r\n", SHELL_TYPE_STRING(mc_global.shell->type)); 331 327 my_exit (FORK_FAILURE); 332 328 } 333 329 … … init_subshell_child (const char *pty_name) 355 351 switch (mc_global.shell->type) 356 352 { 357 353 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); 359 355 break; 360 356 361 357 case SHELL_ZSH: 362 358 /* Use -g to exclude cmds beginning with space from history 363 359 * and -Z to use the line editor on non-interactive term */ 364 360 execl (mc_global.shell->path, "zsh", "-Z", "-g", (char *) NULL); 365 361 366 362 break; 367 363 368 case SHELL_ASH_BUSYBOX :364 case SHELL_ASH_BUSYBOX_LEGACY: 369 365 case SHELL_DASH: 370 366 case SHELL_TCSH: 371 367 case SHELL_FISH: … … init_subshell_precmd (char *precmd, size_t buff_size) 801 797 "PS1='\\u@\\h:\\w\\$ '\n", subshell_pipe[WRITE]); 802 798 break; 803 799 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 */ 800 case SHELL_ASH_BUSYBOX_LEGACY: 825 801 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. 802 /* Debian ash needs a precmd emulation via PS1. 828 803 * Attention! Make sure that the buffer for precmd is big enough. 829 804 * 830 * We want to have a fancy dynamic prompt with user@host:cwd just like in the BusyBox831 * examples above,but because replacing the home directory part of the path by "~" is805 * We want to have a fancy dynamic prompt with user@host:cwd, 806 * but because replacing the home directory part of the path by "~" is 832 807 * complicated, it bloats the precmd to a size > BUF_SMALL (128). 833 808 * 834 809 * The following example is a little less fancy (home directory not replaced) … … subshell_name_quote (const char *s) 924 899 quote_cmd_start = "(printf \"%b\" '"; 925 900 quote_cmd_end = "')"; 926 901 } 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) 902 /* see http://lists.busybox.net/pipermail/busybox/2012-March/077460.html */ 903 else if (mc_global.shell->type == SHELL_ASH_BUSYBOX_LEGACY) 930 904 { 931 905 quote_cmd_start = "\"`echo -en '"; 932 906 quote_cmd_end = "'`\""; 933 } */907 } 934 908 else 935 909 { 936 910 quote_cmd_start = "\"`printf \"%b\" '"; … … init_subshell (void) 1059 1033 return; 1060 1034 } 1061 1035 } 1062 else if (pipe (subshell_pipe)) /* subshell_type is BASH, ASH_BUSYBOX , DASH or ZSH */1036 else if (pipe (subshell_pipe)) /* subshell_type is BASH, ASH_BUSYBOX_LEGACY, DASH or ZSH */ 1063 1037 { 1064 1038 perror (__FILE__ ": couldn't create pipe"); 1065 1039 mc_global.tty.use_subshell = FALSE; … … init_subshell (void) 1102 1076 tty_disable_interrupt_key (); 1103 1077 if (!subshell_alive) 1104 1078 mc_global.tty.use_subshell = FALSE; /* Subshell died instantly, so don't use it */ 1079 if (!mc_global.tty.use_subshell) 1080 fprintf (stderr, __FILE__ ": failed to initialize \"%s\" shell of type %s\r\n", mc_global.shell->path, SHELL_TYPE_STRING(mc_global.shell->type)); 1105 1081 } 1106 1082 1107 1083 /* --------------------------------------------------------------------------------------------- */