Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# These package-supplied legal texts are verified byte-for-byte (apart from the
# documented OPC CRLF normalization) and intentionally retain trailing spaces.
licenses/ONIGWRAP-THIRD-PARTY-NOTICES.txt -whitespace
licenses/OPC-FOUNDATION-LICENSE.txt -whitespace
61 changes: 42 additions & 19 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,21 @@ The project name is **opcilloscope** (lowercase "o") in all contexts except wher
| User-facing text, CLI, URLs | `opcilloscope` | `opcilloscope --help` |
| C# namespaces, classes, projects | `Opcilloscope` | `namespace Opcilloscope.App` |
| File/folder names (code) | `Opcilloscope` | `Opcilloscope.csproj` |
| Config directories (all platforms) | `opcilloscope` | `~/.config/opcilloscope/` |
| Config/data directories | `opcilloscope` | Use the platform locations below |
| Release artifacts | `opcilloscope` | `opcilloscope-linux-x64.tar.gz` |

Platform directories:
- Linux configuration: `${XDG_CONFIG_HOME:-$HOME/.config}/opcilloscope/`
- Linux application data: `${XDG_DATA_HOME:-$HOME/.local/share}/opcilloscope/`
- macOS configuration and application data: `~/Library/Application Support/opcilloscope/`
- Windows configuration: `%APPDATA%\opcilloscope\`; certificates: `%LOCALAPPDATA%\opcilloscope\pki\`

## Build Commands
```bash
dotnet build # Build project
dotnet run # Run application
dotnet test # Run tests
dotnet build Opcilloscope.sln # Build app and tests
dotnet run --project Opcilloscope.csproj # Run application
dotnet test Opcilloscope.sln # Run unit/integration suite
dotnet test Tests/Opcilloscope.E2ETests/Opcilloscope.E2ETests.csproj # Linux real-PTY E2E
```

## Project Architecture
Expand All @@ -31,13 +38,14 @@ dotnet test # Run tests
- `App/` - UI components (MainWindow, Views, Dialogs)
- `OpcUa/` - OPC UA client logic (Session wrapper, Browser, SubscriptionManager)
- `Utilities/` - Helper classes (Logger, UiThread)
- `tests/` - xUnit tests with in-process OPC UA test server
- `Tests/Opcilloscope.Tests/` - Cross-platform xUnit tests with the in-process OPC UA test server
- `Tests/Opcilloscope.E2ETests/` - Linux-only published-binary PTY tests; intentionally outside `Opcilloscope.sln`

### Key Classes
- **MainWindow.cs** - Main UI layout with panels
- **OpcUaClientWrapper.cs** - OPC Foundation Session wrapper
- **NodeBrowser.cs** - Address space navigation
- **SubscriptionManager.cs** - OPC UA Subscription management with Publish/Subscribe
- **SubscriptionManager.cs** - OPC UA subscriptions and monitored-item notifications (not the OPC UA PubSub transport model)
- **TestServer.cs** - In-process OPC UA server for testing

## Coding Guidelines
Expand All @@ -61,19 +69,26 @@ SetNeedsLayout() // OR Update(), NOT SetNeedsDisplay()
// ListView
ObservableCollection<T> // Required for ListView.SetSource()

// Thread marshalling
Application.Invoke(() => {
// Thread marshalling through the repository helper
UiThread.Run(() => {
// UI updates here
});
```

### OPC Foundation SDK Patterns

#### Endpoint Discovery
`DiscoveryClient.Create` and `Session.Create` are obsolete in the current SDK.
Existing wrapper call sites use narrowly scoped `CS0618` pragmas because the
replacement factories need additional telemetry setup. Prefer the repository
wrapper; do not introduce an unsuppressed call or a project-wide suppression.

