diff --git a/CHANGELOG.md b/CHANGELOG.md index cc2adc2..073577a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). To support this, the minimum meson version has been bumped to 0.60, previously 0.59. +3. `mctpd` now supports autonomous periodic discovery on point-to-point + physical links (e.g. Serial, USB, KCS) operating in Bus Owner mode. + Background probing automatically discovers and assigns EIDs to endpoints + once the interface is brought UP, with support for per-interface probe + intervals and static EID reservations. + ### Fixes 1. In v2.6. we lost the peer initial MTU sematics, which gave us a safe minimum diff --git a/conf/mctpd.conf b/conf/mctpd.conf index b6f95f1..1d31e56 100644 --- a/conf/mctpd.conf +++ b/conf/mctpd.conf @@ -20,3 +20,27 @@ max_pool_size = 15 # Bus Owner/bridge polling interval (ms) to check endpoint accessibility. # A value of 0 disables polling. endpoint_poll_ms = 0 + +# Autonomous periodic discovery for point-to-point addressless interfaces (e.g. Serial, USB, KCS) +# Default is false. +auto_discovery = false + +# Periodic probing interval (ms) for point-to-point interfaces (100 - 60000 ms) +probe_interval_ms = 1000 + +# Interface specific configuration examples: +# +# [[interface]] +# match = { phys-type = "serial" } +# role = "bus-owner" +# auto_discovery = true +# probe_interval_ms = 500 +# static_eid = 8 +# +# [[interface]] +# match = { phys-type = "usb" } +# role = "bus-owner" +# auto_discovery = true +# probe_interval_ms = 500 +# static_eid = 9 + diff --git a/docs/mctpd.md b/docs/mctpd.md index 04e824a..b46a862 100644 --- a/docs/mctpd.md +++ b/docs/mctpd.md @@ -434,6 +434,24 @@ Such periodic polling is common for all the briged endpoints among allocated pool space [`.PoolStart` - `.PoolEnd`] of the bridge. Polling could be provisioned to be disabled via setting the value as ```0```. +#### `auto_discovery`: Autonomous periodic probing on point-to-point links + +* type: boolean +* default: `false` + +Enables autonomous background probing (`Get Endpoint ID` 0x02) for addressless +point-to-point links (e.g. Serial, USB, KCS) running in `bus-owner` mode. When enabled, +`mctpd` automatically probes the link when brought `IFF_UP` until the endpoint +responds, assigns an EID, and transitions the link to `DISCOVERY_DISCOVERED`. + +#### `probe_interval_ms`: Periodic probing interval for point-to-point links + +* type: integer, in milliseconds +* default: 1000 + +Specifies the period between probe attempts on point-to-point links. Valid range is +`100` ms to `60000` ms. + ### `[[interface]]`: per-interface configuration The `[[interface]]` table allows configuration to be applied to specific @@ -443,12 +461,34 @@ determines which MCTP interfaces the table applies to. Matches are processed in the order they appear in the configuration file; the first `[[interface]]` section that matches is applied. -Other content of the interface table is configuration to be applied. The -only setting currently supported is `role`, to set mctpd's role as -either bus-owner or endpoint on this interface. +Supported settings per interface: +* `role`: sets mctpd's role as either `bus-owner` or `endpoint` on this interface. +* `auto_discovery`: overrides global `auto_discovery` for this interface (`true`/`false`). +* `probe_interval_ms`: overrides global probe interval in milliseconds (`100` - `60000`). +* `static_eid`: assigns a fixed static EID (must be in `[8, dynamic_eid_start)`) upon successful probe response, preventing dynamic EID allocation collisions. + +For example, to configure static EIDs and custom probe intervals on point-to-point links: -For example, to apply a `bus-owner` role globally, with interface-specific -`endpoint` roles for all i2c devices, and one particular (USB) device: +```toml +[bus-owner] +dynamic_eid_range = [16, 64] +auto_discovery = true +probe_interval_ms = 1000 + +[[interface]] +match = { phys-type = "serial" } +role = "bus-owner" +auto_discovery = true +probe_interval_ms = 500 +static_eid = 8 + +[[interface]] +match = { phys-type = "usb" } +role = "bus-owner" +auto_discovery = true +probe_interval_ms = 500 +static_eid = 9 +``` ```toml role = "bus-owner" diff --git a/src/mctpd.c b/src/mctpd.c index e74f118..3f6ed01 100644 --- a/src/mctpd.c +++ b/src/mctpd.c @@ -67,6 +67,9 @@ static const char *conf_file_default = MCTPD_CONF_FILE_DEFAULT; static const uint64_t max_poll_interval_ms = 10000; static const uint64_t min_poll_interval_ms = 2500; +static const uint64_t default_probe_interval_us = 1000 * 1000; +static const uint64_t max_probe_interval_ms = 60000; +static const uint64_t min_probe_interval_ms = 100; static const mctp_eid_t eid_alloc_min = 0x08; static const mctp_eid_t eid_alloc_max = 0xfe; static const uint8_t MCTP_TYPE_VENDOR_PCIE = 0x7e; @@ -153,6 +156,10 @@ struct link { sd_bus_slot *slot_iface; sd_bus_slot *slot_busowner; sd_event_source *role_defer; + sd_event_source *probe_timer; + bool auto_discovery; + uint64_t probe_interval_us; + mctp_eid_t static_eid; struct ctx *ctx; }; @@ -266,6 +273,11 @@ struct interface_config { bool role_set; enum endpoint_role role; + + bool auto_discovery_set; + bool auto_discovery; + uint64_t probe_interval_us; + mctp_eid_t static_eid; }; struct ctx { @@ -316,6 +328,10 @@ struct ctx { // checking endpoint's accessibility. uint64_t endpoint_poll; + // Global autonomous probing default for addressless links + bool auto_discovery; + uint64_t probe_interval_us; + // interface configuration (from config file), to be matched and // applied on new interface events struct interface_config *interface_configs; @@ -378,6 +394,24 @@ static int peer_neigh_update(struct peer *peer, uint16_t type); static int add_interface_local(struct ctx *ctx, int ifindex); static int del_interface(struct link *link); static int rename_interface(struct ctx *ctx, struct link *link, int ifindex); +static void link_start_probe(struct link *link); +static void link_stop_probe(struct link *link); + +static inline bool binding_is_point_to_point(uint8_t binding) +{ + switch (binding) { + case MCTP_PHYS_BINDING_SERIAL: + case MCTP_PHYS_BINDING_USB: + case MCTP_PHYS_BINDING_KCS: + case MCTP_PHYS_BINDING_MMBI: + case MCTP_PHYS_BINDING_PCC: + case MCTP_PHYS_BINDING_UCIE: + return true; + default: + return false; + } +} + static int change_net_interface(struct ctx *ctx, int ifindex, uint32_t old_net); static int add_local_eid(struct ctx *ctx, uint32_t net, int eid); static int del_local_eid(struct ctx *ctx, uint32_t net, int eid); @@ -1569,7 +1603,16 @@ static int cb_listen_monitor(sd_event_source *s, int sd, uint32_t revents, } case MCTP_NL_CHANGE_UP: { - // 'up' state is currently unused + struct link *link = c->link_userdata; + if (link) { + bool is_up = + mctp_nl_up_byindex(ctx->nl, c->ifindex); + if (is_up) { + link_start_probe(link); + } else { + link_stop_probe(link); + } + } break; } default: @@ -2214,8 +2257,20 @@ static int remove_peer(struct peer *peer) ctx->peers = NULL; } + int ifindex = peer->phys.ifindex; + free(peer); + if (ifindex > 0) { + struct link *link = mctp_nl_get_link_userdata(ctx->nl, ifindex); + if (link && binding_is_point_to_point(link->phys_binding) && + link->role == ENDPOINT_ROLE_BUS_OWNER && + link->auto_discovery) { + link->discovered = DISCOVERY_UNDISCOVERED; + link_start_probe(link); + } + } + return 0; } @@ -2754,6 +2809,101 @@ static int get_endpoint_peer(struct ctx *ctx, sd_bus_error *berr, return 0; } +static int link_probe_timer_cb(sd_event_source *s, uint64_t usec, + void *userdata); + +static void link_stop_probe(struct link *link) +{ + if (link->probe_timer) { + sd_event_source_disable_unref(link->probe_timer); + link->probe_timer = NULL; + if (link->ctx->verbose) { + fprintf(stderr, "Disarmed probe timer for %s\n", + link->path ?: "link"); + } + } +} + +static void link_start_probe(struct link *link) +{ + struct ctx *ctx = link->ctx; + + if (!binding_is_point_to_point(link->phys_binding)) + return; + + if (link->role != ENDPOINT_ROLE_BUS_OWNER) + return; + + if (!link->auto_discovery) + return; + + if (link->discovered != DISCOVERY_UNDISCOVERED) + return; + + if (link->probe_timer) + return; + + if (!mctp_nl_up_byindex(ctx->nl, link->ifindex)) + return; + + sd_event_add_time_relative(ctx->event, &link->probe_timer, + CLOCK_MONOTONIC, link->probe_interval_us, 0, + link_probe_timer_cb, link); + if (ctx->verbose) { + fprintf(stderr, + "Armed probe timer for %s (interval %" PRIu64 " us)\n", + link->path ?: "link", link->probe_interval_us); + } +} + +static int link_probe_timer_cb(sd_event_source *s, uint64_t usec, + void *userdata) +{ + struct link *link = userdata; + struct ctx *ctx = link->ctx; + dest_phys dest = { + .ifindex = link->ifindex, + .hwaddr_len = 0, + }; + mctp_eid_t ret_eid = 0; + uint8_t ret_ep_type = 0, ret_media_spec = 0; + int rc; + + if (link->discovered != DISCOVERY_UNDISCOVERED) + return 0; + + /* Issue non-retrying Get Endpoint ID request */ + rc = query_get_endpoint_id(ctx, &dest, &ret_eid, &ret_ep_type, + &ret_media_spec, /*peer=*/NULL, + /*retry=*/false); + if (rc == 0) { + struct peer *peer = NULL; + + link_stop_probe(link); + link->discovered = DISCOVERY_DISCOVERED; + + if (ctx->verbose) { + fprintf(stderr, "Probe response on %s: assigning EID\n", + link->path ?: "link"); + } + + rc = endpoint_assign_eid(ctx, NULL, &dest, &peer, + link->static_eid, false); + if (rc < 0) { + warnx("Failed to assign EID on %s: %s", + link->path ?: "link", strerror(-rc)); + link->discovered = DISCOVERY_UNDISCOVERED; + link_start_probe(link); + } + return 0; + } + + /* Reschedule next probe tick */ + sd_event_source_set_time_relative(s, link->probe_interval_us); + sd_event_source_set_enabled(s, SD_EVENT_ONESHOT); + return 0; +} + static int query_get_peer_msgtypes(struct peer *peer) { struct mctp_ctrl_resp_get_msg_type_support *resp = NULL; @@ -4974,6 +5124,7 @@ static int prune_old_nets(struct ctx *ctx) static void free_link(struct link *link) { + link_stop_probe(link); sd_event_source_disable_unref(link->role_defer); sd_bus_slot_unref(link->slot_iface); sd_bus_slot_unref(link->slot_busowner); @@ -5361,6 +5512,10 @@ static int link_apply_configuration(struct ctx *ctx, struct link *link) { struct interface_config *config; + link->auto_discovery = ctx->auto_discovery; + link->probe_interval_us = ctx->probe_interval_us; + link->static_eid = 0; + config = link_find_configuration(ctx, link); if (!config) return 0; @@ -5368,6 +5523,14 @@ static int link_apply_configuration(struct ctx *ctx, struct link *link) if (config->role_set) link->role = config->role; + if (config->auto_discovery_set) + link->auto_discovery = config->auto_discovery; + + if (config->probe_interval_us) + link->probe_interval_us = config->probe_interval_us; + + link->static_eid = config->static_eid; + return 0; } @@ -5436,6 +5599,10 @@ static int add_interface(struct ctx *ctx, int ifindex) if (link->phys_binding == MCTP_PHYS_BINDING_PCIE_VDM) { link->discovered = DISCOVERY_UNDISCOVERED; + } else if (binding_is_point_to_point(link->phys_binding) && + link->role == ENDPOINT_ROLE_BUS_OWNER && + link->auto_discovery) { + link->discovered = DISCOVERY_UNDISCOVERED; } link->published = true; @@ -5444,6 +5611,9 @@ static int add_interface(struct ctx *ctx, int ifindex) link->published = false; } + if (mctp_nl_up_byindex(ctx->nl, ifindex)) + link_start_probe(link); + return rc; err_free: @@ -5704,6 +5874,27 @@ static int parse_config_bus_owner(struct ctx *ctx, toml_table_t *bus_owner) ctx->endpoint_poll = i * 1000; } + val = toml_bool_in(bus_owner, "auto_discovery"); + if (!val.ok) + val = toml_bool_in(bus_owner, "auto-discovery"); + if (val.ok) + ctx->auto_discovery = val.u.b; + + val = toml_int_in(bus_owner, "probe_interval_ms"); + if (!val.ok) + val = toml_int_in(bus_owner, "probe-interval-ms"); + if (val.ok && val.u.i) { + uint64_t i = val.u.i; + if ((i > max_probe_interval_ms) || + (i < min_probe_interval_ms)) { + warnx("probe interval invalid (%" PRIu64 " - %" PRIu64 + " ms)", + min_probe_interval_ms, max_probe_interval_ms); + return -1; + } + ctx->probe_interval_us = i * 1000; + } + return 0; } @@ -5857,6 +6048,49 @@ static int parse_config_interface(struct ctx *ctx, unsigned int idx, return rc; } + conf_str = toml_bool_in(interface, "auto_discovery"); + if (!conf_str.ok) + conf_str = toml_bool_in(interface, "auto-discovery"); + if (conf_str.ok) { + config->auto_discovery_set = true; + config->auto_discovery = conf_str.u.b; + } + + conf_str = toml_int_in(interface, "probe_interval_ms"); + if (!conf_str.ok) + conf_str = toml_int_in(interface, "probe-interval-ms"); + if (conf_str.ok && conf_str.u.i) { + uint64_t i = conf_str.u.i; + if ((i > max_probe_interval_ms) || + (i < min_probe_interval_ms)) { + warnx("interface probe interval invalid (%" PRIu64 + " - %" PRIu64 " ms)", + min_probe_interval_ms, max_probe_interval_ms); + return -1; + } + config->probe_interval_us = i * 1000; + } + + conf_str = toml_int_in(interface, "static_eid"); + if (!conf_str.ok) + conf_str = toml_int_in(interface, "static-eid"); + if (conf_str.ok && conf_str.u.i) { + uint64_t eid = conf_str.u.i; + if (eid < 8 || eid > eid_alloc_max) { + warnx("invalid static_eid %" PRIu64 + " (must be in range [8, %d])", + eid, eid_alloc_max); + return -1; + } + if (ctx->dyn_eid_min > 8 && eid >= ctx->dyn_eid_min) { + warnx("invalid static_eid %" PRIu64 + " (must be in range [8, %d) to avoid dynamic range)", + eid, ctx->dyn_eid_min); + return -1; + } + config->static_eid = eid; + } + return 0; } @@ -5889,6 +6123,17 @@ static int parse_config_interfaces(struct ctx *ctx, toml_array_t *interfaces) rc = parse_config_interface(ctx, i, interface, &configs[i]); if (rc) goto err_free; + + if (configs[i].static_eid) { + for (int j = 0; j < i; j++) { + if (configs[j].static_eid == + configs[i].static_eid) { + warnx("duplicate static_eid %d configured in interfaces array", + configs[i].static_eid); + goto err_free; + } + } + } } ctx->interface_configs = configs; @@ -5960,6 +6205,8 @@ static int parse_config(struct ctx *ctx) } interfaces = toml_array_in(conf_root, "interface"); + if (!interfaces) + interfaces = toml_array_in(conf_root, "interfaces"); if (interfaces) { rc = parse_config_interfaces(ctx, interfaces); if (rc) @@ -6014,6 +6261,8 @@ static void setup_config_defaults(struct ctx *ctx) ctx->dyn_eid_min = eid_alloc_min; ctx->dyn_eid_max = eid_alloc_max; ctx->endpoint_poll = 0; + ctx->auto_discovery = false; + ctx->probe_interval_us = default_probe_interval_us; } static void free_config(struct ctx *ctx) diff --git a/tests/test_mctpd.py b/tests/test_mctpd.py index 4f75954..aef8394 100644 --- a/tests/test_mctpd.py +++ b/tests/test_mctpd.py @@ -2396,3 +2396,316 @@ async def test_iface_config_match_path_none(dbus, sysnet, nursery): res = await mctpd.stop_mctpd() assert res == 0 + + +async def test_auto_discovery_serial_probe(dbus, sysnet, nursery): + """Test autonomous periodic discovery on a serial interface""" + config = """ + role = "bus-owner" + [bus-owner] + auto_discovery = true + probe_interval_ms = 100 + """ + + mctpd = MctpdWrapper(dbus, sysnet, config=config) + iface = mctpd.system.interfaces[0] + iface.phys_binding = PhysicalBinding.SERIAL + iface.lladdr = b"" + iface.up = True + mctpd.network.endpoints[0].lladdr = b"" + + await mctpd.start_mctpd(nursery) + + mctp_root = await dbus.get_proxy_object(MCTPD_C, MCTPD_MCTP_P) + objmgr = await mctp_root.get_interface(DBUS_OBJECT_MANAGER_I) + + endpoint_added = trio.Event() + discovered_path = None + + def on_ifaces_added(path, interfaces): + nonlocal discovered_path + if MCTPD_ENDPOINT_I in interfaces and not path.endswith("/8"): + discovered_path = path + endpoint_added.set() + + await objmgr.on_interfaces_added(on_ifaces_added) + + # Check if already added + objects = await objmgr.call_get_managed_objects() + for path, ifaces in objects.items(): + if MCTPD_ENDPOINT_I in ifaces and not path.endswith("/8"): + discovered_path = path + endpoint_added.set() + break + + if not endpoint_added.is_set(): + with trio.move_on_after(2.0) as cancel_scope: + await endpoint_added.wait() + assert not cancel_scope.cancelled_caught + + assert discovered_path is not None + assert len(mctpd.system.routes) == 1 + assert mctpd.system.routes[0].start_eid == 9 + + iface_obj = await mctpd_mctp_iface_control_obj(dbus, iface) + role = await iface_obj.get_role() + assert role == "BusOwner" + + res = await mctpd.stop_mctpd() + assert res == 0 + + +async def test_auto_discovery_point_to_point_usb(dbus, sysnet, nursery): + """Test autonomous periodic discovery on USB point-to-point interface""" + config = """ + role = "bus-owner" + [bus-owner] + auto_discovery = true + probe_interval_ms = 100 + """ + + mctpd = MctpdWrapper(dbus, sysnet, config=config) + iface = mctpd.system.interfaces[0] + iface.phys_binding = PhysicalBinding.USB + iface.lladdr = b"" + iface.up = True + mctpd.network.endpoints[0].lladdr = b"" + + await mctpd.start_mctpd(nursery) + + mctp_root = await dbus.get_proxy_object(MCTPD_C, MCTPD_MCTP_P) + objmgr = await mctp_root.get_interface(DBUS_OBJECT_MANAGER_I) + + endpoint_added = trio.Event() + discovered_path = None + + def on_ifaces_added(path, interfaces): + nonlocal discovered_path + if MCTPD_ENDPOINT_I in interfaces and not path.endswith("/8"): + discovered_path = path + endpoint_added.set() + + await objmgr.on_interfaces_added(on_ifaces_added) + + # Check if already added + objects = await objmgr.call_get_managed_objects() + for path, ifaces in objects.items(): + if MCTPD_ENDPOINT_I in ifaces and not path.endswith("/8"): + discovered_path = path + endpoint_added.set() + break + + if not endpoint_added.is_set(): + with trio.move_on_after(2.0) as cancel_scope: + await endpoint_added.wait() + assert not cancel_scope.cancelled_caught + + assert discovered_path is not None + assert len(mctpd.system.routes) == 1 + assert mctpd.system.routes[0].start_eid == 9 + + iface_obj = await mctpd_mctp_iface_control_obj(dbus, iface) + role = await iface_obj.get_role() + assert role == "BusOwner" + + res = await mctpd.stop_mctpd() + assert res == 0 + + +async def test_auto_discovery_static_eid(dbus, sysnet, nursery): + """Test autonomous discovery with static EID configuration and verification""" + config = """ + role = "bus-owner" + [bus-owner] + dynamic_eid_range = [16, 64] + auto_discovery = true + probe_interval_ms = 100 + + [[interface]] + match = { phys-type = "serial" } + role = "bus-owner" + auto_discovery = true + probe_interval_ms = 100 + static_eid = 9 + """ + + mctpd = MctpdWrapper(dbus, sysnet, config=config) + iface = mctpd.system.interfaces[0] + iface.phys_binding = PhysicalBinding.SERIAL + iface.lladdr = b"" + iface.up = True + mctpd.network.endpoints[0].lladdr = b"" + + await mctpd.start_mctpd(nursery) + + mctp_root = await dbus.get_proxy_object(MCTPD_C, MCTPD_MCTP_P) + objmgr = await mctp_root.get_interface(DBUS_OBJECT_MANAGER_I) + + endpoint_added = trio.Event() + discovered_path = None + + def on_ifaces_added(path, interfaces): + nonlocal discovered_path + if MCTPD_ENDPOINT_I in interfaces and not path.endswith("/8"): + discovered_path = path + endpoint_added.set() + + await objmgr.on_interfaces_added(on_ifaces_added) + + # Check if already added + objects = await objmgr.call_get_managed_objects() + for path, ifaces in objects.items(): + if MCTPD_ENDPOINT_I in ifaces and not path.endswith("/8"): + discovered_path = path + endpoint_added.set() + break + + if not endpoint_added.is_set(): + with trio.move_on_after(2.0) as cancel_scope: + await endpoint_added.wait() + assert not cancel_scope.cancelled_caught + + assert discovered_path is not None + assert len(mctpd.system.routes) == 1 + assert mctpd.system.routes[0].start_eid == 9 + + ep_obj = await mctpd_mctp_endpoint_common_obj(dbus, discovered_path) + assigned_eid = await ep_obj.get_eid() + assert assigned_eid == 9 + + res = await mctpd.stop_mctpd() + assert res == 0 + + +async def test_auto_discovery_disabled(dbus, sysnet, nursery): + """Test that auto_discovery can be disabled globally and per-interface""" + config = """ + role = "bus-owner" + [bus-owner] + auto_discovery = false + """ + + mctpd = MctpdWrapper(dbus, sysnet, config=config) + iface = mctpd.system.interfaces[0] + iface.phys_binding = PhysicalBinding.SERIAL + iface.lladdr = b"" + iface.up = True + mctpd.network.endpoints[0].lladdr = b"" + + await mctpd.start_mctpd(nursery) + + mctp_root = await dbus.get_proxy_object(MCTPD_C, MCTPD_MCTP_P) + objmgr = await mctp_root.get_interface(DBUS_OBJECT_MANAGER_I) + + await trio.sleep(0.2) + + objects = await objmgr.call_get_managed_objects() + remote_eps = [ + p + for p in objects + if p.startswith(f"/au/com/codeconstruct/mctp1/networks/{iface.net}/endpoints/") + and not p.endswith("/8") + ] + assert len(remote_eps) == 0 + assert len(mctpd.system.routes) == 0 + + res = await mctpd.stop_mctpd() + assert res == 0 + + +async def test_auto_discovery_default_disabled(dbus, sysnet, nursery): + """Test that auto_discovery is disabled by default without explicit config""" + config = """ + role = "bus-owner" + """ + + mctpd = MctpdWrapper(dbus, sysnet, config=config) + iface = mctpd.system.interfaces[0] + iface.phys_binding = PhysicalBinding.SERIAL + iface.lladdr = b"" + iface.up = True + mctpd.network.endpoints[0].lladdr = b"" + + await mctpd.start_mctpd(nursery) + + mctp_root = await dbus.get_proxy_object(MCTPD_C, MCTPD_MCTP_P) + objmgr = await mctp_root.get_interface(DBUS_OBJECT_MANAGER_I) + + await trio.sleep(0.2) + + objects = await objmgr.call_get_managed_objects() + remote_eps = [ + p + for p in objects + if p.startswith(f"/au/com/codeconstruct/mctp1/networks/{iface.net}/endpoints/") + and not p.endswith("/8") + ] + assert len(remote_eps) == 0 + assert len(mctpd.system.routes) == 0 + + res = await mctpd.stop_mctpd() + assert res == 0 + + +async def test_auto_discovery_hotplug_requires_up(dbus, sysnet, nursery): + """Test that newly added point-to-point interface remains quiescent while DOWN, and starts probing only when brought UP""" + config = """ + role = "bus-owner" + [bus-owner] + auto_discovery = true + probe_interval_ms = 100 + """ + + mctpd = MctpdWrapper(dbus, sysnet, config=config) + iface = mctpd.system.interfaces[0] + iface.phys_binding = PhysicalBinding.SERIAL + iface.lladdr = b"" + iface.up = False + mctpd.network.endpoints[0].lladdr = b"" + + await mctpd.start_mctpd(nursery) + + mctp_root = await dbus.get_proxy_object(MCTPD_C, MCTPD_MCTP_P) + objmgr = await mctp_root.get_interface(DBUS_OBJECT_MANAGER_I) + + endpoint_added = trio.Event() + discovered_path = None + + def on_ifaces_added(path, interfaces): + nonlocal discovered_path + if MCTPD_ENDPOINT_I in interfaces and not path.endswith("/8"): + discovered_path = path + endpoint_added.set() + + await objmgr.on_interfaces_added(on_ifaces_added) + + # While iface.up is False, no remote endpoint should be discovered + await trio.sleep(0.2) + objects = await objmgr.call_get_managed_objects() + remote_eps = [ + p + for p in objects + if p.startswith(f"/au/com/codeconstruct/mctp1/networks/{iface.net}/endpoints/") + and not p.endswith("/8") + ] + assert len(remote_eps) == 0 + assert len(mctpd.system.routes) == 0 + assert not endpoint_added.is_set() + + # Bring the interface UP at runtime (simulating 'ip link set mctpserial0 up') + iface.up = True + await mctpd.system.notify_interface(iface) + + # Now probe should trigger and endpoint should be added + if not endpoint_added.is_set(): + with trio.move_on_after(2.0) as cancel_scope: + await endpoint_added.wait() + assert not cancel_scope.cancelled_caught + + assert endpoint_added.is_set() + assert discovered_path is not None + assert len(mctpd.system.routes) == 1 + assert mctpd.system.routes[0].start_eid == 9 + + res = await mctpd.stop_mctpd() + assert res == 0