В квартире стояла летняя жара. Впрочем, ненадолго: хозяин квартиры как раз привез мне кондиционер, и его пора было запускать.
Через несколько недель
Я писал на C# программу для распаковки gzip-файлов и неожиданно поймал странное исключение, из которого следовало, что архив поврежден:
Unhandled exception. System.IO.InvalidDataException: The archive entry was compressed using an unsupported compression method. System.IO.Compression.Inflater.Inflate(FlushCode flushCode) at System.IO.Compression.Inflater.ReadInflateOutput(Byte* bufPtr, Int32 length, FlushCode flushCode, Int32& bytesRead) at System.IO.Compression.Inflater.ReadOutput(Byte* bufPtr, Int32 length, Int32& bytesRead) at System.IO.Compression.Inflater.InflateVerified(Byte* bufPtr, Int32 length) at System.IO.Compression.DeflateStream.ReadCore(Span`1 buffer) at System.IO.Compression.DeflateStream.Read(Byte[] array, Int32 offset, Int32 count) at System.IO.StreamReader.ReadBuffer() at System.IO.StreamReader.ReadLine() at MyApp.Program.ReadAllZippedLines(String filename)+MoveNext() at System.Linq.Enumerable.EnumerablePartition`1.MoveNext() at MyApp.Program.Main(String[] args) at MyApp.Program.<Main>(String[] args)
Ситуация выглядела дико: ошибка намекала на поврежденный архив, хотя сам файл не выглядел реально битым. Сказать, что это напрягало, значит ничего не сказать: если бы данные и правда оказались повреждены, потери были бы серьезные. Я просто запустил программу еще раз, и она... отработала нормально. Странно. Первая мысль была такой: может, Windows в этот момент держала файл открытым из-за антивирусной проверки, а я просто попал в неудачный момент.
Но через несколько минут ошибка вернулась. Я выключил Windows Defender, перезагрузил ноутбук и попробовал снова. Без толку. Самое неприятное было в том, что ошибка не воспроизводилась стабильно: иногда все работало, иногда нет.
На очередном запуске Visual Studio остановилась на нужной строке благодаря обработчику исключений. Я смог посмотреть, что происходит внутри C:\Program Files\dotnet\shared\Microsoft.NETCore.App\3.1.16\System.IO.Compression.dll:
internal int ReadCore(Span<byte> buffer) { this.EnsureDecompressionMode(); this.EnsureNotDisposed(); this.EnsureBufferInitialized(); int start = 0; while (true) { do { int num = this._inflater.Inflate(buffer.Slice(start)); start += num; if (start == buffer.Length || this._inflater.Finished() && (!this._inflater.IsGzipStream() || !this._inflater.NeedsInput())) goto label_7; } while (!this._inflater.NeedsInput()); int count = this._stream.Read(this._buffer, 0, this._buffer.Length); if (count > 0) { if (count <= this._buffer.Length) // здесь условие оказалось false this._inflater.SetInput(this._buffer, 0, count); else break; } else goto label_7; } throw new InvalidDataException(SR.GenericInvalidData); label_7: return start; }
Почему вообще count иногда оказывался больше длины буфера? Выглядело это так, будто либо размер буфера, либо сама переменная count в какой-то момент получали некорректное значение. Сам файл при этом не менялся, по крайней мере если верить SHA-1. Правда, во второй раз я даже не смог посчитать хеш: расширение для Проводника Windows, которым я пользовался, само упало с какой-то внутренней ошибкой.
Неужели я наткнулся на состояние гонки? Впрочем, сам распаковщик gzip не выглядел многопоточным, но я все равно пошел глубже по стеку вызовов:
private ZLibNative.ErrorCode Inflate(ZLibNative.FlushCode flushCode) { ZLibNative.ErrorCode errorCode; try { errorCode = this._zlibStream.Inflate(flushCode); } catch (Exception ex) { throw new ZLibException(SR.ZLibErrorDLLLoadError, ex); } switch (errorCode) { case ZLibNative.ErrorCode.BufError: return errorCode; case ZLibNative.ErrorCode.MemError: throw new ZLibException(SR.ZLibErrorNotEnoughMemory, "inflate_", (int) errorCode, this._zlibStream.GetErrorMessage()); case ZLibNative.ErrorCode.DataError: throw new InvalidDataException(SR.UnsupportedCompression); case ZLibNative.ErrorCode.StreamError: throw new ZLibException(SR.ZLibErrorInconsistentStream, "inflate_", (int) errorCode, this._zlibStream.GetErrorMessage()); case ZLibNative.ErrorCode.Ok: case ZLibNative.ErrorCode.StreamEnd: return errorCode; default: throw new ZLibException(SR.ZLibErrorUnexpected, "inflate_", (int) errorCode, this._zlibStream.GetErrorMessage()); } }
Иногда возвращался DataError, иногда StreamError. То есть что-то было не так либо с данными, либо с потоком. Формулировка, мягко говоря, не самая информативная.
Чтобы исключить банальную ошибку в коде, я попробовал распаковать gzip-файл из bash через gzip -dc file. В ответ прилетела странная ошибка “can’t seek file descriptor” при попытке чтения файла. Генерируется она вот здесь, в исходниках bash. Заодно я прогнал еще несколько gzip-файлов, и они тоже не распаковывались.
К этому моменту я уже всерьез подозревал порчу данных. Быстро открыл CrystalDiskInfo, проверил, нет ли переназначенных секторов, но ничего такого не увидел. Потом запустил sfc.exe /SCANNOW, и, насколько помню, он действительно нашел ошибки и исправил их. Я прогонял его еще и еще, и каждый раз он что-то чинил. Где-то на пятый запуск, кажется, ошибок больше не осталось.
В следующие несколько недель с ноутбуком начали происходить совсем уж странные вещи. Приложения запускались бесконечно долго, а иногда не запускались вовсе, исчезали иконки, текст превращался в кашу. Я уже решил, что ноутбук просто умирает. Но оставалась одна загадка: на батарее ничего подобного не происходило. Все странности начинались только при питании от сети. И почему тогда проблемы появились еще и у монитора, который я совсем недавно купил?
Пора копать глубже
Особенно меня насторожило то, что держать руки на корпусе стало неприятно. Это был уже немолодой MacBook Pro 2012 года с Boot Camp, но на алюминии не было никаких видимых следов износа. И снова всплыл тот же паттерн: дискомфорт появлялся только при работе от сети.
Отступление: к этому моменту мне уже два или три раза приходилось восстанавливать Visual Studio через Repair из-за каких-то совсем диких ошибок, по которым Google ничего не находил.
Полная картина сложилась, когда меня как следует ударило током от корпуса ноутбука. Тут я впервые всерьез подумал: а вдруг проблема вообще в электрике?
Ток там, где его быть не должно
К этому моменту у меня накопился такой набор симптомов:
На новом мониторе появлялись странные зелено-красные помехи.
На старом мониторе появлялись такие же зелено-красные помехи.
Трекпад неприятно пощипывал током.
Похоже, местами портились данные.
От ноутбука ощутимо било током.
Иногда свет в квартире после выключения как будто сам по себе чуть-чуть загорался снова.
Слишком уж странно выглядело совпадение, что и старый, и новый монитор показывали один и тот же набор артефактов. В начале года я еще и менял батарею в ноутбуке, так что не исключал и собственную ошибку.
Что вообще может заставить проводить ток то, что по идее проводить его не должно? Вариантов хватало:
Какое-то устройство подключено или собрано неправильно.
Проблемы с заземлением.
Что-то еще, о чем я пока не подумал.
Чтобы исключить влияние других устройств, я выдернул все из сетевого фильтра и включил ноутбук напрямую в розетку в ванной (она подключена через УЗО). После этого нагрузил его Prime95 примерно на 15 минут. Все выглядело нормально. Потом я подключил остальное обратно, и поначалу тоже было тихо.
А затем проблемы вернулись. Но не всегда, как мне тогда казалось. Со временем я заметил корреляцию: они проявлялись, когда на улице шел дождь или просто была высокая влажность. Неужели из-за сырости где-то что-то коротит?
Apple пишет, что использовать MacBook стоит при относительной влажности от 0% до 95% без конденсации. По температуре воздуха я тоже оставался в допустимых пределах. На всякий случай я выкрутил кондиционер на максимум, чтобы сильнее снизить и температуру, и влажность. Но стало только хуже.
Тогда я подумал: может, воздух, наоборот, слишком сухой? Попробовал поднять температуру на кондиционере, но это тоже ничего не дало.
Зато я заметил другое: проблемы коррелировали с включенным кондиционером, а кондиционер, в свою очередь, включался именно тогда, когда становилось жарко и душно.
К этому моменту я уже почти не сомневался, что дело в электрике. Я попросил хозяина квартиры прислать мастера. Пришел техник и посоветовал купить ИБП с AVR, то есть с автоматической стабилизацией напряжения.
Это такой ИБП, который сам подравнивает напряжение до нужного уровня, если оно проседает ниже порога. В моем случае речь шла о 120 В. Я и сам замечал, что свет иногда тускнеет, поэтому техник предположил, что все мои проблемы и с железом, и с софтом могут быть просто следствием просадок по питанию.
Я купил такой ИБП и сперва был очень доволен: он постоянно пищал, а значит, действительно фиксировал какие-то проблемы и компенсировал просадки напряжения.
Слишком уж постоянно. Пищал на созвонах, пищал за обедом, пищал, когда я пытался уснуть. Стоишь, пьешь воду, и тут... пиииип! Хорошо, что в тот момент я просто пил воду.
В какой-то момент я отключил звук, потому что он начал всерьез мешать и будить меня по ночам. По статистике ИБП за несколько дней уже успел отработать как минимум 20 инцидентов с питанием. "Ничего себе, пашет не зря", подумал я.
К сожалению, еще через несколько дней все проблемы вернулись.
Заземление
Следующим логичным шагом было проверить заземление. Заземление - это путь, по которому лишняя или нежелательная электрическая энергия безопасно уходит в грунт. Если с этим есть проблема, то лишний ток может не уходить куда должен. Терминологию я, возможно, местами коверкаю: я не электрик. Но смысл был именно такой.
Простой тестер розеток за 10 долларов быстро подтвердил подозрение: заземления не было.
Отступление: к этому моменту ноутбук стал почти непригоден к использованию даже на батарее. Начались случайные выключения, экран мог просто почернеть. В журнале событий сыпались записи, связанные с проблемами питания APIC. В итоге я параллельно купил новый ноутбук.
После этого я снова попросил хозяина квартиры прислать уже именно электрика. Он задал много вопросов, а потом предложил подключить кондиционер в другую розетку, потому что в квартире было две отдельные линии питания. Я так прожил несколько дней, но симптомы снова вернулись.
Я попросил приехать еще раз. На этот раз мне поставили совершенно новую розетку и заземлили ее (как мне сказали) через подключение водонагревателя: по крайней мере, после этого тестер начал показывать корректное заземление.
После этого, пусть один ноутбук к тому моменту уже и был фактически списан, проблемы исчезли. Никаких странных цветов на мониторе, никакого трекпада, от которого щиплет током, никаких ударов током от корпуса.
К чему я все это: иногда отладка выходит далеко за пределы IDE и сталкивается с реальным миром.
