|
|
e0018b |
From 7eb06a5293531854e7a28666e955106094d3552b Mon Sep 17 00:00:00 2001
|
|
|
e0018b |
From: Dan Williams <dan.j.williams@intel.com>
|
|
|
e0018b |
Date: Sun, 23 Jan 2022 16:53:29 -0800
|
|
|
e0018b |
Subject: [PATCH 105/217] cxl/list: Add endpoints
|
|
|
e0018b |
|
|
|
e0018b |
Endpoints are port-like objects that represent the HDM decoders at terminal
|
|
|
e0018b |
end of a decode chain. Unlike port decoders that route to downstream ports,
|
|
|
e0018b |
endpoint decoders route to endpoint DPA (Device Physical Address) ranges.
|
|
|
e0018b |
|
|
|
e0018b |
Link: https://lore.kernel.org/r/164298560917.3021641.13753578554905796298.stgit@dwillia2-desk3.amr.corp.intel.com
|
|
|
e0018b |
Signed-off-by: Dan Williams <dan.j.williams@intel.com>
|
|
|
e0018b |
Signed-off-by: Vishal Verma <vishal.l.verma@intel.com>
|
|
|
e0018b |
---
|
|
|
e0018b |
.clang-format | 1 +
|
|
|
e0018b |
Documentation/cxl/cxl-list.txt | 16 ++++
|
|
|
e0018b |
Documentation/cxl/lib/libcxl.txt | 31 ++++++-
|
|
|
e0018b |
cxl/filter.c | 147 ++++++++++++++++++++++++++++---
|
|
|
e0018b |
cxl/filter.h | 2 +
|
|
|
e0018b |
cxl/json.c | 20 ++++-
|
|
|
e0018b |
cxl/json.h | 2 +
|
|
|
e0018b |
cxl/lib/libcxl.c | 107 ++++++++++++++++++++++
|
|
|
e0018b |
cxl/lib/libcxl.sym | 9 ++
|
|
|
e0018b |
cxl/lib/private.h | 10 +++
|
|
|
e0018b |
cxl/libcxl.h | 15 ++++
|
|
|
e0018b |
cxl/list.c | 13 ++-
|
|
|
e0018b |
12 files changed, 355 insertions(+), 18 deletions(-)
|
|
|
e0018b |
|
|
|
e0018b |
diff --git a/.clang-format b/.clang-format
|
|
|
e0018b |
index 391cd34..106bc5e 100644
|
|
|
e0018b |
--- a/.clang-format
|
|
|
e0018b |
+++ b/.clang-format
|
|
|
e0018b |
@@ -80,6 +80,7 @@ ForEachMacros:
|
|
|
e0018b |
- 'cxl_memdev_foreach'
|
|
|
e0018b |
- 'cxl_bus_foreach'
|
|
|
e0018b |
- 'cxl_port_foreach'
|
|
|
e0018b |
+ - 'cxl_endpoint_foreach'
|
|
|
e0018b |
- 'daxctl_dev_foreach'
|
|
|
e0018b |
- 'daxctl_mapping_foreach'
|
|
|
e0018b |
- 'daxctl_region_foreach'
|
|
|
e0018b |
diff --git a/Documentation/cxl/cxl-list.txt b/Documentation/cxl/cxl-list.txt
|
|
|
e0018b |
index 42b6de6..d342da2 100644
|
|
|
e0018b |
--- a/Documentation/cxl/cxl-list.txt
|
|
|
e0018b |
+++ b/Documentation/cxl/cxl-list.txt
|
|
|
e0018b |
@@ -190,6 +190,12 @@ OPTIONS
|
|
|
e0018b |
----
|
|
|
e0018b |
# cxl list -B
|
|
|
e0018b |
# cxl list -P -p root
|
|
|
e0018b |
+----
|
|
|
e0018b |
+ Additionally, endpoint objects are also ports so the following commands
|
|
|
e0018b |
+ are also equivalent.
|
|
|
e0018b |
+----
|
|
|
e0018b |
+# cxl list -E
|
|
|
e0018b |
+# cxl list -P -p endpoint
|
|
|
e0018b |
----
|
|
|
e0018b |
By default, only 'switch' ports are listed.
|
|
|
e0018b |
|
|
|
e0018b |
@@ -200,6 +206,16 @@ OPTIONS
|
|
|
e0018b |
descendants of the individual ports that match the filter. By default
|
|
|
e0018b |
all descendant objects are listed.
|
|
|
e0018b |
|
|
|
e0018b |
+-E::
|
|
|
e0018b |
+--endpoints::
|
|
|
e0018b |
+ Include endpoint objects (CXL Memory Device decoders) in the
|
|
|
e0018b |
+ listing.
|
|
|
e0018b |
+
|
|
|
e0018b |
+-e::
|
|
|
e0018b |
+--endpoint::
|
|
|
e0018b |
+ Specify CXL endpoint device name(s), or device id(s) to filter
|
|
|
e0018b |
+ the emitted endpoint(s).
|
|
|
e0018b |
+
|
|
|
e0018b |
--debug::
|
|
|
e0018b |
If the cxl tool was built with debug enabled, turn on debug
|
|
|
e0018b |
messages.
|
|
|
e0018b |
diff --git a/Documentation/cxl/lib/libcxl.txt b/Documentation/cxl/lib/libcxl.txt
|
|
|
e0018b |
index 804e9ca..eebab37 100644
|
|
|
e0018b |
--- a/Documentation/cxl/lib/libcxl.txt
|
|
|
e0018b |
+++ b/Documentation/cxl/lib/libcxl.txt
|
|
|
e0018b |
@@ -199,12 +199,41 @@ int cxl_port_get_id(struct cxl_port *port);
|
|
|
e0018b |
int cxl_port_is_enabled(struct cxl_port *port);
|
|
|
e0018b |
bool cxl_port_is_root(struct cxl_port *port);
|
|
|
e0018b |
bool cxl_port_is_switch(struct cxl_port *port);
|
|
|
e0018b |
+bool cxl_port_is_endpoint(struct cxl_port *port);
|
|
|
e0018b |
----
|
|
|
e0018b |
The port type is communicated via cxl_port_is_<type>(). An 'enabled' port
|
|
|
e0018b |
is one that has succeeded in discovering the CXL component registers in
|
|
|
e0018b |
the host device and has enumerated its downstream ports. In order for a
|
|
|
e0018b |
memdev to be enabled for CXL memory operation all CXL ports in its
|
|
|
e0018b |
-ancestry must also be enabled.
|
|
|
e0018b |
+ancestry must also be enabled including a root port, an arbitrary number
|
|
|
e0018b |
+of intervening switch ports, and a terminal endpoint port.
|
|
|
e0018b |
+
|
|
|
e0018b |
+ENDPOINTS
|
|
|
e0018b |
+---------
|
|
|
e0018b |
+CXL endpoint objects encapsulate the set of host-managed device-memory
|
|
|
e0018b |
+(HDM) decoders in a physical memory device. The endpoint is the last hop
|
|
|
e0018b |
+in a decoder chain that translate SPA to DPA (system-physical-address to
|
|
|
e0018b |
+device-local-physical-address).
|
|
|
e0018b |
+
|
|
|
e0018b |
+=== ENDPOINT: Enumeration
|
|
|
e0018b |
+----
|
|
|
e0018b |
+struct cxl_endpoint *cxl_endpoint_get_first(struct cxl_port *parent);
|
|
|
e0018b |
+struct cxl_endpoint *cxl_endpoint_get_next(struct cxl_endpoint *endpoint);
|
|
|
e0018b |
+struct cxl_ctx *cxl_endpoint_get_ctx(struct cxl_endpoint *endpoint);
|
|
|
e0018b |
+struct cxl_port *cxl_endpoint_get_parent(struct cxl_endpoint *endpoint);
|
|
|
e0018b |
+struct cxl_port *cxl_endpoint_get_port(struct cxl_endpoint *endpoint);
|
|
|
e0018b |
+
|
|
|
e0018b |
+#define cxl_endpoint_foreach(port, endpoint) \
|
|
|
e0018b |
+ for (endpoint = cxl_endpoint_get_first(port); endpoint != NULL; \
|
|
|
e0018b |
+ endpoint = cxl_endpoint_get_next(endpoint))
|
|
|
e0018b |
+----
|
|
|
e0018b |
+
|
|
|
e0018b |
+=== ENDPOINT: Attributes
|
|
|
e0018b |
+----
|
|
|
e0018b |
+const char *cxl_endpoint_get_devname(struct cxl_endpoint *endpoint);
|
|
|
e0018b |
+int cxl_endpoint_get_id(struct cxl_endpoint *endpoint);
|
|
|
e0018b |
+int cxl_endpoint_is_enabled(struct cxl_endpoint *endpoint);
|
|
|
e0018b |
+----
|
|
|
e0018b |
|
|
|
e0018b |
include::../../copyright.txt[]
|
|
|
e0018b |
|
|
|
e0018b |
diff --git a/cxl/filter.c b/cxl/filter.c
|
|
|
e0018b |
index 32171a4..5d80d1b 100644
|
|
|
e0018b |
--- a/cxl/filter.c
|
|
|
e0018b |
+++ b/cxl/filter.c
|
|
|
e0018b |
@@ -47,8 +47,42 @@ bool cxl_filter_has(const char *__filter, const char *needle)
|
|
|
e0018b |
return false;
|
|
|
e0018b |
}
|
|
|
e0018b |
|
|
|
e0018b |
+static struct cxl_endpoint *
|
|
|
e0018b |
+util_cxl_endpoint_filter(struct cxl_endpoint *endpoint, const char *__ident)
|
|
|
e0018b |
+{
|
|
|
e0018b |
+ char *ident, *save;
|
|
|
e0018b |
+ const char *arg;
|
|
|
e0018b |
+ int endpoint_id;
|
|
|
e0018b |
+
|
|
|
e0018b |
+ if (!__ident)
|
|
|
e0018b |
+ return endpoint;
|
|
|
e0018b |
+
|
|
|
e0018b |
+ ident = strdup(__ident);
|
|
|
e0018b |
+ if (!ident)
|
|
|
e0018b |
+ return NULL;
|
|
|
e0018b |
+
|
|
|
e0018b |
+ for (arg = strtok_r(ident, which_sep(__ident), &save); arg;
|
|
|
e0018b |
+ arg = strtok_r(NULL, which_sep(__ident), &save)) {
|
|
|
e0018b |
+ if (strcmp(arg, "all") == 0)
|
|
|
e0018b |
+ break;
|
|
|
e0018b |
+
|
|
|
e0018b |
+ if ((sscanf(arg, "%d", &endpoint_id) == 1 ||
|
|
|
e0018b |
+ sscanf(arg, "endpoint%d", &endpoint_id) == 1) &&
|
|
|
e0018b |
+ cxl_endpoint_get_id(endpoint) == endpoint_id)
|
|
|
e0018b |
+ break;
|
|
|
e0018b |
+
|
|
|
e0018b |
+ if (strcmp(arg, cxl_endpoint_get_devname(endpoint)) == 0)
|
|
|
e0018b |
+ break;
|
|
|
e0018b |
+ }
|
|
|
e0018b |
+
|
|
|
e0018b |
+ free(ident);
|
|
|
e0018b |
+ if (arg)
|
|
|
e0018b |
+ return endpoint;
|
|
|
e0018b |
+ return NULL;
|
|
|
e0018b |
+}
|
|
|
e0018b |
+
|
|
|
e0018b |
static struct cxl_port *__util_cxl_port_filter(struct cxl_port *port,
|
|
|
e0018b |
- const char *__ident)
|
|
|
e0018b |
+ const char *__ident)
|
|
|
e0018b |
{
|
|
|
e0018b |
char *ident, *save;
|
|
|
e0018b |
const char *arg;
|
|
|
e0018b |
@@ -72,6 +106,9 @@ static struct cxl_port *__util_cxl_port_filter(struct cxl_port *port,
|
|
|
e0018b |
if (strcmp(arg, "switch") == 0 && cxl_port_is_switch(port))
|
|
|
e0018b |
break;
|
|
|
e0018b |
|
|
|
e0018b |
+ if (strcmp(arg, "endpoint") == 0 && cxl_port_is_endpoint(port))
|
|
|
e0018b |
+ break;
|
|
|
e0018b |
+
|
|
|
e0018b |
if ((sscanf(arg, "%d", &port_id) == 1 ||
|
|
|
e0018b |
sscanf(arg, "port%d", &port_id) == 1) &&
|
|
|
e0018b |
cxl_port_get_id(port) == port_id)
|
|
|
e0018b |
@@ -116,6 +153,24 @@ static struct cxl_port *util_cxl_port_filter(struct cxl_port *port,
|
|
|
e0018b |
return NULL;
|
|
|
e0018b |
}
|
|
|
e0018b |
|
|
|
e0018b |
+static struct cxl_endpoint *
|
|
|
e0018b |
+util_cxl_endpoint_filter_by_port(struct cxl_endpoint *endpoint,
|
|
|
e0018b |
+ const char *ident,
|
|
|
e0018b |
+ enum cxl_port_filter_mode mode)
|
|
|
e0018b |
+{
|
|
|
e0018b |
+ struct cxl_port *iter = cxl_endpoint_get_port(endpoint);
|
|
|
e0018b |
+
|
|
|
e0018b |
+ if (util_cxl_port_filter(iter, ident, CXL_PF_SINGLE))
|
|
|
e0018b |
+ return endpoint;
|
|
|
e0018b |
+ iter = cxl_port_get_parent(iter);
|
|
|
e0018b |
+ if (!iter)
|
|
|
e0018b |
+ return NULL;
|
|
|
e0018b |
+ if (util_cxl_port_filter(iter, ident, mode))
|
|
|
e0018b |
+ return endpoint;
|
|
|
e0018b |
+
|
|
|
e0018b |
+ return NULL;
|
|
|
e0018b |
+}
|
|
|
e0018b |
+
|
|
|
e0018b |
static struct cxl_bus *util_cxl_bus_filter(struct cxl_bus *bus,
|
|
|
e0018b |
const char *__ident)
|
|
|
e0018b |
{
|
|
|
e0018b |
@@ -325,10 +380,34 @@ static struct json_object *pick_array(struct json_object *child,
|
|
|
e0018b |
return NULL;
|
|
|
e0018b |
}
|
|
|
e0018b |
|
|
|
e0018b |
+static void walk_endpoints(struct cxl_port *port, struct cxl_filter_params *p,
|
|
|
e0018b |
+ struct json_object *jeps, unsigned long flags)
|
|
|
e0018b |
+{
|
|
|
e0018b |
+ struct cxl_endpoint *endpoint;
|
|
|
e0018b |
+
|
|
|
e0018b |
+ cxl_endpoint_foreach(port, endpoint) {
|
|
|
e0018b |
+ struct cxl_port *ep_port = cxl_endpoint_get_port(endpoint);
|
|
|
e0018b |
+ struct json_object *jendpoint;
|
|
|
e0018b |
+
|
|
|
e0018b |
+ if (!util_cxl_endpoint_filter(endpoint, p->endpoint_filter))
|
|
|
e0018b |
+ continue;
|
|
|
e0018b |
+ if (!util_cxl_port_filter_by_bus(ep_port, p->bus_filter))
|
|
|
e0018b |
+ continue;
|
|
|
e0018b |
+ if (!util_cxl_endpoint_filter_by_port(endpoint, p->port_filter,
|
|
|
e0018b |
+ pf_mode(p)))
|
|
|
e0018b |
+ continue;
|
|
|
e0018b |
+ if (!p->idle && !cxl_endpoint_is_enabled(endpoint))
|
|
|
e0018b |
+ continue;
|
|
|
e0018b |
+ jendpoint = util_cxl_endpoint_to_json(endpoint, flags);
|
|
|
e0018b |
+ if (jendpoint)
|
|
|
e0018b |
+ json_object_array_add(jeps, jendpoint);
|
|
|
e0018b |
+ }
|
|
|
e0018b |
+}
|
|
|
e0018b |
+
|
|
|
e0018b |
static void walk_child_ports(struct cxl_port *parent_port,
|
|
|
e0018b |
struct cxl_filter_params *p,
|
|
|
e0018b |
struct json_object *jports,
|
|
|
e0018b |
- unsigned long flags)
|
|
|
e0018b |
+ struct json_object *jeps, unsigned long flags)
|
|
|
e0018b |
{
|
|
|
e0018b |
struct cxl_port *port;
|
|
|
e0018b |
|
|
|
e0018b |
@@ -336,6 +415,7 @@ static void walk_child_ports(struct cxl_port *parent_port,
|
|
|
e0018b |
const char *devname = cxl_port_get_devname(port);
|
|
|
e0018b |
struct json_object *jport = NULL;
|
|
|
e0018b |
struct json_object *jchildports = NULL;
|
|
|
e0018b |
+ struct json_object *jchildendpoints = NULL;
|
|
|
e0018b |
|
|
|
e0018b |
if (!util_cxl_port_filter(port, p->port_filter, pf_mode(p)))
|
|
|
e0018b |
goto walk_children;
|
|
|
e0018b |
@@ -343,21 +423,41 @@ static void walk_child_ports(struct cxl_port *parent_port,
|
|
|
e0018b |
goto walk_children;
|
|
|
e0018b |
if (!p->idle && !cxl_port_is_enabled(port))
|
|
|
e0018b |
continue;
|
|
|
e0018b |
- if (p->ports)
|
|
|
e0018b |
+ if (p->ports) {
|
|
|
e0018b |
jport = util_cxl_port_to_json(port, flags);
|
|
|
e0018b |
- if (!jport)
|
|
|
e0018b |
- continue;
|
|
|
e0018b |
- json_object_array_add(jports, jport);
|
|
|
e0018b |
- jchildports = json_object_new_array();
|
|
|
e0018b |
- if (!jchildports) {
|
|
|
e0018b |
- err(p, "%s: failed to enumerate child ports\n",
|
|
|
e0018b |
- devname);
|
|
|
e0018b |
- continue;
|
|
|
e0018b |
+ if (!jport) {
|
|
|
e0018b |
+ err(p, "%s: failed to list\n", devname);
|
|
|
e0018b |
+ continue;
|
|
|
e0018b |
+ }
|
|
|
e0018b |
+ json_object_array_add(jports, jport);
|
|
|
e0018b |
+ jchildports = json_object_new_array();
|
|
|
e0018b |
+ if (!jchildports) {
|
|
|
e0018b |
+ err(p, "%s: failed to enumerate child ports\n",
|
|
|
e0018b |
+ devname);
|
|
|
e0018b |
+ continue;
|
|
|
e0018b |
+ }
|
|
|
e0018b |
+ }
|
|
|
e0018b |
+
|
|
|
e0018b |
+ if (p->ports && p->endpoints) {
|
|
|
e0018b |
+ jchildendpoints = json_object_new_array();
|
|
|
e0018b |
+ if (!jchildendpoints) {
|
|
|
e0018b |
+ err(p,
|
|
|
e0018b |
+ "%s: failed to enumerate child endpoints\n",
|
|
|
e0018b |
+ devname);
|
|
|
e0018b |
+ continue;
|
|
|
e0018b |
+ }
|
|
|
e0018b |
}
|
|
|
e0018b |
+
|
|
|
e0018b |
walk_children:
|
|
|
e0018b |
+ if (p->endpoints)
|
|
|
e0018b |
+ walk_endpoints(port, p, pick_array(jchildendpoints, jeps),
|
|
|
e0018b |
+ flags);
|
|
|
e0018b |
+
|
|
|
e0018b |
walk_child_ports(port, p, pick_array(jchildports, jports),
|
|
|
e0018b |
- flags);
|
|
|
e0018b |
+ pick_array(jchildendpoints, jeps), flags);
|
|
|
e0018b |
cond_add_put_array_suffix(jport, "ports", devname, jchildports);
|
|
|
e0018b |
+ cond_add_put_array_suffix(jport, "endpoints", devname,
|
|
|
e0018b |
+ jchildendpoints);
|
|
|
e0018b |
}
|
|
|
e0018b |
}
|
|
|
e0018b |
|
|
|
e0018b |
@@ -366,6 +466,7 @@ int cxl_filter_walk(struct cxl_ctx *ctx, struct cxl_filter_params *p)
|
|
|
e0018b |
struct json_object *jdevs = NULL, *jbuses = NULL, *jports = NULL;
|
|
|
e0018b |
struct json_object *jplatform = json_object_new_array();
|
|
|
e0018b |
unsigned long flags = params_to_flags(p);
|
|
|
e0018b |
+ struct json_object *jeps = NULL;
|
|
|
e0018b |
struct cxl_memdev *memdev;
|
|
|
e0018b |
int top_level_objs = 0;
|
|
|
e0018b |
struct cxl_bus *bus;
|
|
|
e0018b |
@@ -387,6 +488,10 @@ int cxl_filter_walk(struct cxl_ctx *ctx, struct cxl_filter_params *p)
|
|
|
e0018b |
if (!jports)
|
|
|
e0018b |
goto err;
|
|
|
e0018b |
|
|
|
e0018b |
+ jeps = json_object_new_array();
|
|
|
e0018b |
+ if (!jeps)
|
|
|
e0018b |
+ goto err;
|
|
|
e0018b |
+
|
|
|
e0018b |
dbg(p, "walk memdevs\n");
|
|
|
e0018b |
cxl_memdev_foreach(ctx, memdev) {
|
|
|
e0018b |
struct json_object *jdev;
|
|
|
e0018b |
@@ -408,6 +513,7 @@ int cxl_filter_walk(struct cxl_ctx *ctx, struct cxl_filter_params *p)
|
|
|
e0018b |
cxl_bus_foreach(ctx, bus) {
|
|
|
e0018b |
struct json_object *jbus = NULL;
|
|
|
e0018b |
struct json_object *jchildports = NULL;
|
|
|
e0018b |
+ struct json_object *jchildeps = NULL;
|
|
|
e0018b |
struct cxl_port *port = cxl_bus_get_port(bus);
|
|
|
e0018b |
const char *devname = cxl_bus_get_devname(bus);
|
|
|
e0018b |
|
|
|
e0018b |
@@ -431,12 +537,23 @@ int cxl_filter_walk(struct cxl_ctx *ctx, struct cxl_filter_params *p)
|
|
|
e0018b |
continue;
|
|
|
e0018b |
}
|
|
|
e0018b |
}
|
|
|
e0018b |
+ if (p->endpoints) {
|
|
|
e0018b |
+ jchildeps = json_object_new_array();
|
|
|
e0018b |
+ if (!jchildeps) {
|
|
|
e0018b |
+ err(p,
|
|
|
e0018b |
+ "%s: failed to enumerate child endpoints\n",
|
|
|
e0018b |
+ devname);
|
|
|
e0018b |
+ continue;
|
|
|
e0018b |
+ }
|
|
|
e0018b |
+ }
|
|
|
e0018b |
}
|
|
|
e0018b |
walk_children:
|
|
|
e0018b |
dbg(p, "walk ports\n");
|
|
|
e0018b |
walk_child_ports(port, p, pick_array(jchildports, jports),
|
|
|
e0018b |
- flags);
|
|
|
e0018b |
+ pick_array(jchildeps, jeps), flags);
|
|
|
e0018b |
cond_add_put_array_suffix(jbus, "ports", devname, jchildports);
|
|
|
e0018b |
+ cond_add_put_array_suffix(jbus, "endpoints", devname,
|
|
|
e0018b |
+ jchildeps);
|
|
|
e0018b |
}
|
|
|
e0018b |
|
|
|
e0018b |
if (json_object_array_length(jdevs))
|
|
|
e0018b |
@@ -445,10 +562,13 @@ walk_children:
|
|
|
e0018b |
top_level_objs++;
|
|
|
e0018b |
if (json_object_array_length(jports))
|
|
|
e0018b |
top_level_objs++;
|
|
|
e0018b |
+ if (json_object_array_length(jeps))
|
|
|
e0018b |
+ top_level_objs++;
|
|
|
e0018b |
|
|
|
e0018b |
splice_array(p, jdevs, jplatform, "anon memdevs", top_level_objs > 1);
|
|
|
e0018b |
splice_array(p, jbuses, jplatform, "buses", top_level_objs > 1);
|
|
|
e0018b |
splice_array(p, jports, jplatform, "ports", top_level_objs > 1);
|
|
|
e0018b |
+ splice_array(p, jeps, jplatform, "endpoints", top_level_objs > 1);
|
|
|
e0018b |
|
|
|
e0018b |
util_display_json_array(stdout, jplatform, flags);
|
|
|
e0018b |
|
|
|
e0018b |
@@ -457,6 +577,7 @@ err:
|
|
|
e0018b |
json_object_put(jdevs);
|
|
|
e0018b |
json_object_put(jbuses);
|
|
|
e0018b |
json_object_put(jports);
|
|
|
e0018b |
+ json_object_put(jeps);
|
|
|
e0018b |
json_object_put(jplatform);
|
|
|
e0018b |
return -ENOMEM;
|
|
|
e0018b |
}
|
|
|
e0018b |
diff --git a/cxl/filter.h b/cxl/filter.h
|
|
|
e0018b |
index 0d83304..bbd341c 100644
|
|
|
e0018b |
--- a/cxl/filter.h
|
|
|
e0018b |
+++ b/cxl/filter.h
|
|
|
e0018b |
@@ -11,7 +11,9 @@ struct cxl_filter_params {
|
|
|
e0018b |
const char *serial_filter;
|
|
|
e0018b |
const char *bus_filter;
|
|
|
e0018b |
const char *port_filter;
|
|
|
e0018b |
+ const char *endpoint_filter;
|
|
|
e0018b |
bool single;
|
|
|
e0018b |
+ bool endpoints;
|
|
|
e0018b |
bool memdevs;
|
|
|
e0018b |
bool ports;
|
|
|
e0018b |
bool buses;
|
|
|
e0018b |
diff --git a/cxl/json.c b/cxl/json.c
|
|
|
e0018b |
index d9f864e..08f6192 100644
|
|
|
e0018b |
--- a/cxl/json.c
|
|
|
e0018b |
+++ b/cxl/json.c
|
|
|
e0018b |
@@ -243,8 +243,9 @@ struct json_object *util_cxl_bus_to_json(struct cxl_bus *bus,
|
|
|
e0018b |
return jbus;
|
|
|
e0018b |
}
|
|
|
e0018b |
|
|
|
e0018b |
-struct json_object *util_cxl_port_to_json(struct cxl_port *port,
|
|
|
e0018b |
- unsigned long flags)
|
|
|
e0018b |
+static struct json_object *__util_cxl_port_to_json(struct cxl_port *port,
|
|
|
e0018b |
+ const char *name_key,
|
|
|
e0018b |
+ unsigned long flags)
|
|
|
e0018b |
{
|
|
|
e0018b |
const char *devname = cxl_port_get_devname(port);
|
|
|
e0018b |
struct json_object *jport, *jobj;
|
|
|
e0018b |
@@ -255,7 +256,7 @@ struct json_object *util_cxl_port_to_json(struct cxl_port *port,
|
|
|
e0018b |
|
|
|
e0018b |
jobj = json_object_new_string(devname);
|
|
|
e0018b |
if (jobj)
|
|
|
e0018b |
- json_object_object_add(jport, "port", jobj);
|
|
|
e0018b |
+ json_object_object_add(jport, name_key, jobj);
|
|
|
e0018b |
|
|
|
e0018b |
if (!cxl_port_is_enabled(port)) {
|
|
|
e0018b |
jobj = json_object_new_string("disabled");
|
|
|
e0018b |
@@ -265,3 +266,16 @@ struct json_object *util_cxl_port_to_json(struct cxl_port *port,
|
|
|
e0018b |
|
|
|
e0018b |
return jport;
|
|
|
e0018b |
}
|
|
|
e0018b |
+
|
|
|
e0018b |
+struct json_object *util_cxl_port_to_json(struct cxl_port *port,
|
|
|
e0018b |
+ unsigned long flags)
|
|
|
e0018b |
+{
|
|
|
e0018b |
+ return __util_cxl_port_to_json(port, "port", flags);
|
|
|
e0018b |
+}
|
|
|
e0018b |
+
|
|
|
e0018b |
+struct json_object *util_cxl_endpoint_to_json(struct cxl_endpoint *endpoint,
|
|
|
e0018b |
+ unsigned long flags)
|
|
|
e0018b |
+{
|
|
|
e0018b |
+ return __util_cxl_port_to_json(cxl_endpoint_get_port(endpoint),
|
|
|
e0018b |
+ "endpoint", flags);
|
|
|
e0018b |
+}
|
|
|
e0018b |
diff --git a/cxl/json.h b/cxl/json.h
|
|
|
e0018b |
index 36653db..8f45190 100644
|
|
|
e0018b |
--- a/cxl/json.h
|
|
|
e0018b |
+++ b/cxl/json.h
|
|
|
e0018b |
@@ -11,4 +11,6 @@ struct json_object *util_cxl_bus_to_json(struct cxl_bus *bus,
|
|
|
e0018b |
struct cxl_port;
|
|
|
e0018b |
struct json_object *util_cxl_port_to_json(struct cxl_port *port,
|
|
|
e0018b |
unsigned long flags);
|
|
|
e0018b |
+struct json_object *util_cxl_endpoint_to_json(struct cxl_endpoint *endpoint,
|
|
|
e0018b |
+ unsigned long flags);
|
|
|
e0018b |
#endif /* __CXL_UTIL_JSON_H__ */
|
|
|
e0018b |
diff --git a/cxl/lib/libcxl.c b/cxl/lib/libcxl.c
|
|
|
e0018b |
index 03eff3c..a25e715 100644
|
|
|
e0018b |
--- a/cxl/lib/libcxl.c
|
|
|
e0018b |
+++ b/cxl/lib/libcxl.c
|
|
|
e0018b |
@@ -67,14 +67,18 @@ static void free_memdev(struct cxl_memdev *memdev, struct list_head *head)
|
|
|
e0018b |
}
|
|
|
e0018b |
|
|
|
e0018b |
static void free_port(struct cxl_port *port, struct list_head *head);
|
|
|
e0018b |
+static void free_endpoint(struct cxl_endpoint *endpoint, struct list_head *head);
|
|
|
e0018b |
static void __free_port(struct cxl_port *port, struct list_head *head)
|
|
|
e0018b |
{
|
|
|
e0018b |
struct cxl_port *child, *_c;
|
|
|
e0018b |
+ struct cxl_endpoint *endpoint, *_e;
|
|
|
e0018b |
|
|
|
e0018b |
if (head)
|
|
|
e0018b |
list_del_from(head, &port->list);
|
|
|
e0018b |
list_for_each_safe(&port->child_ports, child, _c, list)
|
|
|
e0018b |
free_port(child, &port->child_ports);
|
|
|
e0018b |
+ list_for_each_safe(&port->endpoints, endpoint, _e, port.list)
|
|
|
e0018b |
+ free_endpoint(endpoint, &port->endpoints);
|
|
|
e0018b |
kmod_module_unref(port->module);
|
|
|
e0018b |
free(port->dev_buf);
|
|
|
e0018b |
free(port->dev_path);
|
|
|
e0018b |
@@ -87,6 +91,12 @@ static void free_port(struct cxl_port *port, struct list_head *head)
|
|
|
e0018b |
free(port);
|
|
|
e0018b |
}
|
|
|
e0018b |
|
|
|
e0018b |
+static void free_endpoint(struct cxl_endpoint *endpoint, struct list_head *head)
|
|
|
e0018b |
+{
|
|
|
e0018b |
+ __free_port(&endpoint->port, head);
|
|
|
e0018b |
+ free(endpoint);
|
|
|
e0018b |
+}
|
|
|
e0018b |
+
|
|
|
e0018b |
static void free_bus(struct cxl_bus *bus, struct list_head *head)
|
|
|
e0018b |
{
|
|
|
e0018b |
__free_port(&bus->port, head);
|
|
|
e0018b |
@@ -500,6 +510,7 @@ static int cxl_port_init(struct cxl_port *port, struct cxl_port *parent_port,
|
|
|
e0018b |
port->parent = parent_port;
|
|
|
e0018b |
|
|
|
e0018b |
list_head_init(&port->child_ports);
|
|
|
e0018b |
+ list_head_init(&port->endpoints);
|
|
|
e0018b |
|
|
|
e0018b |
port->dev_path = strdup(cxlport_base);
|
|
|
e0018b |
if (!port->dev_path)
|
|
|
e0018b |
@@ -529,6 +540,97 @@ err:
|
|
|
e0018b |
return -ENOMEM;
|
|
|
e0018b |
}
|
|
|
e0018b |
|
|
|
e0018b |
+static void *add_cxl_endpoint(void *parent, int id, const char *cxlep_base)
|
|
|
e0018b |
+{
|
|
|
e0018b |
+ const char *devname = devpath_to_devname(cxlep_base);
|
|
|
e0018b |
+ struct cxl_endpoint *endpoint, *endpoint_dup;
|
|
|
e0018b |
+ struct cxl_port *port = parent;
|
|
|
e0018b |
+ struct cxl_ctx *ctx = cxl_port_get_ctx(port);
|
|
|
e0018b |
+ int rc;
|
|
|
e0018b |
+
|
|
|
e0018b |
+ dbg(ctx, "%s: base: \'%s\'\n", devname, cxlep_base);
|
|
|
e0018b |
+
|
|
|
e0018b |
+ endpoint = calloc(1, sizeof(*endpoint));
|
|
|
e0018b |
+ if (!endpoint)
|
|
|
e0018b |
+ return NULL;
|
|
|
e0018b |
+
|
|
|
e0018b |
+ rc = cxl_port_init(&endpoint->port, port, CXL_PORT_ENDPOINT, ctx, id,
|
|
|
e0018b |
+ cxlep_base);
|
|
|
e0018b |
+ if (rc)
|
|
|
e0018b |
+ goto err;
|
|
|
e0018b |
+
|
|
|
e0018b |
+ cxl_endpoint_foreach(port, endpoint_dup)
|
|
|
e0018b |
+ if (endpoint_dup->port.id == endpoint->port.id) {
|
|
|
e0018b |
+ free_endpoint(endpoint, NULL);
|
|
|
e0018b |
+ return endpoint_dup;
|
|
|
e0018b |
+ }
|
|
|
e0018b |
+
|
|
|
e0018b |
+ list_add(&port->endpoints, &endpoint->port.list);
|
|
|
e0018b |
+ return endpoint;
|
|
|
e0018b |
+
|
|
|
e0018b |
+err:
|
|
|
e0018b |
+ free(endpoint);
|
|
|
e0018b |
+ return NULL;
|
|
|
e0018b |
+
|
|
|
e0018b |
+}
|
|
|
e0018b |
+
|
|
|
e0018b |
+static void cxl_endpoints_init(struct cxl_port *port)
|
|
|
e0018b |
+{
|
|
|
e0018b |
+ struct cxl_ctx *ctx = cxl_port_get_ctx(port);
|
|
|
e0018b |
+
|
|
|
e0018b |
+ if (port->endpoints_init)
|
|
|
e0018b |
+ return;
|
|
|
e0018b |
+
|
|
|
e0018b |
+ port->endpoints_init = 1;
|
|
|
e0018b |
+
|
|
|
e0018b |
+ sysfs_device_parse(ctx, port->dev_path, "endpoint", port,
|
|
|
e0018b |
+ add_cxl_endpoint);
|
|
|
e0018b |
+}
|
|
|
e0018b |
+
|
|
|
e0018b |
+CXL_EXPORT struct cxl_ctx *cxl_endpoint_get_ctx(struct cxl_endpoint *endpoint)
|
|
|
e0018b |
+{
|
|
|
e0018b |
+ return endpoint->port.ctx;
|
|
|
e0018b |
+}
|
|
|
e0018b |
+
|
|
|
e0018b |
+CXL_EXPORT struct cxl_endpoint *cxl_endpoint_get_first(struct cxl_port *port)
|
|
|
e0018b |
+{
|
|
|
e0018b |
+ cxl_endpoints_init(port);
|
|
|
e0018b |
+
|
|
|
e0018b |
+ return list_top(&port->endpoints, struct cxl_endpoint, port.list);
|
|
|
e0018b |
+}
|
|
|
e0018b |
+
|
|
|
e0018b |
+CXL_EXPORT struct cxl_endpoint *cxl_endpoint_get_next(struct cxl_endpoint *endpoint)
|
|
|
e0018b |
+{
|
|
|
e0018b |
+ struct cxl_port *port = endpoint->port.parent;
|
|
|
e0018b |
+
|
|
|
e0018b |
+ return list_next(&port->endpoints, endpoint, port.list);
|
|
|
e0018b |
+}
|
|
|
e0018b |
+
|
|
|
e0018b |
+CXL_EXPORT const char *cxl_endpoint_get_devname(struct cxl_endpoint *endpoint)
|
|
|
e0018b |
+{
|
|
|
e0018b |
+ return devpath_to_devname(endpoint->port.dev_path);
|
|
|
e0018b |
+}
|
|
|
e0018b |
+
|
|
|
e0018b |
+CXL_EXPORT int cxl_endpoint_get_id(struct cxl_endpoint *endpoint)
|
|
|
e0018b |
+{
|
|
|
e0018b |
+ return endpoint->port.id;
|
|
|
e0018b |
+}
|
|
|
e0018b |
+
|
|
|
e0018b |
+CXL_EXPORT struct cxl_port *cxl_endpoint_get_parent(struct cxl_endpoint *endpoint)
|
|
|
e0018b |
+{
|
|
|
e0018b |
+ return endpoint->port.parent;
|
|
|
e0018b |
+}
|
|
|
e0018b |
+
|
|
|
e0018b |
+CXL_EXPORT struct cxl_port *cxl_endpoint_get_port(struct cxl_endpoint *endpoint)
|
|
|
e0018b |
+{
|
|
|
e0018b |
+ return &endpoint->port;
|
|
|
e0018b |
+}
|
|
|
e0018b |
+
|
|
|
e0018b |
+CXL_EXPORT int cxl_endpoint_is_enabled(struct cxl_endpoint *endpoint)
|
|
|
e0018b |
+{
|
|
|
e0018b |
+ return cxl_port_is_enabled(&endpoint->port);
|
|
|
e0018b |
+}
|
|
|
e0018b |
+
|
|
|
e0018b |
static void *add_cxl_port(void *parent, int id, const char *cxlport_base)
|
|
|
e0018b |
{
|
|
|
e0018b |
const char *devname = devpath_to_devname(cxlport_base);
|
|
|
e0018b |
@@ -619,6 +721,11 @@ CXL_EXPORT bool cxl_port_is_switch(struct cxl_port *port)
|
|
|
e0018b |
return port->type == CXL_PORT_SWITCH;
|
|
|
e0018b |
}
|
|
|
e0018b |
|
|
|
e0018b |
+CXL_EXPORT bool cxl_port_is_endpoint(struct cxl_port *port)
|
|
|
e0018b |
+{
|
|
|
e0018b |
+ return port->type == CXL_PORT_ENDPOINT;
|
|
|
e0018b |
+}
|
|
|
e0018b |
+
|
|
|
e0018b |
CXL_EXPORT struct cxl_bus *cxl_port_get_bus(struct cxl_port *port)
|
|
|
e0018b |
{
|
|
|
e0018b |
struct cxl_bus *bus;
|
|
|
e0018b |
diff --git a/cxl/lib/libcxl.sym b/cxl/lib/libcxl.sym
|
|
|
e0018b |
index a7e923f..7a51a0c 100644
|
|
|
e0018b |
--- a/cxl/lib/libcxl.sym
|
|
|
e0018b |
+++ b/cxl/lib/libcxl.sym
|
|
|
e0018b |
@@ -93,5 +93,14 @@ global:
|
|
|
e0018b |
cxl_port_is_root;
|
|
|
e0018b |
cxl_port_is_switch;
|
|
|
e0018b |
cxl_port_to_bus;
|
|
|
e0018b |
+ cxl_port_is_endpoint;
|
|
|
e0018b |
cxl_port_get_bus;
|
|
|
e0018b |
+ cxl_endpoint_get_first;
|
|
|
e0018b |
+ cxl_endpoint_get_next;
|
|
|
e0018b |
+ cxl_endpoint_get_devname;
|
|
|
e0018b |
+ cxl_endpoint_get_id;
|
|
|
e0018b |
+ cxl_endpoint_get_ctx;
|
|
|
e0018b |
+ cxl_endpoint_is_enabled;
|
|
|
e0018b |
+ cxl_endpoint_get_parent;
|
|
|
e0018b |
+ cxl_endpoint_get_port;
|
|
|
e0018b |
} LIBCXL_1;
|
|
|
e0018b |
diff --git a/cxl/lib/private.h b/cxl/lib/private.h
|
|
|
e0018b |
index 637f90d..cedd2f2 100644
|
|
|
e0018b |
--- a/cxl/lib/private.h
|
|
|
e0018b |
+++ b/cxl/lib/private.h
|
|
|
e0018b |
@@ -17,6 +17,7 @@ struct cxl_pmem {
|
|
|
e0018b |
char *dev_path;
|
|
|
e0018b |
};
|
|
|
e0018b |
|
|
|
e0018b |
+struct cxl_endpoint;
|
|
|
e0018b |
struct cxl_memdev {
|
|
|
e0018b |
int id, major, minor;
|
|
|
e0018b |
void *dev_buf;
|
|
|
e0018b |
@@ -32,11 +33,13 @@ struct cxl_memdev {
|
|
|
e0018b |
struct kmod_module *module;
|
|
|
e0018b |
struct cxl_pmem *pmem;
|
|
|
e0018b |
unsigned long long serial;
|
|
|
e0018b |
+ struct cxl_endpoint *endpoint;
|
|
|
e0018b |
};
|
|
|
e0018b |
|
|
|
e0018b |
enum cxl_port_type {
|
|
|
e0018b |
CXL_PORT_ROOT,
|
|
|
e0018b |
CXL_PORT_SWITCH,
|
|
|
e0018b |
+ CXL_PORT_ENDPOINT,
|
|
|
e0018b |
};
|
|
|
e0018b |
|
|
|
e0018b |
struct cxl_port {
|
|
|
e0018b |
@@ -46,6 +49,7 @@ struct cxl_port {
|
|
|
e0018b |
char *dev_path;
|
|
|
e0018b |
char *uport;
|
|
|
e0018b |
int ports_init;
|
|
|
e0018b |
+ int endpoints_init;
|
|
|
e0018b |
struct cxl_ctx *ctx;
|
|
|
e0018b |
struct cxl_bus *bus;
|
|
|
e0018b |
enum cxl_port_type type;
|
|
|
e0018b |
@@ -53,12 +57,18 @@ struct cxl_port {
|
|
|
e0018b |
struct kmod_module *module;
|
|
|
e0018b |
struct list_node list;
|
|
|
e0018b |
struct list_head child_ports;
|
|
|
e0018b |
+ struct list_head endpoints;
|
|
|
e0018b |
};
|
|
|
e0018b |
|
|
|
e0018b |
struct cxl_bus {
|
|
|
e0018b |
struct cxl_port port;
|
|
|
e0018b |
};
|
|
|
e0018b |
|
|
|
e0018b |
+struct cxl_endpoint {
|
|
|
e0018b |
+ struct cxl_port port;
|
|
|
e0018b |
+ struct cxl_memdev *memdev;
|
|
|
e0018b |
+};
|
|
|
e0018b |
+
|
|
|
e0018b |
enum cxl_cmd_query_status {
|
|
|
e0018b |
CXL_CMD_QUERY_NOT_RUN = 0,
|
|
|
e0018b |
CXL_CMD_QUERY_OK,
|
|
|
e0018b |
diff --git a/cxl/libcxl.h b/cxl/libcxl.h
|
|
|
e0018b |
index efbb397..f6ba9a1 100644
|
|
|
e0018b |
--- a/cxl/libcxl.h
|
|
|
e0018b |
+++ b/cxl/libcxl.h
|
|
|
e0018b |
@@ -81,12 +81,27 @@ struct cxl_port *cxl_port_get_parent(struct cxl_port *port);
|
|
|
e0018b |
bool cxl_port_is_root(struct cxl_port *port);
|
|
|
e0018b |
bool cxl_port_is_switch(struct cxl_port *port);
|
|
|
e0018b |
struct cxl_bus *cxl_port_to_bus(struct cxl_port *port);
|
|
|
e0018b |
+bool cxl_port_is_endpoint(struct cxl_port *port);
|
|
|
e0018b |
struct cxl_bus *cxl_port_get_bus(struct cxl_port *port);
|
|
|
e0018b |
|
|
|
e0018b |
#define cxl_port_foreach(parent, port) \
|
|
|
e0018b |
for (port = cxl_port_get_first(parent); port != NULL; \
|
|
|
e0018b |
port = cxl_port_get_next(port))
|
|
|
e0018b |
|
|
|
e0018b |
+struct cxl_endpoint;
|
|
|
e0018b |
+struct cxl_endpoint *cxl_endpoint_get_first(struct cxl_port *parent);
|
|
|
e0018b |
+struct cxl_endpoint *cxl_endpoint_get_next(struct cxl_endpoint *endpoint);
|
|
|
e0018b |
+const char *cxl_endpoint_get_devname(struct cxl_endpoint *endpoint);
|
|
|
e0018b |
+int cxl_endpoint_get_id(struct cxl_endpoint *endpoint);
|
|
|
e0018b |
+struct cxl_ctx *cxl_endpoint_get_ctx(struct cxl_endpoint *endpoint);
|
|
|
e0018b |
+int cxl_endpoint_is_enabled(struct cxl_endpoint *endpoint);
|
|
|
e0018b |
+struct cxl_port *cxl_endpoint_get_parent(struct cxl_endpoint *endpoint);
|
|
|
e0018b |
+struct cxl_port *cxl_endpoint_get_port(struct cxl_endpoint *endpoint);
|
|
|
e0018b |
+
|
|
|
e0018b |
+#define cxl_endpoint_foreach(port, endpoint) \
|
|
|
e0018b |
+ for (endpoint = cxl_endpoint_get_first(port); endpoint != NULL; \
|
|
|
e0018b |
+ endpoint = cxl_endpoint_get_next(endpoint))
|
|
|
e0018b |
+
|
|
|
e0018b |
struct cxl_cmd;
|
|
|
e0018b |
const char *cxl_cmd_get_devname(struct cxl_cmd *cmd);
|
|
|
e0018b |
struct cxl_cmd *cxl_cmd_new_raw(struct cxl_memdev *memdev, int opcode);
|
|
|
e0018b |
diff --git a/cxl/list.c b/cxl/list.c
|
|
|
e0018b |
index 01ab19b..b15e01c 100644
|
|
|
e0018b |
--- a/cxl/list.c
|
|
|
e0018b |
+++ b/cxl/list.c
|
|
|
e0018b |
@@ -31,6 +31,11 @@ static const struct option options[] = {
|
|
|
e0018b |
OPT_BOOLEAN('P', "ports", ¶m.ports, "include CXL port info"),
|
|
|
e0018b |
OPT_BOOLEAN('S', "single", ¶m.single,
|
|
|
e0018b |
"skip listing descendant objects"),
|
|
|
e0018b |
+ OPT_STRING('e', "endpoint", ¶m.endpoint_filter,
|
|
|
e0018b |
+ "endpoint device name",
|
|
|
e0018b |
+ "filter by CXL endpoint device name(s)"),
|
|
|
e0018b |
+ OPT_BOOLEAN('E', "endpoints", ¶m.endpoints,
|
|
|
e0018b |
+ "include CXL endpoint info"),
|
|
|
e0018b |
OPT_BOOLEAN('i', "idle", ¶m.idle, "include disabled devices"),
|
|
|
e0018b |
OPT_BOOLEAN('u', "human", ¶m.human,
|
|
|
e0018b |
"use human friendly number formats "),
|
|
|
e0018b |
@@ -44,7 +49,8 @@ static const struct option options[] = {
|
|
|
e0018b |
|
|
|
e0018b |
static int num_list_flags(void)
|
|
|
e0018b |
{
|
|
|
e0018b |
- return !!param.memdevs + !!param.buses + !!param.ports;
|
|
|
e0018b |
+ return !!param.memdevs + !!param.buses + !!param.ports +
|
|
|
e0018b |
+ !!param.endpoints;
|
|
|
e0018b |
}
|
|
|
e0018b |
|
|
|
e0018b |
int cmd_list(int argc, const char **argv, struct cxl_ctx *ctx)
|
|
|
e0018b |
@@ -74,6 +80,8 @@ int cmd_list(int argc, const char **argv, struct cxl_ctx *ctx)
|
|
|
e0018b |
param.buses = true;
|
|
|
e0018b |
if (param.port_filter)
|
|
|
e0018b |
param.ports = true;
|
|
|
e0018b |
+ if (param.endpoint_filter)
|
|
|
e0018b |
+ param.endpoints = true;
|
|
|
e0018b |
if (num_list_flags() == 0) {
|
|
|
e0018b |
/*
|
|
|
e0018b |
* TODO: We likely want to list regions by default if
|
|
|
e0018b |
@@ -96,6 +104,9 @@ int cmd_list(int argc, const char **argv, struct cxl_ctx *ctx)
|
|
|
e0018b |
if (cxl_filter_has(param.port_filter, "root") && param.ports)
|
|
|
e0018b |
param.buses = true;
|
|
|
e0018b |
|
|
|
e0018b |
+ if (cxl_filter_has(param.port_filter, "endpoint") && param.ports)
|
|
|
e0018b |
+ param.endpoints = true;
|
|
|
e0018b |
+
|
|
|
e0018b |
dbg(¶m, "walk topology\n");
|
|
|
e0018b |
return cxl_filter_walk(ctx, ¶m;;
|
|
|
e0018b |
}
|
|
|
e0018b |
--
|
|
|
e0018b |
2.27.0
|
|
|
e0018b |
|