summaryrefslogtreecommitdiff
path: root/zen/process_exec.cpp
blob: 76a2e4368595256e03c06e68a7ffbb9118d93f5c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
// *****************************************************************************
// * This file is part of the FreeFileSync project. It is distributed under    *
// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0          *
// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved *
// *****************************************************************************

#include "process_exec.h"
//#include <chrono>
#include "guid.h"
#include "file_access.h"
#include "file_io.h"

    #include <unistd.h> //fork, pipe
    #include <sys/wait.h> //waitpid
    #include <fcntl.h>

using namespace zen;


Zstring zen::escapeCommandArg(const Zstring& arg)
{
//*INDENT-OFF*    if not put exactly here, Astyle will seriously mess this .cpp file up!
    Zstring output;
    for (const char c : arg)
        switch (c)
        {
            //case  ' ': output += "\\ "; break; -> maybe nicer to use quotes instead?
            case  '"': output += "\\\""; break; //Windows: not needed; " cannot be used as file name
            case '\\': output += "\\\\"; break; //Windows: path separator! => don't escape
            case '`':  output += "\\`";  break; //yes, used in some paths => Windows: no escaping required
            default:   output += c; break;
        }
    if (contains(arg, ' '))
        output = '"' + output + '"'; //caveat: single-quotes not working on macOS if string contains escaped chars! no such issue on Linux

    return output;
//*INDENT-ON*
}




