Skip to content

Add iOS on-device debugging support#4999

Merged
shai-almog merged 17 commits into
masterfrom
on-device-debug-ios
May 23, 2026
Merged

Add iOS on-device debugging support#4999
shai-almog merged 17 commits into
masterfrom
on-device-debug-ios

Conversation

@shai-almog
Copy link
Copy Markdown
Collaborator

@shai-almog shai-almog commented May 21, 2026

Summary

Adds a JDWP-compatible debugger for ParparVM-built iOS apps so jdb,
IntelliJ IDEA, VS Code, Eclipse, NetBeans — anything that speaks
JDWP — can attach to a real device or the iOS Simulator and set
breakpoints, walk the stack, inspect locals + fields + arrays,
invoke methods, and surface native stdout/stderr in the IDE console
on the running app.

End-user documentation is in docs/developer-guide/On-Device-Debugging.asciidoc.

Architecture

Three pieces, each independent:

  1. Translator instrumentation. When -Dcn1.onDeviceDebug=true is
    set, the ParparVM translator emits per-method side-tables (locals
    address arrays, variable names, line tables, per-class field
    offset tables, per-method invoke thunks
    ) and a cn1-symbols.txt
    sidecar (classes + their superclass, methods + isStatic, lines,
    local-vars, fields with JVM descriptors and JDWP access flags)
    that the desktop proxy uses for name resolution. Release builds
    are unaffected — gated by a CN1_ON_DEVICE_DEBUG preprocessor
    define and a separate filter that keeps java.io.* / java.net.*
    / java.nio.* / com.codename1.impl.* out of the invoke-thunk
    set (those packages have hand-written native shims that have
    drifted from the modern calling convention).

  2. Device runtime. Ports/iOSPort/nativeSources/cn1_debugger.{h,m}
    is compiled into debug builds only. It dials out to a desktop
    proxy over TCP, then services the wire protocol from a listener
    thread. The hot path in __CN1_DEBUG_INFO is one predictable
    load+branch when nothing is attached (__builtin_expect(cn1DebuggerActive, 0)).

    • Suspend / resume that yields the GC bit so a paused thread
      doesn't block collection.
    • dup2-based stdout/stderr capture that forwards lines as
      EVT_STDOUT_LINE / EVT_STDERR_LINE events.
    • A per-class field-offset registry filled at process load via
      __attribute__((constructor)) shims — used to answer
      CMD_GET_OBJECT_FIELDS by offsetof-walking the C struct.
    • A method-invocation dispatcher that queues the call on the
      suspended Java thread (so it runs in a valid tsd context),
      wraps the underlying call in a catch-all setjmp so uncaught
      throws round-trip back as a result.type='X' instead of
      longjmp-ing past suspendCurrent's cond_wait.
    • Array length / value read against the JavaArrayPrototype
      struct with element type-tag derived from primitiveSize plus
      the element class's clsName.
    • "Waiting for debugger" UIWindow overlay when
      CN1ProxyWaitForAttach=YES.
  3. Desktop proxy. Maven module maven/cn1-debug-proxy/ contains
    a minimum-viable JDWP server (JdwpServer) that translates our
    custom protocol to/from JDWP. Coverage:

    • VirtualMachine: Version, IDSizes, Capabilities[New], AllThreads,
      AllClasses[WithGeneric], TopLevelThreadGroups, ClassPaths,
      Suspend, Resume, Exit, Dispose.
    • ReferenceType: Signature, ClassLoader, Modifiers, Fields,
      FieldsWithGeneric, Methods, MethodsWithGeneric, SourceFile,
      Status, Interfaces, ClassObject.
    • ClassType: Superclass, InvokeMethod (static).
    • Method: LineTable, VariableTable[WithGeneric], Bytecodes (stub),
      IsObsolete.
    • ObjectReference: ReferenceType (incl. TYPE_TAG_ARRAY for
      arrays), GetValues (instance fields), InvokeMethod,
      IsCollected.
    • ArrayReference: Length, GetValues.
    • StringReference: Value.
    • ThreadReference: Name, Suspend, Resume, Status, ThreadGroup,
      Frames, FrameCount, SuspendCount.
    • ThreadGroupReference: Name, Parent, Children.
    • StackFrame: GetValues, ThisObject.
    • EventRequest: Set, Clear, ClearAllBreakpoints — full modifier
      parser covering all 12 JDWP modifier kinds (Count, Conditional,
      ThreadOnly, ClassOnly, ClassMatch, ClassExclude, LocationOnly,
      ExceptionOnly, FieldOnly, Step, InstanceOnly, SourceNameMatch).
    • Event.Composite: VM_START, BREAKPOINT, SINGLE_STEP, VM_DEATH.

    --trace-jdwp flag dumps every inbound JDWP command for diagnosing
    IDE-specific issues. Stack and locals caches invalidate on every
    suspend (BP_HIT / STEP_COMPLETE) and every resume so the IDE's
    Frames / Variables panels always reflect the current thread state.

Build hints (UX)

In common/codenameone_settings.properties:

codename1.arg.ios.onDeviceDebug=true
codename1.arg.ios.onDeviceDebug.proxyHost=127.0.0.1
codename1.arg.ios.onDeviceDebug.proxyPort=55333
# Optional: block the app at startup until the proxy is attached.
# codename1.arg.ios.onDeviceDebug.waitForAttach=true

For a physical device, set proxyHost to the laptop's LAN IP.

New Maven goals

  • mvn cn1:ios-on-device-debugging — autodetects the symbol sidecar,
    launches the proxy, and prints attach instructions.
  • mvn cn1:buildIosOnDeviceDebug — cloud-build target that forces the
    on-device-debug flag on. Routes through the debug iOS pipeline (a
    new ios-on-device-debug ant target maps to debug cert / ad-hoc
    provisioning).

