File indexing completed on 2024-11-24 04:42:26

0001 /*
0002  *  shellprocess.cpp  -  execute a shell process
0003  *  Program:  kalarm
0004  *  SPDX-FileCopyrightText: 2004-2023 David Jarvie <djarvie@kde.org>
0005  *
0006  *  SPDX-License-Identifier: GPL-2.0-or-later
0007  */
0008 
0009 #include "shellprocess.h"
0010 
0011 #include "kalarm_debug.h"
0012 
0013 #include <KLocalizedString>
0014 #include <KAuthorized>
0015 
0016 #include <qplatformdefs.h>
0017 
0018 #include <stdlib.h>
0019 
0020 
0021 QByteArray ShellProcess::mShellName;
0022 QByteArray ShellProcess::mShellPath;
0023 bool       ShellProcess::mInitialised = false;
0024 bool       ShellProcess::mAuthorised  = false;
0025 
0026 
0027 ShellProcess::ShellProcess(const QString& command)
0028     : mCommand(command)
0029 {
0030 }
0031 
0032 /******************************************************************************
0033 * Execute a command.
0034 */
0035 bool ShellProcess::start(OpenMode openMode)
0036 {
0037     if (!authorised())
0038     {
0039         mStatus = Status::Unauthorised;
0040         return false;
0041     }
0042     connect(this, &QIODevice::bytesWritten, this, &ShellProcess::writtenStdin);
0043     connect(this, &QProcess::finished, this, &ShellProcess::slotExited);
0044     connect(this, &QProcess::readyReadStandardOutput, this, &ShellProcess::stdoutReady);
0045     connect(this, &QProcess::readyReadStandardError, this, &ShellProcess::stderrReady);
0046     const QStringList args{ QStringLiteral("-c"), mCommand };
0047     QProcess::start(QLatin1String(shellPath()), args, openMode);
0048     if (!waitForStarted())
0049     {
0050         mStatus = Status::StartFail;
0051         return false;
0052     }
0053     mStatus = Status::Running;
0054     return true;
0055 }
0056 
0057 /******************************************************************************
0058 * Called when a shell process execution completes.
0059 * Interprets the exit status according to which shell was called, and emits
0060 * a shellExited() signal.
0061 */
0062 void ShellProcess::slotExited(int exitCode, QProcess::ExitStatus exitStatus)
0063 {
0064     qCDebug(KALARM_LOG) << "ShellProcess::slotExited:" << exitCode << "," << exitStatus;
0065     mStdinQueue.clear();
0066     mStatus = Status::Success;
0067     mExitCode = exitCode;
0068     if (exitStatus != NormalExit)
0069     {
0070         qCWarning(KALARM_LOG) << "ShellProcess::slotExited:" << mCommand << ":" << mShellName << ": crashed/killed";
0071         mStatus = Status::Died;
0072     }
0073     else
0074     {
0075         // Some shells report if the command couldn't be found, or is not executable
0076         if (((mShellName == "bash" || mShellName == "zsh")  &&  (exitCode == 126 || exitCode == 127))
0077         ||  (mShellName == "ksh"  &&  exitCode == 127))
0078         {
0079             qCWarning(KALARM_LOG) << "ShellProcess::slotExited:" << mCommand << ":" << mShellName << ": not found or not executable";
0080             mStatus = Status::NotFound;
0081         }
0082     }
0083     Q_EMIT shellExited(this);
0084 }
0085 
0086 /******************************************************************************
0087 * Write a string to STDIN.
0088 */
0089 void ShellProcess::writeStdin(const char* buffer, int bufflen)
0090 {
0091     QByteArray scopy(buffer, bufflen);    // construct a deep copy
0092     bool doWrite = mStdinQueue.isEmpty();
0093     mStdinQueue.enqueue(scopy);
0094     if (doWrite)
0095     {
0096         mStdinBytes = std::as_const(mStdinQueue).head().length();
0097         write(std::as_const(mStdinQueue).head());
0098     }
0099 }
0100 
0101 /******************************************************************************
0102 * Called when output to STDIN completes.
0103 * Send the next queued output, if any.
0104 * Note that buffers written to STDIN must not be freed until the bytesWritten()
0105 * signal has been processed.
0106 */
0107 void ShellProcess::writtenStdin(qint64 bytes)
0108 {
0109     mStdinBytes -= bytes;
0110     if (mStdinBytes > 0)
0111         return;   // buffer has only been partially written so far
0112     if (!mStdinQueue.isEmpty())
0113         mStdinQueue.dequeue();   // free the buffer which has now been written
0114     if (!mStdinQueue.isEmpty())
0115     {
0116         mStdinBytes = std::as_const(mStdinQueue).head().length();
0117         write(std::as_const(mStdinQueue).head());
0118     }
0119     else if (mStdinExit)
0120         kill();
0121 }
0122 
0123 /******************************************************************************
0124 * Tell the process to exit once all STDIN strings have been written.
0125 */
0126 void ShellProcess::stdinExit()
0127 {
0128     if (mStdinQueue.isEmpty())
0129         kill();
0130     else
0131         mStdinExit = true;
0132 }
0133 
0134 /******************************************************************************
0135 * Return the error message corresponding to the command exit status.
0136 * Reply = null string if not yet exited, or if command successful.
0137 */
0138 QString ShellProcess::errorMessage() const
0139 {
0140     switch (mStatus)
0141     {
0142         case Status::Unauthorised:
0143             return i18nc("@info", "Failed to execute command (shell access not authorized)");
0144         case Status::StartFail:
0145             return i18nc("@info", "Failed to execute command");
0146         case Status::NotFound:
0147             return i18nc("@info", "Failed to execute command (not found or not executable)");
0148         case Status::Died:
0149             return i18nc("@info", "Command execution error");
0150         case Status::Success:
0151             if (mExitCode)
0152                 return i18nc("@info", "Command exit code: %1", mExitCode);
0153             // Fall through to Inactive
0154             [[fallthrough]];
0155         case Status::Inactive:
0156         case Status::Running:
0157         default:
0158             return {};
0159     }
0160 }
0161 
0162 /******************************************************************************
0163 * Determine which shell to use.
0164 * Don't use the KProcess default shell, since we need to know which shell is
0165 * used in order to decide what its exit code means.
0166 */
0167 const QByteArray& ShellProcess::shellPath()
0168 {
0169     if (mShellPath.isEmpty())
0170     {
0171         // Get the path to the shell
0172         mShellPath = "/bin/sh";
0173         QByteArray envshell = qgetenv("SHELL").trimmed();
0174         if (!envshell.isEmpty())
0175         {
0176             QT_STATBUF fileinfo;
0177             if (QT_STAT(envshell.data(), &fileinfo) != -1  // ensure file exists
0178             &&  !S_ISDIR(fileinfo.st_mode)              // and it's not a directory
0179             &&  !S_ISCHR(fileinfo.st_mode)              // and it's not a character device
0180             &&  !S_ISBLK(fileinfo.st_mode)              // and it's not a block device
0181 #ifdef S_ISSOCK
0182             &&  !S_ISSOCK(fileinfo.st_mode)             // and it's not a socket
0183 #endif
0184             &&  !S_ISFIFO(fileinfo.st_mode)             // and it's not a fifo
0185             &&  !access(envshell.data(), X_OK))         // and it's executable
0186                 mShellPath = envshell;
0187         }
0188 
0189         // Get the shell filename with the path stripped off
0190         int i = mShellPath.lastIndexOf('/');
0191         if (i >= 0)
0192             mShellName = mShellPath.mid(i + 1);
0193         else
0194             mShellName = mShellPath;
0195     }
0196     return mShellPath;
0197 }
0198 
0199 /******************************************************************************
0200 * Check whether shell commands are allowed at all.
0201 */
0202 bool ShellProcess::authorised()
0203 {
0204     if (!mInitialised)
0205     {
0206         mAuthorised = KAuthorized::authorize(QStringLiteral("shell_access"));
0207         mInitialised = true;
0208     }
0209     return mAuthorised;
0210 }
0211 
0212 /******************************************************************************
0213 * Splits a command line at the first non-escaped space character.
0214 */
0215 QString ShellProcess::splitCommandLine(QString& cmdline)
0216 {
0217     if (cmdline.isEmpty())
0218         return cmdline;
0219     QString cmd = cmdline;
0220     // Check for a leading quote
0221     const QChar quote = cmdline[0];
0222     const char q = quote.toLatin1();
0223     bool quoted = (q == '"' || q == '\'');
0224     // Split the command at the first non-escaped space
0225     for (int i = quoted ? 1 : 0, count = cmd.length();  i < count;  ++i)
0226     {
0227         switch (cmd.at(i).toLatin1())
0228         {
0229             case '\\':
0230                 ++i;
0231                 break;
0232             case '"':
0233             case '\'':
0234                 if (quoted  &&  cmd.at(i) == quote)
0235                     quoted = false;
0236                 break;
0237             case ' ':
0238                 if (!quoted)
0239                 {
0240                     cmdline = cmd.mid(i);  // command arguments
0241                     cmd.truncate(i);
0242                     return cmd;
0243                 }
0244                 break;
0245             default:
0246                 break;
0247         }
0248     }
0249     cmdline.clear();
0250     return cmd;
0251 }
0252 
0253 #include "moc_shellprocess.cpp"
0254 
0255 // vim: et sw=4: