proc_open(), Again. Always returns TRUE???

PHP programming forum. Ask questions or help people concerning PHP code. Don't understand a function? Need help implementing a class? Don't understand a class? Here is where to ask. Remember to do your homework!

Moderator: General Moderators

Post Reply
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

proc_open(), Again. Always returns TRUE???

Post by Chris Corbyn »

I try to start a non existent process with proc_open() which claims it returns false on failure, however, this is what happens:

Code: Select all

proc_open() returned:

resource(52, process)

The pipes are:

array
  0 => resource(49, stream)
  1 => resource(50, stream)
  2 => resource(51, stream)

std_err says:

string 'sh: /usr/sbin/sendmail_WTF: not found
' (length=38)
/bin/sh is ALWAYS running the process so it never returns false. I can't just read stderr neither because it nothing has been sent to stderr and you try to read from it, the script hangs indefinitely. feof() says that stderr has NEVER reached the end, and stream_get_contents() just hangs anyway.

WTF? Is there any way at all of working out if a process opened by proc_open() is actually valid? This is driving me nuts. What makes it worse it that I tried doing this manually by using is_executable() before starting the process but that method is returning false on some servers even when the binary exists and can be started -- that's getting frustrating too:(

Anyone got even the vaguest of suggestions that will work with PHP 4.3 to PHP 5.2, on both windows and UNIX?
User avatar
volka
DevNet Evangelist
Posts: 8391
Joined: Tue May 07, 2002 9:48 am
Location: Berlin, ger

Post by volka »

May we see the script?
User avatar
Jenk
DevNet Master
Posts: 3587
Joined: Mon Sep 19, 2005 6:24 am
Location: London

Post by Jenk »

Nothing short of a convoluted exec/shell_exec/`` springs to mind unfortunately.
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

Jenk wrote:Nothing short of a convoluted exec/shell_exec/`` springs to mind unfortunately.
For a path that doesn't even exist:

Code: Select all

The command I'm going to run is: /usr/sbin/sendmail_WTF -bs
The result of proc_open() is: resource(36) of type (process)
The pipes are: array(3) {
  [0]=>
  resource(33) of type (stream)
  [1]=>
  resource(34) of type (stream)
  [2]=>
  resource(35) of type (stream)
}
std_err says: string(62) "sh: line 1: /usr/sbin/sendmail_WTF: No such file or directory
"
Now, I have to comment out the fgets() if the process does exist or it will hang, but here it when it works:

Code: Select all

The command I'm going to run is: /usr/sbin/sendmail -bs
The result of proc_open() is: resource(36) of type (process)
The pipes are: array(3) {
  [0]=>
  resource(33) of type (stream)
  [1]=>
  resource(34) of type (stream)
  [2]=>
  resource(35) of type (stream)
}
And here's the class (I could just check for a response when it runs in interactive mode, but I don't want to eliminate support for -t mode (non-interactive)).

Important bit:

Code: Select all

/**
	 * Try to start the connection
	 * @throws Swift_Connection_Exception Upon failure to start
	 */
	public function start()
	{
		if (!$this->getPath() || !$this->getFlags())
		{
			throw new Swift_Connection_Exception("Sendmail cannot be started without a path to the binary including flags.");
		}
		
//		if (!is_executable($this->getPath()))
//		{
//			throw new Swift_Connection_Exception("Sendmail cannot be started.  The path to the executable given [" . $this->getPath() . "] does not appear to be valid.");
//		}
			
		$pipes_spec = array(
			array("pipe", "r"),
			array("pipe", "w"),
			array("pipe", "w")
		);
		
		$this->proc = proc_open($this->getCommand(), $pipes_spec, $this->pipes);
		echo "The command I'm going to run is: " . $this->getCommand() . "\n";
		echo "The result of proc_open() is: "; var_dump($this->proc);
		echo "The pipes are: "; var_dump($this->pipes);
		echo "std_err says: "; var_dump(fgets($this->pipes[2])); //This causes a hang if the process did start
		exit();
		if (!$this->isAlive())
		{
			throw new Swift_Connection_Exception("The sendmail process failed to start.  Please verify that the path exists and PHP has permission to execute it.");
		}
	}
Full class:

Code: Select all

<?php

/**
 * Swift Mailer Sendmail Connection component.
 * Please read the LICENSE file
 * @author Chris Corbyn <chris@w3style.co.uk>
 * @package Swift_Connection
 * @license GNU Lesser General Public License
 */
 
require_once dirname(__FILE__) . "/../ClassLoader.php";
Swift_ClassLoader::load("Swift_ConnectionBase");

/**
 * Swift Sendmail Connection
 * @package Swift_Connection
 * @author Chris Corbyn <chris@w3style.co.uk>
 */
