diff --git a/.hgignore b/.hgignore index 7ec60a4..454e24e 100644 --- a/.hgignore +++ b/.hgignore @@ -1,3 +1,4 @@ -.*\.sw[p-z]$ +.*\.sw[a-z]$ .*\.py[co]$ ^build +^pcaps diff --git a/.hgtags b/.hgtags new file mode 100644 index 0000000..c04cd3c --- /dev/null +++ b/.hgtags @@ -0,0 +1,10 @@ +38014f13f04d5871b70278518221a937867fd8b6 0.9.0 +21d438d34e21479a652ba6aa1ec14646385cae5a 0.9.1 +3a4befe8819ea7a81f89a6ed550cc88803108f6a 0.9.2 +8a41f4cfee7e6ea6d7e161f4e96cfc45bb485cc4 0.9.3 +4ef909311670c1ab1543793afa327749150bf1e2 0.9.4 +31b8ef65d3d239d7e94417de50b4b7e1c6c0a624 1.0.0 +1f7c28381493a9e0a24bf5b918d3772241d15587 1.0.1 +39e9e2070a25b17fac56ebc61338024b20538cc4 1.1.0 +c3284cd6d1af24bff19327344e89032967ce4ad2 1.1.1 +a4a1ac21d226ceb82e1a337123e1d5939ed116f0 1.1.2 diff --git a/.ycm_extra_conf.py b/.ycm_extra_conf.py index 7ae1491..d9077a3 100644 --- a/.ycm_extra_conf.py +++ b/.ycm_extra_conf.py @@ -67,10 +67,9 @@ # Set this to the absolute path to the folder (NOT the file!) containing the # compile_commands.json file to use that instead of 'flags'. See here for # more details: http://clang.llvm.org/docs/JSONCompilationDatabase.html -# -# Most projects will NOT need to set this to anything; you can just change the -# 'flags' list of compilation flags. -compilation_database_folder = '' +compilation_database_folder = os.getenv( + "MY_BUILD", os.path.join(os.getcwd(), "build") +) if os.path.exists( compilation_database_folder ): database = ycm_core.CompilationDatabase( compilation_database_folder ) diff --git a/CMakeLists.txt b/CMakeLists.txt index cbaaa3e..6d5a176 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,50 +1,122 @@ -CMAKE_MINIMUM_REQUIRED(VERSION 2.8) +# it probably works with older versions but this the oldest tested one: +CMAKE_MINIMUM_REQUIRED(VERSION 2.8.9) -PROJECT(LIFXD C) +PROJECT(LIGHTSD C) -SET(CPACK_PACKAGE_VERSION_MAJOR "0") -SET(CPACK_PACKAGE_VERSION_MINOR "0") -SET(CPACK_PACKAGE_VERSION_PATCH "1") -SET(LIFXD_VERSION "${CPACK_PACKAGE_VERSION_MAJOR}.${CPACK_PACKAGE_VERSION_MINOR}.${CPACK_PACKAGE_VERSION_PATCH}") +SET(CPACK_PACKAGE_VERSION_MAJOR "1") +SET(CPACK_PACKAGE_VERSION_MINOR "1") +SET(CPACK_PACKAGE_VERSION_PATCH "2") +SET(LIGHTSD_VERSION "${CPACK_PACKAGE_VERSION_MAJOR}.${CPACK_PACKAGE_VERSION_MINOR}.${CPACK_PACKAGE_VERSION_PATCH}") +MESSAGE(STATUS "lightsd version: ${LIGHTSD_VERSION}") MESSAGE(STATUS "CMake version: ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION}.${CMAKE_PATCH_VERSION}") -MESSAGE(STATUS "lifxd version: ${LIFXD_VERSION}") MESSAGE(STATUS "Build type: ${CMAKE_BUILD_TYPE}") -MESSAGE(STATUS "System: ${CMAKE_SYSTEM_NAME} ${CMAKE_SYSTEM_VERSION}") +MESSAGE(STATUS "Target system: ${CMAKE_SYSTEM_NAME} ${CMAKE_SYSTEM_VERSION} ${CMAKE_SYSTEM_PROCESSOR}") MESSAGE(STATUS "Install prefix: ${CMAKE_INSTALL_PREFIX}") -MESSAGE(STATUS "Source directory: ${LIFXD_SOURCE_DIR}") +MESSAGE(STATUS "Source directory: ${LIGHTSD_SOURCE_DIR}") -SET(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${LIFXD_SOURCE_DIR}/CMakeScripts) +SET(CMAKE_MODULE_PATH "${CMAKE_MODULE_PATH}" "${LIGHTSD_SOURCE_DIR}/CMakeScripts") + +ENABLE_TESTING() ### Platform checks ############################################################ # TODO: we need at least 2.0.19-stable because of the logging defines FIND_PACKAGE(Event2 REQUIRED COMPONENTS core) FIND_PACKAGE(Endian REQUIRED) +FIND_PACKAGE(Sphinx) + +INCLUDE(CheckFunctionExists) +INCLUDE(CheckVariableExists) +INCLUDE(TestBigEndian) + +INCLUDE(CompatReallocArray) +INCLUDE(CompatSetProctitle) INCLUDE(CompatTimeMonotonic) -INCLUDE(CheckTypeSize) -CHECK_TYPE_SIZE(suseconds_t SUSECONDS_T_SIZE) -ADD_DEFINITIONS("-DLIFXD_SUSECONDS_T_SIZE=${SUSECONDS_T_SIZE}") +TEST_BIG_ENDIAN(BIG_ENDIAN_SYSTEM) ### Global definitions ######################################################### -SET(CMAKE_C_FLAGS "-pipe -Wextra -Wall -Wstrict-prototypes -std=c99") +INCLUDE(AddAllSubdirectories) +INCLUDE(AddTestFromSources) + +SET(CMAKE_EXPORT_COMPILE_COMMANDS TRUE) + +SET(CMAKE_C_FLAGS "-pipe ${CMAKE_C_FLAGS}") +STRING(STRIP "${CMAKE_C_FLAGS}" CMAKE_C_FLAGS) +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${CMAKE_C_FLAGS_${CMAKE_BUILD_TYPE}}") +STRING(STRIP "${CMAKE_C_FLAGS}" CMAKE_C_FLAGS) +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wextra -Wall -Wstrict-prototypes -std=c99") +SET(CMAKE_C_FLAGS_${CMAKE_BUILD_TYPE} "") -# Only relevant for the GNU libc: ADD_DEFINITIONS( + # Only relevant for the GNU libc: "-D_POSIX_C_SOURCE=200809L" "-D_BSD_SOURCE=1" "-D_DEFAULT_SOURCE=1" + + "-D_DARWIN_C_SOURCE=1" + + "-DLGTD_BIG_ENDIAN_SYSTEM=${BIG_ENDIAN_SYSTEM}" + "-DLGTD_SIZEOF_VOID_P=${CMAKE_SIZEOF_VOID_P}" + + "-DLGTD_HAVE_SETPROCTITLE=${HAVE_SETPROCTITLE}" + "-DLGTD_HAVE_REALLOCARRAY=${HAVE_REALLOCARRAY}" + + "-DJSMN_STRICT=1" + "-DJSMN_PARENT_LINKS=1" ) IF (CMAKE_BUILD_TYPE MATCHES "DEBUG") ADD_DEFINITIONS("-DQUEUE_MACRO_DEBUG=1") IF (CMAKE_COMPILER_IS_GNUCC) - ADD_DEFINITIONS("-g3" "-ggdb") + SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -g3 -ggdb3") ENDIF () ENDIF () -INCLUDE_DIRECTORIES(${LIFXD_SOURCE_DIR}/compat/generic ${LIFXD_BINARY_DIR}/compat) +IF (NOT LGTD_RUNTIME_DIRECTORY) + SET(LGTD_RUNTIME_DIRECTORY "${LIGHTSD_BINARY_DIR}") +ENDIF () +MESSAGE(STATUS "lightsd runtime directory: ${LGTD_RUNTIME_DIRECTORY}") +INCLUDE_DIRECTORIES( + ${EVENT2_INCLUDE_DIR} + ${LIGHTSD_BINARY_DIR}/compat + ${LIGHTSD_BINARY_DIR}/compat/generic +) + +ADD_SUBDIRECTORY(compat) ADD_SUBDIRECTORY(core) +ADD_SUBDIRECTORY(lifx) + +# 2.8.11 is the first version with TARGET_INCLUDE_DIRECTORIES: +IF (CMAKE_VERSION VERSION_GREATER 2.8.10) + CONFIGURE_FILE( + CTestCustom.cmake.in "${LIGHTSD_BINARY_DIR}/CTestCustom.cmake" @ONLY + ) + ADD_SUBDIRECTORY(tests) +ELSE () + MESSAGE( + STATUS + "The tests suite requires CMake >= 2.8.11 " + "but you have ${CMAKE_VERSION}, disabling it" + ) +ENDIF () + +IF (SPHINX_FOUND) + ADD_SUBDIRECTORY(docs) +ENDIF () + +INSTALL( + FILES COPYING README.rst docs/protocol.rst + DESTINATION share/doc/lightsd +) +INSTALL( + DIRECTORY examples + DESTINATION share/lightsd + USE_SOURCE_PERMISSIONS + REGEX ".*\\.sw.$" EXCLUDE +) +INSTALL(FILES share/lightsc.sh DESTINATION share/lightsd) +INSTALL(FILES dist/lightsd.service DESTINATION lib/systemd/system) diff --git a/CMakeScripts/AddAllSubdirectories.cmake b/CMakeScripts/AddAllSubdirectories.cmake new file mode 100644 index 0000000..8ba2f50 --- /dev/null +++ b/CMakeScripts/AddAllSubdirectories.cmake @@ -0,0 +1,8 @@ +FUNCTION(ADD_ALL_SUBDIRECTORIES) + FILE(GLOB SUBDIRECTORIES "*") + FOREACH (ENTRY ${SUBDIRECTORIES}) + IF (IS_DIRECTORY ${ENTRY} AND EXISTS "${ENTRY}/CMakeLists.txt") + ADD_SUBDIRECTORY(${ENTRY}) + ENDIF () + ENDFOREACH () +ENDFUNCTION() diff --git a/CMakeScripts/AddTestFromSources.cmake b/CMakeScripts/AddTestFromSources.cmake new file mode 100644 index 0000000..ba2bee3 --- /dev/null +++ b/CMakeScripts/AddTestFromSources.cmake @@ -0,0 +1,11 @@ +FUNCTION(ADD_TEST_FROM_C_SOURCES TEST_SOURCE) + STRING(LENGTH ${TEST_SOURCE} TEST_NAME_LEN) + STRING(LENGTH "test_" PREFIX_LEN) + MATH(EXPR TEST_NAME_LEN "${TEST_NAME_LEN} - 2 - ${PREFIX_LEN}") + STRING(SUBSTRING ${ARGV0} ${PREFIX_LEN} ${TEST_NAME_LEN} TEST_NAME) + ADD_EXECUTABLE(${TEST_NAME} ${TEST_SOURCE}) + IF (ARGN) + TARGET_LINK_LIBRARIES(${TEST_NAME} ${ARGN}) + ENDIF () + ADD_TEST(test_${TEST_NAME} ${TEST_NAME}) +ENDFUNCTION() diff --git a/CMakeScripts/CompatReallocArray.cmake b/CMakeScripts/CompatReallocArray.cmake new file mode 100644 index 0000000..e7368d8 --- /dev/null +++ b/CMakeScripts/CompatReallocArray.cmake @@ -0,0 +1,27 @@ +IF (DEFINED HAVE_REALLOCARRAY) + RETURN() +ENDIF () + +MESSAGE(STATUS "Looking for reallocarray") + +SET(CMAKE_REQUIRED_QUIET TRUE) +CHECK_FUNCTION_EXISTS("reallocarray" HAVE_REALLOCARRAY) +UNSET(CMAKE_REQUIRED_QUIET) +IF (HAVE_REALLOCARRAY) + MESSAGE(STATUS "Looking for reallocarray - found") + SET( + HAVE_REALLOCARRAY 1 + CACHE INTERNAL + "reallocarray found on the system" + ) +ELSE () + MESSAGE( + STATUS + "Looking for reallocarray - not found, using built-in compatibilty file" + ) + SET( + HAVE_REALLOCARRAY 0 + CACHE INTERNAL + "reallocarray not found, using internal implementation" + ) +ENDIF () diff --git a/CMakeScripts/CompatSetProctitle.cmake b/CMakeScripts/CompatSetProctitle.cmake new file mode 100644 index 0000000..b079b6b --- /dev/null +++ b/CMakeScripts/CompatSetProctitle.cmake @@ -0,0 +1,59 @@ +IF (DEFINED HAVE_SETPROCTITLE) + IF (HAVE_SETPROCTITLE EQUAL 0) + IF (HAVE_CLEARENV) + ADD_DEFINITIONS("-DHAVE_CLEARENV=1") + ENDIF () + IF (HAVE_GETEXECNAME) + ADD_DEFINITIONS("-DHAVE_GETEXECNAME=1") + ENDIF () + IF (HAVE___PROGNAME) + ADD_DEFINITIONS("-DHAVE___PROGNAME=1") + ENDIF () + IF (HAVE_HAVE_PROGRAM_INVOCATION_SHORT_NAME) + ADD_DEFINITIONS("-DHAVE_HAVE_PROGRAM_INVOCATION_SHORT_NAME=1") + ENDIF () + ENDIF () + RETURN() +ENDIF () + +MESSAGE(STATUS "Looking for setproctitle") + +SET(CMAKE_REQUIRED_QUIET TRUE) +CHECK_FUNCTION_EXISTS("setproctitle" HAVE_SETPROCTITLE) +UNSET(CMAKE_REQUIRED_QUIET) +IF (HAVE_SETPROCTITLE) + MESSAGE( + STATUS + "Looking for setproctitle - found" + ) + SET(HAVE_SETPROCTITLE 1 CACHE INTERNAL "setproctitle found on the system") +ELSE () + MESSAGE( + STATUS + "Looking for setproctitle - not found, using built-in compatibilty file" + ) + SET( + HAVE_SETPROCTITLE 0 + CACHE INTERNAL + "setproctitle not found, using internal implementation" + ) + + CHECK_FUNCTION_EXISTS("clearenv" HAVE_CLEARENV) + CHECK_FUNCTION_EXISTS("getexecname" HAVE_GETEXECNAME) + CHECK_VARIABLE_EXISTS("__progname" HAVE___PROGNAME) + CHECK_VARIABLE_EXISTS( + "program_invocation_short_name" HAVE_PROGRAM_INVOCATION_SHORT_NAME + ) + IF (HAVE_CLEARENV) + ADD_DEFINITIONS("-DHAVE_CLEARENV=1") + ENDIF () + IF (HAVE_GETEXECNAME) + ADD_DEFINITIONS("-DHAVE_GETEXECNAME=1") + ENDIF () + IF (HAVE___PROGNAME) + ADD_DEFINITIONS("-DHAVE___PROGNAME=1") + ENDIF () + IF (HAVE_HAVE_PROGRAM_INVOCATION_SHORT_NAME) + ADD_DEFINITIONS("-DHAVE_HAVE_PROGRAM_INVOCATION_SHORT_NAME=1") + ENDIF () +ENDIF () diff --git a/CMakeScripts/CompatTimeMonotonic.cmake b/CMakeScripts/CompatTimeMonotonic.cmake index ad1e80c..0c099c0 100644 --- a/CMakeScripts/CompatTimeMonotonic.cmake +++ b/CMakeScripts/CompatTimeMonotonic.cmake @@ -1,31 +1,48 @@ -INCLUDE(CheckFunctionExists) - -IF (NOT TIME_MONOTONIC_IMPL) - SET(COMPAT_TIME_MONOTONIC_IMPL "${LIFXD_SOURCE_DIR}/compat/${CMAKE_SYSTEM_NAME}/time_monotonic.c") - SET(COMPAT_TIME_MONOTONIC_H "${LIFXD_SOURCE_DIR}/compat/${CMAKE_SYSTEM_NAME}/time_monotonic.h") - SET(GENERIC_TIME_MONOTONIC_IMPL "${LIFXD_SOURCE_DIR}/compat/generic/time_monotonic.c") - SET(GENERIC_TIME_MONOTONIC_H "${LIFXD_SOURCE_DIR}/compat/generic/time_monotonic.h") +IF (NOT TIME_MONOTONIC_LIBRARY) + SET(COMPAT_TIME_MONOTONIC_IMPL "${LIGHTSD_SOURCE_DIR}/compat/${CMAKE_SYSTEM_NAME}/time_monotonic.c") + SET(COMPAT_TIME_MONOTONIC_H "${LIGHTSD_SOURCE_DIR}/compat/${CMAKE_SYSTEM_NAME}/time_monotonic.h") + SET(GENERIC_TIME_MONOTONIC_IMPL "${LIGHTSD_SOURCE_DIR}/compat/generic/time_monotonic.c") + SET(GENERIC_TIME_MONOTONIC_H "${LIGHTSD_SOURCE_DIR}/compat/generic/time_monotonic.h") + SET(TIME_MONOTONIC_LIBRARY time_monotonic CACHE INTERNAL "lgtd_time_monotonic implementation") SET(CMAKE_REQUIRED_QUIET TRUE) MESSAGE(STATUS "Looking for clock_gettime") CHECK_FUNCTION_EXISTS("clock_gettime" HAVE_CLOCK_GETTIME) + IF (NOT HAVE_CLOCK_GETTIME) + # glibc < 2.17: + MESSAGE(STATUS "Looking for clock_gettime again in librt") + UNSET(HAVE_CLOCK_GETTIME CACHE) + SET(TIME_MONOTONIC_LIBRARY_DEP rt CACHE INTERNAL "dependency for lgtd_time_monotonic") + SET(CMAKE_REQUIRED_LIBRARIES ${TIME_MONOTONIC_LIBRARY_DEP}) + CHECK_FUNCTION_EXISTS("clock_gettime" HAVE_CLOCK_GETTIME) + UNSET(CMAKE_REQUIRED_LIBRARIES) + IF (NOT HAVE_CLOCK_GETTIME) + UNSET(TIME_MONOTONIC_LIBRARY_DEP CACHE) + ENDIF () + ENDIF () UNSET(CMAKE_REQUIRED_QUIET) IF (HAVE_CLOCK_GETTIME) MESSAGE(STATUS "Looking for clock_gettime - found") - FILE(COPY "${GENERIC_TIME_MONOTONIC_H}" DESTINATION "${LIFXD_BINARY_DIR}/compat/") + FILE(COPY "${GENERIC_TIME_MONOTONIC_H}" DESTINATION "${LIGHTSD_BINARY_DIR}/core/") SET( - TIME_MONOTONIC_IMPL "${GENERIC_TIME_MONOTONIC_IMPL}" - CACHE INTERNAL "lifxd_time_monotonic (POSIX generic implementation)" + TIME_MONOTONIC_IMPL ${GENERIC_TIME_MONOTONIC_IMPL} + CACHE INTERNAL "lgtd_time_monotonic (POSIX generic source)" ) ELSEIF (EXISTS "${COMPAT_TIME_MONOTONIC_IMPL}") MESSAGE(STATUS "Looking for clock_gettime - not found, using built-in compatibilty file") - FILE(COPY "${COMPAT_TIME_MONOTONIC_H}" DESTINATION "${LIFXD_BINARY_DIR}/compat/") + FILE(COPY "${COMPAT_TIME_MONOTONIC_H}" DESTINATION "${LIGHTSD_BINARY_DIR}/core/") SET( TIME_MONOTONIC_IMPL "${COMPAT_TIME_MONOTONIC_IMPL}" - CACHE INTERNAL "lifxd_time_monotonic (${CMAKE_SYSTEM_NAME} specific implementation)" + CACHE INTERNAL "lgtd_time_monotonic (${CMAKE_SYSTEM_NAME} specific source)" ) ELSE () MESSAGE(SEND_ERROR "Looking for clock_gettime - not found") ENDIF () ENDIF () + +ADD_LIBRARY(${TIME_MONOTONIC_LIBRARY} STATIC "${TIME_MONOTONIC_IMPL}") + +IF (TIME_MONOTONIC_LIBRARY_DEP) + TARGET_LINK_LIBRARIES(${TIME_MONOTONIC_LIBRARY} ${TIME_MONOTONIC_LIBRARY_DEP}) +ENDIF () diff --git a/CMakeScripts/FindEndian.cmake b/CMakeScripts/FindEndian.cmake index 45ade06..19998d0 100644 --- a/CMakeScripts/FindEndian.cmake +++ b/CMakeScripts/FindEndian.cmake @@ -1,8 +1,8 @@ INCLUDE(CheckIncludeFile) IF (NOT ENDIAN_H_PATH) - SET(COMPAT_ENDIAN_H "${LIFXD_SOURCE_DIR}/compat/${CMAKE_SYSTEM_NAME}/endian.h") - SET(GENERIC_ENDIAN_H "${LIFXD_SOURCE_DIR}/compat/generic/endian.h") + SET(COMPAT_ENDIAN_H "${LIGHTSD_SOURCE_DIR}/compat/${CMAKE_SYSTEM_NAME}/endian.h") + SET(GENERIC_ENDIAN_H "${LIGHTSD_SOURCE_DIR}/compat/generic/endian.h") SET(CMAKE_REQUIRED_QUIET TRUE) MESSAGE(STATUS "Looking for endian.h") @@ -10,11 +10,11 @@ IF (NOT ENDIAN_H_PATH) UNSET(CMAKE_REQUIRED_QUIET) IF (HAVE_ENDIAN_H) - MESSAGE(STATUS "Looking for endan.h - found") + MESSAGE(STATUS "Looking for endian.h - found") SET(ENDIAN_H_PATH "using native headers" CACHE INTERNAL "endian.h path") ELSEIF (EXISTS "${COMPAT_ENDIAN_H}") MESSAGE(STATUS "Looking for endian.h - not found, using built-in compatibility file") - FILE(COPY "${COMPAT_ENDIAN_H}" DESTINATION "${LIFXD_BINARY_DIR}/compat/") + FILE(COPY "${COMPAT_ENDIAN_H}" DESTINATION "${LIGHTSD_BINARY_DIR}/compat/") SET(ENDIAN_H_PATH "${COMPAT_ENDIAN_H}" CACHE INTERNAL "endian.h path") ELSE () MESSAGE(STATUS "Looking for endian.h - not found") diff --git a/CMakeScripts/FindEvent2.cmake b/CMakeScripts/FindEvent2.cmake index bdccc30..15aa8a3 100644 --- a/CMakeScripts/FindEvent2.cmake +++ b/CMakeScripts/FindEvent2.cmake @@ -1,18 +1,24 @@ +FIND_PATH( + EVENT2_INCLUDE_DIR + event2/event.h + # OpenBSD has libevent1 in /usr/lib, always try /usr/local first: + HINTS /usr/local/ +) + FOREACH (COMPONENT ${Event2_FIND_COMPONENTS}) STRING(TOUPPER ${COMPONENT} UPPER_COMPONENT) - FIND_LIBRARY(EVENT2_${UPPER_COMPONENT}_LIBRARY event_${COMPONENT}) + FIND_LIBRARY( + EVENT2_${UPPER_COMPONENT}_LIBRARY event_${COMPONENT} HINTS /usr/local/ + ) IF (EVENT2_${UPPER_COMPONENT}_LIBRARY) SET(Event2_${COMPONENT}_FOUND TRUE) ENDIF () ENDFOREACH () -FIND_PATH(EVENT2_INCLUDE_DIR event2/event.h) - INCLUDE(FindPackageHandleStandardArgs) FIND_PACKAGE_HANDLE_STANDARD_ARGS( Event2 - FOUND_VAR Event2_FOUND HANDLE_COMPONENTS REQUIRED_VARS EVENT2_CORE_LIBRARY EVENT2_INCLUDE_DIR ) diff --git a/CMakeScripts/FindSphinx.cmake b/CMakeScripts/FindSphinx.cmake new file mode 100644 index 0000000..373b2d1 --- /dev/null +++ b/CMakeScripts/FindSphinx.cmake @@ -0,0 +1,7 @@ +FIND_PROGRAM( + SPHINX_EXECUTABLE + NAMES sphinx-build sphinx-build2 + DOC "Path to sphinx-build executable" +) + +FIND_PACKAGE_HANDLE_STANDARD_ARGS(Sphinx DEFAULT_MSG SPHINX_EXECUTABLE) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..80560ae --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,192 @@ +Contributing to lightsd +======================= + +lightsd is open-source_ software licensed under the GPLv3_ (see `Rationale for +the GPLv3`_). This basically means that you can use lightsd free of charge, as +you see fit, for an unlimited amount of time, no ads, no evaluation period, no +activation key. + +Likewise, you can also download and consult the entire source code of lightsd. +Finally, you are welcome to modify lightsd but the GPLv3 forces you to make +those changes public if you decide to share your modified version of lightsd +with other people or companies. + +This document describes how to share your modifications directly with lightsd's +developers so that they can be incorporated, distributed and maintained with the +rest of the project. + +.. _open-source: http://opensource.org/faq#osd +.. _GPLv3: https://www.gnu.org/licenses/quick-guide-gplv3.html + +Project governance +------------------ + +lightsd is a personal project. Its primary goal is for its author_ to learn and +have fun. At the end of the day, no-one tells me what I should do and how I +should do it. I get to say the last word [#]_. + +I don't have any timeline nor any pressure and I will have no problem saying no +to any feature or contribution that doesn't fit my vision of the project or +doesn't reach the quality bar I set for myself and the project. + +Do not take a "no" personally and hold me accountable for constructive feedback. + +.. _author: mailto:Louis Opter + +.. [#] Ultimately, this is not true: the GPL license has the last word. + +Project goals +------------- + +The current goal is to be the best and most correct implementation of the `LIFX +LAN protocol`_. + +Unlike other projects around LIFX bulbs, lightsd is a background service (daemon +in the Unix terminology). It allows lightsd to act as a proxy for the bulbs and +to report or change the status of the bulbs in near real time. Those two +properties are fundamental to the project: + +- being able to work as a proxy makes lightsd easy to extend to other IoT + protocols. It also allows network isolation; +- being as real time as possible will –I hope– pave the way for new kind of + usages. For example lightsd is currently is used to pair LIFX bulbs with + motion sensors, such system requires low latency [#]_. Integrations with + video games also come to mind and may require low latency [#]_. + +lightsd took the bold choice of being implemented in pure C. C simply is the +most portable language both from a tool-chain perspective but also from a +hardware perspective: anything that can run a very stripped down version of +Linux should be able to run lightsd successfully. Portability is a goal and I +hope that it will also unlock new kind of usage & interactions. A corollary to +portability is that lightsd should have the shortest startup and discovery time +possible: lightsd might not always run as a background service. + +lightsd must work out of the box and be installable by a high school student who +discovered command line interfaces last week. When documenting something assume +readers kinda know how to copy paste commands in a terminal emulator and install +packages on their system but nothing more. Do not assume they know how to +properly administrate their system, lightsd should be an opportunity to learn +that and inspire the user to learn more. Using lightsd has to be a rewarding +experience. + +.. [#] When motion is detected you want to turn the lights immediatly not in 100 + or more milliseconds. +.. [#] This is actually `already happening`_. + +.. _already happening: https://community.lifx.com/t/lightsd-a-daemon-with-a-json-rpc-api-to-control-your-bulbs/446/41 +.. _LIFX LAN protocol: http://lan.developer.lifx.com/ + +Non-goals +--------- + +HTTP is overused, misused and is a non-goal, sorry. + +However, I'm very willing to have a lightsd client that can run inside web +browsers. I'm willing to embed a dead simple HTTP server within lightsd to serve +that web application. Communication between the web application and lightsd +cannot use HTTP, I believe this is possible using some newer Javascript +technologies; worst-case websockets may be an acceptable compromise. + +Reporting bugs +-------------- + +I can be joined via email, via IRC in `#lightsd`_ on Freenode, you can also post +in this topic_ and if you feel comfortable writing bug reports you can directly +open an issue on GitHub_. + +.. _#lightsd: irc://chat.freenode.net/#lightsd +.. _topic: https://community.lifx.com/t/lightsd-a-daemon-with-a-json-rpc-api-to-control-your-bulbs/446/ +.. _Github: https://github.com/lopter/lightsd/issues/new + +Submitting contributions +------------------------ + +Submitting a contribution is pretty much like reporting a bug. At this point, +I'll deal with pretty much any non f'ed-up format. What really matters to me is +the content: make sure your understand how the project is governed and what the +goals are. + +It's probably better to start a discussion with me before implementing +something. Feel free to pick an open bug and work on it, if you're not +comfortable in C I'll guide you through the whole thing. I'm ready to work with +motivated beginners, and whatever the outcome is (i.e: your contribution being +merged or not), I'll make sure it's constructive and that you learn something +useful. + +I'm trying to make this a cool project to learn programming: in the middle you +have lightsd: a traditional UNIX daemon written in C that doesn't compromise on +modernity (modular approach, evented I/O, unit-tested, fast and lightweight, +highly portable, well integrated in modern init systems). On the left you have +those bulbs that I'm sure could run cool home-brewed firmware and use some +reverse-engineering. And on the right you have this JSON-RPC API that can be +used to implement any kind of cool client. + +In other words, by simply using lightsd you are already contributing. For +example a better LIFX firmware would be an awesome contribution. A cool mobile +application would be even better. + +Coding style +------------ + +lightsd is written in C99, C11 doesn't bring much and will make us incompatible +with some platforms (e.g: gcc 4.2 and Microsoft is known to really lag behind on +C standards support). + +lightsd must work on 32/64 bits and little/big endian architectures. + +lightsd is designed to work on machines without an FPU_, do not use floats +unless there is no other choice. + +lightsd is portable and uses CMake_ to introspect the system it's being built +on. Currently supported systems are: Linux, BSD and Darwin (Mac OS X), new +features must work on all three of them. + +New code must be unit-tested, CMake is also used as a test runner. + +lightsd coding style is: + +- overall mostly `K&R`_/1TBS_; +- tabs are 4 spaces; +- C++ style comments; +- 80 columns max (kinda flexible in the headers); +- don't use typedef (rationale: reduces readability, typedefs in C are really + only useful on integers for things like ``pid_t`` where an int isn't the right + semantic or for fixed-width integers); +- no includes in the headers (rationale: avoid fucked-up circular dependencies + scenarios); +- includes order: alphabetically sorted system includes (with ``sys/`` includes + first however), then libraries includes, then local includes; +- when defining a function the return type and the function name must be on two + different lines (rationale: make searching a function definition really easy + with the ``^`` regular expression anchor). + +Overall, just be consistent with the existing coding-style, I'll setup `clang +format`_ or astyle_ when I get a chance, it should make the style a non-issue. + +.. _FPU: https://en.wikipedia.org/wiki/Floating-point_unit +.. _CMake: https://cmake.org/overview/ +.. _K&R: https://en.wikipedia.org/wiki/Indent_style#K.26R_style +.. _1TBS: https://en.wikipedia.org/wiki/Indent_style#Variant:_1TBS +.. _clang format: http://clang.llvm.org/docs/ClangFormat.html +.. _astyle: http://astyle.sourceforge.net/astyle.html + +Rationale for the GPLv3 +----------------------- + +I chose the GPLv3 license for lightsd because it's a personal project and I +don't want lightsd to be used then modified to make a new (or improve an +existing) product behind closed doors. Moreover, I work on lightsd on my free +time and I don't want my time to be used by a company for free. + +That said I'd like to make lightsd easy to integrate (i.e: without any +modification) in a closed source context. For example I'm perfectly fine if +lightsd is bundled with a mobile application as long as it's not modified and +that application stays transparent on its use of lightsd and links to lightsd's +homepage. + +In the unlikely event that lightsd gains significant adoption I want it to be +the reference and unique implementation of its own protocol but also a reference +implementation for the other protocols it implements. I hope that the GPL will +be a good incentive to achieve that goal. + +.. vim: set tw=80 spelllang=en spell: diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..94a0453 --- /dev/null +++ b/COPYING @@ -0,0 +1,621 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS diff --git a/CTestCustom.cmake.in b/CTestCustom.cmake.in new file mode 100644 index 0000000..63871c1 --- /dev/null +++ b/CTestCustom.cmake.in @@ -0,0 +1,3 @@ +IF (NOT @BIG_ENDIAN_SYSTEM@) + SET(CTEST_CUSTOM_TESTS_IGNORE test_wire_proto_float_endian_conversion) +ENDIF () diff --git a/README.rst b/README.rst index 3a57bf6..2ffe040 100644 --- a/README.rst +++ b/README.rst @@ -1,18 +1,18 @@ -lifxd, a LIFX broker -==================== +lightsd, a daemon to control smart bulbs +======================================== -lifxd acts a central point of control for your LIFX_ WiFi bulbs. lifxd should be -a small, simple and fast daemon exposing an easy to use protocol inspired by how -musicpd_ works. +lightsd acts a central point of control for your LIFX_ WiFi bulbs. lightsd +should be a small, simple and fast daemon exposing an easy to use protocol +inspired by how musicpd_ works. -Having to run a daemon to control your LIFX bulbs may seem a little bit -backward but has some advantages: +Having to run a daemon to control your LIFX bulbs may seem a little bit backward +but has some advantages: - no discovery delay ever, you get all the bulbs and their state right away; -- lifxd is always in sync with the bulbs and always know their state; -- lifxd act as an abstraction layer and can expose new discovery mechanism (e.g: - zeroconf) and totally new APIs; -- For those of you with a high paranoia factor, lifxd let you place your bulbs +- lightsd is always in sync with the bulbs and always knows their state; +- lightsd act as an abstraction layer and can expose new discovery mechanisms and + an unified API across different kind of smart bulbs; +- For those of you with a high paranoia factor, lightsd let you place your bulbs in a totally separate and closed network. .. _LIFX: http://lifx.co/ @@ -21,26 +21,80 @@ backward but has some advantages: Current features ---------------- -lifxd doesn't do much yet, it just discovers your bulbs and stay in sync with -them. +lightsd discovers your LIFX bulbs, stays in sync with them and support the +following commands through a JSON-RPC_ interface: -Developers ----------- +- power_off (with auto-retry); +- power_on (with auto-retry); +- power_toggle (power on if off and vice-versa, with auto-retry); +- set_light_from_hsbk; +- set_waveform (change the light according to a function like SAW or SINE); +- get_light_state; +- set_label; +- tag/untag (group/ungroup bulbs together). -The project is far from being usable right now, but I'll be happy to hear your -feedback and share ideas. +The JSON-RPC interface works on top of TCP/IPv4/v6, Unix sockets, or over a +command pipe (named pipe, see `mkfifo(1)`_). -Be aware that some parts of the code aren't really clean yet: I'm more focused -on getting things working and good abstractions. Testing is definitely missing. +lightsd can target single or multiple bulbs at once: + +- by device address; +- by device label; +- by tag; +- broadcast; +- composite (list of targets); + +lightsd works and is developed against a variety of LIFX firmwares from the +oldest ones to the newest ones. + +.. _JSON-RPC: http://www.jsonrpc.org/specification +.. _mkfifo(1): http://www.openbsd.org/cgi-bin/man.cgi?query=mkfifo + +Documentation +------------- + +lightsd is packaged for Mac OS X, Arch Linux, Debian based systems and OpenWRT. +Check out http://lightsd.readthedocs.org/en/latest/ for installation +instructions and a walk-through some interactive examples. Requirements ------------ -lifxd aims to be highly portable on any POSIX system (win32 support should be -quite easy, but isn't really the goal) and on any kind of hardware including -embedded devices. Hence why lifxd is written in C with reasonable dependencies: +lightsd aims to be highly portable on any slightly POSIX system and on any kind +of hardware including embedded devices. Hence why lightsd is written in C with +reasonable dependencies: + +- libevent ≥ 2.0.19 (released in May 2012); +- CMake ≥ 2.8.9 (released in August 2012): only if you want to build lightsd + from its sources. + +lightsd is actively developed and tested from Arch Linux, Debian, Mac OS X, +OpenWRT and OpenBSD; both for 32/64 bits and little/big endian architectures. + +Native Windows support has been kept in mind and will be addressed, but isn't +really the focus. Windows build will be targeted at `Windows 10 IoT Core`_ which +is the only one that you don't have to pay for [#]_. + +.. [#] I do need a version of ffu2img_ that actually works, because otherwise you + need Windows to install Windows 10 IoT Core; be my guest. + +.. _Windows 10 IoT Core: https://dev.windows.com/en-us/iot +.. _ffu2img: https://msdn.microsoft.com/en-us/library/windows/hardware/dn757539.aspx + +Contact +------- + +Feel free to reach out via email or irc (`#lightsd`_ on Freenode, insist if I +don't reply). As the project name implies, I'm fairly interested in other smart +bulbs. + +Check out the `contribution guide`_ for the vision behind the project and how to +contribute. + +Join the conversation on the `LIFX forum`_. -- CMake ≥ 2.8; -- libevent ≥ 2.0.19. +.. _#lightsd: irc://chat.freenode.net/#lightsd +.. _contribution guide: https://github.com/lopter/lightsd/blob/master/CONTRIBUTING.rst +.. _LIFX forum: https://community.lifx.com/t/lightsd-a-daemon-with-a-json-rpc-api-to-control-your-bulbs/446 .. vim: set tw=80 spelllang=en spell: diff --git a/compat/CMakeLists.txt b/compat/CMakeLists.txt new file mode 100644 index 0000000..7171054 --- /dev/null +++ b/compat/CMakeLists.txt @@ -0,0 +1 @@ +ADD_SUBDIRECTORY(generic) diff --git a/compat/Darwin/time_monotonic.c b/compat/Darwin/time_monotonic.c index 80457b9..e8b8045 100644 --- a/compat/Darwin/time_monotonic.c +++ b/compat/Darwin/time_monotonic.c @@ -1,31 +1,19 @@ // Copyright (c) 2015, Louis Opter -// All rights reserved. // -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: +// This file is part of lighstd. // -// 1. Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. // -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. // -// 3. Neither the name of the copyright holder nor the names of its contributors -// may be used to endorse or promote products derived from this software -// without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . // This is pretty much Listing 2 from: // @@ -40,7 +28,7 @@ enum { MSECS_IN_NSEC = 1000000 }; time_t -lifxd_time_monotonic_msecs(void) +lgtd_time_monotonic_msecs(void) { static mach_timebase_info_data_t timebase = { 0, 0 }; if (timebase.denom == 0) { diff --git a/compat/Darwin/time_monotonic.h b/compat/Darwin/time_monotonic.h index f6e5aef..0e249cd 100644 --- a/compat/Darwin/time_monotonic.h +++ b/compat/Darwin/time_monotonic.h @@ -1,34 +1,22 @@ // Copyright (c) 2015, Louis Opter -// All rights reserved. // -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: +// This file is part of lighstd. // -// 1. Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. // -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. // -// 3. Neither the name of the copyright holder nor the names of its contributors -// may be used to endorse or promote products derived from this software -// without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . #pragma once -typedef time_t lifxd_time_mono_t; +typedef time_t lgtd_time_mono_t; -lifxd_time_mono_t lifxd_time_monotonic_msecs(void); +lgtd_time_mono_t lgtd_time_monotonic_msecs(void); diff --git a/compat/generic/CMakeLists.txt b/compat/generic/CMakeLists.txt new file mode 100644 index 0000000..76877cb --- /dev/null +++ b/compat/generic/CMakeLists.txt @@ -0,0 +1 @@ +FILE(COPY sys DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/compat/generic/time_monotonic.c b/compat/generic/time_monotonic.c index d4e0483..7bbf789 100644 --- a/compat/generic/time_monotonic.c +++ b/compat/generic/time_monotonic.c @@ -1,38 +1,26 @@ // Copyright (c) 2015, Louis Opter -// All rights reserved. // -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: +// This file is part of lighstd. // -// 1. Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. // -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. // -// 3. Neither the name of the copyright holder nor the names of its contributors -// may be used to endorse or promote products derived from this software -// without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . #include #include "time_monotonic.h" time_t -lifxd_time_monotonic_msecs(void) +lgtd_time_monotonic_msecs(void) { struct timespec tp; clock_gettime(CLOCK_MONOTONIC, &tp); diff --git a/compat/generic/time_monotonic.h b/compat/generic/time_monotonic.h index f6e5aef..0e249cd 100644 --- a/compat/generic/time_monotonic.h +++ b/compat/generic/time_monotonic.h @@ -1,34 +1,22 @@ // Copyright (c) 2015, Louis Opter -// All rights reserved. // -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: +// This file is part of lighstd. // -// 1. Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. // -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. // -// 3. Neither the name of the copyright holder nor the names of its contributors -// may be used to endorse or promote products derived from this software -// without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . #pragma once -typedef time_t lifxd_time_mono_t; +typedef time_t lgtd_time_mono_t; -lifxd_time_mono_t lifxd_time_monotonic_msecs(void); +lgtd_time_mono_t lgtd_time_monotonic_msecs(void); diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index fde6857..fc4b0fb 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -1,18 +1,36 @@ -INCLUDE_DIRECTORIES(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}) +# As you'll find out the code in this directory isn't really generic at all. +# I only have lifx bulbs right now and I don't want to do any premature work. -CONFIGURE_FILE(version.h.in ${CMAKE_CURRENT_BINARY_DIR}/version.h) +INCLUDE_DIRECTORIES( + ${CMAKE_CURRENT_SOURCE_DIR}/../ + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR}/../ + ${CMAKE_CURRENT_BINARY_DIR} +) + +CONFIGURE_FILE(version.h.in "${CMAKE_CURRENT_BINARY_DIR}/version.h") ADD_EXECUTABLE( - lifxd - broadcast.c - bulb.c + lightsd client.c - gateway.c - lifxd.c + console.c + daemon.c + jsmn.c + jsonrpc.c + listen.c + lightsd.c log.c - ${TIME_MONOTONIC_IMPL} + pipe.c + proto.c + router.c + setproctitle.c + stats.c timer.c - wire_proto.c + utils.c +) + +TARGET_LINK_LIBRARIES( + lightsd lifx ${EVENT2_CORE_LIBRARY} ${TIME_MONOTONIC_LIBRARY} ) -TARGET_LINK_LIBRARIES(lifxd ${EVENT2_CORE_LIBRARY}) +INSTALL(TARGETS lightsd RUNTIME DESTINATION bin) diff --git a/core/broadcast.c b/core/broadcast.c deleted file mode 100644 index b1736d3..0000000 --- a/core/broadcast.c +++ /dev/null @@ -1,340 +0,0 @@ -// Copyright (c) 2014, Louis Opter -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// 3. Neither the name of the copyright holder nor the names of its contributors -// may be used to endorse or promote products derived from this software -// without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include "wire_proto.h" -#include "time_monotonic.h" -#include "bulb.h" -#include "gateway.h" -#include "broadcast.h" -#include "lifxd.h" - -static struct { - evutil_socket_t socket; - struct event *read_ev; - struct event *write_ev; -} lifxd_broadcast_endpoint = { - .socket = -1, - .read_ev = NULL, - .write_ev = NULL, -}; - -static bool -lifxd_broadcast_handle_read(void) -{ - assert(lifxd_broadcast_endpoint.socket != -1); - - while (true) { - struct sockaddr_storage peer; - // if we get back from recvfrom with a sockaddr_in the end of the struct - // will not be initialized and we will be comparing unintialized stuff - // in lifxd_gateway_get: - memset(&peer, 0, sizeof(peer)); - ev_socklen_t addrlen = sizeof(peer); - union { - char buf[LIFXD_MAX_PACKET_SIZE]; - struct lifxd_packet_header hdr; - } read; - int nbytes = recvfrom( - lifxd_broadcast_endpoint.socket, - read.buf, - sizeof(read.buf), - 0, - (struct sockaddr *)&peer, - &addrlen - ); - if (nbytes == -1) { - int error = EVUTIL_SOCKET_ERROR(); - if (error == EINTR) { - continue; - } - if (error == EAGAIN) { - return true; - } - lifxd_warn("can't receive broadcast packet"); - return false; - } - - lifxd_time_mono_t received_at = lifxd_time_monotonic_msecs(); - char peer_addr[INET6_ADDRSTRLEN]; - lifxd_sockaddrtoa(&peer, peer_addr, sizeof(peer_addr)); - short peer_port = lifxd_sockaddrport(&peer); - - if (nbytes < LIFXD_PACKET_HEADER_SIZE) { - lifxd_warnx( - "broadcast packet too short from [%s]:%hu", peer_addr, peer_port - ); - return false; - } - - lifxd_wire_decode_header(&read.hdr); - if (read.hdr.size != nbytes) { - lifxd_warnx( - "incomplete broadcast packet from [%s]:%hu", - peer_addr, peer_port - ); - return false; - } - if (read.hdr.protocol.version != LIFXD_LIFX_PROTOCOL_V1) { - lifxd_warnx( - "unsupported protocol %d from [%s]:%hu", - read.hdr.protocol.version, peer_addr, peer_port - ); - } - if (read.hdr.packet_type == LIFXD_GET_PAN_GATEWAY) { - continue; - } - - const struct lifxd_packet_infos *pkt_infos = - lifxd_wire_get_packet_infos(read.hdr.packet_type); - if (!pkt_infos) { - lifxd_warnx( - "received unknown packet %#x from [%s]:%hu", - read.hdr.packet_type, peer_addr, peer_port - ) - continue; - } - if (read.hdr.protocol.tagged || !read.hdr.protocol.addressable) { - lifxd_warnx( - "received non-addressable packet %s from [%s]:%hu", - pkt_infos->name, peer_addr, peer_port - ); - continue; - } - struct lifxd_gateway *gw = lifxd_gateway_get(&peer); - if (!gw && read.hdr.packet_type == LIFXD_PAN_GATEWAY) { - gw = lifxd_gateway_open(&peer, addrlen, read.hdr.site, received_at); - if (!gw) { - lifxd_err(1, "can't allocate gateway"); - } - } - if (gw) { - void *pkt = &read.buf[LIFXD_PACKET_HEADER_SIZE]; - gw->last_pkt_at = received_at; - pkt_infos->decode(pkt); - pkt_infos->handle(gw, &read.hdr, pkt); - } else { - lifxd_warnx( - "got packet from unknown gateway [%s]:%hu", peer_addr, peer_port - ); - } - } -} - -static bool -lifxd_broadcast_handle_write(void) -{ - assert(lifxd_broadcast_endpoint.socket != -1); - - struct sockaddr_in lifx_addr = { - .sin_family = AF_INET, - .sin_addr = { INADDR_BROADCAST }, - .sin_port = htons(lifxd_opts.master_port) - }; - struct lifxd_packet_header get_pan_gateway; - lifxd_wire_setup_header( - &get_pan_gateway, - LIFXD_TARGET_ALL_DEVICES, - LIFXD_UNSPEC_TARGET, - NULL, - LIFXD_GET_PAN_GATEWAY - ); - - int nbytes; -retry: - nbytes = sendto( - lifxd_broadcast_endpoint.socket, - (void *)&get_pan_gateway, - sizeof(get_pan_gateway), - 0, - (const struct sockaddr *)&lifx_addr, - sizeof(lifx_addr) - ); - if (nbytes == sizeof(get_pan_gateway)) { - if (event_del(lifxd_broadcast_endpoint.write_ev)) { - lifxd_err(1, "can't setup events"); - } - return true; - } - if (nbytes == -1) { - if (EVUTIL_SOCKET_ERROR() == EINTR) { - goto retry; - } - lifxd_warn("can't broadcast discovery packet"); - } else { - lifxd_warnx("can't broadcast discovery packet"); - } - return false; -} - -static void -lifxd_broadcast_event_callback(evutil_socket_t socket, short events, void *ctx) -{ - (void)socket; - (void)ctx; - - if (events & EV_TIMEOUT) { - // not sure how that could happen but eh. - lifxd_warnx("timeout on the udp broadcast socket"); - goto error_reset; - } - if (events & EV_READ) { - if (!lifxd_broadcast_handle_read()) { - goto error_reset; - } - } - if (events & EV_WRITE) { - if (!lifxd_broadcast_handle_write()) { - goto error_reset; - } - } - - return; - -error_reset: - lifxd_broadcast_close(); - lifxd_broadcast_setup(); - lifxd_broadcast_discovery(); -} - -void -lifxd_broadcast_close(void) -{ - if (lifxd_broadcast_endpoint.read_ev) { - event_del(lifxd_broadcast_endpoint.read_ev); - event_free(lifxd_broadcast_endpoint.read_ev); - lifxd_broadcast_endpoint.read_ev = NULL; - } - if (lifxd_broadcast_endpoint.write_ev) { - event_del(lifxd_broadcast_endpoint.write_ev); - event_free(lifxd_broadcast_endpoint.write_ev); - lifxd_broadcast_endpoint.write_ev = NULL; - } - if (lifxd_broadcast_endpoint.socket != -1) { - evutil_closesocket(lifxd_broadcast_endpoint.socket); - lifxd_broadcast_endpoint.socket = -1; - } -} - -bool -lifxd_broadcast_setup(void) -{ - assert(lifxd_broadcast_endpoint.socket == -1); - assert(lifxd_broadcast_endpoint.read_ev == NULL); - assert(lifxd_broadcast_endpoint.write_ev == NULL); - - lifxd_broadcast_endpoint.socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); - if (lifxd_broadcast_endpoint.socket == -1) { - return false; - } - - int val = 1; - int err = setsockopt( - lifxd_broadcast_endpoint.socket, - SOL_SOCKET, - SO_BROADCAST, - &val, - sizeof(val) - ); - if (err) { - goto error; - } - - if (evutil_make_socket_nonblocking(lifxd_broadcast_endpoint.socket) == -1) { - goto error; - } - - struct sockaddr_in lifx_addr = { - .sin_family = AF_INET, - .sin_addr = { INADDR_ANY }, - .sin_port = htons(lifxd_opts.master_port) - }; - - err = bind( - lifxd_broadcast_endpoint.socket, - (const struct sockaddr *)&lifx_addr, - sizeof(lifx_addr) - ); - if (err) { - goto error; - } - - lifxd_broadcast_endpoint.read_ev = event_new( - lifxd_ev_base, - lifxd_broadcast_endpoint.socket, - EV_READ|EV_PERSIST, - lifxd_broadcast_event_callback, - NULL - ); - lifxd_broadcast_endpoint.write_ev = event_new( - lifxd_ev_base, - lifxd_broadcast_endpoint.socket, - EV_WRITE|EV_PERSIST, - lifxd_broadcast_event_callback, - NULL - ); - if (!lifxd_broadcast_endpoint.read_ev - || !lifxd_broadcast_endpoint.write_ev) { - goto error; - } - - if (!event_add(lifxd_broadcast_endpoint.read_ev, NULL)) { - return true; - } - - int errsave; -error: - errsave = errno; - lifxd_broadcast_close(); - errno = errsave; - return false; -} - -bool -lifxd_broadcast_discovery(void) -{ - assert(lifxd_broadcast_endpoint.write_ev); - return event_add(lifxd_broadcast_endpoint.write_ev, NULL) == 0; -} diff --git a/core/broadcast.h b/core/broadcast.h deleted file mode 100644 index 47c3667..0000000 --- a/core/broadcast.h +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) 2014, Louis Opter -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// 3. Neither the name of the copyright holder nor the names of its contributors -// may be used to endorse or promote products derived from this software -// without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. - -#pragma once - -bool lifxd_broadcast_setup(void); -void lifxd_broadcast_close(void); -bool lifxd_broadcast_discovery(void); diff --git a/core/bulb.c b/core/bulb.c deleted file mode 100644 index 68f4ead..0000000 --- a/core/bulb.c +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright (c) 2014, Louis Opter -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// 3. Neither the name of the copyright holder nor the names of its contributors -// may be used to endorse or promote products derived from this software -// without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "wire_proto.h" -#include "time_monotonic.h" -#include "bulb.h" -#include "gateway.h" -#include "lifxd.h" - -struct lifxd_bulb_map lifxd_bulbs_table = RB_INITIALIZER(&lifxd_bulbs_table); - -struct lifxd_bulb * -lifxd_bulb_get(struct lifxd_gateway *gw, const uint8_t *addr) -{ - assert(gw); - assert(addr); - - struct lifxd_bulb bulb; - memcpy(bulb.addr, addr, sizeof(bulb.addr)); - return RB_FIND(lifxd_bulb_map, &lifxd_bulbs_table, &bulb); -} - -struct lifxd_bulb * -lifxd_bulb_open(struct lifxd_gateway *gw, const uint8_t *addr) -{ - assert(gw); - assert(addr); - - struct lifxd_bulb *bulb = calloc(1, sizeof(*bulb)); - if (!bulb) { - lifxd_warn("can't allocate a new bulb"); - return NULL; - } - - bulb->gw = gw; - memcpy(bulb->addr, addr, sizeof(bulb->addr)); - RB_INSERT(lifxd_bulb_map, &lifxd_bulbs_table, bulb); - - bulb->last_light_state_at = lifxd_time_monotonic_msecs(); - - return bulb; -} - -void -lifxd_bulb_close(struct lifxd_bulb *bulb) -{ - assert(bulb); - assert(bulb->gw); - - RB_REMOVE(lifxd_bulb_map, &lifxd_bulbs_table, bulb); - SLIST_REMOVE(&bulb->gw->bulbs, bulb, lifxd_bulb, link_by_gw); - lifxd_info( - "closed bulb \"%.*s\" on [%s]:%hu", - LIFXD_LABEL_SIZE, - bulb->state.label, - bulb->gw->ip_addr, - bulb->gw->port - ); - free(bulb); -} - -void -lifxd_bulb_set_light_state(struct lifxd_bulb *bulb, - const struct lifxd_light_state *state, - lifxd_time_mono_t received_at) -{ - assert(bulb); - assert(state); - bulb->last_light_state_at = received_at; - memcpy(&bulb->state, state, sizeof(bulb->state)); -} - -void -lifxd_bulb_set_power_state(struct lifxd_bulb *bulb, uint16_t power) -{ - assert(bulb); - bulb->state.power = power; -} diff --git a/core/bulb.h b/core/bulb.h deleted file mode 100644 index f8e45d2..0000000 --- a/core/bulb.h +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) 2014, Louis Opter -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// 3. Neither the name of the copyright holder nor the names of its contributors -// may be used to endorse or promote products derived from this software -// without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. - -#pragma once - -struct lifxd_gateway; - -#pragma pack(push, 1) -struct lifxd_light_state { - uint16_t hue; - uint16_t saturation; - uint16_t brightness; - uint16_t kelvin; - uint16_t dim; - uint16_t power; - char label[LIFXD_LABEL_SIZE]; - uint64_t tags; -}; -#pragma pack(pop) - -struct lifxd_bulb { - RB_ENTRY(lifxd_bulb) link; - SLIST_ENTRY(lifxd_bulb) link_by_gw; - struct lifxd_gateway *gw; - uint8_t addr[LIFXD_ADDR_LENGTH]; - struct lifxd_light_state state; - lifxd_time_mono_t last_light_state_at; -}; -RB_HEAD(lifxd_bulb_map, lifxd_bulb); -SLIST_HEAD(lifxd_bulb_list, lifxd_bulb); - -extern struct lifxd_bulb_map lifxd_bulbs_table; - -static inline int -lifxd_bulb_cmp(const struct lifxd_bulb *a, const struct lifxd_bulb *b) -{ - return memcmp(a->addr, b->addr, sizeof(a->addr)); -} - -RB_GENERATE_STATIC( - lifxd_bulb_map, - lifxd_bulb, - link, - lifxd_bulb_cmp -); - -struct lifxd_bulb *lifxd_bulb_get(struct lifxd_gateway *, const uint8_t *); -struct lifxd_bulb *lifxd_bulb_open(struct lifxd_gateway *, const uint8_t *); -void lifxd_bulb_close(struct lifxd_bulb *); - -void lifxd_bulb_set_light_state(struct lifxd_bulb *, - const struct lifxd_light_state *, - lifxd_time_mono_t); -void lifxd_bulb_set_power_state(struct lifxd_bulb *, uint16_t); diff --git a/core/client.c b/core/client.c index e69de29..38ce7dd 100644 --- a/core/client.c +++ b/core/client.c @@ -0,0 +1,265 @@ +// Copyright (c) 2015, Louis Opter +// +// This file is part of lighstd. +// +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "lifx/wire_proto.h" +#include "jsmn.h" +#include "jsonrpc.h" +#include "client.h" +#include "proto.h" +#include "stats.h" +#include "daemon.h" +#include "lightsd.h" + +struct lgtd_client_list lgtd_clients = LIST_HEAD_INITIALIZER(&lgtd_clients); + +static void +lgtd_client_close(struct lgtd_client *client) +{ + assert(client); + assert(client->io); + + LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(clients, -1); + + LIST_REMOVE(client, link); + if (client->io) { // XXX: see ugly hack in lgtd_jsonrpc_dispatch_one + bufferevent_free(client->io); + } + free(client->addr); + free(client->jsmn_tokens); + free(client); +} + +void +lgtd_client_close_all(void) +{ + struct lgtd_client *client, *next_client; + LIST_FOREACH_SAFE(client, &lgtd_clients, link, next_client) { + lgtd_client_close(client); + } +} + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wswitch" +static void +lgtd_client_read_callback(struct bufferevent *bev, void *ctx) +{ + assert(ctx); + + struct lgtd_client *client = ctx; + + char addr[LGTD_SOCKADDR_STRLEN]; + LGTD_SOCKADDRTOA(client->addr, addr); + + struct evbuffer *input = bufferevent_get_input(bev); + size_t nbytes = evbuffer_get_contiguous_space(input); + // Get the actual pointer to the beginning of the evbuf: + const char *buf = (char *)evbuffer_pullup(input, nbytes); + + jsmntok_t *tokens = NULL; + int ntokens = 0; + do { + jsmn_parser jsmn_ctx; + parse_after_realloc: + jsmn_init(&jsmn_ctx); + jsmnerr_t rv = jsmn_parse(&jsmn_ctx, buf, nbytes, tokens, ntokens); + switch (rv) { + case JSMN_ERROR_NOMEM: + case JSMN_ERROR_INVAL: + lgtd_warnx("client %s: request too big or invalid", addr); + evbuffer_drain(input, nbytes); + break; + case JSMN_ERROR_PART: + case 0: + (void)0; + size_t buflen = evbuffer_get_length(input); + if (buflen > LGTD_CLIENT_MAX_REQUEST_BUF_SIZE) { + lgtd_warnx("client %s: request too big or invalid", addr); + evbuffer_drain(input, buflen); + } else if (nbytes == buflen) { + return; // We pulled up everything already, wait for more data + } + break; + default: + ntokens = rv; + if (tokens) { + client->json = buf; + lgtd_jsonrpc_dispatch_request(client, ntokens); + client->json = NULL; + size_t request_size = tokens[0].end; + tokens = NULL; + evbuffer_drain(input, request_size); + if (request_size < nbytes) { + buf += request_size; + nbytes -= request_size; + // FIXME: instead of calling jsmn_parse again, return the + // number of tokens consumed from jsonrpc and make this + // case a loop. + continue; + } + break; + } else { + client->jsmn_tokens = reallocarray( + client->jsmn_tokens, ntokens, sizeof(*tokens) + ); + tokens = client->jsmn_tokens; + goto parse_after_realloc; + } + } + // pullup and resume parsing: + buf = (char *)evbuffer_pullup(input, -1); + nbytes = evbuffer_get_contiguous_space(input); + } while (nbytes); +} +#pragma GCC diagnostic pop + +static void +lgtd_client_event_callback(struct bufferevent *bev, short events, void *ctx) +{ + (void)bev; + assert(ctx); + + struct lgtd_client *client = ctx; + + if (events & (BEV_EVENT_ERROR|BEV_EVENT_EOF)) { + char addr[LGTD_SOCKADDR_STRLEN]; + lgtd_info("lost connection with client %s", LGTD_SOCKADDRTOA( + client->addr, addr + )); + lgtd_client_close(client); + } +} + +void +lgtd_client_write_string(struct lgtd_client *client, const char *msg) +{ + assert(client); + assert(msg); + + if (client->io) { + bufferevent_write(client->io, msg, strlen(msg)); + } +} + +void +lgtd_client_write_buf(struct lgtd_client *client, const char *buf, int bufsz) +{ + assert(client); + assert(buf); + assert(bufsz >= 0); + + if (bufsz > 0 && client->io) { + bufferevent_write(client->io, buf, bufsz); + } +} + +void +lgtd_client_send_response(struct lgtd_client *client, const char *msg) +{ + lgtd_jsonrpc_send_response(client, msg); +} + +void +lgtd_client_start_send_response(struct lgtd_client *client) +{ + lgtd_jsonrpc_start_send_response(client); +} + +void +lgtd_client_end_send_response(struct lgtd_client *client) +{ + lgtd_jsonrpc_end_send_response(client); +} + +void +lgtd_client_send_error(struct lgtd_client *client, + enum lgtd_client_error_code error, + const char *msg) +{ + lgtd_jsonrpc_send_error(client, (enum lgtd_jsonrpc_error_code)error, msg); +} + +struct lgtd_client * +lgtd_client_open(evutil_socket_t peer, const struct sockaddr *addr, int addrlen) +{ + assert(peer != -1); + assert(addr); + + struct lgtd_client *client = calloc(1, sizeof(*client)); + if (!client) { + return NULL; + } + + client->io = bufferevent_socket_new( + lgtd_ev_base, peer, BEV_OPT_CLOSE_ON_FREE + ); + if (!client->io) { + goto error; + } + + client->addr = calloc(1, addrlen); + if (!client->addr) { + goto error; + } + memcpy(client->addr, addr, addrlen); + + bufferevent_setcb( + client->io, + lgtd_client_read_callback, + NULL, + lgtd_client_event_callback, + client + ); + if (bufferevent_enable(client->io, EV_READ|EV_WRITE|EV_TIMEOUT) == -1) { + goto error; + } + + LIST_INSERT_HEAD(&lgtd_clients, client, link); + + LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(clients, 1); + + return client; + +error: + if (client->io) { + bufferevent_free(client->io); + } + free(client->addr); + free(client); + return NULL; +} + +void +lgtd_client_open_from_pipe(struct lgtd_client *pipe_client) +{ + assert(pipe_client); + + memset(pipe_client, 0, sizeof(*pipe_client)); +} diff --git a/core/client.h b/core/client.h index 845bb17..1ec2c50 100644 --- a/core/client.h +++ b/core/client.h @@ -1,6 +1,50 @@ +// Copyright (c) 2015, Louis Opter +// +// This file is part of lighstd. +// +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . + #pragma once -struct lifxd_client { - LIST_ENTRY(lifxd_client) link; +enum { LGTD_CLIENT_MAX_REQUEST_BUF_SIZE = 4096 }; + +enum lgtd_client_error_code { + LGTD_CLIENT_SUCCESS = LGTD_JSONRPC_SUCCESS, + LGTD_CLIENT_PARSE_ERROR = LGTD_JSONRPC_PARSE_ERROR, + LGTD_CLIENT_INVALID_REQUEST = LGTD_JSONRPC_INVALID_REQUEST, + LGTD_CLIENT_METHOD_NOT_FOUND = LGTD_JSONRPC_METHOD_NOT_FOUND, + LGTD_CLIENT_INVALID_PARAMS = LGTD_JSONRPC_INVALID_PARAMS, + LGTD_CLIENT_INTERNAL_ERROR = LGTD_JSONRPC_INTERNAL_ERROR, + LGTD_CLIENT_SERVER_ERROR = LGTD_JSONRPC_SERVER_ERROR +}; + +struct lgtd_client { + LIST_ENTRY(lgtd_client) link; + struct bufferevent *io; + struct sockaddr *addr; + jsmntok_t *jsmn_tokens; + const char *json; + struct lgtd_jsonrpc_request *current_request; }; -LIST_HEAD(lifxd_client_list, lifxd_client); +LIST_HEAD(lgtd_client_list, lgtd_client); + +struct lgtd_client *lgtd_client_open(evutil_socket_t, const struct sockaddr *, int); +void lgtd_client_close_all(void); + +void lgtd_client_write_string(struct lgtd_client *, const char *); +void lgtd_client_write_buf(struct lgtd_client *, const char *, int); +void lgtd_client_send_response(struct lgtd_client *, const char *); +void lgtd_client_start_send_response(struct lgtd_client *); +void lgtd_client_end_send_response(struct lgtd_client *); +void lgtd_client_send_error(struct lgtd_client *, enum lgtd_client_error_code, const char *); diff --git a/core/console.c b/core/console.c new file mode 100644 index 0000000..7c0cdb0 --- /dev/null +++ b/core/console.c @@ -0,0 +1,140 @@ +// Copyright (c) 2015, Louis Opter +// +// This file is part of lighstd. +// +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "console.h" +#include "lightsd.h" + +static void +lgtd_console_isotime_now(char *strbuf, int bufsz) +{ + assert(strbuf); + assert(bufsz > 0); + + struct timeval now; + if (gettimeofday(&now, NULL) == -1) { + goto error; + } + struct tm tm_now; + if (!localtime_r(&now.tv_sec, &tm_now)) { + goto error; + } + LGTD_TM_TO_ISOTIME(&tm_now, strbuf, bufsz, now.tv_usec); + return; +error: + strbuf[0] = '\0'; +} + +static void +lgtd_console_log_header(const char *loglvl, bool showprogname) +{ + if (lgtd_opts.log_timestamps) { + char timestr[64]; + lgtd_console_isotime_now(timestr, sizeof(timestr)); + fprintf( + stderr, "[%s] [%s] %s", + timestr, loglvl, showprogname ? "lightsd: " : "" + ); + return; + } + fprintf(stderr, "[%s] %s", loglvl, showprogname ? "lightsd: " : ""); +} + +void +lgtd_console_err(int eval, const char *fmt, va_list ap) +{ + int errsave = errno; + va_list aq; + va_copy(aq, ap); + // lgtd_cleanup is probably going to free some of the arguments we got, so + // let's print to a buffer before we call err. + char errmsg[LGTD_ERROR_MSG_BUFSIZE]; + vsnprintf(errmsg, sizeof(errmsg), fmt, aq); + va_end(aq); + lgtd_cleanup(); + lgtd_console_log_header("ERR", false); + errno = errsave; + err(eval, "%s", errmsg); +} + +void +lgtd_console_errx(int eval, const char *fmt, va_list ap) +{ + va_list aq; + va_copy(aq, ap); + // lgtd_cleanup is probably going to free some of the arguments we got, so + // let's print to a buffer before we call err. + char errmsg[LGTD_ERROR_MSG_BUFSIZE]; + vsnprintf(errmsg, sizeof(errmsg), fmt, aq); + va_end(aq); + lgtd_cleanup(); + lgtd_console_log_header("ERR", false); + errx(eval, "%s", errmsg); +} + +void +lgtd_console_warn(const char *fmt, va_list ap) +{ + va_list aq; + va_copy(aq, ap); + lgtd_console_log_header("WARN", false); + vwarn(fmt, aq); + va_end(aq); +} + +void +lgtd_console_warnx(const char *fmt, va_list ap) +{ + va_list aq; + va_copy(aq, ap); + lgtd_console_log_header("WARN", false); + vwarnx(fmt, aq); + va_end(aq); +} + +void +lgtd_console_info(const char *fmt, va_list ap) +{ + va_list aq; + va_copy(aq, ap); + lgtd_console_log_header("INFO", true); + vfprintf(stderr, fmt, aq); + va_end(aq); + fprintf(stderr, "\n"); +} + +void +lgtd_console_debug(const char *fmt, va_list ap) +{ + va_list aq; + va_copy(aq, ap); + lgtd_console_log_header("DEBUG", true); + vfprintf(stderr, fmt, aq); + va_end(aq); + fprintf(stderr, "\n"); +} diff --git a/core/console.h b/core/console.h new file mode 100644 index 0000000..41dc4ae --- /dev/null +++ b/core/console.h @@ -0,0 +1,31 @@ +// Copyright (c) 2015, Louis Opter +// +// This file is part of lighstd. +// +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . + +#pragma once + +#ifndef __attribute__ +# define __atttribute__(e) +#endif + +void lgtd_console_err(int, const char *, va_list) + __attribute__((noreturn)); +void lgtd_console_errx(int, const char *, va_list) + __attribute__((noreturn)); +void lgtd_console_warn(const char *, va_list); +void lgtd_console_warnx(const char *, va_list); +void lgtd_console_info(const char *, va_list); +void lgtd_console_debug(const char *, va_list); diff --git a/core/daemon.c b/core/daemon.c new file mode 100644 index 0000000..3300536 --- /dev/null +++ b/core/daemon.c @@ -0,0 +1,559 @@ +// Copyright (c) 2015, Louis Opter +// +// This file is part of lighstd. +// +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "time_monotonic.h" +#include "lifx/wire_proto.h" +#include "lifx/bulb.h" +#include "lifx/gateway.h" +#include "jsmn.h" +#include "jsonrpc.h" +#include "client.h" +#include "listen.h" +#include "daemon.h" +#include "pipe.h" +#include "stats.h" +#include "lightsd.h" + +static bool lgtd_daemon_proctitle_initialized = false; +static struct passwd *lgtd_user_info = NULL; +static struct group *lgtd_group_info = NULL; + +bool +lgtd_daemon_unleash(void) +{ + if (chdir("/")) { + return false; + } + + int null = open("/dev/null", O_RDWR); + if (null == -1) { + return false; + } + + const int fds[] = { STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO }; + for (int i = 0; i != LGTD_ARRAY_SIZE(fds); ++i) { + if (dup2(null, fds[i]) == -1) { + close(null); + return false; + } + } + close(null); + +#define SUMMON() do { \ + switch (fork()) { \ + case 0: \ + break; \ + case -1: \ + return false; \ + default: \ + exit(0); \ + } \ +} while (0) + + SUMMON(); // \_o< ! + setsid(); + + SUMMON(); // \_o< !! + + return true; // welcome to UNIX! +} + +void +lgtd_daemon_setup_proctitle(int argc, char *argv[], char *envp[]) +{ +#if LGTD_HAVE_SETPROCTITLE + (void)argc; + (void)argv; + (void)envp; +#else + void setproctitle_init(int argc, char *argv[], char *envp[]); + + setproctitle_init(argc, argv, envp); + lgtd_daemon_update_proctitle(); + lgtd_daemon_proctitle_initialized = true; +#endif +} + +static char * +lgtd_daemon_update_proctitle_format_sockaddr(const struct sockaddr *sa, + char *buf, + int buflen) +{ + assert(sa); + assert(buf); + assert(buflen > 0); + + if (sa->sa_family == AF_UNIX) { + return ((struct sockaddr_un *)sa)->sun_path; + } + + return lgtd_sockaddrtoa(sa, buf, buflen); +} + +void +lgtd_daemon_update_proctitle(void) +{ + if (!lgtd_daemon_proctitle_initialized) { + return; + } + +#if !LGTD_HAVE_SETPROCTITLE + void setproctitle(const char *fmt, ...); +#endif + + char title[LGTD_DAEMON_TITLE_SIZE] = { 0 }; + int i = 0; + +#define TITLE_APPEND(fmt, ...) LGTD_SNPRINTF_APPEND( \ + title, i, (int)sizeof(title), (fmt), __VA_ARGS__ \ +) + +#define PREFIX(fmt, ...) TITLE_APPEND( \ + "%s" fmt, (i && title[i - 1] == ')' ? "; " : ""), __VA_ARGS__ \ +) + +#define ADD_ITEM(fmt, ...) TITLE_APPEND( \ + "%s" fmt, (i && title[i - 1] != '(' ? ", " : ""), __VA_ARGS__ \ +) +#define LOOP(list_type, list, elem_type, prefix, ...) do { \ + if (!list_type ## _EMPTY(list)) { \ + PREFIX("%s(", prefix); \ + elem_type *it; \ + list_type ## _FOREACH(it, list, link) { \ + ADD_ITEM(__VA_ARGS__); \ + } \ + TITLE_APPEND("%s", ")"); \ + } \ +} while (0) + + char addr[LGTD_SOCKADDR_STRLEN]; + LOOP( + SLIST, &lgtd_listeners, struct lgtd_listen, + "listening_on", "%s", + lgtd_daemon_update_proctitle_format_sockaddr( + it->sockaddr, addr, sizeof(addr) + ) + ); + + LOOP( + SLIST, &lgtd_command_pipes, struct lgtd_command_pipe, + "command_pipes", "%s", it->path + ); + + if (!LIST_EMPTY(&lgtd_lifx_gateways)) { + PREFIX("lifx_gateways(found=%d)", LGTD_STATS_GET(gateways)); + } + + PREFIX( + "bulbs(found=%d, on=%d)", + LGTD_STATS_GET(bulbs), LGTD_STATS_GET(bulbs_powered_on) + ); + + PREFIX("clients(connected=%d)", LGTD_STATS_GET(clients)); + + setproctitle("%s", title); +} + +void +lgtd_daemon_die_if_running_as_root_unless_requested(const char *requested_user) +{ + if (requested_user && !strcmp(requested_user, "root")) { + return; + } + + if (geteuid() == 0 || getegid() == 0) { + lgtd_errx( + 1, + "not running as root unless -u root is passed in; if you don't " + "understand why this very basic safety measure is in place and " + "use -u root then you deserve to be thrown under a bus, kthx bye." + ); + } +} + +void +lgtd_daemon_set_user(const char *user) +{ + assert(user); + + static struct passwd user_info_storage; + + struct passwd *user_info = getpwnam(user); + if (!user_info) { + lgtd_err(1, "can't get user info for %s", user); + } + + lgtd_user_info = memcpy(&user_info_storage, user_info, sizeof(*user_info)); +} + +void +lgtd_daemon_set_group(const char *group) +{ + assert(lgtd_user_info); + + static struct group group_info_storage; + + struct group *group_info; + if (group) { + group_info = getgrnam(group); + } else { + group_info = getgrgid(lgtd_user_info->pw_gid); + group = group_info->gr_name; + } + if (!group_info) { + lgtd_err( + 1, "can't get group info for %s", + group ? group : lgtd_user_info->pw_name + ); + } + + lgtd_group_info = memcpy( + &group_info_storage, group_info, sizeof(*group_info) + ); +} + +static int +lgtd_daemon_chown_dir_of(const char *filepath, uid_t uid, gid_t gid) +{ + char *fp = strdup(filepath); + if (!fp) { + return -1; + } + + char *dir = dirname(fp); + int rv = chown(dir, uid, gid); + free(fp); + return rv; +} + +void +lgtd_daemon_drop_privileges(void) +{ + assert(lgtd_user_info); + assert(lgtd_group_info); + + uid_t uid = lgtd_user_info->pw_uid; + const char *user = lgtd_user_info->pw_name; + gid_t gid = lgtd_group_info->gr_gid; + const char *group = lgtd_group_info->gr_name; + + struct lgtd_command_pipe *pipe; + SLIST_FOREACH(pipe, &lgtd_command_pipes, link) { + if (lgtd_daemon_chown_dir_of(pipe->path, uid, gid) == -1 + || fchown(pipe->fd, uid, gid) == -1) { + lgtd_err(1, "can't chown %s to %s:%s", pipe->path, user, group); + } + } + + struct lgtd_listen *listener; + SLIST_FOREACH(listener, &lgtd_listeners, link) { + if (listener->sockaddr->sa_family != AF_UNIX) { + continue; + } + + const char *path = ((struct sockaddr_un *)listener->sockaddr)->sun_path; + if (lgtd_daemon_chown_dir_of(path, uid, gid) == -1 + || chown(path, uid, gid) == -1) { + lgtd_err(1, "can't chown %s to %s:%s", path, user, group); + } + } + + if (setgid(gid) == -1) { + lgtd_err(1, "can't change group to %s", group); + } + + if (setgroups(1, &gid) == -1) { + lgtd_err(1, "can't change group to %s", group); + } + + if (setuid(uid) == -1) { + lgtd_err(1, "can't change user to %s", user); + } +} + +static bool +_lgtd_daemon_makedirs(const char *fp) +{ + char *fpsave = NULL, *next = NULL; + + fpsave = strdup(fp); + if (!fpsave) { + goto err; + } + next = strdup(dirname(fpsave)); + if (!next) { + goto err; + } + + if (!strcmp(next, fp)) { + goto done; + } + + bool ok = _lgtd_daemon_makedirs(next); + if (!ok) { + goto err; + } + + struct stat sb; + if (stat(next, &sb) == -1) { + if (errno == ENOENT) { + mode_t mode = S_IWUSR|S_IRUSR|S_IXUSR + |S_IRGRP|S_IXGRP|S_IWGRP + |S_IXOTH|S_IROTH; + if (mkdir(next, mode) == 0) { + goto done; + } + } + goto err; + } else if (!S_ISDIR(sb.st_mode)) { + errno = ENOTDIR; + goto err; + } + +done: + free(fpsave); + free(next); + return true; + +err: + free(fpsave); + free(next); + return false; +} + +bool +lgtd_daemon_makedirs(const char *filepath) +{ + if (!_lgtd_daemon_makedirs(filepath)) { + lgtd_warn("can't create parent directories for %s", filepath); + return false; + } + + return true; +} + +bool +lgtd_daemon_write_pidfile(const char *filepath) +{ + assert(filepath); + + char pidstr[32]; + int pidlen = snprintf(pidstr, sizeof(pidstr), "%ju", (uintmax_t)getpid()); + int written = 0; + + mode_t mode = S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH; + int fd = open(filepath, O_CREAT|O_WRONLY|O_TRUNC, mode); + if (fd == -1) { + return false; + } + + if (lgtd_user_info && lgtd_group_info + && fchown(fd, lgtd_user_info->pw_uid, lgtd_group_info->gr_gid) == -1) { + lgtd_warn( + "can't chown %s to %s:%s", + filepath, lgtd_user_info->pw_name, lgtd_group_info->gr_name + ); + } + + written = write(fd, pidstr, (size_t)pidlen); + + close(fd); + return written == pidlen; +} + +uint32_t +lgtd_daemon_randuint32(void) +{ + int fd = open("/dev/urandom", O_RDONLY); + if (fd == -1) { + lgtd_err(1, "couldn't open /dev/urandom"); + } + + uint32_t rv; + int nbytes = read(fd, &rv, sizeof(rv)); + if (nbytes != sizeof(rv)) { + close(fd); + lgtd_err( + 1, "couln't fetch %ju bytes from /dev/urandom", + (uintmax_t)sizeof(rv) + ); + } + + close(fd); + return rv; +} + +int +lgtd_daemon_syslog_facilitytoi(const char *facility) +{ + struct { + const char *name; + int value; + } syslog_facilities[] = { + { "daemon", LOG_DAEMON }, + { "user", LOG_USER }, + { "local0", LOG_LOCAL0 }, + { "local1", LOG_LOCAL1 }, + { "local2", LOG_LOCAL2 }, + { "local3", LOG_LOCAL3 }, + { "local4", LOG_LOCAL4 }, + { "local5", LOG_LOCAL5 }, + { "local6", LOG_LOCAL6 }, + { "local7", LOG_LOCAL7 } + }; + + for (int i = 0; i != LGTD_ARRAY_SIZE(syslog_facilities); i++) { + if (!strcmp(facility, syslog_facilities[i].name)) { + return syslog_facilities[i].value; + } + } + + lgtd_errx( + 1, + "invalid syslog facility %s (possible values: daemon, " + "user, local0 through 7)", + facility + ); +} + +static void +lgtd_daemon_setup_errfmt(const char *fmt, char *errfmt, int sz) +{ + int n = LGTD_MIN(sz - 1, (int)strlen(fmt) + 1); + memcpy(errfmt, fmt, n); + if (n - 1 + (int)sizeof(": %m") <= sz) { + memcpy(errfmt + n - 1, ": %m", sizeof(": %m")); + } else { + errfmt[n] = '\0'; +#ifndef NDEBUG + abort(); +#endif + } +} + +void +lgtd_daemon_syslog_err(int eval, const char *fmt, va_list ap) +{ + char errfmt[LGTD_DAEMON_ERRFMT_SIZE]; + lgtd_daemon_setup_errfmt(fmt, errfmt, sizeof(errfmt)); + + va_list aq; + va_copy(aq, ap); + vsyslog(LOG_ERR, errfmt, aq); + va_end(aq); + + lgtd_cleanup(); + exit(eval); +} + +// -Wtautological-constant-out-of-range-compare appears to be a clang thing. +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpragmas" +#pragma GCC diagnostic push // yes, I know, the assert below is stupid. +#pragma GCC diagnostic ignored "-Wtautological-constant-out-of-range-compare" +void +lgtd_daemon_syslog_open(const char *ident, + enum lgtd_verbosity level, + int facility) +{ + static const int syslog_priorities[] = { + LOG_DEBUG, LOG_INFO, LOG_WARNING, LOG_ERR + }; + + assert(level < LGTD_ARRAY_SIZE(syslog_priorities)); + + openlog(ident, LOG_PID, facility); + setlogmask(LOG_UPTO(syslog_priorities[level])); +} +#pragma GCC diagnostic pop +#pragma GCC diagnostic pop + +void +lgtd_daemon_syslog_errx(int eval, const char *fmt, va_list ap) +{ + va_list aq; + va_copy(aq, ap); + vsyslog(LOG_ERR, fmt, aq); + va_end(aq); + + lgtd_cleanup(); + exit(eval); +} + +void +lgtd_daemon_syslog_warn(const char *fmt, va_list ap) +{ + char errfmt[LGTD_DAEMON_ERRFMT_SIZE]; + lgtd_daemon_setup_errfmt(fmt, errfmt, sizeof(errfmt)); + + va_list aq; + va_copy(aq, ap); + vsyslog(LOG_WARNING, errfmt, aq); + va_end(aq); +} + +void +lgtd_daemon_syslog_warnx(const char *fmt, va_list ap) +{ + va_list aq; + va_copy(aq, ap); + vsyslog(LOG_WARNING, fmt, aq); + va_end(aq); +} + +void +lgtd_daemon_syslog_info(const char *fmt, va_list ap) +{ + va_list aq; + va_copy(aq, ap); + vsyslog(LOG_INFO, fmt, aq); + va_end(aq); +} + +void +lgtd_daemon_syslog_debug(const char *fmt, va_list ap) +{ + va_list aq; + va_copy(aq, ap); + vsyslog(LOG_DEBUG, fmt, aq); + va_end(aq); +} diff --git a/core/daemon.h b/core/daemon.h new file mode 100644 index 0000000..dbaa30a --- /dev/null +++ b/core/daemon.h @@ -0,0 +1,48 @@ +// Copyright (c) 2015, Louis Opter +// +// This file is part of lighstd. +// +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . + +#pragma once + +#ifndef __attribute__ +# define __atttribute__(e) +#endif + +enum lgtd_verbosity; + +enum { LGTD_DAEMON_TITLE_SIZE = 2048 }; + +enum { LGTD_DAEMON_ERRFMT_SIZE = 4096 }; + +bool lgtd_daemon_unleash(void); // \_o< +void lgtd_daemon_setup_proctitle(int, char *[], char *[]); +void lgtd_daemon_update_proctitle(void); +void lgtd_daemon_die_if_running_as_root_unless_requested(const char *); +void lgtd_daemon_set_user(const char *); +void lgtd_daemon_set_group(const char *); +bool lgtd_daemon_write_pidfile(const char *); +void lgtd_daemon_drop_privileges(void); +bool lgtd_daemon_makedirs(const char *); +uint32_t lgtd_daemon_randuint32(void); + +int lgtd_daemon_syslog_facilitytoi(const char *); +void lgtd_daemon_syslog_open(const char *, enum lgtd_verbosity, int); +void lgtd_daemon_syslog_err(int, const char *, va_list) __attribute__((noreturn)); +void lgtd_daemon_syslog_errx(int, const char *, va_list) __attribute__((noreturn)); +void lgtd_daemon_syslog_warn(const char *, va_list); +void lgtd_daemon_syslog_warnx(const char *, va_list); +void lgtd_daemon_syslog_info(const char *, va_list); +void lgtd_daemon_syslog_debug(const char *, va_list); diff --git a/core/gateway.c b/core/gateway.c deleted file mode 100644 index 52ae09d..0000000 --- a/core/gateway.c +++ /dev/null @@ -1,372 +0,0 @@ -// Copyright (c) 2014, Louis Opter -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// 3. Neither the name of the copyright holder nor the names of its contributors -// may be used to endorse or promote products derived from this software -// without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include "wire_proto.h" -#include "time_monotonic.h" -#include "bulb.h" -#include "gateway.h" -#include "broadcast.h" -#include "timer.h" -#include "lifxd.h" - -struct lifxd_gateway_list lifxd_gateways = - LIST_HEAD_INITIALIZER(&lifxd_gateways); - -void -lifxd_gateway_close(struct lifxd_gateway *gw) -{ - assert(gw); - - event_del(gw->refresh_ev); - event_del(gw->write_ev); - if (gw->socket != -1) { - evutil_closesocket(gw->socket); - LIST_REMOVE(gw, link); - } - event_free(gw->refresh_ev); - event_free(gw->write_ev); - evbuffer_free(gw->write_buf); - struct lifxd_bulb *bulb, *next_bulb; - SLIST_FOREACH_SAFE(bulb, &gw->bulbs, link_by_gw, next_bulb) { - lifxd_bulb_close(bulb); - } - - lifxd_info( - "connection with gateway bulb [%s]:%hu closed", gw->ip_addr, gw->port - ); - free(gw); -} - -static void -lifxd_gateway_write_callback(evutil_socket_t socket, short events, void *ctx) -{ - (void)socket; - - assert(ctx); - - struct lifxd_gateway *gw = (struct lifxd_gateway *)ctx; - if (events & EV_TIMEOUT) { // Not sure how that could happen in UDP but eh. - lifxd_warn( - "lost connection with gateway bulb [%s]:%hu", gw->ip_addr, gw->port - ); - lifxd_gateway_close(gw); - if (!lifxd_broadcast_discovery()) { - lifxd_err(1, "can't start auto discovery"); - } - return; - } - if (events & EV_WRITE) { - if (evbuffer_write(gw->write_buf, gw->socket) == -1 && errno != EAGAIN) { - lifxd_warn("can't write to [%s]:%hu", gw->ip_addr, gw->port); - lifxd_gateway_close(gw); - if (!lifxd_broadcast_discovery()) { - lifxd_err(1, "can't start auto discovery"); - } - return; - } - // Callbacks are called in any order, so we keep two timers to make - // sure we can get the latency right, otherwise we could be compute the - // latency with last_pkt_at < last_req_at, which isn't true since the - // pkt will be for an answer the previous write: - gw->last_req_at = gw->next_req_at; - gw->next_req_at = lifxd_time_monotonic_msecs(); - if (!evbuffer_get_length(gw->write_buf)) { - event_del(gw->write_ev); - } - } -} - -static void -lifxd_gateway_send_get_all_light_state(struct lifxd_gateway *gw) -{ - assert(gw); - - struct lifxd_packet_header hdr; - union lifxd_target target = { .addr = gw->site }; - lifxd_wire_setup_header( - &hdr, LIFXD_TARGET_SITE, target, gw->site, LIFXD_GET_LIGHT_STATE - ); - lifxd_debug("GET_LIGHT_STATE --> [%s]:%hu", gw->ip_addr, gw->port); - lifxd_gateway_send_packet(gw, &hdr, NULL, 0); -} - -static void -lifxd_gateway_refresh_callback(evutil_socket_t socket, short events, void *ctx) -{ - (void)socket; - (void)events; - lifxd_gateway_send_get_all_light_state((struct lifxd_gateway *)ctx); -} - -static struct lifxd_bulb * -lifxd_gateway_get_or_open_bulb(struct lifxd_gateway *gw, const uint8_t *bulb_addr) -{ - assert(gw); - assert(bulb_addr); - - struct lifxd_bulb *bulb = lifxd_bulb_get(gw, bulb_addr); - if (!bulb) { - bulb = lifxd_bulb_open(gw, bulb_addr); - if (bulb) { - SLIST_INSERT_HEAD(&gw->bulbs, bulb, link_by_gw); - lifxd_info( - "bulb %s on [%s]:%hu", - lifxd_addrtoa(bulb_addr), gw->ip_addr, gw->port - ); - } - } - return bulb; -} - -struct lifxd_gateway * -lifxd_gateway_open(const struct sockaddr_storage *peer, - ev_socklen_t addrlen, - const uint8_t *site, - lifxd_time_mono_t received_at) -{ - assert(peer); - assert(site); - - struct lifxd_gateway *gw = calloc(1, sizeof(*gw)); - if (!gw) { - lifxd_warn("can't allocate a new gateway bulb"); - return false; - } - gw->socket = socket(peer->ss_family, SOCK_DGRAM, IPPROTO_UDP); - if (gw->socket == -1) { - lifxd_warn("can't open a new socket"); - goto error_socket; - } - if (connect(gw->socket, (const struct sockaddr *)peer, addrlen) == -1 - || evutil_make_socket_nonblocking(gw->socket) == -1) { - lifxd_warn("can't open a new socket"); - goto error_connect; - } - gw->write_ev = event_new( - lifxd_ev_base, - gw->socket, - EV_WRITE|EV_PERSIST, - lifxd_gateway_write_callback, - gw - ); - gw->write_buf = evbuffer_new(); - gw->refresh_ev = evtimer_new( - lifxd_ev_base, lifxd_gateway_refresh_callback, gw - ); - memcpy(&gw->peer, peer, sizeof(gw->peer)); - lifxd_sockaddrtoa(peer, gw->ip_addr, sizeof(gw->ip_addr)); - gw->port = lifxd_sockaddrport(peer); - memcpy(gw->site, site, sizeof(gw->site)); - gw->last_req_at = received_at; - gw->next_req_at = received_at; - gw->last_pkt_at = received_at; - - struct timeval refresh_interval = LIFXD_MSECS_TO_TIMEVAL( - LIFXD_GATEWAY_MIN_REFRESH_INTERVAL_MSECS - ); - - if (!gw->write_ev || !gw->write_buf || !gw->refresh_ev - || event_add(gw->refresh_ev, &refresh_interval) != 0) { - lifxd_warn("can't allocate a new gateway bulb"); - goto error_allocate; - } - - lifxd_info( - "gateway for site %s at [%s]:%hu", - lifxd_addrtoa(gw->site), gw->ip_addr, gw->port - ); - LIST_INSERT_HEAD(&lifxd_gateways, gw, link); - - // In case this is the first bulb (re-)discovered, start the watchdog, it - // will stop by itself: - lifxd_timer_start_watchdog(); - - return gw; - -error_allocate: - if (gw->write_ev) { - event_free(gw->write_ev); - } - if (gw->write_buf) { - evbuffer_free(gw->write_buf); - } - if (gw->refresh_ev) { - event_free(gw->refresh_ev); - } -error_connect: - evutil_closesocket(gw->socket); -error_socket: - free(gw); - return NULL; -} - -struct lifxd_gateway * -lifxd_gateway_get(const struct sockaddr_storage *peer) -{ - assert(peer); - - struct lifxd_gateway *gw, *next_gw; - LIST_FOREACH_SAFE(gw, &lifxd_gateways, link, next_gw) { - if (peer->ss_family == gw->peer.ss_family - && !memcmp(&gw->peer, peer, sizeof(*peer))) { - return gw; - } - } - - return NULL; -} - -void -lifxd_gateway_close_all(void) -{ - struct lifxd_gateway *gw, *next_gw; - LIST_FOREACH_SAFE(gw, &lifxd_gateways, link, next_gw) { - lifxd_gateway_close(gw); - } -} - -void -lifxd_gateway_send_packet(struct lifxd_gateway *gw, - const struct lifxd_packet_header *hdr, - const void *pkt, - int pkt_size) -{ - assert(gw); - assert(hdr); - assert(pkt_size >= 0 && pkt_size < LIFXD_MAX_PACKET_SIZE); - assert(!memcmp(hdr->site, gw->site, LIFXD_ADDR_LENGTH)); - - evbuffer_add(gw->write_buf, hdr, sizeof(*hdr)); - if (pkt) { - assert((unsigned)pkt_size == le16toh(hdr->size) - sizeof(*hdr)); - evbuffer_add(gw->write_buf, pkt, pkt_size); - } - event_add(gw->write_ev, NULL); -} - -void -lifxd_gateway_handle_pan_gateway(struct lifxd_gateway *gw, - const struct lifxd_packet_header *hdr, - const struct lifxd_packet_pan_gateway *pkt) -{ - assert(gw && hdr && pkt); - - lifxd_debug( - "SET_PAN_GATEWAY <-- [%s]:%hu - %s site=%s", - gw->ip_addr, gw->port, - lifxd_addrtoa(hdr->target.device_addr), - lifxd_addrtoa(hdr->site) - ); -} - -void -lifxd_gateway_handle_light_status(struct lifxd_gateway *gw, - const struct lifxd_packet_header *hdr, - const struct lifxd_packet_light_status *pkt) -{ - assert(gw && hdr && pkt); - - lifxd_debug( - "SET_LIGHT_STATE <-- [%s]:%hu - %s " - "hue=%#hx, saturation=%#hx, brightness=%#hx, " - "kelvin=%d, dim=%#hx, power=%#hx, label=%.*s, tags=%#llx", - gw->ip_addr, gw->port, lifxd_addrtoa(hdr->target.device_addr), - pkt->hue, pkt->saturation, pkt->brightness, pkt->kelvin, - pkt->dim, pkt->power, LIFXD_LABEL_SIZE, pkt->label, pkt->tags - ); - - struct lifxd_bulb *b = lifxd_gateway_get_or_open_bulb( - gw, hdr->target.device_addr - ); - if (!b) { - return; - } - - assert(sizeof(*pkt) == sizeof(b->state)); - lifxd_bulb_set_light_state( - b, (const struct lifxd_light_state *)pkt, gw->last_pkt_at - ); - - int latency = gw->last_pkt_at - gw->last_req_at; - if (latency < LIFXD_GATEWAY_MIN_REFRESH_INTERVAL_MSECS) { - int timeout = LIFXD_GATEWAY_MIN_REFRESH_INTERVAL_MSECS - latency; - struct timeval tv = LIFXD_MSECS_TO_TIMEVAL(timeout); - evtimer_add(gw->refresh_ev, &tv); - lifxd_debug( - "[%s]:%hu latency is %dms, scheduling next GET_LIGHT_STATE in %dms", - gw->ip_addr, gw->port, latency, timeout - ); - return; - } - - lifxd_debug( - "[%s]:%hu latency is %dms, sending GET_LIGHT_STATE now", - gw->ip_addr, gw->port, latency - ); - lifxd_gateway_send_get_all_light_state(gw); -} - -void -lifxd_gateway_handle_power_state(struct lifxd_gateway *gw, - const struct lifxd_packet_header *hdr, - const struct lifxd_packet_power_state *pkt) -{ - assert(gw && hdr && pkt); - - lifxd_debug( - "SET_POWER_STATE <-- [%s]:%hu - %s power=%#hx", - gw->ip_addr, gw->port, lifxd_addrtoa(hdr->target.device_addr), pkt->power - ); - - struct lifxd_bulb *b = lifxd_gateway_get_or_open_bulb( - gw, hdr->target.device_addr - ); - if (!b) { - return; - } - - lifxd_bulb_set_power_state(b, pkt->power); -} diff --git a/core/gateway.h b/core/gateway.h deleted file mode 100644 index f68bc3f..0000000 --- a/core/gateway.h +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) 2014, Louis Opter -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// 3. Neither the name of the copyright holder nor the names of its contributors -// may be used to endorse or promote products derived from this software -// without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. - -#pragma once - -// Send GET_LIGHT_STATE to the gateway at most every this interval. FYI, -// according to my own tests, aggressively polling a bulb doesn't raise its -// consumption at all (and it's interesting to note that a turned off bulb -// still draw about 2W in ZigBee and about 3W in WiFi). -enum { LIFXD_GATEWAY_MIN_REFRESH_INTERVAL_MSECS = 200 }; - -struct lifxd_gateway { - LIST_ENTRY(lifxd_gateway) link; - struct lifxd_bulb_list bulbs; - // Multiple gateways can share the same site (that happens when bulbs are - // far away enough that ZigBee can't be used). Moreover the SET_PAN_GATEWAY - // packet doesn't include the device address in the header (i.e: site and - // device_addr have the same value) so we have no choice but to use the - // remote ip address to identify a gateway: - struct sockaddr_storage peer; - char ip_addr[INET6_ADDRSTRLEN]; - uint16_t port; - uint8_t site[LIFXD_ADDR_LENGTH]; - evutil_socket_t socket; - // Those three timers let us measure the latency of the gateway. If we - // aren't the only client on the network then this won't be accurate since - // we will get pushed packets we didn't ask for, but good enough for our - // purpose of rate limiting our requests to the gateway: - lifxd_time_mono_t last_req_at; - lifxd_time_mono_t next_req_at; - lifxd_time_mono_t last_pkt_at; - struct event *write_ev; - struct evbuffer *write_buf; - struct event *refresh_ev; -}; -LIST_HEAD(lifxd_gateway_list, lifxd_gateway); - -extern struct lifxd_gateway_list lifxd_gateways; - -struct lifxd_gateway *lifxd_gateway_get(const struct sockaddr_storage *); -struct lifxd_gateway *lifxd_gateway_open(const struct sockaddr_storage *, - ev_socklen_t, - const uint8_t *, - lifxd_time_mono_t); - -void lifxd_gateway_close(struct lifxd_gateway *); -void lifxd_gateway_close_all(void); - -void lifxd_gateway_send_packet(struct lifxd_gateway *, - const struct lifxd_packet_header *, - const void *, - int); - -void lifxd_gateway_handle_pan_gateway(struct lifxd_gateway *, - const struct lifxd_packet_header *, - const struct lifxd_packet_pan_gateway *); -void lifxd_gateway_handle_light_status(struct lifxd_gateway *, - const struct lifxd_packet_header *, - const struct lifxd_packet_light_status *); -void lifxd_gateway_handle_power_state(struct lifxd_gateway *, - const struct lifxd_packet_header *, - const struct lifxd_packet_power_state *); diff --git a/core/jsmn.c b/core/jsmn.c new file mode 100644 index 0000000..0efa8c3 --- /dev/null +++ b/core/jsmn.c @@ -0,0 +1,333 @@ +/* + * Copyright (c) 2010 Serge A. Zaitsev + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include + +#include "jsmn.h" + +/** + * Allocates a fresh unused token from the token pull. + */ +static jsmntok_t *jsmn_alloc_token(jsmn_parser *parser, + jsmntok_t *tokens, size_t num_tokens) { + jsmntok_t *tok; + if (parser->toknext >= num_tokens) { + return NULL; + } + tok = &tokens[parser->toknext++]; + tok->start = tok->end = -1; + tok->size = 0; +#ifdef JSMN_PARENT_LINKS + tok->parent = -1; +#endif + return tok; +} + +/** + * Fills token type and boundaries. + */ +static void jsmn_fill_token(jsmntok_t *token, jsmntype_t type, + int start, int end) { + token->type = type; + token->start = start; + token->end = end; + token->size = 0; +} + +/** + * Fills next available token with JSON primitive. + */ +static jsmnerr_t jsmn_parse_primitive(jsmn_parser *parser, const char *js, + size_t len, jsmntok_t *tokens, size_t num_tokens) { + jsmntok_t *token; + int start; + + start = parser->pos; + + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + switch (js[parser->pos]) { +#ifndef JSMN_STRICT + /* In strict mode primitive must be followed by "," or "}" or "]" */ + case ':': +#endif + case '\t' : case '\r' : case '\n' : case ' ' : + case ',' : case ']' : case '}' : + goto found; + } + if (js[parser->pos] < 32 || js[parser->pos] >= 127) { + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } +#ifdef JSMN_STRICT + /* In strict mode primitive must be followed by a comma/object/array */ + parser->pos = start; + return JSMN_ERROR_PART; +#endif + +found: + if (tokens == NULL) { + parser->pos--; + return 0; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) { + parser->pos = start; + return JSMN_ERROR_NOMEM; + } + jsmn_fill_token(token, JSMN_PRIMITIVE, start, parser->pos); +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + parser->pos--; + return 0; +} + +/** + * Filsl next token with JSON string. + */ +static jsmnerr_t jsmn_parse_string(jsmn_parser *parser, const char *js, + size_t len, jsmntok_t *tokens, size_t num_tokens) { + jsmntok_t *token; + + int start = parser->pos; + + parser->pos++; + + /* Skip starting quote */ + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + char c = js[parser->pos]; + + /* Quote: end of string */ + if (c == '\"') { + if (tokens == NULL) { + return 0; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) { + parser->pos = start; + return JSMN_ERROR_NOMEM; + } + jsmn_fill_token(token, JSMN_STRING, start+1, parser->pos); +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + return 0; + } + + /* Backslash: Quoted symbol expected */ + if (c == '\\' && parser->pos + 1 < len) { + int i; + parser->pos++; + switch (js[parser->pos]) { + /* Allowed escaped symbols */ + case '\"': case '/' : case '\\' : case 'b' : + case 'f' : case 'r' : case 'n' : case 't' : + break; + /* Allows escaped symbol \uXXXX */ + case 'u': + parser->pos++; + for(i = 0; i < 4 && parser->pos < len && js[parser->pos] != '\0'; i++) { + /* If it isn't a hex character we have an error */ + if(!((js[parser->pos] >= 48 && js[parser->pos] <= 57) || /* 0-9 */ + (js[parser->pos] >= 65 && js[parser->pos] <= 70) || /* A-F */ + (js[parser->pos] >= 97 && js[parser->pos] <= 102))) { /* a-f */ + parser->pos = start; + return JSMN_ERROR_INVAL; + } + parser->pos++; + } + parser->pos--; + break; + /* Unexpected symbol */ + default: + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } + } + parser->pos = start; + return JSMN_ERROR_PART; +} + +/** + * Parse JSON string and fill tokens. + */ +jsmnerr_t jsmn_parse(jsmn_parser *parser, const char *js, size_t len, + jsmntok_t *tokens, unsigned int num_tokens) { + jsmnerr_t r; + int i; + jsmntok_t *token; + int count = 0; + + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + char c; + jsmntype_t type; + + c = js[parser->pos]; + switch (c) { + case '{': case '[': + count++; + if (tokens == NULL) { + break; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) + return JSMN_ERROR_NOMEM; + if (parser->toksuper != -1) { + tokens[parser->toksuper].size++; +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + } + token->type = (c == '{' ? JSMN_OBJECT : JSMN_ARRAY); + token->start = parser->pos; + parser->toksuper = parser->toknext - 1; + break; + case '}': case ']': + if (tokens == NULL) + break; + type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY); +#ifdef JSMN_PARENT_LINKS + if (parser->toknext < 1) { + return JSMN_ERROR_INVAL; + } + token = &tokens[parser->toknext - 1]; + for (;;) { + if (token->start != -1 && token->end == -1) { + if (token->type != type) { + return JSMN_ERROR_INVAL; + } + token->end = parser->pos + 1; + parser->toksuper = token->parent; + break; + } + if (token->parent == -1) { + break; + } + token = &tokens[token->parent]; + } +#else + for (i = parser->toknext - 1; i >= 0; i--) { + token = &tokens[i]; + if (token->start != -1 && token->end == -1) { + if (token->type != type) { + return JSMN_ERROR_INVAL; + } + parser->toksuper = -1; + token->end = parser->pos + 1; + break; + } + } + /* Error if unmatched closing bracket */ + if (i == -1) return JSMN_ERROR_INVAL; + for (; i >= 0; i--) { + token = &tokens[i]; + if (token->start != -1 && token->end == -1) { + parser->toksuper = i; + break; + } + } +#endif + break; + case '\"': + r = jsmn_parse_string(parser, js, len, tokens, num_tokens); + if (r < 0) return r; + count++; + if (parser->toksuper != -1 && tokens != NULL) + tokens[parser->toksuper].size++; + break; + case '\t' : case '\r' : case '\n' : case ' ': + break; + case ':': + parser->toksuper = parser->toknext - 1; + break; + case ',': + if (tokens != NULL && + tokens[parser->toksuper].type != JSMN_ARRAY && + tokens[parser->toksuper].type != JSMN_OBJECT) { +#ifdef JSMN_PARENT_LINKS + parser->toksuper = tokens[parser->toksuper].parent; +#else + for (i = parser->toknext - 1; i >= 0; i--) { + if (tokens[i].type == JSMN_ARRAY || tokens[i].type == JSMN_OBJECT) { + if (tokens[i].start != -1 && tokens[i].end == -1) { + parser->toksuper = i; + break; + } + } + } +#endif + } + break; +#ifdef JSMN_STRICT + /* In strict mode primitives are: numbers and booleans */ + case '-': case '0': case '1' : case '2': case '3' : case '4': + case '5': case '6': case '7' : case '8': case '9': + case 't': case 'f': case 'n' : + /* And they must not be keys of the object */ + if (tokens != NULL) { + jsmntok_t *t = &tokens[parser->toksuper]; + if (t->type == JSMN_OBJECT || + (t->type == JSMN_STRING && t->size != 0)) { + return JSMN_ERROR_INVAL; + } + } +#else + /* In non-strict mode every unquoted value is a primitive */ + default: +#endif + r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens); + if (r < 0) return r; + count++; + if (parser->toksuper != -1 && tokens != NULL) + tokens[parser->toksuper].size++; + break; + +#ifdef JSMN_STRICT + /* Unexpected char in strict mode */ + default: + return JSMN_ERROR_INVAL; +#endif + } + } + + for (i = parser->toknext - 1; i >= 0; i--) { + /* Unmatched opened object or array */ + if (tokens[i].start != -1 && tokens[i].end == -1) { + return JSMN_ERROR_PART; + } + } + + return count; +} + +/** + * Creates a new parser based over a given buffer with an array of tokens + * available. + */ +void jsmn_init(jsmn_parser *parser) { + parser->pos = 0; + parser->toknext = 0; + parser->toksuper = -1; +} + diff --git a/core/jsmn.h b/core/jsmn.h new file mode 100644 index 0000000..48a07c1 --- /dev/null +++ b/core/jsmn.h @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2010 Serge A. Zaitsev + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef __JSMN_H_ +#define __JSMN_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * JSON type identifier. Basic types are: + * o Object + * o Array + * o String + * o Other primitive: number, boolean (true/false) or null + */ +typedef enum { + JSMN_PRIMITIVE = 0, + JSMN_OBJECT = 1, + JSMN_ARRAY = 2, + JSMN_STRING = 3 +} jsmntype_t; + +typedef enum { + /* Not enough tokens were provided */ + JSMN_ERROR_NOMEM = -1, + /* Invalid character inside JSON string */ + JSMN_ERROR_INVAL = -2, + /* The string is not a full JSON packet, more bytes expected */ + JSMN_ERROR_PART = -3 +} jsmnerr_t; + +/** + * JSON token description. + * @param type type (object, array, string etc.) + * @param start start position in JSON data string + * @param end end position in JSON data string + */ +typedef struct { + jsmntype_t type; + int start; + int end; + int size; +#ifdef JSMN_PARENT_LINKS + int parent; +#endif +} jsmntok_t; + +/** + * JSON parser. Contains an array of token blocks available. Also stores + * the string being parsed now and current position in that string + */ +typedef struct { + unsigned int pos; /* offset in the JSON string */ + unsigned int toknext; /* next token to allocate */ + int toksuper; /* superior token node, e.g parent object or array */ +} jsmn_parser; + +/** + * Create JSON parser over an array of tokens + */ +void jsmn_init(jsmn_parser *parser); + +/** + * Run JSON parser. It parses a JSON data string into and array of tokens, each describing + * a single JSON object. + */ +jsmnerr_t jsmn_parse(jsmn_parser *parser, const char *js, size_t len, + jsmntok_t *tokens, unsigned int num_tokens); + +#ifdef __cplusplus +} +#endif + +#endif /* __JSMN_H_ */ diff --git a/core/jsonrpc.c b/core/jsonrpc.c new file mode 100644 index 0000000..609a127 --- /dev/null +++ b/core/jsonrpc.c @@ -0,0 +1,1263 @@ +// Copyright (c) 2014, 2015, Louis Opter +// +// This file is part of lighstd. +// +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "lifx/wire_proto.h" +#include "jsmn.h" +#include "jsonrpc.h" +#include "client.h" +#include "proto.h" +#include "lightsd.h" + +static bool +lgtd_jsonrpc_type_integer(const jsmntok_t *t, const char *json) +{ + if (t->type != JSMN_PRIMITIVE) { + return false; + } + + const char *endptr = NULL; + errno = 0; + strtol(&json[t->start], (char **)&endptr, 10); + return endptr == json + t->end && errno != ERANGE; +} + +static bool +lgtd_jsonrpc_type_float_between_0_and_1(const jsmntok_t *t, + const char *json) +{ + if (t->type != JSMN_PRIMITIVE) { + return false; + } + + int i = t->start; + bool dot_seen = false; + bool first_digit_is_one = false; + for (; i < t->end; i++) { + if (json[i] == '.') { + if (dot_seen) { + return false; + } + dot_seen = true; + } else if (dot_seen) { + if (json[i] < '0' || json[i] > '9') { + return false; + } if (first_digit_is_one && json[i] != '0') { + return false; + } + } else { + if (first_digit_is_one) { + return false; + } else if (json[i] == '1') { + first_digit_is_one = true; + } else if (json[i] != '0') { + return false; + } + } + } + + return true; +} + +static bool +lgtd_jsonrpc_type_float_between_0_and_360(const jsmntok_t *t, + const char *json) +{ + if (t->type != JSMN_PRIMITIVE) { + return false; + } + + char c = json[t->start]; + if (c != '-' && (c > '9' || c < '0')) { + return false; + } + + const char *endptr = NULL; + errno = 0; + long intpart = strtol(&json[t->start], (char **)&endptr, 10); + if ((endptr != json + t->end && *endptr != '.') + || errno == ERANGE || intpart < 0 || intpart > 360 + || (intpart == 0 && c == '-')) { + return false; + } + if (endptr == json + t->end) { + return true; + } + long fracpart = strtol(++endptr, (char **)&endptr, 10); + return endptr == json + t->end && errno != ERANGE + && fracpart >= 0 && (intpart < 360 || fracpart == 0); +} + +static bool +lgtd_jsonrpc_type_number(const jsmntok_t *t, const char *json) +{ + if (t->type != JSMN_PRIMITIVE) { + return false; + } + + char c = json[t->start]; + return c == '-' || (c >= '0' && c <= '9'); +} + +static bool +lgtd_jsonrpc_type_bool(const jsmntok_t *t, const char *json) +{ + if (t->type != JSMN_PRIMITIVE) { + return false; + } + + char c = json[t->start]; + return c == 't' || c == 'f'; +} + +static bool +lgtd_jsonrpc_type_null(const jsmntok_t *t, const char *json) +{ + return memcmp( + &json[t->start], "null", LGTD_MIN(t->size, (int)sizeof("null")) + ); +} + +static bool +lgtd_jsonrpc_type_string(const jsmntok_t *t, const char *json) +{ + (void)json; + return t->type == JSMN_STRING; +} + +static bool +lgtd_jsonrpc_type_array(const jsmntok_t *t, const char *json) +{ + (void)json; + return t->type == JSMN_ARRAY; +} + +static bool +lgtd_jsonrpc_type_object(const jsmntok_t *t, const char *json) +{ + (void)json; + return t->type == JSMN_OBJECT; +} + +static bool +lgtd_jsonrpc_type_object_or_array(const jsmntok_t *t, const char *json) +{ + (void)json; + return t->type == JSMN_OBJECT || t->type == JSMN_ARRAY; +} + +static bool +lgtd_jsonrpc_type_string_number_or_null(const jsmntok_t *t, + const char *json) +{ + return lgtd_jsonrpc_type_number(t, json) + || lgtd_jsonrpc_type_null(t, json) + || lgtd_jsonrpc_type_string(t, json); +} + +static bool +lgtd_jsonrpc_type_string_or_number(const jsmntok_t *t, + const char *json) +{ + return lgtd_jsonrpc_type_string(t, json) + || lgtd_jsonrpc_type_number(t, json); +} + +static bool __attribute__((unused)) +lgtd_jsonrpc_type_string_or_array(const jsmntok_t *t, const char *json) +{ + return lgtd_jsonrpc_type_string(t, json) + || lgtd_jsonrpc_type_array(t, json); +} + +static bool +lgtd_jsonrpc_type_string_number_or_array(const jsmntok_t *t, const char *json) +{ + return lgtd_jsonrpc_type_string(t, json) + || lgtd_jsonrpc_type_number(t, json) + || lgtd_jsonrpc_type_array(t, json); +} + +static int +lgtd_jsonrpc_float_range_to_uint16(const char *s, int len, int start, int stop) +{ + assert(s); + assert(len > 0); + assert(start < stop); + + int range; + range = stop * LGTD_JSONRPC_FLOAT_PREC - start * LGTD_JSONRPC_FLOAT_PREC; + const char *dot = NULL; + long fracpart = 0; + long intpart = strtol(s, (char **)&dot, 10) * LGTD_JSONRPC_FLOAT_PREC; + if (dot - s != len && *dot == '.') { + for (int i = dot - s + 1, multiplier = LGTD_JSONRPC_FLOAT_PREC / 10; + i != len && multiplier != 0; + i++, multiplier /= 10) { + fracpart += (s[i] - '0') * multiplier; + } + } + return ((int64_t)intpart + (int64_t)fracpart) * UINT16_MAX / (int64_t)range; +} + +void +lgtd_jsonrpc_uint16_range_to_float_string(uint16_t encoded, int start, int stop, + char *out, int size) +{ + assert(out); + assert(size > 1); + assert(start < stop); + + if (size < 2) { + if (size) { + *out = '\0'; + } + return; + } + + int range; + range = stop * LGTD_JSONRPC_FLOAT_PREC - start * LGTD_JSONRPC_FLOAT_PREC; + int value = (uint64_t)encoded * (uint64_t)range / UINT16_MAX; + + if (!value) { + out[0] = '0'; + out[1] = '\0'; + return; + } + + int multiplier = 1; + while (value / (multiplier * 10)) { + multiplier *= 10; + } + + int i = 0; + + if (LGTD_JSONRPC_FLOAT_PREC / 10 > multiplier) { + out[i++] = '0'; + if (i != size) { + out[i++] = '.'; + } + for (int divider = 10; + LGTD_JSONRPC_FLOAT_PREC / divider > multiplier && i != size; + divider *= 10) { + out[i++] = '0'; + } + } + + do { + if (multiplier == LGTD_JSONRPC_FLOAT_PREC / 10) { + if (i == 0) { + out[i++] = '0'; + } + if (i != size) { + out[i++] = '.'; + } + } + if (i != size) { + out[i++] = '0' + value / multiplier; + } + value -= value / multiplier * multiplier; + multiplier /= 10; + } while ((value || multiplier >= LGTD_JSONRPC_FLOAT_PREC) + && multiplier && i != size); + + out[LGTD_MIN(i, size - 1)] = '\0'; + + assert(i <= size); +} + +static int +lgtd_jsonrpc_consume_object_or_array(const jsmntok_t *tokens, + int ti, + int parsed, + const char *json) +{ + assert(lgtd_jsonrpc_type_object_or_array(&tokens[ti], json)); + assert(ti < parsed); + + jsmntype_t container_type = tokens[ti].type; + int objsize = tokens[ti++].size; + while (objsize-- && ti < parsed) { + if (container_type == JSMN_OBJECT) { + ti++; // move to the value + } + if (lgtd_jsonrpc_type_object_or_array(&tokens[ti], json)) { + ti = lgtd_jsonrpc_consume_object_or_array(tokens, ti, parsed, json); + } else { + ti++; + } + } + + return ti; +} + +static bool +lgtd_jsonrpc_extract_values_from_schema_and_dict(void *output, + const struct lgtd_jsonrpc_node *schema, + int schema_size, + const jsmntok_t *tokens, + int ntokens, + const char *json) +{ + if (!ntokens || tokens[0].type != JSMN_OBJECT) { + return false; + } + + for (int ti = 1; ti < ntokens;) { + // make sure it's a key, otherwise we reached the end of the object: + if (tokens[ti].type != JSMN_STRING) { + break; + } + + int si = 0; + for (;; si++) { + if (si == schema_size) { + ti++; // nothing matched, skip the key + break; + } + + int tokenlen = LGTD_JSONRPC_TOKEN_LEN(&tokens[ti]); + if (schema[si].keylen != tokenlen) { + continue; + } + int diff = memcmp( + schema[si].key, &json[tokens[ti].start], tokenlen + ); + if (!diff) { + ti++; // keys looks good, move to the value + if (!schema[si].type_cmp(&tokens[ti], json)) { + lgtd_debug( + "jsonrpc client sent an invalid value for %s", + schema[si].key + ); + return false; + } + if (schema[si].value_offset != -1) { + const jsmntok_t *seen = LGTD_JSONRPC_GET_JSMNTOK( + output, schema[si].value_offset + ); + if (seen) { // duplicate key + lgtd_debug( + "jsonrpc client sent duplicate parameter %s", + schema[si].key + ); + return false; + } + LGTD_JSONRPC_SET_JSMNTOK( + output, schema[si].value_offset, &tokens[ti] + ); + } + break; + } + } + + // skip the value, if it's an object or an array we need to + // skip everything in it: + int value_ntokens = ti; + if (tokens[ti].type == JSMN_OBJECT || tokens[ti].type == JSMN_ARRAY) { + ti = lgtd_jsonrpc_consume_object_or_array( + tokens, ti, ntokens, json + ); + } else { + ti++; + } + value_ntokens = ti - value_ntokens; + if (si < schema_size && schema[si].ntokens_offset != -1) { + LGTD_JSONRPC_SET_NTOKENS( + output, schema[si].ntokens_offset, value_ntokens + ); + } + } + + for (int si = 0; si != schema_size; si++) { + if (!schema[si].optional) { + const jsmntok_t *seen = LGTD_JSONRPC_GET_JSMNTOK( + output, schema[si].value_offset + ); + if (!seen) { + lgtd_debug("missing jsonrpc parameter %s", schema[si].key); + return false; + } + lgtd_debug("got jsonrpc parameter %s", schema[si].key); + } + } + + return true; +} + +static bool +lgtd_jsonrpc_extract_values_from_schema_and_array(void *output, + const struct lgtd_jsonrpc_node *schema, + int schema_size, + const jsmntok_t *tokens, + int ntokens, + const char *json) +{ + if (!ntokens || tokens[0].type != JSMN_ARRAY) { + return false; + } + + int si, ti, objsize = tokens[0].size; + for (si = 0, ti = 1; si < schema_size && ti < ntokens && objsize--; si++) { + if (!schema[si].type_cmp(&tokens[ti], json)) { + lgtd_debug( + "jsonrpc client sent an invalid value for %s", + schema[si].key + ); + return false; + } + if (schema[si].value_offset != -1) { + LGTD_JSONRPC_SET_JSMNTOK( + output, schema[si].value_offset, &tokens[ti] + ); + } + // skip the value, if it's an object or an array we need to + // skip everything in it: + int value_ntokens = ti; + if (tokens[ti].type == JSMN_OBJECT || tokens[ti].type == JSMN_ARRAY) { + ti = lgtd_jsonrpc_consume_object_or_array( + tokens, ti, ntokens, json + ); + } else { + ti++; + } + value_ntokens = ti - value_ntokens; + if (schema[si].ntokens_offset != -1) { + LGTD_JSONRPC_SET_NTOKENS( + output, schema[si].ntokens_offset, value_ntokens + ); + } + } + + return si == schema_size; +} + +static bool +lgtd_jsonrpc_extract_and_validate_params_against_schema(void *output, + const struct lgtd_jsonrpc_node *schema, + int schema_size, + const jsmntok_t *tokens, + int ntokens, + const char *json) +{ + switch (tokens[0].type) { + case JSMN_OBJECT: + return lgtd_jsonrpc_extract_values_from_schema_and_dict( + output, schema, schema_size, tokens, ntokens, json + ); + case JSMN_ARRAY: + return lgtd_jsonrpc_extract_values_from_schema_and_array( + output, schema, schema_size, tokens, ntokens, json + ); + default: + return false; + } +} + +static void +lgtd_jsonrpc_write_id(struct lgtd_client *client) +{ + if (!client->current_request || !client->current_request->id) { + lgtd_client_write_string(client, "null"); + return; + } + + int start, stop; + if (client->current_request->id->type == JSMN_STRING) { // get the quotes + start = client->current_request->id->start - 1; + stop = client->current_request->id->end + 1; + } else { + start = client->current_request->id->start; + stop = client->current_request->id->end; + } + lgtd_client_write_buf(client, &client->json[start], stop - start); +} + +void +lgtd_jsonrpc_send_error(struct lgtd_client *client, + enum lgtd_jsonrpc_error_code code, + const char *message) +{ + assert(client); + assert(message); + + lgtd_client_write_string(client, "{\"jsonrpc\": \"2.0\", \"id\": "); + lgtd_jsonrpc_write_id(client); + lgtd_client_write_string(client, ", \"error\": {\"code\": "); + char str_code[8] = { 0 }; + snprintf(str_code, sizeof(str_code), "%d", code); + lgtd_client_write_string(client, str_code); + lgtd_client_write_string(client, ", \"message\": \""); + lgtd_client_write_string(client, message); + lgtd_client_write_string(client, "\"}}"); +} + +void +lgtd_jsonrpc_send_response(struct lgtd_client *client, + const char *result) +{ + assert(client); + assert(result); + + lgtd_client_write_string(client, "{\"jsonrpc\": \"2.0\", \"id\": "); + lgtd_jsonrpc_write_id(client); + lgtd_client_write_string(client, ", \"result\": "); + lgtd_client_write_string(client, result); + lgtd_client_write_string(client, "}"); +} + +void +lgtd_jsonrpc_start_send_response(struct lgtd_client *client) +{ + assert(client); + + lgtd_client_write_string(client, "{\"jsonrpc\": \"2.0\", \"id\": "); + lgtd_jsonrpc_write_id(client); + lgtd_client_write_string(client, ", \"result\": "); +} + +void +lgtd_jsonrpc_end_send_response(struct lgtd_client *client) +{ + lgtd_client_write_string(client, "}"); +} + +static bool +lgtd_jsonrpc_check_and_extract_request(struct lgtd_jsonrpc_request *request, + const jsmntok_t *tokens, + int ntokens, + const char *json) +{ + static const struct lgtd_jsonrpc_node request_schema[] = { + LGTD_JSONRPC_NODE( + "jsonrpc", -1, -1, lgtd_jsonrpc_type_string, false + ), + LGTD_JSONRPC_NODE( + "method", + offsetof(struct lgtd_jsonrpc_request, method), + -1, + lgtd_jsonrpc_type_string, + false + ), + LGTD_JSONRPC_NODE( + "params", + offsetof(struct lgtd_jsonrpc_request, params), + offsetof(struct lgtd_jsonrpc_request, params_ntokens), + lgtd_jsonrpc_type_object_or_array, + true + ), + LGTD_JSONRPC_NODE( + "id", + offsetof(struct lgtd_jsonrpc_request, id), + -1, + lgtd_jsonrpc_type_string_number_or_null, + true + ) + }; + + bool ok = lgtd_jsonrpc_extract_values_from_schema_and_dict( + request, + request_schema, + LGTD_ARRAY_SIZE(request_schema), + tokens, + ntokens, + json + ); + if (!ok) { + return false; + } + + request->request_ntokens = 1 + 2 + 2; // dict itself + jsonrpc + method + if (request->params) { + request->request_ntokens += 1 + request->params_ntokens; + } + if (request->id) { + request->request_ntokens += 2; + } + + return true; +} + +static bool +lgtd_jsonrpc_build_target_list(struct lgtd_proto_target_list *targets, + struct lgtd_client *client, + const jsmntok_t *target, + int target_ntokens) +{ + assert(targets); + assert(client); + assert(target); + + if (target_ntokens < 1) { + return false; + } + + if (lgtd_jsonrpc_type_array(target, client->json)) { + target_ntokens -= 1; + target++; + } else if (target_ntokens != 1) { + return false; + } + + for (int ti = target_ntokens; ti--;) { + int token_len = LGTD_JSONRPC_TOKEN_LEN(&target[ti]); + if (lgtd_jsonrpc_type_string_or_number(&target[ti], client->json)) { + struct lgtd_proto_target *t = malloc(sizeof(*t) + token_len + 1); + if (!t) { + lgtd_warn("can't allocate a new target"); + lgtd_jsonrpc_send_error( + client, LGTD_JSONRPC_INTERNAL_ERROR, "Can't allocate memory" + ); + goto error; + } + memcpy(t->target, client->json + target[ti].start, token_len); + t->target[token_len] = '\0'; + SLIST_INSERT_HEAD(targets, t, link); + } else { + lgtd_debug( + "invalid target value %.*s", + token_len, + client->json + target[ti].start + ); + lgtd_jsonrpc_send_error( + client, LGTD_JSONRPC_INVALID_PARAMS, "Invalid parameters" + ); + goto error; + } + } + + return true; + +error: + lgtd_proto_target_list_clear(targets); + return false; +} + +static void +lgtd_jsonrpc_check_and_call_set_light_from_hsbk(struct lgtd_client *client) +{ + struct lgtd_jsonrpc_set_light_from_hsbk_args { + const jsmntok_t *target; + int target_ntokens; + const jsmntok_t *h; + const jsmntok_t *s; + const jsmntok_t *b; + const jsmntok_t *k; + const jsmntok_t *t; + } params = { NULL, 0, NULL, NULL, NULL, NULL, NULL }; + static const struct lgtd_jsonrpc_node schema[] = { + LGTD_JSONRPC_NODE( + "target", + offsetof(struct lgtd_jsonrpc_set_light_from_hsbk_args, target), + offsetof(struct lgtd_jsonrpc_set_light_from_hsbk_args, target_ntokens), + lgtd_jsonrpc_type_string_number_or_array, + false + ), + LGTD_JSONRPC_NODE( + "hue", + offsetof(struct lgtd_jsonrpc_set_light_from_hsbk_args, h), + -1, + lgtd_jsonrpc_type_float_between_0_and_360, + false + ), + LGTD_JSONRPC_NODE( + "saturation", + offsetof(struct lgtd_jsonrpc_set_light_from_hsbk_args, s), + -1, + lgtd_jsonrpc_type_float_between_0_and_1, + false + ), + LGTD_JSONRPC_NODE( + "brightness", + offsetof(struct lgtd_jsonrpc_set_light_from_hsbk_args, b), + -1, + lgtd_jsonrpc_type_float_between_0_and_1, + false + ), + LGTD_JSONRPC_NODE( + "kelvin", + offsetof(struct lgtd_jsonrpc_set_light_from_hsbk_args, k), + -1, + lgtd_jsonrpc_type_integer, + false + ), + LGTD_JSONRPC_NODE( + "transition", + offsetof(struct lgtd_jsonrpc_set_light_from_hsbk_args, t), + -1, + lgtd_jsonrpc_type_integer, + false + ), + }; + + bool ok = lgtd_jsonrpc_extract_and_validate_params_against_schema( + ¶ms, + schema, + LGTD_ARRAY_SIZE(schema), + client->current_request->params, + client->current_request->params_ntokens, + client->json + ); + if (!ok) { + goto error_invalid_params; + } + + int h = lgtd_jsonrpc_float_range_to_uint16( + &client->json[params.h->start], LGTD_JSONRPC_TOKEN_LEN(params.h), 0, 360 + ); + int s = lgtd_jsonrpc_float_range_to_uint16( + &client->json[params.s->start], LGTD_JSONRPC_TOKEN_LEN(params.s), 0, 1 + ); + int b = lgtd_jsonrpc_float_range_to_uint16( + &client->json[params.b->start], LGTD_JSONRPC_TOKEN_LEN(params.b), 0, 1 + ); + errno = 0; + int k = strtol(&client->json[params.k->start], NULL, 10); + if (k < 2500 || k > 9000 || errno == ERANGE) { + goto error_invalid_params; + } + int t = strtol(&client->json[params.t->start], NULL, 10); + if (t < 0 || errno == ERANGE) { + goto error_invalid_params; + } + + struct lgtd_proto_target_list targets = SLIST_HEAD_INITIALIZER(&targets); + ok = lgtd_jsonrpc_build_target_list( + &targets, client, params.target, params.target_ntokens + ); + if (!ok) { + return; + } + + lgtd_proto_set_light_from_hsbk(client, &targets, h, s, b, k, t); + lgtd_proto_target_list_clear(&targets); + return; + +error_invalid_params: + lgtd_jsonrpc_send_error( + client, LGTD_JSONRPC_INVALID_PARAMS, "Invalid parameters" + ); +} + +static void +lgtd_jsonrpc_check_and_call_set_waveform(struct lgtd_client *client) +{ + struct lgtd_jsonrpc_set_waveform_args { + const jsmntok_t *target; + int target_ntokens; + const jsmntok_t *waveform; + const jsmntok_t *h; + const jsmntok_t *s; + const jsmntok_t *b; + const jsmntok_t *k; + const jsmntok_t *period; + const jsmntok_t *cycles; + const jsmntok_t *skew_ratio; + const jsmntok_t *transient; + } params = { NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }; + static const struct lgtd_jsonrpc_node schema[] = { + LGTD_JSONRPC_NODE( + "target", + offsetof(struct lgtd_jsonrpc_set_waveform_args, target), + offsetof(struct lgtd_jsonrpc_set_waveform_args, target_ntokens), + lgtd_jsonrpc_type_string_number_or_array, + false + ), + LGTD_JSONRPC_NODE( + "waveform", + offsetof(struct lgtd_jsonrpc_set_waveform_args, waveform), + -1, + lgtd_jsonrpc_type_string, + false + ), + LGTD_JSONRPC_NODE( + "hue", + offsetof(struct lgtd_jsonrpc_set_waveform_args, h), + -1, + lgtd_jsonrpc_type_float_between_0_and_360, + false + ), + LGTD_JSONRPC_NODE( + "saturation", + offsetof(struct lgtd_jsonrpc_set_waveform_args, s), + -1, + lgtd_jsonrpc_type_float_between_0_and_1, + false + ), + LGTD_JSONRPC_NODE( + "brightness", + offsetof(struct lgtd_jsonrpc_set_waveform_args, b), + -1, + lgtd_jsonrpc_type_float_between_0_and_1, + false + ), + LGTD_JSONRPC_NODE( + "kelvin", + offsetof(struct lgtd_jsonrpc_set_waveform_args, k), + -1, + lgtd_jsonrpc_type_integer, + false + ), + LGTD_JSONRPC_NODE( + "period", + offsetof(struct lgtd_jsonrpc_set_waveform_args, period), + -1, + lgtd_jsonrpc_type_integer, + false + ), + LGTD_JSONRPC_NODE( + "cycles", + offsetof(struct lgtd_jsonrpc_set_waveform_args, cycles), + -1, + lgtd_jsonrpc_type_integer, + false + ), + LGTD_JSONRPC_NODE( + "skew_ratio", + offsetof(struct lgtd_jsonrpc_set_waveform_args, skew_ratio), + -1, + lgtd_jsonrpc_type_float_between_0_and_1, + false + ), + LGTD_JSONRPC_NODE( + "transient", + offsetof(struct lgtd_jsonrpc_set_waveform_args, transient), + -1, + lgtd_jsonrpc_type_bool, + false + ), + }; + + bool ok = lgtd_jsonrpc_extract_and_validate_params_against_schema( + ¶ms, + schema, + LGTD_ARRAY_SIZE(schema), + client->current_request->params, + client->current_request->params_ntokens, + client->json + ); + if (!ok) { + goto error_invalid_params; + } + + enum lgtd_lifx_waveform_type waveform; + waveform = lgtd_lifx_wire_waveform_string_id_to_type( + &client->json[params.waveform->start], LGTD_JSONRPC_TOKEN_LEN(params.waveform) + ); + if (waveform == LGTD_LIFX_WAVEFORM_INVALID) { + goto error_invalid_params; + } + + int h = lgtd_jsonrpc_float_range_to_uint16( + &client->json[params.h->start], LGTD_JSONRPC_TOKEN_LEN(params.h), 0, 360 + ); + int s = lgtd_jsonrpc_float_range_to_uint16( + &client->json[params.s->start], LGTD_JSONRPC_TOKEN_LEN(params.s), 0, 1 + ); + int b = lgtd_jsonrpc_float_range_to_uint16( + &client->json[params.b->start], LGTD_JSONRPC_TOKEN_LEN(params.b), 0, 1 + ); + errno = 0; + int k = strtol(&client->json[params.k->start], NULL, 10); + if (k < 2500 || k > 9000 || errno == ERANGE) { + goto error_invalid_params; + } + int period = strtol(&client->json[params.period->start], NULL, 10); + if (period <= 0 || errno == ERANGE) { + goto error_invalid_params; + } + int cycles = strtol(&client->json[params.cycles->start], NULL, 10); + if (cycles <= 0 || errno == ERANGE) { + goto error_invalid_params; + } + int skew_ratio = lgtd_jsonrpc_float_range_to_uint16( + &client->json[params.skew_ratio->start], + LGTD_JSONRPC_TOKEN_LEN(params.skew_ratio), + 0, 1 + ); + skew_ratio -= UINT16_MAX / 2; + bool transient = client->json[params.transient->start] == 't'; + + struct lgtd_proto_target_list targets = SLIST_HEAD_INITIALIZER(&targets); + ok = lgtd_jsonrpc_build_target_list( + &targets, client, params.target, params.target_ntokens + ); + if (!ok) { + return; + } + + lgtd_proto_set_waveform( + client, &targets, + waveform, h, s, b, k, + period, cycles, skew_ratio, transient + ); + lgtd_proto_target_list_clear(&targets); + return; + +error_invalid_params: + lgtd_jsonrpc_send_error( + client, LGTD_JSONRPC_INVALID_PARAMS, "Invalid parameters" + ); +} + +static bool +lgtd_jsonrpc_extract_target_list(struct lgtd_proto_target_list *targets, + struct lgtd_client *client) +{ + struct lgtd_jsonrpc_target_args { + const jsmntok_t *target; + int target_ntokens; + } params = { NULL, 0 }; + static const struct lgtd_jsonrpc_node schema[] = { + LGTD_JSONRPC_NODE( + "target", + offsetof(struct lgtd_jsonrpc_target_args, target), + offsetof(struct lgtd_jsonrpc_target_args, target_ntokens), + lgtd_jsonrpc_type_string_number_or_array, + false + ) + }; + + struct lgtd_jsonrpc_request *req = client->current_request; + bool ok = lgtd_jsonrpc_extract_and_validate_params_against_schema( + ¶ms, schema, 1, req->params, req->params_ntokens, client->json + ); + if (!ok) { + lgtd_jsonrpc_send_error( + client, LGTD_JSONRPC_INVALID_PARAMS, "Invalid parameters" + ); + return false; + } + + return lgtd_jsonrpc_build_target_list( + targets, client, params.target, params.target_ntokens + ); +} + +#define CHECK_AND_CALL_TARGETS_ONLY_METHOD(proto_method) \ +static void \ +lgtd_jsonrpc_check_and_call_##proto_method(struct lgtd_client *client) \ +{ \ + struct lgtd_proto_target_list targets = SLIST_HEAD_INITIALIZER(&targets); \ + bool ok = lgtd_jsonrpc_extract_target_list(&targets, client); \ + if (!ok) { \ + return; \ + } \ + \ + lgtd_proto_##proto_method(client, &targets); \ + lgtd_proto_target_list_clear(&targets); \ +} + +CHECK_AND_CALL_TARGETS_ONLY_METHOD(power_on); +CHECK_AND_CALL_TARGETS_ONLY_METHOD(power_off); +CHECK_AND_CALL_TARGETS_ONLY_METHOD(power_toggle); +CHECK_AND_CALL_TARGETS_ONLY_METHOD(get_light_state); + +static void +lgtd_jsonrpc_check_and_call_proto_tag_or_untag_or_set_label( + struct lgtd_client *client, + void (*lgtd_proto_fn)(struct lgtd_client *, + const struct lgtd_proto_target_list *, + const char *)) + +{ + struct lgtd_jsonrpc_target_args { + const jsmntok_t *target; + int target_ntokens; + const jsmntok_t *label; + } params = { NULL, 0, NULL }; + static const struct lgtd_jsonrpc_node schema[] = { + LGTD_JSONRPC_NODE( + "target", + offsetof(struct lgtd_jsonrpc_target_args, target), + offsetof(struct lgtd_jsonrpc_target_args, target_ntokens), + lgtd_jsonrpc_type_string_number_or_array, + false + ), + LGTD_JSONRPC_NODE( + "label", + offsetof(struct lgtd_jsonrpc_target_args, label), + -1, + lgtd_jsonrpc_type_string, + false + ) + }; + + struct lgtd_jsonrpc_request *req = client->current_request; + bool ok = lgtd_jsonrpc_extract_and_validate_params_against_schema( + ¶ms, + schema, + LGTD_ARRAY_SIZE(schema), + req->params, + req->params_ntokens, + client->json + ); + if (!ok) { + lgtd_jsonrpc_send_error( + client, LGTD_JSONRPC_INVALID_PARAMS, "Invalid parameters" + ); + return; + } + + struct lgtd_proto_target_list targets = SLIST_HEAD_INITIALIZER(&targets); + ok = lgtd_jsonrpc_build_target_list( + &targets, client, params.target, params.target_ntokens + ); + if (!ok) { + return; + } + + char *label = strndup( + &client->json[params.label->start], LGTD_JSONRPC_TOKEN_LEN(params.label) + ); + if (!label) { + lgtd_warn("can't allocate a label"); + lgtd_jsonrpc_send_error( + client, LGTD_JSONRPC_INTERNAL_ERROR, "Can't allocate memory" + ); + goto error_strdup; + } + + lgtd_proto_fn(client, &targets, label); + + free(label); + +error_strdup: + lgtd_proto_target_list_clear(&targets); +} + +static void +lgtd_jsonrpc_check_and_call_tag(struct lgtd_client *client) +{ + return lgtd_jsonrpc_check_and_call_proto_tag_or_untag_or_set_label( + client, lgtd_proto_tag + ); +} + +static void +lgtd_jsonrpc_check_and_call_untag(struct lgtd_client *client) +{ + return lgtd_jsonrpc_check_and_call_proto_tag_or_untag_or_set_label( + client, lgtd_proto_untag + ); +} + +static void +lgtd_jsonrpc_check_and_call_set_label(struct lgtd_client *client) +{ + return lgtd_jsonrpc_check_and_call_proto_tag_or_untag_or_set_label( + client, lgtd_proto_set_label + ); +} + +static void +lgtd_jsonrpc_batch_prepare_next_part(struct lgtd_client *client, + const int *batch_sent) +{ + assert(client); + + if (batch_sent) { + if (*batch_sent == 1) { + lgtd_client_write_string(client, "["); + } else if (*batch_sent) { + lgtd_client_write_string(client, ","); + } + } +} + +static int +lgtd_jsonrpc_dispatch_one(struct lgtd_client *client, + const jsmntok_t *tokens, + int ntokens, + int *batch_sent) +{ + static const struct lgtd_jsonrpc_method methods[] = { + LGTD_JSONRPC_METHOD( + "power_on", 1, // t + lgtd_jsonrpc_check_and_call_power_on + ), + LGTD_JSONRPC_METHOD( + "power_off", 1, // t + lgtd_jsonrpc_check_and_call_power_off + ), + LGTD_JSONRPC_METHOD( + "power_toggle", 1, // t + lgtd_jsonrpc_check_and_call_power_toggle + ), + LGTD_JSONRPC_METHOD( + "set_light_from_hsbk", 6, // t, h, s, b, k, t + lgtd_jsonrpc_check_and_call_set_light_from_hsbk + ), + LGTD_JSONRPC_METHOD( + // t, waveform, h, s, b, k, period, cycles, skew_ratio, transient + "set_waveform", 10, + lgtd_jsonrpc_check_and_call_set_waveform + ), + LGTD_JSONRPC_METHOD( + "get_light_state", 1, // t + lgtd_jsonrpc_check_and_call_get_light_state + ), + LGTD_JSONRPC_METHOD( + "tag", 2, // t, tag + lgtd_jsonrpc_check_and_call_tag + ), + LGTD_JSONRPC_METHOD( + "untag", 2, // t, tag + lgtd_jsonrpc_check_and_call_untag + ), + LGTD_JSONRPC_METHOD( + "set_label", 2, // t, label + lgtd_jsonrpc_check_and_call_set_label + ) + }; + + if (batch_sent) { + ++*batch_sent; + } + + enum lgtd_jsonrpc_error_code error_code; + const char *error_msg; + + struct lgtd_jsonrpc_request request; + memset(&request, 0, sizeof(request)); + bool ok = lgtd_jsonrpc_check_and_extract_request( + &request, tokens, ntokens, client->json + ); + client->current_request = &request; + if (!ok) { + error_code = LGTD_JSONRPC_INVALID_REQUEST; + error_msg = "Invalid request"; + request.request_ntokens = lgtd_jsonrpc_consume_object_or_array( + tokens, 0, ntokens, client->json + ); + goto error; + } + + assert(request.method); + assert(request.request_ntokens); + + for (int i = 0; i != LGTD_ARRAY_SIZE(methods); i++) { + int parsed_method_namelen = LGTD_JSONRPC_TOKEN_LEN(request.method); + if (parsed_method_namelen != methods[i].namelen) { + continue; + } + int diff = memcmp( + methods[i].name, &client->json[request.method->start], methods[i].namelen + ); + if (!diff) { + int params_count = request.params ? request.params->size : 0; + if (params_count != methods[i].params_count) { + error_code = LGTD_JSONRPC_INVALID_PARAMS; + error_msg = "Invalid number of parameters"; + goto error; + } + struct bufferevent *client_io = NULL; // keep compilers happy... + if (!request.id) { + // Ugly hack to behave correctly on jsonrpc notifications, it's + // not worth doing it properly right now. It is especially ugly + // since we can't properly close that client now (but we don't + // do that in lgtd_proto and signals are deferred with the + // event loop). + client_io = client->io; + client->io = NULL; + if (batch_sent) { + --*batch_sent; + } + } else { + lgtd_jsonrpc_batch_prepare_next_part(client, batch_sent); + } + methods[i].method(client); + if (!request.id) { + client->io = client_io; + } + client->current_request = NULL; + return request.request_ntokens; + } + } + + error_code = LGTD_JSONRPC_METHOD_NOT_FOUND; + error_msg = "Method not found"; + +error: + lgtd_jsonrpc_batch_prepare_next_part(client, batch_sent); + lgtd_jsonrpc_send_error(client, error_code, error_msg); + client->current_request = NULL; + return request.request_ntokens; +} + +void +lgtd_jsonrpc_dispatch_request(struct lgtd_client *client, int parsed) +{ + assert(client); + assert(parsed >= 0); + + if (!parsed || !client->jsmn_tokens[0].size) { + lgtd_jsonrpc_send_error( + client, LGTD_JSONRPC_INVALID_REQUEST, "Invalid request" + ); + return; + } + + if (!lgtd_jsonrpc_type_array(client->jsmn_tokens, client->json)) { + lgtd_jsonrpc_dispatch_one(client, client->jsmn_tokens, parsed, NULL); + return; + } + + int batch_sent = 0; + for (int ti = 1; ti < parsed;) { + const jsmntok_t *tok = &client->jsmn_tokens[ti]; + + if (lgtd_jsonrpc_type_object(tok, client->json)) { + ti += lgtd_jsonrpc_dispatch_one( + client, tok, parsed - ti, &batch_sent + ); + } else { + batch_sent++; + lgtd_jsonrpc_batch_prepare_next_part(client, &batch_sent); + lgtd_jsonrpc_send_error( + client, LGTD_JSONRPC_INVALID_REQUEST, "Invalid request" + ); + if (lgtd_jsonrpc_type_array(tok, client->json)) { + ti = lgtd_jsonrpc_consume_object_or_array( + client->jsmn_tokens, ti, parsed, client->json + ); + } else { + ti++; + } + } + } + + if (batch_sent) { + lgtd_client_write_string(client, "]"); + } +} diff --git a/core/jsonrpc.h b/core/jsonrpc.h new file mode 100644 index 0000000..8bb2417 --- /dev/null +++ b/core/jsonrpc.h @@ -0,0 +1,96 @@ +// Copyright (c) 2015, Louis Opter +// +// This file is part of lighstd. +// +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . + +#pragma once + +struct lgtd_client; + +enum { LGTD_JSONRPC_FLOAT_PREC = (int)10E5 }; + +struct lgtd_jsonrpc_request { + const jsmntok_t *method; + const jsmntok_t *params; + int params_ntokens; + const jsmntok_t *id; + int request_ntokens; +}; + +struct lgtd_jsonrpc_node { + const char *key; + int keylen; + int value_offset; + int ntokens_offset; + bool (*type_cmp)(const jsmntok_t *, const char *); + bool optional; +}; + +#define LGTD_JSONRPC_SET_JSMNTOK(object, value_offset, value) do { \ + *(const jsmntok_t **)(&(((char *)(object))[value_offset])) = (value); \ +} while (0) + +#define LGTD_JSONRPC_GET_JSMNTOK(object, value_offset) \ + *(const jsmntok_t **)(&((char *)(object))[value_offset]); + +#define LGTD_JSONRPC_SET_NTOKENS(object, ntokens_offset, ntokens) do { \ + *(int *)(&(((char *)(object))[ntokens_offset])) = (ntokens); \ +} while (0) + +#define LGTD_JSONRPC_NODE(key_, value_offset_, ntokens_offset_, fn_type_cmp, optional_) { \ + .key = (key_), \ + .keylen = sizeof((key_)) - 1, \ + .value_offset = (value_offset_), \ + .ntokens_offset = (ntokens_offset_), \ + .type_cmp = (fn_type_cmp), \ + .optional = (optional_) \ +} + +#define LGTD_JSONRPC_TOKEN_LEN(t) ((t)->end - (t)->start) + +struct lgtd_jsonrpc_method { + const char *name; + int namelen; + int params_count; + void (*method)(struct lgtd_client *); +}; + +#define LGTD_JSONRPC_METHOD(name_, params_count_, method_) { \ + .name = (name_), \ + .namelen = sizeof((name_)) -1, \ + .params_count = (params_count_), \ + .method = (method_) \ +} + +enum lgtd_jsonrpc_error_code { + LGTD_JSONRPC_SUCCESS = 0, + LGTD_JSONRPC_PARSE_ERROR = -32700, + LGTD_JSONRPC_INVALID_REQUEST = -32600, + LGTD_JSONRPC_METHOD_NOT_FOUND = -32601, + LGTD_JSONRPC_INVALID_PARAMS = -32602, + LGTD_JSONRPC_INTERNAL_ERROR = -32603, + LGTD_JSONRPC_SERVER_ERROR = -32000 // (to -32099) +}; + +void lgtd_jsonrpc_dispatch_request(struct lgtd_client *, int); + +void lgtd_jsonrpc_send_error(struct lgtd_client *, + enum lgtd_jsonrpc_error_code, + const char *); +void lgtd_jsonrpc_send_response(struct lgtd_client *, + const char *); +void lgtd_jsonrpc_start_send_response(struct lgtd_client *); +void lgtd_jsonrpc_end_send_response(struct lgtd_client *); +void lgtd_jsonrpc_uint16_range_to_float_string(uint16_t, int, int, char *, int); diff --git a/core/lifxd.c b/core/lifxd.c deleted file mode 100644 index 9fe1677..0000000 --- a/core/lifxd.c +++ /dev/null @@ -1,219 +0,0 @@ -// Copyright (c) 2014, Louis Opter -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// 3. Neither the name of the copyright holder nor the names of its contributors -// may be used to endorse or promote products derived from this software -// without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include "wire_proto.h" -#include "time_monotonic.h" -#include "bulb.h" -#include "gateway.h" -#include "broadcast.h" -#include "version.h" -#include "timer.h" -#include "lifxd.h" - -struct lifxd_opts lifxd_opts = { - .foreground = false, - .log_timestamps = true, - .master_port = 56700, - .verbosity = LIFXD_DEBUG -}; - -struct event_base *lifxd_ev_base = NULL; - -void -lifxd_cleanup(void) -{ - lifxd_timer_close(); - lifxd_broadcast_close(); - lifxd_gateway_close_all(); - event_base_free(lifxd_ev_base); -#if LIBEVENT_VERSION_NUMBER >= 0x02010100 - libevent_global_shutdown(); -#endif -} - -short -lifxd_sockaddrport(const struct sockaddr_storage *peer) -{ - assert(peer); - - if (peer->ss_family == AF_INET) { - const struct sockaddr_in *in_peer = (const struct sockaddr_in *)peer; - return ntohs(in_peer->sin_port); - } else { - const struct sockaddr_in6 *in6_peer = (const struct sockaddr_in6 *)peer; - return ntohs(in6_peer->sin6_port); - } -} - -static void -lifxd_signal_event_callback(int signum, short events, void *ctx) -{ - assert(ctx); - - lifxd_info( - "received signal %d (%s), exiting...", signum, strsignal(signum) - ); - event_del((struct event *)ctx); // restore default behavior - event_base_loopbreak(lifxd_ev_base); - (void)events; -} - -static void -lifxd_configure_libevent(void) -{ - lifxd_gateway_close_all(); - event_set_log_callback(lifxd_libevent_log); - lifxd_ev_base = event_base_new(); -} - -static void -lifxd_configure_signal_handling(void) -{ - const int signals[] = {SIGINT, SIGTERM, SIGQUIT}; - static struct event sigevs[LIFXD_ARRAY_SIZE(signals)]; - - for (int i = 0; i != LIFXD_ARRAY_SIZE(signals); i++) { - evsignal_assign( - &sigevs[i], - lifxd_ev_base, - signals[i], - lifxd_signal_event_callback, - &sigevs[i] - ); - evsignal_add(&sigevs[i], NULL); - } -} - -static void -lifxd_usage(const char *progname) -{ - printf( - "Usage: %s [-p master_bulb_port] " - "[-v debug|info|warning|error] [-f] [-t] [-h] [-V]\n", - progname - ); - exit(0); -} - -int -main(int argc, char *argv[]) -{ - static const struct option long_opts[] = { - {"foreground", no_argument, NULL, 'f'}, - {"no-timestamps", no_argument, NULL, 't'}, - {"help", no_argument, NULL, 'h'}, - {"master-port", required_argument, NULL, 'p'}, - {"verbosity", required_argument, NULL, 'v'}, - {"version", no_argument, NULL, 'V'}, - {NULL, 0, NULL, 0} - }; - const char short_opts[] = "fthp:v:V"; - - for (int rv = getopt_long(argc, argv, short_opts, long_opts, NULL); - rv != -1; - rv = getopt_long(argc, argv, short_opts, long_opts, NULL)) { - switch (rv) { - case 'f': - lifxd_opts.foreground = true; - break; - case 't': - lifxd_opts.log_timestamps = false; - break; - case 'h': - lifxd_usage(argv[0]); - case 'p': - errno = 0; - long port = strtol(optarg, NULL, 10); - if (!errno && port <= UINT16_MAX && port > 0) { - lifxd_opts.master_port = port; - break; - } - lifxd_errx( - 1, "The master port must be between 1 and %d", UINT16_MAX - ); - case 'v': - for (int i = 0;;) { - const char *verbose_levels[] = { - "debug", "info", "warning", "error" - }; - if (!strcasecmp(optarg, verbose_levels[i])) { - lifxd_opts.verbosity = i; - break; - } - if (++i == LIFXD_ARRAY_SIZE(verbose_levels)) { - lifxd_errx(1, "Unknown verbosity level: %s", optarg); - } - } - break; - case 'V': - printf("%s v%s\n", argv[0], LIFXD_VERSION); - return 0; - default: - lifxd_usage(argv[0]); - } - } - - argc -= optind; - argv += optind; - - lifxd_configure_libevent(); - lifxd_configure_signal_handling(); - - lifxd_wire_load_packet_infos_map(); - if (!lifxd_timer_setup() || !lifxd_broadcast_setup()) { - lifxd_err(1, "can't setup lifxd"); - } - - lifxd_timer_start_discovery(); - - event_base_dispatch(lifxd_ev_base); - - lifxd_cleanup(); - - return 0; -} diff --git a/core/lifxd.h b/core/lifxd.h deleted file mode 100644 index 7c26ef6..0000000 --- a/core/lifxd.h +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) 2014, Louis Opter -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// 3. Neither the name of the copyright holder nor the names of its contributors -// may be used to endorse or promote products derived from this software -// without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. - -#pragma once - -#ifndef __attribute__ -# define __atttribute__(e) -#endif - -#define LIFXD_ABS(v) ((v) >= 0 ? (v) : (v) * -1) -#define LIFXD_ARRAY_SIZE(a) (sizeof(a) / sizeof(a[0])) -#define LIFXD_MSECS_TO_TIMEVAL(v) { \ - .tv_sec = (v) / 1000, \ - .tv_usec = ((v) % 1000) * 1000 \ -} - -enum lifxd_verbosity { - LIFXD_DEBUG = 0, - LIFXD_INFO, - LIFXD_WARN, - LIFXD_ERR -}; - -enum { LIFXD_ERROR_MSG_BUFSIZE = 2048 }; - -struct lifxd_opts { - bool foreground; - bool log_timestamps; - uint16_t master_port; - enum lifxd_verbosity verbosity; -}; - -extern struct lifxd_opts lifxd_opts; -extern struct event_base *lifxd_ev_base; - -const char *lifxd_addrtoa(const uint8_t *); -void lifxd_sockaddrtoa(const struct sockaddr_storage *, char *buf, int buflen); -short lifxd_sockaddrport(const struct sockaddr_storage *); - -void _lifxd_err(void (*)(int, const char *, ...), int, const char *, ...) - __attribute__((format(printf, 3, 4))); -#define lifxd_err(eval, fmt, ...) _lifxd_err(err, (eval), (fmt), ##__VA_ARGS__); -#define lifxd_errx(eval, fmt, ...) _lifxd_err(errx, (eval), (fmt), ##__VA_ARGS__); -void _lifxd_warn(void (*)(const char *, va_list), const char *, ...) - __attribute__((format(printf, 2, 3))); -#define lifxd_warn(fmt, ...) _lifxd_warn(vwarn, (fmt), ##__VA_ARGS__); -#define lifxd_warnx(fmt, ...) _lifxd_warn(vwarnx, (fmt), ##__VA_ARGS__); -void lifxd_info(const char *, ...) __attribute__((format(printf, 1, 2))); -void lifxd_debug(const char *, ...) __attribute__((format(printf, 1, 2))); -void lifxd_libevent_log(int, const char *); - -void lifxd_cleanup(void); diff --git a/core/lightsd.c b/core/lightsd.c new file mode 100644 index 0000000..af61b70 --- /dev/null +++ b/core/lightsd.c @@ -0,0 +1,374 @@ +// Copyright (c) 2014, Louis Opter +// +// This file is part of lighstd. +// +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "lifx/wire_proto.h" +#include "time_monotonic.h" +#include "lifx/bulb.h" +#include "lifx/gateway.h" +#include "lifx/broadcast.h" +#include "lifx/discovery.h" +#include "version.h" +#include "jsmn.h" +#include "jsonrpc.h" +#include "client.h" +#include "pipe.h" +#include "timer.h" +#include "listen.h" +#include "daemon.h" +#include "lightsd.h" + +struct lgtd_opts lgtd_opts = { + .foreground = true, + .log_timestamps = true, +#ifndef NDEBUG + .verbosity = LGTD_INFO, +#else + .verbosity = LGTD_WARN, +#endif + .user = NULL, + .group = NULL, + .syslog = false, + .syslog_facility = LOG_DAEMON, + .syslog_ident = "lightsd", + .pidfile = NULL +}; + +struct event_base *lgtd_ev_base = NULL; + +static int lgtd_last_signal_received = 0; + +void +lgtd_cleanup(void) +{ + lgtd_lifx_discovery_close(); + lgtd_listen_close_all(); + lgtd_command_pipe_close_all(); + lgtd_client_close_all(); + lgtd_lifx_broadcast_close(); + lgtd_lifx_gateway_close_all(); + lgtd_timer_stop_all(); + event_base_free(lgtd_ev_base); +#if LIBEVENT_VERSION_NUMBER >= 0x02010100 + libevent_global_shutdown(); +#endif + if (lgtd_opts.pidfile) { + unlink(lgtd_opts.pidfile); + } +} + +static void +lgtd_signal_event_callback(int signum, short events, void *ctx) +{ + assert(ctx); + + // NOTE: syslog isn't signal safe, don't log anything in this function. + + lgtd_last_signal_received = signum; + event_del((struct event *)ctx); // restore default behavior + event_base_loopbreak(lgtd_ev_base); + (void)events; +} + +static void +lgtd_libevent_log(int severity, const char *msg) +{ + switch (severity) { + case EVENT_LOG_DEBUG: lgtd_debug("%s", msg); break; + case EVENT_LOG_MSG: lgtd_info("%s", msg); break; + case EVENT_LOG_WARN: lgtd_warnx("%s", msg); break; + case EVENT_LOG_ERR: lgtd_warnx("%s", msg); break; + default: break; + } +} + +static void +lgtd_configure_libevent(void) +{ + event_set_log_callback(lgtd_libevent_log); + lgtd_ev_base = event_base_new(); +} + +static void +lgtd_configure_signal_handling(void) +{ + const int signals[] = {SIGINT, SIGTERM, SIGQUIT}; + static struct event sigevs[LGTD_ARRAY_SIZE(signals)]; + + for (int i = 0; i != LGTD_ARRAY_SIZE(signals); i++) { + evsignal_assign( + &sigevs[i], + lgtd_ev_base, + signals[i], + lgtd_signal_event_callback, + &sigevs[i] + ); + evsignal_add(&sigevs[i], NULL); + } + + struct sigaction act = { .sa_handler = SIG_IGN }; + if (sigaction(SIGPIPE, &act, NULL)) { + lgtd_err(1, "can't configure signal handling"); + } +} + +static void +lgtd_usage(const char *progname) +{ + printf( +"Usage: %s ...\n\n" +" [-l,--listen addr:port] Listen for JSON-RPC commands over TCP at\n" +" this address (can be repeated).\n" +" [-c,--command-pipe /command/fifo] Open an unidirectional JSON-RPC\n" +" command pipe at this location (can be\n" +" repeated).\n" +" [-s,--socket /unix/socket] Open an Unix socket at this location\n" +" (can be repeated).\n" +" [-d,--daemonize] Fork in the background.\n" +" [-p,--pidfile /path/to/pid.file] Write lightsd's pid in the given file.\n" +" [-u,--user user] Drop privileges to this user (and the\n" +" group of this user if -g is missing).\n" +" [-g,--group group] Drop privileges to this group (-g requires\n" +" the -u option to be used).\n" +" [-S,--syslog] Divert logging from the console to syslog.\n" +" [-F,--syslog-facility] Facility to use with syslog (defaults to\n" +" daemon, other possible values are user and\n" +" local0-7, see syslog(3)).\n" +" [-I,--syslog-ident] Identifier to use with syslog (defaults to\n" +" lightsd).\n" +" [-t,--no-timestamps] Disable timestamps in the console logs.\n" +" [-h,--help] Display this.\n" +" [-V,--version] Display version and build information.\n" +" [-v,--verbosity debug|info|warning|error]\n" +"\nor,\n\n" +" --prefix Display the install prefix for lightsd.\n" +"\nor,\n\n" +" --rundir Display the runtime directory for lightsd.\n", + progname + ); + lgtd_cleanup(); + exit(0); +} + +int +main(int argc, char *argv[], char *envp[]) +{ + char progname[32] = { 0 }; + memcpy(progname, argv[0], LGTD_MIN(sizeof(progname) - 1, strlen(argv[0]))); + + lgtd_daemon_setup_proctitle(argc, argv, envp); + + lgtd_configure_libevent(); + lgtd_configure_signal_handling(); + + static const struct option long_opts[] = { + {"listen", required_argument, NULL, 'l'}, + {"command-pipe", required_argument, NULL, 'c'}, + {"socket", required_argument, NULL, 's'}, + {"foreground", no_argument, NULL, 'f'}, + {"daemonize", no_argument, NULL, 'd'}, + {"pidfile", required_argument, NULL, 'p'}, + {"user", required_argument, NULL, 'u'}, + {"group", required_argument, NULL, 'g'}, + {"syslog", no_argument, NULL, 'S'}, + {"syslog-facility", required_argument, NULL, 'F'}, + {"syslog-ident", required_argument, NULL, 'I'}, + {"no-timestamps", no_argument, NULL, 't'}, + {"help", no_argument, NULL, 'h'}, + {"verbosity", required_argument, NULL, 'v'}, + {"version", no_argument, NULL, 'V'}, + {"prefix", no_argument, NULL, 'P'}, + {"rundir", no_argument, NULL, 'r'}, + {NULL, 0, NULL, 0} + }; + const char short_opts[] = "l:c:s:fdp:u:g:SF:I:thv:V"; + + if (argc == 1) { + lgtd_usage(progname); + } + + for (int rv = getopt_long(argc, argv, short_opts, long_opts, NULL); + rv != -1; + rv = getopt_long(argc, argv, short_opts, long_opts, NULL)) { + switch (rv) { + case 'l': + (void)0; + char *sep = strrchr(optarg, ':'); + if (!sep || !sep[1]) { + lgtd_usage(progname); + } + *sep = '\0'; + if (!lgtd_listen_open(optarg, sep + 1)) { + exit(1); + } + break; + case 'c': + if (!lgtd_command_pipe_open(optarg)) { + exit(1); + } + break; + case 's': + if (!lgtd_listen_unix_open(optarg)) { + exit(1); + } + break; + case 'f': + lgtd_opts.foreground = true; + break; + case 'd': + lgtd_opts.foreground = false; + break; + case 'p': + lgtd_opts.pidfile = optarg; + break; + case 'u': + lgtd_opts.user = optarg; + break; + case 'g': + lgtd_opts.group = optarg; + break; + case 'S': + lgtd_opts.syslog = true; + break; + case 'F': + lgtd_opts.syslog_facility = lgtd_daemon_syslog_facilitytoi(optarg); + break; + case 'I': + lgtd_opts.syslog_ident = optarg; + break; + case 't': + lgtd_opts.log_timestamps = false; + break; + case 'h': + lgtd_usage(progname); + case 'v': + for (int i = 0;;) { + const char *verbose_levels[] = { + "debug", "info", "warning", "error" + }; + if (!strcasecmp(optarg, verbose_levels[i])) { + lgtd_opts.verbosity = i; + break; + } + if (++i == LGTD_ARRAY_SIZE(verbose_levels)) { + lgtd_errx(1, "Unknown verbosity level: %s", optarg); + } + } + break; + case 'V': + printf("%s %s\n", progname, LGTD_VERSION); + lgtd_cleanup(); + return 0; + case 'P': + printf( + "%s%s\n", LGTD_INSTALL_PREFIX, LGTD_INSTALL_PREFIX[ + LGTD_ARRAY_SIZE(LGTD_INSTALL_PREFIX) - 1 + ] != '/' ? "/" : "" + ); + return 0; + case 'r': + printf( + "%s%s\n", LGTD_RUNTIME_DIRECTORY, LGTD_RUNTIME_DIRECTORY[ + LGTD_ARRAY_SIZE(LGTD_RUNTIME_DIRECTORY) - 1 + ] != '/' ? "/" : "" + ); + return 0; + default: + lgtd_usage(progname); + } + } + + argc -= optind; + argv += optind; + + // Ideally we should parse the syslog relation options first and call that + // before anything can be logged: + lgtd_log_setup(); + + if (lgtd_opts.user) { + lgtd_daemon_set_user(lgtd_opts.user); + lgtd_daemon_set_group(lgtd_opts.group); + // create the pidfile before we drop privileges: + if (lgtd_opts.pidfile + && !lgtd_daemon_write_pidfile(lgtd_opts.pidfile)) { + lgtd_warn("couldn't write pidfile at %s", lgtd_opts.pidfile); + } + lgtd_daemon_drop_privileges(); + } else if (lgtd_opts.group) { + lgtd_errx(1, "please, specify an user with the -u option"); + } + + lgtd_daemon_die_if_running_as_root_unless_requested(lgtd_opts.user); + + lgtd_lifx_wire_setup(); + if (!lgtd_lifx_discovery_setup() || !lgtd_lifx_broadcast_setup()) { + lgtd_err(1, "can't setup lightsd"); + } + + if (!lgtd_opts.foreground) { + lgtd_info("forking into the background now..."); + if (!lgtd_daemon_unleash()) { + lgtd_err(1, "can't fork to the background"); + } + } + + // update the pidfile after we've forked: + if (lgtd_opts.pidfile && !lgtd_daemon_write_pidfile(lgtd_opts.pidfile)) { + lgtd_warn("couldn't write pidfile at %s", lgtd_opts.pidfile); + } + + lgtd_lifx_discovery_start(); + + // update at least once: so that if no bulbs are discovered we still get a + // clear status line. + lgtd_daemon_update_proctitle(); + + event_base_dispatch(lgtd_ev_base); + + if (lgtd_last_signal_received) { + lgtd_info( + "received signal %d (%s), exiting...", + lgtd_last_signal_received, strsignal(lgtd_last_signal_received) + ); + } + + lgtd_cleanup(); + + return 0; +} diff --git a/core/lightsd.h b/core/lightsd.h new file mode 100644 index 0000000..616e002 --- /dev/null +++ b/core/lightsd.h @@ -0,0 +1,149 @@ +// Copyright (c) 2014, Louis Opter +// +// This file is part of lighstd. +// +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . + +// reallocarray has been copied from OpenBSD under the following license: +/* + * Copyright (c) 2008 Otto Moerbeek + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#pragma once + +#ifndef __attribute__ +# define __atttribute__(e) +#endif + +#define LGTD_ABS(v) ((v) >= 0 ? (v) : (v) * -1) +#define LGTD_MIN(a, b) ((a) < (b) ? (a) : (b)) +#define LGTD_MAX(a, b) ((a) > (b) ? (a) : (b)) +#define LGTD_ARRAY_SIZE(a) (sizeof(a) / sizeof(a[0])) +#define LGTD_MSECS_TO_TIMEVAL(v) { \ + .tv_sec = (v) / 1000, \ + .tv_usec = ((v) % 1000) * 1000 \ +} +#define LGTD_NSECS_TO_USECS(v) ((v) / (unsigned int)1E6) +#define LGTD_NSECS_TO_SECS(v) ((v) / (unsigned int)1E9) +#define LGTD_SECS_TO_NSECS(v) ((v) * (unsigned int)1E9) +#define LGTD_TM_TO_ISOTIME(tm, sbuf, bufsz, usec) do { \ + /* '2015-01-02T10:13:16.132222+00:00' */ \ + if ((usec)) { \ + snprintf( \ + (sbuf), (bufsz), "%d-%02d-%02dT%02d:%02d:%02d.%jd%c%02ld:%02ld", \ + 1900 + (tm)->tm_year, 1 + (tm)->tm_mon, (tm)->tm_mday, \ + (tm)->tm_hour, (tm)->tm_min, (tm)->tm_sec, (intmax_t)usec, \ + (tm)->tm_gmtoff >= 0 ? '+' : '-', /* %+02ld doesn't work */ \ + LGTD_ABS((tm)->tm_gmtoff / 60 / 60), (tm)->tm_gmtoff % (60 * 60) \ + ); \ + } else { \ + snprintf( \ + (sbuf), (bufsz), "%d-%02d-%02dT%02d:%02d:%02d%c%02ld:%02ld", \ + 1900 + (tm)->tm_year, 1 + (tm)->tm_mon, (tm)->tm_mday, \ + (tm)->tm_hour, (tm)->tm_min, (tm)->tm_sec, \ + (tm)->tm_gmtoff >= 0 ? '+' : '-', /* %+02ld doesn't work */ \ + LGTD_ABS((tm)->tm_gmtoff / 60 / 60), (tm)->tm_gmtoff % (60 * 60) \ + ); \ + } \ +} while (0) +#define LGTD_SNPRINTF_APPEND(buf, i, bufsz, ...) do { \ + int n = snprintf(&(buf)[(i)], bufsz - i, __VA_ARGS__); \ + (i) = LGTD_MIN((i) + n, bufsz); \ +} while (0) + +enum lgtd_verbosity { + LGTD_DEBUG = 0, + LGTD_INFO, + LGTD_WARN, + LGTD_ERR +}; + +enum { LGTD_ERROR_MSG_BUFSIZE = 2048 }; + +// FIXME: introspect sizeof(sockaddr_un.sun_path) with CMake to generate a +// reasonable value for that: +enum { LGTD_SOCKADDR_STRLEN = 128 }; + +#if !LGTD_HAVE_REALLOCARRAY +# define MUL_NO_OVERFLOW ((size_t)1 << (sizeof(size_t) * 4)) +static inline void * +reallocarray(void *optr, size_t nmemb, size_t size) +{ + if ((nmemb >= MUL_NO_OVERFLOW || size >= MUL_NO_OVERFLOW) && + nmemb > 0 && SIZE_MAX / nmemb < size) { +# ifndef NDEBUG + abort(); +# endif + return NULL; + } + return realloc(optr, size * nmemb); +} +#endif + +struct sockaddr; + +struct lgtd_opts { + bool foreground; + bool log_timestamps; + enum lgtd_verbosity verbosity; + const char *user; + const char *group; + bool syslog; + int syslog_facility; + const char *syslog_ident; + const char *pidfile; +}; + +extern struct lgtd_opts lgtd_opts; +extern struct event_base *lgtd_ev_base; +extern const char *lgtd_progname; + +char *lgtd_iee8023mactoa(const uint8_t *addr, char *buf, int buflen); +#define LGTD_IEEE8023MACTOA(addr, buf) \ + lgtd_iee8023mactoa((addr), (buf), sizeof(buf)) +char *lgtd_sockaddrtoa(const struct sockaddr *, char *buf, int buflen); +#define LGTD_SOCKADDRTOA(addr, buf) \ + lgtd_sockaddrtoa((addr), (buf), sizeof(buf)) + +char *lgtd_print_duration(uint64_t, char *, int); +#define LGTD_PRINT_DURATION(secs, arr) \ + lgtd_print_duration((secs), (arr), sizeof((arr))) +char* lgtd_print_nsec_timestamp(uint64_t, char *, int); +#define LGTD_LIFX_WIRE_PRINT_NSEC_TIMESTAMP(ts, arr) \ + lgtd_print_nsec_timestamp((ts), (arr), sizeof((arr))) + +void lgtd_log_setup(void); + +void lgtd_err(int, const char *, ...) + __attribute__((format(printf, 2, 3), noreturn)); +void lgtd_errx(int, const char *, ...) + __attribute__((format(printf, 2, 3), noreturn)); +void lgtd_warn(const char *, ...) __attribute__((format(printf, 1, 2))); +void lgtd_warnx(const char *, ...) __attribute__((format(printf, 1, 2))); +void lgtd_info(const char *, ...) __attribute__((format(printf, 1, 2))); +void lgtd_debug(const char *, ...) __attribute__((format(printf, 1, 2))); + +void lgtd_cleanup(void); diff --git a/core/listen.c b/core/listen.c new file mode 100644 index 0000000..439054e --- /dev/null +++ b/core/listen.c @@ -0,0 +1,288 @@ +// Copyright (c) 2015, Louis Opter +// +// This file is part of lighstd. +// +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "jsmn.h" +#include "jsonrpc.h" +#include "client.h" +#include "listen.h" +#include "daemon.h" +#include "lightsd.h" + +struct lgtd_listen_list lgtd_listeners = + SLIST_HEAD_INITIALIZER(&lgtd_listeners); + +static void +lgtd_listen_accept_new_client(struct evconnlistener *evlistener, + evutil_socket_t peer, + struct sockaddr *addr, + int addrlen, + void *ctx) +{ + (void)evlistener; + struct lgtd_listen *listener = ctx; + + char bufserver[LGTD_SOCKADDR_STRLEN]; + LGTD_SOCKADDRTOA(listener->sockaddr, bufserver); + + struct lgtd_client *client = NULL; + if (addr->sa_family == AF_UNIX) { + struct sockaddr_storage sockname; + memset(&sockname, 0, sizeof(sockname)); + ev_socklen_t socklen = sizeof(sockname); + getsockname(peer, (struct sockaddr *)&sockname, &socklen); + client = lgtd_client_open(peer, (struct sockaddr *)&sockname, socklen); + } else { + client = lgtd_client_open(peer, addr, addrlen); + } + + if (!client) { + char bufclient[LGTD_SOCKADDR_STRLEN]; + lgtd_warn( + "can't accept new client %s on %s", + LGTD_SOCKADDRTOA(client->addr, bufclient), + bufserver + ); + return; + } + + lgtd_info("accepted new client %s", bufserver); +} + +void +lgtd_listen_close_all(void) +{ + while (!SLIST_EMPTY(&lgtd_listeners)) { + struct lgtd_listen *listener = SLIST_FIRST(&lgtd_listeners); + SLIST_REMOVE_HEAD(&lgtd_listeners, link); + if (listener->sockaddr->sa_family == AF_UNIX) { + unlink(((struct sockaddr_un *)listener->sockaddr)->sun_path); + } + evconnlistener_free(listener->evlistener); + char addr[LGTD_SOCKADDR_STRLEN]; + LGTD_SOCKADDRTOA(listener->sockaddr, addr); + lgtd_info("closed socket %s", addr); + free(listener->sockaddr); + free(listener); + } + + lgtd_daemon_update_proctitle(); +} + +bool +lgtd_listen_open(const char *addr, const char *port) +{ + assert(addr); + assert(port); + + struct evutil_addrinfo *res = NULL, hints = { + .ai_family = AF_UNSPEC, + .ai_socktype = SOCK_STREAM, + .ai_protocol = IPPROTO_TCP, + .ai_flags = EVUTIL_AI_NUMERICSERV|EVUTIL_AI_PASSIVE + }; + + int err = evutil_getaddrinfo(addr, port, &hints, &res); + if (err) { + lgtd_warnx( + "can't listen on %s:%s: %s", addr, port, evutil_gai_strerror(err) + ); + return false; + } + + struct lgtd_listen *listener; + struct evconnlistener *evlistener; + for (struct evutil_addrinfo *it = res; it; it = it->ai_next) { + SLIST_FOREACH(listener, &lgtd_listeners, link) { + if (listener->addrlen == it->ai_addrlen + && memcmp(listener->sockaddr, it->ai_addr, it->ai_addrlen)) { + goto done; + } + } + + evlistener = NULL; + listener = calloc(1, sizeof(*listener)); + if (!listener) { + goto error; + } + listener->sockaddr = calloc(1, it->ai_addrlen); + if (!listener->sockaddr) { + goto error; + } + evlistener = evconnlistener_new_bind( + lgtd_ev_base, + lgtd_listen_accept_new_client, + listener, + LEV_OPT_CLOSE_ON_FREE|LEV_OPT_REUSEABLE, + -1, + it->ai_addr, + it->ai_addrlen + ); + if (!evlistener) { + goto error; + } + + listener->evlistener = evlistener; + listener->addrlen = it->ai_addrlen; + memcpy(listener->sockaddr, it->ai_addr, it->ai_addrlen); + + SLIST_INSERT_HEAD(&lgtd_listeners, listener, link); + lgtd_info( + "listening on %s:%s (%s)", + addr, port, it->ai_family == AF_INET ? "IPv4" : "IPv6" + ); + } + + lgtd_daemon_update_proctitle(); + +done: + evutil_freeaddrinfo(res); + return true; + +error: + lgtd_warn("can't listen on %s:%s", addr, port); + if (listener) { + if (listener->evlistener) { + evconnlistener_free(evlistener); + } + free(listener->sockaddr); + free(listener); + } + evutil_freeaddrinfo(res); + return false; +} + +bool +lgtd_listen_unix_open(const char *path) +{ + assert(path); + + static const int maxpathlen = + sizeof(struct sockaddr_un) - offsetof(struct sockaddr_un, sun_path); + int pathlen = strlen(path); + if (pathlen > maxpathlen) { + lgtd_warnx( + "%s (%d bytes) is too long, your system only supports paths up to " + "%d bytes", path, pathlen, maxpathlen + ); + return false; + } + + struct lgtd_listen *listener; + SLIST_FOREACH(listener, &lgtd_listeners, link) { + if (listener->addrlen == sizeof(struct sockaddr_un)) { + struct sockaddr_un *sockaddr; + sockaddr = (struct sockaddr_un *)listener->sockaddr; + if (!strcmp(sockaddr->sun_path, path)) { + return true; + } + } + } + + if (!lgtd_daemon_makedirs(path)) { + return false; + } + + evutil_socket_t fd = -1; + + listener = calloc(1, sizeof(*listener)); + if (!listener) { + goto error; + } + + struct sockaddr_un *sockpath = calloc(1, sizeof(*sockpath)); + if (!sockpath) { + goto error; + } + sockpath->sun_family = AF_UNIX; + memcpy(sockpath->sun_path, path, pathlen); + listener->sockaddr = (struct sockaddr *)sockpath; + listener->addrlen = sizeof(*sockpath); + + fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd == -1) { + goto error; + } + + if (evutil_make_socket_nonblocking(fd) == -1) { + goto error; + } + + struct stat sb; + if (stat(path, &sb) == -1) { + if (errno != ENOENT) { + goto error; + } + } else if (S_ISSOCK(sb.st_mode)) { + lgtd_warnx("removing existing unix socket: %s", path); + if (unlink(path) == -1 && errno != ENOENT) { + goto error; + } + } else { + errno = EEXIST; + goto error; + } + + if (bind(fd, (struct sockaddr *)sockpath, sizeof(*sockpath)) == -1) { + goto error; + } + + mode_t mode = S_IWUSR|S_IRUSR|S_IXUSR|S_IRGRP|S_IXGRP|S_IWGRP; + if (chmod(path, mode)) { + goto error; + } + + listener->evlistener = evconnlistener_new( + lgtd_ev_base, + lgtd_listen_accept_new_client, + listener, + LEV_OPT_CLOSE_ON_FREE, + -1, + fd + ); + if (!listener->evlistener) { + goto error; + } + + SLIST_INSERT_HEAD(&lgtd_listeners, listener, link); + lgtd_info("unix socket ready at %s", path); + + return true; + +error: + lgtd_warn("can't open unix socket at %s", path); + if (fd != -1) { + close(fd); + } + unlink(path); + free(listener); + return false; +} diff --git a/core/listen.h b/core/listen.h new file mode 100644 index 0000000..96be133 --- /dev/null +++ b/core/listen.h @@ -0,0 +1,34 @@ +// Copyright (c) 2015, Louis Opter +// +// This file is part of lighstd. +// +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . + +#pragma once + +struct evconnlistener; + +struct lgtd_listen { + SLIST_ENTRY(lgtd_listen) link; + ev_socklen_t addrlen; + struct sockaddr *sockaddr; + struct evconnlistener *evlistener; +}; +SLIST_HEAD(lgtd_listen_list, lgtd_listen); + +extern struct lgtd_listen_list lgtd_listeners; + +bool lgtd_listen_open(const char *, const char *); +bool lgtd_listen_unix_open(const char *); +void lgtd_listen_close_all(void); diff --git a/core/log.c b/core/log.c index 32db101..1751808 100644 --- a/core/log.c +++ b/core/log.c @@ -1,190 +1,84 @@ // Copyright (c) 2014, Louis Opter -// All rights reserved. // -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: +// This file is part of lighstd. // -// 1. Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. // -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. // -// 3. Neither the name of the copyright holder nor the names of its contributors -// may be used to endorse or promote products derived from this software -// without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . #include -#include -#include -#include -#include -#include +#include #include #include #include #include -#include - -#include - -#include "wire_proto.h" -#include "lifxd.h" - -static void -lifxd_isotime_now(char *strbuf, int bufsz) -{ - assert(strbuf); - assert(bufsz > 0); - - struct timeval now; - if (gettimeofday(&now, NULL) == -1) { - goto error; - } - struct tm tm_now; - if (!localtime_r(&now.tv_sec, &tm_now)) { - goto error; - } - // '2015-01-02T10:13:16.132222+00:00' - snprintf( -#if LIFXD_SUSECONDS_T_SIZE == 4 - strbuf, bufsz, "%d-%02d-%02dT%02d:%02d:%02d.%d%c%02ld:%02ld", -#else - strbuf, bufsz, "%d-%02d-%02dT%02d:%02d:%02d.%ld%c%02ld:%02ld", -#endif - 1900 + tm_now.tm_year, 1 + tm_now.tm_mon, tm_now.tm_mday, - tm_now.tm_hour, tm_now.tm_min, tm_now.tm_sec, - now.tv_usec, tm_now.tm_gmtoff >= 0 ? '+' : '-', // %+02ld doesn't work - LIFXD_ABS(tm_now.tm_gmtoff / 60 / 60), tm_now.tm_gmtoff % (60 * 60) - ); - return; -error: - strbuf[0] = '\0'; -} +#include -static void -lifxd_log_header(const char *loglvl, bool showprogname) -{ - if (lifxd_opts.log_timestamps) { - char timestr[64]; - lifxd_isotime_now(timestr, sizeof(timestr)); - fprintf( - stderr, "[%s] [%s] %s", - timestr, loglvl, showprogname ? "lifxd: " : "" - ); - return; - } - fprintf(stderr, "[%s] %s", loglvl, showprogname ? "lifxd: " : ""); -} +#include -const char * -lifxd_addrtoa(const uint8_t *addr) -{ - assert(addr); - - static char str[LIFXD_ADDR_LENGTH * 2 + LIFXD_ADDR_LENGTH - 1 + 1]; - snprintf( - str, sizeof(str), "%02hhx:%02hhx:%02hhx:%02hhx:%02hhx:%02hhx", - addr[0], addr[1], addr[2], addr[3], addr[4], addr[5] - ); - return str; -} +#include "lifx/wire_proto.h" +#include "stats.h" +#include "console.h" +#include "daemon.h" +#include "lightsd.h" void -lifxd_sockaddrtoa(const struct sockaddr_storage *peer, char *buf, int buflen) +lgtd_log_setup(void) { - assert(peer); - assert(buf); - assert(buflen > 0); - - if (peer->ss_family == AF_INET) { - const struct sockaddr_in *in_peer = (const struct sockaddr_in *)peer; - inet_ntop(AF_INET, &in_peer->sin_addr, buf, buflen); - } else { - const struct sockaddr_in6 *in6_peer = (const struct sockaddr_in6 *)peer; - inet_ntop(AF_INET6, &in6_peer->sin6_addr, buf, buflen); + if (lgtd_opts.syslog) { + lgtd_daemon_syslog_open( + lgtd_opts.syslog_ident, + lgtd_opts.verbosity, + lgtd_opts.syslog_facility + ); } } -void -_lifxd_err(void (*errfn)(int, const char *, ...), - int eval, - const char *fmt, - ...) -{ - int errsave = errno; - va_list ap; - va_start(ap, fmt); - // lifxd_cleanup is probably going to free some of the arguments we got, so - // let's print to a buffer before we call err. - char errmsg[LIFXD_ERROR_MSG_BUFSIZE]; - vsnprintf(errmsg, sizeof(errmsg), fmt, ap); - va_end(ap); - lifxd_cleanup(); - lifxd_log_header("ERR", false); - errno = errsave; - errfn(eval, errmsg); +#define ERRFN(fn) \ +void \ +lgtd_##fn(int eval, const char *fmt, ...) \ +{ \ + va_list ap; \ + va_start(ap, fmt); \ + if (lgtd_opts.syslog) { \ + lgtd_daemon_syslog_##fn(eval, fmt, ap); \ + } else { \ + lgtd_console_##fn(eval, fmt, ap); \ + } \ + /* not reached */ \ } -void -_lifxd_warn(void (*warnfn)(const char *, va_list), const char *fmt, ...) -{ - if (lifxd_opts.verbosity <= LIFXD_WARN) { - va_list ap; - va_start(ap, fmt); - lifxd_log_header("WARN", false); - warnfn(fmt, ap); - va_end(ap); - } -} - -void -lifxd_info(const char *fmt, ...) -{ - if (lifxd_opts.verbosity <= LIFXD_INFO) { - va_list ap; - va_start(ap, fmt); - lifxd_log_header("INFO", true); - vfprintf(stderr, fmt, ap); - va_end(ap); - fprintf(stderr, "\n"); - } -} +ERRFN(err); +ERRFN(errx); -void -lifxd_debug(const char *fmt, ...) -{ - if (lifxd_opts.verbosity <= LIFXD_DEBUG) { - va_list ap; - va_start(ap, fmt); - lifxd_log_header("DEBUG", true); - vfprintf(stderr, fmt, ap); - va_end(ap); - fprintf(stderr, "\n"); - } +#define LOGFN(level ,fn) \ +void \ +lgtd_##fn(const char *fmt, ...) \ +{ \ + if (lgtd_opts.verbosity > (level)) { \ + return; \ + } \ + \ + va_list ap; \ + va_start(ap, fmt); \ + if (lgtd_opts.syslog) { \ + lgtd_daemon_syslog_##fn(fmt, ap); \ + } else { \ + lgtd_console_##fn(fmt, ap); \ + } \ + va_end(ap); \ } -void -lifxd_libevent_log(int severity, const char *msg) -{ - switch (severity) { - case EVENT_LOG_DEBUG: lifxd_debug("%s", msg); break; - case EVENT_LOG_MSG: lifxd_info("%s", msg); break; - case EVENT_LOG_WARN: lifxd_warnx("%s", msg) break; - case EVENT_LOG_ERR: lifxd_warnx("%s", msg); break; - default: break; - } -} +LOGFN(LGTD_WARN, warn); +LOGFN(LGTD_WARN, warnx); +LOGFN(LGTD_INFO, info); +LOGFN(LGTD_DEBUG, debug); diff --git a/core/pipe.c b/core/pipe.c new file mode 100644 index 0000000..dbb6532 --- /dev/null +++ b/core/pipe.c @@ -0,0 +1,278 @@ +// Copyright (c) 2015, Louis Opter +// +// This file is part of lighstd. +// +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "daemon.h" +#include "jsmn.h" +#include "jsonrpc.h" +#include "client.h" +#include "pipe.h" +#include "lightsd.h" + +struct lgtd_command_pipe_list lgtd_command_pipes = + SLIST_HEAD_INITIALIZER(&lgtd_command_pipes); + +static void +_lgtd_command_pipe_close(struct lgtd_command_pipe *pipe) +{ + assert(pipe); + + event_del(pipe->read_ev); + if (pipe->fd != -1) { + close(pipe->fd); + } + SLIST_REMOVE(&lgtd_command_pipes, pipe, lgtd_command_pipe, link); + evbuffer_free(pipe->read_buf); + event_free(pipe->read_ev); + free(pipe->client.jsmn_tokens); + free(pipe); +} + +static void +lgtd_command_pipe_close(struct lgtd_command_pipe *pipe) +{ + const char *path = pipe->path; + _lgtd_command_pipe_close(pipe); + unlink(path); + lgtd_info("closed command pipe %s", path); +} + +static void lgtd_command_pipe_reset(struct lgtd_command_pipe *); + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wswitch" +static void +lgtd_command_pipe_read_callback(evutil_socket_t socket, short events, void *ctx) +{ + assert(ctx); + assert(socket != -1); + + (void)socket; + (void)events; + + struct lgtd_command_pipe *pipe = ctx; + + bool drain = false; + for (int nbytes = evbuffer_read(pipe->read_buf, pipe->fd, -1); + nbytes; + nbytes = evbuffer_read(pipe->read_buf, pipe->fd, -1)) { + if (nbytes == -1) { + if (errno == EINTR) { + continue; + } + if (errno != EAGAIN) { + lgtd_warn("read error on command pipe %s", pipe->path); + break; + } + return; // EAGAIN, go back to the event loop + } + + if (!drain) { + jsmntok_t *tokens = NULL; + int ntokens = 0; + jsmn_parser jsmn_ctx; + next_request: + (void)0; + const char *buf = (char *)evbuffer_pullup(pipe->read_buf, -1); + ssize_t bufsz = evbuffer_get_length(pipe->read_buf); + parse_after_realloc: + jsmn_init(&jsmn_ctx); + jsmnerr_t rv = jsmn_parse(&jsmn_ctx, buf, bufsz, tokens, ntokens); + switch (rv) { + case JSMN_ERROR_NOMEM: + case JSMN_ERROR_INVAL: + lgtd_warnx("pipe %s: request too big or invalid: %s", pipe->path, buf); + // ignore what's left + drain = true; + break; + case JSMN_ERROR_PART: + case 0: + if (bufsz >= LGTD_CLIENT_MAX_REQUEST_BUF_SIZE) { + lgtd_warnx("pipe %s: request too big", pipe->path); + drain = true; + } + break; + default: + ntokens = rv; + if (tokens) { + pipe->client.json = buf; + lgtd_jsonrpc_dispatch_request(&pipe->client, ntokens); + pipe->client.json = NULL; + int request_size = tokens[0].end; + tokens = NULL; + evbuffer_drain(pipe->read_buf, request_size); + if (request_size >= bufsz) { + break; + } + } else { + pipe->client.jsmn_tokens = reallocarray( + pipe->client.jsmn_tokens, ntokens, sizeof(*tokens) + ); + tokens = pipe->client.jsmn_tokens; + goto parse_after_realloc; + } + goto next_request; + } + } + + if (drain) { + ssize_t bufsz = evbuffer_get_length(pipe->read_buf); + evbuffer_drain(pipe->read_buf, bufsz); + drain = false; + } + } + + lgtd_command_pipe_reset(pipe); +} +#pragma GCC diagnostic pop + +static bool +_lgtd_command_pipe_open(const char *path) +{ + assert(path); + + struct lgtd_command_pipe *pipe; + SLIST_FOREACH(pipe, &lgtd_command_pipes, link) { + if (!strcmp(pipe->path, path)) { + return true; + } + } + + if (!lgtd_daemon_makedirs(path)) { + return false; + } + + pipe = calloc(1, sizeof(*pipe)); + if (!pipe) { + lgtd_warn("can't open command pipe %s", path); + return false; + } + + struct stat sb; + if (stat(path, &sb) == -1) { + if (errno != ENOENT) { + goto error; + } + } else if (!S_ISFIFO(sb.st_mode)) { + errno = EEXIST; + goto error; + } + + pipe->path = path; + pipe->fd = -1; + + mode_t mode = S_IWUSR|S_IRUSR|S_IXUSR|S_IRGRP|S_IXGRP|S_IWGRP; + if (mkfifo(path, mode)) { + if (errno != EEXIST) { + goto error; + } + } + if (chmod(path, mode)) { + goto error; + } + + pipe->fd = open(path, O_RDONLY|O_NONBLOCK); + if (pipe->fd == -1) { + goto error; + } + + if (evutil_make_socket_nonblocking(pipe->fd) == -1) { + goto error; + } + + pipe->read_ev = event_new( + lgtd_ev_base, + pipe->fd, + EV_READ|EV_PERSIST, + lgtd_command_pipe_read_callback, + pipe + ); + if (!pipe->read_ev) { + goto error; + } + + pipe->read_buf = evbuffer_new(); + if (!pipe->read_buf) { + goto error; + } + + if (event_add(pipe->read_ev, NULL)) { + goto error; + } + + SLIST_INSERT_HEAD(&lgtd_command_pipes, pipe, link); + + return true; + +error: + lgtd_warn("can't open command pipe %s", path); + if (pipe->read_buf) { + evbuffer_free(pipe->read_buf); + } + if (pipe->read_ev) { + event_free(pipe->read_ev); + } + if (pipe->fd != -1) { + close(pipe->fd); + } + free(pipe); + return false; +} + +static void +lgtd_command_pipe_reset(struct lgtd_command_pipe *pipe) +{ + const char *path = pipe->path; + // we could optimize a bit to avoid re-allocations here: + _lgtd_command_pipe_close(pipe); + if (!_lgtd_command_pipe_open(path)) { + lgtd_warn("can't re-open pipe %s", path); + } +} + +bool +lgtd_command_pipe_open(const char *path) +{ + if (_lgtd_command_pipe_open(path)) { + lgtd_info("command pipe ready at %s", path); + return true; + } + return false; +} + +void +lgtd_command_pipe_close_all(void) +{ + while (!SLIST_EMPTY(&lgtd_command_pipes)) { + lgtd_command_pipe_close(SLIST_FIRST(&lgtd_command_pipes)); + } +} diff --git a/core/pipe.h b/core/pipe.h new file mode 100644 index 0000000..4e5ca57 --- /dev/null +++ b/core/pipe.h @@ -0,0 +1,33 @@ +// Copyright (c) 2015, Louis Opter +// +// This file is part of lighstd. +// +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . + +#pragma once + +struct lgtd_command_pipe { + SLIST_ENTRY(lgtd_command_pipe) link; + const char *path; + int fd; + struct event *read_ev; + struct evbuffer *read_buf; + struct lgtd_client client; +}; +SLIST_HEAD(lgtd_command_pipe_list, lgtd_command_pipe); + +extern struct lgtd_command_pipe_list lgtd_command_pipes; + +bool lgtd_command_pipe_open(const char *); +void lgtd_command_pipe_close_all(void); diff --git a/core/proto.c b/core/proto.c new file mode 100644 index 0000000..0c9bcfe --- /dev/null +++ b/core/proto.c @@ -0,0 +1,547 @@ +// Copyright (c) 2015, Louis Opter +// +// This file is part of lighstd. +// +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "lifx/wire_proto.h" +#include "time_monotonic.h" +#include "lifx/bulb.h" +#include "lifx/tagging.h" +#include "jsmn.h" +#include "jsonrpc.h" +#include "client.h" +#include "lifx/gateway.h" +#include "proto.h" +#include "router.h" +#include "lightsd.h" + +#define SEND_RESULT(client, ok) do { \ + lgtd_client_send_response((client), (ok) ? "true" : "false"); \ +} while(0) + +void +lgtd_proto_target_list_clear(struct lgtd_proto_target_list *targets) +{ + assert(targets); + + while (!SLIST_EMPTY(targets)) { + struct lgtd_proto_target *target = SLIST_FIRST(targets); + SLIST_REMOVE_HEAD(targets, link); + free(target); + } +} + +void +lgtd_proto_power_on(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets) +{ + assert(targets); + + struct lgtd_lifx_packet_power_state pkt = { .power = LGTD_LIFX_POWER_ON }; + SEND_RESULT( + client, lgtd_router_send(targets, LGTD_LIFX_SET_POWER_STATE, &pkt) + ); +} + +void +lgtd_proto_power_toggle(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets) +{ + assert(targets); + + struct lgtd_router_device_list *devices = NULL; + devices = lgtd_router_targets_to_devices(targets); + if (!devices) { + lgtd_client_send_error( + client, LGTD_CLIENT_INTERNAL_ERROR, "couldn't allocate memory" + ); + return; + } + + struct lgtd_router_device *device; + SLIST_FOREACH(device, devices, link) { + struct lgtd_lifx_bulb *bulb = device->device; + struct lgtd_lifx_packet_power_state pkt = { + .power = ~bulb->state.power + }; + lgtd_router_send_to_device(bulb, LGTD_LIFX_SET_POWER_STATE, &pkt); + } + + SEND_RESULT(client, true); + + lgtd_router_device_list_free(devices); +} + +void +lgtd_proto_power_off(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets) +{ + assert(targets); + + struct lgtd_lifx_packet_power_state pkt = { .power = LGTD_LIFX_POWER_OFF }; + SEND_RESULT( + client, lgtd_router_send(targets, LGTD_LIFX_SET_POWER_STATE, &pkt) + ); +} + +void +lgtd_proto_set_light_from_hsbk(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets, + int hue, + int saturation, + int brightness, + int kelvin, + int transition_msecs) +{ + assert(targets); + assert(hue >= 0 && hue <= UINT16_MAX); + assert(saturation >= 0 && saturation <= UINT16_MAX); + assert(brightness >= 0 && brightness <= UINT16_MAX); + assert(kelvin >= 2500 && kelvin <= 9000); + assert(transition_msecs >= 0); + + struct lgtd_lifx_packet_light_color pkt = { + .stream = 0, + .hue = hue, + .saturation = saturation, + .brightness = brightness, + .kelvin = kelvin, + .transition = transition_msecs + }; + + lgtd_lifx_wire_encode_light_color(&pkt); + SEND_RESULT( + client, lgtd_router_send(targets, LGTD_LIFX_SET_LIGHT_COLOR, &pkt) + ); +} + +void +lgtd_proto_set_waveform(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets, + enum lgtd_lifx_waveform_type waveform, + int hue, int saturation, + int brightness, int kelvin, + int period, float cycles, + int skew_ratio, bool transient) +{ + assert(targets); + assert(hue >= 0 && hue <= UINT16_MAX); + assert(saturation >= 0 && saturation <= UINT16_MAX); + assert(brightness >= 0 && brightness <= UINT16_MAX); + assert(kelvin >= 2500 && kelvin <= 9000); + assert(waveform <= LGTD_LIFX_WAVEFORM_SQUARE); + assert(skew_ratio >= -32767 && skew_ratio <= 32768); + assert(period >= 0); + assert(cycles >= 0); + + struct lgtd_lifx_packet_waveform pkt = { + .stream = 0, + .transient = transient, + .hue = hue, + .saturation = saturation, + .brightness = brightness, + .kelvin = kelvin, + .period = period, + .cycles = cycles, + .skew_ratio = skew_ratio, + .waveform = waveform + }; + + lgtd_lifx_wire_encode_waveform(&pkt); + SEND_RESULT( + client, lgtd_router_send(targets, LGTD_LIFX_SET_WAVEFORM, &pkt) + ); +} + +void +lgtd_proto_get_light_state(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets) +{ + assert(targets); + + struct lgtd_router_device_list *devices; + devices = lgtd_router_targets_to_devices(targets); + if (!devices) { + lgtd_client_send_error( + client, LGTD_CLIENT_INTERNAL_ERROR, "couldn't allocate device list" + ); + return; + } + + char client_ip_addr[LGTD_SOCKADDR_STRLEN]; + LGTD_SOCKADDRTOA(client->addr, client_ip_addr); + + lgtd_client_start_send_response(client); + lgtd_client_write_string(client, "["); + struct lgtd_router_device *device; + SLIST_FOREACH(device, devices, link) { + struct lgtd_lifx_bulb *bulb = device->device; + + char buf[2048], + site_addr[LGTD_LIFX_ADDR_STRLEN], + bulb_addr[LGTD_LIFX_ADDR_STRLEN]; + int i = 0; + + LGTD_IEEE8023MACTOA(bulb->addr, bulb_addr); + LGTD_IEEE8023MACTOA(bulb->gw->site.as_array, site_addr); + + LGTD_SNPRINTF_APPEND( + buf, i, (int)sizeof(buf), + "{" + "\"_lifx\":{" + "\"addr\":\"%s\"," + "\"gateway\":{" + "\"site\":\"%s\"," + "\"url\":\"tcp://%s\"," + "\"latency\":%ju" + "}", + bulb_addr, site_addr, bulb->gw->peeraddr, + (uintmax_t)lgtd_lifx_gateway_latency(bulb->gw) + ); + +#define PRINT_LIFX_FW_TIMESTAMPS(fw_info, built_at_buf, installed_at_buf) \ + LGTD_LIFX_WIRE_PRINT_NSEC_TIMESTAMP((fw_info)->built_at, (built_at_buf)); \ + LGTD_LIFX_WIRE_PRINT_NSEC_TIMESTAMP( \ + (fw_info)->installed_at, (installed_at_buf) \ + ) + + for (int ip = 0; ip != LGTD_LIFX_BULB_IP_COUNT; ip++) { + if (lgtd_opts.verbosity == LGTD_DEBUG) { + char fw_built_at[64], fw_installed_at[64]; + PRINT_LIFX_FW_TIMESTAMPS( + &bulb->ips[ip].fw_info, fw_built_at, fw_installed_at + ); + + LGTD_SNPRINTF_APPEND( + buf, i, (int)sizeof(buf), + ",\"%s\":{" + "\"firmware_built_at\":\"%s\"," + "\"firmware_installed_at\":\"%s\"," + "\"firmware_version\":\"%u.%u\"," + "\"signal_strength\":%f," + "\"tx_bytes\":%u," + "\"rx_bytes\":%u," + "\"temperature\":%u" + "}", + lgtd_lifx_bulb_ip_names[ip], + fw_built_at, fw_installed_at, + (bulb->ips[ip].fw_info.version & 0xffff0000) >> 16, + bulb->ips[ip].fw_info.version & 0xffff, + bulb->ips[ip].state.signal_strength, + bulb->ips[ip].state.tx_bytes, + bulb->ips[ip].state.rx_bytes, + bulb->ips[ip].state.temperature + ); + } else { + LGTD_SNPRINTF_APPEND( + buf, i, (int)sizeof(buf), + ",\"%s\":{\"firmware_version\":\"%u.%u\"}", + lgtd_lifx_bulb_ip_names[ip], + (bulb->ips[ip].fw_info.version & 0xffff0000) >> 16, + bulb->ips[ip].fw_info.version & 0xffff + ); + } + } + + if (lgtd_opts.verbosity == LGTD_DEBUG) { + LGTD_SNPRINTF_APPEND( + buf, i, (int)sizeof(buf), + ",\"product_info\":{" + "\"vendor_id\":\"%x\"," + "\"product_id\":\"%x\"," + "\"version\":%u" + "}", + bulb->product_info.vendor_id, + bulb->product_info.product_id, + bulb->product_info.version + ); + + char bulb_time[64]; + LGTD_SNPRINTF_APPEND( + buf, i, (int)sizeof(buf), + ",\"runtime_info\":{" + "\"time\":\"%s\"," + "\"uptime\":%ju," + "\"downtime\":%ju" + "}" + "}", + LGTD_LIFX_WIRE_PRINT_NSEC_TIMESTAMP( + bulb->runtime_info.time, bulb_time + ), + (uintmax_t)LGTD_NSECS_TO_SECS(bulb->runtime_info.uptime), + (uintmax_t)LGTD_NSECS_TO_SECS(bulb->runtime_info.downtime) + ); + } else { + LGTD_SNPRINTF_APPEND(buf, i, (int)sizeof(buf), "}"); + } + +#define PRINT_STRING_OR_NULL(buf, i, bufsz, v) do { \ + if ((v)) { \ + LGTD_SNPRINTF_APPEND((buf), (i), (bufsz), "\"%s\"", (v)); \ + } else { \ + LGTD_SNPRINTF_APPEND((buf), (i), (bufsz), "null"); \ + } \ +} while (0) + + LGTD_SNPRINTF_APPEND(buf, i, (int)sizeof(buf), ",\"_model\":"); + PRINT_STRING_OR_NULL(buf, i, (int)sizeof(buf), bulb->model); + LGTD_SNPRINTF_APPEND(buf, i, (int)sizeof(buf), ",\"_vendor\":"); + PRINT_STRING_OR_NULL(buf, i, (int)sizeof(buf), bulb->vendor); + +#define PRINT_COMPONENT(src, dst, start, stop) \ + lgtd_jsonrpc_uint16_range_to_float_string( \ + (src), (start), (stop), (dst), sizeof((dst)) \ + ) + + char h[16], s[16], b[16]; + PRINT_COMPONENT(bulb->state.hue, h, 0, 360); + PRINT_COMPONENT(bulb->state.saturation, s, 0, 1); + PRINT_COMPONENT(bulb->state.brightness, b, 0, 1); + + const char *label; + int label_size; + if (bulb->state.label[0]) { + label = bulb->state.label; + label_size = (int)sizeof(bulb->state.label); + } else { + label = bulb_addr; + label_size = (int)sizeof(bulb_addr); + } + + LGTD_SNPRINTF_APPEND( + buf, i, (int)sizeof(buf), + ",\"hsbk\":[%s,%s,%s,%hu]," + "\"power\":%s," + "\"label\":\"%.*s\"," + "\"tags\":[", + h, s, b, bulb->state.kelvin, + bulb->state.power == LGTD_LIFX_POWER_ON ? "true" : "false", + label_size, label + ); + + if (i >= (int)sizeof(buf)) { + lgtd_warnx( + "can't send state of bulb %s (%s) to client " + "%s: output buffer to small", + bulb->state.label, bulb_addr, client_ip_addr + ); + continue; + } + lgtd_client_write_string(client, buf); + + bool comma = false; + int tag_id; + LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, bulb->state.tags) { + if (LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id) & bulb->gw->tag_ids) { + lgtd_client_write_string(client, comma ? ",\"" : "\""); + lgtd_client_write_string(client, bulb->gw->tags[tag_id]->label); + lgtd_client_write_string(client, "\""); + comma = true; + } else { + lgtd_warnx( + "tag_id %d on bulb %.*s (%s) doesn't " + "exist on gw %s (site %s)", + tag_id, (int)sizeof(bulb->state.label), bulb->state.label, + bulb_addr, bulb->gw->peeraddr, site_addr + ); + } + } + + lgtd_client_write_string( + client, SLIST_NEXT(device, link) ? "]}," : "]}" + ); + } + lgtd_client_write_string(client, "]"); + lgtd_client_end_send_response(client); + + lgtd_router_device_list_free(devices); +} + +void +lgtd_proto_tag(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets, + const char *tag_label) +{ + assert(client); + assert(targets); + assert(tag_label); + + struct lgtd_router_device_list *devices; + devices = lgtd_router_targets_to_devices(targets); + if (!devices) { + goto error_tag_alloc; + } + + struct lgtd_lifx_tag *tag = lgtd_lifx_tagging_find_tag(tag_label); + if (!tag) { + tag = lgtd_lifx_tagging_allocate_tag(tag_label); + if (!tag) { + goto error_tag_alloc; + } + lgtd_info("created tag [%s]", tag_label); + } + + struct lgtd_router_device *device; + struct lgtd_lifx_site *site; + + // Loop over the devices and do allocations first, this makes error + // handling easier (since you can't rollback enqueued packets) and build + // the list of affected gateways so we can do SET_TAG_LABELS: + SLIST_FOREACH(device, devices, link) { + struct lgtd_lifx_gateway *gw = device->device->gw; + int tag_id = lgtd_lifx_gateway_get_tag_id(gw, tag); + if (tag_id == -1) { + tag_id = lgtd_lifx_gateway_allocate_tag_id(gw, -1, tag_label); + if (tag_id == -1) { + goto error_site_alloc; + } + } + } + + // SET_TAG_LABELS, this is idempotent, do it everytime so we can recover + // from any bad state: + LIST_FOREACH(site, &tag->sites, link) { + int tag_id = site->tag_id; + assert(tag_id > -1 && tag_id < LGTD_LIFX_GATEWAY_MAX_TAGS); + struct lgtd_lifx_packet_tag_labels pkt = { .tags = 0 }; + pkt.tags = LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id); + strncpy(pkt.label, tag_label, sizeof(pkt.label) - 1); + lgtd_lifx_wire_encode_tag_labels(&pkt); + bool enqueued = lgtd_lifx_gateway_send_to_site( + site->gw, LGTD_LIFX_SET_TAG_LABELS, &pkt + ); + if (!enqueued) { + goto error_site_alloc; + } + lgtd_info( + "created tag [%s] with id %d on gw %s", + tag_label, tag_id, site->gw->peeraddr + ); + } + + // Finally SET_TAGS on the devices: + SLIST_FOREACH(device, devices, link) { + struct lgtd_lifx_bulb *bulb = device->device; + int tag_id = lgtd_lifx_gateway_get_tag_id(bulb->gw, tag); + assert(tag_id > -1 && tag_id < LGTD_LIFX_GATEWAY_MAX_TAGS); + int tag_value = LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id); + if (!(bulb->state.tags & tag_value)) { + struct lgtd_lifx_packet_tags pkt; + pkt.tags = bulb->state.tags | tag_value; + lgtd_lifx_wire_encode_tags(&pkt); + lgtd_router_send_to_device(bulb, LGTD_LIFX_SET_TAGS, &pkt); + } + } + + SEND_RESULT(client, true); + goto fini; + +error_site_alloc: + if (LIST_EMPTY(&tag->sites)) { + lgtd_lifx_tagging_deallocate_tag(tag); + } else { // tagging_decref will deallocate the tag for us: + struct lgtd_lifx_site *next_site; + LIST_FOREACH_SAFE(site, &tag->sites, link, next_site) { + lgtd_lifx_gateway_deallocate_tag_id(site->gw, site->tag_id); + } + } +error_tag_alloc: + lgtd_client_send_error( + client, LGTD_CLIENT_INTERNAL_ERROR, "couldn't allocate new tag" + ); +fini: + lgtd_router_device_list_free(devices); +} + +void +lgtd_proto_untag(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets, + const char *tag_label) +{ + assert(client); + assert(targets); + assert(tag_label); + + struct lgtd_lifx_tag *tag = lgtd_lifx_tagging_find_tag(tag_label); + if (!tag) { + SEND_RESULT(client, true); + return; + } + + struct lgtd_router_device_list *devices = NULL; + devices = lgtd_router_targets_to_devices(targets); + if (!devices) { + lgtd_client_send_error( + client, LGTD_CLIENT_INTERNAL_ERROR, "couldn't allocate memory" + ); + return; + } + + struct lgtd_router_device *device; + SLIST_FOREACH(device, devices, link) { + struct lgtd_lifx_bulb *bulb = device->device; + struct lgtd_lifx_gateway *gw = bulb->gw; + int tag_id = lgtd_lifx_gateway_get_tag_id(gw, tag); + if (tag_id != -1) { + int tag_value = LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id); + if (bulb->state.tags & tag_value) { + struct lgtd_lifx_packet_tags pkt; + pkt.tags = bulb->state.tags & ~tag_value; + lgtd_lifx_wire_encode_tags(&pkt); + lgtd_router_send_to_device(bulb, LGTD_LIFX_SET_TAGS, &pkt); + } + } + } + + SEND_RESULT(client, true); + + lgtd_router_device_list_free(devices); +} + +void +lgtd_proto_set_label(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets, + const char *label) +{ + assert(client); + assert(targets); + assert(label); + + int label_len = strlen(label); + struct lgtd_lifx_packet_label pkt; + memcpy(pkt.label, label, LGTD_MIN(label_len, (int)sizeof(pkt.label))); + // this will go out on the network don't leave garbage in it: + memset(&pkt.label[label_len], 0, LGTD_MAX( + (int)sizeof(pkt.label) - label_len, 0 + )); + SEND_RESULT( + client, lgtd_router_send(targets, LGTD_LIFX_SET_BULB_LABEL, &pkt) + ); +} diff --git a/core/proto.h b/core/proto.h new file mode 100644 index 0000000..585c3d1 --- /dev/null +++ b/core/proto.h @@ -0,0 +1,45 @@ +// Copyright (c) 2015, Louis Opter +// +// This file is part of lighstd. +// +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . + +#pragma once + +struct lgtd_proto_target { + SLIST_ENTRY(lgtd_proto_target) link; + char target[]; +}; +SLIST_HEAD(lgtd_proto_target_list, lgtd_proto_target); + +void lgtd_proto_target_list_clear(struct lgtd_proto_target_list *); +const struct lgtd_proto_target *lgtd_proto_target_list_add(struct lgtd_client *, + struct lgtd_proto_target_list *, + const char *, int); + +void lgtd_proto_set_light_from_hsbk(struct lgtd_client *, + const struct lgtd_proto_target_list *, + int, int, int, int, int); +void lgtd_proto_set_waveform(struct lgtd_client *, + const struct lgtd_proto_target_list *, + enum lgtd_lifx_waveform_type, + int, int, int, int, + int, float, int, bool); +void lgtd_proto_power_on(struct lgtd_client *, const struct lgtd_proto_target_list *); +void lgtd_proto_power_off(struct lgtd_client *, const struct lgtd_proto_target_list *); +void lgtd_proto_power_toggle(struct lgtd_client *, const struct lgtd_proto_target_list *); +void lgtd_proto_get_light_state(struct lgtd_client *, const struct lgtd_proto_target_list *); +void lgtd_proto_tag(struct lgtd_client *, const struct lgtd_proto_target_list *, const char *); +void lgtd_proto_untag(struct lgtd_client *, const struct lgtd_proto_target_list *, const char *); +void lgtd_proto_set_label(struct lgtd_client *, const struct lgtd_proto_target_list *, const char *); diff --git a/core/router.c b/core/router.c new file mode 100644 index 0000000..66d57d0 --- /dev/null +++ b/core/router.c @@ -0,0 +1,381 @@ +// Copyright (c) 2015, Louis Opter +// +// This file is part of lighstd. +// +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "lifx/wire_proto.h" +#include "time_monotonic.h" +#include "lifx/bulb.h" +#include "jsmn.h" +#include "jsonrpc.h" +#include "client.h" +#include "proto.h" +#include "lifx/gateway.h" +#include "lifx/tagging.h" +#include "router.h" +#include "lightsd.h" + +void +lgtd_router_broadcast(enum lgtd_lifx_packet_type pkt_type, void *pkt) +{ + struct lgtd_lifx_packet_header hdr; + union lgtd_lifx_target target = { .tags = 0 }; + + const struct lgtd_lifx_packet_info *pkt_info = NULL; + struct lgtd_lifx_gateway *gw; + LIST_FOREACH(gw, &lgtd_lifx_gateways, link) { + pkt_info = lgtd_lifx_wire_setup_header( + &hdr, + LGTD_LIFX_TARGET_ALL_DEVICES, + target, + gw->site.as_array, + pkt_type + ); + assert(pkt_info); + + lgtd_lifx_gateway_enqueue_packet(gw, &hdr, pkt_info, pkt); + + if (pkt_type == LGTD_LIFX_SET_POWER_STATE) { + struct lgtd_lifx_bulb *bulb; + lgtd_time_mono_t now = lgtd_time_monotonic_msecs(); + SLIST_FOREACH(bulb, &gw->bulbs, link_by_gw) { + bulb->dirty_at = now; + struct lgtd_lifx_packet_power_state *payload = pkt; + bulb->expected_power_on = payload->power; + } + } + } + + if (pkt_info) { + lgtd_info("broadcasting %s", pkt_info->name); + } +} + +void +lgtd_router_send_to_device(struct lgtd_lifx_bulb *bulb, + enum lgtd_lifx_packet_type pkt_type, + void *pkt) +{ + assert(bulb); + + struct lgtd_lifx_packet_header hdr; + union lgtd_lifx_target target = { .addr = bulb->addr }; + + const struct lgtd_lifx_packet_info *pkt_info = lgtd_lifx_wire_setup_header( + &hdr, + LGTD_LIFX_TARGET_DEVICE, + target, + bulb->gw->site.as_array, + pkt_type + ); + assert(pkt_info); + + lgtd_lifx_gateway_enqueue_packet(bulb->gw, &hdr, pkt_info, pkt); + + if (pkt_type == LGTD_LIFX_SET_POWER_STATE) { + bulb->dirty_at = lgtd_time_monotonic_msecs(); + struct lgtd_lifx_packet_power_state *payload = pkt; + bulb->expected_power_on = payload->power; + } + + char addr[LGTD_LIFX_ADDR_STRLEN]; + LGTD_IEEE8023MACTOA(bulb->addr, addr); + lgtd_info( + "sending %s to %s (%.*s)", + pkt_info->name, addr, LGTD_LIFX_LABEL_SIZE, bulb->state.label + ); +} + +void +lgtd_router_send_to_tag(const struct lgtd_lifx_tag *tag, + enum lgtd_lifx_packet_type pkt_type, + void *pkt) +{ + const struct lgtd_lifx_packet_info *pkt_info = NULL; + + struct lgtd_lifx_site *site; + LIST_FOREACH(site, &tag->sites, link) { + struct lgtd_lifx_gateway *gw = site->gw; + int tag_id = site->tag_id; + + struct lgtd_lifx_packet_header hdr; + union lgtd_lifx_target target; + assert(tag == gw->tags[tag_id]); + target.tags = LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id); + pkt_info = lgtd_lifx_wire_setup_header( + &hdr, + LGTD_LIFX_TARGET_TAGS, + target, + gw->site.as_array, + pkt_type + ); + assert(pkt_info); + + lgtd_lifx_gateway_enqueue_packet(gw, &hdr, pkt_info, pkt); + + if (pkt_type == LGTD_LIFX_SET_POWER_STATE) { + struct lgtd_lifx_bulb *bulb; + lgtd_time_mono_t now = lgtd_time_monotonic_msecs(); + SLIST_FOREACH(bulb, &gw->bulbs, link_by_gw) { + if (bulb->state.tags & LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id)) { + bulb->dirty_at = now; + struct lgtd_lifx_packet_power_state *payload = pkt; + bulb->expected_power_on = payload->power; + } + } + } + } + + if (pkt_info) { + lgtd_info("sending %s to #%s", pkt_info->name, tag->label); + } +} + +void +lgtd_router_send_to_label(const char *label, + enum lgtd_lifx_packet_type pkt_type, + void *pkt) +{ + const struct lgtd_lifx_packet_info *pkt_info = NULL; + + struct lgtd_lifx_bulb *bulb; + RB_FOREACH(bulb, lgtd_lifx_bulb_map, &lgtd_lifx_bulbs_table) { + if (lgtd_lifx_bulb_has_label(bulb, label)) { + struct lgtd_lifx_packet_header hdr; + union lgtd_lifx_target target = { .addr = bulb->addr }; + pkt_info = lgtd_lifx_wire_setup_header( + &hdr, + LGTD_LIFX_TARGET_DEVICE, + target, + bulb->gw->site.as_array, + pkt_type + ); + assert(pkt_info); + + lgtd_lifx_gateway_enqueue_packet(bulb->gw, &hdr, pkt_info, pkt); + + if (pkt_type == LGTD_LIFX_SET_POWER_STATE) { + bulb->dirty_at = lgtd_time_monotonic_msecs(); + struct lgtd_lifx_packet_power_state *payload = pkt; + bulb->expected_power_on = payload->power; + } + } + } + + if (pkt_info) { + lgtd_info("sending %s to %s", pkt_info->name, label); + } +} + +static struct lgtd_lifx_bulb * +lgtd_router_device_addr_to_device(const char *device_addr) +{ + errno = 0; + uint64_t device; + const char *endptr = NULL; + device = strtoull(device_addr, (char **)&endptr, 16); + if (!*endptr && errno != ERANGE) { + device = htobe64(device); + return lgtd_lifx_bulb_get( + (uint8_t *)&device + sizeof(device) - LGTD_LIFX_ADDR_LENGTH + ); + } + return NULL; +} + +bool +lgtd_router_send(const struct lgtd_proto_target_list *targets, + enum lgtd_lifx_packet_type pkt_type, + void *pkt) +{ + assert(targets); + + bool rv = true; + + const struct lgtd_proto_target *target; + SLIST_FOREACH(target, targets, link) { + if (!strcmp(target->target, "*")) { + lgtd_router_broadcast(pkt_type, pkt); + continue; + } else if (target->target[0] == '#') { + const struct lgtd_lifx_tag *tag; + tag = lgtd_lifx_tagging_find_tag(&target->target[1]); + if (tag) { + lgtd_router_send_to_tag(tag, pkt_type, pkt); + continue; + } + lgtd_debug("invalid target tag %s", target->target); + } else if (target->target[0]) { + // NOTE: labels and hardware addresses are ambiguous target types, + // we can't really solve this since json doesn't have hexadecimal. + if (isxdigit(target->target[0])) { + struct lgtd_lifx_bulb *bulb = + lgtd_router_device_addr_to_device(target->target); + if (bulb) { + lgtd_router_send_to_device(bulb, pkt_type, pkt); + continue; + } + lgtd_debug( + "%s looked like a device address but didn't " + "yield any device, trying as a label", target->target + ); + } + // Fallback as label: + lgtd_router_send_to_label(target->target, pkt_type, pkt); + continue; + } + rv = false; + } + + return rv; +} + +static void +lgtd_router_clear_device_list(struct lgtd_router_device_list *devices) +{ + assert(devices); + + while (!SLIST_EMPTY(devices)) { + struct lgtd_router_device *device = SLIST_FIRST(devices); + SLIST_REMOVE_HEAD(devices, link); + free(device); + } +} + +static struct lgtd_router_device * +lgtd_router_insert_device_if_not_in_list(struct lgtd_router_device_list *devices, + struct lgtd_lifx_bulb *device) +{ + struct lgtd_router_device *it; + SLIST_FOREACH(it, devices, link) { + if (it->device == device) { + return it; + } + } + + struct lgtd_router_device *new = calloc(1, sizeof(*new)); + if (new) { + new->device = device; + SLIST_INSERT_HEAD(devices, new, link); + } + + return new; +} + +struct lgtd_router_device_list * +lgtd_router_targets_to_devices(const struct lgtd_proto_target_list *targets) +{ + assert(targets); + + struct lgtd_router_device_list *devices = calloc(1, sizeof(*devices)); + if (!devices) { + return NULL; + } + + SLIST_INIT(devices); + + struct lgtd_proto_target *target; + SLIST_FOREACH(target, targets, link) { + if (!strcmp(target->target, "*")) { + lgtd_router_clear_device_list(devices); + struct lgtd_lifx_bulb *bulb; + RB_FOREACH(bulb, lgtd_lifx_bulb_map, &lgtd_lifx_bulbs_table) { + struct lgtd_router_device *device = calloc(1, sizeof(*device)); + if (!device) { + goto device_alloc_error; + } + device->device = bulb; + SLIST_INSERT_HEAD(devices, device, link); + } + return devices; + } else if (target->target[0] == '#') { + const struct lgtd_lifx_tag *tag; + tag = lgtd_lifx_tagging_find_tag(&target->target[1]); + if (tag) { + struct lgtd_lifx_site *site; + LIST_FOREACH(site, &tag->sites, link) { + struct lgtd_lifx_bulb *bulb; + uint64_t tag = LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(site->tag_id); + SLIST_FOREACH(bulb, &site->gw->bulbs, link_by_gw) { + if (bulb->state.tags & tag) { + struct lgtd_router_device *device; + device = lgtd_router_insert_device_if_not_in_list( + devices, bulb + ); + if (!device) { + goto device_alloc_error; + } + } + } + } + } + } else if (target->target[0]) { + struct lgtd_lifx_bulb *bulb = NULL; + if (isxdigit(target->target[0])) { + bulb = lgtd_router_device_addr_to_device(target->target); + if (bulb) { + bool ok = lgtd_router_insert_device_if_not_in_list( + devices, bulb + ); + if (!ok) { + goto device_alloc_error; + } + continue; + } + } + RB_FOREACH(bulb, lgtd_lifx_bulb_map, &lgtd_lifx_bulbs_table) { + if (lgtd_lifx_bulb_has_label(bulb, target->target)) { + bool ok = lgtd_router_insert_device_if_not_in_list( + devices, bulb + ); + if (!ok) { + goto device_alloc_error; + } + } + } + } + } + + return devices; + +device_alloc_error: + lgtd_router_device_list_free(devices); + return NULL; +} + +void +lgtd_router_device_list_free(struct lgtd_router_device_list *devices) +{ + if (devices) { + lgtd_router_clear_device_list(devices); + free(devices); + } +} diff --git a/core/router.h b/core/router.h new file mode 100644 index 0000000..3991aec --- /dev/null +++ b/core/router.h @@ -0,0 +1,38 @@ +// Copyright (c) 2015, Louis Opter +// +// This file is part of lighstd. +// +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . + +#pragma once + +// TODO: return that from the functions in there and handle it: +enum lgtd_router_error { + LGTD_ROUTER_INVALID_TARGET_ERROR, + LGTD_ROUTER_CANNOT_ENQUEUE_PACKET_ERROR +}; + +struct lgtd_router_device { + SLIST_ENTRY(lgtd_router_device) link; + struct lgtd_lifx_bulb *device; +}; +SLIST_HEAD(lgtd_router_device_list, lgtd_router_device); + +bool lgtd_router_send(const struct lgtd_proto_target_list *, enum lgtd_lifx_packet_type, void *); +void lgtd_router_send_to_device(struct lgtd_lifx_bulb *, enum lgtd_lifx_packet_type, void *); +void lgtd_router_send_to_tag(const struct lgtd_lifx_tag *, enum lgtd_lifx_packet_type, void *); +void lgtd_router_send_to_label(const char *, enum lgtd_lifx_packet_type, void *); +void lgtd_router_broadcast(enum lgtd_lifx_packet_type, void *); +struct lgtd_router_device_list *lgtd_router_targets_to_devices(const struct lgtd_proto_target_list *); +void lgtd_router_device_list_free(struct lgtd_router_device_list *); diff --git a/core/setproctitle.c b/core/setproctitle.c new file mode 100644 index 0000000..e14d622 --- /dev/null +++ b/core/setproctitle.c @@ -0,0 +1,325 @@ +#if !LGTD_HAVE_SETPROCTITLE +/* + * Copyright © 2010 William Ahern + * Copyright © 2012-2013 Guillem Jover + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to permit + * persons to whom the Software is furnished to do so, subject to the + * following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN + * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE + * USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern char **environ; + +static struct { + /* Original value. */ + const char *arg0; + + /* Title space available. */ + char *base, *end; + + /* Pointer to original nul character within base. */ + char *nul; + + bool warned; + bool reset; + int error; +} SPT; + + +static inline size_t +spt_min(size_t a, size_t b) +{ + return a < b ? a : b; +} + +/* + * For discussion on the portability of the various methods, see + * http://lists.freebsd.org/pipermail/freebsd-stable/2008-June/043136.html + */ +static int +spt_clearenv(void) +{ +#ifdef HAVE_CLEARENV + return clearenv(); +#else + char **tmp; + + tmp = malloc(sizeof(*tmp)); + if (tmp == NULL) + return errno; + + tmp[0] = NULL; + environ = tmp; + + return 0; +#endif +} + +static int +spt_copyenv(int envc, char *envp[]) +{ + char **envcopy; + char *eq; + int envsize; + int i, error; + + if (environ != envp) + return 0; + + /* Make a copy of the old environ array of pointers, in case + * clearenv() or setenv() is implemented to free the internal + * environ array, because we will need to access the old environ + * contents to make the new copy. */ + envsize = (envc + 1) * sizeof(char *); + envcopy = malloc(envsize); + if (envcopy == NULL) + return errno; + memcpy(envcopy, envp, envsize); + + error = spt_clearenv(); + if (error) { + environ = envp; + free(envcopy); + return error; + } + + for (i = 0; envcopy[i]; i++) { + eq = strchr(envcopy[i], '='); + if (eq == NULL) + continue; + + *eq = '\0'; + if (setenv(envcopy[i], eq + 1, 1) < 0) + error = errno; + *eq = '='; + + if (error) { +#ifdef HAVE_CLEARENV + /* Because the old environ might not be available + * anymore we will make do with the shallow copy. */ + environ = envcopy; +#else + environ = envp; + free(envcopy); +#endif + return error; + } + } + + /* Dispose of the shallow copy, now that we've finished transfering + * the old environment. */ + free(envcopy); + + return 0; +} + +static int +spt_copyargs(int argc, char *argv[]) +{ + char *tmp; + int i; + + for (i = 1; i < argc || (i >= argc && argv[i]); i++) { + if (argv[i] == NULL) + continue; + + tmp = strdup(argv[i]); + if (tmp == NULL) + return errno; + + argv[i] = tmp; + } + + return 0; +} + +/* + Rejected in glibc (http://sourceware.org/ml/libc-alpha/2006-03/msg00125.html) +*/ + +#ifdef HAVE___PROGNAME +extern const char *__progname; +#else +static const char *__progname = NULL; +#endif + +const char * +getprogname(void) +{ +#if defined(HAVE_PROGRAM_INVOCATION_SHORT_NAME) + if (__progname == NULL) + __progname = program_invocation_short_name; +#elif defined(HAVE_GETEXECNAME) + /* getexecname(3) returns an absolute pathname, normalize it. */ + if (__progname == NULL) + setprogname(getexecname()); +#endif + + return __progname; +} + +void +setprogname(const char *progname) +{ + const char *last_slash; + + last_slash = strrchr(progname, '/'); + if (last_slash == NULL) + __progname = progname; + else + __progname = last_slash + 1; +} + +void +setproctitle_init(int argc, char *argv[], char *envp[]) +{ + char *base, *end, *nul, *tmp; + int i, envc, error; + + /* Try to make sure we got called with main() arguments. */ + if (argc < 0) + return; + + base = argv[0]; + if (base == NULL) + return; + + nul = &base[strlen(base)]; + end = nul + 1; + + for (i = 0; i < argc || (i >= argc && argv[i]); i++) { + if (argv[i] == NULL || argv[i] < end) + continue; + + end = argv[i] + strlen(argv[i]) + 1; + } + + for (i = 0; envp[i]; i++) { + if (envp[i] < end) + continue; + + end = envp[i] + strlen(envp[i]) + 1; + } + envc = i; + + SPT.arg0 = strdup(argv[0]); + if (SPT.arg0 == NULL) { + SPT.error = errno; + return; + } + + tmp = strdup(getprogname()); + if (tmp == NULL) { + SPT.error = errno; + return; + } + setprogname(tmp); + + error = spt_copyenv(envc, envp); + if (error) { + SPT.error = error; + return; + } + + error = spt_copyargs(argc, argv); + if (error) { + SPT.error = error; + return; + } + + SPT.nul = nul; + SPT.base = base; + SPT.end = end; +} + +#ifndef SPT_MAXTITLE +#define SPT_MAXTITLE 255 +#endif + +void +setproctitle(const char *fmt, ...) +{ + /* Use buffer in case argv[0] is passed. */ + char buf[SPT_MAXTITLE + 1]; + va_list ap; + char *nul; + int len; + + if (SPT.base == NULL) { + if (!SPT.warned) { + warnx("setproctitle not initialized, please either call " + "setproctitle_init() or link against libbsd-ctor."); + SPT.warned = true; + } + return; + } + + if (fmt) { + if (fmt[0] == '-') { + /* Skip program name prefix. */ + fmt++; + len = 0; + } else { + /* Print program name heading for grep. */ + snprintf(buf, sizeof(buf), "%s: ", getprogname()); + len = strlen(buf); + } + + va_start(ap, fmt); + len += vsnprintf(buf + len, sizeof(buf) - len, fmt, ap); + va_end(ap); + } else { + len = snprintf(buf, sizeof(buf), "%s", SPT.arg0); + } + + if (len <= 0) { + SPT.error = errno; + return; + } + + if (!SPT.reset) { + memset(SPT.base, 0, SPT.end - SPT.base); + SPT.reset = true; + } else { + memset(SPT.base, 0, spt_min(sizeof(buf), SPT.end - SPT.base)); + } + + len = spt_min(len, spt_min(sizeof(buf), SPT.end - SPT.base) - 1); + memcpy(SPT.base, buf, len); + nul = &SPT.base[len]; + + if (nul < SPT.nul) { + *SPT.nul = '.'; + } else if (nul == SPT.nul && &nul[1] < SPT.end) { + *SPT.nul = ' '; + *++nul = '\0'; + } +} +#else +typedef int hello_compiler; +#endif diff --git a/core/stats.c b/core/stats.c new file mode 100644 index 0000000..2463dc1 --- /dev/null +++ b/core/stats.c @@ -0,0 +1,47 @@ +// Copyright (c) 2015, Louis Opter +// +// This file is part of lighstd. +// +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . + +#include +#include + +#include "stats.h" + +struct lgtd_stats lgtd_counters = { .gateways = 0 }; + +void +lgtd_stats_add(int offset, int value) +{ + assert(offset >= 0); + assert(offset < (int)sizeof(lgtd_counters)); + assert(offset % sizeof(int) == 0); + + int *counter = (int *)((uint8_t *)&lgtd_counters + offset); + + assert(*counter + value >= 0); + + *counter += value; +} + +int +lgtd_stats_get(int offset) +{ + assert(offset >= 0); + assert(offset < (int)sizeof(lgtd_counters)); + assert(offset % sizeof(int) == 0); + + return *(int *)((uint8_t *)&lgtd_counters + offset); +} diff --git a/core/stats.h b/core/stats.h new file mode 100644 index 0000000..766c259 --- /dev/null +++ b/core/stats.h @@ -0,0 +1,35 @@ +// Copyright (c) 2015, Louis Opter +// +// This file is part of lighstd. +// +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . + +#pragma once + +struct lgtd_stats { + int gateways; + int bulbs; + int bulbs_powered_on; + int clients; +}; + +void lgtd_stats_add(int, int); +int lgtd_stats_get(int); + +#define LGTD_STATS_GET(name) lgtd_stats_get(offsetof(struct lgtd_stats, name)) + +#define LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(name, value) do { \ + lgtd_stats_add(offsetof(struct lgtd_stats, name), (value)); \ + lgtd_daemon_update_proctitle(); \ +} while (0) diff --git a/core/timer.c b/core/timer.c index 4d247cc..a32772d 100644 --- a/core/timer.c +++ b/core/timer.c @@ -1,209 +1,104 @@ // Copyright (c) 2015, Louis Opter -// All rights reserved. // -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: +// This file is part of lighstd. // -// 1. Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. // -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. // -// 3. Neither the name of the copyright holder nor the names of its contributors -// may be used to endorse or promote products derived from this software -// without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . #include -#include #include #include -#include #include -#include +#include +#include #include #include -#include "wire_proto.h" -#include "time_monotonic.h" -#include "broadcast.h" -#include "bulb.h" -#include "gateway.h" #include "timer.h" -#include "lifxd.h" +#include "lightsd.h" -static struct { - struct event *watchdog_interval_ev; - struct event *discovery_timeout_ev; -} lifxd_timer_context = { - .watchdog_interval_ev = NULL, - .discovery_timeout_ev = NULL -}; +static struct lgtd_timer_list lgtd_timers = LIST_HEAD_INITIALIZER(&lgtd_timers); static void -lifxd_timer_discovery_timeout_event_callback(evutil_socket_t socket, - short events, - void *ctx) +lgtd_timer_callback(evutil_socket_t socket, short events, void *ctx) { + assert(ctx); + (void)socket; (void)events; - (void)ctx; - - int timeout = LIFXD_TIMER_PASSIVE_DISCOVERY_INTERVAL_MSECS; - if (LIST_EMPTY(&lifxd_gateways)) { - lifxd_debug( - "discovery didn't returned anything in %dms, restarting it", - LIFXD_TIMER_ACTIVE_DISCOVERY_INTERVAL_MSECS - ); - timeout = LIFXD_TIMER_ACTIVE_DISCOVERY_INTERVAL_MSECS; - } else { - lifxd_debug("sending periodic discovery packet"); - } - struct timeval tv = LIFXD_MSECS_TO_TIMEVAL(timeout); - if (event_add(lifxd_timer_context.discovery_timeout_ev, &tv) - || !lifxd_broadcast_discovery()) { - lifxd_err(1, "can't start discovery"); - } + struct lgtd_timer *timer = ctx; + timer->callback(timer, timer->ctx); } -static void -lifxd_timer_watchdog_timeout_event_callback(evutil_socket_t socket, - short events, - void *ctx) +struct lgtd_timer * +lgtd_timer_start(int flags, + int ms, + void (*cb)(struct lgtd_timer *, + union lgtd_timer_ctx), + union lgtd_timer_ctx ctx) { - (void)socket; - (void)events; - (void)ctx; + assert(ms > 0); + assert(cb); - bool start_discovery = false; - lifxd_time_mono_t now = lifxd_time_monotonic_msecs(); - - struct lifxd_bulb *bulb, *next_bulb; - RB_FOREACH_SAFE(bulb, lifxd_bulb_map, &lifxd_bulbs_table, next_bulb) { - int light_state_lag = now - bulb->last_light_state_at; - if (light_state_lag >= LIFXD_TIMER_DEVICE_TIMEOUT_MSECS) { - lifxd_info( - "closing bulb \"%.*s\" that hasn't been updated for %dms", - LIFXD_LABEL_SIZE, bulb->state.label, light_state_lag - ); - lifxd_bulb_close(bulb); - start_discovery = true; - } - } - - // Repeat for the gateways, we could also look if we are removing the last - // bulb on the gateway but this will also support architectures where - // gateways aren't bulbs themselves: - struct lifxd_gateway *gw, *next_gw; - LIST_FOREACH_SAFE(gw, &lifxd_gateways, link, next_gw) { - int gw_lag = now - gw->last_pkt_at; - if (gw_lag >= LIFXD_TIMER_DEVICE_TIMEOUT_MSECS) { - lifxd_info( - "closing bulb gateway [%s]:%hu that " - "hasn't received traffic for %dms", - gw->ip_addr, gw->port, - gw_lag - ); - lifxd_gateway_close(gw); - start_discovery = true; - } - } - - // If anything happens restart a discovery right away, maybe something just - // moved on the network: - if (start_discovery) { - lifxd_broadcast_discovery(); + struct lgtd_timer *timer = calloc(1, sizeof(*timer)); + if (!timer) { + return false; } -} + timer->callback = cb; + timer->ctx = ctx; + LIST_INSERT_HEAD(&lgtd_timers, timer, link); -bool -lifxd_timer_setup(void) -{ - assert(!lifxd_timer_context.watchdog_interval_ev); - assert(!lifxd_timer_context.discovery_timeout_ev); - - lifxd_timer_context.discovery_timeout_ev = event_new( - lifxd_ev_base, - -1, - 0, - lifxd_timer_discovery_timeout_event_callback, - NULL - ); - lifxd_timer_context.watchdog_interval_ev = event_new( - lifxd_ev_base, + struct timeval tv = LGTD_MSECS_TO_TIMEVAL(ms); + timer->event = event_new( + lgtd_ev_base, -1, - EV_PERSIST, - lifxd_timer_watchdog_timeout_event_callback, - NULL + flags & LGTD_TIMER_PERSISTENT ? EV_PERSIST : 0, + lgtd_timer_callback, + timer ); - - if (lifxd_timer_context.discovery_timeout_ev - && lifxd_timer_context.watchdog_interval_ev) { - return true; + if (!timer->event || evtimer_add(timer->event, &tv)) { + LIST_REMOVE(timer, link); + if (timer->event) { + event_free(timer->event); + } + free(timer); + return NULL; } - int errsave = errno; - lifxd_timer_close(); - errno = errsave; - return false; -} - -void -lifxd_timer_close(void) -{ - if (lifxd_timer_context.discovery_timeout_ev) { - event_del(lifxd_timer_context.discovery_timeout_ev); - event_free(lifxd_timer_context.discovery_timeout_ev); - lifxd_timer_context.discovery_timeout_ev = NULL; - } - if (lifxd_timer_context.watchdog_interval_ev) { - event_del(lifxd_timer_context.watchdog_interval_ev); - event_free(lifxd_timer_context.watchdog_interval_ev); - lifxd_timer_context.watchdog_interval_ev = NULL; + if (flags & LGTD_TIMER_ACTIVATE_NOW) { + lgtd_timer_activate(timer); } + return timer; } void -lifxd_timer_start_watchdog(void) +lgtd_timer_stop(struct lgtd_timer *timer) { - assert(!RB_EMPTY(&lifxd_bulbs_table) || !LIST_EMPTY(&lifxd_gateways)); + assert(timer); - if (!evtimer_pending(lifxd_timer_context.watchdog_interval_ev, NULL)) { - struct timeval tv = LIFXD_MSECS_TO_TIMEVAL( - LIFXD_TIMER_WATCHDOG_INTERVAL_MSECS - ); - if (event_add(lifxd_timer_context.watchdog_interval_ev, &tv)) { - lifxd_err(1, "can't start watchdog"); - } - lifxd_debug("starting watchdog timer"); - } + LIST_REMOVE(timer, link); + event_del(timer->event); + event_free(timer->event); + free(timer); } void -lifxd_timer_start_discovery(void) +lgtd_timer_stop_all(void) { - assert(!evtimer_pending(lifxd_timer_context.discovery_timeout_ev, NULL)); - - struct timeval tv = LIFXD_MSECS_TO_TIMEVAL( - LIFXD_TIMER_ACTIVE_DISCOVERY_INTERVAL_MSECS - ); - if (event_add(lifxd_timer_context.discovery_timeout_ev, &tv)) { - lifxd_err(1, "can't start discovery timer"); + struct lgtd_timer *timer, *next_timer; + LIST_FOREACH_SAFE(timer, &lgtd_timers, link, next_timer) { + lgtd_timer_stop(timer); } - lifxd_debug("starting discovery timer"); } diff --git a/core/timer.h b/core/timer.h index 3e4aec1..d54c6f2 100644 --- a/core/timer.h +++ b/core/timer.h @@ -1,40 +1,77 @@ // Copyright (c) 2015, Louis Opter -// All rights reserved. // -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: +// This file is part of lighstd. // -// 1. Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. // -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. // -// 3. Neither the name of the copyright holder nor the names of its contributors -// may be used to endorse or promote products derived from this software -// without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . #pragma once -enum { LIFXD_TIMER_WATCHDOG_INTERVAL_MSECS = 200 }; -enum { LIFXD_TIMER_ACTIVE_DISCOVERY_INTERVAL_MSECS = 2000 }; -enum { LIFXD_TIMER_PASSIVE_DISCOVERY_INTERVAL_MSECS = 10000 }; -enum { LIFXD_TIMER_DEVICE_TIMEOUT_MSECS = 2000 }; +struct timeval; + +union lgtd_timer_ctx { + uint64_t as_uint; + void *as_ptr; +}; + +struct lgtd_timer { + LIST_ENTRY(lgtd_timer) link; + void (*callback)(struct lgtd_timer *, + union lgtd_timer_ctx); + union lgtd_timer_ctx ctx; + struct event *event; +}; +LIST_HEAD(lgtd_timer_list, lgtd_timer); + +enum lgtd_timer_flags { + LGTD_TIMER_DEFAULT_FLAGS = 0, + LGTD_TIMER_ACTIVATE_NOW = 1, + LGTD_TIMER_PERSISTENT = 1 << 1, +}; + +// Activate the timer now, in other words make the callback pending: +static inline void +lgtd_timer_activate(struct lgtd_timer *timer) +{ + assert(timer); + + event_active(timer->event, 0, 0); +} + +// Re-schedule a non-persistent timer with the given timeout: +static inline bool +lgtd_timer_reschedule(struct lgtd_timer *timer, const struct timeval *tv) +{ + assert(timer); + assert(tv); + + return !evtimer_add(timer->event, tv); +} + +static inline bool +lgtd_timer_ispending(const struct lgtd_timer *timer) +{ + assert(timer); + + return evtimer_pending(timer->event, NULL); +} -bool lifxd_timer_setup(void); -void lifxd_timer_close(void); -void lifxd_timer_start_watchdog(void); -void lifxd_timer_start_discovery(void); +void lgtd_timer_stop(struct lgtd_timer *); +void lgtd_timer_stop_all(void); +// NOTE: if you start a persistent timer and don't keep track of it, make sure +// you don't end up in a callback using a context that has been freed. +struct lgtd_timer *lgtd_timer_start(int, + int, // ms + void (*)(struct lgtd_timer *, + union lgtd_timer_ctx), + union lgtd_timer_ctx); diff --git a/core/utils.c b/core/utils.c new file mode 100644 index 0000000..ded5adb --- /dev/null +++ b/core/utils.c @@ -0,0 +1,139 @@ +// Copyright (c) 2015, Louis Opter +// +// This file is part of lighstd. +// +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "lightsd.h" + +char * +lgtd_iee8023mactoa(const uint8_t *addr, char *buf, int buflen) +{ + assert(addr); + assert(buf); + assert(buflen >= 2 * 6 + 5 + 1); + + snprintf( + buf, buflen, "%02hhx:%02hhx:%02hhx:%02hhx:%02hhx:%02hhx", + addr[0], addr[1], addr[2], addr[3], addr[4], addr[5] + ); + return buf; +} + +char * +lgtd_sockaddrtoa(const struct sockaddr *peer, char *buf, int buflen) +{ + assert(peer); + assert(buf); + assert(buflen > 1); + + const char *printed = NULL; + int i = 0; + switch (peer->sa_family) { + case AF_INET: + (void)0; + const struct sockaddr_in *in_peer = (const struct sockaddr_in *)peer; + LGTD_SNPRINTF_APPEND(buf, i, buflen, "[::ffff:"); + printed = inet_ntop(AF_INET, &in_peer->sin_addr, &buf[i], buflen - i); + if (printed) { + i += strlen(printed); + LGTD_SNPRINTF_APPEND( + buf, i, buflen, "]:%hu", ntohs(in_peer->sin_port) + ); + } + break; + case AF_INET6: + (void)0; + const struct sockaddr_in6 *in6_peer = (const struct sockaddr_in6 *)peer; + LGTD_SNPRINTF_APPEND(buf, i, buflen, "["); + printed = inet_ntop(AF_INET6, &in6_peer->sin6_addr, &buf[i], buflen - i); + if (printed) { + i += strlen(printed); + LGTD_SNPRINTF_APPEND( + buf, i, buflen, "]:%hu", ntohs(in6_peer->sin6_port) + ); + } + break; + case AF_UNIX: + (void)0; + const struct sockaddr_un *un_path = (const struct sockaddr_un *)peer; + LGTD_SNPRINTF_APPEND(buf, i, buflen, "at %s", un_path->sun_path); + printed = buf; + break; + default: + break; + } + + if (!printed) { + buf[0] = 0; + lgtd_warnx("not enough space to log an ip address"); +#ifndef NDEBUG + abort(); +#endif + } + + return buf; +} + +char * +lgtd_print_duration(uint64_t secs, char *buf, int bufsz) +{ + assert(buf); + assert(bufsz > 0); + + int days = secs / (60 * 60 * 24); + int minutes = secs / 60; + int hours = minutes / 60; + hours = hours % 24; + minutes = minutes % 60; + + int i = 0; + if (days) { + int n = snprintf(buf, bufsz, "%d days ", days); + i = LGTD_MIN(i + n, bufsz); + } + snprintf(&buf[i], bufsz - i, "%02d:%02d", hours, minutes); + return buf; +} + +char * +lgtd_print_nsec_timestamp(uint64_t nsec_ts, char *buf, int bufsz) +{ + assert(buf); + assert(bufsz > 0); + + time_t ts = LGTD_NSECS_TO_SECS(nsec_ts); + + struct tm tm_utc; + if (gmtime_r(&ts, &tm_utc)) { + int64_t usecs = LGTD_NSECS_TO_USECS(nsec_ts - LGTD_SECS_TO_NSECS(ts)); + LGTD_TM_TO_ISOTIME(&tm_utc, buf, bufsz, usecs); + } else { + buf[0] = '\0'; + } + + return buf; +} diff --git a/core/version.h.in b/core/version.h.in index 3f8a435..2f23696 100644 --- a/core/version.h.in +++ b/core/version.h.in @@ -29,4 +29,12 @@ #pragma once -const char LIFXD_VERSION[] = "@LIFXD_VERSION@"; +const char LGTD_VERSION[] = ( + "@LIGHTSD_VERSION@\n\n" + "cflags: @CMAKE_C_FLAGS@\n\n" + "Copyright (c) 2014, 2015, Louis Opter " +); + +const char LGTD_INSTALL_PREFIX[] = "@CMAKE_INSTALL_PREFIX@"; + +const char LGTD_RUNTIME_DIRECTORY[] = "@LGTD_RUNTIME_DIRECTORY@"; diff --git a/core/wire_proto.c b/core/wire_proto.c deleted file mode 100644 index 5973f89..0000000 --- a/core/wire_proto.c +++ /dev/null @@ -1,266 +0,0 @@ -// Copyright (c) 2014, Louis Opter -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// 3. Neither the name of the copyright holder nor the names of its contributors -// may be used to endorse or promote products derived from this software -// without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "wire_proto.h" -#include "time_monotonic.h" -#include "bulb.h" -#include "gateway.h" -#include "lifxd.h" - -union lifxd_target LIFXD_UNSPEC_TARGET = { .tags = 0 }; - -static struct lifxd_packet_infos_map lifxd_packet_infos = - RB_INITIALIZER(&lifxd_packets_infos); - -RB_GENERATE_STATIC( - lifxd_packet_infos_map, - lifxd_packet_infos, - link, - lifxd_packet_infos_cmp -); - -static void -lifxd_wire_null_packet_encoder_decoder(void *pkt) -{ - (void)pkt; -} - -static void -lifxd_wire_null_packet_handler(struct lifxd_gateway *gw, - const struct lifxd_packet_header *hdr, - const void *pkt) -{ - (void)gw; - (void)hdr; - (void)pkt; -} - -void -lifxd_wire_load_packet_infos_map(void) -{ -#define DECODER(x) ((void (*)(void *))(x)) -#define ENCODER(x) ((void (*)(void *))(x)) -#define HANDLER(x) \ - ((void (*)(struct lifxd_gateway *, \ - const struct lifxd_packet_header *, \ - const void *))(x)) -#define REQUEST_ONLY \ - .decode = lifxd_wire_null_packet_encoder_decoder, \ - .encode = lifxd_wire_null_packet_encoder_decoder, \ - .handle = lifxd_wire_null_packet_handler - - static struct lifxd_packet_infos packet_table[] = { - { - REQUEST_ONLY, - .name = "GET_PAN_GATEWAY", - .type = LIFXD_GET_PAN_GATEWAY - }, - { - .name = "PAN_GATEWAY", - .type = LIFXD_PAN_GATEWAY, - .size = sizeof(struct lifxd_packet_pan_gateway), - .decode = DECODER(lifxd_wire_decode_pan_gateway), - .encode = ENCODER(lifxd_wire_encode_pan_gateway), - .handle = HANDLER(lifxd_gateway_handle_pan_gateway) - }, - { - REQUEST_ONLY, - .name = "GET_LIGHT_STATUS", - .type = LIFXD_GET_LIGHT_STATE - }, - { - .name = "LIGHT_STATUS", - .type = LIFXD_LIGHT_STATUS, - .size = sizeof(struct lifxd_packet_light_status), - .decode = DECODER(lifxd_wire_decode_light_status), - .encode = ENCODER(lifxd_wire_encode_light_status), - .handle = HANDLER(lifxd_gateway_handle_light_status) - }, - { - .name = "POWER_STATE", - .type = LIFXD_POWER_STATE, - .size = sizeof(struct lifxd_packet_power_state), - .decode = DECODER(lifxd_wire_decode_power_state), - .handle = HANDLER(lifxd_gateway_handle_power_state) - } - }; - - for (int i = 0; i != LIFXD_ARRAY_SIZE(packet_table); ++i) { - RB_INSERT( - lifxd_packet_infos_map, &lifxd_packet_infos, &packet_table[i] - ); - } -} - -const struct lifxd_packet_infos * -lifxd_wire_get_packet_infos(enum lifxd_packet_type packet_type) -{ - struct lifxd_packet_infos pkt_infos = { .type = packet_type }; - return RB_FIND(lifxd_packet_infos_map, &lifxd_packet_infos, &pkt_infos); -} - -// Convert all the fields in the header to the host endianness. -// -// \return The payload size or -1 if the header is invalid. -void -lifxd_wire_decode_header(struct lifxd_packet_header *hdr) -{ - assert(hdr); - - hdr->size = le16toh(hdr->size); - hdr->protocol.version = le16toh(hdr->protocol.version); - if (hdr->protocol.tagged) { - le64toh(hdr->target.tags); - } - hdr->timestamp = le64toh(hdr->timestamp); - hdr->packet_type = le16toh(hdr->packet_type); -} - -const struct lifxd_packet_infos * -lifxd_wire_setup_header(struct lifxd_packet_header *hdr, - enum lifxd_target_type target_type, - union lifxd_target target, - const uint8_t *site, - enum lifxd_packet_type packet_type) -{ - assert(hdr); - - const struct lifxd_packet_infos *pkt_infos = lifxd_wire_get_packet_infos( - packet_type - ); - - memset(hdr, 0, sizeof(*hdr)); - hdr->size = pkt_infos->size + sizeof(*hdr); - hdr->protocol.version = LIFXD_LIFX_PROTOCOL_V1; - hdr->packet_type = packet_type; - if (site) { - memcpy(hdr->site, site, sizeof(hdr->site)); - } else { - assert(target_type == LIFXD_TARGET_ALL_DEVICES); - } - - switch (target_type) { - case LIFXD_TARGET_SITE: - hdr->protocol.tagged = true; - break; - case LIFXD_TARGET_TAGS: - hdr->protocol.tagged = true; - hdr->target.tags = target.tags; - break; - case LIFXD_TARGET_DEVICE: - hdr->protocol.addressable = false; - memcpy(hdr->target.device_addr, target.addr, LIFXD_ADDR_LENGTH); - break; - case LIFXD_TARGET_ALL_DEVICES: - hdr->protocol.tagged = true; - break; - } - - lifxd_wire_encode_header(hdr); - - return pkt_infos; -} - -void -lifxd_wire_encode_header(struct lifxd_packet_header *hdr) -{ - assert(hdr); - - hdr->size = htole16(hdr->size); - hdr->protocol.version = htole16(hdr->protocol.version); - if (hdr->protocol.tagged) { - le64toh(hdr->target.tags); - } - hdr->timestamp = htole64(hdr->timestamp); - hdr->packet_type = htole16(hdr->packet_type); -} - -void -lifxd_wire_decode_pan_gateway(struct lifxd_packet_pan_gateway *pkt) -{ - assert(pkt); - - pkt->port = le32toh(pkt->port); -} - -void -lifxd_wire_encode_pan_gateway(struct lifxd_packet_pan_gateway *pkt) -{ - assert(pkt); - - pkt->port = htole32(pkt->port); -} - -void -lifxd_wire_decode_light_status(struct lifxd_packet_light_status *pkt) -{ - assert(pkt); - - pkt->hue = le16toh(pkt->hue); - pkt->saturation = le16toh(pkt->saturation); - pkt->brightness = le16toh(pkt->brightness); - pkt->kelvin = le16toh(pkt->kelvin); - pkt->dim = le16toh(pkt->dim); - pkt->power = le16toh(pkt->power); - pkt->tags = le64toh(pkt->tags); -} - -void -lifxd_wire_encode_light_status(struct lifxd_packet_light_status *pkt) -{ - assert(pkt); - - pkt->hue = htole16(pkt->hue); - pkt->saturation = htole16(pkt->saturation); - pkt->brightness = htole16(pkt->brightness); - pkt->kelvin = htole16(pkt->kelvin); - pkt->dim = htole16(pkt->dim); - pkt->power = htole16(pkt->power); - pkt->tags = htole64(pkt->tags); -} - -void -lifxd_wire_decode_power_state(struct lifxd_packet_power_state *pkt) -{ - assert(pkt); -} diff --git a/core/wire_proto.h b/core/wire_proto.h deleted file mode 100644 index 7e2eaa5..0000000 --- a/core/wire_proto.h +++ /dev/null @@ -1,223 +0,0 @@ -// Copyright (c) 2014, Louis Opter -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// 3. Neither the name of the copyright holder nor the names of its contributors -// may be used to endorse or promote products derived from this software -// without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. - -#pragma once - -typedef uint16_t uint16le_t; -typedef uint16_t uint16be_t; -typedef uint32_t uint32le_t; -typedef uint32_t uint32be_t; -typedef uint64_t uint64le_t; -typedef uint64_t uint64be_t; - -enum { LIFXD_ADDR_LENGTH = 6 }; - -#pragma pack(push, 1) - -struct lifxd_packet_header { - //! Packet size including the headers (i.e: this structure). - uint16le_t size; - struct { - //! Protocol version should be LIFXD_LIFX_PROTOCOL_V1. - uint16le_t version:12; - //! True when the target field holds a device address. - uint16le_t addressable:1; - //! True when the target field holds tags. - uint16le_t tagged:1; - //! LIFX internal use should be 0. - uint16le_t origin:2; - } protocol; - //! This seems to be for LIFX internal use only. - uint32le_t source; - union { - //! All targeted tags ORed together. - uint64le_t tags; - //! Address of the targeted device. - uint8_t device_addr[LIFXD_ADDR_LENGTH]; - } target; - uint8_t site[LIFXD_ADDR_LENGTH]; - struct { - //! True when a response is required, called acknowledge in lifx-gem... - uint8_t response_required:1; - //! True when an acknowledgement is required, no idea what it means. - uint8_t ack_required:1; - uint8_t reserved:6; - } flags; - //! Wrap-around sequence number, LIFX internal use. - uint8_t seqn; - uint64le_t timestamp; - uint16le_t packet_type; - uint8_t reserved[2]; -}; - -enum { LIFXD_PACKET_HEADER_SIZE = sizeof(struct lifxd_packet_header) }; - -enum { LIFXD_LIFX_PROTOCOL_V1 = 1024 }; - -// Let's define a maximum packet size just in case somebody sends us weird -// headers: -enum { LIFXD_MAX_PACKET_SIZE = 4096 }; - -enum lifxd_packet_type { - LIFXD_GET_PAN_GATEWAY = 0x02, - LIFXD_PAN_GATEWAY = 0x03, - LIFXD_GET_TIME = 0x04, - LIFXD_SET_TIME = 0x05, - LIFXD_TIME_STATE = 0x06, - LIFXD_GET_RESET_SWITCH_STATE = 0x07, - LIFXD_RESET_SWITCH_STATE = 0x08, - LIFXD_GET_MESH_INFO = 0x0c, - LIFXD_MESH_INFO = 0x0d, - LIFXD_GET_MESH_FIRMWARE = 0x0e, - LIFXD_MESH_FIRMWARE = 0x0f, - LIFXD_GET_WIFI_INFO = 0x10, - LIFXD_WIFI_INFO = 0x11, - LIFXD_GET_WIFI_FIRMWARE_STATE = 0x12, - LIFXD_WIFI_FIRMWARE_STATE = 0x13, - LIFXD_GET_POWER_STATE = 0x14, - LIFXD_SET_POWER_STATE = 0x15, - LIFXD_POWER_STATE = 0x16, - LIFXD_GET_BULB_LABEL = 0x17, - LIFXD_SET_BULB_LABEL = 0x18, - LIFXD_BULB_LABEL = 0x19, - LIFXD_GET_TAGS = 0x1a, - LIFXD_SET_TAGS = 0x1b, - LIFXD_TAGS = 0x1c, - LIFXD_GET_TAG_LABELS = 0x1d, - LIFXD_SET_TAG_LABELS = 0x1e, - LIFXD_TAG_LABELS = 0x1f, - LIFXD_GET_VERSION = 0x20, - LIFXD_VERSION_STATE = 0x21, - LIFXD_GET_INFO = 0x22, - LIFXD_INFO_STATE = 0x23, - LIFXD_GET_MCU_RAIL_VOLTAGE = 0x24, - LIFXD_MCU_RAIL_VOLTAGE = 0x25, - LIFXD_REBOOT = 0x26, - LIFXD_SET_FACTORY_TEST_MODE = 0x27, - LIFXD_DISABLE_FACTORY_TEST_MODE = 0x28, - LIFXD_GET_LIGHT_STATE = 0x65, - LIFXD_SET_LIGHT_COLOUR = 0x66, - LIFXD_SET_WAVEFORM = 0x67, - LIFXD_SET_DIM_ABSOLUTE = 0x68, - LIFXD_SET_DIM_RELATIVE = 0x69, - LIFXD_LIGHT_STATUS = 0x6b, - LIFXD_GET_WIFI_STATE = 0x12d, - LIFXD_SET_WIFI_STATE = 0x12e, - LIFXD_WIFI_STATE = 0x12f, - LIFXD_GET_ACCESS_POINTS = 0x130, - LIFXD_SET_ACCESS_POINTS = 0x131, - LIFXD_ACCESS_POINT = 0x132, -}; - -enum { LIFXD_LABEL_SIZE = 32 }; - -struct lifxd_packet_light_status { - uint16le_t hue; - uint16le_t saturation; - uint16le_t brightness; - uint16le_t kelvin; - uint16le_t dim; - uint16le_t power; - uint8_t label[LIFXD_LABEL_SIZE]; - uint64be_t tags; -}; - -enum lifxd_power_state { - LIFXD_POWER_OFF = 0, - LIFXD_POWER_ON = 0xffff -}; - -struct lifxd_packet_power_state { - uint16_t power; // see enum lifxd_power_state -}; - -enum lifxd_service_type { - LIFXD_SERVICE_TCP = 1, - LIFXD_SERVICE_UDP = 2 -}; - -struct lifxd_packet_pan_gateway { - uint8_t service_type; // see enum lifxd_service_type - uint32le_t port; -}; - -enum lifxd_target_type { - LIFXD_TARGET_SITE, - LIFXD_TARGET_TAGS, - LIFXD_TARGET_DEVICE, - LIFXD_TARGET_ALL_DEVICES -}; - -#pragma pack(pop) - -struct lifxd_gateway; - -struct lifxd_packet_infos { - RB_ENTRY(lifxd_packet_infos) link; - const char *name; - enum lifxd_packet_type type; - unsigned size; - void (*decode)(void *); - void (*encode)(void *); - void (*handle)(struct lifxd_gateway *, - const struct lifxd_packet_header *, - const void *); -}; -RB_HEAD(lifxd_packet_infos_map, lifxd_packet_infos); - -static inline int -lifxd_packet_infos_cmp(struct lifxd_packet_infos *a, - struct lifxd_packet_infos *b) -{ - return a->type - b->type; -} - -union lifxd_target { - uint64_t tags; - const uint8_t *addr; //! site or device address -}; - -extern union lifxd_target LIFXD_UNSPEC_TARGET; - -const struct lifxd_packet_infos *lifxd_wire_get_packet_infos(enum lifxd_packet_type); -void lifxd_wire_load_packet_infos_map(void); - -const struct lifxd_packet_infos *lifxd_wire_setup_header(struct lifxd_packet_header *, - enum lifxd_target_type, - union lifxd_target, - const uint8_t *, - enum lifxd_packet_type); -void lifxd_wire_decode_header(struct lifxd_packet_header *); -void lifxd_wire_encode_header(struct lifxd_packet_header *); - -void lifxd_wire_decode_pan_gateway(struct lifxd_packet_pan_gateway *); -void lifxd_wire_encode_pan_gateway(struct lifxd_packet_pan_gateway *); -void lifxd_wire_decode_light_status(struct lifxd_packet_light_status *); -void lifxd_wire_encode_light_status(struct lifxd_packet_light_status *); -void lifxd_wire_decode_power_state(struct lifxd_packet_power_state *); diff --git a/dist/lightsd-freebsd-rc.sh b/dist/lightsd-freebsd-rc.sh new file mode 100755 index 0000000..dfb503d --- /dev/null +++ b/dist/lightsd-freebsd-rc.sh @@ -0,0 +1,49 @@ +#!/bin/sh +# +# $FreeBSD: head/net/lightsg/files/lightsd.in 367807 2014-09-10 09:36:24Z ehaupt $ +# + +# PROVIDE: lightsd +# REQUIRE: LOGIN +# BEFORE: securelevel +# KEYWORD: shutdown + +# Add the following lines to /etc/rc.conf to enable `lightsd': +# +# lightsd_enable="YES" +# lightsd_flags="" +# +# See lightsg(1) for lightsd_flags +# + +. /etc/rc.subr + +name="lightsd" +rcvar=lightsd_enable + +command="/usr/sbin/daemon" +start_precmd="lightsd_precmd" +pidfile="/var/run/lightsd/$name.pid" + +# read configuration and set defaults +load_rc_config "$name" +: ${lightsd_enable="NO"} +: ${lightsd_user:="lightsd"} +: ${lightsd_group:="lightsd"} +: ${lightsd_listen:="localhost:23456"} +: ${lightsd_log:="info"} +: ${lightsd_flags:=""} + + +command_args="-P $pidfile -p ${pidfile}_child -r /usr/local/bin/$name -l ${lightsd_listen} -v $lightsd_log -S $lightsd_flags 2>&1 " +#command_args="-l ${lightsd_listen} -v debug " + +lightsd_precmd() +{ + if [ ! -d "/var/run/lightsd/" ] ; then + mkdir -pv /var/run/lightsd/ + chown ${lightsd_user}:${lightsd_group} /var/run/lightsd/ + fi +} + +run_rc_command "$1" diff --git a/dist/lightsd.service b/dist/lightsd.service new file mode 100644 index 0000000..e80f64e --- /dev/null +++ b/dist/lightsd.service @@ -0,0 +1,10 @@ +[Unit] +Description=LIFX WiFi smart bulbs control service +After=network.target + +[Service] +ExecStart=/usr/bin/lightsd -t -v warning -u lightsd -s %t/lightsd/socket -c %t/lightsd/pipe +Restart=on-failure + +[Install] +WantedBy=multi-user.target diff --git a/docs/CMakeLists.txt b/docs/CMakeLists.txt new file mode 100644 index 0000000..efa6ae7 --- /dev/null +++ b/docs/CMakeLists.txt @@ -0,0 +1,9 @@ +CONFIGURE_FILE(conf.py.in "${CMAKE_CURRENT_BINARY_DIR}/conf.py") + +ADD_CUSTOM_TARGET( + docs + COMMAND "${SPHINX_EXECUTABLE}" -v -W -b html -c "${CMAKE_CURRENT_BINARY_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}" _build + WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" + COMMENT "Building the documentation in ${CMAKE_CURRENT_BINARY_DIR}/_build with sphinx-build" + VERBATIM +) diff --git a/docs/_static/.dummy b/docs/_static/.dummy new file mode 100644 index 0000000..e69de29 diff --git a/docs/changelog.rst b/docs/changelog.rst new file mode 100644 index 0000000..92170ce --- /dev/null +++ b/docs/changelog.rst @@ -0,0 +1,78 @@ +Changelog +========= + +lightsd uses `semantic versionning `_, here is the summary: + +Given a version number MAJOR.MINOR.PATCH: + +- MAJOR version gets incremented when you may need to modify your lightsd + configuration to keep your setup running; +- MINOR version gets incremented when functionality or substantial improvements + are added in a backwards-compatible manner; +- PATCH version gets incremented for backwards-compatible bug fixes. + +1.1.2 (2015-11-30) +------------------ + +- Fix LIFX LAN protocol V2 handling (properly set RES_REQUIRED and properly + listen on each gateway's socket); +- The bulb timeout has been increased from 3 to 20s; +- Improved LIFX traffic logging. + +1.1.1 (2015-11-17) +------------------ + +.. warning:: + + This release broke the compatibility with the LIFX LAN protocol "v2", please + upgrade to 1.1.2. + +- Greatly improve responsiveness by setting the LIFX source identifier [#]_. +- Fix parallel builds in the Debian package & fix the homebrew formulae for OS X + 10.11 (El Capitan). + +.. [#] http://lan.developer.lifx.com/docs/header-description#frame + +1.1.0 (2015-11-07) +------------------ + +.. note:: + + The ``-f`` (``--foreground``) option is being deprecated with this release + and isn't documented anymore, lightsd starts in the foreground by default and + this option is not necessary, please stop using it. + +New features +~~~~~~~~~~~~ + +- Add syslog support via the ``--syslog`` and ``--syslog-facility`` options + (closes :gh:`1`); +- Debian & OpenWRT packaging and installation instructions. + +Fixes +~~~~~ + +- lightsc.sh: support OSes with openssl but without a base64 utility (closes + :gh:`3`); +- lightsc.py: unix url support fixes and bump the receive buffer size to + accommodate people with many bulbs; +- Add missing product ids/models. + +1.0.1 (2015-09-18) +------------------ + +- Fix set_waveform on big endian architectures; +- Fix build under Debian oldstable; +- Fix build under OpenBSD [#]_; +- Fix process title even when no bulbs are discovered; +- Add product id for the 230V version of the LIFX White 800. + +.. [#] Using GCC 4.2, so you just need to do ``pkg_add cmake libevent`` to + build a release. + +1.0.0 (2015-09-17) +------------------ + +- First announced release. + +.. vim: set tw=80 spelllang=en spell: diff --git a/docs/conf.py.in b/docs/conf.py.in new file mode 100644 index 0000000..4242198 --- /dev/null +++ b/docs/conf.py.in @@ -0,0 +1,260 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# lightsd documentation build configuration file, created by +# sphinx-quickstart on Wed Jan 28 00:04:24 2015. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +needs_sphinx = '1.2.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ['sphinx.ext.extlinks'] + +extlinks = {'gh': ('https://github.com/lopter/lightsd/issues/%s', 'GH-')} + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'lightsd' +copyright = '2015, Louis Opter' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '@LIGHTSD_VERSION@' +# The full version, including alpha/beta/rc tags. +release = '@LIGHTSD_VERSION@' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'nature' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = [os.path.join('@CMAKE_CURRENT_SOURCE_DIR@', '_static')] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'lightsddoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ('index', 'lightsd.tex', 'lightsd Documentation', + 'Louis Opter', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'lightsd', 'lightsd Documentation', + ['Louis Opter'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'lightsd', 'lightsd Documentation', + 'Louis Opter', 'lightsd', 'Daemon to control your WiFi smart bulbs.', + 'Miscellaneous'), ] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False diff --git a/docs/developers.rst b/docs/developers.rst new file mode 100644 index 0000000..9bffd74 --- /dev/null +++ b/docs/developers.rst @@ -0,0 +1,27 @@ +Developers & companies +====================== + +lightsd's development takes place on github: https://github.com/lopter/lightsd. + +Check-out the `contribution guide`_! + +If you wanna get a feel of what I'm working on you can watch my patch queue: +https://github.com/lopter/lightsd-mq. You will need to install Mercurial_ and +hg-git_ to apply it over the main repository. Some of the patches are just +snippets I put aside; the series_ file will tell you which ones. + +Feel free to reach out via email or irc (kalessin on freenode, insist if I +don't reply). As the project name implies, I'm fairly interested in other smart +bulbs. If you are a company trying to use lightsd feel free to reach me out as +well. lightsd is free software under the GPLv3_ but has been designed in a way +that make it usable in closed source environments. + +I'm not looking for a new job, thanks. + +.. _contribution guide: https://github.com/lopter/lightsd/blob/master/CONTRIBUTING.rst +.. _Mercurial: https://mercurial.selenic.com/ +.. _hg-git: http://hg-git.github.io/ +.. _series: https://github.com/lopter/lightsd-mq +.. _GPLv3: https://github.com/lopter/lightsd/blob/master/COPYING + +.. vim: set tw=80 spelllang=en spell: diff --git a/docs/first-steps.rst b/docs/first-steps.rst new file mode 100644 index 0000000..d933716 --- /dev/null +++ b/docs/first-steps.rst @@ -0,0 +1,351 @@ +First steps +=========== + +Those instructions assume that you have followed the :doc:`installation +instructions `. + +Starting and stopping lightsd +----------------------------- + +lightsd listens for UDP traffic from the bulbs on ``0.0.0.0`` port +``udp/56700``. + +Mac OS X +~~~~~~~~ + +.. note:: + + This warning will be displayed the first time you start lightsd after an + install or upgrade: + + .. image:: /images/mac_os_x_startup_warning.png + :width: 500px + + Click Allow, lightsd uses the network to communicate with your bulbs. + +Start lightsd with: + +:: + + launchctl load ~/Library/LaunchAgents/homebrew.mxcl.lightsd.plist + +Stop lightsd with: + +:: + + launchctl unload ~/Library/LaunchAgents/homebrew.mxcl.lightsd.plist + +Check how lightsd is running with: + +:: + + ps aux | grep lightsd + +Read the logs with: + +:: + + tail -F `brew --prefix`/var/log/lightsd.log + +Try to :ref:`toggle your lights ` and read on some of the examples +bundled with lightsd. + +Linux (systemd) +~~~~~~~~~~~~~~~ + +Start lightsd with: + +:: + + systemctl start lightsd + +Stop lightsd with: + +:: + + systemctl stop lightsd + +Enable lightsd at boot: + +:: + + systemctl enable lightsd + +Check how lightsd is running with: + +:: + + ps aux | grep lightsd + +Read the logs with: + +:: + + journalctl -x -f _SYSTEMD_UNIT=lightsd.service + +Try to :ref:`toggle your lights ` and read on some of the examples +bundled with lightsd. + +Linux (System V style) +~~~~~~~~~~~~~~~~~~~~~~ + +Start lightsd with: + +:: + + /etc/init.d/lightsd start + +Stop lightsd with: + +:: + + /etc/init.d/lightsd stop + +Check how lightsd is running with: + +:: + + ps aux | grep lightsd + +The logs will be logged to `syslogd(8)`_. + +.. _syslogd(8): http://manpages.debian.org/cgi-bin/man.cgi?query=syslogd&sektion=8 + +OpenWRT (procd) +~~~~~~~~~~~~~~~ + +Start lightsd with: + +:: + + /etc/init.d/lightsd start + +Stop lightsd with: + +:: + + /etc/init.d/lightsd stop + +Enable lightsd at boot: + +:: + + /etc/init.d/lightsd enable + +Check how lightsd is running with: + +:: + + pgrep -l lightsd + +Read the logs with: + +:: + + logread -e lightsd -f + +Try to :ref:`toggle your lights ` and read on some of the examples +bundled with lightsd. + +Manually (other systems) +~~~~~~~~~~~~~~~~~~~~~~~~ + +Assuming you've just built :ref:`lightsd from the sources +`, lightsd will be in the ``core`` directory [#]_. + +The examples are communicating with lightsd through a pipe or an Unix socket, +start lightsd with them: + +:: + + core/lightsd -c pipe -s socket + +From another terminal, check how lightsd is running with: + +:: + + ps aux | grep lightsd + +You can stop lightsd with ^C (ctrl+c). + +Checkout the :ref:`examples `. + +.. [#] ``build/core`` if you start from the root of the repository. + +.. _cli: + +Command line options +~~~~~~~~~~~~~~~~~~~~ + +:: + + Usage: lightsd ... + + [-l,--listen addr:port [+]] Listen for JSON-RPC commands over TCP at + this address (can be repeated). + [-c,--command-pipe /command/fifo [+]] Open an unidirectional JSON-RPC + command pipe at this location (can be + repeated). + [-s,--socket /unix/socket [+]] Open an Unix socket at this location + (can be repeated). + [-d,--daemonize] Fork in the background. + [-p,--pidfile /path/to/pid.file] Write lightsd's pid in the given file. + [-u,--user user] Drop privileges to this user (and the + group of this user if -g is missing). + [-g,--group group] Drop privileges to this group (-g requires + the -u option to be used). + [-S,--syslog] Divert logging from the console to syslog. + [-F,--syslog-facility] Facility to use with syslog (defaults to + daemon, other possible values are user and + local0-7, see syslog(3)). + [-I,--syslog-ident] Identifier to use with syslog (defaults to + lightsd). + [-t,--no-timestamps] Disable timestamps in logs. + [-h,--help] Display this. + [-V,--version] Display version and build information. + [-v,--verbosity debug|info|warning|error] + + or, + + --prefix Display the install prefix for lightsd. + + or, + + --rundir Display the runtime directory for lightsd. + +.. _toggle: + +Toggle your lights +------------------ + +:: + + `lightsd --prefix`/share/lightsd/examples/toggle + +Or, from the root of the repository: + +:: + + examples/toggle + +Here is the source code of this example, it uses a small client —lightsc.sh— the +next section covers it: + +.. literalinclude:: ../examples/toggle + :language: sh + +.. _examples: + +Using lightsc.sh +---------------- + +`lightsc.sh`_ is a small shell script that wraps a few things around lightsd' +command pipe. Once you've sourced it with: + +:: + + . `lightsd --prefix`/share/lightsd/lightsc.sh + +Or, from the root of the repository: + +:: + + . share/lightsc.sh + +You can use the following variable and functions to send commands to your bulbs +from your current shell or shell script: + +.. data:: LIGHTSD_COMMAND_PIPE + + By default lightsc will use ```lightsd --rundir`/pipe`` but you can set that + to your own value. + +.. describe:: lightsc method [arguments…] + + Call the given :ref:`method ` with the given arguments. + lightsc display the generated JSON that was sent. + +.. describe:: lightsc_get_pipe + + Equivalent to ``${LIGHTSD_COMMAND_PIPE:-`lightsd --rundir`/pipe}`` but also + check if lightsd is running. + +.. describe:: lightsc_make_request method [arguments…] + + Like lightsc but display the generated json instead of sending it out to + lightsd: with this and lightsc_get_pipe you can do batch requests: + +.. note:: + + Keep in mind that arguments must be JSON, you will have to enclose tags and + labels into double quotes ``'"likethis"'``. The command pipe is write-only: + you cannot read any result back. + +Examples: + +Build a batch request manually: + +.. pygmentize sucks on heredocs: + +:: + + tee `lightsc_get_pipe` <` to see everything you can do! + +.. _lightsc.py: https://github.com/lopter/lightsd/blob/master/examples/lightsc.py + +.. vim: set tw=80 spelllang=en spell: diff --git a/docs/images/mac_os_x_startup_warning.png b/docs/images/mac_os_x_startup_warning.png new file mode 100644 index 0000000..a76da03 Binary files /dev/null and b/docs/images/mac_os_x_startup_warning.png differ diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..f713901 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,42 @@ +The lights daemon documentation +=============================== + +Welcome! This is the documentation for lightsd_: a small program that runs in +the background, discovers smart bulbs [#bulbs]_ on your local network and let +you control them easily. + +lightsd exposes a JSON-RPC_ interface over TCP IPv4/IPv6 [#tcp]_ and Unix +sockets. The same interface can be exposed over a `named pipe`_: in that case +responses can't be read back from lightsd but this is useful to control your +lights from very basic shell scripts. + +lightsd works out of the box on Mac OS X and Arch Linux but is very easy to +build thanks to its very limited requirements. Check-out the installation +instructions: + +.. toctree:: + :maxdepth: 2 + + installation + first-steps + protocol + known-issues + developers + packaging + changelog + +.. [#bulbs] Currently only LIFX_ WiFi smart bulbs are supported. +.. [#tcp] And not over HTTP like most JSON-RPC implementations. + +.. _lightsd: https://github.com/lopter/lightsd +.. _LIFX: http://www.lifx.co/ +.. _JSON-RPC: http://www.jsonrpc.org/specification +.. _named pipe: http://www.openbsd.org/cgi-bin/man.cgi?query=mkfifo + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`search` + +.. vim: set tw=80 spelllang=en spell: diff --git a/docs/installation.rst b/docs/installation.rst new file mode 100644 index 0000000..0cfc58b --- /dev/null +++ b/docs/installation.rst @@ -0,0 +1,178 @@ +Installation instructions +========================= + +Installation (for Mac OS X) +--------------------------- + +Make sure you have brew installed and updated: http://brew.sh/. + +:: + + brew install lopter/lightsd/lightsd + +Or, + +:: + + brew tap lopter/lightsd + brew install lightsd + + +Make sure you execute the ``ln -sfv`` command displayed at the end of the +installation: + +:: + + ln -sfv /usr/local/opt/lightsd/*.plist ~/Library/LaunchAgents + +Please, also install Python 3 and ipython if you want to follow the examples in +the next section: + +:: + + brew install python3 + pip3 install ipython + +Read on :doc:`/first-steps` to see how to use lightsd. + +Installation (for Arch Linux) +----------------------------- + +Make sure you have Yaourt installed: https://archlinux.fr/yaourt-en (`wiki +page`_). + +:: + + yaourt -Sya lightsd + +Make sure to follow the post-installation instructions: replace ``$USER`` with +the user you usually use. + + +Please also install ipython if you want to follow the examples in the next +section: + +:: + + yaourt -Sya ipython + +Read on :doc:`/first-steps` to see how to use lightsd. + +.. _wiki page: https://wiki.archlinux.org/index.php/Yaourt + + +Installation (OpenWRT trunk) +---------------------------- + +If you're running `OpenWRT trunk`_ then, from your build root, just add +lightsd's feed: + +:: + + cat >>feeds.conf`[ -f feeds.conf ] || echo .default` < +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import argparse +import contextlib +import json +import locale +import os +import socket +import subprocess +import sys +import urllib.parse +import uuid + + +class LightsClient: + + READ_SIZE = 4096 + TIMEOUT = 2 # seconds + ENCODING = "utf-8" + + class Error(Exception): + pass + + class TimeoutError(Error): + pass + + class JSONError(Error): + + def __init__(self, response): + self._response = response + + def __str__(self): + return "received invalid JSON: {}".format(self._response) + + def __init__(self, url, encoding=ENCODING, timeout=TIMEOUT, + read_size=READ_SIZE): + self.url = url + self.encoding = encoding + + parts = urllib.parse.urlparse(args.url) + + if parts.scheme == "unix": + self._socket = socket.socket(socket.AF_UNIX) + path = os.path.join(parts.netloc, parts.path).rstrip(os.path.sep) + self._socket.connect(path) + elif parts.scheme == "tcp": + self._socket = socket.create_connection((parts.hostname, parts.port)) + else: + raise ValueError("Unsupported url {}".format(url)) + self._socket.settimeout(timeout) + + self._read_size = read_size + self._pipeline = [] + self._batch = False + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, exc_tb): + self.close() + + @classmethod + def _make_payload(cls, method, params): + return { + "method": method, + "params": params, + "jsonrpc": "2.0", + "id": str(uuid.uuid4()), + } + + def _execute_payload(self, payload): + response = bytearray() + try: + payload = json.dumps(payload) + payload = payload.encode(self.encoding, "surrogateescape") + self._socket.sendall(payload) + + while True: + response += self._socket.recv(self._read_size) + try: + return json.loads(response.decode( + self.encoding, "surrogateescape" + )) + except Exception: + continue + except socket.timeout: + if not response: + raise self.TimeoutError + raise self.JSONError(response) + + def _jsonrpc_call(self, method, params): + payload = self._make_payload(method, params) + if self._batch: + self._pipeline.append(payload) + return + return self._execute_payload(payload) + + def close(self): + self._socket.close() + + @contextlib.contextmanager + def batch(self): + self._batch = True + response = [] + yield response + self._batch = False + result = self._execute_payload(self._pipeline) + if isinstance(result, list): + response.extend(result) + else: + response.append(result) + self._pipeline = [] + + def set_light_from_hsbk(self, target, h, s, b, k, t): + return self._jsonrpc_call("set_light_from_hsbk", [ + target, h, s, b, k, t + ]) + + def set_waveform(self, target, waveform, + h, s, b, k, + period, cycles, skew_ratio, transient): + return self._jsonrpc_call("set_waveform", [ + target, waveform, h, s, b, k, period, cycles, skew_ratio, transient + ]) + + def saw(self, target, h, s, b, k, period, cycles, transient=True): + return self.set_waveform( + target, "SAW", h, s, b, k, + cycles=cycles, + period=period, + skew_ratio=0.5, + transient=transient + ) + + def sine(self, target, h, s, b, k, + period, cycles, peak=0.5, transient=True): + return self.set_waveform( + target, "SINE", h, s, b, k, + cycles=cycles, + period=period, + skew_ratio=peak, + transient=transient + ) + + def half_sine(self, target, h, s, b, k, period, cycles, transient=True): + return self.set_waveform( + target, "HALF_SINE", h, s, b, k, + cycles=cycles, + period=period, + skew_ratio=0.5, + transient=transient + ) + + def triangle(self, target, h, s, b, k, + period, cycles, peak=0.5, transient=True): + return self.set_waveform( + target, "TRIANGLE", h, s, b, k, + cycles=cycles, + period=period, + skew_ratio=peak, + transient=transient + ) + + def square(self, target, h, s, b, k, period, cycles, + duty_cycle=0.5, transient=True): + return self.set_waveform( + target, "SQUARE", h, s, b, k, + cycles=cycles, + period=period, + skew_ratio=duty_cycle, + transient=transient + ) + + def power_on(self, target): + return self._jsonrpc_call("power_on", {"target": target}) + + def power_off(self, target): + return self._jsonrpc_call("power_off", {"target": target}) + + def power_toggle(self, target): + return self._jsonrpc_call("power_toggle", {"target": target}) + + def get_light_state(self, target): + return self._jsonrpc_call("get_light_state", [target]) + + def tag(self, target, tag): + return self._jsonrpc_call("tag", [target, tag]) + + def untag(self, target, tag): + return self._jsonrpc_call("untag", [target, tag]) + + def set_label(self, target, label): + return self._jsonrpc_call("set_label", [target, label]) + + def adjust_brightness(self, target, adjustment): + bulbs = self.get_light_state(target)["result"] + for bulb in bulbs: + h, s, b, k = bulb["hsbk"] + b = max(min(b + adjustment, 1.0), 0.0) + self.set_light_from_hsbk(bulb["label"], h, s, b, k, 500) + + +def _drop_to_shell(lightsc): + c = lightsc # noqa + banner = ( + "Connected to {}, use the variable c to interact with your " + "bulbs:\n\n>>> r = c.get_light_state(\"*\")".format(c.url) + ) + + try: + from IPython import embed + + embed(header=banner + "\n>>> r") + return + except ImportError: + pass + + import code + + banner += "\n>>> from pprint import pprint\n>>> pprint(r)\n" + code.interact(banner=banner, local=locals()) + +if __name__ == "__main__": + try: + lightsdrundir = subprocess.check_output(["lightsd", "--rundir"]) + except Exception as ex: + print( + "Couldn't infer lightsd's runtime directory, is lightsd installed? " + "({})\nTrying build/socket...".format(ex), + file=sys.stderr + ) + lightscdir = os.path.realpath(__file__).split(os.path.sep)[:-2] + lightsdrundir = os.path.join(*[os.path.sep] + lightscdir + ["build"]) + else: + encoding = locale.getpreferredencoding() + lightsdrundir = lightsdrundir.decode(encoding).strip() + + parser = argparse.ArgumentParser( + description="Interactive lightsd Python client" + ) + parser.add_argument( + "-u", "--url", type=str, + help="How to connect to lightsd (e.g: " + "unix:///run/lightsd/socket or tcp://[::1]:1234)", + default="unix://" + os.path.join(lightsdrundir, "socket") + ) + args = parser.parse_args() + + try: + print("Connecting to lightsd@{}".format(args.url)) + with LightsClient(args.url) as client: + _drop_to_shell(client) + except Exception as ex: + print( + "Couldn't connect to {}, is lightsd running? " + "({})".format(args.url, ex), + file=sys.stderr + ) + sys.exit(1) diff --git a/examples/toggle b/examples/toggle new file mode 100755 index 0000000..c38eab2 --- /dev/null +++ b/examples/toggle @@ -0,0 +1,5 @@ +#!/bin/sh + +. `lightsd --prefix`/share/lightsd/lightsc.sh + +lightsc power_toggle ${*:-'"*"'} diff --git a/lifx/CMakeLists.txt b/lifx/CMakeLists.txt new file mode 100644 index 0000000..b3e118f --- /dev/null +++ b/lifx/CMakeLists.txt @@ -0,0 +1,16 @@ +INCLUDE_DIRECTORIES( + ${CMAKE_CURRENT_SOURCE_DIR}/../ + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR}/../ + ${CMAKE_CURRENT_BINARY_DIR} +) + +ADD_LIBRARY( + lifx + broadcast.c + bulb.c + discovery.c + gateway.c + tagging.c + wire_proto.c +) diff --git a/lifx/broadcast.c b/lifx/broadcast.c new file mode 100644 index 0000000..cbbf030 --- /dev/null +++ b/lifx/broadcast.c @@ -0,0 +1,235 @@ +// Copyright (c) 2014, Louis Opter +// +// This file is part of lighstd. +// +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "wire_proto.h" +#include "core/time_monotonic.h" +#include "bulb.h" +#include "gateway.h" +#include "broadcast.h" +#include "core/lightsd.h" + +static struct { + evutil_socket_t socket; + struct event *read_ev; + struct event *write_ev; +} lgtd_lifx_broadcast_endpoint = { + .socket = -1, + .read_ev = NULL, + .write_ev = NULL, +}; + +static bool +lgtd_lifx_broadcast_handle_write(void) +{ + assert(lgtd_lifx_broadcast_endpoint.socket != -1); + + struct sockaddr_in lifx_addr = { + .sin_family = AF_INET, + .sin_addr = { INADDR_BROADCAST }, + .sin_port = htons(LGTD_LIFX_PROTOCOL_PORT), + .sin_zero = { 0 } + }; + struct lgtd_lifx_packet_header get_pan_gateway; + lgtd_lifx_wire_setup_header( + &get_pan_gateway, + LGTD_LIFX_TARGET_ALL_DEVICES, + LGTD_LIFX_UNSPEC_TARGET, + NULL, + LGTD_LIFX_GET_PAN_GATEWAY + ); + + int nbytes; +retry: + nbytes = sendto( + lgtd_lifx_broadcast_endpoint.socket, + (void *)&get_pan_gateway, + sizeof(get_pan_gateway), + 0, + (const struct sockaddr *)&lifx_addr, + sizeof(lifx_addr) + ); + if (nbytes == sizeof(get_pan_gateway)) { + if (event_del(lgtd_lifx_broadcast_endpoint.write_ev)) { + lgtd_err(1, "can't setup events"); + } + return true; + } + if (nbytes == -1) { + if (EVUTIL_SOCKET_ERROR() == EINTR) { + goto retry; + } + lgtd_warn("can't broadcast discovery packet"); + } else { + lgtd_warnx("can't broadcast discovery packet"); + } + return false; +} + +static void +lgtd_lifx_broadcast_event_callback(evutil_socket_t socket, + short events, + void *ctx) +{ + (void)socket; + (void)ctx; + + if (events & EV_TIMEOUT) { + // not sure how that could happen but eh. + lgtd_warnx("timeout on the udp broadcast socket"); + goto error_reset; + } + if ((events & EV_READ) && !lgtd_lifx_wire_handle_receive(socket, NULL)) { + goto error_reset; + } + if ((events & EV_WRITE) && !lgtd_lifx_broadcast_handle_write()) { + goto error_reset; + } + + return; + +error_reset: + lgtd_lifx_broadcast_close(); + lgtd_lifx_broadcast_setup(); +} + +void +lgtd_lifx_broadcast_close(void) +{ + if (lgtd_lifx_broadcast_endpoint.read_ev) { + event_del(lgtd_lifx_broadcast_endpoint.read_ev); + event_free(lgtd_lifx_broadcast_endpoint.read_ev); + lgtd_lifx_broadcast_endpoint.read_ev = NULL; + } + if (lgtd_lifx_broadcast_endpoint.write_ev) { + event_del(lgtd_lifx_broadcast_endpoint.write_ev); + event_free(lgtd_lifx_broadcast_endpoint.write_ev); + lgtd_lifx_broadcast_endpoint.write_ev = NULL; + } + if (lgtd_lifx_broadcast_endpoint.socket != -1) { + evutil_closesocket(lgtd_lifx_broadcast_endpoint.socket); + lgtd_lifx_broadcast_endpoint.socket = -1; + } +} + +bool +lgtd_lifx_broadcast_setup(void) +{ + assert(lgtd_lifx_broadcast_endpoint.socket == -1); + assert(lgtd_lifx_broadcast_endpoint.read_ev == NULL); + assert(lgtd_lifx_broadcast_endpoint.write_ev == NULL); + + lgtd_lifx_broadcast_endpoint.socket = socket( + AF_INET, SOCK_DGRAM, IPPROTO_UDP + ); + if (lgtd_lifx_broadcast_endpoint.socket == -1) { + return false; + } + + int val = 1; + int err = setsockopt( + lgtd_lifx_broadcast_endpoint.socket, + SOL_SOCKET, + SO_BROADCAST, + &val, + sizeof(val) + ); + if (err) { + goto error; + } + err = evutil_make_listen_socket_reuseable( + lgtd_lifx_broadcast_endpoint.socket + ); + if (err) { + goto error; + } + + err = evutil_make_socket_nonblocking(lgtd_lifx_broadcast_endpoint.socket); + if (err == -1) { + goto error; + } + + struct sockaddr_in lifx_addr = { + .sin_family = AF_INET, + .sin_addr = { INADDR_ANY }, + .sin_port = htons(LGTD_LIFX_PROTOCOL_PORT), + .sin_zero = { 0 } + }; + + err = bind( + lgtd_lifx_broadcast_endpoint.socket, + (const struct sockaddr *)&lifx_addr, + sizeof(lifx_addr) + ); + if (err) { + goto error; + } + + lgtd_lifx_broadcast_endpoint.read_ev = event_new( + lgtd_ev_base, + lgtd_lifx_broadcast_endpoint.socket, + EV_READ|EV_PERSIST, + lgtd_lifx_broadcast_event_callback, + NULL + ); + lgtd_lifx_broadcast_endpoint.write_ev = event_new( + lgtd_ev_base, + lgtd_lifx_broadcast_endpoint.socket, + EV_WRITE|EV_PERSIST, + lgtd_lifx_broadcast_event_callback, + NULL + ); + if (!lgtd_lifx_broadcast_endpoint.read_ev + || !lgtd_lifx_broadcast_endpoint.write_ev) { + goto error; + } + + if (!event_add(lgtd_lifx_broadcast_endpoint.read_ev, NULL)) { + return true; + } + + int errsave; +error: + errsave = errno; + lgtd_lifx_broadcast_close(); + errno = errsave; + return false; +} + +bool +lgtd_lifx_broadcast_discovery(void) +{ + assert(lgtd_lifx_broadcast_endpoint.write_ev); + return event_add(lgtd_lifx_broadcast_endpoint.write_ev, NULL) == 0; +} diff --git a/lifx/broadcast.h b/lifx/broadcast.h new file mode 100644 index 0000000..451acca --- /dev/null +++ b/lifx/broadcast.h @@ -0,0 +1,22 @@ +// Copyright (c) 2014, Louis Opter +// +// This file is part of lighstd. +// +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . + +#pragma once + +bool lgtd_lifx_broadcast_setup(void); +void lgtd_lifx_broadcast_close(void); +bool lgtd_lifx_broadcast_discovery(void); diff --git a/lifx/bulb.c b/lifx/bulb.c new file mode 100644 index 0000000..02ad5e3 --- /dev/null +++ b/lifx/bulb.c @@ -0,0 +1,344 @@ +// Copyright (c) 2014, Louis Opter +// +// This file is part of lighstd. +// +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "wire_proto.h" +#include "core/time_monotonic.h" +#include "bulb.h" +#include "gateway.h" +#include "core/daemon.h" +#include "core/timer.h" +#include "core/stats.h" +#include "core/jsmn.h" +#include "core/jsonrpc.h" +#include "core/client.h" +#include "core/proto.h" +#include "core/router.h" +#include "core/lightsd.h" + +struct lgtd_lifx_bulb_map lgtd_lifx_bulbs_table = + RB_INITIALIZER(&lgtd_lifx_bulbs_table); + +const char * const lgtd_lifx_bulb_ip_names[] = { "mcu", "wifi" }; + +static const char * +lgtd_lifx_bulb_get_model_name(uint32_t vendor_id, uint32_t product_id) +{ + if (vendor_id != LGTD_LIFX_VENDOR_ID) { + return "Unknown"; + } + + switch (product_id) { + case 0x1: + case 0x2: + return "Original 1000"; + case 0x3: + return "Color 650"; + case 0xa: + case 0xb: + return "White 800"; + case 0x12: + return "White 900 BR30"; + case 0x16: + return "Color 1000"; + default: + return "Unknown"; + } +} + +static const char * +lgtd_lifx_bulb_get_vendor_name(uint32_t vendor_id) +{ + switch (vendor_id) { + case LGTD_LIFX_VENDOR_ID: + return "LIFX"; + default: + return "Unknown"; + } +} + +static void +lgtd_lifx_bulb_fetch_hardware_info(struct lgtd_timer *timer, + union lgtd_timer_ctx ctx) +{ + assert(timer); + assert(ctx.as_uint); + + // Get the bulb again, it might have been closed while we were waiting: + const uint8_t *bulb_addr = (const uint8_t *)&ctx.as_uint; + struct lgtd_lifx_bulb *bulb = lgtd_lifx_bulb_get(bulb_addr); + if (!bulb) { + lgtd_timer_stop(timer); + return; + } + +#define RESEND_IF(test, pkt_type) do { \ + if ((test)) { \ + stop = false; \ + lgtd_router_send_to_device(bulb, (pkt_type), NULL); \ + } \ +} while (0) + + bool stop = true; + RESEND_IF(!bulb->product_info.vendor_id, LGTD_LIFX_GET_VERSION); + RESEND_IF( + !bulb->ips[LGTD_LIFX_BULB_MCU_IP].fw_info.version, + LGTD_LIFX_GET_MESH_FIRMWARE + ); + lgtd_time_mono_t state_updated_at; + state_updated_at = bulb->ips[LGTD_LIFX_BULB_MCU_IP].state_updated_at; + lgtd_time_mono_t timeout = LGTD_LIFX_BULB_FETCH_WIFI_FW_INFO_TIMEOUT_MSECS; + RESEND_IF( + ( + !bulb->ips[LGTD_LIFX_BULB_WIFI_IP].fw_info.version + && (lgtd_time_monotonic_msecs() - state_updated_at < timeout) + ), + LGTD_LIFX_GET_WIFI_FIRMWARE_STATE + ); + if (stop) { + lgtd_timer_stop(timer); + } +} + +struct lgtd_lifx_bulb * +lgtd_lifx_bulb_get(const uint8_t *addr) +{ + assert(addr); + + struct lgtd_lifx_bulb bulb; + memcpy(bulb.addr, addr, sizeof(bulb.addr)); + return RB_FIND(lgtd_lifx_bulb_map, &lgtd_lifx_bulbs_table, &bulb); +} + +struct lgtd_lifx_bulb * +lgtd_lifx_bulb_open(struct lgtd_lifx_gateway *gw, const uint8_t *addr) +{ + assert(gw); + assert(addr); + + struct lgtd_lifx_bulb *bulb = calloc(1, sizeof(*bulb)); + if (!bulb) { + lgtd_warn("can't allocate a new bulb"); + return NULL; + } + + bulb->gw = gw; + memcpy(bulb->addr, addr, sizeof(bulb->addr)); + RB_INSERT(lgtd_lifx_bulb_map, &lgtd_lifx_bulbs_table, bulb); + LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(bulbs, 1); + + bulb->last_light_state_at = lgtd_time_monotonic_msecs(); + + union lgtd_timer_ctx ctx = { .as_uint = 0 }; + memcpy(&ctx.as_uint, addr, LGTD_LIFX_ADDR_LENGTH); + struct lgtd_timer *timer = lgtd_timer_start( + LGTD_TIMER_ACTIVATE_NOW|LGTD_TIMER_PERSISTENT, + LGTD_LIFX_BULB_FETCH_HARDWARE_INFO_TIMER_MSECS, + lgtd_lifx_bulb_fetch_hardware_info, + ctx + ); + if (!timer) { + lgtd_warn("can't start a timer to fetch the bulb hardware specs"); + } + + return bulb; +} + +void +lgtd_lifx_bulb_close(struct lgtd_lifx_bulb *bulb) +{ + assert(bulb); + assert(bulb->gw); + +#ifndef NDEBUG + // FIXME: Yeah, so an unit test lgtd_lifx_gateway_remove_and_close_bulb + // would be better because it can be automated, but this looks so much + // easier to do and this code path is often exercised: + int tag_id; + LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, bulb->state.tags) { + int n = 0; + struct lgtd_lifx_bulb *gw_bulb; + SLIST_FOREACH(gw_bulb, &bulb->gw->bulbs, link_by_gw) { + assert(gw_bulb != bulb); + if (LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id) & gw_bulb->state.tags) { + n++; + } + } + assert(bulb->gw->tag_refcounts[tag_id] == n); + } +#endif + + LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(bulbs, -1); + if (bulb->state.power == LGTD_LIFX_POWER_ON) { + LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(bulbs_powered_on, -1); + } + RB_REMOVE(lgtd_lifx_bulb_map, &lgtd_lifx_bulbs_table, bulb); + char addr[LGTD_LIFX_ADDR_STRLEN]; + lgtd_info( + "closed bulb \"%.*s\" (%s) on %s", + LGTD_LIFX_LABEL_SIZE, + bulb->state.label, + LGTD_IEEE8023MACTOA(bulb->addr, addr), + bulb->gw->peeraddr + ); + free(bulb); +} + +bool +lgtd_lifx_bulb_has_label(const struct lgtd_lifx_bulb *bulb, + const char *label) +{ + assert(bulb); + assert(label); + + const char *bulb_label = &bulb->state.label[0]; + const char *endp = memchr(bulb_label, 0, LGTD_LIFX_LABEL_SIZE); + int bulb_label_len = endp ? endp - bulb_label : LGTD_LIFX_LABEL_SIZE; + // clipping the label at 32 chars seems like the desired default behavior: + int label_len = LGTD_MIN(strlen(label), LGTD_LIFX_LABEL_SIZE); + return label_len == bulb_label_len && !memcmp(bulb_label, label, label_len); +} + +void +lgtd_lifx_bulb_set_light_state(struct lgtd_lifx_bulb *bulb, + const struct lgtd_lifx_light_state *state, + lgtd_time_mono_t received_at) +{ + assert(bulb); + assert(state); + + if (state->power != bulb->state.power) { + LGTD_STATS_ADD_AND_UPDATE_PROCTITLE( + bulbs_powered_on, state->power == LGTD_LIFX_POWER_ON ? 1 : -1 + ); + lgtd_router_send_to_device(bulb, LGTD_LIFX_GET_INFO, NULL); + } + + lgtd_lifx_gateway_update_tag_refcounts(bulb->gw, bulb->state.tags, state->tags); + + bulb->last_light_state_at = received_at; + memcpy(&bulb->state, state, sizeof(bulb->state)); +} + +void +lgtd_lifx_bulb_set_power_state(struct lgtd_lifx_bulb *bulb, uint16_t power) +{ + assert(bulb); + + if (power != bulb->state.power) { + LGTD_STATS_ADD_AND_UPDATE_PROCTITLE( + bulbs_powered_on, power == LGTD_LIFX_POWER_ON ? 1 : -1 + ); + lgtd_router_send_to_device(bulb, LGTD_LIFX_GET_INFO, NULL); + } + + bulb->state.power = power; +} + +void +lgtd_lifx_bulb_set_tags(struct lgtd_lifx_bulb *bulb, uint64_t tags) +{ + assert(bulb); + + lgtd_lifx_gateway_update_tag_refcounts(bulb->gw, bulb->state.tags, tags); + + bulb->state.tags = tags; +} + +void +lgtd_lifx_bulb_set_ip_state(struct lgtd_lifx_bulb *bulb, + enum lgtd_lifx_bulb_ips ip_id, + const struct lgtd_lifx_ip_state *state, + lgtd_time_mono_t received_at) +{ + assert(bulb); + assert(state); + + struct lgtd_lifx_bulb_ip *ip = &bulb->ips[ip_id]; + ip->state_updated_at = received_at; + memcpy(&ip->state, state, sizeof(ip->state)); +} + +void +lgtd_lifx_bulb_set_ip_firmware_info(struct lgtd_lifx_bulb *bulb, + enum lgtd_lifx_bulb_ips ip_id, + const struct lgtd_lifx_ip_firmware_info *info, + lgtd_time_mono_t received_at) +{ + assert(bulb); + assert(info); + + struct lgtd_lifx_bulb_ip *ip = &bulb->ips[ip_id]; + ip->fw_info_updated_at = received_at; + memcpy(&ip->fw_info, info, sizeof(ip->fw_info)); +} + +void +lgtd_lifx_bulb_set_product_info(struct lgtd_lifx_bulb *bulb, + const struct lgtd_lifx_product_info *info) +{ + assert(bulb); + assert(info); + + memcpy(&bulb->product_info, info, sizeof(bulb->product_info)); + bulb->vendor = lgtd_lifx_bulb_get_vendor_name(info->vendor_id); + bulb->model = lgtd_lifx_bulb_get_model_name( + info->vendor_id, info->product_id + ); +} + +void +lgtd_lifx_bulb_set_runtime_info(struct lgtd_lifx_bulb *bulb, + const struct lgtd_lifx_runtime_info *info, + lgtd_time_mono_t received_at) +{ + assert(bulb); + assert(info); + + bulb->runtime_info_updated_at = received_at; + memcpy(&bulb->runtime_info, info, sizeof(bulb->runtime_info)); +} + +void +lgtd_lifx_bulb_set_label(struct lgtd_lifx_bulb *bulb, + const char label[LGTD_LIFX_LABEL_SIZE]) +{ + assert(bulb); + + memcpy(bulb->state.label, label, LGTD_LIFX_LABEL_SIZE); +} + +void +lgtd_lifx_bulb_set_ambient_light(struct lgtd_lifx_bulb *bulb, float illuminance) +{ + assert(bulb); + + bulb->ambient_light = illuminance; +} diff --git a/lifx/bulb.h b/lifx/bulb.h new file mode 100644 index 0000000..b4b82e5 --- /dev/null +++ b/lifx/bulb.h @@ -0,0 +1,147 @@ +// Copyright (c) 2014, Louis Opter +// +// This file is part of lighstd. +// +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . + +#pragma once + +struct lgtd_lifx_gateway; + +#pragma pack(push, 1) +struct lgtd_lifx_light_state { + uint16_t hue; + uint16_t saturation; + uint16_t brightness; + uint16_t kelvin; + uint16_t dim; + uint16_t power; + char label[LGTD_LIFX_LABEL_SIZE]; + uint64_t tags; +}; + +struct lgtd_lifx_ip_state { + float signal_strength; // mW + uint32_t tx_bytes; + uint32_t rx_bytes; + uint16_t temperature; // Deci-celcius: e.g 24.3 -> 2430 +}; + +struct lgtd_lifx_ip_firmware_info { + uint64_t built_at; // ns since epoch + uint64_t installed_at; // ns since epoch + uint32_t version; +}; + +struct lgtd_lifx_product_info { + uint32_t vendor_id; + uint32_t product_id; + uint32_t version; +}; + +struct lgtd_lifx_runtime_info { + uint64_t time; // ns since epoch + uint64_t uptime; // ns + uint64_t downtime; // ns, last power off period duration +}; +#pragma pack(pop) + +enum { LGTD_LIFX_BULB_FETCH_HARDWARE_INFO_TIMER_MSECS = 5000 }; +// non-gateway fw 1.1 bulbs will never send this information, so just bail out +// after a few tries: +enum { + LGTD_LIFX_BULB_FETCH_WIFI_FW_INFO_TIMEOUT_MSECS = + LGTD_LIFX_BULB_FETCH_HARDWARE_INFO_TIMER_MSECS * 4 +}; + +enum lgtd_lifx_bulb_ips { + LGTD_LIFX_BULB_MCU_IP = 0, + LGTD_LIFX_BULB_WIFI_IP, + LGTD_LIFX_BULB_IP_COUNT, +}; + +// keyed with enum lgtd_lifx_bulb_ips: +extern const char * const lgtd_lifx_bulb_ip_names[]; + +struct lgtd_lifx_bulb_ip { + struct lgtd_lifx_ip_state state; + lgtd_time_mono_t state_updated_at; + struct lgtd_lifx_ip_firmware_info fw_info; + lgtd_time_mono_t fw_info_updated_at; +}; + +struct lgtd_lifx_bulb { + RB_ENTRY(lgtd_lifx_bulb) link; + SLIST_ENTRY(lgtd_lifx_bulb) link_by_gw; + lgtd_time_mono_t last_light_state_at; + lgtd_time_mono_t runtime_info_updated_at; + lgtd_time_mono_t dirty_at; + uint16_t expected_power_on; + uint8_t addr[LGTD_LIFX_ADDR_LENGTH]; + float ambient_light; // lux + const char *model; + const char *vendor; + struct lgtd_lifx_gateway *gw; + struct lgtd_lifx_light_state state; + struct lgtd_lifx_bulb_ip ips[LGTD_LIFX_BULB_IP_COUNT]; + struct lgtd_lifx_product_info product_info; + struct lgtd_lifx_runtime_info runtime_info; +}; +RB_HEAD(lgtd_lifx_bulb_map, lgtd_lifx_bulb); +SLIST_HEAD(lgtd_lifx_bulb_list, lgtd_lifx_bulb); + +extern struct lgtd_lifx_bulb_map lgtd_lifx_bulbs_table; + +static inline int +lgtd_lifx_bulb_cmp(const struct lgtd_lifx_bulb *a, const struct lgtd_lifx_bulb *b) +{ + return memcmp(a->addr, b->addr, sizeof(a->addr)); +} + +RB_GENERATE_STATIC( + lgtd_lifx_bulb_map, + lgtd_lifx_bulb, + link, + lgtd_lifx_bulb_cmp +); + +struct lgtd_lifx_bulb *lgtd_lifx_bulb_get(const uint8_t *); +struct lgtd_lifx_bulb *lgtd_lifx_bulb_open(struct lgtd_lifx_gateway *, const uint8_t *); +void lgtd_lifx_bulb_close(struct lgtd_lifx_bulb *); + +bool lgtd_lifx_bulb_has_label(const struct lgtd_lifx_bulb *, + const char *); + +void lgtd_lifx_bulb_set_light_state(struct lgtd_lifx_bulb *, + const struct lgtd_lifx_light_state *, + lgtd_time_mono_t); +void lgtd_lifx_bulb_set_power_state(struct lgtd_lifx_bulb *, uint16_t); +void lgtd_lifx_bulb_set_tags(struct lgtd_lifx_bulb *, uint64_t); + +void lgtd_lifx_bulb_set_ip_state(struct lgtd_lifx_bulb *bulb, + enum lgtd_lifx_bulb_ips ip_id, + const struct lgtd_lifx_ip_state *state, + lgtd_time_mono_t received_at); +void lgtd_lifx_bulb_set_ip_firmware_info(struct lgtd_lifx_bulb *bulb, + enum lgtd_lifx_bulb_ips ip_id, + const struct lgtd_lifx_ip_firmware_info *info, + lgtd_time_mono_t received_at); +void lgtd_lifx_bulb_set_product_info(struct lgtd_lifx_bulb *, + const struct lgtd_lifx_product_info *); +void lgtd_lifx_bulb_set_runtime_info(struct lgtd_lifx_bulb *, + const struct lgtd_lifx_runtime_info *, + lgtd_time_mono_t); +void lgtd_lifx_bulb_set_label(struct lgtd_lifx_bulb *, + const char [LGTD_LIFX_LABEL_SIZE]); +void lgtd_lifx_bulb_set_ambient_light(struct lgtd_lifx_bulb *, float); diff --git a/lifx/discovery.c b/lifx/discovery.c new file mode 100644 index 0000000..e24b8e2 --- /dev/null +++ b/lifx/discovery.c @@ -0,0 +1,216 @@ +// Copyright (c) 2015, Louis Opter +// +// This file is part of lighstd. +// +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "wire_proto.h" +#include "core/time_monotonic.h" +#include "broadcast.h" +#include "bulb.h" +#include "gateway.h" +#include "discovery.h" +#include "core/lightsd.h" + +static struct event *lgtd_watchdog_interval_ev = NULL; +static struct event *lgtd_discovery_timeout_ev = NULL; +static int lgtd_discovery_timeout = + LGTD_LIFX_DISCOVERY_ACTIVE_DISCOVERY_INTERVAL_MSECS; + +static void +lgtd_lifx_discovery_timeout_event_callback(evutil_socket_t socket, + short events, + void *ctx) +{ + (void)socket; + (void)events; + (void)ctx; + + if (LIST_EMPTY(&lgtd_lifx_gateways)) { + lgtd_discovery_timeout = + LGTD_LIFX_DISCOVERY_ACTIVE_DISCOVERY_INTERVAL_MSECS; + lgtd_debug( + "discovery didn't returned anything in %dms, restarting it", + lgtd_discovery_timeout + ); + } else { + lgtd_discovery_timeout = LGTD_MIN( + lgtd_discovery_timeout * 2, + LGTD_LIFX_DISCOVERY_PASSIVE_DISCOVERY_INTERVAL_MSECS + ); + lgtd_debug( + "sending periodic discovery packet, timeout=%d", + lgtd_discovery_timeout + ); + } + + struct timeval tv = LGTD_MSECS_TO_TIMEVAL(lgtd_discovery_timeout); + if (event_add(lgtd_discovery_timeout_ev, &tv) + || !lgtd_lifx_broadcast_discovery()) { + lgtd_err(1, "can't start discovery"); + } +} + +static void +lgtd_lifx_discovery_watchdog_interval_callback(evutil_socket_t socket, + short events, + void *ctx) +{ + (void)socket; + (void)events; + (void)ctx; + + bool start_discovery = false; + lgtd_time_mono_t now = lgtd_time_monotonic_msecs(); + + struct lgtd_lifx_bulb *bulb, *next_bulb; + RB_FOREACH_SAFE( + bulb, + lgtd_lifx_bulb_map, + &lgtd_lifx_bulbs_table, + next_bulb + ) { + int light_state_lag = now - bulb->last_light_state_at; + if (light_state_lag >= LGTD_LIFX_DISCOVERY_DEVICE_TIMEOUT_MSECS) { + lgtd_info( + "closing bulb \"%.*s\" that hasn't been updated for %dms", + LGTD_LIFX_LABEL_SIZE, bulb->state.label, light_state_lag + ); + lgtd_lifx_gateway_remove_and_close_bulb(bulb->gw, bulb); + start_discovery = true; + continue; + } + } + + // Repeat for the gateways, we could also look if we are removing the last + // bulb on the gateway but this will also support architectures where + // gateways aren't bulbs themselves: + struct lgtd_lifx_gateway *gw, *next_gw; + LIST_FOREACH_SAFE(gw, &lgtd_lifx_gateways, link, next_gw) { + // The gateway latency is the difference during the last round-trip-time + // (RTT) and has been a PITA to get right (it's off sometimes). Anyway, + // here we are interested in a timeout: how much time elapsed since the + // last update, this is different than the last RTT. + int gw_lag = lgtd_lifx_gateway_msecs_since_last_update(gw); + if (gw_lag >= LGTD_LIFX_DISCOVERY_DEVICE_TIMEOUT_MSECS) { + lgtd_info( + "closing bulb gateway %s that hasn't received traffic for %dms", + gw->peeraddr, gw_lag + ); + lgtd_lifx_gateway_close(gw); + start_discovery = true; + } else if (gw_lag >= LGTD_LIFX_DISCOVERY_DEVICE_FORCE_REFRESH_MSECS) { + lgtd_info( + "no update on bulb gateway %s for %dms, forcing refresh", + gw->peeraddr, gw_lag + ); + lgtd_lifx_gateway_force_refresh(gw); + } + } + + // If anything happens restart a discovery right away, maybe something just + // moved on the network: + if (start_discovery) { + lgtd_lifx_broadcast_discovery(); + } +} + +bool +lgtd_lifx_discovery_setup(void) +{ + assert(!lgtd_watchdog_interval_ev); + assert(!lgtd_discovery_timeout_ev); + + lgtd_discovery_timeout_ev = event_new( + lgtd_ev_base, + -1, + 0, + lgtd_lifx_discovery_timeout_event_callback, + NULL + ); + lgtd_watchdog_interval_ev = event_new( + lgtd_ev_base, + -1, + EV_PERSIST, + lgtd_lifx_discovery_watchdog_interval_callback, + NULL + ); + + if (lgtd_discovery_timeout_ev && lgtd_watchdog_interval_ev) { + return true; + } + + int errsave = errno; + lgtd_lifx_discovery_close(); + errno = errsave; + return false; +} + +void +lgtd_lifx_discovery_close(void) +{ + if (lgtd_discovery_timeout_ev) { + event_del(lgtd_discovery_timeout_ev); + event_free(lgtd_discovery_timeout_ev); + lgtd_discovery_timeout_ev = NULL; + } + if (lgtd_watchdog_interval_ev) { + event_del(lgtd_watchdog_interval_ev); + event_free(lgtd_watchdog_interval_ev); + lgtd_watchdog_interval_ev = NULL; + } +} + +void +lgtd_lifx_discovery_start_watchdog(void) +{ + assert( + !RB_EMPTY(&lgtd_lifx_bulbs_table) || !LIST_EMPTY(&lgtd_lifx_gateways) + ); + + bool pending = evtimer_pending(lgtd_watchdog_interval_ev, NULL); + if (!pending) { + struct timeval tv = LGTD_MSECS_TO_TIMEVAL( + LGTD_LIFX_DISCOVERY_WATCHDOG_INTERVAL_MSECS + ); + if (event_add(lgtd_watchdog_interval_ev, &tv)) { + lgtd_err(1, "can't start watchdog"); + } + lgtd_debug("starting watchdog timer"); + } +} + +void +lgtd_lifx_discovery_start(void) +{ + assert(!evtimer_pending(lgtd_discovery_timeout_ev, NULL)); + + lgtd_discovery_timeout = + LGTD_LIFX_DISCOVERY_ACTIVE_DISCOVERY_INTERVAL_MSECS; + lgtd_lifx_discovery_timeout_event_callback(-1, 0, NULL); + lgtd_debug("starting discovery timer"); +} diff --git a/lifx/discovery.h b/lifx/discovery.h new file mode 100644 index 0000000..bd15eb9 --- /dev/null +++ b/lifx/discovery.h @@ -0,0 +1,31 @@ +// Copyright (c) 2015, Louis Opter +// +// This file is part of lighstd. +// +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . + +#pragma once + +enum lgtd_lifx_discovery_constants { + LGTD_LIFX_DISCOVERY_WATCHDOG_INTERVAL_MSECS = 500, + LGTD_LIFX_DISCOVERY_DEVICE_TIMEOUT_MSECS = 20000, + LGTD_LIFX_DISCOVERY_DEVICE_FORCE_REFRESH_MSECS = 2000, + LGTD_LIFX_DISCOVERY_ACTIVE_DISCOVERY_INTERVAL_MSECS = 2000, + LGTD_LIFX_DISCOVERY_PASSIVE_DISCOVERY_INTERVAL_MSECS = 10000, +}; + +bool lgtd_lifx_discovery_setup(void); +void lgtd_lifx_discovery_close(void); +void lgtd_lifx_discovery_start_watchdog(void); +void lgtd_lifx_discovery_start(void); diff --git a/lifx/gateway.c b/lifx/gateway.c new file mode 100644 index 0000000..809ea3f --- /dev/null +++ b/lifx/gateway.c @@ -0,0 +1,953 @@ +// Copyright (c) 2014, Louis Opter +// +// This file is part of lighstd. +// +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "wire_proto.h" +#include "core/time_monotonic.h" +#include "bulb.h" +#include "gateway.h" +#include "discovery.h" +#include "broadcast.h" +#include "core/timer.h" +#include "tagging.h" +#include "core/jsmn.h" +#include "core/jsonrpc.h" +#include "core/client.h" +#include "core/proto.h" +#include "core/router.h" +#include "core/stats.h" +#include "core/daemon.h" +#include "core/lightsd.h" + +struct lgtd_lifx_gateway_list lgtd_lifx_gateways = + LIST_HEAD_INITIALIZER(&lgtd_lifx_gateways); + +void +lgtd_lifx_gateway_close(struct lgtd_lifx_gateway *gw) +{ + assert(gw); + + LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(gateways, -1); + lgtd_timer_stop(gw->refresh_timer); + event_del(gw->socket_ev); + if (gw->socket != -1) { + evutil_closesocket(gw->socket); + LIST_REMOVE(gw, link); + } + event_free(gw->socket_ev); + evbuffer_free(gw->write_buf); + for (int i = 0; i != LGTD_LIFX_GATEWAY_MAX_TAGS; i++) { + if (gw->tags[i]) { + lgtd_lifx_tagging_decref(gw->tags[i], gw); + } + } + while (!SLIST_EMPTY(&gw->bulbs)) { + struct lgtd_lifx_bulb *bulb = SLIST_FIRST(&gw->bulbs); + lgtd_lifx_gateway_remove_and_close_bulb(gw, bulb); + } + + char site[LGTD_LIFX_ADDR_STRLEN]; + lgtd_info( + "connection with gateway bulb %s (site %s) closed", + gw->peeraddr, LGTD_IEEE8023MACTOA(gw->site.as_array, site) + ); + free(gw->peer); + free(gw); +} + +void +lgtd_lifx_gateway_remove_and_close_bulb(struct lgtd_lifx_gateway *gw, + struct lgtd_lifx_bulb *bulb) +{ + assert(gw); + assert(bulb); + + int tag_id; + LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, bulb->state.tags) { + assert(gw->tag_refcounts[tag_id] > 0); + gw->tag_refcounts[tag_id]--; + } + SLIST_REMOVE(&gw->bulbs, bulb, lgtd_lifx_bulb, link_by_gw); + + lgtd_lifx_bulb_close(bulb); +} + +void +lgtd_lifx_gateway_handle_packet(struct lgtd_lifx_gateway *gw, + const struct sockaddr *peer, + ev_socklen_t addrlen, + const struct lgtd_lifx_packet_info *pkt_info, + const struct lgtd_lifx_packet_header *hdr, + const void *pkt, + lgtd_time_mono_t received_at) +{ + assert(peer); + assert(addrlen); + assert(pkt_info); + assert(hdr); + assert(pkt); + assert(received_at); + + if (gw) { + assert(gw->peerlen == addrlen); + assert(!memcmp(peer, gw->peer, addrlen)); + } else { + gw = lgtd_lifx_gateway_get(peer, addrlen); + if (!gw && hdr->packet_type == LGTD_LIFX_PAN_GATEWAY) { + gw = lgtd_lifx_gateway_open(peer, addrlen, hdr->site, received_at); + if (!gw) { + lgtd_warn("can't allocate gateway"); + return; + } + } + } + + if (gw) { + // gw->last_pkt_at is used to compute timeouts based on known + // traffic only: + if (pkt_info->handle != lgtd_lifx_wire_enosys_packet_handler) { + gw->last_pkt_at = received_at; + } + pkt_info->handle(gw, hdr, pkt); + } else { + bool addressable = hdr->protocol & LGTD_LIFX_PROTOCOL_ADDRESSABLE; + bool tagged = hdr->protocol & LGTD_LIFX_PROTOCOL_TAGGED; + unsigned int protocol = hdr->protocol & LGTD_LIFX_PROTOCOL_VERSION_MASK; + char target[LGTD_LIFX_ADDR_STRLEN], peer_addr[INET6_ADDRSTRLEN]; + LGTD_LIFX_WIRE_PRINT_TARGET(hdr, target); + LGTD_SOCKADDRTOA(peer, peer_addr); + lgtd_info( + "%s <-- %s - (Packet from unknown client or gateway, header " + "info: addressable=%d, tagged=%d, protocol=%d, target=%s)", + pkt_info->name, peer_addr, addressable, tagged, protocol, target + ); + } +} + +static void +lgtd_lifx_gateway_socket_event_callback(evutil_socket_t socket, + short events, + void *ctx) +{ + (void)socket; + + assert(ctx); + + struct lgtd_lifx_gateway *gw = (struct lgtd_lifx_gateway *)ctx; + + if (events & EV_TIMEOUT) { // Not sure how that could happen in UDP but eh. + lgtd_warn("lost connection with gateway bulb %s", gw->peeraddr); + goto drop_gw_and_restart_discovery; + } + + if (events & EV_READ) { + bool ok = lgtd_lifx_wire_handle_receive(gw->socket, gw); + if (!ok) { + goto drop_gw_and_restart_discovery; + } + } + + if (events & EV_WRITE) { + assert(gw->pkt_ring_tail >= 0); + assert(gw->pkt_ring_tail < (int)LGTD_ARRAY_SIZE(gw->pkt_ring)); + + int to_write = gw->pkt_ring[gw->pkt_ring_tail].size; + int nbytes = evbuffer_write_atmost(gw->write_buf, gw->socket, to_write); + if (nbytes == -1 && errno != EAGAIN) { + lgtd_warn("can't write to %s", gw->peeraddr); + goto drop_gw_and_restart_discovery; + } + + // Callbacks are called in any order, so we keep two timers to make + // sure we can get the latency right, otherwise we could be compute the + // latency with last_pkt_at < last_req_at, which isn't true since the + // pkt will be for an answer to the previous write: + gw->last_req_at = gw->next_req_at; + gw->next_req_at = lgtd_time_monotonic_msecs(); + + gw->pkt_ring[gw->pkt_ring_tail].size -= nbytes; + if (gw->pkt_ring[gw->pkt_ring_tail].size == 0) { + enum lgtd_lifx_packet_type type; + type = gw->pkt_ring[gw->pkt_ring_tail].type; + if (type == LGTD_LIFX_GET_TAG_LABELS) { + gw->pending_refresh_req = false; + } + gw->pkt_ring[gw->pkt_ring_tail].type = 0; + LGTD_LIFX_GATEWAY_INC_MESSAGE_RING_INDEX(gw->pkt_ring_tail); + gw->pkt_ring_full = false; + } + + if (!evbuffer_get_length(gw->write_buf)) { + event_del(gw->socket_ev); + } + } + + return; + +drop_gw_and_restart_discovery: + lgtd_lifx_gateway_close(gw); + if (!lgtd_lifx_broadcast_discovery()) { + lgtd_err(1, "can't start auto discovery"); + } + return; +} + +static bool +lgtd_lifx_gateway_send_to_site_impl(struct lgtd_lifx_gateway *gw, + enum lgtd_lifx_packet_type pkt_type, + void *pkt, + const struct lgtd_lifx_packet_info **pkt_info) +{ + assert(gw); + assert(pkt_info); + + struct lgtd_lifx_packet_header hdr; + union lgtd_lifx_target target = { .addr = gw->site.as_array }; + *pkt_info = lgtd_lifx_wire_setup_header( + &hdr, + LGTD_LIFX_TARGET_SITE, + target, + gw->site.as_array, + pkt_type + ); + assert(*pkt_info); + + lgtd_lifx_gateway_enqueue_packet(gw, &hdr, *pkt_info, pkt); + + return true; // FIXME, have real return values on the send paths... +} + +static bool +lgtd_lifx_gateway_send_to_site_quiet(struct lgtd_lifx_gateway *gw, + enum lgtd_lifx_packet_type pkt_type, + void *pkt) +{ + + const struct lgtd_lifx_packet_info *pkt_info; + bool rv = lgtd_lifx_gateway_send_to_site_impl( + gw, pkt_type, pkt, &pkt_info + ); + + char site[LGTD_LIFX_ADDR_STRLEN]; + lgtd_debug( + "sending %s to site %s", + pkt_info->name, LGTD_IEEE8023MACTOA(gw->site.as_array, site) + ); + + return rv; // FIXME, have real return values on the send paths... +} + +bool +lgtd_lifx_gateway_send_to_site(struct lgtd_lifx_gateway *gw, + enum lgtd_lifx_packet_type pkt_type, + void *pkt) +{ + const struct lgtd_lifx_packet_info *pkt_info; + bool rv = lgtd_lifx_gateway_send_to_site_impl( + gw, pkt_type, pkt, &pkt_info + ); + + char site[LGTD_LIFX_ADDR_STRLEN]; + lgtd_info( + "sending %s to site %s", + pkt_info->name, LGTD_IEEE8023MACTOA(gw->site.as_array, site) + ); + + return rv; // FIXME, have real return values on the send paths... +} + +static void +lgtd_lifx_gateway_send_get_all_light_state(struct lgtd_lifx_gateway *gw) +{ + assert(gw); + + lgtd_lifx_gateway_send_to_site_quiet(gw, LGTD_LIFX_GET_LIGHT_STATE, NULL); + + struct lgtd_lifx_packet_tags pkt = { .tags = LGTD_LIFX_ALL_TAGS }; + lgtd_lifx_gateway_send_to_site_quiet(gw, LGTD_LIFX_GET_TAG_LABELS, &pkt); + + gw->pending_refresh_req = true; +} + +static void +lgtd_lifx_gateway_refresh_callback(struct lgtd_timer *timer, + union lgtd_timer_ctx ctx) +{ + (void)timer; + struct lgtd_lifx_gateway *gw = ctx.as_ptr; + lgtd_lifx_gateway_send_get_all_light_state(gw); + + struct timeval tv = LGTD_MSECS_TO_TIMEVAL( + LGTD_LIFX_GATEWAY_MIN_REFRESH_INTERVAL_MSECS + ); + lgtd_timer_reschedule(gw->refresh_timer, &tv); + lgtd_debug( + "scheduling next GET_LIGHT_STATE on %s in %dms", + gw->peeraddr, LGTD_LIFX_GATEWAY_MIN_REFRESH_INTERVAL_MSECS + ); +} + +void +lgtd_lifx_gateway_force_refresh(struct lgtd_lifx_gateway *gw) +{ + assert(gw); + + lgtd_timer_activate(gw->refresh_timer); +} + +static struct lgtd_lifx_bulb * +lgtd_lifx_gateway_get_or_open_bulb(struct lgtd_lifx_gateway *gw, + const uint8_t *bulb_addr) +{ + assert(gw); + assert(bulb_addr); + + struct lgtd_lifx_bulb *bulb = lgtd_lifx_bulb_get(bulb_addr); + if (!bulb) { + bulb = lgtd_lifx_bulb_open(gw, bulb_addr); + if (bulb) { + SLIST_INSERT_HEAD(&gw->bulbs, bulb, link_by_gw); + char addr[LGTD_LIFX_ADDR_STRLEN]; + lgtd_info( + "bulb %s on %s", + LGTD_IEEE8023MACTOA(bulb->addr, addr), gw->peeraddr + ); + } + } + return bulb; +} + +struct lgtd_lifx_gateway * +lgtd_lifx_gateway_open(const struct sockaddr *peer, + ev_socklen_t addrlen, + const uint8_t *site, + lgtd_time_mono_t received_at) +{ + assert(peer); + assert(site); + + struct lgtd_lifx_gateway *gw = calloc(1, sizeof(*gw)); + if (!gw) { + lgtd_warn("can't allocate a new gateway bulb"); + return false; + } + gw->socket = socket(peer->sa_family, SOCK_DGRAM, IPPROTO_UDP); + if (gw->socket == -1) { + lgtd_warn("can't open a new socket"); + goto error_socket; + } + if (connect(gw->socket, peer, addrlen) == -1 + || evutil_make_socket_nonblocking(gw->socket) == -1) { + lgtd_warn("can't open a new socket"); + goto error_connect; + } + + gw->socket_ev = event_new( + lgtd_ev_base, + gw->socket, + EV_READ|EV_WRITE|EV_PERSIST, + lgtd_lifx_gateway_socket_event_callback, + gw + ); + gw->write_buf = evbuffer_new(); + if (!gw->socket_ev || !gw->write_buf) { + goto error_allocate; + } + gw->peer = malloc(addrlen); + if (!gw->peer) { + goto error_allocate; + } + + memcpy(gw->peer, peer, addrlen); + gw->peerlen = addrlen; + LGTD_SOCKADDRTOA(gw->peer, gw->peeraddr); + memcpy(gw->site.as_array, site, sizeof(gw->site.as_array)); + gw->last_req_at = received_at; + gw->next_req_at = received_at; + gw->last_pkt_at = received_at; + + union lgtd_timer_ctx ctx = { .as_ptr = gw }; + gw->refresh_timer = lgtd_timer_start( + LGTD_TIMER_ACTIVATE_NOW, + LGTD_LIFX_GATEWAY_MIN_REFRESH_INTERVAL_MSECS, + lgtd_lifx_gateway_refresh_callback, + ctx + ); + if (!gw->refresh_timer) { + lgtd_warn("can't allocate a new timer"); + goto error_allocate; + } + + char site_addr[LGTD_LIFX_ADDR_STRLEN]; + lgtd_info( + "gateway for site %s at %s", + LGTD_IEEE8023MACTOA(gw->site.as_array, site_addr), gw->peeraddr + ); + LIST_INSERT_HEAD(&lgtd_lifx_gateways, gw, link); + + // In case this is the first bulb (re-)discovered, start the watchdog, it + // will stop by itself: + lgtd_lifx_discovery_start_watchdog(); + + LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(gateways, 1); + + return gw; + +error_allocate: + lgtd_warn("can't allocate a new gateway bulb"); + if (gw->socket_ev) { + event_free(gw->socket_ev); + } + if (gw->write_buf) { + evbuffer_free(gw->write_buf); + } +error_connect: + evutil_closesocket(gw->socket); +error_socket: + free(gw->peer); + free(gw); + return NULL; +} + +struct lgtd_lifx_gateway * +lgtd_lifx_gateway_get(const struct sockaddr *peer, ev_socklen_t peerlen) +{ + assert(peer); + + struct lgtd_lifx_gateway *gw, *next_gw; + LIST_FOREACH_SAFE(gw, &lgtd_lifx_gateways, link, next_gw) { + if (peer->sa_family == gw->peer->sa_family + && peerlen == gw->peerlen + && !memcmp(gw->peer, peer, peerlen)) { + return gw; + } + } + + return NULL; +} + +void +lgtd_lifx_gateway_close_all(void) +{ + struct lgtd_lifx_gateway *gw, *next_gw; + LIST_FOREACH_SAFE(gw, &lgtd_lifx_gateways, link, next_gw) { + lgtd_lifx_gateway_close(gw); + } +} + +void +lgtd_lifx_gateway_enqueue_packet(struct lgtd_lifx_gateway *gw, + const struct lgtd_lifx_packet_header *hdr, + const struct lgtd_lifx_packet_info *pkt_info, + void *pkt) +{ + assert(gw); + assert(hdr); + assert(pkt_info); + assert(!memcmp(hdr->site, gw->site.as_array, LGTD_LIFX_ADDR_LENGTH)); + assert(gw->pkt_ring_head >= 0); + assert(gw->pkt_ring_head < (int)LGTD_ARRAY_SIZE(gw->pkt_ring)); + + if (gw->pkt_ring_full) { + lgtd_warnx( + "dropping packet type %s: packet queue on %s is full", + pkt_info->name, gw->peeraddr + ); + return; + } + + evbuffer_add(gw->write_buf, hdr, sizeof(*hdr)); + if (pkt) { +#ifndef NDEBUG + // actually decode the header instead of just calling + // le16toh(hdr->size) so it's easier to mock things in the tests: + struct lgtd_lifx_packet_header decoded_hdr; + memcpy(&decoded_hdr, hdr, sizeof(decoded_hdr)); + lgtd_lifx_wire_decode_header(&decoded_hdr); + assert(pkt_info->size == decoded_hdr.size - sizeof(*hdr)); +#endif + evbuffer_add(gw->write_buf, pkt, pkt_info->size); + } + gw->pkt_ring[gw->pkt_ring_head].size = sizeof(*hdr) + pkt_info->size; + gw->pkt_ring[gw->pkt_ring_head].type = pkt_info->type; + LGTD_LIFX_GATEWAY_INC_MESSAGE_RING_INDEX(gw->pkt_ring_head); + if (gw->pkt_ring_head == gw->pkt_ring_tail) { + gw->pkt_ring_full = true; + } + event_add(gw->socket_ev, NULL); +} + +void +lgtd_lifx_gateway_update_tag_refcounts(struct lgtd_lifx_gateway *gw, + uint64_t bulb_tags, + uint64_t pkt_tags) +{ + uint64_t changes = bulb_tags ^ pkt_tags; + uint64_t added_tags = changes & pkt_tags; + uint64_t removed_tags = changes & bulb_tags; + int tag_id; + + LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, added_tags) { + if (gw->tag_refcounts[tag_id] != UINT8_MAX) { + gw->tag_refcounts[tag_id]++; + } else { + lgtd_warnx( + "reached refcount limit (%u) for tag [%s] (%d) on gw %s", + UINT8_MAX, gw->tags[tag_id] ? gw->tags[tag_id]->label : NULL, + tag_id, gw->peeraddr + ); + } + } + + LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, removed_tags) { + assert(gw->tag_refcounts[tag_id] > 0); + if (--gw->tag_refcounts[tag_id] == 0) { + char site[LGTD_LIFX_ADDR_STRLEN]; + lgtd_info( + "deleting unused tag [%s] (%d) from gw %s (site %s)", + gw->tags[tag_id] ? gw->tags[tag_id]->label : NULL, + tag_id, gw->peeraddr, + LGTD_IEEE8023MACTOA(gw->site.as_array, site) + ); + struct lgtd_lifx_packet_tag_labels pkt = { + .tags = ~(gw->tag_ids & ~LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id)) + }; + lgtd_lifx_wire_encode_tag_labels(&pkt); + lgtd_lifx_gateway_send_to_site(gw, LGTD_LIFX_SET_TAG_LABELS, &pkt); + } + } +} + +lgtd_time_mono_t +lgtd_lifx_gateway_latency(const struct lgtd_lifx_gateway *gw) +{ + assert(gw); + + if (gw->last_req_at < gw->last_pkt_at) { // this doesn't look right + return gw->last_pkt_at - gw->last_req_at; + } + return 0; +} + +void +lgtd_lifx_gateway_handle_pan_gateway(struct lgtd_lifx_gateway *gw, + const struct lgtd_lifx_packet_header *hdr, + const struct lgtd_lifx_packet_pan_gateway *pkt) +{ + assert(gw && hdr && pkt); + + char addr[LGTD_LIFX_ADDR_STRLEN], site[LGTD_LIFX_ADDR_STRLEN]; + lgtd_debug( + "SET_PAN_GATEWAY <-- %s - %s site=%s, service_type=%d", + gw->peeraddr, + LGTD_IEEE8023MACTOA(hdr->target.device_addr, addr), + LGTD_IEEE8023MACTOA(hdr->site, site), pkt->service_type + ); +} + +void +lgtd_lifx_gateway_handle_light_status(struct lgtd_lifx_gateway *gw, + const struct lgtd_lifx_packet_header *hdr, + const struct lgtd_lifx_packet_light_status *pkt) +{ + assert(gw && hdr && pkt); + + char addr[LGTD_LIFX_ADDR_STRLEN]; + lgtd_debug( + "SET_LIGHT_STATE <-- %s - %s " + "hue=%#hx, saturation=%#hx, brightness=%#hx, " + "kelvin=%d, dim=%#hx, power=%#hx, label=%.*s, tags=%#jx", + gw->peeraddr, LGTD_IEEE8023MACTOA(hdr->target.device_addr, addr), + pkt->hue, pkt->saturation, pkt->brightness, pkt->kelvin, + pkt->dim, pkt->power, LGTD_LIFX_LABEL_SIZE, pkt->label, + (uintmax_t)pkt->tags + ); + + struct lgtd_lifx_bulb *b; + LGTD_LIFX_GATEWAY_GET_BULB_OR_RETURN(b, gw, hdr->target.device_addr); + + assert(sizeof(*pkt) == sizeof(b->state)); + lgtd_lifx_bulb_set_light_state( + b, (const struct lgtd_lifx_light_state *)pkt, gw->last_pkt_at + ); + + if (b->dirty_at + && b->last_light_state_at > b->dirty_at + && b->gw->last_pkt_at - b->dirty_at > 400) { + if (b->expected_power_on == b->state.power) { + lgtd_debug("clearing dirty_at on %s", b->state.label); + b->dirty_at = 0; + } else { + lgtd_info( + "retransmiting power %s to %s", + b->expected_power_on ? "on" : "off", b->state.label + ); + struct lgtd_lifx_packet_power_state pkt; + pkt.power = b->expected_power_on ? + LGTD_LIFX_POWER_ON : LGTD_LIFX_POWER_OFF; + lgtd_router_send_to_device(b, LGTD_LIFX_SET_POWER_STATE, &pkt); + } + } + + lgtd_time_mono_t latency = lgtd_lifx_gateway_latency(gw); + if (latency < LGTD_LIFX_GATEWAY_MIN_REFRESH_INTERVAL_MSECS) { + int timeout = LGTD_LIFX_GATEWAY_MIN_REFRESH_INTERVAL_MSECS - latency; + struct timeval tv = LGTD_MSECS_TO_TIMEVAL(timeout); + lgtd_timer_reschedule(gw->refresh_timer, &tv); + lgtd_debug( + "%s latency is %jums, re-scheduling next GET_LIGHT_STATE in %dms", + gw->peeraddr, (uintmax_t)latency, timeout + ); + return; + } + + if (!gw->pending_refresh_req) { + lgtd_debug( + "%s latency is %jums, sending GET_LIGHT_STATE now", + gw->peeraddr, (uintmax_t)latency + ); + lgtd_lifx_gateway_send_get_all_light_state(gw); + } else { + lgtd_debug( + "%s GET_LIGHT_STATE for all bulbs on this gw has already " + "been enqueued", gw->peeraddr + ); + } +} + +void +lgtd_lifx_gateway_handle_power_state(struct lgtd_lifx_gateway *gw, + const struct lgtd_lifx_packet_header *hdr, + const struct lgtd_lifx_packet_power_state *pkt) +{ + assert(gw && hdr && pkt); + + char addr[LGTD_LIFX_ADDR_STRLEN]; + lgtd_debug( + "SET_POWER_STATE <-- %s - %s power=%#hx", gw->peeraddr, + LGTD_IEEE8023MACTOA(hdr->target.device_addr, addr), pkt->power + ); + + LGTD_LIFX_GATEWAY_SET_BULB_ATTR( + gw, hdr->target.device_addr, lgtd_lifx_bulb_set_power_state, pkt->power + ); +} + +int +lgtd_lifx_gateway_get_tag_id(const struct lgtd_lifx_gateway *gw, + const struct lgtd_lifx_tag *tag) +{ + assert(gw); + assert(tag); + + int tag_id; + LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, gw->tag_ids) { + if (gw->tags[tag_id] == tag) { + return tag_id; + } + } + + return -1; +} + +int +lgtd_lifx_gateway_allocate_tag_id(struct lgtd_lifx_gateway *gw, + int tag_id, + const char *tag_label) +{ + assert(gw); + assert(tag_label); + assert(tag_id >= -1); + assert(tag_id < LGTD_LIFX_GATEWAY_MAX_TAGS); + + char site[LGTD_LIFX_ADDR_STRLEN]; + LGTD_IEEE8023MACTOA(gw->site.as_array, site); + + if (tag_id == -1) { + tag_id = lgtd_lifx_wire_bitscan64_forward(~gw->tag_ids); + if (tag_id == -1) { + lgtd_warnx( + "no tag_id left for new tag [%s] on gw %s (site %s)", + tag_label, gw->peeraddr, site + ); + return -1; + } + } + + if (!(gw->tag_ids & LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id))) { + struct lgtd_lifx_tag *tag; + tag = lgtd_lifx_tagging_incref(tag_label, gw, tag_id); + if (!tag) { + lgtd_warn( + "couldn't allocate a new reference to tag [%s] (site %s)", + tag_label, site + ); + return -1; + } + lgtd_debug( + "tag_id %d allocated for tag [%s] on gw %s (site %s)", + tag_id, tag_label, gw->peeraddr, site + ); + gw->tag_ids |= LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id); + gw->tags[tag_id] = tag; + } + + return tag_id; +} + +void +lgtd_lifx_gateway_deallocate_tag_id(struct lgtd_lifx_gateway *gw, int tag_id) +{ + assert(gw); + assert(tag_id >= 0); + assert(tag_id < LGTD_LIFX_GATEWAY_MAX_TAGS); + + if (gw->tag_ids & LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id)) { + char site[LGTD_LIFX_ADDR_STRLEN]; + lgtd_debug( + "tag_id %d deallocated for tag [%s] on gw %s (site %s)", + tag_id, gw->tags[tag_id]->label, + gw->peeraddr, LGTD_IEEE8023MACTOA(gw->site.as_array, site) + ); + lgtd_lifx_tagging_decref(gw->tags[tag_id], gw); + gw->tag_ids &= ~LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id); + gw->tags[tag_id] = NULL; + } +} + +void +lgtd_lifx_gateway_handle_tag_labels(struct lgtd_lifx_gateway *gw, + const struct lgtd_lifx_packet_header *hdr, + const struct lgtd_lifx_packet_tag_labels *pkt) +{ + char addr[LGTD_LIFX_ADDR_STRLEN]; + lgtd_debug( + "SET_TAG_LABELS <-- %s - %s label=%.*s, tags=%jx", + gw->peeraddr, LGTD_IEEE8023MACTOA(hdr->target.device_addr, addr), + LGTD_LIFX_LABEL_SIZE, pkt->label, (uintmax_t)pkt->tags + ); + + int tag_id; + LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, pkt->tags) { + if (pkt->label[0]) { + lgtd_lifx_gateway_allocate_tag_id(gw, tag_id, pkt->label); + } else if (gw->tags[tag_id]) { + lgtd_lifx_gateway_deallocate_tag_id(gw, tag_id); + } + } +} + +void +lgtd_lifx_gateway_handle_tags(struct lgtd_lifx_gateway *gw, + const struct lgtd_lifx_packet_header *hdr, + const struct lgtd_lifx_packet_tags *pkt) +{ + char addr[LGTD_LIFX_ADDR_STRLEN]; + lgtd_debug( + "SET_TAGS <-- %s - %s tags=%#jx", + gw->peeraddr, LGTD_IEEE8023MACTOA(hdr->target.device_addr, addr), + (uintmax_t)pkt->tags + ); + + struct lgtd_lifx_bulb *b; + LGTD_LIFX_GATEWAY_GET_BULB_OR_RETURN(b, gw, hdr->target.device_addr); + + char bulb_addr[LGTD_LIFX_ADDR_STRLEN], site_addr[LGTD_LIFX_ADDR_STRLEN]; + int tag_id; + LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, pkt->tags) { + if (!(gw->tag_ids & LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id))) { + lgtd_warnx( + "trying to set unknown tag_id %d (%#jx) " + "on bulb %s (%.*s), gw %s (site %s)", + tag_id, (uintmax_t)LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id), + LGTD_IEEE8023MACTOA(b->addr, bulb_addr), + LGTD_LIFX_LABEL_SIZE, b->state.label, gw->peeraddr, + LGTD_IEEE8023MACTOA(gw->site.as_array, site_addr) + ); + } + } + + lgtd_lifx_bulb_set_tags(b, pkt->tags); +} + +void +lgtd_lifx_gateway_handle_ip_state(struct lgtd_lifx_gateway *gw, + const struct lgtd_lifx_packet_header *hdr, + const struct lgtd_lifx_packet_ip_state *pkt) +{ + const char *type; + enum lgtd_lifx_bulb_ips ip_id; + switch (hdr->packet_type) { + case LGTD_LIFX_MESH_INFO: + type = "MCU_STATE"; + ip_id = LGTD_LIFX_BULB_MCU_IP; + break; + case LGTD_LIFX_WIFI_INFO: + type = "WIFI_STATE"; + ip_id = LGTD_LIFX_BULB_WIFI_IP; + break; + default: + lgtd_info("invalid ip state packet_type %#hx", hdr->packet_type); +#ifndef NDEBUG + abort(); +#endif + return; + } + + char addr[LGTD_LIFX_ADDR_STRLEN]; + lgtd_debug( + "%s <-- %s - %s " + "signal_strength=%f, rx_bytes=%u, tx_bytes=%u, temperature=%hu", + type, gw->peeraddr, LGTD_IEEE8023MACTOA(hdr->target.device_addr, addr), + pkt->signal_strength, pkt->rx_bytes, pkt->tx_bytes, pkt->temperature + ); + + LGTD_LIFX_GATEWAY_SET_BULB_ATTR( + gw, hdr->target.device_addr, lgtd_lifx_bulb_set_ip_state, + ip_id, (const struct lgtd_lifx_ip_state *)pkt, gw->last_pkt_at + ); +} + +void +lgtd_lifx_gateway_handle_ip_firmware_info(struct lgtd_lifx_gateway *gw, + const struct lgtd_lifx_packet_header *hdr, + const struct lgtd_lifx_packet_ip_firmware_info *pkt) +{ + const char *type; + enum lgtd_lifx_bulb_ips ip_id; + switch (hdr->packet_type) { + case LGTD_LIFX_MESH_FIRMWARE: + type = "MCU_FIRMWARE_INFO"; + ip_id = LGTD_LIFX_BULB_MCU_IP; + break; + case LGTD_LIFX_WIFI_FIRMWARE_STATE: + type = "WIFI_FIRMWARE_INFO"; + ip_id = LGTD_LIFX_BULB_WIFI_IP; + break; + default: + lgtd_info("invalid ip firmware packet_type %#hx", hdr->packet_type); +#ifndef NDEBUG + abort(); +#endif + return; + } + + char built_at[64], installed_at[64], addr[LGTD_LIFX_ADDR_STRLEN]; + lgtd_debug( + "%s <-- %s - %s " + "built_at=%s, installed_at=%s, version=%u", + type, gw->peeraddr, LGTD_IEEE8023MACTOA(hdr->target.device_addr, addr), + LGTD_LIFX_WIRE_PRINT_NSEC_TIMESTAMP(pkt->built_at, built_at), + LGTD_LIFX_WIRE_PRINT_NSEC_TIMESTAMP(pkt->installed_at, installed_at), + pkt->version + ); + + LGTD_LIFX_GATEWAY_SET_BULB_ATTR( + gw, hdr->target.device_addr, lgtd_lifx_bulb_set_ip_firmware_info, + ip_id, (const struct lgtd_lifx_ip_firmware_info *)pkt, gw->last_pkt_at + ); +} + +void +lgtd_lifx_gateway_handle_product_info(struct lgtd_lifx_gateway *gw, + const struct lgtd_lifx_packet_header *hdr, + const struct lgtd_lifx_packet_product_info *pkt) +{ + char addr[LGTD_LIFX_ADDR_STRLEN]; + lgtd_debug( + "PRODUCT_INFO <-- %s - %s " + "vendor_id=%#x, product_id=%#x, version=%u", + gw->peeraddr, LGTD_IEEE8023MACTOA(hdr->target.device_addr, addr), + pkt->vendor_id, pkt->product_id, pkt->version + ); + + LGTD_LIFX_GATEWAY_SET_BULB_ATTR( + gw, hdr->target.device_addr, lgtd_lifx_bulb_set_product_info, + (const struct lgtd_lifx_product_info *)pkt + ); +} + +void +lgtd_lifx_gateway_handle_runtime_info(struct lgtd_lifx_gateway *gw, + const struct lgtd_lifx_packet_header *hdr, + const struct lgtd_lifx_packet_runtime_info *pkt) +{ + char device_time[64], uptime[64], downtime[64], addr[LGTD_LIFX_ADDR_STRLEN]; + lgtd_debug( + "PRODUCT_INFO <-- %s - %s time=%s, uptime=%s, downtime=%s", + gw->peeraddr, LGTD_IEEE8023MACTOA(hdr->target.device_addr, addr), + LGTD_LIFX_WIRE_PRINT_NSEC_TIMESTAMP(pkt->time, device_time), + LGTD_PRINT_DURATION(LGTD_NSECS_TO_SECS(pkt->uptime), uptime), + LGTD_PRINT_DURATION(LGTD_NSECS_TO_SECS(pkt->downtime), downtime) + ); + + LGTD_LIFX_GATEWAY_SET_BULB_ATTR( + gw, hdr->target.device_addr, lgtd_lifx_bulb_set_runtime_info, + (const struct lgtd_lifx_runtime_info *)pkt, gw->last_pkt_at + ); +} + +void +lgtd_lifx_gateway_handle_bulb_label(struct lgtd_lifx_gateway *gw, + const struct lgtd_lifx_packet_header *hdr, + const struct lgtd_lifx_packet_label *pkt) +{ + char addr[LGTD_LIFX_ADDR_STRLEN]; + lgtd_debug( + "BULB_LABEL <-- %s - %s label=%.*s", + gw->peeraddr, LGTD_IEEE8023MACTOA(hdr->target.device_addr, addr), + (int)sizeof(pkt->label), pkt->label + ); + + LGTD_LIFX_GATEWAY_SET_BULB_ATTR( + gw, hdr->target.device_addr, lgtd_lifx_bulb_set_label, pkt->label + ); +} + +void +lgtd_lifx_gateway_handle_ambient_light(struct lgtd_lifx_gateway *gw, + const struct lgtd_lifx_packet_header *hdr, + const struct lgtd_lifx_packet_ambient_light *pkt) +{ + char addr[LGTD_LIFX_ADDR_STRLEN]; + lgtd_debug( + "AMBIENT_LIGHT <-- %s - %s ambient_light=%flx", + gw->peeraddr, LGTD_IEEE8023MACTOA(hdr->target.device_addr, addr), + pkt->illuminance + ); + + LGTD_LIFX_GATEWAY_SET_BULB_ATTR( + gw, hdr->target.device_addr, + lgtd_lifx_bulb_set_ambient_light, pkt->illuminance + ); +} diff --git a/lifx/gateway.h b/lifx/gateway.h new file mode 100644 index 0000000..391cb1d --- /dev/null +++ b/lifx/gateway.h @@ -0,0 +1,169 @@ +// Copyright (c) 2014, Louis Opter +// +// This file is part of lighstd. +// +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . + +#pragma once + +// Send GET_LIGHT_STATE to the gateway at most every this interval. FYI, +// according to my own tests, aggressively polling a bulb doesn't raise its +// consumption at all (and it's interesting to note that a turned off bulb +// still draw about 2W in ZigBee and about 3W in WiFi). +enum { LGTD_LIFX_GATEWAY_MIN_REFRESH_INTERVAL_MSECS = 800 }; + +// You can't send more than one lifx packet per UDP datagram. +enum { LGTD_LIFX_GATEWAY_PACKET_RING_SIZE = 16 }; + +enum { LGTD_LIFX_GATEWAY_MAX_TAGS = 64 }; + +struct lgtd_lifx_message { + enum lgtd_lifx_packet_type type; + int size; +}; + +struct lgtd_lifx_gateway { + LIST_ENTRY(lgtd_lifx_gateway) link; + struct lgtd_lifx_bulb_list bulbs; +#define LGTD_LIFX_GATEWAY_GET_BULB_OR_RETURN(b, gw, bulb_addr) do { \ + (b) = lgtd_lifx_gateway_get_or_open_bulb((gw), (bulb_addr)); \ + if (!(b)) { \ + return; \ + } \ +} while (0) + // Multiple gateways can share the same site (that happens when bulbs are + // far away enough that ZigBee can't be used). Moreover the SET_PAN_GATEWAY + // packet doesn't include the device address in the header (i.e: site and + // device_addr have the same value) so we have no choice but to use the + // remote ip address to identify a gateway: + struct sockaddr *peer; + ev_socklen_t peerlen; + // XXX consider having a constants.h file: + char peeraddr[128]; + // TODO: just use an integer and rename it to site_id: + union { + uint8_t as_array[LGTD_LIFX_ADDR_LENGTH]; + uint64_t as_integer; + } site; + uint64_t tag_ids; + struct lgtd_lifx_tag *tags[LGTD_LIFX_GATEWAY_MAX_TAGS]; + uint8_t tag_refcounts[LGTD_LIFX_GATEWAY_MAX_TAGS]; + evutil_socket_t socket; + // Those three timestamps let us measure the latency of the gateway. If we + // aren't the only client on the network then this won't be accurate since + // we will get pushed packets we didn't ask for, but good enough for our + // purpose of rate limiting our requests to the gateway: + lgtd_time_mono_t last_req_at; + lgtd_time_mono_t next_req_at; + lgtd_time_mono_t last_pkt_at; + struct lgtd_lifx_message pkt_ring[LGTD_LIFX_GATEWAY_PACKET_RING_SIZE]; +#define LGTD_LIFX_GATEWAY_INC_MESSAGE_RING_INDEX(idx) do { \ + (idx) += 1; \ + (idx) %= LGTD_LIFX_GATEWAY_PACKET_RING_SIZE; \ +} while(0) + int pkt_ring_head; + int pkt_ring_tail; + bool pkt_ring_full; + struct event *socket_ev; + struct evbuffer *write_buf; + bool pending_refresh_req; + struct lgtd_timer *refresh_timer; +}; +LIST_HEAD(lgtd_lifx_gateway_list, lgtd_lifx_gateway); + +extern struct lgtd_lifx_gateway_list lgtd_lifx_gateways; + +#define LGTD_LIFX_GATEWAY_SET_BULB_ATTR(gw, bulb_addr, bulb_fn, ...) do { \ + struct lgtd_lifx_bulb *b; \ + LGTD_LIFX_GATEWAY_GET_BULB_OR_RETURN(b, gw, bulb_addr); \ + (bulb_fn)(b, __VA_ARGS__); \ +} while (0) + +static inline lgtd_time_mono_t +lgtd_lifx_gateway_msecs_since_last_update(const struct lgtd_lifx_gateway *gw) +{ + assert(gw); + + return lgtd_time_monotonic_msecs() - gw->last_pkt_at; +} + +struct lgtd_lifx_gateway *lgtd_lifx_gateway_get(const struct sockaddr *, ev_socklen_t); +struct lgtd_lifx_gateway *lgtd_lifx_gateway_open(const struct sockaddr *, + ev_socklen_t, + const uint8_t *, + lgtd_time_mono_t); + +void lgtd_lifx_gateway_close(struct lgtd_lifx_gateway *); +void lgtd_lifx_gateway_close_all(void); +void lgtd_lifx_gateway_remove_and_close_bulb(struct lgtd_lifx_gateway *, struct lgtd_lifx_bulb *); + +void lgtd_lifx_gateway_force_refresh(struct lgtd_lifx_gateway *); +lgtd_time_mono_t lgtd_lifx_gateway_latency(const struct lgtd_lifx_gateway *); + +void lgtd_lifx_gateway_enqueue_packet(struct lgtd_lifx_gateway *, + const struct lgtd_lifx_packet_header *, + const struct lgtd_lifx_packet_info *, + void *); +// This could be on router but it's LIFX specific so I'd rather keep it here: +bool lgtd_lifx_gateway_send_to_site(struct lgtd_lifx_gateway *, + enum lgtd_lifx_packet_type, + void *); + +void lgtd_lifx_gateway_update_tag_refcounts(struct lgtd_lifx_gateway *, uint64_t, uint64_t); + +int lgtd_lifx_gateway_get_tag_id(const struct lgtd_lifx_gateway *, const struct lgtd_lifx_tag *); +int lgtd_lifx_gateway_allocate_tag_id(struct lgtd_lifx_gateway *, int, const char *); +void lgtd_lifx_gateway_deallocate_tag_id(struct lgtd_lifx_gateway *, int); + +void lgtd_lifx_gateway_handle_packet(struct lgtd_lifx_gateway *, + const struct sockaddr *, + ev_socklen_t, + const struct lgtd_lifx_packet_info *, + const struct lgtd_lifx_packet_header *, + const void *, + lgtd_time_mono_t); + +void lgtd_lifx_gateway_handle_pan_gateway(struct lgtd_lifx_gateway *, + const struct lgtd_lifx_packet_header *, + const struct lgtd_lifx_packet_pan_gateway *); +void lgtd_lifx_gateway_handle_light_status(struct lgtd_lifx_gateway *, + const struct lgtd_lifx_packet_header *, + const struct lgtd_lifx_packet_light_status *); +void lgtd_lifx_gateway_handle_power_state(struct lgtd_lifx_gateway *, + const struct lgtd_lifx_packet_header *, + const struct lgtd_lifx_packet_power_state *); +void lgtd_lifx_gateway_handle_tag_labels(struct lgtd_lifx_gateway *, + const struct lgtd_lifx_packet_header *, + const struct lgtd_lifx_packet_tag_labels *); +void lgtd_lifx_gateway_handle_tags(struct lgtd_lifx_gateway *, + const struct lgtd_lifx_packet_header *, + const struct lgtd_lifx_packet_tags *); +void lgtd_lifx_gateway_handle_ip_state(struct lgtd_lifx_gateway *, + const struct lgtd_lifx_packet_header *, + const struct lgtd_lifx_packet_ip_state *); +void lgtd_lifx_gateway_handle_ip_firmware_info(struct lgtd_lifx_gateway *, + const struct lgtd_lifx_packet_header *, + const struct lgtd_lifx_packet_ip_firmware_info *); +void lgtd_lifx_gateway_handle_product_info(struct lgtd_lifx_gateway *, + const struct lgtd_lifx_packet_header *, + const struct lgtd_lifx_packet_product_info *); +void lgtd_lifx_gateway_handle_runtime_info(struct lgtd_lifx_gateway *, + const struct lgtd_lifx_packet_header *, + const struct lgtd_lifx_packet_runtime_info *); +void lgtd_lifx_gateway_handle_bulb_label(struct lgtd_lifx_gateway *, + const struct lgtd_lifx_packet_header *, + const struct lgtd_lifx_packet_label *); +void lgtd_lifx_gateway_handle_ambient_light(struct lgtd_lifx_gateway *, + const struct lgtd_lifx_packet_header *, + const struct lgtd_lifx_packet_ambient_light *); diff --git a/lifx/tagging.c b/lifx/tagging.c new file mode 100644 index 0000000..cdcb14a --- /dev/null +++ b/lifx/tagging.c @@ -0,0 +1,164 @@ +// Copyright (c) 2015, Louis Opter +// +// This file is part of lighstd. +// +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "wire_proto.h" +#include "core/time_monotonic.h" +#include "bulb.h" +#include "gateway.h" +#include "tagging.h" +#include "core/lightsd.h" + +struct lgtd_lifx_tag_list lgtd_lifx_tags = + LIST_HEAD_INITIALIZER(&lgtd_lifx_tags); + +static struct lgtd_lifx_site * +lgtd_lifx_tagging_find_site(struct lgtd_lifx_site_list *sites, + struct lgtd_lifx_gateway *gw) +{ + struct lgtd_lifx_site *site = NULL; + LIST_FOREACH(site, sites, link) { + if (site->gw == gw) { + break; + } + } + return site; +} + +struct lgtd_lifx_tag * +lgtd_lifx_tagging_find_tag(const char *tag_label) +{ + struct lgtd_lifx_tag *tag = NULL; + LIST_FOREACH(tag, &lgtd_lifx_tags, link) { + if (!strcmp(tag->label, tag_label)) { + break; + } + } + return tag; +} + +struct lgtd_lifx_tag * +lgtd_lifx_tagging_allocate_tag(const char *tag_label) +{ + assert(tag_label); + assert(strlen(tag_label) < LGTD_LIFX_LABEL_SIZE); + + struct lgtd_lifx_tag *tag = calloc(1, sizeof(*tag)); + if (!tag) { + return NULL; + } + + strncpy(tag->label, tag_label, sizeof(tag->label) - 1); + LIST_INSERT_HEAD(&lgtd_lifx_tags, tag, link); + return tag; +} + +void +lgtd_lifx_tagging_deallocate_tag(struct lgtd_lifx_tag *tag) +{ + assert(tag); + assert(LIST_EMPTY(&tag->sites)); + + LIST_REMOVE(tag, link); + free(tag); +} + +struct lgtd_lifx_tag * +lgtd_lifx_tagging_incref(const char *tag_label, + struct lgtd_lifx_gateway *gw, + int tag_id) +{ + assert(strlen(tag_label) < LGTD_LIFX_LABEL_SIZE); + assert(tag_id < LGTD_LIFX_GATEWAY_MAX_TAGS); + assert(gw); + + bool dealloc_tag = false; + struct lgtd_lifx_tag *tag = lgtd_lifx_tagging_find_tag(tag_label); + if (!tag) { + tag = lgtd_lifx_tagging_allocate_tag(tag_label); + if (!tag) { + return NULL; + } + dealloc_tag = true; + } + + struct lgtd_lifx_site *site = lgtd_lifx_tagging_find_site(&tag->sites, gw); + if (!site) { + site = calloc(1, sizeof(*site)); + if (!site) { + if (dealloc_tag) { + lgtd_lifx_tagging_deallocate_tag(tag); + } + errno = ENOMEM; + return NULL; + } + if (dealloc_tag) { + lgtd_info("discovered tag [%s]", tag_label); + } + char site_addr[LGTD_LIFX_ADDR_STRLEN]; + lgtd_info( + "tag [%s] added to gw %s (site %s) with tag_id %d", + tag_label, gw->peeraddr, + LGTD_IEEE8023MACTOA(gw->site.as_array, site_addr), tag_id + ); + site->gw = gw; + site->tag_id = tag_id; + LIST_INSERT_HEAD(&tag->sites, site, link); + } + assert(site->tag_id == tag_id); + + return tag; +} + +void +lgtd_lifx_tagging_decref(struct lgtd_lifx_tag *tag, + struct lgtd_lifx_gateway *gw) +{ + assert(tag); + assert(gw); + + struct lgtd_lifx_site *site; + site = lgtd_lifx_tagging_find_site(&tag->sites, gw); + if (site) { + char site_addr[LGTD_LIFX_ADDR_STRLEN]; + lgtd_debug( + "tag [%s] removed from gw %s (site %s)", + tag->label, gw->peeraddr, + LGTD_IEEE8023MACTOA(gw->site.as_array, site_addr) + ); + LIST_REMOVE(site, link); + free(site); + } + if (LIST_EMPTY(&tag->sites)) { + lgtd_info("forgetting unused tag [%s]", tag->label); + lgtd_lifx_tagging_deallocate_tag(tag); + } +} diff --git a/lifx/tagging.h b/lifx/tagging.h new file mode 100644 index 0000000..8fe7a16 --- /dev/null +++ b/lifx/tagging.h @@ -0,0 +1,44 @@ +// Copyright (c) 2015, Louis Opter +// +// This file is part of lighstd. +// +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . + +#pragma once + +extern struct lgtd_lifx_tag_list lgtd_lifx_tags; + +struct lgtd_lifx_site { + LIST_ENTRY(lgtd_lifx_site) link; + int tag_id; + struct lgtd_lifx_gateway *gw; +}; +LIST_HEAD(lgtd_lifx_site_list, lgtd_lifx_site); + +struct lgtd_lifx_tag { + LIST_ENTRY(lgtd_lifx_tag) link; + char label[LGTD_LIFX_LABEL_SIZE]; + struct lgtd_lifx_site_list sites; +}; +LIST_HEAD(lgtd_lifx_tag_list, lgtd_lifx_tag); + +struct lgtd_lifx_tag *lgtd_lifx_tagging_incref(const char *, + struct lgtd_lifx_gateway *, + int); +void lgtd_lifx_tagging_decref(struct lgtd_lifx_tag *, struct lgtd_lifx_gateway *); + +struct lgtd_lifx_tag *lgtd_lifx_tagging_find_tag(const char *); +struct lgtd_lifx_tag *lgtd_lifx_tagging_allocate_tag(const char *); + +void lgtd_lifx_tagging_deallocate_tag(struct lgtd_lifx_tag *); diff --git a/lifx/wire_proto.c b/lifx/wire_proto.c new file mode 100644 index 0000000..4794dda --- /dev/null +++ b/lifx/wire_proto.c @@ -0,0 +1,1068 @@ +// Copyright (c) 2014, Louis Opter +// +// This file is part of lighstd. +// +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "wire_proto.h" +#include "core/time_monotonic.h" +#include "bulb.h" +#include "gateway.h" +#include "core/daemon.h" +#include "core/lightsd.h" + +const union lgtd_lifx_target LGTD_LIFX_UNSPEC_TARGET = { .tags = 0 }; + +const int LGTD_LIFX_DEBRUIJN_SEQUENCE[64] = { + 0, 47, 1, 56, 48, 27, 2, 60, + 57, 49, 41, 37, 28, 16, 3, 61, + 54, 58, 35, 52, 50, 42, 21, 44, + 38, 32, 29, 23, 17, 11, 4, 62, + 46, 55, 26, 59, 40, 36, 15, 53, + 34, 51, 20, 43, 31, 22, 10, 45, + 25, 39, 14, 33, 19, 30, 9, 24, + 13, 18, 8, 12, 7, 6, 5, 63 +}; + +static uint32_t lgtd_lifx_client_id = 0; + +static struct lgtd_lifx_packet_info_map lgtd_lifx_packet_info = + RB_INITIALIZER(&lgtd_lifx_packets_infos); + +RB_GENERATE_STATIC( + lgtd_lifx_packet_info_map, + lgtd_lifx_packet_info, + link, + lgtd_lifx_packet_info_cmp +); + +static void +lgtd_lifx_wire_null_packet_encoder_decoder(void *pkt) +{ + (void)pkt; +} + +static void +lgtd_lifx_wire_null_packet_handler(struct lgtd_lifx_gateway *gw, + const struct lgtd_lifx_packet_header *hdr, + const void *pkt) +{ + (void)gw; + (void)hdr; + (void)pkt; +} + +void +lgtd_lifx_wire_enosys_packet_handler(struct lgtd_lifx_gateway *gw, + const struct lgtd_lifx_packet_header *hdr, + const void *pkt) +{ + (void)pkt; + + const struct lgtd_lifx_packet_info *pkt_info; + pkt_info = lgtd_lifx_wire_get_packet_info(hdr->packet_type); + bool addressable = hdr->protocol & LGTD_LIFX_PROTOCOL_ADDRESSABLE; + bool tagged = hdr->protocol & LGTD_LIFX_PROTOCOL_TAGGED; + unsigned int protocol = hdr->protocol & LGTD_LIFX_PROTOCOL_VERSION_MASK; + char target[LGTD_LIFX_ADDR_STRLEN]; + LGTD_LIFX_WIRE_PRINT_TARGET(hdr, target); + lgtd_info( + "%s <-- %s - (Unimplemented, header info: " + "addressable=%d, tagged=%d, protocol=%d, target=%s", + pkt_info->name, gw->peeraddr, addressable, tagged, protocol, target + ); +} + +static void +lgtd_lifx_wire_load_packet_info_map(void) +{ +#define DECODER(x) ((void (*)(void *))(x)) +#define ENCODER(x) ((void (*)(void *))(x)) +#define HANDLER(x) \ + ((void (*)(struct lgtd_lifx_gateway *, \ + const struct lgtd_lifx_packet_header *, \ + const void *))(x)) +#define NO_PAYLOAD \ + .encode = lgtd_lifx_wire_null_packet_encoder_decoder +#define RESPONSE_ONLY \ + .encode = lgtd_lifx_wire_null_packet_encoder_decoder +#define REQUEST_ONLY \ + .decode = lgtd_lifx_wire_null_packet_encoder_decoder, \ + .handle = lgtd_lifx_wire_null_packet_handler +#define UNIMPLEMENTED \ + .decode = lgtd_lifx_wire_null_packet_encoder_decoder, \ + .encode = lgtd_lifx_wire_null_packet_encoder_decoder, \ + .handle = lgtd_lifx_wire_enosys_packet_handler + + static struct lgtd_lifx_packet_info packet_table[] = { + // Gateway packets: + { + REQUEST_ONLY, + NO_PAYLOAD, + .name = "GET_PAN_GATEWAY", + .type = LGTD_LIFX_GET_PAN_GATEWAY + }, + { + .name = "PAN_GATEWAY", + .type = LGTD_LIFX_PAN_GATEWAY, + .size = sizeof(struct lgtd_lifx_packet_pan_gateway), + .decode = DECODER(lgtd_lifx_wire_decode_pan_gateway), + .encode = ENCODER(lgtd_lifx_wire_encode_pan_gateway), + .handle = HANDLER(lgtd_lifx_gateway_handle_pan_gateway) + }, + { + REQUEST_ONLY, + .name = "SET_TAG_LABELS", + .type = LGTD_LIFX_SET_TAG_LABELS, + .size = sizeof(struct lgtd_lifx_packet_tag_labels), + .encode = ENCODER(lgtd_lifx_wire_encode_tag_labels) + }, + { + REQUEST_ONLY, + .name = "GET_TAG_LABELS", + .type = LGTD_LIFX_GET_TAG_LABELS, + .size = sizeof(struct lgtd_lifx_packet_tags), + .encode = ENCODER(lgtd_lifx_wire_encode_tags) + }, + { + RESPONSE_ONLY, + .name = "TAG_LABELS", + .type = LGTD_LIFX_TAG_LABELS, + .size = sizeof(struct lgtd_lifx_packet_tag_labels), + .decode = DECODER(lgtd_lifx_wire_decode_tag_labels), + .handle = HANDLER(lgtd_lifx_gateway_handle_tag_labels) + }, + // Bulb packets: + { + REQUEST_ONLY, + .name = "SET_LIGHT_COLOR", + .type = LGTD_LIFX_SET_LIGHT_COLOR, + .size = sizeof(struct lgtd_lifx_packet_light_color), + .encode = ENCODER(lgtd_lifx_wire_encode_light_color) + }, + { + REQUEST_ONLY, + .name = "SET_WAVEFORM", + .type = LGTD_LIFX_SET_WAVEFORM, + .size = sizeof(struct lgtd_lifx_packet_waveform), + .encode = ENCODER(lgtd_lifx_wire_encode_waveform) + }, + { + REQUEST_ONLY, + NO_PAYLOAD, + .name = "GET_LIGHT_STATUS", + .type = LGTD_LIFX_GET_LIGHT_STATE + }, + { + RESPONSE_ONLY, + .name = "LIGHT_STATUS", + .type = LGTD_LIFX_LIGHT_STATUS, + .size = sizeof(struct lgtd_lifx_packet_light_status), + .decode = DECODER(lgtd_lifx_wire_decode_light_status), + .handle = HANDLER(lgtd_lifx_gateway_handle_light_status) + }, + { + REQUEST_ONLY, + NO_PAYLOAD, // well it has a payload, but it's just 0 or 1... + .size = sizeof(struct lgtd_lifx_packet_power_state), + .name = "SET_POWER_STATE", + .type = LGTD_LIFX_SET_POWER_STATE, + }, + { + RESPONSE_ONLY, + .name = "POWER_STATE", + .type = LGTD_LIFX_POWER_STATE, + .size = sizeof(struct lgtd_lifx_packet_power_state), + .decode = DECODER(lgtd_lifx_wire_decode_power_state), + .handle = HANDLER(lgtd_lifx_gateway_handle_power_state) + }, + { + REQUEST_ONLY, + .name = "SET_TAGS", + .type = LGTD_LIFX_SET_TAGS, + .size = sizeof(struct lgtd_lifx_packet_tags), + .encode = ENCODER(lgtd_lifx_wire_encode_tags) + }, + { + RESPONSE_ONLY, + .name = "TAGS", + .type = LGTD_LIFX_TAGS, + .size = sizeof(struct lgtd_lifx_packet_tags), + .decode = DECODER(lgtd_lifx_wire_decode_tags), + .handle = HANDLER(lgtd_lifx_gateway_handle_tags) + }, + { + REQUEST_ONLY, + NO_PAYLOAD, + .name = "GET_MESH_INFO", + .type = LGTD_LIFX_GET_MESH_INFO + }, + { + RESPONSE_ONLY, + .name = "MESH_INFO", + .type = LGTD_LIFX_MESH_INFO, + .size = sizeof(struct lgtd_lifx_packet_ip_state), + .decode = DECODER(lgtd_lifx_wire_decode_ip_state), + .handle = HANDLER(lgtd_lifx_gateway_handle_ip_state) + }, + { + REQUEST_ONLY, + NO_PAYLOAD, + .name = "GET_MESH_FIRMWARE", + .type = LGTD_LIFX_GET_MESH_FIRMWARE + }, + { + RESPONSE_ONLY, + .name = "MESH_FIRMWARE", + .type = LGTD_LIFX_MESH_FIRMWARE, + .size = sizeof(struct lgtd_lifx_packet_ip_firmware_info), + .decode = DECODER(lgtd_lifx_wire_decode_ip_firmware_info), + .handle = HANDLER(lgtd_lifx_gateway_handle_ip_firmware_info) + }, + { + REQUEST_ONLY, + NO_PAYLOAD, + .name = "GET_WIFI_INFO", + .type = LGTD_LIFX_GET_WIFI_INFO, + }, + { + RESPONSE_ONLY, + .name = "WIFI_INFO", + .type = LGTD_LIFX_WIFI_INFO, + .size = sizeof(struct lgtd_lifx_packet_ip_state), + .decode = DECODER(lgtd_lifx_wire_decode_ip_state), + .handle = HANDLER(lgtd_lifx_gateway_handle_ip_state) + }, + { + REQUEST_ONLY, + NO_PAYLOAD, + .name = "GET_WIFI_FIRMWARE_STATE", + .type = LGTD_LIFX_GET_WIFI_FIRMWARE_STATE + }, + { + RESPONSE_ONLY, + .name = "WIFI_FIRMWARE_STATE", + .type = LGTD_LIFX_WIFI_FIRMWARE_STATE, + .size = sizeof(struct lgtd_lifx_packet_ip_firmware_info), + .decode = DECODER(lgtd_lifx_wire_decode_ip_firmware_info), + .handle = HANDLER(lgtd_lifx_gateway_handle_ip_firmware_info) + }, + { + REQUEST_ONLY, + NO_PAYLOAD, + .name = "GET_VERSION", + .type = LGTD_LIFX_GET_VERSION + }, + { + RESPONSE_ONLY, + .name = "VERSION_STATE", + .type = LGTD_LIFX_VERSION_STATE, + .size = sizeof(struct lgtd_lifx_packet_product_info), + .decode = DECODER(lgtd_lifx_wire_decode_product_info), + .handle = HANDLER(lgtd_lifx_gateway_handle_product_info) + }, + { + REQUEST_ONLY, + NO_PAYLOAD, + .name = "GET_INFO", + .type = LGTD_LIFX_GET_INFO + }, + { + RESPONSE_ONLY, + .name = "INFO_STATE", + .type = LGTD_LIFX_INFO_STATE, + .size = sizeof(struct lgtd_lifx_packet_runtime_info), + .decode = DECODER(lgtd_lifx_wire_decode_runtime_info), + .handle = HANDLER(lgtd_lifx_gateway_handle_runtime_info) + }, + { + REQUEST_ONLY, + .encode = lgtd_lifx_wire_null_packet_encoder_decoder, + .name = "SET_BULB_LABEL", + .type = LGTD_LIFX_SET_BULB_LABEL, + .size = sizeof(struct lgtd_lifx_packet_label) + }, + { + RESPONSE_ONLY, + .name = "BULB_LABEL", + .type = LGTD_LIFX_BULB_LABEL, + .size = sizeof(struct lgtd_lifx_packet_label), + .decode = lgtd_lifx_wire_null_packet_encoder_decoder, + .handle = HANDLER(lgtd_lifx_gateway_handle_bulb_label) + }, + { + REQUEST_ONLY, + NO_PAYLOAD, + .name = "GET_AMBIENT_LIGHT", + .type = LGTD_LIFX_GET_AMBIENT_LIGHT + }, + { + RESPONSE_ONLY, + .name = "STATE_AMBIENT_LIGHT", + .type = LGTD_LIFX_STATE_AMBIENT_LIGHT, + .size = sizeof(struct lgtd_lifx_packet_ambient_light), + .decode = DECODER(lgtd_lifx_wire_decode_ambient_light), + .handle = HANDLER(lgtd_lifx_gateway_handle_ambient_light) + }, + // Unimplemented but "known" packets + { + UNIMPLEMENTED, + .name = "GET_TIME", + .type = LGTD_LIFX_GET_TIME + }, + { + UNIMPLEMENTED, + .name = "SET_TIME", + .type = LGTD_LIFX_SET_TIME + }, + { + UNIMPLEMENTED, + .name = "TIME_STATE", + .type = LGTD_LIFX_TIME_STATE + }, + { + UNIMPLEMENTED, + .name = "GET_RESET_SWITCH_STATE", + .type = LGTD_LIFX_GET_RESET_SWITCH_STATE + }, + { + UNIMPLEMENTED, + .name = "RESET_SWITCH_STATE", + .type = LGTD_LIFX_RESET_SWITCH_STATE + }, + { + UNIMPLEMENTED, + .name = "GET_DUMMY_PAYLOAD", + .type = LGTD_LIFX_GET_DUMMY_PAYLOAD + }, + { + UNIMPLEMENTED, + .name = "SET_DUMMY_PAYLOAD", + .type = LGTD_LIFX_SET_DUMMY_PAYLOAD + }, + { + UNIMPLEMENTED, + .name = "STATE_DUMMY_PAYLOAD", + .type = LGTD_LIFX_STATE_DUMMY_PAYLOAD + }, + { + UNIMPLEMENTED, + .name = "GET_BULB_LABEL", + .type = LGTD_LIFX_GET_BULB_LABEL + }, + { + UNIMPLEMENTED, + .name = "GET_MCU_RAIL_VOLTAGE", + .type = LGTD_LIFX_GET_MCU_RAIL_VOLTAGE + }, + { + UNIMPLEMENTED, + .name = "MCU_RAIL_VOLTAGE", + .type = LGTD_LIFX_MCU_RAIL_VOLTAGE + }, + { + UNIMPLEMENTED, + .name = "REBOOT", + .type = LGTD_LIFX_REBOOT + }, + { + UNIMPLEMENTED, + .name = "SET_FACTORY_TEST_MODE", + .type = LGTD_LIFX_SET_FACTORY_TEST_MODE + }, + { + UNIMPLEMENTED, + .name = "DISABLE_FACTORY_TEST_MODE", + .type = LGTD_LIFX_DISABLE_FACTORY_TEST_MODE + }, + { + UNIMPLEMENTED, + .name = "STATE_FACTORY_TEST_MODE", + .type = LGTD_LIFX_STATE_FACTORY_TEST_MODE + }, + { + UNIMPLEMENTED, + .name = "STATE_SITE", + .type = LGTD_LIFX_STATE_SITE + }, + { + UNIMPLEMENTED, + .name = "STATE_REBOOT", + .type = LGTD_LIFX_STATE_REBOOT + }, + { + UNIMPLEMENTED, + .name = "SET_PAN_GATEWAY", + .type = LGTD_LIFX_SET_PAN_GATEWAY + }, + { + UNIMPLEMENTED, + .name = "ACK", + .type = LGTD_LIFX_ACK + }, + { + UNIMPLEMENTED, + .name = "SET_FACTORY_RESET", + .type = LGTD_LIFX_SET_FACTORY_RESET + }, + { + UNIMPLEMENTED, + .name = "STATE_FACTORY_RESET", + .type = LGTD_LIFX_STATE_FACTORY_RESET + }, + { + UNIMPLEMENTED, + .name = "GET_LOCATION", + .type = LGTD_LIFX_GET_LOCATION + }, + { + UNIMPLEMENTED, + .name = "SET_LOCATION", + .type = LGTD_LIFX_SET_LOCATION + }, + { + UNIMPLEMENTED, + .name = "STATE_LOCATION", + .type = LGTD_LIFX_STATE_LOCATION + }, + { + UNIMPLEMENTED, + .name = "GET_GROUP", + .type = LGTD_LIFX_GET_GROUP + }, + { + UNIMPLEMENTED, + .name = "SET_GROUP", + .type = LGTD_LIFX_SET_GROUP + }, + { + UNIMPLEMENTED, + .name = "STATE_GROUP", + .type = LGTD_LIFX_STATE_GROUP + }, + { + UNIMPLEMENTED, + .name = "GET_OWNER", + .type = LGTD_LIFX_GET_OWNER + }, + { + UNIMPLEMENTED, + .name = "SET_OWNER", + .type = LGTD_LIFX_SET_OWNER + }, + { + UNIMPLEMENTED, + .name = "STATE_OWNER", + .type = LGTD_LIFX_STATE_OWNER + }, + { + UNIMPLEMENTED, + .name = "GET_FACTORY_TEST_MODE", + .type = LGTD_LIFX_GET_FACTORY_TEST_MODE + }, + { + UNIMPLEMENTED, + .name = "ECHO_REQUEST", + .type = LGTD_LIFX_ECHO_REQUEST + }, + { + UNIMPLEMENTED, + .name = "ECHO_RESPONSE", + .type = LGTD_LIFX_ECHO_RESPONSE + }, + { + UNIMPLEMENTED, + .name = "SET_DIM_ABSOLUTE", + .type = LGTD_LIFX_SET_DIM_ABSOLUTE + }, + { + UNIMPLEMENTED, + .name = "SET_DIM_RELATIVE", + .type = LGTD_LIFX_SET_DIM_RELATIVE + }, + { + UNIMPLEMENTED, + .name = "SET_RGBW", + .type = LGTD_LIFX_SET_RGBW + }, + { + UNIMPLEMENTED, + .name = "GET_RAIL_VOLTAGE", + .type = LGTD_LIFX_GET_RAIL_VOLTAGE + }, + { + UNIMPLEMENTED, + .name = "STATE_RAIL_VOLTAGE", + .type = LGTD_LIFX_STATE_RAIL_VOLTAGE + }, + { + UNIMPLEMENTED, + .name = "GET_TEMPERATURE", + .type = LGTD_LIFX_GET_TEMPERATURE + }, + { + UNIMPLEMENTED, + .name = "STATE_TEMPERATURE", + .type = LGTD_LIFX_STATE_TEMPERATURE + }, + { + UNIMPLEMENTED, + .name = "SET_CALIBRATION_COEFFICIENTS", + .type = LGTD_LIFX_SET_CALIBRATION_COEFFICIENTS + }, + { + UNIMPLEMENTED, + .name = "SET_SIMPLE_EVENT", + .type = LGTD_LIFX_SET_SIMPLE_EVENT + }, + { + UNIMPLEMENTED, + .name = "GET_SIMPLE_EVENT", + .type = LGTD_LIFX_GET_SIMPLE_EVENT + }, + { + UNIMPLEMENTED, + .name = "STATE_SIMPLE_EVENT", + .type = LGTD_LIFX_STATE_SIMPLE_EVENT + }, + { + UNIMPLEMENTED, + .name = "GET_POWER", + .type = LGTD_LIFX_GET_POWER + }, + { + UNIMPLEMENTED, + .name = "SET_POWER", + .type = LGTD_LIFX_SET_POWER + }, + { + UNIMPLEMENTED, + .name = "STATE_POWER", + .type = LGTD_LIFX_STATE_POWER + }, + { + UNIMPLEMENTED, + .name = "SET_WAVEFORM_OPTIONAL", + .type = LGTD_LIFX_SET_WAVEFORM_OPTIONAL + }, + { + UNIMPLEMENTED, + .name = "CONNECT_PLAIN", + .type = LGTD_LIFX_CONNECT_PLAIN + }, + { + UNIMPLEMENTED, + .name = "CONNECT_KEY", + .type = LGTD_LIFX_CONNECT_KEY + }, + { + UNIMPLEMENTED, + .name = "STATE_CONNECT", + .type = LGTD_LIFX_STATE_CONNECT + }, + { + UNIMPLEMENTED, + .name = "GET_AUTH_KEY", + .type = LGTD_LIFX_GET_AUTH_KEY + }, + { + UNIMPLEMENTED, + .name = "SET_AUTH_KEY", + .type = LGTD_LIFX_SET_AUTH_KEY + }, + { + UNIMPLEMENTED, + .name = "STATE_AUTH_KEY", + .type = LGTD_LIFX_STATE_AUTH_KEY + }, + { + UNIMPLEMENTED, + .name = "SET_KEEP_ALIVE", + .type = LGTD_LIFX_SET_KEEP_ALIVE + }, + { + UNIMPLEMENTED, + .name = "STATE_KEEP_ALIVE", + .type = LGTD_LIFX_STATE_KEEP_ALIVE + }, + { + UNIMPLEMENTED, + .name = "SET_HOST", + .type = LGTD_LIFX_SET_HOST + }, + { + UNIMPLEMENTED, + .name = "GET_HOST", + .type = LGTD_LIFX_GET_HOST + }, + { + UNIMPLEMENTED, + .name = "STATE_HOST", + .type = LGTD_LIFX_STATE_HOST + }, + { + UNIMPLEMENTED, + .name = "GET_WIFI_STATE", + .type = LGTD_LIFX_GET_WIFI_STATE + }, + { + UNIMPLEMENTED, + .name = "SET_WIFI_STATE", + .type = LGTD_LIFX_SET_WIFI_STATE + }, + { + UNIMPLEMENTED, + .name = "WIFI_STATE", + .type = LGTD_LIFX_WIFI_STATE + }, + { + UNIMPLEMENTED, + .name = "GET_ACCESS_POINTS", + .type = LGTD_LIFX_GET_ACCESS_POINTS + }, + { + UNIMPLEMENTED, + .name = "SET_ACCESS_POINTS", + .type = LGTD_LIFX_SET_ACCESS_POINTS + }, + { + UNIMPLEMENTED, + .name = "STATE_ACCESS_POINTS", + .type = LGTD_LIFX_STATE_ACCESS_POINTS + }, + { + UNIMPLEMENTED, + .name = "GET_ACCESS_POINT", + .type = LGTD_LIFX_GET_ACCESS_POINT + }, + { + UNIMPLEMENTED, + .name = "STATE_ACCESS_POINT", + .type = LGTD_LIFX_STATE_ACCESS_POINT + }, + { + UNIMPLEMENTED, + .name = "SET_ACCESS_POINT_BROADCAST", + .type = LGTD_LIFX_SET_ACCESS_POINT_BROADCAST + }, + { + UNIMPLEMENTED, + .name = "GET_DIMMER_VOLTAGE", + .type = LGTD_LIFX_GET_DIMMER_VOLTAGE + }, + { + UNIMPLEMENTED, + .name = "STATE_DIMMER_VOLTAGE", + .type = LGTD_LIFX_STATE_DIMMER_VOLTAGE + } + }; + + for (int i = 0; i != LGTD_ARRAY_SIZE(packet_table); ++i) { + RB_INSERT( + lgtd_lifx_packet_info_map, + &lgtd_lifx_packet_info, + &packet_table[i] + ); + } +} + +const struct lgtd_lifx_packet_info * +lgtd_lifx_wire_get_packet_info(enum lgtd_lifx_packet_type packet_type) +{ + struct lgtd_lifx_packet_info pkt_info = { .type = packet_type }; + return RB_FIND(lgtd_lifx_packet_info_map, &lgtd_lifx_packet_info, &pkt_info); +} + +void +lgtd_lifx_wire_setup(void) +{ + lgtd_lifx_wire_load_packet_info_map(); + do { + lgtd_lifx_client_id = lgtd_daemon_randuint32(); + } while (!lgtd_lifx_client_id); +} + + +#define WAVEFORM_ENTRY(e) { .str = e, .len = sizeof(e) - 1 } +const struct lgtd_lifx_waveform_string_id lgtd_lifx_waveform_table[] = { + WAVEFORM_ENTRY("SAW"), + WAVEFORM_ENTRY("SINE"), + WAVEFORM_ENTRY("HALF_SINE"), + WAVEFORM_ENTRY("TRIANGLE"), + WAVEFORM_ENTRY("SQUARE"), + WAVEFORM_ENTRY("INVALID") +}; + +enum lgtd_lifx_waveform_type +lgtd_lifx_wire_waveform_string_id_to_type(const char *s, int len) +{ + assert(s); + assert(len >= 0); + + for (int i = 0; i != LGTD_ARRAY_SIZE(lgtd_lifx_waveform_table); i++) { + const struct lgtd_lifx_waveform_string_id *entry; + entry = &lgtd_lifx_waveform_table[i]; + if (entry->len == len && !memcmp(entry->str, s, len)) { + return i; + } + } + + return LGTD_LIFX_WAVEFORM_INVALID; +} + +bool +lgtd_lifx_wire_handle_receive(evutil_socket_t socket, + struct lgtd_lifx_gateway *gw) +{ + assert(socket != -1); + + while (true) { + struct sockaddr_storage peer; + // if we get back from recvfrom with a sockaddr_in the end of the struct + // will not be initialized and we will be comparing unintialized stuff + // in lgtd_lifx_gateway_get: + memset(&peer, 0, sizeof(peer)); + ev_socklen_t addrlen = sizeof(peer); + union { + char buf[LGTD_LIFX_MAX_PACKET_SIZE]; + struct lgtd_lifx_packet_header hdr; + } read; + int nbytes = recvfrom( + socket, + read.buf, + sizeof(read.buf), + 0, + (struct sockaddr *)&peer, + &addrlen + ); + if (nbytes == -1) { + int error = EVUTIL_SOCKET_ERROR(); + if (error == EINTR) { + continue; + } + if (error == EAGAIN) { + return true; + } + lgtd_warn("can't receive LIFX packet"); + return false; + } + + lgtd_time_mono_t received_at = lgtd_time_monotonic_msecs(); + char peer_addr[INET6_ADDRSTRLEN]; + LGTD_SOCKADDRTOA((const struct sockaddr *)&peer, peer_addr); + + if (nbytes < LGTD_LIFX_PACKET_HEADER_SIZE) { + lgtd_warnx("broadcast packet too short from %s", peer_addr); + return false; + } + + lgtd_lifx_wire_decode_header(&read.hdr); + if (read.hdr.size != nbytes) { + lgtd_warnx("incomplete broadcast packet from %s", peer_addr); + return false; + } + int proto_version = read.hdr.protocol & LGTD_LIFX_PROTOCOL_VERSION_MASK; + if (proto_version != LGTD_LIFX_PROTOCOL_V1) { + lgtd_warnx( + "unsupported protocol %d from %s", + read.hdr.protocol & LGTD_LIFX_PROTOCOL_VERSION_MASK, peer_addr + ); + } + if (read.hdr.packet_type == LGTD_LIFX_GET_PAN_GATEWAY) { + continue; + } + + const struct lgtd_lifx_packet_info *pkt_info = + lgtd_lifx_wire_get_packet_info(read.hdr.packet_type); + if (!pkt_info) { + lgtd_warnx( + "received unknown packet %#x from %s", + read.hdr.packet_type, peer_addr + ); + continue; + } + if (!(read.hdr.protocol & LGTD_LIFX_PROTOCOL_ADDRESSABLE)) { + lgtd_warnx( + "received non-addressable packet %s from %s", + pkt_info->name, peer_addr + ); + continue; + } + void *pkt = &read.buf[LGTD_LIFX_PACKET_HEADER_SIZE]; + pkt_info->decode(pkt); + struct sockaddr *addr = (struct sockaddr *)&peer; + lgtd_lifx_gateway_handle_packet( + gw, addr, addrlen, pkt_info, &read.hdr, pkt, received_at + ); + } +} + +static void +lgtd_lifx_wire_encode_header(struct lgtd_lifx_packet_header *hdr, int flags) +{ + assert(hdr); + + hdr->size = htole16(hdr->size); + hdr->protocol = htole16(LGTD_LIFX_PROTOCOL_V1); + if (flags & LGTD_LIFX_ADDRESSABLE) { + hdr->protocol |= LGTD_LIFX_PROTOCOL_ADDRESSABLE; + } + if (flags & LGTD_LIFX_TAGGED) { + hdr->protocol |= LGTD_LIFX_PROTOCOL_TAGGED; + hdr->target.tags = htole64(hdr->target.tags); + } + if (flags & LGTD_LIFX_ACK_REQUIRED) { + hdr->flags |= LGTD_LIFX_FLAG_ACK_REQUIRED; + } + if (flags & LGTD_LIFX_RES_REQUIRED) { + hdr->flags |= LGTD_LIFX_FLAG_RES_REQUIRED; + } + hdr->at_time = htole64(hdr->at_time); + hdr->packet_type = htole16(hdr->packet_type); + hdr->source = htole32(hdr->source); // not strictly necessary but eh. +} + +// Convert all the fields in the header to the host endianness. +// +// \return The payload size or -1 if the header is invalid. +void +lgtd_lifx_wire_decode_header(struct lgtd_lifx_packet_header *hdr) +{ + assert(hdr); + + hdr->size = le16toh(hdr->size); + hdr->protocol = ( + le16toh(hdr->protocol & LGTD_LIFX_PROTOCOL_VERSION_MASK) + | (hdr->protocol & LGTD_LIFX_PROTOCOL_FLAGS_MASK) + ); + if (hdr->protocol & LGTD_LIFX_PROTOCOL_TAGGED) { + hdr->target.tags = le64toh(hdr->target.tags); + } + hdr->at_time = le64toh(hdr->at_time); + hdr->packet_type = le16toh(hdr->packet_type); + hdr->source = le32toh(hdr->source); +} + +const struct lgtd_lifx_packet_info * +lgtd_lifx_wire_setup_header(struct lgtd_lifx_packet_header *hdr, + enum lgtd_lifx_target_type target_type, + union lgtd_lifx_target target, + const uint8_t *site, + enum lgtd_lifx_packet_type packet_type) +{ + assert(hdr); + assert(lgtd_lifx_client_id); + + const struct lgtd_lifx_packet_info *pkt_info = + lgtd_lifx_wire_get_packet_info(packet_type); + + assert(pkt_info); + + memset(hdr, 0, sizeof(*hdr)); + hdr->source = lgtd_lifx_client_id; + hdr->size = pkt_info->size + sizeof(*hdr); + hdr->packet_type = packet_type; + if (site) { + memcpy(hdr->site, site, sizeof(hdr->site)); + } else { + assert(target_type == LGTD_LIFX_TARGET_ALL_DEVICES); + } + + int flags = LGTD_LIFX_ADDRESSABLE|LGTD_LIFX_RES_REQUIRED; + switch (target_type) { + case LGTD_LIFX_TARGET_SITE: + case LGTD_LIFX_TARGET_ALL_DEVICES: + flags |= LGTD_LIFX_TAGGED; + break; + case LGTD_LIFX_TARGET_TAGS: + flags |= LGTD_LIFX_TAGGED; + hdr->target.tags = target.tags; + break; + case LGTD_LIFX_TARGET_DEVICE: + memcpy(hdr->target.device_addr, target.addr, LGTD_LIFX_ADDR_LENGTH); + break; + } + + lgtd_lifx_wire_encode_header(hdr, flags); + + return pkt_info; +} + +void +lgtd_lifx_wire_decode_pan_gateway(struct lgtd_lifx_packet_pan_gateway *pkt) +{ + assert(pkt); + + pkt->port = le32toh(pkt->port); +} + +void +lgtd_lifx_wire_encode_pan_gateway(struct lgtd_lifx_packet_pan_gateway *pkt) +{ + assert(pkt); + + pkt->port = htole32(pkt->port); +} + +void +lgtd_lifx_wire_decode_light_status(struct lgtd_lifx_packet_light_status *pkt) +{ + assert(pkt); + + pkt->hue = le16toh(pkt->hue); + pkt->saturation = le16toh(pkt->saturation); + pkt->brightness = le16toh(pkt->brightness); + pkt->kelvin = le16toh(pkt->kelvin); + pkt->dim = le16toh(pkt->dim); + // The bulbs actually return power values between 0 and 0xffff, not sure + // what the intermediate values mean, let's pull them down to 0: + if (pkt->power != LGTD_LIFX_POWER_ON) { + pkt->power = LGTD_LIFX_POWER_OFF; + } + pkt->tags = le64toh(pkt->tags); +} + +void +lgtd_lifx_wire_decode_power_state(struct lgtd_lifx_packet_power_state *pkt) +{ + assert(pkt); + + if (pkt->power != LGTD_LIFX_POWER_ON) { + pkt->power = LGTD_LIFX_POWER_OFF; + } +} + +void +lgtd_lifx_wire_encode_light_color(struct lgtd_lifx_packet_light_color *pkt) +{ + assert(pkt); + + pkt->hue = htole16(pkt->hue); + pkt->saturation = htole16(pkt->saturation); + pkt->brightness = htole16(pkt->brightness); + pkt->kelvin = htole16(pkt->kelvin); + pkt->transition = htole32(pkt->transition); +} + +void +lgtd_lifx_wire_encode_waveform(struct lgtd_lifx_packet_waveform *pkt) +{ + assert(pkt); + + pkt->hue = htole16(pkt->hue); + pkt->saturation = htole16(pkt->saturation); + pkt->brightness = htole16(pkt->brightness); + pkt->kelvin = htole16(pkt->kelvin); + pkt->period = htole32(pkt->period); + pkt->cycles = lgtd_lifx_wire_htolefloat(pkt->cycles); + pkt->skew_ratio = htole16(pkt->skew_ratio); +} + +void +lgtd_lifx_wire_encode_tag_labels(struct lgtd_lifx_packet_tag_labels *pkt) +{ + assert(pkt); + + pkt->tags = htole64(pkt->tags); +} + +void +lgtd_lifx_wire_decode_tag_labels(struct lgtd_lifx_packet_tag_labels *pkt) +{ + assert(pkt); + + pkt->label[sizeof(pkt->label) - 1] = '\0'; + pkt->tags = le64toh(pkt->tags); +} + +void +lgtd_lifx_wire_encode_tags(struct lgtd_lifx_packet_tags *pkt) +{ + assert(pkt); + + pkt->tags = htole64(pkt->tags); +} + +void +lgtd_lifx_wire_decode_tags(struct lgtd_lifx_packet_tags *pkt) +{ + assert(pkt); + + pkt->tags = le64toh(pkt->tags); +} + +void +lgtd_lifx_wire_decode_ip_state(struct lgtd_lifx_packet_ip_state *pkt) +{ + assert(pkt); + + pkt->signal_strength = lgtd_lifx_wire_lefloattoh(pkt->signal_strength); + pkt->tx_bytes = le32toh(pkt->tx_bytes); + pkt->rx_bytes = le32toh(pkt->rx_bytes); + pkt->temperature = le16toh(pkt->temperature); +} + +void +lgtd_lifx_wire_decode_ip_firmware_info(struct lgtd_lifx_packet_ip_firmware_info *pkt) +{ + assert(pkt); + + pkt->built_at = le64toh(pkt->built_at); + pkt->installed_at = le64toh(pkt->installed_at); + pkt->version = le32toh(pkt->version); +} + +void +lgtd_lifx_wire_decode_product_info(struct lgtd_lifx_packet_product_info *pkt) +{ + assert(pkt); + + pkt->vendor_id = le32toh(pkt->vendor_id); + pkt->product_id = le32toh(pkt->product_id); + pkt->version = le32toh(pkt->version); +} + +void +lgtd_lifx_wire_decode_runtime_info(struct lgtd_lifx_packet_runtime_info *pkt) +{ + assert(pkt); + + pkt->time = le64toh(pkt->time); + pkt->uptime = le64toh(pkt->uptime); + pkt->downtime = le64toh(pkt->downtime); +} + +void +lgtd_lifx_wire_decode_ambient_light(struct lgtd_lifx_packet_ambient_light *pkt) +{ + assert(pkt); + + pkt->illuminance = lgtd_lifx_wire_lefloattoh(pkt->illuminance); +} diff --git a/lifx/wire_proto.h b/lifx/wire_proto.h new file mode 100644 index 0000000..6d55306 --- /dev/null +++ b/lifx/wire_proto.h @@ -0,0 +1,484 @@ +// Copyright (c) 2014, Louis Opter +// +// This file is part of lighstd. +// +// lighstd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// lighstd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with lighstd. If not, see . + +#pragma once + +typedef uint16_t uint16le_t; +typedef uint16_t uint16be_t; +typedef uint32_t uint32le_t; +typedef uint32_t uint32be_t; +typedef uint64_t uint64le_t; +typedef uint64_t uint64be_t; +typedef float floatle_t; + +static inline floatle_t +lgtd_lifx_wire_htolefloat(float f) +{ + union { float f; uint32_t i; } u = { .f = f }; + u.i = htole32(u.i); + return u.f; +} + +static inline floatle_t +lgtd_lifx_wire_lefloattoh(float f) +{ + union { float f; uint32_t i; } u = { .f = f }; + u.i = le32toh(u.i); + return u.f; +} + +enum { LGTD_LIFX_PROTOCOL_PORT = 56700 }; + +enum { LGTD_LIFX_ADDR_LENGTH = 6 }; +enum { LGTD_LIFX_ADDR_STRLEN = 32 }; + +#pragma pack(push, 1) + +struct lgtd_lifx_packet_header { + //! Packet size including the headers (i.e: this structure). + uint16le_t size; + //! 15 (MSB) 13 12 11 0 + //! +-----------------+------------+-----------------+--------------+ + //! | origin (2 bits) | tagged (1) | addressable (1) | version (12) | + //! +-----------------+------------+-----------------+--------------+ + //! + //! - version: protocol version should be LGTD_LIFX_PROTOCOL_V1; + //! - addressable: true when the target field holds a device address; + //! - tagged: true when the target field holds tags; + //! - origin: LIFX internal use, should be 0. + uint16le_t protocol; + //! http://lan.developer.lifx.com/v2.0/docs/header-description + //! The source identifier allows each client to provide an unique value, + //! which will be included by the LIFX device in any message that is sent + //! in response to a message sent by the client. If the source identifier + //! is a non-zero value, then the LIFX device will send a unicast message + //! to the source IP address and port that the client used to send the + //! originating message. If the source identifier is a zero value, then the + //! LIFX device may send a broadcast message that can be received by all + //! clients on the same sub-net. See _ack_required_ and _res_required_ + //! fields in the Frame Address. + uint32le_t source; + union { + //! All targeted tags ORed together. + uint64le_t tags; + //! Address of the targeted device. + uint8_t device_addr[LGTD_LIFX_ADDR_LENGTH]; + } target; + uint8_t site[LGTD_LIFX_ADDR_LENGTH]; + //! 7 2 1 0 + //! +-------------------+------------------+------------------+ + //! | reserved (6 bits) | ack required (1) | res required (1) | + //! +-------------------+------------------+------------------+ + //! + //! - ack required: true when an acknowledge packet is required; + //! - res required: true when a response is required (the response type + //! depends on the request type). + uint8_t flags; + //! The sequence number allows the client to provide a unique value, which + //! will be included by the LIFX device in any message that is sent in + //! response to a message sent by the client. This allows the client to + //! distinguish between different messages sent with the same source + //! identifier in the Frame. See _ack_required_ and _res_required_ fields in + //! the Frame Address. + uint8_t seqn; + //! Apparently this is a unix epoch timestamp in milliseconds at which the + //! payload should be run. + uint64le_t at_time; + uint16le_t packet_type; + uint8_t reserved[2]; +}; + +#define LGTD_LIFX_WIRE_PRINT_TARGET(hdr, buf) do { \ + if ((hdr)->protocol & LGTD_LIFX_PROTOCOL_TAGGED) { \ + snprintf((buf), sizeof((buf)), "%#jx", (uintmax_t)(hdr)->target.tags); \ + } else { \ + LGTD_IEEE8023MACTOA((hdr)->target.device_addr, (buf)); \ + } \ +} while (0) + +enum { LGTD_LIFX_PACKET_HEADER_SIZE = sizeof(struct lgtd_lifx_packet_header) }; + +enum lgtd_lifx_protocol { + LGTD_LIFX_PROTOCOL_V1 = 0x400, +#if LGTD_BIG_ENDIAN_SYSTEM + LGTD_LIFX_PROTOCOL_VERSION_MASK = 0xff0f, + LGTD_LIFX_PROTOCOL_FLAGS_MASK = 0x00f0, + LGTD_LIFX_PROTOCOL_ADDRESSABLE = 0x0010, + LGTD_LIFX_PROTOCOL_TAGGED = 0x0020 +#else + LGTD_LIFX_PROTOCOL_VERSION_MASK = 0x0fff, + LGTD_LIFX_PROTOCOL_FLAGS_MASK = 0xf000, + LGTD_LIFX_PROTOCOL_ADDRESSABLE = 0x1000, + LGTD_LIFX_PROTOCOL_TAGGED = 0x2000 +#endif +}; + +enum lgtd_lifx_flags { + LGTD_LIFX_FLAG_RES_REQUIRED = 1, + LGTD_LIFX_FLAG_ACK_REQUIRED = 1 << 1, +}; + +// Let's define a maximum packet size just in case somebody sends us weird +// headers: +enum { LGTD_LIFX_MAX_PACKET_SIZE = 4096 }; + +enum lgtd_lifx_packet_type { // FIXME: normalize and prefix everything correctly + // Device + LGTD_LIFX_SET_SITE = 0x01, + LGTD_LIFX_GET_PAN_GATEWAY = 0x02, + LGTD_LIFX_PAN_GATEWAY = 0x03, + LGTD_LIFX_GET_TIME = 0x04, + LGTD_LIFX_SET_TIME = 0x05, + LGTD_LIFX_TIME_STATE = 0x06, + LGTD_LIFX_GET_RESET_SWITCH_STATE = 0x07, + LGTD_LIFX_RESET_SWITCH_STATE = 0x08, + LGTD_LIFX_GET_DUMMY_PAYLOAD = 0x09, + LGTD_LIFX_SET_DUMMY_PAYLOAD = 0x0a, + LGTD_LIFX_STATE_DUMMY_PAYLOAD = 0x0b, + LGTD_LIFX_GET_MESH_INFO = 0x0c, + LGTD_LIFX_MESH_INFO = 0x0d, + LGTD_LIFX_GET_MESH_FIRMWARE = 0x0e, + LGTD_LIFX_MESH_FIRMWARE = 0x0f, + LGTD_LIFX_GET_WIFI_INFO = 0x10, + LGTD_LIFX_WIFI_INFO = 0x11, + LGTD_LIFX_GET_WIFI_FIRMWARE_STATE = 0x12, + LGTD_LIFX_WIFI_FIRMWARE_STATE = 0x13, + LGTD_LIFX_GET_POWER_STATE = 0x14, + LGTD_LIFX_SET_POWER_STATE = 0x15, + LGTD_LIFX_POWER_STATE = 0x16, + LGTD_LIFX_GET_BULB_LABEL = 0x17, + LGTD_LIFX_SET_BULB_LABEL = 0x18, + LGTD_LIFX_BULB_LABEL = 0x19, + LGTD_LIFX_GET_TAGS = 0x1a, + LGTD_LIFX_SET_TAGS = 0x1b, + LGTD_LIFX_TAGS = 0x1c, + LGTD_LIFX_GET_TAG_LABELS = 0x1d, + LGTD_LIFX_SET_TAG_LABELS = 0x1e, + LGTD_LIFX_TAG_LABELS = 0x1f, + LGTD_LIFX_GET_VERSION = 0x20, + LGTD_LIFX_VERSION_STATE = 0x21, + LGTD_LIFX_GET_INFO = 0x22, + LGTD_LIFX_INFO_STATE = 0x23, + LGTD_LIFX_GET_MCU_RAIL_VOLTAGE = 0x24, + LGTD_LIFX_MCU_RAIL_VOLTAGE = 0x25, + LGTD_LIFX_REBOOT = 0x26, + LGTD_LIFX_SET_FACTORY_TEST_MODE = 0x27, + LGTD_LIFX_DISABLE_FACTORY_TEST_MODE = 0x28, + LGTD_LIFX_STATE_FACTORY_TEST_MODE = 0x29, + LGTD_LIFX_STATE_SITE = 0x2a, + LGTD_LIFX_STATE_REBOOT = 0x2b, + LGTD_LIFX_SET_PAN_GATEWAY = 0x2c, + LGTD_LIFX_ACK = 0x2d, + LGTD_LIFX_SET_FACTORY_RESET = 0x2e, + LGTD_LIFX_STATE_FACTORY_RESET = 0x2f, + LGTD_LIFX_GET_LOCATION = 0x30, + LGTD_LIFX_SET_LOCATION = 0x31, + LGTD_LIFX_STATE_LOCATION = 0x32, + LGTD_LIFX_GET_GROUP = 0x33, // TODO: replace GET/SET_TAG_LABELS ? + LGTD_LIFX_SET_GROUP = 0x34, + LGTD_LIFX_STATE_GROUP = 0x35, + LGTD_LIFX_GET_OWNER = 0x36, + LGTD_LIFX_SET_OWNER = 0x37, + LGTD_LIFX_STATE_OWNER = 0x38, + LGTD_LIFX_GET_FACTORY_TEST_MODE = 0x39, + LGTD_LIFX_ECHO_REQUEST = 0x3a, + LGTD_LIFX_ECHO_RESPONSE = 0x3b, + // Light + LGTD_LIFX_GET_LIGHT_STATE = 0x65, + LGTD_LIFX_SET_LIGHT_COLOR = 0x66, + LGTD_LIFX_SET_WAVEFORM = 0x67, + LGTD_LIFX_SET_DIM_ABSOLUTE = 0x68, + LGTD_LIFX_SET_DIM_RELATIVE = 0x69, + LGTD_LIFX_SET_RGBW = 0x6a, + LGTD_LIFX_LIGHT_STATUS = 0x6b, + LGTD_LIFX_GET_RAIL_VOLTAGE = 0x6c, + LGTD_LIFX_STATE_RAIL_VOLTAGE = 0x6d, + LGTD_LIFX_GET_TEMPERATURE = 0x6e, + LGTD_LIFX_STATE_TEMPERATURE = 0x6f, + LGTD_LIFX_SET_CALIBRATION_COEFFICIENTS = 0x70, + LGTD_LIFX_SET_SIMPLE_EVENT = 0x71, + LGTD_LIFX_GET_SIMPLE_EVENT = 0x72, + LGTD_LIFX_STATE_SIMPLE_EVENT = 0x73, + LGTD_LIFX_GET_POWER = 0x74, + LGTD_LIFX_SET_POWER = 0x75, + LGTD_LIFX_STATE_POWER = 0x76, + LGTD_LIFX_SET_WAVEFORM_OPTIONAL = 0x77, + // Wan + LGTD_LIFX_CONNECT_PLAIN = 0xc9, + LGTD_LIFX_CONNECT_KEY = 0xca, + LGTD_LIFX_STATE_CONNECT = 0xcb, + LGTD_LIFX_GET_AUTH_KEY = 0xcc, + LGTD_LIFX_SET_AUTH_KEY = 0xcd, + LGTD_LIFX_STATE_AUTH_KEY = 0xce, + LGTD_LIFX_SET_KEEP_ALIVE = 0xcf, + LGTD_LIFX_STATE_KEEP_ALIVE = 0xd0, + LGTD_LIFX_SET_HOST = 0xd1, + LGTD_LIFX_GET_HOST = 0xd2, + LGTD_LIFX_STATE_HOST = 0xd3, + // Wifi + LGTD_LIFX_GET_WIFI_STATE = 0x12d, + LGTD_LIFX_SET_WIFI_STATE = 0x12e, + LGTD_LIFX_WIFI_STATE = 0x12f, + LGTD_LIFX_GET_ACCESS_POINTS = 0x130, + LGTD_LIFX_SET_ACCESS_POINTS = 0x131, + LGTD_LIFX_STATE_ACCESS_POINTS = 0x132, + LGTD_LIFX_GET_ACCESS_POINT = 0x133, + LGTD_LIFX_STATE_ACCESS_POINT = 0x134, + LGTD_LIFX_SET_ACCESS_POINT_BROADCAST = 0x135, + // Sensor + LGTD_LIFX_GET_AMBIENT_LIGHT = 0x191, + LGTD_LIFX_STATE_AMBIENT_LIGHT = 0x192, + LGTD_LIFX_GET_DIMMER_VOLTAGE = 0x193, + LGTD_LIFX_STATE_DIMMER_VOLTAGE = 0x194 +}; + +enum { LGTD_LIFX_LABEL_SIZE = 32 }; + +struct lgtd_lifx_packet_light_status { + uint16le_t hue; + uint16le_t saturation; + uint16le_t brightness; + uint16le_t kelvin; + uint16le_t dim; + uint16le_t power; + uint8_t label[LGTD_LIFX_LABEL_SIZE]; + uint64be_t tags; +}; + +enum lgtd_lifx_power_state { + LGTD_LIFX_POWER_OFF = 0, + LGTD_LIFX_POWER_ON = 0xffff +}; + +struct lgtd_lifx_packet_power_state { + uint16_t power; // see enum lgtd_lifx_power_state +}; + +enum lgtd_lifx_service_type { + LGTD_LIFX_SERVICE_TCP = 1, + LGTD_LIFX_SERVICE_UDP = 2 +}; + +struct lgtd_lifx_packet_pan_gateway { + uint8_t service_type; // see enum lgtd_lifx_service_type + uint32le_t port; +}; + +enum lgtd_lifx_target_type { + LGTD_LIFX_TARGET_SITE, + LGTD_LIFX_TARGET_TAGS, + LGTD_LIFX_TARGET_DEVICE, + LGTD_LIFX_TARGET_ALL_DEVICES +}; + +struct lgtd_lifx_packet_light_color { + uint8_t stream; // should be 0 + uint16le_t hue; + uint16le_t saturation; + uint16le_t brightness; + uint16le_t kelvin; + uint32le_t transition; // transition time to the color in msecs +}; + +// note: those can be used as indexes for lgtd_lifx_waveform_table +enum lgtd_lifx_waveform_type { + LGTD_LIFX_WAVEFORM_SAW = 0, + LGTD_LIFX_WAVEFORM_SINE = 1, + LGTD_LIFX_WAVEFORM_HALF_SINE = 2, + LGTD_LIFX_WAVEFORM_TRIANGLE = 3, + LGTD_LIFX_WAVEFORM_SQUARE = 4, + LGTD_LIFX_WAVEFORM_INVALID = 5, +}; + +struct lgtd_lifx_packet_waveform { + uint8_t stream; + uint8_t transient; + uint16le_t hue; + uint16le_t saturation; + uint16le_t brightness; + uint16le_t kelvin; + uint32le_t period; // milliseconds + floatle_t cycles; // yes, this value is really encoded as a float. + uint16le_t skew_ratio; + uint8_t waveform; // see enum lgtd_lifx_waveform_type +}; + +enum { LGTD_LIFX_ALL_TAGS = ~0 }; +struct lgtd_lifx_packet_tags { + uint64le_t tags; +}; + +struct lgtd_lifx_packet_label { + char label[LGTD_LIFX_LABEL_SIZE]; +}; + +struct lgtd_lifx_packet_tag_labels { + uint64le_t tags; + char label[LGTD_LIFX_LABEL_SIZE]; +}; + +struct lgtd_lifx_packet_ip_state { + floatle_t signal_strength; + uint32le_t tx_bytes; + uint32le_t rx_bytes; + uint16le_t temperature; +}; + +struct lgtd_lifx_packet_ip_firmware_info { + uint64le_t built_at; + uint64le_t installed_at; + uint32le_t version; +}; + +struct lgtd_lifx_packet_product_info { + uint32le_t vendor_id; + uint32le_t product_id; + uint32le_t version; +}; + +struct lgtd_lifx_packet_runtime_info { + uint64le_t time; + uint64le_t uptime; + uint64le_t downtime; +}; + +struct lgtd_lifx_packet_ambient_light { + floatle_t illuminance; // lux +}; + +#pragma pack(pop) + +enum { LGTD_LIFX_VENDOR_ID = 1 }; + +enum lgtd_lifx_header_flags { + LGTD_LIFX_ADDRESSABLE = 1, + LGTD_LIFX_TAGGED = 1 << 1, + LGTD_LIFX_ACK_REQUIRED = 1 << 2, + LGTD_LIFX_RES_REQUIRED = 1 << 3 +}; + +struct lgtd_lifx_waveform_string_id { + const char *str; + int len; +}; + +extern const struct lgtd_lifx_waveform_string_id lgtd_lifx_waveform_table[]; + +struct lgtd_lifx_gateway; + +struct lgtd_lifx_packet_info { + RB_ENTRY(lgtd_lifx_packet_info) link; + const char *name; + enum lgtd_lifx_packet_type type; + unsigned size; + void (*decode)(void *); + void (*encode)(void *); + void (*handle)(struct lgtd_lifx_gateway *, + const struct lgtd_lifx_packet_header *, + const void *); +}; +RB_HEAD(lgtd_lifx_packet_info_map, lgtd_lifx_packet_info); + +static inline int +lgtd_lifx_packet_info_cmp(struct lgtd_lifx_packet_info *a, + struct lgtd_lifx_packet_info *b) +{ + return a->type - b->type; +} + +union lgtd_lifx_target { + uint64_t tags; + const uint8_t *addr; //! site or device address +}; + +extern const union lgtd_lifx_target LGTD_LIFX_UNSPEC_TARGET; + +#if LGTD_SIZEOF_VOID_P == 8 +# define LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(x) (1UL << (x)) +#elif LGTD_SIZEOF_VOID_P == 4 +# define LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(x) (1ULL << (x)) +#endif + +// Kim Walisch (2012) +// http://chessprogramming.wikispaces.com/BitScan#DeBruijnMultiplation + +enum { LGTD_LIFX_DEBRUIJN_NUMBER = 0x03f79d71b4cb0a89 }; +extern const int LGTD_LIFX_DEBRUIJN_SEQUENCE[64]; + +static inline int +lgtd_lifx_wire_bitscan64_forward(uint64_t n) +{ + return n ? LGTD_LIFX_DEBRUIJN_SEQUENCE[ + ((n ^ (n - 1)) * LGTD_LIFX_DEBRUIJN_NUMBER) >> 58 + ] : -1; +} + +static inline int +lgtd_lifx_wire_next_tag_id(int current_tag_id, uint64_t tags) +{ + // A bitshift >= than the width of the type is undefined behavior in C: + if (current_tag_id < 63) { + tags &= ~0ULL << (current_tag_id + 1); + return lgtd_lifx_wire_bitscan64_forward(tags); + } + return -1; +} + +#define LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id_varname, tags) \ + for ((tag_id_varname) = lgtd_lifx_wire_next_tag_id(-1, (tags)); \ + (tag_id_varname) != -1; \ + (tag_id_varname) = lgtd_lifx_wire_next_tag_id((tag_id_varname), (tags))) + +enum lgtd_lifx_waveform_type lgtd_lifx_wire_waveform_string_id_to_type(const char *, int); + +const struct lgtd_lifx_packet_info *lgtd_lifx_wire_get_packet_info(enum lgtd_lifx_packet_type); + +void lgtd_lifx_wire_setup(void); + +bool lgtd_lifx_wire_handle_receive(evutil_socket_t, struct lgtd_lifx_gateway *); + +const struct lgtd_lifx_packet_info *lgtd_lifx_wire_setup_header(struct lgtd_lifx_packet_header *, + enum lgtd_lifx_target_type, + union lgtd_lifx_target, + const uint8_t *, + enum lgtd_lifx_packet_type); +void lgtd_lifx_wire_decode_header(struct lgtd_lifx_packet_header *); + + +void lgtd_lifx_wire_enosys_packet_handler(struct lgtd_lifx_gateway *, + const struct lgtd_lifx_packet_header *, + const void *); + +void lgtd_lifx_wire_decode_pan_gateway(struct lgtd_lifx_packet_pan_gateway *); +void lgtd_lifx_wire_encode_pan_gateway(struct lgtd_lifx_packet_pan_gateway *); +void lgtd_lifx_wire_decode_light_status(struct lgtd_lifx_packet_light_status *); +void lgtd_lifx_wire_encode_light_status(struct lgtd_lifx_packet_light_status *); +void lgtd_lifx_wire_decode_power_state(struct lgtd_lifx_packet_power_state *); + +void lgtd_lifx_wire_encode_light_color(struct lgtd_lifx_packet_light_color *); +void lgtd_lifx_wire_encode_waveform(struct lgtd_lifx_packet_waveform *); + +void lgtd_lifx_wire_encode_tags(struct lgtd_lifx_packet_tags *); +void lgtd_lifx_wire_decode_tags(struct lgtd_lifx_packet_tags *); +void lgtd_lifx_wire_encode_tag_labels(struct lgtd_lifx_packet_tag_labels *); +void lgtd_lifx_wire_decode_tag_labels(struct lgtd_lifx_packet_tag_labels *); + +void lgtd_lifx_wire_decode_ip_state(struct lgtd_lifx_packet_ip_state *); +void lgtd_lifx_wire_decode_ip_firmware_info(struct lgtd_lifx_packet_ip_firmware_info *); +void lgtd_lifx_wire_decode_product_info(struct lgtd_lifx_packet_product_info *); +void lgtd_lifx_wire_decode_runtime_info(struct lgtd_lifx_packet_runtime_info *); +void lgtd_lifx_wire_decode_ambient_light(struct lgtd_lifx_packet_ambient_light *); diff --git a/share/lightsc.sh b/share/lightsc.sh new file mode 100644 index 0000000..7632061 --- /dev/null +++ b/share/lightsc.sh @@ -0,0 +1,110 @@ +# Copyright (c) 2015, Louis Opter +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# NOTE: Keep in mind that arguments must be JSON, you will have to enclose +# tags and labels into double quotes '"likethis"'. Also keep in mind +# that the pipe is write-only you cannot read any result back. + +_lightsc_b64e() { + if type base64 >/dev/null 2>&1 ; then + base64 + elif type openssl >/dev/null 2>&1 ; then + openssl base64 + else + cat >/dev/null + echo null + fi +} + +_lightsc_gen_request_id() { + if type dd >/dev/null 2>&1 ; then + printf '"%s"' `dd if=/dev/urandom bs=8 count=1 2>&- | _lightsc_b64e` + else + echo null + fi +} + +_lightsc_jq() { + if type jq >/dev/null 2>&1 ; then + jq . + else + cat + fi +} + +lightsc_get_pipe() { + local pipe=${LIGHTSD_COMMAND_PIPE:-`lightsd --rundir`/pipe} + if [ ! -p $pipe ] ; then + echo >&2 "$pipe cannot be found, is lightsd running?" + exit 1 + fi + echo $pipe +} + +# Can be used to build batch request: +# +# tee `lightsc_get_pipe` <&2 "Usage: $0 METHOD PARAMS ..." + return 1 + fi + + local method=$1 ; shift + local params=$1 ; shift + for target in $* ; do + params=$params,$target + done + + cat <&2 "Usage: $0 METHOD PARAMS ..." + return 1 + fi + + lightsc_make_request $* | tee `lightsc_get_pipe` | _lightsc_jq +} + +# vim: set expandtab ft=sh sts=4 sw=4: diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..a1633c4 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,11 @@ +FUNCTION(ADD_CORE_LIBRARY LIBNAME) + ADD_LIBRARY(${LIBNAME} ${ARGN}) + TARGET_LINK_LIBRARIES(${LIBNAME} ${TIME_MONOTONIC_LIBRARY}) + TARGET_INCLUDE_DIRECTORIES( + ${LIBNAME} PUBLIC + ${LIGHTSD_SOURCE_DIR}/core/ + ${LIGHTSD_BINARY_DIR}/core/ + ) +ENDFUNCTION() + +ADD_ALL_SUBDIRECTORIES() diff --git a/tests/core/CMakeLists.txt b/tests/core/CMakeLists.txt new file mode 100644 index 0000000..82a32fc --- /dev/null +++ b/tests/core/CMakeLists.txt @@ -0,0 +1,12 @@ +INCLUDE_DIRECTORIES( + ${LIGHTSD_SOURCE_DIR} + ${LIGHTSD_SOURCE_DIR}/core/ + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../lifx + ${LIGHTSD_BINARY_DIR} + ${LIGHTSD_BINARY_DIR}/core/ + ${CMAKE_CURRENT_BINARY_DIR} + ${CMAKE_CURRENT_BINARY_DIR}/../lifx +) + +ADD_ALL_SUBDIRECTORIES() diff --git a/tests/core/client/CMakeLists.txt b/tests/core/client/CMakeLists.txt new file mode 100644 index 0000000..7120998 --- /dev/null +++ b/tests/core/client/CMakeLists.txt @@ -0,0 +1,24 @@ +INCLUDE_DIRECTORIES( + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} +) + +ADD_CORE_LIBRARY( + test_core_client STATIC + ${LIGHTSD_SOURCE_DIR}/core/jsmn.c + ${LIGHTSD_SOURCE_DIR}/core/stats.c + ${LIGHTSD_SOURCE_DIR}/core/utils.c + ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c + ${LIGHTSD_SOURCE_DIR}/lifx/tagging.c + ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c + ${CMAKE_CURRENT_SOURCE_DIR}/../tests_utils.c +) + +FUNCTION(ADD_CLIENT_TEST TEST_SOURCE) + ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_core_client) +ENDFUNCTION() + +FILE(GLOB TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test_*.c") +FOREACH(TEST ${TESTS}) + ADD_CLIENT_TEST(${TEST}) +ENDFOREACH() diff --git a/tests/core/client/test_client_read_callback.c b/tests/core/client/test_client_read_callback.c new file mode 100644 index 0000000..28c332e --- /dev/null +++ b/tests/core/client/test_client_read_callback.c @@ -0,0 +1,167 @@ +#include "client.c" + +#include "lifx/wire_proto.h" + +#include "mock_daemon.h" +#define MOCKED_EVBUFFER_PULLUP +#define MOCKED_EVBUFFER_DRAIN +#define MOCKED_EVBUFFER_GET_CONTIGUOUS_SPACE +#define MOCKED_BUFFEREVENT_GET_INPUT +#include "mock_event2.h" +#include "mock_gateway.h" +#define MOCKED_JSONRPC_DISPATCH_REQUEST +#include "mock_jsonrpc.h" +#include "mock_log.h" +#include "mock_router.h" +#include "mock_timer.h" + +#include "tests_utils.h" +#include "tests_client_utils.h" + +static unsigned char request[] = ("{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"get_light_state\"," + "\"params\": [\"*\"]," + "\"id\": 42" +"}"); + +static int +get_nbytes_read(int call_count) +{ + switch (call_count) { + case 0: + return sizeof(request) - 1; // we don't return the '\0' + default: + return 0; + } +} + +struct evbuffer * +bufferevent_get_input(struct bufferevent *bufev) +{ + (void)bufev; + + return FAKE_BUFFEREVENT_INPUT_BUF; +} + +static int evbuffer_get_contiguous_space_call_count = 0; + +size_t +evbuffer_get_contiguous_space(const struct evbuffer *buf) +{ + if (buf != FAKE_BUFFEREVENT_INPUT_BUF) { + errx( + 1, "evbuffer_get_contiguous_space got buf %p (expected %p)", + buf, FAKE_BUFFEREVENT_INPUT_BUF + ); + } + + return get_nbytes_read(evbuffer_get_contiguous_space_call_count++); +} + +static int jsonrpc_dispatch_request_call_count = 0; + +void +lgtd_jsonrpc_dispatch_request(struct lgtd_client *client, int parsed) +{ + (void)client; + (void)parsed; + + if (!parsed) { + errx(1, "number of parsed json tokens not passed in"); + } + + if (memcmp(client->json, request, sizeof(request))) { + errx(1, "got unexpected json"); + } + + if (jsonrpc_dispatch_request_call_count++) { + errx(1, "jsonrpc_dispatch_request should have been called once"); + } +} + +static int evbuffer_drain_call_count = 0; + +int +evbuffer_drain(struct evbuffer *buf, size_t len) +{ + if (buf != FAKE_BUFFEREVENT_INPUT_BUF) { + errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2); + } + + switch (evbuffer_drain_call_count++) { + case 0: + if (len != sizeof(request) - 1) { + errx( + 1, "trying to drain %ju bytes (expected %ju)", + (uintmax_t)len, (uintmax_t)sizeof(request) - 1 + ); + } + break; + default: + errx(1, "evbuffer_drain should have been called once"); + break; + } + + return 0; +} + +static int evbuffer_pullup_call_count = 0; + +unsigned char * +evbuffer_pullup(struct evbuffer *buf, ev_ssize_t size) +{ + if (buf != FAKE_BUFFEREVENT_INPUT_BUF) { + errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2); + } + + int offset; + switch (evbuffer_pullup_call_count++) { + case 0: + if (size != sizeof(request) - 1) { + errx( + 1, "got unexpected size %jd in pullup (expected %ju)", + (intmax_t)size, (uintmax_t)sizeof(request) - 1 + ); + } + offset = 0; + break; + case 1: + if (size != -1) { + errx( + 1, "got unexpected size %jd in pullup (expected -1)", + (intmax_t)size + ); + } + offset = sizeof(request) - 1; + break; + default: + errx(1, "evbuffer_pullup should have been called twice"); + } + + return &request[offset]; +} + +int +main(void) +{ + struct lgtd_client *client; + client = lgtd_tests_insert_mock_client(FAKE_BUFFEREVENT); + + lgtd_client_read_callback(FAKE_BUFFEREVENT, client); + + if (!evbuffer_get_contiguous_space_call_count) { + errx(1, "evbuffer_get_contiguous_space not called"); + } + if (!jsonrpc_dispatch_request_call_count) { + errx(1, "jsonrpc_dispatch_request not called"); + } + if (!evbuffer_drain_call_count) { + errx(1, "evbuffer_drain not called"); + } + if (!evbuffer_pullup_call_count) { + errx(1, "evbuffer_pullup not called"); + } + + return 0; +} diff --git a/tests/core/client/test_client_read_callback_extra_data.c b/tests/core/client/test_client_read_callback_extra_data.c new file mode 100644 index 0000000..e1ce8d1 --- /dev/null +++ b/tests/core/client/test_client_read_callback_extra_data.c @@ -0,0 +1,227 @@ +#include "client.c" + +#include "lifx/wire_proto.h" + +#include "mock_daemon.h" +#define MOCKED_EVBUFFER_PULLUP +#define MOCKED_EVBUFFER_DRAIN +#define MOCKED_EVBUFFER_GET_CONTIGUOUS_SPACE +#define MOCKED_BUFFEREVENT_GET_INPUT +#include "mock_event2.h" +#include "mock_gateway.h" +#define MOCKED_JSONRPC_DISPATCH_REQUEST +#include "mock_jsonrpc.h" +#include "mock_log.h" +#include "mock_router.h" +#include "mock_timer.h" + +#include "tests_utils.h" +#include "tests_client_utils.h" + +static unsigned char request[] = ( + "lollllllllll \n\n\n" + "{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"get_light_state\"," + "\"params\": [\"*\"]," + "\"id\": 42" + "}HALP HALP \\_o< O)))" +); + +enum { GARBAGE_BEFORE_REQUEST_LEN = sizeof("lollllllllll \n\n\n") - 1 }; +enum { + REQUEST_LEN = sizeof("{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"get_light_state\"," + "\"params\": [\"*\"]," + "\"id\": 42" + "}") - 1 +}; +enum { GARBAGE_AFTER_REQUEST_LEN = sizeof("HALP HALP \\_o< O)))") - 1 }; + +static int +get_nbytes_read(int call_count) +{ + switch (call_count) { + case 0: + return GARBAGE_BEFORE_REQUEST_LEN; + case 1: + return REQUEST_LEN; + case 2: + return GARBAGE_AFTER_REQUEST_LEN; + default: + return 0; + } +} + +struct evbuffer * +bufferevent_get_input(struct bufferevent *bufev) +{ + (void)bufev; + + return FAKE_BUFFEREVENT_INPUT_BUF; +} + +static int evbuffer_get_contiguous_space_call_count = 0; + +size_t +evbuffer_get_contiguous_space(const struct evbuffer *buf) +{ + if (buf != FAKE_BUFFEREVENT_INPUT_BUF) { + errx( + 1, "evbuffer_get_contiguous_space got buf %p (expected %p)", + buf, FAKE_BUFFEREVENT_INPUT_BUF + ); + } + + return get_nbytes_read(evbuffer_get_contiguous_space_call_count++); +} + +static int jsonrpc_dispatch_request_call_count = 0; + +void +lgtd_jsonrpc_dispatch_request(struct lgtd_client *client, int parsed) +{ + (void)client; + (void)parsed; + + if (!parsed) { + errx(1, "number of parsed json tokens not passed in"); + } + + bool diff = memcmp( + client->json, &request[GARBAGE_BEFORE_REQUEST_LEN], REQUEST_LEN + ); + if (diff) { + errx(1, "got unexpected json"); + } + + if (jsonrpc_dispatch_request_call_count++) { + errx(1, "jsonrpc_dispatch_request should have been called once"); + } +} + +static int evbuffer_drain_call_count = 0; + +int +evbuffer_drain(struct evbuffer *buf, size_t len) +{ + if (buf != FAKE_BUFFEREVENT_INPUT_BUF) { + errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2); + } + + switch (evbuffer_drain_call_count++) { + case 0: + if (len != GARBAGE_BEFORE_REQUEST_LEN) { + errx( + 1, "trying to drain %ju bytes (expected %ju)", + (uintmax_t)len, (uintmax_t)GARBAGE_BEFORE_REQUEST_LEN + ); + } + break; + case 1: + if (len != REQUEST_LEN) { + errx( + 1, "trying to drain %ju bytes (expected %ju)", + (uintmax_t)len, (uintmax_t)REQUEST_LEN + ); + } + break; + case 2: + if (len != GARBAGE_AFTER_REQUEST_LEN) { + errx( + 1, "trying to drain %ju bytes (expected %ju)", + (uintmax_t)len, (uintmax_t)REQUEST_LEN + ); + } + break; + default: + errx( + 1, "evbuffer_drain shouldn't have been called %d times", + evbuffer_drain_call_count + ); + } + + return 0; +} + +static int evbuffer_pullup_call_count = 0; + +unsigned char * +evbuffer_pullup(struct evbuffer *buf, ev_ssize_t size) +{ + if (buf != FAKE_BUFFEREVENT_INPUT_BUF) { + errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2); + } + + int offset; + switch (evbuffer_pullup_call_count++) { + case 0: + if (size != GARBAGE_BEFORE_REQUEST_LEN) { + errx( + 1, "trying to pullup %ju bytes (expected %ju)", + (intmax_t)size, (uintmax_t)GARBAGE_BEFORE_REQUEST_LEN + ); + } + offset = 0; + break; + case 1: + if (size != -1) { + errx( + 1, "got unexpected size %jd in pullup (expected -1)", + (intmax_t)size + ); + } + offset = GARBAGE_BEFORE_REQUEST_LEN; + break; + case 2: + if (size != -1) { + errx( + 1, "got unexpected size %jd in pullup (expected -1)", + (intmax_t)size + ); + } + offset = GARBAGE_BEFORE_REQUEST_LEN + REQUEST_LEN; + break; + case 3: + if (size != -1) { + errx( + 1, "got unexpected size %jd in pullup (expected -1)", + (intmax_t)size + ); + } + offset = sizeof(request) - 1; + break; + default: + errx( + 1, "evbuffer_pullup shouldn't have been called %d times", + evbuffer_pullup_call_count + ); + } + + return &request[offset]; +} + +int +main(void) +{ + struct lgtd_client *client; + client = lgtd_tests_insert_mock_client(FAKE_BUFFEREVENT); + + lgtd_client_read_callback(FAKE_BUFFEREVENT, client); + + if (!evbuffer_get_contiguous_space_call_count) { + errx(1, "evbuffer_get_contiguous_space not called"); + } + if (!jsonrpc_dispatch_request_call_count) { + errx(1, "jsonrpc_dispatch_request not called"); + } + if (!evbuffer_drain_call_count) { + errx(1, "evbuffer_drain not called"); + } + if (!evbuffer_pullup_call_count) { + errx(1, "evbuffer_pullup not called"); + } + + return 0; +} diff --git a/tests/core/client/test_client_read_callback_multiple_requests.c b/tests/core/client/test_client_read_callback_multiple_requests.c new file mode 100644 index 0000000..a0b7954 --- /dev/null +++ b/tests/core/client/test_client_read_callback_multiple_requests.c @@ -0,0 +1,212 @@ +#include "client.c" + +#include "lifx/wire_proto.h" + +#include "mock_daemon.h" +#define MOCKED_EVBUFFER_PULLUP +#define MOCKED_EVBUFFER_DRAIN +#define MOCKED_EVBUFFER_GET_CONTIGUOUS_SPACE +#define MOCKED_BUFFEREVENT_GET_INPUT +#include "mock_event2.h" +#include "mock_gateway.h" +#define MOCKED_JSONRPC_DISPATCH_REQUEST +#include "mock_jsonrpc.h" +#include "mock_log.h" +#include "mock_router.h" +#include "mock_timer.h" + +#include "tests_utils.h" +#include "tests_client_utils.h" + +#define REQUEST_1 "{" \ + "\"jsonrpc\": \"2.0\"," \ + "\"method\": \"get_light_state\"," \ + "\"params\": [\"*\"]," \ + "\"id\": 42" \ +"}" +#define REQUEST_1_LEN (sizeof(REQUEST_1) - 1) + +#define REQUEST_2 "{" \ + "\"jsonrpc\": \"2.0\"," \ + "\"method\": \"power_on\"," \ + "\"params\": [\"*\"]," \ + "\"id\": 43" \ +"}" +#define REQUEST_2_LEN (sizeof(REQUEST_2) - 1) + +static unsigned char request[] = ( + REQUEST_1 + REQUEST_2 +); + +#define REQUEST_LEN (REQUEST_1_LEN + REQUEST_2_LEN) + +static int +get_nbytes_read(int call_count) +{ + switch (call_count) { + case 0: + return REQUEST_LEN; + default: + return 0; + } +} + +struct evbuffer * +bufferevent_get_input(struct bufferevent *bufev) +{ + (void)bufev; + + return FAKE_BUFFEREVENT_INPUT_BUF; +} + +static int evbuffer_get_contiguous_space_call_count = 0; + +size_t +evbuffer_get_contiguous_space(const struct evbuffer *buf) +{ + if (buf != FAKE_BUFFEREVENT_INPUT_BUF) { + errx( + 1, "evbuffer_get_contiguous_space got buf %p (expected %p)", + buf, FAKE_BUFFEREVENT_INPUT_BUF + ); + } + + return get_nbytes_read(evbuffer_get_contiguous_space_call_count++); +} + +static int jsonrpc_dispatch_request_call_count = 0; + +void +lgtd_jsonrpc_dispatch_request(struct lgtd_client *client, int parsed) +{ + (void)client; + (void)parsed; + + if (!parsed) { + errx(1, "number of parsed json tokens not passed in"); + } + + bool diff; + switch (jsonrpc_dispatch_request_call_count++) { + case 0: + diff = memcmp(client->json, REQUEST_1, REQUEST_1_LEN); + if (diff) { + errx( + 1, "got unexpected json %s (expected) %s", + client->json, REQUEST_1 + ); + } + break; + case 1: + diff = memcmp(client->json, REQUEST_2, REQUEST_2_LEN); + if (diff) { + errx( + 1, "got unexpected json %s (expected) %s", + client->json, REQUEST_2 + ); + } + break; + default: + errx(1, "jsonrpc_dispatch_request should have been called twice"); + } +} + +static int evbuffer_drain_call_count = 0; + +int +evbuffer_drain(struct evbuffer *buf, size_t len) +{ + if (buf != FAKE_BUFFEREVENT_INPUT_BUF) { + errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2); + } + + switch (evbuffer_drain_call_count++) { + case 0: + if (len != REQUEST_1_LEN) { + errx( + 1, "trying to drain %ju bytes (expected %ju)", + (uintmax_t)len, (uintmax_t)REQUEST_1_LEN + ); + } + break; + case 1: + if (len != REQUEST_2_LEN) { + errx( + 1, "trying to drain %ju bytes (expected %ju)", + (uintmax_t)len, (uintmax_t)REQUEST_2_LEN + ); + } + break; + default: + errx( + 1, "evbuffer_drain shouldn't have been called %d times", + evbuffer_drain_call_count + ); + } + + return 0; +} + +static int evbuffer_pullup_call_count = 0; + +unsigned char * +evbuffer_pullup(struct evbuffer *buf, ev_ssize_t size) +{ + if (buf != FAKE_BUFFEREVENT_INPUT_BUF) { + errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2); + } + + int offset; + switch (evbuffer_pullup_call_count++) { + case 0: + if (size != REQUEST_LEN) { + errx( + 1, "trying to pullup %ju bytes (expected %ju)", + (intmax_t)size, (uintmax_t)REQUEST_LEN + ); + } + offset = 0; + break; + case 1: + if (size != -1) { + errx( + 1, "got unexpected size %jd in pullup (expected -1)", + (intmax_t)size + ); + } + offset = REQUEST_LEN; + break; + default: + errx( + 1, "evbuffer_pullup shouldn't have been called %d times", + evbuffer_pullup_call_count + ); + } + + return &request[offset]; +} + +int +main(void) +{ + struct lgtd_client *client; + client = lgtd_tests_insert_mock_client(FAKE_BUFFEREVENT); + + lgtd_client_read_callback(FAKE_BUFFEREVENT, client); + + if (!evbuffer_get_contiguous_space_call_count) { + errx(1, "evbuffer_get_contiguous_space not called"); + } + if (!jsonrpc_dispatch_request_call_count) { + errx(1, "jsonrpc_dispatch_request not called"); + } + if (!evbuffer_drain_call_count) { + errx(1, "evbuffer_drain not called"); + } + if (!evbuffer_pullup_call_count) { + errx(1, "evbuffer_pullup not called"); + } + + return 0; +} diff --git a/tests/core/client/test_client_read_callback_part_too_large.c b/tests/core/client/test_client_read_callback_part_too_large.c new file mode 100644 index 0000000..87e279e --- /dev/null +++ b/tests/core/client/test_client_read_callback_part_too_large.c @@ -0,0 +1,207 @@ +#include "client.c" + +#include "lifx/wire_proto.h" + +#include "mock_daemon.h" +#define MOCKED_EVBUFFER_PULLUP +#define MOCKED_EVBUFFER_DRAIN +#define MOCKED_EVBUFFER_GET_CONTIGUOUS_SPACE +#define MOCKED_EVBUFFER_GET_LENGTH +#define MOCKED_BUFFEREVENT_GET_INPUT +#include "mock_event2.h" +#include "mock_gateway.h" +#define MOCKED_JSONRPC_DISPATCH_REQUEST +#include "mock_jsonrpc.h" +#include "mock_log.h" +#include "mock_router.h" +#include "mock_timer.h" + +#include "tests_utils.h" +#include "tests_client_utils.h" + +static unsigned char request[] = ("{" + "\"id\": \"verylooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongid\"," + "\"jsonrpc\": \"2.0\"," + "\"method\": \"get_light_state\"," + "\"params\": [\"*\"]" +"}"); + +#define REQUEST_LEN (sizeof(request) - 1) +#define PART_1_LEN ((LGTD_CLIENT_MAX_REQUEST_BUF_SIZE) + 1) +#define PART_2_LEN ((REQUEST_LEN) - (PART_1_LEN)) + +static int +get_nbytes_read(int call_count) +{ + switch (call_count) { + case 0: + return PART_1_LEN; + case 1: + return PART_2_LEN; + default: + return 0; + } +} + +static int evbuffer_get_length_call_count = 0; + +size_t +evbuffer_get_length(const struct evbuffer *buf) +{ + if (buf != FAKE_BUFFEREVENT_INPUT_BUF) { + errx( + 1, "evbuffer_get_length got buf %p (expected %p)", + buf, FAKE_BUFFEREVENT_INPUT_BUF + ); + } + + if (evbuffer_get_length_call_count) { + errx(1, "evbuffer_get_length should have been called once"); + } + + return get_nbytes_read(evbuffer_get_length_call_count++); +} + +struct evbuffer * +bufferevent_get_input(struct bufferevent *bufev) +{ + (void)bufev; + + return FAKE_BUFFEREVENT_INPUT_BUF; +} + +static int evbuffer_get_contiguous_space_call_count = 0; + +size_t +evbuffer_get_contiguous_space(const struct evbuffer *buf) +{ + if (buf != FAKE_BUFFEREVENT_INPUT_BUF) { + errx( + 1, "evbuffer_get_contiguous_space got buf %p (expected %p)", + buf, FAKE_BUFFEREVENT_INPUT_BUF + ); + } + + return get_nbytes_read(evbuffer_get_contiguous_space_call_count++); +} + +void +lgtd_jsonrpc_dispatch_request(struct lgtd_client *client, int parsed) +{ + (void)client; + (void)parsed; + + errx(1, "No request should have been dispatched"); +} + +static int evbuffer_drain_call_count = 0; + +int +evbuffer_drain(struct evbuffer *buf, size_t len) +{ + if (buf != FAKE_BUFFEREVENT_INPUT_BUF) { + errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2); + } + + switch (evbuffer_drain_call_count++) { + case 0: + if (len != PART_1_LEN) { + errx( + 1, "trying to drain %ju bytes (expected %ju)", + (uintmax_t)len, (uintmax_t)PART_1_LEN + ); + } + break; + case 1: + if (len != PART_2_LEN) { + errx( + 1, "trying to drain %ju bytes (expected %ju)", + (uintmax_t)len, (uintmax_t)PART_2_LEN + ); + } + break; + default: + errx( + 1, "evbuffer_drain shouldn't have been called %d times", + evbuffer_drain_call_count + ); + } + + return 0; +} + +static int evbuffer_pullup_call_count = 0; + +unsigned char * +evbuffer_pullup(struct evbuffer *buf, ev_ssize_t size) +{ + if (buf != FAKE_BUFFEREVENT_INPUT_BUF) { + errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2); + } + + int offset; + switch (evbuffer_pullup_call_count++) { + case 0: + if (size != PART_1_LEN) { + errx( + 1, "trying to pullup %ju bytes (expected %ju)", + (intmax_t)size, (uintmax_t)PART_1_LEN + ); + } + offset = 0; + break; + case 1: + if (size != -1) { + errx( + 1, "trying to pullup %ju bytes (expected %ju)", + (intmax_t)size, (uintmax_t)-1 + ); + } + offset = PART_1_LEN; + break; + case 2: + if (size != -1) { + errx(1, "trying to pullup %ju bytes (expected -1)", (intmax_t)size); + } + offset = REQUEST_LEN; + break; + default: + errx( + 1, "evbuffer_pullup shouldn't have been called %d times", + evbuffer_pullup_call_count + ); + } + + return &request[offset]; +} + +int +main(void) +{ + if (LGTD_CLIENT_MAX_REQUEST_BUF_SIZE >= REQUEST_LEN) { + errx( + 1, "Please adjust this test to copy with the new value of " + "LGTD_CLIENT_MAX_REQUEST_BUF_SIZE" + ); + } + + struct lgtd_client *client; + client = lgtd_tests_insert_mock_client(FAKE_BUFFEREVENT); + + lgtd_client_read_callback(FAKE_BUFFEREVENT, client); + + if (!evbuffer_get_contiguous_space_call_count) { + errx(1, "evbuffer_get_contiguous_space not called"); + } + if (!evbuffer_drain_call_count) { + errx(1, "evbuffer_drain not called"); + } + if (!evbuffer_pullup_call_count) { + errx(1, "evbuffer_pullup not called"); + } + if (!evbuffer_get_length_call_count) { + errx(1, "evbuffer_get_length not called"); + } + + return 0; +} diff --git a/tests/core/client/test_client_read_callback_yield_on_eagain.c b/tests/core/client/test_client_read_callback_yield_on_eagain.c new file mode 100644 index 0000000..d8be9bc --- /dev/null +++ b/tests/core/client/test_client_read_callback_yield_on_eagain.c @@ -0,0 +1,212 @@ +#include "client.c" + +#include "lifx/wire_proto.h" + +#include "mock_daemon.h" +#define MOCKED_EVBUFFER_PULLUP +#define MOCKED_EVBUFFER_DRAIN +#define MOCKED_EVBUFFER_GET_CONTIGUOUS_SPACE +#define MOCKED_EVBUFFER_GET_LENGTH +#define MOCKED_BUFFEREVENT_GET_INPUT +#include "mock_event2.h" +#include "mock_gateway.h" +#define MOCKED_JSONRPC_DISPATCH_REQUEST +#include "mock_jsonrpc.h" +#include "mock_log.h" +#include "mock_router.h" +#include "mock_timer.h" + +#include "tests_utils.h" +#include "tests_client_utils.h" + +static unsigned char request[] = ("{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"get_light_state\"," + "\"params\": [\"*\"]," + "\"id\": 42" +"}"); + +#define REQUEST_LEN (sizeof(request) - 1) +#define PART_1_LEN 16 +#define PART_2_LEN (REQUEST_LEN - PART_1_LEN) + +static int +get_nbytes_read(int call_count) +{ + switch (call_count) { + case 0: + return PART_1_LEN; + case 1: + return REQUEST_LEN; + default: + return 0; + } +} + +static int evbuffer_get_length_call_count = 0; + +size_t +evbuffer_get_length(const struct evbuffer *buf) +{ + if (buf != FAKE_BUFFEREVENT_INPUT_BUF) { + errx( + 1, "evbuffer_get_length got buf %p (expected %p)", + buf, FAKE_BUFFEREVENT_INPUT_BUF + ); + } + + if (evbuffer_get_length_call_count++ == 2) { + errx(1, "evbuffer_get_length should have been called twice"); + } + + return PART_1_LEN; +} + +struct evbuffer * +bufferevent_get_input(struct bufferevent *bufev) +{ + (void)bufev; + + return FAKE_BUFFEREVENT_INPUT_BUF; +} + +static int evbuffer_get_contiguous_space_call_count = 0; + +size_t +evbuffer_get_contiguous_space(const struct evbuffer *buf) +{ + if (buf != FAKE_BUFFEREVENT_INPUT_BUF) { + errx( + 1, "evbuffer_get_contiguous_space got buf %p (expected %p)", + buf, FAKE_BUFFEREVENT_INPUT_BUF + ); + } + + return get_nbytes_read(evbuffer_get_contiguous_space_call_count++); +} + +static int jsonrpc_dispatch_request_call_count = 0; + +void +lgtd_jsonrpc_dispatch_request(struct lgtd_client *client, int parsed) +{ + (void)client; + (void)parsed; + + if (!parsed) { + errx(1, "number of parsed json tokens not passed in"); + } + + switch (jsonrpc_dispatch_request_call_count++) { + case 0: + if (memcmp(client->json, request, REQUEST_LEN)) { + errx( + 1, "got unexpected json %s (expected) %s", client->json, request + ); + } + break; + default: + errx(1, "jsonrpc_dispatch_request should have been called once"); + } +} + +static int evbuffer_drain_call_count = 0; + +int +evbuffer_drain(struct evbuffer *buf, size_t len) +{ + if (buf != FAKE_BUFFEREVENT_INPUT_BUF) { + errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2); + } + + switch (evbuffer_drain_call_count++) { + case 0: + if (len != REQUEST_LEN) { + errx( + 1, "trying to drain %ju bytes (expected %ju)", + (uintmax_t)len, (uintmax_t)REQUEST_LEN + ); + } + break; + default: + errx( + 1, "evbuffer_drain shouldn't have been called %d times", + evbuffer_drain_call_count + ); + } + + return 0; +} + +static int evbuffer_pullup_call_count = 0; + +unsigned char * +evbuffer_pullup(struct evbuffer *buf, ev_ssize_t size) +{ + if (buf != FAKE_BUFFEREVENT_INPUT_BUF) { + errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2); + } + + int offset; + switch (evbuffer_pullup_call_count++) { + case 0: + if (size != PART_1_LEN) { + errx( + 1, "trying to pullup %ju bytes (expected %ju)", + (intmax_t)size, (uintmax_t)PART_1_LEN + ); + } + offset = 0; + break; + case 1: + if (size != REQUEST_LEN) { + errx( + 1, "trying to pullup %ju bytes (expected %ju)", + (intmax_t)size, (uintmax_t)REQUEST_LEN + ); + } + offset = 0; + break; + case 2: + if (size != -1) { + errx(1, "trying to pullup %ju bytes (expected -1)", (intmax_t)size); + } + offset = REQUEST_LEN; + break; + default: + errx( + 1, "evbuffer_pullup shouldn't have been called %d times", + evbuffer_pullup_call_count + ); + } + + return &request[offset]; +} + +int +main(void) +{ + struct lgtd_client *client; + client = lgtd_tests_insert_mock_client(FAKE_BUFFEREVENT); + + lgtd_client_read_callback(FAKE_BUFFEREVENT, client); + lgtd_client_read_callback(FAKE_BUFFEREVENT, client); + + if (!evbuffer_get_contiguous_space_call_count) { + errx(1, "evbuffer_get_contiguous_space not called"); + } + if (!jsonrpc_dispatch_request_call_count) { + errx(1, "jsonrpc_dispatch_request not called"); + } + if (!evbuffer_drain_call_count) { + errx(1, "evbuffer_drain not called"); + } + if (!evbuffer_pullup_call_count) { + errx(1, "evbuffer_pullup not called"); + } + if (!evbuffer_get_length_call_count) { + errx(1, "evbuffer_get_length not called"); + } + + return 0; +} diff --git a/tests/core/client/tests_client_utils.h b/tests/core/client/tests_client_utils.h new file mode 100644 index 0000000..1c859a3 --- /dev/null +++ b/tests/core/client/tests_client_utils.h @@ -0,0 +1,4 @@ +#pragma once + +#define FAKE_BUFFEREVENT (void *)0xfeed +#define FAKE_BUFFEREVENT_INPUT_BUF (void *)3412 diff --git a/tests/core/daemon/CMakeLists.txt b/tests/core/daemon/CMakeLists.txt new file mode 100644 index 0000000..7ecd6a8 --- /dev/null +++ b/tests/core/daemon/CMakeLists.txt @@ -0,0 +1,24 @@ +INCLUDE_DIRECTORIES( + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} +) + +ADD_CORE_LIBRARY( + test_core_daemon STATIC + ${LIGHTSD_SOURCE_DIR}/core/setproctitle.c + ${LIGHTSD_SOURCE_DIR}/core/stats.c + ${LIGHTSD_SOURCE_DIR}/core/utils.c + ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c + ${LIGHTSD_SOURCE_DIR}/lifx/tagging.c + ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c + ${CMAKE_CURRENT_SOURCE_DIR}/../tests_utils.c +) + +FUNCTION(ADD_DAEMON_TEST TEST_SOURCE) + ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_core_daemon) +ENDFUNCTION() + +FILE(GLOB TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test_*.c") +FOREACH(TEST ${TESTS}) + ADD_DAEMON_TEST(${TEST}) +ENDFOREACH() diff --git a/tests/core/daemon/mock_pipe.h b/tests/core/daemon/mock_pipe.h new file mode 100644 index 0000000..52f14ea --- /dev/null +++ b/tests/core/daemon/mock_pipe.h @@ -0,0 +1,4 @@ +#pragma once + +struct lgtd_command_pipe_list lgtd_command_pipes = + SLIST_HEAD_INITIALIZER(&lgtd_command_pipes); diff --git a/tests/core/daemon/test_daemon_randuint32.c b/tests/core/daemon/test_daemon_randuint32.c new file mode 100644 index 0000000..49c57ab --- /dev/null +++ b/tests/core/daemon/test_daemon_randuint32.c @@ -0,0 +1,94 @@ +#include + +int mock_open(const char *, int, ...); +ssize_t mock_read(int, void *, size_t); +int mock_close(int); + +#define open(fp, flags, ...) mock_open(fp, flags, ##__VA_ARGS__) +#define read(fd, buf, sz) mock_read(fd, buf, sz) +#define close(fd) mock_close(fd) + +#include "daemon.c" + +#include "mock_gateway.h" +#include "mock_pipe.h" +#include "mock_router.h" +#include "mock_log.h" +#include "mock_timer.h" + +static const uint32_t MOCK_RANDOM_NUMBER = 0x72616e64; + +int mock_open_call_count = 0; + +int +mock_open(const char *fp, int flags, ...) +{ + mock_open_call_count++; + + if (strcmp(fp, "/dev/urandom")) { + errx(1, "got fp %s (expected /dev/urandom)", fp); + } + + if (flags != O_RDONLY) { + errx(1, "got flags %#x (expected %#x)", flags, O_RDONLY); + } + + return 42; +} + +int mock_read_call_count = 0; + +ssize_t +mock_read(int fd, void *buf, size_t nbytes) +{ + mock_read_call_count++; + + if (fd != 42) { + errx(1, "got fd %d (expected 42)", fd); + } + + if (!buf) { + errx(1, "missing buf"); + } + + if (nbytes != 4) { + errx(1, "got nbytes %ju (expected 4)", (uintmax_t)nbytes); + } + + memcpy(buf, &MOCK_RANDOM_NUMBER, sizeof(MOCK_RANDOM_NUMBER)); + + return nbytes; +} + +int mock_close_call_count = 0; + +int +mock_close(int fd) +{ + mock_close_call_count++; + + if (fd != 42) { + errx(1, "got fd %d (expected 42)", fd); + } + + return 0; +} + +int +main(void) +{ + if (lgtd_daemon_randuint32() != MOCK_RANDOM_NUMBER) { + errx(1, "got unexpected value from randuint32"); + } + if (mock_open_call_count != 1) { + errx(1, "open wasn't once"); + } + if (mock_read_call_count != 1) { + errx(1, "read wasn't once"); + } + if (mock_close_call_count != 1) { + errx(1, "close wasn't once"); + } + + return 0; +} diff --git a/tests/core/daemon/test_daemon_update_proctitle.c b/tests/core/daemon/test_daemon_update_proctitle.c new file mode 100644 index 0000000..b6c1ddf --- /dev/null +++ b/tests/core/daemon/test_daemon_update_proctitle.c @@ -0,0 +1,111 @@ +void mock_setproctitle(const char *fmt, ...) + __attribute__((format(printf, 1, 2))); + +#undef LGTD_HAVE_PROCTITLE +#define LGTD_HAVE_PROCTITLE 1 +#define setproctitle mock_setproctitle +#include "daemon.c" + +#include "mock_gateway.h" +#include "mock_pipe.h" +#include "mock_router.h" +#include "mock_log.h" +#include "mock_timer.h" + +#include "tests_utils.h" + +const char *expected = ""; +int setproctitle_call_count = 0; + +void +mock_setproctitle(const char *fmt, ...) +{ + if (strcmp(fmt, "%s")) { + errx(1, "unexepected format %s (expected %%s)", fmt); + } + + va_list ap; + va_start(ap, fmt); + const char *title = va_arg(ap, const char *); + va_end(ap); + + if (strcmp(title, expected)) { + errx(1, "unexpected title: %s (expected %s)", title, expected); + } + + setproctitle_call_count++; +} + +int +main(void) +{ + lgtd_daemon_proctitle_initialized = true; + + expected = "bulbs(found=0, on=0); clients(connected=0)"; + lgtd_daemon_update_proctitle(); + if (setproctitle_call_count != 1) { + errx(1, "setproctitle should have been called"); + } + + expected = ( + "lifx_gateways(found=1); " + "bulbs(found=0, on=0); " + "clients(connected=0)" + ); + struct lgtd_lifx_gateway *gw_1 = lgtd_tests_insert_mock_gateway(1); + if (setproctitle_call_count != 2) { + errx(1, "setproctitle should have been called"); + } + + expected = ( + "lifx_gateways(found=1); " + "bulbs(found=1, on=0); " + "clients(connected=0)" + ); + lgtd_tests_insert_mock_bulb(gw_1, 2); + expected = ( + "lifx_gateways(found=1); " + "bulbs(found=2, on=0); " + "clients(connected=0)" + ); + lgtd_tests_insert_mock_bulb(gw_1, 3); + if (setproctitle_call_count != 4) { + errx(1, "setproctitle should have been called"); + } + + expected = ( + "listening_on([::ffff:127.0.0.1]:1234); " + "lifx_gateways(found=1); " + "bulbs(found=2, on=0); " + "clients(connected=0)" + ); + lgtd_tests_insert_mock_listener("127.0.0.1", 1234); + lgtd_daemon_update_proctitle(); + if (setproctitle_call_count != 5) { + errx(1, "setproctitle should have been called"); + } + + expected = ( + "listening_on([::ffff:127.0.0.1]:1234); " + "lifx_gateways(found=1); " + "bulbs(found=2, on=1); " + "clients(connected=0)" + ); + LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(bulbs_powered_on, 1); + if (setproctitle_call_count != 6) { + errx(1, "setproctitle should have been called"); + } + + expected = ( + "listening_on([::ffff:127.0.0.1]:1234); " + "lifx_gateways(found=1); " + "bulbs(found=2, on=1); " + "clients(connected=1)" + ); + LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(clients, 1); + if (setproctitle_call_count != 7) { + errx(1, "setproctitle should have been called"); + } + + return 0; +} diff --git a/tests/core/daemon/test_daemon_writepidfile.c b/tests/core/daemon/test_daemon_writepidfile.c new file mode 100644 index 0000000..55e6e51 --- /dev/null +++ b/tests/core/daemon/test_daemon_writepidfile.c @@ -0,0 +1,166 @@ +#include +#include +#include + +int mock_open(const char *, int, ...); +int mock_fchown(int, uid_t, gid_t); +int mock_write(int, const void *, size_t); +int mock_close(int); +pid_t mock_getpid(void); + +#define open(fp, flags, ...) mock_open(fp, flags, ##__VA_ARGS__) +#define fchown(fd, uid, gid) mock_fchown(fd, uid, gid) +#define write(fd, buf, sz) mock_write(fd, buf, sz) +#define close(fd) mock_close(fd) +#define getpid() mock_getpid() +#include "daemon.c" + +#include "mock_gateway.h" +#include "mock_pipe.h" +#include "mock_router.h" +#include "mock_log.h" +#include "mock_timer.h" + +#include "tests_utils.h" + +int mock_getpid_call_count = 0; + +pid_t +mock_getpid(void) +{ + mock_getpid_call_count++; + + return 1234; +} + +int mock_open_call_count = 0; + +int +mock_open(const char *fp, int flags, ...) +{ + mock_open_call_count++; + + va_list ap; + va_start(ap, flags); + mode_t mode = va_arg(ap, int); + va_end(ap); + + if (strcmp(fp, "/test.pid")) { + lgtd_errx(1, "got fp %s (expected /test.pid)", fp); + } + + int expected_flags = O_CREAT|O_WRONLY|O_TRUNC; + if (flags != expected_flags) { + lgtd_errx(1, "got flags %#x (expected %#x)", flags, expected_flags); + } + + mode_t expected_mode = S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH; + if (mode != expected_mode) { + lgtd_errx(1, "got mode %#x (expected %#x)", mode, expected_mode); + } + + return 42; +} + +int mock_fchown_call_count = 0; + +int +mock_fchown(int fd, uid_t uid, gid_t gid) +{ + mock_fchown_call_count++; + + if (fd != 42) { + lgtd_errx(1, "god fd %d (expected 42)", fd); + } + + if (uid != 1000) { + lgtd_errx(1, "god uid %d (expected 1000)", uid); + } + + if (gid != 500) { + lgtd_errx(1, "god gid %d (expected 500)", gid); + } + + return 0; +} + +int mock_write_call_count = 0; + +int +mock_write(int fd, const void *buf, size_t nbytes) +{ + mock_write_call_count++; + + if (fd != 42) { + lgtd_errx(1, "got fd %d (expected 42)", fd); + } + + if (!buf || strcmp((const char *)buf, "1234")) { + lgtd_errx(1, "writing pid %s (expected 1234)", (const char *)buf); + } + + if (nbytes != 4) { + lgtd_errx(1, "got nbytes %ju (expected 4)", (uintmax_t)nbytes); + } + + return 0; +} + +int mock_close_call_count = 0; + +int +mock_close(int fd) +{ + mock_close_call_count++; + + if (fd != 42) { + lgtd_errx(1, "got fd %d (expected 42)", fd); + } + + return 0; +} + +int +main(void) +{ + struct passwd user_info = { .pw_uid = 1000, .pw_gid = 500 }; + lgtd_user_info = &user_info; + struct group group_info = { .gr_gid = 500 }; + lgtd_group_info = &group_info; + + lgtd_daemon_write_pidfile("/test.pid"); + if (!mock_open_call_count) { + lgtd_errx(1, "open wasn't called"); + } + if (!mock_fchown_call_count) { + lgtd_errx(1, "fchown wasn't called"); + } + if (!mock_getpid_call_count) { + lgtd_errx(1, "getpid wasn't called"); + } + if (!mock_write_call_count) { + lgtd_errx(1, "write wasn't called"); + } + if (!mock_close_call_count) { + lgtd_errx(1, "close wasn't called"); + } + + lgtd_daemon_write_pidfile("/test.pid"); + if (mock_open_call_count != 2) { + lgtd_errx(1, "open wasn't called"); + } + if (mock_fchown_call_count != 2) { + lgtd_errx(1, "fchown wasn't called"); + } + if (mock_getpid_call_count != 2) { + lgtd_errx(1, "getpid wasn't called"); + } + if (mock_write_call_count != 2) { + lgtd_errx(1, "write wasn't called"); + } + if (mock_close_call_count != 2) { + lgtd_errx(1, "close wasn't called"); + } + + return 0; +} diff --git a/tests/core/jsonrpc/CMakeLists.txt b/tests/core/jsonrpc/CMakeLists.txt new file mode 100644 index 0000000..dcb9453 --- /dev/null +++ b/tests/core/jsonrpc/CMakeLists.txt @@ -0,0 +1,23 @@ +INCLUDE_DIRECTORIES( + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} +) + +ADD_CORE_LIBRARY( + test_core_jsonrpc STATIC + ${LIGHTSD_SOURCE_DIR}/core/jsmn.c + ${LIGHTSD_SOURCE_DIR}/core/stats.c + ${LIGHTSD_SOURCE_DIR}/core/utils.c + ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c + ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c + ${CMAKE_CURRENT_SOURCE_DIR}/../tests_utils.c +) + +FUNCTION(ADD_JSONRPC_TEST TEST_SOURCE) + ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_core_jsonrpc) +ENDFUNCTION() + +FILE(GLOB TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test_*.c") +FOREACH(TEST ${TESTS}) + ADD_JSONRPC_TEST(${TEST}) +ENDFOREACH() diff --git a/tests/core/jsonrpc/test_jsonrpc_batch.c b/tests/core/jsonrpc/test_jsonrpc_batch.c new file mode 100644 index 0000000..5b3079d --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_batch.c @@ -0,0 +1,94 @@ +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#define MOCKED_LGTD_PROTO_GET_LIGHT_STATE +#define MOCKED_LGTD_PROTO_POWER_ON +#include "mock_proto.h" +#include "mock_wire_proto.h" +#include "test_jsonrpc_utils.h" + +static int power_on_call_count = 0; + +void +lgtd_proto_power_on(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets) +{ + if (!client) { + errx(1, "missing client!"); + } + + if (strcmp(SLIST_FIRST(targets)->target, "*")) { + errx( + 1, "Invalid target [%s] (expected=[*])", + SLIST_FIRST(targets)->target + ); + } + + if (power_on_call_count++) { + errx(1, "proto_power_on should have been called once"); + } +} + +static int get_light_state_call_count = 0; + +void +lgtd_proto_get_light_state(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets) +{ + if (!client) { + errx(1, "missing client!"); + } + + if (strcmp(SLIST_FIRST(targets)->target, "*")) { + errx( + 1, "Invalid target [%s] (expected=[*])", + SLIST_FIRST(targets)->target + ); + } + + if (get_light_state_call_count++) { + errx(1, "proto_power_on should have been called once"); + } +} + +int +main(void) +{ + jsmntok_t tokens[32]; + const char json[] = ("[" + "{" + "\"method\": \"power_on\"," + "\"id\": \"004daf12-0561-4fbc-bfdb-bfe69cfbf4b5\"," + "\"params\": [\"*\"]," + "\"jsonrpc\": \"2.0\"" + "}," + "{" + "\"method\": \"get_light_state\"," + "\"id\": \"1f7a32c8-6741-4ee7-bec1-8431c7d514dc\"," + "\"params\": [\"*\"]," + "\"jsonrpc\": \"2.0\"" + "}" + "]"); + struct lgtd_client client = { .json = json, .jsmn_tokens = tokens }; + int parsed = parse_json( + tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json) + ); + + lgtd_jsonrpc_dispatch_request(&client, parsed); + + if (!power_on_call_count) { + errx(1, "power_on was never called"); + } + + if (!get_light_state_call_count) { + errx(1, "get_light_state was never called"); + } + + const char expected[] = "[,]"; // we mocked the functions + if (strcmp(expected, client_write_buf)) { + errx(1, "got client buf %s (expected %s)", client_write_buf, expected); + } + + return 0; +} diff --git a/tests/core/jsonrpc/test_jsonrpc_batch_notifications_only.c b/tests/core/jsonrpc/test_jsonrpc_batch_notifications_only.c new file mode 100644 index 0000000..1e713de --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_batch_notifications_only.c @@ -0,0 +1,92 @@ +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#define MOCKED_LGTD_PROTO_GET_LIGHT_STATE +#define MOCKED_LGTD_PROTO_POWER_ON +#include "mock_proto.h" +#include "mock_wire_proto.h" +#include "test_jsonrpc_utils.h" + +static int power_on_call_count = 0; + +void +lgtd_proto_power_on(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets) +{ + if (!client) { + errx(1, "missing client!"); + } + + if (strcmp(SLIST_FIRST(targets)->target, "*")) { + errx( + 1, "Invalid target [%s] (expected=[*])", + SLIST_FIRST(targets)->target + ); + } + + if (power_on_call_count++) { + errx(1, "proto_power_on should have been called once"); + } +} + +static int get_light_state_call_count = 0; + +void +lgtd_proto_get_light_state(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets) +{ + if (!client) { + errx(1, "missing client!"); + } + + if (strcmp(SLIST_FIRST(targets)->target, "*")) { + errx( + 1, "Invalid target [%s] (expected=[*])", + SLIST_FIRST(targets)->target + ); + } + + if (get_light_state_call_count++) { + errx(1, "proto_power_on should have been called once"); + } +} + +int +main(void) +{ + jsmntok_t tokens[32]; + const char json[] = ("[" + "{" + "\"method\": \"power_on\"," + "\"params\": [\"*\"]," + "\"jsonrpc\": \"2.0\"" + "}," + "{" + "\"method\": \"get_light_state\"," + "\"params\": [\"*\"]," + "\"jsonrpc\": \"2.0\"" + "}" + "]"); + struct lgtd_client client = { .json = json, .jsmn_tokens = tokens }; + int parsed = parse_json( + tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json) + ); + + lgtd_jsonrpc_dispatch_request(&client, parsed); + + if (!power_on_call_count) { + errx(1, "power_on was never called"); + } + + if (!get_light_state_call_count) { + errx(1, "get_light_state was never called"); + } + + const char expected[] = ""; + if (strcmp(expected, client_write_buf)) { + errx(1, "got client buf %s (expected %s)", client_write_buf, expected); + } + + return 0; +} diff --git a/tests/core/jsonrpc/test_jsonrpc_batch_one_invalid_request.c b/tests/core/jsonrpc/test_jsonrpc_batch_one_invalid_request.c new file mode 100644 index 0000000..358fd8c --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_batch_one_invalid_request.c @@ -0,0 +1,74 @@ +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#define MOCKED_LGTD_PROTO_POWER_ON +#include "mock_proto.h" +#include "mock_wire_proto.h" +#include "test_jsonrpc_utils.h" + +static int power_on_call_count = 0; + +void +lgtd_proto_power_on(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets) +{ + if (!client) { + errx(1, "missing client!"); + } + + if (strcmp(SLIST_FIRST(targets)->target, "*")) { + errx( + 1, "Invalid target [%s] (expected=[*])", + SLIST_FIRST(targets)->target + ); + } + + if (power_on_call_count++) { + errx(1, "proto_power_on should have been called once"); + } +} + +int +main(void) +{ + jsmntok_t tokens[32]; + const char json[] = ("[" + "{" + "\"method\": \"power_on\"," + "\"id\": \"004daf12-0561-4fbc-bfdb-bfe69cfbf4b5\"," + "\"params\": [\"*\"]," + "\"jsonrpc\": \"2.0\"" + "}," + "{" + "\"method\": \"la rache\"," + "\"id\": \"1f7a32c8-6741-4ee7-bec1-8431c7d514dc\"," + "\"params\": [\"*\"]," + "\"jsonrpc\": \"2.0\"" + "}" + "]"); + struct lgtd_client client = { .json = json, .jsmn_tokens = tokens }; + int parsed = parse_json( + tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json) + ); + + lgtd_jsonrpc_dispatch_request(&client, parsed); + + if (!power_on_call_count) { + errx(1, "power_on was never called"); + } + + const char expected[] = ("[" + "," // we mocked the first function + "{" + "\"jsonrpc\": \"2.0\", " + "\"id\": \"1f7a32c8-6741-4ee7-bec1-8431c7d514dc\", " + "\"error\": {\"code\": -32601, \"message\": \"Method not found\"}" + "}" + "]"); + if (strcmp(expected, client_write_buf)) { + errx(1, "got client buf %s (expected %s)", client_write_buf, expected); + } + + return 0; +} diff --git a/tests/core/jsonrpc/test_jsonrpc_batch_one_notification.c b/tests/core/jsonrpc/test_jsonrpc_batch_one_notification.c new file mode 100644 index 0000000..f1c0ba4 --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_batch_one_notification.c @@ -0,0 +1,93 @@ +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#define MOCKED_LGTD_PROTO_GET_LIGHT_STATE +#define MOCKED_LGTD_PROTO_POWER_ON +#include "mock_proto.h" +#include "mock_wire_proto.h" +#include "test_jsonrpc_utils.h" + +static int power_on_call_count = 0; + +void +lgtd_proto_power_on(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets) +{ + if (!client) { + errx(1, "missing client!"); + } + + if (strcmp(SLIST_FIRST(targets)->target, "*")) { + errx( + 1, "Invalid target [%s] (expected=[*])", + SLIST_FIRST(targets)->target + ); + } + + if (power_on_call_count++) { + errx(1, "proto_power_on should have been called once"); + } +} + +static int get_light_state_call_count = 0; + +void +lgtd_proto_get_light_state(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets) +{ + if (!client) { + errx(1, "missing client!"); + } + + if (strcmp(SLIST_FIRST(targets)->target, "*")) { + errx( + 1, "Invalid target [%s] (expected=[*])", + SLIST_FIRST(targets)->target + ); + } + + if (get_light_state_call_count++) { + errx(1, "proto_power_on should have been called once"); + } +} + +int +main(void) +{ + jsmntok_t tokens[32]; + const char json[] = ("[" + "{" + "\"method\": \"power_on\"," + "\"id\": \"004daf12-0561-4fbc-bfdb-bfe69cfbf4b5\"," + "\"params\": [\"*\"]," + "\"jsonrpc\": \"2.0\"" + "}," + "{" + "\"method\": \"get_light_state\"," + "\"params\": [\"*\"]," + "\"jsonrpc\": \"2.0\"" + "}" + "]"); + struct lgtd_client client = { .json = json, .jsmn_tokens = tokens }; + int parsed = parse_json( + tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json) + ); + + lgtd_jsonrpc_dispatch_request(&client, parsed); + + if (!power_on_call_count) { + errx(1, "power_on was never called"); + } + + if (!get_light_state_call_count) { + errx(1, "get_light_state was never called"); + } + + const char expected[] = "[]"; // we mocked the functions + if (strcmp(expected, client_write_buf)) { + errx(1, "got client buf %s (expected %s)", client_write_buf, expected); + } + + return 0; +} diff --git a/tests/core/jsonrpc/test_jsonrpc_build_target_list.c b/tests/core/jsonrpc/test_jsonrpc_build_target_list.c new file mode 100644 index 0000000..3a24d20 --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_build_target_list.c @@ -0,0 +1,72 @@ +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#include "mock_proto.h" +#include "mock_wire_proto.h" +#include "test_jsonrpc_utils.h" + +static void +test_params(const char *json, const char **expected_targets) +{ + struct lgtd_jsonrpc_request request = { .id = NULL }; + struct lgtd_client client = { + .io = NULL, .json = json, .current_request = &request + }; + + jsmntok_t tokens[32]; + int parsed = parse_json( + tokens, LGTD_ARRAY_SIZE(tokens), json, strlen(json) + ); + + struct lgtd_proto_target_list targets = SLIST_HEAD_INITIALIZER(&targets); + + reset_client_write_buf(); + + bool ok = lgtd_jsonrpc_build_target_list(&targets, &client, tokens, parsed); + + if (!expected_targets && !SLIST_EMPTY(&targets)) { + if (ok) { + errx(1, "lgtd_jsonrpc_build_target_list returned true on an error"); + } + return; + } + + struct lgtd_proto_target *target; + int i = 0; + SLIST_FOREACH(target, &targets, link) { + if (!expected_targets[i]) { + errx(1, "unexpected target %s", target->target); + } + if (strcmp(target->target, expected_targets[i])) { + errx( + 1, "target mismatch got %s but expected %s", + target->target, expected_targets[i] + ); + } + i++; + } +} + +int +main(void) +{ + const char *expected_1[] = {"on", "12345", "6789", NULL}; + test_params("[\"on\", 12345, \"6789\"]", expected_1); + + const char *expected_2[] = {"#tower", NULL}; + test_params("\"#tower\"", expected_2); + + test_params("{\"key\": 42}", NULL); + + const char *expected_3[] = {NULL}; + test_params("[]", expected_3); + + test_params("[\"on\", {\"lol\": \"wut\"}, \"6789\"]", NULL); + + // and make sure nothing blows up on plain invalid json/parameters + // (jsmn_parse will return a negative value): + test_params("null", NULL); + + return 0; +} diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off.c new file mode 100644 index 0000000..3631b61 --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off.c @@ -0,0 +1,61 @@ +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#define MOCKED_LGTD_PROTO_POWER_OFF +#include "mock_proto.h" +#include "mock_wire_proto.h" + +#include "test_jsonrpc_utils.h" + +static bool power_off_called = false; + +void +lgtd_proto_power_off(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets) +{ + if (!client) { + errx(1, "missing client!"); + } + + if (strcmp(SLIST_FIRST(targets)->target, "*")) { + errx( + 1, "Invalid target [%s] (expected=[*])", + SLIST_FIRST(targets)->target + ); + } + power_off_called = true; +} + +int +main(void) +{ + jsmntok_t tokens[32]; + const char json[] = ("{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"power_off\"," + "\"params\": {\"target\": \"*\"}," + "\"id\": \"42\"" + "}"); + int parsed = parse_json( + tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json) + ); + + bool ok; + struct lgtd_jsonrpc_request req = TEST_REQUEST_INITIALIZER; + struct lgtd_client client = { + .io = NULL, .current_request = &req, .json = json + }; + ok = lgtd_jsonrpc_check_and_extract_request(&req, tokens, parsed, json); + if (!ok) { + errx(1, "can't parse request"); + } + + lgtd_jsonrpc_check_and_call_power_off(&client); + + if (!power_off_called) { + errx(1, "lgtd_proto_power_off wasn't called"); + } + + return 0; +} diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off_missing_target.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off_missing_target.c new file mode 100644 index 0000000..82ec628 --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off_missing_target.c @@ -0,0 +1,57 @@ +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#define MOCKED_LGTD_PROTO_POWER_OFF +#include "mock_proto.h" +#include "mock_wire_proto.h" + +#include "test_jsonrpc_utils.h" + +static bool power_off_called = false; + +void +lgtd_proto_power_off(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets) +{ + (void)targets; + (void)client; + power_off_called = true; +} + +int +main(void) +{ + jsmntok_t tokens[32]; + const char json[] = ("{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"power_off\"," + "\"params\": {\"fensjk\": \"*\"}," + "\"id\": \"42\"" + "}"); + int parsed = parse_json( + tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json) + ); + + bool ok; + struct lgtd_jsonrpc_request req = TEST_REQUEST_INITIALIZER; + struct lgtd_client client = { + .io = NULL, .current_request = &req, .json = json + }; + ok = lgtd_jsonrpc_check_and_extract_request(&req, tokens, parsed, json); + if (!ok) { + errx(1, "can't parse request"); + } + + lgtd_jsonrpc_check_and_call_power_off(&client); + + if (!strstr(client_write_buf, "-32602")) { + errx(1, "no error returned"); + } + + if (power_off_called) { + errx(1, "lgtd_proto_power_off was called"); + } + + return 0; +} diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on.c new file mode 100644 index 0000000..dcf6e75 --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on.c @@ -0,0 +1,61 @@ +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#define MOCKED_LGTD_PROTO_POWER_ON +#include "mock_proto.h" +#include "mock_wire_proto.h" + +#include "test_jsonrpc_utils.h" + +static bool power_on_called = false; + +void +lgtd_proto_power_on(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets) +{ + if (!client) { + errx(1, "missing client!"); + } + + if (strcmp(SLIST_FIRST(targets)->target, "*")) { + errx( + 1, "Invalid target [%s] (expected=[*])", + SLIST_FIRST(targets)->target + ); + } + power_on_called = true; +} + +int +main(void) +{ + jsmntok_t tokens[32]; + const char json[] = ("{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"power_on\"," + "\"params\": {\"target\": \"*\"}," + "\"id\": \"42\"" + "}"); + int parsed = parse_json( + tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json) + ); + + bool ok; + struct lgtd_jsonrpc_request req = TEST_REQUEST_INITIALIZER; + struct lgtd_client client = { + .io = NULL, .current_request = &req, .json = json + }; + ok = lgtd_jsonrpc_check_and_extract_request(&req, tokens, parsed, json); + if (!ok) { + errx(1, "can't parse request"); + } + + lgtd_jsonrpc_check_and_call_power_on(&client); + + if (!power_on_called) { + errx(1, "lgtd_proto_power_on wasn't called"); + } + + return 0; +} diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on_missing_target.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on_missing_target.c new file mode 100644 index 0000000..e78ff21 --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on_missing_target.c @@ -0,0 +1,57 @@ +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#define MOCKED_LGTD_PROTO_POWER_ON +#include "mock_proto.h" +#include "mock_wire_proto.h" + +#include "test_jsonrpc_utils.h" + +static bool power_on_called = false; + +void +lgtd_proto_power_on(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets) +{ + (void)client; + (void)targets; + power_on_called = true; +} + +int +main(void) +{ + jsmntok_t tokens[32]; + const char json[] = ("{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"power_on\"," + "\"params\": {}," + "\"id\": \"42\"" + "}"); + int parsed = parse_json( + tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json) + ); + + bool ok; + struct lgtd_jsonrpc_request req = TEST_REQUEST_INITIALIZER; + struct lgtd_client client = { + .io = NULL, .current_request = &req, .json = json + }; + ok = lgtd_jsonrpc_check_and_extract_request(&req, tokens, parsed, json); + if (!ok) { + errx(1, "can't parse request"); + } + + lgtd_jsonrpc_check_and_call_power_on(&client); + + if (!strstr(client_write_buf, "-32602")) { + errx(1, "no error returned"); + } + + if (power_on_called) { + errx(1, "lgtd_proto_power_off was called"); + } + + return 0; +} diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_toggle.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_toggle.c new file mode 100644 index 0000000..86e6e46 --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_toggle.c @@ -0,0 +1,61 @@ +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#define MOCKED_LGTD_PROTO_POWER_TOGGLE +#include "mock_proto.h" +#include "mock_wire_proto.h" + +#include "test_jsonrpc_utils.h" + +static bool power_toggle_called = false; + +void +lgtd_proto_power_toggle(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets) +{ + if (!client) { + errx(1, "missing client!"); + } + + if (strcmp(SLIST_FIRST(targets)->target, "*")) { + errx( + 1, "Invalid target [%s] (expected=[*])", + SLIST_FIRST(targets)->target + ); + } + power_toggle_called = true; +} + +int +main(void) +{ + jsmntok_t tokens[32]; + const char json[] = ("{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"power_toggle\"," + "\"params\": {\"target\": \"*\"}," + "\"id\": \"42\"" + "}"); + int parsed = parse_json( + tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json) + ); + + bool ok; + struct lgtd_jsonrpc_request req = TEST_REQUEST_INITIALIZER; + struct lgtd_client client = { + .io = NULL, .current_request = &req, .json = json + }; + ok = lgtd_jsonrpc_check_and_extract_request(&req, tokens, parsed, json); + if (!ok) { + errx(1, "can't parse request"); + } + + lgtd_jsonrpc_check_and_call_power_toggle(&client); + + if (!power_toggle_called) { + errx(1, "lgtd_proto_power_toggle wasn't called"); + } + + return 0; +} diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_label.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_label.c new file mode 100644 index 0000000..444dabe --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_label.c @@ -0,0 +1,68 @@ +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#define MOCKED_LGTD_PROTO_SET_LABEL +#include "mock_proto.h" +#include "mock_wire_proto.h" +#include "mock_gateway.h" + +#include "test_jsonrpc_utils.h" + +static bool tag_called = false; + +void +lgtd_proto_set_label(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets, + const char *label) +{ + if (!client) { + errx(1, "missing client!"); + } + + if (strcmp(SLIST_FIRST(targets)->target, "*")) { + errx( + 1, "Invalid target [%s] (expected=[*])", + SLIST_FIRST(targets)->target + ); + } + + if (strcmp(label, "candle")) { + errx(1, "Invalid label [%s] (expected=[candle])", label); + } + + tag_called = true; +} + +int +main(void) +{ + jsmntok_t tokens[32]; + const char json[] = ("{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"set_label\"," + "\"params\": {\"target\": \"*\", \"label\": \"candle\"}," + "\"id\": \"42\"" + "}"); + int parsed = parse_json( + tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json) + ); + + bool ok; + struct lgtd_jsonrpc_request req = TEST_REQUEST_INITIALIZER; + struct lgtd_client client = { + .io = NULL, .current_request = &req, .json = json + }; + ok = lgtd_jsonrpc_check_and_extract_request(&req, tokens, parsed, json); + if (!ok) { + errx(1, "can't parse request"); + } + + lgtd_jsonrpc_check_and_call_set_label(&client); + + if (!tag_called) { + errx(1, "lgtd_proto_label wasn't called"); + } + + return 0; +} diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk.c new file mode 100644 index 0000000..bb97880 --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk.c @@ -0,0 +1,104 @@ +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#define MOCKED_LGTD_PROTO_SET_LIGHT_FROM_HSBK +#include "mock_proto.h" +#include "mock_wire_proto.h" + +#include "test_jsonrpc_utils.h" + +static bool set_light_called = false; + +void +lgtd_proto_set_light_from_hsbk(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets, + int hue, + int saturation, + int brightness, + int kelvin, + int transition_msecs) +{ + if (!client) { + errx(1, "missing client!"); + } + + if (strcmp(SLIST_FIRST(targets)->target, "*")) { + errx( + 1, "Invalid target [%s] (expected=[*])", + SLIST_FIRST(targets)->target + ); + } + int expected_hue = lgtd_jsonrpc_float_range_to_uint16( + "324.2341", strlen("324.2341"), 0, 360 + ); + if (hue != expected_hue) { + errx(1, "Invalid hue: %d, expected: %d", hue, expected_hue); + } + int expected_saturation = lgtd_jsonrpc_float_range_to_uint16( + "0.234", strlen("0.234"), 0, 1 + ); + if (saturation != expected_saturation) { + errx( + 1, "Invalid saturation: %d, expected: %d", + saturation, expected_saturation + ); + } + if (brightness != UINT16_MAX) { + errx( + 1, "Invalid brightness: %d, expected: %d", + brightness, UINT16_MAX + ); + } + if (kelvin != 4200) { + errx( + 1, "Invalid temperature: %d, expected: 4200", kelvin + ); + } + if (transition_msecs != 60) { + errx( + 1, "Invalid transition duration: %d, expected: 60", transition_msecs + ); + } + set_light_called = true; +} + +int +main(void) +{ + jsmntok_t tokens[32]; + const char json[] = ("{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"set_light_from_hsbk\"," + "\"params\": {" + "\"target\": \"*\", " + "\"hue\": 324.2341514, " + "\"saturation\": 0.234, " + "\"brightness\": 1.0, " + "\"kelvin\": 4200," + "\"transition\": 60" + "}," + "\"id\": \"42\"" + "}"); + int parsed = parse_json( + tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json) + ); + + bool ok; + struct lgtd_jsonrpc_request req = TEST_REQUEST_INITIALIZER; + struct lgtd_client client = { + .io = NULL, .current_request = &req, .json = json + }; + ok = lgtd_jsonrpc_check_and_extract_request(&req, tokens, parsed, json); + if (!ok) { + errx(1, "can't parse request"); + } + + lgtd_jsonrpc_check_and_call_set_light_from_hsbk(&client); + + if (!set_light_called) { + errx(1, "lgtd_proto_set_light_from_hsbk wasn't called"); + } + + return 0; +} diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_from_array.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_from_array.c new file mode 100644 index 0000000..dd62450 --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_from_array.c @@ -0,0 +1,99 @@ +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#define MOCKED_LGTD_PROTO_SET_LIGHT_FROM_HSBK +#include "mock_proto.h" +#include "mock_wire_proto.h" + +#include "test_jsonrpc_utils.h" + +static bool set_light_called = false; + +void +lgtd_proto_set_light_from_hsbk(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets, + int hue, + int saturation, + int brightness, + int kelvin, + int transition_msecs) +{ + if (!client) { + errx(1, "missing client!"); + } + + if (strcmp(SLIST_FIRST(targets)->target, "*")) { + errx( + 1, "Invalid target [%s] (expected=[*])", + SLIST_FIRST(targets)->target + ); + } + int expected_hue = lgtd_jsonrpc_float_range_to_uint16( + "324.2341", strlen("324.2341"), 0, 360 + ); + if (hue != expected_hue) { + errx(1, "Invalid hue: %d, expected: %d", hue, expected_hue); + } + int expected_saturation = lgtd_jsonrpc_float_range_to_uint16( + "0.234", strlen("0.234"), 0, 1 + ); + if (saturation != expected_saturation) { + errx( + 1, "Invalid saturation: %d, expected: %d", + saturation, expected_saturation + ); + } + if (brightness != UINT16_MAX) { + errx( + 1, "Invalid brightness: %d, expected: %d", + brightness, UINT16_MAX + ); + } + if (kelvin != 4200) { + errx( + 1, "Invalid temperature: %d, expected: 4200", kelvin + ); + } + if (transition_msecs != 0) { + errx( + 1, "Invalid transition duration: %d, expected: 0", transition_msecs + ); + } + set_light_called = true; +} + +int +main(void) +{ + jsmntok_t tokens[32]; + const char json[] = ("{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"set_light_from_hsbk\"," + "\"params\": [" + "\"*\", 324.2341514, 0.234, 1.0, 4200, 0" + "]," + "\"id\": \"42\"" + "}"); + int parsed = parse_json( + tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json) + ); + + bool ok; + struct lgtd_jsonrpc_request req = TEST_REQUEST_INITIALIZER; + struct lgtd_client client = { + .io = NULL, .current_request = &req, .json = json + }; + ok = lgtd_jsonrpc_check_and_extract_request(&req, tokens, parsed, json); + if (!ok) { + errx(1, "can't parse request"); + } + + lgtd_jsonrpc_check_and_call_set_light_from_hsbk(&client); + + if (!set_light_called) { + errx(1, "lgtd_proto_set_light_from_hsbk wasn't called"); + } + + return 0; +} diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_invalid_params.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_invalid_params.c new file mode 100644 index 0000000..c4256f1 --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_invalid_params.c @@ -0,0 +1,137 @@ +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#define MOCKED_LGTD_PROTO_SET_LIGHT_FROM_HSBK +#include "mock_proto.h" +#include "mock_wire_proto.h" + +#include "test_jsonrpc_utils.h" + +static bool set_light_called = false; + +void +lgtd_proto_set_light_from_hsbk(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets, + int hue, + int saturation, + int brightness, + int kelvin, + int transition_msecs) +{ + (void)client; + (void)targets; + (void)hue; + (void)saturation; + (void)brightness; + (void)kelvin; + (void)transition_msecs; + set_light_called = true; +} + +static void +test_request(const char *json) +{ + jsmntok_t tokens[32]; + int parsed = parse_json( + tokens, LGTD_ARRAY_SIZE(tokens), json, strlen(json) + ); + + bool ok; + struct lgtd_jsonrpc_request req = TEST_REQUEST_INITIALIZER; + struct lgtd_client client = { + .io = NULL, .current_request = &req, .json = json + }; + ok = lgtd_jsonrpc_check_and_extract_request(&req, tokens, parsed, json); + if (!ok) { + errx(1, "can't parse request"); + } + + lgtd_jsonrpc_check_and_call_set_light_from_hsbk(&client); + + if (set_light_called) { + errx(1, "lgtd_proto_power_off was called"); + } + + reset_client_write_buf(); +} + +int +main(void) +{ + // invalid temperature: + test_request("{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"set_light_from_hsbk\"," + "\"params\": {" + "\"target\": \"*\", " + "\"hue\": 324.2341514, " + "\"saturation\": 0.234, " + "\"brightness\": 1.0, " + "\"kelvin\": -4200," + "\"transition\": 42" + "}," + "\"id\": \"42\"" + "}"); + + // saturation to big + test_request("{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"set_light_from_hsbk\"," + "\"params\": {" + "\"target\": \"*\", " + "\"hue\": 324.2341514, " + "\"saturation\": 3.234, " + "\"brightness\": 1.0, " + "\"kelvin\": 4200," + "\"transition\": 42" + "}," + "\"id\": \"42\"" + "}"); + + // hue too big + test_request("{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"set_light_from_hsbk\"," + "\"params\": {" + "\"target\": \"*\", " + "\"hue\": 424.2341514, " + "\"saturation\": 0.234, " + "\"brightness\": 1.0, " + "\"kelvin\": 4200," + "\"transition\": 42" + "}," + "\"id\": \"42\"" + "}"); + + // brightness too small + test_request("{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"set_light_from_hsbk\"," + "\"params\": {" + "\"target\": \"*\", " + "\"hue\": 224.2341514, " + "\"saturation\": 0.234, " + "\"brightness\": -1.0, " + "\"kelvin\": 4200," + "\"transition\": 42" + "}," + "\"id\": \"42\"" + "}"); + + // negative transition + test_request("{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"set_light_from_hsbk\"," + "\"params\": {" + "\"target\": \"*\", " + "\"hue\": 224.2341514, " + "\"saturation\": 0.234, " + "\"brightness\": -1.0, " + "\"kelvin\": 4200," + "\"transition\": -42" + "}," + "\"id\": \"42\"" + "}"); + return 0; +} diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform.c new file mode 100644 index 0000000..d15695a --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform.c @@ -0,0 +1,136 @@ +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#define MOCKED_LGTD_PROTO_SET_WAVEFORM +#include "mock_proto.h" +#define MOCKED_LGTD_LIFX_WIRE_WAVEFORM_STRING_ID_TO_TYPE +#include "mock_wire_proto.h" + +#include "test_jsonrpc_utils.h" + +enum lgtd_lifx_waveform_type +lgtd_lifx_wire_waveform_string_id_to_type(const char *s, int len) +{ + if (len != 3) { + errx(1, "err = %d (expected 3)", len); + } + + if (strncmp(s, "SAW", 3)) { + errx(1, "s = %.3s (expected SAW)", s); + } + + return LGTD_LIFX_WAVEFORM_SAW; +} + +static bool set_waveform_called = false; + +void +lgtd_proto_set_waveform(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets, + enum lgtd_lifx_waveform_type waveform, + int hue, int saturation, + int brightness, int kelvin, + int period, float cycles, + int skew_ratio, bool transient) +{ + if (!client) { + errx(1, "missing client"); + } + + if (strcmp(SLIST_FIRST(targets)->target, "*")) { + errx( + 1, "Invalid target [%s] (expected=[*])", + SLIST_FIRST(targets)->target + ); + } + int expected_hue = lgtd_jsonrpc_float_range_to_uint16( + "324.2341", strlen("324.2341"), 0, 360 + ); + if (hue != expected_hue) { + errx(1, "Invalid hue: %d, expected: %d", hue, expected_hue); + } + int expected_saturation = lgtd_jsonrpc_float_range_to_uint16( + "0.234", strlen("0.234"), 0, 1 + ); + if (saturation != expected_saturation) { + errx( + 1, "Invalid saturation: %d, expected: %d", + saturation, expected_saturation + ); + } + if (brightness != UINT16_MAX) { + errx( + 1, "Invalid brightness: %d, expected: %d", + brightness, UINT16_MAX + ); + } + if (kelvin != 4200) { + errx( + 1, "Invalid temperature: %d, expected: 4200", kelvin + ); + } + if (period != 1000) { + errx(1, "Invalid period: %d, expected: 1000", period); + } + if (cycles != 10) { + errx(1, "Invalid cycles: %d, expected: 10", (int)cycles); + } + if (skew_ratio != 0) { + errx(1, "Invalid skew_ratio: %d, expected: 0", skew_ratio); + } + if (!transient) { + errx(1, "transient is false instead of true"); + } + if (waveform != LGTD_LIFX_WAVEFORM_SAW) { + errx( + 1, "Invalid waveform %d: expected: %d", + waveform, LGTD_LIFX_WAVEFORM_SAW + ); + } + set_waveform_called = true; +} + +int +main(void) +{ + jsmntok_t tokens[32]; + const char json[] = ("{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"set_waveform\"," + "\"params\": {" + "\"target\": \"*\", " + "\"hue\": 324.2341514, " + "\"saturation\": 0.234, " + "\"brightness\": 1.0, " + "\"kelvin\": 4200," + "\"cycles\": 10," + "\"period\": 1000," + "\"skew_ratio\": 0.5," + "\"transient\": true," + "\"waveform\": \"SAW\"" + "}," + "\"id\": \"42\"" + "}"); + int parsed = parse_json( + tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json) + ); + + bool ok; + struct lgtd_jsonrpc_request req = TEST_REQUEST_INITIALIZER; + struct lgtd_client client = { + .io = NULL, .current_request = &req, .json = json + }; + ok = lgtd_jsonrpc_check_and_extract_request(&req, tokens, parsed, json); + if (!ok) { + errx(1, "can't parse request"); + } + + lgtd_jsonrpc_check_and_call_set_waveform(&client); + + if (!set_waveform_called) { + errx(1, "lgtd_proto_set_waveform wasn't called"); + } + + return 0; +} diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform_invalid_params.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform_invalid_params.c new file mode 100644 index 0000000..bd675ca --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform_invalid_params.c @@ -0,0 +1,263 @@ +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#define MOCKED_LGTD_PROTO_SET_WAVEFORM +#include "mock_proto.h" +#define MOCKED_LGTD_LIFX_WIRE_WAVEFORM_STRING_ID_TO_TYPE +#include "mock_wire_proto.h" + +#include "test_jsonrpc_utils.h" + +static bool set_waveform_called = false; + +enum lgtd_lifx_waveform_type +lgtd_lifx_wire_waveform_string_id_to_type(const char *s, int len) +{ + return len == 3 && !strncmp(s, "SAW", 3) ? + LGTD_LIFX_WAVEFORM_SAW : LGTD_LIFX_WAVEFORM_INVALID; +} + +void +lgtd_proto_set_waveform(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets, + enum lgtd_lifx_waveform_type waveform, + int hue, int saturation, + int brightness, int kelvin, + int period, float cycles, + int skew_ratio, bool transient) +{ + (void)client; + (void)targets; + (void)waveform; + (void)hue; + (void)saturation; + (void)brightness; + (void)kelvin; + (void)period; + (void)cycles; + (void)skew_ratio; + (void)transient; + set_waveform_called = true; +} + +static void +test_request(const char *json) +{ + jsmntok_t tokens[32]; + int parsed = parse_json( + tokens, LGTD_ARRAY_SIZE(tokens), json, strlen(json) + ); + + bool ok; + struct lgtd_jsonrpc_request req = TEST_REQUEST_INITIALIZER; + struct lgtd_client client = { + .io = NULL, .current_request = &req, .json = json + }; + ok = lgtd_jsonrpc_check_and_extract_request(&req, tokens, parsed, json); + if (!ok) { + errx(1, "can't parse request"); + } + + lgtd_jsonrpc_check_and_call_set_waveform(&client); + + if (set_waveform_called) { + errx(1, "lgtd_proto_power_off was called"); + } + + reset_client_write_buf(); +} + +int +main(void) +{ + // invalid temperature: + test_request("{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"set_waveform\"," + "\"params\": {" + "\"target\": \"*\", " + "\"hue\": 324.2341514, " + "\"saturation\": 0.234, " + "\"brightness\": 1.0, " + "\"kelvin\": -4200," + "\"cycles\": 10," + "\"period\": 1000," + "\"skew_ratio\": 0.5," + "\"transient\": true," + "\"waveform\": \"SAW\"" + "}," + "\"id\": \"42\"" + "}"); + + // saturation to big + test_request("{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"set_waveform\"," + "\"params\": {" + "\"target\": \"*\", " + "\"hue\": 324.2341514, " + "\"saturation\": 3.234, " + "\"brightness\": 1.0, " + "\"kelvin\": 4200," + "\"cycles\": 10," + "\"period\": 1000," + "\"skew_ratio\": 0.5," + "\"transient\": true," + "\"waveform\": \"SAW\"" + "}," + "\"id\": \"42\"" + "}"); + + // hue too big + test_request("{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"set_waveform\"," + "\"params\": {" + "\"target\": \"*\", " + "\"hue\": 424.2341514, " + "\"saturation\": 0.234, " + "\"brightness\": 1.0, " + "\"kelvin\": 4200," + "\"cycles\": 10," + "\"period\": 1000," + "\"skew_ratio\": 0.5," + "\"transient\": true," + "\"waveform\": \"SAW\"" + "}," + "\"id\": \"42\"" + "}"); + + // brightness too small + test_request("{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"set_waveform\"," + "\"params\": {" + "\"target\": \"*\", " + "\"hue\": 224.2341514, " + "\"saturation\": 0.234, " + "\"brightness\": -1.0, " + "\"kelvin\": 4200," + "\"cycles\": 10," + "\"period\": 1000," + "\"skew_ratio\": 0.5," + "\"transient\": true," + "\"waveform\": \"SAW\"" + "}," + "\"id\": \"42\"" + "}"); + + // cycles must be > 0 + test_request("{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"set_waveform\"," + "\"params\": {" + "\"target\": \"*\", " + "\"hue\": 224.2341514, " + "\"saturation\": 0.234, " + "\"brightness\": 1.0, " + "\"kelvin\": 4200," + "\"cycles\": 0," + "\"period\": 1000," + "\"skew_ratio\": 0.5," + "\"transient\": true," + "\"waveform\": \"SAW\"" + "}," + "\"id\": \"42\"" + "}"); + + // skew ratio too big + test_request("{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"set_waveform\"," + "\"params\": {" + "\"target\": \"*\", " + "\"hue\": 224.2341514, " + "\"saturation\": 0.234, " + "\"brightness\": 1.0, " + "\"kelvin\": 4200," + "\"cycles\": 10," + "\"period\": 1000," + "\"skew_ratio\": 1.5," + "\"transient\": true," + "\"waveform\": \"SAW\"" + "}," + "\"id\": \"42\"" + "}"); + + // skew ratio too small + test_request("{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"set_waveform\"," + "\"params\": {" + "\"target\": \"*\", " + "\"hue\": 224.2341514, " + "\"saturation\": 0.234, " + "\"brightness\": 1.0, " + "\"kelvin\": 4200," + "\"cycles\": 10," + "\"period\": 1000," + "\"skew_ratio\": -1.5," + "\"transient\": true," + "\"waveform\": \"SAW\"" + "}," + "\"id\": \"42\"" + "}"); + + // invalid waveform + test_request("{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"set_waveform\"," + "\"params\": {" + "\"target\": \"*\", " + "\"hue\": 224.2341514, " + "\"saturation\": 0.234, " + "\"brightness\": 1.0, " + "\"kelvin\": 4200," + "\"cycles\": 10," + "\"period\": 1000," + "\"skew_ratio\": 0.5," + "\"transient\": true," + "\"waveform\": \"TEST\"" + "}," + "\"id\": \"42\"" + "}"); + + // invalid transient + test_request("{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"set_waveform\"," + "\"params\": {" + "\"target\": \"*\", " + "\"hue\": 224.2341514, " + "\"saturation\": 0.234, " + "\"brightness\": 1.0, " + "\"kelvin\": 4200," + "\"cycles\": 10," + "\"period\": 1000," + "\"skew_ratio\": 0.5," + "\"transient\": 42," + "\"waveform\": \"SAW\"" + "}," + "\"id\": \"42\"" + "}"); + + // period must be > 0 + test_request("{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"set_waveform\"," + "\"params\": {" + "\"target\": \"*\", " + "\"hue\": 224.2341514, " + "\"saturation\": 0.234, " + "\"brightness\": 1.0, " + "\"kelvin\": 4200," + "\"cycles\": 10," + "\"period\": 0," + "\"skew_ratio\": 0.5," + "\"transient\": true," + "\"waveform\": \"SAW\"" + "}," + "\"id\": \"42\"" + "}"); +} diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_tag.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_tag.c new file mode 100644 index 0000000..80af1e5 --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_tag.c @@ -0,0 +1,68 @@ +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#define MOCKED_LGTD_PROTO_TAG +#include "mock_proto.h" +#include "mock_wire_proto.h" +#include "mock_gateway.h" + +#include "test_jsonrpc_utils.h" + +static bool tag_called = false; + +void +lgtd_proto_tag(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets, + const char *tag) +{ + if (!client) { + errx(1, "missing client!"); + } + + if (strcmp(SLIST_FIRST(targets)->target, "*")) { + errx( + 1, "Invalid target [%s] (expected=[*])", + SLIST_FIRST(targets)->target + ); + } + + if (strcmp(tag, "suspensions")) { + errx(1, "Invalid tag [%s] (expected=[suspensions])", tag); + } + + tag_called = true; +} + +int +main(void) +{ + jsmntok_t tokens[32]; + const char json[] = ("{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"tag\"," + "\"params\": {\"target\": \"*\", \"label\": \"suspensions\"}," + "\"id\": \"42\"" + "}"); + int parsed = parse_json( + tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json) + ); + + bool ok; + struct lgtd_jsonrpc_request req = TEST_REQUEST_INITIALIZER; + struct lgtd_client client = { + .io = NULL, .current_request = &req, .json = json + }; + ok = lgtd_jsonrpc_check_and_extract_request(&req, tokens, parsed, json); + if (!ok) { + errx(1, "can't parse request"); + } + + lgtd_jsonrpc_check_and_call_tag(&client); + + if (!tag_called) { + errx(1, "lgtd_proto_tag wasn't called"); + } + + return 0; +} diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_tag_missing_params.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_tag_missing_params.c new file mode 100644 index 0000000..184358c --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_tag_missing_params.c @@ -0,0 +1,56 @@ +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#define MOCKED_LGTD_PROTO_TAG +#include "mock_proto.h" +#include "mock_wire_proto.h" +#include "mock_gateway.h" + +#include "test_jsonrpc_utils.h" + +static bool tag_called = false; + +void +lgtd_proto_tag(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets, + const char *tag) +{ + (void)client; + (void)targets; + (void)tag; + tag_called = true; +} + +int +main(void) +{ + jsmntok_t tokens[32]; + const char json[] = ("{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"tag\"," + "\"params\": {\"tag\": \"suspensions\"}," + "\"id\": \"42\"" + "}"); + int parsed = parse_json( + tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json) + ); + + bool ok; + struct lgtd_jsonrpc_request req = TEST_REQUEST_INITIALIZER; + struct lgtd_client client = { + .io = NULL, .current_request = &req, .json = json + }; + ok = lgtd_jsonrpc_check_and_extract_request(&req, tokens, parsed, json); + if (!ok) { + errx(1, "can't parse request"); + } + + lgtd_jsonrpc_check_and_call_tag(&client); + + if (tag_called) { + errx(1, "lgtd_proto_tag was called"); + } + + return 0; +} diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_untag.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_untag.c new file mode 100644 index 0000000..584a538 --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_untag.c @@ -0,0 +1,68 @@ +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#define MOCKED_LGTD_PROTO_UNTAG +#include "mock_proto.h" +#include "mock_wire_proto.h" +#include "mock_gateway.h" + +#include "test_jsonrpc_utils.h" + +static bool untag_called = false; + +void +lgtd_proto_untag(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets, + const char *tag) +{ + if (!client) { + errx(1, "missing client!"); + } + + if (strcmp(SLIST_FIRST(targets)->target, "#suspensions")) { + errx( + 1, "Invalid target [%s] (expected=[#suspensions])", + SLIST_FIRST(targets)->target + ); + } + + if (strcmp(tag, "suspensions")) { + errx(1, "Invalid tag [%s] (expected=[suspensions])", tag); + } + + untag_called = true; +} + +int +main(void) +{ + jsmntok_t tokens[32]; + const char json[] = ("{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"tag\"," + "\"params\": [[\"#suspensions\"], \"suspensions\"]," + "\"id\": \"42\"" + "}"); + int parsed = parse_json( + tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json) + ); + + bool ok; + struct lgtd_jsonrpc_request req = TEST_REQUEST_INITIALIZER; + struct lgtd_client client = { + .io = NULL, .current_request = &req, .json = json + }; + ok = lgtd_jsonrpc_check_and_extract_request(&req, tokens, parsed, json); + if (!ok) { + errx(1, "can't parse request"); + } + + lgtd_jsonrpc_check_and_call_untag(&client); + + if (!untag_called) { + errx(1, "lgtd_proto_tag wasn't called"); + } + + return 0; +} diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_untag_invalid_params.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_untag_invalid_params.c new file mode 100644 index 0000000..65c40cd --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_untag_invalid_params.c @@ -0,0 +1,56 @@ +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#define MOCKED_LGTD_PROTO_UNTAG +#include "mock_proto.h" +#include "mock_wire_proto.h" +#include "mock_gateway.h" + +#include "test_jsonrpc_utils.h" + +static bool untag_called = false; + +void +lgtd_proto_untag(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets, + const char *tag) +{ + (void)client; + (void)targets; + (void)tag; + untag_called = true; +} + +int +main(void) +{ + jsmntok_t tokens[32]; + const char json[] = ("{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"tag\"," + "\"params\": [[\"#suspensions\"], [\"suspensions\"]]," + "\"id\": \"42\"" + "}"); + int parsed = parse_json( + tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json) + ); + + bool ok; + struct lgtd_jsonrpc_request req = TEST_REQUEST_INITIALIZER; + struct lgtd_client client = { + .io = NULL, .current_request = &req, .json = json + }; + ok = lgtd_jsonrpc_check_and_extract_request(&req, tokens, parsed, json); + if (!ok) { + errx(1, "can't parse request"); + } + + lgtd_jsonrpc_check_and_call_untag(&client); + + if (untag_called) { + errx(1, "lgtd_proto_tag was called"); + } + + return 0; +} diff --git a/tests/core/jsonrpc/test_jsonrpc_consume_object_or_array.c b/tests/core/jsonrpc/test_jsonrpc_consume_object_or_array.c new file mode 100644 index 0000000..db39cdd --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_consume_object_or_array.c @@ -0,0 +1,38 @@ +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#include "mock_proto.h" +#include "mock_wire_proto.h" +#include "test_jsonrpc_utils.h" + +int +main(void) +{ + jsmntok_t tokens[32]; + memset(tokens, 0, sizeof(tokens)); + const char json[] = ("[" + "{" + "\"method\": \"power_on\"," + "\"id\": \"004daf12-0561-4fbc-bfdb-bfe69cfbf4b5\"," + "\"params\": [\"*\"]," + "\"jsonrpc\": \"2.0\"" + "}," + "{" + "\"method\": \"get_light_state\"," + "\"id\": \"1f7a32c8-6741-4ee7-bec1-8431c7d514dc\"," + "\"params\": [\"*\"]," + "\"jsonrpc\": \"2.0\"" + "}" + "]"); + int parsed = parse_json( + tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json) + ); + + int ti = lgtd_jsonrpc_consume_object_or_array(tokens, 0, parsed, json); + if (ti != parsed) { + errx(1, "ti %d (expected %d)", ti, parsed); + } + + return 0; +} diff --git a/tests/core/jsonrpc/test_jsonrpc_dispatch_one_no_params.c b/tests/core/jsonrpc/test_jsonrpc_dispatch_one_no_params.c new file mode 100644 index 0000000..5c001e7 --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_dispatch_one_no_params.c @@ -0,0 +1,55 @@ +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#define MOCKED_LGTD_PROTO_POWER_ON +#include "mock_proto.h" +#include "mock_wire_proto.h" + +#include "test_jsonrpc_utils.h" + +void +lgtd_proto_power_on(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets) +{ + (void)client; + (void)targets; + + errx(1, "lgtd_proto_power_on shouldn't have been called"); +} + +int +main(void) +{ + jsmntok_t tokens[32]; + const char json[] = ("{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"power_on\"," + "\"id\": \"42\"" + "}"); + struct lgtd_client client = { .json = json }; + int parsed = parse_json( + tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json) + ); + + lgtd_jsonrpc_dispatch_one(&client, tokens, parsed, NULL); + + const char expected[] = ("{" + "\"jsonrpc\": \"2.0\", " + "\"id\": \"42\", " + "\"error\": {" + "\"code\": -32602, " + "\"message\": " + "\"Invalid number of parameters\"" + "}" + "}"); + + if (memcmp(expected, client_write_buf, sizeof(expected))) { + errx( + 1, "got %.*s back (expected %s)", + client_write_buf_idx, client_write_buf, expected + ); + } + + return 0; +} diff --git a/tests/core/jsonrpc/test_jsonrpc_extract_request_no_params.c b/tests/core/jsonrpc/test_jsonrpc_extract_request_no_params.c new file mode 100644 index 0000000..464877e --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_extract_request_no_params.c @@ -0,0 +1,52 @@ +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#include "mock_proto.h" +#include "mock_wire_proto.h" +#include "test_jsonrpc_utils.h" + +int +main(void) +{ + jsmntok_t tokens[32]; + const char json[] = ("{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"hello\"," + "\"id\": \"42\"" + "}"); + int parsed = parse_json( + tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json) + ); + + struct lgtd_jsonrpc_request req = TEST_REQUEST_INITIALIZER; + bool ok = lgtd_jsonrpc_check_and_extract_request( + &req, tokens, parsed, json + ); + + if (!ok) { + errx(1, "return value should be true"); + } + + if (!req.method) { + errx(1, "missing method"); + } + if (req.method->end - req.method->start != sizeof("hello") - 1 + || memcmp(&json[req.method->start], "hello", sizeof("hello") - 1)) { + errx(1, "wrong method name"); + } + + if (req.params) { + errx(1, "params should be null"); + } + + if (!req.id) { + errx(1, "missing id"); + } + if (req.id->end - req.id->start != 2 + || memcmp(&json[req.id->start], "42", 2)) { + errx(1, "wrong id"); + } + + return 0; +} diff --git a/tests/core/jsonrpc/test_jsonrpc_extract_request_notification_no_params.c b/tests/core/jsonrpc/test_jsonrpc_extract_request_notification_no_params.c new file mode 100644 index 0000000..3ca27a6 --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_extract_request_notification_no_params.c @@ -0,0 +1,47 @@ +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#include "mock_proto.h" +#include "mock_wire_proto.h" +#include "test_jsonrpc_utils.h" + +int +main(void) +{ + jsmntok_t tokens[32]; + const char json[] = ("{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"hello\"" + "}"); + int parsed = parse_json( + tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json) + ); + + struct lgtd_jsonrpc_request req = TEST_REQUEST_INITIALIZER; + bool ok = lgtd_jsonrpc_check_and_extract_request( + &req, tokens, parsed, json + ); + + if (!ok) { + errx(1, "return value should be true"); + } + + if (!req.method) { + errx(1, "missing method"); + } + if (req.method->end - req.method->start != sizeof("hello") - 1 + || memcmp(&json[req.method->start], "hello", sizeof("hello") - 1)) { + errx(1, "wrong method name"); + } + + if (req.params) { + errx(1, "params should be null"); + } + + if (req.id) { + errx(1, "id should be null"); + } + + return 0; +} diff --git a/tests/core/jsonrpc/test_jsonrpc_extract_request_params_array.c b/tests/core/jsonrpc/test_jsonrpc_extract_request_params_array.c new file mode 100644 index 0000000..9a10344 --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_extract_request_params_array.c @@ -0,0 +1,65 @@ +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#include "mock_proto.h" +#include "mock_wire_proto.h" +#include "test_jsonrpc_utils.h" + +int +main(void) +{ + jsmntok_t tokens[32]; + const char json[] = ("{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"hello\"," + "\"params\": [\"on\", 12345, null, {\"lol\": \"wut\"}]," + "\"id\": \"42\"" + "}"); + int parsed = parse_json( + tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json) + ); + + struct lgtd_jsonrpc_request req = TEST_REQUEST_INITIALIZER; + bool ok = lgtd_jsonrpc_check_and_extract_request( + &req, tokens, parsed, json + ); + + if (!ok) { + errx(1, "return value should be true"); + } + + if (!req.method) { + errx(1, "missing method"); + } + if (req.method->end - req.method->start != sizeof("hello") - 1 + || memcmp(&json[req.method->start], "hello", sizeof("hello") - 1)) { + errx(1, "wrong method name"); + } + + if (!req.params) { + errx(1, "missing params"); + } + const char params[] = "[\"on\", 12345, null, {\"lol\": \"wut\"}]"; + if (req.params->end - req.params->start != sizeof(params) - 1 + || memcmp(&json[req.params->start], params, req.params->size)) { + errx(1, "wrong params"); + } + + if (!req.id) { + errx(1, "missing id"); + } + if (req.id->end - req.id->start != 2 + || memcmp(&json[req.id->start], "42", 2)) { + errx(1, "wrong id"); + } + + ok = lgtd_jsonrpc_check_and_extract_request( + &req, tokens, 0, json + ); + if (ok) { + errx(1, "empty request should return false"); + } + + return 0; +} diff --git a/tests/core/jsonrpc/test_jsonrpc_extract_request_params_obj.c b/tests/core/jsonrpc/test_jsonrpc_extract_request_params_obj.c new file mode 100644 index 0000000..87782f9 --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_extract_request_params_obj.c @@ -0,0 +1,65 @@ +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#include "mock_proto.h" +#include "mock_wire_proto.h" +#include "test_jsonrpc_utils.h" + +int +main(void) +{ + jsmntok_t tokens[32]; + const char json[] = ("{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"hello\"," + "\"params\": {\"on\": true}," + "\"id\": \"42\"" + "}"); + int parsed = parse_json( + tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json) + ); + + struct lgtd_jsonrpc_request req = TEST_REQUEST_INITIALIZER; + bool ok = lgtd_jsonrpc_check_and_extract_request( + &req, tokens, parsed, json + ); + + if (!ok) { + errx(1, "return value should be true"); + } + + if (!req.method) { + errx(1, "missing method"); + } + if (req.method->end - req.method->start != sizeof("hello") - 1 + || memcmp(&json[req.method->start], "hello", sizeof("hello") - 1)) { + errx(1, "wrong method name"); + } + + if (!req.params) { + errx(1, "missing params"); + } + const char params[] = "{\"on\": true}"; + if (req.params->end - req.params->start != sizeof(params) - 1 + || memcmp(&json[req.params->start], params, req.params->size)) { + errx(1, "wrong params"); + } + + if (!req.id) { + errx(1, "missing id"); + } + if (req.id->end - req.id->start != 2 + || memcmp(&json[req.id->start], "42", 2)) { + errx(1, "wrong id"); + } + + ok = lgtd_jsonrpc_check_and_extract_request( + &req, tokens, 0, json + ); + if (ok) { + errx(1, "empty request should return false"); + } + + return 0; +} diff --git a/tests/core/jsonrpc/test_jsonrpc_extract_request_valid_notification.c b/tests/core/jsonrpc/test_jsonrpc_extract_request_valid_notification.c new file mode 100644 index 0000000..4e77f55 --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_extract_request_valid_notification.c @@ -0,0 +1,53 @@ +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#include "mock_proto.h" +#include "mock_wire_proto.h" +#include "test_jsonrpc_utils.h" + +int +main(void) +{ + jsmntok_t tokens[32]; + const char json[] = ("{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"hello\"," + "\"params\": {\"on\": true}" + "}"); + int parsed = parse_json( + tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json) + ); + + struct lgtd_jsonrpc_request req = TEST_REQUEST_INITIALIZER; + bool ok = lgtd_jsonrpc_check_and_extract_request( + &req, tokens, parsed, json + ); + + if (!ok) { + errx(1, "return value should be true"); + } + + if (!req.method) { + errx(1, "missing method"); + } + if (req.method->end - req.method->start != sizeof("hello") - 1 + || memcmp(&json[req.method->start], "hello", sizeof("hello") - 1)) { + errx(1, "wrong method name"); + } + + if (!req.params) { + errx(1, "missing params"); + } + const char params[] = "{\"on\": true}"; + if (req.params->end - req.params->start != sizeof(params) - 1 + || memcmp(&json[req.params->start], params, req.params->size)) { + errx(1, "wrong params"); + } + + if (req.id) { + errx(1, "id should be NULL"); + } + + return 0; +} diff --git a/tests/core/jsonrpc/test_jsonrpc_extract_values_from_schema_and_array_honors_objsize.c b/tests/core/jsonrpc/test_jsonrpc_extract_values_from_schema_and_array_honors_objsize.c new file mode 100644 index 0000000..aac46f4 --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_extract_values_from_schema_and_array_honors_objsize.c @@ -0,0 +1,63 @@ +#include + +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#include "mock_proto.h" +#include "mock_wire_proto.h" +#include "test_jsonrpc_utils.h" + +int +main(void) +{ + jsmntok_t tokens[32]; + memset(tokens, 0, sizeof(tokens)); + const char json[] = "[[\"*\"],[1,2,3,4]]"; + int parsed = parse_json( + tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json) + ); + + struct lgtd_jsonrpc_target_args { + const jsmntok_t *target; + int target_ntokens; + const jsmntok_t *label; + } params = { NULL, 0, NULL }; + static const struct lgtd_jsonrpc_node schema[] = { + LGTD_JSONRPC_NODE( + "target", + offsetof(struct lgtd_jsonrpc_target_args, target), + offsetof(struct lgtd_jsonrpc_target_args, target_ntokens), + lgtd_jsonrpc_type_string_number_or_array, + false + ), + LGTD_JSONRPC_NODE( + "label", + offsetof(struct lgtd_jsonrpc_target_args, label), + -1, + // this must dereference json from the what's in the token (see + // next comment): + lgtd_jsonrpc_type_number, + false + ) + }; + + // invalidate all the tokens so that the test will crash if we go beyond + // the first list: + for (int i = 3; i != LGTD_ARRAY_SIZE(tokens); i++) { + tokens[i].start = INT_MIN; + tokens[i].end = INT_MIN; + tokens[i].size = INT_MAX; + tokens[i].type = JSMN_PRIMITIVE; + } + + bool ok = lgtd_jsonrpc_extract_and_validate_params_against_schema( + ¶ms, schema, LGTD_ARRAY_SIZE(schema), &tokens[1], parsed - 1, json + ); + + if (ok) { + errx(1, "the schema shouldn't have been validated"); + } + + return 0; +} diff --git a/tests/core/jsonrpc/test_jsonrpc_send_error.c b/tests/core/jsonrpc/test_jsonrpc_send_error.c new file mode 100644 index 0000000..4d724fc --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_send_error.c @@ -0,0 +1,56 @@ +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#include "mock_proto.h" +#include "mock_wire_proto.h" +#include "test_jsonrpc_utils.h" + +int +main(void) +{ + const char *json = "\"42\""; + jsmntok_t token = { .start = 1, .end = 3, .type = JSMN_STRING }; + struct lgtd_jsonrpc_request req = { .id = &token }; + struct lgtd_client client = { + .io = NULL, .current_request = &req, .json = json + }; + const char *expected = ( + "{" + "\"jsonrpc\": \"2.0\", " + "\"id\": \"42\", " + "\"error\": {\"code\": -32600, \"message\": \"Invalid Request\"}" + "}" + ); + lgtd_jsonrpc_send_error( + &client, LGTD_JSONRPC_INVALID_REQUEST, "Invalid Request" + ); + int diff = memcmp(client_write_buf, expected, strlen(expected)); + if (diff) { + printf("expected: %s\n", expected); + printf("received: %s\n", client_write_buf); + return 1; + } + + reset_client_write_buf(); + + req.id = NULL; + expected = ( + "{" + "\"jsonrpc\": \"2.0\", " + "\"id\": null, " + "\"error\": {\"code\": -32600, \"message\": \"Invalid Request\"}" + "}" + ); + lgtd_jsonrpc_send_error( + &client, LGTD_JSONRPC_INVALID_REQUEST, "Invalid Request" + ); + diff = memcmp(client_write_buf, expected, strlen(expected)); + if (diff) { + printf("expected: %s\n", expected); + printf("received: %s\n", client_write_buf); + return 1; + } + + return 0; +} diff --git a/tests/core/jsonrpc/test_jsonrpc_send_response.c b/tests/core/jsonrpc/test_jsonrpc_send_response.c new file mode 100644 index 0000000..c980afe --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_send_response.c @@ -0,0 +1,48 @@ +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#include "mock_proto.h" +#include "mock_wire_proto.h" +#include "test_jsonrpc_utils.h" + +int +main(void) +{ + const char *json = "\"42\""; + jsmntok_t token = { .start = 1, .end = 3, .type = JSMN_STRING }; + struct lgtd_jsonrpc_request req = { .id = &token }; + struct lgtd_client client = { + .io = NULL, .current_request = &req, .json = json + }; + const char *expected = ("{" + "\"jsonrpc\": \"2.0\", " + "\"id\": \"42\", " + "\"result\": false" + "}"); + lgtd_jsonrpc_send_response(&client, "false"); + int diff = memcmp(client_write_buf, expected, strlen(expected)); + if (diff) { + printf("expected: %s\n", expected); + printf("received: %s\n", client_write_buf); + return 1; + } + + reset_client_write_buf(); + + req.id = NULL; + expected = ("{" + "\"jsonrpc\": \"2.0\", " + "\"id\": null, " + "\"result\": true" + "}"); + lgtd_jsonrpc_send_response(&client, "true"); + diff = memcmp(client_write_buf, expected, strlen(expected)); + if (diff) { + printf("expected: %s\n", expected); + printf("received: %s\n", client_write_buf); + return 1; + } + + return 0; +} diff --git a/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_1_invalid.c b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_1_invalid.c new file mode 100644 index 0000000..cf686ab --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_1_invalid.c @@ -0,0 +1,30 @@ +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#include "mock_proto.h" +#include "mock_wire_proto.h" +#include "test_jsonrpc_utils.h" + +static void +test_float(const char *json) +{ + jsmntok_t tokens[8]; + parse_json(tokens, LGTD_ARRAY_SIZE(tokens), json, strlen(json)); + if (lgtd_jsonrpc_type_float_between_0_and_1(&tokens[1], json)) { + errx(1, "%s was considered as a valid float >= 0 and <= 1", json); + } +} + +int +main(void) +{ + test_float("[1.1234]"); + test_float("[-0.1234]"); + test_float("[1.00000001]"); + test_float("[2.0000]"); + test_float("[10]"); + test_float("[0.0.1]"); + + return 0; +} diff --git a/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_1_valid.c b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_1_valid.c new file mode 100644 index 0000000..ac59cd6 --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_1_valid.c @@ -0,0 +1,29 @@ +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#include "mock_proto.h" +#include "mock_wire_proto.h" +#include "test_jsonrpc_utils.h" + +static void +test_float(const char *json) +{ + jsmntok_t tokens[8]; + parse_json(tokens, LGTD_ARRAY_SIZE(tokens), json, strlen(json)); + if (!lgtd_jsonrpc_type_float_between_0_and_1(&tokens[1], json)) { + errx(1, "%s wasn't considered as a valid float >= 0 and <= 1", json); + } +} + +int +main(void) +{ + test_float("[0.1234]"); + test_float("[1.0000000]"); + test_float("[0.9999]"); + test_float("[0.01]"); + test_float("[000.01]"); + + return 0; +} diff --git a/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_360_invalid.c b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_360_invalid.c new file mode 100644 index 0000000..5620378 --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_360_invalid.c @@ -0,0 +1,30 @@ +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#include "mock_proto.h" +#include "mock_wire_proto.h" +#include "test_jsonrpc_utils.h" + +static void +test_float(const char *json) +{ + jsmntok_t tokens[8]; + parse_json(tokens, LGTD_ARRAY_SIZE(tokens), json, strlen(json)); + if (lgtd_jsonrpc_type_float_between_0_and_360(&tokens[1], json)) { + errx(1, "%s was considered as a valid float >= 0 and <= 360", json); + } +} + +int +main(void) +{ + test_float("[-1.1234]"); + test_float("[-0.1234]"); + test_float("[0.1.234]"); + test_float("[0.1a234]"); + test_float("[360a]"); + test_float("[360.1]"); + + return 0; +} diff --git a/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_360_valid.c b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_360_valid.c new file mode 100644 index 0000000..0c14cba --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_360_valid.c @@ -0,0 +1,33 @@ +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#include "mock_proto.h" +#include "mock_wire_proto.h" +#include "test_jsonrpc_utils.h" + +static void +test_float(const char *json) +{ + jsmntok_t tokens[8]; + parse_json(tokens, LGTD_ARRAY_SIZE(tokens), json, strlen(json)); + if (!lgtd_jsonrpc_type_float_between_0_and_360(&tokens[1], json)) { + errx(1, "%s wasn't considered as a valid float >= 0 and <= 360", json); + } +} + +int +main(void) +{ + test_float("[1.1234]"); + test_float("[1.00000001]"); + test_float("[2.0000]"); + test_float("[10]"); + test_float("[0.1]"); + test_float("[0]"); + test_float("[231.]"); + test_float("[359.1]"); + test_float("[360]"); + + return 0; +} diff --git a/tests/core/jsonrpc/test_jsonrpc_type_integer.c b/tests/core/jsonrpc/test_jsonrpc_type_integer.c new file mode 100644 index 0000000..82fe85e --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_type_integer.c @@ -0,0 +1,26 @@ +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#include "mock_proto.h" +#include "mock_wire_proto.h" +#include "test_jsonrpc_utils.h" + +int +main(void) +{ + const char json[] = "[1234]"; + jsmntok_t tokens[8]; + + int rv = parse_json(tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json)); + + printf("rv = %d\n", rv); + + bool ok = lgtd_jsonrpc_type_integer(&tokens[1], json); + + if (!ok) { + errx(1, "%s wasn't considered as a valid integer", json); + } + + return 0; +} diff --git a/tests/core/jsonrpc/test_jsonrpc_type_integer_invalid_characters.c b/tests/core/jsonrpc/test_jsonrpc_type_integer_invalid_characters.c new file mode 100644 index 0000000..4f7e3ba --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_type_integer_invalid_characters.c @@ -0,0 +1,24 @@ +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#include "mock_proto.h" +#include "mock_wire_proto.h" +#include "test_jsonrpc_utils.h" + +int +main(void) +{ + const char json[] = "[-1a]"; + jsmntok_t tokens[8]; + + parse_json(tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json)); + + bool ok = lgtd_jsonrpc_type_integer(&tokens[1], json); + + if (ok) { + errx(1, "%s wasn't considered invalid", json); + } + + return 0; +} diff --git a/tests/core/jsonrpc/test_jsonrpc_type_integer_too_big.c b/tests/core/jsonrpc/test_jsonrpc_type_integer_too_big.c new file mode 100644 index 0000000..215480e --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_type_integer_too_big.c @@ -0,0 +1,24 @@ +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#include "mock_proto.h" +#include "mock_wire_proto.h" +#include "test_jsonrpc_utils.h" + +int +main(void) +{ + const char json[] = "[99999999999999999999999]"; + jsmntok_t tokens[8]; + + parse_json(tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json)); + + bool ok = lgtd_jsonrpc_type_integer(&tokens[1], json); + + if (ok) { + errx(1, "%s wasn't considered invalid", json); + } + + return 0; +} diff --git a/tests/core/jsonrpc/test_jsonrpc_type_integer_too_small.c b/tests/core/jsonrpc/test_jsonrpc_type_integer_too_small.c new file mode 100644 index 0000000..e3026e3 --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_type_integer_too_small.c @@ -0,0 +1,24 @@ +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#include "mock_proto.h" +#include "mock_wire_proto.h" +#include "test_jsonrpc_utils.h" + +int +main(void) +{ + const char json[] = "[-999999999999999999999]"; + jsmntok_t tokens[8]; + + parse_json(tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json)); + + bool ok = lgtd_jsonrpc_type_integer(&tokens[1], json); + + if (ok) { + errx(1, "%s wasn't considered invalid", json); + } + + return 0; +} diff --git a/tests/core/jsonrpc/test_jsonrpc_uint16_range_to_float_string.c b/tests/core/jsonrpc/test_jsonrpc_uint16_range_to_float_string.c new file mode 100644 index 0000000..83150b2 --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_uint16_range_to_float_string.c @@ -0,0 +1,100 @@ +#define NDEBUG 1 + +#include "jsonrpc.c" + +#include "mock_client_buf.h" +#include "mock_log.h" +#include "mock_proto.h" +#include "mock_wire_proto.h" +#include "test_jsonrpc_utils.h" + +int +main(void) +{ + char buf[32]; + int bufsz = sizeof(buf); + + lgtd_jsonrpc_uint16_range_to_float_string(UINT16_MAX, 0, 1, buf, bufsz); + if (strcmp(buf, "1")) { + lgtd_errx( + 1, "UINT16_MAX converted to %.*s (expected %s)", + bufsz, buf, "1" + ); + } + + lgtd_jsonrpc_uint16_range_to_float_string( + UINT16_MAX / 2 + UINT16_MAX / 3, 0, 1, buf, bufsz + ); + if (strcmp(buf, "0.833325")) { + lgtd_errx( + 1, + "UINT16_MAX / 2 + UINT16_MAX / 3 converted to %.*s (expected %s)", + bufsz, buf, "0.499992" + ); + } + + lgtd_jsonrpc_uint16_range_to_float_string(UINT16_MAX / 2, 0, 1, buf, bufsz); + if (strcmp(buf, "0.499992")) { + lgtd_errx( + 1, "UINT16_MAX / 2 converted to %.*s (expected %s)", + bufsz, buf, "0.499992" + ); + } + + lgtd_jsonrpc_uint16_range_to_float_string(UINT16_MAX / 10, 0, 1, buf, bufsz); + if (strcmp(buf, "0.099992")) { + lgtd_errx( + 1, "UINT16_MAX / 10 converted to %.*s (expected %s)", + bufsz, buf, "0.499992" + ); + } + + lgtd_jsonrpc_uint16_range_to_float_string(UINT16_MAX / 100, 0, 1, buf, bufsz); + if (strcmp(buf, "0.009994")) { + lgtd_errx( + 1, "UINT16_MAX / 100 converted to %.*s (expected %s)", + bufsz, buf, "0.499992" + ); + } + + lgtd_jsonrpc_uint16_range_to_float_string(0, 0, 1, buf, bufsz); + if (strcmp(buf, "0")) { + lgtd_errx( + 1, "UINT16_MAX / 2 converted to %.*s (expected %s)", + bufsz, buf, "0" + ); + } + + lgtd_jsonrpc_uint16_range_to_float_string(0xaaaa, 0, 360, buf, bufsz); + if (strcmp(buf, "240")) { + lgtd_errx( + 1, "0xaaaa converted to %.*s (expected %s)", + bufsz, buf, "240" + ); + } + + bufsz = 2; + lgtd_jsonrpc_uint16_range_to_float_string(UINT16_MAX / 2, 0, 1, buf, bufsz); + if (strcmp(buf, "0")) { + lgtd_errx( + 1, + "UINT16_MAX / 2 converted to %.*s " + "(expected %s in case of overflow)", + bufsz, buf, "0" + ); + } + + bufsz = 1; + lgtd_jsonrpc_uint16_range_to_float_string(UINT16_MAX / 2, 0, 1, buf, bufsz); + if (buf[0]) { + lgtd_errx(1, "buffer of one should be '\\0'"); + } + + buf[0] = 'A'; + lgtd_jsonrpc_uint16_range_to_float_string(UINT16_MAX / 2, 0, 1, buf, 0); + if (buf[0] != 'A') { + lgtd_errx(1, "buffer of zero shouldn't be written to"); + } + + return 0; +} diff --git a/tests/core/jsonrpc/test_jsonrpc_utils.h b/tests/core/jsonrpc/test_jsonrpc_utils.h new file mode 100644 index 0000000..cc488e7 --- /dev/null +++ b/tests/core/jsonrpc/test_jsonrpc_utils.h @@ -0,0 +1,13 @@ +#pragma once + +#include "mock_gateway.h" + +#define TEST_REQUEST_INITIALIZER { NULL, NULL, 0, NULL, 0 } + +static inline int +parse_json(jsmntok_t *tokens, size_t capacity, const char *json , size_t len) +{ + jsmn_parser ctx; + jsmn_init(&ctx); + return jsmn_parse(&ctx, json, len, tokens, capacity); +} diff --git a/tests/core/mock_client_buf.h b/tests/core/mock_client_buf.h new file mode 100644 index 0000000..5dc2754 --- /dev/null +++ b/tests/core/mock_client_buf.h @@ -0,0 +1,37 @@ +#pragma once + +#define MOCKED_BUFFEREVENT_WRITE + +static char client_write_buf[4096] = { 0 }; +static int client_write_buf_idx = 0; + +static inline void +reset_client_write_buf(void) +{ + memset(client_write_buf, 0, sizeof(client_write_buf)); + client_write_buf_idx = 0; +} + +int +bufferevent_write(struct bufferevent *bev, const void *data, size_t nbytes) +{ + (void)bev; + int to_write = LGTD_MIN( + nbytes, sizeof(client_write_buf) - client_write_buf_idx + ); + memcpy(&client_write_buf[client_write_buf_idx], data, to_write); + client_write_buf_idx += to_write; + return 0; +} + +void +lgtd_client_write_string(struct lgtd_client *client, const char *msg) +{ + bufferevent_write(client->io, msg, strlen(msg)); +} + +void +lgtd_client_write_buf(struct lgtd_client *client, const char *buf, int bufsz) +{ + bufferevent_write(client->io, buf, bufsz); +} diff --git a/tests/core/mock_daemon.h b/tests/core/mock_daemon.h new file mode 100644 index 0000000..ce0fdf5 --- /dev/null +++ b/tests/core/mock_daemon.h @@ -0,0 +1,25 @@ +#pragma once + +#ifndef MOCKED_DAEMON_UPDATE_PROCTITLE +void +lgtd_daemon_update_proctitle(void) +{ +} +#endif + +#ifndef MOCKED_DAEMON_MAKEDIRS +bool +lgtd_daemon_makedirs(const char *fp) +{ + (void)fp; + return true; +} +#endif + +#ifndef MOCKED_DAEMON_RANDUINT32 +uint32_t +lgtd_daemon_randuint32(void) +{ + return 0x72616e64; +} +#endif diff --git a/tests/core/mock_event2.h b/tests/core/mock_event2.h new file mode 100644 index 0000000..9af79dd --- /dev/null +++ b/tests/core/mock_event2.h @@ -0,0 +1,212 @@ +#pragma once + +#include +#include +#include + +#define MOCK_EVENT_NEW_EVENT_PTR ((void *)0xdadadada) + +#ifndef MOCKED_EVBUFFER_DRAIN +int +evbuffer_drain(struct evbuffer *buf, size_t len) +{ + (void)buf; + (void)len; + return 0; +} +#endif + +#ifndef MOCKED_EVBUFFER_NEW +struct evbuffer * +evbuffer_new(void) +{ + return NULL; +} +#endif + +#ifndef MOCKED_EVENT_FREE +void +evbuffer_free(struct evbuffer *buf) +{ + (void)buf; +} +#endif + +#ifndef MOCKED_EVBUFFER_GET_LENGTH +size_t +evbuffer_get_length(const struct evbuffer *buf) +{ + (void)buf; + return 0; +} +#endif + +#ifndef MOCKED_EVBUFFER_PULLUP +unsigned char * +evbuffer_pullup(struct evbuffer *buf, ev_ssize_t size) +{ + (void)buf; + (void)size; + return NULL; +} +#endif + +#ifndef MOCKED_EVBUFFER_READ +int +evbuffer_read(struct evbuffer *buffer, evutil_socket_t fd, int howmuch) +{ + (void)buffer; + (void)fd; + return howmuch; +} +#endif + +#ifndef MOCKED_EVBUFFER_GET_CONTIGUOUS_SPACE +size_t +evbuffer_get_contiguous_space(const struct evbuffer *buf) +{ + (void)buf; + return 0; +} +#endif + +#ifndef MOCKED_EVENT_ADD +int +event_add(struct event *ev, const struct timeval *timeout) +{ + (void)ev; + (void)timeout; + return 0; +} +#endif + +#ifndef MOCKED_EVENT_DEL +int +event_del(struct event *ev) +{ + (void)ev; + return 0; +} +#endif + +#ifndef MOCKED_EVENT_FREE +void +event_free(struct event *ev) +{ + (void)ev; +} +#endif + +#ifndef MOCKED_EVENT_NEW +struct event * +event_new(struct event_base *base, + evutil_socket_t fd, + short events, + event_callback_fn cb, + void *ctx) +{ + (void)base; + (void)fd; + (void)events; + (void)cb; + (void)ctx; + return MOCK_EVENT_NEW_EVENT_PTR; +} +#endif + +#ifndef MOCKED_EVENT_ACTIVE +void +event_active(struct event *ev, int res, short ncalls) +{ + (void)ev; + (void)res; + (void)ncalls; +} +#endif + +#ifndef MOCKED_EVENT_PENDING +int +event_pending(const struct event *ev, short events, struct timeval *tv) +{ + (void)ev; + (void)events; + (void)tv; + return 0; +} +#endif + +#ifndef MOCKED_EVUTIL_MAKE_SOCKET_NONBLOCKING +int +evutil_make_socket_nonblocking(evutil_socket_t fd) +{ + (void)fd; + return 0; +} +#endif + +#ifndef MOCKED_BUFFEREVENT_GET_INPUT +struct evbuffer * +bufferevent_get_input(struct bufferevent *bufev) +{ + (void)bufev; + return NULL; +} +#endif + +#ifndef MOCKED_BUFFEREVENT_ENABLE +int +bufferevent_enable(struct bufferevent *bufev, short event) +{ + (void)bufev; + (void)event; + return 0; +} +#endif + +#ifndef MOCKED_BUFFEREVENT_FREE +void +bufferevent_free(struct bufferevent *bufev) +{ + (void)bufev; +} +#endif + +#ifndef MOCKED_BUFFEREVENT_SETCB +void +bufferevent_setcb(struct bufferevent *bufev, + bufferevent_data_cb readcb, + bufferevent_data_cb writecb, + bufferevent_event_cb eventcb, + void *cbarg) +{ + (void)bufev; + (void)readcb; + (void)writecb; + (void)eventcb; + (void)cbarg; +} +#endif + +#ifndef MOCKED_BUFFEREVENT_SOCKET_NEW +struct bufferevent * +bufferevent_socket_new(struct event_base *base, evutil_socket_t fd, int options) +{ + (void)base; + (void)fd; + (void)options; + return NULL; +} +#endif + +#ifndef MOCKED_BUFFEREVENT_WRITE +int +bufferevent_write(struct bufferevent *bufev, + const void *data, + size_t size) +{ + (void)bufev; + (void)data; + (void)size; + return 0; +} +#endif diff --git a/tests/core/mock_jsonrpc.h b/tests/core/mock_jsonrpc.h new file mode 100644 index 0000000..5558411 --- /dev/null +++ b/tests/core/mock_jsonrpc.h @@ -0,0 +1,47 @@ +#pragma once + +#ifndef MOCKED_JSONRPC_DISPATCH_REQUEST +void +lgtd_jsonrpc_dispatch_request(struct lgtd_client *client, int parsed) +{ + (void)client; + (void)parsed; +} +#endif + +#ifndef MOCKED_JSONRPC_SEND_ERROR +void +lgtd_jsonrpc_send_error(struct lgtd_client *client, + enum lgtd_jsonrpc_error_code code, + const char *msg) +{ + (void)client; + (void)code; + (void)msg; +} +#endif + +#ifndef MOCKED_JSONRPC_SEND_RESPONSE +void +lgtd_jsonrpc_send_response(struct lgtd_client *client, const char *msg) +{ + (void)client; + (void)msg; +} +#endif + +#ifndef MOCKED_JSONRPC_START_SEND_RESPONSE +void +lgtd_jsonrpc_start_send_response(struct lgtd_client *client) +{ + (void)client; +} +#endif + +#ifndef MOCKED_JSONRPC_END_SEND_RESPONSE +void +lgtd_jsonrpc_end_send_response(struct lgtd_client *client) +{ + (void)client; +} +#endif diff --git a/tests/core/mock_log.h b/tests/core/mock_log.h new file mode 100644 index 0000000..40599d5 --- /dev/null +++ b/tests/core/mock_log.h @@ -0,0 +1,74 @@ +#pragma once + +#include +#include +#include +#include + +void +lgtd_err(int eval, const char *fmt, ...) +{ + fprintf(stderr, "ERR: "); + va_list ap; + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fprintf(stderr, ": %s\n", strerror(errno)); + exit(eval); +} + +void +lgtd_errx(int eval, const char *fmt, ...) +{ + fprintf(stderr, "ERR: "); + va_list ap; + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fputc('\n', stderr); + exit(eval); +} + +void +lgtd_warn(const char *fmt, ...) +{ + fprintf(stderr, "WARN: "); + va_list ap; + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fprintf(stderr, ": %s\n", strerror(errno)); +} + +void +lgtd_warnx(const char *fmt, ...) +{ + fprintf(stderr, "WARN: "); + va_list ap; + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fputc('\n', stderr); +} + +void +lgtd_info(const char *fmt, ...) +{ + fprintf(stderr, "INFO: "); + va_list ap; + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fputc('\n', stderr); +} + +void +lgtd_debug(const char *fmt, ...) +{ + fprintf(stderr, "DEBUG: "); + va_list ap; + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fputc('\n', stderr); +} diff --git a/tests/core/mock_proto.h b/tests/core/mock_proto.h new file mode 100644 index 0000000..c213508 --- /dev/null +++ b/tests/core/mock_proto.h @@ -0,0 +1,133 @@ +#pragma once + +void +lgtd_proto_target_list_clear(struct lgtd_proto_target_list *targets) +{ + (void)targets; +} + +void +lgtd_proto_list_tags(struct lgtd_client *client) +{ + (void)client; +} + +#ifndef MOCKED_LGTD_PROTO_SET_LIGHT_FROM_HSBK +void +lgtd_proto_set_light_from_hsbk(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets, + int hue, + int saturation, + int brightness, + int kelvin, + int transition_msecs) +{ + (void)client; + (void)targets; + (void)hue; + (void)saturation; + (void)brightness; + (void)kelvin; + (void)transition_msecs; +} +#endif + +#ifndef MOCKED_LGTD_PROTO_POWER_ON +void +lgtd_proto_power_on(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets) +{ + (void)client; + (void)targets; +} +#endif + +#ifndef MOCKED_LGTD_PROTO_POWER_OFF +void +lgtd_proto_power_off(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets) +{ + (void)client; + (void)targets; +} +#endif + +#ifndef MOCKED_LGTD_PROTO_SET_WAVEFORM +void +lgtd_proto_set_waveform(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets, + enum lgtd_lifx_waveform_type waveform, + int hue, int saturation, + int brightness, int kelvin, + int period, float cycles, + int skew_ratio, bool transient) +{ + (void)client; + (void)targets; + (void)waveform; + (void)hue; + (void)saturation; + (void)brightness; + (void)kelvin; + (void)period; + (void)cycles; + (void)skew_ratio; + (void)transient; +} +#endif + +#ifndef MOCKED_LGTD_PROTO_GET_LIGHT_STATE +void +lgtd_proto_get_light_state(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets) +{ + (void)client; + (void)targets; +} +#endif + +#ifndef MOCKED_LGTD_PROTO_TAG +void +lgtd_proto_tag(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets, + const char *tag_label) +{ + (void)client; + (void)targets; + (void)tag_label; +} +#endif + +#ifndef MOCKED_LGTD_PROTO_UNTAG +void +lgtd_proto_untag(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets, + const char *tag_label) +{ + (void)client; + (void)targets; + (void)tag_label; +} +#endif + +#ifndef MOCKED_LGTD_PROTO_POWER_TOGGLE +void +lgtd_proto_power_toggle(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets) +{ + (void)client; + (void)targets; +} +#endif + +#ifndef MOCKED_LGTD_PROTO_SET_LABEL +void +lgtd_proto_set_label(struct lgtd_client *client, + const struct lgtd_proto_target_list *targets, + const char *label) +{ + (void)client; + (void)targets; + (void)label; +} +#endif diff --git a/tests/core/mock_router.h b/tests/core/mock_router.h new file mode 100644 index 0000000..36fcec5 --- /dev/null +++ b/tests/core/mock_router.h @@ -0,0 +1,81 @@ +#pragma once + +#include "lifx/wire_proto.h" // enum lgtd_lifx_packet_type + +struct lgtd_proto_target_list; +struct lgtd_router_device_list; + +#ifndef MOCKED_LGTD_ROUTER_SEND +bool +lgtd_router_send(const struct lgtd_proto_target_list *targets, + enum lgtd_lifx_packet_type pkt_type, + void *pkt) +{ + (void)targets; + (void)pkt_type; + (void)pkt; + return true; +} +#endif + +#ifndef MOCKED_LGTD_ROUTER_SEND_TO_DEVICE +void +lgtd_router_send_to_device(struct lgtd_lifx_bulb *bulb, + enum lgtd_lifx_packet_type pkt_type, + void *pkt) +{ + (void)bulb; + (void)pkt_type; + (void)pkt; +} +#endif + +#ifndef MOCKED_LGTD_ROUTER_SEND_TO_TAG +void +lgtd_router_send_to_tag(const struct lgtd_lifx_tag *tag, + enum lgtd_lifx_packet_type pkt_type, + void *pkt) +{ + (void)tag; + (void)pkt_type; + (void)pkt; +} +#endif + +#ifndef MOCKED_LGTD_ROUTER_SEND_TO_LABEL +void +lgtd_router_send_to_label(const char *label, + enum lgtd_lifx_packet_type pkt_type, + void *pkt) +{ + (void)label; + (void)pkt_type; + (void)pkt; +} +#endif + +#ifndef MOCKED_LGTD_ROUTER_BROADCAST +void +lgtd_router_broadcast(enum lgtd_lifx_packet_type pkt_type, void *pkt) +{ + (void)pkt_type; + (void)pkt; +} +#endif + +#ifndef MOCKED_LGTD_ROUTER_TARGETS_TO_DEVICES +struct lgtd_router_device_list * +lgtd_router_targets_to_devices(const struct lgtd_proto_target_list *targets) +{ + (void)targets; + return NULL; +} +#endif + +#ifndef MOCKED_LGTD_ROUTER_DEVICE_LIST_FREE +void +lgtd_router_device_list_free(struct lgtd_router_device_list *devices) +{ + (void)devices; +} +#endif diff --git a/tests/core/mock_timer.h b/tests/core/mock_timer.h new file mode 100644 index 0000000..d6179be --- /dev/null +++ b/tests/core/mock_timer.h @@ -0,0 +1,33 @@ +#pragma once + +// we need those two because mock_timer.h is being used where it shouldn't be +// because it is a dependency for modules (e.g: bulb) that haven't be mocked +// yet: +#include +#include + +#include "core/timer.h" // to pull the union definition + +#ifndef MOCKED_LGTD_TIMER_START +struct lgtd_timer * +lgtd_timer_start(int flags, + int ms, + void (*cb)(struct lgtd_timer *, + union lgtd_timer_ctx), + union lgtd_timer_ctx ctx) +{ + (void)flags; + (void)ms; + (void)cb; + (void)ctx; + return NULL; +} +#endif + +#ifndef MOCKED_LGTD_TIMER_STOP +void +lgtd_timer_stop(struct lgtd_timer *timer) +{ + (void)timer; +} +#endif diff --git a/tests/core/pipe/CMakeLists.txt b/tests/core/pipe/CMakeLists.txt new file mode 100644 index 0000000..81dd391 --- /dev/null +++ b/tests/core/pipe/CMakeLists.txt @@ -0,0 +1,26 @@ +INCLUDE_DIRECTORIES( + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} +) + +ADD_LIBRARY( + test_core_pipe STATIC + ${LIGHTSD_SOURCE_DIR}/core/jsmn.c + ${LIGHTSD_SOURCE_DIR}/core/stats.c + ${LIGHTSD_SOURCE_DIR}/core/utils.c + ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c + ${LIGHTSD_SOURCE_DIR}/lifx/tagging.c + ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c + ${CMAKE_CURRENT_SOURCE_DIR}/../tests_utils.c +) + +TARGET_LINK_LIBRARIES(test_core_pipe ${TIME_MONOTONIC_LIBRARY}) + +FUNCTION(ADD_PIPE_TEST TEST_SOURCE) + ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_core_pipe) +ENDFUNCTION() + +FILE(GLOB TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test_*.c") +FOREACH(TEST ${TESTS}) + ADD_PIPE_TEST(${TEST}) +ENDFOREACH() diff --git a/tests/core/pipe/test_pipe_close.c b/tests/core/pipe/test_pipe_close.c new file mode 100644 index 0000000..2e996e7 --- /dev/null +++ b/tests/core/pipe/test_pipe_close.c @@ -0,0 +1,120 @@ +#include "pipe.c" + +#include +#include +#include + +#include "lifx/wire_proto.h" + +#include "mock_daemon.h" +#define MOCKED_EVENT_NEW +#define MOCKED_EVBUFFER_NEW +#define MOCKED_EVENT_DEL +#define MOCKED_EVBUFFER_FREE +#define MOCKED_EVENT_FREE +#include "mock_event2.h" +#include "mock_gateway.h" +#include "mock_jsonrpc.h" +#include "mock_log.h" +#include "mock_router.h" +#include "mock_timer.h" + +#include "tests_utils.h" + +char *tmpdir = NULL; + +void +cleanup_tmpdir(void) +{ + lgtd_tests_remove_temp_dir(tmpdir); +} + +struct event * +event_new(struct event_base *base, + evutil_socket_t fd, + short events, + event_callback_fn cb, + void *ctx) +{ + (void)base; + (void)fd; + (void)events; + (void)cb; + (void)ctx; + + return (void *)1; +} + +struct evbuffer * +evbuffer_new(void) +{ + return (void *)2; +} + +static int event_del_call_count = 0; + +int +event_del(struct event *ev) +{ + (void)ev; + + event_del_call_count++; + + return 0; +} + +static int evbuffer_free_call_count = 0; + +void +evbuffer_free(struct evbuffer *buf) +{ + (void)buf; + + evbuffer_free_call_count++; +} + +static int event_free_call_count = 0; + +void +event_free(struct event *ev) +{ + (void)ev; + + event_free_call_count++; +} + +int +main(void) +{ + tmpdir = lgtd_tests_make_temp_dir(); + atexit(cleanup_tmpdir); + + char path[PATH_MAX] = { 0 }; + snprintf(path, sizeof(path), "%s/lightsd.pipe", tmpdir); + if (!lgtd_command_pipe_open(path)) { + errx(1, "couldn't open pipe"); + } + + int pipe_fd = SLIST_FIRST(&lgtd_command_pipes)->fd; + + lgtd_command_pipe_close_all(); + + if (event_del_call_count != 1) { + errx(1, "event_del_call_count = %d", event_del_call_count); + } + if (evbuffer_free_call_count != 1) { + errx(1, "evbuffer_free_call_count = %d", evbuffer_free_call_count); + } + if (event_free_call_count != 1) { + errx(1, "event_free_call_count = %d", event_free_call_count); + } + struct stat sb; + if (fstat(pipe_fd, &sb) != -1 && errno != EBADF) { + errx(1, "the pipe file descriptor wasn't closed correctly"); + } + if (stat(path, &sb) != -1 && errno != ENOENT) { + errx(1, "the pipe wasn't removed correctly"); + } + + return 0; +} diff --git a/tests/core/pipe/test_pipe_open.c b/tests/core/pipe/test_pipe_open.c new file mode 100644 index 0000000..d818b2f --- /dev/null +++ b/tests/core/pipe/test_pipe_open.c @@ -0,0 +1,160 @@ +#include "pipe.c" + +#include +#include +#include + +#include "lifx/wire_proto.h" + +#include "mock_daemon.h" +#define MOCKED_EVUTIL_MAKE_SOCKET_NONBLOCKING +#define MOCKED_EVENT_NEW +#define MOCKED_EVBUFFER_NEW +#define MOCKED_EVENT_ADD +#include "mock_event2.h" +#include "mock_gateway.h" +#include "mock_jsonrpc.h" +#include "mock_log.h" +#include "mock_router.h" +#include "mock_timer.h" + +#include "tests_utils.h" + +char *tmpdir = NULL; + +void +cleanup_tmpdir(void) +{ + lgtd_tests_remove_temp_dir(tmpdir); +} + +static bool make_socket_nonblocking_call_count = 0; + +int +evutil_make_socket_nonblocking(evutil_socket_t fd) +{ + if (fd <= 0) { + errx(1, "got invalid fd %d in make_socket_nonblocking", fd); + } + + make_socket_nonblocking_call_count++; + + return 0; +} + +static int event_new_call_count = 0; + +struct event * +event_new(struct event_base *base, + evutil_socket_t fd, + short events, + event_callback_fn cb, + void *ctx) +{ + if (base != lgtd_ev_base) { + errx( + 1, "unexpected lgtd_ev_base = %p (expected %p)", + base, lgtd_ev_base + ); + } + if (fd <= 0) { + errx(1, "got invalid fd %d in event_new", fd); + } + if (events != (EV_READ|EV_PERSIST)) { + errx(1, "got events %#x (expected %#x)", events, EV_READ|EV_PERSIST); + } + if (cb != lgtd_command_pipe_read_callback) { + errx(1, "the read callback wasn't set correctly"); + } + if (!ctx) { + errx(1, "the callback context wasn't set correctly"); + } + + event_new_call_count++; + + return (void *)1; +} + +static int evbuffer_new_call_count = 0; + +struct evbuffer * +evbuffer_new(void) +{ + evbuffer_new_call_count++; + + return (void *)2; +} + +static int event_add_call_count = 0; + +int +event_add(struct event *ev, const struct timeval *timeout) +{ + if (ev != (void *)1) { + errx(1, "got unexpected event %p (expected %p)", ev, (void*)1); + } + + if (timeout) { + errx(1, "a timeout shouldn't have been passed"); + } + + event_add_call_count++; + + return 0; +} + +int +main(void) +{ + tmpdir = lgtd_tests_make_temp_dir(); + atexit(cleanup_tmpdir); + + char path[PATH_MAX] = { 0 }; + snprintf(path, sizeof(path), "%s/lightsd.pipe", tmpdir); + if (!lgtd_command_pipe_open(path)) { + errx(1, "couldn't open pipe"); + } + + if (make_socket_nonblocking_call_count != 1) { + errx( + 1, "make_socket_nonblocking_call_count = %d", + make_socket_nonblocking_call_count + ); + } + if (event_new_call_count != 1) { + errx(1, "event_new_call_count = %d", event_new_call_count); + } + if (evbuffer_new_call_count != 1) { + errx(1, "evbuffer_new_call_count = %d", evbuffer_new_call_count); + } + if (event_add_call_count != 1) { + errx(1, "event_add_call_count = %d", event_add_call_count); + } + if (SLIST_EMPTY(&lgtd_command_pipes)) { + errx(1, "the list of command pipes shouldn't be empty"); + } + + struct stat sb; + if (stat(path, &sb)) { + errx(1, "can't stat pipe %s", path); + } + + mode_t expected_mode; + expected_mode = S_IFIFO|S_IWUSR|S_IRUSR|S_IXUSR|S_IRGRP|S_IXGRP|S_IWGRP; + if (sb.st_mode != expected_mode) { + errx( + 1, "unexpected mode %o (expected %o)", + sb.st_mode, expected_mode + ); + } + + // make sure it's idempotent: + if (!lgtd_command_pipe_open(path)) { + errx(1, "couldn't open pipe"); + } + if (event_new_call_count != 1) { + errx(1, "event_new_call_count = %d", event_new_call_count); + } + + return 0; +} diff --git a/tests/core/pipe/test_pipe_open_fifo_already_exists.c b/tests/core/pipe/test_pipe_open_fifo_already_exists.c new file mode 100644 index 0000000..29a07bb --- /dev/null +++ b/tests/core/pipe/test_pipe_open_fifo_already_exists.c @@ -0,0 +1,165 @@ +#include "pipe.c" + +#include +#include +#include + +#include "lifx/wire_proto.h" + +#include "mock_daemon.h" +#define MOCKED_EVUTIL_MAKE_SOCKET_NONBLOCKING +#define MOCKED_EVENT_NEW +#define MOCKED_EVBUFFER_NEW +#define MOCKED_EVENT_ADD +#include "mock_event2.h" +#include "mock_gateway.h" +#include "mock_jsonrpc.h" +#include "mock_log.h" +#include "mock_router.h" +#include "mock_timer.h" + +#include "tests_utils.h" + +char *tmpdir = NULL; + +void +cleanup_tmpdir(void) +{ + lgtd_tests_remove_temp_dir(tmpdir); +} + +static bool make_socket_nonblocking_call_count = 0; + +int +evutil_make_socket_nonblocking(evutil_socket_t fd) +{ + if (fd <= 0) { + errx(1, "got invalid fd %d in make_socket_nonblocking", fd); + } + + make_socket_nonblocking_call_count++; + + return 0; +} + +static int event_new_call_count = 0; + +struct event * +event_new(struct event_base *base, + evutil_socket_t fd, + short events, + event_callback_fn cb, + void *ctx) +{ + if (base != lgtd_ev_base) { + errx( + 1, "unexpected lgtd_ev_base = %p (expected %p)", + base, lgtd_ev_base + ); + } + if (fd <= 0) { + errx(1, "got invalid fd %d in event_new", fd); + } + if (events != (EV_READ|EV_PERSIST)) { + errx(1, "got events %#x (expected %#x)", events, EV_READ|EV_PERSIST); + } + if (cb != lgtd_command_pipe_read_callback) { + errx(1, "the read callback wasn't set correctly"); + } + if (!ctx) { + errx(1, "the callback context wasn't set correctly"); + } + + event_new_call_count++; + + return (void *)1; +} + +static int evbuffer_new_call_count = 0; + +struct evbuffer * +evbuffer_new(void) +{ + evbuffer_new_call_count++; + + return (void *)2; +} + +static int event_add_call_count = 0; + +int +event_add(struct event *ev, const struct timeval *timeout) +{ + if (ev != (void *)1) { + errx(1, "got unexpected event %p (expected %p)", ev, (void*)1); + } + + if (timeout) { + errx(1, "a timeout shouldn't have been passed"); + } + + event_add_call_count++; + + return 0; +} + +int +main(void) +{ + tmpdir = lgtd_tests_make_temp_dir(); + atexit(cleanup_tmpdir); + + char path[PATH_MAX] = { 0 }; + snprintf(path, sizeof(path), "%s/lightsd.pipe", tmpdir); + + if (mkfifo(path, 0)) { + errx(1, "can't open fifo"); + } + + if (!lgtd_command_pipe_open(path)) { + errx(1, "couldn't open pipe"); + } + + if (make_socket_nonblocking_call_count != 1) { + errx( + 1, "make_socket_nonblocking_call_count = %d", + make_socket_nonblocking_call_count + ); + } + if (event_new_call_count != 1) { + errx(1, "event_new_call_count = %d", event_new_call_count); + } + if (evbuffer_new_call_count != 1) { + errx(1, "evbuffer_new_call_count = %d", evbuffer_new_call_count); + } + if (event_add_call_count != 1) { + errx(1, "event_add_call_count = %d", event_add_call_count); + } + if (SLIST_EMPTY(&lgtd_command_pipes)) { + errx(1, "the list of command pipes shouldn't be empty"); + } + + struct stat sb; + if (stat(path, &sb)) { + errx(1, "can't stat pipe %s", path); + } + + mode_t expected_mode; + expected_mode = S_IFIFO|S_IWUSR|S_IRUSR|S_IXUSR|S_IRGRP|S_IXGRP|S_IWGRP; + if (sb.st_mode != expected_mode) { + errx( + 1, "unexpected mode %o (expected %o)", + sb.st_mode, expected_mode + ); + } + + // make sure it's idempotent: + if (!lgtd_command_pipe_open(path)) { + errx(1, "couldn't open pipe"); + } + if (event_new_call_count != 1) { + errx(1, "event_new_call_count = %d", event_new_call_count); + } + + return 0; +} diff --git a/tests/core/pipe/test_pipe_read_callback.c b/tests/core/pipe/test_pipe_read_callback.c new file mode 100644 index 0000000..d18cd61 --- /dev/null +++ b/tests/core/pipe/test_pipe_read_callback.c @@ -0,0 +1,214 @@ +#include "pipe.c" + +#include +#include +#include + +#include "lifx/wire_proto.h" + +#include "mock_daemon.h" +#define MOCKED_EVENT_NEW +#define MOCKED_EVENT_DEL +#define MOCKED_EVBUFFER_NEW +#define MOCKED_EVBUFFER_READ +#define MOCKED_EVBUFFER_PULLUP +#define MOCKED_EVBUFFER_GET_LENGTH +#define MOCKED_EVBUFFER_DRAIN +#include "mock_event2.h" +#include "mock_gateway.h" +#define MOCKED_JSONRPC_DISPATCH_REQUEST +#include "mock_jsonrpc.h" +#include "mock_log.h" +#include "mock_router.h" +#include "mock_timer.h" + +#include "tests_utils.h" + +static unsigned char request[] = ("{" + "\"jsonrpc\": \"2.0\"," + "\"method\": \"get_light_state\"," + "\"params\": [\"*\"]," + "\"id\": 42" +"}"); + +static char *tmpdir = NULL; + +void +cleanup_tmpdir(void) +{ + lgtd_tests_remove_temp_dir(tmpdir); +} + +static int jsonrpc_dispatch_request_call_count = 0; + +void +lgtd_jsonrpc_dispatch_request(struct lgtd_client *client, int parsed) +{ + (void)client; + (void)parsed; + + if (!parsed) { + errx(1, "number of parsed json tokens not passed in"); + } + + if (memcmp(client->json, request, sizeof(request))) { + errx(1, "got unexpected json"); + } + + jsonrpc_dispatch_request_call_count++; +} + +struct event * +event_new(struct event_base *base, + evutil_socket_t fd, + short events, + event_callback_fn cb, + void *ctx) +{ + (void)base; + (void)fd; + (void)events; + (void)cb; + (void)ctx; + + return (void *)1; +} + +static int event_del_call_count = 0; + +int +event_del(struct event *ev) +{ + (void)ev; + event_del_call_count++; + return 0; +} + +static int +get_nbytes_read(int call_count) +{ + switch (call_count) { + case 0: + return sizeof(request) - 1; // we don't return the '\0' + default: + return 0; + } +} + +struct evbuffer * +evbuffer_new(void) +{ + return (void *)2; +} + +static int evbuffer_drain_call_count = 0; + +int +evbuffer_drain(struct evbuffer *buf, size_t len) +{ + if (buf != (void *)2) { + errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2); + } + + switch (evbuffer_drain_call_count) { + case 0: + if (len != sizeof(request) - 1) { + errx( + 1, "trying to drain %ju bytes (expected %ju)", + (uintmax_t)len, (uintmax_t)sizeof(request) - 1 + ); + } + break; + default: + break; + } + evbuffer_drain_call_count++; + + return 0; +} + +static int evbuffer_pullup_call_count = 0; + +unsigned char * +evbuffer_pullup(struct evbuffer *buf, ev_ssize_t size) +{ + if (buf != (void *)2) { + errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2); + } + + if (size != -1) { + errx( + 1, "got unexpected size %jd in pullup (expected -1)", (intmax_t)size + ); + } + + return &request[evbuffer_pullup_call_count++ ? sizeof(request) - 1 : 0]; +} + +static int evbuffer_get_length_call_count = 0; + +size_t +evbuffer_get_length(const struct evbuffer *buf) +{ + if (buf != (void *)2) { + errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2); + } + + return get_nbytes_read(evbuffer_get_length_call_count++); +} + +static int evbuffer_read_call_count = 0; + +int +evbuffer_read(struct evbuffer *buf, evutil_socket_t fd, int howmuch) +{ + if (buf != (void *)2) { + errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2); + } + + struct lgtd_command_pipe *pipe = SLIST_FIRST(&lgtd_command_pipes); + if (fd != pipe->fd) { + errx(1, "got unexpected fd %d (expected %d)", fd, pipe->fd); + } + + if (howmuch != -1) { + errx( + 1, "got unexpected howmuch bytes to read %d (expected -1)", howmuch + ); + } + + return get_nbytes_read(evbuffer_read_call_count++); +} + +int +main(void) +{ + tmpdir = lgtd_tests_make_temp_dir(); + atexit(cleanup_tmpdir); + + char path[PATH_MAX] = { 0 }; + snprintf(path, sizeof(path), "%s/lightsd.pipe", tmpdir); + if (!lgtd_command_pipe_open(path)) { + errx(1, "couldn't open pipe"); + } + + struct lgtd_command_pipe *pipe = SLIST_FIRST(&lgtd_command_pipes); + lgtd_command_pipe_read_callback(pipe->fd, EV_READ, pipe); + if (event_del_call_count != 1) { + errx(1, "the pipe wasn't reset"); + } + + jsonrpc_dispatch_request_call_count = 0; + evbuffer_drain_call_count = 0; + evbuffer_read_call_count = 0; + evbuffer_pullup_call_count = 0; + evbuffer_get_length_call_count = 0; + event_del_call_count = 0; + pipe = SLIST_FIRST(&lgtd_command_pipes); + lgtd_command_pipe_read_callback(pipe->fd, EV_READ, pipe); + if (event_del_call_count != 1) { + errx(1, "the pipe wasn't reset"); + } + + return 0; +} diff --git a/tests/core/pipe/test_pipe_read_callback_extra_data.c b/tests/core/pipe/test_pipe_read_callback_extra_data.c new file mode 100644 index 0000000..d7399df --- /dev/null +++ b/tests/core/pipe/test_pipe_read_callback_extra_data.c @@ -0,0 +1,217 @@ +#include "pipe.c" + +#include +#include +#include + +#include "lifx/wire_proto.h" + +#include "mock_daemon.h" +#define MOCKED_EVENT_NEW +#define MOCKED_EVBUFFER_NEW +#define MOCKED_EVBUFFER_READ +#define MOCKED_EVBUFFER_PULLUP +#define MOCKED_EVBUFFER_GET_LENGTH +#define MOCKED_EVBUFFER_DRAIN +#include "mock_event2.h" +#include "mock_gateway.h" +#define MOCKED_JSONRPC_DISPATCH_REQUEST +#include "mock_jsonrpc.h" +#include "mock_log.h" +#include "mock_router.h" +#include "mock_timer.h" + +#include "tests_utils.h" + +#define REQUEST_1 "{" \ + "\"jsonrpc\": \"2.0\"," \ + "\"method\": \"get_light_state\"," \ + "\"params\": [\"*\"]," \ + "\"id\": 42" \ +"}" +#define EXTRA_DATA "BLUBLBULBUBUHIFESHFUSsoundsaboutright" + +static unsigned char request[] = ( + REQUEST_1 + EXTRA_DATA +); + +static char *tmpdir = NULL; + +void +cleanup_tmpdir(void) +{ + lgtd_tests_remove_temp_dir(tmpdir); +} + +static int jsonrpc_dispatch_request_call_count = 0; + +void +lgtd_jsonrpc_dispatch_request(struct lgtd_client *client, int parsed) +{ + (void)client; + (void)parsed; + + if (!parsed) { + errx(1, "number of parsed json tokens not passed in"); + } + + if (memcmp(client->json, request, sizeof(request))) { + errx(1, "got unexpected json"); + } + + jsonrpc_dispatch_request_call_count++; +} + +struct event * +event_new(struct event_base *base, + evutil_socket_t fd, + short events, + event_callback_fn cb, + void *ctx) +{ + (void)base; + (void)fd; + (void)events; + (void)cb; + (void)ctx; + + return (void *)1; +} + +static int +get_nbytes_read(int call_count) +{ + switch (call_count) { + case 0: + return sizeof(request) - 1; // we don't return the '\0' + default: + return 0; + } +} + +struct evbuffer * +evbuffer_new(void) +{ + return (void *)2; +} + +static int evbuffer_drain_call_count = 0; + +int +evbuffer_drain(struct evbuffer *buf, size_t len) +{ + if (buf != (void *)2) { + errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2); + } + + switch (evbuffer_drain_call_count) { + case 0: + if (len != sizeof(request) - sizeof(REQUEST_1)) { + errx( + 1, "trying to drain %ju bytes (expected %ju)", + (uintmax_t)len, (uintmax_t)(sizeof(request) - sizeof(REQUEST_1)) + ); + } + break; + case 1: + if (len != sizeof(REQUEST_1) - 1) { + errx( + 1, "trying to drain %ju bytes (expected %ju)", + (uintmax_t)len, (uintmax_t)sizeof(request) - 1 + ); + } + break; + default: + break; + } + evbuffer_drain_call_count++; + + return 0; +} + +static int evbuffer_pullup_call_count = 0; + +unsigned char * +evbuffer_pullup(struct evbuffer *buf, ev_ssize_t size) +{ + if (buf != (void *)2) { + errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2); + } + + if (size != -1) { + errx( + 1, "got unexpected size %jd in pullup (expected -1)", (intmax_t)size + ); + } + + return &request[evbuffer_pullup_call_count++ ? sizeof(request) - 1 : 0]; +} + +static int evbuffer_get_length_call_count = 0; + +size_t +evbuffer_get_length(const struct evbuffer *buf) +{ + if (buf != (void *)2) { + errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2); + } + + size_t len; + switch (evbuffer_get_length_call_count) { + case 0: + len = sizeof(request) - 1; + break; + case 1: + len = sizeof(request) - sizeof(REQUEST_1); + break; + default: + len = 0; + break; + } + evbuffer_get_length_call_count++; + + return len; +} + +static int evbuffer_read_call_count = 0; + +int +evbuffer_read(struct evbuffer *buf, evutil_socket_t fd, int howmuch) +{ + if (buf != (void *)2) { + errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2); + } + + struct lgtd_command_pipe *pipe = SLIST_FIRST(&lgtd_command_pipes); + if (fd != pipe->fd) { + errx(1, "got unexpected fd %d (expected %d)", fd, pipe->fd); + } + + if (howmuch != -1) { + errx( + 1, "got unexpected howmuch bytes to read %d (expected -1)", howmuch + ); + } + + return get_nbytes_read(evbuffer_read_call_count++); +} + +int +main(void) +{ + tmpdir = lgtd_tests_make_temp_dir(); + atexit(cleanup_tmpdir); + + char path[PATH_MAX] = { 0 }; + snprintf(path, sizeof(path), "%s/lightsd.pipe", tmpdir); + if (!lgtd_command_pipe_open(path)) { + errx(1, "couldn't open pipe"); + } + + struct lgtd_command_pipe *pipe = SLIST_FIRST(&lgtd_command_pipes); + + lgtd_command_pipe_read_callback(pipe->fd, EV_READ, pipe); + + return 0; +} diff --git a/tests/core/pipe/test_pipe_read_callback_multiple_requests.c b/tests/core/pipe/test_pipe_read_callback_multiple_requests.c new file mode 100644 index 0000000..0f983a9 --- /dev/null +++ b/tests/core/pipe/test_pipe_read_callback_multiple_requests.c @@ -0,0 +1,257 @@ +#include "pipe.c" + +#include +#include +#include + +#include "lifx/wire_proto.h" + +#include "mock_daemon.h" +#define MOCKED_EVENT_NEW +#define MOCKED_EVBUFFER_NEW +#define MOCKED_EVBUFFER_READ +#define MOCKED_EVBUFFER_PULLUP +#define MOCKED_EVBUFFER_GET_LENGTH +#define MOCKED_EVBUFFER_DRAIN +#include "mock_event2.h" +#include "mock_gateway.h" +#define MOCKED_JSONRPC_DISPATCH_REQUEST +#include "mock_jsonrpc.h" +#include "mock_log.h" +#include "mock_router.h" +#include "mock_timer.h" + +#include "tests_utils.h" + +#define REQUEST_1 "{" \ + "\"jsonrpc\": \"2.0\"," \ + "\"method\": \"get_light_state\"," \ + "\"params\": [\"*\"]," \ + "\"id\": 42" \ +"}" + +#define REQUEST_2 "{" \ + "\"jsonrpc\": \"2.0\"," \ + "\"method\": \"power_on\"," \ + "\"params\": [\"*\"]," \ + "\"id\": 43" \ +"}" + +static unsigned char request[] = ( + REQUEST_1 + REQUEST_2 +); + +static char *tmpdir = NULL; + +void +cleanup_tmpdir(void) +{ + lgtd_tests_remove_temp_dir(tmpdir); +} + +static int jsonrpc_dispatch_request_call_count = 0; + +void +lgtd_jsonrpc_dispatch_request(struct lgtd_client *client, int parsed) +{ + (void)client; + (void)parsed; + + if (!parsed) { + errx(1, "number of parsed json tokens not passed in"); + } + + switch (jsonrpc_dispatch_request_call_count) { + case 0: + if (memcmp(client->json, request, sizeof(request) - 1)) { + errx( + 1, "got unexpected json %s (expected %s)", + client->json, request + ); + } + break; + case 1: + if (memcmp(client->json, REQUEST_2, sizeof(REQUEST_2) - 1)) { + errx( + 1, "got unexpected json %s (expected %s)", + client->json, REQUEST_2 + ); + } + break; + default: + errx( + 1, "jsonrpc_dispatch_request_call_count = %d", + jsonrpc_dispatch_request_call_count + ); + break; + } + + jsonrpc_dispatch_request_call_count++; +} + +struct event * +event_new(struct event_base *base, + evutil_socket_t fd, + short events, + event_callback_fn cb, + void *ctx) +{ + (void)base; + (void)fd; + (void)events; + (void)cb; + (void)ctx; + + return (void *)1; +} + +struct evbuffer * +evbuffer_new(void) +{ + return (void *)2; +} + +static int evbuffer_drain_call_count = 0; + +int +evbuffer_drain(struct evbuffer *buf, size_t len) +{ + if (buf != (void *)2) { + errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2); + } + + switch (evbuffer_drain_call_count) { + case 0: + if (len != sizeof(REQUEST_1) - 1) { + errx( + 1, "trying to drain %ju bytes (expected %ju)", + (uintmax_t)len, (uintmax_t)sizeof(REQUEST_1) - 1 + ); + } + break; + case 1: + if (len != sizeof(REQUEST_2) - 1) { + errx( + 1, "trying to drain %ju bytes (expected %ju)", + (uintmax_t)len, (uintmax_t)sizeof(REQUEST_2) - 1 + ); + } + break; + default: + errx(1, "evbuffer_drain_call_count = %d", evbuffer_drain_call_count); + break; + } + evbuffer_drain_call_count++; + + return 0; +} + +static int evbuffer_pullup_call_count = 0; + +unsigned char * +evbuffer_pullup(struct evbuffer *buf, ev_ssize_t size) +{ + if (buf != (void *)2) { + errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2); + } + + if (size != -1) { + errx( + 1, "got unexpected size %jd in pullup (expected -1)", (intmax_t)size + ); + } + + int offset; + switch (evbuffer_pullup_call_count) { + case 0: + offset = 0; + break; + case 1: + offset = sizeof(REQUEST_1) - 1; + break; + default: + offset = sizeof(request); + break; + } + evbuffer_pullup_call_count++; + + return &request[offset]; +} + +static int evbuffer_get_length_call_count = 0; + +size_t +evbuffer_get_length(const struct evbuffer *buf) +{ + if (buf != (void *)2) { + errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2); + } + + size_t len; + switch (evbuffer_get_length_call_count) { + case 0: + len = sizeof(request) - 1; + break; + case 1: + len = sizeof(request) - sizeof(REQUEST_1); + break; + default: + len = 0; + break; + } + evbuffer_get_length_call_count++; + + return len; +} + +static int evbuffer_read_call_count = 0; + +int +evbuffer_read(struct evbuffer *buf, evutil_socket_t fd, int howmuch) +{ + if (buf != (void *)2) { + errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2); + } + + struct lgtd_command_pipe *pipe = SLIST_FIRST(&lgtd_command_pipes); + if (fd != pipe->fd) { + errx(1, "got unexpected fd %d (expected %d)", fd, pipe->fd); + } + + if (howmuch != -1) { + errx( + 1, "got unexpected howmuch bytes to read %d (expected -1)", howmuch + ); + } + + int rv = 0; + switch (evbuffer_read_call_count) { + case 0: + rv = sizeof(request) - 1; + default: + break; + } + evbuffer_read_call_count++; + + return rv; +} + +int +main(void) +{ + tmpdir = lgtd_tests_make_temp_dir(); + atexit(cleanup_tmpdir); + + char path[PATH_MAX] = { 0 }; + snprintf(path, sizeof(path), "%s/lightsd.pipe", tmpdir); + if (!lgtd_command_pipe_open(path)) { + errx(1, "couldn't open pipe"); + } + + struct lgtd_command_pipe *pipe = SLIST_FIRST(&lgtd_command_pipes); + + lgtd_command_pipe_read_callback(pipe->fd, EV_READ, pipe); + + return 0; +} diff --git a/tests/core/pipe/test_pipe_read_callback_yield_on_eagain.c b/tests/core/pipe/test_pipe_read_callback_yield_on_eagain.c new file mode 100644 index 0000000..93b2bc8 --- /dev/null +++ b/tests/core/pipe/test_pipe_read_callback_yield_on_eagain.c @@ -0,0 +1,266 @@ +#include "pipe.c" + +#include +#include +#include + +#include "lifx/wire_proto.h" + +#include "mock_daemon.h" +#define MOCKED_EVENT_NEW +#define MOCKED_EVBUFFER_NEW +#define MOCKED_EVBUFFER_READ +#define MOCKED_EVBUFFER_PULLUP +#define MOCKED_EVBUFFER_GET_LENGTH +#define MOCKED_EVBUFFER_DRAIN +#include "mock_event2.h" +#include "mock_gateway.h" +#define MOCKED_JSONRPC_DISPATCH_REQUEST +#include "mock_jsonrpc.h" +#include "mock_log.h" +#include "mock_router.h" +#include "mock_timer.h" + +#include "tests_utils.h" + +#define REQUEST_1 "{" \ + "\"jsonrpc\": \"2.0\"," \ + "\"method\": \"get_light_state\"," \ + "\"params\": [\"*\"]," \ + "\"id\": 42" \ +"}" + +#define REQUEST_2 "{" \ + "\"jsonrpc\": \"2.0\"," \ + "\"method\": \"power_on\"," \ + "\"params\": [\"*\"]," \ + "\"id\": 43" \ +"}" + +static unsigned char request[] = ( + REQUEST_1 + REQUEST_2 +); + +static char *tmpdir = NULL; + +void +cleanup_tmpdir(void) +{ + lgtd_tests_remove_temp_dir(tmpdir); +} + +static int jsonrpc_dispatch_request_call_count = 0; + +void +lgtd_jsonrpc_dispatch_request(struct lgtd_client *client, int parsed) +{ + (void)client; + (void)parsed; + + if (!parsed) { + errx(1, "number of parsed json tokens not passed in"); + } + + switch (jsonrpc_dispatch_request_call_count) { + case 0: + if (memcmp(client->json, request, sizeof(request) - 1)) { + errx( + 1, "got unexpected json %s (expected %s)", + client->json, request + ); + } + break; + case 1: + if (memcmp(client->json, REQUEST_2, sizeof(REQUEST_2) - 1)) { + errx( + 1, "got unexpected json %s (expected %s)", + client->json, REQUEST_2 + ); + } + break; + default: + errx( + 1, "jsonrpc_dispatch_request_call_count = %d", + jsonrpc_dispatch_request_call_count + ); + break; + } + + jsonrpc_dispatch_request_call_count++; +} + +struct event * +event_new(struct event_base *base, + evutil_socket_t fd, + short events, + event_callback_fn cb, + void *ctx) +{ + (void)base; + (void)fd; + (void)events; + (void)cb; + (void)ctx; + + return (void *)1; +} + +struct evbuffer * +evbuffer_new(void) +{ + return (void *)2; +} + +static int evbuffer_drain_call_count = 0; + +int +evbuffer_drain(struct evbuffer *buf, size_t len) +{ + if (buf != (void *)2) { + errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2); + } + + switch (evbuffer_drain_call_count) { + case 0: + if (len != sizeof(REQUEST_1) - 1) { + errx( + 1, "trying to drain %ju bytes (expected %ju)", + (uintmax_t)len, (uintmax_t)sizeof(REQUEST_1) - 1 + ); + } + break; + case 1: + if (len != sizeof(REQUEST_2) - 1) { + errx( + 1, "trying to drain %ju bytes (expected %ju)", + (uintmax_t)len, (uintmax_t)sizeof(REQUEST_2) - 1 + ); + } + break; + default: + errx(1, "evbuffer_drain_call_count = %d", evbuffer_drain_call_count); + break; + } + evbuffer_drain_call_count++; + + return 0; +} + +static int evbuffer_pullup_call_count = 0; + +unsigned char * +evbuffer_pullup(struct evbuffer *buf, ev_ssize_t size) +{ + if (buf != (void *)2) { + errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2); + } + + if (size != -1) { + errx( + 1, "got unexpected size %jd in pullup (expected -1)", (intmax_t)size + ); + } + + int offset; + switch (evbuffer_pullup_call_count) { + case 0: + offset = 0; + break; + case 1: + offset = sizeof(REQUEST_1) - 1; + break; + default: + offset = sizeof(request); + break; + } + evbuffer_pullup_call_count++; + + return &request[offset]; +} + +static int evbuffer_get_length_call_count = 0; + +size_t +evbuffer_get_length(const struct evbuffer *buf) +{ + if (buf != (void *)2) { + errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2); + } + + size_t len; + switch (evbuffer_get_length_call_count) { + case 0: + len = sizeof(REQUEST_1) - 1; + break; + case 1: + len = sizeof(request) - sizeof(REQUEST_1); + break; + default: + len = 0; + break; + } + evbuffer_get_length_call_count++; + + return len; +} + +static int evbuffer_read_call_count = 0; + +int +evbuffer_read(struct evbuffer *buf, evutil_socket_t fd, int howmuch) +{ + if (buf != (void *)2) { + errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2); + } + + struct lgtd_command_pipe *pipe = SLIST_FIRST(&lgtd_command_pipes); + if (fd != pipe->fd) { + errx(1, "got unexpected fd %d (expected %d)", fd, pipe->fd); + } + + if (howmuch != -1) { + errx( + 1, "got unexpected howmuch bytes to read %d (expected -1)", howmuch + ); + } + + int rv = 0; + switch (evbuffer_read_call_count) { + case 0: + rv = sizeof(REQUEST_1) - 1; + break; + case 1: + rv = -1; + errno = EAGAIN; + break; + case 2: + rv = sizeof(request) - sizeof(REQUEST_1); + break; + default: + break; + } + evbuffer_read_call_count++; + + return rv; +} + +int +main(void) +{ + tmpdir = lgtd_tests_make_temp_dir(); + atexit(cleanup_tmpdir); + + char path[PATH_MAX] = { 0 }; + snprintf(path, sizeof(path), "%s/lightsd.pipe", tmpdir); + if (!lgtd_command_pipe_open(path)) { + errx(1, "couldn't open pipe"); + } + + struct lgtd_command_pipe *pipe = SLIST_FIRST(&lgtd_command_pipes); + + lgtd_command_pipe_read_callback(pipe->fd, EV_READ, pipe); + lgtd_command_pipe_read_callback(pipe->fd, EV_READ, pipe); + + return 0; +} diff --git a/tests/core/proto/CMakeLists.txt b/tests/core/proto/CMakeLists.txt new file mode 100644 index 0000000..d0a3d51 --- /dev/null +++ b/tests/core/proto/CMakeLists.txt @@ -0,0 +1,24 @@ +INCLUDE_DIRECTORIES( + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} +) + +ADD_CORE_LIBRARY( + test_core_proto STATIC + ${LIGHTSD_SOURCE_DIR}/core/jsonrpc.c + ${LIGHTSD_SOURCE_DIR}/core/stats.c + ${LIGHTSD_SOURCE_DIR}/core/utils.c + ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c + ${LIGHTSD_SOURCE_DIR}/lifx/tagging.c + ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c + ${CMAKE_CURRENT_SOURCE_DIR}/../tests_utils.c +) + +FUNCTION(ADD_PROTO_TEST TEST_SOURCE) + ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_core_proto) +ENDFUNCTION() + +FILE(GLOB TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test_*.c") +FOREACH(TEST ${TESTS}) + ADD_PROTO_TEST(${TEST}) +ENDFOREACH() diff --git a/tests/core/proto/test_proto_get_light_state.c b/tests/core/proto/test_proto_get_light_state.c new file mode 100644 index 0000000..b1fdced --- /dev/null +++ b/tests/core/proto/test_proto_get_light_state.c @@ -0,0 +1,291 @@ +#include "proto.c" + +#include "mock_client_buf.h" +#include "mock_daemon.h" +#include "mock_gateway.h" +#include "mock_event2.h" +#include "mock_log.h" +#include "mock_timer.h" +#include "mock_wire_proto.h" +#include "tests_utils.h" + +#define MOCKED_ROUTER_TARGETS_TO_DEVICES +#define MOCKED_ROUTER_DEVICE_LIST_FREE +#include "tests_proto_utils.h" + +static bool device_list_free_called = false; + +void +lgtd_router_device_list_free(struct lgtd_router_device_list *devices) +{ + if (!devices) { + lgtd_errx(1, "the device list must be passed"); + } + + device_list_free_called = true; +} + +struct lgtd_router_device_list * +lgtd_router_targets_to_devices(const struct lgtd_proto_target_list *targets) +{ + if (targets != (void *)0x2a) { + lgtd_errx(1, "unexpected targets list"); + } + + static struct lgtd_router_device_list devices = + SLIST_HEAD_INITIALIZER(&devices); + if (SLIST_FIRST(&devices)) { + return &devices; + } + + static struct lgtd_lifx_gateway gw_bulb_1 = { + .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_1.bulbs), + .peeraddr = "[::ffff:127.0.0.1]:1" + }; + static struct lgtd_lifx_bulb bulb_1 = { + .addr = { 1, 2, 3, 4, 5 }, + .state = { + .hue = 0xaaaa, + .saturation = 0xffff, + .brightness = 0xbbbb, + .kelvin = 3600, + .label = "wave", + .power = LGTD_LIFX_POWER_ON, + .tags = 0 + }, + .ips[LGTD_LIFX_BULB_WIFI_IP] = { + .fw_info.version = 0x10001 + }, + .model = "testbulb", + .product_info = { + .vendor_id = 1, + .product_id = 0xa, + .version = 9 + }, + .gw = &gw_bulb_1 + }; + static struct lgtd_router_device device_1 = { .device = &bulb_1 }; + SLIST_INSERT_HEAD(&devices, &device_1, link); + + struct lgtd_lifx_tag *gw_2_tag_1 = lgtd_tests_insert_mock_tag("vapor"); + struct lgtd_lifx_tag *gw_2_tag_2 = lgtd_tests_insert_mock_tag("d^-^b"); + struct lgtd_lifx_tag *gw_2_tag_3 = lgtd_tests_insert_mock_tag("wave~"); + static struct lgtd_lifx_gateway gw_bulb_2 = { + .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_2.bulbs), + .tag_ids = 0x7, + .peeraddr = "[::ffff:127.0.0.1]:2" + }; + lgtd_tests_add_tag_to_gw(gw_2_tag_1, &gw_bulb_2, 0); + lgtd_tests_add_tag_to_gw(gw_2_tag_2, &gw_bulb_2, 1); + lgtd_tests_add_tag_to_gw(gw_2_tag_3, &gw_bulb_2, 2); + static struct lgtd_lifx_bulb bulb_2 = { + .addr = { 5, 4, 3, 2, 1 }, + .state = { + .hue = 0x0000, + .saturation = 0x0000, + .brightness = 0xffff, + .kelvin = 4000, + .label = "", + .power = LGTD_LIFX_POWER_OFF, + .tags = 0x3 + }, + .ips[LGTD_LIFX_BULB_MCU_IP] = { + .fw_info.version = 0x20001 + }, + .vendor = "martine", + .runtime_info = { + .uptime = 42E9, + .downtime = 1337E9 + }, + .gw = &gw_bulb_2 + }; + static struct lgtd_router_device device_2 = { .device = &bulb_2 }; + SLIST_INSERT_HEAD(&devices, &device_2, link); + + return &devices; +} + +int +main(void) +{ + struct lgtd_client *client; + client = lgtd_tests_insert_mock_client(FAKE_BUFFEREVENT); + struct lgtd_proto_target_list *targets = (void *)0x2a; + + lgtd_proto_get_light_state(client, targets); + + const char expected[] = ("[" + "{" + "\"_lifx\":{" + "\"addr\":\"05:04:03:02:01:00\"," + "\"gateway\":{" + "\"site\":\"00:00:00:00:00:00\"," + "\"url\":\"tcp://[::ffff:127.0.0.1]:2\"," + "\"latency\":0" + "}," + "\"mcu\":{" + "\"firmware_built_at\":\"1970-01-01T00:00:00+00:00\"," + "\"firmware_installed_at\":\"1970-01-01T00:00:00+00:00\"," + "\"firmware_version\":\"2.1\"," + "\"signal_strength\":0.000000," + "\"tx_bytes\":0," + "\"rx_bytes\":0," + "\"temperature\":0" + "}," + "\"wifi\":{" + "\"firmware_built_at\":\"1970-01-01T00:00:00+00:00\"," + "\"firmware_installed_at\":\"1970-01-01T00:00:00+00:00\"," + "\"firmware_version\":\"0.0\"," + "\"signal_strength\":0.000000," + "\"tx_bytes\":0," + "\"rx_bytes\":0," + "\"temperature\":0" + "}," + "\"product_info\":{" + "\"vendor_id\":\"0\"," + "\"product_id\":\"0\"," + "\"version\":0" + "}," + "\"runtime_info\":{" + "\"time\":\"1970-01-01T00:00:00+00:00\"," + "\"uptime\":42," + "\"downtime\":1337" + "}" + "}," + "\"_model\":null," + "\"_vendor\":\"martine\"," + "\"hsbk\":[0,0,1,4000]," + "\"power\":false," + "\"label\":\"05:04:03:02:01:00\"," + "\"tags\":[\"vapor\",\"d^-^b\"]" + "},{" + "\"_lifx\":{" + "\"addr\":\"01:02:03:04:05:00\"," + "\"gateway\":{" + "\"site\":\"00:00:00:00:00:00\"," + "\"url\":\"tcp://[::ffff:127.0.0.1]:1\"," + "\"latency\":0" + "}," + "\"mcu\":{" + "\"firmware_built_at\":\"1970-01-01T00:00:00+00:00\"," + "\"firmware_installed_at\":\"1970-01-01T00:00:00+00:00\"," + "\"firmware_version\":\"0.0\"," + "\"signal_strength\":0.000000," + "\"tx_bytes\":0," + "\"rx_bytes\":0," + "\"temperature\":0" + "}," + "\"wifi\":{" + "\"firmware_built_at\":\"1970-01-01T00:00:00+00:00\"," + "\"firmware_installed_at\":\"1970-01-01T00:00:00+00:00\"," + "\"firmware_version\":\"1.1\"," + "\"signal_strength\":0.000000," + "\"tx_bytes\":0," + "\"rx_bytes\":0," + "\"temperature\":0" + "}," + "\"product_info\":{" + "\"vendor_id\":\"1\"," + "\"product_id\":\"a\"," + "\"version\":9" + "}," + "\"runtime_info\":{" + "\"time\":\"1970-01-01T00:00:00+00:00\"," + "\"uptime\":0," + "\"downtime\":0" + "}" + "}," + "\"_model\":\"testbulb\"," + "\"_vendor\":null," + "\"hsbk\":[240,1,0.733333,3600]," + "\"power\":true," + "\"label\":\"wave\"," + "\"tags\":[]" + "}" + "]"); + + if (client_write_buf_idx != sizeof(expected) - 1) { + lgtd_errx( + 1, + "%d bytes written, expected %lu " + "(got %.*s instead of %s)", + client_write_buf_idx, sizeof(expected) - 1UL, + client_write_buf_idx, client_write_buf, expected + ); + } + + if (memcmp(expected, client_write_buf, sizeof(expected) - 1)) { + lgtd_errx( + 1, "got %.*s instead of %s", + client_write_buf_idx, client_write_buf, expected + ); + } + + if (!device_list_free_called) { + lgtd_errx(1, "the list of devices hasn't been freed"); + } + + lgtd_opts.verbosity = LGTD_INFO; + + char expected_info[] = ("[" + "{" + "\"_lifx\":{" + "\"addr\":\"05:04:03:02:01:00\"," + "\"gateway\":{" + "\"site\":\"00:00:00:00:00:00\"," + "\"url\":\"tcp://[::ffff:127.0.0.1]:2\"," + "\"latency\":0" + "}," + "\"mcu\":{\"firmware_version\":\"2.1\"}," + "\"wifi\":{\"firmware_version\":\"0.0\"}" + "}," + "\"_model\":null," + "\"_vendor\":\"martine\"," + "\"hsbk\":[0,0,1,4000]," + "\"power\":false," + "\"label\":\"05:04:03:02:01:00\"," + "\"tags\":[\"vapor\",\"d^-^b\"]" + "}," + "{" + "\"_lifx\":{" + "\"addr\":\"01:02:03:04:05:00\"," + "\"gateway\":{" + "\"site\":\"00:00:00:00:00:00\"," + "\"url\":\"tcp://[::ffff:127.0.0.1]:1\"," + "\"latency\":0" + "}," + "\"mcu\":{\"firmware_version\":\"0.0\"}," + "\"wifi\":{\"firmware_version\":\"1.1\"}" + "}," + "\"_model\":\"testbulb\"," + "\"_vendor\":null," + "\"hsbk\":[240,1,0.733333,3600]," + "\"power\":true," + "\"label\":\"wave\"," + "\"tags\":[]" + "}" + "]"); + + reset_client_write_buf(); + + lgtd_proto_get_light_state(client, targets); + + if (client_write_buf_idx != sizeof(expected_info) - 1) { + lgtd_errx( + 1, + "%d bytes written, expected_info %lu " + "(got %.*s instead of %s)", + client_write_buf_idx, sizeof(expected_info) - 1UL, + client_write_buf_idx, client_write_buf, expected_info + ); + } + + if (memcmp(expected_info, client_write_buf, sizeof(expected_info) - 1)) { + lgtd_errx( + 1, "got %.*s instead of %s", + client_write_buf_idx, client_write_buf, expected_info + ); + } + + return 0; +} diff --git a/tests/core/proto/test_proto_get_light_state_empty_device_list.c b/tests/core/proto/test_proto_get_light_state_empty_device_list.c new file mode 100644 index 0000000..01c20a0 --- /dev/null +++ b/tests/core/proto/test_proto_get_light_state_empty_device_list.c @@ -0,0 +1,66 @@ +#include "proto.c" + +#include "mock_client_buf.h" +#include "mock_daemon.h" +#include "mock_event2.h" +#include "mock_log.h" +#include "mock_timer.h" +#include "mock_wire_proto.h" +#include "tests_utils.h" + +#define MOCKED_ROUTER_TARGETS_TO_DEVICES +#define MOCKED_ROUTER_DEVICE_LIST_FREE +#include "tests_proto_utils.h" + +static bool device_list_free_called = false; + +void +lgtd_router_device_list_free(struct lgtd_router_device_list *devices) +{ + if (!devices) { + lgtd_errx(1, "the device list must be passed"); + } + + device_list_free_called = true; +} + +struct lgtd_router_device_list * +lgtd_router_targets_to_devices(const struct lgtd_proto_target_list *targets) +{ + if (targets != (void *)0x2a) { + lgtd_errx(1, "unexpected targets list"); + } + + static struct lgtd_router_device_list devices = + SLIST_HEAD_INITIALIZER(&devices); + + return &devices; +} + +int +main(void) +{ + struct lgtd_client *client; + client = lgtd_tests_insert_mock_client(FAKE_BUFFEREVENT); + struct lgtd_proto_target_list *targets = (void *)0x2a; + + lgtd_proto_get_light_state(client, targets); + + const char expected[] = "[]"; + + if (client_write_buf_idx != sizeof(expected) - 1) { + lgtd_errx( + 1, "%d bytes written, expected %lu", + client_write_buf_idx, sizeof(expected) - 1UL + ); + } + + if (memcmp(expected, client_write_buf, sizeof(expected) - 1)) { + lgtd_errx( + 1, "got %.*s instead of %s", + client_write_buf_idx, client_write_buf, expected + ); + } + + return 0; +} diff --git a/tests/core/proto/test_proto_get_light_state_label_overflow.c b/tests/core/proto/test_proto_get_light_state_label_overflow.c new file mode 100644 index 0000000..7e0e13c --- /dev/null +++ b/tests/core/proto/test_proto_get_light_state_label_overflow.c @@ -0,0 +1,125 @@ +#include "proto.c" + +#include "mock_client_buf.h" +#include "mock_daemon.h" +#include "mock_gateway.h" +#include "mock_event2.h" +#include "mock_log.h" +#include "mock_timer.h" +#include "mock_wire_proto.h" +#include "tests_utils.h" + +#define MOCKED_ROUTER_TARGETS_TO_DEVICES +#define MOCKED_ROUTER_DEVICE_LIST_FREE +#include "tests_proto_utils.h" + +static bool device_list_free_called = false; + +void +lgtd_router_device_list_free(struct lgtd_router_device_list *devices) +{ + if (!devices) { + lgtd_errx(1, "the device list must be passed"); + } + + device_list_free_called = true; +} + +struct lgtd_router_device_list * +lgtd_router_targets_to_devices(const struct lgtd_proto_target_list *targets) +{ + if (targets != (void *)0x2a) { + lgtd_errx(1, "unexpected targets list"); + } + + static struct lgtd_router_device_list devices = + SLIST_HEAD_INITIALIZER(&devices); + if (SLIST_FIRST(&devices)) { + return &devices; + } + + static struct lgtd_lifx_gateway gw_bulb_1 = { + .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_1.bulbs), + .peeraddr = "[::ffff:127.0.0.1]:1" + }; + static struct lgtd_lifx_bulb bulb_1 = { + .addr = { 1, 2, 3, 4, 5 }, + .state = { + .hue = 0xaaaa, + .saturation = 0xffff, + .brightness = 0xbbbb, + .kelvin = 3600, + .power = LGTD_LIFX_POWER_ON, + .tags = 0 + }, + .ips[LGTD_LIFX_BULB_WIFI_IP] = { + .fw_info.version = 0x10001 + }, + .model = "testbulb", + .product_info = { + .vendor_id = 1, + .product_id = 0xa, + .version = 9 + }, + .gw = &gw_bulb_1 + }; + memset(&bulb_1.state.label, 'a', sizeof(bulb_1.state.label)); + static struct lgtd_router_device device_1 = { .device = &bulb_1 }; + SLIST_INSERT_HEAD(&devices, &device_1, link); + + return &devices; +} + +int +main(void) +{ + struct lgtd_client *client; + client = lgtd_tests_insert_mock_client(FAKE_BUFFEREVENT); + struct lgtd_proto_target_list *targets = (void *)0x2a; + + lgtd_opts.verbosity = LGTD_INFO; + + char expected_info[] = ("[" + "{" + "\"_lifx\":{" + "\"addr\":\"01:02:03:04:05:00\"," + "\"gateway\":{" + "\"site\":\"00:00:00:00:00:00\"," + "\"url\":\"tcp://[::ffff:127.0.0.1]:1\"," + "\"latency\":0" + "}," + "\"mcu\":{\"firmware_version\":\"0.0\"}," + "\"wifi\":{\"firmware_version\":\"1.1\"}" + "}," + "\"_model\":\"testbulb\"," + "\"_vendor\":null," + "\"hsbk\":[240,1,0.733333,3600]," + "\"power\":true," + "\"label\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"," + "\"tags\":[]" + "}" + "]"); + + reset_client_write_buf(); + + lgtd_proto_get_light_state(client, targets); + + if (client_write_buf_idx != sizeof(expected_info) - 1) { + lgtd_errx( + 1, + "%d bytes written, expected_info %lu " + "(got %.*s instead of %s)", + client_write_buf_idx, sizeof(expected_info) - 1UL, + client_write_buf_idx, client_write_buf, expected_info + ); + } + + if (memcmp(expected_info, client_write_buf, sizeof(expected_info) - 1)) { + lgtd_errx( + 1, "got %.*s instead of %s", + client_write_buf_idx, client_write_buf, expected_info + ); + } + + return 0; +} diff --git a/tests/core/proto/test_proto_get_light_state_null_device_list.c b/tests/core/proto/test_proto_get_light_state_null_device_list.c new file mode 100644 index 0000000..a5a434d --- /dev/null +++ b/tests/core/proto/test_proto_get_light_state_null_device_list.c @@ -0,0 +1,64 @@ +#include "proto.c" + +#include "mock_client_buf.h" +#include "mock_daemon.h" +#include "mock_event2.h" +#include "mock_log.h" +#include "mock_timer.h" +#include "mock_wire_proto.h" +#include "tests_utils.h" + +#define MOCKED_ROUTER_TARGETS_TO_DEVICES +#define MOCKED_CLIENT_SEND_ERROR +#include "tests_proto_utils.h" + +static bool send_error_called = false; + +struct lgtd_router_device_list * +lgtd_router_targets_to_devices(const struct lgtd_proto_target_list *targets) +{ + if (targets != (void *)0x2a) { + lgtd_errx(1, "unexpected targets list"); + } + + return NULL; +} + +void +lgtd_client_send_error(struct lgtd_client *client, + enum lgtd_client_error_code error, + const char *msg) +{ + if (!client) { + lgtd_errx(1, "Expected client"); + } + + if (error != LGTD_CLIENT_INTERNAL_ERROR) { + lgtd_errx( + 1, "Got error code %d (expected %d)", + error, LGTD_CLIENT_INTERNAL_ERROR + ); + } + + if (!msg) { + lgtd_errx(1, "Expected error message"); + } + + send_error_called = true; +} + +int +main(void) +{ + struct lgtd_client *client; + client = lgtd_tests_insert_mock_client(FAKE_BUFFEREVENT); + struct lgtd_proto_target_list *targets = (void *)0x2a; + + lgtd_proto_get_light_state(client, targets); + + if (!send_error_called) { + lgtd_errx(1, "lgtd_client_send_error hasn't been called"); + } + + return 0; +} diff --git a/tests/core/proto/test_proto_get_light_state_unknown_tag_id.c b/tests/core/proto/test_proto_get_light_state_unknown_tag_id.c new file mode 100644 index 0000000..45f5073 --- /dev/null +++ b/tests/core/proto/test_proto_get_light_state_unknown_tag_id.c @@ -0,0 +1,160 @@ +#include "proto.c" + +#include "mock_client_buf.h" +#include "mock_daemon.h" +#include "mock_gateway.h" +#include "mock_event2.h" +#include "mock_log.h" +#include "mock_timer.h" +#include "mock_wire_proto.h" +#include "tests_utils.h" + +#define MOCKED_ROUTER_TARGETS_TO_DEVICES +#define MOCKED_ROUTER_DEVICE_LIST_FREE +#include "tests_proto_utils.h" + +static bool device_list_free_called = false; + +void +lgtd_router_device_list_free(struct lgtd_router_device_list *devices) +{ + if (!devices) { + lgtd_errx(1, "the device list must be passed"); + } + + device_list_free_called = true; +} + +struct lgtd_router_device_list * +lgtd_router_targets_to_devices(const struct lgtd_proto_target_list *targets) +{ + if (targets != (void *)0x2a) { + lgtd_errx(1, "unexpected targets list"); + } + + static struct lgtd_router_device_list devices = + SLIST_HEAD_INITIALIZER(&devices); + + static struct lgtd_lifx_gateway gw_bulb_1 = { + .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_1.bulbs), + .peeraddr = "[::ffff:127.0.0.1]:1" + }; + static struct lgtd_lifx_bulb bulb_1 = { + .addr = { 1, 2, 3, 4, 5 }, + .state = { + .hue = 0xaaaa, + .saturation = 0xffff, + .brightness = 0xbbbb, + .kelvin = 3600, + .label = "wave", + .power = LGTD_LIFX_POWER_ON, + .tags = 5 + }, + .gw = &gw_bulb_1 + }; + static struct lgtd_router_device device_1 = { .device = &bulb_1 }; + SLIST_INSERT_HEAD(&devices, &device_1, link); + + struct lgtd_lifx_tag *gw_2_tag_1 = lgtd_tests_insert_mock_tag("vapor"); + struct lgtd_lifx_tag *gw_2_tag_2 = lgtd_tests_insert_mock_tag("d^-^b"); + struct lgtd_lifx_tag *gw_2_tag_3 = lgtd_tests_insert_mock_tag("wave~"); + static struct lgtd_lifx_gateway gw_bulb_2 = { + .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_2.bulbs), + .tag_ids = 0x7, + .peeraddr = "[::ffff:127.0.0.1]:2" + }; + lgtd_tests_add_tag_to_gw(gw_2_tag_1, &gw_bulb_2, 0); + lgtd_tests_add_tag_to_gw(gw_2_tag_2, &gw_bulb_2, 1); + lgtd_tests_add_tag_to_gw(gw_2_tag_3, &gw_bulb_2, 2); + static struct lgtd_lifx_bulb bulb_2 = { + .addr = { 5, 4, 3, 2, 1 }, + .state = { + .hue = 0x0000, + .saturation = 0x0000, + .brightness = 0xffff, + .kelvin = 4000, + .label = "", + .power = LGTD_LIFX_POWER_OFF, + .tags = 0x3 + }, + .gw = &gw_bulb_2 + }; + static struct lgtd_router_device device_2 = { .device = &bulb_2 }; + SLIST_INSERT_HEAD(&devices, &device_2, link); + + return &devices; +} + +int +main(void) +{ + struct lgtd_client *client; + client = lgtd_tests_insert_mock_client(FAKE_BUFFEREVENT); + struct lgtd_proto_target_list *targets = (void *)0x2a; + + lgtd_opts.verbosity = LGTD_INFO; + + lgtd_proto_get_light_state(client, targets); + + const char expected[] = ("[" + "{" + "\"_lifx\":{" + "\"addr\":\"05:04:03:02:01:00\"," + "\"gateway\":{" + "\"site\":\"00:00:00:00:00:00\"," + "\"url\":\"tcp://[::ffff:127.0.0.1]:2\"," + "\"latency\":0" + "}," + "\"mcu\":{\"firmware_version\":\"0.0\"}," + "\"wifi\":{\"firmware_version\":\"0.0\"}" + "}," + "\"_model\":null," + "\"_vendor\":null," + "\"hsbk\":[0,0,1,4000]," + "\"power\":false," + "\"label\":\"05:04:03:02:01:00\"," + "\"tags\":[\"vapor\",\"d^-^b\"]" + "}," + "{" + "\"_lifx\":{" + "\"addr\":\"01:02:03:04:05:00\"," + "\"gateway\":{" + "\"site\":\"00:00:00:00:00:00\"," + "\"url\":\"tcp://[::ffff:127.0.0.1]:1\"," + "\"latency\":0" + "}," + "\"mcu\":{\"firmware_version\":\"0.0\"}," + "\"wifi\":{\"firmware_version\":\"0.0\"}" + "}," + "\"_model\":null," + "\"_vendor\":null," + "\"hsbk\":[240,1,0.733333,3600]," + "\"power\":true," + "\"label\":\"wave\"," + "\"tags\":[]" + "}" + "]"); + + if (client_write_buf_idx != sizeof(expected) - 1) { + lgtd_errx( + 1, + "%d bytes written, expected %lu " + "(got %.*s instead of %s)", + client_write_buf_idx, sizeof(expected) - 1UL, + client_write_buf_idx, client_write_buf, expected + ); + } + + if (memcmp(expected, client_write_buf, sizeof(expected) - 1)) { + lgtd_errx( + 1, "got %.*s instead of %s", + client_write_buf_idx, client_write_buf, expected + ); + } + + if (!device_list_free_called) { + lgtd_errx(1, "the list of devices hasn't been freed"); + } + + return 0; +} diff --git a/tests/core/proto/test_proto_power_off.c b/tests/core/proto/test_proto_power_off.c new file mode 100644 index 0000000..a881002 --- /dev/null +++ b/tests/core/proto/test_proto_power_off.c @@ -0,0 +1,66 @@ +#include "proto.c" + +#include "mock_client_buf.h" +#include "mock_daemon.h" +#include "mock_event2.h" +#include "mock_log.h" +#include "mock_timer.h" +#include "mock_wire_proto.h" +#include "tests_utils.h" + +#define MOCKED_CLIENT_SEND_RESPONSE +#define MOCKED_ROUTER_SEND +#include "tests_proto_utils.h" + +bool +lgtd_router_send(const struct lgtd_proto_target_list *targets, + enum lgtd_lifx_packet_type pkt_type, + void *pkt) +{ + if (strcmp(SLIST_FIRST(targets)->target, "*")) { + errx( + 1, "invalid target [%s] (expected=[*])", + SLIST_FIRST(targets)->target + ); + } + + if (pkt_type != LGTD_LIFX_SET_POWER_STATE) { + errx(1, "invalid packet type %d (expected %d)", + pkt_type, LGTD_LIFX_SET_POWER_STATE + ); + } + + struct lgtd_lifx_packet_power_state *power_off = pkt; + if (power_off->power != LGTD_LIFX_POWER_OFF) { + errx(1, "invalid power state %hx (expected %x)", + power_off->power, LGTD_LIFX_POWER_OFF); + } + + return true; +} + +void +lgtd_client_send_response(struct lgtd_client *client, const char *msg) +{ + if (!client) { + errx(1, "client shouldn't be NULL"); + } + + if (strcmp(msg, "true")) { + errx(1, "unexpected response [%s] (expected [true])", msg); + } +} + +int +main(void) +{ + struct lgtd_proto_target_list *targets; + targets = lgtd_tests_build_target_list("*", NULL); + + struct lgtd_client *client; + client = lgtd_tests_insert_mock_client(FAKE_BUFFEREVENT); + + lgtd_proto_power_off(client, targets); + + return 0; +} diff --git a/tests/core/proto/test_proto_power_off_routing_error.c b/tests/core/proto/test_proto_power_off_routing_error.c new file mode 100644 index 0000000..19f9077 --- /dev/null +++ b/tests/core/proto/test_proto_power_off_routing_error.c @@ -0,0 +1,67 @@ +#include "proto.c" + +#include "mock_client_buf.h" +#include "mock_daemon.h" +#include "mock_event2.h" +#include "mock_log.h" +#include "mock_timer.h" +#include "mock_wire_proto.h" +#include "tests_utils.h" + +#define MOCKED_CLIENT_SEND_RESPONSE +#define MOCKED_ROUTER_SEND +#include "tests_proto_utils.h" + +bool +lgtd_router_send(const struct lgtd_proto_target_list *targets, + enum lgtd_lifx_packet_type pkt_type, + void *pkt) +{ + if (strcmp(SLIST_FIRST(targets)->target, "*")) { + errx( + 1, "invalid target [%s] (expected=[*])", + SLIST_FIRST(targets)->target + ); + } + + if (pkt_type != LGTD_LIFX_SET_POWER_STATE) { + errx(1, "invalid packet type %d (expected %d)", + pkt_type, LGTD_LIFX_SET_POWER_STATE + ); + } + + struct lgtd_lifx_packet_power_state *power_off = pkt; + if (power_off->power != LGTD_LIFX_POWER_OFF) { + errx(1, "invalid power state %hx (expected %x)", + power_off->power, LGTD_LIFX_POWER_OFF); + } + + return false; +} + +void +lgtd_client_send_response(struct lgtd_client *client, const char *msg) +{ + if (!client) { + errx(1, "client shouldn't ne NULL"); + } + + if (strcmp(msg, "false")) { + errx(1, "unexpected response [%s] (expected [false])", msg); + } +} + +int +main(void) +{ + struct lgtd_proto_target_list *targets; + targets = lgtd_tests_build_target_list("*", NULL); + + struct lgtd_client *client; + client = lgtd_tests_insert_mock_client(FAKE_BUFFEREVENT); + + lgtd_proto_power_off(client, targets); + + return 0; +} + diff --git a/tests/core/proto/test_proto_power_on.c b/tests/core/proto/test_proto_power_on.c new file mode 100644 index 0000000..e1bb5b9 --- /dev/null +++ b/tests/core/proto/test_proto_power_on.c @@ -0,0 +1,66 @@ +#include "proto.c" + +#include "mock_client_buf.h" +#include "mock_daemon.h" +#include "mock_event2.h" +#include "mock_log.h" +#include "mock_timer.h" +#include "mock_wire_proto.h" +#include "tests_utils.h" + +#define MOCKED_CLIENT_SEND_RESPONSE +#define MOCKED_ROUTER_SEND +#include "tests_proto_utils.h" + +bool +lgtd_router_send(const struct lgtd_proto_target_list *targets, + enum lgtd_lifx_packet_type pkt_type, + void *pkt) +{ + if (strcmp(SLIST_FIRST(targets)->target, "*")) { + errx( + 1, "invalid target [%s] (expected=[*])", + SLIST_FIRST(targets)->target + ); + } + + if (pkt_type != LGTD_LIFX_SET_POWER_STATE) { + errx(1, "invalid packet type %d (expected %d)", + pkt_type, LGTD_LIFX_SET_POWER_STATE + ); + } + + struct lgtd_lifx_packet_power_state *power_on = pkt; + if (power_on->power != LGTD_LIFX_POWER_ON) { + errx(1, "invalid power state %hx (expected %x)", + power_on->power, LGTD_LIFX_POWER_ON); + } + + return true; +} + +void +lgtd_client_send_response(struct lgtd_client *client, const char *msg) +{ + if (!client) { + errx(1, "client shouldn't ne NULL"); + } + + if (strcmp(msg, "true")) { + errx(1, "unexpected response [%s] (expected [true])", msg); + } +} + +int +main(void) +{ + struct lgtd_proto_target_list *targets; + targets = lgtd_tests_build_target_list("*", NULL); + + struct lgtd_client *client; + client = lgtd_tests_insert_mock_client(FAKE_BUFFEREVENT); + + lgtd_proto_power_on(client, targets); + + return 0; +} diff --git a/tests/core/proto/test_proto_power_on_routing_error.c b/tests/core/proto/test_proto_power_on_routing_error.c new file mode 100644 index 0000000..2cb64d6 --- /dev/null +++ b/tests/core/proto/test_proto_power_on_routing_error.c @@ -0,0 +1,66 @@ +#include "proto.c" + +#include "mock_client_buf.h" +#include "mock_daemon.h" +#include "mock_event2.h" +#include "mock_log.h" +#include "mock_timer.h" +#include "mock_wire_proto.h" +#include "tests_utils.h" + +#define MOCKED_CLIENT_SEND_RESPONSE +#define MOCKED_ROUTER_SEND +#include "tests_proto_utils.h" + +bool +lgtd_router_send(const struct lgtd_proto_target_list *targets, + enum lgtd_lifx_packet_type pkt_type, + void *pkt) +{ + if (strcmp(SLIST_FIRST(targets)->target, "*")) { + errx( + 1, "invalid target [%s] (expected=[*])", + SLIST_FIRST(targets)->target + ); + } + + if (pkt_type != LGTD_LIFX_SET_POWER_STATE) { + errx(1, "invalid packet type %d (expected %d)", + pkt_type, LGTD_LIFX_SET_POWER_STATE + ); + } + + struct lgtd_lifx_packet_power_state *power_on = pkt; + if (power_on->power != LGTD_LIFX_POWER_ON) { + errx(1, "invalid power state %hx (expected %x)", + power_on->power, LGTD_LIFX_POWER_ON); + } + + return false; +} + +void +lgtd_client_send_response(struct lgtd_client *client, const char *msg) +{ + if (!client) { + errx(1, "client shouldn't ne NULL"); + } + + if (strcmp(msg, "false")) { + errx(1, "unexpected response [%s] (expected [false])", msg); + } +} + +int +main(void) +{ + struct lgtd_proto_target_list *targets; + targets = lgtd_tests_build_target_list("*", NULL); + + struct lgtd_client *client; + client = lgtd_tests_insert_mock_client(FAKE_BUFFEREVENT); + + lgtd_proto_power_on(client, targets); + + return 0; +} diff --git a/tests/core/proto/test_proto_power_toggle.c b/tests/core/proto/test_proto_power_toggle.c new file mode 100644 index 0000000..b4212e6 --- /dev/null +++ b/tests/core/proto/test_proto_power_toggle.c @@ -0,0 +1,167 @@ +#include "proto.c" + +#include "mock_client_buf.h" +#include "mock_daemon.h" +#include "mock_gateway.h" +#include "mock_event2.h" +#include "mock_log.h" +#include "mock_timer.h" +#include "mock_wire_proto.h" +#include "tests_utils.h" + +#define MOCKED_ROUTER_SEND_TO_DEVICE +#define MOCKED_ROUTER_TARGETS_TO_DEVICES +#define MOCKED_ROUTER_DEVICE_LIST_FREE +#include "tests_proto_utils.h" + +static bool device_list_free_called = false; + +void +lgtd_router_device_list_free(struct lgtd_router_device_list *devices) +{ + if (!devices) { + lgtd_errx(1, "the device list must be passed"); + } + + device_list_free_called = true; +} + +struct lgtd_router_device_list * +lgtd_router_targets_to_devices(const struct lgtd_proto_target_list *targets) +{ + if (targets != (void *)0x2a) { + lgtd_errx(1, "unexpected targets list"); + } + + static struct lgtd_router_device_list devices = + SLIST_HEAD_INITIALIZER(&devices); + + static struct lgtd_lifx_gateway gw_bulb_1 = { + .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_1.bulbs) + }; + static struct lgtd_lifx_bulb bulb_1 = { + .addr = { 1, 2, 3, 4, 5 }, + .state = { + .hue = 0xaaaa, + .saturation = 0xffff, + .brightness = 0xbbbb, + .kelvin = 3600, + .label = "wave", + .power = LGTD_LIFX_POWER_ON, + .tags = 0 + }, + .gw = &gw_bulb_1 + }; + static struct lgtd_router_device device_1 = { .device = &bulb_1 }; + SLIST_INSERT_HEAD(&devices, &device_1, link); + + struct lgtd_lifx_tag *gw_2_tag_1 = lgtd_tests_insert_mock_tag("vapor"); + struct lgtd_lifx_tag *gw_2_tag_2 = lgtd_tests_insert_mock_tag("d^-^b"); + struct lgtd_lifx_tag *gw_2_tag_3 = lgtd_tests_insert_mock_tag("wave~"); + static struct lgtd_lifx_gateway gw_bulb_2 = { + .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_2.bulbs), + .tag_ids = 0x7 + }; + lgtd_tests_add_tag_to_gw(gw_2_tag_1, &gw_bulb_2, 0); + lgtd_tests_add_tag_to_gw(gw_2_tag_2, &gw_bulb_2, 1); + lgtd_tests_add_tag_to_gw(gw_2_tag_3, &gw_bulb_2, 2); + static struct lgtd_lifx_bulb bulb_2 = { + .addr = { 5, 4, 3, 2, 1 }, + .state = { + .hue = 0x0000, + .saturation = 0x0000, + .brightness = 0xffff, + .kelvin = 4000, + .label = "light", + .power = LGTD_LIFX_POWER_OFF, + .tags = 0x3 + }, + .gw = &gw_bulb_2 + }; + static struct lgtd_router_device device_2 = { .device = &bulb_2 }; + SLIST_INSERT_HEAD(&devices, &device_2, link); + + return &devices; +} + +static int router_send_to_device_call_count = 0; + +void +lgtd_router_send_to_device(struct lgtd_lifx_bulb *bulb, + enum lgtd_lifx_packet_type pkt_type, + void *pkt) +{ + if (!bulb) { + errx(1, "lgtd_router_send_to_device called without a device"); + } + + if (pkt_type != LGTD_LIFX_SET_POWER_STATE) { + errx( + 1, "lgtd_router_send_to_device got packet type %#x (expected %#x)", + pkt_type, LGTD_LIFX_SET_POWER_STATE + ); + } + + if (!pkt) { + errx(1, "lgtd_router_send_to_device called without a packet"); + } + + struct lgtd_lifx_packet_power_state *payload = pkt; + + if (!strcmp(bulb->state.label, "light")) { + if (payload->power != LGTD_LIFX_POWER_ON) { + errx(1, "bulb light should be turned off"); + } + } else if (!strcmp(bulb->state.label, "wave")) { + if (payload->power != LGTD_LIFX_POWER_OFF) { + errx(1, "bulb wave should be turned on"); + } + } else { + errx( + 1, "lgtd_router_send_to_deviceg got an unknown bulb: %s", + bulb->state.label + ); + } + + router_send_to_device_call_count++; +} + +int +main(void) +{ + struct lgtd_client *client; + client = lgtd_tests_insert_mock_client(FAKE_BUFFEREVENT); + struct lgtd_proto_target_list *targets = (void *)0x2a; + + lgtd_proto_power_toggle(client, targets); + + const char expected[] = "true"; + + if (client_write_buf_idx != sizeof(expected) - 1) { + errx( + 1, "%d bytes written, expected %lu (got %.*s instead of %s)", + client_write_buf_idx, sizeof(expected) - 1UL, + client_write_buf_idx, client_write_buf, expected + ); + } + + if (memcmp(expected, client_write_buf, sizeof(expected) - 1)) { + errx( + 1, "got %.*s instead of %s", + client_write_buf_idx, client_write_buf, expected + ); + } + + if (!device_list_free_called) { + errx(1, "the list of devices hasn't been freed"); + } + + if (router_send_to_device_call_count != 2) { + errx( + 1, "lgtd_router_send_to_device called %d times (expected 2)", + router_send_to_device_call_count + ); + } + + return 0; +} diff --git a/tests/core/proto/test_proto_power_toggle_targets_to_device_fails.c b/tests/core/proto/test_proto_power_toggle_targets_to_device_fails.c new file mode 100644 index 0000000..1be2edb --- /dev/null +++ b/tests/core/proto/test_proto_power_toggle_targets_to_device_fails.c @@ -0,0 +1,91 @@ +#include "proto.c" + +#include "mock_client_buf.h" +#include "mock_daemon.h" +#include "mock_gateway.h" +#include "mock_event2.h" +#include "mock_log.h" +#include "mock_timer.h" +#include "mock_wire_proto.h" +#include "tests_utils.h" + +#define MOCKED_ROUTER_SEND_TO_DEVICE +#define MOCKED_ROUTER_TARGETS_TO_DEVICES +#define MOCKED_ROUTER_DEVICE_LIST_FREE +#define MOCKED_CLIENT_SEND_ERROR +#include "tests_proto_utils.h" + +static bool device_list_free_called = false; + +void +lgtd_router_device_list_free(struct lgtd_router_device_list *devices) +{ + if (!devices) { + lgtd_errx(1, "the device list must be passed"); + } + + device_list_free_called = true; +} + +struct lgtd_router_device_list * +lgtd_router_targets_to_devices(const struct lgtd_proto_target_list *targets) +{ + if (targets != (void *)0x2a) { + lgtd_errx(1, "unexpected targets list"); + } + + return NULL; +} + +static int router_send_to_device_call_count = 0; + +void +lgtd_router_send_to_device(struct lgtd_lifx_bulb *bulb, + enum lgtd_lifx_packet_type pkt_type, + void *pkt) +{ + (void)bulb; + (void)pkt_type; + (void)pkt; + + router_send_to_device_call_count++; +} + +static int client_send_error_call_count = 0; + +void +lgtd_client_send_error(struct lgtd_client *client, + enum lgtd_client_error_code error, + const char *msg) +{ + (void)client; + (void)error; + (void)msg; + + client_send_error_call_count++; +} + +int +main(void) +{ + struct lgtd_client *client; + client = lgtd_tests_insert_mock_client(FAKE_BUFFEREVENT); + struct lgtd_proto_target_list *targets = (void *)0x2a; + + lgtd_proto_power_toggle(client, targets); + + if (client_send_error_call_count != 1) { + errx(1, "lgtd_client_send_error called %d times (expected 1)", + client_send_error_call_count + ); + } + + if (router_send_to_device_call_count) { + errx( + 1, "lgtd_router_send_to_device called %d times (expected 0)", + router_send_to_device_call_count + ); + } + + return 0; +} diff --git a/tests/core/proto/test_proto_set_label.c b/tests/core/proto/test_proto_set_label.c new file mode 100644 index 0000000..e4c7fa3 --- /dev/null +++ b/tests/core/proto/test_proto_set_label.c @@ -0,0 +1,88 @@ +#include "proto.c" + +#include "mock_client_buf.h" +#include "mock_daemon.h" +#include "mock_event2.h" +#include "mock_log.h" +#include "mock_timer.h" +#include "mock_wire_proto.h" +#include "tests_utils.h" + +#define MOCKED_CLIENT_SEND_RESPONSE +#define MOCKED_ROUTER_SEND +#include "tests_proto_utils.h" + +static int router_send_call_count = 0; + +bool +lgtd_router_send(const struct lgtd_proto_target_list *targets, + enum lgtd_lifx_packet_type pkt_type, + void *pkt) +{ + if (strcmp(SLIST_FIRST(targets)->target, "*")) { + errx( + 1, "invalid target [%s] (expected=[*])", + SLIST_FIRST(targets)->target + ); + } + + if (pkt_type != LGTD_LIFX_SET_BULB_LABEL) { + errx(1, "invalid packet type %d (expected %d)", + pkt_type, LGTD_LIFX_SET_BULB_LABEL + ); + } + + struct lgtd_lifx_packet_label *set_label = pkt; + if (strcmp(set_label->label, "test")) { + errx( + 1, "invalid packet label %.*s (expected test)", + LGTD_LIFX_LABEL_SIZE, set_label->label + ); + } + + if (router_send_call_count++) { + errx(1, "lgtd_router_send should be called once"); + } + + return true; +} + +static int client_send_response_call_count = 0; + +void +lgtd_client_send_response(struct lgtd_client *client, const char *msg) +{ + if (!client) { + errx(1, "client shouldn't be NULL"); + } + + if (strcmp(msg, "true")) { + errx(1, "unexpected response [%s] (expected [true])", msg); + } + + if (client_send_response_call_count++) { + errx(1, "lgtd_client_send_response should be called once"); + } +} + +int +main(void) +{ + struct lgtd_proto_target_list *targets; + targets = lgtd_tests_build_target_list("*", NULL); + + struct lgtd_client *client; + client = lgtd_tests_insert_mock_client(FAKE_BUFFEREVENT); + + lgtd_proto_set_label(client, targets, "test"); + + if (!router_send_call_count) { + errx(1, "lgtd_router_send wasn't called"); + } + + if (!client_send_response_call_count) { + errx(1, "lgtd_client_send_response wasn't called"); + } + + return 0; +} diff --git a/tests/core/proto/test_proto_set_label_too_long.c b/tests/core/proto/test_proto_set_label_too_long.c new file mode 100644 index 0000000..17d13c5 --- /dev/null +++ b/tests/core/proto/test_proto_set_label_too_long.c @@ -0,0 +1,90 @@ +#include "proto.c" + +#include "mock_client_buf.h" +#include "mock_daemon.h" +#include "mock_event2.h" +#include "mock_log.h" +#include "mock_timer.h" +#include "mock_wire_proto.h" +#include "tests_utils.h" + +#define MOCKED_CLIENT_SEND_RESPONSE +#define MOCKED_ROUTER_SEND +#include "tests_proto_utils.h" + +static const char *test_label = "test of a very long label over 32 chars"; + +static int router_send_call_count = 0; + +bool +lgtd_router_send(const struct lgtd_proto_target_list *targets, + enum lgtd_lifx_packet_type pkt_type, + void *pkt) +{ + if (strcmp(SLIST_FIRST(targets)->target, "*")) { + errx( + 1, "invalid target [%s] (expected=[*])", + SLIST_FIRST(targets)->target + ); + } + + if (pkt_type != LGTD_LIFX_SET_BULB_LABEL) { + errx(1, "invalid packet type %d (expected %d)", + pkt_type, LGTD_LIFX_SET_BULB_LABEL + ); + } + + struct lgtd_lifx_packet_label *set_label = pkt; + if (memcmp(set_label->label, test_label, sizeof(set_label->label))) { + errx( + 1, "invalid packet label %2$.*1$s (expected %3$.*1$s)", + LGTD_LIFX_LABEL_SIZE, set_label->label, test_label + ); + } + + if (router_send_call_count++) { + errx(1, "lgtd_router_send should be called once"); + } + + return true; +} + +static int client_send_response_call_count = 0; + +void +lgtd_client_send_response(struct lgtd_client *client, const char *msg) +{ + if (!client) { + errx(1, "client shouldn't be NULL"); + } + + if (strcmp(msg, "true")) { + errx(1, "unexpected response [%s] (expected [true])", msg); + } + + if (client_send_response_call_count++) { + errx(1, "lgtd_client_send_response should be called once"); + } +} + +int +main(void) +{ + struct lgtd_proto_target_list *targets; + targets = lgtd_tests_build_target_list("*", NULL); + + struct lgtd_client *client; + client = lgtd_tests_insert_mock_client(FAKE_BUFFEREVENT); + + lgtd_proto_set_label(client, targets, test_label); + + if (!router_send_call_count) { + errx(1, "lgtd_router_send wasn't called"); + } + + if (!client_send_response_call_count) { + errx(1, "lgtd_client_send_response wasn't called"); + } + + return 0; +} diff --git a/tests/core/proto/test_proto_set_light_from_hsbk.c b/tests/core/proto/test_proto_set_light_from_hsbk.c new file mode 100644 index 0000000..8af1992 --- /dev/null +++ b/tests/core/proto/test_proto_set_light_from_hsbk.c @@ -0,0 +1,119 @@ +#include + +#include "proto.c" + +#include "mock_client_buf.h" +#include "mock_daemon.h" +#include "mock_event2.h" +#include "mock_log.h" +#include "mock_timer.h" +#define MOCKED_LGTD_LIFX_WIRE_ENCODE_LIGHT_COLOR +#include "mock_wire_proto.h" +#include "tests_utils.h" + +#define MOCKED_CLIENT_SEND_RESPONSE +#define MOCKED_ROUTER_SEND +#include "tests_proto_utils.h" + +int lifx_wire_encode_light_color_call_count = 0; + +void +lgtd_lifx_wire_encode_light_color(struct lgtd_lifx_packet_light_color *pkt) +{ + (void)pkt; + + lifx_wire_encode_light_color_call_count++; +} + +int router_send_call_count = 0; + +bool +lgtd_router_send(const struct lgtd_proto_target_list *targets, + enum lgtd_lifx_packet_type pkt_type, + void *pkt) +{ + if (strcmp(SLIST_FIRST(targets)->target, "*")) { + errx( + 1, "invalid target [%s] (expected=[*])", + SLIST_FIRST(targets)->target + ); + } + + if (pkt_type != LGTD_LIFX_SET_LIGHT_COLOR) { + errx( + 1, "invalid packet type %d (expected %d)", + pkt_type, LGTD_LIFX_SET_LIGHT_COLOR + ); + } + + struct lgtd_lifx_packet_light_color *light_color = pkt; + if (lifx_wire_encode_light_color_call_count != 1) { + errx( + 1, "lifx_wire_encode_light_color_call_count = %d (expected 1)", + lifx_wire_encode_light_color_call_count + ); + } + if (light_color->hue != 42) { + errx(1, "got hue = %d (expected 42)", light_color->hue); + } + if (light_color->saturation != 10000) { + errx(1, "got saturation = %d (expected 10000)", light_color->saturation); + } + if (light_color->brightness != 20000) { + errx(1, "got brightness = %d (expected 20000)", light_color->brightness); + } + if (light_color->kelvin != 4500) { + errx(1, "got kelvin = %d (expected 4500)", light_color->kelvin); + } + if (light_color->transition != 150) { + errx(1, "got transition = %d (expected 150)", light_color->transition); + } + + router_send_call_count++; + + return true; +} + +int client_send_response_call_count = 0; + +void +lgtd_client_send_response(struct lgtd_client *client, const char *msg) +{ + if (!client) { + errx(1, "client shouldn't ne NULL"); + } + + if (strcmp(msg, "true")) { + errx(1, "unexpected response [%s] (expected [true])", msg); + } + + client_send_response_call_count++; +} + +int +main(void) +{ + struct lgtd_proto_target_list *targets; + targets = lgtd_tests_build_target_list("*", NULL); + + struct lgtd_client client; + + lgtd_proto_set_light_from_hsbk( + &client, targets, 42, 10000, 20000, 4500, 150 + ); + + if (router_send_call_count != 1) { + errx( + 1, "router_send_call_count = %d (expected 1)", + router_send_call_count + ); + } + if (client_send_response_call_count != 1) { + errx( + 1, "client_send_response_call_count = %d (expected 1)", + client_send_response_call_count + ); + } + + return 0; +} diff --git a/tests/core/proto/test_proto_set_light_from_hsbk_on_routing_error.c b/tests/core/proto/test_proto_set_light_from_hsbk_on_routing_error.c new file mode 100644 index 0000000..1d7d230 --- /dev/null +++ b/tests/core/proto/test_proto_set_light_from_hsbk_on_routing_error.c @@ -0,0 +1,119 @@ +#include + +#include "proto.c" + +#include "mock_client_buf.h" +#include "mock_daemon.h" +#include "mock_event2.h" +#include "mock_log.h" +#include "mock_timer.h" +#define MOCKED_LGTD_LIFX_WIRE_ENCODE_LIGHT_COLOR +#include "mock_wire_proto.h" +#include "tests_utils.h" + +#define MOCKED_CLIENT_SEND_RESPONSE +#define MOCKED_ROUTER_SEND +#include "tests_proto_utils.h" + +int lifx_wire_encode_light_color_call_count = 0; + +void +lgtd_lifx_wire_encode_light_color(struct lgtd_lifx_packet_light_color *pkt) +{ + (void)pkt; + + lifx_wire_encode_light_color_call_count++; +} + +int router_send_call_count = 0; + +bool +lgtd_router_send(const struct lgtd_proto_target_list *targets, + enum lgtd_lifx_packet_type pkt_type, + void *pkt) +{ + if (strcmp(SLIST_FIRST(targets)->target, "*")) { + errx( + 1, "invalid target [%s] (expected=[*])", + SLIST_FIRST(targets)->target + ); + } + + if (pkt_type != LGTD_LIFX_SET_LIGHT_COLOR) { + errx( + 1, "invalid packet type %d (expected %d)", + pkt_type, LGTD_LIFX_SET_LIGHT_COLOR + ); + } + + struct lgtd_lifx_packet_light_color *light_color = pkt; + if (lifx_wire_encode_light_color_call_count != 1) { + errx( + 1, "lifx_wire_encode_light_color_call_count = %d (expected 1)", + lifx_wire_encode_light_color_call_count + ); + } + if (light_color->hue != 42) { + errx(1, "got hue = %d (expected 42)", light_color->hue); + } + if (light_color->saturation != 10000) { + errx(1, "got saturation = %d (expected 10000)", light_color->saturation); + } + if (light_color->brightness != 20000) { + errx(1, "got brightness = %d (expected 20000)", light_color->brightness); + } + if (light_color->kelvin != 4500) { + errx(1, "got kelvin = %d (expected 4500)", light_color->kelvin); + } + if (light_color->transition != 150) { + errx(1, "got transition = %d (expected 150)", light_color->transition); + } + + router_send_call_count++; + + return false; +} + +int client_send_response_call_count = 0; + +void +lgtd_client_send_response(struct lgtd_client *client, const char *msg) +{ + if (!client) { + errx(1, "client shouldn't ne NULL"); + } + + if (strcmp(msg, "false")) { + errx(1, "unexpected response [%s] (expected [false])", msg); + } + + client_send_response_call_count++; +} + +int +main(void) +{ + struct lgtd_proto_target_list *targets; + targets = lgtd_tests_build_target_list("*", NULL); + + struct lgtd_client client; + + lgtd_proto_set_light_from_hsbk( + &client, targets, 42, 10000, 20000, 4500, 150 + ); + + if (router_send_call_count != 1) { + errx( + 1, "router_send_call_count = %d (expected 1)", + router_send_call_count + ); + } + if (client_send_response_call_count != 1) { + errx( + 1, "client_send_response_call_count = %d (expected 1)", + client_send_response_call_count + ); + } + + return 0; +} diff --git a/tests/core/proto/test_proto_set_waveform.c b/tests/core/proto/test_proto_set_waveform.c new file mode 100644 index 0000000..9132245 --- /dev/null +++ b/tests/core/proto/test_proto_set_waveform.c @@ -0,0 +1,132 @@ +#include + +#include "proto.c" + +#include "mock_client_buf.h" +#include "mock_daemon.h" +#include "mock_event2.h" +#include "mock_log.h" +#include "mock_timer.h" +#define MOCKED_LGTD_LIFX_WIRE_ENCODE_WAVEFORM +#include "mock_wire_proto.h" +#include "tests_utils.h" + +#define MOCKED_CLIENT_SEND_RESPONSE +#define MOCKED_ROUTER_SEND +#include "tests_proto_utils.h" + +int lifx_wire_encode_waveform_call_count = 0; + +void +lgtd_lifx_wire_encode_waveform(struct lgtd_lifx_packet_waveform *pkt) +{ + (void)pkt; + + lifx_wire_encode_waveform_call_count++; +} + +int router_send_call_count = 0; + +bool +lgtd_router_send(const struct lgtd_proto_target_list *targets, + enum lgtd_lifx_packet_type pkt_type, + void *pkt) +{ + if (strcmp(SLIST_FIRST(targets)->target, "*")) { + errx( + 1, "invalid target [%s] (expected=[*])", + SLIST_FIRST(targets)->target + ); + } + + if (pkt_type != LGTD_LIFX_SET_WAVEFORM) { + errx( + 1, "invalid packet type %d (expected %d)", + pkt_type, LGTD_LIFX_SET_WAVEFORM + ); + } + + struct lgtd_lifx_packet_waveform *waveform = pkt; + if (lifx_wire_encode_waveform_call_count != 1) { + errx( + 1, "lifx_wire_encode_waveform_call_count = %d (expected 1)", + lifx_wire_encode_waveform_call_count + ); + } + if (waveform->waveform != LGTD_LIFX_WAVEFORM_SAW) { + errx( + 1, "got waveform = %d (expected %d)", + waveform->waveform, LGTD_LIFX_WAVEFORM_SAW + ); + } + if (waveform->hue != 42) { + errx(1, "got hue = %d (expected 42)", waveform->hue); + } + if (waveform->saturation != 10000) { + errx(1, "got saturation = %d (expected 10000)", waveform->saturation); + } + if (waveform->brightness != 20000) { + errx(1, "got brightness = %d (expected 20000)", waveform->brightness); + } + if (waveform->kelvin != 4500) { + errx(1, "got kelvin = %d (expected 4500)", waveform->kelvin); + } + if (waveform->period != 200) { + errx(1, "got period = %d (expected 200)", waveform->period); + } + if (waveform->cycles != 10.) { + errx(1, "got cycles = %f (expected 10)", waveform->cycles); + } + if (waveform->skew_ratio != 0) { + errx(1, "got skew_ratio = %d (expected 0)", waveform->skew_ratio); + } + + router_send_call_count++; + + return true; +} + +int client_send_response_call_count = 0; + +void +lgtd_client_send_response(struct lgtd_client *client, const char *msg) +{ + if (!client) { + errx(1, "client shouldn't ne NULL"); + } + + if (strcmp(msg, "true")) { + errx(1, "unexpected response [%s] (expected [true])", msg); + } + + client_send_response_call_count++; +} + +int +main(void) +{ + struct lgtd_proto_target_list *targets; + targets = lgtd_tests_build_target_list("*", NULL); + + struct lgtd_client client; + + lgtd_proto_set_waveform( + &client, targets, LGTD_LIFX_WAVEFORM_SAW, + 42, 10000, 20000, 4500, 200, 10., 0, false + ); + + if (router_send_call_count != 1) { + errx( + 1, "router_send_call_count = %d (expected 1)", + router_send_call_count + ); + } + if (client_send_response_call_count != 1) { + errx( + 1, "client_send_response_call_count = %d (expected 1)", + client_send_response_call_count + ); + } + + return 0; +} diff --git a/tests/core/proto/test_proto_set_waveform_on_routing_error.c b/tests/core/proto/test_proto_set_waveform_on_routing_error.c new file mode 100644 index 0000000..ddfc91a --- /dev/null +++ b/tests/core/proto/test_proto_set_waveform_on_routing_error.c @@ -0,0 +1,126 @@ +#include + +#include "proto.c" + +#include "mock_client_buf.h" +#include "mock_daemon.h" +#include "mock_event2.h" +#include "mock_log.h" +#include "mock_timer.h" +#define MOCKED_LGTD_LIFX_WIRE_ENCODE_WAVEFORM +#include "mock_wire_proto.h" +#include "tests_utils.h" + +#define MOCKED_CLIENT_SEND_RESPONSE +#define MOCKED_ROUTER_SEND +#include "tests_proto_utils.h" + +int lifx_wire_encode_waveform_call_count = 0; + +void +lgtd_lifx_wire_encode_waveform(struct lgtd_lifx_packet_waveform *pkt) +{ + (void)pkt; + + lifx_wire_encode_waveform_call_count++; +} + +int router_send_call_count = 0; + +bool +lgtd_router_send(const struct lgtd_proto_target_list *targets, + enum lgtd_lifx_packet_type pkt_type, + void *pkt) +{ + if (strcmp(SLIST_FIRST(targets)->target, "*")) { + errx( + 1, "invalid target [%s] (expected=[*])", + SLIST_FIRST(targets)->target + ); + } + + if (pkt_type != LGTD_LIFX_SET_WAVEFORM) { + errx( + 1, "invalid packet type %d (expected %d)", + pkt_type, LGTD_LIFX_SET_WAVEFORM + ); + } + + struct lgtd_lifx_packet_waveform *waveform = pkt; + if (waveform->waveform != LGTD_LIFX_WAVEFORM_SAW) { + errx( + 1, "got waveform = %d (expected %d)", + waveform->waveform, LGTD_LIFX_WAVEFORM_SAW + ); + } + if (waveform->hue != 42) { + errx(1, "got hue = %d (expected 42)", waveform->hue); + } + if (waveform->saturation != 10000) { + errx(1, "got saturation = %d (expected 10000)", waveform->saturation); + } + if (waveform->brightness != 20000) { + errx(1, "got brightness = %d (expected 20000)", waveform->brightness); + } + if (waveform->kelvin != 4500) { + errx(1, "got kelvin = %d (expected 4500)", waveform->kelvin); + } + if (waveform->period != 200) { + errx(1, "got period = %d (expected 200)", waveform->period); + } + if (waveform->cycles != 10.) { + errx(1, "got cycles = %f (expected 10)", waveform->cycles); + } + if (waveform->skew_ratio != 0) { + errx(1, "got skew_ratio = %d (expected 0)", waveform->skew_ratio); + } + + router_send_call_count++; + + return false; +} + +int client_send_response_call_count = 0; + +void +lgtd_client_send_response(struct lgtd_client *client, const char *msg) +{ + if (!client) { + errx(1, "client shouldn't ne NULL"); + } + + if (strcmp(msg, "false")) { + errx(1, "unexpected response [%s] (expected [false])", msg); + } + + client_send_response_call_count++; +} + +int +main(void) +{ + struct lgtd_proto_target_list *targets; + targets = lgtd_tests_build_target_list("*", NULL); + + struct lgtd_client client; + + lgtd_proto_set_waveform( + &client, targets, LGTD_LIFX_WAVEFORM_SAW, + 42, 10000, 20000, 4500, 200, 10., 0, false + ); + + if (router_send_call_count != 1) { + errx( + 1, "router_send_call_count = %d (expected 1)", + router_send_call_count + ); + } + if (client_send_response_call_count != 1) { + errx( + 1, "client_send_response_call_count = %d (expected 1)", + client_send_response_call_count + ); + } + + return 0; +} diff --git a/tests/core/proto/test_proto_tag_create.c b/tests/core/proto/test_proto_tag_create.c new file mode 100644 index 0000000..34c0d9d --- /dev/null +++ b/tests/core/proto/test_proto_tag_create.c @@ -0,0 +1,294 @@ +#include "proto.c" + +#include "mock_client_buf.h" +#include "mock_daemon.h" +#define MOCKED_LIFX_GATEWAY_SEND_TO_SITE +#define MOCKED_LIFX_GATEWAY_ALLOCATE_TAG_ID +#include "mock_gateway.h" +#include "mock_event2.h" +#include "mock_log.h" +#include "mock_timer.h" +#define MOCKED_LGTD_LIFX_WIRE_ENCODE_TAG_LABELS +#define MOCKED_LGTD_LIFX_WIRE_ENCODE_TAGS +#include "mock_wire_proto.h" +#include "tests_utils.h" + +#define MOCKED_ROUTER_TARGETS_TO_DEVICES +#define MOCKED_ROUTER_SEND_TO_DEVICE +#define MOCKED_ROUTER_DEVICE_LIST_FREE +#include "tests_proto_utils.h" + +#define FAKE_TARGET_LIST (void *)0x2a + +static struct lgtd_router_device_list devices = + SLIST_HEAD_INITIALIZER(&devices); +static struct lgtd_router_device_list device_1_only = + SLIST_HEAD_INITIALIZER(&device_1_only); + +static int lifx_wire_encode_tag_labels_call_count = 0; + +void +lgtd_lifx_wire_encode_tag_labels(struct lgtd_lifx_packet_tag_labels *pkt) +{ + (void)pkt; + + lifx_wire_encode_tag_labels_call_count++; +} + +static int lifx_wire_encode_tags_call_count = 0; + +void +lgtd_lifx_wire_encode_tags(struct lgtd_lifx_packet_tags *pkt) +{ + (void)pkt; + + lifx_wire_encode_tags_call_count++; +} + +static bool send_to_device_called = false; + +void +lgtd_router_send_to_device(struct lgtd_lifx_bulb *bulb, + enum lgtd_lifx_packet_type pkt_type, + void *pkt) +{ + if (!bulb) { + errx(1, "lgtd_router_send_to_device must be called with a bulb"); + } + + uint8_t expected_addr[LGTD_LIFX_ADDR_LENGTH] = { 1, 2, 3, 4, 5 }; + char addr[LGTD_LIFX_ADDR_STRLEN], expected[LGTD_LIFX_ADDR_STRLEN]; + if (memcmp(bulb->addr, expected_addr, LGTD_LIFX_ADDR_LENGTH)) { + errx( + 1, "got bulb with addr %s (expected %s)", + LGTD_IEEE8023MACTOA(bulb->addr, addr), + LGTD_IEEE8023MACTOA(expected_addr, expected) + ); + } + + if (pkt_type != LGTD_LIFX_SET_TAGS) { + errx( + 1, "got packet type %d (expected %d)", pkt_type, LGTD_LIFX_SET_TAGS + ); + } + + if (!pkt) { + errx(1, "missing SET_TAGS payload"); + } + + const struct lgtd_lifx_packet_tags *pkt_tags = pkt; + if (lifx_wire_encode_tags_call_count != 1) { + errx( + 1, "lifx_wire_encode_tags_call_count = %d (expected 1)", + lifx_wire_encode_tags_call_count + ); + } + if (pkt_tags->tags != 0x1) { + errx( + 1, "invalid SET_TAGS payload=%#jx (expected %#x)", + (uintmax_t)pkt_tags->tags, 0x1 + ); + } + + send_to_device_called = true; +} + +static bool gateway_send_to_site_called = false; + +bool +lgtd_lifx_gateway_send_to_site(struct lgtd_lifx_gateway *gw, + enum lgtd_lifx_packet_type pkt_type, + void *pkt) +{ + if (!gw) { + errx(1, "missing gateway"); + } + + if (pkt_type != LGTD_LIFX_SET_TAG_LABELS) { + errx( + 1, "got packet type %#x (expected %#x)", + pkt_type, LGTD_LIFX_SET_TAG_LABELS + ); + } + + const struct lgtd_lifx_packet_tag_labels *pkt_tag_labels = pkt; + if (lifx_wire_encode_tag_labels_call_count != 1) { + errx( + 1, "lifx_wire_encode_tag_labels_call_count = %d (expected 1)", + lifx_wire_encode_tag_labels_call_count + ); + } + if (pkt_tag_labels->tags != 0x1) { + errx( + 1, "got tags %#jx (expected %#x)", + (uintmax_t)pkt_tag_labels->tags, 0x1 + ); + } + if (strcmp(pkt_tag_labels->label, "dub")) { + errx(1, "got label %s (expected dub)", pkt_tag_labels->label); + } + + gateway_send_to_site_called = true; + + return true; +} + +static bool gateway_allocate_tag_id_called = false; + +int +lgtd_lifx_gateway_allocate_tag_id(struct lgtd_lifx_gateway *gw, + int tag_id, + const char *tag_label) +{ + if (gateway_allocate_tag_id_called) { + errx( + 1, "lgtd_lifx_gateway_allocate_tag_id " + "should have been called once only" + ); + } + + if (tag_id != -1) { + errx( + 1, "lgtd_lifx_gateway_allocate_tag_id " + "tag_id %d (expected -1)", tag_id + ); + } + + if (!gw) { + errx( + 1, "lgtd_lifx_gateway_allocate_tag_id " + "must be called with gateway" + ); + } + + if (!tag_label) { + errx( + 1, "lgtd_lifx_gateway_allocate_tag_id " + "must be called with a tag_label" + ); + } + + tag_id = 0; + + struct lgtd_lifx_tag *tag = lgtd_lifx_tagging_find_tag(tag_label); + if (!tag) { + errx(1, "tag %s wasn't found", tag_label); + } + lgtd_tests_add_tag_to_gw(tag, gw, tag_id); + + gateway_allocate_tag_id_called = true; + + return tag_id; +} + +static bool device_list_free_called = false; + +void +lgtd_router_device_list_free(struct lgtd_router_device_list *devices) +{ + if (!devices) { + lgtd_errx(1, "the device list must be passed"); + } + + device_list_free_called = true; +} + +struct lgtd_router_device_list * +lgtd_router_targets_to_devices(const struct lgtd_proto_target_list *targets) +{ + if (targets != FAKE_TARGET_LIST) { + lgtd_errx(1, "unexpected targets list"); + } + + return &device_1_only; +} + +static void +setup_devices(void) +{ + static struct lgtd_lifx_gateway gw_bulb_1 = { + .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_1.bulbs) + }; + static struct lgtd_lifx_bulb bulb_1 = { + .addr = { 1, 2, 3, 4, 5 }, + .state = { + .hue = 0xaaaa, + .saturation = 0xffff, + .brightness = 0xbbbb, + .kelvin = 3600, + .label = "wave", + .power = LGTD_LIFX_POWER_ON, + .tags = 0 + }, + .gw = &gw_bulb_1 + }; + static struct lgtd_router_device device_1 = { .device = &bulb_1 }; + SLIST_INSERT_HEAD(&devices, &device_1, link); + SLIST_INSERT_HEAD(&device_1_only, &device_1, link); + + struct lgtd_lifx_tag *gw_2_tag_1 = lgtd_tests_insert_mock_tag("vapor"); + struct lgtd_lifx_tag *gw_2_tag_2 = lgtd_tests_insert_mock_tag("d^-^b"); + struct lgtd_lifx_tag *gw_2_tag_3 = lgtd_tests_insert_mock_tag("wave~"); + static struct lgtd_lifx_gateway gw_bulb_2 = { + .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_2.bulbs), + .tag_ids = 0x7 + }; + lgtd_tests_add_tag_to_gw(gw_2_tag_1, &gw_bulb_2, 0); + lgtd_tests_add_tag_to_gw(gw_2_tag_2, &gw_bulb_2, 1); + lgtd_tests_add_tag_to_gw(gw_2_tag_3, &gw_bulb_2, 2); + static struct lgtd_lifx_bulb bulb_2 = { + .addr = { 5, 4, 3, 2, 1 }, + .state = { + .hue = 0x0000, + .saturation = 0x0000, + .brightness = 0xffff, + .kelvin = 4000, + .label = "", + .power = LGTD_LIFX_POWER_OFF, + .tags = 0x3 + }, + .gw = &gw_bulb_2 + }; + static struct lgtd_router_device device_2 = { .device = &bulb_2 }; + SLIST_INSERT_HEAD(&devices, &device_2, link); +} + +int +main(void) +{ + struct lgtd_client *client; + client = lgtd_tests_insert_mock_client(FAKE_BUFFEREVENT); + + setup_devices(); + + lgtd_proto_tag(client, FAKE_TARGET_LIST, "dub"); + + const char expected[] = "true"; + if (client_write_buf_idx != sizeof(expected) - 1) { + lgtd_errx( + 1, + "%d bytes written, expected %lu " + "(got %.*s instead of %s)", + client_write_buf_idx, sizeof(expected) - 1UL, + client_write_buf_idx, client_write_buf, expected + ); + } + if (memcmp(expected, client_write_buf, sizeof(expected) - 1)) { + lgtd_errx( + 1, "got %.*s instead of %s", + client_write_buf_idx, client_write_buf, expected + ); + } + + if (!gateway_send_to_site_called) { + lgtd_errx(1, "SET_TAG_LABELS wasn't sent"); + } + if (!device_list_free_called) { + lgtd_errx(1, "the list of devices hasn't been freed"); + } + if (!send_to_device_called) { + lgtd_errx(1, "SET_TAGS wasn't send to any device"); + } + + return 0; +} diff --git a/tests/core/proto/test_proto_tag_create_lifx_gw_tag_ids_full.c b/tests/core/proto/test_proto_tag_create_lifx_gw_tag_ids_full.c new file mode 100644 index 0000000..db9f7ae --- /dev/null +++ b/tests/core/proto/test_proto_tag_create_lifx_gw_tag_ids_full.c @@ -0,0 +1,213 @@ +#include "proto.c" + +#include "mock_client_buf.h" +#include "mock_daemon.h" +#define MOCKED_LIFX_GATEWAY_SEND_TO_SITE +#define MOCKED_LIFX_GATEWAY_ALLOCATE_TAG_ID +#include "mock_gateway.h" +#include "mock_event2.h" +#include "mock_log.h" +#include "mock_timer.h" +#include "mock_wire_proto.h" +#include "tests_utils.h" + +#define MOCKED_CLIENT_SEND_ERROR +#define MOCKED_ROUTER_TARGETS_TO_DEVICES +#define MOCKED_ROUTER_SEND_TO_DEVICE +#define MOCKED_ROUTER_DEVICE_LIST_FREE +#include "tests_proto_utils.h" + +#define FAKE_TARGET_LIST (void *)0x2a + +static struct lgtd_router_device_list devices = + SLIST_HEAD_INITIALIZER(&devices); +static struct lgtd_router_device_list device_1_only = + SLIST_HEAD_INITIALIZER(&device_1_only); + +static bool client_send_error_called = false; + +void +lgtd_client_send_error(struct lgtd_client *client, + enum lgtd_client_error_code error, + const char *msg) +{ + if (!client) { + errx(1, "client_send_error called without a client"); + } + + if (!error) { + errx(1, "client_send_error called without an error code"); + } + + if (!msg) { + errx(1, "client_send_error called without an error message"); + } + + client_send_error_called = true; +} + +static bool send_to_device_called = false; + +void +lgtd_router_send_to_device(struct lgtd_lifx_bulb *bulb, + enum lgtd_lifx_packet_type pkt_type, + void *pkt) +{ + (void)bulb; + (void)pkt_type; + (void)pkt; + + send_to_device_called = true; +} + +static bool gateway_send_to_site_called = false; + +bool +lgtd_lifx_gateway_send_to_site(struct lgtd_lifx_gateway *gw, + enum lgtd_lifx_packet_type pkt_type, + void *pkt) +{ + (void)gw; + (void)pkt_type; + (void)pkt; + + gateway_send_to_site_called = true; + + return true; +} + +static bool gateway_allocate_tag_id_called = false; + +int +lgtd_lifx_gateway_allocate_tag_id(struct lgtd_lifx_gateway *gw, + int tag_id, + const char *tag_label) +{ + if (gateway_allocate_tag_id_called) { + errx( + 1, "lgtd_lifx_gateway_allocate_tag_id " + "should have been called once only" + ); + } + + if (tag_id != -1) { + errx( + 1, "lgtd_lifx_gateway_allocate_tag_id " + "tag_id %d (expected -1)", tag_id + ); + } + + if (!gw) { + errx( + 1, "lgtd_lifx_gateway_allocate_tag_id " + "must be called with gateway" + ); + } + + if (!tag_label) { + errx( + 1, "lgtd_lifx_gateway_allocate_tag_id " + "must be called with a tag_label" + ); + } + + return -1; // no more tag id available +} + +static bool device_list_free_called = false; + +void +lgtd_router_device_list_free(struct lgtd_router_device_list *devices) +{ + if (!devices) { + lgtd_errx(1, "the device list must be passed"); + } + + device_list_free_called = true; +} + +struct lgtd_router_device_list * +lgtd_router_targets_to_devices(const struct lgtd_proto_target_list *targets) +{ + if (targets != FAKE_TARGET_LIST) { + lgtd_errx(1, "unexpected targets list"); + } + + return &device_1_only; +} + +static void +setup_devices(void) +{ + static struct lgtd_lifx_gateway gw_bulb_1 = { + .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_1.bulbs) + }; + static struct lgtd_lifx_bulb bulb_1 = { + .addr = { 1, 2, 3, 4, 5 }, + .state = { + .hue = 0xaaaa, + .saturation = 0xffff, + .brightness = 0xbbbb, + .kelvin = 3600, + .label = "wave", + .power = LGTD_LIFX_POWER_ON, + .tags = 0 + }, + .gw = &gw_bulb_1 + }; + static struct lgtd_router_device device_1 = { .device = &bulb_1 }; + SLIST_INSERT_HEAD(&devices, &device_1, link); + SLIST_INSERT_HEAD(&device_1_only, &device_1, link); + + struct lgtd_lifx_tag *gw_2_tag_1 = lgtd_tests_insert_mock_tag("vapor"); + struct lgtd_lifx_tag *gw_2_tag_2 = lgtd_tests_insert_mock_tag("d^-^b"); + struct lgtd_lifx_tag *gw_2_tag_3 = lgtd_tests_insert_mock_tag("wave~"); + static struct lgtd_lifx_gateway gw_bulb_2 = { + .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_2.bulbs), + .tag_ids = 0x7 + }; + lgtd_tests_add_tag_to_gw(gw_2_tag_1, &gw_bulb_2, 0); + lgtd_tests_add_tag_to_gw(gw_2_tag_2, &gw_bulb_2, 1); + lgtd_tests_add_tag_to_gw(gw_2_tag_3, &gw_bulb_2, 2); + static struct lgtd_lifx_bulb bulb_2 = { + .addr = { 5, 4, 3, 2, 1 }, + .state = { + .hue = 0x0000, + .saturation = 0x0000, + .brightness = 0xffff, + .kelvin = 4000, + .label = "", + .power = LGTD_LIFX_POWER_OFF, + .tags = 0x3 + }, + .gw = &gw_bulb_2 + }; + static struct lgtd_router_device device_2 = { .device = &bulb_2 }; + SLIST_INSERT_HEAD(&devices, &device_2, link); +} + +int +main(void) +{ + struct lgtd_client *client; + client = lgtd_tests_insert_mock_client(FAKE_BUFFEREVENT); + + setup_devices(); + + lgtd_proto_tag(client, FAKE_TARGET_LIST, "dub"); + + if (gateway_send_to_site_called) { + lgtd_errx(1, "SET_TAG_LABELS shouldn't have been sent"); + } + if (!device_list_free_called) { + lgtd_errx(1, "the list of devices hasn't been freed"); + } + if (send_to_device_called) { + lgtd_errx(1, "SET_TAGS shouldn't have been to any device"); + } + if (!client_send_error_called) { + lgtd_errx(1, "client_send_error should have been called"); + } + + return 0; +} diff --git a/tests/core/proto/test_proto_tag_update.c b/tests/core/proto/test_proto_tag_update.c new file mode 100644 index 0000000..c6f09aa --- /dev/null +++ b/tests/core/proto/test_proto_tag_update.c @@ -0,0 +1,334 @@ +#include "proto.c" + +#include "mock_client_buf.h" +#include "mock_daemon.h" +#define MOCKED_LIFX_GATEWAY_SEND_TO_SITE +#define MOCKED_LIFX_GATEWAY_ALLOCATE_TAG_ID +#include "mock_gateway.h" +#include "mock_event2.h" +#include "mock_log.h" +#include "mock_timer.h" +#define MOCKED_LGTD_LIFX_WIRE_ENCODE_TAG_LABELS +#define MOCKED_LGTD_LIFX_WIRE_ENCODE_TAGS +#include "mock_wire_proto.h" +#include "tests_utils.h" + +#define MOCKED_ROUTER_TARGETS_TO_DEVICES +#define MOCKED_ROUTER_SEND_TO_DEVICE +#define MOCKED_ROUTER_DEVICE_LIST_FREE +#include "tests_proto_utils.h" + +#define FAKE_TARGET_LIST (void *)0x2a + +static struct lgtd_router_device_list devices = + SLIST_HEAD_INITIALIZER(&devices); + +static int lifx_wire_encode_tag_labels_call_count = 0; + +void +lgtd_lifx_wire_encode_tag_labels(struct lgtd_lifx_packet_tag_labels *pkt) +{ + (void)pkt; + + lifx_wire_encode_tag_labels_call_count++; +} + +static int lifx_wire_encode_tags_call_count = 0; + +void +lgtd_lifx_wire_encode_tags(struct lgtd_lifx_packet_tags *pkt) +{ + (void)pkt; + + lifx_wire_encode_tags_call_count++; +} + +static bool send_to_device_called = false; + +void +lgtd_router_send_to_device(struct lgtd_lifx_bulb *bulb, + enum lgtd_lifx_packet_type pkt_type, + void *pkt) +{ + if (send_to_device_called) { + errx(1, "lgtd_router_send_to_device should have been called once only"); + } + + if (!bulb) { + errx(1, "lgtd_router_send_to_device must be called with a bulb"); + } + + uint8_t expected_addr[LGTD_LIFX_ADDR_LENGTH] = { 5, 4, 3, 2, 1 }; + char addr[LGTD_LIFX_ADDR_STRLEN], expected[LGTD_LIFX_ADDR_STRLEN]; + if (memcmp(bulb->addr, expected_addr, LGTD_LIFX_ADDR_LENGTH)) { + errx( + 1, "got bulb with addr %s (expected %s)", + LGTD_IEEE8023MACTOA(bulb->addr, addr), + LGTD_IEEE8023MACTOA(expected_addr, expected) + ); + } + + if (pkt_type != LGTD_LIFX_SET_TAGS) { + errx( + 1, "got packet type %d (expected %d)", pkt_type, LGTD_LIFX_SET_TAGS + ); + } + + if (!pkt) { + errx(1, "missing SET_TAGS payload"); + } + + const struct lgtd_lifx_packet_tags *pkt_tags = pkt; + if (lifx_wire_encode_tags_call_count != 1) { + errx( + 1, "lifx_wire_encode_tags_call_count = %d (expected 1)", + lifx_wire_encode_tags_call_count + ); + } + if (pkt_tags->tags != 0x7) { + errx( + 1, "invalid SET_TAGS payload=%#jx (expected %#x)", + (uintmax_t)pkt_tags->tags, 0x7 + ); + } + + send_to_device_called = true; +} + +static bool gateway_send_to_site_called_for_gw_1 = false; +static bool gateway_send_to_site_called_for_gw_2 = false; + +bool +lgtd_lifx_gateway_send_to_site(struct lgtd_lifx_gateway *gw, + enum lgtd_lifx_packet_type pkt_type, + void *pkt) +{ + if (!gw) { + errx(1, "missing gateway"); + } + + if (pkt_type != LGTD_LIFX_SET_TAG_LABELS) { + errx( + 1, "got packet type %#x (expected %#x)", + pkt_type, LGTD_LIFX_SET_TAG_LABELS + ); + } + + const struct lgtd_lifx_packet_tag_labels *pkt_tag_labels = pkt; + + if (strcmp(pkt_tag_labels->label, "dub")) { + errx(1, "got label %s (expected dub)", pkt_tag_labels->label); + } + + if (gw->site.as_integer == 42) { + if (pkt_tag_labels->tags != 0x1) { + errx( + 1, "got tags %#jx (expected %#x)", + (uintmax_t)pkt_tag_labels->tags, 0x1 + ); + } + if (gateway_send_to_site_called_for_gw_1) { + errx(1, "LGTD_LIFX_SET_TAG_LABELS already called for gw 1"); + } + gateway_send_to_site_called_for_gw_1 = true; + } else if (gw->site.as_integer == 44) { + if (pkt_tag_labels->tags != 0x4) { + errx( + 1, "got tags %#jx (expected %#x)", + (uintmax_t)pkt_tag_labels->tags, 0x4 + ); + } + if (gateway_send_to_site_called_for_gw_2) { + errx(1, "LGTD_LIFX_SET_TAG_LABELS already called for gw 2"); + } + gateway_send_to_site_called_for_gw_2 = true; + } else { + errx(1, "LGTD_LIFX_SET_TAG_LABELS received an invalid gateway"); + } + + int expected_tag_encode_tag_labels_call_count = ( + (int)(gateway_send_to_site_called_for_gw_1 == true) + + (int)(gateway_send_to_site_called_for_gw_2 == true) + ); + if (lifx_wire_encode_tag_labels_call_count + != expected_tag_encode_tag_labels_call_count) { + errx( + 1, "lifx_wire_encode_tag_labels_call_count = %d (expected %d)", + lifx_wire_encode_tag_labels_call_count, + expected_tag_encode_tag_labels_call_count + ); + } + + return true; +} + +static bool gateway_allocate_tag_id_called = false; + +int +lgtd_lifx_gateway_allocate_tag_id(struct lgtd_lifx_gateway *gw, + int tag_id, + const char *tag_label) +{ + if (gateway_allocate_tag_id_called) { + errx( + 1, "lgtd_lifx_gateway_allocate_tag_id " + "should have been called once only" + ); + } + + if (tag_id != -1) { + errx( + 1, "lgtd_lifx_gateway_allocate_tag_id " + "tag_id %d (expected -1)", tag_id + ); + } + + if (!gw) { + errx( + 1, "lgtd_lifx_gateway_allocate_tag_id " + "must be called with gateway" + ); + } + + if (!tag_label) { + errx( + 1, "lgtd_lifx_gateway_allocate_tag_id " + "must be called with a tag_label" + ); + } + + if (gw->site.as_integer != 44) { + errx( + 1, "lgtd_lifx_gateway_allocate_tag_id got the wrong gateway " + "%#jx (expected %d)", (uintmax_t)gw->site.as_integer, 44 + ); + } + + tag_id = 2; + + struct lgtd_lifx_tag *tag = lgtd_lifx_tagging_find_tag(tag_label); + if (!tag) { + errx(1, "tag %s wasn't found", tag_label); + } + lgtd_tests_add_tag_to_gw(tag, gw, tag_id); + + gateway_allocate_tag_id_called = true; + + return tag_id; +} + +static bool device_list_free_called = false; + +void +lgtd_router_device_list_free(struct lgtd_router_device_list *devices) +{ + if (!devices) { + lgtd_errx(1, "the device list must be passed"); + } + + device_list_free_called = true; +} + +struct lgtd_router_device_list * +lgtd_router_targets_to_devices(const struct lgtd_proto_target_list *targets) +{ + if (targets != FAKE_TARGET_LIST) { + lgtd_errx(1, "unexpected targets list"); + } + + return &devices; +} + +static void +setup_devices(void) +{ + static struct lgtd_lifx_gateway gw_bulb_1 = { + .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_1.bulbs), + .site = { .as_integer = 42 } + }; + static struct lgtd_lifx_bulb bulb_1 = { + .addr = { 1, 2, 3, 4, 5 }, + .state = { + .hue = 0xaaaa, + .saturation = 0xffff, + .brightness = 0xbbbb, + .kelvin = 3600, + .label = "wave", + .power = LGTD_LIFX_POWER_ON, + .tags = 1 + }, + .gw = &gw_bulb_1 + }; + static struct lgtd_router_device device_1 = { .device = &bulb_1 }; + SLIST_INSERT_HEAD(&devices, &device_1, link); + struct lgtd_lifx_tag *gw_1_tag_1 = lgtd_tests_insert_mock_tag("dub"); + lgtd_tests_add_tag_to_gw(gw_1_tag_1, &gw_bulb_1, 0); + + struct lgtd_lifx_tag *gw_2_tag_1 = lgtd_tests_insert_mock_tag("vapor"); + struct lgtd_lifx_tag *gw_2_tag_2 = lgtd_tests_insert_mock_tag("d^-^b"); + static struct lgtd_lifx_gateway gw_bulb_2 = { + .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_2.bulbs), + .site = { .as_integer = 44 }, + .tag_ids = 0x3 + }; + lgtd_tests_add_tag_to_gw(gw_2_tag_1, &gw_bulb_2, 0); + lgtd_tests_add_tag_to_gw(gw_2_tag_2, &gw_bulb_2, 1); + static struct lgtd_lifx_bulb bulb_2 = { + .addr = { 5, 4, 3, 2, 1 }, + .state = { + .hue = 0x0000, + .saturation = 0x0000, + .brightness = 0xffff, + .kelvin = 4000, + .label = "", + .power = LGTD_LIFX_POWER_OFF, + .tags = 0x3 + }, + .gw = &gw_bulb_2 + }; + static struct lgtd_router_device device_2 = { .device = &bulb_2 }; + SLIST_INSERT_HEAD(&devices, &device_2, link); +} + +int +main(void) +{ + struct lgtd_client *client; + client = lgtd_tests_insert_mock_client(FAKE_BUFFEREVENT); + + setup_devices(); + + lgtd_proto_tag(client, FAKE_TARGET_LIST, "dub"); + + const char expected[] = "true"; + if (client_write_buf_idx != sizeof(expected) - 1) { + lgtd_errx( + 1, + "%d bytes written, expected %lu " + "(got %.*s instead of %s)", + client_write_buf_idx, sizeof(expected) - 1UL, + client_write_buf_idx, client_write_buf, expected + ); + } + if (memcmp(expected, client_write_buf, sizeof(expected) - 1)) { + lgtd_errx( + 1, "got %.*s instead of %s", + client_write_buf_idx, client_write_buf, expected + ); + } + + if (!gateway_send_to_site_called_for_gw_1) { + lgtd_errx(1, "SET_TAG_LABELS wasn't sent to gw 1"); + } + if (!gateway_send_to_site_called_for_gw_2) { + lgtd_errx(1, "SET_TAG_LABELS wasn't sent to gw 2"); + } + if (!device_list_free_called) { + lgtd_errx(1, "the list of devices hasn't been freed"); + } + if (!send_to_device_called) { + lgtd_errx(1, "SET_TAGS wasn't send to any device"); + } + + return 0; +} diff --git a/tests/core/proto/test_proto_untag.c b/tests/core/proto/test_proto_untag.c new file mode 100644 index 0000000..2077aaa --- /dev/null +++ b/tests/core/proto/test_proto_untag.c @@ -0,0 +1,194 @@ +#include "proto.c" + +#include "mock_client_buf.h" +#include "mock_daemon.h" +#include "mock_gateway.h" +#include "mock_event2.h" +#include "mock_log.h" +#include "mock_timer.h" +#define MOCKED_LGTD_LIFX_WIRE_ENCODE_TAGS +#include "mock_wire_proto.h" +#include "tests_utils.h" + +#define MOCKED_ROUTER_TARGETS_TO_DEVICES +#define MOCKED_ROUTER_SEND_TO_DEVICE +#define MOCKED_ROUTER_DEVICE_LIST_FREE +#include "tests_proto_utils.h" + +static bool device_list_free_called = false; + +static int lifx_wire_encode_tags_call_count = 0; + +void +lgtd_lifx_wire_encode_tags(struct lgtd_lifx_packet_tags *pkt) +{ + (void)pkt; + + lifx_wire_encode_tags_call_count++; +} + +void +lgtd_router_device_list_free(struct lgtd_router_device_list *devices) +{ + if (device_list_free_called) { + errx(1, "the device list should have been freed once"); + } + + if (!devices) { + errx(1, "the device list must be passed"); + } + + device_list_free_called = true; +} + +static struct lgtd_lifx_tag *tag_vapor = NULL; + +struct lgtd_router_device_list * +lgtd_router_targets_to_devices(const struct lgtd_proto_target_list *targets) +{ + if (targets != (void *)0x2a) { + lgtd_errx(1, "unexpected targets list"); + } + + static struct lgtd_router_device_list devices = + SLIST_HEAD_INITIALIZER(&devices); + + static struct lgtd_lifx_gateway gw_bulb_1 = { + .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_1.bulbs) + }; + static struct lgtd_lifx_bulb bulb_1 = { + .addr = { 1, 2, 3, 4, 5 }, + .state = { + .hue = 0xaaaa, + .saturation = 0xffff, + .brightness = 0xbbbb, + .kelvin = 3600, + .label = "wave", + .power = LGTD_LIFX_POWER_ON, + .tags = 0 + }, + .gw = &gw_bulb_1 + }; + static struct lgtd_router_device device_1 = { .device = &bulb_1 }; + SLIST_INSERT_HEAD(&devices, &device_1, link); + + struct lgtd_lifx_tag *gw_2_tag_2 = lgtd_tests_insert_mock_tag("d^-^b"); + struct lgtd_lifx_tag *gw_2_tag_3 = lgtd_tests_insert_mock_tag("wave~"); + static struct lgtd_lifx_gateway gw_bulb_2 = { + .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_2.bulbs), + .tag_ids = 0x7 + }; + lgtd_tests_add_tag_to_gw(tag_vapor, &gw_bulb_2, 0); + lgtd_tests_add_tag_to_gw(gw_2_tag_2, &gw_bulb_2, 1); + lgtd_tests_add_tag_to_gw(gw_2_tag_3, &gw_bulb_2, 2); + static struct lgtd_lifx_bulb bulb_2 = { + .addr = { 5, 4, 3, 2, 1 }, + .state = { + .hue = 0x0000, + .saturation = 0x0000, + .brightness = 0xffff, + .kelvin = 4000, + .label = "", + .power = LGTD_LIFX_POWER_OFF, + .tags = 0x3 + }, + .gw = &gw_bulb_2 + }; + static struct lgtd_router_device device_2 = { .device = &bulb_2 }; + SLIST_INSERT_HEAD(&devices, &device_2, link); + + return &devices; +} + +static bool send_to_device_called = false; + +void +lgtd_router_send_to_device(struct lgtd_lifx_bulb *bulb, + enum lgtd_lifx_packet_type pkt_type, + void *pkt) +{ + if (send_to_device_called) { + errx(1, "lgtd_router_send_to_device should have been called once"); + } + + if (!bulb) { + errx(1, "lgtd_router_send_to_device must be called with a bulb"); + } + + uint8_t expected_addr[LGTD_LIFX_ADDR_LENGTH] = { 5, 4, 3, 2, 1 }; + char addr[LGTD_LIFX_ADDR_STRLEN], expected[LGTD_LIFX_ADDR_STRLEN]; + if (memcmp(bulb->addr, expected_addr, LGTD_LIFX_ADDR_LENGTH)) { + errx( + 1, "got bulb with addr %s (expected %s)", + LGTD_IEEE8023MACTOA(bulb->addr, addr), + LGTD_IEEE8023MACTOA(expected_addr, expected) + ); + } + + if (pkt_type != LGTD_LIFX_SET_TAGS) { + errx( + 1, "got packet type %d (expected %d)", pkt_type, LGTD_LIFX_SET_TAGS + ); + } + + if (!pkt) { + errx(1, "missing SET_TAGS payload"); + } + + struct lgtd_lifx_packet_tags *pkt_tags = pkt; + if (lifx_wire_encode_tags_call_count != 1) { + errx( + 1, "lifx_wire_encode_tags_call_count = %d (expected 1)", + lifx_wire_encode_tags_call_count + ); + } + if (pkt_tags->tags != 0x2) { + errx( + 1, "invalid SET_TAGS payload=%#jx (expected %#x)", + (uintmax_t)pkt_tags->tags, 0x2 + ); + } + + send_to_device_called = true; +} + +int +main(void) +{ + struct lgtd_client *client; + client = lgtd_tests_insert_mock_client(FAKE_BUFFEREVENT); + + struct lgtd_proto_target_list *targets = (void *)0x2a; + + tag_vapor = lgtd_tests_insert_mock_tag("vapor"); + + lgtd_proto_untag(client, targets, "vapor"); + + const char expected[] = "true"; + + if (client_write_buf_idx != sizeof(expected) - 1) { + lgtd_errx( + 1, + "%d bytes written, expected %lu " + "(got %.*s instead of %s)", + client_write_buf_idx, sizeof(expected) - 1UL, + client_write_buf_idx, client_write_buf, expected + ); + } + + if (memcmp(expected, client_write_buf, sizeof(expected) - 1)) { + lgtd_errx( + 1, "got %.*s instead of %s", + client_write_buf_idx, client_write_buf, expected + ); + } + + if (!device_list_free_called) { + lgtd_errx(1, "the list of devices hasn't been freed"); + } + if (!send_to_device_called) { + lgtd_errx(1, "nothing was send to any device"); + } + + return 0; +} diff --git a/tests/core/proto/test_proto_untag_tag_does_not_exist.c b/tests/core/proto/test_proto_untag_tag_does_not_exist.c new file mode 100644 index 0000000..c84b334 --- /dev/null +++ b/tests/core/proto/test_proto_untag_tag_does_not_exist.c @@ -0,0 +1,95 @@ +#include "proto.c" + +#include "mock_client_buf.h" +#include "mock_daemon.h" +#include "mock_gateway.h" +#include "mock_event2.h" +#include "mock_log.h" +#include "mock_timer.h" +#include "mock_wire_proto.h" +#include "tests_utils.h" + +#define MOCKED_ROUTER_TARGETS_TO_DEVICES +#define MOCKED_ROUTER_SEND_TO_DEVICE +#define MOCKED_ROUTER_DEVICE_LIST_FREE +#include "tests_proto_utils.h" + +static bool device_list_free_called = false; + +void +lgtd_router_device_list_free(struct lgtd_router_device_list *devices) +{ + (void)devices; + + device_list_free_called = true; +} + +static bool targets_to_devices_called = false; + +struct lgtd_router_device_list * +lgtd_router_targets_to_devices(const struct lgtd_proto_target_list *targets) +{ + (void)targets; + + targets_to_devices_called = true; + + static struct lgtd_router_device_list devices = + SLIST_HEAD_INITIALIZER(&devices); + + return &devices; +} + +static bool send_to_device_called = false; + +void +lgtd_router_send_to_device(struct lgtd_lifx_bulb *bulb, + enum lgtd_lifx_packet_type pkt_type, + void *pkt) +{ + (void)bulb; + (void)pkt_type; + (void)pkt; + send_to_device_called = true; +} + +int +main(void) +{ + struct lgtd_client *client; + client = lgtd_tests_insert_mock_client(FAKE_BUFFEREVENT); + + struct lgtd_proto_target_list *targets; + targets = lgtd_tests_build_target_list("*", NULL); + + lgtd_proto_untag(client, targets, "vapor"); + + const char expected[] = "true"; + + if (client_write_buf_idx != sizeof(expected) - 1) { + lgtd_errx( + 1, + "%d bytes written, expected %lu " + "(got %.*s instead of %s)", + client_write_buf_idx, sizeof(expected) - 1UL, + client_write_buf_idx, client_write_buf, expected + ); + } + if (memcmp(expected, client_write_buf, sizeof(expected) - 1)) { + lgtd_errx( + 1, "got %.*s instead of %s", + client_write_buf_idx, client_write_buf, expected + ); + } + + if (targets_to_devices_called) { + lgtd_errx(1, "unexpected call to targets_to_devices"); + } + if (device_list_free_called) { + lgtd_errx(1, "nothing should have been freed"); + } + if (send_to_device_called) { + lgtd_errx(1, "nothing should have been sent to any device"); + } + + return 0; +} diff --git a/tests/core/proto/tests_proto_utils.h b/tests/core/proto/tests_proto_utils.h new file mode 100644 index 0000000..72913ee --- /dev/null +++ b/tests/core/proto/tests_proto_utils.h @@ -0,0 +1,79 @@ +#pragma once + +#include "mock_gateway.h" + +#define FAKE_BUFFEREVENT (void *)0xfeed + +void +lgtd_client_start_send_response(struct lgtd_client *client) +{ + (void)client; +} + +void +lgtd_client_end_send_response(struct lgtd_client *client) +{ + (void)client; +} + +#ifndef MOCKED_CLIENT_SEND_RESPONSE +void +lgtd_client_send_response(struct lgtd_client *client, const char *msg) +{ + lgtd_client_write_string(client, msg); +} +#endif + +#ifndef MOCKED_CLIENT_SEND_ERROR +void +lgtd_client_send_error(struct lgtd_client *client, + enum lgtd_client_error_code error, + const char *msg) +{ + (void)client; + (void)error; + (void)msg; +} +#endif + +#ifndef MOCKED_ROUTER_SEND_TO_DEVICE +void +lgtd_router_send_to_device(struct lgtd_lifx_bulb *bulb, + enum lgtd_lifx_packet_type pkt_type, + void *pkt) +{ + (void)bulb; + (void)pkt_type; + (void)pkt; +} +#endif + +#ifndef MOCKED_ROUTER_SEND +bool +lgtd_router_send(const struct lgtd_proto_target_list *targets, + enum lgtd_lifx_packet_type pkt_type, + void *pkt) +{ + (void)targets; + (void)pkt_type; + (void)pkt; + return true; +} +#endif + +#ifndef MOCKED_ROUTER_TARGETS_TO_DEVICES +struct lgtd_router_device_list * +lgtd_router_targets_to_devices(const struct lgtd_proto_target_list *targets) +{ + (void)targets; + return NULL; +} +#endif + +#ifndef MOCKED_ROUTER_DEVICE_LIST_FREE +void +lgtd_router_device_list_free(struct lgtd_router_device_list *devices) +{ + (void)devices; +} +#endif diff --git a/tests/core/router/CMakeLists.txt b/tests/core/router/CMakeLists.txt new file mode 100644 index 0000000..1381327 --- /dev/null +++ b/tests/core/router/CMakeLists.txt @@ -0,0 +1,27 @@ +INCLUDE_DIRECTORIES( + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} +) + +ADD_CORE_LIBRARY( + test_core_router STATIC + ${LIGHTSD_SOURCE_DIR}/core/proto.c + ${LIGHTSD_SOURCE_DIR}/core/stats.c + ${LIGHTSD_SOURCE_DIR}/core/utils.c + ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c + ${LIGHTSD_SOURCE_DIR}/lifx/tagging.c + ${LIGHTSD_SOURCE_DIR}/lifx/wire_proto.c + ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c + ${CMAKE_CURRENT_SOURCE_DIR}/../tests_utils.c +) + +TARGET_LINK_LIBRARIES(test_core_router ${EVENT2_CORE_LIBRARY}) + +FUNCTION(ADD_ROUTER_TEST TEST_SOURCE) + ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_core_router) +ENDFUNCTION() + +FILE(GLOB TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test_*.c") +FOREACH(TEST ${TESTS}) + ADD_ROUTER_TEST(${TEST}) +ENDFOREACH() diff --git a/tests/core/router/test_router_send_to_broadcast.c b/tests/core/router/test_router_send_to_broadcast.c new file mode 100644 index 0000000..7591e72 --- /dev/null +++ b/tests/core/router/test_router_send_to_broadcast.c @@ -0,0 +1,62 @@ +#include "router.c" + +#include "mock_daemon.h" +#include "mock_log.h" +#include "mock_timer.h" +#include "tests_utils.h" + +#include "tests_router_utils.h" + +int +main(void) +{ + lgtd_lifx_wire_setup(); + + lgtd_tests_insert_mock_gateway(2); + lgtd_tests_insert_mock_gateway(1); + + struct lgtd_lifx_packet_power_state payload = { + .power = LGTD_LIFX_POWER_ON + }; + struct lgtd_proto_target_list *targets; + targets = lgtd_tests_build_target_list("*", NULL); + lgtd_router_send(targets, LGTD_LIFX_SET_POWER_STATE, &payload); + + if (lgtd_tests_gw_pkt_queue_size != 2) { + lgtd_errx(1, "2 packets should have been sent"); + } + + for (int i = lgtd_tests_gw_pkt_queue_size; i--;) { + struct lgtd_lifx_gateway *gw = lgtd_tests_gw_pkt_queue[i].gw; + if (gw->socket != i + 1) { + lgtd_errx( + 1, "packet was sent to wrong gateway (expected %d, got %d)", + i + 1, gw->socket + ); + } + const struct lgtd_lifx_packet_header *hdr; + hdr = lgtd_tests_gw_pkt_queue[i].hdr; + int expected_flags = + LGTD_LIFX_ADDRESSABLE|LGTD_LIFX_TAGGED|LGTD_LIFX_RES_REQUIRED; + if (!lgtd_tests_lifx_header_has_flags(hdr, expected_flags)) { + lgtd_errx(1, "packet header doesn't have the right bits set"); + } + if (hdr->target.tags != 0) { + lgtd_errx(1, "tags should be 0 for broadcast"); + } + if (memcmp(gw->site.as_array, hdr->site, sizeof(hdr->site))) { + lgtd_errx(1, "sites don't match"); + } + if (lgtd_tests_gw_pkt_queue[i].pkt != &payload) { + lgtd_errx(1, "the payload has been improperly set"); + } + if (lgtd_tests_gw_pkt_queue[i].pkt_size != sizeof(payload)) { + lgtd_errx( + 1, "unexpected pkt size %d (expected %ju)", + lgtd_tests_gw_pkt_queue[i].pkt_size, (uintmax_t)sizeof(payload) + ); + } + } + + return 0; +} diff --git a/tests/core/router/test_router_send_to_device.c b/tests/core/router/test_router_send_to_device.c new file mode 100644 index 0000000..ecdeb99 --- /dev/null +++ b/tests/core/router/test_router_send_to_device.c @@ -0,0 +1,64 @@ +#include "router.c" + +#include "mock_daemon.h" +#include "mock_log.h" +#include "mock_timer.h" +#include "tests_utils.h" +#include "tests_router_utils.h" + +int +main(void) +{ + lgtd_lifx_wire_setup(); + + struct lgtd_lifx_gateway *gw_1 = lgtd_tests_insert_mock_gateway(1); + struct lgtd_lifx_bulb *bulb_1 = lgtd_tests_insert_mock_bulb(gw_1, 1); + struct lgtd_lifx_gateway *gw_2 = lgtd_tests_insert_mock_gateway(2); + lgtd_tests_insert_mock_bulb(gw_2, 2); + + struct lgtd_lifx_packet_power_state payload = { + .power = LGTD_LIFX_POWER_ON + }; + struct lgtd_proto_target_list *targets; + targets = lgtd_tests_build_target_list("1", NULL); + lgtd_router_send(targets, LGTD_LIFX_SET_POWER_STATE, &payload); + + if (lgtd_tests_gw_pkt_queue_size != 1) { + lgtd_errx(1, "1 packet should have been sent"); + } + + struct lgtd_lifx_gateway *recpt_gw = lgtd_tests_gw_pkt_queue[0].gw; + struct lgtd_lifx_packet_header *hdr_queued = lgtd_tests_gw_pkt_queue[0].hdr; + const void *pkt_queued = lgtd_tests_gw_pkt_queue[0].pkt; + int pkt_size = lgtd_tests_gw_pkt_queue[0].pkt_size; + + if (recpt_gw != gw_1) { + lgtd_errx(1, "the packet has been sent to the wrong gateway"); + } + + int expected_flags = LGTD_LIFX_ADDRESSABLE|LGTD_LIFX_RES_REQUIRED; + if (!lgtd_tests_lifx_header_has_flags(hdr_queued, expected_flags)) { + lgtd_errx(1, "the packet header doesn't have the right protocol flags"); + } + + if (memcmp(hdr_queued->target.device_addr, bulb_1->addr, sizeof(bulb_1->addr))) { + lgtd_errx(1, "the packet header doesn't have the right target address"); + } + + if (memcmp(gw_1->site.as_array, hdr_queued->site, sizeof(hdr_queued->site))) { + lgtd_errx(1, "incorrect site in the headers"); + } + + if (pkt_queued != &payload) { + lgtd_errx(1, "invalid payload"); + } + + if (pkt_size != sizeof(payload)) { + lgtd_errx( + 1, "unexpected pkt size %d (expected %ju)", + pkt_size, (uintmax_t)sizeof(payload) + ); + } + + return 0; +} diff --git a/tests/core/router/test_router_send_to_invalid_targets.c b/tests/core/router/test_router_send_to_invalid_targets.c new file mode 100644 index 0000000..970de4d --- /dev/null +++ b/tests/core/router/test_router_send_to_invalid_targets.c @@ -0,0 +1,51 @@ +#include "router.c" + +#include "mock_daemon.h" +#include "mock_log.h" +#include "mock_timer.h" +#include "tests_utils.h" +#include "tests_router_utils.h" + +void +test_target(const char *target) +{ + struct lgtd_lifx_packet_power_state payload = { + .power = LGTD_LIFX_POWER_ON + }; + struct lgtd_proto_target_list *targets; + targets = lgtd_tests_build_target_list(target, NULL); + bool ok = lgtd_router_send(targets, LGTD_LIFX_SET_POWER_STATE, &payload); +// XXX: Return proper errors from lgtd_router. +#if 0 + if (ok) { + lgtd_errx( + 1, "router_send didn't return false for unknown device %s", target + ); + } +#else + (void)ok; +#endif + if (lgtd_tests_gw_pkt_queue_size) { + lgtd_errx(1, "no packets should have been sent"); + } +} + +int +main(void) +{ + lgtd_lifx_wire_setup(); + + struct lgtd_lifx_gateway *gw_1 = lgtd_tests_insert_mock_gateway(1); + lgtd_tests_insert_mock_bulb(gw_1, 1); + struct lgtd_lifx_gateway *gw_2 = lgtd_tests_insert_mock_gateway(2); + lgtd_tests_insert_mock_bulb(gw_2, 2); + + test_target("4"); + test_target("-1"); + test_target("blabla"); + test_target("**"); + test_target("ffffffffffffffffffffffffff"); + test_target(""); + + return 0; +} diff --git a/tests/core/router/test_router_send_to_label.c b/tests/core/router/test_router_send_to_label.c new file mode 100644 index 0000000..b77838e --- /dev/null +++ b/tests/core/router/test_router_send_to_label.c @@ -0,0 +1,75 @@ +#include "router.c" + +#include "mock_daemon.h" +#include "mock_log.h" +#include "mock_timer.h" +#include "tests_utils.h" +#include "tests_router_utils.h" + +int +main(void) +{ + lgtd_lifx_wire_setup(); + + struct lgtd_lifx_gateway *gw_1 = lgtd_tests_insert_mock_gateway(1); + struct lgtd_lifx_bulb *bulb_1 = lgtd_tests_insert_mock_bulb(gw_1, 1); + struct lgtd_lifx_gateway *gw_2 = lgtd_tests_insert_mock_gateway(2); + struct lgtd_lifx_bulb *bulb_2 = lgtd_tests_insert_mock_bulb(gw_2, 2); + struct lgtd_lifx_bulb *bulb_3 = lgtd_tests_insert_mock_bulb(gw_2, 3); + + const char *label = "feed"; + strcpy(bulb_1->state.label, label); + strcpy(bulb_3->state.label, label); + strcpy(bulb_2->state.label, "trololo"); + + struct lgtd_lifx_packet_power_state payload = { + .power = LGTD_LIFX_POWER_ON + }; + struct lgtd_proto_target_list *targets; + targets = lgtd_tests_build_target_list(label, NULL); + lgtd_router_send(targets, LGTD_LIFX_SET_POWER_STATE, &payload); + + if (lgtd_tests_gw_pkt_queue_size != 2) { + lgtd_errx(1, "2 packet should have been sent"); + } + + for (int i = 0; i != lgtd_tests_gw_pkt_queue_size; i++) { + struct lgtd_lifx_gateway *recpt_gw = lgtd_tests_gw_pkt_queue[0].gw; + struct lgtd_lifx_packet_header *hdr_queued = lgtd_tests_gw_pkt_queue[0].hdr; + const void *pkt_queued = lgtd_tests_gw_pkt_queue[0].pkt; + int pkt_size = lgtd_tests_gw_pkt_queue[0].pkt_size; + + + int expected_flags = LGTD_LIFX_ADDRESSABLE|LGTD_LIFX_RES_REQUIRED; + if (!lgtd_tests_lifx_header_has_flags(hdr_queued, expected_flags)) { + lgtd_errx(1, "the packet header doesn't have the right protocol flags"); + } + if (pkt_queued != &payload) { + lgtd_errx(1, "invalid payload"); + } + if (pkt_size != sizeof(payload)) { + lgtd_errx( + 1, "unexpected pkt size %d (expected %ju)", + pkt_size, (uintmax_t)sizeof(payload) + ); + } + + if (recpt_gw == gw_1) { + if (memcmp(hdr_queued->target.device_addr, bulb_1->addr, sizeof(bulb_1->addr))) { + lgtd_errx(1, "the packet header doesn't have the right target address"); + } + if (memcmp(gw_1->site.as_array, hdr_queued->site, sizeof(hdr_queued->site))) { + lgtd_errx(1, "incorrect site in the headers"); + } + } else if (recpt_gw == gw_2) { + if (memcmp(hdr_queued->target.device_addr, bulb_3->addr, sizeof(bulb_3->addr))) { + lgtd_errx(1, "the packet header doesn't have the right target address"); + } + if (memcmp(gw_2->site.as_array, hdr_queued->site, sizeof(hdr_queued->site))) { + lgtd_errx(1, "incorrect site in the headers"); + } + } + } + + return 0; +} diff --git a/tests/core/router/test_router_send_to_tag.c b/tests/core/router/test_router_send_to_tag.c new file mode 100644 index 0000000..8121d10 --- /dev/null +++ b/tests/core/router/test_router_send_to_tag.c @@ -0,0 +1,144 @@ +#include "router.c" + +#include "mock_daemon.h" +#include "mock_log.h" +#include "mock_timer.h" +#include "tests_utils.h" +#include "tests_router_utils.h" + +int +main(void) +{ + lgtd_lifx_wire_setup(); + + struct lgtd_lifx_gateway *gw_1 = lgtd_tests_insert_mock_gateway(1); + struct lgtd_lifx_gateway *gw_2 = lgtd_tests_insert_mock_gateway(2); + + struct lgtd_lifx_tag *tag_foo = lgtd_tests_insert_mock_tag("foo"); + lgtd_tests_add_tag_to_gw(tag_foo, gw_1, 42); + + struct lgtd_lifx_packet_power_state payload = { + .power = LGTD_LIFX_POWER_ON + }; + struct lgtd_proto_target_list *targets; + targets = lgtd_tests_build_target_list("#foo", NULL); + lgtd_router_send(targets, LGTD_LIFX_SET_POWER_STATE, &payload); + + if (lgtd_tests_gw_pkt_queue_size != 1) { + lgtd_errx(1, "1 packet should have been sent"); + } + + struct lgtd_lifx_gateway *recpt_gw = lgtd_tests_gw_pkt_queue[0].gw; + struct lgtd_lifx_packet_header *hdr_queued = lgtd_tests_gw_pkt_queue[0].hdr; + const void *pkt_queued = lgtd_tests_gw_pkt_queue[0].pkt; + int pkt_size = lgtd_tests_gw_pkt_queue[0].pkt_size; + + lgtd_lifx_wire_decode_header(hdr_queued); + + if (recpt_gw != gw_1) { + lgtd_errx(1, "the packet has been sent to the wrong gateway"); + } + + int expected_flags = + LGTD_LIFX_ADDRESSABLE|LGTD_LIFX_TAGGED|LGTD_LIFX_RES_REQUIRED; + if (!lgtd_tests_lifx_header_has_flags(hdr_queued, expected_flags)) { + lgtd_errx(1, "the packet header doesn't have the right protocol flags"); + } + + if (hdr_queued->target.tags != LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(42)) { + lgtd_errx(1, "the packet header doesn't have the right tags set"); + } + + if (memcmp(gw_1->site.as_array, hdr_queued->site, sizeof(hdr_queued->site))) { + lgtd_errx(1, "incorrect site in the headers"); + } + + if (pkt_queued != &payload) { + lgtd_errx(1, "invalid payload"); + } + + if (pkt_size != sizeof(payload)) { + lgtd_errx( + 1, "unexpected pkt size %d (expected %ju)", + pkt_size, (uintmax_t)sizeof(payload) + ); + } + + lgtd_tests_router_reset_pkt_queue(); + + struct lgtd_lifx_tag *tag_bar = lgtd_tests_insert_mock_tag("bar"); + lgtd_tests_add_tag_to_gw(tag_bar, gw_1, 26); + lgtd_tests_add_tag_to_gw(tag_foo, gw_2, 21); + + targets = lgtd_tests_build_target_list("#foo", "#bar", NULL); + lgtd_router_send(targets, LGTD_LIFX_SET_POWER_STATE, &payload); + + if (lgtd_tests_gw_pkt_queue_size != 3) { + lgtd_errx(1, "3 packet should have been sent"); + } + + int count_tag_foo_gw_1 = 0; + int count_tag_bar_gw_1 = 0; + int count_tag_foo_gw_2 = 0; + int count_other = 0; + for (int i = 0; lgtd_tests_gw_pkt_queue[i].gw; i++) { + recpt_gw = lgtd_tests_gw_pkt_queue[i].gw; + hdr_queued = lgtd_tests_gw_pkt_queue[i].hdr; + pkt_queued = lgtd_tests_gw_pkt_queue[i].pkt; + pkt_size = lgtd_tests_gw_pkt_queue[i].pkt_size; + + lgtd_lifx_wire_decode_header(hdr_queued); + + if (!lgtd_tests_lifx_header_has_flags(hdr_queued, expected_flags)) { + lgtd_errx(1, "the packet header doesn't have the right protocol flags"); + } + + if (memcmp(recpt_gw->site.as_array, hdr_queued->site, sizeof(hdr_queued->site))) { + lgtd_errx(1, "incorrect site in the headers"); + } + + if (pkt_queued != &payload) { + lgtd_errx(1, "invalid payload"); + } + + if (pkt_size != sizeof(payload)) { + lgtd_errx( + 1, "unexpected pkt size %d (expected %ju)", + pkt_size, (uintmax_t)sizeof(payload) + ); + } + + if (recpt_gw == gw_1) { + if (hdr_queued->target.tags == LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(42)) { + count_tag_foo_gw_1++; + } else if (hdr_queued->target.tags == LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(26)) { + count_tag_bar_gw_1++; + } else { + count_other++; + } + } else if (recpt_gw == gw_2) { + if (hdr_queued->target.tags == LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(21)) { + count_tag_foo_gw_2++; + } else { + count_other++; + } + } else { + lgtd_errx(1, "unexpected gateway %p", recpt_gw); + } + } + + if (count_tag_foo_gw_1 != 1) { + lgtd_errx(1, "The packet for #foo should have been enqueued on gw_1"); + } + if (count_tag_bar_gw_1 != 1) { + lgtd_errx(1, "The packet for #bar should have been enqueued on gw_1"); + } + if (count_tag_foo_gw_2 != 1) { + lgtd_errx(1, "The packet for #foo should have been enqueued on gw_2"); + } + if (count_other) { + lgtd_errx(1, "Unexpected packets have been enqueued"); + } + + return 0; +} diff --git a/tests/core/router/test_router_targets_to_devices.c b/tests/core/router/test_router_targets_to_devices.c new file mode 100644 index 0000000..01c4f2a --- /dev/null +++ b/tests/core/router/test_router_targets_to_devices.c @@ -0,0 +1,189 @@ +#include "router.c" + +#include "mock_daemon.h" +#include "mock_log.h" +#include "mock_timer.h" +#include "tests_utils.h" +#include "tests_router_utils.h" + +static int +count_device(const struct lgtd_router_device_list *devices, const void *device) +{ + if (!devices) { + lgtd_errx(1, "unexpected NULL devices list"); + } + + int count = 0; + struct lgtd_router_device *it; + SLIST_FOREACH(it, devices, link) { + if (it->device == device) { + count++; + } + } + + return count; +} + +static int +len(const struct lgtd_router_device_list *devices) +{ + if (!devices) { + lgtd_errx(1, "unexpected NULL devices list"); + } + + int count = 0; + struct lgtd_router_device *it; + SLIST_FOREACH(it, devices, link) { + count++; + } + + return count; +} + +int +main(void) +{ + lgtd_lifx_wire_setup(); + + struct lgtd_lifx_gateway *gw_1 = lgtd_tests_insert_mock_gateway(1); + struct lgtd_lifx_gateway *gw_2 = lgtd_tests_insert_mock_gateway(2); + + struct lgtd_lifx_tag *tag_foo = lgtd_tests_insert_mock_tag("foo"); + lgtd_tests_add_tag_to_gw(tag_foo, gw_1, 42); + lgtd_tests_add_tag_to_gw(tag_foo, gw_2, 63); + + struct lgtd_lifx_tag *tag_bar = lgtd_tests_insert_mock_tag("bar"); + lgtd_tests_add_tag_to_gw(tag_bar, gw_2, 42); + + struct lgtd_lifx_bulb *bulb_1_gw_1 = lgtd_tests_insert_mock_bulb(gw_1, 3); + bulb_1_gw_1->state.tags = LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(42); + + struct lgtd_lifx_bulb *bulb_2_gw_1 = lgtd_tests_insert_mock_bulb(gw_1, 4); + + struct lgtd_lifx_bulb *bulb_1_gw_2 = lgtd_tests_insert_mock_bulb(gw_2, 5); + strcpy(bulb_1_gw_2->state.label, "desk"); + + struct lgtd_lifx_bulb *bulb_2_gw_2 = lgtd_tests_insert_mock_bulb(gw_2, 6); + bulb_2_gw_2->state.tags = + LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(63) | LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(42); + + struct lgtd_proto_target_list *targets; + struct lgtd_router_device_list *devices; + int count; + + targets = lgtd_tests_build_target_list(NULL); + devices = lgtd_router_targets_to_devices(targets); + if ((count = len(devices)) != 0) { + lgtd_errx(1, "expected 0 devices but got %d", count); + } + + targets = lgtd_tests_build_target_list("#pouet", NULL); + devices = lgtd_router_targets_to_devices(targets); + if ((count = len(devices))) { + lgtd_errx(1, "expected 0 device but got %d", count); + } + + targets = lgtd_tests_build_target_list("#pouet", "label", NULL); + devices = lgtd_router_targets_to_devices(targets); + if ((count = len(devices))) { + lgtd_errx(1, "expected 0 device but got %d", count); + } + + targets = lgtd_tests_build_target_list("#foo", NULL); + devices = lgtd_router_targets_to_devices(targets); + if ((count = count_device(devices, bulb_1_gw_1)) != 1) { + lgtd_errx(1, "bulb bulb_1_gw_1 found %d times, expected 1", count); + } + if ((count = count_device(devices, bulb_2_gw_2)) != 1) { + lgtd_errx(1, "bulb bulb_2_gw_2 found %d times, expected 1", count); + } + if ((count = len(devices)) != 2) { + lgtd_errx(1, "expected 2 devices but got %d", count); + } + + targets = lgtd_tests_build_target_list("#bar", NULL); + devices = lgtd_router_targets_to_devices(targets); + if ((count = count_device(devices, bulb_2_gw_2)) != 1) { + lgtd_errx(1, "bulb bulb_2_gw_2 found %d times, expected 1", count); + } + if ((count = len(devices)) != 1) { + lgtd_errx(1, "expected 1 devices but got %d", count); + } + + targets = lgtd_tests_build_target_list("desk", NULL); + devices = lgtd_router_targets_to_devices(targets); + if ((count = count_device(devices, bulb_1_gw_2)) != 1) { + lgtd_errx(1, "bulb bulb_1_gw_2 found %d times, expected 1", count); + } + if ((count = len(devices)) != 1) { + lgtd_errx(1, "expected 1 device but got %d", count); + } + + targets = lgtd_tests_build_target_list("4", NULL); + devices = lgtd_router_targets_to_devices(targets); + if ((count = count_device(devices, bulb_2_gw_1)) != 1) { + lgtd_errx(1, "bulb bulb_2_gw_1 found %d times, expected 1", count); + } + if ((count = len(devices)) != 1) { + lgtd_errx(1, "expected 1 device but got %d", count); + } + + targets = lgtd_tests_build_target_list("desk", "5", NULL); + devices = lgtd_router_targets_to_devices(targets); + if ((count = count_device(devices, bulb_1_gw_2)) != 1) { + lgtd_errx(1, "bulb bulb_1_gw_2 found %d times, expected 1", count); + } + if ((count = len(devices)) != 1) { + lgtd_errx(1, "expected 1 device but got %d", count); + } + + targets = lgtd_tests_build_target_list("desk", "5", "#foo", NULL); + devices = lgtd_router_targets_to_devices(targets); + if ((count = count_device(devices, bulb_1_gw_1)) != 1) { + lgtd_errx(1, "bulb bulb_1_gw_1 found %d times, expected 1", count); + } + if ((count = count_device(devices, bulb_1_gw_2)) != 1) { + lgtd_errx(1, "bulb bulb_1_gw_2 found %d times, expected 1", count); + } + if ((count = count_device(devices, bulb_2_gw_2)) != 1) { + lgtd_errx(1, "bulb bulb_2_gw_2 found %d times, expected 1", count); + } + if ((count = len(devices)) != 3) { + lgtd_errx(1, "expected 3 device but got %d", count); + } + + targets = lgtd_tests_build_target_list("*", "#foo", "*", NULL); + devices = lgtd_router_targets_to_devices(targets); + if ((count = count_device(devices, bulb_1_gw_1)) != 1) { + lgtd_errx(1, "bulb bulb_1_gw_1 found %d times, expected 1", count); + } + if ((count = count_device(devices, bulb_1_gw_2)) != 1) { + lgtd_errx(1, "bulb bulb_1_gw_2 found %d times, expected 1", count); + } + if ((count = count_device(devices, bulb_1_gw_2)) != 1) { + lgtd_errx(1, "bulb bulb_1_gw_2 found %d times, expected 1", count); + } + if ((count = count_device(devices, bulb_2_gw_2)) != 1) { + lgtd_errx(1, "bulb bulb_2_gw_2 found %d times, expected 1", count); + } + if ((count = len(devices)) != 4) { + lgtd_errx(1, "expected 4 device but got %d", count); + } + + // targeting a label shouldn't break at the first match: + struct lgtd_lifx_bulb *bulb_3_gw_2 = lgtd_tests_insert_mock_bulb(gw_2, 7); + strcpy(bulb_3_gw_2->state.label, "desk"); + targets = lgtd_tests_build_target_list("desk", NULL); + devices = lgtd_router_targets_to_devices(targets); + if ((count = count_device(devices, bulb_1_gw_2)) != 1) { + lgtd_errx(1, "bulb bulb_1_gw_2 found %d times, expected 1", count); + } + if ((count = count_device(devices, bulb_3_gw_2)) != 1) { + lgtd_errx(1, "bulb bulb_3_gw_2 found %d times, expected 1", count); + } + if ((count = len(devices)) != 2) { + lgtd_errx(1, "expected 2 device but got %d", count); + } + + return 0; +} diff --git a/tests/core/router/tests_router_utils.h b/tests/core/router/tests_router_utils.h new file mode 100644 index 0000000..1e6a13b --- /dev/null +++ b/tests/core/router/tests_router_utils.h @@ -0,0 +1,39 @@ +#pragma once + +#include "mock_gateway.h" + +int lgtd_tests_gw_pkt_queue_size = 0; +struct { + struct lgtd_lifx_gateway *gw; + struct lgtd_lifx_packet_header *hdr; + const void *pkt; + int pkt_size; +} lgtd_tests_gw_pkt_queue[16] = { { NULL, NULL, NULL, 0}, }; + +void +lgtd_lifx_gateway_enqueue_packet(struct lgtd_lifx_gateway *gw, + const struct lgtd_lifx_packet_header *hdr, + const struct lgtd_lifx_packet_info *pkt_info, + void *pkt) +{ + lgtd_tests_gw_pkt_queue[lgtd_tests_gw_pkt_queue_size].gw = gw; + // headers are created on the stack so we need to dup them: + lgtd_tests_gw_pkt_queue[lgtd_tests_gw_pkt_queue_size].hdr = malloc( + sizeof(*hdr) + ); + memcpy( + lgtd_tests_gw_pkt_queue[lgtd_tests_gw_pkt_queue_size].hdr, + hdr, + sizeof(*hdr) + ); + lgtd_tests_gw_pkt_queue[lgtd_tests_gw_pkt_queue_size].pkt = pkt; + lgtd_tests_gw_pkt_queue[lgtd_tests_gw_pkt_queue_size].pkt_size = pkt_info->size; + lgtd_tests_gw_pkt_queue_size++; +} + +void +lgtd_tests_router_reset_pkt_queue(void) +{ + memset(lgtd_tests_gw_pkt_queue, 0, sizeof(lgtd_tests_gw_pkt_queue)); + lgtd_tests_gw_pkt_queue_size = 0; +} diff --git a/tests/core/tests_shims.c b/tests/core/tests_shims.c new file mode 100644 index 0000000..a37874a --- /dev/null +++ b/tests/core/tests_shims.c @@ -0,0 +1,46 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "lifx/wire_proto.h" +#include "core/time_monotonic.h" +#include "lifx/bulb.h" +#include "lifx/gateway.h" +#include "lifx/tagging.h" +#include "lightsd.h" + +struct lgtd_opts lgtd_opts = { + .foreground = false, + .log_timestamps = false, + .verbosity = LGTD_DEBUG +}; + +#define MOCK_LGTD_EV_BASE ((void *)2222) + +struct event_base *lgtd_ev_base = MOCK_LGTD_EV_BASE; + +const int LGTD_LIFX_DEBRUIJN_SEQUENCE[64] = { + 0, 47, 1, 56, 48, 27, 2, 60, + 57, 49, 41, 37, 28, 16, 3, 61, + 54, 58, 35, 52, 50, 42, 21, 44, + 38, 32, 29, 23, 17, 11, 4, 62, + 46, 55, 26, 59, 40, 36, 15, 53, + 34, 51, 20, 43, 31, 22, 10, 45, + 25, 39, 14, 33, 19, 30, 9, 24, + 13, 18, 8, 12, 7, 6, 5, 63 +}; + +void +lgtd_cleanup(void) +{ +} diff --git a/tests/core/tests_shims.h b/tests/core/tests_shims.h new file mode 100644 index 0000000..df46480 --- /dev/null +++ b/tests/core/tests_shims.h @@ -0,0 +1,25 @@ +#pragma once + +struct lgtd_opts lgtd_opts = { + .foreground = false, + .log_timestamps = false, + .verbosity = LGTD_DEBUG +}; + +#define MOCK_LGTD_EV_BASE ((void *)2222) + +struct event_base *lgtd_ev_base = MOCK_LGTD_EV_BASE; + +const char *lgtd_binds = NULL; + +void +lgtd_cleanup(void) +{ +} + +#ifndef MOCKED_DAEMON_UPDATE_PROCTITLE +void +lgtd_daemon_update_proctitle(void) +{ +} +#endif diff --git a/tests/core/tests_utils.c b/tests/core/tests_utils.c new file mode 100644 index 0000000..396111e --- /dev/null +++ b/tests/core/tests_utils.c @@ -0,0 +1,215 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "lifx/wire_proto.h" +#include "core/time_monotonic.h" +#include "lifx/tagging.h" +#include "core/jsmn.h" +#include "core/jsonrpc.h" +#include "core/client.h" +#include "core/proto.h" +#include "core/listen.h" +#include "core/daemon.h" +#include "core/stats.h" +#include "lifx/bulb.h" +#include "lifx/gateway.h" +#include "tests_utils.h" +#include "core/lightsd.h" + +struct lgtd_listen_list lgtd_listeners = + SLIST_HEAD_INITIALIZER(&lgtd_listeners); + +struct lgtd_lifx_gateway * +lgtd_tests_insert_mock_gateway(int id) +{ + struct lgtd_lifx_gateway *gw = calloc(1, sizeof(*gw)); + + gw->socket = id; + gw->site.as_array[0] = id; + +#if 0 + struct sockaddr_in in_addr = { + .sin_family = AF_INET, + .sin_addr.s_addr = inet_addr("127.0.0.1"), + .sin_port = htons(id) + }; + gw->peer = calloc(1, sizeof(in_addr)); + memcpy(gw->peeraddr, &in_addr, sizeof(in_addr)); + gw->peerlen = sizeof(in_addr); +#endif + + LIST_INSERT_HEAD(&lgtd_lifx_gateways, gw, link); + + LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(gateways, 1); + + return gw; +} + +struct lgtd_lifx_bulb * +lgtd_tests_insert_mock_bulb(struct lgtd_lifx_gateway *gw, uint64_t addr) +{ + assert(gw); + + union { + uint8_t as_array[LGTD_LIFX_ADDR_LENGTH]; + uint64_t as_scalar; + } bulb_addr = { + .as_scalar = LGTD_BIG_ENDIAN_SYSTEM ? + htobe64(addr) << 16 : htobe64(addr) >> 16 + }; + struct lgtd_lifx_bulb *bulb = lgtd_lifx_bulb_open(gw, bulb_addr.as_array); + + SLIST_INSERT_HEAD(&gw->bulbs, bulb, link_by_gw); + + return bulb; +} + +struct lgtd_proto_target_list * +lgtd_tests_build_target_list(const char *target, ...) +{ + struct lgtd_proto_target_list *targets = malloc(sizeof(*targets)); + SLIST_INIT(targets); + + if (target) { + struct lgtd_proto_target *tail = malloc( + sizeof(*tail) + strlen(target) + 1 + ); + strcpy(tail->target, target); + SLIST_INSERT_HEAD(targets, tail, link); + + va_list ap; + va_start(ap, target); + while ((target = va_arg(ap, const char *))) { + struct lgtd_proto_target *t = malloc( + sizeof(*t) + strlen(target) + 1 + ); + strcpy(t->target, target); + SLIST_INSERT_AFTER(tail, t, link); + tail = t; + } + va_end(ap); + } + + return targets; +} + +struct lgtd_lifx_tag * +lgtd_tests_insert_mock_tag(const char *tag_label) +{ + assert(strlen(tag_label) < LGTD_LIFX_LABEL_SIZE); + struct lgtd_lifx_tag *tag = calloc(1, sizeof(*tag)); + strcpy(tag->label, tag_label); + LIST_INSERT_HEAD(&lgtd_lifx_tags, tag, link); + return tag; +} + +struct lgtd_lifx_site * +lgtd_tests_add_tag_to_gw(struct lgtd_lifx_tag *tag, + struct lgtd_lifx_gateway *gw, + int tag_id) +{ + struct lgtd_lifx_site *site = calloc(1, sizeof(*site)); + site->gw = gw; + site->tag_id = tag_id; + LIST_INSERT_HEAD(&tag->sites, site, link); + + gw->tags[tag_id] = tag; + gw->tag_ids |= LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id); + + return site; +} + +struct lgtd_listen * +lgtd_tests_insert_mock_listener(const char *ipv4, uint16_t port) +{ + struct lgtd_listen *listener = calloc(1, sizeof(*listener)); + struct sockaddr_in in_addr; + memset(&in_addr, 0, sizeof(in_addr)); + in_addr.sin_family = AF_INET; + in_addr.sin_addr.s_addr = inet_addr(ipv4); + in_addr.sin_port = htons(port); + listener->sockaddr = calloc(1, sizeof(in_addr)); + memcpy(listener->sockaddr, &in_addr, sizeof(in_addr)); + listener->addrlen = sizeof(in_addr); + SLIST_INSERT_HEAD(&lgtd_listeners, listener, link); + + return listener; +} + +static struct sockaddr * +lgtd_tests_make_sockaddr(int family, const char *addr, size_t addrlen) +{ + struct sockaddr *sa = calloc(1, sizeof(struct sockaddr_storage)); + sa->sa_family = family; + memcpy(sa->sa_data, addr, LGTD_MIN( + sizeof(*sa) - offsetof(struct sockaddr, sa_data), addrlen + )); + return sa; +} + +struct lgtd_client * +lgtd_tests_insert_mock_client(struct bufferevent *io) +{ + struct lgtd_client *client = calloc(1, sizeof(*client)); + client->io = io; + client->addr = lgtd_tests_make_sockaddr( + AF_UNIX, "/toto.sock", sizeof("/toto.sock") + ); + return client; +} + +char * +lgtd_tests_make_temp_dir(void) +{ + char buf[PATH_MAX] = { 0 }; + int n = snprintf(buf, sizeof(buf), "%s/lightsd.test.XXXXXXXX", P_tmpdir); + if (n >= (int)sizeof(buf)) { + errx(1, "cannot allocate temporary directory"); + } + return strdup(mkdtemp(buf)); +} + +void +lgtd_tests_remove_temp_dir(char *path) +{ + DIR *tmpdir = opendir(path); + if (!tmpdir) { + return; + } + + struct dirent db; + struct dirent *entry = &db; + while (!readdir_r(tmpdir, entry, &entry) && entry) { + if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) { + continue; + } + char buf[PATH_MAX] = { 0 }; + snprintf(buf, sizeof(buf), "%s/%s", path, entry->d_name); + unlink(buf); + } + + closedir(tmpdir); + + if (rmdir(path)) { + warn("couldn't remove tempdir %s", path); + } + + free(path); +} diff --git a/tests/core/tests_utils.h b/tests/core/tests_utils.h new file mode 100644 index 0000000..4eaa316 --- /dev/null +++ b/tests/core/tests_utils.h @@ -0,0 +1,46 @@ +#pragma once + +struct bufferevent; + +static inline bool +lgtd_tests_lifx_header_has_flags(const struct lgtd_lifx_packet_header *hdr, + int flags) +{ + int expected_protocol_flags = 0; + if (flags & LGTD_LIFX_ADDRESSABLE) { + expected_protocol_flags |= LGTD_LIFX_PROTOCOL_ADDRESSABLE; + } + if (flags & LGTD_LIFX_TAGGED) { + expected_protocol_flags |= LGTD_LIFX_PROTOCOL_TAGGED; + } + int protocol_flags = hdr->protocol & LGTD_LIFX_PROTOCOL_FLAGS_MASK; + if (protocol_flags != expected_protocol_flags) { + return false; + } + + int expected_flags = 0; + if (flags & LGTD_LIFX_ACK_REQUIRED) { + expected_flags |= LGTD_LIFX_FLAG_ACK_REQUIRED; + } + if (flags & LGTD_LIFX_RES_REQUIRED) { + expected_flags |= LGTD_LIFX_FLAG_RES_REQUIRED; + } + if (hdr->flags != expected_flags) { + return false; + } + + return true; +} + +char *lgtd_tests_make_temp_dir(void); +void lgtd_tests_remove_temp_dir(char *); + +struct lgtd_lifx_gateway *lgtd_tests_insert_mock_gateway(int); +struct lgtd_lifx_bulb *lgtd_tests_insert_mock_bulb(struct lgtd_lifx_gateway *, uint64_t); +struct lgtd_proto_target_list *lgtd_tests_build_target_list(const char *, ...); +struct lgtd_lifx_tag *lgtd_tests_insert_mock_tag(const char *); +struct lgtd_lifx_site *lgtd_tests_add_tag_to_gw(struct lgtd_lifx_tag *, + struct lgtd_lifx_gateway *, + int); +struct lgtd_listen *lgtd_tests_insert_mock_listener(const char *, uint16_t); +struct lgtd_client *lgtd_tests_insert_mock_client(struct bufferevent *); diff --git a/tests/core/timer/CMakeLists.txt b/tests/core/timer/CMakeLists.txt new file mode 100644 index 0000000..3c76a7a --- /dev/null +++ b/tests/core/timer/CMakeLists.txt @@ -0,0 +1,13 @@ +INCLUDE_DIRECTORIES( + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} +) + +FUNCTION(ADD_TIMER_TEST TEST_SOURCE) + ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE}) +ENDFUNCTION() + +FILE(GLOB TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test_*.c") +FOREACH(TEST ${TESTS}) + ADD_TIMER_TEST(${TEST}) +ENDFOREACH() diff --git a/tests/core/timer/test_timer_ispending.c b/tests/core/timer/test_timer_ispending.c new file mode 100644 index 0000000..e6261e6 --- /dev/null +++ b/tests/core/timer/test_timer_ispending.c @@ -0,0 +1,45 @@ +#include + +#include "core/timer.c" + +#define MOCKED_EVENT_PENDING +#include "mock_event2.h" +#include "tests_shims.h" + +static int event_pending_call_count = 0; + +int +event_pending(const struct event *ev, short events, struct timeval *tv) +{ + (void)events; + + if (ev != MOCK_EVENT_NEW_EVENT_PTR) { + errx(1, "got event %p (expected %p)", ev, MOCK_EVENT_NEW_EVENT_PTR); + } + + if (tv) { + errx(1, "got unexpected parameter tv"); + } + + if (event_pending_call_count++) { + errx(1, "event_pending should have been called once"); + } + + return true; +} + +int +main(void) +{ + struct lgtd_timer timer = { .event = MOCK_EVENT_NEW_EVENT_PTR }; + + if (!lgtd_timer_ispending(&timer)) { + errx(1, "lgtd_timer_ispending returned false (expected true)"); + } + + if (!event_pending_call_count) { + errx(1, "event_pending wasn't called"); + } + + return 0; +} diff --git a/tests/core/timer/test_timer_reschedule.c b/tests/core/timer/test_timer_reschedule.c new file mode 100644 index 0000000..9645af1 --- /dev/null +++ b/tests/core/timer/test_timer_reschedule.c @@ -0,0 +1,45 @@ +#include + +#include "core/timer.c" + +#define MOCKED_EVENT_ADD +#include "mock_event2.h" +#include "tests_shims.h" + +static int event_add_call_count = 0; + +int +event_add(struct event *ev, const struct timeval *tv) +{ + if (ev != MOCK_EVENT_NEW_EVENT_PTR) { + errx(1, "got event %p (expected %p)", ev, MOCK_EVENT_NEW_EVENT_PTR); + } + + struct timeval expected_tv = LGTD_MSECS_TO_TIMEVAL(5); + if (memcmp(tv, &expected_tv, sizeof(*tv))) { + errx(1, "got unexpected timeout"); + } + + if (event_add_call_count++) { + errx(1, "event_add should have been called once"); + } + + return 0; +} + +int +main(void) +{ + struct lgtd_timer timer = { .event = MOCK_EVENT_NEW_EVENT_PTR }; + struct timeval tv = LGTD_MSECS_TO_TIMEVAL(5); + + if (!lgtd_timer_reschedule(&timer, &tv)) { + errx(1, "wrong return value"); + } + + if (!event_add_call_count) { + errx(1, "event_add wasn't called"); + } + + return 0; +} diff --git a/tests/core/timer/test_timer_start_activate_now.c b/tests/core/timer/test_timer_start_activate_now.c new file mode 100644 index 0000000..1b34929 --- /dev/null +++ b/tests/core/timer/test_timer_start_activate_now.c @@ -0,0 +1,145 @@ +#include + +#include "core/timer.c" + +#define MOCKED_EVENT_NEW +#define MOCKED_EVENT_ADD +#define MOCKED_EVENT_ACTIVE +#include "mock_event2.h" +#include "tests_shims.h" + +static void +my_test_callback(struct lgtd_timer *timer, union lgtd_timer_ctx ctx) +{ + (void)timer; + (void)ctx; +} + +static int event_active_call_count = 0; + +void +event_active(struct event *ev, int res, short ncalls) +{ + if (ev != MOCK_EVENT_NEW_EVENT_PTR) { + errx(1, "got event %p (expected %p)", ev, MOCK_EVENT_NEW_EVENT_PTR); + } + + if (res) { + errx(1, "got res = %d (expected 0)", res); + } + + if (ncalls) { + errx(1, "got ncalls = %d (expected 0)", ncalls); + } + + if (event_active_call_count++) { + errx(1, "event_active should be called once"); + } +} + +static int event_new_call_count = 0; + +struct event * +event_new(struct event_base *base, + evutil_socket_t fd, + short events, + event_callback_fn cb, + void *ctx) +{ + if (base != MOCK_LGTD_EV_BASE) { + errx(1, "got base %p (expected %p)", base, MOCK_LGTD_EV_BASE); + } + + if (fd != -1) { + errx(1, "got fd %d (expected -1)", fd); + } + + if (events) { + errx(1, "got events %#x (expected 0)", events); + } + + if (cb != lgtd_timer_callback) { + errx(1, "got cb %p (expected %p)", cb, lgtd_timer_callback); + } + + if (!ctx) { + errx(1, "didn't get any context"); + } + + if (event_new_call_count++) { + errx(1, "event_new should be called once"); + } + + return MOCK_EVENT_NEW_EVENT_PTR; +} + +static int event_add_call_count = 0; + +int +event_add(struct event *ev, const struct timeval *timeout) +{ + if (ev != MOCK_EVENT_NEW_EVENT_PTR) { + errx(1, "got ev %p (expected %p)", ev, MOCK_EVENT_NEW_EVENT_PTR); + } + + if (!timeout) { + errx(1, "a timeout should have been passed in"); + } + // so i don't know wth clang and mac os x are doing but memcmp + // gets whacked in -O2 and returns a difference when there isn't, + // so we have to do it the painful way: + struct timeval expected_tv = LGTD_MSECS_TO_TIMEVAL(5); + if (timeout->tv_sec != expected_tv.tv_sec + || timeout->tv_usec != expected_tv.tv_usec) { + errx(1, "got invalid timeout"); + } + + if (event_add_call_count++) { + errx(1, "event_add should be called once"); + } + + return 0; +} + +int +main(void) +{ + union lgtd_timer_ctx ctx = { .as_uint = 7614 }; + struct lgtd_timer *timer = lgtd_timer_start( + LGTD_TIMER_ACTIVATE_NOW, 5, my_test_callback, ctx + ); + + if (timer->event != MOCK_EVENT_NEW_EVENT_PTR) { + errx( + 1, "timer has event %p (expected %p)", + timer->event, MOCK_EVENT_NEW_EVENT_PTR + ); + } + if (timer->ctx.as_uint != ctx.as_uint) { + errx( + 1, "timer ctx is %ju (expected %ju)", + (uintmax_t)timer->ctx.as_uint, (uintmax_t)ctx.as_uint + ); + } + if (timer->callback != my_test_callback) { + errx( + 1, "timer callback is %p (expected %p)", + timer->callback, my_test_callback + ); + } + if (LIST_FIRST(&lgtd_timers) != timer) { + errx(1, "the timer wasn't inserted in the timers list"); + } + + if (!event_new_call_count) { + errx(1, "event_new wasn't called"); + } + if (!event_add_call_count) { + errx(1, "event_add wasn't called"); + } + if (!event_active_call_count) { + errx(1, "the timer wasn't activated"); + } + + return 0; +} diff --git a/tests/core/timer/test_timer_start_persistent.c b/tests/core/timer/test_timer_start_persistent.c new file mode 100644 index 0000000..878c6bc --- /dev/null +++ b/tests/core/timer/test_timer_start_persistent.c @@ -0,0 +1,129 @@ +#include + +#include "core/timer.c" + +#define MOCKED_EVENT_NEW +#define MOCKED_EVENT_ADD +#define MOCKED_EVENT_ACTIVE +#include "mock_event2.h" +#include "tests_shims.h" + +static void +my_test_callback(struct lgtd_timer *timer, union lgtd_timer_ctx ctx) +{ + (void)timer; + (void)ctx; +} + +void +event_active(struct event *ev, int res, short ncalls) +{ + (void)ev; + (void)res; + (void)ncalls; + errx(1, "event_active shouldn't have been called"); +} + +static int event_new_call_count = 0; + +struct event * +event_new(struct event_base *base, + evutil_socket_t fd, + short events, + event_callback_fn cb, + void *ctx) +{ + if (base != MOCK_LGTD_EV_BASE) { + errx(1, "got base %p (expected %p)", base, MOCK_LGTD_EV_BASE); + } + + if (fd != -1) { + errx(1, "got fd %d (expected -1)", fd); + } + + if (events != EV_PERSIST) { + errx(1, "got events %#x (expected %#x)", events, EV_PERSIST); + } + + if (cb != lgtd_timer_callback) { + errx(1, "got cb %p (expected %p)", cb, lgtd_timer_callback); + } + + if (!ctx) { + errx(1, "didn't get any context"); + } + + if (event_new_call_count++) { + errx(1, "event_new should be called once"); + } + + return MOCK_EVENT_NEW_EVENT_PTR; +} + +static int event_add_call_count = 0; + +int +event_add(struct event *ev, const struct timeval *timeout) +{ + if (ev != MOCK_EVENT_NEW_EVENT_PTR) { + errx(1, "got ev %p (expected %p)", ev, MOCK_EVENT_NEW_EVENT_PTR); + } + + if (!timeout) { + errx(1, "a timeout should have been passed in"); + } + // so i don't know wth clang and mac os x are doing but memcmp + // gets whacked in -O2 and returns a difference when there isn't, + // so we have to do it the painful way: + struct timeval expected_tv = LGTD_MSECS_TO_TIMEVAL(5); + if (timeout->tv_sec != expected_tv.tv_sec + || timeout->tv_usec != expected_tv.tv_usec) { + errx(1, "got invalid timeout"); + } + + if (event_add_call_count++) { + errx(1, "event_add should be called once"); + } + + return 0; +} + +int +main(void) +{ + union lgtd_timer_ctx ctx = { .as_uint = 7614 }; + struct lgtd_timer *timer = lgtd_timer_start( + LGTD_TIMER_PERSISTENT, 5, my_test_callback, ctx + ); + + if (timer->event != MOCK_EVENT_NEW_EVENT_PTR) { + errx( + 1, "timer has event %p (expected %p)", + timer->event, MOCK_EVENT_NEW_EVENT_PTR + ); + } + if (timer->ctx.as_uint != ctx.as_uint) { + errx( + 1, "timer ctx is %ju (expected %ju)", + (uintmax_t)timer->ctx.as_uint, (uintmax_t)ctx.as_uint + ); + } + if (timer->callback != my_test_callback) { + errx( + 1, "timer callback is %p (expected %p)", + timer->callback, my_test_callback + ); + } + if (LIST_FIRST(&lgtd_timers) != timer) { + errx(1, "the timer wasn't inserted in the timers list"); + } + + if (!event_new_call_count) { + errx(1, "event_new wasn't called"); + } + if (!event_add_call_count) { + errx(1, "event_add wasn't called"); + } + + return 0; +} diff --git a/tests/lifx/CMakeLists.txt b/tests/lifx/CMakeLists.txt new file mode 100644 index 0000000..01dbb06 --- /dev/null +++ b/tests/lifx/CMakeLists.txt @@ -0,0 +1,12 @@ +INCLUDE_DIRECTORIES( + ${LIGHTSD_SOURCE_DIR} + ${LIGHTSD_SOURCE_DIR}/lifx/ + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../core + ${LIGHTSD_BINARY_DIR} + ${LIGHTSD_BINARY_DIR}/lifx/ + ${CMAKE_CURRENT_BINARY_DIR} + ${CMAKE_CURRENT_BINARY_DIR}/../core +) + +ADD_ALL_SUBDIRECTORIES() diff --git a/tests/lifx/bulb/CMakeLists.txt b/tests/lifx/bulb/CMakeLists.txt new file mode 100644 index 0000000..7abd17c --- /dev/null +++ b/tests/lifx/bulb/CMakeLists.txt @@ -0,0 +1,30 @@ +INCLUDE_DIRECTORIES( + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} +) + +ADD_CORE_LIBRARY( + test_lifx_bulb_core STATIC + ${LIGHTSD_SOURCE_DIR}/core/stats.c + ${LIGHTSD_SOURCE_DIR}/core/utils.c + ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../core/tests_utils.c +) + +ADD_LIBRARY( + test_lifx_bulb STATIC + ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c + ${LIGHTSD_SOURCE_DIR}/lifx/tagging.c + ${LIGHTSD_SOURCE_DIR}/lifx/wire_proto.c +) + +FUNCTION(ADD_BULB_TEST TEST_SOURCE) + ADD_TEST_FROM_C_SOURCES( + ${TEST_SOURCE} test_lifx_bulb_core test_lifx_bulb + ) +ENDFUNCTION() + +FILE(GLOB TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test_*.c") +FOREACH(TEST ${TESTS}) + ADD_BULB_TEST(${TEST}) +ENDFOREACH() diff --git a/tests/lifx/bulb/test_bulb_close.c b/tests/lifx/bulb/test_bulb_close.c new file mode 100644 index 0000000..1b6c180 --- /dev/null +++ b/tests/lifx/bulb/test_bulb_close.c @@ -0,0 +1,36 @@ +#include "bulb.c" + +#include "mock_gateway.h" +#include "mock_log.h" +#include "mock_router.h" +#include "mock_timer.h" + +int +main(void) +{ + struct lgtd_lifx_gateway gw; + uint8_t bulb_addr[LGTD_LIFX_ADDR_LENGTH] = { 5, 4, 3, 2, 1, 0 }; + struct lgtd_lifx_bulb *bulb = lgtd_lifx_bulb_open(&gw, bulb_addr); + + bulb->state.power = LGTD_LIFX_POWER_ON; + LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(bulbs_powered_on, 1); + + lgtd_lifx_bulb_close(bulb); + + if (!RB_EMPTY(&lgtd_lifx_bulbs_table)) { + errx(1, "The bulbs table should be empty!"); + } + + if (LGTD_STATS_GET(bulbs) != 0) { + errx(1, "The bulbs counter is %d (expected 0)", LGTD_STATS_GET(bulbs)); + } + + if (LGTD_STATS_GET(bulbs_powered_on) != 0) { + errx( + 1, "The powered on bulbs counter is %d (expected 0)", + LGTD_STATS_GET(bulbs_powered_on) + ); + } + + return 0; +} diff --git a/tests/lifx/bulb/test_bulb_fetch_hardware_info.c b/tests/lifx/bulb/test_bulb_fetch_hardware_info.c new file mode 100644 index 0000000..5a54e40 --- /dev/null +++ b/tests/lifx/bulb/test_bulb_fetch_hardware_info.c @@ -0,0 +1,144 @@ +#include "bulb.c" + +#include "mock_gateway.h" +#include "mock_log.h" +#define MOCKED_LGTD_ROUTER_SEND_TO_DEVICE +#include "mock_router.h" +#define MOCKED_LGTD_TIMER_STOP +#include "mock_timer.h" + +#include "tests_utils.h" + +#define FAKE_TIMER (void *)4 + +static struct lgtd_lifx_bulb *test_bulb = NULL; + +static int timer_stop_call_count = 0; + +void +lgtd_timer_stop(struct lgtd_timer *timer) +{ + if (timer != FAKE_TIMER) { + errx(1, "got timer %p (expected %p)", timer, FAKE_TIMER); + } + + timer_stop_call_count++; +} + +static int get_version_sent = 0; +static int get_mesh_firmware_state_sent = 0; +static int get_wifi_firmware_state_sent = 0; + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wswitch" // we don't test the whole enum +void +lgtd_router_send_to_device(struct lgtd_lifx_bulb *bulb, + enum lgtd_lifx_packet_type pkt_type, + void *pkt) +{ + if (bulb != test_bulb) { + errx( + 1, "router_send_to_device got bulb %p (expected %p)", + bulb, test_bulb + ); + } + + if (pkt) { + errx(1, "got unexpected pkt"); + } + + switch (pkt_type) { + case LGTD_LIFX_GET_VERSION: + get_version_sent++; + break; + case LGTD_LIFX_GET_MESH_FIRMWARE: + get_mesh_firmware_state_sent++; + break; + case LGTD_LIFX_GET_WIFI_FIRMWARE_STATE: + get_wifi_firmware_state_sent++; + break; + } +} +#pragma GCC diagnostic pop + +static void +test_counters(int get_version, int mcu_fw_info, int wifi_fw_info) +{ + if (get_version_sent != get_version) { + errx( + 1, "get_version_sent %d (expected %d)", + get_version_sent, get_version + ); + } + if (get_mesh_firmware_state_sent != mcu_fw_info) { + errx( + 1, "get_mesh_firmware_state_sent %d (expected %d)", + get_mesh_firmware_state_sent, mcu_fw_info + ); + } + if (get_wifi_firmware_state_sent != wifi_fw_info) { + errx( + 1, "get_wifi_firmware_state_sent %d (expected %d)", + get_wifi_firmware_state_sent, wifi_fw_info + ); + } +} + +int +main(void) +{ + struct lgtd_lifx_gateway gw; + memset(&gw, 0, sizeof(gw)); + test_bulb = lgtd_tests_insert_mock_bulb(&gw, 75); + union lgtd_timer_ctx ctx = { .as_uint = 1 }; + + // make sure it handles non-existent bulbs: + lgtd_lifx_bulb_fetch_hardware_info(FAKE_TIMER, ctx); + if (timer_stop_call_count != 1) { + errx(1, "timer_stop wasn't called"); + } + + ctx.as_uint = 0; + memcpy(&ctx.as_uint, test_bulb->addr, LGTD_LIFX_ADDR_LENGTH); + lgtd_lifx_bulb_fetch_hardware_info(FAKE_TIMER, ctx); + test_counters(1, 1, 0); + + memset(&test_bulb->product_info, 1, sizeof(test_bulb->product_info)); + lgtd_lifx_bulb_fetch_hardware_info(FAKE_TIMER, ctx); + test_counters(1, 2, 0); + struct lgtd_lifx_bulb_ip *ip = &test_bulb->ips[LGTD_LIFX_BULB_MCU_IP]; + memset(ip, 1, sizeof(*ip)); + + // the retry logic for the wifi firmware is a bit more complex because of + // that v1.1 bug for non-gateway bulbs: + + // state_updated_at for the mcu ip is at zero so we give up and stop the + // timer instead of sending a new packet: + ip->state_updated_at = 0; + lgtd_lifx_bulb_fetch_hardware_info(FAKE_TIMER, ctx); + test_counters(1, 2, 0); + if (timer_stop_call_count != 2) { + errx(1, "timer_stop wasn't called"); + } + + // set it to the current time, the packet should be sent again: + ip = &test_bulb->ips[LGTD_LIFX_BULB_MCU_IP]; + ip->state_updated_at = lgtd_time_monotonic_msecs(); + lgtd_lifx_bulb_fetch_hardware_info(FAKE_TIMER, ctx); + test_counters(1, 2, 1); + if (timer_stop_call_count != 2) { + errx(1, "timer_stop shouldn't have been called"); + } + + // finally make sure we stop the timer if we got the info alright as it + // should just be without the bug: + ip = &test_bulb->ips[LGTD_LIFX_BULB_WIFI_IP]; + memset(ip, 1, sizeof(*ip)); + lgtd_lifx_bulb_fetch_hardware_info(FAKE_TIMER, ctx); + test_counters(1, 2, 1); + if (timer_stop_call_count != 3) { + errx(1, "timer_stop wasn't called"); + } + + return 0; +} diff --git a/tests/lifx/bulb/test_bulb_has_label.c b/tests/lifx/bulb/test_bulb_has_label.c new file mode 100644 index 0000000..1a69da1 --- /dev/null +++ b/tests/lifx/bulb/test_bulb_has_label.c @@ -0,0 +1,48 @@ +#include "bulb.c" + +#include "mock_gateway.h" +#include "mock_log.h" +#include "mock_router.h" +#include "mock_timer.h" + +void +test_label(struct lgtd_lifx_bulb *bulb, + const char *bulb_label, + const char *label, + bool expected) +{ + memcpy(bulb->state.label, bulb_label, LGTD_MIN( + strlen(bulb_label) + 1, LGTD_LIFX_LABEL_SIZE + )); + bool rv = lgtd_lifx_bulb_has_label(bulb, label); + if (rv != expected) { + errx( + 1, "bulb_has_label(%s, %s) -> %s (expected %s)", + bulb_label, label, + rv ? "true" : "false", + expected ? "true" : "false" + ); + } +} + +int +main(void) +{ + struct lgtd_lifx_gateway gw; + uint8_t bulb_addr[LGTD_LIFX_ADDR_LENGTH] = { 5, 4, 3, 2, 1, 0 }; + struct lgtd_lifx_bulb *bulb = lgtd_lifx_bulb_open(&gw, bulb_addr); + + test_label(bulb, "", "test", false); + test_label(bulb, "test", "test", true); + test_label(bulb, "testtest", "test", false); + test_label(bulb, "test", "testtest", false); + test_label(bulb, "", "", true); + test_label( + bulb, + "testtesttesttesttesttesttesttest", + "testtesttesttesttesttesttesttesttest", + true + ); + + return 0; +} diff --git a/tests/lifx/bulb/test_bulb_open.c b/tests/lifx/bulb/test_bulb_open.c new file mode 100644 index 0000000..aa4ccf7 --- /dev/null +++ b/tests/lifx/bulb/test_bulb_open.c @@ -0,0 +1,95 @@ +#include "bulb.c" + +#include "mock_gateway.h" +#include "mock_log.h" +#include "mock_router.h" +#define MOCKED_LGTD_TIMER_START +#include "mock_timer.h" + +static int timer_start_call_count = 0; + +struct lgtd_timer * +lgtd_timer_start(int flags, + int ms, + void (*cb)(struct lgtd_timer *, + union lgtd_timer_ctx), + union lgtd_timer_ctx ctx) +{ + if (flags != (LGTD_TIMER_PERSISTENT|LGTD_TIMER_ACTIVATE_NOW)) { + errx( + 1, "got flags %#x (expected %#x)", + flags, LGTD_TIMER_PERSISTENT|LGTD_TIMER_ACTIVATE_NOW + ); + } + + if (ms != LGTD_LIFX_BULB_FETCH_HARDWARE_INFO_TIMER_MSECS) { + errx( + 1, "got timeout %d (expected %d)", + ms, LGTD_LIFX_BULB_FETCH_HARDWARE_INFO_TIMER_MSECS + ); + } + + if (cb != lgtd_lifx_bulb_fetch_hardware_info) { + errx( + 1, "got callback %p (expected %p)", + cb, lgtd_lifx_bulb_fetch_hardware_info + ); + } + + if (!ctx.as_uint) { + errx(1, "ctx wasn't set"); + } + + if (timer_start_call_count++) { + errx(1, "timer_start should have been called once"); + } + + return (void *)42; +} + +int +main(void) +{ + struct lgtd_lifx_gateway gw; + uint8_t bulb_addr[LGTD_LIFX_ADDR_LENGTH] = { 5, 4, 3, 2, 1, 0 }; + lgtd_time_mono_t now = lgtd_time_monotonic_msecs(); + struct lgtd_lifx_bulb *bulb = lgtd_lifx_bulb_open(&gw, bulb_addr); + + if (!bulb) { + errx(1, "lgtd_lifx_bulb_open didn't return any bulb"); + } + + char addr[LGTD_LIFX_ADDR_STRLEN], expected[LGTD_LIFX_ADDR_STRLEN]; + if (memcmp(bulb->addr, bulb_addr, LGTD_LIFX_ADDR_LENGTH)) { + errx( + 1, "got bulb addr %s (expected %s)", + LGTD_IEEE8023MACTOA(bulb->addr, addr), + LGTD_IEEE8023MACTOA(bulb_addr, expected) + ); + } + + if (bulb->gw != &gw) { + errx(1, "got bulb gateway %p (expected %p)", bulb->gw, &gw); + } + + if (lgtd_lifx_bulb_get(bulb_addr) != bulb) { + errx(1, "the new bulb can't be found"); + } + + if (bulb->last_light_state_at < now) { + errx( + 1, "got bulb->last_light_state_at %ju (expected >= %ju)", + (uintmax_t)bulb->last_light_state_at, (uintmax_t)now + ); + } + + if (LGTD_STATS_GET(bulbs) != 1) { + errx(1, "bulbs counter is %d (expected 1)", LGTD_STATS_GET(bulbs)); + } + + if (!timer_start_call_count) { + errx(1, "timer_start_call_count = 0 (expected 1)"); + } + + return 0; +} diff --git a/tests/lifx/bulb/test_bulb_set_light_state.c b/tests/lifx/bulb/test_bulb_set_light_state.c new file mode 100644 index 0000000..581c7d5 --- /dev/null +++ b/tests/lifx/bulb/test_bulb_set_light_state.c @@ -0,0 +1,95 @@ +#include "bulb.c" + +#define MOCKED_LGTD_LIFX_GATEWAY_UPDATE_TAG_REFCOUNTS +#include "mock_gateway.h" +#include "mock_log.h" +#include "mock_router.h" +#include "mock_timer.h" + +static int update_tag_refcouts_call_counts = 0; + +void +lgtd_lifx_gateway_update_tag_refcounts(struct lgtd_lifx_gateway *gw, + uint64_t bulb_tags, + uint64_t pkt_tags) +{ + if (gw != (void *)0xdeaf) { + errx(1, "got wrong gw %p (expected 0xdeaf)", gw); + } + + if (pkt_tags != 0xfeed) { + errx(1, "got pkt_tags %#jx (expected 0xfeed)", (uintmax_t)pkt_tags); + } + + if (!update_tag_refcouts_call_counts) { + if (bulb_tags != 0x2a) { + errx(1, "got bulb_tags %#jx (expected 0x2a)", (uintmax_t)bulb_tags); + } + } else { + if (bulb_tags != 0xfeed) { + errx(1, "got bulb_tags %#jx (expected 0xfeed)", (uintmax_t)bulb_tags); + } + } + + update_tag_refcouts_call_counts++; +} + +int +main(void) +{ + struct lgtd_lifx_bulb bulb = { + .state = { + .hue = 54321, + .brightness = UINT16_MAX, + .kelvin = 12345, + .dim = 808, + .power = LGTD_LIFX_POWER_OFF, + .label = "lair", + .tags = 0x2a + }, + .gw = (void *)0xdeaf + }; + + struct lgtd_lifx_light_state new_state = { + .hue = 22222, + .brightness = UINT16_MAX / 2, + .kelvin = 54321, + .dim = 303, + .power = LGTD_LIFX_POWER_ON, + .label = "caverne", + .tags = 0xfeed + }; + + lgtd_lifx_bulb_set_light_state(&bulb, &new_state, 2015); + if (memcmp(&bulb.state, &new_state, sizeof(new_state))) { + errx(1, "new light state incorrectly set"); + } + if (LGTD_STATS_GET(bulbs_powered_on) != 1) { + errx( + 1, "unexpected bulbs_powered_on counter value %d (expected 1)", + LGTD_STATS_GET(bulbs_powered_on) + ); + } + if (bulb.last_light_state_at != 2015) { + errx( + 1, "got bulb.last_light_state = %jx (expected 2015)", + (uintmax_t)bulb.last_light_state_at + ); + } + if (update_tag_refcouts_call_counts != 1) { + errx(1, "lgtd_lifx_gateway_update_tag_refcounts wasn't called"); + } + + lgtd_lifx_bulb_set_light_state(&bulb, &new_state, 2015); + if (update_tag_refcouts_call_counts != 2) { + errx(1, "lgtd_lifx_gateway_update_tag_refcounts wasn't called"); + } + if (LGTD_STATS_GET(bulbs_powered_on) != 1) { + errx( + 1, "unexpected bulbs_powered_on counter value %d (expected 1)", + LGTD_STATS_GET(bulbs_powered_on) + ); + } + + return 0; +} diff --git a/tests/lifx/bulb/test_bulb_set_power_state.c b/tests/lifx/bulb/test_bulb_set_power_state.c new file mode 100644 index 0000000..a37e55e --- /dev/null +++ b/tests/lifx/bulb/test_bulb_set_power_state.c @@ -0,0 +1,42 @@ +#include "bulb.c" + +#include "mock_gateway.h" +#include "mock_log.h" +#include "mock_router.h" +#include "mock_timer.h" + +int +main(void) +{ + struct lgtd_lifx_bulb bulb = { + .state = { + .hue = 54321, + .brightness = UINT16_MAX, + .kelvin = 12345, + .dim = 808, + .power = LGTD_LIFX_POWER_OFF, + .label = "lair", + .tags = 0x2a + }, + .gw = (void *)0xdeaf + }; + struct lgtd_lifx_light_state new_state; + memcpy(&new_state, &bulb.state, sizeof(new_state)); + new_state.power = LGTD_LIFX_POWER_ON; + + + for (int i = 0; i != 2; i++) { + lgtd_lifx_bulb_set_power_state(&bulb, LGTD_LIFX_POWER_ON); + if (memcmp(&bulb.state, &new_state, sizeof(new_state))) { + errx(1, "new light state incorrectly set"); + } + if (LGTD_STATS_GET(bulbs_powered_on) != 1) { + errx( + 1, "unexpected bulbs_powered_on counter value %d (expected 1)", + LGTD_STATS_GET(bulbs_powered_on) + ); + } + } + + return 0; +} diff --git a/tests/lifx/bulb/test_bulb_set_tags.c b/tests/lifx/bulb/test_bulb_set_tags.c new file mode 100644 index 0000000..6dcd0fd --- /dev/null +++ b/tests/lifx/bulb/test_bulb_set_tags.c @@ -0,0 +1,53 @@ +#include "bulb.c" + +#define MOCKED_LGTD_LIFX_GATEWAY_UPDATE_TAG_REFCOUNTS +#include "mock_gateway.h" +#include "mock_log.h" +#include "mock_router.h" +#include "mock_timer.h" + +static bool update_tag_refcouts_called = false; + +void +lgtd_lifx_gateway_update_tag_refcounts(struct lgtd_lifx_gateway *gw, + uint64_t bulb_tags, + uint64_t pkt_tags) +{ + if (gw != (void *)0xdeaf) { + errx(1, "got wrong gw %p (expected 0xdeaf)", gw); + } + + if (bulb_tags != 0x2a) { + errx(1, "got bulb_tags %#jx (expected 0x2a)", (uintmax_t)bulb_tags); + } + + if (pkt_tags != 0xfeed) { + errx(1, "got pkt_tags %#jx (expected 0xfeed)", (uintmax_t)pkt_tags); + } + + update_tag_refcouts_called = true; +} + +int +main(void) +{ + struct lgtd_lifx_bulb bulb = { + .state = { .tags = 0x2a }, + .gw = (void *)0xdeaf + }; + + lgtd_lifx_bulb_set_tags(&bulb, 0xfeed); + + if (bulb.state.tags != 0xfeed) { + errx( + 1, "got bulb.state.tags = %#jx (expected 0xfeed)", + (uintmax_t)bulb.state.tags + ); + } + + if (!update_tag_refcouts_called) { + errx(1, "lgtd_lifx_gateway_update_tag_refcounts wasn't called"); + } + + return 0; +} diff --git a/tests/lifx/gateway/CMakeLists.txt b/tests/lifx/gateway/CMakeLists.txt new file mode 100644 index 0000000..34df4e4 --- /dev/null +++ b/tests/lifx/gateway/CMakeLists.txt @@ -0,0 +1,32 @@ +INCLUDE_DIRECTORIES( + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} +) + +ADD_CORE_LIBRARY( + test_lifx_gateway_core STATIC + ${LIGHTSD_SOURCE_DIR}/core/proto.c + ${LIGHTSD_SOURCE_DIR}/core/router.c + ${LIGHTSD_SOURCE_DIR}/core/stats.c + ${LIGHTSD_SOURCE_DIR}/core/utils.c + ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../core/tests_utils.c +) + +ADD_LIBRARY( + test_lifx_gateway STATIC + ${LIGHTSD_SOURCE_DIR}/lifx/broadcast.c + ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c + ${LIGHTSD_SOURCE_DIR}/lifx/discovery.c +) + +FUNCTION(ADD_GATEWAY_TEST TEST_SOURCE) + ADD_TEST_FROM_C_SOURCES( + ${TEST_SOURCE} test_lifx_gateway_core test_lifx_gateway + ) +ENDFUNCTION() + +FILE(GLOB TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test_*.c") +FOREACH(TEST ${TESTS}) + ADD_GATEWAY_TEST(${TEST}) +ENDFOREACH() diff --git a/tests/lifx/gateway/test_gateway_allocate_tag_id.c b/tests/lifx/gateway/test_gateway_allocate_tag_id.c new file mode 100644 index 0000000..0fc9495 --- /dev/null +++ b/tests/lifx/gateway/test_gateway_allocate_tag_id.c @@ -0,0 +1,103 @@ +#include "gateway.c" + +#include + +#define MOCKED_LIFX_TAGGING_INCREF +#include "test_gateway_utils.h" +#include "mock_log.h" +#include "mock_timer.h" +#include "mock_wire_proto.h" + +static bool tagging_incref_called = false; + +struct lgtd_lifx_tag * +lgtd_lifx_tagging_incref(const char *label, + struct lgtd_lifx_gateway *gw, + int tag_id) +{ + if (!label) { + errx(1, "missing tag label"); + } + if (!gw) { + errx(1, "missing gateway"); + } + if (tag_id > 2) { + errx(1, "got tag_id %d but expected < 3", tag_id); + } + + struct lgtd_lifx_tag *tag = lgtd_lifx_tagging_find_tag(label); + if (!tag) { + tag = calloc(1, sizeof(*tag)); + strcpy(tag->label, label); + struct lgtd_lifx_site *site = calloc(1, sizeof(*site)); + site->gw = gw; + site->tag_id = tag_id; + LIST_INSERT_HEAD(&tag->sites, site, link); + } + + tagging_incref_called = true; + + return tag; +} + +int +main(void) +{ + struct lgtd_lifx_gateway gw; + memset(&gw, 0, sizeof(gw)); + + struct lgtd_lifx_packet_header hdr; + memset(&hdr, 0, sizeof(hdr)); + + uint64_t expected_tag_ids = LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(0); + + lgtd_lifx_gateway_allocate_tag_id(&gw, 0, "test"); + if (!gw.tags[0]) { + errx(1, "gw.tag_ids[0] shouldn't be NULL"); + } + if (strcmp(gw.tags[0]->label, "test")) { + errx( + 1, "unexpected tag %.*s (expected test)", + (int)sizeof(gw.tags[0]->label), gw.tags[0]->label + ); + } + if (gw.tag_ids != LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(0)) { + errx( + 1, "tag_ids = %jx (expected %jx)", + (uintmax_t)gw.tag_ids, (uintmax_t)expected_tag_ids + ); + } + if (!tagging_incref_called) { + errx(1, "lgtd_lifx_tagging_incref should have been called"); + } + tagging_incref_called = false; + + for (int i = 1; i != 3; i++) { + int tag_id = lgtd_lifx_gateway_allocate_tag_id(&gw, -1, "lounge"); + if (tag_id < 1) { + errx(1, "no tag_id was allocated (received tag_id %d)", tag_id); + } + if (!gw.tags[tag_id]) { + errx(1, "gw.tag_ids[%d] shouldn't be NULL", i); + } + if (strcmp(gw.tags[tag_id]->label, "lounge")) { + errx( + 1, "unexpected tag %.*s (expected lounge)", + (int)sizeof(gw.tags[tag_id]->label), gw.tags[tag_id]->label + ); + } + expected_tag_ids |= LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id); + if (gw.tag_ids != expected_tag_ids) { + errx( + 1, "tag_ids = %jx (expected %jx)", + (uintmax_t)gw.tag_ids, (uintmax_t)expected_tag_ids + ); + } + if (!tagging_incref_called) { + errx(1, "lgtd_lifx_tagging_incref should have been called"); + } + tagging_incref_called = false; + } + + return 0; +} diff --git a/tests/lifx/gateway/test_gateway_allocate_tag_id_from_lifx_network.c b/tests/lifx/gateway/test_gateway_allocate_tag_id_from_lifx_network.c new file mode 100644 index 0000000..791eb14 --- /dev/null +++ b/tests/lifx/gateway/test_gateway_allocate_tag_id_from_lifx_network.c @@ -0,0 +1,131 @@ +#include + +#include "gateway.c" + +#define MOCKED_LIFX_TAGGING_INCREF +#include "test_gateway_utils.h" +#include "mock_log.h" +#include "mock_timer.h" +#include "mock_wire_proto.h" + +static bool tagging_incref_called = false; + +struct lgtd_lifx_tag * +lgtd_lifx_tagging_incref(const char *label, + struct lgtd_lifx_gateway *gw, + int tag_id) +{ + if (!label) { + errx(1, "missing tag label"); + } + if (!gw) { + errx(1, "missing gateway"); + } + if (tag_id != 4 && tag_id != 63) { + errx(1, "got tag_id %d but expected 4 or 63", tag_id); + } + + static struct lgtd_lifx_tag *tag = NULL; + + if (!tag) { + tag = calloc(1, sizeof(*tag)); + strcpy(tag->label, label); + struct lgtd_lifx_site *site = calloc(1, sizeof(*site)); + site->gw = gw; + site->tag_id = tag_id; + LIST_INSERT_HEAD(&tag->sites, site, link); + } + + tagging_incref_called = true; + + return tag; +} + +int +main(void) +{ + lgtd_lifx_wire_setup(); + + struct lgtd_lifx_gateway gw; + memset(&gw, 0, sizeof(gw)); + + struct lgtd_lifx_packet_header hdr; + memset(&hdr, 0, sizeof(hdr)); + + lgtd_lifx_gateway_allocate_tag_id(&gw, 4, "test"); + if (!gw.tags[4]) { + errx(1, "gw.tag_ids[4] shouldn't be NULL"); + } + if (strcmp(gw.tags[4]->label, "test")) { + errx( + 1, "unexpected tag %.*s (expected test)", + (int)sizeof(gw.tags[0]->label), gw.tags[4]->label + ); + } + if (gw.tag_ids != LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(4)) { + errx( + 1, "tag_ids = %jx (expected %jx)", + (uintmax_t)gw.tag_ids, (uintmax_t)LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(4) + ); + } + + lgtd_lifx_gateway_allocate_tag_id(&gw, 63, "test"); + if (!gw.tags[4]) { + errx(1, "gw.tag_ids[4] shouldn't be NULL"); + } + if (strcmp(gw.tags[4]->label, "test")) { + errx( + 1, "unexpected tag %.*s (expected test)", + (int)sizeof(gw.tags[0]->label), gw.tags[4]->label + ); + } + if (!gw.tags[63]) { + errx(1, "gw.tag_ids[63] shouldn't be NULL"); + } + if (strcmp(gw.tags[63]->label, "test")) { + errx( + 1, "unexpected tag %.*s (expected test)", + (int)sizeof(gw.tags[0]->label), gw.tags[4]->label + ); + } + uint64_t expected_tags = LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(4) + | LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(63); + if (gw.tag_ids != expected_tags) { + errx( + 1, "tag_ids = %jx (expected %jx)", + (uintmax_t)gw.tag_ids, (uintmax_t)expected_tags + ); + } + + tagging_incref_called = false; + lgtd_lifx_gateway_allocate_tag_id(&gw, 4, "test"); + if (!gw.tags[4]) { + errx(1, "gw.tag_ids[4] shouldn't be NULL"); + } + if (strcmp(gw.tags[4]->label, "test")) { + errx( + 1, "unexpected tag %.*s (expected test)", + (int)sizeof(gw.tags[0]->label), gw.tags[4]->label + ); + } + if (!gw.tags[63]) { + errx(1, "gw.tag_ids[63] shouldn't be NULL"); + } + if (strcmp(gw.tags[63]->label, "test")) { + errx( + 1, "unexpected tag %.*s (expected test)", + (int)sizeof(gw.tags[0]->label), gw.tags[4]->label + ); + } + if (gw.tag_ids != expected_tags) { + errx( + 1, "tag_ids = %jx (expected %jx)", + (uintmax_t)gw.tag_ids, (uintmax_t)expected_tags + ); + } + if (tagging_incref_called) { + errx(1, "lgtd_lifx_tagging_incref shouldn't have been called"); + } + + return 0; +} diff --git a/tests/lifx/gateway/test_gateway_allocate_tag_id_no_tag_id_left.c b/tests/lifx/gateway/test_gateway_allocate_tag_id_no_tag_id_left.c new file mode 100644 index 0000000..3d57a0c --- /dev/null +++ b/tests/lifx/gateway/test_gateway_allocate_tag_id_no_tag_id_left.c @@ -0,0 +1,91 @@ +#include + +#include "gateway.c" + +#define MOCKED_LIFX_TAGGING_INCREF +#include "test_gateway_utils.h" +#include "mock_log.h" +#include "mock_timer.h" +#include "mock_wire_proto.h" + +static bool tagging_incref_called = false; + +struct lgtd_lifx_tag * +lgtd_lifx_tagging_incref(const char *label, + struct lgtd_lifx_gateway *gw, + int tag_id) +{ + if (!label) { + errx(1, "missing tag label"); + } + if (!gw) { + errx(1, "missing gateway"); + } + if (tag_id < 0) { + errx(1, "got tag_id %d but expected >= 0", tag_id); + } + + struct lgtd_lifx_tag *tag = lgtd_lifx_tagging_find_tag(label); + if (!tag) { + tag = calloc(1, sizeof(*tag)); + strcpy(tag->label, label); + struct lgtd_lifx_site *site = calloc(1, sizeof(*site)); + site->gw = gw; + site->tag_id = tag_id; + LIST_INSERT_HEAD(&tag->sites, site, link); + } + + tagging_incref_called = true; + + return tag; +} + +int +main(void) +{ + lgtd_lifx_wire_setup(); + + struct lgtd_lifx_gateway gw; + memset(&gw, 0, sizeof(gw)); + + struct lgtd_lifx_packet_header hdr; + memset(&hdr, 0, sizeof(hdr)); + + uint64_t expected_tag_ids = 0; + for (int i = 0; i != LGTD_LIFX_GATEWAY_MAX_TAGS; i++) { + int tag_id = lgtd_lifx_gateway_allocate_tag_id(&gw, -1, "lounge"); + if (tag_id < 0) { + errx(1, "no tag_id was allocated (received tag_id %d)", tag_id); + } + if (!gw.tags[tag_id]) { + errx(1, "gw.tag_ids[%d] shouldn't be NULL", i); + } + if (strcmp(gw.tags[tag_id]->label, "lounge")) { + errx( + 1, "unexpected tag %.*s (expected lounge)", + (int)sizeof(gw.tags[tag_id]->label), gw.tags[tag_id]->label + ); + } + expected_tag_ids |= LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id); + if (gw.tag_ids != expected_tag_ids) { + errx( + 1, "tag_ids = %jx (expected %jx)", + (uintmax_t)gw.tag_ids, (uintmax_t)expected_tag_ids + ); + } + if (!tagging_incref_called) { + errx(1, "lgtd_lifx_tagging_incref should have been called"); + } + tagging_incref_called = false; + } + + int tag_id = lgtd_lifx_gateway_allocate_tag_id(&gw, -1, "lounge"); + if (tag_id != -1) { + errx(1, "tag_ids full but tag_id %d was allocated", tag_id); + } + if (tagging_incref_called) { + errx(1, "lgtd_lifx_tagging_incref should not have been called"); + } + + return 0; +} diff --git a/tests/lifx/gateway/test_gateway_deallocate_tag_id_from_lifx_network.c b/tests/lifx/gateway/test_gateway_deallocate_tag_id_from_lifx_network.c new file mode 100644 index 0000000..b0b97e8 --- /dev/null +++ b/tests/lifx/gateway/test_gateway_deallocate_tag_id_from_lifx_network.c @@ -0,0 +1,76 @@ +#include + +#include "gateway.c" + +#define MOCKED_LIFX_TAGGING_DECREF +#include "test_gateway_utils.h" +#include "mock_log.h" +#include "mock_timer.h" +#include "mock_wire_proto.h" + +static bool tagging_decref_called = false; + +void +lgtd_lifx_tagging_decref(struct lgtd_lifx_tag *tag, struct lgtd_lifx_gateway *gw) +{ + if (!tag) { + errx(1, "missing tag"); + } + if (!gw) { + errx(1, "missing gateway"); + } + + tagging_decref_called = true; +} + +int +main(void) +{ + lgtd_lifx_wire_setup(); + + struct lgtd_lifx_gateway gw; + memset(&gw, 0, sizeof(gw)); + + struct lgtd_lifx_packet_header hdr; + memset(&hdr, 0, sizeof(hdr)); + + struct lgtd_lifx_tag tag = { + .label = "test", + .sites = LIST_HEAD_INITIALIZER(&tag.sites) + }; + + gw.tags[0] = &tag; + gw.tag_ids = LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(0) + | LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(42); + + lgtd_lifx_gateway_deallocate_tag_id(&gw, 0); + if (gw.tags[0]) { + errx(1, "gw.tags[0] should have been set to NULL"); + } + if (gw.tag_ids != LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(42)) { + errx( + 1, "unexpected gw.tag_ids value = %jx (expected %jx)", + (uintmax_t)gw.tag_ids, (uintmax_t)LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(42) + ); + } + if (!tagging_decref_called) { + errx(1, "lgtd_lifx_tagging_decref should have been called"); + } + + tagging_decref_called = false; + lgtd_lifx_gateway_deallocate_tag_id(&gw, 0); + if (gw.tags[0]) { + errx(1, "gw.tags[0] should be NULL"); + } + if (gw.tag_ids != LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(42)) { + errx( + 1, "unexpected gw.tag_ids value = %jx (expected %jx)", + (uintmax_t)gw.tag_ids, (uintmax_t)LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(42) + ); + } + if (tagging_decref_called) { + errx(1, "lgtd_lifx_tagging_decref shouldn't have been called"); + } + + return 0; +} diff --git a/tests/lifx/gateway/test_gateway_enqueue_packet.c b/tests/lifx/gateway/test_gateway_enqueue_packet.c new file mode 100644 index 0000000..9fc356d --- /dev/null +++ b/tests/lifx/gateway/test_gateway_enqueue_packet.c @@ -0,0 +1,66 @@ +#include "gateway.c" + +#include "test_gateway_utils.h" +#include "mock_log.h" +#include "mock_timer.h" +#include "mock_wire_proto.h" + +int +main(void) +{ + lgtd_lifx_wire_setup(); + + struct lgtd_lifx_gateway gw; + memset(&gw, 0, sizeof(gw)); + gw.socket_ev = (void *)42; + + struct lgtd_lifx_packet_power_state pkt; + pkt.power = LGTD_LIFX_POWER_ON; + + union lgtd_lifx_target target = { .tags = 0 }; + + struct lgtd_lifx_packet_header hdr; + const struct lgtd_lifx_packet_info *pkt_info = lgtd_lifx_wire_setup_header( + &hdr, + LGTD_LIFX_TARGET_ALL_DEVICES, + target, + gw.site.as_array, + LGTD_LIFX_SET_POWER_STATE + ); + + lgtd_lifx_gateway_enqueue_packet(&gw, &hdr, pkt_info, &pkt); + + if (memcmp(gw_write_buf, &hdr, sizeof(hdr))) { + errx(1, "header incorrectly buffered"); + } + + if (memcmp(&gw_write_buf[sizeof(hdr)], &pkt, sizeof(pkt))) { + errx(1, "pkt incorrectly buffered"); + } + + if (gw.pkt_ring[0].type != LGTD_LIFX_SET_POWER_STATE) { + errx(1, "packet type incorrectly enqueued"); + } + + if (gw.pkt_ring[0].size != sizeof(pkt) + sizeof(hdr)) { + errx(1, "packet size incorrectly enqueued"); + } + + if (gw.pkt_ring_head != 1) { + errx(1, "packet ring head should be on index 1"); + } + + if (gw.pkt_ring_tail != 0) { + errx(1, "packet ring tail should be on index 0"); + } + + if (gw.pkt_ring_full == true) { + errx(1, "packet ring shouldn't be full"); + } + + if (last_event_passed_to_event_add != gw.socket_ev) { + errx(1, "event_add should have been called with gw.socket_ev"); + } + + return 0; +} diff --git a/tests/lifx/gateway/test_gateway_enqueue_packet_ring_full.c b/tests/lifx/gateway/test_gateway_enqueue_packet_ring_full.c new file mode 100644 index 0000000..346d929 --- /dev/null +++ b/tests/lifx/gateway/test_gateway_enqueue_packet_ring_full.c @@ -0,0 +1,88 @@ +#include "gateway.c" + +#include "test_gateway_utils.h" +#include "mock_log.h" +#include "mock_timer.h" +#include "mock_wire_proto.h" + +int +main(void) +{ + lgtd_lifx_wire_setup(); + + struct lgtd_lifx_gateway gw; + memset(&gw, 0, sizeof(gw)); + gw.socket_ev = (void *)42; + + struct lgtd_lifx_packet_power_state pkt; + pkt.power = LGTD_LIFX_POWER_ON; + + union lgtd_lifx_target target = { .tags = 0 }; + + struct lgtd_lifx_packet_header hdr; + const struct lgtd_lifx_packet_info *pkt_info = lgtd_lifx_wire_setup_header( + &hdr, + LGTD_LIFX_TARGET_ALL_DEVICES, + target, + gw.site.as_array, + LGTD_LIFX_SET_POWER_STATE + ); + + // set the head so it catches up the tail: + gw.pkt_ring_head = 1; + gw.pkt_ring_tail = 2; + + lgtd_lifx_gateway_enqueue_packet(&gw, &hdr, pkt_info, &pkt); + + if (memcmp(gw_write_buf, &hdr, sizeof(hdr))) { + errx(1, "header incorrectly buffered"); + } + + if (memcmp(&gw_write_buf[sizeof(hdr)], &pkt, sizeof(pkt))) { + errx(1, "pkt incorrectly buffered"); + } + + if (gw.pkt_ring[1].type != LGTD_LIFX_SET_POWER_STATE) { + errx(1, "packet type incorrectly enqueued"); + } + + if (gw.pkt_ring[1].size != sizeof(pkt) + sizeof(hdr)) { + errx(1, "packet size incorrectly enqueued"); + } + + if (gw.pkt_ring_head != 2) { + errx(1, "packet ring head should be on index 2"); + } + + if (gw.pkt_ring_tail != 2) { + errx(1, "packet ring tail should be on index 2"); + } + + if (gw.pkt_ring_full != true) { + errx(1, "packet ring should be full"); + } + + if (last_event_passed_to_event_add != gw.socket_ev) { + errx(1, "event_add should have been called with gw.socket_ev"); + } + + lgtd_lifx_gateway_enqueue_packet(&gw, &hdr, pkt_info, &pkt); + + if (gw_write_buf_idx != sizeof(pkt) + sizeof(hdr)) { + errx(1, "nothing should have been buffered"); + } + + if (gw.pkt_ring_head != 2) { + errx(1, "packet ring head should be on index 2"); + } + + if (gw.pkt_ring_tail != 2) { + errx(1, "packet ring tail should be on index 2"); + } + + if (gw.pkt_ring_full != true) { + errx(1, "packet ring should be full"); + } + + return 0; +} diff --git a/tests/lifx/gateway/test_gateway_enqueue_packet_ring_wraparound.c b/tests/lifx/gateway/test_gateway_enqueue_packet_ring_wraparound.c new file mode 100644 index 0000000..7319dd1 --- /dev/null +++ b/tests/lifx/gateway/test_gateway_enqueue_packet_ring_wraparound.c @@ -0,0 +1,72 @@ +#include "gateway.c" + +#include "test_gateway_utils.h" +#include "mock_log.h" +#include "mock_timer.h" +#include "mock_wire_proto.h" + +int +main(void) +{ + lgtd_lifx_wire_setup(); + + struct lgtd_lifx_gateway gw; + memset(&gw, 0, sizeof(gw)); + gw.socket_ev = (void *)42; + + struct lgtd_lifx_packet_power_state pkt; + pkt.power = LGTD_LIFX_POWER_ON; + + union lgtd_lifx_target target = { .tags = 0 }; + + struct lgtd_lifx_packet_header hdr; + const struct lgtd_lifx_packet_info *pkt_info = lgtd_lifx_wire_setup_header( + &hdr, + LGTD_LIFX_TARGET_ALL_DEVICES, + target, + gw.site.as_array, + LGTD_LIFX_SET_POWER_STATE + ); + + int pkt_ring_last_idx = LGTD_ARRAY_SIZE(gw.pkt_ring) - 1; + + // set the head so it has to wrap-around and set the tail somewhere: + gw.pkt_ring_head = pkt_ring_last_idx; + gw.pkt_ring_tail = 2; + + lgtd_lifx_gateway_enqueue_packet(&gw, &hdr, pkt_info, &pkt); + + if (memcmp(gw_write_buf, &hdr, sizeof(hdr))) { + errx(1, "header incorrectly buffered"); + } + + if (memcmp(&gw_write_buf[sizeof(hdr)], &pkt, sizeof(pkt))) { + errx(1, "pkt incorrectly buffered"); + } + + if (gw.pkt_ring[pkt_ring_last_idx].type != LGTD_LIFX_SET_POWER_STATE) { + errx(1, "packet type incorrectly enqueued"); + } + + if (gw.pkt_ring[pkt_ring_last_idx].size != sizeof(pkt) + sizeof(hdr)) { + errx(1, "packet size incorrectly enqueued"); + } + + if (gw.pkt_ring_head != 0) { + errx(1, "packet ring head should have wrapped around to index 0"); + } + + if (gw.pkt_ring_tail != 2) { + errx(1, "packet ring tail should be on index 2"); + } + + if (gw.pkt_ring_full == true) { + errx(1, "packet ring shouldn't be full"); + } + + if (last_event_passed_to_event_add != gw.socket_ev) { + errx(1, "event_add should have been called with gw.socket_ev"); + } + + return 0; +} diff --git a/tests/lifx/gateway/test_gateway_handle_ambient_light.c b/tests/lifx/gateway/test_gateway_handle_ambient_light.c new file mode 100644 index 0000000..4b10a65 --- /dev/null +++ b/tests/lifx/gateway/test_gateway_handle_ambient_light.c @@ -0,0 +1,39 @@ +#include + +#include "gateway.c" + +#include "mock_log.h" +#include "mock_timer.h" +#include "test_gateway_utils.h" +#include "tests_utils.h" +#include "mock_wire_proto.h" + +int +main(void) +{ + lgtd_lifx_wire_setup(); + + struct lgtd_lifx_gateway gw; + memset(&gw, 0, sizeof(gw)); + + struct lgtd_lifx_bulb *bulb = lgtd_tests_insert_mock_bulb(&gw, 42); + + struct lgtd_lifx_packet_header hdr; + memset(&hdr, 0, sizeof(hdr)); + memcpy( + &hdr.target.device_addr, &bulb->addr, sizeof(hdr.target.device_addr) + ); + + struct lgtd_lifx_packet_ambient_light pkt = { .illuminance = 3.14 }; + + lgtd_lifx_gateway_handle_ambient_light(&gw, &hdr, &pkt); + + if (bulb->ambient_light != pkt.illuminance) { + errx( + 1, "bulb->ambient_light = %f (expected %f)", + bulb->ambient_light, pkt.illuminance + ); + } + + return 0; +} diff --git a/tests/lifx/gateway/test_gateway_handle_bulb_label.c b/tests/lifx/gateway/test_gateway_handle_bulb_label.c new file mode 100644 index 0000000..b3b212a --- /dev/null +++ b/tests/lifx/gateway/test_gateway_handle_bulb_label.c @@ -0,0 +1,36 @@ +#include + +#include "gateway.c" + +#include "mock_log.h" +#include "mock_timer.h" +#include "test_gateway_utils.h" +#include "tests_utils.h" +#include "mock_wire_proto.h" + +int +main(void) +{ + lgtd_lifx_wire_setup(); + + struct lgtd_lifx_gateway gw = { .last_pkt_at = 42 }; + + struct lgtd_lifx_bulb *b = lgtd_tests_insert_mock_bulb(&gw, 42); + + struct lgtd_lifx_packet_header hdr = { + .packet_type = LGTD_LIFX_BULB_LABEL + }; + memcpy(&hdr.target.device_addr, &b->addr, sizeof(hdr.target.device_addr)); + struct lgtd_lifx_packet_label pkt = { .label = "TEST LABEL" }; + + lgtd_lifx_gateway_handle_bulb_label(&gw, &hdr, &pkt); + + if (memcmp(&b->state.label, &pkt.label, LGTD_LIFX_LABEL_SIZE)) { + errx( + 1, "got label %2$.*1$s, (expected %3$.*1$s)", + LGTD_LIFX_LABEL_SIZE, b->state.label, pkt.label + ); + } + + return 0; +} diff --git a/tests/lifx/gateway/test_gateway_handle_ip_firmware_info.c b/tests/lifx/gateway/test_gateway_handle_ip_firmware_info.c new file mode 100644 index 0000000..7cec092 --- /dev/null +++ b/tests/lifx/gateway/test_gateway_handle_ip_firmware_info.c @@ -0,0 +1,56 @@ +#include + +#include "gateway.c" + +#include "mock_log.h" +#include "mock_timer.h" +#include "test_gateway_utils.h" +#include "tests_utils.h" +#include "mock_wire_proto.h" + +int +main(void) +{ + lgtd_lifx_wire_setup(); + + struct lgtd_lifx_gateway gw = { .last_pkt_at = 42 }; + + struct lgtd_lifx_bulb *b = lgtd_tests_insert_mock_bulb(&gw, 42); + + struct lgtd_lifx_packet_header hdr; + memset(&hdr, 0, sizeof(hdr)); + memcpy(&hdr.target.device_addr, &b->addr, sizeof(hdr.target.device_addr)); + + struct lgtd_lifx_packet_ip_firmware_info pkt; + struct lgtd_lifx_bulb_ip *ip; + + memset(&pkt, 'A', sizeof(pkt)); + hdr.packet_type = LGTD_LIFX_MESH_FIRMWARE; + lgtd_lifx_gateway_handle_ip_firmware_info(&gw, &hdr, &pkt); + ip = &b->ips[LGTD_LIFX_BULB_MCU_IP]; + if (memcmp(&ip->fw_info, &pkt, sizeof(pkt))) { + errx(1, "The MCU ip firmware info wasn't set properly"); + } + if (ip->fw_info_updated_at != 42) { + errx( + 1, "state_updated_at %ju (expected 42)", + (uintmax_t)ip->fw_info_updated_at + ); + } + + memset(&pkt, 'B', sizeof(pkt)); + hdr.packet_type = LGTD_LIFX_WIFI_FIRMWARE_STATE; + lgtd_lifx_gateway_handle_ip_firmware_info(&gw, &hdr, &pkt); + ip = &b->ips[LGTD_LIFX_BULB_WIFI_IP]; + if (memcmp(&ip->fw_info, &pkt, sizeof(pkt))) { + errx(1, "The WIFI firmware info wasn't set properly"); + } + if (ip->fw_info_updated_at != 42) { + errx( + 1, "state_updated_at %ju (expected 42)", + (uintmax_t)ip->fw_info_updated_at + ); + } + + return 0; +} diff --git a/tests/lifx/gateway/test_gateway_handle_ip_state.c b/tests/lifx/gateway/test_gateway_handle_ip_state.c new file mode 100644 index 0000000..d7f66a2 --- /dev/null +++ b/tests/lifx/gateway/test_gateway_handle_ip_state.c @@ -0,0 +1,56 @@ +#include + +#include "gateway.c" + +#include "mock_log.h" +#include "mock_timer.h" +#include "test_gateway_utils.h" +#include "tests_utils.h" +#include "mock_wire_proto.h" + +int +main(void) +{ + lgtd_lifx_wire_setup(); + + struct lgtd_lifx_gateway gw = { .last_pkt_at = 42 }; + + struct lgtd_lifx_bulb *b = lgtd_tests_insert_mock_bulb(&gw, 42); + + struct lgtd_lifx_packet_header hdr; + memset(&hdr, 0, sizeof(hdr)); + memcpy(&hdr.target.device_addr, &b->addr, sizeof(hdr.target.device_addr)); + + struct lgtd_lifx_packet_ip_state pkt; + struct lgtd_lifx_bulb_ip *ip; + + memset(&pkt, 'A', sizeof(pkt)); + hdr.packet_type = LGTD_LIFX_MESH_INFO; + lgtd_lifx_gateway_handle_ip_state(&gw, &hdr, &pkt); + ip = &b->ips[LGTD_LIFX_BULB_MCU_IP]; + if (memcmp(&ip->state, &pkt, sizeof(pkt))) { + errx(1, "The MCU ip state wasn't set properly"); + } + if (ip->state_updated_at != 42) { + errx( + 1, "state_updated_at %ju (expected 42)", + (uintmax_t)ip->state_updated_at + ); + } + + memset(&pkt, 'B', sizeof(pkt)); + hdr.packet_type = LGTD_LIFX_WIFI_INFO; + lgtd_lifx_gateway_handle_ip_state(&gw, &hdr, &pkt); + ip = &b->ips[LGTD_LIFX_BULB_WIFI_IP]; + if (memcmp(&ip->state, &pkt, sizeof(pkt))) { + errx(1, "The WIFI ip state wasn't set properly"); + } + if (ip->state_updated_at != 42) { + errx( + 1, "state_updated_at %ju (expected 42)", + (uintmax_t)ip->state_updated_at + ); + } + + return 0; +} diff --git a/tests/lifx/gateway/test_gateway_handle_product_info.c b/tests/lifx/gateway/test_gateway_handle_product_info.c new file mode 100644 index 0000000..11ebff8 --- /dev/null +++ b/tests/lifx/gateway/test_gateway_handle_product_info.c @@ -0,0 +1,45 @@ +#include + +#include "gateway.c" + +#include "mock_log.h" +#include "mock_timer.h" +#include "test_gateway_utils.h" +#include "tests_utils.h" +#include "mock_wire_proto.h" + +int +main(void) +{ + lgtd_lifx_wire_setup(); + + struct lgtd_lifx_gateway gw = { .last_pkt_at = 42 }; + + struct lgtd_lifx_bulb *b = lgtd_tests_insert_mock_bulb(&gw, 42); + + struct lgtd_lifx_packet_header hdr = { + .packet_type = LGTD_LIFX_VERSION_STATE + }; + memcpy(&hdr.target.device_addr, &b->addr, sizeof(hdr.target.device_addr)); + struct lgtd_lifx_packet_product_info pkt = { + .vendor_id = 1, .product_id = 3, .version = 42 + }; + + lgtd_lifx_gateway_handle_product_info(&gw, &hdr, &pkt); + + if (memcmp(&b->product_info, &pkt, sizeof(pkt))) { + errx(1, "the product info weren't set correctly on the bulb"); + } + + const char *expected_model = "Color 650"; + if (strcmp(b->model, expected_model)) { + errx(1, "model %s (expected %s)", b->model, expected_model); + } + + const char *expected_vendor = "LIFX"; + if (strcmp(b->vendor, expected_vendor)) { + errx(1, "vendor %s (expected %s)", b->vendor, expected_vendor); + } + + return 0; +} diff --git a/tests/lifx/gateway/test_gateway_handle_runtime_info.c b/tests/lifx/gateway/test_gateway_handle_runtime_info.c new file mode 100644 index 0000000..1cc9254 --- /dev/null +++ b/tests/lifx/gateway/test_gateway_handle_runtime_info.c @@ -0,0 +1,42 @@ +#include + +#include "gateway.c" + +#include "mock_log.h" +#include "mock_timer.h" +#include "test_gateway_utils.h" +#include "tests_utils.h" +#include "mock_wire_proto.h" + +int +main(void) +{ + lgtd_lifx_wire_setup(); + + struct lgtd_lifx_gateway gw = { .last_pkt_at = 42 }; + + struct lgtd_lifx_bulb *b = lgtd_tests_insert_mock_bulb(&gw, 42); + + struct lgtd_lifx_packet_header hdr = { + .packet_type = LGTD_LIFX_INFO_STATE + }; + memcpy(&hdr.target.device_addr, &b->addr, sizeof(hdr.target.device_addr)); + struct lgtd_lifx_packet_runtime_info pkt = { + .time = 1, .downtime = 2, .uptime = 3 + }; + + lgtd_lifx_gateway_handle_runtime_info(&gw, &hdr, &pkt); + + if (memcmp(&b->runtime_info, &pkt, sizeof(pkt))) { + errx(1, "the product info weren't set correctly on the bulb"); + } + + if (b->runtime_info_updated_at != gw.last_pkt_at) { + errx( + 1, "runtime_info_updated_at = %ju (expected 42)", + (uintmax_t)b->runtime_info_updated_at + ); + } + + return 0; +} diff --git a/tests/lifx/gateway/test_gateway_handle_tag_labels.c b/tests/lifx/gateway/test_gateway_handle_tag_labels.c new file mode 100644 index 0000000..6dfdc86 --- /dev/null +++ b/tests/lifx/gateway/test_gateway_handle_tag_labels.c @@ -0,0 +1,83 @@ +#include + +#include "gateway.c" + +#include "test_gateway_utils.h" +#include "mock_log.h" +#include "mock_timer.h" +#include "mock_wire_proto.h" + +int +main(void) +{ + lgtd_lifx_wire_setup(); + + struct lgtd_lifx_gateway gw; + memset(&gw, 0, sizeof(gw)); + + struct lgtd_lifx_packet_header hdr; + memset(&hdr, 0, sizeof(hdr)); + + struct lgtd_lifx_packet_tag_labels pkt = { + .label = "test", .tags = 0 + }; + + lgtd_lifx_gateway_handle_tag_labels(&gw, &hdr, &pkt); + if (gw.tag_ids != 0) { + errx(1, "expected gw.tag_ids == 0 but got %jx", (uintmax_t)gw.tag_ids); + } + + pkt.tags = LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(42); + lgtd_lifx_gateway_handle_tag_labels(&gw, &hdr, &pkt); + if (gw.tag_ids != LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(42)) { + errx( + 1, "expected gw.tag_ids == %jx but got %jx", + (uintmax_t)LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(42), (uintmax_t)gw.tag_ids + ); + } + if (!gw.tags[42]) { + errx(1, "tag_id 42 should have been set"); + } + if (strcmp(gw.tags[42]->label, pkt.label)) { + errx( + 1, "unexpected label %.*s (expected %s)", + (int)sizeof(gw.tags[0]->label), gw.tags[42]->label, pkt.label + ); + } + + strcpy(pkt.label, "toto"); + pkt.tags = LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(2) + | LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(4); + lgtd_lifx_gateway_handle_tag_labels(&gw, &hdr, &pkt); + memset(&pkt, 0, sizeof(pkt)); + lgtd_lifx_gateway_handle_tag_labels(&gw, &hdr, &pkt); + uint64_t expected = LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(42) + | LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(2) + | LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(4); + if (gw.tag_ids != expected) { + errx( + 1, "expected gw.tag_ids == %jx but got %jx", + (uintmax_t)LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(42), (uintmax_t)gw.tag_ids + ); + } + if (strcmp(gw.tags[2]->label, "toto")) { + errx( + 1, "unexpected label %.*s (expected %s)", + (int)sizeof(gw.tags[0]->label), gw.tags[2]->label, "toto" + ); + } + if (strcmp(gw.tags[4]->label, "toto")) { + errx( + 1, "unexpected label %.*s (expected %s)", + (int)sizeof(gw.tags[0]->label), gw.tags[4]->label, "toto" + ); + } + if (strcmp(gw.tags[42]->label, "test")) { + errx( + 1, "unexpected label %.*s (expected %s)", + (int)sizeof(gw.tags[0]->label), gw.tags[42]->label, "test" + ); + } + + return 0; +} diff --git a/tests/lifx/gateway/test_gateway_handle_tags.c b/tests/lifx/gateway/test_gateway_handle_tags.c new file mode 100644 index 0000000..cfe8fbf --- /dev/null +++ b/tests/lifx/gateway/test_gateway_handle_tags.c @@ -0,0 +1,38 @@ +#include + +#include "gateway.c" + +#include "mock_log.h" +#include "mock_timer.h" +#include "test_gateway_utils.h" +#include "tests_utils.h" +#include "mock_wire_proto.h" + +int +main(void) +{ + lgtd_lifx_wire_setup(); + + struct lgtd_lifx_gateway gw; + memset(&gw, 0, sizeof(gw)); + + struct lgtd_lifx_bulb *bulb = lgtd_tests_insert_mock_bulb(&gw, 42); + + struct lgtd_lifx_packet_header hdr; + memset(&hdr, 0, sizeof(hdr)); + memcpy( + &hdr.target.device_addr, &bulb->addr, sizeof(hdr.target.device_addr) + ); + + struct lgtd_lifx_packet_tags pkt = { .tags = 0x7 }; + + lgtd_lifx_gateway_handle_tags(&gw, &hdr, &pkt); + + if (bulb->state.tags != 0x7) { + errx( + 1, "bulb->tags = %#jx (expected 0x7)", (uintmax_t)bulb->state.tags + ); + } + + return 0; +} diff --git a/tests/lifx/gateway/test_gateway_update_tag_refcounts.c b/tests/lifx/gateway/test_gateway_update_tag_refcounts.c new file mode 100644 index 0000000..ac0d38c --- /dev/null +++ b/tests/lifx/gateway/test_gateway_update_tag_refcounts.c @@ -0,0 +1,125 @@ +#include "gateway.c" + +#include "test_gateway_utils.h" +#include "mock_log.h" +#include "mock_timer.h" +#define MOCKED_LGTD_LIFX_WIRE_ENCODE_TAG_LABELS +#include "mock_wire_proto.h" + +static int lifx_wire_encode_tag_labels_call_count = 0; + +void +lgtd_lifx_wire_encode_tag_labels(struct lgtd_lifx_packet_tag_labels *pkt) +{ + (void)pkt; + + lifx_wire_encode_tag_labels_call_count++; +} + +int +main(void) +{ + lgtd_lifx_wire_setup(); + + struct lgtd_lifx_gateway gw; + memset(&gw, 0, sizeof(gw)); + + lgtd_lifx_gateway_update_tag_refcounts(&gw, 0, 0); + for (int i = 0; i != LGTD_LIFX_GATEWAY_MAX_TAGS; i++) { + if (gw.tag_refcounts[i]) { + errx( + 1, "gw.tag_refcounts[%d] was %d, (expected 0)", + i, gw.tag_refcounts[i] + ); + } + } + + for (int n = 1; n != 3; n++) { + lgtd_lifx_gateway_update_tag_refcounts(&gw, 0, 1); + if (gw.tag_refcounts[0] != n) { + errx( + 1, "gw.tag_refcounts[0] was %d (expected %d)", + gw.tag_refcounts[0], n + ); + } + for (int i = 1; i != LGTD_LIFX_GATEWAY_MAX_TAGS; i++) { + if (gw.tag_refcounts[i]) { + errx( + 1, "gw.tag_refcounts[%d] was %d (expected 0)", + i, gw.tag_refcounts[i] + ); + } + } + } + + lgtd_lifx_gateway_update_tag_refcounts(&gw, 0, 2); + gw.tag_ids = 0x2; + + for (int n = 1; n >= 0; n--) { + lgtd_lifx_gateway_update_tag_refcounts(&gw, 1, 0); + if (gw.tag_refcounts[0] != n) { + errx( + 1, "gw.tag_refcounts[0] was %d (expected %d)", + gw.tag_refcounts[0], n - 1 + ); + } + if (gw.tag_refcounts[1] != 1) { + errx( + 1, "gw.tag_refcounts[1] was %d (expected 1)", + gw.tag_refcounts[1] + ); + } + for (int i = 2; i != LGTD_LIFX_GATEWAY_MAX_TAGS; i++) { + if (gw.tag_refcounts[i]) { + errx( + 1, "gw.tag_refcounts[%d] was %d (expected 0)", + i, gw.tag_refcounts[i] + ); + } + } + } + if (gw.pkt_ring[0].type != LGTD_LIFX_SET_TAG_LABELS) { + errx(1, "SET_TAG_LABELS should have been enqueued on the gateway"); + } + + struct lgtd_lifx_packet_tag_labels *pkt = + (void *)&gw_write_buf[sizeof(struct lgtd_lifx_packet_header)]; + if (lifx_wire_encode_tag_labels_call_count != 1) { + errx( + 1, "lifx_wire_encode_tag_labels_call_count == %d (expected 1)", + lifx_wire_encode_tag_labels_call_count + ); + } + if (pkt->tags != ~2ULL) { + errx( + 1, "tags on LGTD_LIFX_SET_TAG_LABELS was %#jx (expected %#jx)", + (uintmax_t)pkt->tags, (uintmax_t)~2ULL + ); + } + const char blank_label[LGTD_LIFX_LABEL_SIZE] = { 0 }; + if (memcmp(pkt->label, blank_label, LGTD_LIFX_LABEL_SIZE)) { + errx( + 1, "label on LGTD_LIFX_SET_TAG_LABELS should be " + "all zero but got %.*s", LGTD_LIFX_LABEL_SIZE, pkt->label + ); + } + + for (int n = 0; n != UINT8_MAX; n++) { + lgtd_lifx_gateway_update_tag_refcounts(&gw, 0, 4); + } + if (gw.tag_refcounts[2] != UINT8_MAX) { + errx( + 1, "gw.tag_refcounts[2] was %d (expected %d)", + gw.tag_refcounts[2], UINT8_MAX + ); + } + lgtd_lifx_gateway_update_tag_refcounts(&gw, 0, 4); + if (gw.tag_refcounts[2] != UINT8_MAX) { + errx( + 1, "gw.tag_refcounts[2] was %d (expected %d)", + gw.tag_refcounts[2], UINT8_MAX + ); + } + + return 0; +} diff --git a/tests/lifx/gateway/test_gateway_utils.h b/tests/lifx/gateway/test_gateway_utils.h new file mode 100644 index 0000000..127f74b --- /dev/null +++ b/tests/lifx/gateway/test_gateway_utils.h @@ -0,0 +1,176 @@ +#pragma once + +static char gw_write_buf[4096] = { 0 }; +static int gw_write_buf_idx = 0; + +static inline void +reset_gw_write_buf(void) +{ + memset(gw_write_buf, 0, sizeof(gw_write_buf)); + gw_write_buf_idx = 0; +} + +int +evbuffer_add(struct evbuffer *buf, const void *data, size_t datlen) +{ + (void)buf; + int to_write = LGTD_MIN( + datlen, sizeof(gw_write_buf) - gw_write_buf_idx + ); + memcpy(&gw_write_buf[gw_write_buf_idx], data, to_write); + gw_write_buf_idx += to_write; + return 0; +} + +struct lgtd_lifx_tag_list lgtd_lifx_tags = LIST_HEAD_INITIALIZER(&lgtd_lifx_tags); + +#ifndef MOCKED_EVBUFFER_GET_LENGTH +size_t +evbuffer_get_length(const struct evbuffer *buf) +{ + (void)buf; + return gw_write_buf_idx + 1; +} +#endif + +#ifndef MOCKED_EVBUFFER_WRITE_ATMOST +int +evbuffer_write_atmost(struct evbuffer *buf, + evutil_socket_t fd, + ev_ssize_t howmuch) +{ + (void)buf; + (void)fd; + (void)howmuch; + return howmuch; +} +#endif + +#ifndef MOCKED_LIFX_TAGGING_INCREF +struct lgtd_lifx_tag * +lgtd_lifx_tagging_incref(const char *label, + struct lgtd_lifx_gateway *gw, + int tag_id) +{ + struct lgtd_lifx_tag *tag = calloc(1, sizeof(*tag)); + strcpy(tag->label, label); + LIST_INSERT_HEAD(&lgtd_lifx_tags, tag, link); + struct lgtd_lifx_site *site = calloc(1, sizeof(*site)); + site->gw = gw; + site->tag_id = tag_id; + LIST_INSERT_HEAD(&tag->sites, site, link); + return tag; +} +#endif + +#ifndef MOCKED_LIFX_TAGGING_DECREF +void +lgtd_lifx_tagging_decref(struct lgtd_lifx_tag *tag, + struct lgtd_lifx_gateway *gw) +{ + (void)tag; + (void)gw; +} +#endif + +struct evbuffer * +evbuffer_new(void) +{ + return NULL; +} + +void +evbuffer_free(struct evbuffer *buf) +{ + (void)buf; +} + +struct lgtd_lifx_tag * +lgtd_lifx_tagging_find_tag(const char *tag_label) +{ + struct lgtd_lifx_tag *tag = NULL; + LIST_FOREACH(tag, &lgtd_lifx_tags, link) { + if (!strcmp(tag->label, tag_label)) { + break; + } + } + return tag; +} + +static struct event *last_event_passed_to_event_add = NULL; + +int +event_add(struct event *ev, const struct timeval *timeout) +{ + (void)timeout; + last_event_passed_to_event_add = ev; + return 0; +} + +static struct event *last_event_passed_to_event_del = NULL; + +int +event_del(struct event *ev) +{ + last_event_passed_to_event_del = ev; + return 0; +} + +void +event_active(struct event *ev, int res, short ncalls) +{ + (void)ev; + (void)res; + (void)ncalls; +} + +struct event * +event_new(struct event_base *evbase, + evutil_socket_t sock, + short events, + event_callback_fn cb, + void *ctx) +{ + (void)evbase; + (void)sock; + (void)events; + (void)cb; + (void)ctx; + return NULL; +} + +void +event_free(struct event *ev) +{ + (void)ev; +} + +int +event_pending(const struct event *ev, short events, struct timeval *tv) +{ + (void)ev; + (void)events; + (void)tv; + return 0; +} + +int +evutil_closesocket(evutil_socket_t sock) +{ + (void)sock; + return 0; +} + +int +evutil_make_socket_nonblocking(evutil_socket_t sock) +{ + (void)sock; + return 0; +} + +int +evutil_make_listen_socket_reuseable(evutil_socket_t sock) +{ + (void)sock; + return 0; +} diff --git a/tests/lifx/gateway/test_gateway_write_callback.c b/tests/lifx/gateway/test_gateway_write_callback.c new file mode 100644 index 0000000..dff4b27 --- /dev/null +++ b/tests/lifx/gateway/test_gateway_write_callback.c @@ -0,0 +1,80 @@ +#include "gateway.c" + +#define MOCKED_EVBUFFER_WRITE_ATMOST +#define MOCKED_EVBUFFER_GET_LENGTH +#include "test_gateway_utils.h" +#include "mock_log.h" +#include "mock_timer.h" +#include "mock_wire_proto.h" + +size_t +evbuffer_get_length(const struct evbuffer *buf) +{ + if (buf != (void *)42) { + errx(1, "didn't get the expected evbuffer"); + } + + // fake another packet to write: + return sizeof(struct lgtd_lifx_packet_header); +} + +int +evbuffer_write_atmost(struct evbuffer *buf, + evutil_socket_t fd, + ev_ssize_t howmuch) +{ + if (fd != 25) { + errx(1, "evbuffer_write_atmost didn't get the expected socket"); + } + + if (buf != (void *)42) { + errx(1, "evbuffer_write_atmost didn't get the expected evbuffer"); + } + + int expected = sizeof(struct lgtd_lifx_packet_header); + expected += sizeof(struct lgtd_lifx_packet_power_state); + if (howmuch != expected) { + errx( + 1, "evbuffer_write_atmost expected %d but got %jd", + expected, (intmax_t)howmuch + ); + } + + return howmuch; +} + +int +main(void) +{ + lgtd_lifx_wire_setup(); + + struct lgtd_lifx_gateway gw; + memset(&gw, 0, sizeof(gw)); + + // fake some values: + gw.socket = 25; + gw.socket_ev = (void *)21; + gw.write_buf = (void *)42; + + gw.pkt_ring[0].size += sizeof(struct lgtd_lifx_packet_header); + gw.pkt_ring[0].size += sizeof(struct lgtd_lifx_packet_power_state); + gw.pkt_ring[0].type = LGTD_LIFX_SET_POWER_STATE; + gw.pkt_ring_head++; + gw.pkt_ring_head++; + + lgtd_lifx_gateway_socket_event_callback(-1, EV_WRITE, &gw); + + if (gw.pkt_ring[0].size != 0 || gw.pkt_ring[0].type != 0) { + errx(1, "the ring entry should have been reset"); + } + + if (gw.pkt_ring_tail != 1) { + errx(1, "the tail shoud have been moved by one"); + } + + if (last_event_passed_to_event_del != NULL) { + errx(1, "event_del shouldn't have ben called"); + } + + return 0; +} diff --git a/tests/lifx/gateway/test_gateway_write_callback_clears_ring_full_flag.c b/tests/lifx/gateway/test_gateway_write_callback_clears_ring_full_flag.c new file mode 100644 index 0000000..05c9a58 --- /dev/null +++ b/tests/lifx/gateway/test_gateway_write_callback_clears_ring_full_flag.c @@ -0,0 +1,83 @@ +#include "gateway.c" + +#define MOCKED_EVBUFFER_WRITE_ATMOST +#define MOCKED_EVBUFFER_GET_LENGTH +#include "test_gateway_utils.h" +#include "mock_log.h" +#include "mock_timer.h" +#include "mock_wire_proto.h" + +size_t +evbuffer_get_length(const struct evbuffer *buf) +{ + if (buf != (void *)42) { + errx(1, "didn't get the expected evbuffer"); + } + + // fake another packet to write: + return sizeof(struct lgtd_lifx_packet_header); +} + +int +evbuffer_write_atmost(struct evbuffer *buf, + evutil_socket_t fd, + ev_ssize_t howmuch) +{ + if (fd != 25) { + errx(1, "evbuffer_write_atmost didn't get the expected socket"); + } + + if (buf != (void *)42) { + errx(1, "evbuffer_write_atmost didn't get the expected evbuffer"); + } + + int expected = sizeof(struct lgtd_lifx_packet_header); + expected += sizeof(struct lgtd_lifx_packet_power_state); + if (howmuch != expected) { + errx( + 1, "evbuffer_write_atmost expected %d but got %jd", + expected, (intmax_t)howmuch + ); + } + + return howmuch; +} + +int +main(void) +{ + lgtd_lifx_wire_setup(); + + struct lgtd_lifx_gateway gw; + memset(&gw, 0, sizeof(gw)); + + // fake some values: + gw.socket = 25; + gw.socket_ev = (void *)21; + gw.write_buf = (void *)42; + + gw.pkt_ring[0].size += sizeof(struct lgtd_lifx_packet_header); + gw.pkt_ring[0].size += sizeof(struct lgtd_lifx_packet_power_state); + gw.pkt_ring[0].type = LGTD_LIFX_SET_POWER_STATE; + gw.pkt_ring_full = true; + + lgtd_lifx_gateway_socket_event_callback(-1, EV_WRITE, &gw); + + if (gw.pkt_ring[0].size != 0 || gw.pkt_ring[0].type != 0) { + errx(1, "the ring entry should have been reset"); + } + + if (gw.pkt_ring_tail != 1) { + errx(1, "the tail shoud have been moved by one"); + } + + if (last_event_passed_to_event_del != NULL) { + errx(1, "event_del shouldn't have ben called"); + } + + if (gw.pkt_ring_full) { + errx(1, "the ring full flag should have been cleared out"); + } + + return 0; +} diff --git a/tests/lifx/gateway/test_gateway_write_callback_last_packet_on_ring.c b/tests/lifx/gateway/test_gateway_write_callback_last_packet_on_ring.c new file mode 100644 index 0000000..a5fd2a3 --- /dev/null +++ b/tests/lifx/gateway/test_gateway_write_callback_last_packet_on_ring.c @@ -0,0 +1,79 @@ +#include "gateway.c" + +#define MOCKED_EVBUFFER_WRITE_ATMOST +#define MOCKED_EVBUFFER_GET_LENGTH +#include "test_gateway_utils.h" +#include "mock_log.h" +#include "mock_timer.h" +#include "mock_wire_proto.h" + +size_t +evbuffer_get_length(const struct evbuffer *buf) +{ + if (buf != (void *)42) { + errx(1, "didn't get the expected evbuffer"); + } + + return 0; +} + +int +evbuffer_write_atmost(struct evbuffer *buf, + evutil_socket_t fd, + ev_ssize_t howmuch) +{ + if (fd != 25) { + errx(1, "evbuffer_write_atmost didn't get the expected socket"); + } + + if (buf != (void *)42) { + errx(1, "evbuffer_write_atmost didn't get the expected evbuffer"); + } + + int expected = sizeof(struct lgtd_lifx_packet_header); + expected += sizeof(struct lgtd_lifx_packet_power_state); + if (howmuch != expected) { + errx( + 1, "evbuffer_write_atmost expected %d but got %jd", + expected, (intmax_t)howmuch + ); + } + + return howmuch; +} + +int +main(void) +{ + lgtd_lifx_wire_setup(); + + struct lgtd_lifx_gateway gw; + memset(&gw, 0, sizeof(gw)); + + // fake some values: + gw.socket = 25; + gw.socket_ev = (void *)21; + gw.write_buf = (void *)42; + + gw.pkt_ring[0].size += sizeof(struct lgtd_lifx_packet_header); + gw.pkt_ring[0].size += sizeof(struct lgtd_lifx_packet_power_state); + gw.pkt_ring[0].type = LGTD_LIFX_SET_POWER_STATE; + gw.pkt_ring_head++; + gw.pkt_ring_head++; + + lgtd_lifx_gateway_socket_event_callback(-1, EV_WRITE, &gw); + + if (gw.pkt_ring[0].size != 0 || gw.pkt_ring[0].type != 0) { + errx(1, "the ring entry should have been reset"); + } + + if (gw.pkt_ring_tail != 1) { + errx(1, "the tail shoud have been moved by one"); + } + + if (last_event_passed_to_event_del != (void *)21) { + errx(1, "event_del should have ben called"); + } + + return 0; +} diff --git a/tests/lifx/gateway/test_gateway_write_callback_partial_write.c b/tests/lifx/gateway/test_gateway_write_callback_partial_write.c new file mode 100644 index 0000000..d3e79f6 --- /dev/null +++ b/tests/lifx/gateway/test_gateway_write_callback_partial_write.c @@ -0,0 +1,104 @@ +#include "gateway.c" + +#define MOCKED_EVBUFFER_WRITE_ATMOST +#define MOCKED_EVBUFFER_GET_LENGTH +#include "test_gateway_utils.h" +#include "mock_log.h" +#include "mock_timer.h" +#include "mock_wire_proto.h" + +size_t +evbuffer_get_length(const struct evbuffer *buf) +{ + if (buf != (void *)42) { + errx(1, "didn't get the expected evbuffer"); + } + + // fake another packet to write: + return sizeof(struct lgtd_lifx_packet_header); +} + +int +evbuffer_write_atmost(struct evbuffer *buf, + evutil_socket_t fd, + ev_ssize_t howmuch) +{ + if (fd != 25) { + errx(1, "evbuffer_write_atmost didn't get the expected socket"); + } + + if (buf != (void *)42) { + errx(1, "evbuffer_write_atmost didn't get the expected evbuffer"); + } + + static int expected = ( + sizeof(struct lgtd_lifx_packet_header) + + sizeof(struct lgtd_lifx_packet_power_state) + ); + if (howmuch != expected) { + errx( + 1, "evbuffer_write_atmost expected %d but got %jd", + expected, (intmax_t)howmuch + ); + } + + if (expected != sizeof(struct lgtd_lifx_packet_power_state)) { + expected -= sizeof(struct lgtd_lifx_packet_header); + return sizeof(struct lgtd_lifx_packet_header); + } + + return howmuch; +} + +int +main(void) +{ + lgtd_lifx_wire_setup(); + + struct lgtd_lifx_gateway gw; + memset(&gw, 0, sizeof(gw)); + + // fake some values: + gw.socket = 25; + gw.write_buf = (void *)42; + + gw.pkt_ring[0].size += sizeof(struct lgtd_lifx_packet_header); + gw.pkt_ring[0].size += sizeof(struct lgtd_lifx_packet_power_state); + gw.pkt_ring[0].type = LGTD_LIFX_SET_POWER_STATE; + gw.pkt_ring_head++; + gw.pkt_ring_head++; + + lgtd_lifx_gateway_socket_event_callback(-1, EV_WRITE, &gw); + + if (gw.pkt_ring[0].type != LGTD_LIFX_SET_POWER_STATE) { + errx(1, "the ring entry doesn't have the right packet type"); + } + + if (gw.pkt_ring[0].size != sizeof(struct lgtd_lifx_packet_power_state)) { + errx(1, "the ring entry doesn't have the right size value"); + } + + if (gw.pkt_ring_tail != 0) { + errx(1, "the tail shoudn't have been moved by one"); + } + + if (last_event_passed_to_event_del != NULL) { + errx(1, "event_del shouldn't have ben called"); + } + + lgtd_lifx_gateway_socket_event_callback(-1, EV_WRITE, &gw); + + if (gw.pkt_ring[0].size != 0 || gw.pkt_ring[0].type != 0) { + errx(1, "the ring entry should have been reset"); + } + + if (gw.pkt_ring_tail != 1) { + errx(1, "the tail shoud have been moved by one"); + } + + if (last_event_passed_to_event_del != NULL) { + errx(1, "event_del shouldn't have ben called"); + } + + return 0; +} diff --git a/tests/lifx/gateway/test_gateway_write_callback_ring_wraparound.c b/tests/lifx/gateway/test_gateway_write_callback_ring_wraparound.c new file mode 100644 index 0000000..a85ad45 --- /dev/null +++ b/tests/lifx/gateway/test_gateway_write_callback_ring_wraparound.c @@ -0,0 +1,82 @@ +#include "gateway.c" + +#define MOCKED_EVBUFFER_WRITE_ATMOST +#define MOCKED_EVBUFFER_GET_LENGTH +#include "test_gateway_utils.h" +#include "mock_log.h" +#include "mock_timer.h" +#include "mock_wire_proto.h" + +size_t +evbuffer_get_length(const struct evbuffer *buf) +{ + if (buf != (void *)42) { + errx(1, "didn't get the expected evbuffer"); + } + + // fake another packet to write: + return sizeof(struct lgtd_lifx_packet_header); +} + +int +evbuffer_write_atmost(struct evbuffer *buf, + evutil_socket_t fd, + ev_ssize_t howmuch) +{ + if (fd != 25) { + errx(1, "evbuffer_write_atmost didn't get the expected socket"); + } + + if (buf != (void *)42) { + errx(1, "evbuffer_write_atmost didn't get the expected evbuffer"); + } + + int expected = sizeof(struct lgtd_lifx_packet_header); + expected += sizeof(struct lgtd_lifx_packet_power_state); + if (howmuch != expected) { + errx( + 1, "evbuffer_write_atmost expected %d but got %jd", + expected, (intmax_t)howmuch + ); + } + + return howmuch; +} + +int +main(void) +{ + lgtd_lifx_wire_setup(); + + struct lgtd_lifx_gateway gw; + memset(&gw, 0, sizeof(gw)); + + // fake some values: + gw.socket = 25; + gw.socket_ev = (void *)21; + gw.write_buf = (void *)42; + + int pkt_ring_last_idx = LGTD_ARRAY_SIZE(gw.pkt_ring) - 1; + gw.pkt_ring_tail = pkt_ring_last_idx; + + gw.pkt_ring[pkt_ring_last_idx].size += sizeof(struct lgtd_lifx_packet_header); + gw.pkt_ring[pkt_ring_last_idx].size += sizeof(struct lgtd_lifx_packet_power_state); + gw.pkt_ring[pkt_ring_last_idx].type = LGTD_LIFX_SET_POWER_STATE; + + lgtd_lifx_gateway_socket_event_callback(-1, EV_WRITE, &gw); + + if (gw.pkt_ring[pkt_ring_last_idx].size != 0 + || gw.pkt_ring[pkt_ring_last_idx].type != 0) { + errx(1, "the ring entry should have been reset"); + } + + if (gw.pkt_ring_tail != 0) { + errx(1, "the tail shoud have wrapped around to 0"); + } + + if (last_event_passed_to_event_del != NULL) { + errx(1, "event_del shouldn't have ben called"); + } + + return 0; +} diff --git a/tests/lifx/mock_gateway.h b/tests/lifx/mock_gateway.h new file mode 100644 index 0000000..0d10499 --- /dev/null +++ b/tests/lifx/mock_gateway.h @@ -0,0 +1,234 @@ +#pragma once + +#include "core/time_monotonic.h" +#include "lifx/bulb.h" +#include "lifx/gateway.h" + +struct lgtd_lifx_tag; + +struct lgtd_lifx_gateway_list lgtd_lifx_gateways = + LIST_HEAD_INITIALIZER(&lgtd_lifx_gateways); + +#ifndef MOCKED_LGTD_LIFX_GATEWAY_LATENCY +lgtd_time_mono_t +lgtd_lifx_gateway_latency(const struct lgtd_lifx_gateway *gw) +{ + (void)gw; + return 0; +} +#endif + +#ifndef MOCKED_LGTD_LIFX_GATEWAY_HANDLE_PACKET +void +lgtd_lifx_gateway_handle_packet(struct lgtd_lifx_gateway *gw, + const struct sockaddr *peer, + ev_socklen_t addrlen, + const struct lgtd_lifx_packet_info *pkt_info, + const struct lgtd_lifx_packet_header *hdr, + const void *pkt, + lgtd_time_mono_t received_at) +{ + (void)gw; + (void)peer; + (void)addrlen; + (void)pkt_info; + (void)hdr; + (void)pkt; + (void)received_at; +} +#endif + +#ifndef MOCKED_LIFX_GATEWAY_SEND_TO_SITE +bool +lgtd_lifx_gateway_send_to_site(struct lgtd_lifx_gateway *gw, + enum lgtd_lifx_packet_type pkt_type, + void *pkt) +{ + (void)gw; + (void)pkt_type; + (void)pkt; + return false; +} +#endif + +#ifndef MOCKED_LIFX_GATEWAY_ALLOCATE_TAG_ID +int +lgtd_lifx_gateway_allocate_tag_id(struct lgtd_lifx_gateway *gw, + int tag_id, + const char *tag_label) +{ + (void)gw; + (void)tag_id; + (void)tag_label; + return -1; +} +#endif + +#ifndef MOCKED_LGTD_LIFX_GATEWAY_HANDLE_PAN_GATEWAY +void +lgtd_lifx_gateway_handle_pan_gateway(struct lgtd_lifx_gateway *gw, + const struct lgtd_lifx_packet_header *hdr, + const struct lgtd_lifx_packet_pan_gateway *pkt) +{ + (void)gw; + (void)hdr; + (void)pkt; +} +#endif + +#ifndef MOCKED_LGTD_LIFX_GATEWAY_HANDLE_LIGHT_STATUS +void +lgtd_lifx_gateway_handle_light_status(struct lgtd_lifx_gateway *gw, + const struct lgtd_lifx_packet_header *hdr, + const struct lgtd_lifx_packet_light_status *pkt) +{ + (void)gw; + (void)hdr; + (void)pkt; +} +#endif + +#ifndef MOCKED_LGTD_LIFX_GATEWAY_HANDLE_POWER_STATE +void +lgtd_lifx_gateway_handle_power_state(struct lgtd_lifx_gateway *gw, + const struct lgtd_lifx_packet_header *hdr, + const struct lgtd_lifx_packet_power_state *pkt) +{ + (void)gw; + (void)hdr; + (void)pkt; +} +#endif + +#ifndef MOCKED_LGTD_LIFX_GATEWAY_HANDLE_TAG_LABELS +void +lgtd_lifx_gateway_handle_tag_labels(struct lgtd_lifx_gateway *gw, + const struct lgtd_lifx_packet_header *hdr, + const struct lgtd_lifx_packet_tag_labels *pkt) +{ + (void)gw; + (void)hdr; + (void)pkt; +} +#endif + +#ifndef MOCKED_LGTD_LIFX_GATEWAY_HANDLE_TAGS +void +lgtd_lifx_gateway_handle_tags(struct lgtd_lifx_gateway *gw, + const struct lgtd_lifx_packet_header *hdr, + const struct lgtd_lifx_packet_tags *pkt) +{ + (void)gw; + (void)hdr; + (void)pkt; +} +#endif + +#ifndef MOCKED_LGTD_LIFX_GATEWAY_DEALLOCATE_TAG_ID +void +lgtd_lifx_gateway_deallocate_tag_id(struct lgtd_lifx_gateway *gw, int tag_id) +{ + (void)gw; + (void)tag_id; +} +#endif + +#ifndef MOCKED_LGTD_LIFX_GATEWAY_GET_TAG_ID +int +lgtd_lifx_gateway_get_tag_id(const struct lgtd_lifx_gateway *gw, + const struct lgtd_lifx_tag *tag) +{ + int tag_id; + LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, gw->tag_ids) { + if (gw->tags[tag_id] == tag) { + return tag_id; + } + } + + return -1; +} +#endif + +#ifndef MOCKED_LGTD_LIFX_GATEWAY_UPDATE_TAG_REFCOUNTS +void +lgtd_lifx_gateway_update_tag_refcounts(struct lgtd_lifx_gateway *gw, + uint64_t bulb_tags, + uint64_t pkt_tags) +{ + (void)gw; + (void)bulb_tags; + (void)pkt_tags; +} +#endif + +#ifndef MOCKED_LGTD_LIFX_GATEWAY_HANDLE_IP_STATE +void +lgtd_lifx_gateway_handle_ip_state(struct lgtd_lifx_gateway *gw, + const struct lgtd_lifx_packet_header *hdr, + const struct lgtd_lifx_packet_ip_state *pkt) +{ + (void)gw; + (void)hdr; + (void)pkt; +} +#endif + +#ifndef MOCKED_LGTD_LIFX_GATEWAY_HANDLE_IP_FIRMWARE_INFO +void +lgtd_lifx_gateway_handle_ip_firmware_info(struct lgtd_lifx_gateway *gw, + const struct lgtd_lifx_packet_header *hdr, + const struct lgtd_lifx_packet_ip_firmware_info *pkt) +{ + (void)gw; + (void)hdr; + (void)pkt; +} +#endif + +#ifndef MOCKED_LGTD_LIFX_GATEWAY_HANDLE_PRODUCT_INFO +void +lgtd_lifx_gateway_handle_product_info(struct lgtd_lifx_gateway *gw, + const struct lgtd_lifx_packet_header *hdr, + const struct lgtd_lifx_packet_product_info *pkt) +{ + (void)gw; + (void)hdr; + (void)pkt; +} +#endif + +#ifndef MOCKED_LGTD_LIFX_GATEWAY_HANDLE_RUNTIME_INFO +void +lgtd_lifx_gateway_handle_runtime_info(struct lgtd_lifx_gateway *gw, + const struct lgtd_lifx_packet_header *hdr, + const struct lgtd_lifx_packet_runtime_info *pkt) +{ + (void)gw; + (void)hdr; + (void)pkt; +} +#endif + +#ifndef MOCKED_LGTD_LIFX_GATEWAY_HANDLE_BULB_LABEL +void +lgtd_lifx_gateway_handle_bulb_label(struct lgtd_lifx_gateway *gw, + const struct lgtd_lifx_packet_header *hdr, + const struct lgtd_lifx_packet_label *pkt) +{ + (void)gw; + (void)hdr; + (void)pkt; +} +#endif + +#ifndef MOCKED_LGTD_LIFX_GATEWAY_HANDLE_AMBIENT_LIGHT +void +lgtd_lifx_gateway_handle_ambient_light(struct lgtd_lifx_gateway *gw, + const struct lgtd_lifx_packet_header *hdr, + const struct lgtd_lifx_packet_ambient_light *pkt) +{ + (void)gw; + (void)hdr; + (void)pkt; +} +#endif diff --git a/tests/lifx/mock_wire_proto.h b/tests/lifx/mock_wire_proto.h new file mode 100644 index 0000000..6fff15f --- /dev/null +++ b/tests/lifx/mock_wire_proto.h @@ -0,0 +1,663 @@ +#pragma once + +const union lgtd_lifx_target LGTD_LIFX_UNSPEC_TARGET = { .tags = 0 }; + +#ifndef MOCKED_LGTD_WIRE_SETUP +void +lgtd_lifx_wire_setup(void) +{ +} +#endif + +#ifndef MOCKED_LGTD_LIFX_WIRE_WAVEFORM_STRING_ID_TO_TYPE +enum lgtd_lifx_waveform_type +lgtd_lifx_wire_waveform_string_id_to_type(const char *s, int len) +{ + (void)s; + (void)len; + return LGTD_LIFX_WAVEFORM_INVALID; +} +#endif + +#ifndef MOCKED_LGTD_LIFX_WIRE_ENOSYS_PACKET_HANDLER +void +lgtd_lifx_wire_enosys_packet_handler(struct lgtd_lifx_gateway *gw, + const struct lgtd_lifx_packet_header *hdr, + const void *pkt) +{ + (void)gw; + (void)hdr; + (void)pkt; +} +#endif + +#ifndef MOCKED_LGTD_LIFX_WIRE_NULL_PACKET_ENCODER_DECODER +static void +lgtd_lifx_wire_null_packet_encoder_decoder(void *pkt) +{ + (void)pkt; +} + +static void +lgtd_lifx_wire_null_packet_handler(struct lgtd_lifx_gateway *gw, + const struct lgtd_lifx_packet_header *hdr, + const void *pkt) +{ + (void)gw; + (void)hdr; + (void)pkt; +} + +const struct lgtd_lifx_packet_info * +lgtd_lifx_wire_get_packet_info(enum lgtd_lifx_packet_type packet_type) +{ +#define UNIMPLEMENTED \ + .decode = lgtd_lifx_wire_null_packet_encoder_decoder, \ + .encode = lgtd_lifx_wire_null_packet_encoder_decoder, \ + .handle = lgtd_lifx_wire_null_packet_handler + + static const struct lgtd_lifx_packet_info packet_table[] = { + // Gateway packets: + { + UNIMPLEMENTED, + .name = "GET_PAN_GATEWAY", + .type = LGTD_LIFX_GET_PAN_GATEWAY + }, + { + UNIMPLEMENTED, + .name = "PAN_GATEWAY", + .type = LGTD_LIFX_PAN_GATEWAY, + .size = sizeof(struct lgtd_lifx_packet_pan_gateway) + }, + { + UNIMPLEMENTED, + .name = "SET_TAG_LABELS", + .type = LGTD_LIFX_SET_TAG_LABELS, + .size = sizeof(struct lgtd_lifx_packet_tag_labels), + }, + { + UNIMPLEMENTED, + .name = "GET_TAG_LABELS", + .type = LGTD_LIFX_GET_TAG_LABELS, + .size = sizeof(struct lgtd_lifx_packet_tags), + }, + { + UNIMPLEMENTED, + .name = "TAG_LABELS", + .type = LGTD_LIFX_TAG_LABELS, + .size = sizeof(struct lgtd_lifx_packet_tag_labels) + }, + // Bulb packets: + { + UNIMPLEMENTED, + .name = "SET_LIGHT_COLOR", + .type = LGTD_LIFX_SET_LIGHT_COLOR, + .size = sizeof(struct lgtd_lifx_packet_light_color) + }, + { + UNIMPLEMENTED, + .name = "SET_WAVEFORM", + .type = LGTD_LIFX_SET_WAVEFORM, + .size = sizeof(struct lgtd_lifx_packet_waveform), + }, + { + UNIMPLEMENTED, + .name = "GET_LIGHT_STATUS", + .type = LGTD_LIFX_GET_LIGHT_STATE + }, + { + UNIMPLEMENTED, + .name = "LIGHT_STATUS", + .type = LGTD_LIFX_LIGHT_STATUS, + .size = sizeof(struct lgtd_lifx_packet_light_status), + }, + { + UNIMPLEMENTED, + .size = sizeof(struct lgtd_lifx_packet_power_state), + .name = "SET_POWER_STATE", + .type = LGTD_LIFX_SET_POWER_STATE, + }, + { + UNIMPLEMENTED, + .name = "POWER_STATE", + .type = LGTD_LIFX_POWER_STATE, + .size = sizeof(struct lgtd_lifx_packet_power_state), + }, + { + UNIMPLEMENTED, + .name = "SET_TAGS", + .type = LGTD_LIFX_SET_TAGS, + .size = sizeof(struct lgtd_lifx_packet_tags), + }, + { + UNIMPLEMENTED, + .name = "TAGS", + .type = LGTD_LIFX_TAGS, + .size = sizeof(struct lgtd_lifx_packet_tags), + }, + { + UNIMPLEMENTED, + .name = "GET_MESH_INFO", + .type = LGTD_LIFX_GET_MESH_INFO + }, + { + UNIMPLEMENTED, + .name = "MESH_INFO", + .type = LGTD_LIFX_MESH_INFO, + .size = sizeof(struct lgtd_lifx_packet_ip_state), + }, + { + UNIMPLEMENTED, + .name = "GET_MESH_FIRMWARE", + .type = LGTD_LIFX_GET_MESH_FIRMWARE + }, + { + UNIMPLEMENTED, + .name = "MESH_FIRMWARE", + .type = LGTD_LIFX_MESH_FIRMWARE, + .size = sizeof(struct lgtd_lifx_packet_ip_firmware_info), + }, + { + UNIMPLEMENTED, + .name = "GET_WIFI_INFO", + .type = LGTD_LIFX_GET_WIFI_INFO, + }, + { + UNIMPLEMENTED, + .name = "WIFI_INFO", + .type = LGTD_LIFX_WIFI_INFO, + .size = sizeof(struct lgtd_lifx_packet_ip_state), + }, + { + UNIMPLEMENTED, + .name = "GET_WIFI_FIRMWARE_STATE", + .type = LGTD_LIFX_GET_WIFI_FIRMWARE_STATE + }, + { + UNIMPLEMENTED, + .name = "WIFI_FIRMWARE_STATE", + .type = LGTD_LIFX_WIFI_FIRMWARE_STATE, + .size = sizeof(struct lgtd_lifx_packet_ip_firmware_info), + }, + { + UNIMPLEMENTED, + .name = "GET_VERSION", + .type = LGTD_LIFX_GET_VERSION + }, + { + UNIMPLEMENTED, + .name = "VERSION_STATE", + .type = LGTD_LIFX_VERSION_STATE, + .size = sizeof(struct lgtd_lifx_packet_product_info), + }, + { + UNIMPLEMENTED, + .name = "GET_INFO", + .type = LGTD_LIFX_GET_INFO + }, + { + UNIMPLEMENTED, + .name = "INFO_STATE", + .type = LGTD_LIFX_INFO_STATE, + .size = sizeof(struct lgtd_lifx_packet_runtime_info), + }, + { + UNIMPLEMENTED, + .name = "SET_BULB_LABEL", + .type = LGTD_LIFX_SET_BULB_LABEL, + .size = sizeof(struct lgtd_lifx_packet_label) + }, + { + UNIMPLEMENTED, + .name = "BULB_LABEL", + .type = LGTD_LIFX_BULB_LABEL, + .size = sizeof(struct lgtd_lifx_packet_label), + }, + { + UNIMPLEMENTED, + .name = "GET_AMBIENT_LIGHT", + .type = LGTD_LIFX_GET_AMBIENT_LIGHT + }, + { + UNIMPLEMENTED, + .name = "STATE_AMBIENT_LIGHT", + .type = LGTD_LIFX_STATE_AMBIENT_LIGHT, + .size = sizeof(struct lgtd_lifx_packet_ambient_light), + }, + // Unimplemented but "known" packets + { + UNIMPLEMENTED, + .name = "GET_TIME", + .type = LGTD_LIFX_GET_TIME + }, + { + UNIMPLEMENTED, + .name = "SET_TIME", + .type = LGTD_LIFX_SET_TIME + }, + { + UNIMPLEMENTED, + .name = "TIME_STATE", + .type = LGTD_LIFX_TIME_STATE + }, + { + UNIMPLEMENTED, + .name = "GET_RESET_SWITCH_STATE", + .type = LGTD_LIFX_GET_RESET_SWITCH_STATE + }, + { + UNIMPLEMENTED, + .name = "RESET_SWITCH_STATE", + .type = LGTD_LIFX_RESET_SWITCH_STATE + }, + { + UNIMPLEMENTED, + .name = "GET_DUMMY_PAYLOAD", + .type = LGTD_LIFX_GET_DUMMY_PAYLOAD + }, + { + UNIMPLEMENTED, + .name = "SET_DUMMY_PAYLOAD", + .type = LGTD_LIFX_SET_DUMMY_PAYLOAD + }, + { + UNIMPLEMENTED, + .name = "STATE_DUMMY_PAYLOAD", + .type = LGTD_LIFX_STATE_DUMMY_PAYLOAD + }, + { + UNIMPLEMENTED, + .name = "GET_BULB_LABEL", + .type = LGTD_LIFX_GET_BULB_LABEL + }, + { + UNIMPLEMENTED, + .name = "GET_MCU_RAIL_VOLTAGE", + .type = LGTD_LIFX_GET_MCU_RAIL_VOLTAGE + }, + { + UNIMPLEMENTED, + .name = "MCU_RAIL_VOLTAGE", + .type = LGTD_LIFX_MCU_RAIL_VOLTAGE + }, + { + UNIMPLEMENTED, + .name = "REBOOT", + .type = LGTD_LIFX_REBOOT + }, + { + UNIMPLEMENTED, + .name = "SET_FACTORY_TEST_MODE", + .type = LGTD_LIFX_SET_FACTORY_TEST_MODE + }, + { + UNIMPLEMENTED, + .name = "DISABLE_FACTORY_TEST_MODE", + .type = LGTD_LIFX_DISABLE_FACTORY_TEST_MODE + }, + { + UNIMPLEMENTED, + .name = "STATE_FACTORY_TEST_MODE", + .type = LGTD_LIFX_STATE_FACTORY_TEST_MODE + }, + { + UNIMPLEMENTED, + .name = "STATE_SITE", + .type = LGTD_LIFX_STATE_SITE + }, + { + UNIMPLEMENTED, + .name = "STATE_REBOOT", + .type = LGTD_LIFX_STATE_REBOOT + }, + { + UNIMPLEMENTED, + .name = "SET_PAN_GATEWAY", + .type = LGTD_LIFX_SET_PAN_GATEWAY + }, + { + UNIMPLEMENTED, + .name = "ACK", + .type = LGTD_LIFX_ACK + }, + { + UNIMPLEMENTED, + .name = "SET_FACTORY_RESET", + .type = LGTD_LIFX_SET_FACTORY_RESET + }, + { + UNIMPLEMENTED, + .name = "STATE_FACTORY_RESET", + .type = LGTD_LIFX_STATE_FACTORY_RESET + }, + { + UNIMPLEMENTED, + .name = "GET_LOCATION", + .type = LGTD_LIFX_GET_LOCATION + }, + { + UNIMPLEMENTED, + .name = "SET_LOCATION", + .type = LGTD_LIFX_SET_LOCATION + }, + { + UNIMPLEMENTED, + .name = "STATE_LOCATION", + .type = LGTD_LIFX_STATE_LOCATION + }, + { + UNIMPLEMENTED, + .name = "GET_GROUP", + .type = LGTD_LIFX_GET_GROUP + }, + { + UNIMPLEMENTED, + .name = "SET_GROUP", + .type = LGTD_LIFX_SET_GROUP + }, + { + UNIMPLEMENTED, + .name = "STATE_GROUP", + .type = LGTD_LIFX_STATE_GROUP + }, + { + UNIMPLEMENTED, + .name = "GET_OWNER", + .type = LGTD_LIFX_GET_OWNER + }, + { + UNIMPLEMENTED, + .name = "SET_OWNER", + .type = LGTD_LIFX_SET_OWNER + }, + { + UNIMPLEMENTED, + .name = "STATE_OWNER", + .type = LGTD_LIFX_STATE_OWNER + }, + { + UNIMPLEMENTED, + .name = "GET_FACTORY_TEST_MODE", + .type = LGTD_LIFX_GET_FACTORY_TEST_MODE + }, + { + UNIMPLEMENTED, + .name = "ECHO_REQUEST", + .type = LGTD_LIFX_ECHO_REQUEST + }, + { + UNIMPLEMENTED, + .name = "ECHO_RESPONSE", + .type = LGTD_LIFX_ECHO_RESPONSE + }, + { + UNIMPLEMENTED, + .name = "SET_DIM_ABSOLUTE", + .type = LGTD_LIFX_SET_DIM_ABSOLUTE + }, + { + UNIMPLEMENTED, + .name = "SET_DIM_RELATIVE", + .type = LGTD_LIFX_SET_DIM_RELATIVE + }, + { + UNIMPLEMENTED, + .name = "SET_RGBW", + .type = LGTD_LIFX_SET_RGBW + }, + { + UNIMPLEMENTED, + .name = "GET_RAIL_VOLTAGE", + .type = LGTD_LIFX_GET_RAIL_VOLTAGE + }, + { + UNIMPLEMENTED, + .name = "STATE_RAIL_VOLTAGE", + .type = LGTD_LIFX_STATE_RAIL_VOLTAGE + }, + { + UNIMPLEMENTED, + .name = "GET_TEMPERATURE", + .type = LGTD_LIFX_GET_TEMPERATURE + }, + { + UNIMPLEMENTED, + .name = "STATE_TEMPERATURE", + .type = LGTD_LIFX_STATE_TEMPERATURE + }, + { + UNIMPLEMENTED, + .name = "SET_CALIBRATION_COEFFICIENTS", + .type = LGTD_LIFX_SET_CALIBRATION_COEFFICIENTS + }, + { + UNIMPLEMENTED, + .name = "SET_SIMPLE_EVENT", + .type = LGTD_LIFX_SET_SIMPLE_EVENT + }, + { + UNIMPLEMENTED, + .name = "GET_SIMPLE_EVENT", + .type = LGTD_LIFX_GET_SIMPLE_EVENT + }, + { + UNIMPLEMENTED, + .name = "STATE_SIMPLE_EVENT", + .type = LGTD_LIFX_STATE_SIMPLE_EVENT + }, + { + UNIMPLEMENTED, + .name = "GET_POWER", + .type = LGTD_LIFX_GET_POWER + }, + { + UNIMPLEMENTED, + .name = "SET_POWER", + .type = LGTD_LIFX_SET_POWER + }, + { + UNIMPLEMENTED, + .name = "STATE_POWER", + .type = LGTD_LIFX_STATE_POWER + }, + { + UNIMPLEMENTED, + .name = "SET_WAVEFORM_OPTIONAL", + .type = LGTD_LIFX_SET_WAVEFORM_OPTIONAL + }, + { + UNIMPLEMENTED, + .name = "CONNECT_PLAIN", + .type = LGTD_LIFX_CONNECT_PLAIN + }, + { + UNIMPLEMENTED, + .name = "CONNECT_KEY", + .type = LGTD_LIFX_CONNECT_KEY + }, + { + UNIMPLEMENTED, + .name = "STATE_CONNECT", + .type = LGTD_LIFX_STATE_CONNECT + }, + { + UNIMPLEMENTED, + .name = "GET_AUTH_KEY", + .type = LGTD_LIFX_GET_AUTH_KEY + }, + { + UNIMPLEMENTED, + .name = "SET_AUTH_KEY", + .type = LGTD_LIFX_SET_AUTH_KEY + }, + { + UNIMPLEMENTED, + .name = "STATE_AUTH_KEY", + .type = LGTD_LIFX_STATE_AUTH_KEY + }, + { + UNIMPLEMENTED, + .name = "SET_KEEP_ALIVE", + .type = LGTD_LIFX_SET_KEEP_ALIVE + }, + { + UNIMPLEMENTED, + .name = "STATE_KEEP_ALIVE", + .type = LGTD_LIFX_STATE_KEEP_ALIVE + }, + { + UNIMPLEMENTED, + .name = "SET_HOST", + .type = LGTD_LIFX_SET_HOST + }, + { + UNIMPLEMENTED, + .name = "GET_HOST", + .type = LGTD_LIFX_GET_HOST + }, + { + UNIMPLEMENTED, + .name = "STATE_HOST", + .type = LGTD_LIFX_STATE_HOST + }, + { + UNIMPLEMENTED, + .name = "GET_WIFI_STATE", + .type = LGTD_LIFX_GET_WIFI_STATE + }, + { + UNIMPLEMENTED, + .name = "SET_WIFI_STATE", + .type = LGTD_LIFX_SET_WIFI_STATE + }, + { + UNIMPLEMENTED, + .name = "WIFI_STATE", + .type = LGTD_LIFX_WIFI_STATE + }, + { + UNIMPLEMENTED, + .name = "GET_ACCESS_POINTS", + .type = LGTD_LIFX_GET_ACCESS_POINTS + }, + { + UNIMPLEMENTED, + .name = "SET_ACCESS_POINTS", + .type = LGTD_LIFX_SET_ACCESS_POINTS + }, + { + UNIMPLEMENTED, + .name = "STATE_ACCESS_POINTS", + .type = LGTD_LIFX_STATE_ACCESS_POINTS + }, + { + UNIMPLEMENTED, + .name = "GET_ACCESS_POINT", + .type = LGTD_LIFX_GET_ACCESS_POINT + }, + { + UNIMPLEMENTED, + .name = "STATE_ACCESS_POINT", + .type = LGTD_LIFX_STATE_ACCESS_POINT + }, + { + UNIMPLEMENTED, + .name = "SET_ACCESS_POINT_BROADCAST", + .type = LGTD_LIFX_SET_ACCESS_POINT_BROADCAST + }, + { + UNIMPLEMENTED, + .name = "GET_DIMMER_VOLTAGE", + .type = LGTD_LIFX_GET_DIMMER_VOLTAGE + }, + { + UNIMPLEMENTED, + .name = "STATE_DIMMER_VOLTAGE", + .type = LGTD_LIFX_STATE_DIMMER_VOLTAGE + } + }; + + for (int i = 0; i != sizeof(packet_table) / sizeof(packet_table[0]); i++) { + if (packet_table[i].type == packet_type) { + return &packet_table[i]; + } + } + + return NULL; +} +#endif + +#ifndef MOCKED_LGTD_LIFX_WIRE_HANDLE_RECEIVE +bool +lgtd_lifx_wire_handle_receive(evutil_socket_t socket, + struct lgtd_lifx_gateway *gw) +{ + (void)socket; + (void)gw; + return false; +} +#endif + +#ifndef MOCKED_LGTD_LIFX_WIRE_SETUP_HEADER +const struct lgtd_lifx_packet_info * +lgtd_lifx_wire_setup_header(struct lgtd_lifx_packet_header *hdr, + enum lgtd_lifx_target_type target_type, + union lgtd_lifx_target target, + const uint8_t *site, + enum lgtd_lifx_packet_type packet_type) +{ + (void)target_type; + (void)target; + (void)site; + + const struct lgtd_lifx_packet_info *pkt_info = + lgtd_lifx_wire_get_packet_info(packet_type); + hdr->size = pkt_info->size + sizeof(*hdr); + hdr->packet_type = packet_type; + if (site) { + memcpy(hdr->site, site, sizeof(hdr->site)); + } + + return pkt_info; +} +#endif + +#ifndef MOCKED_LGTD_LIFX_WIRE_DECODE_HEADER +void +lgtd_lifx_wire_decode_header(struct lgtd_lifx_packet_header *hdr) +{ + (void)hdr; +} +#endif + +#ifndef MOCKED_LGTD_LIFX_WIRE_ENCODE_LIGHT_COLOR +void +lgtd_lifx_wire_encode_light_color(struct lgtd_lifx_packet_light_color *pkt) +{ + (void)pkt; +} +#endif + +#ifndef MOCKED_LGTD_LIFX_WIRE_ENCODE_TAG_LABELS +void +lgtd_lifx_wire_encode_tag_labels(struct lgtd_lifx_packet_tag_labels *pkt) +{ + (void)pkt; +} +#endif + +#ifndef MOCKED_LGTD_LIFX_WIRE_ENCODE_TAGS +void +lgtd_lifx_wire_encode_tags(struct lgtd_lifx_packet_tags *pkt) +{ + (void)pkt; +} +#endif + +#ifndef MOCKED_LGTD_LIFX_WIRE_ENCODE_WAVEFORM +void +lgtd_lifx_wire_encode_waveform(struct lgtd_lifx_packet_waveform *pkt) +{ + (void)pkt; +} +#endif diff --git a/tests/lifx/tagging/CMakeLists.txt b/tests/lifx/tagging/CMakeLists.txt new file mode 100644 index 0000000..08fa1c2 --- /dev/null +++ b/tests/lifx/tagging/CMakeLists.txt @@ -0,0 +1,20 @@ +INCLUDE_DIRECTORIES( + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} +) + +ADD_CORE_LIBRARY( + test_lifx_tagging STATIC + ${LIGHTSD_SOURCE_DIR}/core/stats.c + ${LIGHTSD_SOURCE_DIR}/core/utils.c + ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c +) + +FUNCTION(ADD_TAGGING_TEST TEST_SOURCE) + ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_lifx_tagging) +ENDFUNCTION() + +FILE(GLOB TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test_*.c") +FOREACH(TEST ${TESTS}) + ADD_TAGGING_TEST(${TEST}) +ENDFOREACH() diff --git a/tests/lifx/tagging/test_tagging_decref.c b/tests/lifx/tagging/test_tagging_decref.c new file mode 100644 index 0000000..edcd3bc --- /dev/null +++ b/tests/lifx/tagging/test_tagging_decref.c @@ -0,0 +1,69 @@ +#include "tagging.c" + +#include "mock_log.h" + +static int +count_tag(const char *tag_label) +{ + int count = 0; + struct lgtd_lifx_tag *tag; + LIST_FOREACH(tag, &lgtd_lifx_tags, link) { + if (!strcmp(tag_label, tag->label)) { + count++; + } + } + return count; +} + +static int +count_site(struct lgtd_lifx_site_list *list, const struct lgtd_lifx_gateway *gw) +{ + int count = 0; + struct lgtd_lifx_site *site; + LIST_FOREACH(site, list, link) { + if (site->gw == gw) { + count++; + } + } + return count; +} + +int +main(void) +{ + struct lgtd_lifx_gateway gw1, gw2; + memset(&gw1, 0, sizeof(gw1)); + memset(&gw2, 0, sizeof(gw2)); + + struct lgtd_lifx_site *site_gw1 = calloc(1, sizeof(*site_gw1)); + site_gw1->gw = &gw1; + struct lgtd_lifx_site *site_gw2 = calloc(1, sizeof(*site_gw2)); + site_gw2->gw = &gw2; + + const char *rawr = "rawr"; + struct lgtd_lifx_tag *tag = calloc(1, sizeof(*tag)); + strcpy(tag->label, rawr); + LIST_INSERT_HEAD(&tag->sites, site_gw1, link); + LIST_INSERT_HEAD(&tag->sites, site_gw2, link); + LIST_INSERT_HEAD(&lgtd_lifx_tags, tag, link); + + for (int i = 0; i != 2; i++) { + lgtd_lifx_tagging_decref(tag, &gw2); + if (count_site(&tag->sites, &gw2) != 0) { + errx(1, "gw2 shouldn't be in the sites list"); + } + if (count_site(&tag->sites, &gw1) != 1) { + errx(1, "gw1 wasn't found once in the sites list"); + } + if (count_tag(rawr) != 1) { + errx(1, "%s wasn't found once in the tags list", rawr); + } + } + + lgtd_lifx_tagging_decref(tag, &gw1); + if (count_tag(rawr)) { + errx(1, "the tags list should be empty"); + } + + return 0; +} diff --git a/tests/lifx/tagging/test_tagging_incref.c b/tests/lifx/tagging/test_tagging_incref.c new file mode 100644 index 0000000..889a480 --- /dev/null +++ b/tests/lifx/tagging/test_tagging_incref.c @@ -0,0 +1,76 @@ +#include "tagging.c" + +#include "mock_log.h" + +static int +count_tag(const char *tag_label) +{ + int count = 0; + struct lgtd_lifx_tag *tag; + LIST_FOREACH(tag, &lgtd_lifx_tags, link) { + if (!strcmp(tag_label, tag->label)) { + count++; + } + } + return count; +} + +static int +count_site(struct lgtd_lifx_site_list *list, const struct lgtd_lifx_gateway *gw) +{ + int count = 0; + struct lgtd_lifx_site *site; + LIST_FOREACH(site, list, link) { + if (site->gw == gw) { + count++; + } + } + return count; +} + +int +main(void) +{ + struct lgtd_lifx_gateway gw1, gw2; + memset(&gw1, 0, sizeof(gw1)); + memset(&gw2, 0, sizeof(gw2)); + + const char *rawr = "rawr"; + const char *awww = "awww"; + + for (int i = 0; i != 2; i++) { + lgtd_lifx_tagging_incref(rawr, &gw1, 1); + if (count_tag(rawr) != 1) { + errx(1, "%s wasn't found once the list of tags", rawr); + } + if (count_site(&LIST_FIRST(&lgtd_lifx_tags)->sites, &gw1) != 1) { + errx(1, "site %p wasn't found once in the list of sites", &gw1); + } + } + + lgtd_lifx_tagging_incref(rawr, &gw2, 1); + struct lgtd_lifx_tag *tag = lgtd_lifx_tagging_find_tag(rawr); + if (count_site(&tag->sites, &gw2) != 1) { + errx(1, "gw2 wasn't found once in the sites of tag %s", tag->label); + } + + lgtd_lifx_tagging_incref(awww, &gw1, 1); + if (count_tag(awww) != 1) { + errx(1, "%s wasn't found once in the list of tags", awww); + } + tag = lgtd_lifx_tagging_find_tag(awww); + if (count_site(&tag->sites, &gw1) != 1) { + errx(1, "gw1 wasn't found once in the sites of tag %s", awww); + } + + LIST_FOREACH(tag, &lgtd_lifx_tags, link) { + struct lgtd_lifx_site *site; + LIST_FOREACH(site, &tag->sites, link) { + if (site->tag_id != 1) { + lgtd_errx(1, "site->tag_id = %d (expected 1)", site->tag_id); + } + } + } + + return 0; +} diff --git a/tests/lifx/tests_shims.c b/tests/lifx/tests_shims.c new file mode 100644 index 0000000..49e5d91 --- /dev/null +++ b/tests/lifx/tests_shims.c @@ -0,0 +1,37 @@ +#include +#include +#include +#include +#include +#include + +#include "core/lightsd.h" + +struct lgtd_opts lgtd_opts = { + .foreground = false, + .log_timestamps = false, + .verbosity = LGTD_DEBUG +}; + +struct event_base *lgtd_ev_base = NULL; + +const int LGTD_LIFX_DEBRUIJN_SEQUENCE[64] = { + 0, 47, 1, 56, 48, 27, 2, 60, + 57, 49, 41, 37, 28, 16, 3, 61, + 54, 58, 35, 52, 50, 42, 21, 44, + 38, 32, 29, 23, 17, 11, 4, 62, + 46, 55, 26, 59, 40, 36, 15, 53, + 34, 51, 20, 43, 31, 22, 10, 45, + 25, 39, 14, 33, 19, 30, 9, 24, + 13, 18, 8, 12, 7, 6, 5, 63 +}; + +void +lgtd_cleanup(void) +{ +} + +void +lgtd_daemon_update_proctitle(void) +{ +} diff --git a/tests/lifx/wire_proto/CMakeLists.txt b/tests/lifx/wire_proto/CMakeLists.txt new file mode 100644 index 0000000..456874f --- /dev/null +++ b/tests/lifx/wire_proto/CMakeLists.txt @@ -0,0 +1,18 @@ +INCLUDE_DIRECTORIES( + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} +) + +ADD_CORE_LIBRARY( + test_lifx_wire_proto_core STATIC + ${LIGHTSD_SOURCE_DIR}/core/utils.c +) + +FUNCTION(ADD_WIRE_PROTO_TEST TEST_SOURCE) + ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_lifx_wire_proto_core) +ENDFUNCTION() + +FILE(GLOB TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test_*.c") +FOREACH(TEST ${TESTS}) + ADD_WIRE_PROTO_TEST(${TEST}) +ENDFOREACH() diff --git a/tests/lifx/wire_proto/test_wire_proto_encode_decode_header.c b/tests/lifx/wire_proto/test_wire_proto_encode_decode_header.c new file mode 100644 index 0000000..3babdd2 --- /dev/null +++ b/tests/lifx/wire_proto/test_wire_proto_encode_decode_header.c @@ -0,0 +1,132 @@ +#include + +#include "wire_proto.c" + +#include "mock_daemon.h" +#include "mock_gateway.h" +#include "mock_log.h" + +int +main(void) +{ + struct lgtd_lifx_packet_header hdr = { + .size = 42, + .target = { .tags = 0xbad }, + .packet_type = LGTD_LIFX_ECHO_REQUEST + }; + lgtd_lifx_wire_encode_header(&hdr, LGTD_LIFX_ADDRESSABLE|LGTD_LIFX_TAGGED); + + if (htobe16(hdr.protocol) != 0x34) { + lgtd_errx(1, "protocol = %#hx (expected = 0x34)", hdr.protocol); + } + if (le16toh(hdr.size) != 42) { + lgtd_errx(1, "size = %hu (expected = 42)", le16toh(hdr.size)); + } + if (le64toh(hdr.target.tags) != 0xbad) { + lgtd_errx( + 1, "tags = %#jx (expected = 0xbad)", + (uintmax_t)le64toh(hdr.target.tags) + ); + } + if (le16toh(hdr.packet_type) != LGTD_LIFX_ECHO_REQUEST) { + lgtd_errx( + 1, "packet_type = %hx (expected = %#x)", + le16toh(hdr.packet_type), LGTD_LIFX_ECHO_REQUEST + ); + } + + lgtd_lifx_wire_decode_header(&hdr); + + int proto_version = hdr.protocol & LGTD_LIFX_PROTOCOL_VERSION_MASK; + if (proto_version != LGTD_LIFX_PROTOCOL_V1) { + lgtd_errx( + 1, "protocol version = %d (expected %d)", + proto_version, LGTD_LIFX_PROTOCOL_V1 + ); + } + if (!(hdr.protocol & LGTD_LIFX_PROTOCOL_ADDRESSABLE)) { + lgtd_errx(1, "the protocol addressable bit should be set"); + } + if (!(hdr.protocol & LGTD_LIFX_PROTOCOL_TAGGED)) { + lgtd_errx(1, "the protocol tagged bit should be set"); + } + if (hdr.size != 42) { + lgtd_errx(1, "size = %hu (expected = 42)", hdr.size); + } + if (hdr.target.tags != 0xbad) { + lgtd_errx( + 1, "tags = %#jx (expected = 0xbad)", (uintmax_t)hdr.target.tags + ); + } + if (hdr.packet_type != LGTD_LIFX_ECHO_REQUEST) { + lgtd_errx( + 1, "packet_type = %hx (expected = %#x)", + hdr.packet_type, LGTD_LIFX_ECHO_REQUEST + ); + } + + memset(&hdr, 0, sizeof(hdr)); + hdr.size = 42; + hdr.target.device_addr[2] = 44; + hdr.packet_type = LGTD_LIFX_ECHO_REQUEST; + lgtd_lifx_wire_encode_header(&hdr, LGTD_LIFX_ADDRESSABLE); + + if (htobe16(hdr.protocol) != 0x14) { + lgtd_errx(1, "protocol = %#hx (expected = 0x14)", hdr.protocol); + } + if (le16toh(hdr.size) != 42) { + lgtd_errx(1, "size = %hu (expected = 42)", le16toh(hdr.size)); + } + uint8_t expected_addr[LGTD_LIFX_ADDR_LENGTH] = { + 0, 0, 44, 0, 0, 0 + }; + char expected_addr_buf[LGTD_LIFX_ADDR_STRLEN]; + char dev_addr[LGTD_LIFX_ADDR_STRLEN]; + if (memcmp(hdr.target.device_addr, expected_addr, LGTD_LIFX_ADDR_LENGTH)) { + lgtd_errx( + 1, "device addr = %s (expected = %s)", + LGTD_IEEE8023MACTOA(hdr.target.device_addr, dev_addr), + LGTD_IEEE8023MACTOA(expected_addr, expected_addr_buf) + ); + } + if (le16toh(hdr.packet_type) != LGTD_LIFX_ECHO_REQUEST) { + lgtd_errx( + 1, "packet_type = %#hx (expected = %#x)", + le16toh(hdr.packet_type), LGTD_LIFX_ECHO_REQUEST + ); + } + + lgtd_lifx_wire_decode_header(&hdr); + + proto_version = hdr.protocol & LGTD_LIFX_PROTOCOL_VERSION_MASK; + if (proto_version != LGTD_LIFX_PROTOCOL_V1) { + lgtd_errx( + 1, "protocol version = %d (expected %d)", + proto_version, LGTD_LIFX_PROTOCOL_V1 + ); + } + if (!(hdr.protocol & LGTD_LIFX_PROTOCOL_ADDRESSABLE)) { + lgtd_errx(1, "the protocol addressable bit should be set"); + } + if (hdr.protocol & LGTD_LIFX_PROTOCOL_TAGGED) { + lgtd_errx(1, "the protocol tagged bit should not be set"); + } + if (memcmp(hdr.target.device_addr, expected_addr, LGTD_LIFX_ADDR_LENGTH)) { + lgtd_errx( + 1, "device addr = %s (expected = %s)", + LGTD_IEEE8023MACTOA(hdr.target.device_addr, dev_addr), + LGTD_IEEE8023MACTOA(expected_addr, expected_addr_buf) + ); + } + if (hdr.size != 42) { + lgtd_errx(1, "size = %hu (expected = 42)", hdr.size); + } + if (hdr.packet_type != LGTD_LIFX_ECHO_REQUEST) { + lgtd_errx( + 1, "packet_type = %#hx (expected = %#x)", + hdr.packet_type, LGTD_LIFX_ECHO_REQUEST + ); + } + + return 0; +} diff --git a/tests/lifx/wire_proto/test_wire_proto_encode_light_color.c b/tests/lifx/wire_proto/test_wire_proto_encode_light_color.c new file mode 100644 index 0000000..4239e6d --- /dev/null +++ b/tests/lifx/wire_proto/test_wire_proto_encode_light_color.c @@ -0,0 +1,43 @@ +#include + +#include "wire_proto.c" +#include "mock_daemon.h" +#include "mock_gateway.h" +#include "mock_log.h" + +int +main(void) +{ + struct lgtd_lifx_packet_light_color pkt = { + .hue = 42, + .saturation = 10000, + .brightness = 20000, + .kelvin = 4500, + .transition = 150 + }; + + lgtd_lifx_wire_encode_light_color(&pkt); + + int hue = le16toh(pkt.hue); + int saturation = le16toh(pkt.saturation); + int brightness = le16toh(pkt.brightness); + int kelvin = le16toh(pkt.kelvin); + int transition = htole32(pkt.transition); + if (hue != 42) { + errx(1, "got hue = %d (expected 42)", hue); + } + if (saturation != 10000) { + errx(1, "got saturation = %d (expected 10000)", saturation); + } + if (brightness != 20000) { + errx(1, "got brightness = %d (expected 20000)", brightness); + } + if (kelvin != 4500) { + errx(1, "got kelvin = %d (expected 4500)", kelvin); + } + if (transition != 150) { + errx(1, "got transition = %d (expected 150)", transition); + } + + return 0; +} diff --git a/tests/lifx/wire_proto/test_wire_proto_encode_tag_labels.c b/tests/lifx/wire_proto/test_wire_proto_encode_tag_labels.c new file mode 100644 index 0000000..a53451b --- /dev/null +++ b/tests/lifx/wire_proto/test_wire_proto_encode_tag_labels.c @@ -0,0 +1,29 @@ +#include + +#include "wire_proto.c" +#include "mock_daemon.h" +#include "mock_gateway.h" +#include "mock_log.h" + +int +main(void) +{ + struct lgtd_lifx_packet_tag_labels pkt = { .tags = 0x2a, .label = "42" }; + + lgtd_lifx_wire_encode_tag_labels(&pkt); + + if (pkt.tags != le64toh(0x2a)) { + errx( + 1, "got tags = %#jx (expected %#jx)", + (uintmax_t)pkt.tags, (uintmax_t)le64toh(0x2a) + ); + } + if (strcmp(pkt.label, "42")) { + errx( + 1, "got label = %.*s (expected 42)", + (int)sizeof(pkt.label), pkt.label + ); + } + + return 0; +} diff --git a/tests/lifx/wire_proto/test_wire_proto_encode_tags.c b/tests/lifx/wire_proto/test_wire_proto_encode_tags.c new file mode 100644 index 0000000..6d979f5 --- /dev/null +++ b/tests/lifx/wire_proto/test_wire_proto_encode_tags.c @@ -0,0 +1,23 @@ +#include + +#include "wire_proto.c" +#include "mock_daemon.h" +#include "mock_gateway.h" +#include "mock_log.h" + +int +main(void) +{ + struct lgtd_lifx_packet_tags pkt = { .tags = 0x2a }; + + lgtd_lifx_wire_encode_tags(&pkt); + + if (pkt.tags != le64toh(0x2a)) { + errx( + 1, "got tags = %#jx (expected 0x2a)", + (uintmax_t)le64toh(pkt.tags) + ); + } + + return 0; +} diff --git a/tests/lifx/wire_proto/test_wire_proto_encode_waveform.c b/tests/lifx/wire_proto/test_wire_proto_encode_waveform.c new file mode 100644 index 0000000..495ac89 --- /dev/null +++ b/tests/lifx/wire_proto/test_wire_proto_encode_waveform.c @@ -0,0 +1,53 @@ +#include + +#include "wire_proto.c" +#include "mock_daemon.h" +#include "mock_gateway.h" +#include "mock_log.h" + +int +main(void) +{ + struct lgtd_lifx_packet_waveform pkt = { + .hue = 42, + .saturation = 10000, + .brightness = 20000, + .kelvin = 4500, + .period = 200, + .cycles = 10., + .skew_ratio = 5000 + }; + + lgtd_lifx_wire_encode_waveform(&pkt); + + int hue = le16toh(pkt.hue); + int saturation = le16toh(pkt.saturation); + int brightness = le16toh(pkt.brightness); + int kelvin = le16toh(pkt.kelvin); + int period = le32toh(pkt.period); + float cycles = lgtd_lifx_wire_lefloattoh(pkt.cycles); + int skew_ratio = le16toh(pkt.skew_ratio); + if (hue != 42) { + errx(1, "got hue = %d (expected 42)", hue); + } + if (saturation != 10000) { + errx(1, "got saturation = %d (expected 10000)", saturation); + } + if (brightness != 20000) { + errx(1, "got brightness = %d (expected 20000)", brightness); + } + if (kelvin != 4500) { + errx(1, "got kelvin = %d (expected 4500)", kelvin); + } + if (period != 200) { + errx(1, "got period = %d (expected 200)", period); + } + if (cycles != 10.) { + errx(1, "got cycles = %f (expected 10)", cycles); + } + if (skew_ratio != 5000) { + errx(1, "got skew_ratio = %d (expected 5000)", skew_ratio); + } + + return 0; +} diff --git a/tests/lifx/wire_proto/test_wire_proto_float_endian_conversion.c b/tests/lifx/wire_proto/test_wire_proto_float_endian_conversion.c new file mode 100644 index 0000000..d0c01ef --- /dev/null +++ b/tests/lifx/wire_proto/test_wire_proto_float_endian_conversion.c @@ -0,0 +1,17 @@ +#include +#include +#include +#include + +#include + +#include "lifx/wire_proto.h" + +int +main(void) +{ + union u { float f; uint32_t i; }; + union u value = { .i = 0x11223344 }; + union u new_value = { .f = lgtd_lifx_wire_htolefloat(value.f) }; + return new_value.i != 0x44332211; +} diff --git a/tests/lifx/wire_proto/test_wire_proto_waveform_table.c b/tests/lifx/wire_proto/test_wire_proto_waveform_table.c new file mode 100644 index 0000000..a04dc1e --- /dev/null +++ b/tests/lifx/wire_proto/test_wire_proto_waveform_table.c @@ -0,0 +1,59 @@ +#include "wire_proto.c" + +#include "mock_daemon.h" +#include "mock_gateway.h" +#include "mock_log.h" + +int +main(void) +{ + enum lgtd_lifx_waveform_type rv = LGTD_LIFX_WAVEFORM_INVALID; + + rv = lgtd_lifx_wire_waveform_string_id_to_type("SAW", 3); + if (rv != LGTD_LIFX_WAVEFORM_SAW) { + errx(1, "Expected WAVEFORM_SAW"); + } + + rv = lgtd_lifx_wire_waveform_string_id_to_type("SINE", 4); + if (rv != LGTD_LIFX_WAVEFORM_SINE) { + errx(1, "Expected WAVEFORM_SINE"); + } + + rv = lgtd_lifx_wire_waveform_string_id_to_type("HALF_SINE", 9); + if (rv != LGTD_LIFX_WAVEFORM_HALF_SINE) { + errx(1, "Expected WAVEFORM_HALF_SINE"); + } + + rv = lgtd_lifx_wire_waveform_string_id_to_type("TRIANGLE", 8); + if (rv != LGTD_LIFX_WAVEFORM_TRIANGLE) { + errx(1, "Expected WAVEFORM_TRIANGLE"); + } + + rv = lgtd_lifx_wire_waveform_string_id_to_type("SQUARE", 6); + if (rv != LGTD_LIFX_WAVEFORM_SQUARE) { + errx(1, "Expected WAVEFORM_SQUARE"); + } + + rv = lgtd_lifx_wire_waveform_string_id_to_type("TEST", 4); + if (rv != LGTD_LIFX_WAVEFORM_INVALID) { + errx(1, "Expected WAVEFORM_INVALID"); + } + + rv = lgtd_lifx_wire_waveform_string_id_to_type("", 0); + if (rv != LGTD_LIFX_WAVEFORM_INVALID) { + errx(1, "Expected WAVEFORM_INVALID"); + } + + int cmp = strcmp( + lgtd_lifx_waveform_table[LGTD_LIFX_WAVEFORM_INVALID].str, + "INVALID" + ); + if (cmp) { + errx( + 1, "Expected INVALID got %s", + lgtd_lifx_waveform_table[LGTD_LIFX_WAVEFORM_INVALID].str + ); + } + + return 0; +}