Skip to content

Merge main into integration/1.2 - #1844

Merged
kevinherron merged 5 commits into
integration/1.2from
merge/main-into-1.2
Aug 31, 2026
Merged

Merge main into integration/1.2#1844
kevinherron merged 5 commits into
integration/1.2from
merge/main-into-1.2

Conversation

@kevinherron

Copy link
Copy Markdown
Contributor

Brings integration/1.2 up to date with main.

Commits from main

  • a0170a1 Write the KeyStore atomically and close its streams
  • 6dc726c Close KeyStore streams in the example loaders
  • 2b65449 Make watchForChanges actually reload the KeyStore
  • 2cb1643 Merge pull request Make watchForChanges actually reload the KeyStore #1842 from eclipse-milo/fix/keystore-watch-for-changes

Conflict resolution

One conflict, in KeyStoreCertificateStoreTest. Both branches added tests to the same file
on top of a common base:

  • main refactored newCertificateStore() into a newKeyStoreCertificateStore() helper and
    added tests for atomic writes, reload, and the watch service.
  • integration/1.2 added tests for the ECC default aliases and for initialize() skipping
    unmanaged and missing preloaded aliases.

Kept main's structure and tests, re-applied integration/1.2's three tests on top, and unioned
the imports. Nothing was dropped from either side.

KeyStoreCertificateStore itself merged cleanly: main's atomic-write and reload changes apply
over integration/1.2's ECC alias support without overlap.

Verification

  • mvn spotless:apply — no changes
  • mvn clean compile — passes
  • mvn -pl opc-ua-stack/stack-core test -Dtest=KeyStoreCertificateStoreTest — 13 tests, all pass

KeyStore.load and KeyStore.store do not close the streams they are given, so
every load and store leaked a descriptor until the cleaner ran. On Windows
that also kept the file locked.

Opening a FileOutputStream on the KeyStore file was the worse problem: it
truncates on open, so a store() that failed part way through left behind a
KeyStore with no keys in it. Writes now go to a temporary file in the same
directory and are moved into place, preserving the original file's POSIX
permissions and following symlinks so an existing link is updated rather
than replaced by a regular file.

set() and remove() mutate the in-memory KeyStore before writing it out, so
they now roll that mutation back when the write fails; otherwise memory and
disk stay diverged for the life of the process. set() also picks up the null
alias guard that contains(), get() and remove() already had, and getAlias is
declared @nullable to match how its callers treat it.

The tests open a second store over the same file, which is the only way to
tell that anything reached disk; the inherited assertions all pass against
the in-memory KeyStore alone.
The same unclosed-stream problem as KeyStoreCertificateStore. Both copies
now use Path with Files.newInputStream/newOutputStream, matching the
client-examples copy that already had the fix.
The setting could not have worked. Four separate defects, any one of which
was enough on its own:

The watch key was never reset, and a WatchKey stays signalled and is never
queued again until it is. The watcher could therefore fire at most once,
after which take() blocked forever.

processWatchEvent compared the event context against the KeyStore path, but
a directory watch reports a context relative to the watched directory while
toAbsolutePath resolves against the working directory. Unless the KeyStore
happened to sit in the working directory, the comparison never matched.

Only ENTRY_MODIFY was registered. Replacing a file by renaming a temporary
one over it arrives as a creation, so the safe way to update a KeyStore
produced no event at all. This store now writes its own file that way.

The reload itself only called loadEntries(), which re-queries the KeyStore
already held in memory. Even if an event had arrived, the file was never
re-read, so an externally renewed certificate could not have been seen.

Reloading now reads into a new KeyStore and swaps it in only once it has
loaded, so a file that is unreadable, or still being written, leaves the one
in use untouched. Overflow events are treated as a change, since there is no
way to tell what was dropped.

Closing the store also threw ClosedWatchServiceException out of the watcher
thread rather than ending it, so shutdown left an uncaught exception behind.

The end-to-end test fails against the previous implementation, timing out
after 30 seconds; it passes in well under a second here.
…nges

Make watchForChanges actually reload the KeyStore
Conflicts:
  opc-ua-stack/stack-core/src/test/java/org/eclipse/milo/opcua/stack/core/security/KeyStoreCertificateStoreTest.java

Both branches added tests to KeyStoreCertificateStoreTest. Kept main's
newKeyStoreCertificateStore() helper and its tests, and re-applied the
alias and initialize() tests from integration/1.2 on top.
@kevinherron
kevinherron merged commit f2024e6 into integration/1.2 Aug 31, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant