mpd_common.c (37306B)
1 #define _POSIX_C_SOURCE 200809L 2 3 #include "mpd_common.h" 4 5 #include <errno.h> 6 #include <fcntl.h> 7 #include <limits.h> 8 #include <poll.h> 9 #include <signal.h> 10 #include <spawn.h> 11 #include <stdint.h> 12 #include <stdlib.h> 13 #include <string.h> 14 #include <sys/wait.h> 15 #include <unistd.h> 16 #include <wordexp.h> 17 18 extern char **environ; 19 20 enum helper_state { 21 HELPER_UNRESOLVED, 22 HELPER_READY, 23 HELPER_FAILED, 24 }; 25 26 static enum helper_state profile_helper_state; 27 28 #define HELPER_DEFAULT_TIMEOUT_SECONDS 10U 29 #define HELPER_MAX_TIMEOUT_SECONDS 3600U 30 #define PLAYER_SNAPSHOT_RETRIES 3U 31 32 static bool safe_helper_value(const char *value) { 33 if (*value == '\0') 34 return false; 35 for (const unsigned char *cursor = (const unsigned char *)value; 36 *cursor != '\0'; cursor++) { 37 if (*cursor < 0x20U || *cursor == 0x7fU) 38 return false; 39 } 40 return true; 41 } 42 43 static bool parse_helper_port(const char *value) { 44 if (*value == '\0' || *value == '-') 45 return false; 46 errno = 0; 47 char *end; 48 unsigned long port = strtoul(value, &end, 10); 49 return errno == 0 && *end == '\0' && port > 0UL && port <= 65535UL; 50 } 51 52 static unsigned helper_timeout_seconds(void) { 53 const char *configured = getenv("MPD_CONFIG_HELPER_TIMEOUT"); 54 if (configured == NULL || *configured == '\0') 55 return HELPER_DEFAULT_TIMEOUT_SECONDS; 56 57 size_t timeout; 58 if (!mt_parse_size(configured, &timeout) || timeout == 0U || 59 timeout > HELPER_MAX_TIMEOUT_SECONDS) { 60 fprintf(stderr, 61 "mpd: MPD_CONFIG_HELPER_TIMEOUT must be between 1 and %u " 62 "seconds\n", 63 HELPER_MAX_TIMEOUT_SECONDS); 64 return 0U; 65 } 66 return (unsigned)timeout; 67 } 68 69 static bool monotonic_now(struct timespec *value) { 70 if (clock_gettime(CLOCK_MONOTONIC, value) == 0) 71 return true; 72 perror("mpd: read monotonic clock"); 73 return false; 74 } 75 76 static int milliseconds_until(const struct timespec *deadline) { 77 struct timespec now; 78 if (!monotonic_now(&now)) 79 return -1; 80 time_t seconds = deadline->tv_sec - now.tv_sec; 81 long nanoseconds = deadline->tv_nsec - now.tv_nsec; 82 if (nanoseconds < 0) { 83 seconds--; 84 nanoseconds += 1000000000L; 85 } 86 if (seconds < 0 || (seconds == 0 && nanoseconds == 0)) 87 return 0; 88 if (seconds > (time_t)(INT_MAX / 1000)) 89 return INT_MAX; 90 long milliseconds = nanoseconds / 1000000L; 91 if (nanoseconds % 1000000L != 0) 92 milliseconds++; 93 return (int)(seconds * 1000 + milliseconds); 94 } 95 96 static void terminate_helper(pid_t pid) { 97 if (kill(-pid, SIGKILL) != 0 && errno != ESRCH) 98 perror("mpd: terminate config helper"); 99 int status; 100 while (waitpid(pid, &status, 0) < 0 && errno == EINTR) { 101 } 102 } 103 104 static bool wait_for_helper(pid_t pid, int *status, 105 const struct timespec *deadline) { 106 for (;;) { 107 pid_t result = waitpid(pid, status, WNOHANG); 108 if (result == pid) 109 return true; 110 if (result < 0) { 111 if (errno == EINTR) 112 continue; 113 perror("mpd: wait for config helper"); 114 return false; 115 } 116 int remaining = milliseconds_until(deadline); 117 if (remaining <= 0) 118 return false; 119 int pause = remaining < 20 ? remaining : 20; 120 struct timespec delay = {.tv_sec = 0, .tv_nsec = (long)pause * 1000000L}; 121 while (nanosleep(&delay, &delay) < 0 && errno == EINTR) { 122 } 123 } 124 } 125 126 static bool parse_helper_output(char *output, size_t length, 127 const char *helper) { 128 char *host = NULL; 129 char *port = NULL; 130 bool valid = true; 131 132 if (memchr(output, '\0', length) != NULL) 133 valid = false; 134 output[length] = '\0'; 135 char *cursor = output; 136 while (valid && *cursor != '\0') { 137 char *line = cursor; 138 char *newline = strchr(cursor, '\n'); 139 if (newline == NULL) { 140 cursor += strlen(cursor); 141 } else { 142 *newline = '\0'; 143 cursor = newline + 1; 144 } 145 size_t line_length = strlen(line); 146 while (line_length > 0U && line[line_length - 1U] == '\r') 147 line[--line_length] = '\0'; 148 if (line_length == 0U) 149 continue; 150 151 char **destination = NULL; 152 const char *value = NULL; 153 if (strncmp(line, "MPD_HOST=", 9U) == 0) { 154 destination = &host; 155 value = line + 9; 156 } else if (strncmp(line, "MPD_PORT=", 9U) == 0) { 157 destination = &port; 158 value = line + 9; 159 } else { 160 valid = false; 161 continue; 162 } 163 if (*destination != NULL || !safe_helper_value(value) || 164 (destination == &port && !parse_helper_port(value))) { 165 valid = false; 166 continue; 167 } 168 *destination = strdup(value); 169 if (*destination == NULL) { 170 perror("mpd: store config helper output"); 171 valid = false; 172 } 173 } 174 175 if (host == NULL || port == NULL) 176 valid = false; 177 if (!valid) { 178 fprintf(stderr, 179 "mpd: config helper '%s' must print exactly valid MPD_HOST and " 180 "MPD_PORT assignments\n", 181 helper); 182 } else if (setenv("MPD_HOST", host, 1) != 0 || 183 setenv("MPD_PORT", port, 1) != 0) { 184 perror("mpd: apply config helper output"); 185 valid = false; 186 } 187 188 free(host); 189 free(port); 190 return valid; 191 } 192 193 static bool collect_helper_output(int descriptor, pid_t pid, const char *helper, 194 unsigned timeout_seconds) { 195 int flags = fcntl(descriptor, F_GETFL); 196 if (flags < 0 || fcntl(descriptor, F_SETFL, flags | O_NONBLOCK) != 0) { 197 perror("mpd: configure config helper output"); 198 close(descriptor); 199 terminate_helper(pid); 200 return false; 201 } 202 203 struct timespec deadline; 204 if (!monotonic_now(&deadline)) { 205 close(descriptor); 206 terminate_helper(pid); 207 return false; 208 } 209 deadline.tv_sec += (time_t)timeout_seconds; 210 211 char output[4098U]; 212 size_t length = 0U; 213 bool overflow = false; 214 bool complete = false; 215 while (!complete) { 216 char discard[1024U]; 217 void *destination = overflow ? (void *)discard : (void *)(output + length); 218 size_t available = overflow ? sizeof(discard) : 4097U - length; 219 ssize_t count = read(descriptor, destination, available); 220 if (count > 0) { 221 if (!overflow) { 222 length += (size_t)count; 223 if (length > 4096U) 224 overflow = true; 225 } 226 continue; 227 } 228 if (count == 0) { 229 complete = true; 230 break; 231 } 232 if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) { 233 perror("mpd: read config helper output"); 234 close(descriptor); 235 terminate_helper(pid); 236 return false; 237 } 238 if (errno == EINTR) 239 continue; 240 241 int remaining = milliseconds_until(&deadline); 242 if (remaining <= 0) 243 break; 244 struct pollfd poll_descriptor = {.fd = descriptor, .events = POLLIN}; 245 int result; 246 do { 247 result = poll(&poll_descriptor, 1U, remaining); 248 } while (result < 0 && errno == EINTR); 249 if (result < 0) { 250 perror("mpd: poll config helper output"); 251 close(descriptor); 252 terminate_helper(pid); 253 return false; 254 } 255 if (result == 0) 256 break; 257 } 258 if (close(descriptor) != 0) 259 perror("mpd: close config helper output"); 260 261 int status = 0; 262 if (!complete || !wait_for_helper(pid, &status, &deadline)) { 263 fprintf(stderr, "mpd: config helper '%s' timed out after %u seconds\n", 264 helper, timeout_seconds); 265 terminate_helper(pid); 266 return false; 267 } 268 if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { 269 fprintf(stderr, "mpd: config helper '%s' exited unsuccessfully\n", helper); 270 return false; 271 } 272 if (overflow) { 273 fprintf(stderr, "mpd: config helper '%s' output exceeds 4096 bytes\n", 274 helper); 275 return false; 276 } 277 return parse_helper_output(output, length, helper); 278 } 279 280 static bool run_profile_helper(const char *helper) { 281 unsigned timeout_seconds = helper_timeout_seconds(); 282 if (timeout_seconds == 0U) 283 return false; 284 int descriptors[2]; 285 if (pipe(descriptors) != 0) { 286 perror("mpd: create config helper pipe"); 287 return false; 288 } 289 290 posix_spawn_file_actions_t actions; 291 posix_spawnattr_t attributes; 292 int error = posix_spawn_file_actions_init(&actions); 293 bool actions_initialized = error == 0; 294 bool attributes_initialized = false; 295 if (error == 0) 296 error = posix_spawn_file_actions_addclose(&actions, descriptors[0]); 297 if (error == 0) 298 error = posix_spawn_file_actions_adddup2(&actions, descriptors[1], 299 STDOUT_FILENO); 300 if (error == 0) 301 error = posix_spawn_file_actions_addclose(&actions, descriptors[1]); 302 if (error == 0) { 303 error = posix_spawnattr_init(&attributes); 304 attributes_initialized = error == 0; 305 } 306 if (error == 0) 307 error = posix_spawnattr_setflags(&attributes, POSIX_SPAWN_SETPGROUP); 308 if (error == 0) 309 error = posix_spawnattr_setpgroup(&attributes, 0); 310 if (error != 0) { 311 fprintf(stderr, "mpd: prepare config helper: %s\n", strerror(error)); 312 if (actions_initialized) { 313 int destroy_error = posix_spawn_file_actions_destroy(&actions); 314 if (destroy_error != 0) 315 fprintf(stderr, "mpd: release config helper actions: %s\n", 316 strerror(destroy_error)); 317 } 318 if (attributes_initialized) { 319 int destroy_error = posix_spawnattr_destroy(&attributes); 320 if (destroy_error != 0) 321 fprintf(stderr, "mpd: release config helper attributes: %s\n", 322 strerror(destroy_error)); 323 } 324 close(descriptors[0]); 325 close(descriptors[1]); 326 return false; 327 } 328 329 pid_t pid; 330 char *arguments[] = {(char *)helper, NULL}; 331 error = posix_spawn(&pid, helper, &actions, &attributes, arguments, environ); 332 int destroy_error = posix_spawn_file_actions_destroy(&actions); 333 if (destroy_error != 0) 334 fprintf(stderr, "mpd: release config helper actions: %s\n", 335 strerror(destroy_error)); 336 destroy_error = posix_spawnattr_destroy(&attributes); 337 if (destroy_error != 0) 338 fprintf(stderr, "mpd: release config helper attributes: %s\n", 339 strerror(destroy_error)); 340 if (error != 0) { 341 fprintf(stderr, "mpd: start config helper '%s': %s\n", helper, 342 strerror(error)); 343 close(descriptors[0]); 344 close(descriptors[1]); 345 return false; 346 } 347 348 close(descriptors[1]); 349 return collect_helper_output(descriptors[0], pid, helper, timeout_seconds); 350 } 351 352 static bool resolve_profile_helper(void) { 353 if (profile_helper_state != HELPER_UNRESOLVED) 354 return profile_helper_state == HELPER_READY; 355 356 const char *configured = getenv("MPD_CONFIG_HELPER"); 357 if (configured != NULL && 358 (*configured == '\0' || strcmp(configured, "off") == 0)) { 359 profile_helper_state = HELPER_READY; 360 return true; 361 } 362 363 char *default_helper = NULL; 364 const char *helper = configured; 365 if (helper == NULL) { 366 const char *home = getenv("HOME"); 367 if (home == NULL || *home == '\0') { 368 profile_helper_state = HELPER_READY; 369 return true; 370 } 371 size_t home_length = strlen(home); 372 if (home_length > SIZE_MAX - sizeof("/bin/amen")) { 373 fprintf(stderr, "mpd: HOME is too long\n"); 374 profile_helper_state = HELPER_FAILED; 375 return false; 376 } 377 size_t length = home_length + sizeof("/bin/amen"); 378 default_helper = malloc(length); 379 if (default_helper == NULL) { 380 perror("mpd: config helper path"); 381 profile_helper_state = HELPER_FAILED; 382 return false; 383 } 384 snprintf(default_helper, length, "%s/bin/amen", home); 385 if (access(default_helper, X_OK) != 0) { 386 free(default_helper); 387 profile_helper_state = HELPER_READY; 388 return true; 389 } 390 helper = default_helper; 391 } 392 393 bool valid = run_profile_helper(helper); 394 free(default_helper); 395 profile_helper_state = valid ? HELPER_READY : HELPER_FAILED; 396 return valid; 397 } 398 399 struct mpd_connection *mt_connect_to(const char *host, unsigned port) { 400 if (!resolve_profile_helper()) 401 return NULL; 402 struct mpd_connection *connection = mpd_connection_new(host, port, 0); 403 if (connection == NULL) { 404 fprintf(stderr, "mpd: out of memory while creating connection\n"); 405 return NULL; 406 } 407 408 if (mpd_connection_get_error(connection) != MPD_ERROR_SUCCESS) { 409 mt_report_error(connection, "connect"); 410 mpd_connection_free(connection); 411 return NULL; 412 } 413 return connection; 414 } 415 416 struct mpd_connection *mt_connect(void) { return mt_connect_to(NULL, 0U); } 417 418 void mt_report_error(const struct mpd_connection *connection, 419 const char *context) { 420 const char *message = connection == NULL 421 ? "unknown MPD error" 422 : mpd_connection_get_error_message(connection); 423 fprintf(stderr, "mpd: %s: %s\n", context, message); 424 } 425 426 void mt_strvec_init(struct mt_strvec *vec) { 427 vec->items = NULL; 428 vec->len = 0; 429 vec->cap = 0; 430 } 431 432 void mt_strvec_clear(struct mt_strvec *vec) { 433 for (size_t i = 0; i < vec->len; i++) 434 free(vec->items[i]); 435 free(vec->items); 436 mt_strvec_init(vec); 437 } 438 439 bool mt_strvec_push_n(struct mt_strvec *vec, const char *value, size_t length) { 440 if (vec->len == vec->cap) { 441 size_t cap = vec->cap == 0 ? 16U : vec->cap * 2U; 442 if (cap < vec->cap || cap > SIZE_MAX / sizeof(*vec->items)) { 443 errno = ENOMEM; 444 return false; 445 } 446 char **items = realloc(vec->items, cap * sizeof(*items)); 447 if (items == NULL) 448 return false; 449 vec->items = items; 450 vec->cap = cap; 451 } 452 453 if (length == SIZE_MAX) { 454 errno = ENOMEM; 455 return false; 456 } 457 char *copy = malloc(length + 1U); 458 if (copy == NULL) 459 return false; 460 memcpy(copy, value, length); 461 copy[length] = '\0'; 462 vec->items[vec->len] = copy; 463 vec->len++; 464 return true; 465 } 466 467 bool mt_strvec_push(struct mt_strvec *vec, const char *value) { 468 return mt_strvec_push_n(vec, value, strlen(value)); 469 } 470 471 bool mt_read_lines(FILE *input, struct mt_strvec *lines) { 472 char *line = NULL; 473 size_t capacity = 0; 474 ssize_t length; 475 476 while ((length = getline(&line, &capacity, input)) >= 0) { 477 while (length > 0 && (line[(size_t)length - 1U] == '\n' || 478 line[(size_t)length - 1U] == '\r')) 479 line[--length] = '\0'; 480 if (length == 0 || line[0] == '#') 481 continue; 482 if (!mt_strvec_push(lines, line)) { 483 perror("mpd: storing input line"); 484 free(line); 485 return false; 486 } 487 } 488 489 free(line); 490 if (ferror(input)) { 491 perror("mpd: reading input"); 492 return false; 493 } 494 return true; 495 } 496 497 FILE *mt_open_input(const char *path, bool *must_close) { 498 if (path == NULL || strcmp(path, "-") == 0) { 499 *must_close = false; 500 return stdin; 501 } 502 503 FILE *input = fopen(path, "r"); 504 if (input == NULL) 505 perror(path); 506 *must_close = input != NULL; 507 return input; 508 } 509 510 static bool finish_list(struct mpd_connection *connection, 511 const char *context) { 512 if (!mpd_command_list_end(connection) || !mpd_response_finish(connection)) { 513 mt_report_error(connection, context); 514 return false; 515 } 516 return true; 517 } 518 519 bool mt_add_queue(struct mpd_connection *connection, 520 const struct mt_strvec *tracks, bool verbose) { 521 for (size_t start = 0; start < tracks->len; start += MPD_TOOLS_BATCH_SIZE) { 522 size_t end = start + MPD_TOOLS_BATCH_SIZE; 523 if (end > tracks->len) 524 end = tracks->len; 525 if (!mpd_command_list_begin(connection, false)) { 526 mt_report_error(connection, "begin add command list"); 527 return false; 528 } 529 for (size_t i = start; i < end; i++) { 530 if (!mpd_send_add(connection, tracks->items[i])) { 531 mt_report_error(connection, "queue add"); 532 return false; 533 } 534 } 535 if (!finish_list(connection, "add tracks")) { 536 fprintf(stderr, "mpd: failed input batch %zu..%zu\n", start + 1U, end); 537 return false; 538 } 539 if (verbose) { 540 for (size_t i = start; i < end; i++) 541 printf("Added: %s\n", tracks->items[i]); 542 } 543 } 544 return true; 545 } 546 547 bool mt_insert_queue(struct mpd_connection *connection, 548 const struct mt_strvec *tracks, unsigned position, 549 bool verbose) { 550 size_t available = (size_t)UINT_MAX - (size_t)position + 1U; 551 if (tracks->len > available) { 552 fprintf(stderr, "mpd: too many tracks for the requested queue position\n"); 553 return false; 554 } 555 size_t inserted = 0; 556 while (inserted < tracks->len) { 557 size_t end = inserted + MPD_TOOLS_BATCH_SIZE; 558 if (end > tracks->len) 559 end = tracks->len; 560 if (!mpd_command_list_begin(connection, false)) { 561 mt_report_error(connection, "begin insert command list"); 562 return false; 563 } 564 for (size_t i = inserted; i < end; i++) { 565 if (i - inserted > UINT_MAX - position || 566 !mpd_send_add_id_to(connection, tracks->items[i], 567 position + (unsigned)(i - inserted))) { 568 mt_report_error(connection, "queue insert"); 569 return false; 570 } 571 } 572 if (!finish_list(connection, "insert tracks")) 573 return false; 574 if (end < tracks->len) 575 position += (unsigned)(end - inserted); 576 if (verbose) { 577 for (size_t i = inserted; i < end; i++) 578 printf("Inserted: %s\n", tracks->items[i]); 579 } 580 inserted = end; 581 } 582 return true; 583 } 584 585 bool mt_add_playlist(struct mpd_connection *connection, const char *playlist, 586 const struct mt_strvec *tracks, bool verbose) { 587 for (size_t start = 0; start < tracks->len; start += MPD_TOOLS_BATCH_SIZE) { 588 size_t end = start + MPD_TOOLS_BATCH_SIZE; 589 if (end > tracks->len) 590 end = tracks->len; 591 if (!mpd_command_list_begin(connection, false)) { 592 mt_report_error(connection, "begin playlist command list"); 593 return false; 594 } 595 for (size_t i = start; i < end; i++) { 596 if (!mpd_send_playlist_add(connection, playlist, tracks->items[i])) { 597 mt_report_error(connection, "playlist add"); 598 return false; 599 } 600 } 601 if (!finish_list(connection, "add playlist tracks")) 602 return false; 603 if (verbose) { 604 for (size_t i = start; i < end; i++) 605 printf("Added to %s: %s\n", playlist, tracks->items[i]); 606 } 607 } 608 return true; 609 } 610 611 static bool add_stream(struct mpd_connection *connection, const char *playlist, 612 FILE *input, bool verbose, size_t *count) { 613 struct mt_strvec batch; 614 mt_strvec_init(&batch); 615 char *line = NULL; 616 size_t capacity = 0U; 617 bool ok = true; 618 *count = 0U; 619 620 while (ok) { 621 ssize_t length = getline(&line, &capacity, input); 622 if (length < 0) 623 break; 624 while (length > 0 && (line[(size_t)length - 1U] == '\n' || 625 line[(size_t)length - 1U] == '\r')) 626 line[--length] = '\0'; 627 if (length == 0 || line[0] == '#') 628 continue; 629 if (!mt_strvec_push(&batch, line)) { 630 perror("mpd: storing input line"); 631 ok = false; 632 } 633 if (ok && batch.len == MPD_TOOLS_BATCH_SIZE) { 634 ok = playlist == NULL 635 ? mt_add_queue(connection, &batch, verbose) 636 : mt_add_playlist(connection, playlist, &batch, verbose); 637 if (ok) 638 *count += batch.len; 639 mt_strvec_clear(&batch); 640 mt_strvec_init(&batch); 641 } 642 } 643 if (ok && ferror(input)) { 644 perror("mpd: reading input"); 645 ok = false; 646 } 647 if (ok && batch.len > 0U) { 648 ok = playlist == NULL 649 ? mt_add_queue(connection, &batch, verbose) 650 : mt_add_playlist(connection, playlist, &batch, verbose); 651 if (ok) 652 *count += batch.len; 653 } 654 free(line); 655 mt_strvec_clear(&batch); 656 return ok; 657 } 658 659 bool mt_add_queue_stream(struct mpd_connection *connection, FILE *input, 660 bool verbose, size_t *count) { 661 return add_stream(connection, NULL, input, verbose, count); 662 } 663 664 bool mt_add_playlist_stream(struct mpd_connection *connection, 665 const char *playlist, FILE *input, bool verbose, 666 size_t *count) { 667 return add_stream(connection, playlist, input, verbose, count); 668 } 669 670 void mt_player_state_clear(struct mt_player_state *state) { 671 free(state->current_uri); 672 memset(state, 0, sizeof(*state)); 673 state->song_pos = UINT_MAX; 674 state->song_id = UINT_MAX; 675 } 676 677 bool mt_get_player_state(struct mpd_connection *connection, 678 struct mt_player_state *state) { 679 for (unsigned attempt = 0U; attempt < PLAYER_SNAPSHOT_RETRIES; attempt++) { 680 memset(state, 0, sizeof(*state)); 681 state->song_pos = UINT_MAX; 682 state->song_id = UINT_MAX; 683 684 struct mpd_status *status = mpd_run_status(connection); 685 if (status == NULL) { 686 mt_report_error(connection, "get status"); 687 return false; 688 } 689 state->state = mpd_status_get_state(status); 690 state->queue_version = mpd_status_get_queue_version(status); 691 state->queue_length = mpd_status_get_queue_length(status); 692 int song_pos = mpd_status_get_song_pos(status); 693 int song_id = mpd_status_get_song_id(status); 694 if (song_pos >= 0) 695 state->song_pos = (unsigned)song_pos; 696 if (song_id >= 0) 697 state->song_id = (unsigned)song_id; 698 mpd_status_free(status); 699 700 if (state->song_id == UINT_MAX) 701 return true; 702 703 struct mpd_song *song = mpd_run_current_song(connection); 704 if (song == NULL) { 705 if (mpd_connection_get_error(connection) != MPD_ERROR_SUCCESS) { 706 mt_report_error(connection, "get current song"); 707 return false; 708 } 709 continue; 710 } 711 bool consistent = mpd_song_get_id(song) == state->song_id && 712 mpd_song_get_pos(song) == state->song_pos; 713 const char *uri = mpd_song_get_uri(song); 714 bool has_uri = uri != NULL; 715 if (consistent && has_uri) 716 state->current_uri = strdup(uri); 717 mpd_song_free(song); 718 if (consistent && has_uri && state->current_uri == NULL) { 719 perror("mpd: current song"); 720 return false; 721 } 722 if (consistent) 723 return true; 724 } 725 fprintf(stderr, "mpd: player changed repeatedly while reading its state\n"); 726 return false; 727 } 728 729 static bool receive_song_uris(struct mpd_connection *connection, 730 struct mt_strvec *tracks) { 731 struct mpd_entity *entity; 732 while ((entity = mpd_recv_entity(connection)) != NULL) { 733 if (mpd_entity_get_type(entity) == MPD_ENTITY_TYPE_SONG) { 734 const struct mpd_song *song = mpd_entity_get_song(entity); 735 const char *uri = mpd_song_get_uri(song); 736 if (uri != NULL && !mt_strvec_push(tracks, uri)) { 737 mpd_entity_free(entity); 738 perror("mpd: storing queue"); 739 return false; 740 } 741 } 742 mpd_entity_free(entity); 743 } 744 if (!mpd_response_finish(connection)) { 745 mt_report_error(connection, "receive song list"); 746 return false; 747 } 748 return true; 749 } 750 751 bool mt_read_queue(struct mpd_connection *connection, 752 struct mt_strvec *tracks) { 753 if (!mpd_send_list_queue_meta(connection)) { 754 mt_report_error(connection, "list queue"); 755 return false; 756 } 757 return receive_song_uris(connection, tracks); 758 } 759 760 bool mt_read_playlist(struct mpd_connection *connection, const char *playlist, 761 struct mt_strvec *tracks) { 762 if (!mpd_send_list_playlist(connection, playlist)) { 763 mt_report_error(connection, "list stored playlist"); 764 return false; 765 } 766 return receive_song_uris(connection, tracks); 767 } 768 769 enum mt_state_match mt_player_state_is(struct mpd_connection *connection, 770 unsigned expected_version, 771 unsigned expected_song_id) { 772 struct mpd_status *status = mpd_run_status(connection); 773 if (status == NULL) { 774 mt_report_error(connection, "check player state"); 775 return MT_STATE_ERROR; 776 } 777 int song_id = mpd_status_get_song_id(status); 778 bool song_matches = 779 expected_song_id == UINT_MAX 780 ? song_id < 0 781 : song_id >= 0 && (unsigned)song_id == expected_song_id; 782 bool matches = 783 mpd_status_get_queue_version(status) == expected_version && song_matches; 784 mpd_status_free(status); 785 return matches ? MT_STATE_MATCH : MT_STATE_CHANGED; 786 } 787 788 static size_t first_uri(const struct mt_strvec *tracks, const char *uri) { 789 if (uri == NULL) 790 return SIZE_MAX; 791 for (size_t i = 0; i < tracks->len; i++) { 792 if (strcmp(tracks->items[i], uri) == 0) 793 return i; 794 } 795 return SIZE_MAX; 796 } 797 798 static bool add_queue_skipping(struct mpd_connection *connection, 799 const struct mt_strvec *tracks, size_t skip) { 800 size_t index = 0U; 801 while (index < tracks->len) { 802 while (index < tracks->len && index == skip) 803 index++; 804 if (index == tracks->len) 805 break; 806 807 if (!mpd_command_list_begin(connection, false)) { 808 mt_report_error(connection, "begin replacement add list"); 809 return false; 810 } 811 size_t sent = 0U; 812 while (index < tracks->len && sent < MPD_TOOLS_BATCH_SIZE) { 813 if (index != skip) { 814 if (!mpd_send_add(connection, tracks->items[index])) { 815 mt_report_error(connection, "add replacement track"); 816 return false; 817 } 818 sent++; 819 } 820 index++; 821 } 822 if (!finish_list(connection, "add replacement tracks")) 823 return false; 824 } 825 return true; 826 } 827 828 static bool restore_queue_snapshot(const struct mt_strvec *tracks, 829 const struct mt_player_state *state) { 830 struct mpd_connection *connection = mt_connect(); 831 if (connection == NULL) 832 return false; 833 bool ok = mpd_run_clear(connection); 834 if (!ok) 835 mt_report_error(connection, "rollback: clear partial queue"); 836 if (ok) 837 ok = mt_add_queue(connection, tracks, false); 838 if (ok && tracks->len > 0U && state->song_pos < tracks->len && 839 (state->state == MPD_STATE_PLAY || state->state == MPD_STATE_PAUSE)) { 840 ok = mpd_run_play_pos(connection, state->song_pos); 841 if (!ok) { 842 mt_report_error(connection, "rollback: restore playback position"); 843 } else if (state->state == MPD_STATE_PAUSE) { 844 ok = mpd_run_pause(connection, true); 845 if (!ok) 846 mt_report_error(connection, "rollback: restore paused state"); 847 } 848 } 849 mpd_connection_free(connection); 850 return ok; 851 } 852 853 bool mt_replace_queue(struct mpd_connection *connection, 854 const struct mt_strvec *tracks, unsigned expected_version, 855 bool force) { 856 struct mt_player_state state; 857 struct mt_strvec original; 858 mt_strvec_init(&original); 859 bool snapshot_ready = false; 860 bool state_changed = false; 861 for (unsigned attempt = 0U; attempt < PLAYER_SNAPSHOT_RETRIES; attempt++) { 862 if (!mt_get_player_state(connection, &state)) 863 break; 864 if (!force && expected_version != UINT_MAX && 865 state.queue_version != expected_version) { 866 fprintf(stderr, "mpd: queue changed while it was being edited; use " 867 "--force to replace it anyway\n"); 868 mt_player_state_clear(&state); 869 break; 870 } 871 if (!mt_read_queue(connection, &original)) { 872 mt_player_state_clear(&state); 873 break; 874 } 875 enum mt_state_match match = 876 mt_player_state_is(connection, state.queue_version, state.song_id); 877 if (match == MT_STATE_MATCH) { 878 snapshot_ready = true; 879 break; 880 } 881 if (match == MT_STATE_CHANGED) 882 state_changed = true; 883 mt_player_state_clear(&state); 884 mt_strvec_clear(&original); 885 mt_strvec_init(&original); 886 if (match == MT_STATE_ERROR) 887 break; 888 } 889 if (!snapshot_ready) { 890 if (state_changed) 891 fprintf(stderr, "mpd: player changed repeatedly before replacement\n"); 892 mt_strvec_clear(&original); 893 return false; 894 } 895 896 size_t keep = first_uri(tracks, state.current_uri); 897 bool can_keep = keep != SIZE_MAX && state.song_pos != UINT_MAX && 898 state.song_pos < state.queue_length; 899 bool ok = true; 900 bool mutation_started = false; 901 902 if (can_keep) { 903 bool needs_trim = 904 state.song_pos > 0U || state.song_pos + 1U < state.queue_length; 905 if (needs_trim) { 906 mutation_started = true; 907 if (!mpd_command_list_begin(connection, false)) { 908 mt_report_error(connection, "begin queue trim"); 909 ok = false; 910 } 911 if (ok && state.song_pos + 1U < state.queue_length && 912 !mpd_send_delete_range(connection, state.song_pos + 1U, 913 state.queue_length)) { 914 mt_report_error(connection, "delete queue suffix"); 915 ok = false; 916 } 917 if (ok && state.song_pos > 0U && 918 !mpd_send_delete_range(connection, 0U, state.song_pos)) { 919 mt_report_error(connection, "delete queue prefix"); 920 ok = false; 921 } 922 if (ok) 923 ok = finish_list(connection, "trim queue around current song"); 924 } 925 if (ok) { 926 mutation_started = true; 927 ok = add_queue_skipping(connection, tracks, keep); 928 } 929 if (ok && keep > 0U && !mpd_run_move(connection, 0U, (unsigned)keep)) { 930 mt_report_error(connection, "restore current queue position"); 931 ok = false; 932 } 933 } else { 934 mutation_started = true; 935 if (!mpd_run_clear(connection)) { 936 mt_report_error(connection, "clear queue"); 937 ok = false; 938 } 939 if (ok) 940 ok = mt_add_queue(connection, tracks, false); 941 if (ok && tracks->len > 0U && 942 (state.state == MPD_STATE_PLAY || state.state == MPD_STATE_PAUSE)) { 943 if (!mpd_run_play_pos(connection, 0U)) { 944 mt_report_error(connection, "restart playback"); 945 ok = false; 946 } else if (state.state == MPD_STATE_PAUSE && 947 !mpd_run_pause(connection, true)) { 948 mt_report_error(connection, "restore paused state"); 949 ok = false; 950 } 951 } 952 } 953 954 if (!ok && mutation_started) { 955 fprintf(stderr, "mpd: replacement failed; restoring the previous queue\n"); 956 if (!restore_queue_snapshot(&original, &state)) 957 fprintf(stderr, "mpd: rollback failed; the queue may be partial\n"); 958 } 959 960 mt_player_state_clear(&state); 961 mt_strvec_clear(&original); 962 return ok; 963 } 964 965 static bool same_tracks(const struct mt_strvec *left, 966 const struct mt_strvec *right) { 967 if (left->len != right->len) 968 return false; 969 for (size_t i = 0U; i < left->len; i++) { 970 if (strcmp(left->items[i], right->items[i]) != 0) 971 return false; 972 } 973 return true; 974 } 975 976 static unsigned long playlist_name_sequence; 977 978 static void temporary_playlist_name(char *buffer, size_t size, 979 const char *purpose) { 980 playlist_name_sequence++; 981 struct timespec now = {0}; 982 if (clock_gettime(CLOCK_REALTIME, &now) != 0) 983 now.tv_nsec = (long)playlist_name_sequence; 984 snprintf(buffer, size, "mpd-tools-%ld-%lld-%ld-%lu-%s", (long)getpid(), 985 (long long)now.tv_sec, now.tv_nsec, playlist_name_sequence, purpose); 986 } 987 988 static void remove_temporary_playlist(const char *playlist) { 989 struct mpd_connection *cleanup = mt_connect(); 990 if (cleanup == NULL) 991 return; 992 if (!mpd_run_rm(cleanup, playlist) && 993 mpd_connection_get_error(cleanup) != MPD_ERROR_SERVER) 994 mt_report_error(cleanup, "remove temporary playlist"); 995 mpd_connection_free(cleanup); 996 } 997 998 bool mt_replace_playlist(struct mpd_connection *connection, 999 const char *playlist, const struct mt_strvec *tracks, 1000 const struct mt_strvec *expected, bool force) { 1001 if (tracks->len == 0U) { 1002 if (!force) { 1003 struct mt_strvec current; 1004 mt_strvec_init(¤t); 1005 bool read_ok = mt_read_playlist(connection, playlist, ¤t); 1006 bool unchanged = read_ok && same_tracks(expected, ¤t); 1007 mt_strvec_clear(¤t); 1008 if (!read_ok) 1009 return false; 1010 if (!unchanged) { 1011 fprintf(stderr, "mpd: stored playlist changed while it was being " 1012 "edited; use --force to replace it anyway\n"); 1013 return false; 1014 } 1015 } 1016 if (!mpd_run_playlist_clear(connection, playlist)) { 1017 mt_report_error(connection, "clear stored playlist"); 1018 return false; 1019 } 1020 return true; 1021 } 1022 1023 char staged[128U]; 1024 char backup[128U]; 1025 temporary_playlist_name(staged, sizeof(staged), "new"); 1026 temporary_playlist_name(backup, sizeof(backup), "old"); 1027 if (!mt_add_playlist(connection, staged, tracks, false)) { 1028 remove_temporary_playlist(staged); 1029 return false; 1030 } 1031 1032 if (!force) { 1033 struct mt_strvec current; 1034 mt_strvec_init(¤t); 1035 bool read_ok = mt_read_playlist(connection, playlist, ¤t); 1036 bool unchanged = read_ok && same_tracks(expected, ¤t); 1037 mt_strvec_clear(¤t); 1038 if (!read_ok) { 1039 remove_temporary_playlist(staged); 1040 return false; 1041 } 1042 if (!unchanged) { 1043 fprintf(stderr, "mpd: stored playlist changed while it was being " 1044 "edited; use --force to replace it anyway\n"); 1045 remove_temporary_playlist(staged); 1046 return false; 1047 } 1048 } 1049 1050 if (!mpd_run_rename(connection, playlist, backup)) { 1051 mt_report_error(connection, "move old stored playlist aside"); 1052 remove_temporary_playlist(staged); 1053 return false; 1054 } 1055 if (!mpd_run_rename(connection, staged, playlist)) { 1056 mt_report_error(connection, "install staged stored playlist"); 1057 struct mpd_connection *rollback = mt_connect(); 1058 if (rollback == NULL || !mpd_run_rename(rollback, backup, playlist)) 1059 fprintf(stderr, 1060 "mpd: playlist rollback failed; '%s' contains the " 1061 "backup\n", 1062 backup); 1063 if (rollback != NULL) 1064 mpd_connection_free(rollback); 1065 remove_temporary_playlist(staged); 1066 return false; 1067 } 1068 if (!mpd_run_rm(connection, backup)) { 1069 mt_report_error(connection, "remove stored playlist backup"); 1070 fprintf(stderr, "mpd: replacement succeeded, but backup '%s' remains\n", 1071 backup); 1072 return false; 1073 } 1074 return true; 1075 } 1076 1077 static bool run_editor(const char *path) { 1078 const char *editor = getenv("VISUAL"); 1079 if (editor == NULL || *editor == '\0') 1080 editor = getenv("EDITOR"); 1081 if (editor == NULL || *editor == '\0') 1082 editor = "vi"; 1083 1084 wordexp_t words; 1085 int result = wordexp(editor, &words, WRDE_NOCMD); 1086 if (result != 0 || words.we_wordc == 0U) { 1087 fprintf(stderr, "mpd: invalid editor command\n"); 1088 if (result == 0) 1089 wordfree(&words); 1090 return false; 1091 } 1092 1093 char **args = calloc(words.we_wordc + 2U, sizeof(*args)); 1094 if (args == NULL) { 1095 perror("mpd: editor arguments"); 1096 wordfree(&words); 1097 return false; 1098 } 1099 for (size_t i = 0; i < words.we_wordc; i++) 1100 args[i] = words.we_wordv[i]; 1101 args[words.we_wordc] = (char *)path; 1102 1103 pid_t pid = fork(); 1104 if (pid == 0) { 1105 execvp(args[0], args); 1106 perror(args[0]); 1107 _exit(127); 1108 } 1109 if (pid < 0) { 1110 perror("mpd: fork editor"); 1111 free(args); 1112 wordfree(&words); 1113 return false; 1114 } 1115 1116 int status; 1117 while (waitpid(pid, &status, 0) < 0) { 1118 if (errno != EINTR) { 1119 perror("mpd: wait for editor"); 1120 free(args); 1121 wordfree(&words); 1122 return false; 1123 } 1124 } 1125 free(args); 1126 wordfree(&words); 1127 if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { 1128 fprintf(stderr, "mpd: editor exited unsuccessfully\n"); 1129 return false; 1130 } 1131 return true; 1132 } 1133 1134 static char *editor_file_template(void) { 1135 const char *directory = getenv("TMPDIR"); 1136 if (directory == NULL || *directory == '\0') 1137 directory = "/tmp"; 1138 size_t directory_length = strlen(directory); 1139 bool needs_slash = directory[directory_length - 1U] != '/'; 1140 const char suffix[] = "mpd_tools_XXXXXX"; 1141 if (directory_length > SIZE_MAX - sizeof(suffix) - (size_t)needs_slash) { 1142 errno = ENAMETOOLONG; 1143 return NULL; 1144 } 1145 size_t length = directory_length + (size_t)needs_slash + sizeof(suffix); 1146 char *path = malloc(length); 1147 if (path == NULL) 1148 return NULL; 1149 snprintf(path, length, "%s%s%s", directory, needs_slash ? "/" : "", suffix); 1150 return path; 1151 } 1152 1153 bool mt_edit_lines(struct mt_strvec *lines) { 1154 char *path = editor_file_template(); 1155 if (path == NULL) { 1156 perror("mpd: create editor file path"); 1157 return false; 1158 } 1159 int fd = mkstemp(path); 1160 if (fd < 0) { 1161 perror("mpd: create editor file"); 1162 free(path); 1163 return false; 1164 } 1165 FILE *file = fdopen(fd, "w"); 1166 if (file == NULL) { 1167 perror("mpd: open editor file"); 1168 close(fd); 1169 unlink(path); 1170 free(path); 1171 return false; 1172 } 1173 bool ok = true; 1174 for (size_t i = 0; i < lines->len; i++) { 1175 if (fprintf(file, "%s\n", lines->items[i]) < 0) { 1176 perror("mpd: write editor file"); 1177 ok = false; 1178 break; 1179 } 1180 } 1181 if (fclose(file) != 0) { 1182 perror("mpd: close editor file"); 1183 ok = false; 1184 } 1185 1186 if (ok) 1187 ok = run_editor(path); 1188 if (ok) { 1189 int read_fd = open(path, O_RDONLY | O_NOFOLLOW); 1190 if (read_fd < 0) { 1191 perror("mpd: reopen editor file"); 1192 ok = false; 1193 } else { 1194 file = fdopen(read_fd, "r"); 1195 if (file == NULL) { 1196 perror("mpd: read editor file"); 1197 close(read_fd); 1198 ok = false; 1199 } else { 1200 struct mt_strvec edited; 1201 mt_strvec_init(&edited); 1202 ok = mt_read_lines(file, &edited); 1203 if (fclose(file) != 0) { 1204 perror("mpd: close edited file"); 1205 ok = false; 1206 } 1207 if (ok) { 1208 mt_strvec_clear(lines); 1209 *lines = edited; 1210 } else { 1211 mt_strvec_clear(&edited); 1212 } 1213 } 1214 } 1215 } 1216 1217 if (unlink(path) != 0) 1218 perror("mpd: remove editor file"); 1219 free(path); 1220 return ok; 1221 } 1222 1223 bool mt_parse_size(const char *text, size_t *value) { 1224 if (text == NULL || *text == '\0' || *text == '-') 1225 return false; 1226 errno = 0; 1227 char *end; 1228 unsigned long long parsed = strtoull(text, &end, 10); 1229 if (errno != 0 || *end != '\0' || parsed > SIZE_MAX) 1230 return false; 1231 *value = (size_t)parsed; 1232 return true; 1233 }