[SOLVED] proc_open() is hanging??

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

[SOLVED] proc_open() is hanging??

Post by Chris Corbyn »

My PHP4 install seems to be hanging when I use proc_open(). It gets right up to the last command I need to run all successfully with data read to/from the buffer then suddenly it hangs when I want to issue the QUIT command and before I even get the chance. I tried running the same set of commands under the userid of the www-data from the command line and it all works fine. I suspect that the process is dying in the background but I can't for the life of me see a way to detect that in PHP4. In PHP5 there's a proc_get_status() command which can tell if the process terminated - not so in PHP4.

NOTE, the same code runs in my PHP5 setup just fine.

The methods you're looking to in this code are pipeIn() and pipeOut(). All the stream_set_blocking()/fflush() confusion is my bungled attempt at identifying the source of the issue :(

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__) . "/../ConnectionBase.php";

if (!defined("SWIFT_SENDMAIL_AUTO_DETECT")) define("SWIFT_SENDMAIL_AUTO_DETECT", -2);

/**
 * 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
	 */
	var $AUTO_DETECT = SWIFT_SENDMAIL_AUTO_DETECT;
	/**
	 * Flags for the MTA (options such as bs or t)
	 * @var string
	 */
	var $flags = null;
	/**
	 * The full path to the MTA
	 * @var string
	 */
	var $path = null;
	/**
	 * The type of last request sent
	 * For example MAIL, RCPT, DATA
	 * @var string
	 */
	var $request = null;
	/**
	 * The process handle
	 * @var resource
	 */
	var $proc;
	/**
	 * I/O pipes for the process
	 * @var array
	 */
	var $pipes;
	/**
	 * A reference to STDIN on the pipe
	 * @var resource
	 */
	var $in;
	/**
	 * A reference to STDOUT on the pipe
	 * @var resource
	 */
	var $out;
	/**
	 * Switches to true for just one command when DATA has been issued
	 * @var boolean
	 */
	var $send = false;
	/**
	 * The timeout in seconds before giving up
	 * @var int Seconds
	 */
	var $timeout = 10;
	
	/**
	 * Constructor
	 * @param string The command to execute
	 * @param int The timeout in seconds before giving up
	 */
	function Swift_Connection_Sendmail($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
	 */
	function setTimeout($secs)
	{
		$this->timeout = (int)$secs;
	}
	/**
	 * Get the timeout on the process
	 * @return int
	 */
	function getTimeout()
	{
		return $this->timeout;
	}
	/**
	 * Set the operating flags for the MTA
	 * @param string
	 */
	function setFlags($flags)
	{
		$this->flags = $flags;
	}
	/**
	 * Get the operating flags for the MTA
	 * @return string
	 */
	function getFlags()
	{
		return $this->flags;
	}
	/**
	 * Set the path to the binary
	 * @param string The path (must be absolute!)
	 */
	function setPath($path)
	{
		if ($path == $this->AUTO_DETECT) $path = $this->findSendmail();
		$this->path = $path;
	}
	/**
	 * Get the path to the binary
	 * @return string
	 */
	function getPath()
	{
		return $this->path;
	}
	/**
	 * For auto-detection of sendmail path
	 * Thanks to "Joe Cotroneo" for providing the enhancement
	 * @return string
	 */
	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
	 */
	function setCommand($command)
	{
		if ($command == $this->AUTO_DETECT) $command = $this->findSendmail() . " -bs";
        
		if (!strrpos($command, " -"))
		{
			Swift_Errors::throw(new Swift_Connection_Exception(
				"Cannot set sendmail command with no command line flags. e.g. /usr/sbin/sendmail -t"));
			return;
		}
		$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
	 */
	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
	 */
	function pipeIn($command)
	{
		if (!$this->pipes[0])
		{
			Swift_Errors::throw(new Swift_Connection_Exception(
				"The sendmail process is not alive and cannot be written to."));
			return;
		}
		stream_set_blocking($this->pipes[1], false);
		stream_set_blocking($this->pipes[0], true);
		$written = @fwrite($this->pipes[0], $command . "\r\n");
		stream_set_blocking($this->pipes[0], false);
		fflush($this->pipes[0]);
		if (!$written)
		{
			Swift_Errors::throw(new Swift_Connection_Exception(
				"The sendmail process did not allow the command '" . $command . "' to be sent."));
			return;
		}
	}
	/**
	 * Read data from the open pipe
	 * @return string
	 * @throws Swift_Connection_Exception If the pipe is not operating as expected
	 */
	function pipeOut()
	{
		if ($this->getFlags() == "t") return;
		if (!$this->pipes[1])
		{
			Swift_Errors::throw(new Swift_Connection_Exception(
				"The sendmail process is not alive and cannot be read from."));
			return;
		}
		$ret = "";
		$line = 0;
		stream_set_blocking($this->pipes[1], true);
		stream_set_blocking($this->pipes[0], false);
		while (!feof($this->pipes[1]))
		{
			$line++;
			stream_set_timeout($this->pipes[1], $this->timeout);
			$tmp = @fgets($this->pipes[1]);
			if ($tmp === false)
			{
				Swift_Errors::throw(new Swift_Connection_Exception(
					"There was a problem reading line " . $line . " of a sendmail SMTP response. The response so far was:<br />" . $ret));
				return;
			}
			$ret .= trim($tmp) . "\r\n";
			if ($tmp{3} == " ") break;
		}
		stream_set_blocking($this->pipes[1], false);
		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
	 */
	function read()
	{
		if ($this->getFlags() == "t")
		{
			switch (strtolower($this->request))
			{
				case null:
					return "220 Greetings";
				case "helo": case "ehlo":
					return "250 hello";
				case "mail": case "rcpt": case "rset": case "quit":
					return "250 ok";
				case "data":
					$this->send = true;
					return "354 go ahead";
				default:
					return "250 ok";
			}
		}
		else
		{
			$read = $this->pipeOut();
			return $read;
		}
	}
	/**
	 * Write a command to the process (leave off trailing CRLF)
	 * @param string The command to send
	 * @throws Swift_Connection_Exception Upon failure to write
	 */
	function write($command)
	{
		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);
	}
	/**
	 * Try to start the connection
	 * @throws Swift_Connection_Exception Upon failure to start
	 */
	function start()
	{
		if (!$this->getPath() || !$this->getFlags())
		{
			trigger_error("Sendmail cannot be started without a path to the binary including flags.");
			return;
		}
		
		$pipes_spec = array(
			array("pipe", "r"),
			array("pipe", "w"),
			array("pipe", "w")
		);
		
		$this->proc = proc_open($this->getCommand(), $pipes_spec, $this->pipes);
		$this->in =& $this->pipes[0];
		$this->out =& $this->pipes[1];
		stream_set_blocking($this->pipes[0], false);
		stream_set_blocking($this->pipes[1], false);
		
		if (!$this->proc)
		{
			Swift_Errors::throw(new Swift_Connection_Exception(
				"The sendmail process failed to start.  Please verify that the path exists and PHP has permission to execute it."));
			return;
		}
		
		return true;
	}
	/**
	 * Try to close the connection
	 * @throws Swift_Connection_Exception Upon failure to close
	 */
	function stop()
	{
		foreach ($this->pipes as $i => $pipe)
		{
			fclose($this->pipes[$i]);
		}
		
		if ($this->proc)
		{
			proc_terminate($this->proc);
			$this->in = null;
			$this->out = null;
			$this->pipes = null;
			$this->proc = null;
		}
	}
	/**
	 * Check if the process is still alive
	 * @return boolean
	 */
	function isAlive()
	{
		return ($this->proc !== null);
	}
}
Last edited by Chris Corbyn on Tue Feb 13, 2007 11:04 am, edited 1 time 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 »

Hmm... never mind. I haven't fixed it but I do know it's not PHP's fault, unless it doesn't like the resources as class properties, because this works and it's technically running the same commands:

Will have to have a closer look when I get home. I'm letting myself out of work early today :D

Code: Select all

<?php

$pipes_spec = array(
			array("pipe", "r"),
			array("pipe", "w"),
			array("pipe", "w")
		);
		
		$proc = proc_open("/usr/sbin/sendmail -bs", $pipes_spec, $pipes);

function read_all(&$pipe)
{
	while (!feof($pipe))
		{
			stream_set_timeout($pipe, 5);
			$tmp = @fgets($pipe);
			if ($tmp === false)
			{
				die('screwed');
			}
			$ret .= trim($tmp) . "\r\n";
			if ($tmp{3} == " ") break;
		}
	echo $ret;
}

read_all($pipes[1]);
fwrite($pipes[0], "EHLO foo\r\n");
read_all($pipes[1]);
fwrite($pipes[0], "MAIL FROM: <chris@w3style.co.uk>\r\n");
read_all($pipes[1]);
fwrite($pipes[0], "RCPT TO: <chris@w3style.co.uk>\r\n");
read_all($pipes[1]);
fwrite($pipes[0], "DATA\r\n");
read_all($pipes[1]);
fwrite($pipes[0], "Subject: test\r\n\r\ntest\r\n.\r\n");
read_all($pipes[1]);
fwrite($pipes[0], "QUIT\r\n");
read_all($pipes[1]);

fclose($pipes[0]);
fclose($pipes[1]);
proc_close($proc);

echo 'DONE!!';
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

The problem was odd. I'm still not sure why the class behaves like this, but the snippet I made second works without it. I needed to explicitly close the process before ending the script. Even the 30 second timeout gets ignored if the process is still open. PHP5 doesn't seem to do that. In the absense of a destructor in PHP4 it looks like I may have to resort to using a registered shutdown function - although I'm not sure the script will run it till the process gets closed :?

EDIT | No, register_shutdown_function() doesn't help since the script isn't ready to shut down. Hmmf. Looks like it'll just have to be documented that you need to disconnect once done.
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

OMFG (I apologise for the AOLSpeak! but I couldn't say it literally)... guess what the solution is?

I'll leave ya to guess for 5 mins. I'll remind you that it worked in PHP5, but not in PHP4 and that I've wrapped the functionality in a class (big hint!).
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

Any takers? No? Ok then...

Code: Select all

//Sorry guys, it has to be done and it's not my lazy coding, blame PHP4/proc_open()
$GLOBALS["_SWIFT_PROC"] = null;

/**
 * Swift Sendmail Connection
 * @package Swift_Connection
 * @author Chris Corbyn <chris@w3style.co.uk>
 */
class Swift_Connection_Sendmail extends Swift_ConnectionBase
{

// ... snip ...

        /**
         * Try to start the connection
         * @throws Swift_Connection_Exception Upon failure to start
         */
        function start()
        {
                if (!$this->getPath() || !$this->getFlags())
                {
                        Swift_Errors::throw(new Swift_Connection_Exception(
                                "Sendmail cannot be started without a path to the binary including flags."));
                        return;
                }

                $pipes_spec = array(
                        array("pipe", "r"),
                        array("pipe", "w"),
                        array("pipe", "w")
                );

                $GLOBALS["_SWIFT_PROC"] = proc_open($this->getCommand(), $pipes_spec, $this->pipes);
                $this->proc =& $GLOBALS["_SWIFT_PROC"];

// ... snip ...
PHP4 doesn't self-close resourcs/processes which were not opened in global scope. I was pained to have to add that global to my 100% OO code.

EDIT | Testicles, even that won't work if two instances are created... grr...

EDIT2|

Code: Select all

$i = count($GLOBALS["_SWIFT_PROC"]);
		$GLOBALS["_SWIFT_PROC"][$i] = proc_open($this->getCommand(), $pipes_spec, $this->pipes);
		$this->proc =& $GLOBALS["_SWIFT_PROC"][$i];
User avatar
feyd
Neighborhood Spidermoddy
Posts: 31559
Joined: Mon Mar 29, 2004 3:24 pm
Location: Bothell, Washington, USA

Post by feyd »

d's gone senile! He's talking to himself!
User avatar
Oren
DevNet Resident
Posts: 1640
Joined: Fri Apr 07, 2006 5:13 am
Location: Israel

Post by Oren »

feyd wrote:d's gone senile! He's talking to himself!
Damn, that's exactly what I've told myself. By the way... it's not the first time he is doing this :lol:
Post Reply