Fixed owned paths and their exact expected contents. */ private function files(): array { $identity = hash( 'sha256', DB_NAME . ':' . $this->wpdb->prefix ); $notice = ' $mu, ABSPATH . '.maintenance' => $core, ); } /** @return void */ public function install(): void { if ( PHP_SAPI !== 'cli' ) { throw new RuntimeException( 'ABECE_BOOKING_OFFLINE_CLI_REQUIRED' ); } if ( ! is_dir( WPMU_PLUGIN_DIR ) && ! wp_mkdir_p( WPMU_PLUGIN_DIR ) ) { throw new RuntimeException( 'ABECE_BOOKING_GATE_DIRECTORY_UNWRITABLE' ); } foreach ( $this->files() as $path => $content ) { if ( file_exists( $path ) ) { if ( file_get_contents( $path ) !== $content ) { throw new RuntimeException( 'ABECE_BOOKING_FOREIGN_MAINTENANCE_FILE' ); } continue; } $temp = $path . '.' . bin2hex( random_bytes( 10 ) ) . '.tmp'; if ( file_put_contents( $temp, $content, LOCK_EX ) !== strlen( $content ) || ! chmod( $temp, 0644 ) ) { throw new RuntimeException( 'ABECE_BOOKING_GATE_WRITE_FAILED' ); } // Atomic no-clobber publication; never overwrite another maintenance owner. $published = link( $temp, $path ); unlink( $temp ); if ( ! $published ) { throw new RuntimeException( 'ABECE_BOOKING_GATE_PUBLISH_FAILED' ); } } if ( ! $this->active() ) { throw new RuntimeException( 'ABECE_BOOKING_GATE_NOT_ACTIVE' ); } } /** @return bool */ public function active(): bool { foreach ( $this->files() as $path => $content ) { if ( ! is_file( $path ) || file_get_contents( $path ) !== $content ) { return false; } } return true; } /** @return void */ public function release(): void { if ( PHP_SAPI !== 'cli' || ! DatabaseState::ready( $this->wpdb ) || ! $this->active() ) { throw new RuntimeException( 'ABECE_BOOKING_GATE_RELEASE_REFUSED' ); } // Remove the core marker last, after the full current namespace is confirmed. foreach ( $this->files() as $path => $content ) { if ( file_get_contents( $path ) !== $content || ! unlink( $path ) ) { throw new RuntimeException( 'ABECE_BOOKING_GATE_RELEASE_FAILED' ); } } } /** * Wait for other site database sessions; never kill workers or unrelated sites. * * @param int $seconds Maximum time to wait. * @return void */ public function drain( int $seconds = 15 ): void { if ( ! $this->active() ) { throw new RuntimeException( 'ABECE_BOOKING_GATE_NOT_ACTIVE' ); } $id = (int) $this->wpdb->get_var( 'SELECT CONNECTION_ID()' ); $waiting_migrator = $this->wpdb->prepare( 'SELECT GET_LOCK(%s,15)', Tables::lock( $this->wpdb, 'schema' ) ); $deadline = microtime( true ) + $seconds; do { $processes = $this->wpdb->get_results( 'SHOW FULL PROCESSLIST', ARRAY_A ); if ( $this->wpdb->last_error ) { throw new RuntimeException( 'ABECE_BOOKING_DRAIN_CHECK_FAILED' ); } $others = array_filter( $processes, static fn( array $row ): bool => ( $row['db'] ?? '' ) === DB_NAME && (int) $row['Id'] !== $id && ( $row['Info'] ?? '' ) !== $waiting_migrator ); if ( ! $others ) { return; } usleep( 100000 ); } while ( microtime( true ) < $deadline ); throw new RuntimeException( 'ABECE_BOOKING_ACTIVE_DATABASE_WORKERS' ); } }