namespace
{
std::pair<int /*exit code*/, std::string> processExecuteImpl(const Zstring& filePath, const std::vector<Zstring>& arguments,
                                                             std::optional<int> timeoutMs) //throw SysError, SysErrorTimeOut
{
    const Zstring tempFilePath = appendPath(getTempFolderPath(), //throw FileError
                                            Zstr("FFS-") + utfTo<Zstring>(formatAsHexString(generateGUID())));
    /*  can't use popen(): does NOT return the exit code on Linux (despite the documentation!), although it works correctly on macOS
          => use pipes instead: https://linux.die.net/man/2/waitpid
             bonus: no need for "2>&1" to redirect STDERR to STDOUT

             What about premature exit via SysErrorTimeOut?
                Linux: child process' end of the pipe *still works* even after the parent process is gone:
                        There does not seem to be any output buffer size limit + no observable strain on system memory or disk space! :)
                macOS: child process exits if parent end of pipe is closed: fuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu..........

       => solution: buffer output in temporary file

       Unresolved problem: premature exit via SysErrorTimeOut (=> no waitpid()) creates zombie proceses:
                "As long as a zombie is not removed from the system via a wait,
                 it will consume a slot in the kernel process table, and if this table fills,
                 it will not be possible to create further processes."                 */

    const int EC_CHILD_LAUNCH_FAILED = 120; //avoid 127: used by the system, e.g. failure to execute due to missing .so file

    //use O_TMPFILE? sounds nice, but support is probably crap: https://github.com/libvips/libvips/issues/1151
    const int fdTempFile = ::open(tempFilePath.c_str(), O_CREAT | O_EXCL | O_RDWR | O_CLOEXEC,
                                  S_IRUSR | S_IWUSR); //0600
    if (fdTempFile == -1)
        THROW_LAST_SYS_ERROR("open(" + utfTo<std::string>(tempFilePath) + ")");
    auto guardTmpFile = makeGuard<ScopeGuardRunMode::onExit>([&] { ::close(fdTempFile); });

    //"deleting while handle is open" == FILE_FLAG_DELETE_ON_CLOSE
    if (::unlink(tempFilePath.c_str()) != 0)
        THROW_LAST_SYS_ERROR("unlink");

    //--------------------------------------------------------------
    //waitpid() is a useless pile of garbage without time out => check EOF from dummy pipe instead
    int pipe[2] = {};
    if (::pipe2(pipe, O_CLOEXEC) != 0)
        THROW_LAST_SYS_ERROR("pipe2");

    const int fdLifeSignR = pipe[0]; //for parent process
    const int fdLifeSignW = pipe[1]; //for child process
    ZEN_ON_SCOPE_EXIT(::close(fdLifeSignR));
    auto guardFdLifeSignW = makeGuard<ScopeGuardRunMode::onExit>([&] { ::close(fdLifeSignW ); });

    //--------------------------------------------------------------

    //follow implemenation of ::system(): https://github.com/lattera/glibc/blob/master/sysdeps/posix/system.c
    const pid_t pid = ::fork();
    if (pid < 0) //pids are never negative, empiric proof: https://linux.die.net/man/2/wait
        THROW_LAST_SYS_ERROR("fork");

    if (pid == 0) //child process
        try
        {
            //first task: set STDOUT redirection in case an error needs to be reported
            if (::dup2(fdTempFile, STDOUT_FILENO) != STDOUT_FILENO) //O_CLOEXEC does NOT propagate with dup2()
                THROW_LAST_SYS_ERROR("dup2(STDOUT)");

            if (::dup2(fdTempFile, STDERR_FILENO) != STDERR_FILENO) //O_CLOEXEC does NOT propagate with dup2()
                THROW_LAST_SYS_ERROR("dup2(STDERR)");

            //avoid blocking scripts waiting for user input
            // => appending " < /dev/null" is not good enough! e.g. hangs for: read -p "still hanging here"; echo fuuuuu...
            const int fdDevNull = ::open("/dev/null", O_RDONLY | O_CLOEXEC);
            if (fdDevNull == -1) //don't check "< 0" -> docu seems to allow "-2" to be a valid file handle
                THROW_LAST_SYS_ERROR("open(/dev/null)");
            ZEN_ON_SCOPE_EXIT(::close(fdDevNull));

            if (::dup2(fdDevNull, STDIN_FILENO) != STDIN_FILENO) //O_CLOEXEC does NOT propagate with dup2()
                THROW_LAST_SYS_ERROR("dup2(STDIN)");

            //*leak* the fd and have it closed automatically on child process exit after execv()
            if (::dup(fdLifeSignW) == -1) //O_CLOEXEC does NOT propagate with dup()
                THROW_LAST_SYS_ERROR("dup(fdLifeSignW)");

            std::vector<const char*> argv{filePath.c_str()};
            for (const Zstring& arg : arguments)
                argv.push_back(arg.c_str());
            argv.push_back(nullptr);

            /*int rv =*/::execv(argv[0], const_cast<char**>(argv.data())); //only returns if an error occurred
            //safe to cast away const: https://pubs.opengroup.org/onlinepubs/9699919799/functions/exec.html
            //  "The statement about argv[] and envp[] being constants is included to make explicit to future
            //   writers of language bindings that these objects are completely constant. Due to a limitation of
            //   the ISO C standard, it is not possible to state that idea in standard C."
            THROW_LAST_SYS_ERROR("execv");
        }
        catch (const SysError& e)
        {
            ::puts(utfTo<std::string>(e.toString()).c_str());
            ::fflush(stdout); //note: stderr is unbuffered by default
            ::_exit(EC_CHILD_LAUNCH_FAILED); //[!] avoid flushing I/O buffers or doing other clean up from child process like with "exit()"!
        }
    //else: parent process


    if (timeoutMs)
    {
        guardFdLifeSignW.dismiss();
        ::close(fdLifeSignW); //[!] make sure we get EOF when fd is closed by child!

        const int flags = ::fcntl(fdLifeSignR, F_GETFL);
        if (flags == -1)
            THROW_LAST_SYS_ERROR("fcntl(F_GETFL)");

        //fcntl() success: Linux: 0
        //                 macOS: "Value other than -1."
        if (::fcntl(fdLifeSignR, F_SETFL, flags | O_NONBLOCK) == -1)
            THROW_LAST_SYS_ERROR("fcntl(F_SETFL, O_NONBLOCK)");


        const auto stopTime = std::chrono::steady_clock::now() + std::chrono::milliseconds(*timeoutMs);
        for (;;) //EINTR handling? => allow interruption!?
        {
            //read until EAGAIN
            char buf[16];
            const ssize_t bytesRead = ::read(fdLifeSignR, buf, sizeof(buf));
            if (bytesRead < 0)
            {
                if (errno != EAGAIN)
                    THROW_LAST_SYS_ERROR("read");
            }
            else if (bytesRead > 0)
                throw SysError(formatSystemError("read", L"", L"Unexpected data."));
            else //bytesRead == 0: EOF
                break;

            //wait for stream input
            const auto now = std::chrono::steady_clock::now();
            if (now > stopTime)
                throw SysErrorTimeOut(_P("Operation timed out after 1 second.", "Operation timed out after %x seconds.", *timeoutMs / 1000));

            const auto waitTimeMs = std::chrono::duration_cast<std::chrono::milliseconds>(stopTime - now).count();

            timeval tv{.tv_sec = static_cast<long>(waitTimeMs / 1000)};
            tv.tv_usec = static_cast<long>(waitTimeMs - tv.tv_sec * 1000) * 1000;

            fd_set rfd{}; //includes FD_ZERO
            FD_SET(fdLifeSignR, &rfd);

            if (const int rv = ::select(fdLifeSignR + 1, //int nfds = "highest-numbered file descriptor in any of the three sets, plus 1"
                                        &rfd,            //fd_set* readfds
                                        nullptr,         //fd_set* writefds
                                        nullptr,         //fd_set* exceptfds
                                        &tv);            //struct timeval* timeout
                rv < 0)
                THROW_LAST_SYS_ERROR("select");
            else if (rv == 0)
                throw SysErrorTimeOut(_P("Operation timed out after 1 second.", "Operation timed out after %x seconds.", *timeoutMs / 1000));
        }
    }

    //https://linux.die.net/man/2/waitpid
    int statusCode = 0;
    if (::waitpid(pid,         //pid_t pid
                  &statusCode, //int* status
                  0) != pid)   //int options
        THROW_LAST_SYS_ERROR("waitpid");


    if (::lseek(fdTempFile, 0, SEEK_SET) != 0)
        THROW_LAST_SYS_ERROR("lseek");

    guardTmpFile.dismiss();
    FileInputPlain streamIn(fdTempFile, tempFilePath); //pass ownership!

    std::string output = unbufferedLoad<std::string>([&](void* buffer, size_t bytesToRead)
    {
        return streamIn.tryRead(buffer, bytesToRead); //throw FileError; may return short, only 0 means EOF! =>  CONTRACT: bytesToRead > 0!
    },
    streamIn.getBlockSize()); //throw FileError

    if (!WIFEXITED(statusCode)) //signalled, crashed?
        throw SysError(formatSystemError("waitpid", WIFSIGNALED(statusCode) ?
                                         L"Killed by signal " + numberTo<std::wstring>(WTERMSIG(statusCode)) :
                                         L"Exit status "      + numberTo<std::wstring>(statusCode),
                                         utfTo<std::wstring>(trimCpy(output))));

    const int exitCode = WEXITSTATUS(statusCode); //precondition: "WIFEXITED() == true"
    if (exitCode == EC_CHILD_LAUNCH_FAILED || //child process should already have provided details to STDOUT
        exitCode == 127) //details should have been streamed to STDERR: used by /bin/sh, e.g. failure to execute due to missing .so file
        throw SysError(utfTo<std::wstring>(trimCpy(output)));

    return {exitCode, std::move(output)};
}
}


