| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | #include "libnetdata/libnetdata.h" |
| 4 | |
| 5 | #include <cups/cups.h> |
| 6 | #include <limits.h> |
| 7 | |
| 8 | // Variables |
| 9 | |
| 10 | static int debug = 0; |
| 11 | |
| 12 | static int netdata_update_every = 1; |
| 13 | static uint32_t netdata_priority = 100004; |
| 14 | |
| 15 | http_t *http; // connection to the cups daemon |
| 16 | |
| 17 | /* |
| 18 | * Used to aggregate job metrics for a destination (and all destinations). |
| 19 | */ |
| 20 | struct job_metrics { |
| 21 | uint32_t id; |
| 22 | |
| 23 | bool is_collected; // flag if this was collected in the current cycle |
| 24 | |
| 25 | int num_pending; |
| 26 | int num_processing; |
| 27 | int num_held; |
| 28 | |
| 29 | int size_pending; // in kilobyte |
| 30 | int size_processing; // in kilobyte |
| 31 | int size_held; // in kilobyte |
| 32 | }; |
| 33 | DICTIONARY *dict_dest_job_metrics = NULL; |
| 34 | struct job_metrics global_job_metrics; |
| 35 | |
| 36 | int num_dest_total; |
| 37 | int num_dest_accepting_jobs; |
| 38 | int num_dest_shared; |
| 39 | |
| 40 | int num_dest_idle; |
| 41 | int num_dest_printing; |
| 42 | int num_dest_stopped; |
| 43 | |
| 44 | void print_help() { |
| 45 | fprintf(stderr, |
| 46 | "\n" |
| 47 | "netdata cups.plugin %s\n" |
| 48 | "\n" |
| 49 | "Copyright 2018-2025 Netdata Inc.\n" |
| 50 | "Original Author: Simon Nagl <simon.nagl@gmx.de>\n" |
| 51 | "Released under GNU General Public License v3+.\n" |
| 52 | "\n" |
| 53 | "This program is a data collector plugin for netdata.\n" |
| 54 | "\n" |
| 55 | "SYNOPSIS: cups.plugin [-d][-h][-v] COLLECTION_FREQUENCY\n" |
| 56 | "\n" |
| 57 | "Options:" |
| 58 | "\n" |
| 59 | " COLLECTION_FREQUENCY data collection frequency in seconds\n" |
| 60 | "\n" |
| 61 | " -d enable verbose output\n" |
| 62 | " default: disabled\n" |
| 63 | "\n" |
| 64 | " -v print version and exit\n" |
| 65 | "\n" |
| 66 | " -h print this message and exit\n" |
| 67 | "\n", |
| 68 | NETDATA_VERSION); |
| 69 | } |
| 70 | |
| 71 | void parse_command_line(int argc, char **argv) { |
| 72 | int i; |
| 73 | int freq = 0; |
| 74 | int update_every_found = 0; |
| 75 | for (i = 1; i < argc; i++) { |
| 76 | if (isdigit(*argv[i]) && !update_every_found) { |
| 77 | int n = str2i(argv[i]); |
| 78 | if (n > 0 && n < 86400) { |
| 79 | freq = n; |
| 80 | continue; |
| 81 | } |
| 82 | } else if (strcmp("-v", argv[i]) == 0) { |
| 83 | printf("cups.plugin %s\n", NETDATA_VERSION); |
| 84 | exit(0); |
| 85 | } else if (strcmp("-d", argv[i]) == 0) { |
| 86 | debug = 1; |
| 87 | continue; |
| 88 | } else if (strcmp("-h", argv[i]) == 0) { |
| 89 | print_help(); |
| 90 | exit(0); |
| 91 | } |
| 92 | |
| 93 | print_help(); |
| 94 | exit(1); |
| 95 | } |
| 96 | |
| 97 | if (freq >= netdata_update_every) { |
| 98 | netdata_update_every = freq; |
| 99 | } else if (freq) { |
| 100 | netdata_log_error("update frequency %d seconds is too small for CUPS. Using %d.", freq, netdata_update_every); |
| 101 | } |
| 102 | } |
| 103 | |
| 104 | /* |
| 105 | * 'cupsGetIntegerOption()' - Get an integer option value. |
| 106 | * |
| 107 | * INT_MIN is returned when the option does not exist, is not an integer, or |
| 108 | * exceeds the range of values for the "int" type. |
| 109 | * |
| 110 | * @since CUPS 2.2.4/macOS 10.13@ |
| 111 | */ |
| 112 | |
| 113 | int /* O - Option value or @code INT_MIN@ */ |
| 114 | getIntegerOption( |
| 115 | const char *name, /* I - Name of option */ |
| 116 | int num_options, /* I - Number of options */ |
| 117 | cups_option_t *options) /* I - Options */ |
| 118 | { |
| 119 | const char *value = cupsGetOption(name, num_options, options); |
| 120 | /* String value of option */ |
| 121 | char *ptr; /* Pointer into string value */ |
| 122 | long intvalue; /* Integer value */ |
| 123 | |
| 124 | |
| 125 | if (!value || !*value) |
| 126 | return (INT_MIN); |
| 127 | |
| 128 | intvalue = strtol(value, &ptr, 10); |
| 129 | if (intvalue < INT_MIN || intvalue > INT_MAX || *ptr) |
| 130 | return (INT_MIN); |
| 131 | |
| 132 | return ((int)intvalue); |
| 133 | } |
| 134 | |
| 135 | static int reset_job_metrics(const DICTIONARY_ITEM *item __maybe_unused, void *entry, void *data __maybe_unused) { |
| 136 | struct job_metrics *jm = (struct job_metrics *)entry; |
| 137 | |
| 138 | jm->is_collected = false; |
| 139 | jm->num_held = 0; |
| 140 | jm->num_pending = 0; |
| 141 | jm->num_processing = 0; |
| 142 | jm->size_held = 0; |
| 143 | jm->size_pending = 0; |
| 144 | jm->size_processing = 0; |
| 145 | |
| 146 | return 0; |
| 147 | } |
| 148 | |
| 149 | void send_job_charts_definitions_to_netdata(const char *name, uint32_t job_id, bool obsolete) { |
| 150 | printf("CHART cups.job_num_%s '' 'Active jobs of %s' jobs '%s' cups.destination_job_num stacked %u %i %s\n", |
| 151 | name, name, name, netdata_priority + job_id, netdata_update_every, obsolete?"obsolete":""); |
| 152 | printf("DIMENSION pending '' absolute 1 1\n"); |
| 153 | printf("DIMENSION held '' absolute 1 1\n"); |
| 154 | printf("DIMENSION processing '' absolute 1 1\n"); |
| 155 | |
| 156 | printf("CHART cups.job_size_%s '' 'Active jobs size of %s' KB '%s' cups.destination_job_size stacked %u %i %s\n", |
| 157 | name, name, name, netdata_priority + 1 + job_id, netdata_update_every, obsolete?"obsolete":""); |
| 158 | printf("DIMENSION pending '' absolute 1 1\n"); |
| 159 | printf("DIMENSION held '' absolute 1 1\n"); |
| 160 | printf("DIMENSION processing '' absolute 1 1\n"); |
| 161 | } |
| 162 | |
| 163 | struct job_metrics *get_job_metrics(char *dest) { |
| 164 | struct job_metrics *jm = dictionary_get(dict_dest_job_metrics, dest); |
| 165 | |
| 166 | if (unlikely(!jm)) { |
| 167 | static uint32_t job_id = 0; |
| 168 | struct job_metrics new_job_metrics = { .id = ++job_id }; |
| 169 | jm = dictionary_set(dict_dest_job_metrics, dest, &new_job_metrics, sizeof(struct job_metrics)); |
| 170 | send_job_charts_definitions_to_netdata(dest, jm->id, false); |
| 171 | } |
| 172 | |
| 173 | return jm; |
| 174 | } |
| 175 | |
| 176 | int send_job_metrics_to_netdata(const DICTIONARY_ITEM *item, void *entry, void *data __maybe_unused) { |
| 177 | const char *name = dictionary_acquired_item_name(item); |
| 178 | |
| 179 | struct job_metrics *jm = (struct job_metrics *)entry; |
| 180 | |
| 181 | if (jm->is_collected) { |
| 182 | printf( |
| 183 | "BEGIN cups.job_num_%s\n" |
| 184 | "SET pending = %d\n" |
| 185 | "SET held = %d\n" |
| 186 | "SET processing = %d\n" |
| 187 | "END\n", |
| 188 | name, jm->num_pending, jm->num_held, jm->num_processing); |
| 189 | printf( |
| 190 | "BEGIN cups.job_size_%s\n" |
| 191 | "SET pending = %d\n" |
| 192 | "SET held = %d\n" |
| 193 | "SET processing = %d\n" |
| 194 | "END\n", |
| 195 | name, jm->size_pending, jm->size_held, jm->size_processing); |
| 196 | } |
| 197 | else { |
| 198 | // mark it obsolete |
| 199 | send_job_charts_definitions_to_netdata(name, jm->id, true); |
| 200 | |
| 201 | // delete it |
| 202 | dictionary_del(dict_dest_job_metrics, name); |
| 203 | } |
| 204 | |
| 205 | return 0; |
| 206 | } |
| 207 | |
| 208 | void reset_metrics() { |
| 209 | num_dest_total = 0; |
| 210 | num_dest_accepting_jobs = 0; |
| 211 | num_dest_shared = 0; |
| 212 | |
| 213 | num_dest_idle = 0; |
| 214 | num_dest_printing = 0; |
| 215 | num_dest_stopped = 0; |
| 216 | |
| 217 | reset_job_metrics(NULL, &global_job_metrics, NULL); |
| 218 | dictionary_walkthrough_write(dict_dest_job_metrics, reset_job_metrics, NULL); |
| 219 | } |
| 220 | |
| 221 | int main(int argc, char **argv) { |
| 222 | nd_log_initialize_for_external_plugins("cups.plugin"); |
| 223 | netdata_threads_init_for_external_plugins(0); |
| 224 | |
| 225 | parse_command_line(argc, argv); |
| 226 | |
| 227 | errno_clear(); |
| 228 | |
| 229 | dict_dest_job_metrics = dictionary_create_advanced(DICT_OPTION_SINGLE_THREADED | DICT_OPTION_FIXED_SIZE, NULL, sizeof(struct job_metrics)); |
| 230 | |
| 231 | // ------------------------------------------------------------------------ |
| 232 | // the main loop |
| 233 | |
| 234 | if (debug) |
| 235 | fprintf(stderr, "starting data collection\n"); |
| 236 | |
| 237 | time_t started_t = now_monotonic_sec(); |
| 238 | size_t iteration = 0; |
| 239 | |
| 240 | heartbeat_t hb; |
| 241 | heartbeat_init(&hb, netdata_update_every * USEC_PER_SEC); |
| 242 | for (iteration = 0; 1; iteration++) { |
| 243 | heartbeat_next(&hb); |
| 244 | |
| 245 | if (unlikely(exit_initiated_get())) |
| 246 | break; |
| 247 | |
| 248 | reset_metrics(); |
| 249 | |
| 250 | cups_dest_t *dests; |
| 251 | num_dest_total = cupsGetDests2(http, &dests); |
| 252 | |
| 253 | if(unlikely(num_dest_total == 0)) { |
| 254 | // reconnect to cups to check if the server is down. |
| 255 | httpClose(http); |
| 256 | http = httpConnect2(cupsServer(), ippPort(), NULL, AF_UNSPEC, cupsEncryption(), 0, netdata_update_every * 1000, NULL); |
| 257 | if(http == NULL) { |
| 258 | netdata_log_error("cups daemon is not running. Exiting!"); |
| 259 | exit(1); |
| 260 | } |
| 261 | } |
| 262 | |
| 263 | cups_dest_t *curr_dest = dests; |
| 264 | int counter = 0; |
| 265 | while (counter < num_dest_total) { |
| 266 | if (counter != 0) { |
| 267 | curr_dest++; |
| 268 | } |
| 269 | counter++; |
| 270 | |
| 271 | const char *printer_uri_supported = cupsGetOption("printer-uri-supported", curr_dest->num_options, curr_dest->options); |
| 272 | if (!printer_uri_supported) { |
| 273 | if(debug) |
| 274 | fprintf(stderr, "destination %s discovered, but not yet setup as a local printer", curr_dest->name); |
| 275 | continue; |
| 276 | } |
| 277 | |
| 278 | const char *printer_is_accepting_jobs = cupsGetOption("printer-is-accepting-jobs", curr_dest->num_options, curr_dest->options); |
| 279 | if (printer_is_accepting_jobs && !strcmp(printer_is_accepting_jobs, "true")) { |
| 280 | num_dest_accepting_jobs++; |
| 281 | } |
| 282 | |
| 283 | const char *printer_is_shared = cupsGetOption("printer-is-shared", curr_dest->num_options, curr_dest->options); |
| 284 | if (printer_is_shared && !strcmp(printer_is_shared, "true")) { |
| 285 | num_dest_shared++; |
| 286 | } |
| 287 | |
| 288 | int printer_state = getIntegerOption("printer-state", curr_dest->num_options, curr_dest->options); |
| 289 | switch (printer_state) { |
| 290 | case 3: |
| 291 | num_dest_idle++; |
| 292 | break; |
| 293 | case 4: |
| 294 | num_dest_printing++; |
| 295 | break; |
| 296 | case 5: |
| 297 | num_dest_stopped++; |
| 298 | break; |
| 299 | case INT_MIN: |
| 300 | if(debug) |
| 301 | fprintf(stderr, "printer state is missing for destination %s", curr_dest->name); |
| 302 | break; |
| 303 | default: |
| 304 | netdata_log_error("Unknown printer state (%d) found.", printer_state); |
| 305 | break; |
| 306 | } |
| 307 | |
| 308 | /* |
| 309 | * flag job metrics to print values. |
| 310 | * This is needed to report also destinations with zero active jobs. |
| 311 | */ |
| 312 | struct job_metrics *jm = get_job_metrics(curr_dest->name); |
| 313 | jm->is_collected = true; |
| 314 | } |
| 315 | cupsFreeDests(num_dest_total, dests); |
| 316 | |
| 317 | if (unlikely(exit_initiated_get())) |
| 318 | break; |
| 319 | |
| 320 | cups_job_t *jobs, *curr_job; |
| 321 | int num_jobs = cupsGetJobs2(http, &jobs, NULL, 0, CUPS_WHICHJOBS_ACTIVE); |
| 322 | int i; |
| 323 | for (i = num_jobs, curr_job = jobs; i > 0; i--, curr_job++) { |
| 324 | struct job_metrics *jm = get_job_metrics(curr_job->dest); |
| 325 | jm->is_collected = true; |
| 326 | |
| 327 | switch (curr_job->state) { |
| 328 | case IPP_JOB_PENDING: |
| 329 | jm->num_pending++; |
| 330 | jm->size_pending += curr_job->size; |
| 331 | global_job_metrics.num_pending++; |
| 332 | global_job_metrics.size_pending += curr_job->size; |
| 333 | break; |
| 334 | case IPP_JOB_HELD: |
| 335 | jm->num_held++; |
| 336 | jm->size_held += curr_job->size; |
| 337 | global_job_metrics.num_held++; |
| 338 | global_job_metrics.size_held += curr_job->size; |
| 339 | break; |
| 340 | case IPP_JOB_PROCESSING: |
| 341 | jm->num_processing++; |
| 342 | jm->size_processing += curr_job->size; |
| 343 | global_job_metrics.num_processing++; |
| 344 | global_job_metrics.size_processing += curr_job->size; |
| 345 | break; |
| 346 | default: |
| 347 | netdata_log_error("Unsupported job state (%u) found.", curr_job->state); |
| 348 | break; |
| 349 | } |
| 350 | } |
| 351 | cupsFreeJobs(num_jobs, jobs); |
| 352 | |
| 353 | dictionary_walkthrough_write(dict_dest_job_metrics, send_job_metrics_to_netdata, NULL); |
| 354 | dictionary_garbage_collect(dict_dest_job_metrics); |
| 355 | |
| 356 | static int cups_printer_by_option_created = 0; |
| 357 | if (unlikely(!cups_printer_by_option_created)) |
| 358 | { |
| 359 | cups_printer_by_option_created = 1; |
| 360 | printf("CHART cups.dest_state '' 'Destinations by state' dests overview cups.dests_state stacked 100000 %i\n", netdata_update_every); |
| 361 | printf("DIMENSION idle '' absolute 1 1\n"); |
| 362 | printf("DIMENSION printing '' absolute 1 1\n"); |
| 363 | printf("DIMENSION stopped '' absolute 1 1\n"); |
| 364 | |
| 365 | printf("CHART cups.dest_option '' 'Destinations by option' dests overview cups.dests_option line 100001 %i\n", netdata_update_every); |
| 366 | printf("DIMENSION total '' absolute 1 1\n"); |
| 367 | printf("DIMENSION acceptingjobs '' absolute 1 1\n"); |
| 368 | printf("DIMENSION shared '' absolute 1 1\n"); |
| 369 | |
| 370 | printf("CHART cups.job_num '' 'Active jobs' jobs overview cups.job_num stacked 100002 %i\n", netdata_update_every); |
| 371 | printf("DIMENSION pending '' absolute 1 1\n"); |
| 372 | printf("DIMENSION held '' absolute 1 1\n"); |
| 373 | printf("DIMENSION processing '' absolute 1 1\n"); |
| 374 | |
| 375 | printf("CHART cups.job_size '' 'Active jobs size' KB overview cups.job_size stacked 100003 %i\n", netdata_update_every); |
| 376 | printf("DIMENSION pending '' absolute 1 1\n"); |
| 377 | printf("DIMENSION held '' absolute 1 1\n"); |
| 378 | printf("DIMENSION processing '' absolute 1 1\n"); |
| 379 | } |
| 380 | |
| 381 | printf( |
| 382 | "BEGIN cups.dest_state\n" |
| 383 | "SET idle = %d\n" |
| 384 | "SET printing = %d\n" |
| 385 | "SET stopped = %d\n" |
| 386 | "END\n", |
| 387 | num_dest_idle, num_dest_printing, num_dest_stopped); |
| 388 | printf( |
| 389 | "BEGIN cups.dest_option\n" |
| 390 | "SET total = %d\n" |
| 391 | "SET acceptingjobs = %d\n" |
| 392 | "SET shared = %d\n" |
| 393 | "END\n", |
| 394 | num_dest_total, num_dest_accepting_jobs, num_dest_shared); |
| 395 | printf( |
| 396 | "BEGIN cups.job_num\n" |
| 397 | "SET pending = %d\n" |
| 398 | "SET held = %d\n" |
| 399 | "SET processing = %d\n" |
| 400 | "END\n", |
| 401 | global_job_metrics.num_pending, global_job_metrics.num_held, global_job_metrics.num_processing); |
| 402 | printf( |
| 403 | "BEGIN cups.job_size\n" |
| 404 | "SET pending = %d\n" |
| 405 | "SET held = %d\n" |
| 406 | "SET processing = %d\n" |
| 407 | "END\n", |
| 408 | global_job_metrics.size_pending, global_job_metrics.size_held, global_job_metrics.size_processing); |
| 409 | |
| 410 | fflush(stdout); |
| 411 | |
| 412 | if (unlikely(exit_initiated_get())) |
| 413 | break; |
| 414 | |
| 415 | // restart check (14400 seconds) |
| 416 | if (!now_monotonic_sec() - started_t > 14400) |
| 417 | break; |
| 418 | |
| 419 | fprintf(stdout, "\n"); |
| 420 | fflush(stdout); |
| 421 | if (ferror(stdout) && errno == EPIPE) { |
| 422 | netdata_log_error("error writing to stdout: EPIPE. Exiting..."); |
| 423 | return 1; |
| 424 | } |
| 425 | } |
| 426 | |
| 427 | httpClose(http); |
| 428 | netdata_log_info("CUPS process exiting"); |
| 429 | } |