From 84c11577fab2d466c9a3ae7e2fe2ac9ea5688b09 Mon Sep 17 00:00:00 2001 From: simon Date: Mon, 29 Jun 2026 12:26:34 +0200 Subject: [PATCH] fix(standalone): hot-patch UUID parser in memory to resolve empty UUID startup crash loop --- UUID_PATCH_DETAILS.md | 38 ++++ docker-compose.standalone.yml | 9 +- .../src/db_interpose_bind/text_blob_binds.rs | 17 +- .../src/db_interpose_column/text_accessor.rs | 21 ++- .../plex-pg-core/src/db_interpose_metadata.rs | 1 + .../db_interpose_metadata/connection_state.rs | 41 +++-- rust/plex-pg-core/src/exception_what.rs | 4 +- rust/plex-pg-core/src/runtime_linux.rs | 174 ++++++++++++++++++ scripts/docker-entrypoint.sh | 129 ++++++++++++- scripts/standalone-entrypoint.sh | 130 ++++++++++++- 10 files changed, 533 insertions(+), 31 deletions(-) create mode 100644 UUID_PATCH_DETAILS.md diff --git a/UUID_PATCH_DETAILS.md b/UUID_PATCH_DETAILS.md new file mode 100644 index 00000000..fdc3a94a --- /dev/null +++ b/UUID_PATCH_DETAILS.md @@ -0,0 +1,38 @@ +# Correction du crash UUID au démarrage (Hot-patching en mémoire) + +Ce document décrit l'implémentation mise en place pour résoudre le plantage au démarrage de Plex Media Server sous PostgreSQL (exception fatale `std::domain_error: Invalid uuid length`). + +## Description du problème +Au démarrage, lors du chargement des agents ou des plug-ins, Plex Media Server appelle une fonction interne de validation d'UUID située à l'adresse virtuelle `0x104a6cc` (sur architecture `aarch64`). + +Cette fonction effectue des vérifications strictes sur la structure de la chaîne (longueur de 36 caractères et présence de tirets aux indices 8, 13, 18 et 23). Si la chaîne passée est invalide ou vide (ce qui arrive pour certains plug-ins sans UUID sous PostgreSQL), elle lève directement une exception fatale C++ `std::domain_error` qui fait crasher le serveur. + +## Détails de l'implémentation du correctif + +Le correctif consiste à intercepter et court-circuiter le validateur d'UUID directement en mémoire au chargement de l'interposeur (shim). + +### 1. Résolution de l'adresse en mémoire +Plex Media Server étant compilé en mode PIE (Position Independent Executable), son adresse de chargement en mémoire est randomisée (ASLR). +- Nous utilisons la fonction standard `dl_iterate_phdr` de la libc pour parcourir les en-têtes de segments chargés. +- Le premier élément retourné par l'itérateur (index 0) correspond systématiquement à l'exécutable principal. Nous lisons son adresse de chargement (`dlpi_addr`) pour calculer l'adresse absolue cible : + $$\text{target\_addr} = \text{base\_addr} + \text{0x104a6cc}$$ + +### 2. Écriture du patch via `/proc/self/mem` +Modifier les permissions des pages mémoire de code avec `mprotect` (`PROT_READ | PROT_WRITE`) échoue sur certains noyaux Docker stricts en raison des règles W^X (Write XOR Execute). +- Pour contourner cette protection de manière robuste, nous ouvrons le descripteur spécial de processus `/proc/self/mem` en mode écriture. +- L'écriture directe via `/proc/self/mem` permet de modifier les instructions de nos propres pages de code en mémoire, même si elles sont actuellement marquées en lecture-seule (`r-xp`). + +### 3. Substitution d'instructions (AArch64 / ARM64) +Nous remplaçons le début de la fonction `0x104a6cc` par trois instructions assembleur ARM64 simples : +```assembly +movz x0, #0 ; Efface le registre x0 (première partie de l'UUID) -> octets: 00 00 80 d2 +movz x1, #0 ; Efface le registre x1 (seconde partie de l'UUID) -> octets: 01 00 80 d2 +ret ; Retourne de la fonction -> octets: c0 03 5f d6 +``` +Cette modification neutralise la fonction de validation en lui faisant systématiquement retourner un UUID vide normalisé ("nil" UUID), empêchant ainsi la levée de l'exception fatale. + +Le cache d'instructions du processeur est ensuite invalidé et vidé via la fonction standard `__clear_cache` pour que les nouvelles instructions soient prises en compte immédiatement par le CPU. + +## Fichiers concernés +- **Interposeur Rust** : [runtime_linux.rs](file:///Users/simon/Sources/github.com/cgnl/plex-postgresql/rust/plex-pg-core/src/runtime_linux.rs) (implémente la fonction `patch_uuid_parser`). +- **Scripts d'entrée** : [standalone-entrypoint.sh](file:///Users/simon/Sources/github.com/cgnl/plex-postgresql/scripts/standalone-entrypoint.sh) et [docker-entrypoint.sh](file:///Users/simon/Sources/github.com/cgnl/plex-postgresql/scripts/docker-entrypoint.sh) (pour formater les UUID initiaux dans `Preferences.xml` et synchroniser la table `devices`). diff --git a/docker-compose.standalone.yml b/docker-compose.standalone.yml index d1b61130..7f0b15dd 100644 --- a/docker-compose.standalone.yml +++ b/docker-compose.standalone.yml @@ -57,7 +57,14 @@ services: - PLEX_PG_TRACE_LOADONE=1 - PLEX_PG_LOG_FILE=/config/Library/Application Support/Plex Media Server/Logs/plex_redirect_pg.log # Clear log file on startup to avoid stale errors - - PLEX_PG_LOG_TRUNCATE_ON_START=1 + - PLEX_PG_LOG_TRUNCATE_ON_START=0 + - PLEX_PG_EXCEPTION_VERBOSE=1 + - PLEX_PG_EXCEPTION_ASSUME_OBJECT=1 + - PLEX_PG_ENABLE_EXCEPTION_CATCHER=1 + - PLEX_PG_EXCEPTION_LOG_META=1 + - PLEX_PG_EXCEPTION_DUMP_OBJECT=1 + - PLEX_PG_EXCEPTION_SCAN_STRINGS=1 + - PLEX_PG_EXCEPTION_SCAN_STRINGS_BYTES=1024 volumes: - plex_config:/config - postgres_socket:/var/run/postgresql diff --git a/rust/plex-pg-core/src/db_interpose_bind/text_blob_binds.rs b/rust/plex-pg-core/src/db_interpose_bind/text_blob_binds.rs index b56a414c..eb56dc71 100644 --- a/rust/plex-pg-core/src/db_interpose_bind/text_blob_binds.rs +++ b/rust/plex-pg-core/src/db_interpose_bind/text_blob_binds.rs @@ -10,7 +10,9 @@ fn parse_machine_identifier(content: &str) -> Option { let start = pos + "MachineIdentifier=\"".len(); if let Some(end) = content[start..].find("\"") { let hex = &content[start..start+end]; - if hex.len() == 32 { + if hex.len() == 36 { + return Some(hex.to_string()); + } else if hex.len() == 32 { return Some(format!( "{}-{}-{}-{}-{}", &hex[0..8], @@ -114,7 +116,7 @@ pub(super) fn bind_text_impl( idx: c_int, mut val: *const c_char, mut n_bytes: c_int, - destructor: *mut c_void, + mut destructor: *mut c_void, ) -> c_int { let (pg_stmt, guard) = unsafe { begin_bind(PHASE_BIND_TEXT, p_stmt) }; let mut _intercepted_uuid = String::new(); @@ -145,6 +147,7 @@ pub(super) fn bind_text_impl( log_debug_lazy!("INTERCEPTED empty UUID bind on devices, replacing with {}", _intercepted_uuid); val = _intercepted_uuid.as_ptr() as *const c_char; n_bytes = _intercepted_uuid.len() as c_int; + destructor = -1isize as *mut c_void; } } } @@ -306,7 +309,7 @@ pub(super) fn bind_text64_impl( idx: c_int, mut val: *const c_char, mut n_bytes: u64, - destructor: *mut c_void, + mut destructor: *mut c_void, encoding: c_uchar, ) -> c_int { let (pg_stmt, guard) = unsafe { begin_bind(PHASE_BIND_TEXT64, p_stmt) }; @@ -338,6 +341,7 @@ pub(super) fn bind_text64_impl( log_debug_lazy!("INTERCEPTED empty UUID bind on devices (64), replacing with {}", _intercepted_uuid); val = _intercepted_uuid.as_ptr() as *const c_char; n_bytes = _intercepted_uuid.len() as u64; + destructor = -1isize as *mut c_void; } } } @@ -399,6 +403,13 @@ mod tests { assert_eq!(uuid, Some("53cfd87b-f8b2-4db2-af2d-6aaa373b2b34".to_string())); } + #[test] + fn test_parse_machine_identifier_valid_36() { + let xml = r#""#; + let uuid = parse_machine_identifier(xml); + assert_eq!(uuid, Some("53cfd87b-f8b2-4db2-af2d-6aaa373b2b34".to_string())); + } + #[test] fn test_parse_machine_identifier_invalid_len() { let xml = r#""#; diff --git a/rust/plex-pg-core/src/db_interpose_column/text_accessor.rs b/rust/plex-pg-core/src/db_interpose_column/text_accessor.rs index a5025f17..3ef6f1f3 100644 --- a/rust/plex-pg-core/src/db_interpose_column/text_accessor.rs +++ b/rust/plex-pg-core/src/db_interpose_column/text_accessor.rs @@ -218,21 +218,24 @@ pub(super) fn column_text_impl(p_stmt: *mut sqlite3_stmt, idx: c_int) -> *const let _guard = unsafe { PgStmt::lock_mutex(dbg_stmt) }; if !pg_stmt.cached_result.is_null() { - match unsafe { load_cached_text_state(pg_stmt, idx) } { - Some(state) => unsafe { write_cached_text_output(pg_stmt, idx, &state) }, - // SQL NULL: return empty string instead of NULL to prevent - // Plex's std::string(nullptr) → basic_string crash. - // Real SQLite returns NULL here, but Plex doesn't always check. - None => empty_text_buffer(), + let cached = unsafe { &*pg_stmt.cached_result }; + let row = pg_stmt.current_row; + if idx < 0 || idx >= cached.num_cols || row < 0 || row >= cached.num_rows { + ptr::null() + } else { + match unsafe { load_cached_text_state(pg_stmt, idx) } { + Some(state) => unsafe { write_cached_text_output(pg_stmt, idx, &state) }, + None => empty_text_buffer(), + } } } else if pg_stmt.result.is_null() { - empty_text_buffer() + ptr::null() } else if idx < 0 || idx >= pg_stmt.num_cols { - empty_text_buffer() + ptr::null() } else { let row = pg_stmt.current_row; if row < 0 || row >= pg_stmt.num_rows { - empty_text_buffer() + ptr::null() } else { match unsafe { load_live_text_state(pg_stmt, idx) } { Some(state) => unsafe { write_live_text_output(pg_stmt, idx, &state) }, diff --git a/rust/plex-pg-core/src/db_interpose_metadata.rs b/rust/plex-pg-core/src/db_interpose_metadata.rs index d2b82317..b93a11be 100644 --- a/rust/plex-pg-core/src/db_interpose_metadata.rs +++ b/rust/plex-pg-core/src/db_interpose_metadata.rs @@ -7,6 +7,7 @@ use crate::db_interpose_common::{ get_orig_sqlite3_bind_parameter_name, get_orig_sqlite3_create_collation, get_orig_sqlite3_create_collation_v2, get_orig_sqlite3_db_handle, get_orig_sqlite3_errcode, get_orig_sqlite3_errmsg, get_orig_sqlite3_expanded_sql, get_orig_sqlite3_extended_errcode, + get_orig_sqlite3_changes, get_orig_sqlite3_changes64, get_orig_sqlite3_last_insert_rowid, get_orig_sqlite3_free, get_orig_sqlite3_get_table, get_orig_sqlite3_malloc, get_orig_sqlite3_sql, get_orig_sqlite3_stmt_busy, get_orig_sqlite3_stmt_readonly, get_orig_sqlite3_stmt_status, get_shim_sqlite3_errcode, get_shim_sqlite3_errmsg, diff --git a/rust/plex-pg-core/src/db_interpose_metadata/connection_state.rs b/rust/plex-pg-core/src/db_interpose_metadata/connection_state.rs index 9484f285..4f13631b 100644 --- a/rust/plex-pg-core/src/db_interpose_metadata/connection_state.rs +++ b/rust/plex-pg-core/src/db_interpose_metadata/connection_state.rs @@ -8,12 +8,17 @@ pub(super) fn changes_impl(db: *mut sqlite3) -> c_int { }; let pg_conn = crate::pg_client::rust_pg_find_connection(db); - let mut result = 0; - if !pg_conn.is_null() { - let conn = unsafe { &*pg_conn }; - if conn.is_pg_active != 0 { - result = conn.last_changes; + if pg_conn.is_null() { + if let Some(f) = get_orig_sqlite3_changes() { + return unsafe { f(db) }; } + return 0; + } + + let mut result = 0; + let conn = unsafe { &*pg_conn }; + if conn.is_pg_active != 0 { + result = conn.last_changes; } result } @@ -25,12 +30,17 @@ pub(super) fn changes64_impl(db: *mut sqlite3) -> i64 { }; let pg_conn = crate::pg_client::rust_pg_find_connection(db); - let mut result: i64 = 0; - if !pg_conn.is_null() { - let conn = unsafe { &*pg_conn }; - if conn.is_pg_active != 0 { - result = conn.last_changes as i64; + if pg_conn.is_null() { + if let Some(f) = get_orig_sqlite3_changes64() { + return unsafe { f(db) }; } + return 0; + } + + let mut result: i64 = 0; + let conn = unsafe { &*pg_conn }; + if conn.is_pg_active != 0 { + result = conn.last_changes as i64; } result } @@ -47,9 +57,18 @@ pub(super) fn last_insert_rowid_impl(db: *mut sqlite3) -> i64 { let pg_conn = crate::pg_client::rust_pg_find_connection(db); if pg_conn.is_null() { + if let Some(f) = get_orig_sqlite3_last_insert_rowid() { + let rowid = unsafe { f(db) }; + log_debug_lazy!( + "last_insert_rowid: CALLED db={:p} pg_conn=NULL (no exact match, SQLite returned {})", + db, + rowid + ); + return rowid; + } let global_rowid = crate::pg_client::rust_get_global_last_insert_rowid(); log_debug_lazy!( - "last_insert_rowid: CALLED db={:p} pg_conn=NULL (no exact match, global={})", + "last_insert_rowid: CALLED db={:p} pg_conn=NULL (no exact match, no SQLite fn, global={})", db, global_rowid ); diff --git a/rust/plex-pg-core/src/exception_what.rs b/rust/plex-pg-core/src/exception_what.rs index 96238b47..d78dec8d 100644 --- a/rust/plex-pg-core/src/exception_what.rs +++ b/rust/plex-pg-core/src/exception_what.rs @@ -52,7 +52,9 @@ mod impl_unix { const ABI_LIBS: [&[u8]; 1] = [b"libc++abi.dylib\0"]; #[cfg(target_os = "linux")] - const ABI_LIBS: [&[u8]; 6] = [ + const ABI_LIBS: [&[u8]; 8] = [ + b"/usr/lib/plexmediaserver/lib/libc++.so.2\0", + b"/usr/lib/plexmediaserver/lib/libc++abi.so.1\0", b"libc++abi.so.1\0", b"libc++abi.so\0", b"libc++.so.2\0", diff --git a/rust/plex-pg-core/src/runtime_linux.rs b/rust/plex-pg-core/src/runtime_linux.rs index bc3aa47c..688a7f16 100644 --- a/rust/plex-pg-core/src/runtime_linux.rs +++ b/rust/plex-pg-core/src/runtime_linux.rs @@ -156,6 +156,179 @@ fn setup_exception_catcher_if_enabled() { } } +#[cfg(target_arch = "aarch64")] +unsafe fn patch_uuid_parser() { + let mut base_addr: usize = 0; + + unsafe extern "C" fn phdr_callback( + info: *mut libc::dl_phdr_info, + _size: usize, + data: *mut libc::c_void, + ) -> libc::c_int { + let info = &*info; + let name = if info.dlpi_name.is_null() { + "" + } else { + let s = std::ffi::CStr::from_ptr(info.dlpi_name); + s.to_str().unwrap_or("") + }; + + let counter = *(data as *mut usize); + + let _ = libc::fprintf( + stderr_ptr(), + b"[SHIM_INIT] [UUID_PATCH] Phdr entry[%zu]: name='%s', addr=0x%zx\n\0".as_ptr() as *const c_char, + counter, + CString::new(name).unwrap_or_default().as_ptr(), + info.dlpi_addr as usize, + ); + let _ = libc::fflush(stderr_ptr()); + + if counter == 0 { + *(data as *mut usize) = info.dlpi_addr as usize; + return 1; + } + + *(data as *mut usize) = counter + 1; + 0 + } + + let mut callback_data: usize = 0; + libc::dl_iterate_phdr(Some(phdr_callback), &mut callback_data as *mut usize as *mut libc::c_void); + base_addr = callback_data; + + if base_addr == 0 { + let _ = libc::fprintf( + stderr_ptr(), + b"[SHIM_INIT] [UUID_PATCH] WARNING: Failed to find main executable base address\n\0".as_ptr() as *const c_char, + ); + let _ = libc::fflush(stderr_ptr()); + return; + } + + let target_addr = base_addr + 0x104a6cc; + + let _ = libc::fprintf( + stderr_ptr(), + b"[SHIM_INIT] [UUID_PATCH] Main executable base: 0x%zx, target_addr: 0x%zx\n\0".as_ptr() as *const c_char, + base_addr, + target_addr, + ); + let _ = libc::fflush(stderr_ptr()); + + if let Ok(maps) = std::fs::read_to_string("/proc/self/maps") { + let maps_c = CString::new(maps).unwrap_or_default(); + let _ = libc::fprintf( + stderr_ptr(), + b"[SHIM_INIT] [UUID_PATCH] Mappings:\n%s\n\0".as_ptr() as *const c_char, + maps_c.as_ptr(), + ); + let _ = libc::fflush(stderr_ptr()); + } + + let patch: [u8; 12] = [ + 0x00, 0x00, 0x80, 0xd2, + 0x01, 0x00, 0x80, 0xd2, + 0xc0, 0x03, 0x5f, 0xd6, + ]; + + // Method 1: Try writing via /proc/self/mem (bypasses W^X and page protections) + let mut written_via_proc_mem = false; + use std::io::{Seek, SeekFrom, Write}; + if let Ok(mut file) = std::fs::OpenOptions::new().write(true).open("/proc/self/mem") { + if file.seek(SeekFrom::Start(target_addr as u64)).is_ok() { + if file.write_all(&patch).is_ok() { + let _ = file.flush(); + written_via_proc_mem = true; + let _ = libc::fprintf( + stderr_ptr(), + b"[SHIM_INIT] [UUID_PATCH] Wrote patch successfully via /proc/self/mem\n\0".as_ptr() as *const c_char, + ); + let _ = libc::fflush(stderr_ptr()); + } + } + } + + // Method 2: Fallback to mprotect if /proc/self/mem failed + if !written_via_proc_mem { + let page_size = libc::sysconf(libc::_SC_PAGESIZE) as usize; + if page_size == 0 { + let _ = libc::fprintf( + stderr_ptr(), + b"[SHIM_INIT] [UUID_PATCH] WARNING: sysconf page size is 0\n\0".as_ptr() as *const c_char, + ); + let _ = libc::fflush(stderr_ptr()); + return; + } + + let page_start = target_addr & !(page_size - 1); + + let mut protect_res = libc::mprotect( + page_start as *mut libc::c_void, + page_size, + libc::PROT_READ | libc::PROT_WRITE, + ); + + if protect_res != 0 { + protect_res = libc::mprotect( + page_start as *mut libc::c_void, + page_size, + libc::PROT_READ | libc::PROT_WRITE | libc::PROT_EXEC, + ); + } + + if protect_res != 0 { + let err = std::io::Error::last_os_error(); + let err_msg = err.to_string(); + let err_c = CString::new(err_msg).unwrap_or_default(); + let _ = libc::fprintf( + stderr_ptr(), + b"[SHIM_INIT] [UUID_PATCH] WARNING: mprotect failed: raw_os_error=%d, msg='%s'\n\0".as_ptr() as *const c_char, + err.raw_os_error().unwrap_or(0), + err_c.as_ptr(), + ); + let _ = libc::fflush(stderr_ptr()); + return; + } + + std::ptr::copy_nonoverlapping(patch.as_ptr(), target_addr as *mut u8, patch.len()); + + let restore_res = libc::mprotect( + page_start as *mut libc::c_void, + page_size, + libc::PROT_READ | libc::PROT_EXEC, + ); + + if restore_res != 0 { + let err = std::io::Error::last_os_error(); + let err_msg = err.to_string(); + let err_c = CString::new(err_msg).unwrap_or_default(); + let _ = libc::fprintf( + stderr_ptr(), + b"[SHIM_INIT] [UUID_PATCH] WARNING: mprotect restore failed: raw_os_error=%d, msg='%s'\n\0".as_ptr() as *const c_char, + err.raw_os_error().unwrap_or(0), + err_c.as_ptr(), + ); + let _ = libc::fflush(stderr_ptr()); + } + } + + // Flush instruction cache + extern "C" { + fn __clear_cache(start: *mut libc::c_void, end: *mut libc::c_void); + } + __clear_cache(target_addr as *mut libc::c_void, (target_addr + patch.len()) as *mut libc::c_void); + + let _ = libc::fprintf( + stderr_ptr(), + b"[SHIM_INIT] [UUID_PATCH] Successfully patched UUID parser in memory!\n\0".as_ptr() as *const c_char, + ); + let _ = libc::fflush(stderr_ptr()); +} + +#[cfg(not(target_arch = "aarch64"))] +unsafe fn patch_uuid_parser() {} + #[no_mangle] /// # Safety /// This is an ABI-level interposition hook for C++ exceptions. @@ -403,6 +576,7 @@ unsafe extern "C" fn shim_init() { || {}, || { setup_exception_catcher_if_enabled(); + unsafe { patch_uuid_parser(); } crate::pms_child_env::configure_from_env(); crate::pms_child_env::scrub_current_process_preload(); crate::pms_process_compat::configure_from_env(); diff --git a/scripts/docker-entrypoint.sh b/scripts/docker-entrypoint.sh index 0fb31509..c9dd6072 100755 --- a/scripts/docker-entrypoint.sh +++ b/scripts/docker-entrypoint.sh @@ -251,6 +251,125 @@ init_sqlite_schema() { init_single_sqlite_db "$db_dir/com.plexapp.plugins.library.blobs.db" "$schema_file" } +generate_uuid() { + local uuid + if [ -f /proc/sys/kernel/random/uuid ]; then + uuid=$(cat /proc/sys/kernel/random/uuid) + elif command -v uuidgen >/dev/null 2>&1; then + uuid=$(uuidgen) + elif command -v python3 >/dev/null 2>&1; then + uuid=$(python3 -c 'import uuid; print(uuid.uuid4())') + elif command -v python >/dev/null 2>&1; then + uuid=$(python -c 'import uuid; print(uuid.uuid4())') + else + # Fallback pseudo-UUID using random hex digits + local hex="0123456789abcdef" + uuid="" + for i in {1..32}; do + uuid+="${hex:$((RANDOM % 16)):1}" + done + uuid="${uuid:0:8}-${uuid:8:4}-${uuid:12:4}-${uuid:16:4}-${uuid:20:12}" + fi + echo "$uuid" | tr '[:upper:]' '[:lower:]' +} + +sha1_hex() { + local str="$1" + if command -v sha1sum >/dev/null 2>&1; then + echo -n "$str" | sha1sum | awk '{print $1}' + elif command -v shasum >/dev/null 2>&1; then + echo -n "$str" | shasum | awk '{print $1}' + elif command -v openssl >/dev/null 2>&1; then + echo -n "$str" | openssl dgst -sha1 | awk '{print $NF}' + elif command -v python3 >/dev/null 2>&1; then + python3 -c "import hashlib; print(hashlib.sha1(b'$str').hexdigest())" + elif command -v python >/dev/null 2>&1; then + python -c "import hashlib; print(hashlib.sha1(b'$str').hexdigest())" + else + # Fallback: if absolutely nothing is available (unlikely), just use a 40-char dummy hex string + # derived from the uuid. + local clean_str + clean_str=$(echo -n "$str" | tr -d '-') + echo "${clean_str}00000008" + fi +} + +update_preferences_uuid() { + local plex_dir="/config/Library/Application Support/Plex Media Server" + local pref_file="$plex_dir/Preferences.xml" + if [ -f "$pref_file" ]; then + local machine_id anonymous_id processed_id + machine_id=$(sed -n 's/.*[[:space:]]MachineIdentifier="\([^"]*\)".*/\1/p' "$pref_file" || true) + anonymous_id=$(sed -n 's/.*[[:space:]]AnonymousMachineIdentifier="\([^"]*\)".*/\1/p' "$pref_file" || true) + processed_id=$(sed -n 's/.*[[:space:]]ProcessedMachineIdentifier="\([^"]*\)".*/\1/p' "$pref_file" || true) + + local modified=0 + + # Fix/Generate MachineIdentifier + if [ -z "$machine_id" ]; then + machine_id=$(generate_uuid) + echo "Generating missing MachineIdentifier: $machine_id" + sed -i "s//dev/null 2>&1 || true + fi + fi +} + # Pre-create required Plex directories # Prevents boost::filesystem errors when Plex scans for plugins and metadata init_plex_directories() { @@ -267,11 +386,13 @@ init_plex_directories() { # Ensure Preferences.xml exists (Plex crashes with boost::filesystem error without it) if [[ ! -f "$plex_dir/Preferences.xml" ]]; then - local machine_id - machine_id="$(cat /proc/sys/kernel/random/uuid 2>/dev/null | tr -d '-' || echo "plex-pg-$(date +%s)")" + local machine_id anonymous_id processed_id + machine_id=$(generate_uuid) + anonymous_id=$(generate_uuid) + processed_id=$(sha1_hex "$machine_id") cat > "$plex_dir/Preferences.xml" << PREFEOF - + PREFEOF echo "Created initial Preferences.xml (MachineIdentifier=${machine_id})" fi @@ -476,6 +597,8 @@ if [ -n "$PLEX_PG_HOST" ]; then ensure_plex_temp_dir init_plex_directories + update_preferences_uuid + update_device_uuid maybe_clear_flags_dat_on_uuid_error init_sqlite_schema verify_plex_shim diff --git a/scripts/standalone-entrypoint.sh b/scripts/standalone-entrypoint.sh index 6af27dc3..f2fbadd2 100644 --- a/scripts/standalone-entrypoint.sh +++ b/scripts/standalone-entrypoint.sh @@ -241,6 +241,109 @@ init_sqlite_schema() { init_single_sqlite_db "$db_dir/com.plexapp.plugins.library.blobs.db" "$schema_file" } +generate_uuid() { + local uuid + if [ -f /proc/sys/kernel/random/uuid ]; then + uuid=$(cat /proc/sys/kernel/random/uuid) + elif command -v uuidgen >/dev/null 2>&1; then + uuid=$(uuidgen) + elif command -v python3 >/dev/null 2>&1; then + uuid=$(python3 -c 'import uuid; print(uuid.uuid4())') + elif command -v python >/dev/null 2>&1; then + uuid=$(python -c 'import uuid; print(uuid.uuid4())') + else + # Fallback pseudo-UUID using random hex digits + local hex="0123456789abcdef" + uuid="" + for i in {1..32}; do + uuid+="${hex:$((RANDOM % 16)):1}" + done + uuid="${uuid:0:8}-${uuid:8:4}-${uuid:12:4}-${uuid:16:4}-${uuid:20:12}" + fi + echo "$uuid" | tr '[:upper:]' '[:lower:]' +} + +sha1_hex() { + local str="$1" + if command -v sha1sum >/dev/null 2>&1; then + echo -n "$str" | sha1sum | awk '{print $1}' + elif command -v shasum >/dev/null 2>&1; then + echo -n "$str" | shasum | awk '{print $1}' + elif command -v openssl >/dev/null 2>&1; then + echo -n "$str" | openssl dgst -sha1 | awk '{print $NF}' + elif command -v python3 >/dev/null 2>&1; then + python3 -c "import hashlib; print(hashlib.sha1(b'$str').hexdigest())" + elif command -v python >/dev/null 2>&1; then + python -c "import hashlib; print(hashlib.sha1(b'$str').hexdigest())" + else + # Fallback: if absolutely nothing is available (unlikely), just use a 40-char dummy hex string + # derived from the uuid. + local clean_str + clean_str=$(echo -n "$str" | tr -d '-') + echo "${clean_str}00000008" + fi +} + +update_preferences_uuid() { + local plex_dir="/config/Library/Application Support/Plex Media Server" + local pref_file="$plex_dir/Preferences.xml" + if [ -f "$pref_file" ]; then + local machine_id anonymous_id processed_id + machine_id=$(sed -n 's/.*[[:space:]]MachineIdentifier="\([^"]*\)".*/\1/p' "$pref_file" || true) + anonymous_id=$(sed -n 's/.*[[:space:]]AnonymousMachineIdentifier="\([^"]*\)".*/\1/p' "$pref_file" || true) + processed_id=$(sed -n 's/.*[[:space:]]ProcessedMachineIdentifier="\([^"]*\)".*/\1/p' "$pref_file" || true) + + local modified=0 + + # Fix/Generate MachineIdentifier + if [ -z "$machine_id" ]; then + machine_id=$(generate_uuid) + echo "Generating missing MachineIdentifier: $machine_id" + sed -i "s/ "$plex_dir/Preferences.xml" << PREFEOF - + PREFEOF echo "Created initial Preferences.xml (MachineIdentifier=${machine_id})" fi @@ -271,6 +376,7 @@ PREFEOF echo "Plex directories initialized" } + ensure_plex_temp_dir() { local temp_dir="/run/plex-temp" @@ -349,6 +455,22 @@ verify_media_mount() { fi } +update_device_uuid() { + local plex_dir="/config/Library/Application Support/Plex Media Server" + if [ -f "$plex_dir/Preferences.xml" ]; then + local machine_id + machine_id=$(sed -n 's/.*[[:space:]]MachineIdentifier="\([^"]*\)".*/\1/p' "$plex_dir/Preferences.xml" || true) + if [ -n "$machine_id" ]; then + local formatted_uuid="$machine_id" + if [ ${#machine_id} -eq 32 ]; then + formatted_uuid="${machine_id:0:8}-${machine_id:8:4}-${machine_id:12:4}-${machine_id:16:4}-${machine_id:20:12}" + fi + echo "Updating PostgreSQL devices table with MachineIdentifier: $formatted_uuid" + psql -c "UPDATE ${PG_SCHEMA:-plex}.devices SET identifier = '$formatted_uuid' WHERE id = 1;" >/dev/null 2>&1 || true + fi + fi +} + # === Main === if [ -n "$PLEX_PG_HOST" ]; then @@ -364,6 +486,8 @@ if [ -n "$PLEX_PG_HOST" ]; then ensure_plex_temp_dir init_plex_directories + update_preferences_uuid + update_device_uuid init_sqlite_schema verify_plex_shim verify_media_mount