std::pair<int /*exit code*/, Zstring> zen::consoleExecute(const Zstring& cmdLine, std::optional<int> timeoutMs) //throw SysError, SysErrorTimeOut
{
    const auto& [exitCode, output] = processExecuteImpl("/bin/sh", {"-c", cmdLine.c_str()}, timeoutMs); //throw SysError, SysErrorTimeOut
    return {exitCode, copyStringTo<Zstring>(output)};
}


void zen::openWithDefaultApp(const Zstring& itemPath) //throw FileError
{
    try
    {
        std::optional<int> timeoutMs;
        const Zstring cmdTemplate = R"(xdg-open "%x")"; //*might* block!
        timeoutMs = 0; //e.g. on Lubuntu if Firefox is started and not already running => no need for time out! https://freefilesync.org/forum/viewtopic.php?t=8260
        const Zstring cmdLine = replaceCpy(cmdTemplate, Zstr("%x"), itemPath);

        if (const auto& [exitCode, output] = consoleExecute(cmdLine, timeoutMs); //throw SysError, SysErrorTimeOut
            exitCode != 0)
            throw SysError(formatSystemError(utfTo<std::string>(cmdTemplate),
                                             replaceCpy(_("Exit code %x"), L"%x", numberTo<std::wstring>(exitCode)), utfTo<std::wstring>(output)));
    }
    catch (SysErrorTimeOut&) {} //child process not failed yet => probably fine :>
    catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot open file %x."), L"%x", fmtPath(itemPath)), e.toString()); }
}


bgstack15