Update copyright
[friendica.git/.git] / src / Core / Process.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2021, the Friendica project
4  *
5  * @license GNU AGPL version 3 or any later version
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Affero General Public License as
9  * published by the Free Software Foundation, either version 3 of the
10  * License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19  *
20  */
21
22 namespace Friendica\Core;
23
24 use Friendica\App;
25 use Friendica\Core\Config\IConfig;
26 use Friendica\Model;
27 use Psr\Log\LoggerInterface;
28
29 /**
30  * Methods for interacting with the current process or create new process
31  *
32  * @todo 2019.12 Next release, this class holds all process relevant methods based on the big Worker class
33  *       - Starting new processes (including checks)
34  *       - Enabling multi-node processing (e.g. for docker service)
35  *         - Using an process-id per node
36  *         - Using memory locks for multi-node locking (redis, memcached, ..)
37  */
38 class Process
39 {
40         /**
41          * @var LoggerInterface
42          */
43         private $logger;
44
45         /**
46          * @var App\Mode
47          */
48         private $mode;
49
50         /**
51          * @var IConfig
52          */
53         private $config;
54
55         /**
56          * @var string
57          */
58         private $basePath;
59
60         /** @var Model\Process */
61         private $processModel;
62
63         /**
64          * The Process ID of this process
65          *
66          * @var int
67          */
68         private $pid;
69
70         public function __construct(LoggerInterface $logger, App\Mode $mode, IConfig $config, Model\Process $processModel, string $basepath, int $pid)
71         {
72                 $this->logger = $logger;
73                 $this->mode = $mode;
74                 $this->config = $config;
75                 $this->basePath = $basepath;
76                 $this->processModel = $processModel;
77                 $this->pid = $pid;
78         }
79
80         /**
81          * Set the process id
82          *
83          * @param integer $pid
84          * @return void
85          */
86         public function setPid(int $pid)
87         {
88                 $this->pid = $pid;
89         }
90
91         /**
92          * Log active processes into the "process" table
93          */
94         public function start()
95         {
96                 $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
97
98                 $command = basename($trace[0]['file']);
99
100                 $this->processModel->deleteInactive();
101                 $this->processModel->insert($command, $this->pid);
102         }
103
104         /**
105          * Remove the active process from the "process" table
106          *
107          * @return bool
108          * @throws \Exception
109          */
110         public function end()
111         {
112                 return $this->processModel->deleteByPid($this->pid);
113         }
114
115         /**
116          * Checks if the maximum number of database processes is reached
117          *
118          * @return bool Is the limit reached?
119          */
120         public function isMaxProcessesReached()
121         {
122                 // Deactivated, needs more investigating if this check really makes sense
123                 return false;
124
125                 /*
126                  * Commented out to suppress static analyzer issues
127                  *
128                 if ($this->mode->isBackend()) {
129                         $process = 'backend';
130                         $max_processes = $this->config->get('system', 'max_processes_backend');
131                         if (intval($max_processes) == 0) {
132                                 $max_processes = 5;
133                         }
134                 } else {
135                         $process = 'frontend';
136                         $max_processes = $this->config->get('system', 'max_processes_frontend');
137                         if (intval($max_processes) == 0) {
138                                 $max_processes = 20;
139                         }
140                 }
141
142                 $processlist = DBA::processlist();
143                 if ($processlist['list'] != '') {
144                         $this->logger->debug('Processcheck: Processes: ' . $processlist['amount'] . ' - Processlist: ' . $processlist['list']);
145
146                         if ($processlist['amount'] > $max_processes) {
147                                 $this->logger->debug('Processcheck: Maximum number of processes for ' . $process . ' tasks (' . $max_processes . ') reached.');
148                                 return true;
149                         }
150                 }
151                 return false;
152                  */
153         }
154
155         /**
156          * Checks if the minimal memory is reached
157          *
158          * @return bool Is the memory limit reached?
159          */
160         public function isMinMemoryReached()
161         {
162                 $min_memory = $this->config->get('system', 'min_memory', 0);
163                 if ($min_memory == 0) {
164                         return false;
165                 }
166
167                 if (!is_readable('/proc/meminfo')) {
168                         return false;
169                 }
170
171                 $memdata = explode("\n", file_get_contents('/proc/meminfo'));
172
173                 $meminfo = [];
174                 foreach ($memdata as $line) {
175                         $data = explode(':', $line);
176                         if (count($data) != 2) {
177                                 continue;
178                         }
179                         list($key, $val) = $data;
180                         $meminfo[$key] = (int)trim(str_replace('kB', '', $val));
181                         $meminfo[$key] = (int)($meminfo[$key] / 1024);
182                 }
183
184                 if (!isset($meminfo['MemFree'])) {
185                         return false;
186                 }
187
188                 $free = $meminfo['MemFree'];
189
190                 $reached = ($free < $min_memory);
191
192                 if ($reached) {
193                         $this->logger->warning('Minimal memory reached.', ['free' => $free, 'memtotal' => $meminfo['MemTotal'], 'limit' => $min_memory]);
194                 }
195
196                 return $reached;
197         }
198
199         /**
200          * Checks if the maximum load is reached
201          *
202          * @return bool Is the load reached?
203          */
204         public function isMaxLoadReached()
205         {
206                 if ($this->mode->isBackend()) {
207                         $process    = 'backend';
208                         $maxsysload = intval($this->config->get('system', 'maxloadavg'));
209                         if ($maxsysload < 1) {
210                                 $maxsysload = 50;
211                         }
212                 } else {
213                         $process    = 'frontend';
214                         $maxsysload = intval($this->config->get('system', 'maxloadavg_frontend'));
215                         if ($maxsysload < 1) {
216                                 $maxsysload = 50;
217                         }
218                 }
219
220                 $load = System::currentLoad();
221                 if ($load) {
222                         if (intval($load) > $maxsysload) {
223                                 $this->logger->warning('system load for process too high.', ['load' => $load, 'process' => $process, 'maxsysload' => $maxsysload]);
224                                 return true;
225                         }
226                 }
227                 return false;
228         }
229
230         /**
231          * Executes a child process with 'proc_open'
232          *
233          * @param string $command The command to execute
234          * @param array  $args    Arguments to pass to the command ( [ 'key' => value, 'key2' => value2, ... ]
235          */
236         public function run($command, $args)
237         {
238                 if (!function_exists('proc_open')) {
239                         $this->logger->warning('"proc_open" not available - quitting');
240                         return;
241                 }
242
243                 $cmdline = $this->config->get('config', 'php_path', 'php') . ' ' . escapeshellarg($command);
244
245                 foreach ($args as $key => $value) {
246                         if (!is_null($value) && is_bool($value) && !$value) {
247                                 continue;
248                         }
249
250                         $cmdline .= ' --' . $key;
251                         if (!is_null($value) && !is_bool($value)) {
252                                 $cmdline .= ' ' . $value;
253                         }
254                 }
255
256                 if ($this->isMinMemoryReached()) {
257                         $this->logger->warning('Memory limit reached - quitting');
258                         return;
259                 }
260
261                 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
262                         $resource = proc_open('cmd /c start /b ' . $cmdline, [], $foo, $this->basePath);
263                 } else {
264                         $resource = proc_open($cmdline . ' &', [], $foo, $this->basePath);
265                 }
266                 if (!is_resource($resource)) {
267                         $this->logger->warning('We got no resource for command.', ['command' => $cmdline]);
268                         return;
269                 }
270                 proc_close($resource);
271
272                 $this->logger->info('Executed "proc_open"', ['command' => $cmdline, 'callstack' => System::callstack(10)]);
273         }
274 }