aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDamian Hobson-Garcia <dhobsong@igel.co.jp>2020-11-24 17:16:39 +0900
committerDamian Hobson-Garcia <dhobsong@igel.co.jp>2021-02-19 10:48:23 +0000
commitf991de200799118355fd75237a740321bda7aaa7 (patch)
tree589f536d835d81f1166410e67aedb578d6e70395
parent464ec461237b36e03c4cdb175e9d87ab502ee1d5 (diff)
Add initial version
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
-rw-r--r--.clang-format9
-rw-r--r--LICENSE202
-rw-r--r--README.md85
-rw-r--r--common/log.c38
-rw-r--r--common/log.h33
-rw-r--r--common/meson.build22
-rw-r--r--common/socket-path.c48
-rw-r--r--common/socket-path.h26
-rw-r--r--common/test/test-helpers.c56
-rw-r--r--common/test/test-helpers.h34
-rw-r--r--drm-lease-manager/drm-lease.h22
-rw-r--r--drm-lease-manager/lease-manager.c386
-rw-r--r--drm-lease-manager/lease-manager.h29
-rw-r--r--drm-lease-manager/lease-server.c335
-rw-r--r--drm-lease-manager/lease-server.h42
-rw-r--r--drm-lease-manager/main.c120
-rw-r--r--drm-lease-manager/meson.build12
-rw-r--r--drm-lease-manager/test/lease-manager-test.c448
-rw-r--r--drm-lease-manager/test/lease-server-test.c380
-rw-r--r--drm-lease-manager/test/meson.build30
-rw-r--r--drm-lease-manager/test/test-drm-device.c122
-rw-r--r--drm-lease-manager/test/test-drm-device.h81
-rw-r--r--drm-lease-manager/test/test-socket-client.c162
-rw-r--r--drm-lease-manager/test/test-socket-client.h37
-rw-r--r--examples/dlm-client-test/dlm-client-test.c94
-rw-r--r--examples/dlm-client-test/meson.build4
-rw-r--r--examples/meson.build1
-rw-r--r--libdlmclient/dlmclient.c170
-rw-r--r--libdlmclient/dlmclient.h86
-rw-r--r--libdlmclient/docs/Doxyfile.in9
-rw-r--r--libdlmclient/docs/meson.build21
-rw-r--r--libdlmclient/meson.build38
-rw-r--r--libdlmclient/test/libdlmclient-test.c291
-rw-r--r--libdlmclient/test/meson.build6
-rw-r--r--libdlmclient/test/test-socket-server.c169
-rw-r--r--libdlmclient/test/test-socket-server.h33
-rw-r--r--meson.build61
-rw-r--r--meson_options.txt17
-rw-r--r--subprojects/fff.wrap8
-rw-r--r--subprojects/packagefiles/fff-1.0/meson.build8
40 files changed, 3775 insertions, 0 deletions
diff --git a/.clang-format b/.clang-format
new file mode 100644
index 0000000..97c7b2c
--- /dev/null
+++ b/.clang-format
@@ -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
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
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
index 0000000..f3f8899
--- /dev/null
+++ b/common/log.c
@@ -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
index 0000000..cd0c0e3
--- /dev/null
+++ b/common/log.h
@@ -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
index 0000000..e465fa5
--- /dev/null
+++ b/common/meson.build
@@ -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
index 0000000..7a412da
--- /dev/null
+++ b/common/socket-path.c
@@ -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
index 0000000..b05c60f
--- /dev/null
+++ b/common/socket-path.h
@@ -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
index 0000000..ea37a89
--- /dev/null
+++ b/common/test/test-helpers.c
@@ -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
index 0000000..8886acd
--- /dev/null
+++ b/common/test/test-helpers.h
@@ -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
index 0000000..6c618a0
--- /dev/null
+++ b/drm-lease-manager/drm-lease.h
@@ -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
index 0000000..5cfc5de
--- /dev/null
+++ b/drm-lease-manager/lease-manager.c
@@ -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
index 0000000..ed5bcdc
--- /dev/null
+++ b/drm-lease-manager/lease-manager.h
@@ -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
index 0000000..e05e8e4
--- /dev/null
+++ b/drm-lease-manager/lease-server.c
@@ -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
index 0000000..c87b834
--- /dev/null
+++ b/drm-lease-manager/lease-server.h
@@ -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
index 0000000..c3a933f
--- /dev/null
+++ b/drm-lease-manager/main.c
@@ -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
index 0000000..1171b02
--- /dev/null
+++ b/drm-lease-manager/meson.build
@@ -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
index 0000000..c741cb3
--- /dev/null
+++ b/drm-lease-manager/test/lease-manager-test.c
@@ -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
index 0000000..f8e000e
--- /dev/null
+++ b/drm-lease-manager/test/lease-server-test.c
@@ -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
index 0000000..7d42bec
--- /dev/null
+++ b/drm-lease-manager/test/meson.build
@@ -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
index 0000000..844599a
--- /dev/null
+++ b/drm-lease-manager/test/test-drm-device.c
@@ -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
index 0000000..1d5b683
--- /dev/null
+++ b/drm-lease-manager/test/test-drm-device.h
@@ -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
index 0000000..260437a
--- /dev/null
+++ b/drm-lease-manager/test/test-socket-client.c
@@ -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
index 0000000..b5bdf6d
--- /dev/null
+++ b/drm-lease-manager/test/test-socket-client.h
@@ -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
index 0000000..411fce7
--- /dev/null
+++ b/examples/dlm-client-test/dlm-client-test.c
@@ -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
index 0000000..aa51c78
--- /dev/null
+++ b/examples/dlm-client-test/meson.build
@@ -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
index 0000000..706c2b8
--- /dev/null
+++ b/examples/meson.build
@@ -0,0 +1 @@
+subdir('dlm-client-test')
diff --git a/libdlmclient/dlmclient.c b/libdlmclient/dlmclient.c
new file mode 100644
index 0000000..32493d3
--- /dev/null
+++ b/libdlmclient/dlmclient.c
@@ -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
index 0000000..908cd6b
--- /dev/null
+++ b/libdlmclient/dlmclient.h
@@ -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
index 0000000..dce4244
--- /dev/null
+++ b/libdlmclient/docs/Doxyfile.in
@@ -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
index 0000000..563e5fb
--- /dev/null
+++ b/libdlmclient/docs/meson.build
@@ -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
index 0000000..d63a842
--- /dev/null
+++ b/libdlmclient/meson.build
@@ -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
index 0000000..5b04c09
--- /dev/null
+++ b/libdlmclient/test/libdlmclient-test.c
@@ -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
index 0000000..35e8575
--- /dev/null
+++ b/libdlmclient/test/meson.build
@@ -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
index 0000000..281aaf7
--- /dev/null
+++ b/libdlmclient/test/test-socket-server.c
@@ -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
index 0000000..ebdbd2d
--- /dev/null
+++ b/libdlmclient/test/test-socket-server.h
@@ -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
index 0000000..2f6cd15
--- /dev/null
+++ b/meson.build
@@ -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
index 0000000..ee1f8dc
--- /dev/null
+++ b/meson_options.txt
@@ -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
index 0000000..047a44f
--- /dev/null
+++ b/subprojects/fff.wrap
@@ -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
index 0000000..c7ce1f2
--- /dev/null
+++ b/subprojects/packagefiles/fff-1.0/meson.build
@@ -0,0 +1,8 @@
+project('fff', 'c',
+ version: '1.0',
+ license: 'MIT'
+)
+
+fff_dep = declare_dependency(
+ include_directories: include_directories('.')
+)