class Swift_Connection_Sendmail extends Swift_ConnectionBase
{
	/**
	 * Constant for auto-detection of paths
	 */
	const AUTO_DETECT = -2;
	/**
	 * Flags for the MTA (options such as bs or t)
	 * @var string
	 */
	protected $flags = null;
	/**
	 * The full path to the MTA
	 * @var string
	 */
	protected $path = null;
	/**
	 * The type of last request sent
	 * For example MAIL, RCPT, DATA
	 * @var string
	 */
	protected $request = null;
	/**
	 * The process handle
	 * @var resource
	 */
	protected $proc;
	/**
	 * I/O pipes for the process
	 * @var array
	 */
	protected $pipes;
	/**
	 * Switches to true for just one command when DATA has been issued
	 * @var boolean
	 */
	protected $send = false;
	/**
	 * The timeout in seconds before giving up
	 * @var int Seconds
	 */
	protected $timeout = 10;
	
	/**
	 * Constructor
	 * @param string The command to execute
	 * @param int The timeout in seconds before giving up
	 */
	public function __construct($command="/usr/sbin/sendmail -bs", $timeout=10)
	{
		$this->setCommand($command);
		$this->setTimeout($timeout);
	}
	/**
	 * Set the timeout on the process
	 * @param int The number of seconds
	 */
	public function setTimeout($secs)
	{
		$this->timeout = (int)$secs;
	}
	/**
	 * Get the timeout on the process
	 * @return int
	 */
	public function getTimeout()
	{
		return $this->timeout;
	}
	/**
	 * Set the operating flags for the MTA
	 * @param string
	 */
	public function setFlags($flags)
	{
		$this->flags = $flags;
	}
	/**
	 * Get the operating flags for the MTA
	 * @return string
	 */
	public function getFlags()
	{
		return $this->flags;
	}
	/**
	 * Set the path to the binary
	 * @param string The path (must be absolute!)
	 */
	public function setPath($path)
	{
		if ($path == self::AUTO_DETECT) $path = $this->findSendmail();
		$this->path = $path;
	}
	/**
	 * Get the path to the binary
	 * @return string
	 */
	public function getPath()
	{
		return $this->path;
	}
	/**
	 * For auto-detection of sendmail path
	 * Thanks to "Joe Cotroneo" for providing the enhancement
	 * @return string
	 */
	public function findSendmail()
	{
		$path = @trim(shell_exec('which sendmail'));
		if (!is_executable($path))
		{
			$common_locations = array(
				'/usr/bin/sendmail',
				'/usr/lib/sendmail',
				'/var/qmail/bin/sendmail',
				'/bin/sendmail',
				'/usr/sbin/sendmail',
				'/sbin/sendmail'
			);
			foreach ($common_locations as $path)
			{
				if (is_executable($path)) return $path;
			}
			//Fallback (swift will still throw an error)
			return "/usr/sbin/sendmail";
		}
		else return $path;
	}
	/**
	 * Set the sendmail command (path + flags)
	 * @param string Command
	 * @throws Swift_Connection_Exception If the command is not correctly structured
	 */
	public function setCommand($command)
	{
		if ($command == self::AUTO_DETECT) $command = $this->findSendmail() . " -bs";
		
		if (!strrpos($command, " -"))
		{
			throw new Swift_Connection_Exception("Cannot set sendmail command with no command line flags. e.g. /usr/sbin/sendmail -t");
		}
		$path = substr($command, 0, strrpos($command, " -"));
		$flags = substr($command, strrpos($command, " -")+2);
		$this->setPath($path);
		$this->setFlags($flags);
	}
	/**
	 * Get the sendmail command (path + flags)
	 * @return string
	 */
	public function getCommand()
	{
		return $this->getPath() . " -" . $this->getFlags();
	}
	/**
	 * Write a command to the open pipe
	 * @param string The command to write
	 * @throws Swift_Connection_Exception If the pipe cannot be written to
	 */
	protected function pipeIn($command, $end="\r\n")
	{
		if (!$this->isAlive()) throw new Swift_Connection_Exception("The sendmail process is not alive and cannot be written to.");
		if (!@fwrite($this->pipes[0], $command . $end)) throw new Swift_Connection_Exception("The sendmail process did not allow the command '" . $command . "' to be sent.");
		fflush($this->pipes[0]);
	}
	/**
	 * Read data from the open pipe
	 * @return string
	 * @throws Swift_Connection_Exception If the pipe is not operating as expected
	 */
	protected function pipeOut()
	{
		if (strpos($this->getFlags(), "t") !== false) return;
		if (!$this->isAlive()) throw new Swift_Connection_Exception("The sendmail process is not alive and cannot be read from.");
		$ret = "";
		$line = 0;
		while (true)
		{
			$line++;
			stream_set_timeout($this->pipes[1], $this->timeout);
			$tmp = @fgets($this->pipes[1]);
			if ($tmp === false)
			{
				throw new Swift_Connection_Exception("There was a problem reading line " . $line . " of a sendmail SMTP response. The response so far was:<br />" . $ret);
			}
			$ret .= trim($tmp) . "\r\n";
			if ($tmp{3} == " ") break;
		}
		fflush($this->pipes[1]);
		return $ret = substr($ret, 0, -2);
	}
	/**
	 * Read a full response from the buffer (this is spoofed if running in -t mode)
	 * @return string
	 * @throws Swift_Connection_Exception Upon failure to read
	 */
	public function read()
	{
		if (strpos($this->getFlags(), "t") !== false)
		{
			switch (strtolower($this->request))
			{
				case null:
					return "220 Greetings";
				case "helo": case "ehlo":
					return "250 hello";
				case "mail": case "rcpt": case "rset":
					return "250 ok";
				case "data":
					$this->send = true;
					return "354 go ahead";
				case "quit":
					return "221 bye";
				default:
					return "250 ok";
			}
		}
		else return $this->pipeOut();
	}
	/**
	 * Write a command to the process (leave off trailing CRLF)
	 * @param string The command to send
	 * @throws Swift_Connection_Exception Upon failure to write
	 */
	public function write($command, $end="\r\n")
	{
		if ($this->getFlags() == "t")
		{
			if (!$this->send && strpos($command, " ")) $command = substr($command, strpos($command, " ")+1);
			elseif ($this->send)
			{
				$this->pipeIn($command);
			}
			$this->request = $command;
			$this->send = (strtolower($command) == "data");
		}
		else $this->pipeIn($command, $end);
	}
	/**
	 * Try to start the connection
	 * @throws Swift_Connection_Exception Upon failure to start
	 */
	public function start()
	{
		if (!$this->getPath() || !$this->getFlags())
		{
			throw new Swift_Connection_Exception("Sendmail cannot be started without a path to the binary including flags.");
		}
		
//		if (!is_executable($this->getPath()))
//		{
//			throw new Swift_Connection_Exception("Sendmail cannot be started.  The path to the executable given [" . $this->getPath() . "] does not appear to be valid.");
//		}
			
		$pipes_spec = array(
			array("pipe", "r"),
			array("pipe", "w"),
			array("pipe", "w")
		);
		
		$this->proc = proc_open($this->getCommand(), $pipes_spec, $this->pipes);
		echo "The command I'm going to run is: " . $this->getCommand() . "\n";
		echo "The result of proc_open() is: "; var_dump($this->proc);
		echo "The pipes are: "; var_dump($this->pipes);
		echo "std_err says: "; var_dump(fgets($this->pipes[2])); //This causes a hang if the process did start
		exit();
		if (!$this->isAlive())
		{
			throw new Swift_Connection_Exception("The sendmail process failed to start.  Please verify that the path exists and PHP has permission to execute it.");
		}
	}
	/**
	 * Try to close the connection
	 * @throws Swift_Connection_Exception Upon failure to close
	 */
	public function stop()
	{
		foreach ($this->pipes as $pipe)
		{
			if (!@fclose($pipe)) throw new Swift_Connection_Exception("The open sendmail process is failing to close.");
		}
		
		if ($this->proc)
		{
			proc_close($this->proc);
			$this->pipes = null;
			$this->proc = null;
		}
	}
	/**
	 * Check if the process is still alive
	 * @return boolean
	 */
	public function isAlive()
	{ //I have been hacking at this heaps in frustration
		return ($this->proc !== false
			&& is_resource($this->proc)
			&& is_resource($this->pipes[0])
			&& is_resource($this->pipes[1])
			&& $this->proc !== null);
	}
}
Last edited by Chris Corbyn on Tue Mar 06, 2007 1:56 pm, edited 2 times in total.
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

