MDL-64359 core: Respect shutdown handlers on SIG
[moodle.git] / lib / classes / shutdown_manager.php
CommitLineData
38fc0130
PS
1<?php
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16
17/**
18 * Shutdown management class.
19 *
20 * @package core
21 * @copyright 2013 Petr Skoda {@link http://skodak.org}
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24
25defined('MOODLE_INTERNAL') || die();
26
27/**
28 * Shutdown management class.
29 *
30 * @package core
31 * @copyright 2013 Petr Skoda {@link http://skodak.org}
32 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
33 */
34class core_shutdown_manager {
35 /** @var array list of custom callbacks */
36 protected static $callbacks = array();
37 /** @var bool is this manager already registered? */
38 protected static $registered = false;
39
40 /**
41 * Register self as main shutdown handler.
42 *
43 * @private to be called from lib/setup.php only!
44 */
45 public static function initialize() {
46 if (self::$registered) {
47 debugging('Shutdown manager is already initialised!');
48 }
49 self::$registered = true;
50 register_shutdown_function(array('core_shutdown_manager', 'shutdown_handler'));
102d6ea3
AN
51
52 // Signal handlers should only be used when dealing with a CLI script.
53 // In the case of PHP called in a web server the server is the owning process and should handle the signal chain
54 // properly itself.
55 // The 'pcntl' extension is optional and not available on Windows.
56 if (CLI_SCRIPT && extension_loaded('pcntl') && function_exists('pcntl_async_signals')) {
57 // We capture and handle SIGINT (Ctrl+C) and SIGTERM (termination requested).
58 pcntl_async_signals(true);
59 pcntl_signal(SIGINT, ['core_shutdown_manager', 'signal_handler']);
60 pcntl_signal(SIGTERM, ['core_shutdown_manager', 'signal_handler']);
61 }
62 }
63
64 /**
65 * Signal handler for SIGINT, and SIGTERM.
66 *
67 * @param int $signo The signal being handled
68 */
69 public static function signal_handler($signo) {
70 // Note: There is no need to manually call the shutdown handler.
71 // The fact that we are calling exit() in this script means that the standard shutdown handling is performed
72 // anyway.
73 switch ($signo) {
74 case SIGTERM:
75 // Replicate native behaviour.
76 echo "Terminated: {$signo}\n";
77
78 // The standard exit code for SIGTERM is 143.
79 $exitcode = 143;
80 break;
81 case SIGINT:
82 // Replicate native behaviour.
83 echo "\n";
84
85 // The standard exit code for SIGINT (Ctrl+C) is 130.
86 $exitcode = 130;
87 break;
88 default:
89 // The signal handler was called with a signal it was not expecting.
90 // We should exit and complain.
91 echo "Warning: \core_shutdown_manager::signal_handler() was called with an unexpected signal ({$signo}).\n";
92 $exitcode = 1;
93 }
94
95 exit ($exitcode);
38fc0130
PS
96 }
97
98 /**
99 * Register custom shutdown function.
100 *
101 * @param callable $callback
102 * @param array $params
103 */
104 public static function register_function($callback, array $params = null) {
105 self::$callbacks[] = array($callback, $params);
106 }
107
108 /**
109 * @private - do NOT call directly.
110 */
111 public static function shutdown_handler() {
112 global $DB;
113
114 // Custom stuff first.
115 foreach (self::$callbacks as $data) {
116 list($callback, $params) = $data;
117 try {
118 if (!is_callable($callback)) {
119 error_log('Invalid custom shutdown function detected '.var_export($callback, true));
120 continue;
121 }
122 if ($params === null) {
123 call_user_func($callback);
124 } else {
125 call_user_func_array($callback, $params);
126 }
127 } catch (Exception $e) {
75ab4d2e 128 error_log('Exception ignored in shutdown function '.get_callable_name($callback).': '.$e->getMessage());
1766e6a1
MG
129 } catch (Throwable $e) {
130 // Engine errors in PHP7 throw exceptions of type Throwable (this "catch" will be ignored in PHP5).
75ab4d2e 131 error_log('Exception ignored in shutdown function '.get_callable_name($callback).': '.$e->getMessage());
38fc0130
PS
132 }
133 }
134
135 // Handle DB transactions, session need to be written afterwards
136 // in order to maintain consistency in all session handlers.
137 if ($DB->is_transaction_started()) {
138 if (!defined('PHPUNIT_TEST') or !PHPUNIT_TEST) {
139 // This should not happen, it usually indicates wrong catching of exceptions,
140 // because all transactions should be finished manually or in default exception handler.
141 $backtrace = $DB->get_transaction_start_backtrace();
142 error_log('Potential coding error - active database transaction detected during request shutdown:'."\n".format_backtrace($backtrace, true));
143 }
144 $DB->force_transaction_rollback();
145 }
146
147 // Close sessions - do it here to make it consistent for all session handlers.
148 \core\session\manager::write_close();
149
150 // Other cleanup.
151 self::request_shutdown();
152
153 // Stop profiling.
154 if (function_exists('profiling_is_running')) {
155 if (profiling_is_running()) {
156 profiling_stop();
157 }
158 }
159
160 // NOTE: do not dispose $DB and MUC here, they might be used from legacy shutdown functions.
161 }
162
163 /**
164 * Standard shutdown sequence.
165 */
166 protected static function request_shutdown() {
167 global $CFG;
168
169 // Help apache server if possible.
170 $apachereleasemem = false;
171 if (function_exists('apache_child_terminate') && function_exists('memory_get_usage') && ini_get_bool('child_terminate')) {
172 $limit = (empty($CFG->apachemaxmem) ? 64*1024*1024 : $CFG->apachemaxmem); // 64MB default.
173 if (memory_get_usage() > get_real_size($limit)) {
174 $apachereleasemem = $limit;
175 @apache_child_terminate();
176 }
177 }
178
179 // Deal with perf logging.
180 if (defined('MDL_PERF') || (!empty($CFG->perfdebug) and $CFG->perfdebug > 7)) {
181 if ($apachereleasemem) {
182 error_log('Mem usage over '.$apachereleasemem.': marking Apache child for reaping.');
183 }
184 if (defined('MDL_PERFTOLOG')) {
185 $perf = get_performance_info();
186 error_log("PERF: " . $perf['txt']);
187 }
188 if (defined('MDL_PERFINC')) {
189 $inc = get_included_files();
190 $ts = 0;
191 foreach ($inc as $f) {
192 if (preg_match(':^/:', $f)) {
193 $fs = filesize($f);
194 $ts += $fs;
195 $hfs = display_size($fs);
196 error_log(substr($f, strlen($CFG->dirroot)) . " size: $fs ($hfs)", null, null, 0);
197 } else {
198 error_log($f , null, null, 0);
199 }
200 }
201 if ($ts > 0 ) {
202 $hts = display_size($ts);
203 error_log("Total size of files included: $ts ($hts)");
204 }
205 }
206 }
207 }
208}