1212namespace Swoole \Thread ;
1313
1414use PhpParser \Error ;
15+ use PhpParser \Node \Stmt ;
1516use PhpParser \ParserFactory ;
1617use Swoole \Thread ;
1718
@@ -95,54 +96,41 @@ public function start(): void
9596 }
9697 }
9798
98- if ($ this ->autoloader ) {
99- $ proxyDirectory = dirname ($ this ->autoloader );
100- } else {
101- $ proxyDirectory = dirname ($ this ->classDefinitionFile );
99+ // Thread requests have their own working directory; resolve paths before launching them.
100+ if ($ this ->autoloader !== '' ) {
101+ $ this ->autoloader = realpath ($ this ->autoloader ) ?: $ this ->autoloader ;
102102 }
103-
104- $ script = '<?php ' . PHP_EOL ;
105- $ script .= '$arguments = Swoole\Thread::getArguments(); ' . PHP_EOL ;
106- $ script .= '$autoloader = $arguments[0]; ' . PHP_EOL ;
107- $ script .= '$runnableClass = $arguments[1]; ' . PHP_EOL ;
108- $ script .= '$queue = $arguments[2]; ' . PHP_EOL ;
109- $ script .= '$index = $arguments[5]; ' . PHP_EOL ;
110- // PHP exit() and fatal errors skip finally, but still run request shutdown functions.
111- $ script .= 'register_shutdown_function(static function () use ($queue, $index): void { ' . PHP_EOL ;
112- $ script .= ' $queue->push($index, Swoole\Thread\Queue::NOTIFY_ONE); ' . PHP_EOL ;
113- $ script .= '}); ' . PHP_EOL ;
114- $ script .= '$classDefinitionFile = $arguments[3]; ' . PHP_EOL ;
115- $ script .= '$running = $arguments[4]; ' . PHP_EOL ;
116- $ script .= '$threadArguments = array_slice($arguments, 6); ' . PHP_EOL ;
117- $ script .= 'if ($autoloader) require_once $autoloader; ' . PHP_EOL ;
118- $ script .= 'if ($classDefinitionFile) require_once $classDefinitionFile; ' . PHP_EOL ;
119- $ script .= '$runnable = new $runnableClass($running, $index); ' . PHP_EOL ;
120- $ script .= '$runnable->run($threadArguments); ' . PHP_EOL ;
121-
122- // Existing installations may still have a runner using the old finally-based notification.
123- $ this ->proxyFile = $ proxyDirectory . '/thread_runner_ ' . sha1 ($ script ) . '.php ' ;
124- if (!is_file ($ this ->proxyFile )) {
125- file_put_contents ($ this ->proxyFile , $ script );
103+ if ($ this ->classDefinitionFile !== '' ) {
104+ $ this ->classDefinitionFile = realpath ($ this ->classDefinitionFile ) ?: $ this ->classDefinitionFile ;
126105 }
127106
128- $ this ->queue = new Queue ();
129- $ this ->running = new Atomic (1 );
130-
131- for ($ index = 0 ; $ index < $ this ->threadNum ; $ index ++) {
132- $ this ->createThread ($ index );
133- }
107+ $ this ->queue = new Queue ();
108+ $ this ->running = new Atomic (1 );
109+ $ this ->proxyFile = $ this ->createRunner ();
134110
135- while ($ this ->running ->get ()) {
136- $ index = $ this ->queue ->pop (-1 );
137- $ thread = $ this ->threads [$ index ];
138- $ thread ->join ();
139- unset($ this ->threads [$ index ]);
111+ try {
112+ for ($ index = 0 ; $ index < $ this ->threadNum ; $ index ++) {
113+ $ this ->createThread ($ index );
114+ }
140115
141- $ this ->createThread ($ index );
142- }
116+ while ($ this ->running ->get ()) {
117+ $ index = $ this ->queue ->pop (-1 );
118+ $ thread = $ this ->threads [$ index ];
119+ $ thread ->join ();
120+ unset($ this ->threads [$ index ]);
143121
144- foreach ($ this ->threads as $ thread ) {
145- $ thread ->join ();
122+ $ this ->createThread ($ index );
123+ }
124+ } finally {
125+ $ this ->running ->set (0 );
126+ try {
127+ foreach ($ this ->threads as $ thread ) {
128+ $ thread ->join ();
129+ }
130+ } finally {
131+ $ this ->threads = [];
132+ unlink ($ this ->proxyFile );
133+ }
146134 }
147135 }
148136
@@ -161,42 +149,22 @@ protected function isValidPhpFile(string $filePath): bool
161149 throw new \Exception ('The file of the class to run cannot be checked without the package nikic/php-parser. Install it, or set the file with withClassDefinitionFile(). ' );
162150 }
163151
164- $ allowedNodeTypes = [
165- \PhpParser \Node \Stmt \Class_::class,
166- \PhpParser \Node \Stmt \Const_::class,
167- \PhpParser \Node \Stmt \Use_::class,
168- \PhpParser \Node \Stmt \Namespace_::class,
169- \PhpParser \Node \Stmt \Declare_::class,
170- ];
171-
172152 $ parser = (new ParserFactory ())->createForNewestSupportedVersion ();
173153 try {
174- $ code = file_get_contents ($ filePath );
175- $ stmts = $ parser ->parse ($ code );
176- $ skipLine = -1 ;
177- foreach ($ stmts as $ stmt ) {
178- $ isAllowed = false ;
179- foreach ($ allowedNodeTypes as $ allowedNodeType ) {
180- if ($ stmt instanceof $ allowedNodeType ) {
181- $ isAllowed = true ;
182- break ;
183- }
184- }
185- if (!$ isAllowed ) {
186- if ($ stmt ->getLine () == $ skipLine ) {
187- continue ;
188- }
189- return false ;
190- }
191- }
154+ $ code = file_get_contents ($ filePath );
155+ $ stmts = $ parser ->parse ($ code );
156+ return $ this ->containsOnlyDeclarations ($ stmts ?? []);
192157 } catch (Error ) {
193158 return false ;
194159 }
195- return true ;
196160 }
197161
198162 protected function createThread (int $ index ): void
199163 {
164+ if (!$ this ->running ->get ()) {
165+ return ;
166+ }
167+
200168 $ this ->threads [$ index ] = new Thread ($ this ->proxyFile ,
201169 $ this ->autoloader ,
202170 $ this ->runnableClass ,
@@ -207,4 +175,70 @@ protected function createThread(int $index): void
207175 ...$ this ->arguments
208176 );
209177 }
178+
179+ private function createRunner (): string
180+ {
181+ $ script = <<<'PHP'
182+ <?php
183+ $arguments = Swoole\Thread::getArguments();
184+ $autoloader = $arguments[0];
185+ $runnableClass = $arguments[1];
186+ $queue = $arguments[2];
187+ $index = $arguments[5];
188+ // PHP exit() and fatal errors skip finally, but still run request shutdown functions.
189+ register_shutdown_function(static function () use ($queue, $index): void {
190+ $queue->push($index, Swoole\Thread\Queue::NOTIFY_ONE);
191+ });
192+ $classDefinitionFile = $arguments[3];
193+ $running = $arguments[4];
194+ $threadArguments = array_slice($arguments, 6);
195+ // Preserve the working directory used when runners lived next to the autoloader or class file.
196+ chdir(dirname($autoloader ?: $classDefinitionFile));
197+ if ($autoloader) require_once $autoloader;
198+ if ($classDefinitionFile) require_once $classDefinitionFile;
199+ $runnable = new $runnableClass($running, $index);
200+ $runnable->run($threadArguments);
201+ PHP;
202+
203+ $ file = @tempnam (sys_get_temp_dir (), 'swoole_thread_runner_ ' );
204+ if ($ file === false ) {
205+ throw new \RuntimeException ('Failed to create thread runner in the temporary directory. ' );
206+ }
207+ try {
208+ if (@file_put_contents ($ file , $ script ) !== strlen ($ script )) {
209+ throw new \RuntimeException ("Failed to write thread runner ' {$ file }'. " );
210+ }
211+ return $ file ;
212+ } catch (\Throwable $ exception ) {
213+ unlink ($ file );
214+ throw $ exception ;
215+ }
216+ }
217+
218+ /**
219+ * @param Stmt[] $statements
220+ */
221+ private function containsOnlyDeclarations (array $ statements ): bool
222+ {
223+ foreach ($ statements as $ statement ) {
224+ if ($ statement instanceof Stmt \Namespace_ || $ statement instanceof Stmt \Declare_) {
225+ if (!$ this ->containsOnlyDeclarations ($ statement ->stmts ?? [])) {
226+ return false ;
227+ }
228+ continue ;
229+ }
230+
231+ // Class and function bodies are deferred; only inspect containers executed when loading the file.
232+ if ($ statement instanceof Stmt \ClassLike
233+ || $ statement instanceof Stmt \Function_
234+ || $ statement instanceof Stmt \Const_
235+ || $ statement instanceof Stmt \Use_
236+ || $ statement instanceof Stmt \GroupUse
237+ || $ statement instanceof Stmt \Nop) {
238+ continue ;
239+ }
240+ return false ;
241+ }
242+ return true ;
243+ }
210244}
0 commit comments