Jenk wrote:Nothing short of a convoluted exec/shell_exec/`` springs to mind unfortunately.
I've tried this but it caused a hang :( I can look into it further though.
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

This seems to work, but it feels so... wrong.

Code: Select all

$test = popen($this->getCommand() . " 2>&1", "r");
		var_dump($test);
		$read = fread($test, 2096);
		if (empty($read))
		{
			echo "probably running in non-interactive mode - I'll run for you then!";
		}
		elseif (substr($read, 0, 4) != "220 ")
		{
			echo "Running in interactive mode but speaking gibberish so cannot use this (could be stderr)";
		}
		else
		{
			echo "Ok, this will do";
		}
		fclose($test);
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Re: proc_open(), Again. Always returns TRUE???

Post by Chris Corbyn »

/me digs up old thread

So I finally figured this out in v4! :)

Reading from STDERR ($pipes[2]) DOES work, provided you set it to non-blocking mode before attempting to read.

Code: Select all

 
    $this->_proc = proc_open($command, $descriptorSpec, $pipes);
    stream_set_blocking($pipes[2], 0);
    if ($err = stream_get_contents($pipes[2]))
    {
      throw new Swift_Transport_TransportException(
        'Process could not be started [' . $err . ']'
        );
    }
The process is always opened (as a bash or sh process) even if the command you're trying to run doesn't exist, but stderr will immediately have data in it if the command failed to be started.

I can't believe it took so long for me to find a solution to this :( At least I can get rid of that annoying lstat() which was causing issues now.

/leaves dead thread to rest
Post Reply