Repository navigation
Releases: 07artem132/SignalCli.NET
Release list
SignalCli.NET v4.10.3
Критичний фікс вхідних повідомлень + захист event-диспетчера
Якщо ти споживаєш вхідні повідомлення (receive) — онови обов'язково. Підняли бандл до signal-cli 0.14.6, який фіксить upstream #2059: з ~10 червня 2026 Signal-сервер перестав слати serverGuid у частині sealed-sender конвертів, і signal-cli ≤0.14.4.1 кидав NullPointerException → дропав УСІ вхідні sealed-sender повідомлення (надсилання не зачеплене).
Пакети
SignalCli.Runtime,.Native,.Jre.win-x64,.Jre.osx-arm64:0.14.3.1→0.14.6.1(signal-cli 0.14.6). JDK-вимога незмінна — 25.SignalCli.NET/SignalCli.NET.HealthChecks: 4.10.3.
Інше
SignalEventService.OnNotificationReceivedтепер стійкий до порожніх/малформед receive-нотіфікацій (лог-і-скіп на Debug замість NRE-спаму на Error).
Деталі — CHANGELOG.md [4.10.3].
SignalCli.NET 4.10.2 — per-call RPC timeout
API-additive per-call timeout override для InvokeMethodAsync/FinishLinkAsync (trailing TimeSpan? timeout = null; null/≤0 → клієнтський default, поведінка незмінна; від'ємний → ArgumentOutOfRange). Закриває залежність консюмера WsRpcServer: finishLink потребує ≥130с на ручний QR-скан, тісніший за глобальний 30с.
514 тестів. Реліз тригерить publish-nuget → GitHub Packages.
SignalCli.NET 4.10.1
Patch: виправлення silent-null wire-bug'у у device-linking флоу.
StartLinkResponse.DeviceLinkUriтепер несе явний[JsonPropertyName("deviceLinkUri")]— signal-cli віддає camelCase, а case-sensitive source-gen контекст мовчки дававnull(RPC-відповідь"result": {}), QR-лінкування було зламане.- Новий regression guard RG10 (
WireShapeAnnotationTests): кожна DTO-властивість контексту мусить мати явний[JsonPropertyName]/[JsonIgnore]. DeviceLinkingSerializationTests: wire-піни startLink/finishLink через продакшн-шлях.
Повний опис: CHANGELOG.md [4.10.1]. Тести: 509/509.
v4.0.2 — HealthChecks lockstep + JSON hardening + test guards
Patch-реліз без breaking changes. Якщо ти використовуєш SignalCli.NET.HealthChecks — онови його разом з main package: до 4.0.2 версії розійшлися й змішування пакетів давало runtime crash. Решта — 8 нових захисних тестів і одна реальна знахідка про JSON-валідацію, яка мовчки не fire'ила у продакшені.
🐛 Виправлено
-
Якщо консьюм'иш обидва пакети (
SignalCli.NET+SignalCli.NET.HealthChecks) — онови їх разом. До 4.0.2 main lib був4.0.1, а health-checks-adapter —3.0.0. Змішування пакетів кидалоMissingMethodExceptionна першому health-check probe (бо adapter читає internal'и main lib через[InternalsVisibleTo]). Тепер обидва завжди ходять в lockstep через єдиний MSBuild property уDirectory.Build.props. Bug-class виключено: reflection-тестVersionLockstepTestsловить розбіжність до merge'у. (NF-003, RG07) -
JSON duplicate-key захист (rule #18) тепер реально діє на production-шляху. З 4.0.0 ми оголошували що malformed signal-cli response з повтореним ключем fail'ить deserialize. У реальності — fail'ило тільки на reflection-шляху, а production деsеріалізує через source-gen fast-path який власний runtime-flag не консумує. Тепер захист увімкнено на обох рівнях (runtime flag + source-gen attribute). Без впливу на нормальні signal-cli responses — Jackson на upstream-стороні фізично не може емітити дублікати; fix чисто defensive проти MITM/corruption. Знайшли під час audit'у коли наш же RG-тест спочатку failed. (json-hardening-source-gen-attribute)
-
CI pipeline нарешті пушить
SignalCli.NET.HealthChecksна NuGet feed разом з main package. До 4.0.2publish-nuget.ymlworkflow мав pack-steps для 5 пакетів (SignalCli.NET,SignalCli.Runtime,SignalCli.Runtime.Native,SignalCli.Runtime.Jre.{win-x64,osx-arm64}) — алеSignalCli.NET.HealthChecksбув відсутній. Net effect: csproj-сторона мала версії в lockstep (NF-003 main fix), але consumer-сторона все одно отримувала stale3.0.0HealthChecks-пакета з registry бо нічого свіжіше не пушилось. Цей gap робив весь NF-003 lockstep косметичним на consumer-стороні. Тепер крок присутній — після першогоgh release create v4.0.2HealthChecks теж потрапить на feed. (nf003-completion follow-up)
🛡️ Захист від регресій
8 нових unit + 1 E2E-тести закривають declared-але-untested invariants з CLAUDE.md "Future development guardrails":
-
Паралельні JSON-RPC виклики тепер pinned проти реального signal-cli — не лише проти mock'а на unit-рівні. Новий E2E запускає bundled JRE, кидає 10 паралельних
version-викликів через справжній signal-cli virtual-thread dispatcher, асертить що всі 10 повертають свою власну відповідь. Якщо хтось у майбутньому замінитьConcurrentDictionary<id, TCS>наQueue<TCS>"бо порядок збережений" — unit-тести з in-orderSubject<T>-моком цього б не побачили; цей E2E одразу впаде. (e2e-coverage-expansion) -
Observability counters тепер pinned на всіх 3 тригерах перезапуску. Раніше privacy-guards перевіряли лише відсутність PII у Meter-tags, а фактичне інкрементування
signalcli.process.restarts{trigger=…}було pinned тільки наtrigger=force. Теперtrigger=crash(через event-raise на mock IProcess) іtrigger=health(черезFakeTimeProvider.AdvanceповзHealthCheckIntervalSeconds) теж покриті. (G4 subcases — T04, T05) -
Event-API симетрія enforced reflection-тестом. CLAUDE.md правило "кожен
IObservable<T>event-kind має парнийIAsyncEnumerable<T>метод" раніше enforcъ'вся code-review'ом. ТеперEventApiSymmetryTestsreflectively скануєISignalEventServiceі fail'ить build якщо хтось додав 11-й event без парного методу. (NF-002, RG06) -
JSON duplicate-key захист — 3-шаровий guard. Окрім самого fix'у з "Виправлено" вище, додано 3 facts у
JsonSerializationTests:SignalJsonOptions_…IsFalse,JsonDocumentOptions_…ThrowsOnDuplicateKey,SignalJsonContext_…ThrowsOnDuplicateKey. Видалення будь-якого з 3 шарів (runtime flag, .NET 10 API contract, source-gen attribute) surface'иться як failed test. (G12, RG05) -
JSON-RPC error wins over result. Захисний test пінує що при malformed-response (одночасно
resultІerror) JsonRpcClient кидає exception, а не повертає result. CHANGELOG [4.0.1] стверджував покриття;grepпоказав що тесту не існувало. Тепер дійсно існує. (NF-001, G9)
🛠 Інше
-
Test-проект тепер ловить warnings як errors (як main lib давно). Раніше тихо акумулювалися 3
xUnit1031violations уSyncDisposeDuringCleanupTests.cs(deadlock-prone.GetAwaiter().GetResult()). Виправлено: метод-сигнатуриasync Task+await;service.Dispose()навмисно залишається SYNC бо саме його тести й вимірюють. (NF-004) -
Microsoft.Extensions.*.Testingbumped 9.0.0 → 10.0.0 — закриває last mismatched-major у dependency graph. Public-surface unchanged per MS Learn. (NF-005) -
Версія обох NuGet-пакетів тепер централізована в
Directory.Build.props → <SignalCliPackageVersion>. Bump = single-file edit. Hardcoded<Version>у будь-якому csproj заборонено. -
CLAUDE.md розширено двома секціями для майбутніх контрибуторів + новим правилом:
- "Audit baseline" — мінімальна планка яку PR не може опустити (test counts, regression guards, архітектурні invariants).
- "How we discovered these issues" — 5 failure-mode-сценаріїв з PR-time checklists, щоб не повторювати ті самі паттерни.
- "Version-CHANGELOG lockstep" + "CHANGELOG voice template" — bump версії МУСИТЬ йти з CHANGELOG-секцією в одному коміті; новий template для consumer-first voice (bold leading claim, технічна обгортка, internal-IDs у дужках).
- Виправлено константа
MaxInlineEncodedAttachmentBytes: 15 000 000 → 12 000 000 (drift з 4.0.0 — реальне значення в коді змінилось уsignal-cli-protocol-alignment §5, doc відстав). (NF-006)
-
CHANGELOG voice tightened — починаючи з [4.0.2] (цей entry), пишемо у consumer-first voice. Старіші версії [4.0.1]/[4.0.0] перероблено в тому ж коміті задля консистенції.
📊 Тестова статистика
- Unit: 279 → 287 (+8 захисних тестів).
- Integration: 7 → 8 (+1 E2E на паралельний RPC).
- Build на src/ + Tests/: 0 warnings, 0 errors.
Pending follow-up
Нічого — усі audit v2.1 знахідки закриті.
v3.0.0 — post-modernize-tuning
[3.0.0] — 2026-05-24
Друга велика хвиля модернізації — фокус на correctness/observability/agent-friendly-API. Містить breaking changes, перерахованих нижче.
Реалізація триває; цей розділ оновлюється у міру викочування кластерів. Див. openspec/changes/post-modernize-tuning/.
⚠️ Breaking
FinishLinkResponse.number→FinishLinkResponse.Number(PascalCase property; JSON wire-name збережено через[JsonPropertyName("number")]).SubscribeReceiveResponse.id→SubscribeReceiveResponse.Id(так само PascalCase).BaseSignalEventArgs.Accountтеперstring(non-nullable). Те ж саме поширено на всі 10*EventArgs-records. Раніше булоstring?, що змушувало кожного підписника null-чекати гарантовано-присутнє значення.Config.EnvironmentVariablesіSignalCliOptions.EnvironmentVariablesтеперIReadOnlyDictionary<string,string>на читання. Для мутації —Config.WithEnvironment(IDictionary<string,string>)(defensive copy + fluent return). Раніше можна було.Add(key, value)на shared посилання після DI-capture.JsonRpcException(string, Exception?)ctor з нестандартним кодом -32000 видалено. Замість нього — три CA1032-стандартні ctors:(),(string),(string, Exception)— усі з канонічним JSON-RPC 2.0 кодом -32603 ("Internal error"). Консумери, що каталі legacy-конструктор, мають мігрувати на CA1032-ctors або передавати власнийJsonRpcError.SignalEventService.SubscribeAsync(account)тепер ідемпотентний — повторні виклики для того самого облікового запису повертають той самийsubscriptionIdбез RPC. Раніше другий виклик кидавInvalidOperationException.catch (InvalidOperationException) when (msg.Contains("вже підписаний"))більше не зловить — операція тепер успіх.JsonRequiredна always-present поляхEnvelope.cs:JsonRemoteDelete.RemoteDeleteId,Offer.Type/Offer.Opaque,Answer.Opaque,IceUpdate.Opaque,Hangup.Type. Якщо signal-cli колись поверне ці поля зnull— десеріалізація фолтиться зJsonExceptionзамість тихо пропуститиnullу non-nullable property.UserRecipient/GroupRecipientctor: null →ArgumentNullException, empty →ArgumentException. Раніше empty теж кидавArgumentNullException(порушення контракту обох типів).SignalCliHostedServiceтеперsealed— інхеріт не підтримується.- Стандартний шлях
dotnet publish /p:PublishAot=trueще не enable'нений (deferred — потребує redesign наJsonTypeInfo<T>overloads), але всі предумови (drop Nito.AsyncEx, drop.ValidateDataAnnotations(), source-gen JSON fast-path) на місці. - (round 9 §4.7)
CancellationToken-property +WithCancellationToken-Builder-method видалено зTextMessageOptions/AttachmentMessageOptions/StickerMessageOptions. Єдиний шлях скасування — параметрSend*Async(options, cancellationToken).[Obsolete]-shim після одного major-релізу (як обіцяно в CLAUDE.md "Backward compatibility convention"). Migration:.WithCancellationToken(ct).Build(); → .Build();+ передатиctдругим аргументом. - (round 9 §4.23/4.24)
ISignalMessage.{SendText,SendAttachment,SendSticker}MessageAsyncповертаютьTask<SendMessageResponse>(single response), а неTask<List<SendMessageResponse>>— все одно завжди було[response]-wrap. Migration:(await SendTextMessageAsync(opts))[0] → await SendTextMessageAsync(opts). - (round 9 §4.27) Generic-параметри
InvokeMethodAsyncпоміняли порядок:<TResponse, TRequest>→<TRequest, TResponse>наISignalCliClient,IJsonRpcSender, обох impls + ~22 callsites. Узгоджено зJsonSerializer.Deserialize<TValue>-конвенцією. Shim неможливий — C# не розрізняє overload'и за порядком typeparam'ів (same runtime signature). Migration: розверни<X, Y>→<Y, X>на кожному виклику.
✨ Додано
Observability (capability observability)
- Єдиний
internal static readonly ActivitySource SignalCliDiagnostics.ActivitySource = new("SignalCli.NET", AssemblyVersion)— спаниrpc.<method>,signalcli.process.start,signalcli.healthcheck.ping,signalcli.subscribe. Теги: method name, status enum, integer id, exception type name — без PII. - Єдиний
internal static readonly Meter SignalCliDiagnostics.Meter = new("SignalCli.NET", AssemblyVersion):Counter<long> signalcli.rpc.requests(тегиmethod,status∈ {ok,timeout,error})Histogram<double> signalcli.rpc.duration(мс, тегmethod)Counter<long> signalcli.process.restarts(тегtrigger∈ {force,crash,health})Counter<long> signalcli.events.dropped(тегevent_type∈ 10 значень) — замінює приватний_droppedCount.ObservableGauge<int> signalcli.subscriptions.active.
- Документація:
docs/cloud-development.mdмає нову секцію Observability з drop-in OTel-snippet.
RPC robustness
SignalCliOptions.NotificationChannelCapacity(default 1024). Між stdout-парсером і fan-out-споживачем — bounded Channel; повільний підписник створює back-pressure аж до signal-cli.JsonRpcClientприймаєTimeProvider—CancellationTokenSource(_requestTimeout, _timeProvider)робить timeout-шлях віртуалізованим у тестах.SignalCliHostedService.StopProcessInternalAsyncNoLockтеж використовуєCancellationTokenSource(_, _timeProvider).BeginScope(RpcMethod, RpcRequestId)уJsonRpcClient.InvokeMethodAsync— кожний нижчийJsonRpcClientLog.*несе structured-properties.
Subscription race safety
- Reservation placeholder pattern у
SignalEventService.SubscribeAsyncчерезDictionary<string, TaskCompletionSource<int>> _pendingSubscribes. Конкурентні виклики для того самого облікового запису роблять РІВНО 1 RPC; усі N викликачів отримують той самий ID. ObjectDisposedException.ThrowIf(_disposed, this)наSubscribeAsync/UnsubscribeAsync(audit C6).
Async-suffix shims (one-major-grace)
ISignalAccounts.ListAccountsAsync/SyncAccountAsync,ISignalDevices.StartLinkAsync/FinishLinkAsync,ISignalGroups.ListGroupsAsync— нові методи +[Obsolete]DIM-shims на старі імена ("will be removed in 4.0").
🛠 Внутрішнє
ProcessStateManager: snapshot-then-emit (OnNext поза локом — System.Threading.Lock не реентрантний)._disposedвсюди →intзInterlocked.Exchange(lock-free disposal short-circuit). CatchObjectDisposedExceptionз OnNext (documented disposal race window)._disposedстандартизовано якint + Interlocked.ExchangeуSignalCliHostedService,JsonRpcClient,JsonRpcClientHostedService,SignalEventService.Nito.AsyncExвидалено.JsonRpcClient._sendLockіSignalCliHostedService._operationLock→SemaphoreSlim(1,1)зWaitAsync/Release..ValidateDataAnnotations()видалено з options-pipeline —[OptionsValidator]source-gen самостійно перевіряє[Required]/[Range]без reflection. Знято останній AOT-blocker у options-шляху.SignalJsonContext.GenerationMode = Default(fast-path emission + metadata) замість Metadata-only.SignalEventService,ProcessWrapper,ProcessFactory,JsonRpcClientFactory,SignalAccounts,SignalDevices,SignalGroups— sealed (CA1052).Config.BuildClasspathкешує classpath;Directory.GetFilesвикликається рівно 1 раз наConfig-інстанс.ValidateRecipients: single-pass materialization + одинforeachна user/group split (раніше — 3 пройдення).ArgumentException.ThrowIfNullOrEmptyboundary checks уSignalDevices.FinishLinkAsync/SignalGroups.ListGroupsAsync.JetBrains.AnnotationsPackageReferencePrivateAssets="all"— більше не leak у consumer dependency graph.Example/Program.csповністю переписаний наasync Task Main/await host.StopAsync()/awaitedSendTextMessageAsync— LLM-агенти, що копіюють приклад, успадковують правильні async-патерни.- Forward-slash MSBuild paths у
SignalCli.runtime.csprojіSignalCli.Native.targets— Linux-збірки runtime-пакетів більше не ламаються тихо. - (round 8) Logging-perf analyzer rules:
CA1848 → warning(блокує regression на direct_logger.Log*— кожна нова log-callsite має йти через[LoggerMessage]);CA1873 → suggestion(analyzer не розпізнає manualIsEnabled-guards, тожwarning-рівень дає false-positives на legitimate Trace-only eager-eval сайтах). Trade-off задокументовано в.editorconfig. - (round 8) Manual
if (_logger.IsEnabled(LogLevel.Trace))guards надstring.Join(", ", response)уSignalAccounts.ListAccountsAsync+SignalGroups.ListGroupsAsync—[LoggerMessage]-внутрішній IsEnabled живе всередині generated-methоду, тож callsite-level allocation платилася на КОЖНОМУ виклику (навіть Info-level). Економить N × string-allocation на listAccounts/listGroups. - (round 8)
ObservabilityPrivacyTestsflake-fix: lock-snapshot pattern для_capturedActivities/_capturedMeasurements. ActivityListener/MeterListener реєструються глобально на ActivitySource/Meter, тож callback'и можуть прилітати з потоків паралельних тестів —List<T>не thread-safe →Collection was modifiedintermittent. Всі writes тепер підLock, читачі enumerate'ять snapshot. - (round 9 §4.9)
.editorconfigпіднятоCA2007 (ConfigureAwait)доwarningпісля audit-перевірки: 0 missing-sites уsrc/SignalCli/**. Тепер регресія неможлива — будь-який майбутній bareawaitловиться build-warning'ом. - (round 9 §4.25)
[StringSyntax(StringSyntaxAttribute.Uri)]наTextMessageOptions.PreviewUrl,PreviewImage, та параметрахBuilder.WithPreview(previewUrl, …, previewImage). Zero runtime cost; IDEs тепер валідують URL-syntax. - (round 9 §4.26) XMLDoc'и на 3
Send*Async-методах вISignalMessageотримали<exception cref="TimeoutException">із посиланням наSignalCliOptions.RequestTimeoutSeconds. Closes audit-doc-gap. - (round 10 §6.12) Новий test-file
JsonContextRegistrationTests— рефлексивно стверджує, що кожен*Parameters/*ResponseDTO уSignalCli.Models.Signal.*зареєстрований уSignalJsonContextчерез[JsonSerializable(typeof(...))]. Захист від "silent {}"-регресії, ...