IPhoneBuilder reads ios.onDeviceDebug and (a) threads
-Dcn1.onDeviceDebug=true into the translator JVM and (b) injects
CN1ProxyHost / CN1ProxyPort / CN1ProxyWaitForAttach and an ATS
exemption into Info.plist when the flag is set.

What works today

  • Class loading: the IDE sees every class in the build.
  • Line breakpoints in user code and in the Codename One framework
    (com.codename1.ui.*, com.codename1.io.*, etc.).
  • Step into / over / out (with full JDWP modifier-kind support so
    IntelliJ's default class excludes don't truncate the request).
  • Full stack walking, including Codename One framework frames.
  • Primitive local inspection (int/long/float/double/boolean/byte/char/short).
  • java.lang.String value inspection — strings show inline.
  • Object references — class name, identity, and field-by-field
    drill-down
    . The proxy walks the ParparVM struct layout to read
    instance fields directly at known offsets, so dump this /
    "Variables" shows the same fields you'd see in a simulator debug
    session.
  • Array inspectionObject[], int[], byte[], etc. report
    their length and per-index values, so expanding an ArrayList's
    elementData shows actual element references rather than an
    opaque pointer. Element type tags round-trip properly so
    byte[] and boolean[] don't get conflated.
  • Method invocation from the IDE — Evaluate Expression /
    print myForm.getTitle() /
    Display.getInstance().getCurrent().getTitle() all run on the
    suspended Java thread inside a catch-all try block.
  • Native device outputSystem.out.println, Log.p, and
    printf / NSLog are surfaced in the proxy console (and the IDE
    Run window when the proxy is launched as a run config) prefixed
    with [device].
  • Pause / resume from the IDE.

