Repository navigation
fix(lib): a malformed saved record or send argument no longer stops a driver, and errors keep their stack - #205
derek-miller wants to merge 2 commits into
Conversation
… driver, and errors keep their stack The fleet audit after the Home Connect arm/disarm outage found these in the template: - A nil before the last argument given to a C4 send is refused with an error naming the method and the argument, and a flag given as something other than a boolean reaches C4 as one. - Saved events, conditionals and bindings skip a namespace that is not a table and a record without its id; a restore says what it skipped, and adding that key again gives it an id, or keeps the one it had. - An event is declared before it is saved, so a declaration C4 refuses leaves no record. - captureError splits a message that holds its own traceback. - persist:defer rethrows an error with the stack where it was raised. - The diagnostics header says how stored messages and tracebacks are escaped.
There was a problem hiding this comment.
Approving. I checked the riskiest change, the send wrappers' new raise, against the dev controller. C4:SendToDevice(id, cmd, nil, false), C4:SendToProxy(b, cmd, nil, "NOTIFY") and C4:SendToNetwork(b, nil, data) already raise there ("tParams should be a table", "3rd parameter must be a string (strValue) or table (tParams)", "nPort should be a number"), so no call that works today now fails. I also rendered the default and diagnostics answer sets and reverted each change one at a time. Every revert failed a named check except the one in the inline nit.
| local binding = Select(bindings, namespace, key) | ||
| if binding == nil then | ||
| local bindingId = self:_getNextBindingId(type) | ||
| if not holds(binding, "bindingId", "type", "class") then |
There was a problem hiding this comment.
Nit: the "type" half of this guard has no test. With it reduced to holds(binding, "bindingId"), the suite still passes, because the only incomplete fixture (noClass in test_saved_entries_without_ids.lua:120) has its type and differs in class, so the rename branch re-adds it anyway. A saved record with its id and class but no type, added again with the same name, provider and class, would go to neither branch. It would stay typeless, and restoreBindings would skip it on every restart. A noType fixture added again unchanged would cover it.
There was a problem hiding this comment.
Done in f118046. A noType fixture (id and class, no type) is added again unchanged and comes back whole at its own id, and the restore warns about it. Reducing the guard to the id alone now fails that check. Its mirror, dropping the class half, used to survive for the same reason: the rename branch re-added the record. That path isn't equivalent, though. It removes the binding and drops its connections when the class differs. So the test now seeds a connection Director still holds on the classless binding and checks that it's kept. Dropping the class half fails that.
| --- @param entry any | ||
| --- @param ... string The fields it must hold. | ||
| --- @return boolean | ||
| local function holds(entry, ...) |
There was a problem hiding this comment.
Nit: bindings checks a saved entry with holds(), while eachEvent (events.lua:35) and eachConditional (conditionals.lua:34), and both add paths, use type(keys) == "table" and Select(x, "<id>") ~= nil for the same check. One idiom across the three modules would read better.
There was a problem hiding this comment.
Done in f118046. bindings.lua now uses the same idiom as events.lua and conditionals.lua: type(keys) == "table" for a namespace and Select(binding, "bindingId") ~= nil for a record, plus field checks once it's known to be a table. holds() is gone. I only added it because a parameter named type shadowed the global in getOrAddDynamicBinding and _getNextBindingId, so I renamed that parameter to bindingType in both.
…and a test for a binding saved without its type
There was a problem hiding this comment.
Approving f118046. Both guard halves in getOrAddDynamicBinding now have tests that catch them. With either the type or class check removed, its own new case fails (the noType record comes back without a type; the classless binding loses the connection Director held), and so does the restore's type/class skip. Select returns nil for a non-table, so the unified idiom is still safe for a string or number record, and no remaining parameter shadows type now that it's bindingType. I mutated each bindings.lua guard against test_saved_entries_without_ids.lua (all nine failed a named check, the unmutated control passed) and ran render-checks for default and diagnostics.
The fleet audit after Home Connect's arm and disarm outage found a few more places in the template where a malformed argument or saved record could stop a driver, or where an error reached Cloud Monitoring without its stack. None is hit in the field today. Each would otherwise reach every driver on its next
copier update.What I changed:
SendToDevice,SendToProxy,SendToNetworkorSendUIRequestis refused with an error that names the method and the argument, before it reaches C4. It raises rather than logging and dropping the send, so it reaches Cloud Monitoring the way this outage did. Library code checks it, so a test that spies onC4:SendToDevicestill catches it. A flag given as anything but a boolean reaches C4 as one, and onlytrueturns it on.getOrAddEventdeclares an event before saving it, so a declaration C4 refuses leaves no record for every restart to replay.captureErrorsplits a message that holds its own traceback, so adebug.traceback()string passed as the message is stored as a message and a traceback. The module header says that stored messages and tracebacks hold literal\n,\tand\r.persist:defer. It rethrows an error with the stack where it was raised. It adds the stack only once, however many scopes the error passes through, and rethrows a non-string error as it is.Undoing any one of these fails a named check in its test. The one exception is the string check before
deferadds a stack, sincedebug.tracebackreturns a non-string error unchanged anyway.