From: Tulio A M Mendes Date: Tue, 17 Feb 2026 07:15:18 +0000 (-0300) Subject: feat: shell job control (&, &&, ||) and CTRL+C/CTRL+Z support X-Git-Url: https://projects.tadryanom.me/docs/static/gitweb.js?a=commitdiff_plain;h=684d84b0ed8ca035597be1807b6659268fad8035;p=AdrOS.git feat: shell job control (&, &&, ||) and CTRL+C/CTRL+Z support 1. Background processes (&): trailing & forks without waiting, prints [bg] PID. Works for simple commands and pipelines. 2. Command chaining (&&): executes next command only if previous succeeded (exit status 0). Skips remaining && chain on failure until a || or ; is found. 3. OR chaining (||): executes next command only if previous failed (exit status != 0). Skips remaining || chain on success until a && or ; is found. 4. CTRL+C / CTRL+Z: shell ignores SIGINT/SIGTSTP/SIGQUIT. Child processes get their own process group (setpgid) and are set as the foreground group (TIOCSPGRP). CTRL+C sends SIGINT only to the child, not the shell. After child exits, shell restores itself as foreground group. New files: - user/ulibc/include/sys/wait.h: WIFEXITED/WIFSIGNALED/etc macros Modified: - user/sh.c: process_line rewritten for ;/&&/||/& operators, run_simple and run_pipeline use setpgid+TIOCSPGRP job control - user/ulibc/include/termios.h: added TIOCSPGRP/TIOCGPGRP --- diff --git a/user/sh.c b/user/sh.c index 64087b6..051b4d0 100644 --- a/user/sh.c +++ b/user/sh.c @@ -7,6 +7,8 @@ * - Command history (up/down arrow keys) * - Pipes (cmd1 | cmd2 | cmd3) * - Redirections (< > >>) + * - Operators: ; && || & + * - Job control: CTRL+C (SIGINT), CTRL+Z (SIGTSTP), background (&) * - Builtins: cd, exit, echo, export, unset, set, pwd, type * - PATH-based command resolution * - Quote handling (single and double quotes) @@ -18,6 +20,9 @@ #include #include #include +#include +#include +#include static struct termios orig_termios; @@ -531,8 +536,16 @@ static const char* resolve(const char* cmd) { return cmd; } +/* ---- Helper: set foreground process group on controlling TTY ---- */ + +static void set_fg_pgrp(int pgrp) { + ioctl(STDIN_FILENO, TIOCSPGRP, &pgrp); +} + /* ---- Run a single simple command ---- */ +static int bg_flag = 0; /* set by caller when trailing & detected */ + static void run_simple(char* cmd) { /* Expand variables */ char expanded[LINE_MAX]; @@ -709,7 +722,15 @@ static void run_simple(char* cmd) { if (pid < 0) { fprintf(stderr, "sh: fork failed\n"); return; } if (pid == 0) { - /* child */ + /* child: own process group, restore default signals */ + setpgid(0, 0); + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = (uintptr_t)SIG_DFL; + sigaction(SIGINT, &sa, NULL); + sigaction(SIGTSTP, &sa, NULL); + sigaction(SIGQUIT, &sa, NULL); + if (redir_in) { int fd = open(redir_in, O_RDONLY); if (fd >= 0) { dup2(fd, 0); close(fd); } @@ -725,10 +746,21 @@ static void run_simple(char* cmd) { _exit(127); } - /* parent waits */ - int st; - waitpid(pid, &st, 0); - last_status = st; + /* parent */ + setpgid(pid, pid); + + if (bg_flag) { + /* Background: don't wait, print job info */ + printf("[bg] %d\n", pid); + last_status = 0; + } else { + /* Foreground: make child the fg process group, wait, then restore */ + set_fg_pgrp(pid); + int st; + waitpid(pid, &st, 0); + set_fg_pgrp(getpgrp()); + last_status = st; + } goto restore_redir; restore_redir: @@ -748,6 +780,7 @@ static void run_pipeline(char* cmdline) { if (*p == '\'' && !in_dq) in_sq = !in_sq; else if (*p == '"' && !in_sq) in_dq = !in_dq; else if (*p == '|' && !in_sq && !in_dq && ncmds < 7) { + if (*(p + 1) == '|') { p++; continue; } /* skip || */ *p = '\0'; cmds[++ncmds] = p + 1; } @@ -762,6 +795,7 @@ static void run_pipeline(char* cmdline) { /* Multi-stage pipeline */ int prev_rd = -1; int pids[8]; + int pgid = 0; /* pipeline process group = first child's PID */ for (int i = 0; i < ncmds; i++) { int pfd[2] = {-1, -1}; @@ -776,6 +810,16 @@ static void run_pipeline(char* cmdline) { if (pids[i] < 0) { fprintf(stderr, "sh: fork failed\n"); return; } if (pids[i] == 0) { + /* child: join pipeline process group, restore signals */ + int mypgid = pgid ? pgid : getpid(); + setpgid(0, mypgid); + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = (uintptr_t)SIG_DFL; + sigaction(SIGINT, &sa, NULL); + sigaction(SIGTSTP, &sa, NULL); + sigaction(SIGQUIT, &sa, NULL); + if (prev_rd >= 0) { dup2(prev_rd, 0); close(prev_rd); } if (pfd[1] >= 0) { dup2(pfd[1], 1); close(pfd[1]); } if (pfd[0] >= 0) close(pfd[0]); @@ -793,7 +837,10 @@ static void run_pipeline(char* cmdline) { _exit(127); } - /* parent */ + /* parent: set pipeline pgid */ + if (i == 0) pgid = pids[0]; + setpgid(pids[i], pgid); + if (prev_rd >= 0) close(prev_rd); if (pfd[1] >= 0) close(pfd[1]); prev_rd = pfd[0]; @@ -801,39 +848,110 @@ static void run_pipeline(char* cmdline) { if (prev_rd >= 0) close(prev_rd); - /* Wait for all children */ - for (int i = 0; i < ncmds; i++) { - int st; - waitpid(pids[i], &st, 0); - if (i == ncmds - 1) last_status = st; + if (!bg_flag) { + /* Foreground pipeline: make it fg, wait, restore */ + set_fg_pgrp(pgid); + for (int i = 0; i < ncmds; i++) { + int st; + waitpid(pids[i], &st, 0); + if (i == ncmds - 1) last_status = st; + } + set_fg_pgrp(getpgrp()); + } else { + printf("[bg] %d\n", pgid); + last_status = 0; } } -/* ---- Process a command line (handle ; and &&) ---- */ +/* ---- Process a command line (handle ;, &&, ||, &) ---- */ + +enum { OP_NONE = 0, OP_SEMI, OP_AND, OP_OR, OP_BG }; static void process_line(char* input) { - /* Split on ';' */ char* p = input; + while (*p) { - /* skip leading whitespace */ while (*p == ' ' || *p == '\t') p++; if (*p == '\0') break; + /* Find the next operator outside quotes */ char* start = p; int in_sq = 0, in_dq = 0; + int op = OP_NONE; + while (*p) { - if (*p == '\'' && !in_dq) in_sq = !in_sq; - else if (*p == '"' && !in_sq) in_dq = !in_dq; - else if (*p == ';' && !in_sq && !in_dq) break; + if (*p == '\'' && !in_dq) { in_sq = !in_sq; p++; continue; } + if (*p == '"' && !in_sq) { in_dq = !in_dq; p++; continue; } + if (in_sq || in_dq) { p++; continue; } + + if (*p == '&' && *(p + 1) == '&') { + *p = '\0'; p += 2; op = OP_AND; break; + } + if (*p == '|' && *(p + 1) == '|') { + *p = '\0'; p += 2; op = OP_OR; break; + } + if (*p == ';') { + *p = '\0'; p++; op = OP_SEMI; break; + } + if (*p == '&') { + *p = '\0'; p++; op = OP_BG; break; + } p++; } - char saved = *p; - if (*p) *p++ = '\0'; - if (start[0] != '\0') - run_pipeline(start); + /* Trim leading whitespace from segment */ + while (*start == ' ' || *start == '\t') start++; + if (*start != '\0') { + if (op == OP_BG) { + bg_flag = 1; + run_pipeline(start); + bg_flag = 0; + } else { + bg_flag = 0; + run_pipeline(start); + } + } + + /* For &&: skip remaining commands if last failed */ + if (op == OP_AND && last_status != 0) { + /* Skip until we hit || or ; or & or end */ + while (*p) { + while (*p == ' ' || *p == '\t') p++; + if (*p == '\0') break; + int skip_sq = 0, skip_dq = 0; + while (*p) { + if (*p == '\'' && !skip_dq) { skip_sq = !skip_sq; p++; continue; } + if (*p == '"' && !skip_sq) { skip_dq = !skip_dq; p++; continue; } + if (skip_sq || skip_dq) { p++; continue; } + if (*p == '|' && *(p + 1) == '|') break; + if (*p == ';') break; + if (*p == '&' && *(p + 1) != '&') break; + if (*p == '&' && *(p + 1) == '&') { p += 2; continue; } + p++; + } + break; + } + } - if (saved == '\0') break; + /* For ||: skip remaining commands if last succeeded */ + if (op == OP_OR && last_status == 0) { + while (*p) { + while (*p == ' ' || *p == '\t') p++; + if (*p == '\0') break; + int skip_sq = 0, skip_dq = 0; + while (*p) { + if (*p == '\'' && !skip_dq) { skip_sq = !skip_sq; p++; continue; } + if (*p == '"' && !skip_sq) { skip_dq = !skip_dq; p++; continue; } + if (skip_sq || skip_dq) { p++; continue; } + if (*p == '&' && *(p + 1) == '&') break; + if (*p == ';') break; + if (*p == '&' && *(p + 1) != '&') break; + if (*p == '|' && *(p + 1) == '|') { p += 2; continue; } + p++; + } + break; + } + } } } @@ -880,6 +998,18 @@ int main(int argc, char** argv, char** envp) { if (!var_get("HOME")) var_set("HOME", "/", 1); + /* Job control: put shell in its own process group and make it fg */ + setpgid(0, 0); + set_fg_pgrp(getpgrp()); + + /* Ignore job control signals in the shell itself */ + struct sigaction sa_ign; + memset(&sa_ign, 0, sizeof(sa_ign)); + sa_ign.sa_handler = (uintptr_t)SIG_IGN; + sigaction(SIGINT, &sa_ign, NULL); + sigaction(SIGTSTP, &sa_ign, NULL); + sigaction(SIGQUIT, &sa_ign, NULL); + tty_raw_mode(); print_prompt(); diff --git a/user/ulibc/include/sys/wait.h b/user/ulibc/include/sys/wait.h new file mode 100644 index 0000000..b0d1515 --- /dev/null +++ b/user/ulibc/include/sys/wait.h @@ -0,0 +1,25 @@ +#ifndef ULIBC_SYS_WAIT_H +#define ULIBC_SYS_WAIT_H + +/* AdrOS wait status encoding: + * Normal exit: exit_status = code (from _exit(code)) + * Signal kill: exit_status = 128 + sig + * + * We provide Linux-compatible macros that work with AdrOS's simple encoding. + * Since AdrOS passes exit_status directly (not Linux-style packed), these + * macros approximate the behavior: + * - exit code 0..127 → normal exit + * - exit code 128+ → killed by signal (128+sig) + */ + +#define WNOHANG 1 +#define WUNTRACED 2 + +#define WIFEXITED(s) ((s) < 128) +#define WEXITSTATUS(s) (s) +#define WIFSIGNALED(s) ((s) >= 128) +#define WTERMSIG(s) ((s) - 128) +#define WIFSTOPPED(s) 0 +#define WSTOPSIG(s) 0 + +#endif diff --git a/user/ulibc/include/termios.h b/user/ulibc/include/termios.h index 4fdb006..1d4c157 100644 --- a/user/ulibc/include/termios.h +++ b/user/ulibc/include/termios.h @@ -30,10 +30,12 @@ #define VTIME 9 /* ioctl commands */ -#define TCGETS 0x5401 -#define TCSETS 0x5402 -#define TCSETSW 0x5403 -#define TCSETSF 0x5404 +#define TCGETS 0x5401 +#define TCSETS 0x5402 +#define TCSETSW 0x5403 +#define TCSETSF 0x5404 +#define TIOCGPGRP 0x540F +#define TIOCSPGRP 0x5410 struct termios { uint32_t c_iflag;