PageRenderTime 43ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/code/classes/pinetd/Core.class.php

https://github.com/blekkzor/pinetd2
PHP | 611 lines | 495 code | 49 blank | 67 comment | 97 complexity | 2a416d7ad088a76f20482453821fd5b8 MD5 | raw file
Possible License(s): GPL-2.0
  1. <?php
  2. /* Portable INET daemon v2 in PHP
  3. * Copyright (C) 2007 Mark Karpeles <mark@kinoko.fr>
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 2 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License
  16. * along with this program; if not, write to the Free Software
  17. * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
  18. */
  19. /**
  20. * \file Core.class.php
  21. * \brief Contains core code for pinetd
  22. */
  23. namespace pinetd;
  24. /**
  25. * \class Core
  26. * \brief Core code for pinetd
  27. */
  28. class Core {
  29. /**
  30. * \brief Contains SimpleXML object for current config
  31. *
  32. * This is the configuration file, as a SimpleXML object.
  33. */
  34. private $config;
  35. /**
  36. * \brief List of currently running daemons
  37. *
  38. * Currently running daemons, in a pid-indexed array
  39. */
  40. private $daemons;
  41. /**
  42. * \brief List of active sockets
  43. *
  44. * This list contains all active sockets and function to be called if something
  45. * happens on this socket.
  46. */
  47. private $fdlist = array();
  48. /**
  49. * \brief List of currently enabled ports
  50. */
  51. private $ports = array();
  52. private $transport_engine;
  53. public function __construct() {
  54. $this->config = ConfigManager::invoke();
  55. $this->transport_engine = new TransportEngine($this);
  56. $this->daemons = array();
  57. pcntl_signal(SIGTERM, array(&$this, 'sighandler'), false);
  58. pcntl_signal(SIGINT, array(&$this, 'sighandler'), false);
  59. pcntl_signal(SIGCHLD, SIG_DFL, false);
  60. if (file_exists(PINETD_ROOT.'/control.sock')) unlink(PINETD_ROOT.'/control.sock');
  61. $master = stream_socket_server('unix://'.PINETD_ROOT.'/control.sock', $errno, $errstr);
  62. if ($master) {
  63. $this->registerSocketWait($master, array($this, 'newMasterConnection'), $d = array($master));
  64. } else {
  65. Logger::log(Logger::LOG_WARN, 'Could not create master socket: ['.$errno.'] '.$errstr);
  66. }
  67. }
  68. public function newMasterConnection($sock) {
  69. $new = stream_socket_accept($sock, 0, $peer);
  70. $no = '';
  71. $this->registerSocketWait($new, array($this, 'masterData'), $d = array($new));
  72. }
  73. public function masterData($sock) {
  74. if (!isset($this->fdlist[(int)$sock])) return;
  75. $data = fread($sock, 65535);
  76. if (($data === false) || ($data === '')) {
  77. $this->removeSocket($sock);
  78. fclose($sock);
  79. return;
  80. }
  81. $peer = &$this->fdlist[(int)$sock];
  82. if (!isset($peer['buffer'])) $peer['buffer'] = '';
  83. $buffer = &$peer['buffer'];
  84. $buffer .= $data;
  85. while(1) {
  86. if (strlen($buffer) < 4) break;
  87. list(,$len) = unpack('N', $buffer);
  88. if ($len > strlen($buffer)) break;
  89. $packet = substr($buffer, 4, $len-4);
  90. $buffer = substr($buffer, $len);
  91. $packet = unserialize($packet);
  92. if (!is_array($packet)) continue;
  93. $packet['sock'] = $sock;
  94. $this->masterPacket($packet);
  95. }
  96. }
  97. public function masterPacket(array $packet) {
  98. switch(strtolower($packet['cmd'])) {
  99. case 'getpid':
  100. $this->masterReply($packet, array('type' => 'pid', 'pid' => getmypid()));
  101. break;
  102. case 'getversion':
  103. $this->masterReply($packet, array('type' => 'version', 'version' => PINETD_VERSION));
  104. break;
  105. case 'list_daemons':
  106. $list = array();
  107. foreach($this->daemons as $id => $dat) {
  108. unset($dat['IPC']);
  109. unset($dat['daemon']);
  110. $dat['socket'] = (int)$dat['socket'];
  111. $list[$id] = $dat;
  112. }
  113. $this->masterReply($packet, array('type' => 'daemons', 'daemons' => $list));
  114. break;
  115. case 'stop': // STOP DAEMON
  116. Logger::log(Logger::LOG_INFO, 'Stop command received via control socket, stopping...');
  117. $this->masterReply($packet, array('type' => 'ack', 'ack' => 'stop'));
  118. foreach(array_keys($this->daemons) as $port) {
  119. $this->killDaemon($port, 800);
  120. }
  121. $exp = time() + 12;
  122. while(count($this->daemons) > 0) {
  123. $this->checkRunning();
  124. $this->receiveStopped();
  125. $this->childrenStatus();
  126. $this->readIPCs(200000);
  127. foreach($this->daemons as $id=>$dat) {
  128. if ($dat['status'] == 'Z') {
  129. $this->masterReply($packet, array('type' => 'finished', 'finished' => $id));
  130. unset($this->daemons[$id]);
  131. }
  132. }
  133. if ($exp <= time()) {
  134. Logger::log(Logger::LOG_WARN, 'Not all processes finished after 12 seconds');
  135. $this->masterReply($packet, array('type' => 'notify', 'notify' => 'stop_soft_failure'));
  136. break;
  137. }
  138. }
  139. Logger::log(Logger::LOG_INFO, 'Good bye!');
  140. $this->masterReply($packet, array('type' => 'ack', 'ack' => 'stop_finished'));
  141. exit;
  142. default:
  143. $this->masterReply($packet, array('exception' => 'No such command!'));
  144. break;
  145. }
  146. }
  147. public function masterReply($packet, array $data) {
  148. if (isset($packet['seq'])) $data['seq'] = $packet['seq'];
  149. $data = serialize($data);
  150. return fwrite($packet['sock'], pack('N', strlen($data)+4).$data);
  151. }
  152. /**
  153. * \brief load a certificate for a given certificate name
  154. * \param $SSL string Name of the SSL certificate to load
  155. * \return array SSL descriptor
  156. */
  157. public function loadCertificate($SSL) {
  158. foreach($this->config->SSL->Certificate as $node) {
  159. if ($node['name'] != $SSL) continue;
  160. $options = array();
  161. foreach($node->Option as $opt) {
  162. if ($opt['Disabled']) continue;
  163. $val = (string)$opt['Value'];
  164. if ($val === 'true') $val = true;
  165. if ($val === 'false') $val = false;
  166. $var = (string)$opt['name'];
  167. switch($var) {
  168. case 'cafile':
  169. case 'local_cert':
  170. $val = PINETD_ROOT . '/ssl/' . $val;
  171. break;
  172. }
  173. $options[$var] = $val;
  174. }
  175. return $options;
  176. }
  177. return null;
  178. }
  179. public function _ChildIPC_loadCertificate(&$daemon, $SSL) {
  180. return $this->loadCertificate($SSL);
  181. }
  182. public function createPort($port, &$class) {
  183. if (isset($this->ports[$port])) return false;
  184. $this->ports[$port] = array('type' => 'class', 'class' => &$class);
  185. return true;
  186. }
  187. public function routePortReply($reply, $is_exception = false) {
  188. $next = array_pop($reply[1]);
  189. if (!isset($this->fdlist[$next])) return; // ?!
  190. $key = $this->fdlist[$next]['key'];
  191. $daemon = &$this->daemons[$key];
  192. $code = IPC::RES_CALLPORT;
  193. if ($is_exception) $code = IPC::RES_CALLPORT_EXCEPT;
  194. $daemon['IPC']->sendcmd($code, $reply);
  195. }
  196. public function openPort($port) {
  197. if (!isset($this->ports[$port])) return NULL;
  198. return $this->ports[$port]['class'];
  199. }
  200. public function callPort($call) {
  201. // ok, determine where we should put this call
  202. if (!isset($this->ports[$call[0]])) {
  203. // port does not exists => die!
  204. $exception = array(
  205. $call[0],
  206. $call[1],
  207. 'Requested port '.$call[0].' does not exists!',
  208. );
  209. $this->routePortReply($exception, true);
  210. return;
  211. }
  212. $call[1][] = '@parent';
  213. $class = &$this->ports[$call[0]]['class']; // at this point, ports are only class type
  214. if ($class instanceof IPC) {
  215. $class->sendcmd(IPC::CMD_CALLPORT, $call);
  216. } else {
  217. $method = $call[2];
  218. try {
  219. $res = call_user_func_array(array($class, $method), $call[3]);
  220. } catch(\Exception $e) {
  221. $exception = array(
  222. $call[0],
  223. $call[1],
  224. $e->getMessage(),
  225. );
  226. $this->routePortReply($exception, true);
  227. return;
  228. }
  229. $result = array(
  230. $call[0],
  231. $call[1],
  232. $res,
  233. );
  234. $this->routePortReply($result);
  235. }
  236. }
  237. public function broadcast($code, $data = null, $except = 0) {
  238. foreach($this->fdlist as $id => $info) {
  239. if ($id == $except) continue;
  240. if ($info['type'] != 'daemon') continue;
  241. $this->daemons[$info['key']]['IPC']->broadcast($code, $data);
  242. }
  243. }
  244. public function registerSocketWait($socket, $callback, &$data) {
  245. $this->fdlist[(int)$socket] = array('type'=>'callback', 'fd'=>$socket, 'callback'=>$callback, 'data'=>&$data);
  246. }
  247. public function removeSocket($fd) {
  248. unset($this->fdlist[(int)$fd]);
  249. }
  250. public function sighandler($signal) {
  251. switch($signal) {
  252. case SIGTERM:
  253. case SIGINT:
  254. Logger::log(Logger::LOG_INFO, 'Ending signal received, killing children...');
  255. foreach(array_keys($this->daemons) as $port) {
  256. $this->killDaemon($port, 800);
  257. }
  258. $exp = time() + 12;
  259. while(count($this->daemons) > 0) {
  260. $this->checkRunning();
  261. $this->receiveStopped();
  262. $this->childrenStatus();
  263. $this->readIPCs(200000);
  264. foreach($this->daemons as $id=>$dat) {
  265. if ($dat['status'] == 'Z') unset($this->daemons[$id]);
  266. }
  267. if ($exp <= time()) {
  268. Logger::log(Logger::LOG_WARN, 'Not all processes finished after 12 seconds');
  269. break;
  270. }
  271. }
  272. Logger::log(Logger::LOG_INFO, 'Good bye!');
  273. exit;
  274. #
  275. }
  276. }
  277. private function loadProcessDaemon($port, $node) {
  278. return $this->loadDaemon($port, $node, 'Process');
  279. }
  280. private function loadTCPDaemon($port, $node) {
  281. return $this->loadDaemon($port, $node, 'TCP');
  282. }
  283. private function loadUDPDaemon($port, $node) {
  284. return $this->loadDaemon($port, $node, 'UDP');
  285. }
  286. private function loadDaemon($port, $node, $type) {
  287. $key = $this->makeDaemonKey($node, $type);
  288. // determine HERE if we should fork...
  289. $daemon = &$this->daemons[$key];
  290. $good_keys = array('Type' => 1, 'Daemon'=>1, 'SSL' => 1, 'Port' => 1, 'Ip' => 1, 'Service' => 1);
  291. foreach(array_keys($daemon) as $_key) {
  292. if (!isset($good_keys[$_key])) unset($daemon[$_key]);
  293. }
  294. if (!$daemon['Service']) $daemon['Service'] = 'Base';
  295. $class = 'Daemon\\'.$daemon['Daemon'].'\\'.$daemon['Service'];
  296. if ((isset($this->config->Global->Security->Fork)) && PINETD_CAN_FORK) {
  297. // prepare an IPC
  298. $pair = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0);
  299. if (is_array($pair)) {
  300. $pid = pcntl_fork();
  301. if ($pid > 0) {
  302. SQL::parentForked();
  303. // parent, record infos about this child
  304. $this->daemons[$key]['pid'] = $pid;
  305. $this->daemons[$key]['socket'] = $pair[0];
  306. $this->daemons[$key]['IPC'] = new IPC($pair[0], false, $this, $this);
  307. $this->daemons[$key]['status'] = 'R'; // running
  308. $this->fdlist[$pair[0]] = array('type'=>'daemon', 'port'=>$port, 'key' => $key,'fd'=>$pair[0]);
  309. fclose($pair[1]);
  310. return true;
  311. } elseif ($pid == 0) {
  312. SQL::forked();
  313. Timer::reset();
  314. fclose($pair[0]);
  315. pcntl_signal(SIGTERM, SIG_DFL, false);
  316. pcntl_signal(SIGINT, SIG_IGN, false); // fix against Ctrl+C
  317. pcntl_signal(SIGCHLD, SIG_DFL, false);
  318. // cleanup
  319. foreach($this->fdlist as $dat) fclose($dat['fd']);
  320. $this->fdlist = array();
  321. $IPC = new IPC($pair[1], true, $this, $this);
  322. $IPC->ping();
  323. Logger::setIPC($IPC);
  324. try {
  325. $daemon = new $class($port, $this->daemons[$key], $IPC, $node);
  326. $IPC->setParent($daemon);
  327. $daemon->mainLoop();
  328. } catch(\Exception $e) {
  329. $IPC->Exception($e);
  330. exit;
  331. }
  332. $IPC->Error('Unexpected end of program!', 60);
  333. exit;
  334. }
  335. fclose($pair[0]);
  336. fclose($pair[1]);
  337. }
  338. // if an error occured here, we fallback to no-fork method
  339. }
  340. // invoke the process in local scope
  341. try {
  342. $this->daemons[$key]['daemon'] = new $class($port, $this->daemons[$key], $this, $node);
  343. $this->daemons[$key]['status'] = 'I'; // Invoked (nofork)
  344. } catch(\Exception $e) {
  345. $this->daemons[$key]['status'] = 'Z';
  346. $this->daemons[$key]['deadline'] = time() + 60;
  347. Logger::log(Logger::LOG_ERR, 'From daemon on '.$key.': '.$e->getMessage());
  348. }
  349. }
  350. protected function makeDaemonKey($daemon, $type) {
  351. if(isset($daemon['Port'])) {
  352. $ip = (string)$this->config->Global->Network->Bind->Ip;
  353. if (isset($daemon['Ip'])) $ip = $daemon['Ip'];
  354. $port = '['.$ip.']:'.(int)$daemon['Port'];
  355. } else {
  356. $port = $daemon['Daemon'] . '::' . $daemon['Service'];
  357. if (isset($daemon['Ip'])) $port.='::'.$daemon['Ip'];
  358. }
  359. return $port . '/' .strtolower($type);
  360. }
  361. function startMissing() {
  362. foreach($this->config->Processes->children() as $Type => $Entry) {
  363. $data = array();
  364. foreach($Entry->attributes() as $attr => $aval) $data[$attr] = (string)$aval;
  365. $offset = (int)$this->config->Processes['PortOffset'];
  366. $data['Port'] += $offset;
  367. $data['Type'] = $Type;
  368. $key = $this->makeDaemonKey($Entry, $Type);
  369. if (isset($this->daemons[$key]))
  370. continue; // no care
  371. $this->daemons[$key] = $data;
  372. $startfunc = 'load'.$Type.'Daemon';
  373. $this->$startfunc((int)$data['Port'], $Entry);
  374. }
  375. }
  376. function checkRunning() {
  377. foreach($this->daemons as $key => $data) {
  378. // TODO: do something
  379. }
  380. }
  381. public function Error($errstr) {
  382. throw new \Exception($errstr);
  383. }
  384. public function _ChildIPC_killSelf(&$daemon) {
  385. // mark it "to be killed"
  386. Logger::log(Logger::LOG_DEBUG, 'pinetd\\Core\\_ChildIPC_killSelf() called for child on port #'.$daemon['Port']);
  387. if (
  388. ($daemon['status'] != 'R') &&
  389. ($daemon['status'] != 'T')
  390. )
  391. return;
  392. $daemon['IPC']->stop();
  393. if (!isset($daemon['kill']))
  394. $daemon['kill'] = time() + 5;
  395. $daemon['status'] = 'K';
  396. }
  397. private function killDaemon($key, $timeout) {
  398. // kill it!
  399. if (!isset($this->daemons[$key])) throw new \Exception('Unknown daemon '.$key);
  400. if ($this->daemons[$key]['status'] == 'I') {
  401. // not forked
  402. $this->daemons[$key]['daemon']->shutdown();
  403. $this->daemons[$key]['status'] = 'Z';
  404. return;
  405. }
  406. $this->removePorts($this->daemons[$key]['IPC']);
  407. if (
  408. ($this->daemons[$key]['status'] != 'R') &&
  409. ($this->daemons[$key]['status'] != 'K') &&
  410. ($this->daemons[$key]['status'] != 'T')
  411. )
  412. return;
  413. if (posix_kill($this->daemons[$key]['pid'], 0)) {
  414. // still running
  415. $this->daemons[$key]['IPC']->stop();
  416. if (!isset($this->daemons[$key]['kill']))
  417. $this->daemons[$key]['kill'] = time() + 5;
  418. $this->daemons[$key]['status'] = 'K'; // kill
  419. } else {
  420. $this->daemons[$key]['status'] = 'Z'; // zombie
  421. @fclose($this->daemons[$key]['socket']); // make sure this is closed
  422. unset($this->fdlist[$this->daemons[$key]['socket']]);
  423. }
  424. if (!isset($this->daemons[$key]['deadline']))
  425. $this->daemons[$key]['deadline'] = time() + $timeout;
  426. }
  427. function IPCDied($fd) {
  428. if (!isset($this->fdlist[$fd])) return; // can't do anything about this
  429. switch($this->fdlist[$fd]['type']) {
  430. case 'daemon':
  431. $key = $this->fdlist[$fd]['key'];
  432. if ($this->daemons[$key]['status'] == 'R')
  433. Logger::log(Logger::LOG_DEBUG, 'IPC died on '.$key);
  434. fclose($fd);
  435. unset($this->fdlist[$fd]);
  436. $this->killDaemon($key, 10);
  437. break;
  438. }
  439. }
  440. private function removePorts($class) {
  441. if (!is_object($class)) return false;
  442. foreach($this->ports as $port => &$info) {
  443. if ($info['type'] != 'class') continue;
  444. if ($info['class'] === $class)
  445. unset($this->ports[$port]);
  446. }
  447. return true;
  448. }
  449. private function receiveStopped() {
  450. if (!PINETD_CAN_FORK) return;
  451. if (count($this->daemons) == 0) return; // nothing to do
  452. $res = pcntl_wait($status, WNOHANG);
  453. if ($res == -1) return; // something bad happened
  454. if ($res == 0) return; // no process terminated
  455. // search what ended
  456. $ended = null;
  457. foreach($this->daemons as $key => $dat) {
  458. if ($dat['pid'] == $res) {
  459. $ended = $key;
  460. break;
  461. }
  462. }
  463. if (is_null($ended)) return; // we do not know what ended
  464. if (pcntl_wifexited($status)) {
  465. $code = pcntl_wexitstatus($status);
  466. Logger::log(Logger::LOG_INFO, 'Child with pid #'.$res.' on ['.$key.'] exited');
  467. $this->killDaemon($key, 10);
  468. return;
  469. }
  470. if (pcntl_wifstopped($status)) {
  471. Logger::log(Logger::LOG_INFO, 'Waking up stopped child on pid '.$res);
  472. posix_kill($res, SIGCONT);
  473. return;
  474. }
  475. if (pcntl_wifsignaled($status)) {
  476. $signal = pcntl_wtermsig($status);
  477. $const = get_defined_constants(true);
  478. $const = $const['pcntl'];
  479. foreach($const as $var => $val) {
  480. if (substr($var, 0, 3) != 'SIG') continue;
  481. if (substr($var, 0, 4) == 'SIG_') continue;
  482. if ($val != $signal) continue;
  483. $signal = $var;
  484. break;
  485. }
  486. Logger::log(Logger::LOG_INFO, 'Child with pid #'.$res.' on '.$key.' died due to signal '.$signal);
  487. $this->killDaemon($key, 10);
  488. }
  489. }
  490. function readIPCs($timeout) {
  491. // build $r
  492. $r = array();
  493. foreach($this->fdlist as $dat) $r[] = $dat['fd'];
  494. $res = @stream_select($r, $w = null, $e = null, 0, $timeout);
  495. pcntl_signal_dispatch();
  496. if (($res == 0) && (count($r) > 0)) $res = count($r);
  497. if ($res > 0) {
  498. foreach($r as $fd) {
  499. switch($this->fdlist[$fd]['type']) {
  500. case 'daemon':
  501. $this->daemons[$this->fdlist[(int)$fd]['key']]['IPC']->run($this->daemons[$this->fdlist[(int)$fd]['key']], $fd);
  502. break;
  503. case 'callback':
  504. $info = &$this->fdlist[$fd];
  505. call_user_func_array($info['callback'], $info['data']);
  506. break;
  507. }
  508. }
  509. }
  510. }
  511. function childrenStatus() {
  512. $now = time();
  513. foreach($this->daemons as $key => &$data) {
  514. switch($data['status']) {
  515. case 'R': // running (forked)
  516. case 'I': // invoked (not forked)
  517. break;
  518. case 'K': // to kill
  519. if ($data['kill'] <= $now) {
  520. posix_kill($data['pid'], SIGTERM); // die!
  521. $data['status'] = 'T';
  522. $data['kill'] = time()+2;
  523. }
  524. break;
  525. case 'T': // terminating
  526. if ($data['kill'] <= $now) {
  527. posix_kill($data['pid'], SIGKILL); // DIE!!
  528. $data['status'] = 'Z';
  529. break;
  530. }
  531. case 'Z': // zombie
  532. if (!isset($data['deadline'])) {
  533. unset($this->daemons[$key]);
  534. break;
  535. }
  536. if ($data['deadline'] > $now) break;
  537. // Restart this daemon
  538. foreach($this->config->Processes->children() as $Type => $Entry) {
  539. $tmpkey = $this->makeDaemonKey($Entry, $Type);
  540. if ($tmpkey != $key) continue; // we don't want to start this one
  541. $startfunc = 'load'.$Type.'Daemon';
  542. $this->$startfunc($tmpport, $Entry);
  543. break; // ok, finished
  544. }
  545. break;
  546. default:
  547. echo 'UNKNOWN STATUS '.$data['status']."\n";
  548. #
  549. }
  550. }
  551. }
  552. function mainLoop() {
  553. while(1) {
  554. $this->checkRunning();
  555. $this->startMissing();
  556. $this->receiveStopped(); // waitpid
  557. $this->childrenStatus();
  558. $this->readIPCs(200000);
  559. Timer::processTimers();
  560. }
  561. }
  562. }