Add initial version 81/26081/2
authorDamian Hobson-Garcia <dhobsong@igel.co.jp>
Tue, 24 Nov 2020 08:16:39 +0000 (17:16 +0900)
committerDamian Hobson-Garcia <dhobsong@igel.co.jp>
Fri, 19 Feb 2021 10:48:23 +0000 (10:48 +0000)
The initial version implements the basic functionality
of the client/server communication and lease management.

For now, one lease is created per valid connector (dependent
on CRTC availablity).

Bug-AGL: SPEC-3729

Signed-off-by: Damian Hobson-Garcia <dhobsong@igel.co.jp>
Change-Id: I2b37a892742cc22bdc53a5172c8ad3d8a7bb5e66

40 files changed:
.clang-format [new file with mode: 0644]
LICENSE [new file with mode: 0644]
README.md [new file with mode: 0644]
common/log.c [new file with mode: 0644]
common/log.h [new file with mode: 0644]
common/meson.build [new file with mode: 0644]
common/socket-path.c [new file with mode: 0644]
common/socket-path.h [new file with mode: 0644]
common/test/test-helpers.c [new file with mode: 0644]
common/test/test-helpers.h [new file with mode: 0644]
drm-lease-manager/drm-lease.h [new file with mode: 0644]
drm-lease-manager/lease-manager.c [new file with mode: 0644]
drm-lease-manager/lease-manager.h [new file with mode: 0644]
drm-lease-manager/lease-server.c [new file with mode: 0644]
drm-lease-manager/lease-server.h [new file with mode: 0644]
drm-lease-manager/main.c [new file with mode: 0644]
drm-lease-manager/meson.build [new file with mode: 0644]
drm-lease-manager/test/lease-manager-test.c [new file with mode: 0644]
drm-lease-manager/test/lease-server-test.c [new file with mode: 0644]
drm-lease-manager/test/meson.build [new file with mode: 0644]
drm-lease-manager/test/test-drm-device.c [new file with mode: 0644]
drm-lease-manager/test/test-drm-device.h [new file with mode: 0644]
drm-lease-manager/test/test-socket-client.c [new file with mode: 0644]
drm-lease-manager/test/test-socket-client.h [new file with mode: 0644]
examples/dlm-client-test/dlm-client-test.c [new file with mode: 0644]
examples/dlm-client-test/meson.build [new file with mode: 0644]
examples/meson.build [new file with mode: 0644]
libdlmclient/dlmclient.c [new file with mode: 0644]
libdlmclient/dlmclient.h [new file with mode: 0644]
libdlmclient/docs/Doxyfile.in [new file with mode: 0644]
libdlmclient/docs/meson.build [new file with mode: 0644]
libdlmclient/meson.build [new file with mode: 0644]
libdlmclient/test/libdlmclient-test.c [new file with mode: 0644]
libdlmclient/test/meson.build [new file with mode: 0644]
libdlmclient/test/test-socket-server.c [new file with mode: 0644]
libdlmclient/test/test-socket-server.h [new file with mode: 0644]
meson.build [new file with mode: 0644]
meson_options.txt [new file with mode: 0644]
subprojects/fff.wrap [new file with mode: 0644]
subprojects/packagefiles/fff-1.0/meson.build [new file with mode: 0644]

