-1

I'm actually working on project and we're trying to recreate a bash terminal in C. I've got some problems about handling pipes in my code. As you know when you write "cat | ls", it first shows ls result then standard input is opened but only for one line. The problem is the way I'm handling pipes, in first it shows the ls result as excepted but after the standard input is reading until I make CTRL-D. I want to make it end after the first line, like bash does. I think I have a problem with my pipes but I tried to remake things in other ways and it's always the same problem.

My code :

void    make_command(t_list *cmds, char **env)
{
    char    *path;
    int     exec;
    
    path = get_path((char *)cmds->content[0], env);
    if (!path)
        return ;
    exec = execve(path, (char **)cmds->content, env);
    if (exec < 0)
        exit_error();
}

void    make_pipe(t_list *cmds, char **env, t_listpids **pids, int *fd_old)
{
    pid_t       pid;
    int         tube[2];

    while (cmds)
    {
        if (pipe(tube) == -1)
            exit_error();
        pid = fork();
        if (pid == 0)
        {
            if (cmds->previous) //only if not first command
            {
                dup2((*fd_old), STDIN_FILENO);
                close(*fd_old);
            }
            if (cmds->next) //only if not last command
            {
                dup2(tube[1], STDOUT_FILENO);
                close(tube[1]);
            }
            make_command(cmds, env); //execute command function with execve
            close(tube[0]);
            close(tube[1]);
            exit(EXIT_SUCCESS);
        }
        else
        {
            add_pids(pid, pids); //add pid to chained list of pids (used to waitpid them after)
            *fd_old = tube[0];
            close(tube[1]);
            cmds = cmds->next;
            while (cmds && ft_strncmp((char *)cmds->content[0], "|", 1) == 0)
                cmds = cmds->next;
        }
    }
}
oguz ismail
  • 1
  • 16
  • 47
  • 69
  • 2
    `cat | ls` makes no sense. `ls` doesn't read its standard input, so piping to it doesn't do anything useful. Did you mean `ls | cat`? – Barmar Feb 27 '23 at 21:05
  • yeah I know it's a stupid case, but we have to handle in the same way that bash does. – ethaaalpha Feb 27 '23 at 21:07
  • 2
    Huh? *As you know when you write "cat | ls", it first shows ls result then standard input is opened but only for one line* -- um. No. Pipes don't operate line-by-line at all, and the pipeline is set up before `ls` starts running at all. This statement doesn't have any rational relationship with the truth. – Charles Duffy Feb 27 '23 at 21:14
  • 1
    _"then standard input is opened but only for one line"_ - `cat` will terminate when it fails to write to `stdout` which will happen as soon as `ls` dies. – Ted Lyngmo Feb 27 '23 at 21:15
  • Yes, what I describe is really what `cat` and `ls` does too – Ted Lyngmo Feb 27 '23 at 21:16
  • @EmanuelP, no, it's really not. `ls` has its stdin connected to `cat`'s stdout _before it starts to execute at all_, so the claim that `ls` prints and _then_ stdin is opened has no rational relationship to any kind of truth. – Charles Duffy Feb 27 '23 at 21:16
  • @EmanuelP, huh? `cat` doesn't ever try to "write back" anything, because it never receives anything on its stdin. – Charles Duffy Feb 27 '23 at 21:17
  • @CharlesDuffy i'm probably explaining it in the wrong way but Ted said it in a better way – ethaaalpha Feb 27 '23 at 21:17
  • 1
    @EmanuelP, if I use `strace` to follow this in practice, it's a SIGPIPE that kills `cat` -- sent by the OS kernel, not the shell. – Charles Duffy Feb 27 '23 at 21:18
  • @EmanuelP, instead of insulting the people who disagree with you, how about defending your assertions? It **can't possibly** be true that "standard input is opened" _after_ `ls` runs, _because the shell sets up each executable's part of the pipeline before it `exec`s that executable at all_, unless we're disagreeing on some part of what those words mean. Indeed, after an executable has been `exec`'d, the shell is no longer able to run code in that PID, so _of course_ the shell can no longer make changes on how that process's input or output is wired. – Charles Duffy Feb 27 '23 at 21:21
  • Yes, sorry. I was just confused about the issue for a moment. – Emanuel P Feb 27 '23 at 21:23
  • (well -- sometimes an EPIPE, if its stdin is connected to a TTY; it just exits because it has nothing to do, if stdin is connected to `/dev/null`, so details do depend on the runtime environment) – Charles Duffy Feb 27 '23 at 21:26
  • i found something that maybe can explain this case, https://unix.stackexchange.com/questions/673855/i-dont-understand-cat-behaviour-when-running-cat-ls @CharlesDuffy But i still can't see how to handle it – ethaaalpha Feb 27 '23 at 21:26
  • @ethaaalpha, so what's happening here is that `cat` exits _when it tries to write to its stdout_, because that's when an EPIPE or SIGPIPE is triggered (assuming that stdout is a FIFO where the other side is a copy of `ls` that already exited). For it to write to its stdout, it has to read something -- anything, even one byte of input -- from its stdin. If it's just blocking without anything to read, _that's_ where you have your interim sitting-around-doing-nothing state. – Charles Duffy Feb 27 '23 at 21:27
  • @ethaaalpha, ...so, one way you could fail to reproduce this behavior is if your shell isn't even `wait()`ing for the earlier parts of the pipeline, so it doesn't _notice_ that `cat` is still running even though `ls` exited. (With real bash, it _has_ to `wait()` so it can collect exit status details to populate the PIPESTATUS array) – Charles Duffy Feb 27 '23 at 21:29
  • @CharlesDuffy okay thanks for the explanation it's better for me – ethaaalpha Feb 27 '23 at 21:30
  • Maybe I'm missing something, but couldn't you just use `popen("cat | ls")`? – Stephen Quan Feb 27 '23 at 21:30
  • 1
    @StephenQuan, someone isn't _recreating_ a shell in that case, they're just using the existing installed `/bin/sh`. Defeats the purpose of the exercise. (There are also plenty of good reasons to avoid `system()` or `popen()` in real-world code; it's difficult to parameterize without introducing security issues) – Charles Duffy Feb 27 '23 at 21:31
  • yeah the goal of this projects is to handle fork(), pipe(), dup2().. for the execution part – ethaaalpha Feb 27 '23 at 21:35
  • (What explains the "for one line" behavior in practice, btw, is buffering; if stdin and stdout are configured to be completely unbuffered you'd see the failure at the first character) – Charles Duffy Feb 27 '23 at 21:37
  • @CharlesDuffy, I don't see what you were responding to, but `cat` certainly can, and by default *does*, receive data on its stdin. – John Bollinger Feb 27 '23 at 21:37
  • @JohnBollinger, it does, but in the scenario posited its only stdin is that inherited from the invocation environment, so it's at that environment's mercy. `/dev/null`? Immediately gracefully closes. Contains content that's immediately readable? Echo it immediately and get a EPIPE. All reads block? Then the pipeline blocks too, even when `ls` has finished and exited. Nothing _in the code or scenario presented_ puts content on `cat`'s stdin – Charles Duffy Feb 27 '23 at 21:38
  • I see, @CharlesDuffy. It was not clear from context that you were speaking about the behavior of a specific run of `cat` under the circumstances presented here, as opposed to the behavior of the `cat` command in general. Thanks for clarifying. – John Bollinger Feb 27 '23 at 21:45
  • so is there someone seeing something that i can do to fix that ? – ethaaalpha Feb 27 '23 at 21:50
  • @ethaaalpha It's hard to say exacly where there's a problem in your code by just reading the snippets. I made a [mre] (without error checking to keep it short) which should display the correct behavior. [here](https://pastebin.com/Htn34Nhv) – Ted Lyngmo Feb 27 '23 at 22:06
  • @TedLyngmo okay, thanks i'm looking at the code – ethaaalpha Feb 27 '23 at 22:12
  • @ethaaalpha You're welcome! If you can't get it to work, please update your question and remove all but the essential bits and make it possible for us to compile and run it _as-is_. – Ted Lyngmo Feb 27 '23 at 22:15

1 Answers1

1

As you know when you write "cat | ls", it first shows ls result then standard input is opened but only for one line.

That's a confusing characterization of what happens with that largely nonsensical pipeline. Here's a better one: ls runs to completion without waiting for any input, meanwhile cat waits for input on its standard input, and terminates after reading one line, without echoing that line to the terminal.

Important distinctions between the two:

  • the ls command does not run first in the sense of forcing cat to wait for it. It simply does not itself have to wait for any user input, therefore its output is displayed immediately.

  • no new standard input is "opened". The cat command inherits its standard input from the shell, and reads from that file until it terminates. It receives the input instead of the shell by virtue of being in the foreground.

Additionally, that cat terminates after reading one line is incidental. It might read more if more were queued up for it to read immediately, or perhaps if the ls ran slowly for some reason. When it does terminate, it's because it tries to write to a pipe that has no readers. That causes a SIGPIPE to be delivered to it, which causes it to terminate.

And that is all relevant, because it points towards the issue with the (mis)behavior you describe. That in your case, the cat keeps reading input until you type a Ctrl-D tells me right away that either its standard output is not connected to a pipe at all, or else that the pipe to which it is connected is still open for reading somewhere.

And knowing what to look for, we can satisfy ourselves that yes, the parent process fails to close the read end of every one of the pipes it creates. It preserves them, one at a time, in *fd_old, so as to be able to redirect the next child's standard input, but after providing for that redirection, it leaks its copy of the open file descriptor.

It only needs to hold that FD open until the next fork(), so perhaps what you want is to close it in the else block, something like this:

        else
        {
            if (cmds->previous) //only if not first command
            {
                close(*fd_old);
            }
            add_pids(pid, pids); //add pid to chained list of pids (used to waitpid them after)
            *fd_old = tube[0];
            close(tube[1]);
            cmds = cmds->next;
            while (cmds && ft_strncmp((char *)cmds->content[0], "|", 1) == 0)
                cmds = cmds->next;
        }

You may also need to close the last one after the loop terminates. I would ordinarily think so, but the fact that you are storing it indirectly, via a pointer, leaves me uncertain as to what exactly you're trying to accomplish.

John Bollinger
  • 160,171
  • 8
  • 81
  • 157