PageRenderTime 39ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 0ms

/debugproxy.php

https://github.com/dfeyer/debugproxy
PHP | 448 lines | 305 code | 73 blank | 70 comment | 47 complexity | cb40f20584b9f2912c394c3f0ba51389 MD5 | raw file
Possible License(s): GPL-3.0
  1. #!/opt/local/bin/php5
  2. <?php
  3. /*
  4. $Id: dbgp-mapper.php 7 2007-11-25 22:35:19Z drslump $
  5. DBGp Path Mapper
  6. An intercepting proxy for DBGp connections to apply custom path mappings
  7. to the filenames transmitted. It can be useful to improve debugging sessions
  8. initiated from a remote server.
  9. License:
  10. The GNU General Public License version 3 (GPLv3)
  11. This file is part of DBGp Path Mapper.
  12. DBGp Path Mapper is free software; you can redistribute it and/or modify it
  13. under the terms of the GNU General Public License as published by the Free
  14. Software Foundation; either version 2 of the License, or (at your option)
  15. any later version.
  16. DBGp Path Mapper is distributed in the hope that it will be useful, but
  17. WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
  18. or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
  19. for more details.
  20. You should have received a copy of the GNU General Public License along
  21. with DBGp Path Mapper; if not, write to the Free Software Foundation, Inc.,
  22. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
  23. See bundled license.txt or check <http://www.gnu.org/copyleft/gpl.html>
  24. Copyright:
  25. copyright (c) 2007 Ivan Montes <http://blog.netxus.es>
  26. adjustments 2012 by Sebastian Kurf端rst <http://sandstorm-media.de>
  27. */
  28. class DBGp_Mapper {
  29. static $asDaemon = false;
  30. static $listenSocket = null;
  31. static $dbgSocket = null;
  32. static $ideSocket = null;
  33. static $mappings = array();
  34. static public $debug = FALSE;
  35. static public $additionalContexts = '';
  36. static function addMapping($idePath, $dbgPath) {
  37. self::$mappings[$idePath] = $dbgPath;
  38. }
  39. static function destroySockets($sockets) {
  40. foreach ($sockets as $sock) {
  41. @socket_shutdown($sock, 2);
  42. @socket_close($sock);
  43. }
  44. }
  45. static function output($msg) {
  46. if (!self::$asDaemon)
  47. echo $msg;
  48. }
  49. static function shutdown($msg = '') {
  50. // make sure all sockets are closed
  51. self::destroySockets(array(self::$dbgSocket, self::$ideSocket, self::$listenSocket));
  52. if ($msg) {
  53. die($msg);
  54. } else {
  55. exit();
  56. }
  57. }
  58. static function parseCommandArguments($args) {
  59. //preg_match_all( '/-([A-Za-z]+)\s+("[^"]*"|[^\s]*)|--\s+([A-Za-z0-9]*)$/', $args, $m, PREG_SET_ORDER);
  60. preg_match_all('/-([A-Za-z-]+)\s+("[^"]*"|[^\s]*)/', $args, $m, PREG_SET_ORDER);
  61. $args = array();
  62. foreach ($m as $arg) {
  63. $args[$arg[1]] = $arg[2];
  64. }
  65. return $args;
  66. }
  67. static function buildCommandArguments($args) {
  68. $out = array();
  69. foreach ($args as $arg => $value) {
  70. $out[] = trim('-' . $arg . ' ' . $value);
  71. }
  72. return implode(' ', $out);
  73. }
  74. static protected function constructClassNameFromPath($path) {
  75. $matches = array();
  76. preg_match('#(.*?)/Packages/(.*?)/(.*).php#', $path, $matches);
  77. $flowBaseUri = $matches[1];
  78. $classPath = preg_replace('#.*[^/]+/Classes/#', '', $matches[3]);
  79. $className = str_replace(array('.', '/'), '\\', $classPath);
  80. return array($flowBaseUri, $className);
  81. }
  82. static function map($path) {
  83. if (strpos($path, '/Packages/') !== FALSE) {
  84. // We assume it's a Flow class where a breakpoint was set
  85. list ($flowBaseUri, $className) = self::constructClassNameFromPath($path);
  86. $setBreakpointsInFiles = array($path);
  87. // TODO: currently we only support ONE context!!
  88. $contexts = array();
  89. if (strlen(self::$additionalContexts) > 0) {
  90. foreach (explode(',', self::$additionalContexts) as $ctx) {
  91. $contexts[] = $ctx;
  92. }
  93. }
  94. foreach ($contexts as $context) {
  95. $codeCacheFileName = $flowBaseUri . '/Data/Temporary/' . $context . '/Cache/Code/Flow_Object_Classes/' . str_replace('\\', '_', $className) . '_Original.php';
  96. if (strpos('@Flow\\', file_get_contents($path)) !== FALSE || file_exists($codeCacheFileName)) {
  97. $setBreakpointsInFiles = array($codeCacheFileName);
  98. // TODO: currently we only support ONE context
  99. }
  100. self::$mappings[$codeCacheFileName] = $path;
  101. }
  102. return $setBreakpointsInFiles;
  103. } else {
  104. return array($path);
  105. }
  106. }
  107. static function unmap($path) {
  108. foreach (self::$mappings as $k => $v) {
  109. $path = str_ireplace($k, $v, $path);
  110. }
  111. return $path;
  112. }
  113. static function run($ideHost, $idePort, $bindIp, $bindPort) {
  114. # Initialize the listenning socket
  115. self::$listenSocket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)
  116. or self::shutdown('Unable to create listenning socket: ' . socket_strerror(socket_last_error()));
  117. socket_set_option(self::$listenSocket, SOL_SOCKET, SO_REUSEADDR, 1)
  118. or self::shutdown('Failed setting options on listenning socket: ' . socket_strerror(socket_last_error()));
  119. socket_bind(self::$listenSocket, $bindIp, $bindPort)
  120. or self::shutdown("Failed binding listenning socket ($bindIp:$bindPort): " . socket_strerror(socket_last_error()));
  121. socket_listen(self::$listenSocket)
  122. or self::shutdown('Failed listenning on socket: ' . socket_strerror(socket_last_error()));
  123. self::output("Running DBGp Path Mapper\n");
  124. $ideBuffer = $dbgBuffer = '';
  125. $dbgLengthSize = 0;
  126. $sockets = array(self::$listenSocket);
  127. while (true) {
  128. # create a copy of the sockets list since it'll be modified
  129. $toRead = $sockets;
  130. # get a list of all clients which have data to be read from
  131. if (socket_select($toRead, $write = null, $except = null, 0, 10) < 1) {
  132. continue;
  133. }
  134. # check for new connections
  135. if (in_array(self::$listenSocket, $toRead)) {
  136. # check if there are connections opened
  137. if (count($sockets) > 1) {
  138. self::output("Resetting debug session\n");
  139. $sockets = array(self::$listenSocket);
  140. self::destroySockets(array(self::$dbgSocket, self::$ideSocket));
  141. }
  142. # accept the debugger connection
  143. self::$dbgSocket = socket_accept(self::$listenSocket);
  144. # create a new connection to the IDE
  145. self::$ideSocket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP) or
  146. self::shutdown('Fatal: Unable to create a new socket');
  147. if (!@socket_connect(self::$ideSocket, $ideHost, $idePort)) {
  148. self::output("Error: Unable to contact the IDE at $ideHost:$idePort\n");
  149. # destroy the connection with the debugger
  150. self::destroySockets(array(self::$dbgSocket));
  151. } else {
  152. # add the debugger and the IDE to the socket to the list
  153. $sockets[] = self::$dbgSocket;
  154. $sockets[] = self::$ideSocket;
  155. self::output("New debug session\n");
  156. }
  157. # remove listenning socket from the clients-with-data array
  158. $key = array_search($listener, $toRead);
  159. unset($toRead[$key]);
  160. }
  161. # process the sockets with data
  162. foreach ($toRead as $sock) {
  163. # read data
  164. $data = @socket_read($sock, 1024);
  165. # check if the ide or the debugger has disconnected
  166. if ($data === '' || $data === false) {
  167. # reset the sockets list
  168. $sockets = array(self::$listenSocket);
  169. self::destroySockets(array(self::$dbgSocket, self::$ideSocket));
  170. self::output("Debug session closed\n");
  171. # nothing else to do
  172. continue;
  173. }
  174. $pos = strpos($data, "\0");
  175. # check if the data comes from the debugger or the IDE
  176. if ($sock === self::$ideSocket) {
  177. # the command is not complete so just store in buffer
  178. if ($pos === false) {
  179. $ideBuffer .= $data;
  180. } else {
  181. # end of command found, store it in the buffer
  182. $ideBuffer .= substr($data, 0, $pos + 1);
  183. $buf = '';
  184. $commands = explode("\0", $ideBuffer);
  185. foreach ($commands as $cmd) {
  186. if (strlen($cmd)) {
  187. $parts = explode(" ", $cmd);
  188. $command = array_shift($parts);
  189. if ($command === 'breakpoint_set') {
  190. $args = self::parseCommandArguments(implode(' ', $parts));
  191. $files = self::map($args['f']);
  192. foreach ($files as $file) {
  193. $args['f'] = $file;
  194. $cmd = $command . ' ' . self::buildCommandArguments($args);
  195. $buf .= $cmd . "\0";
  196. }
  197. } else {
  198. $buf .= $cmd . "\0";
  199. }
  200. }
  201. }
  202. if (self::$debug) {
  203. echo "\n\n\n TO SERVER:\n";
  204. echo $buf;
  205. echo "\n\n";
  206. }
  207. socket_write(self::$dbgSocket, $buf);
  208. # set the buffer with the start of a new command if any
  209. $ideBuffer = substr($data, $pos + 1);
  210. }
  211. } else {
  212. if ($pos === false) {
  213. $dbgBuffer .= $data;
  214. continue;
  215. }
  216. # check if we found the null byte after the packet length
  217. if (!$dbgLengthSize) {
  218. $dbgLengthSize = $pos;
  219. $pos = strpos($data, "\0", $pos + 1);
  220. if ($pos === false) {
  221. $dbgBuffer .= $data;
  222. continue;
  223. }
  224. }
  225. # add the remaining data for a packet to the buffer
  226. $dbgBuffer .= substr($data, 0, $pos + 1);
  227. $sxe = simplexml_load_string(trim(substr($dbgBuffer, $dbgLengthSize)));
  228. # reset the buffer with the data left over
  229. $dbgBuffer = substr($data, $pos + 1);
  230. $dbgLengthSize = 0;
  231. if ($sxe->children('http://xdebug.org/dbgp/xdebug')->message) {
  232. foreach($sxe->children('http://xdebug.org/dbgp/xdebug')->message as $msg) {
  233. if ($msg->attributes()->filename) {
  234. $msg->addAttribute('filename', self::unmap((string)$msg->attributes()->filename));
  235. }
  236. }
  237. }
  238. if (!empty($sxe['fileuri'])) {
  239. $sxe['fileuri'] = self::unmap($sxe['fileuri']);
  240. } elseif ($sxe->stack['filename']) {
  241. foreach ($sxe->stack as $stack) {
  242. $stack['filename'] = self::unmap($stack['filename']);
  243. }
  244. }
  245. # prepare the processed xml to send a packet message
  246. $xml = trim($sxe->asXML(), " \t\n\r");
  247. $xml = (strlen($xml)) . "\0" . $xml . "\0";
  248. if (self::$debug) {
  249. echo "\n\n\n SENDING TO IDE: ";
  250. echo $xml;
  251. echo "\n\n\n";
  252. }
  253. socket_write(self::$ideSocket, $xml);
  254. }
  255. }
  256. }
  257. # close listenner socket
  258. socket_close($listener);
  259. }
  260. static function help() {
  261. global $argv;
  262. $help = array(
  263. $argv[0] . " - DBGp Path Mapper <http://blog.netxus.es>, adjusted by Sebastian Kurf端rst for Flow (http://sandstorm-media.de)",
  264. "",
  265. "If you set a breakpoint in one of Flow-managed PHP classes, this proxy",
  266. "will instead set the breakpoint in the proxy class, if that makes sense.",
  267. "Thus, you can work with the debugger as if proxy classes would not exist.",
  268. "",
  269. "Usage:",
  270. "\t" . $argv[0] . " -c CONTEXTNAME",
  271. "",
  272. "With the default configuration, xdebug needs to connect to port 9000,",
  273. "and your IDE should listen on port 9010.",
  274. "",
  275. "You need to specify the context your Flow runs in, so Testing for functional tests",
  276. "and Development/Production for real-world runs.",
  277. "",
  278. "Options:",
  279. "\t-h Show this help and exit",
  280. "\t-V Show version and exit",
  281. "\t-i hostname Client/IDE ip or host address (default: 127.0.0.1)",
  282. "\t-p port Client/IDE port number (default: 9010)",
  283. "\t-I ip Bind to this ip address (default: all interfaces)",
  284. "\t-P port Bind to this port number (default: 9000)",
  285. "\t-f Run in foreground (default: disabled)",
  286. "\t-c Development",
  287. "\t The context to run as",
  288. "\t Note: There is NO SPACE ALLOWED between the additional contexts.",
  289. "\t-d enable debugging mode",
  290. "",
  291. "",
  292. "Note: We use the following heuristic to determine whether the breakpoints",
  293. "should happen in the original file or the cached one:",
  294. " - if a file exists inside the cache directory, use this one",
  295. " - if the code file contains a Flow annotation, we use a cached file",
  296. "",
  297. "This heuristic might especially be wrong in case the cache file does not exist yet,",
  298. "i.e. when the caches are empty on first run."
  299. );
  300. echo implode(PHP_EOL, $help);
  301. }
  302. static function processArguments() {
  303. if (function_exists('getopt')) {
  304. $r = getopt('dhVfi:p:I:P:c:');
  305. } else {
  306. $args = implode(' ', $GLOBALS['argv']);
  307. $r = self::parseCommandArguments($args);
  308. }
  309. if (isset($r['V'])) {
  310. echo "DBGp Path Mapper v2.0 - Flow version of 09.08.2012\n";
  311. exit();
  312. } else if (isset($r['h'])) {
  313. self::help();
  314. exit();
  315. }
  316. return array(
  317. 'i' => isset($r['i']) ? $r['i'] : '127.0.0.1',
  318. 'p' => isset($r['p']) ? $r['p'] : '9010',
  319. 'I' => isset($r['I']) ? $r['I'] : '0.0.0.0',
  320. 'P' => isset($r['P']) ? $r['P'] : '9000',
  321. 'f' => isset($r['f']) ? true : false,
  322. 'c' => isset($r['c']) ? $r['c'] : '',
  323. 'd' => isset($r['d']) ? true : false
  324. );
  325. }
  326. static function daemonize() {
  327. if (!function_exists('pcntl_fork') || !function_exists('posix_setsid')) {
  328. echo "Warning: Unable to run as daemon, falling back to foreground\n";
  329. return;
  330. }
  331. $pid = pcntl_fork();
  332. if ($pid === -1) {
  333. die('Could not fork');
  334. } else if ($pid) {
  335. //die("Forked child ($pid)");
  336. }
  337. if (!posix_setsid()) {
  338. die('Could not detach from terminal');
  339. }
  340. self::$asDaemon = true;
  341. fclose(STDIN);
  342. fclose(STDOUT);
  343. fclose(STDERR);
  344. }
  345. }
  346. # set up some stuff to run as a daemon
  347. set_time_limit(0);
  348. error_reporting(E_ERROR);
  349. ini_set('output_handler', '');
  350. @ob_end_flush();
  351. # parse arguments
  352. $args = DBGp_Mapper::processArguments();
  353. # makes sure we exit gracefully
  354. register_shutdown_function('DBGp_Mapper::shutdown');
  355. if (!$args['f']) {
  356. echo "Initializing daemon...\n";
  357. DBGp_Mapper::daemonize();
  358. }
  359. DBGp_Mapper::$debug = $args['d'];
  360. DBGp_Mapper::$additionalContexts = $args['c'];
  361. # run the process to listen for connections
  362. DBGp_Mapper::run($args['i'], $args['p'], $args['I'], $args['P']);