diff --git a/.clang-format b/.clang-format
new file mode 100644 (file)
index 0000000..97c7b2c
--- /dev/null
@@ -0,0 +1,9 @@
+BasedOnStyle: LLVM
+IndentWidth: 8
+ColumnLimit: 80
+UseTab: Always
+BreakBeforeBraces: Linux
+AllowShortIfStatementsOnASingleLine: false
+AllowShortFunctionsOnASingleLine: None
+IndentCaseLabels: false
+AlignEscapedNewlines: Left
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..d645695
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..5368f6b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,85 @@
+# DRM Lease Manager
+
+The DRM Lease Manager uses the DRM Lease feature, introduced in the Linux kernel version 4.15,
+to partition display controller output resources between multiple processes.
+
+For more information on the DRM lease functionality, please see the blog posts published by Keith
+Packard, who developed the feature, at https://keithp.com/blogs/DRM-lease/
+
+This repository contains a user space daemon to create and manage DRM leases, and distribute them
+to client applications.  This implementation is independent of any window system / compositor,
+such as X11 or wayland, so can be used with clients that directly access a DRM device.
+
+This repository also provides a library that clients can use to communicate with the DRM Lease Manager.
+
+## Building
+
+Build this repository requires a recent version of the [meson](https://mesonbuild.com/Getting-meson.html) build system.
+
+The basic build procedure is as follows:
+
+    meson <build_dir>
+    ninja -C <build_dir>
+    sudo ninja -C <build_dir> install
+
+`<build_dir>` can be any directory name, but `build` is commonly used.
+
+## Running
+
+Once installed, running the following command will start the DRM Lease Manager daemon
+
+    drm-lease-manager [<path DRM device>]
+
+If no DRM device is specified, `/dev/dri/card0` will be used.  
+More detailed options can be displayed by specifying the `-h` flag.
+
+### Lease naming
+
+One DRM lease will be created for each connector on the DRM device (up to the number of available CRTCs).
+
+The names of the DRM leases will have the following pattern:
+
+    <device>-<connector name>
+
+So, for example, a DRM lease for the first LVDS device on the device `/dev/dri/card0` would be named
+`card0-LVDS-1`.
+
+## Client API usage
+
+The libdmclient handles all communication with the DRM Lease Manager and provides file descriptors that
+can be used as if the DRM device was opened directly. Clients only need to replace their calls to
+`drmOpen()` and `drmClose()` with the appropriate libdlmclient API calls.
+
+The client library API is described in `dlmclient.h` in the `libdlmclient` directory.
+
+If doxygen is available, building the repository will generate doxygen documentation in the
+`<build_dir>/libdlmclient/docs/html` directory.
+
+### Examples
+
+_Error handling has been omitted for brevity and clarity of examples._
+
+#### Requesting a lease from the DRM Lease Manager
+
+```c
+  struct dlm_lease *lease = dlm_get_lease("card0-HDMI-A-1");
+  int drm_device_fd = dlm_lease_fd(lease);
+```
+
+`drm_device_fd` can now be used to access the DRM device
+
+#### Releasing a lease
+
+```c
+  dlm_release_lease(lease);
+```
+
+**Note: `drm_device_fd` is not usable after calling `dlm_release_lease()`**
+
+## Runtime directory
+A runtime directory under the `/var` system directory is used by the drm-lease-manager and clients to
+communicate with each other.  
+The default path is `/var/run/drm-lease-manager`, but can be changed by setting the `-Druntime_subdir`
+option during configuration with `meson`.
+
+The runtime directory can also be specified at runtime by setting the `DLM_RUNTIME_PATH` environment variable.
diff --git a/common/log.c b/common/log.c
new file mode 100644 (file)
index 0000000..f3f8899
--- /dev/null
@@ -0,0 +1,38 @@
+/* Copyright 2020-2021 IGEL Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "log.h"
+
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdio.h>
+
+static bool debug_log = false;
+
+void dlm_log_enable_debug(bool enable)
+{
+       debug_log = enable;
+}
+
+void dlm_log_print(bool debug, FILE *stream, char *fmt, ...)
+{
+       if (debug && !debug_log)
+               return;
+
+       va_list argl;
+       va_start(argl, fmt);
+       vfprintf(stream, fmt, argl);
+       va_end(argl);
+}
diff --git a/common/log.h b/common/log.h
new file mode 100644 (file)
index 0000000..cd0c0e3
--- /dev/null
@@ -0,0 +1,33 @@
+/* Copyright 2020-2021 IGEL Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef LOG_H
+#define LOG_H
+
+#include <stdbool.h>
+#include <stdio.h>
+
+#define DEBUG_LOG(FMT, ...) \
+       dlm_log_print(true, stdout, "DEBUG: %s: " FMT, __func__, ##__VA_ARGS__)
+#define INFO_LOG(FMT, ...) \
+       dlm_log_print(false, stdout, "INFO: " FMT, ##__VA_ARGS__)
+#define WARN_LOG(FMT, ...) \
+       dlm_log_print(false, stderr, "WARNING: " FMT, ##__VA_ARGS__)
+#define ERROR_LOG(FMT, ...) \
+       dlm_log_print(false, stderr, "ERROR: " FMT, ##__VA_ARGS__)
+
+void dlm_log_enable_debug(bool enable);
+void dlm_log_print(bool debug, FILE *stream, char *fmt, ...);
+#endif
diff --git a/common/meson.build b/common/meson.build
new file mode 100644 (file)
index 0000000..e465fa5
--- /dev/null
@@ -0,0 +1,22 @@
+libdlmcommon_sources = [
+        'socket-path.c',
+        'log.c'
+]
+
+libdlmcommon_inc = [include_directories('.')]
+
+if enable_tests
+    libdlmcommon_inc += include_directories('test')
+    libdlmcommon_sources += ['test/test-helpers.c']
+endif
+
+libdlmcommon = static_library(
+        'common',
+        sources: libdlmcommon_sources,
+        include_directories : configuration_inc,
+)
+
+dlmcommon_dep = declare_dependency(
+    link_with : libdlmcommon,
+    include_directories : libdlmcommon_inc
+)
diff --git a/common/socket-path.c b/common/socket-path.c
new file mode 100644 (file)
index 0000000..7a412da
--- /dev/null
@@ -0,0 +1,48 @@
+/* Copyright 2020-2021 IGEL Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "socket-path.h"
+#include "config.h"
+#include "log.h"
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define RUNTIME_PATH DLM_DEFAULT_RUNTIME_PATH
+
+bool sockaddr_set_lease_server_path(struct sockaddr_un *sa,
+                                   const char *lease_name)
+{
+       int maxlen = sizeof(sa->sun_path);
+       char *socket_dir = getenv("DLM_RUNTIME_PATH") ?: RUNTIME_PATH;
+
+       int len =
+           snprintf(sa->sun_path, maxlen, "%s/%s", socket_dir, lease_name);
+
+       if (len < 0) {
+               DEBUG_LOG("Socket path creation failed: %s\n", strerror(errno));
+               return false;
+       }
+       if (len >= maxlen) {
+               errno = ENAMETOOLONG;
+               DEBUG_LOG("Socket directoy path too long. "
+                         "Full path to socket must be less than %d bytes\n",
+                         maxlen);
+               return false;
+       }
+       return true;
+}
diff --git a/common/socket-path.h b/common/socket-path.h
new file mode 100644 (file)
index 0000000..b05c60f
--- /dev/null
@@ -0,0 +1,26 @@
+/* Copyright 2020-2021 IGEL Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SOCKET_PATH_H
+#define SOCKET_PATH_H
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <sys/un.h>
+
+bool sockaddr_set_lease_server_path(struct sockaddr_un *dest,
+                                   const char *lease_name);
+
+#endif
diff --git a/common/test/test-helpers.c b/common/test/test-helpers.c
new file mode 100644 (file)
index 0000000..ea37a89
--- /dev/null
@@ -0,0 +1,56 @@
+/* Copyright 2020-2021 IGEL Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "test-helpers.h"
+#include <check.h>
+
+#include <errno.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+int get_dummy_fd(void)
+{
+       return dup(STDIN_FILENO);
+}
+
+void check_fd_equality(int fd1, int fd2)
+{
+       struct stat s1, s2;
+       ck_assert_int_eq(fstat(fd1, &s1), 0);
+       ck_assert_int_eq(fstat(fd2, &s2), 0);
+       ck_assert_int_eq(s1.st_dev, s2.st_dev);
+       ck_assert_int_eq(s1.st_ino, s2.st_ino);
+}
+
+void check_fd_is_open(int fd)
+{
+       struct stat st;
+       ck_assert_int_eq(fstat(fd, &st), 0);
+}
+
+void check_fd_is_closed(int fd)
+{
+       struct stat st;
+       ck_assert_int_ne(fstat(fd, &st), 0);
+       ck_assert_int_eq(errno, EBADF);
+}
+
+void check_uint_array_eq(const uint32_t *a, const uint32_t *b, int cnt)
+{
+       for (int i = 0; i < cnt; i++) {
+               ck_assert_msg(a[i] == b[i], "Array diff at index %d (%u != %u)",
+                             i, a[i], b[i]);
+       }
+}
diff --git a/common/test/test-helpers.h b/common/test/test-helpers.h
new file mode 100644 (file)
index 0000000..8886acd
--- /dev/null
@@ -0,0 +1,34 @@
+/* Copyright 2020-2021 IGEL Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef TEST_HELPERS_H
+#define TEST_HELPERS_H
+
+#include <stdint.h>
+
+#define UNUSED(x) (void)(x)
+#define ARRAY_LEN(x) ((int)(sizeof(x) / sizeof(x[0])))
+
+/* Get a vaild fd to use a a placeholder.
+ * The dummy fd should never be used for anything other
+ * than comparing the fd value or the referenced file description. */
+int get_dummy_fd(void);
+
+void check_fd_equality(int fd1, int fd2);
+void check_fd_is_open(int fd);
+void check_fd_is_closed(int fd);
+
+void check_uint_array_eq(const uint32_t *a, const uint32_t *b, int cnt);
+#endif
diff --git a/drm-lease-manager/drm-lease.h b/drm-lease-manager/drm-lease.h
new file mode 100644 (file)
index 0000000..6c618a0
--- /dev/null
@@ -0,0 +1,22 @@
+/* Copyright 2020-2021 IGEL Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef DRM_LEASE_H
+#define DRM_LEASE_H
+
+struct lease_handle {
+       char *name;
+};
+#endif
diff --git a/drm-lease-manager/lease-manager.c b/drm-lease-manager/lease-manager.c
new file mode 100644 (file)
index 0000000..5cfc5de
--- /dev/null
@@ -0,0 +1,386 @@
+/* Copyright 2020-2021 IGEL Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define _GNU_SOURCE
+#include "lease-manager.h"
+
+#include "drm-lease.h"
+#include "log.h"
+
+#include <assert.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <sys/stat.h>
+#include <sys/sysmacros.h>
+#include <unistd.h>
+#include <xf86drm.h>
+#include <xf86drmMode.h>
+
+/* Number of resources, excluding planes, to be included in each DRM lease.
+ * Each lease needs at least a CRTC and conector. */
+#define DRM_LEASE_MIN_RES (2)
+
+#define ARRAY_LENGTH(x) (sizeof(x) / sizeof(x[0]))
+
+struct lease {
+       struct lease_handle base;
+
+       bool is_granted;
+       uint32_t lessee_id;
+       int lease_fd;
+
+       uint32_t *object_ids;
+       int nobject_ids;
+};
+
+struct lm {
+       int drm_fd;
+       dev_t dev_id;
+
+       drmModeResPtr drm_resource;
+       drmModePlaneResPtr drm_plane_resource;
+       uint32_t available_crtcs;
+
+       struct lease **leases;
+       int nleases;
+};
+
+static const char *const connector_type_names[] = {
+    [DRM_MODE_CONNECTOR_Unknown] = "Unknown",
+    [DRM_MODE_CONNECTOR_VGA] = "VGA",
+    [DRM_MODE_CONNECTOR_DVII] = "DVI-I",
+    [DRM_MODE_CONNECTOR_DVID] = "DVI-D",
+    [DRM_MODE_CONNECTOR_DVIA] = "DVI-A",
+    [DRM_MODE_CONNECTOR_Composite] = "Composite",
+    [DRM_MODE_CONNECTOR_SVIDEO] = "SVIDEO",
+    [DRM_MODE_CONNECTOR_LVDS] = "LVDS",
+    [DRM_MODE_CONNECTOR_Component] = "Component",
+    [DRM_MODE_CONNECTOR_9PinDIN] = "DIN",
+    [DRM_MODE_CONNECTOR_DisplayPort] = "DP",
+    [DRM_MODE_CONNECTOR_HDMIA] = "HDMI-A",
+    [DRM_MODE_CONNECTOR_HDMIB] = "HDMI-B",
+    [DRM_MODE_CONNECTOR_TV] = "TV",
+    [DRM_MODE_CONNECTOR_eDP] = "eDP",
+    [DRM_MODE_CONNECTOR_VIRTUAL] = "Virtual",
+    [DRM_MODE_CONNECTOR_DSI] = "DSI",
+    [DRM_MODE_CONNECTOR_DPI] = "DPI",
+    [DRM_MODE_CONNECTOR_WRITEBACK] = "Writeback",
+};
+
+static char *drm_create_lease_name(struct lm *lm, drmModeConnectorPtr connector)
+{
+       uint32_t type = connector->connector_type;
+       uint32_t id = connector->connector_type_id;
+
+       if (type >= ARRAY_LENGTH(connector_type_names))
+               type = DRM_MODE_CONNECTOR_Unknown;
+
+       /* If the type is "Unknown", use the connector_id as the identify to
+        * guarantee that the name will be unique. */
+       if (type == DRM_MODE_CONNECTOR_Unknown)
+               id = connector->connector_id;
+
+       char *name;
+       if (asprintf(&name, "card%d-%s-%d", minor(lm->dev_id),
+                    connector_type_names[type], id) < 0)
+               return NULL;
+
+       return name;
+}
+
+static int drm_get_encoder_crtc_index(struct lm *lm, drmModeEncoderPtr encoder)
+{
+       uint32_t crtc_id = encoder->crtc_id;
+       if (!crtc_id)
+               return -1;
+
+       // The CRTC index only makes sense if it is less than the number of
+       // bits in the encoder possible_crtcs bitmap, which is 32.
+       assert(lm->drm_resource->count_crtcs < 32);
+
+       for (int i = 0; i < lm->drm_resource->count_crtcs; i++) {
+               if (lm->drm_resource->crtcs[i] == crtc_id)
+                       return i;
+       }
+       return -1;
+}
+
+static int drm_get_active_crtc_index(struct lm *lm,
+                                    drmModeConnectorPtr connector)
+{
+       drmModeEncoder *encoder =
+           drmModeGetEncoder(lm->drm_fd, connector->encoder_id);
+       if (!encoder)
+               return -1;
+
+       int crtc_idx = drm_get_encoder_crtc_index(lm, encoder);
+       drmModeFreeEncoder(encoder);
+       return crtc_idx;
+}
+
+static int drm_get_crtc_index(struct lm *lm, drmModeConnectorPtr connector)
+{
+
+       // try the active CRTC first
+       int crtc_index = drm_get_active_crtc_index(lm, connector);
+       if (crtc_index != -1)
+               return crtc_index;
+
+       // If not try the first available CRTC on the connector/encoder
+       for (int i = 0; i < connector->count_encoders; i++) {
+               drmModeEncoder *encoder =
+                   drmModeGetEncoder(lm->drm_fd, connector->encoders[i]);
+
+               assert(encoder);
+
+               uint32_t usable_crtcs =
+                   lm->available_crtcs & encoder->possible_crtcs;
+               int crtc = ffs(usable_crtcs);
+               drmModeFreeEncoder(encoder);
+               if (crtc == 0)
+                       continue;
+               crtc_index = crtc - 1;
+               lm->available_crtcs &= ~(1 << crtc_index);
+               break;
+       }
+       return crtc_index;
+}
+
+static void drm_find_available_crtcs(struct lm *lm)
+{
+       // Assume all CRTCS are available by default,
+       lm->available_crtcs = ~0;
+
+       // then remove any that are in use. */
+       for (int i = 0; i < lm->drm_resource->count_encoders; i++) {
+               int enc_id = lm->drm_resource->encoders[i];
+               drmModeEncoderPtr enc = drmModeGetEncoder(lm->drm_fd, enc_id);
+               if (!enc)
+                       continue;
+
+               int crtc_idx = drm_get_encoder_crtc_index(lm, enc);
+               if (crtc_idx >= 0)
+                       lm->available_crtcs &= ~(1 << crtc_idx);
+
+               drmModeFreeEncoder(enc);
+       }
+}
+
+static bool lease_add_planes(struct lm *lm, struct lease *lease, int crtc_index)
+{
+       for (uint32_t i = 0; i < lm->drm_plane_resource->count_planes; i++) {
+               uint32_t plane_id = lm->drm_plane_resource->planes[i];
+               drmModePlanePtr plane = drmModeGetPlane(lm->drm_fd, plane_id);
+
+               assert(plane);
+
+               // Exclude planes that can be used with multiple CRTCs for now
+               if (plane->possible_crtcs == (1u << crtc_index)) {
+                       lease->object_ids[lease->nobject_ids++] = plane_id;
+               }
+               drmModeFreePlane(plane);
+       }
+       return true;
+}
+
+static void lease_free(struct lease *lease)
+{
+       free(lease->base.name);
+       free(lease->object_ids);
+       free(lease);
+}
+
+static struct lease *lease_create(struct lm *lm, drmModeConnectorPtr connector)
+{
+       struct lease *lease = calloc(1, sizeof(struct lease));
+       if (!lease) {
+               DEBUG_LOG("Memory allocation failed: %s\n", strerror(errno));
+               return NULL;
+       }
+
+       lease->base.name = drm_create_lease_name(lm, connector);
+       if (!lease->base.name) {
+               DEBUG_LOG("Can't create lease name: %s\n", strerror(errno));
+               goto err;
+       }
+
+       int nobjects = lm->drm_plane_resource->count_planes + DRM_LEASE_MIN_RES;
+       lease->object_ids = calloc(nobjects, sizeof(uint32_t));
+       if (!lease->object_ids) {
+               DEBUG_LOG("Memory allocation failed: %s\n", strerror(errno));
+               goto err;
+       }
+
+       int crtc_index = drm_get_crtc_index(lm, connector);
+       if (crtc_index < 0) {
+               DEBUG_LOG("No crtc found for connector: %s\n",
+                         lease->base.name);
+               goto err;
+       }
+
+       if (!lease_add_planes(lm, lease, crtc_index))
+               goto err;
+
+       uint32_t crtc_id = lm->drm_resource->crtcs[crtc_index];
+       lease->object_ids[lease->nobject_ids++] = crtc_id;
+       lease->object_ids[lease->nobject_ids++] = connector->connector_id;
+
+       lease->is_granted = false;
+
+       return lease;
+
+err:
+       lease_free(lease);
+       return NULL;
+}
+
+struct lm *lm_create(const char *device)
+{
+       struct lm *lm = calloc(1, sizeof(struct lm));
+       if (!lm) {
+               DEBUG_LOG("Memory allocation failed: %s\n", strerror(errno));
+               return NULL;
+       }
+       lm->drm_fd = open(device, O_RDWR);
+       if (lm->drm_fd < 0) {
+               ERROR_LOG("Cannot open DRM device (%s): %s\n", device,
+                         strerror(errno));
+               goto err;
+       }
+
+       lm->drm_resource = drmModeGetResources(lm->drm_fd);
+       if (!lm->drm_resource) {
+               ERROR_LOG("Invalid DRM device(%s)\n", device);
+               DEBUG_LOG("drmModeGetResources failed: %s\n", strerror(errno));
+               goto err;
+       }
+
+       lm->drm_plane_resource = drmModeGetPlaneResources(lm->drm_fd);
+       if (!lm->drm_plane_resource) {
+               DEBUG_LOG("drmModeGetPlaneResources failed: %s\n",
+                         strerror(errno));
+               goto err;
+       }
+
+       struct stat st;
+       if (fstat(lm->drm_fd, &st) < 0 || !S_ISCHR(st.st_mode)) {
+               DEBUG_LOG("%s is not a valid device file\n", device);
+               goto err;
+       }
+
+       lm->dev_id = st.st_rdev;
+
+       int num_leases = lm->drm_resource->count_connectors;
+
+       lm->leases = calloc(num_leases, sizeof(struct lease *));
+       if (!lm->leases) {
+               DEBUG_LOG("Memory allocation failed: %s\n", strerror(errno));
+               goto err;
+       }
+
+       drm_find_available_crtcs(lm);
+
+       for (int i = 0; i < num_leases; i++) {
+               uint32_t connector_id = lm->drm_resource->connectors[i];
+               drmModeConnectorPtr connector =
+                   drmModeGetConnector(lm->drm_fd, connector_id);
+
+               if (!connector)
+                       continue;
+
+               struct lease *lease = lease_create(lm, connector);
+               drmModeFreeConnector(connector);
+
+               if (!lease)
+                       continue;
+
+               lm->leases[lm->nleases] = lease;
+               lm->nleases++;
+       }
+       if (lm->nleases == 0)
+               goto err;
+
+       return lm;
+
+err:
+       lm_destroy(lm);
+       return NULL;
+}
+
+void lm_destroy(struct lm *lm)
+{
+       assert(lm);
+
+       for (int i = 0; i < lm->nleases; i++) {
+               lm_lease_revoke(lm, (struct lease_handle *)lm->leases[i]);
+               lease_free(lm->leases[i]);
+       }
+
+       free(lm->leases);
+       drmModeFreeResources(lm->drm_resource);
+       drmModeFreePlaneResources(lm->drm_plane_resource);
+       close(lm->drm_fd);
+       free(lm);
+}
+
+int lm_get_lease_handles(struct lm *lm, struct lease_handle ***handles)
+{
+       assert(lm);
+       assert(handles);
+
+       *handles = (struct lease_handle **)lm->leases;
+       return lm->nleases;
+}
+
+int lm_lease_grant(struct lm *lm, struct lease_handle *handle)
+{
+       assert(lm);
+       assert(handle);
+
+       struct lease *lease = (struct lease *)handle;
+       if (lease->is_granted)
+               return lease->lease_fd;
+
+       lease->lease_fd =
+           drmModeCreateLease(lm->drm_fd, lease->object_ids,
+                              lease->nobject_ids, 0, &lease->lessee_id);
+       if (lease->lease_fd < 0) {
+               ERROR_LOG("drmModeCreateLease failed on lease %s: %s\n",
+                         lease->base.name, strerror(errno));
+               return -1;
+       }
+
+       lease->is_granted = true;
+       return lease->lease_fd;
+}
+
+void lm_lease_revoke(struct lm *lm, struct lease_handle *handle)
+{
+       assert(lm);
+       assert(handle);
+
+       struct lease *lease = (struct lease *)handle;
+
+       if (!lease->is_granted)
+               return;
+
+       drmModeRevokeLease(lm->drm_fd, lease->lessee_id);
+       close(lease->lease_fd);
+       lease->is_granted = false;
+}
diff --git a/drm-lease-manager/lease-manager.h b/drm-lease-manager/lease-manager.h
new file mode 100644 (file)
index 0000000..ed5bcdc
--- /dev/null
@@ -0,0 +1,29 @@
+/* Copyright 2020-2021 IGEL Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef LEASE_MANAGER_H
+#define LEASE_MANAGER_H
+#include "drm-lease.h"
+
+struct lm;
+
+struct lm *lm_create(const char *path);
+void lm_destroy(struct lm *lm);
+
+int lm_get_lease_handles(struct lm *lm, struct lease_handle ***lease_handles);
+
+int lm_lease_grant(struct lm *lm, struct lease_handle *lease_handle);
+void lm_lease_revoke(struct lm *lm, struct lease_handle *lease_handle);
+#endif
diff --git a/drm-lease-manager/lease-server.c b/drm-lease-manager/lease-server.c
new file mode 100644 (file)
index 0000000..e05e8e4
--- /dev/null
@@ -0,0 +1,335 @@
+/* Copyright 2020-2021 IGEL Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "lease-server.h"
+#include "log.h"
+#include "socket-path.h"
+
+#include <assert.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <poll.h>
+#include <stdlib.h>
+#include <sys/epoll.h>
+#include <sys/file.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/un.h>
+#include <unistd.h>
+
+#define SOCK_LOCK_SUFFIX ".lock"
+
+struct ls_socket {
+       int fd;
+       struct ls_server *serv;
+};
+
+struct ls_server {
+       struct lease_handle *lease_handle;
+       struct sockaddr_un address;
+       int server_socket_lock;
+
+       struct ls_socket listen;
+       struct ls_socket client;
+
+       bool is_client_connected;
+};
+
+struct ls {
+       int epoll_fd;
+
+       struct ls_server *servers;
+       int nservers;
+};
+
+static bool client_connect(struct ls *ls, struct ls_server *serv)
+{
+       int cfd = accept(serv->listen.fd, NULL, NULL);
+       if (cfd < 0) {
+               DEBUG_LOG("accept failed on %s: %s\n", serv->address.sun_path,
+                         strerror(errno));
+               return false;
+       }
+
+       if (serv->is_client_connected) {
+               WARN_LOG("Client already connected on %s\n",
+                        serv->address.sun_path);
+               close(cfd);
+               return false;
+       }
+
+       serv->client.fd = cfd;
+       serv->client.serv = serv;
+
+       struct epoll_event ev = {
+           .events = POLLHUP,
+           .data.ptr = &serv->client,
+       };
+       if (epoll_ctl(ls->epoll_fd, EPOLL_CTL_ADD, cfd, &ev)) {
+               DEBUG_LOG("epoll_ctl add failed: %s\n", strerror(errno));
+               close(cfd);
+               return false;
+       }
+
+       serv->is_client_connected = true;
+       return true;
+}
+
+static int create_socket_lock(struct sockaddr_un *addr)
+{
+       int lock_fd;
+
+       int lockfile_len = sizeof(addr->sun_path) + sizeof(SOCK_LOCK_SUFFIX);
+       char lockfile[lockfile_len];
+       int len = snprintf(lockfile, lockfile_len, "%s%s", addr->sun_path,
+                          SOCK_LOCK_SUFFIX);
+
+       if (len < 0 || len >= lockfile_len) {
+               DEBUG_LOG("Can't create socket lock filename\n");
+               return -1;
+       }
+
+       lock_fd = open(lockfile, O_CREAT | O_RDWR,
+                      S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP);
+
+       if (lock_fd < 0) {
+               ERROR_LOG("Cannot access runtime directory\n");
+               return -1;
+       }
+
+       if (flock(lock_fd, LOCK_EX | LOCK_NB)) {
+               ERROR_LOG(
+                   "socket %s: in use.  Possible duplicate lease name or "
+                   "mutiple drm-lease-manager instances running\n",
+                   addr->sun_path);
+               close(lock_fd);
+               return -1;
+       }
+
+       return lock_fd;
+}
+
+static bool server_setup(struct ls *ls, struct ls_server *serv,
+                        struct lease_handle *lease_handle)
+{
+       struct sockaddr_un *address = &serv->address;
+
+       if (!sockaddr_set_lease_server_path(address, lease_handle->name))
+               return false;
+
+       int socket_lock = create_socket_lock(address);
+       if (socket_lock < 0)
+               return false;
+
+       /* The socket address is now owned by this instance, so any existing
+        * sockets can safely be removed */
+       unlink(address->sun_path);
+
+       address->sun_family = AF_UNIX;
+
+       int server_socket = socket(PF_UNIX, SOCK_STREAM | SOCK_NONBLOCK, 0);
+       if (server_socket < 0) {
+               DEBUG_LOG("Socket creation failed: %s\n", strerror(errno));
+               return false;
+       }
+
+       if (bind(server_socket, (struct sockaddr *)address, sizeof(*address))) {
+               ERROR_LOG("Failed to create named socket at %s: %s\n",
+                         address->sun_path, strerror(errno));
+               close(server_socket);
+               return false;
+       }
+
+       if (listen(server_socket, 0)) {
+               DEBUG_LOG("listen failed on %s: %s\n", address->sun_path,
+                         strerror(errno));
+               close(server_socket);
+               unlink(address->sun_path);
+               return false;
+       }
+
+       serv->is_client_connected = false;
+       serv->lease_handle = lease_handle;
+       serv->server_socket_lock = socket_lock;
+       serv->listen.fd = server_socket;
+       serv->listen.serv = serv;
+
+       struct epoll_event ev = {
+           .events = POLLIN,
+           .data.ptr = &serv->listen,
+       };
+
+       if (epoll_ctl(ls->epoll_fd, EPOLL_CTL_ADD, server_socket, &ev)) {
+               DEBUG_LOG("epoll_ctl add failed: %s\n", strerror(errno));
+               close(server_socket);
+               unlink(address->sun_path);
+               return false;
+       }
+
+       INFO_LOG("Lease server (%s) initialized at %s\n", lease_handle->name,
+                address->sun_path);
+       return true;
+}
+
+static void server_shutdown(struct ls *ls, struct ls_server *serv)
+{
+       if (unlink(serv->address.sun_path)) {
+               WARN_LOG("Server socket %s delete failed: %s\n",
+                        serv->address.sun_path, strerror(errno));
+       }
+
+       epoll_ctl(ls->epoll_fd, EPOLL_CTL_DEL, serv->listen.fd, NULL);
+       close(serv->listen.fd);
+       ls_disconnect_client(ls, serv);
+       close(serv->server_socket_lock);
+}
+
+struct ls *ls_create(struct lease_handle **lease_handles, int count)
+{
+       assert(lease_handles);
+       assert(count > 0);
+
+       struct ls *ls = calloc(1, sizeof(struct ls));
+       if (!ls) {
+               DEBUG_LOG("Memory allocation failed: %s\n", strerror(errno));
+               return NULL;
+       }
+
+       ls->servers = calloc(count, sizeof(struct ls_server));
+       if (!ls->servers) {
+               DEBUG_LOG("Memory allocation failed: %s\n", strerror(errno));
+               goto err;
+       }
+
+       ls->epoll_fd = epoll_create1(0);
+       if (ls->epoll_fd < 0) {
+               DEBUG_LOG("epoll_create failed: %s\n", strerror(errno));
+               goto err;
+       }
+
+       for (int i = 0; i < count; i++) {
+               if (!server_setup(ls, &ls->servers[i], lease_handles[i]))
+                       goto err;
+               ls->nservers++;
+       }
+       return ls;
+err:
+       ls_destroy(ls);
+       return NULL;
+}
+
+void ls_destroy(struct ls *ls)
+{
+       assert(ls);
+
+       for (int i = 0; i < ls->nservers; i++)
+               server_shutdown(ls, &ls->servers[i]);
+
+       close(ls->epoll_fd);
+       free(ls->servers);
+       free(ls);
+}
+
+bool ls_get_request(struct ls *ls, struct ls_req *req)
+{
+       assert(ls);
+       assert(req);
+
+       int request = -1;
+       while (request < 0) {
+               struct epoll_event ev;
+               if (epoll_wait(ls->epoll_fd, &ev, 1, -1) < 0) {
+                       if (errno == EINTR)
+                               continue;
+                       DEBUG_LOG("epoll_wait failed: %s\n", strerror(errno));
+                       return false;
+               }
+
+               struct ls_socket *sock = ev.data.ptr;
+               assert(sock);
+
+               struct ls_server *server = sock->serv;
+               req->lease_handle = server->lease_handle;
+               req->server = server;
+
+               if (sock == &server->listen) {
+                       if (!(ev.events & POLLIN))
+                               continue;
+                       if (client_connect(ls, server))
+                               request = LS_REQ_GET_LEASE;
+               } else if (sock == &server->client) {
+                       if (!(ev.events & POLLHUP))
+                               continue;
+                       request = LS_REQ_RELEASE_LEASE;
+               } else {
+                       ERROR_LOG("Internal error: Invalid socket context\n");
+                       return false;
+               }
+       }
+       req->type = request;
+       return true;
+}
+
+bool ls_send_fd(struct ls *ls, struct ls_server *serv, int fd)
+{
+       assert(ls);
+       assert(serv);
+
+       if (fd < 0)
+               return false;
+
+       char data[1];
+       struct iovec iov = {
+           .iov_base = data,
+           .iov_len = sizeof(data),
+       };
+
+       char ctrl_buf[CMSG_SPACE(sizeof(int))] = {0};
+
+       struct msghdr msg = {
+           .msg_iov = &iov,
+           .msg_iovlen = 1,
+           .msg_controllen = sizeof(ctrl_buf),
+           .msg_control = ctrl_buf,
+       };
+
+       struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
+       cmsg->cmsg_level = SOL_SOCKET;
+       cmsg->cmsg_type = SCM_RIGHTS;
+       cmsg->cmsg_len = CMSG_LEN(sizeof(int));
+       *((int *)CMSG_DATA(cmsg)) = fd;
+
+       if (sendmsg(serv->client.fd, &msg, 0) < 0) {
+               DEBUG_LOG("sendmsg failed on %s: %s\n", serv->address.sun_path,
+                         strerror(errno));
+               return false;
+       }
+
+       INFO_LOG("Lease request granted on %s\n", serv->address.sun_path);
+       return true;
+}
+
+void ls_disconnect_client(struct ls *ls, struct ls_server *serv)
+{
+       assert(ls);
+       assert(serv);
+       if (!serv->is_client_connected)
+               return;
+
+       epoll_ctl(ls->epoll_fd, EPOLL_CTL_DEL, serv->client.fd, NULL);
+       close(serv->client.fd);
+       serv->is_client_connected = false;
+}
diff --git a/drm-lease-manager/lease-server.h b/drm-lease-manager/lease-server.h
new file mode 100644 (file)
index 0000000..c87b834
--- /dev/null
@@ -0,0 +1,42 @@
+/* Copyright 2020-2021 IGEL Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef LEASE_SERVER_H
+#define LEASE_SERVER_H
+#include <stdbool.h>
+
+#include "drm-lease.h"
+
+struct ls;
+struct ls_server;
+enum ls_req_type {
+       LS_REQ_GET_LEASE,
+       LS_REQ_RELEASE_LEASE,
+};
+
+struct ls_req {
+       struct lease_handle *lease_handle;
+       struct ls_server *server;
+       enum ls_req_type type;
+};
+
+struct ls *ls_create(struct lease_handle **lease_handles, int count);
+void ls_destroy(struct ls *ls);
+
+bool ls_get_request(struct ls *ls, struct ls_req *req);
+bool ls_send_fd(struct ls *ls, struct ls_server *server, int fd);
+
+void ls_disconnect_client(struct ls *ls, struct ls_server *server);
+#endif
diff --git a/drm-lease-manager/main.c b/drm-lease-manager/main.c
new file mode 100644 (file)
index 0000000..c3a933f
--- /dev/null
@@ -0,0 +1,120 @@
+/* Copyright 2020-2021 IGEL Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "lease-manager.h"
+#include "lease-server.h"
+#include "log.h"
+
+#include <assert.h>
+#include <getopt.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+static void usage(const char *progname)
+{
+       printf("Usage: %s [OPTIONS] [<DRM device>]\n\n"
+              "Options:\n"
+              "-h, --help \tPrint this help\n"
+              "-v, --verbose \tEnable verbose debug messages\n",
+              progname);
+}
+
+const char *opts = "vh";
+const struct option options[] = {
+    {"help", no_argument, NULL, 'h'},
+    {"verbose", no_argument, NULL, 'v'},
+    {NULL, 0, NULL, 0},
+};
+
+int main(int argc, char **argv)
+{
+       char *device = "/dev/dri/card0";
+
+       bool debug_log = false;
+
+       int c;
+       while ((c = getopt_long(argc, argv, opts, options, NULL)) != -1) {
+               int ret = EXIT_FAILURE;
+               switch (c) {
+               case 'v':
+                       debug_log = true;
+                       break;
+               case 'h':
+                       ret = EXIT_SUCCESS;
+                       /* fall through */
+               default:
+                       usage(argv[0]);
+                       return ret;
+               }
+       }
+
+       if (optind < argc)
+               device = argv[optind];
+
+       dlm_log_enable_debug(debug_log);
+
+       struct lm *lm = lm_create(device);
+       if (!lm) {
+               ERROR_LOG("DRM Lease initialization failed\n");
+               return EXIT_FAILURE;
+       }
+
+       struct lease_handle **lease_handles = NULL;
+       int count_ids = lm_get_lease_handles(lm, &lease_handles);
+       assert(count_ids > 0);
+
+       struct ls *ls = ls_create(lease_handles, count_ids);
+       if (!ls) {
+               lm_destroy(lm);
+               ERROR_LOG("Client socket initialization failed\n");
+               return EXIT_FAILURE;
+       }
+
+       struct ls_req req;
+       while (ls_get_request(ls, &req)) {
+               switch (req.type) {
+               case LS_REQ_GET_LEASE: {
+                       int fd = lm_lease_grant(lm, req.lease_handle);
+                       if (fd < 0) {
+                               ERROR_LOG(
+                                   "Can't fulfill lease request: lease=%s\n",
+                                   req.lease_handle->name);
+                               ls_disconnect_client(ls, req.server);
+                               break;
+                       }
+
+                       if (!ls_send_fd(ls, req.server, fd)) {
+                               ERROR_LOG(
+                                   "Client communication error: lease=%s\n",
+                                   req.lease_handle->name);
+                               ls_disconnect_client(ls, req.server);
+                               lm_lease_revoke(lm, req.lease_handle);
+                       }
+                       break;
+               }
+               case LS_REQ_RELEASE_LEASE:
+                       ls_disconnect_client(ls, req.server);
+                       lm_lease_revoke(lm, req.lease_handle);
+                       break;
+               default:
+                       ERROR_LOG("Internal error: Invalid lease request\n");
+                       goto done;
+               }
+       }
+done:
+       ls_destroy(ls);
+       lm_destroy(lm);
+       return EXIT_FAILURE;
+}
diff --git a/drm-lease-manager/meson.build b/drm-lease-manager/meson.build
new file mode 100644 (file)
index 0000000..1171b02
--- /dev/null
@@ -0,0 +1,12 @@
+
+lease_manager_files = files('lease-manager.c')
+lease_server_files = files('lease-server.c')
+main = executable('drm-lease-manager',
+    [ 'main.c', lease_manager_files, lease_server_files ],
+    dependencies: [ drm_dep, dlmcommon_dep ],
+    install: true,
+)
+
+if enable_tests
+  subdir('test')
+endif
diff --git a/drm-lease-manager/test/lease-manager-test.c b/drm-lease-manager/test/lease-manager-test.c
new file mode 100644 (file)
index 0000000..c741cb3
--- /dev/null
@@ -0,0 +1,448 @@
+/* Copyright 2020-2021 IGEL Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <check.h>
+#include <fff.h>
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <xf86drmMode.h>
+
+#include "lease-manager.h"
+#include "log.h"
+#include "test-drm-device.h"
+#include "test-helpers.h"
+
+/* CHECK_LEASE_OBJECTS
+ *
+ * Checks the list of objects associated with a given lease_index.
+ * Asks the lease manager to create the lease, and checks that
+ * the requested objects are the ones given in the supplied list. */
+
+#define CHECK_LEASE_OBJECTS(lease, ...)                                     \
+       do {                                                                \
+               lm_lease_grant(lm, lease);                                  \
+               uint32_t objs[] = {__VA_ARGS__};                            \
+               int nobjs = ARRAY_LEN(objs);                                \
+               ck_assert_int_eq(drmModeCreateLease_fake.arg2_val, nobjs);  \
+               check_uint_array_eq(drmModeCreateLease_fake.arg1_val, objs, \
+                                   nobjs);                                 \
+       } while (0)
+
+/**************  Mock functions  *************/
+DEFINE_FFF_GLOBALS;
+
+FAKE_VALUE_FUNC(drmModeResPtr, drmModeGetResources, int);
+FAKE_VOID_FUNC(drmModeFreeResources, drmModeResPtr);
+FAKE_VALUE_FUNC(drmModePlaneResPtr, drmModeGetPlaneResources, int);
+FAKE_VOID_FUNC(drmModeFreePlaneResources, drmModePlaneResPtr);
+
+FAKE_VALUE_FUNC(drmModePlanePtr, drmModeGetPlane, int, uint32_t);
+FAKE_VOID_FUNC(drmModeFreePlane, drmModePlanePtr);
+FAKE_VALUE_FUNC(drmModeConnectorPtr, drmModeGetConnector, int, uint32_t);
+FAKE_VOID_FUNC(drmModeFreeConnector, drmModeConnectorPtr);
+FAKE_VALUE_FUNC(drmModeEncoderPtr, drmModeGetEncoder, int, uint32_t);
+FAKE_VOID_FUNC(drmModeFreeEncoder, drmModeEncoderPtr);
+
+FAKE_VALUE_FUNC(int, drmModeCreateLease, int, const uint32_t *, int, int,
+               uint32_t *);
+FAKE_VALUE_FUNC(int, drmModeRevokeLease, int, uint32_t);
+
+/************** Test fixutre functions *************************/
+
+static void test_setup(void)
+{
+       RESET_FAKE(drmModeGetResources);
+       RESET_FAKE(drmModeFreeResources);
+       RESET_FAKE(drmModeGetPlaneResources);
+       RESET_FAKE(drmModeFreePlaneResources);
+
+       RESET_FAKE(drmModeGetPlane);
+       RESET_FAKE(drmModeFreePlane);
+       RESET_FAKE(drmModeGetConnector);
+       RESET_FAKE(drmModeFreeConnector);
+       RESET_FAKE(drmModeGetEncoder);
+       RESET_FAKE(drmModeFreeEncoder);
+
+       RESET_FAKE(drmModeCreateLease);
+       RESET_FAKE(drmModeRevokeLease);
+
+       drmModeGetResources_fake.return_val = TEST_DEVICE_RESOURCES;
+       drmModeGetPlaneResources_fake.return_val = TEST_DEVICE_PLANE_RESOURCES;
+
+       drmModeGetPlane_fake.custom_fake = get_plane;
+       drmModeGetConnector_fake.custom_fake = get_connector;
+       drmModeGetEncoder_fake.custom_fake = get_encoder;
+       drmModeCreateLease_fake.custom_fake = create_lease;
+}
+
+static void test_shutdown(void)
+{
+       reset_drm_test_device();
+}
+
+/************** Resource enumeration tests *************/
+
+/* These tests verify that the lease manager correctly assigns
+ * DRM resources to thier respective leases. In some cases
+ * the lease manager must choose which resources to include in
+ * each lease, so these tests verify that a valid (but not
+ * necessarily optimal) choice is made.
+ */
+
+/* all_outputs_connected
+ *
+ * Test details: Create leases when all crtc/encoder/connector paths are
+ *               connected.
+ *
+ * Expected results: Leases are created for the currently connected sets of
+ *                   resources.
+ */
+START_TEST(all_outputs_connected)
+{
+       int out_cnt = 2, plane_cnt = 0;
+
+       ck_assert_int_eq(
+           setup_drm_test_device(out_cnt, out_cnt, out_cnt, plane_cnt), true);
+
+       drmModeConnector connectors[] = {
+           CONNECTOR(CONNECTOR_ID(0), ENCODER_ID(0), &ENCODER_ID(0), 1),
+           CONNECTOR(CONNECTOR_ID(1), ENCODER_ID(1), &ENCODER_ID(1), 1),
+       };
+
+       drmModeEncoder encoders[] = {
+           ENCODER(ENCODER_ID(0), CRTC_ID(0), 0x3),
+           ENCODER(ENCODER_ID(1), CRTC_ID(1), 0x2),
+       };
+
+       setup_test_device_layout(connectors, encoders, NULL);
+
+       struct lm *lm = lm_create(TEST_DRM_DEVICE);
+       ck_assert_ptr_ne(lm, NULL);
+
+       struct lease_handle **handles;
+       ck_assert_int_eq(out_cnt, lm_get_lease_handles(lm, &handles));
+       ck_assert_ptr_ne(handles, NULL);
+
+       CHECK_LEASE_OBJECTS(handles[0], CRTC_ID(0), CONNECTOR_ID(0));
+       CHECK_LEASE_OBJECTS(handles[1], CRTC_ID(1), CONNECTOR_ID(1));
+
+       lm_destroy(lm);
+}
+END_TEST
+
+/* no_outputs_connected
+ *
+ * Test details: Create leases when no crtc/encoder/connector paths are
+ *               connected.
+ *
+ * Expected results: Available resources are divided between the leases.
+ *                   The same resource should not appear in multiple leases.
+ */
+START_TEST(no_outputs_connected)
+{
+       int out_cnt = 2, plane_cnt = 0;
+
+       ck_assert_int_eq(
+           setup_drm_test_device(out_cnt, out_cnt, out_cnt, plane_cnt), true);
+
+       drmModeConnector connectors[] = {
+           CONNECTOR(CONNECTOR_ID(0), 0, &ENCODER_ID(0), 1),
+           CONNECTOR(CONNECTOR_ID(1), 0, &ENCODER_ID(1), 1),
+       };
+
+       drmModeEncoder encoders[] = {
+           ENCODER(ENCODER_ID(0), 0, 0x2),
+           ENCODER(ENCODER_ID(1), 0, 0x3),
+       };
+
+       setup_test_device_layout(connectors, encoders, NULL);
+
+       struct lm *lm = lm_create(TEST_DRM_DEVICE);
+       ck_assert_ptr_ne(lm, NULL);
+
+       struct lease_handle **handles;
+       ck_assert_int_eq(out_cnt, lm_get_lease_handles(lm, &handles));
+       ck_assert_ptr_ne(handles, NULL);
+
+       CHECK_LEASE_OBJECTS(handles[0], CRTC_ID(1), CONNECTOR_ID(0));
+       CHECK_LEASE_OBJECTS(handles[1], CRTC_ID(0), CONNECTOR_ID(1));
+
+       lm_destroy(lm);
+}
+END_TEST
+
+/* some_outputs_connected  */
+/* Test details: Create leases when one output is connected and one is not.
+ * Expected results: Currently connected resources should be added to
+ *                   the same lease.
+ *                   The non-connected resources should be added to a second
+ *                   lease.
+ */
+START_TEST(some_outputs_connected)
+{
+       int out_cnt = 2, plane_cnt = 0;
+
+       ck_assert_int_eq(
+           setup_drm_test_device(out_cnt, out_cnt, out_cnt, plane_cnt), true);
+
+       drmModeConnector connectors[] = {
+           CONNECTOR(CONNECTOR_ID(0), ENCODER_ID(0), &ENCODER_ID(0), 1),
+           CONNECTOR(CONNECTOR_ID(1), 0, &ENCODER_ID(1), 1),
+       };
+
+       drmModeEncoder encoders[] = {
+           ENCODER(ENCODER_ID(0), CRTC_ID(0), 0x3),
+           ENCODER(ENCODER_ID(1), 0, 0x3),
+       };
+
+       setup_test_device_layout(connectors, encoders, NULL);
+
+       struct lm *lm = lm_create(TEST_DRM_DEVICE);
+       ck_assert_ptr_ne(lm, NULL);
+
+       struct lease_handle **handles;
+       ck_assert_int_eq(out_cnt, lm_get_lease_handles(lm, &handles));
+       ck_assert_ptr_ne(handles, NULL);
+
+       CHECK_LEASE_OBJECTS(handles[0], CRTC_ID(0), CONNECTOR_ID(0));
+       CHECK_LEASE_OBJECTS(handles[1], CRTC_ID(1), CONNECTOR_ID(1));
+
+       lm_destroy(lm);
+}
+END_TEST
+
+/* fewer_crtcs_than_connectors  */
+/* Test details: Create leases on a system with more connectors than CRTCs
+ * Expected results: Number of leases generated should correspond to number of
+ *                   CRTCs.
+ *                   Leases contain one valid connector for each CRTC.
+ */
+START_TEST(fewer_crtcs_than_connectors)
+{
+       int out_cnt = 3, plane_cnt = 0, crtc_cnt = 2;
+
+       ck_assert_int_eq(
+           setup_drm_test_device(crtc_cnt, out_cnt, out_cnt, plane_cnt), true);
+
+       drmModeConnector connectors[] = {
+           CONNECTOR(CONNECTOR_ID(0), 0, &ENCODER_ID(0), 1),
+           CONNECTOR(CONNECTOR_ID(1), 0, &ENCODER_ID(1), 1),
+           CONNECTOR(CONNECTOR_ID(2), 0, &ENCODER_ID(2), 1),
+       };
+
+       drmModeEncoder encoders[] = {
+           ENCODER(ENCODER_ID(0), 0, 0x3),
+           ENCODER(ENCODER_ID(1), 0, 0x1),
+           ENCODER(ENCODER_ID(2), 0, 0x3),
+       };
+
+       setup_test_device_layout(connectors, encoders, NULL);
+
+       struct lm *lm = lm_create(TEST_DRM_DEVICE);
+       ck_assert_ptr_ne(lm, NULL);
+
+       struct lease_handle **handles;
+       ck_assert_int_eq(lm_get_lease_handles(lm, &handles), crtc_cnt);
+       ck_assert_ptr_ne(handles, NULL);
+
+       CHECK_LEASE_OBJECTS(handles[0], CRTC_ID(0), CONNECTOR_ID(0));
+       CHECK_LEASE_OBJECTS(handles[1], CRTC_ID(1), CONNECTOR_ID(2));
+       lm_destroy(lm);
+}
+END_TEST
+
+/* separate_overlay_planes_by_crtc  */
+/* Test details: Add overlay planes to leases. Each plane is tied to a
+ *               specific CRTC.
+ * Expected results: The leases contain all of the planes for connected to
+ *                   each CRTC and no others.
+ */
+START_TEST(separate_overlay_planes_by_crtc)
+{
+
+       int out_cnt = 2, plane_cnt = 3;
+
+       ck_assert_int_eq(
+           setup_drm_test_device(out_cnt, out_cnt, out_cnt, plane_cnt), true);
+
+       drmModeConnector connectors[] = {
+           CONNECTOR(CONNECTOR_ID(0), ENCODER_ID(0), &ENCODER_ID(0), 1),
+           CONNECTOR(CONNECTOR_ID(1), ENCODER_ID(1), &ENCODER_ID(1), 1),
+       };
+
+       drmModeEncoder encoders[] = {
+           ENCODER(ENCODER_ID(0), CRTC_ID(0), 0x1),
+           ENCODER(ENCODER_ID(1), CRTC_ID(1), 0x2),
+       };
+
+       drmModePlane planes[] = {
+           PLANE(PLANE_ID(0), 0x2),
+           PLANE(PLANE_ID(1), 0x1),
+           PLANE(PLANE_ID(2), 0x2),
+       };
+
+       setup_test_device_layout(connectors, encoders, planes);
+
+       struct lm *lm = lm_create(TEST_DRM_DEVICE);
+       ck_assert_ptr_ne(lm, NULL);
+
+       struct lease_handle **handles;
+       ck_assert_int_eq(out_cnt, lm_get_lease_handles(lm, &handles));
+       ck_assert_ptr_ne(handles, NULL);
+
+       CHECK_LEASE_OBJECTS(handles[0], PLANE_ID(1), CRTC_ID(0),
+                           CONNECTOR_ID(0));
+       CHECK_LEASE_OBJECTS(handles[1], PLANE_ID(0), PLANE_ID(2), CRTC_ID(1),
+                           CONNECTOR_ID(1));
+       lm_destroy(lm);
+}
+END_TEST
+
+/* reject_planes_shared_between_multiple_crtcs */
+/* Test details: Add overlay planes to leases. Some planes are shared between
+ *               multiple CRTCs.
+ * Expected results: The leases contain all of the unique planes for each CRTC.
+ *                   Planes that can be used on multiple CRTCs are not included
+ *                   in any lease.
+ */
+START_TEST(reject_planes_shared_between_multiple_crtcs)
+{
+
+       int out_cnt = 2, plane_cnt = 3;
+
+       ck_assert_int_eq(
+           setup_drm_test_device(out_cnt, out_cnt, out_cnt, plane_cnt), true);
+
+       drmModeConnector connectors[] = {
+           CONNECTOR(CONNECTOR_ID(0), ENCODER_ID(0), &ENCODER_ID(0), 1),
+           CONNECTOR(CONNECTOR_ID(1), ENCODER_ID(1), &ENCODER_ID(1), 1),
+       };
+
+       drmModeEncoder encoders[] = {
+           ENCODER(ENCODER_ID(0), CRTC_ID(0), 0x1),
+           ENCODER(ENCODER_ID(1), CRTC_ID(1), 0x2),
+       };
+
+       drmModePlane planes[] = {
+           PLANE(PLANE_ID(0), 0x2),
+           PLANE(PLANE_ID(1), 0x1),
+           PLANE(PLANE_ID(2), 0x3),
+       };
+
+       setup_test_device_layout(connectors, encoders, planes);
+
+       struct lm *lm = lm_create(TEST_DRM_DEVICE);
+       ck_assert_ptr_ne(lm, NULL);
+
+       struct lease_handle **handles;
+       ck_assert_int_eq(out_cnt, lm_get_lease_handles(lm, &handles));
+       ck_assert_ptr_ne(handles, NULL);
+
+       CHECK_LEASE_OBJECTS(handles[0], PLANE_ID(1), CRTC_ID(0),
+                           CONNECTOR_ID(0));
+       CHECK_LEASE_OBJECTS(handles[1], PLANE_ID(0), CRTC_ID(1),
+                           CONNECTOR_ID(1));
+       lm_destroy(lm);
+}
+END_TEST
+
+static void add_connector_enum_tests(Suite *s)
+{
+       TCase *tc = tcase_create("Resource enumeration");
+
+       tcase_add_checked_fixture(tc, test_setup, test_shutdown);
+
+       tcase_add_test(tc, all_outputs_connected);
+       tcase_add_test(tc, no_outputs_connected);
+       tcase_add_test(tc, fewer_crtcs_than_connectors);
+       tcase_add_test(tc, some_outputs_connected);
+       tcase_add_test(tc, separate_overlay_planes_by_crtc);
+       tcase_add_test(tc, reject_planes_shared_between_multiple_crtcs);
+       suite_add_tcase(s, tc);
+}
+
+/************** Lease management tests *************/
+
+/* create_and_revoke_lease */
+/* Test details: Create leases and revoke them.
+ * Expected results: drmModeRevokeLease() is called with the correct leasee_id.
+ */
+START_TEST(create_and_revoke_lease)
+{
+       int lease_cnt = 2;
+       bool res = setup_drm_test_device(lease_cnt, lease_cnt, lease_cnt, 0);
+       ck_assert_int_eq(res, true);
+
+       drmModeConnector connectors[] = {
+           CONNECTOR(CONNECTOR_ID(0), ENCODER_ID(0), &ENCODER_ID(0), 1),
+           CONNECTOR(CONNECTOR_ID(1), ENCODER_ID(1), &ENCODER_ID(1), 1),
+       };
+
+       drmModeEncoder encoders[] = {
+           ENCODER(ENCODER_ID(0), CRTC_ID(0), 0x1),
+           ENCODER(ENCODER_ID(1), CRTC_ID(1), 0x2),
+       };
+
+       setup_test_device_layout(connectors, encoders, NULL);
+
+       struct lm *lm = lm_create(TEST_DRM_DEVICE);
+       ck_assert_ptr_ne(lm, NULL);
+
+       struct lease_handle **handles;
+       ck_assert_int_eq(lease_cnt, lm_get_lease_handles(lm, &handles));
+       ck_assert_ptr_ne(handles, NULL);
+
+       for (int i = 0; i < lease_cnt; i++) {
+               ck_assert_int_ge(lm_lease_grant(lm, handles[i]), 0);
+               lm_lease_revoke(lm, handles[i]);
+       }
+
+       ck_assert_int_eq(drmModeRevokeLease_fake.call_count, lease_cnt);
+
+       for (int i = 0; i < lease_cnt; i++) {
+               ck_assert_int_eq(drmModeRevokeLease_fake.arg1_history[i],
+                                LESSEE_ID(i));
+       }
+}
+END_TEST
+
+static void add_lease_management_tests(Suite *s)
+{
+       TCase *tc = tcase_create("Lease management");
+
+       tcase_add_checked_fixture(tc, test_setup, test_shutdown);
+
+       tcase_add_test(tc, create_and_revoke_lease);
+       suite_add_tcase(s, tc);
+}
+
+int main(void)
+{
+       int number_failed;
+       Suite *s;
+       SRunner *sr;
+
+       s = suite_create("DLM lease manager tests");
+
+       add_connector_enum_tests(s);
+       add_lease_management_tests(s);
+
+       sr = srunner_create(s);
+       srunner_run_all(sr, CK_NORMAL);
+       number_failed = srunner_ntests_failed(sr);
+       srunner_free(sr);
+       return (number_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE;
+}
diff --git a/drm-lease-manager/test/lease-server-test.c b/drm-lease-manager/test/lease-server-test.c
new file mode 100644 (file)
index 0000000..f8e000e
--- /dev/null
@@ -0,0 +1,380 @@
+/* Copyright 2020-2021 IGEL Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <check.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include <pthread.h>
+
+#include "lease-server.h"
+#include "log.h"
+#include "test-helpers.h"
+#include "test-socket-client.h"
+
+#define SOCKETDIR "/tmp"
+
+/************** Test fixutre functions *************************/
+struct test_config default_test_config;
+
+#define TEST_LEASE_NAME "test-lease"
+
+static struct lease_handle test_lease = {
+    .name = TEST_LEASE_NAME,
+};
+
+static void test_setup(void)
+{
+       dlm_log_enable_debug(true);
+       setenv("DLM_RUNTIME_PATH", SOCKETDIR, 1);
+
+       default_test_config = (struct test_config){
+           .lease = &test_lease,
+       };
+}
+
+static void test_shutdown(void)
+{
+       test_config_cleanup(&default_test_config);
+}
+
+static struct ls *create_default_server(void)
+{
+       struct lease_handle *leases[] = {
+           &test_lease,
+       };
+       struct ls *ls = ls_create(leases, 1);
+       ck_assert_ptr_ne(ls, NULL);
+       return ls;
+}
+
+/**************  Lease server error handling tests *************/
+
+/* duplicate_server_failure
+ *
+ * Test details: Try to intialize the same server twice
+ * Expected results: ls_create() fails.
+ */
+START_TEST(duplicate_server_failure)
+{
+       struct lease_handle *leases[] = {&test_lease, &test_lease};
+       struct ls *ls = ls_create(leases, 2);
+       ck_assert_ptr_eq(ls, NULL);
+}
+END_TEST
+
+START_TEST(long_lease_name_failure)
+{
+       char long_lease_name[200];
+
+       size_t len = sizeof(long_lease_name) - 1;
+       memset(long_lease_name, 'a', len);
+       long_lease_name[len] = '\0';
+
+       struct lease_handle long_name_lease = {.name = long_lease_name};
+
+       struct lease_handle *leases[] = {&long_name_lease};
+       struct ls *ls = ls_create(leases, 1);
+       ck_assert_ptr_eq(ls, NULL);
+}
+END_TEST
+
+static void add_error_tests(Suite *s)
+{
+       TCase *tc = tcase_create("Lease server errors");
+
+       tcase_add_checked_fixture(tc, test_setup, test_shutdown);
+
+       tcase_add_test(tc, duplicate_server_failure);
+       tcase_add_test(tc, long_lease_name_failure);
+       suite_add_tcase(s, tc);
+}
+
+/**************  Client request handling tests ************/
+
+/* Test the handling of client requests.  Make sure that the
+ * proper struct ls_req are generated for each client request.
+ */
+
+static void check_request(struct ls_req *req,
+                         struct lease_handle *expected_lease,
+                         enum ls_req_type expected_type)
+{
+       ck_assert_ptr_eq(req->lease_handle, expected_lease);
+       ck_assert_int_eq(req->type, expected_type);
+}
+
+static void get_and_check_request(struct ls *ls,
+                                 struct lease_handle *expected_lease,
+                                 enum ls_req_type expected_type)
+{
+       struct ls_req req;
+       bool req_valid = ls_get_request(ls, &req);
+       ck_assert_int_eq(req_valid, true);
+       check_request(&req, expected_lease, expected_type);
+}
+
+/* Asynchronous version of the above.  Has the extra overhead of
+ * spawning a new thread, so should be used sparingly. */
+struct async_req {
+       pthread_t tid;
+       struct ls *ls;
+
+       bool req_valid;
+       struct ls_req expected;
+       struct ls_req actual;
+};
+
+static void *get_request_thread(void *arg)
+{
+       struct async_req *async_req = arg;
+       async_req->req_valid =
+           ls_get_request(async_req->ls, &async_req->actual);
+
+       return NULL;
+}
+
+static struct async_req *
+get_and_check_request_async(struct ls *ls, struct lease_handle *expected_lease,
+                           enum ls_req_type expected_type)
+
+{
+       struct async_req *req = malloc(sizeof(struct async_req));
+       ck_assert_ptr_ne(req, NULL);
+
+       *req = (struct async_req){
+           .ls = ls,
+           .expected =
+               {
+                   .lease_handle = expected_lease,
+                   .type = expected_type,
+               },
+       };
+
+       int ret = pthread_create(&req->tid, NULL, get_request_thread, req);
+       ck_assert_int_eq(ret, 0);
+
+       return req;
+}
+
+static void check_async_req_result(struct async_req *req)
+{
+
+       pthread_join(req->tid, NULL);
+       ck_assert_int_eq(req->req_valid, true);
+       check_request(&req->actual, req->expected.lease_handle,
+                     req->expected.type);
+       free(req);
+}
+
+/* issue_lease_request_and_release
+ *
+ * Test details: Generate a lease request and lease release command from
+ *               a client.
+ * Expected results: One get lease and one release lease request are returned
+ *                   from ls_get_request().
+ */
+START_TEST(issue_lease_request_and_release)
+{
+       struct ls *ls = create_default_server();
+
+       struct client_state *cstate = test_client_start(&default_test_config);
+
+       get_and_check_request(ls, &test_lease, LS_REQ_GET_LEASE);
+       test_client_stop(cstate);
+       get_and_check_request(ls, &test_lease, LS_REQ_RELEASE_LEASE);
+}
+END_TEST
+
+/* issue_lease_request_and_early_release
+ *
+ * Test details: Close client connection immediately after connecting (before
+ *               lease request is processed)
+ * Expected results: Should be the same result as
+ * issue_lease_request_and_release.
+ */
+START_TEST(issue_lease_request_and_early_release)
+{
+       struct ls *ls = create_default_server();
+
+       struct client_state *cstate = test_client_start(&default_test_config);
+
+       test_client_stop(cstate);
+       get_and_check_request(ls, &test_lease, LS_REQ_GET_LEASE);
+       get_and_check_request(ls, &test_lease, LS_REQ_RELEASE_LEASE);
+}
+END_TEST
+
+/* issue_multiple_lease_requests
+ *
+ * Test details: Generate multiple lease requests to the same lease server from
+ *               multiple clients at the same time
+ * Expected results: One get lease and one release lease request are returned
+ *                   from ls_get_request().
+ *                   Requests from all but the first client are rejected
+ *                   (sockets are closed).
+ */
+START_TEST(issue_multiple_lease_requests)
+{
+       struct lease_handle *leases[] = {
+           &test_lease,
+       };
+       struct ls *ls = ls_create(leases, 1);
+
+       struct test_config accepted_config;
+       struct client_state *accepted_cstate;
+
+       accepted_config = default_test_config;
+       accepted_cstate = test_client_start(&accepted_config);
+       get_and_check_request(ls, &test_lease, LS_REQ_GET_LEASE);
+
+       /*Try to make additional connections while the first is still
+        *connected. */
+       const int nextra_clients = 2;
+       struct test_config extra_configs[nextra_clients];
+       struct client_state *extra_cstates[nextra_clients];
+
+       for (int i = 0; i < nextra_clients; i++) {
+               extra_configs[i] = default_test_config;
+               extra_cstates[i] = test_client_start(&extra_configs[i]);
+       }
+
+       // Start asyncronously checking for the accepted client to release.
+       struct async_req *async_release_req =
+           get_and_check_request_async(ls, &test_lease, LS_REQ_RELEASE_LEASE);
+
+       for (int i = 0; i < nextra_clients; i++) {
+               test_client_stop(extra_cstates[i]);
+       }
+
+       /* Release the first connection and check results */
+       test_client_stop(accepted_cstate);
+       check_async_req_result(async_release_req);
+
+       /* Only one connection should be granted access by the lease manager */
+       ck_assert_int_eq(accepted_config.connection_completed, true);
+       for (int i = 0; i < nextra_clients; i++)
+               ck_assert_int_eq(extra_configs[i].connection_completed, false);
+}
+END_TEST
+
+static void add_client_request_tests(Suite *s)
+{
+       TCase *tc = tcase_create("Client request testing");
+
+       tcase_add_checked_fixture(tc, test_setup, test_shutdown);
+
+       tcase_add_test(tc, issue_lease_request_and_release);
+       tcase_add_test(tc, issue_lease_request_and_early_release);
+       tcase_add_test(tc, issue_multiple_lease_requests);
+       suite_add_tcase(s, tc);
+}
+
+/**************  File descriptor sending tests ************/
+
+/* Test the sending (and failure to send) of file descriptors
+ * to the client.
+ */
+
+/* send_fd_to_client
+ *
+ * Test details: Send a valid fd to a given client.
+ * Expected results: The correct fd is successfully sent.
+ */
+START_TEST(send_fd_to_client)
+{
+       struct ls *ls = create_default_server();
+
+       struct client_state *cstate = test_client_start(&default_test_config);
+
+       struct ls_req req;
+       bool req_valid = ls_get_request(ls, &req);
+       ck_assert_int_eq(req_valid, true);
+       check_request(&req, &test_lease, LS_REQ_GET_LEASE);
+
+       /* send an fd to the client*/
+       int test_fd = get_dummy_fd();
+       ck_assert_int_eq(ls_send_fd(ls, req.server, test_fd), true);
+
+       test_client_stop(cstate);
+       get_and_check_request(ls, &test_lease, LS_REQ_RELEASE_LEASE);
+
+       ck_assert_int_eq(default_test_config.connection_completed, true);
+       ck_assert_int_eq(default_test_config.has_data, true);
+       check_fd_equality(test_fd, default_test_config.received_fd);
+}
+END_TEST
+
+/* ls_send_fd_is_noop_when_fd_is_invalid
+ *
+ * Test details: Call ls_send_fd() with an invalid  fd.
+ * Expected results: No fd is sent to client.  The connection to the
+ *                   client is closed.
+ */
+START_TEST(ls_send_fd_is_noop_when_fd_is_invalid)
+{
+       struct ls *ls = create_default_server();
+
+       struct client_state *cstate = test_client_start(&default_test_config);
+
+       struct ls_req req;
+       bool req_valid = ls_get_request(ls, &req);
+       ck_assert_int_eq(req_valid, true);
+       check_request(&req, &test_lease, LS_REQ_GET_LEASE);
+
+       int invalid_fd = get_dummy_fd();
+       close(invalid_fd);
+
+       ck_assert_int_eq(ls_send_fd(ls, req.server, invalid_fd), false);
+
+       test_client_stop(cstate);
+       get_and_check_request(ls, &test_lease, LS_REQ_RELEASE_LEASE);
+       ck_assert_int_eq(default_test_config.connection_completed, true);
+       ck_assert_int_eq(default_test_config.has_data, false);
+}
+END_TEST
+
+static void add_fd_send_tests(Suite *s)
+{
+       TCase *tc = tcase_create("File descriptor sending tests");
+
+       tcase_add_checked_fixture(tc, test_setup, test_shutdown);
+
+       tcase_add_test(tc, send_fd_to_client);
+       tcase_add_test(tc, ls_send_fd_is_noop_when_fd_is_invalid);
+       suite_add_tcase(s, tc);
+}
+
+int main(void)
+{
+       int number_failed;
+       Suite *s;
+       SRunner *sr;
+
+       s = suite_create("DLM lease server tests");
+
+       add_error_tests(s);
+       add_client_request_tests(s);
+       add_fd_send_tests(s);
+
+       sr = srunner_create(s);
+
+       srunner_run_all(sr, CK_NORMAL);
+       number_failed = srunner_ntests_failed(sr);
+       srunner_free(sr);
+       return (number_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE;
+}
diff --git a/drm-lease-manager/test/meson.build b/drm-lease-manager/test/meson.build
new file mode 100644 (file)
index 0000000..7d42bec
--- /dev/null
@@ -0,0 +1,30 @@
+check_dep = dependency('check')
+
+ls_inc = include_directories('..')
+
+ls_objects = main.extract_objects(lease_server_files)
+ls_test_sources = [
+   'lease-server-test.c',
+   'test-socket-client.c',
+]
+
+ls_test = executable('lease-server-test',
+           sources: ls_test_sources,
+           objects: ls_objects,
+           dependencies: [check_dep, fff_dep, dlmcommon_dep, thread_dep],
+           include_directories: ls_inc)
+
+lm_objects = main.extract_objects(lease_manager_files)
+lm_test_sources = [
+    'lease-manager-test.c',
+    'test-drm-device.c',
+]
+
+lm_test = executable('lease-manager-test',
+           sources: lm_test_sources,
+           objects: lm_objects,
+           dependencies: [check_dep, fff_dep, dlmcommon_dep, drm_dep],
+           include_directories: ls_inc)
+
+test('DRM Lease manager - socket server test', ls_test, is_parallel: false)
+test('DRM Lease manager - DRM interface test', lm_test)
diff --git a/drm-lease-manager/test/test-drm-device.c b/drm-lease-manager/test/test-drm-device.c
new file mode 100644 (file)
index 0000000..844599a
--- /dev/null
@@ -0,0 +1,122 @@
+/* Copyright 2020-2021 IGEL Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <check.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <xf86drmMode.h>
+
+#include "test-drm-device.h"
+#define UNUSED(x) (void)(x)
+
+/* Set the base value for IDs of each resource type.
+ * These can be adjusted if test cases need more IDs. */
+#define IDS_PER_RES_TYPE 32
+
+#define CRTC_BASE (IDS_PER_RES_TYPE)
+#define CONNECTOR_BASE (CRTC_BASE + IDS_PER_RES_TYPE)
+#define ENCODER_BASE (CONNECTOR_BASE + IDS_PER_RES_TYPE)
+#define PLANE_BASE (ENCODER_BASE + IDS_PER_RES_TYPE)
+#define LESSEE_ID_BASE (PLANE_BASE + IDS_PER_RES_TYPE)
+
+struct drm_device test_device;
+
+#define ALLOC_RESOURCE(res, container)                           \
+       do {                                                     \
+               if (res != 0) {                                  \
+                       test_device.container.res =              \
+                           malloc(sizeof(uint32_t) * res);      \
+                       if (!test_device.container.res)          \
+                               return false;                    \
+                       test_device.container.count_##res = res; \
+               }                                                \
+       } while (0)
+
+#define FILL_RESOURCE(res, RES, container)                     \
+       for (int i = 0; i < res; i++) {                        \
+               test_device.container.res[i] = RES##_BASE + i; \
+       }
+
+bool setup_drm_test_device(int crtcs, int connectors, int encoders, int planes)
+{
+       int lessee_ids = crtcs;
+       ALLOC_RESOURCE(crtcs, resources);
+       ALLOC_RESOURCE(connectors, resources);
+       ALLOC_RESOURCE(encoders, resources);
+       ALLOC_RESOURCE(planes, plane_resources);
+       ALLOC_RESOURCE(lessee_ids, leases);
+
+       FILL_RESOURCE(crtcs, CRTC, resources);
+       FILL_RESOURCE(connectors, CONNECTOR, resources);
+       FILL_RESOURCE(encoders, ENCODER, resources);
+       FILL_RESOURCE(planes, PLANE, plane_resources);
+       FILL_RESOURCE(lessee_ids, LESSEE_ID, leases);
+
+       return true;
+}
+
+void reset_drm_test_device(void)
+{
+       free(test_device.resources.crtcs);
+       free(test_device.resources.connectors);
+       free(test_device.resources.encoders);
+       free(test_device.plane_resources.planes);
+       free(test_device.leases.lessee_ids);
+       memset(&test_device, 0, sizeof(test_device));
+}
+
+void setup_test_device_layout(drmModeConnector *connectors,
+                             drmModeEncoder *encoders, drmModePlane *planes)
+{
+       test_device.layout.connectors = connectors;
+       test_device.layout.encoders = encoders;
+       test_device.layout.planes = planes;
+}
+
+#define GET_DRM_RESOURCE_FN(Res, res, RES, container)                       \
+       drmMode##Res##Ptr get_##res(int fd, uint32_t id)                    \
+       {                                                                   \
+               UNUSED(fd);                                                 \
+               if (id == 0)                                                \
+                       return NULL;                                        \
+               ck_assert_int_ge(id, RES##_BASE);                           \
+               ck_assert_int_lt(                                           \
+                   id, RES##_BASE + test_device.container.count_##res##s); \
+               return &test_device.layout.res##s[id - RES##_BASE];         \
+       }
+
+GET_DRM_RESOURCE_FN(Connector, connector, CONNECTOR, resources)
+GET_DRM_RESOURCE_FN(Encoder, encoder, ENCODER, resources)
+GET_DRM_RESOURCE_FN(Plane, plane, PLANE, plane_resources)
+
+int create_lease(int fd, const uint32_t *objects, int num_objects, int flags,
+                uint32_t *lessee_id)
+{
+       UNUSED(fd);
+       UNUSED(objects);
+       UNUSED(num_objects);
+       UNUSED(flags);
+
+       int lease_count = test_device.leases.count;
+       if (lease_count < test_device.leases.count_lessee_ids)
+               *lessee_id = test_device.leases.lessee_ids[lease_count];
+       else
+               *lessee_id = 0;
+
+       test_device.leases.count++;
+
+       return 0;
+}
diff --git a/drm-lease-manager/test/test-drm-device.h b/drm-lease-manager/test/test-drm-device.h
new file mode 100644 (file)
index 0000000..1d5b683
--- /dev/null
@@ -0,0 +1,81 @@
+/* Copyright 2020-2021 IGEL Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef TEST_DRM_DEVICE_H
+#define TEST_DRM_DEVICE_H
+
+#include <xf86drmMode.h>
+
+/* TEST_DRM_DEVICE can be the path to any
+ * file that can be opened.
+ */
+#define TEST_DRM_DEVICE "/dev/null"
+
+struct drm_device {
+       drmModeRes resources;
+       drmModePlaneRes plane_resources;
+       struct {
+               drmModeConnector *connectors;
+               drmModeEncoder *encoders;
+               drmModePlane *planes;
+       } layout;
+
+       struct {
+               int count;
+               uint32_t *lessee_ids;
+               int count_lessee_ids;
+       } leases;
+};
+
+extern struct drm_device test_device;
+
+bool setup_drm_test_device(int crtcs, int connectors, int encoders, int planes);
+void setup_test_device_layout(drmModeConnector *connectors,
+                             drmModeEncoder *encoders, drmModePlane *planes);
+void reset_drm_test_device(void);
+
+drmModeConnectorPtr get_connector(int fd, uint32_t id);
+drmModeEncoderPtr get_encoder(int fd, uint32_t id);
+drmModePlanePtr get_plane(int fd, uint32_t id);
+int create_lease(int fd, const uint32_t *objects, int num_objects, int flags,
+                uint32_t *lessee_id);
+
+#define TEST_DEVICE_RESOURCES (&test_device.resources)
+#define TEST_DEVICE_PLANE_RESOURCES (&test_device.plane_resources)
+
+#define CRTC_ID(x) (test_device.resources.crtcs[x])
+#define CONNECTOR_ID(x) (test_device.resources.connectors[x])
+#define ENCODER_ID(x) (test_device.resources.encoders[x])
+#define PLANE_ID(x) (test_device.plane_resources.planes[x])
+#define LESSEE_ID(x) (test_device.leases.lessee_ids[x])
+
+#define CONNECTOR(cid, eid, encs, enc_cnt)                   \
+       {                                                    \
+               .connector_id = cid, .encoder_id = eid,      \
+               .count_encoders = enc_cnt, .encoders = encs, \
+       }
+
+#define ENCODER(eid, crtc, crtc_mask)               \
+       {                                           \
+               .encoder_id = eid, .crtc_id = crtc, \
+               .possible_crtcs = crtc_mask,        \
+       }
+
+#define PLANE(pid, crtc_mask)                                 \
+       {                                                     \
+               .plane_id = pid, .possible_crtcs = crtc_mask, \
+       }
+
+#endif
diff --git a/drm-lease-manager/test/test-socket-client.c b/drm-lease-manager/test/test-socket-client.c
new file mode 100644 (file)
index 0000000..260437a
--- /dev/null
@@ -0,0 +1,162 @@
+/* Copyright 2020-2021 IGEL Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "test-socket-client.h"
+
+#include <check.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <poll.h>
+#include <pthread.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <unistd.h>
+
+#include "socket-path.h"
+
+#define DEFAULT_RECV_TIMEOUT (100) // timeout in ms to receive data from server
+
+struct client_state {
+       pthread_t tid;
+
+       int socket_fd;
+       struct test_config *config;
+};
+
+static void client_gst_socket_status(int socket_fd, struct test_config *config)
+{
+
+       config->connection_completed = true;
+
+       struct pollfd pfd = {.fd = socket_fd, .events = POLLIN};
+       if (poll(&pfd, 1, config->recv_timeout) <= 0)
+               return;
+
+       if (pfd.revents & POLLHUP)
+               config->connection_completed = false;
+
+       if (pfd.revents & POLLIN)
+               config->has_data = true;
+
+       return;
+}
+
+static int receive_fd_from_socket(int sockfd)
+{
+       union {
+               struct cmsghdr align;
+               char buf[CMSG_SPACE(sizeof(int))];
+       } u;
+
+       char data;
+       struct iovec iov = {.iov_base = &data, .iov_len = sizeof(data)};
+       struct msghdr msg = {
+           .msg_iov = &iov,
+           .msg_iovlen = 1,
+           .msg_control = u.buf,
+           .msg_controllen = sizeof(u.buf),
+       };
+
+       if (recvmsg(sockfd, &msg, 0) < 0)
+               return -1;
+
+       int recv_fd = -1;
+       for (struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); cmsg != NULL;
+            cmsg = CMSG_NXTHDR(&msg, cmsg)) {
+               ck_assert_int_eq(cmsg->cmsg_level, SOL_SOCKET);
+
+               if (cmsg->cmsg_type != SCM_RIGHTS)
+                       continue;
+
+               int nfds = (cmsg->cmsg_len - CMSG_LEN(0)) / sizeof(int);
+               ck_assert_int_eq(nfds, 1);
+               recv_fd = *(int *)CMSG_DATA(cmsg);
+       }
+       return recv_fd;
+}
+
+static void *test_client_thread(void *arg)
+{
+       struct client_state *cstate = arg;
+       struct test_config *config = cstate->config;
+
+       struct sockaddr_un address = {
+           .sun_family = AF_UNIX,
+       };
+
+       ck_assert_int_eq(
+           sockaddr_set_lease_server_path(&address, config->lease->name),
+           true);
+
+       int client = socket(PF_UNIX, SOCK_STREAM, 0);
+       ck_assert_int_ge(client, 0);
+
+       int ret;
+       ret = connect(client, (struct sockaddr *)&address, sizeof(address));
+       if (ret != 0) {
+               printf("Connect failed;: %s\n", strerror(errno));
+               close(client);
+               return NULL;
+       }
+
+       if (!config->recv_timeout)
+               config->recv_timeout = DEFAULT_RECV_TIMEOUT;
+
+       client_gst_socket_status(client, config);
+
+       if (config->has_data) {
+               config->received_fd = receive_fd_from_socket(client);
+       }
+
+       cstate->socket_fd = client;
+
+       return NULL;
+}
+
+struct client_state *test_client_start(struct test_config *test_config)
+{
+       struct client_state *cstate = malloc(sizeof(*cstate));
+
+       *cstate = (struct client_state){
+           .config = test_config,
+       };
+
+       pthread_create(&cstate->tid, NULL, test_client_thread, cstate);
+
+       return cstate;
+}
+
+void test_client_stop(struct client_state *cstate)
+{
+
+       ck_assert_ptr_ne(cstate, NULL);
+
+       pthread_join(cstate->tid, NULL);
+
+       if (cstate->socket_fd >= 0)
+               close(cstate->socket_fd);
+
+       free(cstate);
+}
+
+void test_config_cleanup(struct test_config *config)
+{
+       if (config->has_data && config->received_fd >= 0)
+               close(config->received_fd);
+}
diff --git a/drm-lease-manager/test/test-socket-client.h b/drm-lease-manager/test/test-socket-client.h
new file mode 100644 (file)
index 0000000..b5bdf6d
--- /dev/null
@@ -0,0 +1,37 @@
+/* Copyright 2020-2021 IGEL Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef TEST_SOCKET_CLIENT_H
+#define TEST_SOCKET_CLIENT_H
+#include <stdbool.h>
+
+#include "drm-lease.h"
+struct test_config {
+       // settings
+       struct lease_handle *lease;
+       int recv_timeout;
+
+       // outputs
+       int received_fd;
+       bool has_data;
+       bool connection_completed;
+};
+
+void test_config_cleanup(struct test_config *config);
+
+struct client_state;
+struct client_state *test_client_start(struct test_config *test_config);
+void test_client_stop(struct client_state *cstate);
+#endif
diff --git a/examples/dlm-client-test/dlm-client-test.c b/examples/dlm-client-test/dlm-client-test.c
new file mode 100644 (file)
index 0000000..411fce7
--- /dev/null
@@ -0,0 +1,94 @@
+/* Copyright 2020-2021 IGEL Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "dlmclient.h"
+
+#include <errno.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <xf86drmMode.h>
+
+static void usage(const char *name)
+{
+       fprintf(stderr,
+               "%s <lease name>\n"
+               "\tlease name: Name of lease to check\n",
+               name);
+}
+
+static void dump_lease_resources(int lease_fd)
+{
+       drmModeObjectListPtr ol = drmModeGetLease(lease_fd);
+       if (!ol) {
+               fprintf(
+                   stderr,
+                   "drmModeGetLease failed. Received fd is not a DRM lease\n");
+               return;
+       }
+       free(ol);
+
+       drmModeRes *res = drmModeGetResources(lease_fd);
+       if (!res) {
+               fprintf(stderr, "drmModeGetResources failed\n");
+               return;
+       }
+
+       for (int i = 0; i < res->count_crtcs; i++)
+               printf("crtc-id: %u\n", res->crtcs[i]);
+
+       for (int i = 0; i < res->count_connectors; i++)
+               printf("connector-id: %u\n", res->connectors[i]);
+
+       drmModeFreeResources(res);
+
+       drmModePlaneRes *plane_res = drmModeGetPlaneResources(lease_fd);
+       if (!plane_res) {
+               fprintf(stderr, "drmModeGetPlaneResources failed\n");
+               return;
+       }
+
+       for (uint32_t i = 0; i < plane_res->count_planes; i++)
+               printf("plane-id: %u\n", plane_res->planes[i]);
+
+       drmModeFreePlaneResources(plane_res);
+}
+
+int main(int argc, char **argv)
+{
+       if (argc < 2) {
+               usage(argv[0]);
+               return EXIT_FAILURE;
+       }
+
+       struct dlm_lease *lease = dlm_get_lease(argv[1]);
+       if (!lease) {
+               fprintf(stderr, "dlm_get_lease: %s\n", strerror(errno));
+               return EXIT_FAILURE;
+       }
+
+       int lease_fd = dlm_lease_fd(lease);
+       if (lease_fd < 0) {
+               fprintf(stderr, "dlm_lease_fd: %s\n", strerror(errno));
+               dlm_release_lease(lease);
+               return EXIT_FAILURE;
+       }
+
+       dump_lease_resources(lease_fd);
+       dlm_release_lease(lease);
+       return EXIT_SUCCESS;
+}
diff --git a/examples/dlm-client-test/meson.build b/examples/dlm-client-test/meson.build
new file mode 100644 (file)
index 0000000..aa51c78
--- /dev/null
@@ -0,0 +1,4 @@
+executable('dlm-client-test',
+    ['dlm-client-test.c'],
+    dependencies : [dlmclient_dep, drm_dep],
+)
diff --git a/examples/meson.build b/examples/meson.build
new file mode 100644 (file)
index 0000000..706c2b8
--- /dev/null
@@ -0,0 +1 @@
+subdir('dlm-client-test')
diff --git a/libdlmclient/dlmclient.c b/libdlmclient/dlmclient.c
new file mode 100644 (file)
index 0000000..32493d3
--- /dev/null
@@ -0,0 +1,170 @@
+/* Copyright 2020-2021 IGEL Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "dlmclient.h"
+#include "log.h"
+#include "socket-path.h"
+
+#include <assert.h>
+#include <errno.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <unistd.h>
+
+void dlm_enable_debug_log(bool enable)
+{
+       dlm_log_enable_debug(enable);
+}
+
+struct dlm_lease {
+       int dlm_server_sock;
+       int lease_fd;
+};
+
+static bool lease_connect(struct dlm_lease *lease, const char *name)
+{
+       struct sockaddr_un sa = {
+           .sun_family = AF_UNIX,
+       };
+
+       if (!sockaddr_set_lease_server_path(&sa, name))
+               return false;
+
+       int dlm_server_sock = socket(AF_UNIX, SOCK_STREAM, 0);
+       if (dlm_server_sock < 0) {
+               DEBUG_LOG("Socket creation failed: %s\n", strerror(errno));
+               return false;
+       }
+
+       while (connect(dlm_server_sock, (struct sockaddr *)&sa,
+                      sizeof(struct sockaddr_un)) == -1) {
+               if (errno == EINTR)
+                       continue;
+               DEBUG_LOG("Cannot connect to %s: %s\n", sa.sun_path,
+                         strerror(errno));
+               close(dlm_server_sock);
+               return false;
+       }
+       lease->dlm_server_sock = dlm_server_sock;
+       return true;
+}
+
+static bool lease_recv_fd(struct dlm_lease *lease)
+{
+       char ctrl_buf[CMSG_SPACE(sizeof(int))] = {0};
+       char data[1] = {0};
+
+       struct iovec iov[1];
+       iov[0].iov_base = data;
+       iov[0].iov_len = sizeof(data);
+
+       struct msghdr msg = {
+           .msg_control = ctrl_buf,
+           .msg_controllen = CMSG_SPACE(sizeof(int)),
+           .msg_iov = iov,
+           .msg_iovlen = 1,
+       };
+
+       int ret;
+       while ((ret = recvmsg(lease->dlm_server_sock, &msg, 0)) <= 0) {
+               if (ret == 0) {
+                       errno = EACCES;
+                       DEBUG_LOG("Request rejected by DRM lease manager\n");
+                       // TODO: Report why the request was rejected.
+                       return false;
+               }
+               if (errno != EINTR) {
+                       DEBUG_LOG("Socket data receive error: %s\n",
+                                 strerror(errno));
+                       return false;
+               }
+       }
+
+       lease->lease_fd = -1;
+       struct cmsghdr *cmsg;
+       for (cmsg = CMSG_FIRSTHDR(&msg); cmsg != NULL;
+            cmsg = CMSG_NXTHDR(&msg, cmsg)) {
+               if (cmsg->cmsg_level == SOL_SOCKET &&
+                   cmsg->cmsg_type == SCM_RIGHTS) {
+                       int nfds = (cmsg->cmsg_len - CMSG_LEN(0)) / sizeof(int);
+                       int *fds = (int *)CMSG_DATA(cmsg);
+
+                       if (nfds == 1) {
+                               lease->lease_fd = fds[0];
+                               break;
+                       }
+
+                       DEBUG_LOG(
+                           "Expected 1 fd from lease manager. Received %d\n",
+                           nfds);
+                       /* Close any unexpected fds so we don't leak them. */
+                       for (int i = 0; i < nfds; i++)
+                               close(fds[i]);
+                       break;
+               }
+       }
+
+       if (lease->lease_fd < 0) {
+               DEBUG_LOG("Expected data not received from lease manager\n");
+               errno = EPROTO;
+               return false;
+       }
+
+       return true;
+}
+
+struct dlm_lease *dlm_get_lease(const char *name)
+{
+       struct dlm_lease *lease = calloc(1, sizeof(struct dlm_lease));
+       if (!lease) {
+               DEBUG_LOG("can't allocate memory : %s\n", strerror(errno));
+               return NULL;
+       }
+
+       if (!lease_connect(lease, name)) {
+               free(lease);
+               return NULL;
+       }
+
+       if (!lease_recv_fd(lease)) {
+               close(lease->dlm_server_sock);
+               free(lease);
+               return NULL;
+       }
+
+       return lease;
+}
+
+void dlm_release_lease(struct dlm_lease *lease)
+{
+       if (!lease)
+               return;
+
+       close(lease->lease_fd);
+       close(lease->dlm_server_sock);
+       free(lease);
+}
+
+int dlm_lease_fd(struct dlm_lease *lease)
+{
+       if (!lease)
+               return -1;
+
+       return lease->lease_fd;
+}
diff --git a/libdlmclient/dlmclient.h b/libdlmclient/dlmclient.h
new file mode 100644 (file)
index 0000000..908cd6b
--- /dev/null
@@ -0,0 +1,86 @@
+/* Copyright 2020-2021 IGEL Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @file dlmclient.h
+ */
+#ifndef DLM_CLIENT_H
+#define DLM_CLIENT_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdbool.h>
+
+/**
+ * @brief Enable debug logging
+ *
+ * @param[in] enable enable/disable debug logging
+ */
+void dlm_enable_debug_log(bool enable);
+
+/**
+ * @brief lease handle
+ */
+struct dlm_lease;
+
+/**
+ * @brief  Get a DRM lease from the lease manager
+ *
+ * @param[in] name requested lease
+ * @return A pointer to a lease handle on success.
+ *         On error this function returns NULL and errno is set accordingly.
+ *
+ *  Possible errors:
+ *
+ *  errno        |  Meaning
+ *  -------------|-------------------------------------------------------------
+ *  EACCESS      |  Cannot access lease manager socket directory
+ *  EACCESS      |  Lease request denied by lease manager
+ *  ENAMETOOLONG |  The path to the lease manager socket directory is too long
+ *  ENOENT       |  Lease manager or requested lease not available
+ *  ENOMEM       |  Out of memory during operation
+ *  EPROTO       |  Protocol error in communication with lease manager
+ *
+ *  This list is not exhaustive, and errno may be set to other error codes,
+ *  especially those related to socket communication.
+ */
+struct dlm_lease *dlm_get_lease(const char *name);
+
+/**
+ * @brief  Release a lease handle
+ *
+ * @details Release a lease handle.  The lease handle will be invalidated and
+ *          the associated DRM lease wil be revoked.  Any fd's retrieved from
+ *          dlm_lease_fd() will be closed.
+ * @param[in] lease pointer to lease handle
+ */
+void dlm_release_lease(struct dlm_lease *lease);
+
+/**
+ * @brief Get a DRM Master fd from a valid lease handle
+ *
+ * @param[in] lease pointer to a lease handle
+ * @return A DRM Master file descriptor for the lease on success.
+ *         -1 is returned when called with a NULL lease handle.
+ */
+int dlm_lease_fd(struct dlm_lease *lease);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/libdlmclient/docs/Doxyfile.in b/libdlmclient/docs/Doxyfile.in
new file mode 100644 (file)
index 0000000..dce4244
--- /dev/null
@@ -0,0 +1,9 @@
+# General information
+PROJECT_NAME = "DRM Lease Manager Client Library"
+OUTPUT_DIRECTORY = @DOXYGEN_OUTPUT@
+INPUT = @README@ @CLIENT_HEADER_DIR@
+USE_MDFILE_AS_MAINPAGE = @README@
+FILE_PATTERNS = *.h
+GENERATE_LATEX = NO
+OPTIMIZE_OUTPUT_FOR_C = YES
+STRIP_FROM_PATH    = @CLIENT_HEADER_DIR@
diff --git a/libdlmclient/docs/meson.build b/libdlmclient/docs/meson.build
new file mode 100644 (file)
index 0000000..563e5fb
--- /dev/null
@@ -0,0 +1,21 @@
+doxygen = find_program('doxygen', required : false)
+
+readme = join_paths(meson.source_root(), 'README.md')
+
+if get_option('enable-docs') and doxygen.found()
+    conf_data = configuration_data()
+    conf_data.set('README', readme)
+    conf_data.set('CLIENT_HEADER_DIR', dlmclient_header_dir)
+    conf_data.set('DOXYGEN_OUTPUT', meson.current_build_dir())
+    doxyfile = configure_file(
+        input: 'Doxyfile.in',
+        output: 'Doxyfile',
+        configuration: conf_data
+    )
+    custom_target('docs',
+      input: [doxyfile, readme, dlmclient_headers],
+      build_by_default: true,
+      command: [doxygen, '@INPUT0@'],
+      output: ['html']
+    )
+endif
diff --git a/libdlmclient/meson.build b/libdlmclient/meson.build
new file mode 100644 (file)
index 0000000..d63a842
--- /dev/null
@@ -0,0 +1,38 @@
+dlmclient_sources = files(
+    'dlmclient.c'
+)
+
+dlmclient_headers = files(
+    'dlmclient.h'
+)
+
+libdlmclient = library(
+    'dlmclient',
+    sources: dlmclient_sources,
+    version: meson.project_version(),
+    dependencies: [dlmcommon_dep],
+    install: true,
+)
+
+dlmclient_dep = declare_dependency(
+    link_with: libdlmclient,
+    include_directories: include_directories('.')
+)
+
+
+install_headers(dlmclient_headers, subdir: 'libdlmclient')
+
+pkg.generate(
+    name: 'libdlmclient',
+    libraries: libdlmclient,
+    subdirs: [ 'libdlmclient' ],
+    version: meson.project_version(),
+    description: 'DRM lease manager client library',
+)
+
+dlmclient_header_dir = meson.current_source_dir()
+subdir('docs')
+
+if enable_tests
+  subdir('test')
+endif
diff --git a/libdlmclient/test/libdlmclient-test.c b/libdlmclient/test/libdlmclient-test.c
new file mode 100644 (file)
index 0000000..5b04c09
--- /dev/null
@@ -0,0 +1,291 @@
+/* Copyright 2020-2021 IGEL Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <check.h>
+
+#include <dirent.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "config.h"
+#include "dlmclient.h"
+#include "test-helpers.h"
+#include "test-socket-server.h"
+
+#define SOCKETDIR "/tmp"
+
+#define TEST_LEASE_NAME "test-lease"
+
+/**************  Test fixutre functions *************/
+struct test_config default_test_config;
+
+static void test_setup(void)
+{
+       dlm_enable_debug_log(true);
+       setenv("DLM_RUNTIME_PATH", SOCKETDIR, 1);
+
+       default_test_config = (struct test_config){
+           .lease_name = TEST_LEASE_NAME,
+           .nfds = 1,
+       };
+}
+
+static void test_shutdown(void)
+{
+       test_config_cleanup(&default_test_config);
+}
+
+/**************  Lease manager error tests *************/
+
+/* These tests verify that the client library gracefully handles
+ * failures when trying to receive data from the lease manager.
+ * Failures or errors in the lease manager should cause meaningful
+ * errors to be reported by the client.  Lease manager errors should
+ * not cause crashes or invalid state in the client */
+
+/* manager_connection_err
+ *
+ * Test details: Simulate socket connection failure.
+ * Expected results: dlm_get_lease() fails.
+ */
+START_TEST(manager_connection_err)
+{
+       struct server_state *sstate = test_server_start(&default_test_config);
+
+       struct dlm_lease *lease = dlm_get_lease(TEST_LEASE_NAME "-bad");
+
+       ck_assert_ptr_eq(lease, NULL);
+
+       test_server_stop(sstate);
+}
+END_TEST
+
+/* no_data_from_manager
+ *
+ * Test details: Close the remote (lease manager) without sending any data.
+ *               Currently this means that the lease request has been rejected
+ *               for some reason.
+ *
+ * TODO: Update this when the client-server protocol is updated to
+ *       include the reason for the lease rejection.
+ *
+ * Expected results: dlm_get_lease() fails, errno set to EACCESS.
+ */
+START_TEST(no_data_from_manager)
+{
+
+       struct test_config config = {
+           .lease_name = TEST_LEASE_NAME,
+           .send_no_data = true,
+       };
+
+       struct server_state *sstate = test_server_start(&config);
+
+       struct dlm_lease *lease = dlm_get_lease(TEST_LEASE_NAME);
+
+       ck_assert_ptr_eq(lease, NULL);
+       ck_assert_int_eq(errno, EACCES);
+
+       test_server_stop(sstate);
+}
+END_TEST
+
+/* no_lease_fd_from_manager
+ *
+ * Test details: Simulate receiving response from lease manager with
+ *               no fd attached.  (i.e. a protocol error)
+ *
+ * Expected results: dlm_get_lease() fails, errno set to EPROTO.
+ */
+START_TEST(no_lease_fd_from_manager)
+{
+       /* Receive message from the lease manager with missing lease fd */
+       struct test_config config = {
+           .lease_name = TEST_LEASE_NAME,
+           .send_data_without_fd = true,
+       };
+
+       struct server_state *sstate = test_server_start(&config);
+
+       struct dlm_lease *lease = dlm_get_lease(TEST_LEASE_NAME);
+
+       ck_assert_ptr_eq(lease, NULL);
+       ck_assert_int_eq(errno, EPROTO);
+
+       test_server_stop(sstate);
+}
+END_TEST
+
+static void add_lease_manager_error_tests(Suite *s)
+{
+       TCase *tc = tcase_create("Lease manager error handling");
+
+       tcase_add_checked_fixture(tc, test_setup, test_shutdown);
+
+       tcase_add_test(tc, manager_connection_err);
+       tcase_add_test(tc, no_data_from_manager);
+       tcase_add_test(tc, no_lease_fd_from_manager);
+
+       suite_add_tcase(s, tc);
+}
+
+/**************  Lease handling tests  *****************/
+
+/* These tests verify that the client library handles the received
+ * lease data properly. Receiving the lease fds without leaks,
+ * properly passing the fds to the client application and cleaning
+ * them up on release.
+ */
+
+static int count_open_fds(void)
+{
+       int fds = 0;
+       DIR *dirp = opendir("/proc/self/fd");
+       while ((readdir(dirp) != NULL))
+               fds++;
+       closedir(dirp);
+       return fds;
+}
+
+/* receive_fd_from_manager
+ *
+ * Test details: Successfully receive a file descriptor.
+ * Expected results: dlm_get_lease() succeeds.
+ *                   dlm_lease_fd() returns the correct fd value.
+ */
+START_TEST(receive_fd_from_manager)
+{
+       struct server_state *sstate = test_server_start(&default_test_config);
+
+       struct dlm_lease *lease = dlm_get_lease(TEST_LEASE_NAME);
+       ck_assert_ptr_ne(lease, NULL);
+
+       int received_fd = dlm_lease_fd(lease);
+
+       int sent_fd = default_test_config.fds[0];
+
+       check_fd_equality(received_fd, sent_fd);
+
+       dlm_release_lease(lease);
+
+       test_server_stop(sstate);
+       close(sent_fd);
+}
+END_TEST
+
+/* lease_fd_is_closed_on_release
+ *
+ * Test details: Verify that dlm_release_lease() closes the lease fd.
+ * Expected results: lease fd is closed.
+ */
+START_TEST(lease_fd_is_closed_on_release)
+{
+       struct server_state *sstate = test_server_start(&default_test_config);
+
+       struct dlm_lease *lease = dlm_get_lease(TEST_LEASE_NAME);
+       ck_assert_ptr_ne(lease, NULL);
+
+       int received_fd = dlm_lease_fd(lease);
+
+       check_fd_is_open(received_fd);
+       dlm_release_lease(lease);
+       check_fd_is_closed(received_fd);
+
+       test_server_stop(sstate);
+}
+END_TEST
+
+/* dlm_lease_fd_always_returns_same_lease
+ *
+ * Test details: Verify that dlm_lease_fd() always returns the same value
+ *               for a given lease.
+ * Expected results: same value is returned when called multiple times.
+ */
+START_TEST(dlm_lease_fd_always_returns_same_lease)
+{
+       struct server_state *sstate = test_server_start(&default_test_config);
+
+       struct dlm_lease *lease = dlm_get_lease(TEST_LEASE_NAME);
+       ck_assert_ptr_ne(lease, NULL);
+
+       int received_fd = dlm_lease_fd(lease);
+
+       ck_assert_int_eq(received_fd, dlm_lease_fd(lease));
+       ck_assert_int_eq(received_fd, dlm_lease_fd(lease));
+
+       dlm_release_lease(lease);
+
+       test_server_stop(sstate);
+}
+END_TEST
+
+START_TEST(verify_that_unused_fds_are_not_leaked)
+{
+       int nopen_fds = count_open_fds();
+
+       struct test_config config = {
+           .lease_name = TEST_LEASE_NAME,
+           .nfds = 2,
+       };
+
+       struct server_state *sstate = test_server_start(&config);
+
+       struct dlm_lease *lease = dlm_get_lease(TEST_LEASE_NAME);
+
+       ck_assert_ptr_eq(lease, NULL);
+       ck_assert_int_eq(errno, EPROTO);
+
+       dlm_release_lease(lease);
+
+       test_server_stop(sstate);
+
+       test_config_cleanup(&config);
+       ck_assert_int_eq(nopen_fds, count_open_fds());
+}
+END_TEST
+
+static void add_lease_handling_tests(Suite *s)
+{
+       TCase *tc = tcase_create("Lease processing tests");
+
+       tcase_add_checked_fixture(tc, test_setup, test_shutdown);
+
+       tcase_add_test(tc, receive_fd_from_manager);
+       tcase_add_test(tc, lease_fd_is_closed_on_release);
+       tcase_add_test(tc, dlm_lease_fd_always_returns_same_lease);
+       tcase_add_test(tc, verify_that_unused_fds_are_not_leaked);
+       suite_add_tcase(s, tc);
+}
+
+int main(void)
+{
+       int number_failed;
+       Suite *s;
+       SRunner *sr;
+
+       s = suite_create("DLM client library tests");
+
+       add_lease_manager_error_tests(s);
+       add_lease_handling_tests(s);
+
+       sr = srunner_create(s);
+
+       srunner_run_all(sr, CK_NORMAL);
+       number_failed = srunner_ntests_failed(sr);
+       srunner_free(sr);
+       return (number_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE;
+}
diff --git a/libdlmclient/test/meson.build b/libdlmclient/test/meson.build
new file mode 100644 (file)
index 0000000..35e8575
--- /dev/null
@@ -0,0 +1,6 @@
+cl_test = executable('libdlmclient-test',
+           sources: [ 'libdlmclient-test.c', 'test-socket-server.c'],
+           dependencies: [check_dep, fff_dep, dlmcommon_dep, dlmclient_dep, thread_dep],
+           include_directories: configuration_inc)
+
+test('Client library test', cl_test, is_parallel: false)
diff --git a/libdlmclient/test/test-socket-server.c b/libdlmclient/test/test-socket-server.c
new file mode 100644 (file)
index 0000000..281aaf7
--- /dev/null
@@ -0,0 +1,169 @@
+/* Copyright 2020-2021 IGEL Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "test-socket-server.h"
+#include <check.h>
+
+#include <pthread.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <unistd.h>
+
+#include "socket-path.h"
+#include "test-helpers.h"
+
+static void send_fd_list_over_socket(int socket, int nfds, int *fds)
+{
+       char data;
+       struct iovec iov = {
+           .iov_base = &data,
+           .iov_len = 1,
+       };
+
+       int bufsize = CMSG_SPACE(nfds * sizeof(int));
+       char *buf = malloc(bufsize);
+
+       struct msghdr msg = {
+           .msg_iov = &iov,
+           .msg_iovlen = 1,
+           .msg_controllen = bufsize,
+           .msg_control = buf,
+       };
+
+       struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
+       cmsg->cmsg_level = SOL_SOCKET;
+       cmsg->cmsg_type = SCM_RIGHTS;
+       cmsg->cmsg_len = CMSG_LEN(nfds * sizeof(int));
+       memcpy(CMSG_DATA(cmsg), fds, sizeof(int) * nfds);
+
+       ck_assert_int_gt(sendmsg(socket, &msg, 0), 0);
+       free(buf);
+}
+
+struct server_state {
+       pthread_t tid;
+       pthread_mutex_t lock;
+       pthread_cond_t cond;
+       bool is_server_started;
+
+       struct test_config *config;
+};
+
+static void *test_server_thread(void *arg)
+{
+       struct server_state *sstate = arg;
+       struct test_config *config = sstate->config;
+
+       struct sockaddr_un address = {
+           .sun_family = AF_UNIX,
+       };
+
+       ck_assert_int_eq(
+           sockaddr_set_lease_server_path(&address, config->lease_name), true);
+
+       int server = socket(PF_UNIX, SOCK_STREAM, 0);
+       ck_assert_int_ge(server, 0);
+
+       unlink(address.sun_path);
+
+       int ret;
+       ret = bind(server, (struct sockaddr *)&address, sizeof(address));
+       ck_assert_int_eq(ret, 0);
+
+       ret = listen(server, 1);
+       ck_assert_int_eq(ret, 0);
+
+       sstate->is_server_started = true;
+       pthread_cond_signal(&sstate->cond);
+
+       int client = accept(server, NULL, NULL);
+       /* accept is the cancellation point for this thread. If
+        * pthread_cancel() is called on this thread, accept() may return
+        * -1, so don't assert on it. */
+
+       if (client < 0) {
+               close(server);
+               return NULL;
+       }
+
+       if (config->send_no_data)
+               goto done;
+
+       if (config->send_data_without_fd) {
+               char data;
+               write(client, &data, 1);
+               goto done;
+       }
+
+       if (config->nfds == 0)
+               config->nfds = 1;
+
+       config->fds = calloc(config->nfds, sizeof(int));
+
+       for (int i = 0; i < config->nfds; i++)
+               config->fds[i] = get_dummy_fd();
+
+       send_fd_list_over_socket(client, config->nfds, config->fds);
+done:
+       close(client);
+       close(server);
+       return NULL;
+}
+
+struct server_state *test_server_start(struct test_config *test_config)
+{
+       struct server_state *sstate = malloc(sizeof(*sstate));
+
+       *sstate = (struct server_state){
+           .lock = PTHREAD_MUTEX_INITIALIZER,
+           .cond = PTHREAD_COND_INITIALIZER,
+           .is_server_started = false,
+           .config = test_config,
+       };
+
+       pthread_create(&sstate->tid, NULL, test_server_thread, sstate);
+
+       pthread_mutex_lock(&sstate->lock);
+       while (!sstate->is_server_started)
+               pthread_cond_wait(&sstate->cond, &sstate->lock);
+       pthread_mutex_unlock(&sstate->lock);
+       return sstate;
+}
+
+void test_server_stop(struct server_state *sstate)
+{
+
+       ck_assert_ptr_ne(sstate, NULL);
+
+       pthread_cancel(sstate->tid);
+       pthread_join(sstate->tid, NULL);
+
+       free(sstate);
+}
+
+void test_config_cleanup(struct test_config *config)
+{
+       if (!config->fds)
+               return;
+
+       for (int i = 0; i < config->nfds; i++)
+               close(config->fds[i]);
+
+       free(config->fds);
+}
diff --git a/libdlmclient/test/test-socket-server.h b/libdlmclient/test/test-socket-server.h
new file mode 100644 (file)
index 0000000..ebdbd2d
--- /dev/null
@@ -0,0 +1,33 @@
+/* Copyright 2020-2021 IGEL Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef TEST_SOCKET_SERVER_H
+#define TEST_SOCKET_SERVER_H
+#include <stdbool.h>
+
+struct test_config {
+       char *lease_name;
+       int nfds;
+       int *fds;
+
+       bool send_data_without_fd;
+       bool send_no_data;
+};
+
+void test_config_cleanup(struct test_config *config);
+
+struct server_state *test_server_start(struct test_config *test_config);
+void test_server_stop(struct server_state *sstate);
+#endif
diff --git a/meson.build b/meson.build
new file mode 100644 (file)
index 0000000..2f6cd15
--- /dev/null
@@ -0,0 +1,61 @@
+project(
+    'lease_manager', 'c',
+    # Version based on Semantic Versioning 2.0.0 (https://semver.org/)
+    version: '0.1.0',
+    default_options: [
+        'warning_level=2',
+    ]
+)
+
+config = configuration_data()
+
+pkg = import('pkgconfig')
+
+compiler_flags = [
+    '-Wstrict-prototypes',
+    '-Wmissing-prototypes',
+]
+
+#Setup and create runtime path
+runtime_path = join_paths(get_option('localstatedir'), get_option('runtime_subdir'))
+config.set_quoted('DLM_DEFAULT_RUNTIME_PATH', runtime_path)
+
+meson.add_install_script('sh', '-c', 'install -d $DESTDIR/$1', '_', runtime_path)
+
+cc = meson.get_compiler('c')
+add_project_arguments(
+    cc.get_supported_arguments(compiler_flags),
+    language: 'c'
+)
+
+configure_file(output: 'config.h',
+               configuration: config)
+
+configuration_inc = include_directories('.')
+
+drm_dep = dependency('libdrm', version: '>= 2.4.89')
+
+enable_tests = get_option('enable-tests')
+
+if enable_tests
+  check_dep = dependency('check')
+  thread_dep = dependency('threads')
+
+# Default to the system provided version of fff.h.
+# otherwise fetch it ourselves.
+  if cc.check_header('fff.h')
+    fff_dep = declare_dependency()
+  else
+    if meson.version().version_compare('>=0.55')
+      fff_proj = subproject('fff')
+      fff_dep = fff_proj.get_variable('fff_dep')
+    else
+      error('Update meson version to >=0.55 to enable unit testing')
+    endif
+  endif
+endif
+
+subdir('common')
+subdir('libdlmclient')
+subdir('drm-lease-manager')
+subdir('examples')
diff --git a/meson_options.txt b/meson_options.txt
new file mode 100644 (file)
index 0000000..ee1f8dc
--- /dev/null
@@ -0,0 +1,17 @@
+option('enable-docs',
+    type: 'boolean',
+    value: true,
+    description: 'Build Doxygen document if available.'
+)
+
+option('enable-tests',
+    type: 'boolean',
+    value: false,
+    description: 'Build unit tests'
+)
+
+option('runtime_subdir',
+    type: 'string',
+    value: 'run/drm-lease-manager',
+    description: 'subdirectory to use for runtime data'
+)
diff --git a/subprojects/fff.wrap b/subprojects/fff.wrap
new file mode 100644 (file)
index 0000000..047a44f
--- /dev/null
@@ -0,0 +1,8 @@
+[wrap-file]
+directory = fff-1.0
+
+source_url = https://github.com/meekrosoft/fff/archive/v1.0.tar.gz
+source_filename = v1.0.tar.gz
+source_hash = 3fbe25ac451d3f56b5b0f82b9a4dbf68e4412b3cee278f7b2feda47fc4d692c6
+
+patch_directory = fff-1.0
diff --git a/subprojects/packagefiles/fff-1.0/meson.build b/subprojects/packagefiles/fff-1.0/meson.build
new file mode 100644 (file)
index 0000000..c7ce1f2
--- /dev/null
@@ -0,0 +1,8 @@
+project('fff', 'c',
+  version: '1.0',
+  license: 'MIT'
+)
+
+fff_dep = declare_dependency(
+        include_directories: include_directories('.')
+)