feat: get printers shared via cups #49

Merged
davide merged 5 commits from feat/cups-get-printers into main 2026-07-02 13:53:09 +00:00
Owner
No description provided.
feat: get printers shared via cups
Some checks failed
check-commitizen.yaml / Check the PR title (pull_request) Successful in 9s
check-linter.yaml / Checks with linters (pull_request) Has been cancelled
check-syntax.yaml / Checks with static tools (pull_request) Has been cancelled
check-formatter.yaml / Checks with formatters (pull_request) Has been cancelled
check-yamllint.yaml / Lint yaml files (pull_request) Has been cancelled
check-build.yaml / Check that project builds (pull_request) Has been cancelled
forgejo-agent / LLM Bot response generation (pull_request) Successful in 2m25s
bc9e872349
Merge branch 'main' into feat/cups-get-printers
All checks were successful
check-build.yaml / Check that project builds (pull_request) Successful in 1m7s
check-formatter.yaml / Checks with formatters (pull_request) Successful in 14s
check-linter.yaml / Checks with linters (pull_request) Successful in 19s
check-syntax.yaml / Checks with static tools (pull_request) Successful in 21s
check-yamllint.yaml / Lint yaml files (pull_request) Successful in 4s
6ad9cedc6b
davide requested review from llm-bot 2026-07-02 12:07:03 +00:00
llm-bot left a comment

Review Summary

Good work on implementing the CUPS printer discovery feature. The C++ native module compiles cleanly, passes both clang-tidy and clang-format checks, and the overall architecture (using Napi::AsyncWorker for blocking I/O) is correct.

Here are my findings organized by severity:

🔴 Critical Issues

1. Use-after-free race condition in cups-get-dests.cpp

In CupsGetDestsWorker, the raw http_t* pointer is stored from External::Data(). If the CupsConnection JS object is garbage collected while the worker is executing Execute(), the finalizer will call httpClose() on the underlying pointer, but the worker thread will still be using it — leading to a use-after-free.

The External is only kept alive by the JS-side CupsConnection.data reference, but the native worker doesn't hold a strong reference to it during execution.

Fix: Store a Napi::ObjectReference (or Napi::Persistent) to the JS object in the worker to keep it alive during execution:

Napi::ObjectReference connectionRef;
// In constructor:
this->connectionRef.Reset(connection.As<Napi::Object>(), 1);
// In destructor:
this->connectionRef.Reset();

🟡 Medium Issues

2. No explicit resource cleanup for CupsConnection

The CupsConnection class has no destroy() or close() method. The underlying http_t* is only closed when the External finalizer runs, which is non-deterministic. In long-running servers, this could lead to leaked HTTP connections.

Suggestion: Add a close() method:

close(): void {
  // The External finalizer will handle httpClose when GC'd,
  // but we can clear our reference here.
  this.data = undefined as unknown as CupsConnectionData;
}

3. Empty stub methods in CupsPrinter

getInfo() and sendJob() are empty bodies. These should either be implemented or marked with // TODO: comments so consumers know they're not yet available.

4. Unused exported type CupsPrinterInfoData

This branded type is exported but never used anywhere. It appears to be a placeholder for future getInfo() implementation. Either use it or remove it to avoid confusion.

🟢 Minor / Style

5. CupsPrinter creates a circular reference

CupsPrinter holds a reference to CupsConnection, which in turn would hold references to CupsPrinter (if added to a list). This is handled by the JS GC, but it means CupsConnection cannot be collected while any CupsPrinter references exist. Consider whether CupsPrinter needs to hold the connection reference at all, or if it could be accessed lazily.

6. Buffer size constants in http-connect-uri.cpp

The local variables schemeLen, usernameLen, etc. are declared const auto but are only used in one place. Consider simplifying:

auto schemeBuf = std::array<char, HTTP_MAX_URI>();

7. Missing C++23 requirement in docs

The meson.build upgrades to C++23 for std::views::enumerate, but the README and AGENTS.md don't mention a minimum C++23 compiler requirement. Consider adding this to the prerequisites.

Positive Notes

  • Clean separation of concerns: http-connect-uri.cpp and cups-get-dests.cpp each handle a single responsibility
  • Proper use of Napi::AsyncWorker for blocking libcups calls
  • Correct memory management with External finalizers (httpClose and cupsFreeDests)
  • Good documentation comments pointing to libcups v2/v3 docs
  • Meson configuration is clean with is_system: true for system headers
  • The TypeScript branding pattern (unknown & { __brand: "..." }) provides nominal type safety
