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: