PageRenderTime 41ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/hphp/runtime/ext/hh_client/ext_hh_client.php

http://github.com/facebook/hiphop-php
PHP | 251 lines | 177 code | 36 blank | 38 comment | 25 complexity | a3e9db1921899bf53628a624c61e28c5 MD5 | raw file
Possible License(s): LGPL-2.1, BSD-2-Clause, BSD-3-Clause, MPL-2.0-no-copyleft-exception, MIT, LGPL-2.0, Apache-2.0
  1. <?hh // partial
  2. namespace __SystemLib\HH\Client {
  3. use \HH\Client\TypecheckStatus;
  4. use \HH\Client\TypecheckResult;
  5. abstract final class CacheKeys {
  6. const string TIME_CACHE_KEY = '__systemlib__hh_client_time';
  7. const string RESULT_CACHE_KEY = '__systemlib__hh_client_result';
  8. }
  9. function locate_hh(string $client_name): ?string {
  10. if (\strlen($client_name) > 0 && $client_name[0] == '/') {
  11. if (\is_executable($client_name)) {
  12. return $client_name;
  13. } else {
  14. return null;
  15. }
  16. }
  17. $base = \dirname(\PHP_BINARY);
  18. $next_to_hhvm = $base . '/' . $client_name;
  19. if (\is_executable($next_to_hhvm)) {
  20. return $next_to_hhvm;
  21. }
  22. $oss_build = $base . '/../hack/bin/' . $client_name;
  23. if (\is_executable($oss_build)) {
  24. return $oss_build;
  25. }
  26. $cmd = \sprintf('which %s > /dev/null 2>&1', \escapeshellarg($client_name));
  27. $ret = null;
  28. $output_arr = null;
  29. \exec($cmd, inout $output_arr, inout $ret);
  30. if ($ret === 0) {
  31. return $client_name;
  32. } else {
  33. return null;
  34. }
  35. }
  36. function typecheck_impl(string $input_client_name): TypecheckResult {
  37. $client_name = locate_hh($input_client_name);
  38. if (!$client_name) {
  39. $error_text = \sprintf(
  40. 'Hack typechecking failed: typechecker command not found: %s',
  41. $input_client_name,
  42. );
  43. return new TypecheckResult(TypecheckStatus::COMMAND_NOT_FOUND, $error_text);
  44. }
  45. // Use a largeish timeout in CLI mode, so we can wait for hh_server startup if
  46. // we need to. CLI is both tolerant of occasionally larger latencies, and also
  47. // we want to make sure that we don't *always* get an error that the server
  48. // isn't ready on the first CLI script execution in a repo.
  49. $timeout = \php_sapi_name() === 'cli' ? 5 : 0;
  50. $cmd = \sprintf(
  51. '%s --timeout %d --retries 0 --json %s 2>&1',
  52. \escapeshellarg($client_name),
  53. $timeout,
  54. \escapeshellarg(\dirname($_SERVER['SCRIPT_FILENAME'])),
  55. );
  56. $ret = null;
  57. $output_arr = null;
  58. $output = \exec($cmd, inout $output_arr, inout $ret);
  59. // 7 -> timeout, or ran out of retries
  60. if ($ret == 7) {
  61. return new TypecheckResult(
  62. TypecheckStatus::SERVER_BUSY,
  63. 'Hack typechecking failed: server not ready. Unless you have run '.
  64. '"hh_client" manually, you may be missing Hack type errors! Once the '.
  65. 'typechecker server has started up, the errors, if any, will then show '.
  66. 'up in this error log.'
  67. );
  68. }
  69. $json = \json_decode($output, true);
  70. if (!$json) {
  71. // This is typically "could not find hhconfig", which of course uses the
  72. // same exit code as "type error". See above about fixing this.
  73. return new TypecheckResult(
  74. TypecheckStatus::OTHER_ERROR,
  75. \implode(' ', $output_arr)
  76. );
  77. }
  78. $passed = ($ret === 0) && \hphp_array_idx($json, 'passed', false);
  79. if ($passed) {
  80. return new TypecheckResult(TypecheckStatus::SUCCESS, null, $json);
  81. } else {
  82. $errors = \hphp_array_idx($json, 'errors', null);
  83. if ($errors) {
  84. $first_msg = \reset(inout $errors)['message'];
  85. $first_msg = \reset(inout $first_msg);
  86. $error_text = \sprintf(
  87. 'Hack type error: %s at %s line %d',
  88. $first_msg['descr'],
  89. $first_msg['path'],
  90. $first_msg['line'],
  91. );
  92. } else {
  93. $error_text = \sprintf('Hack typechecking failed: %s', $output);
  94. }
  95. return new TypecheckResult(TypecheckStatus::TYPE_ERROR, $error_text, $json);
  96. }
  97. }
  98. }
  99. namespace HH\Client {
  100. use \__SystemLib\HH\Client\CacheKeys;
  101. enum TypecheckStatus : int {
  102. SUCCESS = 0;
  103. TYPE_ERROR = 1;
  104. SERVER_BUSY = 2;
  105. COMMAND_NOT_FOUND = 3;
  106. OTHER_ERROR = -1;
  107. }
  108. final class TypecheckResult implements \JsonSerializable {
  109. public function __construct(
  110. private TypecheckStatus $status,
  111. private ?string $error,
  112. private ?varray_or_darray $rawResult = null
  113. ) {}
  114. public function getStatus(): TypecheckStatus {
  115. return $this->status;
  116. }
  117. public function getError(): ?string {
  118. return $this->error;
  119. }
  120. public function triggerError(
  121. int $type_error_level = \E_RECOVERABLE_ERROR,
  122. int $client_error_level = \E_RECOVERABLE_ERROR,
  123. int $server_busy_level = \E_WARNING,
  124. ): void {
  125. switch ($this->status) {
  126. case TypecheckStatus::SUCCESS:
  127. // No error to trigger.
  128. break;
  129. case TypecheckStatus::TYPE_ERROR:
  130. \trigger_error($this->error, $type_error_level);
  131. break;
  132. case TypecheckStatus::SERVER_BUSY:
  133. \trigger_error($this->error, $server_busy_level);
  134. break;
  135. case TypecheckStatus::COMMAND_NOT_FOUND:
  136. case TypecheckStatus::OTHER_ERROR:
  137. \trigger_error($this->error, $client_error_level);
  138. break;
  139. }
  140. }
  141. public function jsonSerialize() {
  142. if ($this->rawResult) {
  143. return $this->rawResult;
  144. } else {
  145. // Return something that looks close to the hh_client response.
  146. return Map {
  147. 'passed' => false,
  148. 'errors' => Vector {
  149. Map {
  150. 'message' => Vector {
  151. Map {
  152. 'descr' => $this->error,
  153. },
  154. },
  155. },
  156. },
  157. };
  158. }
  159. }
  160. }
  161. /**
  162. * Typecheck the currently running endpoint with a given `hh_client`. Does some
  163. * caching to hopefully be pretty cheap to call, especially when there are no
  164. * errors and the code isn't changing. Relies on `hh_server` to poke a stamp
  165. * file to say "something has changed" to invalidate our cache.
  166. *
  167. * TODO Areas for future improvement:
  168. * - Key the cache by endpoint/hhconfig location, so that we correctly support
  169. * more than one project per HHVM instance.
  170. * - Populate the cache separately from this function, so that
  171. * typecheck_and_error can have a hot path that just checks "is the world
  172. * clean" without paying the apc_fetch deserialization cost (which is most
  173. * of the cost of this function, if I'm benchmarking correctly).
  174. * - Storing an array (instead of an object) in APC might be faster, due to the
  175. * way I think HHVM can optimize COW arrays.
  176. */
  177. function typecheck(string $client_name = 'hh_client'): TypecheckResult {
  178. // Fetch times from cache and from the stamp file. Both will return "false" on
  179. // error (no cached time or the stamp doesn't exist). The latter will also
  180. // emit a warning, which we don't care about, so suppress it.
  181. $success = false;
  182. $cached_time = \apc_fetch(CacheKeys::TIME_CACHE_KEY, inout $success);
  183. $old = \error_reporting();
  184. try {
  185. \error_reporting(0);
  186. $file_time = \filemtime('/tmp/hh_server/stamp');
  187. } finally {
  188. if (\error_reporting() === 0) {
  189. \error_reporting($old);
  190. }
  191. }
  192. $time = (int)$file_time;
  193. // If we actually have something in cache, and the times match, use it. Note
  194. // that we still don't care if the stamp file actually exists -- we just treat
  195. // that as "time 0" (cast bool to int); it will stay zero as long as the file
  196. // doesn't exist and become nonzero when hh_server starts up and creates it,
  197. // which is what we want.
  198. if ($cached_time !== false && (int)$cached_time === $time) {
  199. $result = \apc_fetch(CacheKeys::RESULT_CACHE_KEY, inout $success);
  200. } else {
  201. $result = \__SystemLib\HH\Client\typecheck_impl($client_name);
  202. \apc_store(CacheKeys::TIME_CACHE_KEY, $time);
  203. \apc_store(CacheKeys::RESULT_CACHE_KEY, $result);
  204. }
  205. return $result;
  206. }
  207. /**
  208. * This is deliberately an unconfigurable convenience wrapper. If you want
  209. * full configurability, call typecheck() and use the TypecheckResult
  210. * yourself.
  211. */
  212. function typecheck_and_error(): void {
  213. typecheck()->triggerError();
  214. }
  215. }