Known limitations (documented in the dev guide)

  • Method invocation on java.io.*, java.net.*, java.nio.*,
    com.codename1.impl.* is unsupported — the translator skips
    invoke-thunk generation for those packages because their
    hand-written native shims drift from the modern calling convention
    and would break the link.
  • Constructor invocation is unsupported.
  • Hot-swap is unsupported.
  • Local variable names depend on -g having been used at javac time
    — Codename One archetypes set this by default.
  • Static field reads are unsupported (instance fields are).
  • NSLog on a real device may bypass the stdout forwarder (it routes
    to os_log which doesn't always mirror to stderr); printf /
    fprintf / Log.p are unaffected.

Performance

Release builds: zero overhead (no listener, no metadata, no per-line
callback, no invoke thunks, no field tables — guarded by
CN1_ON_DEVICE_DEBUG).

Debug builds, no debugger attached: one predictable load+branch per
source line (the existing __CN1_DEBUG_INFO for stack-trace line
recording is already there; the on-device-debug flag check is the
one added branch).

Debug builds, debugger attached: ~2-3× slowdown in tight numeric
loops, consistent with -g overhead on other native VMs.

Verification

End-to-end smoke test against an iPhone 17 Pro / iOS 26.3 simulator
under Xcode 26 with a minimal CN1 app:

  • BP at com.example.DebugApp.heartbeat:41 (tickCount++) fires
    every second; dump this returns { tickCount: 4, pointerCount: 0 }
    with the actual field values.
  • BP at com.codename1.ui.Display.edtLoopImpl fires on every EDT
    cycle; list renders the live Display.java source; named
    locals (current, qt, ie) populate.
  • F8 step-over from Log.p lands at the next executable line;
    F7 step-into lands at line 242 of com.codename1.io.Log.p.
  • Evaluate Expression com.codename1.ui.Display.getInstance().getCurrent().getTitle()
    returns the live form's title string.
  • Expanding an ArrayList in the Variables view shows
    elementData[0..size-1] with their actual values.
  • System.out.println, Log.p, and NSLog lines appear in the
    proxy's console prefixed with [device].

Companion BuildDaemon PR mirrors the cloud-build binding:
https://github.com/codenameone/BuildDaemon (separate PR).

Test plan

  • mvn -Plocal-dev-javase install still succeeds with the new
    cn1-debug-proxy module registered.
  • Release iOS builds are byte-for-byte unchanged when
    ios.onDeviceDebug is unset.
  • Repeat the simulator + IntelliJ smoke test on a fresh
    archetype project; confirm Frames panel, Variables panel,
    Evaluate Expression, and array drill-down all populate.
  • Cloud buildIosOnDeviceDebug round-trip — resulting .ipa
    connects to the proxy from a tethered device.
  • Dev guide renders cleanly in the asciidoctor build (vale
    clean).

Adds a JDWP-compatible debugger for ParparVM-built iOS apps so jdb /
IntelliJ / VS Code can attach to a real device or the iOS Simulator
and set breakpoints, walk the stack, and inspect locals + Strings.

Three pieces:
- ParparVM translator emits per-method side-tables (locals addresses,
  variable names, line tables) and a cn1-symbols.txt sidecar when
  -Dcn1.onDeviceDebug=true is set. Release builds are unaffected.
- A listener thread (Ports/iOSPort/nativeSources/cn1_debugger.{h,m})
  is compiled into debug builds, dials out to a desktop proxy over
  TCP, and services set/clear-bp, resume, step, get-stack/locals,
  get-object-class, and get-string commands. The hot path in
  __CN1_DEBUG_INFO is one predictable load+branch when nothing is
  attached.
- A new Maven module (cn1-debug-proxy) bridges that custom protocol
  to JDWP so any standard Java debugger speaks to it. Includes a
  minimum-viable JDWP implementation covering everything jdb needs
  for breakpoint, where, locals, and String inspection.

Maven goals: cn1:ios-on-device-debugging (launches the proxy) and
cn1:buildIosOnDeviceDebug (cloud build target).

Build-hint UX: codename1.arg.ios.onDeviceDebug=true plus
proxyHost/proxyPort. End-user docs live in
docs/developer-guide/On-Device-Debugging.asciidoc.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 21, 2026

Developer Guide build artifacts are available for download from this workflow run:

Developer Guide quality checks:

  • AsciiDoc linter: No issues found (report)
  • Vale: No alerts found (report)
  • Paragraph capitalization: No paragraph capitalization issues (report)
  • LanguageTool: No grammar matches (report)
  • Image references: No unused images detected (report)

@github-actions
Copy link
Copy Markdown
Contributor

Cloudflare Preview

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 21, 2026

Compared 20 screenshots: 20 matched.
✅ JavaScript-port screenshot tests passed.

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 21, 2026

Compared 110 screenshots: 110 matched.

Native Android coverage

  • 📊 Line coverage: 11.91% (6761/56788 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 9.67% (34016/351672), branch 4.18% (1396/33359), complexity 5.21% (1666/32007), method 9.04% (1354/14973), class 14.72% (302/2051)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6327 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.ClassReader – 0.00% (0/1519 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1148 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.MethodWriter – 0.00% (0/923 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/730 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/623 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.Frame – 0.00% (0/564 lines covered)
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysJvmKt – 0.00% (0/495 lines covered)
      • kotlinx.coroutines.kotlinx.coroutines.JobSupport – 0.00% (0/423 lines covered)

✅ Native Android screenshot tests passed.

Native Android coverage

  • 📊 Line coverage: 11.91% (6761/56788 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 9.67% (34016/351672), branch 4.18% (1396/33359), complexity 5.21% (1666/32007), method 9.04% (1354/14973), class 14.72% (302/2051)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6327 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.ClassReader – 0.00% (0/1519 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1148 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.MethodWriter – 0.00% (0/923 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/730 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/623 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.Frame – 0.00% (0/564 lines covered)
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysJvmKt – 0.00% (0/495 lines covered)
      • kotlinx.coroutines.kotlinx.coroutines.JobSupport – 0.00% (0/423 lines covered)

Benchmark Results

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 1248.000 ms
Base64 CN1 encode 232.000 ms
Base64 encode ratio (CN1/native) 0.186x (81.4% faster)
Base64 native decode 1322.000 ms
Base64 CN1 decode 351.000 ms
Base64 decode ratio (CN1/native) 0.266x (73.4% faster)
Image encode benchmark status skipped (SIMD unsupported)

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 21, 2026

Compared 109 screenshots: 109 matched.
✅ Native iOS Metal screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 331 seconds

Build and Run Timing

Metric Duration
Simulator Boot 94000 ms
Simulator Boot (Run) 1000 ms
App Install 11000 ms
App Launch 4000 ms
Test Execution 267000 ms

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 732.000 ms
Base64 CN1 encode 1969.000 ms
Base64 encode ratio (CN1/native) 2.690x (169.0% slower)
Base64 native decode 352.000 ms
Base64 CN1 decode 1246.000 ms
Base64 decode ratio (CN1/native) 3.540x (254.0% slower)
Base64 SIMD encode 584.000 ms
Base64 encode ratio (SIMD/native) 0.798x (20.2% faster)
Base64 encode ratio (SIMD/CN1) 0.297x (70.3% faster)
Base64 SIMD decode 377.000 ms
Base64 decode ratio (SIMD/native) 1.071x (7.1% slower)
Base64 decode ratio (SIMD/CN1) 0.303x (69.7% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 121.000 ms
Image createMask (SIMD on) 22.000 ms
Image createMask ratio (SIMD on/off) 0.182x (81.8% faster)
Image applyMask (SIMD off) 241.000 ms
Image applyMask (SIMD on) 104.000 ms
Image applyMask ratio (SIMD on/off) 0.432x (56.8% faster)
Image modifyAlpha (SIMD off) 275.000 ms
Image modifyAlpha (SIMD on) 170.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.618x (38.2% faster)
Image modifyAlpha removeColor (SIMD off) 301.000 ms
Image modifyAlpha removeColor (SIMD on) 105.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.349x (65.1% faster)
Image PNG encode (SIMD off) 2567.000 ms
Image PNG encode (SIMD on) 1088.000 ms
Image PNG encode ratio (SIMD on/off) 0.424x (57.6% faster)
Image JPEG encode 824.000 ms

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 21, 2026

✅ ByteCodeTranslator Quality Report

Test & Coverage

  • Tests: 647 total, 0 failed, 2 skipped

Benchmark Results

  • Execution Time: 10805 ms

  • Hotspots (Top 20 sampled methods):

    • 20.55% java.lang.String.indexOf (381 samples)
    • 19.90% com.codename1.tools.translator.Parser.isMethodUsed (369 samples)
    • 17.85% java.util.ArrayList.indexOf (331 samples)
    • 5.23% com.codename1.tools.translator.BytecodeMethod.addToConstantPool (97 samples)
    • 5.18% java.lang.Object.hashCode (96 samples)
    • 2.64% java.lang.System.identityHashCode (49 samples)
    • 2.21% com.codename1.tools.translator.ByteCodeClass.calcUsedByNative (41 samples)
    • 1.83% com.codename1.tools.translator.BytecodeMethod.optimize (34 samples)
    • 1.67% com.codename1.tools.translator.ByteCodeClass.updateAllDependencies (31 samples)
    • 1.51% com.codename1.tools.translator.ByteCodeClass.markDependent (28 samples)
    • 1.46% com.codename1.tools.translator.BytecodeMethod.equals (27 samples)
    • 1.29% com.codename1.tools.translator.Parser.generateClassAndMethodIndexHeader (24 samples)
    • 1.24% com.codename1.tools.translator.Parser.getClassByName (23 samples)
    • 1.19% java.lang.StringBuilder.append (22 samples)
    • 1.08% java.lang.StringCoding.encode (20 samples)
    • 0.70% com.codename1.tools.translator.BytecodeMethod.appendMethodSignatureSuffixFromDesc (13 samples)
    • 0.70% com.codename1.tools.translator.Parser.cullMethods (13 samples)
    • 0.70% com.codename1.tools.translator.BytecodeMethod.appendCMethodPrefix (13 samples)
    • 0.65% com.codename1.tools.translator.BytecodeMethod.isMethodUsedByNative (12 samples)
    • 0.54% com.codename1.tools.translator.Parser.writeOutput (10 samples)
  • ⚠️ Coverage report not generated.

Static Analysis

  • ✅ SpotBugs: no findings (report was not generated by the build).
  • ⚠️ PMD report not generated.
  • ⚠️ Checkstyle report not generated.

Generated automatically by the PR CI workflow.

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 21, 2026

Compared 110 screenshots: 110 matched.
✅ Native iOS screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 199 seconds

Build and Run Timing

Metric Duration
Simulator Boot 60000 ms
Simulator Boot (Run) 2000 ms
App Install 15000 ms
App Launch 4000 ms
Test Execution 340000 ms

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 1420.000 ms
Base64 CN1 encode 3146.000 ms
Base64 encode ratio (CN1/native) 2.215x (121.5% slower)
Base64 native decode 659.000 ms
Base64 CN1 decode 2776.000 ms
Base64 decode ratio (CN1/native) 4.212x (321.2% slower)
Base64 SIMD encode 958.000 ms
Base64 encode ratio (SIMD/native) 0.675x (32.5% faster)
Base64 encode ratio (SIMD/CN1) 0.305x (69.5% faster)
Base64 SIMD decode 738.000 ms
Base64 decode ratio (SIMD/native) 1.120x (12.0% slower)
Base64 decode ratio (SIMD/CN1) 0.266x (73.4% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 72.000 ms
Image createMask (SIMD on) 12.000 ms
Image createMask ratio (SIMD on/off) 0.167x (83.3% faster)
Image applyMask (SIMD off) 201.000 ms
Image applyMask (SIMD on) 106.000 ms
Image applyMask ratio (SIMD on/off) 0.527x (47.3% faster)
Image modifyAlpha (SIMD off) 284.000 ms
Image modifyAlpha (SIMD on) 232.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.817x (18.3% faster)
Image modifyAlpha removeColor (SIMD off) 400.000 ms
Image modifyAlpha removeColor (SIMD on) 120.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.300x (70.0% faster)
Image PNG encode (SIMD off) 1785.000 ms
Image PNG encode (SIMD on) 1087.000 ms
Image PNG encode ratio (SIMD on/off) 0.609x (39.1% faster)
Image JPEG encode 592.000 ms

- Force-off ios.onDeviceDebug on release builds (ios.buildType=release)
  in both the translator JVM flag and the Info.plist injection, so a
  stray hint in codenameone_settings.properties can't leak the debug
  listener thread into an App Store binary.
- Document the new hints (ios.onDeviceDebug, .proxyHost, .proxyPort,
  .waitForAttach) in the iOS build hints table in
  Advanced-Topics-Under-The-Hood.asciidoc.
- Drop unused Parser.getClasses() that triggered MS_EXPOSE_REP.
- Rework the dev-guide chapter: remove the {cn1-release-version}
  sentence from Prerequisites, drop the "macOS with Xcode required"
  claim (the cloud build path works equally), drop the redundant
  JDWP-debugger line, collapse the duplicated build instructions
  into one step that points at the normal build flow, switch to
  build-hint vocabulary, and strip the codename1.arg. prefixes from
  the user-facing hint names.
- Fix Vale prose-linter regressions (contractions, first-person,
  Latinisms).
# Conflicts:
#	maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 21, 2026

✅ Continuous Quality Report

Test & Coverage

Static Analysis

  • SpotBugs [Report archive]
    • ByteCodeTranslator: 0 findings (no issues)
    • android: 0 findings (no issues)
    • codenameone-maven-plugin: 0 findings (no issues)
    • core-unittests: 0 findings (no issues)
    • ios: 0 findings (no issues)
  • PMD: 0 findings (no issues) [Report archive]
  • Checkstyle: 0 findings (no issues) [Report archive]

Generated automatically by the PR CI workflow.

shai-almog added 13 commits May 22, 2026 08:52
Quality-of-life improvements that emerged while running the proxy end-to-end
locally against the iOS simulator.

Device-side runtime (cn1_debugger.m + .h):
  - cn1_debugger_start() no longer blocks the AppDelegate on
    didFinishLaunchingWithOptions. The proxy connection runs on its own
    thread regardless of CN1ProxyWaitForAttach, so UIKit can finish boot,
    draw the launch transition, and -- when waitForAttach is on -- present
    a translucent "Waiting for debugger..." overlay UIWindow. The previous
    behaviour left the user staring at the splash with no signal that the
    app was waiting on anything.
  - New cn1_debugger_run_when_ready(block) API lets the AppDelegate defer
    the VM start callback until the proxy reports the IDE has attached.
    When waitForAttach is off (or on-device-debug is disabled at build
    time) the block runs synchronously and behaves identically to the
    pre-change boot flow.

GLAppDelegate:
  - Calls cn1_debugger_run_when_ready around the VM callback so wait-mode
    no longer races against splash dismissal, and captures the location
    launch option into the block so it survives the deferral.

JDWP proxy (JdwpServer.java):
  - acceptAndServe() now loops on accept() so the developer can detach and
    reattach the IDE without restarting the proxy. Per-attach state is
    reset via closeJdwpSession(); breakpoint registrations persist across
    attaches.
  - After handshake completes the proxy schedules an auto-resume that
    releases the device-side waitForAttach gate 500 ms later. The delay
    gives IntelliJ / VS Code time to register breakpoints before the app
    races past them; without this the app sat on the waiting overlay
    forever because most JDWP debuggers don't auto-send VM.Resume.

Misc:
  - Add /artifacts/ to .gitignore (build wrapper drop-zone used by the
    new ios-on-device-debugging mojo).
Three additions that turn the proxy from "stack-trace viewer" into a
real interactive debugger.

1. Instance-field inspection
   Translator: ByteCodeClass.appendOnDeviceDebugFieldTable emits a
   per-class field-offset table (fieldId, offsetof, JVM type char,
   name) in each generated .m file, behind a CN1_ON_DEVICE_DEBUG guard.
   An __attribute__((constructor)) shim registers the table with
   cn1_debugger at process load. Parser.writeSymbolSidecar also emits
   `field <classId> <fieldId> <name> <descriptor> <accessFlags>` rows
   so the proxy can answer JDWP ClassType.Fields / FieldsWithGeneric
   without a device round-trip.
   Device: new CMD_GET_OBJECT_FIELDS handler walks the registered
   field table for the object's runtime classId and reads each field
   straight out of the struct using offsetof. Replies as
   EVT_OBJECT_FIELDS (count, then [type-char, value] tuples).
   Proxy: JdwpServer.handleObject case 2 (GetValues), handleStackFrame
   case 3 (ThisObject), and ClassType cases 4/14 (Fields,
   FieldsWithGeneric) now read real data instead of returning empty
   stubs. ThisObject piggybacks on the existing GetLocals path,
   reading slot 0 as an object reference for instance methods (the
   JVM always parks `this` there on entry); for statics slot 0 is
   zero, which is the correct JDWP reply.
   `dump this` in jdb against a running ParparVM-built iOS app now
   shows `tickCount: 4, pointerCount: 0` etc. — actual field values.

2. Native stdout / stderr forwarding
   cn1_debugger_start dup2()'s STDOUT_FILENO and STDERR_FILENO to
   pipes, then runs two streamCaptureThreads that chunk the pipes by
   newline. Each completed line is mirrored back to the original FD
   (so xcrun simctl log / Xcode console still works) and, if a proxy
   is connected, sent as EVT_STDOUT_LINE / EVT_STDERR_LINE.
   Proxy: handles the events and prints them prefixed with `[device]`
   to its own stdout (IntelliJ surfaces this in the proxy's debug
   console when it's launched as an IDE run config).
   System.out.println, Log.p, printf, fprintf(stderr,...) all flow
   through. NSLog on the iOS Simulator works too; on a real device
   NSLog may bypass stderr (documented as a limit).

3. CN1 core class breakpoints
   Confirmed working — sidecar already covers framework classes the
   same way it covers user code. The missing piece was just docs:
   On-Device-Debugging.asciidoc now describes how to attach the
   CodenameOne/src source directory in IntelliJ / jdb so the source
   pane resolves while stepping through framework code. Also
   tightened the "What works today" list and added a fresh "Known
   limitations" entry for static-field reads, plus a note that NSLog
   on a real device may bypass the stdout forwarder.

Wire protocol additions:
  CMD_GET_OBJECT_FIELDS (0x0D), EVT_OBJECT_FIELDS (0x8A),
  EVT_STDOUT_LINE (0x8B), EVT_STDERR_LINE (0x8C).
SymbolTable additions:
  FieldInfo, ClassInfo.instanceFields, fieldById, fieldCount.

Verified end-to-end on iPhone 17 Pro / iOS 26.3 simulator under
Xcode 26 against a minimal CN1 app: BP in framework's
Display.edtLoopImpl fires, list shows the actual source, locals
populate with their real names, and `dump this` walks the
instance-field table.
Real bug found while testing stepping in IntelliJ: jdb's `next`
(step-over) returned `JDWP Error: 103` because the EventRequest.Set
modifier-loop walked past the end of the payload when JDI sent its
default ClassExclude modifiers (`java.*`, `javax.*`, `sun.*`,
`com.sun.*`, `jdk.internal.*`, etc. — JDI auto-attaches these to
every step request) and our switch's `default` branch set
`off = p.length`, then the next iteration read `p[p.length]` and
ArrayIndexOutOfBounds'd. The IDE's view: step request rejected → no
step event → app keeps running → looks indistinguishable from
"continue".

Fix: every modifier-case now bounds-checks before reading and bails
the loop via a `badModifier` flag if anything's off. Added the
missing modifier kinds JDI emits but we hadn't seen on the wire
yet (`FieldOnly`=9, `SourceNameMatch`=12), and changed the unknown-
kind branch to abort the loop with a `[jdwp]` log instead of trying
to guess the width.

Also fixed a related NPE: when the IDE detached mid-session, the
device-disconnect path tried to send VM_DEATH on an already-null
out stream and crashed the listener thread. writeEventCommand now
no-ops when out is null.

Added `[jdwp] STEP request` / `STEP_COMPLETE` / `VM.Resume` /
`Thread.Resume` log lines so future debugging of stepping is just
a matter of reading the proxy console.

Docs: documented the IntelliJ run-config trick for surfacing
device output ([device] lines) in the IDE console — launch the
proxy as an Application configuration alongside the Remote JVM
Debug attach and group them with a Compound run config. Without
this, the proxy's stdout (where device prints end up) only shows
up if the user runs the proxy from a terminal.

Verified end-to-end on iPhone 17 Pro / iOS 26.3 sim:
  - jdb's `next` from a BP at heartbeat line 41 lands on line 42
    (STEP_COMPLETE logged).
  - jdb's `step` from a BP at line 42 enters the Log.p method
    (STEP_COMPLETE for methodId=13294, line=242).
  - Proxy survives an IDE detach and accepts the next reattach.
The IDE can now call any framework / user method on a paused VM and
have the result come back as a real object reference (or a value, or
a thrown Throwable). This is what makes `print
Display.getInstance().getCurrent().getTitle()` work in jdb, and
what makes IntelliJ's "Evaluate Expression" pop-up usable for chains
that involve method calls.

Translator (BytecodeMethod / ByteCodeClass / Parser):
  - Emits one C "invoke thunk" per non-eliminated method, per class,
    under CN1_ON_DEVICE_DEBUG. The thunk has a uniform signature
    (tsd, this, args, *result), unpacks the args from a union into
    the typed C parameters the translated method expects, dispatches
    through virtual_<sym>() / <sym>() depending on virtuality, and
    packs the return value back into the result union. The call is
    wrapped in a catch-all try block so an uncaught Throwable
    round-trips as result.type='X' instead of longjmp-ing past
    suspendCurrent's cond_wait.
  - Skips classes whose hand-written native impl has fallen out of
    sync with the translator's calling convention: java.io.*,
    java.net.*, java.nio.*, com.codename1.impl.*. The other system
    packages (java.lang, java.util, ...) are fine because their
    native impls are in nativeMethods.m and use the modern names.
  - When on-device-debug is on, the unused-method optimiser keeps
    every instance method of java.lang.Object alive — jdb's `print`
    formats every object through Object.toString, so silently
    dropping it earlier made every evaluation return "<void value>".
  - Sidecar `method` rows now carry an isStatic flag; `class` rows
    carry their superclass id so the proxy can answer
    ClassType.Superclass and let JDI walk to inherited methods.

Device runtime (cn1_debugger.h/m):
  - New cn1_invoke_arg union + cn1_invoke_result struct (JVM type-
    char plus value slot), and a cn1_invoke_thunk_t function-pointer
    type that the translator-emitted thunks all match.
  - cn1_debugger_register_invoke_thunk(methodId, thunk) registry,
    array-indexed by methodId for O(1) lookup.
  - New CMD_INVOKE_METHOD handler. It queues the call on the
    target thread's sus_state, signals s->cv, and blocks the
    listener thread on a result-ready predicate. suspendCurrent's
    cond_wait loop services the request on the suspended Java
    thread (so the call runs in a valid tsd / GC context), then
    goes back to waiting.
  - New EVT_INVOKE_RESULT response carrying (type-char, 8-byte value).

Proxy (WireProtocol / DeviceConnection / SymbolTable / JdwpServer):
  - WireProtocol: CMD_INVOKE_METHOD=0x0E, EVT_INVOKE_RESULT=0x8D.
  - DeviceConnection: invokeMethod(threadId, methodId, thisObj,
    argTypes[], argValues[]) and onInvokeResult(type, value)
    callback wired through the listener.
  - SymbolTable: MethodInfo.isStatic, ClassInfo.superId, and
    extended `method`/`class` row parsing (still tolerates older
    4-column rows).
  - JdwpServer:
      * ClassType.InvokeMethod (cmd 3) and
        ObjectReference.InvokeMethod (cmd 6) parse the JDWP args,
        forward as a CMD_INVOKE_METHOD, and pack the device's
        EVT_INVOKE_RESULT into a JDWP returnValue + exception slot.
      * ClassType.Superclass now returns the actual sidecar superId
        so JDI walks the hierarchy properly instead of stopping at
        every class.
      * Methods / MethodsWithGeneric set the JDWP STATIC bit when
        the sidecar marks the method static — without it jdb's
        expression parser refuses to resolve `Class.method()`.

Verified end-to-end on iPhone 17 Pro / iOS 26.3 simulator. Single-
invoke and chained:

  print com.codename1.ui.Display.getInstance()              -> Display ref
  print Display.getInstance().getCurrent()                  -> Form ref
  print Display.getInstance().getCurrent().getTitle()       -> String "hello, world"

Object.toString round-trips through nativeMethods.m, so jdb's
default object-display formatting also works.
…sues

When IntelliJ's step behaviour diverges from jdb's (different default
class-exclude modifiers, additional pre-step queries, etc.), the proxy
needs a way to surface every JDWP command it receives so the wire-
level difference is visible without rebuilding. Add a --trace-jdwp
flag that toggles a single-line log per inbound command (cmd-set /
cmd / id / payload length).

Off by default — release sessions shouldn't pay the log overhead.
JDWP modifier-kind values per the spec:
  1 Count                 2 ConditionalExpr (deprecated)
  3 ThreadOnly            4 ClassOnly
  5 ClassMatch            6 ClassExclude
  7 LocationOnly          8 ExceptionOnly
  9 FieldOnly            10 Step
 11 InstanceOnly         12 SourceNameMatch

The previous switch had every kind shifted by one — 2 was treated as
ThreadOnly (really Conditional, deprecated), 3 as ClassOnly (really
ThreadOnly), and the string-payload kinds were 4/5 instead of 5/6.
The practical bite: IntelliJ auto-attaches a handful of ClassExclude
modifiers (`java.*`, `javax.*`, `sun.*`, `com.sun.*`, `jdk.internal.*`)
to every StepRequest. With kind=6 unrecognised the parser bailed
mid-payload, sent the modKind=6 warning to the proxy log, and
truncated the modifier list — IntelliJ then either retries the step
or shows "Source code does not match the bytecode" depending on
exactly which modifiers it expected to be honoured.

Caught from a JDWP packet trace where IntelliJ sent a 561-byte
EventRequest.Set with 27 modifiers; the proxy logged
"unknown modKind=6 — ignoring remaining 26 modifiers" right before
each step.

Also added an explicit case for modKind=2 (Conditional, deprecated)
even though no current debugger sends it, so the parser handles
every value in the spec without falling to the default branch.
Frames panel was sticking to the previous suspend location after each
step because fetchStackForThread short-circuits on a populated cache.
onBreakpointHit eagerly fetches the stack and populates that cache;
onStepComplete had no eager fetch but also didn't invalidate, so when
IntelliJ asked for Frames after a step it got back the BP_HIT stack —
e.g. user sets BP on line 48, presses F8 once, the cursor / Frames /
double-click all still point to line 48 while the device is actually
suspended on line 49.

Same bug affected locals (StackFrame.GetValues short-circuits via
its own pendingLocals cache, kept across suspensions of the same
frameIdx) — the IDE evaluated expressions against a stale frame.

Fix: introduce invalidateStack() / invalidateLocals() helpers and
call them in every state-changing handler:
  - onBreakpointHit  (before the eager fetch)
  - onStepComplete   (no eager fetch — IDE will pull)
  - VM.Resume        (everything cached is now meaningless)
  - Thread.Resume    (same)

End result: Frames panel and Variables panel both refresh after every
step / resume cycle without the IDE having to explicitly re-suspend.
ArrayList's `elementData` (and any other Object[] / int[] / etc.) was
showing as a single ref in the Variables view with no children — the
proxy stubbed both ArrayReference commands with NOT_IMPLEMENTED, so
IntelliJ couldn't expand the array.

Device side (cn1_debugger.m):
  - CMD_GET_ARRAY_LENGTH reads ((JAVA_ARRAY)obj)->length.
  - CMD_GET_ARRAY_VALUES walks ->data with element width chosen from
    primitiveSize + the element class's clsName (so byte/boolean
    arrays don't get conflated, neither do int/float and long/double).
    Element bytes go on the wire as packed big-endian — same layout
    JDWP expects for ArrayRegion primitive payloads.
  - CMD_GET_OBJECT_CLASS now reports the class struct's `isArray`
    flag as a trailing byte. Older proxies that only read 4 bytes
    still work.

Wire protocol additions: CMD_GET_ARRAY_LENGTH (0x0F),
CMD_GET_ARRAY_VALUES (0x10), EVT_ARRAY_LENGTH (0x8E),
EVT_ARRAY_VALUES (0x8F).

Proxy:
  - JdwpServer.handleArray wires JDWP ArrayReference.Length (cmd 1)
    and GetValues (cmd 2) to the device commands above. For object
    arrays the GetValues reply tags each element 'L' inline (JDWP's
    ArrayRegion encoding for non-primitive arrays); for primitives
    we forward the packed bytes verbatim.
  - DeviceConnection.onObjectClass now carries an isArray flag from
    the device. JdwpServer's ObjectReference.ReferenceType uses
    TYPE_TAG_ARRAY (3) for array instances so IntelliJ knows to
    issue ArrayReference commands rather than treating the object
    as a class instance and giving up.

Verified: dropping a BP in user code, expanding an ArrayList in the
Variables view now shows `elementData[0..size-1]` with values, not
just an opaque object reference.
Update the "What works today" list:
  - Add a bullet for array inspection (length + per-index values for
    Object[] and primitive arrays, including ArrayList.elementData
    drilldown).
  - Expand the method-invocation bullet to mention the
    Display.getInstance().getCurrent().getTitle() chain that motivated
    the implementation, plus the catch-all try block that keeps an
    uncaught throw from tearing down the session.

Vale: move punctuation inside the quoted phrase and rephrase the
first-person plural in the troubleshooting section.
After merging master (which promoted LanguageTool to a hard quality
gate via #5007) the developer guide had 7 grammar-checker matches
attributable to this branch's chapter plus 1 pre-existing match
elsewhere. This drives the count back to 0.

Chapter-local fixes (docs/developer-guide/On-Device-Debugging.asciidoc):
  - Replace "source dir" with "source directory" so LanguageTool's
    morfologik English dictionary stops flagging the abbreviation.
  - Rephrase the "Watching expressions follow the same rule" sentence
    so QUESTION_MARK no longer fires (LanguageTool reads the "ing"
    opening as a rhetorical fragment expecting a question mark).
  - Rephrase the "What doesn't work:" lead so the wh-word doesn't
    trip QUESTION_MARK on the surrounding sentence.

Accept-list additions (docs/developer-guide/languagetool-accept.txt):
  - jdb (the JDK command-line debugger),
  - loopback (the networking term),
  - rethrow / rethrows (standard exception-handling vocabulary).
  These are well-known technical terms LanguageTool's default
  dictionary doesn't recognise but appear unaltered in any
  reasonable developer-facing prose.

Pre-existing match (docs/developer-guide/Maven-Getting-Started.adoc):
  Replace "(aka X. aka Y)" with "(also known as X or Y)" — the
  period between the two "aka"s made LanguageTool flag the second
  "aka" as a sentence-start lowercase letter. Master's docs job is
  PR-trigger-only so this never showed up there; it surfaced here
  because the merge brought in the gate.
Wire on-device debugging into the cn1app-archetype so a fresh project
already has everything needed for the IDE-driven flow, not just the
command-line one:

- common/codenameone_settings.properties carries the four
  `ios.onDeviceDebug.*` build hints commented out, with a one-line
  pointer to the developer guide. Discoverable from the file every
  Codename One developer already edits.
- archetype-resources/.idea/runConfigurations/CN1_Debug_Proxy.xml is
  a Maven run config that invokes `codenameone:ios-on-device-debugging`
  from the project root — same proxy, same defaults, no path
  hardcoding.
- archetype-resources/.idea/runConfigurations/CN1_Attach_iOS.xml is a
  Remote JVM Debug config attaching to `localhost:8000`, scoped to
  the `${rootArtifactId}-common` module so IntelliJ's source
  resolution lines up with the user's code.
- .gitignore un-ignores the archetype's .idea/ subtree so these
  configs stay versioned (it still ignores everyone else's local
  .idea/).

Developer guide:
- Re-orders so the IntelliJ Quick start leads, the Maven /
  command-line + jdb flow becomes a smaller section after it. The
  text now explicitly says to **Run** (not Debug) the proxy config
  to avoid accidentally attaching IntelliJ's own debugger to the
  proxy process.
- Renames "iOS Simulator" to "native iOS simulator" wherever the
  Codename One simulator could otherwise be implied.
- Rewrites the "Working from a clone of this repository" bullet —
  promotes the Maven sources-jar approach (the path almost every
  reader will take), explains the cloned-CodenameOne-repo alternative
  by name, and links to the GitHub repository instead of implying
  the reader is already inside it.

Misc:
- `cn1_debugger.h` now wraps the `cn1_globals.h` include inside the
  `CN1_ON_DEVICE_DEBUG` ifdef so release builds don't pull in the
  ParparVM globals header at all.
- Single Microsoft.Auto vale warning fixed ("auto-resume" →
  "resume automatically").

The build-hints table already carried entries for
`ios.onDeviceDebug` and the three companion hints; updated one
parenthetical to say "native iOS simulator" for consistency.
The previous commit added the two run-config XMLs under
archetype-resources/.idea/runConfigurations/ but git silently dropped
them because of a second `.idea/` line further down in .gitignore that
re-applied the ignore after my un-ignore exception. Move the exception
below both ignore rules so the files actually land in the tree.
@shai-almog shai-almog merged commit c3d11d0 into master May 23, 2026
27 of 28 checks passed
shai-almog added a commit that referenced this pull request May 23, 2026
* Add Android on-device debugging support

Mirrors the iOS on-device-debug flow from #4999 for Android, taking
advantage of the fact that Dalvik/ART already speaks JDWP — no
desktop proxy needed. The Codename One Maven plugin orchestrates adb:
install the APK, mark it as the debug-app, launch the Activity, poll
for the PID, forward JDWP onto localhost, and tail logcat. The
existing IntelliJ workflow extends: there is now a CN1 Android
On-Device Debug + CN1 Attach Android pair under the same On-Device
Debug folder as the iOS configs.

Pieces:
- AndroidGradleBuilder parses a new android.onDeviceDebug build hint
  that flips the generated manifest to debuggable=true and disables
  R8/proguard so symbols and locals survive the build. Release builds
  (anything without the hint) are unaffected.
- cn1:android-on-device-debugging — new Mojo that drives the adb
  session end-to-end, with autodetection of adb from ANDROID_HOME /
  ANDROID_SDK_ROOT / standard Studio SDK paths / $PATH, optional
  adb connect <ip:port> for wireless devices (Android 11+
  adb pair / adb connect and legacy adb tcpip both covered), APK
  autodetection from target/, am set-debug-app -w for wait-for-
  attach, and a logcat stream prefixed [device] for the lifetime of
  the session. Cleans up adb forward on shutdown.
- cn1:buildAndroidOnDeviceDebug — wrapper that force-sets the hint
  and invokes the existing android-device cloud build, so the IDE
  menu has a one-click entry that doesn't depend on the project's
  codenameone_settings.properties.

Archetype:
- common/codenameone_settings.properties carries the new hint
  commented out, next to the existing ios.onDeviceDebug.* lines.
- .idea/runConfigurations/CN1_Android_OnDeviceDebug.xml — Maven run
  config that invokes the new Mojo from the project root.
- .idea/runConfigurations/CN1_Attach_Android.xml — Remote JVM Debug
  to localhost:5005, scoped to the -common module.

Developer guide:
- New On-Device-Debugging-Android.asciidoc chapter: IntelliJ quick
  start, wireless debugging (both Android 11+ and legacy paths),
  Maven CLI flow, flag table, source-resolution for both
  codenameone-core and codenameone-android sources jars, and
  troubleshooting. Explicitly calls out that JNI C/C++ is out of
  scope (use Android Studio + LLDB for that, can run alongside).
- Advanced-Topics-Under-The-Hood gets an android.onDeviceDebug
  entry next to the existing android.debug / android.release rows.

* Address CI feedback on Android on-device debugging

Three gates failed on the original push:

- SpotBugs DE_MIGHT_IGNORE in AndroidOnDeviceDebuggingMojo's shutdown
  hook (catch-all swallowed every Exception silently). Narrow the
  catch to MojoFailureException — the only checked exception runAdb
  can throw — and surface the message to stderr so a stuck adb
  forward is at least visible at shutdown. Process.destroy() doesn't
  declare a checked exception, so the surrounding try/catch is no
  longer needed for that call.
- Vale (Microsoft.Contractions + Microsoft.Adverbs) on
  On-Device-Debugging-Android.adoc: "it is" → "it's"; drop "rarely"
  by rewriting the command-line-flags lead sentence; drop "silently"
  from the breakpoint-not-firing troubleshooter.
- LanguageTool typo flags: "codepath" → "code path" in the
  waitForAttach=false note; allowlist "adb", "logcat", and "pidof"
  in languagetool-accept.txt under a new "Android tooling" section
  so future Android docs don't re-trip the same rule.

* android.onDeviceDebug: also disable ProGuard and pin to debug-only

Two follow-ups to the initial hint binding:

- Force off android.enableProguard alongside disableR8. ProGuard runs
  on the non-Gradle-8 legacy path, and even on Gradle 8 it can layer
  on top of R8 when android.enableProguard=true; either way the user
  loses method names and locals. The hint now disables both
  obfuscators so symbols survive the build regardless of which
  Gradle line the project is on.
- Force android.release=false and android.debug=true. Android's
  manifest is shared between the release and debug variants, so
  android:debuggable="true" propagates to both. The cloud build also
  produces both APKs by default (android.release defaults to true).
  Without this pin, a stray hint left in codenameone_settings.properties
  would silently ship a release-signed APK marked debuggable=true —
  a serious security problem since any device that side-loads it
  would be exposing a JDWP socket. Pinning to debug-only means the
  release APK is simply not produced when the hint is on, and the
  developer guide row for the hint now spells that out.

Companion change in the BuildDaemon's AndroidGradleBuilder (separate
repo) applies the same guard for the cloud build path.

* docs: contraction fixes in android.onDeviceDebug hint row

Vale Microsoft.Contractions flagged the expanded row on the second CI
run: "cannot" → "can't", "that is" → "that's". Same fix pattern as
the earlier batch of contraction tweaks in the Android chapter.
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