## Review Summary Good work on implementing the CUPS printer discovery feature. The C++ native module compiles cleanly, passes both clang-tidy and clang-format checks, and the overall architecture (using `Napi::AsyncWorker` for blocking I/O) is correct. Here are my findings organized by severity: ### 🔴 Critical Issues **1. Use-after-free race condition in `cups-get-dests.cpp`** In `CupsGetDestsWorker`, the raw `http_t*` pointer is stored from `External::Data()`. If the `CupsConnection` JS object is garbage collected while the worker is executing `Execute()`, the finalizer will call `httpClose()` on the underlying pointer, but the worker thread will still be using it — leading to a use-after-free. The `External` is only kept alive by the JS-side `CupsConnection.data` reference, but the native worker doesn't hold a strong reference to it during execution. **Fix:** Store a `Napi::ObjectReference` (or `Napi::Persistent`) to the JS object in the worker to keep it alive during execution: ```cpp Napi::ObjectReference connectionRef; // In constructor: this->connectionRef.Reset(connection.As<Napi::Object>(), 1); // In destructor: this->connectionRef.Reset(); ``` ### 🟡 Medium Issues **2. No explicit resource cleanup for `CupsConnection`** The `CupsConnection` class has no `destroy()` or `close()` method. The underlying `http_t*` is only closed when the `External` finalizer runs, which is non-deterministic. In long-running servers, this could lead to leaked HTTP connections. **Suggestion:** Add a `close()` method: ```typescript close(): void { // The External finalizer will handle httpClose when GC'd, // but we can clear our reference here. this.data = undefined as unknown as CupsConnectionData; } ``` **3. Empty stub methods in `CupsPrinter`** `getInfo()` and `sendJob()` are empty bodies. These should either be implemented or marked with `// TODO:` comments so consumers know they're not yet available. **4. Unused exported type `CupsPrinterInfoData`** This branded type is exported but never used anywhere. It appears to be a placeholder for future `getInfo()` implementation. Either use it or remove it to avoid confusion. ### 🟢 Minor / Style **5. `CupsPrinter` creates a circular reference** `CupsPrinter` holds a reference to `CupsConnection`, which in turn would hold references to `CupsPrinter` (if added to a list). This is handled by the JS GC, but it means `CupsConnection` cannot be collected while any `CupsPrinter` references exist. Consider whether `CupsPrinter` needs to hold the connection reference at all, or if it could be accessed lazily. **6. Buffer size constants in `http-connect-uri.cpp`** The local variables `schemeLen`, `usernameLen`, etc. are declared `const auto` but are only used in one place. Consider simplifying: ```cpp auto schemeBuf = std::array<char, HTTP_MAX_URI>(); ``` **7. Missing C++23 requirement in docs** The meson.build upgrades to C++23 for `std::views::enumerate`, but the README and AGENTS.md don't mention a minimum C++23 compiler requirement. Consider adding this to the prerequisites. ### ✅ Positive Notes - Clean separation of concerns: `http-connect-uri.cpp` and `cups-get-dests.cpp` each handle a single responsibility - Proper use of `Napi::AsyncWorker` for blocking libcups calls - Correct memory management with `External` finalizers (`httpClose` and `cupsFreeDests`) - Good documentation comments pointing to libcups v2/v3 docs - Meson configuration is clean with `is_system: true` for system headers - The TypeScript branding pattern (`unknown & { __brand: "..." }`) provides nominal type safety
@ -0,0 +10,4 @@
Napi::Promise::Deferred deferred;
http_t* connection;
std::vector<cups_dest_t> dests;
Member

Use-after-free: raw http_t* stored from External::Data() without keeping JS object alive during worker execution. If CupsConnection is GC'd mid-execution, httpClose() frees the pointer while worker thread still uses it.

Use-after-free: raw http_t* stored from External::Data() without keeping JS object alive during worker execution. If CupsConnection is GC'd mid-execution, httpClose() frees the pointer while worker thread still uses it.
davide marked this conversation as resolved
@ -0,0 +28,4 @@
auto usernameBuf = std::array<char, usernameLen>();
const auto hostLen = HTTP_MAX_HOST;
auto hostBuf = std::array<char, hostLen>();
const auto resourceLen = HTTP_MAX_URI;
Member

Consider simplifying — no need for const auto intermediate variables.

Consider simplifying — no need for const auto intermediate variables.
davide marked this conversation as resolved
@ -0,0 +2,4 @@
export type CupsConnectionData = unknown & { __brand: "CupsConnectionData" };
export type CupsPrinterData = unknown & { __brand: "CupsPrinterData" };
export type CupsPrinterInfoData = unknown & { __brand: "CupsPrinterInfoData" };
Member

Unused exported type. Either use it for getInfo() or remove it.

Unused exported type. Either use it for getInfo() or remove it.
davide marked this conversation as resolved
@ -0,0 +9,4 @@
constructor(data: CupsConnectionData) {
this.data = data;
}
Member

Consider adding a close()/destroy() method to explicitly clean up the underlying HTTP connection instead of relying on GC.

Consider adding a close()/destroy() method to explicitly clean up the underlying HTTP connection instead of relying on GC.
davide marked this conversation as resolved
@ -0,0 +32,4 @@
this.connection = connection;
this.name = name;
this.isDefault = isDefault;
this.data = data;
Member

Empty stub method — implement or mark as TODO.

Empty stub method — implement or mark as TODO.
davide marked this conversation as resolved
@ -0,0 +34,4 @@
this.isDefault = isDefault;
this.data = data;
}
Member

Empty stub method — implement or mark as TODO.

Empty stub method — implement or mark as TODO.
davide marked this conversation as resolved
chore: fix review
Some checks failed
check-formatter.yaml / Checks with formatters (pull_request) Failing after 12s
check-linter.yaml / Checks with linters (pull_request) Successful in 24s
check-yamllint.yaml / Lint yaml files (pull_request) Has been cancelled
check-syntax.yaml / Checks with static tools (pull_request) Has been cancelled
check-build.yaml / Check that project builds (pull_request) Has been cancelled
7d9918d8a0
davide scheduled this pull request to auto merge when all checks succeed 2026-07-02 13:51:22 +00:00
chore: format file
All checks were successful
check-formatter.yaml / Checks with formatters (pull_request) Successful in 12s
check-linter.yaml / Checks with linters (pull_request) Successful in 23s
check-syntax.yaml / Checks with static tools (pull_request) Successful in 24s
check-yamllint.yaml / Lint yaml files (pull_request) Successful in 3s
check-build.yaml / Check that project builds (pull_request) Successful in 1m9s
2d90f602d2
davide merged commit 59dbe0810f into main 2026-07-02 13:53:09 +00:00
davide deleted branch feat/cups-get-printers 2026-07-02 13:53:09 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
projects/events-cash-register!49
No description provided.