summaryrefslogtreecommitdiff
path: root/libopie/oprocess.cpp
Side-by-side diff
Diffstat (limited to 'libopie/oprocess.cpp') (more/less context) (show whitespace changes)
-rw-r--r--libopie/oprocess.cpp922
1 files changed, 922 insertions, 0 deletions
diff --git a/libopie/oprocess.cpp b/libopie/oprocess.cpp
new file mode 100644
index 0000000..f3e52bd
--- a/dev/null
+++ b/libopie/oprocess.cpp
@@ -0,0 +1,922 @@
+/*
+
+ $Id$
+
+ This file is part of the KDE libraries
+ Copyright (C) 1997 Christian Czezatke (e9025461@student.tuwien.ac.at)
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Library General Public
+ License as published by the Free Software Foundation; either
+ version 2 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Library General Public License for more details.
+
+ You should have received a copy of the GNU Library General Public License
+ along with this library; see the file COPYING.LIB. If not, write to
+ the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ Boston, MA 02111-1307, USA.
+
+*/
+
+
+//
+// KPROCESS -- A class for handling child processes in KDE without
+// having to take care of Un*x specific implementation details
+//
+// version 0.3.1, Jan 8th 1998
+//
+// (C) Christian Czezatke
+// e9025461@student.tuwien.ac.at
+//
+// Changes:
+//
+// March 2nd, 1998: Changed parameter list for KShellProcess:
+// Arguments are now placed in a single string so that
+// <shell> -c <commandstring> is passed to the shell
+// to make the use of "operator<<" consistent with KProcess
+//
+//
+// Ported by Holger Freyther
+// <zekce> Harlekin: oprocess and say it was ported to Qt by the Opie developers an Qt 2
+
+
+
+#include "oprocess.h"
+#define _MAY_INCLUDE_KPROCESSCONTROLLER_
+#include "oprocctrl.h"
+
+//#include <config.h>
+
+#include <qfile.h>
+#include <qsocketnotifier.h>
+#include <qregexp.h>
+
+#include <sys/time.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/socket.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <signal.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#ifdef HAVE_SYS_SELECT_H
+#include <sys/select.h>
+#endif
+#ifdef HAVE_INITGROUPS
+#include <grp.h>
+#endif
+#include <pwd.h>
+
+#include <qapplication.h>
+#include <qmap.h>
+//#include <kdebug.h>
+
+/////////////////////////////
+// public member functions //
+/////////////////////////////
+
+class OProcessPrivate {
+public:
+ OProcessPrivate() : useShell(false) { }
+
+ bool useShell;
+ QMap<QString,QString> env;
+ QString wd;
+ QCString shell;
+};
+
+
+OProcess::OProcess()
+ : QObject(),
+ run_mode(NotifyOnExit),
+ runs(false),
+ pid_(0),
+ status(0),
+ keepPrivs(false),
+ innot(0),
+ outnot(0),
+ errnot(0),
+ communication(NoCommunication),
+ input_data(0),
+ input_sent(0),
+ input_total(0),
+ d(0)
+{
+ if (0 == OProcessController::theOProcessController) {
+ (void) new OProcessController();
+ CHECK_PTR(OProcessController::theOProcessController);
+ }
+
+ OProcessController::theOProcessController->addOProcess(this);
+ out[0] = out[1] = -1;
+ in[0] = in[1] = -1;
+ err[0] = err[1] = -1;
+}
+
+void
+OProcess::setEnvironment(const QString &name, const QString &value)
+{
+ if (!d)
+ d = new OProcessPrivate;
+ d->env.insert(name, value);
+}
+
+void
+OProcess::setWorkingDirectory(const QString &dir)
+{
+ if (!d)
+ d = new OProcessPrivate;
+ d->wd = dir;
+}
+
+void
+OProcess::setupEnvironment()
+{
+ if (d)
+ {
+ QMap<QString,QString>::Iterator it;
+ for(it = d->env.begin(); it != d->env.end(); ++it)
+ setenv(QFile::encodeName(it.key()).data(),
+ QFile::encodeName(it.data()).data(), 1);
+ if (!d->wd.isEmpty())
+ chdir(QFile::encodeName(d->wd).data());
+ }
+}
+
+void
+OProcess::setRunPrivileged(bool keepPrivileges)
+{
+ keepPrivs = keepPrivileges;
+}
+
+bool
+OProcess::runPrivileged() const
+{
+ return keepPrivs;
+}
+
+
+OProcess::~OProcess()
+{
+ // destroying the OProcess instance sends a SIGKILL to the
+ // child process (if it is running) after removing it from the
+ // list of valid processes (if the process is not started as
+ // "DontCare")
+
+ OProcessController::theOProcessController->removeOProcess(this);
+ // this must happen before we kill the child
+ // TODO: block the signal while removing the current process from the process list
+
+ if (runs && (run_mode != DontCare))
+ kill(SIGKILL);
+
+ // Clean up open fd's and socket notifiers.
+ closeStdin();
+ closeStdout();
+ closeStderr();
+
+ // TODO: restore SIGCHLD and SIGPIPE handler if this is the last OProcess
+ delete d;
+}
+
+void OProcess::detach()
+{
+ OProcessController::theOProcessController->removeOProcess(this);
+
+ runs = false;
+ pid_ = 0;
+
+ // Clean up open fd's and socket notifiers.
+ closeStdin();
+ closeStdout();
+ closeStderr();
+}
+
+bool OProcess::setExecutable(const QString& proc)
+{
+ if (runs) return false;
+
+ if (proc.isEmpty()) return false;
+
+ if (!arguments.isEmpty())
+ arguments.remove(arguments.begin());
+ arguments.prepend(QFile::encodeName(proc));
+
+ return true;
+}
+
+OProcess &OProcess::operator<<(const QStringList& args)
+{
+ QStringList::ConstIterator it = args.begin();
+ for ( ; it != args.end() ; ++it )
+ arguments.append(QFile::encodeName(*it));
+ return *this;
+}
+
+OProcess &OProcess::operator<<(const QCString& arg)
+{
+ return operator<< (arg.data());
+}
+
+OProcess &OProcess::operator<<(const char* arg)
+{
+ arguments.append(arg);
+ return *this;
+}
+
+OProcess &OProcess::operator<<(const QString& arg)
+{
+ arguments.append(QFile::encodeName(arg));
+ return *this;
+}
+
+void OProcess::clearArguments()
+{
+ arguments.clear();
+}
+
+bool OProcess::start(RunMode runmode, Communication comm)
+{
+ uint i;
+ uint n = arguments.count();
+ char **arglist;
+
+ if (runs || (0 == n)) {
+ return false; // cannot start a process that is already running
+ // or if no executable has been assigned
+ }
+ run_mode = runmode;
+ status = 0;
+
+ QCString shellCmd;
+ if (d && d->useShell)
+ {
+ if (d->shell.isEmpty())
+ {
+ qWarning( "Could not find a valid shell" );
+ return false;
+ }
+
+ arglist = static_cast<char **>(malloc( (4)*sizeof(char *)));
+ for (i=0; i < n; i++) {
+ shellCmd += arguments[i];
+ shellCmd += " "; // CC: to separate the arguments
+ }
+
+ arglist[0] = d->shell.data();
+ arglist[1] = (char *) "-c";
+ arglist[2] = shellCmd.data();
+ arglist[3] = 0;
+ }
+ else
+ {
+ arglist = static_cast<char **>(malloc( (n+1)*sizeof(char *)));
+ for (i=0; i < n; i++)
+ arglist[i] = arguments[i].data();
+ arglist[n]= 0;
+ }
+
+ if (!setupCommunication(comm))
+ qWarning( "Could not setup Communication!");
+
+ // We do this in the parent because if we do it in the child process
+ // gdb gets confused when the application runs from gdb.
+ uid_t uid = getuid();
+ gid_t gid = getgid();
+#ifdef HAVE_INITGROUPS
+ struct passwd *pw = getpwuid(uid);
+#endif
+
+ int fd[2];
+ if (0 > pipe(fd))
+ {
+ fd[0] = fd[1] = 0; // Pipe failed.. continue
+ }
+
+ runs = true;
+
+ QApplication::flushX();
+
+ // WABA: Note that we use fork() and not vfork() because
+ // vfork() has unclear semantics and is not standardized.
+ pid_ = fork();
+
+ if (0 == pid_) {
+ if (fd[0])
+ close(fd[0]);
+ if (!runPrivileged())
+ {
+ setgid(gid);
+#if defined( HAVE_INITGROUPS)
+ if(pw)
+ initgroups(pw->pw_name, pw->pw_gid);
+#endif
+ setuid(uid);
+ }
+ // The child process
+ if(!commSetupDoneC())
+ qWarning( "Could not finish comm setup in child!" );
+
+ setupEnvironment();
+
+ // Matthias
+ if (run_mode == DontCare)
+ setpgid(0,0);
+ // restore default SIGPIPE handler (Harri)
+ struct sigaction act;
+ sigemptyset(&(act.sa_mask));
+ sigaddset(&(act.sa_mask), SIGPIPE);
+ act.sa_handler = SIG_DFL;
+ act.sa_flags = 0;
+ sigaction(SIGPIPE, &act, 0L);
+
+ // We set the close on exec flag.
+ // Closing of fd[1] indicates that the execvp succeeded!
+ if (fd[1])
+ fcntl(fd[1], F_SETFD, FD_CLOEXEC);
+ execvp(arglist[0], arglist);
+ char resultByte = 1;
+ if (fd[1])
+ write(fd[1], &resultByte, 1);
+ _exit(-1);
+ } else if (-1 == pid_) {
+ // forking failed
+
+ runs = false;
+ free(arglist);
+ return false;
+ } else {
+ if (fd[1])
+ close(fd[1]);
+ // the parent continues here
+
+ // Discard any data for stdin that might still be there
+ input_data = 0;
+
+ // Check whether client could be started.
+ if (fd[0]) for(;;)
+ {
+ char resultByte;
+ int n = ::read(fd[0], &resultByte, 1);
+ if (n == 1)
+ {
+ // Error
+ runs = false;
+ close(fd[0]);
+ free(arglist);
+ pid_ = 0;
+ return false;
+ }
+ if (n == -1)
+ {
+ if ((errno == ECHILD) || (errno == EINTR))
+ continue; // Ignore
+ }
+ break; // success
+ }
+ if (fd[0])
+ close(fd[0]);
+
+ if (!commSetupDoneP()) // finish communication socket setup for the parent
+ qWarning( "Could not finish comm setup in parent!" );
+
+ if (run_mode == Block) {
+ commClose();
+
+ // The SIGCHLD handler of the process controller will catch
+ // the exit and set the status
+ while(runs)
+ {
+ OProcessController::theOProcessController->
+ slotDoHousekeeping(0);
+ }
+ runs = FALSE;
+ emit processExited(this);
+ }
+ }
+ free(arglist);
+ return true;
+}
+
+
+
+bool OProcess::kill(int signo)
+{
+ bool rv=false;
+
+ if (0 != pid_)
+ rv= (-1 != ::kill(pid_, signo));
+ // probably store errno somewhere...
+ return rv;
+}
+
+
+
+bool OProcess::isRunning() const
+{
+ return runs;
+}
+
+
+
+pid_t OProcess::pid() const
+{
+ return pid_;
+}
+
+
+
+bool OProcess::normalExit() const
+{
+ int _status = status;
+ return (pid_ != 0) && (!runs) && (WIFEXITED((_status)));
+}
+
+
+
+int OProcess::exitStatus() const
+{
+ int _status = status;
+ return WEXITSTATUS((_status));
+}
+
+
+
+bool OProcess::writeStdin(const char *buffer, int buflen)
+{
+ bool rv;
+
+ // if there is still data pending, writing new data
+ // to stdout is not allowed (since it could also confuse
+ // kprocess...
+ if (0 != input_data)
+ return false;
+
+ if (runs && (communication & Stdin)) {
+ input_data = buffer;
+ input_sent = 0;
+ input_total = buflen;
+ slotSendData(0);
+ innot->setEnabled(true);
+ rv = true;
+ } else
+ rv = false;
+ return rv;
+}
+
+void OProcess::suspend()
+{
+ if ((communication & Stdout) && outnot)
+ outnot->setEnabled(false);
+}
+
+void OProcess::resume()
+{
+ if ((communication & Stdout) && outnot)
+ outnot->setEnabled(true);
+}
+
+bool OProcess::closeStdin()
+{
+ bool rv;
+
+ if (communication & Stdin) {
+ communication = (Communication) (communication & ~Stdin);
+ delete innot;
+ innot = 0;
+ close(in[1]);
+ rv = true;
+ } else
+ rv = false;
+ return rv;
+}
+
+bool OProcess::closeStdout()
+{
+ bool rv;
+
+ if (communication & Stdout) {
+ communication = (Communication) (communication & ~Stdout);
+ delete outnot;
+ outnot = 0;
+ close(out[0]);
+ rv = true;
+ } else
+ rv = false;
+ return rv;
+}
+
+bool OProcess::closeStderr()
+{
+ bool rv;
+
+ if (communication & Stderr) {
+ communication = static_cast<Communication>(communication & ~Stderr);
+ delete errnot;
+ errnot = 0;
+ close(err[0]);
+ rv = true;
+ } else
+ rv = false;
+ return rv;
+}
+
+
+/////////////////////////////
+// protected slots //
+/////////////////////////////
+
+
+
+void OProcess::slotChildOutput(int fdno)
+{
+ if (!childOutput(fdno))
+ closeStdout();
+}
+
+
+void OProcess::slotChildError(int fdno)
+{
+ if (!childError(fdno))
+ closeStderr();
+}
+
+
+void OProcess::slotSendData(int)
+{
+ if (input_sent == input_total) {
+ innot->setEnabled(false);
+ input_data = 0;
+ emit wroteStdin(this);
+ } else
+ input_sent += ::write(in[1], input_data+input_sent, input_total-input_sent);
+}
+
+
+
+//////////////////////////////
+// private member functions //
+//////////////////////////////
+
+
+
+void OProcess::processHasExited(int state)
+{
+ if (runs)
+ {
+ runs = false;
+ status = state;
+
+ commClose(); // cleanup communication sockets
+
+ // also emit a signal if the process was run Blocking
+ if (DontCare != run_mode)
+ {
+ emit processExited(this);
+ }
+ }
+}
+
+
+
+int OProcess::childOutput(int fdno)
+{
+ if (communication & NoRead) {
+ int len = -1;
+ emit receivedStdout(fdno, len);
+ errno = 0; // Make sure errno doesn't read "EAGAIN"
+ return len;
+ }
+ else
+ {
+ char buffer[1024];
+ int len;
+
+ len = ::read(fdno, buffer, 1024);
+
+ if ( 0 < len) {
+ emit receivedStdout(this, buffer, len);
+ }
+ return len;
+ }
+}
+
+
+
+int OProcess::childError(int fdno)
+{
+ char buffer[1024];
+ int len;
+
+ len = ::read(fdno, buffer, 1024);
+
+ if ( 0 < len)
+ emit receivedStderr(this, buffer, len);
+ return len;
+}
+
+
+
+int OProcess::setupCommunication(Communication comm)
+{
+ int ok;
+
+ communication = comm;
+
+ ok = 1;
+ if (comm & Stdin)
+ ok &= socketpair(AF_UNIX, SOCK_STREAM, 0, in) >= 0;
+
+ if (comm & Stdout)
+ ok &= socketpair(AF_UNIX, SOCK_STREAM, 0, out) >= 0;
+
+ if (comm & Stderr)
+ ok &= socketpair(AF_UNIX, SOCK_STREAM, 0, err) >= 0;
+
+ return ok;
+}
+
+
+
+int OProcess::commSetupDoneP()
+{
+ int ok = 1;
+
+ if (communication != NoCommunication) {
+ if (communication & Stdin)
+ close(in[0]);
+ if (communication & Stdout)
+ close(out[1]);
+ if (communication & Stderr)
+ close(err[1]);
+
+ // Don't create socket notifiers and set the sockets non-blocking if
+ // blocking is requested.
+ if (run_mode == Block) return ok;
+
+ if (communication & Stdin) {
+// ok &= (-1 != fcntl(in[1], F_SETFL, O_NONBLOCK));
+ innot = new QSocketNotifier(in[1], QSocketNotifier::Write, this);
+ CHECK_PTR(innot);
+ innot->setEnabled(false); // will be enabled when data has to be sent
+ QObject::connect(innot, SIGNAL(activated(int)),
+ this, SLOT(slotSendData(int)));
+ }
+
+ if (communication & Stdout) {
+// ok &= (-1 != fcntl(out[0], F_SETFL, O_NONBLOCK));
+ outnot = new QSocketNotifier(out[0], QSocketNotifier::Read, this);
+ CHECK_PTR(outnot);
+ QObject::connect(outnot, SIGNAL(activated(int)),
+ this, SLOT(slotChildOutput(int)));
+ if (communication & NoRead)
+ suspend();
+ }
+
+ if (communication & Stderr) {
+// ok &= (-1 != fcntl(err[0], F_SETFL, O_NONBLOCK));
+ errnot = new QSocketNotifier(err[0], QSocketNotifier::Read, this );
+ CHECK_PTR(errnot);
+ QObject::connect(errnot, SIGNAL(activated(int)),
+ this, SLOT(slotChildError(int)));
+ }
+ }
+ return ok;
+}
+
+
+
+int OProcess::commSetupDoneC()
+{
+ int ok = 1;
+ struct linger so;
+ memset(&so, 0, sizeof(so));
+
+ if (communication & Stdin)
+ close(in[1]);
+ if (communication & Stdout)
+ close(out[0]);
+ if (communication & Stderr)
+ close(err[0]);
+
+ if (communication & Stdin)
+ ok &= dup2(in[0], STDIN_FILENO) != -1;
+ else {
+ int null_fd = open( "/dev/null", O_RDONLY );
+ ok &= dup2( null_fd, STDIN_FILENO ) != -1;
+ close( null_fd );
+ }
+ if (communication & Stdout) {
+ ok &= dup2(out[1], STDOUT_FILENO) != -1;
+ ok &= !setsockopt(out[1], SOL_SOCKET, SO_LINGER, (char*)&so, sizeof(so));
+ }
+ else {
+ int null_fd = open( "/dev/null", O_WRONLY );
+ ok &= dup2( null_fd, STDOUT_FILENO ) != -1;
+ close( null_fd );
+ }
+ if (communication & Stderr) {
+ ok &= dup2(err[1], STDERR_FILENO) != -1;
+ ok &= !setsockopt(err[1], SOL_SOCKET, SO_LINGER, reinterpret_cast<char *>(&so), sizeof(so));
+ }
+ else {
+ int null_fd = open( "/dev/null", O_WRONLY );
+ ok &= dup2( null_fd, STDERR_FILENO ) != -1;
+ close( null_fd );
+ }
+ return ok;
+}
+
+
+
+void OProcess::commClose()
+{
+ if (NoCommunication != communication) {
+ bool b_in = (communication & Stdin);
+ bool b_out = (communication & Stdout);
+ bool b_err = (communication & Stderr);
+ if (b_in)
+ delete innot;
+
+ if (b_out || b_err) {
+ // If both channels are being read we need to make sure that one socket buffer
+ // doesn't fill up whilst we are waiting for data on the other (causing a deadlock).
+ // Hence we need to use select.
+
+ // Once one or other of the channels has reached EOF (or given an error) go back
+ // to the usual mechanism.
+
+ int fds_ready = 1;
+ fd_set rfds;
+
+ int max_fd = 0;
+ if (b_out) {
+ fcntl(out[0], F_SETFL, O_NONBLOCK);
+ if (out[0] > max_fd)
+ max_fd = out[0];
+ delete outnot;
+ outnot = 0;
+ }
+ if (b_err) {
+ fcntl(err[0], F_SETFL, O_NONBLOCK);
+ if (err[0] > max_fd)
+ max_fd = err[0];
+ delete errnot;
+ errnot = 0;
+ }
+
+
+ while (b_out || b_err) {
+ // * If the process is still running we block until we
+ // receive data. (p_timeout = 0, no timeout)
+ // * If the process has already exited, we only check
+ // the available data, we don't wait for more.
+ // (p_timeout = &timeout, timeout immediately)
+ struct timeval timeout;
+ timeout.tv_sec = 0;
+ timeout.tv_usec = 0;
+ struct timeval *p_timeout = runs ? 0 : &timeout;
+
+ FD_ZERO(&rfds);
+ if (b_out)
+ FD_SET(out[0], &rfds);
+
+ if (b_err)
+ FD_SET(err[0], &rfds);
+
+ fds_ready = select(max_fd+1, &rfds, 0, 0, p_timeout);
+ if (fds_ready <= 0) break;
+
+ if (b_out && FD_ISSET(out[0], &rfds)) {
+ int ret = 1;
+ while (ret > 0) ret = childOutput(out[0]);
+ if ((ret == -1 && errno != EAGAIN) || ret == 0)
+ b_out = false;
+ }
+
+ if (b_err && FD_ISSET(err[0], &rfds)) {
+ int ret = 1;
+ while (ret > 0) ret = childError(err[0]);
+ if ((ret == -1 && errno != EAGAIN) || ret == 0)
+ b_err = false;
+ }
+ }
+ }
+
+ if (b_in) {
+ communication = (Communication) (communication & ~Stdin);
+ close(in[1]);
+ }
+ if (b_out) {
+ communication = (Communication) (communication & ~Stdout);
+ close(out[0]);
+ }
+ if (b_err) {
+ communication = (Communication) (communication & ~Stderr);
+ close(err[0]);
+ }
+ }
+}
+
+void OProcess::setUseShell(bool useShell, const char *shell)
+{
+ if (!d)
+ d = new OProcessPrivate;
+ d->useShell = useShell;
+ d->shell = shell;
+ if (d->shell.isEmpty())
+ d->shell = searchShell();
+}
+
+QString OProcess::quote(const QString &arg)
+{
+ QString res = arg;
+ res.replace(QRegExp(QString::fromLatin1("\'")),
+ QString::fromLatin1("'\"'\"'"));
+ res.prepend('\'');
+ res.append('\'');
+ return res;
+}
+
+QCString OProcess::searchShell()
+{
+ QCString tmpShell = QCString(getenv("SHELL")).stripWhiteSpace();
+ if (!isExecutable(tmpShell))
+ {
+ tmpShell = "/bin/sh";
+ }
+
+ return tmpShell;
+}
+
+bool OProcess::isExecutable(const QCString &filename)
+{
+ struct stat fileinfo;
+
+ if (filename.isEmpty()) return false;
+
+ // CC: we've got a valid filename, now let's see whether we can execute that file
+
+ if (-1 == stat(filename.data(), &fileinfo)) return false;
+ // CC: return false if the file does not exist
+
+ // CC: anyway, we cannot execute directories, block/character devices, fifos or sockets
+ if ( (S_ISDIR(fileinfo.st_mode)) ||
+ (S_ISCHR(fileinfo.st_mode)) ||
+ (S_ISBLK(fileinfo.st_mode)) ||
+#ifdef S_ISSOCK
+ // CC: SYSVR4 systems don't have that macro
+ (S_ISSOCK(fileinfo.st_mode)) ||
+#endif
+ (S_ISFIFO(fileinfo.st_mode)) ||
+ (S_ISDIR(fileinfo.st_mode)) ) {
+ return false;
+ }
+
+ // CC: now check for permission to execute the file
+ if (access(filename.data(), X_OK) != 0) return false;
+
+ // CC: we've passed all the tests...
+ return true;
+}
+
+void OProcess::virtual_hook( int, void* )
+{ /*BASE::virtual_hook( id, data );*/ }
+
+
+///////////////////////////
+// CC: Class KShellProcess
+///////////////////////////
+
+KShellProcess::KShellProcess(const char *shellname):
+ OProcess()
+{
+ setUseShell(true, shellname);
+}
+
+
+KShellProcess::~KShellProcess() {
+}
+
+QString KShellProcess::quote(const QString &arg)
+{
+ return OProcess::quote(arg);
+}
+
+bool KShellProcess::start(RunMode runmode, Communication comm)
+{
+ return OProcess::start(runmode, comm);
+}
+
+void KShellProcess::virtual_hook( int id, void* data )
+{ OProcess::virtual_hook( id, data ); }
+
+//#include "kprocess.moc"