```csharp
// DiscoveryClient.Create requires EndpointConfiguration, not ApplicationConfiguration
var endpointConfig = EndpointConfiguration.Create(config);
#pragma warning disable CS0618 // Existing wrapper exception: async factory needs telemetry setup
using var client = DiscoveryClient.Create(uri, endpointConfig);
#pragma warning restore CS0618
var endpoints = await client.GetEndpointsAsync(null);

// Valid DiscoveryClient.Create overloads:
Expand All @@ -94,6 +109,7 @@ await _server.StopAsync();

#### Session Creation
```csharp
#pragma warning disable CS0618 // Existing wrapper exception: async factory needs telemetry setup
var session = await Session.Create(
config,
endpoint,
Expand All @@ -103,6 +119,7 @@ var session = await Session.Create(
new UserIdentity(new AnonymousIdentityToken()),
null
);
#pragma warning restore CS0618
```

#### Subscription with MonitoredItems
Expand All @@ -113,7 +130,7 @@ var subscription = new Subscription(session.DefaultSubscription) {
PublishingEnabled = true
};
session.AddSubscription(subscription);
subscription.Create();
await subscription.CreateAsync();

// Add monitored item
var monitoredItem = new MonitoredItem(subscription.DefaultItem) {
Expand All @@ -123,7 +140,7 @@ var monitoredItem = new MonitoredItem(subscription.DefaultItem) {
};
monitoredItem.Notification += OnNotification;
subscription.AddItem(monitoredItem);
subscription.ApplyChanges();
await subscription.ApplyChangesAsync();
```

#### NodeId Usage
Expand Down Expand Up @@ -199,11 +216,11 @@ public class OtherTests

## Thread Safety

⚠️ **Critical:** OPC Foundation callbacks arrive on background threads. Always use `Application.Invoke()` for UI updates:
⚠️ **Critical:** OPC Foundation callbacks arrive on background threads. Always use the repository's `UiThread.Run` helper for UI updates; the legacy static `Application` API is obsolete:

```csharp
monitoredItem.Notification += (item, e) => {
Application.Invoke(() => {
UiThread.Run(() => {
// Safe to update UI here
label.Text = newValue;
});
Expand All @@ -221,17 +238,23 @@ Required packages:

## Common Pitfalls

1. **Tests fail with Xunit errors in main project** - Ensure `tests/**` is excluded in Opcilloscope.csproj
2. **UI thread exceptions** - Always use `Application.Invoke()` for UI updates from background threads
1. **Tests fail with Xunit errors in main project** - Ensure `Tests/**` is excluded in Opcilloscope.csproj
2. **UI thread exceptions** - Always use `UiThread.Run()` for UI updates from background threads
3. **Ambiguous NodeBrowser reference** - OPC Foundation has its own `Browser` class; use fully qualified names
4. **Certificate validation errors** - Set `AutoAcceptUntrustedCertificates = true` in SecurityConfiguration for development
4. **Certificate validation errors** - Fix or trust the server certificate using the path reported by the connection log, or bypass validation with `--insecure` for development only

## Security Notes

For development environments:
```csharp
config.SecurityConfiguration.AutoAcceptUntrustedCertificates = true;
```
An automatic/omitted or partial security profile requires a `SignAndEncrypt`
endpoint and selects the strongest matching candidate. Explicit
`SecurityMode=Sign` opts into signed-but-unencrypted traffic. Explicit
anonymous `SecurityMode=None` opts into unsecured plaintext; username
credentials never permit `None`.

Certificates that fail validation are rejected by default.
`opcilloscope --insecure` may be used for a development run only; it bypasses
certificate validation and never enables plaintext transport. Do not weaken
`SecurityConfiguration` in production code.

## Naming Conventions

Expand Down
54 changes: 37 additions & 17 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,55 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
# MinVer needs the full commit history (and tags) to compute versions.
fetch-depth: 0

- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: '10.0.x'
# Keep the SDK/runtime pack aligned with the reviewed RID locks and notices.
dotnet-version: '10.0.109'

- name: Restore dependencies
run: dotnet restore
run: |
dotnet restore Opcilloscope.csproj --locked-mode
dotnet restore Tests/Opcilloscope.Tests/Opcilloscope.Tests.csproj
dotnet restore Tests/Opcilloscope.E2ETests/Opcilloscope.E2ETests.csproj

- name: Validate third-party inventory
run: ./scripts/verify-third-party-inventory.sh

- name: Verify formatting
run: |
dotnet format Opcilloscope.sln --verify-no-changes --no-restore
dotnet format Tests/Opcilloscope.E2ETests/Opcilloscope.E2ETests.csproj --verify-no-changes --no-restore

- name: Build main solution
run: dotnet build Opcilloscope.sln --no-restore --configuration Release

- name: Build
run: dotnet build --no-restore --configuration Release
- name: Test unit, integration, and TUI components
run: dotnet test Tests/Opcilloscope.Tests/Opcilloscope.Tests.csproj --no-build --configuration Release --verbosity normal

- name: Test
run: dotnet test --no-build --configuration Release --verbosity normal
- name: Build Linux E2E harness
run: dotnet build Tests/Opcilloscope.E2ETests/Opcilloscope.E2ETests.csproj --no-restore --configuration Release

- name: Publish (smoke)
run: dotnet publish Opcilloscope.csproj -c Release -r linux-x64 -o ./publish
- name: Publish exact Linux artifact under test
run: dotnet publish Opcilloscope.csproj --no-restore -c Release -r linux-x64 -o ./publish -p:DebugType=none

- name: Smoke test published binary
- name: Verify single-file publish layout
run: |
chmod +x ./publish/opcilloscope
# Run --help under a pseudo-tty: this exercises Terminal.Gui's Application.Init
# (catching trim/startup regressions in the self-contained binary) and then exits
# via the --help path. `script -e` propagates the binary's exit code.
TERM=xterm script -qec "./publish/opcilloscope --help" /dev/null
code=$?
echo "Published binary exited with code $code"
test "$code" -eq 0
test -f ./publish/opcilloscope
test ! -e ./publish/libonigwrap.so
test ! -e ./publish/opcilloscope.dll
test ! -e ./publish/opcilloscope.pdb
file_count="$(find ./publish -maxdepth 1 -type f | wc -l)"
test "$file_count" -eq 1
./publish/opcilloscope --help

- name: Test published TUI over a real PTY
env:
OPCILLOSCOPE_BIN: ${{ github.workspace }}/publish/opcilloscope
run: dotnet test Tests/Opcilloscope.E2ETests/Opcilloscope.E2ETests.csproj --no-build --configuration Release --verbosity normal
5 changes: 2 additions & 3 deletions .github/workflows/claude-code-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
pull-requests: write
issues: read
id-token: write

steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 1

Expand All @@ -52,4 +52,3 @@ jobs:
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options
claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"'

9 changes: 4 additions & 5 deletions .github/workflows/claude.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ jobs:
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
contents: write
pull-requests: write
issues: write
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 1

Expand All @@ -47,4 +47,3 @@ jobs:
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options
# claude_args: '--allowed-tools Bash(gh pr:*)'

Loading
Loading