3131use App \Models \Eventlog ;
3232use App \Polling \Measure \Measurement ;
3333use Exception ;
34+ use Illuminate \Support \Facades \Log ;
3435use Illuminate \Support \Str ;
35- use LibreNMS \Enum \ImageFormat ;
3636use LibreNMS \Enum \Severity ;
3737use LibreNMS \Exceptions \FileExistsException ;
3838use LibreNMS \Exceptions \RrdException ;
3939use LibreNMS \Exceptions \RrdGraphException ;
40- use LibreNMS \Proc ;
40+ use LibreNMS \Exceptions \RrdNotFoundException ;
41+ use LibreNMS \Exceptions \RrdUpdateTooFrequentException ;
4142use LibreNMS \RRD \RrdProcess ;
4243use LibreNMS \Util \Debug ;
4344use LibreNMS \Util \Rewrite ;
44- use Log ;
45- use Symfony \Component \Process \Process ;
4645
4746class Rrd extends BaseDatastore
4847{
4948 private $ disabled = false ;
5049
51- private ?Proc $ sync_process = null ;
52- private ?Proc $ async_process = null ;
50+ private ?RrdProcess $ rrd = null ;
5351 /** @var string */
5452 private $ rrd_dir ;
5553 /** @var string */
@@ -60,8 +58,6 @@ class Rrd extends BaseDatastore
6058 private array $ rra ;
6159 /** @var int */
6260 private $ step ;
63- /** @var string */
64- private $ rrdtool_executable ;
6561
6662 public function __construct ()
6763 {
@@ -92,67 +88,20 @@ protected function loadConfig(): void
9288 ' RRA:LAST:0.5:1:2016 '
9389 )));
9490 $ this ->version = LibrenmsConfig::get ('rrdtool_version ' , '1.4 ' );
95- $ this ->rrdtool_executable = LibrenmsConfig::get ('rrdtool ' , 'rrdtool ' );
96- }
97-
98- /**
99- * Opens up a pipe to RRDTool using handles provided
100- *
101- * @param bool $dual_process start an additional process that's output should be read after every command
102- * @return bool the process(s) have been successfully started
103- */
104- public function init (bool $ dual_process = true ): bool
105- {
106- $ command = $ this ->rrdtool_executable . ' - ' ;
107-
108- $ descriptor_spec = [
109- 0 => ['pipe ' , 'r ' ], // stdin is a pipe that the child will read from
110- 1 => ['pipe ' , 'w ' ], // stdout is a pipe that the child will write to
111- 2 => ['pipe ' , 'w ' ], // stderr is a pipe that the child will write to
112- ];
113-
114- $ cwd = $ this ->rrd_dir ;
115-
116- try {
117- if (! $ this ->isSyncRunning ()) {
118- $ this ->sync_process = new Proc ($ command , $ descriptor_spec , $ cwd );
119- }
120-
121- if ($ dual_process && ! $ this ->isAsyncRunning ()) {
122- $ this ->async_process = new Proc ($ command , $ descriptor_spec , $ cwd );
123- $ this ->async_process ->setSynchronous (false );
124- }
125-
126- return $ this ->isSyncRunning () && ($ dual_process ? $ this ->isAsyncRunning () : true );
127- } catch (Exception $ e ) {
128- Log::error ('Failed to start RRD datastore: ' . $ e ->getMessage ());
129-
130- return false ;
131- }
13291 }
13392
134- public function isSyncRunning ( ): bool
93+ public function init ( int $ timeout = 600 ): void
13594 {
136- return isset ($ this ->sync_process ) && $ this ->sync_process ->isRunning ();
137- }
138-
139- public function isAsyncRunning (): bool
140- {
141- return isset ($ this ->async_process ) && $ this ->async_process ->isRunning ();
95+ $ this ->rrd ??= app (RrdProcess::class, ['timeout ' => $ timeout ]);
14296 }
14397
14498 /**
145- * Close all open rrdtool processes .
99+ * Close rrdtool process .
146100 * This should be done before exiting
147101 */
148102 public function terminate (): void
149103 {
150- if ($ this ->isSyncRunning ()) {
151- $ this ->sync_process ->close ('quit ' );
152- }
153- if ($ this ->isAsyncRunning ()) {
154- $ this ->async_process ->close ('quit ' );
155- }
104+ $ this ->rrd ?->stop();
156105 }
157106
158107 /**
@@ -187,21 +136,25 @@ public function write(string $measurement, array $fields, array $tags = [], arra
187136
188137 return $ valid ;
189138 }, ARRAY_FILTER_USE_KEY );
139+ }
190140
191- if (! $ this ->checkRrdExists ($ rrd )) {
192- $ options = ['--step ' , $ step , ...$ rrd_def ->getArguments (), ...$ this ->rra ];
193- $ this ->command ('create ' , $ rrd , $ options );
141+ try {
142+ $ this ->update ($ rrd , $ fields );
143+ } catch (RrdUpdateTooFrequentException ) {
144+ Log::debug ("RRD warning: update too soon for $ rrd " );
145+ } catch (RrdNotFoundException ) {
146+ if (isset ($ rrd_def )) {
147+ $ this ->command ('create ' , $ rrd , ['--step ' , $ step , ...$ rrd_def ->getArguments (), ...$ this ->rra ]);
148+ $ this ->update ($ rrd , $ fields );
194149 }
195150 }
196-
197- $ this ->update ($ rrd , $ fields );
198151 }
199152
200153 public function lastUpdate (string $ filename ): ?TimeSeriesPoint
201154 {
202- $ output = $ this ->command ('lastupdate ' , $ filename )[ 0 ] ;
155+ $ output = $ this ->command ('lastupdate ' , $ filename );
203156
204- if (preg_match ('/((?: \w+)+)\n\n(\d+):((?: [\d.-]+)+)\nOK/ ' , ( string ) $ output , $ matches )) {
157+ if (preg_match ('/((?: \w+)+)\n\n(\d+):((?: [\d.-]+)+)\nOK/ ' , $ output , $ matches )) {
205158 $ data = array_combine (
206159 explode (' ' , ltrim ($ matches [1 ])),
207160 explode (' ' , ltrim ($ matches [3 ])),
@@ -219,33 +172,17 @@ public function lastUpdate(string $filename): ?TimeSeriesPoint
219172 *
220173 * @param string $filename
221174 * @param array $data
222- * @return array
223175 *
224176 * @throws RrdException
177+ * @throws Exception
225178 *
226179 * @internal
227180 */
228- public function update ($ filename , $ data ): array
181+ public function update (string $ filename , array $ data ): void
229182 {
230- $ values = [];
231- // Do some sanitation on the data if passed as an array.
232-
233- if (is_array ($ data )) {
234- $ values [] = 'N ' ;
235- foreach ($ data as $ v ) {
236- if (! is_numeric ($ v )) {
237- $ v = 'U ' ;
238- }
239-
240- $ values [] = $ v ;
241- }
183+ $ data = 'N: ' . implode (': ' , array_map (fn ($ v ) => is_numeric ($ v ) ? $ v : 'U ' , $ data ));
242184
243- $ data = implode (': ' , $ values );
244-
245- return $ this ->command ('update ' , $ filename , [$ data ]);
246- }
247-
248- throw new RrdException ('Bad options passed to rrdtool_update ' );
185+ $ this ->command ('update ' , $ filename , [$ data ]);
249186 }
250187
251188 // rrdtool_update
@@ -379,7 +316,7 @@ public function name($host, $extra, $extension = '.rrd'): string
379316 */
380317 public function dirFromHost ($ host ): string
381318 {
382- $ host = self ::safeName (trim ($ host , '[] ' ));
319+ $ host = self ::safeName (trim (( string ) $ host , '[] ' ));
383320
384321 return Str::finish ($ this ->rrd_dir , '/ ' ) . $ host ;
385322 }
@@ -392,24 +329,24 @@ public function dirFromHost($host): string
392329 * @param string $command create, update, updatev, graph, graphv, dump, restore, fetch, tune, first, last, lastupdate, info, resize, xport, flushcached
393330 * @param string $filename The full patth to the rrd file
394331 * @param array $options rrdtool command options
395- * @return array the output of stdout and stderr in an array
332+ * @return string the output of the command
396333 *
397334 * @throws Exception thrown when the rrdtool process(s) cannot be started
398335 */
399- private function command (string $ command , string $ filename , array $ options = []): array
336+ private function command (string $ command , string $ filename , array $ options = []): string
400337 {
401338 $ stat = Measurement::start ($ this ->coalesceStatisticType ($ command ));
402- $ output = null ;
339+ $ output = '' ;
403340
404341 try {
405342 $ cmd = self ::buildCommand ($ command , $ filename , $ options );
406343 } catch (FileExistsException ) {
407344 Log::debug ("RRD[%g $ filename already exists%n] " , ['color ' => true ]);
408345
409- return [ null , null ] ;
346+ return $ output ;
410347 }
411348
412- Log:: debug ( ' RRD[%g ' . implode (' ' , $ cmd) . ' %n] ' , [ ' color ' => true ] );
349+ $ commandLine = implode (' ' , $ cmd );
413350
414351 // do not write rrd files, but allow read-only commands
415352 $ ro_commands = ['graph ' , 'graphv ' , 'dump ' , 'fetch ' , 'first ' , 'last ' , 'lastupdate ' , 'info ' , 'xport ' ];
@@ -418,24 +355,14 @@ private function command(string $command, string $filename, array $options = [])
418355 Log::debug ('[%rRRD Disabled%n] ' , ['color ' => true ]);
419356 }
420357
421- return [ null , null ] ;
358+ return $ output ;
422359 }
423360
424- // send the command!
425- if (in_array ($ command , ['last ' , 'list ' , 'lastupdate ' ]) && $ this ->init (false )) {
426- // send this to our synchronous process so output is guaranteed
427- $ output = $ this ->sync_process ->sendCommand (implode (' ' , $ cmd ));
428- } elseif ($ this ->init ()) {
429- // don't care about the return of other commands, so send them to the faster async process
430- $ output = $ this ->async_process ->sendCommand (implode (' ' , $ cmd ));
431- } else {
432- Log::error ('rrdtool could not start ' );
433- }
361+ $ this ->init ();
362+ $ output = $ this ->rrd ->run ($ commandLine );
434363
435- if (Debug::isVerbose ()) {
436- echo 'RRDtool Output: ' ;
437- echo $ output [0 ];
438- echo $ output [1 ];
364+ if (Debug::isVerbose () && $ output ) {
365+ Log::debug ('RRDtool Output: ' . $ output );
439366 }
440367
441368 $ this ->recordStatistic ($ stat ->end ());
@@ -493,8 +420,7 @@ public function getRrdFiles(string $hostname): array
493420 {
494421 if ($ this ->rrdcached ) {
495422 $ output = $ this ->command ('list ' , '/ ' . self ::safeName ($ hostname ));
496-
497- $ files = explode ("\n" , trim ($ output [0 ] ?? '' ));
423+ $ files = explode ("\n" , trim ($ output ));
498424 array_pop ($ files ); // remove rrdcached status line
499425 } else {
500426 $ files = glob ($ this ->dirFromHost ($ hostname ) . '/*.rrd ' ) ?: [];
@@ -552,10 +478,14 @@ public function getRrdApplicationArrays($device, $app_id, $app_name, $category =
552478 public function checkRrdExists ($ filename ): bool
553479 {
554480 if ($ this ->rrdcached && version_compare ($ this ->version , '1.5 ' , '>= ' )) {
555- $ check_output = implode ('' , $ this ->command ('last ' , $ filename ));
556- $ filename = str_replace ([$ this ->rrd_dir . '/ ' , $ this ->rrd_dir ], '' , $ filename );
481+ try {
482+ $ check_output = $ this ->command ('last ' , $ filename );
483+ $ filename = str_replace ([$ this ->rrd_dir . '/ ' , $ this ->rrd_dir ], '' , $ filename );
557484
558- return ! (str_contains ($ check_output , $ filename ) && str_contains ($ check_output , 'No such file or directory ' ));
485+ return ! (str_contains ($ check_output , $ filename ) && str_contains ($ check_output , 'No such file or directory ' ));
486+ } catch (RrdNotFoundException ) {
487+ return false ;
488+ }
559489 } else {
560490 return is_file ($ filename );
561491 }
@@ -585,52 +515,23 @@ public function purge($hostname, $prefix): void
585515 * Graphs are a single command per run, so this just runs rrdtool
586516 *
587517 * @param array $options
588- * @param array|null $env
589518 * @return string
590519 *
591520 * @throws RrdGraphException
592521 */
593- public function graph (array $ options, ? array $ env = null ): string
522+ public function graph (array $ options ): string
594523 {
595- $ process = new Process ([$ this ->rrdtool_executable , '- ' ], $ this ->rrd_dir , $ env );
596- $ process ->setTimeout (300 );
597- $ process ->setIdleTimeout (300 );
598-
599524 try {
600525 $ command = $ this ->buildCommand ('graph ' , '- ' , $ options );
601- $ process ->setInput ('" ' . implode ('" " ' , $ command ) . "\"\nquit " );
602- $ process ->run ();
603- } catch (FileExistsException $ e ) {
604- throw new RrdGraphException ($ e ->getMessage (), 'File Exists ' );
605- }
606526
607- $ feedback_position = strrpos ($ process ->getOutput (), 'OK ' );
608- if ($ feedback_position !== false ) {
609- return substr ($ process ->getOutput (), 0 , $ feedback_position );
610- }
527+ $ this ->init (300 );
528+ $ image = $ this ->rrd ->run ('" ' . implode ('" " ' , $ command ) . '" ' );
529+ $ this ->rrd ->stop ();
611530
612- // if valid image is returned with error, extract image and feedback
613- // rrdtool defaults to png if imgformat not specified
614- $ imgformat_option = array_find ($ options , fn ($ o ) => str_starts_with ((string ) $ o , '--imgformat= ' ));
615- $ graph_type = $ imgformat_option ? strtolower (substr ((string ) $ imgformat_option , 12 )) : 'png ' ;
616- $ imageFormat = ImageFormat::forGraph ($ graph_type );
617-
618- $ search = $ imageFormat ->getImageEnd ();
619- if (($ position = strrpos ($ process ->getOutput (), $ search )) !== false ) {
620- $ position += strlen ($ search );
621- throw new RrdGraphException (
622- substr ($ process ->getOutput (), $ position ),
623- null ,
624- null ,
625- null ,
626- $ process ->getExitCode (),
627- substr ($ process ->getOutput (), 0 , $ position )
628- );
531+ return $ image ;
532+ } catch (RrdException $ e ) {
533+ throw new RrdGraphException ($ e ->getMessage (), 'Error ' );
629534 }
630-
631- // only error text was returned
632- $ error = trim ($ process ->getOutput () . PHP_EOL . $ process ->getErrorOutput ());
633- throw new RrdGraphException ($ error , null , null , null , $ process ->getExitCode ());
634535 }
635536
636537 /**
@@ -690,7 +591,7 @@ public static function fixedSafeDescr($descr, $length): string
690591 *
691592 * @return string
692593 */
693- public static function version (): ? string
594+ public static function version (): string
694595 {
695596 try {
696597 $ rrd = app (RrdProcess::class, ['timeout ' => 10 ]);
@@ -704,7 +605,7 @@ public static function version(): ?string
704605 //
705606 }
706607
707- return null ;
608+ return '' ;
708609 }
709610
710611 /**
0 commit comments