summaryrefslogtreecommitdiff
path: root/includes/PHPAsync.php
blob: 92c1e070ba5429dec5baddfa56b18086161ae49a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
<?php
/**

 Usage
 
 $yourObject = new YourClass();
 $yourObject->asyncProcess = new PHPAsync();
 $async->runProcess(
     $yourObject,
     'doSomeWork',
     'getBufferContent',
     'getLogHtml',
 );

 in YourClass::doSomeWork send notifications to the async log like so
 
 $this->asyncProcess->updateStatus( <percentage_complete> );  a numeric value, do not include "%"
 *
 * @package kernel
 */

/**
 * Setup
 */
require_once( UTIL_PKG_INCLUDE_PATH.'phpcontrib_lib.php' );

// written for bitweaver environment, but you can override 
if( !defined( 'PHPASYNC_TEMP_DIR' ) ){
	define( 'PHPASYNC_TEMP_DIR', TEMP_PKG_PATH.'phpasync' );
}

/**
 * @package kernel
 */
class PHPAsync extends BitBase{
	private $mPid;

	private $mLogFile;

	private $mUpdateLogHandler;

	private $mFileHandle;

	private $mStatus; // pct complete

	// default config
	public $mConfig = array();

	// constructor
	public function __construct( $pPidId = NULL, $pConfig = array() ){
		if( !empty( $pPidId ) ){
			// set id
			$this->mPid = $pPidId;

			// log file path
			$this->mLogFile = PHPASYNC_TEMP_DIR.'/'.$this->mPid; 
		}

		// set default config values
		$this->mConfig = array(
			'append_log' => FALSE,
			'max_execution_time' => "1200",
			'memory_limit' => '128M', 
			'no-gzip' => 1,
			'zlib.output_compression' => 0,
			'ignore_user_abort' => FALSE,
			);

		// override default config
		if( !empty( $pConfig ) ){
			extract_to( $pConfig, $this->mConfig, EXTR_IF_EXISTS );
		}

		parent::__construct();
	}

	// runProcess
	/**
	 * wraps a class instance method as a async process
	 * @param $pObject - The object instance whose process is being wrapped
	 * @param $pProcessHandler - The process being wrapped to run in the background
	 * @param $pOutputHandler - The output handler is invoked to get whatever page content should be returned to the user before the background process begins
	 * @param $pUpdateLogHandler - The update log handler is invoked when updateStatus is called by the wrapped process. the object can push customize content into the log by defining this callback
	 */
	public function runProcess( $pObject, $pProcessHandler, $pProcessHash = NULL, $pOutputHandler, $pUpdateLogHandler = NULL ){
		global $gBitSystem, $gBitSmarty;

		// create file tracking id
		$this->mLogFile = $this->genLogFile();

		$this->mPid = substr( $this->mLogFile, strlen(PHPASYNC_TEMP_DIR.'/') );

		$this->mProcessObject = $pObject;

		// register log callback
		if( !empty( $pUpdateLogHandler ) ){
			$this->mUpdateLogHandler = $pUpdateLogHandler;
		}

		// override some apache settings that might screw up flushing the buffer
		@apache_setenv('no-gzip', $this->getConfig('no-gzip') );
		@ini_set('zlib.output_compression', $this->getConfig('zlib.output_compression') );

		// set time and memory
		ini_set('max_execution_time', $this->getConfig('max_execution_time') ); 
		ini_set('memory_limit', $this->getConfig('memory_limit') );

		// allow disallow process abort
		ignore_user_abort( $this->getConfig('ignore_user_abort') );

		// create the file for writing
		$this->mFileHandle = fopen($this->mLogFile, 'wrx+');
		if (! $this->mFileHandle){
			$gBitSystem->fatalError( "Error in PHPAsync: could not create log file - process aborted. Please notify your website developer." );
		}

		// convenience make the pid available to smarty as most output handlers will want access to it
		$gBitSmarty->assign( 'pid', $this->mPid );

		// output Buffered content
		$this->outputBuffer( $pObject->$pOutputHandler() );

		// run the background process
		$pObject->$pProcessHandler( $pProcessHash );

		// clean up
		sleep(20);	// number of seconds the temp file should remain available for status check before it is deleted

		fclose($this->mFileHandle);

		// delete the progress file
		unlink($this->mLogFile);
		
		//return true if completed
		return true;
	}

	/**
	 * this is our output buffer
	 * send it anything and it will 
	 * send it to the browser
	 */
	public function outputBuffer( $pContent ){
		ob_start(); 

		echo( $pContent );
		
		$size = ob_get_length();
		header("Content-Length: $size");
		header('Connection: close');

		ob_end_flush();
		// ob_flush(); -- worked fine in preliminary test and was suggested by web sample, but causes warning here
		flush();
		session_write_close();
	}

	/**
	 * notifies our running process 
	 * of the percentage of a task completed
	 */
	public function updateStatus( $pPct, $pLogText = NULL ){
		global $gBitSystem;

		// crap - hack to store pct and msg - no other way to know without storing this info in table or session or something
		$logText = !empty( $pLogText )?$pPct.":".$pLogText:$pPct;

		if( (int)$pPct >= 100 ){
			$this->mStatus = 100;
		}else{
			$this->mStatus = $pPct;
		}

		// if a custom log message handler is registered get the log message from it 
		if( $func = $this->mUpdateLogHandler ){
			$this->mProcessObject->$func( $pPct );
		}
		// rewind to the beginning of the log file if append is false
		if( $this->getConfig( 'append_log' ) == FALSE ){
			rewind( $this->mFileHandle ); 
			ftruncate( $this->mFileHandle, 0 ); //filesize($this->mLogFile));
		}
		// update the log file
		if( fwrite( $this->mFileHandle, $logText ) == false ){ 
            $gBitSystem->fatalError( "Error in PHPAsync: Write to log file failed - process aborted. " . error_get_last() );
            exit;
        }
	}

	/**
	 * Retrieves the content of the log file. To be used by request checking on the status of the process
	 */
	public function getStatus(){ 
		if ( file_exists( $this->mLogFile ) ){
			if(! $progress = file_get_contents($this->mLogFile)){
				header('HTTP/1.1 500 Internal Server Error');
				exit;
			}
			// crap = hack to get pct and msg - see related hack in updateStatus
			$delimpos = strpos( $progress, ':' );
			$pct = substr( $progress, 0, $delimpos ); 
			$log = substr( $progress, $delimpos+1 ); 
			return array( 'pct_complete' => $pct, 'log' => $log );
		}else{
			$this->setError( 'get_status', 'log file not found' );
			return FALSE;
		}
	}

	public function getPidId(){
		if( !empty( $this->mPid ) ){
			return $this->mPid;
		}
		return NULL;
	}

	private function genLogFile(){
        if( !is_dir( PHPASYNC_TEMP_DIR )) {
            mkdir_p( PHPASYNC_TEMP_DIR );
        }
		return tempnam( PHPASYNC_TEMP_DIR, '');
	}

	private function setLogFile(){
		if( !empty( $this->mPid ) ){
			$this->mLogFile = PHPASYNC_TEMP_DIR.'/'.$this->mPid; 
		}
	}

	private function getConfig( $pKey ){
		if( isset( $this->mConfig[$pKey] ) ){
			return $this->mConfig[$pKey]; 
		}
		return NULL;
	}
}