В квартире стояла летняя жара. Впрочем, ненадолго: хозяин квартиры как раз привез мне кондиционер, и его пора было запускать.

Через несколько недель

Я писал на 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 и сталкивается с реальным миром.